From 9b441fa35c1635e0656c0bb4a523121329704c8a Mon Sep 17 00:00:00 2001 From: Xevion Date: Thu, 24 Jul 2025 16:23:09 -0500 Subject: [PATCH] feat: shared blinking textures for power pellets --- src/entity/edible.rs | 41 +++++++++++++++++---------- src/game.rs | 29 ++++++++++--------- src/texture/blinking.rs | 62 +++++++++++++++++++++++++++++++++++++++++ src/texture/mod.rs | 1 + 4 files changed, 106 insertions(+), 27 deletions(-) create mode 100644 src/texture/blinking.rs diff --git a/src/entity/edible.rs b/src/entity/edible.rs index f12e4ee..db75bf8 100644 --- a/src/entity/edible.rs +++ b/src/entity/edible.rs @@ -4,6 +4,7 @@ use crate::entity::direction::Direction; use crate::entity::{Entity, Renderable, StaticEntity}; use crate::map::Map; use crate::texture::atlas::AtlasTexture; +use crate::texture::blinking::BlinkingTexture; use crate::texture::FrameDrawn; use glam::{IVec2, UVec2}; use sdl2::{render::Canvas, video::Window}; @@ -17,19 +18,32 @@ pub enum EdibleKind { Fruit(FruitType), } +pub enum EdibleSprite { + Pellet(Rc>), + PowerPellet(Rc>), +} + pub struct Edible { pub base: StaticEntity, pub kind: EdibleKind, - pub sprite: Rc, + pub sprite: EdibleSprite, } impl Edible { - pub fn new(kind: EdibleKind, cell_position: UVec2, sprite: Rc) -> Self { + pub fn new_pellet(cell_position: UVec2, sprite: Rc>) -> Self { let pixel_position = Map::cell_to_pixel(cell_position); Edible { base: StaticEntity::new(pixel_position, cell_position), - kind, - sprite, + kind: EdibleKind::Pellet, + sprite: EdibleSprite::Pellet(sprite), + } + } + pub fn new_power_pellet(cell_position: UVec2, sprite: Rc>) -> Self { + let pixel_position = Map::cell_to_pixel(cell_position); + Edible { + base: StaticEntity::new(pixel_position, cell_position), + kind: EdibleKind::PowerPellet, + sprite: EdibleSprite::PowerPellet(sprite), } } @@ -48,16 +62,19 @@ impl Entity for Edible { impl Renderable for Edible { fn render(&self, canvas: &mut Canvas) { let pos = self.base.pixel_position; - self.sprite.render(canvas, pos, Direction::Right, Some(0)); + match &self.sprite { + EdibleSprite::Pellet(sprite) => sprite.render(canvas, pos, Direction::Right, Some(0)), + EdibleSprite::PowerPellet(sprite) => sprite.borrow().render(canvas, pos, Direction::Right, Some(0)), + } } } /// Reconstruct all edibles from the original map layout pub fn reconstruct_edibles( map: Rc>, - pellet_sprite: Rc, - power_pellet_sprite: Rc, - _fruit_sprite: Rc, + pellet_sprite: Rc>, + power_pellet_sprite: Rc>, + _fruit_sprite: Rc>, ) -> Vec { let mut edibles = Vec::new(); for x in 0..BOARD_WIDTH { @@ -65,14 +82,10 @@ pub fn reconstruct_edibles( let tile = map.borrow().get_tile(IVec2::new(x as i32, y as i32)); match tile { Some(MapTile::Pellet) => { - edibles.push(Edible::new(EdibleKind::Pellet, UVec2::new(x, y), Rc::clone(&pellet_sprite))); + edibles.push(Edible::new_pellet(UVec2::new(x, y), Rc::clone(&pellet_sprite))); } Some(MapTile::PowerPellet) => { - edibles.push(Edible::new( - EdibleKind::PowerPellet, - UVec2::new(x, y), - Rc::clone(&power_pellet_sprite), - )); + edibles.push(Edible::new_power_pellet(UVec2::new(x, y), Rc::clone(&power_pellet_sprite))); } // Fruits can be added here if you have fruit positions _ => {} diff --git a/src/game.rs b/src/game.rs index cf80367..4a4856f 100644 --- a/src/game.rs +++ b/src/game.rs @@ -26,6 +26,8 @@ use crate::entity::pacman::Pacman; use crate::entity::Renderable; use crate::map::Map; use crate::texture::atlas::{texture_to_static, AtlasTexture}; +use crate::texture::blinking::BlinkingTexture; +use crate::texture::FrameDrawn; /// The main game state. /// @@ -34,8 +36,8 @@ use crate::texture::atlas::{texture_to_static, AtlasTexture}; pub struct Game { canvas: &'static mut Canvas, map_texture: Texture<'static>, - pellet_texture: Rc, - power_pellet_texture: Rc, + pellet_texture: Rc>, + power_pellet_texture: Rc>, font: Font<'static, 'static>, pacman: Rc>, map: Rc>, @@ -92,7 +94,7 @@ impl Game { // Load pellet texture from asset API let pellet_bytes = get_asset_bytes(Asset::Pellet).expect("Failed to load asset"); let power_pellet_bytes = get_asset_bytes(Asset::Energizer).expect("Failed to load asset"); - let pellet_texture = Rc::new(AtlasTexture::new( + let pellet_texture: Rc> = Rc::new(Box::new(AtlasTexture::new( unsafe { texture_to_static( texture_creator @@ -104,20 +106,18 @@ impl Game { 24, 24, None, - )); - let power_pellet_texture = Rc::new(AtlasTexture::new( - unsafe { - texture_to_static( - texture_creator - .load_texture_bytes(&power_pellet_bytes) - .expect("Could not load power pellet texture from asset API"), - ) - }, + ))); + let power_pellet_texture = Rc::new(RefCell::new(BlinkingTexture::new( + texture_creator + .load_texture_bytes(&power_pellet_bytes) + .expect("Could not load power pellet texture from asset API"), 1, 24, 24, None, - )); + 30, // on_ticks + 9, // off_ticks + ))); // Load map texture from asset API let map_bytes = get_asset_bytes(Asset::Map).expect("Failed to load asset"); @@ -262,6 +262,9 @@ impl Game { self.blinky.body_sprite.tick(); self.blinky.eyes_sprite.tick(); + // Advance blinking for power pellets + self.power_pellet_texture.borrow_mut().tick(); + let pacman = self.pacman.borrow(); let mut eaten_indices = vec![]; for (i, edible) in self.edibles.iter().enumerate() { diff --git a/src/texture/blinking.rs b/src/texture/blinking.rs new file mode 100644 index 0000000..59ad3a1 --- /dev/null +++ b/src/texture/blinking.rs @@ -0,0 +1,62 @@ +//! A texture that blinks on/off for a specified number of ticks. +use glam::IVec2; +use sdl2::{ + render::{Canvas, Texture}, + video::Window, +}; + +use crate::texture::atlas::AtlasTexture; +use crate::texture::FrameDrawn; +use crate::{entity::direction::Direction, texture::atlas::texture_to_static}; + +pub struct BlinkingTexture { + pub atlas: AtlasTexture, + pub on_ticks: u32, + pub off_ticks: u32, + pub ticker: u32, + pub visible: bool, +} + +impl BlinkingTexture { + pub fn new( + texture: Texture<'_>, + frame_count: u32, + width: u32, + height: u32, + offset: Option, + on_ticks: u32, + off_ticks: u32, + ) -> Self { + BlinkingTexture { + atlas: AtlasTexture::new(unsafe { texture_to_static(texture) }, frame_count, width, height, offset), + on_ticks, + off_ticks, + ticker: 0, + visible: true, + } + } + + /// Advances the blinking state by one tick. + pub fn tick(&mut self) { + self.ticker += 1; + if self.visible && self.ticker >= self.on_ticks { + self.visible = false; + self.ticker = 0; + } else if !self.visible && self.ticker >= self.off_ticks { + self.visible = true; + self.ticker = 0; + } + } + + pub fn set_color_modulation(&mut self, r: u8, g: u8, b: u8) { + self.atlas.set_color_modulation(r, g, b); + } +} + +impl FrameDrawn for BlinkingTexture { + fn render(&self, canvas: &mut Canvas, position: IVec2, direction: Direction, frame: Option) { + if self.visible { + self.atlas.render(canvas, position, direction, frame); + } + } +} diff --git a/src/texture/mod.rs b/src/texture/mod.rs index 75c34b0..c1e49fc 100644 --- a/src/texture/mod.rs +++ b/src/texture/mod.rs @@ -10,3 +10,4 @@ pub trait FrameDrawn { pub mod animated; pub mod atlas; +pub mod blinking;