Files
mod-playerbots/src/Ai/Base/Actions/BuyAction.cpp
Keleborn d1cac8d027 Bug fix. Equip Action triggered action (#2142)
# Pull Request

Brighton caught a mistake I made changeing the action registry, so the
correct action was no longer triggering. I cleaned that up, and renamed
the action.


## How to Test the Changes

- This was tested by adding logging to both equip actions. But to test
this without that, the best way to verify the fix is to stop alts from
auto upgrading via config. Then they should correctly follow the
configured behavior.

## Complexity & Impact

Does this change add new decision branches?
- - [x ] No
- - [ ] Yes (**explain below**)

Does this change increase per-bot or per-tick processing?
- - [x] No
- - [ ] Yes (**describe and justify impact**)

Could this logic scale poorly under load?
- - [x] No
- - [ ] Yes (**explain why**)
---

## Defaults & Configuration

Does this change modify default bot behavior?
- - [x] No
- - [ ] Yes (**explain why**)

If this introduces more advanced or AI-heavy logic:
- - [x] Lightweight mode remains the default
- - [ ] More complex behavior is optional and thereby configurable
---

## AI Assistance

Was AI assistance (e.g. ChatGPT or similar tools) used while working on
this change?
- - [x] No
- - [ ] Yes (**explain below**)

If yes, please specify:

- AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.)
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation)
- Which parts of the change were influenced or generated
- Whether the result was manually reviewed and adapted

AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
Any AI-influenced changes must be verified against existing CORE and PB
logic. We expect contributors to be honest
about what they do and do not understand.

---

## Final Checklist

- - [x] Stability is not compromised
- - [x] Performance impact is understood, tested, and acceptable
- - [x] Added logic complexity is justified and explained
- - [x] Documentation updated if needed

---

## Notes for Reviewers

Anything that significantly improves realism at the cost of stability or
performance should be carefully discussed
before merging.
2026-02-15 16:29:20 -08:00

261 lines
9.2 KiB
C++

