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:
2026-02-18 12:58:14 +01:00
co-authored by Claude Opus 4.6
parent b4a3784218
commit 273d29f26f
18 changed files with 1219 additions and 50 deletions
+34 -3
View File
@@ -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)
+77 -17
View File
@@ -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));
}
}
+4
View File
@@ -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;