#include "rendering/animation_controller.hpp" #include "rendering/animation/emote_registry.hpp" #include "rendering/animation/anim_capability_probe.hpp" #include "rendering/animation/mount_fsm.hpp" #include "rendering/animation/animation_ids.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 "game/inventory.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 namespace wowee { namespace rendering { // ── AnimationController implementation ─────────────────────────────────────── AnimationController::AnimationController() = default; AnimationController::~AnimationController() = default; void AnimationController::initialize(Renderer* renderer) { renderer_ = renderer; } void AnimationController::probeCapabilities() { if (!renderer_) return; uint32_t instanceId = renderer_->getCharacterInstanceId(); if (instanceId == 0) return; auto caps = AnimCapabilityProbe::probe(renderer_, instanceId); characterAnimator_.setCapabilities(caps); capabilitiesProbed_ = true; } void AnimationController::onCharacterFollow(uint32_t /*instanceId*/) { // Reset animation state when follow target changes capabilitiesProbed_ = false; } // ── Emote support ──────────────────────────────────────────────────────────── void AnimationController::playEmote(const std::string& emoteName) { auto& registry = EmoteRegistry::instance(); registry.loadFromDbc(); auto result = registry.findEmote(emoteName); if (!result) return; uint32_t animId = result->animId; bool loop = result->loop; // For looping emotes, prefer the EMOTE_STATE_* variant if the model has it if (loop) { uint32_t stateVariant = registry.getStateVariant(animId); if (stateVariant != 0) { auto* characterRenderer = renderer_->getCharacterRenderer(); uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); if (characterRenderer && characterInstanceId > 0 && characterRenderer->hasAnimation(characterInstanceId, stateVariant)) { animId = stateVariant; } } } // Forward to CharacterAnimator (ActivityFSM handles emote state) characterAnimator_.playEmote(animId, loop); // Immediately play the emote animation on the renderer auto* characterRenderer = renderer_->getCharacterRenderer(); uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); if (characterRenderer && characterInstanceId > 0) { characterRenderer->playAnimation(characterInstanceId, animId, loop); lastPlayerAnimRequest_ = animId; lastPlayerAnimLoopRequest_ = loop; } } void AnimationController::cancelEmote() { characterAnimator_.cancelEmote(); } std::string AnimationController::getEmoteText(const std::string& emoteName, const std::string* targetName) { auto& registry = EmoteRegistry::instance(); registry.loadFromDbc(); return registry.textFor(emoteName, targetName); } uint32_t AnimationController::getEmoteDbcId(const std::string& emoteName) { auto& registry = EmoteRegistry::instance(); registry.loadFromDbc(); return registry.dbcIdFor(emoteName); } std::string AnimationController::getEmoteTextByDbcId(uint32_t dbcId, const std::string& senderName, const std::string* targetName) { auto& registry = EmoteRegistry::instance(); registry.loadFromDbc(); return registry.textByDbcId(dbcId, senderName, targetName); } uint32_t AnimationController::getEmoteAnimByDbcId(uint32_t dbcId) { auto& registry = EmoteRegistry::instance(); registry.loadFromDbc(); return registry.animByDbcId(dbcId); } // ── Spell casting ──────────────────────────────────────────────────────────── void AnimationController::startSpellCast(uint32_t precastAnimId, uint32_t castAnimId, bool castLoop, uint32_t finalizeAnimId) { characterAnimator_.startSpellCast(precastAnimId, castAnimId, castLoop, finalizeAnimId); } void AnimationController::stopSpellCast() { characterAnimator_.stopSpellCast(); } // ── Loot animation ─────────────────────────────────────────────────────────── void AnimationController::startLooting() { characterAnimator_.startLooting(); } void AnimationController::stopLooting() { characterAnimator_.stopLooting(); } // ── Hit reactions ──────────────────────────────────────────────────────────── void AnimationController::triggerHitReaction(uint32_t animId) { characterAnimator_.triggerHitReaction(animId); } // ── Crowd control ──────────────────────────────────────────────────────────── void AnimationController::setStunned(bool stunned) { stunned_ = stunned; characterAnimator_.setStunned(stunned); } // ── Health-based idle ──────────────────────────────────────────────────────── void AnimationController::setLowHealth(bool low) { characterAnimator_.setLowHealth(low); } // ── Stand state ────────────────────────────────────────────────────────────── void AnimationController::setStandState(uint8_t state) { characterAnimator_.setStandState(state); } // ── Stealth ────────────────────────────────────────────────────────────────── void AnimationController::setStealthed(bool stealth) { characterAnimator_.setStealthed(stealth); } // ── Sprint aura ────────────────────────────────────────────────────────────── void AnimationController::setSprintAuraActive(bool active) { sprintAuraActive_ = active; characterAnimator_.setSprintAuraActive(active); } // ── Targeting / combat ─────────────────────────────────────────────────────── void AnimationController::setTargetPosition(const glm::vec3* pos) { targetPosition_ = pos; } void AnimationController::setInCombat(bool combat) { inCombat_ = combat; characterAnimator_.setInCombat(combat); } void AnimationController::resetCombatVisualState() { inCombat_ = false; targetPosition_ = nullptr; meleeSwingTimer_ = 0.0f; meleeSwingCooldown_ = 0.0f; specialAttackAnimId_ = 0; rangedShootTimer_ = 0.0f; rangedAnimId_ = 0; stunned_ = false; charging_ = false; // Reset all CharacterAnimator combat state characterAnimator_.setInCombat(false); characterAnimator_.setStunned(false); characterAnimator_.setCharging(false); characterAnimator_.setLowHealth(false); characterAnimator_.stopSpellCast(); characterAnimator_.triggerHitReaction(0); // Clear hit reaction 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 (characterAnimator_.getActivity().isEmoteActive()) { characterAnimator_.cancelEmote(); } specialAttackAnimId_ = 0; // Clear any special attack override 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(); } } void AnimationController::setEquippedWeaponType(uint32_t inventoryType, bool is2HLoose, bool isFist, bool isDagger, bool hasOffHand, bool hasShield) { weaponLoadout_.inventoryType = inventoryType; weaponLoadout_.is2HLoose = is2HLoose; weaponLoadout_.isFist = isFist; weaponLoadout_.isDagger = isDagger; weaponLoadout_.hasOffHand = hasOffHand; weaponLoadout_.hasShield = hasShield; meleeAnimId_ = 0; // Force re-resolve on next swing characterAnimator_.setEquippedWeaponType(weaponLoadout_); } void AnimationController::triggerSpecialAttack(uint32_t /*spellId*/) { auto* characterRenderer = renderer_->getCharacterRenderer(); uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); if (!characterRenderer || characterInstanceId == 0) return; if (meleeSwingCooldown_ > 0.0f) return; if (characterAnimator_.getActivity().isEmoteActive()) { characterAnimator_.cancelEmote(); } auto has = [&](uint32_t id) { return characterRenderer->hasAnimation(characterInstanceId, id); }; // Choose special attack animation based on equipped weapon type uint32_t specAnim = 0; if (weaponLoadout_.hasShield && has(anim::SHIELD_BASH)) { specAnim = anim::SHIELD_BASH; } else if ((weaponLoadout_.inventoryType == game::InvType::TWO_HAND || weaponLoadout_.is2HLoose) && has(anim::SPECIAL_2H)) { specAnim = anim::SPECIAL_2H; } else if (weaponLoadout_.inventoryType != game::InvType::NON_EQUIP && has(anim::SPECIAL_1H)) { specAnim = anim::SPECIAL_1H; } else if (has(anim::SPECIAL_UNARMED)) { specAnim = anim::SPECIAL_UNARMED; } else if (has(anim::SPECIAL_1H)) { specAnim = anim::SPECIAL_1H; } if (specAnim == 0) { // No special animation available — fall back to regular melee swing triggerMeleeSwing(); return; } specialAttackAnimId_ = specAnim; meleeSwingCooldown_ = 0.1f; // Query the special attack animation duration std::vector sequences; float dur = 0.6f; if (characterRenderer->getAnimationSequences(characterInstanceId, sequences)) { for (const auto& seq : sequences) { if (seq.id == specAnim && seq.duration > 0) { dur = static_cast(seq.duration) / 1000.0f; break; } } } if (dur < 0.25f) dur = 0.25f; if (dur > 1.0f) dur = 1.0f; meleeSwingTimer_ = dur; if (renderer_->getAudioCoordinator()->getActivitySoundManager()) { renderer_->getAudioCoordinator()->getActivitySoundManager()->playMeleeSwing(); } } // ── Ranged combat ──────────────────────────────────────────────────────────── void AnimationController::setEquippedRangedType(RangedWeaponType type) { weaponLoadout_.rangedType = type; rangedAnimId_ = 0; characterAnimator_.setEquippedRangedType(type); } void AnimationController::setCharging(bool c) { charging_ = c; characterAnimator_.setCharging(c); } void AnimationController::triggerRangedShot() { auto* characterRenderer = renderer_->getCharacterRenderer(); uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); if (!characterRenderer || characterInstanceId == 0) return; if (rangedShootTimer_ > 0.0f) return; if (characterAnimator_.getActivity().isEmoteActive()) characterAnimator_.cancelEmote(); auto has = [&](uint32_t id) { return characterRenderer->hasAnimation(characterInstanceId, id); }; // Resolve ranged attack animation based on weapon type uint32_t shootAnim = 0; switch (weaponLoadout_.rangedType) { case RangedWeaponType::BOW: if (has(anim::FIRE_BOW)) shootAnim = anim::FIRE_BOW; else if (has(anim::ATTACK_BOW)) shootAnim = anim::ATTACK_BOW; break; case RangedWeaponType::GUN: if (has(anim::ATTACK_RIFLE)) shootAnim = anim::ATTACK_RIFLE; break; case RangedWeaponType::CROSSBOW: if (has(anim::ATTACK_CROSSBOW)) shootAnim = anim::ATTACK_CROSSBOW; else if (has(anim::ATTACK_BOW)) shootAnim = anim::ATTACK_BOW; break; case RangedWeaponType::THROWN: if (has(anim::ATTACK_THROWN)) shootAnim = anim::ATTACK_THROWN; break; default: break; } if (shootAnim == 0) return; // Model has no ranged animation rangedAnimId_ = shootAnim; // Query animation duration std::vector sequences; float dur = 0.6f; if (characterRenderer->getAnimationSequences(characterInstanceId, sequences)) { for (const auto& seq : sequences) { if (seq.id == shootAnim && seq.duration > 0) { dur = static_cast(seq.duration) / 1000.0f; break; } } } if (dur < 0.25f) dur = 0.25f; if (dur > 1.5f) dur = 1.5f; rangedShootTimer_ = dur; } uint32_t AnimationController::resolveMeleeAnimId() { auto* characterRenderer = renderer_->getCharacterRenderer(); uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); if (!characterRenderer || characterInstanceId == 0) { meleeAnimId_ = 0; meleeAnimDurationMs_ = 0.0f; return 0; } // When dual-wielding, bypass cache to alternate main/off-hand animations if (!weaponLoadout_.hasOffHand && 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[] = {anim::ATTACK_2H, anim::ATTACK_1H, anim::ATTACK_UNARMED, anim::ATTACK_2H_LOOSE, anim::PARRY_UNARMED, anim::PARRY_1H}; static const uint32_t candidates2HLoosePierce[] = {anim::ATTACK_2H_LOOSE_PIERCE, anim::ATTACK_2H_LOOSE, anim::ATTACK_2H, anim::ATTACK_1H, anim::ATTACK_UNARMED}; static const uint32_t candidates1H[] = {anim::ATTACK_1H, anim::ATTACK_2H, anim::ATTACK_UNARMED, anim::ATTACK_2H_LOOSE, anim::PARRY_UNARMED, anim::PARRY_1H}; static const uint32_t candidatesDagger[] = {anim::ATTACK_1H_PIERCE, anim::ATTACK_1H, anim::ATTACK_UNARMED}; static const uint32_t candidatesUnarmed[] = {anim::ATTACK_UNARMED, anim::ATTACK_1H, anim::ATTACK_2H, anim::ATTACK_2H_LOOSE, anim::PARRY_UNARMED, anim::PARRY_1H}; static const uint32_t candidatesFist[] = {anim::ATTACK_FIST_1H, anim::ATTACK_FIST_1H_OFF, anim::ATTACK_1H, anim::ATTACK_UNARMED, anim::PARRY_FIST_1H, anim::PARRY_1H}; // Off-hand attack variants (used when dual-wielding on off-hand turn) static const uint32_t candidatesOffHand[] = {anim::ATTACK_OFF, anim::ATTACK_1H, anim::ATTACK_UNARMED}; static const uint32_t candidatesOffHandPierce[] = {anim::ATTACK_OFF_PIERCE, anim::ATTACK_OFF, anim::ATTACK_1H_PIERCE, anim::ATTACK_1H}; static const uint32_t candidatesOffHandFist[] = {anim::ATTACK_FIST_1H_OFF, anim::ATTACK_OFF, anim::ATTACK_FIST_1H, anim::ATTACK_1H}; static const uint32_t candidatesOffHandUnarmed[] = {anim::ATTACK_UNARMED_OFF, anim::ATTACK_UNARMED, anim::ATTACK_OFF, anim::ATTACK_1H}; // Dual-wield: alternate main-hand and off-hand swings bool useOffHand = weaponLoadout_.hasOffHand && meleeOffHandTurn_; meleeOffHandTurn_ = weaponLoadout_.hasOffHand ? !meleeOffHandTurn_ : false; if (useOffHand) { if (weaponLoadout_.isFist) { attackCandidates = candidatesOffHandFist; candidateCount = 4; } else if (weaponLoadout_.isDagger) { attackCandidates = candidatesOffHandPierce; candidateCount = 4; } else if (weaponLoadout_.inventoryType == game::InvType::NON_EQUIP) { attackCandidates = candidatesOffHandUnarmed; candidateCount = 4; } else { attackCandidates = candidatesOffHand; candidateCount = 3; } } else if (weaponLoadout_.isFist) { attackCandidates = candidatesFist; candidateCount = 6; } else if (weaponLoadout_.isDagger) { attackCandidates = candidatesDagger; candidateCount = 3; } else if (weaponLoadout_.is2HLoose) { // Polearm thrust uses pierce variant attackCandidates = candidates2HLoosePierce; candidateCount = 5; } else if (weaponLoadout_.inventoryType == game::InvType::TWO_HAND) { attackCandidates = candidates2H; candidateCount = 6; } else if (weaponLoadout_.inventoryType == game::InvType::NON_EQUIP) { 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[] = {anim::STAND, anim::DEATH, anim::WALK, anim::RUN, anim::SHUFFLE_LEFT, anim::SHUFFLE_RIGHT, anim::WALK_BACKWARDS, anim::JUMP_START, anim::JUMP, anim::JUMP_END, anim::SWIM_IDLE, anim::SWIM, anim::SITTING}; 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; mountPitch_ = 0.0f; 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({anim::RUN, anim::WALK}); uint32_t standId = findFirst({anim::STAND}); 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(); // Build MountAnimSet for MountFSM MountFSM::MountAnimSet mountAnims; mountAnims.jumpStart = discoveredStart > 0 ? discoveredStart : findFirst({anim::FALL, anim::JUMP_START}); mountAnims.jumpLoop = discoveredLoop > 0 ? discoveredLoop : findFirst({anim::JUMP}); mountAnims.jumpEnd = discoveredEnd > 0 ? discoveredEnd : findFirst({anim::JUMP_END}); mountAnims.rearUp = findFirst({anim::MOUNT_SPECIAL, anim::RUN_RIGHT, anim::FALL}); mountAnims.run = findFirst({anim::RUN, anim::WALK}); mountAnims.stand = findFirst({anim::STAND}); // Discover flight animations (flying mounts only — may all be 0 for ground mounts) mountAnims.flyIdle = findFirst({anim::FLY_IDLE}); mountAnims.flyForward = findFirst({anim::FLY_FORWARD, anim::FLY_RUN_2}); mountAnims.flyBackwards = findFirst({anim::FLY_BACKWARDS, anim::FLY_WALK_BACKWARDS}); mountAnims.flyLeft = findFirst({anim::FLY_LEFT, anim::FLY_SHUFFLE_LEFT}); mountAnims.flyRight = findFirst({anim::FLY_RIGHT, anim::FLY_SHUFFLE_RIGHT}); mountAnims.flyUp = findFirst({anim::FLY_UP, anim::FLY_RISE}); mountAnims.flyDown = findFirst({anim::FLY_DOWN}); // Discover idle fidget animations using proper WoW M2 metadata 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()); // Configure MountFSM via CharacterAnimator characterAnimator_.configureMountFSM(mountAnims, taxiFlight_); 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; // Clear MountFSM via CharacterAnimator characterAnimator_.clearMountFSM(); 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 { auto state = characterAnimator_.getLocomotion().getState(); return state == LocomotionFSM::State::WALK || state == LocomotionFSM::State::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); if (meleeSwingTimer_ <= 0.0f) specialAttackAnimId_ = 0; } // Ranged shot timer (same pattern as melee) if (rangedShootTimer_ > 0.0f) { rangedShootTimer_ = std::max(0.0f, rangedShootTimer_ - deltaTime); } } // ── Mount positioning helper ───────────────────────────────────────────────── void AnimationController::applyMountPositioning(float mountBob, float mountRoll, float characterYaw) { auto* characterRenderer = renderer_->getCharacterRenderer(); uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); if (!characterRenderer || characterInstanceId == 0) return; float mountYawRad = glm::radians(characterYaw); // Position mount model if (mountInstanceId_ > 0) { const glm::vec3& characterPosition = renderer_->getCharacterPosition(); characterRenderer->setInstancePosition(mountInstanceId_, characterPosition); characterRenderer->setInstanceRotation(mountInstanceId_, glm::vec3(mountPitch_, mountRoll, mountYawRad)); } // 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; const glm::vec3& characterPosition = renderer_->getCharacterPosition(); 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. const glm::vec3& characterPosition = renderer_->getCharacterPosition(); bool moving = renderer_->getCameraController() && renderer_->getCameraController()->isMoving(); 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)); } } // ── Mounted animation update (uses MountFSM) ──────────────────────────────── void AnimationController::updateMountedAnimation(float deltaTime) { auto* characterRenderer = renderer_->getCharacterRenderer(); auto* cameraController = renderer_->getCameraController(); uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); float characterYaw = renderer_->getCharacterYaw(); auto& mountFSM = characterAnimator_.getMountFSM(); // Build MountFSM input MountFSM::Input mountIn; mountIn.moving = cameraController->isMoving(); mountIn.movingBackward = cameraController->isMovingBackward(); mountIn.strafeLeft = cameraController->isStrafingLeft(); mountIn.strafeRight = cameraController->isStrafingRight(); mountIn.grounded = cameraController->isGrounded(); mountIn.jumpKeyPressed = cameraController->isJumpKeyPressed(); mountIn.flying = cameraController->isFlyingActive(); mountIn.swimming = cameraController->isSwimming(); mountIn.ascending = cameraController->isAscending(); mountIn.descending = cameraController->isDescending(); mountIn.taxiFlight = taxiFlight_; mountIn.deltaTime = deltaTime; mountIn.characterYaw = characterYaw; // Mount animation state query if (mountInstanceId_ > 0 && characterRenderer) { mountIn.haveMountState = characterRenderer->getAnimationState( mountInstanceId_, mountIn.curMountAnim, mountIn.curMountTime, mountIn.curMountDuration); } // Evaluate MountFSM auto mountOut = mountFSM.evaluate(mountIn); // Apply mount animation if changed if (mountOut.mountAnimChanged && mountInstanceId_ > 0 && characterRenderer) { characterRenderer->playAnimation(mountInstanceId_, mountOut.mountAnimId, mountOut.mountAnimLoop); } // Rider animation — defaults to MOUNT, but uses MOUNT_FLIGHT_* variants when flying uint32_t riderAnim = anim::MOUNT; if (cameraController->isFlyingActive()) { auto hasRider = [&](uint32_t id) { return characterRenderer->hasAnimation(characterInstanceId, id); }; if (mountIn.moving) { if (cameraController->isAscending() && hasRider(anim::MOUNT_FLIGHT_UP)) riderAnim = anim::MOUNT_FLIGHT_UP; else if (cameraController->isDescending() && hasRider(anim::MOUNT_FLIGHT_DOWN)) riderAnim = anim::MOUNT_FLIGHT_DOWN; else if (hasRider(anim::MOUNT_FLIGHT_FORWARD)) riderAnim = anim::MOUNT_FLIGHT_FORWARD; } else { if (hasRider(anim::MOUNT_FLIGHT_IDLE)) riderAnim = anim::MOUNT_FLIGHT_IDLE; } } // Apply rider animation uint32_t currentAnimId = 0; float currentAnimTimeMs = 0.0f, currentAnimDurationMs = 0.0f; bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs); if (!haveState || currentAnimId != riderAnim) { characterRenderer->playAnimation(characterInstanceId, riderAnim, true); lastPlayerAnimRequest_ = riderAnim; lastPlayerAnimLoopRequest_ = true; } // Handle mount sounds auto* mountSoundMgr = renderer_->getAudioCoordinator()->getMountSoundManager(); if (mountOut.playJumpSound && mountSoundMgr) { mountSoundMgr->playJumpSound(); } if (mountOut.playLandSound && mountSoundMgr) { mountSoundMgr->playLandSound(); } if (mountOut.playRearUpSound && mountSoundMgr) { mountSoundMgr->playRearUpSound(); } if (mountOut.playIdleSound && mountSoundMgr) { mountSoundMgr->playIdleSound(); } if (mountOut.triggerMountJump && cameraController) { cameraController->triggerMountJump(); } // Apply positioning (uses mountBob and mountRoll from MountFSM) // For taxi flights, use external mountRoll_ set by setMountPitchRoll // For ground mounts, use MountFSM's computed lean roll float finalRoll = taxiFlight_ ? mountRoll_ : mountOut.mountRoll; applyMountPositioning(mountOut.mountBob, finalRoll, characterYaw); } // ── Character animation state machine (delegates to CharacterAnimator) ────────── void AnimationController::updateCharacterAnimation() { auto* characterRenderer = renderer_->getCharacterRenderer(); auto* cameraController = renderer_->getCameraController(); uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); // Lazy probe: populate capability set once per model. // Re-probe if melee capabilities are missing (model may not have been fully // loaded on the first probe attempt). if (characterRenderer && characterInstanceId != 0) { if (!capabilitiesProbed_) { probeCapabilities(); } else if (meleeSwingTimer_ > 0.0f && !characterAnimator_.getCapabilities().hasMelee) { capabilitiesProbed_ = false; probeCapabilities(); } } // When mounted, delegate to MountFSM and handle positioning if (isMounted()) { updateMountedAnimation(lastDeltaTime_); return; } // Build FrameInput for CharacterAnimator from camera/renderer state CharacterAnimator::FrameInput fi; fi.moving = cameraController->isMoving(); fi.sprinting = cameraController->isSprinting(); fi.movingForward = cameraController->isMovingForward(); fi.movingBackward = cameraController->isMovingBackward(); fi.autoRunning = cameraController->isAutoRunning(); fi.strafeLeft = cameraController->isStrafingLeft(); fi.strafeRight = cameraController->isStrafingRight(); fi.grounded = cameraController->isGrounded(); fi.jumping = cameraController->isJumping(); fi.swimming = cameraController->isSwimming(); fi.sitting = cameraController->isSitting(); fi.flyingActive = cameraController->isFlyingActive(); fi.ascending = cameraController->isAscending(); fi.descending = cameraController->isDescending(); fi.jumpKeyPressed = cameraController->isJumpKeyPressed(); fi.characterYaw = renderer_->getCharacterYaw(); // Melee/ranged timers fi.meleeSwingTimer = meleeSwingTimer_; fi.rangedShootTimer = rangedShootTimer_; fi.specialAttackAnimId = specialAttackAnimId_; fi.rangedAnimId = rangedAnimId_; // Animation state query for one-shot completion detection if (characterRenderer && characterInstanceId > 0) { fi.haveAnimState = characterRenderer->getAnimationState( characterInstanceId, fi.currentAnimId, fi.currentAnimTime, fi.currentAnimDuration); } // Inject FrameInput and resolve animation via CharacterAnimator characterAnimator_.setFrameInput(fi); characterAnimator_.update(lastDeltaTime_); // Read the resolved animation output AnimOutput output = characterAnimator_.getLastOutput(); // STAY policy: if CharacterAnimator returns invalid, keep current animation if (!output.valid) return; uint32_t animId = output.animId; bool loop = output.loop; // Apply animation to the character renderer 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); // Only re-assert looping animations if the renderer drifted (e.g., external // playAnimation call). One-shot animations must NOT be re-asserted after the // renderer auto-resets them to STAND on completion — the FSM detects the ID // change via oneShotComplete and transitions to the next state in the same frame. const bool drifted = haveState && currentAnimId != animId && loop; const bool shouldPlay = requestChanged || drifted; // Debug: log animation decisions (only when animation changes or replays) static uint32_t dbgLastAnim = UINT32_MAX; if (shouldPlay || animId != dbgLastAnim) { LOG_DEBUG("[AnimDbg] FSM→", animId, " loop=", loop, " cur=", currentAnimId, " t=", currentAnimTimeMs, "/", currentAnimDurationMs, " haveState=", haveState, " reqChanged=", requestChanged, " drifted=", drifted, " shouldPlay=", shouldPlay, " lastReq=", lastPlayerAnimRequest_, " locoState=", static_cast(characterAnimator_.getLocomotion().getState()), " actState=", static_cast(characterAnimator_.getActivity().getState())); dbgLastAnim = animId; } if (shouldPlay) { characterRenderer->playAnimation(characterInstanceId, animId, loop); lastPlayerAnimRequest_ = animId; lastPlayerAnimLoopRequest_ = loop; } } // ── Footstep update (delegated to FootstepDriver) ─────────────────────────── void AnimationController::updateFootsteps(float deltaTime) { footstepDriver_.update(deltaTime, renderer_, isMounted(), mountInstanceId_, taxiFlight_, isFootstepAnimationState()); } // ── Activity SFX state tracking ────────────────────────────────────────────── void AnimationController::updateSfxState(float deltaTime) { sfxStateDriver_.update(deltaTime, renderer_, isMounted(), taxiFlight_, footstepDriver_); } } // namespace rendering } // namespace wowee