feat(simulation): add Sound Lab, Decay Observatory, Shift Change gauntlet rooms (#505)
Three new gauntlet test rooms for Sprint 13, each targeting a specific system under test. StableIds are additive-only (66–74). Sound Lab (66-68): Three NPCs at calibrated tile distances (2, 6, 12 tiles) verify D-018 three-range sound model. Tests confirm Close ≤3, Medium ≤8, Long ≤20 tile thresholds via SoundEvent::audible_at(). Decay Observatory (69): Single NPC observed in LOS then broken. Tests confirm D-041 confidence decay: KnowsDetails → KnowsOf → Suspects (floor), and Stale state when stale_after threshold is exceeded. Shift Change (70-71): Two NPCs with DailyRoutine components. Test confirms D-031 phase-boundary check_phase_transition issues PathRequest only to NPCs not already at their target location. Sprint 13 reset plates (72-74) wired per existing pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
//! Decay Observatory — Room 13 (24x14)
|
||||
//!
|
||||
//! Tests D-041 (knowledge graph decay). Observer starts with Direct confidence
|
||||
//! on an NPC in LOS, then LOS is broken. Tick advances verify confidence
|
||||
//! degrades: Direct → KnowsDetails → KnowsOf → Suspects → Stale (D-041).
|
||||
//!
|
||||
//! Decay runs once per game-minute (every 10 ticks, D-031). Confidence floor
|
||||
//! is Suspects — decay never removes an entity from the knowledge graph.
|
||||
//!
|
||||
//! Layout: Open room. Single observable NPC east of observer.
|
||||
//! This room sits in the corridor-NW path (x=12-18, y=22-40); the corridor
|
||||
//! creates natural north/south doorways through the room walls.
|
||||
//!
|
||||
//! Observer position: (10, 30) absolute, facing East.
|
||||
//!
|
||||
//! Entities (StableId 69):
|
||||
//! npc_observe_target (16, 30) — 6 tiles east of observer, starts in LOS
|
||||
|
||||
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;
|
||||
use crate::simulation::tier::ActiveSim;
|
||||
|
||||
/// Room origin (top-left corner including walls).
|
||||
const ORIGIN_X: i32 = 0;
|
||||
const ORIGIN_Y: i32 = 24;
|
||||
|
||||
/// Observer position for Decay Observatory tests (absolute).
|
||||
pub const OBSERVER_POS: TilePosition = TilePosition { x: 10, y: 30, z: 0 };
|
||||
|
||||
/// NPC position — 6 tiles east of observer, starts in direct LOS.
|
||||
pub const NPC_OBSERVE_POS: TilePosition = TilePosition { x: 16, y: 30, z: 0 };
|
||||
|
||||
/// Spawn Decay Observatory entities in canonical order (StableId 69).
|
||||
pub fn spawn_entities(app: &mut App, registry: &mut EntityRegistry) {
|
||||
let pos = TilePosition::new(ORIGIN_X + 16, ORIGIN_Y + 6, 0);
|
||||
let entity = app
|
||||
.world_mut()
|
||||
.spawn((
|
||||
Npc,
|
||||
ActiveSim,
|
||||
Interactable,
|
||||
pos,
|
||||
Want {
|
||||
primary: WantKind::Safety,
|
||||
intensity: 4,
|
||||
description: "Decay Observatory: observable NPC".to_string(),
|
||||
},
|
||||
Contentment { level: 5 },
|
||||
ToleranceThreshold {
|
||||
current_stress: 0,
|
||||
threshold: 60,
|
||||
},
|
||||
MovementSpeed::default(),
|
||||
))
|
||||
.id();
|
||||
let sid = registry.register(entity);
|
||||
app.world_mut()
|
||||
.entity_mut(entity)
|
||||
.insert(StableEntityId(sid));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::knowledge::graph::KnowledgeGraph;
|
||||
use crate::knowledge::types::{DecayThresholds, KnowledgeConfidence, KnowledgeState, StableId};
|
||||
|
||||
const TARGET: StableId = StableId(1);
|
||||
|
||||
fn observe_then_leave(tick_observe: u64, tick_leave: u64) -> KnowledgeGraph {
|
||||
let mut g = KnowledgeGraph::new();
|
||||
g.observe_entity(TARGET, NPC_OBSERVE_POS, tick_observe);
|
||||
g.observe_entity_leaving_los(&TARGET, tick_leave);
|
||||
g
|
||||
}
|
||||
|
||||
/// After LOS loss, confidence degrades one level per decay pass:
|
||||
/// KnowsDetails → KnowsOf → Suspects (floor).
|
||||
#[test]
|
||||
fn decay_observatory_confidence_degrades_after_los_loss() {
|
||||
let mut g = observe_then_leave(100, 110);
|
||||
|
||||
// Verify starting state: KnowsDetails after LOS loss.
|
||||
assert_eq!(
|
||||
g.confidence_of(&TARGET),
|
||||
Some(KnowledgeConfidence::KnowsDetails),
|
||||
"confidence must be KnowsDetails immediately after LOS loss"
|
||||
);
|
||||
|
||||
let thresholds = DecayThresholds {
|
||||
decay_after: 10,
|
||||
stale_after: 10_000,
|
||||
};
|
||||
|
||||
// Tick 200: age = 200 - 100 = 100 > decay_after=10 → KnowsOf
|
||||
g.decay(200, &thresholds);
|
||||
assert_eq!(
|
||||
g.confidence_of(&TARGET),
|
||||
Some(KnowledgeConfidence::KnowsOf),
|
||||
"KnowsDetails must decay to KnowsOf after one decay pass"
|
||||
);
|
||||
|
||||
// Tick 300: age still > decay_after → Suspects
|
||||
g.decay(300, &thresholds);
|
||||
assert_eq!(
|
||||
g.confidence_of(&TARGET),
|
||||
Some(KnowledgeConfidence::Suspects),
|
||||
"KnowsOf must decay to Suspects after second decay pass"
|
||||
);
|
||||
|
||||
// Tick 400: Suspects is the floor — no further degradation.
|
||||
g.decay(400, &thresholds);
|
||||
assert_eq!(
|
||||
g.confidence_of(&TARGET),
|
||||
Some(KnowledgeConfidence::Suspects),
|
||||
"Suspects is the confidence floor — decay must not go below Suspects"
|
||||
);
|
||||
}
|
||||
|
||||
/// After exceeding the stale threshold the entry state becomes Stale.
|
||||
#[test]
|
||||
fn decay_observatory_stale_after_threshold() {
|
||||
let mut g = observe_then_leave(100, 110);
|
||||
|
||||
let thresholds = DecayThresholds {
|
||||
decay_after: 10,
|
||||
stale_after: 50,
|
||||
};
|
||||
|
||||
// Tick 200: age = 200 - 100 = 100 > stale_after=50 → Stale
|
||||
g.decay(200, &thresholds);
|
||||
|
||||
let entry = g.entity_knowledge(&TARGET).unwrap();
|
||||
assert_eq!(
|
||||
entry.state,
|
||||
KnowledgeState::Stale,
|
||||
"entry state must become Stale when age exceeds stale_after threshold"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
pub mod confrontation_stage;
|
||||
pub mod crowd_plaza;
|
||||
pub mod decay_observatory;
|
||||
pub mod dialogue_room;
|
||||
pub mod eavesdrop_alcove;
|
||||
pub mod fog_theater;
|
||||
@@ -13,4 +14,6 @@ pub mod interaction_gallery;
|
||||
pub mod inventory_warehouse;
|
||||
pub mod occlusion_corridor;
|
||||
pub mod pause_chamber;
|
||||
pub mod shift_change;
|
||||
pub mod sound_lab;
|
||||
pub mod sprint_gauntlet;
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
//! Shift Change — Room 14 (16x24)
|
||||
//!
|
||||
//! Tests D-031 (day phases) and NPC routine transitions at phase boundaries.
|
||||
//! Two NPCs have DailyRoutine components with entries for Morning and Afternoon.
|
||||
//! Advancing game clock to the Afternoon boundary triggers PathRequests for
|
||||
//! both NPCs (they must move to their Afternoon locations).
|
||||
//!
|
||||
//! This room sits adjacent to the corridor-E2 path (x=58-80, y=84-90), which
|
||||
//! provides natural east-west access through the room interior.
|
||||
//!
|
||||
//! Observer position: (72, 90) absolute, facing North.
|
||||
//!
|
||||
//! Entities (StableId 70-71):
|
||||
//! npc_shift_morning (70, 84) — Morning location: 6 tiles north-west of observer
|
||||
//! npc_shift_afternoon (76, 96) — Afternoon location: 4 tiles east, 6 south of observer
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
|
||||
use crate::knowledge::registry::{EntityRegistry, StableEntityId};
|
||||
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;
|
||||
use crate::simulation::tier::ActiveSim;
|
||||
use crate::simulation::time::DayPhase;
|
||||
|
||||
/// Room origin (top-left corner including walls).
|
||||
const ORIGIN_X: i32 = 64;
|
||||
const ORIGIN_Y: i32 = 78;
|
||||
|
||||
/// Observer position for Shift Change tests (absolute).
|
||||
pub const OBSERVER_POS: TilePosition = TilePosition { x: 72, y: 90, z: 0 };
|
||||
|
||||
/// Morning routine location for both NPCs — where they stand during Morning phase.
|
||||
pub const MORNING_LOCATION: TilePosition = TilePosition { x: 70, y: 84, z: 0 };
|
||||
|
||||
/// Afternoon routine location — where NPCs should walk to at Afternoon boundary.
|
||||
pub const AFTERNOON_LOCATION: TilePosition = TilePosition { x: 76, y: 96, z: 0 };
|
||||
|
||||
/// Spawn Shift Change entities in canonical order (StableId 70-71).
|
||||
pub fn spawn_entities(app: &mut App, registry: &mut EntityRegistry) {
|
||||
// npc_shift_morning (StableId 70): starts at Morning location, has both Morning + Afternoon entries.
|
||||
let entity = app
|
||||
.world_mut()
|
||||
.spawn((
|
||||
Npc,
|
||||
ActiveSim,
|
||||
Interactable,
|
||||
MORNING_LOCATION,
|
||||
Want {
|
||||
primary: WantKind::Safety,
|
||||
intensity: 4,
|
||||
description: "Shift Change: morning shift NPC".to_string(),
|
||||
},
|
||||
Contentment { level: 0 },
|
||||
ToleranceThreshold {
|
||||
current_stress: 5,
|
||||
threshold: 45,
|
||||
},
|
||||
MovementSpeed::default(),
|
||||
DailyRoutine {
|
||||
entries: vec![
|
||||
RoutineEntry {
|
||||
phase: DayPhase::Morning,
|
||||
location: MORNING_LOCATION,
|
||||
activity: "Morning station".to_string(),
|
||||
},
|
||||
RoutineEntry {
|
||||
phase: DayPhase::Afternoon,
|
||||
location: AFTERNOON_LOCATION,
|
||||
activity: "Afternoon patrol".to_string(),
|
||||
},
|
||||
],
|
||||
description: "Shifts between morning station and afternoon patrol".to_string(),
|
||||
},
|
||||
))
|
||||
.id();
|
||||
let sid = registry.register(entity);
|
||||
app.world_mut()
|
||||
.entity_mut(entity)
|
||||
.insert(StableEntityId(sid));
|
||||
|
||||
// npc_shift_afternoon (StableId 71): starts at Afternoon location, same routine.
|
||||
// Starting position differs from Morning location so a transition always triggers a PathRequest.
|
||||
let entity = app
|
||||
.world_mut()
|
||||
.spawn((
|
||||
Npc,
|
||||
ActiveSim,
|
||||
Interactable,
|
||||
AFTERNOON_LOCATION,
|
||||
Want {
|
||||
primary: WantKind::Connection,
|
||||
intensity: 3,
|
||||
description: "Shift Change: afternoon shift NPC".to_string(),
|
||||
},
|
||||
Contentment { level: 2 },
|
||||
ToleranceThreshold {
|
||||
current_stress: 10,
|
||||
threshold: 55,
|
||||
},
|
||||
MovementSpeed::default(),
|
||||
DailyRoutine {
|
||||
entries: vec![
|
||||
RoutineEntry {
|
||||
phase: DayPhase::Morning,
|
||||
location: MORNING_LOCATION,
|
||||
activity: "Morning briefing".to_string(),
|
||||
},
|
||||
RoutineEntry {
|
||||
phase: DayPhase::Afternoon,
|
||||
location: AFTERNOON_LOCATION,
|
||||
activity: "Afternoon post".to_string(),
|
||||
},
|
||||
],
|
||||
description: "Covers afternoon post, returns to morning briefing".to_string(),
|
||||
},
|
||||
))
|
||||
.id();
|
||||
let sid = registry.register(entity);
|
||||
app.world_mut()
|
||||
.entity_mut(entity)
|
||||
.insert(StableEntityId(sid));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::npc::routine::{check_phase_transition, PreviousDayPhase};
|
||||
use crate::simulation::pathfinding::PathRequest;
|
||||
use crate::simulation::time::{SimulationTime, MINUTES_PER_PHASE, TICKS_PER_GAME_MINUTE};
|
||||
|
||||
/// At the Morning→Afternoon boundary, NPCs receive PathRequests for their Afternoon location.
|
||||
#[test]
|
||||
fn shift_change_npc_transitions_routine_at_phase_boundary() {
|
||||
let mut world = bevy_ecs::world::World::new();
|
||||
world.insert_resource(SimulationTime::default());
|
||||
world.init_resource::<PreviousDayPhase>();
|
||||
|
||||
// Spawn npc_shift_morning: currently at MORNING_LOCATION.
|
||||
// On Afternoon transition it should receive a PathRequest to AFTERNOON_LOCATION.
|
||||
let npc_morning = world
|
||||
.spawn((
|
||||
Npc,
|
||||
ActiveSim,
|
||||
MORNING_LOCATION,
|
||||
DailyRoutine {
|
||||
entries: vec![
|
||||
RoutineEntry {
|
||||
phase: DayPhase::Morning,
|
||||
location: MORNING_LOCATION,
|
||||
activity: "Morning station".to_string(),
|
||||
},
|
||||
RoutineEntry {
|
||||
phase: DayPhase::Afternoon,
|
||||
location: AFTERNOON_LOCATION,
|
||||
activity: "Afternoon patrol".to_string(),
|
||||
},
|
||||
],
|
||||
description: "Test routine".to_string(),
|
||||
},
|
||||
))
|
||||
.id();
|
||||
|
||||
// Spawn npc_shift_afternoon: currently at AFTERNOON_LOCATION.
|
||||
// Already at Afternoon location — PathRequest should NOT fire for it.
|
||||
let npc_afternoon = world
|
||||
.spawn((
|
||||
Npc,
|
||||
ActiveSim,
|
||||
AFTERNOON_LOCATION,
|
||||
DailyRoutine {
|
||||
entries: vec![RoutineEntry {
|
||||
phase: DayPhase::Afternoon,
|
||||
location: AFTERNOON_LOCATION,
|
||||
activity: "Afternoon post".to_string(),
|
||||
}],
|
||||
description: "Test routine".to_string(),
|
||||
},
|
||||
))
|
||||
.id();
|
||||
|
||||
// Advance time to Afternoon boundary (Morning→Afternoon).
|
||||
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);
|
||||
schedule.run(&mut world);
|
||||
|
||||
// npc_shift_morning must have received a PathRequest to AFTERNOON_LOCATION.
|
||||
let req = world
|
||||
.get::<PathRequest>(npc_morning)
|
||||
.expect("npc_shift_morning must receive PathRequest at Afternoon boundary");
|
||||
assert_eq!(
|
||||
req.goal, AFTERNOON_LOCATION,
|
||||
"PathRequest goal must be the Afternoon routine location"
|
||||
);
|
||||
|
||||
// npc_shift_afternoon is already at AFTERNOON_LOCATION — no PathRequest.
|
||||
assert!(
|
||||
world.get::<PathRequest>(npc_afternoon).is_none(),
|
||||
"npc_shift_afternoon already at Afternoon location — must NOT receive PathRequest"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
//! Sound Lab — Room 12 (34x20)
|
||||
//!
|
||||
//! Tests D-018 (three-range sound model). Sound-emitting entities placed at
|
||||
//! calibrated Manhattan distances from the observer. Verifies that:
|
||||
//! - Close (≤3 tiles): SoundEvent is audible at observer
|
||||
//! - Medium (≤8 tiles): SoundEvent is audible with Medium range but NOT Close
|
||||
//! - Long (≤20 tiles): SoundEvent is audible with Long range but NOT Medium
|
||||
//!
|
||||
//! Layout: Observer at west interior, emitters placed east at increasing
|
||||
//! distances along the same row.
|
||||
//!
|
||||
//! Observer position: (10, 114) absolute, facing East.
|
||||
//!
|
||||
//! Entities (StableId 66-68):
|
||||
//! npc_sound_close (12, 114) — 2 tiles east of observer (Close range, dist ≤3)
|
||||
//! npc_sound_medium (16, 114) — 6 tiles east of observer (Medium range, dist ≤8)
|
||||
//! npc_sound_long (22, 114) — 12 tiles east of observer (Long range, dist ≤20)
|
||||
|
||||
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;
|
||||
use crate::simulation::tier::ActiveSim;
|
||||
|
||||
/// Room origin (top-left corner including walls).
|
||||
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 };
|
||||
|
||||
/// 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 };
|
||||
|
||||
/// 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 };
|
||||
|
||||
/// 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 };
|
||||
|
||||
/// 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
|
||||
(16, 10, WantKind::Knowledge, 4), // npc_sound_medium — StableId 67
|
||||
(22, 10, WantKind::Freedom, 5), // npc_sound_long — StableId 68
|
||||
];
|
||||
|
||||
/// Spawn Sound Lab entities in canonical order (StableId 66-68).
|
||||
pub fn spawn_entities(app: &mut App, registry: &mut EntityRegistry) {
|
||||
for &(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,
|
||||
ActiveSim,
|
||||
Interactable,
|
||||
pos,
|
||||
Want {
|
||||
primary: want_kind,
|
||||
intensity,
|
||||
description: "Sound Lab test emitter".to_string(),
|
||||
},
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::knowledge::types::SoundRange;
|
||||
use crate::simulation::sound::{SoundEvent, SoundEventKind};
|
||||
|
||||
/// Close emitter is 2 tiles away — must be audible with SoundRange::Close (≤3 tiles, D-018).
|
||||
#[test]
|
||||
fn sound_lab_close_range_in_snapshot() {
|
||||
let event = SoundEvent::at(
|
||||
&CLOSE_EMITTER_POS,
|
||||
SoundEventKind::Footstep,
|
||||
0.8,
|
||||
SoundRange::Close,
|
||||
None,
|
||||
);
|
||||
assert!(
|
||||
event.audible_at(&OBSERVER_POS),
|
||||
"Close emitter at dist=2 must be audible with SoundRange::Close (max 3 tiles)"
|
||||
);
|
||||
}
|
||||
|
||||
/// Medium emitter is 6 tiles away — audible with Medium (≤8) but NOT with Close (≤3).
|
||||
#[test]
|
||||
fn sound_lab_medium_range_indicator_present() {
|
||||
let medium_event = SoundEvent::at(
|
||||
&MEDIUM_EMITTER_POS,
|
||||
SoundEventKind::Voice,
|
||||
0.6,
|
||||
SoundRange::Medium,
|
||||
None,
|
||||
);
|
||||
assert!(
|
||||
medium_event.audible_at(&OBSERVER_POS),
|
||||
"Medium emitter at dist=6 must be audible with SoundRange::Medium (max 8 tiles)"
|
||||
);
|
||||
|
||||
// Negative: Close range cannot reach 6 tiles.
|
||||
let close_event = SoundEvent::at(
|
||||
&MEDIUM_EMITTER_POS,
|
||||
SoundEventKind::Voice,
|
||||
0.6,
|
||||
SoundRange::Close,
|
||||
None,
|
||||
);
|
||||
assert!(
|
||||
!close_event.audible_at(&OBSERVER_POS),
|
||||
"Medium-position emitter (dist=6) must NOT be audible with SoundRange::Close (max 3 tiles)"
|
||||
);
|
||||
}
|
||||
|
||||
/// Long emitter is 12 tiles away — audible with Long (≤20) but NOT with Medium (≤8).
|
||||
#[test]
|
||||
fn sound_lab_long_range_insert_only() {
|
||||
let long_event = SoundEvent::at(
|
||||
&LONG_EMITTER_POS,
|
||||
SoundEventKind::Alert,
|
||||
0.9,
|
||||
SoundRange::Long,
|
||||
None,
|
||||
);
|
||||
assert!(
|
||||
long_event.audible_at(&OBSERVER_POS),
|
||||
"Long emitter at dist=12 must be audible with SoundRange::Long (max 20 tiles)"
|
||||
);
|
||||
|
||||
// Negative: Medium range cannot reach 12 tiles.
|
||||
let medium_event = SoundEvent::at(
|
||||
&LONG_EMITTER_POS,
|
||||
SoundEventKind::Alert,
|
||||
0.9,
|
||||
SoundRange::Medium,
|
||||
None,
|
||||
);
|
||||
assert!(
|
||||
!medium_event.audible_at(&OBSERVER_POS),
|
||||
"Long-position emitter (dist=12) must NOT be audible with SoundRange::Medium (max 8 tiles)"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user