NPC mood, trust, routine execution, NPC-to-NPC conversations with D-078 occlusion, monologue display, content line pools, and visual specs. Teams: server (7), client (3), copy (6), visual (6). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
12 KiB
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
Moodcomponent toserver/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 aMoodSystemin a newserver/src/npc/mood.rsmodule. 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 toNpcPlugininserver/src/npc/mod.rs. Keep all valuesi16/enum — no floats, per D-010 determinism.- Delivery:
Moodcomponent,update_moodsystem, unit tests inserver/src/npc/mood.rs.
- Delivery:
-
#324 — Trust progression system: Trust level per player-NPC pair drives D-028 Layer 3 (trust-gated gossip). The
RelationshipGraphatserver/src/npc/relationships.rsalready holdstrust: i8on everyRelationshipEdge. Sprint 14 work: (1) a system that advances trust based on interaction quality —TalkVerbcompletion → small positive increment, confrontation walk-away → negative decrement, repeat visit (from #325 interaction count) → small positive; (2) expose the current trust value onObserverSnapshotor leave it in the KnowledgeGraph (D-041). Trust maps to D-028 TrustTier viarelationship_to_trust()inserver/src/simulation/dialogue.rs— D-075 adds KnowledgeConfidence as co-gate to that function. No new ECS component needed —RelationshipGraphresource is the store. Write aupdate_trustsystem inserver/src/npc/relationships.rs. Blocks #171 (Layer 3 trust-gated gossip, deferred to S15).- Integration point:
server/src/simulation/dialogue.rsprocess_talk_interactionmust callrelationship_to_trust()withKnowledgeConfidence(D-075 implementation if not already wired). - Delivery:
update_trustsystem, tests verifying trust increments/decrements on interaction events.
- Integration point:
-
#325 — Interaction tracking component:
InteractionMemorycomponent per NPC-pair — fields:interaction_count: u32,last_interaction_tick: u64,notable_events: Vec<InteractionEvent>. Theinteraction_countdrives D-028 Layer 2 situation activation:first_meetingwhen count == 0,repeated_visitwhen count >= 3. Add toserver/src/npc/mod.rsor a newserver/src/npc/interaction.rs. Populated byprocess_talk_interactioninserver/src/simulation/dialogue.rseach time a Talk verb completes.notable_eventsstores walk-aways (D-064) and confrontations (D-063) — these are already recorded in the KG butInteractionMemoryprovides 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.rsalready reads KG and RelationshipGraph; add InteractionMemory to the same query. - Delivery:
InteractionMemorycomponent, incremented by dialogue system, used by situation resolver for Layer 2.
- Integration point:
-
#101 — Routine execution system: The routine data model exists (
DailyRoutine,RoutineEntryinserver/src/npc/mod.rs). The phase-transition trigger exists (check_phase_transitioninserver/src/npc/routine.rs) — it already issuesPathRequeston 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 anactivity: String→ActivityStateECS component (or reuseDailyRoutine.entries[].activity) so the simulation knows what an NPC is currently doing. This feeds theDuringActivitytell trigger (TellTrigger::DuringActivityinserver/src/npc/mod.rs) and the Layer 2situationtag matching. Existing pathfinding:server/src/simulation/pathfinding.rs,server/src/simulation/path_follow.rs. Gauntlet roomshift_change_*(Sprint 13 #505) already validates phase-boundary transition — extend it or write new integration tests for full routine loop.- Delivery:
ActivityStatecomponent (or equivalent) attached/updated as NPCs execute routines; tests verifying NPC reaches routine destination and holds activity state.
- Delivery:
-
#103 — Relationship dynamics: Relationship values decay and reinforce over time. Existing
RelationshipEdgeinserver/src/npc/relationships.rshastrust: i8andhistory: Vec<RelationshipEvent>. Sprint 14 work: (1) passive decay — trust drifts toward 0 at ~1 point per game-day if no recent interaction (controlled bylast_interaction_tick); (2) interaction reinforcement — talking, helping (future), witnessing positive events strengthens; (3)RelationshipEventis appended on notable interactions. This system runs in the Background tier (D-026) — lightweight, 1 update per game-minute. Addupdate_relationship_dynamicssystem toserver/src/npc/relationships.rs, registered inNpcPlugin. 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
ConversationSysteminserver/src/simulation/that detects eligible NPC pairs (same zone, ActiveSim, not already in player conversation, not sprinting away), initiates aNpcConversationstate component for the pair, and emits aSoundEventof kindVoiceat the conversation tile — picked up byserver/src/simulation/sound.rsand included inObserverSnapshot.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'ssound_indicator_renderer.gdalready handlesVoicesound events. This is the first source of overheard conversations — the eavesdrop mechanic (D-426ListeningFocus) becomes meaningful when NPCs are actually talking.- D-078 addition (amended — occlusion is SERVER-AUTHORITATIVE): Each
ConversationEventonObserverSnapshotmust 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
Voicesound range boundary; (b) ambient noise level at the player position (already tracked inObserverSnapshot.ambient_noise) — adds up to 0.3 to drop probability; (c) whether the player entity hasListeningFocusstance active — subtracts 0.2 from drop probability (clamped to [0.0, 1.0]). Words that fail the trial are replaced with...inoccluded_line. - When a conversation ends (NPC departs zone or duration expires), emit a
conversation_endevent with the same pair IDs so the client can dismiss the panel. Triggerwitness_interactionon 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:
NpcConversationstate component,run_npc_conversationssystem,VoiceSoundEventemission,ConversationEventstruct withoccluded_lineonObserverSnapshot,conversation_endevent, 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).
- D-078 addition (amended — occlusion is SERVER-AUTHORITATIVE): Each
-
#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 arun_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.rsmodule with 36 test assertions; each gauntlet room test callsrun_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