diff --git a/assets/game/sound/begin.ogg b/assets/game/sound/begin.ogg new file mode 100644 index 0000000..7d2616c Binary files /dev/null and b/assets/game/sound/begin.ogg differ diff --git a/assets/game/sound/intermission.ogg b/assets/game/sound/intermission.ogg new file mode 100644 index 0000000..8786ef8 Binary files /dev/null and b/assets/game/sound/intermission.ogg differ diff --git a/assets/game/sound/pacman/extra_life.ogg b/assets/game/sound/pacman/extra_life.ogg new file mode 100644 index 0000000..7f2cb6e Binary files /dev/null and b/assets/game/sound/pacman/extra_life.ogg differ diff --git a/assets/game/sound/pacman/fruit.ogg b/assets/game/sound/pacman/fruit.ogg new file mode 100644 index 0000000..f033f88 Binary files /dev/null and b/assets/game/sound/pacman/fruit.ogg differ diff --git a/assets/game/sound/pacman/ghost.ogg b/assets/game/sound/pacman/ghost.ogg new file mode 100644 index 0000000..88fc498 Binary files /dev/null and b/assets/game/sound/pacman/ghost.ogg differ diff --git a/src/asset.rs b/src/asset.rs index 7619962..ef7ebf8 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -2,21 +2,62 @@ //! On desktop, assets are embedded using include_bytes!; on Emscripten, assets are loaded from the filesystem. use std::borrow::Cow; -use strum_macros::EnumIter; +use std::iter; + +use crate::audio::Sound; /// Enumeration of all game assets with cross-platform loading support. /// /// Each variant corresponds to a specific file that can be loaded either from /// binary-embedded data or embedded filesystem (Emscripten). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Asset { - Waka(u8), /// Main sprite atlas containing all game graphics (atlas.png) AtlasImage, /// Terminal Vector font for text rendering (TerminalVector.ttf) Font, - /// Sound effect for Pac-Man's death - DeathSound, + /// Sound file assets + SoundFile(Sound), +} + +use strum::IntoEnumIterator; + +impl Asset { + #[allow(dead_code)] + pub fn into_iter() -> AssetIter { + AssetIter { + sound_iter: None, + state: 0, + } + } +} + +#[allow(clippy::type_complexity)] +pub struct AssetIter { + sound_iter: Option::Iterator, fn(Sound) -> Asset>>, + state: u8, +} + +impl Iterator for AssetIter { + type Item = Asset; + + fn next(&mut self) -> Option { + match self.state { + 0 => { + self.state = 1; + Some(Asset::AtlasImage) + } + 1 => { + self.state = 2; + Some(Asset::Font) + } + 2 => self + .sound_iter + .get_or_insert_with(|| Sound::iter().map(Asset::SoundFile)) + .next(), + _ => None, + } + } } impl Asset { @@ -29,11 +70,16 @@ impl Asset { pub fn path(&self) -> &str { use Asset::*; match self { - Waka(0) => "sound/pacman/waka/1.ogg", - Waka(1) => "sound/pacman/waka/2.ogg", - Waka(2) => "sound/pacman/waka/3.ogg", - Waka(3..=u8::MAX) => "sound/pacman/waka/4.ogg", - DeathSound => "sound/pacman/death.ogg", + SoundFile(Sound::Waka(0)) => "sound/pacman/waka/1.ogg", + SoundFile(Sound::Waka(1)) => "sound/pacman/waka/2.ogg", + SoundFile(Sound::Waka(2)) => "sound/pacman/waka/3.ogg", + SoundFile(Sound::Waka(3..=u8::MAX)) => "sound/pacman/waka/4.ogg", + SoundFile(Sound::PacmanDeath) => "sound/pacman/death.ogg", + SoundFile(Sound::ExtraLife) => "sound/pacman/extra_life.ogg", + SoundFile(Sound::Fruit) => "sound/pacman/fruit.ogg", + SoundFile(Sound::Ghost) => "sound/pacman/ghost.ogg", + SoundFile(Sound::Beginning) => "sound/begin.ogg", + SoundFile(Sound::Intermission) => "sound/intermission.ogg", AtlasImage => "atlas.png", Font => "TerminalVector.ttf", } diff --git a/src/audio.rs b/src/audio.rs index 13516da..f7fe4ee 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -1,11 +1,43 @@ //! This module handles the audio playback for the game. +use std::collections::HashMap; + use crate::asset::{get_asset_bytes, Asset}; use sdl2::{ mixer::{self, Chunk, InitFlag, LoaderRWops, AUDIO_S16LSB, DEFAULT_CHANNELS}, rwops::RWops, }; +use strum::IntoEnumIterator; -const SOUND_ASSETS: [Asset; 4] = [Asset::Waka(0), Asset::Waka(1), Asset::Waka(2), Asset::Waka(3)]; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Sound { + Waka(u8), + PacmanDeath, + ExtraLife, + Fruit, + Ghost, + Beginning, + Intermission, +} + +impl IntoEnumIterator for Sound { + type Iterator = std::vec::IntoIter; + + fn iter() -> Self::Iterator { + vec![ + Sound::Waka(0), + Sound::Waka(1), + Sound::Waka(2), + Sound::Waka(3), + Sound::PacmanDeath, + Sound::ExtraLife, + Sound::Fruit, + Sound::Ghost, + Sound::Beginning, + Sound::Intermission, + ] + .into_iter() + } +} /// The audio system for the game. /// @@ -14,9 +46,8 @@ const SOUND_ASSETS: [Asset; 4] = [Asset::Waka(0), Asset::Waka(1), Asset::Waka(2) /// functions will silently do nothing. pub struct Audio { _mixer_context: Option, - sounds: Vec, - death_sound: Option, - next_sound_index: usize, + sounds: HashMap, + next_waka_index: u8, muted: bool, disabled: bool, } @@ -54,9 +85,8 @@ impl Audio { tracing::warn!("Failed to open audio: {}. Audio will be disabled.", e); return Self { _mixer_context: None, - sounds: Vec::new(), - death_sound: None, - next_sound_index: 0, + sounds: HashMap::new(), + next_waka_index: 0u8, muted: false, disabled: true, }; @@ -77,9 +107,8 @@ impl Audio { tracing::warn!("Failed to initialize SDL2_mixer: {}. Audio will be disabled.", e); return Self { _mixer_context: None, - sounds: Vec::new(), - death_sound: None, - next_sound_index: 0, + sounds: HashMap::new(), + next_waka_index: 0u8, muted: false, disabled: true, }; @@ -87,12 +116,14 @@ impl Audio { }; // Try to load sounds, but don't panic if any fail - let mut sounds = Vec::new(); - for (i, asset) in SOUND_ASSETS.iter().enumerate() { - match get_asset_bytes(*asset) { + let mut sounds = HashMap::new(); + for (i, asset) in Sound::iter().enumerate() { + match get_asset_bytes(Asset::SoundFile(asset)) { Ok(data) => match RWops::from_bytes(&data) { Ok(rwops) => match rwops.load_wav() { - Ok(chunk) => sounds.push(chunk), + Ok(chunk) => { + sounds.insert(asset, chunk); + } Err(e) => { tracing::warn!("Failed to load sound {} from asset API: {}", i + 1, e); } @@ -107,7 +138,7 @@ impl Audio { } } - let death_sound = match get_asset_bytes(Asset::DeathSound) { + let death_sound = match get_asset_bytes(Asset::SoundFile(Sound::PacmanDeath)) { Ok(data) => match RWops::from_bytes(&data) { Ok(rwops) => match rwops.load_wav() { Ok(chunk) => Some(chunk), @@ -132,9 +163,8 @@ impl Audio { tracing::warn!("No sounds loaded successfully. Audio will be disabled."); return Self { _mixer_context: Some(mixer_context), - sounds: Vec::new(), - death_sound: None, - next_sound_index: 0, + sounds: HashMap::new(), + next_waka_index: 0u8, muted: false, disabled: true, }; @@ -143,8 +173,7 @@ impl Audio { Audio { _mixer_context: Some(mixer_context), sounds, - death_sound, - next_sound_index: 0, + next_waka_index: 0u8, muted: false, disabled: false, } @@ -160,17 +189,17 @@ impl Audio { return; } - if let Some(chunk) = self.sounds.get(self.next_sound_index) { + if let Some(chunk) = self.sounds.get(&Sound::Waka(self.next_waka_index)) { match mixer::Channel(0).play(chunk, 0) { Ok(channel) => { - tracing::trace!("Playing sound #{} on channel {:?}", self.next_sound_index + 1, channel); + tracing::trace!("Playing sound #{} on channel {:?}", self.next_waka_index + 1, channel); } Err(e) => { - tracing::warn!("Could not play sound #{}: {}", self.next_sound_index + 1, e); + tracing::warn!("Could not play sound #{}: {}", self.next_waka_index + 1, e); } } } - self.next_sound_index = (self.next_sound_index + 1) % self.sounds.len(); + self.next_waka_index = (self.next_waka_index + 1) & 3; } /// Plays the death sound effect. @@ -179,7 +208,7 @@ impl Audio { return; } - if let Some(chunk) = &self.death_sound { + if let Some(chunk) = self.sounds.get(&Sound::PacmanDeath) { mixer::Channel::all().play(chunk, 0).ok(); } } diff --git a/src/platform/desktop.rs b/src/platform/desktop.rs index e92beeb..addb3a7 100644 --- a/src/platform/desktop.rs +++ b/src/platform/desktop.rs @@ -6,6 +6,7 @@ use std::time::Duration; use rand::rngs::ThreadRng; use crate::asset::Asset; +use crate::audio::Sound; use crate::error::{AssetError, PlatformError}; /// Desktop platform implementation. @@ -59,13 +60,20 @@ pub fn init_console() -> Result<(), PlatformError> { pub fn get_asset_bytes(asset: Asset) -> Result, AssetError> { match asset { - Asset::Waka(0) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/1.ogg"))), - Asset::Waka(1) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/2.ogg"))), - Asset::Waka(2) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/3.ogg"))), - Asset::Waka(3..=u8::MAX) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/4.ogg"))), + Asset::SoundFile(sound) => match sound { + Sound::Waka(0) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/1.ogg"))), + Sound::Waka(1) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/2.ogg"))), + Sound::Waka(2) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/3.ogg"))), + Sound::Waka(3..=u8::MAX) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/4.ogg"))), + Sound::PacmanDeath => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/death.ogg"))), + Sound::ExtraLife => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/extra_life.ogg"))), + Sound::Fruit => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/fruit.ogg"))), + Sound::Ghost => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/ghost.ogg"))), + Sound::Beginning => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/begin.ogg"))), + Sound::Intermission => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/intermission.ogg"))), + }, Asset::AtlasImage => Ok(Cow::Borrowed(include_bytes!("../../assets/game/atlas.png"))), Asset::Font => Ok(Cow::Borrowed(include_bytes!("../../assets/game/TerminalVector.ttf"))), - Asset::DeathSound => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/death.ogg"))), } } diff --git a/tests/asset.rs b/tests/asset.rs index b344dd5..4ea8dbc 100644 --- a/tests/asset.rs +++ b/tests/asset.rs @@ -1,10 +1,9 @@ use pacman::asset::Asset; use speculoos::prelude::*; -use strum::IntoEnumIterator; #[test] fn all_asset_paths_exist() { - for asset in Asset::iter() { + for asset in Asset::into_iter() { let path = asset.path(); let full_path = format!("assets/game/{}", path);