mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
feat(Core/Travel): Align MoveFarTo and probe pipeline with cmangos
This commit is contained in:
parent
44cb9a5783
commit
e8da8e0e43
@ -3308,7 +3308,7 @@ bool MovementAction::GetTravelPlan(TravelPlan& plan, WorldPosition destination)
|
||||
destination.GetPositionX(), destination.GetPositionY(), destination.GetPositionZ(),
|
||||
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)
|
||||
|
||||
@ -52,21 +52,29 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL))
|
||||
return false;
|
||||
|
||||
// Already-at-dest short-stop. Below targetPosRecalcDistance the
|
||||
// move is effectively done — stop any active spline and clear
|
||||
// the cached path if it pointed here, so we don't keep gliding.
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Let previously committed movement finish before recomputing.
|
||||
//
|
||||
// MoveTo internally caps its stored delay at maxWaitForMove
|
||||
// (default 5s), but a long path (200+ yd routed around a
|
||||
// mountain) takes 30+ seconds to walk. After 5s
|
||||
// IsWaitingForLastMove returns false and MoveFarTo re-enters.
|
||||
// Without this gate, DoMovePoint would call mm->Clear() and
|
||||
// reissue MovePoint from the new bot position — and from a new
|
||||
// position mmap's partial-path endpoint often differs, so the
|
||||
// bot gets clobbered mid-walk and ends up oscillating around an
|
||||
// unreachable destination.
|
||||
//
|
||||
// If the bot is still actively walking toward its last
|
||||
// committed point on the same map, just let the current spline
|
||||
// finish.
|
||||
// If the bot is still actively walking toward its last committed
|
||||
// point on the same map, just let the current spline finish.
|
||||
// Prevents oscillation when a re-resolve produces a slightly
|
||||
// different partial-path endpoint mid-walk.
|
||||
{
|
||||
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
|
||||
if (bot->isMoving() && lastMove.lastMoveToMapId == bot->GetMapId())
|
||||
@ -119,24 +127,7 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
points.reserve(pts.size());
|
||||
for (auto const& wp : pts)
|
||||
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
|
||||
for (auto& pt : points)
|
||||
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;
|
||||
return DispatchPathPoints(dest, points, "reuse");
|
||||
}
|
||||
}
|
||||
// Path was cleared or collapsed — fall through to fresh planning.
|
||||
@ -148,27 +139,25 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
float disToDest = bot->GetDistance(dest);
|
||||
float dis = bot->GetExactDist(dest);
|
||||
|
||||
// Decision tree (cmangos ResolveMovePath order — travel nodes first):
|
||||
// Decision tree:
|
||||
// 1. Active node plan with matching dest → ride it.
|
||||
// 2. Long-distance / cross-map: try the node graph FIRST.
|
||||
// Graph internally probes mmap and falls back to A* route.
|
||||
// 3. Else: 40-step chained mmap probe + regression guard.
|
||||
// 4. Empty / non-progressing probe: single-waypoint MoveTo.
|
||||
//
|
||||
// 1. Active node plan? Ride it.
|
||||
//
|
||||
// 2. Long-distance move (>= nodeFirstDis) and travel nodes
|
||||
// enabled: try the node graph FIRST. The graph holds
|
||||
// curated waypoints that avoid known bad terrain.
|
||||
//
|
||||
// 3. If no node plan returned: run the 40-step chained mmap
|
||||
// probe and dispatch its waypoint chain.
|
||||
//
|
||||
// 4. Empty / non-progressing probe: fall back to single-
|
||||
// waypoint spline at dest.
|
||||
bool tryNodes = (dis >= nodeFirstDis && sPlayerbotAIConfig.enableTravelNodes);
|
||||
// needsLongPath gate — cross-map or > sightDistance go to graph.
|
||||
// BG gating: graph holds open-world routes only.
|
||||
bool tryNodes = sPlayerbotAIConfig.enableTravelNodes &&
|
||||
!bot->InBattleground() &&
|
||||
((bot->GetMapId() != dest.GetMapId()) ||
|
||||
(dis > sPlayerbotAIConfig.sightDistance));
|
||||
|
||||
// If a node plan is already active, ride it — but only if its
|
||||
// destination still matches the requested dest. Otherwise the
|
||||
// old plan (e.g. built toward a quest objective POI) would keep
|
||||
// driving the bot after the caller switched targets (e.g. to a
|
||||
// turn-in NPC). cmangos's ResolveMovePath dodges this by being
|
||||
// stateless; we have a long-lived plan flag, so check explicitly.
|
||||
// turn-in NPC).
|
||||
if (tryNodes && botAI->rpgInfo.HasActiveTravelPlan())
|
||||
{
|
||||
if (botAI->rpgInfo.travelPlan.destination.distance(dest) > 10.0f)
|
||||
@ -198,8 +187,8 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
botAI->rpgInfo.ClearTravel();
|
||||
}
|
||||
|
||||
// 40-step chained mmap probe — fallback when the node graph
|
||||
// returned no plan (or for short moves below nodeFirstDis).
|
||||
// 40-step chained mmap probe — primary for short moves and
|
||||
// fallback when the node graph returned no plan.
|
||||
WorldPosition botPos(bot);
|
||||
std::vector<WorldPosition> probe = botPos.getPathTo(dest, bot);
|
||||
|
||||
@ -234,24 +223,7 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
points.reserve(pts.size());
|
||||
for (auto const& wp : pts)
|
||||
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
|
||||
for (auto& pt : points)
|
||||
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;
|
||||
return DispatchPathPoints(dest, points, "regress-keep");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -259,10 +231,7 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
}
|
||||
}
|
||||
|
||||
// Walk the chained probe's full waypoint chain via MoveSplinePath.
|
||||
// Handing the full waypoint vector to the motion master removes
|
||||
// its discretion to introduce a straight-line shortcut between
|
||||
// intermediate points.
|
||||
// Walk the chained probe's full waypoint chain via DispatchPathPoints.
|
||||
if (!probe.empty() && probe.size() >= 2)
|
||||
{
|
||||
WorldPosition stepDest = probe.back();
|
||||
@ -270,56 +239,21 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
stepDest.GetPositionY(), stepDest.GetPositionZ());
|
||||
if (endDistToDest + 5.0f < disToDest)
|
||||
{
|
||||
// Convert WorldPosition probe to G3D::Vector3 array.
|
||||
Movement::PointsArray points;
|
||||
points.reserve(probe.size());
|
||||
for (auto const& wp : probe)
|
||||
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
|
||||
|
||||
// Per-waypoint Z-snap to current ground.
|
||||
for (auto& pt : points)
|
||||
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
|
||||
|
||||
if (points.size() >= 2)
|
||||
{
|
||||
LOG_INFO("playerbots", "[MoveFar] {} mmap-path | dis={:.0f} | endDist={:.0f} | wp={}",
|
||||
bot->GetName(), dis, endDistToDest, (uint32)points.size());
|
||||
EmitDebugMove("MoveFar", "mmap",
|
||||
points.back().x, points.back().y, points.back().z);
|
||||
|
||||
// Mount up if outdoors and not in combat.
|
||||
if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive())
|
||||
botAI->DoSpecificAction("check mount state", Event(), true);
|
||||
|
||||
// Bulk dispatch: hand the full waypoint chain to the
|
||||
// motion master via MoveSplinePath. Motion master plays
|
||||
// every point in sequence — no per-tick re-dispatching.
|
||||
bot->GetMotionMaster()->Clear();
|
||||
bot->GetMotionMaster()->MoveSplinePath(&points, FORCED_MOVEMENT_RUN);
|
||||
|
||||
// Update LastMovement to the chain endpoint so spline-
|
||||
// active early-exit at the top of MoveFarTo silences
|
||||
// recompute attempts during the walk.
|
||||
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();
|
||||
float speed = std::max(bot->GetSpeed(MOVE_RUN), 0.1f);
|
||||
uint32 expectedMs = static_cast<uint32>((totalDist / speed) * IN_MILLISECONDS);
|
||||
uint32 cappedMs = std::min(expectedMs, (uint32)sPlayerbotAIConfig.maxWaitForMove);
|
||||
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
|
||||
lastMove.Set(bot->GetMapId(), last.x, last.y, last.z,
|
||||
bot->GetOrientation(), cappedMs, MovementPriority::MOVEMENT_NORMAL);
|
||||
|
||||
// Cache full chain for downstream consumers
|
||||
// (LastLongMoveValue) and the lastPath reuse check.
|
||||
std::vector<WorldPosition> wpts;
|
||||
wpts.reserve(points.size());
|
||||
for (auto const& pt : points)
|
||||
wpts.emplace_back(bot->GetMapId(), pt.x, pt.y, pt.z);
|
||||
lastMove.setPath(TravelPath(wpts));
|
||||
|
||||
return true;
|
||||
return DispatchPathPoints(dest, points, "mmap");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -339,31 +273,184 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
}
|
||||
}
|
||||
|
||||
// Empty / non-progressing path falls back to dispatching the
|
||||
// destination as a single waypoint. Spline only when target is
|
||||
// line-of-sight: dispatching a straight line through walls
|
||||
// produces visible clipping/glitching. If LOS is blocked we
|
||||
// refuse and let UnstuckAction (5/10 min) catch the stuck.
|
||||
bool const inLOS = bot->IsWithinLOS(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||
LOG_INFO("playerbots", "[MoveFar] {} spline | dis={:.0f} | probe.empty={} | LOS={}",
|
||||
bot->GetName(), dis,
|
||||
probe.empty() ? "y" : "n",
|
||||
inLOS ? "y" : "n");
|
||||
if (!inLOS)
|
||||
{
|
||||
EmitDebugMove("MoveFar", "spline-blocked",
|
||||
dest.GetPositionX(), dest.GetPositionY(),
|
||||
dest.GetPositionZ());
|
||||
return false; // Refuse to dispatch a straight line through geometry.
|
||||
}
|
||||
// Empty-probe fallback — single-waypoint MoveTo. Engine
|
||||
// MovePoint(generatePath=true) does the local PathGenerator
|
||||
// resolution. Cross-map can't be served by a single-map spline
|
||||
// — bail rather than dispatching toward unreachable coords.
|
||||
if (bot->GetMapId() != dest.GetMapId())
|
||||
return false;
|
||||
|
||||
EmitDebugMove("MoveFar", "spline",
|
||||
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||
// Same exact_waypoint=false rationale as the mmap branch — terrain-
|
||||
// following spline, not a straight diagonal.
|
||||
return MoveTo(dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
|
||||
false, false, false, false);
|
||||
}
|
||||
|
||||
bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest,
|
||||
Movement::PointsArray& points,
|
||||
char const* label)
|
||||
{
|
||||
if (points.size() < 2)
|
||||
return false;
|
||||
|
||||
// Save planner output BEFORE any clip/fixup mutation so next-tick
|
||||
// reuse/regress branches see the original intent, not a clip-
|
||||
// truncated tail.
|
||||
{
|
||||
LastMovement& lm = AI_VALUE(LastMovement&, "last movement");
|
||||
std::vector<WorldPosition> wpts;
|
||||
wpts.reserve(points.size());
|
||||
for (auto const& pt : points)
|
||||
wpts.emplace_back(dest.GetMapId(), pt.x, pt.y, pt.z);
|
||||
lm.setPath(TravelPath(wpts));
|
||||
}
|
||||
|
||||
// Underwater fixup — push waypoints submerged below the water
|
||||
// surface up to the surface itself, unless the destination is
|
||||
// itself underwater.
|
||||
if (Map* map = bot->GetMap())
|
||||
{
|
||||
WorldPosition destWp = dest;
|
||||
if (!destWp.isUnderWater())
|
||||
{
|
||||
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 path at first hostile creature within its
|
||||
// own attack range. Skipped while in combat or dead.
|
||||
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;
|
||||
if (!unit->IsWithinLOSInMap(bot))
|
||||
continue;
|
||||
clipAt = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (clipAt < points.size() && clipAt + 1 < points.size())
|
||||
points.erase(points.begin() + clipAt + 1, points.end());
|
||||
}
|
||||
}
|
||||
|
||||
if (points.size() < 2)
|
||||
return false;
|
||||
|
||||
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");
|
||||
|
||||
// Inactive-bot teleport — when the path is longer than reactDistance
|
||||
// and no real player is around to witness, jump to the path tail and
|
||||
// schedule a cooldown. Skips cosmetic walking for unobserved random
|
||||
// bots. Self-bots are excluded so observed sessions always 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.
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-dispatch state cleanup. Clear emote / stand up / interrupt
|
||||
// any non-melee cast so the spline can begin without state conflicts.
|
||||
bot->ClearEmoteState();
|
||||
if (!bot->IsStandState())
|
||||
bot->SetStandState(UNIT_STAND_STATE_STAND);
|
||||
if (bot->IsNonMeleeSpellCast(true))
|
||||
bot->InterruptNonMeleeSpells(true);
|
||||
|
||||
bot->GetMotionMaster()->Clear();
|
||||
bot->GetMotionMaster()->MoveSplinePath(&points, moveMode);
|
||||
|
||||
EmitDebugMove("MoveFar", label, last.x, last.y, last.z);
|
||||
|
||||
// WaitForReach scheduling.
|
||||
// waitDist = (totalDist > reactDistance) ? totalDist - 10 : totalDist
|
||||
// duration = 1000 * (waitDist / speed) + reactDelay, capped at maxWaitForMove
|
||||
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;
|
||||
}
|
||||
|
||||
void NewRpgBaseAction::StartTravelPlan(WorldPosition dest)
|
||||
{
|
||||
TravelPlan& plan = botAI->rpgInfo.travelPlan;
|
||||
|
||||
@ -76,19 +76,18 @@ protected:
|
||||
bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
|
||||
bool CheckRpgStatusAvailable(NewRpgStatus status);
|
||||
|
||||
protected:
|
||||
/* FOR MOVE FAR */
|
||||
// Distance at which MoveFarTo considers the travel-node graph as
|
||||
// a routing option. Below this, the move is short enough that
|
||||
// mmap handles it directly. Above this, mmap is *still probed
|
||||
// first* via the 40-step chained pathfinder; the node graph
|
||||
// only takes over if mmap can't get within spellDistance of
|
||||
// the destination.
|
||||
const float nodeFirstDis = 75.0f;
|
||||
|
||||
private:
|
||||
void StartTravelPlan(WorldPosition dest);
|
||||
bool UpdateTravelPlan();
|
||||
|
||||
// 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
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
#include "ChatHelper.h"
|
||||
#include "MapCollisionData.h"
|
||||
#include "MapMgr.h"
|
||||
#include "ModelIgnoreFlags.h"
|
||||
#include "PathGenerator.h"
|
||||
#include "Playerbots.h"
|
||||
#include "RaceMgr.h"
|
||||
@ -698,7 +699,6 @@ std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos
|
||||
|
||||
if (!pathUnit)
|
||||
{
|
||||
// Create a temporary creature for PathGenerator (same entry as DebugAction "show node")
|
||||
Map* map = sMapMgr->FindBaseMap(startPos.GetMapId());
|
||||
if (!map)
|
||||
return {};
|
||||
@ -714,52 +714,82 @@ std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos
|
||||
}
|
||||
pathUnit = tempCreature;
|
||||
|
||||
// Ensure grids are created at both endpoints so mmap tiles are available.
|
||||
// EnsureGridCreated loads terrain + vmaps + mmaps but NOT objects,
|
||||
// which is all PathGenerator needs.
|
||||
map->EnsureGridCreated(Acore::ComputeGridCoord(startPos.GetPositionX(), startPos.GetPositionY()));
|
||||
map->EnsureGridCreated(Acore::ComputeGridCoord(GetPositionX(), GetPositionY()));
|
||||
}
|
||||
|
||||
// Explicit-start overload (PathGenerator.h:67). Without this,
|
||||
// CalculatePath(destX,destY,destZ) defaults to the unit's
|
||||
// current position as start — which means every iteration of
|
||||
// getPathFromPath's "chain" begins from the bot's same real
|
||||
// location and produces the same ~296y partial path. The chain
|
||||
// never advances. With explicit start, each step extends from
|
||||
// the previous step's endpoint, giving the 40-attempt walker
|
||||
// its intended multi-tile reach.
|
||||
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. Used by getPathFromPath to thread one
|
||||
// PathGenerator 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. Without this, the chain begins from the
|
||||
// unit's current position every step and never advances.
|
||||
path.CalculatePath(startPos.GetPositionX(), startPos.GetPositionY(), startPos.GetPositionZ(),
|
||||
GetPositionX(), GetPositionY(), GetPositionZ(), false);
|
||||
|
||||
Movement::PointsArray points = path.GetPath();
|
||||
PathType type = path.GetPathType();
|
||||
|
||||
if (tempCreature)
|
||||
delete tempCreature;
|
||||
|
||||
// PathType is a bitmask. Two things to handle:
|
||||
//
|
||||
// 1. AC's PathGenerator can return INCOMPLETE | FARFROMPOLY_END
|
||||
// (0x84) etc. — strict `== PATHFIND_INCOMPLETE` would reject
|
||||
// these perfectly usable partial paths. Use bitwise to accept
|
||||
// NORMAL/INCOMPLETE plus auxiliary flags.
|
||||
//
|
||||
// 2. AC's PathGenerator at PathGenerator.cpp:177-188 returns
|
||||
// NORMAL | NOT_USING_PATH for player units when start or end
|
||||
// polygon is INVALID_POLYREF (BuildShortcut → 2-point straight
|
||||
// line through whatever's in the way). cmangos by contrast
|
||||
// returns NOPATH for the same case (PathFinder.cpp:437-441).
|
||||
// To match cmangos's intent (never silently dispatch a
|
||||
// geometry-ignoring shortcut), reject any path with the
|
||||
// NOT_USING_PATH bit set.
|
||||
if ((type & (PATHFIND_NORMAL | PATHFIND_INCOMPLETE))
|
||||
&& !(type & PATHFIND_NOT_USING_PATH))
|
||||
return fromPointsArray(points);
|
||||
|
||||
// PathType is a bitmask. AC's PathGenerator returns
|
||||
// NORMAL | NOT_USING_PATH when start/end poly is INVALID_POLYREF
|
||||
// (BuildShortcut produces a 2-point straight line through whatever's
|
||||
// in the way). Reject those to avoid silently dispatching a
|
||||
// geometry-ignoring shortcut.
|
||||
if (!(type & (PATHFIND_NORMAL | PATHFIND_INCOMPLETE)) ||
|
||||
(type & PATHFIND_NOT_USING_PATH))
|
||||
return {};
|
||||
|
||||
std::vector<WorldPosition> retvec = fromPointsArray(points);
|
||||
|
||||
// Underwater path-extension. When PATHFIND_INCOMPLETE ends within
|
||||
// 50y of dest and both endpoints are underwater with LOS, extend
|
||||
// by one 5y step (or straight to dest if <5y). Lets bots traverse
|
||||
// navmesh-poor water volumes.
|
||||
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)
|
||||
@ -795,27 +825,60 @@ std::vector<WorldPosition> WorldPosition::getPathFromPath(std::vector<WorldPosit
|
||||
|
||||
std::vector<WorldPosition> subPath, fullPath = startPath;
|
||||
|
||||
// Construct ONE PathGenerator and thread it through every step
|
||||
// to avoid the per-step alloc cost. AC's BuildPolyPath has a
|
||||
// subpath-prefix optimization that can bend chained probes, so
|
||||
// call Clear() before each step to reset the poly cache.
|
||||
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);
|
||||
|
||||
// Limit the pathfinding attempts
|
||||
for (uint32 i = 0; i < maxAttempt; i++)
|
||||
{
|
||||
// Try to pathfind to this position.
|
||||
subPath = getPathStepFrom(currentPos, bot);
|
||||
// Reset cached poly state from the previous step so each call
|
||||
// is a fresh A* (otherwise the prefix-recycling at
|
||||
// PathGenerator.cpp BuildPolyPath snaps the start to the
|
||||
// cached corridor, bending the chain).
|
||||
path.Clear();
|
||||
|
||||
subPath = getPathStepFrom(currentPos, path);
|
||||
|
||||
// If we could not find a path return what we have now.
|
||||
if (subPath.empty() || currentPos.distance(&subPath.back()) < sPlayerbotAIConfig.targetPosRecalcDistance)
|
||||
break;
|
||||
|
||||
// Append the path excluding the start (this should be the same as the end of the startPath)
|
||||
fullPath.insert(fullPath.end(), std::next(subPath.begin(), 1), subPath.end());
|
||||
|
||||
// Are we there yet?
|
||||
if (isPathTo(subPath))
|
||||
break;
|
||||
|
||||
// Continue pathfinding.
|
||||
currentPos = subPath.back();
|
||||
}
|
||||
|
||||
if (tempCreature)
|
||||
delete tempCreature;
|
||||
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
|
||||
class Creature;
|
||||
class GuidPosition;
|
||||
class PathGenerator;
|
||||
class ObjectGuid;
|
||||
class Quest;
|
||||
class Player;
|
||||
@ -283,6 +284,7 @@ public:
|
||||
|
||||
// Pathfinding
|
||||
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> getPathFrom(WorldPosition startPos, Unit* bot)
|
||||
|
||||
@ -1285,19 +1285,26 @@ TravelNodeRoute TravelNodeMap::FindRouteNearestNodes(WorldPosition startPos, Wor
|
||||
|
||||
bool TravelNodeMap::GetFullPath(TravelPlan& plan,
|
||||
WorldPosition botPos, uint32 botZoneId,
|
||||
WorldPosition destination)
|
||||
WorldPosition destination, Unit* bot)
|
||||
{
|
||||
plan.Reset();
|
||||
plan.destination = destination;
|
||||
|
||||
// Short distance — direct walk, no nodes needed
|
||||
if (botPos.fDist(destination) < MAX_PATHFINDING_DISTANCE &&
|
||||
botPos.GetMapId() == destination.GetMapId())
|
||||
// mmap-probe-first. Run a 40-step chained probe; if it gets within
|
||||
// spellDistance of dest, emit it as plan steps and 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 (probe.size() >= 2 && destination.isPathTo(probe, sPlayerbotAIConfig.spellDistance))
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_lock<std::shared_timed_mutex> guard(m_nMapMtx);
|
||||
|
||||
|
||||
@ -725,7 +725,7 @@ public:
|
||||
std::vector<TravelNode*> const& GetNodesInZone(uint32 zoneId) const;
|
||||
|
||||
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)
|
||||
std::vector<TravelNode*> ResolveRoute(WorldPosition startPos,
|
||||
|
||||
@ -98,6 +98,7 @@ bool PlayerbotAIConfig::Initialize()
|
||||
tooCloseDistance = sConfigMgr->GetOption<float>("AiPlayerbot.TooCloseDistance", 5.0f);
|
||||
meleeDistance = sConfigMgr->GetOption<float>("AiPlayerbot.MeleeDistance", 0.75f);
|
||||
followDistance = sConfigMgr->GetOption<float>("AiPlayerbot.FollowDistance", 1.5f);
|
||||
walkDistance = sConfigMgr->GetOption<float>("AiPlayerbot.WalkDistance", 5.0f);
|
||||
whisperDistance = sConfigMgr->GetOption<float>("AiPlayerbot.WhisperDistance", 6000.0f);
|
||||
contactDistance = sConfigMgr->GetOption<float>("AiPlayerbot.ContactDistance", 0.45f);
|
||||
aoeRadius = sConfigMgr->GetOption<float>("AiPlayerbot.AoeRadius", 10.0f);
|
||||
|
||||
@ -89,7 +89,7 @@ public:
|
||||
bool dynamicReactDelay;
|
||||
float sightDistance, spellDistance, reactDistance, grindDistance, lootDistance, shootDistance, fleeDistance,
|
||||
tooCloseDistance, meleeDistance, followDistance, whisperDistance, contactDistance, aoeRadius, rpgDistance,
|
||||
targetPosRecalcDistance, farDistance, healDistance, aggroDistance;
|
||||
targetPosRecalcDistance, farDistance, healDistance, aggroDistance, walkDistance;
|
||||
uint32 criticalHealth, lowHealth, mediumHealth, almostFullHealth;
|
||||
uint32 lowMana, mediumMana, highMana;
|
||||
bool autoSaveMana;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user