diff --git a/include/game/movement_handler.hpp b/include/game/movement_handler.hpp index f8e4276c..2a195e83 100644 --- a/include/game/movement_handler.hpp +++ b/include/game/movement_handler.hpp @@ -37,6 +37,14 @@ public: // Follow target (moved from GameHandler) void followTarget(); void cancelFollow(); + // Per-frame movement toward the followed target's live position, called + // from GameHandler::update() alongside the existing followRenderPos_ + // refresh. A no-op when no follow target is set. Recomputes the target + // position fresh every call (from the live entity, not a stored point), + // so it doesn't need the long-distance Z-interpolation guard the + // headless client's single-shot /movement/goto needs - there's never a + // large stale remaining distance to interpolate across. + void updateFollowMovement(float deltaTime); // Area trigger detection void loadAreaTriggerDbc(); @@ -287,6 +295,8 @@ private: uint32_t knownTaxiMask_[12] = {}; bool taxiMaskInitialized_ = false; std::unordered_map taxiCostMap_; + + bool followMoveMoving_ = false; // whether MSG_MOVE_START_FORWARD has been sent for the current follow }; } // namespace game diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 5bb4a766..187e3fac 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -813,6 +813,7 @@ void GameHandler::update(float deltaTime) { if (followEnt) { followRenderPos_ = core::coords::canonicalToRender( glm::vec3(followEnt->getX(), followEnt->getY(), followEnt->getZ())); + if (movementHandler_) movementHandler_->updateFollowMovement(deltaTime); } else { cancelFollow(); } diff --git a/src/game/movement_handler.cpp b/src/game/movement_handler.cpp index 0acffacb..03c33f88 100644 --- a/src/game/movement_handler.cpp +++ b/src/game/movement_handler.cpp @@ -3085,6 +3085,10 @@ void MovementHandler::cancelFollow() { return; } owner_.followTargetGuidRef() = 0; + if (followMoveMoving_) { + sendMovement(Opcode::MSG_MOVE_STOP); + followMoveMoving_ = false; + } if (owner_.autoFollowCallbackRef()) { owner_.autoFollowCallbackRef()(nullptr); } @@ -3092,5 +3096,65 @@ void MovementHandler::cancelFollow() { owner_.fireAddonEvent("AUTOFOLLOW_END", {}); } +// Same straight-line step used by the headless client's single-shot +// /movement/goto (updateMovementTask() in tools/headless_client/main.cpp), +// but the target position is read fresh from the live entity every call +// instead of a stored waypoint, and stops at kFollowStopDistance rather +// than snapping onto the target. +void MovementHandler::updateFollowMovement(float deltaTime) { + const uint64_t guid = owner_.followTargetGuidRef(); + if (guid == 0) { + return; + } + if (owner_.getState() != WorldState::IN_WORLD) { + return; + } + if (isPlayerRooted() || !isServerMovementAllowed() || isOnTaxiFlight()) { + if (followMoveMoving_) { + sendMovement(Opcode::MSG_MOVE_STOP); + followMoveMoving_ = false; + } + return; + } + + auto target = owner_.getEntityManager().getEntity(guid); + if (!target) { + // cancelFollow() (called from GameHandler::update()'s + // followRenderPos_ refresh) handles the disappeared-target case; + // just stay quiet here rather than duplicate that check. + return; + } + + constexpr float kFollowStopDistance = 1.0f; + const float dx = target->getX() - movementInfo.x; + const float dy = target->getY() - movementInfo.y; + const float dz = target->getZ() - movementInfo.z; + const float horizontalDist = std::sqrt(dx * dx + dy * dy); + const float distance = std::sqrt(dx * dx + dy * dy + dz * dz); + + if (distance <= kFollowStopDistance) { + if (followMoveMoving_) { + sendMovement(Opcode::MSG_MOVE_STOP); + followMoveMoving_ = false; + } + return; + } + + if (horizontalDist > 0.001f) { + setOrientation(std::atan2(-dy, dx)); + sendMovement(Opcode::MSG_MOVE_SET_FACING); + } + if (!followMoveMoving_) { + sendMovement(Opcode::MSG_MOVE_START_FORWARD); + followMoveMoving_ = true; + } + + const float speed = std::max(0.1f, getServerRunSpeed()); + const float step = std::min(distance - kFollowStopDistance, speed * std::max(0.0f, deltaTime)); + const float t = distance > 0.001f ? (step / distance) : 0.0f; + setPosition(movementInfo.x + dx * t, movementInfo.y + dy * t, movementInfo.z + dz * t); + sendMovement(Opcode::MSG_MOVE_HEARTBEAT); +} + } // namespace game } // namespace wowee diff --git a/src/ui/chat/commands/misc_commands.cpp b/src/ui/chat/commands/misc_commands.cpp index f049f11b..5722e787 100644 --- a/src/ui/chat/commands/misc_commands.cpp +++ b/src/ui/chat/commands/misc_commands.cpp @@ -189,7 +189,7 @@ public: return {}; } std::vector aliases() const override { return {"follow", "f"}; } - std::string helpText() const override { return "Follow target"; } + std::string helpText() const override { return "Walk toward and camera-follow your current target"; } }; // --- /stopfollow ---