feat(Core/Travel): Match cmangos MoveTo2 flow in MoveFarTo and DispatchPathPoints

This commit is contained in:
bash 2026-05-10 02:22:14 +02:00
parent 5c73cde20d
commit 3710c35a41
9 changed files with 392 additions and 120 deletions

View File

@ -330,7 +330,7 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle,
return false; return false;
} }
bool generatePath = !bot->IsFlying() && !bot->isSwimming(); bool generatePath = !bot->IsFlying() && !bot->isSwimming() && !bot->IsInWater();
bool disableMoveSplinePath = bool disableMoveSplinePath =
sPlayerbotAIConfig.disableMoveSplinePath >= 2 || sPlayerbotAIConfig.disableMoveSplinePath >= 2 ||
(sPlayerbotAIConfig.disableMoveSplinePath == 1 && bot->InBattleground()); (sPlayerbotAIConfig.disableMoveSplinePath == 1 && bot->InBattleground());
@ -3308,7 +3308,7 @@ bool MovementAction::GetTravelPlan(TravelPlan& plan, WorldPosition destination)
destination.GetPositionX(), destination.GetPositionY(), destination.GetPositionZ(), destination.GetPositionX(), destination.GetPositionY(), destination.GetPositionZ(),
destination.GetMapId(), botPos.fDist(destination)); destination.GetMapId(), botPos.fDist(destination));
return sTravelNodeMap.GetFullPath(plan, botPos, bot->GetZoneId(), destination); return sTravelNodeMap.GetFullPath(plan, botPos, bot->GetZoneId(), destination, bot);
} }
bool MovementAction::ExecuteTravelPlan(TravelPlan& state) bool MovementAction::ExecuteTravelPlan(TravelPlan& state)

View File

