feat(simulation): Sprint 18 — 9 server systems
Background tier state machines (#95), NPC vision (#115), player- awareness behavior (#244), skill system & combat flag (#91), social propagation (#249), examine mechanic (#242), character pressure framework (#248), save state data model (#256), tell state wiring (#337). Protocol version bumped 13→14 for examine_result and character_pressure snapshot fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2583,3 +2583,169 @@ fn no_zone_map_resource_tiles_have_no_zone_id() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #337 — Tell state → snapshot integration (D-024 tell system)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Helper: run derive_tell_state + two-stage observer pipeline together.
|
||||
fn run_tell_plus_observer_pipeline(world: &mut World) {
|
||||
use crate::npc::tell_state::derive_tell_state;
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems((
|
||||
derive_tell_state,
|
||||
compute_visibility_geometry.after(derive_tell_state),
|
||||
compute_observer_snapshot
|
||||
.after(compute_visibility_geometry)
|
||||
.after(derive_tell_state),
|
||||
));
|
||||
schedule.run(world);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tell_state_nervous_appears_in_snapshot_for_major_secret_high_stress() {
|
||||
// Spec (#337, D-024): NPC with Major secret + stress past midpoint shows
|
||||
// TellCategory::Nervous in ObserverSnapshot.entities[].tell_state.
|
||||
// End-to-end pipeline: axis values → derive_tell_state → DerivedTellState
|
||||
// → compute_observer_snapshot → VisibleEntity.tell_state.
|
||||
use crate::npc::mood::{MoodState, NpcMood};
|
||||
use crate::npc::tell_state::{DerivedTellState, TellCategory};
|
||||
use crate::npc::{Contentment, Npc, Secret, SecretSeverity, ToleranceThreshold};
|
||||
use crate::simulation::tier::ActiveSim;
|
||||
|
||||
let mut world = setup_world(32, 32);
|
||||
|
||||
world.spawn((
|
||||
PlayerCharacter,
|
||||
TilePosition::new(16, 16, 0),
|
||||
Facing(FacingDirection::North),
|
||||
KnowledgeGraph::new(),
|
||||
NearbyInteractionBuffer::default(),
|
||||
MonologueBuffer::default(),
|
||||
));
|
||||
|
||||
// NPC directly north — in forward LOS — with Major secret + stress > midpoint.
|
||||
// stress=60, threshold=100 → stress*2=120 > 100 → Nervous (D-024 priority 2)
|
||||
world.spawn((
|
||||
Npc,
|
||||
ActiveSim,
|
||||
TilePosition::new(16, 14, 0),
|
||||
Secret {
|
||||
description: "criminal record".into(),
|
||||
severity: SecretSeverity::Major,
|
||||
known_by: vec![],
|
||||
},
|
||||
ToleranceThreshold { current_stress: 60, threshold: 100 },
|
||||
Contentment { level: 0 },
|
||||
MoodState { mood: NpcMood::Neutral, changed_tick: 0 },
|
||||
DerivedTellState::default(),
|
||||
));
|
||||
|
||||
run_tell_plus_observer_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().expect("snapshot should exist");
|
||||
|
||||
let npc = snapshot
|
||||
.entities
|
||||
.iter()
|
||||
.find(|e| matches!(e.kind, EntityKind::Npc))
|
||||
.expect("NPC should be visible in snapshot");
|
||||
|
||||
assert_eq!(
|
||||
npc.tell_state,
|
||||
Some(TellCategory::Nervous),
|
||||
"NPC with Major secret + stress past midpoint should show Nervous tell in snapshot"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tell_state_none_for_neutral_npc_in_snapshot() {
|
||||
// Spec (#337, D-024): neutral NPC shows tell_state = None in snapshot.
|
||||
// Verifies the pipeline correctly omits tell when no conditions are met.
|
||||
use crate::npc::mood::{MoodState, NpcMood};
|
||||
use crate::npc::tell_state::DerivedTellState;
|
||||
use crate::npc::{Contentment, Npc, Secret, SecretSeverity, ToleranceThreshold};
|
||||
use crate::simulation::tier::ActiveSim;
|
||||
|
||||
let mut world = setup_world(32, 32);
|
||||
|
||||
world.spawn((
|
||||
PlayerCharacter,
|
||||
TilePosition::new(16, 16, 0),
|
||||
Facing(FacingDirection::North),
|
||||
KnowledgeGraph::new(),
|
||||
NearbyInteractionBuffer::default(),
|
||||
MonologueBuffer::default(),
|
||||
));
|
||||
|
||||
// NPC in LOS — neutral state (Minor secret, low stress, neutral mood, low contentment)
|
||||
world.spawn((
|
||||
Npc,
|
||||
ActiveSim,
|
||||
TilePosition::new(16, 14, 0),
|
||||
Secret {
|
||||
description: "minor embarrassment".into(),
|
||||
severity: SecretSeverity::Minor,
|
||||
known_by: vec![],
|
||||
},
|
||||
ToleranceThreshold { current_stress: 10, threshold: 100 },
|
||||
Contentment { level: 0 },
|
||||
MoodState { mood: NpcMood::Neutral, changed_tick: 0 },
|
||||
DerivedTellState::default(),
|
||||
));
|
||||
|
||||
run_tell_plus_observer_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().expect("snapshot should exist");
|
||||
|
||||
let npc = snapshot
|
||||
.entities
|
||||
.iter()
|
||||
.find(|e| matches!(e.kind, EntityKind::Npc))
|
||||
.expect("NPC should be visible in snapshot");
|
||||
|
||||
assert_eq!(
|
||||
npc.tell_state,
|
||||
None,
|
||||
"neutral NPC should have no tell state in snapshot"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tell_state_none_when_npc_has_no_derived_tell_component() {
|
||||
// Spec (#337): NPC without DerivedTellState component has tell_state = None.
|
||||
// Verifies Option<&DerivedTellState> query handles absent component gracefully.
|
||||
|
||||
let mut world = setup_world(32, 32);
|
||||
|
||||
world.spawn((
|
||||
PlayerCharacter,
|
||||
TilePosition::new(16, 16, 0),
|
||||
Facing(FacingDirection::North),
|
||||
KnowledgeGraph::new(),
|
||||
NearbyInteractionBuffer::default(),
|
||||
MonologueBuffer::default(),
|
||||
));
|
||||
|
||||
// NPC with no DerivedTellState component at all
|
||||
world.spawn((crate::npc::Npc, TilePosition::new(16, 14, 0)));
|
||||
|
||||
run_observer_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().expect("snapshot should exist");
|
||||
|
||||
let npc = snapshot
|
||||
.entities
|
||||
.iter()
|
||||
.find(|e| matches!(e.kind, EntityKind::Npc))
|
||||
.expect("NPC should be visible in snapshot");
|
||||
|
||||
assert_eq!(
|
||||
npc.tell_state,
|
||||
None,
|
||||
"NPC without DerivedTellState component should have tell_state = None"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user