mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
feat(Core/Loot): Quest GO loot, bag-make-room, item-pursuit
This commit is contained in:
parent
01d91092ec
commit
80dc6a856e
@ -29,6 +29,10 @@ void DestroyItemAction::DestroyItem(FindItemVisitor* visitor)
|
||||
std::vector<Item*> items = visitor->GetResult();
|
||||
for (Item* item : items)
|
||||
{
|
||||
// backstop: never drop an active quest item
|
||||
if (bot->HasQuestForItem(item->GetTemplate()->ItemId))
|
||||
continue;
|
||||
|
||||
std::ostringstream out;
|
||||
out << chat->FormatItem(item->GetTemplate()) << " destroyed";
|
||||
botAI->TellMaster(out);
|
||||
@ -67,18 +71,11 @@ bool SmartDestroyItemAction::Execute(Event /*event*/)
|
||||
return true;
|
||||
}
|
||||
|
||||
// ITEM_USAGE_QUEST is excluded — those are still-needed quest items
|
||||
std::vector<uint32> bestToDestroy = {ITEM_USAGE_NONE}; // First destroy anything useless.
|
||||
|
||||
if (!AI_VALUE(bool, "can sell") &&
|
||||
AI_VALUE(
|
||||
bool,
|
||||
"should get money")) // We need money so quest items are less important since they can't directly be sold.
|
||||
bestToDestroy.push_back(ITEM_USAGE_QUEST);
|
||||
else // We don't need money so destroy the cheapest stuff.
|
||||
{
|
||||
bestToDestroy.push_back(ITEM_USAGE_VENDOR);
|
||||
bestToDestroy.push_back(ITEM_USAGE_AH);
|
||||
}
|
||||
bestToDestroy.push_back(ITEM_USAGE_VENDOR);
|
||||
bestToDestroy.push_back(ITEM_USAGE_AH);
|
||||
|
||||
// If we still need room
|
||||
bestToDestroy.push_back(
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
|
||||
#include "LootAction.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include "Bag.h"
|
||||
#include "ChatHelper.h"
|
||||
#include "Event.h"
|
||||
#include "GuildMgr.h"
|
||||
@ -76,7 +79,11 @@ bool OpenLootAction::Execute(Event /*event*/)
|
||||
bool result = DoLoot(lootObject);
|
||||
if (result)
|
||||
{
|
||||
AI_VALUE(LootObjectStack*, "available loot")->Remove(lootObject.guid);
|
||||
// MarkCompleted (not Remove) — "add all loot" reads
|
||||
// "nearest corpses" without a lootable filter, so a plain
|
||||
// Remove lets the same corpse re-enter the stack on the next
|
||||
// tick. The completed set blocks re-add for ~5 min.
|
||||
AI_VALUE(LootObjectStack*, "available loot")->MarkCompleted(lootObject.guid);
|
||||
context->GetValue<LootObject>("loot target")->Set(LootObject());
|
||||
}
|
||||
return result;
|
||||
@ -139,8 +146,9 @@ bool OpenLootAction::DoLoot(LootObject& lootObject)
|
||||
if (go && (go->GetGoState() != GO_STATE_READY))
|
||||
return false;
|
||||
|
||||
// This prevents dungeon chests like Tribunal Chest (Halls of Stone) from being ninja'd by the bots
|
||||
if (go && go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_INTERACT_COND))
|
||||
// Block event-gated chests (Tribunal Chest, Gunship Armory) but allow
|
||||
// wild quest GOs (Moonpetal Lily etc.) when the bot is on the quest.
|
||||
if (go && go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_INTERACT_COND) && !lootObject.isNeededQuestItem)
|
||||
return false;
|
||||
|
||||
// This prevents raid chests like Gunship Armory (ICC) from being ninja'd by the bots
|
||||
@ -377,6 +385,12 @@ bool StoreLootAction::Execute(Event event)
|
||||
// bot->GetSession()->HandleLootMoneyOpcode(packet);
|
||||
}
|
||||
|
||||
// one make-room destroy per loot packet — CanStoreNewItem after a junk
|
||||
// destroy can still report full while CMSG_AUTOSTORE_LOOT_ITEM is
|
||||
// queued, so a multi-quest-item packet would otherwise destroy more
|
||||
// junk than necessary
|
||||
bool destroyedThisPacket = false;
|
||||
|
||||
for (uint8 i = 0; i < items; ++i)
|
||||
{
|
||||
uint32 itemid;
|
||||
@ -402,7 +416,9 @@ bool StoreLootAction::Execute(Event event)
|
||||
if (!proto)
|
||||
continue;
|
||||
|
||||
if (!botAI->HasActivePlayerMaster() && AI_VALUE(uint8, "bag space") > 80)
|
||||
// bags >80%: skip non-stackable junk (quest items exempt)
|
||||
if (!botAI->HasActivePlayerMaster() && AI_VALUE(uint8, "bag space") > 80 &&
|
||||
!bot->HasQuestForItem(itemid))
|
||||
{
|
||||
uint32 maxStack = proto->GetMaxStackSize();
|
||||
if (maxStack == 1)
|
||||
@ -438,6 +454,55 @@ bool StoreLootAction::Execute(Event event)
|
||||
GuildTaskMgr::instance().CheckItemTask(itemid, itemcount, ref->GetSource(), bot);
|
||||
}
|
||||
|
||||
// bags full + quest item: make room by dropping cheapest junk
|
||||
if (!destroyedThisPacket && bot->HasQuestForItem(itemid))
|
||||
{
|
||||
ItemPosCountVec dest;
|
||||
InventoryResult can =
|
||||
bot->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, itemid, itemcount);
|
||||
if (can == EQUIP_ERR_INVENTORY_FULL || can == EQUIP_ERR_BAG_FULL)
|
||||
{
|
||||
// picked by usage, not quality — high-level bots have no grays
|
||||
Item* victim = nullptr;
|
||||
uint32 minPrice = std::numeric_limits<uint32>::max();
|
||||
auto consider = [&](uint8 bag, uint8 slot)
|
||||
{
|
||||
Item* it = bot->GetItemByPos(bag, slot);
|
||||
if (!it)
|
||||
return;
|
||||
ItemTemplate const* tpl = it->GetTemplate();
|
||||
if (!tpl)
|
||||
return;
|
||||
if (bot->HasQuestForItem(tpl->ItemId))
|
||||
return;
|
||||
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", tpl->ItemId);
|
||||
if (usage != ITEM_USAGE_NONE && usage != ITEM_USAGE_VENDOR &&
|
||||
usage != ITEM_USAGE_BAD_EQUIP && usage != ITEM_USAGE_BROKEN_EQUIP)
|
||||
return;
|
||||
if (tpl->SellPrice < minPrice)
|
||||
{
|
||||
minPrice = tpl->SellPrice;
|
||||
victim = it;
|
||||
}
|
||||
};
|
||||
for (uint8 slot = INVENTORY_SLOT_ITEM_START; slot < INVENTORY_SLOT_ITEM_END; ++slot)
|
||||
consider(INVENTORY_SLOT_BAG_0, slot);
|
||||
for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag)
|
||||
{
|
||||
Bag* pBag = bot->GetBagByPos(bag);
|
||||
if (!pBag)
|
||||
continue;
|
||||
for (uint32 slot = 0; slot < pBag->GetBagSize(); ++slot)
|
||||
consider(bag, static_cast<uint8>(slot));
|
||||
}
|
||||
if (victim)
|
||||
{
|
||||
bot->DestroyItem(victim->GetBagSlot(), victim->GetSlot(), true);
|
||||
destroyedThisPacket = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WorldPacket* packet = new WorldPacket(CMSG_AUTOSTORE_LOOT_ITEM, 1);
|
||||
*packet << itemindex;
|
||||
bot->GetSession()->QueuePacket(packet);
|
||||
@ -453,7 +518,7 @@ bool StoreLootAction::Execute(Event event)
|
||||
BroadcastHelper::BroadcastLootingItem(botAI, bot, proto);
|
||||
}
|
||||
|
||||
AI_VALUE(LootObjectStack*, "available loot")->Remove(guid);
|
||||
AI_VALUE(LootObjectStack*, "available loot")->MarkCompleted(guid);
|
||||
|
||||
// release loot
|
||||
WorldPacket* packet = new WorldPacket(CMSG_LOOT_RELEASE, 8);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -45,7 +45,8 @@ void LootTargetList::shrink(time_t fromTime)
|
||||
}
|
||||
}
|
||||
|
||||
LootObject::LootObject(Player* bot, ObjectGuid guid) : guid(), skillId(SKILL_NONE), reqSkillValue(0), reqItem(0)
|
||||
LootObject::LootObject(Player* bot, ObjectGuid guid)
|
||||
: guid(), skillId(SKILL_NONE), reqSkillValue(0), reqItem(0), isNeededQuestItem(false)
|
||||
{
|
||||
Refresh(bot, guid);
|
||||
}
|
||||
@ -55,6 +56,7 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID)
|
||||
skillId = SKILL_NONE;
|
||||
reqSkillValue = 0;
|
||||
reqItem = 0;
|
||||
isNeededQuestItem = false;
|
||||
guid.Clear();
|
||||
|
||||
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
||||
@ -101,6 +103,7 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID)
|
||||
if (IsNeededForQuest(bot, itemId))
|
||||
{
|
||||
this->guid = lootGUID;
|
||||
this->isNeededQuestItem = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -135,10 +138,21 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID)
|
||||
if (!proto)
|
||||
continue;
|
||||
|
||||
// Moonpetal Lily, Hyacinth Mushroom etc. expose quest
|
||||
// drops here (not in gameobject_questitem). Flag it so
|
||||
// the INTERACT_COND gate lets the bot through.
|
||||
if (IsNeededForQuest(bot, itemId))
|
||||
{
|
||||
this->guid = lootGUID;
|
||||
this->isNeededQuestItem = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (proto->Class != ITEM_CLASS_QUEST)
|
||||
{
|
||||
onlyHasQuestItems = false;
|
||||
break;
|
||||
// keep scanning — a later item may be needed
|
||||
continue;
|
||||
}
|
||||
|
||||
// If this item references another loot table, process it
|
||||
@ -157,11 +171,15 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID)
|
||||
if (!refProto)
|
||||
continue;
|
||||
|
||||
if (refProto->Class != ITEM_CLASS_QUEST)
|
||||
if (IsNeededForQuest(bot, refItemId))
|
||||
{
|
||||
onlyHasQuestItems = false;
|
||||
break;
|
||||
this->guid = lootGUID;
|
||||
this->isNeededQuestItem = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (refProto->Class != ITEM_CLASS_QUEST)
|
||||
onlyHasQuestItems = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -270,6 +288,7 @@ LootObject::LootObject(LootObject const& other)
|
||||
skillId = other.skillId;
|
||||
reqSkillValue = other.reqSkillValue;
|
||||
reqItem = other.reqItem;
|
||||
isNeededQuestItem = other.isNeededQuestItem;
|
||||
}
|
||||
|
||||
bool LootObject::IsLootPossible(Player* bot)
|
||||
@ -299,10 +318,13 @@ bool LootObject::IsLootPossible(Player* bot)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prevent bot from running to chests that are unlootable (e.g. Gunship Armory before completing the event) or on
|
||||
// respawn time
|
||||
// Block event-gated chests (Gunship Armory pre-event) and unspawned
|
||||
// GOs. INTERACT_COND alone is allowed when the GO holds a quest
|
||||
// item we need — ConditionMgr already gates on quest state.
|
||||
GameObject* go = botAI->GetGameObject(guid);
|
||||
if (go && (go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_INTERACT_COND | GO_FLAG_NOT_SELECTABLE) || !go->isSpawned()))
|
||||
if (go && (go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_NOT_SELECTABLE) || !go->isSpawned()))
|
||||
return false;
|
||||
if (go && go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_INTERACT_COND) && !isNeededQuestItem)
|
||||
return false;
|
||||
|
||||
if (skillId == SKILL_NONE)
|
||||
@ -340,6 +362,13 @@ bool LootObject::IsLootPossible(Player* bot)
|
||||
|
||||
bool LootObjectStack::Add(ObjectGuid guid)
|
||||
{
|
||||
// expire old completed entries so a despawn/respawn with a reused
|
||||
// guid can still be looted later
|
||||
completedLoot.shrink(time(nullptr) - 300);
|
||||
|
||||
if (completedLoot.find(guid) != completedLoot.end())
|
||||
return false;
|
||||
|
||||
if (availableLoot.size() >= MAX_LOOT_OBJECT_COUNT)
|
||||
{
|
||||
availableLoot.shrink(time(nullptr) - 30);
|
||||
@ -363,7 +392,17 @@ void LootObjectStack::Remove(ObjectGuid guid)
|
||||
availableLoot.erase(i);
|
||||
}
|
||||
|
||||
void LootObjectStack::Clear() { availableLoot.clear(); }
|
||||
void LootObjectStack::MarkCompleted(ObjectGuid guid)
|
||||
{
|
||||
Remove(guid);
|
||||
completedLoot.insert(guid);
|
||||
}
|
||||
|
||||
void LootObjectStack::Clear()
|
||||
{
|
||||
availableLoot.clear();
|
||||
completedLoot.clear();
|
||||
}
|
||||
|
||||
bool LootObjectStack::CanLoot(float maxDistance)
|
||||
{
|
||||
|
||||
@ -26,7 +26,7 @@ public:
|
||||
class LootObject
|
||||
{
|
||||
public:
|
||||
LootObject() : skillId(0), reqSkillValue(0), reqItem(0) {}
|
||||
LootObject() : skillId(0), reqSkillValue(0), reqItem(0), isNeededQuestItem(false) {}
|
||||
LootObject(Player* bot, ObjectGuid guid);
|
||||
LootObject(LootObject const& other);
|
||||
LootObject& operator=(LootObject const& other) = default;
|
||||
@ -40,6 +40,9 @@ public:
|
||||
uint32 skillId;
|
||||
uint32 reqSkillValue;
|
||||
uint32 reqItem;
|
||||
// GO holds a quest item we still need; lets us bypass the
|
||||
// INTERACT_COND blanket reject in the loot path
|
||||
bool isNeededQuestItem;
|
||||
|
||||
private:
|
||||
static bool IsNeededForQuest(Player* bot, uint32 itemId);
|
||||
@ -73,6 +76,7 @@ public:
|
||||
|
||||
bool Add(ObjectGuid guid);
|
||||
void Remove(ObjectGuid guid);
|
||||
void MarkCompleted(ObjectGuid guid);
|
||||
void Clear();
|
||||
bool CanLoot(float maxDistance);
|
||||
LootObject GetLoot(float maxDistance = 0);
|
||||
@ -82,6 +86,9 @@ private:
|
||||
|
||||
Player* bot;
|
||||
LootTargetList availableLoot;
|
||||
// Guids we already opened loot on; blocks "add all loot" from
|
||||
// re-adding the same corpse before it despawns.
|
||||
LootTargetList completedLoot;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user