mod-playerbots/src/Ai/Base/Value/ItemUsageValue.cpp
Crow 94195c3b9b
Bots Don't Autoequip Tools & Other Misc Weapons (#2346)
<!--
Thank you for contributing to mod-playerbots, please make sure that
you...
1. Submit your PR to the test-staging branch, not master.
2. Read the guidelines below before submitting.
3. Don't delete parts of this template.

DESIGN PHILOSOPHY: We prioritize STABILITY, PERFORMANCE, AND
PREDICTABILITY over behavioral realism.

Every action and decision executes PER BOT AND PER TRIGGER. Small
increases in logic complexity scale
poorly across thousands of bots and negatively affect all. We prioritize
a stable system over a smarter
one. Bots don't need to behave perfectly; believable behavior is the
goal, not human simulation.
Default behavior must be cheap in processing; expensive behavior must be
opt-in.

Before submitting, make sure your changes aligns with these principles.
-->

## Pull Request Description
<!-- Describe what this change does and why it is needed -->
Solve the rest of #2344

Now, bots won't autoequip any weapon from ITEM_SUBCLASS_WEAPON_MISC,
which includes all of the basic tools and some other crap that they have
no need to autoequip, either. Bots are still eligible to equip those
weapons (such as through the "e" command).

Note that MISC includes the Argent Tournament lances. I've not played
WotLK, but I assume those might be relevant for a strategy. It shouldn't
be a problem though because I've intentionally not made bots ineligible
for MISC weapons; they just won't consider them upgrades on their own.

I also cleaned up ItemUsageValue::QueryItemUsageForEquip to consolidate
checks and so on. None of that should be functional, or I screwed up.
The check for MISC is on lines 219 through 221.

## Feature Evaluation
<!--
If your PR is very minimal (comment typo, wrong ID reference, etc), and
it is very obvious it will not have
any impact on performance, you may skip these question. If necessary, a
maintainer may ask you for them later.
-->

<!-- Please answer the following: -->
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.



## How to Test the Changes
<!--
- Step-by-step instructions to test the change.
- Any required setup (e.g. multiple players, number of bots, specific
configuration).
- Expected behavior and how to verify it.
-->
Activate selfbot. Unequip all weapons and have nothing in the inventory
except for a MISC weapon such as a skinning knife. Whisper self "equip
upgrade"--nothing should happen. Whisper self "e [LINK TO WEAPON]"--the
bot should equip the weapon.


## Impact Assessment
<!-- As a generic test, before and after measure of pmon (playerbot pmon
tick) can help you here. -->
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
    - - [ ] No, not at all
    - - [x] Minimal impact (**explain below**)
    - - [ ] Moderate impact (**explain below**)

There's an extra check but totally meaningless with respect to
performance.

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

They won't auto-equip crap that will prevent them from using abilities.

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



## AI Assistance
<!--
AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
We expect contributors to be honest about what they do and do not
understand.
-->
Was AI assistance used while working on this change?
- - [ ] No
- - [x] Yes (**explain below**)
<!--
If yes, please specify:
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation).
- Which parts of the change were influenced or generated, and whether it
was thoroughly reviewed.
-->

I had GPT-5.4 evaluate different spots where I thought an exclusion
could be added before settling on this one.

<!--
TRANSLATIONS:
Anything new that the bots say in chat must be in a translatable format.
This is done using GetBotTextOrDefault,
which you can search for in the codebase to find examples. Your code
needs to have English as the default fallback,
while the full translations need to be in an SQL update file. The
languages in the file are the nine language
options supported by AzerothCore: English, Korean, French, German,
Chinese, Taiwanese, Spanish, Spanish Mexico, and
Russian. See
data/sql/playerbots/updates/2025_12_27_ai_playerbot_fishing_text.sql as
an example of a translation SQL
update, whose content are called within the codebase at
src/strategy/actions/FishingAction.cpp
-->

## Final Checklist

- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->
2026-05-02 12:20:03 -07:00

