From e58f9b4b40d5fc1fb1f6afaf8935788b2d333bd9 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 4 Apr 2026 23:02:53 +0300 Subject: [PATCH] feat(animation): 452 named constants, 30-phase character animation state machine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CMakeLists.txt | 1 + Data/expansions/classic/update_fields.json | 1 + Data/expansions/tbc/update_fields.json | 1 + Data/expansions/turtle/update_fields.json | 1 + Data/expansions/wotlk/update_fields.json | 4 +- assets/shaders/character.frag.glsl | 2 +- assets/shaders/m2.vert.glsl | 6 +- assets/shaders/m2_cull.comp.glsl | 2 +- assets/shaders/terrain.frag.glsl | 4 +- assets/shaders/wmo.frag.glsl | 2 +- include/core/appearance_composer.hpp | 4 +- include/game/entity.hpp | 5 + include/game/entity_controller.hpp | 2 +- include/game/game_handler.hpp | 47 +- include/game/inventory.hpp | 32 + include/game/spell_defines.hpp | 71 ++ include/game/update_field_table.hpp | 2 + include/rendering/animation_controller.hpp | 127 ++- include/rendering/animation_ids.hpp | 514 ++++++++++ include/rendering/camera_controller.hpp | 3 + include/rendering/m2_renderer.hpp | 11 +- include/rendering/render_graph.hpp | 2 +- include/rendering/renderer.hpp | 13 +- include/rendering/terrain_renderer.hpp | 4 +- include/ui/auth_screen.hpp | 29 + src/core/application.cpp | 407 ++++++-- src/core/entity_spawner.cpp | 51 +- src/core/world_loader.cpp | 22 +- src/game/combat_handler.cpp | 16 +- src/game/entity_controller.cpp | 49 +- src/game/game_handler.cpp | 36 +- src/game/inventory_handler.cpp | 3 + src/game/spell_handler.cpp | 89 +- src/game/update_field_table.cpp | 2 + src/pipeline/dbc_loader.cpp | 4 +- src/rendering/animation_controller.cpp | 1001 ++++++++++++++++++-- src/rendering/animation_ids.cpp | 567 +++++++++++ src/rendering/celestial.cpp | 8 +- src/rendering/character_preview.cpp | 3 +- src/rendering/character_renderer.cpp | 44 +- src/rendering/m2_renderer.cpp | 31 + src/rendering/renderer.cpp | 109 ++- src/rendering/terrain_renderer.cpp | 10 +- src/ui/auth_screen.cpp | 223 ++++- src/ui/combat_ui.cpp | 6 +- src/ui/dialog_manager.cpp | 2 +- src/ui/game_screen.cpp | 174 +--- tests/CMakeLists.txt | 13 + tests/test_animation_ids.cpp | 184 ++++ tests/test_blp_loader.cpp | 2 +- tests/test_dbc_loader.cpp | 2 +- tests/test_entity.cpp | 2 +- tests/test_frustum.cpp | 2 +- tests/test_m2_structs.cpp | 2 +- tests/test_opcode_table.cpp | 2 +- tests/test_packet.cpp | 2 +- tests/test_srp.cpp | 2 +- tools/asset_pipeline_gui.py | 227 ++++- tools/m2_viewer.py | 199 +++- 59 files changed, 3903 insertions(+), 483 deletions(-) create mode 100644 include/rendering/animation_ids.hpp create mode 100644 src/rendering/animation_ids.cpp create mode 100644 tests/test_animation_ids.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 88daaa4a..866ac38a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/Data/expansions/classic/update_fields.json b/Data/expansions/classic/update_fields.json index 4a602e91..5b214f59 100644 --- a/Data/expansions/classic/update_fields.json +++ b/Data/expansions/classic/update_fields.json @@ -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, diff --git a/Data/expansions/tbc/update_fields.json b/Data/expansions/tbc/update_fields.json index 471ac235..2e75c268 100644 --- a/Data/expansions/tbc/update_fields.json +++ b/Data/expansions/tbc/update_fields.json @@ -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, diff --git a/Data/expansions/turtle/update_fields.json b/Data/expansions/turtle/update_fields.json index 4a602e91..5b214f59 100644 --- a/Data/expansions/turtle/update_fields.json +++ b/Data/expansions/turtle/update_fields.json @@ -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, diff --git a/Data/expansions/wotlk/update_fields.json b/Data/expansions/wotlk/update_fields.json index 06bcbd62..49fcbdd5 100644 --- a/Data/expansions/wotlk/update_fields.json +++ b/Data/expansions/wotlk/update_fields.json @@ -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 } diff --git a/assets/shaders/character.frag.glsl b/assets/shaders/character.frag.glsl index 669eb84f..a4bdada6 100644 --- a/assets/shaders/character.frag.glsl +++ b/assets/shaders/character.frag.glsl @@ -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); diff --git a/assets/shaders/m2.vert.glsl b/assets/shaders/m2.vert.glsl index a5913ca2..3b8c113e 100644 --- a/assets/shaders/m2.vert.glsl +++ b/assets/shaders/m2.vert.glsl @@ -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; diff --git a/assets/shaders/m2_cull.comp.glsl b/assets/shaders/m2_cull.comp.glsl index 831a521e..fd87ff93 100644 --- a/assets/shaders/m2_cull.comp.glsl +++ b/assets/shaders/m2_cull.comp.glsl @@ -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). diff --git a/assets/shaders/terrain.frag.glsl b/assets/shaders/terrain.frag.glsl index 0a424090..0fcdd7dd 100644 --- a/assets/shaders/terrain.frag.glsl +++ b/assets/shaders/terrain.frag.glsl @@ -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); } diff --git a/assets/shaders/wmo.frag.glsl b/assets/shaders/wmo.frag.glsl index c2b3b1cd..ce29a0d1 100644 --- a/assets/shaders/wmo.frag.glsl +++ b/assets/shaders/wmo.frag.glsl @@ -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); diff --git a/include/core/appearance_composer.hpp b/include/core/appearance_composer.hpp index fa722bfe..d38ab53c 100644 --- a/include/core/appearance_composer.hpp +++ b/include/core/appearance_composer.hpp @@ -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); diff --git a/include/game/entity.hpp b/include/game/entity.hpp index 27e47712..bd5dfc5f 100644 --- a/include/game/entity.hpp +++ b/include/game/entity.hpp @@ -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; }; diff --git a/include/game/entity_controller.hpp b/include/game/entity_controller.hpp index 17af614b..f114b8ad 100644 --- a/include/game/entity_controller.hpp +++ b/include/game/entity_controller.hpp @@ -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(); }; diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 8aceba15..36f40aae 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -934,7 +934,8 @@ public: void setGhostStateCallback(GhostStateCallback cb) { ghostStateCallback_ = std::move(cb); } // Melee swing callback (for driving animation/SFX) - using MeleeSwingCallback = std::function; + // spellId: 0 = regular auto-attack swing, non-zero = melee ability (special attack) + using MeleeSwingCallback = std::function; 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 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 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 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 setStealthStateCallback(StealthStateCallback cb) { stealthStateCallback_ = std::move(cb); } + + // Player health changed callback — fires when local player HP changes + using PlayerHealthCallback = std::function; + void setPlayerHealthCallback(PlayerHealthCallback cb) { playerHealthCallback_ = std::move(cb); } + // NPC greeting callback (plays voice line when NPC is clicked) using NpcGreetingCallback = std::function; void setNpcGreetingCallback(NpcGreetingCallback cb) { npcGreetingCallback_ = std::move(cb); } @@ -1093,6 +1111,19 @@ public: using GameObjectCustomAnimCallback = std::function; 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 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 setSprintAuraCallback(SprintAuraCallback cb) { sprintAuraCallback_ = std::move(cb); } + + // Vehicle state callback — fired when player enters/exits a vehicle + using VehicleStateCallback = std::function; + void setVehicleStateCallback(VehicleStateCallback cb) { vehicleStateCallback_ = std::move(cb); } + // Faction hostility map (populated from FactionTemplate.dbc by Application) void setFactionHostileMap(std::unordered_map map) { factionHostileMap_ = std::move(map); } @@ -1806,6 +1837,10 @@ public: using ItemLootCallback = std::function; void setItemLootCallback(ItemLootCallback cb) { itemLootCallback_ = std::move(cb); } + // Loot window open/close callback (for loot kneel animation) + using LootWindowCallback = std::function; + void setLootWindowCallback(LootWindowCallback cb) { lootWindowCallback_ = std::move(cb); } + // Quest turn-in completion callback using QuestCompleteCallback = std::function; 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_; diff --git a/include/game/inventory.hpp b/include/game/inventory.hpp index fd64aa24..4a7a9fa1 100644 --- a/include/game/inventory.hpp +++ b/include/game/inventory.hpp @@ -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; diff --git a/include/game/spell_defines.hpp b/include/game/spell_defines.hpp index c6fe3663..8b431532 100644 --- a/include/game/spell_defines.hpp +++ b/include/game/spell_defines.hpp @@ -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 diff --git a/include/game/update_field_table.hpp b/include/game/update_field_table.hpp index d48065e4..c9d63e99 100644 --- a/include/game/update_field_table.hpp +++ b/include/game/update_field_table.hpp @@ -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, diff --git a/include/rendering/animation_controller.hpp b/include/rendering/animation_controller.hpp index 81166d12..5602e1e6 100644 --- a/include/rendering/animation_controller.hpp +++ b/include/rendering/animation_controller.hpp @@ -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 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); diff --git a/include/rendering/animation_ids.hpp b/include/rendering/animation_ids.hpp new file mode 100644 index 00000000..03836afc --- /dev/null +++ b/include/rendering/animation_ids.hpp @@ -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 +#include + +namespace wowee { +namespace pipeline { class DBCFile; } +namespace rendering { +namespace anim { + +// ============================================================================ +// Classic (Vanilla WoW 1.x) — Core character & creature animations +// IDs 0–145 +// ============================================================================ + +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 146–199 +// ============================================================================ + +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 (0–451 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& dbc); + +} // namespace anim +} // namespace rendering +} // namespace wowee diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index 3e2e46f9..019ce77e 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -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; } diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 0acd9972..45900e8b 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -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& 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) diff --git a/include/rendering/render_graph.hpp b/include/rendering/render_graph.hpp index 39ea34bd..6b17f5b1 100644 --- a/include/rendering/render_graph.hpp +++ b/include/rendering/render_graph.hpp @@ -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. diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index a4d075e9..a8c3520d 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -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_; void buildFrameGraph(game::GameHandler* gameHandler); diff --git a/include/rendering/terrain_renderer.hpp b/include/rendering/terrain_renderer.hpp index 24fa1955..56d235db 100644 --- a/include/rendering/terrain_renderer.hpp +++ b/include/rendering/terrain_renderer.hpp @@ -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; diff --git a/include/ui/auth_screen.hpp b/include/ui/auth_screen.hpp index ff99d963..e02fefe2 100644 --- a/include/ui/auth_screen.hpp +++ b/include/ui/auth_screen.hpp @@ -131,6 +131,35 @@ private: std::vector 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 diff --git a/src/core/application.cpp b/src/core/application.cpp index d4befd39..41b639d0 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -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 #include #include @@ -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{}; 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(spawnPos.x), ", ", static_cast(spawnPos.y), ", ", diff --git a/src/core/entity_spawner.cpp b/src/core/entity_spawner.cpp index d8f28e11..c27574a2 100644 --- a/src/core/entity_spawner.cpp +++ b/src/core/entity_spawner.cpp @@ -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(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; diff --git a/src/core/world_loader.cpp b/src/core/world_loader.cpp index 4e967b18..cf7979e1 100644 --- a/src/core/world_loader.cpp +++ b/src/core/world_loader.cpp @@ -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); } }); } diff --git a/src/game/combat_handler.cpp b/src/game/combat_handler.cpp index a32f1d47..3e6f8497 100644 --- a/src/game/combat_handler.cpp +++ b/src/game/combat_handler.cpp @@ -123,6 +123,9 @@ void CombatHandler::registerOpcodes(DispatchTable& table) { addCombatText(CombatTextEntry::ABSORB, static_cast(envAbs), 0, false, 0, 0, victimGuid); if (envRes > 0) addCombatText(CombatTextEntry::RESIST, static_cast(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( std::chrono::duration_cast( 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(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) { diff --git a/src/game/entity_controller.cpp b/src/game/entity_controller.cpp index f788fca3..05310082 100644 --- a/src/game/entity_controller.cpp +++ b/src/game/entity_controller.cpp @@ -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((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(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(itB->second & 0xFF); + if (owner_.gameObjectStateCallback_) + owner_.gameObjectStateCallback_(block.guid, goState); + } + } } // ============================================================ diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index f955cca9..ff1e34d1 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -31,6 +31,7 @@ #include "pipeline/asset_manager.hpp" #include "pipeline/dbc_loader.hpp" #include "core/logger.hpp" +#include "rendering/animation_ids.hpp" #include #include #include @@ -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::steady_clock::now() - t0).count(); LOG_INFO("DBC cache pre-load complete in ", elapsed, " ms"); diff --git a/src/game/inventory_handler.cpp b/src/game/inventory_handler.cpp index d81f444d..ff13788a 100644 --- a/src/game/inventory_handler.cpp +++ b/src/game/inventory_handler.cpp @@ -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{}; } diff --git a/src/game/spell_handler.cpp b/src/game/spell_handler.cpp index 47346c39..119464ae 100644 --- a/src/game/spell_handler.cpp +++ b/src/game/spell_handler.cpp @@ -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 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}); diff --git a/src/game/update_field_table.cpp b/src/game/update_field_table.cpp index 85ac0458..d4f2c724 100644 --- a/src/game/update_field_table.cpp +++ b/src/game/update_field_table.cpp @@ -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}, diff --git a/src/pipeline/dbc_loader.cpp b/src/pipeline/dbc_loader.cpp index d5ea4938..bb35ba44 100644 --- a/src/pipeline/dbc_loader.cpp +++ b/src/pipeline/dbc_loader.cpp @@ -79,7 +79,9 @@ bool DBCFile::load(const std::vector& 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; diff --git a/src/rendering/animation_controller.cpp b/src/rendering/animation_controller.cpp index 7b46614e..eb459c04 100644 --- a/src/rendering/animation_controller.cpp +++ b/src/rendering/animation_controller.cpp @@ -1,4 +1,5 @@ #include "rendering/animation_controller.hpp" +#include "rendering/animation_ids.hpp" #include "rendering/renderer.hpp" #include "rendering/camera.hpp" #include "rendering/camera_controller.hpp" @@ -14,6 +15,7 @@ #include "pipeline/asset_manager.hpp" #include "pipeline/dbc_loader.hpp" #include "pipeline/dbc_layout.hpp" +#include "game/inventory.hpp" #include "core/application.hpp" #include "core/logger.hpp" #include "audio/audio_coordinator.hpp" @@ -71,36 +73,57 @@ static std::vector parseEmoteCommands(const std::string& raw) { static bool isLoopingEmote(const std::string& command) { static const std::unordered_set kLooping = { - "dance", - "train", + "dance", "train", "dead", "eat", "work", }; return kLooping.find(command) != kLooping.end(); } +// Map one-shot emote animation IDs to their persistent EMOTE_STATE_* looping variants. +// When a looping emote is played, we prefer the STATE variant if the model has it. +static uint32_t getEmoteStateVariant(uint32_t oneShotAnimId) { + static const std::unordered_map kStateMap = { + {anim::EMOTE_DANCE, anim::EMOTE_STATE_DANCE}, + {anim::EMOTE_LAUGH, anim::EMOTE_STATE_LAUGH}, + {anim::EMOTE_POINT, anim::EMOTE_STATE_POINT}, + {anim::EMOTE_EAT, anim::EMOTE_STATE_EAT}, + {anim::EMOTE_ROAR, anim::EMOTE_STATE_ROAR}, + {anim::EMOTE_APPLAUD, anim::EMOTE_STATE_APPLAUD}, + {anim::EMOTE_WORK, anim::EMOTE_STATE_WORK}, + {anim::EMOTE_USE_STANDING, anim::EMOTE_STATE_USE_STANDING}, + {anim::EATING_LOOP, anim::EMOTE_STATE_EAT}, + }; + auto it = kStateMap.find(oneShotAnimId); + return it != kStateMap.end() ? it->second : 0; +} + static void loadFallbackEmotes() { if (!EMOTE_TABLE.empty()) return; EMOTE_TABLE = { - {"wave", {67, 0, false, "You wave.", "You wave at %s.", "%s waves.", "%s waves at %s.", "wave"}}, - {"bow", {66, 0, false, "You bow down graciously.", "You bow down before %s.", "%s bows down graciously.", "%s bows down before %s.", "bow"}}, - {"laugh", {70, 0, false, "You laugh.", "You laugh at %s.", "%s laughs.", "%s laughs at %s.", "laugh"}}, - {"point", {84, 0, false, "You point over yonder.", "You point at %s.", "%s points over yonder.", "%s points at %s.", "point"}}, - {"cheer", {68, 0, false, "You cheer!", "You cheer at %s.", "%s cheers!", "%s cheers at %s.", "cheer"}}, - {"dance", {69, 0, true, "You burst into dance.", "You dance with %s.", "%s bursts into dance.", "%s dances with %s.", "dance"}}, - {"kneel", {75, 0, false, "You kneel down.", "You kneel before %s.", "%s kneels down.", "%s kneels before %s.", "kneel"}}, - {"applaud", {80, 0, false, "You applaud. Bravo!", "You applaud at %s. Bravo!", "%s applauds. Bravo!", "%s applauds at %s. Bravo!", "applaud"}}, - {"shout", {81, 0, false, "You shout.", "You shout at %s.", "%s shouts.", "%s shouts at %s.", "shout"}}, - {"chicken", {78, 0, false, "With arms flapping, you strut around. Cluck, Cluck, Chicken!", + {"wave", {anim::EMOTE_WAVE, 0, false, "You wave.", "You wave at %s.", "%s waves.", "%s waves at %s.", "wave"}}, + {"bow", {anim::EMOTE_BOW, 0, false, "You bow down graciously.", "You bow down before %s.", "%s bows down graciously.", "%s bows down before %s.", "bow"}}, + {"laugh", {anim::EMOTE_LAUGH, 0, false, "You laugh.", "You laugh at %s.", "%s laughs.", "%s laughs at %s.", "laugh"}}, + {"point", {anim::EMOTE_POINT, 0, false, "You point over yonder.", "You point at %s.", "%s points over yonder.", "%s points at %s.", "point"}}, + {"cheer", {anim::EMOTE_CHEER, 0, false, "You cheer!", "You cheer at %s.", "%s cheers!", "%s cheers at %s.", "cheer"}}, + {"dance", {anim::EMOTE_DANCE, 0, true, "You burst into dance.", "You dance with %s.", "%s bursts into dance.", "%s dances with %s.", "dance"}}, + {"kneel", {anim::EMOTE_KNEEL, 0, false, "You kneel down.", "You kneel before %s.", "%s kneels down.", "%s kneels before %s.", "kneel"}}, + {"applaud", {anim::EMOTE_APPLAUD, 0, false, "You applaud. Bravo!", "You applaud at %s. Bravo!", "%s applauds. Bravo!", "%s applauds at %s. Bravo!", "applaud"}}, + {"shout", {anim::EMOTE_SHOUT, 0, false, "You shout.", "You shout at %s.", "%s shouts.", "%s shouts at %s.", "shout"}}, + {"chicken", {anim::EMOTE_CHICKEN, 0, false, "With arms flapping, you strut around. Cluck, Cluck, Chicken!", "With arms flapping, you strut around %s. Cluck, Cluck, Chicken!", "%s struts around. Cluck, Cluck, Chicken!", "%s struts around %s. Cluck, Cluck, Chicken!", "chicken"}}, - {"cry", {77, 0, false, "You cry.", "You cry on %s's shoulder.", "%s cries.", "%s cries on %s's shoulder.", "cry"}}, - {"kiss", {76, 0, false, "You blow a kiss into the wind.", "You blow a kiss to %s.", "%s blows a kiss into the wind.", "%s blows a kiss to %s.", "kiss"}}, - {"roar", {74, 0, false, "You roar with bestial vigor. So fierce!", "You roar with bestial vigor at %s. So fierce!", "%s roars with bestial vigor. So fierce!", "%s roars with bestial vigor at %s. So fierce!", "roar"}}, - {"salute", {113, 0, false, "You salute.", "You salute %s with respect.", "%s salutes.", "%s salutes %s with respect.", "salute"}}, - {"rude", {73, 0, false, "You make a rude gesture.", "You make a rude gesture at %s.", "%s makes a rude gesture.", "%s makes a rude gesture at %s.", "rude"}}, - {"flex", {82, 0, false, "You flex your muscles. Oooooh so strong!", "You flex at %s. Oooooh so strong!", "%s flexes. Oooooh so strong!", "%s flexes at %s. Oooooh so strong!", "flex"}}, - {"shy", {83, 0, false, "You smile shyly.", "You smile shyly at %s.", "%s smiles shyly.", "%s smiles shyly at %s.", "shy"}}, - {"beg", {79, 0, false, "You beg everyone around you. How pathetic.", "You beg %s. How pathetic.", "%s begs everyone around. How pathetic.", "%s begs %s. How pathetic.", "beg"}}, - {"eat", {61, 0, false, "You begin to eat.", "You begin to eat in front of %s.", "%s begins to eat.", "%s begins to eat in front of %s.", "eat"}}, + {"cry", {anim::EMOTE_CRY, 0, false, "You cry.", "You cry on %s's shoulder.", "%s cries.", "%s cries on %s's shoulder.", "cry"}}, + {"kiss", {anim::EMOTE_KISS, 0, false, "You blow a kiss into the wind.", "You blow a kiss to %s.", "%s blows a kiss into the wind.", "%s blows a kiss to %s.", "kiss"}}, + {"roar", {anim::EMOTE_ROAR, 0, false, "You roar with bestial vigor. So fierce!", "You roar with bestial vigor at %s. So fierce!", "%s roars with bestial vigor. So fierce!", "%s roars with bestial vigor at %s. So fierce!", "roar"}}, + {"salute", {anim::EMOTE_SALUTE, 0, false, "You salute.", "You salute %s with respect.", "%s salutes.", "%s salutes %s with respect.", "salute"}}, + {"rude", {anim::EMOTE_RUDE, 0, false, "You make a rude gesture.", "You make a rude gesture at %s.", "%s makes a rude gesture.", "%s makes a rude gesture at %s.", "rude"}}, + {"flex", {anim::EMOTE_FLEX, 0, false, "You flex your muscles. Oooooh so strong!", "You flex at %s. Oooooh so strong!", "%s flexes. Oooooh so strong!", "%s flexes at %s. Oooooh so strong!", "flex"}}, + {"shy", {anim::EMOTE_SHY, 0, false, "You smile shyly.", "You smile shyly at %s.", "%s smiles shyly.", "%s smiles shyly at %s.", "shy"}}, + {"beg", {anim::EMOTE_BEG, 0, false, "You beg everyone around you. How pathetic.", "You beg %s. How pathetic.", "%s begs everyone around. How pathetic.", "%s begs %s. How pathetic.", "beg"}}, + {"eat", {anim::EMOTE_EAT, 0, true, "You begin to eat.", "You begin to eat in front of %s.", "%s begins to eat.", "%s begins to eat in front of %s.", "eat"}}, + {"talk", {anim::EMOTE_TALK, 0, false, "You talk.", "You talk to %s.", "%s talks.", "%s talks to %s.", "talk"}}, + {"work", {anim::EMOTE_WORK, 0, true, "You begin to work.", "You begin to work near %s.", "%s begins to work.", "%s begins to work near %s.", "work"}}, + {"train", {anim::EMOTE_TRAIN, 0, true, "You let off a train whistle. Choo Choo!", "You let off a train whistle at %s. Choo Choo!", "%s lets off a train whistle. Choo Choo!", "%s lets off a train whistle at %s. Choo Choo!", "train"}}, + {"dead", {anim::EMOTE_DEAD, 0, true, "You play dead.", "You play dead in front of %s.", "%s plays dead.", "%s plays dead in front of %s.", "dead"}}, }; } @@ -243,6 +266,20 @@ void AnimationController::playEmote(const std::string& emoteName) { emoteActive_ = true; emoteAnimId_ = info.animId; emoteLoop_ = info.loop; + + // For looping emotes, prefer the EMOTE_STATE_* variant if the model has it + if (emoteLoop_) { + uint32_t stateVariant = getEmoteStateVariant(emoteAnimId_); + if (stateVariant != 0) { + auto* characterRenderer = renderer_->getCharacterRenderer(); + uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); + if (characterRenderer && characterInstanceId > 0 && + characterRenderer->hasAnimation(characterInstanceId, stateVariant)) { + emoteAnimId_ = stateVariant; + } + } + } + charAnimState_ = CharAnimState::EMOTE; auto* characterRenderer = renderer_->getCharacterRenderer(); @@ -258,6 +295,165 @@ void AnimationController::cancelEmote() { emoteLoop_ = false; } +void AnimationController::startSpellCast(uint32_t precastAnimId, uint32_t castAnimId, bool castLoop, + uint32_t finalizeAnimId) { + spellPrecastAnimId_ = precastAnimId; + spellCastAnimId_ = castAnimId; + spellCastLoop_ = castLoop; + spellFinalizeAnimId_ = finalizeAnimId; + + // Start with precast phase if available, otherwise go straight to cast + if (spellPrecastAnimId_ != 0) { + charAnimState_ = CharAnimState::SPELL_PRECAST; + } else { + charAnimState_ = CharAnimState::SPELL_CASTING; + } + // Force immediate animation update by invalidating the last request + lastPlayerAnimRequest_ = UINT32_MAX; +} + +void AnimationController::stopSpellCast() { + if (charAnimState_ != CharAnimState::SPELL_PRECAST && + charAnimState_ != CharAnimState::SPELL_CASTING) return; + + if (spellFinalizeAnimId_ != 0) { + // Transition to finalization phase — one-shot release animation + charAnimState_ = CharAnimState::SPELL_FINALIZE; + lastPlayerAnimRequest_ = UINT32_MAX; + } else if (spellCastLoop_) { + // No finalize anim — let current cast cycle finish as one-shot + spellCastLoop_ = false; + charAnimState_ = CharAnimState::SPELL_FINALIZE; + lastPlayerAnimRequest_ = UINT32_MAX; + } else { + // Instant cast (no finalize, no loop) — wait for completion in current state + charAnimState_ = CharAnimState::SPELL_FINALIZE; + lastPlayerAnimRequest_ = UINT32_MAX; + } +} + +void AnimationController::startLooting() { + // Don't override jump, swim, stun, or death states + if (charAnimState_ == CharAnimState::JUMP_START || + charAnimState_ == CharAnimState::JUMP_MID || + charAnimState_ == CharAnimState::JUMP_END || + charAnimState_ == CharAnimState::SWIM || + charAnimState_ == CharAnimState::SWIM_IDLE || + charAnimState_ == CharAnimState::STUNNED) return; + charAnimState_ = CharAnimState::LOOTING; + lastPlayerAnimRequest_ = UINT32_MAX; +} + +void AnimationController::stopLooting() { + if (charAnimState_ != CharAnimState::LOOTING) return; + charAnimState_ = CharAnimState::IDLE; + lastPlayerAnimRequest_ = UINT32_MAX; +} + +void AnimationController::triggerHitReaction(uint32_t animId) { + // Hit reactions interrupt spell casting but not jumps/swimming/stun + if (charAnimState_ == CharAnimState::JUMP_START || + charAnimState_ == CharAnimState::JUMP_MID || + charAnimState_ == CharAnimState::JUMP_END || + charAnimState_ == CharAnimState::SWIM || + charAnimState_ == CharAnimState::SWIM_IDLE || + charAnimState_ == CharAnimState::STUNNED) return; + if (charAnimState_ == CharAnimState::SPELL_CASTING || + charAnimState_ == CharAnimState::SPELL_PRECAST || + charAnimState_ == CharAnimState::SPELL_FINALIZE) { + spellPrecastAnimId_ = 0; + spellCastAnimId_ = 0; + spellCastLoop_ = false; + spellFinalizeAnimId_ = 0; + } + hitReactionAnimId_ = animId; + charAnimState_ = CharAnimState::HIT_REACTION; + lastPlayerAnimRequest_ = UINT32_MAX; +} + +void AnimationController::setStunned(bool stunned) { + stunned_ = stunned; + if (stunned) { + // Stun overrides most states (not swimming/jumping — those are physics) + if (charAnimState_ == CharAnimState::SWIM || + charAnimState_ == CharAnimState::SWIM_IDLE) return; + // Interrupt spell casting + if (charAnimState_ == CharAnimState::SPELL_CASTING || + charAnimState_ == CharAnimState::SPELL_PRECAST || + charAnimState_ == CharAnimState::SPELL_FINALIZE) { + spellPrecastAnimId_ = 0; + spellCastAnimId_ = 0; + spellCastLoop_ = false; + spellFinalizeAnimId_ = 0; + } + hitReactionAnimId_ = 0; + charAnimState_ = CharAnimState::STUNNED; + lastPlayerAnimRequest_ = UINT32_MAX; + } else { + if (charAnimState_ == CharAnimState::STUNNED) { + charAnimState_ = inCombat_ ? CharAnimState::COMBAT_IDLE : CharAnimState::IDLE; + lastPlayerAnimRequest_ = UINT32_MAX; + } + } +} + +void AnimationController::setStandState(uint8_t state) { + if (state == standState_) return; + standState_ = state; + + if (state == STAND_STATE_STAND) { + // Standing up — exit animation handled by state machine (!sitting → SIT_UP) + // sitUpAnim_ is retained from the entry so the correct exit animation plays. + return; + } + + // Configure transition/loop/exit animations per stand-state type + if (state == STAND_STATE_SIT) { + // Ground sit + sitDownAnim_ = anim::SIT_GROUND_DOWN; + sitLoopAnim_ = anim::SITTING; + sitUpAnim_ = anim::SIT_GROUND_UP; + charAnimState_ = CharAnimState::SIT_DOWN; + } else if (state == STAND_STATE_SLEEP) { + // Sleep + sitDownAnim_ = anim::SLEEP_DOWN; + sitLoopAnim_ = anim::SLEEP; + sitUpAnim_ = anim::SLEEP_UP; + charAnimState_ = CharAnimState::SIT_DOWN; + } else if (state == STAND_STATE_KNEEL) { + // Kneel + sitDownAnim_ = anim::KNEEL_START; + sitLoopAnim_ = anim::KNEEL_LOOP; + sitUpAnim_ = anim::KNEEL_END; + charAnimState_ = CharAnimState::SIT_DOWN; + } else if (state >= STAND_STATE_SIT_CHAIR && state <= STAND_STATE_SIT_HIGH) { + // Chair variants — no transition animation, go directly to loop + sitDownAnim_ = 0; + sitUpAnim_ = 0; + if (state == STAND_STATE_SIT_LOW) { + sitLoopAnim_ = anim::SIT_CHAIR_LOW; + } else if (state == STAND_STATE_SIT_HIGH) { + sitLoopAnim_ = anim::SIT_CHAIR_HIGH; + } else { + sitLoopAnim_ = anim::SIT_CHAIR_MED; + } + charAnimState_ = CharAnimState::SITTING; + } else if (state == STAND_STATE_DEAD) { + // Dead — leave to death handling elsewhere + sitDownAnim_ = 0; + sitLoopAnim_ = 0; + sitUpAnim_ = 0; + return; + } + lastPlayerAnimRequest_ = UINT32_MAX; +} + +void AnimationController::setStealthed(bool stealth) { + if (stealthed_ == stealth) return; + stealthed_ = stealth; + lastPlayerAnimRequest_ = UINT32_MAX; +} + std::string AnimationController::getEmoteText(const std::string& emoteName, const std::string* targetName) { loadEmotesFromDbc(); auto it = EMOTE_TABLE.find(emoteName); @@ -337,6 +533,23 @@ void AnimationController::resetCombatVisualState() { targetPosition_ = nullptr; meleeSwingTimer_ = 0.0f; meleeSwingCooldown_ = 0.0f; + specialAttackAnimId_ = 0; + rangedShootTimer_ = 0.0f; + rangedAnimId_ = 0; + spellPrecastAnimId_ = 0; + spellCastAnimId_ = 0; + spellCastLoop_ = false; + spellFinalizeAnimId_ = 0; + hitReactionAnimId_ = 0; + stunned_ = false; + lowHealth_ = false; + if (charAnimState_ == CharAnimState::SPELL_CASTING || + charAnimState_ == CharAnimState::SPELL_PRECAST || + charAnimState_ == CharAnimState::SPELL_FINALIZE || + charAnimState_ == CharAnimState::HIT_REACTION || + charAnimState_ == CharAnimState::STUNNED || + charAnimState_ == CharAnimState::RANGED_SHOOT) + charAnimState_ = CharAnimState::IDLE; if (auto* svs = renderer_->getSpellVisualSystem()) svs->reset(); } @@ -355,6 +568,7 @@ void AnimationController::triggerMeleeSwing() { if (emoteActive_) { cancelEmote(); } + specialAttackAnimId_ = 0; // Clear any special attack override resolveMeleeAnimId(); meleeSwingCooldown_ = 0.1f; float durationSec = meleeAnimDurationMs_ > 0.0f ? meleeAnimDurationMs_ / 1000.0f : 0.6f; @@ -366,6 +580,108 @@ void AnimationController::triggerMeleeSwing() { } } +void AnimationController::triggerSpecialAttack(uint32_t /*spellId*/) { + auto* characterRenderer = renderer_->getCharacterRenderer(); + uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); + if (!characterRenderer || characterInstanceId == 0) return; + if (meleeSwingCooldown_ > 0.0f) return; + if (emoteActive_) { + cancelEmote(); + } + + auto has = [&](uint32_t id) { return characterRenderer->hasAnimation(characterInstanceId, id); }; + + // Choose special attack animation based on equipped weapon type + uint32_t specAnim = 0; + if (equippedHasShield_ && has(anim::SHIELD_BASH)) { + specAnim = anim::SHIELD_BASH; + } else if ((equippedWeaponInvType_ == game::InvType::TWO_HAND || equippedIs2HLoose_) && has(anim::SPECIAL_2H)) { + specAnim = anim::SPECIAL_2H; + } else if (equippedWeaponInvType_ != game::InvType::NON_EQUIP && has(anim::SPECIAL_1H)) { + specAnim = anim::SPECIAL_1H; + } else if (has(anim::SPECIAL_UNARMED)) { + specAnim = anim::SPECIAL_UNARMED; + } else if (has(anim::SPECIAL_1H)) { + specAnim = anim::SPECIAL_1H; + } + + if (specAnim == 0) { + // No special animation available — fall back to regular melee swing + triggerMeleeSwing(); + return; + } + + specialAttackAnimId_ = specAnim; + meleeSwingCooldown_ = 0.1f; + // Query the special attack animation duration + std::vector sequences; + float dur = 0.6f; + if (characterRenderer->getAnimationSequences(characterInstanceId, sequences)) { + for (const auto& seq : sequences) { + if (seq.id == specAnim && seq.duration > 0) { + dur = static_cast(seq.duration) / 1000.0f; + break; + } + } + } + if (dur < 0.25f) dur = 0.25f; + if (dur > 1.0f) dur = 1.0f; + meleeSwingTimer_ = dur; + if (renderer_->getAudioCoordinator()->getActivitySoundManager()) { + renderer_->getAudioCoordinator()->getActivitySoundManager()->playMeleeSwing(); + } +} + +// ── Ranged combat ──────────────────────────────────────────────────────────── + +void AnimationController::triggerRangedShot() { + auto* characterRenderer = renderer_->getCharacterRenderer(); + uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); + if (!characterRenderer || characterInstanceId == 0) return; + if (rangedShootTimer_ > 0.0f) return; + if (emoteActive_) cancelEmote(); + + auto has = [&](uint32_t id) { return characterRenderer->hasAnimation(characterInstanceId, id); }; + + // Resolve ranged attack animation based on weapon type + uint32_t shootAnim = 0; + switch (equippedRangedType_) { + case RangedWeaponType::BOW: + if (has(anim::FIRE_BOW)) shootAnim = anim::FIRE_BOW; + else if (has(anim::ATTACK_BOW)) shootAnim = anim::ATTACK_BOW; + break; + case RangedWeaponType::GUN: + if (has(anim::ATTACK_RIFLE)) shootAnim = anim::ATTACK_RIFLE; + break; + case RangedWeaponType::CROSSBOW: + if (has(anim::ATTACK_CROSSBOW)) shootAnim = anim::ATTACK_CROSSBOW; + else if (has(anim::ATTACK_BOW)) shootAnim = anim::ATTACK_BOW; + break; + case RangedWeaponType::THROWN: + if (has(anim::ATTACK_THROWN)) shootAnim = anim::ATTACK_THROWN; + break; + default: break; + } + if (shootAnim == 0) return; // Model has no ranged animation + + rangedAnimId_ = shootAnim; + + // Query animation duration + std::vector sequences; + float dur = 0.6f; + if (characterRenderer->getAnimationSequences(characterInstanceId, sequences)) { + for (const auto& seq : sequences) { + if (seq.id == shootAnim && seq.duration > 0) { + dur = static_cast(seq.duration) / 1000.0f; + break; + } + } + } + if (dur < 0.25f) dur = 0.25f; + if (dur > 1.5f) dur = 1.5f; + rangedShootTimer_ = dur; +} + uint32_t AnimationController::resolveMeleeAnimId() { auto* characterRenderer = renderer_->getCharacterRenderer(); uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); @@ -375,7 +691,8 @@ uint32_t AnimationController::resolveMeleeAnimId() { return 0; } - if (meleeAnimId_ != 0 && characterRenderer->hasAnimation(characterInstanceId, meleeAnimId_)) { + // When dual-wielding, bypass cache to alternate main/off-hand animations + if (!equippedHasOffHand_ && meleeAnimId_ != 0 && characterRenderer->hasAnimation(characterInstanceId, meleeAnimId_)) { return meleeAnimId_; } @@ -397,13 +714,50 @@ uint32_t AnimationController::resolveMeleeAnimId() { const uint32_t* attackCandidates; size_t candidateCount; - static const uint32_t candidates2H[] = {18, 17, 16, 19, 20, 21}; - static const uint32_t candidates1H[] = {17, 18, 16, 19, 20, 21}; - static const uint32_t candidatesUnarmed[] = {16, 17, 18, 19, 20, 21}; - if (equippedWeaponInvType_ == 17) { + static const uint32_t candidates2H[] = {anim::ATTACK_2H, anim::ATTACK_1H, anim::ATTACK_UNARMED, anim::ATTACK_2H_LOOSE, anim::PARRY_UNARMED, anim::PARRY_1H}; + static const uint32_t candidates2HLoosePierce[] = {anim::ATTACK_2H_LOOSE_PIERCE, anim::ATTACK_2H_LOOSE, anim::ATTACK_2H, anim::ATTACK_1H, anim::ATTACK_UNARMED}; + static const uint32_t candidates1H[] = {anim::ATTACK_1H, anim::ATTACK_2H, anim::ATTACK_UNARMED, anim::ATTACK_2H_LOOSE, anim::PARRY_UNARMED, anim::PARRY_1H}; + static const uint32_t candidatesDagger[] = {anim::ATTACK_1H_PIERCE, anim::ATTACK_1H, anim::ATTACK_UNARMED}; + static const uint32_t candidatesUnarmed[] = {anim::ATTACK_UNARMED, anim::ATTACK_1H, anim::ATTACK_2H, anim::ATTACK_2H_LOOSE, anim::PARRY_UNARMED, anim::PARRY_1H}; + static const uint32_t candidatesFist[] = {anim::ATTACK_FIST_1H, anim::ATTACK_FIST_1H_OFF, anim::ATTACK_1H, anim::ATTACK_UNARMED, anim::PARRY_FIST_1H, anim::PARRY_1H}; + // Off-hand attack variants (used when dual-wielding on off-hand turn) + static const uint32_t candidatesOffHand[] = {anim::ATTACK_OFF, anim::ATTACK_1H, anim::ATTACK_UNARMED}; + static const uint32_t candidatesOffHandPierce[] = {anim::ATTACK_OFF_PIERCE, anim::ATTACK_OFF, anim::ATTACK_1H_PIERCE, anim::ATTACK_1H}; + static const uint32_t candidatesOffHandFist[] = {anim::ATTACK_FIST_1H_OFF, anim::ATTACK_OFF, anim::ATTACK_FIST_1H, anim::ATTACK_1H}; + static const uint32_t candidatesOffHandUnarmed[] = {anim::ATTACK_UNARMED_OFF, anim::ATTACK_UNARMED, anim::ATTACK_OFF, anim::ATTACK_1H}; + + // Dual-wield: alternate main-hand and off-hand swings + bool useOffHand = equippedHasOffHand_ && meleeOffHandTurn_; + meleeOffHandTurn_ = equippedHasOffHand_ ? !meleeOffHandTurn_ : false; + + if (useOffHand) { + if (equippedIsFist_) { + attackCandidates = candidatesOffHandFist; + candidateCount = 4; + } else if (equippedIsDagger_) { + attackCandidates = candidatesOffHandPierce; + candidateCount = 4; + } else if (equippedWeaponInvType_ == game::InvType::NON_EQUIP) { + attackCandidates = candidatesOffHandUnarmed; + candidateCount = 4; + } else { + attackCandidates = candidatesOffHand; + candidateCount = 3; + } + } else if (equippedIsFist_) { + attackCandidates = candidatesFist; + candidateCount = 6; + } else if (equippedIsDagger_) { + attackCandidates = candidatesDagger; + candidateCount = 3; + } else if (equippedIs2HLoose_) { + // Polearm thrust uses pierce variant + attackCandidates = candidates2HLoosePierce; + candidateCount = 5; + } else if (equippedWeaponInvType_ == game::InvType::TWO_HAND) { attackCandidates = candidates2H; candidateCount = 6; - } else if (equippedWeaponInvType_ == 0) { + } else if (equippedWeaponInvType_ == game::InvType::NON_EQUIP) { attackCandidates = candidatesUnarmed; candidateCount = 6; } else { @@ -419,7 +773,7 @@ uint32_t AnimationController::resolveMeleeAnimId() { } } - const uint32_t avoidIds[] = {0, 1, 4, 5, 11, 12, 13, 37, 38, 39, 41, 42, 97}; + const uint32_t avoidIds[] = {anim::STAND, anim::DEATH, anim::WALK, anim::RUN, anim::SHUFFLE_LEFT, anim::SHUFFLE_RIGHT, anim::WALK_BACKWARDS, anim::JUMP_START, anim::JUMP, anim::JUMP_END, anim::SWIM_IDLE, anim::SWIM, anim::SITTING}; auto isAvoid = [&](uint32_t id) -> bool { for (uint32_t avoid : avoidIds) { if (id == avoid) return true; @@ -587,8 +941,8 @@ void AnimationController::setMounted(uint32_t mountInstId, uint32_t mountDisplay return nullptr; }; - uint32_t runId = findFirst({5, 4}); - uint32_t standId = findFirst({0}); + uint32_t runId = findFirst({anim::RUN, anim::WALK}); + uint32_t standId = findFirst({anim::STAND}); std::vector loops; for (const auto& seq : sequences) { @@ -657,12 +1011,20 @@ void AnimationController::setMounted(uint32_t mountInstId, uint32_t mountDisplay auto [discoveredStart, discoveredLoop, discoveredEnd] = discoverJumpSet(); - mountAnims_.jumpStart = discoveredStart > 0 ? discoveredStart : findFirst({40, 37}); - mountAnims_.jumpLoop = discoveredLoop > 0 ? discoveredLoop : findFirst({38}); - mountAnims_.jumpEnd = discoveredEnd > 0 ? discoveredEnd : findFirst({39}); - mountAnims_.rearUp = findFirst({94, 92, 40}); - mountAnims_.run = findFirst({5, 4}); - mountAnims_.stand = findFirst({0}); + mountAnims_.jumpStart = discoveredStart > 0 ? discoveredStart : findFirst({anim::FALL, anim::JUMP_START}); + mountAnims_.jumpLoop = discoveredLoop > 0 ? discoveredLoop : findFirst({anim::JUMP}); + mountAnims_.jumpEnd = discoveredEnd > 0 ? discoveredEnd : findFirst({anim::JUMP_END}); + mountAnims_.rearUp = findFirst({anim::MOUNT_SPECIAL, anim::RUN_RIGHT, anim::FALL}); + mountAnims_.run = findFirst({anim::RUN, anim::WALK}); + mountAnims_.stand = findFirst({anim::STAND}); + // Discover flight animations (flying mounts only — may all be 0 for ground mounts) + mountAnims_.flyIdle = findFirst({anim::FLY_IDLE}); + mountAnims_.flyForward = findFirst({anim::FLY_FORWARD, anim::FLY_RUN_2}); + mountAnims_.flyBackwards = findFirst({anim::FLY_BACKWARDS, anim::FLY_WALK_BACKWARDS}); + mountAnims_.flyLeft = findFirst({anim::FLY_LEFT, anim::FLY_SHUFFLE_LEFT}); + mountAnims_.flyRight = findFirst({anim::FLY_RIGHT, anim::FLY_SHUFFLE_RIGHT}); + mountAnims_.flyUp = findFirst({anim::FLY_UP, anim::FLY_RISE}); + mountAnims_.flyDown = findFirst({anim::FLY_DOWN}); // Discover idle fidget animations using proper WoW M2 metadata mountAnims_.fidgets.clear(); @@ -767,6 +1129,11 @@ void AnimationController::updateMeleeTimers(float deltaTime) { } if (meleeSwingTimer_ > 0.0f) { meleeSwingTimer_ = std::max(0.0f, meleeSwingTimer_ - deltaTime); + if (meleeSwingTimer_ <= 0.0f) specialAttackAnimId_ = 0; + } + // Ranged shot timer (same pattern as melee) + if (rangedShootTimer_ > 0.0f) { + rangedShootTimer_ = std::max(0.0f, rangedShootTimer_ - deltaTime); } } @@ -777,29 +1144,6 @@ void AnimationController::updateCharacterAnimation() { auto* cameraController = renderer_->getCameraController(); uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); - // WoW WotLK AnimationData.dbc IDs - constexpr uint32_t ANIM_STAND = 0; - constexpr uint32_t ANIM_WALK = 4; - constexpr uint32_t ANIM_RUN = 5; - constexpr uint32_t ANIM_STRAFE_RUN_RIGHT = 92; - constexpr uint32_t ANIM_STRAFE_RUN_LEFT = 93; - constexpr uint32_t ANIM_STRAFE_WALK_LEFT = 11; - constexpr uint32_t ANIM_STRAFE_WALK_RIGHT = 12; - constexpr uint32_t ANIM_BACKPEDAL = 13; - constexpr uint32_t ANIM_JUMP_START = 37; - constexpr uint32_t ANIM_JUMP_MID = 38; - constexpr uint32_t ANIM_JUMP_END = 39; - constexpr uint32_t ANIM_SIT_DOWN = 97; - constexpr uint32_t ANIM_SITTING = 97; - constexpr uint32_t ANIM_SWIM_IDLE = 41; - constexpr uint32_t ANIM_SWIM = 42; - constexpr uint32_t ANIM_MOUNT = 91; - constexpr uint32_t ANIM_READY_UNARMED = 22; - constexpr uint32_t ANIM_READY_1H = 23; - constexpr uint32_t ANIM_READY_2H = 24; - constexpr uint32_t ANIM_READY_2H_L = 25; - constexpr uint32_t ANIM_FLY_IDLE = 158; - constexpr uint32_t ANIM_FLY_FORWARD = 159; CharAnimState newState = charAnimState_; @@ -827,6 +1171,7 @@ void AnimationController::updateCharacterAnimation() { bool sitting = cameraController->isSitting(); bool swim = cameraController->isSwimming(); bool forceMelee = meleeSwingTimer_ > 0.0f && grounded && !swim; + bool forceRanged = rangedShootTimer_ > 0.0f && grounded && !swim; const glm::vec3& characterPosition = renderer_->getCharacterPosition(); float characterYaw = renderer_->getCharacterYaw(); @@ -836,11 +1181,28 @@ void AnimationController::updateCharacterAnimation() { newState = CharAnimState::MOUNT; charAnimState_ = newState; + // Rider animation — defaults to MOUNT, but uses MOUNT_FLIGHT_* variants when flying + uint32_t riderAnim = anim::MOUNT; + if (cameraController->isFlyingActive()) { + auto hasRider = [&](uint32_t id) { return characterRenderer->hasAnimation(characterInstanceId, id); }; + if (moving) { + if (cameraController->isAscending() && hasRider(anim::MOUNT_FLIGHT_UP)) + riderAnim = anim::MOUNT_FLIGHT_UP; + else if (cameraController->isDescending() && hasRider(anim::MOUNT_FLIGHT_DOWN)) + riderAnim = anim::MOUNT_FLIGHT_DOWN; + else if (hasRider(anim::MOUNT_FLIGHT_FORWARD)) + riderAnim = anim::MOUNT_FLIGHT_FORWARD; + } else { + if (hasRider(anim::MOUNT_FLIGHT_IDLE)) + riderAnim = anim::MOUNT_FLIGHT_IDLE; + } + } + uint32_t currentAnimId = 0; float currentAnimTimeMs = 0.0f, currentAnimDurationMs = 0.0f; bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs); - if (!haveState || currentAnimId != ANIM_MOUNT) { - characterRenderer->playAnimation(characterInstanceId, ANIM_MOUNT, true); + if (!haveState || currentAnimId != riderAnim) { + characterRenderer->playAnimation(characterInstanceId, riderAnim, true); } float mountBob = 0.0f; @@ -872,7 +1234,7 @@ void AnimationController::updateCharacterAnimation() { return fallback; }; - uint32_t mountAnimId = ANIM_STAND; + uint32_t mountAnimId = anim::STAND; uint32_t curMountAnim = 0; float curMountTime = 0, curMountDur = 0; @@ -894,8 +1256,8 @@ void AnimationController::updateCharacterAnimation() { } } - uint32_t flyAnims[] = {ANIM_FLY_FORWARD, ANIM_FLY_IDLE, 234, 229, 233, 141, 369, 6, ANIM_RUN}; - mountAnimId = ANIM_STAND; + uint32_t flyAnims[] = {anim::FLY_FORWARD, anim::FLY_IDLE, anim::FLY_RUN_2, anim::FLY_SPELL, anim::FLY_RISE, anim::SPELL_KNEEL_LOOP, anim::FLY_CUSTOM_SPELL_10, anim::DEAD, anim::RUN}; + mountAnimId = anim::STAND; for (uint32_t fa : flyAnims) { if (characterRenderer->hasAnimation(mountInstanceId_, fa)) { mountAnimId = fa; @@ -988,14 +1350,54 @@ void AnimationController::updateCharacterAnimation() { } } } else if (moving) { - if (anyStrafeLeft) { - mountAnimId = pickMountAnim({ANIM_STRAFE_RUN_LEFT, ANIM_STRAFE_WALK_LEFT, ANIM_RUN}, ANIM_RUN); + const bool flying = cameraController->isFlyingActive(); + const bool mountSwim = cameraController->isSwimming(); + if (flying) { + // Directional flying animations for mount + if (cameraController->isAscending()) { + mountAnimId = pickMountAnim({anim::FLY_UP, anim::FLY_FORWARD}, anim::RUN); + } else if (cameraController->isDescending()) { + mountAnimId = pickMountAnim({anim::FLY_DOWN, anim::FLY_FORWARD}, anim::RUN); + } else if (anyStrafeLeft) { + mountAnimId = pickMountAnim({anim::FLY_LEFT, anim::FLY_SHUFFLE_LEFT, anim::FLY_FORWARD}, anim::RUN); + } else if (anyStrafeRight) { + mountAnimId = pickMountAnim({anim::FLY_RIGHT, anim::FLY_SHUFFLE_RIGHT, anim::FLY_FORWARD}, anim::RUN); + } else if (movingBackward) { + mountAnimId = pickMountAnim({anim::FLY_BACKWARDS, anim::FLY_WALK_BACKWARDS, anim::FLY_FORWARD}, anim::RUN); + } else { + mountAnimId = pickMountAnim({anim::FLY_FORWARD, anim::FLY_IDLE}, anim::RUN); + } + } else if (mountSwim) { + // Mounted swimming animations + if (anyStrafeLeft) { + mountAnimId = pickMountAnim({anim::MOUNT_SWIM_LEFT, anim::SWIM_LEFT, anim::MOUNT_SWIM}, anim::RUN); + } else if (anyStrafeRight) { + mountAnimId = pickMountAnim({anim::MOUNT_SWIM_RIGHT, anim::SWIM_RIGHT, anim::MOUNT_SWIM}, anim::RUN); + } else if (movingBackward) { + mountAnimId = pickMountAnim({anim::MOUNT_SWIM_BACKWARDS, anim::SWIM_BACKWARDS, anim::MOUNT_SWIM}, anim::RUN); + } else { + mountAnimId = pickMountAnim({anim::MOUNT_SWIM, anim::SWIM}, anim::RUN); + } + } else if (anyStrafeLeft) { + mountAnimId = pickMountAnim({anim::MOUNT_RUN_LEFT, anim::RUN_LEFT, anim::SHUFFLE_LEFT, anim::RUN}, anim::RUN); } else if (anyStrafeRight) { - mountAnimId = pickMountAnim({ANIM_STRAFE_RUN_RIGHT, ANIM_STRAFE_WALK_RIGHT, ANIM_RUN}, ANIM_RUN); + mountAnimId = pickMountAnim({anim::MOUNT_RUN_RIGHT, anim::RUN_RIGHT, anim::SHUFFLE_RIGHT, anim::RUN}, anim::RUN); } else if (movingBackward) { - mountAnimId = pickMountAnim({ANIM_BACKPEDAL}, ANIM_RUN); + mountAnimId = pickMountAnim({anim::MOUNT_WALK_BACKWARDS, anim::WALK_BACKWARDS}, anim::RUN); } else { - mountAnimId = ANIM_RUN; + mountAnimId = anim::RUN; + } + } else if (!moving && cameraController->isSwimming()) { + // Mounted swim idle + mountAnimId = pickMountAnim({anim::MOUNT_SWIM_IDLE, anim::SWIM_IDLE}, anim::STAND); + } else if (!moving && cameraController->isFlyingActive()) { + // Hovering in flight — use FLY_IDLE instead of STAND + if (cameraController->isAscending()) { + mountAnimId = pickMountAnim({anim::FLY_UP, anim::FLY_IDLE}, anim::STAND); + } else if (cameraController->isDescending()) { + mountAnimId = pickMountAnim({anim::FLY_DOWN, anim::FLY_IDLE}, anim::STAND); + } else { + mountAnimId = pickMountAnim({anim::FLY_IDLE, anim::FLY_FORWARD}, anim::STAND); } } @@ -1165,7 +1567,7 @@ void AnimationController::updateCharacterAnimation() { return; } - if (!forceMelee) switch (charAnimState_) { + if (!forceMelee && !forceRanged) switch (charAnimState_) { case CharAnimState::IDLE: if (swim) { newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; @@ -1180,7 +1582,13 @@ void AnimationController::updateCharacterAnimation() { } else if (moving) { newState = CharAnimState::WALK; } else if (inCombat_ && grounded) { - newState = CharAnimState::COMBAT_IDLE; + // Play unsheathe one-shot before entering combat idle + if (characterRenderer && characterInstanceId > 0 && + characterRenderer->hasAnimation(characterInstanceId, anim::UNSHEATHE)) { + newState = CharAnimState::UNSHEATHE; + } else { + newState = CharAnimState::COMBAT_IDLE; + } } break; @@ -1246,7 +1654,21 @@ void AnimationController::updateCharacterAnimation() { if (swim) { newState = CharAnimState::SWIM_IDLE; } else if (!sitting) { - newState = CharAnimState::IDLE; + // Stand up requested — play exit animation if available and not moving + if (sitUpAnim_ != 0 && !moving) { + newState = CharAnimState::SIT_UP; + } else { + newState = CharAnimState::IDLE; + } + } else if (sitDownAnim_ != 0 && characterRenderer && characterInstanceId > 0) { + // Auto-chain: when sit-down one-shot finishes → enter loop + uint32_t curId = 0; float curT = 0.0f, curDur = 0.0f; + if (characterRenderer->getAnimationState(characterInstanceId, curId, curT, curDur)) { + // Renderer auto-returns one-shots to STAND — detect that OR normal completion + if (curId != sitDownAnim_ || (curDur > 0.1f && curT >= curDur - 0.05f)) { + newState = CharAnimState::SITTING; + } + } } break; @@ -1254,7 +1676,29 @@ void AnimationController::updateCharacterAnimation() { if (swim) { newState = CharAnimState::SWIM_IDLE; } else if (!sitting) { - newState = CharAnimState::IDLE; + if (sitUpAnim_ != 0 && !moving) { + newState = CharAnimState::SIT_UP; + } else { + newState = CharAnimState::IDLE; + } + } + break; + + case CharAnimState::SIT_UP: + if (swim) { + newState = CharAnimState::SWIM_IDLE; + } else if (moving) { + // Movement cancels exit animation + newState = sprinting ? CharAnimState::RUN : CharAnimState::WALK; + } else if (characterRenderer && characterInstanceId > 0) { + uint32_t curId = 0; float curT = 0.0f, curDur = 0.0f; + if (characterRenderer->getAnimationState(characterInstanceId, curId, curT, curDur)) { + // Renderer auto-returns one-shots to STAND — detect that OR normal completion + if (curId != (sitUpAnim_ ? sitUpAnim_ : anim::SIT_GROUND_UP) + || (curDur > 0.1f && curT >= curDur - 0.05f)) { + newState = CharAnimState::IDLE; + } + } } break; @@ -1273,14 +1717,30 @@ void AnimationController::updateCharacterAnimation() { newState = CharAnimState::SIT_DOWN; } else if (!emoteLoop_ && characterRenderer && characterInstanceId > 0) { uint32_t curId = 0; float curT = 0.0f, curDur = 0.0f; - if (characterRenderer->getAnimationState(characterInstanceId, curId, curT, curDur) - && curDur > 0.1f && curT >= curDur - 0.05f) { - cancelEmote(); - newState = CharAnimState::IDLE; + if (characterRenderer->getAnimationState(characterInstanceId, curId, curT, curDur)) { + // Renderer auto-returns one-shots to STAND — detect that OR normal completion + if (curId != emoteAnimId_ || (curDur > 0.1f && curT >= curDur - 0.05f)) { + cancelEmote(); + newState = CharAnimState::IDLE; + } } } break; + case CharAnimState::LOOTING: + // Cancel loot animation on movement, jump, swim, combat + if (swim) { + stopLooting(); + newState = CharAnimState::SWIM_IDLE; + } else if (jumping || !grounded) { + stopLooting(); + newState = CharAnimState::JUMP_START; + } else if (moving) { + stopLooting(); + newState = sprinting ? CharAnimState::RUN : CharAnimState::WALK; + } + break; + case CharAnimState::SWIM_IDLE: if (!swim) { newState = moving ? CharAnimState::WALK : CharAnimState::IDLE; @@ -1317,6 +1777,42 @@ void AnimationController::updateCharacterAnimation() { } break; + case CharAnimState::RANGED_SHOOT: + if (swim) { + newState = CharAnimState::SWIM_IDLE; + } else if (!grounded && jumping) { + newState = CharAnimState::JUMP_START; + } else if (!grounded) { + newState = CharAnimState::JUMP_MID; + } else if (moving && sprinting) { + newState = CharAnimState::RUN; + } else if (moving) { + newState = CharAnimState::WALK; + } else if (inCombat_) { + newState = CharAnimState::RANGED_LOAD; + } else { + newState = CharAnimState::IDLE; + } + break; + + case CharAnimState::RANGED_LOAD: + if (swim) { + newState = CharAnimState::SWIM_IDLE; + } else if (!grounded && jumping) { + newState = CharAnimState::JUMP_START; + } else if (!grounded) { + newState = CharAnimState::JUMP_MID; + } else if (moving && sprinting) { + newState = CharAnimState::RUN; + } else if (moving) { + newState = CharAnimState::WALK; + } else if (inCombat_) { + newState = CharAnimState::COMBAT_IDLE; + } else { + newState = CharAnimState::IDLE; + } + break; + case CharAnimState::MOUNT: if (swim) { newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; @@ -1347,20 +1843,181 @@ void AnimationController::updateCharacterAnimation() { } else if (moving) { newState = CharAnimState::WALK; } else if (!inCombat_) { - newState = CharAnimState::IDLE; + // Play sheathe one-shot before returning to idle + if (characterRenderer && characterInstanceId > 0 && + characterRenderer->hasAnimation(characterInstanceId, anim::SHEATHE)) { + newState = CharAnimState::SHEATHE; + } else { + newState = CharAnimState::IDLE; + } } break; case CharAnimState::CHARGE: break; + + case CharAnimState::UNSHEATHE: + // One-shot weapon draw: when complete → COMBAT_IDLE + if (swim) { + newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; + } else if (moving) { + newState = inCombat_ ? (sprinting ? CharAnimState::RUN : CharAnimState::WALK) + : (sprinting ? CharAnimState::RUN : CharAnimState::WALK); + } else if (characterRenderer && characterInstanceId > 0) { + uint32_t curId = 0; float curT = 0.0f, curDur = 0.0f; + if (characterRenderer->getAnimationState(characterInstanceId, curId, curT, curDur)) { + if (curId != anim::UNSHEATHE || (curDur > 0.1f && curT >= curDur - 0.05f)) { + newState = CharAnimState::COMBAT_IDLE; + } + } + } + break; + + case CharAnimState::SHEATHE: + // One-shot weapon put-away: when complete → IDLE + if (swim) { + newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; + } else if (moving) { + newState = sprinting ? CharAnimState::RUN : CharAnimState::WALK; + } else if (inCombat_) { + // Re-entered combat during sheathe — go straight to combat idle + newState = CharAnimState::COMBAT_IDLE; + } else if (characterRenderer && characterInstanceId > 0) { + uint32_t curId = 0; float curT = 0.0f, curDur = 0.0f; + if (characterRenderer->getAnimationState(characterInstanceId, curId, curT, curDur)) { + if (curId != anim::SHEATHE || (curDur > 0.1f && curT >= curDur - 0.05f)) { + newState = CharAnimState::IDLE; + } + } + } + break; + + case CharAnimState::SPELL_PRECAST: + // One-shot wind-up: auto-advance to SPELL_CASTING when complete + if (swim) { + spellPrecastAnimId_ = 0; spellCastAnimId_ = 0; spellFinalizeAnimId_ = 0; + newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; + } else if (!grounded && jumping) { + spellPrecastAnimId_ = 0; spellCastAnimId_ = 0; spellFinalizeAnimId_ = 0; + newState = CharAnimState::JUMP_START; + } else if (!grounded) { + spellPrecastAnimId_ = 0; spellCastAnimId_ = 0; spellFinalizeAnimId_ = 0; + newState = CharAnimState::JUMP_MID; + } else if (characterRenderer && characterInstanceId > 0) { + uint32_t curId = 0; float curT = 0.0f, curDur = 0.0f; + uint32_t expectedAnim = spellPrecastAnimId_ ? spellPrecastAnimId_ : anim::SPELL_PRECAST; + if (characterRenderer->getAnimationState(characterInstanceId, curId, curT, curDur)) { + if (curId != expectedAnim || (curDur > 0.1f && curT >= curDur - 0.05f)) { + // Precast finished → advance to casting phase + newState = CharAnimState::SPELL_CASTING; + } + } + } + break; + + case CharAnimState::SPELL_CASTING: + // Spell cast loop holds until interrupted by movement, jump, swim, or stopSpellCast() + if (swim) { + spellPrecastAnimId_ = 0; spellCastAnimId_ = 0; spellFinalizeAnimId_ = 0; + newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; + } else if (!grounded && jumping) { + spellPrecastAnimId_ = 0; spellCastAnimId_ = 0; spellFinalizeAnimId_ = 0; + newState = CharAnimState::JUMP_START; + } else if (!grounded) { + spellPrecastAnimId_ = 0; spellCastAnimId_ = 0; spellFinalizeAnimId_ = 0; + newState = CharAnimState::JUMP_MID; + } else if (moving) { + spellPrecastAnimId_ = 0; spellCastAnimId_ = 0; spellFinalizeAnimId_ = 0; + newState = sprinting ? CharAnimState::RUN : CharAnimState::WALK; + } + // Looping cast stays until stopSpellCast() is called externally + break; + + case CharAnimState::SPELL_FINALIZE: { + // One-shot release: play finalize anim completely, then return to idle + if (swim) { + spellPrecastAnimId_ = 0; spellCastAnimId_ = 0; spellFinalizeAnimId_ = 0; + newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; + } else if (!grounded && jumping) { + spellPrecastAnimId_ = 0; spellCastAnimId_ = 0; spellFinalizeAnimId_ = 0; + newState = CharAnimState::JUMP_START; + } else if (characterRenderer && characterInstanceId > 0) { + uint32_t curId = 0; float curT = 0.0f, curDur = 0.0f; + // Determine which animation we expect to be playing + uint32_t expectedAnim = spellFinalizeAnimId_ ? spellFinalizeAnimId_ + : (spellCastAnimId_ ? spellCastAnimId_ : anim::SPELL); + if (characterRenderer->getAnimationState(characterInstanceId, curId, curT, curDur)) { + if (curId != expectedAnim || (curDur > 0.1f && curT >= curDur - 0.05f)) { + // Finalization complete → return to idle + spellPrecastAnimId_ = 0; + spellCastAnimId_ = 0; + spellFinalizeAnimId_ = 0; + newState = inCombat_ ? CharAnimState::COMBAT_IDLE : CharAnimState::IDLE; + } + } + } + break; + } + + case CharAnimState::HIT_REACTION: + // One-shot reaction: exit when animation finishes + if (swim) { + hitReactionAnimId_ = 0; + newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; + } else if (moving) { + hitReactionAnimId_ = 0; + newState = sprinting ? CharAnimState::RUN : CharAnimState::WALK; + } else if (characterRenderer && characterInstanceId > 0) { + uint32_t curId = 0; float curT = 0.0f, curDur = 0.0f; + uint32_t expectedHitAnim = hitReactionAnimId_ ? hitReactionAnimId_ : anim::COMBAT_WOUND; + if (characterRenderer->getAnimationState(characterInstanceId, curId, curT, curDur)) { + // Renderer auto-returns one-shots to STAND — detect that OR normal completion + if (curId != expectedHitAnim || (curDur > 0.1f && curT >= curDur - 0.05f)) { + hitReactionAnimId_ = 0; + newState = inCombat_ ? CharAnimState::COMBAT_IDLE : CharAnimState::IDLE; + } + } + } + break; + + case CharAnimState::STUNNED: + // Stun holds until setStunned(false) is called. + // Only swim can break it (physics override). + if (swim) { + stunned_ = false; + newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; + } + break; } - if (forceMelee) { + // Stun overrides melee/charge (can't act while stunned) + if (stunned_ && newState != CharAnimState::SWIM && newState != CharAnimState::SWIM_IDLE + && newState != CharAnimState::STUNNED) { + newState = CharAnimState::STUNNED; + } + + if (forceMelee && !stunned_) { newState = CharAnimState::MELEE_SWING; + spellPrecastAnimId_ = 0; + spellCastAnimId_ = 0; + spellFinalizeAnimId_ = 0; + hitReactionAnimId_ = 0; } - if (charging_) { + if (forceRanged && !stunned_ && !forceMelee) { + newState = CharAnimState::RANGED_SHOOT; + spellPrecastAnimId_ = 0; + spellCastAnimId_ = 0; + spellFinalizeAnimId_ = 0; + hitReactionAnimId_ = 0; + } + + if (charging_ && !stunned_) { newState = CharAnimState::CHARGE; + spellPrecastAnimId_ = 0; + spellCastAnimId_ = 0; + spellFinalizeAnimId_ = 0; + hitReactionAnimId_ = 0; } if (newState != charAnimState_) { @@ -1376,61 +2033,192 @@ void AnimationController::updateCharacterAnimation() { return fallback; }; - uint32_t animId = ANIM_STAND; + uint32_t animId = anim::STAND; bool loop = true; switch (charAnimState_) { - case CharAnimState::IDLE: animId = ANIM_STAND; loop = true; break; + case CharAnimState::IDLE: + if (lowHealth_ && characterRenderer->hasAnimation(characterInstanceId, anim::STAND_WOUND)) { + animId = anim::STAND_WOUND; + } else { + animId = anim::STAND; + } + loop = true; + break; case CharAnimState::WALK: if (movingBackward) { - animId = pickFirstAvailable({ANIM_BACKPEDAL}, ANIM_WALK); + animId = pickFirstAvailable({anim::WALK_BACKWARDS}, anim::WALK); } else if (anyStrafeLeft) { - animId = pickFirstAvailable({ANIM_STRAFE_WALK_LEFT, ANIM_STRAFE_RUN_LEFT}, ANIM_WALK); + animId = pickFirstAvailable({anim::SHUFFLE_LEFT, anim::RUN_LEFT}, anim::WALK); } else if (anyStrafeRight) { - animId = pickFirstAvailable({ANIM_STRAFE_WALK_RIGHT, ANIM_STRAFE_RUN_RIGHT}, ANIM_WALK); + animId = pickFirstAvailable({anim::SHUFFLE_RIGHT, anim::RUN_RIGHT}, anim::WALK); } else { - animId = pickFirstAvailable({ANIM_WALK, ANIM_RUN}, ANIM_STAND); + animId = pickFirstAvailable({anim::WALK, anim::RUN}, anim::STAND); } loop = true; break; case CharAnimState::RUN: if (movingBackward) { - animId = pickFirstAvailable({ANIM_BACKPEDAL}, ANIM_WALK); + animId = pickFirstAvailable({anim::WALK_BACKWARDS}, anim::WALK); } else if (anyStrafeLeft) { - animId = pickFirstAvailable({ANIM_STRAFE_RUN_LEFT}, ANIM_RUN); + animId = pickFirstAvailable({anim::RUN_LEFT}, anim::RUN); } else if (anyStrafeRight) { - animId = pickFirstAvailable({ANIM_STRAFE_RUN_RIGHT}, ANIM_RUN); + animId = pickFirstAvailable({anim::RUN_RIGHT}, anim::RUN); + } else if (sprintAuraActive_) { + animId = pickFirstAvailable({anim::SPRINT, anim::RUN, anim::WALK}, anim::STAND); } else { - animId = pickFirstAvailable({ANIM_RUN, ANIM_WALK}, ANIM_STAND); + animId = pickFirstAvailable({anim::RUN, anim::WALK}, anim::STAND); } loop = true; break; - case CharAnimState::JUMP_START: animId = ANIM_JUMP_START; loop = false; break; - case CharAnimState::JUMP_MID: animId = ANIM_JUMP_MID; loop = false; break; - case CharAnimState::JUMP_END: animId = ANIM_JUMP_END; loop = false; break; - case CharAnimState::SIT_DOWN: animId = ANIM_SIT_DOWN; loop = false; break; - case CharAnimState::SITTING: animId = ANIM_SITTING; loop = true; break; + case CharAnimState::JUMP_START: animId = anim::JUMP_START; loop = false; break; + case CharAnimState::JUMP_MID: animId = anim::JUMP; loop = false; break; + case CharAnimState::JUMP_END: animId = anim::JUMP_END; loop = false; break; + case CharAnimState::SIT_DOWN: + animId = sitDownAnim_ ? sitDownAnim_ : anim::SIT_GROUND_DOWN; + loop = false; + break; + case CharAnimState::SITTING: + animId = sitLoopAnim_ ? sitLoopAnim_ : anim::SITTING; + loop = true; + break; + case CharAnimState::SIT_UP: + animId = sitUpAnim_ ? sitUpAnim_ : anim::SIT_GROUND_UP; + loop = false; + break; case CharAnimState::EMOTE: animId = emoteAnimId_; loop = emoteLoop_; break; - case CharAnimState::SWIM_IDLE: animId = ANIM_SWIM_IDLE; loop = true; break; - case CharAnimState::SWIM: animId = ANIM_SWIM; loop = true; break; + case CharAnimState::LOOTING: animId = anim::LOOT; loop = true; break; + case CharAnimState::SWIM_IDLE: animId = anim::SWIM_IDLE; loop = true; break; + case CharAnimState::SWIM: + if (movingBackward) { + animId = pickFirstAvailable({anim::SWIM_BACKWARDS}, anim::SWIM); + } else if (anyStrafeLeft) { + animId = pickFirstAvailable({anim::SWIM_LEFT}, anim::SWIM); + } else if (anyStrafeRight) { + animId = pickFirstAvailable({anim::SWIM_RIGHT}, anim::SWIM); + } else { + animId = anim::SWIM; + } + loop = true; + break; case CharAnimState::MELEE_SWING: - animId = resolveMeleeAnimId(); + if (specialAttackAnimId_ != 0) { + animId = specialAttackAnimId_; + } else { + animId = resolveMeleeAnimId(); + } if (animId == 0) { - animId = ANIM_STAND; + animId = anim::STAND; } loop = false; break; - case CharAnimState::MOUNT: animId = ANIM_MOUNT; loop = true; break; + case CharAnimState::RANGED_SHOOT: + animId = rangedAnimId_ ? rangedAnimId_ : anim::ATTACK_BOW; + loop = false; + break; + case CharAnimState::RANGED_LOAD: + switch (equippedRangedType_) { + case RangedWeaponType::BOW: + animId = pickFirstAvailable({anim::LOAD_BOW}, anim::STAND); break; + case RangedWeaponType::GUN: + animId = pickFirstAvailable({anim::LOAD_RIFLE}, anim::STAND); break; + case RangedWeaponType::CROSSBOW: + animId = pickFirstAvailable({anim::LOAD_BOW}, anim::STAND); break; + default: + animId = anim::STAND; break; + } + loop = false; + break; + case CharAnimState::MOUNT: animId = anim::MOUNT; loop = true; break; case CharAnimState::COMBAT_IDLE: - animId = pickFirstAvailable( - {ANIM_READY_1H, ANIM_READY_2H, ANIM_READY_2H_L, ANIM_READY_UNARMED}, - ANIM_STAND); + // Wounded idle overrides combat stance when HP < 20% + if (lowHealth_ && characterRenderer->hasAnimation(characterInstanceId, anim::STAND_WOUND)) { + animId = anim::STAND_WOUND; + } else if (equippedRangedType_ == RangedWeaponType::BOW) { + animId = pickFirstAvailable( + {anim::READY_BOW, anim::READY_1H, anim::READY_UNARMED}, + anim::STAND); + } else if (equippedRangedType_ == RangedWeaponType::GUN) { + animId = pickFirstAvailable( + {anim::READY_RIFLE, anim::READY_BOW, anim::READY_1H, anim::READY_UNARMED}, + anim::STAND); + } else if (equippedRangedType_ == RangedWeaponType::CROSSBOW) { + animId = pickFirstAvailable( + {anim::READY_CROSSBOW, anim::READY_BOW, anim::READY_1H, anim::READY_UNARMED}, + anim::STAND); + } else if (equippedRangedType_ == RangedWeaponType::THROWN) { + animId = pickFirstAvailable( + {anim::READY_THROWN, anim::READY_1H, anim::READY_UNARMED}, + anim::STAND); + } else if (equippedIs2HLoose_) { + animId = pickFirstAvailable( + {anim::READY_2H_LOOSE, anim::READY_2H, anim::READY_1H, anim::READY_UNARMED}, + anim::STAND); + } else if (equippedWeaponInvType_ == game::InvType::TWO_HAND) { + animId = pickFirstAvailable( + {anim::READY_2H, anim::READY_2H_LOOSE, anim::READY_1H, anim::READY_UNARMED}, + anim::STAND); + } else if (equippedIsFist_) { + animId = pickFirstAvailable( + {anim::READY_FIST_1H, anim::READY_FIST, anim::READY_1H, anim::READY_UNARMED}, + anim::STAND); + } else if (equippedWeaponInvType_ == game::InvType::NON_EQUIP) { + animId = pickFirstAvailable( + {anim::READY_UNARMED, anim::READY_1H, anim::READY_FIST}, + anim::STAND); + } else { + // 1H (inventoryType 13, 21, etc.) + animId = pickFirstAvailable( + {anim::READY_1H, anim::READY_2H, anim::READY_UNARMED}, + anim::STAND); + } loop = true; break; case CharAnimState::CHARGE: - animId = ANIM_RUN; + animId = anim::RUN; loop = true; break; + case CharAnimState::UNSHEATHE: + animId = anim::UNSHEATHE; + loop = false; + break; + case CharAnimState::SHEATHE: + animId = pickFirstAvailable({anim::SHEATHE, anim::HIP_SHEATHE}, anim::SHEATHE); + loop = false; + break; + case CharAnimState::SPELL_PRECAST: + animId = spellPrecastAnimId_ ? spellPrecastAnimId_ : anim::SPELL_PRECAST; + loop = false; // One-shot wind-up + break; + case CharAnimState::SPELL_CASTING: + animId = spellCastAnimId_ ? spellCastAnimId_ : anim::SPELL; + loop = spellCastLoop_; + break; + case CharAnimState::SPELL_FINALIZE: + // Play finalization anim if set, otherwise let the cast anim finish as one-shot + animId = spellFinalizeAnimId_ ? spellFinalizeAnimId_ + : (spellCastAnimId_ ? spellCastAnimId_ : anim::SPELL); + loop = false; // One-shot release + break; + case CharAnimState::HIT_REACTION: + animId = hitReactionAnimId_ ? hitReactionAnimId_ : anim::COMBAT_WOUND; + loop = false; + break; + case CharAnimState::STUNNED: + animId = anim::STUN; + loop = true; + break; + } + + // Stealth animation substitution: override idle/walk/run with stealth variants + if (stealthed_) { + if (charAnimState_ == CharAnimState::IDLE || charAnimState_ == CharAnimState::COMBAT_IDLE) { + animId = pickFirstAvailable({anim::STEALTH_STAND}, animId); + } else if (charAnimState_ == CharAnimState::WALK) { + animId = pickFirstAvailable({anim::STEALTH_WALK}, animId); + } else if (charAnimState_ == CharAnimState::RUN) { + animId = pickFirstAvailable({anim::STEALTH_RUN, anim::STEALTH_WALK}, animId); + } } uint32_t currentAnimId = 0; @@ -1438,7 +2226,10 @@ void AnimationController::updateCharacterAnimation() { float currentAnimDurationMs = 0.0f; bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs); const bool requestChanged = (lastPlayerAnimRequest_ != animId) || (lastPlayerAnimLoopRequest_ != loop); - const bool shouldPlay = (haveState && currentAnimId != animId) || (!haveState && requestChanged); + // requestChanged alone is sufficient: covers both anim ID changes AND loop-mode + // changes on the same anim (e.g. spell cast loop → finalization one-shot). + // The currentAnimId check handles engine drift (fallback anim playing instead). + const bool shouldPlay = requestChanged || (haveState && currentAnimId != animId); if (shouldPlay) { characterRenderer->playAnimation(characterInstanceId, animId, loop); lastPlayerAnimRequest_ = animId; diff --git a/src/rendering/animation_ids.cpp b/src/rendering/animation_ids.cpp new file mode 100644 index 00000000..82b244d8 --- /dev/null +++ b/src/rendering/animation_ids.cpp @@ -0,0 +1,567 @@ +// ============================================================================ +// animation_ids.cpp — Inverse lookup & DBC validation +// Generated from animation_ids.hpp (452 constants, IDs 0–451) +// ============================================================================ +#include "rendering/animation_ids.hpp" +#include "pipeline/dbc_loader.hpp" +#include "core/logger.hpp" + +#include + +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 (0–451) → 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& 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 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 diff --git a/src/rendering/celestial.cpp b/src/rendering/celestial.cpp index ad7804ba..320e7fb3 100644 --- a/src/rendering/celestial.cpp +++ b/src/rendering/celestial.cpp @@ -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; diff --git a/src/rendering/character_preview.cpp b/src/rendering/character_preview.cpp index 7002d397..6c5fc7e7 100644 --- a/src/rendering/character_preview.cpp +++ b/src/rendering/character_preview.cpp @@ -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; diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 1d15508b..34a8e266 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -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 #include #include +#include #include #include #include @@ -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(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(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 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(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()); }; diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 8fccc598..fe424809 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -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(seqs.size()); ++i) { + if (seqs[i].id == animationId) { + inst.currentSequenceIndex = i; + inst.animDuration = static_cast(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; diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 1daf09cf..d5884d94 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -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_->initialize(vkCtx); - // Phase 2.5: Create render graph and register virtual resources + // Create render graph and register virtual resources renderGraph_ = std::make_unique(); 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) { diff --git a/src/rendering/terrain_renderer.cpp b/src/rendering/terrain_renderer.cpp index 458714a5..67b27463 100644 --- a/src/rendering/terrain_renderer.cpp +++ b/src/rendering/terrain_renderer.cpp @@ -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(chunk.vertices.size()); uint32_t idxCount = static_cast(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; diff --git a/src/ui/auth_screen.cpp b/src/ui/auth_screen.cpp index e30be6e4..ac613a68 100644 --- a/src/ui/auth_screen.cpp +++ b/src/ui/auth_screen.cpp @@ -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 #include "stb_image.h" #include -#include #include +#include +#include #include #include #include @@ -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 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(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 diff --git a/src/ui/combat_ui.cpp b/src/ui/combat_ui.cpp index 94b4da9d..1f371da0 100644 --- a/src/ui/combat_ui.cpp +++ b/src/ui/combat_ui.cpp @@ -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, diff --git a/src/ui/dialog_manager.cpp b/src/ui/dialog_manager.cpp index 0677efb3..26ba6c21 100644 --- a/src/ui/dialog_manager.cpp +++ b/src/ui/dialog_manager.cpp @@ -63,7 +63,7 @@ void DialogManager::renderLateDialogs(game::GameHandler& gameHandler) { } // ============================================================ -// Group Invite Popup (Phase 4) +// Group Invite Popup // ============================================================ void DialogManager::renderGroupInvitePopup(game::GameHandler& gameHandler) { diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 59d6f03f..68954608 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -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; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index aa82245e..a607dfe3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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) diff --git a/tests/test_animation_ids.cpp b/tests/test_animation_ids.cpp new file mode 100644 index 00000000..1fb4b6a2 --- /dev/null +++ b/tests/test_animation_ids.cpp @@ -0,0 +1,184 @@ +// Animation ID validation tests — covers nameFromId() and validateAgainstDBC() +#include +#include "rendering/animation_ids.hpp" +#include "pipeline/dbc_loader.hpp" +#include +#include + +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 buildAnimationDBC(const std::vector& animIds) { + const uint32_t numRecords = static_cast(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 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(v & 0xFF)); + data.push_back(static_cast((v >> 8) & 0xFF)); + data.push_back(static_cast((v >> 16) & 0xFF)); + data.push_back(static_cast((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(); + 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 allIds; + for (uint32_t i = 0; i < anim::ANIM_COUNT; ++i) { + allIds.push_back(i); + } + auto data = buildAnimationDBC(allIds); + + auto dbc = std::make_shared(); + 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 partialIds = {0, 1, 2, 4, 5}; + auto data = buildAnimationDBC(partialIds); + + auto dbc = std::make_shared(); + 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 extraIds = {0, 1, 500, 600, 1000}; + auto data = buildAnimationDBC(extraIds); + + auto dbc = std::make_shared(); + 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(); + 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(); + 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); +} diff --git a/tests/test_blp_loader.cpp b/tests/test_blp_loader.cpp index b66073ad..6d6a80af 100644 --- a/tests/test_blp_loader.cpp +++ b/tests/test_blp_loader.cpp @@ -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 #include "pipeline/blp_loader.hpp" diff --git a/tests/test_dbc_loader.cpp b/tests/test_dbc_loader.cpp index 6afa0b44..6a6fcea1 100644 --- a/tests/test_dbc_loader.cpp +++ b/tests/test_dbc_loader.cpp @@ -1,4 +1,4 @@ -// Phase 0 – DBC binary parsing tests with synthetic data +// DBC binary parsing tests with synthetic data #include #include "pipeline/dbc_loader.hpp" #include diff --git a/tests/test_entity.cpp b/tests/test_entity.cpp index 0d30f6e2..98de8dde 100644 --- a/tests/test_entity.cpp +++ b/tests/test_entity.cpp @@ -1,4 +1,4 @@ -// Phase 0 – Entity, Unit, Player, GameObject, EntityManager tests +// Entity, Unit, Player, GameObject, EntityManager tests #include #include "game/entity.hpp" #include diff --git a/tests/test_frustum.cpp b/tests/test_frustum.cpp index 19d67a8f..7de37188 100644 --- a/tests/test_frustum.cpp +++ b/tests/test_frustum.cpp @@ -1,4 +1,4 @@ -// Phase 0 – Frustum plane extraction and intersection tests +// Frustum plane extraction and intersection tests #include #include "rendering/frustum.hpp" diff --git a/tests/test_m2_structs.cpp b/tests/test_m2_structs.cpp index 2f9b0a1c..236861e0 100644 --- a/tests/test_m2_structs.cpp +++ b/tests/test_m2_structs.cpp @@ -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 #include "pipeline/m2_loader.hpp" #include diff --git a/tests/test_opcode_table.cpp b/tests/test_opcode_table.cpp index ee123bed..ce2a1cac 100644 --- a/tests/test_opcode_table.cpp +++ b/tests/test_opcode_table.cpp @@ -1,4 +1,4 @@ -// Phase 0 – OpcodeTable load from JSON, toWire/fromWire mapping +// OpcodeTable load from JSON, toWire/fromWire mapping #include #include "game/opcode_table.hpp" #include diff --git a/tests/test_packet.cpp b/tests/test_packet.cpp index 16f2032c..d8899ec1 100644 --- a/tests/test_packet.cpp +++ b/tests/test_packet.cpp @@ -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 #include "network/packet.hpp" diff --git a/tests/test_srp.cpp b/tests/test_srp.cpp index ad0b9550..019faf5f 100644 --- a/tests/test_srp.cpp +++ b/tests/test_srp.cpp @@ -1,4 +1,4 @@ -// Phase 0 – SRP6a challenge/proof smoke tests +// SRP6a challenge/proof smoke tests #include #include "auth/srp.hpp" #include "auth/crypto.hpp" diff --git a/tools/asset_pipeline_gui.py b/tools/asset_pipeline_gui.py index 965b36e0..62022c21 100755 --- a/tools/asset_pipeline_gui.py +++ b/tools/asset_pipeline_gui.py @@ -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 0–145 ── + 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 146–199 ── + 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 diff --git a/tools/m2_viewer.py b/tools/m2_viewer.py index 8648948f..fa20d609 100755 --- a/tools/m2_viewer.py +++ b/tools/m2_viewer.py @@ -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 0–145 ── + 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 146–199 ── + 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", }