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>
203 lines
7.8 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|