mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
Compare commits
No commits in common. "9d3f47b19f1531e969ddee0b3f1f9893b67ff793" and "d51f9b8fb8ac338b0fd91dc34a2422169e9176c4" have entirely different histories.
9d3f47b19f
...
d51f9b8fb8
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -330,7 +330,7 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle,
|
||||
return false;
|
||||
}
|
||||
|
||||
bool generatePath = !bot->IsFlying() && !bot->isSwimming() && !bot->IsInWater();
|
||||
bool generatePath = !bot->IsFlying() && !bot->isSwimming();
|
||||
bool disableMoveSplinePath =
|
||||
sPlayerbotAIConfig.disableMoveSplinePath >= 2 ||
|
||||
(sPlayerbotAIConfig.disableMoveSplinePath == 1 && bot->InBattleground());
|
||||
@ -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, bot);
|
||||
return sTravelNodeMap.GetFullPath(plan, botPos, bot->GetZoneId(), destination);
|
||||
}
|
||||
|
||||
bool MovementAction::ExecuteTravelPlan(TravelPlan& state)
|
||||
|
||||
@ -9,6 +9,23 @@
|
||||
|
||||
LastMovement::LastMovement() { clear(); }
|
||||
|
||||
LastMovement::LastMovement(LastMovement& other)
|
||||
: taxiNodes(other.taxiNodes),
|
||||
taxiMaster(other.taxiMaster),
|
||||
lastFollow(other.lastFollow),
|
||||
lastAreaTrigger(other.lastAreaTrigger),
|
||||
lastMoveToX(other.lastMoveToX),
|
||||
lastMoveToY(other.lastMoveToY),
|
||||
lastMoveToZ(other.lastMoveToZ),
|
||||
lastMoveToOri(other.lastMoveToOri),
|
||||
lastFlee(other.lastFlee)
|
||||
{
|
||||
lastMoveShort = other.lastMoveShort;
|
||||
nextTeleport = other.nextTeleport;
|
||||
lastPath = other.lastPath;
|
||||
priority = other.priority;
|
||||
}
|
||||
|
||||
void LastMovement::clear()
|
||||
{
|
||||
lastMoveShort = WorldPosition();
|
||||
|
||||
@ -27,9 +27,20 @@ class LastMovement
|
||||
{
|
||||
public:
|
||||
LastMovement();
|
||||
LastMovement(LastMovement& other);
|
||||
|
||||
LastMovement(LastMovement const&) = default;
|
||||
LastMovement& operator=(LastMovement const&) = default;
|
||||
LastMovement& operator=(LastMovement const& other)
|
||||
{
|
||||
taxiNodes = other.taxiNodes;
|
||||
taxiMaster = other.taxiMaster;
|
||||
lastFollow = other.lastFollow;
|
||||
lastAreaTrigger = other.lastAreaTrigger;
|
||||
lastMoveShort = other.lastMoveShort;
|
||||
lastPath = other.lastPath;
|
||||
nextTeleport = other.nextTeleport;
|
||||
priority = other.priority;
|
||||
return *this;
|
||||
};
|
||||
|
||||
void clear();
|
||||
|
||||
@ -55,6 +66,7 @@ public:
|
||||
MovementPriority priority;
|
||||
TravelPath lastPath;
|
||||
time_t nextTeleport;
|
||||
std::future<TravelPath> future;
|
||||
};
|
||||
|
||||
class LastMovementValue : public ManualSetValue<LastMovement&>
|
||||
@ -63,7 +75,7 @@ public:
|
||||
LastMovementValue(PlayerbotAI* botAI) : ManualSetValue<LastMovement&>(botAI, data) {}
|
||||
|
||||
private:
|
||||
LastMovement data{};
|
||||
LastMovement data = LastMovement();
|
||||
};
|
||||
|
||||
class StayTimeValue : public ManualSetValue<time_t>
|
||||
|
||||
@ -52,23 +52,32 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL))
|
||||
return false;
|
||||
|
||||
// Already-at-dest short-stop. Below targetPosRecalcDistance
|
||||
// (default 0.1y) the move is effectively done. Stop any spline
|
||||
// still running and clear the cached path if it points to here
|
||||
// — otherwise the bot keeps gliding past dest. Mirrors cmangos
|
||||
// MovementActions.cpp:1095-1106.
|
||||
{
|
||||
float const totalDistance = bot->GetExactDist(dest);
|
||||
if (totalDistance < sPlayerbotAIConfig.targetPosRecalcDistance)
|
||||
// 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.
|
||||
{
|
||||
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
|
||||
if (!lastMove.lastPath.empty() &&
|
||||
lastMove.lastPath.getBack().distance(dest) <= totalDistance)
|
||||
if (bot->isMoving() && lastMove.lastMoveToMapId == bot->GetMapId())
|
||||
{
|
||||
lastMove.clear();
|
||||
float remaining = bot->GetExactDist(lastMove.lastMoveToX, lastMove.lastMoveToY, lastMove.lastMoveToZ);
|
||||
if (remaining > 10.0f)
|
||||
{
|
||||
EmitDebugMove("MoveFar", "spline-plan",
|
||||
lastMove.lastMoveToX, lastMove.lastMoveToY, lastMove.lastMoveToZ);
|
||||
return true;
|
||||
}
|
||||
bot->StopMoving();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,7 +119,24 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
points.reserve(pts.size());
|
||||
for (auto const& wp : pts)
|
||||
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
|
||||
return DispatchPathPoints(dest, points, "reuse");
|
||||
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;
|
||||
}
|
||||
}
|
||||
// Path was cleared or collapsed — fall through to fresh planning.
|
||||
@ -119,33 +145,41 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
}
|
||||
}
|
||||
|
||||
float const dis = bot->GetExactDist(dest);
|
||||
float disToDest = bot->GetDistance(dest);
|
||||
float dis = bot->GetExactDist(dest);
|
||||
|
||||
// Mirrors cmangos ResolveMovePath: pick travel-node graph for
|
||||
// long-distance / cross-map moves; mmap probe for short paths.
|
||||
// Decision tree (cmangos ResolveMovePath order — travel nodes first):
|
||||
//
|
||||
// 1. needsLongPath = cross-map OR dis > sightDistance
|
||||
// 2. If needsLongPath && travel nodes enabled → graph plan
|
||||
// 3. Else 40-step chained mmap probe
|
||||
// 4. Regression guard against the new probe — ride cached path
|
||||
// if it ends at least as close to dest
|
||||
// 5. Dispatch probe (no reach gate — partial probes still make
|
||||
// progress and the next tick replans)
|
||||
// 6. Empty probe → cmangos addPoint(endPosition) fallback:
|
||||
// single-waypoint MoveTo so PathGenerator resolves it.
|
||||
// 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);
|
||||
|
||||
bool const needsLongPath = (bot->GetMapId() != dest.GetMapId()) ||
|
||||
(dis > sPlayerbotAIConfig.sightDistance);
|
||||
|
||||
// needsLongPath branch — graph routing only. cmangos's getFullPath
|
||||
// already attempts a 40-step mmap probe internally before falling
|
||||
// back to A* node routing, so we don't need a second probe here.
|
||||
// If the graph yields no plan, fall straight to the single-point
|
||||
// fallback (cmangos addPoint(endPosition)). BG gate per
|
||||
// MovementActions.cpp:705.
|
||||
if (needsLongPath)
|
||||
// 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.
|
||||
if (tryNodes && botAI->rpgInfo.HasActiveTravelPlan())
|
||||
{
|
||||
if (sPlayerbotAIConfig.enableTravelNodes && !bot->InBattleground())
|
||||
if (botAI->rpgInfo.travelPlan.destination.distance(dest) > 10.0f)
|
||||
botAI->rpgInfo.ClearTravel();
|
||||
else
|
||||
return UpdateTravelPlan();
|
||||
}
|
||||
|
||||
// PRIORITY: try the travel-node graph FIRST when the move is
|
||||
// long enough to need it.
|
||||
if (tryNodes)
|
||||
{
|
||||
StartTravelPlan(dest);
|
||||
if (botAI->rpgInfo.HasActiveTravelPlan())
|
||||
@ -156,25 +190,26 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||
return UpdateTravelPlan();
|
||||
}
|
||||
// Graph returned no plan — fall through to mmap probe.
|
||||
}
|
||||
else if (botAI->rpgInfo.HasActiveTravelPlan())
|
||||
{
|
||||
// Move dropped below node-first threshold — drop any leftover plan.
|
||||
botAI->rpgInfo.ClearTravel();
|
||||
}
|
||||
|
||||
// Long-path fallback. Cross-map can't be served by a same-map
|
||||
// straight-line spline — bail rather than splining toward
|
||||
// unreachable coords.
|
||||
if (bot->GetMapId() != dest.GetMapId())
|
||||
return false;
|
||||
|
||||
Movement::PointsArray fallback;
|
||||
fallback.emplace_back(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ());
|
||||
fallback.emplace_back(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||
return DispatchPathPoints(dest, fallback, "spline-long");
|
||||
}
|
||||
|
||||
// Short-path branch — 40-step chained mmap probe.
|
||||
// 40-step chained mmap probe — fallback when the node graph
|
||||
// returned no plan (or for short moves below nodeFirstDis).
|
||||
WorldPosition botPos(bot);
|
||||
std::vector<WorldPosition> probe = botPos.getPathTo(dest, bot);
|
||||
|
||||
// Regression guard.
|
||||
// Regression guard (cmangos ResolveMovePath parity): if a cached
|
||||
// lastPath ends at least as close to dest as the new probe's
|
||||
// endpoint, prefer the cached path. The 10% reuse block above
|
||||
// already returned early when cached was within 10% of dest;
|
||||
// this catches "cached is far (>10%) but still better than the
|
||||
// probe" — typically when the probe got blocked by geometry and
|
||||
// ended much farther from dest than where cached had reached.
|
||||
{
|
||||
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
|
||||
if (!lastMove.lastPath.empty() && !probe.empty() && probe.size() >= 2)
|
||||
@ -199,7 +234,24 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
points.reserve(pts.size());
|
||||
for (auto const& wp : pts)
|
||||
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
|
||||
return DispatchPathPoints(dest, points, "regress-keep");
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -207,248 +259,109 @@ 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.
|
||||
if (!probe.empty() && probe.size() >= 2)
|
||||
{
|
||||
WorldPosition stepDest = probe.back();
|
||||
float endDistToDest = dest.GetExactDist(stepDest.GetPositionX(),
|
||||
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());
|
||||
|
||||
if (points.size() >= 2)
|
||||
{
|
||||
LOG_INFO("playerbots", "[MoveFar] {} mmap-path | dis={:.0f} | wp={}",
|
||||
bot->GetName(), dis, (uint32)points.size());
|
||||
|
||||
if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive())
|
||||
botAI->DoSpecificAction("check mount state", Event(), true);
|
||||
|
||||
return DispatchPathPoints(dest, points, "mmap");
|
||||
}
|
||||
}
|
||||
|
||||
// Empty-probe fallback — cmangos addPoint(endPosition) + DispatchMovement.
|
||||
// Build a 2-point straight-line path (bot → dest) and route it through
|
||||
// DispatchPathPoints so it gets the same clip / underwater fixup /
|
||||
// setPath / WaitForReach / teleport / mode-flip treatment as the
|
||||
// chained-mmap branches. Trade-off vs the old MoveTo path: lose the
|
||||
// engine's MovePoint(generatePath=true) local PathGenerator resolution.
|
||||
// For the empty-probe case the long mmap probe already failed, so the
|
||||
// engine's local resolution would likely fail too.
|
||||
Movement::PointsArray fallback;
|
||||
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;
|
||||
wpts.reserve(points.size());
|
||||
for (auto const& pt : points)
|
||||
wpts.emplace_back(dest.GetMapId(), pt.x, pt.y, pt.z);
|
||||
lm.setPath(TravelPath(wpts));
|
||||
}
|
||||
|
||||
// Push submerged waypoints to the water surface unless dest 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Per-waypoint Z-snap to current ground.
|
||||
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())
|
||||
if (points.size() >= 2)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
// Truncate after the first hostile waypoint. cmangos's
|
||||
// analog at TravelNode.cpp:1158 sets endP=path.begin() if
|
||||
// i=0 (mob right at start) and erases everything after,
|
||||
// collapsing to a 1-point path. Our caller then sees
|
||||
// points.size() < 2 and bails out — the bot just stops.
|
||||
if (clipAt < points.size() && clipAt + 1 < points.size())
|
||||
points.erase(points.begin() + clipAt + 1, points.end());
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
// After clip, points may have collapsed to one element (mob right
|
||||
// at start). Bail rather than dispatching a degenerate spline.
|
||||
if (points.size() < 2)
|
||||
return false;
|
||||
// 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();
|
||||
|
||||
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;
|
||||
|
||||
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(), (uint32)duration,
|
||||
MovementPriority::MOVEMENT_NORMAL);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Probe failed or didn't progress — emit visibility whisper so
|
||||
// the user can see WHY mmap didn't dispatch.
|
||||
{
|
||||
bool const probeProgressed = !probe.empty() && probe.size() >= 2 &&
|
||||
(dest.GetExactDist(probe.back().GetPositionX(),
|
||||
probe.back().GetPositionY(), probe.back().GetPositionZ()) + 5.0f < disToDest);
|
||||
if (!probeProgressed)
|
||||
{
|
||||
char const* reason = (probe.empty() || probe.size() < 2) ? "mmap-empty" : "mmap-noprogress";
|
||||
EmitDebugMove("MoveFar", reason,
|
||||
dest.GetPositionX(), dest.GetPositionY(),
|
||||
dest.GetPositionZ());
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
void NewRpgBaseAction::StartTravelPlan(WorldPosition dest)
|
||||
|
||||
@ -76,20 +76,19 @@ 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();
|
||||
|
||||
// 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
|
||||
|
||||
@ -50,7 +50,7 @@ void NewRpgInfo::ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition fl
|
||||
|
||||
void NewRpgInfo::ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId)
|
||||
{
|
||||
Reset();
|
||||
startT = getMSTime();
|
||||
OutdoorPvP pvp;
|
||||
pvp.capturePointSpawnId = capturePointSpawnId;
|
||||
data = pvp;
|
||||
|
||||
@ -721,22 +721,6 @@ std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos
|
||||
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,
|
||||
// CalculatePath(destX,destY,destZ) defaults to the unit's
|
||||
// current position as start — which means every iteration of
|
||||
@ -745,12 +729,16 @@ std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos
|
||||
// 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.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
|
||||
@ -766,50 +754,11 @@ std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos
|
||||
// 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))
|
||||
if ((type & (PATHFIND_NORMAL | PATHFIND_INCOMPLETE))
|
||||
&& !(type & PATHFIND_NOT_USING_PATH))
|
||||
return fromPointsArray(points);
|
||||
|
||||
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)
|
||||
@ -845,50 +794,11 @@ std::vector<WorldPosition> WorldPosition::getPathFromPath(std::vector<WorldPosit
|
||||
|
||||
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);
|
||||
|
||||
// Limit the pathfinding attempts
|
||||
for (uint32 i = 0; i < maxAttempt; i++)
|
||||
{
|
||||
// Drop cached poly state from the previous step. AC's
|
||||
// BuildPolyPath has a subpath-prefix optimization
|
||||
// (PathGenerator.cpp:285-389) that keeps ~80% of the prior
|
||||
// call's poly chain when the new startPoly is anywhere in
|
||||
// it, then computes a suffix from there. For a chained
|
||||
// walker this implicitly snaps the start to the previous
|
||||
// corridor instead of using the explicit startPos we pass
|
||||
// in, bending the route. Reset wipes the cache so each step
|
||||
// is a fresh A*.
|
||||
path.Clear();
|
||||
|
||||
// Try to pathfind to this position.
|
||||
subPath = getPathStepFrom(currentPos, path);
|
||||
subPath = getPathStepFrom(currentPos, bot);
|
||||
|
||||
// If we could not find a path return what we have now.
|
||||
if (subPath.empty() || currentPos.distance(&subPath.back()) < sPlayerbotAIConfig.targetPosRecalcDistance)
|
||||
@ -905,9 +815,6 @@ std::vector<WorldPosition> WorldPosition::getPathFromPath(std::vector<WorldPosit
|
||||
currentPos = subPath.back();
|
||||
}
|
||||
|
||||
if (tempCreature)
|
||||
delete tempCreature;
|
||||
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,6 @@
|
||||
class Creature;
|
||||
class GuidPosition;
|
||||
class ObjectGuid;
|
||||
class PathGenerator;
|
||||
class Quest;
|
||||
class Player;
|
||||
class PlayerbotAI;
|
||||
@ -284,7 +283,6 @@ 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,32 +1285,19 @@ TravelNodeRoute TravelNodeMap::FindRouteNearestNodes(WorldPosition startPos, Wor
|
||||
|
||||
bool TravelNodeMap::GetFullPath(TravelPlan& plan,
|
||||
WorldPosition botPos, uint32 botZoneId,
|
||||
WorldPosition destination, Unit* bot)
|
||||
WorldPosition destination)
|
||||
{
|
||||
plan.Reset();
|
||||
plan.destination = destination;
|
||||
|
||||
// mmap probe first — mirrors cmangos getFullPath (TravelNode.cpp:1887-1895).
|
||||
// 40-step chained probe from bot; if it gets within spellDistance of dest
|
||||
// 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);
|
||||
// Guard the degenerate case: getPathFromPath always returns at
|
||||
// least {botPos}, and isPathTo(probe, spellDistance) succeeds
|
||||
// when the bot is already within spellDistance of dest.
|
||||
// Without this guard we'd emit a 1-element plan [botPos as
|
||||
// PREPATH] and ExecuteTravelPlan would silently no-op every
|
||||
// tick.
|
||||
if (probe.size() >= 2 && destination.isPathTo(probe, sPlayerbotAIConfig.spellDistance))
|
||||
// Short distance — direct walk, no nodes needed
|
||||
if (botPos.fDist(destination) < MAX_PATHFINDING_DISTANCE &&
|
||||
botPos.GetMapId() == destination.GetMapId())
|
||||
{
|
||||
plan.steps.addPoint(botPos, PathNodeType::NODE_PREPATH);
|
||||
for (size_t i = 1; i < probe.size(); ++i)
|
||||
plan.steps.addPoint(probe[i], PathNodeType::NODE_PATH);
|
||||
plan.steps.addPoint(destination, 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, Unit* bot = nullptr);
|
||||
uint32 botZoneId, WorldPosition destination);
|
||||
|
||||
// Resolve A* route between two world positions (returns node vector)
|
||||
std::vector<TravelNode*> ResolveRoute(WorldPosition startPos,
|
||||
|
||||
@ -98,7 +98,6 @@ 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, walkDistance;
|
||||
targetPosRecalcDistance, farDistance, healDistance, aggroDistance;
|
||||
uint32 criticalHealth, lowHealth, mediumHealth, almostFullHealth;
|
||||
uint32 lowMana, mediumMana, highMana;
|
||||
bool autoSaveMana;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user