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
952fdc54a1
commit
3f5a558c13
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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*/)
|
||||||
|
|||||||
@ -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"
|
||||||
@ -27,6 +30,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"
|
||||||
@ -725,6 +731,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");
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user