#94 — Active tier simulation (complete): - Add ActiveSim marker to all 9 test world room NPC spawns - Fix test entities in routine.rs and path_follow.rs to include ActiveSim so With<ActiveSim> queries match correctly in unit tests #99 — Tier transition logic (complete): - Implement update_tier_markers system in tier.rs - Promotes/demotes tier markers by manhattan distance from PlayerCharacter: ≤40 tiles → ActiveSim, ≤120 → BackgroundSim, beyond → StateSaved - Handles cross-z-level as u32::MAX (effectively unreachable) - No-op when no PlayerCharacter entity present (headless tests safe) - 11 new unit tests covering all distance bands and boundary cases - TierPlugin now registers the system after movement::validate_movement Also picks up extended test coverage added by hoshe: - observer/tests.rs — 230 lines of perception observer tests - sound.rs — additional sound event integration tests All 548 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -278,4 +278,220 @@ mod tests {
|
||||
schedule.run(&mut world);
|
||||
assert_eq!(world.resource::<SoundEventQueue>().events.len(), 0);
|
||||
}
|
||||
|
||||
// --- Multi-emitter and multi-event tests ---
|
||||
|
||||
#[test]
|
||||
fn multiple_emitters_collected_in_one_tick() {
|
||||
let mut world = bevy_ecs::world::World::new();
|
||||
world.insert_resource(SoundEventQueue::default());
|
||||
|
||||
world.spawn(SoundEventEmitter::new(close_event(&tile(1, 1))));
|
||||
world.spawn(SoundEventEmitter::new(close_event(&tile(2, 2))));
|
||||
world.spawn(SoundEventEmitter::new(medium_event(&tile(3, 3))));
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(collect_sound_events);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let queue = world.resource::<SoundEventQueue>();
|
||||
assert_eq!(queue.events.len(), 3, "all three emitters collected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emitter_with_multiple_pending_events_all_drained() {
|
||||
let mut world = bevy_ecs::world::World::new();
|
||||
world.insert_resource(SoundEventQueue::default());
|
||||
|
||||
let pos = tile(5, 5);
|
||||
let emitter = SoundEventEmitter::new(close_event(&pos))
|
||||
.with(medium_event(&pos))
|
||||
.with(SoundEvent::at(
|
||||
&pos,
|
||||
SoundEventKind::Alert,
|
||||
1.0,
|
||||
SoundRange::Long,
|
||||
None,
|
||||
));
|
||||
world.spawn(emitter);
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(collect_sound_events);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let queue = world.resource::<SoundEventQueue>();
|
||||
assert_eq!(
|
||||
queue.events.len(),
|
||||
3,
|
||||
"all three pending events from one emitter collected"
|
||||
);
|
||||
}
|
||||
|
||||
// --- Long range audibility tests (D-018) ---
|
||||
|
||||
#[test]
|
||||
fn long_range_sound_audible_within_20_tiles() {
|
||||
let source = tile(0, 0);
|
||||
let event =
|
||||
SoundEvent::at(&source, SoundEventKind::Alert, 0.9, SoundRange::Long, None);
|
||||
|
||||
// Manhattan distance 20 — exactly at boundary
|
||||
let listener = tile(10, 10);
|
||||
assert!(
|
||||
event.audible_at(&listener),
|
||||
"Long range sound audible at manhattan 20"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_range_sound_not_audible_beyond_20_tiles() {
|
||||
let source = tile(0, 0);
|
||||
let event =
|
||||
SoundEvent::at(&source, SoundEventKind::Alert, 0.9, SoundRange::Long, None);
|
||||
|
||||
let listener = tile(11, 10); // manhattan 21
|
||||
assert!(
|
||||
!event.audible_at(&listener),
|
||||
"Long range sound not audible at manhattan 21"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_range_audible_at_origin() {
|
||||
let source = tile(5, 5);
|
||||
let event =
|
||||
SoundEvent::at(&source, SoundEventKind::Machinery, 0.5, SoundRange::Long, None);
|
||||
assert!(event.audible_at(&source), "audible at source position");
|
||||
}
|
||||
|
||||
// --- Sound event field preservation tests ---
|
||||
|
||||
#[test]
|
||||
fn source_entity_id_preserved_through_collection() {
|
||||
let mut world = bevy_ecs::world::World::new();
|
||||
world.insert_resource(SoundEventQueue::default());
|
||||
|
||||
let pos = tile(5, 5);
|
||||
let event = SoundEvent::at(
|
||||
&pos,
|
||||
SoundEventKind::Voice,
|
||||
0.8,
|
||||
SoundRange::Close,
|
||||
Some(42),
|
||||
);
|
||||
world.spawn(SoundEventEmitter::new(event));
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(collect_sound_events);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let queue = world.resource::<SoundEventQueue>();
|
||||
assert_eq!(queue.events.len(), 1);
|
||||
assert_eq!(
|
||||
queue.events[0].source_entity_id,
|
||||
Some(42),
|
||||
"source_entity_id must be preserved through collection"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intensity_preserved_through_collection() {
|
||||
let mut world = bevy_ecs::world::World::new();
|
||||
world.insert_resource(SoundEventQueue::default());
|
||||
|
||||
let pos = tile(5, 5);
|
||||
let event = SoundEvent::at(
|
||||
&pos,
|
||||
SoundEventKind::Footstep,
|
||||
0.37,
|
||||
SoundRange::Close,
|
||||
None,
|
||||
);
|
||||
world.spawn(SoundEventEmitter::new(event));
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(collect_sound_events);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let queue = world.resource::<SoundEventQueue>();
|
||||
assert_eq!(queue.events.len(), 1);
|
||||
assert!(
|
||||
(queue.events[0].intensity - 0.37).abs() < f32::EPSILON,
|
||||
"intensity must be preserved through collection"
|
||||
);
|
||||
}
|
||||
|
||||
// --- All sound event kinds ---
|
||||
|
||||
#[test]
|
||||
fn all_sound_event_kinds_can_be_emitted_and_collected() {
|
||||
let kinds = [
|
||||
SoundEventKind::Footstep,
|
||||
SoundEventKind::Voice,
|
||||
SoundEventKind::Machinery,
|
||||
SoundEventKind::Alert,
|
||||
SoundEventKind::Ambient,
|
||||
];
|
||||
|
||||
let mut world = bevy_ecs::world::World::new();
|
||||
world.insert_resource(SoundEventQueue::default());
|
||||
let pos = tile(5, 5);
|
||||
|
||||
for kind in kinds {
|
||||
world.spawn(SoundEventEmitter::new(SoundEvent::at(
|
||||
&pos,
|
||||
kind,
|
||||
0.5,
|
||||
SoundRange::Close,
|
||||
None,
|
||||
)));
|
||||
}
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(collect_sound_events);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let queue = world.resource::<SoundEventQueue>();
|
||||
assert_eq!(
|
||||
queue.events.len(),
|
||||
5,
|
||||
"all five SoundEventKind variants collected"
|
||||
);
|
||||
|
||||
let collected_kinds: std::collections::HashSet<SoundEventKind> =
|
||||
queue.events.iter().map(|e| e.kind).collect();
|
||||
for kind in [
|
||||
SoundEventKind::Footstep,
|
||||
SoundEventKind::Voice,
|
||||
SoundEventKind::Machinery,
|
||||
SoundEventKind::Alert,
|
||||
SoundEventKind::Ambient,
|
||||
] {
|
||||
assert!(collected_kinds.contains(&kind), "{kind:?} not collected");
|
||||
}
|
||||
}
|
||||
|
||||
// --- max_range_tiles spec verification (D-018) ---
|
||||
|
||||
#[test]
|
||||
fn max_range_tiles_values_match_d018() {
|
||||
assert_eq!(SoundEvent::max_range_tiles(SoundRange::Close), 3);
|
||||
assert_eq!(SoundEvent::max_range_tiles(SoundRange::Medium), 8);
|
||||
assert_eq!(SoundEvent::max_range_tiles(SoundRange::Long), 20);
|
||||
}
|
||||
|
||||
// --- SoundEventQueue::drain test ---
|
||||
|
||||
#[test]
|
||||
fn queue_drain_empties_the_queue() {
|
||||
let mut queue = SoundEventQueue::default();
|
||||
let pos = tile(5, 5);
|
||||
queue.events.push(close_event(&pos));
|
||||
queue.events.push(medium_event(&pos));
|
||||
assert_eq!(queue.events.len(), 2);
|
||||
|
||||
let drained = queue.drain();
|
||||
assert_eq!(drained.len(), 2, "drain returns all events");
|
||||
assert_eq!(queue.events.len(), 0, "queue empty after drain");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user