diff --git a/include/game/entity.hpp b/include/game/entity.hpp index 4845e880..2166bfa8 100644 --- a/include/game/entity.hpp +++ b/include/game/entity.hpp @@ -1,7 +1,10 @@ #pragma once #include +#include #include +#include +#include #include #include #include @@ -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>& 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> pathPoints_; + std::vector pathSegDists_; // Cumulative distances for each waypoint }; /** diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 19cbf3ba..8734a815 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -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 waypoints; }; class MonsterMoveParser { diff --git a/src/core/application.cpp b/src/core/application.cpp index 2a006a65..966bbba5 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -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); diff --git a/src/game/movement_handler.cpp b/src/game/movement_handler.cpp index d190389e..6c814235 100644 --- a/src/game/movement_handler.cpp +++ b/src/game/movement_handler.cpp @@ -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> 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, diff --git a/src/game/world_packets_entity.cpp b/src/game/world_packets_entity.cpp index ce281232..f529da70 100644 --- a/src/game/world_packets_entity.cpp +++ b/src/game/world_packets_entity.cpp @@ -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(packed & 0x7FF); + if (sx & 0x400) sx |= static_cast(0xFFFFF800); + int32_t sy = static_cast((packed >> 11) & 0x7FF); + if (sy & 0x400) sy |= static_cast(0xFFFFF800); + int32_t sz = static_cast((packed >> 22) & 0x3FF); + if (sz & 0x200) sz |= static_cast(0xFFFFFC00); + MonsterMoveData::Point wp; + wp.x = midX - static_cast(sx) * 0.25f; + wp.y = midY - static_cast(sy) * 0.25f; + wp.z = midZ - static_cast(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(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(packed & 0x7FF); + if (sx & 0x400) sx |= static_cast(0xFFFFF800); + int32_t sy = static_cast((packed >> 11) & 0x7FF); + if (sy & 0x400) sy |= static_cast(0xFFFFF800); + int32_t sz = static_cast((packed >> 22) & 0x3FF); + if (sz & 0x200) sz |= static_cast(0xFFFFFC00); + MonsterMoveData::Point wp; + wp.x = midX - static_cast(sx) * 0.25f; + wp.y = midY - static_cast(sy) * 0.25f; + wp.z = midZ - static_cast(sz) * 0.25f; + data.waypoints.push_back(wp); + } } LOG_DEBUG("MonsterMove(turtle): guid=0x", std::hex, data.guid, std::dec,