feat(Core/RPG): Pursue and act on quest GOs and items at POI

This commit is contained in:
bash 2026-05-01 15:01:35 +02:00
parent 078a480291
commit 2ff1d08f67
5 changed files with 627 additions and 9 deletions

View File

@ -47,7 +47,13 @@ public:
{ {
if (allowedGOFlags.empty()) if (allowedGOFlags.empty())
{ {
// questgivers for accept/turn-in; rest for quest progression
// (chests, runes, altars, moonwells, lily piles, …)
allowedGOFlags.push_back(GAMEOBJECT_TYPE_QUESTGIVER); allowedGOFlags.push_back(GAMEOBJECT_TYPE_QUESTGIVER);
allowedGOFlags.push_back(GAMEOBJECT_TYPE_CHEST);
allowedGOFlags.push_back(GAMEOBJECT_TYPE_GOOBER);
allowedGOFlags.push_back(GAMEOBJECT_TYPE_SPELL_FOCUS);
allowedGOFlags.push_back(GAMEOBJECT_TYPE_GENERIC);
} }
} }

View File

@ -19,6 +19,7 @@
#include "PathGenerator.h" #include "PathGenerator.h"
#include "Player.h" #include "Player.h"
#include "PlayerbotAI.h" #include "PlayerbotAI.h"
#include "Playerbots.h"
#include "QuestDef.h" #include "QuestDef.h"
#include "Random.h" #include "Random.h"
#include "SharedDefines.h" #include "SharedDefines.h"
@ -292,6 +293,9 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
data.lastReachPOI = 0; data.lastReachPOI = 0;
data.pos = WorldPosition(); data.pos = WorldPosition();
data.objectiveIdx = 0; data.objectiveIdx = 0;
data.pursuedLootGO.Clear();
data.pursuedUseGO.Clear();
data.pursuedUseTarget.Clear();
} }
} }
if (data.pos == WorldPosition()) if (data.pos == WorldPosition())
@ -320,15 +324,38 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
data.lastReachPOI = 0; data.lastReachPOI = 0;
data.pos = pos; data.pos = pos;
data.objectiveIdx = objectiveIdx; data.objectiveIdx = objectiveIdx;
data.pursuedLootGO.Clear();
data.pursuedUseGO.Clear();
data.pursuedUseTarget.Clear();
} }
if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI) if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI)
{ {
// yield to attack-anything if a quest mob is right next to us
if (HasNearbyQuestMob(15.0f))
return false;
// Occasional yield so attack-anything can pick off a passing
// hostile. Gated on "hostile actually in range" so we don't
// burn ticks yielding into nothing, and rate-limited so we
// don't fight every mob we walk past — multiplier still
// dominates, this just opens an occasional window.
GuidVector nearbyTargets = AI_VALUE(GuidVector, "possible targets");
for (ObjectGuid guid : nearbyTargets)
{
Unit* u = botAI->GetUnit(guid);
if (!u || !u->IsAlive())
continue;
if (bot->GetDistance(u) > 25.0f)
continue;
if (urand(0, 9) == 0) // 10% per tick when a hostile is in range
return false;
break;
}
if (MoveFarTo(data.pos)) if (MoveFarTo(data.pos))
return true; return true;
// Long-range sampler couldn't land a candidate — nudge the // sampler found nothing — nudge so next tick tries a new pos
// bot a short distance so the next tick retries from a
// different position instead of sitting idle.
return MoveRandomNear(10.0f); return MoveRandomNear(10.0f);
} }
// Now we are near the quest objective // Now we are near the quest objective
@ -373,14 +400,72 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
data.lastReachPOI = 0; data.lastReachPOI = 0;
data.pos = WorldPosition(); data.pos = WorldPosition();
data.objectiveIdx = 0; data.objectiveIdx = 0;
data.pursuedLootGO.Clear();
data.pursuedUseGO.Clear();
data.pursuedUseTarget.Clear();
return true; return true;
} }
// At the POI: keep the bot actively placed but avoid large // at POI: drive toward specific objectives first
// random 20yd hops that look like pacing back and forth. A small if (TryUseQuestItem(data.pursuedUseGO, data.pursuedUseTarget))
// ~8yd wander reads as the bot looking around while grind/loot return true;
// strategies do their work. if (TryLootQuestGO(data.pursuedLootGO))
return MoveRandomNear(8.0f); return true;
if (TryUseQuestGO(data.pursuedUseGO))
return true;
// 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: 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;
} }
bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data) bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
@ -444,7 +529,9 @@ bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
botAI->rpgInfo.ChangeToIdle(); botAI->rpgInfo.ChangeToIdle();
return true; return true;
} }
return false; // waiting for SearchQuestGiverAndAcceptOrReward to pick up the NPC;
// wander instead of false so we don't fall through to grind
return MoveRandomNear(15.0f);
} }
bool NewRpgTravelFlightAction::Execute(Event /*event*/) bool NewRpgTravelFlightAction::Execute(Event /*event*/)

