feat(animation): 452 named constants, 30-phase character animation state machine

Add animation_ids.hpp/cpp with all 452 WoW animation ID constants (anim::STAND,
anim::RUN, anim::FIRE_BOW, ... anim::FLY_BACKWARDS, etc.), nameFromId() O(1)
lookup, and flyVariant() compact 218-element ground→FLY_* resolver.

Expand AnimationController into a full state machine with 20+ named states:
spell cast (directed→omni→cast fallback chain, instant one-shot release),
hit reactions (WOUND/CRIT/DODGE/BLOCK/SHIELD_BLOCK), stun, wounded idle,
stealth animation substitution, loot, fishing channel, sit/sleep/kneel
down→loop→up transitions, sheathe/unsheathe combat enter/exit, ranged weapons
(BOW/GUN/CROSSBOW/THROWN with reload states), game object OPEN/CLOSE/DESTROY,
vehicle enter/exit, mount flight directionals (FLY_LEFT/RIGHT/UP/DOWN/BACKWARDS),
emote state variants, off-hand/pierce/dual-wield alternation, NPC
birth/spawn/drown/rise, sprint aura override, totem idle, NPC greeting/farewell.

Add spell_defines.hpp with SpellEffect (~45 constants) and SpellMissInfo
(12 constants) namespaces; replace all magic numbers in spell_handler.cpp.

Add GAMEOBJECT_BYTES_1 to update field table (all 4 expansion JSONs) and wire
GameObjectStateCallback. Add DBC cross-validation on world entry.

Expand tools/_ANIM_NAMES from ~35 to 452 entries in m2_viewer.py and
asset_pipeline_gui.py. Add tests/test_animation_ids.cpp.

Bug fixes included:
- Stand state 1 was animating READY_2H(27) — fixed to SITTING(97)
- Spell casts ended freeze-frame — add one-shot release animation
- NPC 2H swing probe chain missing ATTACK_2H_LOOSE (polearm/staff)
- Chair sits (states 2/4/5/6) incorrectly played floor-sit transition
- STOP(3) used for all spell casts — replaced with model-aware chain
This commit is contained in:
Paul 2026-04-04 23:02:53 +03:00
parent d54e262048
commit e58f9b4b40
59 changed files with 3903 additions and 483 deletions

View file

@ -616,6 +616,7 @@ set(WOWEE_SOURCES
src/rendering/spell_visual_system.cpp
src/rendering/post_process_pipeline.cpp
src/rendering/animation_controller.cpp
src/rendering/animation_ids.cpp
src/rendering/loading_screen.cpp
# UI

View file

@ -2,6 +2,7 @@
"CONTAINER_FIELD_NUM_SLOTS": 48,
"CONTAINER_FIELD_SLOT_1": 50,
"GAMEOBJECT_DISPLAYID": 8,
"GAMEOBJECT_BYTES_1": 14,
"ITEM_FIELD_DURABILITY": 48,
"ITEM_FIELD_MAXDURABILITY": 49,
"ITEM_FIELD_STACK_COUNT": 14,

View file

@ -2,6 +2,7 @@
"CONTAINER_FIELD_NUM_SLOTS": 64,
"CONTAINER_FIELD_SLOT_1": 66,
"GAMEOBJECT_DISPLAYID": 8,
"GAMEOBJECT_BYTES_1": 17,
"ITEM_FIELD_DURABILITY": 60,
"ITEM_FIELD_MAXDURABILITY": 61,
"ITEM_FIELD_STACK_COUNT": 14,

View file

@ -2,6 +2,7 @@
"CONTAINER_FIELD_NUM_SLOTS": 48,
"CONTAINER_FIELD_SLOT_1": 50,
"GAMEOBJECT_DISPLAYID": 8,
"GAMEOBJECT_BYTES_1": 14,
"ITEM_FIELD_DURABILITY": 48,
"ITEM_FIELD_MAXDURABILITY": 49,
"ITEM_FIELD_STACK_COUNT": 14,

View file

@ -2,6 +2,7 @@
"CONTAINER_FIELD_NUM_SLOTS": 64,
"CONTAINER_FIELD_SLOT_1": 66,
"GAMEOBJECT_DISPLAYID": 8,
"GAMEOBJECT_BYTES_1": 17,
"ITEM_FIELD_DURABILITY": 60,
"ITEM_FIELD_MAXDURABILITY": 61,
"ITEM_FIELD_STACK_COUNT": 14,
@ -57,5 +58,6 @@
"UNIT_FIELD_STAT4": 88,
"UNIT_FIELD_TARGET_HI": 7,
"UNIT_FIELD_TARGET_LO": 6,
"UNIT_NPC_FLAGS": 82
"UNIT_NPC_FLAGS": 82,
"UNIT_NPC_EMOTESTATE": 164
}

View file

