mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
refactor(Core/Movement): Funnel all MoveTo through MoveTo2 path-aware pipeline
This commit is contained in:
parent
59d9bc6f78
commit
eada942a4e
@ -31,6 +31,7 @@
|
||||
#include "Position.h"
|
||||
#include "PositionValue.h"
|
||||
#include "Random.h"
|
||||
#include "RandomPlayerbotMgr.h"
|
||||
#include "ServerFacade.h"
|
||||
#include "SharedDefines.h"
|
||||
#include "SpellAuraEffects.h"
|
||||
@ -292,53 +293,30 @@ bool MovementAction::MoveToLOS(WorldObject* target, bool ranged)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle, bool react, bool normal_only,
|
||||
bool exact_waypoint, MovementPriority priority, bool lessDelay, bool backwards)
|
||||
bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle, bool react,
|
||||
[[maybe_unused]] bool normal_only,
|
||||
bool exact_waypoint, MovementPriority priority, bool lessDelay,
|
||||
bool backwards, bool ignoreEnemyTargets)
|
||||
{
|
||||
UpdateMovementState();
|
||||
if (!IsMovingAllowed())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (IsDuplicateMove(x, y, z))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool generatePath = !bot->IsFlying() && !bot->isSwimming();
|
||||
bool disableMoveSplinePath =
|
||||
bool const generatePath = !bot->IsFlying() && !bot->isSwimming();
|
||||
bool const disableMoveSplinePath =
|
||||
sPlayerbotAIConfig.disableMoveSplinePath >= 2 ||
|
||||
(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))
|
||||
// {
|
||||
// bot->CastStop();
|
||||
// botAI->InterruptSpell();
|
||||
// }
|
||||
DoMovePoint(bot, x, y, z, generatePath, backwards);
|
||||
float delay = 1000.0f * MoveDelay(distance, 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
|
||||
// Intentional bypass — skip the path-aware pipeline and dispatch
|
||||
// straight to DoMovePoint. Cases:
|
||||
// exact_waypoint: caller wants the raw target, no clipping
|
||||
// disableMoveSplinePath: config-driven engine fallback
|
||||
// flying/swimming: pathfinding via engine MovePoint, not mmap probe
|
||||
// backwards: AC-specific back-shuffle; no parity in MoveTo2
|
||||
if (exact_waypoint || disableMoveSplinePath || !generatePath || backwards)
|
||||
{
|
||||
// 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);
|
||||
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);
|
||||
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)
|
||||
@ -450,8 +433,11 @@ bool MovementAction::ReachCombatTo(Unit* target, float distance)
|
||||
|
||||
path.ShortenPathUntilDist(G3D::Vector3(tx, ty, tz), shortenTo);
|
||||
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,
|
||||
MovementPriority::MOVEMENT_COMBAT, true);
|
||||
MovementPriority::MOVEMENT_COMBAT, true, false, /*ignoreEnemyTargets*/true);
|
||||
// Only emit on a successful new commit — combat ticks call this
|
||||
// many times per second and MoveTo internally suppresses while a
|
||||
// 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);
|
||||
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;
|
||||
}
|
||||
|
||||
@ -56,7 +56,35 @@ protected:
|
||||
bool MoveTo(uint32 mapId, float x, float y, float z, bool idle = false, bool react = false,
|
||||
bool normal_only = false, bool exact_waypoint = 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,
|
||||
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
|
||||
bool MoveNear(WorldObject* target, float distance = sPlayerbotAIConfig.contactDistance,
|
||||
|
||||
@ -48,241 +48,7 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
EmitDebugMove("MoveFar", "empty-dest", 0.0f, 0.0f, 0.0f);
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
return MoveTo2(dest);
|
||||
}
|
||||
|
||||
bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance)
|
||||
|
||||
@ -82,15 +82,6 @@ protected:
|
||||
bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user