fix(movement): multi-segment path interpolation, waypoint parsing & terrain Z clamping

Add proper waypoint support to entity movement:

- Parse intermediate waypoints from MonsterMove packets in both WotLK
  and Vanilla paths. Uncompressed paths store absolute float3 waypoints;
  compressed paths decode TrinityCore's packed uint32 deltas (11-bit
  signed x/y, 10-bit signed z, ×0.25 scale, waypoint = midpoint − delta)
  with correct 2's-complement sign extension.

- Entity::startMoveAlongPath() interpolates along cumulative-distance-
  proportional segments instead of a single straight line.

- MovementHandler builds the full path (start → waypoints → destination)
  in canonical coords and dispatches to startMoveAlongPath() when
  waypoints are present.

- Snap entity x/y/z to moveEnd in the dead-reckoning overrun phase
  before starting a new movement, preventing visible teleports when the
  renderer was showing the entity at its destination.

- Clamp creature and player entity Z to the terrain surface via
  TerrainManager::getHeightAt() during active movement. Idle entities
  keep their server-authoritative Z to avoid breaking flight masters,
  elevator riders, etc.

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
This commit is contained in:
Pavel Okhlopkov 2026-04-10 20:35:18 +03:00
parent e07983b7f6
commit 5b47d034c5
5 changed files with 182 additions and 19 deletions

View file

