feat(simulation): sprint 10 — hub teleport, confrontation response, walk-away phase 2, gauntlet rooms 4-7, blocked_entities debug field
#491: PlayerAction::TeleportToHub — moves player to hub spawn, clears dialogue/monologue/interaction buffer, Gauntlet-only with log warning. #520: ConfrontationDelivered event — Tier 2 animation shift, relationship state decrement (D-033), monologue spike emission. #519: Walk-away Phase 2 — NPC animation shift + routine deviation on dialogue exit (D-064). #498: Four new Gauntlet rooms — Interaction Gallery, Fog Theater, Crowd Plaza, Dialogue Room with constants and wiring. #514: blocked_entities Vec<u64> on ObserverSnapshot, protocol v9. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -189,16 +189,19 @@ pub const INTERACTION_GALLERY_STABLE_IDS: (u64, u64) = (24, 28);
|
||||
pub const PAUSE_CHAMBER_STABLE_IDS: (u64, u64) = (29, 29);
|
||||
pub const DIALOGUE_ROOM_STABLE_IDS: (u64, u64) = (30, 33);
|
||||
pub const CROWD_PLAZA_STABLE_IDS: (u64, u64) = (34, 48);
|
||||
pub const RESET_PLATE_STABLE_IDS: (u64, u64) = (49, 51);
|
||||
pub const RESET_PLATE_STABLE_IDS: (u64, u64) = (49, 55);
|
||||
|
||||
/// Number of actively-spawned entities in the current Gauntlet build.
|
||||
/// Derived from StableId ranges of built rooms + player + reset plates.
|
||||
/// Reserved (unbuilt) rooms do not contribute entities.
|
||||
/// Derived from StableId ranges of all rooms + player + reset plates.
|
||||
pub const EXPECTED_ENTITY_COUNT: usize = 1 // player (StableId 0)
|
||||
+ (HUB_STABLE_IDS.1 - HUB_STABLE_IDS.0 + 1) as usize
|
||||
+ (FOG_THEATER_STABLE_IDS.1 - FOG_THEATER_STABLE_IDS.0 + 1) as usize
|
||||
+ (OCCLUSION_STABLE_IDS.1 - OCCLUSION_STABLE_IDS.0 + 1) as usize
|
||||
+ (INVENTORY_STABLE_IDS.1 - INVENTORY_STABLE_IDS.0 + 1) as usize
|
||||
+ (INTERACTION_GALLERY_STABLE_IDS.1 - INTERACTION_GALLERY_STABLE_IDS.0 + 1) as usize
|
||||
+ (PAUSE_CHAMBER_STABLE_IDS.1 - PAUSE_CHAMBER_STABLE_IDS.0 + 1) as usize
|
||||
+ (DIALOGUE_ROOM_STABLE_IDS.1 - DIALOGUE_ROOM_STABLE_IDS.0 + 1) as usize
|
||||
+ (CROWD_PLAZA_STABLE_IDS.1 - CROWD_PLAZA_STABLE_IDS.0 + 1) as usize
|
||||
+ (RESET_PLATE_STABLE_IDS.1 - RESET_PLATE_STABLE_IDS.0 + 1) as usize;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -233,6 +236,34 @@ mod tests {
|
||||
assert_eq!(room.name, "pause_chamber");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn room_at_finds_fog_theater() {
|
||||
let pos = TilePosition { x: 56, y: 18, z: 0 };
|
||||
let room = room_at(&pos).expect("Fog Theater observer should be in a room");
|
||||
assert_eq!(room.name, "fog_theater");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn room_at_finds_interaction_gallery() {
|
||||
let pos = TilePosition { x: 14, y: 92, z: 0 };
|
||||
let room = room_at(&pos).expect("Interaction Gallery observer should be in a room");
|
||||
assert_eq!(room.name, "interaction_gallery");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn room_at_finds_dialogue_room() {
|
||||
let pos = TilePosition { x: 50, y: 114, z: 0 };
|
||||
let room = room_at(&pos).expect("Dialogue Room observer should be in a room");
|
||||
assert_eq!(room.name, "dialogue_room");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn room_at_finds_crowd_plaza() {
|
||||
let pos = TilePosition { x: 96, y: 94, z: 0 };
|
||||
let room = room_at(&pos).expect("Crowd Plaza observer should be in a room");
|
||||
assert_eq!(room.name, "crowd_plaza");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn room_at_returns_none_for_corridor() {
|
||||
// Point inside corridor-E (between Hub and Occlusion)
|
||||
|
||||
@@ -19,14 +19,14 @@
|
||||
//! StableId ranges (from gestalt-round3.md):
|
||||
//! Player: 0
|
||||
//! Hub signs: 1-4
|
||||
//! Fog Theater: 5-8 (reserved, not yet built)
|
||||
//! Fog Theater: 5-8
|
||||
//! Occlusion Corridor: 9-12
|
||||
//! Inventory Warehouse: 13-23
|
||||
//! Interaction Gallery: 24-28 (reserved, not yet built)
|
||||
//! Interaction Gallery: 24-28
|
||||
//! Pause Chamber: 29
|
||||
//! Dialogue Room: 30-33 (reserved, not yet built)
|
||||
//! Crowd Plaza: 34-48 (reserved, not yet built)
|
||||
//! Reset plates: 49-51 (Occlusion, Inventory, Pause)
|
||||
//! Dialogue Room: 30-33
|
||||
//! Crowd Plaza: 34-48
|
||||
//! Reset plates: 49-55
|
||||
|
||||
#[cfg(feature = "gauntlet")]
|
||||
pub mod constants;
|
||||
@@ -86,14 +86,22 @@ pub fn setup_gauntlet(app: &mut App) {
|
||||
|
||||
// Carve room interiors (2-tile-thick walls → interior starts 2 tiles in)
|
||||
carve_room_interior(&mut walkability, 38, 46, 24, 24); // Hub
|
||||
carve_room_interior(&mut walkability, 28, 2, 44, 32); // Fog Theater
|
||||
carve_room_interior(&mut walkability, 74, 48, 42, 22); // Occlusion Corridor
|
||||
carve_room_interior(&mut walkability, 2, 40, 30, 28); // Inventory Warehouse
|
||||
carve_room_interior(&mut walkability, 2, 82, 24, 20); // Interaction Gallery
|
||||
carve_room_interior(&mut walkability, 42, 78, 16, 16); // Pause Chamber
|
||||
carve_room_interior(&mut walkability, 36, 104, 28, 20); // Dialogue Room
|
||||
carve_room_interior(&mut walkability, 80, 78, 32, 32); // Crowd Plaza
|
||||
|
||||
// Carve corridors between hub and rooms
|
||||
carve_corridor(&mut walkability, 48, 34, 6, 12); // corridor-N: Hub ↔ Fog Theater
|
||||
carve_corridor(&mut walkability, 62, 55, 12, 6); // corridor-E: Hub ↔ Occlusion
|
||||
carve_corridor(&mut walkability, 32, 55, 6, 6); // corridor-W: Hub ↔ Inventory
|
||||
carve_corridor(&mut walkability, 47, 70, 6, 8); // corridor-S: Hub ↔ Pause Chamber
|
||||
carve_corridor(&mut walkability, 12, 68, 6, 14); // corridor-SW: Inventory ↔ Interaction Gallery
|
||||
carve_corridor(&mut walkability, 48, 94, 6, 10); // corridor-S2: Pause ↔ Dialogue Room
|
||||
carve_corridor(&mut walkability, 58, 84, 22, 6); // corridor-E2: Pause ↔ Crowd Plaza
|
||||
|
||||
// Set up Occlusion Corridor walls (relative positions converted to absolute)
|
||||
// North wall segment: rel x=[14,22], y=[6,7] — blocks LOS to npc_hidden_wall
|
||||
@@ -142,8 +150,8 @@ pub fn setup_gauntlet(app: &mut App) {
|
||||
// --- Hub signs (StableId 1-4) ---
|
||||
rooms::hub::spawn_entities(app, &mut registry);
|
||||
|
||||
// --- Fog Theater (StableId 5-8) — reserved, not yet built ---
|
||||
registry.reserve_up_to(9);
|
||||
// --- Fog Theater (StableId 5-8) ---
|
||||
rooms::fog_theater::spawn_entities(app, &mut registry);
|
||||
|
||||
// --- Occlusion Corridor (StableId 9-12) ---
|
||||
rooms::occlusion_corridor::spawn_entities(app, &mut registry);
|
||||
@@ -151,23 +159,29 @@ pub fn setup_gauntlet(app: &mut App) {
|
||||
// --- Inventory Warehouse (StableId 13-23) ---
|
||||
rooms::inventory_warehouse::spawn_entities(app, &mut registry);
|
||||
|
||||
// --- Interaction Gallery (StableId 24-28) — reserved, not yet built ---
|
||||
registry.reserve_up_to(29);
|
||||
// --- Interaction Gallery (StableId 24-28) ---
|
||||
rooms::interaction_gallery::spawn_entities(app, &mut registry);
|
||||
|
||||
// --- Pause Chamber (StableId 29) ---
|
||||
rooms::pause_chamber::spawn_entities(app, &mut registry);
|
||||
|
||||
// --- Dialogue Room (StableId 30-33) — reserved, not yet built ---
|
||||
// --- Crowd Plaza (StableId 34-48) — reserved, not yet built ---
|
||||
registry.reserve_up_to(49);
|
||||
// --- Dialogue Room (StableId 30-33) ---
|
||||
rooms::dialogue_room::spawn_entities(app, &mut registry);
|
||||
|
||||
// --- Reset plates (StableId 49-51) ---
|
||||
// --- Crowd Plaza (StableId 34-48) ---
|
||||
rooms::crowd_plaza::spawn_entities(app, &mut registry);
|
||||
|
||||
// --- Reset plates (StableId 49-55) ---
|
||||
// Spawned at corridor entrances per workshop-outcomes.md Section 8.
|
||||
// Each plate triggers reset of its associated room.
|
||||
let reset_plates: &[(&str, TilePosition)] = &[
|
||||
("occlusion_corridor", constants::OCCLUSION_CORRIDOR.reset_plate.expect("occlusion_corridor should have a reset_plate")),
|
||||
("inventory_warehouse", constants::INVENTORY_WAREHOUSE.reset_plate.expect("inventory_warehouse should have a reset_plate")),
|
||||
("pause_chamber", constants::PAUSE_CHAMBER.reset_plate.expect("pause_chamber should have a reset_plate")),
|
||||
("fog_theater", constants::FOG_THEATER.reset_plate.expect("fog_theater should have a reset_plate")),
|
||||
("interaction_gallery", constants::INTERACTION_GALLERY.reset_plate.expect("interaction_gallery should have a reset_plate")),
|
||||
("dialogue_room", constants::DIALOGUE_ROOM.reset_plate.expect("dialogue_room should have a reset_plate")),
|
||||
("crowd_plaza", constants::CROWD_PLAZA.reset_plate.expect("crowd_plaza should have a reset_plate")),
|
||||
];
|
||||
for &(room_name, pos) in reset_plates {
|
||||
let entity = app
|
||||
@@ -189,6 +203,15 @@ pub fn setup_gauntlet(app: &mut App) {
|
||||
// --- Populate RoomSnapshots for reset mechanism (#490) ---
|
||||
let mut snapshots = RoomSnapshots::default();
|
||||
|
||||
// Fog Theater entities (StableId 5-8): NPCs only
|
||||
for id in constants::FOG_THEATER_STABLE_IDS.0..=constants::FOG_THEATER_STABLE_IDS.1 {
|
||||
if let Some(entity) = registry.to_entity(&StableId(id)) {
|
||||
if let Some(pos) = app.world().get::<TilePosition>(entity) {
|
||||
snapshots.record("fog_theater", entity, *pos, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Occlusion Corridor entities (StableId 9-12): NPCs only, no floor items
|
||||
for id in 9..=12 {
|
||||
if let Some(entity) = registry.to_entity(&StableId(id)) {
|
||||
@@ -208,6 +231,15 @@ pub fn setup_gauntlet(app: &mut App) {
|
||||
}
|
||||
}
|
||||
|
||||
// Interaction Gallery entities (StableId 24-28): objects only, no floor items
|
||||
for id in constants::INTERACTION_GALLERY_STABLE_IDS.0..=constants::INTERACTION_GALLERY_STABLE_IDS.1 {
|
||||
if let Some(entity) = registry.to_entity(&StableId(id)) {
|
||||
if let Some(pos) = app.world().get::<TilePosition>(entity) {
|
||||
snapshots.record("interaction_gallery", entity, *pos, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pause Chamber entity (StableId 29): NPC only
|
||||
if let Some(entity) = registry.to_entity(&StableId(29)) {
|
||||
if let Some(pos) = app.world().get::<TilePosition>(entity) {
|
||||
@@ -215,6 +247,24 @@ pub fn setup_gauntlet(app: &mut App) {
|
||||
}
|
||||
}
|
||||
|
||||
// Dialogue Room entities (StableId 30-33): NPCs only
|
||||
for id in constants::DIALOGUE_ROOM_STABLE_IDS.0..=constants::DIALOGUE_ROOM_STABLE_IDS.1 {
|
||||
if let Some(entity) = registry.to_entity(&StableId(id)) {
|
||||
if let Some(pos) = app.world().get::<TilePosition>(entity) {
|
||||
snapshots.record("dialogue_room", entity, *pos, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Crowd Plaza entities (StableId 34-48): NPCs only
|
||||
for id in constants::CROWD_PLAZA_STABLE_IDS.0..=constants::CROWD_PLAZA_STABLE_IDS.1 {
|
||||
if let Some(entity) = registry.to_entity(&StableId(id)) {
|
||||
if let Some(pos) = app.world().get::<TilePosition>(entity) {
|
||||
snapshots.record("crowd_plaza", entity, *pos, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.insert_resource(snapshots);
|
||||
app.insert_resource(registry);
|
||||
}
|
||||
@@ -331,9 +381,9 @@ mod tests {
|
||||
assert!(registry.to_entity(&StableId(id)).is_some(), "Hub sign at StableId {}", id);
|
||||
}
|
||||
|
||||
// Fog Theater 5-8 reserved (no entities)
|
||||
// Fog Theater at 5-8
|
||||
for id in 5..=8 {
|
||||
assert!(registry.to_entity(&StableId(id)).is_none(), "Fog Theater {} reserved", id);
|
||||
assert!(registry.to_entity(&StableId(id)).is_some(), "Fog Theater at StableId {}", id);
|
||||
}
|
||||
|
||||
// Occlusion Corridor at 9-12
|
||||
@@ -346,14 +396,24 @@ mod tests {
|
||||
assert!(registry.to_entity(&StableId(id)).is_some(), "Inventory at StableId {}", id);
|
||||
}
|
||||
|
||||
// Interaction Gallery 24-28 reserved
|
||||
// Interaction Gallery at 24-28
|
||||
for id in 24..=28 {
|
||||
assert!(registry.to_entity(&StableId(id)).is_none(), "Gallery {} reserved", id);
|
||||
assert!(registry.to_entity(&StableId(id)).is_some(), "Gallery at StableId {}", id);
|
||||
}
|
||||
|
||||
// Pause Chamber at 29
|
||||
assert!(registry.to_entity(&StableId(29)).is_some(), "Pause Chamber at StableId 29");
|
||||
|
||||
// Dialogue Room at 30-33
|
||||
for id in 30..=33 {
|
||||
assert!(registry.to_entity(&StableId(id)).is_some(), "Dialogue Room at StableId {}", id);
|
||||
}
|
||||
|
||||
// Crowd Plaza at 34-48
|
||||
for id in 34..=48 {
|
||||
assert!(registry.to_entity(&StableId(id)).is_some(), "Crowd Plaza at StableId {}", id);
|
||||
}
|
||||
|
||||
// Reset plates at 49-51
|
||||
for id in constants::RESET_PLATE_STABLE_IDS.0..=constants::RESET_PLATE_STABLE_IDS.1 {
|
||||
assert!(
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
//! Crowd Plaza — Room 7 (32x32)
|
||||
//!
|
||||
//! Density stress test room. 15 NPCs in a 5x3 grid to test perception,
|
||||
//! snapshot, and tick-budget performance under high entity density.
|
||||
//! Validates D-026 tick budget holds with many visible entities.
|
||||
//!
|
||||
//! Observer position: (16, 16) relative = (96, 94) absolute, facing West.
|
||||
//!
|
||||
//! Entities (StableId 34-48):
|
||||
//! crowd_npc_00..crowd_npc_14 — 15 NPCs in a 5-column x 3-row grid
|
||||
//! Grid starts at rel (4, 4) = abs (84, 82), spacing: 5x cols, 8y rows.
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
|
||||
use crate::knowledge::registry::{EntityRegistry, StableEntityId};
|
||||
use crate::npc::{Contentment, Npc, ToleranceThreshold, Want, WantKind};
|
||||
use crate::simulation::interaction::Interactable;
|
||||
use crate::simulation::movement::TilePosition;
|
||||
use crate::simulation::path_follow::MovementSpeed;
|
||||
|
||||
/// Room origin (top-left corner including walls).
|
||||
const ORIGIN_X: i32 = 80;
|
||||
const ORIGIN_Y: i32 = 78;
|
||||
|
||||
/// Grid layout constants.
|
||||
const GRID_COLS: usize = 5;
|
||||
const GRID_ROWS: usize = 3;
|
||||
const GRID_START_X: i32 = 4;
|
||||
const GRID_START_Y: i32 = 4;
|
||||
const GRID_SPACING_X: i32 = 5;
|
||||
const GRID_SPACING_Y: i32 = 8;
|
||||
|
||||
/// WantKind cycle for variety across 15 NPCs.
|
||||
const WANT_CYCLE: &[WantKind] = &[
|
||||
WantKind::Wealth,
|
||||
WantKind::Safety,
|
||||
WantKind::Knowledge,
|
||||
WantKind::Connection,
|
||||
WantKind::Power,
|
||||
WantKind::Freedom,
|
||||
WantKind::Justice,
|
||||
WantKind::Revenge,
|
||||
WantKind::Happiness,
|
||||
];
|
||||
|
||||
/// Spawn Crowd Plaza entities in canonical order (StableId 34-48).
|
||||
pub fn spawn_entities(app: &mut App, registry: &mut EntityRegistry) {
|
||||
let mut index = 0usize;
|
||||
for row in 0..GRID_ROWS {
|
||||
for col in 0..GRID_COLS {
|
||||
let rx = GRID_START_X + col as i32 * GRID_SPACING_X;
|
||||
let ry = GRID_START_Y + row as i32 * GRID_SPACING_Y;
|
||||
let pos = TilePosition::new(ORIGIN_X + rx, ORIGIN_Y + ry, 0);
|
||||
let want_kind = WANT_CYCLE[index % WANT_CYCLE.len()];
|
||||
|
||||
let entity = app
|
||||
.world_mut()
|
||||
.spawn((
|
||||
Npc,
|
||||
Interactable,
|
||||
pos,
|
||||
Want {
|
||||
primary: want_kind,
|
||||
intensity: ((index % 8) + 2) as u8, // 2-9 range
|
||||
description: format!("Crowd Plaza NPC #{:02}", index),
|
||||
},
|
||||
Contentment {
|
||||
level: (index as i16 * 7 - 50).clamp(-100, 100),
|
||||
},
|
||||
ToleranceThreshold {
|
||||
current_stress: (index as i16 * 5) % 80,
|
||||
threshold: 40 + (index as i16 % 4) * 10,
|
||||
},
|
||||
MovementSpeed::default(),
|
||||
))
|
||||
.id();
|
||||
let sid = registry.register(entity);
|
||||
app.world_mut()
|
||||
.entity_mut(entity)
|
||||
.insert(StableEntityId(sid));
|
||||
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
//! Dialogue Room — Room 6 (28x20)
|
||||
//!
|
||||
//! Tests D-041 (knowledge graph), D-028 (dialogue filtering pipeline).
|
||||
//!
|
||||
//! Layout: Four NPCs with DialogueProfile components, enabling the full
|
||||
//! dialogue selection pipeline (access tier, situation, trust, mood scoring).
|
||||
//! NPCs have varying trust/contentment to test different dialogue branches.
|
||||
//!
|
||||
//! Observer position: (14, 10) relative = (50, 114) absolute, facing North.
|
||||
//!
|
||||
//! Entities (StableId 30-33):
|
||||
//! npc_dialogue_a (44, 112) — Relaxed dock worker
|
||||
//! npc_dialogue_b (50, 110) — Guarded technician
|
||||
//! npc_dialogue_c (56, 112) — Stressed supervisor
|
||||
//! npc_dialogue_d (50, 118) — Distant observer (range test)
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
|
||||
use crate::knowledge::registry::{EntityRegistry, StableEntityId};
|
||||
use crate::npc::{Contentment, Npc, ToleranceThreshold, Want, WantKind};
|
||||
use crate::simulation::dialogue::{CurrentMood, DialogueProfile};
|
||||
use crate::simulation::interaction::Interactable;
|
||||
use crate::simulation::movement::TilePosition;
|
||||
use crate::simulation::path_follow::MovementSpeed;
|
||||
|
||||
/// Room origin (top-left corner including walls).
|
||||
const ORIGIN_X: i32 = 36;
|
||||
const ORIGIN_Y: i32 = 104;
|
||||
|
||||
/// NPC definitions: (name, rel_x, rel_y, want_kind, intensity, contentment, stress, threshold, location, role).
|
||||
const NPCS: &[(&str, i32, i32, WantKind, u8, i16, i16, i16, &str, &str)] = &[
|
||||
(
|
||||
"npc_dialogue_a", 8, 8, WantKind::Connection, 4, 20, 10, 60,
|
||||
"the-terminal", "dock-worker",
|
||||
),
|
||||
(
|
||||
"npc_dialogue_b", 14, 6, WantKind::Safety, 6, 0, 25, 45,
|
||||
"the-terminal", "technician",
|
||||
),
|
||||
(
|
||||
"npc_dialogue_c", 20, 8, WantKind::Power, 7, -15, 40, 50,
|
||||
"the-terminal", "supervisor",
|
||||
),
|
||||
(
|
||||
"npc_dialogue_d", 14, 14, WantKind::Knowledge, 3, 10, 5, 70,
|
||||
"the-terminal", "observer",
|
||||
),
|
||||
];
|
||||
|
||||
/// Spawn Dialogue Room entities in canonical order (StableId 30-33).
|
||||
pub fn spawn_entities(app: &mut App, registry: &mut EntityRegistry) {
|
||||
for &(name, rx, ry, want_kind, intensity, contentment, stress, threshold, location, role) in
|
||||
NPCS
|
||||
{
|
||||
let pos = TilePosition::new(ORIGIN_X + rx, ORIGIN_Y + ry, 0);
|
||||
let entity = app
|
||||
.world_mut()
|
||||
.spawn((
|
||||
Npc,
|
||||
Interactable,
|
||||
pos,
|
||||
Want {
|
||||
primary: want_kind,
|
||||
intensity,
|
||||
description: format!("Dialogue Room test NPC: {}", name),
|
||||
},
|
||||
Contentment {
|
||||
level: contentment,
|
||||
},
|
||||
ToleranceThreshold {
|
||||
current_stress: stress,
|
||||
threshold,
|
||||
},
|
||||
MovementSpeed::default(),
|
||||
DialogueProfile {
|
||||
location: location.to_string(),
|
||||
role: role.to_string(),
|
||||
},
|
||||
CurrentMood::default(),
|
||||
))
|
||||
.id();
|
||||
let sid = registry.register(entity);
|
||||
app.world_mut()
|
||||
.entity_mut(entity)
|
||||
.insert(StableEntityId(sid));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//! Fog Theater — Room 1 (44x32)
|
||||
//!
|
||||
//! Tests D-059 (fog layers), D-060 (cognitive delay for fog recognition).
|
||||
//!
|
||||
//! Layout: Large open room with NPCs at varying distances from observer.
|
||||
//! Tests visibility at clear, peripheral, deep-fog, and edge-of-range
|
||||
//! distances. No internal walls — fog layers are distance-based, not
|
||||
//! occlusion-based (that's the Occlusion Corridor's job).
|
||||
//!
|
||||
//! Observer position: (28, 16) relative = (56, 18) absolute, facing South.
|
||||
//!
|
||||
//! Entities (StableId 5-8):
|
||||
//! npc_fog_clear (56, 22) — 4 tiles south, clear vision cone
|
||||
//! npc_fog_peripheral (46, 18) — 10 tiles west, peripheral sector
|
||||
//! npc_fog_deep (34, 10) — far NW corner, deep fog range
|
||||
//! npc_fog_edge (68, 28) — far SE, edge-of-range test
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
|
||||
use crate::knowledge::registry::{EntityRegistry, StableEntityId};
|
||||
use crate::npc::{Contentment, Npc, ToleranceThreshold, Want, WantKind};
|
||||
use crate::simulation::interaction::Interactable;
|
||||
use crate::simulation::movement::TilePosition;
|
||||
use crate::simulation::path_follow::MovementSpeed;
|
||||
|
||||
/// Room origin (top-left corner including walls).
|
||||
const ORIGIN_X: i32 = 28;
|
||||
const ORIGIN_Y: i32 = 2;
|
||||
|
||||
/// NPC definitions: (name, relative_x, relative_y, want_kind, want_intensity).
|
||||
const NPCS: &[(&str, i32, i32, WantKind, u8)] = &[
|
||||
("npc_fog_clear", 28, 20, WantKind::Safety, 5),
|
||||
("npc_fog_peripheral", 18, 16, WantKind::Knowledge, 6),
|
||||
("npc_fog_deep", 6, 8, WantKind::Freedom, 3),
|
||||
("npc_fog_edge", 40, 26, WantKind::Wealth, 4),
|
||||
];
|
||||
|
||||
/// Spawn Fog Theater entities in canonical order (StableId 5-8).
|
||||
pub fn spawn_entities(app: &mut App, registry: &mut EntityRegistry) {
|
||||
for &(name, rx, ry, want_kind, intensity) in NPCS {
|
||||
let pos = TilePosition::new(ORIGIN_X + rx, ORIGIN_Y + ry, 0);
|
||||
let entity = app
|
||||
.world_mut()
|
||||
.spawn((
|
||||
Npc,
|
||||
Interactable,
|
||||
pos,
|
||||
Want {
|
||||
primary: want_kind,
|
||||
intensity,
|
||||
description: format!("Fog Theater test NPC: {}", name),
|
||||
},
|
||||
Contentment { level: 0 },
|
||||
ToleranceThreshold {
|
||||
current_stress: 0,
|
||||
threshold: 50,
|
||||
},
|
||||
MovementSpeed::default(),
|
||||
))
|
||||
.id();
|
||||
let sid = registry.register(entity);
|
||||
app.world_mut()
|
||||
.entity_mut(entity)
|
||||
.insert(StableEntityId(sid));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
//! Interaction Gallery — Room 4 (24x20)
|
||||
//!
|
||||
//! Tests D-057 (entity interaction vertical list, verb generation per type).
|
||||
//!
|
||||
//! Layout: One entity of each ObjectType to test that each type generates
|
||||
//! the correct interaction verb(s). Container is already tested in
|
||||
//! Inventory Warehouse, so this room covers the remaining 5 types.
|
||||
//!
|
||||
//! Observer position: (12, 10) relative = (14, 92) absolute, facing East.
|
||||
//!
|
||||
//! Entities (StableId 24-28):
|
||||
//! obj_notice (8, 88) — Readable (Read verb)
|
||||
//! obj_terminal (8, 94) — Terminal (Use verb)
|
||||
//! obj_hatch (20, 88) — Door (Open/Close verb)
|
||||
//! obj_pickup (20, 94) — Pickup (Pick up verb)
|
||||
//! obj_bench (14, 96) — Furniture (Sit verb)
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
|
||||
use crate::bridge::types::ObjectType;
|
||||
use crate::knowledge::registry::{EntityRegistry, StableEntityId};
|
||||
use crate::simulation::interaction::Interactable;
|
||||
use crate::simulation::movement::TilePosition;
|
||||
|
||||
/// Room origin (top-left corner including walls).
|
||||
const ORIGIN_X: i32 = 2;
|
||||
const ORIGIN_Y: i32 = 82;
|
||||
|
||||
/// Object definitions: (name, relative_x, relative_y, object_type).
|
||||
const OBJECTS: &[(&str, i32, i32, ObjectType)] = &[
|
||||
("obj_notice", 6, 6, ObjectType::Readable),
|
||||
("obj_terminal", 6, 12, ObjectType::Terminal),
|
||||
("obj_hatch", 18, 6, ObjectType::Door),
|
||||
("obj_pickup", 18, 12, ObjectType::Pickup),
|
||||
("obj_bench", 12, 14, ObjectType::Furniture),
|
||||
];
|
||||
|
||||
/// Spawn Interaction Gallery entities in canonical order (StableId 24-28).
|
||||
pub fn spawn_entities(app: &mut App, registry: &mut EntityRegistry) {
|
||||
for &(_name, rx, ry, obj_type) in OBJECTS {
|
||||
let pos = TilePosition::new(ORIGIN_X + rx, ORIGIN_Y + ry, 0);
|
||||
let entity = app
|
||||
.world_mut()
|
||||
.spawn((Interactable, obj_type, pos))
|
||||
.id();
|
||||
let sid = registry.register(entity);
|
||||
app.world_mut()
|
||||
.entity_mut(entity)
|
||||
.insert(StableEntityId(sid));
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,11 @@
|
||||
//! Each room module exports a `spawn_entities()` function that creates
|
||||
//! entities in canonical order for deterministic StableId assignment.
|
||||
|
||||
pub mod crowd_plaza;
|
||||
pub mod dialogue_room;
|
||||
pub mod fog_theater;
|
||||
pub mod hub;
|
||||
pub mod interaction_gallery;
|
||||
pub mod inventory_warehouse;
|
||||
pub mod occlusion_corridor;
|
||||
pub mod pause_chamber;
|
||||
|
||||
Reference in New Issue
Block a user