improving RPG traveland minimize wierd path selections but still happen

This commit is contained in:
bash 2026-04-10 12:49:49 +02:00
parent cd16f6baf1
commit 03db0c34b2
4 changed files with 146 additions and 27 deletions

View File

@ -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<int32>(location.GetPositionX()) * 73856093 ^
static_cast<int32>(location.GetPositionY()) * 19349663;
uint32 seed = botLow ^ static_cast<uint32>(destSeed);
float angle = 2.0f * static_cast<float>(M_PI) * static_cast<float>(seed % 1000) / 1000.0f;
float mod = 0.5f + static_cast<float>((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;

View File

@ -151,7 +151,14 @@ bool NewRpgGoGrindAction::Execute(Event /*event*/)
if (SearchQuestGiverAndAcceptOrReward())
return true;
if (auto* data = std::get_if<NewRpgInfo::GoGrind>(&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<NewRpgInfo::GoCamp>(&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

View File

@ -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<float>(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<float>(M_PI);
float dx = x + distance * cos(angle);
float dy = y + distance * sin(angle);

View File

@ -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