From f32ebb8f6b692a7b111f1c44a14f59893e85b4d4 Mon Sep 17 00:00:00 2001 From: bash Date: Sat, 30 May 2026 23:28:02 +0200 Subject: [PATCH] feat(Core/Travel): Port TravelPath::ClipPath; call from MoveFarTo, drop inline clip --- src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp | 44 +++---------- src/Mgr/Travel/TravelNode.cpp | 69 ++++++++++++++++++++ src/Mgr/Travel/TravelNode.h | 7 ++ 3 files changed, 84 insertions(+), 36 deletions(-) diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp index d52cae8c9..51896c484 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp @@ -125,6 +125,12 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) if (onTransport) return false; + // ClipPath — truncate at first hostile creature in range / non-walkable + // hop / drifted past reactDistance / > 125 sqDist jump. + path.ClipPath(botAI, bot, false); + if (path.empty()) + return false; + // Walk dispatch. std::vector const& pts = path.getPointPath(); Movement::PointsArray points; @@ -164,42 +170,8 @@ bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest, for (auto& pt : points) bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z); - // ClipPath — truncate path at first hostile creature within its - // own attack range. Skipped while in combat or dead. - 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; - if (!unit->IsWithinLOSInMap(bot)) - continue; - clipAt = i; - break; - } - } - if (clipAt < points.size() && clipAt + 1 < points.size()) - points.erase(points.begin() + clipAt + 1, points.end()); - } - } + // ClipPath now runs at MoveFarTo level on the TravelPath before the + // points array is built. No per-dispatch clip here. if (points.size() < 2) return false; diff --git a/src/Mgr/Travel/TravelNode.cpp b/src/Mgr/Travel/TravelNode.cpp index 9de265deb..3d887758c 100644 --- a/src/Mgr/Travel/TravelNode.cpp +++ b/src/Mgr/Travel/TravelNode.cpp @@ -956,6 +956,75 @@ bool TravelPath::UpcommingSpecialMovement(WorldPosition startPos, return false; } +void TravelPath::ClipPath(PlayerbotAI* ai, Unit* mover, bool ignoreEnemyTargets) +{ + auto startP = getNextPoint(WorldPosition(mover), 0.0f, false); + cutTo(*startP, false); + + if (startP == fullPath.end()) + return; + + GuidVector targets; + Player* bot = ai ? ai->GetBot() : nullptr; + if (bot && ai->GetState() != BOT_STATE_COMBAT && !bot->isDead() && !ignoreEnemyTargets) + targets = AI_VALUE(GuidVector, "possible targets"); + + auto endP = fullPath.end(); + auto prevP = fullPath.begin(); + float const reactSq = sPlayerbotAIConfig.reactDistance * sPlayerbotAIConfig.reactDistance; + + for (auto p = fullPath.begin(); p != fullPath.end(); ++p) + { + // Hostile-target check: stop before walking into a mob that + // would aggro. Level-capped (mover->level + 5) so over-level + // mobs we'd avoid anyway are ignored. + for (ObjectGuid const& targetGuid : targets) + { + if (!targetGuid.IsCreature()) + continue; + Unit* unit = ai->GetUnit(targetGuid); + if (!unit || unit->isDead()) + continue; + if (unit->GetLevel() > mover->GetLevel() + 5) + continue; + Creature* cre = unit->ToCreature(); + if (!cre) + continue; + float const range = cre->GetAttackDistance(mover); + if (WorldPosition(unit).sqDistance(p->point) > range * range) + continue; + if (!unit->IsHostileTo(mover) || !unit->IsWithinLOSInMap(mover)) + continue; + + endP = p; + break; + } + if (endP != fullPath.end()) + break; + + // Reject paths that drift past reactDistance from the start — + // a sign the path looped or wandered. + if (p->point.sqDistance(fullPath.begin()->point) > reactSq) + endP = p; + // Non-walkable hop in the middle (portal/transport/etc.) terminates. + else if (!p->isWalkable()) + endP = p; + // Gap between adjacent points > ~11y (sqDist 125) — likely bad data. + else if (p->point.sqDistance(prevP->point) > 125.0f) + endP = prevP; + + if (endP != fullPath.end()) + break; + + prevP = p; + } + + if (endP == fullPath.end()) + return; + + fullPath.erase(std::next(endP), fullPath.end()); +} + bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist, Unit* bot) { if (GetPath().empty()) diff --git a/src/Mgr/Travel/TravelNode.h b/src/Mgr/Travel/TravelNode.h index 09634859b..f6ccf1d72 100644 --- a/src/Mgr/Travel/TravelNode.h +++ b/src/Mgr/Travel/TravelNode.h @@ -507,6 +507,13 @@ public: // dispatches the matching special-movement handler on the new head. bool UpcommingSpecialMovement(WorldPosition startPos, float maxDist, bool onTransport); + // Truncate the path at the first waypoint that would put the bot in + // range of a hostile creature (within attack range, in LOS, level-cap + // sane), at a non-walkable hop, after drifting beyond reactDistance + // from the start, or across a > 125-sqDist jump. Set ignoreEnemyTargets + // to suppress the hostile-target check (used by combat repositioning). + void ClipPath(PlayerbotAI* ai, Unit* mover, bool ignoreEnemyTargets = false); + // Reject paths the navmesh accepts but a player can't walk: // 2-point shortcut over 5y, or > 10y vertical drop with slope steeper than 2:1. static bool IsPathCheating(std::vector const& path,