From 03db0c34b2386ea99195c6d93576f5413ba84568 Mon Sep 17 00:00:00 2001 From: bash Date: Fri, 10 Apr 2026 12:49:49 +0200 Subject: [PATCH] improving RPG traveland minimize wierd path selections but still happen --- .../Base/Actions/MoveToTravelTargetAction.cpp | 17 ++- src/Ai/World/Rpg/Action/NewRpgAction.cpp | 43 +++++++- src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp | 104 +++++++++++++++--- src/Ai/World/Rpg/Action/NewRpgBaseAction.h | 9 +- 4 files changed, 146 insertions(+), 27 deletions(-) diff --git a/src/Ai/Base/Actions/MoveToTravelTargetAction.cpp b/src/Ai/Base/Actions/MoveToTravelTargetAction.cpp index f238135d9..37cf064d7 100644 --- a/src/Ai/Base/Actions/MoveToTravelTargetAction.cpp +++ b/src/Ai/Base/Actions/MoveToTravelTargetAction.cpp @@ -74,8 +74,18 @@ bool MoveToTravelTargetAction::Execute(Event /*event*/) float maxDistance = target->getDestination()->getRadiusMin(); - // Evenly distribute around the target. - float angle = 2 * M_PI * urand(0, 100) / 100.0; + // Spread bots around the target but keep the offset stable per + // (bot, destination) pair. Previously the angle and radius were + // re-rolled every time the action re-entered (i.e. every tick the + // bot wasn't already moving), which made bots oscillate between + // two random points around the same quest POI instead of + // committing to one approach. + uint32 botLow = bot->GetGUID().GetCounter(); + int32 destSeed = static_cast(location.GetPositionX()) * 73856093 ^ + static_cast(location.GetPositionY()) * 19349663; + uint32 seed = botLow ^ static_cast(destSeed); + float angle = 2.0f * static_cast(M_PI) * static_cast(seed % 1000) / 1000.0f; + float mod = 0.5f + static_cast((seed / 1000) % 1000) / 2000.0f; // [0.5, 1.0] if (target->getMaxTravelTime() > target->getTimeLeft()) // The bot is late. Speed it up. { @@ -89,9 +99,6 @@ bool MoveToTravelTargetAction::Execute(Event /*event*/) float z = location.GetPositionZ(); float mapId = location.GetMapId(); - // Move between 0.5 and 1.0 times the maxDistance. - float mod = frand(50.f, 100.f) / 100.0f; - x += cos(angle) * maxDistance * mod; y += sin(angle) * maxDistance * mod; diff --git a/src/Ai/World/Rpg/Action/NewRpgAction.cpp b/src/Ai/World/Rpg/Action/NewRpgAction.cpp index 58846b949..ddd1240da 100644 --- a/src/Ai/World/Rpg/Action/NewRpgAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgAction.cpp @@ -151,7 +151,14 @@ bool NewRpgGoGrindAction::Execute(Event /*event*/) if (SearchQuestGiverAndAcceptOrReward()) return true; if (auto* data = std::get_if(&botAI->rpgInfo.data)) - return MoveFarTo(data->pos); + { + if (MoveFarTo(data->pos)) + return true; + // Small nudge so the next tick's MoveFarTo starts from a + // slightly different position. Kept small so it doesn't look + // like the bot is abandoning its destination. + return MoveRandomNear(10.0f); + } return false; } @@ -162,7 +169,11 @@ bool NewRpgGoCampAction::Execute(Event /*event*/) return true; if (auto* data = std::get_if(&botAI->rpgInfo.data)) - return MoveFarTo(data->pos); + { + if (MoveFarTo(data->pos)) + return true; + return MoveRandomNear(10.0f); + } return false; } @@ -215,7 +226,14 @@ bool NewRpgWanderNpcAction::Execute(Event /*event*/) data.lastReach = 0; } else - return MoveWorldObjectTo(data.npcOrGo); + { + if (MoveWorldObjectTo(data.npcOrGo)) + return true; + // NPC pathing failed (random offset in a wall, mmap hiccup, etc). + // Take a small random step so the next tick retries from a + // different spot instead of staring at the NPC from afar. + return MoveRandomNear(15.0f); + } return true; } @@ -305,7 +323,12 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data) if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI) { - return MoveFarTo(data.pos); + if (MoveFarTo(data.pos)) + return true; + // Long-range sampler couldn't land a candidate — nudge the + // bot a short distance so the next tick retries from a + // different position instead of sitting idle. + return MoveRandomNear(10.0f); } // Now we are near the quest objective // kill mobs and looting quest should be done automatically by grind strategy @@ -352,7 +375,11 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data) return true; } - return MoveRandomNear(20.0f); + // At the POI: keep the bot actively placed but avoid large + // random 20yd hops that look like pacing back and forth. A small + // ~8yd wander reads as the bot looking around while grind/loot + // strategies do their work. + return MoveRandomNear(8.0f); } bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data) @@ -392,7 +419,11 @@ bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data) return false; if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI) - return MoveFarTo(data.pos); + { + if (MoveFarTo(data.pos)) + return true; + return MoveRandomNear(10.0f); + } // Now we are near the qoi of reward // the quest should be rewarded by SearchQuestGiverAndAcceptOrReward diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp index b5156d6c1..986b2f0f5 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp @@ -46,17 +46,51 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) return false; } + // Let previously committed movement finish before recomputing. + // + // MoveTo internally caps its stored delay at maxWaitForMove + // (default 5s), but a long path (200+ yd routed around a + // mountain) takes 30+ seconds to walk. After 5s + // IsWaitingForLastMove returns false and MoveFarTo re-enters. + // Without this gate, DoMovePoint would call mm->Clear() and + // reissue MovePoint from the new bot position — and from a new + // position mmap's partial-path endpoint often differs, so the + // bot gets clobbered mid-walk and ends up oscillating (e.g. + // cave entrance -> inside cave -> cave entrance -> mountain + // base -> cave entrance...) around an unreachable destination. + // + // If the bot is still actively walking toward its last + // committed point on the same map, just let the current spline + // finish. The stuck counter below continues to track real + // progress toward dest and triggers teleport recovery if the + // committed paths genuinely aren't closing the gap. + { + LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement"); + if (bot->isMoving() && lastMove.lastMoveToMapId == bot->GetMapId()) + { + float remaining = bot->GetExactDist(lastMove.lastMoveToX, lastMove.lastMoveToY, lastMove.lastMoveToZ); + if (remaining > 10.0f) + return true; + } + } + // stuck check float disToDest = bot->GetDistance(dest); - if (disToDest + 1.0f < botAI->rpgInfo.nearestMoveFarDis) + // Require a meaningful improvement (5yd) to reset the stuck counter. + // The old 1yd threshold was small enough that bots oscillating back + // and forth around an obstacle would keep "making progress" forever + // and never trigger the teleport recovery below. + if (disToDest + 5.0f < botAI->rpgInfo.nearestMoveFarDis) { botAI->rpgInfo.nearestMoveFarDis = disToDest; botAI->rpgInfo.stuckTs = getMSTime(); botAI->rpgInfo.stuckAttempts = 0; } - else if (++botAI->rpgInfo.stuckAttempts >= 10 && GetMSTimeDiffToNow(botAI->rpgInfo.stuckTs) >= stuckTime) + else if (++botAI->rpgInfo.stuckAttempts >= 5 && GetMSTimeDiffToNow(botAI->rpgInfo.stuckTs) >= stuckTime) { - // Unfortunately we've been stuck here for over 5 mins, fallback to teleporting directly to the destination + // No meaningful progress toward dest for `stuckTime`: fall + // back to teleporting directly so the bot can get on with + // its RPG objective instead of oscillating indefinitely. botAI->rpgInfo.stuckTs = getMSTime(); botAI->rpgInfo.stuckAttempts = 0; const AreaTableEntry* entry = sAreaTableStore.LookupEntry(bot->GetZoneId()); @@ -78,26 +112,62 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) false, true); } + const uint32 typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY; + + // Primary strategy: ask mmap for a route to the TRUE destination. + // If mmap can reach it directly (PATHFIND_NORMAL) or partially + // (PATHFIND_INCOMPLETE — destinations beyond the smooth-path cap + // of ~296 yards, or where local geometry blocks the final step), + // walk to the furthest reachable waypoint mmap computed. This + // lets bots follow the real route around obstacles (mountains, + // cave walls, cliffs) instead of trying to cut straight through. + // The spline system walks the whole returned path smoothly, so + // subsequent ticks early-out via IsWaitingForLastMove and no + // further PathGenerator calls fire until the bot arrives. + { + PathGenerator path(bot); + path.CalculatePath(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); + PathType type = path.GetPathType(); + bool canReach = !(type & (~typeOk)); + if (canReach) + { + const G3D::Vector3& endPos = path.GetActualEndPosition(); + // Only commit if the mmap endpoint actually makes progress + // toward the destination. For pathological INCOMPLETE + // results (e.g. disconnected polys that still report + // INCOMPLETE) the endpoint can land right under the bot; + // fall through to cone sampling in that case. + float endDistToDest = dest.GetExactDist(endPos.x, endPos.y, endPos.z); + if (endDistToDest + 5.0f < disToDest) + { + return MoveTo(bot->GetMapId(), endPos.x, endPos.y, endPos.z, false, false, false, true); + } + } + } + + // Fallback: mmap couldn't route to the destination. Sample the + // forward cone for a reachable stepping stone so the bot keeps + // moving and can try again from a new vantage point. Cap at 2 + // samples — we already spent one PathGenerator call above and at + // 3000 bots every extra CalculatePath matters. float minDelta = M_PI; const float x = bot->GetPositionX(); const float y = bot->GetPositionY(); const float z = bot->GetPositionZ(); + const float baseAngle = bot->GetAngle(&dest); float rx, ry, rz; bool found = false; - int attempt = 3; - while (attempt--) + for (int attempt = 0; attempt < 2; ++attempt) { - float angle = bot->GetAngle(&dest); - float delta = urand(1, 100) <= 75 ? (rand_norm() - 0.5) * M_PI * 0.5 : (rand_norm() - 0.5) * M_PI * 2; - angle += delta; - float dis = rand_norm() * pathFinderDis; - float dx = x + cos(angle) * dis; - float dy = y + sin(angle) * dis; + float delta = (rand_norm() - 0.5f) * static_cast(M_PI); // ±π/2, forward cone + float sampleDis = (0.5f + rand_norm() * 0.5f) * pathFinderDis; + float angle = baseAngle + delta; + float dx = x + cos(angle) * sampleDis; + float dy = y + sin(angle) * sampleDis; float dz = z + 0.5f; PathGenerator path(bot); path.CalculatePath(dx, dy, dz); PathType type = path.GetPathType(); - uint32 typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY; bool canReach = !(type & (~typeOk)); if (canReach && fabs(delta) <= minDelta) @@ -159,14 +229,18 @@ bool NewRpgBaseAction::MoveRandomNear(float moveStep, MovementPriority priority) return false; } - float distance = rand_norm() * moveStep; Map* map = bot->GetMap(); const float x = bot->GetPositionX(); const float y = bot->GetPositionY(); const float z = bot->GetPositionZ(); - int attempts = 1; - while (attempts--) + // Previously: attempts = 1. A single random sample often landed in + // water / blocked geometry / unreachable poly, the function returned + // false, and the caller had no fallback — bot stood still. Retry a + // handful of times with a fresh distance each loop so a bad roll + // doesn't lock the bot in place. + for (int attempt = 0; attempt < 8; ++attempt) { + float distance = (0.4f + rand_norm() * 0.6f) * moveStep; float angle = (float)rand_norm() * 2 * static_cast(M_PI); float dx = x + distance * cos(angle); float dy = y + distance * sin(angle); diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h index 9cd939eb7..f17891ffc 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h @@ -61,7 +61,14 @@ protected: protected: /* FOR MOVE FAR */ const float pathFinderDis = 70.0f; - const uint32 stuckTime = 5 * 60 * 1000; + // Time without real progress toward dest before MoveFarTo + // falls back to teleport recovery. Kept short enough that a + // bot truly oscillating around an unreachable destination + // (mmap returning non-progressing partial paths, or NOPATH + + // cone fallback wandering) doesn't spin for 5 minutes before + // the teleport fires, but long enough that a genuine long + // walk that is slowly making progress never triggers it. + const uint32 stuckTime = 90 * 1000; }; #endif