View File

@ -8,6 +8,9 @@
#include "GossipDef.h" #include "GossipDef.h"
#include "GridTerrainData.h" #include "GridTerrainData.h"
#include "IVMapMgr.h" #include "IVMapMgr.h"
#include "Item.h"
#include "ItemTemplate.h"
#include "LootMgr.h"
#include "NewRpgInfo.h" #include "NewRpgInfo.h"
#include "NewRpgStrategy.h" #include "NewRpgStrategy.h"
#include "Object.h" #include "Object.h"
@ -26,6 +29,9 @@
#include "Random.h" #include "Random.h"
#include "RandomPlayerbotMgr.h" #include "RandomPlayerbotMgr.h"
#include "SharedDefines.h" #include "SharedDefines.h"
#include "Spell.h"
#include "SpellInfo.h"
#include "SpellMgr.h"
#include "StatsWeightCalculator.h" #include "StatsWeightCalculator.h"
#include "Timer.h" #include "Timer.h"
#include "TravelMgr.h" #include "TravelMgr.h"
@ -709,6 +715,503 @@ bool NewRpgBaseAction::SearchQuestGiverAndAcceptOrReward()
return false; return false;
} }
static bool BotNeedsItemForQuest(Player* bot, uint32 itemId)
{
for (uint16 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot)
{
uint32 questId = bot->GetQuestSlotQuestId(slot);
if (!questId)
continue;
if (bot->GetQuestStatus(questId) != QUEST_STATUS_INCOMPLETE)
continue;
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
if (!quest)
continue;
QuestStatusData const& qs = bot->getQuestStatusMap().at(questId);
for (int i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; ++i)
{
if (!quest->RequiredItemCount[i])
continue;
if (qs.ItemCount[i] >= quest->RequiredItemCount[i])
continue;
if (quest->RequiredItemId[i] == itemId)
return true;
}
}
return false;
}
bool NewRpgBaseAction::TryLootQuestGO(ObjectGuid& pursuedGO, float searchRange)
{
if (!bot->IsAlive() || bot->IsBeingTeleported() || bot->IsInFlight() ||
bot->GetVehicle() || bot->GetTransport())
return false;
// valid = spawned, selectable, holds a quest item we still need.
// INTERACT_COND is fine — ConditionMgr already gates on quest state.
auto isValidTarget = [&](GameObject* go) -> bool
{
if (!go || !go->IsInWorld() || !go->isSpawned())
return false;
if (!(go->GetPhaseMask() & bot->GetPhaseMask()))
return false;
if (go->HasGameObjectFlag(GO_FLAG_NOT_SELECTABLE))
return false;
GameObjectTemplate const* info = go->GetGOInfo();
if (!info)
return false;
// per-player quest drops via gameobject_questitem (Webwood Eggs…)
if (GameObjectQuestItemList const* items =
sObjectMgr->GetGameObjectQuestItemList(go->GetEntry()))
{
for (size_t i = 0; i < MAX_GAMEOBJECT_QUEST_ITEMS && i < items->size(); ++i)
{
uint32 itemId = uint32((*items)[i]);
if (!itemId)
continue;
if (BotNeedsItemForQuest(bot, itemId))
return true;
}
}
// standard loot template (chests, fishing holes)
if (uint32 lootId = info->GetLootId())
{
if (LootTemplates_Gameobject.HaveQuestLootForPlayer(lootId, bot))
return true;
}
return false;
};
// 2.5y sits inside the 3.5y loot gate with headroom
const float lootRange = 2.5f;
// stick with the committed target — re-picking nearest every tick
// causes zig-zag walks in dense spawn clusters
if (pursuedGO)
{
GameObject* existing = botAI->GetGameObject(pursuedGO);
if (existing && isValidTarget(existing) &&
bot->GetDistance(existing) <= searchRange)
{
if (bot->GetDistance(existing) > lootRange)
return MoveWorldObjectTo(existing->GetGUID(), lootRange);
// in range — loot strategy opens it
return true;
}
pursuedGO.Clear();
}
GuidVector possibleGameObjects = AI_VALUE(GuidVector, "possible new rpg game objects");
if (possibleGameObjects.empty())
return false;
GameObject* best = nullptr;
float bestDist = searchRange;
for (ObjectGuid guid : possibleGameObjects)
{
GameObject* go = botAI->GetGameObject(guid);
if (!isValidTarget(go))
continue;
float d = bot->GetDistance(go);
if (d >= bestDist)
continue;
best = go;
bestDist = d;
}
if (!best)
return false;
// commit
pursuedGO = best->GetGUID();
if (bot->GetDistance(best) > lootRange)
return MoveWorldObjectTo(best->GetGUID(), lootRange);
// in range — consume the tick so we don't fall through to wander
return true;
}
bool NewRpgBaseAction::TryUseQuestGO(ObjectGuid& pursuedGO, float searchRange)
{
if (!bot->IsAlive() || bot->IsBeingTeleported() || bot->IsInFlight() ||
bot->GetVehicle() || bot->GetTransport())
return false;
std::unordered_set<uint32> neededGoEntries;
for (uint16 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot)
{
uint32 questId = bot->GetQuestSlotQuestId(slot);
if (!questId)
continue;
if (bot->GetQuestStatus(questId) != QUEST_STATUS_INCOMPLETE)
continue;
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
if (!quest)
continue;
QuestStatusData const& qs = bot->getQuestStatusMap().at(questId);
for (int i = 0; i < QUEST_OBJECTIVES_COUNT; ++i)
{
int32 entry = quest->RequiredNpcOrGo[i];
if (entry >= 0)
continue;
if (qs.CreatureOrGOCount[i] >= quest->RequiredNpcOrGoCount[i])
continue;
neededGoEntries.insert(uint32(-entry));
}
}
if (neededGoEntries.empty())
return false;
auto isValidTarget = [&](GameObject* go) -> bool
{
if (!go || !go->IsInWorld() || !go->isSpawned())
return false;
if (!(go->GetPhaseMask() & bot->GetPhaseMask()))
return false;
if (go->HasGameObjectFlag(GO_FLAG_NOT_SELECTABLE))
return false;
return neededGoEntries.count(go->GetEntry()) > 0;
};
// commitment first
if (pursuedGO)
{
GameObject* existing = botAI->GetGameObject(pursuedGO);
if (existing && isValidTarget(existing) &&
bot->GetDistance(existing) <= searchRange)
{
if (bot->GetDistance(existing) > INTERACTION_DISTANCE)
return MoveWorldObjectTo(existing->GetGUID(), INTERACTION_DISTANCE);
existing->Use(bot);
ForceToWait(2000);
pursuedGO.Clear();
return true;
}
pursuedGO.Clear();
}
GuidVector possibleGameObjects = AI_VALUE(GuidVector, "possible new rpg game objects");
GameObject* best = nullptr;
float bestDist = searchRange;
for (ObjectGuid guid : possibleGameObjects)
{
GameObject* go = botAI->GetGameObject(guid);
if (!isValidTarget(go))
continue;
float d = bot->GetDistance(go);
if (d >= bestDist)
continue;
best = go;
bestDist = d;
}
if (!best)
return false;
pursuedGO = best->GetGUID();
if (bot->GetDistance(best) > INTERACTION_DISTANCE)
return MoveWorldObjectTo(best->GetGUID(), INTERACTION_DISTANCE);
best->Use(bot);
ForceToWait(2000);
pursuedGO.Clear();
return true;
}
bool NewRpgBaseAction::TryUseQuestItem(ObjectGuid& pursuedGO, ObjectGuid& pursuedTarget, float searchRange)
{
if (!bot->IsAlive() || bot->IsBeingTeleported() || bot->IsInFlight() ||
bot->GetVehicle() || bot->GetTransport())
return false;
std::unordered_set<uint32> candidateItemEntries;
// src items (the quest gave the bot a single item to use); branch C
// (self/area cast) is only safe to fire on these — auto-firing every
// ItemDrop on self can burn kill-credit sentinels and trigger
// unintended scripted side effects.
std::unordered_set<uint32> srcItemEntries;
std::unordered_set<uint32> neededCreatureEntries;
for (uint16 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot)
{
uint32 questId = bot->GetQuestSlotQuestId(slot);
if (!questId)
continue;
if (bot->GetQuestStatus(questId) != QUEST_STATUS_INCOMPLETE)
continue;
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
if (!quest)
continue;
if (uint32 src = quest->GetSrcItemId())
{
candidateItemEntries.insert(src);
srcItemEntries.insert(src);
}
// handed out by the quest (brands, flares, nets, standards)
for (int i = 0; i < QUEST_SOURCE_ITEM_IDS_COUNT; ++i)
{
if (uint32 drop = quest->ItemDrop[i])
candidateItemEntries.insert(drop);
}
QuestStatusData const& qs = bot->getQuestStatusMap().at(questId);
for (int i = 0; i < QUEST_OBJECTIVES_COUNT; ++i)
{
int32 entry = quest->RequiredNpcOrGo[i];
if (entry <= 0)
continue;
if (qs.CreatureOrGOCount[i] >= quest->RequiredNpcOrGoCount[i])
continue;
neededCreatureEntries.insert(uint32(entry));
}
}
if (candidateItemEntries.empty())
return false;
for (uint32 itemEntry : candidateItemEntries)
{
Item* item = bot->GetItemByEntry(itemEntry);
if (!item)
continue;
ItemTemplate const* proto = item->GetTemplate();
if (!proto)
continue;
uint32 useSpellId = 0;
for (uint8 i = 0; i < MAX_ITEM_PROTO_SPELLS; ++i)
{
if (proto->Spells[i].SpellTrigger != ITEM_SPELLTRIGGER_ON_USE)
continue;
if (proto->Spells[i].SpellId <= 0)
continue;
useSpellId = proto->Spells[i].SpellId;
break;
}
if (!useSpellId)
continue;
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(useSpellId);
if (!spellInfo)
continue;
// A: spell needs a focus GO (moonwell / lectern / anvil)
if (uint32 focusId = spellInfo->RequiresSpellFocus)
{
auto focusRadius = [](GameObject* go) -> float
{
GameObjectTemplate const* info = go->GetGOInfo();
// half radius so we end up inside, not on the rim
return std::max<float>(1.0f, float(info->spellFocus.dist) * 0.5f);
};
auto isValidFocus = [&](GameObject* go) -> bool
{
if (!go || !go->IsInWorld() || !go->isSpawned())
return false;
if (!(go->GetPhaseMask() & bot->GetPhaseMask()))
return false;
if (go->HasGameObjectFlag(GO_FLAG_NOT_SELECTABLE))
return false;
GameObjectTemplate const* info = go->GetGOInfo();
if (!info || info->type != GAMEOBJECT_TYPE_SPELL_FOCUS)
return false;
return info->spellFocus.focusId == focusId;
};
// commitment first
if (pursuedGO)
{
GameObject* existing = botAI->GetGameObject(pursuedGO);
if (existing && isValidFocus(existing) &&
bot->GetDistance(existing) <= searchRange)
{
float radius = focusRadius(existing);
if (bot->GetDistance(existing) > radius)
return MoveWorldObjectTo(existing->GetGUID(), radius);
SpellCastTargets targets;
bot->CastItemUseSpell(item, targets, 1, 0);
ForceToWait(2000);
pursuedGO.Clear();
return true;
}
pursuedGO.Clear();
}
GuidVector possibleGameObjects = AI_VALUE(GuidVector, "possible new rpg game objects");
GameObject* best = nullptr;
float bestDist = searchRange;
float bestRadius = INTERACTION_DISTANCE;
for (ObjectGuid guid : possibleGameObjects)
{
GameObject* go = botAI->GetGameObject(guid);
if (!isValidFocus(go))
continue;
float d = bot->GetDistance(go);
if (d >= bestDist)
continue;
best = go;
bestDist = d;
bestRadius = focusRadius(go);
}
if (best)
{
pursuedGO = best->GetGUID();
if (bot->GetDistance(best) > bestRadius)
return MoveWorldObjectTo(best->GetGUID(), bestRadius);
SpellCastTargets targets;
bot->CastItemUseSpell(item, targets, 1, 0);
ForceToWait(2000);
pursuedGO.Clear();
return true;
}
continue;
}
// B: spell needs a unit target — walk to the required creature
if (spellInfo->NeedsExplicitUnitTarget() && !neededCreatureEntries.empty())
{
auto isValidCreature = [&](Creature* c) -> bool
{
if (!c || !c->IsInWorld() || !c->IsAlive())
return false;
if (!(c->GetPhaseMask() & bot->GetPhaseMask()))
return false;
return neededCreatureEntries.count(c->GetEntry()) > 0;
};
// commitment first
if (pursuedTarget)
{
Creature* existing = botAI->GetCreature(pursuedTarget);
if (existing && isValidCreature(existing) &&
bot->GetDistance(existing) <= searchRange)
{
if (bot->GetDistance(existing) > INTERACTION_DISTANCE)
return MoveWorldObjectTo(existing->GetGUID(), INTERACTION_DISTANCE);
SpellCastTargets targets;
targets.SetUnitTarget(existing);
bot->CastItemUseSpell(item, targets, 1, 0);
ForceToWait(2000);
pursuedTarget.Clear();
return true;
}
pursuedTarget.Clear();
}
GuidVector possibleTargets = AI_VALUE(GuidVector, "possible new rpg targets");
Creature* best = nullptr;
float bestDist = searchRange;
for (ObjectGuid guid : possibleTargets)
{
Creature* c = botAI->GetCreature(guid);
if (!isValidCreature(c))
continue;
float d = bot->GetDistance(c);
if (d >= bestDist)
continue;
best = c;
bestDist = d;
}
if (best)
{
pursuedTarget = best->GetGUID();
if (bot->GetDistance(best) > INTERACTION_DISTANCE)
return MoveWorldObjectTo(best->GetGUID(), INTERACTION_DISTANCE);
SpellCastTargets targets;
targets.SetUnitTarget(best);
bot->CastItemUseSpell(item, targets, 1, 0);
ForceToWait(2000);
pursuedTarget.Clear();
return true;
}
continue;
}
// C: self / area — fire at bot's position. Restrict to GetSrcItemId
// items (the single item the quest hands the bot for self-use, e.g.
// a potion). ItemDrop entries can be kill-credit sentinels or
// scripted items that should never be auto-used on self.
if (!srcItemEntries.count(itemEntry))
continue;
SpellCastTargets targets;
if (spellInfo->IsTargetingArea())
targets.SetDst(*bot);
else
targets.SetUnitTarget(bot);
bot->CastItemUseSpell(item, targets, 1, 0);
ForceToWait(2000);
return true;
}
return false;
}
bool NewRpgBaseAction::HasNearbyQuestMob(float range)
{
// kill objectives + mobs that drop required quest items
std::unordered_set<uint32> neededCreatureEntries;
std::unordered_set<uint32> neededItemIds;
for (uint16 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot)
{
uint32 questId = bot->GetQuestSlotQuestId(slot);
if (!questId)
continue;
if (bot->GetQuestStatus(questId) != QUEST_STATUS_INCOMPLETE)
continue;
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
if (!quest)
continue;
QuestStatusData const& qs = bot->getQuestStatusMap().at(questId);
for (int i = 0; i < QUEST_OBJECTIVES_COUNT; ++i)
{
int32 entry = quest->RequiredNpcOrGo[i];
if (entry <= 0)
continue;
if (qs.CreatureOrGOCount[i] >= quest->RequiredNpcOrGoCount[i])
continue;
neededCreatureEntries.insert(uint32(entry));
}
for (int i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; ++i)
{
if (!quest->RequiredItemCount[i])
continue;
if (qs.ItemCount[i] >= quest->RequiredItemCount[i])
continue;
if (quest->RequiredItemId[i])
neededItemIds.insert(quest->RequiredItemId[i]);
}
}
if (neededCreatureEntries.empty() && neededItemIds.empty())
return false;
GuidVector possibleTargets = AI_VALUE(GuidVector, "possible targets");
for (ObjectGuid guid : possibleTargets)
{
Creature* c = botAI->GetCreature(guid);
if (!c || !c->IsInWorld() || !c->IsAlive())
continue;
if (!(c->GetPhaseMask() & bot->GetPhaseMask()))
continue;
if (bot->GetDistance(c) > range)
continue;
// direct kill objective
if (neededCreatureEntries.count(c->GetEntry()))
return true;
// drops a required quest item — HaveQuestLootForPlayer
// already filters by what this player still needs
if (!neededItemIds.empty())
{
CreatureTemplate const* tmpl = c->GetCreatureTemplate();
if (tmpl && tmpl->lootid &&
LootTemplates_Creature.HaveQuestLootForPlayer(tmpl->lootid, bot))
{
return true;
}
}
}
return false;
}
ObjectGuid NewRpgBaseAction::ChooseNpcOrGameObjectToInteract(bool questgiverOnly, float distanceLimit) ObjectGuid NewRpgBaseAction::ChooseNpcOrGameObjectToInteract(bool questgiverOnly, float distanceLimit)
{ {
GuidVector possibleTargets = AI_VALUE(GuidVector, "possible new rpg targets"); GuidVector possibleTargets = AI_VALUE(GuidVector, "possible new rpg targets");

View File

@ -51,6 +51,23 @@ protected:
bool TurnInQuest(Quest const* quest, ObjectGuid guid); bool TurnInQuest(Quest const* quest, ObjectGuid guid);
bool OrganizeQuestLog(); bool OrganizeQuestLog();
/* QUEST PROGRESSION HELPERS (at POI) */
// Walk to a GO that drops a needed quest item. The loot strategy
// opens and loots it once in range.
bool TryLootQuestGO(ObjectGuid& pursuedGO, float searchRange = 60.0f);
// Walk to / use a GO that is itself the objective (rune, lever,
// altar, coffin — RequiredNpcOrGo with a negative entry).
bool TryUseQuestGO(ObjectGuid& pursuedGO, float searchRange = 60.0f);
// Fire a quest item's OnUse spell at the right target: a spell-focus
// GO (moonwell), a required creature, or the bot itself.
bool TryUseQuestItem(ObjectGuid& pursuedGO, ObjectGuid& pursuedTarget, float searchRange = 60.0f);
// True when a quest-relevant mob is within range — used during
// travel so we yield to attack-anything instead of running past.
bool HasNearbyQuestMob(float range = 20.0f);
protected: protected:
bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo>& poiInfo, bool toComplete = false); bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo>& poiInfo, bool toComplete = false);
static WorldPosition SelectRandomGrindPos(Player* bot); static WorldPosition SelectRandomGrindPos(Player* bot);

View File

@ -46,6 +46,11 @@ struct NewRpgInfo
int32 objectiveIdx{0}; int32 objectiveIdx{0};
WorldPosition pos{}; WorldPosition pos{};
uint32 lastReachPOI{0}; uint32 lastReachPOI{0};
// committed target per objective type. stops zig-zagging in
// dense spawn clusters when "nearest" would flip each tick.
ObjectGuid pursuedLootGO{}; // GOs we loot (lilies, eggs)
ObjectGuid pursuedUseGO{}; // GOs we click or focus on
ObjectGuid pursuedUseTarget{}; // creature we apply an item to
}; };
// RPG_TRAVEL_FLIGHT // RPG_TRAVEL_FLIGHT
struct TravelFlight struct TravelFlight