Files
settled-reach/docs/sprints/sprint-24/server.md
T
jpmschweitzerandClaude Opus 4.6 e8263e209e refactor(data): rename Krenn to Van Maanen's Star
System S-057 assigned to real star GJ 35 (Van Maanen's Star, DG white
dwarf at 13.9 ly). Renamed across all content, server code, docs,
decisions, wiki lore, and config files. 224 files updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 00:23:26 +01:00

17 KiB

Sprint 24: Signal — Server Tasks

Goal: Wire the storyteller's activation event into player-visible consequences, thread character archetype through the full session lifecycle, and deliver the first unscripted end-to-end v0.1 playthrough — from main menu to triangle activation.

Branch: server Agents: Dudley (simulation), Tyre (architecture), Hoshe (QA)

This is the capstone sprint for v0.1. Everything server-side must converge on a production playthrough: correct character spawned, opening monologue fired, storyteller active, triangle escalation observable. Sprint 25 is playtest. There is no Sprint 26 before v0.1 ships.


New Tickets

# Title Blocked by
#587 Character archetype — server: accept archetype in StartupMessage, spawn correct PC
#589 Triangle activation consumer — server: behavioral tell escalation on TriangleActivated
#591 News ticker — server: load ticker YAML, emit current headline in snapshot
#595 Opening monologue trigger — server: emit opening lines at session start based on archetype #587
#593 v0.1 playthrough proof: full session from main menu through storyteller activation #587, #589, #591, #595 (+ client #588, #590, #592 + copy #597)
#594 Tile data model design — produce D-record for extensible tile properties — (design only, unblocks post-v0.1)

Use tooling/db/ticket show <id> for full details.