@ -53,10 +53,24 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
return false; return false;
// Already-at-dest short-stop. Below targetPosRecalcDistance // Already-at-dest short-stop. Below targetPosRecalcDistance
// (default 0.1y) the move is effectively done — no need to // (default 0.1y) the move is effectively done. Stop any spline
// recompute or dispatch. // still running and clear the cached path if it points to here
if (bot->GetExactDist(dest) < sPlayerbotAIConfig.targetPosRecalcDistance) // — otherwise the bot keeps gliding past dest. Mirrors cmangos
// MovementActions.cpp:1095-1106.
{
float const totalDistance = bot->GetExactDist(dest);
if (totalDistance < sPlayerbotAIConfig.targetPosRecalcDistance)
{
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
if (!lastMove.lastPath.empty() &&
lastMove.lastPath.getBack().distance(dest) <= totalDistance)
{
lastMove.clear();
}
bot->StopMoving();
return false; return false;
}
}
// 10% lastPath reuse — if the cached path's endpoint is still // 10% lastPath reuse — if the cached path's endpoint is still
// close (within 10%) to the new dest, trim the cached path to // close (within 10%) to the new dest, trim the cached path to
@ -96,24 +110,7 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
points.reserve(pts.size()); points.reserve(pts.size());
for (auto const& wp : pts) for (auto const& wp : pts)
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ()); points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
for (auto& pt : points) return DispatchPathPoints(dest, points, "reuse");
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
bot->GetMotionMaster()->Clear();
bot->GetMotionMaster()->MoveSplinePath(&points, FORCED_MOVEMENT_RUN);
G3D::Vector3 const& last = points.back();
float totalChainDist = 0.f;
for (size_t i = 1; i < points.size(); ++i)
totalChainDist += (points[i] - points[i - 1]).length();
float speed = std::max(bot->GetSpeed(MOVE_RUN), 0.1f);
uint32 expectedMs = static_cast<uint32>((totalChainDist / speed) * IN_MILLISECONDS);
uint32 cappedMs = std::min(expectedMs, (uint32)sPlayerbotAIConfig.maxWaitForMove);
lastMove.Set(bot->GetMapId(), last.x, last.y, last.z,
bot->GetOrientation(), cappedMs, MovementPriority::MOVEMENT_NORMAL);
EmitDebugMove("MoveFar", "reuse",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return true;
} }
} }
// Path was cleared or collapsed — fall through to fresh planning. // Path was cleared or collapsed — fall through to fresh planning.
@ -124,23 +121,44 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
float const dis = bot->GetExactDist(dest); float const dis = bot->GetExactDist(dest);
// Decision tree: // Mirrors cmangos ResolveMovePath: pick travel-node graph for
// long-distance / cross-map moves; mmap probe for short paths.
// //
// 1. 40-step chained mmap probe FIRST. // 1. needsLongPath = cross-map OR dis > sightDistance
// 2. Regression guard — if cached lastPath ends ≤ as close to // 2. If needsLongPath && travel nodes enabled → graph plan
// dest as the new probe, ride the cached path instead. // 3. Else 40-step chained mmap probe
// 3. If probe reaches dest within 25y, dispatch probe waypoints. // 4. Regression guard against the new probe — ride cached path
// 4. Else if travel nodes enabled, try the node graph as fallback. // if it ends at least as close to dest
// 5. Else dispatch the destination as a single-waypoint spline // 5. Dispatch probe (no reach gate — partial probes still make
// via MoveTo — engine MovePoint(generatePath=true) resolves // progress and the next tick replans)
// the local route via PathGenerator. // 6. Empty probe → cmangos addPoint(endPosition) fallback:
// single-waypoint MoveTo so PathGenerator resolves it.
bool const needsLongPath = (bot->GetMapId() != dest.GetMapId()) ||
(dis > sPlayerbotAIConfig.sightDistance);
// Skip travel-node planning inside battlegrounds — the graph is
// built for the open world and yields nonsense routes inside BGs.
// Mirrors cmangos MovementActions.cpp:705.
if (needsLongPath && sPlayerbotAIConfig.enableTravelNodes &&
!bot->InBattleground())
{
StartTravelPlan(dest);
if (botAI->rpgInfo.HasActiveTravelPlan())
{
LOG_INFO("playerbots", "[MoveFar] {} nodetravel | dis={:.0f}",
bot->GetName(), dis);
EmitDebugMove("MoveFar", "travelplan",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return UpdateTravelPlan();
}
}
// 40-step chained mmap probe. // 40-step chained mmap probe.
WorldPosition botPos(bot); WorldPosition botPos(bot);
std::vector<WorldPosition> probe = botPos.getPathTo(dest, bot); std::vector<WorldPosition> probe = botPos.getPathTo(dest, bot);
// Regression guard: if a cached lastPath ends at least as close // Regression guard.
// to dest as the new probe's endpoint, ride the cached path.
{ {
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement"); LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
if (!lastMove.lastPath.empty() && !probe.empty() && probe.size() >= 2) if (!lastMove.lastPath.empty() && !probe.empty() && probe.size() >= 2)
@ -165,24 +183,7 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
points.reserve(pts.size()); points.reserve(pts.size());
for (auto const& wp : pts) for (auto const& wp : pts)
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ()); points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
for (auto& pt : points) return DispatchPathPoints(dest, points, "regress-keep");
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
bot->GetMotionMaster()->Clear();
bot->GetMotionMaster()->MoveSplinePath(&points, FORCED_MOVEMENT_RUN);
G3D::Vector3 const& last = points.back();
float totalChainDist = 0.f;
for (size_t i = 1; i < points.size(); ++i)
totalChainDist += (points[i] - points[i - 1]).length();
float speed = std::max(bot->GetSpeed(MOVE_RUN), 0.1f);
uint32 expectedMs = static_cast<uint32>((totalChainDist / speed) * IN_MILLISECONDS);
uint32 cappedMs = std::min(expectedMs, (uint32)sPlayerbotAIConfig.maxWaitForMove);
lastMove.Set(bot->GetMapId(), last.x, last.y, last.z,
bot->GetOrientation(), cappedMs, MovementPriority::MOVEMENT_NORMAL);
EmitDebugMove("MoveFar", "regress-keep",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return true;
} }
} }
} }
@ -190,76 +191,240 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
} }
} }
// Probe dispatch — only if the probe reaches dest within 25y. if (!probe.empty() && probe.size() >= 2)
// Partial probes that fall short go through the graph fallback
// below instead of being dispatched as-is.
constexpr float PROBE_REACH = 25.0f;
if (!probe.empty() && probe.size() >= 2 && dest.isPathTo(probe, PROBE_REACH))
{ {
Movement::PointsArray points; Movement::PointsArray points;
points.reserve(probe.size()); points.reserve(probe.size());
for (auto const& wp : probe) for (auto const& wp : probe)
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ()); points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
for (auto& pt : points)
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
if (points.size() >= 2) if (points.size() >= 2)
{ {
LOG_INFO("playerbots", "[MoveFar] {} mmap-path | dis={:.0f} | wp={}", LOG_INFO("playerbots", "[MoveFar] {} mmap-path | dis={:.0f} | wp={}",
bot->GetName(), dis, (uint32)points.size()); bot->GetName(), dis, (uint32)points.size());
EmitDebugMove("MoveFar", "mmap",
points.back().x, points.back().y, points.back().z);
if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive()) if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive())
botAI->DoSpecificAction("check mount state", Event(), true); botAI->DoSpecificAction("check mount state", Event(), true);
bot->GetMotionMaster()->Clear(); return DispatchPathPoints(dest, points, "mmap");
bot->GetMotionMaster()->MoveSplinePath(&points, FORCED_MOVEMENT_RUN); }
}
G3D::Vector3 const& last = points.back(); // Empty-probe fallback — cmangos addPoint(endPosition) + DispatchMovement.
float totalDist = 0.f; // Build a 2-point straight-line path (bot → dest) and route it through
for (size_t i = 1; i < points.size(); ++i) // DispatchPathPoints so it gets the same clip / underwater fixup /
totalDist += (points[i] - points[i - 1]).length(); // setPath / WaitForReach / teleport / mode-flip treatment as the
float speed = std::max(bot->GetSpeed(MOVE_RUN), 0.1f); // chained-mmap branches. Trade-off vs the old MoveTo path: lose the
uint32 expectedMs = static_cast<uint32>((totalDist / speed) * IN_MILLISECONDS); // engine's MovePoint(generatePath=true) local PathGenerator resolution.
uint32 cappedMs = std::min(expectedMs, (uint32)sPlayerbotAIConfig.maxWaitForMove); // For the empty-probe case the long mmap probe already failed, so the
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement"); // engine's local resolution would likely fail too.
lastMove.Set(bot->GetMapId(), last.x, last.y, last.z, Movement::PointsArray fallback;
bot->GetOrientation(), cappedMs, MovementPriority::MOVEMENT_NORMAL); fallback.emplace_back(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ());
fallback.emplace_back(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return DispatchPathPoints(dest, fallback, "spline");
}
bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest,
Movement::PointsArray& points,
char const* label)
{
if (points.size() < 2)
return false;
// Save the planner's path BEFORE clip/fixup mutations, so the
// next-tick reuse/regress branches see the original intent (not
// a clip-truncated tail). Mirrors cmangos MovementActions.cpp:1117 +
// 1147 — both saves happen before ClipPath at :1150 and the
// underwater fixup loop at :1170.
{
LastMovement& lm = AI_VALUE(LastMovement&, "last movement");
std::vector<WorldPosition> wpts; std::vector<WorldPosition> wpts;
wpts.reserve(points.size()); wpts.reserve(points.size());
for (auto const& pt : points) for (auto const& pt : points)
wpts.emplace_back(bot->GetMapId(), pt.x, pt.y, pt.z); wpts.emplace_back(dest.GetMapId(), pt.x, pt.y, pt.z);
lastMove.setPath(TravelPath(wpts)); lm.setPath(TravelPath(wpts));
}
// Item 5 — underwater fixup. Push waypoints submerged below the
// water surface up to the surface itself, unless the destination
// is itself underwater (e.g. fishing-loot quest GO, sunken ruins).
{
WorldPosition destWp = dest;
if (!destWp.isUnderWater())
{
Map* map = bot->GetMap();
for (auto& pt : points)
{
WorldPosition wp(dest.GetMapId(), pt.x, pt.y, pt.z);
if (wp.isUnderWater())
{
float surface = map->GetWaterLevel(pt.x, pt.y);
if (surface != INVALID_HEIGHT && surface > pt.z)
pt.z = surface;
}
}
}
}
for (auto& pt : points)
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
// ClipPath — truncate the path at the first hostile creature within
// its own attack range, so the bot stops walking into pulls instead
// of marching past mobs and aggroing them all at once. Skipped while
// already in combat or dead. Mirrors cmangos TravelNode.cpp:1118-1203
// in simplified form (no transport, no hazard list).
if (botAI->GetState() != BOT_STATE_COMBAT && bot->IsAlive())
{
GuidVector targets = AI_VALUE(GuidVector, "possible targets");
if (!targets.empty())
{
size_t clipAt = points.size();
for (size_t i = 0; i < points.size() && clipAt == points.size(); ++i)
{
for (ObjectGuid const& guid : targets)
{
Unit* unit = botAI->GetUnit(guid);
if (!unit || !unit->IsAlive())
continue;
Creature* cre = unit->ToCreature();
if (!cre)
continue;
if (unit->GetLevel() > bot->GetLevel() + 5)
continue;
float range = cre->GetAttackDistance(bot);
float dx = unit->GetPositionX() - points[i].x;
float dy = unit->GetPositionY() - points[i].y;
float dz = unit->GetPositionZ() - points[i].z;
if (dx * dx + dy * dy + dz * dz > range * range)
continue;
// LOS guard — mobs behind walls don't actually
// pull, so clipping there over-truncates. Mirrors
// cmangos TravelNode.cpp:1155.
if (!unit->IsWithinLOSInMap(bot))
continue;
clipAt = i;
break;
}
}
if (clipAt > 0 && clipAt + 1 < points.size())
points.erase(points.begin() + clipAt + 1, points.end());
}
}
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();
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
// Item 2 — inactive-bot teleport. When the path is longer than
// reactDistance and there's no real player around to witness, jump
// to the path tail and schedule a cooldown. Skips cosmetic walking
// for unobserved random bots. Player-owned (self) bots are excluded
// so testing/observed sessions always see the real walk.
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));
}
}
// masterWalking — match the master's walk pace when they're nearby
// and walking. Lets a follower bot trail at walk speed instead of
// sprinting past. No-op for masterless RPG bots. Mirrors cmangos
// MovementActions.cpp:1212-1221. (Flying-mount mode flip deferred —
// requires takeoff/landing infrastructure we haven't ported.)
ForcedMovement moveMode = FORCED_MOVEMENT_RUN;
if (sPlayerbotAIConfig.walkDistance > 0.0f)
{
if (Player* master = botAI->GetMaster())
{
if (bot->IsFriendlyTo(master) && master->IsWalking() &&
bot->GetExactDist2d(master) < sPlayerbotAIConfig.walkDistance)
{
moveMode = FORCED_MOVEMENT_WALK;
}
}
}
// Debug-move beacon — when the `debug move` strategy is active in
// non-combat, summon a visible creature at every waypoint (white
// glow, red glow on the tail). Lets the operator visually verify
// ClipPath truncation, underwater fixup, masterWalking pace, etc.
// Mirrors cmangos MovementActions.cpp:1152-1161.
if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
{
for (size_t i = 0; i < points.size(); ++i)
{
G3D::Vector3 const& p = points[i];
if (Creature* wp = bot->SummonCreature(2334, p.x, p.y, p.z, 0,
TEMPSUMMON_TIMED_DESPAWN, 10000))
{
bot->AddAura(246, wp);
if (i + 1 == points.size())
bot->AddAura(1130, wp);
}
}
}
// Pre-dispatch state cleanup. Mirrors cmangos MovementActions.cpp:1186-1194:
// - Clear any looping emote so the bot doesn't run-while-waving
// - Stand up unconditionally (eating/sitting clients ignore moves)
// - Interrupt any non-melee cast so the spline can begin
bot->ClearEmoteState();
if (!bot->IsStandState())
bot->SetStandState(UNIT_STAND_STATE_STAND);
if (bot->IsNonMeleeSpellCast(true))
bot->InterruptNonMeleeSpells(true);
// Item 7 — DispatchMovement two-step. cmangos calls
// mm.MovePoint(last) followed by mm.MovePath(points). In AC,
// MotionMaster::Mutate at MOTION_SLOT_ACTIVE replaces (not queues)
// the previous generator, so both calls in sequence collapse to
// whichever ran last. We skip the redundant MovePoint and dispatch
// the smooth waypoint path directly.
bot->GetMotionMaster()->Clear();
bot->GetMotionMaster()->MoveSplinePath(&points, moveMode);
EmitDebugMove("MoveFar", label, last.x, last.y, last.z);
// Item 6 — WaitForReach scheduling.
// waitDist = (totalDist > reactDistance) ? totalDist - 10 : totalDist
// duration = 1000 * (waitDist / runSpeed) + reactDelay
// capped at maxWaitForMove. The 10y leaves a small buffer so the
// AI tick can intervene before the path strictly finishes.
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 true;
}
}
// Travel-node graph fallback — fires when the probe didn't reach
// dest within PROBE_REACH and the graph is enabled.
if (sPlayerbotAIConfig.enableTravelNodes)
{
StartTravelPlan(dest);
if (botAI->rpgInfo.HasActiveTravelPlan())
{
LOG_INFO("playerbots", "[MoveFar] {} nodetravel | dis={:.0f}",
bot->GetName(), dis);
EmitDebugMove("MoveFar", "travelplan",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return UpdateTravelPlan();
}
}
// Final fallback: dispatch the destination as a single waypoint.
// MoveTo's MovePoint(generatePath=true) lets the engine resolve the
// local route via PathGenerator. Nothing dispatched if MoveTo refuses.
EmitDebugMove("MoveFar", "spline",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return MoveTo(dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
false, false, false, false);
} }
void NewRpgBaseAction::StartTravelPlan(WorldPosition dest) void NewRpgBaseAction::StartTravelPlan(WorldPosition dest)

