Kelsidavis-WoWee/src/rendering/animation_controller.cpp
Kelsi aff545edef fix(rendering): emote animations, WMO portal culling, transport teleport
Emote animations: fix DBC chain for /laugh, /flirt, /sleep, /fart, /stink.
Previously all emotes with AnimID=0 used emoteRef as animId (wrong DBC
record IDs). Now resolves through Emotes.dbc properly, with per-emote
overrides for emotes whose DBC chain yields 0. Adds Emotes.dbc load
failure warning and diagnostic logging.

WMO culling: skip portal culling when camera is outside all groups (fixes
vanishing Stormwind ground tiles). Also handle indoor/outdoor AABB overlap
by showing all groups when position is in both indoor and outdoor AABBs.

Transport: clear ONTRANSPORT flag and transport state when transport not
found, preventing stale transport data from teleporting player to map
origin. Add area trigger safety net near (0,0,0) on Eastern Kingdoms.
2026-04-05 17:25:25 -07:00

1147 lines
49 KiB
C++

#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 <algorithm>
#include <cstdlib>
#include <unordered_map>
#include <set>
#include <random>
#include <cctype>
#include <cmath>
#include <glm/gtc/matrix_transform.hpp>
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) {
bool hasAnim = characterRenderer->hasAnimation(characterInstanceId, animId);
LOG_WARNING("playEmote '", emoteName, "': animId=", animId, " loop=", loop,
" modelHasAnim=", hasAnim);
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<pipeline::M2Sequence> 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<float>(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<pipeline::M2Sequence> 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<float>(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<pipeline::M2Sequence> 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<float>(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<float>(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<pipeline::M2Sequence> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<int>(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<int>(seq.id), static_cast<int>(loop));
if (loop && (seq.nextAnimation == static_cast<int16_t>(loop) || seq.aliasNext == loop)) sc += 30;
if (loop && scoreNear(seq.nextAnimation, static_cast<int>(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<int>(seq.id), static_cast<int>(loop));
if (seq.nextAnimation == static_cast<int16_t>(runId) || seq.nextAnimation == static_cast<int16_t>(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<int16_t>(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<int>(kTaxiSeatAttachmentId);
}
if (mountSeatAttachmentId_ >= 0) {
haveSeat = characterRenderer->getAttachmentTransform(
mountInstanceId_, static_cast<uint32_t>(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<uint32_t>(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<int>(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
if (!capabilitiesProbed_ && characterRenderer && characterInstanceId != 0) {
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<int>(characterAnimator_.getLocomotion().getState()),
" actState=", static_cast<int>(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