diff --git a/include/game/entity.hpp b/include/game/entity.hpp index 0b9fb647..702bf7a0 100644 --- a/include/game/entity.hpp +++ b/include/game/entity.hpp @@ -83,6 +83,21 @@ public: setPosition(destX, destY, destZ, destO); return; } + // Derive velocity from the displacement this packet implies. + // Use the previous destination (not current lerped pos) as the "from" so + // variable network timing doesn't inflate/shrink the implied speed. + float fromX = isMoving_ ? moveEndX_ : x; + float fromY = isMoving_ ? moveEndY_ : y; + float fromZ = isMoving_ ? moveEndZ_ : z; + float impliedVX = (destX - fromX) / durationSec; + float impliedVY = (destY - fromY) / durationSec; + float impliedVZ = (destZ - fromZ) / durationSec; + // Exponentially smooth velocity so jittery packet timing doesn't snap speed. + const float alpha = 0.65f; + velX_ = alpha * impliedVX + (1.0f - alpha) * velX_; + velY_ = alpha * impliedVY + (1.0f - alpha) * velY_; + velZ_ = alpha * impliedVZ + (1.0f - alpha) * velZ_; + moveStartX_ = x; moveStartY_ = y; moveStartZ_ = z; moveEndX_ = destX; moveEndY_ = destY; moveEndZ_ = destZ; moveDuration_ = durationSec; @@ -94,14 +109,27 @@ public: void updateMovement(float deltaTime) { if (!isMoving_) return; moveElapsed_ += deltaTime; - float t = moveElapsed_ / moveDuration_; - if (t >= 1.0f) { - x = moveEndX_; y = moveEndY_; z = moveEndZ_; - isMoving_ = false; - } else { + if (moveElapsed_ < moveDuration_) { + // Linear interpolation within the packet window + float t = moveElapsed_ / moveDuration_; x = moveStartX_ + (moveEndX_ - moveStartX_) * t; y = moveStartY_ + (moveEndY_ - moveStartY_) * t; z = moveStartZ_ + (moveEndZ_ - moveStartZ_) * t; + } else { + // Past the interpolation window: dead-reckon at the smoothed velocity + // rather than freezing in place. Cap to one extra interval so we don't + // drift endlessly if the entity stops sending packets. + float overrun = moveElapsed_ - moveDuration_; + if (overrun < moveDuration_) { + x = moveEndX_ + velX_ * overrun; + y = moveEndY_ + velY_ * overrun; + z = moveEndZ_ + velZ_ * overrun; + } else { + // Two intervals with no update — entity has probably stopped. + x = moveEndX_; y = moveEndY_; z = moveEndZ_; + velX_ = 0.0f; velY_ = 0.0f; velZ_ = 0.0f; + isMoving_ = false; + } } } @@ -155,6 +183,7 @@ protected: float moveEndX_ = 0, moveEndY_ = 0, moveEndZ_ = 0; float moveDuration_ = 0; float moveElapsed_ = 0; + float velX_ = 0, velY_ = 0, velZ_ = 0; // Smoothed velocity for dead reckoning }; /** diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 61e7661d..f1b92b5f 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1330,6 +1330,7 @@ private: std::unordered_map> otherPlayerVisibleItemEntries_; std::unordered_set otherPlayerVisibleDirty_; std::unordered_map otherPlayerMoveTimeMs_; + std::unordered_map otherPlayerSmoothedIntervalMs_; // EMA of packet intervals // Inspect fallback (when visible item fields are missing/unreliable) std::unordered_map> inspectedPlayerItemEntries_; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 9ce45d01..cb81e4cc 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -7641,15 +7641,23 @@ void GameHandler::handleOtherPlayerMovement(network::Packet& packet) { // Convert server coords to canonical glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(info.x, info.y, info.z)); float canYaw = core::coords::serverToCanonicalYaw(info.orientation); - // Smooth movement between client-relayed snapshots so animations can play. - uint32_t durationMs = 100; + // Compute a smoothed interpolation window for this player. + // Using a raw packet delta causes jitter when timing spikes (e.g. 50ms then 300ms). + // An exponential moving average of intervals gives a stable playback speed that + // dead-reckoning in Entity::updateMovement() can bridge without a visible freeze. + uint32_t durationMs = 120; auto itPrev = otherPlayerMoveTimeMs_.find(moverGuid); if (itPrev != otherPlayerMoveTimeMs_.end()) { - uint32_t dt = info.time - itPrev->second; // handles wrap - if (dt >= 30 && dt <= 1000) { - if (dt < 50) dt = 50; - if (dt > 350) dt = 350; - durationMs = dt; + uint32_t rawDt = info.time - itPrev->second; // wraps naturally on uint32_t + if (rawDt >= 20 && rawDt <= 2000) { + float fDt = static_cast(rawDt); + // EMA: smooth the interval so single spike packets don't stutter playback. + auto& smoothed = otherPlayerSmoothedIntervalMs_[moverGuid]; + if (smoothed < 1.0f) smoothed = fDt; // first observation — seed directly + smoothed = 0.7f * smoothed + 0.3f * fDt; + // Clamp to sane WoW movement rates: ~10 Hz (100ms) normal, up to 2Hz (500ms) slow + float clamped = std::max(60.0f, std::min(500.0f, smoothed)); + durationMs = static_cast(clamped); } } otherPlayerMoveTimeMs_[moverGuid] = info.time;