View File

@ -79,6 +79,17 @@ protected:
private: private:
void StartTravelPlan(WorldPosition dest); void StartTravelPlan(WorldPosition dest);
bool UpdateTravelPlan(); bool UpdateTravelPlan();
// Dispatches a chained mmap path. Applies cmangos-parity tweaks:
// underwater fixup (push submerged waypoints to the surface unless
// dest is itself underwater), inactive-bot teleport (jump to the
// tail when no players are nearby and the path is longer than
// reactDistance), and the WaitForReach formula
// (1000 * dist/speed + reactDelay, capped at maxWaitForMove).
// Returns true if dispatched or teleported.
bool DispatchPathPoints(WorldPosition const& dest,
Movement::PointsArray& points,
char const* label);
}; };
#endif #endif

View File

@ -721,6 +721,22 @@ std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos
map->EnsureGridCreated(Acore::ComputeGridCoord(GetPositionX(), GetPositionY())); map->EnsureGridCreated(Acore::ComputeGridCoord(GetPositionX(), GetPositionY()));
} }
PathGenerator path(pathUnit);
path.AddExcludeFlag(NAV_GROUND_STEEP);
auto result = getPathStepFrom(startPos, path);
if (tempCreature)
delete tempCreature;
return result;
}
// Pathfinder-reuse overload — caller owns the PathGenerator and any
// per-call configuration (filters, area costs). Mirrors cmangos
// WorldPosition.cpp:958 which threads one PathFinder through the whole
// 40-step chain instead of constructing a new one per step.
std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos, PathGenerator& path)
{
// Explicit-start overload (PathGenerator.h:67). Without this, // Explicit-start overload (PathGenerator.h:67). Without this,
// CalculatePath(destX,destY,destZ) defaults to the unit's // CalculatePath(destX,destY,destZ) defaults to the unit's
// current position as start — which means every iteration of // current position as start — which means every iteration of
@ -729,17 +745,12 @@ std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos
// never advances. With explicit start, each step extends from // never advances. With explicit start, each step extends from
// the previous step's endpoint, giving the 40-attempt walker // the previous step's endpoint, giving the 40-attempt walker
// its intended multi-tile reach. // its intended multi-tile reach.
PathGenerator path(pathUnit);
path.AddExcludeFlag(NAV_GROUND_STEEP);
path.CalculatePath(startPos.GetPositionX(), startPos.GetPositionY(), startPos.GetPositionZ(), path.CalculatePath(startPos.GetPositionX(), startPos.GetPositionY(), startPos.GetPositionZ(),
GetPositionX(), GetPositionY(), GetPositionZ(), false); GetPositionX(), GetPositionY(), GetPositionZ(), false);
Movement::PointsArray points = path.GetPath(); Movement::PointsArray points = path.GetPath();
PathType type = path.GetPathType(); PathType type = path.GetPathType();
if (tempCreature)
delete tempCreature;
// PathType is a bitmask. Two things to handle: // PathType is a bitmask. Two things to handle:
// //
// 1. AC's PathGenerator can return INCOMPLETE | FARFROMPOLY_END // 1. AC's PathGenerator can return INCOMPLETE | FARFROMPOLY_END
@ -755,11 +766,50 @@ std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos
// To match cmangos's intent (never silently dispatch a // To match cmangos's intent (never silently dispatch a
// geometry-ignoring shortcut), reject any path with the // geometry-ignoring shortcut), reject any path with the
// NOT_USING_PATH bit set. // NOT_USING_PATH bit set.
if ((type & (PATHFIND_NORMAL | PATHFIND_INCOMPLETE)) if (!(type & (PATHFIND_NORMAL | PATHFIND_INCOMPLETE)) ||
&& !(type & PATHFIND_NOT_USING_PATH)) (type & PATHFIND_NOT_USING_PATH))
return fromPointsArray(points);
return {}; return {};
std::vector<WorldPosition> retvec = fromPointsArray(points);
// Underwater path-extension. Mirrors cmangos WorldPosition.cpp:997-1014.
// When PATHFIND_INCOMPLETE ends within 50y of dest and both endpoints
// are underwater with LOS between them, extend by one 5y step (or
// straight to dest if <5y). Lets bots traverse navmesh-poor water
// volumes the same way real swimmers do.
if (type & PATHFIND_INCOMPLETE)
{
WorldPosition end = *this;
WorldPosition lastPoint = retvec.back();
float dist = lastPoint.distance(&end);
if (dist < 50.0f && lastPoint.isUnderWater() && end.isUnderWater())
{
Map* m = end.getMap();
bool inLos = m && m->isInLineOfSight(
lastPoint.GetPositionX(), lastPoint.GetPositionY(), lastPoint.GetPositionZ() + 1.0f,
end.GetPositionX(), end.GetPositionY(), end.GetPositionZ() + 1.0f,
PHASEMASK_NORMAL, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::Nothing);
if (inLos)
{
if (dist < 5.0f)
retvec.push_back(end);
else
{
float dx = end.GetPositionX() - lastPoint.GetPositionX();
float dy = end.GetPositionY() - lastPoint.GetPositionY();
float dz = end.GetPositionZ() - lastPoint.GetPositionZ();
float scale = 5.0f / dist;
retvec.emplace_back(end.GetMapId(),
lastPoint.GetPositionX() + dx * scale,
lastPoint.GetPositionY() + dy * scale,
lastPoint.GetPositionZ() + dz * scale);
}
}
}
}
return retvec;
} }
bool WorldPosition::cropPathTo(std::vector<WorldPosition>& path, float maxDistance) bool WorldPosition::cropPathTo(std::vector<WorldPosition>& path, float maxDistance)
@ -795,11 +845,44 @@ std::vector<WorldPosition> WorldPosition::getPathFromPath(std::vector<WorldPosit
std::vector<WorldPosition> subPath, fullPath = startPath; std::vector<WorldPosition> subPath, fullPath = startPath;
// Construct ONE PathGenerator and thread it through every step.
// Mirrors cmangos WorldPosition.cpp:1091-1096. Avoids the per-step
// allocation and lets Detour reuse internal scratch.
Unit* pathUnit = bot;
Creature* tempCreature = nullptr;
if (!pathUnit)
{
Map* map = sMapMgr->FindBaseMap(GetMapId());
if (!map)
return fullPath;
tempCreature = new Creature();
if (!tempCreature->Create(map->GenerateLowGuid<HighGuid::Unit>(), map,
PHASEMASK_NORMAL, 1 /*entry*/, 0,
currentPos.GetPositionX(), currentPos.GetPositionY(),
currentPos.GetPositionZ(), 0))
{
delete tempCreature;
return fullPath;
}
pathUnit = tempCreature;
map->EnsureGridCreated(Acore::ComputeGridCoord(currentPos.GetPositionX(), currentPos.GetPositionY()));
map->EnsureGridCreated(Acore::ComputeGridCoord(GetPositionX(), GetPositionY()));
}
PathGenerator path(pathUnit);
path.AddExcludeFlag(NAV_GROUND_STEEP);
// Area-cost biases. Mirrors cmangos WorldPosition.cpp:1098-1100.
// NAV_WATER weighted 10x so A* prefers shore routes over wading
// through lakes when both are reachable.
path.SetAreaCost(NAV_WATER, 10.0f);
// Limit the pathfinding attempts // Limit the pathfinding attempts
for (uint32 i = 0; i < maxAttempt; i++) for (uint32 i = 0; i < maxAttempt; i++)
{ {
// Try to pathfind to this position. // Try to pathfind to this position.
subPath = getPathStepFrom(currentPos, bot); subPath = getPathStepFrom(currentPos, path);
// If we could not find a path return what we have now. // If we could not find a path return what we have now.
if (subPath.empty() || currentPos.distance(&subPath.back()) < sPlayerbotAIConfig.targetPosRecalcDistance) if (subPath.empty() || currentPos.distance(&subPath.back()) < sPlayerbotAIConfig.targetPosRecalcDistance)
@ -816,6 +899,9 @@ std::vector<WorldPosition> WorldPosition::getPathFromPath(std::vector<WorldPosit
currentPos = subPath.back(); currentPos = subPath.back();
} }
if (tempCreature)
delete tempCreature;
return fullPath; return fullPath;
} }

