diff --git a/src/Ai/Base/Actions/MovementActions.cpp b/src/Ai/Base/Actions/MovementActions.cpp index 254a12f50..287f7ca24 100644 --- a/src/Ai/Base/Actions/MovementActions.cpp +++ b/src/Ai/Base/Actions/MovementActions.cpp @@ -3047,202 +3047,6 @@ bool MoveAwayFromPlayerWithDebuffAction::isPossible() return bot->CanFreeMove(); } -bool MovementAction::LaunchWalkSpline(TravelPlan& state) -{ - if (state.walkPoints.size() < 2) - { - state.walkPoints.clear(); - return false; - } - - - // Trim past any stored points the bot has already moved past — useful - // when a spline is interrupted (combat, knockback, mid-spline reissue) - // and we re-launch from a position later in the route. - G3D::Vector3 botPos(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()); - float closestDist = FLT_MAX; - size_t closestIdx = 0; - for (size_t i = 0; i < state.walkPoints.size(); ++i) - { - float distance = (state.walkPoints[i] - botPos).squaredLength(); - if (distance < closestDist) - { - closestDist = distance; - closestIdx = i; - } - } - if (closestIdx > 0) - state.walkPoints.erase(state.walkPoints.begin(), state.walkPoints.begin() + closestIdx); - - if (state.walkPoints.size() < 2) - { - state.walkPoints.clear(); - return true; - } - - // Sparse-segment clip (cmangos parity): truncate the chain at the - // first segment longer than ~11.18y. Spline interpolation between - // sparse waypoints can cut corners through visual obstacles (trees, - // walls) the navmesh routed around. Bot re-plans from a closer - // position next tick where the resolved poly chain is denser. - { - constexpr float SPARSE_SEG_SQ = 125.0f; // sqrt(125) ≈ 11.18y - for (size_t i = 1; i < state.walkPoints.size(); ++i) - { - G3D::Vector3 d = state.walkPoints[i] - state.walkPoints[i - 1]; - if (d.squaredLength() > SPARSE_SEG_SQ) - { - state.walkPoints.resize(i); - break; - } - } - if (state.walkPoints.size() < 2) - { - state.walkPoints.clear(); - return true; - } - } - - // Re-clamp cached waypoints to current valid Z. Rows in - // playerbots_travelnode_path store absolute coords baked at - // offline generation; if the live navmesh has shifted since - // (mmap regen, terrain change, vmap update), the stored z can - // be above ground — MoveSplinePath plays back coords verbatim - // and the bot looks like it's walking through the air. - // UpdateAllowedPositionZ factors mmap polygon Z, water surface, - // swimming, flying and transport state, so cave floors above - // the terrain plane snap correctly. - for (auto& pt : state.walkPoints) - bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z); - - // Mount up - if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive()) - botAI->DoSpecificAction("check mount state", Event(), true); - - float totalDist = 0; - for (size_t i = 1; i < state.walkPoints.size(); ++i) - totalDist += (state.walkPoints[i] - state.walkPoints[i - 1]).length(); - - float speed = bot->GetSpeed(MOVE_RUN); - state.expectedDuration = static_cast((totalDist / speed) * IN_MILLISECONDS); - - bot->GetMotionMaster()->MoveSplinePath(&state.walkPoints, FORCED_MOVEMENT_RUN); - - G3D::Vector3 const& last = state.walkPoints.back(); - - // Mirror what MoveTo does after dispatching a spline so the - // lastPath cache below picks up the in-flight waypoint chain. - { - float delay = static_cast(state.expectedDuration); - delay = std::min(delay, static_cast(sPlayerbotAIConfig.maxWaitForMove)); - delay = std::max(delay, 0.f); - LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement"); - lastMove.Set(bot->GetMapId(), last.x, last.y, last.z, - bot->GetOrientation(), delay, MovementPriority::MOVEMENT_NORMAL); - - // Cache the dispatched waypoint chain so MoveFarTo's 10% - // lastPath reuse and "no worse" reuse can pick it up next tick. - std::vector wpts; - wpts.reserve(state.walkPoints.size()); - for (auto const& pt : state.walkPoints) - wpts.emplace_back(bot->GetMapId(), pt.x, pt.y, pt.z); - lastMove.setPath(TravelPath(wpts)); - } - - EmitDebugMove("TravelPlan:walk-start", "mmap", last.x, last.y, last.z); - - return false; // Walking -} - -bool MovementAction::RefineWalkPoints(std::vector& walkPoints) -{ - if (walkPoints.size() < 2) - return true; - - std::vector refined; - refined.reserve(walkPoints.size() * 4); - - uint32 const mapId = bot->GetMapId(); - - for (size_t i = 0; i + 1 < walkPoints.size(); ++i) - { - G3D::Vector3 const& a = walkPoints[i]; - G3D::Vector3 const& b = walkPoints[i + 1]; - - WorldPosition aPos(mapId, a.x, a.y, a.z); - WorldPosition bPos(mapId, b.x, b.y, b.z); - - // Per-segment mmap query: routes around geometry the offline - // graph didn't account for, or returns empty if unreachable. - std::vector segPath = bPos.getPathStepFrom(aPos, bot); - - // Trust the raw waypoint pair when mmap can't validate it — - // navmesh gaps/tile-edge artifacts shouldn't kill an active plan. - bool const trustRaw = segPath.empty() || - TravelPath::IsPathCheating(segPath, aPos.distance(bPos)); - - if (trustRaw) - { - if (i == 0) - refined.emplace_back(a); - refined.emplace_back(b); - continue; - } - - // Include the first segment's start; skip subsequent starts - // to avoid duplicating the prior segment's tail. - size_t startK = (i == 0) ? 0 : 1; - for (size_t k = startK; k < segPath.size(); ++k) - refined.emplace_back(segPath[k].GetPositionX(), - segPath[k].GetPositionY(), - segPath[k].GetPositionZ()); - } - - walkPoints = std::move(refined); - return true; -} - -bool MovementAction::MoveToSpline(TravelPlan& state, WorldPosition target) -{ - if (!IsMovingAllowed()) - return false; - - EmitDebugMove("TravelPlan:walk-waypoint", "mmap", target.GetPositionX(), target.GetPositionY(), target.GetPositionZ()); - - // Generate path - state.walkPoints.clear(); - PathResult path = GeneratePath(target.GetPositionX(), target.GetPositionY(), target.GetPositionZ()); - // Reject paths that PathGenerator marked unreachable. The default - // accept mask is NORMAL | INCOMPLETE; anything else (NOT_USING_PATH - // from BuildShortcut on invalid polys, NOPATH, etc.) means the - // dispatched waypoints would either be a straight-line through - // geometry or stop short of the target. Abort the plan instead so - // MoveFarTo can re-derive via its own probe. - if (!path.reachable) - { - state.walkPoints.clear(); - return false; - } - for (auto const& pt : path.points) - state.walkPoints.push_back(G3D::Vector3(pt.x, pt.y, pt.z)); - - if (state.walkPoints.size() < 2) - { - state.walkPoints.clear(); - return false; - } - - // Launch spline movement - LaunchWalkSpline(state); - return true; -} - -bool MovementAction::GetTravelPlan(TravelPlan& plan, WorldPosition destination) -{ - WorldPosition botPos(bot->GetMapId(), bot->GetPositionX(), - bot->GetPositionY(), bot->GetPositionZ()); - return sTravelNodeMap.GetFullPath(plan, botPos, bot->GetZoneId(), destination, bot); -} TravelPath MovementAction::ResolveMovePath(WorldPosition const& startPos, WorldPosition const& endPos, @@ -3453,341 +3257,6 @@ bool MovementAction::HandleSpecialMovement(TravelPath& path) } } -bool MovementAction::ExecuteTravelPlan(TravelPlan& state) -{ - if (!state.IsActive()) - return false; - - if (bot->IsInFlight()) - return true; - - // Per-step labels (`walk`, `segment`, `flight`, `transport-*`, - // `teleport(reason)`) cover every actual movement decision; emitting - // an executor-ran-this-tick label here would whisper every tick - // while the plan is active. - - if (state.stepIdx >= state.steps.size()) - { - state.Reset(); - return true; - } - - const PathNodePoint& pt = state.steps[state.stepIdx]; - - switch (pt.type) - { - case PathNodeType::NODE_PREPATH: - case PathNodeType::NODE_PATH: - case PathNodeType::NODE_NODE: - { - // Batch consecutive walkable points (PREPATH, PATH, NODE) into - // one spline. With per-tick re-resolve the plan starts at - // stepIdx=0 each tick, so we must dispatch a real spline (not - // a single-waypoint MoveTo) — otherwise the executor's - // "near closest waypoint" heuristic returns true without - // moving and the bot never advances. - // - // Capped at 20 points per dispatch as a cheap upper bound on - // per-tick work. The engine plays the spline; next tick - // re-resolves from the bot's new position and dispatches the - // next batch. - static constexpr uint32 MAX_SPLINE_POINTS = 20; - state.walkPoints.clear(); - while (state.stepIdx < state.steps.size() && state.walkPoints.size() < MAX_SPLINE_POINTS) - { - const PathNodePoint& wp = state.steps[state.stepIdx]; - if (wp.type != PathNodeType::NODE_PREPATH && - wp.type != PathNodeType::NODE_PATH && - wp.type != PathNodeType::NODE_NODE) - break; // stop at portal/transport/etc — can't walk past - state.walkPoints.push_back(G3D::Vector3(wp.point.GetPositionX(), - wp.point.GetPositionY(), wp.point.GetPositionZ())); - state.stepIdx++; - } - - if (state.walkPoints.empty()) - return true; - - // Already near end of batch? - G3D::Vector3 const& last = state.walkPoints.back(); - float dist = bot->GetExactDist(last.x, last.y, last.z); - if (dist < 10.0f) - { - state.walkPoints.clear(); - return true; - } - - // Too far from first point — abort the plan and let the - // caller's stuck-recovery decide what to do. An abandoned - // plan is recovered by the next MoveFarTo cycle. - if (state.walkPoints.size() >= 2) - { - G3D::Vector3 const& first = state.walkPoints.front(); - float distToFirst = bot->GetExactDist(first.x, first.y, first.z); - if (distToFirst > MAX_PATHFINDING_DISTANCE) - { - state.walkPoints.clear(); - state.Reset(); - return false; - } - } - // Single point — use PathGenerator directly - if (state.walkPoints.size() < 2) - { - WorldPosition target(bot->GetMapId(), last.x, last.y, last.z); - MoveToSpline(state, target); - state.walkPoints.clear(); - return true; - } - - // Re-validate each segment against the live navmesh and - // substitute mmap-routed sub-paths where needed. - if (!RefineWalkPoints(state.walkPoints)) - { - G3D::Vector3 const& failPt = state.walkPoints.empty() - ? G3D::Vector3(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()) - : state.walkPoints.front(); - EmitDebugMove("TravelPlan", "segment-unwalkable", - failPt.x, failPt.y, failPt.z); - state.walkPoints.clear(); - state.Reset(); - return false; - } - - LaunchWalkSpline(state); - return true; - } - - case PathNodeType::NODE_AREA_TRIGGER: - { - // Pair: trigger (pointIdx) + dest (pointIdx+1). - // Bot walks into the area trigger volume; server teleports - // on entry. Bot may need quest/key prereqs to actually cross. - if (state.stepIdx + 1 >= state.steps.size()) - { - state.Reset(); - return false; - } - - const PathNodePoint& trigger = state.steps[state.stepIdx]; - const PathNodePoint& dst = state.steps[state.stepIdx + 1]; - - // Already on destination map — trigger fired, advance. - if (bot->GetMapId() == dst.point.GetMapId()) - { - state.stepIdx += 2; - return true; - } - - // Walk to the trigger position; collision with the trigger - // volume teleports us. - float dist = bot->GetExactDist(trigger.point.GetPositionX(), - trigger.point.GetPositionY(), - trigger.point.GetPositionZ()); - if (dist > INTERACTION_DISTANCE) - return MoveTo(trigger.point.GetMapId(), - trigger.point.GetPositionX(), - trigger.point.GetPositionY(), - trigger.point.GetPositionZ()); - - // At trigger but didn't teleport — likely missing quest/key. - // Abort; the do-quest yield-to-grind multiplier or next - // POI pick can reroute. - state.Reset(); - return false; - } - - case PathNodeType::NODE_STATIC_PORTAL: - { - // Pair: portal-GO position (pointIdx) + dest (pointIdx+1). - // Bot walks within interact range of the portal GameObject - // and sends CMSG_GAMEOBJ_USE to trigger its teleport spell. - if (state.stepIdx + 1 >= state.steps.size()) - { - state.Reset(); - return false; - } - - const PathNodePoint& portal = state.steps[state.stepIdx]; - const PathNodePoint& dst = state.steps[state.stepIdx + 1]; - - if (bot->GetMapId() == dst.point.GetMapId()) - { - state.stepIdx += 2; - return true; - } - - // Walk to portal GO position - float dist = bot->GetExactDist(portal.point.GetPositionX(), - portal.point.GetPositionY(), - portal.point.GetPositionZ()); - if (dist > INTERACTION_DISTANCE) - return MoveTo(portal.point.GetMapId(), - portal.point.GetPositionX(), - portal.point.GetPositionY(), - portal.point.GetPositionZ()); - - // In range — find the portal GameObject and interact - if (!portal.entry) - { - state.Reset(); - return false; - } - - if (bot->IsMounted()) - bot->Dismount(); - botAI->RemoveShapeshift(); - - GuidVector nearGOs = AI_VALUE(GuidVector, "nearest game objects"); - for (ObjectGuid const& guid : nearGOs) - { - GameObject* go = botAI->GetGameObject(guid); - if (!go || go->GetEntry() != portal.entry) - continue; - if (!bot->GetGameObjectIfCanInteractWith(guid, GAMEOBJECT_TYPE_SPELLCASTER)) - continue; - - WorldPacket packet(CMSG_GAMEOBJ_USE); - packet << guid; - bot->GetSession()->QueuePacket(new WorldPacket(packet)); - return true; - } - - // GO not found nearby — abort and let next tick try again - state.Reset(); - return false; - } - - case PathNodeType::NODE_TRANSPORT: - { - if (state.stepIdx + 1 >= state.steps.size()) - { - state.Reset(); - return false; - } - - const PathNodePoint& board = state.steps[state.stepIdx]; - const PathNodePoint& arrive = state.steps[state.stepIdx + 1]; - // Arrived at destination? - if (bot->GetMapId() == arrive.point.GetMapId() && !bot->GetTransport()) - { - state.stepIdx += 2; - return true; - } - // On transport — wait - if (bot->GetTransport()) - { - if (bot->GetMapId() == arrive.point.GetMapId()) - { - bot->GetTransport()->RemovePassenger(bot); - bot->StopMovingOnCurrentPos(); - state.stepIdx += 2; - } - return true; - } - - // Walk to boarding point - float dist = bot->GetExactDist(board.point.GetPositionX(), board.point.GetPositionY(), board.point.GetPositionZ()); - if (dist > 60.0f) - return MoveTo(board.point.GetMapId(), board.point.GetPositionX(), board.point.GetPositionY(), board.point.GetPositionZ()); - - // Try to board - if (board.entry) - { - Map* map = bot->GetMap(); - if (map) - { - Transport* transport = - GetTransportForPosTolerant(map, bot, bot->GetPhaseMask(), board.point.GetPositionX(), - board.point.GetPositionY(), board.point.GetPositionZ()); - if (transport && transport->GetEntry() == board.entry) - { - BoardTransport(transport); - return true; - } - } - } - // Wait at boarding point - if (dist > INTERACTION_DISTANCE) - return MoveTo(board.point.GetMapId(), board.point.GetPositionX(), board.point.GetPositionY(), board.point.GetPositionZ()); - return true; - } - - case PathNodeType::NODE_FLIGHTPATH: - { - if (state.stepIdx + 1 >= state.steps.size()) - { - state.Reset(); - return false; - } - - const PathNodePoint& dep = state.steps[state.stepIdx]; - const PathNodePoint& arr = state.steps[state.stepIdx + 1]; - - if (bot->IsInFlight()) - return true; - - // Resolve taxi path - if (state.route.empty()) - { - uint32 fromTaxi = sObjectMgr->GetNearestTaxiNode(dep.point.GetPositionX(), dep.point.GetPositionY(), - dep.point.GetPositionZ(), dep.point.GetMapId(), bot->GetTeamId()); - uint32 toTaxi = sObjectMgr->GetNearestTaxiNode(arr.point.GetPositionX(), arr.point.GetPositionY(), - arr.point.GetPositionZ(), arr.point.GetMapId(), bot->GetTeamId()); - - if (fromTaxi && toTaxi && fromTaxi != toTaxi) - state.route = sTravelNodeMap.FindTaxiPath(fromTaxi, toTaxi); - - if (state.route.empty()) - { - state.stepIdx += 2; - return true; - } - } - - TravelMgr::FlightMasterInfo const* fmInfo = sTravelMgr.GetNearestFlightMasterInfo(bot); - if (!fmInfo) - { - state.route.clear(); - state.stepIdx += 2; - return true; - } - - if (bot->GetDistance(fmInfo->pos) > INTERACTION_DISTANCE) - return MoveTo(fmInfo->pos.GetMapId(), fmInfo->pos.GetPositionX(), - fmInfo->pos.GetPositionY(), fmInfo->pos.GetPositionZ()); - - ObjectGuid fmGuid = ObjectGuid::Create(fmInfo->templateEntry, fmInfo->dbGuid); - Creature* flightMaster = ObjectAccessor::GetCreature(*bot, fmGuid); - if (!flightMaster || !flightMaster->IsAlive()) - { - state.route.clear(); - state.stepIdx += 2; - return true; - } - - botAI->RemoveShapeshift(); - if (bot->IsMounted()) - bot->Dismount(); - - bot->ActivateTaxiPathTo(state.route, flightMaster, 0); - - state.route.clear(); - state.stepIdx += 2; - return true; - } - - default: - { - LOG_ERROR("playerbots", - "[TravelPlan] Bot {} encountered unknown PathNodeType ({}); resetting plan", - bot->GetName(), static_cast(pt.type)); - state.Reset(); - return false; - } - } - return false; -} Transport* MovementAction::GetTransportForPosTolerant(Map* map, WorldObject* ref, uint32 phaseMask, float x, float y, float z) { diff --git a/src/Ai/Base/Actions/MovementActions.h b/src/Ai/Base/Actions/MovementActions.h index bb0e77cd4..a83c06fc3 100644 --- a/src/Ai/Base/Actions/MovementActions.h +++ b/src/Ai/Base/Actions/MovementActions.h @@ -89,9 +89,6 @@ protected: PathResult GeneratePath(float x, float y, float z, uint32 acceptMask = DEFAULT_PATH_ACCEPT_MASK, bool forceDestination = false); - bool GetTravelPlan(TravelPlan& plan, WorldPosition destination); - bool ExecuteTravelPlan(TravelPlan& state); - // Returns a unified TravelPath for the move. Mirror of the reference // ResolveMovePath shape: 10% lastPath reuse short-circuit, choose // graph (cross-map / >sightDistance) or live mmap probe, regression @@ -126,16 +123,6 @@ protected: float& outX, float& outY, float& outZ); bool BoardTransport(Transport* transport); -private: - bool LaunchWalkSpline(TravelPlan& state); - bool MoveToSpline(TravelPlan& state, WorldPosition target); - // Per-segment mmap refinement of a travel-node-graph walk batch. - // The graph stores offline-baked coords whose straight-line - // interpolation may pass through geometry the bot can't actually - // traverse. Returns false if any segment is unwalkable per the - // live navmesh, in which case the caller should abort the plan. - bool RefineWalkPoints(std::vector& walkPoints); - protected: struct CheckAngle {