diff --git a/server/src/perception/interpretation.rs b/server/src/perception/interpretation.rs index 95fd52300..0e8ad310a 100644 --- a/server/src/perception/interpretation.rs +++ b/server/src/perception/interpretation.rs @@ -3,6 +3,10 @@ //! Interprets what the observer sees (and doesn't see) against known NPC //! routines and knowledge graph state. Produces high-level observation events //! that drive monologue and investigation triggers. +//! +//! Note: HashSet is used as a per-frame lookup table (visible tiles/NPCs). +//! Only membership checks — iteration order is irrelevant. Not simulation state. +#![allow(clippy::disallowed_types)] use bevy_ecs::prelude::*; diff --git a/server/src/perception/shadowcast.rs b/server/src/perception/shadowcast.rs index e89659e05..2b1b46155 100644 --- a/server/src/perception/shadowcast.rs +++ b/server/src/perception/shadowcast.rs @@ -9,6 +9,12 @@ //! References: //! - Symmetric: https://www.albertford.com/shadowcasting/ //! - Traditional: RogueBasin recursive shadowcasting +//! +//! Note: HashSet is used here as a per-frame scratch accumulator for visible +//! tile positions during the FOV sweep. Only `insert` and `contains` are used; +//! iteration order never affects the output (results are handed to BTreeSet in +//! query.rs). Not simulation state — exempt from the determinism constraint. +#![allow(clippy::disallowed_types)] use std::collections::HashSet; diff --git a/server/src/simulation/contraband.rs b/server/src/simulation/contraband.rs index aa3fc9799..a772b6b45 100644 --- a/server/src/simulation/contraband.rs +++ b/server/src/simulation/contraband.rs @@ -80,7 +80,10 @@ impl ScanEventBuffer { pub fn check_contraband_scan( time: Res, registry: Res, - mut npc_query: Query<(Entity, &TilePosition, &mut KnowledgeGraph), (With, With)>, + mut npc_query: Query< + (Entity, &TilePosition, &mut KnowledgeGraph), + (With, With), + >, mut player_query: Query<(Entity, &TilePosition, &mut ScanEventBuffer), With>, items_query: Query<(&CarriedBy, Option<&Contraband>)>, ) { @@ -423,7 +426,10 @@ mod tests { let mut buffer = world.get_mut::(player).unwrap(); let events = buffer.take(); - assert!(events.is_empty(), "NPC without ScanAuthority should not scan"); + assert!( + events.is_empty(), + "NPC without ScanAuthority should not scan" + ); } #[test] @@ -461,12 +467,7 @@ mod tests { ); let npc = world - .spawn(( - Npc, - TilePosition::new(5, 6, 0), - npc_kg, - ScanAuthority, - )) + .spawn((Npc, TilePosition::new(5, 6, 0), npc_kg, ScanAuthority)) .id(); world.resource_mut::().register(npc); @@ -477,12 +478,19 @@ mod tests { // NPC already knew — KG should not be re-written (fact tick stays 0) let npc_kg = world.get::(npc).unwrap(); let fact = npc_kg.facts.get(&fact_id).unwrap(); - assert_eq!(fact.acquired_tick, 0, "should not overwrite existing knowledge"); + assert_eq!( + fact.acquired_tick, 0, + "should not overwrite existing knowledge" + ); // Scan event should still fire even though NPC already knew let mut buffer = world.get_mut::(player).unwrap(); let events = buffer.take(); - assert_eq!(events.len(), 1, "scan event should emit even for already-known contraband"); + assert_eq!( + events.len(), + 1, + "scan event should emit even for already-known contraband" + ); assert!(events[0].detected_contraband); } @@ -542,7 +550,11 @@ mod tests { // Both should emit separate scan events let mut buffer = world.get_mut::(player).unwrap(); let events = buffer.take(); - assert_eq!(events.len(), 2, "each ScanAuthority NPC should emit a scan event"); + assert_eq!( + events.len(), + 2, + "each ScanAuthority NPC should emit a scan event" + ); assert!(events.iter().all(|e| e.detected_contraband)); } diff --git a/server/src/simulation/dialogue.rs b/server/src/simulation/dialogue.rs index de7919052..c9b9366d3 100644 --- a/server/src/simulation/dialogue.rs +++ b/server/src/simulation/dialogue.rs @@ -179,6 +179,7 @@ pub fn available_access_tiers(relationship: RelationshipState) -> Vec, + pub shown_ids: BTreeSet, /// Character type for pool filtering. v0.1: always "detective". pub character: String, } @@ -89,7 +89,7 @@ impl Default for MonologueState { last_position: None, idle_ticks: 0, entered: false, - shown_ids: HashSet::new(), + shown_ids: BTreeSet::new(), // v0.1: default to detective; character selection sets this character: "detective".to_string(), } diff --git a/server/src/simulation/movement.rs b/server/src/simulation/movement.rs index 027ffa166..a0c38aa68 100644 --- a/server/src/simulation/movement.rs +++ b/server/src/simulation/movement.rs @@ -23,7 +23,17 @@ pub struct PlayerCharacter; /// Examples: a Standing character can walk past a Seated NPC at a console, /// a Fixture (terminal) shares a tile with someone Seated at it. #[derive( - Component, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, + Component, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + Default, + Serialize, Deserialize, )] pub enum TilePresence { @@ -42,7 +52,9 @@ pub enum TilePresence { /// Tile position component for grid-based movement. /// Discrete integer coordinates used in simulation; converted to f32 /// at the bridge boundary for VisibleEntity wire format. -#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[derive( + Component, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, +)] pub struct TilePosition { pub x: i32, pub y: i32, diff --git a/server/src/test_world/mod.rs b/server/src/test_world/mod.rs index 6cebdc9ba..55eac4c93 100644 --- a/server/src/test_world/mod.rs +++ b/server/src/test_world/mod.rs @@ -52,13 +52,13 @@ use crate::perception::cognitive_delay::CognitiveDelay; #[cfg(feature = "gauntlet")] use crate::perception::vision_cone::Facing; #[cfg(feature = "gauntlet")] +use crate::simulation::contraband::ScanEventBuffer; +#[cfg(feature = "gauntlet")] use crate::simulation::interaction::{Interactable, NearbyInteractionBuffer}; #[cfg(feature = "gauntlet")] use crate::simulation::inventory::ItemName; #[cfg(feature = "gauntlet")] use crate::simulation::listening::ListeningFocus; -#[cfg(feature = "gauntlet")] -use crate::simulation::contraband::ScanEventBuffer; use crate::simulation::monologue::{MonologueBuffer, MonologueState, SprintAnomalyQueue}; #[cfg(feature = "gauntlet")] use crate::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap}; diff --git a/server/tests/content_scaling.rs b/server/tests/content_scaling.rs index e1acf2b9e..90fec761f 100644 --- a/server/tests/content_scaling.rs +++ b/server/tests/content_scaling.rs @@ -21,7 +21,9 @@ use std::time::Instant; use settled_reach_server::bridge::types::*; use settled_reach_server::bridge::BridgePlugin; use settled_reach_server::knowledge::registry::{EntityRegistry, StableEntityId}; -use settled_reach_server::knowledge::{KnowledgeConfidence, KnowledgeGraph, KnowledgePlugin, StableId}; +use settled_reach_server::knowledge::{ + KnowledgeConfidence, KnowledgeGraph, KnowledgePlugin, StableId, +}; use settled_reach_server::npc::{Contentment, Npc, NpcPlugin, ToleranceThreshold, Want, WantKind}; use settled_reach_server::simulation::interaction::Interactable; use settled_reach_server::simulation::movement::TilePosition; @@ -462,10 +464,8 @@ fn max_npc_pack_behavioral_regression() { .clone(); let stress_kg = player_kg_snapshot(&stress_app, max_gauntlet_id); - let baseline_snap = baseline_snapshot - .expect("baseline Gauntlet should produce a snapshot"); - let stress_snap = stress_snapshot - .expect("80-NPC stress run should produce a snapshot"); + let baseline_snap = baseline_snapshot.expect("baseline Gauntlet should produce a snapshot"); + let stress_snap = stress_snapshot.expect("80-NPC stress run should produce a snapshot"); // Tick index must match (same number of updates). assert_eq!( diff --git a/server/tests/cross_room_transitions.rs b/server/tests/cross_room_transitions.rs index 983462d75..c4b086a40 100644 --- a/server/tests/cross_room_transitions.rs +++ b/server/tests/cross_room_transitions.rs @@ -349,10 +349,7 @@ fn t4_knowledge_graph_survives_room_transition() { "T4 post: KG entry must persist after player moves to Hub" ); assert_eq!( - world - .get::(player) - .unwrap() - .entity_count(), + world.get::(player).unwrap().entity_count(), 1, "T4 post: exactly 1 KG entry after room transition" ); @@ -542,7 +539,10 @@ fn t7_confrontation_verb_disappears_on_retreat_beyond_mid_range() { "T7 close: NPC at distance 2 must appear in interaction buffer" ); assert!( - interactions[0].verbs.iter().any(|v| v.kind == VerbKind::Talk), + interactions[0] + .verbs + .iter() + .any(|v| v.kind == VerbKind::Talk), "T7 close: Talk must be available at CLOSE_RANGE (confrontation possible)" ); @@ -599,7 +599,10 @@ fn t8_sprint_blocks_eavesdrop_then_careful_enables_accumulation() { run_listening_system(&mut world); } assert_eq!( - world.get::(player).unwrap().stationary_ticks, + world + .get::(player) + .unwrap() + .stationary_ticks, 0, "T8 sprint: Sprint must block stationary_ticks (50 ticks at sprint, still 0)" ); @@ -613,7 +616,10 @@ fn t8_sprint_blocks_eavesdrop_then_careful_enables_accumulation() { // resets stationary_ticks to 0, and updates last_position to alcove_pos. run_listening_system(&mut world); assert_eq!( - world.get::(player).unwrap().stationary_ticks, + world + .get::(player) + .unwrap() + .stationary_ticks, 0, "T8 transition: movement tick must reset stationary_ticks to 0" );