Implements server-authoritative per-word occlusion for NPC conversations.
ConversationEventBuffer drains into ObserverSnapshot each tick so the client
receives only words audible from the player's position. Updates test fixtures
to include the new conversation_events and conversation_ended fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8-state NPC mood FSM driven by stress, time of day, and interaction events.
Adds MoodState component, derive_mood() pure function, and update_mood() Bevy
system wired into NpcPlugin. Adds Focused as 9th Mood content tag (D-035
Sprint 8 amendment). Syncs CurrentMood for Layer 4 dialogue selection.
31 unit tests covering priority ordering, boundary conditions, and system
integration. All random values route through SimRng (D-010 determinism).
No floats. No HashMap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds test_world::invariants with 29 world-query invariants (structural,
perception, population, simulation) and 7 system-execution tests. All
invariants are gated behind the gauntlet feature and run against the
fully-initialized gauntlet world. Documents StableId ranges through
Sprint 14 rooms.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
InteractionMemory component tracks interaction_count, last_interaction_tick,
and notable_events per NPC. Drives D-028 Layer 2 situation activation:
first_meeting (count==0) and repeated_visit (count>=3). warm_active in mood
system now derives from InteractionMemory within a 300-tick window.
Trust progression wired into dialogue systems: talk completion (+1),
walk-away (-1), confrontation (-2) emit TrustEvents consumed by update_trust.
InteractionEvent (WalkAway, Confrontation) recorded in notable_events for
fast per-pair access.
Adds FirstMeeting and RepeatedVisit Situation variants. 18 unit tests in
interaction.rs. All arithmetic integer-only (D-010 determinism). No HashMap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ActivityState component tracks the activity an NPC is currently performing
at their routine destination (activity name, phase, started_tick). The
enter_activity system runs after movement validation and sets ActivityState
when an NPC has arrived with no active path. Cleared on phase transitions.
Feeds TellTrigger::DuringActivity and D-028 Layer 2 situation matching.
Unit tests verified via shift_change room integration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
update_relationship_dynamics runs once per game-minute (10 ticks). Edges
inactive for 1+ game-hours (600 ticks) decay trust 1 point toward 0 per
minute. Creates social texture: NPCs who haven't interacted drift to neutral
without active maintenance. Blocks #249 (social propagation, Sprint 15).
Constants: DECAY_INTERVAL_TICKS=10, DECAY_INACTIVITY_THRESHOLD_TICKS=600,
DECAY_DELTA=1. Integer arithmetic only (D-010 determinism).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Additional integration tests for conversation cooldown application, distance
termination with both NPCs receiving cooldown, and edge cases in the NPC-to-NPC
conversation pipeline.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Sprint 14 NPC systems are well-structured, clearly documented, and heavily tested. The state machine logic, relationship graph, and conversation pipeline are solid overall. Three issues need attention before merge.
#
File
Severity
Issue
1
mood.rs:148
warning
current_stress * 100 computed as i16 * i16 — any current_stress above 327 overflows. Widen to i32 before comparison.
2
conversation.rs:403
warning
line_interval = 20 is a magic number defined inline — every other timing constant is a named const. Promote to LINE_INTERVAL_TICKS.
3
invariants.rs (all 36)
warning
No invariants verify the 4 new Sprint 14 components (MoodState, InteractionMemory, ActivityState, ConversationCooldown). Missing: every Active NPC should have MoodState, every NPC should have InteractionMemory, no entity should have both ActivityState and PathRequest.
4
invariants.rs:423
suggestion
Pop3 catches missing-tier NPCs but not double-tagged (both ActiveSim + BackgroundSim would still sum correctly).
5
interaction.rs:68
suggestion
Vec::remove(0) is O(n) — VecDeque::pop_front() would be O(1). Fine at MAX_NOTABLE_EVENTS=16 but inconsistent with codebase performance philosophy.
6
conversation.rs:287
suggestion
O(N^2) pairing loop favors early entities in ECS storage order — first eligible pair that passes 2% roll always wins. Worth a comment.
7
conversation.rs
suggestion
player_query.single_mut() single-player assumption is implicit — relationships.rs has an equivalent TODO comment, this system doesn't.
Tyre (Architecture): REQUEST_CHANGES
Well-structured sprint with strong component discipline and genuine care for D-010 determinism. Separation of concerns is clean — no system reaches into another's internals. Two issues require changes before merge.
#
File
Severity
Issue
1
conversation.rs:271-327
warning
Bevy Query::iter() does not guarantee deterministic entity order across runs. The eligible Vec ordering is not stable, so RNG consumption order diverges between replays. Sort eligible by StableId before the pair loop.
2
conversation.rs:418,505
warning
single_mut() bakes in single-player assumption, violates D-010 principle 3. With two playable characters (D-027), this panics. D-078 requires per-observer occlusion — single_mut() structurally prevents this. Attach ConversationEventBuffer to each observer entity.
3
conversation.rs:430-431
warning
Ambient noise hardcoded to 0 — zone-conspicuousness (D-071) won't wire in cleanly. Function signature should accept zone ambient noise level even if stubbed.
4
conversation.rs:287-291
suggestion
O(N^2) pair scan runs every tick. Acceptable for v0.1 Active-tier counts (30-80 NPCs). Add comment noting the growth limit.
5
mood.rs:50-56
suggestion
Suspicious and Focused are unreachable from derive_mood() — update_mood will overwrite externally-set states next tick. Add guard or manual-set flag before observation pipeline ships.
6
interaction.rs:47
suggestion
Vec::remove(0) should be VecDeque::pop_front() for O(1) amortization.
7
relationships.rs:145
suggestion
who_knows() O(N) full scan — rename to who_knows_full_scan to communicate cost at call site.
Verdict: CHANGES REQUESTED
Key fixes before merge:
Sort eligible NPCs by StableId for deterministic conversation pairing (D-010)
Replace single_mut() with per-observer iteration (D-010 principle 3, D-078)
Widen current_stress * 100 to i32 to prevent overflow
Add invariants for Sprint 14 components (MoodState, InteractionMemory)
Promote line_interval to named constant
Structure ambient noise call site for future zone integration
## PR Review: server -> main (type: code)
### Hoshe (Code Quality): REQUEST_CHANGES
The Sprint 14 NPC systems are well-structured, clearly documented, and heavily tested. The state machine logic, relationship graph, and conversation pipeline are solid overall. Three issues need attention before merge.
| # | File | Severity | Issue |
|---|------|----------|-------|
| 1 | `mood.rs:148` | warning | `current_stress * 100` computed as `i16 * i16` — any `current_stress` above 327 overflows. Widen to `i32` before comparison. |
| 2 | `conversation.rs:403` | warning | `line_interval = 20` is a magic number defined inline — every other timing constant is a named `const`. Promote to `LINE_INTERVAL_TICKS`. |
| 3 | `invariants.rs` (all 36) | warning | No invariants verify the 4 new Sprint 14 components (MoodState, InteractionMemory, ActivityState, ConversationCooldown). Missing: every Active NPC should have MoodState, every NPC should have InteractionMemory, no entity should have both ActivityState and PathRequest. |
| 4 | `invariants.rs:423` | suggestion | Pop3 catches missing-tier NPCs but not double-tagged (both ActiveSim + BackgroundSim would still sum correctly). |
| 5 | `interaction.rs:68` | suggestion | `Vec::remove(0)` is O(n) — `VecDeque::pop_front()` would be O(1). Fine at MAX_NOTABLE_EVENTS=16 but inconsistent with codebase performance philosophy. |
| 6 | `conversation.rs:287` | suggestion | O(N^2) pairing loop favors early entities in ECS storage order — first eligible pair that passes 2% roll always wins. Worth a comment. |
| 7 | `conversation.rs` | suggestion | `player_query.single_mut()` single-player assumption is implicit — relationships.rs has an equivalent TODO comment, this system doesn't. |
### Tyre (Architecture): REQUEST_CHANGES
Well-structured sprint with strong component discipline and genuine care for D-010 determinism. Separation of concerns is clean — no system reaches into another's internals. Two issues require changes before merge.
| # | File | Severity | Issue |
|---|------|----------|-------|
| 1 | `conversation.rs:271-327` | warning | Bevy `Query::iter()` does not guarantee deterministic entity order across runs. The `eligible` Vec ordering is not stable, so RNG consumption order diverges between replays. Sort `eligible` by StableId before the pair loop. |
| 2 | `conversation.rs:418,505` | warning | `single_mut()` bakes in single-player assumption, violates D-010 principle 3. With two playable characters (D-027), this panics. D-078 requires per-observer occlusion — `single_mut()` structurally prevents this. Attach ConversationEventBuffer to each observer entity. |
| 3 | `conversation.rs:430-431` | warning | Ambient noise hardcoded to 0 — zone-conspicuousness (D-071) won't wire in cleanly. Function signature should accept zone ambient noise level even if stubbed. |
| 4 | `conversation.rs:287-291` | suggestion | O(N^2) pair scan runs every tick. Acceptable for v0.1 Active-tier counts (30-80 NPCs). Add comment noting the growth limit. |
| 5 | `mood.rs:50-56` | suggestion | `Suspicious` and `Focused` are unreachable from `derive_mood()` — `update_mood` will overwrite externally-set states next tick. Add guard or manual-set flag before observation pipeline ships. |
| 6 | `interaction.rs:47` | suggestion | `Vec::remove(0)` should be `VecDeque::pop_front()` for O(1) amortization. |
| 7 | `relationships.rs:145` | suggestion | `who_knows()` O(N) full scan — rename to `who_knows_full_scan` to communicate cost at call site. |
### Verdict: CHANGES REQUESTED
**Key fixes before merge:**
1. Sort eligible NPCs by StableId for deterministic conversation pairing (D-010)
2. Replace `single_mut()` with per-observer iteration (D-010 principle 3, D-078)
3. Widen `current_stress * 100` to i32 to prevent overflow
4. Add invariants for Sprint 14 components (MoodState, InteractionMemory)
5. Promote `line_interval` to named constant
6. Structure ambient noise call site for future zone integration
The Sprint 14 NPC systems are well-structured, clearly documented, and heavily tested. The state machine logic, relationship graph, and conversation pipeline are solid overall. Three issues need attention before merge.
#
File
Severity
Issue
1
mood.rs:148
warning
current_stress * 100 computed as i16 * i16 — any current_stress above 327 overflows. Widen to i32 before comparison.
2
conversation.rs:403
warning
line_interval = 20 is a magic number defined inline — every other timing constant is a named const. Promote to LINE_INTERVAL_TICKS.
3
invariants.rs (all 36)
warning
No invariants verify the 4 new Sprint 14 components (MoodState, InteractionMemory, ActivityState, ConversationCooldown). Missing: every Active NPC should have MoodState, every NPC should have InteractionMemory, no entity should have both ActivityState and PathRequest.
4
invariants.rs:423
suggestion
Pop3 catches missing-tier NPCs but not double-tagged (both ActiveSim + BackgroundSim would still sum correctly).
5
interaction.rs:68
suggestion
Vec::remove(0) is O(n) — VecDeque::pop_front() would be O(1). Fine at MAX_NOTABLE_EVENTS=16 but inconsistent with codebase performance philosophy.
6
conversation.rs:287
suggestion
O(N^2) pairing loop favors early entities in ECS storage order — first eligible pair that passes 2% roll always wins. Worth a comment.
7
conversation.rs
suggestion
player_query.single_mut() single-player assumption is implicit — relationships.rs has an equivalent TODO comment, this system doesn't.
Tyre (Architecture): REQUEST_CHANGES
Well-structured sprint with strong component discipline and genuine care for D-010 determinism. Separation of concerns is clean — no system reaches into another's internals. Two issues require changes before merge.
#
File
Severity
Issue
1
conversation.rs:271-327
warning
Bevy Query::iter() does not guarantee deterministic entity order across runs. The eligible Vec ordering is not stable, so RNG consumption order diverges between replays. Sort eligible by StableId before the pair loop.
2
conversation.rs:418,505
warning
single_mut() bakes in single-player assumption, violates D-010 principle 3. With two playable characters (D-027), this panics. D-078 requires per-observer occlusion — single_mut() structurally prevents this. Attach ConversationEventBuffer to each observer entity.
3
conversation.rs:430-431
warning
Ambient noise hardcoded to 0 — zone-conspicuousness (D-071) won't wire in cleanly. Function signature should accept zone ambient noise level even if stubbed.
4
conversation.rs:287-291
suggestion
O(N^2) pair scan runs every tick. Acceptable for v0.1 Active-tier counts (30-80 NPCs). Add comment noting the growth limit.
5
mood.rs:50-56
suggestion
Suspicious and Focused are unreachable from derive_mood() — update_mood will overwrite externally-set states next tick. Add guard or manual-set flag before observation pipeline ships.
6
interaction.rs:47
suggestion
Vec::remove(0) should be VecDeque::pop_front() for O(1) amortization.
7
relationships.rs:145
suggestion
who_knows() O(N) full scan — rename to who_knows_full_scan to communicate cost at call site.
Verdict: CHANGES REQUESTED
Key fixes before merge:
Sort eligible NPCs by StableId for deterministic conversation pairing (D-010)
Replace single_mut() with per-observer iteration (D-010 principle 3, D-078)
Widen current_stress * 100 to i32 to prevent overflow
Add invariants for Sprint 14 components (MoodState, InteractionMemory)
Promote line_interval to named constant
Structure ambient noise call site for future zone integration
## PR Review: server -> main (type: code)
### Hoshe (Code Quality): REQUEST_CHANGES
The Sprint 14 NPC systems are well-structured, clearly documented, and heavily tested. The state machine logic, relationship graph, and conversation pipeline are solid overall. Three issues need attention before merge.
| # | File | Severity | Issue |
|---|------|----------|-------|
| 1 | `mood.rs:148` | warning | `current_stress * 100` computed as `i16 * i16` — any `current_stress` above 327 overflows. Widen to `i32` before comparison. |
| 2 | `conversation.rs:403` | warning | `line_interval = 20` is a magic number defined inline — every other timing constant is a named `const`. Promote to `LINE_INTERVAL_TICKS`. |
| 3 | `invariants.rs` (all 36) | warning | No invariants verify the 4 new Sprint 14 components (MoodState, InteractionMemory, ActivityState, ConversationCooldown). Missing: every Active NPC should have MoodState, every NPC should have InteractionMemory, no entity should have both ActivityState and PathRequest. |
| 4 | `invariants.rs:423` | suggestion | Pop3 catches missing-tier NPCs but not double-tagged (both ActiveSim + BackgroundSim would still sum correctly). |
| 5 | `interaction.rs:68` | suggestion | `Vec::remove(0)` is O(n) — `VecDeque::pop_front()` would be O(1). Fine at MAX_NOTABLE_EVENTS=16 but inconsistent with codebase performance philosophy. |
| 6 | `conversation.rs:287` | suggestion | O(N^2) pairing loop favors early entities in ECS storage order — first eligible pair that passes 2% roll always wins. Worth a comment. |
| 7 | `conversation.rs` | suggestion | `player_query.single_mut()` single-player assumption is implicit — relationships.rs has an equivalent TODO comment, this system doesn't. |
### Tyre (Architecture): REQUEST_CHANGES
Well-structured sprint with strong component discipline and genuine care for D-010 determinism. Separation of concerns is clean — no system reaches into another's internals. Two issues require changes before merge.
| # | File | Severity | Issue |
|---|------|----------|-------|
| 1 | `conversation.rs:271-327` | warning | Bevy `Query::iter()` does not guarantee deterministic entity order across runs. The `eligible` Vec ordering is not stable, so RNG consumption order diverges between replays. Sort `eligible` by StableId before the pair loop. |
| 2 | `conversation.rs:418,505` | warning | `single_mut()` bakes in single-player assumption, violates D-010 principle 3. With two playable characters (D-027), this panics. D-078 requires per-observer occlusion — `single_mut()` structurally prevents this. Attach ConversationEventBuffer to each observer entity. |
| 3 | `conversation.rs:430-431` | warning | Ambient noise hardcoded to 0 — zone-conspicuousness (D-071) won't wire in cleanly. Function signature should accept zone ambient noise level even if stubbed. |
| 4 | `conversation.rs:287-291` | suggestion | O(N^2) pair scan runs every tick. Acceptable for v0.1 Active-tier counts (30-80 NPCs). Add comment noting the growth limit. |
| 5 | `mood.rs:50-56` | suggestion | `Suspicious` and `Focused` are unreachable from `derive_mood()` — `update_mood` will overwrite externally-set states next tick. Add guard or manual-set flag before observation pipeline ships. |
| 6 | `interaction.rs:47` | suggestion | `Vec::remove(0)` should be `VecDeque::pop_front()` for O(1) amortization. |
| 7 | `relationships.rs:145` | suggestion | `who_knows()` O(N) full scan — rename to `who_knows_full_scan` to communicate cost at call site. |
### Verdict: CHANGES REQUESTED
**Key fixes before merge:**
1. Sort eligible NPCs by StableId for deterministic conversation pairing (D-010)
2. Replace `single_mut()` with per-observer iteration (D-010 principle 3, D-078)
3. Widen `current_stress * 100` to i32 to prevent overflow
4. Add invariants for Sprint 14 components (MoodState, InteractionMemory)
5. Promote `line_interval` to named constant
6. Structure ambient noise call site for future zone integration
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Sprint 14 server delivery: NPC social systems that make the simulation breathe independently of the player.
Key constraints met: No floats (D-010), no HashMap in simulation, all RNG through SimRng, integer-only Bernoulli trials for occlusion.
644 lib tests pass. 21 files changed, +4,142 lines.
Test plan
cargo test --lib— 644 tests pass (mood: 31, trust: 12, routine: 8, conversation: 15, invariants: 36+)PR Review: server -> main (type: code)
Hoshe (Code Quality): REQUEST_CHANGES
The Sprint 14 NPC systems are well-structured, clearly documented, and heavily tested. The state machine logic, relationship graph, and conversation pipeline are solid overall. Three issues need attention before merge.
mood.rs:148current_stress * 100computed asi16 * i16— anycurrent_stressabove 327 overflows. Widen toi32before comparison.conversation.rs:403line_interval = 20is a magic number defined inline — every other timing constant is a namedconst. Promote toLINE_INTERVAL_TICKS.invariants.rs(all 36)invariants.rs:423interaction.rs:68Vec::remove(0)is O(n) —VecDeque::pop_front()would be O(1). Fine at MAX_NOTABLE_EVENTS=16 but inconsistent with codebase performance philosophy.conversation.rs:287conversation.rsplayer_query.single_mut()single-player assumption is implicit — relationships.rs has an equivalent TODO comment, this system doesn't.Tyre (Architecture): REQUEST_CHANGES
Well-structured sprint with strong component discipline and genuine care for D-010 determinism. Separation of concerns is clean — no system reaches into another's internals. Two issues require changes before merge.
conversation.rs:271-327Query::iter()does not guarantee deterministic entity order across runs. TheeligibleVec ordering is not stable, so RNG consumption order diverges between replays. Sorteligibleby StableId before the pair loop.conversation.rs:418,505single_mut()bakes in single-player assumption, violates D-010 principle 3. With two playable characters (D-027), this panics. D-078 requires per-observer occlusion —single_mut()structurally prevents this. Attach ConversationEventBuffer to each observer entity.conversation.rs:430-431conversation.rs:287-291mood.rs:50-56SuspiciousandFocusedare unreachable fromderive_mood()—update_moodwill overwrite externally-set states next tick. Add guard or manual-set flag before observation pipeline ships.interaction.rs:47Vec::remove(0)should beVecDeque::pop_front()for O(1) amortization.relationships.rs:145who_knows()O(N) full scan — rename towho_knows_full_scanto communicate cost at call site.Verdict: CHANGES REQUESTED
Key fixes before merge:
single_mut()with per-observer iteration (D-010 principle 3, D-078)current_stress * 100to i32 to prevent overflowline_intervalto named constantPR Review: server -> main (type: code)
Hoshe (Code Quality): REQUEST_CHANGES
The Sprint 14 NPC systems are well-structured, clearly documented, and heavily tested. The state machine logic, relationship graph, and conversation pipeline are solid overall. Three issues need attention before merge.
mood.rs:148current_stress * 100computed asi16 * i16— anycurrent_stressabove 327 overflows. Widen toi32before comparison.conversation.rs:403line_interval = 20is a magic number defined inline — every other timing constant is a namedconst. Promote toLINE_INTERVAL_TICKS.invariants.rs(all 36)invariants.rs:423interaction.rs:68Vec::remove(0)is O(n) —VecDeque::pop_front()would be O(1). Fine at MAX_NOTABLE_EVENTS=16 but inconsistent with codebase performance philosophy.conversation.rs:287conversation.rsplayer_query.single_mut()single-player assumption is implicit — relationships.rs has an equivalent TODO comment, this system doesn't.Tyre (Architecture): REQUEST_CHANGES
Well-structured sprint with strong component discipline and genuine care for D-010 determinism. Separation of concerns is clean — no system reaches into another's internals. Two issues require changes before merge.
conversation.rs:271-327Query::iter()does not guarantee deterministic entity order across runs. TheeligibleVec ordering is not stable, so RNG consumption order diverges between replays. Sorteligibleby StableId before the pair loop.conversation.rs:418,505single_mut()bakes in single-player assumption, violates D-010 principle 3. With two playable characters (D-027), this panics. D-078 requires per-observer occlusion —single_mut()structurally prevents this. Attach ConversationEventBuffer to each observer entity.conversation.rs:430-431conversation.rs:287-291mood.rs:50-56SuspiciousandFocusedare unreachable fromderive_mood()—update_moodwill overwrite externally-set states next tick. Add guard or manual-set flag before observation pipeline ships.interaction.rs:47Vec::remove(0)should beVecDeque::pop_front()for O(1) amortization.relationships.rs:145who_knows()O(N) full scan — rename towho_knows_full_scanto communicate cost at call site.Verdict: CHANGES REQUESTED
Key fixes before merge:
single_mut()with per-observer iteration (D-010 principle 3, D-078)current_stress * 100to i32 to prevent overflowline_intervalto named constant