diff --git a/src/entity.rs b/src/entity.rs index 4a98482..bcd3447 100644 --- a/src/entity.rs +++ b/src/entity.rs @@ -3,14 +3,68 @@ /// A trait for game objects that can be moved and rendered. pub trait Entity { + /// Returns a reference to the base MovableEntity. + fn base(&self) -> &MovableEntity; + /// Returns true if the entity is colliding with the other entity. fn is_colliding(&self, other: &dyn Entity) -> bool; - /// Returns the absolute position of the entity, in pixels. - fn position(&self) -> (i32, i32); - /// Returns the cell position of the entity, in grid coordinates. - fn cell_position(&self) -> (u32, u32); - /// Returns the position of the entity within its current cell, in pixels. - fn internal_position(&self) -> (u32, u32); + /// Ticks the entity, which updates its state and position. fn tick(&mut self); } + +/// A struct for movable game entities with position, direction, speed, and modulation. +pub struct MovableEntity { + /// The absolute position of the entity on the board, in pixels. + pub pixel_position: (i32, i32), + /// The position of the entity on the board, in grid coordinates. + pub cell_position: (u32, u32), + /// The current direction of the entity. + pub direction: crate::direction::Direction, + /// Movement speed (pixels per tick). + pub speed: u32, + /// Movement modulator for controlling speed. + pub modulation: crate::modulation::SimpleTickModulator, + /// Whether the entity is currently in a tunnel. + pub in_tunnel: bool, +} + +impl MovableEntity { + /// Creates a new MovableEntity. + pub fn new( + pixel_position: (i32, i32), + cell_position: (u32, u32), + direction: crate::direction::Direction, + speed: u32, + modulation: crate::modulation::SimpleTickModulator, + ) -> Self { + Self { + pixel_position, + cell_position, + direction, + speed, + modulation, + in_tunnel: false, + } + } + + /// Returns the position within the current cell, in pixels. + pub fn internal_position(&self) -> (u32, u32) { + ( + self.pixel_position.0 as u32 % crate::constants::CELL_SIZE, + self.pixel_position.1 as u32 % crate::constants::CELL_SIZE, + ) + } + + /// Move the entity in its current direction by its speed. + pub fn move_forward(&mut self) { + let speed = self.speed as i32; + use crate::direction::Direction; + match self.direction { + Direction::Right => self.pixel_position.0 += speed, + Direction::Left => self.pixel_position.0 -= speed, + Direction::Up => self.pixel_position.1 -= speed, + Direction::Down => self.pixel_position.1 += speed, + } + } +} diff --git a/src/game.rs b/src/game.rs index 046efe4..e68223f 100644 --- a/src/game.rs +++ b/src/game.rs @@ -187,10 +187,10 @@ impl Game<'_> { // Reset Pacman position let mut pacman = self.pacman.borrow_mut(); - pacman.pixel_position = Map::cell_to_pixel((1, 1)); - pacman.cell_position = (1, 1); - pacman.in_tunnel = false; - pacman.direction = Direction::Right; + pacman.base.pixel_position = Map::cell_to_pixel((1, 1)); + pacman.base.cell_position = (1, 1); + pacman.base.in_tunnel = false; + pacman.base.direction = Direction::Right; pacman.next_direction = None; pacman.stopped = false; @@ -213,10 +213,10 @@ impl Game<'_> { } } if let Some(&(gx, gy)) = valid_positions.iter().choose(&mut rng) { - self.blinky.pixel_position = Map::cell_to_pixel((gx, gy)); - self.blinky.cell_position = (gx, gy); - self.blinky.in_tunnel = false; - self.blinky.direction = Direction::Left; + self.blinky.base.pixel_position = Map::cell_to_pixel((gx, gy)); + self.blinky.base.cell_position = (gx, gy); + self.blinky.base.in_tunnel = false; + self.blinky.base.direction = Direction::Left; self.blinky.mode = crate::ghost::GhostMode::Chase; } } @@ -231,7 +231,7 @@ impl Game<'_> { /// Checks if Pac-Man is currently eating a pellet and updates the game state /// accordingly. fn check_pellet_eating(&mut self) { - let cell_pos = self.pacman.borrow().cell_position(); + let cell_pos = self.pacman.borrow().base.cell_position; // Check if there's a pellet at the current position let tile = { @@ -319,7 +319,7 @@ impl Game<'_> { .unwrap_or(MapTile::Empty); let mut color = None; - if (x, y) == self.pacman.borrow().cell_position() { + if (x, y) == self.pacman.borrow().base.cell_position { self.draw_cell((x, y), Color::CYAN); } else { color = match tile { diff --git a/src/ghost.rs b/src/ghost.rs index 719ed7a..8ce08a4 100644 --- a/src/ghost.rs +++ b/src/ghost.rs @@ -13,7 +13,7 @@ use crate::{ animation::AnimatedTexture, constants::{MapTile, BOARD_OFFSET, BOARD_WIDTH, CELL_SIZE}, direction::Direction, - entity::Entity, + entity::{Entity, MovableEntity}, map::Map, modulation::{SimpleTickModulator, TickModulator}, pacman::Pacman, @@ -57,12 +57,8 @@ impl GhostType { /// Base ghost struct that contains common functionality pub struct Ghost<'a> { - /// The absolute position of the ghost on the board, in pixels - pub pixel_position: (i32, i32), - /// The position of the ghost on the board, in grid coordinates - pub cell_position: (u32, u32), - /// The current direction of the ghost - pub direction: Direction, + /// Shared movement and position fields. + pub base: MovableEntity, /// The current mode of the ghost pub mode: GhostMode, /// The type/personality of this ghost @@ -71,16 +67,10 @@ pub struct Ghost<'a> { pub map: Rc>, /// Reference to Pac-Man for targeting pub pacman: Rc>>, - /// Movement speed - speed: u32, - /// Movement modulator - modulation: SimpleTickModulator, /// Ghost body sprite body_sprite: AnimatedTexture<'a>, /// Ghost eyes sprite eyes_sprite: AnimatedTexture<'a>, - /// Whether the ghost is currently in a tunnel - pub in_tunnel: bool, } impl Ghost<'_> { @@ -96,20 +86,21 @@ impl Ghost<'_> { let color = ghost_type.color(); let mut body_sprite = AnimatedTexture::new(body_texture, 8, 2, 32, 32, Some((-4, -4))); body_sprite.set_color_modulation(color.r, color.g, color.b); - + let pixel_position = Map::cell_to_pixel(starting_position); Ghost { - pixel_position: Map::cell_to_pixel(starting_position), - cell_position: starting_position, - direction: Direction::Left, + base: MovableEntity::new( + pixel_position, + starting_position, + Direction::Left, + 3, + SimpleTickModulator::new(1.0), + ), mode: GhostMode::Chase, ghost_type, map, pacman, - speed: 3, - modulation: SimpleTickModulator::new(1.0), body_sprite, eyes_sprite: AnimatedTexture::new(eyes_texture, 1, 4, 32, 32, Some((-4, -4))), - in_tunnel: false, } } @@ -126,14 +117,14 @@ impl Ghost<'_> { self.body_sprite .set_color_modulation(color.r, color.g, color.b); self.body_sprite - .render(canvas, self.pixel_position, Direction::Right); + .render(canvas, self.base.pixel_position, Direction::Right); } // Always render eyes on top let eye_frame = if self.mode == GhostMode::Frightened { 4 // Frightened frame } else { - match self.direction { + match self.base.direction { Direction::Right => 0, Direction::Up => 1, Direction::Left => 2, @@ -143,7 +134,7 @@ impl Ghost<'_> { self.eyes_sprite.render_static( canvas, - self.pixel_position, + self.base.pixel_position, Direction::Right, Some(eye_frame), ); @@ -151,7 +142,7 @@ impl Ghost<'_> { /// Calculates the path to the target tile using the A* algorithm. pub fn get_path_to_target(&self, target: (u32, u32)) -> Option<(Vec<(u32, u32)>, u32)> { - let start = self.cell_position; + let start = self.base.cell_position; let map = self.map.borrow(); dijkstra( @@ -211,8 +202,8 @@ impl Ghost<'_> { /// Gets a random adjacent tile for frightened mode fn get_random_target(&self) -> (i32, i32) { - let mut rng = rand::thread_rng(); - let (x, y) = self.cell_position; + let mut rng = rand::rng(); + let (x, y) = self.base.cell_position; let mut possible_moves = Vec::new(); // Check all four directions @@ -223,7 +214,7 @@ impl Ghost<'_> { Direction::Right, ] { // Don't allow reversing direction - if *dir == self.direction.opposite() { + if *dir == self.base.direction.opposite() { continue; } @@ -239,7 +230,7 @@ impl Ghost<'_> { if possible_moves.is_empty() { // No valid moves, must reverse - let (dx, dy) = self.direction.opposite().offset(); + let (dx, dy) = self.base.direction.opposite().offset(); return (x as i32 + dx, y as i32 + dy); } @@ -261,7 +252,8 @@ impl Ghost<'_> { fn get_chase_target(&self) -> (i32, i32) { // Default implementation just targets Pac-Man directly let pacman = self.pacman.borrow(); - (pacman.cell_position.0 as i32, pacman.cell_position.1 as i32) + let cell = pacman.base.cell_position; + (cell.0 as i32, cell.1 as i32) } /// Changes the ghost's mode and handles direction reversal @@ -273,66 +265,70 @@ impl Ghost<'_> { self.mode = new_mode; + self.base.speed = match new_mode { + GhostMode::Chase => 3, + GhostMode::Scatter => 2, + GhostMode::Frightened => 2, + GhostMode::Eyes => 7, + GhostMode::House => 0, + }; + if should_reverse { - self.direction = self.direction.opposite(); + self.base.direction = self.base.direction.opposite(); } } } impl Entity for Ghost<'_> { - fn position(&self) -> (i32, i32) { - self.pixel_position - } - - fn cell_position(&self) -> (u32, u32) { - self.cell_position - } - - fn internal_position(&self) -> (u32, u32) { - let (x, y) = self.position(); - (x as u32 % CELL_SIZE, y as u32 % CELL_SIZE) + fn base(&self) -> &MovableEntity { + &self.base } + /// Returns true if the ghost entity is colliding with the other entity. fn is_colliding(&self, other: &dyn Entity) -> bool { - let (x, y) = self.position(); - let (other_x, other_y) = other.position(); + let (x, y) = self.base.pixel_position; + let (other_x, other_y) = other.base().pixel_position; x == other_x && y == other_y } + /// Ticks the ghost entity. fn tick(&mut self) { if self.mode == GhostMode::House { // For now, do nothing in the house return; } - if self.internal_position() == (0, 0) { - self.cell_position = ( - (self.pixel_position.0 as u32 / CELL_SIZE) - BOARD_OFFSET.0, - (self.pixel_position.1 as u32 / CELL_SIZE) - BOARD_OFFSET.1, + if self.base.internal_position() == (0, 0) { + self.base.cell_position = ( + (self.base.pixel_position.0 as u32 / CELL_SIZE) - BOARD_OFFSET.0, + (self.base.pixel_position.1 as u32 / CELL_SIZE) - BOARD_OFFSET.1, ); let current_tile = self .map .borrow() - .get_tile((self.cell_position.0 as i32, self.cell_position.1 as i32)) + .get_tile(( + self.base.cell_position.0 as i32, + self.base.cell_position.1 as i32, + )) .unwrap_or(MapTile::Empty); if current_tile == MapTile::Tunnel { - self.in_tunnel = true; + self.base.in_tunnel = true; } // Tunnel logic: if in tunnel, force movement and prevent direction change - if self.in_tunnel { + if self.base.in_tunnel { // If out of bounds, teleport to the opposite side and exit tunnel - if self.cell_position.0 == 0 { - self.cell_position.0 = BOARD_WIDTH - 2; - self.pixel_position = - Map::cell_to_pixel((self.cell_position.0, self.cell_position.1)); - self.in_tunnel = false; - } else if self.cell_position.0 == BOARD_WIDTH - 1 { - self.cell_position.0 = 1; - self.pixel_position = - Map::cell_to_pixel((self.cell_position.0, self.cell_position.1)); - self.in_tunnel = false; + if self.base.cell_position.0 == 0 { + self.base.cell_position.0 = BOARD_WIDTH - 2; + self.base.pixel_position = + Map::cell_to_pixel((self.base.cell_position.0, self.base.cell_position.1)); + self.base.in_tunnel = false; + } else if self.base.cell_position.0 == BOARD_WIDTH - 1 { + self.base.cell_position.0 = 1; + self.base.pixel_position = + Map::cell_to_pixel((self.base.cell_position.0, self.base.cell_position.1)); + self.base.in_tunnel = false; } else { // While in tunnel, do not allow direction change // and always move in the current direction @@ -345,10 +341,10 @@ impl Entity for Ghost<'_> { { if path.len() > 1 { let next_move = path[1]; - let (x, y) = self.cell_position; + let (x, y) = self.base.cell_position; let dx = next_move.0 as i32 - x as i32; let dy = next_move.1 as i32 - y as i32; - self.direction = if dx > 0 { + self.base.direction = if dx > 0 { Direction::Right } else if dx < 0 { Direction::Left @@ -362,10 +358,10 @@ impl Entity for Ghost<'_> { } // Check if the next tile in the current direction is a wall - let (dx, dy) = self.direction.offset(); + let (dx, dy) = self.base.direction.offset(); let next_cell = ( - self.cell_position.0 as i32 + dx, - self.cell_position.1 as i32 + dy, + self.base.cell_position.0 as i32 + dx, + self.base.cell_position.1 as i32 + dy, ); let next_tile = self .map @@ -378,24 +374,18 @@ impl Entity for Ghost<'_> { } } - if !self.modulation.next() { + if !self.base.modulation.next() { return; } // Update position based on current direction and speed - let speed = self.speed as i32; - match self.direction { - Direction::Right => self.pixel_position.0 += speed, - Direction::Left => self.pixel_position.0 -= speed, - Direction::Up => self.pixel_position.1 -= speed, - Direction::Down => self.pixel_position.1 += speed, - } + self.base.move_forward(); // Update cell position when aligned with grid - if self.internal_position() == (0, 0) { - self.cell_position = ( - (self.pixel_position.0 as u32 / CELL_SIZE) - BOARD_OFFSET.0, - (self.pixel_position.1 as u32 / CELL_SIZE) - BOARD_OFFSET.1, + if self.base.internal_position() == (0, 0) { + self.base.cell_position = ( + (self.base.pixel_position.0 as u32 / CELL_SIZE) - BOARD_OFFSET.0, + (self.base.pixel_position.1 as u32 / CELL_SIZE) - BOARD_OFFSET.1, ); } } diff --git a/src/ghosts/blinky.rs b/src/ghosts/blinky.rs index 38b03a3..ed77a63 100644 --- a/src/ghosts/blinky.rs +++ b/src/ghosts/blinky.rs @@ -4,6 +4,7 @@ use std::rc::Rc; use sdl2::render::{Canvas, Texture}; use sdl2::video::Window; +use crate::entity::MovableEntity; use crate::{ entity::Entity, ghost::{Ghost, GhostMode, GhostType}, @@ -38,7 +39,8 @@ impl<'a> Blinky<'a> { /// Gets Blinky's chase target - directly targets Pac-Man's current position fn get_chase_target(&self) -> (i32, i32) { let pacman = self.ghost.pacman.borrow(); - (pacman.cell_position.0 as i32, pacman.cell_position.1 as i32) + let cell = pacman.base.cell_position; + (cell.0 as i32, cell.1 as i32) } pub fn set_mode(&mut self, mode: GhostMode) { @@ -51,16 +53,8 @@ impl<'a> Blinky<'a> { } impl<'a> Entity for Blinky<'a> { - fn position(&self) -> (i32, i32) { - self.ghost.position() - } - - fn cell_position(&self) -> (u32, u32) { - self.ghost.cell_position() - } - - fn internal_position(&self) -> (u32, u32) { - self.ghost.internal_position() + fn base(&self) -> &MovableEntity { + self.ghost.base() } fn is_colliding(&self, other: &dyn Entity) -> bool { diff --git a/src/pacman.rs b/src/pacman.rs index 6bf3c8a..53ab9cf 100644 --- a/src/pacman.rs +++ b/src/pacman.rs @@ -13,81 +13,65 @@ use crate::{ constants::MapTile, constants::{BOARD_OFFSET, BOARD_WIDTH, CELL_SIZE}, direction::Direction, - entity::Entity, + entity::{Entity, MovableEntity}, map::Map, modulation::{SimpleTickModulator, TickModulator}, }; /// The Pac-Man entity. pub struct Pacman<'a> { - /// The absolute position of Pac-Man on the board, in pixels. - pub pixel_position: (i32, i32), - /// The position of Pac-Man on the board, in grid coordinates. - /// This is only updated at the moment Pac-Man is aligned with the grid. - pub cell_position: (u32, u32), - /// The current direction of Pac-Man. - pub direction: Direction, + /// Shared movement and position fields. + pub base: MovableEntity, /// The next direction of Pac-Man, which will be applied when Pac-Man is next aligned with the grid. pub next_direction: Option, /// Whether Pac-Man is currently stopped. pub stopped: bool, map: Rc>, - speed: u32, - modulation: SimpleTickModulator, sprite: AnimatedTexture<'a>, - pub in_tunnel: bool, } impl Pacman<'_> { /// Creates a new `Pacman` instance. - /// - /// # Arguments - /// - /// * `starting_position` - The starting position of Pac-Man, in grid coordinates. - /// * `atlas` - The texture atlas containing the Pac-Man sprites. - /// * `map` - A reference to the game map. pub fn new<'a>( starting_position: (u32, u32), atlas: Texture<'a>, map: Rc>, ) -> Pacman<'a> { + let pixel_position = Map::cell_to_pixel(starting_position); Pacman { - pixel_position: Map::cell_to_pixel(starting_position), - cell_position: starting_position, - direction: Direction::Right, + base: MovableEntity::new( + pixel_position, + starting_position, + Direction::Right, + 3, + SimpleTickModulator::new(1.0), + ), next_direction: None, - speed: 3, - map, stopped: false, - modulation: SimpleTickModulator::new(1.0), + map, sprite: AnimatedTexture::new(atlas, 2, 3, 32, 32, Some((-4, -4))), - in_tunnel: false, } } /// Renders Pac-Man to the canvas. - /// - /// # Arguments - /// - /// * `canvas` - The SDL canvas to render to. pub fn render(&mut self, canvas: &mut Canvas) { if self.stopped { - self.sprite - .render_static(canvas, self.pixel_position, self.direction, Some(2)); + self.sprite.render_static( + canvas, + self.base.pixel_position, + self.base.direction, + Some(2), + ); } else { self.sprite - .render(canvas, self.pixel_position, self.direction); + .render(canvas, self.base.pixel_position, self.base.direction); } } /// Calculates the next cell in the given direction. - /// - /// # Arguments - /// - /// * `direction` - The direction to check. If `None`, the current direction is used. pub fn next_cell(&self, direction: Option) -> (i32, i32) { - let (x, y) = direction.unwrap_or(self.direction).offset(); - let cell = self.cell_position; + let (x, y) = direction.unwrap_or(self.base.direction).offset(); + let cell = self.base.cell_position; (cell.0 as i32 + x, cell.1 as i32 + y) } @@ -101,7 +85,7 @@ impl Pacman<'_> { None => return false, // If the next direction is the same as the current direction, do nothing. Some(next_direction) => { - if next_direction == self.direction { + if next_direction == self.base.direction { self.next_direction = None; return false; } @@ -125,14 +109,14 @@ impl Pacman<'_> { event!( tracing::Level::DEBUG, "Direction change: {:?} -> {:?} at position ({}, {}) internal ({}, {})", - self.direction, + self.base.direction, self.next_direction.unwrap(), - self.pixel_position.0, - self.pixel_position.1, - self.internal_position().0, - self.internal_position().1 + self.base.pixel_position.0, + self.base.pixel_position.1, + self.base.internal_position().0, + self.base.internal_position().1 ); - self.direction = self.next_direction.unwrap(); + self.base.direction = self.next_direction.unwrap(); self.next_direction = None; true @@ -144,63 +128,63 @@ impl Pacman<'_> { /// This is used to ensure that Pac-Man is aligned with the grid before /// changing direction. fn internal_position_even(&self) -> (u32, u32) { - let (x, y) = self.internal_position(); + let (x, y) = self.base.internal_position(); ((x / 2u32) * 2u32, (y / 2u32) * 2u32) } } impl Entity for Pacman<'_> { + fn base(&self) -> &MovableEntity { + &self.base + } + + /// Returns true if the Pac-Man entity is colliding with the other entity. fn is_colliding(&self, other: &dyn Entity) -> bool { - let (x, y) = self.position(); - let (other_x, other_y) = other.position(); + let (x, y) = self.base.pixel_position; + let (other_x, other_y) = other.base().pixel_position; x == other_x && y == other_y } - fn position(&self) -> (i32, i32) { - self.pixel_position - } - - fn cell_position(&self) -> (u32, u32) { - self.cell_position - } - - fn internal_position(&self) -> (u32, u32) { - let (x, y) = self.position(); - (x as u32 % CELL_SIZE, y as u32 % CELL_SIZE) - } - + /// Ticks the Pac-Man entity. fn tick(&mut self) { // Pac-Man can only change direction when he is perfectly aligned with the grid. let can_change = self.internal_position_even() == (0, 0); if can_change { - self.cell_position = ( - (self.pixel_position.0 as u32 / CELL_SIZE) - BOARD_OFFSET.0, - (self.pixel_position.1 as u32 / CELL_SIZE) - BOARD_OFFSET.1, + self.base.cell_position = ( + (self.base.pixel_position.0 as u32 / CELL_SIZE) - BOARD_OFFSET.0, + (self.base.pixel_position.1 as u32 / CELL_SIZE) - BOARD_OFFSET.1, ); let current_tile = self .map .borrow() - .get_tile((self.cell_position.0 as i32, self.cell_position.1 as i32)) + .get_tile(( + self.base.cell_position.0 as i32, + self.base.cell_position.1 as i32, + )) .unwrap_or(MapTile::Empty); if current_tile == MapTile::Tunnel { - self.in_tunnel = true; + self.base.in_tunnel = true; } // Tunnel logic: if in tunnel, force movement and prevent direction change - if self.in_tunnel { + if self.base.in_tunnel { // If out of bounds, teleport to the opposite side and exit tunnel - if self.cell_position.0 == 0 { - self.cell_position.0 = BOARD_WIDTH - 2; - self.pixel_position = - Map::cell_to_pixel((self.cell_position.0 + 1, self.cell_position.1)); - self.in_tunnel = false; - } else if self.cell_position.0 == BOARD_WIDTH - 1 { - self.cell_position.0 = 1; - self.pixel_position = - Map::cell_to_pixel((self.cell_position.0 - 1, self.cell_position.1)); - self.in_tunnel = false; + if self.base.cell_position.0 == 0 { + self.base.cell_position.0 = BOARD_WIDTH - 2; + self.base.pixel_position = Map::cell_to_pixel(( + self.base.cell_position.0 + 1, + self.base.cell_position.1, + )); + self.base.in_tunnel = false; + } else if self.base.cell_position.0 == BOARD_WIDTH - 1 { + self.base.cell_position.0 = 1; + self.base.pixel_position = Map::cell_to_pixel(( + self.base.cell_position.0 - 1, + self.base.cell_position.1, + )); + self.base.in_tunnel = false; } else { // While in tunnel, do not allow direction change // and always move in the current direction @@ -226,28 +210,13 @@ impl Entity for Pacman<'_> { } if !self.stopped { - if self.modulation.next() { - let speed = self.speed as i32; - match self.direction { - Direction::Right => { - self.pixel_position.0 += speed; - } - Direction::Left => { - self.pixel_position.0 -= speed; - } - Direction::Up => { - self.pixel_position.1 -= speed; - } - Direction::Down => { - self.pixel_position.1 += speed; - } - } - + if self.base.modulation.next() { + self.base.move_forward(); // Update the cell position if Pac-Man is aligned with the grid. if self.internal_position_even() == (0, 0) { - self.cell_position = ( - (self.pixel_position.0 as u32 / CELL_SIZE) - BOARD_OFFSET.0, - (self.pixel_position.1 as u32 / CELL_SIZE) - BOARD_OFFSET.1, + self.base.cell_position = ( + (self.base.pixel_position.0 as u32 / CELL_SIZE) - BOARD_OFFSET.0, + (self.base.pixel_position.1 as u32 / CELL_SIZE) - BOARD_OFFSET.1, ); } }