use bevy_ecs::component::Component; use bevy_ecs::entity::Entity; use bevy_ecs::event::{EventReader, EventWriter}; use bevy_ecs::query::With; use bevy_ecs::system::{Query, Res, ResMut}; use crate::error::GameError; use crate::events::GameEvent; use crate::map::builder::Map; use crate::systems::movement::Position; use crate::systems::{AudioEvent, Ghost, GhostState, PlayerControlled, ScoreResource}; #[derive(Component)] 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; #[derive(Component)] pub struct GhostCollider; #[derive(Component)] pub struct ItemCollider; /// 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 /// using each entity's position and collision radius. When entities overlap, emits /// 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 check_collision(pacman_pos, pacman_collider, item_pos, item_collider, &map) { Ok(colliding) => { if colliding { events.write(GameEvent::Collision(pacman_entity, item_entity)); } } 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 ))); } } } } } pub fn ghost_collision_system( mut collision_events: EventReader, mut score: ResMut, pacman_query: Query<(), With>, ghost_query: Query<(Entity, &Ghost), With>, mut ghost_state_query: Query<&mut GhostState>, mut events: EventWriter, ) { for event in collision_events.read() { if let GameEvent::Collision(entity1, entity2) = event { // Check if one is Pacman and the other is a ghost let (_pacman_entity, ghost_entity) = if pacman_query.get(*entity1).is_ok() && ghost_query.get(*entity2).is_ok() { (*entity1, *entity2) } else if pacman_query.get(*entity2).is_ok() && ghost_query.get(*entity1).is_ok() { (*entity2, *entity1) } else { continue; }; // Check if the ghost is frightened if let Ok((ghost_ent, _ghost_type)) = ghost_query.get(ghost_entity) { if let Ok(mut ghost_state) = ghost_state_query.get_mut(ghost_ent) { // Check if ghost is in frightened state if matches!(*ghost_state, GhostState::Frightened { .. }) { // Pac-Man eats the ghost // Add score (200 points per ghost eaten) score.0 += 200; // Set ghost state to Eyes *ghost_state = GhostState::Eyes; // Play eat sound events.write(AudioEvent::PlayEat); } else { // Pac-Man dies (this would need a death system) // For now, just log it tracing::warn!("Pac-Man collided with ghost while not frightened!"); } } } } } }