mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-08 04:07:52 -06:00
feat: collision helper, ghost/pacman collision events, collision tests
minor format updates from copilot's commit
This commit is contained in:
@@ -6,9 +6,28 @@ use bevy_ecs::system::{Query, Res};
|
|||||||
use crate::error::GameError;
|
use crate::error::GameError;
|
||||||
use crate::events::GameEvent;
|
use crate::events::GameEvent;
|
||||||
use crate::map::builder::Map;
|
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;
|
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<bool, GameError> {
|
||||||
|
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.
|
/// Detects overlapping entities and generates collision events for gameplay systems.
|
||||||
///
|
///
|
||||||
/// Performs distance-based collision detection between Pac-Man and collectible items
|
/// Performs distance-based collision detection between Pac-Man and collectible items
|
||||||
@@ -16,43 +35,50 @@ use crate::systems::movement::Position;
|
|||||||
/// a `GameEvent::Collision` for the item system to handle scoring and removal.
|
/// a `GameEvent::Collision` for the item system to handle scoring and removal.
|
||||||
/// Collision detection accounts for both entities being in motion and supports
|
/// Collision detection accounts for both entities being in motion and supports
|
||||||
/// circular collision boundaries for accurate gameplay feel.
|
/// 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(
|
pub fn collision_system(
|
||||||
map: Res<Map>,
|
map: Res<Map>,
|
||||||
pacman_query: Query<(Entity, &Position, &Collider), With<PacmanCollider>>,
|
pacman_query: Query<(Entity, &Position, &Collider), With<PacmanCollider>>,
|
||||||
item_query: Query<(Entity, &Position, &Collider), With<ItemCollider>>,
|
item_query: Query<(Entity, &Position, &Collider), With<ItemCollider>>,
|
||||||
|
ghost_query: Query<(Entity, &Position, &Collider), With<GhostCollider>>,
|
||||||
mut events: EventWriter<GameEvent>,
|
mut events: EventWriter<GameEvent>,
|
||||||
mut errors: EventWriter<GameError>,
|
mut errors: EventWriter<GameError>,
|
||||||
) {
|
) {
|
||||||
// Check PACMAN × ITEM collisions
|
// Check PACMAN × ITEM collisions
|
||||||
for (pacman_entity, pacman_pos, pacman_collider) in pacman_query.iter() {
|
for (pacman_entity, pacman_pos, pacman_collider) in pacman_query.iter() {
|
||||||
for (item_entity, item_pos, item_collider) in item_query.iter() {
|
for (item_entity, item_pos, item_collider) in item_query.iter() {
|
||||||
match (
|
match check_collision(pacman_pos, pacman_collider, item_pos, item_collider, &map) {
|
||||||
pacman_pos.get_pixel_position(&map.graph),
|
Ok(colliding) => {
|
||||||
item_pos.get_pixel_position(&map.graph),
|
if colliding {
|
||||||
) {
|
|
||||||
(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 {
|
|
||||||
events.write(GameEvent::Collision(pacman_entity, item_entity));
|
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
|
Err(e) => {
|
||||||
(result_a, result_b) => {
|
|
||||||
for result in [result_a, result_b] {
|
|
||||||
if let Err(e) = result {
|
|
||||||
errors.write(GameError::InvalidState(format!(
|
errors.write(GameError::InvalidState(format!(
|
||||||
"Collision system failed to get pixel positions for entities {:?} and {:?}: {}",
|
"Collision system failed to check collision between entities {:?} and {:?}: {}",
|
||||||
pacman_entity, item_entity, e
|
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
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -114,6 +114,14 @@ pub struct Collider {
|
|||||||
pub size: f32,
|
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
|
/// Marker components for collision filtering optimization
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct PacmanCollider;
|
pub struct PacmanCollider;
|
||||||
|
|||||||
150
tests/collision.rs
Normal file
150
tests/collision.rs
Normal file
@@ -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::<GameEvent>::default());
|
||||||
|
world.insert_resource(Events::<GameError>::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");
|
||||||
|
}
|
||||||
@@ -1,14 +1,6 @@
|
|||||||
use pacman::events::{GameCommand, GameEvent};
|
use pacman::events::{GameCommand, GameEvent};
|
||||||
use pacman::map::direction::Direction;
|
use pacman::map::direction::Direction;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_game_command_to_game_event_conversion_all_variants() {
|
fn test_game_command_to_game_event_conversion_all_variants() {
|
||||||
let commands = vec![
|
let commands = vec![
|
||||||
@@ -25,9 +17,3 @@ fn test_game_command_to_game_event_conversion_all_variants() {
|
|||||||
assert_eq!(event, GameEvent::Command(command));
|
assert_eq!(event, GameEvent::Command(command));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -119,8 +119,6 @@ fn test_format_timing_display_basic() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_format_timing_display_units() {
|
fn test_format_timing_display_units() {
|
||||||
let timing_data = vec![
|
let timing_data = vec![
|
||||||
|
|||||||
@@ -193,8 +193,6 @@ fn test_item_system_ignores_non_item_collisions() {
|
|||||||
assert_eq!(ghost_count, 1);
|
assert_eq!(ghost_count, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_item_system_no_collision_events() {
|
fn test_item_system_no_collision_events() {
|
||||||
let mut world = create_test_world();
|
let mut world = create_test_world();
|
||||||
|
|||||||
Reference in New Issue
Block a user