From 4e137c4061b12045726f48da2dcf75a8113ac893 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 10 Mar 2026 10:06:56 -0700 Subject: [PATCH] rendering: drive Run/Stand animations from actual movement state CameraController now transitions the player character to Run (anim 4) on movement start and back to Stand (anim 0) on stop, guarded by a prevPlayerMoving_ flag so animation time is not reset every frame. Death animation (anim 1) is never overridden. Application creature sync similarly switches creature models to Run (4) when they move between server positions and Stand (0) when they stop, with per-guid creatureWasMoving_ tracking to avoid per-frame resets. --- include/core/application.hpp | 1 + include/rendering/camera_controller.hpp | 3 +++ src/core/application.cpp | 20 ++++++++++++++++++-- src/rendering/camera_controller.cpp | 15 +++++++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) 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)