@ -1,7 +1,10 @@
#pragma once
#include <cstdint>
#include <cmath>
#include <string>
#include <array>
#include <vector>
#include <map>
#include <unordered_map>
#include <memory>
@ -76,14 +79,70 @@ public:
z = pz;
orientation = o;
isMoving_ = false; // Instant position set cancels interpolation
usePathMode_ = false;
}
// Multi-segment path movement
void startMoveAlongPath(const std::vector<std::array<float, 3>>& path, float destO, float totalDuration) {
if (path.empty()) return;
if (path.size() == 1 || totalDuration <= 0.0f) {
startMoveTo(path.back()[0], path.back()[1], path.back()[2], destO, totalDuration);
return;
}
// Compute cumulative distances for proportional segment timing
pathPoints_ = path;
pathSegDists_.resize(path.size());
pathSegDists_[0] = 0.0f;
float totalDist = 0.0f;
for (size_t i = 1; i < path.size(); i++) {
float dx = path[i][0] - path[i - 1][0];
float dy = path[i][1] - path[i - 1][1];
float dz = path[i][2] - path[i - 1][2];
totalDist += std::sqrt(dx * dx + dy * dy + dz * dz);
pathSegDists_[i] = totalDist;
}
if (totalDist < 0.001f) {
startMoveTo(path.back()[0], path.back()[1], path.back()[2], destO, totalDuration);
return;
}
// Snap position if in overrun phase
if (isMoving_ && moveElapsed_ >= moveDuration_) {
x = moveEndX_; y = moveEndY_; z = moveEndZ_;
}
moveEndX_ = path.back()[0]; moveEndY_ = path.back()[1]; moveEndZ_ = path.back()[2];
moveDuration_ = totalDuration;
moveElapsed_ = 0.0f;
orientation = destO;
isMoving_ = true;
usePathMode_ = true;
// Velocity for dead-reckoning after path completes
float fromX = isMoving_ ? moveEndX_ : x;
float fromY = isMoving_ ? moveEndY_ : y;
float impliedVX = (path.back()[0] - fromX) / totalDuration;
float impliedVY = (path.back()[1] - fromY) / totalDuration;
float impliedVZ = (path.back()[2] - path[0][2]) / totalDuration;
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_;
}
// Movement interpolation (syncs entity position with renderer during movement)
void startMoveTo(float destX, float destY, float destZ, float destO, float durationSec) {
usePathMode_ = false;
if (durationSec <= 0.0f) {
setPosition(destX, destY, destZ, destO);
return;
}
// If we're in the dead-reckoning overrun phase, snap x/y/z back to the
// destination before using them as the new start. The renderer was showing
// the entity at moveEnd (via getLatest) during overrun, so the new
// interpolation must start there to avoid a visible teleport.
if (isMoving_ && moveElapsed_ >= moveDuration_) {
x = moveEndX_;
y = moveEndY_;
z = moveEndZ_;
}
// 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.
@ -113,11 +172,31 @@ public:
if (!isMoving_) return;
moveElapsed_ += deltaTime;
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;
if (usePathMode_ && pathPoints_.size() > 1) {
// Multi-segment path interpolation
float totalDist = pathSegDists_.back();
float t = moveElapsed_ / moveDuration_;
float targetDist = t * totalDist;
// Find the segment containing targetDist
size_t seg = 1;
while (seg < pathSegDists_.size() - 1 && pathSegDists_[seg] < targetDist)
seg++;
float segStart = pathSegDists_[seg - 1];
float segEnd = pathSegDists_[seg];
float segLen = segEnd - segStart;
float segT = (segLen > 0.001f) ? (targetDist - segStart) / segLen : 0.0f;
const auto& p0 = pathPoints_[seg - 1];
const auto& p1 = pathPoints_[seg];
x = p0[0] + (p1[0] - p0[0]) * segT;
y = p0[1] + (p1[1] - p0[1]) * segT;
z = p0[2] + (p1[2] - p0[2]) * segT;
} else {
// Single-segment linear interpolation
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
@ -192,11 +271,15 @@ protected:
// Movement interpolation state
bool isMoving_ = false;
bool usePathMode_ = false;
float moveStartX_ = 0, moveStartY_ = 0, moveStartZ_ = 0;
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
// Multi-segment path data
std::vector<std::array<float, 3>> pathPoints_;
std::vector<float> pathSegDists_; // Cumulative distances for each waypoint
};
/**

View file

@ -1678,6 +1678,9 @@ struct MonsterMoveData {
// Destination (final point of the spline, server coords)
float destX = 0, destY = 0, destZ = 0;
bool hasDest = false;
// Intermediate waypoints along the path (server coords, excludes start & final dest)
struct Point { float x, y, z; };
std::vector<Point> waypoints;
};
class MonsterMoveParser {

View file

@ -1756,6 +1756,17 @@ void Application::update(float deltaTime) {
inOverrun ? entity->getLatestZ() : entity->getZ());
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
// Clamp creature Z to terrain surface during movement interpolation.
// The server sends single-segment moves and expects the client to place
// creatures on the ground. Only clamp while actively moving — idle
// creatures keep their server-authoritative Z (flight masters, etc.).
if (entity->isActivelyMoving() && renderer->getTerrainManager()) {
auto terrainZ = renderer->getTerrainManager()->getHeightAt(renderPos.x, renderPos.y);
if (terrainZ.has_value()) {
renderPos.z = terrainZ.value();
}
}
// Visual collision guard: keep hostile melee units from rendering inside the
// player's model while attacking. This is client-side only (no server position change).
// Only check for creatures within 8 units (melee range) — saves expensive
@ -1959,6 +1970,14 @@ void Application::update(float deltaTime) {
inOverrun ? entity->getLatestZ() : entity->getZ());
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
// Clamp other players' Z to terrain surface during movement
if (entity->isActivelyMoving() && renderer->getTerrainManager()) {
auto terrainZ = renderer->getTerrainManager()->getHeightAt(renderPos.x, renderPos.y);
if (terrainZ.has_value()) {
renderPos.z = terrainZ.value();
}
}
auto posIt = _pCreatureRenderPosCache.find(guid);
if (posIt == _pCreatureRenderPosCache.end()) {
charRenderer->setInstancePosition(instanceId, renderPos);

View file

@ -1454,8 +1454,23 @@ void MovementHandler::handleMonsterMove(network::Packet& packet) {
}
}
entity->startMoveTo(destCanonical.x, destCanonical.y, destCanonical.z,
orientation, data.duration / 1000.0f);
// Build full path: start → waypoints → destination (all in canonical coords)
if (!data.waypoints.empty()) {
glm::vec3 startCanonical = core::coords::serverToCanonical(
glm::vec3(data.x, data.y, data.z));
std::vector<std::array<float, 3>> path;
path.push_back({startCanonical.x, startCanonical.y, startCanonical.z});
for (const auto& wp : data.waypoints) {
glm::vec3 wpCanonical = core::coords::serverToCanonical(
glm::vec3(wp.x, wp.y, wp.z));
path.push_back({wpCanonical.x, wpCanonical.y, wpCanonical.z});
}
path.push_back({destCanonical.x, destCanonical.y, destCanonical.z});
entity->startMoveAlongPath(path, orientation, data.duration / 1000.0f);
} else {
entity->startMoveTo(destCanonical.x, destCanonical.y, destCanonical.z,
orientation, data.duration / 1000.0f);
}
if (owner_.creatureMoveCallbackRef()) {
owner_.creatureMoveCallbackRef()(data.guid,

View file

@ -637,13 +637,15 @@ bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) {
bool uncompressed = (data.splineFlags & (0x00080000 | 0x00002000)) != 0;
if (uncompressed) {
// Read last point as destination
// Skip to last point: each point is 12 bytes
if (pointCount > 1) {
for (uint32_t i = 0; i < pointCount - 1; i++) {
if (!packet.hasRemaining(12)) return true;
packet.readFloat(); packet.readFloat(); packet.readFloat();
}
// All waypoints stored as absolute float3 (Catmullrom/Flying paths)
// Read all intermediate points, then the final destination
for (uint32_t i = 0; i < pointCount - 1; i++) {
if (!packet.hasRemaining(12)) return true;
MonsterMoveData::Point wp;
wp.x = packet.readFloat();
wp.y = packet.readFloat();
wp.z = packet.readFloat();
data.waypoints.push_back(wp);
}
if (!packet.hasRemaining(12)) return true;
data.destX = packet.readFloat();
@ -657,6 +659,33 @@ bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) {
data.destY = packet.readFloat();
data.destZ = packet.readFloat();
data.hasDest = true;
// Remaining waypoints are packed as uint32 deltas from the midpoint
// between the creature's start position and the destination.
// Encoding matches TrinityCore MoveSpline::PackXYZ:
// x = 11-bit signed (bits 0-10), y = 11-bit signed (bits 11-21),
// z = 10-bit signed (bits 22-31), each scaled by 0.25 units.
if (pointCount > 1) {
float midX = (data.x + data.destX) * 0.5f;
float midY = (data.y + data.destY) * 0.5f;
float midZ = (data.z + data.destZ) * 0.5f;
for (uint32_t i = 0; i < pointCount - 1; i++) {
if (!packet.hasRemaining(4)) break;
uint32_t packed = packet.readUInt32();
// Sign-extend 11-bit x and y, 10-bit z (2's complement)
int32_t sx = static_cast<int32_t>(packed & 0x7FF);
if (sx & 0x400) sx |= static_cast<int32_t>(0xFFFFF800);
int32_t sy = static_cast<int32_t>((packed >> 11) & 0x7FF);
if (sy & 0x400) sy |= static_cast<int32_t>(0xFFFFF800);
int32_t sz = static_cast<int32_t>((packed >> 22) & 0x3FF);
if (sz & 0x200) sz |= static_cast<int32_t>(0xFFFFFC00);
MonsterMoveData::Point wp;
wp.x = midX - static_cast<float>(sx) * 0.25f;
wp.y = midY - static_cast<float>(sy) * 0.25f;
wp.z = midZ - static_cast<float>(sz) * 0.25f;
data.waypoints.push_back(wp);
}
}
}
LOG_DEBUG("MonsterMove: guid=0x", std::hex, data.guid, std::dec,
@ -754,12 +783,26 @@ bool MonsterMoveParser::parseVanilla(network::Packet& packet, MonsterMoveData& d
data.destZ = packet.readFloat();
data.hasDest = true;
// Remaining waypoints are packed as uint32 deltas.
// Remaining waypoints are packed as uint32 deltas from midpoint.
if (pointCount > 1) {
size_t skipBytes = static_cast<size_t>(pointCount - 1) * 4;
size_t newPos = packet.getReadPos() + skipBytes;
if (newPos > packet.getSize()) return false;
packet.setReadPos(newPos);
float midX = (data.x + data.destX) * 0.5f;
float midY = (data.y + data.destY) * 0.5f;
float midZ = (data.z + data.destZ) * 0.5f;
for (uint32_t i = 0; i < pointCount - 1; i++) {
if (!packet.hasRemaining(4)) break;
uint32_t packed = packet.readUInt32();
int32_t sx = static_cast<int32_t>(packed & 0x7FF);
if (sx & 0x400) sx |= static_cast<int32_t>(0xFFFFF800);
int32_t sy = static_cast<int32_t>((packed >> 11) & 0x7FF);
if (sy & 0x400) sy |= static_cast<int32_t>(0xFFFFF800);
int32_t sz = static_cast<int32_t>((packed >> 22) & 0x3FF);
if (sz & 0x200) sz |= static_cast<int32_t>(0xFFFFFC00);
MonsterMoveData::Point wp;
wp.x = midX - static_cast<float>(sx) * 0.25f;
wp.y = midY - static_cast<float>(sy) * 0.25f;
wp.z = midZ - static_cast<float>(sz) * 0.25f;
data.waypoints.push_back(wp);
}
}
LOG_DEBUG("MonsterMove(turtle): guid=0x", std::hex, data.guid, std::dec,