fix(simulation): Clippy cleanup and CI enforcement (#635)

Fix all Clippy warnings across the server codebase (2411 insertions, 1341
deletions). Raise type-complexity-threshold to 750 and too-many-arguments
to 12 in .clippy.toml for idiomatic Bevy ECS system signatures. The server
now passes `cargo clippy -- --deny warnings` cleanly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-17 10:33:15 +01:00
co-authored by Claude Opus 4.6
parent 7f7706442a
commit aa79dd97e7
84 changed files with 2409 additions and 1339 deletions
+1 -5
View File
@@ -205,11 +205,7 @@ pub const CONFRONTATION_STAGE: GauntletRoom = GauntletRoom {
/// distances from the observer: Close (2 tiles), Medium (6 tiles), Long (12 tiles).
pub const SOUND_LAB: GauntletRoom = GauntletRoom {
name: "sound_lab",
origin: TilePosition {
x: 0,
y: 104,
z: 0,
},
origin: TilePosition { x: 0, y: 104, z: 0 },
size: (34, 20),
spawn: TilePosition {
x: 10,
+85 -76
View File
@@ -5,7 +5,7 @@
//! - Perception (5): LOS symmetry, sound range boundaries
//! - Population (8): NPC count limits, tier assignment correctness
//! - Simulation (8): no entity at blocked tile, determinism, pathfinder
//! termination, interaction buffer cleared on sprint
//! termination, interaction buffer cleared on sprint
//!
//! The `run_invariants(world: &mut World)` function covers the 29 structural,
//! perception, population, and simulation invariants checkable via pure
@@ -24,6 +24,12 @@ use crate::knowledge::registry::{EntityRegistry, StableEntityId};
#[cfg(feature = "gauntlet")]
use crate::knowledge::types::SoundRange;
#[cfg(feature = "gauntlet")]
use crate::npc::interaction::InteractionMemory;
#[cfg(feature = "gauntlet")]
use crate::npc::mood::MoodState;
#[cfg(feature = "gauntlet")]
use crate::npc::routine::ActivityState;
#[cfg(feature = "gauntlet")]
use crate::npc::{Npc, Want};
#[cfg(feature = "gauntlet")]
use crate::perception::shadowcast::compute_fov;
@@ -38,12 +44,6 @@ use crate::simulation::pathfinding::{ComputedPath, PathBlocked};
#[cfg(feature = "gauntlet")]
use crate::simulation::sound::SoundEvent;
#[cfg(feature = "gauntlet")]
use crate::npc::interaction::InteractionMemory;
#[cfg(feature = "gauntlet")]
use crate::npc::mood::MoodState;
#[cfg(feature = "gauntlet")]
use crate::npc::routine::ActivityState;
#[cfg(feature = "gauntlet")]
use crate::simulation::tier::{ActiveSim, BackgroundSim, StateSaved};
#[cfg(feature = "gauntlet")]
@@ -164,8 +164,10 @@ fn inv_s2_walkable_count_in_map_bounds(world: &mut World) {
#[cfg(feature = "gauntlet")]
fn inv_s3_player_spawn_walkable(world: &mut World) {
let player_pos = {
let mut q = world.query_filtered::<&TilePosition, bevy_ecs::prelude::With<PlayerCharacter>>();
*q.single(world).expect("S3: exactly one PlayerCharacter must exist")
let mut q =
world.query_filtered::<&TilePosition, bevy_ecs::prelude::With<PlayerCharacter>>();
*q.single(world)
.expect("S3: exactly one PlayerCharacter must exist")
};
let wm = world
.get_resource::<WalkabilityMap>()
@@ -259,11 +261,7 @@ fn inv_s7_room_interiors_have_walkable_tiles(world: &mut World) {
assert!(
found,
"S7: room '{}' (interior x={}..{}, y={}..{}) must have at least one walkable tile",
room.name,
x_start,
x_end,
y_start,
y_end
room.name, x_start, x_end, y_start, y_end
);
}
}
@@ -334,8 +332,8 @@ fn inv_p4_los_symmetry_at_hub(world: &mut World) {
let (ax, ay, bx, by) = (50i32, 58i32, 54i32, 58i32);
let range = 12;
let fov_a = compute_fov(&is_opaque, ax, ay, range, 0);
let fov_b = compute_fov(&is_opaque, bx, by, range, 0);
let fov_a = compute_fov(is_opaque, ax, ay, range, 0);
let fov_b = compute_fov(is_opaque, bx, by, range, 0);
// A sees B → B must see A (symmetric shadowcasting guarantee, D-035)
if fov_a.is_visible(bx, by) {
@@ -375,7 +373,7 @@ fn inv_p5_los_range_bounded(world: &mut World) {
let range = 6;
let (ox, oy) = (50i32, 58i32);
let fov = compute_fov(&is_opaque, ox, oy, range, 0);
let fov = compute_fov(is_opaque, ox, oy, range, 0);
// Tile at Chebyshev distance = range+2 must not be visible.
let (out_x, out_y) = (ox + range + 2, oy);
@@ -399,7 +397,10 @@ fn inv_p5_los_range_bounded(world: &mut World) {
/// Pop1: Active-tier NPC count does not exceed the D-026 tick-budget limit (80).
#[cfg(feature = "gauntlet")]
fn inv_pop1_active_npc_count_within_limit(world: &mut World) {
let mut q = world.query_filtered::<(), (bevy_ecs::prelude::With<Npc>, bevy_ecs::prelude::With<ActiveSim>)>();
let mut q = world.query_filtered::<(), (
bevy_ecs::prelude::With<Npc>,
bevy_ecs::prelude::With<ActiveSim>,
)>();
let count = q.iter(world).count();
assert!(
count <= 80,
@@ -417,15 +418,16 @@ fn inv_pop2_all_npcs_have_tile_position(world: &mut World) {
q.iter(world).count()
};
let with_pos = {
let mut q = world.query_filtered::<(), (bevy_ecs::prelude::With<Npc>, bevy_ecs::prelude::With<TilePosition>)>();
let mut q = world.query_filtered::<(), (
bevy_ecs::prelude::With<Npc>,
bevy_ecs::prelude::With<TilePosition>,
)>();
q.iter(world).count()
};
assert_eq!(
with_pos,
total,
with_pos, total,
"Pop2: all {} NPCs must have TilePosition; only {} do",
total,
with_pos
total, with_pos
);
}
@@ -438,29 +440,32 @@ fn inv_pop3_all_npcs_have_exactly_one_tier(world: &mut World) {
q.iter(world).count()
};
let active = {
let mut q = world.query_filtered::<(), (bevy_ecs::prelude::With<Npc>, bevy_ecs::prelude::With<ActiveSim>)>();
let mut q = world.query_filtered::<(), (
bevy_ecs::prelude::With<Npc>,
bevy_ecs::prelude::With<ActiveSim>,
)>();
q.iter(world).count()
};
let bg = {
let mut q = world.query_filtered::<(), (bevy_ecs::prelude::With<Npc>, bevy_ecs::prelude::With<BackgroundSim>)>();
let mut q = world.query_filtered::<(), (
bevy_ecs::prelude::With<Npc>,
bevy_ecs::prelude::With<BackgroundSim>,
)>();
q.iter(world).count()
};
let ss = {
let mut q = world.query_filtered::<(), (bevy_ecs::prelude::With<Npc>, bevy_ecs::prelude::With<StateSaved>)>();
let mut q = world.query_filtered::<(), (
bevy_ecs::prelude::With<Npc>,
bevy_ecs::prelude::With<StateSaved>,
)>();
q.iter(world).count()
};
let tier_sum = active + bg + ss;
assert_eq!(
tier_sum,
total,
tier_sum, total,
"Pop3: each of {} NPCs must have exactly one tier marker; \
found {} Active + {} Background + {} StateSaved = {} (should equal {})",
total,
active,
bg,
ss,
tier_sum,
total
total, active, bg, ss, tier_sum, total
);
}
@@ -468,9 +473,9 @@ fn inv_pop3_all_npcs_have_exactly_one_tier(world: &mut World) {
/// Same-layer collision on spawn is a world-setup error.
#[cfg(feature = "gauntlet")]
fn inv_pop4_no_same_layer_collision(world: &mut World) {
use std::collections::BTreeSet;
use crate::simulation::movement::TilePresence;
use bevy_ecs::prelude::Entity;
use std::collections::BTreeSet;
let mut q = world.query::<(Entity, &TilePosition, Option<&TilePresence>)>();
let occupied: Vec<(TilePosition, TilePresence, Entity)> = q
@@ -516,15 +521,14 @@ fn inv_pop6_all_npcs_have_want(world: &mut World) {
q.iter(world).count()
};
let with_want = {
let mut q = world.query_filtered::<(), (bevy_ecs::prelude::With<Npc>, bevy_ecs::prelude::With<Want>)>();
let mut q = world
.query_filtered::<(), (bevy_ecs::prelude::With<Npc>, bevy_ecs::prelude::With<Want>)>();
q.iter(world).count()
};
assert_eq!(
with_want,
total,
with_want, total,
"Pop6: all {} NPCs must have a Want component; only {} do (D-024 axis 1)",
total,
with_want
total, with_want
);
}
@@ -545,8 +549,7 @@ fn inv_pop7_crowd_plaza_npc_count(world: &mut World) {
})
.count();
assert_eq!(
count,
15,
count, 15,
"Pop7: Crowd Plaza must contain exactly 15 NPCs; found {}",
count
);
@@ -556,15 +559,12 @@ fn inv_pop7_crowd_plaza_npc_count(world: &mut World) {
/// Duplicate StableIds corrupt knowledge-graph references and snapshot output.
#[cfg(feature = "gauntlet")]
fn inv_pop8_stable_ids_unique(world: &mut World) {
use std::collections::BTreeSet;
use crate::knowledge::types::StableId;
use bevy_ecs::prelude::Entity;
use std::collections::BTreeSet;
let mut q = world.query::<(Entity, &StableEntityId)>();
let ids: Vec<(StableId, Entity)> = q
.iter(world)
.map(|(e, sid)| (sid.0, e))
.collect();
let ids: Vec<(StableId, Entity)> = q.iter(world).map(|(e, sid)| (sid.0, e)).collect();
let mut seen: BTreeSet<StableId> = BTreeSet::new();
for (id, entity) in ids {
@@ -593,7 +593,8 @@ fn inv_sim1_no_entity_at_blocked_tile(world: &mut World) {
// Only check movable entities: Npc + PlayerCharacter. Fixtures/signs/reset plates
// may legitimately sit in wall tiles (interactable from range, not traversed).
let positions: Vec<(Entity, TilePosition)> = {
let mut q = world.query_filtered::<(Entity, &TilePosition), Or<(With<Npc>, With<PlayerCharacter>)>>();
let mut q = world
.query_filtered::<(Entity, &TilePosition), Or<(With<Npc>, With<PlayerCharacter>)>>();
q.iter(world).map(|(e, p)| (e, *p)).collect()
};
let wm = world
@@ -619,9 +620,7 @@ fn inv_sim2_computed_path_steps_walkable(world: &mut World) {
let paths: Vec<(Entity, Vec<TilePosition>)> = {
let mut q = world.query::<(Entity, &ComputedPath)>();
q.iter(world)
.map(|(e, p)| (e, p.steps.clone()))
.collect()
q.iter(world).map(|(e, p)| (e, p.steps.clone())).collect()
};
let wm = world
.get_resource::<WalkabilityMap>()
@@ -667,8 +666,7 @@ fn inv_sim4_player_entity_present(world: &mut World) {
let mut q = world.query_filtered::<(), With<PlayerCharacter>>();
let count = q.iter(world).count();
assert_eq!(
count,
1,
count, 1,
"Sim4: exactly one PlayerCharacter must exist; found {}",
count
);
@@ -704,7 +702,7 @@ fn inv_sim6_registry_has_entities(world: &mut World) {
.get_resource::<EntityRegistry>()
.expect("Sim6: EntityRegistry resource must exist");
assert!(
registry.len() > 0,
!registry.is_empty(),
"Sim6: EntityRegistry must contain at least one entity (the player)"
);
}
@@ -750,19 +748,24 @@ fn inv_sim8_player_has_interaction_buffer(world: &mut World) {
#[cfg(feature = "gauntlet")]
fn inv_s14_active_npcs_have_mood_state(world: &mut World) {
let active = {
let mut q = world.query_filtered::<(), (bevy_ecs::prelude::With<Npc>, bevy_ecs::prelude::With<ActiveSim>)>();
let mut q = world.query_filtered::<(), (
bevy_ecs::prelude::With<Npc>,
bevy_ecs::prelude::With<ActiveSim>,
)>();
q.iter(world).count()
};
let with_mood = {
let mut q = world.query_filtered::<(), (bevy_ecs::prelude::With<Npc>, bevy_ecs::prelude::With<ActiveSim>, bevy_ecs::prelude::With<MoodState>)>();
let mut q = world.query_filtered::<(), (
bevy_ecs::prelude::With<Npc>,
bevy_ecs::prelude::With<ActiveSim>,
bevy_ecs::prelude::With<MoodState>,
)>();
q.iter(world).count()
};
assert_eq!(
with_mood,
active,
with_mood, active,
"S14-1: all {} Active NPCs must have MoodState; only {} do",
active,
with_mood
active, with_mood
);
}
@@ -775,15 +778,16 @@ fn inv_s14_npcs_have_interaction_memory(world: &mut World) {
q.iter(world).count()
};
let with_mem = {
let mut q = world.query_filtered::<(), (bevy_ecs::prelude::With<Npc>, bevy_ecs::prelude::With<InteractionMemory>)>();
let mut q = world.query_filtered::<(), (
bevy_ecs::prelude::With<Npc>,
bevy_ecs::prelude::With<InteractionMemory>,
)>();
q.iter(world).count()
};
assert_eq!(
with_mem,
total,
with_mem, total,
"S14-2: all {} NPCs must have InteractionMemory; only {} do",
total,
with_mem
total, with_mem
);
}
@@ -792,8 +796,8 @@ fn inv_s14_npcs_have_interaction_memory(world: &mut World) {
/// "needs to move somewhere". Both at once is contradictory.
#[cfg(feature = "gauntlet")]
fn inv_s14_no_activity_state_with_path_request(world: &mut World) {
use bevy_ecs::prelude::{Entity, With};
use crate::simulation::pathfinding::PathRequest;
use bevy_ecs::prelude::{Entity, With};
let with_activity: Vec<Entity> = {
let mut q = world.query_filtered::<Entity, With<ActivityState>>();
@@ -816,7 +820,8 @@ fn inv_pop3b_no_double_tagged_tiers(world: &mut World) {
use bevy_ecs::prelude::{Entity, With};
let active_and_bg: Vec<Entity> = {
let mut q = world.query_filtered::<Entity, (With<Npc>, With<ActiveSim>, With<BackgroundSim>)>();
let mut q =
world.query_filtered::<Entity, (With<Npc>, With<ActiveSim>, With<BackgroundSim>)>();
q.iter(world).collect()
};
assert!(
@@ -826,7 +831,8 @@ fn inv_pop3b_no_double_tagged_tiers(world: &mut World) {
);
let active_and_ss: Vec<Entity> = {
let mut q = world.query_filtered::<Entity, (With<Npc>, With<ActiveSim>, With<StateSaved>)>();
let mut q =
world.query_filtered::<Entity, (With<Npc>, With<ActiveSim>, With<StateSaved>)>();
q.iter(world).collect()
};
assert!(
@@ -836,7 +842,8 @@ fn inv_pop3b_no_double_tagged_tiers(world: &mut World) {
);
let bg_and_ss: Vec<Entity> = {
let mut q = world.query_filtered::<Entity, (With<Npc>, With<BackgroundSim>, With<StateSaved>)>();
let mut q =
world.query_filtered::<Entity, (With<Npc>, With<BackgroundSim>, With<StateSaved>)>();
q.iter(world).collect()
};
assert!(
@@ -857,14 +864,16 @@ mod system_tests {
use bevy_ecs::prelude::*;
use bevy_ecs::schedule::Schedule;
use crate::bridge::types::MovementStance;
use crate::knowledge::registry::EntityRegistry;
use crate::npc::{Contentment, Npc, ToleranceThreshold, Want, WantKind};
use crate::simulation::interaction::{compute_nearby_interactions, Interactable, NearbyInteractionBuffer};
use crate::simulation::interaction::{
compute_nearby_interactions, Interactable, NearbyInteractionBuffer,
};
use crate::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap};
use crate::simulation::pathfinding::{compute_paths, ComputedPath, PathBlocked, PathRequest};
use crate::simulation::stance::Stance;
use crate::simulation::tier::ActiveSim;
use crate::bridge::types::MovementStance;
use crate::knowledge::registry::EntityRegistry;
// -----------------------------------------------------------------------
// Invariant 30-31: Pathfinder terminates on adjacent tile
@@ -1042,8 +1051,8 @@ mod system_tests {
schedule.add_systems(compute_nearby_interactions);
schedule.run(&mut world);
let mut q = world
.query_filtered::<&mut NearbyInteractionBuffer, With<PlayerCharacter>>();
let mut q =
world.query_filtered::<&mut NearbyInteractionBuffer, With<PlayerCharacter>>();
let mut buf = q.single_mut(&mut world).expect("player must exist");
let interactions = buf.take();
assert!(
@@ -1074,8 +1083,8 @@ mod system_tests {
schedule.add_systems(compute_nearby_interactions);
schedule.run(&mut world);
let mut q = world
.query_filtered::<&mut NearbyInteractionBuffer, With<PlayerCharacter>>();
let mut q =
world.query_filtered::<&mut NearbyInteractionBuffer, With<PlayerCharacter>>();
let mut buf = q.single_mut(&mut world).expect("player must exist");
let interactions = buf.take();
assert!(
+31 -15
View File
@@ -2,7 +2,7 @@
//!
//! 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
//! and manual QA sessions. Loaded instead of server/content/ when the server runs
//! the Gauntlet map.
//!
//! Module structure:
@@ -111,11 +111,11 @@ pub fn setup_gauntlet(app: &mut App, archetype: crate::bridge::types::CharacterA
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
// Sprint 13 rooms
// Sprint 13 rooms
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
// Sprint 22 rooms
carve_room_interior(&mut walkability, 64, 102, 16, 22); // Zone Gate
// Carve corridors between hub and rooms
@@ -192,8 +192,10 @@ pub fn setup_gauntlet(app: &mut App, archetype: crate::bridge::types::CharacterA
// Spawn at Hub center: absolute (50, 58)
let profile = MovementProfile::smuggler();
let player_pos = TilePosition::new(50, 58, 0);
let mut monologue_state = MonologueState::default();
monologue_state.character = archetype.as_monologue_key().to_string();
let monologue_state = MonologueState {
character: archetype.as_monologue_key().to_string(),
..Default::default()
};
let player = app
.world_mut()
.spawn((
@@ -536,8 +538,7 @@ pub fn setup_gauntlet(app: &mut App, archetype: crate::bridge::types::CharacterA
}
// Decay Observatory entities (StableId 69): NPC only, no floor items
for id in
constants::DECAY_OBSERVATORY_STABLE_IDS.0..=constants::DECAY_OBSERVATORY_STABLE_IDS.1
for id in constants::DECAY_OBSERVATORY_STABLE_IDS.0..=constants::DECAY_OBSERVATORY_STABLE_IDS.1
{
if let Some(entity) = registry.to_entity(&StableId(id)) {
if let Some(pos) = app.world().get::<TilePosition>(entity) {
@@ -616,7 +617,7 @@ pub fn setup_gauntlet(app: &mut App, archetype: crate::bridge::types::CharacterA
use crate::simulation::conversation::NpcColorIndex;
use crate::simulation::dialogue::{CurrentMood, DialogueProfile};
// (location, role) pairs matching content/campaigns/.../dialogue/ YAML pools.
// (location, role) pairs matching server/content/campaigns/.../dialogue/ YAML pools.
// Cycling through these gives NPC variety across rooms.
const DIALOGUE_ROLES: &[(&str, &str)] = &[
("the-terminal", "dock-worker"),
@@ -698,7 +699,10 @@ mod tests {
app.add_plugins(crate::knowledge::KnowledgePlugin);
app.add_plugins(crate::npc::NpcPlugin);
setup_gauntlet(&mut app, crate::bridge::types::CharacterArchetype::default());
setup_gauntlet(
&mut app,
crate::bridge::types::CharacterArchetype::default(),
);
// Run all 29 world-query invariants against the fully-initialized gauntlet world.
invariants::run_invariants(app.world_mut());
@@ -718,7 +722,10 @@ mod tests {
app.add_plugins(crate::knowledge::KnowledgePlugin);
app.add_plugins(crate::npc::NpcPlugin);
setup_gauntlet(&mut app, crate::bridge::types::CharacterArchetype::default());
setup_gauntlet(
&mut app,
crate::bridge::types::CharacterArchetype::default(),
);
let wm = app.world().resource::<WalkabilityMap>();
// Hub center at (50, 58) must be walkable
@@ -732,7 +739,10 @@ mod tests {
app.add_plugins(crate::knowledge::KnowledgePlugin);
app.add_plugins(crate::npc::NpcPlugin);
setup_gauntlet(&mut app, crate::bridge::types::CharacterArchetype::default());
setup_gauntlet(
&mut app,
crate::bridge::types::CharacterArchetype::default(),
);
let wm = app.world().resource::<WalkabilityMap>();
// North wall segment at absolute (90, 54) should be blocked
@@ -748,7 +758,10 @@ mod tests {
app.add_plugins(crate::knowledge::KnowledgePlugin);
app.add_plugins(crate::npc::NpcPlugin);
setup_gauntlet(&mut app, crate::bridge::types::CharacterArchetype::default());
setup_gauntlet(
&mut app,
crate::bridge::types::CharacterArchetype::default(),
);
let wm = app.world().resource::<WalkabilityMap>();
// corridor-E center should be walkable
@@ -762,7 +775,10 @@ mod tests {
app.add_plugins(crate::knowledge::KnowledgePlugin);
app.add_plugins(crate::npc::NpcPlugin);
setup_gauntlet(&mut app, crate::bridge::types::CharacterArchetype::default());
setup_gauntlet(
&mut app,
crate::bridge::types::CharacterArchetype::default(),
);
let registry = app.world().resource::<EntityRegistry>();
@@ -904,8 +920,8 @@ mod tests {
}
// Decay Observatory at 69
for id in constants::DECAY_OBSERVATORY_STABLE_IDS.0
..=constants::DECAY_OBSERVATORY_STABLE_IDS.1
for id in
constants::DECAY_OBSERVATORY_STABLE_IDS.0..=constants::DECAY_OBSERVATORY_STABLE_IDS.1
{
assert!(
registry.to_entity(&StableId(id)).is_some(),
+4 -3
View File
@@ -17,7 +17,9 @@
use bevy_app::prelude::*;
use crate::knowledge::registry::{EntityRegistry, StableEntityId};
use crate::npc::{Contentment, DailyRoutine, Npc, RoutineEntry, ToleranceThreshold, Want, WantKind};
use crate::npc::{
Contentment, DailyRoutine, Npc, RoutineEntry, ToleranceThreshold, Want, WantKind,
};
use crate::simulation::interaction::Interactable;
use crate::simulation::movement::TilePosition;
use crate::simulation::path_follow::MovementSpeed;
@@ -177,8 +179,7 @@ mod tests {
.id();
// Advance time to Afternoon boundary (Morning→Afternoon).
world.resource_mut::<SimulationTime>().tick =
MINUTES_PER_PHASE * TICKS_PER_GAME_MINUTE;
world.resource_mut::<SimulationTime>().tick = MINUTES_PER_PHASE * TICKS_PER_GAME_MINUTE;
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(check_phase_transition);
+22 -6
View File
@@ -30,22 +30,38 @@ const ORIGIN_X: i32 = 0;
const ORIGIN_Y: i32 = 104;
/// Observer position for Sound Lab tests (absolute).
pub const OBSERVER_POS: TilePosition = TilePosition { x: 10, y: 114, z: 0 };
pub const OBSERVER_POS: TilePosition = TilePosition {
x: 10,
y: 114,
z: 0,
};
/// Close-range emitter position — 2 tiles east of observer (dist=2, Close ≤3).
pub const CLOSE_EMITTER_POS: TilePosition = TilePosition { x: 12, y: 114, z: 0 };
pub const CLOSE_EMITTER_POS: TilePosition = TilePosition {
x: 12,
y: 114,
z: 0,
};
/// Medium-range emitter position — 6 tiles east of observer (dist=6, Medium ≤8, outside Close).
pub const MEDIUM_EMITTER_POS: TilePosition = TilePosition { x: 16, y: 114, z: 0 };
pub const MEDIUM_EMITTER_POS: TilePosition = TilePosition {
x: 16,
y: 114,
z: 0,
};
/// Long-range emitter position — 12 tiles east of observer (dist=12, Long ≤20, outside Medium).
pub const LONG_EMITTER_POS: TilePosition = TilePosition { x: 22, y: 114, z: 0 };
pub const LONG_EMITTER_POS: TilePosition = TilePosition {
x: 22,
y: 114,
z: 0,
};
/// NPC definitions: (relative_x, relative_y, want_kind, intensity).
const NPCS: &[(i32, i32, WantKind, u8)] = &[
(12, 10, WantKind::Safety, 3), // npc_sound_close — StableId 66
(12, 10, WantKind::Safety, 3), // npc_sound_close — StableId 66
(16, 10, WantKind::Knowledge, 4), // npc_sound_medium — StableId 67
(22, 10, WantKind::Freedom, 5), // npc_sound_long — StableId 68
(22, 10, WantKind::Freedom, 5), // npc_sound_long — StableId 68
];
/// Spawn Sound Lab entities in canonical order (StableId 66-68).
+28 -6
View File
@@ -25,16 +25,32 @@ use crate::simulation::interaction::{DoorState, Interactable, ObjectType};
use crate::simulation::movement::TilePosition;
/// Observer start position — Terminal side of the zone boundary.
pub const OBSERVER_POS: TilePosition = TilePosition { x: 70, y: 112, z: 0 };
pub const OBSERVER_POS: TilePosition = TilePosition {
x: 70,
y: 112,
z: 0,
};
/// Door position — first tile of the Corridor zone (zone boundary).
pub const DOOR_POS: TilePosition = TilePosition { x: 72, y: 112, z: 0 };
pub const DOOR_POS: TilePosition = TilePosition {
x: 72,
y: 112,
z: 0,
};
/// Last walkable tile of the Terminal zone before the boundary.
pub const TERMINAL_SIDE_POS: TilePosition = TilePosition { x: 71, y: 112, z: 0 };
pub const TERMINAL_SIDE_POS: TilePosition = TilePosition {
x: 71,
y: 112,
z: 0,
};
/// First walkable tile of the Corridor zone after the boundary.
pub const CORRIDOR_SIDE_POS: TilePosition = TilePosition { x: 73, y: 112, z: 0 };
pub const CORRIDOR_SIDE_POS: TilePosition = TilePosition {
x: 73,
y: 112,
z: 0,
};
/// Spawn Zone Gate entities in canonical order (StableId 75).
pub fn spawn_entities(app: &mut App, registry: &mut EntityRegistry) {
@@ -149,7 +165,11 @@ mod tests {
ZONE_GATE_TERMINAL_ZONE_ID
);
assert_eq!(
zone_map.zone_at(TERMINAL_SIDE_POS.x, TERMINAL_SIDE_POS.y, TERMINAL_SIDE_POS.z),
zone_map.zone_at(
TERMINAL_SIDE_POS.x,
TERMINAL_SIDE_POS.y,
TERMINAL_SIDE_POS.z
),
Some(ZONE_GATE_TERMINAL_ZONE_ID),
"Tile (71,112) must be in Terminal zone"
);
@@ -267,7 +287,9 @@ mod tests {
world.resource_mut::<ZoneCrossEventQueue>().drain();
// Move within Terminal zone (same zone, different tile).
world.entity_mut(player).insert(TilePosition::new(68, 112, 0));
world
.entity_mut(player)
.insert(TilePosition::new(68, 112, 0));
sched.run(&mut world);
let events = world.resource_mut::<ZoneCrossEventQueue>().drain();