diff --git a/src/systems/collision.rs b/src/systems/collision.rs index 572a2e6..c120ef8 100644 --- a/src/systems/collision.rs +++ b/src/systems/collision.rs @@ -6,9 +6,28 @@ use bevy_ecs::system::{Query, Res}; use crate::error::GameError; use crate::events::GameEvent; use crate::map::builder::Map; -use crate::systems::components::{Collider, ItemCollider, PacmanCollider}; +use crate::systems::components::{Collider, GhostCollider, ItemCollider, PacmanCollider}; use crate::systems::movement::Position; +/// Helper function to check collision between two entities with colliders. +pub fn check_collision( + pos1: &Position, + collider1: &Collider, + pos2: &Position, + collider2: &Collider, + map: &Map, +) -> Result { + let pixel1 = pos1 + .get_pixel_position(&map.graph) + .map_err(|e| GameError::InvalidState(format!("Failed to get pixel position for entity 1: {}", e)))?; + let pixel2 = pos2 + .get_pixel_position(&map.graph) + .map_err(|e| GameError::InvalidState(format!("Failed to get pixel position for entity 2: {}", e)))?; + + let distance = pixel1.distance(pixel2); + Ok(collider1.collides_with(collider2.size, distance)) +} + /// Detects overlapping entities and generates collision events for gameplay systems. /// /// Performs distance-based collision detection between Pac-Man and collectible items @@ -16,42 +35,49 @@ use crate::systems::movement::Position; /// a `GameEvent::Collision` for the item system to handle scoring and removal. /// Collision detection accounts for both entities being in motion and supports /// circular collision boundaries for accurate gameplay feel. +/// +/// Also detects collisions between Pac-Man and ghosts for gameplay mechanics like +/// power pellet effects, ghost eating, and player death. pub fn collision_system( map: Res, pacman_query: Query<(Entity, &Position, &Collider), With>, item_query: Query<(Entity, &Position, &Collider), With>, + ghost_query: Query<(Entity, &Position, &Collider), With>, mut events: EventWriter, mut errors: EventWriter, ) { // Check PACMAN × ITEM collisions for (pacman_entity, pacman_pos, pacman_collider) in pacman_query.iter() { for (item_entity, item_pos, item_collider) in item_query.iter() { - match ( - pacman_pos.get_pixel_position(&map.graph), - item_pos.get_pixel_position(&map.graph), - ) { - (Ok(pacman_pixel), Ok(item_pixel)) => { - // Calculate the distance between the two entities's precise pixel positions - let distance = pacman_pixel.distance(item_pixel); - // Calculate the distance at which the two entities will collide - let collision_distance = (pacman_collider.size + item_collider.size) / 2.0; - - // If the distance between the two entities is less than the collision distance, then the two entities are colliding - if distance < collision_distance { + match check_collision(pacman_pos, pacman_collider, item_pos, item_collider, &map) { + Ok(colliding) => { + if colliding { events.write(GameEvent::Collision(pacman_entity, item_entity)); } } - // Either or both of the pixel positions failed to get, so we need to report the error - (result_a, result_b) => { - for result in [result_a, result_b] { - if let Err(e) = result { - errors.write(GameError::InvalidState(format!( - "Collision system failed to get pixel positions for entities {:?} and {:?}: {}", - pacman_entity, item_entity, e - ))); - } + Err(e) => { + errors.write(GameError::InvalidState(format!( + "Collision system failed to check collision between entities {:?} and {:?}: {}", + pacman_entity, item_entity, e + ))); + } + } + } + + // Check PACMAN × GHOST collisions + for (ghost_entity, ghost_pos, ghost_collider) in ghost_query.iter() { + match check_collision(pacman_pos, pacman_collider, ghost_pos, ghost_collider, &map) { + Ok(colliding) => { + if colliding { + events.write(GameEvent::Collision(pacman_entity, ghost_entity)); } } + Err(e) => { + errors.write(GameError::InvalidState(format!( + "Collision system failed to check collision between entities {:?} and {:?}: {}", + pacman_entity, ghost_entity, e + ))); + } } } } diff --git a/src/systems/components.rs b/src/systems/components.rs index a098009..2fd8cec 100644 --- a/src/systems/components.rs +++ b/src/systems/components.rs @@ -114,6 +114,14 @@ pub struct Collider { pub size: f32, } +impl Collider { + /// Checks if this collider collides with another collider at the given distance. + pub fn collides_with(&self, other_size: f32, distance: f32) -> bool { + let collision_distance = (self.size + other_size) / 2.0; + distance < collision_distance + } +} + /// Marker components for collision filtering optimization #[derive(Component)] pub struct PacmanCollider; diff --git a/tests/collision.rs b/tests/collision.rs new file mode 100644 index 0000000..fd0faa0 --- /dev/null +++ b/tests/collision.rs @@ -0,0 +1,150 @@ +use bevy_ecs::{event::Events, prelude::*, system::RunSystemOnce, world::World}; + +use pacman::{ + error::GameError, + events::GameEvent, + map::builder::Map, + systems::{ + collision::{check_collision, collision_system}, + components::{Collider, EntityType, Ghost, GhostCollider, ItemCollider, PacmanCollider}, + movement::Position, + }, +}; + +fn create_test_world() -> World { + let mut world = World::new(); + + // Add required resources + world.insert_resource(Events::::default()); + world.insert_resource(Events::::default()); + + // Add a minimal test map + world.insert_resource(create_test_map()); + + world +} + +fn create_test_map() -> Map { + use pacman::constants::RAW_BOARD; + Map::new(RAW_BOARD).expect("Failed to create test map") +} + +fn spawn_test_pacman(world: &mut World) -> Entity { + world + .spawn((Position::Stopped { node: 0 }, Collider { size: 10.0 }, PacmanCollider)) + .id() +} + +fn spawn_test_item(world: &mut World) -> Entity { + world + .spawn(( + Position::Stopped { node: 0 }, + Collider { size: 8.0 }, + ItemCollider, + EntityType::Pellet, + )) + .id() +} + +fn spawn_test_ghost(world: &mut World) -> Entity { + world + .spawn(( + Position::Stopped { node: 0 }, + Collider { size: 12.0 }, + GhostCollider, + Ghost::Blinky, + EntityType::Ghost, + )) + .id() +} + +fn spawn_test_ghost_at_node(world: &mut World, node: usize) -> Entity { + world + .spawn(( + Position::Stopped { node }, + Collider { size: 12.0 }, + GhostCollider, + Ghost::Blinky, + EntityType::Ghost, + )) + .id() +} + +#[test] +fn test_collider_collision_detection() { + let collider1 = Collider { size: 10.0 }; + let collider2 = Collider { size: 8.0 }; + + // Test collision detection + assert!(collider1.collides_with(collider2.size, 5.0)); // Should collide (distance < 9.0) + assert!(!collider1.collides_with(collider2.size, 15.0)); // Should not collide (distance > 9.0) +} + +#[test] +fn test_check_collision_helper() { + let map = create_test_map(); + let pos1 = Position::Stopped { node: 0 }; + let pos2 = Position::Stopped { node: 0 }; // Same position + let collider1 = Collider { size: 10.0 }; + let collider2 = Collider { size: 8.0 }; + + // Test collision at same position + let result = check_collision(&pos1, &collider1, &pos2, &collider2, &map); + assert!(result.is_ok()); + assert!(result.unwrap()); // Should collide at same position + + // Test collision at different positions + let pos3 = Position::Stopped { node: 1 }; // Different position + let result = check_collision(&pos1, &collider1, &pos3, &collider2, &map); + assert!(result.is_ok()); + // May or may not collide depending on actual node positions +} + +#[test] +fn test_collision_system_pacman_item() { + let mut world = create_test_world(); + let _pacman = spawn_test_pacman(&mut world); + let _item = spawn_test_item(&mut world); + + // Run collision system - should not panic + world + .run_system_once(collision_system) + .expect("System should run successfully"); +} + +#[test] +fn test_collision_system_pacman_ghost() { + let mut world = create_test_world(); + let _pacman = spawn_test_pacman(&mut world); + let _ghost = spawn_test_ghost(&mut world); + + // Run collision system - should not panic + world + .run_system_once(collision_system) + .expect("System should run successfully"); +} + +#[test] +fn test_collision_system_no_collision() { + let mut world = create_test_world(); + let _pacman = spawn_test_pacman(&mut world); + let _ghost = spawn_test_ghost_at_node(&mut world, 1); // Different node + + // Run collision system - should not panic + world + .run_system_once(collision_system) + .expect("System should run successfully"); +} + +#[test] +fn test_collision_system_multiple_entities() { + let mut world = create_test_world(); + let _pacman = spawn_test_pacman(&mut world); + let _item = spawn_test_item(&mut world); + let _ghost = spawn_test_ghost(&mut world); + + // Run collision system - should not panic + world + .run_system_once(collision_system) + .expect("System should run successfully"); +} diff --git a/tests/events.rs b/tests/events.rs index 791e7cc..1af42ad 100644 --- a/tests/events.rs +++ b/tests/events.rs @@ -1,14 +1,6 @@ use pacman::events::{GameCommand, GameEvent}; use pacman::map::direction::Direction; - - - - - - - - #[test] fn test_game_command_to_game_event_conversion_all_variants() { let commands = vec![ @@ -25,9 +17,3 @@ fn test_game_command_to_game_event_conversion_all_variants() { assert_eq!(event, GameEvent::Command(command)); } } - - - - - - diff --git a/tests/formatting.rs b/tests/formatting.rs index 404ae8c..91e6a22 100644 --- a/tests/formatting.rs +++ b/tests/formatting.rs @@ -119,8 +119,6 @@ fn test_format_timing_display_basic() { } } - - #[test] fn test_format_timing_display_units() { let timing_data = vec![ diff --git a/tests/item.rs b/tests/item.rs index 37e1d13..6c739df 100644 --- a/tests/item.rs +++ b/tests/item.rs @@ -193,8 +193,6 @@ fn test_item_system_ignores_non_item_collisions() { assert_eq!(ghost_count, 1); } - - #[test] fn test_item_system_no_collision_events() { let mut world = create_test_world();