mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
Compare commits
No commits in common. "eec29230766da3e53043e26c2df0d1145f158990" and "e92af1cc0631619e157ef01801f78a41d7414e97" have entirely different histories.
eec2923076
...
e92af1cc06
@ -62,9 +62,7 @@ void MovementAction::EmitDebugMove(char const* method, char const* generator, fl
|
||||
|
||||
NewRpgInfo& info = botAI->rpgInfo;
|
||||
NewRpgStatus status = info.GetStatus();
|
||||
bool const inCombat = botAI->GetState() == BOT_STATE_COMBAT;
|
||||
char const* statusName =
|
||||
inCombat ? "combat" :
|
||||
status == RPG_IDLE ? "idle" :
|
||||
status == RPG_GO_GRIND ? "go-grind" :
|
||||
status == RPG_GO_CAMP ? "go-camp" :
|
||||
@ -75,22 +73,14 @@ void MovementAction::EmitDebugMove(char const* method, char const* generator, fl
|
||||
status == RPG_TRAVEL_FLIGHT ? "travel-flight" :
|
||||
status == RPG_OUTDOOR_PVP ? "outdoor-pvp" : "?";
|
||||
|
||||
// Resolve a human-readable target name. In combat, the bot is
|
||||
// actively engaging an enemy that is unrelated to the RPG state's
|
||||
// target — show that enemy instead of the now-stale RPG goal.
|
||||
// Out of combat, fall back to the RPG context: quest objective,
|
||||
// wander NPC, flight master, etc. Names are far more useful than
|
||||
// coordinates; loc=(x,y,z) only when nothing nameable applies.
|
||||
// Resolve a human-readable target name from the RPG context. When
|
||||
// we can name the target (quest objective, wander NPC, flight
|
||||
// master, travel-node hop, etc.), it replaces the loc=(x,y,z)
|
||||
// field — names are far more useful than coordinates. When no
|
||||
// target can be named (combat moves, follow, flee, ad-hoc), we
|
||||
// fall through to loc=(x,y,z).
|
||||
std::string targetName;
|
||||
if (inCombat)
|
||||
{
|
||||
Unit* current = *botAI->GetAiObjectContext()->GetValue<Unit*>("current target");
|
||||
Unit* enemyPlayer = *botAI->GetAiObjectContext()->GetValue<Unit*>("enemy player target");
|
||||
Unit* enemy = current ? current : enemyPlayer;
|
||||
if (enemy)
|
||||
targetName = std::string("vs:") + enemy->GetName();
|
||||
}
|
||||
else switch (status)
|
||||
switch (status)
|
||||
{
|
||||
case RPG_DO_QUEST:
|
||||
if (auto* data = std::get_if<NewRpgInfo::DoQuest>(&info.data))
|
||||
@ -174,11 +164,6 @@ void MovementAction::EmitDebugMove(char const* method, char const* generator, fl
|
||||
<< " | " << statusName
|
||||
<< " | " << std::fixed << std::setprecision(2) << dis << " yard"
|
||||
<< " | " << (targetName.empty() ? "-" : targetName.c_str());
|
||||
// Surface the RPG MoveFarTo retry counter so when bots get stuck
|
||||
// it's obvious from the whisper alone (retry=N/MAX) — and the
|
||||
// "give-up" event emitters below show retry=MAX/MAX explicitly.
|
||||
if (info.moveRetryCount > 0)
|
||||
out << " | retry=" << uint32(info.moveRetryCount) << "/" << uint32(NewRpgInfo::MAX_MOVE_RETRIES);
|
||||
if (extra && *extra)
|
||||
out << " | " << extra;
|
||||
botAI->TellMasterNoFacing(out);
|
||||
|
||||
@ -155,22 +155,11 @@ bool NewRpgGoGrindAction::Execute(Event /*event*/)
|
||||
if (auto* data = std::get_if<NewRpgInfo::GoGrind>(&botAI->rpgInfo.data))
|
||||
{
|
||||
if (MoveFarTo(data->pos))
|
||||
{
|
||||
botAI->rpgInfo.moveRetryCount = 0;
|
||||
return true;
|
||||
}
|
||||
// Reference pattern (TravelTarget retry counter): count
|
||||
// consecutive MoveFarTo failures, give up after N tries by
|
||||
// transitioning out of the stuck state instead of nudging in
|
||||
// place. Idle lets the status picker rotate to a new state.
|
||||
if (++botAI->rpgInfo.moveRetryCount >= NewRpgInfo::MAX_MOVE_RETRIES)
|
||||
{
|
||||
EmitDebugMove("MoveFar", "give-up",
|
||||
data->pos.GetPositionX(), data->pos.GetPositionY(), data->pos.GetPositionZ(),
|
||||
"idle");
|
||||
botAI->rpgInfo.ChangeToIdle();
|
||||
}
|
||||
return true; // consume tick, no nudge
|
||||
// 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;
|
||||
@ -184,18 +173,8 @@ bool NewRpgGoCampAction::Execute(Event /*event*/)
|
||||
if (auto* data = std::get_if<NewRpgInfo::GoCamp>(&botAI->rpgInfo.data))
|
||||
{
|
||||
if (MoveFarTo(data->pos))
|
||||
{
|
||||
botAI->rpgInfo.moveRetryCount = 0;
|
||||
return true;
|
||||
}
|
||||
if (++botAI->rpgInfo.moveRetryCount >= NewRpgInfo::MAX_MOVE_RETRIES)
|
||||
{
|
||||
EmitDebugMove("MoveFar", "give-up",
|
||||
data->pos.GetPositionX(), data->pos.GetPositionY(), data->pos.GetPositionZ(),
|
||||
"idle");
|
||||
botAI->rpgInfo.ChangeToIdle();
|
||||
}
|
||||
return true;
|
||||
return MoveRandomNear(10.0f);
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -251,23 +230,11 @@ bool NewRpgWanderNpcAction::Execute(Event /*event*/)
|
||||
else
|
||||
{
|
||||
if (MoveWorldObjectTo(data.npcOrGo))
|
||||
{
|
||||
botAI->rpgInfo.moveRetryCount = 0;
|
||||
return true;
|
||||
}
|
||||
// Retry counter (reference pattern): give up after N failures
|
||||
// by clearing the picked NPC so next tick picks a different
|
||||
// one. No nudge — stand still until retry.
|
||||
if (++botAI->rpgInfo.moveRetryCount >= NewRpgInfo::MAX_MOVE_RETRIES)
|
||||
{
|
||||
EmitDebugMove("MoveFar", "give-up",
|
||||
bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(),
|
||||
"drop-npc");
|
||||
data.npcOrGo = ObjectGuid();
|
||||
data.lastReach = 0;
|
||||
botAI->rpgInfo.moveRetryCount = 0;
|
||||
}
|
||||
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;
|
||||
@ -300,23 +267,14 @@ bool NewRpgDoQuestAction::Execute(Event /*event*/)
|
||||
|
||||
bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
|
||||
{
|
||||
uint32 const questId = data.questId;
|
||||
|
||||
// === Spawn-index pipeline ===
|
||||
// Reference (cmangos) per-spawn pattern: walk to specific known
|
||||
// spawns of the current objective one by one, advance through the
|
||||
// candidate list on per-spawn timeout, refresh the list when the
|
||||
// objective makes progress (so the list reflects what's still
|
||||
// needed). No POI cluster roam, no random nudging.
|
||||
|
||||
// 1. Detect objective completion. If the current objective is done,
|
||||
// drop the cached spawn list so we re-fetch for the next
|
||||
// incomplete objective on this tick.
|
||||
if (!data.candidateSpawns.empty())
|
||||
uint32 questId = data.questId;
|
||||
if (data.pos != WorldPosition())
|
||||
{
|
||||
int32 const currentObjective = data.objectiveIdx;
|
||||
/// @TODO: extract to a new function
|
||||
int32 currentObjective = data.objectiveIdx;
|
||||
// check if the objective has completed
|
||||
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
|
||||
QuestStatusData const& q_status = bot->getQuestStatusMap().at(questId);
|
||||
const QuestStatusData& q_status = bot->getQuestStatusMap().at(questId);
|
||||
bool completed = true;
|
||||
if (currentObjective < QUEST_OBJECTIVES_COUNT)
|
||||
{
|
||||
@ -329,109 +287,89 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
|
||||
quest->RequiredItemCount[currentObjective - QUEST_OBJECTIVES_COUNT])
|
||||
completed = false;
|
||||
}
|
||||
// the current objective is completed, clear and find a new objective later
|
||||
if (completed)
|
||||
{
|
||||
data.candidateSpawns.clear();
|
||||
data.currentSpawnIdx = 0;
|
||||
data.lastReachPOI = 0;
|
||||
data.pos = WorldPosition();
|
||||
data.objectiveIdx = 0;
|
||||
data.pursuedLootGO.Clear();
|
||||
data.pursuedUseGO.Clear();
|
||||
data.pursuedUseTarget.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fetch spawn candidates if we don't have any. Abandon the
|
||||
// quest if no spawns are indexed on the bot's current map (the
|
||||
// quest is for another zone or our index is missing them).
|
||||
if (data.candidateSpawns.empty())
|
||||
if (data.pos == WorldPosition())
|
||||
{
|
||||
std::vector<WorldPosition> spawns;
|
||||
int32 objectiveIdx = 0;
|
||||
if (!FetchQuestSpawnsForObjective(questId, spawns, objectiveIdx))
|
||||
std::vector<POIInfo> poiInfo;
|
||||
if (!GetQuestPOIPosAndObjectiveIdx(questId, poiInfo))
|
||||
{
|
||||
botAI->lowPriorityQuest.insert(questId);
|
||||
botAI->rpgStatistic.questAbandoned++;
|
||||
LOG_DEBUG("playerbots", "[New RPG] {} abandoned quest {} — no spawns indexed",
|
||||
bot->GetName(), questId);
|
||||
// can't find a poi pos to go, stop doing quest for now
|
||||
botAI->rpgInfo.ChangeToIdle();
|
||||
return true;
|
||||
}
|
||||
data.candidateSpawns = std::move(spawns);
|
||||
data.currentSpawnIdx = 0;
|
||||
uint32 rndIdx = urand(0, poiInfo.size() - 1);
|
||||
G3D::Vector2 nearestPoi = poiInfo[rndIdx].pos;
|
||||
int32 objectiveIdx = poiInfo[rndIdx].objectiveIdx;
|
||||
|
||||
float dx = nearestPoi.x, dy = nearestPoi.y;
|
||||
|
||||
// z = MAX_HEIGHT as we do not know accurate z
|
||||
float dz = std::max(bot->GetMap()->GetHeight(dx, dy, MAX_HEIGHT), bot->GetMap()->GetWaterLevel(dx, dy));
|
||||
|
||||
// double check for GetQuestPOIPosAndObjectiveIdx
|
||||
if (dz == INVALID_HEIGHT || dz == VMAP_INVALID_HEIGHT_VALUE)
|
||||
return false;
|
||||
|
||||
WorldPosition pos(bot->GetMapId(), dx, dy, dz);
|
||||
data.lastReachPOI = 0;
|
||||
data.pos = pos;
|
||||
data.objectiveIdx = objectiveIdx;
|
||||
data.pursuedLootGO.Clear();
|
||||
data.pursuedUseGO.Clear();
|
||||
data.pursuedUseTarget.Clear();
|
||||
}
|
||||
|
||||
// 3. If we've exhausted the candidate list, abandon (the spawn
|
||||
// list was sorted by distance and we tried each).
|
||||
if (data.currentSpawnIdx >= data.candidateSpawns.size())
|
||||
{
|
||||
botAI->lowPriorityQuest.insert(questId);
|
||||
botAI->rpgStatistic.questAbandoned++;
|
||||
LOG_DEBUG("playerbots", "[New RPG] {} abandoned quest {} — exhausted all {} candidate spawns",
|
||||
bot->GetName(), questId, static_cast<uint32>(data.candidateSpawns.size()));
|
||||
botAI->rpgInfo.ChangeToIdle();
|
||||
return true;
|
||||
}
|
||||
|
||||
WorldPosition const& target = data.candidateSpawns[data.currentSpawnIdx];
|
||||
|
||||
// 4. Walk to the current target spawn. Yield to attack-anything
|
||||
// only if a quest mob for this specific objective is adjacent
|
||||
// (so we don't walk past the target we just spawned next to).
|
||||
if (bot->GetDistance(target) > 10.0f && !data.lastReachPOI)
|
||||
if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI)
|
||||
{
|
||||
// Yield to attack-anything ONLY if a mob needed by this exact
|
||||
// quest+objective is right next to us. The broad variant (any
|
||||
// quest in the log) yielded for every nearby mob and derailed
|
||||
// turn-ins / cross-zone travel through other quests' clusters.
|
||||
if (HasNearbyQuestMobForObjective(15.0f, data.questId, data.objectiveIdx))
|
||||
return false;
|
||||
|
||||
if (MoveFarTo(target))
|
||||
{
|
||||
botAI->rpgInfo.moveRetryCount = 0;
|
||||
return true;
|
||||
}
|
||||
// Retry counter: on N consecutive MoveFarTo failures, advance
|
||||
// to the next candidate spawn rather than sit on an unreachable
|
||||
// one. If that exhausts the list the abandon branch above
|
||||
// catches it next tick.
|
||||
if (++botAI->rpgInfo.moveRetryCount >= NewRpgInfo::MAX_MOVE_RETRIES)
|
||||
{
|
||||
std::ostringstream nx;
|
||||
nx << "next-spawn(" << (data.currentSpawnIdx + 1) << "/"
|
||||
<< data.candidateSpawns.size() << ")";
|
||||
EmitDebugMove("MoveFar", "give-up",
|
||||
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ(),
|
||||
nx.str().c_str());
|
||||
++data.currentSpawnIdx;
|
||||
data.lastReachPOI = 0;
|
||||
botAI->rpgInfo.moveRetryCount = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Note: previously yielded ~10%/tick when any hostile was
|
||||
// within 25y. That overrode the do-quest multiplier in
|
||||
// practice (combined with bots getting aggroed on the way,
|
||||
// which ALSO bypasses the multiplier via combat engine) and
|
||||
// bots ended up grinding their way to POIs instead of
|
||||
// travelling. Quest-mob exception above is kept so we don't
|
||||
// walk past a quest target while gathering. Anything else
|
||||
// hostile is the multiplier's job to throttle — and bots
|
||||
// that DO get aggroed switch to combat engine where the
|
||||
// class strategy handles it.
|
||||
|
||||
if (MoveFarTo(data.pos))
|
||||
return true;
|
||||
// sampler found nothing — nudge so next tick tries a new pos
|
||||
return MoveRandomNear(10.0f);
|
||||
}
|
||||
// Now we are near the quest objective
|
||||
// kill mobs and looting quest should be done automatically by grind strategy
|
||||
|
||||
// 5. At the spawn. Stamp arrival on first reach so the per-spawn
|
||||
// timeout below has a baseline.
|
||||
if (!data.lastReachPOI)
|
||||
{
|
||||
data.lastReachPOI = getMSTime();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 6. Per-spawn timeout. The reference's TravelTarget expires after
|
||||
// a configurable window; we use 30s — long enough to finish a
|
||||
// melee pull, short enough to advance off an empty/dead spawn.
|
||||
// On any progression since the list was fetched, refresh so we
|
||||
// re-sort by distance and pick the next nearest live spawn.
|
||||
constexpr uint32 perSpawnTimeoutMs = 30 * 1000;
|
||||
if (GetMSTimeDiffToNow(data.lastReachPOI) >= perSpawnTimeoutMs)
|
||||
// stayed at this POI for more than 5 minutes
|
||||
if (GetMSTimeDiffToNow(data.lastReachPOI) >= poiStayTime)
|
||||
{
|
||||
bool hasProgression = false;
|
||||
int32 const currentObjective = data.objectiveIdx;
|
||||
int32 currentObjective = data.objectiveIdx;
|
||||
// check if the objective has progression
|
||||
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
|
||||
QuestStatusData const& q_status = bot->getQuestStatusMap().at(questId);
|
||||
const QuestStatusData& q_status = bot->getQuestStatusMap().at(questId);
|
||||
if (currentObjective < QUEST_OBJECTIVES_COUNT)
|
||||
{
|
||||
if (q_status.CreatureOrGOCount[currentObjective] != 0 && quest->RequiredNpcOrGoCount[currentObjective])
|
||||
@ -443,27 +381,28 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
|
||||
quest->RequiredItemCount[currentObjective - QUEST_OBJECTIVES_COUNT])
|
||||
hasProgression = true;
|
||||
}
|
||||
if (hasProgression)
|
||||
if (!hasProgression)
|
||||
{
|
||||
// Refresh: re-fetch candidates so the list reflects what's
|
||||
// still needed and is sorted from the bot's new position.
|
||||
data.candidateSpawns.clear();
|
||||
data.currentSpawnIdx = 0;
|
||||
data.lastReachPOI = 0;
|
||||
// we has reach the poi for more than 5 mins but no progession
|
||||
// may not be able to complete this quest, marked as abandoned
|
||||
/// @TODO: It may be better to make lowPriorityQuest a global set shared by all bots (or saved in db)
|
||||
botAI->lowPriorityQuest.insert(questId);
|
||||
botAI->rpgStatistic.questAbandoned++;
|
||||
LOG_DEBUG("playerbots", "[New RPG] {} marked as abandoned quest {}", bot->GetName(), questId);
|
||||
botAI->rpgInfo.ChangeToIdle();
|
||||
return true;
|
||||
}
|
||||
// No progression at this spawn — advance to the next candidate.
|
||||
++data.currentSpawnIdx;
|
||||
// clear and select another poi later
|
||||
data.lastReachPOI = 0;
|
||||
data.pos = WorldPosition();
|
||||
data.objectiveIdx = 0;
|
||||
data.pursuedLootGO.Clear();
|
||||
data.pursuedUseGO.Clear();
|
||||
data.pursuedUseTarget.Clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 7. At spawn, within timeout: drive toward specific objectives.
|
||||
// Combat strategy engages adjacent quest mobs; loot/use
|
||||
// actions handle quest GOs and quest items.
|
||||
// at POI: drive toward specific objectives first
|
||||
if (TryUseQuestItem(data.pursuedUseGO, data.pursuedUseTarget))
|
||||
return true;
|
||||
if (TryLootQuestGO(data.pursuedLootGO))
|
||||
@ -471,9 +410,96 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
|
||||
if (TryUseQuestGO(data.pursuedUseGO))
|
||||
return true;
|
||||
|
||||
// Yield this tick to combat/grind. No POI roam, no MoveRandomNear:
|
||||
// bot stays at the spawn until either combat engages or the
|
||||
// per-spawn timeout expires.
|
||||
// gather quests: roam for spawns. kill quests: yield to grind.
|
||||
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
|
||||
if (quest)
|
||||
{
|
||||
int32 obj = data.objectiveIdx;
|
||||
bool isGatherObjective = false;
|
||||
if (obj < QUEST_OBJECTIVES_COUNT)
|
||||
{
|
||||
int32 entry = quest->RequiredNpcOrGo[obj];
|
||||
if (entry < 0) // GO objective
|
||||
isGatherObjective = true;
|
||||
if (entry == 0 && obj < QUEST_ITEM_OBJECTIVES_COUNT && quest->RequiredItemId[obj])
|
||||
isGatherObjective = true;
|
||||
}
|
||||
else if (obj < QUEST_OBJECTIVES_COUNT + QUEST_ITEM_OBJECTIVES_COUNT)
|
||||
{
|
||||
isGatherObjective = true;
|
||||
}
|
||||
// source-item quest: need to find the target to use it on
|
||||
if (quest->GetSrcItemId())
|
||||
isGatherObjective = true;
|
||||
|
||||
if (isGatherObjective)
|
||||
return MoveRandomNear(20.0f);
|
||||
}
|
||||
|
||||
// Kill-quest scout: at POI for 30s+ with no quest mob in sight
|
||||
// means this cluster is empty. Switch to a different POI candidate
|
||||
// (>50y away) if one exists; otherwise roam in place.
|
||||
constexpr uint32 scoutTimeoutMs = 30 * 1000;
|
||||
if (data.lastReachPOI && GetMSTimeDiffToNow(data.lastReachPOI) >= scoutTimeoutMs &&
|
||||
!HasNearbyQuestMob(30.0f))
|
||||
{
|
||||
std::vector<POIInfo> poiInfo;
|
||||
if (GetQuestPOIPosAndObjectiveIdx(questId, poiInfo))
|
||||
{
|
||||
std::vector<size_t> alternatives;
|
||||
for (size_t i = 0; i < poiInfo.size(); ++i)
|
||||
{
|
||||
float dx = poiInfo[i].pos.x - data.pos.GetPositionX();
|
||||
float dy = poiInfo[i].pos.y - data.pos.GetPositionY();
|
||||
if (dx * dx + dy * dy > 50.0f * 50.0f)
|
||||
alternatives.push_back(i);
|
||||
}
|
||||
if (!alternatives.empty())
|
||||
{
|
||||
size_t pickIdx = alternatives[urand(0, alternatives.size() - 1)];
|
||||
G3D::Vector2 newPoi = poiInfo[pickIdx].pos;
|
||||
float dz = std::max(bot->GetMap()->GetHeight(newPoi.x, newPoi.y, MAX_HEIGHT),
|
||||
bot->GetMap()->GetWaterLevel(newPoi.x, newPoi.y));
|
||||
if (dz != INVALID_HEIGHT && dz != VMAP_INVALID_HEIGHT_VALUE)
|
||||
{
|
||||
data.pos = WorldPosition(bot->GetMapId(), newPoi.x, newPoi.y, dz);
|
||||
data.objectiveIdx = poiInfo[pickIdx].objectiveIdx;
|
||||
data.lastReachPOI = 0;
|
||||
data.pursuedLootGO.Clear();
|
||||
data.pursuedUseGO.Clear();
|
||||
data.pursuedUseTarget.Clear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return MoveRandomNear(20.0f);
|
||||
}
|
||||
|
||||
// kill quest: walk toward the marker before handing off to grind.
|
||||
// lastReachPOI trips at ~10y so without this the bot fights on the
|
||||
// edge and never reaches the dense cluster. Skip if a quest mob is
|
||||
// in sight (might be the target) or a hostile is mid-pull.
|
||||
if (bot->GetDistance(data.pos) > 5.0f)
|
||||
{
|
||||
if (HasNearbyQuestMob(30.0f))
|
||||
return false;
|
||||
|
||||
GuidVector nearby = AI_VALUE(GuidVector, "possible targets");
|
||||
bool hostileClose = false;
|
||||
for (ObjectGuid guid : nearby)
|
||||
{
|
||||
Unit* u = botAI->GetUnit(guid);
|
||||
if (u && u->IsAlive() && bot->GetDistance(u) < 15.0f)
|
||||
{
|
||||
hostileClose = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hostileClose)
|
||||
return MoveFarTo(data.pos);
|
||||
}
|
||||
|
||||
// yield to grind
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -525,21 +551,8 @@ bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
|
||||
if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI)
|
||||
{
|
||||
if (MoveFarTo(data.pos))
|
||||
{
|
||||
botAI->rpgInfo.moveRetryCount = 0;
|
||||
return true;
|
||||
}
|
||||
// Retry counter (reference pattern): mark quest as abandoned
|
||||
// if turn-in POI is unreachable repeatedly so the bot doesn't
|
||||
// sit on a broken handler.
|
||||
if (++botAI->rpgInfo.moveRetryCount >= NewRpgInfo::MAX_MOVE_RETRIES)
|
||||
{
|
||||
EmitDebugMove("MoveFar", "give-up",
|
||||
data.pos.GetPositionX(), data.pos.GetPositionY(), data.pos.GetPositionZ(),
|
||||
"idle(turn-in)");
|
||||
botAI->rpgInfo.ChangeToIdle();
|
||||
}
|
||||
return true;
|
||||
return MoveRandomNear(10.0f);
|
||||
}
|
||||
|
||||
// Now we are near the qoi of reward
|
||||
|
||||
@ -20,7 +20,6 @@
|
||||
#include "Object.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "OutdoorPvPMgr.h"
|
||||
#include "QuestSpawnIndex.h"
|
||||
#include "ObjectDefines.h"
|
||||
#include "ObjectGuid.h"
|
||||
#include "ObjectMgr.h"
|
||||
@ -1369,89 +1368,6 @@ bool NewRpgBaseAction::GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NewRpgBaseAction::FetchQuestSpawnsForObjective(uint32 questId,
|
||||
std::vector<WorldPosition>& outSpawns,
|
||||
int32& outObjectiveIdx)
|
||||
{
|
||||
outSpawns.clear();
|
||||
|
||||
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
|
||||
if (!quest)
|
||||
return false;
|
||||
|
||||
auto qStatusIt = bot->getQuestStatusMap().find(questId);
|
||||
if (qStatusIt == bot->getQuestStatusMap().end())
|
||||
return false;
|
||||
QuestStatusData const& q_status = qStatusIt->second;
|
||||
|
||||
if (q_status.Status != QUEST_STATUS_INCOMPLETE)
|
||||
return false;
|
||||
|
||||
uint32 const botMapId = bot->GetMapId();
|
||||
|
||||
// Iterate creature/GO objectives first, then required-item drops.
|
||||
// The first one with at least one indexed spawn on the bot's map
|
||||
// wins. Subsequent objectives are picked next call after this one
|
||||
// completes or the spawn list is exhausted.
|
||||
auto tryFetch = [&](int32 entry, int32 idx) -> bool
|
||||
{
|
||||
if (!entry)
|
||||
return false;
|
||||
auto const& spawns = sQuestSpawnIndex->GetSpawns(botMapId, entry);
|
||||
if (spawns.empty())
|
||||
return false;
|
||||
outSpawns = spawns;
|
||||
outObjectiveIdx = idx;
|
||||
return true;
|
||||
};
|
||||
|
||||
for (int i = 0; i < QUEST_OBJECTIVES_COUNT; ++i)
|
||||
{
|
||||
int32 npcOrGo = quest->RequiredNpcOrGo[i];
|
||||
if (!npcOrGo)
|
||||
continue;
|
||||
if (q_status.CreatureOrGOCount[i] >= quest->RequiredNpcOrGoCount[i])
|
||||
continue; // objective complete
|
||||
if (tryFetch(npcOrGo, i))
|
||||
break;
|
||||
}
|
||||
|
||||
if (outSpawns.empty())
|
||||
{
|
||||
// Required-item drops: source creature is encoded on the
|
||||
// template (the loot-source mob). Use the creature spawn
|
||||
// index for each ItemDrop entry.
|
||||
for (int i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; ++i)
|
||||
{
|
||||
uint32 itemId = quest->RequiredItemId[i];
|
||||
if (!itemId)
|
||||
continue;
|
||||
if (q_status.ItemCount[i] >= quest->RequiredItemCount[i])
|
||||
continue;
|
||||
// ItemDrop entries are encoded as the drop-source creature
|
||||
// template id (positive). Fall back to scanning sObjectMgr
|
||||
// creature-loot if/when we need stricter sourcing; for now
|
||||
// this is best-effort.
|
||||
int32 dropFrom = static_cast<int32>(quest->ItemDrop[i]);
|
||||
if (dropFrom && tryFetch(dropFrom, QUEST_OBJECTIVES_COUNT + i))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (outSpawns.empty())
|
||||
return false;
|
||||
|
||||
// Sort by distance from the bot so currentSpawnIdx=0 is the
|
||||
// nearest. Reference's getNextDestination effectively does this
|
||||
// each pick.
|
||||
WorldPosition botPos(bot);
|
||||
std::sort(outSpawns.begin(), outSpawns.end(),
|
||||
[&botPos](WorldPosition const& a, WorldPosition const& b)
|
||||
{ return botPos.sqDistance(a) < botPos.sqDistance(b); });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot)
|
||||
{
|
||||
const std::vector<WorldLocation>& locs = sTravelMgr.GetLocsPerLevelCache(bot->GetLevel());
|
||||
|
||||
@ -76,16 +76,6 @@ protected:
|
||||
|
||||
protected:
|
||||
bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo>& poiInfo, bool toComplete = false);
|
||||
// Reference per-spawn destination pattern: pick the first
|
||||
// incomplete objective on `questId`, look up its spawns
|
||||
// (creature OR gameobject — RequiredNpcOrGo encodes both) on
|
||||
// the bot's current map, sort by distance from the bot, and
|
||||
// return them in `outSpawns` with the resolved `outObjectiveIdx`.
|
||||
// Returns false if no incomplete objective has spawns on the
|
||||
// current map.
|
||||
bool FetchQuestSpawnsForObjective(uint32 questId,
|
||||
std::vector<WorldPosition>& outSpawns,
|
||||
int32& outObjectiveIdx);
|
||||
static WorldPosition SelectRandomGrindPos(Player* bot);
|
||||
static WorldPosition SelectRandomCampPos(Player* bot);
|
||||
bool SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector<uint32>& path);
|
||||
|
||||
@ -77,7 +77,6 @@ void NewRpgInfo::Reset()
|
||||
{
|
||||
data = Idle{};
|
||||
startT = getMSTime();
|
||||
moveRetryCount = 0;
|
||||
}
|
||||
|
||||
NewRpgStatus NewRpgInfo::GetStatus()
|
||||
|
||||
@ -46,17 +46,7 @@ struct NewRpgInfo
|
||||
const Quest* quest{nullptr};
|
||||
uint32 questId{0};
|
||||
int32 objectiveIdx{0};
|
||||
// Turn-in POI (DoCompletedQuest). Kept POI-based since this is
|
||||
// the quest-giver location, not a mob spawn.
|
||||
WorldPosition pos{};
|
||||
// Reference (cmangos) per-spawn destination pattern for
|
||||
// incomplete objectives: candidate spawn positions sorted by
|
||||
// distance, walked one-by-one (current =
|
||||
// candidateSpawns[currentSpawnIdx]) instead of POI-wander.
|
||||
// Refreshed when the objective changes or the list is
|
||||
// exhausted.
|
||||
std::vector<WorldPosition> candidateSpawns;
|
||||
uint32 currentSpawnIdx{0};
|
||||
uint32 lastReachPOI{0};
|
||||
// committed target per objective type. stops zig-zagging in
|
||||
// dense spawn clusters when "nearest" would flip each tick.
|
||||
@ -88,14 +78,6 @@ struct NewRpgInfo
|
||||
|
||||
uint32 startT{0}; // start timestamp of the current status
|
||||
|
||||
// Counts consecutive MoveFarTo failures for the current state.
|
||||
// Reset on every status change (via Reset) and on every successful
|
||||
// MoveFarTo. When it crosses MAX_MOVE_RETRIES the failing action
|
||||
// gives up and transitions out of the current state instead of
|
||||
// sitting on a stuck objective forever.
|
||||
uint8 moveRetryCount{0};
|
||||
static constexpr uint8 MAX_MOVE_RETRIES = 10;
|
||||
|
||||
using RpgData = std::variant<
|
||||
Idle,
|
||||
GoGrind,
|
||||
|
||||
@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
|
||||
* and/or modify it under version 3 of the License, or (at your option), any later version.
|
||||
*/
|
||||
|
||||
#include "QuestSpawnIndex.h"
|
||||
|
||||
#include "CreatureData.h"
|
||||
#include "GameObjectData.h"
|
||||
#include "Log.h"
|
||||
#include "ObjectMgr.h"
|
||||
|
||||
QuestSpawnIndex* QuestSpawnIndex::instance()
|
||||
{
|
||||
static QuestSpawnIndex inst;
|
||||
return &inst;
|
||||
}
|
||||
|
||||
void QuestSpawnIndex::Init()
|
||||
{
|
||||
if (_initialized)
|
||||
return;
|
||||
|
||||
uint32 creatures = 0;
|
||||
uint32 gos = 0;
|
||||
|
||||
for (auto const& kv : sObjectMgr->GetAllCreatureData())
|
||||
{
|
||||
CreatureData const& cd = kv.second;
|
||||
if (!cd.id1)
|
||||
continue;
|
||||
Key const key{cd.mapid, static_cast<int32>(cd.id1)};
|
||||
_index[key].emplace_back(cd.mapid, cd.posX, cd.posY, cd.posZ, cd.orientation);
|
||||
++creatures;
|
||||
}
|
||||
|
||||
for (auto const& kv : sObjectMgr->GetAllGOData())
|
||||
{
|
||||
GameObjectData const& gd = kv.second;
|
||||
if (!gd.id)
|
||||
continue;
|
||||
// Negative entry encodes GO (matches Quest::RequiredNpcOrGo
|
||||
// convention used by the do-quest action callers).
|
||||
Key const key{gd.mapid, -static_cast<int32>(gd.id)};
|
||||
_index[key].emplace_back(gd.mapid, gd.posX, gd.posY, gd.posZ, gd.orientation);
|
||||
++gos;
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
|
||||
LOG_INFO("playerbots",
|
||||
">> QuestSpawnIndex: indexed {} creature spawns + {} GO spawns ({} unique keys).",
|
||||
creatures, gos, static_cast<uint32>(_index.size()));
|
||||
}
|
||||
|
||||
std::vector<WorldPosition> const& QuestSpawnIndex::GetSpawns(uint32 mapId, int32 entry) const
|
||||
{
|
||||
auto it = _index.find(Key{mapId, entry});
|
||||
return (it != _index.end()) ? it->second : _empty;
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
|
||||
* and/or modify it under version 3 of the License, or (at your option), any later version.
|
||||
*/
|
||||
|
||||
#ifndef _PLAYERBOT_QUESTSPAWNINDEX_H
|
||||
#define _PLAYERBOT_QUESTSPAWNINDEX_H
|
||||
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "Define.h"
|
||||
#include "TravelMgr.h"
|
||||
|
||||
// Maps `(mapId, RequiredNpcOrGo-style entry)` → list of spawn
|
||||
// positions for that template on that map. The entry convention
|
||||
// matches Quest::RequiredNpcOrGo: positive value = creature template
|
||||
// id, negative value = gameobject template id (use the absolute
|
||||
// value to look up in gameobject_template).
|
||||
//
|
||||
// Built once at module startup by scanning sObjectMgr's
|
||||
// CreatureDataStore + GameObjectDataStore. Read-only thereafter.
|
||||
//
|
||||
// Used by the RPG do-quest action to walk directly to specific known
|
||||
// spawns of a quest objective instead of wandering inside a POI
|
||||
// cluster. Mirrors the reference's TravelMgr per-spawn destination
|
||||
// indexing.
|
||||
class QuestSpawnIndex
|
||||
{
|
||||
public:
|
||||
static QuestSpawnIndex* instance();
|
||||
|
||||
// Build the index from sObjectMgr's spawn data. Safe to call
|
||||
// multiple times — second+ calls are no-ops. Call once after
|
||||
// sObjectMgr->LoadCreatures / LoadGameObjects have populated
|
||||
// their stores.
|
||||
void Init();
|
||||
|
||||
// Returns spawns of `entry` on `mapId`. Empty list if none
|
||||
// indexed. Stable reference for the lifetime of the index.
|
||||
std::vector<WorldPosition> const& GetSpawns(uint32 mapId, int32 entry) const;
|
||||
|
||||
[[nodiscard]] bool IsInitialized() const { return _initialized; }
|
||||
|
||||
private:
|
||||
QuestSpawnIndex() = default;
|
||||
|
||||
bool _initialized{false};
|
||||
|
||||
struct Key
|
||||
{
|
||||
uint32 mapId;
|
||||
int32 entry;
|
||||
bool operator==(Key const& o) const { return mapId == o.mapId && entry == o.entry; }
|
||||
};
|
||||
struct KeyHash
|
||||
{
|
||||
std::size_t operator()(Key const& k) const noexcept
|
||||
{
|
||||
return (std::size_t(k.mapId) << 32) ^ std::size_t(uint32(k.entry));
|
||||
}
|
||||
};
|
||||
|
||||
std::unordered_map<Key, std::vector<WorldPosition>, KeyHash> _index;
|
||||
std::vector<WorldPosition> _empty;
|
||||
};
|
||||
|
||||
#define sQuestSpawnIndex QuestSpawnIndex::instance()
|
||||
|
||||
#endif
|
||||
@ -20,7 +20,6 @@
|
||||
#include "ModelIgnoreFlags.h"
|
||||
#include "PathGenerator.h"
|
||||
#include "Playerbots.h"
|
||||
#include "QuestSpawnIndex.h"
|
||||
#include "RaceMgr.h"
|
||||
#include "TransportMgr.h"
|
||||
#include "VMapFactory.h"
|
||||
@ -3585,7 +3584,6 @@ void TravelMgr::Init()
|
||||
PrepareDestinationCache();
|
||||
}
|
||||
sTravelNodeMap.Init();
|
||||
sQuestSpawnIndex->Init();
|
||||
}
|
||||
|
||||
TravelMgr::FlightMasterInfo const* TravelMgr::GetNearestFlightMasterInfo(Player* bot) const
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user