diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 5ce5485d0..3d0866f44 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -806,6 +806,27 @@ AiPlayerbot.RandomGearQualityLimit = 3 # Default: 0 (no limit) AiPlayerbot.RandomGearScoreLimit = 0 +# Prefer armor of the class's ideal type: apply 3x score multiplier to class-appropriate armor. +# When enabled, Warriors strongly prefer plate, Shamans prefer mail, etc. +# A truly superior item can still win (no hard filtering), but same-quality +# armor of the preferred type will score 3x higher and be equipped instead. +# +# ARMOR TYPE PREFERENCES: +# Plate: Warriors, Paladins, Death Knights +# Mail: Hunters, Shamans +# Leather: Rogues, Druids +# Cloth: Priests, Mages, Warlocks +# +# Default: 0 (disabled) +AiPlayerbot.PreferClassArmorType = 0 + + +# When enabled, bots prefer spec-appropriate weapons based on speed and weapon type during autogear. +# Examples: Arms Warriors favor slow 2H axes/polearms (Axe Specialization), Combat Rogues +# favor a slow MH with a fast OH, and Enhancement Shamans favor synchronized slow 1H weapons. +# Default: 0 (disabled) +AiPlayerbot.PreferredSpecWeapons = 0 + # If disabled, random bots can only upgrade equipment through looting and quests # Default: 1 (enabled) AiPlayerbot.IncrementalGearInit = 1 @@ -1836,12 +1857,24 @@ AiPlayerbot.WorldBuffMatrix = # WARRIOR ARMS 1:0,1,0,80,80:53760,57358; # WARRIO # # +# arms pve AiPlayerbot.RandomClassSpecProb.1.0 = 20 AiPlayerbot.RandomClassSpecIndex.1.0 = 0 +# fury pve AiPlayerbot.RandomClassSpecProb.1.1 = 40 AiPlayerbot.RandomClassSpecIndex.1.1 = 1 +# prot pve AiPlayerbot.RandomClassSpecProb.1.2 = 40 AiPlayerbot.RandomClassSpecIndex.1.2 = 2 +# arms pvp +AiPlayerbot.RandomClassSpecProb.1.3 = 0 +AiPlayerbot.RandomClassSpecIndex.1.3 = 3 +# fury pvp +AiPlayerbot.RandomClassSpecProb.1.4 = 0 +AiPlayerbot.RandomClassSpecIndex.1.4 = 4 +# prot pvp +AiPlayerbot.RandomClassSpecProb.1.5 = 0 +AiPlayerbot.RandomClassSpecIndex.1.5 = 5 # # @@ -1853,12 +1886,24 @@ AiPlayerbot.RandomClassSpecIndex.1.2 = 2 # # +# holy pve AiPlayerbot.RandomClassSpecProb.2.0 = 30 AiPlayerbot.RandomClassSpecIndex.2.0 = 0 +# prot pve AiPlayerbot.RandomClassSpecProb.2.1 = 40 AiPlayerbot.RandomClassSpecIndex.2.1 = 1 +# ret pve AiPlayerbot.RandomClassSpecProb.2.2 = 30 AiPlayerbot.RandomClassSpecIndex.2.2 = 2 +# holy pvp +AiPlayerbot.RandomClassSpecProb.2.3 = 0 +AiPlayerbot.RandomClassSpecIndex.2.3 = 3 +# prot pvp +AiPlayerbot.RandomClassSpecProb.2.4 = 0 +AiPlayerbot.RandomClassSpecIndex.2.4 = 4 +# ret pvp +AiPlayerbot.RandomClassSpecProb.2.5 = 0 +AiPlayerbot.RandomClassSpecIndex.2.5 = 5 # # @@ -1870,12 +1915,24 @@ AiPlayerbot.RandomClassSpecIndex.2.2 = 2 # # +# bm pve AiPlayerbot.RandomClassSpecProb.3.0 = 33 AiPlayerbot.RandomClassSpecIndex.3.0 = 0 +# mm pve AiPlayerbot.RandomClassSpecProb.3.1 = 33 AiPlayerbot.RandomClassSpecIndex.3.1 = 1 +# surv pve AiPlayerbot.RandomClassSpecProb.3.2 = 33 AiPlayerbot.RandomClassSpecIndex.3.2 = 2 +# bm pvp +AiPlayerbot.RandomClassSpecProb.3.3 = 0 +AiPlayerbot.RandomClassSpecIndex.3.3 = 3 +# mm pvp +AiPlayerbot.RandomClassSpecProb.3.4 = 0 +AiPlayerbot.RandomClassSpecIndex.3.4 = 4 +# surv pvp +AiPlayerbot.RandomClassSpecProb.3.5 = 0 +AiPlayerbot.RandomClassSpecIndex.3.5 = 5 # # @@ -1887,12 +1944,24 @@ AiPlayerbot.RandomClassSpecIndex.3.2 = 2 # # +# as pve AiPlayerbot.RandomClassSpecProb.4.0 = 45 AiPlayerbot.RandomClassSpecIndex.4.0 = 0 +# combat pve AiPlayerbot.RandomClassSpecProb.4.1 = 45 AiPlayerbot.RandomClassSpecIndex.4.1 = 1 +# subtlety pve AiPlayerbot.RandomClassSpecProb.4.2 = 10 AiPlayerbot.RandomClassSpecIndex.4.2 = 2 +# as pvp +AiPlayerbot.RandomClassSpecProb.4.3 = 0 +AiPlayerbot.RandomClassSpecIndex.4.3 = 3 +# combat pvp +AiPlayerbot.RandomClassSpecProb.4.4 = 0 +AiPlayerbot.RandomClassSpecIndex.4.4 = 4 +# subtlety pvp +AiPlayerbot.RandomClassSpecProb.4.5 = 0 +AiPlayerbot.RandomClassSpecIndex.4.5 = 5 # # @@ -1904,12 +1973,24 @@ AiPlayerbot.RandomClassSpecIndex.4.2 = 2 # # +# disc pve AiPlayerbot.RandomClassSpecProb.5.0 = 40 AiPlayerbot.RandomClassSpecIndex.5.0 = 0 +# holy pve AiPlayerbot.RandomClassSpecProb.5.1 = 35 AiPlayerbot.RandomClassSpecIndex.5.1 = 1 +# shadow pve AiPlayerbot.RandomClassSpecProb.5.2 = 25 AiPlayerbot.RandomClassSpecIndex.5.2 = 2 +# disc pvp +AiPlayerbot.RandomClassSpecProb.5.3 = 0 +AiPlayerbot.RandomClassSpecIndex.5.3 = 3 +# holy pvp +AiPlayerbot.RandomClassSpecProb.5.4 = 0 +AiPlayerbot.RandomClassSpecIndex.5.4 = 4 +# shadow pvp +AiPlayerbot.RandomClassSpecProb.5.5 = 0 +AiPlayerbot.RandomClassSpecIndex.5.5 = 5 # # @@ -1921,12 +2002,27 @@ AiPlayerbot.RandomClassSpecIndex.5.2 = 2 # # +# blood pve AiPlayerbot.RandomClassSpecProb.6.0 = 30 AiPlayerbot.RandomClassSpecIndex.6.0 = 0 +# frost pve AiPlayerbot.RandomClassSpecProb.6.1 = 40 AiPlayerbot.RandomClassSpecIndex.6.1 = 1 +# unholy pve AiPlayerbot.RandomClassSpecProb.6.2 = 30 AiPlayerbot.RandomClassSpecIndex.6.2 = 2 +# double aura blood pve +AiPlayerbot.RandomClassSpecProb.6.3 = 0 +AiPlayerbot.RandomClassSpecIndex.6.3 = 3 +# blood pvp +AiPlayerbot.RandomClassSpecProb.6.4 = 0 +AiPlayerbot.RandomClassSpecIndex.6.4 = 4 +# frost pvp +AiPlayerbot.RandomClassSpecProb.6.5 = 0 +AiPlayerbot.RandomClassSpecIndex.6.5 = 5 +# unholy pvp +AiPlayerbot.RandomClassSpecProb.6.6 = 0 +AiPlayerbot.RandomClassSpecIndex.6.6 = 6 # # @@ -1938,12 +2034,24 @@ AiPlayerbot.RandomClassSpecIndex.6.2 = 2 # # +# ele pve AiPlayerbot.RandomClassSpecProb.7.0 = 33 AiPlayerbot.RandomClassSpecIndex.7.0 = 0 +# enh pve AiPlayerbot.RandomClassSpecProb.7.1 = 33 AiPlayerbot.RandomClassSpecIndex.7.1 = 1 +# resto pve AiPlayerbot.RandomClassSpecProb.7.2 = 33 AiPlayerbot.RandomClassSpecIndex.7.2 = 2 +# ele pvp +AiPlayerbot.RandomClassSpecProb.7.3 = 0 +AiPlayerbot.RandomClassSpecIndex.7.3 = 3 +# enh pvp +AiPlayerbot.RandomClassSpecProb.7.4 = 0 +AiPlayerbot.RandomClassSpecIndex.7.4 = 4 +# resto pvp +AiPlayerbot.RandomClassSpecProb.7.5 = 0 +AiPlayerbot.RandomClassSpecIndex.7.5 = 5 # # @@ -1955,12 +2063,27 @@ AiPlayerbot.RandomClassSpecIndex.7.2 = 2 # # +# arcane pve AiPlayerbot.RandomClassSpecProb.8.0 = 30 AiPlayerbot.RandomClassSpecIndex.8.0 = 0 +# fire pve AiPlayerbot.RandomClassSpecProb.8.1 = 30 AiPlayerbot.RandomClassSpecIndex.8.1 = 1 +# frost pve AiPlayerbot.RandomClassSpecProb.8.2 = 40 AiPlayerbot.RandomClassSpecIndex.8.2 = 2 +# frostfire pve +AiPlayerbot.RandomClassSpecProb.8.3 = 0 +AiPlayerbot.RandomClassSpecIndex.8.3 = 3 +# arcane pvp +AiPlayerbot.RandomClassSpecProb.8.4 = 0 +AiPlayerbot.RandomClassSpecIndex.8.4 = 4 +# fire pvp +AiPlayerbot.RandomClassSpecProb.8.5 = 0 +AiPlayerbot.RandomClassSpecIndex.8.5 = 5 +# frost pvp +AiPlayerbot.RandomClassSpecProb.8.6 = 0 +AiPlayerbot.RandomClassSpecIndex.8.6 = 6 # # @@ -1972,12 +2095,24 @@ AiPlayerbot.RandomClassSpecIndex.8.2 = 2 # # +# affli pve AiPlayerbot.RandomClassSpecProb.9.0 = 33 AiPlayerbot.RandomClassSpecIndex.9.0 = 0 +# demo pve AiPlayerbot.RandomClassSpecProb.9.1 = 34 AiPlayerbot.RandomClassSpecIndex.9.1 = 1 +# destro pve AiPlayerbot.RandomClassSpecProb.9.2 = 33 AiPlayerbot.RandomClassSpecIndex.9.2 = 2 +# affli pvp +AiPlayerbot.RandomClassSpecProb.9.3 = 0 +AiPlayerbot.RandomClassSpecIndex.9.3 = 3 +# demo pvp +AiPlayerbot.RandomClassSpecProb.9.4 = 0 +AiPlayerbot.RandomClassSpecIndex.9.4 = 4 +# destro pvp +AiPlayerbot.RandomClassSpecProb.9.5 = 0 +AiPlayerbot.RandomClassSpecIndex.9.5 = 5 # # @@ -1989,14 +2124,27 @@ AiPlayerbot.RandomClassSpecIndex.9.2 = 2 # # +# balance pve AiPlayerbot.RandomClassSpecProb.11.0 = 20 AiPlayerbot.RandomClassSpecIndex.11.0 = 0 +# bear pve AiPlayerbot.RandomClassSpecProb.11.1 = 25 AiPlayerbot.RandomClassSpecIndex.11.1 = 1 +# resto pve AiPlayerbot.RandomClassSpecProb.11.2 = 35 AiPlayerbot.RandomClassSpecIndex.11.2 = 2 +# cat pve AiPlayerbot.RandomClassSpecProb.11.3 = 20 AiPlayerbot.RandomClassSpecIndex.11.3 = 3 +# balance pvp +AiPlayerbot.RandomClassSpecProb.11.4 = 0 +AiPlayerbot.RandomClassSpecIndex.11.4 = 4 +# cat pvp +AiPlayerbot.RandomClassSpecProb.11.5 = 0 +AiPlayerbot.RandomClassSpecIndex.11.5 = 5 +# resto pvp +AiPlayerbot.RandomClassSpecProb.11.6 = 0 +AiPlayerbot.RandomClassSpecIndex.11.6 = 6 # # diff --git a/src/Ai/Base/Value/ItemUsageValue.cpp b/src/Ai/Base/Value/ItemUsageValue.cpp index 889372127..c3d976f0f 100644 --- a/src/Ai/Base/Value/ItemUsageValue.cpp +++ b/src/Ai/Base/Value/ItemUsageValue.cpp @@ -234,6 +234,11 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto, 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) diff --git a/src/Bot/Factory/PlayerbotFactory.cpp b/src/Bot/Factory/PlayerbotFactory.cpp index e7021e372..f8e0e5dfe 100644 --- a/src/Bot/Factory/PlayerbotFactory.cpp +++ b/src/Bot/Factory/PlayerbotFactory.cpp @@ -59,6 +59,7 @@ std::list PlayerbotFactory::specialQuestIds; std::vector PlayerbotFactory::enchantSpellIdCache; std::vector PlayerbotFactory::enchantGemIdCache; std::unordered_map> PlayerbotFactory::trainerIdCache; +std::vector PlayerbotFactory::ccBreakTrinketCache; bool PlayerbotFactory::IsPrimaryTradeSkill(uint16 skillId) { @@ -460,6 +461,69 @@ void PlayerbotFactory::Init() enchantGemIdCache.push_back(gemId); } LOG_INFO("playerbots", "Loading {} enchantment gems", enchantGemIdCache.size()); + + BuildCcBreakTrinketCache(); +} + +void PlayerbotFactory::BuildCcBreakTrinketCache() +{ + ccBreakTrinketCache.clear(); + // Spell 42292: removes all movement-impairing and loss-of-control effects — the PvP trinket spell. + QueryResult result = WorldDatabase.Query( + "SELECT entry, ItemLevel FROM item_template " + "WHERE Quality >= 2 AND InventoryType = 12 " + "AND (FlagsExtra & 8192) = 0 " + "AND (spellid_1 = 42292 OR spellid_2 = 42292 OR spellid_3 = 42292 " + " OR spellid_4 = 42292 OR spellid_5 = 42292)"); + + if (!result) + { + LOG_INFO("playerbots", "CC-break trinket cache: no items found."); + return; + } + + struct CcItem { uint32 itemId; uint16 itemLevel; }; + std::vector tmp; + do + { + Field* f = result->Fetch(); + tmp.push_back({f[0].Get(), f[1].Get()}); + } while (result->NextRow()); + + std::sort(tmp.begin(), tmp.end(), [](const CcItem& a, const CcItem& b) { + return a.itemLevel > b.itemLevel; + }); + for (auto& c : tmp) + ccBreakTrinketCache.push_back(c.itemId); + + LOG_INFO("playerbots", "CC-break trinket cache: {} items.", ccBreakTrinketCache.size()); +} + +uint8 PlayerbotFactory::GetPreferredArmorType(uint8 cls) +{ + switch (cls) + { + case CLASS_WARRIOR: + case CLASS_PALADIN: + case CLASS_DEATH_KNIGHT: + return ITEM_SUBCLASS_ARMOR_PLATE; + + case CLASS_HUNTER: + case CLASS_SHAMAN: + return ITEM_SUBCLASS_ARMOR_MAIL; + + case CLASS_ROGUE: + case CLASS_DRUID: + return ITEM_SUBCLASS_ARMOR_LEATHER; + + case CLASS_PRIEST: + case CLASS_MAGE: + case CLASS_WARLOCK: + return ITEM_SUBCLASS_ARMOR_CLOTH; + + default: + return 0; + } } void PlayerbotFactory::Prepare() @@ -561,9 +625,9 @@ void PlayerbotFactory::Randomize(bool incremental) if (!incremental || !sPlayerbotAIConfig.equipmentPersistence || bot->GetLevel() < sPlayerbotAIConfig.equipmentPersistenceLevel) { - InitTalentsTree(); + uint32 specIndex = InitTalentsTree(); + sRandomPlayerbotMgr.SetValue(bot->GetGUID().GetCounter(), "specNo", specIndex + 1); } - sRandomPlayerbotMgr.SetValue(bot->GetGUID().GetCounter(), "specNo", 0); if (botAI) { PlayerbotRepository::instance().Reset(botAI); @@ -1359,7 +1423,7 @@ void PlayerbotFactory::ResetQuests() } } -void PlayerbotFactory::InitTalentsTree(bool increment /*false*/, bool use_template /*true*/, bool reset /*false*/) +uint32 PlayerbotFactory::InitTalentsTree(bool increment /*false*/, bool use_template /*true*/, bool reset /*false*/) { uint32 specTab; uint8 cls = bot->getClass(); @@ -1427,7 +1491,14 @@ void PlayerbotFactory::InitTalentsTree(bool increment /*false*/, bool use_templa if (bot->GetFreeTalentPoints()) InitTalents((specTab + 2) % 3); + if (bot->getClass() == CLASS_SHAMAN && bot->HasSpell(30798)) + { + bot->SetSkill(SKILL_DUAL_WIELD, 0, 1, 1); + bot->SetCanDualWield(true); + } + bot->SendTalentsInfoData(false); + return sPlayerbotAIConfig.randomClassSpecIndex[cls][specTab]; } void PlayerbotFactory::InitTalentsBySpecNo(Player* bot, int specNo, bool reset) @@ -1505,7 +1576,15 @@ void PlayerbotFactory::InitTalentsBySpecNo(Player* bot, int specNo, bool reset) break; } } + + if (bot->getClass() == CLASS_SHAMAN && bot->HasSpell(30798)) + { + bot->SetSkill(SKILL_DUAL_WIELD, 0, 1, 1); + bot->SetCanDualWield(true); + } + bot->SendTalentsInfoData(false); + sRandomPlayerbotMgr.SetValue(bot->GetGUID().GetCounter(), "specNo", (uint32)specNo + 1); } void PlayerbotFactory::InitTalentsByParsedSpecLink(Player* bot, std::vector> parsedSpecLink, @@ -2008,7 +2087,35 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) uint32 blevel = bot->GetLevel(); int32 delta = std::min(blevel, 10u); + bool isPvp = sRandomPlayerbotMgr.IsSpecPvp(bot->GetGUID().GetCounter(), bot->getClass()); + StatsWeightCalculator calculator(bot); + if (isPvp) + calculator.SetPvpSpec(true); + + // Pre-select CC-break trinket for PvP specs: best available by item level + // that the bot meets the level requirement for. + // Humans (Every Man for Himself) and Undead (Will of the Forsaken) have a + // racial that shares the PvP trinket cooldown, so they don't need one. + bool racialHasCcBreak = (bot->getRace() == RACE_HUMAN || bot->getRace() == RACE_UNDEAD_PLAYER); + uint32 pvpTrinket1 = 0; + if (isPvp && level >= 50 && !racialHasCcBreak) + { + for (uint32 itemId : ccBreakTrinketCache) + { + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); + if (!proto) continue; + // Respect gear quality limit: trinket must not exceed itemQuality setting + if (static_cast(proto->Quality) > itemQuality) continue; + if (proto->RequiredLevel > level) continue; + if (!CanEquipItem(proto)) continue; + uint16 dest; + if (!CanEquipUnseenItem(EQUIPMENT_SLOT_TRINKET1, dest, itemId)) continue; + pvpTrinket1 = itemId; + break; + } + } + for (int32 slot : initSlotsOrder) { if (slot == EQUIPMENT_SLOT_TABARD || slot == EQUIPMENT_SLOT_BODY) @@ -2028,6 +2135,10 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) (slot != EQUIPMENT_SLOT_RANGED)) continue; + // Exclude resilience weighting for trinkets + bool isTrinketSlot = (slot == EQUIPMENT_SLOT_TRINKET1 || slot == EQUIPMENT_SLOT_TRINKET2); + calculator.SetExcludeResilience(isTrinketSlot); + Item* oldItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot); if (second_chance && oldItem) @@ -2037,6 +2148,28 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) oldItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot); + // PvP specs: force TRINKET1 to the best available CC-break trinket. + if (slot == EQUIPMENT_SLOT_TRINKET1 && pvpTrinket1 != 0) + { + if (oldItem) + { + uint8 bagIndex = oldItem->GetBagSlot(); + uint8 oldSlot = oldItem->GetSlot(); + uint8 dstBag = NULL_BAG; + WorldPacket packet(CMSG_AUTOSTORE_BAG_ITEM, 3); + packet << bagIndex << oldSlot << dstBag; + WorldPackets::Item::AutoStoreBagItem nicePacket(std::move(packet)); + nicePacket.Read(); + bot->GetSession()->HandleAutoStoreBagItemOpcode(nicePacket); + oldItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot); + if (oldItem) continue; + } + uint16 dest; + if (CanEquipUnseenItem(slot, dest, pvpTrinket1)) + bot->EquipNewItem(dest, pvpTrinket1, true); + continue; + } + int32 desiredQuality = itemQuality; if (urand(0, 100) < 100 * sPlayerbotAIConfig.randomGearLoweringChance && desiredQuality > ITEM_QUALITY_NORMAL) desiredQuality--; @@ -2054,6 +2187,7 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) if (urand(1, 100) <= skipProb) continue; + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); // disable next expansion gear if (sPlayerbotAIConfig.limitGearExpansion && bot->GetLevel() <= 60 && itemId >= 23728) continue; @@ -2064,7 +2198,6 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) // wearable TBC items above 35570 but nothing of significance continue; - ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); if (!proto) continue; @@ -2115,7 +2248,15 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) ItemTemplate const* proto = sObjectMgr->GetItemTemplate(newItemId); - float cur_score = calculator.CalculateItem(newItemId); + float cur_score = calculator.CalculateItem(newItemId, 0, slot); + + if (cur_score > 0.0f && proto && proto->Class == ITEM_CLASS_ARMOR && sPlayerbotAIConfig.preferClassArmorType) + { + uint8 preferredArmorType = GetPreferredArmorType(bot->getClass()); + if (preferredArmorType != 0 && proto->SubClass == preferredArmorType) + cur_score *= 3.0f; // 3x multiplier for preferred armor type + } + if (cur_score > bestScoreForSlot) { // delay heavy check to here @@ -2141,7 +2282,7 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) if (incremental && oldItem) { - float old_score = calculator.CalculateItem(oldItem->GetEntry(), oldItem->GetItemRandomPropertyId()); + float old_score = calculator.CalculateItem(oldItem->GetEntry(), oldItem->GetItemRandomPropertyId(), slot); if (bestScoreForSlot < 1.2f * old_score) continue; } @@ -2194,7 +2335,14 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) (slot != EQUIPMENT_SLOT_RANGED)) continue; - if (bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot) != nullptr) + // CC-break trinket was force-equipped in the main pass; leave it alone. + if (slot == EQUIPMENT_SLOT_TRINKET1 && pvpTrinket1 != 0) + continue; + + bool isTrinketSlot = (slot == EQUIPMENT_SLOT_TRINKET1 || slot == EQUIPMENT_SLOT_TRINKET2); + calculator.SetExcludeResilience(isTrinketSlot); + + if (Item* oldItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) bot->DestroyItem(INVENTORY_SLOT_BAG_0, slot, true); std::vector& ids = items[slot]; @@ -2209,7 +2357,15 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) ItemTemplate const* proto = sObjectMgr->GetItemTemplate(newItemId); - float cur_score = calculator.CalculateItem(newItemId); + float cur_score = calculator.CalculateItem(newItemId, 0, slot); + + if (cur_score > 0.0f && proto && proto->Class == ITEM_CLASS_ARMOR && sPlayerbotAIConfig.preferClassArmorType) + { + uint8 preferredArmorType = GetPreferredArmorType(bot->getClass()); + if (preferredArmorType != 0 && proto->SubClass == preferredArmorType) + cur_score *= 3.0f; // 3x multiplier for preferred armor type + } + if (cur_score > bestScoreForSlot) { // delay heavy check to here diff --git a/src/Bot/Factory/PlayerbotFactory.h b/src/Bot/Factory/PlayerbotFactory.h index 0e18e6b83..ba32e6a11 100644 --- a/src/Bot/Factory/PlayerbotFactory.h +++ b/src/Bot/Factory/PlayerbotFactory.h @@ -63,7 +63,7 @@ public: static uint32 tradeSkills[]; static float CalculateEnchantScore(uint32 enchant_id, Player* bot); - void InitTalentsTree(bool incremental = false, bool use_template = true, bool reset = false); + uint32 InitTalentsTree(bool incremental = false, bool use_template = true, bool reset = false); static void InitTalentsBySpecNo(Player* bot, int specNo, bool reset); static void InitTalentsByParsedSpecLink(Player* bot, std::vector> parsedSpecLink, bool reset); void InitAvailableSpells(); @@ -190,6 +190,8 @@ private: std::vector GetCurrentGemsCount(); bool CanEquipArmor(ItemTemplate const* proto); bool CanEquipWeapon(ItemTemplate const* proto); + static void BuildCcBreakTrinketCache(); + uint8 GetPreferredArmorType(uint8 cls); void EnchantItem(Item* item); void AddItemStats(uint32 mod, uint8& sp, uint8& ap, uint8& tank); bool CheckItemStats(uint8 sp, uint8 ap, uint8 tank); @@ -220,6 +222,7 @@ private: static std::unordered_map> trainerIdCache; static std::vector enchantSpellIdCache; static std::vector enchantGemIdCache; + static std::vector ccBreakTrinketCache; protected: EnchantContainer m_EnchantContainer; diff --git a/src/Bot/RandomPlayerbotMgr.cpp b/src/Bot/RandomPlayerbotMgr.cpp index 110a1942e..5c0922fb9 100644 --- a/src/Bot/RandomPlayerbotMgr.cpp +++ b/src/Bot/RandomPlayerbotMgr.cpp @@ -2266,6 +2266,16 @@ CachedEvent* RandomPlayerbotMgr::FindEvent(uint32 bot, std::string const& event) return &e; } +bool RandomPlayerbotMgr::IsSpecPvp(uint32 bot, uint8 cls) +{ + uint32 stored = GetValue(bot, "specNo"); + if (!stored) + return false; + uint32 specIndex = stored - 1; + std::string const& name = sPlayerbotAIConfig.premadeSpecName[cls][specIndex]; + return !name.empty() && name.find("pvp") != std::string::npos; +} + uint32 RandomPlayerbotMgr::GetEventValue(uint32 bot, std::string const& event) { if (CachedEvent* e = FindEvent(bot, event)) diff --git a/src/Bot/RandomPlayerbotMgr.h b/src/Bot/RandomPlayerbotMgr.h index db74f2cbe..b68c77e41 100644 --- a/src/Bot/RandomPlayerbotMgr.h +++ b/src/Bot/RandomPlayerbotMgr.h @@ -140,6 +140,7 @@ public: std::string GetData(uint32 bot, std::string const& type); void SetValue(uint32 bot, std::string const& type, uint32 value, std::string const& data = ""); void SetValue(Player* bot, std::string const& type, uint32 value, std::string const& data = ""); + bool IsSpecPvp(uint32 bot, uint8 cls); void Remove(Player* bot); ObjectGuid GetBattleMasterGUID(Player* bot, BattlegroundTypeId bgTypeId); CreatureData const* GetCreatureDataByEntry(uint32 entry); diff --git a/src/Mgr/Item/StatsWeightCalculator.cpp b/src/Mgr/Item/StatsWeightCalculator.cpp index faa06ff0f..b232b1e6b 100644 --- a/src/Mgr/Item/StatsWeightCalculator.cpp +++ b/src/Mgr/Item/StatsWeightCalculator.cpp @@ -72,7 +72,7 @@ void StatsWeightCalculator::Reset() } } -float StatsWeightCalculator::CalculateItem(uint32 itemId, int32 randomPropertyIds) +float StatsWeightCalculator::CalculateItem(uint32 itemId, int32 randomPropertyIds, int32 slot) { ItemTemplate const* proto = &sObjectMgr->GetItemTemplateStore()->at(itemId); @@ -111,10 +111,12 @@ float StatsWeightCalculator::CalculateItem(uint32 itemId, int32 randomPropertyId weight_ *= PlayerbotFactory::CalcMixedGearScore(lvl, ITEM_QUALITY_EPIC); else weight_ *= PlayerbotFactory::CalcMixedGearScore(proto->ItemLevel, proto->Quality); - - return weight_; } - // If quality/level blending is disabled, also return the calculated weight. + + // Apply weapon speed governance if slot is provided and this is a weapon + if (sPlayerbotAIConfig.preferredSpecWeapons && slot >= 0 && proto->Class == ITEM_CLASS_WEAPON) + weight_ *= ApplyPreferredSpecWeapons(proto, slot); + return weight_; } @@ -212,6 +214,7 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_HIT] += 1.7f; stats_weights_[STATS_TYPE_CRIT] += 1.4f; stats_weights_[STATS_TYPE_HASTE] += 1.6f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; stats_weights_[STATS_TYPE_RANGED_DPS] += 7.5f; } else if (cls == CLASS_HUNTER && tab == HUNTER_TAB_MARKSMANSHIP) @@ -222,6 +225,7 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_HIT] += 2.1f; stats_weights_[STATS_TYPE_CRIT] += 2.0f; stats_weights_[STATS_TYPE_HASTE] += 1.8f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; stats_weights_[STATS_TYPE_RANGED_DPS] += 10.0f; } else if (cls == CLASS_ROGUE && tab == ROGUE_TAB_COMBAT) @@ -233,6 +237,7 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_HIT] += 2.1f; stats_weights_[STATS_TYPE_CRIT] += 1.4f; stats_weights_[STATS_TYPE_HASTE] += 1.7f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 2.0f; stats_weights_[STATS_TYPE_MELEE_DPS] += 7.0f; } @@ -257,64 +262,69 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_HIT] += 2.1f; stats_weights_[STATS_TYPE_CRIT] += 1.1f; stats_weights_[STATS_TYPE_HASTE] += 1.8f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 2.1f; stats_weights_[STATS_TYPE_MELEE_DPS] += 5.0f; } else if (cls == CLASS_WARRIOR && tab == WARRIOR_TAB_FURY) { - stats_weights_[STATS_TYPE_AGILITY] += 1.8f; - stats_weights_[STATS_TYPE_STRENGTH] += 2.6f; - stats_weights_[STATS_TYPE_ATTACK_POWER] += 1.0f; + stats_weights_[STATS_TYPE_AGILITY] += 0.8f; + stats_weights_[STATS_TYPE_STRENGTH] += 2.5f; + stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.8f; stats_weights_[STATS_TYPE_ARMOR_PENETRATION] += 2.1f; stats_weights_[STATS_TYPE_HIT] += 2.3f; stats_weights_[STATS_TYPE_CRIT] += 2.2f; - stats_weights_[STATS_TYPE_HASTE] += 1.8f; + stats_weights_[STATS_TYPE_HASTE] += 0.8f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 2.0f; + stats_weights_[STATS_TYPE_DEFENSE] -= 1.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 2.5f; stats_weights_[STATS_TYPE_MELEE_DPS] += 7.0f; } else if (cls == CLASS_WARRIOR && tab == WARRIOR_TAB_ARMS) { - stats_weights_[STATS_TYPE_AGILITY] += 1.6f; - stats_weights_[STATS_TYPE_STRENGTH] += 2.3f; - stats_weights_[STATS_TYPE_ATTACK_POWER] += 1.0f; + stats_weights_[STATS_TYPE_AGILITY] += 0.8f; + stats_weights_[STATS_TYPE_STRENGTH] += 2.5f; + stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.8f; stats_weights_[STATS_TYPE_ARMOR_PENETRATION] += 1.7f; stats_weights_[STATS_TYPE_HIT] += 2.0f; stats_weights_[STATS_TYPE_CRIT] += 1.9f; stats_weights_[STATS_TYPE_HASTE] += 0.8f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 2.0f; + stats_weights_[STATS_TYPE_DEFENSE] -= 1.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 1.4f; stats_weights_[STATS_TYPE_MELEE_DPS] += 7.0f; } else if (cls == CLASS_DEATH_KNIGHT && tab == DEATH_KNIGHT_TAB_FROST) { - stats_weights_[STATS_TYPE_AGILITY] += 1.7f; - stats_weights_[STATS_TYPE_STRENGTH] += 2.8f; - stats_weights_[STATS_TYPE_ATTACK_POWER] += 1.0f; + stats_weights_[STATS_TYPE_AGILITY] += 0.5f; + stats_weights_[STATS_TYPE_STRENGTH] += 2.5f; + stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.5f; stats_weights_[STATS_TYPE_ARMOR_PENETRATION] += 2.7f; stats_weights_[STATS_TYPE_HIT] += 2.3f; stats_weights_[STATS_TYPE_CRIT] += 2.2f; stats_weights_[STATS_TYPE_HASTE] += 2.1f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 2.5f; stats_weights_[STATS_TYPE_MELEE_DPS] += 7.0f; } else if (cls == CLASS_DEATH_KNIGHT && tab == DEATH_KNIGHT_TAB_UNHOLY) { - stats_weights_[STATS_TYPE_AGILITY] += 0.9f; + stats_weights_[STATS_TYPE_AGILITY] += 0.5f; stats_weights_[STATS_TYPE_STRENGTH] += 2.5f; - stats_weights_[STATS_TYPE_ATTACK_POWER] += 1.0f; + stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.5f; stats_weights_[STATS_TYPE_ARMOR_PENETRATION] += 1.3f; stats_weights_[STATS_TYPE_HIT] += 2.2f; stats_weights_[STATS_TYPE_CRIT] += 1.7f; stats_weights_[STATS_TYPE_HASTE] += 1.8f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 1.5f; stats_weights_[STATS_TYPE_MELEE_DPS] += 5.0f; } else if (cls == CLASS_PALADIN && tab == PALADIN_TAB_RETRIBUTION) { - stats_weights_[STATS_TYPE_AGILITY] += 1.6f; + stats_weights_[STATS_TYPE_AGILITY] += 0.5f; stats_weights_[STATS_TYPE_STRENGTH] += 2.5f; - stats_weights_[STATS_TYPE_INTELLECT] += 0.1f; - stats_weights_[STATS_TYPE_ATTACK_POWER] += 1.0f; - stats_weights_[STATS_TYPE_SPELL_POWER] += 0.3f; + stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.5f; stats_weights_[STATS_TYPE_ARMOR_PENETRATION] += 1.5f; stats_weights_[STATS_TYPE_HIT] += 1.9f; stats_weights_[STATS_TYPE_CRIT] += 1.7f; @@ -328,7 +338,7 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_STRENGTH] += 1.1f; stats_weights_[STATS_TYPE_INTELLECT] += 0.3f; stats_weights_[STATS_TYPE_ATTACK_POWER] += 1.0f; - stats_weights_[STATS_TYPE_SPELL_POWER] += 0.95f; + stats_weights_[STATS_TYPE_SPELL_POWER] += 0.5f; stats_weights_[STATS_TYPE_ARMOR_PENETRATION] += 0.9f; stats_weights_[STATS_TYPE_HIT] += 2.1f; stats_weights_[STATS_TYPE_CRIT] += 1.5f; @@ -347,6 +357,7 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_HIT] += 1.1f; stats_weights_[STATS_TYPE_CRIT] += 0.8f; stats_weights_[STATS_TYPE_HASTE] += 1.0f; + stats_weights_[STATS_TYPE_ATTACK_POWER] -= 1.0f; stats_weights_[STATS_TYPE_RANGED_DPS] += 1.0f; } else if (cls == CLASS_MAGE && tab == MAGE_TAB_FIRE) @@ -357,15 +368,17 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_HIT] += 1.2f; stats_weights_[STATS_TYPE_CRIT] += 1.1f; stats_weights_[STATS_TYPE_HASTE] += 0.8f; + stats_weights_[STATS_TYPE_ATTACK_POWER] -= 1.0f; stats_weights_[STATS_TYPE_RANGED_DPS] += 1.0f; } else if (cls == CLASS_SHAMAN && tab == SHAMAN_TAB_ELEMENTAL) { - stats_weights_[STATS_TYPE_INTELLECT] += 0.25f; - stats_weights_[STATS_TYPE_SPELL_POWER] += 1.0f; + stats_weights_[STATS_TYPE_INTELLECT] += 0.5f; + stats_weights_[STATS_TYPE_SPELL_POWER] += 1.2f; stats_weights_[STATS_TYPE_HIT] += 1.1f; stats_weights_[STATS_TYPE_CRIT] += 0.8f; stats_weights_[STATS_TYPE_HASTE] += 1.0f; + stats_weights_[STATS_TYPE_MANA_REGENERATION] += 0.5f; } else if ((cls == CLASS_PALADIN && tab == PALADIN_TAB_HOLY) || (cls == CLASS_SHAMAN && tab == SHAMAN_TAB_RESTORATION)) @@ -386,14 +399,15 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_MANA_REGENERATION] += 0.9f; stats_weights_[STATS_TYPE_CRIT] += 0.6f; stats_weights_[STATS_TYPE_HASTE] += 0.8f; + stats_weights_[STATS_TYPE_ATTACK_POWER] -= 1.0f; stats_weights_[STATS_TYPE_RANGED_DPS] += 1.0f; } else if ((cls == CLASS_WARRIOR && tab == WARRIOR_TAB_PROTECTION) || (cls == CLASS_PALADIN && tab == PALADIN_TAB_PROTECTION)) { - stats_weights_[STATS_TYPE_AGILITY] += 2.0f; - stats_weights_[STATS_TYPE_STRENGTH] += 1.0f; - stats_weights_[STATS_TYPE_STAMINA] += 3.5f; + stats_weights_[STATS_TYPE_AGILITY] += 0.2f; + stats_weights_[STATS_TYPE_STRENGTH] += 1.3f; + stats_weights_[STATS_TYPE_STAMINA] += 3.0f; stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.2f; stats_weights_[STATS_TYPE_DEFENSE] += 2.5f; stats_weights_[STATS_TYPE_PARRY] += 2.0f; @@ -403,26 +417,26 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_BLOCK_VALUE] += 0.5f; stats_weights_[STATS_TYPE_ARMOR] += 0.15f; stats_weights_[STATS_TYPE_HIT] += 2.0f; - stats_weights_[STATS_TYPE_CRIT] += 0.2f; - stats_weights_[STATS_TYPE_HASTE] += 0.5f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 2.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 3.0f; stats_weights_[STATS_TYPE_MELEE_DPS] += 2.0f; } else if (cls == CLASS_DEATH_KNIGHT && tab == DEATH_KNIGHT_TAB_BLOOD) { - stats_weights_[STATS_TYPE_AGILITY] += 2.0f; - stats_weights_[STATS_TYPE_STRENGTH] += 1.0f; - stats_weights_[STATS_TYPE_STAMINA] += 3.5f; + stats_weights_[STATS_TYPE_AGILITY] += 0.2f; + stats_weights_[STATS_TYPE_STRENGTH] += 1.3f; + stats_weights_[STATS_TYPE_STAMINA] += 3.0f; stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.2f; - stats_weights_[STATS_TYPE_DEFENSE] += 3.5f; + stats_weights_[STATS_TYPE_DEFENSE] += 2.5f; stats_weights_[STATS_TYPE_PARRY] += 2.0f; stats_weights_[STATS_TYPE_DODGE] += 2.0f; + stats_weights_[STATS_TYPE_BLOCK_RATING] -= 2.0f; + stats_weights_[STATS_TYPE_BLOCK_VALUE] -= 2.0f; // stats_weights_[STATS_TYPE_RESILIENCE] += 2.0f; stats_weights_[STATS_TYPE_ARMOR] += 0.15f; stats_weights_[STATS_TYPE_HIT] += 2.0f; - stats_weights_[STATS_TYPE_CRIT] += 0.5f; - stats_weights_[STATS_TYPE_HASTE] += 0.5f; - stats_weights_[STATS_TYPE_EXPERTISE] += 3.5f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; + stats_weights_[STATS_TYPE_EXPERTISE] += 3.0f; stats_weights_[STATS_TYPE_MELEE_DPS] += 2.0f; } else @@ -483,6 +497,11 @@ void StatsWeightCalculator::GenerateAdditionalWeights(Player* player) !player->HasSpell(SPELL_FEL_ARMOR_RANK_3) && !player->HasSpell(SPELL_FEL_ARMOR_RANK_4)) stats_weights_[STATS_TYPE_SPIRIT] -= 0.4f; } + + if (pvpSpec_ && !exclude_resilience_) + stats_weights_[STATS_TYPE_RESILIENCE] += 7.0f; + else if (!pvpSpec_) + stats_weights_[STATS_TYPE_RESILIENCE] -= 3.0f; } void StatsWeightCalculator::CalculateItemSetMod(Player* player, ItemTemplate const* proto) @@ -573,7 +592,8 @@ void StatsWeightCalculator::CalculateItemTypePenalty(ItemTemplate const* proto) (cls == CLASS_WARRIOR && tab == WARRIOR_TAB_FURY && !player_->CanTitanGrip() && player_->CanDualWield()) || (cls == CLASS_WARRIOR && tab == WARRIOR_TAB_PROTECTION) || - (cls == CLASS_PALADIN && tab == PALADIN_TAB_PROTECTION))) + (cls == CLASS_PALADIN && tab == PALADIN_TAB_PROTECTION) || + (cls == CLASS_PALADIN && tab == PALADIN_TAB_HOLY))) { weight_ *= 0.1; } @@ -592,11 +612,16 @@ void StatsWeightCalculator::CalculateItemTypePenalty(ItemTemplate const* proto) weight_ *= 0.1; } // caster's main hand (cannot duel weapon but can equip two-hands stuff) - if (cls == CLASS_MAGE || cls == CLASS_PRIEST || cls == CLASS_WARLOCK || cls == CLASS_DRUID || - (cls == CLASS_SHAMAN && !player_->CanDualWield())) + if ((cls == CLASS_MAGE || cls == CLASS_PRIEST || cls == CLASS_WARLOCK || cls == CLASS_DRUID || + (cls == CLASS_SHAMAN && !player_->CanDualWield())) && + !(cls == CLASS_PALADIN && tab == PALADIN_TAB_HOLY)) { weight_ *= 0.65; } + if (cls == CLASS_PALADIN && tab == PALADIN_TAB_HOLY) + { + weight_ *= 0.8; + } } // fury with titan's grip if ((!isDoubleHand || proto->SubClass == ITEM_SUBCLASS_WEAPON_POLEARM || @@ -767,3 +792,163 @@ void StatsWeightCalculator::ApplyWeightFinetune(Player* player) } } } + +float StatsWeightCalculator::ApplyPreferredSpecWeapons(ItemTemplate const* proto, int32 slot) +{ + // Multiply score by 3x when this weapon's delay matches the spec-ideal speed. + float weight = 2.0f; + + // Applies to mainhand, offhand, and ranged slots only. + if (slot != EQUIPMENT_SLOT_MAINHAND && + slot != EQUIPMENT_SLOT_OFFHAND && + slot != EQUIPMENT_SLOT_RANGED) + return 1.0f; + + uint32 delay = proto->Delay; // milliseconds + float boost = 1.0f + weight; // applied on a match + + // Hunter: melee weapons are stat sticks — speed irrelevant. + // Ranged weapons scale Aimed/Chimera/Explosive Shot from top-end damage, + // so a slow ranged weapon (>=2600 ms) is strongly preferred. + if (cls == CLASS_HUNTER) + { + if (slot == EQUIPMENT_SLOT_RANGED && delay >= 2600) + return boost; + return 1.0f; + } + + // Feral Druid: forms normalise attack speed; raw weapon Delay is irrelevant. + if (cls == CLASS_DRUID && tab == DRUID_TAB_FERAL) + return 1.0f; + + switch (cls) + { + case CLASS_WARRIOR: + if (tab == WARRIOR_TAB_ARMS) + { + // Arms: slow 2H axes or polearms in mainhand only (Axe Specialization: +5% crit). + bool isAxeOrPolearm = (proto->SubClass == ITEM_SUBCLASS_WEAPON_AXE2 || + proto->SubClass == ITEM_SUBCLASS_WEAPON_POLEARM); + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 3400 && isAxeOrPolearm) + return boost; + } + else if (tab == WARRIOR_TAB_FURY) + { + if (!player_->CanDualWield()) + { + // Pre-DW: treat like Arms — slow 2H in mainhand only. + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 3400) + return boost; + } + else if (player_->CanTitanGrip()) + { + // Titan's Grip: slow 2H (>=3400) in both hands. + if (delay >= 3400) + return boost; + } + else + { + // 1H DW: slow 1H (>=2600) in both hands. + // 2H must be excluded — delay >= 2600 would otherwise pass + // for a 2H heirloom (~3600ms) just as it did for Enhancement. + if (proto->InventoryType == INVTYPE_2HWEAPON) + break; + if (delay >= 2600) + return boost; + } + } + else if (tab == WARRIOR_TAB_PROTECTION) + { + // Prot: slow 1H (>=2600) in mainhand. Shield in offhand, no speed bonus. + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 2600) + return boost; + } + break; + + case CLASS_PALADIN: + if (tab == PALADIN_TAB_RETRIBUTION) + { + // Ret: slow 2H in mainhand only. + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 3400) + return boost; + } + else if (tab == PALADIN_TAB_PROTECTION) + { + // Prot: slow 1H (>=2600) in mainhand. Shield in offhand. + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 2600) + return boost; + } + break; + + case CLASS_DEATH_KNIGHT: + if (tab == DEATH_KNIGHT_TAB_BLOOD || tab == DEATH_KNIGHT_TAB_UNHOLY) + { + // Blood / Unholy: slow 2H in mainhand only. + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 3400) + return boost; + } + else if (tab == DEATH_KNIGHT_TAB_FROST) + { + // Frost DK has Dual Wield innately — always dual-wields 1H. + if (proto->InventoryType == INVTYPE_2HWEAPON) + break; + if (delay >= 2600) + return boost; + } + break; + + case CLASS_SHAMAN: + if (tab == SHAMAN_TAB_ENHANCEMENT) + { + if (!player_->CanDualWield()) + { + // Pre-Dual Wield: Enhancement plays like a 2H spec. + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 3400) + return boost; + } + else + { + // Post-Dual Wield: slow 1H (>=2600) in both hands. + if (proto->InventoryType == INVTYPE_2HWEAPON) + break; + + if (delay >= 2600) + { + float mult = boost; + if (slot == EQUIPMENT_SLOT_OFFHAND) + { + Item* mh = player_->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND); + if (mh && mh->GetTemplate() && mh->GetTemplate()->Delay == delay) + mult *= boost; // synchronized: ×(1+weight)² total = ×9 for 2.0f weight + } + return mult; + } + } + } + break; + + case CLASS_ROGUE: + if (tab == ROGUE_TAB_COMBAT) + { + // Combat: slow MH (>=2600), fast OH (<=1500). + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 2600) + return boost; + if (slot == EQUIPMENT_SLOT_OFFHAND && delay <= 1500) + return boost; + } + else // Assassination or Subtlety: slow dagger MH, fast dagger OH. + { + bool isDagger = (proto->SubClass == ITEM_SUBCLASS_WEAPON_DAGGER); + if (slot == EQUIPMENT_SLOT_MAINHAND && isDagger && delay >= 1700) + return boost; + if (slot == EQUIPMENT_SLOT_OFFHAND && isDagger && delay <= 1500) + return boost; + } + break; + + default: + break; + } + + return 1.0f; +} diff --git a/src/Mgr/Item/StatsWeightCalculator.h b/src/Mgr/Item/StatsWeightCalculator.h index 4390e6af1..d97dafbb3 100644 --- a/src/Mgr/Item/StatsWeightCalculator.h +++ b/src/Mgr/Item/StatsWeightCalculator.h @@ -28,12 +28,14 @@ class StatsWeightCalculator public: StatsWeightCalculator(Player* player); void Reset(); - float CalculateItem(uint32 itemId, int32 randomPropertyId = 0); + float CalculateItem(uint32 itemId, int32 randomPropertyId = 0, int32 slot = -1); float CalculateEnchant(uint32 enchantId); void SetOverflowPenalty(bool apply) { enable_overflow_penalty_ = apply; } void SetItemSetBonus(bool apply) { enable_item_set_bonus_ = apply; } void SetQualityBlend(bool apply) { enable_quality_blend_ = apply; } + void SetPvpSpec(bool isPvp) { pvpSpec_ = isPvp; } + void SetExcludeResilience(bool exclude) { exclude_resilience_ = exclude; } private: void GenerateWeights(Player* player); @@ -45,6 +47,7 @@ public: void CalculateSocketBonus(Player* player, ItemTemplate const* proto); void CalculateItemTypePenalty(ItemTemplate const* proto); + float ApplyPreferredSpecWeapons(ItemTemplate const* proto, int32 slot); bool NotBestArmorType(uint32 item_subclass_armor); @@ -65,6 +68,8 @@ private: float weight_; float stats_weights_[STATS_TYPE_MAX]; + bool pvpSpec_ = false; + bool exclude_resilience_ = false; }; #endif diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index a8daf972a..242febd17 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -125,6 +125,8 @@ bool PlayerbotAIConfig::Initialize() incrementalGearInit = sConfigMgr->GetOption("AiPlayerbot.IncrementalGearInit", true); randomGearQualityLimit = sConfigMgr->GetOption("AiPlayerbot.RandomGearQualityLimit", 3); randomGearScoreLimit = sConfigMgr->GetOption("AiPlayerbot.RandomGearScoreLimit", 0); + preferClassArmorType = sConfigMgr->GetOption("AiPlayerbot.PreferClassArmorType", false); + preferredSpecWeapons = sConfigMgr->GetOption("AiPlayerbot.PreferredSpecWeapons", false); randomBotMinLevelChance = sConfigMgr->GetOption("AiPlayerbot.RandomBotMinLevelChance", 0.1f); randomBotMaxLevelChance = sConfigMgr->GetOption("AiPlayerbot.RandomBotMaxLevelChance", 0.1f); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 877672678..210e03ef9 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -128,6 +128,8 @@ public: bool incrementalGearInit; int32 randomGearQualityLimit; int32 randomGearScoreLimit; + bool preferClassArmorType; + bool preferredSpecWeapons; float randomBotMinLevelChance, randomBotMaxLevelChance; float randomBotRpgChance; uint32 minRandomBots, maxRandomBots;