# Conflicts: # CHANGELOG.md # content/_meta/README.md # content/_meta/npc-authoring-style-guide.md # wiki/_templates/cultural-group.md # wiki/_templates/institution.md # wiki/_templates/star-system.md # wiki/characters/devra.md # wiki/characters/drin.md # wiki/characters/harek.md # wiki/characters/lera-sessik.md # wiki/characters/maret-korr.md # wiki/characters/naia-tamm.md # wiki/characters/nils-davan.md # wiki/characters/pell.md # wiki/characters/renn.md # wiki/characters/resha.md # wiki/characters/sabel.md # wiki/characters/sera-venn.md # wiki/characters/torek-lintar.md # wiki/characters/voss.md # wiki/star-systems/krenn/index.md
18 KiB
title, description, type, status, sprint, team
| title | description | type | status | sprint | team |
|---|---|---|---|---|---|
| Sprint 24 — Server Briefing | Character archetype spawning, triangle activation consumer, news ticker | sprint | archived | 24 | server |
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.