feat: setup pacman collision, level restart, game over, death sequence, switch to Vec for TileSequence

This commit is contained in:
Ryan Walters
2025-09-08 01:14:32 -05:00
parent 53306de155
commit 823f480916
10 changed files with 531 additions and 310 deletions

View File

@@ -1,15 +1,20 @@
use bevy_ecs::component::Component;
use bevy_ecs::entity::Entity;
use bevy_ecs::event::{EventReader, EventWriter};
use bevy_ecs::query::With;
use bevy_ecs::system::{Query, Res, ResMut};
use bevy_ecs::{
component::Component,
entity::Entity,
event::{EventReader, EventWriter},
query::With,
system::{Commands, Query, Res, ResMut},
};
use crate::error::GameError;
use crate::events::GameEvent;
use crate::map::builder::Map;
use crate::systems::movement::Position;
use crate::systems::{AudioEvent, Ghost, GhostState, PlayerControlled, ScoreResource};
use crate::systems::{
components::GhostState, movement::Position, AudioEvent, DyingSequence, Frozen, GameStage, Ghost, PlayerControlled,
ScoreResource,
};
/// A component for defining the collision area of an entity.
#[derive(Component)]
pub struct Collider {
pub size: f32,
@@ -62,6 +67,7 @@ pub fn check_collision(
///
/// Also detects collisions between Pac-Man and ghosts for gameplay mechanics like
/// power pellet effects, ghost eating, and player death.
#[allow(clippy::too_many_arguments)]
pub fn collision_system(
map: Res<Map>,
pacman_query: Query<(Entity, &Position, &Collider), With<PacmanCollider>>,
@@ -107,10 +113,13 @@ pub fn collision_system(
}
}
#[allow(clippy::too_many_arguments)]
pub fn ghost_collision_system(
mut commands: Commands,
mut collision_events: EventReader<GameEvent>,
mut score: ResMut<ScoreResource>,
pacman_query: Query<(), With<PlayerControlled>>,
mut game_state: ResMut<GameStage>,
pacman_query: Query<Entity, With<PlayerControlled>>,
ghost_query: Query<(Entity, &Ghost), With<GhostCollider>>,
mut ghost_state_query: Query<&mut GhostState>,
mut events: EventWriter<AudioEvent>,
@@ -118,7 +127,7 @@ pub fn ghost_collision_system(
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)
@@ -140,8 +149,12 @@ pub fn ghost_collision_system(
// Play eat sound
events.write(AudioEvent::PlayEat);
} else {
// Pac-Man dies (this would need a death system)
} else if matches!(*ghost_state, GhostState::Normal) {
// Pac-Man dies
*game_state = GameStage::PlayerDying(DyingSequence::Frozen { remaining_ticks: 60 });
commands.entity(pacman_entity).insert(Frozen);
commands.entity(ghost_entity).insert(Frozen);
events.write(AudioEvent::StopAll);
}
}
}

View File

@@ -101,7 +101,7 @@ pub struct Renderable {
}
/// Directional animation component with shared timing across all directions
#[derive(Component, Clone, Copy)]
#[derive(Component, Clone)]
pub struct DirectionalAnimation {
pub moving_tiles: DirectionalTiles,
pub stopped_tiles: DirectionalTiles,
@@ -123,13 +123,18 @@ impl DirectionalAnimation {
}
}
/// Tag component to mark animations that should loop when they reach the end
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
pub struct Looping;
/// Linear animation component for non-directional animations (frightened ghosts)
#[derive(Component, Clone, Copy)]
#[derive(Component, Resource, Clone)]
pub struct LinearAnimation {
pub tiles: TileSequence,
pub current_frame: usize,
pub time_bank: u16,
pub frame_duration: u16,
pub finished: bool,
}
impl LinearAnimation {
@@ -140,6 +145,7 @@ impl LinearAnimation {
current_frame: 0,
time_bank: 0,
frame_duration,
finished: false,
}
}
}
@@ -218,6 +224,11 @@ pub struct Frozen;
#[derive(Component, Debug, Clone, Copy)]
pub struct Eaten;
/// Tag component for Pac-Man during his death animation.
/// This is mainly because the Frozen tag would stop both movement and animation, while the Dying tag can signal that the animation should continue despite being frozen.
#[derive(Component, Debug, Clone, Copy)]
pub struct Dying;
#[derive(Component, Debug, Clone, Copy)]
pub enum GhostState {
/// Normal ghost behavior - chasing Pac-Man

View File

@@ -1,5 +1,7 @@
use crate::platform;
use crate::systems::components::{DirectionalAnimation, Frozen, GhostAnimation, GhostState, LastAnimationState, LinearAnimation};
use crate::systems::components::{
DirectionalAnimation, Frozen, GhostAnimation, GhostState, LastAnimationState, LinearAnimation, Looping,
};
use crate::{
map::{
builder::Map,
@@ -194,22 +196,26 @@ pub fn ghost_state_system(
if last_animation_state.0 != current_animation_state {
match current_animation_state {
GhostAnimation::Frightened { flash } => {
// Remove DirectionalAnimation, add LinearAnimation
// Remove DirectionalAnimation, add LinearAnimation with Looping component
commands
.entity(entity)
.remove::<DirectionalAnimation>()
.insert(*animations.frightened(flash));
.insert(animations.frightened(flash).clone())
.insert(Looping);
}
GhostAnimation::Normal => {
// Remove LinearAnimation, add DirectionalAnimation
// Remove LinearAnimation and Looping, add DirectionalAnimation
commands
.entity(entity)
.remove::<LinearAnimation>()
.insert(*animations.get_normal(ghost_type).unwrap());
.remove::<(LinearAnimation, Looping)>()
.insert(animations.get_normal(ghost_type).unwrap().clone());
}
GhostAnimation::Eyes => {
// Remove LinearAnimation, add DirectionalAnimation (eyes animation)
commands.entity(entity).remove::<LinearAnimation>().insert(*animations.eyes());
// Remove LinearAnimation and Looping, add DirectionalAnimation (eyes animation)
commands
.entity(entity)
.remove::<(LinearAnimation, Looping)>()
.insert(animations.eyes().clone());
}
}
last_animation_state.0 = current_animation_state;

View File

@@ -1,25 +1,25 @@
//! The Entity-Component-System (ECS) module.
//!
//! This module contains all the ECS-related logic, including components, systems,
//! and resources.
//! This module contains all the systems in the game.
#[cfg_attr(coverage_nightly, coverage(off))]
pub mod audio;
#[cfg_attr(coverage_nightly, coverage(off))]
pub mod debug;
#[cfg_attr(coverage_nightly, coverage(off))]
pub mod profiling;
#[cfg_attr(coverage_nightly, coverage(off))]
pub mod render;
pub mod blinking;
pub mod collision;
pub mod components;
#[cfg_attr(coverage_nightly, coverage(off))]
pub mod debug;
pub mod ghost;
pub mod input;
pub mod item;
pub mod movement;
pub mod player;
#[cfg_attr(coverage_nightly, coverage(off))]
pub mod profiling;
#[cfg_attr(coverage_nightly, coverage(off))]
pub mod render;
pub mod stage;
pub mod state;
// Re-export all the modules. Do not fine-tune the exports.
pub use self::audio::*;
pub use self::blinking::*;
@@ -33,4 +33,4 @@ pub use self::movement::*;
pub use self::player::*;
pub use self::profiling::*;
pub use self::render::*;
pub use self::stage::*;
pub use self::state::*;

View File

@@ -1,18 +1,20 @@
use crate::constants::CANVAS_SIZE;
use crate::error::{GameError, TextureError};
use crate::map::builder::Map;
use crate::systems::input::TouchState;
use crate::systems::{
debug_render_system, BatchedLinesResource, Collider, CursorPosition, DebugState, DebugTextureResource, DeltaTime,
DirectionalAnimation, LinearAnimation, Position, Renderable, ScoreResource, StartupSequence, SystemId, SystemTimings,
TtfAtlasResource, Velocity,
DirectionalAnimation, Dying, Frozen, GameStage, LinearAnimation, Looping, Position, Renderable, ScoreResource,
StartupSequence, SystemId, SystemTimings, TtfAtlasResource, Velocity,
};
use crate::texture::sprite::SpriteAtlas;
use crate::texture::text::TextTexture;
use crate::{
constants::CANVAS_SIZE,
error::{GameError, TextureError},
};
use bevy_ecs::component::Component;
use bevy_ecs::entity::Entity;
use bevy_ecs::event::EventWriter;
use bevy_ecs::query::{Changed, Or, Without};
use bevy_ecs::query::{Changed, Has, Or, With, Without};
use bevy_ecs::removal_detection::RemovedComponents;
use bevy_ecs::resource::Resource;
use bevy_ecs::system::{NonSendMut, Query, Res, ResMut};
@@ -53,7 +55,7 @@ pub fn dirty_render_system(
/// All directions share the same frame timing to ensure perfect synchronization.
pub fn directional_render_system(
dt: Res<DeltaTime>,
mut query: Query<(&Position, &Velocity, &mut DirectionalAnimation, &mut Renderable)>,
mut query: Query<(&Position, &Velocity, &mut DirectionalAnimation, &mut Renderable), Without<Frozen>>,
) {
let ticks = (dt.seconds * 60.0).round() as u16; // Convert from seconds to ticks at 60 ticks/sec
@@ -86,26 +88,35 @@ pub fn directional_render_system(
}
}
/// Updates linear animated entities (used for non-directional animations like frightened ghosts).
///
/// This system handles entities that use LinearAnimation component for simple frame cycling.
pub fn linear_render_system(dt: Res<DeltaTime>, mut query: Query<(&mut LinearAnimation, &mut Renderable)>) {
let ticks = (dt.seconds * 60.0).round() as u16; // Convert from seconds to ticks at 60 ticks/sec
for (mut anim, mut renderable) in query.iter_mut() {
// Tick animation
anim.time_bank += ticks;
while anim.time_bank >= anim.frame_duration {
anim.time_bank -= anim.frame_duration;
anim.current_frame += 1;
/// System that updates `Renderable` sprites for entities with `LinearAnimation`.
#[allow(clippy::type_complexity)]
pub fn linear_render_system(
dt: Res<DeltaTime>,
mut query: Query<(&mut LinearAnimation, &mut Renderable, Has<Looping>), Or<(Without<Frozen>, With<Dying>)>>,
) {
for (mut anim, mut renderable, looping) in query.iter_mut() {
if anim.finished {
continue;
}
if !anim.tiles.is_empty() {
let new_tile = anim.tiles.get_tile(anim.current_frame);
if renderable.sprite != new_tile {
renderable.sprite = new_tile;
}
anim.time_bank += dt.ticks as u16;
let frames_to_advance = (anim.time_bank / anim.frame_duration) as usize;
if frames_to_advance == 0 {
continue;
}
let total_frames = anim.tiles.len();
if !looping && anim.current_frame + frames_to_advance >= total_frames {
anim.finished = true;
anim.current_frame = total_frames - 1;
} else {
anim.current_frame += frames_to_advance;
}
anim.time_bank %= anim.frame_duration;
renderable.sprite = anim.tiles.get_tile(anim.current_frame);
}
}
@@ -194,7 +205,7 @@ pub fn hud_render_system(
mut canvas: NonSendMut<&mut Canvas<Window>>,
mut atlas: NonSendMut<SpriteAtlas>,
score: Res<ScoreResource>,
startup: Res<StartupSequence>,
stage: Res<GameStage>,
mut errors: EventWriter<GameError>,
) {
let _ = canvas.with_texture_canvas(&mut backbuffer.0, |canvas| {
@@ -226,10 +237,21 @@ pub fn hud_render_system(
errors.write(TextureError::RenderFailed(format!("Failed to render high score text: {}", e)).into());
}
// Render GAME OVER text
if matches!(*stage, GameStage::GameOver) {
let game_over_text = "GAME OVER";
let game_over_width = text_renderer.text_width(game_over_text);
let game_over_position = glam::UVec2::new((CANVAS_SIZE.x - game_over_width) / 2, 160);
if let Err(e) = text_renderer.render_with_color(canvas, &mut atlas, game_over_text, game_over_position, Color::RED) {
errors.write(TextureError::RenderFailed(format!("Failed to render GAME OVER text: {}", e)).into());
}
}
// Render text based on StartupSequence stage
if matches!(
*startup,
StartupSequence::TextOnly { .. } | StartupSequence::CharactersVisible { .. }
*stage,
GameStage::Starting(StartupSequence::TextOnly { .. })
| GameStage::Starting(StartupSequence::CharactersVisible { .. })
) {
let ready_text = "READY!";
let ready_width = text_renderer.text_width(ready_text);
@@ -238,7 +260,7 @@ pub fn hud_render_system(
errors.write(TextureError::RenderFailed(format!("Failed to render READY text: {}", e)).into());
}
if matches!(*startup, StartupSequence::TextOnly { .. }) {
if matches!(*stage, GameStage::Starting(StartupSequence::TextOnly { .. })) {
let player_one_text = "PLAYER ONE";
let player_one_width = text_renderer.text_width(player_one_text);
let player_one_position = glam::UVec2::new((CANVAS_SIZE.x - player_one_width) / 2, 113);

View File

@@ -1,101 +0,0 @@
use bevy_ecs::{
entity::Entity,
query::With,
resource::Resource,
system::{Commands, Query, ResMut},
};
use tracing::debug;
use crate::systems::{Blinking, Frozen, GhostCollider, Hidden, PlayerControlled};
#[derive(Resource, Debug, Clone, Copy)]
pub enum StartupSequence {
/// Stage 1: Text-only stage
/// - Player & ghosts are hidden
/// - READY! and PLAYER ONE text are shown
/// - Energizers do not blink
TextOnly {
/// Remaining ticks in this stage
remaining_ticks: u32,
},
/// Stage 2: Characters visible stage
/// - PLAYER ONE text is hidden, READY! text remains
/// - Ghosts and Pac-Man are now shown
CharactersVisible {
/// Remaining ticks in this stage
remaining_ticks: u32,
},
/// Stage 3: Game begins
/// - Final state, game is fully active
GameActive,
}
impl StartupSequence {
/// Creates a new StartupSequence with the specified duration in ticks
pub fn new(text_only_ticks: u32, _characters_visible_ticks: u32) -> Self {
Self::TextOnly {
remaining_ticks: text_only_ticks,
}
}
/// Ticks the timer by one frame, returning transition information if state changes
pub fn tick(&mut self) -> Option<(StartupSequence, StartupSequence)> {
match self {
StartupSequence::TextOnly { remaining_ticks } => {
if *remaining_ticks > 0 {
*remaining_ticks -= 1;
None
} else {
let from = *self;
*self = StartupSequence::CharactersVisible {
remaining_ticks: 60, // 1 second at 60 FPS
};
Some((from, *self))
}
}
StartupSequence::CharactersVisible { remaining_ticks } => {
if *remaining_ticks > 0 {
*remaining_ticks -= 1;
None
} else {
let from = *self;
*self = StartupSequence::GameActive;
Some((from, *self))
}
}
StartupSequence::GameActive => None,
}
}
}
/// Handles startup sequence transitions and component management
pub fn startup_stage_system(
mut startup: ResMut<StartupSequence>,
mut commands: Commands,
mut blinking_query: Query<Entity, With<Blinking>>,
mut player_query: Query<Entity, With<PlayerControlled>>,
mut ghost_query: Query<Entity, With<GhostCollider>>,
) {
if let Some((from, to)) = startup.tick() {
debug!("StartupSequence transition from {from:?} to {to:?}");
match (from, to) {
(StartupSequence::TextOnly { .. }, StartupSequence::CharactersVisible { .. }) => {
// Unhide the player & ghosts
for entity in player_query.iter_mut().chain(ghost_query.iter_mut()) {
commands.entity(entity).remove::<Hidden>();
}
}
(StartupSequence::CharactersVisible { .. }, StartupSequence::GameActive) => {
// Unfreeze the player & ghosts & pellet blinking
for entity in player_query
.iter_mut()
.chain(ghost_query.iter_mut())
.chain(blinking_query.iter_mut())
{
commands.entity(entity).remove::<Frozen>();
}
}
_ => {}
}
}
}

315
src/systems/state.rs Normal file
View File

@@ -0,0 +1,315 @@
use std::mem::discriminant;
use crate::{
map::builder::Map,
systems::{
AudioEvent, Blinking, DirectionalAnimation, Dying, Eaten, Frozen, Ghost, GhostCollider, GhostState, Hidden,
LinearAnimation, Looping, PlayerControlled, Position,
},
};
use bevy_ecs::{
entity::Entity,
event::EventWriter,
query::{With, Without},
resource::Resource,
system::{Commands, Query, Res, ResMut},
};
#[derive(Resource, Clone)]
pub struct PlayerAnimation(pub DirectionalAnimation);
#[derive(Resource, Clone)]
pub struct PlayerDeathAnimation(pub LinearAnimation);
/// A resource to track the overall stage of the game from a high-level perspective.
#[derive(Resource, Debug, PartialEq, Eq, Clone, Copy)]
pub enum GameStage {
Starting(StartupSequence),
/// The main gameplay loop is active.
Playing,
/// The player has died and the death sequence is in progress.
PlayerDying(DyingSequence),
/// The level is restarting after a death.
LevelRestarting,
/// The game has ended.
GameOver,
}
/// A resource that manages the multi-stage startup sequence of the game.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum StartupSequence {
/// Stage 1: Text-only stage
/// - Player & ghosts are hidden
/// - READY! and PLAYER ONE text are shown
/// - Energizers do not blink
TextOnly {
/// Remaining ticks in this stage
remaining_ticks: u32,
},
/// Stage 2: Characters visible stage
/// - PLAYER ONE text is hidden, READY! text remains
/// - Ghosts and Pac-Man are now shown
CharactersVisible {
/// Remaining ticks in this stage
remaining_ticks: u32,
},
}
impl Default for GameStage {
fn default() -> Self {
Self::Playing
}
}
/// The state machine for the multi-stage death sequence.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum DyingSequence {
/// Initial stage: entities are frozen, waiting for a delay.
Frozen { remaining_ticks: u32 },
/// Second stage: Pac-Man's death animation is playing.
Animating { remaining_ticks: u32 },
/// Third stage: Pac-Man is now gone, waiting a moment before the level restarts.
Hidden { remaining_ticks: u32 },
}
/// A resource to store the number of player lives.
#[derive(Resource, Debug)]
pub struct PlayerLives(pub u8);
impl Default for PlayerLives {
fn default() -> Self {
Self(1)
}
}
/// Handles startup sequence transitions and component management
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
pub fn stage_system(
mut game_state: ResMut<GameStage>,
player_death_animation: Res<PlayerDeathAnimation>,
player_animation: Res<PlayerAnimation>,
mut player_lives: ResMut<PlayerLives>,
map: Res<Map>,
mut commands: Commands,
mut audio_events: EventWriter<AudioEvent>,
mut blinking_query: Query<Entity, With<Blinking>>,
mut player_query: Query<(Entity, &mut Position), With<PlayerControlled>>,
mut ghost_query: Query<(Entity, &Ghost, &mut Position), (With<GhostCollider>, Without<PlayerControlled>)>,
) {
let old_state = *game_state;
let new_state: GameStage = match &mut *game_state {
GameStage::Starting(startup) => match startup {
StartupSequence::TextOnly { remaining_ticks } => {
if *remaining_ticks > 0 {
GameStage::Starting(StartupSequence::TextOnly {
remaining_ticks: *remaining_ticks - 1,
})
} else {
GameStage::Starting(StartupSequence::CharactersVisible { remaining_ticks: 60 })
}
}
StartupSequence::CharactersVisible { remaining_ticks } => {
if *remaining_ticks > 0 {
GameStage::Starting(StartupSequence::CharactersVisible {
remaining_ticks: *remaining_ticks - 1,
})
} else {
GameStage::Playing
}
}
},
GameStage::Playing => GameStage::Playing,
GameStage::PlayerDying(dying) => match dying {
DyingSequence::Frozen { remaining_ticks } => {
if *remaining_ticks > 0 {
GameStage::PlayerDying(DyingSequence::Frozen {
remaining_ticks: *remaining_ticks - 1,
})
} else {
let death_animation = &player_death_animation.0;
let remaining_ticks = (death_animation.tiles.len() * death_animation.frame_duration as usize) as u32;
GameStage::PlayerDying(DyingSequence::Animating { remaining_ticks })
}
}
DyingSequence::Animating { remaining_ticks } => {
if *remaining_ticks > 0 {
GameStage::PlayerDying(DyingSequence::Animating {
remaining_ticks: *remaining_ticks - 1,
})
} else {
GameStage::PlayerDying(DyingSequence::Hidden { remaining_ticks: 60 })
}
}
DyingSequence::Hidden { remaining_ticks } => {
if *remaining_ticks > 0 {
GameStage::PlayerDying(DyingSequence::Hidden {
remaining_ticks: *remaining_ticks - 1,
})
} else {
player_lives.0 = player_lives.0.saturating_sub(1);
if player_lives.0 > 0 {
GameStage::LevelRestarting
} else {
GameStage::GameOver
}
}
}
},
GameStage::LevelRestarting => GameStage::Starting(StartupSequence::CharactersVisible { remaining_ticks: 60 }),
GameStage::GameOver => GameStage::GameOver,
};
if old_state == new_state {
return;
}
match (old_state, new_state) {
(GameStage::Playing, GameStage::PlayerDying(DyingSequence::Frozen { .. })) => {
// Freeze the player & ghosts
for entity in player_query
.iter_mut()
.map(|(e, _)| e)
.chain(ghost_query.iter_mut().map(|(e, _, _)| e))
{
commands.entity(entity).insert(Frozen);
}
}
(GameStage::PlayerDying(DyingSequence::Frozen { .. }), GameStage::PlayerDying(DyingSequence::Animating { .. })) => {
// Hide the ghosts
for (entity, _, _) in ghost_query.iter_mut() {
commands.entity(entity).insert(Hidden);
}
// Start Pac-Man's death animation
if let Ok((player_entity, _)) = player_query.single_mut() {
commands
.entity(player_entity)
.insert((Dying, player_death_animation.0.clone()));
}
// Play the death sound
audio_events.write(AudioEvent::PlayDeath);
}
(GameStage::PlayerDying(DyingSequence::Animating { .. }), GameStage::PlayerDying(DyingSequence::Hidden { .. })) => {
// Hide the player
if let Ok((player_entity, _)) = player_query.single_mut() {
commands.entity(player_entity).insert(Hidden);
}
}
(_, GameStage::LevelRestarting) => {
if let Ok((player_entity, mut pos)) = player_query.single_mut() {
*pos = Position::Stopped {
node: map.start_positions.pacman,
};
// Freeze the blinking, force them to be visible (if they were hidden by blinking)
for entity in blinking_query.iter_mut() {
commands.entity(entity).insert(Frozen).remove::<Hidden>();
}
// Reset the player animation
commands
.entity(player_entity)
.remove::<(Frozen, Dying, Hidden, LinearAnimation, Looping)>()
.insert(player_animation.0.clone());
}
// Reset ghost positions and state
for (ghost_entity, ghost, mut ghost_pos) in ghost_query.iter_mut() {
*ghost_pos = Position::Stopped {
node: match ghost {
Ghost::Blinky => map.start_positions.blinky,
Ghost::Pinky => map.start_positions.pinky,
Ghost::Inky => map.start_positions.inky,
Ghost::Clyde => map.start_positions.clyde,
},
};
commands
.entity(ghost_entity)
.remove::<(Frozen, Hidden, Eaten)>()
.insert(GhostState::Normal);
}
}
(
GameStage::Starting(StartupSequence::TextOnly { .. }),
GameStage::Starting(StartupSequence::CharactersVisible { .. }),
) => {
// Unhide the player & ghosts
for entity in player_query
.iter_mut()
.map(|(e, _)| e)
.chain(ghost_query.iter_mut().map(|(e, _, _)| e))
{
commands.entity(entity).remove::<Hidden>();
}
}
(GameStage::Starting(StartupSequence::CharactersVisible { .. }), GameStage::Playing) => {
// Unfreeze the player & ghosts & blinking
for entity in player_query
.iter_mut()
.map(|(e, _)| e)
.chain(ghost_query.iter_mut().map(|(e, _, _)| e))
.chain(blinking_query.iter_mut())
{
commands.entity(entity).remove::<Frozen>();
}
}
(GameStage::PlayerDying(..), GameStage::GameOver) => {
// Freeze blinking
for entity in blinking_query.iter_mut() {
commands.entity(entity).insert(Frozen);
}
}
_ => {
let different = discriminant(&old_state) != discriminant(&new_state);
if different {
tracing::warn!(
new_state = ?new_state,
old_state = ?old_state,
"Unhandled game stage transition");
}
}
}
*game_state = new_state;
}
// if let GameState::LevelRestarting = &*game_state {
// // When restarting, jump straight to the CharactersVisible stage
// // and unhide the entities.
// *startup = StartupSequence::new(0, 60 * 2); // 2 seconds for READY! text
// if let StartupSequence::TextOnly { .. } = *startup {
// // This will immediately transition to CharactersVisible on the next line
// } else {
// // Should be unreachable as we just set it
// }
// // Freeze Pac-Man and ghosts
// for entity in player_query.iter().chain(ghost_query.iter()) {
// commands.entity(entity).insert(Frozen);
// }
// *game_state = GameState::Playing;
// }
// if let Some((old_state, new_state)) = startup.tick() {
// debug!("StartupSequence transition from {old_state:?} to {new_state:?}");
// match (old_state, new_state) {
// (StartupSequence::TextOnly { .. }, StartupSequence::CharactersVisible { .. }) => {
// // Unhide the player & ghosts
// for entity in player_query.iter().chain(ghost_query.iter()) {
// commands.entity(entity).remove::<Hidden>();
// }
// }
// (StartupSequence::CharactersVisible { .. }, StartupSequence::GameActive) => {
// // Unfreeze Pac-Man, ghosts and energizers
// for entity in player_query.iter().chain(ghost_query.iter()).chain(blinking_query.iter()) {
// commands.entity(entity).remove::<Frozen>();
// }
// *game_state = GameState::Playing;
// }
// _ => {}
// }
// }