From 5af9f7aa4bfdaec2d0abf3d2db06d321e359324a Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 2 Apr 2026 13:06:31 +0300 Subject: [PATCH] chore(renderer): extract AnimationController and remove audio pass-throughs Extract ~1,500 lines of character animation state from Renderer into a dedicated AnimationController class, and complete the AudioCoordinator migration by removing all 10 audio pass-through getters from Renderer. AnimationController: - New: include/rendering/animation_controller.hpp (182 lines) - New: src/rendering/animation_controller.cpp (1,703 lines) - Moves: locomotion state machine (50+ members), mount animation (40+ members), emote system, footstep triggering, surface detection, melee combat animations - Renderer holds std::unique_ptr and delegates completely - AnimationController accesses audio via renderer_->getAudioCoordinator() Audio caller migration: - Migrate ~60 external callers from renderer->getXManager() to AudioCoordinator directly, grouped by access pattern: - UIServices: settings_panel, game_screen, toast_manager, chat_panel, combat_ui, window_manager - GameServices: game_handler, spell_handler, inventory_handler, quest_handler, social_handler, combat_handler - Application singleton: application.cpp, auth_screen.cpp, lua_engine.cpp - Remove 10 pass-through getter definitions from renderer.cpp - Remove 10 pass-through getter declarations from renderer.hpp - Remove individual audio manager forward declarations from renderer.hpp - Redirect 69 internal renderer.cpp audio calls to audioCoordinator_ directly - game_handler.cpp: withSoundManager template uses services_.audioCoordinator; MFP changed from &Renderer::getUiSoundManager to &AudioCoordinator::getUiSoundManager - GameServices struct: add AudioCoordinator* audioCoordinator member - settings_panel: applyAudioVolumes(Renderer*) -> applyAudioVolumes(AudioCoordinator*) --- CMakeLists.txt | 1 + include/game/game_services.hpp | 2 + include/rendering/animation_controller.hpp | 182 ++ include/rendering/renderer.hpp | 126 +- include/ui/settings_panel.hpp | 5 +- src/addons/lua_engine.cpp | 7 +- src/core/application.cpp | 23 +- src/game/combat_handler.cpp | 7 +- src/game/game_handler.cpp | 17 +- src/game/inventory_handler.cpp | 47 +- src/game/quest_handler.cpp | 9 +- src/game/social_handler.cpp | 9 +- src/game/spell_handler.cpp | 37 +- src/rendering/animation_controller.cpp | 1703 ++++++++++++++++++ src/rendering/renderer.cpp | 1876 ++------------------ src/ui/auth_screen.cpp | 17 +- src/ui/chat_panel.cpp | 5 +- src/ui/combat_ui.cpp | 9 +- src/ui/game_screen.cpp | 35 +- src/ui/settings_panel.cpp | 27 +- src/ui/toast_manager.cpp | 15 +- src/ui/window_manager.cpp | 7 +- 22 files changed, 2208 insertions(+), 1958 deletions(-) create mode 100644 include/rendering/animation_controller.hpp create mode 100644 src/rendering/animation_controller.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index fe288549..e2a95590 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -613,6 +613,7 @@ set(WOWEE_SOURCES src/rendering/charge_effect.cpp src/rendering/spell_visual_system.cpp src/rendering/post_process_pipeline.cpp + src/rendering/animation_controller.cpp src/rendering/loading_screen.cpp # UI diff --git a/include/game/game_services.hpp b/include/game/game_services.hpp index e01f4487..21a14251 100644 --- a/include/game/game_services.hpp +++ b/include/game/game_services.hpp @@ -4,6 +4,7 @@ namespace wowee { namespace rendering { class Renderer; } namespace pipeline { class AssetManager; } +namespace audio { class AudioCoordinator; } namespace game { class ExpansionRegistry; } namespace game { @@ -13,6 +14,7 @@ namespace game { // Replaces hidden Application::getInstance() singleton access. struct GameServices { rendering::Renderer* renderer = nullptr; + audio::AudioCoordinator* audioCoordinator = nullptr; pipeline::AssetManager* assetManager = nullptr; ExpansionRegistry* expansionRegistry = nullptr; uint32_t gryphonDisplayId = 0; diff --git a/include/rendering/animation_controller.hpp b/include/rendering/animation_controller.hpp new file mode 100644 index 00000000..81166d12 --- /dev/null +++ b/include/rendering/animation_controller.hpp @@ -0,0 +1,182 @@ +#pragma once + +#include +#include +#include +#include + +namespace wowee { +namespace audio { enum class FootstepSurface : uint8_t; } +namespace rendering { + +class Renderer; + +// ============================================================================ +// AnimationController — extracted from Renderer (§4.2) +// +// Owns the character locomotion state machine, mount animation state, +// emote system, footstep triggering, surface detection, melee combat +// animation, and activity SFX transition tracking. +// ============================================================================ +class AnimationController { +public: + AnimationController(); + ~AnimationController(); + + void initialize(Renderer* renderer); + + // ── Per-frame update hooks (called from Renderer::update) ────────────── + // Runs the character animation state machine (mounted + unmounted). + void updateCharacterAnimation(); + // Processes animation-driven footstep events (player + mount). + void updateFootsteps(float deltaTime); + // Tracks state transitions for activity SFX (jump, landing, swim) and + // mount ambient sounds. + void updateSfxState(float deltaTime); + // Decrements melee swing timer / cooldown. + void updateMeleeTimers(float deltaTime); + // Store per-frame delta time (used inside animation state machine). + void setDeltaTime(float dt) { lastDeltaTime_ = dt; } + + // ── Character follow ─────────────────────────────────────────────────── + void onCharacterFollow(uint32_t instanceId); + + // ── Emote support ────────────────────────────────────────────────────── + void playEmote(const std::string& emoteName); + void cancelEmote(); + bool isEmoteActive() const { return emoteActive_; } + static std::string getEmoteText(const std::string& emoteName, + const std::string* targetName = nullptr); + static uint32_t getEmoteDbcId(const std::string& emoteName); + static std::string getEmoteTextByDbcId(uint32_t dbcId, + const std::string& senderName, + const std::string* targetName = nullptr); + static uint32_t getEmoteAnimByDbcId(uint32_t dbcId); + + // ── Targeting / combat ───────────────────────────────────────────────── + void setTargetPosition(const glm::vec3* pos); + void setInCombat(bool combat) { inCombat_ = combat; } + bool isInCombat() const { return inCombat_; } + const glm::vec3* getTargetPosition() const { return targetPosition_; } + void resetCombatVisualState(); + bool isMoving() const; + + // ── Melee combat ─────────────────────────────────────────────────────── + void triggerMeleeSwing(); + void setEquippedWeaponType(uint32_t inventoryType) { equippedWeaponInvType_ = inventoryType; meleeAnimId_ = 0; } + void setCharging(bool charging) { charging_ = charging; } + bool isCharging() const { return charging_; } + + // ── Effect triggers ──────────────────────────────────────────────────── + void triggerLevelUpEffect(const glm::vec3& position); + void startChargeEffect(const glm::vec3& position, const glm::vec3& direction); + void emitChargeEffect(const glm::vec3& position, const glm::vec3& direction); + void stopChargeEffect(); + + // ── Mount ────────────────────────────────────────────────────────────── + void setMounted(uint32_t mountInstId, uint32_t mountDisplayId, + float heightOffset, const std::string& modelPath = ""); + void setTaxiFlight(bool onTaxi) { taxiFlight_ = onTaxi; } + void setMountPitchRoll(float pitch, float roll) { mountPitch_ = pitch; mountRoll_ = roll; } + void clearMount(); + bool isMounted() const { return mountInstanceId_ != 0; } + uint32_t getMountInstanceId() const { return mountInstanceId_; } + + // ── Query helpers (used by Renderer) ─────────────────────────────────── + bool isFootstepAnimationState() const; + float getMeleeSwingTimer() const { return meleeSwingTimer_; } + float getMountHeightOffset() const { return mountHeightOffset_; } + bool isTaxiFlight() const { return taxiFlight_; } + +private: + Renderer* renderer_ = nullptr; + + // Character animation state machine + enum class CharAnimState { + IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING, + EMOTE, SWIM_IDLE, SWIM, MELEE_SWING, MOUNT, CHARGE, COMBAT_IDLE + }; + CharAnimState charAnimState_ = CharAnimState::IDLE; + float locomotionStopGraceTimer_ = 0.0f; + bool locomotionWasSprinting_ = false; + uint32_t lastPlayerAnimRequest_ = UINT32_MAX; + bool lastPlayerAnimLoopRequest_ = true; + + // Emote state + bool emoteActive_ = false; + uint32_t emoteAnimId_ = 0; + bool emoteLoop_ = false; + + // Target facing + const glm::vec3* targetPosition_ = nullptr; + bool inCombat_ = false; + + // Footstep event tracking (animation-driven) + uint32_t footstepLastAnimationId_ = 0; + float footstepLastNormTime_ = 0.0f; + bool footstepNormInitialized_ = false; + + // Footstep surface cache (avoid expensive queries every step) + mutable audio::FootstepSurface cachedFootstepSurface_{}; + mutable glm::vec3 cachedFootstepPosition_{0.0f, 0.0f, 0.0f}; + mutable float cachedFootstepUpdateTimer_{999.0f}; + + // Mount footstep tracking (separate from player's) + uint32_t mountFootstepLastAnimId_ = 0; + float mountFootstepLastNormTime_ = 0.0f; + bool mountFootstepNormInitialized_ = false; + + // SFX transition state + bool sfxStateInitialized_ = false; + bool sfxPrevGrounded_ = true; + bool sfxPrevJumping_ = false; + bool sfxPrevFalling_ = false; + bool sfxPrevSwimming_ = false; + + // Melee combat + bool charging_ = false; + float meleeSwingTimer_ = 0.0f; + float meleeSwingCooldown_ = 0.0f; + float meleeAnimDurationMs_ = 0.0f; + uint32_t meleeAnimId_ = 0; + uint32_t equippedWeaponInvType_ = 0; + + // Mount animation capabilities (discovered at mount time, varies per model) + struct MountAnimSet { + uint32_t jumpStart = 0; // Jump start animation + uint32_t jumpLoop = 0; // Jump airborne loop + uint32_t jumpEnd = 0; // Jump landing + uint32_t rearUp = 0; // Rear-up / special flourish + uint32_t run = 0; // Run animation (discovered, don't assume) + uint32_t stand = 0; // Stand animation (discovered) + std::vector fidgets; // Idle fidget animations (head turn, tail swish, etc.) + }; + + enum class MountAction { None, Jump, RearUp }; + + uint32_t mountInstanceId_ = 0; + float mountHeightOffset_ = 0.0f; + float mountPitch_ = 0.0f; // Up/down tilt (radians) + float mountRoll_ = 0.0f; // Left/right banking (radians) + int mountSeatAttachmentId_ = -1; // -1 unknown, -2 unavailable + glm::vec3 smoothedMountSeatPos_ = glm::vec3(0.0f); + bool mountSeatSmoothingInit_ = false; + float prevMountYaw_ = 0.0f; // Previous yaw for turn rate calculation (procedural lean) + float lastDeltaTime_ = 0.0f; // Cached for use in updateCharacterAnimation() + MountAction mountAction_ = MountAction::None; // Current mount action (jump/rear-up) + uint32_t mountActionPhase_ = 0; // 0=start, 1=loop, 2=end (for jump chaining) + MountAnimSet mountAnims_; // Cached animation IDs for current mount + float mountIdleFidgetTimer_ = 0.0f; // Timer for random idle fidgets + float mountIdleSoundTimer_ = 0.0f; // Timer for ambient idle sounds + uint32_t mountActiveFidget_ = 0; // Currently playing fidget animation ID (0 = none) + bool taxiFlight_ = false; + bool taxiAnimsLogged_ = false; + + // Private animation helpers + bool shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs); + audio::FootstepSurface resolveFootstepSurface() const; + uint32_t resolveMeleeAnimId(); +}; + +} // namespace rendering +} // namespace wowee diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 10867a7b..54372da9 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -19,7 +19,7 @@ namespace wowee { namespace core { class Window; } namespace rendering { class VkContext; } namespace game { class World; class ZoneManager; class GameHandler; } -namespace audio { class AudioCoordinator; class MusicManager; class FootstepManager; class ActivitySoundManager; class MountSoundManager; class NpcVoiceManager; class AmbientSoundManager; class UiSoundManager; class CombatSoundManager; class SpellSoundManager; class MovementSoundManager; enum class FootstepSurface : uint8_t; enum class VoiceType; } +namespace audio { class AudioCoordinator; } namespace pipeline { class AssetManager; } namespace rendering { @@ -52,6 +52,10 @@ class CharacterPreview; class AmdFsr3Runtime; class SpellVisualSystem; class PostProcessPipeline; +class AnimationController; +class LevelUpEffect; +class ChargeEffect; +class SwimEffects; class Renderer { public: @@ -146,7 +150,7 @@ public: float getCharacterYaw() const { return characterYaw; } void setCharacterYaw(float yawDeg) { characterYaw = yawDeg; } - // Emote support + // Emote support — delegates to AnimationController (§4.2) void playEmote(const std::string& emoteName); void triggerLevelUpEffect(const glm::vec3& position); void cancelEmote(); @@ -159,31 +163,37 @@ public: void playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition, bool useImpactKit = false); SpellVisualSystem* getSpellVisualSystem() const { return spellVisualSystem_.get(); } - bool isEmoteActive() const { return emoteActive; } + bool isEmoteActive() const; static std::string getEmoteText(const std::string& emoteName, const std::string* targetName = nullptr); static uint32_t getEmoteDbcId(const std::string& emoteName); static std::string getEmoteTextByDbcId(uint32_t dbcId, const std::string& senderName, const std::string* targetName = nullptr); static uint32_t getEmoteAnimByDbcId(uint32_t dbcId); - // Targeting support + // Targeting support — delegates to AnimationController (§4.2) void setTargetPosition(const glm::vec3* pos); - void setInCombat(bool combat) { inCombat_ = combat; } + void setInCombat(bool combat); void resetCombatVisualState(); bool isMoving() const; void triggerMeleeSwing(); - void setEquippedWeaponType(uint32_t inventoryType) { equippedWeaponInvType_ = inventoryType; meleeAnimId = 0; } - void setCharging(bool charging) { charging_ = charging; } - bool isCharging() const { return charging_; } + void setEquippedWeaponType(uint32_t inventoryType); + void setCharging(bool charging); + bool isCharging() const; void startChargeEffect(const glm::vec3& position, const glm::vec3& direction); void emitChargeEffect(const glm::vec3& position, const glm::vec3& direction); void stopChargeEffect(); - // Mount rendering + // Mount rendering — delegates to AnimationController (§4.2) void setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset, const std::string& modelPath = ""); - void setTaxiFlight(bool onTaxi) { taxiFlight_ = onTaxi; } - void setMountPitchRoll(float pitch, float roll) { mountPitch_ = pitch; mountRoll_ = roll; } + void setTaxiFlight(bool onTaxi); + void setMountPitchRoll(float pitch, float roll); void clearMount(); - bool isMounted() const { return mountInstanceId_ != 0; } + bool isMounted() const; + + // AnimationController access (§4.2) + AnimationController* getAnimationController() const { return animationController_.get(); } + LevelUpEffect* getLevelUpEffect() const { return levelUpEffect.get(); } + ChargeEffect* getChargeEffect() const { return chargeEffect.get(); } + SwimEffects* getSwimEffects() const { return swimEffects.get(); } // Selection circle for targeted entity void setSelectionCircle(const glm::vec3& pos, float radius, const glm::vec3& color); @@ -196,20 +206,9 @@ public: double getLastTerrainRenderMs() const { return lastTerrainRenderMs; } double getLastWMORenderMs() const { return lastWMORenderMs; } double getLastM2RenderMs() const { return lastM2RenderMs; } - // Audio accessors — delegate to AudioCoordinator (owned by Application). - // These pass-throughs remain until §4.2 moves animation audio out of Renderer. + // Audio coordinator — owned by Application, set via setAudioCoordinator(). void setAudioCoordinator(audio::AudioCoordinator* ac) { audioCoordinator_ = ac; } audio::AudioCoordinator* getAudioCoordinator() { return audioCoordinator_; } - audio::MusicManager* getMusicManager(); - audio::FootstepManager* getFootstepManager(); - audio::ActivitySoundManager* getActivitySoundManager(); - audio::MountSoundManager* getMountSoundManager(); - audio::NpcVoiceManager* getNpcVoiceManager(); - audio::AmbientSoundManager* getAmbientSoundManager(); - audio::UiSoundManager* getUiSoundManager(); - audio::CombatSoundManager* getCombatSoundManager(); - audio::SpellSoundManager* getSpellSoundManager(); - audio::MovementSoundManager* getMovementSoundManager(); game::ZoneManager* getZoneManager() { return zoneManager.get(); } LightingManager* getLightingManager() { return lightingManager.get(); } @@ -243,6 +242,7 @@ private: std::unique_ptr worldMap; std::unique_ptr questMarkerRenderer; audio::AudioCoordinator* audioCoordinator_ = nullptr; // Owned by Application + std::unique_ptr animationController_; // §4.2 std::unique_ptr zoneManager; // Shadow mapping (Vulkan) static constexpr uint32_t SHADOW_MAP_SIZE = 4096; @@ -340,27 +340,7 @@ private: uint32_t characterInstanceId = 0; float characterYaw = 0.0f; - // Character animation state - enum class CharAnimState { IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING, EMOTE, SWIM_IDLE, SWIM, MELEE_SWING, MOUNT, CHARGE, COMBAT_IDLE }; - CharAnimState charAnimState = CharAnimState::IDLE; - float locomotionStopGraceTimer_ = 0.0f; - bool locomotionWasSprinting_ = false; - uint32_t lastPlayerAnimRequest_ = UINT32_MAX; - bool lastPlayerAnimLoopRequest_ = true; - void updateCharacterAnimation(); - bool isFootstepAnimationState() const; - bool shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs); - audio::FootstepSurface resolveFootstepSurface() const; - uint32_t resolveMeleeAnimId(); - // Emote state - bool emoteActive = false; - uint32_t emoteAnimId = 0; - bool emoteLoop = false; - - // Target facing - const glm::vec3* targetPosition = nullptr; - bool inCombat_ = false; // Selection circle rendering (Vulkan) VkPipeline selCirclePipeline = VK_NULL_HANDLE; @@ -383,64 +363,7 @@ private: void initOverlayPipeline(); void renderOverlay(const glm::vec4& color, VkCommandBuffer overrideCmd = VK_NULL_HANDLE); - // Footstep event tracking (animation-driven) - uint32_t footstepLastAnimationId = 0; - float footstepLastNormTime = 0.0f; - bool footstepNormInitialized = false; - // Footstep surface cache (avoid expensive queries every step) - mutable audio::FootstepSurface cachedFootstepSurface{}; - mutable glm::vec3 cachedFootstepPosition{0.0f, 0.0f, 0.0f}; - mutable float cachedFootstepUpdateTimer{999.0f}; // Force initial query - - // Mount footstep tracking (separate from player's) - uint32_t mountFootstepLastAnimId = 0; - float mountFootstepLastNormTime = 0.0f; - bool mountFootstepNormInitialized = false; - bool sfxStateInitialized = false; - bool sfxPrevGrounded = true; - bool sfxPrevJumping = false; - bool sfxPrevFalling = false; - bool sfxPrevSwimming = false; - - bool charging_ = false; - float meleeSwingTimer = 0.0f; - float meleeSwingCooldown = 0.0f; - float meleeAnimDurationMs = 0.0f; - uint32_t meleeAnimId = 0; - uint32_t equippedWeaponInvType_ = 0; - - // Mount state - // Mount animation capabilities (discovered at mount time, varies per model) - struct MountAnimSet { - uint32_t jumpStart = 0; // Jump start animation - uint32_t jumpLoop = 0; // Jump airborne loop - uint32_t jumpEnd = 0; // Jump landing - uint32_t rearUp = 0; // Rear-up / special flourish - uint32_t run = 0; // Run animation (discovered, don't assume) - uint32_t stand = 0; // Stand animation (discovered) - std::vector fidgets; // Idle fidget animations (head turn, tail swish, etc.) - }; - - enum class MountAction { None, Jump, RearUp }; - - uint32_t mountInstanceId_ = 0; - float mountHeightOffset_ = 0.0f; - float mountPitch_ = 0.0f; // Up/down tilt (radians) - float mountRoll_ = 0.0f; // Left/right banking (radians) - int mountSeatAttachmentId_ = -1; // -1 unknown, -2 unavailable - glm::vec3 smoothedMountSeatPos_ = glm::vec3(0.0f); - bool mountSeatSmoothingInit_ = false; - float prevMountYaw_ = 0.0f; // Previous yaw for turn rate calculation (procedural lean) - float lastDeltaTime_ = 0.0f; // Cached for use in updateCharacterAnimation() - MountAction mountAction_ = MountAction::None; // Current mount action (jump/rear-up) - uint32_t mountActionPhase_ = 0; // 0=start, 1=loop, 2=end (for jump chaining) - MountAnimSet mountAnims_; // Cached animation IDs for current mount - float mountIdleFidgetTimer_ = 0.0f; // Timer for random idle fidgets - float mountIdleSoundTimer_ = 0.0f; // Timer for ambient idle sounds - uint32_t mountActiveFidget_ = 0; // Currently playing fidget animation ID (0 = none) - bool taxiFlight_ = false; - bool taxiAnimsLogged_ = false; // Vulkan frame state VkContext* vkCtx = nullptr; @@ -491,6 +414,7 @@ private: bool parallelRecordingEnabled_ = false; // set true after pools/buffers created bool endFrameInlineMode_ = false; // true when endFrame switched to INLINE render pass + float lastDeltaTime_ = 0.0f; // cached for post-process pipeline bool createSecondaryCommandResources(); void destroySecondaryCommandResources(); VkCommandBuffer beginSecondary(uint32_t secondaryIndex); diff --git a/include/ui/settings_panel.hpp b/include/ui/settings_panel.hpp index 5dd58136..a024ffad 100644 --- a/include/ui/settings_panel.hpp +++ b/include/ui/settings_panel.hpp @@ -8,6 +8,7 @@ namespace wowee { namespace rendering { class Renderer; } +namespace audio { class AudioCoordinator; } namespace ui { class InventoryScreen; @@ -144,8 +145,8 @@ public: void renderSettingsWindow(InventoryScreen& inventoryScreen, ChatPanel& chatPanel, std::function saveCallback); - /// Apply audio volume levels to all renderer sound managers - void applyAudioVolumes(rendering::Renderer* renderer); + /// Apply audio volume levels to all audio coordinator sound managers + void applyAudioVolumes(audio::AudioCoordinator* ac); /// Return the platform-specific settings file path static std::string getSettingsPath(); diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index 625054c2..bbba10b9 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -6,6 +6,7 @@ #include "core/logger.hpp" #include "core/application.hpp" #include "rendering/renderer.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/ui_sound_manager.hpp" #include "game/expansion_profile.hpp" #include @@ -1003,9 +1004,9 @@ static int lua_IsInRaid(lua_State* L) { // PlaySound(soundId) — play a WoW UI sound by ID or name static int lua_PlaySound(lua_State* L) { - auto* renderer = core::Application::getInstance().getRenderer(); - if (!renderer) return 0; - auto* sfx = renderer->getUiSoundManager(); + auto* ac = core::Application::getInstance().getAudioCoordinator(); + if (!ac) return 0; + auto* sfx = ac->getUiSoundManager(); if (!sfx) return 0; // Accept numeric sound ID or string name diff --git a/src/core/application.cpp b/src/core/application.cpp index c5621daf..a68bf754 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -152,6 +152,7 @@ bool Application::initialize() { // Populate game services — all subsystems now available gameServices_.renderer = renderer.get(); + gameServices_.audioCoordinator = audioCoordinator_.get(); gameServices_.assetManager = assetManager.get(); gameServices_.expansionRegistry = expansionRegistry_.get(); @@ -1091,7 +1092,7 @@ void Application::logoutToLogin() { } renderer->clearMount(); renderer->setCharacterFollow(0); - if (auto* music = renderer->getMusicManager()) { + if (auto* music = audioCoordinator_ ? audioCoordinator_->getMusicManager() : nullptr) { music->stopMusic(0.0f); } } @@ -2828,7 +2829,7 @@ void Application::setupUICallbacks() { // Resolves soundId → SoundEntries.dbc → MPQ path → MusicManager. gameHandler->setPlayMusicCallback([this](uint32_t soundId) { if (!assetManager || !renderer) return; - auto* music = renderer->getMusicManager(); + auto* music = audioCoordinator_ ? audioCoordinator_->getMusicManager() : nullptr; if (!music) return; auto dbc = assetManager->loadDBC("SoundEntries.dbc"); @@ -3418,7 +3419,7 @@ void Application::setupUICallbacks() { // NPC greeting callback - play voice line gameHandler->setNpcGreetingCallback([this](uint64_t guid, const glm::vec3& position) { - if (renderer && renderer->getNpcVoiceManager()) { + if (audioCoordinator_ && audioCoordinator_->getNpcVoiceManager()) { // Convert canonical to render coords for 3D audio glm::vec3 renderPos = core::coords::canonicalToRender(position); @@ -3431,13 +3432,13 @@ void Application::setupUICallbacks() { voiceType = entitySpawner_->detectVoiceTypeFromDisplayId(displayId); } - renderer->getNpcVoiceManager()->playGreeting(guid, voiceType, renderPos); + audioCoordinator_->getNpcVoiceManager()->playGreeting(guid, voiceType, renderPos); } }); // NPC farewell callback - play farewell voice line gameHandler->setNpcFarewellCallback([this](uint64_t guid, const glm::vec3& position) { - if (renderer && renderer->getNpcVoiceManager()) { + if (audioCoordinator_ && audioCoordinator_->getNpcVoiceManager()) { glm::vec3 renderPos = core::coords::canonicalToRender(position); audio::VoiceType voiceType = audio::VoiceType::GENERIC; @@ -3448,13 +3449,13 @@ void Application::setupUICallbacks() { voiceType = entitySpawner_->detectVoiceTypeFromDisplayId(displayId); } - renderer->getNpcVoiceManager()->playFarewell(guid, voiceType, renderPos); + audioCoordinator_->getNpcVoiceManager()->playFarewell(guid, voiceType, renderPos); } }); // NPC vendor callback - play vendor voice line gameHandler->setNpcVendorCallback([this](uint64_t guid, const glm::vec3& position) { - if (renderer && renderer->getNpcVoiceManager()) { + if (audioCoordinator_ && audioCoordinator_->getNpcVoiceManager()) { glm::vec3 renderPos = core::coords::canonicalToRender(position); audio::VoiceType voiceType = audio::VoiceType::GENERIC; @@ -3465,13 +3466,13 @@ void Application::setupUICallbacks() { voiceType = entitySpawner_->detectVoiceTypeFromDisplayId(displayId); } - renderer->getNpcVoiceManager()->playVendor(guid, voiceType, renderPos); + audioCoordinator_->getNpcVoiceManager()->playVendor(guid, voiceType, renderPos); } }); // NPC aggro callback - play combat start voice line gameHandler->setNpcAggroCallback([this](uint64_t guid, const glm::vec3& position) { - if (renderer && renderer->getNpcVoiceManager()) { + if (audioCoordinator_ && audioCoordinator_->getNpcVoiceManager()) { glm::vec3 renderPos = core::coords::canonicalToRender(position); audio::VoiceType voiceType = audio::VoiceType::GENERIC; @@ -3482,7 +3483,7 @@ void Application::setupUICallbacks() { voiceType = entitySpawner_->detectVoiceTypeFromDisplayId(displayId); } - renderer->getNpcVoiceManager()->playAggro(guid, voiceType, renderPos); + audioCoordinator_->getNpcVoiceManager()->playAggro(guid, voiceType, renderPos); } }); @@ -3718,7 +3719,7 @@ void Application::spawnPlayerCharacter() { playerCharacterSpawned = true; // Set voice profile to match character race/gender - if (auto* asm_ = renderer->getActivitySoundManager()) { + if (auto* asm_ = audioCoordinator_ ? audioCoordinator_->getActivitySoundManager() : nullptr) { const char* raceFolder = "Human"; const char* raceBase = "Human"; switch (playerRace_) { diff --git a/src/game/combat_handler.cpp b/src/game/combat_handler.cpp index 32f120b7..a32f1d47 100644 --- a/src/game/combat_handler.cpp +++ b/src/game/combat_handler.cpp @@ -6,6 +6,7 @@ #include "game/update_field_table.hpp" #include "game/opcode_table.hpp" #include "rendering/renderer.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/combat_sound_manager.hpp" #include "audio/activity_sound_manager.hpp" #include "core/application.hpp" @@ -451,8 +452,8 @@ void CombatHandler::handleAttackerStateUpdate(network::Packet& packet) { } // Play combat sounds via CombatSoundManager + character vocalizations - if (auto* renderer = owner_.services().renderer) { - if (auto* csm = renderer->getCombatSoundManager()) { + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* csm = ac->getCombatSoundManager()) { auto weaponSize = audio::CombatSoundManager::WeaponSize::MEDIUM; if (data.isMiss()) { csm->playWeaponMiss(false); @@ -466,7 +467,7 @@ void CombatHandler::handleAttackerStateUpdate(network::Packet& packet) { } } // Character vocalizations - if (auto* asm_ = renderer->getActivitySoundManager()) { + if (auto* asm_ = ac->getActivitySoundManager()) { if (isPlayerAttacker && !data.isMiss() && data.victimState != 1 && data.victimState != 2) { asm_->playAttackGrunt(); } diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 6a8f37a4..ed8087b0 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -17,6 +17,7 @@ #include "game/update_field_table.hpp" #include "game/expansion_profile.hpp" #include "rendering/renderer.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/activity_sound_manager.hpp" #include "audio/combat_sound_manager.hpp" #include "audio/spell_sound_manager.hpp" @@ -599,8 +600,8 @@ static QuestQueryRewards tryParseQuestRewards(const std::vector& data, template void GameHandler::withSoundManager(ManagerGetter getter, Callback cb) { - if (auto* renderer = services_.renderer) { - if (auto* mgr = (renderer->*getter)()) cb(mgr); + if (auto* ac = services_.audioCoordinator) { + if (auto* mgr = (ac->*getter)()) cb(mgr); } } @@ -1198,9 +1199,9 @@ void GameHandler::updateTimers(float deltaTime) { } if (!alreadyAnnounced && pendingLootMoneyAmount_ > 0) { addSystemChatMessage("Looted: " + formatCopperAmount(pendingLootMoneyAmount_)); - auto* renderer = services_.renderer; - if (renderer) { - if (auto* sfx = renderer->getUiSoundManager()) { + auto* ac = services_.audioCoordinator; + if (ac) { + if (auto* sfx = ac->getUiSoundManager()) { if (pendingLootMoneyAmount_ >= 10000) { sfx->playLootCoinLarge(); } else { @@ -1974,7 +1975,7 @@ void GameHandler::registerOpcodeHandlers() { ping.age = 0.0f; minimapPings_.push_back(ping); if (senderGuid != playerGuid) { - withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playMinimapPing(); }); + withSoundManager(&audio::AudioCoordinator::getUiSoundManager, [](auto* sfx) { sfx->playMinimapPing(); }); } }; dispatchTable_[Opcode::SMSG_ZONE_UNDER_ATTACK] = [this](network::Packet& packet) { @@ -2124,7 +2125,7 @@ void GameHandler::registerOpcodeHandlers() { if (info && info->type == 17) { addUIError("A fish is on your line!"); addSystemChatMessage("A fish is on your line!"); - withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playQuestUpdate(); }); + withSoundManager(&audio::AudioCoordinator::getUiSoundManager, [](auto* sfx) { sfx->playQuestUpdate(); }); } } } @@ -2350,7 +2351,7 @@ void GameHandler::registerOpcodeHandlers() { } if (newLevel > oldLevel) { addSystemChatMessage("You have reached level " + std::to_string(newLevel) + "!"); - withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playLevelUp(); }); + withSoundManager(&audio::AudioCoordinator::getUiSoundManager, [](auto* sfx) { sfx->playLevelUp(); }); if (levelUpCallback_) levelUpCallback_(newLevel); fireAddonEvent("PLAYER_LEVEL_UP", {std::to_string(newLevel)}); } diff --git a/src/game/inventory_handler.cpp b/src/game/inventory_handler.cpp index 385a5850..1a4c1ab4 100644 --- a/src/game/inventory_handler.cpp +++ b/src/game/inventory_handler.cpp @@ -4,6 +4,7 @@ #include "game/entity.hpp" #include "game/packet_parsers.hpp" #include "rendering/renderer.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/ui_sound_manager.hpp" #include "core/application.hpp" #include "core/logger.hpp" @@ -70,9 +71,9 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) { } if (!alreadyAnnounced) { owner_.addSystemChatMessage("Looted: " + formatCopperAmount(amount)); - auto* renderer = owner_.services().renderer; - if (renderer) { - if (auto* sfx = renderer->getUiSoundManager()) { + auto* ac = owner_.services().audioCoordinator; + if (ac) { + if (auto* sfx = ac->getUiSoundManager()) { if (amount >= 10000) sfx->playLootCoinLarge(); else sfx->playLootCoinSmall(); } @@ -222,8 +223,8 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) { std::string msg = "Received item: " + link; if (count > 1) msg += " x" + std::to_string(count); owner_.addSystemChatMessage(msg); - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playLootItem(); } if (owner_.addonEventCallback_) { @@ -253,8 +254,8 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) { " result=", static_cast(result)); if (result == 0) { pendingSellToBuyback_.erase(itemGuid); - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playDropOnGround(); } if (owner_.addonEventCallback_) { @@ -295,8 +296,8 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) { const char* msg = (result < 7) ? sellErrors[result] : "Unknown sell error"; owner_.addUIError(std::string("Sell failed: ") + msg); owner_.addSystemChatMessage(std::string("Sell failed: ") + msg); - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playError(); } LOG_WARNING("SMSG_SELL_ITEM error: ", (int)result, " (", msg, ")"); @@ -392,8 +393,8 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) { std::string msg = errMsg ? errMsg : "Inventory error (" + std::to_string(error) + ")."; owner_.addUIError(msg); owner_.addSystemChatMessage(msg); - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playError(); } } @@ -450,8 +451,8 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) { } owner_.addUIError(msg); owner_.addSystemChatMessage(msg); - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playError(); } } @@ -474,8 +475,8 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) { std::string msg = "Purchased: " + buildItemLink(pendingBuyItemId_, buyQuality, itemLabel); if (itemCount > 1) msg += " x" + std::to_string(itemCount); owner_.addSystemChatMessage(msg); - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playPickupBag(); } } @@ -766,8 +767,8 @@ void InventoryHandler::handleLootRemoved(network::Packet& packet) { std::string msgStr = "Looted: " + link; if (it->count > 1) msgStr += " x" + std::to_string(it->count); owner_.addSystemChatMessage(msgStr); - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playLootItem(); } currentLoot_.items.erase(it); @@ -2382,8 +2383,8 @@ void InventoryHandler::handleItemQueryResponse(network::Packet& packet) { std::string msg = "Received: " + link; if (it->count > 1) msg += " x" + std::to_string(it->count); owner_.addSystemChatMessage(msg); - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) sfx->playLootItem(); + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playLootItem(); } if (owner_.itemLootCallback_) owner_.itemLootCallback_(data.entry, it->count, data.quality, itemName); it = owner_.pendingItemPushNotifs_.erase(it); @@ -3149,8 +3150,8 @@ void InventoryHandler::handleTrainerBuySucceeded(network::Packet& packet) { owner_.addSystemChatMessage("You have learned " + name + "."); else owner_.addSystemChatMessage("Spell learned."); - if (auto* renderer = owner_.services().renderer) - if (auto* sfx = renderer->getUiSoundManager()) sfx->playQuestActivate(); + if (auto* ac = owner_.services().audioCoordinator) + if (auto* sfx = ac->getUiSoundManager()) sfx->playQuestActivate(); owner_.fireAddonEvent("TRAINER_UPDATE", {}); owner_.fireAddonEvent("SPELLS_CHANGED", {}); } @@ -3171,8 +3172,8 @@ void InventoryHandler::handleTrainerBuyFailed(network::Packet& packet) { else if (errorCode != 0) msg += " (error " + std::to_string(errorCode) + ")"; owner_.addUIError(msg); owner_.addSystemChatMessage(msg); - if (auto* renderer = owner_.services().renderer) - if (auto* sfx = renderer->getUiSoundManager()) sfx->playError(); + if (auto* ac = owner_.services().audioCoordinator) + if (auto* sfx = ac->getUiSoundManager()) sfx->playError(); } // ============================================================ diff --git a/src/game/quest_handler.cpp b/src/game/quest_handler.cpp index a10bc83e..b4a1aad6 100644 --- a/src/game/quest_handler.cpp +++ b/src/game/quest_handler.cpp @@ -6,6 +6,7 @@ #include "game/packet_parsers.hpp" #include "network/world_socket.hpp" #include "rendering/renderer.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/ui_sound_manager.hpp" #include "core/application.hpp" #include "core/logger.hpp" @@ -469,8 +470,8 @@ void QuestHandler::registerOpcodes(DispatchTable& table) { owner_.questCompleteCallback_(questId, it->title); } // Play quest-complete sound - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playQuestComplete(); } questLog_.erase(it); @@ -1095,8 +1096,8 @@ void QuestHandler::acceptQuest() { pendingQuestAcceptNpcGuids_[questId] = npcGuid; // Play quest-accept sound - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playQuestActivate(); } diff --git a/src/game/social_handler.cpp b/src/game/social_handler.cpp index 2e2e15d3..ae9b2716 100644 --- a/src/game/social_handler.cpp +++ b/src/game/social_handler.cpp @@ -5,6 +5,7 @@ #include "game/packet_parsers.hpp" #include "game/update_field_table.hpp" #include "game/opcode_table.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/ui_sound_manager.hpp" #include "network/world_socket.hpp" #include "rendering/renderer.hpp" @@ -1053,8 +1054,8 @@ void SocialHandler::handleDuelRequested(network::Packet& packet) { } pendingDuelRequest_ = true; owner_.addSystemChatMessage(duelChallengerName_ + " challenges you to a duel!"); - if (auto* renderer = owner_.services().renderer) - if (auto* sfx = renderer->getUiSoundManager()) sfx->playTargetSelect(); + if (auto* ac = owner_.services().audioCoordinator) + if (auto* sfx = ac->getUiSoundManager()) sfx->playTargetSelect(); if (owner_.addonEventCallback_) owner_.addonEventCallback_("DUEL_REQUESTED", {duelChallengerName_}); } @@ -1219,8 +1220,8 @@ void SocialHandler::handleGroupInvite(network::Packet& packet) { pendingInviterName = data.inviterName; if (!data.inviterName.empty()) owner_.addSystemChatMessage(data.inviterName + " has invited you to a group."); - if (auto* renderer = owner_.services().renderer) - if (auto* sfx = renderer->getUiSoundManager()) sfx->playTargetSelect(); + if (auto* ac = owner_.services().audioCoordinator) + if (auto* sfx = ac->getUiSoundManager()) sfx->playTargetSelect(); if (owner_.addonEventCallback_) owner_.addonEventCallback_("PARTY_INVITE_REQUEST", {data.inviterName}); } diff --git a/src/game/spell_handler.cpp b/src/game/spell_handler.cpp index 3fc626de..59e0d29f 100644 --- a/src/game/spell_handler.cpp +++ b/src/game/spell_handler.cpp @@ -4,6 +4,7 @@ #include "game/packet_parsers.hpp" #include "game/entity.hpp" #include "rendering/renderer.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/spell_sound_manager.hpp" #include "audio/combat_sound_manager.hpp" #include "core/application.hpp" @@ -795,8 +796,8 @@ void SpellHandler::handleCastFailed(network::Packet& packet) { queuedSpellTarget_ = 0; // Stop precast sound - if (auto* renderer = owner_.services().renderer) { - if (auto* ssm = renderer->getSpellSoundManager()) { + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* ssm = ac->getSpellSoundManager()) { ssm->stopPrecast(); } } @@ -817,8 +818,8 @@ void SpellHandler::handleCastFailed(network::Packet& packet) { msg.message = errMsg; owner_.addLocalChatMessage(msg); - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playError(); } @@ -869,8 +870,8 @@ void SpellHandler::handleSpellStart(network::Packet& packet) { // Play precast sound — skip profession/tradeskill spells if (!owner_.isProfessionSpell(data.spellId)) { - if (auto* renderer = owner_.services().renderer) { - if (auto* ssm = renderer->getSpellSoundManager()) { + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* ssm = ac->getSpellSoundManager()) { owner_.loadSpellNameCache(); auto it = owner_.spellNameCache_.find(data.spellId); auto school = (it != owner_.spellNameCache_.end() && it->second.schoolMask) @@ -907,8 +908,8 @@ void SpellHandler::handleSpellGo(network::Packet& packet) { if (data.casterUnit == owner_.playerGuid) { // Play cast-complete sound if (!owner_.isProfessionSpell(data.spellId)) { - if (auto* renderer = owner_.services().renderer) { - if (auto* ssm = renderer->getSpellSoundManager()) { + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* ssm = ac->getSpellSoundManager()) { owner_.loadSpellNameCache(); auto it = owner_.spellNameCache_.find(data.spellId); auto school = (it != owner_.spellNameCache_.end() && it->second.schoolMask) @@ -931,8 +932,8 @@ void SpellHandler::handleSpellGo(network::Packet& packet) { } if (isMeleeAbility) { if (owner_.meleeSwingCallback_) owner_.meleeSwingCallback_(); - if (auto* renderer = owner_.services().renderer) { - if (auto* csm = renderer->getCombatSoundManager()) { + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* csm = ac->getCombatSoundManager()) { csm->playWeaponSwing(audio::CombatSoundManager::WeaponSize::MEDIUM, false); csm->playImpact(audio::CombatSoundManager::WeaponSize::MEDIUM, audio::CombatSoundManager::ImpactType::FLESH, false); @@ -990,8 +991,8 @@ void SpellHandler::handleSpellGo(network::Packet& packet) { if (tgt == owner_.playerGuid) { targetsPlayer = true; break; } } if (targetsPlayer) { - if (auto* renderer = owner_.services().renderer) { - if (auto* ssm = renderer->getSpellSoundManager()) { + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* ssm = ac->getSpellSoundManager()) { owner_.loadSpellNameCache(); auto it = owner_.spellNameCache_.find(data.spellId); auto school = (it != owner_.spellNameCache_.end() && it->second.schoolMask) @@ -1036,8 +1037,8 @@ void SpellHandler::handleSpellGo(network::Packet& packet) { } if (playerIsHit || playerHitEnemy) { - if (auto* renderer = owner_.services().renderer) { - if (auto* ssm = renderer->getSpellSoundManager()) { + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* ssm = ac->getSpellSoundManager()) { owner_.loadSpellNameCache(); auto it = owner_.spellNameCache_.find(data.spellId); auto school = (it != owner_.spellNameCache_.end() && it->second.schoolMask) @@ -1396,8 +1397,8 @@ void SpellHandler::handleAchievementEarned(network::Packet& packet) { owner_.earnedAchievements_.insert(achievementId); owner_.achievementDates_[achievementId] = earnDate; - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playAchievementAlert(); } if (owner_.achievementEarnedCallback_) { @@ -2350,8 +2351,8 @@ void SpellHandler::handleSpellFailure(network::Packet& packet) { craftQueueRemaining_ = 0; queuedSpellId_ = 0; queuedSpellTarget_ = 0; - if (auto* renderer = owner_.services().renderer) { - if (auto* ssm = renderer->getSpellSoundManager()) { + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* ssm = ac->getSpellSoundManager()) { ssm->stopPrecast(); } } diff --git a/src/rendering/animation_controller.cpp b/src/rendering/animation_controller.cpp new file mode 100644 index 00000000..7b46614e --- /dev/null +++ b/src/rendering/animation_controller.cpp @@ -0,0 +1,1703 @@ +#include "rendering/animation_controller.hpp" +#include "rendering/renderer.hpp" +#include "rendering/camera.hpp" +#include "rendering/camera_controller.hpp" +#include "rendering/character_renderer.hpp" +#include "rendering/terrain_manager.hpp" +#include "rendering/wmo_renderer.hpp" +#include "rendering/water_renderer.hpp" +#include "rendering/m2_renderer.hpp" +#include "rendering/levelup_effect.hpp" +#include "rendering/charge_effect.hpp" +#include "rendering/spell_visual_system.hpp" +#include "pipeline/m2_loader.hpp" +#include "pipeline/asset_manager.hpp" +#include "pipeline/dbc_loader.hpp" +#include "pipeline/dbc_layout.hpp" +#include "core/application.hpp" +#include "core/logger.hpp" +#include "audio/audio_coordinator.hpp" +#include "audio/audio_engine.hpp" +#include "audio/footstep_manager.hpp" +#include "audio/activity_sound_manager.hpp" +#include "audio/mount_sound_manager.hpp" +#include "audio/music_manager.hpp" +#include "audio/movement_sound_manager.hpp" +#include "rendering/swim_effects.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace rendering { + +// ── Static emote data (shared across all AnimationController instances) ────── + +struct EmoteInfo { + uint32_t animId = 0; + uint32_t dbcId = 0; + bool loop = false; + std::string textNoTarget; + std::string textTarget; + std::string othersNoTarget; + std::string othersTarget; + std::string command; +}; + +static std::unordered_map EMOTE_TABLE; +static std::unordered_map EMOTE_BY_DBCID; +static bool emoteTableLoaded = false; + +static std::vector parseEmoteCommands(const std::string& raw) { + std::vector out; + std::string cur; + for (char c : raw) { + if (std::isalnum(static_cast(c)) || c == '_') { + cur.push_back(static_cast(std::tolower(static_cast(c)))); + } else if (!cur.empty()) { + out.push_back(cur); + cur.clear(); + } + } + if (!cur.empty()) out.push_back(cur); + return out; +} + +static bool isLoopingEmote(const std::string& command) { + static const std::unordered_set kLooping = { + "dance", + "train", + }; + return kLooping.find(command) != kLooping.end(); +} + +static void loadFallbackEmotes() { + if (!EMOTE_TABLE.empty()) return; + EMOTE_TABLE = { + {"wave", {67, 0, false, "You wave.", "You wave at %s.", "%s waves.", "%s waves at %s.", "wave"}}, + {"bow", {66, 0, false, "You bow down graciously.", "You bow down before %s.", "%s bows down graciously.", "%s bows down before %s.", "bow"}}, + {"laugh", {70, 0, false, "You laugh.", "You laugh at %s.", "%s laughs.", "%s laughs at %s.", "laugh"}}, + {"point", {84, 0, false, "You point over yonder.", "You point at %s.", "%s points over yonder.", "%s points at %s.", "point"}}, + {"cheer", {68, 0, false, "You cheer!", "You cheer at %s.", "%s cheers!", "%s cheers at %s.", "cheer"}}, + {"dance", {69, 0, true, "You burst into dance.", "You dance with %s.", "%s bursts into dance.", "%s dances with %s.", "dance"}}, + {"kneel", {75, 0, false, "You kneel down.", "You kneel before %s.", "%s kneels down.", "%s kneels before %s.", "kneel"}}, + {"applaud", {80, 0, false, "You applaud. Bravo!", "You applaud at %s. Bravo!", "%s applauds. Bravo!", "%s applauds at %s. Bravo!", "applaud"}}, + {"shout", {81, 0, false, "You shout.", "You shout at %s.", "%s shouts.", "%s shouts at %s.", "shout"}}, + {"chicken", {78, 0, false, "With arms flapping, you strut around. Cluck, Cluck, Chicken!", + "With arms flapping, you strut around %s. Cluck, Cluck, Chicken!", + "%s struts around. Cluck, Cluck, Chicken!", "%s struts around %s. Cluck, Cluck, Chicken!", "chicken"}}, + {"cry", {77, 0, false, "You cry.", "You cry on %s's shoulder.", "%s cries.", "%s cries on %s's shoulder.", "cry"}}, + {"kiss", {76, 0, false, "You blow a kiss into the wind.", "You blow a kiss to %s.", "%s blows a kiss into the wind.", "%s blows a kiss to %s.", "kiss"}}, + {"roar", {74, 0, false, "You roar with bestial vigor. So fierce!", "You roar with bestial vigor at %s. So fierce!", "%s roars with bestial vigor. So fierce!", "%s roars with bestial vigor at %s. So fierce!", "roar"}}, + {"salute", {113, 0, false, "You salute.", "You salute %s with respect.", "%s salutes.", "%s salutes %s with respect.", "salute"}}, + {"rude", {73, 0, false, "You make a rude gesture.", "You make a rude gesture at %s.", "%s makes a rude gesture.", "%s makes a rude gesture at %s.", "rude"}}, + {"flex", {82, 0, false, "You flex your muscles. Oooooh so strong!", "You flex at %s. Oooooh so strong!", "%s flexes. Oooooh so strong!", "%s flexes at %s. Oooooh so strong!", "flex"}}, + {"shy", {83, 0, false, "You smile shyly.", "You smile shyly at %s.", "%s smiles shyly.", "%s smiles shyly at %s.", "shy"}}, + {"beg", {79, 0, false, "You beg everyone around you. How pathetic.", "You beg %s. How pathetic.", "%s begs everyone around. How pathetic.", "%s begs %s. How pathetic.", "beg"}}, + {"eat", {61, 0, false, "You begin to eat.", "You begin to eat in front of %s.", "%s begins to eat.", "%s begins to eat in front of %s.", "eat"}}, + }; +} + +static std::string replacePlaceholders(const std::string& text, const std::string* targetName) { + if (text.empty()) return text; + std::string out; + out.reserve(text.size() + 16); + for (size_t i = 0; i < text.size(); ++i) { + if (text[i] == '%' && i + 1 < text.size() && text[i + 1] == 's') { + if (targetName && !targetName->empty()) out += *targetName; + i++; + } else { + out.push_back(text[i]); + } + } + return out; +} + +static void loadEmotesFromDbc() { + if (emoteTableLoaded) return; + emoteTableLoaded = true; + + auto* assetManager = core::Application::getInstance().getAssetManager(); + if (!assetManager) { + LOG_WARNING("Emotes: no AssetManager"); + loadFallbackEmotes(); + return; + } + + auto emotesTextDbc = assetManager->loadDBC("EmotesText.dbc"); + auto emotesTextDataDbc = assetManager->loadDBC("EmotesTextData.dbc"); + if (!emotesTextDbc || !emotesTextDataDbc || !emotesTextDbc->isLoaded() || !emotesTextDataDbc->isLoaded()) { + LOG_WARNING("Emotes: DBCs not available (EmotesText/EmotesTextData)"); + loadFallbackEmotes(); + return; + } + + const auto* activeLayout = pipeline::getActiveDBCLayout(); + const auto* etdL = activeLayout ? activeLayout->getLayout("EmotesTextData") : nullptr; + const auto* emL = activeLayout ? activeLayout->getLayout("Emotes") : nullptr; + const auto* etL = activeLayout ? activeLayout->getLayout("EmotesText") : nullptr; + + std::unordered_map textData; + textData.reserve(emotesTextDataDbc->getRecordCount()); + for (uint32_t r = 0; r < emotesTextDataDbc->getRecordCount(); ++r) { + uint32_t id = emotesTextDataDbc->getUInt32(r, etdL ? (*etdL)["ID"] : 0); + std::string text = emotesTextDataDbc->getString(r, etdL ? (*etdL)["Text"] : 1); + if (!text.empty()) textData.emplace(id, std::move(text)); + } + + std::unordered_map emoteIdToAnim; + if (auto emotesDbc = assetManager->loadDBC("Emotes.dbc"); emotesDbc && emotesDbc->isLoaded()) { + emoteIdToAnim.reserve(emotesDbc->getRecordCount()); + for (uint32_t r = 0; r < emotesDbc->getRecordCount(); ++r) { + uint32_t emoteId = emotesDbc->getUInt32(r, emL ? (*emL)["ID"] : 0); + uint32_t animId = emotesDbc->getUInt32(r, emL ? (*emL)["AnimID"] : 2); + if (animId != 0) emoteIdToAnim[emoteId] = animId; + } + } + + EMOTE_TABLE.clear(); + EMOTE_TABLE.reserve(emotesTextDbc->getRecordCount()); + for (uint32_t r = 0; r < emotesTextDbc->getRecordCount(); ++r) { + uint32_t recordId = emotesTextDbc->getUInt32(r, etL ? (*etL)["ID"] : 0); + std::string cmdRaw = emotesTextDbc->getString(r, etL ? (*etL)["Command"] : 1); + if (cmdRaw.empty()) continue; + + uint32_t emoteRef = emotesTextDbc->getUInt32(r, etL ? (*etL)["EmoteRef"] : 2); + uint32_t animId = 0; + auto animIt = emoteIdToAnim.find(emoteRef); + if (animIt != emoteIdToAnim.end()) { + animId = animIt->second; + } else { + animId = emoteRef; + } + + uint32_t senderTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderTargetTextID"] : 5); + uint32_t senderNoTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderNoTargetTextID"] : 9); + uint32_t othersTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["OthersTargetTextID"] : 3); + uint32_t othersNoTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["OthersNoTargetTextID"] : 7); + + std::string textTarget, textNoTarget, oTarget, oNoTarget; + if (auto it = textData.find(senderTargetTextId); it != textData.end()) textTarget = it->second; + if (auto it = textData.find(senderNoTargetTextId); it != textData.end()) textNoTarget = it->second; + if (auto it = textData.find(othersTargetTextId); it != textData.end()) oTarget = it->second; + if (auto it = textData.find(othersNoTargetTextId); it != textData.end()) oNoTarget = it->second; + + for (const std::string& cmd : parseEmoteCommands(cmdRaw)) { + if (cmd.empty()) continue; + EmoteInfo info; + info.animId = animId; + info.dbcId = recordId; + info.loop = isLoopingEmote(cmd); + info.textNoTarget = textNoTarget; + info.textTarget = textTarget; + info.othersNoTarget = oNoTarget; + info.othersTarget = oTarget; + info.command = cmd; + EMOTE_TABLE.emplace(cmd, std::move(info)); + } + } + + if (EMOTE_TABLE.empty()) { + LOG_WARNING("Emotes: DBC loaded but no commands parsed, using fallback list"); + loadFallbackEmotes(); + } else { + LOG_INFO("Emotes: loaded ", EMOTE_TABLE.size(), " commands from DBC"); + } + + EMOTE_BY_DBCID.clear(); + for (auto& [cmd, info] : EMOTE_TABLE) { + if (info.dbcId != 0) { + EMOTE_BY_DBCID.emplace(info.dbcId, &info); + } + } +} + +// ── AnimationController implementation ─────────────────────────────────────── + +AnimationController::AnimationController() = default; +AnimationController::~AnimationController() = default; + +void AnimationController::initialize(Renderer* renderer) { + renderer_ = renderer; +} + +void AnimationController::onCharacterFollow(uint32_t /*instanceId*/) { + // Reset animation state when follow target changes +} + +// ── Emote support ──────────────────────────────────────────────────────────── + +void AnimationController::playEmote(const std::string& emoteName) { + loadEmotesFromDbc(); + auto it = EMOTE_TABLE.find(emoteName); + if (it == EMOTE_TABLE.end()) return; + + const auto& info = it->second; + if (info.animId == 0) return; + emoteActive_ = true; + emoteAnimId_ = info.animId; + emoteLoop_ = info.loop; + charAnimState_ = CharAnimState::EMOTE; + + auto* characterRenderer = renderer_->getCharacterRenderer(); + uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); + if (characterRenderer && characterInstanceId > 0) { + characterRenderer->playAnimation(characterInstanceId, emoteAnimId_, emoteLoop_); + } +} + +void AnimationController::cancelEmote() { + emoteActive_ = false; + emoteAnimId_ = 0; + emoteLoop_ = false; +} + +std::string AnimationController::getEmoteText(const std::string& emoteName, const std::string* targetName) { + loadEmotesFromDbc(); + auto it = EMOTE_TABLE.find(emoteName); + if (it != EMOTE_TABLE.end()) { + const auto& info = it->second; + const std::string& base = (targetName ? info.textTarget : info.textNoTarget); + if (!base.empty()) { + return replacePlaceholders(base, targetName); + } + if (targetName && !targetName->empty()) { + return "You " + info.command + " at " + *targetName + "."; + } + return "You " + info.command + "."; + } + return ""; +} + +uint32_t AnimationController::getEmoteDbcId(const std::string& emoteName) { + loadEmotesFromDbc(); + auto it = EMOTE_TABLE.find(emoteName); + if (it != EMOTE_TABLE.end()) { + return it->second.dbcId; + } + return 0; +} + +std::string AnimationController::getEmoteTextByDbcId(uint32_t dbcId, const std::string& senderName, + const std::string* targetName) { + loadEmotesFromDbc(); + auto it = EMOTE_BY_DBCID.find(dbcId); + if (it == EMOTE_BY_DBCID.end()) return ""; + + const EmoteInfo& info = *it->second; + + if (targetName && !targetName->empty()) { + if (!info.othersTarget.empty()) { + std::string out; + out.reserve(info.othersTarget.size() + senderName.size() + targetName->size()); + bool firstReplaced = false; + for (size_t i = 0; i < info.othersTarget.size(); ++i) { + if (info.othersTarget[i] == '%' && i + 1 < info.othersTarget.size() && info.othersTarget[i + 1] == 's') { + out += firstReplaced ? *targetName : senderName; + firstReplaced = true; + ++i; + } else { + out.push_back(info.othersTarget[i]); + } + } + return out; + } + return senderName + " " + info.command + "s at " + *targetName + "."; + } else { + if (!info.othersNoTarget.empty()) { + return replacePlaceholders(info.othersNoTarget, &senderName); + } + return senderName + " " + info.command + "s."; + } +} + +uint32_t AnimationController::getEmoteAnimByDbcId(uint32_t dbcId) { + loadEmotesFromDbc(); + auto it = EMOTE_BY_DBCID.find(dbcId); + if (it != EMOTE_BY_DBCID.end()) { + return it->second->animId; + } + return 0; +} + +// ── Targeting / combat ─────────────────────────────────────────────────────── + +void AnimationController::setTargetPosition(const glm::vec3* pos) { + targetPosition_ = pos; +} + +void AnimationController::resetCombatVisualState() { + inCombat_ = false; + targetPosition_ = nullptr; + meleeSwingTimer_ = 0.0f; + meleeSwingCooldown_ = 0.0f; + if (auto* svs = renderer_->getSpellVisualSystem()) svs->reset(); +} + +bool AnimationController::isMoving() const { + auto* cameraController = renderer_->getCameraController(); + return cameraController && cameraController->isMoving(); +} + +// ── Melee combat ───────────────────────────────────────────────────────────── + +void AnimationController::triggerMeleeSwing() { + auto* characterRenderer = renderer_->getCharacterRenderer(); + uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); + if (!characterRenderer || characterInstanceId == 0) return; + if (meleeSwingCooldown_ > 0.0f) return; + if (emoteActive_) { + cancelEmote(); + } + resolveMeleeAnimId(); + meleeSwingCooldown_ = 0.1f; + float durationSec = meleeAnimDurationMs_ > 0.0f ? meleeAnimDurationMs_ / 1000.0f : 0.6f; + if (durationSec < 0.25f) durationSec = 0.25f; + if (durationSec > 1.0f) durationSec = 1.0f; + meleeSwingTimer_ = durationSec; + if (renderer_->getAudioCoordinator()->getActivitySoundManager()) { + renderer_->getAudioCoordinator()->getActivitySoundManager()->playMeleeSwing(); + } +} + +uint32_t AnimationController::resolveMeleeAnimId() { + auto* characterRenderer = renderer_->getCharacterRenderer(); + uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); + if (!characterRenderer || characterInstanceId == 0) { + meleeAnimId_ = 0; + meleeAnimDurationMs_ = 0.0f; + return 0; + } + + if (meleeAnimId_ != 0 && characterRenderer->hasAnimation(characterInstanceId, meleeAnimId_)) { + return meleeAnimId_; + } + + std::vector sequences; + if (!characterRenderer->getAnimationSequences(characterInstanceId, sequences)) { + meleeAnimId_ = 0; + meleeAnimDurationMs_ = 0.0f; + return 0; + } + + auto findDuration = [&](uint32_t id) -> float { + for (const auto& seq : sequences) { + if (seq.id == id && seq.duration > 0) { + return static_cast(seq.duration); + } + } + return 0.0f; + }; + + const uint32_t* attackCandidates; + size_t candidateCount; + static const uint32_t candidates2H[] = {18, 17, 16, 19, 20, 21}; + static const uint32_t candidates1H[] = {17, 18, 16, 19, 20, 21}; + static const uint32_t candidatesUnarmed[] = {16, 17, 18, 19, 20, 21}; + if (equippedWeaponInvType_ == 17) { + attackCandidates = candidates2H; + candidateCount = 6; + } else if (equippedWeaponInvType_ == 0) { + attackCandidates = candidatesUnarmed; + candidateCount = 6; + } else { + attackCandidates = candidates1H; + candidateCount = 6; + } + for (size_t ci = 0; ci < candidateCount; ci++) { + uint32_t id = attackCandidates[ci]; + if (characterRenderer->hasAnimation(characterInstanceId, id)) { + meleeAnimId_ = id; + meleeAnimDurationMs_ = findDuration(id); + return meleeAnimId_; + } + } + + const uint32_t avoidIds[] = {0, 1, 4, 5, 11, 12, 13, 37, 38, 39, 41, 42, 97}; + auto isAvoid = [&](uint32_t id) -> bool { + for (uint32_t avoid : avoidIds) { + if (id == avoid) return true; + } + return false; + }; + + uint32_t bestId = 0; + uint32_t bestDuration = 0; + for (const auto& seq : sequences) { + if (seq.duration == 0) continue; + if (isAvoid(seq.id)) continue; + if (seq.movingSpeed > 0.1f) continue; + if (seq.duration < 150 || seq.duration > 2000) continue; + if (bestId == 0 || seq.duration < bestDuration) { + bestId = seq.id; + bestDuration = seq.duration; + } + } + + if (bestId == 0) { + for (const auto& seq : sequences) { + if (seq.duration == 0) continue; + if (isAvoid(seq.id)) continue; + if (bestId == 0 || seq.duration < bestDuration) { + bestId = seq.id; + bestDuration = seq.duration; + } + } + } + + meleeAnimId_ = bestId; + meleeAnimDurationMs_ = static_cast(bestDuration); + return meleeAnimId_; +} + +// ── Effect triggers ────────────────────────────────────────────────────────── + +void AnimationController::triggerLevelUpEffect(const glm::vec3& position) { + auto* levelUpEffect = renderer_->getLevelUpEffect(); + if (!levelUpEffect) return; + + if (!levelUpEffect->isModelLoaded()) { + auto* m2Renderer = renderer_->getM2Renderer(); + if (m2Renderer) { + auto* assetManager = core::Application::getInstance().getAssetManager(); + if (!assetManager) { + LOG_WARNING("LevelUpEffect: no asset manager available"); + } else { + auto m2Data = assetManager->readFile("Spells\\LevelUp\\LevelUp.m2"); + auto skinData = assetManager->readFile("Spells\\LevelUp\\LevelUp00.skin"); + LOG_INFO("LevelUpEffect: m2Data=", m2Data.size(), " skinData=", skinData.size()); + if (!m2Data.empty()) { + levelUpEffect->loadModel(m2Renderer, m2Data, skinData); + } else { + LOG_WARNING("LevelUpEffect: failed to read Spell\\LevelUp\\LevelUp.m2"); + } + } + } + } + + levelUpEffect->trigger(position); +} + +void AnimationController::startChargeEffect(const glm::vec3& position, const glm::vec3& direction) { + auto* chargeEffect = renderer_->getChargeEffect(); + if (!chargeEffect) return; + + if (!chargeEffect->isActive()) { + auto* m2Renderer = renderer_->getM2Renderer(); + if (m2Renderer) { + auto* assetManager = core::Application::getInstance().getAssetManager(); + if (assetManager) { + chargeEffect->tryLoadM2Models(m2Renderer, assetManager); + } + } + } + + chargeEffect->start(position, direction); +} + +void AnimationController::emitChargeEffect(const glm::vec3& position, const glm::vec3& direction) { + if (auto* chargeEffect = renderer_->getChargeEffect()) { + chargeEffect->emit(position, direction); + } +} + +void AnimationController::stopChargeEffect() { + if (auto* chargeEffect = renderer_->getChargeEffect()) { + chargeEffect->stop(); + } +} + +// ── Mount ──────────────────────────────────────────────────────────────────── + +void AnimationController::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset, const std::string& modelPath) { + auto* characterRenderer = renderer_->getCharacterRenderer(); + auto* cameraController = renderer_->getCameraController(); + + mountInstanceId_ = mountInstId; + mountHeightOffset_ = heightOffset; + mountSeatAttachmentId_ = -1; + smoothedMountSeatPos_ = renderer_->getCharacterPosition(); + mountSeatSmoothingInit_ = false; + mountAction_ = MountAction::None; + mountActionPhase_ = 0; + charAnimState_ = CharAnimState::MOUNT; + if (cameraController) { + cameraController->setMounted(true); + cameraController->setMountHeightOffset(heightOffset); + } + + if (characterRenderer && mountInstId > 0) { + characterRenderer->dumpAnimations(mountInstId); + } + + // Discover mount animation capabilities (property-based, not hardcoded IDs) + LOG_DEBUG("=== Mount Animation Dump (Display ID ", mountDisplayId, ") ==="); + if (characterRenderer) characterRenderer->dumpAnimations(mountInstId); + + std::vector sequences; + if (!characterRenderer || !characterRenderer->getAnimationSequences(mountInstId, sequences)) { + LOG_WARNING("Failed to get animation sequences for mount, using fallback IDs"); + sequences.clear(); + } + + auto findFirst = [&](std::initializer_list candidates) -> uint32_t { + for (uint32_t id : candidates) { + if (characterRenderer && characterRenderer->hasAnimation(mountInstId, id)) { + return id; + } + } + return 0; + }; + + // Property-based jump animation discovery with chain-based scoring + auto discoverJumpSet = [&]() { + LOG_DEBUG("=== Full sequence table for mount ==="); + for (const auto& seq : sequences) { + LOG_DEBUG("SEQ id=", seq.id, + " dur=", seq.duration, + " flags=0x", std::hex, seq.flags, std::dec, + " moveSpd=", seq.movingSpeed, + " blend=", seq.blendTime, + " next=", seq.nextAnimation, + " alias=", seq.aliasNext); + } + LOG_DEBUG("=== End sequence table ==="); + + std::set forbiddenIds = {53, 54, 16}; + + auto scoreNear = [](int a, int b) -> int { + int d = std::abs(a - b); + return (d <= 8) ? (20 - d) : 0; + }; + + auto isForbidden = [&](uint32_t id) { + return forbiddenIds.count(id) != 0; + }; + + auto findSeqById = [&](uint32_t id) -> const pipeline::M2Sequence* { + for (const auto& s : sequences) { + if (s.id == id) return &s; + } + return nullptr; + }; + + uint32_t runId = findFirst({5, 4}); + uint32_t standId = findFirst({0}); + + std::vector loops; + for (const auto& seq : sequences) { + if (isForbidden(seq.id)) continue; + bool isLoop = (seq.flags & 0x01) == 0; + if (isLoop && seq.duration >= 350 && seq.duration <= 1000 && + seq.id != runId && seq.id != standId) { + loops.push_back(seq.id); + } + } + + uint32_t loop = 0; + if (!loops.empty()) { + uint32_t best = loops[0]; + int bestScore = -999; + for (uint32_t id : loops) { + int sc = 0; + sc += scoreNear(static_cast(id), 38); + const auto* s = findSeqById(id); + if (s) sc += (s->duration >= 500 && s->duration <= 800) ? 5 : 0; + if (sc > bestScore) { + bestScore = sc; + best = id; + } + } + loop = best; + } + + uint32_t start = 0, end = 0; + int bestStart = -999, bestEnd = -999; + + for (const auto& seq : sequences) { + if (isForbidden(seq.id)) continue; + bool isLoop = (seq.flags & 0x01) == 0; + if (isLoop) continue; + + if (seq.duration >= 450 && seq.duration <= 1100) { + int sc = 0; + if (loop) sc += scoreNear(static_cast(seq.id), static_cast(loop)); + if (loop && (seq.nextAnimation == static_cast(loop) || seq.aliasNext == loop)) sc += 30; + if (loop && scoreNear(seq.nextAnimation, static_cast(loop)) > 0) sc += 10; + if (seq.blendTime > 400) sc -= 5; + + if (sc > bestStart) { + bestStart = sc; + start = seq.id; + } + } + + if (seq.duration >= 650 && seq.duration <= 1600) { + int sc = 0; + if (loop) sc += scoreNear(static_cast(seq.id), static_cast(loop)); + if (seq.nextAnimation == static_cast(runId) || seq.nextAnimation == static_cast(standId)) sc += 10; + if (seq.nextAnimation < 0) sc += 5; + if (sc > bestEnd) { + bestEnd = sc; + end = seq.id; + } + } + } + + LOG_DEBUG("Property-based jump discovery: start=", start, " loop=", loop, " end=", end, + " scores: start=", bestStart, " end=", bestEnd); + return std::make_tuple(start, loop, end); + }; + + auto [discoveredStart, discoveredLoop, discoveredEnd] = discoverJumpSet(); + + mountAnims_.jumpStart = discoveredStart > 0 ? discoveredStart : findFirst({40, 37}); + mountAnims_.jumpLoop = discoveredLoop > 0 ? discoveredLoop : findFirst({38}); + mountAnims_.jumpEnd = discoveredEnd > 0 ? discoveredEnd : findFirst({39}); + mountAnims_.rearUp = findFirst({94, 92, 40}); + mountAnims_.run = findFirst({5, 4}); + mountAnims_.stand = findFirst({0}); + + // Discover idle fidget animations using proper WoW M2 metadata + mountAnims_.fidgets.clear(); + core::Logger::getInstance().debug("Scanning for fidget animations in ", sequences.size(), " sequences"); + + core::Logger::getInstance().debug("=== ALL potential fidgets (no metadata filter) ==="); + for (const auto& seq : sequences) { + bool isLoop = (seq.flags & 0x01) == 0; + bool isStationary = std::abs(seq.movingSpeed) < 0.05f; + bool reasonableDuration = seq.duration >= 400 && seq.duration <= 2500; + + if (!isLoop && reasonableDuration && isStationary) { + core::Logger::getInstance().debug(" ALL: id=", seq.id, + " dur=", seq.duration, "ms", + " freq=", seq.frequency, + " replay=", seq.replayMin, "-", seq.replayMax, + " flags=0x", std::hex, seq.flags, std::dec, + " next=", seq.nextAnimation); + } + } + + for (const auto& seq : sequences) { + bool isLoop = (seq.flags & 0x01) == 0; + bool hasFrequency = seq.frequency > 0; + bool hasReplay = seq.replayMax > 0; + bool isStationary = std::abs(seq.movingSpeed) < 0.05f; + bool reasonableDuration = seq.duration >= 400 && seq.duration <= 2500; + + if (!isLoop && reasonableDuration && isStationary && (hasFrequency || hasReplay)) { + core::Logger::getInstance().debug(" Candidate: id=", seq.id, + " dur=", seq.duration, "ms", + " freq=", seq.frequency, + " replay=", seq.replayMin, "-", seq.replayMax, + " next=", seq.nextAnimation, + " speed=", seq.movingSpeed); + } + + bool isDeathOrWound = (seq.id >= 5 && seq.id <= 9); + bool isAttackOrCombat = (seq.id >= 11 && seq.id <= 21); + bool isSpecial = (seq.id == 2 || seq.id == 3); + + if (!isLoop && (hasFrequency || hasReplay) && isStationary && reasonableDuration && + !isDeathOrWound && !isAttackOrCombat && !isSpecial) { + bool chainsToStand = (seq.nextAnimation == static_cast(mountAnims_.stand)) || + (seq.aliasNext == mountAnims_.stand) || + (seq.nextAnimation == -1); + + mountAnims_.fidgets.push_back(seq.id); + core::Logger::getInstance().debug(" >> Selected fidget: id=", seq.id, + (chainsToStand ? " (chains to stand)" : "")); + } + } + + if (mountAnims_.run == 0) mountAnims_.run = mountAnims_.stand; + + core::Logger::getInstance().debug("Mount animation set: jumpStart=", mountAnims_.jumpStart, + " jumpLoop=", mountAnims_.jumpLoop, + " jumpEnd=", mountAnims_.jumpEnd, + " rearUp=", mountAnims_.rearUp, + " run=", mountAnims_.run, + " stand=", mountAnims_.stand, + " fidgets=", mountAnims_.fidgets.size()); + + if (renderer_->getAudioCoordinator()->getMountSoundManager()) { + bool isFlying = taxiFlight_; + renderer_->getAudioCoordinator()->getMountSoundManager()->onMount(mountDisplayId, isFlying, modelPath); + } +} + +void AnimationController::clearMount() { + mountInstanceId_ = 0; + mountHeightOffset_ = 0.0f; + mountPitch_ = 0.0f; + mountRoll_ = 0.0f; + mountSeatAttachmentId_ = -1; + smoothedMountSeatPos_ = glm::vec3(0.0f); + mountSeatSmoothingInit_ = false; + mountAction_ = MountAction::None; + mountActionPhase_ = 0; + charAnimState_ = CharAnimState::IDLE; + if (auto* cameraController = renderer_->getCameraController()) { + cameraController->setMounted(false); + cameraController->setMountHeightOffset(0.0f); + } + + if (renderer_->getAudioCoordinator()->getMountSoundManager()) { + renderer_->getAudioCoordinator()->getMountSoundManager()->onDismount(); + } +} + +// ── Query helpers ──────────────────────────────────────────────────────────── + +bool AnimationController::isFootstepAnimationState() const { + return charAnimState_ == CharAnimState::WALK || charAnimState_ == CharAnimState::RUN; +} + +// ── Melee timers ───────────────────────────────────────────────────────────── + +void AnimationController::updateMeleeTimers(float deltaTime) { + if (meleeSwingCooldown_ > 0.0f) { + meleeSwingCooldown_ = std::max(0.0f, meleeSwingCooldown_ - deltaTime); + } + if (meleeSwingTimer_ > 0.0f) { + meleeSwingTimer_ = std::max(0.0f, meleeSwingTimer_ - deltaTime); + } +} + +// ── Character animation state machine ──────────────────────────────────────── + +void AnimationController::updateCharacterAnimation() { + auto* characterRenderer = renderer_->getCharacterRenderer(); + auto* cameraController = renderer_->getCameraController(); + uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); + + // WoW WotLK AnimationData.dbc IDs + constexpr uint32_t ANIM_STAND = 0; + constexpr uint32_t ANIM_WALK = 4; + constexpr uint32_t ANIM_RUN = 5; + constexpr uint32_t ANIM_STRAFE_RUN_RIGHT = 92; + constexpr uint32_t ANIM_STRAFE_RUN_LEFT = 93; + constexpr uint32_t ANIM_STRAFE_WALK_LEFT = 11; + constexpr uint32_t ANIM_STRAFE_WALK_RIGHT = 12; + constexpr uint32_t ANIM_BACKPEDAL = 13; + constexpr uint32_t ANIM_JUMP_START = 37; + constexpr uint32_t ANIM_JUMP_MID = 38; + constexpr uint32_t ANIM_JUMP_END = 39; + constexpr uint32_t ANIM_SIT_DOWN = 97; + constexpr uint32_t ANIM_SITTING = 97; + constexpr uint32_t ANIM_SWIM_IDLE = 41; + constexpr uint32_t ANIM_SWIM = 42; + constexpr uint32_t ANIM_MOUNT = 91; + constexpr uint32_t ANIM_READY_UNARMED = 22; + constexpr uint32_t ANIM_READY_1H = 23; + constexpr uint32_t ANIM_READY_2H = 24; + constexpr uint32_t ANIM_READY_2H_L = 25; + constexpr uint32_t ANIM_FLY_IDLE = 158; + constexpr uint32_t ANIM_FLY_FORWARD = 159; + + CharAnimState newState = charAnimState_; + + const bool rawMoving = cameraController->isMoving(); + const bool rawSprinting = cameraController->isSprinting(); + constexpr float kLocomotionStopGraceSec = 0.12f; + if (rawMoving) { + locomotionStopGraceTimer_ = kLocomotionStopGraceSec; + locomotionWasSprinting_ = rawSprinting; + } else { + locomotionStopGraceTimer_ = std::max(0.0f, locomotionStopGraceTimer_ - lastDeltaTime_); + } + bool moving = rawMoving || locomotionStopGraceTimer_ > 0.0f; + bool movingForward = cameraController->isMovingForward(); + bool movingBackward = cameraController->isMovingBackward(); + bool autoRunning = cameraController->isAutoRunning(); + bool strafeLeft = cameraController->isStrafingLeft(); + bool strafeRight = cameraController->isStrafingRight(); + bool pureStrafe = !movingForward && !movingBackward && !autoRunning; + bool anyStrafeLeft = strafeLeft && !strafeRight && pureStrafe; + bool anyStrafeRight = strafeRight && !strafeLeft && pureStrafe; + bool grounded = cameraController->isGrounded(); + bool jumping = cameraController->isJumping(); + bool sprinting = rawSprinting || (!rawMoving && moving && locomotionWasSprinting_); + bool sitting = cameraController->isSitting(); + bool swim = cameraController->isSwimming(); + bool forceMelee = meleeSwingTimer_ > 0.0f && grounded && !swim; + + const glm::vec3& characterPosition = renderer_->getCharacterPosition(); + float characterYaw = renderer_->getCharacterYaw(); + + // When mounted, force MOUNT state and skip normal transitions + if (isMounted()) { + newState = CharAnimState::MOUNT; + charAnimState_ = newState; + + uint32_t currentAnimId = 0; + float currentAnimTimeMs = 0.0f, currentAnimDurationMs = 0.0f; + bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs); + if (!haveState || currentAnimId != ANIM_MOUNT) { + characterRenderer->playAnimation(characterInstanceId, ANIM_MOUNT, true); + } + + float mountBob = 0.0f; + float mountYawRad = glm::radians(characterYaw); + if (mountInstanceId_ > 0) { + characterRenderer->setInstancePosition(mountInstanceId_, characterPosition); + + if (!taxiFlight_ && moving && lastDeltaTime_ > 0.0f) { + float currentYawDeg = characterYaw; + float turnRate = (currentYawDeg - prevMountYaw_) / lastDeltaTime_; + while (turnRate > 180.0f) turnRate -= 360.0f; + while (turnRate < -180.0f) turnRate += 360.0f; + + float targetLean = glm::clamp(turnRate * 0.15f, -0.25f, 0.25f); + mountRoll_ = glm::mix(mountRoll_, targetLean, lastDeltaTime_ * 6.0f); + prevMountYaw_ = currentYawDeg; + } else { + mountRoll_ = glm::mix(mountRoll_, 0.0f, lastDeltaTime_ * 8.0f); + } + + characterRenderer->setInstanceRotation(mountInstanceId_, glm::vec3(mountPitch_, mountRoll_, mountYawRad)); + + auto pickMountAnim = [&](std::initializer_list candidates, uint32_t fallback) -> uint32_t { + for (uint32_t id : candidates) { + if (characterRenderer->hasAnimation(mountInstanceId_, id)) { + return id; + } + } + return fallback; + }; + + uint32_t mountAnimId = ANIM_STAND; + + uint32_t curMountAnim = 0; + float curMountTime = 0, curMountDur = 0; + bool haveMountState = characterRenderer->getAnimationState(mountInstanceId_, curMountAnim, curMountTime, curMountDur); + + if (taxiFlight_) { + if (!taxiAnimsLogged_) { + taxiAnimsLogged_ = true; + LOG_INFO("Taxi flight active: mountInstanceId_=", mountInstanceId_, + " curMountAnim=", curMountAnim, " haveMountState=", haveMountState); + std::vector seqs; + if (characterRenderer->getAnimationSequences(mountInstanceId_, seqs)) { + std::string animList; + for (const auto& s : seqs) { + if (!animList.empty()) animList += ", "; + animList += std::to_string(s.id); + } + LOG_INFO("Taxi mount available animations: [", animList, "]"); + } + } + + uint32_t flyAnims[] = {ANIM_FLY_FORWARD, ANIM_FLY_IDLE, 234, 229, 233, 141, 369, 6, ANIM_RUN}; + mountAnimId = ANIM_STAND; + for (uint32_t fa : flyAnims) { + if (characterRenderer->hasAnimation(mountInstanceId_, fa)) { + mountAnimId = fa; + break; + } + } + + if (!haveMountState || curMountAnim != mountAnimId) { + LOG_INFO("Taxi mount: playing animation ", mountAnimId); + characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); + } + + goto taxi_mount_done; + } else { + taxiAnimsLogged_ = false; + } + + // Check for jump trigger + if (cameraController->isJumpKeyPressed() && grounded && mountAction_ == MountAction::None) { + if (moving && mountAnims_.jumpLoop > 0) { + LOG_DEBUG("Mount jump triggered while moving: using jumpLoop anim ", mountAnims_.jumpLoop); + characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpLoop, true); + mountAction_ = MountAction::Jump; + mountActionPhase_ = 1; + mountAnimId = mountAnims_.jumpLoop; + if (renderer_->getAudioCoordinator()->getMountSoundManager()) { + renderer_->getAudioCoordinator()->getMountSoundManager()->playJumpSound(); + } + if (cameraController) { + cameraController->triggerMountJump(); + } + } else if (!moving && mountAnims_.rearUp > 0) { + LOG_DEBUG("Mount rear-up triggered: playing rearUp anim ", mountAnims_.rearUp); + characterRenderer->playAnimation(mountInstanceId_, mountAnims_.rearUp, false); + mountAction_ = MountAction::RearUp; + mountActionPhase_ = 0; + mountAnimId = mountAnims_.rearUp; + if (renderer_->getAudioCoordinator()->getMountSoundManager()) { + renderer_->getAudioCoordinator()->getMountSoundManager()->playRearUpSound(); + } + } + } + + // Handle active mount actions (jump chaining or rear-up) + if (mountAction_ != MountAction::None) { + bool animFinished = haveMountState && curMountDur > 0.1f && + (curMountTime >= curMountDur - 0.05f); + + if (mountAction_ == MountAction::Jump) { + if (mountActionPhase_ == 0 && animFinished && mountAnims_.jumpLoop > 0) { + LOG_DEBUG("Mount jump: phase 0→1 (JumpStart→JumpLoop anim ", mountAnims_.jumpLoop, ")"); + characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpLoop, true); + mountActionPhase_ = 1; + mountAnimId = mountAnims_.jumpLoop; + } else if (mountActionPhase_ == 0 && animFinished && mountAnims_.jumpLoop == 0) { + LOG_DEBUG("Mount jump: phase 0→1 (no JumpLoop, holding JumpStart)"); + mountActionPhase_ = 1; + } else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd > 0) { + LOG_DEBUG("Mount jump: phase 1→2 (landed, JumpEnd anim ", mountAnims_.jumpEnd, ")"); + characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpEnd, false); + mountActionPhase_ = 2; + mountAnimId = mountAnims_.jumpEnd; + if (renderer_->getAudioCoordinator()->getMountSoundManager()) { + renderer_->getAudioCoordinator()->getMountSoundManager()->playLandSound(); + } + } else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd == 0) { + LOG_DEBUG("Mount jump: phase 1→done (landed, no JumpEnd, returning to ", + moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand), ")"); + mountAction_ = MountAction::None; + mountAnimId = moving ? mountAnims_.run : mountAnims_.stand; + characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); + } else if (mountActionPhase_ == 2 && animFinished) { + LOG_DEBUG("Mount jump: phase 2→done (JumpEnd finished, returning to ", + moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand), ")"); + mountAction_ = MountAction::None; + mountAnimId = moving ? mountAnims_.run : mountAnims_.stand; + characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); + } else { + mountAnimId = curMountAnim; + } + } else if (mountAction_ == MountAction::RearUp) { + if (animFinished) { + LOG_DEBUG("Mount rear-up: finished, returning to ", + moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand)); + mountAction_ = MountAction::None; + mountAnimId = moving ? mountAnims_.run : mountAnims_.stand; + characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); + } else { + mountAnimId = curMountAnim; + } + } + } else if (moving) { + if (anyStrafeLeft) { + mountAnimId = pickMountAnim({ANIM_STRAFE_RUN_LEFT, ANIM_STRAFE_WALK_LEFT, ANIM_RUN}, ANIM_RUN); + } else if (anyStrafeRight) { + mountAnimId = pickMountAnim({ANIM_STRAFE_RUN_RIGHT, ANIM_STRAFE_WALK_RIGHT, ANIM_RUN}, ANIM_RUN); + } else if (movingBackward) { + mountAnimId = pickMountAnim({ANIM_BACKPEDAL}, ANIM_RUN); + } else { + mountAnimId = ANIM_RUN; + } + } + + // Cancel active fidget immediately if movement starts + if (moving && mountActiveFidget_ != 0) { + mountActiveFidget_ = 0; + characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); + } + + // Check if active fidget has completed + if (!moving && mountActiveFidget_ != 0) { + uint32_t curAnim = 0; + float curTime = 0.0f, curDur = 0.0f; + if (characterRenderer->getAnimationState(mountInstanceId_, curAnim, curTime, curDur)) { + if (curAnim != mountActiveFidget_ || curTime >= curDur * 0.95f) { + mountActiveFidget_ = 0; + LOG_DEBUG("Mount fidget completed"); + } + } + } + + // Idle fidgets + if (!moving && mountAction_ == MountAction::None && mountActiveFidget_ == 0 && !mountAnims_.fidgets.empty()) { + mountIdleFidgetTimer_ += lastDeltaTime_; + static std::mt19937 idleRng(std::random_device{}()); + static float nextFidgetTime = std::uniform_real_distribution(6.0f, 12.0f)(idleRng); + + if (mountIdleFidgetTimer_ >= nextFidgetTime) { + std::uniform_int_distribution dist(0, mountAnims_.fidgets.size() - 1); + uint32_t fidgetAnim = mountAnims_.fidgets[dist(idleRng)]; + + characterRenderer->playAnimation(mountInstanceId_, fidgetAnim, false); + mountActiveFidget_ = fidgetAnim; + mountIdleFidgetTimer_ = 0.0f; + nextFidgetTime = std::uniform_real_distribution(6.0f, 12.0f)(idleRng); + + LOG_DEBUG("Mount idle fidget: playing anim ", fidgetAnim); + } + } + if (moving) { + mountIdleFidgetTimer_ = 0.0f; + } + + // Idle ambient sounds + if (!moving && renderer_->getAudioCoordinator()->getMountSoundManager()) { + mountIdleSoundTimer_ += lastDeltaTime_; + static std::mt19937 soundRng(std::random_device{}()); + static float nextIdleSoundTime = std::uniform_real_distribution(45.0f, 90.0f)(soundRng); + + if (mountIdleSoundTimer_ >= nextIdleSoundTime) { + renderer_->getAudioCoordinator()->getMountSoundManager()->playIdleSound(); + mountIdleSoundTimer_ = 0.0f; + nextIdleSoundTime = std::uniform_real_distribution(45.0f, 90.0f)(soundRng); + } + } else if (moving) { + mountIdleSoundTimer_ = 0.0f; + } + + // Only update animation if changed and not in action or fidget + if (mountAction_ == MountAction::None && mountActiveFidget_ == 0 && (!haveMountState || curMountAnim != mountAnimId)) { + bool loop = true; + characterRenderer->playAnimation(mountInstanceId_, mountAnimId, loop); + } + + taxi_mount_done: + mountBob = 0.0f; + if (moving && haveMountState && curMountDur > 1.0f) { + float wrappedTime = curMountTime; + while (wrappedTime >= curMountDur) { + wrappedTime -= curMountDur; + } + float norm = wrappedTime / curMountDur; + float bobSpeed = taxiFlight_ ? 2.0f : 1.0f; + mountBob = std::sin(norm * 2.0f * 3.14159f * bobSpeed) * 0.12f; + } + } + + // Use mount's attachment point for proper bone-driven rider positioning. + if (taxiFlight_) { + glm::mat4 mountSeatTransform(1.0f); + bool haveSeat = false; + static constexpr uint32_t kTaxiSeatAttachmentId = 0; + if (mountSeatAttachmentId_ == -1) { + mountSeatAttachmentId_ = static_cast(kTaxiSeatAttachmentId); + } + if (mountSeatAttachmentId_ >= 0) { + haveSeat = characterRenderer->getAttachmentTransform( + mountInstanceId_, static_cast(mountSeatAttachmentId_), mountSeatTransform); + } + if (!haveSeat) { + mountSeatAttachmentId_ = -2; + } + + if (haveSeat) { + glm::vec3 targetRiderPos = glm::vec3(mountSeatTransform[3]) + glm::vec3(0.0f, 0.0f, 0.02f); + mountSeatSmoothingInit_ = false; + smoothedMountSeatPos_ = targetRiderPos; + characterRenderer->setInstancePosition(characterInstanceId, targetRiderPos); + } else { + mountSeatSmoothingInit_ = false; + glm::vec3 playerPos = characterPosition + glm::vec3(0.0f, 0.0f, mountHeightOffset_ + 0.10f); + characterRenderer->setInstancePosition(characterInstanceId, playerPos); + } + + float riderPitch = mountPitch_ * 0.35f; + float riderRoll = mountRoll_ * 0.35f; + float mountYawRadVal = glm::radians(characterYaw); + characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(riderPitch, riderRoll, mountYawRadVal)); + return; + } + + // Ground mounts: try a seat attachment first. + glm::mat4 mountSeatTransform; + bool haveSeat = false; + if (mountSeatAttachmentId_ >= 0) { + haveSeat = characterRenderer->getAttachmentTransform( + mountInstanceId_, static_cast(mountSeatAttachmentId_), mountSeatTransform); + } else if (mountSeatAttachmentId_ == -1) { + static constexpr uint32_t kSeatAttachments[] = {0, 5, 6, 7, 8}; + for (uint32_t attId : kSeatAttachments) { + if (characterRenderer->getAttachmentTransform(mountInstanceId_, attId, mountSeatTransform)) { + mountSeatAttachmentId_ = static_cast(attId); + haveSeat = true; + break; + } + } + if (!haveSeat) { + mountSeatAttachmentId_ = -2; + } + } + + if (haveSeat) { + glm::vec3 mountSeatPos = glm::vec3(mountSeatTransform[3]); + glm::vec3 seatOffset = glm::vec3(0.0f, 0.0f, taxiFlight_ ? 0.04f : 0.08f); + glm::vec3 targetRiderPos = mountSeatPos + seatOffset; + if (moving) { + mountSeatSmoothingInit_ = false; + smoothedMountSeatPos_ = targetRiderPos; + } else if (!mountSeatSmoothingInit_) { + smoothedMountSeatPos_ = targetRiderPos; + mountSeatSmoothingInit_ = true; + } else { + float smoothHz = taxiFlight_ ? 10.0f : 14.0f; + float alpha = 1.0f - std::exp(-smoothHz * std::max(lastDeltaTime_, 0.001f)); + smoothedMountSeatPos_ = glm::mix(smoothedMountSeatPos_, targetRiderPos, alpha); + } + + characterRenderer->setInstancePosition(characterInstanceId, smoothedMountSeatPos_); + + float yawRad = glm::radians(characterYaw); + float riderPitch = mountPitch_ * 0.35f; + float riderRoll = mountRoll_ * 0.35f; + characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(riderPitch, riderRoll, yawRad)); + } else { + mountSeatSmoothingInit_ = false; + float yawRad = glm::radians(characterYaw); + glm::mat4 mountRotation = glm::mat4(1.0f); + mountRotation = glm::rotate(mountRotation, yawRad, glm::vec3(0.0f, 0.0f, 1.0f)); + mountRotation = glm::rotate(mountRotation, mountRoll_, glm::vec3(1.0f, 0.0f, 0.0f)); + mountRotation = glm::rotate(mountRotation, mountPitch_, glm::vec3(0.0f, 1.0f, 0.0f)); + glm::vec3 localOffset(0.0f, 0.0f, mountHeightOffset_ + mountBob); + glm::vec3 worldOffset = glm::vec3(mountRotation * glm::vec4(localOffset, 0.0f)); + glm::vec3 playerPos = characterPosition + worldOffset; + characterRenderer->setInstancePosition(characterInstanceId, playerPos); + characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(mountPitch_, mountRoll_, yawRad)); + } + return; + } + + if (!forceMelee) switch (charAnimState_) { + case CharAnimState::IDLE: + if (swim) { + newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; + } else if (sitting && grounded) { + newState = CharAnimState::SIT_DOWN; + } else if (!grounded && jumping) { + newState = CharAnimState::JUMP_START; + } else if (!grounded) { + newState = CharAnimState::JUMP_MID; + } else if (moving && sprinting) { + newState = CharAnimState::RUN; + } else if (moving) { + newState = CharAnimState::WALK; + } else if (inCombat_ && grounded) { + newState = CharAnimState::COMBAT_IDLE; + } + break; + + case CharAnimState::WALK: + if (swim) { + newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; + } else if (!grounded && jumping) { + newState = CharAnimState::JUMP_START; + } else if (!grounded) { + newState = CharAnimState::JUMP_MID; + } else if (!moving) { + newState = CharAnimState::IDLE; + } else if (sprinting) { + newState = CharAnimState::RUN; + } + break; + + case CharAnimState::RUN: + if (swim) { + newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; + } else if (!grounded && jumping) { + newState = CharAnimState::JUMP_START; + } else if (!grounded) { + newState = CharAnimState::JUMP_MID; + } else if (!moving) { + newState = CharAnimState::IDLE; + } else if (!sprinting) { + newState = CharAnimState::WALK; + } + break; + + case CharAnimState::JUMP_START: + if (swim) { + newState = CharAnimState::SWIM_IDLE; + } else if (grounded) { + newState = CharAnimState::JUMP_END; + } else { + newState = CharAnimState::JUMP_MID; + } + break; + + case CharAnimState::JUMP_MID: + if (swim) { + newState = CharAnimState::SWIM_IDLE; + } else if (grounded) { + newState = CharAnimState::JUMP_END; + } + break; + + case CharAnimState::JUMP_END: + if (swim) { + newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; + } else if (moving && sprinting) { + newState = CharAnimState::RUN; + } else if (moving) { + newState = CharAnimState::WALK; + } else { + newState = CharAnimState::IDLE; + } + break; + + case CharAnimState::SIT_DOWN: + if (swim) { + newState = CharAnimState::SWIM_IDLE; + } else if (!sitting) { + newState = CharAnimState::IDLE; + } + break; + + case CharAnimState::SITTING: + if (swim) { + newState = CharAnimState::SWIM_IDLE; + } else if (!sitting) { + newState = CharAnimState::IDLE; + } + break; + + case CharAnimState::EMOTE: + if (swim) { + cancelEmote(); + newState = CharAnimState::SWIM_IDLE; + } else if (jumping || !grounded) { + cancelEmote(); + newState = CharAnimState::JUMP_START; + } else if (moving) { + cancelEmote(); + newState = sprinting ? CharAnimState::RUN : CharAnimState::WALK; + } else if (sitting) { + cancelEmote(); + newState = CharAnimState::SIT_DOWN; + } else if (!emoteLoop_ && characterRenderer && characterInstanceId > 0) { + uint32_t curId = 0; float curT = 0.0f, curDur = 0.0f; + if (characterRenderer->getAnimationState(characterInstanceId, curId, curT, curDur) + && curDur > 0.1f && curT >= curDur - 0.05f) { + cancelEmote(); + newState = CharAnimState::IDLE; + } + } + break; + + case CharAnimState::SWIM_IDLE: + if (!swim) { + newState = moving ? CharAnimState::WALK : CharAnimState::IDLE; + } else if (moving) { + newState = CharAnimState::SWIM; + } + break; + + case CharAnimState::SWIM: + if (!swim) { + newState = moving ? CharAnimState::WALK : CharAnimState::IDLE; + } else if (!moving) { + newState = CharAnimState::SWIM_IDLE; + } + break; + + case CharAnimState::MELEE_SWING: + if (swim) { + newState = CharAnimState::SWIM_IDLE; + } else if (!grounded && jumping) { + newState = CharAnimState::JUMP_START; + } else if (!grounded) { + newState = CharAnimState::JUMP_MID; + } else if (moving && sprinting) { + newState = CharAnimState::RUN; + } else if (moving) { + newState = CharAnimState::WALK; + } else if (sitting) { + newState = CharAnimState::SIT_DOWN; + } else if (inCombat_) { + newState = CharAnimState::COMBAT_IDLE; + } else { + newState = CharAnimState::IDLE; + } + break; + + case CharAnimState::MOUNT: + if (swim) { + newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; + } else if (sitting && grounded) { + newState = CharAnimState::SIT_DOWN; + } else if (!grounded && jumping) { + newState = CharAnimState::JUMP_START; + } else if (!grounded) { + newState = CharAnimState::JUMP_MID; + } else if (moving && sprinting) { + newState = CharAnimState::RUN; + } else if (moving) { + newState = CharAnimState::WALK; + } else { + newState = CharAnimState::IDLE; + } + break; + + case CharAnimState::COMBAT_IDLE: + if (swim) { + newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; + } else if (!grounded && jumping) { + newState = CharAnimState::JUMP_START; + } else if (!grounded) { + newState = CharAnimState::JUMP_MID; + } else if (moving && sprinting) { + newState = CharAnimState::RUN; + } else if (moving) { + newState = CharAnimState::WALK; + } else if (!inCombat_) { + newState = CharAnimState::IDLE; + } + break; + + case CharAnimState::CHARGE: + break; + } + + if (forceMelee) { + newState = CharAnimState::MELEE_SWING; + } + + if (charging_) { + newState = CharAnimState::CHARGE; + } + + if (newState != charAnimState_) { + charAnimState_ = newState; + } + + auto pickFirstAvailable = [&](std::initializer_list candidates, uint32_t fallback) -> uint32_t { + for (uint32_t id : candidates) { + if (characterRenderer->hasAnimation(characterInstanceId, id)) { + return id; + } + } + return fallback; + }; + + uint32_t animId = ANIM_STAND; + bool loop = true; + + switch (charAnimState_) { + case CharAnimState::IDLE: animId = ANIM_STAND; loop = true; break; + case CharAnimState::WALK: + if (movingBackward) { + animId = pickFirstAvailable({ANIM_BACKPEDAL}, ANIM_WALK); + } else if (anyStrafeLeft) { + animId = pickFirstAvailable({ANIM_STRAFE_WALK_LEFT, ANIM_STRAFE_RUN_LEFT}, ANIM_WALK); + } else if (anyStrafeRight) { + animId = pickFirstAvailable({ANIM_STRAFE_WALK_RIGHT, ANIM_STRAFE_RUN_RIGHT}, ANIM_WALK); + } else { + animId = pickFirstAvailable({ANIM_WALK, ANIM_RUN}, ANIM_STAND); + } + loop = true; + break; + case CharAnimState::RUN: + if (movingBackward) { + animId = pickFirstAvailable({ANIM_BACKPEDAL}, ANIM_WALK); + } else if (anyStrafeLeft) { + animId = pickFirstAvailable({ANIM_STRAFE_RUN_LEFT}, ANIM_RUN); + } else if (anyStrafeRight) { + animId = pickFirstAvailable({ANIM_STRAFE_RUN_RIGHT}, ANIM_RUN); + } else { + animId = pickFirstAvailable({ANIM_RUN, ANIM_WALK}, ANIM_STAND); + } + loop = true; + break; + case CharAnimState::JUMP_START: animId = ANIM_JUMP_START; loop = false; break; + case CharAnimState::JUMP_MID: animId = ANIM_JUMP_MID; loop = false; break; + case CharAnimState::JUMP_END: animId = ANIM_JUMP_END; loop = false; break; + case CharAnimState::SIT_DOWN: animId = ANIM_SIT_DOWN; loop = false; break; + case CharAnimState::SITTING: animId = ANIM_SITTING; loop = true; break; + case CharAnimState::EMOTE: animId = emoteAnimId_; loop = emoteLoop_; break; + case CharAnimState::SWIM_IDLE: animId = ANIM_SWIM_IDLE; loop = true; break; + case CharAnimState::SWIM: animId = ANIM_SWIM; loop = true; break; + case CharAnimState::MELEE_SWING: + animId = resolveMeleeAnimId(); + if (animId == 0) { + animId = ANIM_STAND; + } + loop = false; + break; + case CharAnimState::MOUNT: animId = ANIM_MOUNT; loop = true; break; + case CharAnimState::COMBAT_IDLE: + animId = pickFirstAvailable( + {ANIM_READY_1H, ANIM_READY_2H, ANIM_READY_2H_L, ANIM_READY_UNARMED}, + ANIM_STAND); + loop = true; + break; + case CharAnimState::CHARGE: + animId = ANIM_RUN; + loop = true; + break; + } + + uint32_t currentAnimId = 0; + float currentAnimTimeMs = 0.0f; + float currentAnimDurationMs = 0.0f; + bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs); + const bool requestChanged = (lastPlayerAnimRequest_ != animId) || (lastPlayerAnimLoopRequest_ != loop); + const bool shouldPlay = (haveState && currentAnimId != animId) || (!haveState && requestChanged); + if (shouldPlay) { + characterRenderer->playAnimation(characterInstanceId, animId, loop); + lastPlayerAnimRequest_ = animId; + lastPlayerAnimLoopRequest_ = loop; + } +} + +// ── Footstep event detection ───────────────────────────────────────────────── + +bool AnimationController::shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs) { + if (animationDurationMs <= 1.0f) { + footstepNormInitialized_ = false; + return false; + } + + float wrappedTime = animationTimeMs; + while (wrappedTime >= animationDurationMs) { + wrappedTime -= animationDurationMs; + } + if (wrappedTime < 0.0f) wrappedTime += animationDurationMs; + float norm = wrappedTime / animationDurationMs; + + if (animationId != footstepLastAnimationId_) { + footstepLastAnimationId_ = animationId; + footstepLastNormTime_ = norm; + footstepNormInitialized_ = true; + return false; + } + + if (!footstepNormInitialized_) { + footstepNormInitialized_ = true; + footstepLastNormTime_ = norm; + return false; + } + + auto crossed = [&](float eventNorm) { + if (footstepLastNormTime_ <= norm) { + return footstepLastNormTime_ < eventNorm && eventNorm <= norm; + } + return footstepLastNormTime_ < eventNorm || eventNorm <= norm; + }; + + bool trigger = crossed(0.22f) || crossed(0.72f); + footstepLastNormTime_ = norm; + return trigger; +} + +audio::FootstepSurface AnimationController::resolveFootstepSurface() const { + auto* cameraController = renderer_->getCameraController(); + if (!cameraController || !cameraController->isThirdPerson()) { + return audio::FootstepSurface::STONE; + } + + const glm::vec3& p = renderer_->getCharacterPosition(); + + float distSq = glm::dot(p - cachedFootstepPosition_, p - cachedFootstepPosition_); + if (distSq < 2.25f && cachedFootstepUpdateTimer_ < 0.5f) { + return cachedFootstepSurface_; + } + + cachedFootstepPosition_ = p; + cachedFootstepUpdateTimer_ = 0.0f; + + if (cameraController->isSwimming()) { + cachedFootstepSurface_ = audio::FootstepSurface::WATER; + return audio::FootstepSurface::WATER; + } + + auto* waterRenderer = renderer_->getWaterRenderer(); + if (waterRenderer) { + auto waterH = waterRenderer->getWaterHeightAt(p.x, p.y); + if (waterH && p.z < (*waterH + 0.25f)) { + cachedFootstepSurface_ = audio::FootstepSurface::WATER; + return audio::FootstepSurface::WATER; + } + } + + auto* wmoRenderer = renderer_->getWMORenderer(); + auto* terrainManager = renderer_->getTerrainManager(); + if (wmoRenderer) { + auto wmoFloor = wmoRenderer->getFloorHeight(p.x, p.y, p.z + 1.5f); + auto terrainFloor = terrainManager ? terrainManager->getHeightAt(p.x, p.y) : std::nullopt; + if (wmoFloor && (!terrainFloor || *wmoFloor >= *terrainFloor - 0.1f)) { + cachedFootstepSurface_ = audio::FootstepSurface::STONE; + return audio::FootstepSurface::STONE; + } + } + + audio::FootstepSurface surface = audio::FootstepSurface::STONE; + + if (terrainManager) { + auto texture = terrainManager->getDominantTextureAt(p.x, p.y); + if (texture) { + std::string t = *texture; + for (char& c : t) c = static_cast(std::tolower(static_cast(c))); + if (t.find("snow") != std::string::npos || t.find("ice") != std::string::npos) surface = audio::FootstepSurface::SNOW; + else if (t.find("grass") != std::string::npos || t.find("moss") != std::string::npos || t.find("leaf") != std::string::npos) surface = audio::FootstepSurface::GRASS; + else if (t.find("sand") != std::string::npos || t.find("dirt") != std::string::npos || t.find("mud") != std::string::npos) surface = audio::FootstepSurface::DIRT; + else if (t.find("wood") != std::string::npos || t.find("timber") != std::string::npos) surface = audio::FootstepSurface::WOOD; + else if (t.find("metal") != std::string::npos || t.find("iron") != std::string::npos) surface = audio::FootstepSurface::METAL; + else if (t.find("stone") != std::string::npos || t.find("rock") != std::string::npos || t.find("cobble") != std::string::npos || t.find("brick") != std::string::npos) surface = audio::FootstepSurface::STONE; + } + } + + cachedFootstepSurface_ = surface; + return surface; +} + +// ── Footstep update (called from Renderer::update) ────────────────────────── + +void AnimationController::updateFootsteps(float deltaTime) { + auto* footstepManager = renderer_->getAudioCoordinator()->getFootstepManager(); + if (!footstepManager) return; + + auto* characterRenderer = renderer_->getCharacterRenderer(); + auto* cameraController = renderer_->getCameraController(); + uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); + + footstepManager->update(deltaTime); + cachedFootstepUpdateTimer_ += deltaTime; + + bool canPlayFootsteps = characterRenderer && characterInstanceId > 0 && + cameraController && cameraController->isThirdPerson() && + cameraController->isGrounded() && !cameraController->isSwimming(); + + if (canPlayFootsteps && isMounted() && mountInstanceId_ > 0 && !taxiFlight_) { + // Mount footsteps: use mount's animation for timing + uint32_t animId = 0; + float animTimeMs = 0.0f, animDurationMs = 0.0f; + if (characterRenderer->getAnimationState(mountInstanceId_, animId, animTimeMs, animDurationMs) && + animDurationMs > 1.0f && cameraController->isMoving()) { + float wrappedTime = animTimeMs; + while (wrappedTime >= animDurationMs) { + wrappedTime -= animDurationMs; + } + if (wrappedTime < 0.0f) wrappedTime += animDurationMs; + float norm = wrappedTime / animDurationMs; + + if (animId != mountFootstepLastAnimId_) { + mountFootstepLastAnimId_ = animId; + mountFootstepLastNormTime_ = norm; + mountFootstepNormInitialized_ = true; + } else if (!mountFootstepNormInitialized_) { + mountFootstepNormInitialized_ = true; + mountFootstepLastNormTime_ = norm; + } else { + auto crossed = [&](float eventNorm) { + if (mountFootstepLastNormTime_ <= norm) { + return mountFootstepLastNormTime_ < eventNorm && eventNorm <= norm; + } + return mountFootstepLastNormTime_ < eventNorm || eventNorm <= norm; + }; + if (crossed(0.25f) || crossed(0.75f)) { + footstepManager->playFootstep(resolveFootstepSurface(), true); + } + mountFootstepLastNormTime_ = norm; + } + } else { + mountFootstepNormInitialized_ = false; + } + footstepNormInitialized_ = false; + } else if (canPlayFootsteps && isFootstepAnimationState()) { + uint32_t animId = 0; + float animTimeMs = 0.0f; + float animDurationMs = 0.0f; + if (characterRenderer->getAnimationState(characterInstanceId, animId, animTimeMs, animDurationMs) && + shouldTriggerFootstepEvent(animId, animTimeMs, animDurationMs)) { + auto surface = resolveFootstepSurface(); + footstepManager->playFootstep(surface, cameraController->isSprinting()); + if (surface == audio::FootstepSurface::WATER) { + if (renderer_->getAudioCoordinator()->getMovementSoundManager()) { + renderer_->getAudioCoordinator()->getMovementSoundManager()->playWaterFootstep(audio::MovementSoundManager::CharacterSize::MEDIUM); + } + auto* swimEffects = renderer_->getSwimEffects(); + auto* waterRenderer = renderer_->getWaterRenderer(); + if (swimEffects && waterRenderer) { + const glm::vec3& characterPosition = renderer_->getCharacterPosition(); + auto wh = waterRenderer->getWaterHeightAt(characterPosition.x, characterPosition.y); + if (wh) { + swimEffects->spawnFootSplash(characterPosition, *wh); + } + } + } + } + mountFootstepNormInitialized_ = false; + } else { + footstepNormInitialized_ = false; + mountFootstepNormInitialized_ = false; + } +} + +// ── Activity SFX state tracking ────────────────────────────────────────────── + +void AnimationController::updateSfxState(float deltaTime) { + auto* activitySoundManager = renderer_->getAudioCoordinator()->getActivitySoundManager(); + if (!activitySoundManager) return; + + auto* cameraController = renderer_->getCameraController(); + + activitySoundManager->update(deltaTime); + if (cameraController && cameraController->isThirdPerson()) { + bool grounded = cameraController->isGrounded(); + bool jumping = cameraController->isJumping(); + bool falling = cameraController->isFalling(); + bool swimming = cameraController->isSwimming(); + bool moving = cameraController->isMoving(); + + if (!sfxStateInitialized_) { + sfxPrevGrounded_ = grounded; + sfxPrevJumping_ = jumping; + sfxPrevFalling_ = falling; + sfxPrevSwimming_ = swimming; + sfxStateInitialized_ = true; + } + + if (jumping && !sfxPrevJumping_ && !swimming) { + activitySoundManager->playJump(); + } + + if (grounded && !sfxPrevGrounded_) { + bool hardLanding = sfxPrevFalling_; + activitySoundManager->playLanding(resolveFootstepSurface(), hardLanding); + } + + if (swimming && !sfxPrevSwimming_) { + activitySoundManager->playWaterEnter(); + } else if (!swimming && sfxPrevSwimming_) { + activitySoundManager->playWaterExit(); + } + + activitySoundManager->setSwimmingState(swimming, moving); + + if (renderer_->getAudioCoordinator()->getMusicManager()) { + renderer_->getAudioCoordinator()->getMusicManager()->setUnderwaterMode(swimming); + } + + sfxPrevGrounded_ = grounded; + sfxPrevJumping_ = jumping; + sfxPrevFalling_ = falling; + sfxPrevSwimming_ = swimming; + } else { + activitySoundManager->setSwimmingState(false, false); + if (renderer_->getAudioCoordinator()->getMusicManager()) { + renderer_->getAudioCoordinator()->getMusicManager()->setUnderwaterMode(false); + } + sfxStateInitialized_ = false; + } + + // Mount ambient sounds + if (renderer_->getAudioCoordinator()->getMountSoundManager()) { + renderer_->getAudioCoordinator()->getMountSoundManager()->update(deltaTime); + if (cameraController && isMounted()) { + bool isMoving = cameraController->isMoving(); + bool flying = taxiFlight_ || !cameraController->isGrounded(); + renderer_->getAudioCoordinator()->getMountSoundManager()->setMoving(isMoving); + renderer_->getAudioCoordinator()->getMountSoundManager()->setFlying(flying); + } + } +} + +} // namespace rendering +} // namespace wowee diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 5ce45f0f..ddd39746 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -59,6 +59,7 @@ #include "rendering/amd_fsr3_runtime.hpp" #include "rendering/spell_visual_system.hpp" #include "rendering/post_process_pipeline.hpp" +#include "rendering/animation_controller.hpp" #include #include #include @@ -86,34 +87,6 @@ namespace wowee { namespace rendering { -// Audio accessor pass-throughs — delegate to AudioCoordinator (owned by Application). -// These remain until §4.2 (AnimationController) removes Renderer's last audio usage. -audio::MusicManager* Renderer::getMusicManager() { return audioCoordinator_ ? audioCoordinator_->getMusicManager() : nullptr; } -audio::FootstepManager* Renderer::getFootstepManager() { return audioCoordinator_ ? audioCoordinator_->getFootstepManager() : nullptr; } -audio::ActivitySoundManager* Renderer::getActivitySoundManager() { return audioCoordinator_ ? audioCoordinator_->getActivitySoundManager() : nullptr; } -audio::MountSoundManager* Renderer::getMountSoundManager() { return audioCoordinator_ ? audioCoordinator_->getMountSoundManager() : nullptr; } -audio::NpcVoiceManager* Renderer::getNpcVoiceManager() { return audioCoordinator_ ? audioCoordinator_->getNpcVoiceManager() : nullptr; } -audio::AmbientSoundManager* Renderer::getAmbientSoundManager() { return audioCoordinator_ ? audioCoordinator_->getAmbientSoundManager() : nullptr; } -audio::UiSoundManager* Renderer::getUiSoundManager() { return audioCoordinator_ ? audioCoordinator_->getUiSoundManager() : nullptr; } -audio::CombatSoundManager* Renderer::getCombatSoundManager() { return audioCoordinator_ ? audioCoordinator_->getCombatSoundManager() : nullptr; } -audio::SpellSoundManager* Renderer::getSpellSoundManager() { return audioCoordinator_ ? audioCoordinator_->getSpellSoundManager() : nullptr; } -audio::MovementSoundManager* Renderer::getMovementSoundManager() { return audioCoordinator_ ? audioCoordinator_->getMovementSoundManager() : nullptr; } - -struct EmoteInfo { - uint32_t animId = 0; - uint32_t dbcId = 0; // EmotesText.dbc record ID (for CMSG_TEXT_EMOTE) - bool loop = false; - std::string textNoTarget; // sender sees, no target: "You dance." - std::string textTarget; // sender sees, with target: "You dance with %s." - std::string othersNoTarget; // others see, no target: "%s dances." - std::string othersTarget; // others see, with target: "%s dances with %s." - std::string command; -}; - -static std::unordered_map EMOTE_TABLE; -static std::unordered_map EMOTE_BY_DBCID; // reverse lookup: dbcId → EmoteInfo* -static bool emoteTableLoaded = false; - static bool envFlagEnabled(const char* key, bool defaultValue) { const char* raw = std::getenv(key); if (!raw || !*raw) return defaultValue; @@ -133,173 +106,6 @@ static int envIntOrDefault(const char* key, int defaultValue) { return static_cast(n); } - - -static std::vector parseEmoteCommands(const std::string& raw) { - std::vector out; - std::string cur; - for (char c : raw) { - if (std::isalnum(static_cast(c)) || c == '_') { - cur.push_back(static_cast(std::tolower(static_cast(c)))); - } else if (!cur.empty()) { - out.push_back(cur); - cur.clear(); - } - } - if (!cur.empty()) out.push_back(cur); - return out; -} - -static bool isLoopingEmote(const std::string& command) { - static const std::unordered_set kLooping = { - "dance", - "train", - }; - return kLooping.find(command) != kLooping.end(); -} - -static void loadFallbackEmotes() { - if (!EMOTE_TABLE.empty()) return; - EMOTE_TABLE = { - {"wave", {67, 0, false, "You wave.", "You wave at %s.", "%s waves.", "%s waves at %s.", "wave"}}, - {"bow", {66, 0, false, "You bow down graciously.", "You bow down before %s.", "%s bows down graciously.", "%s bows down before %s.", "bow"}}, - {"laugh", {70, 0, false, "You laugh.", "You laugh at %s.", "%s laughs.", "%s laughs at %s.", "laugh"}}, - {"point", {84, 0, false, "You point over yonder.", "You point at %s.", "%s points over yonder.", "%s points at %s.", "point"}}, - {"cheer", {68, 0, false, "You cheer!", "You cheer at %s.", "%s cheers!", "%s cheers at %s.", "cheer"}}, - {"dance", {69, 0, true, "You burst into dance.", "You dance with %s.", "%s bursts into dance.", "%s dances with %s.", "dance"}}, - {"kneel", {75, 0, false, "You kneel down.", "You kneel before %s.", "%s kneels down.", "%s kneels before %s.", "kneel"}}, - {"applaud", {80, 0, false, "You applaud. Bravo!", "You applaud at %s. Bravo!", "%s applauds. Bravo!", "%s applauds at %s. Bravo!", "applaud"}}, - {"shout", {81, 0, false, "You shout.", "You shout at %s.", "%s shouts.", "%s shouts at %s.", "shout"}}, - {"chicken", {78, 0, false, "With arms flapping, you strut around. Cluck, Cluck, Chicken!", - "With arms flapping, you strut around %s. Cluck, Cluck, Chicken!", - "%s struts around. Cluck, Cluck, Chicken!", "%s struts around %s. Cluck, Cluck, Chicken!", "chicken"}}, - {"cry", {77, 0, false, "You cry.", "You cry on %s's shoulder.", "%s cries.", "%s cries on %s's shoulder.", "cry"}}, - {"kiss", {76, 0, false, "You blow a kiss into the wind.", "You blow a kiss to %s.", "%s blows a kiss into the wind.", "%s blows a kiss to %s.", "kiss"}}, - {"roar", {74, 0, false, "You roar with bestial vigor. So fierce!", "You roar with bestial vigor at %s. So fierce!", "%s roars with bestial vigor. So fierce!", "%s roars with bestial vigor at %s. So fierce!", "roar"}}, - {"salute", {113, 0, false, "You salute.", "You salute %s with respect.", "%s salutes.", "%s salutes %s with respect.", "salute"}}, - {"rude", {73, 0, false, "You make a rude gesture.", "You make a rude gesture at %s.", "%s makes a rude gesture.", "%s makes a rude gesture at %s.", "rude"}}, - {"flex", {82, 0, false, "You flex your muscles. Oooooh so strong!", "You flex at %s. Oooooh so strong!", "%s flexes. Oooooh so strong!", "%s flexes at %s. Oooooh so strong!", "flex"}}, - {"shy", {83, 0, false, "You smile shyly.", "You smile shyly at %s.", "%s smiles shyly.", "%s smiles shyly at %s.", "shy"}}, - {"beg", {79, 0, false, "You beg everyone around you. How pathetic.", "You beg %s. How pathetic.", "%s begs everyone around. How pathetic.", "%s begs %s. How pathetic.", "beg"}}, - {"eat", {61, 0, false, "You begin to eat.", "You begin to eat in front of %s.", "%s begins to eat.", "%s begins to eat in front of %s.", "eat"}}, - }; -} - -static std::string replacePlaceholders(const std::string& text, const std::string* targetName) { - if (text.empty()) return text; - std::string out; - out.reserve(text.size() + 16); - for (size_t i = 0; i < text.size(); ++i) { - if (text[i] == '%' && i + 1 < text.size() && text[i + 1] == 's') { - if (targetName && !targetName->empty()) out += *targetName; - i++; - } else { - out.push_back(text[i]); - } - } - return out; -} - -static void loadEmotesFromDbc() { - if (emoteTableLoaded) return; - emoteTableLoaded = true; - - auto* assetManager = core::Application::getInstance().getAssetManager(); - if (!assetManager) { - LOG_WARNING("Emotes: no AssetManager"); - loadFallbackEmotes(); - return; - } - - auto emotesTextDbc = assetManager->loadDBC("EmotesText.dbc"); - auto emotesTextDataDbc = assetManager->loadDBC("EmotesTextData.dbc"); - if (!emotesTextDbc || !emotesTextDataDbc || !emotesTextDbc->isLoaded() || !emotesTextDataDbc->isLoaded()) { - LOG_WARNING("Emotes: DBCs not available (EmotesText/EmotesTextData)"); - loadFallbackEmotes(); - return; - } - - const auto* activeLayout = pipeline::getActiveDBCLayout(); - const auto* etdL = activeLayout ? activeLayout->getLayout("EmotesTextData") : nullptr; - const auto* emL = activeLayout ? activeLayout->getLayout("Emotes") : nullptr; - const auto* etL = activeLayout ? activeLayout->getLayout("EmotesText") : nullptr; - - std::unordered_map textData; - textData.reserve(emotesTextDataDbc->getRecordCount()); - for (uint32_t r = 0; r < emotesTextDataDbc->getRecordCount(); ++r) { - uint32_t id = emotesTextDataDbc->getUInt32(r, etdL ? (*etdL)["ID"] : 0); - std::string text = emotesTextDataDbc->getString(r, etdL ? (*etdL)["Text"] : 1); - if (!text.empty()) textData.emplace(id, std::move(text)); - } - - std::unordered_map emoteIdToAnim; - if (auto emotesDbc = assetManager->loadDBC("Emotes.dbc"); emotesDbc && emotesDbc->isLoaded()) { - emoteIdToAnim.reserve(emotesDbc->getRecordCount()); - for (uint32_t r = 0; r < emotesDbc->getRecordCount(); ++r) { - uint32_t emoteId = emotesDbc->getUInt32(r, emL ? (*emL)["ID"] : 0); - uint32_t animId = emotesDbc->getUInt32(r, emL ? (*emL)["AnimID"] : 2); - if (animId != 0) emoteIdToAnim[emoteId] = animId; - } - } - - EMOTE_TABLE.clear(); - EMOTE_TABLE.reserve(emotesTextDbc->getRecordCount()); - for (uint32_t r = 0; r < emotesTextDbc->getRecordCount(); ++r) { - uint32_t recordId = emotesTextDbc->getUInt32(r, etL ? (*etL)["ID"] : 0); - std::string cmdRaw = emotesTextDbc->getString(r, etL ? (*etL)["Command"] : 1); - if (cmdRaw.empty()) continue; - - uint32_t emoteRef = emotesTextDbc->getUInt32(r, etL ? (*etL)["EmoteRef"] : 2); - uint32_t animId = 0; - auto animIt = emoteIdToAnim.find(emoteRef); - if (animIt != emoteIdToAnim.end()) { - animId = animIt->second; - } else { - animId = emoteRef; // fallback if EmotesText stores animation id directly - } - - uint32_t senderTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderTargetTextID"] : 5); - uint32_t senderNoTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderNoTargetTextID"] : 9); - uint32_t othersTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["OthersTargetTextID"] : 3); - uint32_t othersNoTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["OthersNoTargetTextID"] : 7); - - std::string textTarget, textNoTarget, oTarget, oNoTarget; - if (auto it = textData.find(senderTargetTextId); it != textData.end()) textTarget = it->second; - if (auto it = textData.find(senderNoTargetTextId); it != textData.end()) textNoTarget = it->second; - if (auto it = textData.find(othersTargetTextId); it != textData.end()) oTarget = it->second; - if (auto it = textData.find(othersNoTargetTextId); it != textData.end()) oNoTarget = it->second; - - for (const std::string& cmd : parseEmoteCommands(cmdRaw)) { - if (cmd.empty()) continue; - EmoteInfo info; - info.animId = animId; - info.dbcId = recordId; - info.loop = isLoopingEmote(cmd); - info.textNoTarget = textNoTarget; - info.textTarget = textTarget; - info.othersNoTarget = oNoTarget; - info.othersTarget = oTarget; - info.command = cmd; - EMOTE_TABLE.emplace(cmd, std::move(info)); - } - } - - if (EMOTE_TABLE.empty()) { - LOG_WARNING("Emotes: DBC loaded but no commands parsed, using fallback list"); - loadFallbackEmotes(); - } else { - LOG_INFO("Emotes: loaded ", EMOTE_TABLE.size(), " commands from DBC"); - } - - // Build reverse lookup by dbcId (only first command per emote needed) - EMOTE_BY_DBCID.clear(); - for (auto& [cmd, info] : EMOTE_TABLE) { - if (info.dbcId != 0) { - EMOTE_BY_DBCID.emplace(info.dbcId, &info); - } - } -} - Renderer::Renderer() = default; Renderer::~Renderer() = default; @@ -827,6 +633,9 @@ void Renderer::shutdown() { characterRenderer.reset(); } + // Shutdown AnimationController before renderers it references (§4.2) + animationController_.reset(); + LOG_WARNING("Renderer::shutdown - wmoRenderer..."); if (wmoRenderer) { wmoRenderer->shutdown(); @@ -1175,1107 +984,57 @@ void Renderer::setCharacterFollow(uint32_t instanceId) { if (cameraController && instanceId > 0) { cameraController->setFollowTarget(&characterPosition); } + if (animationController_) animationController_->onCharacterFollow(instanceId); } void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset, const std::string& modelPath) { - mountInstanceId_ = mountInstId; - mountHeightOffset_ = heightOffset; - mountSeatAttachmentId_ = -1; - smoothedMountSeatPos_ = characterPosition; - mountSeatSmoothingInit_ = false; - mountAction_ = MountAction::None; // Clear mount action state - mountActionPhase_ = 0; - charAnimState = CharAnimState::MOUNT; - if (cameraController) { - cameraController->setMounted(true); - cameraController->setMountHeightOffset(heightOffset); - } - - // Debug: dump available mount animations - if (characterRenderer && mountInstId > 0) { - characterRenderer->dumpAnimations(mountInstId); - } - - // Discover mount animation capabilities (property-based, not hardcoded IDs) - LOG_DEBUG("=== Mount Animation Dump (Display ID ", mountDisplayId, ") ==="); - characterRenderer->dumpAnimations(mountInstId); - - // Get all sequences for property-based analysis - std::vector sequences; - if (!characterRenderer->getAnimationSequences(mountInstId, sequences)) { - LOG_WARNING("Failed to get animation sequences for mount, using fallback IDs"); - sequences.clear(); - } - - // Helper: ID-based fallback finder - auto findFirst = [&](std::initializer_list candidates) -> uint32_t { - for (uint32_t id : candidates) { - if (characterRenderer->hasAnimation(mountInstId, id)) { - return id; - } - } - return 0; - }; - - // Property-based jump animation discovery with chain-based scoring - auto discoverJumpSet = [&]() { - // Debug: log all sequences for analysis - LOG_DEBUG("=== Full sequence table for mount ==="); - for (const auto& seq : sequences) { - LOG_DEBUG("SEQ id=", seq.id, - " dur=", seq.duration, - " flags=0x", std::hex, seq.flags, std::dec, - " moveSpd=", seq.movingSpeed, - " blend=", seq.blendTime, - " next=", seq.nextAnimation, - " alias=", seq.aliasNext); - } - LOG_DEBUG("=== End sequence table ==="); - - // Known combat/bad animation IDs to avoid - std::set forbiddenIds = {53, 54, 16}; // jumpkick, attack - - auto scoreNear = [](int a, int b) -> int { - int d = std::abs(a - b); - return (d <= 8) ? (20 - d) : 0; // within 8 IDs gets points - }; - - auto isForbidden = [&](uint32_t id) { - return forbiddenIds.count(id) != 0; - }; - - auto findSeqById = [&](uint32_t id) -> const pipeline::M2Sequence* { - for (const auto& s : sequences) { - if (s.id == id) return &s; - } - return nullptr; - }; - - uint32_t runId = findFirst({5, 4}); - uint32_t standId = findFirst({0}); - - // Step A: Find loop candidates - std::vector loops; - for (const auto& seq : sequences) { - if (isForbidden(seq.id)) continue; - // Bit 0x01 NOT set = loops (0x20, 0x60), bit 0x01 set = non-looping (0x21, 0x61) - bool isLoop = (seq.flags & 0x01) == 0; - if (isLoop && seq.duration >= 350 && seq.duration <= 1000 && - seq.id != runId && seq.id != standId) { - loops.push_back(seq.id); - } - } - - // Choose loop: prefer one near known classic IDs (38), else best duration - uint32_t loop = 0; - if (!loops.empty()) { - uint32_t best = loops[0]; - int bestScore = -999; - for (uint32_t id : loops) { - int sc = 0; - sc += scoreNear(static_cast(id), 38); // classic hint - const auto* s = findSeqById(id); - if (s) sc += (s->duration >= 500 && s->duration <= 800) ? 5 : 0; - if (sc > bestScore) { - bestScore = sc; - best = id; - } - } - loop = best; - } - - // Step B: Score start/end candidates - uint32_t start = 0, end = 0; - int bestStart = -999, bestEnd = -999; - - for (const auto& seq : sequences) { - if (isForbidden(seq.id)) continue; - // Only consider non-looping animations for start/end - bool isLoop = (seq.flags & 0x01) == 0; - if (isLoop) continue; - - // Start window - if (seq.duration >= 450 && seq.duration <= 1100) { - int sc = 0; - if (loop) sc += scoreNear(static_cast(seq.id), static_cast(loop)); - // Chain bonus: if this start points at loop or near it - if (loop && (seq.nextAnimation == static_cast(loop) || seq.aliasNext == loop)) sc += 30; - if (loop && scoreNear(seq.nextAnimation, static_cast(loop)) > 0) sc += 10; - // Penalize "stop/brake-ish": very long blendTime can be a stop transition - if (seq.blendTime > 400) sc -= 5; - - if (sc > bestStart) { - bestStart = sc; - start = seq.id; - } - } - - // End window - if (seq.duration >= 650 && seq.duration <= 1600) { - int sc = 0; - if (loop) sc += scoreNear(static_cast(seq.id), static_cast(loop)); - // Chain bonus: end often points to run/stand or has no next - if (seq.nextAnimation == static_cast(runId) || seq.nextAnimation == static_cast(standId)) sc += 10; - if (seq.nextAnimation < 0) sc += 5; // no chain sometimes = terminal - if (sc > bestEnd) { - bestEnd = sc; - end = seq.id; - } - } - } - - LOG_DEBUG("Property-based jump discovery: start=", start, " loop=", loop, " end=", end, - " scores: start=", bestStart, " end=", bestEnd); - return std::make_tuple(start, loop, end); - }; - - auto [discoveredStart, discoveredLoop, discoveredEnd] = discoverJumpSet(); - - // Use discovered animations, fallback to known IDs if discovery fails - mountAnims_.jumpStart = discoveredStart > 0 ? discoveredStart : findFirst({40, 37}); - mountAnims_.jumpLoop = discoveredLoop > 0 ? discoveredLoop : findFirst({38}); - mountAnims_.jumpEnd = discoveredEnd > 0 ? discoveredEnd : findFirst({39}); - mountAnims_.rearUp = findFirst({94, 92, 40}); // RearUp/Special - mountAnims_.run = findFirst({5, 4}); // Run/Walk - mountAnims_.stand = findFirst({0}); // Stand (almost always 0) - - // Discover idle fidget animations using proper WoW M2 metadata (frequency, replay timers) - mountAnims_.fidgets.clear(); - core::Logger::getInstance().debug("Scanning for fidget animations in ", sequences.size(), " sequences"); - - // DEBUG: Log ALL non-looping, short, stationary animations to identify stamps/tosses - core::Logger::getInstance().debug("=== ALL potential fidgets (no metadata filter) ==="); - for (const auto& seq : sequences) { - bool isLoop = (seq.flags & 0x01) == 0; - bool isStationary = std::abs(seq.movingSpeed) < 0.05f; - bool reasonableDuration = seq.duration >= 400 && seq.duration <= 2500; - - if (!isLoop && reasonableDuration && isStationary) { - core::Logger::getInstance().debug(" ALL: id=", seq.id, - " dur=", seq.duration, "ms", - " freq=", seq.frequency, - " replay=", seq.replayMin, "-", seq.replayMax, - " flags=0x", std::hex, seq.flags, std::dec, - " next=", seq.nextAnimation); - } - } - - // Proper fidget discovery: frequency > 0 + replay timers indicate random idle animations - for (const auto& seq : sequences) { - bool isLoop = (seq.flags & 0x01) == 0; - bool hasFrequency = seq.frequency > 0; - bool hasReplay = seq.replayMax > 0; - bool isStationary = std::abs(seq.movingSpeed) < 0.05f; - bool reasonableDuration = seq.duration >= 400 && seq.duration <= 2500; - - // Log candidates with metadata - if (!isLoop && reasonableDuration && isStationary && (hasFrequency || hasReplay)) { - core::Logger::getInstance().debug(" Candidate: id=", seq.id, - " dur=", seq.duration, "ms", - " freq=", seq.frequency, - " replay=", seq.replayMin, "-", seq.replayMax, - " next=", seq.nextAnimation, - " speed=", seq.movingSpeed); - } - - // Exclude known problematic animations: death (5-6), wounds (7-9), combat (16-21), attacks (11-15) - bool isDeathOrWound = (seq.id >= 5 && seq.id <= 9); - bool isAttackOrCombat = (seq.id >= 11 && seq.id <= 21); - bool isSpecial = (seq.id == 2 || seq.id == 3); // Often aggressive specials - - // Select fidgets: (frequency OR replay) + exclude problematic ID ranges - // Relaxed back to OR since some mounts may only have one metadata marker - if (!isLoop && (hasFrequency || hasReplay) && isStationary && reasonableDuration && - !isDeathOrWound && !isAttackOrCombat && !isSpecial) { - // Bonus: chains back to stand (indicates idle behavior) - bool chainsToStand = (seq.nextAnimation == static_cast(mountAnims_.stand)) || - (seq.aliasNext == mountAnims_.stand) || - (seq.nextAnimation == -1); - - mountAnims_.fidgets.push_back(seq.id); - core::Logger::getInstance().debug(" >> Selected fidget: id=", seq.id, - (chainsToStand ? " (chains to stand)" : "")); - } - } - - // Ensure we have fallbacks for movement - if (mountAnims_.run == 0) mountAnims_.run = mountAnims_.stand; // Fallback to stand if no run - - core::Logger::getInstance().debug("Mount animation set: jumpStart=", mountAnims_.jumpStart, - " jumpLoop=", mountAnims_.jumpLoop, - " jumpEnd=", mountAnims_.jumpEnd, - " rearUp=", mountAnims_.rearUp, - " run=", mountAnims_.run, - " stand=", mountAnims_.stand, - " fidgets=", mountAnims_.fidgets.size()); - - // Notify mount sound manager - if (getMountSoundManager()) { - bool isFlying = taxiFlight_; // Taxi flights are flying mounts - getMountSoundManager()->onMount(mountDisplayId, isFlying, modelPath); - } + if (animationController_) animationController_->setMounted(mountInstId, mountDisplayId, heightOffset, modelPath); } void Renderer::clearMount() { - mountInstanceId_ = 0; - mountHeightOffset_ = 0.0f; - mountPitch_ = 0.0f; - mountRoll_ = 0.0f; - mountSeatAttachmentId_ = -1; - smoothedMountSeatPos_ = glm::vec3(0.0f); - mountSeatSmoothingInit_ = false; - mountAction_ = MountAction::None; - mountActionPhase_ = 0; - charAnimState = CharAnimState::IDLE; - if (cameraController) { - cameraController->setMounted(false); - cameraController->setMountHeightOffset(0.0f); - } - - // Notify mount sound manager - if (getMountSoundManager()) { - getMountSoundManager()->onDismount(); - } + if (animationController_) animationController_->clearMount(); } -uint32_t Renderer::resolveMeleeAnimId() { - if (!characterRenderer || characterInstanceId == 0) { - meleeAnimId = 0; - meleeAnimDurationMs = 0.0f; - return 0; - } - if (meleeAnimId != 0 && characterRenderer->hasAnimation(characterInstanceId, meleeAnimId)) { - return meleeAnimId; - } - - std::vector sequences; - if (!characterRenderer->getAnimationSequences(characterInstanceId, sequences)) { - meleeAnimId = 0; - meleeAnimDurationMs = 0.0f; - return 0; - } - - auto findDuration = [&](uint32_t id) -> float { - for (const auto& seq : sequences) { - if (seq.id == id && seq.duration > 0) { - return static_cast(seq.duration); - } - } - return 0.0f; - }; - - // Select animation priority based on equipped weapon type - // WoW inventory types: 17 = 2H weapon, 13/21 = 1H, 0 = unarmed - // WoW anim IDs: 16 = unarmed, 17 = 1H attack, 18 = 2H attack - const uint32_t* attackCandidates; - size_t candidateCount; - static const uint32_t candidates2H[] = {18, 17, 16, 19, 20, 21}; - static const uint32_t candidates1H[] = {17, 18, 16, 19, 20, 21}; - static const uint32_t candidatesUnarmed[] = {16, 17, 18, 19, 20, 21}; - if (equippedWeaponInvType_ == 17) { // INVTYPE_2HWEAPON - attackCandidates = candidates2H; - candidateCount = 6; - } else if (equippedWeaponInvType_ == 0) { - attackCandidates = candidatesUnarmed; - candidateCount = 6; - } else { - attackCandidates = candidates1H; - candidateCount = 6; - } - for (size_t ci = 0; ci < candidateCount; ci++) { - uint32_t id = attackCandidates[ci]; - if (characterRenderer->hasAnimation(characterInstanceId, id)) { - meleeAnimId = id; - meleeAnimDurationMs = findDuration(id); - return meleeAnimId; - } - } - - const uint32_t avoidIds[] = {0, 1, 4, 5, 11, 12, 13, 37, 38, 39, 41, 42, 97}; - auto isAvoid = [&](uint32_t id) -> bool { - for (uint32_t avoid : avoidIds) { - if (id == avoid) return true; - } - return false; - }; - - uint32_t bestId = 0; - uint32_t bestDuration = 0; - for (const auto& seq : sequences) { - if (seq.duration == 0) continue; - if (isAvoid(seq.id)) continue; - if (seq.movingSpeed > 0.1f) continue; - if (seq.duration < 150 || seq.duration > 2000) continue; - if (bestId == 0 || seq.duration < bestDuration) { - bestId = seq.id; - bestDuration = seq.duration; - } - } - - if (bestId == 0) { - for (const auto& seq : sequences) { - if (seq.duration == 0) continue; - if (isAvoid(seq.id)) continue; - if (bestId == 0 || seq.duration < bestDuration) { - bestId = seq.id; - bestDuration = seq.duration; - } - } - } - - meleeAnimId = bestId; - meleeAnimDurationMs = static_cast(bestDuration); - return meleeAnimId; -} - -void Renderer::updateCharacterAnimation() { - // WoW WotLK AnimationData.dbc IDs - constexpr uint32_t ANIM_STAND = 0; - constexpr uint32_t ANIM_WALK = 4; - constexpr uint32_t ANIM_RUN = 5; - // Candidate locomotion clips by common WotLK IDs. - constexpr uint32_t ANIM_STRAFE_RUN_RIGHT = 92; - constexpr uint32_t ANIM_STRAFE_RUN_LEFT = 93; - constexpr uint32_t ANIM_STRAFE_WALK_LEFT = 11; - constexpr uint32_t ANIM_STRAFE_WALK_RIGHT = 12; - constexpr uint32_t ANIM_BACKPEDAL = 13; - constexpr uint32_t ANIM_JUMP_START = 37; - constexpr uint32_t ANIM_JUMP_MID = 38; - constexpr uint32_t ANIM_JUMP_END = 39; - constexpr uint32_t ANIM_SIT_DOWN = 97; // SitGround — transition to sitting - constexpr uint32_t ANIM_SITTING = 97; // Hold on same animation (no separate idle) - constexpr uint32_t ANIM_SWIM_IDLE = 41; // Treading water (SwimIdle) - constexpr uint32_t ANIM_SWIM = 42; // Swimming forward (Swim) - constexpr uint32_t ANIM_MOUNT = 91; // Seated on mount - // Canonical player ready stances (AnimationData.dbc) - constexpr uint32_t ANIM_READY_UNARMED = 22; // ReadyUnarmed - constexpr uint32_t ANIM_READY_1H = 23; // Ready1H - constexpr uint32_t ANIM_READY_2H = 24; // Ready2H - constexpr uint32_t ANIM_READY_2H_L = 25; // Ready2HL (some 2H left-handed rigs) - constexpr uint32_t ANIM_FLY_IDLE = 158; // Flying mount idle/hover - constexpr uint32_t ANIM_FLY_FORWARD = 159; // Flying mount forward - - CharAnimState newState = charAnimState; - - const bool rawMoving = cameraController->isMoving(); - const bool rawSprinting = cameraController->isSprinting(); - constexpr float kLocomotionStopGraceSec = 0.12f; - if (rawMoving) { - locomotionStopGraceTimer_ = kLocomotionStopGraceSec; - locomotionWasSprinting_ = rawSprinting; - } else { - locomotionStopGraceTimer_ = std::max(0.0f, locomotionStopGraceTimer_ - lastDeltaTime_); - } - // Debounce brief input/state dropouts (notably during both-mouse steering) so - // locomotion clips do not restart every few frames. - bool moving = rawMoving || locomotionStopGraceTimer_ > 0.0f; - bool movingForward = cameraController->isMovingForward(); - bool movingBackward = cameraController->isMovingBackward(); - bool autoRunning = cameraController->isAutoRunning(); - bool strafeLeft = cameraController->isStrafingLeft(); - bool strafeRight = cameraController->isStrafingRight(); - // Strafe animation only plays during *pure* strafing (no forward/backward/autorun). - // When forward+strafe are both held, the walk/run animation plays — same as the real client. - bool pureStrafe = !movingForward && !movingBackward && !autoRunning; - bool anyStrafeLeft = strafeLeft && !strafeRight && pureStrafe; - bool anyStrafeRight = strafeRight && !strafeLeft && pureStrafe; - bool grounded = cameraController->isGrounded(); - bool jumping = cameraController->isJumping(); - bool sprinting = rawSprinting || (!rawMoving && moving && locomotionWasSprinting_); - bool sitting = cameraController->isSitting(); - bool swim = cameraController->isSwimming(); - bool forceMelee = meleeSwingTimer > 0.0f && grounded && !swim; - - // When mounted, force MOUNT state and skip normal transitions - if (isMounted()) { - newState = CharAnimState::MOUNT; - charAnimState = newState; - - // Play seated animation on player - uint32_t currentAnimId = 0; - float currentAnimTimeMs = 0.0f, currentAnimDurationMs = 0.0f; - bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs); - if (!haveState || currentAnimId != ANIM_MOUNT) { - characterRenderer->playAnimation(characterInstanceId, ANIM_MOUNT, true); - } - - // Sync mount instance position and rotation - float mountBob = 0.0f; - float mountYawRad = glm::radians(characterYaw); - if (mountInstanceId_ > 0) { - characterRenderer->setInstancePosition(mountInstanceId_, characterPosition); - - // Procedural lean into turns (ground mounts only, optional enhancement) - if (!taxiFlight_ && moving && lastDeltaTime_ > 0.0f) { - float currentYawDeg = characterYaw; - float turnRate = (currentYawDeg - prevMountYaw_) / lastDeltaTime_; - // Normalize to [-180, 180] for wrap-around - while (turnRate > 180.0f) turnRate -= 360.0f; - while (turnRate < -180.0f) turnRate += 360.0f; - - float targetLean = glm::clamp(turnRate * 0.15f, -0.25f, 0.25f); - mountRoll_ = glm::mix(mountRoll_, targetLean, lastDeltaTime_ * 6.0f); - prevMountYaw_ = currentYawDeg; - } else { - // Return to upright when not turning - mountRoll_ = glm::mix(mountRoll_, 0.0f, lastDeltaTime_ * 8.0f); - } - - // Apply pitch (up/down), roll (banking), and yaw for realistic flight - characterRenderer->setInstanceRotation(mountInstanceId_, glm::vec3(mountPitch_, mountRoll_, mountYawRad)); - - // Drive mount model animation: idle when still, run when moving - auto pickMountAnim = [&](std::initializer_list candidates, uint32_t fallback) -> uint32_t { - for (uint32_t id : candidates) { - if (characterRenderer->hasAnimation(mountInstanceId_, id)) { - return id; - } - } - return fallback; - }; - - uint32_t mountAnimId = ANIM_STAND; - - // Get current mount animation state (used throughout) - uint32_t curMountAnim = 0; - float curMountTime = 0, curMountDur = 0; - bool haveMountState = characterRenderer->getAnimationState(mountInstanceId_, curMountAnim, curMountTime, curMountDur); - - // Taxi flight: use flying animations instead of ground movement - if (taxiFlight_) { - // Log available animations once when taxi starts - if (!taxiAnimsLogged_) { - taxiAnimsLogged_ = true; - LOG_INFO("Taxi flight active: mountInstanceId_=", mountInstanceId_, - " curMountAnim=", curMountAnim, " haveMountState=", haveMountState); - std::vector seqs; - if (characterRenderer->getAnimationSequences(mountInstanceId_, seqs)) { - std::string animList; - for (const auto& s : seqs) { - if (!animList.empty()) animList += ", "; - animList += std::to_string(s.id); - } - LOG_INFO("Taxi mount available animations: [", animList, "]"); - } - } - - // Try multiple flying animation IDs in priority order: - // 159=FlyForward, 158=FlyIdle (WotLK flying mounts) - // 234=FlyRun, 229=FlyStand (Vanilla creature fly anims) - // 233=FlyWalk, 141=FlyMounted, 369=FlyRun (alternate IDs) - // 6=Fly (classic creature fly) - // Fallback: Run, then Stand (hover) - uint32_t flyAnims[] = {ANIM_FLY_FORWARD, ANIM_FLY_IDLE, 234, 229, 233, 141, 369, 6, ANIM_RUN}; - mountAnimId = ANIM_STAND; // ultimate fallback: hover/idle - for (uint32_t fa : flyAnims) { - if (characterRenderer->hasAnimation(mountInstanceId_, fa)) { - mountAnimId = fa; - break; - } - } - - if (!haveMountState || curMountAnim != mountAnimId) { - LOG_INFO("Taxi mount: playing animation ", mountAnimId); - characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); - } - - // Skip all ground mount logic (jumps, fidgets, etc.) - goto taxi_mount_done; - } else { - taxiAnimsLogged_ = false; - } - - // Check for jump trigger - use cached per-mount animation IDs - if (cameraController->isJumpKeyPressed() && grounded && mountAction_ == MountAction::None) { - if (moving && mountAnims_.jumpLoop > 0) { - // Moving: skip JumpStart (looks like stopping), go straight to airborne loop - LOG_DEBUG("Mount jump triggered while moving: using jumpLoop anim ", mountAnims_.jumpLoop); - characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpLoop, true); - mountAction_ = MountAction::Jump; - mountActionPhase_ = 1; // Start in airborne phase - mountAnimId = mountAnims_.jumpLoop; - if (getMountSoundManager()) { - getMountSoundManager()->playJumpSound(); - } - if (cameraController) { - cameraController->triggerMountJump(); - } - } else if (!moving && mountAnims_.rearUp > 0) { - // Standing still: rear-up flourish - LOG_DEBUG("Mount rear-up triggered: playing rearUp anim ", mountAnims_.rearUp); - characterRenderer->playAnimation(mountInstanceId_, mountAnims_.rearUp, false); - mountAction_ = MountAction::RearUp; - mountActionPhase_ = 0; - mountAnimId = mountAnims_.rearUp; - // Trigger semantic rear-up sound - if (getMountSoundManager()) { - getMountSoundManager()->playRearUpSound(); - } - } - } - - // Handle active mount actions (jump chaining or rear-up) - if (mountAction_ != MountAction::None) { - bool animFinished = haveMountState && curMountDur > 0.1f && - (curMountTime >= curMountDur - 0.05f); - - if (mountAction_ == MountAction::Jump) { - // Jump sequence: start → loop → end (physics-driven) - if (mountActionPhase_ == 0 && animFinished && mountAnims_.jumpLoop > 0) { - // JumpStart finished, go to JumpLoop (airborne) - LOG_DEBUG("Mount jump: phase 0→1 (JumpStart→JumpLoop anim ", mountAnims_.jumpLoop, ")"); - characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpLoop, true); - mountActionPhase_ = 1; - mountAnimId = mountAnims_.jumpLoop; - } else if (mountActionPhase_ == 0 && animFinished && mountAnims_.jumpLoop == 0) { - // No JumpLoop, go straight to airborne phase 1 (hold JumpStart pose) - LOG_DEBUG("Mount jump: phase 0→1 (no JumpLoop, holding JumpStart)"); - mountActionPhase_ = 1; - } else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd > 0) { - // Landed after airborne phase! Go to JumpEnd (grounded-triggered) - LOG_DEBUG("Mount jump: phase 1→2 (landed, JumpEnd anim ", mountAnims_.jumpEnd, ")"); - characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpEnd, false); - mountActionPhase_ = 2; - mountAnimId = mountAnims_.jumpEnd; - // Trigger semantic landing sound - if (getMountSoundManager()) { - getMountSoundManager()->playLandSound(); - } - } else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd == 0) { - // No JumpEnd animation, return directly to movement after landing - LOG_DEBUG("Mount jump: phase 1→done (landed, no JumpEnd, returning to ", - moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand), ")"); - mountAction_ = MountAction::None; - mountAnimId = moving ? mountAnims_.run : mountAnims_.stand; - characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); - } else if (mountActionPhase_ == 2 && animFinished) { - // JumpEnd finished, return to movement - LOG_DEBUG("Mount jump: phase 2→done (JumpEnd finished, returning to ", - moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand), ")"); - mountAction_ = MountAction::None; - mountAnimId = moving ? mountAnims_.run : mountAnims_.stand; - characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); - } else { - mountAnimId = curMountAnim; // Keep current jump animation - } - } else if (mountAction_ == MountAction::RearUp) { - // Rear-up: single animation, return to stand when done - if (animFinished) { - LOG_DEBUG("Mount rear-up: finished, returning to ", - moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand)); - mountAction_ = MountAction::None; - mountAnimId = moving ? mountAnims_.run : mountAnims_.stand; - characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); - } else { - mountAnimId = curMountAnim; // Keep current rear-up animation - } - } - } else if (moving) { - // Normal movement animations - if (anyStrafeLeft) { - mountAnimId = pickMountAnim({ANIM_STRAFE_RUN_LEFT, ANIM_STRAFE_WALK_LEFT, ANIM_RUN}, ANIM_RUN); - } else if (anyStrafeRight) { - mountAnimId = pickMountAnim({ANIM_STRAFE_RUN_RIGHT, ANIM_STRAFE_WALK_RIGHT, ANIM_RUN}, ANIM_RUN); - } else if (movingBackward) { - mountAnimId = pickMountAnim({ANIM_BACKPEDAL}, ANIM_RUN); - } else { - mountAnimId = ANIM_RUN; - } - } - - // Cancel active fidget immediately if movement starts - if (moving && mountActiveFidget_ != 0) { - mountActiveFidget_ = 0; - // Force play run animation to stop fidget immediately - characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); - } - - // Check if active fidget has completed (only when not moving) - if (!moving && mountActiveFidget_ != 0) { - uint32_t curAnim = 0; - float curTime = 0.0f, curDur = 0.0f; - if (characterRenderer->getAnimationState(mountInstanceId_, curAnim, curTime, curDur)) { - // If animation changed or completed, clear active fidget - if (curAnim != mountActiveFidget_ || curTime >= curDur * 0.95f) { - mountActiveFidget_ = 0; - LOG_DEBUG("Mount fidget completed"); - } - } - } - - // Idle fidgets: random one-shot animations when standing still - if (!moving && mountAction_ == MountAction::None && mountActiveFidget_ == 0 && !mountAnims_.fidgets.empty()) { - mountIdleFidgetTimer_ += lastDeltaTime_; - // Use the seeded mt19937 for timing so fidgets aren't deterministic - // across launches (rand() without srand() always starts from seed 1). - static std::mt19937 idleRng(std::random_device{}()); - static float nextFidgetTime = std::uniform_real_distribution(6.0f, 12.0f)(idleRng); - - if (mountIdleFidgetTimer_ >= nextFidgetTime) { - std::uniform_int_distribution dist(0, mountAnims_.fidgets.size() - 1); - uint32_t fidgetAnim = mountAnims_.fidgets[dist(idleRng)]; - - characterRenderer->playAnimation(mountInstanceId_, fidgetAnim, false); - mountActiveFidget_ = fidgetAnim; - mountIdleFidgetTimer_ = 0.0f; - nextFidgetTime = std::uniform_real_distribution(6.0f, 12.0f)(idleRng); - - LOG_DEBUG("Mount idle fidget: playing anim ", fidgetAnim); - } - } - if (moving) { - mountIdleFidgetTimer_ = 0.0f; // Reset timer when moving - } - - // Idle ambient sounds: snorts and whinnies only, infrequent - if (!moving && getMountSoundManager()) { - mountIdleSoundTimer_ += lastDeltaTime_; - static std::mt19937 soundRng(std::random_device{}()); - static float nextIdleSoundTime = std::uniform_real_distribution(45.0f, 90.0f)(soundRng); - - if (mountIdleSoundTimer_ >= nextIdleSoundTime) { - getMountSoundManager()->playIdleSound(); - mountIdleSoundTimer_ = 0.0f; - nextIdleSoundTime = std::uniform_real_distribution(45.0f, 90.0f)(soundRng); - } - } else if (moving) { - mountIdleSoundTimer_ = 0.0f; // Reset timer when moving - } - - // Only update animation if it changed and we're not in an action sequence or playing a fidget - if (mountAction_ == MountAction::None && mountActiveFidget_ == 0 && (!haveMountState || curMountAnim != mountAnimId)) { - bool loop = true; // Normal movement animations loop - characterRenderer->playAnimation(mountInstanceId_, mountAnimId, loop); - } - - taxi_mount_done: - // Rider bob: sinusoidal motion synced to mount's run animation (only used in fallback positioning) - mountBob = 0.0f; - if (moving && haveMountState && curMountDur > 1.0f) { - // Wrap mount time preserving precision via subtraction instead of fmod - float wrappedTime = curMountTime; - while (wrappedTime >= curMountDur) { - wrappedTime -= curMountDur; - } - float norm = wrappedTime / curMountDur; - // One bounce per stride cycle - float bobSpeed = taxiFlight_ ? 2.0f : 1.0f; - mountBob = std::sin(norm * 2.0f * 3.14159f * bobSpeed) * 0.12f; - } - } - - // Use mount's attachment point for proper bone-driven rider positioning. - if (taxiFlight_) { - glm::mat4 mountSeatTransform(1.0f); - bool haveSeat = false; - static constexpr uint32_t kTaxiSeatAttachmentId = 0; // deterministic rider seat - if (mountSeatAttachmentId_ == -1) { - mountSeatAttachmentId_ = static_cast(kTaxiSeatAttachmentId); - } - if (mountSeatAttachmentId_ >= 0) { - haveSeat = characterRenderer->getAttachmentTransform( - mountInstanceId_, static_cast(mountSeatAttachmentId_), mountSeatTransform); - } - if (!haveSeat) { - mountSeatAttachmentId_ = -2; - } - - if (haveSeat) { - glm::vec3 targetRiderPos = glm::vec3(mountSeatTransform[3]) + glm::vec3(0.0f, 0.0f, 0.02f); - // Taxi passengers should be rigidly parented to mount attachment transforms. - // Smoothing here introduces visible seat lag/drift on turns. - mountSeatSmoothingInit_ = false; - smoothedMountSeatPos_ = targetRiderPos; - characterRenderer->setInstancePosition(characterInstanceId, targetRiderPos); - } else { - mountSeatSmoothingInit_ = false; - glm::vec3 playerPos = characterPosition + glm::vec3(0.0f, 0.0f, mountHeightOffset_ + 0.10f); - characterRenderer->setInstancePosition(characterInstanceId, playerPos); - } - - float riderPitch = mountPitch_ * 0.35f; - float riderRoll = mountRoll_ * 0.35f; - characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(riderPitch, riderRoll, mountYawRad)); - return; - } - - // Ground mounts: try a seat attachment first. - glm::mat4 mountSeatTransform; - bool haveSeat = false; - if (mountSeatAttachmentId_ >= 0) { - haveSeat = characterRenderer->getAttachmentTransform( - mountInstanceId_, static_cast(mountSeatAttachmentId_), mountSeatTransform); - } else if (mountSeatAttachmentId_ == -1) { - // Probe common rider seat attachment IDs once per mount. - static constexpr uint32_t kSeatAttachments[] = {0, 5, 6, 7, 8}; - for (uint32_t attId : kSeatAttachments) { - if (characterRenderer->getAttachmentTransform(mountInstanceId_, attId, mountSeatTransform)) { - mountSeatAttachmentId_ = static_cast(attId); - haveSeat = true; - break; - } - } - if (!haveSeat) { - mountSeatAttachmentId_ = -2; - } - } - - if (haveSeat) { - // Extract position from mount seat transform (attachment point already includes proper seat height) - glm::vec3 mountSeatPos = glm::vec3(mountSeatTransform[3]); - - // Keep seat offset minimal; large offsets amplify visible bobble. - glm::vec3 seatOffset = glm::vec3(0.0f, 0.0f, taxiFlight_ ? 0.04f : 0.08f); - glm::vec3 targetRiderPos = mountSeatPos + seatOffset; - // When moving, smoothing the seat position produces visible lag that looks like - // the rider sliding toward the rump. Anchor rigidly while moving. - if (moving) { - mountSeatSmoothingInit_ = false; - smoothedMountSeatPos_ = targetRiderPos; - } else if (!mountSeatSmoothingInit_) { - smoothedMountSeatPos_ = targetRiderPos; - mountSeatSmoothingInit_ = true; - } else { - float smoothHz = taxiFlight_ ? 10.0f : 14.0f; - float alpha = 1.0f - std::exp(-smoothHz * std::max(lastDeltaTime_, 0.001f)); - smoothedMountSeatPos_ = glm::mix(smoothedMountSeatPos_, targetRiderPos, alpha); - } - - // Position rider at mount seat - characterRenderer->setInstancePosition(characterInstanceId, smoothedMountSeatPos_); - - // Rider uses character facing yaw, not mount bone rotation - // (rider faces character direction, seat bone only provides position) - float yawRad = glm::radians(characterYaw); - float riderPitch = mountPitch_ * 0.35f; - float riderRoll = mountRoll_ * 0.35f; - characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(riderPitch, riderRoll, yawRad)); - } else { - // Fallback to old manual positioning if attachment not found - mountSeatSmoothingInit_ = false; - float yawRad = glm::radians(characterYaw); - glm::mat4 mountRotation = glm::mat4(1.0f); - mountRotation = glm::rotate(mountRotation, yawRad, glm::vec3(0.0f, 0.0f, 1.0f)); - mountRotation = glm::rotate(mountRotation, mountRoll_, glm::vec3(1.0f, 0.0f, 0.0f)); - mountRotation = glm::rotate(mountRotation, mountPitch_, glm::vec3(0.0f, 1.0f, 0.0f)); - glm::vec3 localOffset(0.0f, 0.0f, mountHeightOffset_ + mountBob); - glm::vec3 worldOffset = glm::vec3(mountRotation * glm::vec4(localOffset, 0.0f)); - glm::vec3 playerPos = characterPosition + worldOffset; - characterRenderer->setInstancePosition(characterInstanceId, playerPos); - characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(mountPitch_, mountRoll_, yawRad)); - } - return; - } - - if (!forceMelee) switch (charAnimState) { - case CharAnimState::IDLE: - if (swim) { - newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; - } else if (sitting && grounded) { - newState = CharAnimState::SIT_DOWN; - } else if (!grounded && jumping) { - newState = CharAnimState::JUMP_START; - } else if (!grounded) { - newState = CharAnimState::JUMP_MID; - } else if (moving && sprinting) { - newState = CharAnimState::RUN; - } else if (moving) { - newState = CharAnimState::WALK; - } else if (inCombat_ && grounded) { - newState = CharAnimState::COMBAT_IDLE; - } - break; - - case CharAnimState::WALK: - if (swim) { - newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; - } else if (!grounded && jumping) { - newState = CharAnimState::JUMP_START; - } else if (!grounded) { - newState = CharAnimState::JUMP_MID; - } else if (!moving) { - newState = CharAnimState::IDLE; - } else if (sprinting) { - newState = CharAnimState::RUN; - } - break; - - case CharAnimState::RUN: - if (swim) { - newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; - } else if (!grounded && jumping) { - newState = CharAnimState::JUMP_START; - } else if (!grounded) { - newState = CharAnimState::JUMP_MID; - } else if (!moving) { - newState = CharAnimState::IDLE; - } else if (!sprinting) { - newState = CharAnimState::WALK; - } - break; - - case CharAnimState::JUMP_START: - if (swim) { - newState = CharAnimState::SWIM_IDLE; - } else if (grounded) { - newState = CharAnimState::JUMP_END; - } else { - newState = CharAnimState::JUMP_MID; - } - break; - - case CharAnimState::JUMP_MID: - if (swim) { - newState = CharAnimState::SWIM_IDLE; - } else if (grounded) { - newState = CharAnimState::JUMP_END; - } - break; - - case CharAnimState::JUMP_END: - if (swim) { - newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; - } else if (moving && sprinting) { - newState = CharAnimState::RUN; - } else if (moving) { - newState = CharAnimState::WALK; - } else { - newState = CharAnimState::IDLE; - } - break; - - case CharAnimState::SIT_DOWN: - if (swim) { - newState = CharAnimState::SWIM_IDLE; - } else if (!sitting) { - newState = CharAnimState::IDLE; - } - break; - - case CharAnimState::SITTING: - if (swim) { - newState = CharAnimState::SWIM_IDLE; - } else if (!sitting) { - newState = CharAnimState::IDLE; - } - break; - - case CharAnimState::EMOTE: - if (swim) { - cancelEmote(); - newState = CharAnimState::SWIM_IDLE; - } else if (jumping || !grounded) { - cancelEmote(); - newState = CharAnimState::JUMP_START; - } else if (moving) { - cancelEmote(); - newState = sprinting ? CharAnimState::RUN : CharAnimState::WALK; - } else if (sitting) { - cancelEmote(); - newState = CharAnimState::SIT_DOWN; - } else if (!emoteLoop && characterRenderer && characterInstanceId > 0) { - // Auto-cancel non-looping emotes once animation completes - uint32_t curId = 0; float curT = 0.0f, curDur = 0.0f; - if (characterRenderer->getAnimationState(characterInstanceId, curId, curT, curDur) - && curDur > 0.1f && curT >= curDur - 0.05f) { - cancelEmote(); - newState = CharAnimState::IDLE; - } - } - break; - - case CharAnimState::SWIM_IDLE: - if (!swim) { - newState = moving ? CharAnimState::WALK : CharAnimState::IDLE; - } else if (moving) { - newState = CharAnimState::SWIM; - } - break; - - case CharAnimState::SWIM: - if (!swim) { - newState = moving ? CharAnimState::WALK : CharAnimState::IDLE; - } else if (!moving) { - newState = CharAnimState::SWIM_IDLE; - } - break; - - case CharAnimState::MELEE_SWING: - if (swim) { - newState = CharAnimState::SWIM_IDLE; - } else if (!grounded && jumping) { - newState = CharAnimState::JUMP_START; - } else if (!grounded) { - newState = CharAnimState::JUMP_MID; - } else if (moving && sprinting) { - newState = CharAnimState::RUN; - } else if (moving) { - newState = CharAnimState::WALK; - } else if (sitting) { - newState = CharAnimState::SIT_DOWN; - } else if (inCombat_) { - newState = CharAnimState::COMBAT_IDLE; - } else { - newState = CharAnimState::IDLE; - } - break; - - case CharAnimState::MOUNT: - // If we got here, the mount state was cleared externally but the - // animation state hasn't been reset yet. Fall back to normal logic. - if (swim) { - newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; - } else if (sitting && grounded) { - newState = CharAnimState::SIT_DOWN; - } else if (!grounded && jumping) { - newState = CharAnimState::JUMP_START; - } else if (!grounded) { - newState = CharAnimState::JUMP_MID; - } else if (moving && sprinting) { - newState = CharAnimState::RUN; - } else if (moving) { - newState = CharAnimState::WALK; - } else { - newState = CharAnimState::IDLE; - } - break; - - case CharAnimState::COMBAT_IDLE: - if (swim) { - newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; - } else if (!grounded && jumping) { - newState = CharAnimState::JUMP_START; - } else if (!grounded) { - newState = CharAnimState::JUMP_MID; - } else if (moving && sprinting) { - newState = CharAnimState::RUN; - } else if (moving) { - newState = CharAnimState::WALK; - } else if (!inCombat_) { - newState = CharAnimState::IDLE; - } - break; - - case CharAnimState::CHARGE: - // Stay in CHARGE until charging_ is cleared - break; - } - - if (forceMelee) { - newState = CharAnimState::MELEE_SWING; - } - - if (charging_) { - newState = CharAnimState::CHARGE; - } - - if (newState != charAnimState) { - charAnimState = newState; - } - - auto pickFirstAvailable = [&](std::initializer_list candidates, uint32_t fallback) -> uint32_t { - for (uint32_t id : candidates) { - if (characterRenderer->hasAnimation(characterInstanceId, id)) { - return id; - } - } - return fallback; - }; - - uint32_t animId = ANIM_STAND; - bool loop = true; - - switch (charAnimState) { - case CharAnimState::IDLE: animId = ANIM_STAND; loop = true; break; - case CharAnimState::WALK: - if (movingBackward) { - animId = pickFirstAvailable({ANIM_BACKPEDAL}, ANIM_WALK); - } else if (anyStrafeLeft) { - animId = pickFirstAvailable({ANIM_STRAFE_WALK_LEFT, ANIM_STRAFE_RUN_LEFT}, ANIM_WALK); - } else if (anyStrafeRight) { - animId = pickFirstAvailable({ANIM_STRAFE_WALK_RIGHT, ANIM_STRAFE_RUN_RIGHT}, ANIM_WALK); - } else { - animId = pickFirstAvailable({ANIM_WALK, ANIM_RUN}, ANIM_STAND); - } - loop = true; - break; - case CharAnimState::RUN: - if (movingBackward) { - animId = pickFirstAvailable({ANIM_BACKPEDAL}, ANIM_WALK); - } else if (anyStrafeLeft) { - animId = pickFirstAvailable({ANIM_STRAFE_RUN_LEFT}, ANIM_RUN); - } else if (anyStrafeRight) { - animId = pickFirstAvailable({ANIM_STRAFE_RUN_RIGHT}, ANIM_RUN); - } else { - animId = pickFirstAvailable({ANIM_RUN, ANIM_WALK}, ANIM_STAND); - } - loop = true; - break; - case CharAnimState::JUMP_START: animId = ANIM_JUMP_START; loop = false; break; - case CharAnimState::JUMP_MID: animId = ANIM_JUMP_MID; loop = false; break; - case CharAnimState::JUMP_END: animId = ANIM_JUMP_END; loop = false; break; - case CharAnimState::SIT_DOWN: animId = ANIM_SIT_DOWN; loop = false; break; - case CharAnimState::SITTING: animId = ANIM_SITTING; loop = true; break; - case CharAnimState::EMOTE: animId = emoteAnimId; loop = emoteLoop; break; - case CharAnimState::SWIM_IDLE: animId = ANIM_SWIM_IDLE; loop = true; break; - case CharAnimState::SWIM: animId = ANIM_SWIM; loop = true; break; - case CharAnimState::MELEE_SWING: - animId = resolveMeleeAnimId(); - if (animId == 0) { - animId = ANIM_STAND; - } - loop = false; - break; - case CharAnimState::MOUNT: animId = ANIM_MOUNT; loop = true; break; - case CharAnimState::COMBAT_IDLE: - animId = pickFirstAvailable( - {ANIM_READY_1H, ANIM_READY_2H, ANIM_READY_2H_L, ANIM_READY_UNARMED}, - ANIM_STAND); - loop = true; - break; - case CharAnimState::CHARGE: - animId = ANIM_RUN; - loop = true; - break; - } - - uint32_t currentAnimId = 0; - float currentAnimTimeMs = 0.0f; - float currentAnimDurationMs = 0.0f; - bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs); - // Some frames may transiently fail getAnimationState() while resources/instance state churn. - // Avoid reissuing the same clip on those frames, which restarts locomotion and causes hitches. - const bool requestChanged = (lastPlayerAnimRequest_ != animId) || (lastPlayerAnimLoopRequest_ != loop); - const bool shouldPlay = (haveState && currentAnimId != animId) || (!haveState && requestChanged); - if (shouldPlay) { - characterRenderer->playAnimation(characterInstanceId, animId, loop); - lastPlayerAnimRequest_ = animId; - lastPlayerAnimLoopRequest_ = loop; - } -} void Renderer::playEmote(const std::string& emoteName) { - loadEmotesFromDbc(); - auto it = EMOTE_TABLE.find(emoteName); - if (it == EMOTE_TABLE.end()) return; - - const auto& info = it->second; - if (info.animId == 0) return; - emoteActive = true; - emoteAnimId = info.animId; - emoteLoop = info.loop; - charAnimState = CharAnimState::EMOTE; - - if (characterRenderer && characterInstanceId > 0) { - characterRenderer->playAnimation(characterInstanceId, emoteAnimId, emoteLoop); - } + if (animationController_) animationController_->playEmote(emoteName); } void Renderer::cancelEmote() { - emoteActive = false; - emoteAnimId = 0; - emoteLoop = false; + if (animationController_) animationController_->cancelEmote(); +} + +bool Renderer::isEmoteActive() const { + return animationController_ && animationController_->isEmoteActive(); +} + +void Renderer::setInCombat(bool combat) { + if (animationController_) animationController_->setInCombat(combat); +} + +void Renderer::setEquippedWeaponType(uint32_t inventoryType) { + if (animationController_) animationController_->setEquippedWeaponType(inventoryType); +} + +void Renderer::setCharging(bool c) { + if (animationController_) animationController_->setCharging(c); +} + +bool Renderer::isCharging() const { + return animationController_ && animationController_->isCharging(); +} + +void Renderer::setTaxiFlight(bool taxi) { + if (animationController_) animationController_->setTaxiFlight(taxi); +} + +void Renderer::setMountPitchRoll(float pitch, float roll) { + if (animationController_) animationController_->setMountPitchRoll(pitch, roll); +} + +bool Renderer::isMounted() const { + return animationController_ && animationController_->isMounted(); } bool Renderer::captureScreenshot(const std::string& outputPath) { @@ -2374,56 +1133,19 @@ bool Renderer::captureScreenshot(const std::string& outputPath) { } void Renderer::triggerLevelUpEffect(const glm::vec3& position) { - if (!levelUpEffect) return; - - // Lazy-load the M2 model on first trigger - if (!levelUpEffect->isModelLoaded() && m2Renderer) { - if (!cachedAssetManager) { - cachedAssetManager = core::Application::getInstance().getAssetManager(); - } - if (!cachedAssetManager) { - LOG_WARNING("LevelUpEffect: no asset manager available"); - } else { - auto m2Data = cachedAssetManager->readFile("Spells\\LevelUp\\LevelUp.m2"); - auto skinData = cachedAssetManager->readFile("Spells\\LevelUp\\LevelUp00.skin"); - LOG_INFO("LevelUpEffect: m2Data=", m2Data.size(), " skinData=", skinData.size()); - if (!m2Data.empty()) { - levelUpEffect->loadModel(m2Renderer.get(), m2Data, skinData); - } else { - LOG_WARNING("LevelUpEffect: failed to read Spell\\LevelUp\\LevelUp.m2"); - } - } - } - - levelUpEffect->trigger(position); + if (animationController_) animationController_->triggerLevelUpEffect(position); } void Renderer::startChargeEffect(const glm::vec3& position, const glm::vec3& direction) { - if (!chargeEffect) return; - - // Lazy-load M2 models on first use - if (!chargeEffect->isActive() && m2Renderer) { - if (!cachedAssetManager) { - cachedAssetManager = core::Application::getInstance().getAssetManager(); - } - if (cachedAssetManager) { - chargeEffect->tryLoadM2Models(m2Renderer.get(), cachedAssetManager); - } - } - - chargeEffect->start(position, direction); + if (animationController_) animationController_->startChargeEffect(position, direction); } void Renderer::emitChargeEffect(const glm::vec3& position, const glm::vec3& direction) { - if (chargeEffect) { - chargeEffect->emit(position, direction); - } + if (animationController_) animationController_->emitChargeEffect(position, direction); } void Renderer::stopChargeEffect() { - if (chargeEffect) { - chargeEffect->stop(); - } + if (animationController_) animationController_->stopChargeEffect(); } // ─── Spell Visual Effects — delegated to SpellVisualSystem (§4.4) ──────────── @@ -2434,102 +1156,32 @@ void Renderer::playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition } void Renderer::triggerMeleeSwing() { - if (!characterRenderer || characterInstanceId == 0) return; - if (meleeSwingCooldown > 0.0f) return; - if (emoteActive) { - cancelEmote(); - } - resolveMeleeAnimId(); - meleeSwingCooldown = 0.1f; - float durationSec = meleeAnimDurationMs > 0.0f ? meleeAnimDurationMs / 1000.0f : 0.6f; - if (durationSec < 0.25f) durationSec = 0.25f; - if (durationSec > 1.0f) durationSec = 1.0f; - meleeSwingTimer = durationSec; - if (getActivitySoundManager()) { - getActivitySoundManager()->playMeleeSwing(); - } + if (animationController_) animationController_->triggerMeleeSwing(); } std::string Renderer::getEmoteText(const std::string& emoteName, const std::string* targetName) { - loadEmotesFromDbc(); - auto it = EMOTE_TABLE.find(emoteName); - if (it != EMOTE_TABLE.end()) { - const auto& info = it->second; - const std::string& base = (targetName ? info.textTarget : info.textNoTarget); - if (!base.empty()) { - return replacePlaceholders(base, targetName); - } - if (targetName && !targetName->empty()) { - return "You " + info.command + " at " + *targetName + "."; - } - return "You " + info.command + "."; - } - return ""; + return AnimationController::getEmoteText(emoteName, targetName); } uint32_t Renderer::getEmoteDbcId(const std::string& emoteName) { - loadEmotesFromDbc(); - auto it = EMOTE_TABLE.find(emoteName); - if (it != EMOTE_TABLE.end()) { - return it->second.dbcId; - } - return 0; + return AnimationController::getEmoteDbcId(emoteName); } std::string Renderer::getEmoteTextByDbcId(uint32_t dbcId, const std::string& senderName, const std::string* targetName) { - loadEmotesFromDbc(); - auto it = EMOTE_BY_DBCID.find(dbcId); - if (it == EMOTE_BY_DBCID.end()) return ""; - - const EmoteInfo& info = *it->second; - - // Use "others see" text templates: "%s dances." / "%s dances with %s." - if (targetName && !targetName->empty()) { - if (!info.othersTarget.empty()) { - // Replace first %s with sender, second %s with target - std::string out; - out.reserve(info.othersTarget.size() + senderName.size() + targetName->size()); - bool firstReplaced = false; - for (size_t i = 0; i < info.othersTarget.size(); ++i) { - if (info.othersTarget[i] == '%' && i + 1 < info.othersTarget.size() && info.othersTarget[i + 1] == 's') { - out += firstReplaced ? *targetName : senderName; - firstReplaced = true; - ++i; - } else { - out.push_back(info.othersTarget[i]); - } - } - return out; - } - return senderName + " " + info.command + "s at " + *targetName + "."; - } else { - if (!info.othersNoTarget.empty()) { - return replacePlaceholders(info.othersNoTarget, &senderName); - } - return senderName + " " + info.command + "s."; - } + return AnimationController::getEmoteTextByDbcId(dbcId, senderName, targetName); } uint32_t Renderer::getEmoteAnimByDbcId(uint32_t dbcId) { - loadEmotesFromDbc(); - auto it = EMOTE_BY_DBCID.find(dbcId); - if (it != EMOTE_BY_DBCID.end()) { - return it->second->animId; - } - return 0; + return AnimationController::getEmoteAnimByDbcId(dbcId); } void Renderer::setTargetPosition(const glm::vec3* pos) { - targetPosition = pos; + if (animationController_) animationController_->setTargetPosition(pos); } void Renderer::resetCombatVisualState() { - inCombat_ = false; - targetPosition = nullptr; - meleeSwingTimer = 0.0f; - meleeSwingCooldown = 0.0f; - // Clear lingering spell visual instances from the previous map/combat session. + if (animationController_) animationController_->resetCombatVisualState(); if (spellVisualSystem_) spellVisualSystem_->reset(); } @@ -2537,110 +1189,6 @@ bool Renderer::isMoving() const { return cameraController && cameraController->isMoving(); } -bool Renderer::isFootstepAnimationState() const { - return charAnimState == CharAnimState::WALK || charAnimState == CharAnimState::RUN; -} - -bool Renderer::shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs) { - if (animationDurationMs <= 1.0f) { - footstepNormInitialized = false; - return false; - } - - // Wrap animation time preserving precision via subtraction instead of fmod - float wrappedTime = animationTimeMs; - while (wrappedTime >= animationDurationMs) { - wrappedTime -= animationDurationMs; - } - if (wrappedTime < 0.0f) wrappedTime += animationDurationMs; - float norm = wrappedTime / animationDurationMs; - - if (animationId != footstepLastAnimationId) { - footstepLastAnimationId = animationId; - footstepLastNormTime = norm; - footstepNormInitialized = true; - return false; - } - - if (!footstepNormInitialized) { - footstepNormInitialized = true; - footstepLastNormTime = norm; - return false; - } - - auto crossed = [&](float eventNorm) { - if (footstepLastNormTime <= norm) { - return footstepLastNormTime < eventNorm && eventNorm <= norm; - } - return footstepLastNormTime < eventNorm || eventNorm <= norm; - }; - - bool trigger = crossed(0.22f) || crossed(0.72f); - footstepLastNormTime = norm; - return trigger; -} - -audio::FootstepSurface Renderer::resolveFootstepSurface() const { - if (!cameraController || !cameraController->isThirdPerson()) { - return audio::FootstepSurface::STONE; - } - - const glm::vec3& p = characterPosition; - - // Cache footstep surface to avoid expensive queries every step - // Only update if moved >1.5 units or timer expired (0.5s) - float distSq = glm::dot(p - cachedFootstepPosition, p - cachedFootstepPosition); - if (distSq < 2.25f && cachedFootstepUpdateTimer < 0.5f) { - return cachedFootstepSurface; - } - - // Update cache - cachedFootstepPosition = p; - cachedFootstepUpdateTimer = 0.0f; - - if (cameraController->isSwimming()) { - cachedFootstepSurface = audio::FootstepSurface::WATER; - return audio::FootstepSurface::WATER; - } - - if (waterRenderer) { - auto waterH = waterRenderer->getWaterHeightAt(p.x, p.y); - if (waterH && p.z < (*waterH + 0.25f)) { - cachedFootstepSurface = audio::FootstepSurface::WATER; - return audio::FootstepSurface::WATER; - } - } - - if (wmoRenderer) { - auto wmoFloor = wmoRenderer->getFloorHeight(p.x, p.y, p.z + 1.5f); - auto terrainFloor = terrainManager ? terrainManager->getHeightAt(p.x, p.y) : std::nullopt; - if (wmoFloor && (!terrainFloor || *wmoFloor >= *terrainFloor - 0.1f)) { - cachedFootstepSurface = audio::FootstepSurface::STONE; - return audio::FootstepSurface::STONE; - } - } - - // Determine surface type (expensive - only done when cache needs update) - audio::FootstepSurface surface = audio::FootstepSurface::STONE; - - if (terrainManager) { - auto texture = terrainManager->getDominantTextureAt(p.x, p.y); - if (texture) { - std::string t = *texture; - for (char& c : t) c = static_cast(std::tolower(static_cast(c))); - if (t.find("snow") != std::string::npos || t.find("ice") != std::string::npos) surface = audio::FootstepSurface::SNOW; - else if (t.find("grass") != std::string::npos || t.find("moss") != std::string::npos || t.find("leaf") != std::string::npos) surface = audio::FootstepSurface::GRASS; - else if (t.find("sand") != std::string::npos || t.find("dirt") != std::string::npos || t.find("mud") != std::string::npos) surface = audio::FootstepSurface::DIRT; - else if (t.find("wood") != std::string::npos || t.find("timber") != std::string::npos) surface = audio::FootstepSurface::WOOD; - else if (t.find("metal") != std::string::npos || t.find("iron") != std::string::npos) surface = audio::FootstepSurface::METAL; - else if (t.find("stone") != std::string::npos || t.find("rock") != std::string::npos || t.find("cobble") != std::string::npos || t.find("brick") != std::string::npos) surface = audio::FootstepSurface::STONE; - } - } - - cachedFootstepSurface = surface; - return surface; -} - void Renderer::update(float deltaTime) { globalTime += deltaTime; if (musicSwitchCooldown_ > 0.0f) { @@ -2649,7 +1197,7 @@ void Renderer::update(float deltaTime) { runDeferredWorldInitStep(deltaTime); auto updateStart = std::chrono::steady_clock::now(); - lastDeltaTime_ = deltaTime; // Cache for use in updateCharacterAnimation() + lastDeltaTime_ = deltaTime; if (wmoRenderer) wmoRenderer->resetQueryStats(); if (m2Renderer) m2Renderer->resetQueryStats(); @@ -2675,7 +1223,7 @@ void Renderer::update(float deltaTime) { // Visibility hardening: ensure player instance cannot stay hidden after // taxi/camera transitions, but preserve first-person self-hide. if (characterRenderer && characterInstanceId > 0 && cameraController) { - if ((cameraController->isThirdPerson() && !cameraController->isFirstPersonView()) || taxiFlight_) { + if ((cameraController->isThirdPerson() && !cameraController->isFirstPersonView()) || (animationController_ && animationController_->isTaxiFlight())) { characterRenderer->setInstanceVisible(characterInstanceId, true); } } @@ -2720,25 +1268,17 @@ void Renderer::update(float deltaTime) { // Sync character model position/rotation and animation with follow target if (characterInstanceId > 0 && characterRenderer && cameraController) { - if (meleeSwingCooldown > 0.0f) { - meleeSwingCooldown = std::max(0.0f, meleeSwingCooldown - deltaTime); - } - if (meleeSwingTimer > 0.0f) { - meleeSwingTimer = std::max(0.0f, meleeSwingTimer - deltaTime); - } - characterRenderer->setInstancePosition(characterInstanceId, characterPosition); // Movement-facing comes from camera controller and is decoupled from LMB orbit. - // During taxi flights, orientation is controlled by the flight path (not player input) - if (taxiFlight_) { - // Taxi flight: use orientation from flight path + bool taxiFlight = animationController_ && animationController_->isTaxiFlight(); + if (taxiFlight) { characterYaw = cameraController->getFacingYaw(); } else if (cameraController->isMoving() || cameraController->isRightMouseHeld()) { characterYaw = cameraController->getFacingYaw(); - } else if (inCombat_ && targetPosition && !emoteActive && !isMounted()) { - // Face target when in combat and idle - glm::vec3 toTarget = *targetPosition - characterPosition; + } else if (animationController_ && animationController_->isInCombat() && + animationController_->getTargetPosition() && !animationController_->isEmoteActive() && !isMounted()) { + glm::vec3 toTarget = *animationController_->getTargetPosition() - characterPosition; if (toTarget.x * toTarget.x + toTarget.y * toTarget.y > 0.01f) { float targetYaw = glm::degrees(std::atan2(toTarget.y, toTarget.x)); float diff = targetYaw - characterYaw; @@ -2755,8 +1295,12 @@ void Renderer::update(float deltaTime) { float yawRad = glm::radians(characterYaw); characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(0.0f, 0.0f, yawRad)); - // Update animation based on movement state - updateCharacterAnimation(); + // Update animation based on movement state (delegated to AnimationController §4.2) + if (animationController_) { + animationController_->updateMeleeTimers(deltaTime); + animationController_->setDeltaTime(deltaTime); + animationController_->updateCharacterAnimation(); + } } // Update terrain streaming @@ -2795,7 +1339,7 @@ void Renderer::update(float deltaTime) { mountDust->update(deltaTime); // Spawn dust when mounted and moving on ground - if (isMounted() && camera && cameraController && !taxiFlight_) { + if (isMounted() && camera && cameraController && !(animationController_ && animationController_->isTaxiFlight())) { bool isMoving = cameraController->isMoving(); bool onGround = cameraController->isGrounded(); @@ -2807,7 +1351,8 @@ void Renderer::update(float deltaTime) { velocity.z = 0.0f; // Ignore vertical component // Spawn dust at mount's feet (slightly below character position) - glm::vec3 dustPos = characterPosition - glm::vec3(0.0f, 0.0f, mountHeightOffset_ * 0.8f); + float mho = animationController_ ? animationController_->getMountHeightOffset() : 0.0f; + glm::vec3 dustPos = characterPosition - glm::vec3(0.0f, 0.0f, mho * 0.8f); mountDust->spawnDust(dustPos, velocity, isMoving); } } @@ -2846,144 +1391,11 @@ void Renderer::update(float deltaTime) { // Update AudioEngine (cleanup finished sounds, etc.) audio::AudioEngine::instance().update(deltaTime); - // Footsteps: animation-event driven + surface query at event time. - if (getFootstepManager()) { - getFootstepManager()->update(deltaTime); - cachedFootstepUpdateTimer += deltaTime; // Update surface cache timer - bool canPlayFootsteps = characterRenderer && characterInstanceId > 0 && - cameraController && cameraController->isThirdPerson() && - cameraController->isGrounded() && !cameraController->isSwimming(); + // Footsteps: delegated to AnimationController (§4.2) + if (animationController_) animationController_->updateFootsteps(deltaTime); - if (canPlayFootsteps && isMounted() && mountInstanceId_ > 0 && !taxiFlight_) { - // Mount footsteps: use mount's animation for timing - uint32_t animId = 0; - float animTimeMs = 0.0f, animDurationMs = 0.0f; - if (characterRenderer->getAnimationState(mountInstanceId_, animId, animTimeMs, animDurationMs) && - animDurationMs > 1.0f && cameraController->isMoving()) { - // Wrap animation time preserving precision via subtraction instead of fmod - float wrappedTime = animTimeMs; - while (wrappedTime >= animDurationMs) { - wrappedTime -= animDurationMs; - } - if (wrappedTime < 0.0f) wrappedTime += animDurationMs; - float norm = wrappedTime / animDurationMs; - - if (animId != mountFootstepLastAnimId) { - mountFootstepLastAnimId = animId; - mountFootstepLastNormTime = norm; - mountFootstepNormInitialized = true; - } else if (!mountFootstepNormInitialized) { - mountFootstepNormInitialized = true; - mountFootstepLastNormTime = norm; - } else { - // Mount gait: 2 hoofbeats per cycle (synced with animation) - auto crossed = [&](float eventNorm) { - if (mountFootstepLastNormTime <= norm) { - return mountFootstepLastNormTime < eventNorm && eventNorm <= norm; - } - return mountFootstepLastNormTime < eventNorm || eventNorm <= norm; - }; - if (crossed(0.25f) || crossed(0.75f)) { - getFootstepManager()->playFootstep(resolveFootstepSurface(), true); - } - mountFootstepLastNormTime = norm; - } - } else { - mountFootstepNormInitialized = false; - } - footstepNormInitialized = false; // Reset player footstep tracking - } else if (canPlayFootsteps && isFootstepAnimationState()) { - uint32_t animId = 0; - float animTimeMs = 0.0f; - float animDurationMs = 0.0f; - if (characterRenderer->getAnimationState(characterInstanceId, animId, animTimeMs, animDurationMs) && - shouldTriggerFootstepEvent(animId, animTimeMs, animDurationMs)) { - auto surface = resolveFootstepSurface(); - getFootstepManager()->playFootstep(surface, cameraController->isSprinting()); - // Play additional splash sound and spawn foot splash particles when wading - if (surface == audio::FootstepSurface::WATER) { - if (getMovementSoundManager()) { - getMovementSoundManager()->playWaterFootstep(audio::MovementSoundManager::CharacterSize::MEDIUM); - } - if (swimEffects && waterRenderer) { - auto wh = waterRenderer->getWaterHeightAt(characterPosition.x, characterPosition.y); - if (wh) { - swimEffects->spawnFootSplash(characterPosition, *wh); - } - } - } - } - mountFootstepNormInitialized = false; - } else { - footstepNormInitialized = false; - mountFootstepNormInitialized = false; - } - } - - // Activity SFX: animation/state-driven jump, landing, and swim loops/splashes. - if (getActivitySoundManager()) { - getActivitySoundManager()->update(deltaTime); - if (cameraController && cameraController->isThirdPerson()) { - bool grounded = cameraController->isGrounded(); - bool jumping = cameraController->isJumping(); - bool falling = cameraController->isFalling(); - bool swimming = cameraController->isSwimming(); - bool moving = cameraController->isMoving(); - - if (!sfxStateInitialized) { - sfxPrevGrounded = grounded; - sfxPrevJumping = jumping; - sfxPrevFalling = falling; - sfxPrevSwimming = swimming; - sfxStateInitialized = true; - } - - if (jumping && !sfxPrevJumping && !swimming) { - getActivitySoundManager()->playJump(); - } - - if (grounded && !sfxPrevGrounded) { - bool hardLanding = sfxPrevFalling; - getActivitySoundManager()->playLanding(resolveFootstepSurface(), hardLanding); - } - - if (swimming && !sfxPrevSwimming) { - getActivitySoundManager()->playWaterEnter(); - } else if (!swimming && sfxPrevSwimming) { - getActivitySoundManager()->playWaterExit(); - } - - getActivitySoundManager()->setSwimmingState(swimming, moving); - - // Fade music underwater - if (getMusicManager()) { - getMusicManager()->setUnderwaterMode(swimming); - } - - sfxPrevGrounded = grounded; - sfxPrevJumping = jumping; - sfxPrevFalling = falling; - sfxPrevSwimming = swimming; - } else { - getActivitySoundManager()->setSwimmingState(false, false); - // Restore music volume when activity sounds disabled - if (getMusicManager()) { - getMusicManager()->setUnderwaterMode(false); - } - sfxStateInitialized = false; - } - } - - // Mount ambient sounds: wing flaps, breathing, etc. - if (getMountSoundManager()) { - getMountSoundManager()->update(deltaTime); - if (cameraController && isMounted()) { - bool moving = cameraController->isMoving(); - bool flying = taxiFlight_ || !cameraController->isGrounded(); // Flying if taxi or airborne - getMountSoundManager()->setMoving(moving); - getMountSoundManager()->setFlying(flying); - } - } + // Activity SFX + mount ambient sounds: delegated to AnimationController (§4.2) + if (animationController_) animationController_->updateSfxState(deltaTime); const bool canQueryWmo = (camera && wmoRenderer); const glm::vec3 camPos = camera ? camera->getPosition() : glm::vec3(0.0f); @@ -2993,7 +1405,7 @@ void Renderer::update(float deltaTime) { playerIndoors_ = insideWmo; // Ambient environmental sounds: fireplaces, water, birds, etc. - if (getAmbientSoundManager() && camera && wmoRenderer && cameraController) { + if (audioCoordinator_->getAmbientSoundManager() && camera && wmoRenderer && cameraController) { bool isIndoor = insideWmo; bool isSwimming = cameraController->isSwimming(); @@ -3027,10 +1439,10 @@ void Renderer::update(float deltaTime) { } } - getAmbientSoundManager()->setWeather(audioWeatherType); + audioCoordinator_->getAmbientSoundManager()->setWeather(audioWeatherType); } - getAmbientSoundManager()->update(deltaTime, camPos, isIndoor, isSwimming, isBlacksmith); + audioCoordinator_->getAmbientSoundManager()->update(deltaTime, camPos, isIndoor, isSwimming, isBlacksmith); } // Wait for M2 doodad animation to finish (was launched earlier in parallel with character anim) @@ -3043,14 +1455,14 @@ void Renderer::update(float deltaTime) { auto playZoneMusic = [&](const std::string& music) { if (music.empty()) return; if (music.rfind("file:", 0) == 0) { - getMusicManager()->crossfadeToFile(music.substr(5)); + audioCoordinator_->getMusicManager()->crossfadeToFile(music.substr(5)); } else { - getMusicManager()->crossfadeTo(music); + audioCoordinator_->getMusicManager()->crossfadeTo(music); } }; // Update zone detection and music - if (zoneManager && getMusicManager() && terrainManager && camera) { + if (zoneManager && audioCoordinator_->getMusicManager() && terrainManager && camera) { // Prefer server-authoritative zone ID (from SMSG_INIT_WORLD_STATES); // fall back to tile-based lookup for single-player / offline mode. const auto* gh = core::Application::getInstance().getGameHandler(); @@ -3115,7 +1527,7 @@ void Renderer::update(float deltaTime) { if (!inTavern_ && !tavernMusic.empty()) { inTavern_ = true; LOG_INFO("Entered tavern"); - getMusicManager()->playMusic(tavernMusic, true); // Immediate playback, looping + audioCoordinator_->getMusicManager()->playMusic(tavernMusic, true); // Immediate playback, looping musicSwitchCooldown_ = 6.0f; } } else if (inTavern_) { @@ -3137,7 +1549,7 @@ void Renderer::update(float deltaTime) { if (!inBlacksmith_) { inBlacksmith_ = true; LOG_INFO("Entered blacksmith - stopping music"); - getMusicManager()->stopMusic(); + audioCoordinator_->getMusicManager()->stopMusic(); } } else if (inBlacksmith_) { // Exited blacksmith - restore zone music with crossfade @@ -3169,15 +1581,15 @@ void Renderer::update(float deltaTime) { } } // Update ambient sound manager zone type - if (getAmbientSoundManager()) { - getAmbientSoundManager()->setZoneId(zoneId); + if (audioCoordinator_->getAmbientSoundManager()) { + audioCoordinator_->getAmbientSoundManager()->setZoneId(zoneId); } } - getMusicManager()->update(deltaTime); + audioCoordinator_->getMusicManager()->update(deltaTime); // When a track finishes, pick a new random track from the current zone - if (!getMusicManager()->isPlaying() && !inTavern_ && !inBlacksmith_ && + if (!audioCoordinator_->getMusicManager()->isPlaying() && !inTavern_ && !inBlacksmith_ && currentZoneId != 0 && musicSwitchCooldown_ <= 0.0f) { std::string music = zoneManager->getRandomMusic(currentZoneId); if (!music.empty()) { @@ -3218,24 +1630,24 @@ void Renderer::runDeferredWorldInitStep(float deltaTime) { switch (deferredWorldInitStage_) { case 0: - if (getAmbientSoundManager()) { - getAmbientSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getAmbientSoundManager()) { + audioCoordinator_->getAmbientSoundManager()->initialize(cachedAssetManager); } - if (terrainManager && getAmbientSoundManager()) { - terrainManager->setAmbientSoundManager(getAmbientSoundManager()); + if (terrainManager && audioCoordinator_->getAmbientSoundManager()) { + terrainManager->setAmbientSoundManager(audioCoordinator_->getAmbientSoundManager()); } break; case 1: - if (getUiSoundManager()) getUiSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getUiSoundManager()) audioCoordinator_->getUiSoundManager()->initialize(cachedAssetManager); break; case 2: - if (getCombatSoundManager()) getCombatSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getCombatSoundManager()) audioCoordinator_->getCombatSoundManager()->initialize(cachedAssetManager); break; case 3: - if (getSpellSoundManager()) getSpellSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getSpellSoundManager()) audioCoordinator_->getSpellSoundManager()->initialize(cachedAssetManager); break; case 4: - if (getMovementSoundManager()) getMovementSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getMovementSoundManager()) audioCoordinator_->getMovementSoundManager()->initialize(cachedAssetManager); break; case 5: if (questMarkerRenderer) questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, cachedAssetManager); @@ -4011,6 +2423,12 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s } } + // Initialize AnimationController (§4.2) + if (!animationController_) { + animationController_ = std::make_unique(); + animationController_->initialize(this); + } + // Create and initialize terrain manager if (!terrainManager) { terrainManager = std::make_unique(); @@ -4032,8 +2450,8 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s terrainManager->setWMORenderer(wmoRenderer.get()); } // Set ambient sound manager for environmental audio emitters - if (getAmbientSoundManager()) { - terrainManager->setAmbientSoundManager(getAmbientSoundManager()); + if (audioCoordinator_->getAmbientSoundManager()) { + terrainManager->setAmbientSoundManager(audioCoordinator_->getAmbientSoundManager()); } // Pass asset manager to character renderer for texture loading if (characterRenderer) { @@ -4064,36 +2482,36 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s if (worldMap) worldMap->setMapName(mapName); // Initialize audio managers - if (getMusicManager() && assetManager && !cachedAssetManager) { + if (audioCoordinator_->getMusicManager() && assetManager && !cachedAssetManager) { audio::AudioEngine::instance().setAssetManager(assetManager); - getMusicManager()->initialize(assetManager); - if (getFootstepManager()) { - getFootstepManager()->initialize(assetManager); + audioCoordinator_->getMusicManager()->initialize(assetManager); + if (audioCoordinator_->getFootstepManager()) { + audioCoordinator_->getFootstepManager()->initialize(assetManager); } - if (getActivitySoundManager()) { - getActivitySoundManager()->initialize(assetManager); + if (audioCoordinator_->getActivitySoundManager()) { + audioCoordinator_->getActivitySoundManager()->initialize(assetManager); } - if (getMountSoundManager()) { - getMountSoundManager()->initialize(assetManager); + if (audioCoordinator_->getMountSoundManager()) { + audioCoordinator_->getMountSoundManager()->initialize(assetManager); } - if (getNpcVoiceManager()) { - getNpcVoiceManager()->initialize(assetManager); + if (audioCoordinator_->getNpcVoiceManager()) { + audioCoordinator_->getNpcVoiceManager()->initialize(assetManager); } if (!deferredWorldInitEnabled_) { - if (getAmbientSoundManager()) { - getAmbientSoundManager()->initialize(assetManager); + if (audioCoordinator_->getAmbientSoundManager()) { + audioCoordinator_->getAmbientSoundManager()->initialize(assetManager); } - if (getUiSoundManager()) { - getUiSoundManager()->initialize(assetManager); + if (audioCoordinator_->getUiSoundManager()) { + audioCoordinator_->getUiSoundManager()->initialize(assetManager); } - if (getCombatSoundManager()) { - getCombatSoundManager()->initialize(assetManager); + if (audioCoordinator_->getCombatSoundManager()) { + audioCoordinator_->getCombatSoundManager()->initialize(assetManager); } - if (getSpellSoundManager()) { - getSpellSoundManager()->initialize(assetManager); + if (audioCoordinator_->getSpellSoundManager()) { + audioCoordinator_->getSpellSoundManager()->initialize(assetManager); } - if (getMovementSoundManager()) { - getMovementSoundManager()->initialize(assetManager); + if (audioCoordinator_->getMovementSoundManager()) { + audioCoordinator_->getMovementSoundManager()->initialize(assetManager); } if (questMarkerRenderer) { questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, assetManager); @@ -4102,7 +2520,7 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s if (envFlagEnabled("WOWEE_PREWARM_ZONE_MUSIC", false)) { if (zoneManager) { for (const auto& musicPath : zoneManager->getAllMusicPaths()) { - getMusicManager()->preloadMusic(musicPath); + audioCoordinator_->getMusicManager()->preloadMusic(musicPath); } } static const std::vector tavernTracks = { @@ -4112,7 +2530,7 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s "Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern2A.mp3", }; for (const auto& musicPath : tavernTracks) { - getMusicManager()->preloadMusic(musicPath); + audioCoordinator_->getMusicManager()->preloadMusic(musicPath); } } } else { @@ -4251,42 +2669,42 @@ bool Renderer::loadTerrainArea(const std::string& mapName, int centerX, int cent } // Initialize music manager with asset manager - if (getMusicManager() && cachedAssetManager) { - if (!getMusicManager()->isInitialized()) { - getMusicManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getMusicManager() && cachedAssetManager) { + if (!audioCoordinator_->getMusicManager()->isInitialized()) { + audioCoordinator_->getMusicManager()->initialize(cachedAssetManager); } } - if (getFootstepManager() && cachedAssetManager) { - if (!getFootstepManager()->isInitialized()) { - getFootstepManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getFootstepManager() && cachedAssetManager) { + if (!audioCoordinator_->getFootstepManager()->isInitialized()) { + audioCoordinator_->getFootstepManager()->initialize(cachedAssetManager); } } - if (getActivitySoundManager() && cachedAssetManager) { - if (!getActivitySoundManager()->isInitialized()) { - getActivitySoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getActivitySoundManager() && cachedAssetManager) { + if (!audioCoordinator_->getActivitySoundManager()->isInitialized()) { + audioCoordinator_->getActivitySoundManager()->initialize(cachedAssetManager); } } - if (getMountSoundManager() && cachedAssetManager) { - getMountSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getMountSoundManager() && cachedAssetManager) { + audioCoordinator_->getMountSoundManager()->initialize(cachedAssetManager); } - if (getNpcVoiceManager() && cachedAssetManager) { - getNpcVoiceManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getNpcVoiceManager() && cachedAssetManager) { + audioCoordinator_->getNpcVoiceManager()->initialize(cachedAssetManager); } if (!deferredWorldInitEnabled_) { - if (getAmbientSoundManager() && cachedAssetManager) { - getAmbientSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getAmbientSoundManager() && cachedAssetManager) { + audioCoordinator_->getAmbientSoundManager()->initialize(cachedAssetManager); } - if (getUiSoundManager() && cachedAssetManager) { - getUiSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getUiSoundManager() && cachedAssetManager) { + audioCoordinator_->getUiSoundManager()->initialize(cachedAssetManager); } - if (getCombatSoundManager() && cachedAssetManager) { - getCombatSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getCombatSoundManager() && cachedAssetManager) { + audioCoordinator_->getCombatSoundManager()->initialize(cachedAssetManager); } - if (getSpellSoundManager() && cachedAssetManager) { - getSpellSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getSpellSoundManager() && cachedAssetManager) { + audioCoordinator_->getSpellSoundManager()->initialize(cachedAssetManager); } - if (getMovementSoundManager() && cachedAssetManager) { - getMovementSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getMovementSoundManager() && cachedAssetManager) { + audioCoordinator_->getMovementSoundManager()->initialize(cachedAssetManager); } if (questMarkerRenderer && cachedAssetManager) { questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, cachedAssetManager); @@ -4298,8 +2716,8 @@ bool Renderer::loadTerrainArea(const std::string& mapName, int centerX, int cent } // Wire ambient sound manager to terrain manager for emitter registration - if (terrainManager && getAmbientSoundManager()) { - terrainManager->setAmbientSoundManager(getAmbientSoundManager()); + if (terrainManager && audioCoordinator_->getAmbientSoundManager()) { + terrainManager->setAmbientSoundManager(audioCoordinator_->getAmbientSoundManager()); } // Wire WMO, M2, and water renderer to camera controller diff --git a/src/ui/auth_screen.cpp b/src/ui/auth_screen.cpp index 88264930..e30be6e4 100644 --- a/src/ui/auth_screen.cpp +++ b/src/ui/auth_screen.cpp @@ -6,6 +6,7 @@ #include "rendering/renderer.hpp" #include "rendering/vk_context.hpp" #include "pipeline/asset_manager.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/music_manager.hpp" #include "game/expansion_profile.hpp" #include @@ -199,20 +200,20 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { } auto& app = core::Application::getInstance(); - auto* renderer = app.getRenderer(); + auto* ac = app.getAudioCoordinator(); if (!musicInitAttempted) { musicInitAttempted = true; auto* assets = app.getAssetManager(); - if (renderer) { - auto* music = renderer->getMusicManager(); + if (ac) { + auto* music = ac->getMusicManager(); if (music && assets && assets->isInitialized() && !music->isInitialized()) { music->initialize(assets); } } } // Login screen music - if (renderer) { - auto* music = renderer->getMusicManager(); + if (ac) { + auto* music = ac->getMusicManager(); if (music) { if (!loginMusicVolumeAdjusted_) { savedMusicVolume_ = music->getVolume(); @@ -506,9 +507,9 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { void AuthScreen::stopLoginMusic() { auto& app = core::Application::getInstance(); - auto* renderer = app.getRenderer(); - if (!renderer) return; - auto* music = renderer->getMusicManager(); + auto* ac = app.getAudioCoordinator(); + if (!ac) return; + auto* music = ac->getMusicManager(); if (!music) return; if (musicPlaying) { music->stopMusic(500.0f); diff --git a/src/ui/chat_panel.cpp b/src/ui/chat_panel.cpp index 19ff5331..67fd4fc4 100644 --- a/src/ui/chat_panel.cpp +++ b/src/ui/chat_panel.cpp @@ -11,6 +11,7 @@ #include "rendering/renderer.hpp" #include "rendering/camera.hpp" #include "rendering/camera_controller.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/audio_engine.hpp" #include "audio/ui_sound_manager.hpp" #include "pipeline/asset_manager.hpp" @@ -1109,8 +1110,8 @@ void ChatPanel::render(game::GameHandler& gameHandler, std::string bodyLower = mMsg.message; for (auto& c : bodyLower) c = static_cast(std::tolower(static_cast(c))); if (bodyLower.find(selfNameLower) != std::string::npos) { - if (auto* renderer = services_.renderer) { - if (auto* ui = renderer->getUiSoundManager()) + if (auto* ac = services_.audioCoordinator) { + if (auto* ui = ac->getUiSoundManager()) ui->playWhisperReceived(); } break; // play at most once per scan pass diff --git a/src/ui/combat_ui.cpp b/src/ui/combat_ui.cpp index 17b6e0c9..ecaa88a5 100644 --- a/src/ui/combat_ui.cpp +++ b/src/ui/combat_ui.cpp @@ -15,6 +15,7 @@ #include "rendering/camera.hpp" #include "game/game_handler.hpp" #include "pipeline/asset_manager.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/audio_engine.hpp" #include "audio/ui_sound_manager.hpp" #include @@ -283,9 +284,11 @@ void CombatUI::renderRaidWarningOverlay(game::GameHandler& gameHandler) { raidWarnEntries_.erase(raidWarnEntries_.begin()); } // Whisper audio notification - if (msg.type == game::ChatType::WHISPER && renderer) { - if (auto* ui = renderer->getUiSoundManager()) - ui->playWhisperReceived(); + if (msg.type == game::ChatType::WHISPER) { + if (auto* ac = services_.audioCoordinator) { + if (auto* ui = ac->getUiSoundManager()) + ui->playWhisperReceived(); + } } } raidWarnChatSeenCount_ = newCount; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 1119a7d4..99247350 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -14,6 +14,7 @@ #include "rendering/character_renderer.hpp" #include "rendering/camera.hpp" #include "rendering/camera_controller.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/audio_engine.hpp" #include "audio/music_manager.hpp" #include "game/zone_manager.hpp" @@ -285,8 +286,8 @@ void GameScreen::render(game::GameHandler& gameHandler) { uiErrors_.push_back({msg, 0.0f}); if (uiErrors_.size() > 5) uiErrors_.erase(uiErrors_.begin()); // Play error sound for each new error (rate-limited by deque cap of 5) - if (auto* r = services_.renderer) { - if (auto* sfx = r->getUiSoundManager()) sfx->playError(); + if (auto* ac = services_.audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playError(); } }); uiErrorCallbackSet_ = true; @@ -345,9 +346,9 @@ void GameScreen::render(game::GameHandler& gameHandler) { // Apply saved volume settings once when audio managers first become available if (!settingsPanel_.volumeSettingsApplied_) { - auto* renderer = services_.renderer; - if (renderer && renderer->getUiSoundManager()) { - settingsPanel_.applyAudioVolumes(renderer); + auto* ac = services_.audioCoordinator; + if (ac && ac->getUiSoundManager()) { + settingsPanel_.applyAudioVolumes(ac); settingsPanel_.volumeSettingsApplied_ = true; } } @@ -6525,38 +6526,38 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { } auto applyMuteState = [&]() { - auto* activeRenderer = services_.renderer; + auto* ac = services_.audioCoordinator; float masterScale = settingsPanel_.soundMuted_ ? 0.0f : static_cast(settingsPanel_.pendingMasterVolume) / 100.0f; audio::AudioEngine::instance().setMasterVolume(masterScale); - if (!activeRenderer) return; - if (auto* music = activeRenderer->getMusicManager()) { + if (!ac) return; + if (auto* music = ac->getMusicManager()) { music->setVolume(settingsPanel_.pendingMusicVolume); } - if (auto* ambient = activeRenderer->getAmbientSoundManager()) { + if (auto* ambient = ac->getAmbientSoundManager()) { ambient->setVolumeScale(settingsPanel_.pendingAmbientVolume / 100.0f); } - if (auto* ui = activeRenderer->getUiSoundManager()) { + if (auto* ui = ac->getUiSoundManager()) { ui->setVolumeScale(settingsPanel_.pendingUiVolume / 100.0f); } - if (auto* combat = activeRenderer->getCombatSoundManager()) { + if (auto* combat = ac->getCombatSoundManager()) { combat->setVolumeScale(settingsPanel_.pendingCombatVolume / 100.0f); } - if (auto* spell = activeRenderer->getSpellSoundManager()) { + if (auto* spell = ac->getSpellSoundManager()) { spell->setVolumeScale(settingsPanel_.pendingSpellVolume / 100.0f); } - if (auto* movement = activeRenderer->getMovementSoundManager()) { + if (auto* movement = ac->getMovementSoundManager()) { movement->setVolumeScale(settingsPanel_.pendingMovementVolume / 100.0f); } - if (auto* footstep = activeRenderer->getFootstepManager()) { + if (auto* footstep = ac->getFootstepManager()) { footstep->setVolumeScale(settingsPanel_.pendingFootstepVolume / 100.0f); } - if (auto* npcVoice = activeRenderer->getNpcVoiceManager()) { + if (auto* npcVoice = ac->getNpcVoiceManager()) { npcVoice->setVolumeScale(settingsPanel_.pendingNpcVoiceVolume / 100.0f); } - if (auto* mount = activeRenderer->getMountSoundManager()) { + if (auto* mount = ac->getMountSoundManager()) { mount->setVolumeScale(settingsPanel_.pendingMountVolume / 100.0f); } - if (auto* activity = activeRenderer->getActivitySoundManager()) { + if (auto* activity = ac->getActivitySoundManager()) { activity->setVolumeScale(settingsPanel_.pendingActivityVolume / 100.0f); } }; diff --git a/src/ui/settings_panel.cpp b/src/ui/settings_panel.cpp index 15acfc13..f6353942 100644 --- a/src/ui/settings_panel.cpp +++ b/src/ui/settings_panel.cpp @@ -17,6 +17,7 @@ #include "rendering/wmo_renderer.hpp" #include "rendering/character_renderer.hpp" #include "game/zone_manager.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/audio_engine.hpp" #include "audio/music_manager.hpp" #include "audio/ambient_sound_manager.hpp" @@ -439,7 +440,7 @@ ImGui::BeginChild("AudioSettings", ImVec2(0, 360), true); // Helper lambda to apply audio settings auto applyAudioSettings = [&]() { - applyAudioVolumes(renderer); + applyAudioVolumes(services_.audioCoordinator); saveCallback(); }; @@ -1227,29 +1228,29 @@ std::string SettingsPanel::getSettingsPath() { return dir + "/settings.cfg"; } -void SettingsPanel::applyAudioVolumes(rendering::Renderer* renderer) { - if (!renderer) return; +void SettingsPanel::applyAudioVolumes(audio::AudioCoordinator* ac) { + if (!ac) return; float masterScale = soundMuted_ ? 0.0f : static_cast(pendingMasterVolume) / 100.0f; audio::AudioEngine::instance().setMasterVolume(masterScale); - if (auto* music = renderer->getMusicManager()) + if (auto* music = ac->getMusicManager()) music->setVolume(pendingMusicVolume); - if (auto* ambient = renderer->getAmbientSoundManager()) + if (auto* ambient = ac->getAmbientSoundManager()) ambient->setVolumeScale(pendingAmbientVolume / 100.0f); - if (auto* ui = renderer->getUiSoundManager()) + if (auto* ui = ac->getUiSoundManager()) ui->setVolumeScale(pendingUiVolume / 100.0f); - if (auto* combat = renderer->getCombatSoundManager()) + if (auto* combat = ac->getCombatSoundManager()) combat->setVolumeScale(pendingCombatVolume / 100.0f); - if (auto* spell = renderer->getSpellSoundManager()) + if (auto* spell = ac->getSpellSoundManager()) spell->setVolumeScale(pendingSpellVolume / 100.0f); - if (auto* movement = renderer->getMovementSoundManager()) + if (auto* movement = ac->getMovementSoundManager()) movement->setVolumeScale(pendingMovementVolume / 100.0f); - if (auto* footstep = renderer->getFootstepManager()) + if (auto* footstep = ac->getFootstepManager()) footstep->setVolumeScale(pendingFootstepVolume / 100.0f); - if (auto* npcVoice = renderer->getNpcVoiceManager()) + if (auto* npcVoice = ac->getNpcVoiceManager()) npcVoice->setVolumeScale(pendingNpcVoiceVolume / 100.0f); - if (auto* mount = renderer->getMountSoundManager()) + if (auto* mount = ac->getMountSoundManager()) mount->setVolumeScale(pendingMountVolume / 100.0f); - if (auto* activity = renderer->getActivitySoundManager()) + if (auto* activity = ac->getActivitySoundManager()) activity->setVolumeScale(pendingActivityVolume / 100.0f); } diff --git a/src/ui/toast_manager.cpp b/src/ui/toast_manager.cpp index 7f76c961..957f5e2a 100644 --- a/src/ui/toast_manager.cpp +++ b/src/ui/toast_manager.cpp @@ -2,6 +2,7 @@ #include "game/game_handler.hpp" #include "core/application.hpp" #include "rendering/renderer.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/ui_sound_manager.hpp" #include @@ -461,11 +462,13 @@ void ToastManager::triggerDing(uint32_t newLevel, uint32_t hpDelta, uint32_t man dingStats_[3] = intel; dingStats_[4] = spi; - auto* renderer = services_.renderer; - if (renderer) { - if (auto* sfx = renderer->getUiSoundManager()) { + auto* ac = services_.audioCoordinator; + if (ac) { + if (auto* sfx = ac->getUiSoundManager()) { sfx->playLevelUp(); } + } + if (auto* renderer = services_.renderer) { renderer->playEmote("cheer"); } } @@ -550,9 +553,9 @@ void ToastManager::triggerAchievementToast(uint32_t achievementId, std::string n achievementToastTimer_ = ACHIEVEMENT_TOAST_DURATION; // Play a UI sound if available - auto* renderer = services_.renderer; - if (renderer) { - if (auto* sfx = renderer->getUiSoundManager()) { + auto* ac = services_.audioCoordinator; + if (ac) { + if (auto* sfx = ac->getUiSoundManager()) { sfx->playAchievementAlert(); } } diff --git a/src/ui/window_manager.cpp b/src/ui/window_manager.cpp index c87a3f64..f25db424 100644 --- a/src/ui/window_manager.cpp +++ b/src/ui/window_manager.cpp @@ -16,6 +16,7 @@ #include "game/game_handler.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/dbc_layout.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/ui_sound_manager.hpp" #include "audio/music_manager.hpp" #include @@ -1701,9 +1702,9 @@ void WindowManager::renderEscapeMenu(SettingsPanel& settingsPanel) { settingsPanel.showEscapeSettingsNotice = false; } if (ImGui::Button("Quit", ImVec2(-1, 0))) { - auto* renderer = services_.renderer; - if (renderer) { - if (auto* music = renderer->getMusicManager()) { + auto* ac = services_.audioCoordinator; + if (ac) { + if (auto* music = ac->getMusicManager()) { music->stopMusic(0.0f); } }