Merge remote-tracking branch 'origin/server'
# Conflicts: # CHANGELOG.md # client/tests/fixtures/msgpack/snapshot_boundary_tick_0.msgpack # client/tests/fixtures/msgpack/snapshot_boundary_tick_127.msgpack # client/tests/fixtures/msgpack/snapshot_boundary_tick_2b31m1.msgpack # client/tests/fixtures/msgpack/snapshot_boundary_tick_2b32.msgpack # client/tests/fixtures/msgpack/snapshot_boundary_tick_32767.msgpack # client/tests/fixtures/msgpack/snapshot_empty.msgpack # client/tests/fixtures/msgpack/snapshot_multi_entity.msgpack # client/tests/fixtures/msgpack/snapshot_one_npc.msgpack # client/tests/fixtures/msgpack/snapshot_player.msgpack # client/tests/fixtures/msgpack/snapshot_v2_full.msgpack # server/src/bridge/types.rs
This commit is contained in:
@@ -19,6 +19,7 @@ use crate::perception::vision_cone::Facing;
|
||||
use crate::simulation::contraband::ScanEventBuffer;
|
||||
use crate::simulation::conversation::ConversationEventBuffer;
|
||||
use crate::simulation::dialogue::DialogueResponseBuffer;
|
||||
use crate::simulation::examine::ExamineResultBuffer;
|
||||
use crate::simulation::follow::FollowTarget;
|
||||
use crate::simulation::interaction::NearbyInteractionBuffer;
|
||||
use crate::simulation::inventory::{CarriedBy, InventorySlot, ItemName};
|
||||
@@ -83,6 +84,7 @@ pub fn compute_observer_snapshot(
|
||||
Option<&mut ScanEventBuffer>,
|
||||
Option<&mut ConversationEventBuffer>,
|
||||
Option<&FollowTarget>,
|
||||
Option<&mut ExamineResultBuffer>,
|
||||
),
|
||||
With<PlayerCharacter>,
|
||||
>,
|
||||
@@ -98,6 +100,7 @@ pub fn compute_observer_snapshot(
|
||||
poi_query: Query<&PointOfInterest>,
|
||||
mut buffer: ResMut<SnapshotBuffer>,
|
||||
sim_rng: Option<Res<SimRng>>,
|
||||
pressure_query: Query<&crate::simulation::pressure::CharacterPressure, With<PlayerCharacter>>,
|
||||
) {
|
||||
let Ok((
|
||||
observer_entity,
|
||||
@@ -114,6 +117,7 @@ pub fn compute_observer_snapshot(
|
||||
mut scan_event_buffer_opt,
|
||||
mut conversation_buffer_opt,
|
||||
follow_target_opt,
|
||||
mut examine_result_buffer_opt,
|
||||
)) = observer_query.single_mut()
|
||||
else {
|
||||
tracing::error!("compute_observer_snapshot: PlayerCharacter query failed");
|
||||
@@ -199,6 +203,7 @@ pub fn compute_observer_snapshot(
|
||||
|
||||
let current_monologue = monologue_buffer.take();
|
||||
let dialogue_response = dialogue_response_opt.as_mut().and_then(|buf| buf.take());
|
||||
let examine_result = examine_result_buffer_opt.as_mut().and_then(|buf| buf.take());
|
||||
let scan_events = scan_event_buffer_opt
|
||||
.as_mut()
|
||||
.map(|buf| buf.take())
|
||||
@@ -391,6 +396,10 @@ pub fn compute_observer_snapshot(
|
||||
conversation_events,
|
||||
conversation_ended,
|
||||
follow_state,
|
||||
examine_result,
|
||||
character_pressure: pressure_query.iter().next().map(|p| {
|
||||
crate::simulation::pressure::CharacterPressureWire::from(p)
|
||||
}),
|
||||
sound_events,
|
||||
rng_seed: sim_rng.as_deref().map(|r| r.seed()),
|
||||
poi_list,
|
||||
|
||||
@@ -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