diff --git a/src/entity/ghost.rs b/src/entity/ghost.rs index 43a5c5e..ccf0111 100644 --- a/src/entity/ghost.rs +++ b/src/entity/ghost.rs @@ -4,20 +4,17 @@ //! animation, and rendering. Ghosts move through the game graph using //! a traverser and display directional animated textures. -use glam::Vec2; use pathfinding::prelude::dijkstra; use rand::prelude::*; use smallvec::SmallVec; -use crate::constants::BOARD_PIXEL_OFFSET; use crate::entity::direction::Direction; use crate::entity::graph::{Edge, EdgePermissions, Graph, NodeId}; -use crate::entity::traversal::{Position, Traverser}; -use crate::helpers::centered_with_size; +use crate::entity::r#trait::Entity; +use crate::entity::traversal::Traverser; use crate::texture::animated::AnimatedTexture; use crate::texture::directional::DirectionalAnimatedTexture; use crate::texture::sprite::SpriteAtlas; -use sdl2::render::{Canvas, RenderTarget}; /// Determines if a ghost can traverse a given edge. /// @@ -73,6 +70,42 @@ pub struct Ghost { 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); + } + + self.traverser.advance(graph, dt * 60.0 * self.speed, &can_ghost_traverse); + self.texture.tick(dt); + } +} + impl Ghost { /// Creates a new ghost instance at the specified starting node. /// @@ -113,20 +146,6 @@ impl Ghost { } } - /// Updates the ghost's position and animation state. - /// - /// Advances movement through the graph, updates texture animation, - /// and chooses random directions at intersections. - pub 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); - } - - self.traverser.advance(graph, dt * 60.0 * self.speed, &can_ghost_traverse); - self.texture.tick(dt); - } - /// 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(); @@ -158,24 +177,6 @@ impl Ghost { } } - /// Calculates the current pixel position in the game world. - /// - /// Converts the graph position to screen coordinates, accounting for - /// the board offset and centering the sprite. - fn get_pixel_pos(&self, graph: &Graph) -> Vec2 { - let pos = match self.traverser.position { - Position::AtNode(node_id) => graph.get_node(node_id).unwrap().position, - Position::BetweenNodes { from, to, traversed } => { - let from_pos = graph.get_node(from).unwrap().position; - let to_pos = graph.get_node(to).unwrap().position; - let edge = graph.find_edge(from, to).unwrap(); - from_pos + (to_pos - from_pos) * (traversed / edge.distance) - } - }; - - Vec2::new(pos.x + BOARD_PIXEL_OFFSET.x as f32, pos.y + BOARD_PIXEL_OFFSET.y as f32) - } - /// 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 None if no path exists. @@ -209,26 +210,4 @@ impl Ghost { GhostType::Clyde => sdl2::pixels::Color::RGB(255, 182, 85), // Orange } } - - /// Renders the ghost at its current position. - /// - /// Draws the appropriate directional sprite based on the ghost's - /// current movement state and direction. - pub fn render(&self, canvas: &mut Canvas, atlas: &mut SpriteAtlas, graph: &Graph) { - let pixel_pos = self.get_pixel_pos(graph); - let dest = centered_with_size( - glam::IVec2::new(pixel_pos.x as i32, pixel_pos.y as i32), - glam::UVec2::new(16, 16), - ); - - if self.traverser.position.is_stopped() { - self.texture - .render_stopped(canvas, atlas, dest, self.traverser.direction) - .expect("Failed to render ghost"); - } else { - self.texture - .render(canvas, atlas, dest, self.traverser.direction) - .expect("Failed to render ghost"); - } - } } diff --git a/src/entity/mod.rs b/src/entity/mod.rs index 6af42f9..3539097 100644 --- a/src/entity/mod.rs +++ b/src/entity/mod.rs @@ -2,4 +2,5 @@ pub mod direction; pub mod ghost; pub mod graph; pub mod pacman; +pub mod r#trait; pub mod traversal; diff --git a/src/entity/pacman.rs b/src/entity/pacman.rs index 5e280b9..d773597 100644 --- a/src/entity/pacman.rs +++ b/src/entity/pacman.rs @@ -4,18 +4,14 @@ //! animation, and rendering. Pac-Man moves through the game graph using //! a traverser and displays directional animated textures. -use glam::{UVec2, Vec2}; - -use crate::constants::BOARD_PIXEL_OFFSET; use crate::entity::direction::Direction; use crate::entity::graph::{Edge, EdgePermissions, Graph, NodeId}; -use crate::entity::traversal::{Position, Traverser}; -use crate::helpers::centered_with_size; +use crate::entity::r#trait::Entity; +use crate::entity::traversal::Traverser; use crate::texture::animated::AnimatedTexture; use crate::texture::directional::DirectionalAnimatedTexture; use crate::texture::sprite::SpriteAtlas; use sdl2::keyboard::Keycode; -use sdl2::render::{Canvas, RenderTarget}; /// Determines if Pac-Man can traverse a given edge. /// @@ -35,6 +31,37 @@ pub struct Pacman { texture: DirectionalAnimatedTexture, } +impl Entity for Pacman { + 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 { + 1.125 + } + + fn can_traverse(&self, edge: Edge) -> bool { + can_pacman_traverse(edge) + } + + fn tick(&mut self, dt: f32, graph: &Graph) { + self.traverser.advance(graph, dt * 60.0 * 1.125, &can_pacman_traverse); + self.texture.tick(dt); + } +} + impl Pacman { /// Creates a new Pac-Man instance at the specified starting node. /// @@ -70,15 +97,6 @@ impl Pacman { } } - /// Updates Pac-Man's position and animation state. - /// - /// Advances movement through the graph and updates texture animation. - /// Movement speed is scaled by 60 FPS and a 1.125 multiplier. - pub fn tick(&mut self, dt: f32, graph: &Graph) { - self.traverser.advance(graph, dt * 60.0 * 1.125, &can_pacman_traverse); - self.texture.tick(dt); - } - /// Handles keyboard input to change Pac-Man's direction. /// /// Maps arrow keys to directions and queues the direction change @@ -96,47 +114,4 @@ impl Pacman { self.traverser.set_next_direction(direction); } } - - /// Calculates the current pixel position in the game world. - /// - /// Interpolates between nodes when moving between them. - fn get_pixel_pos(&self, graph: &Graph) -> Vec2 { - match self.traverser.position { - Position::AtNode(node_id) => graph.get_node(node_id).unwrap().position, - Position::BetweenNodes { from, to, traversed } => { - let from_pos = graph.get_node(from).unwrap().position; - let to_pos = graph.get_node(to).unwrap().position; - from_pos.lerp(to_pos, traversed / from_pos.distance(to_pos)) - } - } - } - - /// Returns the current node ID that Pac-Man is at or moving towards. - /// - /// If Pac-Man is at a node, returns that node ID. - /// If Pac-Man is between nodes, returns the node it's moving towards. - pub fn current_node_id(&self) -> NodeId { - match self.traverser.position { - Position::AtNode(node_id) => node_id, - Position::BetweenNodes { to, .. } => to, - } - } - - /// Renders Pac-Man to the canvas. - /// - /// Calculates screen position, determines if Pac-Man is stopped, - /// and renders the appropriate directional texture. - pub fn render(&self, canvas: &mut Canvas, atlas: &mut SpriteAtlas, graph: &Graph) { - let pixel_pos = self.get_pixel_pos(graph).round().as_ivec2() + BOARD_PIXEL_OFFSET.as_ivec2(); - let dest = centered_with_size(pixel_pos, UVec2::new(16, 16)); - let is_stopped = self.traverser.position.is_stopped(); - - if is_stopped { - self.texture - .render_stopped(canvas, atlas, dest, self.traverser.direction) - .unwrap(); - } else { - self.texture.render(canvas, atlas, dest, self.traverser.direction).unwrap(); - } - } } diff --git a/src/entity/trait.rs b/src/entity/trait.rs new file mode 100644 index 0000000..db66e65 --- /dev/null +++ b/src/entity/trait.rs @@ -0,0 +1,108 @@ +//! Entity trait for common movement and rendering functionality. +//! +//! This module defines a trait that captures the shared behavior between +//! different game entities like Ghosts and Pac-Man, including movement, +//! rendering, and position calculations. + +use glam::Vec2; +use sdl2::render::{Canvas, RenderTarget}; + +use crate::entity::direction::Direction; +use crate::entity::graph::{Edge, Graph, NodeId}; +use crate::entity::traversal::{Position, Traverser}; +use crate::texture::directional::DirectionalAnimatedTexture; +use crate::texture::sprite::SpriteAtlas; + +/// Trait defining common functionality for game entities that move through the graph. +/// +/// This trait provides a unified interface for entities that: +/// - Move through the game graph using a traverser +/// - Render using directional animated textures +/// - Have position calculations and movement speed +#[allow(dead_code)] +pub trait Entity { + /// Returns a reference to the entity's traverser for movement control. + fn traverser(&self) -> &Traverser; + + /// Returns a mutable reference to the entity's traverser for movement control. + fn traverser_mut(&mut self) -> &mut Traverser; + + /// Returns a reference to the entity's directional animated texture. + fn texture(&self) -> &DirectionalAnimatedTexture; + + /// Returns a mutable reference to the entity's directional animated texture. + fn texture_mut(&mut self) -> &mut DirectionalAnimatedTexture; + + /// Returns the movement speed multiplier for this entity. + fn speed(&self) -> f32; + + /// Determines if this entity can traverse a given edge. + fn can_traverse(&self, edge: Edge) -> bool; + + /// Updates the entity's position and animation state. + /// + /// This method advances movement through the graph and updates texture animation. + fn tick(&mut self, dt: f32, graph: &Graph); + + /// Calculates the current pixel position in the game world. + /// + /// Converts the graph position to screen coordinates, accounting for + /// the board offset and centering the sprite. + fn get_pixel_pos(&self, graph: &Graph) -> Vec2 { + let pos = match self.traverser().position { + Position::AtNode(node_id) => graph.get_node(node_id).unwrap().position, + Position::BetweenNodes { from, to, traversed } => { + let from_pos = graph.get_node(from).unwrap().position; + let to_pos = graph.get_node(to).unwrap().position; + let edge = graph.find_edge(from, to).unwrap(); + from_pos + (to_pos - from_pos) * (traversed / edge.distance) + } + }; + + Vec2::new( + pos.x + crate::constants::BOARD_PIXEL_OFFSET.x as f32, + pos.y + crate::constants::BOARD_PIXEL_OFFSET.y as f32, + ) + } + + /// Returns the current node ID that the entity is at or moving towards. + /// + /// If the entity is at a node, returns that node ID. + /// If the entity is between nodes, returns the node it's moving towards. + fn current_node_id(&self) -> NodeId { + match self.traverser().position { + Position::AtNode(node_id) => node_id, + Position::BetweenNodes { to, .. } => to, + } + } + + /// Sets the next direction for the entity to take. + /// + /// The direction is buffered and will be applied at the next opportunity, + /// typically when the entity reaches a new node. + fn set_next_direction(&mut self, direction: Direction) { + self.traverser_mut().set_next_direction(direction); + } + + /// Renders the entity at its current position. + /// + /// Draws the appropriate directional sprite based on the entity's + /// current movement state and direction. + fn render(&self, canvas: &mut Canvas, atlas: &mut SpriteAtlas, graph: &Graph) { + let pixel_pos = self.get_pixel_pos(graph); + let dest = crate::helpers::centered_with_size( + glam::IVec2::new(pixel_pos.x as i32, pixel_pos.y as i32), + glam::UVec2::new(16, 16), + ); + + if self.traverser().position.is_stopped() { + self.texture() + .render_stopped(canvas, atlas, dest, self.traverser().direction) + .expect("Failed to render entity"); + } else { + self.texture() + .render(canvas, atlas, dest, self.traverser().direction) + .expect("Failed to render entity"); + } + } +} diff --git a/src/game.rs b/src/game.rs index bc12f00..59c9651 100644 --- a/src/game.rs +++ b/src/game.rs @@ -18,6 +18,7 @@ use crate::{ entity::{ ghost::{Ghost, GhostType}, pacman::Pacman, + r#trait::Entity, }, map::Map, texture::{