Files
settled-reach/docs/sprints/sprint-24/server.md
T
jpmschweitzer 23d9ff0a58 Merge remote-tracking branch 'origin/main' into planning
# 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
2026-03-14 00:24:53 +01:00

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: 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.