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::map::builder::Map;
use crate::map::direction::Direction;
use crate::systems::state::IntroPlayed;
use crate::systems::{
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,
@@ -438,6 +439,7 @@ impl Game {
world.insert_resource(RenderDirty::default());
world.insert_resource(DebugState::default());
world.insert_resource(AudioState::default());
world.insert_resource(IntroPlayed::default());
world.insert_resource(CursorPosition::default());
world.insert_resource(TouchState::default());
world.insert_resource(GameStage::Starting(StartupSequence::TextOnly {

View File

@@ -11,7 +11,7 @@ use bevy_ecs::{
};
use tracing::{debug, trace};
use crate::{audio::Audio, error::GameError};
use crate::{audio::Audio, audio::Sound, error::GameError};
/// Resource for tracking audio state
#[derive(Resource, Debug, Clone, Default)]
@@ -25,10 +25,10 @@ pub struct AudioState {
/// Events for triggering audio playback
#[derive(Event, Debug, Clone, Copy, PartialEq, Eq)]
pub enum AudioEvent {
/// Play the "eat" sound when Pac-Man consumes a pellet
PlayEat,
/// Play the death sound
PlayDeath,
/// Play a specific sound effect
PlaySound(Sound),
/// Play the cycling waka sound variant
Waka,
/// Stop all currently playing sounds
StopAll,
/// Pause all sounds
@@ -61,7 +61,7 @@ pub fn audio_system(
// Process audio events
for event in audio_events.read() {
match event {
AudioEvent::PlayEat => {
AudioEvent::Waka => {
if !audio.0.is_disabled() && !audio_state.muted {
trace!(sound_index = audio_state.sound_index, "Playing eat sound");
audio.0.waka();
@@ -76,15 +76,15 @@ pub fn audio_system(
);
}
}
AudioEvent::PlayDeath => {
AudioEvent::PlaySound(sound) => {
if !audio.0.is_disabled() && !audio_state.muted {
trace!("Playing death sound");
audio.0.play(crate::audio::Sound::PacmanDeath);
trace!(?sound, "Playing sound");
audio.0.play(*sound);
} else {
debug!(
disabled = audio.0.is_disabled(),
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 crate::audio::Sound;
use crate::{
constants,
systems::{movement::Position, AudioEvent, DyingSequence, FruitSprites, GameStage, Ghost, ScoreResource, SpawnTrigger},
@@ -165,8 +166,8 @@ pub fn ghost_collision_observer(
ghost_type,
});
// Play eat sound
events.write(AudioEvent::PlayEat);
// Play ghost eaten sound
events.write(AudioEvent::PlaySound(Sound::Ghost));
} else if matches!(*ghost_state, GhostState::Normal) {
// Pac-Man 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
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

View File

@@ -28,6 +28,10 @@ pub struct PlayerAnimation(pub DirectionalAnimation);
#[derive(Resource, Clone)]
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.
#[derive(Resource, Debug, PartialEq, Eq, Clone, Copy)]
pub enum GameStage {
@@ -183,6 +187,7 @@ pub fn stage_system(
player: Single<(Entity, &mut Position), With<PlayerControlled>>,
mut item_query: Query<(Entity, &EntityType), With<ItemCollider>>,
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 mut new_state_opt: Option<GameStage> = None;
@@ -229,6 +234,11 @@ pub fn stage_system(
}
GameStage::Starting(sequence) => match sequence {
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 {
GameStage::Starting(StartupSequence::TextOnly {
remaining_ticks: remaining_ticks.saturating_sub(1),
@@ -354,7 +364,7 @@ pub fn stage_system(
.insert((Dying, player_death_animation.0.clone()));
// Play the death sound
audio_events.write(AudioEvent::PlayDeath);
audio_events.write(AudioEvent::PlaySound(crate::audio::Sound::PacmanDeath));
}
(_, GameStage::PlayerDying(DyingSequence::Hidden { .. })) => {
// 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() {
commands.entity(entity).remove::<Frozen>();
}
// Reset intro flag for the next round
intro_played.0 = false;
}
(_, GameStage::GameOver) => {
// Freeze blinking