From cde1ea539404c016f7931a5db07ed467c5d0a9e2 Mon Sep 17 00:00:00 2001 From: Ryan Walters Date: Thu, 28 Aug 2025 20:02:27 -0500 Subject: [PATCH] feat: allow freezing of blinking entities, lightly refactor game.rs structure --- src/game.rs | 68 ++++++++++++++++++++++------------------- src/systems/blinking.rs | 44 ++++++++++++++++++-------- src/systems/stage.rs | 10 ++++-- 3 files changed, 77 insertions(+), 45 deletions(-) diff --git a/src/game.rs b/src/game.rs index b51fbe8..04f4b03 100644 --- a/src/game.rs +++ b/src/game.rs @@ -2,7 +2,7 @@ include!(concat!(env!("OUT_DIR"), "/atlas_data.rs")); -use crate::constants::CANVAS_SIZE; +use crate::constants::{MapTile, CANVAS_SIZE}; use crate::error::{GameError, GameResult, TextureError}; use crate::events::GameEvent; use crate::map::builder::Map; @@ -21,6 +21,7 @@ use crate::systems::{ PacmanCollider, PlayerBundle, PlayerControlled, Renderable, ScoreResource, StartupSequence, SystemTimings, }; use crate::texture::animated::AnimatedTexture; +use crate::texture::sprite::AtlasTile; use bevy_ecs::event::EventRegistry; use bevy_ecs::observer::Trigger; use bevy_ecs::schedule::{IntoScheduleConfigs, Schedule, SystemSet}; @@ -81,14 +82,7 @@ impl Game { texture_creator: &'static mut TextureCreator, event_pump: &'static mut EventPump, ) -> GameResult { - let mut world = World::default(); - let mut schedule = Schedule::default(); let ttf_context = Box::leak(Box::new(sdl2::ttf::init().map_err(|e| GameError::Sdl(e.to_string()))?)); - - EventRegistry::register_event::(&mut world); - EventRegistry::register_event::(&mut world); - EventRegistry::register_event::(&mut world); - let mut backbuffer = texture_creator .create_texture_target(None, CANVAS_SIZE.x, CANVAS_SIZE.y) .map_err(|e| GameError::Sdl(e.to_string()))?; @@ -105,6 +99,7 @@ impl Game { .create_texture_target(None, output_size.0, output_size.1) .map_err(|e| GameError::Sdl(e.to_string()))?; + // Debug texture is copied over the backbuffer, it requires transparency abilities debug_texture.set_blend_mode(BlendMode::Blend); debug_texture.set_scale_mode(ScaleMode::Nearest); @@ -151,11 +146,10 @@ impl Game { .map_err(|e| GameError::Sdl(e.to_string()))?; let map = Map::new(constants::RAW_BOARD)?; - let pacman_start_node = map.start_positions.pacman; + // Create directional animated textures for Pac-Man let mut textures = [None, None, None, None]; let mut stopped_textures = [None, None, None, None]; - for direction in Direction::DIRECTIONS { let moving_prefix = match direction { Direction::Up => "pacman/up", @@ -181,7 +175,9 @@ impl Game { let player = PlayerBundle { player: PlayerControlled, - position: Position::Stopped { node: pacman_start_node }, + position: Position::Stopped { + node: map.start_positions.pacman, + }, velocity: Velocity { speed: 1.15, direction: Direction::Left, @@ -204,9 +200,12 @@ impl Game { pacman_collider: PacmanCollider, }; - // Spawn player and attach initial state bundle - let player_entity = world.spawn(player).id(); - world.entity_mut(player_entity).insert(Frozen); + let mut world = World::default(); + let mut schedule = Schedule::default(); + + EventRegistry::register_event::(&mut world); + EventRegistry::register_event::(&mut world); + EventRegistry::register_event::(&mut world); world.insert_non_send_resource(atlas); world.insert_non_send_resource(event_pump); @@ -227,6 +226,7 @@ impl Game { world.insert_resource(DebugState::default()); world.insert_resource(AudioState::default()); world.insert_resource(CursorPosition::default()); + world.insert_resource(StartupSequence::new(60 * 3, 60)); world.add_observer( |event: Trigger, mut state: ResMut, _score: ResMut| { @@ -280,41 +280,47 @@ impl Game { .chain(), )); - world.insert_resource(StartupSequence::new(60 * 3, 60)); + // Spawn player and attach initial state bundle + let player_entity = world.spawn(player).id(); + world.entity_mut(player_entity).insert(Frozen); // Spawn ghosts Self::spawn_ghosts(&mut world)?; - // Spawn items let pellet_sprite = SpriteAtlas::get_tile(world.non_send_resource::(), "maze/pellet.png") .ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("maze/pellet.png".to_string())))?; let energizer_sprite = SpriteAtlas::get_tile(world.non_send_resource::(), "maze/energizer.png") .ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("maze/energizer.png".to_string())))?; - let nodes: Vec<_> = world.resource::().iter_nodes().map(|(id, tile)| (*id, *tile)).collect(); - - for (node_id, tile) in nodes { - let (item_type, sprite, size) = match tile { - crate::constants::MapTile::Pellet => (EntityType::Pellet, pellet_sprite, constants::CELL_SIZE as f32 * 0.4), - crate::constants::MapTile::PowerPellet => { - (EntityType::PowerPellet, energizer_sprite, constants::CELL_SIZE as f32 * 0.95) - } - _ => continue, - }; + // Build a list of item entities to spawn from the map + let nodes: Vec<(usize, EntityType, AtlasTile, f32)> = world + .resource::() + .iter_nodes() + .filter_map(|(id, tile)| match tile { + MapTile::Pellet => Some((*id, EntityType::Pellet, pellet_sprite, constants::CELL_SIZE as f32 * 0.4)), + MapTile::PowerPellet => Some(( + *id, + EntityType::PowerPellet, + energizer_sprite, + constants::CELL_SIZE as f32 * 0.95, + )), + _ => None, + }) + .collect(); + // Construct and spawn the item entities + for (id, item_type, sprite, size) in nodes { let mut item = world.spawn(ItemBundle { - position: Position::Stopped { node: node_id }, + position: Position::Stopped { node: id }, sprite: Renderable { sprite, layer: 1 }, entity_type: item_type, collider: Collider { size }, item_collider: ItemCollider, }); + // Make power pellets blink if item_type == EntityType::PowerPellet { - item.insert(Blinking { - timer: 0.0, - interval: 0.2, - }); + item.insert((Frozen, Blinking::new(0.2))); } } diff --git a/src/systems/blinking.rs b/src/systems/blinking.rs index 9623972..728d029 100644 --- a/src/systems/blinking.rs +++ b/src/systems/blinking.rs @@ -7,15 +7,21 @@ use bevy_ecs::{ use crate::systems::{ components::{DeltaTime, Renderable}, - Hidden, + Frozen, Hidden, }; -#[derive(Component)] +#[derive(Component, Debug)] pub struct Blinking { pub timer: f32, pub interval: f32, } +impl Blinking { + pub fn new(interval: f32) -> Self { + Self { timer: 0.0, interval } + } +} + /// Updates blinking entities by toggling their visibility at regular intervals. /// /// This system manages entities that have both `Blinking` and `Renderable` components, @@ -23,20 +29,34 @@ pub struct Blinking { pub fn blinking_system( mut commands: Commands, time: Res, - mut query: Query<(Entity, &mut Blinking, Has), With>, + mut query: Query<(Entity, &mut Blinking, Has, Has), With>, ) { - for (entity, mut blinking, hidden) in query.iter_mut() { - blinking.timer += time.0; - - if blinking.timer >= blinking.interval { - blinking.timer = 0.0; - - // Add or remove the Visible component based on whether it is currently in the query + for (entity, mut blinking, hidden, frozen) in query.iter_mut() { + // If the entity is frozen, blinking is disabled and the entity is unhidden (if it was hidden) + if frozen { if hidden { commands.entity(entity).remove::(); - } else { - commands.entity(entity).insert(Hidden); } + + continue; + } + + // Increase the timer by the delta time + blinking.timer += time.0; + + // If the timer is less than the interval, there's nothing to do yet + if blinking.timer < blinking.interval { + continue; + } + + // Subtract the interval (allows for the timer to retain partial interval progress) + blinking.timer -= blinking.interval; + + // Toggle the Hidden component + if hidden { + commands.entity(entity).remove::(); + } else { + commands.entity(entity).insert(Hidden); } } } diff --git a/src/systems/stage.rs b/src/systems/stage.rs index e0905e0..96f6d4d 100644 --- a/src/systems/stage.rs +++ b/src/systems/stage.rs @@ -6,7 +6,7 @@ use bevy_ecs::{ }; use tracing::debug; -use crate::systems::{Frozen, GhostCollider, Hidden, PlayerControlled}; +use crate::systems::{Blinking, Frozen, GhostCollider, Hidden, PlayerControlled}; #[derive(Resource, Debug, Clone, Copy)] pub enum StartupSequence { @@ -72,6 +72,7 @@ impl StartupSequence { pub fn startup_stage_system( mut startup: ResMut, mut commands: Commands, + mut blinking_query: Query>, mut player_query: Query>, mut ghost_query: Query>, ) { @@ -80,10 +81,15 @@ pub fn startup_stage_system( match (from, to) { // (StartupSequence::TextOnly { .. }, StartupSequence::CharactersVisible { .. }) => {} (StartupSequence::CharactersVisible { .. }, StartupSequence::GameActive) => { - // Remove Frozen/Hidden tags from all entities and enable player input + // Unfreeze and unhide the player & ghosts for entity in player_query.iter_mut().chain(ghost_query.iter_mut()) { commands.entity(entity).remove::<(Frozen, Hidden)>(); } + + // Unfreeze pellet blinking + for entity in blinking_query.iter_mut() { + commands.entity(entity).remove::(); + } } _ => {} }