mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
feat(Core/RPG): Pursue and act on quest GOs and items at POI
This commit is contained in:
parent
078a480291
commit
2ff1d08f67
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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*/)
|
||||
|
||||
@ -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<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)
|
||||
{
|
||||
GuidVector possibleTargets = AI_VALUE(GuidVector, "possible new rpg targets");
|
||||
|
||||
@ -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>& poiInfo, bool toComplete = false);
|
||||
static WorldPosition SelectRandomGrindPos(Player* bot);
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user