@ -176,7 +176,7 @@ void main() {
if (proj.x >= 0.0 && proj.x <= 1.0 &&
proj.y >= 0.0 && proj.y <= 1.0 &&
proj.z >= 0.0 && proj.z <= 1.0) {
float bias = max(0.0005 * (1.0 - dot(norm, ldir)), 0.00005);
float bias = max(0.0005 * (1.0 - abs(dot(norm, ldir))), 0.00005);
shadow = sampleShadowPCF(uShadowMap, vec3(proj.xy, proj.z - bias));
}
shadow = mix(1.0, shadow, shadowParams.y);

View file

@ -13,7 +13,7 @@ layout(set = 0, binding = 0) uniform PerFrame {
vec4 shadowParams;
};
// Phase 2.1: Per-draw push constants (batch-level data only)
// Per-draw push constants (batch-level data only)
layout(push_constant) uniform Push {
int texCoordSet; // UV set index (0 or 1)
int isFoliage; // Foliage wind animation flag
@ -24,7 +24,7 @@ layout(set = 2, binding = 0) readonly buffer BoneSSBO {
mat4 bones[];
};
// Phase 2.1: Per-instance data read via gl_InstanceIndex (GPU instancing)
// Per-instance data read via gl_InstanceIndex (GPU instancing)
struct InstanceData {
mat4 model;
vec2 uvOffset;
@ -51,7 +51,7 @@ layout(location = 4) out float ModelHeight;
layout(location = 5) out float vFadeAlpha;
void main() {
// Phase 2.1: Fetch per-instance data from SSBO
// Fetch per-instance data from SSBO
int instIdx = push.instanceDataOffset + gl_InstanceIndex;
mat4 model = instanceData[instIdx].model;
vec2 uvOff = instanceData[instIdx].uvOffset;

View file

@ -1,6 +1,6 @@
#version 450
// Phase 2.3: GPU Frustum Culling for M2 doodads
// GPU Frustum Culling for M2 doodads
// Each compute thread tests one M2 instance against 6 frustum planes.
// Input: per-instance bounding sphere + flags.
// Output: uint visibility array (1 = visible, 0 = culled).

View file

@ -116,8 +116,8 @@ void main() {
vec4 lsPos = lightSpaceMatrix * vec4(biasedPos, 1.0);
vec3 proj = lsPos.xyz / lsPos.w;
proj.xy = proj.xy * 0.5 + 0.5;
if (proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0 && proj.z <= 1.0) {
float bias = 0.0002;
if (proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0 && proj.z >= 0.0 && proj.z <= 1.0) {
float bias = max(0.0005 * (1.0 - abs(dot(norm, ldir))), 0.00005);
shadow = sampleShadowPCF(uShadowMap, vec3(proj.xy, proj.z - bias));
shadow = mix(1.0, shadow, shadowParams.y);
}

View file

@ -176,7 +176,7 @@ void main() {
if (proj.x >= 0.0 && proj.x <= 1.0 &&
proj.y >= 0.0 && proj.y <= 1.0 &&
proj.z >= 0.0 && proj.z <= 1.0) {
float bias = max(0.0005 * (1.0 - dot(norm, ldir)), 0.00005);
float bias = max(0.0005 * (1.0 - abs(dot(norm, ldir))), 0.00005);
shadow = sampleShadowPCF(uShadowMap, vec3(proj.xy, proj.z - bias));
}
shadow = mix(1.0, shadow, shadowParams.y);

View file

@ -52,13 +52,13 @@ public:
// Player model path resolution
std::string getPlayerModelPath(game::Race race, game::Gender gender) const;
// Phase 1: Resolve texture paths from CharSections.dbc and fill model texture slots.
// Resolve texture paths from CharSections.dbc and fill model texture slots.
// Call BEFORE charRenderer->loadModel().
PlayerTextureInfo resolvePlayerTextures(pipeline::M2Model& model,
game::Race race, game::Gender gender,
uint32_t appearanceBytes);
// Phase 2: Apply composited textures to loaded model instance.
// Apply composited textures to loaded model instance.
// Call AFTER charRenderer->loadModel(). Saves skin state for re-compositing.
void compositePlayerSkin(uint32_t modelSlotId, const PlayerTextureInfo& texInfo);

View file

@ -261,6 +261,10 @@ public:
uint32_t getNpcFlags() const { return npcFlags; }
void setNpcFlags(uint32_t f) { npcFlags = f; }
// NPC emote state (UNIT_NPC_EMOTESTATE) — persistent looping animation for NPCs
uint32_t getNpcEmoteState() const { return npcEmoteState; }
void setNpcEmoteState(uint32_t e) { npcEmoteState = e; }
// Returns true if NPC has interaction flags (gossip/vendor/quest/trainer)
bool isInteractable() const { return npcFlags != 0; }
@ -284,6 +288,7 @@ protected:
uint32_t unitFlags = 0;
uint32_t dynamicFlags = 0;
uint32_t npcFlags = 0;
uint32_t npcEmoteState = 0;
uint32_t factionTemplate = 0;
bool hostile = false;
};

View file

@ -173,7 +173,7 @@ private:
struct UnitFieldIndices {
uint16_t health, maxHealth, powerBase, maxPowerBase;
uint16_t level, faction, flags, dynFlags;
uint16_t displayId, mountDisplayId, npcFlags;
uint16_t displayId, mountDisplayId, npcFlags, npcEmoteState;
uint16_t bytes0, bytes1;
static UnitFieldIndices resolve();
};

View file

@ -934,7 +934,8 @@ public:
void setGhostStateCallback(GhostStateCallback cb) { ghostStateCallback_ = std::move(cb); }
// Melee swing callback (for driving animation/SFX)
using MeleeSwingCallback = std::function<void()>;
// spellId: 0 = regular auto-attack swing, non-zero = melee ability (special attack)
using MeleeSwingCallback = std::function<void(uint32_t spellId)>;
void setMeleeSwingCallback(MeleeSwingCallback cb) { meleeSwingCallback_ = std::move(cb); }
// Spell cast animation callbacks — true=start cast/channel, false=finish/cancel
@ -959,6 +960,23 @@ public:
using NpcSwingCallback = std::function<void(uint64_t guid)>;
void setNpcSwingCallback(NpcSwingCallback cb) { npcSwingCallback_ = std::move(cb); }
// Hit reaction callback — triggers victim animation (dodge, block, wound, crit wound)
enum class HitReaction : uint8_t { WOUND, CRIT_WOUND, DODGE, PARRY, BLOCK, SHIELD_BLOCK };
using HitReactionCallback = std::function<void(uint64_t victimGuid, HitReaction reaction)>;
void setHitReactionCallback(HitReactionCallback cb) { hitReactionCallback_ = std::move(cb); }
// Stun state callback — fires when UNIT_FLAG_STUNNED changes on the local player
using StunStateCallback = std::function<void(bool stunned)>;
void setStunStateCallback(StunStateCallback cb) { stunStateCallback_ = std::move(cb); }
// Stealth state callback — fires when UNIT_FLAG_SNEAKING changes on the local player
using StealthStateCallback = std::function<void(bool stealthed)>;
void setStealthStateCallback(StealthStateCallback cb) { stealthStateCallback_ = std::move(cb); }
// Player health changed callback — fires when local player HP changes
using PlayerHealthCallback = std::function<void(uint32_t health, uint32_t maxHealth)>;
void setPlayerHealthCallback(PlayerHealthCallback cb) { playerHealthCallback_ = std::move(cb); }
// NPC greeting callback (plays voice line when NPC is clicked)
using NpcGreetingCallback = std::function<void(uint64_t guid, const glm::vec3& position)>;
void setNpcGreetingCallback(NpcGreetingCallback cb) { npcGreetingCallback_ = std::move(cb); }
@ -1093,6 +1111,19 @@ public:
using GameObjectCustomAnimCallback = std::function<void(uint64_t guid, uint32_t animId)>;
void setGameObjectCustomAnimCallback(GameObjectCustomAnimCallback cb) { gameObjectCustomAnimCallback_ = std::move(cb); }
// GameObject state change callback (triggered when GAMEOBJECT_BYTES_1 updates — state byte changes)
// goState: 0=READY(closed), 1=OPEN, 2=DESTROYED
using GameObjectStateCallback = std::function<void(uint64_t guid, uint8_t goState)>;
void setGameObjectStateCallback(GameObjectStateCallback cb) { gameObjectStateCallback_ = std::move(cb); }
// Sprint aura callback — fired when sprint-type aura active state changes on player
using SprintAuraCallback = std::function<void(bool active)>;
void setSprintAuraCallback(SprintAuraCallback cb) { sprintAuraCallback_ = std::move(cb); }
// Vehicle state callback — fired when player enters/exits a vehicle
using VehicleStateCallback = std::function<void(bool entered, uint32_t vehicleId)>;
void setVehicleStateCallback(VehicleStateCallback cb) { vehicleStateCallback_ = std::move(cb); }
// Faction hostility map (populated from FactionTemplate.dbc by Application)
void setFactionHostileMap(std::unordered_map<uint32_t, bool> map) { factionHostileMap_ = std::move(map); }
@ -1806,6 +1837,10 @@ public:
using ItemLootCallback = std::function<void(uint32_t itemId, uint32_t count, uint32_t quality, const std::string& name)>;
void setItemLootCallback(ItemLootCallback cb) { itemLootCallback_ = std::move(cb); }
// Loot window open/close callback (for loot kneel animation)
using LootWindowCallback = std::function<void(bool open)>;
void setLootWindowCallback(LootWindowCallback cb) { lootWindowCallback_ = std::move(cb); }
// Quest turn-in completion callback
using QuestCompleteCallback = std::function<void(uint32_t questId, const std::string& questTitle)>;
void setQuestCompleteCallback(QuestCompleteCallback cb) { questCompleteCallback_ = std::move(cb); }
@ -2532,6 +2567,9 @@ private:
GameObjectMoveCallback gameObjectMoveCallback_;
GameObjectDespawnCallback gameObjectDespawnCallback_;
GameObjectCustomAnimCallback gameObjectCustomAnimCallback_;
GameObjectStateCallback gameObjectStateCallback_;
SprintAuraCallback sprintAuraCallback_;
VehicleStateCallback vehicleStateCallback_;
// Transport tracking
struct TransportAttachment {
@ -3111,6 +3149,10 @@ private:
UnitAnimHintCallback unitAnimHintCallback_;
UnitMoveFlagsCallback unitMoveFlagsCallback_;
NpcSwingCallback npcSwingCallback_;
HitReactionCallback hitReactionCallback_;
StunStateCallback stunStateCallback_;
StealthStateCallback stealthStateCallback_;
PlayerHealthCallback playerHealthCallback_;
NpcGreetingCallback npcGreetingCallback_;
NpcFarewellCallback npcFarewellCallback_;
NpcVendorCallback npcVendorCallback_;
@ -3210,6 +3252,9 @@ private:
// ---- Item loot callback ----
ItemLootCallback itemLootCallback_;
// ---- Loot window callback ----
LootWindowCallback lootWindowCallback_;
// ---- Quest completion callback ----
QuestCompleteCallback questCompleteCallback_;

View file

@ -28,6 +28,38 @@ enum class EquipSlot : uint8_t {
NUM_SLOTS // = 23
};
// WoW InventoryType field values (from ItemDisplayInfo / Item.dbc / CMSG_ITEM_QUERY)
// Used in ItemDef::inventoryType and equipment update packets.
namespace InvType {
constexpr uint8_t NON_EQUIP = 0; // Not equippable / unarmed
constexpr uint8_t HEAD = 1;
constexpr uint8_t NECK = 2;
constexpr uint8_t SHOULDERS = 3;
constexpr uint8_t SHIRT = 4;
constexpr uint8_t CHEST = 5; // Chest armor
constexpr uint8_t WAIST = 6;
constexpr uint8_t LEGS = 7;
constexpr uint8_t FEET = 8;
constexpr uint8_t WRISTS = 9;
constexpr uint8_t HANDS = 10;
constexpr uint8_t FINGER = 11; // Ring
constexpr uint8_t TRINKET = 12;
constexpr uint8_t ONE_HAND = 13; // One-handed weapon (sword, mace, dagger, fist)
constexpr uint8_t SHIELD = 14;
constexpr uint8_t RANGED_BOW = 15; // Bow
constexpr uint8_t BACK = 16; // Cloak
constexpr uint8_t TWO_HAND = 17; // Two-handed weapon (also polearm/staff by inventoryType alone)
constexpr uint8_t BAG = 18;
constexpr uint8_t TABARD = 19;
constexpr uint8_t ROBE = 20; // Chest (robe variant)
constexpr uint8_t MAIN_HAND = 21; // Main-hand only weapon
constexpr uint8_t OFF_HAND = 22; // Off-hand (held-in-off-hand items, not weapons)
constexpr uint8_t HOLDABLE = 23; // Off-hand holdable (books, orbs)
constexpr uint8_t AMMO = 24;
constexpr uint8_t THROWN = 25;
constexpr uint8_t RANGED_GUN = 26; // Gun / Crossbow / Wand
} // namespace InvType
struct ItemDef {
uint32_t itemId = 0;
std::string name;

View file

@ -300,5 +300,76 @@ inline const char* getSpellCastResultString(uint8_t result, int powerType = -1)
}
}
// ── SpellEffect — SMSG_SPELLLOGEXECUTE effectType field (3.3.5a) ──────────
// Full WoW enum has 164 entries; only values used in the codebase or commonly
// relevant are defined here. Values match SharedDefines.h SpellEffects enum.
namespace SpellEffect {
constexpr uint8_t NONE = 0;
constexpr uint8_t INSTAKILL = 1;
constexpr uint8_t SCHOOL_DAMAGE = 2;
constexpr uint8_t DUMMY = 3;
constexpr uint8_t TELEPORT_UNITS = 5;
constexpr uint8_t APPLY_AURA = 6;
constexpr uint8_t ENVIRONMENTAL_DAMAGE = 7;
constexpr uint8_t POWER_DRAIN = 10;
constexpr uint8_t HEALTH_LEECH = 11;
constexpr uint8_t HEAL = 12;
constexpr uint8_t WEAPON_DAMAGE_NOSCHOOL = 16;
constexpr uint8_t RESURRECT = 18;
constexpr uint8_t EXTRA_ATTACKS = 19;
constexpr uint8_t CREATE_ITEM = 24;
constexpr uint8_t WEAPON_DAMAGE = 25;
constexpr uint8_t INTERRUPT_CAST = 26;
constexpr uint8_t OPEN_LOCK = 27;
constexpr uint8_t APPLY_AREA_AURA_PARTY = 35;
constexpr uint8_t LEARN_SPELL = 36;
constexpr uint8_t DISPEL = 38;
constexpr uint8_t SUMMON = 40;
constexpr uint8_t ENERGIZE = 43;
constexpr uint8_t WEAPON_PERCENT_DAMAGE = 44;
constexpr uint8_t TRIGGER_SPELL = 45;
constexpr uint8_t FEED_PET = 49;
constexpr uint8_t DISMISS_PET = 50;
constexpr uint8_t ENCHANT_ITEM_PERM = 53;
constexpr uint8_t ENCHANT_ITEM_TEMP = 54;
constexpr uint8_t SUMMON_PET = 56;
constexpr uint8_t LEARN_PET_SPELL = 57;
constexpr uint8_t WEAPON_DAMAGE_PLUS = 58;
constexpr uint8_t CREATE_HOUSE = 60;
constexpr uint8_t DUEL = 62;
constexpr uint8_t QUEST_COMPLETE = 63;
constexpr uint8_t NORMALIZED_WEAPON_DMG = 75;
constexpr uint8_t OPEN_LOCK_ITEM = 79;
constexpr uint8_t APPLY_AREA_AURA_RAID = 81;
constexpr uint8_t ACTIVATE_RUNE = 92;
constexpr uint8_t KNOCK_BACK = 99;
constexpr uint8_t PULL = 100;
constexpr uint8_t DISPEL_MECHANIC = 108;
constexpr uint8_t RESURRECT_NEW = 113;
constexpr uint8_t CREATE_ITEM2 = 114;
constexpr uint8_t MILLING = 115;
constexpr uint8_t PROSPECTING = 118;
constexpr uint8_t CHARGE = 126;
constexpr uint8_t TITAN_GRIP = 155;
constexpr uint8_t TOTAL_SPELL_EFFECTS = 164;
} // namespace SpellEffect
// ── SpellMissInfo — SMSG_SPELLLOGMISS / SMSG_SPELL_GO miss type (3.3.5a) ─
namespace SpellMissInfo {
constexpr uint8_t NONE = 0; // Miss
constexpr uint8_t MISS = 0;
constexpr uint8_t DODGE = 1;
constexpr uint8_t PARRY = 2;
constexpr uint8_t BLOCK = 3;
constexpr uint8_t EVADE = 4;
constexpr uint8_t IMMUNE = 5;
constexpr uint8_t DEFLECT = 6;
constexpr uint8_t ABSORB = 7;
constexpr uint8_t RESIST = 8;
constexpr uint8_t IMMUNE2 = 9; // Second immunity flag
constexpr uint8_t IMMUNE3 = 10; // Third immunity flag
constexpr uint8_t REFLECT = 11;
} // namespace SpellMissInfo
} // namespace game
} // namespace wowee

View file

@ -34,6 +34,7 @@ enum class UF : uint16_t {
UNIT_FIELD_AURAS, // Start of aura spell ID array (48 consecutive uint32 slots, classic/vanilla only)
UNIT_FIELD_AURAFLAGS, // Aura flags packed 4-per-uint32 (12 uint32 slots); 0x01=cancelable,0x02=harmful,0x04=helpful
UNIT_NPC_FLAGS,
UNIT_NPC_EMOTESTATE, // Persistent NPC emote animation ID (uint32)
UNIT_DYNAMIC_FLAGS,
UNIT_FIELD_RESISTANCES, // Physical armor (index 0 of the resistance array)
UNIT_FIELD_STAT0, // Strength (effective base, includes items)
@ -84,6 +85,7 @@ enum class UF : uint16_t {
// GameObject fields
GAMEOBJECT_DISPLAYID,
GAMEOBJECT_BYTES_1,
// Item fields
ITEM_FIELD_STACK_COUNT,

View file

@ -11,6 +11,9 @@ namespace rendering {
class Renderer;
/// Ranged weapon type for animation selection (bow/gun/crossbow/thrown)
enum class RangedWeaponType : uint8_t { NONE = 0, BOW, GUN, CROSSBOW, THROWN };
// ============================================================================
// AnimationController — extracted from Renderer (§4.2)
//
@ -63,10 +66,82 @@ public:
// ── Melee combat ───────────────────────────────────────────────────────
void triggerMeleeSwing();
void setEquippedWeaponType(uint32_t inventoryType) { equippedWeaponInvType_ = inventoryType; meleeAnimId_ = 0; }
/// inventoryType: WoW inventory type (0=unarmed, 13=1H, 17=2H, 21=main-hand, …)
/// is2HLoose: true for polearms/staves (use ATTACK_2H_LOOSE instead of ATTACK_2H)
void setEquippedWeaponType(uint32_t inventoryType, bool is2HLoose = false,
bool isFist = false, bool isDagger = false,
bool hasOffHand = false, bool hasShield = false) {
equippedWeaponInvType_ = inventoryType;
equippedIs2HLoose_ = is2HLoose;
equippedIsFist_ = isFist;
equippedIsDagger_ = isDagger;
equippedHasOffHand_ = hasOffHand;
equippedHasShield_ = hasShield;
meleeAnimId_ = 0; // Force re-resolve on next swing
}
/// Play a special attack animation for a melee ability (spellId → SPECIAL_1H/2H/SHIELD_BASH/WHIRLWIND)
void triggerSpecialAttack(uint32_t spellId);
// ── Sprint aura animation ────────────────────────────────────────────
void setSprintAuraActive(bool active) { sprintAuraActive_ = active; }
// ── Ranged combat ──────────────────────────────────────────────────────
void setEquippedRangedType(RangedWeaponType type) {
equippedRangedType_ = type;
rangedAnimId_ = 0; // Force re-resolve
}
/// Trigger a ranged shot animation (Auto Shot, Shoot, Throw)
void triggerRangedShot();
RangedWeaponType getEquippedRangedType() const { return equippedRangedType_; }
void setCharging(bool charging) { charging_ = charging; }
bool isCharging() const { return charging_; }
// ── Spell casting ──────────────────────────────────────────────────────
/// Enter spell cast animation sequence:
/// precastAnimId (one-shot wind-up) → castAnimId (looping hold) → finalizeAnimId (one-shot release)
/// Any phase can be 0 to skip it.
void startSpellCast(uint32_t precastAnimId, uint32_t castAnimId, bool castLoop,
uint32_t finalizeAnimId = 0);
/// Leave spell cast animation state → plays finalization anim then idle.
void stopSpellCast();
// ── Loot animation ─────────────────────────────────────────────────────
void startLooting();
void stopLooting();
// ── Hit reactions ──────────────────────────────────────────────────────
/// Play a one-shot hit reaction animation (wound, dodge, block, etc.)
/// on the player character. The state machine returns to the previous
/// state once the reaction animation finishes.
void triggerHitReaction(uint32_t animId);
// ── Crowd control ──────────────────────────────────────────────────────
/// Enter/exit stunned state (loops STUN animation until cleared).
void setStunned(bool stunned);
bool isStunned() const { return stunned_; }
// ── Health-based idle ──────────────────────────────────────────────────
/// When true, idle/combat-idle will prefer STAND_WOUND if the model has it.
void setLowHealth(bool low) { lowHealth_ = low; }
// ── Stand state (sit/sleep/kneel transitions) ──────────────────────────
// WoW UnitStandStateType constants
static constexpr uint8_t STAND_STATE_STAND = 0;
static constexpr uint8_t STAND_STATE_SIT = 1;
static constexpr uint8_t STAND_STATE_SIT_CHAIR = 2;
static constexpr uint8_t STAND_STATE_SLEEP = 3;
static constexpr uint8_t STAND_STATE_SIT_LOW = 4;
static constexpr uint8_t STAND_STATE_SIT_MED = 5;
static constexpr uint8_t STAND_STATE_SIT_HIGH = 6;
static constexpr uint8_t STAND_STATE_DEAD = 7;
static constexpr uint8_t STAND_STATE_KNEEL = 8;
static constexpr uint8_t STAND_STATE_SUBMERGED = 9;
void setStandState(uint8_t state);
// ── Stealth ────────────────────────────────────────────────────────────
/// When true, idle/walk/run use stealth animation variants.
void setStealthed(bool stealth);
// ── Effect triggers ────────────────────────────────────────────────────
void triggerLevelUpEffect(const glm::vec3& position);
void startChargeEffect(const glm::vec3& position, const glm::vec3& direction);
@ -94,7 +169,10 @@ private:
// 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
SIT_UP, EMOTE, SWIM_IDLE, SWIM, MELEE_SWING, MOUNT, CHARGE, COMBAT_IDLE,
SPELL_PRECAST, SPELL_CASTING, SPELL_FINALIZE, HIT_REACTION, STUNNED, LOOTING,
UNSHEATHE, SHEATHE, // Weapon draw/put-away one-shot transitions
RANGED_SHOOT, RANGED_LOAD // Ranged attack sequence: shoot → reload
};
CharAnimState charAnimState_ = CharAnimState::IDLE;
float locomotionStopGraceTimer_ = 0.0f;
@ -107,6 +185,30 @@ private:
uint32_t emoteAnimId_ = 0;
bool emoteLoop_ = false;
// Spell cast sequence state (PRECAST → CASTING → FINALIZE)
uint32_t spellPrecastAnimId_ = 0; // One-shot wind-up (phase 1)
uint32_t spellCastAnimId_ = 0; // Looping cast hold (phase 2)
uint32_t spellFinalizeAnimId_ = 0; // One-shot release (phase 3)
bool spellCastLoop_ = false;
// Hit reaction state
uint32_t hitReactionAnimId_ = 0;
// Crowd control
bool stunned_ = false;
// Health-based idle
bool lowHealth_ = false;
// Stand state (sit/sleep/kneel)
uint8_t standState_ = 0;
uint32_t sitDownAnim_ = 0; // Transition-in animation (one-shot)
uint32_t sitLoopAnim_ = 0; // Looping pose animation
uint32_t sitUpAnim_ = 0; // Transition-out animation (one-shot)
// Stealth
bool stealthed_ = false;
// Target facing
const glm::vec3* targetPosition_ = nullptr;
bool inCombat_ = false;
@ -139,7 +241,19 @@ private:
float meleeSwingCooldown_ = 0.0f;
float meleeAnimDurationMs_ = 0.0f;
uint32_t meleeAnimId_ = 0;
uint32_t specialAttackAnimId_ = 0; // Non-zero during special attack (overrides resolveMeleeAnimId)
uint32_t equippedWeaponInvType_ = 0;
bool equippedIs2HLoose_ = false; // Polearm or staff
bool equippedIsFist_ = false; // Fist weapon
bool equippedIsDagger_ = false; // Dagger (uses pierce variants)
bool equippedHasOffHand_ = false; // Has off-hand weapon (dual wield)
bool equippedHasShield_ = false; // Has shield equipped (for SHIELD_BASH)
bool meleeOffHandTurn_ = false; // Alternates main/off-hand swings
// Ranged weapon state
RangedWeaponType equippedRangedType_ = RangedWeaponType::NONE;
float rangedShootTimer_ = 0.0f; // Countdown for ranged attack animation
uint32_t rangedAnimId_ = 0; // Cached ranged attack animation
// Mount animation capabilities (discovered at mount time, varies per model)
struct MountAnimSet {
@ -149,6 +263,14 @@ private:
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)
// Flight animations (discovered from mount model)
uint32_t flyIdle = 0;
uint32_t flyForward = 0;
uint32_t flyBackwards = 0;
uint32_t flyLeft = 0;
uint32_t flyRight = 0;
uint32_t flyUp = 0;
uint32_t flyDown = 0;
std::vector<uint32_t> fidgets; // Idle fidget animations (head turn, tail swish, etc.)
};
@ -171,6 +293,7 @@ private:
uint32_t mountActiveFidget_ = 0; // Currently playing fidget animation ID (0 = none)
bool taxiFlight_ = false;
bool taxiAnimsLogged_ = false;
bool sprintAuraActive_ = false; // Sprint/Dash aura active → use SPRINT anim
// Private animation helpers
bool shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs);

View file

@ -0,0 +1,514 @@
#pragma once
// ============================================================================
// M2 Animation IDs — AnimationData.dbc
//
// Complete list from https://wowdev.wiki/M2/AnimationList
// Community names in comments describe what each animation looks like in-game.
// Organized by World of Warcraft expansion for easier management.
// ============================================================================
#include <cstdint>
#include <memory>
namespace wowee {
namespace pipeline { class DBCFile; }
namespace rendering {
namespace anim {
// ============================================================================
// Classic (Vanilla WoW 1.x) — Core character & creature animations
// IDs 0145
// ============================================================================
constexpr uint32_t STAND = 0; // Idle standing pose
constexpr uint32_t DEATH = 1; // Death animation
constexpr uint32_t SPELL = 2; // Generic spell cast
constexpr uint32_t STOP = 3; // Transition to stop
constexpr uint32_t WALK = 4; // Walking forward
constexpr uint32_t RUN = 5; // Running forward
constexpr uint32_t DEAD = 6; // Corpse on the ground
constexpr uint32_t RISE = 7; // Rising from death (resurrection)
constexpr uint32_t STAND_WOUND = 8; // Wounded idle stance
constexpr uint32_t COMBAT_WOUND = 9; // Wounded combat idle
constexpr uint32_t COMBAT_CRITICAL = 10; // Critical hit reaction
constexpr uint32_t SHUFFLE_LEFT = 11; // Strafe walk left
constexpr uint32_t SHUFFLE_RIGHT = 12; // Strafe walk right
constexpr uint32_t WALK_BACKWARDS = 13; // Walking backwards / backpedal
constexpr uint32_t STUN = 14; // Stunned
constexpr uint32_t HANDS_CLOSED = 15; // Hands closed (weapon grip idle)
constexpr uint32_t ATTACK_UNARMED = 16; // Unarmed melee attack
constexpr uint32_t ATTACK_1H = 17; // One-handed melee attack
constexpr uint32_t ATTACK_2H = 18; // Two-handed melee attack
constexpr uint32_t ATTACK_2H_LOOSE = 19; // Polearm/staff two-hand attack
constexpr uint32_t PARRY_UNARMED = 20; // Unarmed parry
constexpr uint32_t PARRY_1H = 21; // One-handed weapon parry
constexpr uint32_t PARRY_2H = 22; // Two-handed weapon parry
constexpr uint32_t PARRY_2H_LOOSE = 23; // Polearm/staff parry
constexpr uint32_t SHIELD_BLOCK = 24; // Shield block
constexpr uint32_t READY_UNARMED = 25; // Unarmed combat ready stance
constexpr uint32_t READY_1H = 26; // One-handed weapon ready stance
constexpr uint32_t READY_2H = 27; // Two-handed weapon ready stance
constexpr uint32_t READY_2H_LOOSE = 28; // Polearm/staff ready stance
constexpr uint32_t READY_BOW = 29; // Bow ready stance
constexpr uint32_t DODGE = 30; // Dodge
constexpr uint32_t SPELL_PRECAST = 31; // Spell precast wind-up
constexpr uint32_t SPELL_CAST = 32; // Spell cast
constexpr uint32_t SPELL_CAST_AREA = 33; // Area-of-effect spell cast
constexpr uint32_t NPC_WELCOME = 34; // NPC greeting animation
constexpr uint32_t NPC_GOODBYE = 35; // NPC farewell animation
constexpr uint32_t BLOCK = 36; // Block
constexpr uint32_t JUMP_START = 37; // Jump takeoff
constexpr uint32_t JUMP = 38; // Mid-air jump loop
constexpr uint32_t JUMP_END = 39; // Jump landing
constexpr uint32_t FALL = 40; // Falling
constexpr uint32_t SWIM_IDLE = 41; // Treading water
constexpr uint32_t SWIM = 42; // Swimming forward
constexpr uint32_t SWIM_LEFT = 43; // Swim strafe left
constexpr uint32_t SWIM_RIGHT = 44; // Swim strafe right
constexpr uint32_t SWIM_BACKWARDS = 45; // Swim backwards
constexpr uint32_t ATTACK_BOW = 46; // Bow attack
constexpr uint32_t FIRE_BOW = 47; // Fire bow shot
constexpr uint32_t READY_RIFLE = 48; // Rifle/gun ready stance
constexpr uint32_t ATTACK_RIFLE = 49; // Rifle/gun attack
constexpr uint32_t LOOT = 50; // Looting / bending down to pick up
constexpr uint32_t READY_SPELL_DIRECTED = 51; // Directed spell ready
constexpr uint32_t READY_SPELL_OMNI = 52; // Omni spell ready
constexpr uint32_t SPELL_CAST_DIRECTED = 53; // Directed spell cast
constexpr uint32_t SPELL_CAST_OMNI = 54; // Omni spell cast
constexpr uint32_t BATTLE_ROAR = 55; // Battle shout / roar
constexpr uint32_t READY_ABILITY = 56; // Ability ready stance
constexpr uint32_t SPECIAL_1H = 57; // Special one-handed attack
constexpr uint32_t SPECIAL_2H = 58; // Special two-handed attack
constexpr uint32_t SHIELD_BASH = 59; // Shield bash
constexpr uint32_t EMOTE_TALK = 60; // /talk
constexpr uint32_t EMOTE_EAT = 61; // /eat
constexpr uint32_t EMOTE_WORK = 62; // /work
constexpr uint32_t EMOTE_USE_STANDING = 63; // Standing use animation
constexpr uint32_t EMOTE_EXCLAMATION = 64; // NPC exclamation (!)
constexpr uint32_t EMOTE_QUESTION = 65; // NPC question (?)
constexpr uint32_t EMOTE_BOW = 66; // /bow
constexpr uint32_t EMOTE_WAVE = 67; // /wave
constexpr uint32_t EMOTE_CHEER = 68; // /cheer
constexpr uint32_t EMOTE_DANCE = 69; // /dance
constexpr uint32_t EMOTE_LAUGH = 70; // /laugh
constexpr uint32_t EMOTE_SLEEP = 71; // /sleep
constexpr uint32_t EMOTE_SIT_GROUND = 72; // /sit on ground
constexpr uint32_t EMOTE_RUDE = 73; // /rude
constexpr uint32_t EMOTE_ROAR = 74; // /roar
constexpr uint32_t EMOTE_KNEEL = 75; // /kneel
constexpr uint32_t EMOTE_KISS = 76; // /kiss
constexpr uint32_t EMOTE_CRY = 77; // /cry
constexpr uint32_t EMOTE_CHICKEN = 78; // /chicken — flap arms and strut
constexpr uint32_t EMOTE_BEG = 79; // /beg
constexpr uint32_t EMOTE_APPLAUD = 80; // /applaud
constexpr uint32_t EMOTE_SHOUT = 81; // /shout
constexpr uint32_t EMOTE_FLEX = 82; // /flex — show off muscles
constexpr uint32_t EMOTE_SHY = 83; // /shy
constexpr uint32_t EMOTE_POINT = 84; // /point
constexpr uint32_t ATTACK_1H_PIERCE = 85; // One-handed pierce (dagger stab)
constexpr uint32_t ATTACK_2H_LOOSE_PIERCE = 86; // Polearm/staff pierce
constexpr uint32_t ATTACK_OFF = 87; // Off-hand attack
constexpr uint32_t ATTACK_OFF_PIERCE = 88; // Off-hand pierce attack
constexpr uint32_t SHEATHE = 89; // Sheathe weapons
constexpr uint32_t HIP_SHEATHE = 90; // Hip sheathe
constexpr uint32_t MOUNT = 91; // Mounted idle
constexpr uint32_t RUN_RIGHT = 92; // Strafe run right
constexpr uint32_t RUN_LEFT = 93; // Strafe run left
constexpr uint32_t MOUNT_SPECIAL = 94; // Mount rearing / special move
constexpr uint32_t KICK = 95; // Kick
constexpr uint32_t SIT_GROUND_DOWN = 96; // Transition: standing → sitting
constexpr uint32_t SITTING = 97; // Sitting on ground loop
constexpr uint32_t SIT_GROUND_UP = 98; // Transition: sitting → standing
constexpr uint32_t SLEEP_DOWN = 99; // Transition: standing → sleeping
constexpr uint32_t SLEEP = 100; // Sleeping loop
constexpr uint32_t SLEEP_UP = 101; // Transition: sleeping → standing
constexpr uint32_t SIT_CHAIR_LOW = 102; // Sit in low chair
constexpr uint32_t SIT_CHAIR_MED = 103; // Sit in medium chair
constexpr uint32_t SIT_CHAIR_HIGH = 104; // Sit in high chair
constexpr uint32_t LOAD_BOW = 105; // Nock/load bow
constexpr uint32_t LOAD_RIFLE = 106; // Load rifle/gun
constexpr uint32_t ATTACK_THROWN = 107; // Thrown weapon attack
constexpr uint32_t READY_THROWN = 108; // Thrown weapon ready
constexpr uint32_t HOLD_BOW = 109; // Hold bow idle
constexpr uint32_t HOLD_RIFLE = 110; // Hold rifle/gun idle
constexpr uint32_t HOLD_THROWN = 111; // Hold thrown weapon idle
constexpr uint32_t LOAD_THROWN = 112; // Load thrown weapon
constexpr uint32_t EMOTE_SALUTE = 113; // /salute
constexpr uint32_t KNEEL_START = 114; // Transition: standing → kneeling
constexpr uint32_t KNEEL_LOOP = 115; // Kneeling loop
constexpr uint32_t KNEEL_END = 116; // Transition: kneeling → standing
constexpr uint32_t ATTACK_UNARMED_OFF = 117; // Off-hand unarmed attack
constexpr uint32_t SPECIAL_UNARMED = 118; // Special unarmed attack
constexpr uint32_t STEALTH_WALK = 119; // Stealth walking (rogue sneak)
constexpr uint32_t STEALTH_STAND = 120; // Stealth standing idle
constexpr uint32_t KNOCKDOWN = 121; // Knocked down
constexpr uint32_t EATING_LOOP = 122; // Eating loop (food/drink)
constexpr uint32_t USE_STANDING_LOOP = 123; // Use standing loop
constexpr uint32_t CHANNEL_CAST_DIRECTED = 124; // Channeled directed cast
constexpr uint32_t CHANNEL_CAST_OMNI = 125; // Channeled omni cast
constexpr uint32_t WHIRLWIND = 126; // Whirlwind attack (warrior)
constexpr uint32_t BIRTH = 127; // Creature birth/spawn
constexpr uint32_t USE_STANDING_START = 128; // Use standing start
constexpr uint32_t USE_STANDING_END = 129; // Use standing end
constexpr uint32_t CREATURE_SPECIAL = 130; // Creature special ability
constexpr uint32_t DROWN = 131; // Drowning
constexpr uint32_t DROWNED = 132; // Drowned corpse underwater
constexpr uint32_t FISHING_CAST = 133; // Fishing cast
constexpr uint32_t FISHING_LOOP = 134; // Fishing idle loop
constexpr uint32_t FLY = 135; // Flying generic
constexpr uint32_t EMOTE_WORK_NO_SHEATHE = 136; // Work emote (no weapon sheathe)
constexpr uint32_t EMOTE_STUN_NO_SHEATHE = 137; // Stun emote (no weapon sheathe)
constexpr uint32_t EMOTE_USE_STANDING_NO_SHEATHE = 138; // Use standing (no weapon sheathe)
constexpr uint32_t SPELL_SLEEP_DOWN = 139; // Spell-induced sleep down
constexpr uint32_t SPELL_KNEEL_START = 140; // Spell-induced kneel start
constexpr uint32_t SPELL_KNEEL_LOOP = 141; // Spell-induced kneel loop
constexpr uint32_t SPELL_KNEEL_END = 142; // Spell-induced kneel end
constexpr uint32_t SPRINT = 143; // Sprint / Custom Spell 01
constexpr uint32_t IN_FLIGHT = 144; // In-flight (flight path travel)
constexpr uint32_t SPAWN = 145; // Object/creature spawn animation
// ============================================================================
// The Burning Crusade (TBC 2.x) — Flying mounts, game objects, stealth run
// IDs 146199
// ============================================================================
constexpr uint32_t CLOSE = 146; // Game object close
constexpr uint32_t CLOSED = 147; // Game object closed loop
constexpr uint32_t OPEN = 148; // Game object open
constexpr uint32_t DESTROY = 149; // Game object destroy
constexpr uint32_t DESTROYED = 150; // Game object destroyed state
constexpr uint32_t UNSHEATHE = 151; // Unsheathe weapons
constexpr uint32_t SHEATHE_ALT = 152; // Sheathe weapons (alternate)
constexpr uint32_t ATTACK_UNARMED_NO_SHEATHE = 153; // Unarmed attack (no sheathe)
constexpr uint32_t STEALTH_RUN = 154; // Stealth running (rogue sprint)
constexpr uint32_t READY_CROSSBOW = 155; // Crossbow ready stance
constexpr uint32_t ATTACK_CROSSBOW = 156; // Crossbow attack
constexpr uint32_t EMOTE_TALK_EXCLAMATION = 157; // /talk with exclamation
constexpr uint32_t FLY_IDLE = 158; // Flying mount idle / hovering
constexpr uint32_t FLY_FORWARD = 159; // Flying mount forward
constexpr uint32_t FLY_BACKWARDS = 160; // Flying mount backwards
constexpr uint32_t FLY_LEFT = 161; // Flying mount strafe left
constexpr uint32_t FLY_RIGHT = 162; // Flying mount strafe right
constexpr uint32_t FLY_UP = 163; // Flying mount ascending
constexpr uint32_t FLY_DOWN = 164; // Flying mount descending
constexpr uint32_t FLY_LAND_START = 165; // Flying mount land start
constexpr uint32_t FLY_LAND_RUN = 166; // Flying mount land run
constexpr uint32_t FLY_LAND_END = 167; // Flying mount land end
constexpr uint32_t EMOTE_TALK_QUESTION = 168; // /talk with question
constexpr uint32_t EMOTE_READ = 169; // /read (reading animation)
constexpr uint32_t EMOTE_SHIELDBLOCK = 170; // Shield block emote
constexpr uint32_t EMOTE_CHOP = 171; // Chopping emote (lumber)
constexpr uint32_t EMOTE_HOLDRIFLE = 172; // Hold rifle emote
constexpr uint32_t EMOTE_HOLDBOW = 173; // Hold bow emote
constexpr uint32_t EMOTE_HOLDTHROWN = 174; // Hold thrown weapon emote
constexpr uint32_t CUSTOM_SPELL_02 = 175; // Custom spell animation 02
constexpr uint32_t CUSTOM_SPELL_03 = 176; // Custom spell animation 03
constexpr uint32_t CUSTOM_SPELL_04 = 177; // Custom spell animation 04
constexpr uint32_t CUSTOM_SPELL_05 = 178; // Custom spell animation 05
constexpr uint32_t CUSTOM_SPELL_06 = 179; // Custom spell animation 06
constexpr uint32_t CUSTOM_SPELL_07 = 180; // Custom spell animation 07
constexpr uint32_t CUSTOM_SPELL_08 = 181; // Custom spell animation 08
constexpr uint32_t CUSTOM_SPELL_09 = 182; // Custom spell animation 09
constexpr uint32_t CUSTOM_SPELL_10 = 183; // Custom spell animation 10
constexpr uint32_t EMOTE_STATE_DANCE = 184; // /dance state (looping dance)
// ============================================================================
// Wrath of the Lich King (WotLK 3.x) — Vehicles, reclined, crafting, etc.
// IDs 185+
// ============================================================================
constexpr uint32_t FLY_STAND = 185; // Flying stand (hover in place)
constexpr uint32_t EMOTE_STATE_LAUGH = 186; // /laugh state loop
constexpr uint32_t EMOTE_STATE_POINT = 187; // /point state loop
constexpr uint32_t EMOTE_STATE_EAT = 188; // /eat state loop
constexpr uint32_t EMOTE_STATE_WORK = 189; // /work state loop (crafting NPC)
constexpr uint32_t EMOTE_STATE_SIT_GROUND = 190; // /sit ground state loop
constexpr uint32_t EMOTE_STATE_HOLD_BOW = 191; // Hold bow state loop
constexpr uint32_t EMOTE_STATE_HOLD_RIFLE = 192; // Hold rifle state loop
constexpr uint32_t EMOTE_STATE_HOLD_THROWN = 193; // Hold thrown state loop
constexpr uint32_t FLY_COMBAT_WOUND = 194; // Flying wounded
constexpr uint32_t FLY_COMBAT_CRITICAL = 195; // Flying critical hit reaction
constexpr uint32_t RECLINED = 196; // Reclined / laid back pose
constexpr uint32_t EMOTE_STATE_ROAR = 197; // /roar state loop
constexpr uint32_t EMOTE_USE_STANDING_LOOP_2 = 198; // Use standing loop variant
constexpr uint32_t EMOTE_STATE_APPLAUD = 199; // /applaud state loop
constexpr uint32_t READY_FIST = 200; // Fist weapon ready stance
constexpr uint32_t SPELL_CHANNEL_DIRECTED_OMNI = 201; // Channel directed omni
constexpr uint32_t SPECIAL_ATTACK_1H_OFF = 202; // Special off-hand one-handed attack
constexpr uint32_t ATTACK_FIST_1H = 203; // Fist weapon one-hand attack
constexpr uint32_t ATTACK_FIST_1H_OFF = 204; // Fist weapon off-hand attack
constexpr uint32_t PARRY_FIST_1H = 205; // Fist weapon parry
constexpr uint32_t READY_FIST_1H = 206; // Fist weapon one-hand ready
constexpr uint32_t EMOTE_STATE_READ_AND_TALK = 207; // Read and talk NPC loop
constexpr uint32_t EMOTE_STATE_WORK_NO_SHEATHE = 208; // Work no sheathe state loop
constexpr uint32_t FLY_RUN = 209; // Flying run (fast forward flight)
constexpr uint32_t EMOTE_STATE_KNEEL_2 = 210; // Kneel state variant
constexpr uint32_t EMOTE_STATE_SPELL_KNEEL = 211; // Spell kneel state loop
constexpr uint32_t EMOTE_STATE_USE_STANDING = 212; // Use standing state
constexpr uint32_t EMOTE_STATE_STUN = 213; // Stun state loop
constexpr uint32_t EMOTE_STATE_STUN_NO_SHEATHE = 214; // Stun no sheathe state
constexpr uint32_t EMOTE_TRAIN = 215; // /train — choo choo!
constexpr uint32_t EMOTE_DEAD = 216; // /dead — play dead
constexpr uint32_t EMOTE_STATE_DANCE_ONCE = 217; // Single dance animation
constexpr uint32_t FLY_DEATH = 218; // Flying death
constexpr uint32_t FLY_STAND_WOUND = 219; // Flying wounded stand
constexpr uint32_t FLY_SHUFFLE_LEFT = 220; // Flying strafe left
constexpr uint32_t FLY_SHUFFLE_RIGHT = 221; // Flying strafe right
constexpr uint32_t FLY_WALK_BACKWARDS = 222; // Flying walk backwards
constexpr uint32_t FLY_STUN = 223; // Flying stunned
constexpr uint32_t FLY_HANDS_CLOSED = 224; // Flying hands closed
constexpr uint32_t FLY_ATTACK_UNARMED = 225; // Flying unarmed attack
constexpr uint32_t FLY_ATTACK_1H = 226; // Flying one-hand attack
constexpr uint32_t FLY_ATTACK_2H = 227; // Flying two-hand attack
constexpr uint32_t FLY_ATTACK_2H_LOOSE = 228; // Flying polearm attack
constexpr uint32_t FLY_SPELL = 229; // Flying spell — generic spell while flying
constexpr uint32_t FLY_STOP = 230; // Flying stop
constexpr uint32_t FLY_WALK = 231; // Flying walk
constexpr uint32_t FLY_DEAD = 232; // Flying dead (corpse mid-air)
constexpr uint32_t FLY_RISE = 233; // Flying rise — resurrection mid-air
constexpr uint32_t FLY_RUN_2 = 234; // Flying run variant
constexpr uint32_t FLY_FALL = 235; // Flying fall
constexpr uint32_t FLY_SWIM_IDLE = 236; // Flying swim idle
constexpr uint32_t FLY_SWIM = 237; // Flying swim
constexpr uint32_t FLY_SWIM_LEFT = 238; // Flying swim left
constexpr uint32_t FLY_SWIM_RIGHT = 239; // Flying swim right
constexpr uint32_t FLY_SWIM_BACKWARDS = 240; // Flying swim backwards
constexpr uint32_t FLY_ATTACK_BOW = 241; // Flying bow attack
constexpr uint32_t FLY_FIRE_BOW = 242; // Flying fire bow
constexpr uint32_t FLY_READY_RIFLE = 243; // Flying rifle ready
constexpr uint32_t FLY_ATTACK_RIFLE = 244; // Flying rifle attack
// ── WotLK Vehicle & extended movement animations ──────────────────────────
constexpr uint32_t TOTEM_SMALL = 245; // Small totem idle (shaman)
constexpr uint32_t TOTEM_MEDIUM = 246; // Medium totem idle
constexpr uint32_t TOTEM_LARGE = 247; // Large totem idle
constexpr uint32_t FLY_LOOT = 248; // Flying loot
constexpr uint32_t FLY_READY_SPELL_DIRECTED = 249; // Flying directed spell ready
constexpr uint32_t FLY_READY_SPELL_OMNI = 250; // Flying omni spell ready
constexpr uint32_t FLY_SPELL_CAST_DIRECTED = 251; // Flying directed spell cast
constexpr uint32_t FLY_SPELL_CAST_OMNI = 252; // Flying omni spell cast
constexpr uint32_t FLY_BATTLE_ROAR = 253; // Flying battle shout
constexpr uint32_t FLY_READY_ABILITY = 254; // Flying ability ready
constexpr uint32_t FLY_SPECIAL_1H = 255; // Flying special one-hand
constexpr uint32_t FLY_SPECIAL_2H = 256; // Flying special two-hand
constexpr uint32_t FLY_SHIELD_BASH = 257; // Flying shield bash
constexpr uint32_t FLY_EMOTE_TALK = 258; // Flying emote talk
constexpr uint32_t FLY_EMOTE_EAT = 259; // Flying emote eat
constexpr uint32_t FLY_EMOTE_WORK = 260; // Flying emote work
constexpr uint32_t FLY_EMOTE_USE_STANDING = 261; // Flying emote use standing
constexpr uint32_t FLY_EMOTE_BOW = 262; // Flying emote bow
constexpr uint32_t FLY_EMOTE_WAVE = 263; // Flying emote wave
constexpr uint32_t FLY_EMOTE_CHEER = 264; // Flying emote cheer
constexpr uint32_t FLY_EMOTE_DANCE = 265; // Flying emote dance
constexpr uint32_t FLY_EMOTE_LAUGH = 266; // Flying emote laugh
constexpr uint32_t FLY_EMOTE_SLEEP = 267; // Flying emote sleep
constexpr uint32_t FLY_EMOTE_SIT_GROUND = 268; // Flying emote sit ground
constexpr uint32_t FLY_EMOTE_RUDE = 269; // Flying emote rude
constexpr uint32_t FLY_EMOTE_ROAR = 270; // Flying emote roar
constexpr uint32_t FLY_EMOTE_KNEEL = 271; // Flying emote kneel
constexpr uint32_t FLY_EMOTE_KISS = 272; // Flying emote kiss
constexpr uint32_t FLY_EMOTE_CRY = 273; // Flying emote cry
constexpr uint32_t FLY_EMOTE_CHICKEN = 274; // Flying emote chicken
constexpr uint32_t FLY_EMOTE_BEG = 275; // Flying emote beg
constexpr uint32_t FLY_EMOTE_APPLAUD = 276; // Flying emote applaud
constexpr uint32_t FLY_EMOTE_SHOUT = 277; // Flying emote shout
constexpr uint32_t FLY_EMOTE_FLEX = 278; // Flying emote flex
constexpr uint32_t FLY_EMOTE_SHY = 279; // Flying emote shy
constexpr uint32_t FLY_EMOTE_POINT = 280; // Flying emote point
constexpr uint32_t FLY_ATTACK_1H_PIERCE = 281; // Flying one-hand pierce
constexpr uint32_t FLY_ATTACK_2H_LOOSE_PIERCE = 282; // Flying polearm pierce
constexpr uint32_t FLY_ATTACK_OFF = 283; // Flying off-hand attack
constexpr uint32_t FLY_ATTACK_OFF_PIERCE = 284; // Flying off-hand pierce
constexpr uint32_t FLY_SHEATHE = 285; // Flying sheathe
constexpr uint32_t FLY_HIP_SHEATHE = 286; // Flying hip sheathe
constexpr uint32_t FLY_MOUNT = 287; // Flying mounted
constexpr uint32_t FLY_RUN_RIGHT = 288; // Flying strafe run right
constexpr uint32_t FLY_RUN_LEFT = 289; // Flying strafe run left
constexpr uint32_t FLY_MOUNT_SPECIAL = 290; // Flying mount special
constexpr uint32_t FLY_KICK = 291; // Flying kick
constexpr uint32_t FLY_SIT_GROUND_DOWN = 292; // Flying sit ground down
constexpr uint32_t FLY_SITTING = 293; // Flying sitting
constexpr uint32_t FLY_SIT_GROUND_UP = 294; // Flying sit ground up
constexpr uint32_t FLY_SLEEP_DOWN = 295; // Flying sleep down
constexpr uint32_t FLY_SLEEP = 296; // Flying sleeping
constexpr uint32_t FLY_SLEEP_UP = 297; // Flying sleep up
constexpr uint32_t FLY_SIT_CHAIR_LOW = 298; // Flying sit chair low
constexpr uint32_t FLY_SIT_CHAIR_MED = 299; // Flying sit chair med
constexpr uint32_t FLY_SIT_CHAIR_HIGH = 300; // Flying sit chair high
constexpr uint32_t FLY_LOAD_BOW = 301; // Flying load bow
constexpr uint32_t FLY_LOAD_RIFLE = 302; // Flying load rifle
constexpr uint32_t FLY_ATTACK_THROWN = 303; // Flying thrown attack
constexpr uint32_t FLY_READY_THROWN = 304; // Flying thrown ready
constexpr uint32_t FLY_HOLD_BOW = 305; // Flying hold bow
constexpr uint32_t FLY_HOLD_RIFLE = 306; // Flying hold rifle
constexpr uint32_t FLY_HOLD_THROWN = 307; // Flying hold thrown
constexpr uint32_t FLY_LOAD_THROWN = 308; // Flying load thrown
constexpr uint32_t FLY_EMOTE_SALUTE = 309; // Flying emote salute
constexpr uint32_t FLY_KNEEL_START = 310; // Flying kneel start
constexpr uint32_t FLY_KNEEL_LOOP = 311; // Flying kneel loop
constexpr uint32_t FLY_KNEEL_END = 312; // Flying kneel end
constexpr uint32_t FLY_ATTACK_UNARMED_OFF = 313; // Flying off-hand unarmed
constexpr uint32_t FLY_SPECIAL_UNARMED = 314; // Flying special unarmed
constexpr uint32_t FLY_STEALTH_WALK = 315; // Flying stealth walk
constexpr uint32_t FLY_STEALTH_STAND = 316; // Flying stealth stand
constexpr uint32_t FLY_KNOCKDOWN = 317; // Flying knockdown
constexpr uint32_t FLY_EATING_LOOP = 318; // Flying eating loop
constexpr uint32_t FLY_USE_STANDING_LOOP = 319; // Flying use standing loop
constexpr uint32_t FLY_CHANNEL_CAST_DIRECTED = 320; // Flying directed channel
constexpr uint32_t FLY_CHANNEL_CAST_OMNI = 321; // Flying omni channel
constexpr uint32_t FLY_WHIRLWIND = 322; // Flying whirlwind
constexpr uint32_t FLY_BIRTH = 323; // Flying birth/spawn
constexpr uint32_t FLY_USE_STANDING_START = 324; // Flying use standing start
constexpr uint32_t FLY_USE_STANDING_END = 325; // Flying use standing end
constexpr uint32_t FLY_CREATURE_SPECIAL = 326; // Flying creature special
constexpr uint32_t FLY_DROWN = 327; // Flying drown
constexpr uint32_t FLY_DROWNED = 328; // Flying drowned
constexpr uint32_t FLY_FISHING_CAST = 329; // Flying fishing cast
constexpr uint32_t FLY_FISHING_LOOP = 330; // Flying fishing loop
constexpr uint32_t FLY_FLY = 331; // Flying fly
constexpr uint32_t FLY_EMOTE_WORK_NO_SHEATHE = 332; // Flying work no sheathe
constexpr uint32_t FLY_EMOTE_STUN_NO_SHEATHE = 333; // Flying stun no sheathe
constexpr uint32_t FLY_EMOTE_USE_STANDING_NO_SHEATHE = 334; // Flying use standing no sheathe
constexpr uint32_t FLY_SPELL_SLEEP_DOWN = 335; // Flying spell sleep down
constexpr uint32_t FLY_SPELL_KNEEL_START = 336; // Flying spell kneel start
constexpr uint32_t FLY_SPELL_KNEEL_LOOP = 337; // Flying spell kneel loop
constexpr uint32_t FLY_SPELL_KNEEL_END = 338; // Flying spell kneel end
constexpr uint32_t FLY_SPRINT = 339; // Flying sprint
constexpr uint32_t FLY_IN_FLIGHT = 340; // Flying in-flight
constexpr uint32_t FLY_SPAWN = 341; // Flying spawn
constexpr uint32_t FLY_CLOSE = 342; // Flying close
constexpr uint32_t FLY_CLOSED = 343; // Flying closed
constexpr uint32_t FLY_OPEN = 344; // Flying open
constexpr uint32_t FLY_DESTROY = 345; // Flying destroy
constexpr uint32_t FLY_DESTROYED = 346; // Flying destroyed
constexpr uint32_t FLY_UNSHEATHE = 347; // Flying unsheathe
constexpr uint32_t FLY_SHEATHE_ALT = 348; // Flying sheathe alt
constexpr uint32_t FLY_ATTACK_UNARMED_NO_SHEATHE = 349; // Flying unarmed no sheathe
constexpr uint32_t FLY_STEALTH_RUN = 350; // Flying stealth run
constexpr uint32_t FLY_READY_CROSSBOW = 351; // Flying crossbow ready
constexpr uint32_t FLY_ATTACK_CROSSBOW = 352; // Flying crossbow attack
constexpr uint32_t FLY_EMOTE_TALK_EXCLAMATION = 353; // Flying talk exclamation
constexpr uint32_t FLY_EMOTE_TALK_QUESTION = 354; // Flying talk question
constexpr uint32_t FLY_EMOTE_READ = 355; // Flying emote read
// ── WotLK extended creature animations ────────────────────────────────────
constexpr uint32_t EMOTE_HOLD_CROSSBOW = 356; // Hold crossbow emote
constexpr uint32_t FLY_EMOTE_HOLD_BOW = 357; // Flying hold bow emote
constexpr uint32_t FLY_EMOTE_HOLD_RIFLE = 358; // Flying hold rifle emote
constexpr uint32_t FLY_EMOTE_HOLD_THROWN = 359; // Flying hold thrown emote
constexpr uint32_t FLY_EMOTE_HOLD_CROSSBOW = 360; // Flying hold crossbow emote
constexpr uint32_t FLY_CUSTOM_SPELL_02 = 361; // Flying custom spell 02
constexpr uint32_t FLY_CUSTOM_SPELL_03 = 362; // Flying custom spell 03
constexpr uint32_t FLY_CUSTOM_SPELL_04 = 363; // Flying custom spell 04
constexpr uint32_t FLY_CUSTOM_SPELL_05 = 364; // Flying custom spell 05
constexpr uint32_t FLY_CUSTOM_SPELL_06 = 365; // Flying custom spell 06
constexpr uint32_t FLY_CUSTOM_SPELL_07 = 366; // Flying custom spell 07
constexpr uint32_t FLY_CUSTOM_SPELL_08 = 367; // Flying custom spell 08
constexpr uint32_t FLY_CUSTOM_SPELL_09 = 368; // Flying custom spell 09
constexpr uint32_t FLY_CUSTOM_SPELL_10 = 369; // Flying custom spell 10
constexpr uint32_t FLY_EMOTE_STATE_DANCE = 370; // Flying dance state
constexpr uint32_t EMOTE_EAT_NO_SHEATHE = 371; // Eat emote (no weapon sheathe)
constexpr uint32_t MOUNT_RUN_RIGHT = 372; // Mounted strafe run right
constexpr uint32_t MOUNT_RUN_LEFT = 373; // Mounted strafe run left
constexpr uint32_t MOUNT_WALK_BACKWARDS = 374; // Mounted walk backwards
constexpr uint32_t MOUNT_SWIM_IDLE = 375; // Mounted swimming idle
constexpr uint32_t MOUNT_SWIM = 376; // Mounted swimming forward
constexpr uint32_t MOUNT_SWIM_LEFT = 377; // Mounted swimming left
constexpr uint32_t MOUNT_SWIM_RIGHT = 378; // Mounted swimming right
constexpr uint32_t MOUNT_SWIM_BACKWARDS = 379; // Mounted swimming backwards
constexpr uint32_t MOUNT_FLIGHT_IDLE = 380; // Mounted flight idle (hovering)
constexpr uint32_t MOUNT_FLIGHT_FORWARD = 381; // Mounted flight forward
constexpr uint32_t MOUNT_FLIGHT_BACKWARDS = 382; // Mounted flight backwards
constexpr uint32_t MOUNT_FLIGHT_LEFT = 383; // Mounted flight left
constexpr uint32_t MOUNT_FLIGHT_RIGHT = 384; // Mounted flight right
constexpr uint32_t MOUNT_FLIGHT_UP = 385; // Mounted flight ascending
constexpr uint32_t MOUNT_FLIGHT_DOWN = 386; // Mounted flight descending
constexpr uint32_t MOUNT_FLIGHT_LAND_START = 387; // Mounted flight land start
constexpr uint32_t MOUNT_FLIGHT_LAND_RUN = 388; // Mounted flight land run
constexpr uint32_t MOUNT_FLIGHT_LAND_END = 389; // Mounted flight land end
constexpr uint32_t FLY_EMOTE_STATE_LAUGH = 390; // Flying laugh state
constexpr uint32_t FLY_EMOTE_STATE_POINT = 391; // Flying point state
constexpr uint32_t FLY_EMOTE_STATE_EAT = 392; // Flying eat state
constexpr uint32_t FLY_EMOTE_STATE_WORK = 393; // Flying work state
constexpr uint32_t FLY_EMOTE_STATE_SIT_GROUND = 394; // Flying sit ground state
constexpr uint32_t FLY_EMOTE_STATE_HOLD_BOW = 395; // Flying hold bow state
constexpr uint32_t FLY_EMOTE_STATE_HOLD_RIFLE = 396; // Flying hold rifle state
constexpr uint32_t FLY_EMOTE_STATE_HOLD_THROWN = 397; // Flying hold thrown state
constexpr uint32_t FLY_EMOTE_STATE_ROAR = 398; // Flying roar state
constexpr uint32_t FLY_RECLINED = 399; // Flying reclined
constexpr uint32_t EMOTE_TRAIN_2 = 400; // /train variant — choo choo!
constexpr uint32_t EMOTE_DEAD_2 = 401; // /dead variant (play dead)
constexpr uint32_t FLY_EMOTE_USE_STANDING_LOOP_2 = 402; // Flying use standing loop
constexpr uint32_t FLY_EMOTE_STATE_APPLAUD = 403; // Flying applaud state
constexpr uint32_t FLY_READY_FIST = 404; // Flying fist ready
constexpr uint32_t FLY_SPELL_CHANNEL_DIRECTED_OMNI = 405; // Flying channel directed omni
constexpr uint32_t FLY_SPECIAL_ATTACK_1H_OFF = 406; // Flying special off-hand
constexpr uint32_t FLY_ATTACK_FIST_1H = 407; // Flying fist attack
constexpr uint32_t FLY_ATTACK_FIST_1H_OFF = 408; // Flying fist off-hand
constexpr uint32_t FLY_PARRY_FIST_1H = 409; // Flying fist parry
constexpr uint32_t FLY_READY_FIST_1H = 410; // Flying fist one-hand ready
constexpr uint32_t FLY_EMOTE_STATE_READ_AND_TALK = 411; // Flying read and talk state
constexpr uint32_t FLY_EMOTE_STATE_WORK_NO_SHEATHE = 412; // Flying work no sheathe state
constexpr uint32_t FLY_EMOTE_STATE_KNEEL_2 = 413; // Flying kneel state variant
constexpr uint32_t FLY_EMOTE_STATE_SPELL_KNEEL = 414; // Flying spell kneel state
constexpr uint32_t FLY_EMOTE_STATE_USE_STANDING = 415; // Flying use standing state
constexpr uint32_t FLY_EMOTE_STATE_STUN = 416; // Flying stun state
constexpr uint32_t FLY_EMOTE_STATE_STUN_NO_SHEATHE = 417; // Flying stun no sheathe state
constexpr uint32_t FLY_EMOTE_TRAIN = 418; // Flying train emote
constexpr uint32_t FLY_EMOTE_DEAD = 419; // Flying dead emote
constexpr uint32_t FLY_EMOTE_STATE_DANCE_ONCE = 420; // Flying single dance
constexpr uint32_t FLY_EMOTE_EAT_NO_SHEATHE = 421; // Flying eat no sheathe
constexpr uint32_t FLY_MOUNT_RUN_RIGHT = 422; // Flying mount run right
constexpr uint32_t FLY_MOUNT_RUN_LEFT = 423; // Flying mount run left
constexpr uint32_t FLY_MOUNT_WALK_BACKWARDS = 424; // Flying mount walk backwards
constexpr uint32_t FLY_MOUNT_SWIM_IDLE = 425; // Flying mount swim idle
constexpr uint32_t FLY_MOUNT_SWIM = 426; // Flying mount swim
constexpr uint32_t FLY_MOUNT_SWIM_LEFT = 427; // Flying mount swim left
constexpr uint32_t FLY_MOUNT_SWIM_RIGHT = 428; // Flying mount swim right
constexpr uint32_t FLY_MOUNT_SWIM_BACKWARDS = 429; // Flying mount swim backwards
constexpr uint32_t FLY_MOUNT_FLIGHT_IDLE = 430; // Flying mount flight idle
constexpr uint32_t FLY_MOUNT_FLIGHT_FORWARD = 431; // Flying mount flight forward
constexpr uint32_t FLY_MOUNT_FLIGHT_BACKWARDS = 432; // Flying mount flight backwards
constexpr uint32_t FLY_MOUNT_FLIGHT_LEFT = 433; // Flying mount flight left
constexpr uint32_t FLY_MOUNT_FLIGHT_RIGHT = 434; // Flying mount flight right
constexpr uint32_t FLY_MOUNT_FLIGHT_UP = 435; // Flying mount flight up
constexpr uint32_t FLY_MOUNT_FLIGHT_DOWN = 436; // Flying mount flight down
constexpr uint32_t FLY_MOUNT_FLIGHT_LAND_START = 437; // Flying mount flight land start
constexpr uint32_t FLY_MOUNT_FLIGHT_LAND_RUN = 438; // Flying mount flight land run
constexpr uint32_t FLY_MOUNT_FLIGHT_LAND_END = 439; // Flying mount flight land end
constexpr uint32_t FLY_TOTEM_SMALL = 440; // Flying small totem
constexpr uint32_t FLY_TOTEM_MEDIUM = 441; // Flying medium totem
constexpr uint32_t FLY_TOTEM_LARGE = 442; // Flying large totem
constexpr uint32_t FLY_EMOTE_HOLD_CROSSBOW_2 = 443; // Flying hold crossbow (variant)
// ── WotLK vehicle-specific & late additions ───────────────────────────────
constexpr uint32_t VEHICLE_GRAB = 444; // Vehicle: grab object
constexpr uint32_t VEHICLE_THROW = 445; // Vehicle: throw object
constexpr uint32_t FLY_VEHICLE_GRAB = 446; // Flying vehicle grab
constexpr uint32_t FLY_VEHICLE_THROW = 447; // Flying vehicle throw
constexpr uint32_t GUILD_CHAMPION_1 = 448; // Guild champion pose 1
constexpr uint32_t GUILD_CHAMPION_2 = 449; // Guild champion pose 2
constexpr uint32_t FLY_GUILD_CHAMPION_1 = 450; // Flying guild champion 1
constexpr uint32_t FLY_GUILD_CHAMPION_2 = 451; // Flying guild champion 2
// Total number of animation IDs (0451 inclusive)
constexpr uint32_t ANIM_COUNT = 452;
/// Return the symbolic name for an animation ID (e.g. 0 → "STAND").
/// Returns "UNKNOWN" for IDs outside the known range.
const char* nameFromId(uint32_t id);
/// Return the FLY_* variant of a ground animation ID, or 0 if none exists.
uint32_t flyVariant(uint32_t groundId);
/// Validate animation_ids.hpp constants against AnimationData.dbc.
/// Logs warnings for IDs present in DBC but missing from constants, and vice versa.
void validateAgainstDBC(const std::shared_ptr<wowee::pipeline::DBCFile>& dbc);
} // namespace anim
} // namespace rendering
} // namespace wowee

View file

@ -118,6 +118,9 @@ public:
void setFeatherFallActive(bool active) { featherFallActive_ = active; }
void setWaterWalkActive(bool active) { waterWalkActive_ = active; }
void setFlyingActive(bool active) { flyingActive_ = active; }
bool isFlyingActive() const { return flyingActive_; }
bool isAscending() const { return wasAscending_; }
bool isDescending() const { return wasDescending_; }
void setHoverActive(bool active) { hoverActive_ = active; }
void setMounted(bool m) { mounted_ = m; }
void setMountHeightOffset(float offset) { mountHeightOffset_ = offset; }

View file

@ -295,7 +295,7 @@ public:
*/
/** Pre-allocate GPU resources (bone SSBOs, descriptors) on main thread before parallel render. */
void prepareRender(uint32_t frameIndex, const Camera& camera);
/** Phase 2.3: Dispatch GPU frustum culling compute shader on primary cmd before render pass. */
/** Dispatch GPU frustum culling compute shader on primary cmd before render pass. */
void dispatchCullCompute(VkCommandBuffer cmd, uint32_t frameIndex, const Camera& camera);
void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera);
@ -329,6 +329,11 @@ public:
void setInstancePosition(uint32_t instanceId, const glm::vec3& position);
void setInstanceTransform(uint32_t instanceId, const glm::mat4& transform);
void setInstanceAnimationFrozen(uint32_t instanceId, bool frozen);
/// Set the animation sequence by animation ID (e.g. anim::OPEN, anim::CLOSE).
/// Finds the first sequence with matching ID. Unfreezes the instance and resets time.
void setInstanceAnimation(uint32_t instanceId, uint32_t animationId, bool loop = true);
/// Check if a model instance has a specific animation ID in its sequence table.
bool hasAnimation(uint32_t instanceId, uint32_t animationId) const;
float getInstanceAnimDuration(uint32_t instanceId) const;
void removeInstance(uint32_t instanceId);
void removeInstances(const std::vector<uint32_t>& instanceIds);
@ -439,7 +444,7 @@ private:
void* megaBoneMapped_[2] = {};
VkDescriptorSet megaBoneSet_[2] = {};
// Phase 2.1: GPU instance data SSBO — per-instance transforms, fade, bones for instanced draws.
// GPU instance data SSBO — per-instance transforms, fade, bones for instanced draws.
// Shader reads instanceData[push.instanceDataOffset + gl_InstanceIndex].
struct M2InstanceGPU {
glm::mat4 model; // 64 bytes @ offset 0
@ -458,7 +463,7 @@ private:
VkDescriptorSet instanceSet_[2] = {};
uint32_t instanceDataCount_ = 0; // reset each frame in render()
// Phase 2.3: GPU Frustum Culling via Compute Shader
// GPU Frustum Culling via Compute Shader
// Compute shader tests each M2 instance against frustum planes + distance, writes visibility[].
// CPU reads back visibility to build sortedVisible_ without per-instance frustum/distance tests.
struct CullInstanceGPU { // matches CullInstance in m2_cull.comp.glsl (32 bytes, std430)

View file

@ -9,7 +9,7 @@
namespace wowee {
namespace rendering {
// Phase 2.5: Lightweight Render Graph / Frame Graph
// Lightweight Render Graph / Frame Graph
// Converts hardcoded pass sequence (shadow → reflection → compute cull →
// main → post-process → ImGui → present) into declarative graph nodes.
// Graph auto-inserts VkImageMemoryBarrier between passes.

View file

@ -53,6 +53,7 @@ class AmdFsr3Runtime;
class SpellVisualSystem;
class PostProcessPipeline;
class AnimationController;
enum class RangedWeaponType : uint8_t;
class LevelUpEffect;
class ChargeEffect;
class SwimEffects;
@ -176,7 +177,13 @@ public:
void resetCombatVisualState();
bool isMoving() const;
void triggerMeleeSwing();
void setEquippedWeaponType(uint32_t inventoryType);
void setEquippedWeaponType(uint32_t inventoryType, bool is2HLoose = false,
bool isFist = false, bool isDagger = false,
bool hasOffHand = false, bool hasShield = false);
void triggerSpecialAttack(uint32_t spellId);
void setEquippedRangedType(RangedWeaponType type);
void triggerRangedShot();
RangedWeaponType getEquippedRangedType() const;
void setCharging(bool charging);
bool isCharging() const;
void startChargeEffect(const glm::vec3& position, const glm::vec3& direction);
@ -386,7 +393,7 @@ private:
VkBuffer reflPerFrameUBO = VK_NULL_HANDLE;
VmaAllocation reflPerFrameUBOAlloc = VK_NULL_HANDLE;
void* reflPerFrameUBOMapped = nullptr;
VkDescriptorSet reflPerFrameDescSet = VK_NULL_HANDLE;
VkDescriptorSet reflPerFrameDescSet[MAX_FRAMES] = {};
bool createPerFrameResources();
void destroyPerFrameResources();
@ -434,7 +441,7 @@ private:
bool ghostMode_ = false; // set each frame from gameHandler->isPlayerGhost()
// Phase 2.5: Render Graph — declarative pass ordering with automatic barriers
// Render Graph — declarative pass ordering with automatic barriers
std::unique_ptr<RenderGraph> renderGraph_;
void buildFrameGraph(game::GameHandler* gameHandler);

View file

@ -60,7 +60,7 @@ struct TerrainChunkGPU {
float boundingSphereRadius = 0.0f;
glm::vec3 boundingSphereCenter = glm::vec3(0.0f);
// Phase 2.2: Offsets into mega buffers for indirect drawing (-1 = not in mega buffer)
// Offsets into mega buffers for indirect drawing (-1 = not in mega buffer)
int32_t megaBaseVertex = -1;
uint32_t megaFirstIndex = 0;
uint32_t vertexCount = 0;
@ -206,7 +206,7 @@ private:
int renderedChunks = 0;
int culledChunks = 0;
// Phase 2.2: Mega vertex/index buffers for indirect drawing
// Mega vertex/index buffers for indirect drawing
// All terrain chunks share a single VB + IB, eliminating per-chunk rebinds.
// Indirect draw commands are built CPU-side each frame for visible chunks.
VkBuffer megaVB_ = VK_NULL_HANDLE;

View file

@ -131,6 +131,35 @@ private:
std::vector<std::string> introTracks_;
bool loginMusicVolumeAdjusted_ = false;
int savedMusicVolume_ = 30;
// ----- Login-screen graphics settings popup -----
bool showLoginSettings_ = false;
// Local copies of the settings keys we expose in the login popup.
// Loaded on first open; saved on Apply.
struct LoginGraphicsState {
int preset = 2; // 0=Custom 1=Low 2=Medium 3=High 4=Ultra
bool shadows = true;
float shadowDistance = 300.0f;
int antiAliasing = 0; // 0=Off 1=2x 2=4x 3=8x
bool fxaa = false;
bool normalMapping = true;
bool pom = true;
int pomQuality = 1; // 0=Low 1=Medium 2=High
int upscalingMode = 0; // 0=Off 1=FSR1 2=FSR3
bool waterRefraction = true;
int groundClutter = 100; // 0-150
int brightness = 50; // 0-100
bool vsync = false;
bool fullscreen = false;
};
LoginGraphicsState loginGfx_;
bool loginGfxLoaded_ = false;
void renderLoginSettingsWindow();
void loadLoginGraphicsState();
void saveLoginGraphicsState();
static void applyPresetToState(LoginGraphicsState& s, int preset);
};
}} // namespace wowee::ui

View file

@ -1,6 +1,8 @@
#include "core/application.hpp"
#include "core/coordinates.hpp"
#include "core/profiler.hpp"
#include "rendering/animation_ids.hpp"
#include "rendering/animation_controller.hpp"
#include <unordered_set>
#include <cmath>
#include <chrono>
@ -941,7 +943,7 @@ void Application::setState(AppState newState) {
});
cc->setStandUpCallback([this]() {
if (gameHandler) {
gameHandler->setStandState(0); // CMSG_STAND_STATE_CHANGE(STAND)
gameHandler->setStandState(rendering::AnimationController::STAND_STATE_STAND);
}
});
cc->setAutoFollowCancelCallback([this]() {
@ -952,9 +954,16 @@ void Application::setState(AppState newState) {
cc->setUseWoWSpeed(true);
}
if (gameHandler) {
gameHandler->setMeleeSwingCallback([this]() {
gameHandler->setMeleeSwingCallback([this](uint32_t spellId) {
if (renderer) {
renderer->triggerMeleeSwing();
// Ranged auto-attack spells: Auto Shot (75), Shoot (5019), Throw (2764)
if (spellId == 75 || spellId == 5019 || spellId == 2764) {
renderer->triggerRangedShot();
} else if (spellId != 0) {
renderer->triggerSpecialAttack(spellId);
} else {
renderer->triggerMeleeSwing();
}
}
});
gameHandler->setKnockBackCallback([this](float vcos, float vsin, float hspeed, float vspeed) {
@ -1924,17 +1933,17 @@ void Application::update(float deltaTime) {
_creatureWasWalking[guid] = isWalkingNow;
uint32_t curAnimId = 0; float curT = 0.0f, curDur = 0.0f;
bool gotState = charRenderer->getAnimationState(instanceId, curAnimId, curT, curDur);
if (!gotState || curAnimId != 1 /*Death*/) {
if (!gotState || curAnimId != rendering::anim::DEATH) {
uint32_t targetAnim;
if (isMovingNow) {
if (isFlyingNow) targetAnim = 159u; // FlyForward
else if (isSwimmingNow) targetAnim = 42u; // Swim
else if (isWalkingNow) targetAnim = 4u; // Walk
else targetAnim = 5u; // Run
if (isFlyingNow) targetAnim = rendering::anim::FLY_FORWARD;
else if (isSwimmingNow) targetAnim = rendering::anim::SWIM;
else if (isWalkingNow) targetAnim = rendering::anim::WALK;
else targetAnim = rendering::anim::RUN;
} else {
if (isFlyingNow) targetAnim = 158u; // FlyIdle (hover)
else if (isSwimmingNow) targetAnim = 41u; // SwimIdle
else targetAnim = 0u; // Stand
if (isFlyingNow) targetAnim = rendering::anim::FLY_IDLE;
else if (isSwimmingNow) targetAnim = rendering::anim::SWIM_IDLE;
else targetAnim = rendering::anim::STAND;
}
charRenderer->playAnimation(instanceId, targetAnim, /*loop=*/true);
}
@ -2038,17 +2047,17 @@ void Application::update(float deltaTime) {
_pCreatureWasWalking[guid] = isWalkingNow;
uint32_t curAnimId = 0; float curT = 0.0f, curDur = 0.0f;
bool gotState = charRenderer->getAnimationState(instanceId, curAnimId, curT, curDur);
if (!gotState || curAnimId != 1 /*Death*/) {
if (!gotState || curAnimId != rendering::anim::DEATH) {
uint32_t targetAnim;
if (isMovingNow) {
if (isFlyingNow) targetAnim = 159u; // FlyForward
else if (isSwimmingNow) targetAnim = 42u; // Swim
else if (isWalkingNow) targetAnim = 4u; // Walk
else targetAnim = 5u; // Run
if (isFlyingNow) targetAnim = rendering::anim::FLY_FORWARD;
else if (isSwimmingNow) targetAnim = rendering::anim::SWIM;
else if (isWalkingNow) targetAnim = rendering::anim::WALK;
else targetAnim = rendering::anim::RUN;
} else {
if (isFlyingNow) targetAnim = 158u; // FlyIdle (hover)
else if (isSwimmingNow) targetAnim = 41u; // SwimIdle
else targetAnim = 0u; // Stand
if (isFlyingNow) targetAnim = rendering::anim::FLY_IDLE;
else if (isSwimmingNow) targetAnim = rendering::anim::SWIM_IDLE;
else targetAnim = rendering::anim::STAND;
}
charRenderer->playAnimation(instanceId, targetAnim, /*loop=*/true);
}
@ -2748,19 +2757,70 @@ void Application::setupUICallbacks() {
});
// GameObject custom animation callback (e.g. chest opening)
gameHandler->setGameObjectCustomAnimCallback([this](uint64_t guid, uint32_t /*animId*/) {
if (!entitySpawner_) return;
gameHandler->setGameObjectCustomAnimCallback([this](uint64_t guid, uint32_t animId) {
if (!entitySpawner_ || !renderer) return;
auto& goInstances = entitySpawner_->getGameObjectInstances();
auto it = goInstances.find(guid);
if (it == goInstances.end() || !renderer) return;
if (it == goInstances.end()) return;
auto& info = it->second;
if (!info.isWmo) {
if (auto* m2r = renderer->getM2Renderer()) {
m2r->setInstanceAnimationFrozen(info.instanceId, false);
// Play the custom animation as a one-shot if model supports it
if (m2r->hasAnimation(info.instanceId, animId))
m2r->setInstanceAnimation(info.instanceId, animId, false);
else
m2r->setInstanceAnimationFrozen(info.instanceId, false);
}
}
});
// GameObject state change callback — animate doors/chests opening/closing/destroying
gameHandler->setGameObjectStateCallback([this](uint64_t guid, uint8_t goState) {
if (!entitySpawner_ || !renderer) return;
auto& goInstances = entitySpawner_->getGameObjectInstances();
auto it = goInstances.find(guid);
if (it == goInstances.end()) return;
auto& info = it->second;
if (info.isWmo) return; // WMOs don't have M2 animation sequences
auto* m2r = renderer->getM2Renderer();
if (!m2r) return;
uint32_t instId = info.instanceId;
// GO states: 0=READY(closed), 1=OPEN, 2=DESTROYED/ACTIVE
if (goState == 1) {
// Opening: play OPEN(148) one-shot, fall back to unfreezing
if (m2r->hasAnimation(instId, 148))
m2r->setInstanceAnimation(instId, 148, false);
else
m2r->setInstanceAnimationFrozen(instId, false);
} else if (goState == 2) {
// Destroyed: play DESTROY(149) one-shot
if (m2r->hasAnimation(instId, 149))
m2r->setInstanceAnimation(instId, 149, false);
} else {
// Closed: play CLOSE(146) one-shot, else freeze
if (m2r->hasAnimation(instId, 146))
m2r->setInstanceAnimation(instId, 146, false);
else
m2r->setInstanceAnimationFrozen(instId, true);
}
});
// Sprint aura callback — use SPRINT(143) animation when sprint-type buff is active
gameHandler->setSprintAuraCallback([this](bool active) {
if (!renderer) return;
auto* ac = renderer->getAnimationController();
if (ac) ac->setSprintAuraActive(active);
});
// Vehicle state callback — hide player character when inside a vehicle
gameHandler->setVehicleStateCallback([this](bool entered, uint32_t /*vehicleId*/) {
if (!renderer) return;
auto* cr = renderer->getCharacterRenderer();
uint32_t instId = renderer->getCharacterInstanceId();
if (!cr || instId == 0) return;
cr->setInstanceVisible(instId, !entered);
});
// Charge callback — warrior rushes toward target
gameHandler->setChargeCallback([this](uint64_t targetGuid, float tx, float ty, float tz) {
if (!renderer || !renderer->getCameraController() || !gameHandler) return;
@ -3059,8 +3119,8 @@ void Application::setupUICallbacks() {
auto* cr = renderer->getCharacterRenderer();
bool gotState = cr->getAnimationState(instanceId, curAnimId, curT, curDur);
// Only start Run if not already running and not in Death animation.
if (!gotState || (curAnimId != 1 /*Death*/ && curAnimId != 5u /*Run*/)) {
cr->playAnimation(instanceId, 5u, /*loop=*/true);
if (!gotState || (curAnimId != rendering::anim::DEATH && curAnimId != rendering::anim::RUN)) {
cr->playAnimation(instanceId, rendering::anim::RUN, /*loop=*/true);
}
entitySpawner_->getCreatureWasMoving()[guid] = true;
}
@ -3256,11 +3316,11 @@ void Application::setupUICallbacks() {
uint32_t instanceId = entitySpawner_->getCreatureInstanceId(guid);
if (instanceId == 0) instanceId = entitySpawner_->getPlayerInstanceId(guid);
if (instanceId != 0) {
renderer->getCharacterRenderer()->playAnimation(instanceId, 1, false); // Death
renderer->getCharacterRenderer()->playAnimation(instanceId, rendering::anim::DEATH, false);
}
});
// NPC/player respawn callback (online mode) - reset to idle animation
// NPC/player respawn callback (online mode) - play rise animation then idle
gameHandler->setNpcRespawnCallback([this](uint64_t guid) {
if (!entitySpawner_) return;
entitySpawner_->unmarkCreatureDead(guid);
@ -3268,11 +3328,18 @@ void Application::setupUICallbacks() {
uint32_t instanceId = entitySpawner_->getCreatureInstanceId(guid);
if (instanceId == 0) instanceId = entitySpawner_->getPlayerInstanceId(guid);
if (instanceId != 0) {
renderer->getCharacterRenderer()->playAnimation(instanceId, 0, true); // Idle
auto* cr = renderer->getCharacterRenderer();
// Play RISE one-shot (auto-returns to STAND when finished), fall back to STAND
if (cr->hasAnimation(instanceId, rendering::anim::RISE))
cr->playAnimation(instanceId, rendering::anim::RISE, false);
else
cr->playAnimation(instanceId, rendering::anim::STAND, true);
}
});
// NPC/player swing callback (online mode) - play attack animation
// Probes the model for the best available attack animation:
// ATTACK_1H(17) → ATTACK_2H(18) → ATTACK_2H_LOOSE(19) → ATTACK_UNARMED(16)
gameHandler->setNpcSwingCallback([this](uint64_t guid) {
if (!entitySpawner_) return;
if (!renderer || !renderer->getCharacterRenderer()) return;
@ -3280,8 +3347,12 @@ void Application::setupUICallbacks() {
if (instanceId == 0) instanceId = entitySpawner_->getPlayerInstanceId(guid);
if (instanceId != 0) {
auto* cr = renderer->getCharacterRenderer();
// Try weapon-appropriate attack anim: 17=1H, 18=2H, 16=unarmed fallback
static const uint32_t attackAnims[] = {17, 18, 16};
static const uint32_t attackAnims[] = {
rendering::anim::ATTACK_1H,
rendering::anim::ATTACK_2H,
rendering::anim::ATTACK_2H_LOOSE,
rendering::anim::ATTACK_UNARMED
};
bool played = false;
for (uint32_t anim : attackAnims) {
if (cr->hasAnimation(instanceId, anim)) {
@ -3290,10 +3361,70 @@ void Application::setupUICallbacks() {
break;
}
}
if (!played) cr->playAnimation(instanceId, 16, false);
if (!played) cr->playAnimation(instanceId, rendering::anim::ATTACK_UNARMED, false);
}
});
// Hit reaction callback — plays one-shot dodge/block/wound animation on the victim
gameHandler->setHitReactionCallback([this](uint64_t victimGuid, game::GameHandler::HitReaction reaction) {
if (!renderer) return;
auto* cr = renderer->getCharacterRenderer();
if (!cr) return;
// Determine animation based on reaction type
uint32_t animId = rendering::anim::COMBAT_WOUND;
switch (reaction) {
case game::GameHandler::HitReaction::DODGE: animId = rendering::anim::DODGE; break;
case game::GameHandler::HitReaction::PARRY: break; // Parry already handled by existing system
case game::GameHandler::HitReaction::BLOCK: animId = rendering::anim::BLOCK; break;
case game::GameHandler::HitReaction::SHIELD_BLOCK: animId = rendering::anim::SHIELD_BLOCK; break;
case game::GameHandler::HitReaction::CRIT_WOUND: animId = rendering::anim::COMBAT_CRITICAL; break;
case game::GameHandler::HitReaction::WOUND: animId = rendering::anim::COMBAT_WOUND; break;
}
// For local player: use AnimationController state
bool isLocalPlayer = (victimGuid == gameHandler->getPlayerGuid());
if (isLocalPlayer) {
auto* ac = renderer->getAnimationController();
if (ac) {
uint32_t charInstId = renderer->getCharacterInstanceId();
if (charInstId && cr->hasAnimation(charInstId, animId))
ac->triggerHitReaction(animId);
}
return;
}
// For NPCs/other players: direct playAnimation
if (!entitySpawner_) return;
uint32_t instanceId = entitySpawner_->getCreatureInstanceId(victimGuid);
if (instanceId == 0) instanceId = entitySpawner_->getPlayerInstanceId(victimGuid);
if (instanceId != 0 && cr->hasAnimation(instanceId, animId))
cr->playAnimation(instanceId, animId, false);
});
// Stun state callback — enters/exits STUNNED animation on local player
gameHandler->setStunStateCallback([this](bool stunned) {
if (!renderer) return;
auto* ac = renderer->getAnimationController();
if (ac) ac->setStunned(stunned);
});
// Stealth state callback — switches to stealth animation variants
gameHandler->setStealthStateCallback([this](bool stealthed) {
if (!renderer) return;
auto* ac = renderer->getAnimationController();
if (ac) ac->setStealthed(stealthed);
});
// Player health callback — switches to wounded idle when HP < 20%
gameHandler->setPlayerHealthCallback([this](uint32_t health, uint32_t maxHealth) {
if (!renderer) return;
auto* ac = renderer->getAnimationController();
if (!ac) return;
bool lowHp = (maxHealth > 0) && (health > 0) && (health * 5 <= maxHealth);
ac->setLowHealth(lowHp);
});
// Unit animation hint callback — plays jump (38=JumpMid) animation on other players/NPCs.
// Swim/walking state is now authoritative from the move-flags callback below.
// animId=38 (JumpMid): airborne jump animation; land detection is via per-frame sync.
@ -3305,9 +3436,9 @@ void Application::setupUICallbacks() {
uint32_t instanceId = entitySpawner_->getPlayerInstanceId(guid);
if (instanceId == 0) instanceId = entitySpawner_->getCreatureInstanceId(guid);
if (instanceId == 0) return;
// Don't override Death animation (1)
// Don't override Death animation
uint32_t curAnim = 0; float curT = 0.0f, curDur = 0.0f;
if (cr->getAnimationState(instanceId, curAnim, curT, curDur) && curAnim == 1) return;
if (cr->getAnimationState(instanceId, curAnim, curT, curDur) && curAnim == rendering::anim::DEATH) return;
cr->playAnimation(instanceId, animId, /*loop=*/true);
});
@ -3331,16 +3462,23 @@ void Application::setupUICallbacks() {
else flyState.erase(guid);
});
// Emote animation callback — play server-driven emote animations on NPCs and other players
// Emote animation callback — play server-driven emote animations on NPCs and other players.
// When emoteAnim is 0, the NPC's emote state was cleared → revert to STAND.
// Non-zero values from UNIT_NPC_EMOTESTATE updates are persistent (played looping).
gameHandler->setEmoteAnimCallback([this](uint64_t guid, uint32_t emoteAnim) {
if (!entitySpawner_) return;
if (!renderer || emoteAnim == 0) return;
if (!renderer) return;
auto* cr = renderer->getCharacterRenderer();
if (!cr) return;
// Look up creature instance first, then online players
uint32_t emoteInstanceId = entitySpawner_->getCreatureInstanceId(guid);
if (emoteInstanceId != 0) {
cr->playAnimation(emoteInstanceId, emoteAnim, false);
if (emoteAnim == 0) {
// Emote state cleared → return to idle
cr->playAnimation(emoteInstanceId, rendering::anim::STAND, true);
} else {
cr->playAnimation(emoteInstanceId, emoteAnim, false);
}
return;
}
emoteInstanceId = entitySpawner_->getPlayerInstanceId(guid);
@ -3350,34 +3488,134 @@ void Application::setupUICallbacks() {
});
// Spell cast animation callback — play cast animation on caster (player or NPC/other player)
gameHandler->setSpellCastAnimCallback([this](uint64_t guid, bool start, bool /*isChannel*/) {
// Probes the model for the best available spell animation with fallback chain:
// Regular cast: SPELL_CAST_DIRECTED(53) → SPELL_CAST_OMNI(54) → SPELL_CAST(32) → SPELL(2)
// Channel: CHANNEL_CAST_DIRECTED(124) → CHANNEL_CAST_OMNI(125) → SPELL_CAST_DIRECTED(53) → SPELL(2)
// For the local player, uses AnimationController state machine to prevent
// COMBAT_IDLE from overriding the spell animation. For NPCs/other players,
// calls playAnimation directly (they don't share the player state machine).
gameHandler->setSpellCastAnimCallback([this](uint64_t guid, bool start, bool isChannel) {
if (!entitySpawner_) return;
if (!renderer) return;
auto* cr = renderer->getCharacterRenderer();
if (!cr) return;
// Animation 3 = SpellCast (one-shot; return-to-idle handled by character_renderer)
const uint32_t castAnim = 3;
// Check player character
// Determine if this is the local player
bool isLocalPlayer = false;
uint32_t instanceId = 0;
{
uint32_t charInstId = renderer->getCharacterInstanceId();
if (charInstId != 0 && guid == gameHandler->getPlayerGuid()) {
if (start) cr->playAnimation(charInstId, castAnim, false);
// On finish: playAnimation(castAnim, loop=false) will auto-return to Stand
return;
instanceId = charInstId;
isLocalPlayer = true;
}
}
// Check creatures and other online players
{
uint32_t cInst = entitySpawner_->getCreatureInstanceId(guid);
if (cInst != 0) {
if (start) cr->playAnimation(cInst, castAnim, false);
return;
if (instanceId == 0) instanceId = entitySpawner_->getCreatureInstanceId(guid);
if (instanceId == 0) instanceId = entitySpawner_->getPlayerInstanceId(guid);
if (instanceId == 0) return;
if (start) {
// Detect fishing spells (channeled) — use FISHING_LOOP instead of generic cast
auto isFishingSpell = [](uint32_t spellId) {
return spellId == 7620 || spellId == 7731 || spellId == 7732 ||
spellId == 18248 || spellId == 33095 || spellId == 51294;
};
uint32_t currentSpell = isLocalPlayer ? gameHandler->getCurrentCastSpellId() : 0;
bool isFishing = isChannel && isFishingSpell(currentSpell);
if (isFishing && cr->hasAnimation(instanceId, rendering::anim::FISHING_LOOP)) {
// Fishing: use FISHING_LOOP (looping idle) for the channel duration
if (isLocalPlayer) {
auto* ac = renderer->getAnimationController();
if (ac) ac->startSpellCast(0, rendering::anim::FISHING_LOOP, true, 0);
} else {
cr->playAnimation(instanceId, rendering::anim::FISHING_LOOP, true);
}
} else {
// Spell animation sequence: PRECAST (one-shot) → CAST (loop) → FINALIZE (one-shot) → idle
// Probe model for best available animations with fallback chains:
// Regular cast: SPELL_CAST_DIRECTED → SPELL_CAST_OMNI → SPELL_CAST → SPELL
// Channel: CHANNEL_CAST_DIRECTED → CHANNEL_CAST_OMNI → SPELL_CAST_DIRECTED → SPELL
bool hasTarget = gameHandler->hasTarget();
// Phase 1: Precast wind-up (one-shot, non-channels only)
uint32_t precastAnim = 0;
if (!isChannel && cr->hasAnimation(instanceId, rendering::anim::SPELL_PRECAST)) {
precastAnim = rendering::anim::SPELL_PRECAST;
}
}
{
uint32_t pInst = entitySpawner_->getPlayerInstanceId(guid);
if (pInst != 0) {
if (start) cr->playAnimation(pInst, castAnim, false);
// Phase 2: Cast hold (looping until stopSpellCast)
static const uint32_t castDirected[] = {
rendering::anim::SPELL_CAST_DIRECTED,
rendering::anim::SPELL_CAST_OMNI,
rendering::anim::SPELL_CAST,
rendering::anim::SPELL
};
static const uint32_t castOmni[] = {
rendering::anim::SPELL_CAST_OMNI,
rendering::anim::SPELL_CAST_DIRECTED,
rendering::anim::SPELL_CAST,
rendering::anim::SPELL
};
static const uint32_t channelDirected[] = {
rendering::anim::CHANNEL_CAST_DIRECTED,
rendering::anim::CHANNEL_CAST_OMNI,
rendering::anim::SPELL_CAST_DIRECTED,
rendering::anim::SPELL
};
static const uint32_t channelOmni[] = {
rendering::anim::CHANNEL_CAST_OMNI,
rendering::anim::CHANNEL_CAST_DIRECTED,
rendering::anim::SPELL_CAST_DIRECTED,
rendering::anim::SPELL
};
const uint32_t* chain;
if (isChannel) {
chain = hasTarget ? channelDirected : channelOmni;
} else {
chain = hasTarget ? castDirected : castOmni;
}
uint32_t castAnim = rendering::anim::SPELL;
for (size_t i = 0; i < 4; ++i) {
if (cr->hasAnimation(instanceId, chain[i])) {
castAnim = chain[i];
break;
}
}
// Phase 3: Finalization release (one-shot after cast ends)
// Pick a different animation from the cast loop for visual variety
static const uint32_t finalizeChain[] = {
rendering::anim::SPELL_CAST_OMNI,
rendering::anim::SPELL_CAST,
rendering::anim::SPELL
};
uint32_t finalizeAnim = 0;
if (isLocalPlayer && !isChannel) {
for (uint32_t fa : finalizeChain) {
if (fa != castAnim && cr->hasAnimation(instanceId, fa)) {
finalizeAnim = fa;
break;
}
}
if (finalizeAnim == 0 && cr->hasAnimation(instanceId, rendering::anim::SPELL))
finalizeAnim = rendering::anim::SPELL;
}
if (isLocalPlayer) {
auto* ac = renderer->getAnimationController();
if (ac) ac->startSpellCast(precastAnim, castAnim, true, finalizeAnim);
} else {
cr->playAnimation(instanceId, castAnim, true);
}
} // end !isFishing
} else {
// Cast/channel ended — plays finalization anim completely then returns to idle
if (isLocalPlayer) {
auto* ac = renderer->getAnimationController();
if (ac) ac->stopSpellCast();
} else if (isChannel) {
cr->playAnimation(instanceId, rendering::anim::STAND, true);
}
}
});
@ -3392,41 +3630,54 @@ void Application::setupUICallbacks() {
cr->setInstanceOpacity(charInstId, isGhost ? 0.5f : 1.0f);
});
// Stand state animation callback — map server stand state to M2 animation on player
// and sync camera sit flag so movement is blocked while sitting
// Stand state animation callback — route through AnimationController state machine
// for proper sit/sleep/kneel transition animations (down → loop → up)
gameHandler->setStandStateCallback([this](uint8_t standState) {
if (!renderer) return;
using AC = rendering::AnimationController;
// Sync camera controller sitting flag: block movement while sitting/kneeling
if (auto* cc = renderer->getCameraController()) {
cc->setSitting(standState >= 1 && standState <= 8 && standState != 7);
cc->setSitting(standState >= AC::STAND_STATE_SIT &&
standState <= AC::STAND_STATE_KNEEL &&
standState != AC::STAND_STATE_DEAD);
}
auto* cr = renderer->getCharacterRenderer();
if (!cr) return;
uint32_t charInstId = renderer->getCharacterInstanceId();
if (charInstId == 0) return;
// WoW stand state → M2 animation ID mapping
// 0=Stand→0, 1-6=Sit variants→27 (SitGround), 7=Dead→1, 8=Kneel→72
// Do not force Stand(0) here: locomotion state machine already owns standing/running.
// Forcing Stand on packet timing causes visible run-cycle hitching while steering.
uint32_t animId = 0;
if (standState == 0) {
auto* ac = renderer->getAnimationController();
if (!ac) return;
// Death is special — play directly, not through sit state machine
if (standState == AC::STAND_STATE_DEAD) {
auto* cr = renderer->getCharacterRenderer();
if (!cr) return;
uint32_t charInstId = renderer->getCharacterInstanceId();
if (charInstId == 0) return;
cr->playAnimation(charInstId, rendering::anim::DEATH, false);
return;
} else if (standState >= 1 && standState <= 6) {
animId = 27; // SitGround (covers sit-chair too; correct visual differs by chair height)
} else if (standState == 7) {
animId = 1; // Death
} else if (standState == 8) {
animId = 72; // Kneel
}
// Loop sit/kneel (not death) so the held-pose frame stays visible
const bool loop = (animId != 1);
cr->playAnimation(charInstId, animId, loop);
ac->setStandState(standState);
});
// Loot window callback — play kneel/loot animation while looting
gameHandler->setLootWindowCallback([this](bool open) {
if (!renderer) return;
auto* ac = renderer->getAnimationController();
if (!ac) return;
if (open) ac->startLooting();
else ac->stopLooting();
});
// NPC greeting callback - play voice line
gameHandler->setNpcGreetingCallback([this](uint64_t guid, const glm::vec3& position) {
// Play NPC_WELCOME animation on the NPC
if (entitySpawner_ && renderer) {
auto* cr = renderer->getCharacterRenderer();
if (cr) {
uint32_t instanceId = entitySpawner_->getCreatureInstanceId(guid);
if (instanceId != 0) cr->playAnimation(instanceId, rendering::anim::NPC_WELCOME, false);
}
}
if (audioCoordinator_ && audioCoordinator_->getNpcVoiceManager()) {
// Convert canonical to render coords for 3D audio
glm::vec3 renderPos = core::coords::canonicalToRender(position);
@ -3722,8 +3973,8 @@ void Application::spawnPlayerCharacter() {
: std::unordered_set<uint16_t>{};
charRenderer->setActiveGeosets(instanceId, activeGeosets);
// Play idle animation (Stand = animation ID 0)
charRenderer->playAnimation(instanceId, 0, true);
// Play idle animation
charRenderer->playAnimation(instanceId, rendering::anim::STAND, true);
LOG_INFO("Spawned player character at (",
static_cast<int>(spawnPos.x), ", ",
static_cast<int>(spawnPos.y), ", ",

View file

@ -9,6 +9,7 @@
#include "audio/npc_voice_manager.hpp"
#include "pipeline/m2_loader.hpp"
#include "pipeline/wmo_loader.hpp"
#include "rendering/animation_ids.hpp"
#include "pipeline/dbc_loader.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_layout.hpp"
@ -2214,9 +2215,26 @@ void EntitySpawner::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float
// Spawn in the correct pose. If the server marked this creature dead before
// the queued spawn was processed, start directly in death animation.
if (deadCreatureGuids_.count(guid)) {
charRenderer->playAnimation(instanceId, 1, false); // Death
charRenderer->playAnimation(instanceId, rendering::anim::DEATH, false);
} else {
charRenderer->playAnimation(instanceId, 0, true); // Idle
// Check if this NPC has a persistent emote state (e.g. working, eating, dancing)
uint32_t npcEmote = 0;
if (gameHandler_) {
auto entity = gameHandler_->getEntityManager().getEntity(guid);
if (entity && entity->getType() == game::ObjectType::UNIT) {
npcEmote = std::static_pointer_cast<game::Unit>(entity)->getNpcEmoteState();
}
}
if (npcEmote != 0 && charRenderer->hasAnimation(instanceId, npcEmote)) {
charRenderer->playAnimation(instanceId, npcEmote, true);
} else if (charRenderer->hasAnimation(instanceId, rendering::anim::BIRTH)) {
// Play birth animation (one-shot) — will return to STAND after
charRenderer->playAnimation(instanceId, rendering::anim::BIRTH, false);
} else if (charRenderer->hasAnimation(instanceId, rendering::anim::SPAWN)) {
charRenderer->playAnimation(instanceId, rendering::anim::SPAWN, false);
} else {
charRenderer->playAnimation(instanceId, rendering::anim::STAND, true);
}
}
charRenderer->startFadeIn(instanceId, 0.5f);
@ -2316,7 +2334,7 @@ void EntitySpawner::spawnOnlinePlayer(uint64_t guid,
for (uint32_t si = 0; si < model.sequences.size(); si++) {
if (!(model.sequences[si].flags & 0x20)) {
uint32_t animId = model.sequences[si].id;
if (animId != 0 && animId != 4 && animId != 5) continue;
if (animId != rendering::anim::STAND && animId != rendering::anim::WALK && animId != rendering::anim::RUN) continue;
char animFileName[256];
snprintf(animFileName, sizeof(animFileName),
"%s%s%04u-%02u.anim",
@ -2488,7 +2506,7 @@ void EntitySpawner::spawnOnlinePlayer(uint64_t guid,
activeGeosets.insert(kGeosetBareFeet);
charRenderer->setActiveGeosets(instanceId, activeGeosets);
charRenderer->playAnimation(instanceId, 0, true);
charRenderer->playAnimation(instanceId, rendering::anim::STAND, true);
playerInstances_[guid] = instanceId;
OnlinePlayerAppearanceState st;
@ -3373,7 +3391,21 @@ void EntitySpawner::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_
lowerPath.find("portalfx") != std::string::npos ||
lowerPath.find("spellportal") != std::string::npos);
if (!isAnimatedEffect && !isTransportGO) {
m2Renderer->setInstanceAnimationFrozen(instanceId, true);
// Check for totem idle animations — totems should animate, not freeze
bool isTotem = false;
if (m2Renderer->hasAnimation(instanceId, 245)) { // TOTEM_SMALL
m2Renderer->setInstanceAnimation(instanceId, 245, true);
isTotem = true;
} else if (m2Renderer->hasAnimation(instanceId, 246)) { // TOTEM_MEDIUM
m2Renderer->setInstanceAnimation(instanceId, 246, true);
isTotem = true;
} else if (m2Renderer->hasAnimation(instanceId, 247)) { // TOTEM_LARGE
m2Renderer->setInstanceAnimation(instanceId, 247, true);
isTotem = true;
}
if (!isTotem) {
m2Renderer->setInstanceAnimationFrozen(instanceId, true);
}
}
gameObjectInstances_[guid] = {modelId, instanceId, false};
@ -4601,8 +4633,8 @@ void EntitySpawner::processPendingMount() {
for (uint32_t si = 0; si < model.sequences.size(); si++) {
if (!(model.sequences[si].flags & 0x20)) {
uint32_t animId = model.sequences[si].id;
// Only load stand(0), walk(4), run(5) anims to avoid hang
if (animId != 0 && animId != 4 && animId != 5) continue;
// Only load stand, walk, run anims to avoid hang
if (animId != rendering::anim::STAND && animId != rendering::anim::WALK && animId != rendering::anim::RUN) continue;
char animFileName[256];
snprintf(animFileName, sizeof(animFileName), "%s%04u-%02u.anim",
basePath.c_str(), animId, model.sequences[si].variationIndex);
@ -4854,10 +4886,11 @@ void EntitySpawner::processPendingMount() {
// For taxi mounts, start with flying animation; for ground mounts, start with stand
bool isTaxi = gameHandler_ && gameHandler_->isOnTaxiFlight();
uint32_t startAnim = 0; // ANIM_STAND
uint32_t startAnim = rendering::anim::STAND;
if (isTaxi) {
// Try WotLK fly anims first, then Vanilla-friendly fallbacks
uint32_t taxiCandidates[] = {159, 158, 234, 229, 233, 141, 369, 6, 5}; // FlyForward, FlyIdle, FlyRun(234), FlyStand(229), FlyWalk(233), FlyMounted, FlyRun, Fly, Run
using namespace rendering::anim;
uint32_t taxiCandidates[] = {FLY_FORWARD, FLY_IDLE, FLY_RUN_2, FLY_SPELL, FLY_RISE, SPELL_KNEEL_LOOP, FLY_CUSTOM_SPELL_10, DEAD, RUN};
for (uint32_t anim : taxiCandidates) {
if (charRenderer->hasAnimation(instanceId, anim)) {
startAnim = anim;

View file

@ -3,6 +3,7 @@
#include "core/world_loader.hpp"
#include "core/application.hpp"
#include "rendering/animation_ids.hpp"
#include "core/entity_spawner.hpp"
#include "core/appearance_composer.hpp"
#include "core/window.hpp"
@ -876,7 +877,7 @@ void WorldLoader::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
uint32_t instanceId = spawner->getCreatureInstanceId(guid);
if (instanceId == 0) instanceId = spawner->getPlayerInstanceId(guid);
if (instanceId != 0 && cr) {
cr->playAnimation(instanceId, 1, false); // animation ID 1 = Death
cr->playAnimation(instanceId, rendering::anim::DEATH, false);
}
});
@ -885,15 +886,30 @@ void WorldLoader::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
uint32_t instanceId = spawner->getCreatureInstanceId(guid);
if (instanceId == 0) instanceId = spawner->getPlayerInstanceId(guid);
if (instanceId != 0 && cr) {
cr->playAnimation(instanceId, 0, true); // animation ID 0 = Idle
cr->playAnimation(instanceId, rendering::anim::STAND, true);
}
});
// Probe the creature model for the best available attack animation
gameHandler_->setNpcSwingCallback([cr, spawner](uint64_t guid) {
uint32_t instanceId = spawner->getCreatureInstanceId(guid);
if (instanceId == 0) instanceId = spawner->getPlayerInstanceId(guid);
if (instanceId != 0 && cr) {
cr->playAnimation(instanceId, 16, false); // animation ID 16 = Attack1
static const uint32_t attackAnims[] = {
rendering::anim::ATTACK_1H,
rendering::anim::ATTACK_2H,
rendering::anim::ATTACK_2H_LOOSE,
rendering::anim::ATTACK_UNARMED
};
bool played = false;
for (uint32_t anim : attackAnims) {
if (cr->hasAnimation(instanceId, anim)) {
cr->playAnimation(instanceId, anim, false);
played = true;
break;
}
}
if (!played) cr->playAnimation(instanceId, rendering::anim::ATTACK_UNARMED, false);
}
});
}

View file

@ -123,6 +123,9 @@ void CombatHandler::registerOpcodes(DispatchTable& table) {
addCombatText(CombatTextEntry::ABSORB, static_cast<int32_t>(envAbs), 0, false, 0, 0, victimGuid);
if (envRes > 0)
addCombatText(CombatTextEntry::RESIST, static_cast<int32_t>(envRes), 0, false, 0, 0, victimGuid);
// Drowning damage → play DROWN one-shot on player
if (envType == 1 && dmg > 0 && owner_.emoteAnimCallback_)
owner_.emoteAnimCallback_(victimGuid, 131); // anim::DROWN
}
packet.skipAll();
};
@ -440,7 +443,7 @@ void CombatHandler::handleAttackerStateUpdate(network::Packet& packet) {
lastMeleeSwingMs_ = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count());
if (owner_.meleeSwingCallback_) owner_.meleeSwingCallback_();
if (owner_.meleeSwingCallback_) owner_.meleeSwingCallback_(0);
}
if (!isPlayerAttacker && owner_.npcSwingCallback_) {
owner_.npcSwingCallback_(data.attackerGuid);
@ -520,6 +523,17 @@ void CombatHandler::handleAttackerStateUpdate(network::Packet& packet) {
addCombatText(CombatTextEntry::RESIST, static_cast<int32_t>(totalResisted), 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
}
// Fire hit reaction animation on the victim
if (owner_.hitReactionCallback_ && !data.isMiss()) {
using HR = GameHandler::HitReaction;
HR reaction = HR::WOUND;
if (data.victimState == 1) reaction = HR::DODGE;
else if (data.victimState == 2) reaction = HR::PARRY;
else if (data.victimState == 4) reaction = HR::BLOCK;
else if (data.isCrit()) reaction = HR::CRIT_WOUND;
owner_.hitReactionCallback_(data.targetGuid, reaction);
}
}
void CombatHandler::handleSpellDamageLog(network::Packet& packet) {

View file

@ -542,6 +542,7 @@ EntityController::UnitFieldIndices EntityController::UnitFieldIndices::resolve()
fieldIndex(UF::UNIT_FIELD_DISPLAYID),
fieldIndex(UF::UNIT_FIELD_MOUNTDISPLAYID),
fieldIndex(UF::UNIT_NPC_FLAGS),
fieldIndex(UF::UNIT_NPC_EMOTESTATE),
fieldIndex(UF::UNIT_FIELD_BYTES_0),
fieldIndex(UF::UNIT_FIELD_BYTES_1)
};
@ -697,6 +698,7 @@ bool EntityController::applyUnitFieldsOnCreate(const UpdateBlock& block,
}
}
else if (key == ufi.npcFlags) { unit->setNpcFlags(val); }
else if (key == ufi.npcEmoteState) { unit->setNpcEmoteState(val); }
else if (key == ufi.dynFlags) {
unit->setDynamicFlags(val);
if (block.objectType == ObjectType::UNIT &&
@ -795,7 +797,28 @@ EntityController::UnitFieldUpdateResult EntityController::applyUnitFieldsOnUpdat
if (!uid.empty())
pendingEvents_.emit("UNIT_DISPLAYPOWER", {uid});
}
} else if (key == ufi.flags) { unit->setUnitFlags(val); }
} else if (key == ufi.flags) {
uint32_t oldFlags = unit->getUnitFlags();
unit->setUnitFlags(val);
// Detect stun state change on local player
constexpr uint32_t UNIT_FLAG_STUNNED = 0x00040000;
if (block.guid == owner_.playerGuid && owner_.stunStateCallback_) {
bool wasStunned = (oldFlags & UNIT_FLAG_STUNNED) != 0;
bool nowStunned = (val & UNIT_FLAG_STUNNED) != 0;
if (wasStunned != nowStunned) {
owner_.stunStateCallback_(nowStunned);
}
}
// Detect stealth state change on local player
constexpr uint32_t UNIT_FLAG_SNEAKING = 0x02000000;
if (block.guid == owner_.playerGuid && owner_.stealthStateCallback_) {
bool wasStealth = (oldFlags & UNIT_FLAG_SNEAKING) != 0;
bool nowStealth = (val & UNIT_FLAG_SNEAKING) != 0;
if (wasStealth != nowStealth) {
owner_.stealthStateCallback_(nowStealth);
}
}
}
else if (ufi.bytes1 != 0xFFFF && key == ufi.bytes1 && block.guid == owner_.playerGuid) {
uint8_t newForm = static_cast<uint8_t>((val >> 24) & 0xFF);
if (newForm != owner_.shapeshiftFormId_) {
@ -863,6 +886,14 @@ EntityController::UnitFieldUpdateResult EntityController::applyUnitFieldsOnUpdat
}
unit->setMountDisplayId(val);
} else if (key == ufi.npcFlags) { unit->setNpcFlags(val); }
else if (key == ufi.npcEmoteState) {
uint32_t oldEmote = unit->getNpcEmoteState();
unit->setNpcEmoteState(val);
// Fire emote animation callback so entity_spawner can update the NPC's idle anim
if (val != oldEmote && owner_.emoteAnimCallback_) {
owner_.emoteAnimCallback_(block.guid, val);
}
}
// Power/maxpower range checks AFTER all specific fields
else if (key >= ufi.powerBase && key < ufi.powerBase + 7) {
unit->setPowerByType(static_cast<uint8_t>(key - ufi.powerBase), val);
@ -889,6 +920,11 @@ EntityController::UnitFieldUpdateResult EntityController::applyUnitFieldsOnUpdat
}
}
// Fire player health callback for wounded-idle animation
if (result.healthChanged && block.guid == owner_.playerGuid && owner_.playerHealthCallback_) {
owner_.playerHealthCallback_(unit->getHealth(), unit->getMaxHealth());
}
return result;
}
@ -1632,6 +1668,17 @@ void EntityController::onValuesUpdateGameObject(const UpdateBlock& block, std::s
entity->getZ(), entity->getOrientation());
}
}
// Detect GO state changes from GAMEOBJECT_BYTES_1 (packed: byte0=state, byte1=type, byte2=artKit, byte3=animProgress)
const uint16_t ufGoBytes1 = fieldIndex(UF::GAMEOBJECT_BYTES_1);
if (ufGoBytes1 != 0xFFFF) {
auto itB = block.fields.find(ufGoBytes1);
if (itB != block.fields.end()) {
uint8_t goState = static_cast<uint8_t>(itB->second & 0xFF);
if (owner_.gameObjectStateCallback_)
owner_.gameObjectStateCallback_(block.guid, goState);
}
}
}
// ============================================================

View file

@ -31,6 +31,7 @@
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_loader.hpp"
#include "core/logger.hpp"
#include "rendering/animation_ids.hpp"
#include <glm/gtx/quaternion.hpp>
#include <algorithm>
#include <cmath>
@ -1275,12 +1276,25 @@ void GameHandler::registerOpcodeHandlers() {
};
// Consume silently — opcodes we receive but don't need to act on
for (auto op : {
Opcode::SMSG_GAMEOBJECT_DESPAWN_ANIM, Opcode::SMSG_GAMEOBJECT_RESET_STATE,
Opcode::SMSG_FLIGHT_SPLINE_SYNC, Opcode::SMSG_FORCE_DISPLAY_UPDATE,
Opcode::SMSG_FORCE_SEND_QUEUED_PACKETS, Opcode::SMSG_FORCE_SET_VEHICLE_REC_ID,
Opcode::SMSG_CORPSE_MAP_POSITION_QUERY_RESPONSE, Opcode::SMSG_DAMAGE_CALC_LOG,
Opcode::SMSG_DYNAMIC_DROP_ROLL_RESULT, Opcode::SMSG_DESTRUCTIBLE_BUILDING_DAMAGE,
}) { registerSkipHandler(op); }
// Game object despawn animation — reset state to closed before actual despawn
dispatchTable_[Opcode::SMSG_GAMEOBJECT_DESPAWN_ANIM] = [this](network::Packet& packet) {
if (!packet.hasRemaining(8)) return;
uint64_t guid = packet.readUInt64();
// Trigger a CLOSE animation / freeze before the object is removed
if (gameObjectStateCallback_) gameObjectStateCallback_(guid, 0);
};
// Game object reset state — return to READY(closed) state
dispatchTable_[Opcode::SMSG_GAMEOBJECT_RESET_STATE] = [this](network::Packet& packet) {
if (!packet.hasRemaining(8)) return;
uint64_t guid = packet.readUInt64();
if (gameObjectStateCallback_) gameObjectStateCallback_(guid, 0);
};
dispatchTable_[Opcode::SMSG_FORCED_DEATH_UPDATE] = [this](network::Packet& packet) {
playerDead_ = true;
if (ghostStateCallback_) ghostStateCallback_(false);
@ -2124,10 +2138,15 @@ void GameHandler::registerOpcodeHandlers() {
if (packet.hasRemaining(1)) {
(void)packet.readPackedGuid(); // player guid (unused)
}
uint32_t newVehicleId = 0;
if (packet.hasRemaining(4)) {
vehicleId_ = packet.readUInt32();
} else {
vehicleId_ = 0;
newVehicleId = packet.readUInt32();
}
bool wasInVehicle = vehicleId_ != 0;
bool nowInVehicle = newVehicleId != 0;
vehicleId_ = newVehicleId;
if (wasInVehicle != nowInVehicle && vehicleStateCallback_) {
vehicleStateCallback_(nowInVehicle, newVehicleId);
}
};
// guid(8) + status(1): status 1 = NPC has available/new routes for this player
@ -2842,6 +2861,9 @@ void GameHandler::registerOpcodeHandlers() {
};
dispatchTable_[Opcode::SMSG_ON_CANCEL_EXPECTED_RIDE_VEHICLE_AURA] = [this](network::Packet& packet) {
vehicleId_ = 0; // Vehicle ride cancelled; clear UI
if (vehicleStateCallback_) {
vehicleStateCallback_(false, 0);
}
packet.skipAll();
};
// uint32 type (0=normal, 1=heavy, 2=tired/restricted) + uint32 minutes played
@ -6048,6 +6070,12 @@ void GameHandler::preloadDBCCaches() const {
loadMapNameCache(); // Map.dbc
loadLfgDungeonDbc(); // LFGDungeons.dbc
// Validate animation constants against AnimationData.dbc
if (auto* am = services_.assetManager) {
auto animDbc = am->loadDBC("AnimationData.dbc");
rendering::anim::validateAgainstDBC(animDbc);
}
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t0).count();
LOG_INFO("DBC cache pre-load complete in ", elapsed, " ms");

View file

@ -679,6 +679,7 @@ void InventoryHandler::closeLoot() {
owner_.socket->send(packet);
}
lootWindowOpen_ = false;
if (owner_.lootWindowCallback_) owner_.lootWindowCallback_(false);
if (owner_.addonEventCallback_) owner_.addonEventCallback_("LOOT_CLOSED", {});
currentLoot_ = LootResponseData{};
}
@ -704,6 +705,7 @@ void InventoryHandler::handleLootResponse(network::Packet& packet) {
return;
}
lootWindowOpen_ = true;
if (owner_.lootWindowCallback_) owner_.lootWindowCallback_(true);
if (owner_.addonEventCallback_) {
owner_.addonEventCallback_("LOOT_OPENED", {});
owner_.addonEventCallback_("LOOT_READY", {});
@ -749,6 +751,7 @@ void InventoryHandler::handleLootReleaseResponse(network::Packet& packet) {
(void)packet;
localLootState_.erase(currentLoot_.lootGuid);
lootWindowOpen_ = false;
if (owner_.lootWindowCallback_) owner_.lootWindowCallback_(false);
if (owner_.addonEventCallback_) owner_.addonEventCallback_("LOOT_CLOSED", {});
currentLoot_ = LootResponseData{};
}

View file

@ -34,19 +34,19 @@ static float mergeCooldownSeconds(float current, float incoming) {
static CombatTextEntry::Type combatTextTypeFromSpellMissInfo(uint8_t missInfo) {
switch (missInfo) {
case 0: return CombatTextEntry::MISS;
case 1: return CombatTextEntry::DODGE;
case 2: return CombatTextEntry::PARRY;
case 3: return CombatTextEntry::BLOCK;
case 4: return CombatTextEntry::EVADE;
case 5: return CombatTextEntry::IMMUNE;
case 6: return CombatTextEntry::DEFLECT;
case 7: return CombatTextEntry::ABSORB;
case 8: return CombatTextEntry::RESIST;
case 9:
case 10:
case SpellMissInfo::MISS: return CombatTextEntry::MISS;
case SpellMissInfo::DODGE: return CombatTextEntry::DODGE;
case SpellMissInfo::PARRY: return CombatTextEntry::PARRY;
case SpellMissInfo::BLOCK: return CombatTextEntry::BLOCK;
case SpellMissInfo::EVADE: return CombatTextEntry::EVADE;
case SpellMissInfo::IMMUNE: return CombatTextEntry::IMMUNE;
case SpellMissInfo::DEFLECT: return CombatTextEntry::DEFLECT;
case SpellMissInfo::ABSORB: return CombatTextEntry::ABSORB;
case SpellMissInfo::RESIST: return CombatTextEntry::RESIST;
case SpellMissInfo::IMMUNE2:
case SpellMissInfo::IMMUNE3:
return CombatTextEntry::IMMUNE;
case 11: return CombatTextEntry::REFLECT;
case SpellMissInfo::REFLECT: return CombatTextEntry::REFLECT;
default: return CombatTextEntry::MISS;
}
}
@ -939,7 +939,7 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
}
}
if (isMeleeAbility) {
if (owner_.meleeSwingCallback_) owner_.meleeSwingCallback_();
if (owner_.meleeSwingCallback_) owner_.meleeSwingCallback_(sid);
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* csm = ac->getCombatSoundManager()) {
csm->playWeaponSwing(audio::CombatSoundManager::WeaponSize::MEDIUM, false);
@ -951,6 +951,14 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
const bool wasInTimedCast = casting_ && (data.spellId == currentCastSpellId_);
// Instant spell cast animation — if this wasn't a timed cast and isn't a
// melee ability, play a brief spell cast animation (one-shot)
if (!wasInTimedCast && !isMeleeAbility && !owner_.isProfessionSpell(data.spellId)) {
if (owner_.spellCastAnimCallback_) {
owner_.spellCastAnimCallback_(owner_.playerGuid, true, false);
}
}
LOG_WARNING("[GO-DIAG] SPELL_GO: spellId=", data.spellId,
" casting=", casting_, " currentCast=", currentCastSpellId_,
" wasInTimedCast=", wasInTimedCast,
@ -991,6 +999,13 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
castSpell(nextSpell, nextTarget);
}
} else {
// For non-player casters: if no tracked cast state exists, this was an
// instant cast — play a brief one-shot spell animation before stopping
auto castIt = unitCastStates_.find(data.casterUnit);
bool wasTrackedCast = (castIt != unitCastStates_.end());
if (!wasTrackedCast && owner_.spellCastAnimCallback_) {
owner_.spellCastAnimCallback_(data.casterUnit, true, false);
}
if (owner_.spellCastAnimCallback_) {
owner_.spellCastAnimCallback_(data.casterUnit, false, false);
}
@ -1181,6 +1196,26 @@ void SpellHandler::handleAuraUpdate(network::Packet& packet, bool isAll) {
}
}
}
// Sprint aura detection — check if any sprint/dash speed buff is active
if (data.guid == owner_.playerGuid && owner_.sprintAuraCallback_) {
static const uint32_t sprintSpells[] = {
2983, 8696, 11305, // Rogue Sprint (ranks 1-3)
1850, 9821, 33357, // Druid Dash (ranks 1-3)
36554, // Shadowstep (speed component)
68992, 68991, // Darkflight (worgen racial)
58984, // Aspect of the Pack speed
};
bool hasSprint = false;
for (const auto& a : playerAuras_) {
if (a.isEmpty()) continue;
for (uint32_t sid : sprintSpells) {
if (a.spellId == sid) { hasSprint = true; break; }
}
if (hasSprint) break;
}
owner_.sprintAuraCallback_(hasSprint);
}
}
}
@ -2222,7 +2257,7 @@ void SpellHandler::handleSpellLogMiss(network::Packet& packet) {
// TBC: spellId(4) + uint64 caster + uint8 unk + uint32 count
// + count × (uint64 victim + uint8 missInfo)
// All expansions append uint32 reflectSpellId + uint8 reflectResult when
// missInfo==11 (REFLECT).
// missInfo==REFLECT (11).
const bool spellMissUsesFullGuid = isActiveExpansion("tbc");
auto readSpellMissGuid = [&]() -> uint64_t {
if (spellMissUsesFullGuid)
@ -2248,7 +2283,7 @@ void SpellHandler::handleSpellLogMiss(network::Packet& packet) {
struct SpellMissLogEntry {
uint64_t victimGuid = 0;
uint8_t missInfo = 0;
uint32_t reflectSpellId = 0; // Only valid when missInfo==11 (REFLECT)
uint32_t reflectSpellId = 0; // Only valid when missInfo==REFLECT
};
std::vector<SpellMissLogEntry> parsedMisses;
parsedMisses.reserve(storedLimit);
@ -2266,9 +2301,9 @@ void SpellHandler::handleSpellLogMiss(network::Packet& packet) {
return;
}
const uint8_t missInfo = packet.readUInt8();
// REFLECT (11): extra uint32 reflectSpellId + uint8 reflectResult
// REFLECT: extra uint32 reflectSpellId + uint8 reflectResult
uint32_t reflectSpellId = 0;
if (missInfo == 11) {
if (missInfo == SpellMissInfo::REFLECT) {
if (packet.hasRemaining(5)) {
reflectSpellId = packet.readUInt32();
/*uint8_t reflectResult =*/ packet.readUInt8();
@ -2912,7 +2947,7 @@ void SpellHandler::handleSpellLogExecute(network::Packet& packet) {
uint8_t effectType = packet.readUInt8();
uint32_t effectLogCount = packet.readUInt32();
effectLogCount = std::min(effectLogCount, 64u); // sanity
if (effectType == 10) {
if (effectType == SpellEffect::POWER_DRAIN) {
// SPELL_EFFECT_POWER_DRAIN: packed_guid target + uint32 amount + uint32 powerType + float multiplier
for (uint32_t li = 0; li < effectLogCount; ++li) {
if (!packet.hasRemaining(exeUsesFullGuid ? 8u : 1u)
@ -2950,7 +2985,7 @@ void SpellHandler::handleSpellLogExecute(network::Packet& packet) {
" power=", drainPower, " amount=", drainAmount,
" multiplier=", drainMult);
}
} else if (effectType == 11) {
} else if (effectType == SpellEffect::HEALTH_LEECH) {
// SPELL_EFFECT_HEALTH_LEECH: packed_guid target + uint32 amount + float multiplier
for (uint32_t li = 0; li < effectLogCount; ++li) {
if (!packet.hasRemaining(exeUsesFullGuid ? 8u : 1u)
@ -2983,7 +3018,7 @@ void SpellHandler::handleSpellLogExecute(network::Packet& packet) {
LOG_DEBUG("SMSG_SPELLLOGEXECUTE HEALTH_LEECH: spell=", exeSpellId,
" amount=", leechAmount, " multiplier=", leechMult);
}
} else if (effectType == 24 || effectType == 114) {
} else if (effectType == SpellEffect::CREATE_ITEM || effectType == SpellEffect::CREATE_ITEM2) {
// SPELL_EFFECT_CREATE_ITEM / CREATE_ITEM2: uint32 itemEntry per log entry
for (uint32_t li = 0; li < effectLogCount; ++li) {
if (!packet.hasRemaining(4)) break;
@ -3012,7 +3047,7 @@ void SpellHandler::handleSpellLogExecute(network::Packet& packet) {
}
}
}
} else if (effectType == 26) {
} else if (effectType == SpellEffect::INTERRUPT_CAST) {
// SPELL_EFFECT_INTERRUPT_CAST: packed_guid target + uint32 interrupted_spell_id
for (uint32_t li = 0; li < effectLogCount; ++li) {
if (!packet.hasRemaining(exeUsesFullGuid ? 8u : 1u)
@ -3033,7 +3068,7 @@ void SpellHandler::handleSpellLogExecute(network::Packet& packet) {
LOG_DEBUG("SMSG_SPELLLOGEXECUTE INTERRUPT_CAST: spell=", exeSpellId,
" interrupted=", icSpellId, " target=0x", std::hex, icTarget, std::dec);
}
} else if (effectType == 49) {
} else if (effectType == SpellEffect::FEED_PET) {
// SPELL_EFFECT_FEED_PET: uint32 itemEntry per log entry
for (uint32_t li = 0; li < effectLogCount; ++li) {
if (!packet.hasRemaining(4)) break;
@ -3182,6 +3217,12 @@ void SpellHandler::handleChannelStart(network::Packet& packet) {
}
LOG_DEBUG("MSG_CHANNEL_START: caster=0x", std::hex, chanCaster, std::dec,
" spell=", chanSpellId, " total=", chanTotalMs, "ms");
// Play channeling animation (looping)
if (owner_.spellCastAnimCallback_) {
owner_.spellCastAnimCallback_(chanCaster, true, true);
}
// Fire UNIT_SPELLCAST_CHANNEL_START for Lua addons
if (owner_.addonEventCallback_) {
auto unitId = owner_.guidToUnitId(chanCaster);
@ -3217,6 +3258,10 @@ void SpellHandler::handleChannelUpdate(network::Packet& packet) {
" remaining=", chanRemainMs, "ms");
// Fire UNIT_SPELLCAST_CHANNEL_STOP when channel ends
if (chanRemainMs == 0) {
// Stop channeling animation — return to idle
if (owner_.spellCastAnimCallback_) {
owner_.spellCastAnimCallback_(chanCaster2, false, true);
}
auto unitId = owner_.guidToUnitId(chanCaster2);
if (!unitId.empty())
owner_.fireAddonEvent("UNIT_SPELLCAST_CHANNEL_STOP", {unitId});

View file

@ -36,6 +36,7 @@ static const UFNameEntry kUFNames[] = {
{"UNIT_FIELD_AURAS", UF::UNIT_FIELD_AURAS},
{"UNIT_FIELD_AURAFLAGS", UF::UNIT_FIELD_AURAFLAGS},
{"UNIT_NPC_FLAGS", UF::UNIT_NPC_FLAGS},
{"UNIT_NPC_EMOTESTATE", UF::UNIT_NPC_EMOTESTATE},
{"UNIT_DYNAMIC_FLAGS", UF::UNIT_DYNAMIC_FLAGS},
{"UNIT_FIELD_RESISTANCES", UF::UNIT_FIELD_RESISTANCES},
{"UNIT_FIELD_STAT0", UF::UNIT_FIELD_STAT0},
@ -61,6 +62,7 @@ static const UFNameEntry kUFNames[] = {
{"PLAYER_SKILL_INFO_START", UF::PLAYER_SKILL_INFO_START},
{"PLAYER_EXPLORED_ZONES_START", UF::PLAYER_EXPLORED_ZONES_START},
{"GAMEOBJECT_DISPLAYID", UF::GAMEOBJECT_DISPLAYID},
{"GAMEOBJECT_BYTES_1", UF::GAMEOBJECT_BYTES_1},
{"ITEM_FIELD_STACK_COUNT", UF::ITEM_FIELD_STACK_COUNT},
{"ITEM_FIELD_DURABILITY", UF::ITEM_FIELD_DURABILITY},
{"ITEM_FIELD_MAXDURABILITY", UF::ITEM_FIELD_MAXDURABILITY},

View file

@ -79,7 +79,9 @@ bool DBCFile::load(const std::vector<uint8_t>& dbcData) {
const uint8_t* recordStart = dbcData.data() + sizeof(DBCHeader);
uint32_t totalRecordSize = recordCount * recordSize;
recordData.resize(totalRecordSize);
std::memcpy(recordData.data(), recordStart, totalRecordSize);
if (totalRecordSize > 0) {
std::memcpy(recordData.data(), recordStart, totalRecordSize);
}
// Copy string block
const uint8_t* stringStart = recordStart + totalRecordSize;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,567 @@
// ============================================================================
// animation_ids.cpp — Inverse lookup & DBC validation
// Generated from animation_ids.hpp (452 constants, IDs 0451)
// ============================================================================
#include "rendering/animation_ids.hpp"
#include "pipeline/dbc_loader.hpp"
#include "core/logger.hpp"
#include <unordered_set>
namespace wowee {
namespace rendering {
namespace anim {
const char* nameFromId(uint32_t id) {
static const char* const names[ANIM_COUNT] = {
/* 0 */ "STAND",
/* 1 */ "DEATH",
/* 2 */ "SPELL",
/* 3 */ "STOP",
/* 4 */ "WALK",
/* 5 */ "RUN",
/* 6 */ "DEAD",
/* 7 */ "RISE",
/* 8 */ "STAND_WOUND",
/* 9 */ "COMBAT_WOUND",
/* 10 */ "COMBAT_CRITICAL",
/* 11 */ "SHUFFLE_LEFT",
/* 12 */ "SHUFFLE_RIGHT",
/* 13 */ "WALK_BACKWARDS",
/* 14 */ "STUN",
/* 15 */ "HANDS_CLOSED",
/* 16 */ "ATTACK_UNARMED",
/* 17 */ "ATTACK_1H",
/* 18 */ "ATTACK_2H",
/* 19 */ "ATTACK_2H_LOOSE",
/* 20 */ "PARRY_UNARMED",
/* 21 */ "PARRY_1H",
/* 22 */ "PARRY_2H",
/* 23 */ "PARRY_2H_LOOSE",
/* 24 */ "SHIELD_BLOCK",
/* 25 */ "READY_UNARMED",
/* 26 */ "READY_1H",
/* 27 */ "READY_2H",
/* 28 */ "READY_2H_LOOSE",
/* 29 */ "READY_BOW",
/* 30 */ "DODGE",
/* 31 */ "SPELL_PRECAST",
/* 32 */ "SPELL_CAST",
/* 33 */ "SPELL_CAST_AREA",
/* 34 */ "NPC_WELCOME",
/* 35 */ "NPC_GOODBYE",
/* 36 */ "BLOCK",
/* 37 */ "JUMP_START",
/* 38 */ "JUMP",
/* 39 */ "JUMP_END",
/* 40 */ "FALL",
/* 41 */ "SWIM_IDLE",
/* 42 */ "SWIM",
/* 43 */ "SWIM_LEFT",
/* 44 */ "SWIM_RIGHT",
/* 45 */ "SWIM_BACKWARDS",
/* 46 */ "ATTACK_BOW",
/* 47 */ "FIRE_BOW",
/* 48 */ "READY_RIFLE",
/* 49 */ "ATTACK_RIFLE",
/* 50 */ "LOOT",
/* 51 */ "READY_SPELL_DIRECTED",
/* 52 */ "READY_SPELL_OMNI",
/* 53 */ "SPELL_CAST_DIRECTED",
/* 54 */ "SPELL_CAST_OMNI",
/* 55 */ "BATTLE_ROAR",
/* 56 */ "READY_ABILITY",
/* 57 */ "SPECIAL_1H",
/* 58 */ "SPECIAL_2H",
/* 59 */ "SHIELD_BASH",
/* 60 */ "EMOTE_TALK",
/* 61 */ "EMOTE_EAT",
/* 62 */ "EMOTE_WORK",
/* 63 */ "EMOTE_USE_STANDING",
/* 64 */ "EMOTE_EXCLAMATION",
/* 65 */ "EMOTE_QUESTION",
/* 66 */ "EMOTE_BOW",
/* 67 */ "EMOTE_WAVE",
/* 68 */ "EMOTE_CHEER",
/* 69 */ "EMOTE_DANCE",
/* 70 */ "EMOTE_LAUGH",
/* 71 */ "EMOTE_SLEEP",
/* 72 */ "EMOTE_SIT_GROUND",
/* 73 */ "EMOTE_RUDE",
/* 74 */ "EMOTE_ROAR",
/* 75 */ "EMOTE_KNEEL",
/* 76 */ "EMOTE_KISS",
/* 77 */ "EMOTE_CRY",
/* 78 */ "EMOTE_CHICKEN",
/* 79 */ "EMOTE_BEG",
/* 80 */ "EMOTE_APPLAUD",
/* 81 */ "EMOTE_SHOUT",
/* 82 */ "EMOTE_FLEX",
/* 83 */ "EMOTE_SHY",
/* 84 */ "EMOTE_POINT",
/* 85 */ "ATTACK_1H_PIERCE",
/* 86 */ "ATTACK_2H_LOOSE_PIERCE",
/* 87 */ "ATTACK_OFF",
/* 88 */ "ATTACK_OFF_PIERCE",
/* 89 */ "SHEATHE",
/* 90 */ "HIP_SHEATHE",
/* 91 */ "MOUNT",
/* 92 */ "RUN_RIGHT",
/* 93 */ "RUN_LEFT",
/* 94 */ "MOUNT_SPECIAL",
/* 95 */ "KICK",
/* 96 */ "SIT_GROUND_DOWN",
/* 97 */ "SITTING",
/* 98 */ "SIT_GROUND_UP",
/* 99 */ "SLEEP_DOWN",
/* 100 */ "SLEEP",
/* 101 */ "SLEEP_UP",
/* 102 */ "SIT_CHAIR_LOW",
/* 103 */ "SIT_CHAIR_MED",
/* 104 */ "SIT_CHAIR_HIGH",
/* 105 */ "LOAD_BOW",
/* 106 */ "LOAD_RIFLE",
/* 107 */ "ATTACK_THROWN",
/* 108 */ "READY_THROWN",
/* 109 */ "HOLD_BOW",
/* 110 */ "HOLD_RIFLE",
/* 111 */ "HOLD_THROWN",
/* 112 */ "LOAD_THROWN",
/* 113 */ "EMOTE_SALUTE",
/* 114 */ "KNEEL_START",
/* 115 */ "KNEEL_LOOP",
/* 116 */ "KNEEL_END",
/* 117 */ "ATTACK_UNARMED_OFF",
/* 118 */ "SPECIAL_UNARMED",
/* 119 */ "STEALTH_WALK",
/* 120 */ "STEALTH_STAND",
/* 121 */ "KNOCKDOWN",
/* 122 */ "EATING_LOOP",
/* 123 */ "USE_STANDING_LOOP",
/* 124 */ "CHANNEL_CAST_DIRECTED",
/* 125 */ "CHANNEL_CAST_OMNI",
/* 126 */ "WHIRLWIND",
/* 127 */ "BIRTH",
/* 128 */ "USE_STANDING_START",
/* 129 */ "USE_STANDING_END",
/* 130 */ "CREATURE_SPECIAL",
/* 131 */ "DROWN",
/* 132 */ "DROWNED",
/* 133 */ "FISHING_CAST",
/* 134 */ "FISHING_LOOP",
/* 135 */ "FLY",
/* 136 */ "EMOTE_WORK_NO_SHEATHE",
/* 137 */ "EMOTE_STUN_NO_SHEATHE",
/* 138 */ "EMOTE_USE_STANDING_NO_SHEATHE",
/* 139 */ "SPELL_SLEEP_DOWN",
/* 140 */ "SPELL_KNEEL_START",
/* 141 */ "SPELL_KNEEL_LOOP",
/* 142 */ "SPELL_KNEEL_END",
/* 143 */ "SPRINT",
/* 144 */ "IN_FLIGHT",
/* 145 */ "SPAWN",
/* 146 */ "CLOSE",
/* 147 */ "CLOSED",
/* 148 */ "OPEN",
/* 149 */ "DESTROY",
/* 150 */ "DESTROYED",
/* 151 */ "UNSHEATHE",
/* 152 */ "SHEATHE_ALT",
/* 153 */ "ATTACK_UNARMED_NO_SHEATHE",
/* 154 */ "STEALTH_RUN",
/* 155 */ "READY_CROSSBOW",
/* 156 */ "ATTACK_CROSSBOW",
/* 157 */ "EMOTE_TALK_EXCLAMATION",
/* 158 */ "FLY_IDLE",
/* 159 */ "FLY_FORWARD",
/* 160 */ "FLY_BACKWARDS",
/* 161 */ "FLY_LEFT",
/* 162 */ "FLY_RIGHT",
/* 163 */ "FLY_UP",
/* 164 */ "FLY_DOWN",
/* 165 */ "FLY_LAND_START",
/* 166 */ "FLY_LAND_RUN",
/* 167 */ "FLY_LAND_END",
/* 168 */ "EMOTE_TALK_QUESTION",
/* 169 */ "EMOTE_READ",
/* 170 */ "EMOTE_SHIELDBLOCK",
/* 171 */ "EMOTE_CHOP",
/* 172 */ "EMOTE_HOLDRIFLE",
/* 173 */ "EMOTE_HOLDBOW",
/* 174 */ "EMOTE_HOLDTHROWN",
/* 175 */ "CUSTOM_SPELL_02",
/* 176 */ "CUSTOM_SPELL_03",
/* 177 */ "CUSTOM_SPELL_04",
/* 178 */ "CUSTOM_SPELL_05",
/* 179 */ "CUSTOM_SPELL_06",
/* 180 */ "CUSTOM_SPELL_07",
/* 181 */ "CUSTOM_SPELL_08",
/* 182 */ "CUSTOM_SPELL_09",
/* 183 */ "CUSTOM_SPELL_10",
/* 184 */ "EMOTE_STATE_DANCE",
/* 185 */ "FLY_STAND",
/* 186 */ "EMOTE_STATE_LAUGH",
/* 187 */ "EMOTE_STATE_POINT",
/* 188 */ "EMOTE_STATE_EAT",
/* 189 */ "EMOTE_STATE_WORK",
/* 190 */ "EMOTE_STATE_SIT_GROUND",
/* 191 */ "EMOTE_STATE_HOLD_BOW",
/* 192 */ "EMOTE_STATE_HOLD_RIFLE",
/* 193 */ "EMOTE_STATE_HOLD_THROWN",
/* 194 */ "FLY_COMBAT_WOUND",
/* 195 */ "FLY_COMBAT_CRITICAL",
/* 196 */ "RECLINED",
/* 197 */ "EMOTE_STATE_ROAR",
/* 198 */ "EMOTE_USE_STANDING_LOOP_2",
/* 199 */ "EMOTE_STATE_APPLAUD",
/* 200 */ "READY_FIST",
/* 201 */ "SPELL_CHANNEL_DIRECTED_OMNI",
/* 202 */ "SPECIAL_ATTACK_1H_OFF",
/* 203 */ "ATTACK_FIST_1H",
/* 204 */ "ATTACK_FIST_1H_OFF",
/* 205 */ "PARRY_FIST_1H",
/* 206 */ "READY_FIST_1H",
/* 207 */ "EMOTE_STATE_READ_AND_TALK",
/* 208 */ "EMOTE_STATE_WORK_NO_SHEATHE",
/* 209 */ "FLY_RUN",
/* 210 */ "EMOTE_STATE_KNEEL_2",
/* 211 */ "EMOTE_STATE_SPELL_KNEEL",
/* 212 */ "EMOTE_STATE_USE_STANDING",
/* 213 */ "EMOTE_STATE_STUN",
/* 214 */ "EMOTE_STATE_STUN_NO_SHEATHE",
/* 215 */ "EMOTE_TRAIN",
/* 216 */ "EMOTE_DEAD",
/* 217 */ "EMOTE_STATE_DANCE_ONCE",
/* 218 */ "FLY_DEATH",
/* 219 */ "FLY_STAND_WOUND",
/* 220 */ "FLY_SHUFFLE_LEFT",
/* 221 */ "FLY_SHUFFLE_RIGHT",
/* 222 */ "FLY_WALK_BACKWARDS",
/* 223 */ "FLY_STUN",
/* 224 */ "FLY_HANDS_CLOSED",
/* 225 */ "FLY_ATTACK_UNARMED",
/* 226 */ "FLY_ATTACK_1H",
/* 227 */ "FLY_ATTACK_2H",
/* 228 */ "FLY_ATTACK_2H_LOOSE",
/* 229 */ "FLY_SPELL",
/* 230 */ "FLY_STOP",
/* 231 */ "FLY_WALK",
/* 232 */ "FLY_DEAD",
/* 233 */ "FLY_RISE",
/* 234 */ "FLY_RUN_2",
/* 235 */ "FLY_FALL",
/* 236 */ "FLY_SWIM_IDLE",
/* 237 */ "FLY_SWIM",
/* 238 */ "FLY_SWIM_LEFT",
/* 239 */ "FLY_SWIM_RIGHT",
/* 240 */ "FLY_SWIM_BACKWARDS",
/* 241 */ "FLY_ATTACK_BOW",
/* 242 */ "FLY_FIRE_BOW",
/* 243 */ "FLY_READY_RIFLE",
/* 244 */ "FLY_ATTACK_RIFLE",
/* 245 */ "TOTEM_SMALL",
/* 246 */ "TOTEM_MEDIUM",
/* 247 */ "TOTEM_LARGE",
/* 248 */ "FLY_LOOT",
/* 249 */ "FLY_READY_SPELL_DIRECTED",
/* 250 */ "FLY_READY_SPELL_OMNI",
/* 251 */ "FLY_SPELL_CAST_DIRECTED",
/* 252 */ "FLY_SPELL_CAST_OMNI",
/* 253 */ "FLY_BATTLE_ROAR",
/* 254 */ "FLY_READY_ABILITY",
/* 255 */ "FLY_SPECIAL_1H",
/* 256 */ "FLY_SPECIAL_2H",
/* 257 */ "FLY_SHIELD_BASH",
/* 258 */ "FLY_EMOTE_TALK",
/* 259 */ "FLY_EMOTE_EAT",
/* 260 */ "FLY_EMOTE_WORK",
/* 261 */ "FLY_EMOTE_USE_STANDING",
/* 262 */ "FLY_EMOTE_BOW",
/* 263 */ "FLY_EMOTE_WAVE",
/* 264 */ "FLY_EMOTE_CHEER",
/* 265 */ "FLY_EMOTE_DANCE",
/* 266 */ "FLY_EMOTE_LAUGH",
/* 267 */ "FLY_EMOTE_SLEEP",
/* 268 */ "FLY_EMOTE_SIT_GROUND",
/* 269 */ "FLY_EMOTE_RUDE",
/* 270 */ "FLY_EMOTE_ROAR",
/* 271 */ "FLY_EMOTE_KNEEL",
/* 272 */ "FLY_EMOTE_KISS",
/* 273 */ "FLY_EMOTE_CRY",
/* 274 */ "FLY_EMOTE_CHICKEN",
/* 275 */ "FLY_EMOTE_BEG",
/* 276 */ "FLY_EMOTE_APPLAUD",
/* 277 */ "FLY_EMOTE_SHOUT",
/* 278 */ "FLY_EMOTE_FLEX",
/* 279 */ "FLY_EMOTE_SHY",
/* 280 */ "FLY_EMOTE_POINT",
/* 281 */ "FLY_ATTACK_1H_PIERCE",
/* 282 */ "FLY_ATTACK_2H_LOOSE_PIERCE",
/* 283 */ "FLY_ATTACK_OFF",
/* 284 */ "FLY_ATTACK_OFF_PIERCE",
/* 285 */ "FLY_SHEATHE",
/* 286 */ "FLY_HIP_SHEATHE",
/* 287 */ "FLY_MOUNT",
/* 288 */ "FLY_RUN_RIGHT",
/* 289 */ "FLY_RUN_LEFT",
/* 290 */ "FLY_MOUNT_SPECIAL",
/* 291 */ "FLY_KICK",
/* 292 */ "FLY_SIT_GROUND_DOWN",
/* 293 */ "FLY_SITTING",
/* 294 */ "FLY_SIT_GROUND_UP",
/* 295 */ "FLY_SLEEP_DOWN",
/* 296 */ "FLY_SLEEP",
/* 297 */ "FLY_SLEEP_UP",
/* 298 */ "FLY_SIT_CHAIR_LOW",
/* 299 */ "FLY_SIT_CHAIR_MED",
/* 300 */ "FLY_SIT_CHAIR_HIGH",
/* 301 */ "FLY_LOAD_BOW",
/* 302 */ "FLY_LOAD_RIFLE",
/* 303 */ "FLY_ATTACK_THROWN",
/* 304 */ "FLY_READY_THROWN",
/* 305 */ "FLY_HOLD_BOW",
/* 306 */ "FLY_HOLD_RIFLE",
/* 307 */ "FLY_HOLD_THROWN",
/* 308 */ "FLY_LOAD_THROWN",
/* 309 */ "FLY_EMOTE_SALUTE",
/* 310 */ "FLY_KNEEL_START",
/* 311 */ "FLY_KNEEL_LOOP",
/* 312 */ "FLY_KNEEL_END",
/* 313 */ "FLY_ATTACK_UNARMED_OFF",
/* 314 */ "FLY_SPECIAL_UNARMED",
/* 315 */ "FLY_STEALTH_WALK",
/* 316 */ "FLY_STEALTH_STAND",
/* 317 */ "FLY_KNOCKDOWN",
/* 318 */ "FLY_EATING_LOOP",
/* 319 */ "FLY_USE_STANDING_LOOP",
/* 320 */ "FLY_CHANNEL_CAST_DIRECTED",
/* 321 */ "FLY_CHANNEL_CAST_OMNI",
/* 322 */ "FLY_WHIRLWIND",
/* 323 */ "FLY_BIRTH",
/* 324 */ "FLY_USE_STANDING_START",
/* 325 */ "FLY_USE_STANDING_END",
/* 326 */ "FLY_CREATURE_SPECIAL",
/* 327 */ "FLY_DROWN",
/* 328 */ "FLY_DROWNED",
/* 329 */ "FLY_FISHING_CAST",
/* 330 */ "FLY_FISHING_LOOP",
/* 331 */ "FLY_FLY",
/* 332 */ "FLY_EMOTE_WORK_NO_SHEATHE",
/* 333 */ "FLY_EMOTE_STUN_NO_SHEATHE",
/* 334 */ "FLY_EMOTE_USE_STANDING_NO_SHEATHE",
/* 335 */ "FLY_SPELL_SLEEP_DOWN",
/* 336 */ "FLY_SPELL_KNEEL_START",
/* 337 */ "FLY_SPELL_KNEEL_LOOP",
/* 338 */ "FLY_SPELL_KNEEL_END",
/* 339 */ "FLY_SPRINT",
/* 340 */ "FLY_IN_FLIGHT",
/* 341 */ "FLY_SPAWN",
/* 342 */ "FLY_CLOSE",
/* 343 */ "FLY_CLOSED",
/* 344 */ "FLY_OPEN",
/* 345 */ "FLY_DESTROY",
/* 346 */ "FLY_DESTROYED",
/* 347 */ "FLY_UNSHEATHE",
/* 348 */ "FLY_SHEATHE_ALT",
/* 349 */ "FLY_ATTACK_UNARMED_NO_SHEATHE",
/* 350 */ "FLY_STEALTH_RUN",
/* 351 */ "FLY_READY_CROSSBOW",
/* 352 */ "FLY_ATTACK_CROSSBOW",
/* 353 */ "FLY_EMOTE_TALK_EXCLAMATION",
/* 354 */ "FLY_EMOTE_TALK_QUESTION",
/* 355 */ "FLY_EMOTE_READ",
/* 356 */ "EMOTE_HOLD_CROSSBOW",
/* 357 */ "FLY_EMOTE_HOLD_BOW",
/* 358 */ "FLY_EMOTE_HOLD_RIFLE",
/* 359 */ "FLY_EMOTE_HOLD_THROWN",
/* 360 */ "FLY_EMOTE_HOLD_CROSSBOW",
/* 361 */ "FLY_CUSTOM_SPELL_02",
/* 362 */ "FLY_CUSTOM_SPELL_03",
/* 363 */ "FLY_CUSTOM_SPELL_04",
/* 364 */ "FLY_CUSTOM_SPELL_05",
/* 365 */ "FLY_CUSTOM_SPELL_06",
/* 366 */ "FLY_CUSTOM_SPELL_07",
/* 367 */ "FLY_CUSTOM_SPELL_08",
/* 368 */ "FLY_CUSTOM_SPELL_09",
/* 369 */ "FLY_CUSTOM_SPELL_10",
/* 370 */ "FLY_EMOTE_STATE_DANCE",
/* 371 */ "EMOTE_EAT_NO_SHEATHE",
/* 372 */ "MOUNT_RUN_RIGHT",
/* 373 */ "MOUNT_RUN_LEFT",
/* 374 */ "MOUNT_WALK_BACKWARDS",
/* 375 */ "MOUNT_SWIM_IDLE",
/* 376 */ "MOUNT_SWIM",
/* 377 */ "MOUNT_SWIM_LEFT",
/* 378 */ "MOUNT_SWIM_RIGHT",
/* 379 */ "MOUNT_SWIM_BACKWARDS",
/* 380 */ "MOUNT_FLIGHT_IDLE",
/* 381 */ "MOUNT_FLIGHT_FORWARD",
/* 382 */ "MOUNT_FLIGHT_BACKWARDS",
/* 383 */ "MOUNT_FLIGHT_LEFT",
/* 384 */ "MOUNT_FLIGHT_RIGHT",
/* 385 */ "MOUNT_FLIGHT_UP",
/* 386 */ "MOUNT_FLIGHT_DOWN",
/* 387 */ "MOUNT_FLIGHT_LAND_START",
/* 388 */ "MOUNT_FLIGHT_LAND_RUN",
/* 389 */ "MOUNT_FLIGHT_LAND_END",
/* 390 */ "FLY_EMOTE_STATE_LAUGH",
/* 391 */ "FLY_EMOTE_STATE_POINT",
/* 392 */ "FLY_EMOTE_STATE_EAT",
/* 393 */ "FLY_EMOTE_STATE_WORK",
/* 394 */ "FLY_EMOTE_STATE_SIT_GROUND",
/* 395 */ "FLY_EMOTE_STATE_HOLD_BOW",
/* 396 */ "FLY_EMOTE_STATE_HOLD_RIFLE",
/* 397 */ "FLY_EMOTE_STATE_HOLD_THROWN",
/* 398 */ "FLY_EMOTE_STATE_ROAR",
/* 399 */ "FLY_RECLINED",
/* 400 */ "EMOTE_TRAIN_2",
/* 401 */ "EMOTE_DEAD_2",
/* 402 */ "FLY_EMOTE_USE_STANDING_LOOP_2",
/* 403 */ "FLY_EMOTE_STATE_APPLAUD",
/* 404 */ "FLY_READY_FIST",
/* 405 */ "FLY_SPELL_CHANNEL_DIRECTED_OMNI",
/* 406 */ "FLY_SPECIAL_ATTACK_1H_OFF",
/* 407 */ "FLY_ATTACK_FIST_1H",
/* 408 */ "FLY_ATTACK_FIST_1H_OFF",
/* 409 */ "FLY_PARRY_FIST_1H",
/* 410 */ "FLY_READY_FIST_1H",
/* 411 */ "FLY_EMOTE_STATE_READ_AND_TALK",
/* 412 */ "FLY_EMOTE_STATE_WORK_NO_SHEATHE",
/* 413 */ "FLY_EMOTE_STATE_KNEEL_2",
/* 414 */ "FLY_EMOTE_STATE_SPELL_KNEEL",
/* 415 */ "FLY_EMOTE_STATE_USE_STANDING",
/* 416 */ "FLY_EMOTE_STATE_STUN",
/* 417 */ "FLY_EMOTE_STATE_STUN_NO_SHEATHE",
/* 418 */ "FLY_EMOTE_TRAIN",
/* 419 */ "FLY_EMOTE_DEAD",
/* 420 */ "FLY_EMOTE_STATE_DANCE_ONCE",
/* 421 */ "FLY_EMOTE_EAT_NO_SHEATHE",
/* 422 */ "FLY_MOUNT_RUN_RIGHT",
/* 423 */ "FLY_MOUNT_RUN_LEFT",
/* 424 */ "FLY_MOUNT_WALK_BACKWARDS",
/* 425 */ "FLY_MOUNT_SWIM_IDLE",
/* 426 */ "FLY_MOUNT_SWIM",
/* 427 */ "FLY_MOUNT_SWIM_LEFT",
/* 428 */ "FLY_MOUNT_SWIM_RIGHT",
/* 429 */ "FLY_MOUNT_SWIM_BACKWARDS",
/* 430 */ "FLY_MOUNT_FLIGHT_IDLE",
/* 431 */ "FLY_MOUNT_FLIGHT_FORWARD",
/* 432 */ "FLY_MOUNT_FLIGHT_BACKWARDS",
/* 433 */ "FLY_MOUNT_FLIGHT_LEFT",
/* 434 */ "FLY_MOUNT_FLIGHT_RIGHT",
/* 435 */ "FLY_MOUNT_FLIGHT_UP",
/* 436 */ "FLY_MOUNT_FLIGHT_DOWN",
/* 437 */ "FLY_MOUNT_FLIGHT_LAND_START",
/* 438 */ "FLY_MOUNT_FLIGHT_LAND_RUN",
/* 439 */ "FLY_MOUNT_FLIGHT_LAND_END",
/* 440 */ "FLY_TOTEM_SMALL",
/* 441 */ "FLY_TOTEM_MEDIUM",
/* 442 */ "FLY_TOTEM_LARGE",
/* 443 */ "FLY_EMOTE_HOLD_CROSSBOW_2",
/* 444 */ "VEHICLE_GRAB",
/* 445 */ "VEHICLE_THROW",
/* 446 */ "FLY_VEHICLE_GRAB",
/* 447 */ "FLY_VEHICLE_THROW",
/* 448 */ "GUILD_CHAMPION_1",
/* 449 */ "GUILD_CHAMPION_2",
/* 450 */ "FLY_GUILD_CHAMPION_1",
/* 451 */ "FLY_GUILD_CHAMPION_2",
};
if (id < ANIM_COUNT) return names[id];
return "UNKNOWN";
}
uint32_t flyVariant(uint32_t groundId) {
// Compact lookup: ground animation ID (0451) → FLY_* variant, or 0 if none.
// Built from the 155 ground→fly pairs in animation_ids.hpp.
static const uint16_t table[] = {
// 0-9
185, 218, 229, 230, 231, 209, 232, 233, 219, 194,
// 10-19
195, 220, 221, 222, 223, 224, 225, 226, 227, 228,
// 20-29 (PARRY/READY/DODGE — no fly variants)
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// 30-39 (BLOCK/SPELL_PRECAST/NPC — no fly variants)
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// 40-49
235, 236, 237, 238, 239, 240, 241, 242, 243, 244,
// 50-59
248, 249, 250, 251, 252, 253, 254, 255, 256, 257,
// 60-69
258, 259, 260, 261, 0, 0, 262, 263, 264, 265,
// 70-79
266, 267, 268, 269, 270, 271, 272, 273, 274, 275,
// 80-89
276, 277, 278, 279, 280, 281, 282, 283, 284, 285,
// 90-99
286, 287, 288, 289, 290, 291, 292, 293, 294, 295,
// 100-109
296, 297, 298, 299, 300, 301, 302, 303, 304, 305,
// 110-119
306, 307, 308, 309, 310, 311, 312, 313, 314, 315,
// 120-129
316, 317, 318, 319, 320, 321, 322, 323, 324, 325,
// 130-139
326, 327, 328, 329, 330, 331, 332, 333, 334, 335,
// 140-149
336, 337, 338, 339, 340, 341, 342, 343, 344, 345,
// 150-159
346, 347, 348, 349, 350, 351, 352, 353, 0, 0,
// 160-169 (FLY_BACKWARDS..FLY_LAND_END are already FLY_ themselves: 0)
0, 0, 0, 0, 0, 0, 0, 0, 354, 355,
// 170-179
0, 0, 0, 0, 0, 361, 362, 363, 364, 365,
// 180-189
366, 367, 368, 369, 370, 0, 390, 391, 392, 393,
// 190-199
394, 395, 396, 397, 0, 0, 399, 398, 402, 403,
// 200-209
404, 405, 406, 407, 408, 409, 410, 411, 412, 0,
// 210-217
413, 414, 415, 416, 417, 418, 419, 420,
};
constexpr uint32_t tableSize = sizeof(table) / sizeof(table[0]);
if (groundId >= tableSize) return 0;
return table[groundId];
}
void validateAgainstDBC(const std::shared_ptr<wowee::pipeline::DBCFile>& dbc) {
if (!dbc || !dbc->isLoaded()) {
LOG_WARNING("AnimationData.dbc not available — skipping animation ID validation");
return;
}
// Collect all IDs present in the DBC (first field is the animation ID)
std::unordered_set<uint32_t> dbcIds;
for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) {
uint32_t id = dbc->getUInt32(i, 0);
dbcIds.insert(id);
}
// Check: constants we define that are missing from DBC
uint32_t missingInDbc = 0;
for (uint32_t id = 0; id < ANIM_COUNT; ++id) {
if (dbcIds.find(id) == dbcIds.end()) {
LOG_WARNING("Animation ID ", id, " (", nameFromId(id),
") defined in constants but missing from AnimationData.dbc");
++missingInDbc;
}
}
// Check: DBC IDs beyond our constant range
uint32_t extraInDbc = 0;
for (uint32_t dbcId : dbcIds) {
if (dbcId >= ANIM_COUNT) {
++extraInDbc;
}
}
LOG_INFO("AnimationData.dbc validation: ", dbc->getRecordCount(), " DBC records, ",
ANIM_COUNT, " constants, ",
missingInDbc, " missing from DBC, ",
extraInDbc, " DBC-only IDs beyond constant range");
}
} // namespace anim
} // namespace rendering
} // namespace wowee

View file

@ -84,7 +84,7 @@ bool Celestial::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout)
.setVertexInput({binding}, {posAttr, uvAttr})
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
.setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) // test on, write off (sky layer)
.setNoDepthTest() // Sky layer: celestials always render (skybox doesn't write depth)
.setColorBlendAttachment(PipelineBuilder::blendAdditive())
.setMultisample(vkCtx_->getMsaaSamples())
.setLayout(pipelineLayout_)
@ -411,6 +411,12 @@ float Celestial::calculateCelestialAngle(float timeOfDay, float riseTime, float
void Celestial::update(float deltaTime) {
sunHazeTimer_ += deltaTime;
// Keep timer in a range where GPU sin() precision is reliable (< ~10000).
// The noise period repeats at multiples of 1.0 on each axis, so fmod by a
// large integer preserves visual continuity.
if (sunHazeTimer_ > 10000.0f) {
sunHazeTimer_ = std::fmod(sunHazeTimer_, 10000.0f);
}
if (!moonPhaseCycling_) {
return;

View file

@ -1,5 +1,6 @@
#include "rendering/character_preview.hpp"
#include "rendering/character_renderer.hpp"
#include "rendering/animation_ids.hpp"
#include "rendering/vk_render_target.hpp"
#include "rendering/vk_texture.hpp"
#include "rendering/vk_context.hpp"
@ -584,7 +585,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
charRenderer_->setActiveGeosets(instanceId_, activeGeosets);
// Play idle animation (Stand = animation ID 0)
charRenderer_->playAnimation(instanceId_, 0, true);
charRenderer_->playAnimation(instanceId_, rendering::anim::STAND, true);
// Cache core appearance for later equipment geosets.
race_ = race;

View file

@ -15,6 +15,7 @@
* the original WoW Model Viewer (charcontrol.h, REGION_FAC=2).
*/
#include "rendering/character_renderer.hpp"
#include "rendering/animation_ids.hpp"
#include "rendering/vk_context.hpp"
#include "rendering/vk_texture.hpp"
#include "rendering/vk_pipeline.hpp"
@ -34,6 +35,7 @@
#include <cmath>
#include <filesystem>
#include <future>
#include <numeric>
#include <thread>
#include <functional>
#include <unordered_map>
@ -261,7 +263,8 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram
.setVertexInput({charBinding}, charAttrs)
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
.setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS_OR_EQUAL)
.setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS)
.setDepthBias(0.0f, 0.0f)
.setColorBlendAttachment(blendState)
.setMultisample(samples);
if (alphaToCoverage)
@ -269,7 +272,7 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram
return builder
.setLayout(pipelineLayout_)
.setRenderPass(mainPass)
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR, VK_DYNAMIC_STATE_DEPTH_BIAS})
.build(device, vkCtx_->getPipelineCache());
};
@ -1733,9 +1736,9 @@ void CharacterRenderer::update(float deltaTime, const glm::vec3& cameraPos) {
inst.animationTime -= static_cast<float>(seq.duration);
}
} else {
// One-shot animation finished: return to Stand (0) unless dead
if (inst.currentAnimationId != 1 /*Death*/) {
playAnimation(pair.first, 0, true);
// One-shot animation finished: return to Stand unless dead
if (inst.currentAnimationId != anim::DEATH) {
playAnimation(pair.first, anim::STAND, true);
} else {
// Stay on last frame of death
inst.animationTime = static_cast<float>(seq.duration);
@ -2380,8 +2383,24 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
return gpuModel.data.materials[b.materialIndex].blendMode;
return 0;
};
// Sort batches by (priorityPlane, materialLayer) so equipment layers
// render in the order the M2 format intends. priorityPlane separates
// overlay effects; materialLayer orders coplanar body parts.
std::vector<size_t> sortedBatchIndices(gpuModel.data.batches.size());
std::iota(sortedBatchIndices.begin(), sortedBatchIndices.end(), 0);
std::stable_sort(sortedBatchIndices.begin(), sortedBatchIndices.end(),
[&](size_t a, size_t b) {
const auto& ba = gpuModel.data.batches[a];
const auto& bb = gpuModel.data.batches[b];
if (ba.priorityPlane != bb.priorityPlane)
return ba.priorityPlane < bb.priorityPlane;
return ba.materialLayer < bb.materialLayer;
});
for (int pass = 0; pass < 2; pass++) {
for (const auto& batch : gpuModel.data.batches) {
for (size_t bi : sortedBatchIndices) {
const auto& batch = gpuModel.data.batches[bi];
uint16_t bm = getBatchBlendMode(batch);
if (pass == 0 && bm != 0) continue; // pass 0: opaque only
if (pass == 1 && bm == 0) continue; // pass 1: non-opaque only
@ -2599,6 +2618,10 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
pipelineLayout_, 1, 1, &materialSet, 0, nullptr);
// Per-batch depth bias from materialLayer to separate coplanar
// armor pieces (chest/legs/gloves) that share identical depth.
vkCmdSetDepthBias(cmd, static_cast<float>(batch.materialLayer) * 0.5f, 0.0f, 0.0f);
vkCmdDrawIndexed(cmd, batch.indexCount, 1, batch.indexStart, 0, 0);
}
} // end pass loop
@ -3030,8 +3053,8 @@ void CharacterRenderer::moveInstanceTo(uint32_t instanceId, const glm::vec3& des
// Stop at current location.
inst.position = destination;
inst.isMoving = false;
if (inst.currentAnimationId == 4 || inst.currentAnimationId == 5) {
playAnimation(instanceId, 0, true);
if (inst.currentAnimationId == anim::WALK || inst.currentAnimationId == anim::RUN) {
playAnimation(instanceId, anim::STAND, true);
}
return;
}
@ -3509,7 +3532,8 @@ void CharacterRenderer::recreatePipelines() {
.setVertexInput({charBinding}, charAttrs)
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
.setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS_OR_EQUAL)
.setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS)
.setDepthBias(0.0f, 0.0f)
.setColorBlendAttachment(blendState)
.setMultisample(samples);
if (alphaToCoverage)
@ -3517,7 +3541,7 @@ void CharacterRenderer::recreatePipelines() {
return builder
.setLayout(pipelineLayout_)
.setRenderPass(mainPass)
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR, VK_DYNAMIC_STATE_DEPTH_BIAS})
.build(device, vkCtx_->getPipelineCache());
};

View file

@ -4212,6 +4212,37 @@ void M2Renderer::setInstanceAnimationFrozen(uint32_t instanceId, bool frozen) {
}
}
void M2Renderer::setInstanceAnimation(uint32_t instanceId, uint32_t animationId, bool loop) {
auto idxIt = instanceIndexById.find(instanceId);
if (idxIt == instanceIndexById.end()) return;
auto& inst = instances[idxIt->second];
if (!inst.cachedModel) return;
const auto& seqs = inst.cachedModel->sequences;
// Find the first sequence matching the requested animation ID
for (int i = 0; i < static_cast<int>(seqs.size()); ++i) {
if (seqs[i].id == animationId) {
inst.currentSequenceIndex = i;
inst.animDuration = static_cast<float>(seqs[i].duration);
inst.animTime = 0.0f;
inst.animSpeed = 1.0f;
// Use playingVariation=true for one-shot (returns to idle when done)
inst.playingVariation = !loop;
return;
}
}
}
bool M2Renderer::hasAnimation(uint32_t instanceId, uint32_t animationId) const {
auto idxIt = instanceIndexById.find(instanceId);
if (idxIt == instanceIndexById.end()) return false;
const auto& inst = instances[idxIt->second];
if (!inst.cachedModel) return false;
for (const auto& seq : inst.cachedModel->sequences) {
if (seq.id == animationId) return true;
}
return false;
}
float M2Renderer::getInstanceAnimDuration(uint32_t instanceId) const {
auto idxIt = instanceIndexById.find(instanceId);
if (idxIt == instanceIndexById.end()) return 0.0f;

View file

@ -250,13 +250,13 @@ bool Renderer::createPerFrameResources() {
// --- Create descriptor pool for UBO + image sampler (normal frames + reflection) ---
VkDescriptorPoolSize poolSizes[2]{};
poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSizes[0].descriptorCount = MAX_FRAMES + 1; // +1 for reflection perFrame UBO
poolSizes[0].descriptorCount = MAX_FRAMES * 2; // normal frames + reflection frames
poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
poolSizes[1].descriptorCount = MAX_FRAMES + 1;
poolSizes[1].descriptorCount = MAX_FRAMES * 2;
VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.maxSets = MAX_FRAMES + 1; // +1 for reflection descriptor set
poolInfo.maxSets = MAX_FRAMES * 2; // normal frames + reflection frames
poolInfo.poolSizeCount = 2;
poolInfo.pPoolSizes = poolSizes;
@ -344,42 +344,48 @@ bool Renderer::createPerFrameResources() {
}
reflPerFrameUBOMapped = mapInfo.pMappedData;
VkDescriptorSetLayout layouts[MAX_FRAMES];
for (uint32_t i = 0; i < MAX_FRAMES; i++) layouts[i] = perFrameSetLayout;
VkDescriptorSetAllocateInfo setAlloc{};
setAlloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
setAlloc.descriptorPool = sceneDescriptorPool;
setAlloc.descriptorSetCount = 1;
setAlloc.pSetLayouts = &perFrameSetLayout;
setAlloc.descriptorSetCount = MAX_FRAMES;
setAlloc.pSetLayouts = layouts;
if (vkAllocateDescriptorSets(device, &setAlloc, &reflPerFrameDescSet) != VK_SUCCESS) {
LOG_ERROR("Failed to allocate reflection per-frame descriptor set");
if (vkAllocateDescriptorSets(device, &setAlloc, reflPerFrameDescSet) != VK_SUCCESS) {
LOG_ERROR("Failed to allocate reflection per-frame descriptor sets");
return false;
}
VkDescriptorBufferInfo descBuf{};
descBuf.buffer = reflPerFrameUBO;
descBuf.offset = 0;
descBuf.range = sizeof(GPUPerFrameData);
// Bind each reflection descriptor to the same UBO but its own frame's shadow view
for (uint32_t i = 0; i < MAX_FRAMES; i++) {
VkDescriptorBufferInfo descBuf{};
descBuf.buffer = reflPerFrameUBO;
descBuf.offset = 0;
descBuf.range = sizeof(GPUPerFrameData);
VkDescriptorImageInfo shadowImgInfo{};
shadowImgInfo.sampler = shadowSampler;
shadowImgInfo.imageView = shadowDepthView[0]; // reflection uses frame 0 shadow view
shadowImgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkDescriptorImageInfo shadowImgInfo{};
shadowImgInfo.sampler = shadowSampler;
shadowImgInfo.imageView = shadowDepthView[i];
shadowImgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkWriteDescriptorSet writes[2]{};
writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[0].dstSet = reflPerFrameDescSet;
writes[0].dstBinding = 0;
writes[0].descriptorCount = 1;
writes[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
writes[0].pBufferInfo = &descBuf;
writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[1].dstSet = reflPerFrameDescSet;
writes[1].dstBinding = 1;
writes[1].descriptorCount = 1;
writes[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
writes[1].pImageInfo = &shadowImgInfo;
VkWriteDescriptorSet writes[2]{};
writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[0].dstSet = reflPerFrameDescSet[i];
writes[0].dstBinding = 0;
writes[0].descriptorCount = 1;
writes[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
writes[0].pBufferInfo = &descBuf;
writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[1].dstSet = reflPerFrameDescSet[i];
writes[1].dstBinding = 1;
writes[1].descriptorCount = 1;
writes[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
writes[1].pImageInfo = &shadowImgInfo;
vkUpdateDescriptorSets(device, 2, writes, 0, nullptr);
vkUpdateDescriptorSets(device, 2, writes, 0, nullptr);
}
}
LOG_INFO("Per-frame Vulkan resources created (shadow map ", SHADOW_MAP_SIZE, "x", SHADOW_MAP_SIZE, ")");
@ -460,7 +466,7 @@ void Renderer::updatePerFrameUBO() {
currentFrameData.lightSpaceMatrix = lightSpaceMatrix;
// Scale shadow bias proportionally to ortho extent to avoid acne at close range / gaps at far range
float shadowBias = 0.8f * (shadowDistance_ / 300.0f);
float shadowBias = glm::clamp(0.8f * (shadowDistance_ / 300.0f), 0.0f, 1.0f);
currentFrameData.shadowParams = glm::vec4(shadowsEnabled ? 1.0f : 0.0f, shadowBias, 0.0f, 0.0f);
// Player water ripple data: pack player XY into shadowParams.zw, ripple strength into fogParams.w
@ -566,7 +572,7 @@ bool Renderer::initialize(core::Window* win) {
postProcessPipeline_ = std::make_unique<PostProcessPipeline>();
postProcessPipeline_->initialize(vkCtx);
// Phase 2.5: Create render graph and register virtual resources
// Create render graph and register virtual resources
renderGraph_ = std::make_unique<RenderGraph>();
renderGraph_->registerResource("shadow_depth");
renderGraph_->registerResource("reflection_texture");
@ -687,7 +693,7 @@ void Renderer::shutdown() {
postProcessPipeline_.reset();
}
// Phase 2.5: Destroy render graph
// Destroy render graph
renderGraph_.reset();
destroyPerFrameResources();
@ -1018,8 +1024,26 @@ void Renderer::setInCombat(bool combat) {
if (animationController_) animationController_->setInCombat(combat);
}
void Renderer::setEquippedWeaponType(uint32_t inventoryType) {
if (animationController_) animationController_->setEquippedWeaponType(inventoryType);
void Renderer::setEquippedWeaponType(uint32_t inventoryType, bool is2HLoose, bool isFist,
bool isDagger, bool hasOffHand, bool hasShield) {
if (animationController_) animationController_->setEquippedWeaponType(inventoryType, is2HLoose, isFist, isDagger, hasOffHand, hasShield);
}
void Renderer::triggerSpecialAttack(uint32_t spellId) {
if (animationController_) animationController_->triggerSpecialAttack(spellId);
}
void Renderer::setEquippedRangedType(RangedWeaponType type) {
if (animationController_) animationController_->setEquippedRangedType(type);
}
void Renderer::triggerRangedShot() {
if (animationController_) animationController_->triggerRangedShot();
}
RangedWeaponType Renderer::getEquippedRangedType() const {
return animationController_ ? animationController_->getEquippedRangedType()
: RangedWeaponType::NONE;
}
void Renderer::setCharging(bool c) {
@ -2797,8 +2821,8 @@ glm::mat4 Renderer::computeLightSpaceMatrix() {
sunDir = -sunDir;
}
// Keep a minimum downward component so the frustum doesn't collapse at grazing angles.
if (sunDir.z > -0.08f) {
sunDir.z = -0.08f;
if (sunDir.z > -0.15f) {
sunDir.z = -0.15f;
sunDir = glm::normalize(sunDir);
}
@ -2986,6 +3010,11 @@ void Renderer::renderReflectionPass() {
if (!waterRenderer || !camera || !waterRenderer->hasReflectionPass() || !waterRenderer->hasSurfaces()) return;
if (currentCmd == VK_NULL_HANDLE || !reflPerFrameUBOMapped) return;
// Select the current frame's pre-bound reflection descriptor set
// (each frame's set was bound to its own shadow depth view at init).
uint32_t frame = vkCtx->getCurrentFrame();
VkDescriptorSet reflDescSet = reflPerFrameDescSet[frame];
// Reflection pass uses 1x MSAA. Scene pipelines must be render-pass-compatible,
// which requires matching sample counts. Only render scene into reflection when MSAA is off.
bool canRenderScene = (vkCtx->getMsaaSamples() == VK_SAMPLE_COUNT_1_BIT);
@ -3040,13 +3069,13 @@ void Renderer::renderReflectionPass() {
skyParams.horizonGlow = lp.horizonGlow;
}
// weatherIntensity left at default 0 for reflection pass (no game handler in scope)
skySystem->render(currentCmd, reflPerFrameDescSet, *camera, skyParams);
skySystem->render(currentCmd, reflDescSet, *camera, skyParams);
}
if (terrainRenderer && terrainEnabled) {
terrainRenderer->render(currentCmd, reflPerFrameDescSet, *camera);
terrainRenderer->render(currentCmd, reflDescSet, *camera);
}
if (wmoRenderer) {
wmoRenderer->render(currentCmd, reflPerFrameDescSet, *camera);
wmoRenderer->render(currentCmd, reflDescSet, *camera);
}
}
@ -3139,7 +3168,7 @@ void Renderer::renderShadowPass() {
shadowDepthLayout_[frame] = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
}
// Phase 2.5: Build the per-frame render graph for off-screen pre-passes.
// Build the per-frame render graph for off-screen pre-passes.
// Declares passes as graph nodes with input/output dependencies.
// compile() performs topological sort; execute() runs them with auto barriers.
void Renderer::buildFrameGraph(game::GameHandler* gameHandler) {

View file

@ -193,7 +193,7 @@ bool TerrainRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameL
envSizeMBOrDefault("WOWEE_TERRAIN_TEX_CACHE_MB", 4096) * 1024ull * 1024ull;
LOG_INFO("Terrain texture cache budget: ", textureCacheBudgetBytes_ / (1024 * 1024), " MB");
// Phase 2.2: Allocate mega vertex/index buffers and indirect draw buffer.
// Allocate mega vertex/index buffers and indirect draw buffer.
// All terrain chunks share these buffers, eliminating per-chunk VB/IB rebinds.
{
VmaAllocator allocator = vkCtx->getAllocator();
@ -375,7 +375,7 @@ void TerrainRenderer::shutdown() {
if (shadowParamsLayout_) { vkDestroyDescriptorSetLayout(device, shadowParamsLayout_, nullptr); shadowParamsLayout_ = VK_NULL_HANDLE; }
if (shadowParamsUBO_) { vmaDestroyBuffer(allocator, shadowParamsUBO_, shadowParamsAlloc_); shadowParamsUBO_ = VK_NULL_HANDLE; shadowParamsAlloc_ = VK_NULL_HANDLE; }
// Phase 2.2: Destroy mega buffers and indirect draw buffer
// Destroy mega buffers and indirect draw buffer
if (megaVB_) { vmaDestroyBuffer(allocator, megaVB_, megaVBAlloc_); megaVB_ = VK_NULL_HANDLE; megaVBAlloc_ = VK_NULL_HANDLE; megaVBMapped_ = nullptr; }
if (megaIB_) { vmaDestroyBuffer(allocator, megaIB_, megaIBAlloc_); megaIB_ = VK_NULL_HANDLE; megaIBAlloc_ = VK_NULL_HANDLE; megaIBMapped_ = nullptr; }
if (indirectBuffer_) { vmaDestroyBuffer(allocator, indirectBuffer_, indirectAlloc_); indirectBuffer_ = VK_NULL_HANDLE; indirectAlloc_ = VK_NULL_HANDLE; indirectMapped_ = nullptr; }
@ -622,7 +622,7 @@ TerrainChunkGPU TerrainRenderer::uploadChunk(const pipeline::ChunkMesh& chunk) {
gpuChunk.indexBuffer = ib.buffer;
gpuChunk.indexAlloc = ib.allocation;
// Phase 2.2: Also copy into mega buffers for indirect drawing
// Also copy into mega buffers for indirect drawing
uint32_t vertCount = static_cast<uint32_t>(chunk.vertices.size());
uint32_t idxCount = static_cast<uint32_t>(chunk.indices.size());
if (megaVBMapped_ && megaIBMapped_ &&
@ -880,7 +880,7 @@ void TerrainRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, c
renderedChunks = 0;
culledChunks = 0;
// Phase 2.2: Use mega VB + IB when available.
// Use mega VB + IB when available.
// Bind mega buffers once, then use direct draws with base vertex/index offsets.
const bool useMegaBuffers = (megaVB_ && megaIB_);
if (useMegaBuffers) {
@ -1092,7 +1092,7 @@ void TerrainRenderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSp
vkCmdPushConstants(cmd, shadowPipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT,
0, 128, &push);
// Phase 2.2: Bind mega buffers once for shadow pass (same as opaque)
// Bind mega buffers once for shadow pass (same as opaque)
const bool useMegaShadow = (megaVB_ && megaIB_);
if (useMegaShadow) {
VkDeviceSize megaOffset = 0;

View file

@ -1,5 +1,6 @@
#include "ui/auth_screen.hpp"
#include "ui/ui_colors.hpp"
#include "ui/settings_panel.hpp"
#include "auth/crypto.hpp"
#include "core/application.hpp"
#include "core/logger.hpp"
@ -13,8 +14,9 @@
#include <imgui_impl_vulkan.h>
#include "stb_image.h"
#include <filesystem>
#include <sstream>
#include <fstream>
#include <map>
#include <sstream>
#include <cstdlib>
#include <cstring>
#include <cstdio>
@ -492,6 +494,11 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
if (ImGui::Button("Clear", ImVec2(160, 40))) {
statusMessage.clear();
}
ImGui::SameLine();
if (ImGui::Button("Settings", ImVec2(160, 40))) {
showLoginSettings_ = true;
}
}
ImGui::Spacing();
@ -503,6 +510,8 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
ImGui::TextWrapped("Default port is 3724.");
ImGui::End();
renderLoginSettingsWindow();
}
void AuthScreen::stopLoginMusic() {
@ -945,4 +954,216 @@ void AuthScreen::destroyBackgroundImage() {
if (bgMemory) { vkFreeMemory(device, bgMemory, nullptr); bgMemory = VK_NULL_HANDLE; }
}
// ---------------------------------------------------------------------------
// Login-screen graphics settings popup
// ---------------------------------------------------------------------------
void AuthScreen::applyPresetToState(LoginGraphicsState& s, int preset) {
switch (preset) {
case 1: // Low
s.shadows = false; s.shadowDistance = 75.0f; s.antiAliasing = 0;
s.fxaa = false; s.normalMapping = false; s.pom = false; s.pomQuality = 1;
s.upscalingMode = 0; s.waterRefraction = false; s.groundClutter = 25;
s.brightness = 50; s.vsync = false; s.fullscreen = false;
break;
case 2: // Medium
s.shadows = true; s.shadowDistance = 150.0f; s.antiAliasing = 0;
s.fxaa = false; s.normalMapping = true; s.pom = true; s.pomQuality = 1;
s.upscalingMode = 0; s.waterRefraction = true; s.groundClutter = 100;
s.brightness = 50; s.vsync = false; s.fullscreen = false;
break;
case 3: // High
s.shadows = true; s.shadowDistance = 250.0f; s.antiAliasing = 1;
s.fxaa = true; s.normalMapping = true; s.pom = true; s.pomQuality = 1;
s.upscalingMode = 0; s.waterRefraction = true; s.groundClutter = 130;
s.brightness = 50; s.vsync = false; s.fullscreen = false;
break;
case 4: // Ultra
s.shadows = true; s.shadowDistance = 400.0f; s.antiAliasing = 2;
s.fxaa = true; s.normalMapping = true; s.pom = true; s.pomQuality = 2;
s.upscalingMode = 0; s.waterRefraction = true; s.groundClutter = 150;
s.brightness = 50; s.vsync = false; s.fullscreen = false;
break;
default: // Custom — no change
break;
}
}
void AuthScreen::loadLoginGraphicsState() {
std::ifstream file(SettingsPanel::getSettingsPath());
if (!file.is_open()) {
// File doesn't exist yet — keep struct defaults (Medium equivalent)
return;
}
std::string line;
while (std::getline(file, line)) {
auto eq = line.find('=');
if (eq == std::string::npos) continue;
std::string key = line.substr(0, eq);
std::string val = line.substr(eq + 1);
if (key == "graphics_preset") loginGfx_.preset = std::stoi(val);
else if (key == "shadows") loginGfx_.shadows = (val == "1");
else if (key == "shadow_distance") loginGfx_.shadowDistance = std::stof(val);
else if (key == "antialiasing") loginGfx_.antiAliasing = std::stoi(val);
else if (key == "fxaa") loginGfx_.fxaa = (val == "1");
else if (key == "normal_mapping") loginGfx_.normalMapping = (val == "1");
else if (key == "pom") loginGfx_.pom = (val == "1");
else if (key == "pom_quality") loginGfx_.pomQuality = std::stoi(val);
else if (key == "upscaling_mode") loginGfx_.upscalingMode = std::stoi(val);
else if (key == "water_refraction") loginGfx_.waterRefraction = (val == "1");
else if (key == "ground_clutter_density") loginGfx_.groundClutter = std::stoi(val);
else if (key == "brightness") loginGfx_.brightness = std::stoi(val);
else if (key == "vsync") loginGfx_.vsync = (val == "1");
else if (key == "fullscreen") loginGfx_.fullscreen = (val == "1");
}
}
void AuthScreen::saveLoginGraphicsState() {
// Read the full settings file into a map to preserve non-graphics keys.
std::map<std::string, std::string> cfg;
std::ifstream in(SettingsPanel::getSettingsPath());
if (in.is_open()) {
std::string line;
while (std::getline(in, line)) {
auto eq = line.find('=');
if (eq != std::string::npos)
cfg[line.substr(0, eq)] = line.substr(eq + 1);
}
in.close();
}
// Overwrite graphics keys.
cfg["graphics_preset"] = std::to_string(loginGfx_.preset);
cfg["shadows"] = loginGfx_.shadows ? "1" : "0";
cfg["shadow_distance"] = std::to_string(static_cast<int>(loginGfx_.shadowDistance));
cfg["antialiasing"] = std::to_string(loginGfx_.antiAliasing);
cfg["fxaa"] = loginGfx_.fxaa ? "1" : "0";
cfg["normal_mapping"] = loginGfx_.normalMapping ? "1" : "0";
cfg["pom"] = loginGfx_.pom ? "1" : "0";
cfg["pom_quality"] = std::to_string(loginGfx_.pomQuality);
cfg["upscaling_mode"] = std::to_string(loginGfx_.upscalingMode);
cfg["water_refraction"] = loginGfx_.waterRefraction ? "1" : "0";
cfg["ground_clutter_density"]= std::to_string(loginGfx_.groundClutter);
cfg["brightness"] = std::to_string(loginGfx_.brightness);
cfg["vsync"] = loginGfx_.vsync ? "1" : "0";
cfg["fullscreen"] = loginGfx_.fullscreen ? "1" : "0";
// Write everything back.
std::ofstream out(SettingsPanel::getSettingsPath());
if (!out.is_open()) return;
for (const auto& [k, v] : cfg)
out << k << "=" << v << "\n";
}
void AuthScreen::renderLoginSettingsWindow() {
if (showLoginSettings_) {
ImGui::OpenPopup("Graphics Settings");
showLoginSettings_ = false;
loginGfxLoaded_ = false; // Reload from disk each time the popup opens.
}
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(500, 560), ImGuiCond_Always);
if (ImGui::BeginPopupModal("Graphics Settings", nullptr, ImGuiWindowFlags_NoResize)) {
if (!loginGfxLoaded_) {
loadLoginGraphicsState();
loginGfxLoaded_ = true;
}
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "Graphics Settings");
ImGui::TextWrapped("Adjust settings below or reset to a safe preset. Changes take effect on next login.");
ImGui::Separator();
ImGui::Spacing();
// Preset selector
const char* presetNames[] = {"Custom", "Low", "Medium", "High", "Ultra"};
ImGui::Text("Preset:");
ImGui::SameLine();
ImGui::SetNextItemWidth(160.0f);
if (ImGui::Combo("##preset", &loginGfx_.preset, presetNames, 5)) {
if (loginGfx_.preset != 0) // 0 = Custom — don't override manually set values
applyPresetToState(loginGfx_, loginGfx_.preset);
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Shadow settings
ImGui::Checkbox("Shadows", &loginGfx_.shadows);
if (loginGfx_.shadows) {
ImGui::SameLine();
ImGui::SetNextItemWidth(200.0f);
float sd = loginGfx_.shadowDistance;
if (ImGui::SliderFloat("Shadow Distance", &sd, 50.0f, 600.0f, "%.0f"))
loginGfx_.shadowDistance = sd;
}
// Anti-aliasing
const char* aaNames[] = {"Off", "2x MSAA", "4x MSAA"};
ImGui::Text("Anti-Aliasing:");
ImGui::SameLine();
ImGui::SetNextItemWidth(130.0f);
ImGui::Combo("##aa", &loginGfx_.antiAliasing, aaNames, 3);
ImGui::Checkbox("FXAA", &loginGfx_.fxaa);
ImGui::Checkbox("Normal Mapping", &loginGfx_.normalMapping);
// POM
ImGui::Checkbox("Parallax Occlusion Mapping (POM)", &loginGfx_.pom);
if (loginGfx_.pom) {
const char* pomQ[] = {"Medium", "High"};
ImGui::Text(" POM Quality:");
ImGui::SameLine();
ImGui::SetNextItemWidth(110.0f);
ImGui::Combo("##pomq", &loginGfx_.pomQuality, pomQ, 2);
}
ImGui::Checkbox("Water Refraction", &loginGfx_.waterRefraction);
// Ground clutter density
ImGui::Text("Ground Clutter:");
ImGui::SameLine();
ImGui::SetNextItemWidth(200.0f);
ImGui::SliderInt("##clutter", &loginGfx_.groundClutter, 0, 200);
// Brightness
ImGui::Text("Brightness:");
ImGui::SameLine();
ImGui::SetNextItemWidth(200.0f);
ImGui::SliderInt("##brightness", &loginGfx_.brightness, 0, 100);
ImGui::Checkbox("V-Sync", &loginGfx_.vsync);
ImGui::Checkbox("Fullscreen", &loginGfx_.fullscreen);
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Action buttons
if (ImGui::Button("Reset to Medium", ImVec2(160, 32))) {
applyPresetToState(loginGfx_, 2);
loginGfx_.preset = 2;
}
ImGui::SameLine();
float rightEdge = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + rightEdge - 220.0f);
if (ImGui::Button("Cancel", ImVec2(100, 32))) {
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Apply", ImVec2(100, 32))) {
saveLoginGraphicsState();
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
}} // namespace wowee::ui

View file

@ -40,7 +40,7 @@ namespace ui {
// ============================================================
// Cast Bar (Phase 3)
// Cast Bar
// ============================================================
void CombatUI::renderCastBar(game::GameHandler& gameHandler, SpellIconFn getSpellIcon) {
@ -341,7 +341,7 @@ void CombatUI::renderRaidWarningOverlay(game::GameHandler& gameHandler) {
// ============================================================
// Floating Combat Text (Phase 2)
// Floating Combat Text
// ============================================================
void CombatUI::renderCombatText(game::GameHandler& gameHandler) {
@ -838,7 +838,7 @@ void CombatUI::renderDPSMeter(game::GameHandler& gameHandler,
// ============================================================
// Buff/Debuff Bar (Phase 3)
// Buff/Debuff Bar
// ============================================================
void CombatUI::renderBuffBar(game::GameHandler& gameHandler,

View file

@ -63,7 +63,7 @@ void DialogManager::renderLateDialogs(game::GameHandler& gameHandler) {
}
// ============================================================
// Group Invite Popup (Phase 4)
// Group Invite Popup
// ============================================================
void DialogManager::renderGroupInvitePopup(game::GameHandler& gameHandler) {

View file

@ -8,6 +8,7 @@
#include "core/coordinates.hpp"
#include "core/input.hpp"
#include "rendering/renderer.hpp"
#include "rendering/animation_controller.hpp"
#include "rendering/wmo_renderer.hpp"
#include "rendering/terrain_manager.hpp"
#include "rendering/minimap.hpp"
@ -104,10 +105,10 @@ GameScreen::GameScreen() {
loadSettings();
}
// Section 3.5: Set UI services and propagate to child components
// Set UI services and propagate to child components
void GameScreen::setServices(const UIServices& services) {
services_ = services;
// Update legacy pointer for Phase A compatibility
// Update legacy pointer for compatibility
appearanceComposer_ = services.appearanceComposer;
// Propagate to child panels
chatPanel_.setServices(services);
@ -503,7 +504,37 @@ void GameScreen::render(game::GameHandler& gameHandler) {
auto* r = services_.renderer;
if (r) {
const auto& mh = gameHandler.getInventory().getEquipSlot(game::EquipSlot::MAIN_HAND);
r->setEquippedWeaponType(mh.empty() ? 0 : mh.item.inventoryType);
const auto& oh = gameHandler.getInventory().getEquipSlot(game::EquipSlot::OFF_HAND);
if (mh.empty()) {
r->setEquippedWeaponType(0, false);
} else {
// Polearms and staves use ATTACK_2H_LOOSE instead of ATTACK_2H
bool is2HLoose = (mh.item.subclassName == "Polearm" || mh.item.subclassName == "Staff");
bool isFist = (mh.item.subclassName == "Fist Weapon");
bool isDagger = (mh.item.subclassName == "Dagger");
bool hasOffHand = !oh.empty() &&
(oh.item.inventoryType == game::InvType::ONE_HAND ||
oh.item.subclassName == "Fist Weapon");
bool hasShield = !oh.empty() && oh.item.inventoryType == game::InvType::SHIELD;
r->setEquippedWeaponType(mh.item.inventoryType, is2HLoose, isFist, isDagger, hasOffHand, hasShield);
}
// Detect ranged weapon type from RANGED slot
const auto& rangedSlot = gameHandler.getInventory().getEquipSlot(game::EquipSlot::RANGED);
if (rangedSlot.empty()) {
r->setEquippedRangedType(rendering::RangedWeaponType::NONE);
} else if (rangedSlot.item.inventoryType == game::InvType::RANGED_BOW) {
// subclassName distinguishes Bow vs Crossbow
if (rangedSlot.item.subclassName == "Crossbow")
r->setEquippedRangedType(rendering::RangedWeaponType::CROSSBOW);
else
r->setEquippedRangedType(rendering::RangedWeaponType::BOW);
} else if (rangedSlot.item.inventoryType == game::InvType::RANGED_GUN) {
r->setEquippedRangedType(rendering::RangedWeaponType::GUN);
} else if (rangedSlot.item.inventoryType == game::InvType::THROWN) {
r->setEquippedRangedType(rendering::RangedWeaponType::THROWN);
} else {
r->setEquippedRangedType(rendering::RangedWeaponType::NONE);
}
}
}
@ -4103,7 +4134,7 @@ void GameScreen::renderWorldMap(game::GameHandler& gameHandler) {
}
// ============================================================
// Action Bar (Phase 3)
// Action Bar
// ============================================================
VkDescriptorSet GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManager* am) {
@ -4217,36 +4248,6 @@ VkDescriptorSet GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManage
return ds;
}
// ============================================================
// Stance / Form / Presence Bar
// Shown for Warriors (stances), Death Knights (presences),
// Druids (shapeshift forms), Rogues (stealth), Priests (Shadowform).
// Buttons display the player's known stance/form spells.
// Active form is detected by checking permanent player auras.
// ============================================================
// ============================================================
// Bag Bar
// ============================================================
// ============================================================
// XP Bar
// ============================================================
// ============================================================
// Reputation Bar
// ============================================================
// ============================================================
// Cast Bar (Phase 3)
// ============================================================
// ============================================================
// Mirror Timers (breath / fatigue / feign death)
// ============================================================
@ -4527,18 +4528,6 @@ void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) {
ImGui::PopStyleColor();
}
// ============================================================
// Raid Warning / Boss Emote Center-Screen Overlay
// ============================================================
// ============================================================
// Floating Combat Text (Phase 2)
// ============================================================
// ============================================================
// DPS / HPS Meter
// ============================================================
// ============================================================
// Nameplates — world-space health bars projected to screen
// ============================================================
@ -5147,10 +5136,6 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) {
}
}
// ============================================================
// Party Frames (Phase 4)
// ============================================================
// ============================================================
// Durability Warning (equipment damage indicator)
// ============================================================
@ -5313,95 +5298,6 @@ void GameScreen::renderUIErrors(game::GameHandler& /*gameHandler*/, float deltaT
ImGui::PopStyleVar();
}
// ============================================================
// Boss Encounter Frames
// ============================================================
// ============================================================
// Social Frame — compact online friends panel (toggled by socialPanel_.showSocialFrame_)
// ============================================================
// ============================================================
// Buff/Debuff Bar (Phase 3)
// ============================================================
// ============================================================
// Loot Window (Phase 5)
// ============================================================
// ============================================================
// Gossip Window (Phase 5)
// ============================================================
// ============================================================
// Quest Details Window
// ============================================================
// ============================================================
// Quest Request Items Window (turn-in progress check)
// ============================================================
// ============================================================
// Quest Offer Reward Window (choose reward)
// ============================================================
// ============================================================
// ItemExtendedCost.dbc loader
// ============================================================
// ============================================================
// Vendor Window (Phase 5)
// ============================================================
// ============================================================
// Trainer
// ============================================================
// ============================================================
// Teleporter Panel
// ============================================================
// ============================================================
// Escape Menu
// ============================================================
// ============================================================
// Barber Shop Window
// ============================================================
// ============================================================
// Pet Stable Window
// ============================================================
// ============================================================
// Taxi Window
// ============================================================
// ============================================================
// Logout Countdown
// ============================================================
// ============================================================
// Death Screen
// ============================================================
void GameScreen::renderQuestMarkers(game::GameHandler& gameHandler) {
const auto& statuses = gameHandler.getNpcQuestStatuses();
if (statuses.empty()) return;

View file

@ -135,6 +135,19 @@ endif()
add_test(NAME frustum COMMAND test_frustum)
register_test_target(test_frustum)
# ── test_animation_ids ───────────────────────────────────────
add_executable(test_animation_ids
test_animation_ids.cpp
${TEST_COMMON_SOURCES}
${CMAKE_SOURCE_DIR}/src/rendering/animation_ids.cpp
${CMAKE_SOURCE_DIR}/src/pipeline/dbc_loader.cpp
)
target_include_directories(test_animation_ids PRIVATE ${TEST_INCLUDE_DIRS})
target_include_directories(test_animation_ids SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
target_link_libraries(test_animation_ids PRIVATE catch2_main)
add_test(NAME animation_ids COMMAND test_animation_ids)
register_test_target(test_animation_ids)
# ── ASAN / UBSan for test targets ────────────────────────────
if(WOWEE_ENABLE_ASAN AND NOT MSVC)
foreach(_t IN LISTS ALL_TEST_TARGETS)

View file

@ -0,0 +1,184 @@
// Animation ID validation tests — covers nameFromId() and validateAgainstDBC()
#include <catch_amalgamated.hpp>
#include "rendering/animation_ids.hpp"
#include "pipeline/dbc_loader.hpp"
#include <cstring>
#include <vector>
using wowee::pipeline::DBCFile;
namespace anim = wowee::rendering::anim;
// Build a synthetic AnimationData.dbc in memory.
// AnimationData.dbc layout: each record has at least 1 field (the animation ID).
// We use numFields=2 (id + dummy) to mirror the real DBC which has multiple fields.
static std::vector<uint8_t> buildAnimationDBC(const std::vector<uint32_t>& animIds) {
const uint32_t numRecords = static_cast<uint32_t>(animIds.size());
const uint32_t numFields = 2; // id + a dummy field
const uint32_t recordSize = numFields * 4;
const uint32_t stringBlockSize = 1; // single null byte
std::vector<uint8_t> data;
data.reserve(20 + numRecords * recordSize + stringBlockSize);
// Magic "WDBC"
data.push_back('W'); data.push_back('D'); data.push_back('B'); data.push_back('C');
auto writeU32 = [&](uint32_t v) {
data.push_back(static_cast<uint8_t>(v & 0xFF));
data.push_back(static_cast<uint8_t>((v >> 8) & 0xFF));
data.push_back(static_cast<uint8_t>((v >> 16) & 0xFF));
data.push_back(static_cast<uint8_t>((v >> 24) & 0xFF));
};
writeU32(numRecords);
writeU32(numFields);
writeU32(recordSize);
writeU32(stringBlockSize);
// Records: [animId, 0]
for (uint32_t id : animIds) {
writeU32(id);
writeU32(0);
}
// String block: single null
data.push_back('\0');
return data;
}
// ── nameFromId tests ────────────────────────────────────────────────────────
TEST_CASE("nameFromId returns correct names for known IDs", "[animation]") {
REQUIRE(std::string(anim::nameFromId(anim::STAND)) == "STAND");
REQUIRE(std::string(anim::nameFromId(anim::DEATH)) == "DEATH");
REQUIRE(std::string(anim::nameFromId(anim::WALK)) == "WALK");
REQUIRE(std::string(anim::nameFromId(anim::RUN)) == "RUN");
REQUIRE(std::string(anim::nameFromId(anim::ATTACK_UNARMED)) == "ATTACK_UNARMED");
REQUIRE(std::string(anim::nameFromId(anim::ATTACK_1H)) == "ATTACK_1H");
REQUIRE(std::string(anim::nameFromId(anim::ATTACK_2H)) == "ATTACK_2H");
REQUIRE(std::string(anim::nameFromId(anim::ATTACK_2H_LOOSE)) == "ATTACK_2H_LOOSE");
REQUIRE(std::string(anim::nameFromId(anim::SPELL_CAST_DIRECTED)) == "SPELL_CAST_DIRECTED");
REQUIRE(std::string(anim::nameFromId(anim::SPELL_CAST_OMNI)) == "SPELL_CAST_OMNI");
REQUIRE(std::string(anim::nameFromId(anim::READY_1H)) == "READY_1H");
REQUIRE(std::string(anim::nameFromId(anim::READY_2H)) == "READY_2H");
REQUIRE(std::string(anim::nameFromId(anim::READY_2H_LOOSE)) == "READY_2H_LOOSE");
REQUIRE(std::string(anim::nameFromId(anim::READY_UNARMED)) == "READY_UNARMED");
REQUIRE(std::string(anim::nameFromId(anim::EMOTE_DANCE)) == "EMOTE_DANCE");
}
TEST_CASE("nameFromId returns UNKNOWN for out-of-range IDs", "[animation]") {
REQUIRE(std::string(anim::nameFromId(anim::ANIM_COUNT)) == "UNKNOWN");
REQUIRE(std::string(anim::nameFromId(anim::ANIM_COUNT + 1)) == "UNKNOWN");
REQUIRE(std::string(anim::nameFromId(9999)) == "UNKNOWN");
REQUIRE(std::string(anim::nameFromId(UINT32_MAX)) == "UNKNOWN");
}
TEST_CASE("nameFromId covers first and last IDs", "[animation]") {
REQUIRE(std::string(anim::nameFromId(0)) == "STAND");
REQUIRE(std::string(anim::nameFromId(anim::ANIM_COUNT - 1)) == "FLY_GUILD_CHAMPION_2");
}
// ── validateAgainstDBC tests ────────────────────────────────────────────────
TEST_CASE("validateAgainstDBC handles null DBC", "[animation][dbc]") {
// Should not crash — just logs a warning
anim::validateAgainstDBC(nullptr);
}
TEST_CASE("validateAgainstDBC handles unloaded DBC", "[animation][dbc]") {
auto dbc = std::make_shared<DBCFile>();
REQUIRE_FALSE(dbc->isLoaded());
// Should not crash — just logs a warning
anim::validateAgainstDBC(dbc);
}
TEST_CASE("validateAgainstDBC with perfect match", "[animation][dbc]") {
// Build a DBC containing IDs 0..ANIM_COUNT-1 (exact match)
std::vector<uint32_t> allIds;
for (uint32_t i = 0; i < anim::ANIM_COUNT; ++i) {
allIds.push_back(i);
}
auto data = buildAnimationDBC(allIds);
auto dbc = std::make_shared<DBCFile>();
REQUIRE(dbc->load(data));
REQUIRE(dbc->getRecordCount() == anim::ANIM_COUNT);
// Should complete without crashing — all IDs match
anim::validateAgainstDBC(dbc);
}
TEST_CASE("validateAgainstDBC with missing IDs in DBC", "[animation][dbc]") {
// DBC only contains a subset of IDs — misses many
std::vector<uint32_t> partialIds = {0, 1, 2, 4, 5};
auto data = buildAnimationDBC(partialIds);
auto dbc = std::make_shared<DBCFile>();
REQUIRE(dbc->load(data));
REQUIRE(dbc->getRecordCount() == 5);
// Should log warnings for missing IDs but not crash
anim::validateAgainstDBC(dbc);
}
TEST_CASE("validateAgainstDBC with extra IDs beyond range", "[animation][dbc]") {
// DBC has some IDs beyond ANIM_COUNT
std::vector<uint32_t> extraIds = {0, 1, 500, 600, 1000};
auto data = buildAnimationDBC(extraIds);
auto dbc = std::make_shared<DBCFile>();
REQUIRE(dbc->load(data));
REQUIRE(dbc->getRecordCount() == 5);
// Should log info about extra DBC-only IDs but not crash
anim::validateAgainstDBC(dbc);
}
TEST_CASE("validateAgainstDBC with empty DBC", "[animation][dbc]") {
// DBC with zero records
auto data = buildAnimationDBC({});
auto dbc = std::make_shared<DBCFile>();
REQUIRE(dbc->load(data));
REQUIRE(dbc->getRecordCount() == 0);
// Should log warnings for all ANIM_COUNT missing IDs but not crash
anim::validateAgainstDBC(dbc);
}
TEST_CASE("validateAgainstDBC with single ID", "[animation][dbc]") {
// DBC with just STAND (id=0)
auto data = buildAnimationDBC({0});
auto dbc = std::make_shared<DBCFile>();
REQUIRE(dbc->load(data));
REQUIRE(dbc->getRecordCount() == 1);
anim::validateAgainstDBC(dbc);
}
TEST_CASE("ANIM_COUNT matches expected value", "[animation]") {
REQUIRE(anim::ANIM_COUNT == 452);
}
TEST_CASE("Animation constant IDs are unique and sequential from documentation", "[animation]") {
// Verify key animation ID values match WoW's AnimationData.dbc layout
REQUIRE(anim::STAND == 0);
REQUIRE(anim::DEATH == 1);
REQUIRE(anim::SPELL == 2);
REQUIRE(anim::ATTACK_UNARMED == 16);
REQUIRE(anim::ATTACK_1H == 17);
REQUIRE(anim::ATTACK_2H == 18);
REQUIRE(anim::ATTACK_2H_LOOSE == 19);
REQUIRE(anim::READY_UNARMED == 25);
REQUIRE(anim::READY_1H == 26);
REQUIRE(anim::READY_2H == 27);
REQUIRE(anim::READY_2H_LOOSE == 28);
REQUIRE(anim::SPELL_CAST == 32);
REQUIRE(anim::SPELL_CAST_DIRECTED == 53);
REQUIRE(anim::SPELL_CAST_OMNI == 54);
REQUIRE(anim::CHANNEL_CAST_DIRECTED == 124);
REQUIRE(anim::CHANNEL_CAST_OMNI == 125);
REQUIRE(anim::EMOTE_DANCE == 69);
}

View file

@ -1,4 +1,4 @@
// Phase 0 BLP loader tests: isValid, format names, invalid data handling
// BLP loader tests: isValid, format names, invalid data handling
#include <catch_amalgamated.hpp>
#include "pipeline/blp_loader.hpp"

View file

@ -1,4 +1,4 @@
// Phase 0 DBC binary parsing tests with synthetic data
// DBC binary parsing tests with synthetic data
#include <catch_amalgamated.hpp>
#include "pipeline/dbc_loader.hpp"
#include <cstring>

View file

@ -1,4 +1,4 @@
// Phase 0 Entity, Unit, Player, GameObject, EntityManager tests
// Entity, Unit, Player, GameObject, EntityManager tests
#include <catch_amalgamated.hpp>
#include "game/entity.hpp"
#include <memory>

View file

@ -1,4 +1,4 @@
// Phase 0 Frustum plane extraction and intersection tests
// Frustum plane extraction and intersection tests
#include <catch_amalgamated.hpp>
#include "rendering/frustum.hpp"

View file

@ -1,4 +1,4 @@
// Phase 0 M2 struct layout and field tests (header-only, no loader source)
// M2 struct layout and field tests (header-only, no loader source)
#include <catch_amalgamated.hpp>
#include "pipeline/m2_loader.hpp"
#include <cstring>

View file

@ -1,4 +1,4 @@
// Phase 0 OpcodeTable load from JSON, toWire/fromWire mapping
// OpcodeTable load from JSON, toWire/fromWire mapping
#include <catch_amalgamated.hpp>
#include "game/opcode_table.hpp"
#include <fstream>

View file

@ -1,4 +1,4 @@
// Phase 0 Packet read/write round-trip, packed GUID, bounds checks
// Packet read/write round-trip, packed GUID, bounds checks
#include <catch_amalgamated.hpp>
#include "network/packet.hpp"

View file

@ -1,4 +1,4 @@
// Phase 0 SRP6a challenge/proof smoke tests
// SRP6a challenge/proof smoke tests
#include <catch_amalgamated.hpp>
#include "auth/srp.hpp"
#include "auth/crypto.hpp"

View file

@ -873,24 +873,217 @@ class AssetPipelineGUI:
# ── M2 Preview (wireframe + textures + animations) ──
# Common animation ID names
# Common animation ID names — complete list from animation_ids.hpp (452 entries)
_ANIM_NAMES: dict[int, str] = {
0: "Stand", 1: "Death", 2: "Spell", 3: "Stop", 4: "Walk", 5: "Run",
6: "Dead", 7: "Rise", 8: "StandWound", 9: "CombatWound", 10: "CombatCritical",
11: "ShuffleLeft", 12: "ShuffleRight", 13: "Walkbackwards", 14: "Stun",
15: "HandsClosed", 16: "AttackUnarmed", 17: "Attack1H", 18: "Attack2H",
19: "Attack2HL", 20: "ParryUnarmed", 21: "Parry1H", 22: "Parry2H",
23: "Parry2HL", 24: "ShieldBlock", 25: "ReadyUnarmed", 26: "Ready1H",
27: "Ready2H", 28: "Ready2HL", 29: "ReadyBow", 30: "Dodge",
31: "SpellPrecast", 32: "SpellCast", 33: "SpellCastArea",
34: "NPCWelcome", 35: "NPCGoodbye", 36: "Block", 37: "JumpStart",
38: "Jump", 39: "JumpEnd", 40: "Fall", 41: "SwimIdle", 42: "Swim",
43: "SwimLeft", 44: "SwimRight", 45: "SwimBackwards",
60: "SpellChannelDirected", 61: "SpellChannelOmni",
69: "CombatAbility", 70: "CombatAbility2H",
94: "Kneel", 113: "Loot",
135: "ReadyRifle", 138: "Fly", 143: "CustomSpell01",
157: "EmoteTalk", 185: "FlyIdle",
# ── Classic (Vanilla WoW 1.x) — IDs 0145 ──
0: "STAND", 1: "DEATH", 2: "SPELL", 3: "STOP", 4: "WALK", 5: "RUN",
6: "DEAD", 7: "RISE", 8: "STAND_WOUND", 9: "COMBAT_WOUND",
10: "COMBAT_CRITICAL", 11: "SHUFFLE_LEFT", 12: "SHUFFLE_RIGHT",
13: "WALK_BACKWARDS", 14: "STUN", 15: "HANDS_CLOSED",
16: "ATTACK_UNARMED", 17: "ATTACK_1H", 18: "ATTACK_2H",
19: "ATTACK_2H_LOOSE", 20: "PARRY_UNARMED", 21: "PARRY_1H",
22: "PARRY_2H", 23: "PARRY_2H_LOOSE", 24: "SHIELD_BLOCK",
25: "READY_UNARMED", 26: "READY_1H", 27: "READY_2H",
28: "READY_2H_LOOSE", 29: "READY_BOW", 30: "DODGE",
31: "SPELL_PRECAST", 32: "SPELL_CAST", 33: "SPELL_CAST_AREA",
34: "NPC_WELCOME", 35: "NPC_GOODBYE", 36: "BLOCK",
37: "JUMP_START", 38: "JUMP", 39: "JUMP_END", 40: "FALL",
41: "SWIM_IDLE", 42: "SWIM", 43: "SWIM_LEFT", 44: "SWIM_RIGHT",
45: "SWIM_BACKWARDS", 46: "ATTACK_BOW", 47: "FIRE_BOW",
48: "READY_RIFLE", 49: "ATTACK_RIFLE", 50: "LOOT",
51: "READY_SPELL_DIRECTED", 52: "READY_SPELL_OMNI",
53: "SPELL_CAST_DIRECTED", 54: "SPELL_CAST_OMNI", 55: "BATTLE_ROAR",
56: "READY_ABILITY", 57: "SPECIAL_1H", 58: "SPECIAL_2H",
59: "SHIELD_BASH", 60: "EMOTE_TALK", 61: "EMOTE_EAT",
62: "EMOTE_WORK", 63: "EMOTE_USE_STANDING", 64: "EMOTE_EXCLAMATION",
65: "EMOTE_QUESTION", 66: "EMOTE_BOW", 67: "EMOTE_WAVE",
68: "EMOTE_CHEER", 69: "EMOTE_DANCE", 70: "EMOTE_LAUGH",
71: "EMOTE_SLEEP", 72: "EMOTE_SIT_GROUND", 73: "EMOTE_RUDE",
74: "EMOTE_ROAR", 75: "EMOTE_KNEEL", 76: "EMOTE_KISS",
77: "EMOTE_CRY", 78: "EMOTE_CHICKEN", 79: "EMOTE_BEG",
80: "EMOTE_APPLAUD", 81: "EMOTE_SHOUT", 82: "EMOTE_FLEX",
83: "EMOTE_SHY", 84: "EMOTE_POINT", 85: "ATTACK_1H_PIERCE",
86: "ATTACK_2H_LOOSE_PIERCE", 87: "ATTACK_OFF",
88: "ATTACK_OFF_PIERCE", 89: "SHEATHE", 90: "HIP_SHEATHE",
91: "MOUNT", 92: "RUN_RIGHT", 93: "RUN_LEFT",
94: "MOUNT_SPECIAL", 95: "KICK", 96: "SIT_GROUND_DOWN",
97: "SITTING", 98: "SIT_GROUND_UP", 99: "SLEEP_DOWN",
100: "SLEEP", 101: "SLEEP_UP", 102: "SIT_CHAIR_LOW",
103: "SIT_CHAIR_MED", 104: "SIT_CHAIR_HIGH", 105: "LOAD_BOW",
106: "LOAD_RIFLE", 107: "ATTACK_THROWN", 108: "READY_THROWN",
109: "HOLD_BOW", 110: "HOLD_RIFLE", 111: "HOLD_THROWN",
112: "LOAD_THROWN", 113: "EMOTE_SALUTE", 114: "KNEEL_START",
115: "KNEEL_LOOP", 116: "KNEEL_END", 117: "ATTACK_UNARMED_OFF",
118: "SPECIAL_UNARMED", 119: "STEALTH_WALK", 120: "STEALTH_STAND",
121: "KNOCKDOWN", 122: "EATING_LOOP", 123: "USE_STANDING_LOOP",
124: "CHANNEL_CAST_DIRECTED", 125: "CHANNEL_CAST_OMNI",
126: "WHIRLWIND", 127: "BIRTH", 128: "USE_STANDING_START",
129: "USE_STANDING_END", 130: "CREATURE_SPECIAL", 131: "DROWN",
132: "DROWNED", 133: "FISHING_CAST", 134: "FISHING_LOOP",
135: "FLY", 136: "EMOTE_WORK_NO_SHEATHE",
137: "EMOTE_STUN_NO_SHEATHE", 138: "EMOTE_USE_STANDING_NO_SHEATHE",
139: "SPELL_SLEEP_DOWN", 140: "SPELL_KNEEL_START",
141: "SPELL_KNEEL_LOOP", 142: "SPELL_KNEEL_END", 143: "SPRINT",
144: "IN_FLIGHT", 145: "SPAWN",
# ── The Burning Crusade (TBC 2.x) — IDs 146199 ──
146: "CLOSE", 147: "CLOSED", 148: "OPEN", 149: "DESTROY",
150: "DESTROYED", 151: "UNSHEATHE", 152: "SHEATHE_ALT",
153: "ATTACK_UNARMED_NO_SHEATHE", 154: "STEALTH_RUN",
155: "READY_CROSSBOW", 156: "ATTACK_CROSSBOW",
157: "EMOTE_TALK_EXCLAMATION", 158: "FLY_IDLE", 159: "FLY_FORWARD",
160: "FLY_BACKWARDS", 161: "FLY_LEFT", 162: "FLY_RIGHT",
163: "FLY_UP", 164: "FLY_DOWN", 165: "FLY_LAND_START",
166: "FLY_LAND_RUN", 167: "FLY_LAND_END",
168: "EMOTE_TALK_QUESTION", 169: "EMOTE_READ",
170: "EMOTE_SHIELDBLOCK", 171: "EMOTE_CHOP",
172: "EMOTE_HOLDRIFLE", 173: "EMOTE_HOLDBOW",
174: "EMOTE_HOLDTHROWN", 175: "CUSTOM_SPELL_02",
176: "CUSTOM_SPELL_03", 177: "CUSTOM_SPELL_04",
178: "CUSTOM_SPELL_05", 179: "CUSTOM_SPELL_06",
180: "CUSTOM_SPELL_07", 181: "CUSTOM_SPELL_08",
182: "CUSTOM_SPELL_09", 183: "CUSTOM_SPELL_10",
184: "EMOTE_STATE_DANCE",
# ── Wrath of the Lich King (WotLK 3.x) — IDs 185+ ──
185: "FLY_STAND", 186: "EMOTE_STATE_LAUGH",
187: "EMOTE_STATE_POINT", 188: "EMOTE_STATE_EAT",
189: "EMOTE_STATE_WORK", 190: "EMOTE_STATE_SIT_GROUND",
191: "EMOTE_STATE_HOLD_BOW", 192: "EMOTE_STATE_HOLD_RIFLE",
193: "EMOTE_STATE_HOLD_THROWN", 194: "FLY_COMBAT_WOUND",
195: "FLY_COMBAT_CRITICAL", 196: "RECLINED",
197: "EMOTE_STATE_ROAR", 198: "EMOTE_USE_STANDING_LOOP_2",
199: "EMOTE_STATE_APPLAUD", 200: "READY_FIST",
201: "SPELL_CHANNEL_DIRECTED_OMNI", 202: "SPECIAL_ATTACK_1H_OFF",
203: "ATTACK_FIST_1H", 204: "ATTACK_FIST_1H_OFF",
205: "PARRY_FIST_1H", 206: "READY_FIST_1H",
207: "EMOTE_STATE_READ_AND_TALK",
208: "EMOTE_STATE_WORK_NO_SHEATHE", 209: "FLY_RUN",
210: "EMOTE_STATE_KNEEL_2", 211: "EMOTE_STATE_SPELL_KNEEL",
212: "EMOTE_STATE_USE_STANDING", 213: "EMOTE_STATE_STUN",
214: "EMOTE_STATE_STUN_NO_SHEATHE", 215: "EMOTE_TRAIN",
216: "EMOTE_DEAD", 217: "EMOTE_STATE_DANCE_ONCE",
218: "FLY_DEATH", 219: "FLY_STAND_WOUND",
220: "FLY_SHUFFLE_LEFT", 221: "FLY_SHUFFLE_RIGHT",
222: "FLY_WALK_BACKWARDS", 223: "FLY_STUN",
224: "FLY_HANDS_CLOSED", 225: "FLY_ATTACK_UNARMED",
226: "FLY_ATTACK_1H", 227: "FLY_ATTACK_2H",
228: "FLY_ATTACK_2H_LOOSE", 229: "FLY_SPELL", 230: "FLY_STOP",
231: "FLY_WALK", 232: "FLY_DEAD", 233: "FLY_RISE",
234: "FLY_RUN_2", 235: "FLY_FALL", 236: "FLY_SWIM_IDLE",
237: "FLY_SWIM", 238: "FLY_SWIM_LEFT", 239: "FLY_SWIM_RIGHT",
240: "FLY_SWIM_BACKWARDS", 241: "FLY_ATTACK_BOW",
242: "FLY_FIRE_BOW", 243: "FLY_READY_RIFLE",
244: "FLY_ATTACK_RIFLE", 245: "TOTEM_SMALL", 246: "TOTEM_MEDIUM",
247: "TOTEM_LARGE", 248: "FLY_LOOT",
249: "FLY_READY_SPELL_DIRECTED", 250: "FLY_READY_SPELL_OMNI",
251: "FLY_SPELL_CAST_DIRECTED", 252: "FLY_SPELL_CAST_OMNI",
253: "FLY_BATTLE_ROAR", 254: "FLY_READY_ABILITY",
255: "FLY_SPECIAL_1H", 256: "FLY_SPECIAL_2H",
257: "FLY_SHIELD_BASH", 258: "FLY_EMOTE_TALK",
259: "FLY_EMOTE_EAT", 260: "FLY_EMOTE_WORK",
261: "FLY_EMOTE_USE_STANDING", 262: "FLY_EMOTE_BOW",
263: "FLY_EMOTE_WAVE", 264: "FLY_EMOTE_CHEER",
265: "FLY_EMOTE_DANCE", 266: "FLY_EMOTE_LAUGH",
267: "FLY_EMOTE_SLEEP", 268: "FLY_EMOTE_SIT_GROUND",
269: "FLY_EMOTE_RUDE", 270: "FLY_EMOTE_ROAR",
271: "FLY_EMOTE_KNEEL", 272: "FLY_EMOTE_KISS",
273: "FLY_EMOTE_CRY", 274: "FLY_EMOTE_CHICKEN",
275: "FLY_EMOTE_BEG", 276: "FLY_EMOTE_APPLAUD",
277: "FLY_EMOTE_SHOUT", 278: "FLY_EMOTE_FLEX",
279: "FLY_EMOTE_SHY", 280: "FLY_EMOTE_POINT",
281: "FLY_ATTACK_1H_PIERCE", 282: "FLY_ATTACK_2H_LOOSE_PIERCE",
283: "FLY_ATTACK_OFF", 284: "FLY_ATTACK_OFF_PIERCE",
285: "FLY_SHEATHE", 286: "FLY_HIP_SHEATHE", 287: "FLY_MOUNT",
288: "FLY_RUN_RIGHT", 289: "FLY_RUN_LEFT",
290: "FLY_MOUNT_SPECIAL", 291: "FLY_KICK",
292: "FLY_SIT_GROUND_DOWN", 293: "FLY_SITTING",
294: "FLY_SIT_GROUND_UP", 295: "FLY_SLEEP_DOWN",
296: "FLY_SLEEP", 297: "FLY_SLEEP_UP",
298: "FLY_SIT_CHAIR_LOW", 299: "FLY_SIT_CHAIR_MED",
300: "FLY_SIT_CHAIR_HIGH", 301: "FLY_LOAD_BOW",
302: "FLY_LOAD_RIFLE", 303: "FLY_ATTACK_THROWN",
304: "FLY_READY_THROWN", 305: "FLY_HOLD_BOW",
306: "FLY_HOLD_RIFLE", 307: "FLY_HOLD_THROWN",
308: "FLY_LOAD_THROWN", 309: "FLY_EMOTE_SALUTE",
310: "FLY_KNEEL_START", 311: "FLY_KNEEL_LOOP",
312: "FLY_KNEEL_END", 313: "FLY_ATTACK_UNARMED_OFF",
314: "FLY_SPECIAL_UNARMED", 315: "FLY_STEALTH_WALK",
316: "FLY_STEALTH_STAND", 317: "FLY_KNOCKDOWN",
318: "FLY_EATING_LOOP", 319: "FLY_USE_STANDING_LOOP",
320: "FLY_CHANNEL_CAST_DIRECTED", 321: "FLY_CHANNEL_CAST_OMNI",
322: "FLY_WHIRLWIND", 323: "FLY_BIRTH",
324: "FLY_USE_STANDING_START", 325: "FLY_USE_STANDING_END",
326: "FLY_CREATURE_SPECIAL", 327: "FLY_DROWN",
328: "FLY_DROWNED", 329: "FLY_FISHING_CAST",
330: "FLY_FISHING_LOOP", 331: "FLY_FLY",
332: "FLY_EMOTE_WORK_NO_SHEATHE",
333: "FLY_EMOTE_STUN_NO_SHEATHE",
334: "FLY_EMOTE_USE_STANDING_NO_SHEATHE",
335: "FLY_SPELL_SLEEP_DOWN", 336: "FLY_SPELL_KNEEL_START",
337: "FLY_SPELL_KNEEL_LOOP", 338: "FLY_SPELL_KNEEL_END",
339: "FLY_SPRINT", 340: "FLY_IN_FLIGHT", 341: "FLY_SPAWN",
342: "FLY_CLOSE", 343: "FLY_CLOSED", 344: "FLY_OPEN",
345: "FLY_DESTROY", 346: "FLY_DESTROYED", 347: "FLY_UNSHEATHE",
348: "FLY_SHEATHE_ALT", 349: "FLY_ATTACK_UNARMED_NO_SHEATHE",
350: "FLY_STEALTH_RUN", 351: "FLY_READY_CROSSBOW",
352: "FLY_ATTACK_CROSSBOW", 353: "FLY_EMOTE_TALK_EXCLAMATION",
354: "FLY_EMOTE_TALK_QUESTION", 355: "FLY_EMOTE_READ",
356: "EMOTE_HOLD_CROSSBOW", 357: "FLY_EMOTE_HOLD_BOW",
358: "FLY_EMOTE_HOLD_RIFLE", 359: "FLY_EMOTE_HOLD_THROWN",
360: "FLY_EMOTE_HOLD_CROSSBOW", 361: "FLY_CUSTOM_SPELL_02",
362: "FLY_CUSTOM_SPELL_03", 363: "FLY_CUSTOM_SPELL_04",
364: "FLY_CUSTOM_SPELL_05", 365: "FLY_CUSTOM_SPELL_06",
366: "FLY_CUSTOM_SPELL_07", 367: "FLY_CUSTOM_SPELL_08",
368: "FLY_CUSTOM_SPELL_09", 369: "FLY_CUSTOM_SPELL_10",
370: "FLY_EMOTE_STATE_DANCE", 371: "EMOTE_EAT_NO_SHEATHE",
372: "MOUNT_RUN_RIGHT", 373: "MOUNT_RUN_LEFT",
374: "MOUNT_WALK_BACKWARDS", 375: "MOUNT_SWIM_IDLE",
376: "MOUNT_SWIM", 377: "MOUNT_SWIM_LEFT",
378: "MOUNT_SWIM_RIGHT", 379: "MOUNT_SWIM_BACKWARDS",
380: "MOUNT_FLIGHT_IDLE", 381: "MOUNT_FLIGHT_FORWARD",
382: "MOUNT_FLIGHT_BACKWARDS", 383: "MOUNT_FLIGHT_LEFT",
384: "MOUNT_FLIGHT_RIGHT", 385: "MOUNT_FLIGHT_UP",
386: "MOUNT_FLIGHT_DOWN", 387: "MOUNT_FLIGHT_LAND_START",
388: "MOUNT_FLIGHT_LAND_RUN", 389: "MOUNT_FLIGHT_LAND_END",
390: "FLY_EMOTE_STATE_LAUGH", 391: "FLY_EMOTE_STATE_POINT",
392: "FLY_EMOTE_STATE_EAT", 393: "FLY_EMOTE_STATE_WORK",
394: "FLY_EMOTE_STATE_SIT_GROUND",
395: "FLY_EMOTE_STATE_HOLD_BOW",
396: "FLY_EMOTE_STATE_HOLD_RIFLE",
397: "FLY_EMOTE_STATE_HOLD_THROWN",
398: "FLY_EMOTE_STATE_ROAR", 399: "FLY_RECLINED",
400: "EMOTE_TRAIN_2", 401: "EMOTE_DEAD_2",
402: "FLY_EMOTE_USE_STANDING_LOOP_2",
403: "FLY_EMOTE_STATE_APPLAUD", 404: "FLY_READY_FIST",
405: "FLY_SPELL_CHANNEL_DIRECTED_OMNI",
406: "FLY_SPECIAL_ATTACK_1H_OFF", 407: "FLY_ATTACK_FIST_1H",
408: "FLY_ATTACK_FIST_1H_OFF", 409: "FLY_PARRY_FIST_1H",
410: "FLY_READY_FIST_1H", 411: "FLY_EMOTE_STATE_READ_AND_TALK",
412: "FLY_EMOTE_STATE_WORK_NO_SHEATHE",
413: "FLY_EMOTE_STATE_KNEEL_2",
414: "FLY_EMOTE_STATE_SPELL_KNEEL",
415: "FLY_EMOTE_STATE_USE_STANDING",
416: "FLY_EMOTE_STATE_STUN",
417: "FLY_EMOTE_STATE_STUN_NO_SHEATHE",
418: "FLY_EMOTE_TRAIN", 419: "FLY_EMOTE_DEAD",
420: "FLY_EMOTE_STATE_DANCE_ONCE",
421: "FLY_EMOTE_EAT_NO_SHEATHE", 422: "FLY_MOUNT_RUN_RIGHT",
423: "FLY_MOUNT_RUN_LEFT", 424: "FLY_MOUNT_WALK_BACKWARDS",
425: "FLY_MOUNT_SWIM_IDLE", 426: "FLY_MOUNT_SWIM",
427: "FLY_MOUNT_SWIM_LEFT", 428: "FLY_MOUNT_SWIM_RIGHT",
429: "FLY_MOUNT_SWIM_BACKWARDS", 430: "FLY_MOUNT_FLIGHT_IDLE",
431: "FLY_MOUNT_FLIGHT_FORWARD",
432: "FLY_MOUNT_FLIGHT_BACKWARDS",
433: "FLY_MOUNT_FLIGHT_LEFT", 434: "FLY_MOUNT_FLIGHT_RIGHT",
435: "FLY_MOUNT_FLIGHT_UP", 436: "FLY_MOUNT_FLIGHT_DOWN",
437: "FLY_MOUNT_FLIGHT_LAND_START",
438: "FLY_MOUNT_FLIGHT_LAND_RUN",
439: "FLY_MOUNT_FLIGHT_LAND_END", 440: "FLY_TOTEM_SMALL",
441: "FLY_TOTEM_MEDIUM", 442: "FLY_TOTEM_LARGE",
443: "FLY_EMOTE_HOLD_CROSSBOW_2", 444: "VEHICLE_GRAB",
445: "VEHICLE_THROW", 446: "FLY_VEHICLE_GRAB",
447: "FLY_VEHICLE_THROW", 448: "GUILD_CHAMPION_1",
449: "GUILD_CHAMPION_2", 450: "FLY_GUILD_CHAMPION_1",
451: "FLY_GUILD_CHAMPION_2",
}
# Texture type names for non-filename textures

View file

@ -538,15 +538,196 @@ class M2Parser:
# ---------------------------------------------------------------------------
_ANIM_NAMES: dict[int, str] = {
0: "Stand", 1: "Death", 2: "Spell", 3: "Stop", 4: "Walk", 5: "Run",
6: "Dead", 7: "Rise", 8: "StandWound", 9: "CombatWound", 10: "CombatCritical",
11: "ShuffleLeft", 12: "ShuffleRight", 13: "Walkbackwards", 14: "Stun",
15: "HandsClosed", 16: "AttackUnarmed", 17: "Attack1H", 18: "Attack2H",
24: "ShieldBlock", 25: "ReadyUnarmed", 26: "Ready1H",
27: "Ready2H", 34: "NPCWelcome", 35: "NPCGoodbye",
37: "JumpStart", 38: "Jump", 39: "JumpEnd", 40: "Fall",
41: "SwimIdle", 42: "Swim", 60: "SpellChannelDirected",
69: "CombatAbility", 138: "Fly", 157: "EmoteTalk", 185: "FlyIdle",
# ── Classic (Vanilla WoW 1.x) — IDs 0145 ──
0: "STAND", 1: "DEATH", 2: "SPELL", 3: "STOP", 4: "WALK", 5: "RUN",
6: "DEAD", 7: "RISE", 8: "STAND_WOUND", 9: "COMBAT_WOUND",
10: "COMBAT_CRITICAL", 11: "SHUFFLE_LEFT", 12: "SHUFFLE_RIGHT",
13: "WALK_BACKWARDS", 14: "STUN", 15: "HANDS_CLOSED",
16: "ATTACK_UNARMED", 17: "ATTACK_1H", 18: "ATTACK_2H",
19: "ATTACK_2H_LOOSE", 20: "PARRY_UNARMED", 21: "PARRY_1H",
22: "PARRY_2H", 23: "PARRY_2H_LOOSE", 24: "SHIELD_BLOCK",
25: "READY_UNARMED", 26: "READY_1H", 27: "READY_2H",
28: "READY_2H_LOOSE", 29: "READY_BOW", 30: "DODGE",
31: "SPELL_PRECAST", 32: "SPELL_CAST", 33: "SPELL_CAST_AREA",
34: "NPC_WELCOME", 35: "NPC_GOODBYE", 36: "BLOCK",
37: "JUMP_START", 38: "JUMP", 39: "JUMP_END", 40: "FALL",
41: "SWIM_IDLE", 42: "SWIM", 43: "SWIM_LEFT", 44: "SWIM_RIGHT",
45: "SWIM_BACKWARDS", 46: "ATTACK_BOW", 47: "FIRE_BOW",
48: "READY_RIFLE", 49: "ATTACK_RIFLE", 50: "LOOT",
51: "READY_SPELL_DIRECTED", 52: "READY_SPELL_OMNI",
53: "SPELL_CAST_DIRECTED", 54: "SPELL_CAST_OMNI", 55: "BATTLE_ROAR",
56: "READY_ABILITY", 57: "SPECIAL_1H", 58: "SPECIAL_2H",
59: "SHIELD_BASH", 60: "EMOTE_TALK", 61: "EMOTE_EAT",
62: "EMOTE_WORK", 63: "EMOTE_USE_STANDING", 64: "EMOTE_EXCLAMATION",
65: "EMOTE_QUESTION", 66: "EMOTE_BOW", 67: "EMOTE_WAVE",
68: "EMOTE_CHEER", 69: "EMOTE_DANCE", 70: "EMOTE_LAUGH",
71: "EMOTE_SLEEP", 72: "EMOTE_SIT_GROUND", 73: "EMOTE_RUDE",
74: "EMOTE_ROAR", 75: "EMOTE_KNEEL", 76: "EMOTE_KISS",
77: "EMOTE_CRY", 78: "EMOTE_CHICKEN", 79: "EMOTE_BEG",
80: "EMOTE_APPLAUD", 81: "EMOTE_SHOUT", 82: "EMOTE_FLEX",
83: "EMOTE_SHY", 84: "EMOTE_POINT", 85: "ATTACK_1H_PIERCE",
86: "ATTACK_2H_LOOSE_PIERCE", 87: "ATTACK_OFF", 88: "ATTACK_OFF_PIERCE",
89: "SHEATHE", 90: "HIP_SHEATHE", 91: "MOUNT",
92: "RUN_RIGHT", 93: "RUN_LEFT", 94: "MOUNT_SPECIAL", 95: "KICK",
96: "SIT_GROUND_DOWN", 97: "SITTING", 98: "SIT_GROUND_UP",
99: "SLEEP_DOWN", 100: "SLEEP", 101: "SLEEP_UP",
102: "SIT_CHAIR_LOW", 103: "SIT_CHAIR_MED", 104: "SIT_CHAIR_HIGH",
105: "LOAD_BOW", 106: "LOAD_RIFLE", 107: "ATTACK_THROWN",
108: "READY_THROWN", 109: "HOLD_BOW", 110: "HOLD_RIFLE",
111: "HOLD_THROWN", 112: "LOAD_THROWN", 113: "EMOTE_SALUTE",
114: "KNEEL_START", 115: "KNEEL_LOOP", 116: "KNEEL_END",
117: "ATTACK_UNARMED_OFF", 118: "SPECIAL_UNARMED",
119: "STEALTH_WALK", 120: "STEALTH_STAND", 121: "KNOCKDOWN",
122: "EATING_LOOP", 123: "USE_STANDING_LOOP",
124: "CHANNEL_CAST_DIRECTED", 125: "CHANNEL_CAST_OMNI",
126: "WHIRLWIND", 127: "BIRTH", 128: "USE_STANDING_START",
129: "USE_STANDING_END", 130: "CREATURE_SPECIAL", 131: "DROWN",
132: "DROWNED", 133: "FISHING_CAST", 134: "FISHING_LOOP", 135: "FLY",
136: "EMOTE_WORK_NO_SHEATHE", 137: "EMOTE_STUN_NO_SHEATHE",
138: "EMOTE_USE_STANDING_NO_SHEATHE", 139: "SPELL_SLEEP_DOWN",
140: "SPELL_KNEEL_START", 141: "SPELL_KNEEL_LOOP",
142: "SPELL_KNEEL_END", 143: "SPRINT", 144: "IN_FLIGHT", 145: "SPAWN",
# ── The Burning Crusade (TBC 2.x) — IDs 146199 ──
146: "CLOSE", 147: "CLOSED", 148: "OPEN", 149: "DESTROY",
150: "DESTROYED", 151: "UNSHEATHE", 152: "SHEATHE_ALT",
153: "ATTACK_UNARMED_NO_SHEATHE", 154: "STEALTH_RUN",
155: "READY_CROSSBOW", 156: "ATTACK_CROSSBOW",
157: "EMOTE_TALK_EXCLAMATION", 158: "FLY_IDLE", 159: "FLY_FORWARD",
160: "FLY_BACKWARDS", 161: "FLY_LEFT", 162: "FLY_RIGHT",
163: "FLY_UP", 164: "FLY_DOWN", 165: "FLY_LAND_START",
166: "FLY_LAND_RUN", 167: "FLY_LAND_END",
168: "EMOTE_TALK_QUESTION", 169: "EMOTE_READ",
170: "EMOTE_SHIELDBLOCK", 171: "EMOTE_CHOP", 172: "EMOTE_HOLDRIFLE",
173: "EMOTE_HOLDBOW", 174: "EMOTE_HOLDTHROWN",
175: "CUSTOM_SPELL_02", 176: "CUSTOM_SPELL_03", 177: "CUSTOM_SPELL_04",
178: "CUSTOM_SPELL_05", 179: "CUSTOM_SPELL_06", 180: "CUSTOM_SPELL_07",
181: "CUSTOM_SPELL_08", 182: "CUSTOM_SPELL_09", 183: "CUSTOM_SPELL_10",
184: "EMOTE_STATE_DANCE",
# ── Wrath of the Lich King (WotLK 3.x) — IDs 185+ ──
185: "FLY_STAND", 186: "EMOTE_STATE_LAUGH", 187: "EMOTE_STATE_POINT",
188: "EMOTE_STATE_EAT", 189: "EMOTE_STATE_WORK",
190: "EMOTE_STATE_SIT_GROUND", 191: "EMOTE_STATE_HOLD_BOW",
192: "EMOTE_STATE_HOLD_RIFLE", 193: "EMOTE_STATE_HOLD_THROWN",
194: "FLY_COMBAT_WOUND", 195: "FLY_COMBAT_CRITICAL", 196: "RECLINED",
197: "EMOTE_STATE_ROAR", 198: "EMOTE_USE_STANDING_LOOP_2",
199: "EMOTE_STATE_APPLAUD", 200: "READY_FIST",
201: "SPELL_CHANNEL_DIRECTED_OMNI", 202: "SPECIAL_ATTACK_1H_OFF",
203: "ATTACK_FIST_1H", 204: "ATTACK_FIST_1H_OFF",
205: "PARRY_FIST_1H", 206: "READY_FIST_1H",
207: "EMOTE_STATE_READ_AND_TALK", 208: "EMOTE_STATE_WORK_NO_SHEATHE",
209: "FLY_RUN", 210: "EMOTE_STATE_KNEEL_2",
211: "EMOTE_STATE_SPELL_KNEEL", 212: "EMOTE_STATE_USE_STANDING",
213: "EMOTE_STATE_STUN", 214: "EMOTE_STATE_STUN_NO_SHEATHE",
215: "EMOTE_TRAIN", 216: "EMOTE_DEAD",
217: "EMOTE_STATE_DANCE_ONCE", 218: "FLY_DEATH",
219: "FLY_STAND_WOUND", 220: "FLY_SHUFFLE_LEFT",
221: "FLY_SHUFFLE_RIGHT", 222: "FLY_WALK_BACKWARDS",
223: "FLY_STUN", 224: "FLY_HANDS_CLOSED", 225: "FLY_ATTACK_UNARMED",
226: "FLY_ATTACK_1H", 227: "FLY_ATTACK_2H",
228: "FLY_ATTACK_2H_LOOSE", 229: "FLY_SPELL", 230: "FLY_STOP",
231: "FLY_WALK", 232: "FLY_DEAD", 233: "FLY_RISE", 234: "FLY_RUN_2",
235: "FLY_FALL", 236: "FLY_SWIM_IDLE", 237: "FLY_SWIM",
238: "FLY_SWIM_LEFT", 239: "FLY_SWIM_RIGHT",
240: "FLY_SWIM_BACKWARDS", 241: "FLY_ATTACK_BOW",
242: "FLY_FIRE_BOW", 243: "FLY_READY_RIFLE",
244: "FLY_ATTACK_RIFLE", 245: "TOTEM_SMALL", 246: "TOTEM_MEDIUM",
247: "TOTEM_LARGE", 248: "FLY_LOOT",
249: "FLY_READY_SPELL_DIRECTED", 250: "FLY_READY_SPELL_OMNI",
251: "FLY_SPELL_CAST_DIRECTED", 252: "FLY_SPELL_CAST_OMNI",
253: "FLY_BATTLE_ROAR", 254: "FLY_READY_ABILITY",
255: "FLY_SPECIAL_1H", 256: "FLY_SPECIAL_2H",
257: "FLY_SHIELD_BASH", 258: "FLY_EMOTE_TALK", 259: "FLY_EMOTE_EAT",
260: "FLY_EMOTE_WORK", 261: "FLY_EMOTE_USE_STANDING",
262: "FLY_EMOTE_BOW", 263: "FLY_EMOTE_WAVE", 264: "FLY_EMOTE_CHEER",
265: "FLY_EMOTE_DANCE", 266: "FLY_EMOTE_LAUGH",
267: "FLY_EMOTE_SLEEP", 268: "FLY_EMOTE_SIT_GROUND",
269: "FLY_EMOTE_RUDE", 270: "FLY_EMOTE_ROAR",
271: "FLY_EMOTE_KNEEL", 272: "FLY_EMOTE_KISS", 273: "FLY_EMOTE_CRY",
274: "FLY_EMOTE_CHICKEN", 275: "FLY_EMOTE_BEG",
276: "FLY_EMOTE_APPLAUD", 277: "FLY_EMOTE_SHOUT",
278: "FLY_EMOTE_FLEX", 279: "FLY_EMOTE_SHY", 280: "FLY_EMOTE_POINT",
281: "FLY_ATTACK_1H_PIERCE", 282: "FLY_ATTACK_2H_LOOSE_PIERCE",
283: "FLY_ATTACK_OFF", 284: "FLY_ATTACK_OFF_PIERCE",
285: "FLY_SHEATHE", 286: "FLY_HIP_SHEATHE", 287: "FLY_MOUNT",
288: "FLY_RUN_RIGHT", 289: "FLY_RUN_LEFT",
290: "FLY_MOUNT_SPECIAL", 291: "FLY_KICK",
292: "FLY_SIT_GROUND_DOWN", 293: "FLY_SITTING",
294: "FLY_SIT_GROUND_UP", 295: "FLY_SLEEP_DOWN", 296: "FLY_SLEEP",
297: "FLY_SLEEP_UP", 298: "FLY_SIT_CHAIR_LOW",
299: "FLY_SIT_CHAIR_MED", 300: "FLY_SIT_CHAIR_HIGH",
301: "FLY_LOAD_BOW", 302: "FLY_LOAD_RIFLE",
303: "FLY_ATTACK_THROWN", 304: "FLY_READY_THROWN",
305: "FLY_HOLD_BOW", 306: "FLY_HOLD_RIFLE", 307: "FLY_HOLD_THROWN",
308: "FLY_LOAD_THROWN", 309: "FLY_EMOTE_SALUTE",
310: "FLY_KNEEL_START", 311: "FLY_KNEEL_LOOP", 312: "FLY_KNEEL_END",
313: "FLY_ATTACK_UNARMED_OFF", 314: "FLY_SPECIAL_UNARMED",
315: "FLY_STEALTH_WALK", 316: "FLY_STEALTH_STAND",
317: "FLY_KNOCKDOWN", 318: "FLY_EATING_LOOP",
319: "FLY_USE_STANDING_LOOP", 320: "FLY_CHANNEL_CAST_DIRECTED",
321: "FLY_CHANNEL_CAST_OMNI", 322: "FLY_WHIRLWIND",
323: "FLY_BIRTH", 324: "FLY_USE_STANDING_START",
325: "FLY_USE_STANDING_END", 326: "FLY_CREATURE_SPECIAL",
327: "FLY_DROWN", 328: "FLY_DROWNED", 329: "FLY_FISHING_CAST",
330: "FLY_FISHING_LOOP", 331: "FLY_FLY",
332: "FLY_EMOTE_WORK_NO_SHEATHE", 333: "FLY_EMOTE_STUN_NO_SHEATHE",
334: "FLY_EMOTE_USE_STANDING_NO_SHEATHE",
335: "FLY_SPELL_SLEEP_DOWN", 336: "FLY_SPELL_KNEEL_START",
337: "FLY_SPELL_KNEEL_LOOP", 338: "FLY_SPELL_KNEEL_END",
339: "FLY_SPRINT", 340: "FLY_IN_FLIGHT", 341: "FLY_SPAWN",
342: "FLY_CLOSE", 343: "FLY_CLOSED", 344: "FLY_OPEN",
345: "FLY_DESTROY", 346: "FLY_DESTROYED", 347: "FLY_UNSHEATHE",
348: "FLY_SHEATHE_ALT", 349: "FLY_ATTACK_UNARMED_NO_SHEATHE",
350: "FLY_STEALTH_RUN", 351: "FLY_READY_CROSSBOW",
352: "FLY_ATTACK_CROSSBOW", 353: "FLY_EMOTE_TALK_EXCLAMATION",
354: "FLY_EMOTE_TALK_QUESTION", 355: "FLY_EMOTE_READ",
356: "EMOTE_HOLD_CROSSBOW", 357: "FLY_EMOTE_HOLD_BOW",
358: "FLY_EMOTE_HOLD_RIFLE", 359: "FLY_EMOTE_HOLD_THROWN",
360: "FLY_EMOTE_HOLD_CROSSBOW", 361: "FLY_CUSTOM_SPELL_02",
362: "FLY_CUSTOM_SPELL_03", 363: "FLY_CUSTOM_SPELL_04",
364: "FLY_CUSTOM_SPELL_05", 365: "FLY_CUSTOM_SPELL_06",
366: "FLY_CUSTOM_SPELL_07", 367: "FLY_CUSTOM_SPELL_08",
368: "FLY_CUSTOM_SPELL_09", 369: "FLY_CUSTOM_SPELL_10",
370: "FLY_EMOTE_STATE_DANCE", 371: "EMOTE_EAT_NO_SHEATHE",
372: "MOUNT_RUN_RIGHT", 373: "MOUNT_RUN_LEFT",
374: "MOUNT_WALK_BACKWARDS", 375: "MOUNT_SWIM_IDLE",
376: "MOUNT_SWIM", 377: "MOUNT_SWIM_LEFT", 378: "MOUNT_SWIM_RIGHT",
379: "MOUNT_SWIM_BACKWARDS", 380: "MOUNT_FLIGHT_IDLE",
381: "MOUNT_FLIGHT_FORWARD", 382: "MOUNT_FLIGHT_BACKWARDS",
383: "MOUNT_FLIGHT_LEFT", 384: "MOUNT_FLIGHT_RIGHT",
385: "MOUNT_FLIGHT_UP", 386: "MOUNT_FLIGHT_DOWN",
387: "MOUNT_FLIGHT_LAND_START", 388: "MOUNT_FLIGHT_LAND_RUN",
389: "MOUNT_FLIGHT_LAND_END", 390: "FLY_EMOTE_STATE_LAUGH",
391: "FLY_EMOTE_STATE_POINT", 392: "FLY_EMOTE_STATE_EAT",
393: "FLY_EMOTE_STATE_WORK", 394: "FLY_EMOTE_STATE_SIT_GROUND",
395: "FLY_EMOTE_STATE_HOLD_BOW", 396: "FLY_EMOTE_STATE_HOLD_RIFLE",
397: "FLY_EMOTE_STATE_HOLD_THROWN", 398: "FLY_EMOTE_STATE_ROAR",
399: "FLY_RECLINED", 400: "EMOTE_TRAIN_2", 401: "EMOTE_DEAD_2",
402: "FLY_EMOTE_USE_STANDING_LOOP_2", 403: "FLY_EMOTE_STATE_APPLAUD",
404: "FLY_READY_FIST", 405: "FLY_SPELL_CHANNEL_DIRECTED_OMNI",
406: "FLY_SPECIAL_ATTACK_1H_OFF", 407: "FLY_ATTACK_FIST_1H",
408: "FLY_ATTACK_FIST_1H_OFF", 409: "FLY_PARRY_FIST_1H",
410: "FLY_READY_FIST_1H", 411: "FLY_EMOTE_STATE_READ_AND_TALK",
412: "FLY_EMOTE_STATE_WORK_NO_SHEATHE",
413: "FLY_EMOTE_STATE_KNEEL_2", 414: "FLY_EMOTE_STATE_SPELL_KNEEL",
415: "FLY_EMOTE_STATE_USE_STANDING", 416: "FLY_EMOTE_STATE_STUN",
417: "FLY_EMOTE_STATE_STUN_NO_SHEATHE", 418: "FLY_EMOTE_TRAIN",
419: "FLY_EMOTE_DEAD", 420: "FLY_EMOTE_STATE_DANCE_ONCE",
421: "FLY_EMOTE_EAT_NO_SHEATHE", 422: "FLY_MOUNT_RUN_RIGHT",
423: "FLY_MOUNT_RUN_LEFT", 424: "FLY_MOUNT_WALK_BACKWARDS",
425: "FLY_MOUNT_SWIM_IDLE", 426: "FLY_MOUNT_SWIM",
427: "FLY_MOUNT_SWIM_LEFT", 428: "FLY_MOUNT_SWIM_RIGHT",
429: "FLY_MOUNT_SWIM_BACKWARDS", 430: "FLY_MOUNT_FLIGHT_IDLE",
431: "FLY_MOUNT_FLIGHT_FORWARD", 432: "FLY_MOUNT_FLIGHT_BACKWARDS",
433: "FLY_MOUNT_FLIGHT_LEFT", 434: "FLY_MOUNT_FLIGHT_RIGHT",
435: "FLY_MOUNT_FLIGHT_UP", 436: "FLY_MOUNT_FLIGHT_DOWN",
437: "FLY_MOUNT_FLIGHT_LAND_START", 438: "FLY_MOUNT_FLIGHT_LAND_RUN",
439: "FLY_MOUNT_FLIGHT_LAND_END", 440: "FLY_TOTEM_SMALL",
441: "FLY_TOTEM_MEDIUM", 442: "FLY_TOTEM_LARGE",
443: "FLY_EMOTE_HOLD_CROSSBOW_2", 444: "VEHICLE_GRAB",
445: "VEHICLE_THROW", 446: "FLY_VEHICLE_GRAB",
447: "FLY_VEHICLE_THROW", 448: "GUILD_CHAMPION_1",
449: "GUILD_CHAMPION_2", 450: "FLY_GUILD_CHAMPION_1",
451: "FLY_GUILD_CHAMPION_2",
}