diff --git a/src/game.rs b/src/game.rs index 180d565..cd54642 100644 --- a/src/game.rs +++ b/src/game.rs @@ -245,6 +245,7 @@ impl Game { let ghost_movement_system = profile(SystemId::Ghost, ghost_movement_system); let collision_system = profile(SystemId::Collision, collision_system); let ghost_collision_system = profile(SystemId::GhostCollision, ghost_collision_system); + let vulnerable_tick_system = profile(SystemId::Ghost, systems::vulnerable_tick_system); let item_system = profile(SystemId::Item, item_system); let audio_system = profile(SystemId::Audio, audio_system); let blinking_system = profile(SystemId::Blinking, blinking_system); @@ -278,6 +279,7 @@ impl Game { .chain(), player_tunnel_slowdown_system, ghost_movement_system, + vulnerable_tick_system, (collision_system, ghost_collision_system, item_system).chain(), audio_system, blinking_system, diff --git a/src/systems/collision.rs b/src/systems/collision.rs index 2ba2763..ee776a2 100644 --- a/src/systems/collision.rs +++ b/src/systems/collision.rs @@ -8,7 +8,7 @@ use crate::error::GameError; use crate::events::GameEvent; use crate::map::builder::Map; use crate::systems::movement::Position; -use crate::systems::{AudioEvent, CombatState, Ghost, PlayerControlled, ScoreResource}; +use crate::systems::{AudioEvent, Ghost, PlayerControlled, ScoreResource, Vulnerable}; #[derive(Component)] pub struct Collider { @@ -111,14 +111,15 @@ pub fn ghost_collision_system( mut commands: Commands, mut collision_events: EventReader, mut score: ResMut, - pacman_query: Query<&CombatState, With>, + pacman_query: Query<(), With>, ghost_query: Query<(Entity, &Ghost), With>, + vulnerable_query: Query>, mut events: EventWriter, ) { for event in collision_events.read() { if let GameEvent::Collision(entity1, entity2) = event { // Check if one is Pacman and the other is a ghost - let (pacman_entity, ghost_entity) = if pacman_query.get(*entity1).is_ok() && ghost_query.get(*entity2).is_ok() { + let (_pacman_entity, ghost_entity) = if pacman_query.get(*entity1).is_ok() && ghost_query.get(*entity2).is_ok() { (*entity1, *entity2) } else if pacman_query.get(*entity2).is_ok() && ghost_query.get(*entity1).is_ok() { (*entity2, *entity1) @@ -126,24 +127,23 @@ pub fn ghost_collision_system( continue; }; - // Check if Pac-Man is energized - if let Ok(combat_state) = pacman_query.get(pacman_entity) { - if combat_state.is_energized() { + // Check if the ghost is vulnerable + if let Ok((ghost_ent, _ghost_type)) = ghost_query.get(ghost_entity) { + // Check if ghost has Vulnerable component + if vulnerable_query.get(ghost_ent).is_ok() { // Pac-Man eats the ghost - if let Ok((ghost_ent, _ghost_type)) = ghost_query.get(ghost_entity) { - // Add score (200 points per ghost eaten) - score.0 += 200; + // Add score (200 points per ghost eaten) + score.0 += 200; - // Remove the ghost - commands.entity(ghost_ent).despawn(); + // Remove the ghost + commands.entity(ghost_ent).despawn(); - // Play eat sound - events.write(AudioEvent::PlayEat); - } + // Play eat sound + events.write(AudioEvent::PlayEat); } else { // Pac-Man dies (this would need a death system) // For now, just log it - tracing::warn!("Pac-Man collided with ghost while not energized!"); + tracing::warn!("Pac-Man collided with ghost while not vulnerable!"); } } } diff --git a/src/systems/components.rs b/src/systems/components.rs index b4d3a37..c88be59 100644 --- a/src/systems/components.rs +++ b/src/systems/components.rs @@ -5,7 +5,7 @@ use crate::{ map::graph::TraversalFlags, systems::{ movement::{BufferedDirection, Position, Velocity}, - Collider, CombatState, ControlState, GhostCollider, ItemCollider, PacmanCollider, PlayerLifecycle, + Collider, GhostCollider, ItemCollider, PacmanCollider, PlayerLifecycle, }, texture::{animated::AnimatedTexture, sprite::AtlasTile}, }; @@ -174,6 +174,12 @@ impl LevelTiming { #[derive(Component, Debug, Clone, Copy)] pub struct Frozen; +/// Component for ghosts that are vulnerable to Pac-Man +#[derive(Component, Debug, Clone, Copy)] +pub struct Vulnerable { + pub remaining_ticks: u32, +} + #[derive(Bundle)] pub struct PlayerBundle { pub player: PlayerControlled, @@ -191,8 +197,6 @@ pub struct PlayerBundle { #[derive(Bundle, Default)] pub struct PlayerStateBundle { pub lifecycle: PlayerLifecycle, - pub control: ControlState, - pub combat: CombatState, pub movement_modifiers: MovementModifiers, } diff --git a/src/systems/item.rs b/src/systems/item.rs index 0897e23..e5d32f2 100644 --- a/src/systems/item.rs +++ b/src/systems/item.rs @@ -7,7 +7,7 @@ use bevy_ecs::{ use crate::{ events::GameEvent, - systems::{AudioEvent, CombatState, EntityType, ItemCollider, LevelTiming, PacmanCollider, ScoreResource}, + systems::{AudioEvent, EntityType, GhostCollider, ItemCollider, LevelTiming, PacmanCollider, ScoreResource, Vulnerable}, }; /// Determines if a collision between two entity types should be handled by the item system. @@ -26,8 +26,8 @@ pub fn item_system( mut collision_events: EventReader, mut score: ResMut, pacman_query: Query>, - mut combat_q: Query<&mut CombatState, With>, item_query: Query<(Entity, &EntityType), With>, + ghost_query: Query>, mut events: EventWriter, level_timing: Res, ) { @@ -55,16 +55,16 @@ pub fn item_system( events.write(AudioEvent::PlayEat); } - // Activate energizer on power pellet using tick-based durations + // Make ghosts vulnerable when power pellet is collected if *entity_type == EntityType::PowerPellet { - if let Ok(mut combat) = combat_q.single_mut() { - // Convert seconds to frames (assumes 60 FPS) - let total_ticks = (level_timing.energizer_duration * 60.0).round().clamp(0.0, u32::MAX as f32) as u32; - // Flash lead: e.g., 3 seconds (180 ticks) before end; ensure it doesn't underflow - let flash_lead_ticks = (level_timing.energizer_flash_threshold * 60.0) - .round() - .clamp(0.0, u32::MAX as f32) as u32; - combat.activate_energizer_ticks(total_ticks, flash_lead_ticks); + // Convert seconds to frames (assumes 60 FPS) + let total_ticks = (level_timing.energizer_duration * 60.0).round().clamp(0.0, u32::MAX as f32) as u32; + + // Add Vulnerable component to all ghosts + for ghost_entity in ghost_query.iter() { + commands.entity(ghost_entity).insert(Vulnerable { + remaining_ticks: total_ticks, + }); } } } diff --git a/src/systems/mod.rs b/src/systems/mod.rs index 98abe3b..6ac39da 100644 --- a/src/systems/mod.rs +++ b/src/systems/mod.rs @@ -17,6 +17,7 @@ pub mod player; pub mod profiling; pub mod render; pub mod stage; +pub mod vulnerable; pub use self::audio::*; pub use self::blinking::*; @@ -31,3 +32,4 @@ pub use self::player::*; pub use self::profiling::*; pub use self::render::*; pub use self::stage::*; +pub use self::vulnerable::*; diff --git a/src/systems/player.rs b/src/systems/player.rs index c355a47..3bd6599 100644 --- a/src/systems/player.rs +++ b/src/systems/player.rs @@ -1,7 +1,7 @@ use bevy_ecs::{ component::Component, event::{EventReader, EventWriter}, - query::With, + query::{With, Without}, system::{Query, Res, ResMut}, }; @@ -10,7 +10,7 @@ use crate::{ events::{GameCommand, GameEvent}, map::{builder::Map, graph::Edge}, systems::{ - components::{DeltaTime, EntityType, GlobalState, MovementModifiers, PlayerControlled}, + components::{DeltaTime, EntityType, Frozen, GlobalState, MovementModifiers, PlayerControlled}, debug::DebugState, movement::{BufferedDirection, Position, Velocity}, AudioState, @@ -39,79 +39,6 @@ impl Default for PlayerLifecycle { } } -/// Whether player input should be processed. -#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)] -pub enum ControlState { - InputEnabled, - InputLocked, -} - -impl Default for ControlState { - fn default() -> Self { - Self::InputLocked - } -} - -/// Combat-related state for Pac-Man. Tick-based energizer logic. -#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)] -pub enum CombatState { - Normal, - Energized { - /// Remaining energizer duration in ticks (frames) - remaining_ticks: u32, - /// Ticks until flashing begins (counts down to 0, then flashing is active) - flash_countdown_ticks: u32, - }, -} - -impl Default for CombatState { - fn default() -> Self { - CombatState::Normal - } -} - -impl CombatState { - pub fn is_energized(&self) -> bool { - matches!(self, CombatState::Energized { .. }) - } - - pub fn is_flashing(&self) -> bool { - matches!(self, CombatState::Energized { flash_countdown_ticks, .. } if *flash_countdown_ticks == 0) - } - - pub fn deactivate_energizer(&mut self) { - *self = CombatState::Normal; - } - - /// Activate energizer using tick-based durations. - pub fn activate_energizer_ticks(&mut self, total_ticks: u32, flash_lead_ticks: u32) { - let flash_countdown_ticks = total_ticks.saturating_sub(flash_lead_ticks); - *self = CombatState::Energized { - remaining_ticks: total_ticks, - flash_countdown_ticks, - }; - } - - /// Advance one frame. When ticks reach zero, returns to Normal. - pub fn tick_frame(&mut self) { - if let CombatState::Energized { - remaining_ticks, - flash_countdown_ticks, - } = self - { - if *remaining_ticks > 0 { - *remaining_ticks -= 1; - if *flash_countdown_ticks > 0 { - *flash_countdown_ticks -= 1; - } - } - if *remaining_ticks == 0 { - *self = CombatState::Normal; - } - } - } -} - /// Processes player input commands and updates game state accordingly. /// /// Handles keyboard-driven commands like movement direction changes, debug mode @@ -123,11 +50,11 @@ pub fn player_control_system( mut state: ResMut, mut debug_state: ResMut, mut audio_state: ResMut, - mut players: Query<(&PlayerLifecycle, &ControlState, &mut BufferedDirection), With>, + mut players: Query<(&PlayerLifecycle, &mut BufferedDirection), (With, Without)>, mut errors: EventWriter, ) { // Get the player's movable component (ensuring there is only one player) - let (lifecycle, control, mut buffered_direction) = match players.single_mut() { + let (lifecycle, mut buffered_direction) = match players.single_mut() { Ok(tuple) => tuple, Err(e) => { errors.write(GameError::InvalidState(format!( @@ -139,19 +66,17 @@ pub fn player_control_system( }; // If the player is not interactive or input is locked, ignore movement commands - let allow_input = lifecycle.is_interactive() && matches!(control, ControlState::InputEnabled); + // let allow_input = lifecycle.is_interactive(); // Handle events for event in events.read() { if let GameEvent::Command(command) = event { match command { GameCommand::MovePlayer(direction) => { - if allow_input { - *buffered_direction = BufferedDirection::Some { - direction: *direction, - remaining_time: 0.25, - }; - } + *buffered_direction = BufferedDirection::Some { + direction: *direction, + remaining_time: 0.25, + }; } GameCommand::Exit => { state.exit = true; @@ -185,21 +110,16 @@ pub fn player_movement_system( mut entities: Query< ( &PlayerLifecycle, - &ControlState, &MovementModifiers, &mut Position, &mut Velocity, &mut BufferedDirection, ), - With, + (With, Without), >, // mut errors: EventWriter, ) { - for (lifecycle, control, modifiers, mut position, mut velocity, mut buffered_direction) in entities.iter_mut() { - if !lifecycle.is_interactive() || !matches!(control, ControlState::InputEnabled) { - continue; - } - + for (lifecycle, modifiers, mut position, mut velocity, mut buffered_direction) in entities.iter_mut() { // Decrement the buffered direction remaining time if let BufferedDirection::Some { direction, diff --git a/src/systems/stage.rs b/src/systems/stage.rs index 1d86f63..0f3ece5 100644 --- a/src/systems/stage.rs +++ b/src/systems/stage.rs @@ -82,10 +82,12 @@ pub fn startup_stage_system( // TODO: Remove TextOnly tag component } (StartupSequence::CharactersVisible { .. }, StartupSequence::GameActive) => { - // Remove Frozen tag from all entities + // Remove Frozen tag from all entities and enable player input for entity in player_query.iter_mut().chain(ghost_query.iter_mut()) { + tracing::info!("Removing Frozen component from entity {}", entity); commands.entity(entity).remove::(); } + // TODO: Add GameActive tag component // TODO: Remove CharactersVisible tag component } diff --git a/src/systems/vulnerable.rs b/src/systems/vulnerable.rs new file mode 100644 index 0000000..41d4c59 --- /dev/null +++ b/src/systems/vulnerable.rs @@ -0,0 +1,20 @@ +use bevy_ecs::query::With; +use bevy_ecs::system::{Commands, Query}; + +use crate::systems::{GhostCollider, Vulnerable}; + +/// System that decrements the remaining_ticks on Vulnerable components and removes them when they reach zero +pub fn vulnerable_tick_system( + mut commands: Commands, + mut vulnerable_query: Query<(bevy_ecs::entity::Entity, &mut Vulnerable), With>, +) { + for (entity, mut vulnerable) in vulnerable_query.iter_mut() { + if vulnerable.remaining_ticks > 0 { + vulnerable.remaining_ticks -= 1; + } + + if vulnerable.remaining_ticks == 0 { + commands.entity(entity).remove::(); + } + } +}