diff --git a/include/core/application.hpp b/include/core/application.hpp index d2ef3f36..61951d7d 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -187,6 +187,7 @@ private: std::unordered_map creatureInstances_; // guid → render instanceId std::unordered_map creatureModelIds_; // guid → loaded modelId std::unordered_map creatureRenderPosCache_; // guid -> last synced render position + std::unordered_map creatureWasMoving_; // guid -> previous-frame movement state std::unordered_set creatureWeaponsAttached_; // guid set when NPC virtual weapons attached std::unordered_map creatureWeaponAttachAttempts_; // guid -> attach attempts std::unordered_map modelIdIsWolfLike_; // modelId → cached wolf/worg check diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index 79a7d622..fc513117 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -226,6 +226,9 @@ private: bool autoRunning = false; bool tildeWasDown = false; + // Movement animation state tracking + bool prevPlayerMoving_ = false; + // Movement state tracking (for sending opcodes on state change) bool wasMovingForward = false; bool wasMovingBackward = false; diff --git a/src/core/application.cpp b/src/core/application.cpp index 5065cc68..b707e3d6 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -749,6 +749,7 @@ void Application::logoutToLogin() { creatureRenderPosCache_.clear(); creatureWeaponsAttached_.clear(); creatureWeaponAttachAttempts_.clear(); + creatureWasMoving_.clear(); deadCreatureGuids_.clear(); nonRenderableCreatureDisplayIds_.clear(); creaturePermanentFailureGuids_.clear(); @@ -1466,14 +1467,28 @@ void Application::update(float deltaTime) { auto unitPtr = std::static_pointer_cast(entity); const bool deadOrCorpse = unitPtr->getHealth() == 0; const bool largeCorrection = (planarDist > 6.0f) || (dz > 3.0f); + const bool isMovingNow = !deadOrCorpse && (planarDist > 0.03f || dz > 0.08f); if (deadOrCorpse || largeCorrection) { charRenderer->setInstancePosition(instanceId, renderPos); - } else if (planarDist > 0.03f || dz > 0.08f) { - // Use movement interpolation so step/run animation can play. + } else if (isMovingNow) { float duration = std::clamp(planarDist / 5.5f, 0.05f, 0.22f); charRenderer->moveInstanceTo(instanceId, renderPos, duration); } posIt->second = renderPos; + + // Drive movement animation: Run (4) when moving, Stand (0) when idle. + // Only switch on transitions to avoid resetting animation time. + // Don't override Death (1) animation. + bool prevMoving = creatureWasMoving_[guid]; + if (isMovingNow != prevMoving) { + creatureWasMoving_[guid] = isMovingNow; + uint32_t curAnimId = 0; float curT = 0.0f, curDur = 0.0f; + bool gotState = charRenderer->getAnimationState(instanceId, curAnimId, curT, curDur); + if (!gotState || curAnimId != 1 /*Death*/) { + charRenderer->playAnimation(instanceId, + isMovingNow ? 4u : 0u, /*loop=*/true); + } + } } float renderYaw = entity->getOrientation() + glm::radians(90.0f); charRenderer->setInstanceRotation(instanceId, glm::vec3(0.0f, 0.0f, renderYaw)); @@ -8449,6 +8464,7 @@ void Application::despawnOnlineCreature(uint64_t guid) { creatureRenderPosCache_.erase(guid); creatureWeaponsAttached_.erase(guid); creatureWeaponAttachAttempts_.erase(guid); + creatureWasMoving_.erase(guid); LOG_DEBUG("Despawned creature: guid=0x", std::hex, guid, std::dec); } diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 4e0d6ff2..21626f52 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -1445,6 +1445,21 @@ void CameraController::update(float deltaTime) { // Honor first-person intent even if anti-clipping pushes camera back slightly. bool shouldHidePlayer = isFirstPersonView() || (actualDist < MIN_DISTANCE + 0.1f); characterRenderer->setInstanceVisible(playerInstanceId, !shouldHidePlayer); + + // Drive movement animation: Run (4) when moving, Stand (0) when idle. + // Only transition on state changes to avoid resetting animation time every frame. + // Skip if current animation is Death (1) — death pose must persist. + bool nowMoving = isMoving(); + if (nowMoving != prevPlayerMoving_) { + prevPlayerMoving_ = nowMoving; + uint32_t curAnimId = 0; float curT = 0.0f, curDur = 0.0f; + bool gotState = characterRenderer->getAnimationState( + playerInstanceId, curAnimId, curT, curDur); + if (!gotState || curAnimId != 1 /*Death*/) { + characterRenderer->playAnimation(playerInstanceId, + nowMoving ? 4u : 0u, /*loop=*/true); + } + } } } else { // Free-fly camera mode (original behavior)