mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
feat(Core/RPG): Per-spawn destination pattern for incomplete quests (drops POI roam)
This commit is contained in:
parent
5ffd2ad89c
commit
61067a302e
@ -287,14 +287,23 @@ bool NewRpgDoQuestAction::Execute(Event /*event*/)
|
|||||||
|
|
||||||
bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
|
bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
|
||||||
{
|
{
|
||||||
uint32 questId = data.questId;
|
uint32 const questId = data.questId;
|
||||||
if (data.pos != WorldPosition())
|
|
||||||
|
// === 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())
|
||||||
{
|
{
|
||||||
/// @TODO: extract to a new function
|
int32 const currentObjective = data.objectiveIdx;
|
||||||
int32 currentObjective = data.objectiveIdx;
|
|
||||||
// check if the objective has completed
|
|
||||||
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
|
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
|
||||||
const QuestStatusData& q_status = bot->getQuestStatusMap().at(questId);
|
QuestStatusData const& q_status = bot->getQuestStatusMap().at(questId);
|
||||||
bool completed = true;
|
bool completed = true;
|
||||||
if (currentObjective < QUEST_OBJECTIVES_COUNT)
|
if (currentObjective < QUEST_OBJECTIVES_COUNT)
|
||||||
{
|
{
|
||||||
@ -307,96 +316,103 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
|
|||||||
quest->RequiredItemCount[currentObjective - QUEST_OBJECTIVES_COUNT])
|
quest->RequiredItemCount[currentObjective - QUEST_OBJECTIVES_COUNT])
|
||||||
completed = false;
|
completed = false;
|
||||||
}
|
}
|
||||||
// the current objective is completed, clear and find a new objective later
|
|
||||||
if (completed)
|
if (completed)
|
||||||
{
|
{
|
||||||
|
data.candidateSpawns.clear();
|
||||||
|
data.currentSpawnIdx = 0;
|
||||||
data.lastReachPOI = 0;
|
data.lastReachPOI = 0;
|
||||||
data.pos = WorldPosition();
|
|
||||||
data.objectiveIdx = 0;
|
data.objectiveIdx = 0;
|
||||||
data.pursuedLootGO.Clear();
|
data.pursuedLootGO.Clear();
|
||||||
data.pursuedUseGO.Clear();
|
data.pursuedUseGO.Clear();
|
||||||
data.pursuedUseTarget.Clear();
|
data.pursuedUseTarget.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data.pos == WorldPosition())
|
|
||||||
|
// 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())
|
||||||
{
|
{
|
||||||
std::vector<POIInfo> poiInfo;
|
std::vector<WorldPosition> spawns;
|
||||||
if (!GetQuestPOIPosAndObjectiveIdx(questId, poiInfo))
|
int32 objectiveIdx = 0;
|
||||||
|
if (!FetchQuestSpawnsForObjective(questId, spawns, objectiveIdx))
|
||||||
{
|
{
|
||||||
// can't find a poi pos to go, stop doing quest for now
|
botAI->lowPriorityQuest.insert(questId);
|
||||||
|
botAI->rpgStatistic.questAbandoned++;
|
||||||
|
LOG_DEBUG("playerbots", "[New RPG] {} abandoned quest {} — no spawns indexed",
|
||||||
|
bot->GetName(), questId);
|
||||||
botAI->rpgInfo.ChangeToIdle();
|
botAI->rpgInfo.ChangeToIdle();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
uint32 rndIdx = urand(0, poiInfo.size() - 1);
|
data.candidateSpawns = std::move(spawns);
|
||||||
G3D::Vector2 nearestPoi = poiInfo[rndIdx].pos;
|
data.currentSpawnIdx = 0;
|
||||||
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.lastReachPOI = 0;
|
||||||
data.pos = pos;
|
|
||||||
data.objectiveIdx = objectiveIdx;
|
data.objectiveIdx = objectiveIdx;
|
||||||
data.pursuedLootGO.Clear();
|
data.pursuedLootGO.Clear();
|
||||||
data.pursuedUseGO.Clear();
|
data.pursuedUseGO.Clear();
|
||||||
data.pursuedUseTarget.Clear();
|
data.pursuedUseTarget.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI)
|
// 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)
|
||||||
{
|
{
|
||||||
// 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))
|
if (HasNearbyQuestMobForObjective(15.0f, data.questId, data.objectiveIdx))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Note: previously yielded ~10%/tick when any hostile was
|
if (MoveFarTo(target))
|
||||||
// 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))
|
|
||||||
{
|
{
|
||||||
botAI->rpgInfo.moveRetryCount = 0;
|
botAI->rpgInfo.moveRetryCount = 0;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Retry counter (reference pattern): on N consecutive
|
// Retry counter: on N consecutive MoveFarTo failures, advance
|
||||||
// failures, drop this objective and go idle so the picker can
|
// to the next candidate spawn rather than sit on an unreachable
|
||||||
// try another quest / state.
|
// one. If that exhausts the list the abandon branch above
|
||||||
|
// catches it next tick.
|
||||||
if (++botAI->rpgInfo.moveRetryCount >= NewRpgInfo::MAX_MOVE_RETRIES)
|
if (++botAI->rpgInfo.moveRetryCount >= NewRpgInfo::MAX_MOVE_RETRIES)
|
||||||
botAI->rpgInfo.ChangeToIdle();
|
{
|
||||||
|
++data.currentSpawnIdx;
|
||||||
|
data.lastReachPOI = 0;
|
||||||
|
botAI->rpgInfo.moveRetryCount = 0;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// 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)
|
if (!data.lastReachPOI)
|
||||||
{
|
{
|
||||||
data.lastReachPOI = getMSTime();
|
data.lastReachPOI = getMSTime();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// stayed at this POI for more than 5 minutes
|
|
||||||
if (GetMSTimeDiffToNow(data.lastReachPOI) >= poiStayTime)
|
// 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)
|
||||||
{
|
{
|
||||||
bool hasProgression = false;
|
bool hasProgression = false;
|
||||||
int32 currentObjective = data.objectiveIdx;
|
int32 const currentObjective = data.objectiveIdx;
|
||||||
// check if the objective has progression
|
|
||||||
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
|
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
|
||||||
const QuestStatusData& q_status = bot->getQuestStatusMap().at(questId);
|
QuestStatusData const& q_status = bot->getQuestStatusMap().at(questId);
|
||||||
if (currentObjective < QUEST_OBJECTIVES_COUNT)
|
if (currentObjective < QUEST_OBJECTIVES_COUNT)
|
||||||
{
|
{
|
||||||
if (q_status.CreatureOrGOCount[currentObjective] != 0 && quest->RequiredNpcOrGoCount[currentObjective])
|
if (q_status.CreatureOrGOCount[currentObjective] != 0 && quest->RequiredNpcOrGoCount[currentObjective])
|
||||||
@ -408,28 +424,27 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
|
|||||||
quest->RequiredItemCount[currentObjective - QUEST_OBJECTIVES_COUNT])
|
quest->RequiredItemCount[currentObjective - QUEST_OBJECTIVES_COUNT])
|
||||||
hasProgression = true;
|
hasProgression = true;
|
||||||
}
|
}
|
||||||
if (!hasProgression)
|
if (hasProgression)
|
||||||
{
|
{
|
||||||
// we has reach the poi for more than 5 mins but no progession
|
// Refresh: re-fetch candidates so the list reflects what's
|
||||||
// may not be able to complete this quest, marked as abandoned
|
// still needed and is sorted from the bot's new position.
|
||||||
/// @TODO: It may be better to make lowPriorityQuest a global set shared by all bots (or saved in db)
|
data.candidateSpawns.clear();
|
||||||
botAI->lowPriorityQuest.insert(questId);
|
data.currentSpawnIdx = 0;
|
||||||
botAI->rpgStatistic.questAbandoned++;
|
data.lastReachPOI = 0;
|
||||||
LOG_DEBUG("playerbots", "[New RPG] {} marked as abandoned quest {}", bot->GetName(), questId);
|
|
||||||
botAI->rpgInfo.ChangeToIdle();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// clear and select another poi later
|
// No progression at this spawn — advance to the next candidate.
|
||||||
|
++data.currentSpawnIdx;
|
||||||
data.lastReachPOI = 0;
|
data.lastReachPOI = 0;
|
||||||
data.pos = WorldPosition();
|
|
||||||
data.objectiveIdx = 0;
|
|
||||||
data.pursuedLootGO.Clear();
|
data.pursuedLootGO.Clear();
|
||||||
data.pursuedUseGO.Clear();
|
data.pursuedUseGO.Clear();
|
||||||
data.pursuedUseTarget.Clear();
|
data.pursuedUseTarget.Clear();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// at POI: drive toward specific objectives first
|
// 7. At spawn, within timeout: drive toward specific objectives.
|
||||||
|
// Combat strategy engages adjacent quest mobs; loot/use
|
||||||
|
// actions handle quest GOs and quest items.
|
||||||
if (TryUseQuestItem(data.pursuedUseGO, data.pursuedUseTarget))
|
if (TryUseQuestItem(data.pursuedUseGO, data.pursuedUseTarget))
|
||||||
return true;
|
return true;
|
||||||
if (TryLootQuestGO(data.pursuedLootGO))
|
if (TryLootQuestGO(data.pursuedLootGO))
|
||||||
@ -437,96 +452,9 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
|
|||||||
if (TryUseQuestGO(data.pursuedUseGO))
|
if (TryUseQuestGO(data.pursuedUseGO))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
// gather quests: roam for spawns. kill quests: yield to grind.
|
// Yield this tick to combat/grind. No POI roam, no MoveRandomNear:
|
||||||
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
|
// bot stays at the spawn until either combat engages or the
|
||||||
if (quest)
|
// per-spawn timeout expires.
|
||||||
{
|
|
||||||
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
#include "Object.h"
|
#include "Object.h"
|
||||||
#include "ObjectAccessor.h"
|
#include "ObjectAccessor.h"
|
||||||
#include "OutdoorPvPMgr.h"
|
#include "OutdoorPvPMgr.h"
|
||||||
|
#include "QuestSpawnIndex.h"
|
||||||
#include "ObjectDefines.h"
|
#include "ObjectDefines.h"
|
||||||
#include "ObjectGuid.h"
|
#include "ObjectGuid.h"
|
||||||
#include "ObjectMgr.h"
|
#include "ObjectMgr.h"
|
||||||
@ -1368,6 +1369,89 @@ bool NewRpgBaseAction::GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector
|
|||||||
return true;
|
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)
|
WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot)
|
||||||
{
|
{
|
||||||
const std::vector<WorldLocation>& locs = sTravelMgr.GetLocsPerLevelCache(bot->GetLevel());
|
const std::vector<WorldLocation>& locs = sTravelMgr.GetLocsPerLevelCache(bot->GetLevel());
|
||||||
|
|||||||
@ -76,6 +76,16 @@ protected:
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo>& poiInfo, bool toComplete = false);
|
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 SelectRandomGrindPos(Player* bot);
|
||||||
static WorldPosition SelectRandomCampPos(Player* bot);
|
static WorldPosition SelectRandomCampPos(Player* bot);
|
||||||
bool SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector<uint32>& path);
|
bool SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector<uint32>& path);
|
||||||
|
|||||||
@ -46,7 +46,17 @@ struct NewRpgInfo
|
|||||||
const Quest* quest{nullptr};
|
const Quest* quest{nullptr};
|
||||||
uint32 questId{0};
|
uint32 questId{0};
|
||||||
int32 objectiveIdx{0};
|
int32 objectiveIdx{0};
|
||||||
|
// Turn-in POI (DoCompletedQuest). Kept POI-based since this is
|
||||||
|
// the quest-giver location, not a mob spawn.
|
||||||
WorldPosition pos{};
|
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};
|
uint32 lastReachPOI{0};
|
||||||
// committed target per objective type. stops zig-zagging in
|
// committed target per objective type. stops zig-zagging in
|
||||||
// dense spawn clusters when "nearest" would flip each tick.
|
// dense spawn clusters when "nearest" would flip each tick.
|
||||||
|
|||||||
60
src/Mgr/Quest/QuestSpawnIndex.cpp
Normal file
60
src/Mgr/Quest/QuestSpawnIndex.cpp
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
70
src/Mgr/Quest/QuestSpawnIndex.h
Normal file
70
src/Mgr/Quest/QuestSpawnIndex.h
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* 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,6 +20,7 @@
|
|||||||
#include "ModelIgnoreFlags.h"
|
#include "ModelIgnoreFlags.h"
|
||||||
#include "PathGenerator.h"
|
#include "PathGenerator.h"
|
||||||
#include "Playerbots.h"
|
#include "Playerbots.h"
|
||||||
|
#include "QuestSpawnIndex.h"
|
||||||
#include "RaceMgr.h"
|
#include "RaceMgr.h"
|
||||||
#include "TransportMgr.h"
|
#include "TransportMgr.h"
|
||||||
#include "VMapFactory.h"
|
#include "VMapFactory.h"
|
||||||
@ -3584,6 +3585,7 @@ void TravelMgr::Init()
|
|||||||
PrepareDestinationCache();
|
PrepareDestinationCache();
|
||||||
}
|
}
|
||||||
sTravelNodeMap.Init();
|
sTravelNodeMap.Init();
|
||||||
|
sQuestSpawnIndex->Init();
|
||||||
}
|
}
|
||||||
|
|
||||||
TravelMgr::FlightMasterInfo const* TravelMgr::GetNearestFlightMasterInfo(Player* bot) const
|
TravelMgr::FlightMasterInfo const* TravelMgr::GetNearestFlightMasterInfo(Player* bot) const
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user