From f5363c94655e293d530e0e05ba28a73bdcb2729e Mon Sep 17 00:00:00 2001 From: Crow Date: Sat, 4 Apr 2026 05:59:45 -0500 Subject: [PATCH] Fix Warrior Battle Shout Spam (#2259) ## Pull Request Description There is a known problem that Warriors will repeatedly spam Battle Shout (BS) if there is a stronger Blessing of Might (BoM) present that they cannot override. It has been long known that the issue arises from the lack of a BS trigger. It was not a problem in the past because BS and BoM used to (erroneously) stack, and that was eliminated when AC ported TC's stacking rules. This PR adds a trigger for BS that prevents the bot from attempting to cast it if a stronger BoM (or Greater BoM) is present. To do this, it compares the AP bonus of any BoM/GBoM applied to the Warrior bot, and then will attempt BS only if the Warrior has a stronger one. I also did some minor reformatting of the other Warrior triggers, mostly just deleting comments and extra braces. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. The trigger is fairly complicated because it uses an AP comparison and takes into account spell ranks and the talent Commanding Presence (which increases the strength of BS). The bug could be solved by a very simple aura check for BoM/GBoM. That's suboptimal though because BS will be stronger if the Paladin has a lower rank of BoM or the Warrior has Commanding Presence and the Paladin does not have Improved BoM. At the end of the day, this is a new trigger that runs every tick, but it's not really more expensive than generic BuffTriggers, which all do this. And it is needed to solve a very annoying bug that essentially renders Warriors useless in any optimized raid unless BS is disabled. I don't believe this more robust trigger (as opposed to just calling BuffTrigger and additionally checking for any BoM/GBoM aura) has any further impact on performance--any added logic is practically free AFAIK (e.g., checking if the bot has a particular talent is just an integer lookup). ## How to Test the Changes Form a group with a Warrior and Paladin bot, with the Warrior bot having a weaker BS than the Paladin's BoM (this is naturally the case at max level, unless the Warrior has Commanding Presence and the Paladin does not have Improved BoM). Have the Paladin cast BoM on the Warrior if it has not done so automatically. Enter combat and see if the Warrior uses BS. Try again with the Warrior having a stronger BS. ## Impact Assessment - 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**) As noted above, this is a new BuffTrigger that runs every tick. - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Default behavior was broken. - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) This is, with respect to the code, a pretty complex trigger. But that should not impact other logic, and if need be, there is a very simple version that could be implemented that would at least fix the bug, as noted above. ## Messages to Translate - Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance - Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) I used Claude Sonnet 4.6. This is essentially the first PR that I entirely vibe coded. I knew what the problem was and how to fix it, including the conditions I wanted in the trigger, and the LLM did the rest. I can tell you what each block does and why that's needed, but I can't tell you how they all work technically. I tested this ingame in scenarios where the available BS was stronger and not as strong, and in each case, the correct behavior was observed. I've since run several raids without BS disabled on my Warriors, and they are correctly not using it (as my Ret Paladin is applying a stronger BoM). The trigger does not show up as an expensive one with pmon (unlike, say, VIgilance, which is probably the most expensive class trigger in the mod (excluding any DK triggers just because I've never used one so I have no idea)). ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- .../Class/Warrior/Trigger/WarriorTriggers.cpp | 114 ++++++++++++------ .../Class/Warrior/Trigger/WarriorTriggers.h | 9 +- 2 files changed, 83 insertions(+), 40 deletions(-) diff --git a/src/Ai/Class/Warrior/Trigger/WarriorTriggers.cpp b/src/Ai/Class/Warrior/Trigger/WarriorTriggers.cpp index 5aa419c92..f6e5bd234 100644 --- a/src/Ai/Class/Warrior/Trigger/WarriorTriggers.cpp +++ b/src/Ai/Class/Warrior/Trigger/WarriorTriggers.cpp @@ -4,7 +4,6 @@ */ #include "WarriorTriggers.h" - #include "Playerbots.h" bool BloodrageBuffTrigger::IsActive() @@ -16,15 +15,11 @@ bool BloodrageBuffTrigger::IsActive() bool VigilanceTrigger::IsActive() { if (!bot->HasSpell(50720)) - { return false; - } Group* group = bot->GetGroup(); if (!group) - { return false; - } Player* currentVigilanceTarget = nullptr; Player* mainTank = nullptr; @@ -33,37 +28,23 @@ bool VigilanceTrigger::IsActive() Player* highestGearScorePlayer = nullptr; uint32 highestGearScore = 0; - // Iterate once through the group to gather all necessary information for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member || member == bot || !member->IsAlive()) continue; - // Check if member has Vigilance applied by the bot if (!currentVigilanceTarget && botAI->HasAura("vigilance", member, false, true)) - { currentVigilanceTarget = member; - } - // Identify Main Tank if (!mainTank && botAI->IsMainTank(member)) - { mainTank = member; - } - - // Identify Assist Tanks - if (assistTank1 == nullptr && botAI->IsAssistTankOfIndex(member, 0)) - { + else if (!assistTank1 && botAI->IsAssistTankOfIndex(member, 0)) assistTank1 = member; - } - else if (assistTank2 == nullptr && botAI->IsAssistTankOfIndex(member, 1)) - { + else if (!assistTank2 && botAI->IsAssistTankOfIndex(member, 1)) assistTank2 = member; - } - // Determine Highest Gear Score - uint32 gearScore = botAI->GetEquipGearScore(member/*, false, false*/); + uint32 gearScore = botAI->GetEquipGearScore(member); if (gearScore > highestGearScore) { highestGearScore = gearScore; @@ -71,33 +52,20 @@ bool VigilanceTrigger::IsActive() } } - // Determine the highest-priority target Player* highestPriorityTarget = mainTank ? mainTank : (assistTank1 ? assistTank1 : (assistTank2 ? assistTank2 : highestGearScorePlayer)); - // Trigger if no Vigilance is active or the current target is not the highest-priority target if (!currentVigilanceTarget || currentVigilanceTarget != highestPriorityTarget) - { return true; - } - return false; // No need to reassign Vigilance + return false; } bool ShatteringThrowTrigger::IsActive() { - // Spell cooldown check - if (!bot->HasSpell(64382)) - { + if (!bot->HasSpell(64382) || bot->HasSpellCooldown(64382)) return false; - } - - // Spell cooldown check - if (bot->HasSpellCooldown(64382)) - { - return false; - } GuidVector enemies = AI_VALUE(GuidVector, "possible targets"); @@ -107,7 +75,6 @@ bool ShatteringThrowTrigger::IsActive() if (!enemy || !enemy->IsAlive() || enemy->IsFriendlyTo(bot)) continue; - // Check if the enemy is within 25 yards and has the specific auras if (bot->IsWithinDistInMap(enemy, 25.0f) && (enemy->HasAura(642) || // Divine Shield enemy->HasAura(45438) || // Ice Block @@ -117,5 +84,74 @@ bool ShatteringThrowTrigger::IsActive() } } - return false; // No valid targets within range + return false; +} + +bool BattleShoutTrigger::IsActive() +{ + if (!BuffTrigger::IsActive()) + return false; + + uint32 battleShoutSpellId = AI_VALUE2(uint32, "spell id", "battle shout"); + if (!battleShoutSpellId) + return false; + + SpellInfo const* bsInfo = sSpellMgr->GetSpellInfo(battleShoutSpellId); + if (!bsInfo) + return false; + + int32 bsApValue = 0; + for (uint8 eff = 0; eff < MAX_SPELL_EFFECTS; ++eff) + { + if (bsInfo->Effects[eff].ApplyAuraName == SPELL_AURA_MOD_ATTACK_POWER) + { + bsApValue = bsInfo->Effects[eff].BasePoints + 1; + break; + } + } + if (!bsApValue) + return false; + + static const uint32 commandingPresenceSpells[] = { + 12318, 12857, 12858, 12860, 12861 }; + static const float commandingPresenceBonus[] = { + 0.05f, 0.10f, 0.15f, 0.20f, 0.25f }; + + float cpBonus = 0.0f; + for (int rank = 4; rank >= 0; --rank) + { + if (bot->HasAura(commandingPresenceSpells[rank])) + { + cpBonus = commandingPresenceBonus[rank]; + break; + } + } + int32 effectiveBsAp = int32(bsApValue * (1.0f + cpBonus)); + + static const char* blessingNames[] = { + "blessing of might", "greater blessing of might", nullptr + }; + for (int i = 0; blessingNames[i] != nullptr; ++i) + { + Aura* bom = botAI->GetAura(blessingNames[i], bot); + if (!bom) + continue; + + SpellInfo const* bomInfo = bom->GetSpellInfo(); + if (!bomInfo) + continue; + + for (uint8 eff = 0; eff < MAX_SPELL_EFFECTS; ++eff) + { + if (bomInfo->Effects[eff].ApplyAuraName == SPELL_AURA_MOD_ATTACK_POWER) + { + int32 bomApValue = bomInfo->Effects[eff].BasePoints + 1; + if (bomApValue >= effectiveBsAp) + return false; + break; + } + } + } + + return true; } diff --git a/src/Ai/Class/Warrior/Trigger/WarriorTriggers.h b/src/Ai/Class/Warrior/Trigger/WarriorTriggers.h index 563d70769..8a9ed9248 100644 --- a/src/Ai/Class/Warrior/Trigger/WarriorTriggers.h +++ b/src/Ai/Class/Warrior/Trigger/WarriorTriggers.h @@ -9,7 +9,13 @@ #include "GenericTriggers.h" #include "PlayerbotAI.h" -BUFF_TRIGGER(BattleShoutTrigger, "battle shout"); +class BattleShoutTrigger : public BuffTrigger +{ +public: + BattleShoutTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "battle shout") {} + bool IsActive() override; +}; + BUFF_TRIGGER(BattleStanceTrigger, "battle stance"); BUFF_TRIGGER(DefensiveStanceTrigger, "defensive stance"); BUFF_TRIGGER(BerserkerStanceTrigger, "berserker stance"); @@ -85,4 +91,5 @@ public: // public: // SlamTrigger(PlayerbotAI* ai) : HasAuraTrigger(ai, "slam!") {} // }; + #endif