Files
settled-reach/docs/sprints/sprint-14/server.md
T
jpmschweitzerandClaude Opus 4.6 bccdcfcdcb docs(docs): add frontmatter to all sprint briefings
Standardized YAML frontmatter on all 115 sprint briefing files across
sprints 1-26 with title, description, type, status, sprint number, and
team fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 00:15:45 +01:00

13 KiB

title, description, type, status, sprint, team
title description type status sprint team
Sprint 14 — Server Briefing NPC mood state machine, trust progression, routine execution system sprint archived 14 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 <id> 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<InteractionEvent>. 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: StringActivityState 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<RelationshipEvent>. 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):

tea pr create --repo jpmschweitzer/settled-reach --login schweitz --title "feat(simulation): description" --description "body" --base main --head server