diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 886cc8cd5..c12049d66 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -865,8 +865,9 @@ AiPlayerbot.LimitGearExpansion = 1 AiPlayerbot.RandomGearLoweringChance = 0 # Unobtainable or unusable items (comma-separated list of item IDs) -# Default: Chilton Wand (12468), Totem of the Earthen Ring (46978) -AiPlayerbot.UnobtainableItems = 12468,46978 +# Defaults: Chilton Wand (12468), Frenzyheart Insignia of Fury test/on-use row (44869), +# Oracle Talisman of Ablution test/on-use row (44870), Totem of the Earthen Ring (46978) +AiPlayerbot.UnobtainableItems = 12468,44869,44870,46978 # Randombots check player's gearscore level and deny the group invitation if it's too low # Default: 0 (disabled) diff --git a/src/Ai/Base/Actions/GenericSpellActions.cpp b/src/Ai/Base/Actions/GenericSpellActions.cpp index d4b54f16f..657fda7cd 100644 --- a/src/Ai/Base/Actions/GenericSpellActions.cpp +++ b/src/Ai/Base/Actions/GenericSpellActions.cpp @@ -6,6 +6,7 @@ #include "GenericSpellActions.h" #include +#include #include "Event.h" #include "ItemTemplate.h" @@ -23,6 +24,116 @@ using ai::buff::MakeAuraQualifierForBuff; using ai::spell::HasSpellOrCategoryCooldown; +namespace +{ + std::unordered_set const& GetMixedTriggerTrinketSpellIds() + { + static std::unordered_set const mixedTriggerSpellIds = []() + { + std::unordered_set onUseSpellIds; + std::unordered_set onEquipSpellIds; + std::unordered_set mixedSpellIds; + + auto const* itemTemplates = sObjectMgr->GetItemTemplateStore(); + if (!itemTemplates) + return mixedSpellIds; + + auto const markSpellId = [&](int32 spellId, uint8 spellTrigger) + { + if (spellId <= 0) + return; + + if (spellTrigger == ITEM_SPELLTRIGGER_ON_USE) + { + if (onEquipSpellIds.find(spellId) != onEquipSpellIds.end()) + mixedSpellIds.insert(spellId); + + onUseSpellIds.insert(spellId); + } + else if (spellTrigger == ITEM_SPELLTRIGGER_ON_EQUIP) + { + if (onUseSpellIds.find(spellId) != onUseSpellIds.end()) + mixedSpellIds.insert(spellId); + + onEquipSpellIds.insert(spellId); + } + }; + + for (auto const& itr : *itemTemplates) + { + ItemTemplate const& proto = itr.second; + if (proto.InventoryType != INVTYPE_TRINKET) + continue; + + for (uint8 spellIndex = 0; spellIndex < MAX_ITEM_PROTO_SPELLS; ++spellIndex) + { + auto const& spellData = proto.Spells[spellIndex]; + markSpellId(spellData.SpellId, spellData.SpellTrigger); + } + } + + return mixedSpellIds; + }(); + + return mixedTriggerSpellIds; + } + + bool IsManaRestoreEffect(SpellEffectInfo const& effectInfo) + { + return (effectInfo.Effect == SPELL_EFFECT_ENERGIZE && + effectInfo.MiscValue == POWER_MANA) || + (effectInfo.Effect == SPELL_EFFECT_APPLY_AURA && + effectInfo.ApplyAuraName == SPELL_AURA_PERIODIC_ENERGIZE && + effectInfo.MiscValue == POWER_MANA); + } + + bool IsManaEfficiencyEffect(SpellEffectInfo const& effectInfo) + { + return effectInfo.Effect == SPELL_EFFECT_APPLY_AURA && + (((effectInfo.ApplyAuraName == SPELL_AURA_MOD_POWER_REGEN || + effectInfo.ApplyAuraName == SPELL_AURA_MOD_POWER_REGEN_PERCENT) && + effectInfo.MiscValue == POWER_MANA) || + effectInfo.ApplyAuraName == SPELL_AURA_MOD_POWER_COST_SCHOOL || + effectInfo.ApplyAuraName == SPELL_AURA_MOD_POWER_COST_SCHOOL_PCT || + effectInfo.ApplyAuraName == SPELL_AURA_MOD_MANA_REGEN_INTERRUPT); + } + + bool IsDefensiveTankEffect(SpellEffectInfo const& effectInfo) + { + if (effectInfo.Effect != SPELL_EFFECT_APPLY_AURA) + return false; + + uint32 const tankRatingsMask = + (1u << CR_DEFENSE_SKILL) | + (1u << CR_DODGE) | + (1u << CR_PARRY) | + (1u << CR_BLOCK) | + (1u << CR_HIT_TAKEN_MELEE) | + (1u << CR_HIT_TAKEN_RANGED) | + (1u << CR_HIT_TAKEN_SPELL) | + (1u << CR_CRIT_TAKEN_MELEE) | + (1u << CR_CRIT_TAKEN_RANGED) | + (1u << CR_CRIT_TAKEN_SPELL); + + switch (effectInfo.ApplyAuraName) + { + case SPELL_AURA_MOD_RESISTANCE: + return (effectInfo.MiscValue & SPELL_SCHOOL_MASK_NORMAL) != 0; + case SPELL_AURA_MOD_RATING: + return (effectInfo.MiscValue & tankRatingsMask) != 0; + case SPELL_AURA_MOD_INCREASE_HEALTH: + case SPELL_AURA_MOD_INCREASE_HEALTH_PERCENT: + case SPELL_AURA_MOD_PARRY_PERCENT: + case SPELL_AURA_MOD_DODGE_PERCENT: + case SPELL_AURA_MOD_BLOCK_PERCENT: + case SPELL_AURA_MOD_DAMAGE_PERCENT_TAKEN: + return true; + default: + return false; + } + } +} + CastSpellAction::CastSpellAction(PlayerbotAI* botAI, std::string const spell) : Action(botAI, spell), range(botAI->GetRange("spell")), spell(spell) {} @@ -429,52 +540,109 @@ bool UseTrinketAction::UseTrinket(Item* item) uint8 bagIndex = item->GetBagSlot(); uint8 slot = item->GetSlot(); - // uint8 spell_index = 0; //not used, line marked for removal. uint8 cast_count = 1; ObjectGuid item_guid = item->GetGUID(); uint32 glyphIndex = 0; uint8 castFlags = 0; uint32 targetFlag = TARGET_FLAG_NONE; uint32 spellId = 0; + int32 itemSpellCooldown = 0; + uint32 itemSpellCategory = 0; + int32 itemSpellCategoryCooldown = 0; + for (uint8 i = 0; i < MAX_ITEM_PROTO_SPELLS; ++i) { if (item->GetTemplate()->Spells[i].SpellId > 0 && item->GetTemplate()->Spells[i].SpellTrigger == ITEM_SPELLTRIGGER_ON_USE) { spellId = item->GetTemplate()->Spells[i].SpellId; - const SpellInfo* spellInfo = sSpellMgr->GetSpellInfo(spellId); + itemSpellCooldown = item->GetTemplate()->Spells[i].SpellCooldown; + itemSpellCategory = item->GetTemplate()->Spells[i].SpellCategory; + itemSpellCategoryCooldown = item->GetTemplate()->Spells[i].SpellCategoryCooldown; + uint64 const itemCooldownKey = (static_cast(item->GetEntry()) << 32) | spellId; + uint32 const now = getMSTime(); + if (itemSpellCooldown > 0) + { + auto const itemCooldownItr = trinketItemCooldownExpiries.find(itemCooldownKey); + if (itemCooldownItr != trinketItemCooldownExpiries.end()) + { + if (itemCooldownItr->second > now) + return false; + + trinketItemCooldownExpiries.erase(itemCooldownItr); + } + } + + if (itemSpellCategory && itemSpellCategoryCooldown > 0) + { + auto const categoryCooldownItr = trinketCategoryCooldownExpiries.find(itemSpellCategory); + if (categoryCooldownItr != trinketCategoryCooldownExpiries.end()) + { + if (categoryCooldownItr->second > now) + return false; + + trinketCategoryCooldownExpiries.erase(categoryCooldownItr); + } + } + + const SpellInfo* spellInfo = sSpellMgr->GetSpellInfo(spellId); if (!spellInfo || !spellInfo->IsPositive()) return false; bool applyAura = false; + bool restoresMana = false; + bool improvesManaEfficiency = false; + bool defensiveTankEffect = false; for (int i = 0; i < MAX_SPELL_EFFECTS; i++) { const SpellEffectInfo& effectInfo = spellInfo->Effects[i]; if (effectInfo.Effect == SPELL_EFFECT_APPLY_AURA) - { applyAura = true; - break; + + restoresMana = restoresMana || IsManaRestoreEffect(effectInfo); + improvesManaEfficiency = improvesManaEfficiency || IsManaEfficiencyEffect(effectInfo); + defensiveTankEffect = defensiveTankEffect || IsDefensiveTankEffect(effectInfo); + } + + if (!applyAura && !restoresMana) + return false; + + if (restoresMana || improvesManaEfficiency) + { + if (!AI_VALUE2(bool, "has mana", "self target")) + return false; + + uint8 const manaPct = AI_VALUE2(uint8, "mana", "self target"); + if ((restoresMana && manaPct >= sPlayerbotAIConfig.mediumMana) || + manaPct >= sPlayerbotAIConfig.highMana) + { + return false; } } - if (!applyAura) + if (defensiveTankEffect) + { + uint8 const healthPct = AI_VALUE2(uint8, "health", "self target"); + if (healthPct > sPlayerbotAIConfig.lowHealth) + return false; + } + + auto const& mixedTriggerTrinketSpellIds = GetMixedTriggerTrinketSpellIds(); + // Exclude trinkets that expose the same spell as both ON_EQUIP and ON_USE across + // item templates. Those are equip/proc effects leaking into the active-use path, + // as seen with the error versions of Oracle Talisman of Ablution (44870) and + // Frenzyheart Insignia of Fury (44869). + if (mixedTriggerTrinketSpellIds.find(spellId) != mixedTriggerTrinketSpellIds.end()) return false; - uint32 spellProcFlag = spellInfo->ProcFlags; - - // Handle items with procflag "if you kill a target that grants honor or experience" - // Bots will "learn" the trinket proc, so CanCastSpell() will be true - // e.g. on Item https://www.wowhead.com/wotlk/item=44074/oracle-talisman-of-ablution leading to - // constant casting of the proc spell onto themselfes https://www.wowhead.com/wotlk/spell=59787/oracle-ablutions - // This will lead to multiple hundreds of entries in m_appliedAuras -> Once killing an enemy -> Big diff time spikes - if (spellProcFlag != 0) return false; - - if (!botAI->CanCastSpell(spellId, bot, false)) + if (!botAI->CanCastSpell(spellId, bot, false, nullptr, item)) return false; + break; } } + if (!spellId) return false; @@ -483,7 +651,25 @@ bool UseTrinketAction::UseTrinket(Item* item) targetFlag = TARGET_FLAG_NONE; packet << targetFlag << bot->GetPackGUID(); + bot->GetSession()->HandleUseItemOpcode(packet); + + uint32 const now = getMSTime(); + uint32 const cooldownDelay = bot->GetSpellCooldownDelay(spellId); + if (cooldownDelay > 0) + { + if (itemSpellCooldown > 0) + { + uint64 const itemCooldownKey = (static_cast(item->GetEntry()) << 32) | spellId; + trinketItemCooldownExpiries[itemCooldownKey] = now + static_cast(itemSpellCooldown); + } + + if (itemSpellCategory && itemSpellCategoryCooldown > 0) + { + trinketCategoryCooldownExpiries[itemSpellCategory] = now + static_cast(itemSpellCategoryCooldown); + } + } + return true; } diff --git a/src/Ai/Base/Actions/GenericSpellActions.h b/src/Ai/Base/Actions/GenericSpellActions.h index 5c52b7fd9..c17d96907 100644 --- a/src/Ai/Base/Actions/GenericSpellActions.h +++ b/src/Ai/Base/Actions/GenericSpellActions.h @@ -334,6 +334,10 @@ public: protected: bool UseTrinket(Item* trinket); + +private: + std::unordered_map trinketItemCooldownExpiries; + std::unordered_map trinketCategoryCooldownExpiries; }; class CastSpellOnEnemyHealerAction : public CastSpellAction diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index 06e88a67b..c01769eb8 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -215,7 +215,7 @@ bool PlayerbotAIConfig::Initialize() attunementQuests); LoadSet>( - sConfigMgr->GetOption("AiPlayerbot.UnobtainableItems", "12468,46978"), + sConfigMgr->GetOption("AiPlayerbot.UnobtainableItems", "12468,44869,44870,46978"), unobtainableItems); botAutologin = sConfigMgr->GetOption("AiPlayerbot.BotAutologin", false);