feat(audio): setup intro jingle, use fruit & ghost sounds, improve AudioEvent

This commit is contained in:
Ryan Walters
2025-09-11 02:24:15 -05:00
parent 86331afd52
commit 9ad1704806
4 changed files with 37 additions and 14 deletions

View File

@@ -11,6 +11,7 @@ use crate::error::{GameError, GameResult};
use crate::events::{CollisionTrigger, GameEvent, StageTransition}; use crate::events::{CollisionTrigger, GameEvent, StageTransition};
use crate::map::builder::Map; use crate::map::builder::Map;
use crate::map::direction::Direction; use crate::map::direction::Direction;
use crate::systems::state::IntroPlayed;
use crate::systems::{ use crate::systems::{
self, audio_system, blinking_system, collision_system, combined_render_system, directional_render_system, self, audio_system, blinking_system, collision_system, combined_render_system, directional_render_system,
dirty_render_system, eaten_ghost_system, fruit_sprite_system, ghost_collision_observer, ghost_movement_system, dirty_render_system, eaten_ghost_system, fruit_sprite_system, ghost_collision_observer, ghost_movement_system,
@@ -438,6 +439,7 @@ impl Game {
world.insert_resource(RenderDirty::default()); world.insert_resource(RenderDirty::default());
world.insert_resource(DebugState::default()); world.insert_resource(DebugState::default());
world.insert_resource(AudioState::default()); world.insert_resource(AudioState::default());
world.insert_resource(IntroPlayed::default());
world.insert_resource(CursorPosition::default()); world.insert_resource(CursorPosition::default());
world.insert_resource(TouchState::default()); world.insert_resource(TouchState::default());
world.insert_resource(GameStage::Starting(StartupSequence::TextOnly { world.insert_resource(GameStage::Starting(StartupSequence::TextOnly {

View File

@@ -11,7 +11,7 @@ use bevy_ecs::{
}; };
use tracing::{debug, trace}; use tracing::{debug, trace};
use crate::{audio::Audio, error::GameError}; use crate::{audio::Audio, audio::Sound, error::GameError};
/// Resource for tracking audio state /// Resource for tracking audio state
#[derive(Resource, Debug, Clone, Default)] #[derive(Resource, Debug, Clone, Default)]
@@ -25,10 +25,10 @@ pub struct AudioState {
/// Events for triggering audio playback /// Events for triggering audio playback
#[derive(Event, Debug, Clone, Copy, PartialEq, Eq)] #[derive(Event, Debug, Clone, Copy, PartialEq, Eq)]
pub enum AudioEvent { pub enum AudioEvent {
/// Play the "eat" sound when Pac-Man consumes a pellet /// Play a specific sound effect
PlayEat, PlaySound(Sound),
/// Play the death sound /// Play the cycling waka sound variant
PlayDeath, Waka,
/// Stop all currently playing sounds /// Stop all currently playing sounds
StopAll, StopAll,
/// Pause all sounds /// Pause all sounds
@@ -61,7 +61,7 @@ pub fn audio_system(
// Process audio events // Process audio events
for event in audio_events.read() { for event in audio_events.read() {
match event { match event {
AudioEvent::PlayEat => { AudioEvent::Waka => {
if !audio.0.is_disabled() && !audio_state.muted { if !audio.0.is_disabled() && !audio_state.muted {
trace!(sound_index = audio_state.sound_index, "Playing eat sound"); trace!(sound_index = audio_state.sound_index, "Playing eat sound");
audio.0.waka(); audio.0.waka();
@@ -76,15 +76,15 @@ pub fn audio_system(
); );
} }
} }
AudioEvent::PlayDeath => { AudioEvent::PlaySound(sound) => {
if !audio.0.is_disabled() && !audio_state.muted { if !audio.0.is_disabled() && !audio_state.muted {
trace!("Playing death sound"); trace!(?sound, "Playing sound");
audio.0.play(crate::audio::Sound::PacmanDeath); audio.0.play(*sound);
} else { } else {
debug!( debug!(
disabled = audio.0.is_disabled(), disabled = audio.0.is_disabled(),
muted = audio_state.muted, muted = audio_state.muted,
"Skipping death sound due to audio state" "Skipping sound due to audio state"
); );
} }
} }

View File

@@ -8,6 +8,7 @@ use bevy_ecs::{
}; };
use tracing::{debug, trace, warn}; use tracing::{debug, trace, warn};
use crate::audio::Sound;
use crate::{ use crate::{
constants, constants,
systems::{movement::Position, AudioEvent, DyingSequence, FruitSprites, GameStage, Ghost, ScoreResource, SpawnTrigger}, systems::{movement::Position, AudioEvent, DyingSequence, FruitSprites, GameStage, Ghost, ScoreResource, SpawnTrigger},
@@ -165,8 +166,8 @@ pub fn ghost_collision_observer(
ghost_type, ghost_type,
}); });
// Play eat sound // Play ghost eaten sound
events.write(AudioEvent::PlayEat); events.write(AudioEvent::PlaySound(Sound::Ghost));
} else if matches!(*ghost_state, GhostState::Normal) { } else if matches!(*ghost_state, GhostState::Normal) {
// Pac-Man dies // Pac-Man dies
warn!(ghost = ?ghost_type, "Pacman hit by normal ghost, player dies"); warn!(ghost = ?ghost_type, "Pacman hit by normal ghost, player dies");
@@ -226,7 +227,15 @@ pub fn item_collision_observer(
// Trigger audio if appropriate // Trigger audio if appropriate
if entity_type.is_collectible() { if entity_type.is_collectible() {
events.write(AudioEvent::PlayEat); match *entity_type {
EntityType::Fruit(_) => {
events.write(AudioEvent::PlaySound(Sound::Fruit));
}
EntityType::Pellet | EntityType::PowerPellet => {
events.write(AudioEvent::Waka);
}
_ => {}
}
} }
// Make non-eaten ghosts frightened when power pellet is collected // Make non-eaten ghosts frightened when power pellet is collected

View File

@@ -28,6 +28,10 @@ pub struct PlayerAnimation(pub DirectionalAnimation);
#[derive(Resource, Clone)] #[derive(Resource, Clone)]
pub struct PlayerDeathAnimation(pub LinearAnimation); pub struct PlayerDeathAnimation(pub LinearAnimation);
/// Tracks whether the beginning sound has been played for the current startup sequence
#[derive(Resource, Debug, Default, Clone, Copy)]
pub struct IntroPlayed(pub bool);
/// A resource to track the overall stage of the game from a high-level perspective. /// A resource to track the overall stage of the game from a high-level perspective.
#[derive(Resource, Debug, PartialEq, Eq, Clone, Copy)] #[derive(Resource, Debug, PartialEq, Eq, Clone, Copy)]
pub enum GameStage { pub enum GameStage {
@@ -183,6 +187,7 @@ pub fn stage_system(
player: Single<(Entity, &mut Position), With<PlayerControlled>>, player: Single<(Entity, &mut Position), With<PlayerControlled>>,
mut item_query: Query<(Entity, &EntityType), With<ItemCollider>>, mut item_query: Query<(Entity, &EntityType), With<ItemCollider>>,
mut ghost_query: Query<(Entity, &Ghost, &mut Position, &mut GhostState), (With<GhostCollider>, Without<PlayerControlled>)>, mut ghost_query: Query<(Entity, &Ghost, &mut Position, &mut GhostState), (With<GhostCollider>, Without<PlayerControlled>)>,
mut intro_played: ResMut<IntroPlayed>,
) { ) {
let old_state = *game_state; let old_state = *game_state;
let mut new_state_opt: Option<GameStage> = None; let mut new_state_opt: Option<GameStage> = None;
@@ -229,6 +234,11 @@ pub fn stage_system(
} }
GameStage::Starting(sequence) => match sequence { GameStage::Starting(sequence) => match sequence {
StartupSequence::TextOnly { remaining_ticks } => { StartupSequence::TextOnly { remaining_ticks } => {
// Play the beginning sound once at the start of TextOnly stage
if !intro_played.0 {
audio_events.write(AudioEvent::PlaySound(crate::audio::Sound::Beginning));
intro_played.0 = true;
}
if remaining_ticks > 0 { if remaining_ticks > 0 {
GameStage::Starting(StartupSequence::TextOnly { GameStage::Starting(StartupSequence::TextOnly {
remaining_ticks: remaining_ticks.saturating_sub(1), remaining_ticks: remaining_ticks.saturating_sub(1),
@@ -354,7 +364,7 @@ pub fn stage_system(
.insert((Dying, player_death_animation.0.clone())); .insert((Dying, player_death_animation.0.clone()));
// Play the death sound // Play the death sound
audio_events.write(AudioEvent::PlayDeath); audio_events.write(AudioEvent::PlaySound(crate::audio::Sound::PacmanDeath));
} }
(_, GameStage::PlayerDying(DyingSequence::Hidden { .. })) => { (_, GameStage::PlayerDying(DyingSequence::Hidden { .. })) => {
// Pac-Man's death animation is complete, so he should be hidden just like the ghosts. // Pac-Man's death animation is complete, so he should be hidden just like the ghosts.
@@ -423,6 +433,8 @@ pub fn stage_system(
for entity in blinking_query.iter_mut() { for entity in blinking_query.iter_mut() {
commands.entity(entity).remove::<Frozen>(); commands.entity(entity).remove::<Frozen>();
} }
// Reset intro flag for the next round
intro_played.0 = false;
} }
(_, GameStage::GameOver) => { (_, GameStage::GameOver) => {
// Freeze blinking // Freeze blinking