diff --git a/src/Ai/Base/Value/PossibleRpgTargetsValue.h b/src/Ai/Base/Value/PossibleRpgTargetsValue.h index 00e5f8012..ad00f70ff 100644 --- a/src/Ai/Base/Value/PossibleRpgTargetsValue.h +++ b/src/Ai/Base/Value/PossibleRpgTargetsValue.h @@ -47,7 +47,13 @@ public: { 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_CHEST); + allowedGOFlags.push_back(GAMEOBJECT_TYPE_GOOBER); + allowedGOFlags.push_back(GAMEOBJECT_TYPE_SPELL_FOCUS); + allowedGOFlags.push_back(GAMEOBJECT_TYPE_GENERIC); } } diff --git a/src/Ai/World/Rpg/Action/NewRpgAction.cpp b/src/Ai/World/Rpg/Action/NewRpgAction.cpp index fe3aeeb58..a9ff376be 100644 --- a/src/Ai/World/Rpg/Action/NewRpgAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgAction.cpp @@ -19,6 +19,7 @@ #include "PathGenerator.h" #include "Player.h" #include "PlayerbotAI.h" +#include "Playerbots.h" #include "QuestDef.h" #include "Random.h" #include "SharedDefines.h" @@ -292,6 +293,9 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data) data.lastReachPOI = 0; data.pos = WorldPosition(); data.objectiveIdx = 0; + data.pursuedLootGO.Clear(); + data.pursuedUseGO.Clear(); + data.pursuedUseTarget.Clear(); } } if (data.pos == WorldPosition()) @@ -320,15 +324,38 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data) data.lastReachPOI = 0; data.pos = pos; data.objectiveIdx = objectiveIdx; + data.pursuedLootGO.Clear(); + data.pursuedUseGO.Clear(); + data.pursuedUseTarget.Clear(); } 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)) 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. + // sampler found nothing — nudge so next tick tries a new pos return MoveRandomNear(10.0f); } // Now we are near the quest objective @@ -373,14 +400,72 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data) data.lastReachPOI = 0; data.pos = WorldPosition(); data.objectiveIdx = 0; + data.pursuedLootGO.Clear(); + data.pursuedUseGO.Clear(); + data.pursuedUseTarget.Clear(); return true; } - // 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); + // at POI: drive toward specific objectives first + if (TryUseQuestItem(data.pursuedUseGO, data.pursuedUseTarget)) + return true; + if (TryLootQuestGO(data.pursuedLootGO)) + 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) @@ -444,7 +529,9 @@ bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data) botAI->rpgInfo.ChangeToIdle(); 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*/) diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp index a8fefe7b5..80ce6758b 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp @@ -8,6 +8,9 @@ #include "GossipDef.h" #include "GridTerrainData.h" #include "IVMapMgr.h" +#include "Item.h" +#include "ItemTemplate.h" +#include "LootMgr.h" #include "NewRpgInfo.h" #include "NewRpgStrategy.h" #include "Object.h" @@ -26,6 +29,9 @@ #include "Random.h" #include "RandomPlayerbotMgr.h" #include "SharedDefines.h" +#include "Spell.h" +#include "SpellInfo.h" +#include "SpellMgr.h" #include "StatsWeightCalculator.h" #include "Timer.h" #include "TravelMgr.h" @@ -709,6 +715,503 @@ bool NewRpgBaseAction::SearchQuestGiverAndAcceptOrReward() 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 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 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 srcItemEntries; + std::unordered_set 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(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 neededCreatureEntries; + std::unordered_set 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) { GuidVector possibleTargets = AI_VALUE(GuidVector, "possible new rpg targets"); diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h index 1a62d053c..e33d3760a 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h @@ -51,6 +51,23 @@ protected: bool TurnInQuest(Quest const* quest, ObjectGuid guid); 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: bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector& poiInfo, bool toComplete = false); static WorldPosition SelectRandomGrindPos(Player* bot); diff --git a/src/Ai/World/Rpg/NewRpgInfo.h b/src/Ai/World/Rpg/NewRpgInfo.h index 9b3f51485..894f1302b 100644 --- a/src/Ai/World/Rpg/NewRpgInfo.h +++ b/src/Ai/World/Rpg/NewRpgInfo.h @@ -46,6 +46,11 @@ struct NewRpgInfo int32 objectiveIdx{0}; WorldPosition pos{}; 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 struct TravelFlight