rendering/game: track per-entity swim state for correct water animations

- Add creatureSwimmingState_ map to track which units are swimming
- unitAnimHintCallback with animId=42 (Swim): marks entity as swimming
- unitAnimHintCallback with animId=0 (MSG_MOVE_STOP_SWIM): clears swim state
- Per-frame sync: uses Swim(42)/SwimIdle(41) when swimming, Run(5)/Stand(0) otherwise
  — creatures/players now show SwimIdle when standing still in water
- Clear creatureSwimmingState_ on creature/player despawn and world reset
This commit is contained in:
Kelsi 2026-03-10 10:36:45 -07:00
parent 14c2bc97b1
commit 333ada8eb6
3 changed files with 31 additions and 9 deletions

View file

@ -188,6 +188,7 @@ private:
std::unordered_map<uint64_t, uint32_t> creatureModelIds_; // guid → loaded modelId std::unordered_map<uint64_t, uint32_t> creatureModelIds_; // guid → loaded modelId
std::unordered_map<uint64_t, glm::vec3> creatureRenderPosCache_; // guid -> last synced render position std::unordered_map<uint64_t, glm::vec3> creatureRenderPosCache_; // guid -> last synced render position
std::unordered_map<uint64_t, bool> creatureWasMoving_; // guid -> previous-frame movement state std::unordered_map<uint64_t, bool> creatureWasMoving_; // guid -> previous-frame movement state
std::unordered_map<uint64_t, bool> creatureSwimmingState_; // guid -> currently in swim mode
std::unordered_set<uint64_t> creatureWeaponsAttached_; // guid set when NPC virtual weapons attached std::unordered_set<uint64_t> creatureWeaponsAttached_; // guid set when NPC virtual weapons attached
std::unordered_map<uint64_t, uint8_t> creatureWeaponAttachAttempts_; // guid -> attach attempts std::unordered_map<uint64_t, uint8_t> creatureWeaponAttachAttempts_; // guid -> attach attempts
std::unordered_map<uint32_t, bool> modelIdIsWolfLike_; // modelId → cached wolf/worg check std::unordered_map<uint32_t, bool> modelIdIsWolfLike_; // modelId → cached wolf/worg check

View file

