From 665f2a3e56209df4d3351ce3a00f7fded4297137 Mon Sep 17 00:00:00 2001 From: bash Date: Sat, 30 May 2026 22:38:47 +0200 Subject: [PATCH] feat(Core/Movement): Add MovementAction::ResolveMovePath unified resolver --- src/Ai/Base/Actions/MovementActions.cpp | 49 +++++++++++++++++++++++++ src/Ai/Base/Actions/MovementActions.h | 9 +++++ 2 files changed, 58 insertions(+) diff --git a/src/Ai/Base/Actions/MovementActions.cpp b/src/Ai/Base/Actions/MovementActions.cpp index 8ca2eb101..3325d082d 100644 --- a/src/Ai/Base/Actions/MovementActions.cpp +++ b/src/Ai/Base/Actions/MovementActions.cpp @@ -3260,6 +3260,55 @@ bool MovementAction::GetTravelPlan(TravelPlan& plan, WorldPosition destination) return sTravelNodeMap.GetFullPath(plan, botPos, bot->GetZoneId(), destination, bot); } +TravelPath MovementAction::ResolveMovePath(WorldPosition const& startPos, + WorldPosition const& endPos, + LastMovement& lastMove) +{ + float const totalDistance = startPos.distance(endPos); + float const maxDistChange = totalDistance * 0.1f; + + // 10% reuse: cached path's tail close enough to new dest? Return as-is. + if (!lastMove.lastPath.empty() && + lastMove.lastPath.getBack().distance(endPos) < maxDistChange) + return lastMove.lastPath; + + // Long path = cross-map or beyond sight; otherwise pure mmap probe. + bool const needsLongPath = + startPos.GetMapId() != endPos.GetMapId() || + totalDistance > sPlayerbotAIConfig.sightDistance; + + TravelPath out; + + if (needsLongPath && !sTravelNodeMap.getNodes().empty() && !bot->InBattleground()) + { + // Wrap the legacy TravelPlan-populating call; the steps field on + // TravelPlan IS a TravelPath, so extract it directly. + TravelPlan tmp; + if (sTravelNodeMap.GetFullPath(tmp, startPos, bot->GetZoneId(), endPos, bot)) + out = tmp.steps; + } + else + { + WorldPosition mutableStart = startPos; + std::vector probe = mutableStart.getPathTo(endPos, bot); + out.addPath(probe); + } + + // Regression guard: if cached path's tail is no worse than the new + // path's tail, keep the cached one (catches probes blocked by geometry). + if (!lastMove.lastPath.empty() && !out.empty() && + lastMove.lastPath.getBack().distance(endPos) <= + out.getBack().distance(endPos)) + out = lastMove.lastPath; + + // Last-ditch fallback: a single point at the destination, so the + // caller has at least something to dispatch. + if (out.empty()) + out.addPoint(endPos); + + return out; +} + bool MovementAction::ExecuteTravelPlan(TravelPlan& state) { if (!state.IsActive()) diff --git a/src/Ai/Base/Actions/MovementActions.h b/src/Ai/Base/Actions/MovementActions.h index 5a2ba84f2..ec113073f 100644 --- a/src/Ai/Base/Actions/MovementActions.h +++ b/src/Ai/Base/Actions/MovementActions.h @@ -92,6 +92,15 @@ protected: 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 + // guard preferring cached path when no better, fall back to a + // single-point path on dest. Stateless — does not dispatch. + TravelPath ResolveMovePath(WorldPosition const& startPos, + WorldPosition const& endPos, + LastMovement& lastMove); + // Transport boarding helpers (shared by FollowAction and travel plan) static Transport* GetTransportForPosTolerant(Map* map, WorldObject* ref, uint32 phaseMask, float x, float y, float z);