refactor(Core/Movement): Close walking-path divergences from reference (A-E)

This commit is contained in:
bash 2026-05-31 18:38:29 +02:00
parent 3f8aa0b6b3
commit 0b41ab391f
6 changed files with 105 additions and 13 deletions

View File

@ -31,7 +31,6 @@
#include "Position.h"
#include "PositionValue.h"
#include "Random.h"
#include "RandomPlayerbotMgr.h"
#include "ServerFacade.h"
#include "SharedDefines.h"
#include "SpellAuraEffects.h"
@ -436,8 +435,11 @@ bool MovementAction::ReachCombatTo(Unit* target, float distance)
// 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, false, /*ignoreEnemyTargets*/true);
// react=true skips the end-of-dispatch WaitForReach so the bot keeps
// re-evaluating mid-chase instead of waiting for the spline to play
// out (which would suspend combat reactions for seconds at a time).
bool moved = MoveTo(target->GetMapId(), endPos.x, endPos.y, endPos.z, /*idle*/false, /*react*/true, false, false,
MovementPriority::MOVEMENT_COMBAT, /*lessDelay*/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
@ -2898,7 +2900,7 @@ bool MovementAction::BoardTransport(Transport* transport)
}
bool MovementAction::MoveTo2(WorldPosition endPos,
bool idle, [[maybe_unused]] bool react,
bool idle, bool react,
[[maybe_unused]] bool noPath,
bool ignoreEnemyTargets,
MovementPriority priority,
@ -2918,6 +2920,25 @@ bool MovementAction::MoveTo2(WorldPosition endPos,
WorldPosition botPos(bot);
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
// Detailed-move throttle: if this bot is in low-activity mode
// (random/background) and a teleport cooldown is still in effect
// from a prior dispatch, postpone re-evaluation until the cooldown
// expires instead of re-resolving the path every tick.
bool const detailedMove = botAI->AllowActivity(DETAILED_MOVE_ACTIVITY, true);
if (!detailedMove && lastMove.nextTeleport)
{
time_t const now = time(nullptr);
if (lastMove.nextTeleport > now)
{
botAI->SetNextCheckDelay((uint32)((lastMove.nextTeleport - now) * 1000));
return true;
}
}
else
{
lastMove.nextTeleport = 0;
}
// Short-stop: at destination — stop and clear the cached path.
float const totalDistance = botPos.distance(endPos);
if (totalDistance < sPlayerbotAIConfig.targetPosRecalcDistance)
@ -2976,6 +2997,10 @@ bool MovementAction::MoveTo2(WorldPosition endPos,
if (path.empty())
return false;
// If destination is on land, snap any underwater waypoints to the
// water surface so the bot swims along the top instead of diving.
path.surfaceSnapWaypoints(endPos);
// 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))
@ -3001,7 +3026,7 @@ bool MovementAction::MoveTo2(WorldPosition endPos,
botAI->DoSpecificAction("check mount state", Event(), true);
bool const dispatched =
DispatchMovement(path, endPos, "walk", priority, lessDelay);
DispatchMovement(path, endPos, "walk", priority, lessDelay, react);
if (dispatched && !idle)
ClearIdleState();
@ -3013,7 +3038,8 @@ bool MovementAction::DispatchMovement(TravelPath path,
WorldPosition dest,
char const* label,
MovementPriority priority,
bool lessDelay)
bool lessDelay,
bool react)
{
// Build the PointsArray from the TravelPath. Done here (not at the
// caller) so DispatchMovement can be invoked with a TravelPath
@ -3033,11 +3059,11 @@ bool MovementAction::DispatchMovement(TravelPath path,
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))
// Skip cosmetic walking for low-activity bots with no nearby
// player — teleport to the path tail and schedule a cooldown
// instead. Matches the reference's MoveTo2 gate
// (`!detailedMove && !HasPlayerNearby`).
if (!botAI->AllowActivity(DETAILED_MOVE_ACTIVITY, true))
{
WorldPosition tail(dest.GetMapId(), last.x, last.y, last.z);
time_t now = time(nullptr);
@ -3072,7 +3098,11 @@ bool MovementAction::DispatchMovement(TravelPath path,
}
}
bool const generatePath = !bot->IsFlying() && !bot->isSwimming();
// Reference: also gates on !IsInWater && !IsUnderWater so a bot
// wading through shallow water (no SWIMMING movement flag yet)
// doesn't trigger engine pathfinding mid-dispatch.
bool const generatePath = !bot->IsFlying() && !bot->isSwimming() &&
!bot->IsInWater() && !bot->IsUnderWater();
// Pre-dispatch normalization: clear looping emote, stand, interrupt
// non-melee cast. Reference does this at MoveTo2 level before
@ -3132,5 +3162,11 @@ bool MovementAction::DispatchMovement(TravelPath path,
lastMove.Set(bot->GetMapId(), last.x, last.y, last.z,
bot->GetOrientation(), (uint32)duration, priority);
// Reference: DispatchMovement ends with WaitForReach(size) to block
// the AI loop while the spline plays. Combat callers (react=true)
// opt out so they can keep re-evaluating mid-chase.
if (!react)
WaitForReach(points);
return true;
}

View File

@ -69,6 +69,10 @@ protected:
// MoveTo(mapId,...) delegates here unless an intentional bypass
// (exact_waypoint / disableMoveSplinePath / flying / swimming /
// backwards) routes the move straight to DoMovePoint.
// `react=true` opts the move out of the end-of-dispatch
// WaitForReach AI-loop block — combat callers should set this so the
// bot can keep re-evaluating mid-chase. Default false matches the
// reference's MoveTo2 default.
bool MoveTo2(WorldPosition endPos,
bool idle = false, bool react = false,
bool noPath = false, bool ignoreEnemyTargets = false,
@ -91,11 +95,15 @@ protected:
// re-evaluation for the full move duration. Until combat dispatch is
// restructured to bypass MoveTo2, the WaitForReach is deliberately
// omitted.
// `react=true` skips the end-of-dispatch WaitForReach so the AI
// loop isn't blocked while the spline plays — combat callers use
// this to keep re-evaluating mid-chase.
bool DispatchMovement(TravelPath path,
WorldPosition dest,
char const* label,
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL,
bool lessDelay = false);
bool lessDelay = false,
bool react = false);
bool MoveTo(WorldObject* target, float distance = 0.0f,
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
bool MoveNear(WorldObject* target, float distance = sPlayerbotAIConfig.contactDistance,

View File

@ -274,6 +274,27 @@ bool WorldPosition::isUnderWater()
: false;
};
bool WorldPosition::setAtWaterSurface()
{
if (!isInWater() && !isUnderWater())
return false;
Map* map = getMap();
if (!map)
return false;
// Returns the water level when liquid is present; falls back to
// ground level otherwise. Our isInWater/isUnderWater preconditions
// ensure liquid exists, so the +0.5y nudge lands the point on top
// of the surface (matches the reference's surface snap).
float const level = map->GetWaterOrGroundLevel(PHASEMASK_NORMAL,
GetPositionX(),
GetPositionY(),
GetPositionZ());
if (level <= INVALID_HEIGHT)
return false;
setZ(level + 0.5f);
return true;
}
bool WorldPosition::IsValid()
{
return !(GetMapId() == MAPID_INVALID && GetPositionX() == 0 && GetPositionY() == 0 && GetPositionZ() == 0);

View File

@ -138,6 +138,9 @@ public:
bool isOverworld();
bool isInWater();
bool isUnderWater();
// Snap Z to the water surface (level + 0.5y). Returns false if the
// point isn't in/under water or the water level can't be sampled.
bool setAtWaterSurface();
bool IsValid();
WorldPosition relPoint(WorldPosition* center);

View File

@ -908,6 +908,22 @@ void TravelPath::ClipPath(PlayerbotAI* ai, Unit* mover, bool ignoreEnemyTargets)
fullPath.erase(std::next(endP), fullPath.end());
}
void TravelPath::surfaceSnapWaypoints(WorldPosition endPos)
{
if (fullPath.empty())
return;
// Same map + dest is on land. If dest is itself underwater the bot
// wants to dive; leave waypoints alone.
if (fullPath.front().point.GetMapId() != endPos.GetMapId() ||
endPos.isUnderWater())
return;
for (auto& p : fullPath)
{
if (p.point.isUnderWater())
p.point.setAtWaterSurface();
}
}
bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist, Unit* bot)
{
if (GetPath().empty())

View File

@ -486,6 +486,14 @@ public:
bool makeShortCut(WorldPosition startPos, float maxDist, Unit* bot = nullptr);
// For each waypoint that's in/under water, snap its Z to the water
// surface. No-op when destination is itself underwater (caller wants
// the bot to dive) or path's front map differs from dest map.
// Mirrors the reference's underwater→surface snap so bots swim
// along the top of shallow water on land-bound paths instead of
// diving and air-walking the seafloor.
void surfaceSnapWaypoints(WorldPosition endPos);
// Trim the path up to (and optionally including) the given point.
// Returns true if the point was found. Used by upcoming special-
// movement detection to advance the path past a portal/transport/