Key Decisions

  • decisions/scope.md — D-027 (vertical slice success criteria — the 4 tests the playthrough must satisfy), D-039 (6 wow moments — opening arrival, character's eye, FRIEND contradiction, divergence reveal, news ticker gut-punch, quiet moment)
  • decisions/content.md — D-032 (separate monologue pools: character tag is a hard partition, not a filter), D-035 (tag taxonomy: trigger, character, prerequisite fields on monologue lines), D-023 (three-tier content model — storyteller activates Tier 1 modules based on engagement)
  • decisions/content.md — D-024 (NPC 10-axis model — tell system axis 9; tells flow from simulation state not authored), D-036 (Sova Transit District — The Terminal, The Last Shift, news ticker content at ticker/the-last-shift.yaml)
  • decisions/architecture.md — D-020 (StartupMessage is the only initialization crossing IPC; PROTOCOL_VERSION gates wire compatibility), D-041 (knowledge graph data model — KnowledgeConfidence confidence levels), D-010 principle 3 (no player identity baked into game loop — archetype is a configuration, not a special case)

Open Questions to Resolve Early

  • Q-052: Storyteller hint delivery channels — #589 implements channels 1 (behavioral tell escalation) and 7 (proximity monologue, via copy #597). Environmental change (channel 2) and overheard NPC conversation (channel 4) are deferred to Sprint 25. This sprint resolves Q-052 at v0.1 scope. No design discussion needed — the channel inventory is decided; implementation scope is constrained to what's achievable this sprint.

Notes

#587 — Character archetype in StartupMessage

What exists: StartupMessage in server/src/bridge/types.rs has one field: pub world_seed: u64. The production startup path initializes all NPCs and spawns the player entity without any archetype selection — the player entity carries CharacterArchetype::Detective by default (see server/src/simulation/monologue.rs line ~174: character: "detective".to_string()). CharacterArchetype enum is already defined in bridge/types.rs with Smuggler and Detective variants (used by phase-2 verb filter in server/src/perception/observer/mod.rs).

What to deliver:

  1. Add pub character_archetype: CharacterArchetype to StartupMessage. Default to Detective if absent during deserialization (backward-compatible via #[serde(default)]).

  2. In the production startup path (server/src/main.rs or server/src/content/spawn.rs), read startup_msg.character_archetype and insert the correct CharacterArchetype component on the player entity. The player entity is currently spawned without an archetype component — insert it here.

  3. In server/src/simulation/monologue.rs, read the CharacterArchetype component from the player entity at MonologueState initialization (or on first tick) and set MonologueState.character from it: "smuggler" or "detective". This is the string that gates all monologue pool selection.

  4. Bump PROTOCOL_VERSION to 19. The new field in StartupMessage is a breaking change — old clients send a message that omits character_archetype, new server will default it correctly, but old servers receiving the new format will fail. Coordinate with Stig (#588) on timing.

Gotcha: CharacterArchetype already serializes via serde. The existing impl From<CharacterArchetype> for ObserverSnapshot path in bridge tests validates round-trips — make sure the new StartupMessage test also covers the default case (Smuggler serializes and round-trips; missing field deserializes as Detective).

Unblocks: #595 (opening monologue), #588 (client select screen can now send the archetype).


#589 — Triangle activation consumer: behavioral tell escalation

What exists: server/src/npc/tell_state.rsTellCategory enum (Nervous, Angry, Friendly, Guarded, RoutineDeviation) and derive_tell_state() function. Tell state is derived from NPC axis values each tick and emitted in ObserverSnapshot.entities[].tell_state. RoutineDeviation component exists. The storyteller emits TriangleActivated { triangle_id, npc_entity } into TriangleActivatedQueue.

What to deliver: A system escalate_tells_on_activation() that:

  1. Drains TriangleActivatedQueue each tick (non-destructively — queue must still be readable by other consumers; use events() pattern or check if existing drain is appropriate).

  2. For each TriangleActivatedEvent, find the anchor NPC entity (event.npc_entity) and the 1-2 NPCs who are in the same triangle (via TriangleState query). These are the triangle's NPCs.

  3. Insert a RoutineDeviation component on all triangle NPCs. RoutineDeviation is the strongest tell category per derive_tell_state() priority order — it overrides Nervous/Angry/Guarded. This makes the triangle NPCs immediately observable as anomalous.

  4. The RoutineDeviation component should carry a expires_at_tick: u64 field (if not already present) so it can be removed after a configurable window (suggest TELL_ESCALATION_DURATION_TICKS = 300 = 30 game-minutes). Add a system to remove expired RoutineDeviation components.

Why this is the right channel: D-024 axis 9 (tell system) is a simulation output, not authored content. The RoutineDeviation tell fires when the NPC is off their usual schedule — which is exactly true post-activation (the triangle is hot). No new content required. Client sees it as tell_state: RoutineDeviation on the visible entity. Copy (#597) authors the monologue lines the client fires when the player observes this tell.

Integration point: TriangleActivatedQueue is in server/src/storyteller/mod.rs. The new system should live in server/src/storyteller/ or server/src/simulation/pressure.rs — either is appropriate. Register in StorytellerPlugin.build().

Test: cargo test — verify that after TriangleActivatedQueue is populated with a test event, triangle NPCs have RoutineDeviation inserted and derive_tell_state() returns RoutineDeviation for them.


#591 — News ticker: server-side

What exists: content/campaigns/main/systems/van-maanens-star/stations/sova/districts/transit/ticker/the-last-shift.yaml — 30 authored headlines (freight, politics, infrastructure, sports, commission, community categories). The YAML is fully authored (Sprint 12, #306). It is NOT loaded at runtime — the content loader (server/src/content/loader.rs) does not parse ticker files. ObserverSnapshotWire in server/src/bridge/types.rs has no ticker field.

What to deliver:

  1. TickerLine struct: id: String, text: String, category: String. Add to bridge/types.rs.

  2. Add current_ticker: Option<TickerLine> to ObserverSnapshotWire. Only populated when player is in The Last Shift zone (zone_id "bar" — check server/src/simulation/zone.rs). None in all other zones.

  3. TickerPool resource: loads ticker/the-last-shift.yaml at startup via the content loader. Holds all 30 headlines. Uses SimRng to advance to a new headline every TICKER_ROTATION_TICKS = 200 ticks (20 game-minutes). Deterministic under D-010 — always use SimRng, never system randomness.

  4. In the observer snapshot system (server/src/perception/observer/mod.rs), read TickerPool and populate current_ticker when player zone is "bar".

Gotcha: The ticker rotation must use SimRng (the seeded bevy resource), not rand::thread_rng(). D-010 principle 4 — deterministic simulation. A ticker rotation driven by thread_rng() would produce different headlines on replay, breaking golden-file tests.

Content note: The ticker content file at ticker/the-last-shift.yaml uses a dual_lens field per headline (separate monologue notes for smuggler vs detective perspective). These notes are for copy authoring reference only — do NOT include them in the TickerLine wire struct. The text field is what crosses the boundary; dual_lens is authoring metadata.


#595 — Opening monologue trigger

What exists: server/src/simulation/monologue.rsMonologueState has pub character: String (initialized as "detective") and pub enter_location_fired: bool. The system tick_monologue() calls select_monologue_line() which filters pools by pool.character != character — so the character string gate already works. Opening monologue content is fully authored at:

  • content/campaigns/main/.../monologue/detective/opening.yaml (12 lines, Sprint 12 #300)
  • content/campaigns/main/.../monologue/smuggler/opening.yaml (Sprint 12 #299)

Both files use trigger: enter_location and situation: [arrival, shift_start]. The enter_location trigger fires on the first tick (MonologueState.enter_location_fired = false → fires → sets true).

What to deliver: This ticket is small because the infrastructure is almost complete. The only missing piece is that MonologueState.character is initialized as "detective" before the archetype is known. Fix:

  1. After #587 lands (archetype inserted on player entity), read CharacterArchetype in MonologuePlugin.build() or the first-tick setup system and set MonologueState.character correctly: CharacterArchetype::Smuggler → "smuggler", CharacterArchetype::Detective → "detective".

  2. Verify that select_monologue_line() with trigger = "enter_location" and character = "smuggler" correctly selects from opening.yaml in the smuggler subdirectory. Run the existing monologue integration test with a smuggler archetype — it should already pass once #587 sets the character string.

  3. Add a regression test: spawn two sessions (smuggler + detective), advance 1 tick each, assert different character on MonologueState. This verifies the archetype flows end-to-end.

Blocked by: #587 (needs CharacterArchetype on player entity before this system can read it).


#594 — Tile data model design (D-record)

What exists: Current tile format is single characters (F/W/V/R) in string arrays in location YAML files. This cannot represent per-tile properties (door access lists, container contents, damage state, visual variant, sound properties, trigger zones). Epic #586 tracks this. This ticket produces the design only — no migration, no implementation.

What to deliver: A filed D-record in decisions/architecture.md via tooling/db/decision claim D architecture "Tile data model — extensible per-tile properties" before writing. The D-record must:

  1. Survey what tile-level data the game needs across systems (doors, containers, damage, visual variants, trigger zones, material properties).
  2. Choose between: (a) tile palette/registry (tiles are typed by ID, properties on the type), (b) per-tile property bags (each tile can have arbitrary key-value), (c) ECS-style tile components (tiles are entities), or (d) hybrid.
  3. Specify the YAML authoring format (human-writable, survives merge conflicts), the loader contract (how the server parses it), and the runtime representation (what ECS queries use).
  4. Estimate migration effort for the 5 existing location YAMLs.

Scope: This is design work only. No code changes. The D-record is the deliverable. Tyre should author it; Dudley reviews for implementation feasibility. File via the standard decision workflow (decision claim → edit decisions/architecture.md → commit with pre-commit hook running decisions-sync).


#593 — v0.1 playthrough proof

What exists: By the time this ticket starts, all upstream tickets are merged: archetype in StartupMessage (#587), tell escalation (#589), ticker in snapshot (#591), opening monologue archetype-gated (#595), client character select (#588), client triangle consumer (#590), client ticker (#592), copy activation monologue lines (#597).

What to deliver: This is an integration proof ticket, not an implementation ticket. Deliverable is a written test plan execution + green CI.

  1. Full playthrough test (manual): Boot server in production mode (no --gauntlet). Boot client in production mode (no SR_TEST=1). From main menu, click "New Game." Character select screen appears — select Smuggler. Game scene loads. Confirm: opening monologue fires (Smuggler voice). Walk to The Terminal. Observe Kael for ~3 game-minutes (check EngagementRecord via debug console npc <id>). Use debug console contaminate to skip to contamination phase. Wait for activation pass — triangles command shows one triangle in Active phase. Walk back to Kael — tell_state is RoutineDeviation. Proximity monologue fires (from copy #597). Walk to The Last Shift — news ticker visible. CI green.

  2. Automated integration test (server): Add test_v0_1_integration_playthrough in server/tests/. Uses the existing test-client infrastructure (tooling/test-client): boot server, send StartupMessage { world_seed: 12345, character_archetype: Smuggler }, advance 1 tick, assert snapshot contains smuggler opening monologue, advance to tick 3000, send SkipToContamination debug command, advance 10 more ticks, assert TriangleActivatedQueue is non-empty in server state (via golden snapshot comparison).

  3. PR merge coordination: Server #593 and client #588/#590/#592 must all be green on their respective branches before this ticket can be filed as done. The playthrough proof is the collective gate for the sprint.

Gotcha — PROTOCOL_VERSION: #587 bumps to 19. Client #588 must update Protocol.PROTOCOL_VERSION to 19 simultaneously. The version mismatch is a connection crash. Coordinate with Stig on merge timing.


Dependency Chain

#587 (archetype in StartupMessage) ─────────────────────────────────────────────────┐
  └→ #595 (opening monologue archetype-gated)                                       │
  └→ #588 (client: character select screen)                      ← client work      │
                                                                                     ↓
#589 (tell escalation on TriangleActivated) ─────────────────────────────────────── #593
  └→ #590 (client: triangle consumer)                            ← client work       (v0.1
                                                                                  playthrough
#591 (news ticker in snapshot) ─────────────────────────────────────────────────── proof)
  └→ #592 (client: ticker HUD)                                   ← client work      │
                                                                                     │
#597 (copy: activation monologue lines)                          ← copy work ────────┘

#594 (tile data model D-record)  ← standalone design track, no v0.1 dependency

Parallel server tracks: #587, #589, #591, #594 are all independent — start all simultaneously. #595 blocked on #587.


PR Workflow

tea pr create --repo jpmschweitzer/settled-reach --login schweitz \
  --title "feat(server): character archetype in StartupMessage and monologue gating" \
  --description "body" --base main --head server

Sprint Completion (Server Criteria)

  1. cargo test green on server branch. All existing tests pass. New tests for archetype round-trip and tell escalation pass.
  2. StartupMessage with character_archetype: Smuggler produces a smuggler player entity with MonologueState.character = "smuggler" on tick 1.
  3. After TriangleActivated fires, triangle NPCs have RoutineDeviation inserted and tell_state: RoutineDeviation appears in the snapshot.
  4. Snapshot contains current_ticker (non-null headline) when player is in "bar" zone.
  5. Opening monologue (enter_location) fires on tick 1 with lines from the correct character pool.
  6. Integration test test_v0_1_integration_playthrough passes end-to-end.
  7. PROTOCOL_VERSION = 19 — matches client.