From 3710c35a4118bfa28b2f89b4a930ecd13eb0ea5f Mon Sep 17 00:00:00 2001 From: bash Date: Sun, 10 May 2026 02:22:14 +0200 Subject: [PATCH] feat(Core/Travel): Match cmangos MoveTo2 flow in MoveFarTo and DispatchPathPoints --- src/Ai/Base/Actions/MovementActions.cpp | 4 +- src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp | 363 ++++++++++++++----- src/Ai/World/Rpg/Action/NewRpgBaseAction.h | 11 + src/Mgr/Travel/TravelMgr.cpp | 106 +++++- src/Mgr/Travel/TravelMgr.h | 2 + src/Mgr/Travel/TravelNode.cpp | 21 +- src/Mgr/Travel/TravelNode.h | 2 +- src/PlayerbotAIConfig.cpp | 1 + src/PlayerbotAIConfig.h | 2 +- 9 files changed, 392 insertions(+), 120 deletions(-) diff --git a/src/Ai/Base/Actions/MovementActions.cpp b/src/Ai/Base/Actions/MovementActions.cpp index 456ab96f3..bdec4dcd2 100644 --- a/src/Ai/Base/Actions/MovementActions.cpp +++ b/src/Ai/Base/Actions/MovementActions.cpp @@ -330,7 +330,7 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle, return false; } - bool generatePath = !bot->IsFlying() && !bot->isSwimming(); + bool generatePath = !bot->IsFlying() && !bot->isSwimming() && !bot->IsInWater(); bool disableMoveSplinePath = sPlayerbotAIConfig.disableMoveSplinePath >= 2 || (sPlayerbotAIConfig.disableMoveSplinePath == 1 && bot->InBattleground()); @@ -3308,7 +3308,7 @@ bool MovementAction::GetTravelPlan(TravelPlan& plan, WorldPosition destination) destination.GetPositionX(), destination.GetPositionY(), destination.GetPositionZ(), destination.GetMapId(), botPos.fDist(destination)); - return sTravelNodeMap.GetFullPath(plan, botPos, bot->GetZoneId(), destination); + return sTravelNodeMap.GetFullPath(plan, botPos, bot->GetZoneId(), destination, bot); } bool MovementAction::ExecuteTravelPlan(TravelPlan& state) diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp index a0fdeed2b..c9184b400 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp @@ -53,10 +53,24 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) return false; // Already-at-dest short-stop. Below targetPosRecalcDistance - // (default 0.1y) the move is effectively done — no need to - // recompute or dispatch. - if (bot->GetExactDist(dest) < sPlayerbotAIConfig.targetPosRecalcDistance) - return false; + // (default 0.1y) the move is effectively done. Stop any spline + // still running and clear the cached path if it points to here + // — otherwise the bot keeps gliding past dest. Mirrors cmangos + // MovementActions.cpp:1095-1106. + { + float const totalDistance = bot->GetExactDist(dest); + if (totalDistance < sPlayerbotAIConfig.targetPosRecalcDistance) + { + LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement"); + if (!lastMove.lastPath.empty() && + lastMove.lastPath.getBack().distance(dest) <= totalDistance) + { + lastMove.clear(); + } + bot->StopMoving(); + return false; + } + } // 10% lastPath reuse — if the cached path's endpoint is still // close (within 10%) to the new dest, trim the cached path to @@ -96,24 +110,7 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) points.reserve(pts.size()); for (auto const& wp : pts) points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ()); - for (auto& pt : points) - bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z); - bot->GetMotionMaster()->Clear(); - bot->GetMotionMaster()->MoveSplinePath(&points, FORCED_MOVEMENT_RUN); - - G3D::Vector3 const& last = points.back(); - float totalChainDist = 0.f; - for (size_t i = 1; i < points.size(); ++i) - totalChainDist += (points[i] - points[i - 1]).length(); - float speed = std::max(bot->GetSpeed(MOVE_RUN), 0.1f); - uint32 expectedMs = static_cast((totalChainDist / speed) * IN_MILLISECONDS); - uint32 cappedMs = std::min(expectedMs, (uint32)sPlayerbotAIConfig.maxWaitForMove); - lastMove.Set(bot->GetMapId(), last.x, last.y, last.z, - bot->GetOrientation(), cappedMs, MovementPriority::MOVEMENT_NORMAL); - - EmitDebugMove("MoveFar", "reuse", - dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); - return true; + return DispatchPathPoints(dest, points, "reuse"); } } // Path was cleared or collapsed — fall through to fresh planning. @@ -124,23 +121,44 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) float const dis = bot->GetExactDist(dest); - // Decision tree: + // Mirrors cmangos ResolveMovePath: pick travel-node graph for + // long-distance / cross-map moves; mmap probe for short paths. // - // 1. 40-step chained mmap probe FIRST. - // 2. Regression guard — if cached lastPath ends ≤ as close to - // dest as the new probe, ride the cached path instead. - // 3. If probe reaches dest within 25y, dispatch probe waypoints. - // 4. Else if travel nodes enabled, try the node graph as fallback. - // 5. Else dispatch the destination as a single-waypoint spline - // via MoveTo — engine MovePoint(generatePath=true) resolves - // the local route via PathGenerator. + // 1. needsLongPath = cross-map OR dis > sightDistance + // 2. If needsLongPath && travel nodes enabled → graph plan + // 3. Else 40-step chained mmap probe + // 4. Regression guard against the new probe — ride cached path + // if it ends at least as close to dest + // 5. Dispatch probe (no reach gate — partial probes still make + // progress and the next tick replans) + // 6. Empty probe → cmangos addPoint(endPosition) fallback: + // single-waypoint MoveTo so PathGenerator resolves it. + + bool const needsLongPath = (bot->GetMapId() != dest.GetMapId()) || + (dis > sPlayerbotAIConfig.sightDistance); + + // Skip travel-node planning inside battlegrounds — the graph is + // built for the open world and yields nonsense routes inside BGs. + // Mirrors cmangos MovementActions.cpp:705. + if (needsLongPath && sPlayerbotAIConfig.enableTravelNodes && + !bot->InBattleground()) + { + StartTravelPlan(dest); + if (botAI->rpgInfo.HasActiveTravelPlan()) + { + LOG_INFO("playerbots", "[MoveFar] {} nodetravel | dis={:.0f}", + bot->GetName(), dis); + EmitDebugMove("MoveFar", "travelplan", + dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); + return UpdateTravelPlan(); + } + } // 40-step chained mmap probe. WorldPosition botPos(bot); std::vector probe = botPos.getPathTo(dest, bot); - // Regression guard: if a cached lastPath ends at least as close - // to dest as the new probe's endpoint, ride the cached path. + // Regression guard. { LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement"); if (!lastMove.lastPath.empty() && !probe.empty() && probe.size() >= 2) @@ -165,24 +183,7 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) points.reserve(pts.size()); for (auto const& wp : pts) points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ()); - for (auto& pt : points) - bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z); - bot->GetMotionMaster()->Clear(); - bot->GetMotionMaster()->MoveSplinePath(&points, FORCED_MOVEMENT_RUN); - - G3D::Vector3 const& last = points.back(); - float totalChainDist = 0.f; - for (size_t i = 1; i < points.size(); ++i) - totalChainDist += (points[i] - points[i - 1]).length(); - float speed = std::max(bot->GetSpeed(MOVE_RUN), 0.1f); - uint32 expectedMs = static_cast((totalChainDist / speed) * IN_MILLISECONDS); - uint32 cappedMs = std::min(expectedMs, (uint32)sPlayerbotAIConfig.maxWaitForMove); - lastMove.Set(bot->GetMapId(), last.x, last.y, last.z, - bot->GetOrientation(), cappedMs, MovementPriority::MOVEMENT_NORMAL); - - EmitDebugMove("MoveFar", "regress-keep", - dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); - return true; + return DispatchPathPoints(dest, points, "regress-keep"); } } } @@ -190,76 +191,240 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) } } - // Probe dispatch — only if the probe reaches dest within 25y. - // Partial probes that fall short go through the graph fallback - // below instead of being dispatched as-is. - constexpr float PROBE_REACH = 25.0f; - if (!probe.empty() && probe.size() >= 2 && dest.isPathTo(probe, PROBE_REACH)) + if (!probe.empty() && probe.size() >= 2) { Movement::PointsArray points; points.reserve(probe.size()); for (auto const& wp : probe) points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ()); - for (auto& pt : points) - bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z); - if (points.size() >= 2) { LOG_INFO("playerbots", "[MoveFar] {} mmap-path | dis={:.0f} | wp={}", bot->GetName(), dis, (uint32)points.size()); - EmitDebugMove("MoveFar", "mmap", - points.back().x, points.back().y, points.back().z); if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive()) botAI->DoSpecificAction("check mount state", Event(), true); - bot->GetMotionMaster()->Clear(); - bot->GetMotionMaster()->MoveSplinePath(&points, FORCED_MOVEMENT_RUN); - - 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(); - float speed = std::max(bot->GetSpeed(MOVE_RUN), 0.1f); - uint32 expectedMs = static_cast((totalDist / speed) * IN_MILLISECONDS); - uint32 cappedMs = std::min(expectedMs, (uint32)sPlayerbotAIConfig.maxWaitForMove); - LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement"); - lastMove.Set(bot->GetMapId(), last.x, last.y, last.z, - bot->GetOrientation(), cappedMs, MovementPriority::MOVEMENT_NORMAL); - - std::vector wpts; - wpts.reserve(points.size()); - for (auto const& pt : points) - wpts.emplace_back(bot->GetMapId(), pt.x, pt.y, pt.z); - lastMove.setPath(TravelPath(wpts)); - - return true; + return DispatchPathPoints(dest, points, "mmap"); } } - // Travel-node graph fallback — fires when the probe didn't reach - // dest within PROBE_REACH and the graph is enabled. - if (sPlayerbotAIConfig.enableTravelNodes) + // Empty-probe fallback — cmangos addPoint(endPosition) + DispatchMovement. + // Build a 2-point straight-line path (bot → dest) and route it through + // DispatchPathPoints so it gets the same clip / underwater fixup / + // setPath / WaitForReach / teleport / mode-flip treatment as the + // chained-mmap branches. Trade-off vs the old MoveTo path: lose the + // engine's MovePoint(generatePath=true) local PathGenerator resolution. + // For the empty-probe case the long mmap probe already failed, so the + // engine's local resolution would likely fail too. + Movement::PointsArray fallback; + fallback.emplace_back(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()); + fallback.emplace_back(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); + return DispatchPathPoints(dest, fallback, "spline"); +} + +bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest, + Movement::PointsArray& points, + char const* label) +{ + if (points.size() < 2) + return false; + + // Save the planner's path BEFORE clip/fixup mutations, so the + // next-tick reuse/regress branches see the original intent (not + // a clip-truncated tail). Mirrors cmangos MovementActions.cpp:1117 + + // 1147 — both saves happen before ClipPath at :1150 and the + // underwater fixup loop at :1170. { - StartTravelPlan(dest); - if (botAI->rpgInfo.HasActiveTravelPlan()) + LastMovement& lm = AI_VALUE(LastMovement&, "last movement"); + std::vector wpts; + wpts.reserve(points.size()); + for (auto const& pt : points) + wpts.emplace_back(dest.GetMapId(), pt.x, pt.y, pt.z); + lm.setPath(TravelPath(wpts)); + } + + // Item 5 — underwater fixup. Push waypoints submerged below the + // water surface up to the surface itself, unless the destination + // is itself underwater (e.g. fishing-loot quest GO, sunken ruins). + { + WorldPosition destWp = dest; + if (!destWp.isUnderWater()) { - LOG_INFO("playerbots", "[MoveFar] {} nodetravel | dis={:.0f}", - bot->GetName(), dis); - EmitDebugMove("MoveFar", "travelplan", - dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); - return UpdateTravelPlan(); + Map* map = bot->GetMap(); + for (auto& pt : points) + { + WorldPosition wp(dest.GetMapId(), pt.x, pt.y, pt.z); + if (wp.isUnderWater()) + { + float surface = map->GetWaterLevel(pt.x, pt.y); + if (surface != INVALID_HEIGHT && surface > pt.z) + pt.z = surface; + } + } } } - // Final fallback: dispatch the destination as a single waypoint. - // MoveTo's MovePoint(generatePath=true) lets the engine resolve the - // local route via PathGenerator. Nothing dispatched if MoveTo refuses. - EmitDebugMove("MoveFar", "spline", - dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); - return MoveTo(dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), - false, false, false, false); + for (auto& pt : points) + bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z); + + // ClipPath — truncate the path at the first hostile creature within + // its own attack range, so the bot stops walking into pulls instead + // of marching past mobs and aggroing them all at once. Skipped while + // already in combat or dead. Mirrors cmangos TravelNode.cpp:1118-1203 + // in simplified form (no transport, no hazard list). + if (botAI->GetState() != BOT_STATE_COMBAT && bot->IsAlive()) + { + GuidVector targets = AI_VALUE(GuidVector, "possible targets"); + if (!targets.empty()) + { + size_t clipAt = points.size(); + for (size_t i = 0; i < points.size() && clipAt == points.size(); ++i) + { + for (ObjectGuid const& guid : targets) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + Creature* cre = unit->ToCreature(); + if (!cre) + continue; + if (unit->GetLevel() > bot->GetLevel() + 5) + continue; + float range = cre->GetAttackDistance(bot); + float dx = unit->GetPositionX() - points[i].x; + float dy = unit->GetPositionY() - points[i].y; + float dz = unit->GetPositionZ() - points[i].z; + if (dx * dx + dy * dy + dz * dz > range * range) + continue; + // LOS guard — mobs behind walls don't actually + // pull, so clipping there over-truncates. Mirrors + // cmangos TravelNode.cpp:1155. + if (!unit->IsWithinLOSInMap(bot)) + continue; + clipAt = i; + break; + } + } + if (clipAt > 0 && clipAt + 1 < points.size()) + points.erase(points.begin() + clipAt + 1, points.end()); + } + } + + 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"); + + // Item 2 — inactive-bot teleport. When the path is longer than + // reactDistance and there's no real player around to witness, jump + // to the path tail and schedule a cooldown. Skips cosmetic walking + // for unobserved random bots. Player-owned (self) bots are excluded + // so testing/observed sessions always see the real walk. + 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)); + } + } + + // masterWalking — match the master's walk pace when they're nearby + // and walking. Lets a follower bot trail at walk speed instead of + // sprinting past. No-op for masterless RPG bots. Mirrors cmangos + // MovementActions.cpp:1212-1221. (Flying-mount mode flip deferred — + // requires takeoff/landing infrastructure we haven't ported.) + ForcedMovement moveMode = FORCED_MOVEMENT_RUN; + if (sPlayerbotAIConfig.walkDistance > 0.0f) + { + if (Player* master = botAI->GetMaster()) + { + if (bot->IsFriendlyTo(master) && master->IsWalking() && + bot->GetExactDist2d(master) < sPlayerbotAIConfig.walkDistance) + { + moveMode = FORCED_MOVEMENT_WALK; + } + } + } + + // Debug-move beacon — when the `debug move` strategy is active in + // non-combat, summon a visible creature at every waypoint (white + // glow, red glow on the tail). Lets the operator visually verify + // ClipPath truncation, underwater fixup, masterWalking pace, etc. + // Mirrors cmangos MovementActions.cpp:1152-1161. + if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT)) + { + for (size_t i = 0; i < points.size(); ++i) + { + G3D::Vector3 const& p = points[i]; + if (Creature* wp = bot->SummonCreature(2334, p.x, p.y, p.z, 0, + TEMPSUMMON_TIMED_DESPAWN, 10000)) + { + bot->AddAura(246, wp); + if (i + 1 == points.size()) + bot->AddAura(1130, wp); + } + } + } + + // Pre-dispatch state cleanup. Mirrors cmangos MovementActions.cpp:1186-1194: + // - Clear any looping emote so the bot doesn't run-while-waving + // - Stand up unconditionally (eating/sitting clients ignore moves) + // - Interrupt any non-melee cast so the spline can begin + bot->ClearEmoteState(); + if (!bot->IsStandState()) + bot->SetStandState(UNIT_STAND_STATE_STAND); + if (bot->IsNonMeleeSpellCast(true)) + bot->InterruptNonMeleeSpells(true); + + // Item 7 — DispatchMovement two-step. cmangos calls + // mm.MovePoint(last) followed by mm.MovePath(points). In AC, + // MotionMaster::Mutate at MOTION_SLOT_ACTIVE replaces (not queues) + // the previous generator, so both calls in sequence collapse to + // whichever ran last. We skip the redundant MovePoint and dispatch + // the smooth waypoint path directly. + bot->GetMotionMaster()->Clear(); + bot->GetMotionMaster()->MoveSplinePath(&points, moveMode); + + EmitDebugMove("MoveFar", label, last.x, last.y, last.z); + + // Item 6 — WaitForReach scheduling. + // waitDist = (totalDist > reactDistance) ? totalDist - 10 : totalDist + // duration = 1000 * (waitDist / runSpeed) + reactDelay + // capped at maxWaitForMove. The −10y leaves a small buffer so the + // AI tick can intervene before the path strictly finishes. + 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; } void NewRpgBaseAction::StartTravelPlan(WorldPosition dest) diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h index c720473f9..fba0e5c4b 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h @@ -79,6 +79,17 @@ protected: private: void StartTravelPlan(WorldPosition dest); bool UpdateTravelPlan(); + + // Dispatches a chained mmap path. Applies cmangos-parity tweaks: + // underwater fixup (push submerged waypoints to the surface unless + // dest is itself underwater), inactive-bot teleport (jump to the + // tail when no players are nearby and the path is longer than + // reactDistance), and the WaitForReach formula + // (1000 * dist/speed + reactDelay, capped at maxWaitForMove). + // Returns true if dispatched or teleported. + bool DispatchPathPoints(WorldPosition const& dest, + Movement::PointsArray& points, + char const* label); }; #endif diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp index c1979f1c3..b524a69ca 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -721,6 +721,22 @@ std::vector WorldPosition::getPathStepFrom(WorldPosition startPos map->EnsureGridCreated(Acore::ComputeGridCoord(GetPositionX(), GetPositionY())); } + PathGenerator path(pathUnit); + path.AddExcludeFlag(NAV_GROUND_STEEP); + auto result = getPathStepFrom(startPos, path); + + if (tempCreature) + delete tempCreature; + + return result; +} + +// Pathfinder-reuse overload — caller owns the PathGenerator and any +// per-call configuration (filters, area costs). Mirrors cmangos +// WorldPosition.cpp:958 which threads one PathFinder through the whole +// 40-step chain instead of constructing a new one per step. +std::vector WorldPosition::getPathStepFrom(WorldPosition startPos, PathGenerator& path) +{ // Explicit-start overload (PathGenerator.h:67). Without this, // CalculatePath(destX,destY,destZ) defaults to the unit's // current position as start — which means every iteration of @@ -729,17 +745,12 @@ std::vector WorldPosition::getPathStepFrom(WorldPosition startPos // never advances. With explicit start, each step extends from // the previous step's endpoint, giving the 40-attempt walker // its intended multi-tile reach. - PathGenerator path(pathUnit); - path.AddExcludeFlag(NAV_GROUND_STEEP); path.CalculatePath(startPos.GetPositionX(), startPos.GetPositionY(), startPos.GetPositionZ(), GetPositionX(), GetPositionY(), GetPositionZ(), false); Movement::PointsArray points = path.GetPath(); PathType type = path.GetPathType(); - if (tempCreature) - delete tempCreature; - // PathType is a bitmask. Two things to handle: // // 1. AC's PathGenerator can return INCOMPLETE | FARFROMPOLY_END @@ -755,11 +766,50 @@ std::vector WorldPosition::getPathStepFrom(WorldPosition startPos // To match cmangos's intent (never silently dispatch a // geometry-ignoring shortcut), reject any path with the // NOT_USING_PATH bit set. - if ((type & (PATHFIND_NORMAL | PATHFIND_INCOMPLETE)) - && !(type & PATHFIND_NOT_USING_PATH)) - return fromPointsArray(points); + if (!(type & (PATHFIND_NORMAL | PATHFIND_INCOMPLETE)) || + (type & PATHFIND_NOT_USING_PATH)) + return {}; - return {}; + std::vector retvec = fromPointsArray(points); + + // Underwater path-extension. Mirrors cmangos WorldPosition.cpp:997-1014. + // When PATHFIND_INCOMPLETE ends within 50y of dest and both endpoints + // are underwater with LOS between them, extend by one 5y step (or + // straight to dest if <5y). Lets bots traverse navmesh-poor water + // volumes the same way real swimmers do. + if (type & PATHFIND_INCOMPLETE) + { + WorldPosition end = *this; + WorldPosition lastPoint = retvec.back(); + float dist = lastPoint.distance(&end); + + if (dist < 50.0f && lastPoint.isUnderWater() && end.isUnderWater()) + { + Map* m = end.getMap(); + bool inLos = m && m->isInLineOfSight( + lastPoint.GetPositionX(), lastPoint.GetPositionY(), lastPoint.GetPositionZ() + 1.0f, + end.GetPositionX(), end.GetPositionY(), end.GetPositionZ() + 1.0f, + PHASEMASK_NORMAL, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::Nothing); + if (inLos) + { + if (dist < 5.0f) + retvec.push_back(end); + else + { + float dx = end.GetPositionX() - lastPoint.GetPositionX(); + float dy = end.GetPositionY() - lastPoint.GetPositionY(); + float dz = end.GetPositionZ() - lastPoint.GetPositionZ(); + float scale = 5.0f / dist; + retvec.emplace_back(end.GetMapId(), + lastPoint.GetPositionX() + dx * scale, + lastPoint.GetPositionY() + dy * scale, + lastPoint.GetPositionZ() + dz * scale); + } + } + } + } + + return retvec; } bool WorldPosition::cropPathTo(std::vector& path, float maxDistance) @@ -795,11 +845,44 @@ std::vector WorldPosition::getPathFromPath(std::vector subPath, fullPath = startPath; + // Construct ONE PathGenerator and thread it through every step. + // Mirrors cmangos WorldPosition.cpp:1091-1096. Avoids the per-step + // allocation and lets Detour reuse internal scratch. + Unit* pathUnit = bot; + Creature* tempCreature = nullptr; + if (!pathUnit) + { + Map* map = sMapMgr->FindBaseMap(GetMapId()); + if (!map) + return fullPath; + + tempCreature = new Creature(); + if (!tempCreature->Create(map->GenerateLowGuid(), map, + PHASEMASK_NORMAL, 1 /*entry*/, 0, + currentPos.GetPositionX(), currentPos.GetPositionY(), + currentPos.GetPositionZ(), 0)) + { + delete tempCreature; + return fullPath; + } + pathUnit = tempCreature; + map->EnsureGridCreated(Acore::ComputeGridCoord(currentPos.GetPositionX(), currentPos.GetPositionY())); + map->EnsureGridCreated(Acore::ComputeGridCoord(GetPositionX(), GetPositionY())); + } + + PathGenerator path(pathUnit); + path.AddExcludeFlag(NAV_GROUND_STEEP); + + // Area-cost biases. Mirrors cmangos WorldPosition.cpp:1098-1100. + // NAV_WATER weighted 10x so A* prefers shore routes over wading + // through lakes when both are reachable. + path.SetAreaCost(NAV_WATER, 10.0f); + // Limit the pathfinding attempts for (uint32 i = 0; i < maxAttempt; i++) { // Try to pathfind to this position. - subPath = getPathStepFrom(currentPos, bot); + subPath = getPathStepFrom(currentPos, path); // If we could not find a path return what we have now. if (subPath.empty() || currentPos.distance(&subPath.back()) < sPlayerbotAIConfig.targetPosRecalcDistance) @@ -816,6 +899,9 @@ std::vector WorldPosition::getPathFromPath(std::vector getPathStepFrom(WorldPosition startPos, Unit* bot); + std::vector getPathStepFrom(WorldPosition startPos, PathGenerator& pathfinder); std::vector getPathFromPath(std::vector startPath, Unit* bot, uint8 maxAttempt = 40); std::vector getPathFrom(WorldPosition startPos, Unit* bot) diff --git a/src/Mgr/Travel/TravelNode.cpp b/src/Mgr/Travel/TravelNode.cpp index df77344ac..208888f9b 100644 --- a/src/Mgr/Travel/TravelNode.cpp +++ b/src/Mgr/Travel/TravelNode.cpp @@ -1285,18 +1285,25 @@ TravelNodeRoute TravelNodeMap::FindRouteNearestNodes(WorldPosition startPos, Wor bool TravelNodeMap::GetFullPath(TravelPlan& plan, WorldPosition botPos, uint32 botZoneId, - WorldPosition destination) + WorldPosition destination, Unit* bot) { plan.Reset(); plan.destination = destination; - // Short distance — direct walk, no nodes needed - if (botPos.fDist(destination) < MAX_PATHFINDING_DISTANCE && - botPos.GetMapId() == destination.GetMapId()) + // mmap probe first — mirrors cmangos getFullPath (TravelNode.cpp:1887-1895). + // 40-step chained probe from bot; if it gets within spellDistance of dest + // we skip the graph entirely (a short walk is always better than a node + // hop). When the probe falls short, fall through to graph routing. + if (botPos.GetMapId() == destination.GetMapId()) { - plan.steps.addPoint(botPos, PathNodeType::NODE_PREPATH); - plan.steps.addPoint(destination, PathNodeType::NODE_PATH); - return true; + std::vector probe = destination.getPathFromPath({botPos}, bot, 40); + if (destination.isPathTo(probe, sPlayerbotAIConfig.spellDistance)) + { + plan.steps.addPoint(botPos, PathNodeType::NODE_PREPATH); + for (size_t i = 1; i < probe.size(); ++i) + plan.steps.addPoint(probe[i], PathNodeType::NODE_PATH); + return true; + } } std::shared_lock guard(m_nMapMtx); diff --git a/src/Mgr/Travel/TravelNode.h b/src/Mgr/Travel/TravelNode.h index 780898a51..2481ac2cd 100644 --- a/src/Mgr/Travel/TravelNode.h +++ b/src/Mgr/Travel/TravelNode.h @@ -725,7 +725,7 @@ public: std::vector const& GetNodesInZone(uint32 zoneId) const; bool GetFullPath(TravelPlan& plan, WorldPosition botPos, - uint32 botZoneId, WorldPosition destination); + uint32 botZoneId, WorldPosition destination, Unit* bot = nullptr); // Resolve A* route between two world positions (returns node vector) std::vector ResolveRoute(WorldPosition startPos, diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index f3365628c..117cec12a 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -98,6 +98,7 @@ bool PlayerbotAIConfig::Initialize() tooCloseDistance = sConfigMgr->GetOption("AiPlayerbot.TooCloseDistance", 5.0f); meleeDistance = sConfigMgr->GetOption("AiPlayerbot.MeleeDistance", 0.75f); followDistance = sConfigMgr->GetOption("AiPlayerbot.FollowDistance", 1.5f); + walkDistance = sConfigMgr->GetOption("AiPlayerbot.WalkDistance", 5.0f); whisperDistance = sConfigMgr->GetOption("AiPlayerbot.WhisperDistance", 6000.0f); contactDistance = sConfigMgr->GetOption("AiPlayerbot.ContactDistance", 0.45f); aoeRadius = sConfigMgr->GetOption("AiPlayerbot.AoeRadius", 10.0f); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 63209da3b..ba7489af6 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -89,7 +89,7 @@ public: bool dynamicReactDelay; float sightDistance, spellDistance, reactDistance, grindDistance, lootDistance, shootDistance, fleeDistance, tooCloseDistance, meleeDistance, followDistance, whisperDistance, contactDistance, aoeRadius, rpgDistance, - targetPosRecalcDistance, farDistance, healDistance, aggroDistance; + targetPosRecalcDistance, farDistance, healDistance, aggroDistance, walkDistance; uint32 criticalHealth, lowHealth, mediumHealth, almostFullHealth; uint32 lowMana, mediumMana, highMana; bool autoSaveMana;