mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
<!-- 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. -->
913 lines
33 KiB
C++
913 lines
33 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 "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 bot’s 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;
|
||
}
|