mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
Compare commits
37 Commits
0c9131692c
...
899f2cba94
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
899f2cba94 | ||
|
|
77db342969 | ||
|
|
694ba0c64c | ||
|
|
14ac3a39b0 | ||
|
|
4fab2e4fe6 | ||
|
|
e2bcf9683b | ||
|
|
14c7de977a | ||
|
|
0682817b42 | ||
|
|
69dd655b96 | ||
|
|
0cbee1621d | ||
|
|
8efe3a4321 | ||
|
|
1c9fd126ba | ||
|
|
1c12d8ff3e | ||
|
|
ea69b56829 | ||
|
|
35d00b499e | ||
|
|
8844a775f4 | ||
|
|
77feb8ea56 | ||
|
|
2110529b6b | ||
|
|
bdc11b07b3 | ||
|
|
f8f3de001b | ||
|
|
a24e1b033c | ||
|
|
8a3a91070b | ||
|
|
3bbe51c232 | ||
|
|
990e2f2016 | ||
|
|
2b50205e2a | ||
|
|
82e7958d2c | ||
|
|
cf0bdf13fc | ||
|
|
3952ebff6e | ||
|
|
7ab57c184e | ||
|
|
db87416f04 | ||
|
|
8b87ab091f | ||
|
|
05cf5a7702 | ||
|
|
35a30cdbef | ||
|
|
c55f554bb4 | ||
|
|
d26ac742bb | ||
|
|
c1285bb0ae | ||
|
|
77c5c6d8cd |
@ -40,7 +40,44 @@
|
|||||||
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" rebase origin/fix/mmaps-config-overrides-and-aliases)",
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" rebase origin/fix/mmaps-config-overrides-and-aliases)",
|
||||||
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" log --oneline -3)",
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" log --oneline -3)",
|
||||||
"Bash(grep -v \"//\")",
|
"Bash(grep -v \"//\")",
|
||||||
"Bash(grep -v \"^//\")"
|
"Bash(grep -v \"^//\")",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk push --force-with-lease)",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots push --force-with-lease)",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk diff --stat)",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk add src/server/game/Movement/MovementGenerators/PathGenerator.h src/server/game/Movement/MovementGenerators/PathGenerator.cpp)",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk commit -m \"feat\\(Core/Movement\\): Apply bot filter rules in PathGenerator::CreateFilter\")",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots commit -m \"refactor\\(Core/Movement\\): Drop redundant bot filter setters at PathGenerator sites\")",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk log --all --oneline -- src/server/game/Movement/MovementGenerators/PathGenerator.cpp)",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk show 82a544b03 -- src/server/game/Movement/MovementGenerators/PathGenerator.cpp)",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk log --all --oneline -- src/tools/mmaps_generator/MapBuilder.cpp)",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk log --all --oneline -S \"modAlmostUnwalkableTriangles\")",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots log --oneline --since=\"2026-05-25\" -- src/Ai/Base/Actions/LootAction.cpp src/Mgr/Item/LootObjectStack.cpp src/Mgr/Item/LootObjectStack.h src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp)",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots log --oneline -20)",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots log --oneline 82a92f62 d0ba99f3 --not 82a92f62~30 -- src/Ai/Base/Actions/LootAction.cpp src/Mgr/Item/)",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots log --oneline d0ba99f3..82a92f62 -- src/Ai/ src/Mgr/Item/)",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots show c7b4b9aa --stat)",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots log --oneline 82a92f62..origin/test-staging)",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk rev-parse origin/test-staging)",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots commit -m \"fix\\(Core/RPG\\): Drop over-strict MoveFarTo and MoveWorldObjectTo guards\")",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots show 3269d1a4 --stat)",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots commit -m \"fix\\(Core/RPG\\): Align MoveFarTo, MoveWorldObjectTo, MoveRandomNear with cmangos\")",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk commit -m \"feat\\(Core/Movement\\): Double MAX_PATH_LENGTH to 148 under MOD_PLAYERBOTS\")",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots commit -m \"fix\\(Core/RPG\\): Drop chained probe and waypoint dispatch in MoveFarTo\")",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots revert 3384fa4f --no-edit)",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots add src/Mgr/Travel/TravelMgr.cpp)",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots commit -m \"fix\\(Core/Travel\\): Soft-bias STEEP at regen PathGenerator sites\")",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk add src/server/game/Movement/MovementGenerators/PathGenerator.cpp)",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk commit -m \"fix\\(Core/Movement\\): Bot filter uses cost bias for STEEP, not hard exclude\")",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots commit -m \"feat\\(Core/Travel\\): K-nearest node search, cropPathTo reuse, cross-map pathToEnd\")",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots add src/Mgr/Travel/TravelNode.cpp src/Mgr/Travel/TravelNode.h src/Ai/Base/Actions/MovementActions.cpp)",
|
||||||
|
"Bash(git -C c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots commit -m \"feat\\(Core/Travel\\): Re-enable area-trigger, static-portal, and teleport-spell nodes\")",
|
||||||
|
"Bash(git -C /c/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots add src/Ai/Base/Actions/FollowActions.cpp src/Ai/Base/Actions/MovementActions.cpp src/Ai/Base/Actions/MovementActions.h src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.cpp)",
|
||||||
|
"Bash(git -C /c/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots commit -m \"refactor\\(Core/Movement\\): Drop IsWaitingForLastMove throttle\")",
|
||||||
|
"Bash(git -C /c/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots push)",
|
||||||
|
"Bash(git -C /c/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots diff --stat)",
|
||||||
|
"Bash(grep -v \"EmitDebugMove\\\\|//\")",
|
||||||
|
"Bash(git -C /c/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots add src/Mgr/Travel/TravelNode.h src/Mgr/Travel/TravelNode.cpp src/Ai/Base/Actions/MovementActions.cpp src/Ai/World/Rpg/NewRpgInfo.h src/Ai/World/Rpg/NewRpgInfo.cpp)",
|
||||||
|
"Bash(git -C /c/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots commit -m \"refactor\\(Core/Travel\\): Drop TravelPlan struct; GetFullPath returns TravelPath\")"
|
||||||
],
|
],
|
||||||
"additionalDirectories": [
|
"additionalDirectories": [
|
||||||
"C:\\Users\\Admin\\git\\main\\azerothcore-wotlk\\src\\common\\Collision\\Maps",
|
"C:\\Users\\Admin\\git\\main\\azerothcore-wotlk\\src\\common\\Collision\\Maps",
|
||||||
|
|||||||
@ -162,22 +162,6 @@ void MovementAction::EmitDebugMove(char const* method, char const* generator, fl
|
|||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Travel-plan override: when actively routing through the node
|
|
||||||
// graph, prefer the next-hop node name over any RPG-level target.
|
|
||||||
if (info.HasActiveTravelPlan())
|
|
||||||
{
|
|
||||||
TravelPlan const& plan = info.travelPlan;
|
|
||||||
if (plan.stepIdx < plan.steps.GetPathRef().size())
|
|
||||||
{
|
|
||||||
PathNodePoint const& pnt = plan.steps.GetPathRef()[plan.stepIdx];
|
|
||||||
if (pnt.type == PathNodeType::NODE_NODE || pnt.type == PathNodeType::NODE_PATH)
|
|
||||||
{
|
|
||||||
if (TravelNode* n = sTravelNodeMap.getNode(pnt.point, nullptr, 5.0f))
|
|
||||||
targetName = "node:" + n->getName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float dis = bot->GetExactDist(x, y, z);
|
float dis = bot->GetExactDist(x, y, z);
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "[M] | " << method
|
out << "[M] | " << method
|
||||||
@ -235,7 +219,10 @@ bool MovementAction::MoveNear(WorldObject* target, float distance, MovementPrior
|
|||||||
if (!target)
|
if (!target)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
distance += target->GetCombatReach();
|
// Reference uses bounding radius (collision footprint), not combat
|
||||||
|
// reach (which is wider for big mobs). Bounding radius lands the bot
|
||||||
|
// at the requested standoff from the model edge, not arbitrarily far.
|
||||||
|
distance += target->GetObjectSize();
|
||||||
|
|
||||||
float followAngle = GetFollowAngle();
|
float followAngle = GetFollowAngle();
|
||||||
|
|
||||||
@ -356,7 +343,7 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle,
|
|||||||
float distance = bot->GetExactDist(x, y, z);
|
float distance = bot->GetExactDist(x, y, z);
|
||||||
if (distance > 0.01f)
|
if (distance > 0.01f)
|
||||||
{
|
{
|
||||||
if (bot->IsSitState())
|
if (!bot->IsStandState())
|
||||||
bot->SetStandState(UNIT_STAND_STATE_STAND);
|
bot->SetStandState(UNIT_STAND_STATE_STAND);
|
||||||
|
|
||||||
// if (bot->IsNonMeleeSpellCast(true))
|
// if (bot->IsNonMeleeSpellCast(true))
|
||||||
@ -384,7 +371,7 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle,
|
|||||||
float distance = bot->GetExactDist(x, y, z);
|
float distance = bot->GetExactDist(x, y, z);
|
||||||
if (distance > 0.01f)
|
if (distance > 0.01f)
|
||||||
{
|
{
|
||||||
if (bot->IsSitState())
|
if (!bot->IsStandState())
|
||||||
bot->SetStandState(UNIT_STAND_STATE_STAND);
|
bot->SetStandState(UNIT_STAND_STATE_STAND);
|
||||||
|
|
||||||
DoMovePoint(bot, x, y, z, generatePath, backwards);
|
DoMovePoint(bot, x, y, z, generatePath, backwards);
|
||||||
@ -1222,11 +1209,23 @@ void MovementAction::UpdateMovementState()
|
|||||||
|
|
||||||
bool MovementAction::Follow(Unit* target, float distance, float angle)
|
bool MovementAction::Follow(Unit* target, float distance, float angle)
|
||||||
{
|
{
|
||||||
UpdateMovementState();
|
|
||||||
|
|
||||||
if (!target)
|
if (!target)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// Unsafe target (cross-faction / phased / leaving) — fall through to
|
||||||
|
// a generic MoveTo so the bot at least heads in their direction
|
||||||
|
// instead of refusing to move.
|
||||||
|
if (!botAI->IsSafe(target))
|
||||||
|
return MoveTo(target, distance);
|
||||||
|
|
||||||
|
// Subtract the target's hitbox so we end up at the requested
|
||||||
|
// standoff from its edge, not from its centre.
|
||||||
|
distance = distance <= target->GetObjectSize()
|
||||||
|
? 0.0f
|
||||||
|
: distance - target->GetObjectSize();
|
||||||
|
|
||||||
|
UpdateMovementState();
|
||||||
|
|
||||||
if (!bot->InBattleground() && ServerFacade::instance().IsDistanceLessOrEqualThan(ServerFacade::instance().GetDistance2d(bot, target),
|
if (!bot->InBattleground() && ServerFacade::instance().IsDistanceLessOrEqualThan(ServerFacade::instance().GetDistance2d(bot, target),
|
||||||
sPlayerbotAIConfig.followDistance))
|
sPlayerbotAIConfig.followDistance))
|
||||||
{
|
{
|
||||||
@ -1372,7 +1371,7 @@ bool MovementAction::Follow(Unit* target, float distance, float angle)
|
|||||||
|
|
||||||
bot->HandleEmoteCommand(0);
|
bot->HandleEmoteCommand(0);
|
||||||
|
|
||||||
if (bot->IsSitState())
|
if (!bot->IsStandState())
|
||||||
bot->SetStandState(UNIT_STAND_STATE_STAND);
|
bot->SetStandState(UNIT_STAND_STATE_STAND);
|
||||||
|
|
||||||
if (bot->IsNonMeleeSpellCast(true))
|
if (bot->IsNonMeleeSpellCast(true))
|
||||||
@ -1418,6 +1417,10 @@ bool MovementAction::ChaseTo(WorldObject* obj, float distance)
|
|||||||
|
|
||||||
UpdateMovementState();
|
UpdateMovementState();
|
||||||
|
|
||||||
|
// Drop any looping emote (sit/dance/etc.) before the chase, matching
|
||||||
|
// the reference pre-dispatch normalization.
|
||||||
|
bot->ClearEmoteState();
|
||||||
|
|
||||||
if (!bot->IsStandState())
|
if (!bot->IsStandState())
|
||||||
bot->SetStandState(UNIT_STAND_STATE_STAND);
|
bot->SetStandState(UNIT_STAND_STATE_STAND);
|
||||||
|
|
||||||
@ -3063,393 +3066,128 @@ bool MoveAwayFromPlayerWithDebuffAction::isPossible()
|
|||||||
return bot->CanFreeMove();
|
return bot->CanFreeMove();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MovementAction::LaunchWalkSpline(TravelPlan& state)
|
|
||||||
|
TravelPath MovementAction::ResolveMovePath(WorldPosition startPos,
|
||||||
|
WorldPosition endPos,
|
||||||
|
LastMovement& lastMove)
|
||||||
{
|
{
|
||||||
if (state.walkPoints.size() < 2)
|
float const totalDistance = startPos.distance(endPos);
|
||||||
|
float const maxDistChange = totalDistance * 0.1f;
|
||||||
|
|
||||||
|
// 10% reuse: cached path's tail close enough to new dest? Return as-is.
|
||||||
|
if (!lastMove.lastPath.empty() &&
|
||||||
|
lastMove.lastPath.getBack().distance(endPos) < maxDistChange)
|
||||||
|
return lastMove.lastPath;
|
||||||
|
|
||||||
|
// Long path = cross-map or beyond sight; otherwise pure mmap probe.
|
||||||
|
// Map 609 (Ebon Hold, DK starter) special-case: the area is stacked
|
||||||
|
// vertically, so a horizontally-close target on a different floor
|
||||||
|
// needs graph routing through the spiral stairs even when within
|
||||||
|
// sight distance.
|
||||||
|
bool const needsLongPath =
|
||||||
|
startPos.GetMapId() != endPos.GetMapId() ||
|
||||||
|
totalDistance > sPlayerbotAIConfig.sightDistance ||
|
||||||
|
(startPos.GetMapId() == 609 &&
|
||||||
|
std::fabs(startPos.GetPositionZ() - endPos.GetPositionZ()) > 20.0f);
|
||||||
|
|
||||||
|
TravelPath out;
|
||||||
|
|
||||||
|
if (needsLongPath && !sTravelNodeMap.getNodes().empty() && !bot->InBattleground())
|
||||||
{
|
{
|
||||||
state.walkPoints.clear();
|
out = sTravelNodeMap.GetFullPath(startPos, bot->GetZoneId(), endPos, bot);
|
||||||
return false;
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::vector<WorldPosition> probe = startPos.getPathTo(endPos, bot);
|
||||||
|
out.addPath(probe);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regression guard: if cached path's tail is no worse than the new
|
||||||
|
// path's tail, keep the cached one (catches probes blocked by geometry).
|
||||||
|
if (!lastMove.lastPath.empty() && !out.empty() &&
|
||||||
|
lastMove.lastPath.getBack().distance(endPos) <=
|
||||||
|
out.getBack().distance(endPos))
|
||||||
|
out = lastMove.lastPath;
|
||||||
|
|
||||||
// Trim past any stored points the bot has already moved past — useful
|
// Last-ditch fallback: a single point at the destination, so the
|
||||||
// when a spline is interrupted (combat, knockback, mid-spline reissue)
|
// caller has at least something to dispatch.
|
||||||
// and we re-launch from a position later in the route.
|
if (out.empty())
|
||||||
G3D::Vector3 botPos(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ());
|
out.addPoint(endPos);
|
||||||
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)
|
return out;
|
||||||
{
|
|
||||||
state.walkPoints.clear();
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sparse-segment clip (cmangos parity): truncate the chain at the
|
bool MovementAction::WaitForTransport()
|
||||||
// first segment longer than ~11.18y. Spline interpolation between
|
|
||||||
// sparse waypoints can cut corners through visual obstacles (trees,
|
|
||||||
// walls) the navmesh routed around. Bot re-plans from a closer
|
|
||||||
// position next tick where the resolved poly chain is denser.
|
|
||||||
{
|
{
|
||||||
constexpr float SPARSE_SEG_SQ = 125.0f; // sqrt(125) ≈ 11.18y
|
|
||||||
for (size_t i = 1; i < state.walkPoints.size(); ++i)
|
|
||||||
{
|
|
||||||
G3D::Vector3 d = state.walkPoints[i] - state.walkPoints[i - 1];
|
|
||||||
if (d.squaredLength() > SPARSE_SEG_SQ)
|
|
||||||
{
|
|
||||||
state.walkPoints.resize(i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (state.walkPoints.size() < 2)
|
|
||||||
{
|
|
||||||
state.walkPoints.clear();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-clamp cached waypoints to current valid Z. Rows in
|
|
||||||
// playerbots_travelnode_path store absolute coords baked at
|
|
||||||
// offline generation; if the live navmesh has shifted since
|
|
||||||
// (mmap regen, terrain change, vmap update), the stored z can
|
|
||||||
// be above ground — MoveSplinePath plays back coords verbatim
|
|
||||||
// and the bot looks like it's walking through the air.
|
|
||||||
// UpdateAllowedPositionZ factors mmap polygon Z, water surface,
|
|
||||||
// swimming, flying and transport state, so cave floors above
|
|
||||||
// the terrain plane snap correctly.
|
|
||||||
for (auto& pt : state.walkPoints)
|
|
||||||
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
G3D::Vector3 const& last = state.walkPoints.back();
|
|
||||||
|
|
||||||
// Mirror what MoveTo does after dispatching a spline so the
|
|
||||||
// lastPath cache below picks up the in-flight waypoint chain.
|
|
||||||
{
|
|
||||||
float delay = static_cast<float>(state.expectedDuration);
|
|
||||||
delay = std::min(delay, static_cast<float>(sPlayerbotAIConfig.maxWaitForMove));
|
|
||||||
delay = std::max(delay, 0.f);
|
|
||||||
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
|
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
|
||||||
lastMove.Set(bot->GetMapId(), last.x, last.y, last.z,
|
if (!lastMove.lastTransportEntry)
|
||||||
bot->GetOrientation(), delay, MovementPriority::MOVEMENT_NORMAL);
|
|
||||||
|
|
||||||
// Cache the dispatched waypoint chain so MoveFarTo's 10%
|
|
||||||
// lastPath reuse and "no worse" reuse can pick it up next tick.
|
|
||||||
std::vector<WorldPosition> wpts;
|
|
||||||
wpts.reserve(state.walkPoints.size());
|
|
||||||
for (auto const& pt : state.walkPoints)
|
|
||||||
wpts.emplace_back(bot->GetMapId(), pt.x, pt.y, pt.z);
|
|
||||||
lastMove.setPath(TravelPath(wpts));
|
|
||||||
}
|
|
||||||
|
|
||||||
EmitDebugMove("TravelPlan:walk-start", "mmap", last.x, last.y, last.z);
|
|
||||||
|
|
||||||
return false; // Walking
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MovementAction::RefineWalkPoints(std::vector<G3D::Vector3>& walkPoints)
|
|
||||||
{
|
|
||||||
if (walkPoints.size() < 2)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
std::vector<G3D::Vector3> refined;
|
|
||||||
refined.reserve(walkPoints.size() * 4);
|
|
||||||
|
|
||||||
uint32 const mapId = bot->GetMapId();
|
|
||||||
|
|
||||||
for (size_t i = 0; i + 1 < walkPoints.size(); ++i)
|
|
||||||
{
|
|
||||||
G3D::Vector3 const& a = walkPoints[i];
|
|
||||||
G3D::Vector3 const& b = walkPoints[i + 1];
|
|
||||||
|
|
||||||
WorldPosition aPos(mapId, a.x, a.y, a.z);
|
|
||||||
WorldPosition bPos(mapId, b.x, b.y, b.z);
|
|
||||||
|
|
||||||
// Per-segment mmap query: routes around geometry the offline
|
|
||||||
// graph didn't account for, or returns empty if unreachable.
|
|
||||||
std::vector<WorldPosition> segPath = bPos.getPathStepFrom(aPos, bot);
|
|
||||||
|
|
||||||
// Trust the raw waypoint pair when mmap can't validate it —
|
|
||||||
// navmesh gaps/tile-edge artifacts shouldn't kill an active plan.
|
|
||||||
bool const trustRaw = segPath.empty() ||
|
|
||||||
TravelPath::IsPathCheating(segPath, aPos.distance(bPos));
|
|
||||||
|
|
||||||
if (trustRaw)
|
|
||||||
{
|
|
||||||
if (i == 0)
|
|
||||||
refined.emplace_back(a);
|
|
||||||
refined.emplace_back(b);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include the first segment's start; skip subsequent starts
|
|
||||||
// to avoid duplicating the prior segment's tail.
|
|
||||||
size_t startK = (i == 0) ? 0 : 1;
|
|
||||||
for (size_t k = startK; k < segPath.size(); ++k)
|
|
||||||
refined.emplace_back(segPath[k].GetPositionX(),
|
|
||||||
segPath[k].GetPositionY(),
|
|
||||||
segPath[k].GetPositionZ());
|
|
||||||
}
|
|
||||||
|
|
||||||
walkPoints = std::move(refined);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MovementAction::MoveToSpline(TravelPlan& state, WorldPosition target)
|
|
||||||
{
|
|
||||||
if (!IsMovingAllowed())
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
EmitDebugMove("TravelPlan:walk-waypoint", "mmap", target.GetPositionX(), target.GetPositionY(), target.GetPositionZ());
|
Transport* transport = bot->GetTransport();
|
||||||
|
if (!transport || transport->GetEntry() != lastMove.lastTransportEntry)
|
||||||
// Generate path
|
|
||||||
state.walkPoints.clear();
|
|
||||||
PathResult path = GeneratePath(target.GetPositionX(), target.GetPositionY(), target.GetPositionZ());
|
|
||||||
// Reject paths that PathGenerator marked unreachable. The default
|
|
||||||
// accept mask is NORMAL | INCOMPLETE; anything else (NOT_USING_PATH
|
|
||||||
// from BuildShortcut on invalid polys, NOPATH, etc.) means the
|
|
||||||
// dispatched waypoints would either be a straight-line through
|
|
||||||
// geometry or stop short of the target. Abort the plan instead so
|
|
||||||
// MoveFarTo can re-derive via its own probe.
|
|
||||||
if (!path.reachable)
|
|
||||||
{
|
{
|
||||||
state.walkPoints.clear();
|
lastMove.lastTransportEntry = 0;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launch spline movement
|
// Mid-ride only when the cached path head is still the boarded
|
||||||
LaunchWalkSpline(state);
|
// transport node. If the head moved (next tick's resolution shifted
|
||||||
return true;
|
// it off, or we cut to a disembark point), let MoveFarTo continue
|
||||||
}
|
// so HandleSpecialMovement can dispatch the disembark.
|
||||||
|
if (lastMove.lastPath.empty())
|
||||||
bool MovementAction::GetTravelPlan(TravelPlan& plan, WorldPosition destination)
|
|
||||||
{
|
|
||||||
WorldPosition botPos(bot->GetMapId(), bot->GetPositionX(),
|
|
||||||
bot->GetPositionY(), bot->GetPositionZ());
|
|
||||||
return sTravelNodeMap.GetFullPath(plan, botPos, bot->GetZoneId(), destination, bot);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MovementAction::ExecuteTravelPlan(TravelPlan& state)
|
|
||||||
{
|
|
||||||
if (!state.IsActive())
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (bot->IsInFlight())
|
PathNodePoint const& front = lastMove.lastPath[0];
|
||||||
return true;
|
if (front.type != PathNodeType::NODE_TRANSPORT ||
|
||||||
|
front.entry != lastMove.lastTransportEntry)
|
||||||
// Per-step labels (`walk`, `segment`, `flight`, `transport-*`,
|
|
||||||
// `teleport(reason)`) cover every actual movement decision; emitting
|
|
||||||
// an executor-ran-this-tick label here would whisper every tick
|
|
||||||
// while the plan is active.
|
|
||||||
|
|
||||||
if (state.stepIdx >= state.steps.size())
|
|
||||||
{
|
|
||||||
state.Reset();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PathNodePoint& pt = state.steps[state.stepIdx];
|
|
||||||
|
|
||||||
switch (pt.type)
|
|
||||||
{
|
|
||||||
case PathNodeType::NODE_PREPATH:
|
|
||||||
case PathNodeType::NODE_PATH:
|
|
||||||
case PathNodeType::NODE_NODE:
|
|
||||||
{
|
|
||||||
// Batch consecutive walkable points (PREPATH, PATH, NODE) into
|
|
||||||
// one spline. With per-tick re-resolve the plan starts at
|
|
||||||
// stepIdx=0 each tick, so we must dispatch a real spline (not
|
|
||||||
// a single-waypoint MoveTo) — otherwise the executor's
|
|
||||||
// "near closest waypoint" heuristic returns true without
|
|
||||||
// moving and the bot never advances.
|
|
||||||
//
|
|
||||||
// Capped at 20 points per dispatch as a cheap upper bound on
|
|
||||||
// per-tick work. The engine plays the spline; next tick
|
|
||||||
// re-resolves from the bot's new position and dispatches the
|
|
||||||
// next batch.
|
|
||||||
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_PREPATH &&
|
|
||||||
wp.type != PathNodeType::NODE_PATH &&
|
|
||||||
wp.type != PathNodeType::NODE_NODE)
|
|
||||||
break; // stop at portal/transport/etc — can't walk past
|
|
||||||
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. 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;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-validate each segment against the live navmesh and
|
bool MovementAction::HandleSpecialMovement(TravelPath& path)
|
||||||
// substitute mmap-routed sub-paths where needed.
|
|
||||||
if (!RefineWalkPoints(state.walkPoints))
|
|
||||||
{
|
{
|
||||||
G3D::Vector3 const& failPt = state.walkPoints.empty()
|
if (path.empty())
|
||||||
? G3D::Vector3(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ())
|
|
||||||
: state.walkPoints.front();
|
|
||||||
EmitDebugMove("TravelPlan", "segment-unwalkable",
|
|
||||||
failPt.x, failPt.y, failPt.z);
|
|
||||||
state.walkPoints.clear();
|
|
||||||
state.Reset();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
LaunchWalkSpline(state);
|
PathNodePoint const& cur = path[0];
|
||||||
return true;
|
bool const hasNext = path.size() > 1;
|
||||||
}
|
|
||||||
|
|
||||||
case PathNodeType::NODE_AREA_TRIGGER:
|
// Head is special — dispatch based on the head segment's type.
|
||||||
|
switch (cur.type)
|
||||||
{
|
{
|
||||||
// Pair: trigger (pointIdx) + dest (pointIdx+1).
|
|
||||||
// Bot walks into the area trigger volume; server teleports
|
|
||||||
// on entry. Bot may need quest/key prereqs to actually cross.
|
|
||||||
if (state.stepIdx + 1 >= state.steps.size())
|
|
||||||
{
|
|
||||||
state.Reset();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PathNodePoint& trigger = state.steps[state.stepIdx];
|
|
||||||
const PathNodePoint& dst = state.steps[state.stepIdx + 1];
|
|
||||||
|
|
||||||
// Already on destination map — trigger fired, advance.
|
|
||||||
if (bot->GetMapId() == dst.point.GetMapId())
|
|
||||||
{
|
|
||||||
state.stepIdx += 2;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walk to the trigger position; collision with the trigger
|
|
||||||
// volume teleports us.
|
|
||||||
float dist = bot->GetExactDist(trigger.point.GetPositionX(),
|
|
||||||
trigger.point.GetPositionY(),
|
|
||||||
trigger.point.GetPositionZ());
|
|
||||||
if (dist > INTERACTION_DISTANCE)
|
|
||||||
return MoveTo(trigger.point.GetMapId(),
|
|
||||||
trigger.point.GetPositionX(),
|
|
||||||
trigger.point.GetPositionY(),
|
|
||||||
trigger.point.GetPositionZ());
|
|
||||||
|
|
||||||
// At trigger but didn't teleport — likely missing quest/key.
|
|
||||||
// Abort; the do-quest yield-to-grind multiplier or next
|
|
||||||
// POI pick can reroute.
|
|
||||||
state.Reset();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
case PathNodeType::NODE_STATIC_PORTAL:
|
case PathNodeType::NODE_STATIC_PORTAL:
|
||||||
{
|
{
|
||||||
// Pair: portal-GO position (pointIdx) + dest (pointIdx+1).
|
if (!cur.entry)
|
||||||
// Bot walks within interact range of the portal GameObject
|
|
||||||
// and sends CMSG_GAMEOBJ_USE to trigger its teleport spell.
|
|
||||||
if (state.stepIdx + 1 >= state.steps.size())
|
|
||||||
{
|
|
||||||
state.Reset();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
const PathNodePoint& portal = state.steps[state.stepIdx];
|
// Validate the GO template is actually a teleport spellcaster.
|
||||||
const PathNodePoint& dst = state.steps[state.stepIdx + 1];
|
// Rejects mis-labeled portal entries before we waste a CMSG.
|
||||||
|
GameObjectTemplate const* goInfo = sObjectMgr->GetGameObjectTemplate(cur.entry);
|
||||||
if (bot->GetMapId() == dst.point.GetMapId())
|
if (!goInfo || (goInfo->type != GAMEOBJECT_TYPE_SPELLCASTER &&
|
||||||
{
|
goInfo->type != GAMEOBJECT_TYPE_GOOBER))
|
||||||
state.stepIdx += 2;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walk to portal GO position
|
|
||||||
float dist = bot->GetExactDist(portal.point.GetPositionX(),
|
|
||||||
portal.point.GetPositionY(),
|
|
||||||
portal.point.GetPositionZ());
|
|
||||||
if (dist > INTERACTION_DISTANCE)
|
|
||||||
return MoveTo(portal.point.GetMapId(),
|
|
||||||
portal.point.GetPositionX(),
|
|
||||||
portal.point.GetPositionY(),
|
|
||||||
portal.point.GetPositionZ());
|
|
||||||
|
|
||||||
// In range — find the portal GameObject and interact
|
|
||||||
if (!portal.entry)
|
|
||||||
{
|
|
||||||
state.Reset();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
|
uint32 const spellId = goInfo->spellcaster.spellId;
|
||||||
|
SpellInfo const* spellInfo = SpellMgr::instance()->GetSpellInfo(spellId);
|
||||||
|
if (!spellInfo || !spellInfo->HasEffect(SPELL_EFFECT_TELEPORT_UNITS))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Mounted handling: refuse the interact while flying high
|
||||||
|
// (the dismount would drop the bot). Otherwise dismount.
|
||||||
if (bot->IsMounted())
|
if (bot->IsMounted())
|
||||||
|
{
|
||||||
|
if (bot->IsFlying())
|
||||||
|
return false;
|
||||||
bot->Dismount();
|
bot->Dismount();
|
||||||
|
}
|
||||||
botAI->RemoveShapeshift();
|
botAI->RemoveShapeshift();
|
||||||
|
|
||||||
GuidVector nearGOs = AI_VALUE(GuidVector, "nearest game objects");
|
GuidVector nearGOs = AI_VALUE(GuidVector, "nearest game objects");
|
||||||
for (ObjectGuid const& guid : nearGOs)
|
for (ObjectGuid const& guid : nearGOs)
|
||||||
{
|
{
|
||||||
GameObject* go = botAI->GetGameObject(guid);
|
GameObject* go = botAI->GetGameObject(guid);
|
||||||
if (!go || go->GetEntry() != portal.entry)
|
if (!go || go->GetEntry() != cur.entry)
|
||||||
continue;
|
continue;
|
||||||
if (!bot->GetGameObjectIfCanInteractWith(guid, GAMEOBJECT_TYPE_SPELLCASTER))
|
if (!bot->GetGameObjectIfCanInteractWith(guid, GAMEOBJECT_TYPE_SPELLCASTER))
|
||||||
continue;
|
continue;
|
||||||
@ -3459,142 +3197,179 @@ bool MovementAction::ExecuteTravelPlan(TravelPlan& state)
|
|||||||
bot->GetSession()->QueuePacket(new WorldPacket(packet));
|
bot->GetSession()->QueuePacket(new WorldPacket(packet));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// GO not found nearby — abort and let next tick try again
|
case PathNodeType::NODE_AREA_TRIGGER:
|
||||||
state.Reset();
|
{
|
||||||
|
if (cur.entry)
|
||||||
|
{
|
||||||
|
// Marker for the trigger we're walking into; server-side
|
||||||
|
// collision handles the actual teleport. Caller still
|
||||||
|
// dispatches the walk this tick.
|
||||||
|
AI_VALUE(LastMovement&, "last movement").lastAreaTrigger = cur.entry;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// No entry: direct teleport to next-point destination.
|
||||||
|
// Reference uses the next point's stored orientation (the
|
||||||
|
// baked exit facing), not the bot's current facing.
|
||||||
|
if (hasNext)
|
||||||
|
{
|
||||||
|
PathNodePoint const& dst = path[1];
|
||||||
|
return bot->TeleportTo(dst.point.GetMapId(),
|
||||||
|
dst.point.GetPositionX(),
|
||||||
|
dst.point.GetPositionY(),
|
||||||
|
dst.point.GetPositionZ(),
|
||||||
|
dst.point.GetOrientation());
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
case PathNodeType::NODE_TRANSPORT:
|
case PathNodeType::NODE_TRANSPORT:
|
||||||
{
|
{
|
||||||
if (state.stepIdx + 1 >= state.steps.size())
|
// Disembark: head is a transport node and bot is on one.
|
||||||
{
|
// Remove passenger + teleport to the next-step world position
|
||||||
state.Reset();
|
// (cmangos uses UseTransport with current→next teleport here;
|
||||||
|
// our equivalent is RemovePassenger + TeleportTo).
|
||||||
|
if (!hasNext)
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
const PathNodePoint& board = state.steps[state.stepIdx];
|
Transport* transport = bot->GetTransport();
|
||||||
const PathNodePoint& arrive = state.steps[state.stepIdx + 1];
|
if (!transport)
|
||||||
// Arrived at destination?
|
return false;
|
||||||
if (bot->GetMapId() == arrive.point.GetMapId() && !bot->GetTransport())
|
|
||||||
{
|
PathNodePoint const& dst = path[1];
|
||||||
state.stepIdx += 2;
|
transport->RemovePassenger(bot);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// On transport — wait
|
|
||||||
if (bot->GetTransport())
|
|
||||||
{
|
|
||||||
if (bot->GetMapId() == arrive.point.GetMapId())
|
|
||||||
{
|
|
||||||
bot->GetTransport()->RemovePassenger(bot);
|
|
||||||
bot->StopMovingOnCurrentPos();
|
bot->StopMovingOnCurrentPos();
|
||||||
state.stepIdx += 2;
|
bool const teleported = bot->TeleportTo(dst.point.GetMapId(),
|
||||||
}
|
dst.point.GetPositionX(),
|
||||||
return true;
|
dst.point.GetPositionY(),
|
||||||
|
dst.point.GetPositionZ(),
|
||||||
|
bot->GetOrientation());
|
||||||
|
AI_VALUE(LastMovement&, "last movement").lastTransportEntry = 0;
|
||||||
|
return teleported;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk to boarding point
|
default:
|
||||||
float dist = bot->GetExactDist(board.point.GetPositionX(), board.point.GetPositionY(), board.point.GetPositionZ());
|
break;
|
||||||
if (dist > 60.0f)
|
}
|
||||||
return MoveTo(board.point.GetMapId(), board.point.GetPositionX(), board.point.GetPositionY(), board.point.GetPositionZ());
|
|
||||||
|
|
||||||
// Try to board
|
// Head not special — check next-step for board/taxi handlers.
|
||||||
if (board.entry)
|
if (!hasNext)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
PathNodePoint const& next = path[1];
|
||||||
|
switch (next.type)
|
||||||
{
|
{
|
||||||
|
case PathNodeType::NODE_TRANSPORT:
|
||||||
|
{
|
||||||
|
if (!next.entry)
|
||||||
|
return false;
|
||||||
Map* map = bot->GetMap();
|
Map* map = bot->GetMap();
|
||||||
if (map)
|
if (!map)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Always consume the tick (return true) + throttle 1s,
|
||||||
|
// matching reference. Prevents per-tick board retries
|
||||||
|
// while we wait for the transport to actually receive us.
|
||||||
|
Transport* transport = GetTransportForPosTolerant(
|
||||||
|
map, bot, bot->GetPhaseMask(),
|
||||||
|
next.point.GetPositionX(),
|
||||||
|
next.point.GetPositionY(),
|
||||||
|
next.point.GetPositionZ());
|
||||||
|
if (transport && transport->GetEntry() == next.entry)
|
||||||
{
|
{
|
||||||
Transport* transport =
|
if (BoardTransport(transport))
|
||||||
GetTransportForPosTolerant(map, bot, bot->GetPhaseMask(), board.point.GetPositionX(),
|
AI_VALUE(LastMovement&, "last movement").lastTransportEntry = next.entry;
|
||||||
board.point.GetPositionY(), board.point.GetPositionZ());
|
|
||||||
if (transport && transport->GetEntry() == board.entry)
|
|
||||||
{
|
|
||||||
BoardTransport(transport);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
WaitForReach(1000.0f);
|
||||||
// Wait at boarding point
|
|
||||||
if (dist > INTERACTION_DISTANCE)
|
|
||||||
return MoveTo(board.point.GetMapId(), board.point.GetPositionX(), board.point.GetPositionY(), board.point.GetPositionZ());
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
case PathNodeType::NODE_FLIGHTPATH:
|
case PathNodeType::NODE_FLIGHTPATH:
|
||||||
{
|
{
|
||||||
if (state.stepIdx + 1 >= state.steps.size())
|
if (!next.entry)
|
||||||
{
|
|
||||||
state.Reset();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
const PathNodePoint& dep = state.steps[state.stepIdx];
|
TravelMgr::FlightMasterInfo const* fmInfo =
|
||||||
const PathNodePoint& arr = state.steps[state.stepIdx + 1];
|
sTravelMgr.GetNearestFlightMasterInfo(bot);
|
||||||
|
|
||||||
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)
|
if (!fmInfo)
|
||||||
{
|
return false;
|
||||||
state.route.clear();
|
|
||||||
state.stepIdx += 2;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bot->GetDistance(fmInfo->pos) > INTERACTION_DISTANCE)
|
ObjectGuid fmGuid = ObjectGuid::Create<HighGuid::Unit>(
|
||||||
return MoveTo(fmInfo->pos.GetMapId(), fmInfo->pos.GetPositionX(),
|
fmInfo->templateEntry, fmInfo->dbGuid);
|
||||||
fmInfo->pos.GetPositionY(), fmInfo->pos.GetPositionZ());
|
|
||||||
|
|
||||||
ObjectGuid fmGuid = ObjectGuid::Create<HighGuid::Unit>(fmInfo->templateEntry, fmInfo->dbGuid);
|
|
||||||
Creature* flightMaster = ObjectAccessor::GetCreature(*bot, fmGuid);
|
Creature* flightMaster = ObjectAccessor::GetCreature(*bot, fmGuid);
|
||||||
if (!flightMaster || !flightMaster->IsAlive())
|
if (!flightMaster || !flightMaster->IsAlive())
|
||||||
{
|
return false;
|
||||||
state.route.clear();
|
|
||||||
state.stepIdx += 2;
|
uint32 fromTaxi = sObjectMgr->GetNearestTaxiNode(
|
||||||
return true;
|
cur.point.GetPositionX(), cur.point.GetPositionY(),
|
||||||
}
|
cur.point.GetPositionZ(), cur.point.GetMapId(),
|
||||||
|
bot->GetTeamId());
|
||||||
|
uint32 toTaxi = sObjectMgr->GetNearestTaxiNode(
|
||||||
|
next.point.GetPositionX(), next.point.GetPositionY(),
|
||||||
|
next.point.GetPositionZ(), next.point.GetMapId(),
|
||||||
|
bot->GetTeamId());
|
||||||
|
if (!fromTaxi || !toTaxi || fromTaxi == toTaxi)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
std::vector<uint32> route = sTravelNodeMap.FindTaxiPath(fromTaxi, toTaxi);
|
||||||
|
if (route.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
botAI->RemoveShapeshift();
|
botAI->RemoveShapeshift();
|
||||||
if (bot->IsMounted())
|
if (bot->IsMounted())
|
||||||
bot->Dismount();
|
bot->Dismount();
|
||||||
|
|
||||||
bot->ActivateTaxiPathTo(state.route, flightMaster, 0);
|
return bot->ActivateTaxiPathTo(route, flightMaster, 0);
|
||||||
|
}
|
||||||
|
|
||||||
state.route.clear();
|
case PathNodeType::NODE_TELEPORT:
|
||||||
state.stepIdx += 2;
|
{
|
||||||
|
if (!next.entry)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Can't cast while flying — let the bot land first.
|
||||||
|
bool const canCastNow = !bot->IsFlying();
|
||||||
|
|
||||||
|
if (next.entry == 8690) // Hearthstone
|
||||||
|
{
|
||||||
|
if (canCastNow)
|
||||||
|
{
|
||||||
|
bool const ok = botAI->DoSpecificAction("hearthstone",
|
||||||
|
Event("move action"), true);
|
||||||
|
if (ok)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (canCastNow)
|
||||||
|
{
|
||||||
|
// Mage city portal / similar spell — dismount, drop
|
||||||
|
// shapeshift, queue cast. We don't gate on reagents (no
|
||||||
|
// "has reagents for" value on AC); the server-side cast
|
||||||
|
// attempt will fail cleanly if reagents are missing.
|
||||||
|
if (bot->IsMounted())
|
||||||
|
bot->Dismount();
|
||||||
|
botAI->RemoveShapeshift();
|
||||||
|
if (botAI->DoSpecificAction(
|
||||||
|
"cast",
|
||||||
|
Event("rpg action", std::to_string(next.entry)), true))
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cast didn't happen or failed — clear the cached path so
|
||||||
|
// the next tick re-resolves cleanly instead of retrying the
|
||||||
|
// same teleport edge that just failed.
|
||||||
|
AI_VALUE(LastMovement&, "last movement").setPath(TravelPath());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
{
|
|
||||||
LOG_ERROR("playerbots",
|
|
||||||
"[TravelPlan] Bot {} encountered unknown PathNodeType ({}); resetting plan",
|
|
||||||
bot->GetName(), static_cast<uint32>(pt.type));
|
|
||||||
state.Reset();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Transport* MovementAction::GetTransportForPosTolerant(Map* map, WorldObject* ref, uint32 phaseMask, float x, float y, float z)
|
Transport* MovementAction::GetTransportForPosTolerant(Map* map, WorldObject* ref, uint32 phaseMask, float x, float y, float z)
|
||||||
{
|
{
|
||||||
@ -3679,7 +3454,7 @@ bool MovementAction::BoardTransport(Transport* transport)
|
|||||||
{
|
{
|
||||||
transport->AddPassenger(bot, true);
|
transport->AddPassenger(bot, true);
|
||||||
bot->StopMovingOnCurrentPos();
|
bot->StopMovingOnCurrentPos();
|
||||||
EmitDebugMove("TravelPlan:transport-board", "teleport", transport->GetPositionX(),
|
EmitDebugMove("Transport:board", "teleport", transport->GetPositionX(),
|
||||||
transport->GetPositionY(), transport->GetPositionZ());
|
transport->GetPositionY(), transport->GetPositionZ());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -3701,11 +3476,11 @@ bool MovementAction::BoardTransport(Transport* transport)
|
|||||||
// MovePoint without pathfinding (transport is a moving object)
|
// MovePoint without pathfinding (transport is a moving object)
|
||||||
if (MotionMaster* mm = bot->GetMotionMaster())
|
if (MotionMaster* mm = bot->GetMotionMaster())
|
||||||
{
|
{
|
||||||
if (bot->IsSitState())
|
if (!bot->IsStandState())
|
||||||
bot->SetStandState(UNIT_STAND_STATE_STAND);
|
bot->SetStandState(UNIT_STAND_STATE_STAND);
|
||||||
|
|
||||||
mm->MovePoint(0, destX, destY, destZ, FORCED_MOVEMENT_NONE, 0.0f, 0.0f, false, false);
|
mm->MovePoint(0, destX, destY, destZ, FORCED_MOVEMENT_NONE, 0.0f, 0.0f, false, false);
|
||||||
EmitDebugMove("TravelPlan:transport-walk", "spline", destX, destY, destZ);
|
EmitDebugMove("Transport:walk", "spline", destX, destY, destZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -89,8 +89,30 @@ protected:
|
|||||||
|
|
||||||
PathResult GeneratePath(float x, float y, float z, uint32 acceptMask = DEFAULT_PATH_ACCEPT_MASK, bool forceDestination = false);
|
PathResult GeneratePath(float x, float y, float z, uint32 acceptMask = DEFAULT_PATH_ACCEPT_MASK, bool forceDestination = false);
|
||||||
|
|
||||||
bool GetTravelPlan(TravelPlan& plan, WorldPosition destination);
|
// Returns a unified TravelPath for the move. Mirror of the reference
|
||||||
bool ExecuteTravelPlan(TravelPlan& state);
|
// ResolveMovePath shape: 10% lastPath reuse short-circuit, choose
|
||||||
|
// graph (cross-map / >sightDistance) or live mmap probe, regression
|
||||||
|
// guard preferring cached path when no better, fall back to a
|
||||||
|
// single-point path on dest. Stateless — does not dispatch.
|
||||||
|
TravelPath ResolveMovePath(WorldPosition startPos,
|
||||||
|
WorldPosition endPos,
|
||||||
|
LastMovement& lastMove);
|
||||||
|
|
||||||
|
// Dispatches the head-of-path special segment (portal interact /
|
||||||
|
// area-trigger marker / transport boarding / flight master taxi).
|
||||||
|
// Caller is expected to first call TravelPath::UpcommingSpecialMovement
|
||||||
|
// which cuts the path so the head is the special segment. Returns
|
||||||
|
// true if a movement-consuming action was dispatched this tick.
|
||||||
|
// Returns false for AREA_TRIGGER-with-entry (caller still dispatches
|
||||||
|
// the walk into the trigger volume).
|
||||||
|
bool HandleSpecialMovement(TravelPath& path);
|
||||||
|
|
||||||
|
// Top-of-MoveFarTo gate that keeps a bot riding a transport across
|
||||||
|
// ticks. Returns true if the bot is still on the transport we last
|
||||||
|
// boarded (caller should skip the rest of MoveFarTo this tick).
|
||||||
|
// Clears lastTransportEntry and returns false if the bot has
|
||||||
|
// disembarked or is no longer on the expected transport.
|
||||||
|
bool WaitForTransport();
|
||||||
|
|
||||||
// Transport boarding helpers (shared by FollowAction and travel plan)
|
// Transport boarding helpers (shared by FollowAction and travel plan)
|
||||||
static Transport* GetTransportForPosTolerant(Map* map, WorldObject* ref,
|
static Transport* GetTransportForPosTolerant(Map* map, WorldObject* ref,
|
||||||
@ -101,16 +123,6 @@ protected:
|
|||||||
float& outX, float& outY, float& outZ);
|
float& outX, float& outY, float& outZ);
|
||||||
bool BoardTransport(Transport* transport);
|
bool BoardTransport(Transport* transport);
|
||||||
|
|
||||||
private:
|
|
||||||
bool LaunchWalkSpline(TravelPlan& state);
|
|
||||||
bool MoveToSpline(TravelPlan& state, WorldPosition target);
|
|
||||||
// Per-segment mmap refinement of a travel-node-graph walk batch.
|
|
||||||
// The graph stores offline-baked coords whose straight-line
|
|
||||||
// interpolation may pass through geometry the bot can't actually
|
|
||||||
// traverse. Returns false if any segment is unwalkable per the
|
|
||||||
// live navmesh, in which case the caller should abort the plan.
|
|
||||||
bool RefineWalkPoints(std::vector<G3D::Vector3>& walkPoints);
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
struct CheckAngle
|
struct CheckAngle
|
||||||
{
|
{
|
||||||
|
|||||||
@ -14,34 +14,26 @@ LastMovement::LastMovement(LastMovement& other)
|
|||||||
taxiMaster(other.taxiMaster),
|
taxiMaster(other.taxiMaster),
|
||||||
lastFollow(other.lastFollow),
|
lastFollow(other.lastFollow),
|
||||||
lastAreaTrigger(other.lastAreaTrigger),
|
lastAreaTrigger(other.lastAreaTrigger),
|
||||||
lastMoveToX(other.lastMoveToX),
|
|
||||||
lastMoveToY(other.lastMoveToY),
|
|
||||||
lastMoveToZ(other.lastMoveToZ),
|
|
||||||
lastMoveToOri(other.lastMoveToOri),
|
|
||||||
lastFlee(other.lastFlee)
|
lastFlee(other.lastFlee)
|
||||||
{
|
{
|
||||||
lastMoveShort = other.lastMoveShort;
|
lastMoveShort = other.lastMoveShort;
|
||||||
nextTeleport = other.nextTeleport;
|
nextTeleport = other.nextTeleport;
|
||||||
lastPath = other.lastPath;
|
lastPath = other.lastPath;
|
||||||
priority = other.priority;
|
priority = other.priority;
|
||||||
|
lastTransportEntry = other.lastTransportEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
void LastMovement::clear()
|
void LastMovement::clear()
|
||||||
{
|
{
|
||||||
lastMoveShort = WorldPosition();
|
lastMoveShort = WorldPosition();
|
||||||
lastPath.clear();
|
lastPath.clear();
|
||||||
lastMoveToMapId = 0;
|
|
||||||
lastMoveToX = 0;
|
|
||||||
lastMoveToY = 0;
|
|
||||||
lastMoveToZ = 0;
|
|
||||||
lastMoveToOri = 0;
|
|
||||||
lastFollow = nullptr;
|
lastFollow = nullptr;
|
||||||
lastAreaTrigger = 0;
|
lastAreaTrigger = 0;
|
||||||
lastFlee = 0;
|
lastFlee = 0;
|
||||||
nextTeleport = 0;
|
nextTeleport = 0;
|
||||||
msTime = 0;
|
msTime = 0;
|
||||||
lastdelayTime = 0;
|
|
||||||
priority = MovementPriority::MOVEMENT_NORMAL;
|
priority = MovementPriority::MOVEMENT_NORMAL;
|
||||||
|
lastTransportEntry = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void LastMovement::Set(Unit* follow)
|
void LastMovement::Set(Unit* follow)
|
||||||
@ -54,15 +46,9 @@ void LastMovement::Set(Unit* follow)
|
|||||||
|
|
||||||
void LastMovement::Set(uint32 mapId, float x, float y, float z, float ori, float delayTime, MovementPriority pri)
|
void LastMovement::Set(uint32 mapId, float x, float y, float z, float ori, float delayTime, MovementPriority pri)
|
||||||
{
|
{
|
||||||
lastMoveToMapId = mapId;
|
|
||||||
lastMoveToX = x;
|
|
||||||
lastMoveToY = y;
|
|
||||||
lastMoveToZ = z;
|
|
||||||
lastMoveToOri = ori;
|
|
||||||
lastFollow = nullptr;
|
lastFollow = nullptr;
|
||||||
lastMoveShort = WorldPosition(mapId, x, y, z, ori);
|
lastMoveShort = WorldPosition(mapId, x, y, z, ori);
|
||||||
msTime = getMSTime();
|
msTime = getMSTime();
|
||||||
lastdelayTime = delayTime;
|
|
||||||
priority = pri;
|
priority = pri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -39,6 +39,7 @@ public:
|
|||||||
lastPath = other.lastPath;
|
lastPath = other.lastPath;
|
||||||
nextTeleport = other.nextTeleport;
|
nextTeleport = other.nextTeleport;
|
||||||
priority = other.priority;
|
priority = other.priority;
|
||||||
|
lastTransportEntry = other.lastTransportEntry;
|
||||||
return *this;
|
return *this;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -55,18 +56,15 @@ public:
|
|||||||
Unit* lastFollow;
|
Unit* lastFollow;
|
||||||
uint32 lastAreaTrigger;
|
uint32 lastAreaTrigger;
|
||||||
time_t lastFlee;
|
time_t lastFlee;
|
||||||
uint32 lastMoveToMapId;
|
|
||||||
float lastMoveToX;
|
|
||||||
float lastMoveToY;
|
|
||||||
float lastMoveToZ;
|
|
||||||
float lastMoveToOri;
|
|
||||||
float lastdelayTime;
|
|
||||||
WorldPosition lastMoveShort;
|
WorldPosition lastMoveShort;
|
||||||
uint32 msTime;
|
uint32 msTime;
|
||||||
MovementPriority priority;
|
MovementPriority priority;
|
||||||
TravelPath lastPath;
|
TravelPath lastPath;
|
||||||
time_t nextTeleport;
|
time_t nextTeleport;
|
||||||
std::future<TravelPath> future;
|
// Entry of the transport the bot is currently aboard mid-journey,
|
||||||
|
// used by WaitForTransport to resume a transport segment if the
|
||||||
|
// bot is still on it next tick (e.g. boat in motion). 0 = none.
|
||||||
|
uint32 lastTransportEntry{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
class LastMovementValue : public ManualSetValue<LastMovement&>
|
class LastMovementValue : public ManualSetValue<LastMovement&>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#include "NewRpgBaseAction.h"
|
#include "NewRpgBaseAction.h"
|
||||||
|
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <iomanip>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
#include "BroadcastHelper.h"
|
#include "BroadcastHelper.h"
|
||||||
@ -62,19 +63,21 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Already-at-dest short-stop. Below targetPosRecalcDistance the
|
// Resume a transport ride if we're still on the same boat as last tick.
|
||||||
// move is effectively done — stop any active spline and clear
|
if (WaitForTransport())
|
||||||
// the cached path if it pointed here, so we don't keep gliding.
|
return true;
|
||||||
|
|
||||||
|
WorldPosition botPos(bot);
|
||||||
|
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
|
||||||
|
|
||||||
|
// Short-stop: at destination — stop and clear the cached path.
|
||||||
{
|
{
|
||||||
float const totalDistance = bot->GetExactDist(dest);
|
float const totalDistance = botPos.distance(dest);
|
||||||
if (totalDistance < sPlayerbotAIConfig.targetPosRecalcDistance)
|
if (totalDistance < sPlayerbotAIConfig.targetPosRecalcDistance)
|
||||||
{
|
{
|
||||||
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
|
|
||||||
if (!lastMove.lastPath.empty() &&
|
if (!lastMove.lastPath.empty() &&
|
||||||
lastMove.lastPath.getBack().distance(dest) <= totalDistance)
|
lastMove.lastPath.getBack().distance(dest) <= totalDistance)
|
||||||
{
|
|
||||||
lastMove.clear();
|
lastMove.clear();
|
||||||
}
|
|
||||||
bot->StopMoving();
|
bot->StopMoving();
|
||||||
EmitDebugMove("MoveFar", "arrived",
|
EmitDebugMove("MoveFar", "arrived",
|
||||||
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||||
@ -82,174 +85,95 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10% lastPath reuse — if the cached path's endpoint is still
|
// Per-tick re-resolve: rebuild the TravelPath from the bot's current
|
||||||
// close (within 10%) to the new dest, trim the cached path to
|
// position every tick (10% reuse short-circuits via the cached
|
||||||
// the bot's current position via makeShortCut and re-dispatch.
|
// lastPath). Recovers naturally from knockback, off-route drift,
|
||||||
// Per-tick re-dispatch of the (trimmed) last path keeps the bot
|
// destination changes, and blocked waypoints.
|
||||||
// on-route after interrupts (knockback, combat, manual move)
|
TravelPath path = ResolveMovePath(botPos, dest, lastMove);
|
||||||
// without needing a full replan.
|
lastMove.setPath(path);
|
||||||
{
|
|
||||||
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
|
|
||||||
if (!lastMove.lastPath.empty())
|
|
||||||
{
|
|
||||||
WorldPosition lastBack = lastMove.lastPath.getBack();
|
|
||||||
if (lastBack.GetMapId() == dest.GetMapId())
|
|
||||||
{
|
|
||||||
float totalDist = bot->GetExactDist(dest);
|
|
||||||
float maxDistChange = totalDist * 0.10f;
|
|
||||||
float distFromBotToBack = bot->GetExactDist(&lastBack);
|
|
||||||
if (lastBack.distance(dest) < maxDistChange && distFromBotToBack > 10.0f)
|
|
||||||
{
|
|
||||||
WorldPosition botPos(bot);
|
|
||||||
lastMove.lastPath.makeShortCut(botPos, sPlayerbotAIConfig.reactDistance, bot);
|
|
||||||
|
|
||||||
// makeShortCut may clear the path if the bot drifted
|
if (path.empty())
|
||||||
// too far off (>reactDistance from any waypoint). In
|
|
||||||
// that case fall through to fresh planning.
|
|
||||||
if (lastMove.lastPath.empty())
|
|
||||||
{
|
{
|
||||||
EmitDebugMove("MoveFar", "reuse-trim-failed",
|
EmitDebugMove("MoveFar", "no-path",
|
||||||
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
|
||||||
}
|
|
||||||
if (!lastMove.lastPath.empty())
|
|
||||||
{
|
|
||||||
std::vector<WorldPosition> const& pts = lastMove.lastPath.getPointPath();
|
|
||||||
if (pts.size() >= 2)
|
|
||||||
{
|
|
||||||
Movement::PointsArray points;
|
|
||||||
points.reserve(pts.size());
|
|
||||||
for (auto const& wp : pts)
|
|
||||||
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
|
|
||||||
return DispatchPathPoints(dest, points, "reuse");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Path was cleared or collapsed — fall through to fresh planning.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float disToDest = bot->GetDistance(dest);
|
|
||||||
float dis = bot->GetExactDist(dest);
|
|
||||||
|
|
||||||
// Try the travel-node graph for cross-map or moves longer than the
|
|
||||||
// bot's sight distance; otherwise the chained mmap probe handles it.
|
|
||||||
// BGs skip the graph.
|
|
||||||
bool tryNodes = sPlayerbotAIConfig.enableTravelNodes &&
|
|
||||||
!bot->InBattleground() &&
|
|
||||||
((bot->GetMapId() != dest.GetMapId()) ||
|
|
||||||
(dis > sPlayerbotAIConfig.sightDistance));
|
|
||||||
|
|
||||||
// Per-tick re-resolve (cmangos pattern). Rebuild the travel plan
|
|
||||||
// from the bot's CURRENT position every tick rather than caching
|
|
||||||
// a multi-step plan and advancing through it. Recovers naturally
|
|
||||||
// from knockback, off-route drift, mid-execution destination
|
|
||||||
// changes, and blocked waypoints. Cost: per-tick GetFullPath call;
|
|
||||||
// the lastPath cache (10% reuse block above) handles the common
|
|
||||||
// case where the cached path still ends near the same destination
|
|
||||||
// and avoids re-derivation.
|
|
||||||
if (tryNodes)
|
|
||||||
{
|
|
||||||
if (botAI->rpgInfo.HasActiveTravelPlan() &&
|
|
||||||
botAI->rpgInfo.travelPlan.destination.distance(dest) > 10.0f)
|
|
||||||
botAI->rpgInfo.ClearTravel();
|
|
||||||
|
|
||||||
StartTravelPlan(dest);
|
|
||||||
if (botAI->rpgInfo.HasActiveTravelPlan())
|
|
||||||
{
|
|
||||||
// No `travelplan` label here — per-tick re-resolve calls
|
|
||||||
// StartTravelPlan every tick, which would whisper-spam.
|
|
||||||
// The executor emits per-step labels (TravelPlan:walk-start,
|
|
||||||
// TravelPlan:flight, TravelPlan:transport-*) on actual dispatch.
|
|
||||||
return UpdateTravelPlan();
|
|
||||||
}
|
|
||||||
// Graph returned no plan — fall through to mmap probe.
|
|
||||||
}
|
|
||||||
else if (botAI->rpgInfo.HasActiveTravelPlan())
|
|
||||||
{
|
|
||||||
// Move dropped below node-first threshold — drop any leftover plan.
|
|
||||||
botAI->rpgInfo.ClearTravel();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 40-step chained mmap probe — primary for short moves and
|
|
||||||
// fallback when the node graph returned no plan.
|
|
||||||
WorldPosition botPos(bot);
|
|
||||||
std::vector<WorldPosition> probe = botPos.getPathTo(dest, bot);
|
|
||||||
|
|
||||||
// Regression guard: prefer cached lastPath if it still ends closer
|
|
||||||
// to dest than the new probe — catches probes blocked by geometry.
|
|
||||||
{
|
|
||||||
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
|
|
||||||
if (!lastMove.lastPath.empty() && !probe.empty() && probe.size() >= 2)
|
|
||||||
{
|
|
||||||
WorldPosition lastBack = lastMove.lastPath.getBack();
|
|
||||||
if (lastBack.GetMapId() == dest.GetMapId())
|
|
||||||
{
|
|
||||||
float cachedToDest = lastBack.distance(dest);
|
|
||||||
float probeToDest = dest.GetExactDist(probe.back().GetPositionX(),
|
|
||||||
probe.back().GetPositionY(),
|
|
||||||
probe.back().GetPositionZ());
|
|
||||||
if (cachedToDest <= probeToDest)
|
|
||||||
{
|
|
||||||
WorldPosition botPosNow(bot);
|
|
||||||
lastMove.lastPath.makeShortCut(botPosNow, sPlayerbotAIConfig.reactDistance, bot);
|
|
||||||
if (!lastMove.lastPath.empty())
|
|
||||||
{
|
|
||||||
std::vector<WorldPosition> const& pts = lastMove.lastPath.getPointPath();
|
|
||||||
if (pts.size() >= 2)
|
|
||||||
{
|
|
||||||
Movement::PointsArray points;
|
|
||||||
points.reserve(pts.size());
|
|
||||||
for (auto const& wp : pts)
|
|
||||||
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
|
|
||||||
return DispatchPathPoints(dest, points, "regress-keep");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walk the chained probe's full waypoint chain via DispatchPathPoints.
|
|
||||||
if (!probe.empty() && probe.size() >= 2)
|
|
||||||
{
|
|
||||||
float endDistToDest = dest.GetExactDist(probe.back().GetPositionX(),
|
|
||||||
probe.back().GetPositionY(), probe.back().GetPositionZ());
|
|
||||||
if (endDistToDest + 5.0f < disToDest)
|
|
||||||
{
|
|
||||||
Movement::PointsArray points;
|
|
||||||
points.reserve(probe.size());
|
|
||||||
for (auto const& wp : probe)
|
|
||||||
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
|
|
||||||
|
|
||||||
if (points.size() >= 2)
|
|
||||||
{
|
|
||||||
// Mount up if outdoors and not in combat.
|
|
||||||
if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive())
|
|
||||||
botAI->DoSpecificAction("check mount state", Event(), true);
|
|
||||||
|
|
||||||
return DispatchPathPoints(dest, points, "mmap");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Probe failed or didn't progress. Attempt straight-line MoveTo to
|
|
||||||
// the destination — engine PathFinder handles per-poly filtering and
|
|
||||||
// the bot's STEEP/water filter is honored via CreateFilter. If even
|
|
||||||
// that fails, the engine falls back to a direct spline.
|
|
||||||
if (bot->GetMapId() != dest.GetMapId())
|
|
||||||
{
|
|
||||||
EmitDebugMove("MoveFar", "cross-map",
|
|
||||||
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
char const* reason = (probe.empty() || probe.size() < 2) ? "mmap-empty" : "mmap-noprogress";
|
// Trim leading waypoints behind the bot, bridge with mmap probe if
|
||||||
EmitDebugMove("MoveFar", reason,
|
// the new head requires it. May empty the path (collapsed) — let
|
||||||
dest.GetPositionX(), dest.GetPositionY(),
|
// the next tick rebuild from a fresh start.
|
||||||
dest.GetPositionZ());
|
path.makeShortCut(botPos, sPlayerbotAIConfig.reactDistance, bot);
|
||||||
return MoveTo(dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(),
|
if (path.empty())
|
||||||
dest.GetPositionZ(), false, false, false, false);
|
return true;
|
||||||
|
|
||||||
|
// Special head segment (portal / area-trigger / transport / flight)?
|
||||||
|
// UpcommingSpecialMovement cuts the path so the head is the special;
|
||||||
|
// HandleSpecialMovement dispatches the matching action.
|
||||||
|
bool const onTransport = bot->GetTransport() != nullptr;
|
||||||
|
if (path.UpcommingSpecialMovement(botPos,
|
||||||
|
sPlayerbotAIConfig.reactDistance,
|
||||||
|
onTransport))
|
||||||
|
{
|
||||||
|
if (HandleSpecialMovement(path))
|
||||||
|
return true;
|
||||||
|
// Special handler declined (e.g. AREA_TRIGGER with entry → caller
|
||||||
|
// dispatches the walk into the trigger volume). Fall through.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transport guard: bot is on a transport but no special movement
|
||||||
|
// applies this tick — don't dispatch a walk spline (would fight the
|
||||||
|
// transport's own movement).
|
||||||
|
if (onTransport)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// ClipPath — truncate at first hostile creature in range / non-walkable
|
||||||
|
// hop / drifted past reactDistance / > 125 sqDist jump.
|
||||||
|
path.ClipPath(botAI, bot, false);
|
||||||
|
if (path.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Telemetry: show the path's actual tail coords vs bot + dest so we
|
||||||
|
// can see whether the resolved path is heading toward the right
|
||||||
|
// place. dest-distance == 0 means tail IS the dest (good); large
|
||||||
|
// dest-distance means graph picked a far-off endNode.
|
||||||
|
if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
|
||||||
|
{
|
||||||
|
WorldPosition tail = path.getBack();
|
||||||
|
float const tailToDest = tail.distance(dest);
|
||||||
|
float const botToTail = bot->GetExactDist(tail.GetPositionX(),
|
||||||
|
tail.GetPositionY(),
|
||||||
|
tail.GetPositionZ());
|
||||||
|
std::ostringstream tlog;
|
||||||
|
tlog << "[PATH] tail=(" << std::fixed << std::setprecision(1)
|
||||||
|
<< tail.GetPositionX() << "," << tail.GetPositionY() << "," << tail.GetPositionZ()
|
||||||
|
<< ") botToTail=" << botToTail << "y tailToDest=" << tailToDest << "y";
|
||||||
|
botAI->TellMasterNoFacing(tlog);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk dispatch.
|
||||||
|
std::vector<WorldPosition> const& pts = path.getPointPath();
|
||||||
|
Movement::PointsArray points;
|
||||||
|
points.reserve(pts.size());
|
||||||
|
for (auto const& wp : pts)
|
||||||
|
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
|
||||||
|
|
||||||
|
if (points.size() < 2)
|
||||||
|
{
|
||||||
|
// Single-point fallback path (cmangos pattern: ResolveMovePath
|
||||||
|
// emits a single dest point if nothing else worked). Hand it
|
||||||
|
// to the engine's MovePoint via MoveTo.
|
||||||
|
EmitDebugMove("MoveFar", "single-point",
|
||||||
|
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||||
|
return MoveTo(dest.GetMapId(), dest.GetPositionX(),
|
||||||
|
dest.GetPositionY(), dest.GetPositionZ(),
|
||||||
|
false, false, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bot->IsMounted() && !bot->IsInCombat() &&
|
||||||
|
bot->IsOutdoors() && bot->IsAlive())
|
||||||
|
botAI->DoSpecificAction("check mount state", Event(), true);
|
||||||
|
|
||||||
|
return DispatchPathPoints(dest, points, "walk");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest,
|
bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest,
|
||||||
@ -259,84 +183,14 @@ bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest,
|
|||||||
if (points.size() < 2)
|
if (points.size() < 2)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Prefix trim (cmangos parity: makeShortCut on every dispatch).
|
// MoveFarTo runs makeShortCut + setPath upstream now, so no need
|
||||||
// Drop leading waypoints behind the bot's current position so the
|
// for the local prefix-trim or lastMove.setPath here.
|
||||||
// spline begins from where the bot actually is, not from a stale
|
|
||||||
// planner-start. Picks the waypoint closest to the bot in 3D and
|
|
||||||
// erases everything before it.
|
|
||||||
{
|
|
||||||
float const bx = bot->GetPositionX();
|
|
||||||
float const by = bot->GetPositionY();
|
|
||||||
float const bz = bot->GetPositionZ();
|
|
||||||
float minSq = std::numeric_limits<float>::max();
|
|
||||||
size_t closest = 0;
|
|
||||||
for (size_t i = 0; i < points.size(); ++i)
|
|
||||||
{
|
|
||||||
float dx = points[i].x - bx;
|
|
||||||
float dy = points[i].y - by;
|
|
||||||
float dz = points[i].z - bz;
|
|
||||||
float sq = dx * dx + dy * dy + dz * dz;
|
|
||||||
if (sq < minSq)
|
|
||||||
{
|
|
||||||
minSq = sq;
|
|
||||||
closest = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (closest > 0)
|
|
||||||
points.erase(points.begin(), points.begin() + closest);
|
|
||||||
if (points.size() < 2)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save planner output for next-tick reuse.
|
|
||||||
{
|
|
||||||
LastMovement& lm = AI_VALUE(LastMovement&, "last movement");
|
|
||||||
std::vector<WorldPosition> wpts;
|
|
||||||
wpts.reserve(points.size());
|
|
||||||
for (auto const& pt : points)
|
|
||||||
wpts.emplace_back(dest.GetMapId(), pt.x, pt.y, pt.z);
|
|
||||||
lm.setPath(TravelPath(wpts));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto& pt : points)
|
for (auto& pt : points)
|
||||||
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
|
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
|
||||||
|
|
||||||
// ClipPath — truncate path at first hostile creature within its
|
// ClipPath now runs at MoveFarTo level on the TravelPath before the
|
||||||
// own attack range. Skipped while in combat or dead.
|
// points array is built. No per-dispatch clip here.
|
||||||
if (botAI->GetState() != BOT_STATE_COMBAT && bot->IsAlive())
|
|
||||||
{
|
|
||||||
GuidVector targets = AI_VALUE(GuidVector, "possible targets");
|
|
||||||
if (!targets.empty())
|
|
||||||
{
|
|
||||||
size_t clipAt = points.size();
|
|
||||||
for (size_t i = 0; i < points.size() && clipAt == points.size(); ++i)
|
|
||||||
{
|
|
||||||
for (ObjectGuid const& guid : targets)
|
|
||||||
{
|
|
||||||
Unit* unit = botAI->GetUnit(guid);
|
|
||||||
if (!unit || !unit->IsAlive())
|
|
||||||
continue;
|
|
||||||
Creature* cre = unit->ToCreature();
|
|
||||||
if (!cre)
|
|
||||||
continue;
|
|
||||||
if (unit->GetLevel() > bot->GetLevel() + 5)
|
|
||||||
continue;
|
|
||||||
float range = cre->GetAttackDistance(bot);
|
|
||||||
float dx = unit->GetPositionX() - points[i].x;
|
|
||||||
float dy = unit->GetPositionY() - points[i].y;
|
|
||||||
float dz = unit->GetPositionZ() - points[i].z;
|
|
||||||
if (dx * dx + dy * dy + dz * dz > range * range)
|
|
||||||
continue;
|
|
||||||
if (!unit->IsWithinLOSInMap(bot))
|
|
||||||
continue;
|
|
||||||
clipAt = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (clipAt < points.size() && clipAt + 1 < points.size())
|
|
||||||
points.erase(points.begin() + clipAt + 1, points.end());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (points.size() < 2)
|
if (points.size() < 2)
|
||||||
return false;
|
return false;
|
||||||
@ -413,24 +267,6 @@ bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewRpgBaseAction::StartTravelPlan(WorldPosition dest)
|
|
||||||
{
|
|
||||||
TravelPlan& plan = botAI->rpgInfo.travelPlan;
|
|
||||||
GetTravelPlan(plan, dest);
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
WorldObject* object = botAI->GetWorldObject(guid);
|
WorldObject* object = botAI->GetWorldObject(guid);
|
||||||
@ -508,7 +344,7 @@ bool NewRpgBaseAction::TakeFlight(std::vector<uint32> const& taxiNodes, Creature
|
|||||||
|
|
||||||
LOG_DEBUG("playerbots", "[New RPG] Bot {} taking flight ({} nodes, {} to {})",
|
LOG_DEBUG("playerbots", "[New RPG] Bot {} taking flight ({} nodes, {} to {})",
|
||||||
bot->GetName(), taxiNodes.size(), taxiNodes.front(), taxiNodes.back());
|
bot->GetName(), taxiNodes.size(), taxiNodes.front(), taxiNodes.back());
|
||||||
EmitDebugMove("TravelPlan:flight", "taxi", flightMaster->GetPositionX(), flightMaster->GetPositionY(),
|
EmitDebugMove("Flight:taxi", "taxi", flightMaster->GetPositionX(), flightMaster->GetPositionY(),
|
||||||
flightMaster->GetPositionZ());
|
flightMaster->GetPositionZ());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,9 +83,6 @@ protected:
|
|||||||
bool CheckRpgStatusAvailable(NewRpgStatus status);
|
bool CheckRpgStatusAvailable(NewRpgStatus status);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void StartTravelPlan(WorldPosition dest);
|
|
||||||
bool UpdateTravelPlan();
|
|
||||||
|
|
||||||
// Centralized dispatch helper. Applies underwater fixup, ClipPath
|
// Centralized dispatch helper. Applies underwater fixup, ClipPath
|
||||||
// (truncate at first hostile in attack range with LOS, level+5 cap),
|
// (truncate at first hostile in attack range with LOS, level+5 cap),
|
||||||
// inactive-bot teleport (with self-bot carve-out), masterWalking
|
// inactive-bot teleport (with self-bot carve-out), masterWalking
|
||||||
|
|||||||
@ -77,7 +77,6 @@ void NewRpgInfo::Reset()
|
|||||||
{
|
{
|
||||||
data = Idle{};
|
data = Idle{};
|
||||||
startT = getMSTime();
|
startT = getMSTime();
|
||||||
ClearTravel();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NewRpgStatus NewRpgInfo::GetStatus()
|
NewRpgStatus NewRpgInfo::GetStatus()
|
||||||
|
|||||||
@ -78,11 +78,6 @@ struct NewRpgInfo
|
|||||||
|
|
||||||
uint32 startT{0}; // start timestamp of the current status
|
uint32 startT{0}; // start timestamp of the current status
|
||||||
|
|
||||||
// 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,
|
||||||
GoGrind,
|
GoGrind,
|
||||||
|
|||||||
@ -229,6 +229,28 @@ public:
|
|||||||
|
|
||||||
float getAngleBetween(WorldPosition dir1, WorldPosition dir2) { return abs(getAngleTo(dir1) - getAngleTo(dir2)); }
|
float getAngleBetween(WorldPosition dir1, WorldPosition dir2) { return abs(getAngleTo(dir1) - getAngleTo(dir2)); }
|
||||||
|
|
||||||
|
// Project this point onto the segment [p1, p2]. Returns t such that
|
||||||
|
// p1 + t*(p2-p1) is the projection. t=0 means at p1, t=1 means at p2,
|
||||||
|
// 0<t<1 means strictly between. Used to decide whether the bot has
|
||||||
|
// already passed a path waypoint and should skip to the next one.
|
||||||
|
float projectOnSegment(WorldPosition const& p1, WorldPosition const& p2) const
|
||||||
|
{
|
||||||
|
if (p1.GetMapId() != p2.GetMapId() || p1.GetMapId() != GetMapId())
|
||||||
|
return 0.0f;
|
||||||
|
|
||||||
|
float dx = p2.GetPositionX() - p1.GetPositionX();
|
||||||
|
float dy = p2.GetPositionY() - p1.GetPositionY();
|
||||||
|
float dz = p2.GetPositionZ() - p1.GetPositionZ();
|
||||||
|
|
||||||
|
float lenSq = dx * dx + dy * dy + dz * dz;
|
||||||
|
if (lenSq == 0.0f)
|
||||||
|
return 0.0f;
|
||||||
|
|
||||||
|
return ((GetPositionX() - p1.GetPositionX()) * dx +
|
||||||
|
(GetPositionY() - p1.GetPositionY()) * dy +
|
||||||
|
(GetPositionZ() - p1.GetPositionZ()) * dz) / lenSq;
|
||||||
|
}
|
||||||
|
|
||||||
WorldPosition lastInRange(std::vector<WorldPosition> list, float minDist = -1.f, float maxDist = -1.f);
|
WorldPosition lastInRange(std::vector<WorldPosition> list, float minDist = -1.f, float maxDist = -1.f);
|
||||||
WorldPosition firstOutRange(std::vector<WorldPosition> list, float minDist = -1.f, float maxDist = -1.f);
|
WorldPosition firstOutRange(std::vector<WorldPosition> list, float minDist = -1.f, float maxDist = -1.f);
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
#include "Playerbots.h"
|
#include "Playerbots.h"
|
||||||
#include "RaceMgr.h"
|
#include "RaceMgr.h"
|
||||||
#include "ServerFacade.h"
|
#include "ServerFacade.h"
|
||||||
|
#include "Transport.h"
|
||||||
#include "TransportMgr.h"
|
#include "TransportMgr.h"
|
||||||
|
|
||||||
// TravelNodePath(float distance = 0.1f, float extraCost = 0, TravelNodePathType pathType = TravelNodePathType::walk,
|
// TravelNodePath(float distance = 0.1f, float extraCost = 0, TravelNodePathType pathType = TravelNodePathType::walk,
|
||||||
@ -706,6 +707,328 @@ bool TravelPath::IsPathCheating(std::vector<WorldPosition> const& path, float en
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TravelPath::cutTo(PathNodePoint point, bool including)
|
||||||
|
{
|
||||||
|
auto it = std::find(fullPath.begin(), fullPath.end(), point);
|
||||||
|
|
||||||
|
if (it == fullPath.end())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto cutIt = including ? std::next(it) : it;
|
||||||
|
fullPath.erase(fullPath.begin(), cutIt);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// Inlined zone-test: cylinder (radius>0) or rotated AABB.
|
||||||
|
bool IsPointInAreaTrigger(AreaTrigger const* at, uint32 mapId,
|
||||||
|
float x, float y, float z, float delta)
|
||||||
|
{
|
||||||
|
if (mapId != at->map)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (at->radius > 0)
|
||||||
|
{
|
||||||
|
float dx = x - at->x;
|
||||||
|
float dy = y - at->y;
|
||||||
|
float dz = z - at->z;
|
||||||
|
float distSq = dx * dx + dy * dy + dz * dz;
|
||||||
|
float r = at->radius + delta;
|
||||||
|
return distSq <= r * r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Box: rotate the test point back to AT-local axes, then check
|
||||||
|
// axis-aligned half-extents (length=X, width=Y, height=Z).
|
||||||
|
double rot = 2.0 * M_PI - at->orientation;
|
||||||
|
double sv = std::sin(rot);
|
||||||
|
double cv = std::cos(rot);
|
||||||
|
|
||||||
|
float lx = x - at->x;
|
||||||
|
float ly = y - at->y;
|
||||||
|
float rx = float(at->x + lx * cv - ly * sv) - at->x;
|
||||||
|
float ry = float(at->y + ly * cv + lx * sv) - at->y;
|
||||||
|
float rz = z - at->z;
|
||||||
|
|
||||||
|
return std::fabs(rx) <= at->length / 2 + delta &&
|
||||||
|
std::fabs(ry) <= at->width / 2 + delta &&
|
||||||
|
std::fabs(rz) <= at->height / 2 + delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TravelPath::shouldMoveToNextPoint(WorldPosition startPos,
|
||||||
|
std::vector<PathNodePoint>::iterator beg,
|
||||||
|
std::vector<PathNodePoint>::iterator ed,
|
||||||
|
std::vector<PathNodePoint>::iterator p,
|
||||||
|
float& moveDist, float maxDist)
|
||||||
|
{
|
||||||
|
if (p == ed)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto nextP = std::next(p);
|
||||||
|
if (nextP == ed)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Stop at adjacent area-trigger pair sharing entry — second is the
|
||||||
|
// teleport-out point we want to land on, not skip past.
|
||||||
|
if (p->type == PathNodeType::NODE_AREA_TRIGGER &&
|
||||||
|
nextP->type == PathNodeType::NODE_AREA_TRIGGER &&
|
||||||
|
p->entry == nextP->entry)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Same idea for static-portal pair.
|
||||||
|
if (p->type == PathNodeType::NODE_STATIC_PORTAL &&
|
||||||
|
nextP->type == PathNodeType::NODE_STATIC_PORTAL &&
|
||||||
|
p->entry == nextP->entry)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Approaching a transport boarding node — stop before it.
|
||||||
|
if (nextP->type == PathNodeType::NODE_TRANSPORT && nextP->entry)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Mid-transport: traverse to the disembark side.
|
||||||
|
if (p->type == PathNodeType::NODE_TRANSPORT && p->entry)
|
||||||
|
{
|
||||||
|
// Off-transport detour around a transport segment (rare): skip.
|
||||||
|
if (nextP->type != PathNodeType::NODE_TRANSPORT && p != beg &&
|
||||||
|
std::prev(p)->type != PathNodeType::NODE_TRANSPORT)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop within a flightpath run.
|
||||||
|
if (p->type == PathNodeType::NODE_FLIGHTPATH &&
|
||||||
|
nextP->type == PathNodeType::NODE_FLIGHTPATH)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
float nextMove = p->point.distance(nextP->point);
|
||||||
|
|
||||||
|
if (p->point.GetMapId() != startPos.GetMapId() ||
|
||||||
|
((moveDist + nextMove > maxDist ||
|
||||||
|
startPos.distance(nextP->point) > maxDist) && moveDist > 0))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
moveDist += nextMove;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<PathNodePoint>::iterator
|
||||||
|
TravelPath::getNextPoint(WorldPosition startPos, float maxDist, bool onTransport)
|
||||||
|
{
|
||||||
|
float minDist = FLT_MAX;
|
||||||
|
auto startP = fullPath.begin();
|
||||||
|
|
||||||
|
if (!onTransport)
|
||||||
|
{
|
||||||
|
// Closest walkable point on the path (same map as the bot).
|
||||||
|
for (auto p = fullPath.begin(); p != fullPath.end(); ++p)
|
||||||
|
{
|
||||||
|
if (p->point.GetMapId() != startPos.GetMapId())
|
||||||
|
continue;
|
||||||
|
if (!p->isWalkable())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
float curDist = p->point.distance(startPos);
|
||||||
|
if (curDist <= minDist)
|
||||||
|
{
|
||||||
|
minDist = curDist;
|
||||||
|
startP = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startP == fullPath.end())
|
||||||
|
return startP;
|
||||||
|
|
||||||
|
float moveDist = startP->point.distance(startPos);
|
||||||
|
|
||||||
|
for (auto p = startP; p != fullPath.end(); ++p)
|
||||||
|
{
|
||||||
|
if (shouldMoveToNextPoint(startPos, fullPath.begin(), fullPath.end(),
|
||||||
|
p, moveDist, maxDist))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
startP = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startP == fullPath.end() || !startP->isWalkable())
|
||||||
|
return startP;
|
||||||
|
|
||||||
|
auto nextP = std::next(startP);
|
||||||
|
if (nextP == fullPath.end())
|
||||||
|
return startP;
|
||||||
|
|
||||||
|
// If startPos is between startP and nextP, skip ahead to nextP.
|
||||||
|
float project = startPos.projectOnSegment(startP->point, nextP->point);
|
||||||
|
if (project > 0.0f && project < 1.0f)
|
||||||
|
return nextP;
|
||||||
|
|
||||||
|
return startP;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TravelPath::UpcommingSpecialMovement(WorldPosition startPos,
|
||||||
|
float maxDist, bool onTransport)
|
||||||
|
{
|
||||||
|
if (fullPath.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto startP = getNextPoint(startPos, maxDist, onTransport);
|
||||||
|
if (startP == fullPath.end())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto prevP = startP, nextP = startP;
|
||||||
|
if (startP != fullPath.begin())
|
||||||
|
prevP = std::prev(prevP);
|
||||||
|
if (std::next(nextP) != fullPath.end())
|
||||||
|
nextP = std::next(nextP);
|
||||||
|
|
||||||
|
// Area trigger: zone-gated. With entry, must be inside the trigger
|
||||||
|
// zone; without entry, fire as soon as we reach it.
|
||||||
|
if (startP->type == PathNodeType::NODE_AREA_TRIGGER)
|
||||||
|
{
|
||||||
|
if (startP->entry)
|
||||||
|
{
|
||||||
|
AreaTrigger const* at = sObjectMgr->GetAreaTrigger(startP->entry);
|
||||||
|
if (!at)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!IsPointInAreaTrigger(at, startPos.GetMapId(),
|
||||||
|
startPos.GetPositionX(),
|
||||||
|
startPos.GetPositionY(),
|
||||||
|
startPos.GetPositionZ(), 0.5f))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cutTo(*startP, false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static portal (game-object spellcaster): interact when in range.
|
||||||
|
if (startP->type == PathNodeType::NODE_STATIC_PORTAL &&
|
||||||
|
startPos.distance(startP->point) < INTERACTION_DISTANCE)
|
||||||
|
{
|
||||||
|
cutTo(*startP, false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Teleport spell (hearthstone et al.): fire on the next-step marker.
|
||||||
|
if (nextP->type == PathNodeType::NODE_TELEPORT)
|
||||||
|
{
|
||||||
|
cutTo(*nextP, false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flight path: interact with flight master when in range.
|
||||||
|
if (startP->type == PathNodeType::NODE_FLIGHTPATH &&
|
||||||
|
startPos.distance(startP->point) < INTERACTION_DISTANCE)
|
||||||
|
{
|
||||||
|
cutTo(*startP, false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transport boarding/disembark. We don't expose a teleport-vs-walk
|
||||||
|
// toggle yet, so always take the walk-on-board path: cut to dock if
|
||||||
|
// off-transport, traverse to disembark if on-transport.
|
||||||
|
if (startP->type == PathNodeType::NODE_TRANSPORT)
|
||||||
|
{
|
||||||
|
uint32 entry = nextP->entry;
|
||||||
|
|
||||||
|
if (!onTransport)
|
||||||
|
{
|
||||||
|
// prevP = dock, startP = where transport will stop.
|
||||||
|
cutTo(*prevP, false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On transport: walk to disembark.
|
||||||
|
for (auto p = startP; p != fullPath.end(); ++p)
|
||||||
|
{
|
||||||
|
if (p->type != PathNodeType::NODE_TRANSPORT ||
|
||||||
|
(p->entry && p->entry != entry))
|
||||||
|
{
|
||||||
|
cutTo(*p, false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
prevP = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TravelPath::ClipPath(PlayerbotAI* ai, Unit* mover, bool ignoreEnemyTargets)
|
||||||
|
{
|
||||||
|
auto startP = getNextPoint(WorldPosition(mover), 0.0f, false);
|
||||||
|
cutTo(*startP, false);
|
||||||
|
|
||||||
|
if (startP == fullPath.end())
|
||||||
|
return;
|
||||||
|
|
||||||
|
GuidVector targets;
|
||||||
|
Player* bot = ai ? ai->GetBot() : nullptr;
|
||||||
|
if (bot && ai->GetState() != BOT_STATE_COMBAT && !bot->isDead() && !ignoreEnemyTargets)
|
||||||
|
{
|
||||||
|
AiObjectContext* context = ai->GetAiObjectContext();
|
||||||
|
targets = AI_VALUE(GuidVector, "possible targets");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto endP = fullPath.end();
|
||||||
|
auto prevP = fullPath.begin();
|
||||||
|
float const reactSq = sPlayerbotAIConfig.reactDistance * sPlayerbotAIConfig.reactDistance;
|
||||||
|
|
||||||
|
for (auto p = fullPath.begin(); p != fullPath.end(); ++p)
|
||||||
|
{
|
||||||
|
// Hostile-target check: stop before walking into a mob that
|
||||||
|
// would aggro. Level-capped (mover->level + 5) so over-level
|
||||||
|
// mobs we'd avoid anyway are ignored.
|
||||||
|
for (ObjectGuid const& targetGuid : targets)
|
||||||
|
{
|
||||||
|
if (!targetGuid.IsCreature())
|
||||||
|
continue;
|
||||||
|
Unit* unit = ai->GetUnit(targetGuid);
|
||||||
|
if (!unit || unit->isDead())
|
||||||
|
continue;
|
||||||
|
if (unit->GetLevel() > mover->GetLevel() + 5)
|
||||||
|
continue;
|
||||||
|
Creature* cre = unit->ToCreature();
|
||||||
|
if (!cre)
|
||||||
|
continue;
|
||||||
|
float const range = cre->GetAttackDistance(mover);
|
||||||
|
if (WorldPosition(unit).sqDistance(p->point) > range * range)
|
||||||
|
continue;
|
||||||
|
if (!unit->IsHostileTo(mover) || !unit->IsWithinLOSInMap(mover))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
endP = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (endP != fullPath.end())
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Reject paths that drift past reactDistance from the start —
|
||||||
|
// a sign the path looped or wandered.
|
||||||
|
if (p->point.sqDistance(fullPath.begin()->point) > reactSq)
|
||||||
|
endP = p;
|
||||||
|
// Non-walkable hop in the middle (portal/transport/etc.) terminates.
|
||||||
|
else if (!p->isWalkable())
|
||||||
|
endP = p;
|
||||||
|
// Gap between adjacent points > ~11y (sqDist 125) — likely bad data.
|
||||||
|
else if (p->point.sqDistance(prevP->point) > 125.0f)
|
||||||
|
endP = prevP;
|
||||||
|
|
||||||
|
if (endP != fullPath.end())
|
||||||
|
break;
|
||||||
|
|
||||||
|
prevP = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endP == fullPath.end())
|
||||||
|
return;
|
||||||
|
|
||||||
|
fullPath.erase(std::next(endP), fullPath.end());
|
||||||
|
}
|
||||||
|
|
||||||
bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist, Unit* bot)
|
bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist, Unit* bot)
|
||||||
{
|
{
|
||||||
if (GetPath().empty())
|
if (GetPath().empty())
|
||||||
@ -898,6 +1221,14 @@ TravelPath TravelNodeRoute::BuildPath(std::vector<WorldPosition> pathToStart, st
|
|||||||
// Full taxi waypoint route; same reasoning as transport.
|
// Full taxi waypoint route; same reasoning as transport.
|
||||||
travelPath.addPath(nodePath->GetPath(), PathNodeType::NODE_FLIGHTPATH, nodePath->getPathObject());
|
travelPath.addPath(nodePath->GetPath(), PathNodeType::NODE_FLIGHTPATH, nodePath->getPathObject());
|
||||||
}
|
}
|
||||||
|
else if (nodePath->getPathType() == TravelNodePathType::teleportSpell)
|
||||||
|
{
|
||||||
|
// Hearthstone or spell-cast teleport edge: emit a paired
|
||||||
|
// NODE_TELEPORT (entry = exit) so HandleSpecialMovement can
|
||||||
|
// dispatch the cast when the head reaches the entry point.
|
||||||
|
travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_TELEPORT, nodePath->getPathObject());
|
||||||
|
travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_TELEPORT, nodePath->getPathObject());
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
std::vector<WorldPosition> path = nodePath->GetPath();
|
std::vector<WorldPosition> path = nodePath->GetPath();
|
||||||
@ -1086,32 +1417,136 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal,
|
|||||||
|
|
||||||
std::vector<TravelNodeStub*> open, closed;
|
std::vector<TravelNodeStub*> open, closed;
|
||||||
|
|
||||||
|
std::vector<TravelNode*> portNodes; // synthetic teleport/portal edges
|
||||||
|
|
||||||
if (bot)
|
if (bot)
|
||||||
{
|
{
|
||||||
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
||||||
if (botAI)
|
if (botAI)
|
||||||
{
|
{
|
||||||
|
AiObjectContext* context = botAI->GetAiObjectContext();
|
||||||
|
|
||||||
if (botAI->HasCheat(BotCheatMask::gold))
|
if (botAI->HasCheat(BotCheatMask::gold))
|
||||||
startStub->currentGold = 10000000;
|
startStub->currentGold = 10000000;
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AiObjectContext* context = botAI->GetAiObjectContext();
|
// Group-gold accounting (reference parity): A* must
|
||||||
|
// budget against the MIN travel-money across all safe
|
||||||
|
// group members — a taxi/transport edge the leader
|
||||||
|
// can afford but a member can't would split the group.
|
||||||
startStub->currentGold = AI_VALUE2(uint32, "free money for", (uint32)NeedMoneyFor::travel);
|
startStub->currentGold = AI_VALUE2(uint32, "free money for", (uint32)NeedMoneyFor::travel);
|
||||||
|
bool const isLeader = botAI->GetGroupLeader() == bot;
|
||||||
|
for (ObjectGuid guid : AI_VALUE(GuidVector, "group members"))
|
||||||
|
{
|
||||||
|
Player* player = ObjectAccessor::FindPlayer(guid);
|
||||||
|
if (!player)
|
||||||
|
continue;
|
||||||
|
if (!isLeader && player != bot)
|
||||||
|
continue;
|
||||||
|
if (!botAI->IsSafe(player))
|
||||||
|
{
|
||||||
|
startStub->currentGold = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!GET_PLAYERBOT_AI(player))
|
||||||
|
continue;
|
||||||
|
startStub->currentGold = std::min(
|
||||||
|
startStub->currentGold,
|
||||||
|
PAI_VALUE2(uint32, "free money for", (uint32)NeedMoneyFor::travel));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hearthstone (item 6948 / spell 8690): inject a synthetic
|
||||||
|
// teleport edge from start to the node nearest the bot's
|
||||||
|
// home bind, so A* can pick hearthing over walking.
|
||||||
|
if (bot->IsAlive() && bot->HasItemCount(6948, 1))
|
||||||
|
{
|
||||||
|
WorldPosition homePos = AI_VALUE(WorldPosition, "home bind");
|
||||||
|
std::vector<WorldPosition> dummy;
|
||||||
|
TravelNode* homeNode = sTravelNodeMap.getNode(homePos, dummy, nullptr, 50.0f);
|
||||||
|
if (homeNode && homeNode != start)
|
||||||
|
{
|
||||||
|
PortalNode* portNode = new PortalNode(start);
|
||||||
|
portNode->SetPortal(start, homeNode, 8690);
|
||||||
|
|
||||||
|
TravelNodeStub* hsStub = &m_stubs.insert(std::make_pair(
|
||||||
|
static_cast<TravelNode*>(portNode), TravelNodeStub(portNode))).first->second;
|
||||||
|
|
||||||
|
// Cost: max(2 seconds, (10 - deathCount) * MINUTE).
|
||||||
|
// Fresh bot → 10 minutes (walks if anything's closer);
|
||||||
|
// recently-died bot → drops toward 2 seconds (hearth wins).
|
||||||
|
// Clamp deathCount to 10 to avoid uint32 underflow that
|
||||||
|
// the reference implementation has at deathCount > 10.
|
||||||
|
uint32 const dc = std::min<uint32>(10, AI_VALUE(uint32, "death count"));
|
||||||
|
hsStub->costFromStart = std::max<uint32>(2, (10 - dc) * MINUTE);
|
||||||
|
hsStub->heuristic = hsStub->dataNode->fDist(goal) / botSpeed;
|
||||||
|
hsStub->totalCost = hsStub->costFromStart + hsStub->heuristic;
|
||||||
|
|
||||||
|
open.push_back(hsStub);
|
||||||
|
hsStub->open = true;
|
||||||
|
portNodes.push_back(portNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mage teleport spells: 3561 Stormwind, 3562 Ironforge, 3563 Undercity,
|
||||||
|
// 3565 Darnassus, 3566 Thunder Bluff, 3567 Orgrimmar, 18960 Moonglade.
|
||||||
|
// Inject one synthetic teleport edge per known + ready spell.
|
||||||
|
static const uint32 teleSpells[] = {3561, 3562, 3563, 3565, 3566, 3567, 18960};
|
||||||
|
for (uint32 spellId : teleSpells)
|
||||||
|
{
|
||||||
|
if (!bot->IsAlive() || bot->IsInCombat())
|
||||||
|
break;
|
||||||
|
if (!bot->HasSpell(spellId))
|
||||||
|
continue;
|
||||||
|
if (bot->HasSpellCooldown(spellId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
SpellTargetPosition const* stp =
|
||||||
|
sSpellMgr->GetSpellTargetPosition(spellId, EFFECT_0);
|
||||||
|
if (!stp)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
WorldPosition telePos(stp->target_mapId, stp->target_X,
|
||||||
|
stp->target_Y, stp->target_Z, 0.0f);
|
||||||
|
std::vector<WorldPosition> dummy;
|
||||||
|
TravelNode* destNode = sTravelNodeMap.getNode(telePos, dummy, nullptr, 10.0f);
|
||||||
|
if (!destNode || destNode == start)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
PortalNode* portNode = new PortalNode(start);
|
||||||
|
portNode->SetPortal(start, destNode, spellId);
|
||||||
|
|
||||||
|
TravelNodeStub* tsStub = &m_stubs.insert(std::make_pair(
|
||||||
|
static_cast<TravelNode*>(portNode), TravelNodeStub(portNode))).first->second;
|
||||||
|
|
||||||
|
tsStub->costFromStart = MINUTE; // cheaper than ~1-min walk
|
||||||
|
tsStub->heuristic = tsStub->dataNode->fDist(goal) / botSpeed;
|
||||||
|
tsStub->totalCost = tsStub->costFromStart + tsStub->heuristic;
|
||||||
|
|
||||||
|
open.push_back(tsStub);
|
||||||
|
tsStub->open = true;
|
||||||
|
portNodes.push_back(portNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
startStub->currentGold = bot->GetMoney();
|
startStub->currentGold = bot->GetMoney();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!start->hasRouteTo(goal))
|
if (open.empty() && !start->hasRouteTo(goal))
|
||||||
|
{
|
||||||
|
for (auto* p : portNodes)
|
||||||
|
delete p;
|
||||||
return TravelNodeRoute();
|
return TravelNodeRoute();
|
||||||
|
}
|
||||||
|
|
||||||
// Min-heap: smallest f at front
|
// Min-heap: smallest f at front
|
||||||
auto heapComp = [](TravelNodeStub* i, TravelNodeStub* j) { return i->totalCost > j->totalCost; };
|
auto heapComp = [](TravelNodeStub* i, TravelNodeStub* j) { return i->totalCost > j->totalCost; };
|
||||||
|
|
||||||
open.push_back(startStub);
|
open.push_back(startStub);
|
||||||
std::push_heap(open.begin(), open.end(), heapComp);
|
|
||||||
startStub->open = true;
|
startStub->open = true;
|
||||||
|
// Heapify all of open in one pass — covers both startStub and any
|
||||||
|
// PortalNode stubs injected above.
|
||||||
|
std::make_heap(open.begin(), open.end(), heapComp);
|
||||||
|
|
||||||
constexpr uint32 MAX_A_STAR_EXPLORED = 500;
|
constexpr uint32 MAX_A_STAR_EXPLORED = 500;
|
||||||
uint32 nodesExplored = 0;
|
uint32 nodesExplored = 0;
|
||||||
@ -1119,7 +1554,11 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal,
|
|||||||
while (!open.empty())
|
while (!open.empty())
|
||||||
{
|
{
|
||||||
if (++nodesExplored > MAX_A_STAR_EXPLORED)
|
if (++nodesExplored > MAX_A_STAR_EXPLORED)
|
||||||
|
{
|
||||||
|
for (auto* p : portNodes)
|
||||||
|
delete p;
|
||||||
return TravelNodeRoute();
|
return TravelNodeRoute();
|
||||||
|
}
|
||||||
|
|
||||||
std::pop_heap(open.begin(), open.end(), heapComp);
|
std::pop_heap(open.begin(), open.end(), heapComp);
|
||||||
currentNode = open.back();
|
currentNode = open.back();
|
||||||
@ -1144,7 +1583,11 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal,
|
|||||||
|
|
||||||
reverse(path.begin(), path.end());
|
reverse(path.begin(), path.end());
|
||||||
|
|
||||||
return TravelNodeRoute(path);
|
// Successful route: hand off ownership of any synthetic
|
||||||
|
// PortalNodes injected at the head. Caller (GetFullPath)
|
||||||
|
// is expected to call cleanTempNodes() when done with the
|
||||||
|
// route — see the call site for the lifecycle.
|
||||||
|
return TravelNodeRoute(path, portNodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto const& link : *currentNode->dataNode->getLinks()) // for each successor n' of n
|
for (auto const& link : *currentNode->dataNode->getLinks()) // for each successor n' of n
|
||||||
@ -1183,6 +1626,9 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A* exhausted open without reaching goal. Clean up synthetic nodes.
|
||||||
|
for (auto* p : portNodes)
|
||||||
|
delete p;
|
||||||
return TravelNodeRoute();
|
return TravelNodeRoute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1262,36 +1708,21 @@ TravelNodeRoute TravelNodeMap::FindRouteNearestNodes(WorldPosition startPos, Wor
|
|||||||
return TravelNodeRoute();
|
return TravelNodeRoute();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TravelNodeMap::GetFullPath(TravelPlan& plan,
|
TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos, [[maybe_unused]] uint32 botZoneId,
|
||||||
WorldPosition botPos, uint32 botZoneId,
|
|
||||||
WorldPosition destination, Unit* bot)
|
WorldPosition destination, Unit* bot)
|
||||||
{
|
{
|
||||||
// Capture previous pathToStart from the about-to-be-reset plan so we
|
TravelPath path;
|
||||||
// can try cropPathTo to reuse it across the per-tick re-resolve.
|
|
||||||
std::vector<WorldPosition> prevPathToStart;
|
|
||||||
for (auto const& pt : plan.steps.GetPathRef())
|
|
||||||
{
|
|
||||||
if (pt.type == PathNodeType::NODE_PREPATH)
|
|
||||||
prevPathToStart.push_back(pt.point);
|
|
||||||
else
|
|
||||||
break; // PREPATH is always at the head
|
|
||||||
}
|
|
||||||
|
|
||||||
plan.Reset();
|
// AC-side workaround that reference doesn't have: if a 40-step mmap
|
||||||
plan.destination = destination;
|
// probe from bot to destination either reaches close to dest OR
|
||||||
|
// makes any meaningful forward progress (>30y absolute), prefer
|
||||||
// mmap-probe first: if a 40-step probe makes meaningful progress,
|
// that direct path over the graph. The graph's K=5 endNode pick +
|
||||||
// prefer it over the graph. Loosened from "reaches within spellDistance"
|
// strict 1y endPath validation rejects destinations that are
|
||||||
// because the strict gate falls through to graph routing whenever the
|
// slightly off-mesh (cave shelves, alcoves), so without this the
|
||||||
// probe stops a few yards short of the destination (e.g., bot can't
|
// bot falls to a single-point MoveTo fallback and wiggles in place.
|
||||||
// reach the exact GO position, or destination is inside an area the
|
// Loosened from "50% AND >30y" to just ">30y" so a partial probe
|
||||||
// probe can't fully enter). Graph paths come from DB-cached walk
|
// toward the cave entrance gets accepted; the next tick's
|
||||||
// edges baked at offline generation time and can route through
|
// re-resolve from the new bot position can extend further.
|
||||||
// terrain that current mmaps treat as unwalkable.
|
|
||||||
//
|
|
||||||
// Accept the probe if EITHER:
|
|
||||||
// (a) it reaches within 30y of destination, OR
|
|
||||||
// (b) it makes >50% progress and got at least 30y total
|
|
||||||
if (botPos.GetMapId() == destination.GetMapId())
|
if (botPos.GetMapId() == destination.GetMapId())
|
||||||
{
|
{
|
||||||
std::vector<WorldPosition> probe = destination.getPathFromPath({botPos}, bot, 40);
|
std::vector<WorldPosition> probe = destination.getPathFromPath({botPos}, bot, 40);
|
||||||
@ -1302,120 +1733,183 @@ bool TravelNodeMap::GetFullPath(TravelPlan& plan,
|
|||||||
float const probeProgress = totalDist - probeEndToDest;
|
float const probeProgress = totalDist - probeEndToDest;
|
||||||
|
|
||||||
bool const closeEnough = probeEndToDest < 30.0f;
|
bool const closeEnough = probeEndToDest < 30.0f;
|
||||||
bool const meaningfulProgress = probeProgress > totalDist * 0.5f && probeProgress > 30.0f;
|
bool const meaningfulProgress = probeProgress > 30.0f;
|
||||||
|
|
||||||
if (closeEnough || meaningfulProgress)
|
if (closeEnough || meaningfulProgress)
|
||||||
{
|
{
|
||||||
plan.steps.addPoint(botPos, PathNodeType::NODE_PREPATH);
|
path.addPoint(botPos, PathNodeType::NODE_PREPATH);
|
||||||
for (size_t i = 1; i < probe.size(); ++i)
|
for (size_t i = 1; i < probe.size(); ++i)
|
||||||
plan.steps.addPoint(probe[i], PathNodeType::NODE_PATH);
|
path.addPoint(probe[i], PathNodeType::NODE_PATH);
|
||||||
return true;
|
return path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_lock<std::shared_timed_mutex> guard(m_nMapMtx);
|
std::shared_lock<std::shared_timed_mutex> guard(m_nMapMtx);
|
||||||
|
|
||||||
// K-nearest start + end node candidates (cmangos parity: K=5).
|
// Mirror reference: if the bot is mid-transport, the first valid
|
||||||
// Iterate combinations — first pair with a graph route wins. The
|
// route wins immediately (no per-candidate validation against the
|
||||||
// single-nearest may have no route while the 2nd/3rd does.
|
// ground — the transport handles position).
|
||||||
|
uint32 transportEntry = 0;
|
||||||
|
if (bot && bot->GetTransport())
|
||||||
|
transportEntry = bot->GetTransport()->GetEntry();
|
||||||
|
|
||||||
|
// K-nearest start + end node candidates (K=5). Map-wide scan to
|
||||||
|
// mirror reference `getNodes(pos, -1)` — restricting to bot's zone
|
||||||
|
// misses nodes that sit just across a zone boundary (e.g. a cave
|
||||||
|
// whose interior node is in a different zone than its entrance).
|
||||||
constexpr uint32 K = 5;
|
constexpr uint32 K = 5;
|
||||||
auto pickKNearest = [&](WorldPosition pos, uint32 zoneId) -> std::vector<TravelNode*>
|
auto pickKNearest = [&](WorldPosition pos) -> std::vector<TravelNode*>
|
||||||
{
|
{
|
||||||
std::vector<TravelNode*> const& zoneNodes = GetNodesInZone(zoneId);
|
std::vector<TravelNode*> candidates;
|
||||||
std::vector<TravelNode*> candidates(zoneNodes.begin(), zoneNodes.end());
|
|
||||||
if (candidates.empty())
|
|
||||||
{
|
|
||||||
// Fallback to per-map scan
|
|
||||||
for (TravelNode* n : nodes)
|
for (TravelNode* n : nodes)
|
||||||
if (n && n->getPosition()->GetMapId() == pos.GetMapId())
|
if (n && n->getPosition()->GetMapId() == pos.GetMapId())
|
||||||
candidates.push_back(n);
|
candidates.push_back(n);
|
||||||
}
|
|
||||||
if (candidates.empty())
|
if (candidates.empty())
|
||||||
return {};
|
return {};
|
||||||
uint32 n = std::min<uint32>(K, candidates.size());
|
uint32 const n = std::min<uint32>(K, (uint32)candidates.size());
|
||||||
std::partial_sort(candidates.begin(), candidates.begin() + n, candidates.end(),
|
std::partial_sort(candidates.begin(), candidates.begin() + n, candidates.end(),
|
||||||
[pos](TravelNode* i, TravelNode* j) { return i->fDist(pos) < j->fDist(pos); });
|
[pos](TravelNode* i, TravelNode* j) { return i->fDist(pos) < j->fDist(pos); });
|
||||||
candidates.resize(n);
|
candidates.resize(n);
|
||||||
return candidates;
|
return candidates;
|
||||||
};
|
};
|
||||||
|
|
||||||
uint32 destZone = sMapMgr->GetZoneId(PHASEMASK_NORMAL, destination);
|
std::vector<TravelNode*> startCandidates = pickKNearest(botPos);
|
||||||
std::vector<TravelNode*> startCandidates = pickKNearest(botPos, botZoneId);
|
std::vector<TravelNode*> endCandidates = pickKNearest(destination);
|
||||||
std::vector<TravelNode*> endCandidates = pickKNearest(destination, destZone);
|
|
||||||
|
|
||||||
if (startCandidates.empty() || endCandidates.empty())
|
if (startCandidates.empty() || endCandidates.empty())
|
||||||
return false;
|
return path; // empty
|
||||||
|
|
||||||
|
// Iterate combinations with per-candidate path validation. Skip
|
||||||
|
// nodes that failed a prior pass (bad*Nodes), reject endNodes whose
|
||||||
|
// mmap-path to dest can't reach within 1y, and reject startNodes
|
||||||
|
// whose mmap-path from bot can't reach within maxStartDistance
|
||||||
|
// (20y for transport, 1y otherwise — matches reference).
|
||||||
|
std::vector<TravelNode*> badStartNodes, badEndNodes;
|
||||||
|
|
||||||
TravelNode* startNode = nullptr;
|
|
||||||
TravelNode* endNode = nullptr;
|
|
||||||
TravelNodeRoute route;
|
|
||||||
for (TravelNode* s : startCandidates)
|
|
||||||
{
|
|
||||||
for (TravelNode* e : endCandidates)
|
for (TravelNode* e : endCandidates)
|
||||||
{
|
{
|
||||||
if (!s || !e || s == e)
|
if (std::find(badEndNodes.begin(), badEndNodes.end(), e) != badEndNodes.end())
|
||||||
continue;
|
continue;
|
||||||
if (!s->hasRouteTo(e))
|
if (!e)
|
||||||
continue;
|
continue;
|
||||||
TravelNodeRoute r = GetNodeRoute(s, e, nullptr);
|
WorldPosition endNodePos = *e->getPosition();
|
||||||
if (r.isEmpty())
|
|
||||||
continue;
|
|
||||||
startNode = s;
|
|
||||||
endNode = e;
|
|
||||||
route = r;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (!route.isEmpty())
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route.isEmpty() || !startNode || !endNode)
|
// Validate endNode -> destination is pathable. Reference uses 1y
|
||||||
return false;
|
// strict tolerance, but that rejects valid cave-interior nodes
|
||||||
|
// when the destination (quest GO, mob, item) is slightly off the
|
||||||
WorldPosition startNodePos = *startNode->getPosition();
|
// navmesh (small shelf, alcove). Use INTERACTION_DISTANCE so any
|
||||||
WorldPosition endNodePos = *endNode->getPosition();
|
// endNode whose mmap can reach close enough for the bot to
|
||||||
|
// interact with the destination is accepted.
|
||||||
// pathToStart: mmap-path from bot to the first node. Try cropping
|
std::vector<WorldPosition> endProbe;
|
||||||
// the previous pathToStart first (cmangos parity) — if it still
|
bool endPathOk = false;
|
||||||
// reaches the chosen startNode within reactDistance we avoid a full
|
|
||||||
// re-probe. Falls back to fresh getPathTo if crop fails or invalid.
|
|
||||||
std::vector<WorldPosition> pathToStart;
|
|
||||||
if (!prevPathToStart.empty())
|
|
||||||
{
|
|
||||||
std::vector<WorldPosition> cropped = prevPathToStart;
|
|
||||||
bool ok = startNodePos.cropPathTo(cropped, sPlayerbotAIConfig.reactDistance);
|
|
||||||
if (ok && cropped.size() >= 2)
|
|
||||||
pathToStart = cropped;
|
|
||||||
}
|
|
||||||
if (pathToStart.empty() && bot && botPos.GetMapId() == startNodePos.GetMapId())
|
|
||||||
{
|
|
||||||
std::vector<WorldPosition> probe = botPos.getPathTo(startNodePos, bot);
|
|
||||||
if (probe.size() >= 2)
|
|
||||||
pathToStart = probe;
|
|
||||||
}
|
|
||||||
if (pathToStart.empty())
|
|
||||||
pathToStart = {botPos};
|
|
||||||
|
|
||||||
// pathToEnd: mmap-path from the last node to the destination.
|
|
||||||
// Single-map case: use bot's PathGenerator directly.
|
|
||||||
// Cross-map case: pass nullptr — getPathTo constructs a tempCreature
|
|
||||||
// on the destination's base map so we can pathfind there even though
|
|
||||||
// bot isn't loaded into it.
|
|
||||||
std::vector<WorldPosition> pathToEnd;
|
|
||||||
if (endNodePos.GetMapId() == destination.GetMapId())
|
if (endNodePos.GetMapId() == destination.GetMapId())
|
||||||
{
|
{
|
||||||
Unit* pathBot = (bot && bot->GetMapId() == destination.GetMapId()) ? bot : nullptr;
|
Unit* pathBot = (bot && bot->GetMapId() == destination.GetMapId()) ? bot : nullptr;
|
||||||
std::vector<WorldPosition> probe = endNodePos.getPathTo(destination, pathBot);
|
endProbe = endNodePos.getPathTo(destination, pathBot);
|
||||||
if (probe.size() >= 2)
|
endPathOk = destination.isPathTo(endProbe, INTERACTION_DISTANCE);
|
||||||
pathToEnd = probe;
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Cross-map endNode is its own teleport destination.
|
||||||
|
endProbe = {endNodePos, destination};
|
||||||
|
endPathOk = true;
|
||||||
}
|
}
|
||||||
if (pathToEnd.empty())
|
|
||||||
pathToEnd = {destination};
|
|
||||||
|
|
||||||
plan.steps = route.BuildPath(pathToStart, pathToEnd, nullptr);
|
if (!endPathOk)
|
||||||
|
{
|
||||||
|
badEndNodes.push_back(e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
return !plan.steps.empty();
|
for (TravelNode* s : startCandidates)
|
||||||
|
{
|
||||||
|
if (std::find(badStartNodes.begin(), badStartNodes.end(), s) != badStartNodes.end())
|
||||||
|
continue;
|
||||||
|
if (!s || s == e)
|
||||||
|
continue;
|
||||||
|
if (!s->hasRouteTo(e))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
WorldPosition startNodePos = *s->getPosition();
|
||||||
|
|
||||||
|
// A* on the graph.
|
||||||
|
TravelNodeRoute route = GetNodeRoute(s, e, dynamic_cast<Player*>(bot));
|
||||||
|
if (route.isEmpty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// On a transport: skip ground validation, accept the route.
|
||||||
|
if (transportEntry)
|
||||||
|
{
|
||||||
|
path = route.BuildPath({botPos}, endProbe, bot);
|
||||||
|
route.cleanTempNodes();
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate bot -> startNode is pathable within maxStartDistance.
|
||||||
|
float const maxStartDistance = s->isTransport() ? 20.0f : 1.0f;
|
||||||
|
std::vector<WorldPosition> pathToStart;
|
||||||
|
bool startPathOk = false;
|
||||||
|
if (bot && botPos.GetMapId() == startNodePos.GetMapId())
|
||||||
|
{
|
||||||
|
pathToStart = botPos.getPathTo(startNodePos, bot);
|
||||||
|
startPathOk = startNodePos.isPathTo(pathToStart, maxStartDistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!startPathOk)
|
||||||
|
{
|
||||||
|
badStartNodes.push_back(s);
|
||||||
|
route.cleanTempNodes();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both ends validated — build and return.
|
||||||
|
path = route.BuildPath(pathToStart, endProbe, bot);
|
||||||
|
route.cleanTempNodes();
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No graph route found. Last-resort hearthstone fallback (reference
|
||||||
|
// also does this): if bot has hearthstone item and is alive, treat
|
||||||
|
// the bot's current position as a one-off node and try routing from
|
||||||
|
// it to each endCandidate via the hearthstone PortalNode edge.
|
||||||
|
if (Player* player = dynamic_cast<Player*>(bot))
|
||||||
|
{
|
||||||
|
if (player->IsAlive() && player->HasItemCount(6948, 1))
|
||||||
|
{
|
||||||
|
TravelNode* botNode = new TravelNode(botPos, "Bot Pos", false);
|
||||||
|
botNode->setPoint(botPos);
|
||||||
|
|
||||||
|
for (TravelNode* e : endCandidates)
|
||||||
|
{
|
||||||
|
if (!e || std::find(badEndNodes.begin(), badEndNodes.end(), e) != badEndNodes.end())
|
||||||
|
continue;
|
||||||
|
TravelNodeRoute route = GetNodeRoute(botNode, e, player);
|
||||||
|
if (route.isEmpty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Build the end-side path again for this candidate.
|
||||||
|
WorldPosition endNodePos = *e->getPosition();
|
||||||
|
std::vector<WorldPosition> endProbe;
|
||||||
|
if (endNodePos.GetMapId() == destination.GetMapId())
|
||||||
|
{
|
||||||
|
Unit* pathBot = (bot && bot->GetMapId() == destination.GetMapId()) ? bot : nullptr;
|
||||||
|
endProbe = endNodePos.getPathTo(destination, pathBot);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
endProbe = {endNodePos, destination};
|
||||||
|
|
||||||
|
route.addTempNodes({botNode}); // transfer ownership of botNode
|
||||||
|
path = route.BuildPath({botPos}, endProbe, bot);
|
||||||
|
route.cleanTempNodes();
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
delete botNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return path; // empty
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TravelNodeMap::cropUselessNode(TravelNode* startNode)
|
bool TravelNodeMap::cropUselessNode(TravelNode* startNode)
|
||||||
|
|||||||
@ -68,7 +68,9 @@
|
|||||||
//
|
//
|
||||||
// GetFullPath finds nearest nodes (zone-indexed), runs A* to get a node route, then
|
// 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).
|
// BuildPath assembles a flat TravelPath with typed waypoints (walk, portal, transport, flight).
|
||||||
// ExecuteTravelPlan iterates the path by stepIdx, dispatching on each point's PathNodeType.
|
// MoveFarTo re-resolves a fresh TravelPath each tick; UpcommingSpecialMovement cuts
|
||||||
|
// to the head segment when special; HandleSpecialMovement dispatches the matching
|
||||||
|
// action (portal interact, area-trigger marker, transport board, flight taxi).
|
||||||
// Cross-map travel is handled naturally by portal/transport edges in the A* graph.
|
// 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
|
// If setup cannot resolve (no node, no route, no flight), the bot teleports directly to the destination
|
||||||
@ -93,9 +95,10 @@ enum class TravelNodePathType : uint8
|
|||||||
areaTrigger = 2,
|
areaTrigger = 2,
|
||||||
transport = 3,
|
transport = 3,
|
||||||
flightPath = 4,
|
flightPath = 4,
|
||||||
// value 5 (teleportSpell) reserved — no generator emits it and no
|
// Teleport-spell edges (hearthstone, mage portals). Generated at A*
|
||||||
// consumer handles it. Re-add when a teleport-spell edge generator
|
// search start via PortalNode injection; consumed by
|
||||||
// / executor handler returns.
|
// HandleSpecialMovement's NODE_TELEPORT case.
|
||||||
|
teleportSpell = 5,
|
||||||
staticPortal = 6
|
staticPortal = 6
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -407,6 +410,26 @@ protected:
|
|||||||
// uint32 transportId = 0;
|
// uint32 transportId = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Synthetic A* node injected at search start to represent a teleport-spell
|
||||||
|
// (hearthstone, mage portal, etc.) as an alternative travel edge. Owned
|
||||||
|
// by GetNodeRoute caller; deleted after the route is built.
|
||||||
|
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 class PathNodeType : uint8
|
enum class PathNodeType : uint8
|
||||||
{
|
{
|
||||||
@ -416,8 +439,10 @@ enum class PathNodeType : uint8
|
|||||||
NODE_AREA_TRIGGER = 3,
|
NODE_AREA_TRIGGER = 3,
|
||||||
NODE_TRANSPORT = 4,
|
NODE_TRANSPORT = 4,
|
||||||
NODE_FLIGHTPATH = 5,
|
NODE_FLIGHTPATH = 5,
|
||||||
// value 6 (NODE_TELEPORT) reserved — no consumer; re-add when a
|
// Teleport-spell endpoint (hearthstone, mage portal). Emitted by
|
||||||
// teleport-spell handler / generator returns.
|
// TravelNodeRoute::BuildPath when traversing a teleportSpell-type
|
||||||
|
// edge; consumed by HandleSpecialMovement.
|
||||||
|
NODE_TELEPORT = 6,
|
||||||
NODE_STATIC_PORTAL = 7
|
NODE_STATIC_PORTAL = 7
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -492,6 +517,26 @@ public:
|
|||||||
|
|
||||||
bool makeShortCut(WorldPosition startPos, float maxDist, Unit* bot = nullptr);
|
bool makeShortCut(WorldPosition startPos, float maxDist, Unit* bot = nullptr);
|
||||||
|
|
||||||
|
// Trim the path up to (and optionally including) the given point.
|
||||||
|
// Returns true if the point was found. Used by upcoming special-
|
||||||
|
// movement detection to advance the path past a portal/transport/
|
||||||
|
// area-trigger node once the bot reaches it.
|
||||||
|
bool cutTo(PathNodePoint point, bool including);
|
||||||
|
|
||||||
|
// Returns true if the next reachable segment is a special-handling
|
||||||
|
// node (portal / area-trigger / transport / flightpath / teleport)
|
||||||
|
// and the bot is close enough / positioned right to handle it now.
|
||||||
|
// Trims the path up to that segment as a side effect. Caller then
|
||||||
|
// dispatches the matching special-movement handler on the new head.
|
||||||
|
bool UpcommingSpecialMovement(WorldPosition startPos, float maxDist, bool onTransport);
|
||||||
|
|
||||||
|
// Truncate the path at the first waypoint that would put the bot in
|
||||||
|
// range of a hostile creature (within attack range, in LOS, level-cap
|
||||||
|
// sane), at a non-walkable hop, after drifting beyond reactDistance
|
||||||
|
// from the start, or across a > 125-sqDist jump. Set ignoreEnemyTargets
|
||||||
|
// to suppress the hostile-target check (used by combat repositioning).
|
||||||
|
void ClipPath(PlayerbotAI* ai, Unit* mover, bool ignoreEnemyTargets = false);
|
||||||
|
|
||||||
// Reject paths the navmesh accepts but a player can't walk:
|
// Reject paths the navmesh accepts but a player can't walk:
|
||||||
// 2-point shortcut over 5y, or > 10y vertical drop with slope steeper than 2:1.
|
// 2-point shortcut over 5y, or > 10y vertical drop with slope steeper than 2:1.
|
||||||
static bool IsPathCheating(std::vector<WorldPosition> const& path,
|
static bool IsPathCheating(std::vector<WorldPosition> const& path,
|
||||||
@ -500,6 +545,24 @@ public:
|
|||||||
std::ostringstream const print();
|
std::ostringstream const print();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Returns the next-best-point iterator within maxDist from startPos:
|
||||||
|
// skips waypoints behind the bot, advances while shouldMoveToNextPoint
|
||||||
|
// allows, projects onto current segment to decide if the bot has
|
||||||
|
// already passed it.
|
||||||
|
std::vector<PathNodePoint>::iterator getNextPoint(WorldPosition startPos,
|
||||||
|
float maxDist,
|
||||||
|
bool onTransport);
|
||||||
|
|
||||||
|
// Heuristic for getNextPoint: decides whether the iterator should
|
||||||
|
// step forward to nextP. Stops at special nodes (area triggers,
|
||||||
|
// portals, transports, flight paths), at map boundaries, and when
|
||||||
|
// accumulated distance exceeds maxDist.
|
||||||
|
bool shouldMoveToNextPoint(WorldPosition startPos,
|
||||||
|
std::vector<PathNodePoint>::iterator beg,
|
||||||
|
std::vector<PathNodePoint>::iterator ed,
|
||||||
|
std::vector<PathNodePoint>::iterator p,
|
||||||
|
float& moveDist, float maxDist);
|
||||||
|
|
||||||
std::vector<PathNodePoint> fullPath;
|
std::vector<PathNodePoint> fullPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -512,6 +575,13 @@ public:
|
|||||||
{
|
{
|
||||||
nodes = nodes1;
|
nodes = nodes1;
|
||||||
}
|
}
|
||||||
|
TravelNodeRoute(std::vector<TravelNode*> nodes1,
|
||||||
|
std::vector<TravelNode*> const& tempNodes_)
|
||||||
|
{
|
||||||
|
nodes = nodes1;
|
||||||
|
if (!tempNodes_.empty())
|
||||||
|
addTempNodes(tempNodes_);
|
||||||
|
}
|
||||||
|
|
||||||
bool isEmpty() { return nodes.empty(); }
|
bool isEmpty() { return nodes.empty(); }
|
||||||
|
|
||||||
@ -523,6 +593,19 @@ public:
|
|||||||
|
|
||||||
std::vector<TravelNode*> getNodes() { return nodes; }
|
std::vector<TravelNode*> getNodes() { return nodes; }
|
||||||
|
|
||||||
|
// Take ownership of synthetic A* nodes (PortalNode etc.). Must call
|
||||||
|
// cleanTempNodes() to delete them when the route is no longer needed.
|
||||||
|
void addTempNodes(std::vector<TravelNode*> const& tempNodes_)
|
||||||
|
{
|
||||||
|
tempNodes.insert(tempNodes.end(), tempNodes_.begin(), tempNodes_.end());
|
||||||
|
}
|
||||||
|
void cleanTempNodes()
|
||||||
|
{
|
||||||
|
for (auto* n : tempNodes)
|
||||||
|
delete n;
|
||||||
|
tempNodes.clear();
|
||||||
|
}
|
||||||
|
|
||||||
TravelPath BuildPath(
|
TravelPath BuildPath(
|
||||||
std::vector<WorldPosition> pathToStart = {},
|
std::vector<WorldPosition> pathToStart = {},
|
||||||
std::vector<WorldPosition> pathToEnd = {},
|
std::vector<WorldPosition> pathToEnd = {},
|
||||||
@ -536,6 +619,7 @@ private:
|
|||||||
return std::find(nodes.begin(), nodes.end(), node);
|
return std::find(nodes.begin(), nodes.end(), node);
|
||||||
}
|
}
|
||||||
std::vector<TravelNode*> nodes;
|
std::vector<TravelNode*> nodes;
|
||||||
|
std::vector<TravelNode*> tempNodes; // owned synthetic nodes (PortalNode etc.)
|
||||||
};
|
};
|
||||||
|
|
||||||
// A node container to aid A* calculations with nodes.
|
// A node container to aid A* calculations with nodes.
|
||||||
@ -554,34 +638,6 @@ public:
|
|||||||
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;
|
|
||||||
uint32 expectedDuration{0}; // used to derive the lastMove delay
|
|
||||||
|
|
||||||
// Taxi scratch:
|
|
||||||
std::vector<uint32> route;
|
|
||||||
|
|
||||||
bool IsActive() const { return !steps.empty(); }
|
|
||||||
|
|
||||||
void Reset()
|
|
||||||
{
|
|
||||||
destination = WorldPosition();
|
|
||||||
steps.clear();
|
|
||||||
stepIdx = 0;
|
|
||||||
walkPoints.clear();
|
|
||||||
expectedDuration = 0;
|
|
||||||
route.clear();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// The container of all nodes.
|
// The container of all nodes.
|
||||||
class TravelNodeMap
|
class TravelNodeMap
|
||||||
{
|
{
|
||||||
@ -711,8 +767,11 @@ public:
|
|||||||
// empty static vector for unknown zones.
|
// empty static vector for unknown zones.
|
||||||
std::vector<TravelNode*> const& GetNodesInZone(uint32 zoneId) const;
|
std::vector<TravelNode*> const& GetNodesInZone(uint32 zoneId) const;
|
||||||
|
|
||||||
bool GetFullPath(TravelPlan& plan, WorldPosition botPos,
|
// Resolve a full TravelPath from botPos to destination. Returns an
|
||||||
uint32 botZoneId, WorldPosition destination, Unit* bot = nullptr);
|
// empty TravelPath if no graph route + mmap stitch is reachable;
|
||||||
|
// the caller is then expected to fall back to a single-point path.
|
||||||
|
TravelPath GetFullPath(WorldPosition botPos, uint32 botZoneId,
|
||||||
|
WorldPosition destination, Unit* bot = nullptr);
|
||||||
|
|
||||||
// Resolve A* route between two world positions (returns node vector)
|
// Resolve A* route between two world positions (returns node vector)
|
||||||
std::vector<TravelNode*> ResolveRoute(WorldPosition startPos,
|
std::vector<TravelNode*> ResolveRoute(WorldPosition startPos,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user