/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#include "BuyAction.h"
#include "BudgetValues.h"
#include "Event.h"
#include "ItemCountValue.h"
#include "ItemUsageValue.h"
#include "ItemVisitors.h"
#include "Playerbots.h"
#include "StatsWeightCalculator.h"
bool BuyAction::Execute(Event event)
{
bool buyUseful = false;
ItemIds itemIds;
std::string const link = event.getParam();
if (link == "vendor")
buyUseful = true;
else
{
itemIds = chat->parseItems(link);
}
GuidVector vendors = botAI->GetAiObjectContext()->GetValue<GuidVector>("nearest npcs")->Get();
bool vendored = false;
bool result = false;
for (GuidVector::iterator i = vendors.begin(); i != vendors.end(); ++i)
{
ObjectGuid vendorguid = *i;
Creature* pCreature = bot->GetNPCIfCanInteractWith(vendorguid, UNIT_NPC_FLAG_VENDOR);
if (!pCreature)
continue;
vendored = true;
if (buyUseful)
{
// Items are evaluated from high-level to low level.
// For each item the bot checks again if an item is usefull.
// Bot will buy until no usefull items are left.
VendorItemData const* tItems = pCreature->GetVendorItems();
if (!tItems)
continue;
VendorItemList m_items_sorted = tItems->m_items;
m_items_sorted.erase(std::remove_if(m_items_sorted.begin(), m_items_sorted.end(),
[](VendorItem* i)
{
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(i->item);
return !proto;
}),
m_items_sorted.end());
if (m_items_sorted.empty())
continue;
StatsWeightCalculator calculator(bot);
calculator.SetItemSetBonus(false);
calculator.SetOverflowPenalty(false);
std::sort(m_items_sorted.begin(), m_items_sorted.end(),
[&calculator](VendorItem* i, VendorItem* j)
{
ItemTemplate const* item1 = sObjectMgr->GetItemTemplate(i->item);
ItemTemplate const* item2 = sObjectMgr->GetItemTemplate(j->item);
if (!item1 || !item2)
return false;
float score1 = calculator.CalculateItem(item1->ItemId);
float score2 = calculator.CalculateItem(item2->ItemId);
// Fallback to itemlevel if either score is 0
if (score1 == 0 || score2 == 0)
{
score1 = item1->ItemLevel;
score2 = item2->ItemLevel;
}
return score1 > score2; // Sort in descending order (highest score first)
});
std::unordered_map<uint32, float> bestPurchasedItemScore; // Track best item score per InventoryType
for (auto& tItem : m_items_sorted)
{
uint32 maxPurchases = 1; // Default to buying once
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(tItem->item);
if (!proto)
continue;
if (proto->Class == ITEM_CLASS_CONSUMABLE || proto->Class == ITEM_CLASS_PROJECTILE)
{
maxPurchases = 10; // Allow up to 10 purchases if it's a consumable or projectile
}
for (uint32 i = 0; i < maxPurchases; i++)
{
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", tItem->item);
uint32 invType = proto->InventoryType;
// Calculate item score
float newScore = calculator.CalculateItem(proto->ItemId);
// Skip if we already bought a better item for this slot
if (bestPurchasedItemScore.find(invType) != bestPurchasedItemScore.end() &&
bestPurchasedItemScore[invType] > newScore)
{
break; // Skip lower-scoring items
}
// Check the bot's currently equipped item for this slot
uint8 dstSlot = botAI->FindEquipSlot(proto, NULL_SLOT, true);
Item* oldItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, dstSlot);
float oldScore = 0.0f;
if (oldItem)
{
ItemTemplate const* oldItemProto = oldItem->GetTemplate();
if (oldItemProto)
oldScore = calculator.CalculateItem(oldItemProto->ItemId);
}
// Skip if the bot already has a better or equal item equipped
if (oldScore > newScore)
break;
uint32 price = proto->BuyPrice;
price = uint32(floor(price * bot->GetReputationPriceDiscount(pCreature)));
NeedMoneyFor needMoneyFor = NeedMoneyFor::none;
switch (usage)
{
case ITEM_USAGE_REPLACE:
case ITEM_USAGE_EQUIP:
case ITEM_USAGE_BAD_EQUIP:
case ITEM_USAGE_BROKEN_EQUIP:
needMoneyFor = NeedMoneyFor::gear;
break;
case ITEM_USAGE_AMMO:
needMoneyFor = NeedMoneyFor::ammo;
break;
case ITEM_USAGE_QUEST:
needMoneyFor = NeedMoneyFor::anything;
break;
case ITEM_USAGE_USE:
needMoneyFor = NeedMoneyFor::consumables;
break;
case ITEM_USAGE_SKILL:
needMoneyFor = NeedMoneyFor::tradeskill;
break;
default:
break;
}
if (needMoneyFor == NeedMoneyFor::none)
break;
if (AI_VALUE2(uint32, "free money for", uint32(needMoneyFor)) < price)
break;
if (!BuyItem(tItems, vendorguid, proto))
break;
// Store the best item score per InventoryType
bestPurchasedItemScore[invType] = newScore;
if (needMoneyFor == NeedMoneyFor::gear)
{
botAI->DoSpecificAction("equip upgrades packet action");
}
}
}
}
else
{
if (itemIds.empty())
return false;
for (ItemIds::iterator i = itemIds.begin(); i != itemIds.end(); i++)
{
uint32 itemId = *i;
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId);
if (!proto)
continue;
result |= BuyItem(pCreature->GetVendorItems(), vendorguid, proto);
if (!result)
{
std::ostringstream out;
out << "Nobody sells " << ChatHelper::FormatItem(proto) << " nearby";
botAI->TellMaster(out.str());
continue;
}
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemId);
if (usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_EQUIP ||
usage == ITEM_USAGE_BAD_EQUIP || usage == ITEM_USAGE_BROKEN_EQUIP)
{
botAI->DoSpecificAction("equip upgrades packet action");
break;
}
}
}
}
if (!vendored)
{
botAI->TellError("There are no vendors nearby");
return false;
}
return true;
}
bool BuyAction::BuyItem(VendorItemData const* tItems, ObjectGuid vendorguid, ItemTemplate const* proto)
{
if (!tItems || !proto)
return false;
uint32 itemId = proto->ItemId;
uint32 oldCount = bot->GetItemCount(itemId, false);
for (uint32 slot = 0; slot < tItems->GetItemCount(); ++slot)
{
if (tItems->GetItem(slot)->item != itemId)
continue;
uint32 botMoney = bot->GetMoney();
if (botAI->HasCheat(BotCheatMask::gold))
bot->SetMoney(10000000);
bot->BuyItemFromVendorSlot(vendorguid, slot, itemId, 1, NULL_BAG, NULL_SLOT);
if (botAI->HasCheat(BotCheatMask::gold))
bot->SetMoney(botMoney);
uint32 newCount = bot->GetItemCount(itemId, false);
if (newCount > oldCount)
{
std::ostringstream out;
out << "Buying " << ChatHelper::FormatItem(proto);
botAI->TellMaster(out.str());
return true;
}
return false;
}
return false;
}