@ -750,6 +750,7 @@ void Application::logoutToLogin() {
creatureWeaponsAttached_.clear(); creatureWeaponsAttached_.clear();
creatureWeaponAttachAttempts_.clear(); creatureWeaponAttachAttempts_.clear();
creatureWasMoving_.clear(); creatureWasMoving_.clear();
creatureSwimmingState_.clear();
deadCreatureGuids_.clear(); deadCreatureGuids_.clear();
nonRenderableCreatureDisplayIds_.clear(); nonRenderableCreatureDisplayIds_.clear();
creaturePermanentFailureGuids_.clear(); creaturePermanentFailureGuids_.clear();
@ -1476,18 +1477,23 @@ void Application::update(float deltaTime) {
} }
posIt->second = renderPos; posIt->second = renderPos;
// Drive movement animation: Run (anim 5) when moving, Stand (0) when idle. // Drive movement animation: Run/Swim (5/42) when moving, Stand/SwimIdle (0/41) when idle.
// WoW M2 animation IDs: 4=Walk, 5=Run. Use Run for all server-driven NPC movement. // WoW M2 animation IDs: 4=Walk, 5=Run, 41=SwimIdle, 42=Swim.
// Only switch on transitions to avoid resetting animation time. // Only switch on transitions to avoid resetting animation time.
// Don't override Death (1) animation. // Don't override Death (1) animation.
const bool isSwimmingNow = creatureSwimmingState_.count(guid) > 0;
bool prevMoving = creatureWasMoving_[guid]; bool prevMoving = creatureWasMoving_[guid];
if (isMovingNow != prevMoving) { if (isMovingNow != prevMoving) {
creatureWasMoving_[guid] = isMovingNow; creatureWasMoving_[guid] = isMovingNow;
uint32_t curAnimId = 0; float curT = 0.0f, curDur = 0.0f; uint32_t curAnimId = 0; float curT = 0.0f, curDur = 0.0f;
bool gotState = charRenderer->getAnimationState(instanceId, curAnimId, curT, curDur); bool gotState = charRenderer->getAnimationState(instanceId, curAnimId, curT, curDur);
if (!gotState || curAnimId != 1 /*Death*/) { if (!gotState || curAnimId != 1 /*Death*/) {
charRenderer->playAnimation(instanceId, uint32_t targetAnim;
isMovingNow ? 5u : 0u, /*loop=*/true); if (isMovingNow)
targetAnim = isSwimmingNow ? 42u : 5u; // Swim vs Run
else
targetAnim = isSwimmingNow ? 41u : 0u; // SwimIdle vs Stand
charRenderer->playAnimation(instanceId, targetAnim, /*loop=*/true);
} }
} }
} }
@ -2771,10 +2777,20 @@ void Application::setupUICallbacks() {
} }
}); });
// Unit animation hint callback — play jump (38) or swim (42) on other players/NPCs // Unit animation hint callback — play jump (38) or swim (42) on other players/NPCs.
// when MSG_MOVE_JUMP or MSG_MOVE_START_SWIM arrives. The per-frame sync handles the // animId=42 (Swim): marks entity as swimming; per-frame sync will use SwimIdle(41) when stopped.
// return to Stand/Run once the unit lands or exits water. // animId=0: clears swim state (MSG_MOVE_STOP_SWIM); per-frame sync reverts to Stand(0).
// animId=38 (JumpMid): airborne jump animation; land detection is via per-frame sync.
gameHandler->setUnitAnimHintCallback([this](uint64_t guid, uint32_t animId) { gameHandler->setUnitAnimHintCallback([this](uint64_t guid, uint32_t animId) {
// Track swim state regardless of whether the instance is visible yet.
if (animId == 42u) {
creatureSwimmingState_[guid] = true;
} else if (animId == 0u) {
creatureSwimmingState_.erase(guid);
// Don't play Stand here — per-frame sync will do it when movement ceases.
return;
}
if (!renderer) return; if (!renderer) return;
auto* cr = renderer->getCharacterRenderer(); auto* cr = renderer->getCharacterRenderer();
if (!cr) return; if (!cr) return;
@ -6910,6 +6926,7 @@ void Application::despawnOnlinePlayer(uint64_t guid) {
playerInstances_.erase(it); playerInstances_.erase(it);
onlinePlayerAppearance_.erase(guid); onlinePlayerAppearance_.erase(guid);
pendingOnlinePlayerEquipment_.erase(guid); pendingOnlinePlayerEquipment_.erase(guid);
creatureSwimmingState_.erase(guid);
} }
void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) { void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) {
@ -8503,6 +8520,7 @@ void Application::despawnOnlineCreature(uint64_t guid) {
creatureWeaponsAttached_.erase(guid); creatureWeaponsAttached_.erase(guid);
creatureWeaponAttachAttempts_.erase(guid); creatureWeaponAttachAttempts_.erase(guid);
creatureWasMoving_.erase(guid); creatureWasMoving_.erase(guid);
creatureSwimmingState_.erase(guid);
LOG_DEBUG("Despawned creature: guid=0x", std::hex, guid, std::dec); LOG_DEBUG("Despawned creature: guid=0x", std::hex, guid, std::dec);
} }

View file

@ -12190,9 +12190,12 @@ void GameHandler::handleOtherPlayerMovement(network::Packet& packet) {
// Signal specific animation transitions that the per-frame sync can't detect reliably. // Signal specific animation transitions that the per-frame sync can't detect reliably.
// WoW M2 animation IDs: 38=JumpMid (loops during airborne), 42=Swim // WoW M2 animation IDs: 38=JumpMid (loops during airborne), 42=Swim
// animId=0 signals "exit swim mode" (MSG_MOVE_STOP_SWIM) so per-frame sync reverts to Stand.
const bool isStopSwimOpcode = (wireOp == wireOpcode(Opcode::MSG_MOVE_STOP_SWIM));
if (unitAnimHintCallback_) { if (unitAnimHintCallback_) {
if (isJumpOpcode) unitAnimHintCallback_(moverGuid, 38u); if (isJumpOpcode) unitAnimHintCallback_(moverGuid, 38u);
else if (isSwimOpcode) unitAnimHintCallback_(moverGuid, 42u); else if (isSwimOpcode) unitAnimHintCallback_(moverGuid, 42u);
else if (isStopSwimOpcode) unitAnimHintCallback_(moverGuid, 0u);
} }
} }