View File

@ -20,6 +20,7 @@
class Creature; class Creature;
class GuidPosition; class GuidPosition;
class ObjectGuid; class ObjectGuid;
class PathGenerator;
class Quest; class Quest;
class Player; class Player;
class PlayerbotAI; class PlayerbotAI;
@ -283,6 +284,7 @@ public:
// Pathfinding // Pathfinding
std::vector<WorldPosition> getPathStepFrom(WorldPosition startPos, Unit* bot); std::vector<WorldPosition> getPathStepFrom(WorldPosition startPos, Unit* bot);
std::vector<WorldPosition> getPathStepFrom(WorldPosition startPos, PathGenerator& pathfinder);
std::vector<WorldPosition> getPathFromPath(std::vector<WorldPosition> startPath, Unit* bot, uint8 maxAttempt = 40); std::vector<WorldPosition> getPathFromPath(std::vector<WorldPosition> startPath, Unit* bot, uint8 maxAttempt = 40);
std::vector<WorldPosition> getPathFrom(WorldPosition startPos, Unit* bot) std::vector<WorldPosition> getPathFrom(WorldPosition startPos, Unit* bot)

View File

@ -1285,19 +1285,26 @@ TravelNodeRoute TravelNodeMap::FindRouteNearestNodes(WorldPosition startPos, Wor
bool TravelNodeMap::GetFullPath(TravelPlan& plan, bool TravelNodeMap::GetFullPath(TravelPlan& plan,
WorldPosition botPos, uint32 botZoneId, WorldPosition botPos, uint32 botZoneId,
WorldPosition destination) WorldPosition destination, Unit* bot)
{ {
plan.Reset(); plan.Reset();
plan.destination = destination; plan.destination = destination;
// Short distance — direct walk, no nodes needed // mmap probe first — mirrors cmangos getFullPath (TravelNode.cpp:1887-1895).
if (botPos.fDist(destination) < MAX_PATHFINDING_DISTANCE && // 40-step chained probe from bot; if it gets within spellDistance of dest
botPos.GetMapId() == destination.GetMapId()) // we skip the graph entirely (a short walk is always better than a node
// hop). When the probe falls short, fall through to graph routing.
if (botPos.GetMapId() == destination.GetMapId())
{
std::vector<WorldPosition> probe = destination.getPathFromPath({botPos}, bot, 40);
if (destination.isPathTo(probe, sPlayerbotAIConfig.spellDistance))
{ {
plan.steps.addPoint(botPos, PathNodeType::NODE_PREPATH); plan.steps.addPoint(botPos, PathNodeType::NODE_PREPATH);
plan.steps.addPoint(destination, PathNodeType::NODE_PATH); for (size_t i = 1; i < probe.size(); ++i)
plan.steps.addPoint(probe[i], PathNodeType::NODE_PATH);
return true; return true;
} }
}
std::shared_lock<std::shared_timed_mutex> guard(m_nMapMtx); std::shared_lock<std::shared_timed_mutex> guard(m_nMapMtx);

View File

@ -725,7 +725,7 @@ public:
std::vector<TravelNode*> const& GetNodesInZone(uint32 zoneId) const; std::vector<TravelNode*> const& GetNodesInZone(uint32 zoneId) const;
bool GetFullPath(TravelPlan& plan, WorldPosition botPos, bool GetFullPath(TravelPlan& plan, WorldPosition botPos,
uint32 botZoneId, WorldPosition destination); uint32 botZoneId, WorldPosition destination, Unit* bot = nullptr);
// Resolve A* route between two world positions (returns node vector) // Resolve A* route between two world positions (returns node vector)
std::vector<TravelNode*> ResolveRoute(WorldPosition startPos, std::vector<TravelNode*> ResolveRoute(WorldPosition startPos,

View File

@ -98,6 +98,7 @@ bool PlayerbotAIConfig::Initialize()
tooCloseDistance = sConfigMgr->GetOption<float>("AiPlayerbot.TooCloseDistance", 5.0f); tooCloseDistance = sConfigMgr->GetOption<float>("AiPlayerbot.TooCloseDistance", 5.0f);
meleeDistance = sConfigMgr->GetOption<float>("AiPlayerbot.MeleeDistance", 0.75f); meleeDistance = sConfigMgr->GetOption<float>("AiPlayerbot.MeleeDistance", 0.75f);
followDistance = sConfigMgr->GetOption<float>("AiPlayerbot.FollowDistance", 1.5f); followDistance = sConfigMgr->GetOption<float>("AiPlayerbot.FollowDistance", 1.5f);
walkDistance = sConfigMgr->GetOption<float>("AiPlayerbot.WalkDistance", 5.0f);
whisperDistance = sConfigMgr->GetOption<float>("AiPlayerbot.WhisperDistance", 6000.0f); whisperDistance = sConfigMgr->GetOption<float>("AiPlayerbot.WhisperDistance", 6000.0f);
contactDistance = sConfigMgr->GetOption<float>("AiPlayerbot.ContactDistance", 0.45f); contactDistance = sConfigMgr->GetOption<float>("AiPlayerbot.ContactDistance", 0.45f);
aoeRadius = sConfigMgr->GetOption<float>("AiPlayerbot.AoeRadius", 10.0f); aoeRadius = sConfigMgr->GetOption<float>("AiPlayerbot.AoeRadius", 10.0f);

View File

@ -89,7 +89,7 @@ public:
bool dynamicReactDelay; bool dynamicReactDelay;
float sightDistance, spellDistance, reactDistance, grindDistance, lootDistance, shootDistance, fleeDistance, float sightDistance, spellDistance, reactDistance, grindDistance, lootDistance, shootDistance, fleeDistance,
tooCloseDistance, meleeDistance, followDistance, whisperDistance, contactDistance, aoeRadius, rpgDistance, tooCloseDistance, meleeDistance, followDistance, whisperDistance, contactDistance, aoeRadius, rpgDistance,
targetPosRecalcDistance, farDistance, healDistance, aggroDistance; targetPosRecalcDistance, farDistance, healDistance, aggroDistance, walkDistance;
uint32 criticalHealth, lowHealth, mediumHealth, almostFullHealth; uint32 criticalHealth, lowHealth, mediumHealth, almostFullHealth;
uint32 lowMana, mediumMana, highMana; uint32 lowMana, mediumMana, highMana;
bool autoSaveMana; bool autoSaveMana;