From a87999bef55c213ea093af842ff1213a2eefe9f8 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 4 Apr 2026 07:32:32 +0200 Subject: [PATCH] Berserker Rage support (#2261) ## Pull Request Description Added Berserker Rage usage (in combat and outside combat) for Warrior (all specs) Related with: #1755 ## How to Test the Changes - invite warrior to party - [optional] start combat (for example with dummy) - use command `.aura 6215` - bot should cast Berserker Rage ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Warriors use Berserker Rage when they got fear, sleep or sap - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## 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**) Copilot CLI to review ## 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 obraz [Berserker Rage performance test.txt](https://github.com/user-attachments/files/26333365/Berserker.Rage.performance.test.txt) --- src/Ai/Base/Trigger/GenericTriggers.cpp | 7 +++++ src/Ai/Base/Trigger/GenericTriggers.h | 8 ++++++ src/Ai/Base/TriggerContext.h | 2 ++ .../Class/Warrior/Action/WarriorActions.cpp | 27 +++++++++++++++++++ src/Ai/Class/Warrior/Action/WarriorActions.h | 10 ++++++- .../GenericWarriorNonCombatStrategy.cpp | 24 +++++++++++++++++ .../GenericWarriorNonCombatStrategy.h | 2 +- .../Strategy/GenericWarriorStrategy.cpp | 21 ++++++++++++++- 8 files changed, 98 insertions(+), 3 deletions(-) diff --git a/src/Ai/Base/Trigger/GenericTriggers.cpp b/src/Ai/Base/Trigger/GenericTriggers.cpp index 68f5a5239..27dd5999c 100644 --- a/src/Ai/Base/Trigger/GenericTriggers.cpp +++ b/src/Ai/Base/Trigger/GenericTriggers.cpp @@ -481,6 +481,13 @@ bool FearCharmSleepTrigger::IsActive() bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP); } +bool FearSleepSapTrigger::IsActive() +{ + return bot->HasAuraType(SPELL_AURA_MOD_FEAR) || + bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP) || + bot->HasAuraWithMechanic(1 << MECHANIC_SAPPED); +} + bool HasAuraStackTrigger::IsActive() { Aura* aura = botAI->GetAura(getName(), GetTarget(), false, true, stack); diff --git a/src/Ai/Base/Trigger/GenericTriggers.h b/src/Ai/Base/Trigger/GenericTriggers.h index f78728cd5..7a44112f3 100644 --- a/src/Ai/Base/Trigger/GenericTriggers.h +++ b/src/Ai/Base/Trigger/GenericTriggers.h @@ -762,6 +762,14 @@ public: bool IsActive() override; }; +class FearSleepSapTrigger : public Trigger +{ +public: + FearSleepSapTrigger(PlayerbotAI* botAI) : Trigger(botAI, "fear sleep sap", 1) {} + + bool IsActive() override; +}; + class IsSwimmingTrigger : public Trigger { public: diff --git a/src/Ai/Base/TriggerContext.h b/src/Ai/Base/TriggerContext.h index 63f9be404..c77df3a31 100644 --- a/src/Ai/Base/TriggerContext.h +++ b/src/Ai/Base/TriggerContext.h @@ -62,6 +62,7 @@ public: creators["generic boost"] = &TriggerContext::generic_boost; creators["loss of control"] = &TriggerContext::loss_of_control; creators["fear charm sleep"] = &TriggerContext::fear_charm_sleep; + creators["fear sleep sap"] = &TriggerContext::fear_sleep_sap; creators["protect party member"] = &TriggerContext::protect_party_member; @@ -369,6 +370,7 @@ private: static Trigger* generic_boost(PlayerbotAI* botAI) { return new GenericBoostTrigger(botAI); } static Trigger* loss_of_control(PlayerbotAI* botAI) { return new LossOfControlTrigger(botAI); } static Trigger* fear_charm_sleep(PlayerbotAI* botAI) { return new FearCharmSleepTrigger(botAI); } + static Trigger* fear_sleep_sap(PlayerbotAI* botAI) { return new FearSleepSapTrigger(botAI); } static Trigger* PartyMemberCriticalHealth(PlayerbotAI* botAI) { return new PartyMemberCriticalHealthTrigger(botAI); diff --git a/src/Ai/Class/Warrior/Action/WarriorActions.cpp b/src/Ai/Class/Warrior/Action/WarriorActions.cpp index 9f15cf767..20a42c219 100644 --- a/src/Ai/Class/Warrior/Action/WarriorActions.cpp +++ b/src/Ai/Class/Warrior/Action/WarriorActions.cpp @@ -7,6 +7,33 @@ #include "Playerbots.h" +bool CastBerserkerRageAction::isPossible() +{ + if (botAI->IsInVehicle() && !botAI->IsInVehicle(false, false, true)) + return false; + + uint32 spellId = AI_VALUE2(uint32, "spell id", spell); + if (!spellId) + return false; + + if (!bot->HasSpell(spellId)) + return false; + + if (bot->HasSpellCooldown(spellId)) + return false; + + return true; +} + +bool CastBerserkerRageAction::isUseful() +{ + return (bot->HasAuraType(SPELL_AURA_MOD_FEAR) || + bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP) || + bot->HasAuraWithMechanic(1 << MECHANIC_SAPPED)) + && !botAI->HasAura("berserker rage", bot) + && CastSpellAction::isUseful(); +} + bool CastSunderArmorAction::isUseful() { Aura* aura = botAI->GetAura("sunder armor", GetTarget(), false, true); diff --git a/src/Ai/Class/Warrior/Action/WarriorActions.h b/src/Ai/Class/Warrior/Action/WarriorActions.h index 7910fc0d8..da004fc70 100644 --- a/src/Ai/Class/Warrior/Action/WarriorActions.h +++ b/src/Ai/Class/Warrior/Action/WarriorActions.h @@ -78,7 +78,15 @@ REACH_ACTION(CastInterceptAction, "intercept", 8.0f); ENEMY_HEALER_ACTION(CastInterceptOnEnemyHealerAction, "intercept"); SNARE_ACTION(CastInterceptOnSnareTargetAction, "intercept"); MELEE_ACTION(CastSlamAction, "slam"); -BUFF_ACTION(CastBerserkerRageAction, "berserker rage"); +class CastBerserkerRageAction : public CastSpellAction +{ +public: + CastBerserkerRageAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "berserker rage") {} + + std::string const GetTargetName() override { return "self target"; } + bool isPossible() override; + bool isUseful() override; +}; MELEE_ACTION(CastWhirlwindAction, "whirlwind"); MELEE_ACTION(CastPummelAction, "pummel"); ENEMY_HEALER_ACTION(CastPummelOnEnemyHealerAction, "pummel"); diff --git a/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.cpp b/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.cpp index 091aed2b8..05a6c2c63 100644 --- a/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.cpp +++ b/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.cpp @@ -7,9 +7,33 @@ #include "Playerbots.h" +class GenericWarriorNonCombatStrategyActionNodeFactory : public NamedObjectFactory +{ +public: + GenericWarriorNonCombatStrategyActionNodeFactory() { creators["berserker rage"] = &berserker_rage; } + +private: + static ActionNode* berserker_rage([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "berserker rage", + /*P*/ { NextAction("berserker stance") }, + /*A*/ {}, + /*C*/ {} + ); + } +}; + +GenericWarriorNonCombatStrategy::GenericWarriorNonCombatStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) +{ + actionNodeFactories.Add(new GenericWarriorNonCombatStrategyActionNodeFactory()); +} + void GenericWarriorNonCombatStrategy::InitTriggers(std::vector& triggers) { NonCombatStrategy::InitTriggers(triggers); triggers.push_back(new TriggerNode("often", { NextAction("apply stone", 1.0f) })); + triggers.push_back(new TriggerNode( + "fear sleep sap", { NextAction("berserker rage", ACTION_EMERGENCY + 1) })); } diff --git a/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.h b/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.h index 276432583..160d8df8f 100644 --- a/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.h +++ b/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.h @@ -13,7 +13,7 @@ class PlayerbotAI; class GenericWarriorNonCombatStrategy : public NonCombatStrategy { public: - GenericWarriorNonCombatStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) {} + GenericWarriorNonCombatStrategy(PlayerbotAI* botAI); std::string const getName() override { return "nc"; } void InitTriggers(std::vector& triggers) override; diff --git a/src/Ai/Class/Warrior/Strategy/GenericWarriorStrategy.cpp b/src/Ai/Class/Warrior/Strategy/GenericWarriorStrategy.cpp index 178984de9..18781ef31 100644 --- a/src/Ai/Class/Warrior/Strategy/GenericWarriorStrategy.cpp +++ b/src/Ai/Class/Warrior/Strategy/GenericWarriorStrategy.cpp @@ -7,9 +7,26 @@ #include "Playerbots.h" +class GenericWarriorStrategyActionNodeFactory : public NamedObjectFactory +{ +public: + GenericWarriorStrategyActionNodeFactory() { creators["berserker rage"] = &berserker_rage; } + +private: + static ActionNode* berserker_rage([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "berserker rage", + /*P*/ { NextAction("berserker stance") }, + /*A*/ {}, + /*C*/ {} + ); + } +}; + GenericWarriorStrategy::GenericWarriorStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) { - + actionNodeFactories.Add(new GenericWarriorStrategyActionNodeFactory()); } void GenericWarriorStrategy::InitTriggers(std::vector& triggers) @@ -17,6 +34,8 @@ void GenericWarriorStrategy::InitTriggers(std::vector& triggers) CombatStrategy::InitTriggers(triggers); triggers.push_back(new TriggerNode( "enemy out of melee", { NextAction("reach melee", ACTION_HIGH + 1) })); + triggers.push_back(new TriggerNode( + "fear sleep sap", { NextAction("berserker rage", ACTION_EMERGENCY + 1) })); } class WarrirorAoeStrategyActionNodeFactory : public NamedObjectFactory