mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 01:23:51 +00:00
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:
parent
d54e262048
commit
e58f9b4b40
59 changed files with 3903 additions and 483 deletions
|
|
@ -1,6 +1,8 @@
|
|||
#include "core/application.hpp"
|
||||
#include "core/coordinates.hpp"
|
||||
#include "core/profiler.hpp"
|
||||
#include "rendering/animation_ids.hpp"
|
||||
#include "rendering/animation_controller.hpp"
|
||||
#include <unordered_set>
|
||||
#include <cmath>
|
||||
#include <chrono>
|
||||
|
|
@ -941,7 +943,7 @@ void Application::setState(AppState newState) {
|
|||
});
|
||||
cc->setStandUpCallback([this]() {
|
||||
if (gameHandler) {
|
||||
gameHandler->setStandState(0); // CMSG_STAND_STATE_CHANGE(STAND)
|
||||
gameHandler->setStandState(rendering::AnimationController::STAND_STATE_STAND);
|
||||
}
|
||||
});
|
||||
cc->setAutoFollowCancelCallback([this]() {
|
||||
|
|
@ -952,9 +954,16 @@ void Application::setState(AppState newState) {
|
|||
cc->setUseWoWSpeed(true);
|
||||
}
|
||||
if (gameHandler) {
|
||||
gameHandler->setMeleeSwingCallback([this]() {
|
||||
gameHandler->setMeleeSwingCallback([this](uint32_t spellId) {
|
||||
if (renderer) {
|
||||
renderer->triggerMeleeSwing();
|
||||
// Ranged auto-attack spells: Auto Shot (75), Shoot (5019), Throw (2764)
|
||||
if (spellId == 75 || spellId == 5019 || spellId == 2764) {
|
||||
renderer->triggerRangedShot();
|
||||
} else if (spellId != 0) {
|
||||
renderer->triggerSpecialAttack(spellId);
|
||||
} else {
|
||||
renderer->triggerMeleeSwing();
|
||||
}
|
||||
}
|
||||
});
|
||||
gameHandler->setKnockBackCallback([this](float vcos, float vsin, float hspeed, float vspeed) {
|
||||
|
|
@ -1924,17 +1933,17 @@ void Application::update(float deltaTime) {
|
|||
_creatureWasWalking[guid] = isWalkingNow;
|
||||
uint32_t curAnimId = 0; float curT = 0.0f, curDur = 0.0f;
|
||||
bool gotState = charRenderer->getAnimationState(instanceId, curAnimId, curT, curDur);
|
||||
if (!gotState || curAnimId != 1 /*Death*/) {
|
||||
if (!gotState || curAnimId != rendering::anim::DEATH) {
|
||||
uint32_t targetAnim;
|
||||
if (isMovingNow) {
|
||||
if (isFlyingNow) targetAnim = 159u; // FlyForward
|
||||
else if (isSwimmingNow) targetAnim = 42u; // Swim
|
||||
else if (isWalkingNow) targetAnim = 4u; // Walk
|
||||
else targetAnim = 5u; // Run
|
||||
if (isFlyingNow) targetAnim = rendering::anim::FLY_FORWARD;
|
||||
else if (isSwimmingNow) targetAnim = rendering::anim::SWIM;
|
||||
else if (isWalkingNow) targetAnim = rendering::anim::WALK;
|
||||
else targetAnim = rendering::anim::RUN;
|
||||
} else {
|
||||
if (isFlyingNow) targetAnim = 158u; // FlyIdle (hover)
|
||||
else if (isSwimmingNow) targetAnim = 41u; // SwimIdle
|
||||
else targetAnim = 0u; // Stand
|
||||
if (isFlyingNow) targetAnim = rendering::anim::FLY_IDLE;
|
||||
else if (isSwimmingNow) targetAnim = rendering::anim::SWIM_IDLE;
|
||||
else targetAnim = rendering::anim::STAND;
|
||||
}
|
||||
charRenderer->playAnimation(instanceId, targetAnim, /*loop=*/true);
|
||||
}
|
||||
|
|
@ -2038,17 +2047,17 @@ void Application::update(float deltaTime) {
|
|||
_pCreatureWasWalking[guid] = isWalkingNow;
|
||||
uint32_t curAnimId = 0; float curT = 0.0f, curDur = 0.0f;
|
||||
bool gotState = charRenderer->getAnimationState(instanceId, curAnimId, curT, curDur);
|
||||
if (!gotState || curAnimId != 1 /*Death*/) {
|
||||
if (!gotState || curAnimId != rendering::anim::DEATH) {
|
||||
uint32_t targetAnim;
|
||||
if (isMovingNow) {
|
||||
if (isFlyingNow) targetAnim = 159u; // FlyForward
|
||||
else if (isSwimmingNow) targetAnim = 42u; // Swim
|
||||
else if (isWalkingNow) targetAnim = 4u; // Walk
|
||||
else targetAnim = 5u; // Run
|
||||
if (isFlyingNow) targetAnim = rendering::anim::FLY_FORWARD;
|
||||
else if (isSwimmingNow) targetAnim = rendering::anim::SWIM;
|
||||
else if (isWalkingNow) targetAnim = rendering::anim::WALK;
|
||||
else targetAnim = rendering::anim::RUN;
|
||||
} else {
|
||||
if (isFlyingNow) targetAnim = 158u; // FlyIdle (hover)
|
||||
else if (isSwimmingNow) targetAnim = 41u; // SwimIdle
|
||||
else targetAnim = 0u; // Stand
|
||||
if (isFlyingNow) targetAnim = rendering::anim::FLY_IDLE;
|
||||
else if (isSwimmingNow) targetAnim = rendering::anim::SWIM_IDLE;
|
||||
else targetAnim = rendering::anim::STAND;
|
||||
}
|
||||
charRenderer->playAnimation(instanceId, targetAnim, /*loop=*/true);
|
||||
}
|
||||
|
|
@ -2748,19 +2757,70 @@ void Application::setupUICallbacks() {
|
|||
});
|
||||
|
||||
// GameObject custom animation callback (e.g. chest opening)
|
||||
gameHandler->setGameObjectCustomAnimCallback([this](uint64_t guid, uint32_t /*animId*/) {
|
||||
if (!entitySpawner_) return;
|
||||
gameHandler->setGameObjectCustomAnimCallback([this](uint64_t guid, uint32_t animId) {
|
||||
if (!entitySpawner_ || !renderer) return;
|
||||
auto& goInstances = entitySpawner_->getGameObjectInstances();
|
||||
auto it = goInstances.find(guid);
|
||||
if (it == goInstances.end() || !renderer) return;
|
||||
if (it == goInstances.end()) return;
|
||||
auto& info = it->second;
|
||||
if (!info.isWmo) {
|
||||
if (auto* m2r = renderer->getM2Renderer()) {
|
||||
m2r->setInstanceAnimationFrozen(info.instanceId, false);
|
||||
// Play the custom animation as a one-shot if model supports it
|
||||
if (m2r->hasAnimation(info.instanceId, animId))
|
||||
m2r->setInstanceAnimation(info.instanceId, animId, false);
|
||||
else
|
||||
m2r->setInstanceAnimationFrozen(info.instanceId, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// GameObject state change callback — animate doors/chests opening/closing/destroying
|
||||
gameHandler->setGameObjectStateCallback([this](uint64_t guid, uint8_t goState) {
|
||||
if (!entitySpawner_ || !renderer) return;
|
||||
auto& goInstances = entitySpawner_->getGameObjectInstances();
|
||||
auto it = goInstances.find(guid);
|
||||
if (it == goInstances.end()) return;
|
||||
auto& info = it->second;
|
||||
if (info.isWmo) return; // WMOs don't have M2 animation sequences
|
||||
auto* m2r = renderer->getM2Renderer();
|
||||
if (!m2r) return;
|
||||
uint32_t instId = info.instanceId;
|
||||
// GO states: 0=READY(closed), 1=OPEN, 2=DESTROYED/ACTIVE
|
||||
if (goState == 1) {
|
||||
// Opening: play OPEN(148) one-shot, fall back to unfreezing
|
||||
if (m2r->hasAnimation(instId, 148))
|
||||
m2r->setInstanceAnimation(instId, 148, false);
|
||||
else
|
||||
m2r->setInstanceAnimationFrozen(instId, false);
|
||||
} else if (goState == 2) {
|
||||
// Destroyed: play DESTROY(149) one-shot
|
||||
if (m2r->hasAnimation(instId, 149))
|
||||
m2r->setInstanceAnimation(instId, 149, false);
|
||||
} else {
|
||||
// Closed: play CLOSE(146) one-shot, else freeze
|
||||
if (m2r->hasAnimation(instId, 146))
|
||||
m2r->setInstanceAnimation(instId, 146, false);
|
||||
else
|
||||
m2r->setInstanceAnimationFrozen(instId, true);
|
||||
}
|
||||
});
|
||||
|
||||
// Sprint aura callback — use SPRINT(143) animation when sprint-type buff is active
|
||||
gameHandler->setSprintAuraCallback([this](bool active) {
|
||||
if (!renderer) return;
|
||||
auto* ac = renderer->getAnimationController();
|
||||
if (ac) ac->setSprintAuraActive(active);
|
||||
});
|
||||
|
||||
// Vehicle state callback — hide player character when inside a vehicle
|
||||
gameHandler->setVehicleStateCallback([this](bool entered, uint32_t /*vehicleId*/) {
|
||||
if (!renderer) return;
|
||||
auto* cr = renderer->getCharacterRenderer();
|
||||
uint32_t instId = renderer->getCharacterInstanceId();
|
||||
if (!cr || instId == 0) return;
|
||||
cr->setInstanceVisible(instId, !entered);
|
||||
});
|
||||
|
||||
// Charge callback — warrior rushes toward target
|
||||
gameHandler->setChargeCallback([this](uint64_t targetGuid, float tx, float ty, float tz) {
|
||||
if (!renderer || !renderer->getCameraController() || !gameHandler) return;
|
||||
|
|
@ -3059,8 +3119,8 @@ void Application::setupUICallbacks() {
|
|||
auto* cr = renderer->getCharacterRenderer();
|
||||
bool gotState = cr->getAnimationState(instanceId, curAnimId, curT, curDur);
|
||||
// Only start Run if not already running and not in Death animation.
|
||||
if (!gotState || (curAnimId != 1 /*Death*/ && curAnimId != 5u /*Run*/)) {
|
||||
cr->playAnimation(instanceId, 5u, /*loop=*/true);
|
||||
if (!gotState || (curAnimId != rendering::anim::DEATH && curAnimId != rendering::anim::RUN)) {
|
||||
cr->playAnimation(instanceId, rendering::anim::RUN, /*loop=*/true);
|
||||
}
|
||||
entitySpawner_->getCreatureWasMoving()[guid] = true;
|
||||
}
|
||||
|
|
@ -3256,11 +3316,11 @@ void Application::setupUICallbacks() {
|
|||
uint32_t instanceId = entitySpawner_->getCreatureInstanceId(guid);
|
||||
if (instanceId == 0) instanceId = entitySpawner_->getPlayerInstanceId(guid);
|
||||
if (instanceId != 0) {
|
||||
renderer->getCharacterRenderer()->playAnimation(instanceId, 1, false); // Death
|
||||
renderer->getCharacterRenderer()->playAnimation(instanceId, rendering::anim::DEATH, false);
|
||||
}
|
||||
});
|
||||
|
||||
// NPC/player respawn callback (online mode) - reset to idle animation
|
||||
// NPC/player respawn callback (online mode) - play rise animation then idle
|
||||
gameHandler->setNpcRespawnCallback([this](uint64_t guid) {
|
||||
if (!entitySpawner_) return;
|
||||
entitySpawner_->unmarkCreatureDead(guid);
|
||||
|
|
@ -3268,11 +3328,18 @@ void Application::setupUICallbacks() {
|
|||
uint32_t instanceId = entitySpawner_->getCreatureInstanceId(guid);
|
||||
if (instanceId == 0) instanceId = entitySpawner_->getPlayerInstanceId(guid);
|
||||
if (instanceId != 0) {
|
||||
renderer->getCharacterRenderer()->playAnimation(instanceId, 0, true); // Idle
|
||||
auto* cr = renderer->getCharacterRenderer();
|
||||
// Play RISE one-shot (auto-returns to STAND when finished), fall back to STAND
|
||||
if (cr->hasAnimation(instanceId, rendering::anim::RISE))
|
||||
cr->playAnimation(instanceId, rendering::anim::RISE, false);
|
||||
else
|
||||
cr->playAnimation(instanceId, rendering::anim::STAND, true);
|
||||
}
|
||||
});
|
||||
|
||||
// NPC/player swing callback (online mode) - play attack animation
|
||||
// Probes the model for the best available attack animation:
|
||||
// ATTACK_1H(17) → ATTACK_2H(18) → ATTACK_2H_LOOSE(19) → ATTACK_UNARMED(16)
|
||||
gameHandler->setNpcSwingCallback([this](uint64_t guid) {
|
||||
if (!entitySpawner_) return;
|
||||
if (!renderer || !renderer->getCharacterRenderer()) return;
|
||||
|
|
@ -3280,8 +3347,12 @@ void Application::setupUICallbacks() {
|
|||
if (instanceId == 0) instanceId = entitySpawner_->getPlayerInstanceId(guid);
|
||||
if (instanceId != 0) {
|
||||
auto* cr = renderer->getCharacterRenderer();
|
||||
// Try weapon-appropriate attack anim: 17=1H, 18=2H, 16=unarmed fallback
|
||||
static const uint32_t attackAnims[] = {17, 18, 16};
|
||||
static const uint32_t attackAnims[] = {
|
||||
rendering::anim::ATTACK_1H,
|
||||
rendering::anim::ATTACK_2H,
|
||||
rendering::anim::ATTACK_2H_LOOSE,
|
||||
rendering::anim::ATTACK_UNARMED
|
||||
};
|
||||
bool played = false;
|
||||
for (uint32_t anim : attackAnims) {
|
||||
if (cr->hasAnimation(instanceId, anim)) {
|
||||
|
|
@ -3290,10 +3361,70 @@ void Application::setupUICallbacks() {
|
|||
break;
|
||||
}
|
||||
}
|
||||
if (!played) cr->playAnimation(instanceId, 16, false);
|
||||
if (!played) cr->playAnimation(instanceId, rendering::anim::ATTACK_UNARMED, false);
|
||||
}
|
||||
});
|
||||
|
||||
// Hit reaction callback — plays one-shot dodge/block/wound animation on the victim
|
||||
gameHandler->setHitReactionCallback([this](uint64_t victimGuid, game::GameHandler::HitReaction reaction) {
|
||||
if (!renderer) return;
|
||||
auto* cr = renderer->getCharacterRenderer();
|
||||
if (!cr) return;
|
||||
|
||||
// Determine animation based on reaction type
|
||||
uint32_t animId = rendering::anim::COMBAT_WOUND;
|
||||
switch (reaction) {
|
||||
case game::GameHandler::HitReaction::DODGE: animId = rendering::anim::DODGE; break;
|
||||
case game::GameHandler::HitReaction::PARRY: break; // Parry already handled by existing system
|
||||
case game::GameHandler::HitReaction::BLOCK: animId = rendering::anim::BLOCK; break;
|
||||
case game::GameHandler::HitReaction::SHIELD_BLOCK: animId = rendering::anim::SHIELD_BLOCK; break;
|
||||
case game::GameHandler::HitReaction::CRIT_WOUND: animId = rendering::anim::COMBAT_CRITICAL; break;
|
||||
case game::GameHandler::HitReaction::WOUND: animId = rendering::anim::COMBAT_WOUND; break;
|
||||
}
|
||||
|
||||
// For local player: use AnimationController state
|
||||
bool isLocalPlayer = (victimGuid == gameHandler->getPlayerGuid());
|
||||
if (isLocalPlayer) {
|
||||
auto* ac = renderer->getAnimationController();
|
||||
if (ac) {
|
||||
uint32_t charInstId = renderer->getCharacterInstanceId();
|
||||
if (charInstId && cr->hasAnimation(charInstId, animId))
|
||||
ac->triggerHitReaction(animId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// For NPCs/other players: direct playAnimation
|
||||
if (!entitySpawner_) return;
|
||||
uint32_t instanceId = entitySpawner_->getCreatureInstanceId(victimGuid);
|
||||
if (instanceId == 0) instanceId = entitySpawner_->getPlayerInstanceId(victimGuid);
|
||||
if (instanceId != 0 && cr->hasAnimation(instanceId, animId))
|
||||
cr->playAnimation(instanceId, animId, false);
|
||||
});
|
||||
|
||||
// Stun state callback — enters/exits STUNNED animation on local player
|
||||
gameHandler->setStunStateCallback([this](bool stunned) {
|
||||
if (!renderer) return;
|
||||
auto* ac = renderer->getAnimationController();
|
||||
if (ac) ac->setStunned(stunned);
|
||||
});
|
||||
|
||||
// Stealth state callback — switches to stealth animation variants
|
||||
gameHandler->setStealthStateCallback([this](bool stealthed) {
|
||||
if (!renderer) return;
|
||||
auto* ac = renderer->getAnimationController();
|
||||
if (ac) ac->setStealthed(stealthed);
|
||||
});
|
||||
|
||||
// Player health callback — switches to wounded idle when HP < 20%
|
||||
gameHandler->setPlayerHealthCallback([this](uint32_t health, uint32_t maxHealth) {
|
||||
if (!renderer) return;
|
||||
auto* ac = renderer->getAnimationController();
|
||||
if (!ac) return;
|
||||
bool lowHp = (maxHealth > 0) && (health > 0) && (health * 5 <= maxHealth);
|
||||
ac->setLowHealth(lowHp);
|
||||
});
|
||||
|
||||
// Unit animation hint callback — plays jump (38=JumpMid) animation on other players/NPCs.
|
||||
// Swim/walking state is now authoritative from the move-flags callback below.
|
||||
// animId=38 (JumpMid): airborne jump animation; land detection is via per-frame sync.
|
||||
|
|
@ -3305,9 +3436,9 @@ void Application::setupUICallbacks() {
|
|||
uint32_t instanceId = entitySpawner_->getPlayerInstanceId(guid);
|
||||
if (instanceId == 0) instanceId = entitySpawner_->getCreatureInstanceId(guid);
|
||||
if (instanceId == 0) return;
|
||||
// Don't override Death animation (1)
|
||||
// Don't override Death animation
|
||||
uint32_t curAnim = 0; float curT = 0.0f, curDur = 0.0f;
|
||||
if (cr->getAnimationState(instanceId, curAnim, curT, curDur) && curAnim == 1) return;
|
||||
if (cr->getAnimationState(instanceId, curAnim, curT, curDur) && curAnim == rendering::anim::DEATH) return;
|
||||
cr->playAnimation(instanceId, animId, /*loop=*/true);
|
||||
});
|
||||
|
||||
|
|
@ -3331,16 +3462,23 @@ void Application::setupUICallbacks() {
|
|||
else flyState.erase(guid);
|
||||
});
|
||||
|
||||
// Emote animation callback — play server-driven emote animations on NPCs and other players
|
||||
// Emote animation callback — play server-driven emote animations on NPCs and other players.
|
||||
// When emoteAnim is 0, the NPC's emote state was cleared → revert to STAND.
|
||||
// Non-zero values from UNIT_NPC_EMOTESTATE updates are persistent (played looping).
|
||||
gameHandler->setEmoteAnimCallback([this](uint64_t guid, uint32_t emoteAnim) {
|
||||
if (!entitySpawner_) return;
|
||||
if (!renderer || emoteAnim == 0) return;
|
||||
if (!renderer) return;
|
||||
auto* cr = renderer->getCharacterRenderer();
|
||||
if (!cr) return;
|
||||
// Look up creature instance first, then online players
|
||||
uint32_t emoteInstanceId = entitySpawner_->getCreatureInstanceId(guid);
|
||||
if (emoteInstanceId != 0) {
|
||||
cr->playAnimation(emoteInstanceId, emoteAnim, false);
|
||||
if (emoteAnim == 0) {
|
||||
// Emote state cleared → return to idle
|
||||
cr->playAnimation(emoteInstanceId, rendering::anim::STAND, true);
|
||||
} else {
|
||||
cr->playAnimation(emoteInstanceId, emoteAnim, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
emoteInstanceId = entitySpawner_->getPlayerInstanceId(guid);
|
||||
|
|
@ -3350,34 +3488,134 @@ void Application::setupUICallbacks() {
|
|||
});
|
||||
|
||||
// Spell cast animation callback — play cast animation on caster (player or NPC/other player)
|
||||
gameHandler->setSpellCastAnimCallback([this](uint64_t guid, bool start, bool /*isChannel*/) {
|
||||
// Probes the model for the best available spell animation with fallback chain:
|
||||
// Regular cast: SPELL_CAST_DIRECTED(53) → SPELL_CAST_OMNI(54) → SPELL_CAST(32) → SPELL(2)
|
||||
// Channel: CHANNEL_CAST_DIRECTED(124) → CHANNEL_CAST_OMNI(125) → SPELL_CAST_DIRECTED(53) → SPELL(2)
|
||||
// For the local player, uses AnimationController state machine to prevent
|
||||
// COMBAT_IDLE from overriding the spell animation. For NPCs/other players,
|
||||
// calls playAnimation directly (they don't share the player state machine).
|
||||
gameHandler->setSpellCastAnimCallback([this](uint64_t guid, bool start, bool isChannel) {
|
||||
if (!entitySpawner_) return;
|
||||
if (!renderer) return;
|
||||
auto* cr = renderer->getCharacterRenderer();
|
||||
if (!cr) return;
|
||||
// Animation 3 = SpellCast (one-shot; return-to-idle handled by character_renderer)
|
||||
const uint32_t castAnim = 3;
|
||||
// Check player character
|
||||
|
||||
// Determine if this is the local player
|
||||
bool isLocalPlayer = false;
|
||||
uint32_t instanceId = 0;
|
||||
{
|
||||
uint32_t charInstId = renderer->getCharacterInstanceId();
|
||||
if (charInstId != 0 && guid == gameHandler->getPlayerGuid()) {
|
||||
if (start) cr->playAnimation(charInstId, castAnim, false);
|
||||
// On finish: playAnimation(castAnim, loop=false) will auto-return to Stand
|
||||
return;
|
||||
instanceId = charInstId;
|
||||
isLocalPlayer = true;
|
||||
}
|
||||
}
|
||||
// Check creatures and other online players
|
||||
{
|
||||
uint32_t cInst = entitySpawner_->getCreatureInstanceId(guid);
|
||||
if (cInst != 0) {
|
||||
if (start) cr->playAnimation(cInst, castAnim, false);
|
||||
return;
|
||||
if (instanceId == 0) instanceId = entitySpawner_->getCreatureInstanceId(guid);
|
||||
if (instanceId == 0) instanceId = entitySpawner_->getPlayerInstanceId(guid);
|
||||
if (instanceId == 0) return;
|
||||
|
||||
if (start) {
|
||||
// Detect fishing spells (channeled) — use FISHING_LOOP instead of generic cast
|
||||
auto isFishingSpell = [](uint32_t spellId) {
|
||||
return spellId == 7620 || spellId == 7731 || spellId == 7732 ||
|
||||
spellId == 18248 || spellId == 33095 || spellId == 51294;
|
||||
};
|
||||
uint32_t currentSpell = isLocalPlayer ? gameHandler->getCurrentCastSpellId() : 0;
|
||||
bool isFishing = isChannel && isFishingSpell(currentSpell);
|
||||
|
||||
if (isFishing && cr->hasAnimation(instanceId, rendering::anim::FISHING_LOOP)) {
|
||||
// Fishing: use FISHING_LOOP (looping idle) for the channel duration
|
||||
if (isLocalPlayer) {
|
||||
auto* ac = renderer->getAnimationController();
|
||||
if (ac) ac->startSpellCast(0, rendering::anim::FISHING_LOOP, true, 0);
|
||||
} else {
|
||||
cr->playAnimation(instanceId, rendering::anim::FISHING_LOOP, true);
|
||||
}
|
||||
} else {
|
||||
// Spell animation sequence: PRECAST (one-shot) → CAST (loop) → FINALIZE (one-shot) → idle
|
||||
// Probe model for best available animations with fallback chains:
|
||||
// Regular cast: SPELL_CAST_DIRECTED → SPELL_CAST_OMNI → SPELL_CAST → SPELL
|
||||
// Channel: CHANNEL_CAST_DIRECTED → CHANNEL_CAST_OMNI → SPELL_CAST_DIRECTED → SPELL
|
||||
bool hasTarget = gameHandler->hasTarget();
|
||||
|
||||
// Phase 1: Precast wind-up (one-shot, non-channels only)
|
||||
uint32_t precastAnim = 0;
|
||||
if (!isChannel && cr->hasAnimation(instanceId, rendering::anim::SPELL_PRECAST)) {
|
||||
precastAnim = rendering::anim::SPELL_PRECAST;
|
||||
}
|
||||
}
|
||||
{
|
||||
uint32_t pInst = entitySpawner_->getPlayerInstanceId(guid);
|
||||
if (pInst != 0) {
|
||||
if (start) cr->playAnimation(pInst, castAnim, false);
|
||||
|
||||
// Phase 2: Cast hold (looping until stopSpellCast)
|
||||
static const uint32_t castDirected[] = {
|
||||
rendering::anim::SPELL_CAST_DIRECTED,
|
||||
rendering::anim::SPELL_CAST_OMNI,
|
||||
rendering::anim::SPELL_CAST,
|
||||
rendering::anim::SPELL
|
||||
};
|
||||
static const uint32_t castOmni[] = {
|
||||
rendering::anim::SPELL_CAST_OMNI,
|
||||
rendering::anim::SPELL_CAST_DIRECTED,
|
||||
rendering::anim::SPELL_CAST,
|
||||
rendering::anim::SPELL
|
||||
};
|
||||
static const uint32_t channelDirected[] = {
|
||||
rendering::anim::CHANNEL_CAST_DIRECTED,
|
||||
rendering::anim::CHANNEL_CAST_OMNI,
|
||||
rendering::anim::SPELL_CAST_DIRECTED,
|
||||
rendering::anim::SPELL
|
||||
};
|
||||
static const uint32_t channelOmni[] = {
|
||||
rendering::anim::CHANNEL_CAST_OMNI,
|
||||
rendering::anim::CHANNEL_CAST_DIRECTED,
|
||||
rendering::anim::SPELL_CAST_DIRECTED,
|
||||
rendering::anim::SPELL
|
||||
};
|
||||
const uint32_t* chain;
|
||||
if (isChannel) {
|
||||
chain = hasTarget ? channelDirected : channelOmni;
|
||||
} else {
|
||||
chain = hasTarget ? castDirected : castOmni;
|
||||
}
|
||||
uint32_t castAnim = rendering::anim::SPELL;
|
||||
for (size_t i = 0; i < 4; ++i) {
|
||||
if (cr->hasAnimation(instanceId, chain[i])) {
|
||||
castAnim = chain[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Finalization release (one-shot after cast ends)
|
||||
// Pick a different animation from the cast loop for visual variety
|
||||
static const uint32_t finalizeChain[] = {
|
||||
rendering::anim::SPELL_CAST_OMNI,
|
||||
rendering::anim::SPELL_CAST,
|
||||
rendering::anim::SPELL
|
||||
};
|
||||
uint32_t finalizeAnim = 0;
|
||||
if (isLocalPlayer && !isChannel) {
|
||||
for (uint32_t fa : finalizeChain) {
|
||||
if (fa != castAnim && cr->hasAnimation(instanceId, fa)) {
|
||||
finalizeAnim = fa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (finalizeAnim == 0 && cr->hasAnimation(instanceId, rendering::anim::SPELL))
|
||||
finalizeAnim = rendering::anim::SPELL;
|
||||
}
|
||||
|
||||
if (isLocalPlayer) {
|
||||
auto* ac = renderer->getAnimationController();
|
||||
if (ac) ac->startSpellCast(precastAnim, castAnim, true, finalizeAnim);
|
||||
} else {
|
||||
cr->playAnimation(instanceId, castAnim, true);
|
||||
}
|
||||
} // end !isFishing
|
||||
} else {
|
||||
// Cast/channel ended — plays finalization anim completely then returns to idle
|
||||
if (isLocalPlayer) {
|
||||
auto* ac = renderer->getAnimationController();
|
||||
if (ac) ac->stopSpellCast();
|
||||
} else if (isChannel) {
|
||||
cr->playAnimation(instanceId, rendering::anim::STAND, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -3392,41 +3630,54 @@ void Application::setupUICallbacks() {
|
|||
cr->setInstanceOpacity(charInstId, isGhost ? 0.5f : 1.0f);
|
||||
});
|
||||
|
||||
// Stand state animation callback — map server stand state to M2 animation on player
|
||||
// and sync camera sit flag so movement is blocked while sitting
|
||||
// Stand state animation callback — route through AnimationController state machine
|
||||
// for proper sit/sleep/kneel transition animations (down → loop → up)
|
||||
gameHandler->setStandStateCallback([this](uint8_t standState) {
|
||||
if (!renderer) return;
|
||||
using AC = rendering::AnimationController;
|
||||
|
||||
// Sync camera controller sitting flag: block movement while sitting/kneeling
|
||||
if (auto* cc = renderer->getCameraController()) {
|
||||
cc->setSitting(standState >= 1 && standState <= 8 && standState != 7);
|
||||
cc->setSitting(standState >= AC::STAND_STATE_SIT &&
|
||||
standState <= AC::STAND_STATE_KNEEL &&
|
||||
standState != AC::STAND_STATE_DEAD);
|
||||
}
|
||||
|
||||
auto* cr = renderer->getCharacterRenderer();
|
||||
if (!cr) return;
|
||||
uint32_t charInstId = renderer->getCharacterInstanceId();
|
||||
if (charInstId == 0) return;
|
||||
// WoW stand state → M2 animation ID mapping
|
||||
// 0=Stand→0, 1-6=Sit variants→27 (SitGround), 7=Dead→1, 8=Kneel→72
|
||||
// Do not force Stand(0) here: locomotion state machine already owns standing/running.
|
||||
// Forcing Stand on packet timing causes visible run-cycle hitching while steering.
|
||||
uint32_t animId = 0;
|
||||
if (standState == 0) {
|
||||
auto* ac = renderer->getAnimationController();
|
||||
if (!ac) return;
|
||||
|
||||
// Death is special — play directly, not through sit state machine
|
||||
if (standState == AC::STAND_STATE_DEAD) {
|
||||
auto* cr = renderer->getCharacterRenderer();
|
||||
if (!cr) return;
|
||||
uint32_t charInstId = renderer->getCharacterInstanceId();
|
||||
if (charInstId == 0) return;
|
||||
cr->playAnimation(charInstId, rendering::anim::DEATH, false);
|
||||
return;
|
||||
} else if (standState >= 1 && standState <= 6) {
|
||||
animId = 27; // SitGround (covers sit-chair too; correct visual differs by chair height)
|
||||
} else if (standState == 7) {
|
||||
animId = 1; // Death
|
||||
} else if (standState == 8) {
|
||||
animId = 72; // Kneel
|
||||
}
|
||||
// Loop sit/kneel (not death) so the held-pose frame stays visible
|
||||
const bool loop = (animId != 1);
|
||||
cr->playAnimation(charInstId, animId, loop);
|
||||
|
||||
ac->setStandState(standState);
|
||||
});
|
||||
|
||||
// Loot window callback — play kneel/loot animation while looting
|
||||
gameHandler->setLootWindowCallback([this](bool open) {
|
||||
if (!renderer) return;
|
||||
auto* ac = renderer->getAnimationController();
|
||||
if (!ac) return;
|
||||
if (open) ac->startLooting();
|
||||
else ac->stopLooting();
|
||||
});
|
||||
|
||||
// NPC greeting callback - play voice line
|
||||
gameHandler->setNpcGreetingCallback([this](uint64_t guid, const glm::vec3& position) {
|
||||
// Play NPC_WELCOME animation on the NPC
|
||||
if (entitySpawner_ && renderer) {
|
||||
auto* cr = renderer->getCharacterRenderer();
|
||||
if (cr) {
|
||||
uint32_t instanceId = entitySpawner_->getCreatureInstanceId(guid);
|
||||
if (instanceId != 0) cr->playAnimation(instanceId, rendering::anim::NPC_WELCOME, false);
|
||||
}
|
||||
}
|
||||
if (audioCoordinator_ && audioCoordinator_->getNpcVoiceManager()) {
|
||||
// Convert canonical to render coords for 3D audio
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(position);
|
||||
|
|
@ -3722,8 +3973,8 @@ void Application::spawnPlayerCharacter() {
|
|||
: std::unordered_set<uint16_t>{};
|
||||
charRenderer->setActiveGeosets(instanceId, activeGeosets);
|
||||
|
||||
// Play idle animation (Stand = animation ID 0)
|
||||
charRenderer->playAnimation(instanceId, 0, true);
|
||||
// Play idle animation
|
||||
charRenderer->playAnimation(instanceId, rendering::anim::STAND, true);
|
||||
LOG_INFO("Spawned player character at (",
|
||||
static_cast<int>(spawnPos.x), ", ",
|
||||
static_cast<int>(spawnPos.y), ", ",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include "audio/npc_voice_manager.hpp"
|
||||
#include "pipeline/m2_loader.hpp"
|
||||
#include "pipeline/wmo_loader.hpp"
|
||||
#include "rendering/animation_ids.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
|
|
@ -2214,9 +2215,26 @@ void EntitySpawner::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float
|
|||
// Spawn in the correct pose. If the server marked this creature dead before
|
||||
// the queued spawn was processed, start directly in death animation.
|
||||
if (deadCreatureGuids_.count(guid)) {
|
||||
charRenderer->playAnimation(instanceId, 1, false); // Death
|
||||
charRenderer->playAnimation(instanceId, rendering::anim::DEATH, false);
|
||||
} else {
|
||||
charRenderer->playAnimation(instanceId, 0, true); // Idle
|
||||
// Check if this NPC has a persistent emote state (e.g. working, eating, dancing)
|
||||
uint32_t npcEmote = 0;
|
||||
if (gameHandler_) {
|
||||
auto entity = gameHandler_->getEntityManager().getEntity(guid);
|
||||
if (entity && entity->getType() == game::ObjectType::UNIT) {
|
||||
npcEmote = std::static_pointer_cast<game::Unit>(entity)->getNpcEmoteState();
|
||||
}
|
||||
}
|
||||
if (npcEmote != 0 && charRenderer->hasAnimation(instanceId, npcEmote)) {
|
||||
charRenderer->playAnimation(instanceId, npcEmote, true);
|
||||
} else if (charRenderer->hasAnimation(instanceId, rendering::anim::BIRTH)) {
|
||||
// Play birth animation (one-shot) — will return to STAND after
|
||||
charRenderer->playAnimation(instanceId, rendering::anim::BIRTH, false);
|
||||
} else if (charRenderer->hasAnimation(instanceId, rendering::anim::SPAWN)) {
|
||||
charRenderer->playAnimation(instanceId, rendering::anim::SPAWN, false);
|
||||
} else {
|
||||
charRenderer->playAnimation(instanceId, rendering::anim::STAND, true);
|
||||
}
|
||||
}
|
||||
charRenderer->startFadeIn(instanceId, 0.5f);
|
||||
|
||||
|
|
@ -2316,7 +2334,7 @@ void EntitySpawner::spawnOnlinePlayer(uint64_t guid,
|
|||
for (uint32_t si = 0; si < model.sequences.size(); si++) {
|
||||
if (!(model.sequences[si].flags & 0x20)) {
|
||||
uint32_t animId = model.sequences[si].id;
|
||||
if (animId != 0 && animId != 4 && animId != 5) continue;
|
||||
if (animId != rendering::anim::STAND && animId != rendering::anim::WALK && animId != rendering::anim::RUN) continue;
|
||||
char animFileName[256];
|
||||
snprintf(animFileName, sizeof(animFileName),
|
||||
"%s%s%04u-%02u.anim",
|
||||
|
|
@ -2488,7 +2506,7 @@ void EntitySpawner::spawnOnlinePlayer(uint64_t guid,
|
|||
activeGeosets.insert(kGeosetBareFeet);
|
||||
charRenderer->setActiveGeosets(instanceId, activeGeosets);
|
||||
|
||||
charRenderer->playAnimation(instanceId, 0, true);
|
||||
charRenderer->playAnimation(instanceId, rendering::anim::STAND, true);
|
||||
playerInstances_[guid] = instanceId;
|
||||
|
||||
OnlinePlayerAppearanceState st;
|
||||
|
|
@ -3373,7 +3391,21 @@ void EntitySpawner::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_
|
|||
lowerPath.find("portalfx") != std::string::npos ||
|
||||
lowerPath.find("spellportal") != std::string::npos);
|
||||
if (!isAnimatedEffect && !isTransportGO) {
|
||||
m2Renderer->setInstanceAnimationFrozen(instanceId, true);
|
||||
// Check for totem idle animations — totems should animate, not freeze
|
||||
bool isTotem = false;
|
||||
if (m2Renderer->hasAnimation(instanceId, 245)) { // TOTEM_SMALL
|
||||
m2Renderer->setInstanceAnimation(instanceId, 245, true);
|
||||
isTotem = true;
|
||||
} else if (m2Renderer->hasAnimation(instanceId, 246)) { // TOTEM_MEDIUM
|
||||
m2Renderer->setInstanceAnimation(instanceId, 246, true);
|
||||
isTotem = true;
|
||||
} else if (m2Renderer->hasAnimation(instanceId, 247)) { // TOTEM_LARGE
|
||||
m2Renderer->setInstanceAnimation(instanceId, 247, true);
|
||||
isTotem = true;
|
||||
}
|
||||
if (!isTotem) {
|
||||
m2Renderer->setInstanceAnimationFrozen(instanceId, true);
|
||||
}
|
||||
}
|
||||
|
||||
gameObjectInstances_[guid] = {modelId, instanceId, false};
|
||||
|
|
@ -4601,8 +4633,8 @@ void EntitySpawner::processPendingMount() {
|
|||
for (uint32_t si = 0; si < model.sequences.size(); si++) {
|
||||
if (!(model.sequences[si].flags & 0x20)) {
|
||||
uint32_t animId = model.sequences[si].id;
|
||||
// Only load stand(0), walk(4), run(5) anims to avoid hang
|
||||
if (animId != 0 && animId != 4 && animId != 5) continue;
|
||||
// Only load stand, walk, run anims to avoid hang
|
||||
if (animId != rendering::anim::STAND && animId != rendering::anim::WALK && animId != rendering::anim::RUN) continue;
|
||||
char animFileName[256];
|
||||
snprintf(animFileName, sizeof(animFileName), "%s%04u-%02u.anim",
|
||||
basePath.c_str(), animId, model.sequences[si].variationIndex);
|
||||
|
|
@ -4854,10 +4886,11 @@ void EntitySpawner::processPendingMount() {
|
|||
|
||||
// For taxi mounts, start with flying animation; for ground mounts, start with stand
|
||||
bool isTaxi = gameHandler_ && gameHandler_->isOnTaxiFlight();
|
||||
uint32_t startAnim = 0; // ANIM_STAND
|
||||
uint32_t startAnim = rendering::anim::STAND;
|
||||
if (isTaxi) {
|
||||
// Try WotLK fly anims first, then Vanilla-friendly fallbacks
|
||||
uint32_t taxiCandidates[] = {159, 158, 234, 229, 233, 141, 369, 6, 5}; // FlyForward, FlyIdle, FlyRun(234), FlyStand(229), FlyWalk(233), FlyMounted, FlyRun, Fly, Run
|
||||
using namespace rendering::anim;
|
||||
uint32_t taxiCandidates[] = {FLY_FORWARD, FLY_IDLE, FLY_RUN_2, FLY_SPELL, FLY_RISE, SPELL_KNEEL_LOOP, FLY_CUSTOM_SPELL_10, DEAD, RUN};
|
||||
for (uint32_t anim : taxiCandidates) {
|
||||
if (charRenderer->hasAnimation(instanceId, anim)) {
|
||||
startAnim = anim;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,9 @@ void CombatHandler::registerOpcodes(DispatchTable& table) {
|
|||
addCombatText(CombatTextEntry::ABSORB, static_cast<int32_t>(envAbs), 0, false, 0, 0, victimGuid);
|
||||
if (envRes > 0)
|
||||
addCombatText(CombatTextEntry::RESIST, static_cast<int32_t>(envRes), 0, false, 0, 0, victimGuid);
|
||||
// Drowning damage → play DROWN one-shot on player
|
||||
if (envType == 1 && dmg > 0 && owner_.emoteAnimCallback_)
|
||||
owner_.emoteAnimCallback_(victimGuid, 131); // anim::DROWN
|
||||
}
|
||||
packet.skipAll();
|
||||
};
|
||||
|
|
@ -440,7 +443,7 @@ void CombatHandler::handleAttackerStateUpdate(network::Packet& packet) {
|
|||
lastMeleeSwingMs_ = static_cast<uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count());
|
||||
if (owner_.meleeSwingCallback_) owner_.meleeSwingCallback_();
|
||||
if (owner_.meleeSwingCallback_) owner_.meleeSwingCallback_(0);
|
||||
}
|
||||
if (!isPlayerAttacker && owner_.npcSwingCallback_) {
|
||||
owner_.npcSwingCallback_(data.attackerGuid);
|
||||
|
|
@ -520,6 +523,17 @@ void CombatHandler::handleAttackerStateUpdate(network::Packet& packet) {
|
|||
addCombatText(CombatTextEntry::RESIST, static_cast<int32_t>(totalResisted), 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
|
||||
}
|
||||
|
||||
// Fire hit reaction animation on the victim
|
||||
if (owner_.hitReactionCallback_ && !data.isMiss()) {
|
||||
using HR = GameHandler::HitReaction;
|
||||
HR reaction = HR::WOUND;
|
||||
if (data.victimState == 1) reaction = HR::DODGE;
|
||||
else if (data.victimState == 2) reaction = HR::PARRY;
|
||||
else if (data.victimState == 4) reaction = HR::BLOCK;
|
||||
else if (data.isCrit()) reaction = HR::CRIT_WOUND;
|
||||
owner_.hitReactionCallback_(data.targetGuid, reaction);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CombatHandler::handleSpellDamageLog(network::Packet& packet) {
|
||||
|
|
|
|||
|
|
@ -542,6 +542,7 @@ EntityController::UnitFieldIndices EntityController::UnitFieldIndices::resolve()
|
|||
fieldIndex(UF::UNIT_FIELD_DISPLAYID),
|
||||
fieldIndex(UF::UNIT_FIELD_MOUNTDISPLAYID),
|
||||
fieldIndex(UF::UNIT_NPC_FLAGS),
|
||||
fieldIndex(UF::UNIT_NPC_EMOTESTATE),
|
||||
fieldIndex(UF::UNIT_FIELD_BYTES_0),
|
||||
fieldIndex(UF::UNIT_FIELD_BYTES_1)
|
||||
};
|
||||
|
|
@ -697,6 +698,7 @@ bool EntityController::applyUnitFieldsOnCreate(const UpdateBlock& block,
|
|||
}
|
||||
}
|
||||
else if (key == ufi.npcFlags) { unit->setNpcFlags(val); }
|
||||
else if (key == ufi.npcEmoteState) { unit->setNpcEmoteState(val); }
|
||||
else if (key == ufi.dynFlags) {
|
||||
unit->setDynamicFlags(val);
|
||||
if (block.objectType == ObjectType::UNIT &&
|
||||
|
|
@ -795,7 +797,28 @@ EntityController::UnitFieldUpdateResult EntityController::applyUnitFieldsOnUpdat
|
|||
if (!uid.empty())
|
||||
pendingEvents_.emit("UNIT_DISPLAYPOWER", {uid});
|
||||
}
|
||||
} else if (key == ufi.flags) { unit->setUnitFlags(val); }
|
||||
} else if (key == ufi.flags) {
|
||||
uint32_t oldFlags = unit->getUnitFlags();
|
||||
unit->setUnitFlags(val);
|
||||
// Detect stun state change on local player
|
||||
constexpr uint32_t UNIT_FLAG_STUNNED = 0x00040000;
|
||||
if (block.guid == owner_.playerGuid && owner_.stunStateCallback_) {
|
||||
bool wasStunned = (oldFlags & UNIT_FLAG_STUNNED) != 0;
|
||||
bool nowStunned = (val & UNIT_FLAG_STUNNED) != 0;
|
||||
if (wasStunned != nowStunned) {
|
||||
owner_.stunStateCallback_(nowStunned);
|
||||
}
|
||||
}
|
||||
// Detect stealth state change on local player
|
||||
constexpr uint32_t UNIT_FLAG_SNEAKING = 0x02000000;
|
||||
if (block.guid == owner_.playerGuid && owner_.stealthStateCallback_) {
|
||||
bool wasStealth = (oldFlags & UNIT_FLAG_SNEAKING) != 0;
|
||||
bool nowStealth = (val & UNIT_FLAG_SNEAKING) != 0;
|
||||
if (wasStealth != nowStealth) {
|
||||
owner_.stealthStateCallback_(nowStealth);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (ufi.bytes1 != 0xFFFF && key == ufi.bytes1 && block.guid == owner_.playerGuid) {
|
||||
uint8_t newForm = static_cast<uint8_t>((val >> 24) & 0xFF);
|
||||
if (newForm != owner_.shapeshiftFormId_) {
|
||||
|
|
@ -863,6 +886,14 @@ EntityController::UnitFieldUpdateResult EntityController::applyUnitFieldsOnUpdat
|
|||
}
|
||||
unit->setMountDisplayId(val);
|
||||
} else if (key == ufi.npcFlags) { unit->setNpcFlags(val); }
|
||||
else if (key == ufi.npcEmoteState) {
|
||||
uint32_t oldEmote = unit->getNpcEmoteState();
|
||||
unit->setNpcEmoteState(val);
|
||||
// Fire emote animation callback so entity_spawner can update the NPC's idle anim
|
||||
if (val != oldEmote && owner_.emoteAnimCallback_) {
|
||||
owner_.emoteAnimCallback_(block.guid, val);
|
||||
}
|
||||
}
|
||||
// Power/maxpower range checks AFTER all specific fields
|
||||
else if (key >= ufi.powerBase && key < ufi.powerBase + 7) {
|
||||
unit->setPowerByType(static_cast<uint8_t>(key - ufi.powerBase), val);
|
||||
|
|
@ -889,6 +920,11 @@ EntityController::UnitFieldUpdateResult EntityController::applyUnitFieldsOnUpdat
|
|||
}
|
||||
}
|
||||
|
||||
// Fire player health callback for wounded-idle animation
|
||||
if (result.healthChanged && block.guid == owner_.playerGuid && owner_.playerHealthCallback_) {
|
||||
owner_.playerHealthCallback_(unit->getHealth(), unit->getMaxHealth());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -1632,6 +1668,17 @@ void EntityController::onValuesUpdateGameObject(const UpdateBlock& block, std::s
|
|||
entity->getZ(), entity->getOrientation());
|
||||
}
|
||||
}
|
||||
|
||||
// Detect GO state changes from GAMEOBJECT_BYTES_1 (packed: byte0=state, byte1=type, byte2=artKit, byte3=animProgress)
|
||||
const uint16_t ufGoBytes1 = fieldIndex(UF::GAMEOBJECT_BYTES_1);
|
||||
if (ufGoBytes1 != 0xFFFF) {
|
||||
auto itB = block.fields.find(ufGoBytes1);
|
||||
if (itB != block.fields.end()) {
|
||||
uint8_t goState = static_cast<uint8_t>(itB->second & 0xFF);
|
||||
if (owner_.gameObjectStateCallback_)
|
||||
owner_.gameObjectStateCallback_(block.guid, goState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "rendering/animation_ids.hpp"
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
|
@ -1275,12 +1276,25 @@ void GameHandler::registerOpcodeHandlers() {
|
|||
};
|
||||
// Consume silently — opcodes we receive but don't need to act on
|
||||
for (auto op : {
|
||||
Opcode::SMSG_GAMEOBJECT_DESPAWN_ANIM, Opcode::SMSG_GAMEOBJECT_RESET_STATE,
|
||||
Opcode::SMSG_FLIGHT_SPLINE_SYNC, Opcode::SMSG_FORCE_DISPLAY_UPDATE,
|
||||
Opcode::SMSG_FORCE_SEND_QUEUED_PACKETS, Opcode::SMSG_FORCE_SET_VEHICLE_REC_ID,
|
||||
Opcode::SMSG_CORPSE_MAP_POSITION_QUERY_RESPONSE, Opcode::SMSG_DAMAGE_CALC_LOG,
|
||||
Opcode::SMSG_DYNAMIC_DROP_ROLL_RESULT, Opcode::SMSG_DESTRUCTIBLE_BUILDING_DAMAGE,
|
||||
}) { registerSkipHandler(op); }
|
||||
|
||||
// Game object despawn animation — reset state to closed before actual despawn
|
||||
dispatchTable_[Opcode::SMSG_GAMEOBJECT_DESPAWN_ANIM] = [this](network::Packet& packet) {
|
||||
if (!packet.hasRemaining(8)) return;
|
||||
uint64_t guid = packet.readUInt64();
|
||||
// Trigger a CLOSE animation / freeze before the object is removed
|
||||
if (gameObjectStateCallback_) gameObjectStateCallback_(guid, 0);
|
||||
};
|
||||
// Game object reset state — return to READY(closed) state
|
||||
dispatchTable_[Opcode::SMSG_GAMEOBJECT_RESET_STATE] = [this](network::Packet& packet) {
|
||||
if (!packet.hasRemaining(8)) return;
|
||||
uint64_t guid = packet.readUInt64();
|
||||
if (gameObjectStateCallback_) gameObjectStateCallback_(guid, 0);
|
||||
};
|
||||
dispatchTable_[Opcode::SMSG_FORCED_DEATH_UPDATE] = [this](network::Packet& packet) {
|
||||
playerDead_ = true;
|
||||
if (ghostStateCallback_) ghostStateCallback_(false);
|
||||
|
|
@ -2124,10 +2138,15 @@ void GameHandler::registerOpcodeHandlers() {
|
|||
if (packet.hasRemaining(1)) {
|
||||
(void)packet.readPackedGuid(); // player guid (unused)
|
||||
}
|
||||
uint32_t newVehicleId = 0;
|
||||
if (packet.hasRemaining(4)) {
|
||||
vehicleId_ = packet.readUInt32();
|
||||
} else {
|
||||
vehicleId_ = 0;
|
||||
newVehicleId = packet.readUInt32();
|
||||
}
|
||||
bool wasInVehicle = vehicleId_ != 0;
|
||||
bool nowInVehicle = newVehicleId != 0;
|
||||
vehicleId_ = newVehicleId;
|
||||
if (wasInVehicle != nowInVehicle && vehicleStateCallback_) {
|
||||
vehicleStateCallback_(nowInVehicle, newVehicleId);
|
||||
}
|
||||
};
|
||||
// guid(8) + status(1): status 1 = NPC has available/new routes for this player
|
||||
|
|
@ -2842,6 +2861,9 @@ void GameHandler::registerOpcodeHandlers() {
|
|||
};
|
||||
dispatchTable_[Opcode::SMSG_ON_CANCEL_EXPECTED_RIDE_VEHICLE_AURA] = [this](network::Packet& packet) {
|
||||
vehicleId_ = 0; // Vehicle ride cancelled; clear UI
|
||||
if (vehicleStateCallback_) {
|
||||
vehicleStateCallback_(false, 0);
|
||||
}
|
||||
packet.skipAll();
|
||||
};
|
||||
// uint32 type (0=normal, 1=heavy, 2=tired/restricted) + uint32 minutes played
|
||||
|
|
@ -6048,6 +6070,12 @@ void GameHandler::preloadDBCCaches() const {
|
|||
loadMapNameCache(); // Map.dbc
|
||||
loadLfgDungeonDbc(); // LFGDungeons.dbc
|
||||
|
||||
// Validate animation constants against AnimationData.dbc
|
||||
if (auto* am = services_.assetManager) {
|
||||
auto animDbc = am->loadDBC("AnimationData.dbc");
|
||||
rendering::anim::validateAgainstDBC(animDbc);
|
||||
}
|
||||
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - t0).count();
|
||||
LOG_INFO("DBC cache pre-load complete in ", elapsed, " ms");
|
||||
|
|
|
|||
|
|
@ -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{};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,19 +34,19 @@ static float mergeCooldownSeconds(float current, float incoming) {
|
|||
|
||||
static CombatTextEntry::Type combatTextTypeFromSpellMissInfo(uint8_t missInfo) {
|
||||
switch (missInfo) {
|
||||
case 0: return CombatTextEntry::MISS;
|
||||
case 1: return CombatTextEntry::DODGE;
|
||||
case 2: return CombatTextEntry::PARRY;
|
||||
case 3: return CombatTextEntry::BLOCK;
|
||||
case 4: return CombatTextEntry::EVADE;
|
||||
case 5: return CombatTextEntry::IMMUNE;
|
||||
case 6: return CombatTextEntry::DEFLECT;
|
||||
case 7: return CombatTextEntry::ABSORB;
|
||||
case 8: return CombatTextEntry::RESIST;
|
||||
case 9:
|
||||
case 10:
|
||||
case SpellMissInfo::MISS: return CombatTextEntry::MISS;
|
||||
case SpellMissInfo::DODGE: return CombatTextEntry::DODGE;
|
||||
case SpellMissInfo::PARRY: return CombatTextEntry::PARRY;
|
||||
case SpellMissInfo::BLOCK: return CombatTextEntry::BLOCK;
|
||||
case SpellMissInfo::EVADE: return CombatTextEntry::EVADE;
|
||||
case SpellMissInfo::IMMUNE: return CombatTextEntry::IMMUNE;
|
||||
case SpellMissInfo::DEFLECT: return CombatTextEntry::DEFLECT;
|
||||
case SpellMissInfo::ABSORB: return CombatTextEntry::ABSORB;
|
||||
case SpellMissInfo::RESIST: return CombatTextEntry::RESIST;
|
||||
case SpellMissInfo::IMMUNE2:
|
||||
case SpellMissInfo::IMMUNE3:
|
||||
return CombatTextEntry::IMMUNE;
|
||||
case 11: return CombatTextEntry::REFLECT;
|
||||
case SpellMissInfo::REFLECT: return CombatTextEntry::REFLECT;
|
||||
default: return CombatTextEntry::MISS;
|
||||
}
|
||||
}
|
||||
|
|
@ -939,7 +939,7 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
if (isMeleeAbility) {
|
||||
if (owner_.meleeSwingCallback_) owner_.meleeSwingCallback_();
|
||||
if (owner_.meleeSwingCallback_) owner_.meleeSwingCallback_(sid);
|
||||
if (auto* ac = owner_.services().audioCoordinator) {
|
||||
if (auto* csm = ac->getCombatSoundManager()) {
|
||||
csm->playWeaponSwing(audio::CombatSoundManager::WeaponSize::MEDIUM, false);
|
||||
|
|
@ -951,6 +951,14 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
|
|||
|
||||
const bool wasInTimedCast = casting_ && (data.spellId == currentCastSpellId_);
|
||||
|
||||
// Instant spell cast animation — if this wasn't a timed cast and isn't a
|
||||
// melee ability, play a brief spell cast animation (one-shot)
|
||||
if (!wasInTimedCast && !isMeleeAbility && !owner_.isProfessionSpell(data.spellId)) {
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(owner_.playerGuid, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_WARNING("[GO-DIAG] SPELL_GO: spellId=", data.spellId,
|
||||
" casting=", casting_, " currentCast=", currentCastSpellId_,
|
||||
" wasInTimedCast=", wasInTimedCast,
|
||||
|
|
@ -991,6 +999,13 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
|
|||
castSpell(nextSpell, nextTarget);
|
||||
}
|
||||
} else {
|
||||
// For non-player casters: if no tracked cast state exists, this was an
|
||||
// instant cast — play a brief one-shot spell animation before stopping
|
||||
auto castIt = unitCastStates_.find(data.casterUnit);
|
||||
bool wasTrackedCast = (castIt != unitCastStates_.end());
|
||||
if (!wasTrackedCast && owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(data.casterUnit, true, false);
|
||||
}
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(data.casterUnit, false, false);
|
||||
}
|
||||
|
|
@ -1181,6 +1196,26 @@ void SpellHandler::handleAuraUpdate(network::Packet& packet, bool isAll) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sprint aura detection — check if any sprint/dash speed buff is active
|
||||
if (data.guid == owner_.playerGuid && owner_.sprintAuraCallback_) {
|
||||
static const uint32_t sprintSpells[] = {
|
||||
2983, 8696, 11305, // Rogue Sprint (ranks 1-3)
|
||||
1850, 9821, 33357, // Druid Dash (ranks 1-3)
|
||||
36554, // Shadowstep (speed component)
|
||||
68992, 68991, // Darkflight (worgen racial)
|
||||
58984, // Aspect of the Pack speed
|
||||
};
|
||||
bool hasSprint = false;
|
||||
for (const auto& a : playerAuras_) {
|
||||
if (a.isEmpty()) continue;
|
||||
for (uint32_t sid : sprintSpells) {
|
||||
if (a.spellId == sid) { hasSprint = true; break; }
|
||||
}
|
||||
if (hasSprint) break;
|
||||
}
|
||||
owner_.sprintAuraCallback_(hasSprint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2222,7 +2257,7 @@ void SpellHandler::handleSpellLogMiss(network::Packet& packet) {
|
|||
// TBC: spellId(4) + uint64 caster + uint8 unk + uint32 count
|
||||
// + count × (uint64 victim + uint8 missInfo)
|
||||
// All expansions append uint32 reflectSpellId + uint8 reflectResult when
|
||||
// missInfo==11 (REFLECT).
|
||||
// missInfo==REFLECT (11).
|
||||
const bool spellMissUsesFullGuid = isActiveExpansion("tbc");
|
||||
auto readSpellMissGuid = [&]() -> uint64_t {
|
||||
if (spellMissUsesFullGuid)
|
||||
|
|
@ -2248,7 +2283,7 @@ void SpellHandler::handleSpellLogMiss(network::Packet& packet) {
|
|||
struct SpellMissLogEntry {
|
||||
uint64_t victimGuid = 0;
|
||||
uint8_t missInfo = 0;
|
||||
uint32_t reflectSpellId = 0; // Only valid when missInfo==11 (REFLECT)
|
||||
uint32_t reflectSpellId = 0; // Only valid when missInfo==REFLECT
|
||||
};
|
||||
std::vector<SpellMissLogEntry> parsedMisses;
|
||||
parsedMisses.reserve(storedLimit);
|
||||
|
|
@ -2266,9 +2301,9 @@ void SpellHandler::handleSpellLogMiss(network::Packet& packet) {
|
|||
return;
|
||||
}
|
||||
const uint8_t missInfo = packet.readUInt8();
|
||||
// REFLECT (11): extra uint32 reflectSpellId + uint8 reflectResult
|
||||
// REFLECT: extra uint32 reflectSpellId + uint8 reflectResult
|
||||
uint32_t reflectSpellId = 0;
|
||||
if (missInfo == 11) {
|
||||
if (missInfo == SpellMissInfo::REFLECT) {
|
||||
if (packet.hasRemaining(5)) {
|
||||
reflectSpellId = packet.readUInt32();
|
||||
/*uint8_t reflectResult =*/ packet.readUInt8();
|
||||
|
|
@ -2912,7 +2947,7 @@ void SpellHandler::handleSpellLogExecute(network::Packet& packet) {
|
|||
uint8_t effectType = packet.readUInt8();
|
||||
uint32_t effectLogCount = packet.readUInt32();
|
||||
effectLogCount = std::min(effectLogCount, 64u); // sanity
|
||||
if (effectType == 10) {
|
||||
if (effectType == SpellEffect::POWER_DRAIN) {
|
||||
// SPELL_EFFECT_POWER_DRAIN: packed_guid target + uint32 amount + uint32 powerType + float multiplier
|
||||
for (uint32_t li = 0; li < effectLogCount; ++li) {
|
||||
if (!packet.hasRemaining(exeUsesFullGuid ? 8u : 1u)
|
||||
|
|
@ -2950,7 +2985,7 @@ void SpellHandler::handleSpellLogExecute(network::Packet& packet) {
|
|||
" power=", drainPower, " amount=", drainAmount,
|
||||
" multiplier=", drainMult);
|
||||
}
|
||||
} else if (effectType == 11) {
|
||||
} else if (effectType == SpellEffect::HEALTH_LEECH) {
|
||||
// SPELL_EFFECT_HEALTH_LEECH: packed_guid target + uint32 amount + float multiplier
|
||||
for (uint32_t li = 0; li < effectLogCount; ++li) {
|
||||
if (!packet.hasRemaining(exeUsesFullGuid ? 8u : 1u)
|
||||
|
|
@ -2983,7 +3018,7 @@ void SpellHandler::handleSpellLogExecute(network::Packet& packet) {
|
|||
LOG_DEBUG("SMSG_SPELLLOGEXECUTE HEALTH_LEECH: spell=", exeSpellId,
|
||||
" amount=", leechAmount, " multiplier=", leechMult);
|
||||
}
|
||||
} else if (effectType == 24 || effectType == 114) {
|
||||
} else if (effectType == SpellEffect::CREATE_ITEM || effectType == SpellEffect::CREATE_ITEM2) {
|
||||
// SPELL_EFFECT_CREATE_ITEM / CREATE_ITEM2: uint32 itemEntry per log entry
|
||||
for (uint32_t li = 0; li < effectLogCount; ++li) {
|
||||
if (!packet.hasRemaining(4)) break;
|
||||
|
|
@ -3012,7 +3047,7 @@ void SpellHandler::handleSpellLogExecute(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else if (effectType == 26) {
|
||||
} else if (effectType == SpellEffect::INTERRUPT_CAST) {
|
||||
// SPELL_EFFECT_INTERRUPT_CAST: packed_guid target + uint32 interrupted_spell_id
|
||||
for (uint32_t li = 0; li < effectLogCount; ++li) {
|
||||
if (!packet.hasRemaining(exeUsesFullGuid ? 8u : 1u)
|
||||
|
|
@ -3033,7 +3068,7 @@ void SpellHandler::handleSpellLogExecute(network::Packet& packet) {
|
|||
LOG_DEBUG("SMSG_SPELLLOGEXECUTE INTERRUPT_CAST: spell=", exeSpellId,
|
||||
" interrupted=", icSpellId, " target=0x", std::hex, icTarget, std::dec);
|
||||
}
|
||||
} else if (effectType == 49) {
|
||||
} else if (effectType == SpellEffect::FEED_PET) {
|
||||
// SPELL_EFFECT_FEED_PET: uint32 itemEntry per log entry
|
||||
for (uint32_t li = 0; li < effectLogCount; ++li) {
|
||||
if (!packet.hasRemaining(4)) break;
|
||||
|
|
@ -3182,6 +3217,12 @@ void SpellHandler::handleChannelStart(network::Packet& packet) {
|
|||
}
|
||||
LOG_DEBUG("MSG_CHANNEL_START: caster=0x", std::hex, chanCaster, std::dec,
|
||||
" spell=", chanSpellId, " total=", chanTotalMs, "ms");
|
||||
|
||||
// Play channeling animation (looping)
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(chanCaster, true, true);
|
||||
}
|
||||
|
||||
// Fire UNIT_SPELLCAST_CHANNEL_START for Lua addons
|
||||
if (owner_.addonEventCallback_) {
|
||||
auto unitId = owner_.guidToUnitId(chanCaster);
|
||||
|
|
@ -3217,6 +3258,10 @@ void SpellHandler::handleChannelUpdate(network::Packet& packet) {
|
|||
" remaining=", chanRemainMs, "ms");
|
||||
// Fire UNIT_SPELLCAST_CHANNEL_STOP when channel ends
|
||||
if (chanRemainMs == 0) {
|
||||
// Stop channeling animation — return to idle
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(chanCaster2, false, true);
|
||||
}
|
||||
auto unitId = owner_.guidToUnitId(chanCaster2);
|
||||
if (!unitId.empty())
|
||||
owner_.fireAddonEvent("UNIT_SPELLCAST_CHANNEL_STOP", {unitId});
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -79,7 +79,9 @@ bool DBCFile::load(const std::vector<uint8_t>& dbcData) {
|
|||
const uint8_t* recordStart = dbcData.data() + sizeof(DBCHeader);
|
||||
uint32_t totalRecordSize = recordCount * recordSize;
|
||||
recordData.resize(totalRecordSize);
|
||||
std::memcpy(recordData.data(), recordStart, totalRecordSize);
|
||||
if (totalRecordSize > 0) {
|
||||
std::memcpy(recordData.data(), recordStart, totalRecordSize);
|
||||
}
|
||||
|
||||
// Copy string block
|
||||
const uint8_t* stringStart = recordStart + totalRecordSize;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
567
src/rendering/animation_ids.cpp
Normal file
567
src/rendering/animation_ids.cpp
Normal file
|
|
@ -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 <unordered_set>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
namespace anim {
|
||||
|
||||
const char* nameFromId(uint32_t id) {
|
||||
static const char* const names[ANIM_COUNT] = {
|
||||
/* 0 */ "STAND",
|
||||
/* 1 */ "DEATH",
|
||||
/* 2 */ "SPELL",
|
||||
/* 3 */ "STOP",
|
||||
/* 4 */ "WALK",
|
||||
/* 5 */ "RUN",
|
||||
/* 6 */ "DEAD",
|
||||
/* 7 */ "RISE",
|
||||
/* 8 */ "STAND_WOUND",
|
||||
/* 9 */ "COMBAT_WOUND",
|
||||
/* 10 */ "COMBAT_CRITICAL",
|
||||
/* 11 */ "SHUFFLE_LEFT",
|
||||
/* 12 */ "SHUFFLE_RIGHT",
|
||||
/* 13 */ "WALK_BACKWARDS",
|
||||
/* 14 */ "STUN",
|
||||
/* 15 */ "HANDS_CLOSED",
|
||||
/* 16 */ "ATTACK_UNARMED",
|
||||
/* 17 */ "ATTACK_1H",
|
||||
/* 18 */ "ATTACK_2H",
|
||||
/* 19 */ "ATTACK_2H_LOOSE",
|
||||
/* 20 */ "PARRY_UNARMED",
|
||||
/* 21 */ "PARRY_1H",
|
||||
/* 22 */ "PARRY_2H",
|
||||
/* 23 */ "PARRY_2H_LOOSE",
|
||||
/* 24 */ "SHIELD_BLOCK",
|
||||
/* 25 */ "READY_UNARMED",
|
||||
/* 26 */ "READY_1H",
|
||||
/* 27 */ "READY_2H",
|
||||
/* 28 */ "READY_2H_LOOSE",
|
||||
/* 29 */ "READY_BOW",
|
||||
/* 30 */ "DODGE",
|
||||
/* 31 */ "SPELL_PRECAST",
|
||||
/* 32 */ "SPELL_CAST",
|
||||
/* 33 */ "SPELL_CAST_AREA",
|
||||
/* 34 */ "NPC_WELCOME",
|
||||
/* 35 */ "NPC_GOODBYE",
|
||||
/* 36 */ "BLOCK",
|
||||
/* 37 */ "JUMP_START",
|
||||
/* 38 */ "JUMP",
|
||||
/* 39 */ "JUMP_END",
|
||||
/* 40 */ "FALL",
|
||||
/* 41 */ "SWIM_IDLE",
|
||||
/* 42 */ "SWIM",
|
||||
/* 43 */ "SWIM_LEFT",
|
||||
/* 44 */ "SWIM_RIGHT",
|
||||
/* 45 */ "SWIM_BACKWARDS",
|
||||
/* 46 */ "ATTACK_BOW",
|
||||
/* 47 */ "FIRE_BOW",
|
||||
/* 48 */ "READY_RIFLE",
|
||||
/* 49 */ "ATTACK_RIFLE",
|
||||
/* 50 */ "LOOT",
|
||||
/* 51 */ "READY_SPELL_DIRECTED",
|
||||
/* 52 */ "READY_SPELL_OMNI",
|
||||
/* 53 */ "SPELL_CAST_DIRECTED",
|
||||
/* 54 */ "SPELL_CAST_OMNI",
|
||||
/* 55 */ "BATTLE_ROAR",
|
||||
/* 56 */ "READY_ABILITY",
|
||||
/* 57 */ "SPECIAL_1H",
|
||||
/* 58 */ "SPECIAL_2H",
|
||||
/* 59 */ "SHIELD_BASH",
|
||||
/* 60 */ "EMOTE_TALK",
|
||||
/* 61 */ "EMOTE_EAT",
|
||||
/* 62 */ "EMOTE_WORK",
|
||||
/* 63 */ "EMOTE_USE_STANDING",
|
||||
/* 64 */ "EMOTE_EXCLAMATION",
|
||||
/* 65 */ "EMOTE_QUESTION",
|
||||
/* 66 */ "EMOTE_BOW",
|
||||
/* 67 */ "EMOTE_WAVE",
|
||||
/* 68 */ "EMOTE_CHEER",
|
||||
/* 69 */ "EMOTE_DANCE",
|
||||
/* 70 */ "EMOTE_LAUGH",
|
||||
/* 71 */ "EMOTE_SLEEP",
|
||||
/* 72 */ "EMOTE_SIT_GROUND",
|
||||
/* 73 */ "EMOTE_RUDE",
|
||||
/* 74 */ "EMOTE_ROAR",
|
||||
/* 75 */ "EMOTE_KNEEL",
|
||||
/* 76 */ "EMOTE_KISS",
|
||||
/* 77 */ "EMOTE_CRY",
|
||||
/* 78 */ "EMOTE_CHICKEN",
|
||||
/* 79 */ "EMOTE_BEG",
|
||||
/* 80 */ "EMOTE_APPLAUD",
|
||||
/* 81 */ "EMOTE_SHOUT",
|
||||
/* 82 */ "EMOTE_FLEX",
|
||||
/* 83 */ "EMOTE_SHY",
|
||||
/* 84 */ "EMOTE_POINT",
|
||||
/* 85 */ "ATTACK_1H_PIERCE",
|
||||
/* 86 */ "ATTACK_2H_LOOSE_PIERCE",
|
||||
/* 87 */ "ATTACK_OFF",
|
||||
/* 88 */ "ATTACK_OFF_PIERCE",
|
||||
/* 89 */ "SHEATHE",
|
||||
/* 90 */ "HIP_SHEATHE",
|
||||
/* 91 */ "MOUNT",
|
||||
/* 92 */ "RUN_RIGHT",
|
||||
/* 93 */ "RUN_LEFT",
|
||||
/* 94 */ "MOUNT_SPECIAL",
|
||||
/* 95 */ "KICK",
|
||||
/* 96 */ "SIT_GROUND_DOWN",
|
||||
/* 97 */ "SITTING",
|
||||
/* 98 */ "SIT_GROUND_UP",
|
||||
/* 99 */ "SLEEP_DOWN",
|
||||
/* 100 */ "SLEEP",
|
||||
/* 101 */ "SLEEP_UP",
|
||||
/* 102 */ "SIT_CHAIR_LOW",
|
||||
/* 103 */ "SIT_CHAIR_MED",
|
||||
/* 104 */ "SIT_CHAIR_HIGH",
|
||||
/* 105 */ "LOAD_BOW",
|
||||
/* 106 */ "LOAD_RIFLE",
|
||||
/* 107 */ "ATTACK_THROWN",
|
||||
/* 108 */ "READY_THROWN",
|
||||
/* 109 */ "HOLD_BOW",
|
||||
/* 110 */ "HOLD_RIFLE",
|
||||
/* 111 */ "HOLD_THROWN",
|
||||
/* 112 */ "LOAD_THROWN",
|
||||
/* 113 */ "EMOTE_SALUTE",
|
||||
/* 114 */ "KNEEL_START",
|
||||
/* 115 */ "KNEEL_LOOP",
|
||||
/* 116 */ "KNEEL_END",
|
||||
/* 117 */ "ATTACK_UNARMED_OFF",
|
||||
/* 118 */ "SPECIAL_UNARMED",
|
||||
/* 119 */ "STEALTH_WALK",
|
||||
/* 120 */ "STEALTH_STAND",
|
||||
/* 121 */ "KNOCKDOWN",
|
||||
/* 122 */ "EATING_LOOP",
|
||||
/* 123 */ "USE_STANDING_LOOP",
|
||||
/* 124 */ "CHANNEL_CAST_DIRECTED",
|
||||
/* 125 */ "CHANNEL_CAST_OMNI",
|
||||
/* 126 */ "WHIRLWIND",
|
||||
/* 127 */ "BIRTH",
|
||||
/* 128 */ "USE_STANDING_START",
|
||||
/* 129 */ "USE_STANDING_END",
|
||||
/* 130 */ "CREATURE_SPECIAL",
|
||||
/* 131 */ "DROWN",
|
||||
/* 132 */ "DROWNED",
|
||||
/* 133 */ "FISHING_CAST",
|
||||
/* 134 */ "FISHING_LOOP",
|
||||
/* 135 */ "FLY",
|
||||
/* 136 */ "EMOTE_WORK_NO_SHEATHE",
|
||||
/* 137 */ "EMOTE_STUN_NO_SHEATHE",
|
||||
/* 138 */ "EMOTE_USE_STANDING_NO_SHEATHE",
|
||||
/* 139 */ "SPELL_SLEEP_DOWN",
|
||||
/* 140 */ "SPELL_KNEEL_START",
|
||||
/* 141 */ "SPELL_KNEEL_LOOP",
|
||||
/* 142 */ "SPELL_KNEEL_END",
|
||||
/* 143 */ "SPRINT",
|
||||
/* 144 */ "IN_FLIGHT",
|
||||
/* 145 */ "SPAWN",
|
||||
/* 146 */ "CLOSE",
|
||||
/* 147 */ "CLOSED",
|
||||
/* 148 */ "OPEN",
|
||||
/* 149 */ "DESTROY",
|
||||
/* 150 */ "DESTROYED",
|
||||
/* 151 */ "UNSHEATHE",
|
||||
/* 152 */ "SHEATHE_ALT",
|
||||
/* 153 */ "ATTACK_UNARMED_NO_SHEATHE",
|
||||
/* 154 */ "STEALTH_RUN",
|
||||
/* 155 */ "READY_CROSSBOW",
|
||||
/* 156 */ "ATTACK_CROSSBOW",
|
||||
/* 157 */ "EMOTE_TALK_EXCLAMATION",
|
||||
/* 158 */ "FLY_IDLE",
|
||||
/* 159 */ "FLY_FORWARD",
|
||||
/* 160 */ "FLY_BACKWARDS",
|
||||
/* 161 */ "FLY_LEFT",
|
||||
/* 162 */ "FLY_RIGHT",
|
||||
/* 163 */ "FLY_UP",
|
||||
/* 164 */ "FLY_DOWN",
|
||||
/* 165 */ "FLY_LAND_START",
|
||||
/* 166 */ "FLY_LAND_RUN",
|
||||
/* 167 */ "FLY_LAND_END",
|
||||
/* 168 */ "EMOTE_TALK_QUESTION",
|
||||
/* 169 */ "EMOTE_READ",
|
||||
/* 170 */ "EMOTE_SHIELDBLOCK",
|
||||
/* 171 */ "EMOTE_CHOP",
|
||||
/* 172 */ "EMOTE_HOLDRIFLE",
|
||||
/* 173 */ "EMOTE_HOLDBOW",
|
||||
/* 174 */ "EMOTE_HOLDTHROWN",
|
||||
/* 175 */ "CUSTOM_SPELL_02",
|
||||
/* 176 */ "CUSTOM_SPELL_03",
|
||||
/* 177 */ "CUSTOM_SPELL_04",
|
||||
/* 178 */ "CUSTOM_SPELL_05",
|
||||
/* 179 */ "CUSTOM_SPELL_06",
|
||||
/* 180 */ "CUSTOM_SPELL_07",
|
||||
/* 181 */ "CUSTOM_SPELL_08",
|
||||
/* 182 */ "CUSTOM_SPELL_09",
|
||||
/* 183 */ "CUSTOM_SPELL_10",
|
||||
/* 184 */ "EMOTE_STATE_DANCE",
|
||||
/* 185 */ "FLY_STAND",
|
||||
/* 186 */ "EMOTE_STATE_LAUGH",
|
||||
/* 187 */ "EMOTE_STATE_POINT",
|
||||
/* 188 */ "EMOTE_STATE_EAT",
|
||||
/* 189 */ "EMOTE_STATE_WORK",
|
||||
/* 190 */ "EMOTE_STATE_SIT_GROUND",
|
||||
/* 191 */ "EMOTE_STATE_HOLD_BOW",
|
||||
/* 192 */ "EMOTE_STATE_HOLD_RIFLE",
|
||||
/* 193 */ "EMOTE_STATE_HOLD_THROWN",
|
||||
/* 194 */ "FLY_COMBAT_WOUND",
|
||||
/* 195 */ "FLY_COMBAT_CRITICAL",
|
||||
/* 196 */ "RECLINED",
|
||||
/* 197 */ "EMOTE_STATE_ROAR",
|
||||
/* 198 */ "EMOTE_USE_STANDING_LOOP_2",
|
||||
/* 199 */ "EMOTE_STATE_APPLAUD",
|
||||
/* 200 */ "READY_FIST",
|
||||
/* 201 */ "SPELL_CHANNEL_DIRECTED_OMNI",
|
||||
/* 202 */ "SPECIAL_ATTACK_1H_OFF",
|
||||
/* 203 */ "ATTACK_FIST_1H",
|
||||
/* 204 */ "ATTACK_FIST_1H_OFF",
|
||||
/* 205 */ "PARRY_FIST_1H",
|
||||
/* 206 */ "READY_FIST_1H",
|
||||
/* 207 */ "EMOTE_STATE_READ_AND_TALK",
|
||||
/* 208 */ "EMOTE_STATE_WORK_NO_SHEATHE",
|
||||
/* 209 */ "FLY_RUN",
|
||||
/* 210 */ "EMOTE_STATE_KNEEL_2",
|
||||
/* 211 */ "EMOTE_STATE_SPELL_KNEEL",
|
||||
/* 212 */ "EMOTE_STATE_USE_STANDING",
|
||||
/* 213 */ "EMOTE_STATE_STUN",
|
||||
/* 214 */ "EMOTE_STATE_STUN_NO_SHEATHE",
|
||||
/* 215 */ "EMOTE_TRAIN",
|
||||
/* 216 */ "EMOTE_DEAD",
|
||||
/* 217 */ "EMOTE_STATE_DANCE_ONCE",
|
||||
/* 218 */ "FLY_DEATH",
|
||||
/* 219 */ "FLY_STAND_WOUND",
|
||||
/* 220 */ "FLY_SHUFFLE_LEFT",
|
||||
/* 221 */ "FLY_SHUFFLE_RIGHT",
|
||||
/* 222 */ "FLY_WALK_BACKWARDS",
|
||||
/* 223 */ "FLY_STUN",
|
||||
/* 224 */ "FLY_HANDS_CLOSED",
|
||||
/* 225 */ "FLY_ATTACK_UNARMED",
|
||||
/* 226 */ "FLY_ATTACK_1H",
|
||||
/* 227 */ "FLY_ATTACK_2H",
|
||||
/* 228 */ "FLY_ATTACK_2H_LOOSE",
|
||||
/* 229 */ "FLY_SPELL",
|
||||
/* 230 */ "FLY_STOP",
|
||||
/* 231 */ "FLY_WALK",
|
||||
/* 232 */ "FLY_DEAD",
|
||||
/* 233 */ "FLY_RISE",
|
||||
/* 234 */ "FLY_RUN_2",
|
||||
/* 235 */ "FLY_FALL",
|
||||
/* 236 */ "FLY_SWIM_IDLE",
|
||||
/* 237 */ "FLY_SWIM",
|
||||
/* 238 */ "FLY_SWIM_LEFT",
|
||||
/* 239 */ "FLY_SWIM_RIGHT",
|
||||
/* 240 */ "FLY_SWIM_BACKWARDS",
|
||||
/* 241 */ "FLY_ATTACK_BOW",
|
||||
/* 242 */ "FLY_FIRE_BOW",
|
||||
/* 243 */ "FLY_READY_RIFLE",
|
||||
/* 244 */ "FLY_ATTACK_RIFLE",
|
||||
/* 245 */ "TOTEM_SMALL",
|
||||
/* 246 */ "TOTEM_MEDIUM",
|
||||
/* 247 */ "TOTEM_LARGE",
|
||||
/* 248 */ "FLY_LOOT",
|
||||
/* 249 */ "FLY_READY_SPELL_DIRECTED",
|
||||
/* 250 */ "FLY_READY_SPELL_OMNI",
|
||||
/* 251 */ "FLY_SPELL_CAST_DIRECTED",
|
||||
/* 252 */ "FLY_SPELL_CAST_OMNI",
|
||||
/* 253 */ "FLY_BATTLE_ROAR",
|
||||
/* 254 */ "FLY_READY_ABILITY",
|
||||
/* 255 */ "FLY_SPECIAL_1H",
|
||||
/* 256 */ "FLY_SPECIAL_2H",
|
||||
/* 257 */ "FLY_SHIELD_BASH",
|
||||
/* 258 */ "FLY_EMOTE_TALK",
|
||||
/* 259 */ "FLY_EMOTE_EAT",
|
||||
/* 260 */ "FLY_EMOTE_WORK",
|
||||
/* 261 */ "FLY_EMOTE_USE_STANDING",
|
||||
/* 262 */ "FLY_EMOTE_BOW",
|
||||
/* 263 */ "FLY_EMOTE_WAVE",
|
||||
/* 264 */ "FLY_EMOTE_CHEER",
|
||||
/* 265 */ "FLY_EMOTE_DANCE",
|
||||
/* 266 */ "FLY_EMOTE_LAUGH",
|
||||
/* 267 */ "FLY_EMOTE_SLEEP",
|
||||
/* 268 */ "FLY_EMOTE_SIT_GROUND",
|
||||
/* 269 */ "FLY_EMOTE_RUDE",
|
||||
/* 270 */ "FLY_EMOTE_ROAR",
|
||||
/* 271 */ "FLY_EMOTE_KNEEL",
|
||||
/* 272 */ "FLY_EMOTE_KISS",
|
||||
/* 273 */ "FLY_EMOTE_CRY",
|
||||
/* 274 */ "FLY_EMOTE_CHICKEN",
|
||||
/* 275 */ "FLY_EMOTE_BEG",
|
||||
/* 276 */ "FLY_EMOTE_APPLAUD",
|
||||
/* 277 */ "FLY_EMOTE_SHOUT",
|
||||
/* 278 */ "FLY_EMOTE_FLEX",
|
||||
/* 279 */ "FLY_EMOTE_SHY",
|
||||
/* 280 */ "FLY_EMOTE_POINT",
|
||||
/* 281 */ "FLY_ATTACK_1H_PIERCE",
|
||||
/* 282 */ "FLY_ATTACK_2H_LOOSE_PIERCE",
|
||||
/* 283 */ "FLY_ATTACK_OFF",
|
||||
/* 284 */ "FLY_ATTACK_OFF_PIERCE",
|
||||
/* 285 */ "FLY_SHEATHE",
|
||||
/* 286 */ "FLY_HIP_SHEATHE",
|
||||
/* 287 */ "FLY_MOUNT",
|
||||
/* 288 */ "FLY_RUN_RIGHT",
|
||||
/* 289 */ "FLY_RUN_LEFT",
|
||||
/* 290 */ "FLY_MOUNT_SPECIAL",
|
||||
/* 291 */ "FLY_KICK",
|
||||
/* 292 */ "FLY_SIT_GROUND_DOWN",
|
||||
/* 293 */ "FLY_SITTING",
|
||||
/* 294 */ "FLY_SIT_GROUND_UP",
|
||||
/* 295 */ "FLY_SLEEP_DOWN",
|
||||
/* 296 */ "FLY_SLEEP",
|
||||
/* 297 */ "FLY_SLEEP_UP",
|
||||
/* 298 */ "FLY_SIT_CHAIR_LOW",
|
||||
/* 299 */ "FLY_SIT_CHAIR_MED",
|
||||
/* 300 */ "FLY_SIT_CHAIR_HIGH",
|
||||
/* 301 */ "FLY_LOAD_BOW",
|
||||
/* 302 */ "FLY_LOAD_RIFLE",
|
||||
/* 303 */ "FLY_ATTACK_THROWN",
|
||||
/* 304 */ "FLY_READY_THROWN",
|
||||
/* 305 */ "FLY_HOLD_BOW",
|
||||
/* 306 */ "FLY_HOLD_RIFLE",
|
||||
/* 307 */ "FLY_HOLD_THROWN",
|
||||
/* 308 */ "FLY_LOAD_THROWN",
|
||||
/* 309 */ "FLY_EMOTE_SALUTE",
|
||||
/* 310 */ "FLY_KNEEL_START",
|
||||
/* 311 */ "FLY_KNEEL_LOOP",
|
||||
/* 312 */ "FLY_KNEEL_END",
|
||||
/* 313 */ "FLY_ATTACK_UNARMED_OFF",
|
||||
/* 314 */ "FLY_SPECIAL_UNARMED",
|
||||
/* 315 */ "FLY_STEALTH_WALK",
|
||||
/* 316 */ "FLY_STEALTH_STAND",
|
||||
/* 317 */ "FLY_KNOCKDOWN",
|
||||
/* 318 */ "FLY_EATING_LOOP",
|
||||
/* 319 */ "FLY_USE_STANDING_LOOP",
|
||||
/* 320 */ "FLY_CHANNEL_CAST_DIRECTED",
|
||||
/* 321 */ "FLY_CHANNEL_CAST_OMNI",
|
||||
/* 322 */ "FLY_WHIRLWIND",
|
||||
/* 323 */ "FLY_BIRTH",
|
||||
/* 324 */ "FLY_USE_STANDING_START",
|
||||
/* 325 */ "FLY_USE_STANDING_END",
|
||||
/* 326 */ "FLY_CREATURE_SPECIAL",
|
||||
/* 327 */ "FLY_DROWN",
|
||||
/* 328 */ "FLY_DROWNED",
|
||||
/* 329 */ "FLY_FISHING_CAST",
|
||||
/* 330 */ "FLY_FISHING_LOOP",
|
||||
/* 331 */ "FLY_FLY",
|
||||
/* 332 */ "FLY_EMOTE_WORK_NO_SHEATHE",
|
||||
/* 333 */ "FLY_EMOTE_STUN_NO_SHEATHE",
|
||||
/* 334 */ "FLY_EMOTE_USE_STANDING_NO_SHEATHE",
|
||||
/* 335 */ "FLY_SPELL_SLEEP_DOWN",
|
||||
/* 336 */ "FLY_SPELL_KNEEL_START",
|
||||
/* 337 */ "FLY_SPELL_KNEEL_LOOP",
|
||||
/* 338 */ "FLY_SPELL_KNEEL_END",
|
||||
/* 339 */ "FLY_SPRINT",
|
||||
/* 340 */ "FLY_IN_FLIGHT",
|
||||
/* 341 */ "FLY_SPAWN",
|
||||
/* 342 */ "FLY_CLOSE",
|
||||
/* 343 */ "FLY_CLOSED",
|
||||
/* 344 */ "FLY_OPEN",
|
||||
/* 345 */ "FLY_DESTROY",
|
||||
/* 346 */ "FLY_DESTROYED",
|
||||
/* 347 */ "FLY_UNSHEATHE",
|
||||
/* 348 */ "FLY_SHEATHE_ALT",
|
||||
/* 349 */ "FLY_ATTACK_UNARMED_NO_SHEATHE",
|
||||
/* 350 */ "FLY_STEALTH_RUN",
|
||||
/* 351 */ "FLY_READY_CROSSBOW",
|
||||
/* 352 */ "FLY_ATTACK_CROSSBOW",
|
||||
/* 353 */ "FLY_EMOTE_TALK_EXCLAMATION",
|
||||
/* 354 */ "FLY_EMOTE_TALK_QUESTION",
|
||||
/* 355 */ "FLY_EMOTE_READ",
|
||||
/* 356 */ "EMOTE_HOLD_CROSSBOW",
|
||||
/* 357 */ "FLY_EMOTE_HOLD_BOW",
|
||||
/* 358 */ "FLY_EMOTE_HOLD_RIFLE",
|
||||
/* 359 */ "FLY_EMOTE_HOLD_THROWN",
|
||||
/* 360 */ "FLY_EMOTE_HOLD_CROSSBOW",
|
||||
/* 361 */ "FLY_CUSTOM_SPELL_02",
|
||||
/* 362 */ "FLY_CUSTOM_SPELL_03",
|
||||
/* 363 */ "FLY_CUSTOM_SPELL_04",
|
||||
/* 364 */ "FLY_CUSTOM_SPELL_05",
|
||||
/* 365 */ "FLY_CUSTOM_SPELL_06",
|
||||
/* 366 */ "FLY_CUSTOM_SPELL_07",
|
||||
/* 367 */ "FLY_CUSTOM_SPELL_08",
|
||||
/* 368 */ "FLY_CUSTOM_SPELL_09",
|
||||
/* 369 */ "FLY_CUSTOM_SPELL_10",
|
||||
/* 370 */ "FLY_EMOTE_STATE_DANCE",
|
||||
/* 371 */ "EMOTE_EAT_NO_SHEATHE",
|
||||
/* 372 */ "MOUNT_RUN_RIGHT",
|
||||
/* 373 */ "MOUNT_RUN_LEFT",
|
||||
/* 374 */ "MOUNT_WALK_BACKWARDS",
|
||||
/* 375 */ "MOUNT_SWIM_IDLE",
|
||||
/* 376 */ "MOUNT_SWIM",
|
||||
/* 377 */ "MOUNT_SWIM_LEFT",
|
||||
/* 378 */ "MOUNT_SWIM_RIGHT",
|
||||
/* 379 */ "MOUNT_SWIM_BACKWARDS",
|
||||
/* 380 */ "MOUNT_FLIGHT_IDLE",
|
||||
/* 381 */ "MOUNT_FLIGHT_FORWARD",
|
||||
/* 382 */ "MOUNT_FLIGHT_BACKWARDS",
|
||||
/* 383 */ "MOUNT_FLIGHT_LEFT",
|
||||
/* 384 */ "MOUNT_FLIGHT_RIGHT",
|
||||
/* 385 */ "MOUNT_FLIGHT_UP",
|
||||
/* 386 */ "MOUNT_FLIGHT_DOWN",
|
||||
/* 387 */ "MOUNT_FLIGHT_LAND_START",
|
||||
/* 388 */ "MOUNT_FLIGHT_LAND_RUN",
|
||||
/* 389 */ "MOUNT_FLIGHT_LAND_END",
|
||||
/* 390 */ "FLY_EMOTE_STATE_LAUGH",
|
||||
/* 391 */ "FLY_EMOTE_STATE_POINT",
|
||||
/* 392 */ "FLY_EMOTE_STATE_EAT",
|
||||
/* 393 */ "FLY_EMOTE_STATE_WORK",
|
||||
/* 394 */ "FLY_EMOTE_STATE_SIT_GROUND",
|
||||
/* 395 */ "FLY_EMOTE_STATE_HOLD_BOW",
|
||||
/* 396 */ "FLY_EMOTE_STATE_HOLD_RIFLE",
|
||||
/* 397 */ "FLY_EMOTE_STATE_HOLD_THROWN",
|
||||
/* 398 */ "FLY_EMOTE_STATE_ROAR",
|
||||
/* 399 */ "FLY_RECLINED",
|
||||
/* 400 */ "EMOTE_TRAIN_2",
|
||||
/* 401 */ "EMOTE_DEAD_2",
|
||||
/* 402 */ "FLY_EMOTE_USE_STANDING_LOOP_2",
|
||||
/* 403 */ "FLY_EMOTE_STATE_APPLAUD",
|
||||
/* 404 */ "FLY_READY_FIST",
|
||||
/* 405 */ "FLY_SPELL_CHANNEL_DIRECTED_OMNI",
|
||||
/* 406 */ "FLY_SPECIAL_ATTACK_1H_OFF",
|
||||
/* 407 */ "FLY_ATTACK_FIST_1H",
|
||||
/* 408 */ "FLY_ATTACK_FIST_1H_OFF",
|
||||
/* 409 */ "FLY_PARRY_FIST_1H",
|
||||
/* 410 */ "FLY_READY_FIST_1H",
|
||||
/* 411 */ "FLY_EMOTE_STATE_READ_AND_TALK",
|
||||
/* 412 */ "FLY_EMOTE_STATE_WORK_NO_SHEATHE",
|
||||
/* 413 */ "FLY_EMOTE_STATE_KNEEL_2",
|
||||
/* 414 */ "FLY_EMOTE_STATE_SPELL_KNEEL",
|
||||
/* 415 */ "FLY_EMOTE_STATE_USE_STANDING",
|
||||
/* 416 */ "FLY_EMOTE_STATE_STUN",
|
||||
/* 417 */ "FLY_EMOTE_STATE_STUN_NO_SHEATHE",
|
||||
/* 418 */ "FLY_EMOTE_TRAIN",
|
||||
/* 419 */ "FLY_EMOTE_DEAD",
|
||||
/* 420 */ "FLY_EMOTE_STATE_DANCE_ONCE",
|
||||
/* 421 */ "FLY_EMOTE_EAT_NO_SHEATHE",
|
||||
/* 422 */ "FLY_MOUNT_RUN_RIGHT",
|
||||
/* 423 */ "FLY_MOUNT_RUN_LEFT",
|
||||
/* 424 */ "FLY_MOUNT_WALK_BACKWARDS",
|
||||
/* 425 */ "FLY_MOUNT_SWIM_IDLE",
|
||||
/* 426 */ "FLY_MOUNT_SWIM",
|
||||
/* 427 */ "FLY_MOUNT_SWIM_LEFT",
|
||||
/* 428 */ "FLY_MOUNT_SWIM_RIGHT",
|
||||
/* 429 */ "FLY_MOUNT_SWIM_BACKWARDS",
|
||||
/* 430 */ "FLY_MOUNT_FLIGHT_IDLE",
|
||||
/* 431 */ "FLY_MOUNT_FLIGHT_FORWARD",
|
||||
/* 432 */ "FLY_MOUNT_FLIGHT_BACKWARDS",
|
||||
/* 433 */ "FLY_MOUNT_FLIGHT_LEFT",
|
||||
/* 434 */ "FLY_MOUNT_FLIGHT_RIGHT",
|
||||
/* 435 */ "FLY_MOUNT_FLIGHT_UP",
|
||||
/* 436 */ "FLY_MOUNT_FLIGHT_DOWN",
|
||||
/* 437 */ "FLY_MOUNT_FLIGHT_LAND_START",
|
||||
/* 438 */ "FLY_MOUNT_FLIGHT_LAND_RUN",
|
||||
/* 439 */ "FLY_MOUNT_FLIGHT_LAND_END",
|
||||
/* 440 */ "FLY_TOTEM_SMALL",
|
||||
/* 441 */ "FLY_TOTEM_MEDIUM",
|
||||
/* 442 */ "FLY_TOTEM_LARGE",
|
||||
/* 443 */ "FLY_EMOTE_HOLD_CROSSBOW_2",
|
||||
/* 444 */ "VEHICLE_GRAB",
|
||||
/* 445 */ "VEHICLE_THROW",
|
||||
/* 446 */ "FLY_VEHICLE_GRAB",
|
||||
/* 447 */ "FLY_VEHICLE_THROW",
|
||||
/* 448 */ "GUILD_CHAMPION_1",
|
||||
/* 449 */ "GUILD_CHAMPION_2",
|
||||
/* 450 */ "FLY_GUILD_CHAMPION_1",
|
||||
/* 451 */ "FLY_GUILD_CHAMPION_2",
|
||||
};
|
||||
if (id < ANIM_COUNT) return names[id];
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
uint32_t flyVariant(uint32_t groundId) {
|
||||
// Compact lookup: ground animation ID (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<wowee::pipeline::DBCFile>& dbc) {
|
||||
if (!dbc || !dbc->isLoaded()) {
|
||||
LOG_WARNING("AnimationData.dbc not available — skipping animation ID validation");
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect all IDs present in the DBC (first field is the animation ID)
|
||||
std::unordered_set<uint32_t> dbcIds;
|
||||
for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) {
|
||||
uint32_t id = dbc->getUInt32(i, 0);
|
||||
dbcIds.insert(id);
|
||||
}
|
||||
|
||||
// Check: constants we define that are missing from DBC
|
||||
uint32_t missingInDbc = 0;
|
||||
for (uint32_t id = 0; id < ANIM_COUNT; ++id) {
|
||||
if (dbcIds.find(id) == dbcIds.end()) {
|
||||
LOG_WARNING("Animation ID ", id, " (", nameFromId(id),
|
||||
") defined in constants but missing from AnimationData.dbc");
|
||||
++missingInDbc;
|
||||
}
|
||||
}
|
||||
|
||||
// Check: DBC IDs beyond our constant range
|
||||
uint32_t extraInDbc = 0;
|
||||
for (uint32_t dbcId : dbcIds) {
|
||||
if (dbcId >= ANIM_COUNT) {
|
||||
++extraInDbc;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("AnimationData.dbc validation: ", dbc->getRecordCount(), " DBC records, ",
|
||||
ANIM_COUNT, " constants, ",
|
||||
missingInDbc, " missing from DBC, ",
|
||||
extraInDbc, " DBC-only IDs beyond constant range");
|
||||
}
|
||||
|
||||
} // namespace anim
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
* the original WoW Model Viewer (charcontrol.h, REGION_FAC=2).
|
||||
*/
|
||||
#include "rendering/character_renderer.hpp"
|
||||
#include "rendering/animation_ids.hpp"
|
||||
#include "rendering/vk_context.hpp"
|
||||
#include "rendering/vk_texture.hpp"
|
||||
#include "rendering/vk_pipeline.hpp"
|
||||
|
|
@ -34,6 +35,7 @@
|
|||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <future>
|
||||
#include <numeric>
|
||||
#include <thread>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
|
|
@ -261,7 +263,8 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram
|
|||
.setVertexInput({charBinding}, charAttrs)
|
||||
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
||||
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
||||
.setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS_OR_EQUAL)
|
||||
.setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS)
|
||||
.setDepthBias(0.0f, 0.0f)
|
||||
.setColorBlendAttachment(blendState)
|
||||
.setMultisample(samples);
|
||||
if (alphaToCoverage)
|
||||
|
|
@ -269,7 +272,7 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram
|
|||
return builder
|
||||
.setLayout(pipelineLayout_)
|
||||
.setRenderPass(mainPass)
|
||||
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
|
||||
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR, VK_DYNAMIC_STATE_DEPTH_BIAS})
|
||||
.build(device, vkCtx_->getPipelineCache());
|
||||
};
|
||||
|
||||
|
|
@ -1733,9 +1736,9 @@ void CharacterRenderer::update(float deltaTime, const glm::vec3& cameraPos) {
|
|||
inst.animationTime -= static_cast<float>(seq.duration);
|
||||
}
|
||||
} else {
|
||||
// One-shot animation finished: return to Stand (0) unless dead
|
||||
if (inst.currentAnimationId != 1 /*Death*/) {
|
||||
playAnimation(pair.first, 0, true);
|
||||
// One-shot animation finished: return to Stand unless dead
|
||||
if (inst.currentAnimationId != anim::DEATH) {
|
||||
playAnimation(pair.first, anim::STAND, true);
|
||||
} else {
|
||||
// Stay on last frame of death
|
||||
inst.animationTime = static_cast<float>(seq.duration);
|
||||
|
|
@ -2380,8 +2383,24 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
|
|||
return gpuModel.data.materials[b.materialIndex].blendMode;
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Sort batches by (priorityPlane, materialLayer) so equipment layers
|
||||
// render in the order the M2 format intends. priorityPlane separates
|
||||
// overlay effects; materialLayer orders coplanar body parts.
|
||||
std::vector<size_t> sortedBatchIndices(gpuModel.data.batches.size());
|
||||
std::iota(sortedBatchIndices.begin(), sortedBatchIndices.end(), 0);
|
||||
std::stable_sort(sortedBatchIndices.begin(), sortedBatchIndices.end(),
|
||||
[&](size_t a, size_t b) {
|
||||
const auto& ba = gpuModel.data.batches[a];
|
||||
const auto& bb = gpuModel.data.batches[b];
|
||||
if (ba.priorityPlane != bb.priorityPlane)
|
||||
return ba.priorityPlane < bb.priorityPlane;
|
||||
return ba.materialLayer < bb.materialLayer;
|
||||
});
|
||||
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
for (const auto& batch : gpuModel.data.batches) {
|
||||
for (size_t bi : sortedBatchIndices) {
|
||||
const auto& batch = gpuModel.data.batches[bi];
|
||||
uint16_t bm = getBatchBlendMode(batch);
|
||||
if (pass == 0 && bm != 0) continue; // pass 0: opaque only
|
||||
if (pass == 1 && bm == 0) continue; // pass 1: non-opaque only
|
||||
|
|
@ -2599,6 +2618,10 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
|
|||
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
pipelineLayout_, 1, 1, &materialSet, 0, nullptr);
|
||||
|
||||
// Per-batch depth bias from materialLayer to separate coplanar
|
||||
// armor pieces (chest/legs/gloves) that share identical depth.
|
||||
vkCmdSetDepthBias(cmd, static_cast<float>(batch.materialLayer) * 0.5f, 0.0f, 0.0f);
|
||||
|
||||
vkCmdDrawIndexed(cmd, batch.indexCount, 1, batch.indexStart, 0, 0);
|
||||
}
|
||||
} // end pass loop
|
||||
|
|
@ -3030,8 +3053,8 @@ void CharacterRenderer::moveInstanceTo(uint32_t instanceId, const glm::vec3& des
|
|||
// Stop at current location.
|
||||
inst.position = destination;
|
||||
inst.isMoving = false;
|
||||
if (inst.currentAnimationId == 4 || inst.currentAnimationId == 5) {
|
||||
playAnimation(instanceId, 0, true);
|
||||
if (inst.currentAnimationId == anim::WALK || inst.currentAnimationId == anim::RUN) {
|
||||
playAnimation(instanceId, anim::STAND, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -3509,7 +3532,8 @@ void CharacterRenderer::recreatePipelines() {
|
|||
.setVertexInput({charBinding}, charAttrs)
|
||||
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
||||
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
||||
.setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS_OR_EQUAL)
|
||||
.setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS)
|
||||
.setDepthBias(0.0f, 0.0f)
|
||||
.setColorBlendAttachment(blendState)
|
||||
.setMultisample(samples);
|
||||
if (alphaToCoverage)
|
||||
|
|
@ -3517,7 +3541,7 @@ void CharacterRenderer::recreatePipelines() {
|
|||
return builder
|
||||
.setLayout(pipelineLayout_)
|
||||
.setRenderPass(mainPass)
|
||||
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
|
||||
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR, VK_DYNAMIC_STATE_DEPTH_BIAS})
|
||||
.build(device, vkCtx_->getPipelineCache());
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4212,6 +4212,37 @@ void M2Renderer::setInstanceAnimationFrozen(uint32_t instanceId, bool frozen) {
|
|||
}
|
||||
}
|
||||
|
||||
void M2Renderer::setInstanceAnimation(uint32_t instanceId, uint32_t animationId, bool loop) {
|
||||
auto idxIt = instanceIndexById.find(instanceId);
|
||||
if (idxIt == instanceIndexById.end()) return;
|
||||
auto& inst = instances[idxIt->second];
|
||||
if (!inst.cachedModel) return;
|
||||
const auto& seqs = inst.cachedModel->sequences;
|
||||
// Find the first sequence matching the requested animation ID
|
||||
for (int i = 0; i < static_cast<int>(seqs.size()); ++i) {
|
||||
if (seqs[i].id == animationId) {
|
||||
inst.currentSequenceIndex = i;
|
||||
inst.animDuration = static_cast<float>(seqs[i].duration);
|
||||
inst.animTime = 0.0f;
|
||||
inst.animSpeed = 1.0f;
|
||||
// Use playingVariation=true for one-shot (returns to idle when done)
|
||||
inst.playingVariation = !loop;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool M2Renderer::hasAnimation(uint32_t instanceId, uint32_t animationId) const {
|
||||
auto idxIt = instanceIndexById.find(instanceId);
|
||||
if (idxIt == instanceIndexById.end()) return false;
|
||||
const auto& inst = instances[idxIt->second];
|
||||
if (!inst.cachedModel) return false;
|
||||
for (const auto& seq : inst.cachedModel->sequences) {
|
||||
if (seq.id == animationId) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
float M2Renderer::getInstanceAnimDuration(uint32_t instanceId) const {
|
||||
auto idxIt = instanceIndexById.find(instanceId);
|
||||
if (idxIt == instanceIndexById.end()) return 0.0f;
|
||||
|
|
|
|||
|
|
@ -250,13 +250,13 @@ bool Renderer::createPerFrameResources() {
|
|||
// --- Create descriptor pool for UBO + image sampler (normal frames + reflection) ---
|
||||
VkDescriptorPoolSize poolSizes[2]{};
|
||||
poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
||||
poolSizes[0].descriptorCount = MAX_FRAMES + 1; // +1 for reflection perFrame UBO
|
||||
poolSizes[0].descriptorCount = MAX_FRAMES * 2; // normal frames + reflection frames
|
||||
poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
poolSizes[1].descriptorCount = MAX_FRAMES + 1;
|
||||
poolSizes[1].descriptorCount = MAX_FRAMES * 2;
|
||||
|
||||
VkDescriptorPoolCreateInfo poolInfo{};
|
||||
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||
poolInfo.maxSets = MAX_FRAMES + 1; // +1 for reflection descriptor set
|
||||
poolInfo.maxSets = MAX_FRAMES * 2; // normal frames + reflection frames
|
||||
poolInfo.poolSizeCount = 2;
|
||||
poolInfo.pPoolSizes = poolSizes;
|
||||
|
||||
|
|
@ -344,42 +344,48 @@ bool Renderer::createPerFrameResources() {
|
|||
}
|
||||
reflPerFrameUBOMapped = mapInfo.pMappedData;
|
||||
|
||||
VkDescriptorSetLayout layouts[MAX_FRAMES];
|
||||
for (uint32_t i = 0; i < MAX_FRAMES; i++) layouts[i] = perFrameSetLayout;
|
||||
|
||||
VkDescriptorSetAllocateInfo setAlloc{};
|
||||
setAlloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
||||
setAlloc.descriptorPool = sceneDescriptorPool;
|
||||
setAlloc.descriptorSetCount = 1;
|
||||
setAlloc.pSetLayouts = &perFrameSetLayout;
|
||||
setAlloc.descriptorSetCount = MAX_FRAMES;
|
||||
setAlloc.pSetLayouts = layouts;
|
||||
|
||||
if (vkAllocateDescriptorSets(device, &setAlloc, &reflPerFrameDescSet) != VK_SUCCESS) {
|
||||
LOG_ERROR("Failed to allocate reflection per-frame descriptor set");
|
||||
if (vkAllocateDescriptorSets(device, &setAlloc, reflPerFrameDescSet) != VK_SUCCESS) {
|
||||
LOG_ERROR("Failed to allocate reflection per-frame descriptor sets");
|
||||
return false;
|
||||
}
|
||||
|
||||
VkDescriptorBufferInfo descBuf{};
|
||||
descBuf.buffer = reflPerFrameUBO;
|
||||
descBuf.offset = 0;
|
||||
descBuf.range = sizeof(GPUPerFrameData);
|
||||
// Bind each reflection descriptor to the same UBO but its own frame's shadow view
|
||||
for (uint32_t i = 0; i < MAX_FRAMES; i++) {
|
||||
VkDescriptorBufferInfo descBuf{};
|
||||
descBuf.buffer = reflPerFrameUBO;
|
||||
descBuf.offset = 0;
|
||||
descBuf.range = sizeof(GPUPerFrameData);
|
||||
|
||||
VkDescriptorImageInfo shadowImgInfo{};
|
||||
shadowImgInfo.sampler = shadowSampler;
|
||||
shadowImgInfo.imageView = shadowDepthView[0]; // reflection uses frame 0 shadow view
|
||||
shadowImgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
VkDescriptorImageInfo shadowImgInfo{};
|
||||
shadowImgInfo.sampler = shadowSampler;
|
||||
shadowImgInfo.imageView = shadowDepthView[i];
|
||||
shadowImgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
|
||||
VkWriteDescriptorSet writes[2]{};
|
||||
writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
writes[0].dstSet = reflPerFrameDescSet;
|
||||
writes[0].dstBinding = 0;
|
||||
writes[0].descriptorCount = 1;
|
||||
writes[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
||||
writes[0].pBufferInfo = &descBuf;
|
||||
writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
writes[1].dstSet = reflPerFrameDescSet;
|
||||
writes[1].dstBinding = 1;
|
||||
writes[1].descriptorCount = 1;
|
||||
writes[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
writes[1].pImageInfo = &shadowImgInfo;
|
||||
VkWriteDescriptorSet writes[2]{};
|
||||
writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
writes[0].dstSet = reflPerFrameDescSet[i];
|
||||
writes[0].dstBinding = 0;
|
||||
writes[0].descriptorCount = 1;
|
||||
writes[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
||||
writes[0].pBufferInfo = &descBuf;
|
||||
writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
writes[1].dstSet = reflPerFrameDescSet[i];
|
||||
writes[1].dstBinding = 1;
|
||||
writes[1].descriptorCount = 1;
|
||||
writes[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
writes[1].pImageInfo = &shadowImgInfo;
|
||||
|
||||
vkUpdateDescriptorSets(device, 2, writes, 0, nullptr);
|
||||
vkUpdateDescriptorSets(device, 2, writes, 0, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Per-frame Vulkan resources created (shadow map ", SHADOW_MAP_SIZE, "x", SHADOW_MAP_SIZE, ")");
|
||||
|
|
@ -460,7 +466,7 @@ void Renderer::updatePerFrameUBO() {
|
|||
|
||||
currentFrameData.lightSpaceMatrix = lightSpaceMatrix;
|
||||
// Scale shadow bias proportionally to ortho extent to avoid acne at close range / gaps at far range
|
||||
float shadowBias = 0.8f * (shadowDistance_ / 300.0f);
|
||||
float shadowBias = glm::clamp(0.8f * (shadowDistance_ / 300.0f), 0.0f, 1.0f);
|
||||
currentFrameData.shadowParams = glm::vec4(shadowsEnabled ? 1.0f : 0.0f, shadowBias, 0.0f, 0.0f);
|
||||
|
||||
// Player water ripple data: pack player XY into shadowParams.zw, ripple strength into fogParams.w
|
||||
|
|
@ -566,7 +572,7 @@ bool Renderer::initialize(core::Window* win) {
|
|||
postProcessPipeline_ = std::make_unique<PostProcessPipeline>();
|
||||
postProcessPipeline_->initialize(vkCtx);
|
||||
|
||||
// Phase 2.5: Create render graph and register virtual resources
|
||||
// Create render graph and register virtual resources
|
||||
renderGraph_ = std::make_unique<RenderGraph>();
|
||||
renderGraph_->registerResource("shadow_depth");
|
||||
renderGraph_->registerResource("reflection_texture");
|
||||
|
|
@ -687,7 +693,7 @@ void Renderer::shutdown() {
|
|||
postProcessPipeline_.reset();
|
||||
}
|
||||
|
||||
// Phase 2.5: Destroy render graph
|
||||
// Destroy render graph
|
||||
renderGraph_.reset();
|
||||
|
||||
destroyPerFrameResources();
|
||||
|
|
@ -1018,8 +1024,26 @@ void Renderer::setInCombat(bool combat) {
|
|||
if (animationController_) animationController_->setInCombat(combat);
|
||||
}
|
||||
|
||||
void Renderer::setEquippedWeaponType(uint32_t inventoryType) {
|
||||
if (animationController_) animationController_->setEquippedWeaponType(inventoryType);
|
||||
void Renderer::setEquippedWeaponType(uint32_t inventoryType, bool is2HLoose, bool isFist,
|
||||
bool isDagger, bool hasOffHand, bool hasShield) {
|
||||
if (animationController_) animationController_->setEquippedWeaponType(inventoryType, is2HLoose, isFist, isDagger, hasOffHand, hasShield);
|
||||
}
|
||||
|
||||
void Renderer::triggerSpecialAttack(uint32_t spellId) {
|
||||
if (animationController_) animationController_->triggerSpecialAttack(spellId);
|
||||
}
|
||||
|
||||
void Renderer::setEquippedRangedType(RangedWeaponType type) {
|
||||
if (animationController_) animationController_->setEquippedRangedType(type);
|
||||
}
|
||||
|
||||
void Renderer::triggerRangedShot() {
|
||||
if (animationController_) animationController_->triggerRangedShot();
|
||||
}
|
||||
|
||||
RangedWeaponType Renderer::getEquippedRangedType() const {
|
||||
return animationController_ ? animationController_->getEquippedRangedType()
|
||||
: RangedWeaponType::NONE;
|
||||
}
|
||||
|
||||
void Renderer::setCharging(bool c) {
|
||||
|
|
@ -2797,8 +2821,8 @@ glm::mat4 Renderer::computeLightSpaceMatrix() {
|
|||
sunDir = -sunDir;
|
||||
}
|
||||
// Keep a minimum downward component so the frustum doesn't collapse at grazing angles.
|
||||
if (sunDir.z > -0.08f) {
|
||||
sunDir.z = -0.08f;
|
||||
if (sunDir.z > -0.15f) {
|
||||
sunDir.z = -0.15f;
|
||||
sunDir = glm::normalize(sunDir);
|
||||
}
|
||||
|
||||
|
|
@ -2986,6 +3010,11 @@ void Renderer::renderReflectionPass() {
|
|||
if (!waterRenderer || !camera || !waterRenderer->hasReflectionPass() || !waterRenderer->hasSurfaces()) return;
|
||||
if (currentCmd == VK_NULL_HANDLE || !reflPerFrameUBOMapped) return;
|
||||
|
||||
// Select the current frame's pre-bound reflection descriptor set
|
||||
// (each frame's set was bound to its own shadow depth view at init).
|
||||
uint32_t frame = vkCtx->getCurrentFrame();
|
||||
VkDescriptorSet reflDescSet = reflPerFrameDescSet[frame];
|
||||
|
||||
// Reflection pass uses 1x MSAA. Scene pipelines must be render-pass-compatible,
|
||||
// which requires matching sample counts. Only render scene into reflection when MSAA is off.
|
||||
bool canRenderScene = (vkCtx->getMsaaSamples() == VK_SAMPLE_COUNT_1_BIT);
|
||||
|
|
@ -3040,13 +3069,13 @@ void Renderer::renderReflectionPass() {
|
|||
skyParams.horizonGlow = lp.horizonGlow;
|
||||
}
|
||||
// weatherIntensity left at default 0 for reflection pass (no game handler in scope)
|
||||
skySystem->render(currentCmd, reflPerFrameDescSet, *camera, skyParams);
|
||||
skySystem->render(currentCmd, reflDescSet, *camera, skyParams);
|
||||
}
|
||||
if (terrainRenderer && terrainEnabled) {
|
||||
terrainRenderer->render(currentCmd, reflPerFrameDescSet, *camera);
|
||||
terrainRenderer->render(currentCmd, reflDescSet, *camera);
|
||||
}
|
||||
if (wmoRenderer) {
|
||||
wmoRenderer->render(currentCmd, reflPerFrameDescSet, *camera);
|
||||
wmoRenderer->render(currentCmd, reflDescSet, *camera);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3139,7 +3168,7 @@ void Renderer::renderShadowPass() {
|
|||
shadowDepthLayout_[frame] = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
}
|
||||
|
||||
// Phase 2.5: Build the per-frame render graph for off-screen pre-passes.
|
||||
// Build the per-frame render graph for off-screen pre-passes.
|
||||
// Declares passes as graph nodes with input/output dependencies.
|
||||
// compile() performs topological sort; execute() runs them with auto barriers.
|
||||
void Renderer::buildFrameGraph(game::GameHandler* gameHandler) {
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ bool TerrainRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameL
|
|||
envSizeMBOrDefault("WOWEE_TERRAIN_TEX_CACHE_MB", 4096) * 1024ull * 1024ull;
|
||||
LOG_INFO("Terrain texture cache budget: ", textureCacheBudgetBytes_ / (1024 * 1024), " MB");
|
||||
|
||||
// Phase 2.2: Allocate mega vertex/index buffers and indirect draw buffer.
|
||||
// Allocate mega vertex/index buffers and indirect draw buffer.
|
||||
// All terrain chunks share these buffers, eliminating per-chunk VB/IB rebinds.
|
||||
{
|
||||
VmaAllocator allocator = vkCtx->getAllocator();
|
||||
|
|
@ -375,7 +375,7 @@ void TerrainRenderer::shutdown() {
|
|||
if (shadowParamsLayout_) { vkDestroyDescriptorSetLayout(device, shadowParamsLayout_, nullptr); shadowParamsLayout_ = VK_NULL_HANDLE; }
|
||||
if (shadowParamsUBO_) { vmaDestroyBuffer(allocator, shadowParamsUBO_, shadowParamsAlloc_); shadowParamsUBO_ = VK_NULL_HANDLE; shadowParamsAlloc_ = VK_NULL_HANDLE; }
|
||||
|
||||
// Phase 2.2: Destroy mega buffers and indirect draw buffer
|
||||
// Destroy mega buffers and indirect draw buffer
|
||||
if (megaVB_) { vmaDestroyBuffer(allocator, megaVB_, megaVBAlloc_); megaVB_ = VK_NULL_HANDLE; megaVBAlloc_ = VK_NULL_HANDLE; megaVBMapped_ = nullptr; }
|
||||
if (megaIB_) { vmaDestroyBuffer(allocator, megaIB_, megaIBAlloc_); megaIB_ = VK_NULL_HANDLE; megaIBAlloc_ = VK_NULL_HANDLE; megaIBMapped_ = nullptr; }
|
||||
if (indirectBuffer_) { vmaDestroyBuffer(allocator, indirectBuffer_, indirectAlloc_); indirectBuffer_ = VK_NULL_HANDLE; indirectAlloc_ = VK_NULL_HANDLE; indirectMapped_ = nullptr; }
|
||||
|
|
@ -622,7 +622,7 @@ TerrainChunkGPU TerrainRenderer::uploadChunk(const pipeline::ChunkMesh& chunk) {
|
|||
gpuChunk.indexBuffer = ib.buffer;
|
||||
gpuChunk.indexAlloc = ib.allocation;
|
||||
|
||||
// Phase 2.2: Also copy into mega buffers for indirect drawing
|
||||
// Also copy into mega buffers for indirect drawing
|
||||
uint32_t vertCount = static_cast<uint32_t>(chunk.vertices.size());
|
||||
uint32_t idxCount = static_cast<uint32_t>(chunk.indices.size());
|
||||
if (megaVBMapped_ && megaIBMapped_ &&
|
||||
|
|
@ -880,7 +880,7 @@ void TerrainRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, c
|
|||
renderedChunks = 0;
|
||||
culledChunks = 0;
|
||||
|
||||
// Phase 2.2: Use mega VB + IB when available.
|
||||
// Use mega VB + IB when available.
|
||||
// Bind mega buffers once, then use direct draws with base vertex/index offsets.
|
||||
const bool useMegaBuffers = (megaVB_ && megaIB_);
|
||||
if (useMegaBuffers) {
|
||||
|
|
@ -1092,7 +1092,7 @@ void TerrainRenderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSp
|
|||
vkCmdPushConstants(cmd, shadowPipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT,
|
||||
0, 128, &push);
|
||||
|
||||
// Phase 2.2: Bind mega buffers once for shadow pass (same as opaque)
|
||||
// Bind mega buffers once for shadow pass (same as opaque)
|
||||
const bool useMegaShadow = (megaVB_ && megaIB_);
|
||||
if (useMegaShadow) {
|
||||
VkDeviceSize megaOffset = 0;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "ui/auth_screen.hpp"
|
||||
#include "ui/ui_colors.hpp"
|
||||
#include "ui/settings_panel.hpp"
|
||||
#include "auth/crypto.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "core/logger.hpp"
|
||||
|
|
@ -13,8 +14,9 @@
|
|||
#include <imgui_impl_vulkan.h>
|
||||
#include "stb_image.h"
|
||||
#include <filesystem>
|
||||
#include <sstream>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
|
|
@ -492,6 +494,11 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
|
|||
if (ImGui::Button("Clear", ImVec2(160, 40))) {
|
||||
statusMessage.clear();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Settings", ImVec2(160, 40))) {
|
||||
showLoginSettings_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
|
@ -503,6 +510,8 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
|
|||
ImGui::TextWrapped("Default port is 3724.");
|
||||
|
||||
ImGui::End();
|
||||
|
||||
renderLoginSettingsWindow();
|
||||
}
|
||||
|
||||
void AuthScreen::stopLoginMusic() {
|
||||
|
|
@ -945,4 +954,216 @@ void AuthScreen::destroyBackgroundImage() {
|
|||
if (bgMemory) { vkFreeMemory(device, bgMemory, nullptr); bgMemory = VK_NULL_HANDLE; }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Login-screen graphics settings popup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void AuthScreen::applyPresetToState(LoginGraphicsState& s, int preset) {
|
||||
switch (preset) {
|
||||
case 1: // Low
|
||||
s.shadows = false; s.shadowDistance = 75.0f; s.antiAliasing = 0;
|
||||
s.fxaa = false; s.normalMapping = false; s.pom = false; s.pomQuality = 1;
|
||||
s.upscalingMode = 0; s.waterRefraction = false; s.groundClutter = 25;
|
||||
s.brightness = 50; s.vsync = false; s.fullscreen = false;
|
||||
break;
|
||||
case 2: // Medium
|
||||
s.shadows = true; s.shadowDistance = 150.0f; s.antiAliasing = 0;
|
||||
s.fxaa = false; s.normalMapping = true; s.pom = true; s.pomQuality = 1;
|
||||
s.upscalingMode = 0; s.waterRefraction = true; s.groundClutter = 100;
|
||||
s.brightness = 50; s.vsync = false; s.fullscreen = false;
|
||||
break;
|
||||
case 3: // High
|
||||
s.shadows = true; s.shadowDistance = 250.0f; s.antiAliasing = 1;
|
||||
s.fxaa = true; s.normalMapping = true; s.pom = true; s.pomQuality = 1;
|
||||
s.upscalingMode = 0; s.waterRefraction = true; s.groundClutter = 130;
|
||||
s.brightness = 50; s.vsync = false; s.fullscreen = false;
|
||||
break;
|
||||
case 4: // Ultra
|
||||
s.shadows = true; s.shadowDistance = 400.0f; s.antiAliasing = 2;
|
||||
s.fxaa = true; s.normalMapping = true; s.pom = true; s.pomQuality = 2;
|
||||
s.upscalingMode = 0; s.waterRefraction = true; s.groundClutter = 150;
|
||||
s.brightness = 50; s.vsync = false; s.fullscreen = false;
|
||||
break;
|
||||
default: // Custom — no change
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void AuthScreen::loadLoginGraphicsState() {
|
||||
std::ifstream file(SettingsPanel::getSettingsPath());
|
||||
if (!file.is_open()) {
|
||||
// File doesn't exist yet — keep struct defaults (Medium equivalent)
|
||||
return;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
while (std::getline(file, line)) {
|
||||
auto eq = line.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
std::string key = line.substr(0, eq);
|
||||
std::string val = line.substr(eq + 1);
|
||||
|
||||
if (key == "graphics_preset") loginGfx_.preset = std::stoi(val);
|
||||
else if (key == "shadows") loginGfx_.shadows = (val == "1");
|
||||
else if (key == "shadow_distance") loginGfx_.shadowDistance = std::stof(val);
|
||||
else if (key == "antialiasing") loginGfx_.antiAliasing = std::stoi(val);
|
||||
else if (key == "fxaa") loginGfx_.fxaa = (val == "1");
|
||||
else if (key == "normal_mapping") loginGfx_.normalMapping = (val == "1");
|
||||
else if (key == "pom") loginGfx_.pom = (val == "1");
|
||||
else if (key == "pom_quality") loginGfx_.pomQuality = std::stoi(val);
|
||||
else if (key == "upscaling_mode") loginGfx_.upscalingMode = std::stoi(val);
|
||||
else if (key == "water_refraction") loginGfx_.waterRefraction = (val == "1");
|
||||
else if (key == "ground_clutter_density") loginGfx_.groundClutter = std::stoi(val);
|
||||
else if (key == "brightness") loginGfx_.brightness = std::stoi(val);
|
||||
else if (key == "vsync") loginGfx_.vsync = (val == "1");
|
||||
else if (key == "fullscreen") loginGfx_.fullscreen = (val == "1");
|
||||
}
|
||||
}
|
||||
|
||||
void AuthScreen::saveLoginGraphicsState() {
|
||||
// Read the full settings file into a map to preserve non-graphics keys.
|
||||
std::map<std::string, std::string> cfg;
|
||||
std::ifstream in(SettingsPanel::getSettingsPath());
|
||||
if (in.is_open()) {
|
||||
std::string line;
|
||||
while (std::getline(in, line)) {
|
||||
auto eq = line.find('=');
|
||||
if (eq != std::string::npos)
|
||||
cfg[line.substr(0, eq)] = line.substr(eq + 1);
|
||||
}
|
||||
in.close();
|
||||
}
|
||||
|
||||
// Overwrite graphics keys.
|
||||
cfg["graphics_preset"] = std::to_string(loginGfx_.preset);
|
||||
cfg["shadows"] = loginGfx_.shadows ? "1" : "0";
|
||||
cfg["shadow_distance"] = std::to_string(static_cast<int>(loginGfx_.shadowDistance));
|
||||
cfg["antialiasing"] = std::to_string(loginGfx_.antiAliasing);
|
||||
cfg["fxaa"] = loginGfx_.fxaa ? "1" : "0";
|
||||
cfg["normal_mapping"] = loginGfx_.normalMapping ? "1" : "0";
|
||||
cfg["pom"] = loginGfx_.pom ? "1" : "0";
|
||||
cfg["pom_quality"] = std::to_string(loginGfx_.pomQuality);
|
||||
cfg["upscaling_mode"] = std::to_string(loginGfx_.upscalingMode);
|
||||
cfg["water_refraction"] = loginGfx_.waterRefraction ? "1" : "0";
|
||||
cfg["ground_clutter_density"]= std::to_string(loginGfx_.groundClutter);
|
||||
cfg["brightness"] = std::to_string(loginGfx_.brightness);
|
||||
cfg["vsync"] = loginGfx_.vsync ? "1" : "0";
|
||||
cfg["fullscreen"] = loginGfx_.fullscreen ? "1" : "0";
|
||||
|
||||
// Write everything back.
|
||||
std::ofstream out(SettingsPanel::getSettingsPath());
|
||||
if (!out.is_open()) return;
|
||||
for (const auto& [k, v] : cfg)
|
||||
out << k << "=" << v << "\n";
|
||||
}
|
||||
|
||||
void AuthScreen::renderLoginSettingsWindow() {
|
||||
if (showLoginSettings_) {
|
||||
ImGui::OpenPopup("Graphics Settings");
|
||||
showLoginSettings_ = false;
|
||||
loginGfxLoaded_ = false; // Reload from disk each time the popup opens.
|
||||
}
|
||||
|
||||
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
|
||||
ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
|
||||
ImGui::SetNextWindowSize(ImVec2(500, 560), ImGuiCond_Always);
|
||||
|
||||
if (ImGui::BeginPopupModal("Graphics Settings", nullptr, ImGuiWindowFlags_NoResize)) {
|
||||
if (!loginGfxLoaded_) {
|
||||
loadLoginGraphicsState();
|
||||
loginGfxLoaded_ = true;
|
||||
}
|
||||
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "Graphics Settings");
|
||||
ImGui::TextWrapped("Adjust settings below or reset to a safe preset. Changes take effect on next login.");
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Preset selector
|
||||
const char* presetNames[] = {"Custom", "Low", "Medium", "High", "Ultra"};
|
||||
ImGui::Text("Preset:");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(160.0f);
|
||||
if (ImGui::Combo("##preset", &loginGfx_.preset, presetNames, 5)) {
|
||||
if (loginGfx_.preset != 0) // 0 = Custom — don't override manually set values
|
||||
applyPresetToState(loginGfx_, loginGfx_.preset);
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Shadow settings
|
||||
ImGui::Checkbox("Shadows", &loginGfx_.shadows);
|
||||
if (loginGfx_.shadows) {
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(200.0f);
|
||||
float sd = loginGfx_.shadowDistance;
|
||||
if (ImGui::SliderFloat("Shadow Distance", &sd, 50.0f, 600.0f, "%.0f"))
|
||||
loginGfx_.shadowDistance = sd;
|
||||
}
|
||||
|
||||
// Anti-aliasing
|
||||
const char* aaNames[] = {"Off", "2x MSAA", "4x MSAA"};
|
||||
ImGui::Text("Anti-Aliasing:");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(130.0f);
|
||||
ImGui::Combo("##aa", &loginGfx_.antiAliasing, aaNames, 3);
|
||||
|
||||
ImGui::Checkbox("FXAA", &loginGfx_.fxaa);
|
||||
ImGui::Checkbox("Normal Mapping", &loginGfx_.normalMapping);
|
||||
|
||||
// POM
|
||||
ImGui::Checkbox("Parallax Occlusion Mapping (POM)", &loginGfx_.pom);
|
||||
if (loginGfx_.pom) {
|
||||
const char* pomQ[] = {"Medium", "High"};
|
||||
ImGui::Text(" POM Quality:");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(110.0f);
|
||||
ImGui::Combo("##pomq", &loginGfx_.pomQuality, pomQ, 2);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("Water Refraction", &loginGfx_.waterRefraction);
|
||||
|
||||
// Ground clutter density
|
||||
ImGui::Text("Ground Clutter:");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(200.0f);
|
||||
ImGui::SliderInt("##clutter", &loginGfx_.groundClutter, 0, 200);
|
||||
|
||||
// Brightness
|
||||
ImGui::Text("Brightness:");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(200.0f);
|
||||
ImGui::SliderInt("##brightness", &loginGfx_.brightness, 0, 100);
|
||||
|
||||
ImGui::Checkbox("V-Sync", &loginGfx_.vsync);
|
||||
ImGui::Checkbox("Fullscreen", &loginGfx_.fullscreen);
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Action buttons
|
||||
if (ImGui::Button("Reset to Medium", ImVec2(160, 32))) {
|
||||
applyPresetToState(loginGfx_, 2);
|
||||
loginGfx_.preset = 2;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
float rightEdge = ImGui::GetContentRegionAvail().x;
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + rightEdge - 220.0f);
|
||||
if (ImGui::Button("Cancel", ImVec2(100, 32))) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Apply", ImVec2(100, 32))) {
|
||||
saveLoginGraphicsState();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
}} // namespace wowee::ui
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ void DialogManager::renderLateDialogs(game::GameHandler& gameHandler) {
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Group Invite Popup (Phase 4)
|
||||
// Group Invite Popup
|
||||
// ============================================================
|
||||
|
||||
void DialogManager::renderGroupInvitePopup(game::GameHandler& gameHandler) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue