refactor(Core/Movement): Funnel all MoveTo through MoveTo2 path-aware pipeline

This commit is contained in:
bash 2026-05-31 17:55:30 +02:00
parent 97b3e345a8
commit bd7422e98b
4 changed files with 277 additions and 283 deletions

View File

@ -31,6 +31,7 @@
#include "Position.h" #include "Position.h"
#include "PositionValue.h" #include "PositionValue.h"
#include "Random.h" #include "Random.h"
#include "RandomPlayerbotMgr.h"
#include "ServerFacade.h" #include "ServerFacade.h"
#include "SharedDefines.h" #include "SharedDefines.h"
#include "SpellAuraEffects.h" #include "SpellAuraEffects.h"
@ -292,53 +293,30 @@ bool MovementAction::MoveToLOS(WorldObject* target, bool ranged)
return false; return false;
} }
bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle, bool react, bool normal_only, bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle, bool react,
bool exact_waypoint, MovementPriority priority, bool lessDelay, bool backwards) [[maybe_unused]] bool normal_only,
bool exact_waypoint, MovementPriority priority, bool lessDelay,
bool backwards, bool ignoreEnemyTargets)
{ {
UpdateMovementState(); UpdateMovementState();
if (!IsMovingAllowed()) if (!IsMovingAllowed())
{
return false; return false;
}
if (IsDuplicateMove(x, y, z)) if (IsDuplicateMove(x, y, z))
{
return false; return false;
}
bool generatePath = !bot->IsFlying() && !bot->isSwimming(); bool const generatePath = !bot->IsFlying() && !bot->isSwimming();
bool disableMoveSplinePath = bool const disableMoveSplinePath =
sPlayerbotAIConfig.disableMoveSplinePath >= 2 || sPlayerbotAIConfig.disableMoveSplinePath >= 2 ||
(sPlayerbotAIConfig.disableMoveSplinePath == 1 && bot->InBattleground()); (sPlayerbotAIConfig.disableMoveSplinePath == 1 && bot->InBattleground());
if (exact_waypoint || disableMoveSplinePath || !generatePath)
{
float distance = bot->GetExactDist(x, y, z);
if (distance > 0.01f)
{
if (!bot->IsStandState())
bot->SetStandState(UNIT_STAND_STATE_STAND);
// if (bot->IsNonMeleeSpellCast(true)) // Intentional bypass — skip the path-aware pipeline and dispatch
// { // straight to DoMovePoint. Cases:
// bot->CastStop(); // exact_waypoint: caller wants the raw target, no clipping
// botAI->InterruptSpell(); // disableMoveSplinePath: config-driven engine fallback
// } // flying/swimming: pathfinding via engine MovePoint, not mmap probe
DoMovePoint(bot, x, y, z, generatePath, backwards); // backwards: AC-specific back-shuffle; no parity in MoveTo2
float delay = 1000.0f * MoveDelay(distance, backwards); if (exact_waypoint || disableMoveSplinePath || !generatePath || backwards)
if (lessDelay)
{ {
delay -= botAI->GetReactDelay();
}
delay = std::max(.0f, delay);
delay = std::min((float)sPlayerbotAIConfig.maxWaitForMove, delay);
AI_VALUE(LastMovement&, "last movement").Set(mapId, x, y, z, bot->GetOrientation(), delay, priority);
return true;
}
}
else
{
// Direct dispatch — engine MovePoint(generatePath=true) handles
// pathfinding. Avoid ±z probes: their "shortest path" preference
// can pick an unreachable ledge and air-walk via NOPATH fallback.
float distance = bot->GetExactDist(x, y, z); float distance = bot->GetExactDist(x, y, z);
if (distance > 0.01f) if (distance > 0.01f)
{ {
@ -355,9 +333,14 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle,
.Set(mapId, x, y, z, bot->GetOrientation(), delay, priority); .Set(mapId, x, y, z, bot->GetOrientation(), delay, priority);
return true; return true;
} }
return false;
} }
return false; // Path-aware funnel: ResolveMovePath → makeShortCut →
// UpcommingSpecialMovement/HandleSpecialMovement → ClipPath →
// DispatchPathPoints. Matches the reference's MoveTo2 flow.
return MoveTo2(WorldPosition(mapId, x, y, z),
idle, react, false, ignoreEnemyTargets, priority, lessDelay);
} }
bool MovementAction::MoveTo(WorldObject* target, float distance, MovementPriority priority) bool MovementAction::MoveTo(WorldObject* target, float distance, MovementPriority priority)
@ -450,8 +433,11 @@ bool MovementAction::ReachCombatTo(Unit* target, float distance)
path.ShortenPathUntilDist(G3D::Vector3(tx, ty, tz), shortenTo); path.ShortenPathUntilDist(G3D::Vector3(tx, ty, tz), shortenTo);
G3D::Vector3 endPos = path.GetPath().back(); G3D::Vector3 endPos = path.GetPath().back();
// Combat callers pass ignoreEnemyTargets=true so ClipPath doesn't
// halt the chase at an intermediate hostile when funnelling through
// MoveTo2 — the chase target itself is the enemy we want to reach.
bool moved = MoveTo(target->GetMapId(), endPos.x, endPos.y, endPos.z, false, false, false, false, bool moved = MoveTo(target->GetMapId(), endPos.x, endPos.y, endPos.z, false, false, false, false,
MovementPriority::MOVEMENT_COMBAT, true); MovementPriority::MOVEMENT_COMBAT, true, false, /*ignoreEnemyTargets*/true);
// Only emit on a successful new commit — combat ticks call this // Only emit on a successful new commit — combat ticks call this
// many times per second and MoveTo internally suppresses while a // many times per second and MoveTo internally suppresses while a
// prior spline is still playing. Emitting before the suppression // prior spline is still playing. Emitting before the suppression
@ -2883,3 +2869,226 @@ bool MovementAction::BoardTransport(Transport* transport)
EmitDebugMove("Transport:board", "snap", edgeX, edgeY, edgeZ); EmitDebugMove("Transport:board", "snap", edgeX, edgeY, edgeZ);
return true; return true;
} }
bool MovementAction::MoveTo2(WorldPosition const& endPos,
bool idle, [[maybe_unused]] bool react,
[[maybe_unused]] bool noPath,
bool ignoreEnemyTargets,
MovementPriority priority,
bool lessDelay)
{
if (!endPos.isValid())
return false;
UpdateMovementState();
if (!IsMovingAllowed())
return false;
// Resume a transport ride if we're still on the same boat as last tick.
if (WaitForTransport())
return true;
WorldPosition botPos(bot);
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
// Short-stop: at destination — stop and clear the cached path.
float const totalDistance = botPos.distance(endPos);
if (totalDistance < sPlayerbotAIConfig.targetPosRecalcDistance)
{
if (!lastMove.lastPath.empty() &&
lastMove.lastPath.getBack().distance(endPos) <= totalDistance)
lastMove.clear();
bot->StopMoving();
return false;
}
// Per-tick re-resolve: rebuild the TravelPath from the bot's current
// position every tick. ResolveMovePath internally gates graph A* by
// sightDistance — short moves skip the graph and use a raw probe, so
// funnelling every MoveTo here is cost-bounded for in-zone moves.
TravelPath path = ResolveMovePath(botPos, endPos, lastMove);
lastMove.setPath(path);
if (path.empty())
return false;
// Trim leading waypoints behind the bot. Skip on transports — bot's
// world-space position diverges from path coords mid-ride.
if (!bot->GetTransport())
path.makeShortCut(botPos, sPlayerbotAIConfig.reactDistance, bot);
if (path.empty())
{
lastMove.setPath(path);
return true;
}
bool const onTransport = bot->GetTransport() != nullptr;
if (path.UpcommingSpecialMovement(botPos,
sPlayerbotAIConfig.reactDistance,
onTransport))
{
if (HandleSpecialMovement(path))
return true;
// Special handler declined (e.g. AREA_TRIGGER with entry → caller
// dispatches the walk into the trigger volume). Fall through.
}
// Transport guard: bot is on a transport but no special movement
// applies this tick — don't dispatch a walk spline (would fight the
// transport's own movement).
if (onTransport)
return false;
if (!path.empty())
lastMove.setPath(path);
// ClipPath — truncate at first hostile creature in range / non-walkable
// hop / drifted past reactDistance / > 125 sqDist jump. Combat callers
// pass ignoreEnemyTargets=true so the chase doesn't stop at an
// intermediate enemy.
path.ClipPath(botAI, bot, ignoreEnemyTargets);
if (path.empty())
return false;
// Telemetry: show the path's actual tail coords vs bot + dest so we
// can see whether the resolved path is heading toward the right place.
if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
{
WorldPosition tail = path.getBack();
float const tailToDest = tail.distance(endPos);
float const botToTail = bot->GetExactDist(tail.GetPositionX(),
tail.GetPositionY(),
tail.GetPositionZ());
std::ostringstream tlog;
tlog << "[PATH] tail=(" << std::fixed << std::setprecision(1)
<< tail.GetPositionX() << "," << tail.GetPositionY() << ","
<< tail.GetPositionZ()
<< ") botToTail=" << botToTail << "y tailToDest=" << tailToDest << "y";
botAI->TellMasterNoFacing(tlog);
}
std::vector<WorldPosition> const& pts = path.getPointPath();
Movement::PointsArray points;
points.reserve(pts.size());
for (auto const& wp : pts)
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
if (points.empty())
return false;
if (!bot->IsMounted() && !bot->IsInCombat() &&
bot->IsOutdoors() && bot->IsAlive())
botAI->DoSpecificAction("check mount state", Event(), true);
bool const dispatched =
DispatchPathPoints(endPos, points, "walk", priority, lessDelay);
if (dispatched && !idle)
ClearIdleState();
return dispatched;
}
bool MovementAction::DispatchPathPoints(WorldPosition const& dest,
Movement::PointsArray& points,
char const* label,
MovementPriority priority,
bool lessDelay)
{
if (points.empty())
return false;
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
G3D::Vector3 const& last = points.back();
float totalDist = 0.f;
for (size_t i = 1; i < points.size(); ++i)
totalDist += (points[i] - points[i - 1]).length();
// Skip cosmetic walking for random bots with no nearby player —
// teleport to the path tail and schedule a cooldown instead.
// (Reference equivalent: long-distance teleport in MoveTo2 gated
// on !detailedMove && !HasPlayerNearby. Our gate is IsRandomBot.)
if (sRandomPlayerbotMgr.IsRandomBot(bot))
{
WorldPosition tail(dest.GetMapId(), last.x, last.y, last.z);
time_t now = time(nullptr);
if (totalDist > sPlayerbotAIConfig.reactDistance &&
lastMove.nextTeleport <= now &&
!botAI->HasPlayerNearby(&tail))
{
float speed = std::max(bot->GetSpeed(MOVE_RUN), 0.1f);
lastMove.nextTeleport = now + (time_t)(totalDist / speed);
EmitDebugMove("MoveFar", "teleport",
tail.GetPositionX(), tail.GetPositionY(), tail.GetPositionZ());
WorldPosition botPos(bot);
return bot->TeleportTo(dest.GetMapId(),
tail.GetPositionX(), tail.GetPositionY(),
tail.GetPositionZ(),
botPos.getAngleTo(tail));
}
}
// Match master's walk pace when they're walking and within 5y.
// AC's ForcedMovement enum has no FLIGHT variant — flying is handled
// via the MovePoint speed/flight flags below, not the moveMode.
ForcedMovement moveMode = FORCED_MOVEMENT_RUN;
if (Player* master = botAI->GetMaster())
{
if (bot->IsFriendlyTo(master) && master->IsWalking() &&
bot->GetExactDist2d(master) < 5.0f)
{
moveMode = FORCED_MOVEMENT_WALK;
}
}
bool const generatePath = !bot->IsFlying() && !bot->isSwimming();
// Pre-dispatch normalization: clear looping emote, stand, interrupt
// non-melee cast. Reference does this at MoveTo2 level before
// DispatchMovement; we do it here at the equivalent point in the flow.
bot->ClearEmoteState();
if (!bot->IsStandState())
bot->SetStandState(UNIT_STAND_STATE_STAND);
if (bot->IsNonMeleeSpellCast(true))
bot->InterruptNonMeleeSpells(true);
// Per-point terrain clamp.
for (auto& pt : points)
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
// mm.Clear → MovePoint(last) → MoveSplinePath → WaitForReach.
MotionMaster* mm = bot->GetMotionMaster();
mm->Clear();
if (!generatePath || !bot->IsFreeFlying())
{
float const flySpeed = bot->IsFlying() ? bot->GetSpeed(MOVE_FLIGHT) : 0.0f;
mm->MovePoint(0, last.x, last.y, last.z, moveMode,
flySpeed, 0.0f, generatePath, false);
}
if (points.size() >= 2)
mm->MoveSplinePath(&points, moveMode);
EmitDebugMove("MoveFar", label, last.x, last.y, last.z);
// WaitForReach equivalent: cache the dispatched target + duration on
// lastMove. Leave ~10y headroom on long paths so we re-evaluate
// before arrival.
float waitDist = totalDist > sPlayerbotAIConfig.reactDistance
? std::max(totalDist - 10.0f, 0.0f) : totalDist;
UnitMoveType const speedType = (moveMode == FORCED_MOVEMENT_WALK) ? MOVE_WALK : MOVE_RUN;
float speed = std::max(bot->GetSpeed(speedType), 0.1f);
float duration = 1000.0f * (waitDist / speed) + sPlayerbotAIConfig.reactDelay;
if (lessDelay)
duration -= sPlayerbotAIConfig.reactDelay;
duration = std::min(duration, (float)sPlayerbotAIConfig.maxWaitForMove);
if (duration < 0.0f)
duration = 0.0f;
lastMove.Set(bot->GetMapId(), last.x, last.y, last.z,
bot->GetOrientation(), (uint32)duration, priority);
return true;
}

