diff --git a/src/PlayerbotAI.cpp b/src/PlayerbotAI.cpp index 58979abc..e1ad040d 100644 --- a/src/PlayerbotAI.cpp +++ b/src/PlayerbotAI.cpp @@ -4867,6 +4867,40 @@ Item* PlayerbotAI::FindPoison() const { return pItemProto->Class == ITEM_CLASS_CONSUMABLE && pItemProto->SubClass == 6; }); } +// Find Ammo +Item* PlayerbotAI::FindAmmo() const +{ + // Get equipped ranged weapon + if (Item* rangedWeapon = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED)) + { + uint32 weaponSubClass = rangedWeapon->GetTemplate()->SubClass; + uint32 requiredAmmoType = 0; + + // Determine the correct ammo type based on the weapon + switch (weaponSubClass) + { + case ITEM_SUBCLASS_WEAPON_GUN: + requiredAmmoType = ITEM_SUBCLASS_BULLET; + break; + case ITEM_SUBCLASS_WEAPON_BOW: + case ITEM_SUBCLASS_WEAPON_CROSSBOW: + requiredAmmoType = ITEM_SUBCLASS_ARROW; + break; + default: + return nullptr; // Not a ranged weapon that requires ammo + } + + // Search inventory for the correct ammo type + return FindItemInInventory([requiredAmmoType](ItemTemplate const* pItemProto) -> bool + { + return pItemProto->Class == ITEM_CLASS_PROJECTILE && + pItemProto->SubClass == requiredAmmoType; + }); + } + + return nullptr; // No ranged weapon equipped +} + // Find Consumable Item* PlayerbotAI::FindConsumable(uint32 displayId) const { diff --git a/src/PlayerbotAI.h b/src/PlayerbotAI.h index f9cfbadd..717be94b 100644 --- a/src/PlayerbotAI.h +++ b/src/PlayerbotAI.h @@ -462,6 +462,7 @@ public: bool PlayEmote(uint32 emote); void Ping(float x, float y); Item* FindPoison() const; + Item* FindAmmo() const; Item* FindBandage() const; Item* FindConsumable(uint32 displayId) const; Item* FindStoneFor(Item* weapon) const; diff --git a/src/strategy/actions/BuyAction.cpp b/src/strategy/actions/BuyAction.cpp index 18587346..d5e2505b 100644 --- a/src/strategy/actions/BuyAction.cpp +++ b/src/strategy/actions/BuyAction.cpp @@ -11,6 +11,7 @@ #include "ItemUsageValue.h" #include "ItemVisitors.h" #include "Playerbots.h" +#include "StatsWeightCalculator.h" bool BuyAction::Execute(Event event) { @@ -61,30 +62,87 @@ bool BuyAction::Execute(Event event) if (m_items_sorted.empty()) continue; - std::sort(m_items_sorted.begin(), m_items_sorted.end(), - [](VendorItem* i, VendorItem* j) { - return sObjectMgr->GetItemTemplate(i->item)->ItemLevel > - sObjectMgr->GetItemTemplate(j->item)->ItemLevel; - }); + 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 bestPurchasedItemScore; // Track best item score per InventoryType + for (auto& tItem : m_items_sorted) { - for (uint32 i = 0; i < 10; i++) // Buy 10 times or until no longer usefull/possible + 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); - ItemTemplate const* proto = sObjectMgr->GetItemTemplate(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; - - // reputation discount 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: @@ -112,11 +170,12 @@ bool BuyAction::Execute(Event event) if (!BuyItem(tItems, vendorguid, proto)) break; - if (usage == ITEM_USAGE_REPLACE || - usage == ITEM_USAGE_EQUIP) // Equip upgrades and stop buying this time. + // Store the best item score per InventoryType + bestPurchasedItemScore[invType] = newScore; + + if (needMoneyFor == NeedMoneyFor::gear) { botAI->DoSpecificAction("equip upgrades"); - break; } } } @@ -140,6 +199,15 @@ bool BuyAction::Execute(Event event) 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"); + break; } } } diff --git a/src/strategy/hunter/GenericHunterNonCombatStrategy.cpp b/src/strategy/hunter/GenericHunterNonCombatStrategy.cpp index e64a9d78..203c2fea 100644 --- a/src/strategy/hunter/GenericHunterNonCombatStrategy.cpp +++ b/src/strategy/hunter/GenericHunterNonCombatStrategy.cpp @@ -51,6 +51,8 @@ void GenericHunterNonCombatStrategy::InitTriggers(std::vector& tri new TriggerNode("low ammo", NextAction::array(0, new NextAction("say::low ammo", ACTION_NORMAL), nullptr))); triggers.push_back( new TriggerNode("no track", NextAction::array(0, new NextAction("track humanoids", ACTION_NORMAL), nullptr))); + triggers.push_back(new TriggerNode("no ammo", + NextAction::array(0, new NextAction("equip upgrades", ACTION_HIGH + 1), nullptr))); // triggers.push_back(new TriggerNode("no ammo", NextAction::array(0, new NextAction("switch to melee", // ACTION_NORMAL + 1), new NextAction("say::no ammo", ACTION_NORMAL), nullptr))); triggers.push_back(new // TriggerNode("has ammo", NextAction::array(0, new NextAction("switch to ranged", ACTION_NORMAL), nullptr))); diff --git a/src/strategy/hunter/GenericHunterStrategy.cpp b/src/strategy/hunter/GenericHunterStrategy.cpp index 9113e40f..e48b5011 100644 --- a/src/strategy/hunter/GenericHunterStrategy.cpp +++ b/src/strategy/hunter/GenericHunterStrategy.cpp @@ -102,8 +102,8 @@ void GenericHunterStrategy::InitTriggers(std::vector& triggers) new TriggerNode("medium threat", NextAction::array(0, new NextAction("feign death", 35.0f), nullptr))); triggers.push_back(new TriggerNode("hunters pet medium health", NextAction::array(0, new NextAction("mend pet", ACTION_HIGH + 2), nullptr))); - // triggers.push_back(new TriggerNode("no ammo", NextAction::array(0, new NextAction("switch to melee", ACTION_HIGH - // + 1), new NextAction("say::no ammo", ACTION_HIGH), nullptr))); + triggers.push_back(new TriggerNode("no ammo", + NextAction::array(0, new NextAction("equip upgrades", ACTION_HIGH + 9), nullptr))); triggers.push_back(new TriggerNode("aspect of the viper", NextAction::array(0, new NextAction("aspect of the viper", ACTION_HIGH), NULL))); triggers.push_back(new TriggerNode("enemy too close for auto shot", diff --git a/src/strategy/triggers/GenericTriggers.cpp b/src/strategy/triggers/GenericTriggers.cpp index 26572394..ce60ee82 100644 --- a/src/strategy/triggers/GenericTriggers.cpp +++ b/src/strategy/triggers/GenericTriggers.cpp @@ -10,6 +10,7 @@ #include "BattlegroundWS.h" #include "CreatureAI.h" #include "GameTime.h" +#include "ItemVisitors.h" #include "LastSpellCastValue.h" #include "ObjectGuid.h" #include "PlayerbotAIConfig.h" @@ -629,4 +630,15 @@ bool IsFallingFarTrigger::IsActive() { return bot->HasUnitMovementFlag(MOVEMENTF bool HasAreaDebuffTrigger::IsActive() { return AI_VALUE2(bool, "has area debuff", "self target"); } -Value* BuffOnMainTankTrigger::GetTargetValue() { return context->GetValue("main tank", spell); } \ No newline at end of file +Value* BuffOnMainTankTrigger::GetTargetValue() { return context->GetValue("main tank", spell); } + +bool AmmoCountTrigger::IsActive() +{ + if (bot->GetUInt32Value(PLAYER_AMMO_ID) != 0) + return ItemCountTrigger::IsActive(); // Ammo already equipped + + if (botAI->FindAmmo()) + return true; // Found ammo in inventory but not equipped + + return ItemCountTrigger::IsActive(); +} diff --git a/src/strategy/triggers/GenericTriggers.h b/src/strategy/triggers/GenericTriggers.h index ac25dfc4..f7e1fb2a 100644 --- a/src/strategy/triggers/GenericTriggers.h +++ b/src/strategy/triggers/GenericTriggers.h @@ -596,6 +596,7 @@ public: : ItemCountTrigger(botAI, item, count, interval) { } + bool IsActive() override; }; class HasAuraTrigger : public Trigger diff --git a/src/strategy/values/ItemUsageValue.cpp b/src/strategy/values/ItemUsageValue.cpp index fbf9ccda..8af900c1 100644 --- a/src/strategy/values/ItemUsageValue.cpp +++ b/src/strategy/values/ItemUsageValue.cpp @@ -64,8 +64,9 @@ ItemUsage ItemUsageValue::Calculate() if (proto->Class == ITEM_CLASS_KEY) return ITEM_USAGE_USE; - - if (proto->Class == ITEM_CLASS_CONSUMABLE) + + if (proto->Class == ITEM_CLASS_CONSUMABLE && + (proto->MaxCount == 0 || AI_VALUE2(uint32, "item count", proto->Name1) < proto->MaxCount)) { std::string const foodType = GetConsumableType(proto, bot->GetPower(POWER_MANA)); @@ -103,39 +104,67 @@ ItemUsage ItemUsageValue::Calculate() return ITEM_USAGE_QUEST; if (proto->Class == ITEM_CLASS_PROJECTILE && bot->CanUseItem(proto) == EQUIP_ERR_OK) + { if (bot->getClass() == CLASS_HUNTER || bot->getClass() == CLASS_ROGUE || bot->getClass() == CLASS_WARRIOR) { - if (Item* const pItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED)) + Item* rangedWeapon = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED); + uint32 requiredSubClass = 0; + + if (rangedWeapon) { - uint32 subClass = 0; - switch (pItem->GetTemplate()->SubClass) + switch (rangedWeapon->GetTemplate()->SubClass) { case ITEM_SUBCLASS_WEAPON_GUN: - subClass = ITEM_SUBCLASS_BULLET; + requiredSubClass = ITEM_SUBCLASS_BULLET; break; case ITEM_SUBCLASS_WEAPON_BOW: case ITEM_SUBCLASS_WEAPON_CROSSBOW: - subClass = ITEM_SUBCLASS_ARROW; + requiredSubClass = ITEM_SUBCLASS_ARROW; break; } + } - if (proto->SubClass == subClass) + // 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) { - float ammo = BetterStacks(proto, "ammo"); - float needAmmo = (bot->getClass() == CLASS_HUNTER) ? 8 : 2; - - if (ammo < needAmmo) // We already have enough of the current ammo. + 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 { - ammo += CurrentStacks(proto); - - if (ammo < needAmmo) // Buy ammo to get to the proper supply - return ITEM_USAGE_AMMO; - else if (ammo < needAmmo + 1) - return ITEM_USAGE_KEEP; // Keep the ammo until we have too much. + 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. } } } + } // Need to add something like free bagspace or item value. if (proto->SellPrice > 0)