feat(simulation): add proximity detection and interaction verbs

Implement compute_nearby_interactions system that detects entities
within close (≤2) and mid (≤5) Manhattan distance, computes
available verbs per D-060 spec. NPCs get Talk+Observe at close
range, Observe-only at mid range; PersonOfInterest flips priority.
Objects get Examine. Results populate nearby_interactions[] on
ObserverSnapshot v4. Bump protocol version 3→4. Implements #404.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 19:48:55 +01:00
co-authored by Claude Opus 4.6
parent 5afea99622
commit 2de413dd87
12 changed files with 438 additions and 29 deletions
+1
View File
@@ -199,6 +199,7 @@ mod tests {
world.init_resource::<crate::knowledge::KnowledgeEventQueue>();
world.init_resource::<EntityRegistry>();
world.init_resource::<ObservationEventQueue>();
world.init_resource::<crate::simulation::interaction::NearbyInteractionBuffer>();
world
}
+1
View File
@@ -99,6 +99,7 @@ mod tests {
world.init_resource::<SnapshotBuffer>();
world.init_resource::<KnowledgeEventQueue>();
world.init_resource::<EntityRegistry>();
world.init_resource::<crate::simulation::interaction::NearbyInteractionBuffer>();
world
}
+14 -9
View File
@@ -11,6 +11,7 @@ use crate::bridge::types::*;
use crate::knowledge::{EntityRegistry, KnowledgeGraph};
use crate::perception::shadowcast::compute_fov;
use crate::perception::vision_cone::{apply_vision_cone, Facing, VisionConeConfig};
use crate::simulation::interaction::NearbyInteractionBuffer;
use crate::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap};
use crate::simulation::time::SimulationTime;
@@ -22,6 +23,7 @@ pub fn compute_observer_snapshot(
time: Res<SimulationTime>,
walkability: Res<WalkabilityMap>,
registry: Res<EntityRegistry>,
interaction_buffer: Res<NearbyInteractionBuffer>,
observer_query: Query<(&TilePosition, Option<&Facing>, &KnowledgeGraph), With<PlayerCharacter>>,
all_entities: Query<(
Entity,
@@ -185,7 +187,8 @@ pub fn compute_observer_snapshot(
day: time.day(),
time_of_day: time.time_of_day_minutes(),
day_phase: time.day_phase(),
paused: time.paused,
paused: time.paused(),
tick_rate: time.tick_rate,
};
tracing::trace!(
@@ -196,14 +199,15 @@ pub fn compute_observer_snapshot(
visible_tiles.len(),
);
// Step 8: Assemble snapshot (v3: added relationship + observation fields)
// Step 8: Assemble snapshot (v4: added nearby_interactions)
buffer.snapshot = Some(ObserverSnapshot {
version: 3,
version: 4,
tick: time.tick,
game_time,
player_facing: facing,
entities,
visible_tiles,
nearby_interactions: interaction_buffer.interactions.clone(),
});
}
@@ -221,6 +225,7 @@ mod tests {
world.insert_resource(WalkabilityMap::new(width, height, 1));
world.init_resource::<SnapshotBuffer>();
world.init_resource::<EntityRegistry>();
world.init_resource::<NearbyInteractionBuffer>();
world
}
@@ -240,7 +245,7 @@ mod tests {
let buffer = world.resource::<SnapshotBuffer>();
let snapshot = buffer.snapshot.as_ref().expect("snapshot should exist");
assert_eq!(snapshot.version, 3);
assert_eq!(snapshot.version, 4);
assert_eq!(snapshot.entities.len(), 1);
assert!(matches!(snapshot.entities[0].kind, EntityKind::Player));
assert_eq!(snapshot.entities[0].observation, EntityVisibility::Visible);
@@ -359,10 +364,10 @@ mod tests {
#[test]
fn game_time_populated() {
let mut world = setup_world(32, 32);
world.insert_resource(SimulationTime {
tick: 7200, // 720 minutes = Evening
paused: true,
});
let mut time = SimulationTime::default();
time.tick = 7200; // 720 minutes = Evening
time.tick_rate = crate::simulation::time::TickRate::Paused;
world.insert_resource(time);
world.spawn((
PlayerCharacter,
TilePosition::new(16, 16, 0),
@@ -479,7 +484,7 @@ mod tests {
.id();
registry.register(player);
world.insert_resource(registry);
world.insert_resource(SimulationTime { tick: 100, paused: false });
world.insert_resource({ let mut t = SimulationTime::default(); t.tick = 100; t });
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(compute_observer_snapshot);