View File

@ -56,7 +56,35 @@ protected:
bool MoveTo(uint32 mapId, float x, float y, float z, bool idle = false, bool react = false, bool MoveTo(uint32 mapId, float x, float y, float z, bool idle = false, bool react = false,
bool normal_only = false, bool exact_waypoint = false, bool normal_only = false, bool exact_waypoint = false,
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, bool lessDelay = false, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, bool lessDelay = false,
bool backwards = false); bool backwards = false, bool ignoreEnemyTargets = false);
// Path-aware funnel mirroring the reference movement implementation.
// Runs UpdateMovementState + IsMovingAllowed + WaitForTransport gates,
// applies the targetPosRecalcDistance short-stop, resolves a TravelPath
// via ResolveMovePath (which gates graph A* by sightDistance), trims
// with makeShortCut, handles special head segments
// (portal/area-trigger/transport/flight) via HandleSpecialMovement,
// clips at hostile creatures via ClipPath (unless ignoreEnemyTargets),
// and dispatches the resulting walk via DispatchPathPoints.
// MoveTo(mapId,...) delegates here unless an intentional bypass
// (exact_waypoint / disableMoveSplinePath / flying / swimming /
// backwards) routes the move straight to DoMovePoint.
bool MoveTo2(WorldPosition const& endPos,
bool idle = false, bool react = false,
bool noPath = false, bool ignoreEnemyTargets = false,
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL,
bool lessDelay = false);
// Centralized walk dispatch. Applies inactive-bot teleport carve-out,
// masterWalking mode, pre-dispatch state cleanup (clear emote, stand,
// interrupt cast), per-point UpdateAllowedPositionZ, mm.Clear →
// MovePoint(last) → MoveSplinePath, and a WaitForReach equivalent
// that caches the destination + duration on lastMove.
bool DispatchPathPoints(WorldPosition const& dest,
Movement::PointsArray& points,
char const* label,
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL,
bool lessDelay = false);
bool MoveTo(WorldObject* target, float distance = 0.0f, bool MoveTo(WorldObject* target, float distance = 0.0f,
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL); MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
bool MoveNear(WorldObject* target, float distance = sPlayerbotAIConfig.contactDistance, bool MoveNear(WorldObject* target, float distance = sPlayerbotAIConfig.contactDistance,

View File

@ -48,241 +48,7 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
EmitDebugMove("MoveFar", "empty-dest", 0.0f, 0.0f, 0.0f); EmitDebugMove("MoveFar", "empty-dest", 0.0f, 0.0f, 0.0f);
return false; return false;
} }
return MoveTo2(dest);
UpdateMovementState();
if (!IsMovingAllowed())
{
EmitDebugMove("MoveFar", "cant-move",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return false;
}
// Resume a transport ride if we're still on the same boat as last tick.
if (WaitForTransport())
return true;
WorldPosition botPos(bot);
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
// Short-stop: at destination — stop and clear the cached path.
{
float const totalDistance = botPos.distance(dest);
if (totalDistance < sPlayerbotAIConfig.targetPosRecalcDistance)
{
if (!lastMove.lastPath.empty() &&
lastMove.lastPath.getBack().distance(dest) <= totalDistance)
lastMove.clear();
bot->StopMoving();
EmitDebugMove("MoveFar", "arrived",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return false;
}
}
// Per-tick re-resolve: rebuild the TravelPath from the bot's current
// position every tick (10% reuse short-circuits via the cached
// lastPath). Recovers naturally from knockback, off-route drift,
// destination changes, and blocked waypoints.
TravelPath path = ResolveMovePath(botPos, dest, lastMove);
lastMove.setPath(path);
if (path.empty())
{
EmitDebugMove("MoveFar", "no-path",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return false;
}
// Trim leading waypoints behind the bot, bridge with mmap probe if
// the new head requires it. May empty the path (collapsed) — clear
// the cached path explicitly so the next tick re-resolves cleanly.
path.makeShortCut(botPos, sPlayerbotAIConfig.reactDistance, bot);
if (path.empty())
{
lastMove.setPath(path);
return true;
}
// Special head segment (portal / area-trigger / transport / flight)?
// UpcommingSpecialMovement cuts the path so the head is the special;
// HandleSpecialMovement dispatches the matching action.
bool const onTransport = bot->GetTransport() != nullptr;
if (path.UpcommingSpecialMovement(botPos,
sPlayerbotAIConfig.reactDistance,
onTransport))
{
if (HandleSpecialMovement(path))
return true;
// Special handler declined (e.g. AREA_TRIGGER with entry → caller
// dispatches the walk into the trigger volume). Fall through.
}
// Transport guard: bot is on a transport but no special movement
// applies this tick — don't dispatch a walk spline (would fight the
// transport's own movement).
if (onTransport)
return false;
// Re-cache the (potentially cut) path so next tick's 10% reuse and
// WaitForTransport gates see the latest shape. Reference does this
// at the same point in MoveTo2 (after UpcommingSpecialMovement
// examined / trimmed the path, before ClipPath / dispatch).
if (!path.empty())
lastMove.setPath(path);
// ClipPath — truncate at first hostile creature in range / non-walkable
// hop / drifted past reactDistance / > 125 sqDist jump.
path.ClipPath(botAI, bot, false);
if (path.empty())
return false;
// Telemetry: show the path's actual tail coords vs bot + dest so we
// can see whether the resolved path is heading toward the right
// place. dest-distance == 0 means tail IS the dest (good); large
// dest-distance means graph picked a far-off endNode.
if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
{
WorldPosition tail = path.getBack();
float const tailToDest = tail.distance(dest);
float const botToTail = bot->GetExactDist(tail.GetPositionX(),
tail.GetPositionY(),
tail.GetPositionZ());
std::ostringstream tlog;
tlog << "[PATH] tail=(" << std::fixed << std::setprecision(1)
<< tail.GetPositionX() << "," << tail.GetPositionY() << "," << tail.GetPositionZ()
<< ") botToTail=" << botToTail << "y tailToDest=" << tailToDest << "y";
botAI->TellMasterNoFacing(tlog);
}
// Walk dispatch — DispatchPathPoints handles any path size (single
// point falls through to a MovePoint dispatch matching reference's
// DispatchMovement). No size<2 branch on this side.
std::vector<WorldPosition> const& pts = path.getPointPath();
Movement::PointsArray points;
points.reserve(pts.size());
for (auto const& wp : pts)
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
if (points.empty())
return false;
if (!bot->IsMounted() && !bot->IsInCombat() &&
bot->IsOutdoors() && bot->IsAlive())
botAI->DoSpecificAction("check mount state", Event(), true);
return DispatchPathPoints(dest, points, "walk");
}
bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest,
Movement::PointsArray& points,
char const* label)
{
if (points.empty())
return false;
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
G3D::Vector3 const& last = points.back();
float totalDist = 0.f;
for (size_t i = 1; i < points.size(); ++i)
totalDist += (points[i] - points[i - 1]).length();
// Skip cosmetic walking for random bots with no nearby player —
// teleport to the path tail and schedule a cooldown instead.
// (Reference equivalent: long-distance teleport in MoveTo2 gated
// on !detailedMove && !HasPlayerNearby. Our gate is IsRandomBot.)
if (sRandomPlayerbotMgr.IsRandomBot(bot))
{
WorldPosition tail(dest.GetMapId(), last.x, last.y, last.z);
time_t now = time(nullptr);
if (totalDist > sPlayerbotAIConfig.reactDistance &&
lastMove.nextTeleport <= now &&
!botAI->HasPlayerNearby(&tail))
{
float speed = std::max(bot->GetSpeed(MOVE_RUN), 0.1f);
lastMove.nextTeleport = now + (time_t)(totalDist / speed);
EmitDebugMove("MoveFar", "teleport",
tail.GetPositionX(), tail.GetPositionY(), tail.GetPositionZ());
WorldPosition botPos(bot);
return bot->TeleportTo(dest.GetMapId(),
tail.GetPositionX(), tail.GetPositionY(),
tail.GetPositionZ(),
botPos.getAngleTo(tail));
}
}
// Match master's walk pace when they're walking and within 5y.
// AC's ForcedMovement enum has no FLIGHT variant — flying is handled
// via the MovePoint speed/flight flags below, not the moveMode.
ForcedMovement moveMode = FORCED_MOVEMENT_RUN;
if (Player* master = botAI->GetMaster())
{
if (bot->IsFriendlyTo(master) && master->IsWalking() &&
bot->GetExactDist2d(master) < 5.0f)
{
moveMode = FORCED_MOVEMENT_WALK;
}
}
bool const generatePath = !bot->IsFlying() && !bot->isSwimming();
// Pre-dispatch normalization: clear looping emote, stand, interrupt
// non-melee cast. Reference does this at MoveTo2 level before
// DispatchMovement; we do it here at the equivalent point in the
// flow (immediately before the dispatch).
bot->ClearEmoteState();
if (!bot->IsStandState())
bot->SetStandState(UNIT_STAND_STATE_STAND);
if (bot->IsNonMeleeSpellCast(true))
bot->InterruptNonMeleeSpells(true);
// Per-point terrain clamp (reference does this on the converted
// pointPath). Skip transport-passenger conversion — our transport
// guard upstream prevents reaching here while on transport.
for (auto& pt : points)
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
// Reference DispatchMovement: mm.Clear → MovePoint(last) (for
// non-free-flying or when generatePath disabled) → MovePath(all) →
// WaitForReach. The MovePoint primes the motion master with a
// single-target goal so even if MoveSplinePath fails to register a
// ≥2-point spline, the bot still has something to walk to.
MotionMaster* mm = bot->GetMotionMaster();
mm->Clear();
if (!generatePath || !bot->IsFreeFlying())
{
float const flySpeed = bot->IsFlying() ? bot->GetSpeed(MOVE_FLIGHT) : 0.0f;
mm->MovePoint(0, last.x, last.y, last.z, moveMode,
flySpeed, 0.0f, generatePath, false);
}
if (points.size() >= 2)
mm->MoveSplinePath(&points, moveMode);
EmitDebugMove("MoveFar", label, last.x, last.y, last.z);
// WaitForReach equivalent: cache the dispatched target + duration
// on lastMove so the action chain knows the bot is mid-move.
// Leave ~10y headroom on long paths so we re-evaluate before
// arrival (matches reference's WaitForReach with a small slack).
float waitDist = totalDist > sPlayerbotAIConfig.reactDistance
? std::max(totalDist - 10.0f, 0.0f) : totalDist;
UnitMoveType const speedType = (moveMode == FORCED_MOVEMENT_WALK) ? MOVE_WALK : MOVE_RUN;
float speed = std::max(bot->GetSpeed(speedType), 0.1f);
float duration = 1000.0f * (waitDist / speed) + sPlayerbotAIConfig.reactDelay;
duration = std::min(duration, (float)sPlayerbotAIConfig.maxWaitForMove);
if (duration < 0.0f)
duration = 0.0f;
lastMove.Set(bot->GetMapId(), last.x, last.y, last.z,
bot->GetOrientation(), (uint32)duration,
MovementPriority::MOVEMENT_NORMAL);
return true;
} }
bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance) bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance)

View File

@ -82,15 +82,6 @@ protected:
bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus); bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
bool CheckRpgStatusAvailable(NewRpgStatus status); bool CheckRpgStatusAvailable(NewRpgStatus status);
private:
// Centralized dispatch helper. Applies underwater fixup, ClipPath
// (truncate at first hostile in attack range with LOS, level+5 cap),
// inactive-bot teleport (with self-bot carve-out), masterWalking
// mode, pre-dispatch state cleanup, then dispatches via
// MoveSplinePath and schedules via WaitForReach formula.
bool DispatchPathPoints(WorldPosition const& dest,
Movement::PointsArray& points,
char const* label);
}; };
#endif #endif