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>
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:charactertag is a hard partition, not a filter), D-035 (tag taxonomy:trigger,character,prerequisitefields 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 atticker/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:
-
Add
pub character_archetype: CharacterArchetypetoStartupMessage. Default toDetectiveif absent during deserialization (backward-compatible via#[serde(default)]). -
In the production startup path (
server/src/main.rsorserver/src/content/spawn.rs), readstartup_msg.character_archetypeand insert the correctCharacterArchetypecomponent on the player entity. The player entity is currently spawned without an archetype component — insert it here. -
In
server/src/simulation/monologue.rs, read theCharacterArchetypecomponent from the player entity atMonologueStateinitialization (or on first tick) and setMonologueState.characterfrom it:"smuggler"or"detective". This is the string that gates all monologue pool selection. -
Bump
PROTOCOL_VERSIONto 19. The new field inStartupMessageis a breaking change — old clients send a message that omitscharacter_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.rs — TellCategory 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:
-
Drains
TriangleActivatedQueueeach tick (non-destructively — queue must still be readable by other consumers; useevents()pattern or check if existing drain is appropriate). -
For each
TriangleActivatedEvent, find the anchor NPC entity (event.npc_entity) and the 1-2 NPCs who are in the same triangle (viaTriangleStatequery). These are the triangle's NPCs. -
Insert a
RoutineDeviationcomponent on all triangle NPCs.RoutineDeviationis the strongest tell category perderive_tell_state()priority order — it overrides Nervous/Angry/Guarded. This makes the triangle NPCs immediately observable as anomalous. -
The
RoutineDeviationcomponent should carry aexpires_at_tick: u64field (if not already present) so it can be removed after a configurable window (suggestTELL_ESCALATION_DURATION_TICKS = 300= 30 game-minutes). Add a system to remove expiredRoutineDeviationcomponents.
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:
-
TickerLinestruct:id: String,text: String,category: String. Add tobridge/types.rs. -
Add
current_ticker: Option<TickerLine>toObserverSnapshotWire. Only populated when player is in The Last Shift zone (zone_id"bar"— checkserver/src/simulation/zone.rs).Nonein all other zones. -
TickerPoolresource: loadsticker/the-last-shift.yamlat startup via the content loader. Holds all 30 headlines. UsesSimRngto advance to a new headline everyTICKER_ROTATION_TICKS = 200ticks (20 game-minutes). Deterministic under D-010 — always useSimRng, never system randomness. -
In the observer snapshot system (
server/src/perception/observer/mod.rs), readTickerPooland populatecurrent_tickerwhen 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.rs — MonologueState 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:
-
After #587 lands (archetype inserted on player entity), read
CharacterArchetypeinMonologuePlugin.build()or the first-tick setup system and setMonologueState.charactercorrectly:CharacterArchetype::Smuggler → "smuggler",CharacterArchetype::Detective → "detective". -
Verify that
select_monologue_line()withtrigger = "enter_location"andcharacter = "smuggler"correctly selects fromopening.yamlin the smuggler subdirectory. Run the existing monologue integration test with a smuggler archetype — it should already pass once #587 sets the character string. -
Add a regression test: spawn two sessions (smuggler + detective), advance 1 tick each, assert different
characteronMonologueState. 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:
- Survey what tile-level data the game needs across systems (doors, containers, damage, visual variants, trigger zones, material properties).
- 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.
- 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).
- 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.
-
Full playthrough test (manual): Boot server in production mode (no
--gauntlet). Boot client in production mode (noSR_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 (checkEngagementRecordvia debug consolenpc <id>). Use debug consolecontaminateto skip to contamination phase. Wait for activation pass —trianglescommand shows one triangle in Active phase. Walk back to Kael —tell_stateisRoutineDeviation. Proximity monologue fires (from copy #597). Walk to The Last Shift — news ticker visible. CI green. -
Automated integration test (server): Add
test_v0_1_integration_playthroughinserver/tests/. Uses the existing test-client infrastructure (tooling/test-client): boot server, sendStartupMessage { world_seed: 12345, character_archetype: Smuggler }, advance 1 tick, assert snapshot contains smuggler opening monologue, advance to tick 3000, sendSkipToContaminationdebug command, advance 10 more ticks, assertTriangleActivatedQueueis non-empty in server state (via golden snapshot comparison). -
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)
cargo testgreen on server branch. All existing tests pass. New tests for archetype round-trip and tell escalation pass.StartupMessagewithcharacter_archetype: Smugglerproduces a smuggler player entity withMonologueState.character = "smuggler"on tick 1.- After
TriangleActivatedfires, triangle NPCs haveRoutineDeviationinserted andtell_state: RoutineDeviationappears in the snapshot. - Snapshot contains
current_ticker(non-null headline) when player is in"bar"zone. - Opening monologue (enter_location) fires on tick 1 with lines from the correct character pool.
- Integration test
test_v0_1_integration_playthroughpasses end-to-end. PROTOCOL_VERSION = 19— matches client.