From 826a22eed340d77629b7b47871e6d94c3d8901e7 Mon Sep 17 00:00:00 2001 From: Pavel Okhlopkov Date: Fri, 10 Apr 2026 19:50:24 +0300 Subject: [PATCH] fix(animation): prevent creature walk/run animation persisting after arriving Use destination position (getLatest) instead of dead-reckoned position (getX/Y/Z) during the overrun window to avoid visible forward-drift and backward-snap. Only fall back to position-change movement detection for entities without active movement tracking, preventing residual velocity drift from keeping walk/run animation playing after arrival. Signed-off-by: Pavel Okhlopkov --- src/core/application.cpp | 42 +++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src/core/application.cpp b/src/core/application.cpp index f62aabc9..04ed7549 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -1740,7 +1740,16 @@ void Application::update(float deltaTime) { if (canonDistSq > syncRadiusSq) continue; } - glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ()); + // Use the destination position once the entity has reached its + // target. During the dead-reckoning overrun window getX/Y/Z + // drifts past the destination at the last known velocity; + // using getLatest (== moveEnd while isMoving_) avoids the + // visible forward-drift followed by a backward snap. + const bool inOverrun = entity->isEntityMoving() && !entity->isActivelyMoving(); + glm::vec3 canonical( + inOverrun ? entity->getLatestX() : entity->getX(), + inOverrun ? entity->getLatestY() : entity->getY(), + inOverrun ? entity->getLatestZ() : entity->getZ()); glm::vec3 renderPos = core::coords::canonicalToRender(canonical); // Visual collision guard: keep hostile melee units from rendering inside the @@ -1822,17 +1831,18 @@ void Application::update(float deltaTime) { auto unitPtr = std::static_pointer_cast(entity); const bool deadOrCorpse = unitPtr->getHealth() == 0; const bool largeCorrection = (planarDistSq > 36.0f) || (dz > 3.0f); - // isEntityMoving() reflects server-authoritative move state set by - // startMoveTo() in handleMonsterMove, regardless of distance-cull. - // This correctly detects movement for distant creatures (> 150u) - // where updateMovement() is not called and getX/Y/Z() stays stale. - // Use isActivelyMoving() (not isEntityMoving()) so the - // Run/Walk animation stops when the creature reaches its - // destination, rather than persisting through the dead- - // reckoning overrun window. + // Use isActivelyMoving() so Run/Walk animation stops when the + // creature reaches its destination. Don't use position-change + // (planarDistSq) as a movement indicator when the entity is in + // the dead-reckoning overrun window — the residual velocity + // drift would keep the walk/run animation playing long after + // the creature has actually arrived. Only fall back to position- + // change detection for entities with no active movement tracking + // (e.g. teleports or position-only updates from the server). const bool entityIsMoving = entity->isActivelyMoving(); constexpr float kMoveThreshSq = 0.03f * 0.03f; - const bool isMovingNow = !deadOrCorpse && (entityIsMoving || planarDistSq > kMoveThreshSq || dz > 0.08f); + const bool posChanging = planarDistSq > kMoveThreshSq || dz > 0.08f; + const bool isMovingNow = !deadOrCorpse && (entityIsMoving || (posChanging && !entity->isEntityMoving())); if (deadOrCorpse || largeCorrection) { charRenderer->setInstancePosition(instanceId, renderPos); } else if (planarDistSq > kMoveThreshSq || dz > 0.08f) { @@ -1936,8 +1946,13 @@ void Application::update(float deltaTime) { if (glm::dot(d, d) > pSyncRadiusSq) continue; } - // Position sync - glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ()); + // Position sync — clamp to destination during dead-reckoning + // overrun to avoid drift + backward snap (same as creature loop). + const bool inOverrun = entity->isEntityMoving() && !entity->isActivelyMoving(); + glm::vec3 canonical( + inOverrun ? entity->getLatestX() : entity->getX(), + inOverrun ? entity->getLatestY() : entity->getY(), + inOverrun ? entity->getLatestZ() : entity->getZ()); glm::vec3 renderPos = core::coords::canonicalToRender(canonical); auto posIt = _pCreatureRenderPosCache.find(guid); @@ -1956,7 +1971,8 @@ void Application::update(float deltaTime) { const bool largeCorrection = (planarDistSq > 36.0f) || (dz > 3.0f); const bool entityIsMoving = entity->isActivelyMoving(); constexpr float kMoveThreshSq2 = 0.03f * 0.03f; - const bool isMovingNow = !deadOrCorpse && (entityIsMoving || planarDistSq > kMoveThreshSq2 || dz > 0.08f); + const bool posChanging2 = planarDistSq > kMoveThreshSq2 || dz > 0.08f; + const bool isMovingNow = !deadOrCorpse && (entityIsMoving || (posChanging2 && !entity->isEntityMoving())); if (deadOrCorpse || largeCorrection) { charRenderer->setInstancePosition(instanceId, renderPos);