feat(perception): add cognitive delay system for fog recognition (#423)
New CognitiveDelay component buffers perception events before emitting KnowledgeEvents. Normal delay: 6 ticks (0.6s), urgent: 3 ticks (0.3s). Drain system runs after emit_observation_events, before process_knowledge_events. Adds pending_recognitions to ObserverSnapshot (protocol v7) for client fog entity visualization. Includes cancellation when entity leaves LOS. Monologue fires during delay (not after) per D-060 — cross-system wiring deferred to follow-up ticket #451. Ref: D-060 (cognitive delay), D-031 (10 tps), D-059 (fog layers) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ use std::collections::HashSet;
|
||||
use crate::bridge::types::*;
|
||||
use crate::knowledge::types::KnowledgeState;
|
||||
use crate::knowledge::{EntityRegistry, KnowledgeGraph, StableId};
|
||||
use crate::perception::cognitive_delay::CognitiveDelay;
|
||||
use crate::perception::query::{ActivePerceptionMode, VisibilityGeometry};
|
||||
use crate::perception::vision_cone::Facing;
|
||||
use crate::simulation::interaction::NearbyInteractionBuffer;
|
||||
@@ -53,7 +54,18 @@ pub fn compute_observer_snapshot(
|
||||
geometry: Res<VisibilityGeometry>,
|
||||
registry: Res<EntityRegistry>,
|
||||
mut observer_query: Query<
|
||||
(Entity, &TilePosition, Option<&Facing>, &KnowledgeGraph, &mut NearbyInteractionBuffer, &mut MonologueBuffer, Option<&Stance>, Option<&CharacterArchetype>, Option<&mut SprintAnomalyQueue>),
|
||||
(
|
||||
Entity,
|
||||
&TilePosition,
|
||||
Option<&Facing>,
|
||||
&KnowledgeGraph,
|
||||
&mut NearbyInteractionBuffer,
|
||||
&mut MonologueBuffer,
|
||||
Option<&Stance>,
|
||||
Option<&CharacterArchetype>,
|
||||
Option<&mut SprintAnomalyQueue>,
|
||||
Option<&CognitiveDelay>,
|
||||
),
|
||||
With<PlayerCharacter>,
|
||||
>,
|
||||
all_entities: Query<(
|
||||
@@ -65,8 +77,18 @@ pub fn compute_observer_snapshot(
|
||||
inventory_items: Query<(Entity, &CarriedBy, &ItemName, &InventorySlot)>,
|
||||
mut buffer: ResMut<SnapshotBuffer>,
|
||||
) {
|
||||
let Ok((observer_entity, _observer_pos, facing_opt, observer_kg, mut interaction_buffer, mut monologue_buffer, stance_opt, archetype_opt, mut anomaly_queue_opt)) =
|
||||
observer_query.single_mut()
|
||||
let Ok((
|
||||
observer_entity,
|
||||
_observer_pos,
|
||||
facing_opt,
|
||||
observer_kg,
|
||||
mut interaction_buffer,
|
||||
mut monologue_buffer,
|
||||
stance_opt,
|
||||
archetype_opt,
|
||||
mut anomaly_queue_opt,
|
||||
cognitive_delay_opt,
|
||||
)) = observer_query.single_mut()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -141,6 +163,27 @@ pub fn compute_observer_snapshot(
|
||||
|
||||
let current_monologue = monologue_buffer.take();
|
||||
|
||||
// Build pending recognitions from CognitiveDelay (#423, D-060)
|
||||
let pending_recognitions = cognitive_delay_opt
|
||||
.map(|delay| {
|
||||
delay
|
||||
.pending()
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let (rx, ry, rz) = p.position.to_render_coords();
|
||||
PendingRecognitionWire {
|
||||
entity_id: p.stable_id.0,
|
||||
x: rx,
|
||||
y: ry,
|
||||
z: rz,
|
||||
remaining_ticks: p.delay_until_tick.saturating_sub(time.tick),
|
||||
total_delay_ticks: p.trigger.delay_ticks(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
buffer.snapshot = Some(ObserverSnapshot {
|
||||
version: crate::bridge::types::PROTOCOL_VERSION,
|
||||
tick: time.tick,
|
||||
@@ -152,6 +195,7 @@ pub fn compute_observer_snapshot(
|
||||
visible_tiles: geometry.visible_tiles.clone(),
|
||||
nearby_interactions,
|
||||
current_monologue,
|
||||
pending_recognitions,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -360,13 +404,16 @@ fn apply_phase2_verb_filter(
|
||||
// This implements D-057: "Character differentiation via Phase 2 observer
|
||||
// filter, not separate verb systems."
|
||||
for verb in &mut interaction.verbs {
|
||||
if let Some(label) = archetype_verb_label(archetype, interaction.object_type, verb.kind) {
|
||||
if let Some(label) = archetype_verb_label(archetype, interaction.object_type, verb.kind)
|
||||
{
|
||||
verb.label = label.into();
|
||||
}
|
||||
}
|
||||
|
||||
// Re-sort after priority changes and verb additions
|
||||
interaction.verbs.sort_by_key(|v| (v.priority, v.kind as u8));
|
||||
interaction
|
||||
.verbs
|
||||
.sort_by_key(|v| (v.priority, v.kind as u8));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,11 +432,17 @@ fn archetype_verb_label(
|
||||
match (archetype, object_type, kind) {
|
||||
// Smuggler: Container verbs — physical manipulation vocabulary
|
||||
(CharacterArchetype::Smuggler, Some(ObjectType::Container), VerbKind::Open) => Some("Move"),
|
||||
(CharacterArchetype::Smuggler, Some(ObjectType::Container), VerbKind::Search) => Some("Stash"),
|
||||
(CharacterArchetype::Smuggler, Some(ObjectType::Container), VerbKind::Search) => {
|
||||
Some("Stash")
|
||||
}
|
||||
|
||||
// Detective: Container verbs — investigation vocabulary
|
||||
(CharacterArchetype::Detective, Some(ObjectType::Container), VerbKind::Open) => Some("Scan"),
|
||||
(CharacterArchetype::Detective, Some(ObjectType::Container), VerbKind::Search) => Some("Flag"),
|
||||
(CharacterArchetype::Detective, Some(ObjectType::Container), VerbKind::Open) => {
|
||||
Some("Scan")
|
||||
}
|
||||
(CharacterArchetype::Detective, Some(ObjectType::Container), VerbKind::Search) => {
|
||||
Some("Flag")
|
||||
}
|
||||
|
||||
// All other combinations: keep Phase 1 default label
|
||||
_ => None,
|
||||
|
||||
@@ -57,7 +57,7 @@ fn player_always_visible_in_snapshot() {
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().expect("snapshot should exist");
|
||||
assert_eq!(snapshot.version, 6);
|
||||
assert_eq!(snapshot.version, PROTOCOL_VERSION);
|
||||
assert_eq!(snapshot.entities.len(), 1);
|
||||
assert!(matches!(snapshot.entities[0].kind, EntityKind::Player));
|
||||
assert_eq!(snapshot.entities[0].observation, EntityVisibility::Visible);
|
||||
@@ -198,7 +198,10 @@ fn game_time_populated() {
|
||||
snapshot.game_time.day_phase,
|
||||
crate::simulation::time::DayPhase::Evening
|
||||
);
|
||||
assert_eq!(snapshot.game_time.tick_rate, crate::simulation::time::TickRate::Paused);
|
||||
assert_eq!(
|
||||
snapshot.game_time.tick_rate,
|
||||
crate::simulation::time::TickRate::Paused
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -298,7 +301,11 @@ fn remembered_entity_appears_as_ghost() {
|
||||
.id();
|
||||
registry.register(player);
|
||||
world.insert_resource(registry);
|
||||
world.insert_resource({ let mut t = SimulationTime::default(); t.tick = 100; t });
|
||||
world.insert_resource({
|
||||
let mut t = SimulationTime::default();
|
||||
t.tick = 100;
|
||||
t
|
||||
});
|
||||
|
||||
run_observer_pipeline(&mut world);
|
||||
|
||||
@@ -312,13 +319,20 @@ fn remembered_entity_appears_as_ghost() {
|
||||
.filter(|e| matches!(e.observation, EntityVisibility::Remembered { .. }))
|
||||
.collect();
|
||||
assert_eq!(remembered.len(), 1, "should have one remembered entity");
|
||||
assert_eq!(remembered[0].relationship, RelationshipState::PersonOfInterest);
|
||||
assert_eq!(
|
||||
remembered[0].relationship,
|
||||
RelationshipState::PersonOfInterest
|
||||
);
|
||||
|
||||
// Remembered entity at last_known_position (16, 28), not actual (16, 30)
|
||||
assert_eq!(remembered[0].x, 16.5);
|
||||
assert_eq!(remembered[0].y, 28.5);
|
||||
|
||||
if let EntityVisibility::Remembered { confidence, age_ticks } = &remembered[0].observation {
|
||||
if let EntityVisibility::Remembered {
|
||||
confidence,
|
||||
age_ticks,
|
||||
} = &remembered[0].observation
|
||||
{
|
||||
assert_eq!(*confidence, KnowledgeConfidence::KnowsDetails);
|
||||
assert_eq!(*age_ticks, 50); // tick 100 - last_observed 50
|
||||
}
|
||||
@@ -479,16 +493,19 @@ fn knowledge_without_position_not_shown() {
|
||||
// Player knows about NPC but has never seen it (no position)
|
||||
let mut kg = KnowledgeGraph::new();
|
||||
// Insert knowledge manually without a position
|
||||
kg.entities.insert(npc_sid, crate::knowledge::EntityKnowledge {
|
||||
last_known_position: None,
|
||||
last_observed_tick: 0,
|
||||
last_updated_tick: 50,
|
||||
confidence: KnowledgeConfidence::KnowsOf,
|
||||
source: crate::knowledge::KnowledgeSource::Background,
|
||||
state: crate::knowledge::KnowledgeState::Active,
|
||||
relationship: RelationshipState::PersonOfInterest,
|
||||
known_attributes: std::collections::BTreeMap::new(),
|
||||
});
|
||||
kg.entities.insert(
|
||||
npc_sid,
|
||||
crate::knowledge::EntityKnowledge {
|
||||
last_known_position: None,
|
||||
last_observed_tick: 0,
|
||||
last_updated_tick: 50,
|
||||
confidence: KnowledgeConfidence::KnowsOf,
|
||||
source: crate::knowledge::KnowledgeSource::Background,
|
||||
state: crate::knowledge::KnowledgeState::Active,
|
||||
relationship: RelationshipState::PersonOfInterest,
|
||||
known_attributes: std::collections::BTreeMap::new(),
|
||||
},
|
||||
);
|
||||
|
||||
let player = world
|
||||
.spawn((
|
||||
@@ -547,7 +564,9 @@ fn multiple_npcs_in_los_all_visible() {
|
||||
.filter(|e| matches!(e.kind, EntityKind::Npc))
|
||||
.collect();
|
||||
assert_eq!(npcs.len(), 3);
|
||||
assert!(npcs.iter().all(|n| n.observation == EntityVisibility::Visible));
|
||||
assert!(npcs
|
||||
.iter()
|
||||
.all(|n| n.observation == EntityVisibility::Visible));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -657,9 +676,19 @@ fn snapshot_v6_fields_default_through_pipeline() {
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().expect("snapshot should exist");
|
||||
|
||||
assert_eq!(snapshot.version, 6, "should be protocol v6");
|
||||
assert_eq!(snapshot.player_stance, MovementStance::Walk, "default stance is Walk");
|
||||
assert!(snapshot.player_inventory.is_empty(), "default inventory is empty");
|
||||
assert_eq!(
|
||||
snapshot.version, PROTOCOL_VERSION,
|
||||
"should be current protocol version"
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.player_stance,
|
||||
MovementStance::Walk,
|
||||
"default stance is Walk"
|
||||
);
|
||||
assert!(
|
||||
snapshot.player_inventory.is_empty(),
|
||||
"default inventory is empty"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -731,8 +760,14 @@ fn phase2_confront_injected_for_npc_with_knows_details() {
|
||||
let interaction = &snapshot.nearby_interactions[0];
|
||||
// Should have Talk, ExamineNpc, AND Confront (Phase 2 injected)
|
||||
assert_eq!(interaction.verbs.len(), 3);
|
||||
let confront = interaction.verbs.iter().find(|v| v.kind == VerbKind::Confront);
|
||||
assert!(confront.is_some(), "Confront should be injected for KnowsDetails+");
|
||||
let confront = interaction
|
||||
.verbs
|
||||
.iter()
|
||||
.find(|v| v.kind == VerbKind::Confront);
|
||||
assert!(
|
||||
confront.is_some(),
|
||||
"Confront should be injected for KnowsDetails+"
|
||||
);
|
||||
assert_eq!(confront.unwrap().priority, 3);
|
||||
assert_eq!(confront.unwrap().label, "Confront");
|
||||
}
|
||||
@@ -753,16 +788,19 @@ fn phase2_no_confront_without_knows_details() {
|
||||
|
||||
// Player only Suspects this NPC (below KnowsDetails threshold)
|
||||
let mut kg = KnowledgeGraph::new();
|
||||
kg.entities.insert(npc_sid, crate::knowledge::EntityKnowledge {
|
||||
last_known_position: Some(TilePosition::new(16, 15, 0)),
|
||||
last_observed_tick: 50,
|
||||
last_updated_tick: 50,
|
||||
confidence: KnowledgeConfidence::Suspects,
|
||||
source: crate::knowledge::KnowledgeSource::Background,
|
||||
state: KnowledgeState::Active,
|
||||
relationship: RelationshipState::Unknown,
|
||||
known_attributes: std::collections::BTreeMap::new(),
|
||||
});
|
||||
kg.entities.insert(
|
||||
npc_sid,
|
||||
crate::knowledge::EntityKnowledge {
|
||||
last_known_position: Some(TilePosition::new(16, 15, 0)),
|
||||
last_observed_tick: 50,
|
||||
last_updated_tick: 50,
|
||||
confidence: KnowledgeConfidence::Suspects,
|
||||
source: crate::knowledge::KnowledgeSource::Background,
|
||||
state: KnowledgeState::Active,
|
||||
relationship: RelationshipState::Unknown,
|
||||
known_attributes: std::collections::BTreeMap::new(),
|
||||
},
|
||||
);
|
||||
|
||||
let player = world
|
||||
.spawn((
|
||||
@@ -783,8 +821,14 @@ fn phase2_no_confront_without_knows_details() {
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
assert_eq!(snapshot.nearby_interactions.len(), 1);
|
||||
let interaction = &snapshot.nearby_interactions[0];
|
||||
let confront = interaction.verbs.iter().find(|v| v.kind == VerbKind::Confront);
|
||||
assert!(confront.is_none(), "Confront should NOT appear for Suspects confidence");
|
||||
let confront = interaction
|
||||
.verbs
|
||||
.iter()
|
||||
.find(|v| v.kind == VerbKind::Confront);
|
||||
assert!(
|
||||
confront.is_none(),
|
||||
"Confront should NOT appear for Suspects confidence"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -827,7 +871,10 @@ fn phase2_no_confront_at_mid_range() {
|
||||
assert_eq!(snapshot.nearby_interactions.len(), 1);
|
||||
let interaction = &snapshot.nearby_interactions[0];
|
||||
// Mid range: only ExamineNpc, no Talk, no Confront
|
||||
let confront = interaction.verbs.iter().find(|v| v.kind == VerbKind::Confront);
|
||||
let confront = interaction
|
||||
.verbs
|
||||
.iter()
|
||||
.find(|v| v.kind == VerbKind::Confront);
|
||||
assert!(confront.is_none(), "Confront requires close range");
|
||||
}
|
||||
|
||||
@@ -954,8 +1001,14 @@ fn phase2_smuggler_relabels_container_verbs() {
|
||||
let interaction = &snapshot.nearby_interactions[0];
|
||||
// Container at close range: Open→"Move", Search→"Stash", Observe stays "Observe"
|
||||
let open_verb = interaction.verbs.iter().find(|v| v.kind == VerbKind::Open);
|
||||
let search_verb = interaction.verbs.iter().find(|v| v.kind == VerbKind::Search);
|
||||
let observe_verb = interaction.verbs.iter().find(|v| v.kind == VerbKind::Observe);
|
||||
let search_verb = interaction
|
||||
.verbs
|
||||
.iter()
|
||||
.find(|v| v.kind == VerbKind::Search);
|
||||
let observe_verb = interaction
|
||||
.verbs
|
||||
.iter()
|
||||
.find(|v| v.kind == VerbKind::Observe);
|
||||
assert_eq!(open_verb.unwrap().label, "Move", "smuggler Open→Move");
|
||||
assert_eq!(search_verb.unwrap().label, "Stash", "smuggler Search→Stash");
|
||||
assert_eq!(observe_verb.unwrap().label, "Observe", "Observe unchanged");
|
||||
@@ -997,7 +1050,10 @@ fn phase2_detective_relabels_container_verbs() {
|
||||
assert_eq!(snapshot.nearby_interactions.len(), 1);
|
||||
let interaction = &snapshot.nearby_interactions[0];
|
||||
let open_verb = interaction.verbs.iter().find(|v| v.kind == VerbKind::Open);
|
||||
let search_verb = interaction.verbs.iter().find(|v| v.kind == VerbKind::Search);
|
||||
let search_verb = interaction
|
||||
.verbs
|
||||
.iter()
|
||||
.find(|v| v.kind == VerbKind::Search);
|
||||
assert_eq!(open_verb.unwrap().label, "Scan", "detective Open→Scan");
|
||||
assert_eq!(search_verb.unwrap().label, "Flag", "detective Search→Flag");
|
||||
}
|
||||
@@ -1039,7 +1095,11 @@ fn phase2_default_archetype_is_detective() {
|
||||
let interaction = &snapshot.nearby_interactions[0];
|
||||
// Default = Detective labels
|
||||
let open_verb = interaction.verbs.iter().find(|v| v.kind == VerbKind::Open);
|
||||
assert_eq!(open_verb.unwrap().label, "Scan", "default archetype should use Detective labels");
|
||||
assert_eq!(
|
||||
open_verb.unwrap().label,
|
||||
"Scan",
|
||||
"default archetype should use Detective labels"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1078,7 +1138,11 @@ fn phase2_non_container_keeps_default_labels() {
|
||||
assert_eq!(snapshot.nearby_interactions.len(), 1);
|
||||
let interaction = &snapshot.nearby_interactions[0];
|
||||
let read_verb = interaction.verbs.iter().find(|v| v.kind == VerbKind::Read);
|
||||
assert_eq!(read_verb.unwrap().label, "Read", "Readable labels unchanged for smuggler");
|
||||
assert_eq!(
|
||||
read_verb.unwrap().label,
|
||||
"Read",
|
||||
"Readable labels unchanged for smuggler"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1155,8 +1219,7 @@ fn phase2_npc_object_type_is_none() {
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
assert_eq!(snapshot.nearby_interactions.len(), 1);
|
||||
assert_eq!(
|
||||
snapshot.nearby_interactions[0].object_type,
|
||||
None,
|
||||
snapshot.nearby_interactions[0].object_type, None,
|
||||
"NPC should have object_type=None"
|
||||
);
|
||||
}
|
||||
@@ -1256,7 +1319,11 @@ fn phase2_poi_with_confront_verb_order() {
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
assert_eq!(snapshot.nearby_interactions.len(), 1);
|
||||
let verbs = &snapshot.nearby_interactions[0].verbs;
|
||||
assert_eq!(verbs.len(), 3, "POI+KnowsDetails: ExamineNpc + Talk + Confront");
|
||||
assert_eq!(
|
||||
verbs.len(),
|
||||
3,
|
||||
"POI+KnowsDetails: ExamineNpc + Talk + Confront"
|
||||
);
|
||||
// POI flips ExamineNpc to priority 1, Talk to 2, Confront at 3
|
||||
assert_eq!(verbs[0].kind, VerbKind::ExamineNpc);
|
||||
assert_eq!(verbs[0].priority, 1);
|
||||
@@ -1306,7 +1373,11 @@ fn carried_item_appears_in_snapshot_inventory() {
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
assert_eq!(snapshot.player_inventory.len(), 1, "carried item should appear in snapshot");
|
||||
assert_eq!(
|
||||
snapshot.player_inventory.len(),
|
||||
1,
|
||||
"carried item should appear in snapshot"
|
||||
);
|
||||
assert_eq!(snapshot.player_inventory[0].name, "Manifest Copy");
|
||||
assert_eq!(snapshot.player_inventory[0].slot, 0);
|
||||
}
|
||||
@@ -1380,7 +1451,11 @@ fn multiple_carried_items_sorted_in_snapshot() {
|
||||
let player_sid = registry.register(player);
|
||||
|
||||
// Spawn 3 v0.1 items in reverse slot order
|
||||
for (slot, name) in [(2, "Personal Comm Log"), (0, "Manifest Copy"), (1, "Corridor Access Token")] {
|
||||
for (slot, name) in [
|
||||
(2, "Personal Comm Log"),
|
||||
(0, "Manifest Copy"),
|
||||
(1, "Corridor Access Token"),
|
||||
] {
|
||||
let item = world
|
||||
.spawn((
|
||||
CarriedBy(player_sid),
|
||||
@@ -1449,7 +1524,10 @@ fn sprint_past_contradicted_npc_queues_anomaly() {
|
||||
// Anomaly should be queued
|
||||
let mut query = world.query::<&SprintAnomalyQueue>();
|
||||
let queue = query.single(&world).unwrap();
|
||||
assert!(queue.has_pending(), "contradicted NPC while sprinting should queue anomaly");
|
||||
assert!(
|
||||
queue.has_pending(),
|
||||
"contradicted NPC while sprinting should queue anomaly"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1488,7 +1566,10 @@ fn walk_past_contradicted_npc_does_not_queue_anomaly() {
|
||||
|
||||
let mut query = world.query::<&SprintAnomalyQueue>();
|
||||
let queue = query.single(&world).unwrap();
|
||||
assert!(!queue.has_pending(), "walking past contradicted NPC should NOT queue anomaly");
|
||||
assert!(
|
||||
!queue.has_pending(),
|
||||
"walking past contradicted NPC should NOT queue anomaly"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1527,7 +1608,10 @@ fn sprint_past_active_npc_does_not_queue_anomaly() {
|
||||
|
||||
let mut query = world.query::<&SprintAnomalyQueue>();
|
||||
let queue = query.single(&world).unwrap();
|
||||
assert!(!queue.has_pending(), "sprint past Active NPC should NOT queue anomaly");
|
||||
assert!(
|
||||
!queue.has_pending(),
|
||||
"sprint past Active NPC should NOT queue anomaly"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1612,7 +1696,10 @@ fn sprint_anomaly_without_queue_component_no_crash() {
|
||||
run_observer_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
assert!(buffer.snapshot.is_some(), "snapshot should still be produced");
|
||||
assert!(
|
||||
buffer.snapshot.is_some(),
|
||||
"snapshot should still be produced"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1667,12 +1754,18 @@ fn sprint_anomaly_npc_visible_but_interactions_suppressed() {
|
||||
assert_eq!(npcs.len(), 1, "NPC should be visible during sprint");
|
||||
|
||||
// Interactions should be empty (sprint suppression)
|
||||
assert!(snapshot.nearby_interactions.is_empty(), "sprint suppresses interactions");
|
||||
assert!(
|
||||
snapshot.nearby_interactions.is_empty(),
|
||||
"sprint suppresses interactions"
|
||||
);
|
||||
|
||||
// Anomaly should be queued
|
||||
let mut query = world.query::<&SprintAnomalyQueue>();
|
||||
let queue = query.single(&world).unwrap();
|
||||
assert!(queue.has_pending(), "anomaly should be queued despite interaction suppression");
|
||||
assert!(
|
||||
queue.has_pending(),
|
||||
"anomaly should be queued despite interaction suppression"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user