//! Shift Change — Room 14 (16x24) //! //! Tests D-031 (day phases) and NPC routine transitions at phase boundaries. //! Two NPCs have DailyRoutine components with entries for Morning and Afternoon. //! Advancing game clock to the Afternoon boundary triggers PathRequests for //! both NPCs (they must move to their Afternoon locations). //! //! This room sits adjacent to the corridor-E2 path (x=58-80, y=84-90), which //! provides natural east-west access through the room interior. //! //! Observer position: (72, 90) absolute, facing North. //! //! Entities (StableId 70-71): //! npc_shift_morning (70, 84) — Morning location: 6 tiles north-west of observer //! npc_shift_afternoon (76, 96) — Afternoon location: 4 tiles east, 6 south of observer use bevy_app::prelude::*; use crate::knowledge::registry::{EntityRegistry, StableEntityId}; 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; use crate::simulation::tier::ActiveSim; use crate::simulation::time::DayPhase; /// Observer position for Shift Change tests (absolute). pub const OBSERVER_POS: TilePosition = TilePosition { x: 72, y: 90, z: 0 }; /// Morning routine location for both NPCs — where they stand during Morning phase. pub const MORNING_LOCATION: TilePosition = TilePosition { x: 70, y: 84, z: 0 }; /// Afternoon routine location — where NPCs should walk to at Afternoon boundary. pub const AFTERNOON_LOCATION: TilePosition = TilePosition { x: 76, y: 96, z: 0 }; /// Spawn Shift Change entities in canonical order (StableId 70-71). pub fn spawn_entities(app: &mut App, registry: &mut EntityRegistry) { // npc_shift_morning (StableId 70): starts at Morning location, has both Morning + Afternoon entries. let entity = app .world_mut() .spawn(( Npc, ActiveSim, Interactable, MORNING_LOCATION, Want { primary: WantKind::Safety, intensity: 4, description: "Shift Change: morning shift NPC".to_string(), }, Contentment { level: 0 }, ToleranceThreshold { current_stress: 5, threshold: 45, }, MovementSpeed::default(), DailyRoutine { entries: vec![ RoutineEntry { phase: DayPhase::Morning, location: MORNING_LOCATION, activity: "Morning station".to_string(), }, RoutineEntry { phase: DayPhase::Afternoon, location: AFTERNOON_LOCATION, activity: "Afternoon patrol".to_string(), }, ], description: "Shifts between morning station and afternoon patrol".to_string(), }, )) .id(); let sid = registry.register(entity); app.world_mut() .entity_mut(entity) .insert(StableEntityId(sid)); // npc_shift_afternoon (StableId 71): starts at Afternoon location, same routine. // Starting position differs from Morning location so a transition always triggers a PathRequest. let entity = app .world_mut() .spawn(( Npc, ActiveSim, Interactable, AFTERNOON_LOCATION, Want { primary: WantKind::Connection, intensity: 3, description: "Shift Change: afternoon shift NPC".to_string(), }, Contentment { level: 2 }, ToleranceThreshold { current_stress: 10, threshold: 55, }, MovementSpeed::default(), DailyRoutine { entries: vec![ RoutineEntry { phase: DayPhase::Morning, location: MORNING_LOCATION, activity: "Morning briefing".to_string(), }, RoutineEntry { phase: DayPhase::Afternoon, location: AFTERNOON_LOCATION, activity: "Afternoon post".to_string(), }, ], description: "Covers afternoon post, returns to morning briefing".to_string(), }, )) .id(); let sid = registry.register(entity); app.world_mut() .entity_mut(entity) .insert(StableEntityId(sid)); } #[cfg(test)] mod tests { use super::*; use crate::npc::routine::{check_phase_transition, PreviousDayPhase}; use crate::simulation::pathfinding::PathRequest; use crate::simulation::time::{SimulationTime, MINUTES_PER_PHASE, TICKS_PER_GAME_MINUTE}; /// At the Morning→Afternoon boundary, NPCs receive PathRequests for their Afternoon location. #[test] fn shift_change_npc_transitions_routine_at_phase_boundary() { let mut world = bevy_ecs::world::World::new(); world.insert_resource(SimulationTime::default()); world.init_resource::(); // Spawn npc_shift_morning: currently at MORNING_LOCATION. // On Afternoon transition it should receive a PathRequest to AFTERNOON_LOCATION. let npc_morning = world .spawn(( Npc, ActiveSim, MORNING_LOCATION, DailyRoutine { entries: vec![ RoutineEntry { phase: DayPhase::Morning, location: MORNING_LOCATION, activity: "Morning station".to_string(), }, RoutineEntry { phase: DayPhase::Afternoon, location: AFTERNOON_LOCATION, activity: "Afternoon patrol".to_string(), }, ], description: "Test routine".to_string(), }, )) .id(); // Spawn npc_shift_afternoon: currently at AFTERNOON_LOCATION. // Already at Afternoon location — PathRequest should NOT fire for it. let npc_afternoon = world .spawn(( Npc, ActiveSim, AFTERNOON_LOCATION, DailyRoutine { entries: vec![RoutineEntry { phase: DayPhase::Afternoon, location: AFTERNOON_LOCATION, activity: "Afternoon post".to_string(), }], description: "Test routine".to_string(), }, )) .id(); // Advance time to Afternoon boundary (Morning→Afternoon). 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); schedule.run(&mut world); // npc_shift_morning must have received a PathRequest to AFTERNOON_LOCATION. let req = world .get::(npc_morning) .expect("npc_shift_morning must receive PathRequest at Afternoon boundary"); assert_eq!( req.goal, AFTERNOON_LOCATION, "PathRequest goal must be the Afternoon routine location" ); // npc_shift_afternoon is already at AFTERNOON_LOCATION — no PathRequest. assert!( world.get::(npc_afternoon).is_none(), "npc_shift_afternoon already at Afternoon location — must NOT receive PathRequest" ); } }