mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
Compare commits
No commits in common. "e92af1cc0631619e157ef01801f78a41d7414e97" and "7772dc4c0d774ec6858a5e19ee45d1696f3015b0" have entirely different histories.
e92af1cc06
...
7772dc4c0d
@ -1065,16 +1065,6 @@ AiPlayerbot.EnableNewRpgStrategy = 1
|
||||
# Default: 0 (disabled)
|
||||
AiPlayerbot.EnableTravelNodes = 0
|
||||
|
||||
# Transport ride mode (travel node graph only):
|
||||
# 0 = bot walks to dock, teleport-snaps onto transport, rides, teleport-snaps off
|
||||
# 1 = bot teleports directly across the transport route, skipping the ride
|
||||
# Default 0 is the visible behavior. Set to 1 for invisible/random-bot mass
|
||||
# travel performance — the bot never actually boards anything.
|
||||
# (AC has no transport-surface mmap, so an on-deck walking mode can't be
|
||||
# faithfully implemented; the on-board phase always teleport-snaps.)
|
||||
# Default: 0
|
||||
AiPlayerbot.TransportSkipRide = 0
|
||||
|
||||
# Control probability weights for RPG status of bots. Takes effect only when the status meets its premise.
|
||||
# Sum of weights need not be 100. Set to 0 to disable the status.
|
||||
#
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -19,6 +19,7 @@ class Unit;
|
||||
class WorldObject;
|
||||
class Position;
|
||||
|
||||
#define ANGLE_45_DEG (static_cast<float>(M_PI) / 4.f)
|
||||
#define ANGLE_90_DEG M_PI_2
|
||||
#define ANGLE_120_DEG (2.f * static_cast<float>(M_PI) / 3.f)
|
||||
|
||||
@ -56,54 +57,7 @@ protected:
|
||||
bool MoveTo(uint32 mapId, float x, float y, float z, bool idle = false, bool react = false,
|
||||
bool normal_only = false, bool exact_waypoint = false,
|
||||
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, bool lessDelay = false,
|
||||
bool backwards = false, bool ignoreEnemyTargets = false);
|
||||
|
||||
// Path-aware funnel mirroring the reference movement implementation.
|
||||
// Runs UpdateMovementState + IsMovingAllowed + WaitForTransport gates,
|
||||
// applies the targetPosRecalcDistance short-stop, resolves a TravelPath
|
||||
// via ResolveMovePath (which gates graph A* by sightDistance), trims
|
||||
// with makeShortCut, handles special head segments
|
||||
// (portal/area-trigger/transport/flight) via HandleSpecialMovement,
|
||||
// clips at hostile creatures via ClipPath (unless ignoreEnemyTargets),
|
||||
// and dispatches the resulting walk via DispatchMovement.
|
||||
// MoveTo(mapId,...) delegates here unless an intentional bypass
|
||||
// (exact_waypoint / disableMoveSplinePath / flying / swimming /
|
||||
// backwards) routes the move straight to DoMovePoint.
|
||||
// `react=true` opts the move out of the end-of-dispatch
|
||||
// WaitForReach AI-loop block — combat callers should set this so the
|
||||
// bot can keep re-evaluating mid-chase. Default false matches the
|
||||
// reference's MoveTo2 default.
|
||||
bool MoveTo2(WorldPosition endPos,
|
||||
bool idle = false, bool react = false,
|
||||
bool noPath = false, bool ignoreEnemyTargets = false,
|
||||
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL,
|
||||
bool lessDelay = false);
|
||||
|
||||
// Centralized walk dispatch. Mirrors the reference's DispatchMovement
|
||||
// shape: takes a TravelPath, builds the PointsArray internally,
|
||||
// applies inactive-bot teleport carve-out, masterWalking mode,
|
||||
// pre-dispatch state cleanup (clear emote, stand, interrupt cast),
|
||||
// transport-passenger coordinate sandwich
|
||||
// (CalculatePassengerPosition → UpdateAllowedPositionZ → Offset)
|
||||
// around the per-point Z snap, mm.Clear → MovePoint(last) →
|
||||
// MoveSplinePath. Caches the destination + duration on lastMove.
|
||||
//
|
||||
// Divergence from reference: reference ends with WaitForReach(size)
|
||||
// which blocks the AI loop until the move completes. AC's combat
|
||||
// callers (ReachCombatTo) currently funnel through MoveTo → MoveTo2
|
||||
// → DispatchMovement; blocking the AI loop here would suspend combat
|
||||
// re-evaluation for the full move duration. Until combat dispatch is
|
||||
// restructured to bypass MoveTo2, the WaitForReach is deliberately
|
||||
// omitted.
|
||||
// `react=true` skips the end-of-dispatch WaitForReach so the AI
|
||||
// loop isn't blocked while the spline plays — combat callers use
|
||||
// this to keep re-evaluating mid-chase.
|
||||
bool DispatchMovement(TravelPath path,
|
||||
WorldPosition dest,
|
||||
char const* label,
|
||||
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL,
|
||||
bool lessDelay = false,
|
||||
bool react = false);
|
||||
bool backwards = false);
|
||||
bool MoveTo(WorldObject* target, float distance = 0.0f,
|
||||
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
|
||||
bool MoveNear(WorldObject* target, float distance = sPlayerbotAIConfig.contactDistance,
|
||||
@ -111,22 +65,10 @@ protected:
|
||||
float GetFollowAngle();
|
||||
bool Follow(Unit* target, float distance = sPlayerbotAIConfig.followDistance);
|
||||
bool Follow(Unit* target, float distance, float angle);
|
||||
// Handles the cross-transport follow case: when bot and target are
|
||||
// on different transports (or one is off-transport) and within
|
||||
// sight, this disembarks the bot from its current transport (if
|
||||
// any), teleports it to the target's position, and boards the
|
||||
// target's transport (if any). Returns true if the transport
|
||||
// transition was performed this tick (caller should skip the
|
||||
// engine-level follow for this tick).
|
||||
bool FollowOnTransport(Unit* target);
|
||||
bool ChaseTo(WorldObject* obj, float distance = 0.0f);
|
||||
bool ReachCombatTo(Unit* target, float distance = 0.0f);
|
||||
float MoveDelay(float distance, bool backwards = false);
|
||||
void WaitForReach(float distance);
|
||||
// PointsArray overload: sums segment distances and calls the float
|
||||
// version. Matches the reference's WaitForReach(PointsArray) used at
|
||||
// the end of DispatchMovement.
|
||||
void WaitForReach(Movement::PointsArray const& path);
|
||||
void SetNextMovementDelay(float delayMillis);
|
||||
bool IsMovingAllowed(WorldObject* target);
|
||||
bool IsDuplicateMove(float x, float y, float z);
|
||||
|
||||
@ -12,6 +12,7 @@ LastMovement::LastMovement() { clear(); }
|
||||
LastMovement::LastMovement(LastMovement& other)
|
||||
: taxiNodes(other.taxiNodes),
|
||||
taxiMaster(other.taxiMaster),
|
||||
lastFollow(other.lastFollow),
|
||||
lastAreaTrigger(other.lastAreaTrigger),
|
||||
lastFlee(other.lastFlee)
|
||||
{
|
||||
@ -26,6 +27,7 @@ void LastMovement::clear()
|
||||
{
|
||||
lastMoveShort = WorldPosition();
|
||||
lastPath.clear();
|
||||
lastFollow = nullptr;
|
||||
lastAreaTrigger = 0;
|
||||
lastFlee = 0;
|
||||
nextTeleport = 0;
|
||||
@ -34,18 +36,17 @@ void LastMovement::clear()
|
||||
lastTransportEntry = 0;
|
||||
}
|
||||
|
||||
void LastMovement::Set([[maybe_unused]] Unit* follow)
|
||||
void LastMovement::Set(Unit* follow)
|
||||
{
|
||||
// Legacy signature — `follow` is ignored (lastFollow field removed).
|
||||
// The function still serves callers that want a soft-reset:
|
||||
// clears short + path, resets msTime/priority via the chain below.
|
||||
Set(0, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
|
||||
setShort(WorldPosition());
|
||||
setPath(TravelPath());
|
||||
lastFollow = follow;
|
||||
}
|
||||
|
||||
void LastMovement::Set(uint32 mapId, float x, float y, float z, float ori, float delayTime, MovementPriority pri)
|
||||
{
|
||||
lastFollow = nullptr;
|
||||
lastMoveShort = WorldPosition(mapId, x, y, z, ori);
|
||||
msTime = getMSTime();
|
||||
priority = pri;
|
||||
@ -54,6 +55,7 @@ void LastMovement::Set(uint32 mapId, float x, float y, float z, float ori, float
|
||||
void LastMovement::setShort(WorldPosition point)
|
||||
{
|
||||
lastMoveShort = point;
|
||||
lastFollow = nullptr;
|
||||
}
|
||||
|
||||
void LastMovement::setPath(TravelPath path) { lastPath = path; }
|
||||
|
||||
@ -33,6 +33,7 @@ public:
|
||||
{
|
||||
taxiNodes = other.taxiNodes;
|
||||
taxiMaster = other.taxiMaster;
|
||||
lastFollow = other.lastFollow;
|
||||
lastAreaTrigger = other.lastAreaTrigger;
|
||||
lastMoveShort = other.lastMoveShort;
|
||||
lastPath = other.lastPath;
|
||||
@ -52,6 +53,7 @@ public:
|
||||
|
||||
std::vector<uint32> taxiNodes;
|
||||
ObjectGuid taxiMaster;
|
||||
Unit* lastFollow;
|
||||
uint32 lastAreaTrigger;
|
||||
time_t lastFlee;
|
||||
WorldPosition lastMoveShort;
|
||||
|
||||
@ -9,8 +9,12 @@
|
||||
#include "Creature.h"
|
||||
#include "G3D/Vector2.h"
|
||||
#include "GameObject.h"
|
||||
#include "GossipDef.h"
|
||||
#include "GridTerrainData.h"
|
||||
#include "IVMapMgr.h"
|
||||
#include "Item.h"
|
||||
#include "ItemTemplate.h"
|
||||
#include "LootMgr.h"
|
||||
#include "Map.h"
|
||||
#include "ModelIgnoreFlags.h"
|
||||
#include "MotionMaster.h"
|
||||
@ -30,6 +34,7 @@
|
||||
#include "PlayerbotTextMgr.h"
|
||||
#include "Playerbots.h"
|
||||
#include "Position.h"
|
||||
#include "QuestDef.h"
|
||||
#include "Random.h"
|
||||
#include "RandomPlayerbotMgr.h"
|
||||
#include "SharedDefines.h"
|
||||
@ -48,7 +53,218 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
EmitDebugMove("MoveFar", "empty-dest", 0.0f, 0.0f, 0.0f);
|
||||
return false;
|
||||
}
|
||||
return MoveTo2(dest);
|
||||
|
||||
UpdateMovementState();
|
||||
|
||||
if (!IsMovingAllowed())
|
||||
{
|
||||
EmitDebugMove("MoveFar", "cant-move",
|
||||
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resume a transport ride if we're still on the same boat as last tick.
|
||||
if (WaitForTransport())
|
||||
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 = botPos.distance(dest);
|
||||
if (totalDistance < sPlayerbotAIConfig.targetPosRecalcDistance)
|
||||
{
|
||||
if (!lastMove.lastPath.empty() &&
|
||||
lastMove.lastPath.getBack().distance(dest) <= totalDistance)
|
||||
lastMove.clear();
|
||||
bot->StopMoving();
|
||||
EmitDebugMove("MoveFar", "arrived",
|
||||
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Per-tick re-resolve: rebuild the TravelPath from the bot's current
|
||||
// position every tick (10% reuse short-circuits via the cached
|
||||
// lastPath). Recovers naturally from knockback, off-route drift,
|
||||
// destination changes, and blocked waypoints.
|
||||
TravelPath path = ResolveMovePath(botPos, dest, lastMove);
|
||||
lastMove.setPath(path);
|
||||
|
||||
if (path.empty())
|
||||
{
|
||||
EmitDebugMove("MoveFar", "no-path",
|
||||
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Trim leading waypoints behind the bot, bridge with mmap probe if
|
||||
// the new head requires it. May empty the path (collapsed) — let
|
||||
// the next tick rebuild from a fresh start.
|
||||
path.makeShortCut(botPos, sPlayerbotAIConfig.reactDistance, bot);
|
||||
if (path.empty())
|
||||
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,
|
||||
Movement::PointsArray& points,
|
||||
char const* label)
|
||||
{
|
||||
if (points.size() < 2)
|
||||
return false;
|
||||
|
||||
// MoveFarTo runs makeShortCut + setPath upstream now, so no need
|
||||
// for the local prefix-trim or lastMove.setPath here.
|
||||
|
||||
for (auto& pt : points)
|
||||
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
|
||||
|
||||
// ClipPath now runs at MoveFarTo level on the TravelPath before the
|
||||
// points array is built. No per-dispatch clip here.
|
||||
|
||||
if (points.size() < 2)
|
||||
return false;
|
||||
|
||||
G3D::Vector3 const& last = points.back();
|
||||
|
||||
float totalDist = 0.f;
|
||||
for (size_t i = 1; i < points.size(); ++i)
|
||||
totalDist += (points[i] - points[i - 1]).length();
|
||||
|
||||
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
|
||||
|
||||
// Skip cosmetic walking for random bots with no nearby player —
|
||||
// teleport to the path tail and schedule a cooldown instead.
|
||||
if (sRandomPlayerbotMgr.IsRandomBot(bot))
|
||||
{
|
||||
WorldPosition tail(dest.GetMapId(), last.x, last.y, last.z);
|
||||
time_t now = time(nullptr);
|
||||
if (totalDist > sPlayerbotAIConfig.reactDistance &&
|
||||
lastMove.nextTeleport <= now &&
|
||||
!botAI->HasPlayerNearby(&tail))
|
||||
{
|
||||
float speed = std::max(bot->GetSpeed(MOVE_RUN), 0.1f);
|
||||
lastMove.nextTeleport = now + (time_t)(totalDist / speed);
|
||||
|
||||
EmitDebugMove("MoveFar", "teleport",
|
||||
tail.GetPositionX(), tail.GetPositionY(), tail.GetPositionZ());
|
||||
|
||||
WorldPosition botPos(bot);
|
||||
return bot->TeleportTo(dest.GetMapId(),
|
||||
tail.GetPositionX(), tail.GetPositionY(),
|
||||
tail.GetPositionZ(),
|
||||
botPos.getAngleTo(tail));
|
||||
}
|
||||
}
|
||||
|
||||
// Match master's walk pace when they're walking and within 5y.
|
||||
ForcedMovement moveMode = FORCED_MOVEMENT_RUN;
|
||||
if (Player* master = botAI->GetMaster())
|
||||
{
|
||||
if (bot->IsFriendlyTo(master) && master->IsWalking() &&
|
||||
bot->GetExactDist2d(master) < 5.0f)
|
||||
{
|
||||
moveMode = FORCED_MOVEMENT_WALK;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear emote/sit/cast so the spline can begin cleanly.
|
||||
bot->ClearEmoteState();
|
||||
if (!bot->IsStandState())
|
||||
bot->SetStandState(UNIT_STAND_STATE_STAND);
|
||||
if (bot->IsNonMeleeSpellCast(true))
|
||||
bot->InterruptNonMeleeSpells(true);
|
||||
|
||||
bot->GetMotionMaster()->Clear();
|
||||
bot->GetMotionMaster()->MoveSplinePath(&points, moveMode);
|
||||
|
||||
EmitDebugMove("MoveFar", label, last.x, last.y, last.z);
|
||||
|
||||
// WaitForReach: leave ~10y headroom on long paths.
|
||||
float waitDist = totalDist > sPlayerbotAIConfig.reactDistance
|
||||
? std::max(totalDist - 10.0f, 0.0f) : totalDist;
|
||||
UnitMoveType const speedType = (moveMode == FORCED_MOVEMENT_WALK) ? MOVE_WALK : MOVE_RUN;
|
||||
float speed = std::max(bot->GetSpeed(speedType), 0.1f);
|
||||
float duration = 1000.0f * (waitDist / speed) + sPlayerbotAIConfig.reactDelay;
|
||||
duration = std::min(duration, (float)sPlayerbotAIConfig.maxWaitForMove);
|
||||
if (duration < 0.0f)
|
||||
duration = 0.0f;
|
||||
|
||||
lastMove.Set(bot->GetMapId(), last.x, last.y, last.z,
|
||||
bot->GetOrientation(), (uint32)duration,
|
||||
MovementPriority::MOVEMENT_NORMAL);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance)
|
||||
|
||||
@ -82,6 +82,15 @@ protected:
|
||||
bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
|
||||
bool CheckRpgStatusAvailable(NewRpgStatus status);
|
||||
|
||||
private:
|
||||
// Centralized dispatch helper. Applies underwater fixup, ClipPath
|
||||
// (truncate at first hostile in attack range with LOS, level+5 cap),
|
||||
// inactive-bot teleport (with self-bot carve-out), masterWalking
|
||||
// mode, pre-dispatch state cleanup, then dispatches via
|
||||
// MoveSplinePath and schedules via WaitForReach formula.
|
||||
bool DispatchPathPoints(WorldPosition const& dest,
|
||||
Movement::PointsArray& points,
|
||||
char const* label);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@ -274,27 +274,6 @@ bool WorldPosition::isUnderWater()
|
||||
: false;
|
||||
};
|
||||
|
||||
bool WorldPosition::setAtWaterSurface()
|
||||
{
|
||||
if (!isInWater() && !isUnderWater())
|
||||
return false;
|
||||
Map* map = getMap();
|
||||
if (!map)
|
||||
return false;
|
||||
// Returns the water level when liquid is present; falls back to
|
||||
// ground level otherwise. Our isInWater/isUnderWater preconditions
|
||||
// ensure liquid exists, so the +0.5y nudge lands the point on top
|
||||
// of the surface (matches the reference's surface snap).
|
||||
float const level = map->GetWaterOrGroundLevel(PHASEMASK_NORMAL,
|
||||
GetPositionX(),
|
||||
GetPositionY(),
|
||||
GetPositionZ());
|
||||
if (level <= INVALID_HEIGHT)
|
||||
return false;
|
||||
setZ(level + 0.5f);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WorldPosition::IsValid()
|
||||
{
|
||||
return !(GetMapId() == MAPID_INVALID && GetPositionX() == 0 && GetPositionY() == 0 && GetPositionZ() == 0);
|
||||
@ -744,13 +723,6 @@ std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos
|
||||
// fire — apply the same bot cost biases here so generated paths
|
||||
// match what bots prefer at runtime (STEEP/water are reachable
|
||||
// but not preferred).
|
||||
//
|
||||
// Reference also applies setAreaCost(12, 5) + setAreaCost(13, 20)
|
||||
// here. Not ported: reference and AC use different mmap generators
|
||||
// and Detour area-id assignments diverge — raw IDs 12/13 are
|
||||
// unlikely to match any polys on AC's navmesh and could no-op or
|
||||
// bias something unintended. If we ever regenerate mmaps to match
|
||||
// the reference dataset, revisit.
|
||||
path.SetNavTerrainCost(NAV_GROUND_STEEP, 5.0f);
|
||||
path.SetNavTerrainCost(NAV_WATER, 10.0f);
|
||||
auto result = getPathStepFrom(startPos, path);
|
||||
@ -1971,6 +1943,93 @@ void TravelMgr::LoadQuestTravelTable()
|
||||
units.push_back(t_unit);
|
||||
}
|
||||
|
||||
/*
|
||||
// 0 1 2 3 4 5 6 7 8
|
||||
std::string const query = "SELECT 0,guid,id,map,position_x,position_y,position_z,orientation, (SELECT COUNT(*) FROM
|
||||
creature k WHERE c.id1 = k.id1) FROM creature c UNION ALL SELECT
|
||||
1,guid,id,map,position_x,position_y,position_z,orientation, (SELECT COUNT(*) FROM gameobject h WHERE h.id = g.id)
|
||||
FROM gameobject g";
|
||||
|
||||
QueryResult result = WorldDatabase.Query(query.c_str());
|
||||
if (result)
|
||||
{
|
||||
do
|
||||
{
|
||||
Field* fields = result->Fetch();
|
||||
|
||||
t_unit.type = fields[0].Get<uint32>();
|
||||
t_unit.guid = fields[1].Get<uint32>();
|
||||
t_unit.entry = fields[2].Get<uint32>();
|
||||
t_unit.map = fields[3].Get<uint32>();
|
||||
t_unit.x = fields[4].Get<float>();
|
||||
t_unit.y = fields[5].Get<float>();
|
||||
t_unit.z = fields[6].Get<float>();
|
||||
t_unit.o = fields[7].Get<float>();
|
||||
t_unit.c = uint32(fields[8].Get<uint64>());
|
||||
|
||||
units.push_back(t_unit);
|
||||
|
||||
} while (result->NextRow());
|
||||
|
||||
LOG_INFO("playerbots", ">> Loaded {} units locations.", units.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERROR("playerbots", ">> Error loading units locations.");
|
||||
}
|
||||
|
||||
query = "SELECT 0, 0, id, quest FROM creature_queststarter UNION ALL SELECT 0, 1, id, quest FROM creature_questender
|
||||
UNION ALL SELECT 1, 0, id, quest FROM gameobject_queststarter UNION ALL SELECT 1, 1, id, quest FROM
|
||||
gameobject_questender"; result = WorldDatabase.Query(query.c_str());
|
||||
|
||||
if (result)
|
||||
{
|
||||
do
|
||||
{
|
||||
Field* fields = result->Fetch();
|
||||
|
||||
t_rel.type = fields[0].Get<uint32>();
|
||||
t_rel.role = fields[1].Get<uint32>();
|
||||
t_rel.entry = fields[2].Get<uint32>();
|
||||
t_rel.questId = fields[3].Get<uint32>();
|
||||
|
||||
relations.push_back(t_rel);
|
||||
|
||||
} while (result->NextRow());
|
||||
|
||||
LOG_INFO("playerbots", ">> Loaded {} relations.", relations.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERROR("playerbots", ">> Error loading relations.");
|
||||
}
|
||||
|
||||
query = "SELECT 0, ct.entry, item FROM creature_template ct JOIN creature_loot_template clt ON (ct.lootid =
|
||||
clt.entry) UNION ALL SELECT 0, entry, item FROM npc_vendor UNION ALL SELECT 1, gt.entry, item FROM
|
||||
gameobject_template gt JOIN gameobject_loot_template glt ON (gt.TYPE = 3 AND gt.DATA1 = glt.entry)"; result =
|
||||
WorldDatabase.Query(query.c_str());
|
||||
|
||||
if (result)
|
||||
{
|
||||
do
|
||||
{
|
||||
Field* fields = result->Fetch();
|
||||
|
||||
t_loot.type = fields[0].Get<uint32>();
|
||||
t_loot.entry = fields[1].Get<uint32>();
|
||||
t_loot.item = fields[2].Get<uint32>();
|
||||
|
||||
loots.push_back(t_loot);
|
||||
|
||||
} while (result->NextRow());
|
||||
|
||||
LOG_INFO("playerbots", ">> Loaded {} loot lists.", loots.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERROR("playerbots", ">> Error loading loot lists.");
|
||||
}
|
||||
*/
|
||||
|
||||
LOG_INFO("playerbots", "Loading quest data.");
|
||||
|
||||
@ -2056,6 +2115,164 @@ void TravelMgr::LoadQuestTravelTable()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
if (loadQuestData && false)
|
||||
{
|
||||
for (auto& questId : questIds)
|
||||
{
|
||||
Quest* quest = questMap.find(questId)->second;
|
||||
|
||||
QuestContainer* container = new QuestContainer;
|
||||
QuestTravelDestination* loc = nullptr;
|
||||
WorldPosition point;
|
||||
|
||||
bool hasError = false;
|
||||
|
||||
//Relations
|
||||
for (auto& r : relations)
|
||||
{
|
||||
if (questId != r.questId)
|
||||
continue;
|
||||
|
||||
int32 entry = r.type == 0 ? r.entry : r.entry * -1;
|
||||
|
||||
loc = new QuestRelationTravelDestination(r.questId, entry, r.role, sPlayerbotAIConfig.tooCloseDistance,
|
||||
sPlayerbotAIConfig.sightDistance); loc->setExpireDelay(5 * 60 * 1000); loc->setMaxVisitors(15, 0);
|
||||
|
||||
for (auto& u : units)
|
||||
{
|
||||
if (r.type != u.type || r.entry != u.entry)
|
||||
continue;
|
||||
|
||||
int32 guid = u.type == 0 ? u.guid : u.guid * -1;
|
||||
|
||||
point = WorldPosition(u.map, u.x, u.y, u.z, u.o);
|
||||
loc->addPoint(&point);
|
||||
}
|
||||
|
||||
if (loc->getPoints(0).empty())
|
||||
{
|
||||
logQuestError(1, quest, r.role, entry);
|
||||
delete loc;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (r.role == 0)
|
||||
{
|
||||
container->questGivers.push_back(loc);
|
||||
}
|
||||
else
|
||||
container->questTakers.push_back(loc);
|
||||
|
||||
}
|
||||
|
||||
//Mobs
|
||||
for (uint32 i = 0; i < 4; i++)
|
||||
{
|
||||
if (quest->RequiredNpcOrGoCount[i] == 0)
|
||||
continue;
|
||||
|
||||
uint32 reqEntry = quest->RequiredNpcOrGo[i];
|
||||
|
||||
loc = new QuestObjectiveTravelDestination(questId, reqEntry, i, sPlayerbotAIConfig.tooCloseDistance,
|
||||
sPlayerbotAIConfig.sightDistance); loc->setExpireDelay(1 * 60 * 1000); loc->setMaxVisitors(100, 1);
|
||||
|
||||
for (auto& u : units)
|
||||
{
|
||||
int32 entry = u.type == 0 ? u.entry : u.entry * -1;
|
||||
|
||||
if (entry != reqEntry)
|
||||
continue;
|
||||
|
||||
int32 guid = u.type == 0 ? u.guid : u.guid * -1;
|
||||
|
||||
point = WorldPosition(u.map, u.x, u.y, u.z, u.o);
|
||||
loc->addPoint(&point);
|
||||
}
|
||||
|
||||
if (loc->getPoints(0).empty())
|
||||
{
|
||||
logQuestError(2, quest, i, reqEntry);
|
||||
|
||||
delete loc;
|
||||
hasError = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
container->questObjectives.push_back(loc);
|
||||
}
|
||||
|
||||
//Loot
|
||||
for (uint32 i = 0; i < 4; i++)
|
||||
{
|
||||
if (quest->RequiredItemCount[i] == 0)
|
||||
continue;
|
||||
|
||||
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(quest->RequiredItemId[i]);
|
||||
if (!proto)
|
||||
{
|
||||
logQuestError(3, quest, i, 0, quest->RequiredItemId[i]);
|
||||
hasError = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32 foundLoot = 0;
|
||||
|
||||
for (auto& l : loots)
|
||||
{
|
||||
if (l.item != quest->RequiredItemId[i])
|
||||
continue;
|
||||
|
||||
int32 entry = l.type == 0 ? l.entry : l.entry * -1;
|
||||
|
||||
loc = new QuestObjectiveTravelDestination(questId, entry, i, sPlayerbotAIConfig.tooCloseDistance,
|
||||
sPlayerbotAIConfig.sightDistance, l.item); loc->setExpireDelay(1 * 60 * 1000); loc->setMaxVisitors(100, 1);
|
||||
|
||||
for (auto& u : units)
|
||||
{
|
||||
if (l.type != u.type || l.entry != u.entry)
|
||||
continue;
|
||||
|
||||
int32 guid = u.type == 0 ? u.guid : u.guid * -1;
|
||||
|
||||
point = WorldPosition(u.map, u.x, u.y, u.z, u.o);
|
||||
loc->addPoint(&point);
|
||||
}
|
||||
|
||||
if (loc->getPoints(0).empty())
|
||||
{
|
||||
logQuestError(4, quest, i, entry, quest->RequiredItemId[i]);
|
||||
delete loc;
|
||||
continue;
|
||||
}
|
||||
|
||||
container->questObjectives.push_back(loc);
|
||||
|
||||
foundLoot++;
|
||||
}
|
||||
|
||||
if (foundLoot == 0)
|
||||
{
|
||||
hasError = true;
|
||||
logQuestError(5, quest, i, 0, quest->RequiredItemId[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (container->questTakers.empty())
|
||||
logQuestError(7, quest);
|
||||
|
||||
if (!container->questGivers.empty() || !container->questTakers.empty() || hasError)
|
||||
{
|
||||
quests.insert(std::make_pair(questId, container));
|
||||
|
||||
for (auto loc : container->questGivers)
|
||||
questGivers.push_back(loc);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("playerbots", ">> Loaded {} quest details.", questIds.size());
|
||||
}
|
||||
*/
|
||||
|
||||
WorldPosition point;
|
||||
|
||||
@ -2190,7 +2407,531 @@ void TravelMgr::LoadQuestTravelTable()
|
||||
|
||||
// Node loading/generation is handled by TravelNodeMap::Init() called from TravelMgr::Init().
|
||||
|
||||
/*
|
||||
bool fullNavPointReload = false;
|
||||
bool storeNavPointReload = true;
|
||||
|
||||
if (!fullNavPointReload && true)
|
||||
TravelNodeStore::loadNodes();
|
||||
|
||||
//TravelNodeMap::instance().loadNodeStore();
|
||||
|
||||
for (auto node : TravelNodeMap::instance().getNodes())
|
||||
{
|
||||
node->setLinked(true);
|
||||
}
|
||||
|
||||
bool reloadNavigationPoints = false || fullNavPointReload || storeNavPointReload;
|
||||
|
||||
if (reloadNavigationPoints)
|
||||
{
|
||||
LOG_INFO("playerbots", "Loading navigation points");
|
||||
|
||||
//Npc nodes
|
||||
|
||||
WorldPosition pos;
|
||||
|
||||
for (auto& u : units)
|
||||
{
|
||||
if (u.type != 0)
|
||||
continue;
|
||||
|
||||
CreatureTemplate const* cInfo = sObjectMgr->GetCreatureTemplate(u.entry);
|
||||
if (!cInfo)
|
||||
continue;
|
||||
|
||||
std::vector<uint32> allowedNpcFlags;
|
||||
|
||||
allowedNpcFlags.push_back(UNIT_NPC_FLAG_INNKEEPER);
|
||||
allowedNpcFlags.push_back(UNIT_NPC_FLAG_FLIGHTMASTER);
|
||||
//allowedNpcFlags.push_back(UNIT_NPC_FLAG_QUESTGIVER);
|
||||
|
||||
for (std::vector<uint32>::iterator i = allowedNpcFlags.begin(); i != allowedNpcFlags.end(); ++i)
|
||||
{
|
||||
if ((cInfo->npcflag & *i) != 0)
|
||||
{
|
||||
pos = WorldPosition(u.map, u.x, u.y, u.z, u.o);
|
||||
|
||||
std::string const nodeName = pos.getAreaName(false);
|
||||
if ((cInfo->npcflag & UNIT_NPC_FLAG_INNKEEPER) != 0)
|
||||
nodeName += " innkeeper";
|
||||
else
|
||||
nodeName += " flightMaster";
|
||||
|
||||
TravelNodeMap::instance().addNode(&pos, nodeName, true, true);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Build flight paths
|
||||
for (uint32 i = 0; i < sTaxiPathStore.GetNumRows(); ++i)
|
||||
{
|
||||
TaxiPathEntry const* taxiPath = sTaxiPathStore.LookupEntry(i);
|
||||
|
||||
if (!taxiPath)
|
||||
continue;
|
||||
|
||||
TaxiNodesEntry const* startTaxiNode = sTaxiNodesStore.LookupEntry(taxiPath->from);
|
||||
if (!startTaxiNode)
|
||||
continue;
|
||||
|
||||
TaxiNodesEntry const* endTaxiNode = sTaxiNodesStore.LookupEntry(taxiPath->to);
|
||||
if (!endTaxiNode)
|
||||
continue;
|
||||
|
||||
TaxiPathNodeList const& nodes = sTaxiPathNodesByPath[taxiPath->ID];
|
||||
if (nodes.empty())
|
||||
continue;
|
||||
|
||||
WorldPosition startPos(startTaxiNode->map_id, startTaxiNode->x, startTaxiNode->y, startTaxiNode->z);
|
||||
WorldPosition endPos(endTaxiNode->map_id, endTaxiNode->x, endTaxiNode->y, endTaxiNode->z);
|
||||
|
||||
TravelNode* startNode = TravelNodeMap::instance().getNode(&startPos, nullptr, 15.0f);
|
||||
TravelNode* endNode = TravelNodeMap::instance().getNode(&endPos, nullptr, 15.0f);
|
||||
|
||||
if (!startNode || !endNode)
|
||||
continue;
|
||||
|
||||
std::vector<WorldPosition> ppath;
|
||||
|
||||
for (auto& n : nodes)
|
||||
ppath.push_back(WorldPosition(n->mapid, n->x, n->y, n->z, 0.0));
|
||||
|
||||
float totalTime = startPos.getPathLength(ppath) / (450 * 8.0f);
|
||||
|
||||
TravelNodePath travelPath(0.1f, totalTime, (uint8) TravelNodePathType::flightPath, i, true);
|
||||
travelPath.setPath(ppath);
|
||||
|
||||
startNode->setPathTo(endNode, travelPath);
|
||||
}
|
||||
|
||||
//Unique bosses
|
||||
for (auto& u : units)
|
||||
{
|
||||
if (u.type != 0)
|
||||
continue;
|
||||
|
||||
CreatureTemplate const* cInfo = sObjectMgr->GetCreatureTemplate(u.entry);
|
||||
if (!cInfo)
|
||||
continue;
|
||||
|
||||
pos = WorldPosition(u.map, u.x, u.y, u.z, u.o);
|
||||
|
||||
if (cInfo->rank == 3 || (cInfo->rank == 1 && !pos.isOverworld() && u.c == 1))
|
||||
{
|
||||
std::string const nodeName = cInfo->Name;
|
||||
TravelNodeMap::instance().addNode(&pos, nodeName, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
std::map<uint8, std::string> startNames;
|
||||
startNames[RACE_HUMAN] = "Human";
|
||||
startNames[RACE_ORC] = "Orc and Troll";
|
||||
startNames[RACE_DWARF] = "Dwarf and Gnome";
|
||||
startNames[RACE_NIGHTELF] = "Night Elf";
|
||||
startNames[RACE_UNDEAD_PLAYER] = "Undead";
|
||||
startNames[RACE_TAUREN] = "Tauren";
|
||||
startNames[RACE_GNOME] = "Dwarf and Gnome";
|
||||
startNames[RACE_TROLL] = "Orc and Troll";
|
||||
startNames[RACE_DRAENEI] = "Draenei";
|
||||
startNames[RACE_BLOODELF] = "Blood Elf";
|
||||
|
||||
for (uint32 i = 0; i < MAX_RACES; i++)
|
||||
{
|
||||
for (uint32 j = 0; j < MAX_CLASSES; j++)
|
||||
{
|
||||
PlayerInfo const* info = sObjectMgr->GetPlayerInfo(i, j);
|
||||
if (!info)
|
||||
continue;
|
||||
|
||||
pos = WorldPosition(info->mapId, info->positionX, info->positionY, info->positionZ, info->orientation);
|
||||
|
||||
std::string const nodeName = startNames[i] + " start";
|
||||
TravelNodeMap::instance().addNode(&pos, nodeName, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
//Transports
|
||||
GameObjectTemplateContainer const* goTemplates = sObjectMgr->GetGameObjectTemplates();
|
||||
for (auto const& iter : *goTemplates)
|
||||
{
|
||||
GameObjectTemplate const* data = &iter.second;
|
||||
if (data && (data->type == GAMEOBJECT_TYPE_TRANSPORT || data->type == GAMEOBJECT_TYPE_MO_TRANSPORT))
|
||||
{
|
||||
TransportAnimation const* animation = sTransportMgr->GetTransportAnimInfo(iter.first);
|
||||
|
||||
uint32 pathId = data->moTransport.taxiPathId;
|
||||
float moveSpeed = data->moTransport.moveSpeed;
|
||||
if (pathId >= sTaxiPathNodesByPath.size())
|
||||
continue;
|
||||
|
||||
TaxiPathNodeList const& path = sTaxiPathNodesByPath[pathId];
|
||||
|
||||
std::vector<WorldPosition> ppath;
|
||||
TravelNode* prevNode = nullptr;
|
||||
|
||||
//Elevators/Trams
|
||||
if (path.empty())
|
||||
{
|
||||
if (animation)
|
||||
{
|
||||
TransportPathContainer aPath = animation->Path;
|
||||
float timeStart;
|
||||
|
||||
for (auto& u : units)
|
||||
{
|
||||
if (u.type != 1)
|
||||
continue;
|
||||
|
||||
if (u.entry != iter.first)
|
||||
continue;
|
||||
|
||||
prevNode = nullptr;
|
||||
WorldPosition lPos = WorldPosition(u.map, 0, 0, 0, 0);
|
||||
|
||||
for (auto& p : aPath)
|
||||
{
|
||||
float dx = cos(u.o) * p.second->X - sin(u.o) * p.second->Y;
|
||||
float dy = sin(u.o) * p.second->X + cos(u.o) * p.second->Y;
|
||||
WorldPosition pos = WorldPosition(u.map, u.x + dx, u.y + dy, u.z + p.second->Z, u.o);
|
||||
|
||||
if (prevNode)
|
||||
{
|
||||
ppath.push_back(pos);
|
||||
}
|
||||
|
||||
if (pos.distance(&lPos) == 0)
|
||||
{
|
||||
TravelNode* node = TravelNodeMap::instance().addNode(&pos, data->name, true, true, true,
|
||||
iter.first);
|
||||
|
||||
if (!prevNode)
|
||||
{
|
||||
ppath.push_back(pos);
|
||||
timeStart = p.second->TimeSeg;
|
||||
}
|
||||
else
|
||||
{
|
||||
float totalTime = (p.second->TimeSeg - timeStart) / 1000.0f;
|
||||
TravelNodePath travelPath(0.1f, totalTime, (uint8)
|
||||
TravelNodePathType::transport, entry, true); node->setPathTo(prevNode, travelPath); ppath.clear();
|
||||
ppath.push_back(pos);
|
||||
timeStart = p.second->TimeSeg;
|
||||
}
|
||||
|
||||
prevNode = node;
|
||||
}
|
||||
|
||||
lPos = pos;
|
||||
}
|
||||
|
||||
if (prevNode)
|
||||
{
|
||||
for (auto& p : aPath)
|
||||
{
|
||||
float dx = cos(u.o) * p.second->X - sin(u.o) * p.second->Y;
|
||||
float dy = sin(u.o) * p.second->X + cos(u.o) * p.second->Y;
|
||||
WorldPosition pos = WorldPosition(u.map, u.x + dx, u.y + dy, u.z + p.second->Z,
|
||||
u.o);
|
||||
|
||||
ppath.push_back(pos);
|
||||
|
||||
if (pos.distance(&lPos) == 0)
|
||||
{
|
||||
TravelNode* node = TravelNodeMap::instance().addNode(&pos, data->name, true, true, true,
|
||||
iter.first); if (node != prevNode)
|
||||
{
|
||||
float totalTime = (p.second->TimeSeg - timeStart) / 1000.0f;
|
||||
TravelNodePath travelPath(0.1f, totalTime, (uint8)
|
||||
TravelNodePathType::transport, entry, true); travelPath.setPath(ppath); node->setPathTo(prevNode, travelPath);
|
||||
ppath.clear();
|
||||
ppath.push_back(pos);
|
||||
timeStart = p.second->TimeSeg;
|
||||
}
|
||||
}
|
||||
|
||||
lPos = pos;
|
||||
}
|
||||
}
|
||||
|
||||
ppath.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
else //Boats/Zepelins
|
||||
{
|
||||
//Loop over the path and connect stop locations.
|
||||
for (auto& p : path)
|
||||
{
|
||||
WorldPosition pos = WorldPosition(p->mapid, p->x, p->y, p->z, 0);
|
||||
|
||||
//if (data->displayId == 3015)
|
||||
// pos.setZ(pos.getZ() + 6.0f);
|
||||
//else if (data->displayId == 3031)
|
||||
// pos.setZ(pos.getZ() - 17.0f);
|
||||
|
||||
if (prevNode)
|
||||
{
|
||||
ppath.push_back(pos);
|
||||
}
|
||||
|
||||
if (p->delay > 0)
|
||||
{
|
||||
TravelNode* node = TravelNodeMap::instance().addNode(&pos, data->name, true, true, true, iter.first);
|
||||
|
||||
if (!prevNode)
|
||||
{
|
||||
ppath.push_back(pos);
|
||||
}
|
||||
else
|
||||
{
|
||||
TravelNodePath travelPath(0.1f, 0.0, (uint8) TravelNodePathType::transport, entry,
|
||||
true); travelPath.setPathAndCost(ppath, moveSpeed); node->setPathTo(prevNode, travelPath); ppath.clear();
|
||||
ppath.push_back(pos);
|
||||
}
|
||||
|
||||
prevNode = node;
|
||||
}
|
||||
}
|
||||
|
||||
if (prevNode)
|
||||
{
|
||||
//Continue from start until first stop and connect to end.
|
||||
for (auto& p : path)
|
||||
{
|
||||
WorldPosition pos = WorldPosition(p->mapid, p->x, p->y, p->z, 0);
|
||||
|
||||
//if (data->displayId == 3015)
|
||||
// pos.setZ(pos.getZ() + 6.0f);
|
||||
//else if (data->displayId == 3031)
|
||||
// pos.setZ(pos.getZ() - 17.0f);
|
||||
|
||||
ppath.push_back(pos);
|
||||
|
||||
if (p->delay > 0)
|
||||
{
|
||||
TravelNode* node = TravelNodeMap::instance().getNode(&pos, nullptr, 5.0f);
|
||||
if (node != prevNode)
|
||||
{
|
||||
TravelNodePath travelPath(0.1f, 0.0, (uint8) TravelNodePathType::transport, entry,
|
||||
true); travelPath.setPathAndCost(ppath, moveSpeed); node->setPathTo(prevNode, travelPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ppath.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Zone means
|
||||
for (auto& loc : exploreLocs)
|
||||
{
|
||||
std::vector<WorldPosition*> points;
|
||||
|
||||
for (auto p : loc.second->getPoints(true))
|
||||
if (!p->isUnderWater())
|
||||
points.push_back(p);
|
||||
|
||||
if (points.empty())
|
||||
points = loc.second->getPoints(true);
|
||||
|
||||
WorldPosition pos = WorldPosition(points, WP_MEAN_CENTROID);
|
||||
|
||||
TravelNode* node = TravelNodeMap::instance().addNode(&pos, pos.getAreaName(), true, true, false);
|
||||
}
|
||||
|
||||
LOG_INFO("playerbots", ">> Loaded {} navigation points.", TravelNodeMap::instance().getNodes().size());
|
||||
}
|
||||
|
||||
TravelNodeMap::instance().calcMapOffset();
|
||||
loadMapTransfers();
|
||||
*/
|
||||
|
||||
/*
|
||||
bool preloadNodePaths = false || fullNavPointReload || storeNavPointReload; //Calculate paths using
|
||||
PathGenerator. bool preloadReLinkFullyLinked = false || fullNavPointReload || storeNavPointReload; //Retry
|
||||
nodes that are fully linked. bool preloadUnlinkedPaths = false || fullNavPointReload; //Try to connect points
|
||||
currently unlinked. bool preloadWorldPaths = true; //Try to load paths in overworld. bool
|
||||
preloadInstancePaths = true; //Try to load paths in instances. bool preloadSubPrint = false; //Print output
|
||||
every 2%.
|
||||
|
||||
if (preloadNodePaths)
|
||||
{
|
||||
std::unordered_map<uint32, Map*> instances;
|
||||
|
||||
//PathGenerator
|
||||
std::vector<WorldPosition> ppath;
|
||||
|
||||
uint32 cur = 0, max = TravelNodeMap::instance().getNodes().size();
|
||||
|
||||
for (auto& startNode : TravelNodeMap::instance().getNodes())
|
||||
{
|
||||
if (!preloadReLinkFullyLinked && startNode->isLinked())
|
||||
continue;
|
||||
|
||||
for (auto& endNode : TravelNodeMap::instance().getNodes())
|
||||
{
|
||||
if (startNode == endNode)
|
||||
continue;
|
||||
|
||||
if (startNode->getPosition()->isOverworld() && !preloadWorldPaths)
|
||||
continue;
|
||||
|
||||
if (!startNode->getPosition()->isOverworld() && !preloadInstancePaths)
|
||||
continue;
|
||||
|
||||
if (startNode->hasCompletePathTo(endNode))
|
||||
continue;
|
||||
|
||||
if (!preloadUnlinkedPaths && !startNode->hasLinkTo(endNode))
|
||||
continue;
|
||||
|
||||
if (startNode->getMapId() != endNode->getMapId())
|
||||
continue;
|
||||
|
||||
//if (preloadUnlinkedPaths && !startNode->hasLinkTo(endNode) && startNode->isUselessLink(endNode))
|
||||
// continue;
|
||||
|
||||
startNode->BuildPath(endNode, nullptr, false);
|
||||
|
||||
//if (startNode->hasLinkTo(endNode) && !startNode->getPathTo(endNode)->getComplete())
|
||||
//startNode->removeLinkTo(endNode);
|
||||
}
|
||||
|
||||
startNode->setLinked(true);
|
||||
|
||||
cur++;
|
||||
|
||||
if (preloadSubPrint && (cur * 50) / max > ((cur - 1) * 50) / max)
|
||||
{
|
||||
TravelNodeMap::instance().printMap();
|
||||
TravelNodeMap::instance().printNodeStore();
|
||||
}
|
||||
}
|
||||
|
||||
if (!preloadSubPrint)
|
||||
{
|
||||
TravelNodeMap::instance().printNodeStore();
|
||||
TravelNodeMap::instance().printMap();
|
||||
}
|
||||
|
||||
LOG_INFO("playerbots", ">> Loaded paths for {} nodes.", TravelNodeMap::instance().getNodes().size());
|
||||
}
|
||||
|
||||
bool removeLowLinkNodes = false || fullNavPointReload || storeNavPointReload;
|
||||
|
||||
if (removeLowLinkNodes)
|
||||
{
|
||||
std::vector<TravelNode*> goodNodes;
|
||||
std::vector<TravelNode*> remNodes;
|
||||
for (auto& node : TravelNodeMap::instance().getNodes())
|
||||
{
|
||||
if (!node->getPosition()->isOverworld())
|
||||
continue;
|
||||
|
||||
if (std::find(goodNodes.begin(), goodNodes.end(), node) != goodNodes.end())
|
||||
continue;
|
||||
|
||||
if (std::find(remNodes.begin(), remNodes.end(), node) != remNodes.end())
|
||||
continue;
|
||||
|
||||
std::vector<TravelNode*> nodes = node->getNodeMap(true);
|
||||
|
||||
if (nodes.size() < 5)
|
||||
remNodes.insert(remNodes.end(), nodes.begin(), nodes.end());
|
||||
else
|
||||
goodNodes.insert(goodNodes.end(), nodes.begin(), nodes.end());
|
||||
}
|
||||
|
||||
for (auto& node : remNodes)
|
||||
TravelNodeMap::instance().removeNode(node);
|
||||
|
||||
LOG_INFO("playerbots", ">> Checked {} nodes.", TravelNodeMap::instance().getNodes().size());
|
||||
}
|
||||
|
||||
bool cleanUpNodeLinks = false || fullNavPointReload || storeNavPointReload;
|
||||
bool cleanUpSubPrint = false; //Print output every 2%.
|
||||
|
||||
if (cleanUpNodeLinks)
|
||||
{
|
||||
//Routes
|
||||
uint32 cur = 0;
|
||||
uint32 max = TravelNodeMap::instance().getNodes().size();
|
||||
|
||||
//Clean up node links
|
||||
for (auto& startNode : TravelNodeMap::instance().getNodes())
|
||||
{
|
||||
startNode->cropUselessLinks();
|
||||
|
||||
cur++;
|
||||
if (cleanUpSubPrint && (cur * 10) / max > ((cur - 1) * 10) / max)
|
||||
{
|
||||
TravelNodeMap::instance().printMap();
|
||||
TravelNodeMap::instance().printNodeStore();
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("playerbots", ">> Cleaned paths for {} nodes.", TravelNodeMap::instance().getNodes().size());
|
||||
}
|
||||
|
||||
bool reCalculateCost = false || fullNavPointReload || storeNavPointReload;
|
||||
bool forceReCalculate = false;
|
||||
|
||||
if (reCalculateCost)
|
||||
{
|
||||
for (auto& startNode : TravelNodeMap::instance().getNodes())
|
||||
{
|
||||
for (auto& path : *startNode->getLinks())
|
||||
{
|
||||
TravelNodePath* nodePath = path.second;
|
||||
|
||||
if (path.second->getPathType() != TravelNodePathType::walk)
|
||||
continue;
|
||||
|
||||
if (nodePath->getCalculated() && !forceReCalculate)
|
||||
continue;
|
||||
|
||||
nodePath->calculateCost();
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("playerbots", ">> Calculated pathcost for {} nodes.", TravelNodeMap::instance().getNodes().size());
|
||||
}
|
||||
|
||||
bool mirrorMissingPaths = true || fullNavPointReload || storeNavPointReload;
|
||||
|
||||
if (mirrorMissingPaths)
|
||||
{
|
||||
for (auto& startNode : TravelNodeMap::instance().getNodes())
|
||||
{
|
||||
for (auto& path : *startNode->getLinks())
|
||||
{
|
||||
TravelNode* endNode = path.first;
|
||||
|
||||
if (endNode->hasLinkTo(startNode))
|
||||
continue;
|
||||
|
||||
if (path.second->getPathType() != TravelNodePathType::walk)
|
||||
continue;
|
||||
|
||||
TravelNodePath nodePath = *path.second;
|
||||
|
||||
std::vector<WorldPosition> pPath = nodePath.GetPath();
|
||||
std::reverse(pPath.begin(), pPath.end());
|
||||
|
||||
nodePath.setPath(pPath);
|
||||
|
||||
endNode->setPathTo(startNode, nodePath, true);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("playerbots", ">> Reversed missing paths for {} nodes.", TravelNodeMap::instance().getNodes().size());
|
||||
}
|
||||
*/
|
||||
|
||||
TravelNodeMap::instance().printMap();
|
||||
TravelNodeMap::instance().printNodeStore();
|
||||
@ -2945,6 +3686,65 @@ void TravelMgr::LoadQuestTravelTable()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
sPlayerbotAIConfig.openLog(7, "w");
|
||||
|
||||
//Zone area map REMOVE!
|
||||
uint32 k = 0;
|
||||
for (auto& node : TravelNodeMap::instance().getNodes())
|
||||
{
|
||||
WorldPosition* pos = node->getPosition();
|
||||
//map area
|
||||
for (uint32 x = 0; x < 2000; x++)
|
||||
{
|
||||
for (uint32 y = 0; y < 2000; y++)
|
||||
{
|
||||
if (!pos->getMap())
|
||||
continue;
|
||||
|
||||
float nx = pos->GetPositionX() + (x * 5) - 5000.0f;
|
||||
float ny = pos->GetPositionY() + (y * 5) - 5000.0f;
|
||||
float nz = pos->GetPositionZ() + 100.0f;
|
||||
|
||||
//pos->getMap()->GetHitPosition(nx, ny, nz + 200.0f, nx, ny, nz, -0.5f);
|
||||
|
||||
if (!pos->getMap()->GetHeightInRange(nx, ny, nz, 5000.0f)) // GetHeight can fail
|
||||
continue;
|
||||
|
||||
WorldPosition npos = WorldPosition(pos->GetMapId(), nx, ny, nz, 0.0);
|
||||
uint32 area = path.getArea(npos.GetMapId(), npos.GetPositionX(), npos.GetPositionY(),
|
||||
npos.GetPositionZ());
|
||||
|
||||
std::ostringstream out;
|
||||
out << std::fixed << area << "," << npos.getDisplayX() << "," << npos.getDisplayY();
|
||||
sPlayerbotAIConfig.log(7, out.str().c_str());
|
||||
}
|
||||
}
|
||||
k++;
|
||||
|
||||
if (k > 0)
|
||||
break;
|
||||
}
|
||||
|
||||
//Explore map output (REMOVE!)
|
||||
|
||||
sPlayerbotAIConfig.openLog(5, "w");
|
||||
for (auto i : exploreLocs)
|
||||
{
|
||||
for (auto j : i.second->getPoints())
|
||||
{
|
||||
std::ostringstream out;
|
||||
std::string const name = i.second->getTitle();
|
||||
name.erase(remove(name.begin(), name.end(), '\"'), name.end());
|
||||
out << std::fixed << std::setprecision(2) << name.c_str() << "," << i.first << "," << j->getDisplayX() <<
|
||||
"," << j->getDisplayY() << "," << j->GetPositionX() << "," << j->GetPositionY() << "," << j->GetPositionZ();
|
||||
sPlayerbotAIConfig.log(5,
|
||||
out.str().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
uint32 TravelMgr::getDialogStatus(Player* pPlayer, int32 questgiver, Quest const* pQuest)
|
||||
|
||||
@ -138,9 +138,6 @@ public:
|
||||
bool isOverworld();
|
||||
bool isInWater();
|
||||
bool isUnderWater();
|
||||
// Snap Z to the water surface (level + 0.5y). Returns false if the
|
||||
// point isn't in/under water or the water level can't be sampled.
|
||||
bool setAtWaterSurface();
|
||||
bool IsValid();
|
||||
|
||||
WorldPosition relPoint(WorldPosition* center);
|
||||
|
||||
@ -425,6 +425,11 @@ bool TravelNode::isUselessLink(TravelNode* farNode)
|
||||
return false;
|
||||
}
|
||||
|
||||
void TravelNode::cropUselessLink(TravelNode* farNode)
|
||||
{
|
||||
if (isUselessLink(farNode))
|
||||
removeLinkTo(farNode);
|
||||
}
|
||||
|
||||
bool TravelNode::cropUselessLinks()
|
||||
{
|
||||
@ -470,8 +475,135 @@ bool TravelNode::cropUselessLinks()
|
||||
|
||||
return hasRemoved;
|
||||
|
||||
/*
|
||||
|
||||
//std::vector<std::pair<TravelNode*, TravelNode*>> toRemove;
|
||||
for (auto& firstLink : getLinks())
|
||||
{
|
||||
|
||||
TravelNode* firstNode = firstLink.first;
|
||||
float firstLength = firstLink.second.getDistance();
|
||||
for (auto& secondLink : getLinks())
|
||||
{
|
||||
TravelNode* secondNode = secondLink.first;
|
||||
float secondLength = secondLink.second.getDistance();
|
||||
|
||||
if (firstNode == secondNode)
|
||||
continue;
|
||||
|
||||
if (std::find(toRemove.begin(), toRemove.end(), [firstNode, secondNode](std::pair<TravelNode*, TravelNode*>
|
||||
pair) {return pair.first == firstNode || pair.first == secondNode;}) != toRemove.end()) continue;
|
||||
|
||||
if (firstNode->hasLinkTo(secondNode))
|
||||
{
|
||||
//Is it quicker to go past first node to reach second node instead of going directly?
|
||||
if (firstLength + firstNode->linkLengthTo(secondNode) < secondLength * 1.1)
|
||||
{
|
||||
if (secondNode->hasLinkTo(this) && !firstNode->hasLinkTo(this))
|
||||
continue;
|
||||
|
||||
toRemove.push_back(make_pair(this, secondNode));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TravelNodeRoute route = TravelNodeMap::instance().GetNodeRoute(firstNode, secondNode, nullptr);
|
||||
|
||||
if (route.isEmpty())
|
||||
continue;
|
||||
|
||||
if (route.hasNode(this))
|
||||
continue;
|
||||
|
||||
//Is it quicker to go past first (and multiple) nodes to reach the second node instead of going
|
||||
directly? if (firstLength + route.getLength() < secondLength * 1.1)
|
||||
{
|
||||
if (secondNode->hasLinkTo(this) && !firstNode->hasLinkTo(this))
|
||||
continue;
|
||||
|
||||
toRemove.push_back(make_pair(this, secondNode));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Reverse cleanup. This is needed when we add a node in an existing map.
|
||||
if (firstNode->hasLinkTo(this))
|
||||
{
|
||||
firstLength = firstNode->getPathTo(this)->getDistance();
|
||||
|
||||
for (auto& secondLink : firstNode->getLinks())
|
||||
{
|
||||
TravelNode* secondNode = secondLink.first;
|
||||
float secondLength = secondLink.second.getDistance();
|
||||
|
||||
if (this == secondNode)
|
||||
continue;
|
||||
|
||||
if (std::find(toRemove.begin(), toRemove.end(), [firstNode, secondNode](std::pair<TravelNode*,
|
||||
TravelNode*> pair) {return pair.first == firstNode || pair.first == secondNode; }) != toRemove.end()) continue;
|
||||
|
||||
if (firstNode->hasLinkTo(secondNode))
|
||||
{
|
||||
//Is it quicker to go past first node to reach second node instead of going directly?
|
||||
if (firstLength + firstNode->linkLengthTo(secondNode) < secondLength * 1.1)
|
||||
{
|
||||
if (secondNode->hasLinkTo(this) && !firstNode->hasLinkTo(this))
|
||||
continue;
|
||||
|
||||
toRemove.push_back(make_pair(this, secondNode));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TravelNodeRoute route = TravelNodeMap::instance().GetNodeRoute(firstNode, secondNode, nullptr);
|
||||
|
||||
if (route.isEmpty())
|
||||
continue;
|
||||
|
||||
if (route.hasNode(this))
|
||||
continue;
|
||||
|
||||
//Is it quicker to go past first (and multiple) nodes to reach the second node instead of going
|
||||
directly? if (firstLength + route.getLength() < secondLength * 1.1)
|
||||
{
|
||||
if (secondNode->hasLinkTo(this) && !firstNode->hasLinkTo(this))
|
||||
continue;
|
||||
|
||||
toRemove.push_back(make_pair(this, secondNode));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (auto& nodePair : toRemove)
|
||||
nodePair.first->unlinkNode(nodePair.second, false);
|
||||
*/
|
||||
}
|
||||
|
||||
bool TravelNode::isEqual(TravelNode* compareNode)
|
||||
{
|
||||
if (!hasLinkTo(compareNode))
|
||||
return false;
|
||||
|
||||
if (!compareNode->hasLinkTo(this))
|
||||
return false;
|
||||
|
||||
for (auto& node : TravelNodeMap::instance().getNodes())
|
||||
{
|
||||
if (node == this || node == compareNode)
|
||||
continue;
|
||||
|
||||
if (node->hasLinkTo(this) != node->hasLinkTo(compareNode))
|
||||
return false;
|
||||
|
||||
if (hasLinkTo(node) != compareNode->hasLinkTo(node))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void TravelNode::print([[maybe_unused]] bool printFailed)
|
||||
{
|
||||
@ -757,11 +889,6 @@ bool TravelPath::UpcommingSpecialMovement(WorldPosition startPos,
|
||||
{
|
||||
if (startP->entry)
|
||||
{
|
||||
// Reference also checks an AreaTriggerEntry DBC store
|
||||
// (sAreaTriggerStore). AC doesn't expose a separate DBC
|
||||
// store for area triggers — sObjectMgr->GetAreaTrigger is
|
||||
// the loaded view of the same data, so it's the only
|
||||
// existence check we need on this side.
|
||||
AreaTrigger const* at = sObjectMgr->GetAreaTrigger(startP->entry);
|
||||
if (!at)
|
||||
return false;
|
||||
@ -785,6 +912,13 @@ bool TravelPath::UpcommingSpecialMovement(WorldPosition startPos,
|
||||
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)
|
||||
@ -793,12 +927,12 @@ bool TravelPath::UpcommingSpecialMovement(WorldPosition startPos,
|
||||
return true;
|
||||
}
|
||||
|
||||
// Board-and-ride mode (transportSkipRide == false). Cut to dock if
|
||||
// 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 (!sPlayerbotAIConfig.transportSkipRide &&
|
||||
startP->type == PathNodeType::NODE_TRANSPORT)
|
||||
if (startP->type == PathNodeType::NODE_TRANSPORT)
|
||||
{
|
||||
uint32 const entry = nextP->entry;
|
||||
uint32 entry = nextP->entry;
|
||||
|
||||
if (!onTransport)
|
||||
{
|
||||
@ -820,24 +954,6 @@ bool TravelPath::UpcommingSpecialMovement(WorldPosition startPos,
|
||||
}
|
||||
}
|
||||
|
||||
// Skip-ride mode (transportSkipRide == true): bot is approaching a
|
||||
// transport node — walk forward to find the first non-transport node
|
||||
// (the disembark side), cut to prevP (last transport node) so
|
||||
// HandleSpecialMovement teleports the bot across directly.
|
||||
if (sPlayerbotAIConfig.transportSkipRide &&
|
||||
nextP->type == PathNodeType::NODE_TRANSPORT)
|
||||
{
|
||||
for (auto p = std::next(startP); p != fullPath.end(); ++p)
|
||||
{
|
||||
if (p->type != PathNodeType::NODE_TRANSPORT)
|
||||
{
|
||||
cutTo(*prevP, false);
|
||||
return true;
|
||||
}
|
||||
prevP = p;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -881,13 +997,7 @@ void TravelPath::ClipPath(PlayerbotAI* ai, Unit* mover, bool ignoreEnemyTargets)
|
||||
float const range = cre->GetAttackDistance(mover);
|
||||
if (WorldPosition(unit).sqDistance(p->point) > range * range)
|
||||
continue;
|
||||
// Reference uses CanAttackOnSight (faction + sanctuary +
|
||||
// feign + phased + passive flags). AC equivalent is
|
||||
// CanCreatureAttack with skipDistCheck=true (range already
|
||||
// confirmed above). IsHostileTo alone over-clipped at
|
||||
// neutral mobs that wouldn't actually aggro.
|
||||
if (!cre->CanCreatureAttack(mover, true) ||
|
||||
!unit->IsWithinLOSInMap(mover))
|
||||
if (!unit->IsHostileTo(mover) || !unit->IsWithinLOSInMap(mover))
|
||||
continue;
|
||||
|
||||
endP = p;
|
||||
@ -919,22 +1029,6 @@ void TravelPath::ClipPath(PlayerbotAI* ai, Unit* mover, bool ignoreEnemyTargets)
|
||||
fullPath.erase(std::next(endP), fullPath.end());
|
||||
}
|
||||
|
||||
void TravelPath::surfaceSnapWaypoints(WorldPosition endPos)
|
||||
{
|
||||
if (fullPath.empty())
|
||||
return;
|
||||
// Same map + dest is on land. If dest is itself underwater the bot
|
||||
// wants to dive; leave waypoints alone.
|
||||
if (fullPath.front().point.GetMapId() != endPos.GetMapId() ||
|
||||
endPos.isUnderWater())
|
||||
return;
|
||||
for (auto& p : fullPath)
|
||||
{
|
||||
if (p.point.isUnderWater())
|
||||
p.point.setAtWaterSurface();
|
||||
}
|
||||
}
|
||||
|
||||
bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist, Unit* bot)
|
||||
{
|
||||
if (GetPath().empty())
|
||||
@ -1127,6 +1221,14 @@ TravelPath TravelNodeRoute::BuildPath(std::vector<WorldPosition> pathToStart, st
|
||||
// Full taxi waypoint route; same reasoning as transport.
|
||||
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
|
||||
{
|
||||
std::vector<WorldPosition> path = nodePath->GetPath();
|
||||
@ -1315,6 +1417,8 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal,
|
||||
|
||||
std::vector<TravelNodeStub*> open, closed;
|
||||
|
||||
std::vector<TravelNode*> portNodes; // synthetic teleport/portal edges
|
||||
|
||||
if (bot)
|
||||
{
|
||||
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
||||
@ -1351,20 +1455,97 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal,
|
||||
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, (10 - deathCount) * MINUTE) — matches
|
||||
// reference exactly, including the uint32 underflow at
|
||||
// deathCount > 10 (which makes hearthstone prohibitive
|
||||
// for very-dead bots — apparently intentional).
|
||||
hsStub->costFromStart = std::max<uint32>(2,
|
||||
(10 - AI_VALUE(uint32, "death count")) * 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
|
||||
startStub->currentGold = bot->GetMoney();
|
||||
}
|
||||
|
||||
if (!start->hasRouteTo(goal))
|
||||
if (open.empty() && !start->hasRouteTo(goal))
|
||||
{
|
||||
for (auto* p : portNodes)
|
||||
delete p;
|
||||
return TravelNodeRoute();
|
||||
}
|
||||
|
||||
// Min-heap: smallest f at front
|
||||
auto heapComp = [](TravelNodeStub* i, TravelNodeStub* j) { return i->totalCost > j->totalCost; };
|
||||
|
||||
open.push_back(startStub);
|
||||
startStub->open = true;
|
||||
std::push_heap(open.begin(), open.end(), heapComp);
|
||||
// Heapify all of open in one pass — covers both startStub and any
|
||||
// PortalNode stubs injected above.
|
||||
std::make_heap(open.begin(), open.end(), heapComp);
|
||||
|
||||
while (!open.empty())
|
||||
{
|
||||
@ -1390,7 +1571,12 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal,
|
||||
}
|
||||
|
||||
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
|
||||
@ -1429,6 +1615,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();
|
||||
}
|
||||
|
||||
@ -1508,7 +1697,7 @@ TravelNodeRoute TravelNodeMap::FindRouteNearestNodes(WorldPosition startPos, Wor
|
||||
return TravelNodeRoute();
|
||||
}
|
||||
|
||||
TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos,
|
||||
TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos, [[maybe_unused]] uint32 botZoneId,
|
||||
WorldPosition destination, Unit* bot)
|
||||
{
|
||||
TravelPath path;
|
||||
@ -1516,14 +1705,14 @@ TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos,
|
||||
// Probe-first short-circuit (matches reference exactly): if a 40-step
|
||||
// mmap probe from bot to destination reaches within spellDistance of
|
||||
// dest, use the probe directly and skip graph routing. Otherwise
|
||||
// the probe waypoints are kept as `beginPath` and fed into per-
|
||||
// candidate startPath cropping below.
|
||||
std::vector<WorldPosition> beginPath;
|
||||
// fall through to the graph A* below — the failed probe waypoints
|
||||
// would ideally feed into getRoute as startPath (reference does
|
||||
// this; we don't yet — TODO).
|
||||
if (botPos.GetMapId() == destination.GetMapId())
|
||||
{
|
||||
beginPath = destination.getPathFromPath({botPos}, bot, 40);
|
||||
if (destination.isPathTo(beginPath, sPlayerbotAIConfig.spellDistance))
|
||||
return TravelPath(beginPath);
|
||||
std::vector<WorldPosition> probe = destination.getPathFromPath({botPos}, bot, 40);
|
||||
if (destination.isPathTo(probe, sPlayerbotAIConfig.spellDistance))
|
||||
return TravelPath(probe);
|
||||
}
|
||||
|
||||
std::shared_lock<std::shared_timed_mutex> guard(m_nMapMtx);
|
||||
@ -1621,21 +1810,15 @@ TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos,
|
||||
if (transportEntry)
|
||||
{
|
||||
path = route.BuildPath({botPos}, endProbe, bot);
|
||||
route.cleanTempNodes();
|
||||
return path;
|
||||
}
|
||||
|
||||
// Validate bot -> startNode is pathable within maxStartDistance.
|
||||
// Reference reuses the (failed) probe waypoints first via
|
||||
// cropPathTo, falling back to a fresh getPathTo only if the
|
||||
// probe can't be cropped to reach startNode. This saves
|
||||
// re-running mmap when the probe already covers part of
|
||||
// the journey to startNode.
|
||||
float const maxStartDistance = s->isTransport() ? 20.0f : 1.0f;
|
||||
std::vector<WorldPosition> pathToStart = beginPath;
|
||||
bool startPathOk = !pathToStart.empty() &&
|
||||
startNodePos.cropPathTo(pathToStart, maxStartDistance);
|
||||
|
||||
if (!startPathOk && bot && botPos.GetMapId() == startNodePos.GetMapId())
|
||||
std::vector<WorldPosition> pathToStart;
|
||||
bool startPathOk = false;
|
||||
if (bot && botPos.GetMapId() == startNodePos.GetMapId())
|
||||
{
|
||||
pathToStart = botPos.getPathTo(startNodePos, bot);
|
||||
startPathOk = startNodePos.isPathTo(pathToStart, maxStartDistance);
|
||||
@ -1644,21 +1827,139 @@ TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos,
|
||||
if (!startPathOk)
|
||||
{
|
||||
badStartNodes.push_back(s);
|
||||
route.cleanTempNodes();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Both ends validated — build and return. Save the
|
||||
// successful pathToStart back as beginPath so subsequent
|
||||
// ResolveMovePath cycles can reuse it.
|
||||
beginPath = pathToStart;
|
||||
// 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)
|
||||
{
|
||||
if (!startNode->isLinked() || startNode->isImportant())
|
||||
return false;
|
||||
|
||||
std::vector<TravelNode*> ignore = {startNode};
|
||||
|
||||
for (auto& node : getNodes(*startNode->getPosition(), 5000.f))
|
||||
{
|
||||
if (startNode == node)
|
||||
continue;
|
||||
|
||||
if (node->getNodeMap(true).size() > node->getNodeMap(true, ignore).size())
|
||||
return false;
|
||||
}
|
||||
|
||||
removeNode(startNode);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
TravelNode* TravelNodeMap::addZoneLinkNode(TravelNode* startNode)
|
||||
{
|
||||
for (auto& path : *startNode->getPaths())
|
||||
{
|
||||
//TravelNode* endNode = path.first; //not used, line marked for removal.
|
||||
|
||||
std::string zoneName = startNode->getPosition()->getAreaName(true, true);
|
||||
for (auto& pos : path.second.GetPath())
|
||||
{
|
||||
std::string const newZoneName = pos.getAreaName(true, true);
|
||||
if (zoneName != newZoneName)
|
||||
{
|
||||
if (!getNode(pos, nullptr, 100.0f))
|
||||
{
|
||||
std::string const nodeName = zoneName + " to " + newZoneName;
|
||||
return TravelNodeMap::instance().addNode(pos, nodeName, false, true);
|
||||
}
|
||||
|
||||
zoneName = newZoneName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TravelNode* TravelNodeMap::addRandomExtNode(TravelNode* startNode)
|
||||
{
|
||||
std::unordered_map<TravelNode*, TravelNodePath> paths = *startNode->getPaths();
|
||||
|
||||
if (paths.empty())
|
||||
return nullptr;
|
||||
|
||||
for (uint32 i = 0; i < 20; i++)
|
||||
{
|
||||
auto random_it = std::next(std::begin(paths), urand(0, paths.size() - 1));
|
||||
|
||||
TravelNode* endNode = random_it->first;
|
||||
std::vector<WorldPosition> path = random_it->second.GetPath();
|
||||
|
||||
if (path.empty())
|
||||
continue;
|
||||
|
||||
// Prefer to skip complete links
|
||||
if (endNode->hasLinkTo(startNode) && startNode->hasLinkTo(endNode) && !urand(0, 20))
|
||||
continue;
|
||||
|
||||
// Prefer to skip no links
|
||||
if (!startNode->hasLinkTo(endNode) && !urand(0, 20))
|
||||
continue;
|
||||
|
||||
WorldPosition point = path[urand(0, path.size() - 1)];
|
||||
|
||||
if (!getNode(point, nullptr, 100.0f))
|
||||
return TravelNodeMap::instance().addNode(point, startNode->getName(), false, true);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void TravelNodeMap::generateNpcNodes()
|
||||
{
|
||||
@ -2120,6 +2421,7 @@ void TravelNodeMap::generateAll()
|
||||
hasToSave = true;
|
||||
saveNodeStore();
|
||||
|
||||
BuildZoneIndex();
|
||||
PrecomputeReachability();
|
||||
}
|
||||
|
||||
@ -2144,6 +2446,7 @@ void TravelNodeMap::Init()
|
||||
saveNodeStore();
|
||||
}
|
||||
|
||||
BuildZoneIndex();
|
||||
PrecomputeReachability();
|
||||
}
|
||||
|
||||
@ -2212,6 +2515,16 @@ void TravelNodeMap::printNodeStore()
|
||||
out << "," << (node->isTransport() ? "true" : "false") << "," << node->getTransportId();
|
||||
out << "});";
|
||||
|
||||
/*
|
||||
out << std::fixed << std::setprecision(2) << " nodes[" << i << "] =
|
||||
TravelNodeMap::instance().addNode(&WorldPosition(" << node->GetMapId() << "," << node->getX() << "f," << node->getY()
|
||||
<< "f," << node->getZ() << "f,"<< node->getO() <<"f), \""
|
||||
<< name << "\", " << (node->isImportant() ? "true" : "false") << ", true";
|
||||
if (node->isTransport())
|
||||
out << "," << (node->isTransport() ? "true" : "false") << "," << node->getTransportId();
|
||||
|
||||
out << ");";
|
||||
*/
|
||||
sPlayerbotAIConfig.log(nodeStore, out.str().c_str());
|
||||
|
||||
saveNodes.insert(std::make_pair(node, i));
|
||||
@ -2743,6 +3056,87 @@ std::vector<uint32> TravelNodeMap::BuildPath(uint32 fromNode, uint32 toNode,
|
||||
return path;
|
||||
}
|
||||
|
||||
void TravelNodeMap::BuildZoneIndex()
|
||||
{
|
||||
m_zoneIndex.clear();
|
||||
m_mapIndex.clear();
|
||||
|
||||
for (auto* node : nodes)
|
||||
{
|
||||
if (!node)
|
||||
continue;
|
||||
|
||||
WorldPosition* pos = node->getPosition();
|
||||
uint32 mapId = pos->GetMapId();
|
||||
|
||||
m_mapIndex[mapId].push_back(node);
|
||||
|
||||
uint32 zoneId = sMapMgr->GetZoneId(PHASEMASK_NORMAL, *pos);
|
||||
if (zoneId)
|
||||
m_zoneIndex[zoneId].push_back(node);
|
||||
}
|
||||
}
|
||||
|
||||
TravelNode* TravelNodeMap::GetNearestNodeInZone(WorldPosition pos, uint32 zoneId)
|
||||
{
|
||||
auto it = m_zoneIndex.find(zoneId);
|
||||
if (it == m_zoneIndex.end() || it->second.empty())
|
||||
return GetNearestNodeOnMap(pos); // Fallback to map-wide
|
||||
|
||||
TravelNode* bestNode = nullptr;
|
||||
float bestDist = FLT_MAX;
|
||||
|
||||
for (auto* node : it->second)
|
||||
{
|
||||
if (!node || node->GetMapId() != pos.GetMapId())
|
||||
continue;
|
||||
float dist = node->fDist(pos);
|
||||
if (dist < bestDist)
|
||||
{
|
||||
bestDist = dist;
|
||||
bestNode = node;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bestNode)
|
||||
return GetNearestNodeOnMap(pos);
|
||||
|
||||
return bestNode;
|
||||
}
|
||||
|
||||
std::vector<TravelNode*> const& TravelNodeMap::GetNodesInZone(uint32 zoneId) const
|
||||
{
|
||||
static std::vector<TravelNode*> const empty;
|
||||
auto it = m_zoneIndex.find(zoneId);
|
||||
if (it == m_zoneIndex.end())
|
||||
return empty;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
TravelNode* TravelNodeMap::GetNearestNodeOnMap(WorldPosition pos)
|
||||
{
|
||||
auto it = m_mapIndex.find(pos.GetMapId());
|
||||
if (it == m_mapIndex.end() || it->second.empty())
|
||||
return nullptr;
|
||||
|
||||
TravelNode* bestNode = nullptr;
|
||||
float bestDist = FLT_MAX;
|
||||
|
||||
for (auto* node : it->second)
|
||||
{
|
||||
if (!node)
|
||||
continue;
|
||||
float d = node->fDist(pos);
|
||||
if (d < bestDist)
|
||||
{
|
||||
bestDist = d;
|
||||
bestNode = node;
|
||||
}
|
||||
}
|
||||
|
||||
return bestNode;
|
||||
}
|
||||
|
||||
void TravelNodeMap::PrecomputeReachability()
|
||||
{
|
||||
// Find connected components via BFS
|
||||
|
||||
@ -86,6 +86,8 @@
|
||||
// (saveNodeStore) and by the debug dump command.
|
||||
//
|
||||
|
||||
constexpr float MAX_PATHFINDING_DISTANCE = 296.0f;
|
||||
|
||||
enum class TravelNodePathType : uint8
|
||||
{
|
||||
none = 0,
|
||||
@ -93,7 +95,10 @@ enum class TravelNodePathType : uint8
|
||||
areaTrigger = 2,
|
||||
transport = 3,
|
||||
flightPath = 4,
|
||||
// teleportSpell = 5 // maybe someday
|
||||
// Teleport-spell edges (hearthstone, mage portals). Generated at A*
|
||||
// search start via PortalNode injection; consumed by
|
||||
// HandleSpecialMovement's NODE_TELEPORT case.
|
||||
teleportSpell = 5,
|
||||
staticPortal = 6
|
||||
};
|
||||
|
||||
@ -353,8 +358,11 @@ public:
|
||||
}
|
||||
void removeLinkTo(TravelNode* node, bool removePaths = false);
|
||||
|
||||
bool isEqual(TravelNode* compareNode);
|
||||
|
||||
// Removes links to other nodes that can also be reached by passing another node.
|
||||
bool isUselessLink(TravelNode* farNode);
|
||||
void cropUselessLink(TravelNode* farNode);
|
||||
bool cropUselessLinks();
|
||||
|
||||
// Returns all nodes that can be reached from this node.
|
||||
@ -402,6 +410,26 @@ protected:
|
||||
// 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
|
||||
enum class PathNodeType : uint8
|
||||
{
|
||||
@ -411,7 +439,10 @@ enum class PathNodeType : uint8
|
||||
NODE_AREA_TRIGGER = 3,
|
||||
NODE_TRANSPORT = 4,
|
||||
NODE_FLIGHTPATH = 5,
|
||||
// value 6 reserved (was NODE_TELEPORT — removed with teleportSpell)
|
||||
// Teleport-spell endpoint (hearthstone, mage portal). Emitted by
|
||||
// TravelNodeRoute::BuildPath when traversing a teleportSpell-type
|
||||
// edge; consumed by HandleSpecialMovement.
|
||||
NODE_TELEPORT = 6,
|
||||
NODE_STATIC_PORTAL = 7
|
||||
};
|
||||
|
||||
@ -486,14 +517,6 @@ public:
|
||||
|
||||
bool makeShortCut(WorldPosition startPos, float maxDist, Unit* bot = nullptr);
|
||||
|
||||
// For each waypoint that's in/under water, snap its Z to the water
|
||||
// surface. No-op when destination is itself underwater (caller wants
|
||||
// the bot to dive) or path's front map differs from dest map.
|
||||
// Mirrors the reference's underwater→surface snap so bots swim
|
||||
// along the top of shallow water on land-bound paths instead of
|
||||
// diving and air-walking the seafloor.
|
||||
void surfaceSnapWaypoints(WorldPosition endPos);
|
||||
|
||||
// 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/
|
||||
@ -552,6 +575,14 @@ public:
|
||||
{
|
||||
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 hasNode(TravelNode* node)
|
||||
@ -562,6 +593,19 @@ public:
|
||||
|
||||
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(
|
||||
std::vector<WorldPosition> pathToStart = {},
|
||||
std::vector<WorldPosition> pathToEnd = {},
|
||||
@ -575,6 +619,7 @@ private:
|
||||
return std::find(nodes.begin(), nodes.end(), node);
|
||||
}
|
||||
std::vector<TravelNode*> nodes;
|
||||
std::vector<TravelNode*> tempNodes; // owned synthetic nodes (PortalNode etc.)
|
||||
};
|
||||
|
||||
// A node container to aid A* calculations with nodes.
|
||||
@ -701,6 +746,9 @@ public:
|
||||
void saveNodeStore();
|
||||
void LoadNodeStore();
|
||||
|
||||
bool cropUselessNode(TravelNode* startNode);
|
||||
TravelNode* addZoneLinkNode(TravelNode* startNode);
|
||||
TravelNode* addRandomExtNode(TravelNode* startNode);
|
||||
|
||||
void calcMapOffset();
|
||||
WorldPosition getMapOffset(uint32 mapId);
|
||||
@ -709,12 +757,20 @@ public:
|
||||
void InitTaxiGraph();
|
||||
std::vector<uint32> FindTaxiPath(uint32 fromNode, uint32 toNode);
|
||||
|
||||
void BuildZoneIndex();
|
||||
void PrecomputeReachability();
|
||||
|
||||
TravelNode* GetNearestNodeInZone(WorldPosition pos, uint32 zoneId);
|
||||
TravelNode* GetNearestNodeOnMap(WorldPosition pos);
|
||||
|
||||
// All nodes registered to a zone (post-BuildZoneIndex). Returns an
|
||||
// empty static vector for unknown zones.
|
||||
std::vector<TravelNode*> const& GetNodesInZone(uint32 zoneId) const;
|
||||
|
||||
// Resolve a full TravelPath from botPos to destination. Returns an
|
||||
// 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,
|
||||
TravelPath GetFullPath(WorldPosition botPos, uint32 botZoneId,
|
||||
WorldPosition destination, Unit* bot = nullptr);
|
||||
|
||||
// Resolve A* route between two world positions (returns node vector)
|
||||
@ -751,6 +807,9 @@ private:
|
||||
|
||||
std::vector<TravelNode*> nodes;
|
||||
|
||||
std::unordered_map<uint32, std::vector<TravelNode*>> m_zoneIndex;
|
||||
std::unordered_map<uint32, std::vector<TravelNode*>> m_mapIndex;
|
||||
|
||||
std::vector<std::pair<uint32, WorldPosition>> mapOffsets;
|
||||
|
||||
bool hasToSave = false;
|
||||
|
||||
@ -88,7 +88,6 @@ bool PlayerbotAIConfig::Initialize()
|
||||
|
||||
farDistance = sConfigMgr->GetOption<float>("AiPlayerbot.FarDistance", 20.0f);
|
||||
sightDistance = sConfigMgr->GetOption<float>("AiPlayerbot.SightDistance", 100.0f);
|
||||
transportSkipRide = sConfigMgr->GetOption<bool>("AiPlayerbot.TransportSkipRide", false);
|
||||
spellDistance = sConfigMgr->GetOption<float>("AiPlayerbot.SpellDistance", 28.5f);
|
||||
shootDistance = sConfigMgr->GetOption<float>("AiPlayerbot.ShootDistance", 5.0f);
|
||||
healDistance = sConfigMgr->GetOption<float>("AiPlayerbot.HealDistance", 38.5f);
|
||||
|
||||
@ -93,12 +93,6 @@ public:
|
||||
bool randomBotGuildNearby, randomBotInvitePlayer, inviteChat;
|
||||
uint32 globalCoolDown, reactDelay, maxWaitForMove, disableMoveSplinePath, expireActionTime,
|
||||
dispelAuraDuration, passiveDelay, repeatDelay, errorDelay, rpgDelay, sitDelay, returnDelay, lootDelay;
|
||||
// Transport handling:
|
||||
// false (default) = teleport-board, ride the transport, teleport-disembark
|
||||
// true = skip the ride entirely (teleport directly across)
|
||||
// AC has no transport-surface mmap so an in-deck walking mode can't be
|
||||
// faithfully implemented — the on-board phase always teleport-snaps.
|
||||
bool transportSkipRide;
|
||||
bool dynamicReactDelay;
|
||||
float sightDistance, spellDistance, reactDistance, grindDistance, lootDistance, shootDistance, fleeDistance,
|
||||
tooCloseDistance, meleeDistance, followDistance, whisperDistance, contactDistance, aoeRadius, rpgDistance,
|
||||
|
||||
@ -16,7 +16,6 @@
|
||||
#include "BattleGroundTactics.h"
|
||||
#include "Chat.h"
|
||||
#include "GuildTaskMgr.h"
|
||||
#include "MapMgr.h"
|
||||
#include "PerfMonitor.h"
|
||||
#include "PlayerbotMgr.h"
|
||||
#include "RandomPlayerbotMgr.h"
|
||||
@ -187,24 +186,7 @@ public:
|
||||
}
|
||||
|
||||
uint32 zoneId = player->GetZoneId();
|
||||
uint32 const phaseMask = player->GetPhaseMask();
|
||||
uint32 const mapId = player->GetMapId();
|
||||
std::vector<TravelNode*> nodes;
|
||||
for (TravelNode* n : sTravelNodeMap.getNodes())
|
||||
{
|
||||
if (!n)
|
||||
continue;
|
||||
WorldPosition* pos = n->getPosition();
|
||||
if (!pos || pos->GetMapId() != mapId)
|
||||
continue;
|
||||
uint32 const nodeZone = sMapMgr->GetZoneId(phaseMask, mapId,
|
||||
pos->GetPositionX(),
|
||||
pos->GetPositionY(),
|
||||
pos->GetPositionZ());
|
||||
if (nodeZone != zoneId)
|
||||
continue;
|
||||
nodes.push_back(n);
|
||||
}
|
||||
std::vector<TravelNode*> const& nodes = sTravelNodeMap.GetNodesInZone(zoneId);
|
||||
if (nodes.empty())
|
||||
{
|
||||
handler->PSendSysMessage("No travel nodes registered in zone {} (is the travel node system loaded?)", zoneId);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user