From f1e7f75141ee2dcc539ceb68a1337ef6fc235783 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 9 Feb 2026 16:45:30 -0800 Subject: [PATCH] Add comprehensive spell sound manager with 35+ magic sounds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented complete spell casting audio system with all magic schools: Magic schools supported: - Fire: Precast (Low/Medium/High), Cast, Fireball impacts - Frost: Precast (Low/Medium/High), Cast, Blizzard impacts - Holy: Precast (Low/Medium/High), Cast, Holy impacts (4 levels) - Nature: Precast (Low/Medium/High), Cast - Shadow: Precast (Low/Medium/High), Cast - Arcane: Precast, Arcane Missile impacts - Physical: Non-magical abilities Spell phases: - Precast: Channeling/preparation sounds (before cast) - Cast: Spell release sounds (when spell fires) - Impact: Spell hit sounds (when spell hits target) Power levels: - Low: Weak spells, low level abilities - Medium: Standard power spells - High: Powerful high-level spells Sound coverage (35+ sounds): - 16 precast sounds (Fire/Frost/Holy/Nature/Shadow × Low/Med/High + Arcane) - 5 cast sounds (one per school) - 16 impact sounds (Fireball ×3, Blizzard ×6, Holy ×4, Arcane Missile ×3) Technical details: - Loads 35+ sound files from Sound\Spells directory - Simple API: playPrecast(school, power), playCast(school), playImpact(school, power) - Convenience methods: playFireball(), playFrostbolt(), playHeal(), etc. - Random variation selection for impacts - Volume at 0.75 with global scale control - Ready for integration with spell casting system Usage examples: ```cpp // Full spell sequence spellSoundManager->playPrecast(MagicSchool::FIRE, SpellPower::HIGH); // ... cast time ... spellSoundManager->playCast(MagicSchool::FIRE); // ... projectile travel ... spellSoundManager->playImpact(MagicSchool::FIRE, SpellPower::HIGH); // Convenience methods spellSoundManager->playFireball(); spellSoundManager->playHeal(); ``` This adds essential magic feedback for spell casting gameplay! --- CMakeLists.txt | 1 + include/audio/spell_sound_manager.hpp | 106 +++++++++ src/audio/spell_sound_manager.cpp | 295 ++++++++++++++++++++++++++ 3 files changed, 402 insertions(+) create mode 100644 include/audio/spell_sound_manager.hpp create mode 100644 src/audio/spell_sound_manager.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b41c0b41..6d5ad953 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -108,6 +108,7 @@ set(WOWEE_SOURCES src/audio/ambient_sound_manager.cpp src/audio/ui_sound_manager.cpp src/audio/combat_sound_manager.cpp + src/audio/spell_sound_manager.cpp # Pipeline (asset loaders) src/pipeline/mpq_manager.cpp diff --git a/include/audio/spell_sound_manager.hpp b/include/audio/spell_sound_manager.hpp new file mode 100644 index 00000000..e3969719 --- /dev/null +++ b/include/audio/spell_sound_manager.hpp @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#include +#include + +namespace wowee { +namespace pipeline { +class AssetManager; +} + +namespace audio { + +class SpellSoundManager { +public: + SpellSoundManager() = default; + ~SpellSoundManager() = default; + + // Initialization + bool initialize(pipeline::AssetManager* assets); + void shutdown(); + + // Volume control + void setVolumeScale(float scale); + + // Magic school types + enum class MagicSchool { + FIRE, + FROST, + HOLY, + NATURE, + SHADOW, + ARCANE, + PHYSICAL // Non-magical abilities + }; + + // Spell power level + enum class SpellPower { + LOW, // Weak spells, low level + MEDIUM, // Standard spells + HIGH // Powerful spells + }; + + // Spell casting sounds + void playPrecast(MagicSchool school, SpellPower power); // Channeling/preparation + void playCast(MagicSchool school); // When spell fires + void playImpact(MagicSchool school, SpellPower power); // When spell hits target + + // Specific spell sounds + void playFireball(); + void playFrostbolt(); + void playLightningBolt(); + void playHeal(); + void playShadowBolt(); + +private: + struct SpellSample { + std::string path; + std::vector data; + bool loaded; + }; + + // Precast sound libraries (channeling) + std::vector precastFireLowSounds_; + std::vector precastFireMediumSounds_; + std::vector precastFireHighSounds_; + std::vector precastFrostLowSounds_; + std::vector precastFrostMediumSounds_; + std::vector precastFrostHighSounds_; + std::vector precastHolyLowSounds_; + std::vector precastHolyMediumSounds_; + std::vector precastHolyHighSounds_; + std::vector precastNatureLowSounds_; + std::vector precastNatureMediumSounds_; + std::vector precastNatureHighSounds_; + std::vector precastShadowLowSounds_; + std::vector precastShadowMediumSounds_; + std::vector precastShadowHighSounds_; + std::vector precastArcaneSounds_; + + // Cast sound libraries (spell release) + std::vector castFireSounds_; + std::vector castFrostSounds_; + std::vector castHolySounds_; + std::vector castNatureSounds_; + std::vector castShadowSounds_; + + // Impact sound libraries (spell hits) + std::vector impactFireballSounds_; + std::vector impactBlizzardSounds_; + std::vector impactHolySounds_; + std::vector impactArcaneMissileSounds_; + + // State tracking + float volumeScale_ = 1.0f; + bool initialized_ = false; + + // Helper methods + bool loadSound(const std::string& path, SpellSample& sample, pipeline::AssetManager* assets); + void playSound(const std::vector& library, float volumeMultiplier = 1.0f); + void playRandomSound(const std::vector& library, float volumeMultiplier = 1.0f); +}; + +} // namespace audio +} // namespace wowee diff --git a/src/audio/spell_sound_manager.cpp b/src/audio/spell_sound_manager.cpp new file mode 100644 index 00000000..255e83bf --- /dev/null +++ b/src/audio/spell_sound_manager.cpp @@ -0,0 +1,295 @@ +#include "audio/spell_sound_manager.hpp" +#include "audio/audio_engine.hpp" +#include "pipeline/asset_manager.hpp" +#include "core/logger.hpp" +#include + +namespace wowee { +namespace audio { + +namespace { + std::random_device rd; + std::mt19937 gen(rd()); +} + +bool SpellSoundManager::initialize(pipeline::AssetManager* assets) { + if (!assets) { + LOG_ERROR("SpellSoundManager: AssetManager is null"); + return false; + } + + LOG_INFO("SpellSoundManager: Initializing..."); + + // Load Fire precast sounds + precastFireLowSounds_.resize(1); + loadSound("Sound\\Spells\\PreCastFireLow.wav", precastFireLowSounds_[0], assets); + + precastFireMediumSounds_.resize(1); + loadSound("Sound\\Spells\\PreCastFireMedium.wav", precastFireMediumSounds_[0], assets); + + precastFireHighSounds_.resize(1); + loadSound("Sound\\Spells\\PreCastFireHigh.wav", precastFireHighSounds_[0], assets); + + // Load Frost precast sounds + precastFrostLowSounds_.resize(1); + loadSound("Sound\\Spells\\PreCastFrostMagicLow.wav", precastFrostLowSounds_[0], assets); + + precastFrostMediumSounds_.resize(1); + loadSound("Sound\\Spells\\PreCastFrostMagicMedium.wav", precastFrostMediumSounds_[0], assets); + + precastFrostHighSounds_.resize(1); + loadSound("Sound\\Spells\\PreCastFrostMagicHigh.wav", precastFrostHighSounds_[0], assets); + + // Load Holy precast sounds + precastHolyLowSounds_.resize(1); + loadSound("Sound\\Spells\\PreCastHolyMagicLow.wav", precastHolyLowSounds_[0], assets); + + precastHolyMediumSounds_.resize(1); + loadSound("Sound\\Spells\\PreCastHolyMagicMedium.wav", precastHolyMediumSounds_[0], assets); + + precastHolyHighSounds_.resize(1); + loadSound("Sound\\Spells\\PreCastHolyMagicHigh.wav", precastHolyHighSounds_[0], assets); + + // Load Nature precast sounds + precastNatureLowSounds_.resize(1); + loadSound("Sound\\Spells\\PreCastNatureMagicLow.wav", precastNatureLowSounds_[0], assets); + + precastNatureMediumSounds_.resize(1); + loadSound("Sound\\Spells\\PreCastNatureMagicMedium.wav", precastNatureMediumSounds_[0], assets); + + precastNatureHighSounds_.resize(1); + loadSound("Sound\\Spells\\PreCastNatureMagicHigh.wav", precastNatureHighSounds_[0], assets); + + // Load Shadow precast sounds + precastShadowLowSounds_.resize(1); + loadSound("Sound\\Spells\\PreCastShadowMagicLow.wav", precastShadowLowSounds_[0], assets); + + precastShadowMediumSounds_.resize(1); + loadSound("Sound\\Spells\\PreCastShadowMagicMedium.wav", precastShadowMediumSounds_[0], assets); + + precastShadowHighSounds_.resize(1); + loadSound("Sound\\Spells\\PreCastShadowMagicHigh.wav", precastShadowHighSounds_[0], assets); + + // Load Arcane precast sounds + precastArcaneSounds_.resize(1); + loadSound("Sound\\Spells\\Arcane_Form_Precast.wav", precastArcaneSounds_[0], assets); + + // Load Cast sounds (when spell fires) + castFireSounds_.resize(1); + loadSound("Sound\\Spells\\Cast\\FireCast.wav", castFireSounds_[0], assets); + + castFrostSounds_.resize(1); + loadSound("Sound\\Spells\\Cast\\IceCast.wav", castFrostSounds_[0], assets); + + castHolySounds_.resize(1); + loadSound("Sound\\Spells\\Cast\\HolyCast.wav", castHolySounds_[0], assets); + + castNatureSounds_.resize(1); + loadSound("Sound\\Spells\\Cast\\NatureCast.wav", castNatureSounds_[0], assets); + + castShadowSounds_.resize(1); + loadSound("Sound\\Spells\\Cast\\ShadowCast.wav", castShadowSounds_[0], assets); + + // Load Impact sounds + impactFireballSounds_.resize(3); + loadSound("Sound\\Spells\\FireBallImpactA.wav", impactFireballSounds_[0], assets); + loadSound("Sound\\Spells\\FireBallImpactB.wav", impactFireballSounds_[1], assets); + loadSound("Sound\\Spells\\FireBallImpactC.wav", impactFireballSounds_[2], assets); + + impactBlizzardSounds_.resize(6); + loadSound("Sound\\Spells\\BlizzardImpact1a.wav", impactBlizzardSounds_[0], assets); + loadSound("Sound\\Spells\\BlizzardImpact1b.wav", impactBlizzardSounds_[1], assets); + loadSound("Sound\\Spells\\BlizzardImpact1c.wav", impactBlizzardSounds_[2], assets); + loadSound("Sound\\Spells\\BlizzardImpact1d.wav", impactBlizzardSounds_[3], assets); + loadSound("Sound\\Spells\\BlizzardImpact1e.wav", impactBlizzardSounds_[4], assets); + loadSound("Sound\\Spells\\BlizzardImpact1f.wav", impactBlizzardSounds_[5], assets); + + impactHolySounds_.resize(4); + loadSound("Sound\\Spells\\DirectDamage\\HolyImpactDDLow.wav", impactHolySounds_[0], assets); + loadSound("Sound\\Spells\\DirectDamage\\HolyImpactDDMedium.wav", impactHolySounds_[1], assets); + loadSound("Sound\\Spells\\DirectDamage\\HolyImpactDDHigh.wav", impactHolySounds_[2], assets); + loadSound("Sound\\Spells\\DirectDamage\\HolyImpactDDUber.wav", impactHolySounds_[3], assets); + + impactArcaneMissileSounds_.resize(3); + loadSound("Sound\\Spells\\ArcaneMissileImpact1a.wav", impactArcaneMissileSounds_[0], assets); + loadSound("Sound\\Spells\\ArcaneMissileImpact1b.wav", impactArcaneMissileSounds_[1], assets); + loadSound("Sound\\Spells\\ArcaneMissileImpact1c.wav", impactArcaneMissileSounds_[2], assets); + + LOG_INFO("SpellSoundManager: Precast sounds - Fire: ", precastFireLowSounds_[0].loaded ? "YES" : "NO", + ", Frost: ", precastFrostLowSounds_[0].loaded ? "YES" : "NO", + ", Holy: ", precastHolyLowSounds_[0].loaded ? "YES" : "NO"); + LOG_INFO("SpellSoundManager: Cast sounds - Fire: ", castFireSounds_[0].loaded ? "YES" : "NO", + ", Frost: ", castFrostSounds_[0].loaded ? "YES" : "NO", + ", Shadow: ", castShadowSounds_[0].loaded ? "YES" : "NO"); + LOG_INFO("SpellSoundManager: Impact sounds - Fireball: ", impactFireballSounds_[0].loaded ? "YES" : "NO", + ", Blizzard: ", impactBlizzardSounds_[0].loaded ? "YES" : "NO", + ", Holy: ", impactHolySounds_[0].loaded ? "YES" : "NO"); + + initialized_ = true; + LOG_INFO("SpellSoundManager: Initialization complete"); + return true; +} + +void SpellSoundManager::shutdown() { + initialized_ = false; +} + +bool SpellSoundManager::loadSound(const std::string& path, SpellSample& sample, pipeline::AssetManager* assets) { + sample.path = path; + sample.loaded = false; + + try { + sample.data = assets->readFile(path); + if (!sample.data.empty()) { + sample.loaded = true; + return true; + } + } catch (const std::exception& e) { + // Silently fail - not all sounds may exist + } + + return false; +} + +void SpellSoundManager::playSound(const std::vector& library, float volumeMultiplier) { + if (!initialized_ || library.empty() || !library[0].loaded) return; + + float volume = 0.75f * volumeScale_ * volumeMultiplier; + AudioEngine::instance().playSound2D(library[0].data, volume, 1.0f); +} + +void SpellSoundManager::playRandomSound(const std::vector& library, float volumeMultiplier) { + if (!initialized_ || library.empty()) return; + + // Count loaded sounds + std::vector loadedSounds; + for (const auto& sample : library) { + if (sample.loaded) { + loadedSounds.push_back(&sample); + } + } + + if (loadedSounds.empty()) return; + + // Pick random sound + std::uniform_int_distribution dist(0, loadedSounds.size() - 1); + size_t index = dist(gen); + + float volume = 0.75f * volumeScale_ * volumeMultiplier; + AudioEngine::instance().playSound2D(loadedSounds[index]->data, volume, 1.0f); +} + +void SpellSoundManager::setVolumeScale(float scale) { + volumeScale_ = std::max(0.0f, std::min(1.0f, scale)); +} + +void SpellSoundManager::playPrecast(MagicSchool school, SpellPower power) { + const std::vector* library = nullptr; + + switch (school) { + case MagicSchool::FIRE: + library = (power == SpellPower::LOW) ? &precastFireLowSounds_ : + (power == SpellPower::MEDIUM) ? &precastFireMediumSounds_ : + &precastFireHighSounds_; + break; + case MagicSchool::FROST: + library = (power == SpellPower::LOW) ? &precastFrostLowSounds_ : + (power == SpellPower::MEDIUM) ? &precastFrostMediumSounds_ : + &precastFrostHighSounds_; + break; + case MagicSchool::HOLY: + library = (power == SpellPower::LOW) ? &precastHolyLowSounds_ : + (power == SpellPower::MEDIUM) ? &precastHolyMediumSounds_ : + &precastHolyHighSounds_; + break; + case MagicSchool::NATURE: + library = (power == SpellPower::LOW) ? &precastNatureLowSounds_ : + (power == SpellPower::MEDIUM) ? &precastNatureMediumSounds_ : + &precastNatureHighSounds_; + break; + case MagicSchool::SHADOW: + library = (power == SpellPower::LOW) ? &precastShadowLowSounds_ : + (power == SpellPower::MEDIUM) ? &precastShadowMediumSounds_ : + &precastShadowHighSounds_; + break; + case MagicSchool::ARCANE: + library = &precastArcaneSounds_; + break; + default: + return; + } + + if (library) { + playSound(*library); + } +} + +void SpellSoundManager::playCast(MagicSchool school) { + switch (school) { + case MagicSchool::FIRE: + playSound(castFireSounds_); + break; + case MagicSchool::FROST: + playSound(castFrostSounds_); + break; + case MagicSchool::HOLY: + playSound(castHolySounds_); + break; + case MagicSchool::NATURE: + playSound(castNatureSounds_); + break; + case MagicSchool::SHADOW: + playSound(castShadowSounds_); + break; + default: + break; + } +} + +void SpellSoundManager::playImpact(MagicSchool school, SpellPower power) { + switch (school) { + case MagicSchool::FIRE: + playRandomSound(impactFireballSounds_); + break; + case MagicSchool::FROST: + playRandomSound(impactBlizzardSounds_); + break; + case MagicSchool::HOLY: + if (power == SpellPower::LOW) { + playSound(impactHolySounds_); // Use first (low) + } else if (power == SpellPower::MEDIUM && impactHolySounds_.size() > 1) { + playSound({impactHolySounds_[1]}); + } else if (power == SpellPower::HIGH && impactHolySounds_.size() > 2) { + playSound({impactHolySounds_[2]}); + } + break; + case MagicSchool::ARCANE: + playRandomSound(impactArcaneMissileSounds_); + break; + default: + break; + } +} + +void SpellSoundManager::playFireball() { + playPrecast(MagicSchool::FIRE, SpellPower::MEDIUM); +} + +void SpellSoundManager::playFrostbolt() { + playPrecast(MagicSchool::FROST, SpellPower::MEDIUM); +} + +void SpellSoundManager::playLightningBolt() { + playPrecast(MagicSchool::NATURE, SpellPower::MEDIUM); +} + +void SpellSoundManager::playHeal() { + playPrecast(MagicSchool::HOLY, SpellPower::MEDIUM); +} + +void SpellSoundManager::playShadowBolt() { + playPrecast(MagicSchool::SHADOW, SpellPower::MEDIUM); +} + +} // namespace audio +} // namespace wowee