use crate::systems::components::{DirectionalAnimated, Frozen, GhostState, LastAnimationState}; use crate::{ map::{ builder::Map, direction::Direction, graph::{Edge, TraversalFlags}, }, systems::{ components::{DeltaTime, Ghost}, movement::{Position, Velocity}, }, }; use crate::systems::GhostAnimations; use bevy_ecs::query::Without; use bevy_ecs::system::{Query, Res}; use rand::rngs::SmallRng; use rand::seq::IndexedRandom; use rand::SeedableRng; use smallvec::SmallVec; /// Autonomous ghost AI system implementing randomized movement with backtracking avoidance. pub fn ghost_movement_system( map: Res, delta_time: Res, mut ghosts: Query<(&Ghost, &mut Velocity, &mut Position), Without>, ) { for (_ghost, mut velocity, mut position) in ghosts.iter_mut() { let mut distance = velocity.speed * 60.0 * delta_time.0; loop { match *position { Position::Stopped { node: current_node } => { let intersection = &map.graph.adjacency_list[current_node as usize]; let opposite = velocity.direction.opposite(); let mut non_opposite_options: SmallVec<[Edge; 3]> = SmallVec::new(); // Collect all available directions that ghosts can traverse for edge in Direction::DIRECTIONS.iter().flat_map(|d| intersection.get(*d)) { if edge.traversal_flags.contains(TraversalFlags::GHOST) && edge.direction != opposite { non_opposite_options.push(edge); } } let new_edge: Edge = if non_opposite_options.is_empty() { if let Some(edge) = intersection.get(opposite) { edge } else { break; } } else { *non_opposite_options.choose(&mut SmallRng::from_os_rng()).unwrap() }; velocity.direction = new_edge.direction; *position = Position::Moving { from: current_node, to: new_edge.target, remaining_distance: new_edge.distance, }; } Position::Moving { .. } => { if let Some(overflow) = position.tick(distance) { distance = overflow; } else { break; } } } } } } /// System that handles eaten ghost behavior and respawn logic. /// /// When a ghost is eaten by Pac-Man, it enters an "eaten" state where: /// 1. It displays eyes-only animation /// 2. It moves directly back to the ghost house at increased speed /// 3. Once it reaches the ghost house center, it respawns as a normal ghost /// /// This system runs after the main movement system to override eaten ghost movement. pub fn eaten_ghost_system( map: Res, delta_time: Res, mut eaten_ghosts: Query<(&Ghost, &mut Position, &mut Velocity, &mut GhostState)>, ) { for (ghost_type, mut position, mut velocity, mut ghost_state) in eaten_ghosts.iter_mut() { // Only process ghosts that are in Eyes state if !matches!(*ghost_state, GhostState::Eyes) { continue; } // Set higher speed for eaten ghosts returning to ghost house let original_speed = velocity.speed; velocity.speed = ghost_type.base_speed() * 2.0; // Move twice as fast when eaten // Calculate direction towards ghost house center (using Clyde's start position) let ghost_house_center = map.start_positions.clyde; match *position { Position::Stopped { node: current_node } => { // Find path to ghost house center and start moving if let Some(direction) = find_direction_to_target(&map, current_node, ghost_house_center) { velocity.direction = direction; *position = Position::Moving { from: current_node, to: map.graph.adjacency_list[current_node as usize].get(direction).unwrap().target, remaining_distance: map.graph.adjacency_list[current_node as usize] .get(direction) .unwrap() .distance, }; } } Position::Moving { to, .. } => { let distance = velocity.speed * 60.0 * delta_time.0; if let Some(_overflow) = position.tick(distance) { // Reached target node, check if we're at ghost house center if to == ghost_house_center { // Respawn the ghost - set state back to normal *ghost_state = GhostState::Normal; // Reset to stopped at ghost house center *position = Position::Stopped { node: ghost_house_center, }; } else { // Continue pathfinding to ghost house if let Some(next_direction) = find_direction_to_target(&map, to, ghost_house_center) { velocity.direction = next_direction; *position = Position::Moving { from: to, to: map.graph.adjacency_list[to as usize].get(next_direction).unwrap().target, remaining_distance: map.graph.adjacency_list[to as usize].get(next_direction).unwrap().distance, }; } } } } } // Restore original speed velocity.speed = original_speed; } } /// Helper function to find the direction from a node towards a target node. /// Uses simple greedy pathfinding - prefers straight lines when possible. fn find_direction_to_target( map: &Map, from_node: crate::systems::movement::NodeId, target_node: crate::systems::movement::NodeId, ) -> Option { let from_pos = map.graph.get_node(from_node).unwrap().position; let target_pos = map.graph.get_node(target_node).unwrap().position; let dx = target_pos.x as i32 - from_pos.x as i32; let dy = target_pos.y as i32 - from_pos.y as i32; // Prefer horizontal movement first, then vertical let preferred_dirs = if dx.abs() > dy.abs() { if dx > 0 { [Direction::Right, Direction::Up, Direction::Down, Direction::Left] } else { [Direction::Left, Direction::Up, Direction::Down, Direction::Right] } } else if dy > 0 { [Direction::Down, Direction::Left, Direction::Right, Direction::Up] } else { [Direction::Up, Direction::Left, Direction::Right, Direction::Down] }; // Return first available direction towards target for direction in preferred_dirs { if let Some(edge) = map.graph.adjacency_list[from_node as usize].get(direction) { if edge.traversal_flags.contains(TraversalFlags::GHOST) { return Some(direction); } } } None } /// Unified system that manages ghost state transitions and animations pub fn ghost_state_system( animations: Res, mut ghosts: Query<(&Ghost, &mut GhostState, &mut DirectionalAnimated, &mut LastAnimationState)>, ) { for (ghost_type, mut ghost_state, mut directional_animated, mut last_animation_state) in ghosts.iter_mut() { // Tick the ghost state to handle internal transitions (like flashing) let _ = ghost_state.tick(); // Only update animation if the animation state actually changed let current_animation_state = ghost_state.animation_state(); if last_animation_state.0 != current_animation_state { let animation_set = animations.0.get(ghost_type).unwrap(); let animation = animation_set.get(current_animation_state).unwrap(); *directional_animated = (*animation).clone(); // Reset animation timers to synchronize all ghosts directional_animated.reset_all_animations(); last_animation_state.0 = current_animation_state; } } }