diff --git a/src/Ai/Base/Actions/MovementActions.cpp b/src/Ai/Base/Actions/MovementActions.cpp index 45d8ee69a..754adaef9 100644 --- a/src/Ai/Base/Actions/MovementActions.cpp +++ b/src/Ai/Base/Actions/MovementActions.cpp @@ -214,26 +214,28 @@ bool MovementAction::MoveNear(WorldObject* target, float distance, MovementPrior if (!target) return false; - // 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 const followAngle = GetFollowAngle(); + float const followRange = botAI->GetRange("follow"); for (float angle = followAngle; angle <= followAngle + static_cast(2 * M_PI); angle += static_cast(M_PI / 4.f)) { - float x = target->GetPositionX() + cos(angle) * distance; - float y = target->GetPositionY() + sin(angle) * distance; + // GetNearPoint is engine-aware: snaps to walkable terrain, + // 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(); - // Clamp Z to the terrain under the offset point so we don't - // hand PointMovementGenerator a Z that matches the target's - // floor but not the sampled (x,y) — avoids straight-line - // fallbacks through geometry. - bot->UpdateAllowedPositionZ(x, y, z); + float const dist = distance + target->GetObjectSize(); + target->GetNearPoint(bot, x, y, z, bot->GetObjectSize(), + std::min(dist, followRange), angle); - 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; 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(); + // 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), sPlayerbotAIConfig.followDistance)) { @@ -816,6 +826,42 @@ bool MovementAction::Follow(Unit* target, float distance, float angle) 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) { if (!IsMovingAllowed()) @@ -2530,7 +2576,7 @@ TravelPath MovementAction::ResolveMovePath(WorldPosition startPos, if (needsLongPath && !sTravelNodeMap.getNodes().empty() && !bot->InBattleground()) { - out = sTravelNodeMap.GetFullPath(startPos, bot->GetZoneId(), endPos, bot); + out = sTravelNodeMap.GetFullPath(startPos, endPos, bot); } else { diff --git a/src/Ai/Base/Actions/MovementActions.h b/src/Ai/Base/Actions/MovementActions.h index 10e4f2813..0d6e70cc3 100644 --- a/src/Ai/Base/Actions/MovementActions.h +++ b/src/Ai/Base/Actions/MovementActions.h @@ -111,6 +111,14 @@ 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); diff --git a/src/Mgr/Travel/TravelNode.cpp b/src/Mgr/Travel/TravelNode.cpp index 532e3e9fd..91dfbfe49 100644 --- a/src/Mgr/Travel/TravelNode.cpp +++ b/src/Mgr/Travel/TravelNode.cpp @@ -1512,7 +1512,7 @@ TravelNodeRoute TravelNodeMap::FindRouteNearestNodes(WorldPosition startPos, Wor return TravelNodeRoute(); } -TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos, [[maybe_unused]] uint32 botZoneId, +TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos, WorldPosition destination, Unit* bot) { TravelPath path; diff --git a/src/Mgr/Travel/TravelNode.h b/src/Mgr/Travel/TravelNode.h index ab9ac8555..20e5885ef 100644 --- a/src/Mgr/Travel/TravelNode.h +++ b/src/Mgr/Travel/TravelNode.h @@ -714,7 +714,7 @@ public: // 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, uint32 botZoneId, + TravelPath GetFullPath(WorldPosition botPos, WorldPosition destination, Unit* bot = nullptr); // Resolve A* route between two world positions (returns node vector)