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
+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!(