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:
@@ -4,7 +4,7 @@ use settled_reach_server::bridge::framing::{read_framed, write_framed};
|
||||
use settled_reach_server::bridge::local::LocalBridge;
|
||||
use settled_reach_server::bridge::types::*;
|
||||
use settled_reach_server::bridge::SimBridge;
|
||||
use settled_reach_server::simulation::time::DayPhase;
|
||||
use settled_reach_server::simulation::time::{DayPhase, TickRate};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::PathBuf;
|
||||
use std::thread;
|
||||
@@ -33,13 +33,14 @@ fn snapshot_roundtrip_over_unix_socket() {
|
||||
let bridge = LocalBridge::accept(&server_path).expect("failed to accept");
|
||||
|
||||
let snapshot = ObserverSnapshot {
|
||||
version: 3,
|
||||
version: 4,
|
||||
tick: 42,
|
||||
game_time: GameTime {
|
||||
day: 0,
|
||||
time_of_day: 0,
|
||||
day_phase: DayPhase::Morning,
|
||||
paused: false,
|
||||
tick_rate: TickRate::Full,
|
||||
},
|
||||
player_facing: FacingDirection::North,
|
||||
entities: vec![VisibleEntity {
|
||||
@@ -53,6 +54,7 @@ fn snapshot_roundtrip_over_unix_socket() {
|
||||
observation: EntityVisibility::Visible,
|
||||
}],
|
||||
visible_tiles: vec![],
|
||||
nearby_interactions: vec![],
|
||||
};
|
||||
|
||||
bridge
|
||||
|
||||
@@ -4,7 +4,7 @@ use settled_reach_server::bridge::framing::{read_framed, write_framed};
|
||||
use settled_reach_server::bridge::tcp::TcpBridge;
|
||||
use settled_reach_server::bridge::types::*;
|
||||
use settled_reach_server::bridge::SimBridge;
|
||||
use settled_reach_server::simulation::time::DayPhase;
|
||||
use settled_reach_server::simulation::time::{DayPhase, TickRate};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use std::thread;
|
||||
|
||||
@@ -19,13 +19,14 @@ fn snapshot_roundtrip_over_tcp() {
|
||||
let bridge = TcpBridge::accept_on(listener).expect("failed to accept");
|
||||
|
||||
let snapshot = ObserverSnapshot {
|
||||
version: 3,
|
||||
version: 4,
|
||||
tick: 42,
|
||||
game_time: GameTime {
|
||||
day: 0,
|
||||
time_of_day: 0,
|
||||
day_phase: DayPhase::Morning,
|
||||
paused: false,
|
||||
tick_rate: TickRate::Full,
|
||||
},
|
||||
player_facing: FacingDirection::North,
|
||||
entities: vec![VisibleEntity {
|
||||
@@ -39,6 +40,7 @@ fn snapshot_roundtrip_over_tcp() {
|
||||
observation: EntityVisibility::Visible,
|
||||
}],
|
||||
visible_tiles: vec![],
|
||||
nearby_interactions: vec![],
|
||||
};
|
||||
|
||||
bridge
|
||||
|
||||
@@ -61,7 +61,7 @@ fn player_moves_north_through_full_pipeline() {
|
||||
rmp_serde::from_slice(&response).expect("deserialize snapshot");
|
||||
|
||||
// Snapshot captures state at end of tick 0 (before advance_tick increments to 1)
|
||||
assert_eq!(snapshot.version, 3);
|
||||
assert_eq!(snapshot.version, 4);
|
||||
assert_eq!(snapshot.tick, 0);
|
||||
assert_eq!(snapshot.entities.len(), 1);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! Run with: cargo test --test gen_fixtures -- --ignored
|
||||
|
||||
use settled_reach_server::bridge::types::*;
|
||||
use settled_reach_server::simulation::time::DayPhase;
|
||||
use settled_reach_server::simulation::time::{DayPhase, TickRate};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -18,17 +18,19 @@ fn write_fixture(name: &str, bytes: &[u8]) {
|
||||
/// Helper to create a minimal v2 snapshot for fixtures
|
||||
fn fixture_snapshot(tick: u64, entities: Vec<VisibleEntity>) -> ObserverSnapshot {
|
||||
ObserverSnapshot {
|
||||
version: 3,
|
||||
version: 4,
|
||||
tick,
|
||||
game_time: GameTime {
|
||||
day: 0,
|
||||
time_of_day: 0,
|
||||
day_phase: DayPhase::Morning,
|
||||
paused: false,
|
||||
tick_rate: TickRate::Full,
|
||||
},
|
||||
player_facing: FacingDirection::North,
|
||||
entities,
|
||||
visible_tiles: vec![],
|
||||
nearby_interactions: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,13 +152,14 @@ fn generate_msgpack_fixtures() {
|
||||
|
||||
// v2 snapshot with visible_tiles and game_time populated
|
||||
let snapshot_v2_full = ObserverSnapshot {
|
||||
version: 3,
|
||||
version: 4,
|
||||
tick: 500,
|
||||
game_time: GameTime {
|
||||
day: 1,
|
||||
time_of_day: 720,
|
||||
day_phase: DayPhase::Evening,
|
||||
paused: false,
|
||||
tick_rate: TickRate::Full,
|
||||
},
|
||||
player_facing: FacingDirection::Southeast,
|
||||
entities: vec![VisibleEntity {
|
||||
@@ -189,6 +192,7 @@ fn generate_msgpack_fixtures() {
|
||||
visibility: VisibilitySector::Forward,
|
||||
},
|
||||
],
|
||||
nearby_interactions: vec![],
|
||||
};
|
||||
write_fixture(
|
||||
"snapshot_v2_full",
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
//! IPC serialization round-trip tests (D-030 Layer 1: fixture-based).
|
||||
|
||||
use settled_reach_server::bridge::types::*;
|
||||
use settled_reach_server::simulation::time::DayPhase;
|
||||
use settled_reach_server::simulation::time::{DayPhase, TickRate};
|
||||
use std::fs;
|
||||
|
||||
/// Helper to create a minimal v2 snapshot for tests
|
||||
fn test_snapshot(tick: u64, entities: Vec<VisibleEntity>) -> ObserverSnapshot {
|
||||
ObserverSnapshot {
|
||||
version: 3,
|
||||
version: 4,
|
||||
tick,
|
||||
game_time: GameTime {
|
||||
day: 0,
|
||||
time_of_day: 0,
|
||||
day_phase: DayPhase::Morning,
|
||||
paused: false,
|
||||
tick_rate: TickRate::Full,
|
||||
},
|
||||
player_facing: FacingDirection::North,
|
||||
entities,
|
||||
visible_tiles: vec![],
|
||||
nearby_interactions: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +42,7 @@ fn observer_snapshot_roundtrip() {
|
||||
let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize");
|
||||
let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize");
|
||||
|
||||
assert_eq!(decoded.version, 3);
|
||||
assert_eq!(decoded.version, 4);
|
||||
assert_eq!(decoded.tick, 42);
|
||||
assert_eq!(decoded.entities.len(), 1);
|
||||
assert_eq!(decoded.entities[0].entity_id, 1);
|
||||
@@ -178,13 +180,14 @@ fn all_entity_kind_variants_roundtrip() {
|
||||
#[test]
|
||||
fn snapshot_v2_fields_roundtrip() {
|
||||
let snapshot = ObserverSnapshot {
|
||||
version: 3,
|
||||
version: 4,
|
||||
tick: 100,
|
||||
game_time: GameTime {
|
||||
day: 3,
|
||||
time_of_day: 720,
|
||||
day_phase: DayPhase::Evening,
|
||||
paused: true,
|
||||
tick_rate: TickRate::Paused,
|
||||
},
|
||||
player_facing: FacingDirection::Southeast,
|
||||
entities: vec![VisibleEntity {
|
||||
@@ -211,12 +214,13 @@ fn snapshot_v2_fields_roundtrip() {
|
||||
visibility: VisibilitySector::Peripheral,
|
||||
},
|
||||
],
|
||||
nearby_interactions: vec![],
|
||||
};
|
||||
|
||||
let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize");
|
||||
let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize");
|
||||
|
||||
assert_eq!(decoded.version, 3);
|
||||
assert_eq!(decoded.version, 4);
|
||||
assert_eq!(decoded.game_time.day, 3);
|
||||
assert_eq!(decoded.game_time.time_of_day, 720);
|
||||
assert_eq!(decoded.game_time.day_phase, DayPhase::Evening);
|
||||
@@ -244,17 +248,19 @@ fn all_facing_direction_variants_roundtrip() {
|
||||
|
||||
for dir in directions {
|
||||
let snapshot = ObserverSnapshot {
|
||||
version: 3,
|
||||
version: 4,
|
||||
tick: 0,
|
||||
game_time: GameTime {
|
||||
day: 0,
|
||||
time_of_day: 0,
|
||||
day_phase: DayPhase::Morning,
|
||||
paused: false,
|
||||
tick_rate: TickRate::Full,
|
||||
},
|
||||
player_facing: dir,
|
||||
entities: vec![],
|
||||
visible_tiles: vec![],
|
||||
nearby_interactions: vec![],
|
||||
};
|
||||
let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize");
|
||||
let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize");
|
||||
|
||||
Reference in New Issue
Block a user