// //! Ghost entity implementation. // //! // //! This module contains the ghost character logic, including movement, // //! animation, and rendering. Ghosts move through the game graph using // //! a traverser and display directional animated textures. // use pathfinding::prelude::dijkstra; // use rand::prelude::*; // use smallvec::SmallVec; // use tracing::error; // use crate::entity::{ // collision::Collidable, // direction::Direction, // graph::{Edge, EdgePermissions, Graph, NodeId}, // r#trait::Entity, // traversal::Traverser, // }; // use crate::texture::animated::AnimatedTexture; // use crate::texture::directional::DirectionalAnimatedTexture; // use crate::texture::sprite::SpriteAtlas; // use crate::error::{EntityError, GameError, GameResult, TextureError}; // /// Determines if a ghost can traverse a given edge. // /// // /// Ghosts can move through edges that allow all entities or ghost-only edges. // fn can_ghost_traverse(edge: Edge) -> bool { // matches!(edge.permissions, EdgePermissions::All | EdgePermissions::GhostsOnly) // } // /// The four classic ghost types. // #[derive(Debug, Clone, Copy, PartialEq, Eq)] // pub enum GhostType { // Blinky, // Pinky, // Inky, // Clyde, // } // impl GhostType { // /// Returns the ghost type name for atlas lookups. // pub fn as_str(self) -> &'static str { // match self { // GhostType::Blinky => "blinky", // GhostType::Pinky => "pinky", // GhostType::Inky => "inky", // GhostType::Clyde => "clyde", // } // } // /// Returns the base movement speed for this ghost type. // pub fn base_speed(self) -> f32 { // match self { // GhostType::Blinky => 1.0, // GhostType::Pinky => 0.95, // GhostType::Inky => 0.9, // GhostType::Clyde => 0.85, // } // } // } // /// A ghost entity that roams the game world. // /// // /// Ghosts move through the game world using a graph-based navigation system // /// and display directional animated sprites. They randomly choose directions // /// at each intersection. // pub struct Ghost { // /// Handles movement through the game graph // pub traverser: Traverser, // /// The type of ghost (affects appearance and speed) // pub ghost_type: GhostType, // /// Manages directional animated textures for different movement states // texture: DirectionalAnimatedTexture, // /// Current movement speed // speed: f32, // } // impl Entity for Ghost { // fn traverser(&self) -> &Traverser { // &self.traverser // } // fn traverser_mut(&mut self) -> &mut Traverser { // &mut self.traverser // } // fn texture(&self) -> &DirectionalAnimatedTexture { // &self.texture // } // fn texture_mut(&mut self) -> &mut DirectionalAnimatedTexture { // &mut self.texture // } // fn speed(&self) -> f32 { // self.speed // } // fn can_traverse(&self, edge: Edge) -> bool { // can_ghost_traverse(edge) // } // fn tick(&mut self, dt: f32, graph: &Graph) { // // Choose random direction when at a node // if self.traverser.position.is_at_node() { // self.choose_random_direction(graph); // } // if let Err(e) = self.traverser.advance(graph, dt * 60.0 * self.speed, &can_ghost_traverse) { // error!("Ghost movement error: {}", e); // } // self.texture.tick(dt); // } // } // impl Ghost { // /// Creates a new ghost instance at the specified starting node. // /// // /// Sets up animated textures for all four directions with moving and stopped states. // /// The moving animation cycles through two sprite variants. // pub fn new(graph: &Graph, start_node: NodeId, ghost_type: GhostType, atlas: &SpriteAtlas) -> GameResult { // let mut textures = [None, None, None, None]; // let mut stopped_textures = [None, None, None, None]; // for direction in Direction::DIRECTIONS { // let moving_prefix = match direction { // Direction::Up => "up", // Direction::Down => "down", // Direction::Left => "left", // Direction::Right => "right", // }; // let moving_tiles = vec![ // SpriteAtlas::get_tile(atlas, &format!("ghost/{}/{}_{}.png", ghost_type.as_str(), moving_prefix, "a")) // .ok_or_else(|| { // GameError::Texture(TextureError::AtlasTileNotFound(format!( // "ghost/{}/{}_{}.png", // ghost_type.as_str(), // moving_prefix, // "a" // ))) // })?, // SpriteAtlas::get_tile(atlas, &format!("ghost/{}/{}_{}.png", ghost_type.as_str(), moving_prefix, "b")) // .ok_or_else(|| { // GameError::Texture(TextureError::AtlasTileNotFound(format!( // "ghost/{}/{}_{}.png", // ghost_type.as_str(), // moving_prefix, // "b" // ))) // })?, // ]; // let stopped_tiles = // vec![ // SpriteAtlas::get_tile(atlas, &format!("ghost/{}/{}_{}.png", ghost_type.as_str(), moving_prefix, "a")) // .ok_or_else(|| { // GameError::Texture(TextureError::AtlasTileNotFound(format!( // "ghost/{}/{}_{}.png", // ghost_type.as_str(), // moving_prefix, // "a" // ))) // })?, // ]; // textures[direction.as_usize()] = Some(AnimatedTexture::new(moving_tiles, 0.2)?); // stopped_textures[direction.as_usize()] = Some(AnimatedTexture::new(stopped_tiles, 0.1)?); // } // Ok(Self { // traverser: Traverser::new(graph, start_node, Direction::Left, &can_ghost_traverse), // ghost_type, // texture: DirectionalAnimatedTexture::new(textures, stopped_textures), // speed: ghost_type.base_speed(), // }) // } // /// Chooses a random available direction at the current intersection. // fn choose_random_direction(&mut self, graph: &Graph) { // let current_node = self.traverser.position.from_node_id(); // let intersection = &graph.adjacency_list[current_node]; // // Collect all available directions // let mut available_directions = SmallVec::<[_; 4]>::new(); // for direction in Direction::DIRECTIONS { // if let Some(edge) = intersection.get(direction) { // if can_ghost_traverse(edge) { // available_directions.push(direction); // } // } // } // // Choose a random direction (avoid reversing unless necessary) // if !available_directions.is_empty() { // let mut rng = SmallRng::from_os_rng(); // // Filter out the opposite direction if possible, but allow it if we have limited options // let opposite = self.traverser.direction.opposite(); // let filtered_directions: Vec<_> = available_directions // .iter() // .filter(|&&dir| dir != opposite || available_directions.len() <= 2) // .collect(); // if let Some(&random_direction) = filtered_directions.choose(&mut rng) { // self.traverser.set_next_direction(*random_direction); // } // } // } // /// Calculates the shortest path from the ghost's current position to a target node using Dijkstra's algorithm. // /// // /// Returns a vector of NodeIds representing the path, or an error if pathfinding fails. // /// The path includes the current node and the target node. // pub fn calculate_path_to_target(&self, graph: &Graph, target: NodeId) -> GameResult> { // let start_node = self.traverser.position.from_node_id(); // // Use Dijkstra's algorithm to find the shortest path // let result = dijkstra( // &start_node, // |&node_id| { // // Get all edges from the current node // graph.adjacency_list[node_id] // .edges() // .filter(|edge| can_ghost_traverse(*edge)) // .map(|edge| (edge.target, (edge.distance * 100.0) as u32)) // .collect::>() // }, // |&node_id| node_id == target, // ); // result.map(|(path, _cost)| path).ok_or_else(|| { // GameError::Entity(EntityError::PathfindingFailed(format!( // "No path found from node {} to target {}", // start_node, target // ))) // }) // } // /// Returns the ghost's color for debug rendering. // pub fn debug_color(&self) -> sdl2::pixels::Color { // match self.ghost_type { // GhostType::Blinky => sdl2::pixels::Color::RGB(255, 0, 0), // Red // GhostType::Pinky => sdl2::pixels::Color::RGB(255, 182, 255), // Pink // GhostType::Inky => sdl2::pixels::Color::RGB(0, 255, 255), // Cyan // GhostType::Clyde => sdl2::pixels::Color::RGB(255, 182, 85), // Orange // } // } // } // impl Collidable for Ghost { // fn position(&self) -> crate::entity::traversal::Position { // self.traverser.position // } // }