Files
settled-reach/server/src/test_world/rooms/shift_change.rs
T
jpmschweitzerandClaude Opus 4.6 28e7e6d9ad feat(simulation): add NPC name masking with role labels and color index
Server-side infrastructure for information asymmetry in dialogue.
NPCs display role labels (Worker, Supervisor, Patron) instead of
real names until the player explicitly learns them via KG lookup.
NpcColorIndex (stable_id % 8) persists across name reveal.

ConversationEvent and DialogueResponseEvent carry display names +
color indices on the wire with serde(default) for compat.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:42:59 +01:00

203 lines
7.8 KiB
Rust

//! 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::<PreviousDayPhase>();
// 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::<SimulationTime>().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::<PathRequest>(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::<PathRequest>(npc_afternoon).is_none(),
"npc_shift_afternoon already at Afternoon location — must NOT receive PathRequest"
);
}
}