fix(Core/Movement): MoveNear engine-aware near-point + FollowOnTransport port + drop dead botZoneId

This commit is contained in:
bash 2026-05-31 19:05:06 +02:00
parent cbd5f8748c
commit b97da5c741
4 changed files with 71 additions and 17 deletions

View File

@ -214,26 +214,28 @@ bool MovementAction::MoveNear(WorldObject* target, float distance, MovementPrior
if (!target) if (!target)
return false; return false;
// Reference uses bounding radius (collision footprint), not combat float const followAngle = GetFollowAngle();
// reach (which is wider for big mobs). Bounding radius lands the bot float const followRange = botAI->GetRange("follow");
// at the requested standoff from the model edge, not arbitrarily far.
distance += target->GetObjectSize();
float followAngle = GetFollowAngle();
for (float angle = followAngle; angle <= followAngle + static_cast<float>(2 * M_PI); for (float angle = followAngle; angle <= followAngle + static_cast<float>(2 * M_PI);
angle += static_cast<float>(M_PI / 4.f)) angle += static_cast<float>(M_PI / 4.f))
{ {
float x = target->GetPositionX() + cos(angle) * distance; // GetNearPoint is engine-aware: snaps to walkable terrain,
float y = target->GetPositionY() + sin(angle) * distance; // avoids collision geometry, clamps Z. Drops the raw cos/sin
// offset that could land the bot inside walls or on ledges.
// Matches the reference's MoveNear shape.
float x = target->GetPositionX();
float y = target->GetPositionY();
float z = target->GetPositionZ(); float z = target->GetPositionZ();
// Clamp Z to the terrain under the offset point so we don't float const dist = distance + target->GetObjectSize();
// hand PointMovementGenerator a Z that matches the target's target->GetNearPoint(bot, x, y, z, bot->GetObjectSize(),
// floor but not the sampled (x,y) — avoids straight-line std::min(dist, followRange), angle);
// fallbacks through geometry.
bot->UpdateAllowedPositionZ(x, y, z);
if (!bot->IsWithinLOS(x, y, z)) // LOS test at eye-level (collision-height above feet) ignoring
// M2 models — large M2 trees/canopies otherwise block the test
// even when the bot can walk to the spot.
if (!bot->IsWithinLOS(x, y, z + bot->GetCollisionHeight(),
VMAP::ModelIgnoreFlags::M2))
continue; continue;
bool moved = MoveTo(target->GetMapId(), x, y, z, false, false, false, false, priority); bool moved = MoveTo(target->GetMapId(), x, y, z, false, false, false, false, priority);
@ -696,6 +698,14 @@ bool MovementAction::Follow(Unit* target, float distance, float angle)
UpdateMovementState(); UpdateMovementState();
// Cross-transport follow: if bot and target are on different
// transports (or only one is on a transport) and within sight,
// disembark/teleport/board to match. Handled here before the
// distance gates so a bot on a stationary boat following a
// master who just boarded doesn't get stuck at "no need to follow".
if (FollowOnTransport(target))
return true;
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))
{ {
@ -816,6 +826,42 @@ bool MovementAction::Follow(Unit* target, float distance, float angle)
return true; return true;
} }
bool MovementAction::FollowOnTransport(Unit* target)
{
if (!target)
return false;
bool const onDifferentTransports =
bot->m_movementInfo.transport.guid != target->m_movementInfo.transport.guid;
if (!onDifferentTransports)
return false;
if (!ServerFacade::instance().IsDistanceLessOrEqualThan(
ServerFacade::instance().GetDistance2d(bot, target),
sPlayerbotAIConfig.sightDistance))
return false;
bot->StopMoving();
// Disembark from our current transport (if any) before relocating.
if (Transport* myTransport = bot->GetTransport())
myTransport->RemovePassenger(bot);
// NearTeleportTo is the AC equivalent of cmangos's Relocate+
// SendHeartBeat sequence: it relocates the bot AND broadcasts the
// movement update so server-side state stays consistent.
bot->NearTeleportTo(target->GetPositionX(),
target->GetPositionY(),
target->GetPositionZ(),
bot->GetOrientation());
// Board target's transport (if any).
if (Transport* hisTransport = target->GetTransport())
hisTransport->AddPassenger(bot);
return true;
}
bool MovementAction::ChaseTo(WorldObject* obj, float distance) bool MovementAction::ChaseTo(WorldObject* obj, float distance)
{ {
if (!IsMovingAllowed()) if (!IsMovingAllowed())
@ -2530,7 +2576,7 @@ TravelPath MovementAction::ResolveMovePath(WorldPosition startPos,
if (needsLongPath && !sTravelNodeMap.getNodes().empty() && !bot->InBattleground()) if (needsLongPath && !sTravelNodeMap.getNodes().empty() && !bot->InBattleground())
{ {
out = sTravelNodeMap.GetFullPath(startPos, bot->GetZoneId(), endPos, bot); out = sTravelNodeMap.GetFullPath(startPos, endPos, bot);
} }
else else
{ {

View File

@ -111,6 +111,14 @@ protected:
float GetFollowAngle(); float GetFollowAngle();
bool Follow(Unit* target, float distance = sPlayerbotAIConfig.followDistance); bool Follow(Unit* target, float distance = sPlayerbotAIConfig.followDistance);
bool Follow(Unit* target, float distance, float angle); 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 ChaseTo(WorldObject* obj, float distance = 0.0f);
bool ReachCombatTo(Unit* target, float distance = 0.0f); bool ReachCombatTo(Unit* target, float distance = 0.0f);
float MoveDelay(float distance, bool backwards = false); float MoveDelay(float distance, bool backwards = false);

View File

@ -1512,7 +1512,7 @@ TravelNodeRoute TravelNodeMap::FindRouteNearestNodes(WorldPosition startPos, Wor
return TravelNodeRoute(); return TravelNodeRoute();
} }
TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos, [[maybe_unused]] uint32 botZoneId, TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos,
WorldPosition destination, Unit* bot) WorldPosition destination, Unit* bot)
{ {
TravelPath path; TravelPath path;

View File

@ -714,7 +714,7 @@ public:
// Resolve a full TravelPath from botPos to destination. Returns an // Resolve a full TravelPath from botPos to destination. Returns an
// empty TravelPath if no graph route + mmap stitch is reachable; // empty TravelPath if no graph route + mmap stitch is reachable;
// the caller is then expected to fall back to a single-point path. // the caller is then expected to fall back to a single-point path.
TravelPath GetFullPath(WorldPosition botPos, uint32 botZoneId, TravelPath GetFullPath(WorldPosition botPos,
WorldPosition destination, Unit* bot = nullptr); 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)