mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
feat(Core/Travel): Enable travel node system for RPG pathfinding (#2312)
This commit is contained in:
parent
4bd5a9b89c
commit
b19ab481b3
@ -1054,6 +1054,12 @@ AiPlayerbot.RestrictedHealerDPSMaps = "33,34,36,43,47,48,70,90,109,129,209,229,2
|
|||||||
# Default: 1 (enabled)
|
# Default: 1 (enabled)
|
||||||
AiPlayerbot.EnableNewRpgStrategy = 1
|
AiPlayerbot.EnableNewRpgStrategy = 1
|
||||||
|
|
||||||
|
# Use pre-computed travel node paths for long-distance movement (>300 yards).
|
||||||
|
# When enabled, bots use the travel node graph (A*, flight paths, transports)
|
||||||
|
# instead of repeated mmap hops. Experimental.
|
||||||
|
# Default: 0 (disabled)
|
||||||
|
AiPlayerbot.EnableTravelNodes = 0
|
||||||
|
|
||||||
# Control probability weights for RPG status of bots. Takes effect only when the status meets its premise.
|
# Control probability weights for RPG status of bots. Takes effect only when the status meets its premise.
|
||||||
# Sum of weights need not be 100. Set to 0 to disable the status.
|
# Sum of weights need not be 100. Set to 0 to disable the status.
|
||||||
#
|
#
|
||||||
|
|||||||
@ -6,10 +6,10 @@
|
|||||||
#include "CheckValuesAction.h"
|
#include "CheckValuesAction.h"
|
||||||
|
|
||||||
#include "Event.h"
|
#include "Event.h"
|
||||||
|
#include "ObjectGuid.h"
|
||||||
#include "ServerFacade.h"
|
#include "ServerFacade.h"
|
||||||
|
|
||||||
#include "PlayerbotAI.h"
|
#include "PlayerbotAI.h"
|
||||||
#include "TravelNode.h"
|
|
||||||
#include "AiObjectContext.h"
|
#include "AiObjectContext.h"
|
||||||
|
|
||||||
CheckValuesAction::CheckValuesAction(PlayerbotAI* botAI) : Action(botAI, "check values") {}
|
CheckValuesAction::CheckValuesAction(PlayerbotAI* botAI) : Action(botAI, "check values") {}
|
||||||
@ -21,11 +21,6 @@ bool CheckValuesAction::Execute(Event /*event*/)
|
|||||||
botAI->Ping(bot->GetPositionX(), bot->GetPositionY());
|
botAI->Ping(bot->GetPositionX(), bot->GetPositionY());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (botAI->HasStrategy("map", BOT_STATE_NON_COMBAT) || botAI->HasStrategy("map full", BOT_STATE_NON_COMBAT))
|
|
||||||
{
|
|
||||||
TravelNodeMap::instance().manageNodes(bot, botAI->HasStrategy("map full", BOT_STATE_NON_COMBAT));
|
|
||||||
}
|
|
||||||
|
|
||||||
GuidVector possible_targets = *context->GetValue<GuidVector>("possible targets");
|
GuidVector possible_targets = *context->GetValue<GuidVector>("possible targets");
|
||||||
GuidVector all_targets = *context->GetValue<GuidVector>("all targets");
|
GuidVector all_targets = *context->GetValue<GuidVector>("all targets");
|
||||||
GuidVector npcs = *context->GetValue<GuidVector>("nearest npcs");
|
GuidVector npcs = *context->GetValue<GuidVector>("nearest npcs");
|
||||||
|
|||||||
@ -76,7 +76,7 @@ bool DebugAction::Execute(Event event)
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
std::vector<WorldPosition> beginPath, endPath;
|
std::vector<WorldPosition> beginPath, endPath;
|
||||||
TravelNodeRoute route = TravelNodeMap::instance().getRoute(botPos, *points.front(), beginPath, bot);
|
TravelNodeRoute route = TravelNodeMap::instance().FindRouteNearestNodes(botPos, *points.front(), beginPath, bot);
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Traveling to " << dest->getTitle() << ": ";
|
out << "Traveling to " << dest->getTitle() << ": ";
|
||||||
@ -196,18 +196,18 @@ bool DebugAction::Execute(Event event)
|
|||||||
{
|
{
|
||||||
WorldPosition pos(bot);
|
WorldPosition pos(bot);
|
||||||
|
|
||||||
std::string const name = "USER:" + text.substr(9);
|
std::string suffix = text.size() > 9 ? text.substr(9) : pos.getAreaName();
|
||||||
|
std::string const name = "USER:" + suffix;
|
||||||
|
|
||||||
/* TravelNode* startNode = */ TravelNodeMap::instance().addNode(pos, name, false, false); // startNode not used, but addNode as side effect, fragment marked for removal.
|
{
|
||||||
|
std::lock_guard<std::shared_timed_mutex> lock(TravelNodeMap::instance().m_nMapMtx);
|
||||||
|
TravelNodeMap::instance().addNode(pos, name, false, true);
|
||||||
|
|
||||||
for (auto& endNode : TravelNodeMap::instance().getNodes(pos, 2000))
|
for (auto& endNode : TravelNodeMap::instance().getNodes(pos, 2000))
|
||||||
{
|
|
||||||
endNode->setLinked(false);
|
endNode->setLinked(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
botAI->TellMasterNoFacing("Node " + name + " created.");
|
botAI->TellMasterNoFacing("Node " + name + " created. Use console command '.playerbots travel generatenode' to connect nodes.");
|
||||||
|
|
||||||
TravelNodeMap::instance().setHasToGen();
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -223,14 +223,15 @@ bool DebugAction::Execute(Event event)
|
|||||||
if (startNode->isImportant())
|
if (startNode->isImportant())
|
||||||
{
|
{
|
||||||
botAI->TellMasterNoFacing("Node can not be removed.");
|
botAI->TellMasterNoFacing("Node can not be removed.");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
TravelNodeMap::instance().m_nMapMtx.lock();
|
{
|
||||||
|
std::lock_guard<std::shared_timed_mutex> lock(TravelNodeMap::instance().m_nMapMtx);
|
||||||
TravelNodeMap::instance().removeNode(startNode);
|
TravelNodeMap::instance().removeNode(startNode);
|
||||||
botAI->TellMasterNoFacing("Node removed.");
|
}
|
||||||
TravelNodeMap::instance().m_nMapMtx.unlock();
|
|
||||||
|
|
||||||
TravelNodeMap::instance().setHasToGen();
|
botAI->TellMasterNoFacing("Node removed. Use console command '.playerbots travel generatenode' to finalize nodes.");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -247,15 +248,17 @@ bool DebugAction::Execute(Event event)
|
|||||||
node->removeLinkTo(path.first, true);
|
node->removeLinkTo(path.first, true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if (text.find("gen node") != std::string::npos)
|
else if (text.find("gen node") != std::string::npos ||
|
||||||
|
text.find("gen path") != std::string::npos)
|
||||||
{
|
{
|
||||||
// Pathfinder
|
// Disabled: generateAll() touches Map / grid / mmap state that is only
|
||||||
TravelNodeMap::instance().generateNodes();
|
// safe to mutate on the world thread. Running it from a detached worker
|
||||||
return true;
|
// (or from a bot tick on a MapUpdater thread) races with world updates
|
||||||
}
|
// and freezes the server. Use the console command instead, which runs
|
||||||
else if (text.find("gen path") != std::string::npos)
|
// synchronously on the world thread:
|
||||||
{
|
// .playerbots travel generatenode
|
||||||
TravelNodeMap::instance().generatePaths();
|
botAI->TellMasterNoFacing(
|
||||||
|
"Disabled in chat. Run '.playerbots travel generatenode' from the server console.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if (text.find("crop path") != std::string::npos)
|
else if (text.find("crop path") != std::string::npos)
|
||||||
@ -275,7 +278,7 @@ bool DebugAction::Execute(Event event)
|
|||||||
[]
|
[]
|
||||||
{
|
{
|
||||||
TravelNodeMap::instance().removeNodes();
|
TravelNodeMap::instance().removeNodes();
|
||||||
TravelNodeMap::instance().loadNodeStore();
|
TravelNodeMap::instance().LoadNodeStore();
|
||||||
});
|
});
|
||||||
|
|
||||||
t.detach();
|
t.detach();
|
||||||
@ -297,7 +300,7 @@ bool DebugAction::Execute(Event event)
|
|||||||
|
|
||||||
// uint32 time = 60 * IN_MILLISECONDS; //not used, line marked for removal.
|
// uint32 time = 60 * IN_MILLISECONDS; //not used, line marked for removal.
|
||||||
|
|
||||||
std::vector<WorldPosition> ppath = l.second->getPath();
|
std::vector<WorldPosition> ppath = l.second->GetPath();
|
||||||
|
|
||||||
for (auto p : ppath)
|
for (auto p : ppath)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -19,83 +19,8 @@
|
|||||||
#include "Transport.h"
|
#include "Transport.h"
|
||||||
#include "Map.h"
|
#include "Map.h"
|
||||||
|
|
||||||
namespace
|
// Transport helpers (GetTransportForPosTolerant, FindBoardingPointOnTransport,
|
||||||
{
|
// BoardTransport) are now on MovementAction — inherited by FollowAction.
|
||||||
Transport* GetTransportForPosTolerant(Map* map, WorldObject* ref, uint32 phaseMask, float x, float y, float z)
|
|
||||||
{
|
|
||||||
if (!map || !ref)
|
|
||||||
return nullptr;
|
|
||||||
|
|
||||||
std::array<float, 4> const probes = { z, z + 0.5f, z + 1.5f, z - 0.5f };
|
|
||||||
for (float const pz : probes)
|
|
||||||
{
|
|
||||||
if (Transport* t = map->GetTransportForPos(phaseMask, x, y, pz, ref))
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempts to find a point on the leader's transport that is closer to the bot,
|
|
||||||
// by probing along the segment from master -> bot and returning the last point
|
|
||||||
// that is still detected as being on the expected transport.
|
|
||||||
bool FindBoardingPointOnTransport(Map* map, Transport* expectedTransport, WorldObject* ref,
|
|
||||||
float masterX, float masterY, float masterZ,
|
|
||||||
float botX, float botY, float botZ,
|
|
||||||
float& outX, float& outY, float& outZ)
|
|
||||||
{
|
|
||||||
if (!map || !expectedTransport || !ref)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
uint32 const phaseMask = ref->GetPhaseMask();
|
|
||||||
|
|
||||||
// Ensure master is actually detected on that transport (tolerant).
|
|
||||||
if (GetTransportForPosTolerant(map, ref, phaseMask, masterX, masterY, masterZ) != expectedTransport)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// The raycast in GetTransportForPos starts at (z + 2). Probe with a safe Z.
|
|
||||||
float const probeZ = std::max(masterZ, botZ);
|
|
||||||
|
|
||||||
// Adaptive step count: small platforms need tighter sampling.
|
|
||||||
float const dx2 = botX - masterX;
|
|
||||||
float const dy2 = botY - masterY;
|
|
||||||
float const dist2d = std::sqrt(dx2 * dx2 + dy2 * dy2);
|
|
||||||
int32 const steps = std::clamp(static_cast<int32>(dist2d / 0.75f), 10, 28);
|
|
||||||
|
|
||||||
float const dx = (botX - masterX) / static_cast<float>(steps);
|
|
||||||
float const dy = (botY - masterY) / static_cast<float>(steps);
|
|
||||||
|
|
||||||
// Master must actually be on the expected transport for this to work.
|
|
||||||
if (map->GetTransportForPos(ref->GetPhaseMask(), masterX, masterY, probeZ, ref) != expectedTransport)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
float lastX = masterX;
|
|
||||||
float lastY = masterY;
|
|
||||||
bool found = false;
|
|
||||||
|
|
||||||
for (int32 i = 1; i <= steps; ++i)
|
|
||||||
{
|
|
||||||
float const px = masterX + dx * i;
|
|
||||||
float const py = masterY + dy * i;
|
|
||||||
|
|
||||||
Transport* const t = GetTransportForPosTolerant(map, ref, phaseMask, px, py, probeZ);
|
|
||||||
if (t != expectedTransport)
|
|
||||||
break;
|
|
||||||
|
|
||||||
lastX = px;
|
|
||||||
lastY = py;
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
outX = lastX;
|
|
||||||
outY = lastY;
|
|
||||||
outZ = masterZ; // keep deck-level Z to encourage stepping onto the platform/boat
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool FollowAction::Execute(Event /*event*/)
|
bool FollowAction::Execute(Event /*event*/)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "Corpse.h"
|
#include "Corpse.h"
|
||||||
|
#include "DBCStores.h"
|
||||||
#include "Event.h"
|
#include "Event.h"
|
||||||
#include "FleeManager.h"
|
#include "FleeManager.h"
|
||||||
#include "G3D/Vector3.h"
|
#include "G3D/Vector3.h"
|
||||||
@ -19,7 +20,9 @@
|
|||||||
#include "LootObjectStack.h"
|
#include "LootObjectStack.h"
|
||||||
#include "Map.h"
|
#include "Map.h"
|
||||||
#include "MotionMaster.h"
|
#include "MotionMaster.h"
|
||||||
|
#include "MoveSpline.h"
|
||||||
#include "MoveSplineInitArgs.h"
|
#include "MoveSplineInitArgs.h"
|
||||||
|
#include "TravelNode.h"
|
||||||
#include "MovementGenerator.h"
|
#include "MovementGenerator.h"
|
||||||
#include "ObjectDefines.h"
|
#include "ObjectDefines.h"
|
||||||
#include "ObjectGuid.h"
|
#include "ObjectGuid.h"
|
||||||
@ -36,6 +39,7 @@
|
|||||||
#include "SpellInfo.h"
|
#include "SpellInfo.h"
|
||||||
#include "Stances.h"
|
#include "Stances.h"
|
||||||
#include "Timer.h"
|
#include "Timer.h"
|
||||||
|
#include "Transport.h"
|
||||||
#include "Unit.h"
|
#include "Unit.h"
|
||||||
#include "Vehicle.h"
|
#include "Vehicle.h"
|
||||||
#include "WaypointMovementGenerator.h"
|
#include "WaypointMovementGenerator.h"
|
||||||
@ -128,10 +132,8 @@ bool MovementAction::MoveToLOS(WorldObject* target, bool ranged)
|
|||||||
float z = target->GetPositionZ();
|
float z = target->GetPositionZ();
|
||||||
|
|
||||||
// Use standard PathGenerator to find a route.
|
// Use standard PathGenerator to find a route.
|
||||||
PathGenerator path(bot);
|
PathResult path = GeneratePath(x, y, z, DEFAULT_PATH_ACCEPT_MASK, false);
|
||||||
path.CalculatePath(x, y, z, false);
|
if (!path.reachable)
|
||||||
PathType type = path.GetPathType();
|
|
||||||
if (type != PATHFIND_NORMAL && type != PATHFIND_INCOMPLETE)
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (!ranged)
|
if (!ranged)
|
||||||
@ -140,9 +142,9 @@ bool MovementAction::MoveToLOS(WorldObject* target, bool ranged)
|
|||||||
float dist = FLT_MAX;
|
float dist = FLT_MAX;
|
||||||
PositionInfo dest;
|
PositionInfo dest;
|
||||||
|
|
||||||
if (!path.GetPath().empty())
|
if (!path.points.empty())
|
||||||
{
|
{
|
||||||
for (auto& point : path.GetPath())
|
for (auto& point : path.points)
|
||||||
{
|
{
|
||||||
if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
|
if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
|
||||||
CreateWp(bot, point.x, point.y, point.z, 0.0, 2334);
|
CreateWp(bot, point.x, point.y, point.z, 0.0, 2334);
|
||||||
@ -1719,6 +1721,19 @@ bool MovementAction::MoveInside(uint32 mapId, float x, float y, float z, float d
|
|||||||
// return current_z;
|
// return current_z;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
PathResult MovementAction::GeneratePath(float x, float y, float z, uint32 acceptMask, bool forceDestination)
|
||||||
|
{
|
||||||
|
PathResult result;
|
||||||
|
PathGenerator gen(bot);
|
||||||
|
gen.CalculatePath(x, y, z, forceDestination);
|
||||||
|
result.pathType = gen.GetPathType();
|
||||||
|
result.reachable = !(result.pathType & (~acceptMask));
|
||||||
|
result.points = gen.GetPath();
|
||||||
|
result.actualEnd = gen.GetActualEndPosition();
|
||||||
|
result.end = gen.GetEndPosition();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
const Movement::PointsArray MovementAction::SearchForBestPath(float x, float y, float z, float& modified_z,
|
const Movement::PointsArray MovementAction::SearchForBestPath(float x, float y, float z, float& modified_z,
|
||||||
int maxSearchCount, bool normal_only, float step)
|
int maxSearchCount, bool normal_only, float step)
|
||||||
{
|
{
|
||||||
@ -2959,4 +2974,571 @@ bool MoveAwayFromPlayerWithDebuffAction::Execute(Event /*event*/)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MoveAwayFromPlayerWithDebuffAction::isPossible() { return bot->CanFreeMove(); }
|
bool MoveAwayFromPlayerWithDebuffAction::isPossible()
|
||||||
|
{
|
||||||
|
return bot->CanFreeMove();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MovementAction::CheckSplineProgress(TravelPlan& state)
|
||||||
|
{
|
||||||
|
if (!state.splineActive)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// walkPoints may have been cleared by a map transfer or external reset
|
||||||
|
// while the spline was still flagged active; bail out safely.
|
||||||
|
if (state.walkPoints.empty())
|
||||||
|
{
|
||||||
|
state.splineActive = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bot->movespline->Finalized())
|
||||||
|
{
|
||||||
|
G3D::Vector3 const& endPt = state.walkPoints.back();
|
||||||
|
float distToEnd = bot->GetExactDist(endPt.x, endPt.y, endPt.z);
|
||||||
|
|
||||||
|
if (distToEnd < 10.0f)
|
||||||
|
{
|
||||||
|
state.splineActive = false;
|
||||||
|
state.walkPoints.clear();
|
||||||
|
return true; // Arrived
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spline finalized short of target — interrupted (combat/knockback/etc).
|
||||||
|
// Caller will re-launch.
|
||||||
|
state.splineActive = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stuck detection
|
||||||
|
if (state.splineStartTime &&
|
||||||
|
GetMSTimeDiffToNow(state.splineStartTime) > state.expectedDuration * 2 + (30 * IN_MILLISECONDS))
|
||||||
|
{
|
||||||
|
G3D::Vector3 const& endPt = state.walkPoints.back();
|
||||||
|
botAI->TeleportTo(WorldLocation(bot->GetMapId(), endPt.x, endPt.y, endPt.z));
|
||||||
|
state.splineActive = false;
|
||||||
|
state.walkPoints.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // Still moving
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MovementAction::LaunchWalkSpline(TravelPlan& state)
|
||||||
|
{
|
||||||
|
if (state.walkPoints.size() < 2)
|
||||||
|
{
|
||||||
|
state.walkPoints.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Trim past any stored points the bot has already moved past — useful
|
||||||
|
// when a spline is interrupted (combat, knockback, mid-spline reissue)
|
||||||
|
// and we re-launch from a position later in the route.
|
||||||
|
G3D::Vector3 botPos(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ());
|
||||||
|
float closestDist = FLT_MAX;
|
||||||
|
size_t closestIdx = 0;
|
||||||
|
for (size_t i = 0; i < state.walkPoints.size(); ++i)
|
||||||
|
{
|
||||||
|
float distance = (state.walkPoints[i] - botPos).squaredLength();
|
||||||
|
if (distance < closestDist)
|
||||||
|
{
|
||||||
|
closestDist = distance;
|
||||||
|
closestIdx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (closestIdx > 0)
|
||||||
|
state.walkPoints.erase(state.walkPoints.begin(), state.walkPoints.begin() + closestIdx);
|
||||||
|
|
||||||
|
if (state.walkPoints.size() < 2)
|
||||||
|
{
|
||||||
|
state.walkPoints.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mount up
|
||||||
|
if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive())
|
||||||
|
botAI->DoSpecificAction("check mount state", Event(), true);
|
||||||
|
|
||||||
|
float totalDist = 0;
|
||||||
|
for (size_t i = 1; i < state.walkPoints.size(); ++i)
|
||||||
|
totalDist += (state.walkPoints[i] - state.walkPoints[i - 1]).length();
|
||||||
|
|
||||||
|
float speed = bot->GetSpeed(MOVE_RUN);
|
||||||
|
state.expectedDuration = static_cast<uint32>((totalDist / speed) * IN_MILLISECONDS);
|
||||||
|
|
||||||
|
bot->GetMotionMaster()->MoveSplinePath(&state.walkPoints, FORCED_MOVEMENT_RUN);
|
||||||
|
|
||||||
|
state.splineStartTime = getMSTime();
|
||||||
|
state.splineActive = true;
|
||||||
|
|
||||||
|
return false; // Walking
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MovementAction::MoveToSpline(TravelPlan& state, WorldPosition target)
|
||||||
|
{
|
||||||
|
if (!IsMovingAllowed())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Generate path
|
||||||
|
state.walkPoints.clear();
|
||||||
|
PathResult path = GeneratePath(target.GetPositionX(), target.GetPositionY(), target.GetPositionZ());
|
||||||
|
for (auto const& pt : path.points)
|
||||||
|
state.walkPoints.push_back(G3D::Vector3(pt.x, pt.y, pt.z));
|
||||||
|
|
||||||
|
if (state.walkPoints.size() < 2)
|
||||||
|
{
|
||||||
|
state.walkPoints.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch spline movement
|
||||||
|
LaunchWalkSpline(state);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MovementAction::GetTravelPlan(TravelPlan& plan, WorldPosition destination)
|
||||||
|
{
|
||||||
|
WorldPosition botPos(bot->GetMapId(), bot->GetPositionX(),
|
||||||
|
bot->GetPositionY(), bot->GetPositionZ());
|
||||||
|
|
||||||
|
LOG_DEBUG("playerbots",
|
||||||
|
"[TravelPlan] {} requesting plan: from ({:.0f},{:.0f},{:.0f}) map={} zone={} → "
|
||||||
|
"({:.0f},{:.0f},{:.0f}) map={} (straight={:.0f}yd)",
|
||||||
|
bot->GetName(), botPos.GetPositionX(), botPos.GetPositionY(), botPos.GetPositionZ(),
|
||||||
|
bot->GetMapId(), bot->GetZoneId(),
|
||||||
|
destination.GetPositionX(), destination.GetPositionY(), destination.GetPositionZ(),
|
||||||
|
destination.GetMapId(), botPos.fDist(destination));
|
||||||
|
|
||||||
|
return sTravelNodeMap.GetFullPath(plan, botPos, bot->GetZoneId(), destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MovementAction::ExecuteTravelPlan(TravelPlan& state)
|
||||||
|
{
|
||||||
|
if (!state.IsActive())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (bot->IsInFlight())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Handle active spline
|
||||||
|
if (state.splineActive)
|
||||||
|
{
|
||||||
|
if (!CheckSplineProgress(state))
|
||||||
|
{
|
||||||
|
if (state.splineActive)
|
||||||
|
return true; // Still moving
|
||||||
|
else
|
||||||
|
LaunchWalkSpline(state); // Interrupted, re-launch
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.stepIdx >= state.steps.size())
|
||||||
|
{
|
||||||
|
state.Reset();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PathNodePoint& pt = state.steps[state.stepIdx];
|
||||||
|
|
||||||
|
switch (pt.type)
|
||||||
|
{
|
||||||
|
case PathNodeType::NODE_PREPATH:
|
||||||
|
{
|
||||||
|
if (state.stepIdx + 1 >= state.steps.size())
|
||||||
|
{
|
||||||
|
state.stepIdx++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
float const botX = bot->GetPositionX();
|
||||||
|
float const botY = bot->GetPositionY();
|
||||||
|
float const botZ = bot->GetPositionZ();
|
||||||
|
|
||||||
|
// Walk forward through the route while distance keeps shrinking.
|
||||||
|
// Once it starts growing we're past the closest waypoint — break.
|
||||||
|
size_t bestIdx = state.stepIdx + 1;
|
||||||
|
float bestDistSq = FLT_MAX;
|
||||||
|
for (size_t i = state.stepIdx + 1; i < state.steps.size(); ++i)
|
||||||
|
{
|
||||||
|
const PathNodePoint& cand = state.steps[i];
|
||||||
|
if (cand.type != PathNodeType::NODE_PATH &&
|
||||||
|
cand.type != PathNodeType::NODE_NODE)
|
||||||
|
break; // stop at portal/transport/etc — can't walk past
|
||||||
|
|
||||||
|
float const dx = cand.point.GetPositionX() - botX;
|
||||||
|
float const dy = cand.point.GetPositionY() - botY;
|
||||||
|
float const dz = cand.point.GetPositionZ() - botZ;
|
||||||
|
float const dSq = dx * dx + dy * dy + dz * dz;
|
||||||
|
if (dSq >= bestDistSq)
|
||||||
|
break; // moving away — closest waypoint already found
|
||||||
|
|
||||||
|
bestDistSq = dSq;
|
||||||
|
bestIdx = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr float ARRIVAL_DIST = 5.0f;
|
||||||
|
|
||||||
|
WorldPosition const& target = state.steps[bestIdx].point;
|
||||||
|
float const distToTarget = bot->GetExactDist(
|
||||||
|
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ());
|
||||||
|
|
||||||
|
if (distToTarget < ARRIVAL_DIST)
|
||||||
|
{
|
||||||
|
state.stepIdx = bestIdx;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MoveTo(target.GetMapId(),
|
||||||
|
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ(),
|
||||||
|
false, false, false, true /*exact_waypoint*/);
|
||||||
|
}
|
||||||
|
|
||||||
|
case PathNodeType::NODE_PATH:
|
||||||
|
case PathNodeType::NODE_NODE:
|
||||||
|
{
|
||||||
|
// Batch consecutive walk points into one spline. Capped small 20 points per tick.
|
||||||
|
static constexpr uint32 MAX_SPLINE_POINTS = 20;
|
||||||
|
state.walkPoints.clear();
|
||||||
|
while (state.stepIdx < state.steps.size() && state.walkPoints.size() < MAX_SPLINE_POINTS)
|
||||||
|
{
|
||||||
|
const PathNodePoint& wp = state.steps[state.stepIdx];
|
||||||
|
if (wp.type != PathNodeType::NODE_PATH && wp.type != PathNodeType::NODE_NODE)
|
||||||
|
break;
|
||||||
|
state.walkPoints.push_back(G3D::Vector3(wp.point.GetPositionX(), wp.point.GetPositionY(), wp.point.GetPositionZ()));
|
||||||
|
state.stepIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.walkPoints.empty())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Already near end of batch?
|
||||||
|
G3D::Vector3 const& last = state.walkPoints.back();
|
||||||
|
float dist = bot->GetExactDist(last.x, last.y, last.z);
|
||||||
|
if (dist < 10.0f)
|
||||||
|
{
|
||||||
|
state.walkPoints.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Too far from first point — abort the plan and let the
|
||||||
|
// caller's stuck-recovery decide what to do. (cmangos
|
||||||
|
// doesn't teleport here either; an abandoned plan is
|
||||||
|
// recovered by the next MoveFarTo cycle.)
|
||||||
|
if (state.walkPoints.size() >= 2)
|
||||||
|
{
|
||||||
|
G3D::Vector3 const& first = state.walkPoints.front();
|
||||||
|
float distToFirst = bot->GetExactDist(first.x, first.y, first.z);
|
||||||
|
if (distToFirst > MAX_PATHFINDING_DISTANCE)
|
||||||
|
{
|
||||||
|
state.walkPoints.clear();
|
||||||
|
state.Reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Single point — use PathGenerator directly
|
||||||
|
if (state.walkPoints.size() < 2)
|
||||||
|
{
|
||||||
|
WorldPosition target(bot->GetMapId(), last.x, last.y, last.z);
|
||||||
|
MoveToSpline(state, target);
|
||||||
|
state.walkPoints.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
LaunchWalkSpline(state);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case PathNodeType::NODE_PORTAL:
|
||||||
|
{
|
||||||
|
// Pair: source (pointIdx) + dest (pointIdx+1)
|
||||||
|
if (state.stepIdx + 1 >= state.steps.size())
|
||||||
|
{
|
||||||
|
state.Reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PathNodePoint& src = state.steps[state.stepIdx];
|
||||||
|
const PathNodePoint& dst = state.steps[state.stepIdx + 1];
|
||||||
|
|
||||||
|
// Already on destination map?
|
||||||
|
if (bot->GetMapId() == dst.point.GetMapId())
|
||||||
|
{
|
||||||
|
state.stepIdx += 2;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Walk to portal source
|
||||||
|
float dist = bot->GetExactDist(src.point.GetPositionX(), src.point.GetPositionY(), src.point.GetPositionZ());
|
||||||
|
if (dist > INTERACTION_DISTANCE)
|
||||||
|
return MoveTo(src.point.GetMapId(), src.point.GetPositionX(), src.point.GetPositionY(), src.point.GetPositionZ());
|
||||||
|
|
||||||
|
// At portal but didn't cross — natural collision missed.
|
||||||
|
// Abort the plan; stuck-recovery in MoveFarTo will decide
|
||||||
|
// whether to retry or teleport the bot. (cmangos doesn't
|
||||||
|
// teleport here either.)
|
||||||
|
state.Reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
case PathNodeType::NODE_TRANSPORT:
|
||||||
|
{
|
||||||
|
if (state.stepIdx + 1 >= state.steps.size())
|
||||||
|
{
|
||||||
|
state.Reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PathNodePoint& board = state.steps[state.stepIdx];
|
||||||
|
const PathNodePoint& arrive = state.steps[state.stepIdx + 1];
|
||||||
|
// Arrived at destination?
|
||||||
|
if (bot->GetMapId() == arrive.point.GetMapId() && !bot->GetTransport())
|
||||||
|
{
|
||||||
|
state.stepIdx += 2;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// On transport — wait
|
||||||
|
if (bot->GetTransport())
|
||||||
|
{
|
||||||
|
if (bot->GetMapId() == arrive.point.GetMapId())
|
||||||
|
{
|
||||||
|
bot->GetTransport()->RemovePassenger(bot);
|
||||||
|
bot->StopMovingOnCurrentPos();
|
||||||
|
state.stepIdx += 2;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk to boarding point
|
||||||
|
float dist = bot->GetExactDist(board.point.GetPositionX(), board.point.GetPositionY(), board.point.GetPositionZ());
|
||||||
|
if (dist > 60.0f)
|
||||||
|
return MoveTo(board.point.GetMapId(), board.point.GetPositionX(), board.point.GetPositionY(), board.point.GetPositionZ());
|
||||||
|
|
||||||
|
// Try to board
|
||||||
|
if (board.entry)
|
||||||
|
{
|
||||||
|
Map* map = bot->GetMap();
|
||||||
|
if (map)
|
||||||
|
{
|
||||||
|
Transport* transport =
|
||||||
|
GetTransportForPosTolerant(map, bot, bot->GetPhaseMask(), board.point.GetPositionX(),
|
||||||
|
board.point.GetPositionY(), board.point.GetPositionZ());
|
||||||
|
if (transport && transport->GetEntry() == board.entry)
|
||||||
|
{
|
||||||
|
BoardTransport(transport);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Wait at boarding point
|
||||||
|
if (dist > INTERACTION_DISTANCE)
|
||||||
|
return MoveTo(board.point.GetMapId(), board.point.GetPositionX(), board.point.GetPositionY(), board.point.GetPositionZ());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case PathNodeType::NODE_FLIGHTPATH:
|
||||||
|
{
|
||||||
|
if (state.stepIdx + 1 >= state.steps.size())
|
||||||
|
{
|
||||||
|
state.Reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PathNodePoint& dep = state.steps[state.stepIdx];
|
||||||
|
const PathNodePoint& arr = state.steps[state.stepIdx + 1];
|
||||||
|
|
||||||
|
if (bot->IsInFlight())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Resolve taxi path
|
||||||
|
if (state.route.empty())
|
||||||
|
{
|
||||||
|
uint32 fromTaxi = sObjectMgr->GetNearestTaxiNode(dep.point.GetPositionX(), dep.point.GetPositionY(),
|
||||||
|
dep.point.GetPositionZ(), dep.point.GetMapId(), bot->GetTeamId());
|
||||||
|
uint32 toTaxi = sObjectMgr->GetNearestTaxiNode(arr.point.GetPositionX(), arr.point.GetPositionY(),
|
||||||
|
arr.point.GetPositionZ(), arr.point.GetMapId(), bot->GetTeamId());
|
||||||
|
|
||||||
|
if (fromTaxi && toTaxi && fromTaxi != toTaxi)
|
||||||
|
state.route = sTravelNodeMap.FindTaxiPath(fromTaxi, toTaxi);
|
||||||
|
|
||||||
|
if (state.route.empty())
|
||||||
|
{
|
||||||
|
state.stepIdx += 2;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TravelMgr::FlightMasterInfo const* fmInfo = sTravelMgr.GetNearestFlightMasterInfo(bot);
|
||||||
|
if (!fmInfo)
|
||||||
|
{
|
||||||
|
state.route.clear();
|
||||||
|
state.stepIdx += 2;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bot->GetDistance(fmInfo->pos) > INTERACTION_DISTANCE)
|
||||||
|
return MoveTo(fmInfo->pos.GetMapId(), fmInfo->pos.GetPositionX(),
|
||||||
|
fmInfo->pos.GetPositionY(), fmInfo->pos.GetPositionZ());
|
||||||
|
|
||||||
|
ObjectGuid fmGuid = ObjectGuid::Create<HighGuid::Unit>(fmInfo->templateEntry, fmInfo->dbGuid);
|
||||||
|
Creature* flightMaster = ObjectAccessor::GetCreature(*bot, fmGuid);
|
||||||
|
if (!flightMaster || !flightMaster->IsAlive())
|
||||||
|
{
|
||||||
|
state.route.clear();
|
||||||
|
state.stepIdx += 2;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
botAI->RemoveShapeshift();
|
||||||
|
if (bot->IsMounted())
|
||||||
|
bot->Dismount();
|
||||||
|
|
||||||
|
if (bot->ActivateTaxiPathTo(state.route, flightMaster, 0))
|
||||||
|
LOG_DEBUG("playerbots","[TravelPlan] Bot {} taking flight ({} nodes)", bot->GetName(), state.route.size());
|
||||||
|
|
||||||
|
state.route.clear();
|
||||||
|
state.stepIdx += 2;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case PathNodeType::NODE_TELEPORT:
|
||||||
|
{
|
||||||
|
// Teleport-spell node (e.g. mage portals). Not implemented
|
||||||
|
// — abort the plan instead of silently teleporting the
|
||||||
|
// bot. The plan executor regards this node as terminal.
|
||||||
|
state.Reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
case PathNodeType::NODE_FLYING_MOUNT:
|
||||||
|
{
|
||||||
|
// Flying-mount node not implemented — abort. cmangos
|
||||||
|
// produces these nodes during graph generation but their
|
||||||
|
// execution is server-specific; we treat them as
|
||||||
|
// unreachable rather than papering over with a teleport.
|
||||||
|
state.Reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
LOG_ERROR("playerbots",
|
||||||
|
"[TravelPlan] Bot {} encountered unknown PathNodeType ({}); resetting plan",
|
||||||
|
bot->GetName(), static_cast<uint32>(pt.type));
|
||||||
|
state.Reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Transport* MovementAction::GetTransportForPosTolerant(Map* map, WorldObject* ref, uint32 phaseMask, float x, float y, float z)
|
||||||
|
{
|
||||||
|
if (!map || !ref)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
std::array<float, 4> const probes = { z, z + 0.5f, z + 1.5f, z - 0.5f };
|
||||||
|
for (float const pz : probes)
|
||||||
|
{
|
||||||
|
if (Transport* transport = map->GetTransportForPos(phaseMask, x, y, pz, ref))
|
||||||
|
return transport;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MovementAction::FindBoardingPointOnTransport(Map* map, Transport* expectedTransport, WorldObject* ref,
|
||||||
|
float refX, float refY, float refZ, float botX, float botY, float botZ, float& outX, float& outY, float& outZ)
|
||||||
|
{
|
||||||
|
if (!map || !expectedTransport || !ref)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
uint32 const phaseMask = ref->GetPhaseMask();
|
||||||
|
if (GetTransportForPosTolerant(map, ref, phaseMask, refX, refY, refZ)
|
||||||
|
!= expectedTransport)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
float const probeZ = std::max(refZ, botZ);
|
||||||
|
float const dx2 = botX - refX;
|
||||||
|
float const dy2 = botY - refY;
|
||||||
|
float const dist2d = std::sqrt(dx2 * dx2 + dy2 * dy2);
|
||||||
|
int32 const steps = std::clamp(static_cast<int32>(dist2d / 0.75f), 10, 28);
|
||||||
|
float const dx = (botX - refX) / static_cast<float>(steps);
|
||||||
|
float const dy = (botY - refY) / static_cast<float>(steps);
|
||||||
|
|
||||||
|
if (map->GetTransportForPos(phaseMask, refX, refY, probeZ, ref) != expectedTransport)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
float lastX = refX;
|
||||||
|
float lastY = refY;
|
||||||
|
bool found = false;
|
||||||
|
|
||||||
|
for (int32 i = 1; i <= steps; ++i)
|
||||||
|
{
|
||||||
|
float const px = refX + dx * i;
|
||||||
|
float const py = refY + dy * i;
|
||||||
|
Transport* const t = GetTransportForPosTolerant(map, ref, phaseMask, px, py, probeZ);
|
||||||
|
if (t != expectedTransport)
|
||||||
|
break;
|
||||||
|
lastX = px;
|
||||||
|
lastY = py;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
outX = lastX;
|
||||||
|
outY = lastY;
|
||||||
|
outZ = refZ;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MovementAction::BoardTransport(Transport* transport)
|
||||||
|
{
|
||||||
|
if (!transport || transport->IsStaticTransport())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Map* map = bot->GetMap();
|
||||||
|
if (!map)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Already on this transport
|
||||||
|
if (bot->GetTransport() == transport)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Check if bot is on the transport surface
|
||||||
|
float probeZ = std::max(bot->GetPositionZ(), transport->GetPositionZ());
|
||||||
|
Transport* surface = GetTransportForPosTolerant(map, bot, bot->GetPhaseMask(), bot->GetPositionX(),
|
||||||
|
bot->GetPositionY(), probeZ);
|
||||||
|
|
||||||
|
if (surface == transport)
|
||||||
|
{
|
||||||
|
transport->AddPassenger(bot, true);
|
||||||
|
bot->StopMovingOnCurrentPos();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Not on surface — move toward the transport
|
||||||
|
float destX = transport->GetPositionX();
|
||||||
|
float destY = transport->GetPositionY();
|
||||||
|
float destZ = transport->GetPositionZ();
|
||||||
|
|
||||||
|
// Try to find nearest boarding edge
|
||||||
|
float edgeX, edgeY, edgeZ;
|
||||||
|
if (FindBoardingPointOnTransport(map, transport, transport, transport->GetPositionX(), transport->GetPositionY(),
|
||||||
|
transport->GetPositionZ(), bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), edgeX, edgeY, edgeZ))
|
||||||
|
{
|
||||||
|
destX = edgeX;
|
||||||
|
destY = edgeY;
|
||||||
|
destZ = edgeZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MovePoint without pathfinding (transport is a moving object)
|
||||||
|
if (MotionMaster* mm = bot->GetMotionMaster())
|
||||||
|
{
|
||||||
|
if (bot->IsSitState())
|
||||||
|
bot->SetStandState(UNIT_STAND_STATE_STAND);
|
||||||
|
|
||||||
|
mm->MovePoint(0, destX, destY, destZ, FORCED_MOVEMENT_NONE, 0.0f, 0.0f, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include "Action.h"
|
#include "Action.h"
|
||||||
#include "LastMovementValue.h"
|
#include "LastMovementValue.h"
|
||||||
|
#include "PathGenerator.h"
|
||||||
#include "PlayerbotAIConfig.h"
|
#include "PlayerbotAIConfig.h"
|
||||||
|
|
||||||
class Player;
|
class Player;
|
||||||
@ -22,6 +23,19 @@ class Position;
|
|||||||
#define ANGLE_90_DEG M_PI_2
|
#define ANGLE_90_DEG M_PI_2
|
||||||
#define ANGLE_120_DEG (2.f * static_cast<float>(M_PI) / 3.f)
|
#define ANGLE_120_DEG (2.f * static_cast<float>(M_PI) / 3.f)
|
||||||
|
|
||||||
|
// Default acceptable path types for GeneratePath
|
||||||
|
constexpr uint32 DEFAULT_PATH_ACCEPT_MASK = PATHFIND_NORMAL | PATHFIND_INCOMPLETE;
|
||||||
|
constexpr uint32 RELAXED_PATH_ACCEPT_MASK = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY;
|
||||||
|
|
||||||
|
struct PathResult
|
||||||
|
{
|
||||||
|
Movement::PointsArray points;
|
||||||
|
G3D::Vector3 actualEnd;
|
||||||
|
G3D::Vector3 end;
|
||||||
|
PathType pathType;
|
||||||
|
bool reachable;
|
||||||
|
};
|
||||||
|
|
||||||
class MovementAction : public Action
|
class MovementAction : public Action
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@ -66,6 +80,25 @@ protected:
|
|||||||
bool FleePosition(Position pos, float radius, uint32 minInterval = 1000);
|
bool FleePosition(Position pos, float radius, uint32 minInterval = 1000);
|
||||||
bool CheckLastFlee(float curAngle, std::list<FleeInfo>& infoList);
|
bool CheckLastFlee(float curAngle, std::list<FleeInfo>& infoList);
|
||||||
|
|
||||||
|
PathResult GeneratePath(float x, float y, float z, uint32 acceptMask = DEFAULT_PATH_ACCEPT_MASK, bool forceDestination = true);
|
||||||
|
|
||||||
|
bool GetTravelPlan(TravelPlan& plan, WorldPosition destination);
|
||||||
|
bool ExecuteTravelPlan(TravelPlan& state);
|
||||||
|
|
||||||
|
// Transport boarding helpers (shared by FollowAction and travel plan)
|
||||||
|
static Transport* GetTransportForPosTolerant(Map* map, WorldObject* ref,
|
||||||
|
uint32 phaseMask, float x, float y, float z);
|
||||||
|
static bool FindBoardingPointOnTransport(Map* map, Transport* transport,
|
||||||
|
WorldObject* ref, float refX, float refY, float refZ,
|
||||||
|
float botX, float botY, float botZ,
|
||||||
|
float& outX, float& outY, float& outZ);
|
||||||
|
bool BoardTransport(Transport* transport);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool LaunchWalkSpline(TravelPlan& state);
|
||||||
|
bool CheckSplineProgress(TravelPlan& state);
|
||||||
|
bool MoveToSpline(TravelPlan& state, WorldPosition target);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
struct CheckAngle
|
struct CheckAngle
|
||||||
{
|
{
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
|
||||||
|
#include "AreaDefines.h"
|
||||||
#include "BroadcastHelper.h"
|
#include "BroadcastHelper.h"
|
||||||
#include "ChatHelper.h"
|
#include "ChatHelper.h"
|
||||||
#include "G3D/Vector2.h"
|
#include "G3D/Vector2.h"
|
||||||
@ -119,17 +120,8 @@ bool NewRpgStatusUpdateAction::Execute(Event /*event*/)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case RPG_TRAVEL_FLIGHT:
|
// RPG_TRAVEL_FLIGHT arrival is handled inside NewRpgTravelFlightAction
|
||||||
{
|
// so the flight action owns both take-off and landing transitions.
|
||||||
auto& data = std::get<NewRpgInfo::TravelFlight>(info.data);
|
|
||||||
if (data.inFlight && !bot->IsInFlight())
|
|
||||||
{
|
|
||||||
// flight arrival
|
|
||||||
info.ChangeToIdle();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case RPG_REST:
|
case RPG_REST:
|
||||||
{
|
{
|
||||||
// REST -> IDLE
|
// REST -> IDLE
|
||||||
@ -463,31 +455,42 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
auto& data = *dataPtr;
|
auto& data = *dataPtr;
|
||||||
|
|
||||||
|
// Arrival: we had boarded a flight (data.inFlight) and we're no longer in
|
||||||
|
// it → we just landed. Special-case Rut'theran: walk to the portal GO so
|
||||||
|
// it teleports the bot into Darnassus, flipping the zone to AREA_DARNASSUS
|
||||||
|
// so this branch falls through to ChangeToIdle on the next tick.
|
||||||
|
if (data.inFlight && !bot->IsInFlight())
|
||||||
|
{
|
||||||
|
if (bot->GetZoneId() == AREA_TELDRASSIL)
|
||||||
|
{
|
||||||
|
static WorldPosition const rutTheranPortalEntrance(1, 8799.41f, 969.787f, 26.2409f, 0.0f);
|
||||||
|
return MoveFarTo(rutTheranPortalEntrance);
|
||||||
|
}
|
||||||
|
info.ChangeToIdle();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (bot->IsInFlight())
|
if (bot->IsInFlight())
|
||||||
{
|
{
|
||||||
data.inFlight = true;
|
data.inFlight = true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Creature* flightMaster = ObjectAccessor::GetCreature(*bot, data.fromFlightMaster);
|
|
||||||
|
if (bot->GetDistance(data.flightMasterPos) > INTERACTION_DISTANCE)
|
||||||
|
return MoveFarTo(data.flightMasterPos);
|
||||||
|
|
||||||
|
Creature* flightMaster = bot->FindNearestCreature(data.flightMasterEntry, INTERACTION_DISTANCE * 3);
|
||||||
if (!flightMaster || !flightMaster->IsAlive())
|
if (!flightMaster || !flightMaster->IsAlive())
|
||||||
{
|
{
|
||||||
botAI->rpgInfo.ChangeToIdle();
|
info.ChangeToIdle();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (bot->GetDistance(flightMaster) > INTERACTION_DISTANCE)
|
|
||||||
return MoveFarTo(flightMaster);
|
|
||||||
|
|
||||||
std::vector<uint32> nodes = data.path;
|
if (!TakeFlight(data.path, flightMaster))
|
||||||
|
|
||||||
botAI->RemoveShapeshift();
|
|
||||||
if (bot->IsMounted())
|
|
||||||
bot->Dismount();
|
|
||||||
|
|
||||||
if (!bot->ActivateTaxiPathTo(nodes, flightMaster, 0))
|
|
||||||
{
|
{
|
||||||
LOG_DEBUG("playerbots", "[New RPG] {} active taxi path {} (from {} to {}) failed", bot->GetName(),
|
info.ChangeToIdle();
|
||||||
flightMaster->GetEntry(), nodes[0], nodes[nodes.size() - 1]);
|
return true;
|
||||||
botAI->rpgInfo.ChangeToIdle();
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,11 +47,11 @@ public:
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
// static NewRpgStatusTransitionProb transitionMat;
|
// static NewRpgStatusTransitionProb transitionMat;
|
||||||
const int32 statusWanderNpcDuration = 5 * MINUTE * IN_MILLISECONDS ;
|
const int32 statusWanderNpcDuration = 5 * MINUTE * IN_MILLISECONDS;
|
||||||
const int32 statusWanderRandomDuration = 5 * MINUTE * IN_MILLISECONDS ;
|
const int32 statusWanderRandomDuration = 5 * MINUTE * IN_MILLISECONDS;
|
||||||
const int32 statusRestDuration = 30 * IN_MILLISECONDS ;
|
const int32 statusRestDuration = 30 * IN_MILLISECONDS;
|
||||||
const int32 statusDoQuestDuration = 30 * MINUTE * IN_MILLISECONDS ;
|
const int32 statusDoQuestDuration = 30 * MINUTE * IN_MILLISECONDS;
|
||||||
const int32 statusOutDoorPvPDuration = HOUR * IN_MILLISECONDS ;
|
const int32 statusOutDoorPvPDuration = HOUR * IN_MILLISECONDS;
|
||||||
};
|
};
|
||||||
|
|
||||||
class NewRpgGoGrindAction : public NewRpgBaseAction
|
class NewRpgGoGrindAction : public NewRpgBaseAction
|
||||||
|
|||||||
@ -96,25 +96,35 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
botAI->rpgInfo.stuckAttempts = 0;
|
botAI->rpgInfo.stuckAttempts = 0;
|
||||||
const AreaTableEntry* entry = sAreaTableStore.LookupEntry(bot->GetZoneId());
|
const AreaTableEntry* entry = sAreaTableStore.LookupEntry(bot->GetZoneId());
|
||||||
std::string zone_name = PlayerbotAI::GetLocalizedAreaName(entry);
|
std::string zone_name = PlayerbotAI::GetLocalizedAreaName(entry);
|
||||||
LOG_DEBUG(
|
LOG_DEBUG("playerbots","[New RPG] Teleport {} from ({},{},{},{}) to ({},{},{},{}) as it stuck when moving far - Zone: {} ({})",
|
||||||
"playerbots",
|
|
||||||
"[New RPG] Teleport {} from ({},{},{},{}) to ({},{},{},{}) as it stuck when moving far - Zone: {} ({})",
|
|
||||||
bot->GetName(), bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetMapId(),
|
bot->GetName(), bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetMapId(),
|
||||||
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), dest.GetMapId(), bot->GetZoneId(),
|
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), dest.GetMapId(), bot->GetZoneId(), zone_name);
|
||||||
zone_name);
|
botAI->TeleportTo(dest);
|
||||||
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
|
return true;
|
||||||
return bot->TeleportTo(dest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
float dis = bot->GetExactDist(dest);
|
float dis = bot->GetExactDist(dest);
|
||||||
|
|
||||||
|
// Long distance + travel nodes enabled: use the pre-computed node graph
|
||||||
|
// (A*, flight paths, transports) instead of repeated mmap hops.
|
||||||
|
if (dis > MAX_PATHFINDING_DISTANCE && sPlayerbotAIConfig.enableTravelNodes)
|
||||||
|
{
|
||||||
|
if (!botAI->rpgInfo.HasActiveTravelPlan())
|
||||||
|
StartTravelPlan(dest);
|
||||||
|
|
||||||
|
return UpdateTravelPlan();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crossed below the travel-node threshold — clear any leftover plan
|
||||||
|
if (botAI->rpgInfo.HasActiveTravelPlan())
|
||||||
|
botAI->rpgInfo.ClearTravel();
|
||||||
|
|
||||||
|
// Short range: close enough for a single mmap call
|
||||||
if (dis < pathFinderDis)
|
if (dis < pathFinderDis)
|
||||||
{
|
{
|
||||||
return MoveTo(dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), false, false,
|
return MoveTo(dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), false, false,
|
||||||
false, true);
|
false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint32 typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY;
|
|
||||||
|
|
||||||
// Primary strategy: ask mmap for a route to the TRUE destination.
|
// Primary strategy: ask mmap for a route to the TRUE destination.
|
||||||
// If mmap can reach it directly (PATHFIND_NORMAL) or partially
|
// If mmap can reach it directly (PATHFIND_NORMAL) or partially
|
||||||
// (PATHFIND_INCOMPLETE — destinations beyond the smooth-path cap
|
// (PATHFIND_INCOMPLETE — destinations beyond the smooth-path cap
|
||||||
@ -126,23 +136,18 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
// subsequent ticks early-out via IsWaitingForLastMove and no
|
// subsequent ticks early-out via IsWaitingForLastMove and no
|
||||||
// further PathGenerator calls fire until the bot arrives.
|
// further PathGenerator calls fire until the bot arrives.
|
||||||
{
|
{
|
||||||
PathGenerator path(bot);
|
PathResult path = GeneratePath(dest.GetPositionX(), dest.GetPositionY(),
|
||||||
path.CalculatePath(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
dest.GetPositionZ(), RELAXED_PATH_ACCEPT_MASK);
|
||||||
PathType type = path.GetPathType();
|
if (path.reachable)
|
||||||
bool canReach = !(type & (~typeOk));
|
|
||||||
if (canReach)
|
|
||||||
{
|
{
|
||||||
const G3D::Vector3& endPos = path.GetActualEndPosition();
|
|
||||||
// Only commit if the mmap endpoint actually makes progress
|
// Only commit if the mmap endpoint actually makes progress
|
||||||
// toward the destination. For pathological INCOMPLETE
|
// toward the destination. For pathological INCOMPLETE
|
||||||
// results (e.g. disconnected polys that still report
|
// results (e.g. disconnected polys that still report
|
||||||
// INCOMPLETE) the endpoint can land right under the bot;
|
// INCOMPLETE) the endpoint can land right under the bot;
|
||||||
// fall through to cone sampling in that case.
|
// fall through to cone sampling in that case.
|
||||||
float endDistToDest = dest.GetExactDist(endPos.x, endPos.y, endPos.z);
|
float endDistToDest = dest.GetExactDist(path.actualEnd.x, path.actualEnd.y, path.actualEnd.z);
|
||||||
if (endDistToDest + 5.0f < disToDest)
|
if (endDistToDest + 5.0f < disToDest)
|
||||||
{
|
return MoveTo(bot->GetMapId(), path.actualEnd.x, path.actualEnd.y, path.actualEnd.z, false, false, false, true);
|
||||||
return MoveTo(bot->GetMapId(), endPos.x, endPos.y, endPos.z, false, false, false, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,18 +171,14 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
float dx = x + cos(angle) * sampleDis;
|
float dx = x + cos(angle) * sampleDis;
|
||||||
float dy = y + sin(angle) * sampleDis;
|
float dy = y + sin(angle) * sampleDis;
|
||||||
float dz = z + 0.5f;
|
float dz = z + 0.5f;
|
||||||
PathGenerator path(bot);
|
PathResult path = GeneratePath(dx, dy, dz, RELAXED_PATH_ACCEPT_MASK);
|
||||||
path.CalculatePath(dx, dy, dz);
|
|
||||||
PathType type = path.GetPathType();
|
|
||||||
bool canReach = !(type & (~typeOk));
|
|
||||||
|
|
||||||
if (canReach && fabs(delta) <= minDelta)
|
if (path.reachable && fabs(delta) <= minDelta)
|
||||||
{
|
{
|
||||||
found = true;
|
found = true;
|
||||||
const G3D::Vector3& endPos = path.GetActualEndPosition();
|
rx = path.actualEnd.x;
|
||||||
rx = endPos.x;
|
ry = path.actualEnd.y;
|
||||||
ry = endPos.y;
|
rz = path.actualEnd.z;
|
||||||
rz = endPos.z;
|
|
||||||
minDelta = fabs(delta);
|
minDelta = fabs(delta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -188,12 +189,31 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NewRpgBaseAction::StartTravelPlan(WorldPosition dest)
|
||||||
|
{
|
||||||
|
TravelPlan& plan = botAI->rpgInfo.travelPlan;
|
||||||
|
GetTravelPlan(plan, dest);
|
||||||
|
|
||||||
|
LOG_DEBUG("playerbots","[New RPG] Bot {} starting travel plan to ({:.0f},{:.0f},{:.0f}) map={}, {} points",
|
||||||
|
bot->GetName(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), dest.GetMapId(), plan.steps.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NewRpgBaseAction::UpdateTravelPlan()
|
||||||
|
{
|
||||||
|
TravelPlan& plan = botAI->rpgInfo.travelPlan;
|
||||||
|
|
||||||
|
bool result = ExecuteTravelPlan(plan);
|
||||||
|
|
||||||
|
if (!plan.IsActive())
|
||||||
|
botAI->rpgInfo.ClearTravel();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance)
|
bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance)
|
||||||
{
|
{
|
||||||
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL))
|
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL))
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
WorldObject* object = botAI->GetWorldObject(guid);
|
WorldObject* object = botAI->GetWorldObject(guid);
|
||||||
if (!object)
|
if (!object)
|
||||||
@ -245,13 +265,9 @@ bool NewRpgBaseAction::MoveRandomNear(float moveStep, MovementPriority priority,
|
|||||||
float dy = y + distance * sin(angle);
|
float dy = y + distance * sin(angle);
|
||||||
float dz = z;
|
float dz = z;
|
||||||
|
|
||||||
PathGenerator path(bot);
|
PathResult path = GeneratePath(dx, dy, dz, RELAXED_PATH_ACCEPT_MASK);
|
||||||
path.CalculatePath(dx, dy, dz);
|
|
||||||
PathType type = path.GetPathType();
|
|
||||||
uint32 typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY;
|
|
||||||
bool canReach = !(type & (~typeOk));
|
|
||||||
|
|
||||||
if (!canReach)
|
if (!path.reachable)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!map->CanReachPositionAndGetValidCoords(bot, dx, dy, dz))
|
if (!map->CanReachPositionAndGetValidCoords(bot, dx, dy, dz))
|
||||||
@ -276,6 +292,27 @@ bool NewRpgBaseAction::ForceToWait(uint32 duration, MovementPriority priority)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool NewRpgBaseAction::TakeFlight(std::vector<uint32> const& taxiNodes, Creature* flightMaster)
|
||||||
|
{
|
||||||
|
if (taxiNodes.size() < 2 || !flightMaster || !flightMaster->IsAlive())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
botAI->RemoveShapeshift();
|
||||||
|
if (bot->IsMounted())
|
||||||
|
bot->Dismount();
|
||||||
|
|
||||||
|
if (!bot->ActivateTaxiPathTo(taxiNodes, flightMaster, 0))
|
||||||
|
{
|
||||||
|
LOG_DEBUG("playerbots", "[New RPG] Bot {} flight ({} nodes, {} to {}) failed",
|
||||||
|
bot->GetName(), taxiNodes.size(), taxiNodes.front(), taxiNodes.back());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("playerbots", "[New RPG] Bot {} taking flight ({} nodes, {} to {})",
|
||||||
|
bot->GetName(), taxiNodes.size(), taxiNodes.front(), taxiNodes.back());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// @TODO: Fix redundant code
|
/// @TODO: Fix redundant code
|
||||||
/// Quest related method refer to TalkToQuestGiverAction.h
|
/// Quest related method refer to TalkToQuestGiverAction.h
|
||||||
bool NewRpgBaseAction::InteractWithNpcOrGameObjectForQuest(ObjectGuid guid)
|
bool NewRpgBaseAction::InteractWithNpcOrGameObjectForQuest(ObjectGuid guid)
|
||||||
@ -978,6 +1015,10 @@ WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot)
|
|||||||
uint32 idx = urand(0, lo_prepared_locs.size() - 1);
|
uint32 idx = urand(0, lo_prepared_locs.size() - 1);
|
||||||
dest = lo_prepared_locs[idx];
|
dest = lo_prepared_locs[idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!dest.IsValid())
|
||||||
|
return dest;
|
||||||
|
|
||||||
LOG_DEBUG("playerbots", "[New RPG] Bot {} select random grind pos Map:{} X:{} Y:{} Z:{} ({}+{} available in {})",
|
LOG_DEBUG("playerbots", "[New RPG] Bot {} select random grind pos Map:{} X:{} Y:{} Z:{} ({}+{} available in {})",
|
||||||
bot->GetName(), dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
|
bot->GetName(), dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
|
||||||
hi_prepared_locs.size(), lo_prepared_locs.size() - hi_prepared_locs.size(), locs.size());
|
hi_prepared_locs.size(), lo_prepared_locs.size() - hi_prepared_locs.size(), locs.size());
|
||||||
@ -1021,25 +1062,31 @@ WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot)
|
|||||||
uint32 idx = urand(0, prepared_locs.size() - 1);
|
uint32 idx = urand(0, prepared_locs.size() - 1);
|
||||||
dest = prepared_locs[idx];
|
dest = prepared_locs[idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!dest.IsValid())
|
||||||
|
return dest;
|
||||||
|
|
||||||
LOG_DEBUG("playerbots", "[New RPG] Bot {} select random inn keeper pos Map:{} X:{} Y:{} Z:{} ({} available in {})",
|
LOG_DEBUG("playerbots", "[New RPG] Bot {} select random inn keeper pos Map:{} X:{} Y:{} Z:{} ({} available in {})",
|
||||||
bot->GetName(), dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
|
bot->GetName(), dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
|
||||||
prepared_locs.size(), locs.size());
|
prepared_locs.size(), locs.size());
|
||||||
return dest;
|
return dest;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NewRpgBaseAction::SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector<uint32>& path)
|
bool NewRpgBaseAction::SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector<uint32>& path)
|
||||||
{
|
{
|
||||||
flightMaster = sTravelMgr.GetNearestFlightMasterGuid(bot);
|
TravelMgr::FlightMasterInfo const* info = sTravelMgr.GetNearestFlightMasterInfo(bot);
|
||||||
if (!flightMaster)
|
if (!info)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
std::vector<std::vector<uint32>> availablePaths = sTravelMgr.GetOptimalFlightDestinations(bot);
|
std::vector<std::vector<uint32>> availablePaths = sTravelMgr.GetOptimalFlightDestinations(bot);
|
||||||
if (availablePaths.empty())
|
if (availablePaths.empty())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
flightMasterEntry = info->templateEntry;
|
||||||
|
flightMasterPos = info->pos;
|
||||||
path = availablePaths[urand(0, availablePaths.size() - 1)];
|
path = availablePaths[urand(0, availablePaths.size() - 1)];
|
||||||
LOG_DEBUG("playerbots", "[New RPG] Bot {} select random flight taxi node from:{} (node {}) to:{} ({} available)",
|
LOG_DEBUG("playerbots", "[New RPG] Bot {} select random flight taxi node from:{} (node {}) to:{} ({} available)",
|
||||||
bot->GetName(), flightMaster.GetEntry(), path[0], path[path.size() - 1], availablePaths.size());
|
bot->GetName(), flightMasterEntry, path[0], path[path.size() - 1], availablePaths.size());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1058,7 +1105,6 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector<NewRpgStatus> candidateSta
|
|||||||
probSum += sPlayerbotAIConfig.RpgStatusProbWeight[status];
|
probSum += sPlayerbotAIConfig.RpgStatusProbWeight[status];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Safety check. Default to "rest" if all RPG weights = 0
|
|
||||||
if (availableStatus.empty() || probSum == 0)
|
if (availableStatus.empty() || probSum == 0)
|
||||||
{
|
{
|
||||||
botAI->rpgInfo.ChangeToRest();
|
botAI->rpgInfo.ChangeToRest();
|
||||||
@ -1139,11 +1185,12 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector<NewRpgStatus> candidateSta
|
|||||||
}
|
}
|
||||||
case RPG_TRAVEL_FLIGHT:
|
case RPG_TRAVEL_FLIGHT:
|
||||||
{
|
{
|
||||||
ObjectGuid flightMaster;
|
uint32 flightMasterEntry = 0;
|
||||||
|
WorldPosition flightMasterPos;
|
||||||
std::vector<uint32> path;
|
std::vector<uint32> path;
|
||||||
if (SelectRandomFlightTaxiNode(flightMaster, path))
|
if (SelectRandomFlightTaxiNode(flightMasterEntry, flightMasterPos, path))
|
||||||
{
|
{
|
||||||
botAI->rpgInfo.ChangeToTravelFlight(flightMaster, path);
|
botAI->rpgInfo.ChangeToTravelFlight(flightMasterEntry, flightMasterPos, path);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -1220,9 +1267,10 @@ bool NewRpgBaseAction::CheckRpgStatusAvailable(NewRpgStatus status)
|
|||||||
}
|
}
|
||||||
case RPG_TRAVEL_FLIGHT:
|
case RPG_TRAVEL_FLIGHT:
|
||||||
{
|
{
|
||||||
ObjectGuid flightMaster;
|
uint32 flightMasterEntry = 0;
|
||||||
|
WorldPosition flightMasterPos;
|
||||||
std::vector<uint32> path;
|
std::vector<uint32> path;
|
||||||
return SelectRandomFlightTaxiNode(flightMaster, path);
|
return SelectRandomFlightTaxiNode(flightMasterEntry, flightMasterPos, path);
|
||||||
}
|
}
|
||||||
case RPG_OUTDOOR_PVP:
|
case RPG_OUTDOOR_PVP:
|
||||||
{
|
{
|
||||||
|
|||||||
@ -33,6 +33,7 @@ protected:
|
|||||||
bool MoveWorldObjectTo(ObjectGuid guid, float distance = INTERACTION_DISTANCE);
|
bool MoveWorldObjectTo(ObjectGuid guid, float distance = INTERACTION_DISTANCE);
|
||||||
bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, WorldObject* center = nullptr);
|
bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, WorldObject* center = nullptr);
|
||||||
bool ForceToWait(uint32 duration, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
|
bool ForceToWait(uint32 duration, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
|
||||||
|
bool TakeFlight(std::vector<uint32> const& taxiNodes, Creature* flightMaster);
|
||||||
|
|
||||||
/* QUEST RELATED CHECK */
|
/* QUEST RELATED CHECK */
|
||||||
ObjectGuid ChooseNpcOrGameObjectToInteract(bool questgiverOnly = false, float distanceLimit = 0.0f);
|
ObjectGuid ChooseNpcOrGameObjectToInteract(bool questgiverOnly = false, float distanceLimit = 0.0f);
|
||||||
@ -54,7 +55,7 @@ protected:
|
|||||||
bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo>& poiInfo, bool toComplete = false);
|
bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo>& poiInfo, bool toComplete = false);
|
||||||
static WorldPosition SelectRandomGrindPos(Player* bot);
|
static WorldPosition SelectRandomGrindPos(Player* bot);
|
||||||
static WorldPosition SelectRandomCampPos(Player* bot);
|
static WorldPosition SelectRandomCampPos(Player* bot);
|
||||||
bool SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector<uint32>& path);
|
bool SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector<uint32>& path);
|
||||||
bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
|
bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
|
||||||
bool CheckRpgStatusAvailable(NewRpgStatus status);
|
bool CheckRpgStatusAvailable(NewRpgStatus status);
|
||||||
|
|
||||||
@ -69,6 +70,10 @@ protected:
|
|||||||
// the teleport fires, but long enough that a genuine long
|
// the teleport fires, but long enough that a genuine long
|
||||||
// walk that is slowly making progress never triggers it.
|
// walk that is slowly making progress never triggers it.
|
||||||
const uint32 stuckTime = 90 * 1000;
|
const uint32 stuckTime = 90 * 1000;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void StartTravelPlan(WorldPosition dest);
|
||||||
|
bool UpdateTravelPlan();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -6,42 +6,43 @@
|
|||||||
|
|
||||||
void NewRpgInfo::ChangeToGoGrind(WorldPosition pos)
|
void NewRpgInfo::ChangeToGoGrind(WorldPosition pos)
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
data = GoGrind{pos};
|
data = GoGrind{pos};
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewRpgInfo::ChangeToGoCamp(WorldPosition pos)
|
void NewRpgInfo::ChangeToGoCamp(WorldPosition pos)
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
data = GoCamp{pos};
|
data = GoCamp{pos};
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewRpgInfo::ChangeToWanderNpc()
|
void NewRpgInfo::ChangeToWanderNpc()
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
data = WanderNpc{};
|
data = WanderNpc{};
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewRpgInfo::ChangeToWanderRandom()
|
void NewRpgInfo::ChangeToWanderRandom()
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
data = WanderRandom{};
|
data = WanderRandom{};
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest)
|
void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest)
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
DoQuest do_quest;
|
DoQuest do_quest;
|
||||||
do_quest.questId = questId;
|
do_quest.questId = questId;
|
||||||
do_quest.quest = quest;
|
do_quest.quest = quest;
|
||||||
data = do_quest;
|
data = do_quest;
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector<uint32> path)
|
void NewRpgInfo::ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector<uint32> path)
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
TravelFlight flight;
|
TravelFlight flight;
|
||||||
flight.fromFlightMaster = fromFlightMaster;
|
flight.flightMasterEntry = flightMasterEntry;
|
||||||
|
flight.flightMasterPos = flightMasterPos;
|
||||||
flight.path = std::move(path);
|
flight.path = std::move(path);
|
||||||
flight.inFlight = false;
|
flight.inFlight = false;
|
||||||
data = flight;
|
data = flight;
|
||||||
@ -57,13 +58,13 @@ void NewRpgInfo::ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId)
|
|||||||
|
|
||||||
void NewRpgInfo::ChangeToRest()
|
void NewRpgInfo::ChangeToRest()
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
data = Rest{};
|
data = Rest{};
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewRpgInfo::ChangeToIdle()
|
void NewRpgInfo::ChangeToIdle()
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
data = Idle{};
|
data = Idle{};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,6 +77,7 @@ void NewRpgInfo::Reset()
|
|||||||
{
|
{
|
||||||
data = Idle{};
|
data = Idle{};
|
||||||
startT = getMSTime();
|
startT = getMSTime();
|
||||||
|
ClearTravel();
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewRpgInfo::SetMoveFarTo(WorldPosition pos)
|
void NewRpgInfo::SetMoveFarTo(WorldPosition pos)
|
||||||
@ -157,7 +159,7 @@ std::string NewRpgInfo::ToString()
|
|||||||
else if constexpr (std::is_same_v<T, TravelFlight>)
|
else if constexpr (std::is_same_v<T, TravelFlight>)
|
||||||
{
|
{
|
||||||
out << "TRAVEL_FLIGHT";
|
out << "TRAVEL_FLIGHT";
|
||||||
out << "\nfromFlightMaster: " << arg.fromFlightMaster.GetEntry();
|
out << "\nflightMasterEntry: " << arg.flightMasterEntry;
|
||||||
out << "\nfromNode: " << arg.path[0];
|
out << "\nfromNode: " << arg.path[0];
|
||||||
out << "\ntoNode: " << arg.path[arg.path.size() - 1];
|
out << "\ntoNode: " << arg.path[arg.path.size() - 1];
|
||||||
out << "\ninFlight: " << arg.inFlight;
|
out << "\ninFlight: " << arg.inFlight;
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
#include "Strategy.h"
|
#include "Strategy.h"
|
||||||
#include "Timer.h"
|
#include "Timer.h"
|
||||||
#include "TravelMgr.h"
|
#include "TravelMgr.h"
|
||||||
|
#include "TravelNode.h"
|
||||||
|
|
||||||
using NewRpgStatusTransitionProb = std::vector<std::vector<int>>;
|
using NewRpgStatusTransitionProb = std::vector<std::vector<int>>;
|
||||||
|
|
||||||
@ -49,7 +50,8 @@ struct NewRpgInfo
|
|||||||
// RPG_TRAVEL_FLIGHT
|
// RPG_TRAVEL_FLIGHT
|
||||||
struct TravelFlight
|
struct TravelFlight
|
||||||
{
|
{
|
||||||
ObjectGuid fromFlightMaster{};
|
uint32 flightMasterEntry{0};
|
||||||
|
WorldPosition flightMasterPos{};
|
||||||
std::vector<uint32> path;
|
std::vector<uint32> path;
|
||||||
bool inFlight{false};
|
bool inFlight{false};
|
||||||
};
|
};
|
||||||
@ -74,7 +76,10 @@ struct NewRpgInfo
|
|||||||
uint32 stuckTs{0};
|
uint32 stuckTs{0};
|
||||||
uint32 stuckAttempts{0};
|
uint32 stuckAttempts{0};
|
||||||
WorldPosition moveFarPos;
|
WorldPosition moveFarPos;
|
||||||
// END MOVE_FAR
|
// Travel Node System
|
||||||
|
TravelPlan travelPlan;
|
||||||
|
bool HasActiveTravelPlan() const { return travelPlan.IsActive(); }
|
||||||
|
void ClearTravel() { travelPlan.Reset(); }
|
||||||
|
|
||||||
using RpgData = std::variant<
|
using RpgData = std::variant<
|
||||||
Idle,
|
Idle,
|
||||||
@ -96,7 +101,7 @@ struct NewRpgInfo
|
|||||||
void ChangeToWanderNpc();
|
void ChangeToWanderNpc();
|
||||||
void ChangeToWanderRandom();
|
void ChangeToWanderRandom();
|
||||||
void ChangeToDoQuest(uint32 questId, const Quest* quest);
|
void ChangeToDoQuest(uint32 questId, const Quest* quest);
|
||||||
void ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector<uint32> path);
|
void ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector<uint32> path);
|
||||||
void ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId = 0);
|
void ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId = 0);
|
||||||
void ChangeToRest();
|
void ChangeToRest();
|
||||||
void ChangeToIdle();
|
void ChangeToIdle();
|
||||||
|
|||||||
@ -762,6 +762,21 @@ void PlayerbotAI::HandleCommand(uint32 type, const std::string& text, Player& fr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlayerbotAI::TeleportTo(WorldLocation loc, bool resetAI)
|
||||||
|
{
|
||||||
|
if (!bot || bot->IsBeingTeleported() || !bot->IsInWorld())
|
||||||
|
return;
|
||||||
|
|
||||||
|
bot->GetMotionMaster()->Clear();
|
||||||
|
if (resetAI)
|
||||||
|
Reset(true);
|
||||||
|
else
|
||||||
|
InterruptSpell();
|
||||||
|
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
|
||||||
|
bot->TeleportTo(loc.GetMapId(), loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ(), 0);
|
||||||
|
bot->SendMovementFlagUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
void PlayerbotAI::HandleTeleportAck()
|
void PlayerbotAI::HandleTeleportAck()
|
||||||
{
|
{
|
||||||
if (!bot || !bot->GetSession())
|
if (!bot || !bot->GetSession())
|
||||||
|
|||||||
@ -396,6 +396,7 @@ public:
|
|||||||
void HandleMasterIncomingPacket(WorldPacket const& packet);
|
void HandleMasterIncomingPacket(WorldPacket const& packet);
|
||||||
void HandleMasterOutgoingPacket(WorldPacket const& packet);
|
void HandleMasterOutgoingPacket(WorldPacket const& packet);
|
||||||
void HandleTeleportAck();
|
void HandleTeleportAck();
|
||||||
|
void TeleportTo(WorldLocation loc, bool resetAI = false);
|
||||||
void ChangeEngine(BotState type);
|
void ChangeEngine(BotState type);
|
||||||
void ChangeEngineOnCombat();
|
void ChangeEngineOnCombat();
|
||||||
void ChangeEngineOnNonCombat();
|
void ChangeEngineOnNonCombat();
|
||||||
|
|||||||
@ -1696,14 +1696,7 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector<WorldLocation>&
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
bot->GetMotionMaster()->Clear();
|
botAI->TeleportTo(WorldLocation(loc.GetMapId(), x, y, z, 0), true);
|
||||||
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
|
||||||
if (botAI)
|
|
||||||
botAI->Reset(true);
|
|
||||||
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
|
|
||||||
bot->TeleportTo(loc.GetMapId(), x, y, z, 0);
|
|
||||||
bot->SendMovementFlagUpdate();
|
|
||||||
|
|
||||||
if (pmo)
|
if (pmo)
|
||||||
pmo->finish();
|
pmo->finish();
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
|
|
||||||
|
#include "AreaDefines.h"
|
||||||
#include "Creature.h"
|
#include "Creature.h"
|
||||||
#include "Log.h"
|
#include "Log.h"
|
||||||
#include "ObjectAccessor.h"
|
#include "ObjectAccessor.h"
|
||||||
@ -28,67 +29,60 @@
|
|||||||
|
|
||||||
// Navigation data
|
// Navigation data
|
||||||
|
|
||||||
enum class CityId : uint8
|
struct Capital
|
||||||
{
|
{
|
||||||
STORMWIND,
|
uint32 zoneId;
|
||||||
IRONFORGE,
|
TeamId team;
|
||||||
DARNASSUS,
|
char const* name;
|
||||||
EXODAR,
|
std::vector<uint16> bankers;
|
||||||
ORGRIMMAR,
|
|
||||||
UNDERCITY,
|
|
||||||
THUNDER_BLUFF,
|
|
||||||
SILVERMOON_CITY,
|
|
||||||
SHATTRATH_CITY,
|
|
||||||
DALARAN
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static const std::unordered_map<uint16, std::pair<CityId, TeamId>> bankerToCity = {
|
static const std::vector<Capital> capitals = {
|
||||||
{2455, {CityId::STORMWIND, TEAM_ALLIANCE}}, {2456, {CityId::STORMWIND, TEAM_ALLIANCE}}, {2457, {CityId::STORMWIND, TEAM_ALLIANCE}},
|
{ AREA_STORMWIND_CITY, TEAM_ALLIANCE, "Stormwind", {2455, 2456, 2457} },
|
||||||
{2460, {CityId::IRONFORGE, TEAM_ALLIANCE}}, {2461, {CityId::IRONFORGE, TEAM_ALLIANCE}}, {5099, {CityId::IRONFORGE, TEAM_ALLIANCE}},
|
{ AREA_IRONFORGE, TEAM_ALLIANCE, "Ironforge", {2460, 2461, 5099} },
|
||||||
{4155, {CityId::DARNASSUS, TEAM_ALLIANCE}}, {4208, {CityId::DARNASSUS, TEAM_ALLIANCE}}, {4209, {CityId::DARNASSUS, TEAM_ALLIANCE}},
|
{ AREA_DARNASSUS, TEAM_ALLIANCE, "Darnassus", {4155, 4208, 4209} },
|
||||||
{17773, {CityId::EXODAR, TEAM_ALLIANCE}}, {18350, {CityId::EXODAR, TEAM_ALLIANCE}}, {16710, {CityId::EXODAR, TEAM_ALLIANCE}},
|
{ AREA_THE_EXODAR, TEAM_ALLIANCE, "Exodar", {17773, 18350, 16710} },
|
||||||
{3320, {CityId::ORGRIMMAR, TEAM_HORDE}}, {3309, {CityId::ORGRIMMAR, TEAM_HORDE}}, {3318, {CityId::ORGRIMMAR, TEAM_HORDE}},
|
{ AREA_ORGRIMMAR, TEAM_HORDE, "Orgrimmar", {3320, 3309, 3318} },
|
||||||
{4549, {CityId::UNDERCITY, TEAM_HORDE}}, {2459, {CityId::UNDERCITY, TEAM_HORDE}}, {2458, {CityId::UNDERCITY, TEAM_HORDE}}, {4550, {CityId::UNDERCITY, TEAM_HORDE}},
|
{ AREA_UNDERCITY, TEAM_HORDE, "Undercity", {4549, 2459, 2458, 4550} },
|
||||||
{2996, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, {8356, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, {8357, {CityId::THUNDER_BLUFF, TEAM_HORDE}},
|
{ AREA_THUNDER_BLUFF, TEAM_HORDE, "Thunder Bluff", {2996, 8356, 8357} },
|
||||||
{17631, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {17632, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {17633, {CityId::SILVERMOON_CITY, TEAM_HORDE}},
|
{ AREA_SILVERMOON_CITY, TEAM_HORDE, "Silvermoon", {17631, 17632, 17633, 16615, 16616, 16617} },
|
||||||
{16615, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {16616, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {16617, {CityId::SILVERMOON_CITY, TEAM_HORDE}},
|
{ AREA_SHATTRATH_CITY, TEAM_NEUTRAL, "Shattrath", {19246, 19338, 19034, 19318} },
|
||||||
{19246, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, {19338, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}},
|
{ AREA_DALARAN, TEAM_NEUTRAL, "Dalaran", {30604, 30605, 30607, 28675, 28676, 28677, 29530} }
|
||||||
{19034, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, {19318, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}},
|
|
||||||
{30604, {CityId::DALARAN, TEAM_NEUTRAL}}, {30605, {CityId::DALARAN, TEAM_NEUTRAL}}, {30607, {CityId::DALARAN, TEAM_NEUTRAL}},
|
|
||||||
{28675, {CityId::DALARAN, TEAM_NEUTRAL}}, {28676, {CityId::DALARAN, TEAM_NEUTRAL}}, {28677, {CityId::DALARAN, TEAM_NEUTRAL}}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static const std::unordered_map<CityId, std::vector<uint16>> cityToBankers = {
|
static Capital const* FindCapitalByZone(uint32 zoneId)
|
||||||
{CityId::STORMWIND, {2455, 2456, 2457}},
|
|
||||||
{CityId::IRONFORGE, {2460, 2461, 5099}},
|
|
||||||
{CityId::DARNASSUS, {4155, 4208, 4209}},
|
|
||||||
{CityId::EXODAR, {17773, 18350, 16710}},
|
|
||||||
{CityId::ORGRIMMAR, {3320, 3309, 3318}},
|
|
||||||
{CityId::UNDERCITY, {4549, 2459, 2458, 4550}},
|
|
||||||
{CityId::THUNDER_BLUFF, {2996, 8356, 8357}},
|
|
||||||
{CityId::SILVERMOON_CITY, {17631, 17632, 17633, 16615, 16616, 16617}},
|
|
||||||
{CityId::SHATTRATH_CITY, {19246, 19338, 19034, 19318}},
|
|
||||||
{CityId::DALARAN, {30604, 30605, 30607, 28675, 28676, 28677, 29530}}
|
|
||||||
};
|
|
||||||
|
|
||||||
static int GetCityWeight(CityId city)
|
|
||||||
{
|
{
|
||||||
int weight = 0;
|
for (Capital const& capital : capitals)
|
||||||
switch (city)
|
if (capital.zoneId == zoneId)
|
||||||
|
return &capital;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Capital const* FindCapitalByBanker(uint16 bankerEntry)
|
||||||
|
{
|
||||||
|
for (Capital const& capital : capitals)
|
||||||
|
for (uint16 bankerId : capital.bankers)
|
||||||
|
if (bankerId == bankerEntry)
|
||||||
|
return &capital;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int GetCityWeight(uint32 zoneId)
|
||||||
|
{
|
||||||
|
switch (zoneId)
|
||||||
{
|
{
|
||||||
case CityId::STORMWIND: weight = sPlayerbotAIConfig.weightTeleToStormwind; break;
|
case AREA_STORMWIND_CITY: return sPlayerbotAIConfig.weightTeleToStormwind;
|
||||||
case CityId::IRONFORGE: weight = sPlayerbotAIConfig.weightTeleToIronforge; break;
|
case AREA_IRONFORGE: return sPlayerbotAIConfig.weightTeleToIronforge;
|
||||||
case CityId::DARNASSUS: weight = sPlayerbotAIConfig.weightTeleToDarnassus; break;
|
case AREA_DARNASSUS: return sPlayerbotAIConfig.weightTeleToDarnassus;
|
||||||
case CityId::EXODAR: weight = sPlayerbotAIConfig.weightTeleToExodar; break;
|
case AREA_THE_EXODAR: return sPlayerbotAIConfig.weightTeleToExodar;
|
||||||
case CityId::ORGRIMMAR: weight = sPlayerbotAIConfig.weightTeleToOrgrimmar; break;
|
case AREA_ORGRIMMAR: return sPlayerbotAIConfig.weightTeleToOrgrimmar;
|
||||||
case CityId::UNDERCITY: weight = sPlayerbotAIConfig.weightTeleToUndercity; break;
|
case AREA_UNDERCITY: return sPlayerbotAIConfig.weightTeleToUndercity;
|
||||||
case CityId::THUNDER_BLUFF: weight = sPlayerbotAIConfig.weightTeleToThunderBluff; break;
|
case AREA_THUNDER_BLUFF: return sPlayerbotAIConfig.weightTeleToThunderBluff;
|
||||||
case CityId::SILVERMOON_CITY: weight = sPlayerbotAIConfig.weightTeleToSilvermoonCity; break;
|
case AREA_SILVERMOON_CITY: return sPlayerbotAIConfig.weightTeleToSilvermoonCity;
|
||||||
case CityId::SHATTRATH_CITY: weight = sPlayerbotAIConfig.weightTeleToShattrathCity; break;
|
case AREA_SHATTRATH_CITY: return sPlayerbotAIConfig.weightTeleToShattrathCity;
|
||||||
case CityId::DALARAN: weight = sPlayerbotAIConfig.weightTeleToDalaran; break;
|
case AREA_DALARAN: return sPlayerbotAIConfig.weightTeleToDalaran;
|
||||||
default: weight = 0; break;
|
|
||||||
}
|
}
|
||||||
return weight;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
WorldPosition::WorldPosition(std::string const str)
|
WorldPosition::WorldPosition(std::string const str)
|
||||||
@ -687,93 +681,6 @@ std::vector<WorldPosition> WorldPosition::frommGridCoord(mGridCoord GridCoord)
|
|||||||
return retVec;
|
return retVec;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Cleanup — make this actually work.
|
|
||||||
void WorldPosition::loadMapAndVMap(uint32 mapId, uint8 x, uint8 y)
|
|
||||||
{
|
|
||||||
std::string const fileName = "load_map_grid.csv";
|
|
||||||
/*
|
|
||||||
if (isOverworld() && false || false)
|
|
||||||
{
|
|
||||||
if (!MMAP::MMapFactory::createOrGetMMapMgr()->loadMap(mapId, x, y))
|
|
||||||
if (sPlayerbotAIConfig.hasLog(fileName))
|
|
||||||
{
|
|
||||||
std::ostringstream out;
|
|
||||||
out << sPlayerbotAIConfig.GetTimestampStr();
|
|
||||||
out << "+00,\"mmap\", " << x << "," << y << "," << (TravelMgr::instance().isBadMmap(mapId, x, y) ? "0" : "1")
|
|
||||||
<< ",";
|
|
||||||
printWKT(fromGridCoord(GridCoord(x, y)), out, 1, true);
|
|
||||||
sPlayerbotAIConfig.log(fileName, out.str().c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// This needs to be disabled or maps will not load.
|
|
||||||
// Needs more testing to check for impact on movement.
|
|
||||||
if (false)
|
|
||||||
if (!TravelMgr::instance().isBadVmap(mapId, x, y))
|
|
||||||
{
|
|
||||||
// load VMAPs for current map/grid...
|
|
||||||
const MapEntry* i_mapEntry = sMapStore.LookupEntry(mapId);
|
|
||||||
//const char* mapName = i_mapEntry ? i_mapEntry->name[sWorld->GetDefaultDbcLocale()] : "UNNAMEDMAP\x0"; //not used, (usage are commented out below), line marked for removal.
|
|
||||||
|
|
||||||
int vmapLoadResult = VMAP::VMapFactory::createOrGetVMapMgr()->loadMap(
|
|
||||||
(sWorld->GetDataPath() + "vmaps").c_str(), mapId, x, y);
|
|
||||||
switch (vmapLoadResult)
|
|
||||||
{
|
|
||||||
case VMAP::VMAP_LOAD_RESULT_OK:
|
|
||||||
// LOG_ERROR("playerbots", "VMAP loaded name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{}, y:{})",
|
|
||||||
// mapName, mapId, x, y, x, y);
|
|
||||||
break;
|
|
||||||
case VMAP::VMAP_LOAD_RESULT_ERROR:
|
|
||||||
// LOG_ERROR("playerbots", "Could not load VMAP name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{},
|
|
||||||
// y:{})", mapName, mapId, x, y, x, y);
|
|
||||||
TravelMgr::instance().addBadVmap(mapId, x, y);
|
|
||||||
break;
|
|
||||||
case VMAP::VMAP_LOAD_RESULT_IGNORED:
|
|
||||||
TravelMgr::instance().addBadVmap(mapId, x, y);
|
|
||||||
// LOG_INFO("playerbots", "Ignored VMAP name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{}, y:{})",
|
|
||||||
// mapName, mapId, x, y, x, y);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sPlayerbotAIConfig.hasLog(fileName))
|
|
||||||
{
|
|
||||||
std::ostringstream out;
|
|
||||||
out << sPlayerbotAIConfig.GetTimestampStr();
|
|
||||||
out << "+00,\"vmap\", " << x << "," << y << ", " << (TravelMgr::instance().isBadVmap(mapId, x, y) ? "0" : "1")
|
|
||||||
<< ",";
|
|
||||||
printWKT(frommGridCoord(mGridCoord(x, y)), out, 1, true);
|
|
||||||
sPlayerbotAIConfig.log(fileName, out.str().c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
if (!TravelMgr::instance().isBadMmap(mapId, x, y))
|
|
||||||
{
|
|
||||||
// load navmesh
|
|
||||||
Map* map = getMap();
|
|
||||||
if (map && map->GetMapCollisionData().LoadMMapTile(x, y) == MMAP::MMAP_LOAD_RESULT_ERROR)
|
|
||||||
TravelMgr::instance().addBadMmap(mapId, x, y);
|
|
||||||
|
|
||||||
if (sPlayerbotAIConfig.hasLog(fileName))
|
|
||||||
{
|
|
||||||
std::ostringstream out;
|
|
||||||
out << sPlayerbotAIConfig.GetTimestampStr();
|
|
||||||
out << "+00,\"mmap\", " << x << "," << y << "," << (TravelMgr::instance().isBadMmap(mapId, x, y) ? "0" : "1")
|
|
||||||
<< ",";
|
|
||||||
printWKT(fromGridCoord(GridCoord(x, y)), out, 1, true);
|
|
||||||
sPlayerbotAIConfig.log(fileName, out.str().c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void WorldPosition::loadMapAndVMaps(WorldPosition secondPos)
|
|
||||||
{
|
|
||||||
for (auto& grid : getmGridCoords(secondPos))
|
|
||||||
{
|
|
||||||
loadMapAndVMap(GetMapId(), grid.first, grid.second);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<WorldPosition> WorldPosition::fromPointsArray(std::vector<G3D::Vector3> path)
|
std::vector<WorldPosition> WorldPosition::fromPointsArray(std::vector<G3D::Vector3> path)
|
||||||
{
|
{
|
||||||
std::vector<WorldPosition> retVec;
|
std::vector<WorldPosition> retVec;
|
||||||
@ -786,34 +693,42 @@ std::vector<WorldPosition> WorldPosition::fromPointsArray(std::vector<G3D::Vecto
|
|||||||
// A single pathfinding attempt from one position to another. Returns pathfinding status and path.
|
// A single pathfinding attempt from one position to another. Returns pathfinding status and path.
|
||||||
std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos, Unit* bot)
|
std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos, Unit* bot)
|
||||||
{
|
{
|
||||||
if (!bot)
|
Unit* pathUnit = bot;
|
||||||
|
Creature* tempCreature = nullptr;
|
||||||
|
|
||||||
|
if (!pathUnit)
|
||||||
|
{
|
||||||
|
// Create a temporary creature for PathGenerator (same entry as DebugAction "show node")
|
||||||
|
Map* map = sMapMgr->FindBaseMap(startPos.GetMapId());
|
||||||
|
if (!map)
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
// Load mmaps and vmaps between the two points.
|
tempCreature = new Creature();
|
||||||
loadMapAndVMaps(startPos);
|
if (!tempCreature->Create(map->GenerateLowGuid<HighGuid::Unit>(), map,
|
||||||
|
PHASEMASK_NORMAL, 1 /*entry*/, 0,
|
||||||
|
startPos.GetPositionX(), startPos.GetPositionY(),
|
||||||
|
startPos.GetPositionZ(), 0))
|
||||||
|
{
|
||||||
|
delete tempCreature;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
pathUnit = tempCreature;
|
||||||
|
|
||||||
PathGenerator path(bot);
|
// Ensure grids are created at both endpoints so mmap tiles are available.
|
||||||
path.CalculatePath(startPos.GetPositionX(), startPos.GetPositionY(), startPos.GetPositionZ());
|
// 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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
PathGenerator path(pathUnit);
|
||||||
|
path.CalculatePath(GetPositionX(), GetPositionY(), GetPositionZ());
|
||||||
|
|
||||||
Movement::PointsArray points = path.GetPath();
|
Movement::PointsArray points = path.GetPath();
|
||||||
PathType type = path.GetPathType();
|
PathType type = path.GetPathType();
|
||||||
|
|
||||||
if (sPlayerbotAIConfig.hasLog("pathfind_attempt_point.csv"))
|
if (tempCreature)
|
||||||
{
|
delete tempCreature;
|
||||||
std::ostringstream out;
|
|
||||||
out << std::fixed << std::setprecision(1);
|
|
||||||
printWKT({startPos, *this}, out);
|
|
||||||
sPlayerbotAIConfig.log("pathfind_attempt_point.csv", out.str().c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sPlayerbotAIConfig.hasLog("pathfind_attempt.csv") && (type == PATHFIND_INCOMPLETE || type == PATHFIND_NORMAL))
|
|
||||||
{
|
|
||||||
std::ostringstream out;
|
|
||||||
out << sPlayerbotAIConfig.GetTimestampStr() << "+00,";
|
|
||||||
out << std::fixed << std::setprecision(1) << type << ",";
|
|
||||||
printWKT(fromPointsArray(points), out, 1);
|
|
||||||
sPlayerbotAIConfig.log("pathfind_attempt.csv", out.str().c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == PATHFIND_INCOMPLETE || type == PATHFIND_NORMAL)
|
if (type == PATHFIND_INCOMPLETE || type == PATHFIND_NORMAL)
|
||||||
return fromPointsArray(points);
|
return fromPointsArray(points);
|
||||||
@ -1079,6 +994,14 @@ GuidPosition::GuidPosition(GameObjectData const& goData)
|
|||||||
loadedFromDB = true;
|
loadedFromDB = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TravelDestination::~TravelDestination()
|
||||||
|
{
|
||||||
|
for (WorldPosition* point : points)
|
||||||
|
delete point;
|
||||||
|
|
||||||
|
points.clear();
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<WorldPosition*> TravelDestination::getPoints(bool ignoreFull)
|
std::vector<WorldPosition*> TravelDestination::getPoints(bool ignoreFull)
|
||||||
{
|
{
|
||||||
if (ignoreFull)
|
if (ignoreFull)
|
||||||
@ -2385,9 +2308,7 @@ void TravelMgr::LoadQuestTravelTable()
|
|||||||
sPlayerbotAIConfig.openLog("unload_grid.csv", "w");
|
sPlayerbotAIConfig.openLog("unload_grid.csv", "w");
|
||||||
sPlayerbotAIConfig.openLog("unload_obj.csv", "w");
|
sPlayerbotAIConfig.openLog("unload_obj.csv", "w");
|
||||||
|
|
||||||
TravelNodeMap::instance().loadNodeStore();
|
// Node loading/generation is handled by TravelNodeMap::Init() called from TravelMgr::Init().
|
||||||
|
|
||||||
TravelNodeMap::instance().generateAll();
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
bool fullNavPointReload = false;
|
bool fullNavPointReload = false;
|
||||||
@ -2778,7 +2699,7 @@ void TravelMgr::LoadQuestTravelTable()
|
|||||||
//if (preloadUnlinkedPaths && !startNode->hasLinkTo(endNode) && startNode->isUselessLink(endNode))
|
//if (preloadUnlinkedPaths && !startNode->hasLinkTo(endNode) && startNode->isUselessLink(endNode))
|
||||||
// continue;
|
// continue;
|
||||||
|
|
||||||
startNode->buildPath(endNode, nullptr, false);
|
startNode->BuildPath(endNode, nullptr, false);
|
||||||
|
|
||||||
//if (startNode->hasLinkTo(endNode) && !startNode->getPathTo(endNode)->getComplete())
|
//if (startNode->hasLinkTo(endNode) && !startNode->getPathTo(endNode)->getComplete())
|
||||||
//startNode->removeLinkTo(endNode);
|
//startNode->removeLinkTo(endNode);
|
||||||
@ -2902,7 +2823,7 @@ void TravelMgr::LoadQuestTravelTable()
|
|||||||
|
|
||||||
TravelNodePath nodePath = *path.second;
|
TravelNodePath nodePath = *path.second;
|
||||||
|
|
||||||
std::vector<WorldPosition> pPath = nodePath.getPath();
|
std::vector<WorldPosition> pPath = nodePath.GetPath();
|
||||||
std::reverse(pPath.begin(), pPath.end());
|
std::reverse(pPath.begin(), pPath.end());
|
||||||
|
|
||||||
nodePath.setPath(pPath);
|
nodePath.setPath(pPath);
|
||||||
@ -4365,80 +4286,120 @@ void TravelMgr::Init()
|
|||||||
PrepareZone2LevelBracket();
|
PrepareZone2LevelBracket();
|
||||||
PrepareDestinationCache();
|
PrepareDestinationCache();
|
||||||
}
|
}
|
||||||
sTravelNodeMap.InitTaxiGraph();
|
sTravelNodeMap.Init();
|
||||||
LOG_INFO("playerbots", "Playerbots Taxi graph and destination cache built.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Creature* TravelMgr::GetNearestFlightMaster(Player* bot)
|
TravelMgr::FlightMasterInfo const* TravelMgr::GetNearestFlightMasterInfo(Player* bot) const
|
||||||
{
|
{
|
||||||
std::map<uint32, WorldPosition>& flightMasterCache =
|
auto const& flightMasterCache =
|
||||||
(bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache;
|
(bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache;
|
||||||
|
|
||||||
Creature* nearestFlightMaster = nullptr;
|
FlightMasterInfo const* nearest = nullptr;
|
||||||
float nearestDistance = std::numeric_limits<float>::max();
|
float nearestDistance = std::numeric_limits<float>::max();
|
||||||
|
|
||||||
for (auto const& [entry, pos] : flightMasterCache)
|
for (auto const& [dbGuid, info] : flightMasterCache)
|
||||||
{
|
{
|
||||||
if (pos.GetMapId() != bot->GetMapId())
|
if (info.pos.GetMapId() != bot->GetMapId())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
float distance = bot->GetExactDist2dSq(pos);
|
float distance = bot->GetExactDist2dSq(info.pos);
|
||||||
if (distance > nearestDistance)
|
if (distance < nearestDistance)
|
||||||
continue;
|
|
||||||
|
|
||||||
Creature* flightMaster = ObjectAccessor::GetSpawnedCreatureByDBGUID(bot->GetMapId(), entry);
|
|
||||||
if (flightMaster)
|
|
||||||
{
|
{
|
||||||
nearestDistance = distance;
|
nearestDistance = distance;
|
||||||
nearestFlightMaster = flightMaster;
|
nearest = &info;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nearestFlightMaster;
|
return nearest;
|
||||||
}
|
}
|
||||||
|
|
||||||
ObjectGuid TravelMgr::GetNearestFlightMasterGuid(Player* bot)
|
std::vector<uint32> TravelMgr::GetFlightNodesInZone(uint32 zoneId, TeamId team, uint32 excludeNode) const
|
||||||
{
|
{
|
||||||
Creature* nearestFlightMaster = GetNearestFlightMaster(bot);
|
auto const& cache = (team == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache;
|
||||||
if (!nearestFlightMaster)
|
std::unordered_set<uint32> seen;
|
||||||
return ObjectGuid::Empty;
|
std::vector<uint32> result;
|
||||||
|
for (auto const& [entry, info] : cache)
|
||||||
return nearestFlightMaster->GetGUID();
|
{
|
||||||
|
if (info.zoneId != zoneId || info.taxiNodeId == 0 || info.taxiNodeId == excludeNode)
|
||||||
|
continue;
|
||||||
|
if (seen.insert(info.taxiNodeId).second)
|
||||||
|
result.push_back(info.taxiNodeId);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::vector<uint32>> TravelMgr::GetOptimalFlightDestinations(Player* bot)
|
std::vector<std::vector<uint32>> TravelMgr::GetOptimalFlightDestinations(Player* bot)
|
||||||
{
|
{
|
||||||
std::vector<std::vector<uint32>> validDestinations;
|
std::vector<std::vector<uint32>> validDestinations;
|
||||||
|
|
||||||
Creature* nearestFlightMaster = GetNearestFlightMaster(bot);
|
FlightMasterInfo const* nearestFlightMaster = GetNearestFlightMasterInfo(bot);
|
||||||
if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster) > 500.0f)
|
if (!nearestFlightMaster)
|
||||||
return validDestinations;
|
return validDestinations;
|
||||||
|
|
||||||
uint32 fromNode = sObjectMgr->GetNearestTaxiNode(nearestFlightMaster->GetPositionX(), nearestFlightMaster->GetPositionY(),
|
uint32 fromNode = nearestFlightMaster->taxiNodeId;
|
||||||
nearestFlightMaster->GetPositionZ(), nearestFlightMaster->GetMapId(),
|
|
||||||
bot->GetTeamId());
|
|
||||||
if (!fromNode)
|
if (!fromNode)
|
||||||
return validDestinations;
|
return validDestinations;
|
||||||
|
|
||||||
std::vector<WorldLocation> candidateLocations;
|
TaxiNodesEntry const* startNode = sTaxiNodesStore.LookupEntry(fromNode);
|
||||||
if (bot->GetLevel() >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100)
|
if (!startNode)
|
||||||
candidateLocations = GetCityLocations(bot);
|
return validDestinations;
|
||||||
|
|
||||||
std::vector<WorldLocation> hubLocations = GetTravelHubs(bot);
|
uint32 botLevel = bot->GetLevel();
|
||||||
candidateLocations.insert(candidateLocations.end(), hubLocations.begin(), hubLocations.end());
|
|
||||||
|
|
||||||
for (auto const& loc : candidateLocations)
|
// Bots already in a capital shouldn't have another capital picked as a
|
||||||
|
// flight destination — that just shuffles them between cities.
|
||||||
|
bool botInCapital = false;
|
||||||
|
if (AreaTableEntry const* area = sAreaTableStore.LookupEntry(bot->GetZoneId()))
|
||||||
|
botInCapital = (area->flags & AREA_FLAG_CAPITAL) != 0;
|
||||||
|
|
||||||
|
std::vector<uint32> candidateZones;
|
||||||
|
if (botLevel >= 10 && !botInCapital &&
|
||||||
|
urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100)
|
||||||
{
|
{
|
||||||
uint32 candidateNode = sObjectMgr->GetNearestTaxiNode(loc.GetPositionX(), loc.GetPositionY(),
|
TeamId botTeam = bot->GetTeamId();
|
||||||
loc.GetPositionZ(), loc.GetMapId(),
|
for (Capital const& capital : capitals)
|
||||||
bot->GetTeamId());
|
{
|
||||||
if (!candidateNode)
|
if (capital.team != TEAM_NEUTRAL && capital.team != botTeam)
|
||||||
continue;
|
continue;
|
||||||
|
candidateZones.push_back(capital.zoneId);
|
||||||
std::vector<uint32> path = sTravelNodeMap.FindTaxiPath(fromNode, candidateNode);
|
|
||||||
if (!path.empty())
|
|
||||||
validDestinations.push_back(path);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (candidateZones.empty())
|
||||||
|
{
|
||||||
|
for (auto const& [zoneId, bracket] : zone2LevelBracket)
|
||||||
|
{
|
||||||
|
if (botLevel < bracket.low || botLevel > bracket.high)
|
||||||
|
continue;
|
||||||
|
if (GetFlightNodesInZone(zoneId, bot->GetTeamId(), fromNode).empty())
|
||||||
|
continue;
|
||||||
|
candidateZones.push_back(zoneId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidateZones.empty())
|
||||||
|
return validDestinations;
|
||||||
|
|
||||||
|
while (!candidateZones.empty())
|
||||||
|
{
|
||||||
|
uint32 zoneIndex = urand(0, candidateZones.size() - 1);
|
||||||
|
uint32 pickedZone = candidateZones[zoneIndex];
|
||||||
|
|
||||||
|
std::vector<uint32> usableNodes = GetFlightNodesInZone(pickedZone, bot->GetTeamId(), fromNode);
|
||||||
|
|
||||||
|
if (!usableNodes.empty())
|
||||||
|
{
|
||||||
|
uint32 pickedNode = usableNodes[urand(0, usableNodes.size() - 1)];
|
||||||
|
std::vector<uint32> path = sTravelNodeMap.FindTaxiPath(fromNode, pickedNode);
|
||||||
|
if (!path.empty())
|
||||||
|
{
|
||||||
|
validDestinations.push_back(std::move(path));
|
||||||
|
return validDestinations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
candidateZones.erase(candidateZones.begin() + zoneIndex);
|
||||||
|
}
|
||||||
|
|
||||||
return validDestinations;
|
return validDestinations;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4472,34 +4433,34 @@ std::vector<WorldLocation> TravelMgr::GetCityLocations(Player* bot)
|
|||||||
return fallbackLocations;
|
return fallbackLocations;
|
||||||
|
|
||||||
TeamId botTeamId = bot->GetTeamId();
|
TeamId botTeamId = bot->GetTeamId();
|
||||||
std::unordered_set<CityId> validBankerCities;
|
std::unordered_set<uint32> validBankerCities;
|
||||||
for (auto& loc : bankerLocsPerLevelCache[level])
|
for (auto& loc : bankerLocsPerLevelCache[level])
|
||||||
{
|
{
|
||||||
auto cityIt = bankerToCity.find(loc.entry);
|
Capital const* capital = FindCapitalByBanker(loc.entry);
|
||||||
if (cityIt == bankerToCity.end())
|
if (!capital)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
TeamId cityTeamId = cityIt->second.second;
|
TeamId cityTeamId = capital->team;
|
||||||
|
|
||||||
if (cityTeamId == botTeamId ||
|
if (cityTeamId == botTeamId ||
|
||||||
(cityTeamId == TEAM_NEUTRAL)
|
(cityTeamId == TEAM_NEUTRAL)
|
||||||
)
|
)
|
||||||
validBankerCities.insert(cityIt->second.first);
|
validBankerCities.insert(capital->zoneId);
|
||||||
}
|
}
|
||||||
// Fallback if no valid cities
|
// Fallback if no valid cities
|
||||||
if (validBankerCities.empty())
|
if (validBankerCities.empty())
|
||||||
return fallbackLocations;
|
return fallbackLocations;
|
||||||
|
|
||||||
// Apply weights to valid cities
|
// Apply weights to valid cities
|
||||||
std::vector<CityId> weightedCities;
|
std::vector<uint32> weightedCities;
|
||||||
for (CityId city : validBankerCities)
|
for (uint32 zoneId : validBankerCities)
|
||||||
{
|
{
|
||||||
int weight = GetCityWeight(city);
|
int weight = GetCityWeight(zoneId);
|
||||||
if (weight <= 0)
|
if (weight <= 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
for (int i = 0; i < weight; ++i)
|
for (int i = 0; i < weight; ++i)
|
||||||
weightedCities.push_back(city);
|
weightedCities.push_back(zoneId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback if no valid cities
|
// Fallback if no valid cities
|
||||||
@ -4507,9 +4468,11 @@ std::vector<WorldLocation> TravelMgr::GetCityLocations(Player* bot)
|
|||||||
return fallbackLocations;
|
return fallbackLocations;
|
||||||
|
|
||||||
// Pick a weighted city randomly, then a random banker in that city
|
// Pick a weighted city randomly, then a random banker in that city
|
||||||
CityId selectedCity = weightedCities[urand(0, weightedCities.size() - 1)];
|
uint32 selectedCity = weightedCities[urand(0, weightedCities.size() - 1)];
|
||||||
|
Capital const* selectedCapital = FindCapitalByZone(selectedCity);
|
||||||
auto const& bankers = cityToBankers.at(selectedCity);
|
if (!selectedCapital)
|
||||||
|
return fallbackLocations;
|
||||||
|
auto const& bankers = selectedCapital->bankers;
|
||||||
uint32 selectedBankerEntry = bankers[urand(0, bankers.size() - 1)];
|
uint32 selectedBankerEntry = bankers[urand(0, bankers.size() - 1)];
|
||||||
auto locIt = bankerEntryToLocation.find(selectedBankerEntry);
|
auto locIt = bankerEntryToLocation.find(selectedBankerEntry);
|
||||||
if (locIt != bankerEntryToLocation.end())
|
if (locIt != bankerEntryToLocation.end())
|
||||||
@ -4518,80 +4481,108 @@ std::vector<WorldLocation> TravelMgr::GetCityLocations(Player* bot)
|
|||||||
return fallbackLocations;
|
return fallbackLocations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TravelMgr::SelectAuctioneerByMap(Player* bot, NpcLocation& outAuctioneer)
|
||||||
|
{
|
||||||
|
uint16 botMapId = bot->GetMapId();
|
||||||
|
auto const& cache = (bot->GetTeamId() == TEAM_HORDE) ? hordeAuctioneerCache : allianceAuctioneerCache;
|
||||||
|
|
||||||
|
auto mapIt = cache.find(botMapId);
|
||||||
|
if (mapIt == cache.end() || mapIt->second.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Collect all areas on this map that have auctioneers
|
||||||
|
std::vector<uint32> areaIds;
|
||||||
|
areaIds.reserve(mapIt->second.size());
|
||||||
|
for (auto const& [areaId, npcs] : mapIt->second)
|
||||||
|
{
|
||||||
|
if (!npcs.empty())
|
||||||
|
areaIds.push_back(areaId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areaIds.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Pick a random area, then a random auctioneer in that area
|
||||||
|
uint32 selectedArea = areaIds[urand(0, areaIds.size() - 1)];
|
||||||
|
auto const& auctioneers = mapIt->second.at(selectedArea);
|
||||||
|
outAuctioneer = auctioneers[urand(0, auctioneers.size() - 1)];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void TravelMgr::PrepareZone2LevelBracket()
|
void TravelMgr::PrepareZone2LevelBracket()
|
||||||
{
|
{
|
||||||
// Classic WoW - Low - level zones
|
// Classic WoW - starter zones
|
||||||
zone2LevelBracket[1] = {5, 12}; // Dun Morogh
|
zone2LevelBracket[AREA_DUN_MOROGH] = {5, 12};
|
||||||
zone2LevelBracket[12] = {5, 12}; // Elwynn Forest
|
zone2LevelBracket[AREA_ELWYNN_FOREST] = {5, 12};
|
||||||
zone2LevelBracket[14] = {5, 12}; // Durotar
|
zone2LevelBracket[AREA_DUROTAR] = {5, 12};
|
||||||
zone2LevelBracket[85] = {5, 12}; // Tirisfal Glades
|
zone2LevelBracket[AREA_TIRISFAL_GLADES] = {5, 12};
|
||||||
zone2LevelBracket[141] = {5, 12}; // Teldrassil
|
zone2LevelBracket[AREA_TELDRASSIL] = {5, 12};
|
||||||
zone2LevelBracket[215] = {5, 12}; // Mulgore
|
zone2LevelBracket[AREA_MULGORE] = {5, 12};
|
||||||
zone2LevelBracket[3430] = {5, 12}; // Eversong Woods
|
zone2LevelBracket[AREA_EVERSONG_WOODS] = {5, 12};
|
||||||
zone2LevelBracket[3524] = {5, 12}; // Azuremyst Isle
|
zone2LevelBracket[AREA_AZUREMYST_ISLE] = {5, 12};
|
||||||
|
|
||||||
// Classic WoW - Mid - level zones
|
// Classic WoW - low level zones
|
||||||
zone2LevelBracket[17] = {10, 25}; // Barrens
|
zone2LevelBracket[AREA_THE_BARRENS] = {10, 25};
|
||||||
zone2LevelBracket[38] = {10, 20}; // Loch Modan
|
zone2LevelBracket[AREA_LOCH_MODAN] = {10, 20};
|
||||||
zone2LevelBracket[40] = {10, 21}; // Westfall
|
zone2LevelBracket[AREA_WESTFALL] = {10, 21};
|
||||||
zone2LevelBracket[130] = {10, 23}; // Silverpine Forest
|
zone2LevelBracket[AREA_SILVERPINE_FOREST] = {10, 23};
|
||||||
zone2LevelBracket[148] = {10, 21}; // Darkshore
|
zone2LevelBracket[AREA_DARKSHORE] = {10, 21};
|
||||||
zone2LevelBracket[3433] = {10, 22}; // Ghostlands
|
zone2LevelBracket[AREA_GHOSTLANDS] = {10, 22};
|
||||||
zone2LevelBracket[3525] = {10, 21}; // Bloodmyst Isle
|
zone2LevelBracket[AREA_BLOODMYST_ISLE] = {10, 21};
|
||||||
|
|
||||||
// Classic WoW - High - level zones
|
// Classic WoW - mid-level zones
|
||||||
zone2LevelBracket[10] = {19, 33}; // Deadwind Pass
|
zone2LevelBracket[AREA_DUSKWOOD] = {19, 33};
|
||||||
zone2LevelBracket[11] = {21, 30}; // Wetlands
|
zone2LevelBracket[AREA_WETLANDS] = {21, 30};
|
||||||
zone2LevelBracket[44] = {16, 28}; // Redridge Mountains
|
zone2LevelBracket[AREA_REDRIDGE_MOUNTAINS] = {16, 28};
|
||||||
zone2LevelBracket[267] = {20, 34}; // Hillsbrad Foothills
|
zone2LevelBracket[AREA_HILLSBRAD_FOOTHILLS] = {20, 34};
|
||||||
zone2LevelBracket[331] = {18, 33}; // Ashenvale
|
zone2LevelBracket[AREA_ASHENVALE] = {18, 33};
|
||||||
zone2LevelBracket[400] = {24, 36}; // Thousand Needles
|
zone2LevelBracket[AREA_THOUSAND_NEEDLES] = {24, 36};
|
||||||
zone2LevelBracket[406] = {16, 29}; // Stonetalon Mountains
|
zone2LevelBracket[AREA_STONETALON_MOUNTAINS] = {16, 29};
|
||||||
|
|
||||||
// Classic WoW - Higher - level zones
|
// Classic WoW - 30-52 zones
|
||||||
zone2LevelBracket[3] = {36, 46}; // Badlands
|
zone2LevelBracket[AREA_BADLANDS] = {36, 46};
|
||||||
zone2LevelBracket[8] = {36, 46}; // Swamp of Sorrows
|
zone2LevelBracket[AREA_SWAMP_OF_SORROWS] = {36, 46};
|
||||||
zone2LevelBracket[15] = {35, 46}; // Dustwallow Marsh
|
zone2LevelBracket[AREA_DUSTWALLOW_MARSH] = {35, 46};
|
||||||
zone2LevelBracket[16] = {45, 52}; // Azshara
|
zone2LevelBracket[AREA_AZSHARA] = {45, 52};
|
||||||
zone2LevelBracket[33] = {32, 47}; // Stranglethorn Vale
|
zone2LevelBracket[AREA_STRANGLETHORN_VALE] = {32, 47};
|
||||||
zone2LevelBracket[45] = {30, 42}; // Arathi Highlands
|
zone2LevelBracket[AREA_ARATHI_HIGHLANDS] = {30, 42};
|
||||||
zone2LevelBracket[47] = {42, 51}; // Hinterlands
|
zone2LevelBracket[AREA_THE_HINTERLANDS] = {42, 51};
|
||||||
zone2LevelBracket[51] = {45, 51}; // Searing Gorge
|
zone2LevelBracket[AREA_SEARING_GORGE] = {45, 51};
|
||||||
zone2LevelBracket[357] = {40, 52}; // Feralas
|
zone2LevelBracket[AREA_FERALAS] = {40, 52};
|
||||||
zone2LevelBracket[405] = {30, 41}; // Desolace
|
zone2LevelBracket[AREA_DESOLACE] = {30, 41};
|
||||||
zone2LevelBracket[440] = {41, 52}; // Tanaris
|
zone2LevelBracket[AREA_TANARIS] = {41, 52};
|
||||||
|
|
||||||
// Classic WoW - Top - level zones
|
// Classic WoW - top level zones
|
||||||
zone2LevelBracket[4] = {52, 57}; // Blasted Lands
|
zone2LevelBracket[AREA_BLASTED_LANDS] = {52, 57};
|
||||||
zone2LevelBracket[28] = {50, 60}; // Western Plaguelands
|
zone2LevelBracket[AREA_WESTERN_PLAGUELANDS] = {50, 60};
|
||||||
zone2LevelBracket[46] = {51, 60}; // Burning Steppes
|
zone2LevelBracket[AREA_BURNING_STEPPES] = {51, 60};
|
||||||
zone2LevelBracket[139] = {54, 62}; // Eastern Plaguelands
|
zone2LevelBracket[AREA_EASTERN_PLAGUELANDS] = {54, 62};
|
||||||
zone2LevelBracket[361] = {47, 57}; // Felwood
|
zone2LevelBracket[361] = {47, 57}; // Felwood (no AREA_ define)
|
||||||
zone2LevelBracket[490] = {49, 56}; // Un'Goro Crater
|
zone2LevelBracket[490] = {49, 56}; // Un'Goro Crater (no AREA_ define)
|
||||||
zone2LevelBracket[618] = {54, 61}; // Winterspring
|
zone2LevelBracket[AREA_WINTERSPRING] = {54, 61};
|
||||||
zone2LevelBracket[1377] = {54, 63}; // Silithus
|
zone2LevelBracket[AREA_SILITHUS] = {54, 63};
|
||||||
|
|
||||||
// The Burning Crusade - Zones
|
// The Burning Crusade zones
|
||||||
zone2LevelBracket[3483] = {58, 66}; // Hellfire Peninsula
|
zone2LevelBracket[AREA_HELLFIRE_PENINSULA] = {58, 66};
|
||||||
zone2LevelBracket[3518] = {64, 70}; // Nagrand
|
zone2LevelBracket[AREA_NAGRAND] = {64, 70};
|
||||||
zone2LevelBracket[3519] = {62, 73}; // Terokkar Forest
|
zone2LevelBracket[AREA_TEROKKAR_FOREST] = {62, 73};
|
||||||
zone2LevelBracket[3520] = {66, 73}; // Shadowmoon Valley
|
zone2LevelBracket[AREA_SHADOWMOON_VALLEY] = {66, 73};
|
||||||
zone2LevelBracket[3521] = {60, 67}; // Zangarmarsh
|
zone2LevelBracket[AREA_ZANGARMARSH] = {60, 67};
|
||||||
zone2LevelBracket[3522] = {64, 73}; // Blade's Edge Mountains
|
zone2LevelBracket[AREA_BLADES_EDGE_MOUNTAINS] = {64, 73};
|
||||||
zone2LevelBracket[3523] = {67, 73}; // Netherstorm
|
zone2LevelBracket[AREA_NETHERSTORM] = {67, 73};
|
||||||
zone2LevelBracket[4080] = {68, 73}; // Isle of Quel'Danas
|
zone2LevelBracket[AREA_ISLE_OF_QUEL_DANAS] = {68, 73};
|
||||||
|
|
||||||
// Wrath of the Lich King - Zones
|
// Wrath of the Lich King zones
|
||||||
zone2LevelBracket[65] = {71, 77}; // Dragonblight
|
zone2LevelBracket[AREA_DRAGONBLIGHT] = {71, 77};
|
||||||
zone2LevelBracket[66] = {74, 80}; // Zul'Drak
|
zone2LevelBracket[AREA_ZUL_DRAK] = {74, 80};
|
||||||
zone2LevelBracket[67] = {77, 80}; // Storm Peaks
|
zone2LevelBracket[AREA_THE_STORM_PEAKS] = {77, 80};
|
||||||
zone2LevelBracket[210] = {77, 80}; // Icecrown Glacier
|
zone2LevelBracket[210] = {77, 80}; // Icecrown Glacier (no AREA_ define)
|
||||||
zone2LevelBracket[394] = {72, 78}; // Grizzly Hills
|
zone2LevelBracket[AREA_GRIZZLY_HILLS] = {72, 78};
|
||||||
zone2LevelBracket[495] = {68, 74}; // Howling Fjord
|
zone2LevelBracket[AREA_HOWLING_FJORD] = {68, 74};
|
||||||
zone2LevelBracket[2817] = {77, 80}; // Crystalsong Forest
|
zone2LevelBracket[AREA_CRYSTALSONG_FOREST] = {77, 80};
|
||||||
zone2LevelBracket[3537] = {68, 75}; // Borean Tundra
|
zone2LevelBracket[AREA_BOREAN_TUNDRA] = {68, 75};
|
||||||
zone2LevelBracket[3711] = {75, 80}; // Sholazar Basin
|
zone2LevelBracket[AREA_SHOLAZAR_BASIN] = {75, 80};
|
||||||
zone2LevelBracket[4197] = {79, 80}; // Wintergrasp
|
zone2LevelBracket[AREA_WINTERGRASP] = {79, 80};
|
||||||
|
|
||||||
// Override with values from config
|
// Override with values from config
|
||||||
for (auto const& [zoneId, bracketPair] : sPlayerbotAIConfig.zoneBrackets)
|
for (auto const& [zoneId, bracketPair] : sPlayerbotAIConfig.zoneBrackets)
|
||||||
@ -4604,6 +4595,7 @@ void TravelMgr::PrepareDestinationCache()
|
|||||||
uint32 flightMastersCount = 0;
|
uint32 flightMastersCount = 0;
|
||||||
uint32 innkeepersCount = 0;
|
uint32 innkeepersCount = 0;
|
||||||
uint32 bankerCount = 0;
|
uint32 bankerCount = 0;
|
||||||
|
uint32 auctioneerCount = 0;
|
||||||
|
|
||||||
LOG_INFO("playerbots", "Preparing destination caches for {} levels...", maxLevel);
|
LOG_INFO("playerbots", "Preparing destination caches for {} levels...", maxLevel);
|
||||||
// Temporary map to group creatures by entry and area
|
// Temporary map to group creatures by entry and area
|
||||||
@ -4650,16 +4642,18 @@ void TravelMgr::PrepareDestinationCache()
|
|||||||
(creatureTemplate->unit_flags & 4096) == 0 &&
|
(creatureTemplate->unit_flags & 4096) == 0 &&
|
||||||
creatureTemplate->rank == 0)
|
creatureTemplate->rank == 0)
|
||||||
{
|
{
|
||||||
uint32 roundX = (x / 50.0f) * 10.0f;
|
uint32 roundX = static_cast<uint32>(std::round(x / 50.0f));
|
||||||
uint32 roundY = (y / 50.0f) * 10.0f;
|
uint32 roundY = static_cast<uint32>(std::round(y / 50.0f));
|
||||||
uint32 roundZ = (z / 50.0f) * 10.0f;
|
uint32 roundZ = static_cast<uint32>(std::round(z / 50.0f));
|
||||||
tempLocsCache[std::make_tuple(mapId, roundX, roundY, roundZ)].push_back(creatureData);
|
tempLocsCache[std::make_tuple(mapId, roundX, roundY, roundZ)].push_back(creatureData);
|
||||||
tempCreatureCache[templateEntry][areaId].push_back(WorldLocation(mapId, x, y, z));
|
tempCreatureCache[templateEntry][areaId].push_back(WorldLocation(mapId, x, y, z));
|
||||||
}
|
}
|
||||||
// FLIGHT MASTERS
|
// FLIGHT MASTERS
|
||||||
|
// Entry 29480 is Grimwing (Storm Peaks) — has FLIGHTMASTER flag but
|
||||||
|
// isn't a real usable flight master; skip it.
|
||||||
else if ((creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER ||
|
else if ((creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER ||
|
||||||
creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) &&
|
creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) &&
|
||||||
creatureTemplate->Entry != 3838 && creatureTemplate->Entry != 29480)
|
creatureTemplate->Entry != 29480)
|
||||||
{
|
{
|
||||||
FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction);
|
FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction);
|
||||||
bool forHorde = !(factionEntry->hostileMask & 4);
|
bool forHorde = !(factionEntry->hostileMask & 4);
|
||||||
@ -4669,23 +4663,39 @@ void TravelMgr::PrepareDestinationCache()
|
|||||||
{
|
{
|
||||||
WorldPosition pos(mapId, x, y, z, orient);
|
WorldPosition pos(mapId, x, y, z, orient);
|
||||||
if (forHorde)
|
if (forHorde)
|
||||||
hordeFlightMasterCache[guid] = pos;
|
{
|
||||||
|
FlightMasterInfo info;
|
||||||
|
info.pos = pos;
|
||||||
|
info.zoneId = areaId;
|
||||||
|
info.taxiNodeId = sObjectMgr->GetNearestTaxiNode(x, y, z, mapId, TEAM_HORDE);
|
||||||
|
info.templateEntry = templateEntry;
|
||||||
|
info.dbGuid = guid;
|
||||||
|
hordeFlightMasterCache[guid] = info;
|
||||||
|
}
|
||||||
|
|
||||||
if (forAlliance)
|
if (forAlliance)
|
||||||
allianceFlightMasterCache[guid] = pos;
|
{
|
||||||
|
FlightMasterInfo info;
|
||||||
|
info.pos = pos;
|
||||||
|
info.zoneId = areaId;
|
||||||
|
info.taxiNodeId = sObjectMgr->GetNearestTaxiNode(x, y, z, mapId, TEAM_ALLIANCE);
|
||||||
|
info.templateEntry = templateEntry;
|
||||||
|
info.dbGuid = guid;
|
||||||
|
allianceFlightMasterCache[guid] = info;
|
||||||
|
}
|
||||||
flightMastersCount++;
|
flightMastersCount++;
|
||||||
|
|
||||||
// Zones that have flight masters but no innkeepers — use flight master as hub
|
// Zones that have flight masters but no innkeepers — use flight master as hub
|
||||||
static const std::set<uint32> zonesWithoutInnkeeper = {
|
static const std::set<uint32> zonesWithoutInnkeeper = {
|
||||||
4, // Blasted Lands (52-57)
|
AREA_BLASTED_LANDS,
|
||||||
16, // Azshara (45-52)
|
AREA_AZSHARA,
|
||||||
28, // Western Plaguelands (50-60)
|
AREA_WESTERN_PLAGUELANDS,
|
||||||
46, // Burning Steppes (51-60)
|
AREA_BURNING_STEPPES,
|
||||||
51, // Searing Gorge (45-51)
|
AREA_SEARING_GORGE,
|
||||||
361, // Felwood (47-57)
|
361, // Felwood (47-57)
|
||||||
490, // Un'Goro Crater (49-56)
|
490, // Un'Goro Crater (49-56)
|
||||||
2817, // Crystalsong Forest (77-80)
|
AREA_CRYSTALSONG_FOREST,
|
||||||
4197 // Wintergrasp (79-80)
|
AREA_WINTERGRASP
|
||||||
};
|
};
|
||||||
if (zonesWithoutInnkeeper.count(areaId))
|
if (zonesWithoutInnkeeper.count(areaId))
|
||||||
{
|
{
|
||||||
@ -4728,7 +4738,7 @@ void TravelMgr::PrepareDestinationCache()
|
|||||||
creatureTemplate->Entry != 30606 && creatureTemplate->Entry != 30608 &&
|
creatureTemplate->Entry != 30606 && creatureTemplate->Entry != 30608 &&
|
||||||
creatureTemplate->Entry != 29282)
|
creatureTemplate->Entry != 29282)
|
||||||
{
|
{
|
||||||
BankerLocation bLoc;
|
NpcLocation bLoc;
|
||||||
bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI);
|
bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI);
|
||||||
bLoc.entry = templateEntry;
|
bLoc.entry = templateEntry;
|
||||||
uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2;
|
uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2;
|
||||||
@ -4751,22 +4761,63 @@ void TravelMgr::PrepareDestinationCache()
|
|||||||
}
|
}
|
||||||
bankerCount++;
|
bankerCount++;
|
||||||
}
|
}
|
||||||
|
// === AUCTIONEERS ===
|
||||||
|
else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_AUCTIONEER)
|
||||||
|
{
|
||||||
|
FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction);
|
||||||
|
if (!factionEntry)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bool forHorde = !(factionEntry->hostileMask & 4);
|
||||||
|
bool forAlliance = !(factionEntry->hostileMask & 2);
|
||||||
|
|
||||||
|
if (!forHorde && !forAlliance)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
NpcLocation aLoc;
|
||||||
|
aLoc.loc = WorldLocation(mapId, x + cos(orient) * 3.0f, y + sin(orient) * 3.0f, z + 0.5f, orient + M_PI);
|
||||||
|
aLoc.entry = templateEntry;
|
||||||
|
|
||||||
|
if (forHorde)
|
||||||
|
hordeAuctioneerCache[mapId][areaId].push_back(aLoc);
|
||||||
|
|
||||||
|
if (forAlliance)
|
||||||
|
allianceAuctioneerCache[mapId][areaId].push_back(aLoc);
|
||||||
|
|
||||||
|
auctioneerCount++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process temporary caches
|
// Process temporary caches
|
||||||
for (auto const& [gridTuple, creatureDataList] : tempLocsCache)
|
for (auto const& [gridTuple, creatureDataList] : tempLocsCache)
|
||||||
{
|
{
|
||||||
if (creatureDataList.size() > 2)
|
if (creatureDataList.size() >= 2)
|
||||||
{
|
{
|
||||||
CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureDataList[0].id1);
|
CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureDataList[0].id1);
|
||||||
uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2;
|
uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2;
|
||||||
|
|
||||||
|
float totalX = 0.0f;
|
||||||
|
float totalY = 0.0f;
|
||||||
|
float totalZ = 0.0f;
|
||||||
|
for (CreatureData const& creatureData : creatureDataList)
|
||||||
|
{
|
||||||
|
totalX += creatureData.posX;
|
||||||
|
totalY += creatureData.posY;
|
||||||
|
totalZ += creatureData.posZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
float avgX = totalX / creatureDataList.size();
|
||||||
|
float avgY = totalY / creatureDataList.size();
|
||||||
|
float avgZ = totalZ / creatureDataList.size();
|
||||||
|
uint32 mapId = std::get<0>(gridTuple);
|
||||||
|
|
||||||
for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel;
|
for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel;
|
||||||
l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++)
|
l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++)
|
||||||
{
|
{
|
||||||
if (l < 1 || l > maxLevel)
|
if (l < 1 || l > maxLevel)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
locsPerLevelCache[(uint8)l].push_back(WorldLocation(std::get<0>(gridTuple)));
|
locsPerLevelCache[(uint8)l].push_back(WorldLocation(mapId, avgX, avgY, avgZ, 0.0f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4812,5 +4863,5 @@ void TravelMgr::PrepareDestinationCache()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LOG_INFO("playerbots", ">> {} flight masters and {} innkeepers and {} banker locations for level collected.", flightMastersCount, innkeepersCount, bankerCount);
|
LOG_INFO("playerbots", ">> {} flight masters, {} innkeepers, {} bankers, {} auctioneers collected.", flightMastersCount, innkeepersCount, bankerCount, auctioneerCount);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
#define _PLAYERBOT_TRAVELMGR_H
|
#define _PLAYERBOT_TRAVELMGR_H
|
||||||
|
|
||||||
#include <boost/functional/hash.hpp>
|
#include <boost/functional/hash.hpp>
|
||||||
|
#include <cmath>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <random>
|
#include <random>
|
||||||
|
|
||||||
@ -268,12 +269,6 @@ public:
|
|||||||
std::vector<mGridCoord> getmGridCoords(WorldPosition secondPos);
|
std::vector<mGridCoord> getmGridCoords(WorldPosition secondPos);
|
||||||
std::vector<WorldPosition> frommGridCoord(mGridCoord GridCoord);
|
std::vector<WorldPosition> frommGridCoord(mGridCoord GridCoord);
|
||||||
|
|
||||||
void loadMapAndVMap(uint32 mapId, uint8 x, uint8 y);
|
|
||||||
|
|
||||||
void loadMapAndVMap() { loadMapAndVMap(GetMapId(), getmGridCoord().first, getmGridCoord().second); }
|
|
||||||
|
|
||||||
void loadMapAndVMaps(WorldPosition secondPos);
|
|
||||||
|
|
||||||
// Display functions
|
// Display functions
|
||||||
WorldPosition getDisplayLocation();
|
WorldPosition getDisplayLocation();
|
||||||
float getDisplayX() { return getDisplayLocation().GetPositionY() * -1.0; }
|
float getDisplayX() { return getDisplayLocation().GetPositionY() * -1.0; }
|
||||||
@ -297,10 +292,26 @@ public:
|
|||||||
|
|
||||||
std::vector<WorldPosition> getPathTo(WorldPosition endPos, Unit* bot) { return endPos.getPathFrom(*this, bot); }
|
std::vector<WorldPosition> getPathTo(WorldPosition endPos, Unit* bot) { return endPos.getPathFrom(*this, bot); }
|
||||||
|
|
||||||
bool isPathTo(std::vector<WorldPosition> path, float maxDistance = sPlayerbotAIConfig.targetPosRecalcDistance)
|
// Cmangos-aligned (WorldPosition.h:317): the path "reaches" this
|
||||||
|
// position when its last point is on the same map, within
|
||||||
|
// maxDistance horizontally, and within maxZDistance vertically.
|
||||||
|
// 3D Euclidean distance would falsely accept paths that end the
|
||||||
|
// right horizontal distance from us but on a roof/floor below.
|
||||||
|
// maxDistance == 0 falls back to targetPosRecalcDistance (0.1y).
|
||||||
|
bool isPathTo(std::vector<WorldPosition> const& path, float const maxDistance = 0.0f,
|
||||||
|
float const maxZDistance = 2.0f) const
|
||||||
{
|
{
|
||||||
return !path.empty() && distance(path.back()) < maxDistance;
|
if (path.empty())
|
||||||
};
|
return false;
|
||||||
|
WorldPosition const& back = path.back();
|
||||||
|
if (back.GetMapId() != GetMapId())
|
||||||
|
return false;
|
||||||
|
float const realMax = maxDistance > 0.0f ? maxDistance
|
||||||
|
: sPlayerbotAIConfig.targetPosRecalcDistance;
|
||||||
|
if (GetExactDist2dSq(&back) >= realMax * realMax)
|
||||||
|
return false;
|
||||||
|
return std::fabs(back.GetPositionZ() - GetPositionZ()) < maxZDistance;
|
||||||
|
}
|
||||||
bool cropPathTo(std::vector<WorldPosition>& path, float maxDistance = sPlayerbotAIConfig.targetPosRecalcDistance);
|
bool cropPathTo(std::vector<WorldPosition>& path, float maxDistance = sPlayerbotAIConfig.targetPosRecalcDistance);
|
||||||
bool canPathTo(WorldPosition endPos, Unit* bot) { return endPos.isPathTo(getPathTo(endPos, bot)); }
|
bool canPathTo(WorldPosition endPos, Unit* bot) { return endPos.isPathTo(getPathTo(endPos, bot)); }
|
||||||
|
|
||||||
@ -507,9 +518,15 @@ public:
|
|||||||
radiusMin = radiusMin1;
|
radiusMin = radiusMin1;
|
||||||
radiusMax = radiusMax1;
|
radiusMax = radiusMax1;
|
||||||
}
|
}
|
||||||
virtual ~TravelDestination() = default;
|
virtual ~TravelDestination();
|
||||||
|
|
||||||
void addPoint(WorldPosition* pos) { points.push_back(pos); }
|
void addPoint(WorldPosition* pos)
|
||||||
|
{
|
||||||
|
if (!pos)
|
||||||
|
return;
|
||||||
|
|
||||||
|
points.push_back(new WorldPosition(*pos));
|
||||||
|
}
|
||||||
|
|
||||||
void setExpireDelay(uint32 delay) { expireDelay = delay; }
|
void setExpireDelay(uint32 delay) { expireDelay = delay; }
|
||||||
|
|
||||||
@ -673,7 +690,7 @@ public:
|
|||||||
bool isActive(Player* bot) override;
|
bool isActive(Player* bot) override;
|
||||||
virtual CreatureTemplate const* GetCreatureTemplate();
|
virtual CreatureTemplate const* GetCreatureTemplate();
|
||||||
std::string const getName() override { return "RpgTravelDestination"; }
|
std::string const getName() override { return "RpgTravelDestination"; }
|
||||||
int32 getEntry() override { return 0; }
|
int32 getEntry() override { return entry; }
|
||||||
std::string const getTitle() override;
|
std::string const getTitle() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@ -846,6 +863,21 @@ protected:
|
|||||||
class TravelMgr
|
class TravelMgr
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
struct NpcLocation
|
||||||
|
{
|
||||||
|
WorldLocation loc;
|
||||||
|
uint32 entry;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FlightMasterInfo
|
||||||
|
{
|
||||||
|
WorldPosition pos;
|
||||||
|
uint32 zoneId; // resolved once at cache load
|
||||||
|
uint32 taxiNodeId; // DBC taxi node nearest to this flight master
|
||||||
|
uint32 templateEntry; // creature template ID (for ObjectGuid construction)
|
||||||
|
uint32 dbGuid; // DB spawn GUID (for ObjectGuid construction)
|
||||||
|
};
|
||||||
|
|
||||||
static TravelMgr& instance()
|
static TravelMgr& instance()
|
||||||
{
|
{
|
||||||
static TravelMgr instance;
|
static TravelMgr instance;
|
||||||
@ -858,12 +890,14 @@ public:
|
|||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
void Init();
|
void Init();
|
||||||
Creature* GetNearestFlightMaster(Player* bot);
|
|
||||||
ObjectGuid GetNearestFlightMasterGuid(Player* bot);
|
FlightMasterInfo const* GetNearestFlightMasterInfo(Player* bot) const;
|
||||||
std::vector<std::vector<uint32>> GetOptimalFlightDestinations(Player* bot);
|
std::vector<std::vector<uint32>> GetOptimalFlightDestinations(Player* bot);
|
||||||
const std::vector<WorldLocation> GetTeleportLocations(Player* bot);
|
const std::vector<WorldLocation> GetTeleportLocations(Player* bot);
|
||||||
const std::vector<WorldLocation> GetTravelHubs(Player* bot);
|
const std::vector<WorldLocation> GetTravelHubs(Player* bot);
|
||||||
std::vector<WorldLocation> GetCityLocations(Player* bot);
|
std::vector<WorldLocation> GetCityLocations(Player* bot);
|
||||||
|
std::vector<uint32> GetFlightNodesInZone(uint32 zoneId, TeamId team, uint32 excludeNode = 0) const;
|
||||||
|
bool SelectAuctioneerByMap(Player* bot, NpcLocation& outAuctioneer);
|
||||||
const std::vector<WorldLocation>& GetLocsPerLevelCache(uint8 level) { return locsPerLevelCache[level]; }
|
const std::vector<WorldLocation>& GetLocsPerLevelCache(uint8 level) { return locsPerLevelCache[level]; }
|
||||||
|
|
||||||
template <class D, class W, class URBG>
|
template <class D, class W, class URBG>
|
||||||
@ -968,18 +1002,14 @@ private:
|
|||||||
bool InsideBracket(uint32 val) const { return val >= low && val <= high; }
|
bool InsideBracket(uint32 val) const { return val >= low && val <= high; }
|
||||||
};
|
};
|
||||||
|
|
||||||
struct BankerLocation
|
|
||||||
{
|
|
||||||
WorldLocation loc;
|
|
||||||
uint32 entry;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Navigation caches
|
// Navigation caches
|
||||||
std::map<uint32, WorldPosition> allianceFlightMasterCache;
|
std::map<uint32, FlightMasterInfo> allianceFlightMasterCache;
|
||||||
std::map<uint32, WorldPosition> hordeFlightMasterCache;
|
std::map<uint32, FlightMasterInfo> hordeFlightMasterCache;
|
||||||
std::map<uint8, std::vector<WorldLocation>> allianceHubsPerLevelCache;
|
std::map<uint8, std::vector<WorldLocation>> allianceHubsPerLevelCache;
|
||||||
std::map<uint8, std::vector<WorldLocation>> hordeHubsPerLevelCache;
|
std::map<uint8, std::vector<WorldLocation>> hordeHubsPerLevelCache;
|
||||||
std::map<uint8, std::vector<BankerLocation>> bankerLocsPerLevelCache;
|
std::map<uint8, std::vector<NpcLocation>> bankerLocsPerLevelCache;
|
||||||
|
std::unordered_map<uint16, std::unordered_map<uint32, std::vector<NpcLocation>>> hordeAuctioneerCache;
|
||||||
|
std::unordered_map<uint16, std::unordered_map<uint32, std::vector<NpcLocation>>> allianceAuctioneerCache;
|
||||||
std::unordered_map<uint32, WorldLocation> bankerEntryToLocation;
|
std::unordered_map<uint32, WorldLocation> bankerEntryToLocation;
|
||||||
std::map<uint8, std::vector<WorldLocation>> locsPerLevelCache;
|
std::map<uint8, std::vector<WorldLocation>> locsPerLevelCache;
|
||||||
std::unordered_map<uint32, std::vector<WorldLocation>> creatureSpawnsByTemplate;
|
std::unordered_map<uint32, std::vector<WorldLocation>> creatureSpawnsByTemplate;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -8,11 +8,12 @@
|
|||||||
|
|
||||||
#include <shared_mutex>
|
#include <shared_mutex>
|
||||||
|
|
||||||
|
#include "G3D/Vector3.h"
|
||||||
#include "TravelMgr.h"
|
#include "TravelMgr.h"
|
||||||
|
|
||||||
// THEORY
|
// THEORY
|
||||||
//
|
//
|
||||||
// Pathfinding in (c)mangos is based on detour recast an opensource nashmesh creation and pathfinding codebase.
|
// Pathfinding in (c)mangos is based on detour recast, an opensource navmesh creation and pathfinding codebase.
|
||||||
// This system is used for mob and npc pathfinding and in this codebase also for bots.
|
// This system is used for mob and npc pathfinding and in this codebase also for bots.
|
||||||
// Because mobs and npc movement is based on following a player or a set path the PathGenerator is limited to 296y.
|
// Because mobs and npc movement is based on following a player or a set path the PathGenerator is limited to 296y.
|
||||||
// This means that when trying to find a path from A to B distances beyond 296y will be a best guess often moving in a
|
// This means that when trying to find a path from A to B distances beyond 296y will be a best guess often moving in a
|
||||||
@ -24,33 +25,68 @@
|
|||||||
// <S> ---> [N1] ---> [N2] ---> [N3] ---> <E>
|
// <S> ---> [N1] ---> [N2] ---> [N3] ---> <E>
|
||||||
//
|
//
|
||||||
// Bot at <S> wants to move to <E>
|
// Bot at <S> wants to move to <E>
|
||||||
// [N1],[N2],[N3] are predefined nodes for wich we know we can move from [N1] to [N2] and from [N2] to [N3] but not
|
// [N1],[N2],[N3] are predefined nodes for which we know we can move from [N1] to [N2] and from [N2] to [N3] but not
|
||||||
// from [N1] to [N3] If we can move fom [S] to [N1] and from [N3] to [E] we have a complete route to travel.
|
// from [N1] to [N3]. If we can move from [S] to [N1] and from [N3] to [E] we have a complete route to travel.
|
||||||
//
|
//
|
||||||
// Termonology:
|
// Terminology:
|
||||||
// Node: a location on a map for which we know bots are likely to want to travel to or need to travel past to reach
|
// Node: A location on a map for which we know bots are likely to want to travel to or need to travel past to reach
|
||||||
// other nodes. Link: the connection between two nodes. A link signifies that the bot can travel from one node to
|
// other nodes. Stored in DB table `playerbots_travelnode`.
|
||||||
// another. A link is one-directional. Path: the waypointpath returned by the standard PathGenerator to move from one
|
// Link: The connection between two nodes. A link signifies that the bot can travel from one node to another.
|
||||||
// node (or position) to another. A path can be imcomplete or empty which means there is no link. Route: the list of
|
// A link is one-directional. Stored in `playerbots_travelnode_link`.
|
||||||
// nodes that give the shortest route from a node to a distant node. Routes are calculated using a standard A* search
|
// Path: The waypoint path returned by the standard PathGenerator to move from one node (or position) to another.
|
||||||
// based on links.
|
// A path can be incomplete or empty which means there is no link. Stored in `playerbots_travelnode_path`.
|
||||||
|
// Route: The list of nodes that give the shortest route from a node to a distant node. Routes are calculated using
|
||||||
|
// a standard A* search based on links.
|
||||||
//
|
//
|
||||||
// On server start saved nodes and links are loaded. Paths and routes are calculated on the fly but saved for future
|
// Edge types (TravelNodePathType):
|
||||||
// use. Nodes can be added and removed realtime however because bots access the nodes from different threads this
|
// walk(1) — Walk via navmesh waypoints (stored in DB)
|
||||||
// requires a locking mechanism.
|
// portal(2) — AreaTrigger teleport (auto-discovered at startup)
|
||||||
|
// transport(3) — Boat/zeppelin (auto-discovered from MO_TRANSPORT)
|
||||||
|
// flightPath(4) — Taxi flight between flight masters
|
||||||
|
// teleportSpell(5) — Spell-based teleport (e.g. mage portals)
|
||||||
|
// staticPortal(6) — Manually defined teleport link (DB only, not pruned by generation)
|
||||||
|
// flyingMount (7) — Use Bots Flying mount to travel (Not currently enabled)
|
||||||
|
//
|
||||||
|
// On server start saved nodes and links are loaded via TravelNodeMap::Init(). An index of nodes by zone is prepared
|
||||||
|
// (instead of scanning all ~4000 nodes), precomputes connected components for O(1) reachability checks, and builds
|
||||||
|
// a taxi BFS graph. Paths and routes are calculated on the fly and saved for future use. Nodes are only added at
|
||||||
|
// startup or via the console `.generate` command — runtime mutation was removed because taking a unique_lock
|
||||||
|
// caused 100-250ms contention spikes against bot threads.
|
||||||
//
|
//
|
||||||
// Initially the current nodes have been made:
|
// Initially the current nodes have been made:
|
||||||
// Flightmasters and Inns (Bots can use these to fast-travel so eventually they will be included in the route
|
// Flightmasters and Inns (Bots can use these to fast-travel so eventually they will be included in the route
|
||||||
// calculation) WorldBosses and Unique bosses in instances (These are a logical places bots might want to go in
|
// calculation) WorldBosses and Unique bosses in instances (These are logical places bots might want to go in
|
||||||
// instances) Player start spawns (Obviously all lvl1 bots will spawn and move from here) Area triggers locations with
|
// instances) Player start spawns (Obviously all lvl1 bots will spawn and move from here) Area triggers locations with
|
||||||
// teleport and their teleport destinations (These used to travel in or between maps) Transports including elevators
|
// teleport and their teleport destinations (These used to travel in or between maps) Transports including elevators
|
||||||
// (Again used to travel in and in maps) (sub)Zone means (These are the center most point for each sub-zone which is
|
// (Again used to travel in and in maps) (sub)Zone means (These are the center most point for each sub-zone which is
|
||||||
// good for global coverage)
|
// good for global coverage).
|
||||||
//
|
//
|
||||||
// To increase coverage/linking extra nodes can be automatically be created.
|
// To increase coverage/linking extra nodes must be manually created via the "playerbot travel generatenode"
|
||||||
// Current implentation places nodes on paths (including complete) at sub-zone transitions or randomly.
|
// console command after importing the specified node. Current implementation places nodes on paths (including
|
||||||
// After calculating possible links the node is removed if it does not create local coverage.
|
// complete) at sub-zone transitions or randomly. After calculating possible links the node is removed if it
|
||||||
|
// does not create local coverage (.fullgenerate only).
|
||||||
//
|
//
|
||||||
|
// Travel Flow:
|
||||||
|
//
|
||||||
|
// GetFullPath finds nearest nodes (zone-indexed), runs A* to get a node route, then
|
||||||
|
// BuildPath assembles a flat TravelPath with typed waypoints (walk, portal, transport, flight).
|
||||||
|
// ExecuteTravelPlan iterates the path by stepIdx, dispatching on each point's PathNodeType.
|
||||||
|
// Cross-map travel is handled naturally by portal/transport edges in the A* graph.
|
||||||
|
//
|
||||||
|
// If setup cannot resolve (no node, no route, no flight), the bot teleports directly to the destination
|
||||||
|
// as a fallback.
|
||||||
|
//
|
||||||
|
// The use of hearthstones and mage teleporting was removed — it caused route mutations requiring locking that no longer made sense. Mage portals may be future item.
|
||||||
|
//
|
||||||
|
// Thread Safety:
|
||||||
|
//
|
||||||
|
// The node graph is immutable at runtime (no adds/removes after Init). A shared_timed_mutex (m_nMapMtx) still
|
||||||
|
// exists and shared_locks are taken in GetFullPath and GenerateWalkPath for safety, but since there are no
|
||||||
|
// runtime mutations these are effectively uncontested. The only exclusive locks are taken at startup
|
||||||
|
// (saveNodeStore) and by the debug dump command.
|
||||||
|
//
|
||||||
|
|
||||||
|
constexpr float MAX_PATHFINDING_DISTANCE = 296.0f;
|
||||||
|
|
||||||
enum class TravelNodePathType : uint8
|
enum class TravelNodePathType : uint8
|
||||||
{
|
{
|
||||||
@ -59,21 +95,20 @@ enum class TravelNodePathType : uint8
|
|||||||
portal = 2,
|
portal = 2,
|
||||||
transport = 3,
|
transport = 3,
|
||||||
flightPath = 4,
|
flightPath = 4,
|
||||||
teleportSpell = 5
|
teleportSpell = 5,
|
||||||
|
staticPortal = 6,
|
||||||
|
flyingMount = 7
|
||||||
};
|
};
|
||||||
|
|
||||||
// A connection between two nodes.
|
// A connection between two nodes.
|
||||||
class TravelNodePath
|
class TravelNodePath
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
// Legacy Constructor for travelnodestore
|
|
||||||
// TravelNodePath(float distance1, float extraCost1, bool portal1 = false, uint32 portalId1 = 0, bool transport1 =
|
|
||||||
// false, bool calculated = false, uint8 maxLevelMob1 = 0, uint8 maxLevelAlliance1 = 0, uint8 maxLevelHorde1 = 0,
|
|
||||||
// float swimDistance1 = 0, bool flightPath1 = false);
|
|
||||||
|
|
||||||
// Constructor
|
// Constructor
|
||||||
TravelNodePath(float distance = 0.1f, float extraCost = 0, uint8 pathType = (uint8)TravelNodePathType::walk,
|
TravelNodePath(float distance = 0.1f, float extraCost = 0,
|
||||||
uint32 pathObject = 0, bool calculated = false, std::vector<uint8> maxLevelCreature = {0, 0, 0},
|
uint8 pathType = (uint8)TravelNodePathType::walk,
|
||||||
|
uint32 pathObject = 0, bool calculated = false,
|
||||||
|
std::vector<uint8> maxLevelCreature = {0, 0, 0},
|
||||||
float swimDistance = 0)
|
float swimDistance = 0)
|
||||||
: extraCost(extraCost),
|
: extraCost(extraCost),
|
||||||
calculated(calculated),
|
calculated(calculated),
|
||||||
@ -85,7 +120,7 @@ public:
|
|||||||
{
|
{
|
||||||
if (pathType != (uint8)TravelNodePathType::walk)
|
if (pathType != (uint8)TravelNodePathType::walk)
|
||||||
complete = true;
|
complete = true;
|
||||||
};
|
}
|
||||||
|
|
||||||
TravelNodePath(TravelNodePath* basePath)
|
TravelNodePath(TravelNodePath* basePath)
|
||||||
{
|
{
|
||||||
@ -98,11 +133,11 @@ public:
|
|||||||
swimDistance = basePath->swimDistance;
|
swimDistance = basePath->swimDistance;
|
||||||
pathType = basePath->pathType;
|
pathType = basePath->pathType;
|
||||||
pathObject = basePath->pathObject;
|
pathObject = basePath->pathObject;
|
||||||
};
|
}
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
bool getComplete() { return complete || pathType != TravelNodePathType::walk; }
|
bool getComplete() { return complete || pathType != TravelNodePathType::walk; }
|
||||||
std::vector<WorldPosition> getPath() { return path; }
|
std::vector<WorldPosition> GetPath() { return path; }
|
||||||
|
|
||||||
TravelNodePathType getPathType() { return pathType; }
|
TravelNodePathType getPathType() { return pathType; }
|
||||||
uint32 getPathObject() { return pathObject; }
|
uint32 getPathObject() { return pathObject; }
|
||||||
@ -130,9 +165,6 @@ public:
|
|||||||
extraCost = distance / speed;
|
extraCost = distance / speed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// void setPortal(bool portal1, uint32 portalId1 = 0) { portal = portal1; portalId = portalId1; }
|
|
||||||
// void setTransport(bool transport1) { transport = transport1; }
|
|
||||||
|
|
||||||
void setPathType(TravelNodePathType pathType1) { pathType = pathType1; }
|
void setPathType(TravelNodePathType pathType1) { pathType = pathType1; }
|
||||||
|
|
||||||
void setPathObject(uint32 pathObject1) { pathObject = pathObject1; }
|
void setPathObject(uint32 pathObject1) { pathObject = pathObject1; }
|
||||||
@ -186,9 +218,10 @@ class TravelNode
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
// Constructors
|
// Constructors
|
||||||
TravelNode(){};
|
TravelNode() {}
|
||||||
|
|
||||||
TravelNode(WorldPosition point1, std::string const nodeName1 = "Travel Node", bool important1 = false)
|
TravelNode(WorldPosition point1, std::string const nodeName1 = "Travel Node",
|
||||||
|
bool important1 = false)
|
||||||
{
|
{
|
||||||
nodeName = nodeName1;
|
nodeName = nodeName1;
|
||||||
point = point1;
|
point = point1;
|
||||||
@ -207,11 +240,11 @@ public:
|
|||||||
void setPoint(WorldPosition point1) { point = point1; }
|
void setPoint(WorldPosition point1) { point = point1; }
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
std::string const getName() { return nodeName; };
|
std::string const getName() { return nodeName; }
|
||||||
WorldPosition* getPosition() { return &point; };
|
WorldPosition* getPosition() { return &point; }
|
||||||
std::unordered_map<TravelNode*, TravelNodePath>* getPaths() { return &paths; }
|
std::unordered_map<TravelNode*, TravelNodePath>* getPaths() { return &paths; }
|
||||||
std::unordered_map<TravelNode*, TravelNodePath*>* getLinks() { return &links; }
|
std::unordered_map<TravelNode*, TravelNodePath*>* getLinks() { return &links; }
|
||||||
bool isImportant() { return important; };
|
bool isImportant() { return important; }
|
||||||
bool isLinked() { return linked; }
|
bool isLinked() { return linked; }
|
||||||
|
|
||||||
bool isTransport()
|
bool isTransport()
|
||||||
@ -235,7 +268,8 @@ public:
|
|||||||
bool isPortal()
|
bool isPortal()
|
||||||
{
|
{
|
||||||
for (auto const& link : *getLinks())
|
for (auto const& link : *getLinks())
|
||||||
if (link.second->getPathType() == TravelNodePathType::portal)
|
if (link.second->getPathType() == TravelNodePathType::portal ||
|
||||||
|
link.second->getPathType() == TravelNodePathType::staticPortal)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -251,17 +285,25 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WorldLocation shortcuts
|
// WorldLocation shortcuts
|
||||||
uint32 getMapId() { return point.GetMapId(); }
|
uint32 GetMapId() { return point.GetMapId(); }
|
||||||
float getX() { return point.GetPositionX(); }
|
float getX() { return point.GetPositionX(); }
|
||||||
float getY() { return point.GetPositionY(); }
|
float getY() { return point.GetPositionY(); }
|
||||||
float getZ() { return point.GetPositionZ(); }
|
float getZ() { return point.GetPositionZ(); }
|
||||||
float getO() { return point.GetOrientation(); }
|
float getO() { return point.GetOrientation(); }
|
||||||
float getDistance(WorldPosition pos) { return point.distance(pos); }
|
float getDistance(WorldPosition pos) { return point.distance(pos); }
|
||||||
float getDistance(TravelNode* node) { return point.distance(node->getPosition()); }
|
float getDistance(TravelNode* node)
|
||||||
float fDist(TravelNode* node) { return point.fDist(node->getPosition()); }
|
{
|
||||||
|
return point.distance(node->getPosition());
|
||||||
|
}
|
||||||
|
float fDist(TravelNode* node)
|
||||||
|
{
|
||||||
|
return point.fDist(node->getPosition());
|
||||||
|
}
|
||||||
float fDist(WorldPosition pos) { return point.fDist(pos); }
|
float fDist(WorldPosition pos) { return point.fDist(pos); }
|
||||||
|
|
||||||
TravelNodePath* setPathTo(TravelNode* node, TravelNodePath path = TravelNodePath(), bool isLink = true)
|
TravelNodePath* setPathTo(TravelNode* node,
|
||||||
|
TravelNodePath path = TravelNodePath(),
|
||||||
|
bool isLink = true)
|
||||||
{
|
{
|
||||||
if (this != node)
|
if (this != node)
|
||||||
{
|
{
|
||||||
@ -275,10 +317,20 @@ public:
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasPathTo(TravelNode* node) { return paths.find(node) != paths.end(); }
|
bool hasPathTo(TravelNode* node)
|
||||||
TravelNodePath* getPathTo(TravelNode* node) { return &paths[node]; }
|
{
|
||||||
bool hasCompletePathTo(TravelNode* node) { return hasPathTo(node) && getPathTo(node)->getComplete(); }
|
return paths.find(node) != paths.end();
|
||||||
TravelNodePath* buildPath(TravelNode* endNode, Unit* bot, bool postProcess = false);
|
}
|
||||||
|
TravelNodePath* getPathTo(TravelNode* node)
|
||||||
|
{
|
||||||
|
return &paths[node];
|
||||||
|
}
|
||||||
|
bool hasCompletePathTo(TravelNode* node)
|
||||||
|
{
|
||||||
|
return hasPathTo(node) && getPathTo(node)->getComplete();
|
||||||
|
}
|
||||||
|
TravelNodePath* BuildPath(TravelNode* endNode, Unit* bot,
|
||||||
|
bool postProcess = false);
|
||||||
|
|
||||||
void setLinkTo(TravelNode* node, float distance = 0.1f)
|
void setLinkTo(TravelNode* node, float distance = 0.1f)
|
||||||
{
|
{
|
||||||
@ -291,9 +343,18 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasLinkTo(TravelNode* node) { return links.find(node) != links.end(); }
|
bool hasLinkTo(TravelNode* node)
|
||||||
float linkCostTo(TravelNode* node) { return paths.find(node)->second.getDistance(); }
|
{
|
||||||
float linkDistanceTo(TravelNode* node) { return paths.find(node)->second.getDistance(); }
|
return links.find(node) != links.end();
|
||||||
|
}
|
||||||
|
float linkCostTo(TravelNode* node)
|
||||||
|
{
|
||||||
|
return paths.find(node)->second.getDistance();
|
||||||
|
}
|
||||||
|
float linkDistanceTo(TravelNode* node)
|
||||||
|
{
|
||||||
|
return paths.find(node)->second.getDistance();
|
||||||
|
}
|
||||||
void removeLinkTo(TravelNode* node, bool removePaths = false);
|
void removeLinkTo(TravelNode* node, bool removePaths = false);
|
||||||
|
|
||||||
bool isEqual(TravelNode* compareNode);
|
bool isEqual(TravelNode* compareNode);
|
||||||
@ -304,7 +365,8 @@ public:
|
|||||||
bool cropUselessLinks();
|
bool cropUselessLinks();
|
||||||
|
|
||||||
// Returns all nodes that can be reached from this node.
|
// Returns all nodes that can be reached from this node.
|
||||||
std::vector<TravelNode*> getNodeMap(bool importantOnly = false, std::vector<TravelNode*> ignoreNodes = {});
|
std::vector<TravelNode*> getNodeMap(bool importantOnly = false,
|
||||||
|
std::vector<TravelNode*> ignoreNodes = {});
|
||||||
|
|
||||||
// Checks if it is even possible to route to this node.
|
// Checks if it is even possible to route to this node.
|
||||||
bool hasRouteTo(TravelNode* node)
|
bool hasRouteTo(TravelNode* node)
|
||||||
@ -314,7 +376,10 @@ public:
|
|||||||
routes[mNode] = true;
|
routes[mNode] = true;
|
||||||
|
|
||||||
return routes.find(node) != routes.end();
|
return routes.find(node) != routes.end();
|
||||||
};
|
}
|
||||||
|
|
||||||
|
void clearRoutes() { routes.clear(); }
|
||||||
|
void setRouteTo(TravelNode* node) { routes[node] = true; }
|
||||||
|
|
||||||
void print(bool printFailed = true);
|
void print(bool printFailed = true);
|
||||||
|
|
||||||
@ -344,24 +409,8 @@ protected:
|
|||||||
// uint32 transportId = 0;
|
// uint32 transportId = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class PortalNode : public TravelNode
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
PortalNode(TravelNode* baseNode) : TravelNode(baseNode){};
|
|
||||||
|
|
||||||
void SetPortal(TravelNode* baseNode, TravelNode* endNode, uint32 portalSpell)
|
|
||||||
{
|
|
||||||
nodeName = baseNode->getName();
|
|
||||||
point = *baseNode->getPosition();
|
|
||||||
paths.clear();
|
|
||||||
links.clear();
|
|
||||||
TravelNodePath path(0.1f, 0.1f, (uint8)TravelNodePathType::teleportSpell, portalSpell, true);
|
|
||||||
setPathTo(endNode, path);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Route step type
|
// Route step type
|
||||||
enum PathNodeType
|
enum class PathNodeType : uint8
|
||||||
{
|
{
|
||||||
NODE_PREPATH = 0,
|
NODE_PREPATH = 0,
|
||||||
NODE_PATH = 1,
|
NODE_PATH = 1,
|
||||||
@ -369,13 +418,14 @@ enum PathNodeType
|
|||||||
NODE_PORTAL = 3,
|
NODE_PORTAL = 3,
|
||||||
NODE_TRANSPORT = 4,
|
NODE_TRANSPORT = 4,
|
||||||
NODE_FLIGHTPATH = 5,
|
NODE_FLIGHTPATH = 5,
|
||||||
NODE_TELEPORT = 6
|
NODE_TELEPORT = 6,
|
||||||
|
NODE_FLYING_MOUNT = 7
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PathNodePoint
|
struct PathNodePoint
|
||||||
{
|
{
|
||||||
WorldPosition point;
|
WorldPosition point;
|
||||||
PathNodeType type = NODE_PATH;
|
PathNodeType type = PathNodeType::NODE_PATH;
|
||||||
uint32 entry = 0;
|
uint32 entry = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -383,24 +433,31 @@ struct PathNodePoint
|
|||||||
class TravelPath
|
class TravelPath
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
TravelPath(){};
|
TravelPath() {}
|
||||||
TravelPath(std::vector<PathNodePoint> fullPath1) { fullPath = fullPath1; }
|
TravelPath(std::vector<PathNodePoint> fullPath1)
|
||||||
TravelPath(std::vector<WorldPosition> path, PathNodeType type = NODE_PATH, uint32 entry = 0)
|
{
|
||||||
|
fullPath = fullPath1;
|
||||||
|
}
|
||||||
|
TravelPath(std::vector<WorldPosition> path,
|
||||||
|
PathNodeType type = PathNodeType::NODE_PATH,
|
||||||
|
uint32 entry = 0)
|
||||||
{
|
{
|
||||||
addPath(path, type, entry);
|
addPath(path, type, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addPoint(PathNodePoint point) { fullPath.push_back(point); }
|
void addPoint(PathNodePoint point) { fullPath.push_back(point); }
|
||||||
void addPoint(WorldPosition point, PathNodeType type = NODE_PATH, uint32 entry = 0)
|
void addPoint(WorldPosition point,
|
||||||
|
PathNodeType type = PathNodeType::NODE_PATH,
|
||||||
|
uint32 entry = 0)
|
||||||
{
|
{
|
||||||
fullPath.push_back(PathNodePoint{point, type, entry});
|
fullPath.push_back(PathNodePoint{point, type, entry});
|
||||||
}
|
}
|
||||||
void addPath(std::vector<WorldPosition> path, PathNodeType type = NODE_PATH, uint32 entry = 0)
|
void addPath(std::vector<WorldPosition> path,
|
||||||
|
PathNodeType type = PathNodeType::NODE_PATH,
|
||||||
|
uint32 entry = 0)
|
||||||
{
|
{
|
||||||
for (auto& p : path)
|
for (auto& p : path)
|
||||||
{
|
|
||||||
fullPath.push_back(PathNodePoint{p, type, entry});
|
fullPath.push_back(PathNodePoint{p, type, entry});
|
||||||
};
|
|
||||||
}
|
}
|
||||||
void addPath(std::vector<PathNodePoint> newPath)
|
void addPath(std::vector<PathNodePoint> newPath)
|
||||||
{
|
{
|
||||||
@ -408,8 +465,11 @@ public:
|
|||||||
}
|
}
|
||||||
void clear() { fullPath.clear(); }
|
void clear() { fullPath.clear(); }
|
||||||
|
|
||||||
bool empty() { return fullPath.empty(); }
|
bool empty() const { return fullPath.empty(); }
|
||||||
std::vector<PathNodePoint> getPath() { return fullPath; }
|
size_t size() const { return fullPath.size(); }
|
||||||
|
const PathNodePoint& operator[](size_t idx) const { return fullPath[idx]; }
|
||||||
|
std::vector<PathNodePoint> GetPath() { return fullPath; }
|
||||||
|
const std::vector<PathNodePoint>& GetPathRef() const { return fullPath; }
|
||||||
WorldPosition getFront() { return fullPath.front().point; }
|
WorldPosition getFront() { return fullPath.front().point; }
|
||||||
WorldPosition getBack() { return fullPath.back().point; }
|
WorldPosition getBack() { return fullPath.back().point; }
|
||||||
|
|
||||||
@ -419,13 +479,9 @@ public:
|
|||||||
for (auto const& p : fullPath)
|
for (auto const& p : fullPath)
|
||||||
retVec.push_back(p.point);
|
retVec.push_back(p.point);
|
||||||
return retVec;
|
return retVec;
|
||||||
};
|
}
|
||||||
|
|
||||||
bool makeShortCut(WorldPosition startPos, float maxDist);
|
bool makeShortCut(WorldPosition startPos, float maxDist);
|
||||||
bool shouldMoveToNextPoint(WorldPosition startPos, std::vector<PathNodePoint>::iterator beg,
|
|
||||||
std::vector<PathNodePoint>::iterator ed, std::vector<PathNodePoint>::iterator p,
|
|
||||||
float& moveDist, float maxDist);
|
|
||||||
WorldPosition getNextPoint(WorldPosition startPos, float maxDist, TravelNodePathType& pathType, uint32& entry);
|
|
||||||
|
|
||||||
std::ostringstream const print();
|
std::ostringstream const print();
|
||||||
|
|
||||||
@ -438,16 +494,24 @@ class TravelNodeRoute
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
TravelNodeRoute() {}
|
TravelNodeRoute() {}
|
||||||
TravelNodeRoute(std::vector<TravelNode*> nodes1) { nodes = nodes1; /*currentNode = route.begin();*/ }
|
TravelNodeRoute(std::vector<TravelNode*> nodes1)
|
||||||
|
{
|
||||||
|
nodes = nodes1;
|
||||||
|
}
|
||||||
|
|
||||||
bool isEmpty() { return nodes.empty(); }
|
bool isEmpty() { return nodes.empty(); }
|
||||||
|
|
||||||
bool hasNode(TravelNode* node) { return findNode(node) != nodes.end(); }
|
bool hasNode(TravelNode* node)
|
||||||
|
{
|
||||||
|
return findNode(node) != nodes.end();
|
||||||
|
}
|
||||||
float getTotalDistance();
|
float getTotalDistance();
|
||||||
|
|
||||||
std::vector<TravelNode*> getNodes() { return nodes; }
|
std::vector<TravelNode*> getNodes() { return nodes; }
|
||||||
|
|
||||||
TravelPath buildPath(std::vector<WorldPosition> pathToStart = {}, std::vector<WorldPosition> pathToEnd = {},
|
TravelPath BuildPath(
|
||||||
|
std::vector<WorldPosition> pathToStart = {},
|
||||||
|
std::vector<WorldPosition> pathToEnd = {},
|
||||||
Unit* bot = nullptr);
|
Unit* bot = nullptr);
|
||||||
|
|
||||||
std::ostringstream const print();
|
std::ostringstream const print();
|
||||||
@ -467,12 +531,47 @@ public:
|
|||||||
TravelNodeStub(TravelNode* dataNode1) { dataNode = dataNode1; }
|
TravelNodeStub(TravelNode* dataNode1) { dataNode = dataNode1; }
|
||||||
|
|
||||||
TravelNode* dataNode;
|
TravelNode* dataNode;
|
||||||
float m_f = 0.0, m_g = 0.0, m_h = 0.0;
|
float totalCost = 0.0;
|
||||||
bool open = false, close = false;
|
float costFromStart = 0.0;
|
||||||
|
float heuristic = 0.0;
|
||||||
|
bool open = false;
|
||||||
|
bool closed = false;
|
||||||
TravelNodeStub* parent = nullptr;
|
TravelNodeStub* parent = nullptr;
|
||||||
uint32 currentGold = 0;
|
uint32 currentGold = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct TravelPlan
|
||||||
|
{
|
||||||
|
WorldPosition destination;
|
||||||
|
|
||||||
|
// Flat waypoint path built upfront by GetFullPath:
|
||||||
|
TravelPath steps;
|
||||||
|
uint32 stepIdx{0};
|
||||||
|
|
||||||
|
// Spline scratch (used by executor):
|
||||||
|
std::vector<G3D::Vector3> walkPoints;
|
||||||
|
bool splineActive{false};
|
||||||
|
uint32 splineStartTime{0};
|
||||||
|
uint32 expectedDuration{0};
|
||||||
|
|
||||||
|
// Taxi scratch:
|
||||||
|
std::vector<uint32> route;
|
||||||
|
|
||||||
|
bool IsActive() const { return !steps.empty(); }
|
||||||
|
|
||||||
|
void Reset()
|
||||||
|
{
|
||||||
|
destination = WorldPosition();
|
||||||
|
steps.clear();
|
||||||
|
stepIdx = 0;
|
||||||
|
walkPoints.clear();
|
||||||
|
splineActive = false;
|
||||||
|
splineStartTime = 0;
|
||||||
|
expectedDuration = 0;
|
||||||
|
route.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// The container of all nodes.
|
// The container of all nodes.
|
||||||
class TravelNodeMap
|
class TravelNodeMap
|
||||||
{
|
{
|
||||||
@ -484,14 +583,18 @@ public:
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
TravelNode* addNode(WorldPosition pos, std::string const preferedName = "Travel Node", bool isImportant = false,
|
TravelNode* addNode(WorldPosition pos,
|
||||||
bool checkDuplicate = true, bool transport = false, uint32 transportId = 0);
|
std::string const preferedName = "Travel Node",
|
||||||
|
bool isImportant = false,
|
||||||
|
bool checkDuplicate = true,
|
||||||
|
bool transport = false,
|
||||||
|
uint32 transportId = 0);
|
||||||
void removeNode(TravelNode* node);
|
void removeNode(TravelNode* node);
|
||||||
bool removeNodes()
|
bool removeNodes()
|
||||||
{
|
{
|
||||||
if (m_nMapMtx.try_lock_for(std::chrono::seconds(10)))
|
if (m_nMapMtx.try_lock_for(std::chrono::seconds(10)))
|
||||||
{
|
{
|
||||||
for (auto& node : m_nodes)
|
for (auto& node : nodes)
|
||||||
removeNode(node);
|
removeNode(node);
|
||||||
|
|
||||||
m_nMapMtx.unlock();
|
m_nMapMtx.unlock();
|
||||||
@ -499,28 +602,32 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
}
|
||||||
|
|
||||||
void fullLinkNode(TravelNode* startNode, Unit* bot);
|
void fullLinkNode(TravelNode* startNode, Unit* bot);
|
||||||
|
|
||||||
// Get all nodes
|
// Get all nodes
|
||||||
std::vector<TravelNode*> getNodes() { return m_nodes; }
|
std::vector<TravelNode*> getNodes() { return nodes; }
|
||||||
std::vector<TravelNode*> getNodes(WorldPosition pos, float range = -1);
|
std::vector<TravelNode*> getNodes(WorldPosition pos, float range = -1);
|
||||||
|
|
||||||
// Find nearest node.
|
// Find nearest node.
|
||||||
TravelNode* getNode(TravelNode* sameNode)
|
TravelNode* getNode(TravelNode* sameNode)
|
||||||
{
|
{
|
||||||
for (auto& node : m_nodes)
|
for (auto& node : nodes)
|
||||||
{
|
{
|
||||||
if (node->getName() == sameNode->getName() && node->getPosition() == sameNode->getPosition())
|
if (node->getName() == sameNode->getName()
|
||||||
|
&& node->getPosition() == sameNode->getPosition())
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
TravelNode* getNode(WorldPosition pos, std::vector<WorldPosition>& ppath, Unit* bot = nullptr, float range = -1);
|
TravelNode* getNode(WorldPosition pos,
|
||||||
TravelNode* getNode(WorldPosition pos, Unit* bot = nullptr, float range = -1)
|
std::vector<WorldPosition>& ppath,
|
||||||
|
Unit* bot = nullptr, float range = -1);
|
||||||
|
TravelNode* getNode(WorldPosition pos, Unit* bot = nullptr,
|
||||||
|
float range = -1)
|
||||||
{
|
{
|
||||||
std::vector<WorldPosition> ppath;
|
std::vector<WorldPosition> ppath;
|
||||||
return getNode(pos, ppath, bot, range);
|
return getNode(pos, ppath, bot, range);
|
||||||
@ -536,19 +643,17 @@ public:
|
|||||||
return rNodes[urand(0, rNodes.size() - 1)];
|
return rNodes[urand(0, rNodes.size() - 1)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finds the best nodePath between two nodes
|
// Finds the best nodePath between two nodes (A* over the node graph)
|
||||||
TravelNodeRoute getRoute(TravelNode* start, TravelNode* goal, Player* bot = nullptr);
|
TravelNodeRoute GetNodeRoute(TravelNode* start, TravelNode* goal,
|
||||||
|
Player* bot);
|
||||||
|
|
||||||
// Find the best node between two positions
|
// Picks the nearest start/end nodes for two world positions and runs A*
|
||||||
TravelNodeRoute getRoute(WorldPosition startPos, WorldPosition endPos, std::vector<WorldPosition>& startPath,
|
// over the node graph to return a full route between them.
|
||||||
|
TravelNodeRoute FindRouteNearestNodes(WorldPosition startPos,
|
||||||
|
WorldPosition endPos,
|
||||||
|
std::vector<WorldPosition>& startPath,
|
||||||
Player* bot = nullptr);
|
Player* bot = nullptr);
|
||||||
|
|
||||||
// Find the full path between those locations
|
|
||||||
static TravelPath getFullPath(WorldPosition startPos, WorldPosition endPos, Player* bot = nullptr);
|
|
||||||
|
|
||||||
// Manage/update nodes
|
|
||||||
void manageNodes(Unit* bot, bool mapFull = false);
|
|
||||||
|
|
||||||
void setHasToGen() { hasToGen = true; }
|
void setHasToGen() { hasToGen = true; }
|
||||||
|
|
||||||
void generateNpcNodes();
|
void generateNpcNodes();
|
||||||
@ -563,15 +668,17 @@ public:
|
|||||||
void removeUselessPaths();
|
void removeUselessPaths();
|
||||||
void calculatePathCosts();
|
void calculatePathCosts();
|
||||||
void generateTaxiPaths();
|
void generateTaxiPaths();
|
||||||
void generatePaths();
|
void generatePaths(bool fullGen = false);
|
||||||
|
|
||||||
void generateAll();
|
void generateAll();
|
||||||
|
|
||||||
|
void Init();
|
||||||
|
|
||||||
void printMap();
|
void printMap();
|
||||||
|
|
||||||
void printNodeStore();
|
void printNodeStore();
|
||||||
void saveNodeStore();
|
void saveNodeStore();
|
||||||
void loadNodeStore();
|
void LoadNodeStore();
|
||||||
|
|
||||||
bool cropUselessNode(TravelNode* startNode);
|
bool cropUselessNode(TravelNode* startNode);
|
||||||
TravelNode* addZoneLinkNode(TravelNode* startNode);
|
TravelNode* addZoneLinkNode(TravelNode* startNode);
|
||||||
@ -584,8 +691,24 @@ public:
|
|||||||
void InitTaxiGraph();
|
void InitTaxiGraph();
|
||||||
std::vector<uint32> FindTaxiPath(uint32 fromNode, uint32 toNode);
|
std::vector<uint32> FindTaxiPath(uint32 fromNode, uint32 toNode);
|
||||||
|
|
||||||
|
void BuildZoneIndex();
|
||||||
|
void PrecomputeReachability();
|
||||||
|
|
||||||
|
TravelNode* GetNearestNodeInZone(WorldPosition pos, uint32 zoneId);
|
||||||
|
TravelNode* GetNearestNodeOnMap(WorldPosition pos);
|
||||||
|
|
||||||
|
bool GetFullPath(TravelPlan& plan, WorldPosition botPos,
|
||||||
|
uint32 botZoneId, WorldPosition destination);
|
||||||
|
|
||||||
|
// Resolve A* route between two world positions (returns node vector)
|
||||||
|
std::vector<TravelNode*> ResolveRoute(WorldPosition startPos,
|
||||||
|
WorldPosition endPos);
|
||||||
|
|
||||||
|
// Get stored walk points for one edge (from→to). Empty if no path.
|
||||||
|
std::vector<G3D::Vector3> GetEdgeWalkPoints(TravelNode* from,
|
||||||
|
TravelNode* to);
|
||||||
|
|
||||||
std::shared_timed_mutex m_nMapMtx;
|
std::shared_timed_mutex m_nMapMtx;
|
||||||
std::unordered_map<ObjectGuid, std::unordered_map<uint32, TravelNode*>> teleportNodes;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
TravelNodeMap() = default;
|
TravelNodeMap() = default;
|
||||||
@ -601,13 +724,18 @@ private:
|
|||||||
void BuildTaxiGraph();
|
void BuildTaxiGraph();
|
||||||
void ComputeAllPaths();
|
void ComputeAllPaths();
|
||||||
std::unordered_map<uint32, uint32> BFS(uint32 startNode);
|
std::unordered_map<uint32, uint32> BFS(uint32 startNode);
|
||||||
std::vector<uint32> BuildPath(uint32 fromNode, uint32 toNode,
|
std::vector<uint32> BuildPath(
|
||||||
|
uint32 fromNode, uint32 toNode,
|
||||||
const std::unordered_map<uint32, uint32>& parentMap);
|
const std::unordered_map<uint32, uint32>& parentMap);
|
||||||
|
|
||||||
std::unordered_map<uint32, std::vector<uint32>> taxiGraph;
|
std::unordered_map<uint32, std::vector<uint32>> m_taxiGraph;
|
||||||
std::map<uint32, std::map<uint32, std::vector<uint32>>> taxiPathCache;
|
std::map<uint32, std::map<uint32, std::vector<uint32>>>
|
||||||
|
m_taxiPathCache;
|
||||||
|
|
||||||
std::vector<TravelNode*> m_nodes;
|
std::vector<TravelNode*> nodes;
|
||||||
|
|
||||||
|
std::unordered_map<uint32, std::vector<TravelNode*>> m_zoneIndex;
|
||||||
|
std::unordered_map<uint32, std::vector<TravelNode*>> m_mapIndex;
|
||||||
|
|
||||||
std::vector<std::pair<uint32, WorldPosition>> mapOffsets;
|
std::vector<std::pair<uint32, WorldPosition>> mapOffsets;
|
||||||
|
|
||||||
|
|||||||
@ -651,6 +651,7 @@ bool PlayerbotAIConfig::Initialize()
|
|||||||
autoTeleportForLevel = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoTeleportForLevel", false);
|
autoTeleportForLevel = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoTeleportForLevel", false);
|
||||||
autoDoQuests = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoDoQuests", true);
|
autoDoQuests = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoDoQuests", true);
|
||||||
enableNewRpgStrategy = sConfigMgr->GetOption<bool>("AiPlayerbot.EnableNewRpgStrategy", true);
|
enableNewRpgStrategy = sConfigMgr->GetOption<bool>("AiPlayerbot.EnableNewRpgStrategy", true);
|
||||||
|
enableTravelNodes = sConfigMgr->GetOption<bool>("AiPlayerbot.EnableTravelNodes", false);
|
||||||
|
|
||||||
RpgStatusProbWeight[RPG_WANDER_RANDOM] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderRandom", 15);
|
RpgStatusProbWeight[RPG_WANDER_RANDOM] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderRandom", 15);
|
||||||
RpgStatusProbWeight[RPG_WANDER_NPC] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderNpc", 20);
|
RpgStatusProbWeight[RPG_WANDER_NPC] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderNpc", 20);
|
||||||
|
|||||||
@ -371,6 +371,7 @@ public:
|
|||||||
bool autoLearnTrainerSpells;
|
bool autoLearnTrainerSpells;
|
||||||
bool autoDoQuests;
|
bool autoDoQuests;
|
||||||
bool enableNewRpgStrategy;
|
bool enableNewRpgStrategy;
|
||||||
|
bool enableTravelNodes;
|
||||||
std::unordered_map<NewRpgStatus, uint32> RpgStatusProbWeight;
|
std::unordered_map<NewRpgStatus, uint32> RpgStatusProbWeight;
|
||||||
bool syncLevelWithPlayers;
|
bool syncLevelWithPlayers;
|
||||||
bool autoLearnQuestSpells;
|
bool autoLearnQuestSpells;
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
#include "PlayerbotMgr.h"
|
#include "PlayerbotMgr.h"
|
||||||
#include "RandomPlayerbotMgr.h"
|
#include "RandomPlayerbotMgr.h"
|
||||||
#include "ScriptMgr.h"
|
#include "ScriptMgr.h"
|
||||||
|
#include "TravelNode.h"
|
||||||
|
|
||||||
using namespace Acore::ChatCommands;
|
using namespace Acore::ChatCommands;
|
||||||
|
|
||||||
@ -41,11 +42,16 @@ public:
|
|||||||
{"unlink", HandleUnlinkAccountCommand, SEC_PLAYER, Console::No},
|
{"unlink", HandleUnlinkAccountCommand, SEC_PLAYER, Console::No},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static ChatCommandTable playerbotsTravelCommandTable = {
|
||||||
|
{"generatenode", HandleGenerateTravelNodesCommand, SEC_GAMEMASTER, Console::Yes},
|
||||||
|
};
|
||||||
|
|
||||||
static ChatCommandTable playerbotsCommandTable = {
|
static ChatCommandTable playerbotsCommandTable = {
|
||||||
{"bot", HandlePlayerbotCommand, SEC_PLAYER, Console::No},
|
{"bot", HandlePlayerbotCommand, SEC_PLAYER, Console::No},
|
||||||
{"gtask", HandleGuildTaskCommand, SEC_GAMEMASTER, Console::Yes},
|
{"gtask", HandleGuildTaskCommand, SEC_GAMEMASTER, Console::Yes},
|
||||||
{"pmon", HandlePerfMonCommand, SEC_GAMEMASTER, Console::Yes},
|
{"pmon", HandlePerfMonCommand, SEC_GAMEMASTER, Console::Yes},
|
||||||
{"rndbot", HandleRandomPlayerbotCommand, SEC_GAMEMASTER, Console::Yes},
|
{"rndbot", HandleRandomPlayerbotCommand, SEC_GAMEMASTER, Console::Yes},
|
||||||
|
{"travel", playerbotsTravelCommandTable},
|
||||||
{"debug", playerbotsDebugCommandTable},
|
{"debug", playerbotsDebugCommandTable},
|
||||||
{"account", playerbotsAccountCommandTable},
|
{"account", playerbotsAccountCommandTable},
|
||||||
};
|
};
|
||||||
@ -106,6 +112,15 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool HandleGenerateTravelNodesCommand(ChatHandler* handler, char const* /*args*/)
|
||||||
|
{
|
||||||
|
handler->PSendSysMessage("Regenerating travel node paths...");
|
||||||
|
LOG_INFO("playerbots", "Manual travel node regeneration started via console command.");
|
||||||
|
sTravelNodeMap.generateAll();
|
||||||
|
handler->PSendSysMessage("Travel node regeneration complete. Paths saved to database.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static bool HandleDebugBGCommand(ChatHandler* handler, char const* args)
|
static bool HandleDebugBGCommand(ChatHandler* handler, char const* args)
|
||||||
{
|
{
|
||||||
return BGTactics::HandleConsoleCommand(handler, args);
|
return BGTactics::HandleConsoleCommand(handler, args);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user