diff --git a/src/entity.rs b/src/entity.rs index bcd3447..a1e8c3b 100644 --- a/src/entity.rs +++ b/src/entity.rs @@ -1,5 +1,11 @@ -//! This module defines the `Entity` trait, which is implemented by all game -//! objects that can be moved and rendered. +use crate::{ + constants::{MapTile, BOARD_OFFSET, BOARD_WIDTH, CELL_SIZE}, + direction::Direction, + map::Map, + modulation::SimpleTickModulator, +}; +use std::cell::RefCell; +use std::rc::Rc; /// A trait for game objects that can be moved and rendered. pub trait Entity { @@ -7,7 +13,11 @@ pub trait Entity { fn base(&self) -> &MovableEntity; /// Returns true if the entity is colliding with the other entity. - fn is_colliding(&self, other: &dyn Entity) -> bool; + fn is_colliding(&self, other: &dyn Entity) -> bool { + let (x, y) = self.base().pixel_position; + let (other_x, other_y) = other.base().pixel_position; + x == other_x && y == other_y + } /// Ticks the entity, which updates its state and position. fn tick(&mut self); @@ -20,13 +30,15 @@ pub struct MovableEntity { /// 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, + pub direction: Direction, /// Movement speed (pixels per tick). pub speed: u32, /// Movement modulator for controlling speed. - pub modulation: crate::modulation::SimpleTickModulator, + pub modulation: SimpleTickModulator, /// Whether the entity is currently in a tunnel. pub in_tunnel: bool, + /// Reference to the game map. + pub map: Rc>, } impl MovableEntity { @@ -34,9 +46,10 @@ impl MovableEntity { pub fn new( pixel_position: (i32, i32), cell_position: (u32, u32), - direction: crate::direction::Direction, + direction: Direction, speed: u32, - modulation: crate::modulation::SimpleTickModulator, + modulation: SimpleTickModulator, + map: Rc>, ) -> Self { Self { pixel_position, @@ -45,21 +58,21 @@ impl MovableEntity { speed, modulation, in_tunnel: false, + map, } } /// 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, + self.pixel_position.0 as u32 % CELL_SIZE, + self.pixel_position.1 as u32 % 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, @@ -67,4 +80,87 @@ impl MovableEntity { Direction::Down => self.pixel_position.1 += speed, } } + + /// Updates the cell position based on the current pixel position. + pub fn update_cell_position(&mut self) { + 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, + ); + } + + /// Calculates the next cell in the given direction. + pub fn next_cell(&self, direction: Option) -> (i32, i32) { + let (x, y) = direction.unwrap_or(self.direction).offset(); + ( + self.cell_position.0 as i32 + x, + self.cell_position.1 as i32 + y, + ) + } + + /// Returns true if the next cell in the given direction is a wall. + pub fn is_wall_ahead(&self, direction: Option) -> bool { + let next_cell = self.next_cell(direction); + matches!(self.map.borrow().get_tile(next_cell), Some(MapTile::Wall)) + } + + /// Handles tunnel movement and wrapping. + /// Returns true if the entity is in a tunnel and was handled. + pub fn handle_tunnel(&mut self) -> bool { + if !self.in_tunnel { + let current_tile = self + .map + .borrow() + .get_tile((self.cell_position.0 as i32, self.cell_position.1 as i32)); + if matches!(current_tile, Some(MapTile::Tunnel)) { + self.in_tunnel = true; + } + } + + if self.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; + true + } 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; + true + } else { + // Still in tunnel, keep moving + true + } + } else { + false + } + } + + /// Returns true if the entity is aligned with the grid. + pub fn is_grid_aligned(&self) -> bool { + self.internal_position() == (0, 0) + } + + /// Attempts to set the direction if the next cell is not a wall. + /// Returns true if the direction was changed. + pub fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool { + if new_direction == self.direction { + return false; + } + if self.is_wall_ahead(Some(new_direction)) { + return false; + } + self.direction = new_direction; + true + } +} + +/// A trait for entities that can be rendered to the screen. +pub trait Renderable { + /// Renders the entity to the canvas. + fn render(&mut self, canvas: &mut sdl2::render::Canvas); } diff --git a/src/game.rs b/src/game.rs index e68223f..f1b7b0b 100644 --- a/src/game.rs +++ b/src/game.rs @@ -13,12 +13,14 @@ use sdl2::{pixels::Color, render::Canvas, video::Window}; use tracing::event; use crate::audio::Audio; -use crate::constants::{MapTile, BOARD_HEIGHT, BOARD_WIDTH, RAW_BOARD}; -use crate::direction::Direction; -use crate::entity::Entity; -use crate::ghosts::Blinky; -use crate::map::Map; -use crate::pacman::Pacman; +use crate::{ + constants::{MapTile, BOARD_HEIGHT, BOARD_WIDTH, RAW_BOARD}, + direction::Direction, + entity::{Entity, Renderable}, + ghosts::blinky::Blinky, + map::Map, + pacman::Pacman, +}; // Embed texture data directly into the executable static PACMAN_TEXTURE_DATA: &[u8] = include_bytes!("../assets/32/pacman.png"); @@ -40,6 +42,7 @@ pub enum DebugMode { None, Grid, Pathfinding, + ValidPositions, } pub struct Game<'a> { @@ -52,7 +55,7 @@ pub struct Game<'a> { map: Rc>, debug_mode: DebugMode, score: u32, - audio: Audio, + audio: crate::audio::Audio, // Add ghost blinky: Blinky<'a>, } @@ -155,7 +158,8 @@ impl Game<'_> { self.debug_mode = match self.debug_mode { DebugMode::None => DebugMode::Grid, DebugMode::Grid => DebugMode::Pathfinding, - DebugMode::Pathfinding => DebugMode::None, + DebugMode::Pathfinding => DebugMode::ValidPositions, + DebugMode::ValidPositions => DebugMode::None, }; } @@ -185,36 +189,26 @@ impl Game<'_> { // Reset the score self.score = 0; - // Reset Pacman position - let mut pacman = self.pacman.borrow_mut(); - 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; - - // Reset ghost positions and mode + // Get valid positions from the cached flood fill + let mut map = self.map.borrow_mut(); + let valid_positions = map.get_valid_playable_positions(); let mut rng = rand::rng(); - let map = self.map.borrow(); - let mut valid_positions = vec![]; - for x in 1..(crate::constants::BOARD_WIDTH - 1) { - for y in 1..(crate::constants::BOARD_HEIGHT - 1) { - let tile_option = map.get_tile((x as i32, y as i32)); - if let Some(tile) = tile_option { - match tile { - MapTile::Empty | MapTile::Pellet | MapTile::PowerPellet => { - valid_positions.push((x, y)); - } - _ => {} - } - } - } + // Randomize Pac-Man position + if let Some(pos) = valid_positions.iter().choose(&mut rng) { + let mut pacman = self.pacman.borrow_mut(); + pacman.base.pixel_position = Map::cell_to_pixel((pos.x, pos.y)); + pacman.base.cell_position = (pos.x, pos.y); + pacman.base.in_tunnel = false; + pacman.base.direction = Direction::Right; + pacman.next_direction = None; + pacman.stopped = false; } - if let Some(&(gx, gy)) = valid_positions.iter().choose(&mut rng) { - self.blinky.base.pixel_position = Map::cell_to_pixel((gx, gy)); - self.blinky.base.cell_position = (gx, gy); + + // Randomize ghost position + if let Some(pos) = valid_positions.iter().choose(&mut rng) { + self.blinky.base.pixel_position = Map::cell_to_pixel((pos.x, pos.y)); + self.blinky.base.cell_position = (pos.x, pos.y); self.blinky.base.in_tunnel = false; self.blinky.base.direction = Direction::Left; self.blinky.mode = crate::ghost::GhostMode::Chase; @@ -339,10 +333,21 @@ impl Game<'_> { } // Draw the next cell - let next_cell = self.pacman.borrow().next_cell(None); + let next_cell = self.pacman.borrow().base.next_cell(None); self.draw_cell((next_cell.0 as u32, next_cell.1 as u32), Color::YELLOW); } + // Show valid playable positions + if self.debug_mode == DebugMode::ValidPositions { + let valid_positions_vec = { + let mut map = self.map.borrow_mut(); + map.get_valid_playable_positions().clone() + }; + for &pos in &valid_positions_vec { + self.draw_cell((pos.x, pos.y), Color::RGB(255, 140, 0)); // ORANGE + } + } + // Pathfinding debug mode if self.debug_mode == DebugMode::Pathfinding { // Show the current path for Blinky diff --git a/src/ghost.rs b/src/ghost.rs index 8ce08a4..d8584f2 100644 --- a/src/ghost.rs +++ b/src/ghost.rs @@ -1,19 +1,11 @@ use pathfinding::prelude::dijkstra; -use sdl2::{ - pixels::Color, - render::{Canvas, Texture}, - video::Window, -}; -use std::cell::RefCell; -use std::rc::Rc; - use rand::Rng; use crate::{ animation::AnimatedTexture, - constants::{MapTile, BOARD_OFFSET, BOARD_WIDTH, CELL_SIZE}, + constants::{MapTile, BOARD_WIDTH}, direction::Direction, - entity::{Entity, MovableEntity}, + entity::{Entity, MovableEntity, Renderable}, map::Map, modulation::{SimpleTickModulator, TickModulator}, pacman::Pacman, @@ -45,12 +37,12 @@ pub enum GhostType { impl GhostType { /// Returns the color of the ghost. - pub fn color(&self) -> Color { + pub fn color(&self) -> sdl2::pixels::Color { match self { - GhostType::Blinky => Color::RGB(255, 0, 0), - GhostType::Pinky => Color::RGB(255, 184, 255), - GhostType::Inky => Color::RGB(0, 255, 255), - GhostType::Clyde => Color::RGB(255, 184, 82), + GhostType::Blinky => sdl2::pixels::Color::RGB(255, 0, 0), + GhostType::Pinky => sdl2::pixels::Color::RGB(255, 184, 255), + GhostType::Inky => sdl2::pixels::Color::RGB(0, 255, 255), + GhostType::Clyde => sdl2::pixels::Color::RGB(255, 184, 82), } } } @@ -63,10 +55,8 @@ pub struct Ghost<'a> { pub mode: GhostMode, /// The type/personality of this ghost pub ghost_type: GhostType, - /// Reference to the game map - pub map: Rc>, /// Reference to Pac-Man for targeting - pub pacman: Rc>>, + pub pacman: std::rc::Rc>>, /// Ghost body sprite body_sprite: AnimatedTexture<'a>, /// Ghost eyes sprite @@ -78,10 +68,10 @@ impl Ghost<'_> { pub fn new<'a>( ghost_type: GhostType, starting_position: (u32, u32), - body_texture: Texture<'a>, - eyes_texture: Texture<'a>, - map: Rc>, - pacman: Rc>>, + body_texture: sdl2::render::Texture<'a>, + eyes_texture: sdl2::render::Texture<'a>, + map: std::rc::Rc>, + pacman: std::rc::Rc>>, ) -> Ghost<'a> { let color = ghost_type.color(); let mut body_sprite = AnimatedTexture::new(body_texture, 8, 2, 32, 32, Some((-4, -4))); @@ -94,56 +84,94 @@ impl Ghost<'_> { Direction::Left, 3, SimpleTickModulator::new(1.0), + map, ), mode: GhostMode::Chase, ghost_type, - map, pacman, body_sprite, eyes_sprite: AnimatedTexture::new(eyes_texture, 1, 4, 32, 32, Some((-4, -4))), } } - /// Renders the ghost to the canvas - pub fn render(&mut self, canvas: &mut Canvas) { - // Render body - if self.mode != GhostMode::Eyes { - let color = if self.mode == GhostMode::Frightened { - Color::RGB(0, 0, 255) - } else { - self.ghost_type.color() - }; + /// Gets the target tile for this ghost based on its current mode + pub fn get_target_tile(&self) -> (i32, i32) { + match self.mode { + GhostMode::Scatter => self.get_scatter_target(), + GhostMode::Chase => self.get_chase_target(), + GhostMode::Frightened => self.get_random_target(), + GhostMode::Eyes => self.get_house_target(), + GhostMode::House => self.get_house_exit_target(), + } + } - self.body_sprite - .set_color_modulation(color.r, color.g, color.b); - self.body_sprite - .render(canvas, self.base.pixel_position, Direction::Right); + /// Gets this ghost's home corner target for scatter mode + fn get_scatter_target(&self) -> (i32, i32) { + match self.ghost_type { + GhostType::Blinky => (25, 0), // Top right + GhostType::Pinky => (2, 0), // Top left + GhostType::Inky => (27, 35), // Bottom right + GhostType::Clyde => (0, 35), // Bottom left + } + } + + /// Gets a random adjacent tile for frightened mode + fn get_random_target(&self) -> (i32, i32) { + let mut rng = rand::rng(); + let mut possible_moves = Vec::new(); + + // Check all four directions + for dir in &[ + Direction::Up, + Direction::Down, + Direction::Left, + Direction::Right, + ] { + // Don't allow reversing direction + if *dir == self.base.direction.opposite() { + continue; + } + + let next_cell = self.base.next_cell(Some(*dir)); + if !matches!( + self.base.map.borrow().get_tile(next_cell), + Some(MapTile::Wall) + ) { + possible_moves.push(next_cell); + } } - // Always render eyes on top - let eye_frame = if self.mode == GhostMode::Frightened { - 4 // Frightened frame + if possible_moves.is_empty() { + // No valid moves, must reverse + self.base.next_cell(Some(self.base.direction.opposite())) } else { - match self.base.direction { - Direction::Right => 0, - Direction::Up => 1, - Direction::Left => 2, - Direction::Down => 3, - } - }; + // Choose a random valid move + possible_moves[rng.random_range(0..possible_moves.len())] + } + } - self.eyes_sprite.render_static( - canvas, - self.base.pixel_position, - Direction::Right, - Some(eye_frame), - ); + /// Gets the ghost house target for returning eyes + fn get_house_target(&self) -> (i32, i32) { + (13, 14) // Center of ghost house + } + + /// Gets the exit point target when leaving house + fn get_house_exit_target(&self) -> (i32, i32) { + (13, 11) // Just above ghost house + } + + /// Gets this ghost's chase mode target (to be implemented by each ghost type) + fn get_chase_target(&self) -> (i32, i32) { + // Default implementation just targets Pac-Man directly + let pacman = self.pacman.borrow(); + let cell = pacman.base().cell_position; + (cell.0 as i32, cell.1 as i32) } /// 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.base.cell_position; - let map = self.map.borrow(); + let map = self.base.map.borrow(); dijkstra( &start, @@ -179,83 +207,6 @@ impl Ghost<'_> { ) } - /// Gets the target tile for this ghost based on its current mode - pub fn get_target_tile(&self) -> (i32, i32) { - match self.mode { - GhostMode::Scatter => self.get_scatter_target(), - GhostMode::Chase => self.get_chase_target(), - GhostMode::Frightened => self.get_random_target(), - GhostMode::Eyes => self.get_house_target(), - GhostMode::House => self.get_house_exit_target(), - } - } - - /// Gets this ghost's home corner target for scatter mode - fn get_scatter_target(&self) -> (i32, i32) { - match self.ghost_type { - GhostType::Blinky => (25, 0), // Top right - GhostType::Pinky => (2, 0), // Top left - GhostType::Inky => (27, 35), // Bottom right - GhostType::Clyde => (0, 35), // Bottom left - } - } - - /// Gets a random adjacent tile for frightened mode - fn get_random_target(&self) -> (i32, i32) { - let mut rng = rand::rng(); - let (x, y) = self.base.cell_position; - let mut possible_moves = Vec::new(); - - // Check all four directions - for dir in &[ - Direction::Up, - Direction::Down, - Direction::Left, - Direction::Right, - ] { - // Don't allow reversing direction - if *dir == self.base.direction.opposite() { - continue; - } - - let (dx, dy) = dir.offset(); - let next_cell = (x as i32 + dx, y as i32 + dy); - let tile = self.map.borrow().get_tile(next_cell); - if let Some(MapTile::Wall) = tile { - // It's a wall, not a valid move - } else { - possible_moves.push(next_cell); - } - } - - if possible_moves.is_empty() { - // No valid moves, must reverse - let (dx, dy) = self.base.direction.opposite().offset(); - return (x as i32 + dx, y as i32 + dy); - } - - // Choose a random valid move - possible_moves[rng.gen_range(0..possible_moves.len())] - } - - /// Gets the ghost house target for returning eyes - fn get_house_target(&self) -> (i32, i32) { - (13, 14) // Center of ghost house - } - - /// Gets the exit point target when leaving house - fn get_house_exit_target(&self) -> (i32, i32) { - (13, 11) // Just above ghost house - } - - /// Gets this ghost's chase mode target (to be implemented by each ghost type) - fn get_chase_target(&self) -> (i32, i32) { - // Default implementation just targets Pac-Man directly - let pacman = self.pacman.borrow(); - let cell = pacman.base.cell_position; - (cell.0 as i32, cell.1 as i32) - } - /// Changes the ghost's mode and handles direction reversal pub fn set_mode(&mut self, new_mode: GhostMode) { // Don't reverse if going to/from frightened or if in house @@ -274,7 +225,8 @@ impl Ghost<'_> { }; if should_reverse { - self.base.direction = self.base.direction.opposite(); + self.base + .set_direction_if_valid(self.base.direction.opposite()); } } } @@ -284,56 +236,16 @@ impl Entity for Ghost<'_> { &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.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.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, - ); + if self.base.is_grid_aligned() { + self.base.update_cell_position(); - let current_tile = self - .map - .borrow() - .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.base.in_tunnel = true; - } - - // Tunnel logic: if in tunnel, force movement and prevent direction change - if self.base.in_tunnel { - // If out of bounds, teleport to the opposite side and exit tunnel - 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 - } - } else { + if !self.base.handle_tunnel() { // Pathfinding logic (only if not in tunnel) let target_tile = self.get_target_tile(); if let Some((path, _)) = @@ -344,7 +256,7 @@ impl Entity for Ghost<'_> { 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.base.direction = if dx > 0 { + let new_direction = if dx > 0 { Direction::Right } else if dx < 0 { Direction::Left @@ -353,40 +265,60 @@ impl Entity for Ghost<'_> { } else { Direction::Up }; + self.base.set_direction_if_valid(new_direction); } } } - // Check if the next tile in the current direction is a wall - let (dx, dy) = self.base.direction.offset(); - let next_cell = ( - self.base.cell_position.0 as i32 + dx, - self.base.cell_position.1 as i32 + dy, - ); - let next_tile = self - .map - .borrow() - .get_tile(next_cell) - .unwrap_or(MapTile::Empty); - if next_tile == MapTile::Wall { - // Don't move if the next tile is a wall + // Don't move if the next tile is a wall + if self.base.is_wall_ahead(None) { return; } } - if !self.base.modulation.next() { - return; - } + if self.base.modulation.next() { + self.base.move_forward(); - // Update position based on current direction and speed - self.base.move_forward(); - - // Update cell position when aligned with grid - 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, - ); + if self.base.is_grid_aligned() { + self.base.update_cell_position(); + } } } } + +impl Renderable for Ghost<'_> { + fn render(&mut self, canvas: &mut sdl2::render::Canvas) { + // Render body + if self.mode != GhostMode::Eyes { + let color = if self.mode == GhostMode::Frightened { + sdl2::pixels::Color::RGB(0, 0, 255) + } else { + self.ghost_type.color() + }; + + self.body_sprite + .set_color_modulation(color.r, color.g, color.b); + self.body_sprite + .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.base.direction { + Direction::Right => 0, + Direction::Up => 1, + Direction::Left => 2, + Direction::Down => 3, + } + }; + + self.eyes_sprite.render_static( + canvas, + self.base.pixel_position, + Direction::Right, + Some(eye_frame), + ); + } +} diff --git a/src/ghosts/blinky.rs b/src/ghosts/blinky.rs index ed77a63..6106ea5 100644 --- a/src/ghosts/blinky.rs +++ b/src/ghosts/blinky.rs @@ -4,9 +4,8 @@ use std::rc::Rc; use sdl2::render::{Canvas, Texture}; use sdl2::video::Window; -use crate::entity::MovableEntity; use crate::{ - entity::Entity, + entity::{Entity, MovableEntity, Renderable}, ghost::{Ghost, GhostMode, GhostType}, map::Map, pacman::Pacman, @@ -39,7 +38,7 @@ 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(); - let cell = pacman.base.cell_position; + let cell = pacman.base().cell_position; (cell.0 as i32, cell.1 as i32) } @@ -48,7 +47,7 @@ impl<'a> Blinky<'a> { } pub fn render(&mut self, canvas: &mut Canvas) { - self.ghost.render(canvas); + Renderable::render(&mut self.ghost, canvas); } } diff --git a/src/ghosts/mod.rs b/src/ghosts/mod.rs index 88c9ce6..6dc5c58 100644 --- a/src/ghosts/mod.rs +++ b/src/ghosts/mod.rs @@ -1,3 +1 @@ -mod blinky; - -pub use blinky::Blinky; +pub mod blinky; diff --git a/src/pacman.rs b/src/pacman.rs index 53ab9cf..b59e395 100644 --- a/src/pacman.rs +++ b/src/pacman.rs @@ -10,10 +10,8 @@ use tracing::event; use crate::{ animation::AnimatedTexture, - constants::MapTile, - constants::{BOARD_OFFSET, BOARD_WIDTH, CELL_SIZE}, direction::Direction, - entity::{Entity, MovableEntity}, + entity::{Entity, MovableEntity, Renderable}, map::Map, modulation::{SimpleTickModulator, TickModulator}, }; @@ -26,7 +24,6 @@ pub struct Pacman<'a> { pub next_direction: Option, /// Whether Pac-Man is currently stopped. pub stopped: bool, - map: Rc>, sprite: AnimatedTexture<'a>, } @@ -45,16 +42,70 @@ impl Pacman<'_> { Direction::Right, 3, SimpleTickModulator::new(1.0), + map, ), next_direction: None, stopped: false, - map, sprite: AnimatedTexture::new(atlas, 2, 3, 32, 32, Some((-4, -4))), } } - /// Renders Pac-Man to the canvas. - pub fn render(&mut self, canvas: &mut Canvas) { + /// Handles a requested direction change. + fn handle_direction_change(&mut self) -> bool { + match self.next_direction { + None => return false, + Some(next_direction) => { + if self.base.set_direction_if_valid(next_direction) { + self.next_direction = None; + return true; + } + } + } + false + } + + /// Returns the internal position of Pac-Man, rounded down to the nearest even number. + fn internal_position_even(&self) -> (u32, u32) { + let (x, y) = self.base.internal_position(); + ((x / 2u32) * 2u32, (y / 2u32) * 2u32) + } +} + +impl Entity for Pacman<'_> { + fn base(&self) -> &MovableEntity { + &self.base + } + + fn tick(&mut self) { + let can_change = self.internal_position_even() == (0, 0); + + if can_change { + self.base.update_cell_position(); + + if !self.base.handle_tunnel() { + // Handle direction change as normal if not in tunnel + self.handle_direction_change(); + + // Check if the next tile in the current direction is a wall + if !self.stopped && self.base.is_wall_ahead(None) { + self.stopped = true; + } else if self.stopped && !self.base.is_wall_ahead(None) { + self.stopped = false; + } + } + } + + if !self.stopped && self.base.modulation.next() { + self.base.move_forward(); + if self.internal_position_even() == (0, 0) { + self.base.update_cell_position(); + } + } + } +} + +impl Renderable for Pacman<'_> { + fn render(&mut self, canvas: &mut Canvas) { if self.stopped { self.sprite.render_static( canvas, @@ -67,159 +118,4 @@ impl Pacman<'_> { .render(canvas, self.base.pixel_position, self.base.direction); } } - - /// Calculates the next cell in the given direction. - pub fn next_cell(&self, direction: Option) -> (i32, i32) { - 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) - } - - /// Handles a requested direction change. - /// - /// The direction change is only applied if the next tile in the requested - /// direction is not a wall. - fn handle_direction_change(&mut self) -> bool { - match self.next_direction { - // If there is no next direction, do nothing. - None => return false, - // If the next direction is the same as the current direction, do nothing. - Some(next_direction) => { - if next_direction == self.base.direction { - self.next_direction = None; - return false; - } - } - } - - // Get the next cell in the proposed direction. - let proposed_next_cell = self.next_cell(self.next_direction); - let proposed_next_tile = self - .map - .borrow() - .get_tile(proposed_next_cell) - .unwrap_or(MapTile::Empty); - - // If the next tile is a wall, do nothing. - if proposed_next_tile == MapTile::Wall { - return false; - } - - // If the next tile is not a wall, change direction. - event!( - tracing::Level::DEBUG, - "Direction change: {:?} -> {:?} at position ({}, {}) internal ({}, {})", - self.base.direction, - self.next_direction.unwrap(), - self.base.pixel_position.0, - self.base.pixel_position.1, - self.base.internal_position().0, - self.base.internal_position().1 - ); - self.base.direction = self.next_direction.unwrap(); - self.next_direction = None; - - true - } - - /// Returns the internal position of Pac-Man, rounded down to the nearest - /// even number. - /// - /// 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.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.base.pixel_position; - let (other_x, other_y) = other.base().pixel_position; - x == other_x && y == other_y - } - - /// 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.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.base.cell_position.0 as i32, - self.base.cell_position.1 as i32, - )) - .unwrap_or(MapTile::Empty); - if current_tile == MapTile::Tunnel { - self.base.in_tunnel = true; - } - - // Tunnel logic: if in tunnel, force movement and prevent direction change - if self.base.in_tunnel { - // If out of bounds, teleport to the opposite side and exit tunnel - 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 - } - } else { - // Handle direction change as normal - self.handle_direction_change(); - - // Check if the next tile in the current direction is a wall. - let next_tile_position = self.next_cell(None); - let next_tile = self - .map - .borrow() - .get_tile(next_tile_position) - .unwrap_or(MapTile::Empty); - - if !self.stopped && next_tile == MapTile::Wall { - self.stopped = true; - } else if self.stopped && next_tile != MapTile::Wall { - self.stopped = false; - } - } - } - - if !self.stopped { - 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.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, - ); - } - } - } - } }