feat(simulation): add Zone Gate gauntlet room and zone crossing detection (#512)
New test_world room with two zones (Terminal/Corridor) separated by a door. Adds ZoneCrossEventQueue resource and detect_zone_crossings system to fire events when the player crosses zone boundaries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
//! - Entities spawned in canonical order → StableId assignment is deterministic
|
||||
//! - Additive-only: existing rooms/entities never reordered
|
||||
//!
|
||||
//! StableId ranges (from gestalt-round3.md + Sprint 11 + Sprint 13):
|
||||
//! StableId ranges (from gestalt-round3.md + Sprint 11 + Sprint 13 + Sprint 22):
|
||||
//! Player: 0
|
||||
//! Hub signs: 1-4
|
||||
//! Fog Theater: 5-8
|
||||
@@ -35,6 +35,8 @@
|
||||
//! Decay Observatory: 69
|
||||
//! Shift Change: 70-71
|
||||
//! Reset plates (Sprint 13 rooms): 72-74
|
||||
//! Zone Gate: 75 (door entity)
|
||||
//! Reset plates (Sprint 22 rooms): 76
|
||||
|
||||
#[cfg(feature = "gauntlet")]
|
||||
pub mod constants;
|
||||
@@ -113,6 +115,8 @@ pub fn setup_gauntlet(app: &mut App) {
|
||||
carve_room_interior(&mut walkability, 0, 104, 34, 20); // Sound Lab
|
||||
carve_room_interior(&mut walkability, 0, 24, 24, 14); // Decay Observatory
|
||||
carve_room_interior(&mut walkability, 64, 78, 16, 24); // Shift Change
|
||||
// Sprint 22 rooms
|
||||
carve_room_interior(&mut walkability, 64, 102, 16, 22); // Zone Gate
|
||||
|
||||
// Carve corridors between hub and rooms
|
||||
carve_corridor(&mut walkability, 48, 34, 6, 12); // corridor-N: Hub ↔ Fog Theater
|
||||
@@ -144,8 +148,13 @@ pub fn setup_gauntlet(app: &mut App) {
|
||||
|
||||
// Zone map: assign zone IDs per room bounding box (D-077, D-073).
|
||||
// Zone IDs are sequential per ROOMS order. Corridors remain unzoned (None).
|
||||
// Exception: Zone Gate (index 14) uses two explicit zone IDs for its dual-zone split.
|
||||
let mut zone_map = ZoneMap::default();
|
||||
for (i, room) in constants::ROOMS.iter().enumerate() {
|
||||
// Zone Gate handled separately below — skip in the sequential loop.
|
||||
if room.name == "zone_gate" {
|
||||
continue;
|
||||
}
|
||||
let zone_id = i as u16;
|
||||
zone_map.set_rect(
|
||||
room.origin.x,
|
||||
@@ -156,6 +165,23 @@ pub fn setup_gauntlet(app: &mut App) {
|
||||
zone_id,
|
||||
);
|
||||
}
|
||||
// Zone Gate: Terminal side (left 8 cols, x=64-71) and Corridor side (right 8 cols, x=72-79).
|
||||
zone_map.set_rect(
|
||||
constants::ZONE_GATE.origin.x,
|
||||
constants::ZONE_GATE.origin.y,
|
||||
constants::ZONE_GATE.size.0 / 2,
|
||||
constants::ZONE_GATE.size.1,
|
||||
constants::ZONE_GATE.origin.z,
|
||||
constants::ZONE_GATE_TERMINAL_ZONE_ID,
|
||||
);
|
||||
zone_map.set_rect(
|
||||
constants::ZONE_GATE.origin.x + constants::ZONE_GATE.size.0 / 2,
|
||||
constants::ZONE_GATE.origin.y,
|
||||
constants::ZONE_GATE.size.0 / 2,
|
||||
constants::ZONE_GATE.size.1,
|
||||
constants::ZONE_GATE.origin.z,
|
||||
constants::ZONE_GATE_CORRIDOR_ZONE_ID,
|
||||
);
|
||||
app.insert_resource(zone_map);
|
||||
|
||||
// Entity spawning in canonical StableId order.
|
||||
@@ -333,6 +359,9 @@ pub fn setup_gauntlet(app: &mut App) {
|
||||
}
|
||||
|
||||
// --- Sprint 13 reset plates (StableId 72-74) ---
|
||||
// NOTE: Sprint 22 reset plate (zone_gate) is spawned AFTER these to maintain additive order.
|
||||
// The sprint13 block is numbered 72-74; zone_gate entities are 75; sprint22 plate is 76.
|
||||
// The spawn order below matches the StableId allocation table in the module doc.
|
||||
let sprint13_reset_plates: &[(&str, TilePosition)] = &[
|
||||
(
|
||||
"sound_lab",
|
||||
@@ -370,6 +399,34 @@ pub fn setup_gauntlet(app: &mut App) {
|
||||
.insert(StableEntityId(sid));
|
||||
}
|
||||
|
||||
// --- Zone Gate (StableId 75) ---
|
||||
// Spawned AFTER Sprint 13 reset plates so it gets ID 75 per the allocation table.
|
||||
rooms::zone_gate::spawn_entities(app, &mut registry);
|
||||
|
||||
// --- Sprint 22 reset plates (StableId 76) ---
|
||||
let sprint22_reset_plates: &[(&str, TilePosition)] = &[(
|
||||
"zone_gate",
|
||||
constants::ZONE_GATE
|
||||
.reset_plate
|
||||
.expect("zone_gate should have a reset_plate"),
|
||||
)];
|
||||
for &(room_name, pos) in sprint22_reset_plates {
|
||||
let entity = app
|
||||
.world_mut()
|
||||
.spawn((
|
||||
Interactable,
|
||||
RoomResetTrigger {
|
||||
room_name: room_name.to_string(),
|
||||
},
|
||||
pos,
|
||||
))
|
||||
.id();
|
||||
let sid = registry.register(entity);
|
||||
app.world_mut()
|
||||
.entity_mut(entity)
|
||||
.insert(StableEntityId(sid));
|
||||
}
|
||||
|
||||
// --- Populate RoomSnapshots for reset mechanism (#490) ---
|
||||
let mut snapshots = RoomSnapshots::default();
|
||||
|
||||
@@ -495,6 +552,15 @@ pub fn setup_gauntlet(app: &mut App) {
|
||||
}
|
||||
}
|
||||
|
||||
// Zone Gate entities (StableId 75): door only, no floor items
|
||||
for id in constants::ZONE_GATE_STABLE_IDS.0..=constants::ZONE_GATE_STABLE_IDS.1 {
|
||||
if let Some(entity) = registry.to_entity(&StableId(id)) {
|
||||
if let Some(pos) = app.world().get::<TilePosition>(entity) {
|
||||
snapshots.record("zone_gate", entity, *pos, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.insert_resource(snapshots);
|
||||
|
||||
// --- Sprint 14 component fixup ---
|
||||
@@ -864,5 +930,25 @@ mod tests {
|
||||
id
|
||||
);
|
||||
}
|
||||
|
||||
// Zone Gate at 75
|
||||
for id in constants::ZONE_GATE_STABLE_IDS.0..=constants::ZONE_GATE_STABLE_IDS.1 {
|
||||
assert!(
|
||||
registry.to_entity(&StableId(id)).is_some(),
|
||||
"Zone Gate at StableId {}",
|
||||
id
|
||||
);
|
||||
}
|
||||
|
||||
// Sprint 22 reset plate at 76
|
||||
for id in constants::SPRINT22_RESET_PLATE_STABLE_IDS.0
|
||||
..=constants::SPRINT22_RESET_PLATE_STABLE_IDS.1
|
||||
{
|
||||
assert!(
|
||||
registry.to_entity(&StableId(id)).is_some(),
|
||||
"Sprint 22 reset plate at StableId {}",
|
||||
id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user