From aa79dd97e7b03d131f5c5834b65d9b097330a5f2 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Tue, 17 Mar 2026 10:33:15 +0100 Subject: [PATCH] fix(simulation): Clippy cleanup and CI enforcement (#635) Fix all Clippy warnings across the server codebase (2411 insertions, 1341 deletions). Raise type-complexity-threshold to 750 and too-many-arguments to 12 in .clippy.toml for idiomatic Bevy ECS system signatures. The server now passes `cargo clippy -- --deny warnings` cleanly. Co-Authored-By: Claude Opus 4.6 (1M context) --- server/.clippy.toml | 6 + server/Cargo.lock | 2 +- server/src/bridge/debug.rs | 189 ++++++++++----- server/src/bridge/mod.rs | 9 +- server/src/bridge/tcp.rs | 10 +- server/src/bridge/types.rs | 22 +- server/src/knowledge/events.rs | 3 +- server/src/knowledge/graph.rs | 44 +++- server/src/knowledge/types.rs | 9 +- server/src/main.rs | 32 ++- server/src/npc/awareness.rs | 11 +- server/src/npc/background.rs | 92 +++++-- server/src/npc/disclosure.rs | 109 +++++++-- server/src/npc/generate.rs | 69 ++++-- server/src/npc/interaction.rs | 5 +- server/src/npc/mod.rs | 1 - server/src/npc/mood.rs | 26 +- server/src/npc/relationships.rs | 143 ++++++----- server/src/npc/routine.rs | 22 +- server/src/npc/tell_state.rs | 35 ++- server/src/npc/tolerance.rs | 13 +- server/src/npc/trait_modifiers.rs | 54 +++-- server/src/npc/vision.rs | 22 +- server/src/perception/observer/mod.rs | 36 +-- server/src/perception/observer/tests.rs | 34 ++- server/src/settings/mod.rs | 13 +- server/src/settings/store.rs | 28 ++- server/src/simulation/chunk_streaming.rs | 2 +- server/src/simulation/conversation.rs | 72 +++--- server/src/simulation/dialogue.rs | 60 +++-- server/src/simulation/examine.rs | 121 ++++++---- server/src/simulation/follow.rs | 7 +- server/src/simulation/generator.rs | 7 +- server/src/simulation/input.rs | 226 ++++++++++-------- server/src/simulation/interaction.rs | 15 +- server/src/simulation/knowledge_grant.rs | 5 +- server/src/simulation/line_pool.rs | 89 +++++-- server/src/simulation/mod.rs | 2 +- server/src/simulation/monologue.rs | 197 ++++++++------- server/src/simulation/movement.rs | 8 +- .../src/simulation/npc_knowledge_transfer.rs | 109 +++++---- server/src/simulation/path_follow.rs | 5 +- server/src/simulation/poi.rs | 38 ++- server/src/simulation/poi_discovery.rs | 29 ++- server/src/simulation/pressure.rs | 15 +- server/src/simulation/save_io.rs | 42 ++-- server/src/simulation/save_state.rs | 83 ++++--- server/src/simulation/sound.rs | 21 +- server/src/simulation/spatial.rs | 39 ++- server/src/simulation/tier.rs | 221 +++++++++++------ server/src/simulation/triangle.rs | 53 ++-- server/src/storyteller/mod.rs | 172 +++++++------ server/src/test_world/constants.rs | 6 +- server/src/test_world/invariants.rs | 161 +++++++------ server/src/test_world/mod.rs | 46 ++-- server/src/test_world/rooms/shift_change.rs | 7 +- server/src/test_world/rooms/sound_lab.rs | 28 ++- server/src/test_world/rooms/zone_gate.rs | 34 ++- server/src/voice/cache.rs | 18 +- server/src/voice/hardware.rs | 21 +- server/src/voice/lookup.rs | 33 ++- server/src/voice/prompt_builder.rs | 18 +- server/src/voice/queue.rs | 6 + server/src/voice/worker.rs | 8 +- server/tests/bridge_ipc.rs | 4 +- server/tests/bridge_tcp.rs | 4 +- server/tests/contamination.rs | 22 +- server/tests/determinism.rs | 8 +- server/tests/environmental_interaction.rs | 94 ++++---- server/tests/error_handling.rs | 36 +-- server/tests/fuzzy_map.rs | 13 +- server/tests/game_loop.rs | 2 +- server/tests/gen_gauntlet_fixtures.rs | 9 +- server/tests/information_boundaries.rs | 32 +-- server/tests/ipc_bench.rs | 6 +- server/tests/news_ticker.rs | 56 +++-- server/tests/perf_bench.rs | 3 +- server/tests/serialization.rs | 26 +- server/tests/tell_escalation.rs | 26 +- server/tests/triangle_escalation.rs | 168 ++++++++++--- server/tests/triangle_validation.rs | 75 +++--- server/tests/v01_integration_playthrough.rs | 69 ++++-- server/tests/voice_pipeline.rs | 14 +- server/tests/voice_quality_batch.rs | 18 +- 84 files changed, 2409 insertions(+), 1339 deletions(-) diff --git a/server/.clippy.toml b/server/.clippy.toml index 88f1e5b57..676d3c391 100644 --- a/server/.clippy.toml +++ b/server/.clippy.toml @@ -7,3 +7,9 @@ disallowed-types = [ { path = "std::collections::HashMap", reason = "HashMap iteration order is non-deterministic. Use BTreeMap or IndexMap for deterministic simulation." }, { path = "std::collections::HashSet", reason = "HashSet iteration order is non-deterministic. Use BTreeSet or IndexSet." }, ] + +# Bevy ECS system functions naturally accumulate many Res<>/Query<> parameters and +# complex Query type signatures. These thresholds are tuned to allow idiomatic +# Bevy system code while still catching genuinely problematic complexity elsewhere. +type-complexity-threshold = 750 +too-many-arguments-threshold = 12 diff --git a/server/Cargo.lock b/server/Cargo.lock index 26eabff95..364fc1454 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "settled-reach-server" -version = "0.1.25" +version = "0.1.26" dependencies = [ "bevy_app", "bevy_ecs", diff --git a/server/src/bridge/debug.rs b/server/src/bridge/debug.rs index 02991e115..2a78f23aa 100644 --- a/server/src/bridge/debug.rs +++ b/server/src/bridge/debug.rs @@ -8,16 +8,14 @@ use bevy_ecs::prelude::*; -use crate::bridge::types::{ - DebugCommandKind, DebugEnabled, DebugResponsePayload, SnapshotBuffer, -}; -use crate::simulation::triangle::TriangleState; +use crate::bridge::types::{DebugCommandKind, DebugEnabled, DebugResponsePayload, SnapshotBuffer}; use crate::knowledge::EntityRegistry; use crate::npc::Npc; use crate::simulation::conversation::NpcName; use crate::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap}; use crate::simulation::tier::ActiveSim; use crate::simulation::time::SimulationTime; +use crate::simulation::triangle::TriangleState; use crate::storyteller::{ContaminationActive, ContaminationEventQueue, CONTAMINATION_DELAY_TICKS}; // --------------------------------------------------------------------------- @@ -67,10 +65,13 @@ pub fn handle_debug_commands( registry: Res, mut player_query: Query<(Entity, &mut TilePosition), With>, triangles: Query<(Entity, &TriangleState), With>, - npcs: Query<(Entity, &TilePosition, Option<&NpcName>), (With, With, Without)>, + npcs: Query< + (Entity, &TilePosition, Option<&NpcName>), + (With, With, Without), + >, ) { // Gate: debug must be enabled - let enabled = debug_enabled.as_ref().map_or(false, |d| d.0); + let enabled = debug_enabled.as_ref().is_some_and(|d| d.0); let commands = cmd_buffer.drain(); if commands.is_empty() { return; @@ -91,10 +92,7 @@ pub fn handle_debug_commands( time.tick = time.tick.saturating_add(n); DebugResponsePayload { command: format!("AdvanceTicks({})", n), - text: format!( - "Advanced {} ticks: {} -> {}", - n, old_tick, time.tick - ), + text: format!("Advanced {} ticks: {} -> {}", n, old_tick, time.tick), success: true, } } @@ -145,7 +143,7 @@ pub fn handle_debug_commands( // Validate target is walkable (if WalkabilityMap available) let walkable = walkability .as_ref() - .map_or(true, |wm| wm.can_move_to(&target)); + .is_none_or(|wm| wm.can_move_to(&target)); if !walkable { DebugResponsePayload { command: format!("TeleportToPosition({}, {}, {})", x, y, z), @@ -181,42 +179,42 @@ pub fn handle_debug_commands( // center from tile_bounds). For now, report unimplemented. DebugResponsePayload { command: format!("TeleportToLocation({})", name), - text: format!( - "TeleportToLocation not yet implemented (needs location tile_bounds from ContentStore). Use TeleportToPosition instead." - ), + text: "TeleportToLocation not yet implemented (needs location tile_bounds from ContentStore). Use TeleportToPosition instead.".to_string(), success: false, } } DebugCommandKind::ForceContaminationActivate => { - if contamination.is_none() { + if let Some(ref mut cont) = contamination { + if cont.0 { + DebugResponsePayload { + command: "ForceContaminationActivate".to_string(), + text: "Contamination already active.".to_string(), + success: true, + } + } else { + cont.0 = true; + // Also push an event so downstream systems react + if let Some(ref mut queue) = contamination_queue { + queue.push(crate::storyteller::ContaminationEvent { + tick: time.tick, + triangles_affected: 0, // no pressure delta applied — use SkipToContamination for that + }); + } + DebugResponsePayload { + command: "ForceContaminationActivate".to_string(), + text: format!( + "Contamination force-activated at tick {}. Note: no tension delta applied (use SkipToContamination for full effect).", + time.tick + ), + success: true, + } + } + } else { DebugResponsePayload { command: "ForceContaminationActivate".to_string(), text: "Contamination system not available.".to_string(), success: false, } - } else if contamination.as_ref().unwrap().0 { - DebugResponsePayload { - command: "ForceContaminationActivate".to_string(), - text: "Contamination already active.".to_string(), - success: true, - } - } else { - contamination.as_mut().unwrap().0 = true; - // Also push an event so downstream systems react - if let Some(ref mut queue) = contamination_queue { - queue.push(crate::storyteller::ContaminationEvent { - tick: time.tick, - triangles_affected: 0, // no pressure delta applied — use SkipToContamination for that - }); - } - DebugResponsePayload { - command: "ForceContaminationActivate".to_string(), - text: format!( - "Contamination force-activated at tick {}. Note: no tension delta applied (use SkipToContamination for full effect).", - time.tick - ), - success: true, - } } } DebugCommandKind::ForceTriangleActivation(ref slug) => { @@ -244,7 +242,10 @@ pub fn handle_debug_commands( } else { DebugResponsePayload { command: format!("InspectNpc({})", wire_id), - text: format!("Entity {} exists but is not an Active-tier NPC.", wire_id), + text: format!( + "Entity {} exists but is not an Active-tier NPC.", + wire_id + ), success: false, } } @@ -290,8 +291,12 @@ pub fn handle_debug_commands( lines.push(format!( " {} | sid={} | pos=({},{},{}) | {}", entity, - stable.map(|s| s.0.to_string()).unwrap_or_else(|| "?".to_string()), - pos.x, pos.y, pos.z, + stable + .map(|s| s.0.to_string()) + .unwrap_or_else(|| "?".to_string()), + pos.x, + pos.y, + pos.z, name_str, )); } @@ -366,12 +371,18 @@ mod tests { fn advance_ticks_updates_simulation_time() { let (mut world, mut schedule) = setup_debug_world(); world.resource_mut::().tick = 100; - world.resource_mut::().push(DebugCommandKind::AdvanceTicks(50)); + world + .resource_mut::() + .push(DebugCommandKind::AdvanceTicks(50)); schedule.run(&mut world); assert_eq!(world.resource::().tick, 150); - let resp = world.resource::().pending_debug_response.as_ref().unwrap(); + let resp = world + .resource::() + .pending_debug_response + .as_ref() + .unwrap(); assert!(resp.success); assert!(resp.text.contains("150")); } @@ -380,21 +391,30 @@ mod tests { fn skip_to_contamination_sets_tick() { let (mut world, mut schedule) = setup_debug_world(); world.resource_mut::().tick = 10; - world.resource_mut::().push(DebugCommandKind::SkipToContamination); + world + .resource_mut::() + .push(DebugCommandKind::SkipToContamination); schedule.run(&mut world); - assert_eq!(world.resource::().tick, CONTAMINATION_DELAY_TICKS); - let resp = world.resource::().pending_debug_response.as_ref().unwrap(); + assert_eq!( + world.resource::().tick, + CONTAMINATION_DELAY_TICKS + ); + let resp = world + .resource::() + .pending_debug_response + .as_ref() + .unwrap(); assert!(resp.success); } #[test] fn teleport_moves_player() { let (mut world, mut schedule) = setup_debug_world(); - world.resource_mut::().push( - DebugCommandKind::TeleportToPosition { x: 50, y: 60, z: 1 }, - ); + world + .resource_mut::() + .push(DebugCommandKind::TeleportToPosition { x: 50, y: 60, z: 1 }); schedule.run(&mut world); @@ -402,7 +422,11 @@ mod tests { let pos = q.single(&world).unwrap(); assert_eq!((pos.x, pos.y, pos.z), (50, 60, 1)); - let resp = world.resource::().pending_debug_response.as_ref().unwrap(); + let resp = world + .resource::() + .pending_debug_response + .as_ref() + .unwrap(); assert!(resp.success); } @@ -411,12 +435,18 @@ mod tests { let (mut world, mut schedule) = setup_debug_world(); assert!(!world.resource::().0); - world.resource_mut::().push(DebugCommandKind::ForceContaminationActivate); + world + .resource_mut::() + .push(DebugCommandKind::ForceContaminationActivate); schedule.run(&mut world); assert!(world.resource::().0); assert!(!world.resource::().is_empty()); - let resp = world.resource::().pending_debug_response.as_ref().unwrap(); + let resp = world + .resource::() + .pending_debug_response + .as_ref() + .unwrap(); assert!(resp.success); } @@ -424,11 +454,17 @@ mod tests { fn debug_disabled_rejects_commands() { let (mut world, mut schedule) = setup_debug_world(); world.insert_resource(DebugEnabled(false)); - world.resource_mut::().push(DebugCommandKind::GetContaminationStatus); + world + .resource_mut::() + .push(DebugCommandKind::GetContaminationStatus); schedule.run(&mut world); - let resp = world.resource::().pending_debug_response.as_ref().unwrap(); + let resp = world + .resource::() + .pending_debug_response + .as_ref() + .unwrap(); assert!(!resp.success); assert!(resp.text.contains("disabled")); } @@ -437,21 +473,30 @@ mod tests { fn get_contamination_status_reports_state() { let (mut world, mut schedule) = setup_debug_world(); world.resource_mut::().tick = 42; - world.resource_mut::().push(DebugCommandKind::GetContaminationStatus); + world + .resource_mut::() + .push(DebugCommandKind::GetContaminationStatus); schedule.run(&mut world); - let resp = world.resource::().pending_debug_response.as_ref().unwrap(); + let resp = world + .resource::() + .pending_debug_response + .as_ref() + .unwrap(); assert!(resp.success); assert!(resp.text.contains("false")); // not yet active - assert!(resp.text.contains("42")); // current tick + assert!(resp.text.contains("42")); // current tick } #[test] fn no_commands_produces_no_response() { let (mut world, mut schedule) = setup_debug_world(); schedule.run(&mut world); - assert!(world.resource::().pending_debug_response.is_none()); + assert!(world + .resource::() + .pending_debug_response + .is_none()); } #[test] @@ -461,17 +506,25 @@ mod tests { map.set_walkable(&TilePosition::new(5, 5, 0), false); world.insert_resource(map); - world.resource_mut::().push( - DebugCommandKind::TeleportToPosition { x: 5, y: 5, z: 0 }, - ); + world + .resource_mut::() + .push(DebugCommandKind::TeleportToPosition { x: 5, y: 5, z: 0 }); schedule.run(&mut world); // Player should NOT have moved let mut q = world.query_filtered::<&TilePosition, With>(); let pos = q.single(&world).unwrap(); - assert_eq!((pos.x, pos.y, pos.z), (10, 20, 0), "player must not teleport to unwalkable tile"); + assert_eq!( + (pos.x, pos.y, pos.z), + (10, 20, 0), + "player must not teleport to unwalkable tile" + ); - let resp = world.resource::().pending_debug_response.as_ref().unwrap(); + let resp = world + .resource::() + .pending_debug_response + .as_ref() + .unwrap(); assert!(!resp.success); assert!(resp.text.contains("not walkable")); } @@ -482,7 +535,9 @@ mod tests { // Set tick past the delay but contamination not yet active world.resource_mut::().tick = CONTAMINATION_DELAY_TICKS + 100; - world.resource_mut::().push(DebugCommandKind::SkipToContamination); + world + .resource_mut::() + .push(DebugCommandKind::SkipToContamination); schedule.run(&mut world); // Tick should NOT have changed (no rewind) @@ -492,7 +547,11 @@ mod tests { "tick must not rewind" ); - let resp = world.resource::().pending_debug_response.as_ref().unwrap(); + let resp = world + .resource::() + .pending_debug_response + .as_ref() + .unwrap(); assert!(!resp.success); assert!(resp.text.contains("already past")); } diff --git a/server/src/bridge/mod.rs b/server/src/bridge/mod.rs index 8f34db088..8245b103e 100644 --- a/server/src/bridge/mod.rs +++ b/server/src/bridge/mod.rs @@ -87,20 +87,15 @@ impl BridgeResource { /// Inserted by BridgePlugin as Pending. Set to Complete in main.rs after /// `send_handshake()` succeeds. `receive_bridge_inputs` logs a warning /// if inputs arrive while still Pending. -#[derive(Resource, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Resource, Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum HandshakeState { /// Handshake not yet sent. Inputs arriving in this state trigger a warning. + #[default] Pending, /// Handshake sent. Normal operation. Complete, } -impl Default for HandshakeState { - fn default() -> Self { - Self::Pending - } -} - /// Receive inputs from bridge and push to InputQueue. /// Protocol errors (malformed input) are recoverable: the frame is skipped /// and a SimError is pushed to the SimErrorBuffer for client reporting (#85). diff --git a/server/src/bridge/tcp.rs b/server/src/bridge/tcp.rs index 23c4bb8e1..8fffb9b6d 100644 --- a/server/src/bridge/tcp.rs +++ b/server/src/bridge/tcp.rs @@ -137,10 +137,16 @@ impl SimBridge for TcpBridge { // Toggle to blocking for reliable startup message read. // The client sends StartupMessage immediately after handshake validation, // so this read should complete quickly. - reader.get_mut().set_nonblocking(false).map_err(BridgeError::Io)?; + reader + .get_mut() + .set_nonblocking(false) + .map_err(BridgeError::Io)?; let result = read_framed(reader.get_mut()); // Restore non-blocking for the tick loop - reader.get_mut().set_nonblocking(true).map_err(BridgeError::Io)?; + reader + .get_mut() + .set_nonblocking(true) + .map_err(BridgeError::Io)?; match result? { Some(payload) => { let msg: super::StartupMessage = rmp_serde::from_slice(&payload)?; diff --git a/server/src/bridge/types.rs b/server/src/bridge/types.rs index f1c116add..21bbfda36 100644 --- a/server/src/bridge/types.rs +++ b/server/src/bridge/types.rs @@ -351,6 +351,7 @@ pub struct VisibleTile { /// Server-authoritative zone assignment. Client maps zone_id to: /// - Audio crossfade target (D-073) /// - Deep fog temperature tint (D-059 layer 3) + /// /// None for tiles outside any defined zone (corridors, transition spaces). #[serde(default, skip_serializing_if = "Option::is_none")] pub zone_id: Option, @@ -506,7 +507,9 @@ pub enum PlayerAction { ToggleStanceDown, /// Update player facing without movement (D-054). Client sends when /// the player turns in place (e.g. mouse aim, turn keys). - SetFacing { facing: String }, + SetFacing { + facing: String, + }, /// Teleport player to the Gauntlet hub spawn point (#491). /// Clears dialogue, monologue, and interaction buffers. /// Rejected with a log warning on non-Gauntlet maps. @@ -521,11 +524,15 @@ pub enum PlayerAction { /// Save the current game state to `path` (#553, D-085). /// Client sends this when the player activates the save UI. /// Server executes save_to_file and sends SaveLoadResultWire confirmation. - SaveGame { path: String }, + SaveGame { + path: String, + }, /// Load a previously saved game from `path` (#553, D-085). /// Client sends this when the player selects a save file to load. /// Server executes load_from_file and sends SaveLoadResultWire confirmation. - LoadGame { path: String }, + LoadGame { + path: String, + }, /// Debug console command (#580). Only processed when `DebugEnabled` is true. /// Response delivered via `ObserverSnapshot.debug_response`. DebugCommand(DebugCommandKind), @@ -540,7 +547,9 @@ pub enum PlayerAction { RequestAllSettings, /// Delete a single setting (#627). Restores the key to its default /// (absent from the database). Confirmation via `settings_response`. - DeleteSetting { key: String }, + DeleteSetting { + key: String, + }, } impl PlayerAction { @@ -1032,6 +1041,9 @@ mod tests { }; let bytes = rmp_serde::to_vec_named(&msg).expect("serialize"); let result = rmp_serde::from_slice::(&bytes); - assert!(result.is_err(), "HandshakeMessage must not deserialize as ObserverSnapshot"); + assert!( + result.is_err(), + "HandshakeMessage must not deserialize as ObserverSnapshot" + ); } } diff --git a/server/src/knowledge/events.rs b/server/src/knowledge/events.rs index 45b8a8982..e131bd6af 100644 --- a/server/src/knowledge/events.rs +++ b/server/src/knowledge/events.rs @@ -197,8 +197,7 @@ pub fn process_knowledge_events( match event.event_type { KnowledgeEventType::DirectObservation { target, position } => { if let Some(stable_id) = registry.to_stable(target) { - if let Some(claim) = - observer_kg.observe_entity(stable_id, position, event.tick) + if let Some(claim) = observer_kg.observe_entity(stable_id, position, event.tick) { // StableId is Copy — capture before moving claim into event. let told_by = claim.told_by; diff --git a/server/src/knowledge/graph.rs b/server/src/knowledge/graph.rs index 728b2f9a1..501c7e521 100644 --- a/server/src/knowledge/graph.rs +++ b/server/src/knowledge/graph.rs @@ -622,7 +622,12 @@ mod tests { ); // Public is symmetric — even self-observation passes - assert!(filter_by_access(observer, observer, &ObserverAccess::Public, &kg)); + assert!(filter_by_access( + observer, + observer, + &ObserverAccess::Public, + &kg + )); } #[test] @@ -630,7 +635,7 @@ mod tests { // THE critical negative test (Sprint 12 joint briefing). // A non-owner observer must NOT get access to OwnerOnly data. let observer = StableId(1); // some other entity - let target = StableId(2); // owns the component + let target = StableId(2); // owns the component let kg = KnowledgeGraph::new(); assert!( @@ -657,8 +662,14 @@ mod tests { let kg = KnowledgeGraph::new(); for id in 1u64..=10 { assert!( - !filter_by_access(StableId(id), StableId(id + 1), &ObserverAccess::OwnerOnly, &kg), - "StableId({id}) should not match StableId({})", id + 1 + !filter_by_access( + StableId(id), + StableId(id + 1), + &ObserverAccess::OwnerOnly, + &kg + ), + "StableId({id}) should not match StableId({})", + id + 1 ); } } @@ -886,7 +897,10 @@ mod tests { // Observe at SAME position — no contradiction let result = g.observe_entity(target, make_position(10, 10), 200); - assert!(result.is_none(), "Same position should not be a contradiction"); + assert!( + result.is_none(), + "Same position should not be a contradiction" + ); let entry = g.entity_knowledge(&target).unwrap(); assert_eq!(entry.state, KnowledgeState::Active); assert!(entry.contradicted_claim.is_none()); @@ -1067,7 +1081,10 @@ mod tests { .expect("contradiction should be detected"); let entry = g.entity_knowledge(&target).unwrap(); - let stored = entry.contradicted_claim.as_ref().expect("field should be populated"); + let stored = entry + .contradicted_claim + .as_ref() + .expect("field should be populated"); assert_eq!(stored.told_by, returned.told_by); assert_eq!(stored.told_tick, returned.told_tick); @@ -1114,7 +1131,10 @@ mod tests { // Second observation (now source is DirectObservation, different position): // state must stay Contradicted — contradiction is still unresolved. let claim2 = g.observe_entity(target, make_position(11, 3), 300); - assert!(claim2.is_none(), "no new contradiction: DirectObservation source"); + assert!( + claim2.is_none(), + "no new contradiction: DirectObservation source" + ); assert_eq!( g.entity_knowledge(&target).unwrap().state, KnowledgeState::Contradicted, @@ -1158,8 +1178,14 @@ mod tests { let a_result = g.observe_entity(entity_a, make_position(8, 1), 200); let b_result = g.observe_entity(entity_b, make_position(9, 5), 200); - assert!(a_result.is_some(), "Entity A (ToldBy source) should contradict"); - assert!(b_result.is_none(), "Entity B (DirectObservation) should not contradict"); + assert!( + a_result.is_some(), + "Entity A (ToldBy source) should contradict" + ); + assert!( + b_result.is_none(), + "Entity B (DirectObservation) should not contradict" + ); assert_eq!( g.entity_knowledge(&entity_a).unwrap().state, KnowledgeState::Contradicted diff --git a/server/src/knowledge/types.rs b/server/src/knowledge/types.rs index ef1e72e3d..be04faf86 100644 --- a/server/src/knowledge/types.rs +++ b/server/src/knowledge/types.rs @@ -279,9 +279,10 @@ impl Default for DecayThresholds { /// /// Design: coarse-grained component-level tags rather than per-field. /// A component either passes or fails its access check as a whole. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] pub enum ObserverAccess { /// Anyone can observe this data. Default for non-sensitive components. + #[default] Public, /// Only the entity that owns this component (e.g. player's own inventory). OwnerOnly, @@ -295,12 +296,6 @@ pub enum ObserverAccess { KnowledgeGated(String), } -impl Default for ObserverAccess { - fn default() -> Self { - Self::Public - } -} - /// Component that attaches an access rule to an entity's sensitive data. /// /// When an observer snapshot is built, `filter_by_access` (implemented in diff --git a/server/src/main.rs b/server/src/main.rs index 763d923dd..720ff8312 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -165,7 +165,10 @@ fn main() { )); } Err(e) => { - tracing::error!("Failed to open settings store: {}. Settings will not persist.", e); + tracing::error!( + "Failed to open settings store: {}. Settings will not persist.", + e + ); } } @@ -176,9 +179,11 @@ fn main() { app.insert_resource(settled_reach_server::simulation::rng::SimRng::new(seed)); // Initialize empty line pool index (populated by generator pipeline in v0.2). - app.insert_resource(settled_reach_server::simulation::line_pool::LinePoolIndexResource( - settled_reach_server::simulation::line_pool::LinePoolIndex::default(), - )); + app.insert_resource( + settled_reach_server::simulation::line_pool::LinePoolIndexResource( + settled_reach_server::simulation::line_pool::LinePoolIndex::default(), + ), + ); // Character archetype from client's StartupMessage (#587). let archetype = startup.character_archetype; @@ -384,7 +389,10 @@ fn dump_schedule_graph() { /// Proof room: 32x32 map, wall at (16,14), player at (16,16), 3 NPCs. /// Extracted from the original inline setup for reuse by both test-mode and normal mode. -fn setup_proof_room(app: &mut App, archetype: settled_reach_server::bridge::types::CharacterArchetype) { +fn setup_proof_room( + app: &mut App, + archetype: settled_reach_server::bridge::types::CharacterArchetype, +) { use settled_reach_server::knowledge::registry::EntityRegistry; use settled_reach_server::knowledge::KnowledgeGraph; use settled_reach_server::npc::relationships::{RelationshipEdge, RelationshipGraph}; @@ -418,11 +426,17 @@ fn setup_proof_room(app: &mut App, archetype: settled_reach_server::bridge::type // Player at (16,16) — archetype from StartupMessage (#587, D-053) let profile = match archetype { - settled_reach_server::bridge::types::CharacterArchetype::Smuggler => MovementProfile::smuggler(), - settled_reach_server::bridge::types::CharacterArchetype::Detective => MovementProfile::detective(), + settled_reach_server::bridge::types::CharacterArchetype::Smuggler => { + MovementProfile::smuggler() + } + settled_reach_server::bridge::types::CharacterArchetype::Detective => { + MovementProfile::detective() + } + }; + let monologue_state = MonologueState { + character: archetype.as_monologue_key().to_string(), + ..Default::default() }; - let mut monologue_state = MonologueState::default(); - monologue_state.character = archetype.as_monologue_key().to_string(); let player = app .world_mut() .spawn(( diff --git a/server/src/npc/awareness.rs b/server/src/npc/awareness.rs index 1682e710e..e8dde13d3 100644 --- a/server/src/npc/awareness.rs +++ b/server/src/npc/awareness.rs @@ -28,8 +28,8 @@ use serde::{Deserialize, Serialize}; use crate::npc::vision::NpcVisionState; use crate::npc::{Npc, ToleranceThreshold}; -use crate::simulation::time::SimulationTime; use crate::simulation::tier::ActiveSim; +use crate::simulation::time::SimulationTime; // --------------------------------------------------------------------------- // Constants @@ -117,7 +117,7 @@ pub fn detect_player_awareness( awareness.consecutive_los_ticks = 0; // Decay suspicion slowly when player is not visible - if time.tick % AWARENESS_DECAY_INTERVAL == 0 && awareness.suspicion_level > 0 { + if time.tick.is_multiple_of(AWARENESS_DECAY_INTERVAL) && awareness.suspicion_level > 0 { awareness.suspicion_level = (awareness.suspicion_level - AWARENESS_DECAY_AMOUNT).max(0); } @@ -134,8 +134,8 @@ mod tests { use super::*; use crate::npc::vision::NpcVisionState; use crate::npc::{Npc, ToleranceThreshold}; - use crate::simulation::time::SimulationTime; use crate::simulation::tier::ActiveSim; + use crate::simulation::time::SimulationTime; use bevy_ecs::world::World; fn setup_world() -> World { @@ -372,7 +372,10 @@ mod tests { run_system(&mut world); let awareness = world.get::(npc).unwrap(); - assert_eq!(awareness.suspicion_level, 100, "suspicion should cap at 100"); + assert_eq!( + awareness.suspicion_level, 100, + "suspicion should cap at 100" + ); } #[test] diff --git a/server/src/npc/background.rs b/server/src/npc/background.rs index 366378442..6b2c50e5c 100644 --- a/server/src/npc/background.rs +++ b/server/src/npc/background.rs @@ -16,11 +16,11 @@ use bevy_ecs::prelude::*; +use crate::npc::mood::{MoodState, NpcMood}; +use crate::npc::routine::ActivityState; use crate::npc::{ Contentment, DailyRoutine, JobPerformance, Npc, Relationships, ToleranceThreshold, }; -use crate::npc::mood::{MoodState, NpcMood}; -use crate::npc::routine::ActivityState; use crate::simulation::tier::BackgroundSim; use crate::simulation::time::{SimulationTime, TICKS_PER_GAME_MINUTE}; @@ -110,7 +110,7 @@ pub fn background_tick( >, ) { // Fire once per game-minute (D-031: 10 ticks/minute) - if time.tick % TICKS_PER_GAME_MINUTE != 0 { + if !time.tick.is_multiple_of(TICKS_PER_GAME_MINUTE) { return; } @@ -185,16 +185,16 @@ pub fn background_tick( #[cfg(test)] mod tests { use super::*; + use crate::knowledge::types::StableId; + use crate::npc::mood::{MoodState, NpcMood}; + use crate::npc::routine::ActivityState; use crate::npc::{ Contentment, DailyRoutine, JobPerformance, Npc, Relationship, RelationshipKind, Relationships, RoutineEntry, ToleranceThreshold, }; - use crate::npc::mood::{MoodState, NpcMood}; - use crate::npc::routine::ActivityState; use crate::simulation::movement::TilePosition; use crate::simulation::tier::{ActiveSim, BackgroundSim}; use crate::simulation::time::{DayPhase, SimulationTime, TICKS_PER_GAME_MINUTE}; - use crate::knowledge::types::StableId; use bevy_ecs::world::World; // --- derive_background_mood --- @@ -311,7 +311,10 @@ mod tests { Npc, BackgroundSim, MoodState::default(), - ToleranceThreshold { current_stress: 60, threshold: 50 }, + ToleranceThreshold { + current_stress: 60, + threshold: 50, + }, )) .id(); @@ -333,7 +336,10 @@ mod tests { Npc, BackgroundSim, MoodState::default(), - ToleranceThreshold { current_stress: 60, threshold: 50 }, + ToleranceThreshold { + current_stress: 60, + threshold: 50, + }, )) .id(); @@ -353,7 +359,10 @@ mod tests { Npc, BackgroundSim, MoodState::default(), - ToleranceThreshold { current_stress: 60, threshold: 50 }, + ToleranceThreshold { + current_stress: 60, + threshold: 50, + }, )) .id(); @@ -379,14 +388,21 @@ mod tests { mood: NpcMood::Warm, changed_tick: 0, }, - ToleranceThreshold { current_stress: 60, threshold: 50 }, + ToleranceThreshold { + current_stress: 60, + threshold: 50, + }, )) .id(); run_system(&mut world); let mood = world.get::(npc).unwrap(); - assert_eq!(mood.mood, NpcMood::Warm, "ActiveSim NPC must not be updated by background_tick"); + assert_eq!( + mood.mood, + NpcMood::Warm, + "ActiveSim NPC must not be updated by background_tick" + ); } // --- Mood state machine --- @@ -401,7 +417,10 @@ mod tests { Npc, BackgroundSim, MoodState::default(), - ToleranceThreshold { current_stress: 50, threshold: 50 }, + ToleranceThreshold { + current_stress: 50, + threshold: 50, + }, )) .id(); @@ -416,9 +435,7 @@ mod tests { world.resource_mut::().tick = 0; // No ToleranceThreshold → defaults (0, 50) → Content (0 < 20) - let npc = world - .spawn((Npc, BackgroundSim, MoodState::default())) - .id(); + let npc = world.spawn((Npc, BackgroundSim, MoodState::default())).id(); run_system(&mut world); @@ -434,8 +451,14 @@ mod tests { .spawn(( Npc, BackgroundSim, - MoodState { mood: NpcMood::Warm, changed_tick: 0 }, - ToleranceThreshold { current_stress: 55, threshold: 50 }, + MoodState { + mood: NpcMood::Warm, + changed_tick: 0, + }, + ToleranceThreshold { + current_stress: 55, + threshold: 50, + }, )) .id(); @@ -456,7 +479,10 @@ mod tests { .spawn(( Npc, BackgroundSim, - MoodState { mood: NpcMood::Content, changed_tick: 42 }, + MoodState { + mood: NpcMood::Content, + changed_tick: 42, + }, )) .id(); @@ -464,7 +490,10 @@ mod tests { let mood = world.get::(npc).unwrap(); assert_eq!(mood.mood, NpcMood::Content); - assert_eq!(mood.changed_tick, 42, "changed_tick must not update when mood unchanged"); + assert_eq!( + mood.changed_tick, 42, + "changed_tick must not update when mood unchanged" + ); } // --- Schedule state machine --- @@ -564,7 +593,10 @@ mod tests { run_system(&mut world); let rels = world.get::(npc).unwrap(); - assert_eq!(rels.entries[0].trust_level, 4, "positive trust decrements by 1"); + assert_eq!( + rels.entries[0].trust_level, 4, + "positive trust decrements by 1" + ); } #[test] @@ -591,7 +623,10 @@ mod tests { run_system(&mut world); let rels = world.get::(npc).unwrap(); - assert_eq!(rels.entries[0].trust_level, -3, "negative trust increments by 1"); + assert_eq!( + rels.entries[0].trust_level, -3, + "negative trust increments by 1" + ); } #[test] @@ -709,7 +744,10 @@ mod tests { Npc, BackgroundSim, MoodState::default(), - ToleranceThreshold { current_stress: 5, threshold: 50 }, + ToleranceThreshold { + current_stress: 5, + threshold: 50, + }, )) .id(); @@ -718,13 +756,19 @@ mod tests { Npc, BackgroundSim, MoodState::default(), - ToleranceThreshold { current_stress: 55, threshold: 50 }, + ToleranceThreshold { + current_stress: 55, + threshold: 50, + }, )) .id(); run_system(&mut world); assert_eq!(world.get::(calm).unwrap().mood, NpcMood::Content); - assert_eq!(world.get::(hostile).unwrap().mood, NpcMood::Hostile); + assert_eq!( + world.get::(hostile).unwrap().mood, + NpcMood::Hostile + ); } } diff --git a/server/src/npc/disclosure.rs b/server/src/npc/disclosure.rs index d68ae79bc..864eff35e 100644 --- a/server/src/npc/disclosure.rs +++ b/server/src/npc/disclosure.rs @@ -16,7 +16,9 @@ use crate::bridge::types::MonologueEvent; use crate::knowledge::events::{ KnowledgeEvent, KnowledgeEventType, ProcessedFactGrant, ProcessedKnowledgeGrant, }; -use crate::knowledge::types::{FactId, KnowledgeConfidence, KnowledgeSource, KnowledgeState, RelationshipState, StableId}; +use crate::knowledge::types::{ + FactId, KnowledgeConfidence, KnowledgeSource, KnowledgeState, RelationshipState, StableId, +}; use crate::knowledge::{KnowledgeEventQueue, KnowledgeGraph, StableEntityId}; use crate::npc::mood::{MoodState, NpcMood}; use crate::npc::relationships::RelationshipGraph; @@ -513,12 +515,23 @@ mod tests { let fact_id = FactId("investigation.clue".to_string()); let kg = make_kg(vec![( fact_id.clone(), - make_fact(KnowledgeConfidence::KnowsOf, KnowledgeState::Active, 5, false), + make_fact( + KnowledgeConfidence::KnowsOf, + KnowledgeState::Active, + 5, + false, + ), )]); let npc = app .world_mut() - .spawn((Npc, ActiveSim, kg, DisclosureCooldown::default(), DisclosureCandidates::default())) + .spawn(( + Npc, + ActiveSim, + kg, + DisclosureCooldown::default(), + DisclosureCandidates::default(), + )) .id(); app.update(); @@ -537,12 +550,23 @@ mod tests { let fact_id = FactId("secret.dangerous".to_string()); let kg = make_kg(vec![( fact_id.clone(), - make_fact(KnowledgeConfidence::KnowsDetails, KnowledgeState::Active, 5, true), + make_fact( + KnowledgeConfidence::KnowsDetails, + KnowledgeState::Active, + 5, + true, + ), )]); let npc = app .world_mut() - .spawn((Npc, ActiveSim, kg, DisclosureCooldown::default(), DisclosureCandidates::default())) + .spawn(( + Npc, + ActiveSim, + kg, + DisclosureCooldown::default(), + DisclosureCandidates::default(), + )) .id(); app.update(); @@ -561,12 +585,23 @@ mod tests { let fact_id = FactId("cargo.manifest".to_string()); let kg = make_kg(vec![( fact_id.clone(), - make_fact(KnowledgeConfidence::KnowsOf, KnowledgeState::Stale, 5, false), + make_fact( + KnowledgeConfidence::KnowsOf, + KnowledgeState::Stale, + 5, + false, + ), )]); let npc = app .world_mut() - .spawn((Npc, ActiveSim, kg, DisclosureCooldown::default(), DisclosureCandidates::default())) + .spawn(( + Npc, + ActiveSim, + kg, + DisclosureCooldown::default(), + DisclosureCandidates::default(), + )) .id(); app.update(); @@ -585,7 +620,12 @@ mod tests { let fact_id = FactId("dock.schedule".to_string()); let kg = make_kg(vec![( fact_id.clone(), - make_fact(KnowledgeConfidence::KnowsOf, KnowledgeState::Active, 5, false), + make_fact( + KnowledgeConfidence::KnowsOf, + KnowledgeState::Active, + 5, + false, + ), )]); let mut cooldown = DisclosureCooldown::default(); @@ -593,7 +633,13 @@ mod tests { let npc = app .world_mut() - .spawn((Npc, ActiveSim, kg, cooldown, DisclosureCandidates::default())) + .spawn(( + Npc, + ActiveSim, + kg, + cooldown, + DisclosureCandidates::default(), + )) .id(); app.update(); @@ -614,7 +660,12 @@ mod tests { .map(|i| { ( FactId(format!("fact.{:02}", i)), - make_fact(KnowledgeConfidence::KnowsOf, KnowledgeState::Active, i as u64, false), + make_fact( + KnowledgeConfidence::KnowsOf, + KnowledgeState::Active, + i as u64, + false, + ), ) }) .collect(); @@ -622,7 +673,13 @@ mod tests { let kg = make_kg(facts); let npc = app .world_mut() - .spawn((Npc, ActiveSim, kg, DisclosureCooldown::default(), DisclosureCandidates::default())) + .spawn(( + Npc, + ActiveSim, + kg, + DisclosureCooldown::default(), + DisclosureCandidates::default(), + )) .id(); app.update(); @@ -642,7 +699,12 @@ mod tests { let fact_id = FactId("investigation.clue".to_string()); let kg = make_kg(vec![( fact_id.clone(), - make_fact(KnowledgeConfidence::KnowsOf, KnowledgeState::Active, 5, false), + make_fact( + KnowledgeConfidence::KnowsOf, + KnowledgeState::Active, + 5, + false, + ), )]); // Set computed_tick = 1 (non-zero). Tick 5 is within the 30-tick refresh window. @@ -651,7 +713,13 @@ mod tests { let npc = app .world_mut() - .spawn((Npc, ActiveSim, kg, DisclosureCooldown::default(), candidates)) + .spawn(( + Npc, + ActiveSim, + kg, + DisclosureCooldown::default(), + candidates, + )) .id(); // Advance tick to 5 (within CANDIDATE_REFRESH_TICKS = 30). @@ -677,12 +745,23 @@ mod tests { let fact_id = FactId("rumour.vague".to_string()); let kg = make_kg(vec![( fact_id.clone(), - make_fact(KnowledgeConfidence::Suspects, KnowledgeState::Active, 5, false), + make_fact( + KnowledgeConfidence::Suspects, + KnowledgeState::Active, + 5, + false, + ), )]); let npc = app .world_mut() - .spawn((Npc, ActiveSim, kg, DisclosureCooldown::default(), DisclosureCandidates::default())) + .spawn(( + Npc, + ActiveSim, + kg, + DisclosureCooldown::default(), + DisclosureCandidates::default(), + )) .id(); app.update(); diff --git a/server/src/npc/generate.rs b/server/src/npc/generate.rs index 3fa55a1f8..074ff4ff2 100644 --- a/server/src/npc/generate.rs +++ b/server/src/npc/generate.rs @@ -29,17 +29,17 @@ use std::collections::BTreeMap; use bevy_ecs::prelude::*; +use crate::knowledge::graph::KnowledgeGraph; use crate::knowledge::types::{FactId, KnowledgeConfidence, StableId}; +use crate::npc::awareness::PlayerAwareness; +use crate::npc::mood::MoodState; +use crate::npc::vision::{NpcMemory, NpcVisionState}; use crate::npc::{ CombatCapability, CombatStyle, Contentment, DailyRoutine, InformationInventory, JobPerformance, KnownFact, Npc, PersonalityTrait, PersonalityTraits, Relationship, RelationshipKind, Relationships, RoutineEntry, Secret, SecretSeverity, Skill, SkillSet, Tell, TellSystem, TellTrigger, ToleranceThreshold, Want, WantKind, MAX_KEY_RELATIONSHIPS, }; -use crate::knowledge::graph::KnowledgeGraph; -use crate::npc::mood::MoodState; -use crate::npc::awareness::PlayerAwareness; -use crate::npc::vision::{NpcMemory, NpcVisionState}; use crate::simulation::movement::TilePosition; use crate::simulation::rng::SimRng; use crate::simulation::tier::ActiveSim; @@ -116,15 +116,30 @@ fn pick_secret_severity(idx: usize) -> SecretSeverity { fn pick_relationship_kind(idx: usize) -> RelationshipKind { use RelationshipKind::*; - const VARIANTS: [RelationshipKind; 7] = - [Colleague, Friend, Rival, Romantic, Family, Superior, Subordinate]; + const VARIANTS: [RelationshipKind; 7] = [ + Colleague, + Friend, + Rival, + Romantic, + Family, + Superior, + Subordinate, + ]; VARIANTS[idx % VARIANTS.len()] } fn pick_personality_trait(idx: usize) -> PersonalityTrait { use PersonalityTrait::*; const VARIANTS: [PersonalityTrait; 10] = [ - Cautious, Bold, Honest, Deceptive, Compassionate, Ruthless, Curious, Incurious, Social, + Cautious, + Bold, + Honest, + Deceptive, + Compassionate, + Ruthless, + Curious, + Incurious, + Social, Reclusive, ]; VARIANTS[idx % VARIANTS.len()] @@ -132,8 +147,16 @@ fn pick_personality_trait(idx: usize) -> PersonalityTrait { fn pick_skill(idx: usize) -> Skill { use Skill::*; - const VARIANTS: [Skill; 8] = - [Combat, Intimidation, Medical, Observation, Persuasion, Piloting, Stealth, Technical]; + const VARIANTS: [Skill; 8] = [ + Combat, + Intimidation, + Medical, + Observation, + Persuasion, + Piloting, + Stealth, + Technical, + ]; VARIANTS[idx % VARIANTS.len()] } @@ -257,7 +280,14 @@ fn gen_routine(rng: &mut SimRng, location_pool: &[(DayPhase, TilePosition)]) -> .iter() .map(|&i| { let (phase, location) = available[i]; - let activity_names = ["Work", "Patrol", "Rest", "Meeting", "Training", "Maintenance"]; + let activity_names = [ + "Work", + "Patrol", + "Rest", + "Meeting", + "Training", + "Maintenance", + ]; let act_idx = rng.rng.random_range(0..activity_names.len()); RoutineEntry { phase, @@ -391,7 +421,9 @@ fn gen_skills(rng: &mut SimRng, role: &RoleDefinition) -> (SkillSet, Option(entity).is_some(), "must have TellSystem" ); - assert!(world.get::(entity).is_some(), "must have SkillSet"); + assert!( + world.get::(entity).is_some(), + "must have SkillSet" + ); assert!( world.get::(entity).is_some(), "must have MoodState" @@ -629,7 +667,10 @@ mod tests { let want_a = world_a.get::(ea).unwrap(); let want_b = world_b.get::(eb).unwrap(); assert_eq!(want_a.primary, want_b.primary, "Want.primary must match"); - assert_eq!(want_a.intensity, want_b.intensity, "Want.intensity must match"); + assert_eq!( + want_a.intensity, want_b.intensity, + "Want.intensity must match" + ); let tol_a = world_a.get::(ea).unwrap(); let tol_b = world_b.get::(eb).unwrap(); diff --git a/server/src/npc/interaction.rs b/server/src/npc/interaction.rs index aaad2f2b1..6592b962f 100644 --- a/server/src/npc/interaction.rs +++ b/server/src/npc/interaction.rs @@ -82,7 +82,10 @@ impl InteractionMemory { /// Count notable events of a given kind. pub fn count_events(&self, kind: InteractionEventKind) -> usize { - self.notable_events.iter().filter(|e| e.kind == kind).count() + self.notable_events + .iter() + .filter(|e| e.kind == kind) + .count() } } diff --git a/server/src/npc/mod.rs b/server/src/npc/mod.rs index 2ea3189c0..212d5f206 100644 --- a/server/src/npc/mod.rs +++ b/server/src/npc/mod.rs @@ -126,7 +126,6 @@ pub struct NpcVoiceProfile { pub culture_id: String, } - /// NPC animation tier (D-047). /// /// Tier 1 (clear): public daily activities — instantly readable. diff --git a/server/src/npc/mood.rs b/server/src/npc/mood.rs index c88e1ebe4..e23edd421 100644 --- a/server/src/npc/mood.rs +++ b/server/src/npc/mood.rs @@ -18,10 +18,10 @@ use bevy_ecs::prelude::*; use serde::{Deserialize, Serialize}; -use crate::simulation::line_pool::Mood as ContentMood; use crate::npc::interaction::InteractionMemory; use crate::npc::{Npc, ToleranceThreshold}; use crate::simulation::dialogue::CurrentMood; +use crate::simulation::line_pool::Mood as ContentMood; use crate::simulation::tier::ActiveSim; use crate::simulation::time::{DayPhase, SimulationTime}; @@ -293,10 +293,7 @@ mod tests { #[test] fn mood_warm_when_positive_interaction() { - assert_eq!( - derive_mood(0, 50, DayPhase::Morning, true), - NpcMood::Warm - ); + assert_eq!(derive_mood(0, 50, DayPhase::Morning, true), NpcMood::Warm); } #[test] @@ -381,10 +378,7 @@ mod tests { #[test] fn mood_priority_warm_over_frustrated() { // Warm takes priority over Frustrated (checked before Evening test) - assert_eq!( - derive_mood(25, 50, DayPhase::Evening, true), - NpcMood::Warm - ); + assert_eq!(derive_mood(25, 50, DayPhase::Evening, true), NpcMood::Warm); } #[test] @@ -489,12 +483,7 @@ mod tests { // No ToleranceThreshold → defaults (stress=0, threshold=50) → Content let npc = world - .spawn(( - Npc, - ActiveSim, - MoodState::default(), - CurrentMood::default(), - )) + .spawn((Npc, ActiveSim, MoodState::default(), CurrentMood::default())) .id(); let mut schedule = bevy_ecs::schedule::Schedule::default(); @@ -669,7 +658,12 @@ mod tests { // This test documents the invariant: derive_mood never emits these states. use std::collections::HashSet; - let phases = [DayPhase::Morning, DayPhase::Afternoon, DayPhase::Evening, DayPhase::Night]; + let phases = [ + DayPhase::Morning, + DayPhase::Afternoon, + DayPhase::Evening, + DayPhase::Night, + ]; let stresses: &[i16] = &[-50, -1, 0, 1, 19, 20, 29, 30, 49, 50, 51, 100]; let thresholds: &[i16] = &[0, 1, 50, 100]; let warm_flags = [false, true]; diff --git a/server/src/npc/relationships.rs b/server/src/npc/relationships.rs index 0c6a063dc..1e9f7711e 100644 --- a/server/src/npc/relationships.rs +++ b/server/src/npc/relationships.rs @@ -40,20 +40,11 @@ pub const CONFRONTATION_TRUST_DELTA: i8 = -2; #[derive(Debug, Clone)] pub enum TrustEvent { /// Player completed a Talk exchange with an NPC. - TalkCompleted { - npc: Entity, - player: Entity, - }, + TalkCompleted { npc: Entity, player: Entity }, /// Player walked away during active dialogue (D-064). - WalkAway { - npc: Entity, - player: Entity, - }, + WalkAway { npc: Entity, player: Entity }, /// Player delivered a confrontation (D-063). - ConfrontationDelivered { - npc: Entity, - player: Entity, - }, + ConfrontationDelivered { npc: Entity, player: Entity }, } /// Resource: queue of pending trust events. @@ -300,7 +291,11 @@ fn scale_delta(delta: i8, factor_tenths: i8) -> i8 { // Multiply by factor, round up (ceiling of absolute value) let scaled_abs = (delta.unsigned_abs() as i16 * factor_tenths as i16 + 9) / 10; let scaled = scaled_abs.min(10) as i8; - if delta < 0 { -(scaled as i8) } else { scaled as i8 } + if delta < 0 { + -scaled + } else { + scaled + } } /// Drain pending trust events and apply deltas to the RelationshipGraph. @@ -489,9 +484,12 @@ const DECAY_DELTA: i8 = 1; /// meaningful. Blocks #249 (player-action social propagation, Sprint 15). /// /// System ordering: after update_trust, before advance_tick. -pub fn update_relationship_dynamics(time: Res, mut graph: ResMut) { +pub fn update_relationship_dynamics( + time: Res, + mut graph: ResMut, +) { // Lightweight: evaluate once per game-minute - if time.tick % DECAY_INTERVAL_TICKS != 0 { + if !time.tick.is_multiple_of(DECAY_INTERVAL_TICKS) { return; } @@ -1065,11 +1063,13 @@ mod tests { ); // Queue propagation from A - world.resource_mut::().push(PropagationEvent { - npc: npc_a, - player, - delta: 2, - }); + world + .resource_mut::() + .push(PropagationEvent { + npc: npc_a, + player, + delta: 2, + }); let mut schedule = bevy_ecs::schedule::Schedule::default(); schedule.add_systems(propagate_social_actions); @@ -1077,7 +1077,9 @@ mod tests { // B should now have a trust edge toward the player (positive) let graph = world.resource::(); - let edge = graph.get_relationship(&npc_b, &player).expect("B should have edge to player"); + let edge = graph + .get_relationship(&npc_b, &player) + .expect("B should have edge to player"); assert!( edge.trust > 0, "Second-order NPC should trust player more after positive first-order event" @@ -1104,11 +1106,13 @@ mod tests { make_edge(RelationshipKind::Colleague, 2), ); - world.resource_mut::().push(PropagationEvent { - npc: npc_a, - player, - delta: 5, - }); + world + .resource_mut::() + .push(PropagationEvent { + npc: npc_a, + player, + delta: 5, + }); let mut schedule = bevy_ecs::schedule::Schedule::default(); schedule.add_systems(propagate_social_actions); @@ -1141,11 +1145,13 @@ mod tests { graph.set_relationship(npc_a, npc_b, make_edge(RelationshipKind::Friend, 5)); graph.set_relationship(npc_b, npc_c, make_edge(RelationshipKind::Friend, 5)); - world.resource_mut::().push(PropagationEvent { - npc: npc_a, - player, - delta: 5, - }); + world + .resource_mut::() + .push(PropagationEvent { + npc: npc_a, + player, + delta: 5, + }); let mut schedule = bevy_ecs::schedule::Schedule::default(); schedule.add_systems(propagate_social_actions); @@ -1181,11 +1187,13 @@ mod tests { graph.set_relationship(npc_b, npc_c, make_edge(RelationshipKind::Friend, 5)); // First run: queue the third-order change - world.resource_mut::().push(PropagationEvent { - npc: npc_a, - player, - delta: 5, - }); + world + .resource_mut::() + .push(PropagationEvent { + npc: npc_a, + player, + delta: 5, + }); let mut schedule = bevy_ecs::schedule::Schedule::default(); schedule.add_systems(propagate_social_actions); @@ -1197,9 +1205,13 @@ mod tests { // C should now have an edge with positive trust let graph = world.resource::(); - let edge = graph.get_relationship(&npc_c, &player) + let edge = graph + .get_relationship(&npc_c, &player) .expect("Delayed change should have been applied by now"); - assert!(edge.trust > 0, "Third-order trust should be positive after delayed application"); + assert!( + edge.trust > 0, + "Third-order trust should be positive after delayed application" + ); } #[test] @@ -1227,11 +1239,13 @@ mod tests { make_edge(RelationshipKind::Friend, 5), ); - world.resource_mut::().push(PropagationEvent { - npc: npc_a, - player, - delta: 2, - }); + world + .resource_mut::() + .push(PropagationEvent { + npc: npc_a, + player, + delta: 2, + }); let mut schedule = bevy_ecs::schedule::Schedule::default(); schedule.add_systems(propagate_social_actions); @@ -1269,11 +1283,13 @@ mod tests { make_edge(RelationshipKind::Friend, 5), ); - world.resource_mut::().push(PropagationEvent { - npc: npc_a, - player, - delta: -2, // confrontation - }); + world + .resource_mut::() + .push(PropagationEvent { + npc: npc_a, + player, + delta: -2, // confrontation + }); let mut schedule = bevy_ecs::schedule::Schedule::default(); schedule.add_systems(propagate_social_actions); @@ -1281,7 +1297,8 @@ mod tests { // B should trust player LESS after A was confronted let graph = world.resource::(); - let edge = graph.get_relationship(&npc_b, &player) + let edge = graph + .get_relationship(&npc_b, &player) .expect("B should have edge to player"); assert!( edge.trust < 0, @@ -1298,21 +1315,30 @@ mod tests { let npc_a = StableId(1); let player = StableId(99); - world.resource_mut::().push(PropagationEvent { - npc: npc_a, - player, - delta: 5, - }); + world + .resource_mut::() + .push(PropagationEvent { + npc: npc_a, + player, + delta: 5, + }); let mut schedule = bevy_ecs::schedule::Schedule::default(); schedule.add_systems(propagate_social_actions); schedule.run(&mut world); let graph = world.resource::(); - assert!(graph.is_empty(), "No relationships → no propagation edges created"); + assert!( + graph.is_empty(), + "No relationships → no propagation edges created" + ); let delay_queue = world.resource::(); - assert_eq!(delay_queue.pending_count(), 0, "No delay queue entries either"); + assert_eq!( + delay_queue.pending_count(), + 0, + "No delay queue entries either" + ); } #[test] @@ -1357,6 +1383,9 @@ mod tests { schedule.run(&mut world); // Should not panic let graph = world.resource::(); - assert!(graph.is_empty(), "no edge should be created for unregistered entity"); + assert!( + graph.is_empty(), + "no edge should be created for unregistered entity" + ); } } diff --git a/server/src/npc/routine.rs b/server/src/npc/routine.rs index 8c25c75ea..8e393de57 100644 --- a/server/src/npc/routine.rs +++ b/server/src/npc/routine.rs @@ -132,12 +132,7 @@ pub fn enter_activity( mut commands: Commands, time: Res, npcs: Query< - ( - Entity, - &TilePosition, - &DailyRoutine, - Option<&ActivityState>, - ), + (Entity, &TilePosition, &DailyRoutine, Option<&ActivityState>), ( With, With, @@ -287,8 +282,7 @@ pub fn detect_routine_deviation( let phase = time.day_phase(); let tick = time.tick; - for (entity, pos, routine, activity_opt, path_req, computed_path, deviating_opt) in - npcs.iter() + for (entity, pos, routine, activity_opt, path_req, computed_path, deviating_opt) in npcs.iter() { let Some(entry) = routine.entry_for_phase(phase) else { // No routine entry for this phase — nothing to deviate from. @@ -388,7 +382,7 @@ mod tests { .spawn(( Npc, crate::simulation::tier::ActiveSim, // system requires With (#94) - TilePosition::new(5, 5, 0), // Not at afternoon location + TilePosition::new(5, 5, 0), // Not at afternoon location DailyRoutine { entries: vec![RoutineEntry { phase: DayPhase::Afternoon, @@ -570,7 +564,10 @@ mod tests { let state = world.get::(entity).unwrap(); assert_eq!(state.activity, "Work"); assert_eq!(state.phase, DayPhase::Afternoon); - assert_eq!(state.started_tick, MINUTES_PER_PHASE * TICKS_PER_GAME_MINUTE); + assert_eq!( + state.started_tick, + MINUTES_PER_PHASE * TICKS_PER_GAME_MINUTE + ); } #[test] @@ -933,7 +930,10 @@ mod tests { run_deviation_system(&mut world); let queue = world.resource::(); - assert!(queue.is_empty(), "on-schedule NPC should not emit a deviation event"); + assert!( + queue.is_empty(), + "on-schedule NPC should not emit a deviation event" + ); } #[test] diff --git a/server/src/npc/tell_state.rs b/server/src/npc/tell_state.rs index a982d30e3..3f477ba53 100644 --- a/server/src/npc/tell_state.rs +++ b/server/src/npc/tell_state.rs @@ -154,8 +154,16 @@ pub fn derive_tell_state( (With, With), >, ) { - for (secret, tolerance, contentment, mood_state, relationships_opt, deviation_opt, kg_opt, mut tell) in - npcs.iter_mut() + for ( + secret, + tolerance, + contentment, + mood_state, + relationships_opt, + deviation_opt, + kg_opt, + mut tell, + ) in npcs.iter_mut() { tell.category = derive_category( secret, @@ -177,10 +185,10 @@ pub fn derive_tell_state( mod tests { use super::*; use crate::knowledge::types::StableId; - use crate::npc::{Relationship, RelationshipKind, RoutineDeviation as NpcRoutineDeviation}; - use crate::npc::{SecretSeverity, ToleranceThreshold}; use crate::npc::mood::NpcMood; use crate::npc::DeviationTrigger; + use crate::npc::{Relationship, RelationshipKind, RoutineDeviation as NpcRoutineDeviation}; + use crate::npc::{SecretSeverity, ToleranceThreshold}; fn neutral_secret() -> Secret { Secret { @@ -218,7 +226,10 @@ mod tests { } fn mood(m: NpcMood) -> MoodState { - MoodState { mood: m, changed_tick: 0 } + MoodState { + mood: m, + changed_tick: 0, + } } fn positive_relationships() -> Relationships { @@ -539,7 +550,7 @@ mod tests { let result = derive_category( &neutral_secret(), &tolerance(0, 50), - &contentment(-5), // Not low enough for angry + &contentment(-5), // Not low enough for angry &mood(NpcMood::Anxious), // Not Hostile None, None, @@ -554,8 +565,8 @@ mod tests { #[test] fn system_updates_derived_tell_state() { - use bevy_ecs::world::World; use crate::npc::Npc; + use bevy_ecs::world::World; let mut world = World::new(); @@ -583,8 +594,8 @@ mod tests { #[test] fn system_sets_none_for_neutral_npc() { - use bevy_ecs::world::World; use crate::npc::Npc; + use bevy_ecs::world::World; let mut world = World::new(); @@ -721,8 +732,8 @@ mod tests { // Priority 2 (Nervous) must win over Priority 3 (Angry). let result = derive_category( &major_secret(), - &tolerance(80, 100), // stress*2=160 > 100 → Nervous - &contentment(-50), // < -20 → Angry condition met + &tolerance(80, 100), // stress*2=160 > 100 → Nervous + &contentment(-50), // < -20 → Angry condition met &mood(NpcMood::Hostile), // Angry condition met None, None, @@ -762,8 +773,8 @@ mod tests { // relationship (Friendly). Priority 4 (Guarded) must win over Priority 5. let result = derive_category( &major_secret(), - &tolerance(0, 100), // Low stress — not Nervous - &contentment(50), // > +20 → Friendly condition met + &tolerance(0, 100), // Low stress — not Nervous + &contentment(50), // > +20 → Friendly condition met &mood(NpcMood::Neutral), Some(&positive_relationships()), // Friendly condition met None, diff --git a/server/src/npc/tolerance.rs b/server/src/npc/tolerance.rs index 19d336b18..ffce2a9a2 100644 --- a/server/src/npc/tolerance.rs +++ b/server/src/npc/tolerance.rs @@ -239,7 +239,10 @@ mod tests { run_system(&mut world); let queue = world.resource::(); - assert!(queue.is_empty(), "stress=49 < threshold=50 should not breach"); + assert!( + queue.is_empty(), + "stress=49 < threshold=50 should not breach" + ); } #[test] @@ -402,8 +405,7 @@ mod tests { // Should not crash — query requires ToleranceThreshold, so entity is simply skipped let mut world = setup_world(); world.spawn(( - Npc, - ActiveSim, + Npc, ActiveSim, // No ToleranceThreshold component )); @@ -529,7 +531,10 @@ mod tests { run_system(&mut world); let queue = world.resource::(); - assert!(queue.is_empty(), "stress=99 < threshold=100 should not breach"); + assert!( + queue.is_empty(), + "stress=99 < threshold=100 should not breach" + ); } #[test] diff --git a/server/src/npc/trait_modifiers.rs b/server/src/npc/trait_modifiers.rs index 677c78174..8f7e86607 100644 --- a/server/src/npc/trait_modifiers.rs +++ b/server/src/npc/trait_modifiers.rs @@ -110,10 +110,8 @@ impl Stage1Filter { } // Exclude ToldBy-source facts (Cautious behavior) - if self.exclude_told_by { - if matches!(fact.source, KnowledgeSource::ToldBy { .. }) { - return false; - } + if self.exclude_told_by && matches!(fact.source, KnowledgeSource::ToldBy { .. }) { + return false; } true @@ -192,9 +190,11 @@ impl TraitModifierConfig { /// Delegates to `KnowledgeConfidence::try_from` (which accepts both /// camelCase and underscore forms) rather than duplicating the match. fn parse_confidence(s: &str) -> Option { - KnowledgeConfidence::try_from(s).map_err(|e| { - tracing::warn!("Unknown confidence level in trait config: {}", e); - }).ok() + KnowledgeConfidence::try_from(s) + .map_err(|e| { + tracing::warn!("Unknown confidence level in trait config: {}", e); + }) + .ok() } /// Convert a `PersonalityTrait` to its lowercase YAML key. @@ -286,8 +286,14 @@ modifiers: KnowledgeSource::DirectObservation { tick: 50 }, ); - assert!(!cautious.allows_fact(&suspects_fact), "Cautious excludes Suspects"); - assert!(cautious.allows_fact(&details_fact), "Cautious allows KnowsDetails"); + assert!( + !cautious.allows_fact(&suspects_fact), + "Cautious excludes Suspects" + ); + assert!( + cautious.allows_fact(&details_fact), + "Cautious allows KnowsDetails" + ); } #[test] @@ -302,7 +308,10 @@ modifiers: tick: 50, }, ); - assert!(!cautious.allows_fact(&told_fact), "Cautious excludes ToldBy"); + assert!( + !cautious.allows_fact(&told_fact), + "Cautious excludes ToldBy" + ); } #[test] @@ -314,7 +323,10 @@ modifiers: KnowledgeConfidence::Suspects, KnowledgeSource::DirectObservation { tick: 50 }, ); - assert!(gossipy.allows_fact(&suspects_fact), "Gossipy includes Suspects"); + assert!( + gossipy.allows_fact(&suspects_fact), + "Gossipy includes Suspects" + ); } #[test] @@ -417,10 +429,22 @@ modifiers: #[test] fn parse_confidence_values() { - assert_eq!(parse_confidence("suspects"), Some(KnowledgeConfidence::Suspects)); - assert_eq!(parse_confidence("knows_of"), Some(KnowledgeConfidence::KnowsOf)); - assert_eq!(parse_confidence("knows_details"), Some(KnowledgeConfidence::KnowsDetails)); - assert_eq!(parse_confidence("direct"), Some(KnowledgeConfidence::Direct)); + assert_eq!( + parse_confidence("suspects"), + Some(KnowledgeConfidence::Suspects) + ); + assert_eq!( + parse_confidence("knows_of"), + Some(KnowledgeConfidence::KnowsOf) + ); + assert_eq!( + parse_confidence("knows_details"), + Some(KnowledgeConfidence::KnowsDetails) + ); + assert_eq!( + parse_confidence("direct"), + Some(KnowledgeConfidence::Direct) + ); assert_eq!(parse_confidence("invalid"), None); } diff --git a/server/src/npc/vision.rs b/server/src/npc/vision.rs index a095c5ab4..5872e6bb7 100644 --- a/server/src/npc/vision.rs +++ b/server/src/npc/vision.rs @@ -30,8 +30,8 @@ use crate::perception::shadowcast::compute_fov; use crate::perception::vision_cone::{apply_vision_cone, Facing, VisionConeConfig}; use crate::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap}; use crate::simulation::spatial::{NaiveSpatialIndex, SpatialIndex}; -use crate::simulation::time::{SimulationTime, TICKS_PER_GAME_MINUTE}; use crate::simulation::tier::ActiveSim; +use crate::simulation::time::{SimulationTime, TICKS_PER_GAME_MINUTE}; /// NPC vision range in tiles (matches player forward range from VisionConeConfig). pub const NPC_VISION_RANGE: i32 = 20; @@ -128,8 +128,7 @@ pub fn compute_npc_vision( // Collect visible tile positions — apply vision cone if NPC has facing let visible_positions: BTreeSet<(i32, i32)> = if let Some(facing_comp) = facing { - let cone_tiles = - apply_vision_cone(&fov, npc_pos.x, npc_pos.y, facing_comp.0, &config); + let cone_tiles = apply_vision_cone(&fov, npc_pos.x, npc_pos.y, facing_comp.0, &config); cone_tiles.into_iter().map(|(x, y, _)| (x, y)).collect() } else { // No facing → omnidirectional vision (full FOV) @@ -261,7 +260,7 @@ pub fn degrade_npc_inferences( time: Res, mut npc_query: Query<&mut NpcMemory, (With, With)>, ) { - if time.tick % TICKS_PER_GAME_MINUTE != 0 { + if !time.tick.is_multiple_of(TICKS_PER_GAME_MINUTE) { return; } @@ -595,7 +594,10 @@ mod tests { schedule.run(&mut world); let queue = world.resource::(); - assert!(!queue.is_empty(), "should emit DirectObservation for player"); + assert!( + !queue.is_empty(), + "should emit DirectObservation for player" + ); let event = &queue.events[0]; assert_eq!(event.observer, npc); @@ -653,7 +655,10 @@ mod tests { KnowledgeEventType::LeftLOS { target } if target == player ) }); - assert!(has_left_los, "should emit LeftLOS when player leaves NPC LOS"); + assert!( + has_left_los, + "should emit LeftLOS when player leaves NPC LOS" + ); // Check zone inference was created let npc_memory = world.get::(npc).unwrap(); @@ -705,7 +710,10 @@ mod tests { let entry = memory.last_known.get(&player_sid).unwrap(); assert_eq!(entry.position, pos(10, 10)); assert_eq!(entry.observed_tick, 100); - assert!(entry.zone_inference.is_none(), "active observation = no inference"); + assert!( + entry.zone_inference.is_none(), + "active observation = no inference" + ); } // --- degrade_npc_inferences tests --- diff --git a/server/src/perception/observer/mod.rs b/server/src/perception/observer/mod.rs index c1e546964..28ea21ab3 100644 --- a/server/src/perception/observer/mod.rs +++ b/server/src/perception/observer/mod.rs @@ -9,7 +9,6 @@ use bevy_ecs::prelude::*; use std::collections::BTreeSet; - use crate::bridge::types::*; use crate::knowledge::graph::filter_by_access; use crate::knowledge::types::{AccessRule, KnowledgeState}; @@ -28,11 +27,11 @@ use crate::simulation::monologue::{MonologueBuffer, SprintAnomalyQueue}; use crate::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap}; use crate::simulation::poi::PointOfInterest; use crate::simulation::rng::SimRng; -use crate::simulation::triangle::TriangleCrisisEventQueue; use crate::simulation::sound::SoundEventQueue; use crate::simulation::stance::Stance; use crate::simulation::ticker::{TickerPool, LAST_SHIFT_ZONE_ID}; use crate::simulation::time::SimulationTime; +use crate::simulation::triangle::TriangleCrisisEventQueue; use crate::simulation::zone::ZoneMap; /// Compute visibility geometry using the active perception mode. @@ -154,12 +153,15 @@ pub fn compute_observer_snapshot( .unwrap_or_default(); // Resolve observer's StableId for component-level access control (#139, D-010) - let observer_stable_id = registry - .to_stable(observer_entity) - .unwrap_or(StableId(0)); + let observer_stable_id = registry.to_stable(observer_entity).unwrap_or(StableId(0)); - let (mut entities, visible_ids, blocked_entities) = - filter_visible_entities(&geometry, ®istry, observer_kg, observer_stable_id, &all_entities); + let (mut entities, visible_ids, blocked_entities) = filter_visible_entities( + &geometry, + ®istry, + observer_kg, + observer_stable_id, + &all_entities, + ); collect_remembered_entities( observer_kg, @@ -210,13 +212,14 @@ 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()).map( - |evt| crate::bridge::types::ExamineResultWire { + let examine_result = examine_result_buffer_opt + .as_mut() + .and_then(|buf| buf.take()) + .map(|evt| crate::bridge::types::ExamineResultWire { entity_id: evt.target_entity_id, text: evt.text, confidence: crate::knowledge::types::KnowledgeConfidence::KnowsDetails, - }, - ); + }); let scan_events = scan_event_buffer_opt .as_mut() .map(|buf| buf.take()) @@ -438,9 +441,7 @@ pub fn compute_observer_snapshot( }; // Drain sim errors collected this tick (#85) - let sim_errors = error_buffer - .map(|mut buf| buf.drain()) - .unwrap_or_default(); + let sim_errors = error_buffer.map(|mut buf| buf.drain()).unwrap_or_default(); // News ticker (#591): populate when player is in The Last Shift zone. let player_zone = zone_map @@ -472,9 +473,10 @@ pub fn compute_observer_snapshot( conversation_events, conversation_ended, follow_state, - character_pressure: pressure_query.iter().next().map(|p| { - crate::simulation::pressure::CharacterPressureWire::from(p) - }), + character_pressure: pressure_query + .iter() + .next() + .map(crate::simulation::pressure::CharacterPressureWire::from), sound_events, rng_seed: sim_rng.as_deref().map(|r| r.seed()), poi_list, diff --git a/server/src/perception/observer/tests.rs b/server/src/perception/observer/tests.rs index 69a9fb7ae..4ee5f93d6 100644 --- a/server/src/perception/observer/tests.rs +++ b/server/src/perception/observer/tests.rs @@ -2258,7 +2258,9 @@ fn access_rule_knowledge_gated_passes_with_matching_fact() { .spawn(( crate::npc::Npc, TilePosition::new(16, 14, 0), - AccessRule(ObserverAccess::KnowledgeGated("contraband.ring_exists".into())), + AccessRule(ObserverAccess::KnowledgeGated( + "contraband.ring_exists".into(), + )), )) .id(); let npc_sid = registry.register(npc); @@ -2323,7 +2325,9 @@ fn access_rule_knowledge_gated_redacts_without_fact() { .spawn(( crate::npc::Npc, TilePosition::new(16, 14, 0), - AccessRule(ObserverAccess::KnowledgeGated("conspiracy.mastermind".into())), + AccessRule(ObserverAccess::KnowledgeGated( + "conspiracy.mastermind".into(), + )), )) .id(); let npc_sid = registry.register(npc); @@ -2636,9 +2640,15 @@ fn tell_state_nervous_appears_in_snapshot_for_major_secret_high_stress() { severity: SecretSeverity::Major, known_by: vec![], }, - ToleranceThreshold { current_stress: 60, threshold: 100 }, + ToleranceThreshold { + current_stress: 60, + threshold: 100, + }, Contentment { level: 0 }, - MoodState { mood: NpcMood::Neutral, changed_tick: 0 }, + MoodState { + mood: NpcMood::Neutral, + changed_tick: 0, + }, DerivedTellState::default(), )); @@ -2690,9 +2700,15 @@ fn tell_state_none_for_neutral_npc_in_snapshot() { severity: SecretSeverity::Minor, known_by: vec![], }, - ToleranceThreshold { current_stress: 10, threshold: 100 }, + ToleranceThreshold { + current_stress: 10, + threshold: 100, + }, Contentment { level: 0 }, - MoodState { mood: NpcMood::Neutral, changed_tick: 0 }, + MoodState { + mood: NpcMood::Neutral, + changed_tick: 0, + }, DerivedTellState::default(), )); @@ -2708,8 +2724,7 @@ fn tell_state_none_for_neutral_npc_in_snapshot() { .expect("NPC should be visible in snapshot"); assert_eq!( - npc.tell_state, - None, + npc.tell_state, None, "neutral NPC should have no tell state in snapshot" ); } @@ -2745,8 +2760,7 @@ fn tell_state_none_when_npc_has_no_derived_tell_component() { .expect("NPC should be visible in snapshot"); assert_eq!( - npc.tell_state, - None, + npc.tell_state, None, "NPC without DerivedTellState component should have tell_state = None" ); } diff --git a/server/src/settings/mod.rs b/server/src/settings/mod.rs index 52f506e22..59bbcf7a8 100644 --- a/server/src/settings/mod.rs +++ b/server/src/settings/mod.rs @@ -192,13 +192,12 @@ pub struct SettingsPlugin; impl Plugin for SettingsPlugin { fn build(&self, app: &mut App) { - app.init_resource::() - .add_systems( - Update, - process_settings_commands - .after(crate::simulation::input::process_player_input) - .before(crate::perception::observer::compute_observer_snapshot), - ); + app.init_resource::().add_systems( + Update, + process_settings_commands + .after(crate::simulation::input::process_player_input) + .before(crate::perception::observer::compute_observer_snapshot), + ); tracing::debug!("SettingsPlugin initialized"); } } diff --git a/server/src/settings/store.rs b/server/src/settings/store.rs index 58f176d67..4bdfbe8cb 100644 --- a/server/src/settings/store.rs +++ b/server/src/settings/store.rs @@ -129,10 +129,7 @@ impl SettingsStore { } /// Get all settings as a Vec for wire serialization. - pub fn get_all_entries( - &self, - player_id: &str, - ) -> Result, rusqlite::Error> { + pub fn get_all_entries(&self, player_id: &str) -> Result, rusqlite::Error> { self.get_all(player_id).map(|map| { map.into_iter() .map(|(key, value)| SettingEntry { key, value }) @@ -175,7 +172,10 @@ fn columns_to_value( "float" => Ok(SettingValue::Float(row.get::<_, f64>(offset + 2)?)), "bool" => Ok(SettingValue::Bool(row.get::<_, bool>(offset + 3)?)), other => { - tracing::warn!("unknown settings value_type '{}', treating as String", other); + tracing::warn!( + "unknown settings value_type '{}', treating as String", + other + ); Ok(SettingValue::String(format!("", other))) } } @@ -252,14 +252,16 @@ mod tests { #[test] fn player_isolation() { let store = test_store(); - store - .set("p1", "volume", &SettingValue::Int(50)) - .unwrap(); - store - .set("p2", "volume", &SettingValue::Int(90)) - .unwrap(); - assert_eq!(store.get("p1", "volume").unwrap(), Some(SettingValue::Int(50))); - assert_eq!(store.get("p2", "volume").unwrap(), Some(SettingValue::Int(90))); + store.set("p1", "volume", &SettingValue::Int(50)).unwrap(); + store.set("p2", "volume", &SettingValue::Int(90)).unwrap(); + assert_eq!( + store.get("p1", "volume").unwrap(), + Some(SettingValue::Int(50)) + ); + assert_eq!( + store.get("p2", "volume").unwrap(), + Some(SettingValue::Int(90)) + ); } #[test] diff --git a/server/src/simulation/chunk_streaming.rs b/server/src/simulation/chunk_streaming.rs index a0282c302..3010809b9 100644 --- a/server/src/simulation/chunk_streaming.rs +++ b/server/src/simulation/chunk_streaming.rs @@ -66,7 +66,7 @@ pub fn chunk_streaming( }; // Cadence gate — only run every N ticks - if cadence.ticks > 0 && time.tick % cadence.ticks != 0 { + if cadence.ticks > 0 && !time.tick.is_multiple_of(cadence.ticks) { return; } diff --git a/server/src/simulation/conversation.rs b/server/src/simulation/conversation.rs index 739292e88..4d1048041 100644 --- a/server/src/simulation/conversation.rs +++ b/server/src/simulation/conversation.rs @@ -352,7 +352,9 @@ pub fn run_npc_conversations( } // Start conversation - let duration = rng.rng.random_range(MIN_DURATION_TICKS..=MAX_DURATION_TICKS); + let duration = rng + .rng + .random_range(MIN_DURATION_TICKS..=MAX_DURATION_TICKS); commands.entity(entity_a).insert(NpcConversation { partner: entity_b, started_tick: time.tick, @@ -422,12 +424,15 @@ pub fn run_npc_conversations( } // Check termination: partner moved away or no longer ActiveSim - let partner_ok = npc_query.get(conv.partner).ok().map(|(_, pos, _, _, _, _, _, _)| { - speaker_pos - .manhattan_distance(pos) - .map(|d| d <= CONVERSATION_PROXIMITY) - .unwrap_or(false) - }); + let partner_ok = npc_query + .get(conv.partner) + .ok() + .map(|(_, pos, _, _, _, _, _, _)| { + speaker_pos + .manhattan_distance(pos) + .map(|d| d <= CONVERSATION_PROXIMITY) + .unwrap_or(false) + }); if partner_ok != Some(true) { terminate_conversation( @@ -497,8 +502,7 @@ pub fn run_npc_conversations( // (D-071) wires in. Function signature already accepts the value. let ambient_noise_pct = 0u32; - let drop_pct = - compute_drop_probability(distance, ambient_noise_pct, listening); + let drop_pct = compute_drop_probability(distance, ambient_noise_pct, listening); let occluded = occlude_line(line_text, drop_pct, &mut rng.rng); if let (Some(s_sid), Some(t_sid)) = (speaker_sid, target_sid) { @@ -509,11 +513,13 @@ pub fn run_npc_conversations( .map(|e| e.known_attributes.contains_key("name")) .unwrap_or(false); if known { - speaker_name.clone().unwrap_or_else(|| "Unknown".to_string()) + speaker_name + .clone() + .unwrap_or_else(|| "Unknown".to_string()) } else { speaker_role .as_deref() - .map(|r| display_label_for_role(r)) + .map(display_label_for_role) .unwrap_or_else(|| "Bystander".to_string()) } }; @@ -525,11 +531,13 @@ pub fn run_npc_conversations( .map(|e| e.known_attributes.contains_key("name")) .unwrap_or(false); if known { - partner_real_name.clone().unwrap_or_else(|| "Unknown".to_string()) + partner_real_name + .clone() + .unwrap_or_else(|| "Unknown".to_string()) } else { partner_role .as_deref() - .map(|r| display_label_for_role(r)) + .map(display_label_for_role) .unwrap_or_else(|| "Bystander".to_string()) } }; @@ -607,11 +615,7 @@ fn terminate_conversation( } } - tracing::debug!( - "NPC conversation ended: {:?} ↔ {:?}", - speaker, - partner, - ); + tracing::debug!("NPC conversation ended: {:?} ↔ {:?}", speaker, partner,); } // --------------------------------------------------------------------------- @@ -654,7 +658,10 @@ mod tests { let without_focus = compute_drop_probability(2, 0, false); let with_focus = compute_drop_probability(2, 0, true); - assert!(with_focus < without_focus, "focus should reduce drop probability"); + assert!( + with_focus < without_focus, + "focus should reduce drop probability" + ); assert_eq!(without_focus, 25); // 2 * 100 / 8 = 25 assert_eq!(with_focus, 5); // 25 - 20 = 5 } @@ -1281,13 +1288,11 @@ mod tests { assert_eq!(buffer.events.len(), 1); // No "name" attribute → falls back to role label assert_eq!( - buffer.events[0].speaker_name, - "Dock Worker", + buffer.events[0].speaker_name, "Dock Worker", "speaker with no KG name attribute should show role label" ); assert_eq!( - buffer.events[0].target_name, - "Courier", + buffer.events[0].target_name, "Courier", "target with no KG name attribute should show role label" ); } @@ -1368,13 +1373,11 @@ mod tests { assert_eq!(buffer.events.len(), 1); // "name" attribute present → use NpcName.0 assert_eq!( - buffer.events[0].speaker_name, - "Alice", + buffer.events[0].speaker_name, "Alice", "speaker with KG name attribute should show real name" ); assert_eq!( - buffer.events[0].target_name, - "Bob", + buffer.events[0].target_name, "Bob", "target with KG name attribute should show real name" ); } @@ -1403,11 +1406,17 @@ mod tests { let taken = buffer.take_events(); assert_eq!(taken.len(), 2, "take_events should return all events"); - assert!(buffer.events.is_empty(), "Buffer should be empty after take_events"); + assert!( + buffer.events.is_empty(), + "Buffer should be empty after take_events" + ); // Second call returns empty let taken2 = buffer.take_events(); - assert!(taken2.is_empty(), "Second take_events call should return empty vec"); + assert!( + taken2.is_empty(), + "Second take_events call should return empty vec" + ); } #[test] @@ -1420,7 +1429,10 @@ mod tests { let taken = buffer.take_ended(); assert_eq!(taken.len(), 1, "take_ended should return all end events"); - assert!(buffer.ended.is_empty(), "ended buffer should be empty after take_ended"); + assert!( + buffer.ended.is_empty(), + "ended buffer should be empty after take_ended" + ); // Second call returns empty assert!(buffer.take_ended().is_empty()); diff --git a/server/src/simulation/dialogue.rs b/server/src/simulation/dialogue.rs index ca3622587..acfef774a 100644 --- a/server/src/simulation/dialogue.rs +++ b/server/src/simulation/dialogue.rs @@ -9,7 +9,7 @@ //! is implemented here. //! //! Integration points: -//! - Reads LinePoolIndexResource (content/mod.rs) +//! - Reads LinePoolIndexResource (server/content/mod.rs) //! - Reads KnowledgeGraph + EntityRegistry for access/trust derivation //! - Reads DialogueProfile on NPCs for pool lookup coordinates //! - Writes DialogueResponseBuffer for snapshot inclusion @@ -21,23 +21,23 @@ use bevy_ecs::prelude::*; use rand::Rng; use crate::bridge::types::{DialogueResponseEvent, MonologueEvent, RelationshipState}; -use crate::simulation::conversation::{display_label_for_role, NpcColorIndex, NpcName}; -use crate::storyteller::EngagementRecord; -use crate::simulation::line_pool::{ - AccessTier, IndexedDialogueLine, Mood, Situation, Topic, TrustTier, -}; -use crate::simulation::knowledge_grant::KnowledgeGrant; -use crate::simulation::line_pool::LinePoolIndexResource; use crate::knowledge::content_registry::ContentEntityRegistry; use crate::knowledge::events::{ProcessedEntityGrant, ProcessedFactGrant, ProcessedKnowledgeGrant}; use crate::knowledge::types::{FactId, KnowledgeConfidence, KnowledgeSource, StableId}; use crate::knowledge::{EntityRegistry, KnowledgeGraph}; use crate::npc::interaction::{InteractionEvent, InteractionEventKind, InteractionMemory}; use crate::npc::relationships::{TrustEvent, TrustEventQueue}; +use crate::simulation::conversation::{display_label_for_role, NpcColorIndex, NpcName}; +use crate::simulation::knowledge_grant::KnowledgeGrant; +use crate::simulation::line_pool::LinePoolIndexResource; +use crate::simulation::line_pool::{ + AccessTier, IndexedDialogueLine, Mood, Situation, Topic, TrustTier, +}; use crate::simulation::monologue::{MonologueBuffer, MonologueState}; use crate::simulation::movement::PlayerCharacter; use crate::simulation::rng::SimRng; use crate::simulation::time::SimulationTime; +use crate::storyteller::EngagementRecord; /// Cooldown ticks before the same dialogue line can be selected again. /// At 10 ticks/game-minute, 600 ticks = 1 game-hour. @@ -532,7 +532,9 @@ pub fn process_talk_interaction( .map(|e| e.known_attributes.contains_key("name")) .unwrap_or(false); if known { - npc_name_opt.map(|n| n.0.clone()).unwrap_or_else(|| "Unknown".to_string()) + npc_name_opt + .map(|n| n.0.clone()) + .unwrap_or_else(|| "Unknown".to_string()) } else { display_label_for_role(&profile.role) } @@ -648,7 +650,10 @@ fn emit_knowledge_grant( }; match grant { - KnowledgeGrant::Fact { fact_id, confidence } => { + KnowledgeGrant::Fact { + fact_id, + confidence, + } => { let conf = match KnowledgeConfidence::try_from(confidence.as_str()) { Ok(c) => c, Err(e) => { @@ -658,9 +663,7 @@ fn emit_knowledge_grant( }; let fid = FactId(fact_id.clone()); // Guardrail: NPC must know this fact to grant it (D-079). - let npc_knows = npc_kg_opt - .map(|kg| kg.knows_fact(&fid)) - .unwrap_or(false); + let npc_knows = npc_kg_opt.map(|kg| kg.knows_fact(&fid)).unwrap_or(false); if !npc_knows { tracing::warn!( "KnowledgeGrant dropped: NPC {:?} does not know fact '{}' — grant guardrail", @@ -1110,14 +1113,14 @@ pub fn process_dialogue_response( #[cfg(test)] mod tests { use super::*; + use crate::knowledge::graph::KnowledgeGraph; + use crate::knowledge::registry::EntityRegistry; + use crate::npc::Npc; + use crate::simulation::line_pool::LinePoolIndexResource; use crate::simulation::line_pool::{ AccessTier, IndexedDialogueLine, IndexedDialoguePool, LinePoolIndex, Mood, Situation, Topic, TrustTier, }; - use crate::simulation::line_pool::LinePoolIndexResource; - use crate::knowledge::graph::KnowledgeGraph; - use crate::knowledge::registry::EntityRegistry; - use crate::npc::Npc; use crate::simulation::movement::TilePosition; use crate::simulation::rng::SimRng; use crate::simulation::time::SimulationTime; @@ -2216,10 +2219,15 @@ mod tests { // Run with multiple seeds — Secret-tier line must appear at least once let mut saw_secret_line = false; for seed in 0u64..50 { - world.get_mut::(player).unwrap().response = None; + world + .get_mut::(player) + .unwrap() + .response = None; world.entity_mut(player).insert(TalkRequest { target: npc }); // Reset cooldown so the pool is not exhausted between iterations - world.entity_mut(player).insert(DialogueCooldownTracker::default()); + world + .entity_mut(player) + .insert(DialogueCooldownTracker::default()); world.insert_resource(SimRng::new(seed)); let mut schedule = bevy_ecs::schedule::Schedule::default(); @@ -2293,9 +2301,14 @@ mod tests { let mut saw_secret = false; for seed in 0u64..50 { - world.get_mut::(player).unwrap().response = None; + world + .get_mut::(player) + .unwrap() + .response = None; world.entity_mut(player).insert(TalkRequest { target: npc }); - world.entity_mut(player).insert(DialogueCooldownTracker::default()); + world + .entity_mut(player) + .insert(DialogueCooldownTracker::default()); world.insert_resource(SimRng::new(seed)); let mut schedule = bevy_ecs::schedule::Schedule::default(); @@ -2388,7 +2401,10 @@ mod tests { let mut seen_ids: Vec = Vec::new(); for seed in 0u64..10 { - world.get_mut::(player).unwrap().response = None; + world + .get_mut::(player) + .unwrap() + .response = None; world.entity_mut(player).insert(TalkRequest { target: npc }); // NOTE: SimulationTime is NOT advanced — all 10 talks happen within tick 0 world.insert_resource(SimRng::new(seed)); diff --git a/server/src/simulation/examine.rs b/server/src/simulation/examine.rs index 8a0012cd9..c13943fa3 100644 --- a/server/src/simulation/examine.rs +++ b/server/src/simulation/examine.rs @@ -223,9 +223,15 @@ pub fn process_examine_interaction( // Try NPC examine path first if let Ok((target_pos, mood_opt, tolerance_opt, traits_opt)) = npc_query.get(target) { - let distance = player_pos.manhattan_distance(target_pos).unwrap_or(u32::MAX); + let distance = player_pos + .manhattan_distance(target_pos) + .unwrap_or(u32::MAX); if distance > CLOSE_RANGE { - tracing::info!(distance, "Examine: NPC target out of range (max {})", CLOSE_RANGE); + tracing::info!( + distance, + "Examine: NPC target out of range (max {})", + CLOSE_RANGE + ); commands.entity(player_entity).remove::(); return; } @@ -243,12 +249,18 @@ pub fn process_examine_interaction( }, }); - let target_entity_id = registry.to_stable(target).map(|sid| sid.0).unwrap_or_else(|| { - tracing::warn!(?target, "Examine: NPC not in EntityRegistry, using bits"); - target.to_bits() - }); + let target_entity_id = registry + .to_stable(target) + .map(|sid| sid.0) + .unwrap_or_else(|| { + tracing::warn!(?target, "Examine: NPC not in EntityRegistry, using bits"); + target.to_bits() + }); - result_buffer.result = Some(ExamineResultEvent { text, target_entity_id }); + result_buffer.result = Some(ExamineResultEvent { + text, + target_entity_id, + }); tracing::debug!(target_entity_id, "Examine: NPC result written to buffer"); commands.entity(player_entity).remove::(); return; @@ -256,9 +268,15 @@ pub fn process_examine_interaction( // Object examine path: entity has a TilePosition but no NPC mood components. if let Ok((target_pos, examine_text_opt)) = examine_text_query.get(target) { - let distance = player_pos.manhattan_distance(target_pos).unwrap_or(u32::MAX); + let distance = player_pos + .manhattan_distance(target_pos) + .unwrap_or(u32::MAX); if distance > CLOSE_RANGE { - tracing::info!(distance, "Examine: object target out of range (max {})", CLOSE_RANGE); + tracing::info!( + distance, + "Examine: object target out of range (max {})", + CLOSE_RANGE + ); commands.entity(player_entity).remove::(); return; } @@ -267,18 +285,27 @@ pub fn process_examine_interaction( .map(|et| et.0.clone()) .unwrap_or_else(|| "No further details are apparent.".to_string()); - let target_entity_id = registry.to_stable(target).map(|sid| sid.0).unwrap_or_else(|| { - tracing::warn!(?target, "Examine: object not in EntityRegistry, using bits"); - target.to_bits() - }); + let target_entity_id = registry + .to_stable(target) + .map(|sid| sid.0) + .unwrap_or_else(|| { + tracing::warn!(?target, "Examine: object not in EntityRegistry, using bits"); + target.to_bits() + }); - result_buffer.result = Some(ExamineResultEvent { text, target_entity_id }); + result_buffer.result = Some(ExamineResultEvent { + text, + target_entity_id, + }); tracing::debug!(target_entity_id, "Examine: object result written to buffer"); commands.entity(player_entity).remove::(); return; } - tracing::warn!(?target, "process_examine_interaction: target has no position component"); + tracing::warn!( + ?target, + "process_examine_interaction: target has no position component" + ); commands.entity(player_entity).remove::(); } @@ -297,35 +324,29 @@ mod tests { #[test] fn smuggler_hostile_npc_gives_threat_read() { - let text = generate_examine_text( - NpcMood::Hostile, - 20, - CharacterArchetype::Smuggler, - None, + let text = generate_examine_text(NpcMood::Hostile, 20, CharacterArchetype::Smuggler, None); + assert!( + text.contains("Threat posture"), + "expected threat read, got: {text}" ); - assert!(text.contains("Threat posture"), "expected threat read, got: {text}"); } #[test] fn smuggler_focused_npc_notes_attention() { - let text = generate_examine_text( - NpcMood::Focused, - 30, - CharacterArchetype::Smuggler, - None, + let text = generate_examine_text(NpcMood::Focused, 30, CharacterArchetype::Smuggler, None); + assert!( + text.contains("close attention"), + "expected attention note, got: {text}" ); - assert!(text.contains("close attention"), "expected attention note, got: {text}"); } #[test] fn smuggler_high_stress_identifies_distraction() { - let text = generate_examine_text( - NpcMood::Anxious, - 80, - CharacterArchetype::Smuggler, - None, + let text = generate_examine_text(NpcMood::Anxious, 80, CharacterArchetype::Smuggler, None); + assert!( + text.contains("Too distracted"), + "expected distraction read, got: {text}" ); - assert!(text.contains("Too distracted"), "expected distraction read, got: {text}"); } #[test] @@ -337,17 +358,15 @@ mod tests { CharacterArchetype::Detective, Some(&t), ); - assert!(text.contains("Controlled affect"), "expected concealment note, got: {text}"); + assert!( + text.contains("Controlled affect"), + "expected concealment note, got: {text}" + ); } #[test] fn detective_anxious_npc_notes_stress_markers() { - let text = generate_examine_text( - NpcMood::Anxious, - 50, - CharacterArchetype::Detective, - None, - ); + let text = generate_examine_text(NpcMood::Anxious, 50, CharacterArchetype::Detective, None); assert!( text.contains("stress markers"), "expected stress markers, got: {text}" @@ -356,12 +375,7 @@ mod tests { #[test] fn detective_content_npc_notes_low_guard() { - let text = generate_examine_text( - NpcMood::Content, - 10, - CharacterArchetype::Detective, - None, - ); + let text = generate_examine_text(NpcMood::Content, 10, CharacterArchetype::Detective, None); assert!( text.contains("Less guarded"), "expected low guard note, got: {text}" @@ -370,19 +384,28 @@ mod tests { #[test] fn stress_ratio_zero_when_threshold_zero() { - let t = ToleranceThreshold { current_stress: 50, threshold: 0 }; + let t = ToleranceThreshold { + current_stress: 50, + threshold: 0, + }; assert_eq!(stress_ratio(&t), 0); } #[test] fn stress_ratio_clamped_at_100() { - let t = ToleranceThreshold { current_stress: 200, threshold: 100 }; + let t = ToleranceThreshold { + current_stress: 200, + threshold: 100, + }; assert_eq!(stress_ratio(&t), 100); } #[test] fn stress_ratio_negative_stress_is_zero() { - let t = ToleranceThreshold { current_stress: -10, threshold: 70 }; + let t = ToleranceThreshold { + current_stress: -10, + threshold: 70, + }; assert_eq!(stress_ratio(&t), 0); } diff --git a/server/src/simulation/follow.rs b/server/src/simulation/follow.rs index 266938dbe..d3464ee78 100644 --- a/server/src/simulation/follow.rs +++ b/server/src/simulation/follow.rs @@ -13,11 +13,11 @@ use bevy_ecs::prelude::*; +use crate::npc::tolerance::ToleranceBreached; use crate::npc::{Npc, ToleranceThreshold}; use crate::perception::query::VisibilityGeometry; use crate::simulation::movement::{PlayerCharacter, TilePosition}; use crate::simulation::time::SimulationTime; -use crate::npc::tolerance::ToleranceBreached; // --------------------------------------------------------------------------- // Constants @@ -783,7 +783,10 @@ mod tests { #[test] fn follow_constants_have_expected_values() { // Spec-defined in #241 — changes here break the design contract - assert_eq!(FOLLOW_PROXIMITY_RANGE, 2, "D-241: 'too close' range is 2 Manhattan tiles"); + assert_eq!( + FOLLOW_PROXIMITY_RANGE, 2, + "D-241: 'too close' range is 2 Manhattan tiles" + ); assert_eq!( FOLLOW_SUSPICION_TICKS, 60, "D-241: suspicion starts after 60 sustained proximity ticks" diff --git a/server/src/simulation/generator.rs b/server/src/simulation/generator.rs index ce814ac98..35c81dd3b 100644 --- a/server/src/simulation/generator.rs +++ b/server/src/simulation/generator.rs @@ -80,7 +80,7 @@ pub type Season = String; pub type RoleSlot = String; /// Day phase (morning, afternoon, evening, night, late-night). Stub. pub type DayPhase = String; -/// Triangle template reference (links to content/templates/). Stub. +/// Triangle template reference (links to server/content/templates/). Stub. pub type TriangleTemplate = String; /// Raw chunk tile data for generator use (64×64 bool grid, true = walkable). Stub. pub type GeneratorChunkData = Vec; @@ -559,7 +559,10 @@ mod tests { vertical_corridors: vec![], hosted_sites: vec![], }; - assert_eq!(r.base_z, -30, "deep mine base_z must be representable as i8"); + assert_eq!( + r.base_z, -30, + "deep mine base_z must be representable as i8" + ); assert_eq!(r.z_levels, 30u8, "z_levels count must remain u8"); } diff --git a/server/src/simulation/input.rs b/server/src/simulation/input.rs index d88111251..e6657c213 100644 --- a/server/src/simulation/input.rs +++ b/server/src/simulation/input.rs @@ -4,9 +4,9 @@ use crate::bridge::debug::DebugCommandBuffer; use crate::bridge::types::{FacingDirection, ObjectType, PlayerAction, PlayerInput}; -use crate::settings::{SettingsCommand, SettingsCommandBuffer}; use crate::knowledge::{EntityRegistry, StableId}; use crate::perception::vision_cone::{facing_from_delta, Facing}; +use crate::settings::{SettingsCommand, SettingsCommandBuffer}; use crate::simulation::interaction::{DoorInteractRequest, DoorState, TerminalInteractRequest}; use crate::simulation::inventory::{ find_next_slot, occupied_slots_for, CarriedBy, InventorySlot, ItemName, MAX_INVENTORY_SLOTS, @@ -207,93 +207,97 @@ pub fn process_player_input( } } match verb.as_deref() { - Some("Take") => { - handle_take( - &mut commands, - ®istry, - &player_query, - &inventory_items, - target_entity_id, - ); + Some("Take") => { + handle_take( + &mut commands, + ®istry, + &player_query, + &inventory_items, + target_entity_id, + ); + } + Some("Place") => { + handle_place(&mut commands, ®istry, &player_query, target_entity_id); + } + Some("Talk") => { + handle_talk( + &mut commands, + ®istry, + &player_query, + &all_positions, + target_entity_id, + ); + } + Some("Follow") => { + handle_follow( + &mut commands, + ®istry, + &player_query, + &all_positions, + target_entity_id, + current_tick, + ); + } + Some("Examine NPC") + | Some("ExamineNpc") + | Some("Examine Object") + | Some("ExamineObject") + | Some("Observe") => { + handle_examine( + &mut commands, + ®istry, + &player_query, + &all_positions, + target_entity_id, + ); + } + Some("Confront") => { + handle_confront( + &mut commands, + ®istry, + &player_query, + &all_positions, + target_entity_id, + ); + } + Some("Reset") => { + handle_reset( + &mut commands, + ®istry, + &reset_triggers, + &mut room_snapshots, + target_entity_id, + current_tick, + ); + } + // #246: Door and Terminal behavior + Some("Open") | Some("Close") => { + handle_door_interact( + &mut commands, + ®istry, + &player_query, + &door_states, + target_entity_id, + ); + } + Some("Use") => { + handle_terminal_interact( + &mut commands, + ®istry, + &player_query, + &object_types, + target_entity_id, + ); + } + _ => { + tracing::info!( + "Interact: target={:?}, verb={:?} — logged only", + target_entity_id, + verb, + ); + } } - Some("Place") => { - handle_place(&mut commands, ®istry, &player_query, target_entity_id); - } - Some("Talk") => { - handle_talk( - &mut commands, - ®istry, - &player_query, - &all_positions, - target_entity_id, - ); - } - Some("Follow") => { - handle_follow( - &mut commands, - ®istry, - &player_query, - &all_positions, - target_entity_id, - current_tick, - ); - } - Some("Examine NPC") | Some("ExamineNpc") | Some("Examine Object") - | Some("ExamineObject") | Some("Observe") => { - handle_examine( - &mut commands, - ®istry, - &player_query, - &all_positions, - target_entity_id, - ); - } - Some("Confront") => { - handle_confront( - &mut commands, - ®istry, - &player_query, - &all_positions, - target_entity_id, - ); - } - Some("Reset") => { - handle_reset( - &mut commands, - ®istry, - &reset_triggers, - &mut room_snapshots, - target_entity_id, - current_tick, - ); - } - // #246: Door and Terminal behavior - Some("Open") | Some("Close") => { - handle_door_interact( - &mut commands, - ®istry, - &player_query, - &door_states, - target_entity_id, - ); - } - Some("Use") => { - handle_terminal_interact( - &mut commands, - ®istry, - &player_query, - &object_types, - target_entity_id, - ); - } - _ => { - tracing::info!( - "Interact: target={:?}, verb={:?} — logged only", - target_entity_id, - verb, - ); - } - }} + } PlayerAction::WalkAway => { if let Ok((player_entity, _, _, _)) = player_query.single() { commands @@ -383,21 +387,27 @@ pub fn process_player_input( if let Some(ref mut buf) = settings_cmd_buffer { buf.push(SettingsCommand::Change { key, value }); } else { - tracing::warn!("ChangeSetting received but SettingsCommandBuffer not registered"); + tracing::warn!( + "ChangeSetting received but SettingsCommandBuffer not registered" + ); } } PlayerAction::RequestAllSettings => { if let Some(ref mut buf) = settings_cmd_buffer { buf.push(SettingsCommand::RequestAll); } else { - tracing::warn!("RequestAllSettings received but SettingsCommandBuffer not registered"); + tracing::warn!( + "RequestAllSettings received but SettingsCommandBuffer not registered" + ); } } PlayerAction::DeleteSetting { key } => { if let Some(ref mut buf) = settings_cmd_buffer { buf.push(SettingsCommand::Delete { key }); } else { - tracing::warn!("DeleteSetting received but SettingsCommandBuffer not registered"); + tracing::warn!( + "DeleteSetting received but SettingsCommandBuffer not registered" + ); } } } @@ -663,7 +673,10 @@ fn handle_dialogue_response( let target_stable = StableId(target_entity_id); let Some(target_entity) = registry.to_entity(&target_stable) else { - tracing::warn!(target_entity_id, "DialogueResponse: target entity not in registry"); + tracing::warn!( + target_entity_id, + "DialogueResponse: target entity not in registry" + ); return; }; @@ -690,7 +703,11 @@ fn handle_dialogue_response( response_id: response_id.to_string(), }); - tracing::debug!(target_entity_id, response_id, "DialogueResponse: marker set on player"); + tracing::debug!( + target_entity_id, + response_id, + "DialogueResponse: marker set on player" + ); } /// Handle Confront verb: set ConfrontationDelivered marker on the player entity (#520, D-063). @@ -971,11 +988,14 @@ fn handle_door_interact( return; }; - commands - .entity(player_entity) - .insert(DoorInteractRequest { door_entity: target_entity }); + commands.entity(player_entity).insert(DoorInteractRequest { + door_entity: target_entity, + }); - tracing::debug!(target_id, "Door interact: DoorInteractRequest inserted on player"); + tracing::debug!( + target_id, + "Door interact: DoorInteractRequest inserted on player" + ); } /// Handle Terminal Use: insert `TerminalInteractRequest` on the player entity (#246). @@ -1001,7 +1021,10 @@ fn handle_terminal_interact( let target_stable = StableId(target_id); let Some(target_entity) = registry.to_entity(&target_stable) else { - tracing::warn!(target_id, "Terminal interact: target entity not in registry"); + tracing::warn!( + target_id, + "Terminal interact: target entity not in registry" + ); return; }; @@ -1020,9 +1043,14 @@ fn handle_terminal_interact( commands .entity(player_entity) - .insert(TerminalInteractRequest { terminal_entity: target_entity }); + .insert(TerminalInteractRequest { + terminal_entity: target_entity, + }); - tracing::debug!(target_id, "Terminal interact: TerminalInteractRequest inserted on player"); + tracing::debug!( + target_id, + "Terminal interact: TerminalInteractRequest inserted on player" + ); } /// Handle TeleportToHub: move player to hub spawn, clear interaction state (#491). diff --git a/server/src/simulation/interaction.rs b/server/src/simulation/interaction.rs index 6cff96115..7b897013a 100644 --- a/server/src/simulation/interaction.rs +++ b/server/src/simulation/interaction.rs @@ -16,8 +16,8 @@ use serde::{Deserialize, Serialize}; // Re-export ObjectType for backward compatibility — definition moved to bridge::types (#422). pub use crate::bridge::types::ObjectType; use crate::bridge::types::{EntityKind, MovementStance, NearbyInteraction, VerbKind, VerbOption}; -use crate::knowledge::EntityRegistry; use crate::knowledge::types::StableId; +use crate::knowledge::EntityRegistry; use crate::npc::Npc; use crate::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap}; use crate::simulation::stance::Stance; @@ -416,10 +416,15 @@ pub fn process_door_interaction( }; let door_entity = req.door_entity; - commands.entity(player_entity).remove::(); + commands + .entity(player_entity) + .remove::(); let Ok(mut door) = door_query.get_mut(door_entity) else { - tracing::warn!(?door_entity, "process_door_interaction: no DoorState on target"); + tracing::warn!( + ?door_entity, + "process_door_interaction: no DoorState on target" + ); return; }; @@ -455,7 +460,9 @@ pub fn process_terminal_interaction( }; let terminal_entity = req.terminal_entity; - commands.entity(player_entity).remove::(); + commands + .entity(player_entity) + .remove::(); let terminal_id = registry .to_stable(terminal_entity) diff --git a/server/src/simulation/knowledge_grant.rs b/server/src/simulation/knowledge_grant.rs index 7ea651ed1..b0dd1a90d 100644 --- a/server/src/simulation/knowledge_grant.rs +++ b/server/src/simulation/knowledge_grant.rs @@ -20,10 +20,7 @@ use std::collections::BTreeMap; pub enum KnowledgeGrant { /// Grant knowledge of a non-entity fact. /// Format: fact_id "category.topic", confidence string. - Fact { - fact_id: String, - confidence: String, - }, + Fact { fact_id: String, confidence: String }, /// Grant knowledge of an entity (creates EntityKnowledge entry in observer's KG). /// Required for contradiction detection: testimony must create ToldBy EntityKnowledge /// so a subsequent DirectObservation can detect a discrepancy (D-079, D-083). diff --git a/server/src/simulation/line_pool.rs b/server/src/simulation/line_pool.rs index c9fc1ddec..1420d6af8 100644 --- a/server/src/simulation/line_pool.rs +++ b/server/src/simulation/line_pool.rs @@ -467,10 +467,19 @@ mod tests { #[test] fn access_tier_parse_all_values() { assert_eq!("public".parse::().unwrap(), AccessTier::Public); - assert_eq!("insider".parse::().unwrap(), AccessTier::Insider); - assert_eq!("authority".parse::().unwrap(), AccessTier::Authority); + assert_eq!( + "insider".parse::().unwrap(), + AccessTier::Insider + ); + assert_eq!( + "authority".parse::().unwrap(), + AccessTier::Authority + ); assert_eq!("peer".parse::().unwrap(), AccessTier::Peer); - assert_eq!("hostile".parse::().unwrap(), AccessTier::Hostile); + assert_eq!( + "hostile".parse::().unwrap(), + AccessTier::Hostile + ); assert!("invalid".parse::().is_err()); } @@ -486,12 +495,29 @@ mod tests { #[test] fn situation_parse_all_values() { let values = [ - "arrival", "shift_start", "shift_end", "shift_transition", "bar_evening", - "night_shift", "investigation", "confrontation", "social", "alone", - "emergency", "routine", "observation", "greeting", "first_meeting", "repeated_visit", + "arrival", + "shift_start", + "shift_end", + "shift_transition", + "bar_evening", + "night_shift", + "investigation", + "confrontation", + "social", + "alone", + "emergency", + "routine", + "observation", + "greeting", + "first_meeting", + "repeated_visit", ]; for v in values { - assert!(v.parse::().is_ok(), "Failed to parse situation: {}", v); + assert!( + v.parse::().is_ok(), + "Failed to parse situation: {}", + v + ); } assert!("invalid".parse::().is_err()); } @@ -499,8 +525,15 @@ mod tests { #[test] fn topic_parse_all_values() { let values = [ - "colleague", "routine", "cargo", "money", "trust", "danger", - "institution", "personal", "investigation", + "colleague", + "routine", + "cargo", + "money", + "trust", + "danger", + "institution", + "personal", + "investigation", ]; for v in values { assert!(v.parse::().is_ok(), "Failed to parse topic: {}", v); @@ -510,8 +543,14 @@ mod tests { #[test] fn mood_parse_all_values() { let values = [ - "anxious", "frustrated", "content", "suspicious", "warm", - "hostile", "relieved", "focused", + "anxious", + "frustrated", + "content", + "suspicious", + "warm", + "hostile", + "relieved", + "focused", ]; for v in values { assert!(v.parse::().is_ok(), "Failed to parse mood: {}", v); @@ -521,19 +560,35 @@ mod tests { #[test] fn trigger_parse_all_values() { let values = [ - "enter_location", "observe_npc", "hear_sound", "observe_anomaly", - "post_conversation", "discover_evidence", "witness_interaction", - "time_idle", "return_visit", + "enter_location", + "observe_npc", + "hear_sound", + "observe_anomaly", + "post_conversation", + "discover_evidence", + "witness_interaction", + "time_idle", + "return_visit", ]; for v in values { - assert!(v.parse::().is_ok(), "Failed to parse trigger: {}", v); + assert!( + v.parse::().is_ok(), + "Failed to parse trigger: {}", + v + ); } } #[test] fn character_parse() { - assert_eq!("smuggler".parse::().unwrap(), Character::Smuggler); - assert_eq!("detective".parse::().unwrap(), Character::Detective); + assert_eq!( + "smuggler".parse::().unwrap(), + Character::Smuggler + ); + assert_eq!( + "detective".parse::().unwrap(), + Character::Detective + ); assert!("other".parse::().is_err()); } } diff --git a/server/src/simulation/mod.rs b/server/src/simulation/mod.rs index 145c7c596..81740c95b 100644 --- a/server/src/simulation/mod.rs +++ b/server/src/simulation/mod.rs @@ -32,9 +32,9 @@ pub mod save_state; pub mod sound; pub mod spatial; pub mod stance; +pub mod ticker; pub mod tier; pub mod time; -pub mod ticker; pub mod triangle; pub mod zone; diff --git a/server/src/simulation/monologue.rs b/server/src/simulation/monologue.rs index 0af0509a0..09b628bc2 100644 --- a/server/src/simulation/monologue.rs +++ b/server/src/simulation/monologue.rs @@ -22,7 +22,6 @@ use crate::simulation::rng::SimRng; use crate::simulation::time::SimulationTime; use crate::storyteller::EngagementRecord; - /// Display duration for monologue text on client (seconds). const DISPLAY_DURATION: f32 = 5.0; @@ -89,10 +88,7 @@ const HEAR_SOUND_LINES: &[(&str, &str)] = &[ /// Fire when the player overhears an NPC-to-NPC conversation (D-078). /// Future: move to content pools with trigger="witness_interaction". const WITNESS_INTERACTION_LINES: &[(&str, &str)] = &[ - ( - "witness_01", - "Interesting. Wonder what that was about.", - ), + ("witness_01", "Interesting. Wonder what that was about."), ("witness_02", "I should remember what they just said."), ("witness_03", "They didn't know I was listening."), ]; @@ -102,10 +98,7 @@ const WITNESS_INTERACTION_LINES: &[(&str, &str)] = &[ /// Future: move to content pools with trigger="post_conversation". const POST_CONVERSATION_LINES: &[(&str, &str)] = &[ ("post_conv_01", "More questions than answers."), - ( - "post_conv_02", - "I'll have to think about what they said.", - ), + ("post_conv_02", "I'll have to think about what they said."), ( "post_conv_03", "Something about that exchange didn't sit right.", @@ -384,7 +377,10 @@ fn select_hardcoded_fallback(trigger: &str, rng: &mut impl Rng) -> (String, Stri "witness_interaction" => WITNESS_INTERACTION_LINES, "post_conversation" => POST_CONVERSATION_LINES, unknown => { - tracing::warn!("select_hardcoded_fallback: unrecognized trigger '{}', using observe_npc", unknown); + tracing::warn!( + "select_hardcoded_fallback: unrecognized trigger '{}', using observe_npc", + unknown + ); OBSERVE_NPC_LINES } }; @@ -469,7 +465,10 @@ pub fn trigger_event_monologue( .unwrap_or(false) { Some("hear_sound") - } else if conv_buffer_opt.map(|b| !b.events.is_empty()).unwrap_or(false) { + } else if conv_buffer_opt + .map(|b| !b.events.is_empty()) + .unwrap_or(false) + { Some("witness_interaction") } else if !post_conv_npcs.is_empty() { Some("post_conversation") @@ -1425,15 +1424,16 @@ mod tests { let mut world = setup_event_world(); let player = spawn_event_player(&mut world); - world.resource_mut::().events.push( - SoundEvent::at( + world + .resource_mut::() + .events + .push(SoundEvent::at( &TilePosition::new(12, 10, 0), // distance 2 from player at (10,10) SoundEventKind::Machinery, 0.8, crate::knowledge::types::SoundRange::Medium, None, - ), - ); + )); run_event_system(&mut world); @@ -1450,15 +1450,16 @@ mod tests { let mut world = setup_event_world(); let _player = spawn_event_player(&mut world); - world.resource_mut::().events.push( - SoundEvent::at( + world + .resource_mut::() + .events + .push(SoundEvent::at( &TilePosition::new(11, 10, 0), SoundEventKind::Footstep, 0.5, crate::knowledge::types::SoundRange::Close, None, - ), - ); + )); run_event_system(&mut world); @@ -1476,15 +1477,16 @@ mod tests { let _player = spawn_event_player(&mut world); // Machinery sound at distance 20 with Close range (3 tiles) - world.resource_mut::().events.push( - SoundEvent::at( + world + .resource_mut::() + .events + .push(SoundEvent::at( &TilePosition::new(30, 10, 0), // distance 20 from (10,10) SoundEventKind::Machinery, 0.8, crate::knowledge::types::SoundRange::Close, None, - ), - ); + )); run_event_system(&mut world); @@ -1551,12 +1553,11 @@ mod tests { let npc = world.spawn_empty().id(); // Pre-fill buffer (another system wrote first) - world.get_mut::(player).unwrap().event = - Some(MonologueEvent { - id: "existing".to_string(), - text: "Already have something.".to_string(), - duration_seconds: 5.0, - }); + world.get_mut::(player).unwrap().event = Some(MonologueEvent { + id: "existing".to_string(), + text: "Already have something.".to_string(), + duration_seconds: 5.0, + }); world.resource_mut::().push(npc); @@ -1579,12 +1580,11 @@ mod tests { let player = spawn_event_player(&mut world); // Pre-fill buffer - world.get_mut::(player).unwrap().event = - Some(MonologueEvent { - id: "prior_line".to_string(), - text: "I was already thinking.".to_string(), - duration_seconds: 5.0, - }); + world.get_mut::(player).unwrap().event = Some(MonologueEvent { + id: "prior_line".to_string(), + text: "I was already thinking.".to_string(), + duration_seconds: 5.0, + }); // Push observation event that would normally fire world @@ -1625,15 +1625,16 @@ mod tests { observer: player, }); - world.resource_mut::().events.push( - SoundEvent::at( + world + .resource_mut::() + .events + .push(SoundEvent::at( &TilePosition::new(11, 10, 0), SoundEventKind::Machinery, 0.8, crate::knowledge::types::SoundRange::Medium, None, - ), - ); + )); run_event_system(&mut world); @@ -1650,15 +1651,16 @@ mod tests { let player = spawn_event_player(&mut world); // Sound event - world.resource_mut::().events.push( - SoundEvent::at( + world + .resource_mut::() + .events + .push(SoundEvent::at( &TilePosition::new(11, 10, 0), SoundEventKind::Alert, 1.0, crate::knowledge::types::SoundRange::Medium, None, - ), - ); + )); // Conversation event world @@ -1739,15 +1741,16 @@ mod tests { let mut world = setup_event_world(); let player = spawn_event_player(&mut world); - world.resource_mut::().events.push( - SoundEvent::at( + world + .resource_mut::() + .events + .push(SoundEvent::at( &TilePosition::new(10, 11, 0), // distance 1 SoundEventKind::Alert, 1.0, crate::knowledge::types::SoundRange::Long, None, - ), - ); + )); run_event_system(&mut world); @@ -1764,15 +1767,16 @@ mod tests { let _player = spawn_event_player(&mut world); // Sound on z=1, player on z=0 - world.resource_mut::().events.push( - SoundEvent::at( + world + .resource_mut::() + .events + .push(SoundEvent::at( &TilePosition::new(10, 11, 1), // same xy but different z SoundEventKind::Machinery, 0.8, crate::knowledge::types::SoundRange::Medium, None, - ), - ); + )); run_event_system(&mut world); @@ -1814,15 +1818,16 @@ mod tests { let _player = spawn_event_player(&mut world); // Voice sound in range — should NOT trigger (routine background noise) - world.resource_mut::().events.push( - SoundEvent::at( + world + .resource_mut::() + .events + .push(SoundEvent::at( &TilePosition::new(11, 10, 0), // distance 1 SoundEventKind::Voice, 0.7, crate::knowledge::types::SoundRange::Medium, None, - ), - ); + )); run_event_system(&mut world); @@ -1840,15 +1845,16 @@ mod tests { let _player = spawn_event_player(&mut world); // Ambient sound in range — should NOT trigger (background atmosphere) - world.resource_mut::().events.push( - SoundEvent::at( + world + .resource_mut::() + .events + .push(SoundEvent::at( &TilePosition::new(10, 12, 0), // distance 2 SoundEventKind::Ambient, 0.9, crate::knowledge::types::SoundRange::Long, None, - ), - ); + )); run_event_system(&mut world); @@ -1899,12 +1905,14 @@ mod tests { } fn spawn_contradiction_player(world: &mut bevy_ecs::world::World) -> bevy_ecs::entity::Entity { - world.spawn(( - PlayerCharacter, - TilePosition::new(0, 0, 0), - MonologueState::default(), - MonologueBuffer::default(), - )).id() + world + .spawn(( + PlayerCharacter, + TilePosition::new(0, 0, 0), + MonologueState::default(), + MonologueBuffer::default(), + )) + .id() } #[test] @@ -1966,11 +1974,14 @@ mod tests { let player = spawn_contradiction_player(&mut world); // Pre-fill buffer with a higher-priority monologue - world.get_mut::(player).unwrap().set(MonologueEvent { - id: "prior_event".to_string(), - text: "Something already fired.".to_string(), - duration_seconds: 5.0, - }); + world + .get_mut::(player) + .unwrap() + .set(MonologueEvent { + id: "prior_event".to_string(), + text: "Something already fired.".to_string(), + duration_seconds: 5.0, + }); world.resource_mut::().push( crate::knowledge::ContradictionDetectedEvent { @@ -1995,7 +2006,10 @@ mod tests { // Buffer should still have the prior event let buf = world.get::(player).unwrap(); let event = buf.event.as_ref().unwrap(); - assert_eq!(event.id, "prior_event", "prior monologue should not be overridden"); + assert_eq!( + event.id, "prior_event", + "prior monologue should not be overridden" + ); // Queue should have been drained regardless assert!( @@ -2049,15 +2063,15 @@ mod tests { // Tick T3: process_contradiction_monologue fires monologue with Sera/Kael names // // Tests the full D-083 event chain end-to-end. - use crate::knowledge::{ - EntityRegistry, KnowledgeGraph, KnowledgeEventQueue, KnowledgeEventType, - }; use crate::knowledge::events::{process_knowledge_events, KnowledgeEvent}; use crate::knowledge::registry::StableEntityId; use crate::knowledge::types::{ EntityKnowledge, KnowledgeConfidence, KnowledgeSource, KnowledgeState, RelationshipState, StableId, }; + use crate::knowledge::{ + EntityRegistry, KnowledgeEventQueue, KnowledgeEventType, KnowledgeGraph, + }; use std::collections::BTreeMap; let mut world = bevy_ecs::world::World::new(); @@ -2070,10 +2084,9 @@ mod tests { // Spawn NPCs with NpcName components let sera_entity = world.spawn(NpcName("Sera".to_string())).id(); - let kael_entity = world.spawn(( - NpcName("Kael".to_string()), - TilePosition::new(10, 10, 0), - )).id(); + let kael_entity = world + .spawn((NpcName("Kael".to_string()), TilePosition::new(10, 10, 0))) + .id(); let sera_sid = registry.register(sera_entity); let kael_sid = registry.register(kael_entity); @@ -2100,14 +2113,16 @@ mod tests { }, ); - let player = world.spawn(( - PlayerCharacter, - TilePosition::new(0, 0, 0), - MonologueState::default(), - MonologueBuffer::default(), - player_kg, - StableEntityId(StableId(999)), - )).id(); + let player = world + .spawn(( + PlayerCharacter, + TilePosition::new(0, 0, 0), + MonologueState::default(), + MonologueBuffer::default(), + player_kg, + StableEntityId(StableId(999)), + )) + .id(); registry.register(player); world.insert_resource(registry); @@ -2156,13 +2171,19 @@ mod tests { { // Drain to inspect event contents, then re-push for the monologue consumer. let mut events = world.resource_mut::().drain(); - assert_eq!(events.len(), 1, "should have exactly one contradiction event"); + assert_eq!( + events.len(), + 1, + "should have exactly one contradiction event" + ); let event = &events[0]; assert_eq!(event.source_display_name, "Sera"); assert_eq!(event.subject_display_name, "Kael"); // Re-push so process_contradiction_monologue can consume it on T3. let event = events.remove(0); - world.resource_mut::().push(event); + world + .resource_mut::() + .push(event); } // Tick T3: Run process_contradiction_monologue diff --git a/server/src/simulation/movement.rs b/server/src/simulation/movement.rs index 671677117..ae5460cc0 100644 --- a/server/src/simulation/movement.rs +++ b/server/src/simulation/movement.rs @@ -434,15 +434,15 @@ pub fn validate_movement( Some(MovementStance::Careful) => 0.3, Some(MovementStance::Crouch) => 0.15, }; - commands.entity(entity).insert(SoundEventEmitter::new( - SoundEvent::at( + commands + .entity(entity) + .insert(SoundEventEmitter::new(SoundEvent::at( &target, SoundEventKind::Footstep, intensity, SoundRange::Close, None, - ), - )); + ))); } commands.entity(entity).remove::(); } diff --git a/server/src/simulation/npc_knowledge_transfer.rs b/server/src/simulation/npc_knowledge_transfer.rs index 311593acb..34fd7d84c 100644 --- a/server/src/simulation/npc_knowledge_transfer.rs +++ b/server/src/simulation/npc_knowledge_transfer.rs @@ -14,6 +14,7 @@ use bevy_ecs::prelude::*; use rand::Rng; +use std::cmp::Reverse; use std::collections::BTreeMap; use crate::knowledge::registry::StableEntityId; @@ -30,8 +31,8 @@ use crate::npc::Npc; use crate::simulation::conversation::NpcConversation; use crate::simulation::movement::{PlayerCharacter, TilePosition}; use crate::simulation::rng::SimRng; -use crate::simulation::time::SimulationTime; use crate::simulation::tier::ActiveSim; +use crate::simulation::time::SimulationTime; // --------------------------------------------------------------------------- // Constants @@ -123,7 +124,12 @@ pub fn transfer_npc_knowledge( mut event_queue: ResMut, // NPCs that just started a conversation — Added fires once per conversation. new_conv_query: Query< - (Entity, &NpcConversation, &TilePosition, Option<&StableEntityId>), + ( + Entity, + &NpcConversation, + &TilePosition, + Option<&StableEntityId>, + ), (With, With, Added), >, // Read-only StableEntityId on NPC partner (distinct query, no KG conflict). @@ -267,7 +273,7 @@ pub fn transfer_npc_knowledge( } // Sort by most recently updated (deterministic: descending tick, stable by BTreeMap key order) - candidates.sort_by(|a, b| b.sort_key().cmp(&a.sort_key())); + candidates.sort_by_key(|c| Reverse(c.sort_key())); // Take top 1–3 entries by recency (random count, deterministic selection). // The random element is HOW MANY facts transfer, not WHICH ones. @@ -327,13 +333,10 @@ pub fn transfer_npc_knowledge( let capped = ek.confidence.min(KnowledgeConfidence::KnowsOf); // Preserve existing relationship state if the partner already knows this entity. - let (should_write, existing_relationship) = - match partner_kg.entities.get(&id) { - None => (true, RelationshipState::Unknown), - Some(existing) => { - (existing.confidence < capped, existing.relationship) - } - }; + let (should_write, existing_relationship) = match partner_kg.entities.get(&id) { + None => (true, RelationshipState::Unknown), + Some(existing) => (existing.confidence < capped, existing.relationship), + }; if should_write { partner_kg.entities.insert( @@ -438,8 +441,8 @@ mod tests { use crate::simulation::conversation::NpcConversation; use crate::simulation::movement::TilePosition; use crate::simulation::rng::SimRng; - use crate::simulation::time::SimulationTime; use crate::simulation::tier::ActiveSim; + use crate::simulation::time::SimulationTime; fn build_test_world() -> App { let mut app = App::new(); @@ -551,12 +554,14 @@ mod tests { } // Start conversation — tick 0, so started_tick == 0 - app.world_mut().entity_mut(entity_a).insert(NpcConversation { - partner: entity_b, - started_tick: 0, - end_tick: 100, - ticks_since_last_line: 0, - }); + app.world_mut() + .entity_mut(entity_a) + .insert(NpcConversation { + partner: entity_b, + started_tick: 0, + end_tick: 100, + ticks_since_last_line: 0, + }); app.update(); @@ -599,12 +604,14 @@ mod tests { ); } - app.world_mut().entity_mut(entity_a).insert(NpcConversation { - partner: entity_b, - started_tick: 0, - end_tick: 100, - ticks_since_last_line: 0, - }); + app.world_mut() + .entity_mut(entity_a) + .insert(NpcConversation { + partner: entity_b, + started_tick: 0, + end_tick: 100, + ticks_since_last_line: 0, + }); app.update(); @@ -645,12 +652,14 @@ mod tests { ); } - app.world_mut().entity_mut(entity_a).insert(NpcConversation { - partner: entity_b, - started_tick: 0, - end_tick: 100, - ticks_since_last_line: 0, - }); + app.world_mut() + .entity_mut(entity_a) + .insert(NpcConversation { + partner: entity_b, + started_tick: 0, + end_tick: 100, + ticks_since_last_line: 0, + }); app.update(); @@ -703,12 +712,14 @@ mod tests { ); } - app.world_mut().entity_mut(entity_a).insert(NpcConversation { - partner: entity_b, - started_tick: 0, - end_tick: 100, - ticks_since_last_line: 0, - }); + app.world_mut() + .entity_mut(entity_a) + .insert(NpcConversation { + partner: entity_b, + started_tick: 0, + end_tick: 100, + ticks_since_last_line: 0, + }); app.update(); @@ -750,12 +761,14 @@ mod tests { ); } - app.world_mut().entity_mut(entity_a).insert(NpcConversation { - partner: entity_b, - started_tick: 0, - end_tick: 100, - ticks_since_last_line: 0, - }); + app.world_mut() + .entity_mut(entity_a) + .insert(NpcConversation { + partner: entity_b, + started_tick: 0, + end_tick: 100, + ticks_since_last_line: 0, + }); app.update(); @@ -815,12 +828,14 @@ mod tests { ); } - app.world_mut().entity_mut(entity_a).insert(NpcConversation { - partner: entity_b, - started_tick: 0, - end_tick: 100, - ticks_since_last_line: 0, - }); + app.world_mut() + .entity_mut(entity_a) + .insert(NpcConversation { + partner: entity_b, + started_tick: 0, + end_tick: 100, + ticks_since_last_line: 0, + }); app.update(); diff --git a/server/src/simulation/path_follow.rs b/server/src/simulation/path_follow.rs index 39702a655..397c15d42 100644 --- a/server/src/simulation/path_follow.rs +++ b/server/src/simulation/path_follow.rs @@ -56,7 +56,10 @@ impl MovementSpeed { #[tracing::instrument(level = "debug", skip_all)] pub fn follow_paths( mut commands: Commands, - mut query: Query<(Entity, &mut ComputedPath, Option<&mut MovementSpeed>), (With, With)>, + mut query: Query< + (Entity, &mut ComputedPath, Option<&mut MovementSpeed>), + (With, With), + >, ) { for (entity, mut path, speed_opt) in query.iter_mut() { if let Some(mut speed) = speed_opt { diff --git a/server/src/simulation/poi.rs b/server/src/simulation/poi.rs index 04eade838..5f88f9a9e 100644 --- a/server/src/simulation/poi.rs +++ b/server/src/simulation/poi.rs @@ -115,21 +115,41 @@ mod tests { #[test] fn fact_id_uses_poi_namespace() { - let poi = make_poi("docking_bay_7", PoiCategory::Location, PoiVisibility::LineOfSight); + let poi = make_poi( + "docking_bay_7", + PoiCategory::Location, + PoiVisibility::LineOfSight, + ); assert_eq!(poi.fact_id(), FactId("poi.docking_bay_7".to_string())); } #[test] fn fact_id_format_is_deterministic() { - let poi1 = make_poi("cargo_hold", PoiCategory::Hidden, PoiVisibility::KnowledgeOnly); - let poi2 = make_poi("cargo_hold", PoiCategory::Hidden, PoiVisibility::KnowledgeOnly); + let poi1 = make_poi( + "cargo_hold", + PoiCategory::Hidden, + PoiVisibility::KnowledgeOnly, + ); + let poi2 = make_poi( + "cargo_hold", + PoiCategory::Hidden, + PoiVisibility::KnowledgeOnly, + ); assert_eq!(poi1.fact_id(), poi2.fact_id()); } #[test] fn different_poi_ids_produce_different_fact_ids() { - let poi1 = make_poi("bay_alpha", PoiCategory::Location, PoiVisibility::LineOfSight); - let poi2 = make_poi("bay_beta", PoiCategory::Location, PoiVisibility::LineOfSight); + let poi1 = make_poi( + "bay_alpha", + PoiCategory::Location, + PoiVisibility::LineOfSight, + ); + let poi2 = make_poi( + "bay_beta", + PoiCategory::Location, + PoiVisibility::LineOfSight, + ); assert_ne!(poi1.fact_id(), poi2.fact_id()); } @@ -172,7 +192,10 @@ mod tests { #[test] fn poi_discovery_sources_are_distinct() { - assert_ne!(PoiDiscoverySource::MapTemplate, PoiDiscoverySource::Procedural); + assert_ne!( + PoiDiscoverySource::MapTemplate, + PoiDiscoverySource::Procedural + ); assert_ne!( PoiDiscoverySource::QuestGenerated, PoiDiscoverySource::NpcRevealed @@ -183,8 +206,7 @@ mod tests { fn poi_serialization_roundtrip() { let poi = make_poi("med_bay", PoiCategory::Service, PoiVisibility::LineOfSight); let serialized = serde_yaml::to_string(&poi).expect("serialize"); - let deserialized: PointOfInterest = - serde_yaml::from_str(&serialized).expect("deserialize"); + let deserialized: PointOfInterest = serde_yaml::from_str(&serialized).expect("deserialize"); assert_eq!(deserialized.poi_id, "med_bay"); assert_eq!(deserialized.category, PoiCategory::Service); } diff --git a/server/src/simulation/poi_discovery.rs b/server/src/simulation/poi_discovery.rs index 8056d7228..89c6ef780 100644 --- a/server/src/simulation/poi_discovery.rs +++ b/server/src/simulation/poi_discovery.rs @@ -152,11 +152,7 @@ mod tests { use crate::simulation::poi::{PoiCategory, PoiDiscoverySource}; use std::collections::BTreeSet; - fn make_poi( - id: &str, - position: TilePosition, - visibility: PoiVisibility, - ) -> PointOfInterest { + fn make_poi(id: &str, position: TilePosition, visibility: PoiVisibility) -> PointOfInterest { PointOfInterest { poi_id: id.to_string(), name: format!("Test {}", id), @@ -181,7 +177,11 @@ mod tests { #[test] fn los_poi_discovered_when_in_visible_positions() { let observer_pos = TilePosition::new(5, 5, 0); - let poi = make_poi("bay", TilePosition::new(10, 5, 0), PoiVisibility::LineOfSight); + let poi = make_poi( + "bay", + TilePosition::new(10, 5, 0), + PoiVisibility::LineOfSight, + ); let geometry = make_geometry(&[(10, 5)], 0); let kg = KnowledgeGraph::new(); @@ -191,7 +191,11 @@ mod tests { #[test] fn los_poi_not_discovered_when_not_visible() { let observer_pos = TilePosition::new(5, 5, 0); - let poi = make_poi("bay", TilePosition::new(10, 5, 0), PoiVisibility::LineOfSight); + let poi = make_poi( + "bay", + TilePosition::new(10, 5, 0), + PoiVisibility::LineOfSight, + ); let geometry = make_geometry(&[(8, 5)], 0); // (10,5) not in visible set let kg = KnowledgeGraph::new(); @@ -201,7 +205,11 @@ mod tests { #[test] fn los_poi_not_discovered_on_different_z() { let observer_pos = TilePosition::new(5, 5, 0); - let poi = make_poi("bay", TilePosition::new(10, 5, 1), PoiVisibility::LineOfSight); + let poi = make_poi( + "bay", + TilePosition::new(10, 5, 1), + PoiVisibility::LineOfSight, + ); let geometry = make_geometry(&[(10, 5)], 0); // observer on z=0, poi on z=1 let kg = KnowledgeGraph::new(); @@ -371,7 +379,10 @@ mod tests { let mut query = world.query_filtered::<&KnowledgeGraph, With>(); let kg = query.single(&world).expect("player should exist"); let fact_id = FactId("poi.docking_bay".to_string()); - assert!(kg.knows_fact(&fact_id), "Player should know poi.docking_bay"); + assert!( + kg.knows_fact(&fact_id), + "Player should know poi.docking_bay" + ); assert_eq!( kg.facts.get(&fact_id).unwrap().confidence, KnowledgeConfidence::KnowsOf diff --git a/server/src/simulation/pressure.rs b/server/src/simulation/pressure.rs index 2264aca21..8d6f1afa5 100644 --- a/server/src/simulation/pressure.rs +++ b/server/src/simulation/pressure.rs @@ -163,7 +163,7 @@ pub fn update_character_pressure( mut player_query: Query<(Entity, &mut CharacterPressure), With>, ) { // Only run on interval ticks - if time.tick % PRESSURE_UPDATE_INTERVAL != 0 { + if !time.tick.is_multiple_of(PRESSURE_UPDATE_INTERVAL) { return; } @@ -210,8 +210,8 @@ mod tests { use crate::npc::relationships::{RelationshipEdge, RelationshipGraph}; use crate::npc::{Npc, RelationshipKind}; use crate::simulation::movement::PlayerCharacter; - use crate::simulation::time::SimulationTime; use crate::simulation::tier::ActiveSim; + use crate::simulation::time::SimulationTime; use bevy_ecs::world::World; fn setup_world() -> World { @@ -251,7 +251,10 @@ mod tests { let mut q = world.query::<&CharacterPressure>(); let pressure = q.single(&world).unwrap(); - assert_eq!(pressure.exposure, 0, "should not update on non-interval tick"); + assert_eq!( + pressure.exposure, 0, + "should not update on non-interval tick" + ); } #[test] @@ -301,11 +304,7 @@ mod tests { } // 2 non-suspicious NPCs for _ in 0..2 { - world.spawn(( - Npc, - ActiveSim, - PlayerAwareness::default(), - )); + world.spawn((Npc, ActiveSim, PlayerAwareness::default())); } world.spawn((PlayerCharacter, CharacterPressure::default())); diff --git a/server/src/simulation/save_io.rs b/server/src/simulation/save_io.rs index 91cb948be..f101421b2 100644 --- a/server/src/simulation/save_io.rs +++ b/server/src/simulation/save_io.rs @@ -16,21 +16,21 @@ use thiserror::Error; use crate::bridge::types::SaveLoadResultWire; use crate::bridge::types::SnapshotBuffer; -use crate::simulation::triangle::{TemplateReferenceMap, TriangleState}; use crate::knowledge::graph::KnowledgeGraph; use crate::knowledge::registry::EntityRegistry; -use crate::npc::Npc; +use crate::knowledge::types::StableId; use crate::npc::relationships::RelationshipGraph; +use crate::npc::Npc; +use crate::simulation::interaction::DoorState; use crate::simulation::movement::PlayerCharacter; use crate::simulation::rng::SimRng; use crate::simulation::save_state::{ deserialize_npc_from_frozen, serialize_npc_to_frozen, SaveStateV1, SAVE_FORMAT_VERSION, }; -use crate::knowledge::types::StableId; -use crate::simulation::interaction::DoorState; use crate::simulation::tier::{ActiveSim, BackgroundSim}; use crate::simulation::time::SimulationTime; use crate::simulation::triangle::TriangleCrisisEventQueue; +use crate::simulation::triangle::{TemplateReferenceMap, TriangleState}; use crate::storyteller::{ ActivationState, ContaminationActive, ContaminationEventQueue, MovementHistoryBuffer, TriangleActivatedQueue, @@ -93,7 +93,9 @@ pub fn save_to_file(path: &Path, world: &mut World) -> Result<(), SaveLoadError> let player_knowledge = { let mut q = world.query_filtered::<&KnowledgeGraph, With>(); q.single(world).cloned().unwrap_or_else(|_| { - tracing::warn!("save_to_file: no PlayerCharacter with KnowledgeGraph found — saving empty graph"); + tracing::warn!( + "save_to_file: no PlayerCharacter with KnowledgeGraph found — saving empty graph" + ); KnowledgeGraph::new() }) }; @@ -151,7 +153,7 @@ pub fn save_to_file(path: &Path, world: &mut World) -> Result<(), SaveLoadError> modifications: vec![], // TODO: persist when modification system is implemented contamination_active: world .get_resource::() - .map_or(false, |c| c.0), + .is_some_and(|c| c.0), activated_count: world .get_resource::() .map_or(0, |a| a.activated_count), @@ -294,7 +296,11 @@ pub fn load_from_file(path: &Path, world: &mut World) -> Result<(), SaveLoadErro if !state.open_doors.is_empty() { let open_set: std::collections::BTreeSet<_> = state.open_doors.iter().copied().collect(); let door_entities: Vec<(Entity, StableId)> = { - let mut q = world.query::<(Entity, &crate::knowledge::registry::StableEntityId, &DoorState)>(); + let mut q = world.query::<( + Entity, + &crate::knowledge::registry::StableEntityId, + &DoorState, + )>(); q.iter(world) .filter(|(_, sid, _)| open_set.contains(&sid.0)) .map(|(e, sid, _)| (e, sid.0)) @@ -304,7 +310,9 @@ pub fn load_from_file(path: &Path, world: &mut World) -> Result<(), SaveLoadErro if let Some(mut door) = world.get_mut::(entity) { door.is_open = true; let tile = door.blocking_tile; - if let Some(mut wmap) = world.get_resource_mut::() { + if let Some(mut wmap) = + world.get_resource_mut::() + { wmap.set_walkable(&tile, true); } tracing::debug!(stable_id = sid.0, "load: restored open door state"); @@ -395,8 +403,8 @@ mod tests { use crate::knowledge::graph::KnowledgeGraph; use crate::knowledge::registry::{EntityRegistry, StableEntityId}; use crate::knowledge::types::StableId; - use crate::npc::Npc; use crate::npc::relationships::RelationshipGraph; + use crate::npc::Npc; use crate::simulation::movement::TilePosition; use crate::simulation::rng::SimRng; use crate::simulation::save_state::{SaveStateV1, SAVE_FORMAT_VERSION}; @@ -467,7 +475,11 @@ mod tests { let bytes = std::fs::read(&path).expect("read saved file"); let state = SaveStateV1::from_bytes(&bytes).unwrap(); let ids: Vec = state.npc_states.iter().map(|n| n.stable_id.0).collect(); - assert_eq!(ids, vec![10, 30, 50], "npc_states must be sorted by stable_id"); + assert_eq!( + ids, + vec![10, 30, 50], + "npc_states must be sorted by stable_id" + ); let _ = std::fs::remove_file(&path); } @@ -651,10 +663,7 @@ mod tests { let mut q = world.query_filtered::, With)>(); q.iter(&world).count() > 0 }; - assert!( - has_background, - "loaded NPC should be in BackgroundSim tier" - ); + assert!(has_background, "loaded NPC should be in BackgroundSim tier"); let _ = std::fs::remove_file(&path); } @@ -903,7 +912,10 @@ mod tests { // Reset activation state to defaults before load world.insert_resource(ActivationState::default()); assert_eq!(world.resource::().activated_count, 0); - assert!(world.resource::().last_activation_tick.is_none()); + assert!(world + .resource::() + .last_activation_tick + .is_none()); load_from_file(&path, &mut world).expect("load"); diff --git a/server/src/simulation/save_state.rs b/server/src/simulation/save_state.rs index f9aba2d8e..685c5c810 100644 --- a/server/src/simulation/save_state.rs +++ b/server/src/simulation/save_state.rs @@ -39,22 +39,22 @@ use bevy_ecs::entity::Entity; use bevy_ecs::world::World; use serde::{Deserialize, Serialize}; -use crate::simulation::triangle::{TemplateOwnership, TemplateReferenceMap, TriangleState}; use crate::knowledge::graph::KnowledgeGraph; use crate::knowledge::registry::StableEntityId; use crate::knowledge::types::StableId; -use crate::simulation::modification::Modification; +use crate::npc::awareness::PlayerAwareness; +use crate::npc::mood::MoodState; +use crate::npc::relationships::RelationshipGraph; +use crate::npc::vision::{NpcMemory, NpcVisionState}; use crate::npc::{ CombatCapability, Contentment, DailyRoutine, InformationInventory, JobPerformance, Npc, PersonalityTraits, Relationships, Secret, SecretSeverity, SkillSet, TellSystem, ToleranceThreshold, Want, WantKind, }; -use crate::npc::awareness::PlayerAwareness; -use crate::npc::mood::MoodState; -use crate::npc::relationships::RelationshipGraph; -use crate::npc::vision::{NpcMemory, NpcVisionState}; +use crate::simulation::modification::Modification; use crate::simulation::movement::TilePosition; use crate::simulation::time::TickRate; +use crate::simulation::triangle::{TemplateOwnership, TemplateReferenceMap, TriangleState}; use crate::storyteller::EngagementRecord; /// Current format version. Bump on any breaking schema change. @@ -181,7 +181,6 @@ pub struct NpcSaveState { // --- Full reconstruction fields (added #96, for tier eviction freeze) --- // All fields below use serde(default) for backward compatibility with saves // created before #96 shipped. - /// Axis 1: Want (primary drive, intensity, and description). #[serde(default)] pub want: Option, @@ -350,10 +349,7 @@ pub fn deserialize_npc_from_frozen(state: &NpcSaveState, world: &mut World) -> E level: state.contentment, }; - let kg = state - .knowledge_graph - .clone() - .unwrap_or_else(KnowledgeGraph::new); + let kg = state.knowledge_graph.clone().unwrap_or_default(); // Spawn the entity with all required components. Tier marker (ActiveSim / // BackgroundSim) is NOT added here — the caller assigns it after registration. @@ -579,17 +575,14 @@ mod tests { // Roundtrip the recovered state again — bytes must still match let bytes2 = recovered.to_bytes().expect("re-serialize"); - assert_eq!( - bytes, bytes2, - "KnowledgeGraph roundtrip must be idempotent" - ); + assert_eq!(bytes, bytes2, "KnowledgeGraph roundtrip must be idempotent"); } #[test] fn relationship_graph_roundtrips() { + use crate::knowledge::types::StableId; use crate::npc::relationships::RelationshipEdge; use crate::npc::RelationshipKind; - use crate::knowledge::types::StableId; let mut state = minimal_save_state(); let mut rg = RelationshipGraph::new(); @@ -608,7 +601,10 @@ mod tests { let bytes = state.to_bytes().expect("serialize"); let recovered = SaveStateV1::from_bytes(&bytes).expect("deserialize"); let bytes2 = recovered.to_bytes().expect("re-serialize"); - assert_eq!(bytes, bytes2, "RelationshipGraph roundtrip must be lossless"); + assert_eq!( + bytes, bytes2, + "RelationshipGraph roundtrip must be lossless" + ); } #[test] @@ -660,12 +656,12 @@ mod tests { // ----------------------------------------------------------------------- fn spawn_minimal_npc(world: &mut World, stable_id: StableId) -> Entity { + use crate::npc::awareness::PlayerAwareness; + use crate::npc::mood::MoodState; + use crate::npc::vision::{NpcMemory, NpcVisionState}; use crate::npc::{ Contentment, Relationships, Secret, SecretSeverity, ToleranceThreshold, Want, WantKind, }; - use crate::npc::mood::MoodState; - use crate::npc::vision::{NpcMemory, NpcVisionState}; - use crate::npc::awareness::PlayerAwareness; world .spawn(( @@ -763,13 +759,22 @@ mod tests { // Want let orig_want = world.get::(original).cloned().unwrap(); let rest_want = world.get::(restored).cloned().unwrap(); - assert_eq!(orig_want.primary, rest_want.primary, "want.primary must match"); - assert_eq!(orig_want.intensity, rest_want.intensity, "want.intensity must match"); + assert_eq!( + orig_want.primary, rest_want.primary, + "want.primary must match" + ); + assert_eq!( + orig_want.intensity, rest_want.intensity, + "want.intensity must match" + ); // Secret severity let orig_secret = world.get::(original).cloned().unwrap(); let rest_secret = world.get::(restored).cloned().unwrap(); - assert_eq!(orig_secret.severity, rest_secret.severity, "secret severity must match"); + assert_eq!( + orig_secret.severity, rest_secret.severity, + "secret severity must match" + ); } #[test] @@ -806,15 +811,24 @@ mod tests { assert!(world.get::(entity).is_some()); assert!(world.get::(entity).is_some()); assert!(world.get::(entity).is_some()); - assert!(world.get::(entity).is_some(), "Want defaults to Safety"); - assert!(world.get::(entity).is_some(), "Secret built from secret_severity"); + assert!( + world.get::(entity).is_some(), + "Want defaults to Safety" + ); + assert!( + world.get::(entity).is_some(), + "Secret built from secret_severity" + ); // Secret severity must be preserved from the legacy field let secret = world.get::(entity).unwrap(); assert_eq!(secret.severity, SecretSeverity::Moderate); // Optional axes absent in frozen state → not inserted or use defaults - assert!(world.get::(entity).is_none(), "routine absent when not frozen"); + assert!( + world.get::(entity).is_none(), + "routine absent when not frozen" + ); } #[test] @@ -845,7 +859,10 @@ mod tests { let bytes = save.to_bytes().expect("serialize"); let recovered = SaveStateV1::from_bytes(&bytes).expect("deserialize"); let bytes2 = recovered.to_bytes().expect("re-serialize"); - assert_eq!(bytes, bytes2, "frozen NPC state must roundtrip via MessagePack"); + assert_eq!( + bytes, bytes2, + "frozen NPC state must roundtrip via MessagePack" + ); } #[test] @@ -885,7 +902,7 @@ mod tests { #[test] fn populated_modifications_roundtrips_in_save_state() { // Acceptance (#567): non-empty modifications field survives save/load. - use crate::simulation::modification::{ModificationType, Modification}; + use crate::simulation::modification::{Modification, ModificationType}; let mut state = minimal_save_state(); state.modifications = vec![ @@ -908,9 +925,15 @@ mod tests { 2, "two modifications must survive roundtrip" ); - assert_eq!(recovered.modifications[0].position, TilePosition::new(10, 20, 0)); + assert_eq!( + recovered.modifications[0].position, + TilePosition::new(10, 20, 0) + ); assert_eq!(recovered.modifications[0].placed_at_tick, 500); - assert_eq!(recovered.modifications[1].position, TilePosition::new(3, 7, -1)); + assert_eq!( + recovered.modifications[1].position, + TilePosition::new(3, 7, -1) + ); assert_eq!(recovered.modifications[1].placed_at_tick, 1200); let bytes2 = recovered.to_bytes().expect("re-serialize"); diff --git a/server/src/simulation/sound.rs b/server/src/simulation/sound.rs index 3b3e638fa..98e7cf124 100644 --- a/server/src/simulation/sound.rs +++ b/server/src/simulation/sound.rs @@ -169,7 +169,7 @@ pub fn collect_sound_events( ) { queue.events.clear(); for (entity, mut emitter) in emitters.iter_mut() { - queue.events.extend(emitter.pending.drain(..)); + queue.events.append(&mut emitter.pending); commands.entity(entity).remove::(); } } @@ -221,9 +221,7 @@ mod tests { world.insert_resource(SoundEventQueue::default()); let pos = tile(5, 5); - let _entity = world - .spawn(SoundEventEmitter::new(close_event(&pos))) - .id(); + let _entity = world.spawn(SoundEventEmitter::new(close_event(&pos))).id(); let mut schedule = bevy_ecs::schedule::Schedule::default(); schedule.add_systems(collect_sound_events); @@ -337,8 +335,7 @@ mod tests { #[test] fn long_range_sound_audible_within_20_tiles() { let source = tile(0, 0); - let event = - SoundEvent::at(&source, SoundEventKind::Alert, 0.9, SoundRange::Long, None); + let event = SoundEvent::at(&source, SoundEventKind::Alert, 0.9, SoundRange::Long, None); // Manhattan distance 20 — exactly at boundary let listener = tile(10, 10); @@ -351,8 +348,7 @@ mod tests { #[test] fn long_range_sound_not_audible_beyond_20_tiles() { let source = tile(0, 0); - let event = - SoundEvent::at(&source, SoundEventKind::Alert, 0.9, SoundRange::Long, None); + let event = SoundEvent::at(&source, SoundEventKind::Alert, 0.9, SoundRange::Long, None); let listener = tile(11, 10); // manhattan 21 assert!( @@ -364,8 +360,13 @@ mod tests { #[test] fn long_range_audible_at_origin() { let source = tile(5, 5); - let event = - SoundEvent::at(&source, SoundEventKind::Machinery, 0.5, SoundRange::Long, None); + let event = SoundEvent::at( + &source, + SoundEventKind::Machinery, + 0.5, + SoundRange::Long, + None, + ); assert!(event.audible_at(&source), "audible at source position"); } diff --git a/server/src/simulation/spatial.rs b/server/src/simulation/spatial.rs index 678285604..5b604ce59 100644 --- a/server/src/simulation/spatial.rs +++ b/server/src/simulation/spatial.rs @@ -173,7 +173,10 @@ mod tests { index.update(e_near, TilePosition::new(5, 6, 0)); let in_range = index.entities_in_range(¢er, 2); - assert!(!in_range.contains(&e_at), "entity at center should be excluded"); + assert!( + !in_range.contains(&e_at), + "entity at center should be excluded" + ); assert!(in_range.contains(&e_near)); } @@ -331,7 +334,10 @@ mod tests { let in_range = index.entities_in_range(¢er, 1); assert!(in_range.contains(&e_dist1_x)); assert!(in_range.contains(&e_dist1_y)); - assert!(!in_range.contains(&e_dist2), "distance 2 must not appear in radius-1 result"); + assert!( + !in_range.contains(&e_dist2), + "distance 2 must not appear in radius-1 result" + ); } /// Diagonal: Manhattan distance covers all 4 orthogonal directions. @@ -348,9 +354,9 @@ mod tests { let center = TilePosition::new(10, 10, 0); index.update(north, TilePosition::new(10, 11, 0)); // distance 1 - index.update(south, TilePosition::new(10, 9, 0)); // distance 1 - index.update(east, TilePosition::new(11, 10, 0)); // distance 1 - index.update(west, TilePosition::new(9, 10, 0)); // distance 1 + index.update(south, TilePosition::new(10, 9, 0)); // distance 1 + index.update(east, TilePosition::new(11, 10, 0)); // distance 1 + index.update(west, TilePosition::new(9, 10, 0)); // distance 1 index.update(corner, TilePosition::new(11, 11, 0)); // distance 2 let in_range = index.entities_in_range(¢er, 2); @@ -425,7 +431,11 @@ mod tests { } let in_range = index.entities_in_range(¢er, 200); - assert_eq!(in_range.len(), 10, "radius 200 should include all 10 entities"); + assert_eq!( + in_range.len(), + 10, + "radius 200 should include all 10 entities" + ); } /// Remove one entity from a multi-entity index, others remain. @@ -500,7 +510,11 @@ mod tests { { let idx = world.resource::(); - assert_eq!(idx.entities_at(&start).len(), 1, "entity should be at start after first sync"); + assert_eq!( + idx.entities_at(&start).len(), + 1, + "entity should be at start after first sync" + ); } // Update position @@ -511,8 +525,15 @@ mod tests { { let idx = world.resource::(); - assert!(idx.entities_at(&start).is_empty(), "old position should be cleared"); - assert_eq!(idx.entities_at(&dest).len(), 1, "entity should be at new position"); + assert!( + idx.entities_at(&start).is_empty(), + "old position should be cleared" + ); + assert_eq!( + idx.entities_at(&dest).len(), + 1, + "entity should be at new position" + ); } } } diff --git a/server/src/simulation/tier.rs b/server/src/simulation/tier.rs index 9a312fbd8..4abbd4a88 100644 --- a/server/src/simulation/tier.rs +++ b/server/src/simulation/tier.rs @@ -4,8 +4,8 @@ // Scope tag system: NPCs with active scope tags stay pinned to ActiveSim (#98). // Timestamp-based eviction: LRU eviction when ActiveSim exceeds capacity (#97). -use std::collections::{BTreeSet, BinaryHeap}; use std::cmp::Reverse; +use std::collections::{BTreeSet, BinaryHeap}; use bevy_app::prelude::*; use bevy_ecs::prelude::*; @@ -14,8 +14,8 @@ use serde::{Deserialize, Serialize}; use crate::knowledge::graph::KnowledgeGraph; use crate::knowledge::registry::StableEntityId; use crate::knowledge::types::KnowledgeConfidence; -use crate::npc::{Npc, RelationshipKind}; use crate::npc::relationships::RelationshipGraph; +use crate::npc::{Npc, RelationshipKind}; use crate::simulation::movement::{PlayerCharacter, TilePosition}; // --- Tier radius constants (D-026) --- @@ -238,7 +238,10 @@ pub fn assign_scope_tags( .relationships_of(&player_id) .into_iter() .filter(|(_, edge)| { - matches!(edge.kind, RelationshipKind::Friend | RelationshipKind::Colleague) + matches!( + edge.kind, + RelationshipKind::Friend | RelationshipKind::Colleague + ) }) .map(|(target_id, _)| *target_id) .collect(); @@ -324,19 +327,17 @@ pub fn update_last_interaction_tick( // Update existing LastInteractionTick for visible NPCs. for (pos, mut last_tick) in &mut npcs_with_tick { - if pos.z == vis_geo.observer_z - && vis_geo.visible_positions.contains(&(pos.x, pos.y)) - { + if pos.z == vis_geo.observer_z && vis_geo.visible_positions.contains(&(pos.x, pos.y)) { last_tick.0 = current_tick; } } // Insert LastInteractionTick for NPCs that don't have it yet but are visible. for (entity, pos) in &npcs_without_tick { - if pos.z == vis_geo.observer_z - && vis_geo.visible_positions.contains(&(pos.x, pos.y)) - { - commands.entity(entity).insert(LastInteractionTick(current_tick)); + if pos.z == vis_geo.observer_z && vis_geo.visible_positions.contains(&(pos.x, pos.y)) { + commands + .entity(entity) + .insert(LastInteractionTick(current_tick)); } } } @@ -392,9 +393,15 @@ pub fn evict_excess_active( let dist = tile_distance(player_pos, &pos); if dist > BACKGROUND_RADIUS { - commands.entity(entity).remove::().insert(StateSaved); + commands + .entity(entity) + .remove::() + .insert(StateSaved); } else { - commands.entity(entity).remove::().insert(BackgroundSim); + commands + .entity(entity) + .remove::() + .insert(BackgroundSim); } evicted += 1; } @@ -517,8 +524,7 @@ mod tests { let background = world.spawn(BackgroundSim).id(); let _state_saved = world.spawn(StateSaved).id(); - let mut query = - world.query_filtered::>(); + let mut query = world.query_filtered::>(); let results: Vec = query.iter(&world).collect(); assert_eq!(results.len(), 1, "only one BackgroundSim entity expected"); @@ -532,8 +538,7 @@ mod tests { let _background = world.spawn(BackgroundSim).id(); let state_saved = world.spawn(StateSaved).id(); - let mut query = - world.query_filtered::>(); + let mut query = world.query_filtered::>(); let results: Vec = query.iter(&world).collect(); assert_eq!(results.len(), 1, "only one StateSaved entity expected"); @@ -610,11 +615,13 @@ mod tests { world.get::(entity).is_some(), "BackgroundSim added" ); - assert!(world.get::(entity).is_none(), "ActiveSim removed"); + assert!( + world.get::(entity).is_none(), + "ActiveSim removed" + ); // Must NOT appear in ActiveSim query after demotion - let mut active_query = - world.query_filtered::>(); + let mut active_query = world.query_filtered::>(); assert_eq!( active_query.iter(&world).count(), 0, @@ -686,7 +693,10 @@ mod tests { let npc = world.spawn((ActiveSim, make_pos(60, 0))).id(); run_tier_update(&mut world); assert!(world.get::(npc).is_none(), "ActiveSim removed"); - assert!(world.get::(npc).is_some(), "BackgroundSim added"); + assert!( + world.get::(npc).is_some(), + "BackgroundSim added" + ); } #[test] @@ -707,7 +717,10 @@ mod tests { world.spawn((PlayerCharacter, make_pos(0, 0))); let npc = world.spawn((BackgroundSim, make_pos(20, 0))).id(); run_tier_update(&mut world); - assert!(world.get::(npc).is_none(), "BackgroundSim removed"); + assert!( + world.get::(npc).is_none(), + "BackgroundSim removed" + ); assert!(world.get::(npc).is_some(), "ActiveSim added"); } @@ -718,7 +731,10 @@ mod tests { world.spawn((PlayerCharacter, make_pos(0, 0))); let npc = world.spawn((BackgroundSim, make_pos(200, 0))).id(); run_tier_update(&mut world); - assert!(world.get::(npc).is_none(), "BackgroundSim removed"); + assert!( + world.get::(npc).is_none(), + "BackgroundSim removed" + ); assert!(world.get::(npc).is_some(), "StateSaved added"); } @@ -741,7 +757,10 @@ mod tests { let npc = world.spawn((StateSaved, make_pos(80, 0))).id(); run_tier_update(&mut world); assert!(world.get::(npc).is_none(), "StateSaved removed"); - assert!(world.get::(npc).is_some(), "BackgroundSim added"); + assert!( + world.get::(npc).is_some(), + "BackgroundSim added" + ); } #[test] @@ -761,13 +780,14 @@ mod tests { let mut world = World::new(); world.spawn((PlayerCharacter, TilePosition::new(0, 0, 0))); // Spawn as ActiveSim at same x/y but different floor - let npc = world - .spawn((ActiveSim, TilePosition::new(0, 0, 1))) - .id(); + let npc = world.spawn((ActiveSim, TilePosition::new(0, 0, 1))).id(); run_tier_update(&mut world); // Should demote: u32::MAX > BACKGROUND_RADIUS → StateSaved assert!(world.get::(npc).is_none(), "ActiveSim removed"); - assert!(world.get::(npc).is_some(), "StateSaved due to z-distance"); + assert!( + world.get::(npc).is_some(), + "StateSaved due to z-distance" + ); } #[test] @@ -775,18 +795,28 @@ mod tests { // Distance = ACTIVE_RADIUS exactly → should stay Active (threshold is >) let mut world = World::new(); world.spawn((PlayerCharacter, make_pos(0, 0))); - let npc = world.spawn((ActiveSim, make_pos(ACTIVE_RADIUS as i32, 0))).id(); + let npc = world + .spawn((ActiveSim, make_pos(ACTIVE_RADIUS as i32, 0))) + .id(); run_tier_update(&mut world); - assert!(world.get::(npc).is_some(), "stays Active at exact boundary"); + assert!( + world.get::(npc).is_some(), + "stays Active at exact boundary" + ); } #[test] fn npc_one_tile_beyond_active_radius_demotes() { let mut world = World::new(); world.spawn((PlayerCharacter, make_pos(0, 0))); - let npc = world.spawn((ActiveSim, make_pos(ACTIVE_RADIUS as i32 + 1, 0))).id(); + let npc = world + .spawn((ActiveSim, make_pos(ACTIVE_RADIUS as i32 + 1, 0))) + .id(); run_tier_update(&mut world); - assert!(world.get::(npc).is_none(), "demoted to Background"); + assert!( + world.get::(npc).is_none(), + "demoted to Background" + ); assert!(world.get::(npc).is_some()); } @@ -934,7 +964,9 @@ mod tests { world.init_resource::(); // NPC exists but no PlayerCharacter - let npc = world.spawn((Npc, StableEntityId(crate::knowledge::types::StableId(1)))).id(); + let npc = world + .spawn((Npc, StableEntityId(crate::knowledge::types::StableId(1)))) + .id(); run_assign_scope_tags(&mut world); @@ -970,7 +1002,9 @@ mod tests { run_assign_scope_tags(&mut world); - let scope_tag = world.get::(npc).expect("ScopeTag should be assigned"); + let scope_tag = world + .get::(npc) + .expect("ScopeTag should be assigned"); assert!( scope_tag.contains(ScopeTagKind::KnownContact), "NPC known at KnowsOf level should get KnownContact tag" @@ -1017,7 +1051,9 @@ mod tests { run_assign_scope_tags(&mut world); - let scope_tag = world.get::(npc).expect("ScopeTag assigned for colleague"); + let scope_tag = world + .get::(npc) + .expect("ScopeTag assigned for colleague"); assert!( scope_tag.contains(ScopeTagKind::Colleague), "Friend relationship should grant Colleague scope tag" @@ -1067,7 +1103,10 @@ mod tests { assert_eq!(unpinned_results.len(), 1, "only one unpinned NPC"); assert_eq!(unpinned_results[0], unpinned); - assert!(!unpinned_results.contains(&pinned), "pinned NPC excluded from eviction query"); + assert!( + !unpinned_results.contains(&pinned), + "pinned NPC excluded from eviction query" + ); } // ----------------------------------------------------------------------- @@ -1090,9 +1129,15 @@ mod tests { world.spawn((PlayerCharacter, make_pos(0, 0))); // Spawn 3 active NPCs (under cap of 5) - let npc1 = world.spawn((Npc, ActiveSim, make_pos(5, 0), LastInteractionTick(10))).id(); - let npc2 = world.spawn((Npc, ActiveSim, make_pos(6, 0), LastInteractionTick(20))).id(); - let npc3 = world.spawn((Npc, ActiveSim, make_pos(7, 0), LastInteractionTick(30))).id(); + let npc1 = world + .spawn((Npc, ActiveSim, make_pos(5, 0), LastInteractionTick(10))) + .id(); + let npc2 = world + .spawn((Npc, ActiveSim, make_pos(6, 0), LastInteractionTick(20))) + .id(); + let npc3 = world + .spawn((Npc, ActiveSim, make_pos(7, 0), LastInteractionTick(30))) + .id(); run_evict_excess_active(&mut world); @@ -1112,16 +1157,28 @@ mod tests { world.spawn((PlayerCharacter, make_pos(0, 0))); // 3 NPCs, cap=2 → must evict 1 (the oldest: tick 10) - let oldest = world.spawn((Npc, ActiveSim, make_pos(5, 0), LastInteractionTick(10))).id(); - let mid = world.spawn((Npc, ActiveSim, make_pos(6, 0), LastInteractionTick(20))).id(); - let newest = world.spawn((Npc, ActiveSim, make_pos(7, 0), LastInteractionTick(30))).id(); + let oldest = world + .spawn((Npc, ActiveSim, make_pos(5, 0), LastInteractionTick(10))) + .id(); + let mid = world + .spawn((Npc, ActiveSim, make_pos(6, 0), LastInteractionTick(20))) + .id(); + let newest = world + .spawn((Npc, ActiveSim, make_pos(7, 0), LastInteractionTick(30))) + .id(); run_evict_excess_active(&mut world); assert!(world.get::(oldest).is_none(), "oldest evicted"); - assert!(world.get::(oldest).is_some(), "oldest → Background"); + assert!( + world.get::(oldest).is_some(), + "oldest → Background" + ); assert!(world.get::(mid).is_some(), "mid stays Active"); - assert!(world.get::(newest).is_some(), "newest stays Active"); + assert!( + world.get::(newest).is_some(), + "newest stays Active" + ); } #[test] @@ -1134,20 +1191,30 @@ mod tests { world.spawn((PlayerCharacter, make_pos(0, 0))); // 2 NPCs, cap=1. The oldest is ScopePinned → skip it, evict the other. - let pinned = world.spawn(( - Npc, ActiveSim, ScopePinned, - ScopeTag::with(ScopeTagKind::KnownContact), - make_pos(5, 0), LastInteractionTick(5), - )).id(); - let unpinned = world.spawn(( - Npc, ActiveSim, - make_pos(6, 0), LastInteractionTick(20), - )).id(); + let pinned = world + .spawn(( + Npc, + ActiveSim, + ScopePinned, + ScopeTag::with(ScopeTagKind::KnownContact), + make_pos(5, 0), + LastInteractionTick(5), + )) + .id(); + let unpinned = world + .spawn((Npc, ActiveSim, make_pos(6, 0), LastInteractionTick(20))) + .id(); run_evict_excess_active(&mut world); - assert!(world.get::(pinned).is_some(), "pinned NPC stays Active"); - assert!(world.get::(unpinned).is_none(), "unpinned NPC evicted"); + assert!( + world.get::(pinned).is_some(), + "pinned NPC stays Active" + ); + assert!( + world.get::(unpinned).is_none(), + "unpinned NPC evicted" + ); assert!(world.get::(unpinned).is_some()); } @@ -1161,21 +1228,25 @@ mod tests { world.spawn((PlayerCharacter, make_pos(0, 0))); // NPC at distance 200 (beyond BACKGROUND_RADIUS=120) → StateSaved - let far = world.spawn(( - Npc, ActiveSim, - make_pos(200, 0), LastInteractionTick(5), - )).id(); + let far = world + .spawn((Npc, ActiveSim, make_pos(200, 0), LastInteractionTick(5))) + .id(); // NPC at distance 5 (within ACTIVE_RADIUS) → stays - let near = world.spawn(( - Npc, ActiveSim, - make_pos(5, 0), LastInteractionTick(50), - )).id(); + let near = world + .spawn((Npc, ActiveSim, make_pos(5, 0), LastInteractionTick(50))) + .id(); run_evict_excess_active(&mut world); assert!(world.get::(far).is_none(), "far NPC evicted"); - assert!(world.get::(far).is_some(), "far NPC → StateSaved"); - assert!(world.get::(near).is_some(), "near NPC stays Active"); + assert!( + world.get::(far).is_some(), + "far NPC → StateSaved" + ); + assert!( + world.get::(near).is_some(), + "near NPC stays Active" + ); } #[test] @@ -1189,16 +1260,21 @@ mod tests { // NPC without LastInteractionTick defaults to tick 0 (most stale) let no_tick = world.spawn((Npc, ActiveSim, make_pos(5, 0))).id(); - let with_tick = world.spawn(( - Npc, ActiveSim, - make_pos(6, 0), LastInteractionTick(100), - )).id(); + let with_tick = world + .spawn((Npc, ActiveSim, make_pos(6, 0), LastInteractionTick(100))) + .id(); run_evict_excess_active(&mut world); - assert!(world.get::(no_tick).is_none(), "no-tick NPC evicted first"); + assert!( + world.get::(no_tick).is_none(), + "no-tick NPC evicted first" + ); assert!(world.get::(no_tick).is_some()); - assert!(world.get::(with_tick).is_some(), "with-tick NPC stays"); + assert!( + world.get::(with_tick).is_some(), + "with-tick NPC stays" + ); } #[test] @@ -1219,7 +1295,10 @@ mod tests { let pressure = world.resource::(); // active_count is set BEFORE eviction runs (it reads the pre-eviction count). // The actual count changes via deferred commands, which apply after the system. - assert_eq!(pressure.active_count, 3, "pressure tracks pre-eviction count"); + assert_eq!( + pressure.active_count, 3, + "pressure tracks pre-eviction count" + ); } #[test] diff --git a/server/src/simulation/triangle.rs b/server/src/simulation/triangle.rs index 622704f23..e4b1cc0cc 100644 --- a/server/src/simulation/triangle.rs +++ b/server/src/simulation/triangle.rs @@ -562,7 +562,8 @@ impl FullTemplateDef { // 2 — each role validates for role in &self.roles { - role.validate().map_err(|e| format!("role '{}': {}", role.role_id.0, e))?; + role.validate() + .map_err(|e| format!("role '{}': {}", role.role_id.0, e))?; } // 3 — space spec @@ -854,7 +855,7 @@ impl std::fmt::Display for ValidationError { /// divergence). pub fn validate_triangle_def(def: &TriangleDef) -> Result<(), ValidationError> { // 1. Conflict viability: at least one Want axis - if !def.interest_axes.iter().any(|a| *a == NpcAxis::Want) { + if !def.interest_axes.contains(&NpcAxis::Want) { return Err(ValidationError::ConflictViability { triangle_id: def.triangle_id, }); @@ -1104,7 +1105,7 @@ pub fn tick_triangle_escalation( thresholds: Query<&ToleranceThreshold>, ) { // Only process on game-minute boundaries (every 10 ticks, D-031) - if time.tick % TICKS_PER_GAME_MINUTE != 0 { + if !time.tick.is_multiple_of(TICKS_PER_GAME_MINUTE) { return; } @@ -1178,7 +1179,7 @@ pub fn apply_resolve_triangle( // Avoids O(N*M) full scan when multiple resolves fire in one tick. let id_to_entity: BTreeMap = triangles .iter() - .map(|(entity, state)| (state.triangle_id.clone(), entity)) + .map(|(entity, state)| (state.triangle_id, entity)) .collect(); for cmd in commands { @@ -1347,7 +1348,11 @@ mod tests { RoleId::new("supervisor"), ], conflict_type: ConflictType::LatentTension, - interest_axes: [NpcAxis::Contentment, NpcAxis::Contentment, NpcAxis::Tolerance], + interest_axes: [ + NpcAxis::Contentment, + NpcAxis::Contentment, + NpcAxis::Tolerance, + ], relationship_constraints: vec![], }; @@ -1438,7 +1443,11 @@ mod tests { let result = generate_intra_template_triangles(&mut world, tid, &defs, &mut rng); assert_eq!(result.triangles.len(), 2, "should generate 2 triangles"); - assert!(result.warnings.is_empty(), "no warnings expected: {:?}", result.warnings); + assert!( + result.warnings.is_empty(), + "no warnings expected: {:?}", + result.warnings + ); // Verify role assignments let t1 = &result.triangles[0]; @@ -1511,7 +1520,10 @@ mod tests { // Actually with only 2 NPCs, both are already assigned before we need a 3rd. // The triangle should be skipped with a warning. - assert!(!result.warnings.is_empty(), "should have warnings about missing roles"); + assert!( + !result.warnings.is_empty(), + "should have warnings about missing roles" + ); } #[test] @@ -1553,11 +1565,7 @@ mod tests { let defs = vec![ TriangleDef { triangle_id: TriangleId(300), - roles: [ - RoleId::new("a"), - RoleId::new("b"), - RoleId::new("c"), - ], + roles: [RoleId::new("a"), RoleId::new("b"), RoleId::new("c")], conflict_type: ConflictType::LoyaltyConflict, interest_axes: [NpcAxis::Want, NpcAxis::Secret, NpcAxis::Relationships], relationship_constraints: vec![RelationshipConstraint { @@ -1568,11 +1576,7 @@ mod tests { }, TriangleDef { triangle_id: TriangleId(301), - roles: [ - RoleId::new("a"), - RoleId::new("c"), - RoleId::new("d"), - ], + roles: [RoleId::new("a"), RoleId::new("c"), RoleId::new("d")], conflict_type: ConflictType::LatentTension, interest_axes: [NpcAxis::Want, NpcAxis::Contentment, NpcAxis::Tolerance], relationship_constraints: vec![RelationshipConstraint { @@ -1603,7 +1607,10 @@ mod tests { assert_eq!(result1.triangles.len(), result2.triangles.len()); for (t1, t2) in result1.triangles.iter().zip(result2.triangles.iter()) { assert_eq!(t1.tension, t2.tension, "tension must be deterministic"); - assert_eq!(t1.tension_rate, t2.tension_rate, "tension_rate must be deterministic"); + assert_eq!( + t1.tension_rate, t2.tension_rate, + "tension_rate must be deterministic" + ); assert_eq!(t1.role_assignments, t2.role_assignments); } } @@ -1681,7 +1688,10 @@ mod tests { schedule.run(&mut world); let state = world.get::(triangle).unwrap(); - assert_eq!(state.tension, 60, "Active triangle should gain +5 per game-minute × 2"); + assert_eq!( + state.tension, 60, + "Active triangle should gain +5 per game-minute × 2" + ); assert_eq!( state.phase, TrianglePhase::Active, @@ -1721,7 +1731,10 @@ mod tests { schedule.run(&mut world); let state = world.get::(triangle).unwrap(); - assert_eq!(state.tension, 255, "tension should saturate at u8::MAX (255)"); + assert_eq!( + state.tension, 255, + "tension should saturate at u8::MAX (255)" + ); } #[test] diff --git a/server/src/storyteller/mod.rs b/server/src/storyteller/mod.rs index ab3b54abc..eb0a3e6a2 100644 --- a/server/src/storyteller/mod.rs +++ b/server/src/storyteller/mod.rs @@ -55,13 +55,15 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; use rand::Rng; -use crate::simulation::triangle::{TriangleClassification, TriangleId, TrianglePhase, TriangleState}; use crate::knowledge::EntityRegistry; use crate::npc::Npc; use crate::simulation::movement::{PlayerCharacter, TilePosition}; use crate::simulation::rng::SimRng; use crate::simulation::tier::ActiveSim; use crate::simulation::time::{SimulationTime, TICKS_PER_GAME_MINUTE}; +use crate::simulation::triangle::{ + TriangleClassification, TriangleId, TrianglePhase, TriangleState, +}; // --------------------------------------------------------------------------- // Constants @@ -151,7 +153,7 @@ pub struct ContaminationActive(pub bool); /// Tracks how many triangles have been activated this session. For v0.1 this /// is a simple boolean gate — once `activated_count >= MAX_CONCURRENT_ACTIVATIONS`, /// the activation pass no-ops. Persisted in `SaveStateV1`. -#[derive(Resource, Debug, Clone)] +#[derive(Resource, Debug, Clone, Default)] pub struct ActivationState { /// Number of triangles activated this session. pub activated_count: u32, @@ -159,15 +161,6 @@ pub struct ActivationState { pub last_activation_tick: Option, } -impl Default for ActivationState { - fn default() -> Self { - Self { - activated_count: 0, - last_activation_tick: None, - } - } -} - impl ActivationState { /// Whether the activation pass should attempt to activate a new triangle. /// @@ -177,15 +170,9 @@ impl ActivationState { if self.activated_count >= MAX_CONCURRENT_ACTIVATIONS { return false; } - // v0.1: cooldown is 0, so no cooldown check needed. - // Future: check (current_tick - last_activation_tick) >= ACTIVATION_COOLDOWN_TICKS - if ACTIVATION_COOLDOWN_TICKS > 0 { - if let Some(last) = self.last_activation_tick { - if _current_tick.saturating_sub(last) < ACTIVATION_COOLDOWN_TICKS { - return false; - } - } - } + // v0.1: ACTIVATION_COOLDOWN_TICKS = 0, so no cooldown check needed. + // Future (v0.3+): when ACTIVATION_COOLDOWN_TICKS > 0, gate on + // (_current_tick - last_activation_tick) >= ACTIVATION_COOLDOWN_TICKS. true } @@ -375,8 +362,7 @@ impl Plugin for StorytellerPlugin { .add_systems(Update, tick_contamination_activation) .add_systems( Update, - append_player_history - .after(crate::simulation::movement::validate_movement), + append_player_history.after(crate::simulation::movement::validate_movement), ) .add_systems( Update, @@ -393,8 +379,7 @@ impl Plugin for StorytellerPlugin { ) .add_systems( Update, - expire_routine_deviations - .before(crate::npc::tell_state::derive_tell_state), + expire_routine_deviations.before(crate::npc::tell_state::derive_tell_state), ); tracing::debug!("StorytellerPlugin initialized"); @@ -458,11 +443,12 @@ pub fn tick_contamination_activation( /// Formula: capped conversations × weight + observation ticks × weight + monologue triggers × weight. /// Implements #162 step 3 scoring. fn compute_engagement_score(record: &EngagementRecord) -> f32 { - let conv_score = record.conversation_count.min(CONVERSATION_CAP) as f32 - * ENGAGEMENT_WEIGHT_CONVERSATION; + let conv_score = + record.conversation_count.min(CONVERSATION_CAP) as f32 * ENGAGEMENT_WEIGHT_CONVERSATION; // Use f64 intermediate to avoid precision loss beyond ~16.8M ticks (2^24), // where f32 can no longer distinguish adjacent integers. - let obs_score = (record.observation_time_ticks as f64 * ENGAGEMENT_WEIGHT_OBSERVATION as f64) as f32; + let obs_score = + (record.observation_time_ticks as f64 * ENGAGEMENT_WEIGHT_OBSERVATION as f64) as f32; let mono_score = record.monologue_trigger_count as f32 * ENGAGEMENT_WEIGHT_MONOLOGUE; conv_score + obs_score + mono_score } @@ -475,9 +461,8 @@ fn compute_engagement_score(record: &EngagementRecord) -> f32 { /// 1. Gate: contamination active + activation limit not reached + cadence check. /// 2. Co-presence query: find NPCs near any recorded player position (MovementHistoryBuffer). /// 3. Engagement scoring: score each co-present NPC via EngagementRecord. -/// 4+5. Triangle routing: find Simmering triangle containing highest-scoring co-present NPC. -/// v0.1: NPCs not assigned to any triangle are excluded (D-025 reference link routing deferred to v0.3+). -/// Holds (no activation) if no triangle-assigned NPC is co-present. +/// 4. Find Simmering triangle containing highest-scoring co-present NPC. +/// 5. Routing note: NPCs not in any triangle excluded (D-025, deferred to v0.3+). Holds if none. /// 6. Activation event: emit TriangleActivatedEvent, record in ActivationState. #[allow(clippy::too_many_arguments)] pub fn activation_pass( @@ -502,14 +487,15 @@ pub fn activation_pass( // Gate 3: poll cadence — only run every ACTIVATION_CADENCE_TICKS. // Guard: at tick 0 all engagement scores are 0.0 and selection is random // (e.g. contamination pre-set in a save file). Skip tick 0. - if time.tick == 0 || time.tick % ACTIVATION_CADENCE_TICKS != 0 { + if time.tick == 0 || !time.tick.is_multiple_of(ACTIVATION_CADENCE_TICKS) { return; } // Step 2: co-presence query let npc_positions: Vec<(Entity, TilePosition)> = npcs.iter().map(|(e, pos, _)| (e, *pos)).collect(); - let mut copresent = history.npcs_copresent_in_window(npc_positions.into_iter(), COPRESENCE_THRESHOLD); + let mut copresent = + history.npcs_copresent_in_window(npc_positions.into_iter(), COPRESENCE_THRESHOLD); // Deduplicate: the co-presence query can return the same entity more than once // if multiple player positions fall near the same NPC. Without dedup, the NPC // gets added to candidates twice, doubling its RNG weight. @@ -679,7 +665,9 @@ pub fn expire_routine_deviations( ) { for (entity, deviation) in deviations.iter() { if deviation.expires_at_tick > 0 && time.tick >= deviation.expires_at_tick { - commands.entity(entity).remove::(); + commands + .entity(entity) + .remove::(); } } } @@ -691,10 +679,10 @@ pub fn expire_routine_deviations( #[cfg(test)] mod tests { use super::*; + use crate::knowledge::types::StableId; use crate::simulation::triangle::{ RoleId, TemplateId, TriangleClassification, TriangleId, TrianglePhase, TriangleState, }; - use crate::knowledge::types::StableId; use std::collections::BTreeMap; fn make_triangle(classification: TriangleClassification) -> TriangleState { @@ -922,19 +910,19 @@ mod tests { let mut world = World::new(); let entity_old = world.spawn_empty().id(); let entity_new = world.spawn_empty().id(); - let copresent = buf.npcs_copresent_in_window( - [(entity_old, TilePosition::new(0, 0, 0))].into_iter(), - 0, - ); + let copresent = + buf.npcs_copresent_in_window([(entity_old, TilePosition::new(0, 0, 0))].into_iter(), 0); assert!( copresent.is_empty(), "NPC at evicted position x=0 should not be copresent" ); - let copresent_new = buf.npcs_copresent_in_window( - [(entity_new, TilePosition::new(9999, 0, 0))].into_iter(), - 0, + let copresent_new = buf + .npcs_copresent_in_window([(entity_new, TilePosition::new(9999, 0, 0))].into_iter(), 0); + assert_eq!( + copresent_new.len(), + 1, + "NPC at newest position should be copresent" ); - assert_eq!(copresent_new.len(), 1, "NPC at newest position should be copresent"); } #[test] @@ -958,7 +946,10 @@ mod tests { ); assert!(result.contains(&nearby), "nearby NPC should be copresent"); - assert!(!result.contains(&distant), "distant NPC should not be copresent"); + assert!( + !result.contains(&distant), + "distant NPC should not be copresent" + ); } #[test] @@ -970,10 +961,8 @@ mod tests { let npc = world.spawn_empty().id(); // Same x/y but different z - let result = buf.npcs_copresent_in_window( - [(npc, TilePosition::new(10, 10, 1))].into_iter(), - 0, - ); + let result = + buf.npcs_copresent_in_window([(npc, TilePosition::new(10, 10, 1))].into_iter(), 0); assert!( result.is_empty(), "NPC on different z-level must not be copresent" @@ -1085,7 +1074,9 @@ mod tests { world.resource_mut::().tick = tick; // Register NPC entity in EntityRegistry to get a StableId - let npc_entity = world.spawn((crate::npc::Npc, ActiveSim, TilePosition::new(5, 5, 0))).id(); + let npc_entity = world + .spawn((crate::npc::Npc, ActiveSim, TilePosition::new(5, 5, 0))) + .id(); let stable_id = world.resource_mut::().register(npc_entity); // Spawn a Simmering triangle with the NPC in a role @@ -1094,14 +1085,20 @@ mod tests { let tri_entity = world.spawn((tri, ActiveSim)).id(); // Put the player nearby in the history buffer - world.resource_mut::().append(TilePosition::new(5, 5, 0)); + world + .resource_mut::() + .append(TilePosition::new(5, 5, 0)); schedule.run(&mut world); let events = world.resource_mut::().drain(); assert_eq!(events.len(), 1); let tri_state = world.get::(tri_entity).unwrap(); - assert_eq!(tri_state.phase, TrianglePhase::Active, "triangle containing copresent NPC should be Active"); + assert_eq!( + tri_state.phase, + TrianglePhase::Active, + "triangle containing copresent NPC should be Active" + ); } #[test] @@ -1111,7 +1108,9 @@ mod tests { let (mut world, mut schedule) = setup_activation_world(); // Register an NPC and assign it to a triangle role so candidates are non-empty - let npc = world.spawn((crate::npc::Npc, ActiveSim, TilePosition::new(1, 1, 0))).id(); + let npc = world + .spawn((crate::npc::Npc, ActiveSim, TilePosition::new(1, 1, 0))) + .id(); let stable_id = world.resource_mut::().register(npc); let mut tri = make_triangle(TriangleClassification::ActiveFork); @@ -1119,15 +1118,22 @@ mod tests { world.spawn((tri, ActiveSim)); // Put the player nearby so the NPC is copresent - world.resource_mut::().append(TilePosition::new(1, 1, 0)); + world + .resource_mut::() + .append(TilePosition::new(1, 1, 0)); // First run — should activate world.resource_mut::().tick = ACTIVATION_CADENCE_TICKS; schedule.run(&mut world); let first_events = world.resource_mut::().drain(); - assert_eq!(first_events.len(), 1, "first pass must activate the triangle"); assert_eq!( - world.resource::().activated_count, 1, + first_events.len(), + 1, + "first pass must activate the triangle" + ); + assert_eq!( + world.resource::().activated_count, + 1, "ActivationState must record the first activation" ); @@ -1209,12 +1215,14 @@ mod tests { )); // Populate the activation queue - world.resource_mut::().push(TriangleActivatedEvent { - triangle_id, - tick: 100, - anchor_entity: npc_a, - anchor_score: 15.0, - }); + world + .resource_mut::() + .push(TriangleActivatedEvent { + triangle_id, + tick: 100, + anchor_entity: npc_a, + anchor_score: 15.0, + }); // Run system let mut schedule = Schedule::default(); @@ -1222,11 +1230,23 @@ mod tests { schedule.run(&mut world); // Both NPCs should have RoutineDeviation - assert!(world.get::(npc_a).is_some(), "NPC A should have RoutineDeviation"); - assert!(world.get::(npc_b).is_some(), "NPC B should have RoutineDeviation"); + assert!( + world.get::(npc_a).is_some(), + "NPC A should have RoutineDeviation" + ); + assert!( + world.get::(npc_b).is_some(), + "NPC B should have RoutineDeviation" + ); let dev = world.get::(npc_a).unwrap(); - assert_eq!(dev.trigger, crate::npc::DeviationTrigger::TriangleEscalation); - assert_eq!(dev.expires_at_tick, crate::npc::TELL_ESCALATION_DURATION_TICKS); + assert_eq!( + dev.trigger, + crate::npc::DeviationTrigger::TriangleEscalation + ); + assert_eq!( + dev.expires_at_tick, + crate::npc::TELL_ESCALATION_DURATION_TICKS + ); } #[test] @@ -1239,11 +1259,13 @@ mod tests { world.insert_resource(time); // Spawn entity with expired deviation - let entity = world.spawn(RoutineDeviation { - trigger: DeviationTrigger::TriangleEscalation, - tick: 100, - expires_at_tick: 400, // expired at tick 500 - }).id(); + let entity = world + .spawn(RoutineDeviation { + trigger: DeviationTrigger::TriangleEscalation, + tick: 100, + expires_at_tick: 400, // expired at tick 500 + }) + .id(); let mut schedule = Schedule::default(); schedule.add_systems(expire_routine_deviations); @@ -1265,11 +1287,13 @@ mod tests { world.insert_resource(time); // Deviation that expires at tick 500 — should survive at tick 200 - let entity = world.spawn(RoutineDeviation { - trigger: DeviationTrigger::TriangleEscalation, - tick: 100, - expires_at_tick: 500, - }).id(); + let entity = world + .spawn(RoutineDeviation { + trigger: DeviationTrigger::TriangleEscalation, + tick: 100, + expires_at_tick: 500, + }) + .id(); let mut schedule = Schedule::default(); schedule.add_systems(expire_routine_deviations); diff --git a/server/src/test_world/constants.rs b/server/src/test_world/constants.rs index 5a0766ac9..4c9be98c3 100644 --- a/server/src/test_world/constants.rs +++ b/server/src/test_world/constants.rs @@ -205,11 +205,7 @@ pub const CONFRONTATION_STAGE: GauntletRoom = GauntletRoom { /// distances from the observer: Close (2 tiles), Medium (6 tiles), Long (12 tiles). pub const SOUND_LAB: GauntletRoom = GauntletRoom { name: "sound_lab", - origin: TilePosition { - x: 0, - y: 104, - z: 0, - }, + origin: TilePosition { x: 0, y: 104, z: 0 }, size: (34, 20), spawn: TilePosition { x: 10, diff --git a/server/src/test_world/invariants.rs b/server/src/test_world/invariants.rs index a32532f9a..df8146fb4 100644 --- a/server/src/test_world/invariants.rs +++ b/server/src/test_world/invariants.rs @@ -5,7 +5,7 @@ //! - Perception (5): LOS symmetry, sound range boundaries //! - Population (8): NPC count limits, tier assignment correctness //! - Simulation (8): no entity at blocked tile, determinism, pathfinder -//! termination, interaction buffer cleared on sprint +//! termination, interaction buffer cleared on sprint //! //! The `run_invariants(world: &mut World)` function covers the 29 structural, //! perception, population, and simulation invariants checkable via pure @@ -24,6 +24,12 @@ use crate::knowledge::registry::{EntityRegistry, StableEntityId}; #[cfg(feature = "gauntlet")] use crate::knowledge::types::SoundRange; #[cfg(feature = "gauntlet")] +use crate::npc::interaction::InteractionMemory; +#[cfg(feature = "gauntlet")] +use crate::npc::mood::MoodState; +#[cfg(feature = "gauntlet")] +use crate::npc::routine::ActivityState; +#[cfg(feature = "gauntlet")] use crate::npc::{Npc, Want}; #[cfg(feature = "gauntlet")] use crate::perception::shadowcast::compute_fov; @@ -38,12 +44,6 @@ use crate::simulation::pathfinding::{ComputedPath, PathBlocked}; #[cfg(feature = "gauntlet")] use crate::simulation::sound::SoundEvent; #[cfg(feature = "gauntlet")] -use crate::npc::interaction::InteractionMemory; -#[cfg(feature = "gauntlet")] -use crate::npc::mood::MoodState; -#[cfg(feature = "gauntlet")] -use crate::npc::routine::ActivityState; -#[cfg(feature = "gauntlet")] use crate::simulation::tier::{ActiveSim, BackgroundSim, StateSaved}; #[cfg(feature = "gauntlet")] @@ -164,8 +164,10 @@ fn inv_s2_walkable_count_in_map_bounds(world: &mut World) { #[cfg(feature = "gauntlet")] fn inv_s3_player_spawn_walkable(world: &mut World) { let player_pos = { - let mut q = world.query_filtered::<&TilePosition, bevy_ecs::prelude::With>(); - *q.single(world).expect("S3: exactly one PlayerCharacter must exist") + let mut q = + world.query_filtered::<&TilePosition, bevy_ecs::prelude::With>(); + *q.single(world) + .expect("S3: exactly one PlayerCharacter must exist") }; let wm = world .get_resource::() @@ -259,11 +261,7 @@ fn inv_s7_room_interiors_have_walkable_tiles(world: &mut World) { assert!( found, "S7: room '{}' (interior x={}..{}, y={}..{}) must have at least one walkable tile", - room.name, - x_start, - x_end, - y_start, - y_end + room.name, x_start, x_end, y_start, y_end ); } } @@ -334,8 +332,8 @@ fn inv_p4_los_symmetry_at_hub(world: &mut World) { let (ax, ay, bx, by) = (50i32, 58i32, 54i32, 58i32); let range = 12; - let fov_a = compute_fov(&is_opaque, ax, ay, range, 0); - let fov_b = compute_fov(&is_opaque, bx, by, range, 0); + let fov_a = compute_fov(is_opaque, ax, ay, range, 0); + let fov_b = compute_fov(is_opaque, bx, by, range, 0); // A sees B → B must see A (symmetric shadowcasting guarantee, D-035) if fov_a.is_visible(bx, by) { @@ -375,7 +373,7 @@ fn inv_p5_los_range_bounded(world: &mut World) { let range = 6; let (ox, oy) = (50i32, 58i32); - let fov = compute_fov(&is_opaque, ox, oy, range, 0); + let fov = compute_fov(is_opaque, ox, oy, range, 0); // Tile at Chebyshev distance = range+2 must not be visible. let (out_x, out_y) = (ox + range + 2, oy); @@ -399,7 +397,10 @@ fn inv_p5_los_range_bounded(world: &mut World) { /// Pop1: Active-tier NPC count does not exceed the D-026 tick-budget limit (80). #[cfg(feature = "gauntlet")] fn inv_pop1_active_npc_count_within_limit(world: &mut World) { - let mut q = world.query_filtered::<(), (bevy_ecs::prelude::With, bevy_ecs::prelude::With)>(); + let mut q = world.query_filtered::<(), ( + bevy_ecs::prelude::With, + bevy_ecs::prelude::With, + )>(); let count = q.iter(world).count(); assert!( count <= 80, @@ -417,15 +418,16 @@ fn inv_pop2_all_npcs_have_tile_position(world: &mut World) { q.iter(world).count() }; let with_pos = { - let mut q = world.query_filtered::<(), (bevy_ecs::prelude::With, bevy_ecs::prelude::With)>(); + let mut q = world.query_filtered::<(), ( + bevy_ecs::prelude::With, + bevy_ecs::prelude::With, + )>(); q.iter(world).count() }; assert_eq!( - with_pos, - total, + with_pos, total, "Pop2: all {} NPCs must have TilePosition; only {} do", - total, - with_pos + total, with_pos ); } @@ -438,29 +440,32 @@ fn inv_pop3_all_npcs_have_exactly_one_tier(world: &mut World) { q.iter(world).count() }; let active = { - let mut q = world.query_filtered::<(), (bevy_ecs::prelude::With, bevy_ecs::prelude::With)>(); + let mut q = world.query_filtered::<(), ( + bevy_ecs::prelude::With, + bevy_ecs::prelude::With, + )>(); q.iter(world).count() }; let bg = { - let mut q = world.query_filtered::<(), (bevy_ecs::prelude::With, bevy_ecs::prelude::With)>(); + let mut q = world.query_filtered::<(), ( + bevy_ecs::prelude::With, + bevy_ecs::prelude::With, + )>(); q.iter(world).count() }; let ss = { - let mut q = world.query_filtered::<(), (bevy_ecs::prelude::With, bevy_ecs::prelude::With)>(); + let mut q = world.query_filtered::<(), ( + bevy_ecs::prelude::With, + bevy_ecs::prelude::With, + )>(); q.iter(world).count() }; let tier_sum = active + bg + ss; assert_eq!( - tier_sum, - total, + tier_sum, total, "Pop3: each of {} NPCs must have exactly one tier marker; \ found {} Active + {} Background + {} StateSaved = {} (should equal {})", - total, - active, - bg, - ss, - tier_sum, - total + total, active, bg, ss, tier_sum, total ); } @@ -468,9 +473,9 @@ fn inv_pop3_all_npcs_have_exactly_one_tier(world: &mut World) { /// Same-layer collision on spawn is a world-setup error. #[cfg(feature = "gauntlet")] fn inv_pop4_no_same_layer_collision(world: &mut World) { - use std::collections::BTreeSet; use crate::simulation::movement::TilePresence; use bevy_ecs::prelude::Entity; + use std::collections::BTreeSet; let mut q = world.query::<(Entity, &TilePosition, Option<&TilePresence>)>(); let occupied: Vec<(TilePosition, TilePresence, Entity)> = q @@ -516,15 +521,14 @@ fn inv_pop6_all_npcs_have_want(world: &mut World) { q.iter(world).count() }; let with_want = { - let mut q = world.query_filtered::<(), (bevy_ecs::prelude::With, bevy_ecs::prelude::With)>(); + let mut q = world + .query_filtered::<(), (bevy_ecs::prelude::With, bevy_ecs::prelude::With)>(); q.iter(world).count() }; assert_eq!( - with_want, - total, + with_want, total, "Pop6: all {} NPCs must have a Want component; only {} do (D-024 axis 1)", - total, - with_want + total, with_want ); } @@ -545,8 +549,7 @@ fn inv_pop7_crowd_plaza_npc_count(world: &mut World) { }) .count(); assert_eq!( - count, - 15, + count, 15, "Pop7: Crowd Plaza must contain exactly 15 NPCs; found {}", count ); @@ -556,15 +559,12 @@ fn inv_pop7_crowd_plaza_npc_count(world: &mut World) { /// Duplicate StableIds corrupt knowledge-graph references and snapshot output. #[cfg(feature = "gauntlet")] fn inv_pop8_stable_ids_unique(world: &mut World) { - use std::collections::BTreeSet; use crate::knowledge::types::StableId; use bevy_ecs::prelude::Entity; + use std::collections::BTreeSet; let mut q = world.query::<(Entity, &StableEntityId)>(); - let ids: Vec<(StableId, Entity)> = q - .iter(world) - .map(|(e, sid)| (sid.0, e)) - .collect(); + let ids: Vec<(StableId, Entity)> = q.iter(world).map(|(e, sid)| (sid.0, e)).collect(); let mut seen: BTreeSet = BTreeSet::new(); for (id, entity) in ids { @@ -593,7 +593,8 @@ fn inv_sim1_no_entity_at_blocked_tile(world: &mut World) { // Only check movable entities: Npc + PlayerCharacter. Fixtures/signs/reset plates // may legitimately sit in wall tiles (interactable from range, not traversed). let positions: Vec<(Entity, TilePosition)> = { - let mut q = world.query_filtered::<(Entity, &TilePosition), Or<(With, With)>>(); + let mut q = world + .query_filtered::<(Entity, &TilePosition), Or<(With, With)>>(); q.iter(world).map(|(e, p)| (e, *p)).collect() }; let wm = world @@ -619,9 +620,7 @@ fn inv_sim2_computed_path_steps_walkable(world: &mut World) { let paths: Vec<(Entity, Vec)> = { let mut q = world.query::<(Entity, &ComputedPath)>(); - q.iter(world) - .map(|(e, p)| (e, p.steps.clone())) - .collect() + q.iter(world).map(|(e, p)| (e, p.steps.clone())).collect() }; let wm = world .get_resource::() @@ -667,8 +666,7 @@ fn inv_sim4_player_entity_present(world: &mut World) { let mut q = world.query_filtered::<(), With>(); let count = q.iter(world).count(); assert_eq!( - count, - 1, + count, 1, "Sim4: exactly one PlayerCharacter must exist; found {}", count ); @@ -704,7 +702,7 @@ fn inv_sim6_registry_has_entities(world: &mut World) { .get_resource::() .expect("Sim6: EntityRegistry resource must exist"); assert!( - registry.len() > 0, + !registry.is_empty(), "Sim6: EntityRegistry must contain at least one entity (the player)" ); } @@ -750,19 +748,24 @@ fn inv_sim8_player_has_interaction_buffer(world: &mut World) { #[cfg(feature = "gauntlet")] fn inv_s14_active_npcs_have_mood_state(world: &mut World) { let active = { - let mut q = world.query_filtered::<(), (bevy_ecs::prelude::With, bevy_ecs::prelude::With)>(); + let mut q = world.query_filtered::<(), ( + bevy_ecs::prelude::With, + bevy_ecs::prelude::With, + )>(); q.iter(world).count() }; let with_mood = { - let mut q = world.query_filtered::<(), (bevy_ecs::prelude::With, bevy_ecs::prelude::With, bevy_ecs::prelude::With)>(); + let mut q = world.query_filtered::<(), ( + bevy_ecs::prelude::With, + bevy_ecs::prelude::With, + bevy_ecs::prelude::With, + )>(); q.iter(world).count() }; assert_eq!( - with_mood, - active, + with_mood, active, "S14-1: all {} Active NPCs must have MoodState; only {} do", - active, - with_mood + active, with_mood ); } @@ -775,15 +778,16 @@ fn inv_s14_npcs_have_interaction_memory(world: &mut World) { q.iter(world).count() }; let with_mem = { - let mut q = world.query_filtered::<(), (bevy_ecs::prelude::With, bevy_ecs::prelude::With)>(); + let mut q = world.query_filtered::<(), ( + bevy_ecs::prelude::With, + bevy_ecs::prelude::With, + )>(); q.iter(world).count() }; assert_eq!( - with_mem, - total, + with_mem, total, "S14-2: all {} NPCs must have InteractionMemory; only {} do", - total, - with_mem + total, with_mem ); } @@ -792,8 +796,8 @@ fn inv_s14_npcs_have_interaction_memory(world: &mut World) { /// "needs to move somewhere". Both at once is contradictory. #[cfg(feature = "gauntlet")] fn inv_s14_no_activity_state_with_path_request(world: &mut World) { - use bevy_ecs::prelude::{Entity, With}; use crate::simulation::pathfinding::PathRequest; + use bevy_ecs::prelude::{Entity, With}; let with_activity: Vec = { let mut q = world.query_filtered::>(); @@ -816,7 +820,8 @@ fn inv_pop3b_no_double_tagged_tiers(world: &mut World) { use bevy_ecs::prelude::{Entity, With}; let active_and_bg: Vec = { - let mut q = world.query_filtered::, With, With)>(); + let mut q = + world.query_filtered::, With, With)>(); q.iter(world).collect() }; assert!( @@ -826,7 +831,8 @@ fn inv_pop3b_no_double_tagged_tiers(world: &mut World) { ); let active_and_ss: Vec = { - let mut q = world.query_filtered::, With, With)>(); + let mut q = + world.query_filtered::, With, With)>(); q.iter(world).collect() }; assert!( @@ -836,7 +842,8 @@ fn inv_pop3b_no_double_tagged_tiers(world: &mut World) { ); let bg_and_ss: Vec = { - let mut q = world.query_filtered::, With, With)>(); + let mut q = + world.query_filtered::, With, With)>(); q.iter(world).collect() }; assert!( @@ -857,14 +864,16 @@ mod system_tests { use bevy_ecs::prelude::*; use bevy_ecs::schedule::Schedule; + use crate::bridge::types::MovementStance; + use crate::knowledge::registry::EntityRegistry; use crate::npc::{Contentment, Npc, ToleranceThreshold, Want, WantKind}; - use crate::simulation::interaction::{compute_nearby_interactions, Interactable, NearbyInteractionBuffer}; + use crate::simulation::interaction::{ + compute_nearby_interactions, Interactable, NearbyInteractionBuffer, + }; use crate::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap}; use crate::simulation::pathfinding::{compute_paths, ComputedPath, PathBlocked, PathRequest}; use crate::simulation::stance::Stance; use crate::simulation::tier::ActiveSim; - use crate::bridge::types::MovementStance; - use crate::knowledge::registry::EntityRegistry; // ----------------------------------------------------------------------- // Invariant 30-31: Pathfinder terminates on adjacent tile @@ -1042,8 +1051,8 @@ mod system_tests { schedule.add_systems(compute_nearby_interactions); schedule.run(&mut world); - let mut q = world - .query_filtered::<&mut NearbyInteractionBuffer, With>(); + let mut q = + world.query_filtered::<&mut NearbyInteractionBuffer, With>(); let mut buf = q.single_mut(&mut world).expect("player must exist"); let interactions = buf.take(); assert!( @@ -1074,8 +1083,8 @@ mod system_tests { schedule.add_systems(compute_nearby_interactions); schedule.run(&mut world); - let mut q = world - .query_filtered::<&mut NearbyInteractionBuffer, With>(); + let mut q = + world.query_filtered::<&mut NearbyInteractionBuffer, With>(); let mut buf = q.single_mut(&mut world).expect("player must exist"); let interactions = buf.take(); assert!( diff --git a/server/src/test_world/mod.rs b/server/src/test_world/mod.rs index 18957e556..9916aae9a 100644 --- a/server/src/test_world/mod.rs +++ b/server/src/test_world/mod.rs @@ -2,7 +2,7 @@ //! //! NOT production content. This module provides deterministic room layouts //! with precise entity placement for golden file testing, regression testing, -//! and manual QA sessions. Loaded instead of content/ when the server runs +//! and manual QA sessions. Loaded instead of server/content/ when the server runs //! the Gauntlet map. //! //! Module structure: @@ -111,11 +111,11 @@ pub fn setup_gauntlet(app: &mut App, archetype: crate::bridge::types::CharacterA carve_room_interior(&mut walkability, 0, 2, 28, 20); // Sprint Gauntlet carve_room_interior(&mut walkability, 74, 26, 24, 16); // Eavesdrop Alcove carve_room_interior(&mut walkability, 84, 2, 32, 24); // Confrontation Stage - // Sprint 13 rooms + // Sprint 13 rooms carve_room_interior(&mut walkability, 0, 104, 34, 20); // Sound Lab carve_room_interior(&mut walkability, 0, 24, 24, 14); // Decay Observatory carve_room_interior(&mut walkability, 64, 78, 16, 24); // Shift Change - // Sprint 22 rooms + // Sprint 22 rooms carve_room_interior(&mut walkability, 64, 102, 16, 22); // Zone Gate // Carve corridors between hub and rooms @@ -192,8 +192,10 @@ pub fn setup_gauntlet(app: &mut App, archetype: crate::bridge::types::CharacterA // Spawn at Hub center: absolute (50, 58) let profile = MovementProfile::smuggler(); let player_pos = TilePosition::new(50, 58, 0); - let mut monologue_state = MonologueState::default(); - monologue_state.character = archetype.as_monologue_key().to_string(); + let monologue_state = MonologueState { + character: archetype.as_monologue_key().to_string(), + ..Default::default() + }; let player = app .world_mut() .spawn(( @@ -536,8 +538,7 @@ pub fn setup_gauntlet(app: &mut App, archetype: crate::bridge::types::CharacterA } // Decay Observatory entities (StableId 69): NPC only, no floor items - for id in - constants::DECAY_OBSERVATORY_STABLE_IDS.0..=constants::DECAY_OBSERVATORY_STABLE_IDS.1 + for id in constants::DECAY_OBSERVATORY_STABLE_IDS.0..=constants::DECAY_OBSERVATORY_STABLE_IDS.1 { if let Some(entity) = registry.to_entity(&StableId(id)) { if let Some(pos) = app.world().get::(entity) { @@ -616,7 +617,7 @@ pub fn setup_gauntlet(app: &mut App, archetype: crate::bridge::types::CharacterA use crate::simulation::conversation::NpcColorIndex; use crate::simulation::dialogue::{CurrentMood, DialogueProfile}; - // (location, role) pairs matching content/campaigns/.../dialogue/ YAML pools. + // (location, role) pairs matching server/content/campaigns/.../dialogue/ YAML pools. // Cycling through these gives NPC variety across rooms. const DIALOGUE_ROLES: &[(&str, &str)] = &[ ("the-terminal", "dock-worker"), @@ -698,7 +699,10 @@ mod tests { app.add_plugins(crate::knowledge::KnowledgePlugin); app.add_plugins(crate::npc::NpcPlugin); - setup_gauntlet(&mut app, crate::bridge::types::CharacterArchetype::default()); + setup_gauntlet( + &mut app, + crate::bridge::types::CharacterArchetype::default(), + ); // Run all 29 world-query invariants against the fully-initialized gauntlet world. invariants::run_invariants(app.world_mut()); @@ -718,7 +722,10 @@ mod tests { app.add_plugins(crate::knowledge::KnowledgePlugin); app.add_plugins(crate::npc::NpcPlugin); - setup_gauntlet(&mut app, crate::bridge::types::CharacterArchetype::default()); + setup_gauntlet( + &mut app, + crate::bridge::types::CharacterArchetype::default(), + ); let wm = app.world().resource::(); // Hub center at (50, 58) must be walkable @@ -732,7 +739,10 @@ mod tests { app.add_plugins(crate::knowledge::KnowledgePlugin); app.add_plugins(crate::npc::NpcPlugin); - setup_gauntlet(&mut app, crate::bridge::types::CharacterArchetype::default()); + setup_gauntlet( + &mut app, + crate::bridge::types::CharacterArchetype::default(), + ); let wm = app.world().resource::(); // North wall segment at absolute (90, 54) should be blocked @@ -748,7 +758,10 @@ mod tests { app.add_plugins(crate::knowledge::KnowledgePlugin); app.add_plugins(crate::npc::NpcPlugin); - setup_gauntlet(&mut app, crate::bridge::types::CharacterArchetype::default()); + setup_gauntlet( + &mut app, + crate::bridge::types::CharacterArchetype::default(), + ); let wm = app.world().resource::(); // corridor-E center should be walkable @@ -762,7 +775,10 @@ mod tests { app.add_plugins(crate::knowledge::KnowledgePlugin); app.add_plugins(crate::npc::NpcPlugin); - setup_gauntlet(&mut app, crate::bridge::types::CharacterArchetype::default()); + setup_gauntlet( + &mut app, + crate::bridge::types::CharacterArchetype::default(), + ); let registry = app.world().resource::(); @@ -904,8 +920,8 @@ mod tests { } // Decay Observatory at 69 - for id in constants::DECAY_OBSERVATORY_STABLE_IDS.0 - ..=constants::DECAY_OBSERVATORY_STABLE_IDS.1 + for id in + constants::DECAY_OBSERVATORY_STABLE_IDS.0..=constants::DECAY_OBSERVATORY_STABLE_IDS.1 { assert!( registry.to_entity(&StableId(id)).is_some(), diff --git a/server/src/test_world/rooms/shift_change.rs b/server/src/test_world/rooms/shift_change.rs index a71ca99a1..2b4ca1739 100644 --- a/server/src/test_world/rooms/shift_change.rs +++ b/server/src/test_world/rooms/shift_change.rs @@ -17,7 +17,9 @@ use bevy_app::prelude::*; use crate::knowledge::registry::{EntityRegistry, StableEntityId}; -use crate::npc::{Contentment, DailyRoutine, Npc, RoutineEntry, ToleranceThreshold, Want, WantKind}; +use crate::npc::{ + Contentment, DailyRoutine, Npc, RoutineEntry, ToleranceThreshold, Want, WantKind, +}; use crate::simulation::interaction::Interactable; use crate::simulation::movement::TilePosition; use crate::simulation::path_follow::MovementSpeed; @@ -177,8 +179,7 @@ mod tests { .id(); // Advance time to Afternoon boundary (Morning→Afternoon). - world.resource_mut::().tick = - MINUTES_PER_PHASE * TICKS_PER_GAME_MINUTE; + world.resource_mut::().tick = MINUTES_PER_PHASE * TICKS_PER_GAME_MINUTE; let mut schedule = bevy_ecs::schedule::Schedule::default(); schedule.add_systems(check_phase_transition); diff --git a/server/src/test_world/rooms/sound_lab.rs b/server/src/test_world/rooms/sound_lab.rs index 9d2f6e056..ec6632a39 100644 --- a/server/src/test_world/rooms/sound_lab.rs +++ b/server/src/test_world/rooms/sound_lab.rs @@ -30,22 +30,38 @@ const ORIGIN_X: i32 = 0; const ORIGIN_Y: i32 = 104; /// Observer position for Sound Lab tests (absolute). -pub const OBSERVER_POS: TilePosition = TilePosition { x: 10, y: 114, z: 0 }; +pub const OBSERVER_POS: TilePosition = TilePosition { + x: 10, + y: 114, + z: 0, +}; /// Close-range emitter position — 2 tiles east of observer (dist=2, Close ≤3). -pub const CLOSE_EMITTER_POS: TilePosition = TilePosition { x: 12, y: 114, z: 0 }; +pub const CLOSE_EMITTER_POS: TilePosition = TilePosition { + x: 12, + y: 114, + z: 0, +}; /// Medium-range emitter position — 6 tiles east of observer (dist=6, Medium ≤8, outside Close). -pub const MEDIUM_EMITTER_POS: TilePosition = TilePosition { x: 16, y: 114, z: 0 }; +pub const MEDIUM_EMITTER_POS: TilePosition = TilePosition { + x: 16, + y: 114, + z: 0, +}; /// Long-range emitter position — 12 tiles east of observer (dist=12, Long ≤20, outside Medium). -pub const LONG_EMITTER_POS: TilePosition = TilePosition { x: 22, y: 114, z: 0 }; +pub const LONG_EMITTER_POS: TilePosition = TilePosition { + x: 22, + y: 114, + z: 0, +}; /// NPC definitions: (relative_x, relative_y, want_kind, intensity). const NPCS: &[(i32, i32, WantKind, u8)] = &[ - (12, 10, WantKind::Safety, 3), // npc_sound_close — StableId 66 + (12, 10, WantKind::Safety, 3), // npc_sound_close — StableId 66 (16, 10, WantKind::Knowledge, 4), // npc_sound_medium — StableId 67 - (22, 10, WantKind::Freedom, 5), // npc_sound_long — StableId 68 + (22, 10, WantKind::Freedom, 5), // npc_sound_long — StableId 68 ]; /// Spawn Sound Lab entities in canonical order (StableId 66-68). diff --git a/server/src/test_world/rooms/zone_gate.rs b/server/src/test_world/rooms/zone_gate.rs index 718848b14..2d86fa75d 100644 --- a/server/src/test_world/rooms/zone_gate.rs +++ b/server/src/test_world/rooms/zone_gate.rs @@ -25,16 +25,32 @@ use crate::simulation::interaction::{DoorState, Interactable, ObjectType}; use crate::simulation::movement::TilePosition; /// Observer start position — Terminal side of the zone boundary. -pub const OBSERVER_POS: TilePosition = TilePosition { x: 70, y: 112, z: 0 }; +pub const OBSERVER_POS: TilePosition = TilePosition { + x: 70, + y: 112, + z: 0, +}; /// Door position — first tile of the Corridor zone (zone boundary). -pub const DOOR_POS: TilePosition = TilePosition { x: 72, y: 112, z: 0 }; +pub const DOOR_POS: TilePosition = TilePosition { + x: 72, + y: 112, + z: 0, +}; /// Last walkable tile of the Terminal zone before the boundary. -pub const TERMINAL_SIDE_POS: TilePosition = TilePosition { x: 71, y: 112, z: 0 }; +pub const TERMINAL_SIDE_POS: TilePosition = TilePosition { + x: 71, + y: 112, + z: 0, +}; /// First walkable tile of the Corridor zone after the boundary. -pub const CORRIDOR_SIDE_POS: TilePosition = TilePosition { x: 73, y: 112, z: 0 }; +pub const CORRIDOR_SIDE_POS: TilePosition = TilePosition { + x: 73, + y: 112, + z: 0, +}; /// Spawn Zone Gate entities in canonical order (StableId 75). pub fn spawn_entities(app: &mut App, registry: &mut EntityRegistry) { @@ -149,7 +165,11 @@ mod tests { ZONE_GATE_TERMINAL_ZONE_ID ); assert_eq!( - zone_map.zone_at(TERMINAL_SIDE_POS.x, TERMINAL_SIDE_POS.y, TERMINAL_SIDE_POS.z), + zone_map.zone_at( + TERMINAL_SIDE_POS.x, + TERMINAL_SIDE_POS.y, + TERMINAL_SIDE_POS.z + ), Some(ZONE_GATE_TERMINAL_ZONE_ID), "Tile (71,112) must be in Terminal zone" ); @@ -267,7 +287,9 @@ mod tests { world.resource_mut::().drain(); // Move within Terminal zone (same zone, different tile). - world.entity_mut(player).insert(TilePosition::new(68, 112, 0)); + world + .entity_mut(player) + .insert(TilePosition::new(68, 112, 0)); sched.run(&mut world); let events = world.resource_mut::().drain(); diff --git a/server/src/voice/cache.rs b/server/src/voice/cache.rs index 2623572f4..0ede7dfa0 100644 --- a/server/src/voice/cache.rs +++ b/server/src/voice/cache.rs @@ -103,10 +103,7 @@ impl VoiceCacheStore { pub fn zone_cache(&mut self, zone_id: u32) -> &mut ZoneVoiceCache { if !self.zones.contains_key(&zone_id) { let cache = self.load_zone(zone_id).unwrap_or_else(|| { - ZoneVoiceCache::new( - self.model_version.clone(), - self.injector_version.clone(), - ) + ZoneVoiceCache::new(self.model_version.clone(), self.injector_version.clone()) }); self.zones.insert(zone_id, cache); } @@ -139,8 +136,7 @@ impl VoiceCacheStore { fs::create_dir_all(&dir)?; let path = dir.join(format!("{}.msgpack", zone_id)); - let data = rmp_serde::to_vec(cache) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + let data = rmp_serde::to_vec(cache).map_err(io::Error::other)?; fs::write(path, data) } @@ -163,10 +159,7 @@ impl VoiceCacheStore { if cache.model_version != self.model_version || cache.injector_version != self.injector_version { - tracing::info!( - zone_id, - "voice cache version mismatch — invalidating" - ); + tracing::info!(zone_id, "voice cache version mismatch — invalidating"); return None; } @@ -286,7 +279,10 @@ mod tests { let data = rmp_serde::to_vec(&cache).unwrap(); let restored: ZoneVoiceCache = rmp_serde::from_slice(&data).unwrap(); - assert_eq!(restored.lookup(&key), Some("Hands are steady. Eyes aren't.")); + assert_eq!( + restored.lookup(&key), + Some("Hands are steady. Eyes aren't.") + ); assert_eq!(restored.model_version, "v1"); } diff --git a/server/src/voice/hardware.rs b/server/src/voice/hardware.rs index b7ab1d47f..01b545e4a 100644 --- a/server/src/voice/hardware.rs +++ b/server/src/voice/hardware.rs @@ -115,14 +115,20 @@ pub fn probe_hardware(mode: Option) -> HardwareProbe { tracing::warn!("GPU mode set but VRAM probe failed — falling back to system RAM"); let mut sys = System::new_all(); sys.refresh_memory(); - (sys.total_memory() / (1024 * 1024), sys.available_memory() / (1024 * 1024)) + ( + sys.total_memory() / (1024 * 1024), + sys.available_memory() / (1024 * 1024), + ) } } _ => { // CPU mode or Apple Silicon (unified memory = system RAM) let mut sys = System::new_all(); sys.refresh_memory(); - (sys.total_memory() / (1024 * 1024), sys.available_memory() / (1024 * 1024)) + ( + sys.total_memory() / (1024 * 1024), + sys.available_memory() / (1024 * 1024), + ) } }; @@ -228,7 +234,10 @@ pub enum ScalingDecision { /// Returns (total_mb, free_mb) for the first GPU, or None. fn probe_nvidia_vram() -> Option<(u64, u64)> { let output = Command::new("nvidia-smi") - .args(["--query-gpu=memory.total,memory.free", "--format=csv,noheader,nounits"]) + .args([ + "--query-gpu=memory.total,memory.free", + "--format=csv,noheader,nounits", + ]) .output() .ok()?; @@ -480,9 +489,9 @@ mod tests { fn evaluate_scaling_hold_when_calm() { let decision = evaluate_scaling( ResourceMode::Cpu, - 2, // running - 5, // queue depth (low) - 1, // active + 2, // running + 5, // queue depth (low) + 1, // active None, ); assert!(matches!(decision, ScalingDecision::Hold)); diff --git a/server/src/voice/lookup.rs b/server/src/voice/lookup.rs index b7a387905..ccac3241c 100644 --- a/server/src/voice/lookup.rs +++ b/server/src/voice/lookup.rs @@ -96,8 +96,15 @@ mod tests { cache.lock().unwrap().store(100, key, "Voiced line.".into()); let result = voiced_behavior( - &cache, 100, "van-maanens-star", 42, ContentType::Behavior, 0, None, - "Base line.", false, + &cache, + 100, + "van-maanens-star", + 42, + ContentType::Behavior, + 0, + None, + "Base line.", + false, ); assert_eq!(result, "Voiced line."); } @@ -106,8 +113,15 @@ mod tests { fn cache_miss_returns_base_text() { let cache = test_cache(); let result = voiced_behavior( - &cache, 100, "van-maanens-star", 42, ContentType::Behavior, 0, None, - "Base line.", false, + &cache, + 100, + "van-maanens-star", + 42, + ContentType::Behavior, + 0, + None, + "Base line.", + false, ); assert_eq!(result, "Base line."); } @@ -156,8 +170,15 @@ mod tests { cache.lock().unwrap().store(100, key, "Voiced tell.".into()); let result = voiced_behavior( - &cache, 100, "van-maanens-star", 42, ContentType::Behavior, 0, - Some(TellCategory::Angry), "Base tell.", true, + &cache, + 100, + "van-maanens-star", + 42, + ContentType::Behavior, + 0, + Some(TellCategory::Angry), + "Base tell.", + true, ); assert_eq!(result, "Base tell."); } diff --git a/server/src/voice/prompt_builder.rs b/server/src/voice/prompt_builder.rs index 7c601f5aa..118122995 100644 --- a/server/src/voice/prompt_builder.rs +++ b/server/src/voice/prompt_builder.rs @@ -294,7 +294,7 @@ pub fn build_prompt( if rng.random::() < injection.frequency { parts.push(String::new()); // Imperative framing — no "when" conditional, just "include this" - parts.push(format!("INJECT: Include the phrase from this example in your output.")); + parts.push("INJECT: Include the phrase from this example in your output.".to_string()); if let Some(ref example) = injection.example { parts.push(format!("INPUT: {}", example.input)); parts.push(format!("OUTPUT: {}", example.output)); @@ -367,7 +367,7 @@ pub fn build_prompt( mod tests { use super::*; use crate::npc::blueprint::{ - CultureProfile, CulturalValues, NamingConventions, OccasionalInjection, SpeechPatterns, + CulturalValues, CultureProfile, NamingConventions, OccasionalInjection, SpeechPatterns, VoiceExample, }; use crate::npc::PersonalityTrait; @@ -401,12 +401,10 @@ mod tests { 2. You're working-class and pragmatic." .into(), ), - voice_examples: vec![ - VoiceExample { - input: "declines to answer a question".into(), - output: "Look, that's not mine to say.".into(), - }, - ], + voice_examples: vec![VoiceExample { + input: "declines to answer a question".into(), + output: "Look, that's not mine to say.".into(), + }], occasional_injections: vec![OccasionalInjection { kind: "oath".into(), clause: "When something surprises you, use an oath like \"void take it.\"".into(), @@ -467,7 +465,9 @@ mod tests { fn prompt_contains_persona_when_present() { let culture = van_maanens_star_culture(); let result = build_prompt(&culture, "Hello.", ContentType::Dialogue, None, 42); - assert!(result.prompt.contains("PERSONA: You are a Van Maanen's Star station worker")); + assert!(result + .prompt + .contains("PERSONA: You are a Van Maanen's Star station worker")); } #[test] diff --git a/server/src/voice/queue.rs b/server/src/voice/queue.rs index c7e095177..abb15c474 100644 --- a/server/src/voice/queue.rs +++ b/server/src/voice/queue.rs @@ -87,6 +87,12 @@ pub struct VoiceQueue { paused: Arc, } +impl Default for VoiceQueue { + fn default() -> Self { + Self::new() + } +} + impl VoiceQueue { pub fn new() -> Self { let (sender, receiver) = crossbeam_channel::bounded(QUEUE_CAPACITY); diff --git a/server/src/voice/worker.rs b/server/src/voice/worker.rs index dbdf590c4..cea784218 100644 --- a/server/src/voice/worker.rs +++ b/server/src/voice/worker.rs @@ -165,9 +165,13 @@ impl VoicePipe { .spawn() .map_err(|e| format!("failed to spawn sr-voice: {}", e))?; - let stdin = child.stdin.take() + let stdin = child + .stdin + .take() .ok_or_else(|| "failed to capture sr-voice stdin".to_string())?; - let stdout = child.stdout.take() + let stdout = child + .stdout + .take() .ok_or_else(|| "failed to capture sr-voice stdout".to_string())?; Ok(Self { diff --git a/server/tests/bridge_ipc.rs b/server/tests/bridge_ipc.rs index d99ade20e..ebd2c55de 100644 --- a/server/tests/bridge_ipc.rs +++ b/server/tests/bridge_ipc.rs @@ -66,7 +66,7 @@ fn snapshot_roundtrip_over_unix_socket() { conversation_events: vec![], conversation_ended: vec![], follow_state: None, - character_pressure: None, + character_pressure: None, rng_seed: None, poi_list: vec![], examine_result: None, @@ -74,7 +74,7 @@ fn snapshot_roundtrip_over_unix_socket() { save_result: None, triangle_crisis_events: vec![], state_hash: None, - debug_response: None, + debug_response: None, sim_errors: vec![], current_ticker: None, settings_response: None, diff --git a/server/tests/bridge_tcp.rs b/server/tests/bridge_tcp.rs index 7ae456996..32062441a 100644 --- a/server/tests/bridge_tcp.rs +++ b/server/tests/bridge_tcp.rs @@ -52,7 +52,7 @@ fn snapshot_roundtrip_over_tcp() { conversation_events: vec![], conversation_ended: vec![], follow_state: None, - character_pressure: None, + character_pressure: None, rng_seed: None, poi_list: vec![], examine_result: None, @@ -60,7 +60,7 @@ fn snapshot_roundtrip_over_tcp() { save_result: None, triangle_crisis_events: vec![], state_hash: None, - debug_response: None, + debug_response: None, sim_errors: vec![], current_ticker: None, settings_response: None, diff --git a/server/tests/contamination.rs b/server/tests/contamination.rs index 022dc2a07..47e84a0eb 100644 --- a/server/tests/contamination.rs +++ b/server/tests/contamination.rs @@ -7,12 +7,12 @@ use bevy_ecs::prelude::*; use bevy_ecs::schedule::Schedule; use std::collections::BTreeMap; -use settled_reach_server::simulation::triangle::{ - RoleId, TemplateId, TriangleClassification, TriangleId, TrianglePhase, TriangleState, -}; use settled_reach_server::knowledge::types::StableId; use settled_reach_server::simulation::tier::ActiveSim; use settled_reach_server::simulation::time::SimulationTime; +use settled_reach_server::simulation::triangle::{ + RoleId, TemplateId, TriangleClassification, TriangleId, TrianglePhase, TriangleState, +}; use settled_reach_server::storyteller::{ tick_contamination_activation, ContaminationActive, ContaminationEventQueue, CONTAMINATION_DELAY_TICKS, CONTAMINATION_PRESSURE_DELTA, @@ -112,11 +112,15 @@ fn contamination_activates_after_delay() { assert!( world.resource::().0, "ContaminationActive must be true after CONTAMINATION_DELAY_TICKS ({}) ticks", - CONTAMINATION_DELAY_TICKS + CONTAMINATION_DELAY_TICKS ); // Assert all ActiveFork triangles have tension > 0 - for (label, entity) in [("hub-power", fork1), ("bar-tensions", fork2), ("informant-question", fork3)] { + for (label, entity) in [ + ("hub-power", fork1), + ("bar-tensions", fork2), + ("informant-question", fork3), + ] { let state = world .get::(entity) .unwrap_or_else(|| panic!("{} triangle entity should exist", label)); @@ -129,13 +133,15 @@ fn contamination_activates_after_delay() { assert_eq!( state.tension, CONTAMINATION_PRESSURE_DELTA, "ActiveFork triangle '{}' tension should be exactly {} (contamination delta)", - label, - CONTAMINATION_PRESSURE_DELTA + label, CONTAMINATION_PRESSURE_DELTA ); } // Assert PassiveTension triangles were NOT pressured - for (label, entity) in [("worried-knowledge", passive1), ("worried-partner", passive2)] { + for (label, entity) in [ + ("worried-knowledge", passive1), + ("worried-partner", passive2), + ] { let state = world .get::(entity) .unwrap_or_else(|| panic!("{} triangle entity should exist", label)); diff --git a/server/tests/determinism.rs b/server/tests/determinism.rs index fb4da670b..78320afb3 100644 --- a/server/tests/determinism.rs +++ b/server/tests/determinism.rs @@ -352,13 +352,13 @@ fn gauntlet_deterministic_replay() { /// path (select_dialogue_line) which consumes SimRng. #[test] fn different_seed_produces_different_replay() { - use settled_reach_server::simulation::line_pool::{ - AccessTier, IndexedDialogueLine, IndexedDialoguePool, LinePoolIndex, Mood, Situation, - TrustTier, LinePoolIndexResource, - }; use settled_reach_server::simulation::dialogue::{ CurrentMood, DialogueCooldownTracker, DialogueProfile, DialogueResponseBuffer, }; + use settled_reach_server::simulation::line_pool::{ + AccessTier, IndexedDialogueLine, IndexedDialoguePool, LinePoolIndex, LinePoolIndexResource, + Mood, Situation, TrustTier, + }; /// Build a deterministic app with dialogue-capable NPCs. fn build_app_with_dialogue(seed: u64) -> App { diff --git a/server/tests/environmental_interaction.rs b/server/tests/environmental_interaction.rs index 7febad2c2..0ab87a443 100644 --- a/server/tests/environmental_interaction.rs +++ b/server/tests/environmental_interaction.rs @@ -7,24 +7,22 @@ //! - DoorState persists in SaveStateV1.open_doors. use bevy_ecs::{prelude::*, schedule::Schedule, world::World}; +use settled_reach_server::knowledge::KnowledgeGraph; +use settled_reach_server::simulation::triangle::TemplateReferenceMap; use settled_reach_server::{ knowledge::{registry::EntityRegistry, registry::StableEntityId, types::StableId}, npc::relationships::RelationshipGraph, simulation::{ - examine::{ - process_examine_interaction, ExamineRequest, ExamineResultBuffer, ExamineText, - }, + examine::{process_examine_interaction, ExamineRequest, ExamineResultBuffer, ExamineText}, interaction::{ - process_door_interaction, process_terminal_interaction, DoorInteractRequest, - DoorState, Interactable, ObjectType, TerminalInteractRequest, TerminalInteractedQueue, + process_door_interaction, process_terminal_interaction, DoorInteractRequest, DoorState, + Interactable, ObjectType, TerminalInteractRequest, TerminalInteractedQueue, }, movement::{PlayerCharacter, TilePosition, WalkabilityMap}, save_state::{SaveStateV1, SAVE_FORMAT_VERSION}, time::{SimulationTime, TickRate}, }, }; -use settled_reach_server::knowledge::KnowledgeGraph; -use settled_reach_server::simulation::triangle::TemplateReferenceMap; // --------------------------------------------------------------------------- // Helpers @@ -39,10 +37,7 @@ fn make_world_with_walkability(width: i32, height: i32) -> World { fn spawn_player(world: &mut World, x: i32, y: i32) -> Entity { world - .spawn(( - PlayerCharacter, - TilePosition::new(x, y, 0), - )) + .spawn((PlayerCharacter, TilePosition::new(x, y, 0))) .id() } @@ -85,7 +80,9 @@ fn door_toggle_flips_walkability_both_ways() { schedule.run(&mut world); assert!( - world.resource::().can_move_to(&TilePosition::new(10, 5, 0)), + world + .resource::() + .can_move_to(&TilePosition::new(10, 5, 0)), "after Open: blocking tile must become walkable" ); assert!( @@ -106,7 +103,9 @@ fn door_toggle_flips_walkability_both_ways() { schedule.run(&mut world); assert!( - !world.resource::().can_move_to(&TilePosition::new(10, 5, 0)), + !world + .resource::() + .can_move_to(&TilePosition::new(10, 5, 0)), "after Close: blocking tile must be impassable again" ); assert!( @@ -136,7 +135,9 @@ fn door_starts_open_toggle_closes_it() { // Tile starts walkable (default map is all walkable) assert!( - world.resource::().can_move_to(&TilePosition::new(10, 5, 0)), + world + .resource::() + .can_move_to(&TilePosition::new(10, 5, 0)), "precondition: tile is walkable when door starts open" ); @@ -150,7 +151,9 @@ fn door_starts_open_toggle_closes_it() { schedule.run(&mut world); assert!( - !world.resource::().can_move_to(&TilePosition::new(10, 5, 0)), + !world + .resource::() + .can_move_to(&TilePosition::new(10, 5, 0)), "toggling an open door must block the tile" ); assert!( @@ -166,9 +169,9 @@ fn door_interact_without_door_state_does_not_panic() { let player = spawn_player(&mut world, 5, 5); let not_a_door = world.spawn(TilePosition::new(5, 6, 0)).id(); - world - .entity_mut(player) - .insert(DoorInteractRequest { door_entity: not_a_door }); + world.entity_mut(player).insert(DoorInteractRequest { + door_entity: not_a_door, + }); let mut schedule = Schedule::default(); schedule.add_systems(process_door_interaction); @@ -208,7 +211,9 @@ fn examine_readable_returns_authored_text() { TilePosition::new(5, 6, 0), Interactable, ObjectType::Readable, - ExamineText("A logistics manifest. Freight records dating back three cycles.".to_string()), + ExamineText( + "A logistics manifest. Freight records dating back three cycles.".to_string(), + ), )) .id(); @@ -220,10 +225,7 @@ fn examine_readable_returns_authored_text() { schedule.add_systems(process_examine_interaction); schedule.run(&mut world); - let event = world - .get_mut::(player) - .unwrap() - .take(); + let event = world.get_mut::(player).unwrap().take(); assert!( event.is_some(), "ExamineResultBuffer must contain a result after examining a Readable" @@ -274,16 +276,16 @@ fn examine_readable_without_examine_text_returns_fallback() { schedule.add_systems(process_examine_interaction); schedule.run(&mut world); - let event = world - .get_mut::(player) - .unwrap() - .take(); + let event = world.get_mut::(player).unwrap().take(); assert!( event.is_some(), "ExamineResultBuffer must contain a result even without ExamineText" ); let text = event.unwrap().text; - assert!(!text.is_empty(), "fallback text must be non-empty, got: '{text}'"); + assert!( + !text.is_empty(), + "fallback text must be non-empty, got: '{text}'" + ); } /// Examine out of range returns no result. @@ -322,11 +324,11 @@ fn examine_readable_out_of_range_returns_no_result() { schedule.add_systems(process_examine_interaction); schedule.run(&mut world); - let event = world - .get_mut::(player) - .unwrap() - .take(); - assert!(event.is_none(), "examining an out-of-range Readable must not produce a result"); + let event = world.get_mut::(player).unwrap().take(); + assert!( + event.is_none(), + "examining an out-of-range Readable must not produce a result" + ); } // --------------------------------------------------------------------------- @@ -355,19 +357,27 @@ fn terminal_use_emits_terminal_interacted_event() { // Register terminal in EntityRegistry so stable ID resolves let terminal_sid = StableId(42); - world.entity_mut(terminal).insert(StableEntityId(terminal_sid)); - world.resource_mut::().register_existing(terminal, terminal_sid); - world - .entity_mut(player) - .insert(TerminalInteractRequest { terminal_entity: terminal }); + .entity_mut(terminal) + .insert(StableEntityId(terminal_sid)); + world + .resource_mut::() + .register_existing(terminal, terminal_sid); + + world.entity_mut(player).insert(TerminalInteractRequest { + terminal_entity: terminal, + }); let mut schedule = Schedule::default(); schedule.add_systems(process_terminal_interaction); schedule.run(&mut world); let queue = world.resource::(); - assert_eq!(queue.events.len(), 1, "one TerminalInteracted event must be emitted"); + assert_eq!( + queue.events.len(), + 1, + "one TerminalInteracted event must be emitted" + ); assert_eq!( queue.events[0].terminal_id, terminal_sid, "terminal_id must match the interacted terminal" @@ -388,9 +398,9 @@ fn terminal_interact_request_consumed_after_processing() { let terminal = world.spawn(TilePosition::new(5, 6, 0)).id(); - world - .entity_mut(player) - .insert(TerminalInteractRequest { terminal_entity: terminal }); + world.entity_mut(player).insert(TerminalInteractRequest { + terminal_entity: terminal, + }); let mut schedule = Schedule::default(); schedule.add_systems(process_terminal_interaction); diff --git a/server/tests/error_handling.rs b/server/tests/error_handling.rs index b37b0eaab..96ebbecc2 100644 --- a/server/tests/error_handling.rs +++ b/server/tests/error_handling.rs @@ -106,10 +106,7 @@ fn malformed_input_produces_sim_error_and_server_continues() { .expect("not EOF"); let snap1: ObserverSnapshot = rmp_serde::from_slice(&snap1_bytes).expect("deserialize snapshot 1"); - assert!( - snap1.sim_errors.is_empty(), - "no errors expected on tick 1" - ); + assert!(snap1.sim_errors.is_empty(), "no errors expected on tick 1"); // --- Tick 2: send malformed input (properly framed but garbage payload) --- let garbage_payload: Vec = vec![0xFF, 0xFE, 0xFD, 0xFC, 0xAB, 0xCD, 0xEF]; @@ -132,8 +129,12 @@ fn malformed_input_produces_sim_error_and_server_continues() { "error kind must be ProtocolError" ); assert!( - snap2.sim_errors[0].message.contains("Malformed input frame") - || snap2.sim_errors[0].message.contains("Deserialization error"), + snap2.sim_errors[0] + .message + .contains("Malformed input frame") + || snap2.sim_errors[0] + .message + .contains("Deserialization error"), "error message should describe the deserialization failure, got: {}", snap2.sim_errors[0].message, ); @@ -161,7 +162,9 @@ fn malformed_input_produces_sim_error_and_server_continues() { // Clean up drop(reader); drop(writer); - server_handle.join().expect("server thread should not panic"); + server_handle + .join() + .expect("server thread should not panic"); } /// State hash is populated in every snapshot and is deterministic for same state. @@ -206,8 +209,7 @@ fn state_hash_populated_in_snapshot() { let snap_bytes = read_framed(&mut reader) .expect("read snapshot") .expect("not EOF"); - let snap: ObserverSnapshot = - rmp_serde::from_slice(&snap_bytes).expect("deserialize snapshot"); + let snap: ObserverSnapshot = rmp_serde::from_slice(&snap_bytes).expect("deserialize snapshot"); assert!( snap.state_hash.is_some(), @@ -221,7 +223,9 @@ fn state_hash_populated_in_snapshot() { drop(reader); drop(writer); - server_handle.join().expect("server thread should not panic"); + server_handle + .join() + .expect("server thread should not panic"); } /// SimError roundtrips through MessagePack serialization. @@ -296,13 +300,11 @@ fn snapshot_with_sim_errors_roundtrips() { triangle_crisis_events: vec![], state_hash: Some(0xDEADBEEF), debug_response: None, - sim_errors: vec![ - SimError { - kind: SimErrorKind::ProtocolError, - message: "bad frame".into(), - tick: 10, - }, - ], + sim_errors: vec![SimError { + kind: SimErrorKind::ProtocolError, + message: "bad frame".into(), + tick: 10, + }], current_ticker: None, settings_response: None, }; diff --git a/server/tests/fuzzy_map.rs b/server/tests/fuzzy_map.rs index 492f0bf54..061088274 100644 --- a/server/tests/fuzzy_map.rs +++ b/server/tests/fuzzy_map.rs @@ -79,9 +79,9 @@ fn generate_map(seed: u64) -> ProceduralMap { let y: i32 = rng.random_range(2_i32..(MAP_H - h - 2)); // Reject if overlaps an existing room (1-tile padding). - let overlaps = rooms.iter().any(|r| { - x < r.x + r.w + 1 && x + w + 1 > r.x && y < r.y + r.h + 1 && y + h + 1 > r.y - }); + let overlaps = rooms + .iter() + .any(|r| x < r.x + r.w + 1 && x + w + 1 > r.x && y < r.y + r.h + 1 && y + h + 1 > r.y); if !overlaps { for ry in y..(y + h) { @@ -153,11 +153,8 @@ fn generate_map(seed: u64) -> ProceduralMap { } // Player start: centre of first room (always walkable by construction). - let player_start = TilePosition::new( - rooms[0].x + rooms[0].w / 2, - rooms[0].y + rooms[0].h / 2, - 0, - ); + let player_start = + TilePosition::new(rooms[0].x + rooms[0].w / 2, rooms[0].y + rooms[0].h / 2, 0); let mut entities = vec![player_start]; entities.extend(door_placements.iter().map(|d| d.pos)); diff --git a/server/tests/game_loop.rs b/server/tests/game_loop.rs index c2effff41..8e10fcf81 100644 --- a/server/tests/game_loop.rs +++ b/server/tests/game_loop.rs @@ -7,10 +7,10 @@ use settled_reach_server::bridge::tcp::TcpBridge; use settled_reach_server::bridge::types::*; use settled_reach_server::bridge::{BridgePlugin, BridgeResource}; use settled_reach_server::knowledge::{KnowledgeGraph, KnowledgePlugin}; +use settled_reach_server::npc::relationships::TrustEventQueue; use settled_reach_server::simulation::interaction::NearbyInteractionBuffer; use settled_reach_server::simulation::monologue::{MonologueBuffer, MonologueState}; use settled_reach_server::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap}; -use settled_reach_server::npc::relationships::TrustEventQueue; use settled_reach_server::simulation::SimulationPlugin; use std::io::{BufReader, BufWriter}; use std::net::{TcpListener, TcpStream}; diff --git a/server/tests/gen_gauntlet_fixtures.rs b/server/tests/gen_gauntlet_fixtures.rs index 0ca75110c..beb59243f 100644 --- a/server/tests/gen_gauntlet_fixtures.rs +++ b/server/tests/gen_gauntlet_fixtures.rs @@ -45,7 +45,10 @@ fn build_gauntlet(seed: u64) -> App { app.add_plugins(NpcPlugin); app.insert_resource(SimRng::new(seed)); - test_world::setup_gauntlet(&mut app, settled_reach_server::bridge::types::CharacterArchetype::default()); + test_world::setup_gauntlet( + &mut app, + settled_reach_server::bridge::types::CharacterArchetype::default(), + ); app } @@ -60,9 +63,7 @@ fn teleport_player(app: &mut App, pos: TilePosition, facing: Facing) { .query_filtered::>(); query.single(app.world()).expect("player entity must exist") }; - app.world_mut() - .entity_mut(player) - .insert((pos, facing)); + app.world_mut().entity_mut(player).insert((pos, facing)); } /// Run N ticks, feeding inputs each tick, return the last snapshot. diff --git a/server/tests/information_boundaries.rs b/server/tests/information_boundaries.rs index a2c7da566..fb9325efc 100644 --- a/server/tests/information_boundaries.rs +++ b/server/tests/information_boundaries.rs @@ -23,21 +23,21 @@ use bevy_ecs::prelude::*; use bevy_ecs::schedule::Schedule; +use settled_reach_server::bridge::types::FacingDirection; use settled_reach_server::knowledge::events::{ process_knowledge_events, KnowledgeEvent, KnowledgeEventQueue, KnowledgeEventType, }; -use settled_reach_server::knowledge::{ - ContradictionDetectedQueue, EntityRegistry, KnowledgeGraph, -}; use settled_reach_server::knowledge::types::StableId; -use settled_reach_server::npc::{Npc, SecretSeverity}; +use settled_reach_server::knowledge::{ContradictionDetectedQueue, EntityRegistry, KnowledgeGraph}; use settled_reach_server::npc::relationships::RelationshipGraph; +use settled_reach_server::npc::{Npc, SecretSeverity}; use settled_reach_server::perception::query::{NaturalVision, PerceptionQuery}; use settled_reach_server::simulation::movement::{TilePosition, WalkabilityMap}; -use settled_reach_server::simulation::save_state::{NpcSaveState, SaveStateV1, SAVE_FORMAT_VERSION}; +use settled_reach_server::simulation::save_state::{ + NpcSaveState, SaveStateV1, SAVE_FORMAT_VERSION, +}; use settled_reach_server::simulation::tier::{ActiveSim, BackgroundSim}; use settled_reach_server::simulation::time::TickRate; -use settled_reach_server::bridge::types::FacingDirection; // =========================================================================== // Layer 1 — Pure unit: no ECS world, no subprocess @@ -70,9 +70,7 @@ fn player_kg_has_no_passive_npc_leakage() { // Negative assertion: spawning a bare ECS entity doesn't populate a KG. // The knowledge graph is a component, not a global shared resource. let mut world = World::new(); - let player = world - .spawn(KnowledgeGraph::new()) - .id(); + let player = world.spawn(KnowledgeGraph::new()).id(); // Spawn an NPC in the same world — no observation system runs. let _npc = world.spawn((Npc, TilePosition::new(50, 50, 0))).id(); @@ -110,7 +108,9 @@ fn snapshot_excludes_entities_outside_los() { // --- Far entity: 45 tiles away, well outside FOV range (~12 tiles) --- let far_npc_pos = TilePosition::new(50, 5, 0); assert!( - !geometry.visible_positions.contains(&(far_npc_pos.x, far_npc_pos.y)), + !geometry + .visible_positions + .contains(&(far_npc_pos.x, far_npc_pos.y)), "IB-2: entity at {:?} (45 tiles from observer) must NOT be in FOV — \ observer snapshot would exclude this entity (fog of perception, D-010 principle 2)", far_npc_pos @@ -118,14 +118,18 @@ fn snapshot_excludes_entities_outside_los() { // --- Sanity check: the observer's own position is visible --- assert!( - geometry.visible_positions.contains(&(observer_pos.x, observer_pos.y)), + geometry + .visible_positions + .contains(&(observer_pos.x, observer_pos.y)), "IB-2 sanity: observer's own position must always be in the FOV set" ); // --- Additional sanity: tile directly ahead (1 step north) is visible --- let adjacent_pos = TilePosition::new(5, 4, 0); assert!( - geometry.visible_positions.contains(&(adjacent_pos.x, adjacent_pos.y)), + geometry + .visible_positions + .contains(&(adjacent_pos.x, adjacent_pos.y)), "IB-2 sanity: tile directly ahead of observer must be visible" ); } @@ -272,9 +276,7 @@ fn background_npc_kg_not_updated_by_active_tier_events() { world.init_resource::(); // Active-tier NPC: will be the observer in the knowledge event. - let active_npc = world - .spawn((Npc, ActiveSim, KnowledgeGraph::new())) - .id(); + let active_npc = world.spawn((Npc, ActiveSim, KnowledgeGraph::new())).id(); // Background-tier NPC: must NOT be affected. let background_npc = world diff --git a/server/tests/ipc_bench.rs b/server/tests/ipc_bench.rs index 4f9ffee6d..bb4a6e1e7 100644 --- a/server/tests/ipc_bench.rs +++ b/server/tests/ipc_bench.rs @@ -102,11 +102,9 @@ fn ipc_round_trip_latency() { let handshake: HandshakeMessage = rmp_serde::from_slice(&handshake_bytes).expect("deserialize HandshakeMessage"); assert_eq!( - handshake.protocol_version, - PROTOCOL_VERSION, + handshake.protocol_version, PROTOCOL_VERSION, "handshake version mismatch: server={}, client={}", - handshake.protocol_version, - PROTOCOL_VERSION + handshake.protocol_version, PROTOCOL_VERSION ); let make_input = |tick: u64| PlayerInput { diff --git a/server/tests/news_ticker.rs b/server/tests/news_ticker.rs index e41d51164..ad2be5569 100644 --- a/server/tests/news_ticker.rs +++ b/server/tests/news_ticker.rs @@ -24,7 +24,7 @@ use std::path::PathBuf; fn ticker_yaml_path() -> PathBuf { let manifest_dir = env!("CARGO_MANIFEST_DIR"); PathBuf::from(manifest_dir) - .join("../content/campaigns/main/systems/van-maanens-star/stations/sova/districts/transit/ticker/the-last-shift.yaml") + .join("content/campaigns/main/systems/van-maanens-star/stations/sova/districts/transit/ticker/the-last-shift.yaml") } /// Minimal YAML structure for parsing just what we need to validate. @@ -83,7 +83,9 @@ fn ticker_yaml_has_30_headlines() { #[test] fn ticker_yaml_location_is_the_last_shift() { let path = ticker_yaml_path(); - if !path.exists() { return; } + if !path.exists() { + return; + } let content = std::fs::read_to_string(&path).expect("read ticker YAML"); let file: TickerFile = serde_yaml::from_str(&content).expect("parse ticker YAML"); @@ -101,26 +103,26 @@ fn ticker_yaml_location_is_the_last_shift() { #[test] fn ticker_yaml_all_headlines_have_required_fields() { let path = ticker_yaml_path(); - if !path.exists() { return; } + if !path.exists() { + return; + } let content = std::fs::read_to_string(&path).expect("read ticker YAML"); let file: TickerFile = serde_yaml::from_str(&content).expect("parse ticker YAML"); for (i, headline) in file.headlines.iter().enumerate() { - assert!( - !headline.id.is_empty(), - "Headline[{}] missing id field", - i - ); + assert!(!headline.id.is_empty(), "Headline[{}] missing id field", i); assert!( !headline.text.is_empty(), "Headline[{}] (id={}) has empty text", - i, headline.id + i, + headline.id ); assert!( !headline.category.is_empty(), "Headline[{}] (id={}) missing category", - i, headline.id + i, + headline.id ); } } @@ -128,7 +130,9 @@ fn ticker_yaml_all_headlines_have_required_fields() { #[test] fn ticker_yaml_ids_are_unique() { let path = ticker_yaml_path(); - if !path.exists() { return; } + if !path.exists() { + return; + } let content = std::fs::read_to_string(&path).expect("read ticker YAML"); let file: TickerFile = serde_yaml::from_str(&content).expect("parse ticker YAML"); @@ -146,10 +150,19 @@ fn ticker_yaml_ids_are_unique() { #[test] fn ticker_yaml_categories_are_valid() { // D-036 defines 6 categories: freight, politics, infrastructure, sports, commission, community - let valid_categories = ["freight", "politics", "infrastructure", "sports", "commission", "community"]; + let valid_categories = [ + "freight", + "politics", + "infrastructure", + "sports", + "commission", + "community", + ]; let path = ticker_yaml_path(); - if !path.exists() { return; } + if !path.exists() { + return; + } let content = std::fs::read_to_string(&path).expect("read ticker YAML"); let file: TickerFile = serde_yaml::from_str(&content).expect("parse ticker YAML"); @@ -158,7 +171,9 @@ fn ticker_yaml_categories_are_valid() { assert!( valid_categories.contains(&headline.category.as_str()), "Headline '{}' has unknown category '{}'. Valid categories: {:?}", - headline.id, headline.category, valid_categories + headline.id, + headline.category, + valid_categories ); } } @@ -168,7 +183,9 @@ fn ticker_yaml_category_distribution_is_sane() { // Comment in the YAML: freight (9), politics (4), infrastructure (5), sports (3), // commission (5), community (4) = 30 total. Verify no category is completely absent. let path = ticker_yaml_path(); - if !path.exists() { return; } + if !path.exists() { + return; + } let content = std::fs::read_to_string(&path).expect("read ticker YAML"); let file: TickerFile = serde_yaml::from_str(&content).expect("parse ticker YAML"); @@ -178,7 +195,14 @@ fn ticker_yaml_category_distribution_is_sane() { *counts.entry(headline.category.clone()).or_insert(0) += 1; } - for cat in ["freight", "politics", "infrastructure", "sports", "commission", "community"] { + for cat in [ + "freight", + "politics", + "infrastructure", + "sports", + "commission", + "community", + ] { assert!( *counts.get(cat).unwrap_or(&0) > 0, "Category '{}' has no headlines — content is missing or miscategorized", diff --git a/server/tests/perf_bench.rs b/server/tests/perf_bench.rs index 24a90588e..b0e440035 100644 --- a/server/tests/perf_bench.rs +++ b/server/tests/perf_bench.rs @@ -15,12 +15,12 @@ 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::{BridgePlugin, BridgeResource}; -use settled_reach_server::simulation::line_pool::{LinePoolIndex, LinePoolIndexResource}; use settled_reach_server::knowledge::registry::EntityRegistry; use settled_reach_server::knowledge::KnowledgeGraph; use settled_reach_server::perception::cognitive_delay::CognitiveDelay; use settled_reach_server::perception::vision_cone::Facing; use settled_reach_server::simulation::interaction::NearbyInteractionBuffer; +use settled_reach_server::simulation::line_pool::{LinePoolIndex, LinePoolIndexResource}; use settled_reach_server::simulation::listening::ListeningFocus; use settled_reach_server::simulation::monologue::{ MonologueBuffer, MonologueState, SprintAnomalyQueue, @@ -33,7 +33,6 @@ const WARMUP_TICKS: usize = 5; const MEASURE_TICKS: usize = 50; const TOTAL_TICKS: usize = WARMUP_TICKS + MEASURE_TICKS; - fn read_rss_kb() -> Option { std::fs::read_to_string("/proc/self/status") .ok() diff --git a/server/tests/serialization.rs b/server/tests/serialization.rs index 1d0786ec0..2f8c8a7b8 100644 --- a/server/tests/serialization.rs +++ b/server/tests/serialization.rs @@ -404,7 +404,7 @@ fn all_facing_direction_variants_roundtrip() { conversation_events: vec![], conversation_ended: vec![], follow_state: None, - character_pressure: None, + character_pressure: None, rng_seed: None, poi_list: vec![], examine_result: None, @@ -412,7 +412,7 @@ fn all_facing_direction_variants_roundtrip() { save_result: None, triangle_crisis_events: vec![], state_hash: None, - debug_response: None, + debug_response: None, sim_errors: vec![], current_ticker: None, settings_response: None, @@ -1706,8 +1706,7 @@ fn fixture_snapshot_minimal_fields() { #[test] fn fixture_snapshot_full_fields() { let bytes = read_named_fixture("snapshot_full"); - let snap: ObserverSnapshot = - rmp_serde::from_slice(&bytes).expect("deserialize snapshot_full"); + let snap: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize snapshot_full"); assert_eq!(snap.version, PROTOCOL_VERSION, "protocol version mismatch"); assert_eq!(snap.tick, 42, "tick should be 42"); @@ -1715,10 +1714,7 @@ fn fixture_snapshot_full_fields() { // Monologue let monologue = snap.current_monologue.as_ref().expect("monologue absent"); assert_eq!(monologue.id, "test_monologue_001"); - assert_eq!( - monologue.text, - "Something feels off about this place." - ); + assert_eq!(monologue.text, "Something feels off about this place."); // Dialogue let dialogue = snap.dialogue_response.as_ref().expect("dialogue absent"); @@ -1738,7 +1734,10 @@ fn fixture_snapshot_full_fields() { assert_eq!(examine.entity_id, 42); // Player knowledge - let kg = snap.player_knowledge.as_ref().expect("player_knowledge absent"); + let kg = snap + .player_knowledge + .as_ref() + .expect("player_knowledge absent"); assert_eq!(kg.entities.len(), 1); assert_eq!(kg.entities[0].name, "Kael"); assert_eq!(kg.facts.len(), 1); @@ -1758,8 +1757,7 @@ fn fixture_snapshot_full_fields() { #[test] fn fixture_player_input_move_fields() { let bytes = read_named_fixture("player_input_move"); - let input: PlayerInput = - rmp_serde::from_slice(&bytes).expect("deserialize player_input_move"); + let input: PlayerInput = rmp_serde::from_slice(&bytes).expect("deserialize player_input_move"); assert_eq!(input.tick, 1, "tick should be 1"); assert!( @@ -1780,7 +1778,11 @@ fn fixture_player_input_interact_fields() { target_entity_id, verb, } => { - assert_eq!(*target_entity_id, Some(99u64), "target_entity_id should be Some(99)"); + assert_eq!( + *target_entity_id, + Some(99u64), + "target_entity_id should be Some(99)" + ); assert_eq!( verb.as_deref(), Some("Talk"), diff --git a/server/tests/tell_escalation.rs b/server/tests/tell_escalation.rs index 21e36657b..1f41b93f6 100644 --- a/server/tests/tell_escalation.rs +++ b/server/tests/tell_escalation.rs @@ -17,12 +17,12 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; use settled_reach_server::{ + npc::mood::MoodState, npc::{ tell_state::{DerivedTellState, TellCategory}, Contentment, DeviationTrigger, Npc, RoutineDeviation, Secret, SecretSeverity, ToleranceThreshold, }, - npc::mood::MoodState, simulation::tier::ActiveSim, }; @@ -42,9 +42,15 @@ fn make_tell_world_with_deviation(deviation: Option) -> (World severity: SecretSeverity::Minor, known_by: vec![], }, - ToleranceThreshold { current_stress: 0, threshold: 50 }, + ToleranceThreshold { + current_stress: 0, + threshold: 50, + }, Contentment { level: 0 }, - MoodState { mood: settled_reach_server::npc::mood::NpcMood::Neutral, changed_tick: 0 }, + MoodState { + mood: settled_reach_server::npc::mood::NpcMood::Neutral, + changed_tick: 0, + }, DerivedTellState::default(), )); let entity = if let Some(dev) = deviation { @@ -103,9 +109,7 @@ fn no_deviation_component_does_not_produce_deviation_tell() { #[cfg(feature = "gauntlet")] fn build_storyteller_app() -> App { use settled_reach_server::{ - bridge::types::CharacterArchetype, - simulation::SimulationPlugin, - test_world, + bridge::types::CharacterArchetype, simulation::SimulationPlugin, test_world, }; let mut app = App::new(); app.add_plugins(SimulationPlugin); @@ -118,7 +122,9 @@ fn build_storyteller_app() -> App { fn first_npc_entity(app: &mut App) -> Entity { use settled_reach_server::npc::Npc; let mut q = app.world_mut().query_filtered::>(); - q.iter(app.world()).next().expect("gauntlet must have at least one NPC") + q.iter(app.world()) + .next() + .expect("gauntlet must have at least one NPC") } #[cfg(feature = "gauntlet")] @@ -127,8 +133,8 @@ fn first_npc_entity(app: &mut App) -> Entity { fn triangle_activation_event_inserts_routine_deviation_on_anchor_npc() { // Inject a TriangleActivatedEvent pointing to an NPC, run one tick, assert // RoutineDeviation is inserted on that NPC by escalate_tells_on_activation. + use settled_reach_server::simulation::triangle::TriangleId; use settled_reach_server::storyteller::{TriangleActivatedEvent, TriangleActivatedQueue}; - use settled_reach_server::simulation::triangle::{TriangleId}; let mut app = build_storyteller_app(); // Run one tick so the world is fully initialized before we inject @@ -162,8 +168,8 @@ fn triangle_activation_event_inserts_routine_deviation_on_anchor_npc() { fn triangle_activation_produces_routine_deviation_tell_in_snapshot() { // End-to-end: after activation event, DerivedTellState on anchor NPC must be // TellCategory::RoutineDeviation. This verifies the full axis-9 pipeline. - use settled_reach_server::storyteller::{TriangleActivatedEvent, TriangleActivatedQueue}; use settled_reach_server::simulation::triangle::TriangleId; + use settled_reach_server::storyteller::{TriangleActivatedEvent, TriangleActivatedQueue}; let mut app = build_storyteller_app(); app.update(); // initialize @@ -198,8 +204,8 @@ fn routine_deviation_expires_after_duration() { // // Edge case: D-027 criterion 4 must continue to fire DURING the window // and stop firing AFTER it. NPCs shouldn't be permanently flagged. - use settled_reach_server::storyteller::{TriangleActivatedEvent, TriangleActivatedQueue}; use settled_reach_server::simulation::triangle::TriangleId; + use settled_reach_server::storyteller::{TriangleActivatedEvent, TriangleActivatedQueue}; // NOTE: TELL_ESCALATION_DURATION_TICKS constant (= 300) expected in storyteller module. // This test will need updating once the constant is public. diff --git a/server/tests/triangle_escalation.rs b/server/tests/triangle_escalation.rs index 655ca8acc..8bbe0a728 100644 --- a/server/tests/triangle_escalation.rs +++ b/server/tests/triangle_escalation.rs @@ -13,17 +13,14 @@ use std::collections::BTreeMap; use bevy_ecs::{schedule::Schedule, world::World}; use settled_reach_server::{ + knowledge::{registry::EntityRegistry, types::StableId, StableEntityId}, + npc::ToleranceThreshold, simulation::triangle::{ apply_resolve_triangle, tick_triangle_escalation, ResolveTriangleCommand, ResolveTriangleQueue, TemplateId, TriangleClassification, TriangleCrisisEventQueue, TriangleDef, TriangleId, TrianglePhase, TriangleState, }, - knowledge::{registry::EntityRegistry, types::StableId, StableEntityId}, - npc::ToleranceThreshold, - simulation::{ - tier::ActiveSim, - time::SimulationTime, - }, + simulation::{tier::ActiveSim, time::SimulationTime}, }; // --------------------------------------------------------------------------- @@ -43,9 +40,18 @@ fn make_escalation_world() -> World { fn spawn_npc_with_threshold(world: &mut World, stable_id_val: u64, threshold: i16) -> StableId { let sid = StableId(stable_id_val); let entity = world - .spawn((ActiveSim, StableEntityId(sid), ToleranceThreshold { current_stress: 0, threshold })) + .spawn(( + ActiveSim, + StableEntityId(sid), + ToleranceThreshold { + current_stress: 0, + threshold, + }, + )) .id(); - world.resource_mut::().register_existing(entity, sid); + world + .resource_mut::() + .register_existing(entity, sid); sid } @@ -190,7 +196,14 @@ fn d087_seed_dependent_escalation_timing() { assignments.insert(RoleId::new("r"), npc); // Spawn as separate triangles. - let slow = spawn_triangle(&mut world, 10, 0, 2, TrianglePhase::Simmering, assignments.clone()); + let slow = spawn_triangle( + &mut world, + 10, + 0, + 2, + TrianglePhase::Simmering, + assignments.clone(), + ); let fast = spawn_triangle(&mut world, 20, 0, 8, TrianglePhase::Simmering, assignments); let mut schedule = Schedule::default(); @@ -202,8 +215,14 @@ fn d087_seed_dependent_escalation_timing() { } // Both should be Active by 400 ticks. - assert_eq!(world.get::(slow).unwrap().phase, TrianglePhase::Active); - assert_eq!(world.get::(fast).unwrap().phase, TrianglePhase::Active); + assert_eq!( + world.get::(slow).unwrap().phase, + TrianglePhase::Active + ); + assert_eq!( + world.get::(fast).unwrap().phase, + TrianglePhase::Active + ); // Fast triangle should have activated earlier (higher tension accumulated faster). let fast_tension = world.get::(fast).unwrap().tension; @@ -220,7 +239,7 @@ fn crisis_event_trigger_npc_is_lowest_threshold() { let mut world = make_escalation_world(); let npc_high = spawn_npc_with_threshold(&mut world, 1, 50); // high tolerance - let npc_low = spawn_npc_with_threshold(&mut world, 2, 10); // low tolerance — trigger + let npc_low = spawn_npc_with_threshold(&mut world, 2, 10); // low tolerance — trigger use settled_reach_server::simulation::triangle::RoleId; let mut assignments = BTreeMap::new(); @@ -240,7 +259,10 @@ fn crisis_event_trigger_npc_is_lowest_threshold() { queue.events[0].trigger_npc, npc_low, "trigger NPC must be the one with the lowest threshold" ); - assert_eq!(queue.events[0].tick, 10, "crisis tick must match the game-minute"); + assert_eq!( + queue.events[0].tick, 10, + "crisis tick must match the game-minute" + ); } /// No crisis event when tension hasn't exceeded the threshold. @@ -260,7 +282,10 @@ fn no_crisis_event_below_threshold() { run_at_tick(&mut world, &mut schedule, 10); let queue = world.resource::(); - assert!(queue.is_empty(), "no crisis event when tension (5) < threshold (100)"); + assert!( + queue.is_empty(), + "no crisis event when tension (5) < threshold (100)" + ); } // --------------------------------------------------------------------------- @@ -281,28 +306,49 @@ fn active_triangle_continues_incrementing_no_new_event() { run_at_tick(&mut world, &mut schedule, 20); let queue = world.resource::(); - assert!(queue.is_empty(), "no crisis event for already-Active triangle"); + assert!( + queue.is_empty(), + "no crisis event for already-Active triangle" + ); } /// Active triangle tension saturates at u8::MAX (255). #[test] fn active_triangle_tension_saturates_at_u8_max() { let mut world = make_escalation_world(); - spawn_triangle(&mut world, 1, 252, 10, TrianglePhase::Active, BTreeMap::new()); + spawn_triangle( + &mut world, + 1, + 252, + 10, + TrianglePhase::Active, + BTreeMap::new(), + ); let mut schedule = Schedule::default(); schedule.add_systems(tick_triangle_escalation); run_at_tick(&mut world, &mut schedule, 10); // First call: 252 + 10 = 262, saturates to 255 - let entity = world.query::().iter(&world).next().unwrap(); + let entity = world + .query::() + .iter(&world) + .next() + .unwrap(); // Can't query TriangleState after mutable borrow; check via resource // (we verify by spawning directly and checking post-run) let _ = entity; // entity used to ensure spawn worked // Re-run test cleanly let mut world2 = make_escalation_world(); - let e2 = spawn_triangle(&mut world2, 2, 254, 50, TrianglePhase::Active, BTreeMap::new()); + let e2 = spawn_triangle( + &mut world2, + 2, + 254, + 50, + TrianglePhase::Active, + BTreeMap::new(), + ); let mut sched2 = Schedule::default(); sched2.add_systems(tick_triangle_escalation); run_at_tick(&mut world2, &mut sched2, 10); @@ -349,7 +395,14 @@ fn d026_non_active_tier_triangle_not_escalated() { #[test] fn dormant_triangle_not_escalated() { let mut world = make_escalation_world(); - let entity = spawn_triangle(&mut world, 1, 0, 10, TrianglePhase::Dormant, BTreeMap::new()); + let entity = spawn_triangle( + &mut world, + 1, + 0, + 10, + TrianglePhase::Dormant, + BTreeMap::new(), + ); let mut schedule = Schedule::default(); schedule.add_systems(tick_triangle_escalation); @@ -366,7 +419,14 @@ fn dormant_triangle_not_escalated() { #[test] fn resolved_triangle_not_escalated() { let mut world = make_escalation_world(); - let entity = spawn_triangle(&mut world, 1, 50, 5, TrianglePhase::Resolved, BTreeMap::new()); + let entity = spawn_triangle( + &mut world, + 1, + 50, + 5, + TrianglePhase::Resolved, + BTreeMap::new(), + ); let mut schedule = Schedule::default(); schedule.add_systems(tick_triangle_escalation); @@ -401,14 +461,20 @@ fn resolve_command_sets_phase_to_resolved() { }) .id(); - world.resource_mut::().push(ResolveTriangleCommand(TriangleId(100))); + world + .resource_mut::() + .push(ResolveTriangleCommand(TriangleId(100))); let mut schedule = Schedule::default(); schedule.add_systems(apply_resolve_triangle); schedule.run(&mut world); let state = world.get::(entity).unwrap(); - assert_eq!(state.phase, TrianglePhase::Resolved, "resolve command must set phase to Resolved"); + assert_eq!( + state.phase, + TrianglePhase::Resolved, + "resolve command must set phase to Resolved" + ); assert_eq!(state.tension, 50, "tension must not change on resolve"); } @@ -455,7 +521,9 @@ fn d089_resolve_does_not_cascade() { .id(); // Resolve only triangle 100. - world.resource_mut::().push(ResolveTriangleCommand(TriangleId(100))); + world + .resource_mut::() + .push(ResolveTriangleCommand(TriangleId(100))); let mut schedule = Schedule::default(); schedule.add_systems(apply_resolve_triangle); @@ -498,9 +566,13 @@ fn resolve_twice_is_idempotent() { let mut schedule = Schedule::default(); schedule.add_systems(apply_resolve_triangle); - world.resource_mut::().push(ResolveTriangleCommand(TriangleId(100))); + world + .resource_mut::() + .push(ResolveTriangleCommand(TriangleId(100))); schedule.run(&mut world); - world.resource_mut::().push(ResolveTriangleCommand(TriangleId(100))); + world + .resource_mut::() + .push(ResolveTriangleCommand(TriangleId(100))); schedule.run(&mut world); assert_eq!( @@ -526,7 +598,14 @@ fn crisis_events_accumulate_until_drained() { assignments.insert(RoleId::new("r"), npc); // Two triangles that will both escalate. - spawn_triangle(&mut world, 10, 0, 6, TrianglePhase::Simmering, assignments.clone()); + spawn_triangle( + &mut world, + 10, + 0, + 6, + TrianglePhase::Simmering, + assignments.clone(), + ); spawn_triangle(&mut world, 20, 0, 6, TrianglePhase::Simmering, assignments); let mut schedule = Schedule::default(); @@ -578,16 +657,37 @@ fn d087_all_v01_conflict_types_produce_escalatable_states() { use settled_reach_server::simulation::triangle::{ConflictType, NpcAxis, RoleId}; let defs = [ - ("kael-davan", "smuggler", "ring-contact", ConflictType::ResourceCompetition), - ("sera-venn", "detective", "commission-inspector", ConflictType::SecretExposure), + ( + "kael-davan", + "smuggler", + "ring-contact", + ConflictType::ResourceCompetition, + ), + ( + "sera-venn", + "detective", + "commission-inspector", + ConflictType::SecretExposure, + ), ("naia", "kael-davan", "hael", ConflictType::LatentTension), - ("drin", "ring-system", "dock-supervisor", ConflictType::ResourceCompetition), - ("worried-partner", "ring-member", "neighbor", ConflictType::LatentTension), + ( + "drin", + "ring-system", + "dock-supervisor", + ConflictType::ResourceCompetition, + ), + ( + "worried-partner", + "ring-member", + "neighbor", + ConflictType::LatentTension, + ), ]; for (r0, r1, r2, conflict) in &defs { let roles = [RoleId::new(r0), RoleId::new(r1), RoleId::new(r2)]; - let tid = settled_reach_server::simulation::triangle::TriangleId::from_seed_and_roles(42, &roles); + let tid = + settled_reach_server::simulation::triangle::TriangleId::from_seed_and_roles(42, &roles); let def = TriangleDef { triangle_id: tid, roles: roles.clone(), @@ -595,7 +695,11 @@ fn d087_all_v01_conflict_types_produce_escalatable_states() { interest_axes: [NpcAxis::Want, NpcAxis::Secret, NpcAxis::Relationships], relationship_constraints: vec![], }; - assert!(def.validate().is_ok(), "D-087 triangle must be valid: {:?}", def.validate()); + assert!( + def.validate().is_ok(), + "D-087 triangle must be valid: {:?}", + def.validate() + ); // Can construct a TriangleState from the def. let mut assignments = BTreeMap::new(); diff --git a/server/tests/triangle_validation.rs b/server/tests/triangle_validation.rs index d7f60ae17..aa67621c2 100644 --- a/server/tests/triangle_validation.rs +++ b/server/tests/triangle_validation.rs @@ -9,15 +9,14 @@ //! `cargo test -p settled-reach-server -- triangle_validation` use settled_reach_server::{ - simulation::triangle::{ - generate_cross_template_triangles, generate_intra_template_triangles, - validate_triangle_def, ConflictType, NpcAxis, RelationshipConstraint, RoleId, - TemplateId, TemplateOwnership, TriangleDef, TriangleId, TrianglePhase, TrustRange, - ValidationError, - }, knowledge::{registry::StableEntityId, types::StableId}, npc::{Npc, RelationshipKind}, simulation::rng::SimRng, + simulation::triangle::{ + generate_cross_template_triangles, generate_intra_template_triangles, + validate_triangle_def, ConflictType, NpcAxis, RelationshipConstraint, RoleId, TemplateId, + TemplateOwnership, TriangleDef, TriangleId, TrianglePhase, TrustRange, ValidationError, + }, }; // --------------------------------------------------------------------------- @@ -102,7 +101,12 @@ fn triangle_validation_conflict_viability_fails_without_want_axis() { let result = validate_triangle_def(&def); assert!( - matches!(result, Err(ValidationError::ConflictViability { triangle_id: TriangleId(10) })), + matches!( + result, + Err(ValidationError::ConflictViability { + triangle_id: TriangleId(10) + }) + ), "expected ConflictViability error, got: {:?}", result ); @@ -129,7 +133,12 @@ fn triangle_validation_relationship_coherence_fails_without_constraints() { let result = validate_triangle_def(&def); assert!( - matches!(result, Err(ValidationError::RelationshipCoherence { triangle_id: TriangleId(20) })), + matches!( + result, + Err(ValidationError::RelationshipCoherence { + triangle_id: TriangleId(20) + }) + ), "expected RelationshipCoherence error, got: {:?}", result ); @@ -214,11 +223,7 @@ fn triangle_validation_interest_divergence_first_last_duplicate() { fn triangle_validation_conflict_viability_checked_before_coherence() { let def = TriangleDef { triangle_id: TriangleId(40), - roles: [ - RoleId::new("a"), - RoleId::new("b"), - RoleId::new("c"), - ], + roles: [RoleId::new("a"), RoleId::new("b"), RoleId::new("c")], conflict_type: ConflictType::LatentTension, interest_axes: [NpcAxis::Contentment, NpcAxis::Tolerance, NpcAxis::Routine], relationship_constraints: vec![], // also fails coherence @@ -266,13 +271,8 @@ fn triangle_validation_cross_template_spans_two_templates() { ..def }; - let result = generate_cross_template_triangles( - &mut world, - hub_id, - bar_id, - &[overridden], - &mut rng, - ); + let result = + generate_cross_template_triangles(&mut world, hub_id, bar_id, &[overridden], &mut rng); assert!( result.warnings.is_empty(), @@ -297,12 +297,22 @@ fn triangle_validation_cross_template_spans_two_templates() { let freight = state.role_assignments[&RoleId::new("freight-handler")]; let bartender = state.role_assignments[&RoleId::new("bartender")]; assert_eq!(ops, StableId(1), "ops-manager must map to hub NPC 1"); - assert_eq!(freight, StableId(2), "freight-handler must map to hub NPC 2"); + assert_eq!( + freight, + StableId(2), + "freight-handler must map to hub NPC 2" + ); assert_eq!(bartender, StableId(3), "bartender must map to bar NPC 3"); assert_eq!(state.phase, TrianglePhase::Simmering); - assert!(state.tension >= 5 && state.tension <= 25, "tension in seeded range"); - assert!(state.tension_rate >= 1 && state.tension_rate <= 5, "rate in seeded range"); + assert!( + state.tension >= 5 && state.tension <= 25, + "tension in seeded range" + ); + assert!( + state.tension_rate >= 1 && state.tension_rate <= 5, + "rate in seeded range" + ); } /// Cross-template generation skips defs that fail validation, adding a warning. @@ -335,10 +345,15 @@ fn triangle_validation_cross_template_skips_invalid_defs() { }], }; - let result = generate_cross_template_triangles(&mut world, hub_id, bar_id, &[invalid], &mut rng); + let result = + generate_cross_template_triangles(&mut world, hub_id, bar_id, &[invalid], &mut rng); assert_eq!(result.triangles.len(), 0, "invalid def must be skipped"); - assert_eq!(result.warnings.len(), 1, "exactly one warning for the skipped def"); + assert_eq!( + result.warnings.len(), + 1, + "exactly one warning for the skipped def" + ); assert!( result.warnings[0].contains("validation failed"), "warning must mention validation failure: {}", @@ -391,8 +406,7 @@ fn triangle_validation_cross_template_deterministic() { assert_eq!(result1.triangles.len(), 1); assert_eq!(result2.triangles.len(), 1); assert_eq!( - result1.triangles[0].tension, - result2.triangles[0].tension, + result1.triangles[0].tension, result2.triangles[0].tension, "cross-template generation must be deterministic (D-010)" ); assert_eq!( @@ -431,7 +445,12 @@ fn triangle_validation_intra_template_does_not_see_other_template_npcs() { relationship_constraints: vec![], }; - let result = generate_intra_template_triangles(&mut world, hub_id, &[def, valid_triangle_def(6)], &mut rng); + let result = generate_intra_template_triangles( + &mut world, + hub_id, + &[def, valid_triangle_def(6)], + &mut rng, + ); // The def needing "inspector" should fall back (inspector is in bar, not hub) // At least one warning about the missing role diff --git a/server/tests/v01_integration_playthrough.rs b/server/tests/v01_integration_playthrough.rs index eefd5fec8..2856a8292 100644 --- a/server/tests/v01_integration_playthrough.rs +++ b/server/tests/v01_integration_playthrough.rs @@ -70,26 +70,40 @@ impl TestServer { } Err(e) => panic!("failed to read server stdout: {}", e), } - assert!(Instant::now() < deadline, "timed out waiting for LISTENING signal"); + assert!( + Instant::now() < deadline, + "timed out waiting for LISTENING signal" + ); } }; let addr = format!("127.0.0.1:{}", port); let stream = TcpStream::connect(&addr).expect("client connect"); - stream.set_read_timeout(Some(SNAPSHOT_TIMEOUT)).expect("set timeout"); + stream + .set_read_timeout(Some(SNAPSHOT_TIMEOUT)) + .expect("set timeout"); let mut reader = BufReader::new(stream.try_clone().expect("clone stream")); let mut writer = BufWriter::new(stream); // Protocol handshake - let hf = read_framed(&mut reader).expect("read handshake").expect("connection closed"); + let hf = read_framed(&mut reader) + .expect("read handshake") + .expect("connection closed"); let _: HandshakeMessage = rmp_serde::from_slice(&hf).expect("deserialize handshake"); // StartupMessage with chosen archetype - let startup = StartupMessage { world_seed, character_archetype: archetype }; + let startup = StartupMessage { + world_seed, + character_archetype: archetype, + }; let startup_payload = rmp_serde::to_vec_named(&startup).expect("serialize startup"); write_framed(&mut writer, &startup_payload).expect("send startup"); - TestServer { child, reader, writer } + TestServer { + child, + reader, + writer, + } } /// Send a tick's worth of inputs (empty = idle tick) and read back one snapshot. @@ -126,7 +140,10 @@ impl TestServer { } std::thread::sleep(Duration::from_millis(50)); } - Err(_) => { self.child.kill().ok(); break; } + Err(_) => { + self.child.kill().ok(); + break; + } } } } @@ -142,9 +159,15 @@ fn test_smuggler_opening_monologue() { // Monologue IDs from smuggler/opening.yaml start with "pc-smuggler_". // This verifies: archetype → MonologueState.character → pool selection (D-032, #587, #595). let mut server = TestServer::boot_gauntlet(12345, CharacterArchetype::Smuggler); - let snapshot = server.tick(vec![PlayerInput { tick: 0, action: PlayerAction::MoveNorth }]); + let snapshot = server.tick(vec![PlayerInput { + tick: 0, + action: PlayerAction::MoveNorth, + }]); - assert_eq!(snapshot.version, PROTOCOL_VERSION, "protocol version mismatch"); + assert_eq!( + snapshot.version, PROTOCOL_VERSION, + "protocol version mismatch" + ); let monologue = snapshot.current_monologue; assert!( @@ -169,9 +192,15 @@ fn test_detective_opening_monologue() { // Boot with Detective, advance 1 tick, assert opening monologue fires from detective pool. // Monologue IDs from detective/opening.yaml start with "pc-detective_". let mut server = TestServer::boot_gauntlet(12345, CharacterArchetype::Detective); - let snapshot = server.tick(vec![PlayerInput { tick: 0, action: PlayerAction::MoveNorth }]); + let snapshot = server.tick(vec![PlayerInput { + tick: 0, + action: PlayerAction::MoveNorth, + }]); - assert_eq!(snapshot.version, PROTOCOL_VERSION, "protocol version mismatch"); + assert_eq!( + snapshot.version, PROTOCOL_VERSION, + "protocol version mismatch" + ); let monologue = snapshot.current_monologue; assert!( @@ -197,7 +226,8 @@ fn test_smuggler_and_detective_get_different_opening_monologue_ids() { // the same monologue ID on tick 1. If they do, D-032 partitioning is broken. let mut smug = TestServer::boot_gauntlet(12345, CharacterArchetype::Smuggler); let smug_snap = smug.tick(vec![]); - let smug_id = smug_snap.current_monologue + let smug_id = smug_snap + .current_monologue .as_ref() .map(|m| m.id.clone()) .unwrap_or_default(); @@ -205,7 +235,8 @@ fn test_smuggler_and_detective_get_different_opening_monologue_ids() { let mut det = TestServer::boot_gauntlet(12345, CharacterArchetype::Detective); let det_snap = det.tick(vec![]); - let det_id = det_snap.current_monologue + let det_id = det_snap + .current_monologue .as_ref() .map(|m| m.id.clone()) .unwrap_or_default(); @@ -236,7 +267,9 @@ fn test_v0_1_integration_playthrough() { // === Criterion 1: Opening monologue (Smuggler) === let tick1 = server.tick(vec![]); - let monologue = tick1.current_monologue.expect("Opening monologue must fire on tick 1"); + let monologue = tick1 + .current_monologue + .expect("Opening monologue must fire on tick 1"); assert!( monologue.id.starts_with("pc-smuggler_"), "Tick-1 monologue must be from smuggler pool. Got: {}", @@ -266,7 +299,11 @@ fn test_v0_1_integration_playthrough() { let mut deviation_observed = false; for _ in 0..5 { let snap = server.tick(vec![]); - if snap.entities.iter().any(|e| e.tell_state == Some(TellCategory::RoutineDeviation)) { + if snap + .entities + .iter() + .any(|e| e.tell_state == Some(TellCategory::RoutineDeviation)) + { deviation_observed = true; break; } @@ -278,7 +315,9 @@ fn test_v0_1_integration_playthrough() { // === Criterion 3 (D-036): News ticker visible in bar zone === // Teleport to The Last Shift bar zone and check current_ticker is Some. - let _teleport = server.send_debug(DebugCommandKind::TeleportToLocation("the-last-shift".into())); + let _teleport = server.send_debug(DebugCommandKind::TeleportToLocation( + "the-last-shift".into(), + )); let bar_snap = server.tick(vec![]); assert!( bar_snap.current_ticker.is_some(), diff --git a/server/tests/voice_pipeline.rs b/server/tests/voice_pipeline.rs index 001eca4e2..6a46eed9b 100644 --- a/server/tests/voice_pipeline.rs +++ b/server/tests/voice_pipeline.rs @@ -17,8 +17,8 @@ use settled_reach_server::npc::blueprint::{ CulturalValues, CultureProfile, NamingConventions, OccasionalInjection, SpeechPatterns, VoiceExample, }; -use settled_reach_server::npc::PersonalityTrait; use settled_reach_server::npc::tell_state::TellCategory; +use settled_reach_server::npc::PersonalityTrait; use settled_reach_server::voice::cache::VoiceCacheStore; use settled_reach_server::voice::prompt_builder::ContentType; use settled_reach_server::voice::queue::{Priority, VoiceQueue, VoiceRequest}; @@ -59,11 +59,7 @@ fn voice_config() -> VoiceProcessConfig { } else { // Fall back to mock script — no model needed let mock = manifest.join("sr-voice/mock-stdio.sh"); - assert!( - mock.exists(), - "Mock script not found: {}", - mock.display() - ); + assert!(mock.exists(), "Mock script not found: {}", mock.display()); eprintln!("Using mock sr-voice: {}", mock.display()); if !bin_path.exists() { eprintln!(" (real binary not found: {})", bin_path.display()); @@ -299,7 +295,11 @@ fn voice_pipeline_end_to_end() { drop(cache_guard); // Drop triggers save_all via the Drop impl — but the cache is behind // Arc>, so we can't drop it here. Explicitly save instead. - cache.lock().unwrap().save_all().expect("failed to save cache"); + cache + .lock() + .unwrap() + .save_all() + .expect("failed to save cache"); std::fs::write(&results_path, &results).expect("failed to write results"); eprintln!("Results written to {}", results_path.display()); diff --git a/server/tests/voice_quality_batch.rs b/server/tests/voice_quality_batch.rs index e9c5f0e8c..c8660357e 100644 --- a/server/tests/voice_quality_batch.rs +++ b/server/tests/voice_quality_batch.rs @@ -15,8 +15,8 @@ use settled_reach_server::npc::blueprint::{ CulturalValues, CultureProfile, NamingConventions, OccasionalInjection, SpeechPatterns, VoiceExample, }; -use settled_reach_server::npc::PersonalityTrait; use settled_reach_server::npc::tell_state::TellCategory; +use settled_reach_server::npc::PersonalityTrait; use settled_reach_server::voice::cache::{CacheKey, VoiceCacheStore}; use settled_reach_server::voice::prompt_builder::ContentType; use settled_reach_server::voice::queue::{Priority, VoiceQueue, VoiceRequest}; @@ -106,8 +106,9 @@ fn van_maanens_star_culture() -> CultureProfile { ], occasional_injections: vec![OccasionalInjection { kind: "oath".into(), - clause: "Use an oath like \"void take it\" when something is surprising or frustrating." - .into(), + clause: + "Use an oath like \"void take it\" when something is surprising or frustrating." + .into(), example: Some(VoiceExample { input: "discovers a critical part is missing".into(), output: "Void take it. The coupling's not here.".into(), @@ -623,13 +624,20 @@ fn voice_quality_batch() { .unwrap_or("[MISSING — not cached]"); output.push_str(&format!("--- #{}: {} ---\n", i + 1, case.label)); - output.push_str(&format!(" tell: {:?} | seed: {} | type: {:?}\n", case.tell_state, case.seed, case.content_type)); + output.push_str(&format!( + " tell: {:?} | seed: {} | type: {:?}\n", + case.tell_state, case.seed, case.content_type + )); output.push_str(&format!(" BASE: {}\n", case.base_text)); output.push_str(&format!(" VOICED: {}\n\n", voiced)); } drop(cache_guard); - cache.lock().unwrap().save_all().expect("failed to save cache"); + cache + .lock() + .unwrap() + .save_all() + .expect("failed to save cache"); let results_path = out.join("quality-batch.txt"); std::fs::write(&results_path, &output).expect("failed to write results");