use bevy::{app::App, prelude::*, MinimalPlugins};
use bevy_rapier3d::prelude::CollisionEvent;
use bevy_rapier3d::rapier::prelude::CollisionEventFlags;
use brkrs::{BrickTypeId, CountsTowardsCompletion};
use tempfile::NamedTempFile;
fn level_test_app() -> App {
let mut app = App::new();
app.add_plugins((MinimalPlugins, bevy::input::InputPlugin));
app.add_message::<CollisionEvent>();
app.insert_resource(brkrs::GameProgress::default());
app.insert_resource(brkrs::level_loader::LevelAdvanceState::default());
app.insert_resource(brkrs::systems::respawn::SpawnPoints::default());
app.insert_resource(Assets::<Mesh>::default());
app.insert_resource(Assets::<StandardMaterial>::default());
app.insert_resource(bevy::input::ButtonInput::<bevy::prelude::KeyCode>::default());
app.world_mut()
.spawn(bevy_rapier3d::prelude::RapierConfiguration::new(1.0));
app.add_plugins(brkrs::systems::LevelSwitchPlugin);
app.add_plugins(brkrs::level_loader::LevelLoaderPlugin);
brkrs::register_brick_collision_systems(&mut app);
app
}
#[test]
fn spawn_marks_counts_for_non_indestructible_bricks() {
let mut app = level_test_app();
let mut tmp = NamedTempFile::new().expect("create temp level file");
let contents = "LevelDefinition(number:999,matrix:[[90,20,3]])";
use std::io::Write;
tmp.write_all(contents.as_bytes())
.expect("write temp level");
std::env::set_var("BK_LEVEL_PATH", tmp.path().to_str().unwrap());
app.update();
app.update();
let mut found_90 = false;
let mut found_20 = false;
let mut found_3 = false;
let world = &mut app.world_mut();
let mut q = world.query::<(&BrickTypeId, Option<&CountsTowardsCompletion>)>();
for (type_id, maybe_marker) in q.iter(world) {
if type_id.0 == 90 {
found_90 = true;
assert!(
maybe_marker.is_none(),
"indestructible brick must NOT count for completion"
);
}
if type_id.0 == 20 {
found_20 = true;
assert!(
maybe_marker.is_some(),
"simple brick (20) must count for completion"
);
}
if type_id.0 == 3 {
found_3 = true;
assert!(
maybe_marker.is_some(),
"legacy simple brick (3) must count for completion during compatibility window"
);
}
}
assert!(
found_90 && found_20 && found_3,
"All three brick types should be present in spawned bricks"
);
std::env::remove_var("BK_LEVEL_PATH");
}
#[test]
fn completion_triggers_when_only_indestructible_bricks_remain() {
let mut app = level_test_app();
let path = "assets/levels/test_mixed_indestructible.ron";
assert!(
std::path::Path::new(path).exists(),
"test level file must exist"
);
std::env::set_var("BK_LEVEL", "997");
app.update();
app.update();
{
let world = &mut app.world_mut();
let mut q = world.query::<(Entity, Option<&CountsTowardsCompletion>)>();
let mut destructible_count = 0usize;
let mut to_despawn: Vec<Entity> = Vec::new();
for (e, marker) in q.iter(world) {
if marker.is_some() {
destructible_count += 1;
to_despawn.push(e);
}
}
for e in to_despawn {
world.despawn(e);
}
assert!(
destructible_count > 0,
"level must start with at least one destructible brick"
);
}
app.update();
app.update();
let mut paddle_count = 0usize;
{
let world = &mut app.world_mut();
let mut paddle_query = world.query::<(Entity, &brkrs::Paddle)>();
for (_e, _p) in paddle_query.iter(world) {
paddle_count += 1;
}
}
assert_eq!(
paddle_count, 0,
"No paddles should remain after level completion"
);
std::env::remove_var("BK_LEVEL");
}
#[test]
fn destructible_brick_marked_and_despawned_on_ball_collision() {
let mut app = level_test_app();
std::env::set_var("BK_LEVEL", "997");
app.update();
app.update();
let world = &mut app.world_mut();
let mut target: Option<Entity> = None;
let mut q = world.query::<(
Entity,
&brkrs::BrickTypeId,
Option<&brkrs::CountsTowardsCompletion>,
)>();
for (e, type_id, marker) in q.iter(world) {
if type_id.0 == 20 && marker.is_some() {
target = Some(e);
break;
}
}
let brick = target.expect("expected at least one destructible (20) brick in test level");
let ball = app.world_mut().spawn((brkrs::Ball,)).id();
assert!(
app.world().entities().contains(ball),
"ball entity must exist"
);
assert!(
app.world().entities().contains(brick),
"brick entity must exist"
);
{
let world = &mut app.world_mut();
let mut q = world.query::<(Entity, Option<&brkrs::CountsTowardsCompletion>)>();
let mut found = false;
for (e, marker) in q.iter(world) {
if e == brick {
found = marker.is_some();
break;
}
}
assert!(found, "brick must be destructible before collision test");
}
let mut collisions = app.world_mut().resource_mut::<Messages<CollisionEvent>>();
collisions.write(CollisionEvent::Started(
ball,
brick,
CollisionEventFlags::empty(),
));
app.update();
app.update();
assert!(
!app.world().entities().contains(brick),
"destructible brick should be removed on collision"
);
std::env::remove_var("BK_LEVEL");
}
#[test]
fn indestructible_brick_not_marked_on_ball_collision() {
let mut app = level_test_app();
std::env::set_var("BK_LEVEL", "997");
app.update();
app.update();
let world = &mut app.world_mut();
let mut target: Option<Entity> = None;
let mut q = world.query::<(
Entity,
&brkrs::BrickTypeId,
Option<&brkrs::CountsTowardsCompletion>,
)>();
for (e, type_id, marker) in q.iter(world) {
if type_id.0 == 90 && marker.is_none() {
target = Some(e);
break;
}
}
let brick = target.expect("expected at least one indestructible (90) brick in test level");
let ball = app.world_mut().spawn((brkrs::Ball,)).id();
let mut collisions = app.world_mut().resource_mut::<Messages<CollisionEvent>>();
collisions.write(CollisionEvent::Started(
ball,
brick,
CollisionEventFlags::empty(),
));
app.update();
app.update();
assert!(
app.world().entities().contains(brick),
"indestructible brick should not be despawned on collision"
);
std::env::remove_var("BK_LEVEL");
}
#[test]
fn k_key_only_destroys_destructible_bricks() {
let mut app = level_test_app();
std::env::set_var("BK_LEVEL", "997");
app.update();
app.update();
let world = &mut app.world_mut();
let mut destructible: Vec<Entity> = Vec::new();
let mut indestructible: Vec<Entity> = Vec::new();
let mut q = world.query::<(
Entity,
&brkrs::BrickTypeId,
Option<&brkrs::CountsTowardsCompletion>,
)>();
for (e, _type_id, marker) in q.iter(world) {
if marker.is_some() {
destructible.push(e);
} else {
indestructible.push(e);
}
}
assert!(
!destructible.is_empty(),
"expected some destructible bricks in level"
);
assert!(
!indestructible.is_empty(),
"expected some indestructible bricks in level"
);
app.update();
{
let mut input = app.world_mut().resource_mut::<ButtonInput<KeyCode>>();
input.press(KeyCode::KeyK);
}
app.update();
app.update();
let world_ref = app.world();
for e in destructible {
assert!(
!world_ref.entities().contains(e),
"destructible brick should be removed by K"
);
}
for e in indestructible {
assert!(
world_ref.entities().contains(e),
"indestructible brick should remain after K"
);
}
std::env::remove_var("BK_LEVEL");
}