diff --git a/server/src/perception/observer/tests.rs b/server/src/perception/observer/tests.rs index f2b1d3024..d564eac2f 100644 --- a/server/src/perception/observer/tests.rs +++ b/server/src/perception/observer/tests.rs @@ -2466,3 +2466,116 @@ fn blocked_entities_sorted_ascending() { assert!(snapshot.blocked_entities.contains(&npc_b_sid.0)); assert!(snapshot.blocked_entities.contains(&npc_c_sid.0)); } + +// ----------------------------------------------------------------------- +// Zone enrichment tests (#523, D-077) +// ----------------------------------------------------------------------- + +#[test] +fn zone_map_enriches_visible_tiles_with_zone_id() { + // Tile inside a zone should get zone_id = Some(zone_id) + use crate::simulation::zone::ZoneMap; + + let mut world = setup_world(32, 32); + let mut zone_map = ZoneMap::default(); + // Zone 42 covers (14..18, 14..18) — includes the observer's tile at (16,16) + zone_map.set_rect(14, 14, 4, 4, 0, 42); + world.insert_resource(zone_map); + + world.spawn(( + PlayerCharacter, + TilePosition::new(16, 16, 0), + Facing::default(), + KnowledgeGraph::new(), + NearbyInteractionBuffer::default(), + MonologueBuffer::default(), + )); + + run_observer_pipeline(&mut world); + + let buffer = world.resource::(); + let snapshot = buffer.snapshot.as_ref().unwrap(); + + // Observer's tile (16, 16) is inside zone 42 + let observer_tile = snapshot + .visible_tiles + .iter() + .find(|t| t.x == 16 && t.y == 16 && t.z == 0) + .expect("observer tile should be visible"); + assert_eq!( + observer_tile.zone_id, + Some(42), + "tile inside zone should have zone_id" + ); +} + +#[test] +fn zone_map_tiles_outside_zone_get_none() { + // Tile outside any zone should get zone_id = None + use crate::simulation::zone::ZoneMap; + + let mut world = setup_world(32, 32); + let mut zone_map = ZoneMap::default(); + // Zone only covers (0..2, 0..2) — far from observer at (16,16) + zone_map.set_rect(0, 0, 2, 2, 0, 7); + world.insert_resource(zone_map); + + world.spawn(( + PlayerCharacter, + TilePosition::new(16, 16, 0), + Facing::default(), + KnowledgeGraph::new(), + NearbyInteractionBuffer::default(), + MonologueBuffer::default(), + )); + + run_observer_pipeline(&mut world); + + let buffer = world.resource::(); + let snapshot = buffer.snapshot.as_ref().unwrap(); + + // Observer's tile (16, 16) is outside any zone + let observer_tile = snapshot + .visible_tiles + .iter() + .find(|t| t.x == 16 && t.y == 16 && t.z == 0) + .expect("observer tile should be visible"); + assert_eq!( + observer_tile.zone_id, None, + "tile outside any zone should have zone_id = None" + ); +} + +#[test] +fn no_zone_map_resource_tiles_have_no_zone_id() { + // When ZoneMap resource is absent, all tiles should have zone_id = None + let mut world = setup_world(32, 32); + // Do NOT insert ZoneMap resource + + world.spawn(( + PlayerCharacter, + TilePosition::new(16, 16, 0), + Facing::default(), + KnowledgeGraph::new(), + NearbyInteractionBuffer::default(), + MonologueBuffer::default(), + )); + + run_observer_pipeline(&mut world); + + let buffer = world.resource::(); + let snapshot = buffer.snapshot.as_ref().unwrap(); + + assert!( + !snapshot.visible_tiles.is_empty(), + "should have visible tiles" + ); + // All tiles should have zone_id = None when no ZoneMap exists + for tile in &snapshot.visible_tiles { + assert_eq!( + tile.zone_id, None, + "tile ({},{}) should have zone_id = None without ZoneMap resource", + tile.x, tile.y + ); + } +} diff --git a/server/src/simulation/zone.rs b/server/src/simulation/zone.rs index baccf096a..ebefd79ef 100644 --- a/server/src/simulation/zone.rs +++ b/server/src/simulation/zone.rs @@ -18,6 +18,9 @@ use bevy_ecs::prelude::*; /// /// Zone IDs are opaque u16 values — the client maintains its own /// `zone_id → zone_name / temperature_tint / ambient_layer` mapping. +/// +/// Production population path is TBD — currently populated only by +/// Gauntlet room builders via `set_rect` / `set`. #[derive(Resource, Debug, Default)] pub struct ZoneMap { zones: BTreeMap<(i32, i32, i32), u16>, diff --git a/server/src/test_world/rooms/decay_observatory.rs b/server/src/test_world/rooms/decay_observatory.rs index 7daef4412..152a88e50 100644 --- a/server/src/test_world/rooms/decay_observatory.rs +++ b/server/src/test_world/rooms/decay_observatory.rs @@ -1,8 +1,9 @@ //! Decay Observatory — Room 13 (24x14) //! -//! Tests D-041 (knowledge graph decay). Observer starts with Direct confidence -//! on an NPC in LOS, then LOS is broken. Tick advances verify confidence -//! degrades: Direct → KnowsDetails → KnowsOf → Suspects → Stale (D-041). +//! Tests D-041 (knowledge graph decay). Observer sees an NPC (Direct), then +//! LOS is broken (drops to KnowsDetails). Tick advances verify confidence +//! degrades: KnowsDetails → KnowsOf → Suspects (floor). Separate test +//! confirms Stale state triggers when age exceeds stale_after threshold. //! //! Decay runs once per game-minute (every 10 ticks, D-031). Confidence floor //! is Suspects — decay never removes an entity from the knowledge graph.