From 785a7603431d40148963d271291fbda909b10b00 Mon Sep 17 00:00:00 2001 From: Xevion Date: Wed, 23 Jul 2025 17:16:15 -0500 Subject: [PATCH] feat: new edible type for pellet/powerpellet, fruits, separate static/moving entities --- src/animation.rs | 15 ++--- src/constants.rs | 39 +++++++++++ src/edible.rs | 90 +++++++++++++++++++++++++ src/entity.rs | 140 ++++++++++++++++++++------------------- src/game.rs | 152 ++++++++++++++++++++----------------------- src/ghost.rs | 74 ++++++++++----------- src/ghosts/blinky.rs | 40 +++++++++--- src/main.rs | 1 + src/map.rs | 6 ++ src/pacman.rs | 76 +++++++++++++--------- 10 files changed, 398 insertions(+), 235 deletions(-) create mode 100644 src/edible.rs diff --git a/src/animation.rs b/src/animation.rs index 3e40118..f417201 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -10,7 +10,7 @@ use crate::direction::Direction; /// Trait for drawable atlas-based textures pub trait FrameDrawn { fn render( - &mut self, + &self, canvas: &mut Canvas, position: (i32, i32), direction: Direction, @@ -63,7 +63,7 @@ impl<'a> AtlasTexture<'a> { impl<'a> FrameDrawn for AtlasTexture<'a> { fn render( - &mut self, + &self, canvas: &mut Canvas, position: (i32, i32), direction: Direction, @@ -158,18 +158,13 @@ impl<'a> AnimatedAtlasTexture<'a> { impl<'a> FrameDrawn for AnimatedAtlasTexture<'a> { fn render( - &mut self, + &self, canvas: &mut Canvas, position: (i32, i32), direction: Direction, frame: Option, ) { - self.atlas.render( - canvas, - position, - direction, - frame.or(Some(self.current_frame())), - ); - self.tick(); + let frame = frame.unwrap_or_else(|| self.current_frame()); + self.atlas.render(canvas, position, direction, Some(frame)); } } diff --git a/src/constants.rs b/src/constants.rs index 4d58236..8375c7a 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -36,6 +36,45 @@ pub enum MapTile { Tunnel, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum FruitType { + Cherry, + Strawberry, + Orange, + Apple, + Melon, + Galaxian, + Bell, + Key, +} + +impl FruitType { + pub const ALL: [FruitType; 8] = [ + FruitType::Cherry, + FruitType::Strawberry, + FruitType::Orange, + FruitType::Apple, + FruitType::Melon, + FruitType::Galaxian, + FruitType::Bell, + FruitType::Key, + ]; + + pub fn score(self) -> u32 { + match self { + FruitType::Cherry => 100, + FruitType::Strawberry => 300, + FruitType::Orange => 500, + FruitType::Apple => 700, + FruitType::Melon => 1000, + FruitType::Galaxian => 2000, + FruitType::Bell => 3000, + FruitType::Key => 5000, + } + } +} + /// The raw layout of the game board, as a 2D array of characters. pub const RAW_BOARD: [&str; BOARD_HEIGHT as usize] = [ "############################", diff --git a/src/edible.rs b/src/edible.rs new file mode 100644 index 0000000..11502f2 --- /dev/null +++ b/src/edible.rs @@ -0,0 +1,90 @@ +//! Edible entity for Pac-Man: pellets, power pellets, and fruits. +use crate::animation::{AtlasTexture, FrameDrawn}; +use crate::constants::{FruitType, MapTile, BOARD_HEIGHT, BOARD_WIDTH}; +use crate::direction::Direction; +use crate::entity::{Entity, Renderable}; +use crate::map::Map; +use sdl2::{render::Canvas, video::Window}; +use std::cell::RefCell; +use std::rc::Rc; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EdibleKind { + Pellet, + PowerPellet, + Fruit(FruitType), +} + +pub struct Edible<'a> { + pub base: crate::entity::StaticEntity, + pub kind: EdibleKind, + pub sprite: std::rc::Rc>, +} + +impl<'a> Edible<'a> { + pub fn new( + kind: EdibleKind, + cell_position: (u32, u32), + sprite: std::rc::Rc>, + ) -> Self { + let pixel_position = crate::map::Map::cell_to_pixel(cell_position); + Edible { + base: crate::entity::StaticEntity::new(pixel_position, cell_position), + kind, + sprite, + } + } + + /// Checks collision with Pac-Man (or any entity) + pub fn collide(&self, pacman: &dyn crate::entity::Entity) -> bool { + self.base.is_colliding(pacman) + } +} + +impl<'a> Entity for Edible<'a> { + fn base(&self) -> &crate::entity::StaticEntity { + &self.base + } +} + +impl<'a> Renderable for Edible<'a> { + fn render(&self, canvas: &mut Canvas) { + self.sprite + .render(canvas, self.base.pixel_position, Direction::Right, Some(0)); + } +} + +/// Reconstruct all edibles from the original map layout +pub fn reconstruct_edibles<'a>( + map: Rc>, + pellet_sprite: std::rc::Rc>, + power_pellet_sprite: std::rc::Rc>, + fruit_sprite: std::rc::Rc>, +) -> Vec> { + let mut edibles = Vec::new(); + for x in 0..BOARD_WIDTH { + for y in 0..BOARD_HEIGHT { + let tile = map.borrow().get_tile((x as i32, y as i32)); + let cell = (x, y); + match tile { + Some(MapTile::Pellet) => { + edibles.push(Edible::new( + EdibleKind::Pellet, + cell, + Rc::clone(&pellet_sprite), + )); + } + Some(MapTile::PowerPellet) => { + edibles.push(Edible::new( + EdibleKind::PowerPellet, + cell, + Rc::clone(&power_pellet_sprite), + )); + } + // Fruits can be added here if you have fruit positions + _ => {} + } + } + } + edibles +} diff --git a/src/entity.rs b/src/entity.rs index a1e8c3b..ef81283 100644 --- a/src/entity.rs +++ b/src/entity.rs @@ -9,8 +9,8 @@ use std::rc::Rc; /// 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 a reference to the base entity (position, etc). + fn base(&self) -> &StaticEntity; /// Returns true if the entity is colliding with the other entity. fn is_colliding(&self, other: &dyn Entity) -> bool { @@ -18,31 +18,45 @@ pub trait Entity { 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); +/// A trait for entities that can move and interact with the map. +pub trait Moving { + fn move_forward(&mut self); + fn update_cell_position(&mut self); + fn next_cell(&self, direction: Option) -> (i32, i32); + fn is_wall_ahead(&self, direction: Option) -> bool; + fn handle_tunnel(&mut self) -> bool; + fn is_grid_aligned(&self) -> bool; + fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool; +} + +/// A struct for static (non-moving) entities with position only. +pub struct StaticEntity { + pub pixel_position: (i32, i32), + pub cell_position: (u32, u32), +} + +impl StaticEntity { + pub fn new(pixel_position: (i32, i32), cell_position: (u32, u32)) -> Self { + Self { + pixel_position, + cell_position, + } + } } /// 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 base: StaticEntity, pub direction: Direction, - /// Movement speed (pixels per tick). pub speed: u32, - /// Movement modulator for controlling speed. 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 { - /// Creates a new MovableEntity. pub fn new( pixel_position: (i32, i32), cell_position: (u32, u32), @@ -52,8 +66,7 @@ impl MovableEntity { map: Rc>, ) -> Self { Self { - pixel_position, - cell_position, + base: StaticEntity::new(pixel_position, cell_position), direction, speed, modulation, @@ -65,89 +78,79 @@ impl MovableEntity { /// Returns the position within the current cell, in pixels. pub fn internal_position(&self) -> (u32, u32) { ( - self.pixel_position.0 as u32 % CELL_SIZE, - self.pixel_position.1 as u32 % CELL_SIZE, + self.base.pixel_position.0 as u32 % CELL_SIZE, + self.base.pixel_position.1 as u32 % CELL_SIZE, ) } +} - /// Move the entity in its current direction by its speed. - pub fn move_forward(&mut self) { +impl Entity for MovableEntity { + fn base(&self) -> &StaticEntity { + &self.base + } +} + +impl Moving for MovableEntity { + fn move_forward(&mut self) { 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, + Direction::Right => self.base.pixel_position.0 += speed, + Direction::Left => self.base.pixel_position.0 -= speed, + Direction::Up => self.base.pixel_position.1 -= speed, + Direction::Down => self.base.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, + fn update_cell_position(&mut self) { + 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, ); } - - /// Calculates the next cell in the given direction. - pub fn next_cell(&self, direction: Option) -> (i32, i32) { + 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, + self.base.cell_position.0 as i32 + x, + self.base.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 { + 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 { + 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)); + let current_tile = self.map.borrow().get_tile(( + self.base.cell_position.0 as i32, + self.base.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)); + 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.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)); + } 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.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 { + 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 { + fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool { if new_direction == self.direction { return false; } @@ -159,8 +162,13 @@ impl MovableEntity { } } +impl Entity for StaticEntity { + fn base(&self) -> &StaticEntity { + self + } +} + /// 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); + fn render(&self, canvas: &mut sdl2::render::Canvas); } diff --git a/src/game.rs b/src/game.rs index 7e049dd..6114e0d 100644 --- a/src/game.rs +++ b/src/game.rs @@ -24,6 +24,7 @@ use crate::{ }; use crate::debug::{DebugMode, DebugRenderer}; +use crate::edible::{reconstruct_edibles, Edible, EdibleKind}; // Embed texture data directly into the executable static PACMAN_TEXTURE_DATA: &[u8] = include_bytes!("../assets/32/pacman.png"); @@ -43,16 +44,16 @@ static GHOST_EYES_TEXTURE_DATA: &[u8] = include_bytes!("../assets/32/ghost_eyes. pub struct Game<'a> { canvas: &'a mut Canvas, map_texture: Texture<'a>, - pellet_texture: AtlasTexture<'a>, - power_pellet_texture: AtlasTexture<'a>, + pellet_texture: Rc>, + power_pellet_texture: Rc>, font: Font<'a, 'static>, pacman: Rc>>, map: Rc>, debug_mode: DebugMode, score: u32, audio: crate::audio::Audio, - // Add ghost blinky: Blinky<'a>, + edibles: Vec>, } impl Game<'_> { @@ -100,7 +101,7 @@ impl Game<'_> { ); // Load pellet texture from embedded data - let pellet_texture = AtlasTexture::new( + let pellet_texture = Rc::new(AtlasTexture::new( texture_creator .load_texture_bytes(PELLET_TEXTURE_DATA) .expect("Could not load pellet texture from embedded data"), @@ -108,8 +109,8 @@ impl Game<'_> { 24, 24, None, - ); - let power_pellet_texture = AtlasTexture::new( + )); + let power_pellet_texture = Rc::new(AtlasTexture::new( texture_creator .load_texture_bytes(POWER_PELLET_TEXTURE_DATA) .expect("Could not load power pellet texture from embedded data"), @@ -117,7 +118,7 @@ impl Game<'_> { 24, 24, None, - ); + )); // Load font from embedded data let font_rwops = RWops::from_bytes(FONT_DATA).expect("Failed to create RWops for font"); @@ -133,6 +134,13 @@ impl Game<'_> { .expect("Could not load map texture from embedded data"); map_texture.set_color_mod(0, 0, 255); + let edibles = reconstruct_edibles( + Rc::clone(&map), + Rc::clone(&pellet_texture), + Rc::clone(&power_pellet_texture), + Rc::clone(&pellet_texture), // placeholder for fruit sprite + ); + Game { canvas, pacman, @@ -145,6 +153,7 @@ impl Game<'_> { score: 0, audio, blinky, + edibles, } } @@ -156,7 +165,10 @@ impl Game<'_> { pub fn keyboard_event(&mut self, keycode: Keycode) { // Change direction let direction = Direction::from_keycode(keycode); - self.pacman.borrow_mut().next_direction = direction; + if direction.is_some() { + self.pacman.borrow_mut().next_direction = direction; + return; + } // Toggle debug mode if keycode == Keycode::Space { @@ -166,11 +178,13 @@ impl Game<'_> { DebugMode::Pathfinding => DebugMode::ValidPositions, DebugMode::ValidPositions => DebugMode::None, }; + return; } // Reset game if keycode == Keycode::R { self.reset(); + return; } } @@ -202,8 +216,8 @@ impl Game<'_> { // 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.base.pixel_position = Map::cell_to_pixel((pos.x, pos.y)); + pacman.base.base.cell_position = (pos.x, pos.y); pacman.base.in_tunnel = false; pacman.base.direction = Direction::Right; pacman.next_direction = None; @@ -212,54 +226,56 @@ impl Game<'_> { // 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.base.pixel_position = Map::cell_to_pixel((pos.x, pos.y)); + self.blinky.base.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; } + + self.edibles = reconstruct_edibles( + Rc::clone(&self.map), + Rc::clone(&self.pellet_texture), + Rc::clone(&self.power_pellet_texture), + Rc::clone(&self.pellet_texture), // placeholder for fruit sprite + ); } /// Advances the game by one tick. pub fn tick(&mut self) { - self.check_pellet_eating(); - self.pacman.borrow_mut().tick(); - self.blinky.tick(); - } + // Advance animation frames for Pacman and Blinky + self.pacman.borrow_mut().sprite.tick(); + self.blinky.body_sprite.tick(); + self.blinky.eyes_sprite.tick(); - /// 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().base.cell_position; - - // Check if there's a pellet at the current position - let tile = { - let map = self.map.borrow(); - map.get_tile((cell_pos.0 as i32, cell_pos.1 as i32)) - }; - - if let Some(tile) = tile { - let pellet_value = match tile { - MapTile::Pellet => Some(10), - MapTile::PowerPellet => Some(50), - _ => None, - }; - - if let Some(value) = pellet_value { - { - let mut map = self.map.borrow_mut(); - map.set_tile((cell_pos.0 as i32, cell_pos.1 as i32), MapTile::Empty); - } - self.add_score(value); - self.audio.eat(); - event!( - tracing::Level::DEBUG, - "Pellet eaten at ({}, {})", - cell_pos.0, - cell_pos.1 - ); + let pacman = self.pacman.borrow(); + let mut eaten_indices = vec![]; + for (i, edible) in self.edibles.iter().enumerate() { + if edible.collide(&*pacman) { + eaten_indices.push(i); } } + drop(pacman); // Release immutable borrow before mutably borrowing self + for &i in eaten_indices.iter().rev() { + let edible = &self.edibles[i]; + match edible.kind { + EdibleKind::Pellet => { + self.add_score(10); + self.audio.eat(); + } + EdibleKind::PowerPellet => { + self.add_score(50); + self.audio.eat(); + } + EdibleKind::Fruit(_fruit) => { + self.add_score(100); + self.audio.eat(); + } + } + self.edibles.remove(i); + } + self.pacman.borrow_mut().tick(); + self.blinky.tick(); } /// Draws the entire game to the canvas. @@ -273,41 +289,14 @@ impl Game<'_> { .copy(&self.map_texture, None, None) .expect("Could not render texture on canvas"); - // Render pellets - for x in 0..BOARD_WIDTH { - for y in 0..BOARD_HEIGHT { - let tile = self - .map - .borrow() - .get_tile((x as i32, y as i32)) - .unwrap_or(MapTile::Empty); - - match tile { - MapTile::Pellet => { - let position = Map::cell_to_pixel((x, y)); - self.pellet_texture.render( - self.canvas, - position, - Direction::Right, - Some(0), - ); - } - MapTile::PowerPellet => { - let position = Map::cell_to_pixel((x, y)); - self.power_pellet_texture.render( - self.canvas, - position, - Direction::Right, - Some(0), - ); - } - _ => {} - } - } + // Remove old pellet rendering + // Instead, render all edibles + for edible in &self.edibles { + edible.render(self.canvas); } // Render Pac-Man - self.pacman.borrow_mut().render(self.canvas); + self.pacman.borrow().render(self.canvas); // Render ghost self.blinky.render(self.canvas); @@ -321,9 +310,10 @@ impl Game<'_> { DebugRenderer::draw_debug_grid( self.canvas, &self.map.borrow(), - self.pacman.borrow().base.cell_position, + self.pacman.borrow().base.base.cell_position, ); - let next_cell = self.pacman.borrow().base.next_cell(None); + let next_cell = + ::next_cell(&*self.pacman.borrow(), None); DebugRenderer::draw_next_cell( self.canvas, &self.map.borrow(), diff --git a/src/ghost.rs b/src/ghost.rs index 07aa255..d0ea032 100644 --- a/src/ghost.rs +++ b/src/ghost.rs @@ -5,7 +5,7 @@ use crate::{ animation::{AnimatedAtlasTexture, FrameDrawn}, constants::{MapTile, BOARD_WIDTH}, direction::Direction, - entity::{Entity, MovableEntity, Renderable}, + entity::{Entity, MovableEntity, Moving, Renderable, StaticEntity}, map::Map, modulation::{SimpleTickModulator, TickModulator}, pacman::Pacman, @@ -57,10 +57,8 @@ pub struct Ghost<'a> { pub ghost_type: GhostType, /// Reference to Pac-Man for targeting pub pacman: std::rc::Rc>>, - /// Ghost body sprite - body_sprite: AnimatedAtlasTexture<'a>, - /// Ghost eyes sprite - eyes_sprite: AnimatedAtlasTexture<'a>, + pub body_sprite: AnimatedAtlasTexture<'a>, + pub eyes_sprite: AnimatedAtlasTexture<'a>, } impl Ghost<'_> { @@ -162,7 +160,6 @@ impl Ghost<'_> { /// 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) @@ -170,7 +167,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.base.cell_position; + let start = self.base.base.cell_position; let map = self.base.map.borrow(); dijkstra( @@ -229,14 +226,8 @@ impl Ghost<'_> { .set_direction_if_valid(self.base.direction.opposite()); } } -} -impl Entity for Ghost<'_> { - fn base(&self) -> &MovableEntity { - &self.base - } - - fn tick(&mut self) { + pub fn tick(&mut self) { if self.mode == GhostMode::House { // For now, do nothing in the house return; @@ -253,7 +244,7 @@ impl Entity for Ghost<'_> { { if path.len() > 1 { let next_move = path[1]; - let (x, y) = self.base.cell_position; + let (x, y) = self.base.base.cell_position; let dx = next_move.0 as i32 - x as i32; let dy = next_move.1 as i32 - y as i32; let new_direction = if dx > 0 { @@ -286,23 +277,35 @@ impl Entity for Ghost<'_> { } } +impl<'a> Moving for Ghost<'a> { + fn move_forward(&mut self) { + self.base.move_forward(); + } + fn update_cell_position(&mut self) { + self.base.update_cell_position(); + } + fn next_cell(&self, direction: Option) -> (i32, i32) { + self.base.next_cell(direction) + } + fn is_wall_ahead(&self, direction: Option) -> bool { + self.base.is_wall_ahead(direction) + } + fn handle_tunnel(&mut self) -> bool { + self.base.handle_tunnel() + } + fn is_grid_aligned(&self) -> bool { + self.base.is_grid_aligned() + } + fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool { + self.base.set_direction_if_valid(new_direction) + } +} + 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, None); - } - - // Always render eyes on top + fn render(&self, canvas: &mut sdl2::render::Canvas) { + let pos = self.base.base.pixel_position; + self.body_sprite.render(canvas, pos, Direction::Right, None); + // Inline the eye_frame logic here let eye_frame = if self.mode == GhostMode::Frightened { 4 // Frightened frame } else { @@ -313,12 +316,7 @@ impl Renderable for Ghost<'_> { Direction::Down => 3, } }; - - self.eyes_sprite.render( - canvas, - self.base.pixel_position, - Direction::Right, - Some(eye_frame), - ); + self.eyes_sprite + .render(canvas, pos, Direction::Right, Some(eye_frame)); } } diff --git a/src/ghosts/blinky.rs b/src/ghosts/blinky.rs index 6106ea5..7516fe4 100644 --- a/src/ghosts/blinky.rs +++ b/src/ghosts/blinky.rs @@ -46,22 +46,44 @@ impl<'a> Blinky<'a> { self.ghost.set_mode(mode); } - pub fn render(&mut self, canvas: &mut Canvas) { - Renderable::render(&mut self.ghost, canvas); + pub fn tick(&mut self) { + self.ghost.tick(); } } -impl<'a> Entity for Blinky<'a> { - fn base(&self) -> &MovableEntity { - self.ghost.base() +impl<'a> crate::entity::Entity for Blinky<'a> { + fn base(&self) -> &crate::entity::StaticEntity { + self.ghost.base.base() } +} - fn is_colliding(&self, other: &dyn Entity) -> bool { - self.ghost.is_colliding(other) +impl<'a> crate::entity::Renderable for Blinky<'a> { + fn render(&self, canvas: &mut Canvas) { + self.ghost.render(canvas); } +} - fn tick(&mut self) { - self.ghost.tick() +impl<'a> crate::entity::Moving for Blinky<'a> { + fn move_forward(&mut self) { + self.ghost.move_forward(); + } + fn update_cell_position(&mut self) { + self.ghost.update_cell_position(); + } + fn next_cell(&self, direction: Option) -> (i32, i32) { + self.ghost.next_cell(direction) + } + fn is_wall_ahead(&self, direction: Option) -> bool { + self.ghost.is_wall_ahead(direction) + } + fn handle_tunnel(&mut self) -> bool { + self.ghost.handle_tunnel() + } + fn is_grid_aligned(&self) -> bool { + self.ghost.is_grid_aligned() + } + fn set_direction_if_valid(&mut self, new_direction: crate::direction::Direction) -> bool { + self.ghost.set_direction_if_valid(new_direction) } } diff --git a/src/main.rs b/src/main.rs index a526fea..3806be4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,6 +65,7 @@ mod map; mod modulation; mod pacman; mod debug; +mod edible; /// The main entry point of the application. /// diff --git a/src/map.rs b/src/map.rs index 637ef1c..3e7ae86 100644 --- a/src/map.rs +++ b/src/map.rs @@ -28,6 +28,12 @@ impl Add for Position { } } +impl PartialEq<(u32, u32)> for Position { + fn eq(&self, other: &(u32, u32)) -> bool { + self.x == other.0 && self.y == other.1 + } +} + impl Position { pub fn as_i32(&self) -> (i32, i32) { (self.x as i32, self.y as i32) diff --git a/src/pacman.rs b/src/pacman.rs index aca5be7..40ba68f 100644 --- a/src/pacman.rs +++ b/src/pacman.rs @@ -11,7 +11,7 @@ use tracing::event; use crate::{ animation::{AnimatedAtlasTexture, FrameDrawn}, direction::Direction, - entity::{Entity, MovableEntity, Renderable}, + entity::{Entity, MovableEntity, Moving, Renderable, StaticEntity}, map::Map, modulation::{SimpleTickModulator, TickModulator}, }; @@ -24,7 +24,37 @@ pub struct Pacman<'a> { pub next_direction: Option, /// Whether Pac-Man is currently stopped. pub stopped: bool, - sprite: AnimatedAtlasTexture<'a>, + pub sprite: AnimatedAtlasTexture<'a>, +} + +impl<'a> Entity for Pacman<'a> { + fn base(&self) -> &StaticEntity { + &self.base.base + } +} + +impl<'a> Moving for Pacman<'a> { + fn move_forward(&mut self) { + self.base.move_forward(); + } + fn update_cell_position(&mut self) { + self.base.update_cell_position(); + } + fn next_cell(&self, direction: Option) -> (i32, i32) { + self.base.next_cell(direction) + } + fn is_wall_ahead(&self, direction: Option) -> bool { + self.base.is_wall_ahead(direction) + } + fn handle_tunnel(&mut self) -> bool { + self.base.handle_tunnel() + } + fn is_grid_aligned(&self) -> bool { + self.base.is_grid_aligned() + } + fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool { + self.base.set_direction_if_valid(new_direction) + } } impl Pacman<'_> { @@ -55,7 +85,7 @@ impl Pacman<'_> { match self.next_direction { None => return false, Some(next_direction) => { - if self.base.set_direction_if_valid(next_direction) { + if ::set_direction_if_valid(self, next_direction) { self.next_direction = None; return true; } @@ -69,53 +99,37 @@ impl Pacman<'_> { 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) { + pub 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 + ::update_cell_position(self); + if !::handle_tunnel(self) { 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) { + if !self.stopped && ::is_wall_ahead(self, None) { self.stopped = true; - } else if self.stopped && !self.base.is_wall_ahead(None) { + } else if self.stopped && !::is_wall_ahead(self, None) { self.stopped = false; } } } - if !self.stopped && self.base.modulation.next() { - self.base.move_forward(); + ::move_forward(self); if self.internal_position_even() == (0, 0) { - self.base.update_cell_position(); + ::update_cell_position(self); } } } } impl Renderable for Pacman<'_> { - fn render(&mut self, canvas: &mut Canvas) { + fn render(&self, canvas: &mut Canvas) { + let pos = self.base.base.pixel_position; + let dir = self.base.direction; if self.stopped { - self.sprite.render( - canvas, - self.base.pixel_position, - self.base.direction, - Some(2), - ); + self.sprite.render(canvas, pos, dir, Some(2)); } else { - self.sprite - .render(canvas, self.base.pixel_position, self.base.direction, None); + self.sprite.render(canvas, pos, dir, None); } } }