913 lines
33 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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 "ItemUsageValue.h"
#include "AiFactory.h"
#include "ChatHelper.h"
#include "GuildTaskMgr.h"
#include "Item.h"
#include "LootObjectStack.h"
#include "PlayerbotAIConfig.h"
#include "PlayerbotFactory.h"
#include "Playerbots.h"
#include "RandomItemMgr.h"
#include "ServerFacade.h"
#include "StatsWeightCalculator.h"
ItemUsage ItemUsageValue::Calculate()
{
ParsedItemUsage const parsed = GetItemIdFromQualifier();
uint32 itemId = parsed.itemId;
uint32 randomPropertyId = parsed.randomPropertyId;
if (!itemId)
return ITEM_USAGE_NONE;
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId);
if (!proto)
return ITEM_USAGE_NONE;
if (botAI->HasActivePlayerMaster())
{
if (IsItemUsefulForSkill(proto) || IsItemNeededForSkill(proto))
return ITEM_USAGE_SKILL;
}
else
{
bool needItem = false;
if (IsItemNeededForSkill(proto))
needItem = true;
else
{
bool lowBagSpace = AI_VALUE(uint8, "bag space") > 50;
if (proto->Class == ITEM_CLASS_TRADE_GOODS || proto->Class == ITEM_CLASS_MISC ||
proto->Class == ITEM_CLASS_REAGENT)
needItem = IsItemNeededForUsefullSpell(proto, lowBagSpace);
else if (proto->Class == ITEM_CLASS_RECIPE)
{
if (bot->HasSpell(proto->Spells[2].SpellId))
needItem = false;
else
needItem = bot->BotCanUseItem(proto) == EQUIP_ERR_OK;
}
}
if (needItem)
{
float stacks = CurrentStacks(proto);
if (stacks < 1)
return ITEM_USAGE_SKILL; // Buy more.
if (stacks < 2)
return ITEM_USAGE_KEEP; // Keep current amount.
}
}
if (proto->Class == ITEM_CLASS_KEY)
return ITEM_USAGE_USE;
const uint32_t maxCount = proto->MaxCount;
if (proto->Class == ITEM_CLASS_CONSUMABLE &&
(maxCount == 0 || bot->GetItemCount(itemId, false) < maxCount))
{
std::string const foodType = GetConsumableType(proto, bot->GetPower(POWER_MANA));
if (!foodType.empty() && bot->CanUseItem(proto) == EQUIP_ERR_OK)
{
float stacks = BetterStacks(proto, foodType);
if (stacks < 2)
{
stacks += CurrentStacks(proto);
if (stacks < 2)
return ITEM_USAGE_USE; // Buy some to get to 2 stacks
else if (stacks < 3) // Keep the item if less than 3 stacks
return ITEM_USAGE_KEEP;
}
}
}
if (bot->GetGuildId() && GuildTaskMgr::instance().IsGuildTaskItem(itemId, bot->GetGuildId()))
return ITEM_USAGE_GUILD_TASK;
ItemUsage equip = QueryItemUsageForEquip(proto, randomPropertyId);
if (equip != ITEM_USAGE_NONE)
return equip;
// Get item instance to check if it's soulbound
Item* item = bot->GetItemByEntry(proto->ItemId);
bool isSoulbound = item && item->IsSoulBound();
if ((proto->Class == ITEM_CLASS_ARMOR || proto->Class == ITEM_CLASS_WEAPON) &&
botAI->HasSkill(SKILL_ENCHANTING) &&
proto->Quality >= ITEM_QUALITY_UNCOMMON)
{
// Retrieve the bot's Enchanting skill level
uint32 enchantingSkill = bot->GetSkillValue(SKILL_ENCHANTING);
// Check if the bot has a high enough skill to disenchant this item
if (proto->RequiredDisenchantSkill > 0 && enchantingSkill < proto->RequiredDisenchantSkill)
return ITEM_USAGE_NONE; // Not skilled enough to disenchant
// BoE (Bind on Equip) items should NOT be disenchanted unless they are already bound
if (proto->Bonding == BIND_WHEN_PICKED_UP || (proto->Bonding == BIND_WHEN_EQUIPPED && isSoulbound))
return ITEM_USAGE_DISENCHANT;
}
Player* master = botAI->GetMaster();
bool isSelfBot = (master == bot);
bool botNeedsItemForQuest = IsItemUsefulForQuest(bot, proto);
bool masterNeedsItemForQuest = master && sPlayerbotAIConfig.syncQuestWithPlayer && IsItemUsefulForQuest(master, proto);
// Identify the source of loot
LootObject lootObject = AI_VALUE(LootObject, "loot target");
// Get GUID of loot source
ObjectGuid lootGuid = lootObject.guid;
// Check if loot source is an item
bool isLootFromItem = lootGuid.IsItem();
// If the loot is from an item in the bots bags, ignore syncQuestWithPlayer
if (isLootFromItem && botNeedsItemForQuest)
return ITEM_USAGE_QUEST;
// If the bot is NOT acting alone and the master needs this quest item, defer to the master
if (!isSelfBot && masterNeedsItemForQuest)
return ITEM_USAGE_NONE;
// If the bot itself needs the item for a quest, allow looting
if (botNeedsItemForQuest)
return ITEM_USAGE_QUEST;
if (proto->Class == ITEM_CLASS_PROJECTILE && bot->CanUseItem(proto) == EQUIP_ERR_OK)
{
ItemUsage ammoUsage = QueryItemUsageForAmmo(proto);
if (ammoUsage != ITEM_USAGE_NONE)
return ammoUsage;
}
// Need to add something like free bagspace or item value.
if (proto->SellPrice > 0)
{
if (proto->Quality >= ITEM_QUALITY_NORMAL && !isSoulbound && proto->Bonding != BIND_WHEN_PICKED_UP)
return ITEM_USAGE_AH;
else
return ITEM_USAGE_VENDOR;
}
return ITEM_USAGE_NONE;
}
ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto, int32 randomPropertyId)
{
if (bot->BotCanUseItem(itemProto) != EQUIP_ERR_OK)
return ITEM_USAGE_NONE;
if (itemProto->InventoryType == INVTYPE_NON_EQUIP)
return ITEM_USAGE_NONE;
Item* pItem = Item::CreateItem(itemProto->ItemId, 1, bot, false, 0, true);
if (!pItem)
return ITEM_USAGE_NONE;
uint16 dest;
InventoryResult result = botAI->CanEquipItem(NULL_SLOT, dest, pItem, true, true);
pItem->RemoveFromUpdateQueueOf(bot);
delete pItem;
if (result != EQUIP_ERR_OK && result != EQUIP_ERR_CANT_CARRY_MORE_OF_THIS)
return ITEM_USAGE_NONE;
// Check if unique items are equipped or not
bool needToCheckUnique = result == EQUIP_ERR_CANT_CARRY_MORE_OF_THIS ||
itemProto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE);
if (needToCheckUnique)
{
// Count the total number of the item (equipped + in bags)
uint32 totalItemCount = bot->GetItemCount(itemProto->ItemId, true);
// Count the number of the item in bags only
uint32 bagItemCount = bot->GetItemCount(itemProto->ItemId, false);
// Determine if the unique item is already equipped
bool isEquipped = (totalItemCount > bagItemCount);
if (isEquipped)
return ITEM_USAGE_NONE; // Item is already equipped
// If not equipped, continue processing
}
if (itemProto->Class == ITEM_CLASS_QUIVER && bot->getClass() != CLASS_HUNTER)
return ITEM_USAGE_NONE;
if (itemProto->Class == ITEM_CLASS_CONTAINER)
{
if (itemProto->SubClass != ITEM_SUBCLASS_CONTAINER)
return ITEM_USAGE_NONE; // Todo add logic for non-bag containers. We want to look at professions/class and
// only replace if non-bag is larger than bag.
if (GetSmallestBagSize() >= itemProto->ContainerSlots)
return ITEM_USAGE_NONE;
return ITEM_USAGE_EQUIP;
}
if (itemProto->Class == ITEM_CLASS_WEAPON && itemProto->SubClass == ITEM_SUBCLASS_WEAPON_MISC)
return ITEM_USAGE_NONE;
bool shouldEquip = false;
// uint32 statWeight = sRandomItemMgr.GetLiveStatWeight(bot, itemProto->ItemId);
StatsWeightCalculator calculator(bot);
calculator.SetItemSetBonus(false);
calculator.SetOverflowPenalty(false);
// Apply PvP weights if the bot is specced for PvP
bool isPvp = sRandomPlayerbotMgr.IsSpecPvp(bot->GetGUID().GetCounter(), bot->getClass());
if (isPvp)
calculator.SetPvpSpec(true);
float itemScore = calculator.CalculateItem(itemProto->ItemId, randomPropertyId);
if (itemScore)
shouldEquip = true;
if (itemProto->Class == ITEM_CLASS_WEAPON && !sRandomItemMgr.CanEquipWeapon(bot->getClass(), itemProto))
shouldEquip = false;
if (itemProto->Class == ITEM_CLASS_ARMOR &&
!sRandomItemMgr.CanEquipArmor(bot->getClass(), bot->GetLevel(), itemProto))
shouldEquip = false;
uint8 possibleSlots = 1;
uint8 dstSlot = botAI->FindEquipSlot(itemProto, NULL_SLOT, true);
// Check if dest wasn't set correctly by CanEquipItem and use FindEquipSlot instead
// This occurs with unique items that are already in the bots bags when CanEquipItem is called
if (dest == 0 && dstSlot != NULL_SLOT)
{
// Construct dest from dstSlot
dest = (INVENTORY_SLOT_BAG_0 << 8) | dstSlot;
}
if (dstSlot == EQUIPMENT_SLOT_FINGER1 || dstSlot == EQUIPMENT_SLOT_TRINKET1)
possibleSlots = 2;
// Check weapon case separately to keep things a bit cleaner
bool have2HWeapon = false;
bool isValidTGWeapon = false;
if (dstSlot == EQUIPMENT_SLOT_MAINHAND)
{
Item* currentWeapon = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND);
have2HWeapon = currentWeapon && currentWeapon->GetTemplate()->InventoryType == INVTYPE_2HWEAPON;
// Determine if the new weapon is a valid Titan Grip weapon
isValidTGWeapon = (itemProto->SubClass == ITEM_SUBCLASS_WEAPON_AXE2 ||
itemProto->SubClass == ITEM_SUBCLASS_WEAPON_MACE2 ||
itemProto->SubClass == ITEM_SUBCLASS_WEAPON_SWORD2);
// If the bot can Titan Grip, ignore any 2H weapon that isn't a 2H sword, mace, or axe.
// If this weapon is 2H but not one of the valid TG weapon types, do not equip it at all.
if (bot->CanTitanGrip() && itemProto->InventoryType == INVTYPE_2HWEAPON && !isValidTGWeapon)
return ITEM_USAGE_NONE;
// Now handle the logic for equipping and possible offhand slots
// If the bot can Dual Wield and:
// - The weapon is not 2H and we currently don't have a 2H weapon equipped
// OR
// - The bot can Titan Grip and it is a valid TG weapon
// Then we can consider the offhand slot as well.
if (bot->CanDualWield() &&
((itemProto->InventoryType != INVTYPE_2HWEAPON && !have2HWeapon) ||
(bot->CanTitanGrip() && isValidTGWeapon)))
{
possibleSlots = 2;
}
}
for (uint8 i = 0; i < possibleSlots; i++)
{
bool shouldEquipInSlot = shouldEquip;
Item* oldItem = bot->GetItemByPos(dest + i);
// No item equipped
if (!oldItem)
{
if (shouldEquipInSlot)
return ITEM_USAGE_EQUIP;
else
return ITEM_USAGE_BAD_EQUIP;
}
ItemTemplate const* oldItemProto = oldItem->GetTemplate();
float oldScore = calculator.CalculateItem(oldItemProto->ItemId, oldItem->GetInt32Value(ITEM_FIELD_RANDOM_PROPERTIES_ID));
if (oldItem)
{
// uint32 oldStatWeight = sRandomItemMgr.GetLiveStatWeight(bot, oldItemProto->ItemId);
if (itemScore || oldScore)
shouldEquipInSlot = itemScore > oldScore * sPlayerbotAIConfig.equipUpgradeThreshold;
}
// Bigger quiver
if (itemProto->Class == ITEM_CLASS_QUIVER)
{
if (!oldItem || oldItemProto->ContainerSlots < itemProto->ContainerSlots)
return ITEM_USAGE_EQUIP;
else
return ITEM_USAGE_NONE;
}
bool existingShouldEquip = true;
if (oldItemProto->Class == ITEM_CLASS_WEAPON && !sRandomItemMgr.CanEquipWeapon(bot->getClass(), oldItemProto))
existingShouldEquip = false;
if (oldItemProto->Class == ITEM_CLASS_ARMOR &&
!sRandomItemMgr.CanEquipArmor(bot->getClass(), bot->GetLevel(), oldItemProto))
existingShouldEquip = false;
// uint32 oldItemPower = sRandomItemMgr.GetLiveStatWeight(bot, oldItemProto->ItemId);
// uint32 newItemPower = sRandomItemMgr.GetLiveStatWeight(bot, itemProto->ItemId);
// Compare items based on item level, quality or itemId.
bool isBetter = false;
if (itemScore > oldScore)
isBetter = true;
// else if (newItemPower == oldScore && itemProto->Quality > oldItemProto->Quality)
// isBetter = true;
// else if (newItemPower == oldScore && itemProto->Quality == oldItemProto->Quality && itemProto->ItemId >
// oldItemProto->ItemId)
// isBetter = true;
Item* item = CurrentItem(itemProto);
bool itemIsBroken =
item && item->GetUInt32Value(ITEM_FIELD_DURABILITY) == 0 && item->GetUInt32Value(ITEM_FIELD_MAXDURABILITY) > 0;
bool oldItemIsBroken =
oldItem->GetUInt32Value(ITEM_FIELD_DURABILITY) == 0 && oldItem->GetUInt32Value(ITEM_FIELD_MAXDURABILITY) > 0;
if (itemProto->ItemId != oldItemProto->ItemId && (shouldEquipInSlot || !existingShouldEquip) && isBetter)
{
switch (itemProto->Class)
{
case ITEM_CLASS_ARMOR:
if (oldItemProto->SubClass <= itemProto->SubClass)
{
// Need to add some logic to check second slot before returning, but as it happens, all three of these
// return vals will result in an attempted equip action so it wouldn't have much effect currently
if (itemIsBroken && !oldItemIsBroken)
return ITEM_USAGE_BROKEN_EQUIP;
else if (shouldEquipInSlot)
return ITEM_USAGE_REPLACE;
else
return ITEM_USAGE_BAD_EQUIP;
break;
}
default:
{
if (itemIsBroken && !oldItemIsBroken)
return ITEM_USAGE_BROKEN_EQUIP;
else if (shouldEquipInSlot)
return ITEM_USAGE_EQUIP;
else
return ITEM_USAGE_BAD_EQUIP;
}
}
}
// Item is not better but current item is broken and new one is not.
if (oldItemIsBroken && !itemIsBroken)
return ITEM_USAGE_EQUIP;
}
return ITEM_USAGE_NONE;
}
ItemUsage ItemUsageValue::QueryItemUsageForAmmo(ItemTemplate const* proto)
{
if (bot->getClass() != CLASS_HUNTER || bot->getClass() != CLASS_ROGUE || bot->getClass() != CLASS_WARRIOR)
return ITEM_USAGE_NONE;
Item* rangedWeapon = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED);
uint32 requiredSubClass = 0;
if (rangedWeapon)
{
switch (rangedWeapon->GetTemplate()->SubClass)
{
case ITEM_SUBCLASS_WEAPON_GUN:
requiredSubClass = ITEM_SUBCLASS_BULLET;
break;
case ITEM_SUBCLASS_WEAPON_BOW:
case ITEM_SUBCLASS_WEAPON_CROSSBOW:
requiredSubClass = ITEM_SUBCLASS_ARROW;
break;
}
}
// Ensure the item is the correct ammo type for the equipped ranged weapon
if (proto->SubClass == requiredSubClass)
{
float ammoCount = BetterStacks(proto, "ammo");
float requiredAmmo = (bot->getClass() == CLASS_HUNTER) ? 8 : 2; // Hunters get 8 stacks, others 2
uint32 currentAmmoId = bot->GetUInt32Value(PLAYER_AMMO_ID);
// Check if the bot has an ammo type assigned
if (currentAmmoId == 0)
return ITEM_USAGE_EQUIP; // Equip the ammo if no ammo
// Compare new ammo vs current equipped ammo
ItemTemplate const* currentAmmoProto = sObjectMgr->GetItemTemplate(currentAmmoId);
if (currentAmmoProto)
{
uint32 currentAmmoDPS = (currentAmmoProto->Damage[0].DamageMin + currentAmmoProto->Damage[0].DamageMax) * 1000 / 2;
uint32 newAmmoDPS = (proto->Damage[0].DamageMin + proto->Damage[0].DamageMax) * 1000 / 2;
if (newAmmoDPS > currentAmmoDPS) // New ammo meets upgrade condition
return ITEM_USAGE_EQUIP;
if (newAmmoDPS < currentAmmoDPS) // New ammo is worse
return ITEM_USAGE_NONE;
}
// Ensure we have enough ammo in the inventory
if (ammoCount < requiredAmmo)
{
ammoCount += CurrentStacks(proto);
if (ammoCount < requiredAmmo) // Buy ammo to reach the proper supply
return ITEM_USAGE_AMMO;
else if (ammoCount < requiredAmmo + 1)
return ITEM_USAGE_KEEP; // Keep the ammo if we don't have too much.
}
}
return ITEM_USAGE_NONE;
}
ParsedItemUsage ItemUsageValue::GetItemIdFromQualifier()
{
ParsedItemUsage parsed;
size_t const pos = qualifier.find(",");
if (pos != std::string::npos)
{
parsed.itemId = atoi(qualifier.substr(0, pos).c_str());
parsed.randomPropertyId = atoi(qualifier.substr(pos + 1).c_str());
return parsed;
}
else
parsed.itemId = atoi(qualifier.c_str());
return parsed;
}
// Return smaltest bag size equipped
uint32 ItemUsageValue::GetSmallestBagSize()
{
int8 curSlot = 0;
uint32 curSlots = 0;
for (uint8 bag = INVENTORY_SLOT_BAG_START + 1; bag < INVENTORY_SLOT_BAG_END; ++bag)
{
if (Bag const* pBag = (Bag*)bot->GetItemByPos(INVENTORY_SLOT_BAG_0, bag))
{
if (curSlot > 0 && curSlots < pBag->GetBagSize())
continue;
curSlot = pBag->GetSlot();
curSlots = pBag->GetBagSize();
}
else
return 0;
}
return curSlots;
}
bool ItemUsageValue::IsItemUsefulForQuest(Player* player, ItemTemplate const* proto)
{
PlayerbotAI* botAI = GET_PLAYERBOT_AI(player);
if (!botAI)
return false;
for (uint8 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot)
{
uint32 entry = player->GetQuestSlotQuestId(slot);
Quest const* quest = sObjectMgr->GetQuestTemplate(entry);
if (!quest)
continue;
// Check if the item itself is needed for the quest
for (uint8 i = 0; i < 4; i++)
{
if (quest->RequiredItemId[i] == proto->ItemId)
{
if (player->GetItemCount(proto->ItemId, false) >= quest->RequiredItemCount[i])
continue;
return true; // Item is directly required for a quest
}
}
// Check if the item has spells that create a required quest item
for (uint8 i = 0; i < MAX_ITEM_SPELLS; i++)
{
uint32 spellId = proto->Spells[i].SpellId;
if (!spellId)
continue;
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId);
if (!spellInfo)
continue;
for (uint8 effectIndex = 0; effectIndex < MAX_SPELL_EFFECTS; effectIndex++)
{
if (spellInfo->Effects[effectIndex].Effect == SPELL_EFFECT_CREATE_ITEM)
{
uint32 createdItemId = spellInfo->Effects[effectIndex].ItemType;
// Check if the created item is required for a quest
for (uint8 j = 0; j < 4; j++)
{
if (quest->RequiredItemId[j] == createdItemId)
{
if (player->GetItemCount(createdItemId, false) >= quest->RequiredItemCount[j])
continue;
return true; // Item is useful because it creates a required quest item
}
}
}
}
}
}
return false; // Item is not useful for any active quests
}
bool ItemUsageValue::IsItemNeededForSkill(ItemTemplate const* proto)
{
switch (proto->ItemId)
{
case 756: // Tunnel Pick
return botAI->HasSkill(SKILL_MINING);
case 778: // Kobold Excavation Pick
return botAI->HasSkill(SKILL_MINING);
case 1819: // Gouging Pick
return botAI->HasSkill(SKILL_MINING);
case 1893: // Miner's Revenge
return botAI->HasSkill(SKILL_MINING);
case 1959: // Cold Iron Pick
return botAI->HasSkill(SKILL_MINING);
case 2901: // Mining Pick
return botAI->HasSkill(SKILL_MINING);
case 9465: // Digmaster 5000
return botAI->HasSkill(SKILL_MINING);
case 20723: // Brann's Trusty Pick
return botAI->HasSkill(SKILL_MINING);
case 40772: // Gnomish Army Knife
return botAI->HasSkill(SKILL_MINING) || botAI->HasSkill(SKILL_ENGINEERING) || botAI->HasSkill(SKILL_BLACKSMITHING) || botAI->HasSkill(SKILL_COOKING) || botAI->HasSkill(SKILL_SKINNING);
case 40892: // Hammer Pick
return botAI->HasSkill(SKILL_MINING) || botAI->HasSkill(SKILL_BLACKSMITHING);
case 40893: // Bladed Pickaxe
return botAI->HasSkill(SKILL_MINING) || botAI->HasSkill(SKILL_SKINNING);
case 5956: // Blacksmith Hammer
return botAI->HasSkill(SKILL_BLACKSMITHING) || botAI->HasSkill(SKILL_ENGINEERING);
case 6219: // Arclight Spanner
return botAI->HasSkill(SKILL_ENGINEERING);
case 6218: // Runed copper rod
return botAI->HasSkill(SKILL_ENCHANTING);
case 6339: // Runed silver rod
return botAI->HasSkill(SKILL_ENCHANTING);
case 11130: // Runed golden rod
return botAI->HasSkill(SKILL_ENCHANTING);
case 11145: // Runed truesilver rod
return botAI->HasSkill(SKILL_ENCHANTING);
case 16207: // Runed Arcanite Rod
return botAI->HasSkill(SKILL_ENCHANTING);
case 7005: // Skinning Knife
return botAI->HasSkill(SKILL_SKINNING);
case 12709:
return botAI->HasSkill(SKILL_SKINNING);
case 19901:
return botAI->HasSkill(SKILL_SKINNING);
case 4471: // Flint and Tinder
return botAI->HasSkill(SKILL_COOKING);
case 4470: // Simple Wood
return botAI->HasSkill(SKILL_COOKING);
case 6256: // Fishing Rod
return botAI->HasSkill(SKILL_FISHING);
}
return false;
}
bool ItemUsageValue::IsItemUsefulForSkill(ItemTemplate const* proto)
{
switch (proto->Class)
{
case ITEM_CLASS_TRADE_GOODS:
case ITEM_CLASS_MISC:
case ITEM_CLASS_REAGENT:
case ITEM_CLASS_GEM:
{
if (botAI->HasSkill(SKILL_TAILORING) && RandomItemMgr::IsUsedBySkill(proto, SKILL_TAILORING))
return true;
if (botAI->HasSkill(SKILL_LEATHERWORKING) && RandomItemMgr::IsUsedBySkill(proto, SKILL_LEATHERWORKING))
return true;
if (botAI->HasSkill(SKILL_ENGINEERING) && RandomItemMgr::IsUsedBySkill(proto, SKILL_ENGINEERING))
return true;
if (botAI->HasSkill(SKILL_BLACKSMITHING) && RandomItemMgr::IsUsedBySkill(proto, SKILL_BLACKSMITHING))
return true;
if (botAI->HasSkill(SKILL_ALCHEMY) && RandomItemMgr::IsUsedBySkill(proto, SKILL_ALCHEMY))
return true;
if (botAI->HasSkill(SKILL_ENCHANTING) && RandomItemMgr::IsUsedBySkill(proto, SKILL_ENCHANTING))
return true;
if (botAI->HasSkill(SKILL_FISHING) && RandomItemMgr::IsUsedBySkill(proto, SKILL_FISHING))
return true;
if (botAI->HasSkill(SKILL_FIRST_AID) && RandomItemMgr::IsUsedBySkill(proto, SKILL_FIRST_AID))
return true;
if (botAI->HasSkill(SKILL_COOKING) && RandomItemMgr::IsUsedBySkill(proto, SKILL_COOKING))
return true;
if (botAI->HasSkill(SKILL_JEWELCRAFTING) && RandomItemMgr::IsUsedBySkill(proto, SKILL_JEWELCRAFTING))
return true;
if (botAI->HasSkill(SKILL_MINING) && (RandomItemMgr::IsUsedBySkill(proto, SKILL_MINING) ||
RandomItemMgr::IsUsedBySkill(proto, SKILL_BLACKSMITHING) ||
RandomItemMgr::IsUsedBySkill(proto, SKILL_JEWELCRAFTING) ||
RandomItemMgr::IsUsedBySkill(proto, SKILL_ENGINEERING)))
return true;
if (botAI->HasSkill(SKILL_SKINNING) && (RandomItemMgr::IsUsedBySkill(proto, SKILL_SKINNING) ||
RandomItemMgr::IsUsedBySkill(proto, SKILL_LEATHERWORKING)))
return true;
if (botAI->HasSkill(SKILL_HERBALISM) && (RandomItemMgr::IsUsedBySkill(proto, SKILL_HERBALISM) ||
RandomItemMgr::IsUsedBySkill(proto, SKILL_ALCHEMY)))
return true;
return false;
}
case ITEM_CLASS_RECIPE:
{
if (bot->HasSpell(proto->Spells[2].SpellId))
break;
switch (proto->SubClass)
{
case ITEM_SUBCLASS_LEATHERWORKING_PATTERN:
return botAI->HasSkill(SKILL_LEATHERWORKING);
case ITEM_SUBCLASS_TAILORING_PATTERN:
return botAI->HasSkill(SKILL_TAILORING);
case ITEM_SUBCLASS_ENGINEERING_SCHEMATIC:
return botAI->HasSkill(SKILL_ENGINEERING);
case ITEM_SUBCLASS_BLACKSMITHING:
return botAI->HasSkill(SKILL_BLACKSMITHING);
case ITEM_SUBCLASS_COOKING_RECIPE:
return botAI->HasSkill(SKILL_COOKING);
case ITEM_SUBCLASS_ALCHEMY_RECIPE:
return botAI->HasSkill(SKILL_ALCHEMY);
case ITEM_SUBCLASS_FIRST_AID_MANUAL:
return botAI->HasSkill(SKILL_FIRST_AID);
case ITEM_SUBCLASS_ENCHANTING_FORMULA:
return botAI->HasSkill(SKILL_ENCHANTING);
case ITEM_SUBCLASS_FISHING_MANUAL:
return botAI->HasSkill(SKILL_FISHING);
}
}
}
return false;
}
bool ItemUsageValue::IsItemNeededForUsefullSpell(ItemTemplate const* proto, bool checkAllReagents)
{
for (auto spellId : SpellsUsingItem(proto->ItemId, bot))
{
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId);
if (!spellInfo)
continue;
if (checkAllReagents && !HasItemsNeededForSpell(spellId, proto))
continue;
if (SpellGivesSkillUp(spellId, bot))
return true;
uint32 newItemId = spellInfo->Effects[EFFECT_0].ItemType;
if (newItemId && newItemId != proto->ItemId)
{
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", newItemId);
if (usage != ITEM_USAGE_REPLACE && usage != ITEM_USAGE_EQUIP && usage != ITEM_USAGE_AMMO &&
usage != ITEM_USAGE_QUEST && usage != ITEM_USAGE_SKILL && usage != ITEM_USAGE_USE)
continue;
return true;
}
}
return false;
}
bool ItemUsageValue::HasItemsNeededForSpell(uint32 spellId, ItemTemplate const* proto)
{
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId);
if (!spellInfo)
return false;
for (uint8 i = 0; i < MAX_SPELL_REAGENTS; i++)
if (spellInfo->ReagentCount[i] > 0 && spellInfo->Reagent[i])
{
if (proto && proto->ItemId == spellInfo->Reagent[i] &&
spellInfo->ReagentCount[i] == 1) // If we only need 1 item then current item does not need to be
// checked since we are looting/buying or already have it.
continue;
ItemTemplate const* reqProto = sObjectMgr->GetItemTemplate(spellInfo->Reagent[i]);
uint32 count = AI_VALUE2(uint32, "item count", reqProto->Name1);
if (count < spellInfo->ReagentCount[i])
return false;
}
return true;
}
Item* ItemUsageValue::CurrentItem(ItemTemplate const* proto)
{
Item* bestItem = nullptr;
std::vector<Item*> found = AI_VALUE2(std::vector<Item*>, "inventory items", chat->FormatItem(proto));
for (auto item : found)
{
if (bestItem && item->GetUInt32Value(ITEM_FIELD_DURABILITY) < bestItem->GetUInt32Value(ITEM_FIELD_DURABILITY))
continue;
if (bestItem && item->GetCount() < bestItem->GetCount())
continue;
bestItem = item;
}
return bestItem;
}
float ItemUsageValue::CurrentStacks(ItemTemplate const* proto)
{
uint32 maxStack = proto->GetMaxStackSize();
std::vector<Item*> found = AI_VALUE2(std::vector<Item*>, "inventory items", chat->FormatItem(proto));
float itemCount = 0;
for (auto stack : found)
{
itemCount += stack->GetCount();
}
return itemCount / maxStack;
}
float ItemUsageValue::BetterStacks(ItemTemplate const* proto, std::string const itemType)
{
std::vector<Item*> items = AI_VALUE2(std::vector<Item*>, "inventory items", itemType);
float stacks = 0;
for (auto& otherItem : items)
{
ItemTemplate const* otherProto = otherItem->GetTemplate();
if (otherProto->Class != proto->Class || otherProto->SubClass != proto->SubClass)
continue;
if (otherProto->ItemLevel < proto->ItemLevel)
continue;
if (otherProto->ItemId == proto->ItemId)
continue;
stacks += CurrentStacks(otherProto);
}
return stacks;
}
std::vector<uint32> ItemUsageValue::SpellsUsingItem(uint32 itemId, Player* bot)
{
std::vector<uint32> retSpells;
PlayerSpellMap const& spellMap = bot->GetSpellMap();
for (auto& spell : spellMap)
{
uint32 spellId = spell.first;
if (spell.second->State == PLAYERSPELL_REMOVED || !spell.second->Active)
continue;
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId);
if (!spellInfo)
continue;
if (spellInfo->IsPassive())
continue;
if (spellInfo->Effects[EFFECT_0].Effect != SPELL_EFFECT_CREATE_ITEM)
continue;
for (uint8 i = 0; i < MAX_SPELL_REAGENTS; i++)
if (spellInfo->ReagentCount[i] > 0 && spellInfo->Reagent[i] == itemId)
retSpells.push_back(spellId);
}
return retSpells;
}
inline int32 SkillGainChance(uint32 SkillValue, uint32 GrayLevel, uint32 GreenLevel, uint32 YellowLevel)
{
if (SkillValue >= GrayLevel)
return sWorld->getIntConfig(CONFIG_SKILL_CHANCE_GREY) * 10;
if (SkillValue >= GreenLevel)
return sWorld->getIntConfig(CONFIG_SKILL_CHANCE_GREEN) * 10;
if (SkillValue >= YellowLevel)
return sWorld->getIntConfig(CONFIG_SKILL_CHANCE_YELLOW) * 10;
return sWorld->getIntConfig(CONFIG_SKILL_CHANCE_ORANGE) * 10;
}
bool ItemUsageValue::SpellGivesSkillUp(uint32 spellId, Player* bot)
{
SkillLineAbilityMapBounds bounds = sSpellMgr->GetSkillLineAbilityMapBounds(spellId);
for (SkillLineAbilityMap::const_iterator _spell_idx = bounds.first; _spell_idx != bounds.second; ++_spell_idx)
{
SkillLineAbilityEntry const* skill = _spell_idx->second;
if (skill->SkillLine)
{
uint32 SkillValue = bot->GetPureSkillValue(skill->SkillLine);
if (SkillGainChance(SkillValue, skill->TrivialSkillLineRankHigh,
(skill->TrivialSkillLineRankHigh + skill->TrivialSkillLineRankLow) / 2,
skill->TrivialSkillLineRankLow) > 0)
return true;
}
}
return false;
}
std::string const ItemUsageValue::GetConsumableType(ItemTemplate const* proto, bool hasMana)
{
std::string const foodType = "";
if ((proto->SubClass == ITEM_SUBCLASS_CONSUMABLE || proto->SubClass == ITEM_SUBCLASS_FOOD))
{
if (proto->Spells[0].SpellCategory == 11)
return "food";
else if (proto->Spells[0].SpellCategory == 59 && hasMana)
return "drink";
}
if (proto->SubClass == ITEM_SUBCLASS_POTION || proto->SubClass == ITEM_SUBCLASS_FLASK)
{
for (int j = 0; j < MAX_ITEM_PROTO_SPELLS; j++)
{
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(proto->Spells[j].SpellId);
if (spellInfo)
for (int i = 0; i < 3; i++)
{
if (spellInfo->Effects[i].Effect == SPELL_EFFECT_ENERGIZE && hasMana)
return "mana potion";
if (spellInfo->Effects[i].Effect == SPELL_EFFECT_HEAL)
return "healing potion";
}
}
}
if (proto->SubClass == ITEM_SUBCLASS_BANDAGE)
{
return "bandage";
}
return "";
}
ItemUsage ItemUpgradeValue::Calculate()
{
ParsedItemUsage parsed = GetItemIdFromQualifier();
uint32 itemId = parsed.itemId;
uint32 randomPropertyId = parsed.randomPropertyId;
if (!itemId)
return ITEM_USAGE_NONE;
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId);
if (!proto)
return ITEM_USAGE_NONE;
ItemUsage equip = QueryItemUsageForEquip(proto, randomPropertyId);
if (equip != ITEM_USAGE_NONE)
return equip;
if (proto->Class == ITEM_CLASS_PROJECTILE && bot->CanUseItem(proto) == EQUIP_ERR_OK)
return QueryItemUsageForAmmo(proto);
return ITEM_USAGE_NONE;
}