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

209 lines
18 KiB
Markdown

---
title: "Sprint 24 — Server Briefing"
description: "Character archetype spawning, triangle activation consumer, news ticker"
type: sprint
status: archived
sprint: 24
team: "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.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:
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.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:
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
```bash
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.