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

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

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

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

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

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

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

View file

@ -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;