--- title: "Sprint 14 — Server Briefing" description: "NPC mood state machine, trust progression, routine execution system" type: sprint status: archived sprint: 14 team: "server" --- # Sprint 14: Live — Server Tasks **Goal:** Bring NPCs to life — mood, trust, routine execution, and NPC-to-NPC conversation drive the first emergent social observables; the simulation produces a world that breathes independently of the player. **Branch:** `server` **Agents:** Dudley (simulation dev), Tyre (architect), Hoshe (QA) ## Carry-over from Sprint 13 None. All Sprint 13 server tickets done. ## New Tickets | # | Title | Blocked by | |---|-------|------------| | #323 | NPC mood state machine | — | | #324 | Trust progression system | — | | #325 | Interaction tracking component | — | | #101 | Routine execution system | #87 done, #238 done | | #103 | Relationship dynamics | — | | #247 | NPC-to-NPC conversation system | — | | #508 | Map-agnostic invariant tests (36 invariants) | — | Use `db/connectors/ticket show ` for full details. ## Key Decisions - `decisions/architecture.md` — D-024 (10-axis NPC model), D-026 (simulation tiers — ActiveSim scope), D-031 (game clock / day phases — routine transitions), D-041 (knowledge graph — trust feeds Layer 3 filtering), D-054 (tile-based movement), D-075 (dialogue filtering — trust co-gate with KnowledgeConfidence) - `decisions/content.md` — D-023 (three-tier content model), D-028 (dialogue four-layer model — Layers 2-4 fed by trust/interaction tracking), D-029 (population entanglement ratio — 30/50/20), D-034 (THE FRIEND — trust arc requirements), D-062 (invisible locked options — new trust tier = new options appear silently) - `decisions/perception.md` — D-016 (internal monologue — mood drives monologue tone), D-018 (three-range sound model — NPC-to-NPC conversations emit Voice events) ## Notes - **#323 — NPC mood state machine:** Add a `Mood` component to `server/src/npc/mod.rs`. Eight moods for v0.1: `Neutral`, `Anxious`, `Frustrated`, `Content`, `Suspicious`, `Warm`, `Hostile`, `Focused` (9th: added Sprint 8, D-035 amendment). Mood changes based on: triangle pressure (high ToleranceThreshold stress → Anxious/Hostile), time of day (Evening + long shift → Frustrated), recent interactions (positive player interaction → Warm). Write a `MoodSystem` in a new `server/src/npc/mood.rs` module. Mood is read by the monologue trigger system (`server/src/simulation/monologue.rs`) and will gate Layer 4 dialogue selection (`server/src/simulation/dialogue.rs`). Blocks #337 (tell state derivation, deferred to S15). Add to `NpcPlugin` in `server/src/npc/mod.rs`. Keep all values `i16`/enum — no floats, per D-010 determinism. - Delivery: `Mood` component, `update_mood` system, unit tests in `server/src/npc/mood.rs`. - **#324 — Trust progression system:** Trust level per player-NPC pair drives D-028 Layer 3 (trust-gated gossip). The `RelationshipGraph` at `server/src/npc/relationships.rs` already holds `trust: i8` on every `RelationshipEdge`. Sprint 14 work: (1) a system that advances trust based on interaction quality — `TalkVerb` completion → small positive increment, confrontation walk-away → negative decrement, repeat visit (from #325 interaction count) → small positive; (2) expose the current trust value on `ObserverSnapshot` or leave it in the KnowledgeGraph (D-041). Trust maps to D-028 TrustTier via `relationship_to_trust()` in `server/src/simulation/dialogue.rs` — D-075 adds KnowledgeConfidence as co-gate to that function. No new ECS component needed — `RelationshipGraph` resource is the store. Write a `update_trust` system in `server/src/npc/relationships.rs`. Blocks #171 (Layer 3 trust-gated gossip, deferred to S15). - Integration point: `server/src/simulation/dialogue.rs` `process_talk_interaction` must call `relationship_to_trust()` with `KnowledgeConfidence` (D-075 implementation if not already wired). - Delivery: `update_trust` system, tests verifying trust increments/decrements on interaction events. - **#325 — Interaction tracking component:** `InteractionMemory` component per NPC-pair — fields: `interaction_count: u32`, `last_interaction_tick: u64`, `notable_events: Vec`. The `interaction_count` drives D-028 Layer 2 situation activation: `first_meeting` when count == 0, `repeated_visit` when count >= 3. Add to `server/src/npc/mod.rs` or a new `server/src/npc/interaction.rs`. Populated by `process_talk_interaction` in `server/src/simulation/dialogue.rs` each time a Talk verb completes. `notable_events` stores walk-aways (D-064) and confrontations (D-063) — these are already recorded in the KG but `InteractionMemory` provides fast per-pair access without a full KG query. - Integration point: `server/src/simulation/dialogue.rs` — increment count and stamp tick on each completed Talk. `dialogue.rs` already reads KG and RelationshipGraph; add InteractionMemory to the same query. - Delivery: `InteractionMemory` component, incremented by dialogue system, used by situation resolver for Layer 2. - **#101 — Routine execution system:** The routine data model exists (`DailyRoutine`, `RoutineEntry` in `server/src/npc/mod.rs`). The phase-transition trigger exists (`check_phase_transition` in `server/src/npc/routine.rs`) — it already issues `PathRequest` on phase change. Sprint 14 work: verify the full execution loop closes — `PathRequest` → pathfinder → `path_follow` → entity reaches destination and enters the routine activity state. Add an `activity: String` → `ActivityState` ECS component (or reuse `DailyRoutine.entries[].activity`) so the simulation knows what an NPC is currently doing. This feeds the `DuringActivity` tell trigger (`TellTrigger::DuringActivity` in `server/src/npc/mod.rs`) and the Layer 2 `situation` tag matching. Existing pathfinding: `server/src/simulation/pathfinding.rs`, `server/src/simulation/path_follow.rs`. Gauntlet room `shift_change_*` (Sprint 13 #505) already validates phase-boundary transition — extend it or write new integration tests for full routine loop. - Delivery: `ActivityState` component (or equivalent) attached/updated as NPCs execute routines; tests verifying NPC reaches routine destination and holds activity state. - **#103 — Relationship dynamics:** Relationship values decay and reinforce over time. Existing `RelationshipEdge` in `server/src/npc/relationships.rs` has `trust: i8` and `history: Vec`. Sprint 14 work: (1) passive decay — trust drifts toward 0 at ~1 point per game-day if no recent interaction (controlled by `last_interaction_tick`); (2) interaction reinforcement — talking, helping (future), witnessing positive events strengthens; (3) `RelationshipEvent` is appended on notable interactions. This system runs in the Background tier (D-026) — lightweight, 1 update per game-minute. Add `update_relationship_dynamics` system to `server/src/npc/relationships.rs`, registered in `NpcPlugin`. Blocks #249 (player-action social propagation, deferred to S15). - Note: Do not confuse with #324 (player-NPC trust). #103 covers NPC-NPC relationship dynamics — the relationship graph that generates the social texture (#249 feeds into). The player-NPC trust arc (THE FRIEND, #324) is a separate ticket. - **#247 — NPC-to-NPC conversation system:** NPCs in the Active tier who are in proximity (≤3 tiles) and share a social site occasionally enter NPC-to-NPC conversations. Implementation: (1) a `ConversationSystem` in `server/src/simulation/` that detects eligible NPC pairs (same zone, ActiveSim, not already in player conversation, not sprinting away), initiates a `NpcConversation` state component for the pair, and emits a `SoundEvent` of kind `Voice` at the conversation tile — picked up by `server/src/simulation/sound.rs` and included in `ObserverSnapshot.sound_events`; (2) conversation duration in ticks (configurable constant, ~30-120 ticks = 3-12 game-minutes); (3) conversation terminates when one NPC leaves the zone or duration expires. The client's `sound_indicator_renderer.gd` already handles `Voice` sound events. This is the first source of overheard conversations — the eavesdrop mechanic (D-426 `ListeningFocus`) becomes meaningful when NPCs are actually talking. - **D-078 addition (amended — occlusion is SERVER-AUTHORITATIVE):** Each `ConversationEvent` on `ObserverSnapshot` must carry: `occluded_line: String` (the NPC dialogue line with dropped words replaced by `...`), `speaker_id: EntityId`, `target_id: EntityId`, `speaker_name: String`, `target_name: String`. The server performs per-word occlusion before emission — the client receives pre-occluded text and renders it verbatim. - **Per-word occlusion algorithm:** Iterate the dialogue line word by word. For each word, perform an independent Bernoulli trial using a seedable RNG (seeded per tick for deterministic replay, D-010). Per-word drop probability is derived from three inputs at the moment of emission: (a) tile distance from player to the speaking NPC — linear decay from 0.0 drop probability at 0 tiles to 1.0 at the `Voice` sound range boundary; (b) ambient noise level at the player position (already tracked in `ObserverSnapshot.ambient_noise`) — adds up to 0.3 to drop probability; (c) whether the player entity has `ListeningFocus` stance active — subtracts 0.2 from drop probability (clamped to [0.0, 1.0]). Words that fail the trial are replaced with `...` in `occluded_line`. - When a conversation ends (NPC departs zone or duration expires), emit a `conversation_end` event with the same pair IDs so the client can dismiss the panel. Trigger `witness_interaction` on the player observation pipeline whenever the player receives any conversation event (regardless of occlusion level). - No dialogue content needed in this ticket — content is sourced from the NPC-to-NPC line pool (#536, copy team, Sprint 14). - Delivery: `NpcConversation` state component, `run_npc_conversations` system, `Voice` `SoundEvent` emission, `ConversationEvent` struct with `occluded_line` on `ObserverSnapshot`, `conversation_end` event, per-word occlusion function with seedable RNG, tests: `npc_conversation_emits_voice_event_when_in_range`, `npc_conversation_terminates_when_apart`, `occlusion_drops_words_with_distance`, `occlusion_suppressed_by_listening_focus`, `occlusion_deterministic_with_same_seed`. Blocks #535 (client). - **#508 — Map-agnostic invariant tests (36 invariants):** 36 invariants across 4 categories from Gestalt's workshop output. Implement as a test suite in `server/src/test_world/` that runs against any valid map (gauntlet rooms). Categories: structural (8 — tile counts, wall connectivity, spawn point validity), perception (5 — LOS symmetry at range, sound range boundaries), population (8 — NPC count limits, tier assignment correctness), simulation (8 — no entity teleports, determinism, pathfinder termination, interaction buffer clear on sprint). Add a `run_invariants(world: &World)` function callable from gauntlet room tests — each room calls it after setup to assert structural invariants hold. Dynamic map system not required — invariants work on static gauntlet maps. - Note: #509 (fuzzy tests for procedural maps) remains deferred — requires dynamic map generation. - Delivery: `server/src/test_world/invariants.rs` module with 36 test assertions; each gauntlet room test calls `run_invariants`. ## Dependency Chain ``` #323 (mood) ─────────────────────────────────────→ #337 (tell derivation, S15) #324 (trust) ────────────────────────────────────→ #171 (Layer 3, S15) #325 (interaction tracking) → #101 (routine) is parallel → #103 (relationship dynamics) is parallel → #247 (NPC conversations) is parallel #247 (NPC conversations) ────────────────────────→ #535 (client passive panel, blocked) #508 (invariants) — standalone, parallel with all above ``` All six simulation tickets (#323, #324, #325, #101, #103, #247) are independent of each other and can proceed in parallel. #508 is also fully standalone. #535 (client) is blocked on #247 landing the `ConversationEvent` struct on `ObserverSnapshot` — specifically `occluded_line: String` (pre-occluded by server), `speaker_name`, `target_name`, and `conversation_end`. ## PR Workflow When ready to submit, create a PR with `tea` CLI. All flags are required to avoid TTY prompts (see CLAUDE.md "Gitea access" section): ```bash tea pr create --repo jpmschweitzer/settled-reach --login schweitz --title "feat(simulation): description" --description "body" --base main --head server ```