Warnings fixed:
- contraband.rs: scan event now always emits even when NPC already
knows (was skipped by early `continue`). Contract matches doc.
- test_world/mod.rs: ScanEventBuffer added to player spawn bundle
so check_contraband_scan doesn't silently no-op in gauntlet mode.
- npc/mod.rs → simulation/mod.rs: moved check_contraband_scan
registration to SimulationPlugin (operates on player inventory and
snapshot pipeline, consistent with process_talk_interaction).
Suggestions addressed:
- cross_room_transitions.rs T1: clarified standalone position vs
constants.rs observer position in comment.
- dialogue.rs: Vec<&str> dedup replaced with BTreeSet<&str> for
deterministic iteration (project convention).
- contraband.rs: added test for multiple simultaneous ScanAuthority
NPCs in range (564 tests total).
- dialogue.rs: doc-comment on relationship_to_trust explaining
KnowledgeConfidence ordering and Suspects default.
- cross_room_transitions.rs T5: noted direct KG API usage vs full
perception system.
- sprint_gauntlet.rs: documented intentional Contentment { level: 0 }.
- content_scaling.rs: noted GAUNTLET_NPC_COUNT is manually maintained.
- contraband.rs: doc-comment on cross-plugin registration rationale.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
629 lines
22 KiB
Rust
629 lines
22 KiB
Rust
//! Gauntlet test world — purpose-built rooms for systematic QA testing.
|
|
//!
|
|
//! NOT production content. This module provides deterministic room layouts
|
|
//! with precise entity placement for golden file testing, regression testing,
|
|
//! and manual QA sessions. Loaded instead of content/ when the server runs
|
|
//! the Gauntlet map.
|
|
//!
|
|
//! Module structure:
|
|
//! - `reset` — always compiled (used by simulation::input in production)
|
|
//! - `constants`, `rooms`, `setup_gauntlet` — gated behind "gauntlet" feature
|
|
//! (default-on; strip with --no-default-features for release builds)
|
|
//!
|
|
//! Architecture (per workshop-outcomes.md Section 4):
|
|
//! - Rooms are defined as Rust builder functions (hybrid YAML + Rust inject)
|
|
//! - WalkabilityMap covers the full Gauntlet bounds (0-116 x 0-124)
|
|
//! - Entities spawned in canonical order → StableId assignment is deterministic
|
|
//! - Additive-only: existing rooms/entities never reordered
|
|
//!
|
|
//! StableId ranges (from gestalt-round3.md + Sprint 11):
|
|
//! Player: 0
|
|
//! Hub signs: 1-4
|
|
//! Fog Theater: 5-8
|
|
//! Occlusion Corridor: 9-12
|
|
//! Inventory Warehouse: 13-23
|
|
//! Interaction Gallery: 24-28
|
|
//! Pause Chamber: 29
|
|
//! Dialogue Room: 30-33
|
|
//! Crowd Plaza: 34-48
|
|
//! Reset plates (Sprint 1-10 rooms): 49-55
|
|
//! Sprint Gauntlet: 56-57
|
|
//! Eavesdrop Alcove: 58-60
|
|
//! Confrontation Stage: 61-62
|
|
//! Reset plates (Sprint 11 rooms): 63-65
|
|
|
|
#[cfg(feature = "gauntlet")]
|
|
pub mod constants;
|
|
pub mod reset;
|
|
#[cfg(feature = "gauntlet")]
|
|
pub mod rooms;
|
|
|
|
#[cfg(feature = "gauntlet")]
|
|
use bevy_app::prelude::*;
|
|
|
|
#[cfg(feature = "gauntlet")]
|
|
use crate::knowledge::registry::{EntityRegistry, StableEntityId};
|
|
#[cfg(feature = "gauntlet")]
|
|
use crate::knowledge::types::StableId;
|
|
#[cfg(feature = "gauntlet")]
|
|
use crate::knowledge::KnowledgeGraph;
|
|
#[cfg(feature = "gauntlet")]
|
|
use crate::perception::cognitive_delay::CognitiveDelay;
|
|
#[cfg(feature = "gauntlet")]
|
|
use crate::perception::vision_cone::Facing;
|
|
#[cfg(feature = "gauntlet")]
|
|
use crate::simulation::interaction::{Interactable, NearbyInteractionBuffer};
|
|
#[cfg(feature = "gauntlet")]
|
|
use crate::simulation::inventory::ItemName;
|
|
#[cfg(feature = "gauntlet")]
|
|
use crate::simulation::listening::ListeningFocus;
|
|
#[cfg(feature = "gauntlet")]
|
|
use crate::simulation::contraband::ScanEventBuffer;
|
|
use crate::simulation::monologue::{MonologueBuffer, MonologueState, SprintAnomalyQueue};
|
|
#[cfg(feature = "gauntlet")]
|
|
use crate::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap};
|
|
#[cfg(feature = "gauntlet")]
|
|
use crate::simulation::stance::{MovementProfile, PlayerMoveCooldown};
|
|
|
|
#[cfg(feature = "gauntlet")]
|
|
use reset::{RoomResetTrigger, RoomSnapshots};
|
|
|
|
#[cfg(feature = "gauntlet")]
|
|
/// Map bounds for the full Gauntlet world.
|
|
pub const MAP_WIDTH: i32 = 117;
|
|
#[cfg(feature = "gauntlet")]
|
|
pub const MAP_HEIGHT: i32 = 125;
|
|
|
|
/// Set up the Gauntlet test world.
|
|
///
|
|
/// Creates the full WalkabilityMap (blocked by default), carves room
|
|
/// interiors and corridors, spawns the player and all room entities
|
|
/// in canonical StableId order.
|
|
///
|
|
/// NOTE: This bypasses the normal startup system scheduler — entities are
|
|
/// spawned directly into the World rather than via startup systems. This
|
|
/// is intentional for deterministic test setups but should be revisited
|
|
/// if Gauntlet is ever served by the production startup pipeline.
|
|
#[cfg(feature = "gauntlet")]
|
|
pub fn setup_gauntlet(app: &mut App) {
|
|
// Start with a fully blocked map, then carve rooms and corridors.
|
|
let mut walkability = WalkabilityMap::new_blocked(MAP_WIDTH, MAP_HEIGHT, 1);
|
|
|
|
// 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_room_interior(&mut walkability, 0, 2, 28, 20); // Sprint Gauntlet
|
|
carve_room_interior(&mut walkability, 74, 26, 24, 16); // Eavesdrop Alcove
|
|
carve_room_interior(&mut walkability, 84, 2, 32, 24); // Confrontation Stage
|
|
|
|
// 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
|
|
// Sprint 11 corridors
|
|
carve_corridor(&mut walkability, 12, 22, 6, 18); // corridor-NW: Sprint Gauntlet ↔ Inventory south
|
|
carve_corridor(&mut walkability, 62, 30, 12, 6); // corridor-NE: Eavesdrop Alcove ↔ Occlusion north
|
|
|
|
// 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
|
|
for x in 88..=96 {
|
|
for y in 54..=55 {
|
|
walkability.set_walkable(&TilePosition::new(x, y, 0), false);
|
|
}
|
|
}
|
|
// South alcove walls: rel x=[4,8], y=[12,13]
|
|
for x in 78..=82 {
|
|
for y in 60..=61 {
|
|
walkability.set_walkable(&TilePosition::new(x, y, 0), false);
|
|
}
|
|
}
|
|
|
|
app.insert_resource(walkability);
|
|
|
|
// Entity spawning in canonical StableId order.
|
|
// Player gets StableId 0, then entities by room in workshop order.
|
|
let mut registry = EntityRegistry::new(0);
|
|
|
|
// --- Player (StableId 0) ---
|
|
// Spawn at Hub center: absolute (50, 58)
|
|
let profile = MovementProfile::smuggler();
|
|
let player_pos = TilePosition::new(50, 58, 0);
|
|
let player = app
|
|
.world_mut()
|
|
.spawn((
|
|
PlayerCharacter,
|
|
player_pos,
|
|
Facing::default(),
|
|
KnowledgeGraph::new(),
|
|
NearbyInteractionBuffer::default(),
|
|
MonologueState::default(),
|
|
MonologueBuffer::default(),
|
|
SprintAnomalyQueue::default(),
|
|
ScanEventBuffer::default(),
|
|
CognitiveDelay::default(),
|
|
ListeningFocus::new(player_pos),
|
|
profile,
|
|
profile.initial_stance(),
|
|
PlayerMoveCooldown::default(),
|
|
))
|
|
.id();
|
|
registry.register(player);
|
|
|
|
// --- Hub signs (StableId 1-4) ---
|
|
rooms::hub::spawn_entities(app, &mut registry);
|
|
|
|
// --- 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);
|
|
|
|
// --- Inventory Warehouse (StableId 13-23) ---
|
|
rooms::inventory_warehouse::spawn_entities(app, &mut registry);
|
|
|
|
// --- 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) ---
|
|
rooms::dialogue_room::spawn_entities(app, &mut registry);
|
|
|
|
// --- 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
|
|
.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));
|
|
}
|
|
|
|
// --- Sprint Gauntlet (StableId 56-57) ---
|
|
rooms::sprint_gauntlet::spawn_entities(app, &mut registry);
|
|
|
|
// --- Eavesdrop Alcove (StableId 58-60) ---
|
|
rooms::eavesdrop_alcove::spawn_entities(app, &mut registry);
|
|
|
|
// --- Confrontation Stage (StableId 61-62) ---
|
|
rooms::confrontation_stage::spawn_entities(app, &mut registry);
|
|
|
|
// --- Sprint 11 reset plates (StableId 63-65) ---
|
|
let sprint11_reset_plates: &[(&str, TilePosition)] = &[
|
|
(
|
|
"sprint_gauntlet",
|
|
constants::SPRINT_GAUNTLET
|
|
.reset_plate
|
|
.expect("sprint_gauntlet should have a reset_plate"),
|
|
),
|
|
(
|
|
"eavesdrop_alcove",
|
|
constants::EAVESDROP_ALCOVE
|
|
.reset_plate
|
|
.expect("eavesdrop_alcove should have a reset_plate"),
|
|
),
|
|
(
|
|
"confrontation_stage",
|
|
constants::CONFRONTATION_STAGE
|
|
.reset_plate
|
|
.expect("confrontation_stage should have a reset_plate"),
|
|
),
|
|
];
|
|
for &(room_name, pos) in sprint11_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();
|
|
|
|
// 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)) {
|
|
if let Some(pos) = app.world().get::<TilePosition>(entity) {
|
|
snapshots.record("occlusion_corridor", entity, *pos, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Inventory Warehouse entities (StableId 13-23): crates are floor items, NPC is not
|
|
for id in 13..=23 {
|
|
if let Some(entity) = registry.to_entity(&StableId(id)) {
|
|
if let Some(pos) = app.world().get::<TilePosition>(entity) {
|
|
let is_floor_item = app.world().get::<ItemName>(entity).is_some();
|
|
snapshots.record("inventory_warehouse", entity, *pos, is_floor_item);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
snapshots.record("pause_chamber", entity, *pos, false);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sprint Gauntlet entities (StableId 56-57): sign + NPC, no floor items
|
|
for id in constants::SPRINT_GAUNTLET_STABLE_IDS.0..=constants::SPRINT_GAUNTLET_STABLE_IDS.1 {
|
|
if let Some(entity) = registry.to_entity(&StableId(id)) {
|
|
if let Some(pos) = app.world().get::<TilePosition>(entity) {
|
|
snapshots.record("sprint_gauntlet", entity, *pos, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Eavesdrop Alcove entities (StableId 58-60): NPCs + sign, no floor items
|
|
for id in constants::EAVESDROP_ALCOVE_STABLE_IDS.0..=constants::EAVESDROP_ALCOVE_STABLE_IDS.1 {
|
|
if let Some(entity) = registry.to_entity(&StableId(id)) {
|
|
if let Some(pos) = app.world().get::<TilePosition>(entity) {
|
|
snapshots.record("eavesdrop_alcove", entity, *pos, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Confrontation Stage entities (StableId 61-62): NPCs only, no floor items
|
|
for id in
|
|
constants::CONFRONTATION_STAGE_STABLE_IDS.0..=constants::CONFRONTATION_STAGE_STABLE_IDS.1
|
|
{
|
|
if let Some(entity) = registry.to_entity(&StableId(id)) {
|
|
if let Some(pos) = app.world().get::<TilePosition>(entity) {
|
|
snapshots.record("confrontation_stage", entity, *pos, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
app.insert_resource(snapshots);
|
|
app.insert_resource(registry);
|
|
}
|
|
|
|
#[cfg(feature = "gauntlet")]
|
|
/// Carve the walkable interior of a room.
|
|
/// Room has 2-tile-thick walls; interior starts 2 tiles inside each boundary.
|
|
fn carve_room_interior(wm: &mut WalkabilityMap, ox: i32, oy: i32, w: i32, h: i32) {
|
|
for y in (oy + 2)..(oy + h - 2) {
|
|
for x in (ox + 2)..(ox + w - 2) {
|
|
wm.set_walkable(&TilePosition::new(x, y, 0), true);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "gauntlet")]
|
|
/// Carve a corridor as a fully walkable rectangle.
|
|
/// Corridors are 6 tiles wide with walls on both sides — we carve the
|
|
/// interior 2 tiles in from each edge (leaving 2-tile walkable center).
|
|
/// For narrow corridors (width=6), interior is 2 tiles wide.
|
|
fn carve_corridor(wm: &mut WalkabilityMap, ox: i32, oy: i32, w: i32, h: i32) {
|
|
// Corridors are fully walkable rectangles (wall tiles are the
|
|
// surrounding room/map boundary). Carve interior with 1-tile margin.
|
|
for y in (oy + 1)..(oy + h - 1) {
|
|
for x in (ox + 1)..(ox + w - 1) {
|
|
wm.set_walkable(&TilePosition::new(x, y, 0), true);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(all(test, feature = "gauntlet"))]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::knowledge::registry::EntityRegistry;
|
|
|
|
#[test]
|
|
fn gauntlet_setup_creates_expected_entities() {
|
|
let mut app = App::new();
|
|
app.add_plugins(crate::simulation::SimulationPlugin);
|
|
app.add_plugins(crate::knowledge::KnowledgePlugin);
|
|
app.add_plugins(crate::npc::NpcPlugin);
|
|
|
|
setup_gauntlet(&mut app);
|
|
|
|
let registry = app.world().resource::<EntityRegistry>();
|
|
assert_eq!(
|
|
registry.len(),
|
|
constants::EXPECTED_ENTITY_COUNT,
|
|
"entity count should match EXPECTED_ENTITY_COUNT derived from StableId ranges"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hub_center_is_walkable() {
|
|
let mut app = App::new();
|
|
app.add_plugins(crate::simulation::SimulationPlugin);
|
|
app.add_plugins(crate::knowledge::KnowledgePlugin);
|
|
app.add_plugins(crate::npc::NpcPlugin);
|
|
|
|
setup_gauntlet(&mut app);
|
|
|
|
let wm = app.world().resource::<WalkabilityMap>();
|
|
// Hub center at (50, 58) must be walkable
|
|
assert!(wm.can_move_to(&TilePosition::new(50, 58, 0)));
|
|
}
|
|
|
|
#[test]
|
|
fn occlusion_north_wall_blocks() {
|
|
let mut app = App::new();
|
|
app.add_plugins(crate::simulation::SimulationPlugin);
|
|
app.add_plugins(crate::knowledge::KnowledgePlugin);
|
|
app.add_plugins(crate::npc::NpcPlugin);
|
|
|
|
setup_gauntlet(&mut app);
|
|
|
|
let wm = app.world().resource::<WalkabilityMap>();
|
|
// North wall segment at absolute (90, 54) should be blocked
|
|
assert!(!wm.can_move_to(&TilePosition::new(90, 54, 0)));
|
|
// But the corridor interior at (90, 56) should be walkable
|
|
assert!(wm.can_move_to(&TilePosition::new(90, 56, 0)));
|
|
}
|
|
|
|
#[test]
|
|
fn corridor_connects_hub_to_occlusion() {
|
|
let mut app = App::new();
|
|
app.add_plugins(crate::simulation::SimulationPlugin);
|
|
app.add_plugins(crate::knowledge::KnowledgePlugin);
|
|
app.add_plugins(crate::npc::NpcPlugin);
|
|
|
|
setup_gauntlet(&mut app);
|
|
|
|
let wm = app.world().resource::<WalkabilityMap>();
|
|
// corridor-E center should be walkable
|
|
assert!(wm.can_move_to(&TilePosition::new(66, 57, 0)));
|
|
}
|
|
|
|
#[test]
|
|
fn stable_id_ranges_match_spec() {
|
|
let mut app = App::new();
|
|
app.add_plugins(crate::simulation::SimulationPlugin);
|
|
app.add_plugins(crate::knowledge::KnowledgePlugin);
|
|
app.add_plugins(crate::npc::NpcPlugin);
|
|
|
|
setup_gauntlet(&mut app);
|
|
|
|
let registry = app.world().resource::<EntityRegistry>();
|
|
|
|
// Player at StableId 0
|
|
use crate::knowledge::types::StableId;
|
|
assert!(
|
|
registry.to_entity(&StableId(0)).is_some(),
|
|
"Player at StableId 0"
|
|
);
|
|
|
|
// Hub signs at 1-4
|
|
for id in 1..=4 {
|
|
assert!(
|
|
registry.to_entity(&StableId(id)).is_some(),
|
|
"Hub sign at StableId {}",
|
|
id
|
|
);
|
|
}
|
|
|
|
// Fog Theater at 5-8
|
|
for id in 5..=8 {
|
|
assert!(
|
|
registry.to_entity(&StableId(id)).is_some(),
|
|
"Fog Theater at StableId {}",
|
|
id
|
|
);
|
|
}
|
|
|
|
// Occlusion Corridor at 9-12
|
|
for id in 9..=12 {
|
|
assert!(
|
|
registry.to_entity(&StableId(id)).is_some(),
|
|
"Occlusion at StableId {}",
|
|
id
|
|
);
|
|
}
|
|
|
|
// Inventory Warehouse at 13-23
|
|
for id in 13..=23 {
|
|
assert!(
|
|
registry.to_entity(&StableId(id)).is_some(),
|
|
"Inventory at StableId {}",
|
|
id
|
|
);
|
|
}
|
|
|
|
// Interaction Gallery at 24-28
|
|
for id in 24..=28 {
|
|
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-55
|
|
for id in constants::RESET_PLATE_STABLE_IDS.0..=constants::RESET_PLATE_STABLE_IDS.1 {
|
|
assert!(
|
|
registry.to_entity(&StableId(id)).is_some(),
|
|
"Reset plate at StableId {}",
|
|
id
|
|
);
|
|
}
|
|
|
|
// Sprint Gauntlet at 56-57
|
|
for id in constants::SPRINT_GAUNTLET_STABLE_IDS.0..=constants::SPRINT_GAUNTLET_STABLE_IDS.1
|
|
{
|
|
assert!(
|
|
registry.to_entity(&StableId(id)).is_some(),
|
|
"Sprint Gauntlet at StableId {}",
|
|
id
|
|
);
|
|
}
|
|
|
|
// Eavesdrop Alcove at 58-60
|
|
for id in
|
|
constants::EAVESDROP_ALCOVE_STABLE_IDS.0..=constants::EAVESDROP_ALCOVE_STABLE_IDS.1
|
|
{
|
|
assert!(
|
|
registry.to_entity(&StableId(id)).is_some(),
|
|
"Eavesdrop Alcove at StableId {}",
|
|
id
|
|
);
|
|
}
|
|
|
|
// Confrontation Stage at 61-62
|
|
for id in constants::CONFRONTATION_STAGE_STABLE_IDS.0
|
|
..=constants::CONFRONTATION_STAGE_STABLE_IDS.1
|
|
{
|
|
assert!(
|
|
registry.to_entity(&StableId(id)).is_some(),
|
|
"Confrontation Stage at StableId {}",
|
|
id
|
|
);
|
|
}
|
|
|
|
// Sprint 11 reset plates at 63-65
|
|
for id in constants::SPRINT11_RESET_PLATE_STABLE_IDS.0
|
|
..=constants::SPRINT11_RESET_PLATE_STABLE_IDS.1
|
|
{
|
|
assert!(
|
|
registry.to_entity(&StableId(id)).is_some(),
|
|
"Sprint 11 reset plate at StableId {}",
|
|
id
|
|
);
|
|
}
|
|
}
|
|
}
|