From 79562be2e57eb53089bf47899225cf8bd4eb864d Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 3 Apr 2026 15:24:37 -0500 Subject: [PATCH 01/18] Fix Hunter Aspect Switching + Trigger Cleanups (#2203) # Pull Request Note: When I reference Aspect of the Hawk below, it also means Aspect of the Dragonhawk (the code will use Dragonhawk if the Hunter has it, Hawk if not, they share actions, triggers, and strategies). Hunter Aspects are currently bugged. All Hunters, regardless of spec or strategy, are hardcoded to use Aspect of the Hawk when mana is at 70%+ and Aspect of the Viper when mana drops to "lowMana" from the config (default is 15%), divided by 2. This means the following: - Hawk (bdps) and Viper (bmana) strategies are useless - Pack (bspeed) and Wild (rnature) strategies are applied, but bots will rapidly switch back and forth between Pack/Wild and Hawk/Viper, depending on strategy and mana level. This PR addresses the issues by doing the following: - Global Hawk strategy is removed. Now you need to set bdps for Hunters to use Hawk, but bdps remains the default Aspect strategy for all Hunters. - Dedicated Viper strategy is removed, leaving the global strategy. However, Viper will be used (when lowMana/2) ONLY if the bot is set to bdps. If the bot has the Wild or Pack strategy, they will not switch to Viper at all. I did this because I am assuming if you are using Wild or Pack, you need them for reasons other than to pump DPS. - The threshold to switch back to Hawk is lowered from 70% to 60%. The gap between lowMana/2 and 60% is now filled--if bdps is on, Hunters will switch to Hawk whenever above the Viper threshold, _except_ for when they have the Viper aura, in which case they will not switch to Hawk until 60% mana. This lets the Hunter build back mana before swapping back to Hawk (more like general player behavior) while still letting them swap from other Aspects to Hawk without needing to be all the way at 60% mana. - Gets rid of a weird condition in the Hawk trigger that would make it so that Hunters would switch to Hawk when at exactly 0 mana. I'm not sure what the point of that is. Also, I refactored the triggers a bit because I noticed there was some dead code in there. I didn't do a comprehensive refactor, but there was a lot of stuff that clearly didn't make sense even to my eyes, like back-to-back returns. I think there's more unnecessary code even just in the triggers, but I didn't want to get too into the weeds with this PR. --- ## Design Philosophy We prioritize **stability, performance, and predictability** over behavioral realism. Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and long-term robustness. Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all participants. Because every action and decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and negatively affect both players and world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a project goal. Increased behavioral realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and maintained continuously as the system evolves. If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision model**. More complex behavior should only be available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. Principles: - **Stability before intelligence** A stable system is always preferred over a smarter one. - **Performance is a shared resource** Any increase in bot cost affects all players and all bots. - **Simple logic scales better than smart logic** Predictable behavior under load is more valuable than perfect decisions. - **Complexity must justify itself** If a feature cannot clearly explain its cost, it should not exist. - **Defaults must be cheap** Expensive behavior must always be optional and clearly communicated. - **Bots should look reasonable, not perfect** The goal is believable behavior, not human simulation. Before submitting, confirm that this change aligns with those principles. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? - Describe the **cheapest implementation** that produces an acceptable result? - Describe the **runtime cost** when this logic executes across many bots? I don't expect there to be any impact on costs, and if anything this PR removes some unneeded checks from triggers. --- ## How to Test the Changes - Step-by-step instructions to test the change - Any required setup (e.g. multiple players, bots, specific configuration) - Expected behavior and how to verify it The easiest way is to go shoot a dummy with Volley until low on mana and then toggle on selfbot. You can do this with various Aspects active to test. ## Complexity & Impact Does this change add new decision branches? - - [X] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [X] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [X] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [ ] No - - [X] Yes (**explain why**) Described above. Default behavior is broken. If this introduces more advanced or AI-heavy logic: - - [X] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [ ] No - - [X] Yes (**explain below**) I asked Claude some questions about the triggers to make sure I didn't screw anything up. If yes, please specify: - AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) - Which parts of the change were influenced or generated - Whether the result was manually reviewed and adapted AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest about what they do and do not understand. --- ## 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 --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- src/Ai/Class/Hunter/Action/HunterActions.cpp | 49 ++-- src/Ai/Class/Hunter/Action/HunterActions.h | 219 +++++++++++------- src/Ai/Class/Hunter/HunterAiObjectContext.cpp | 63 +++-- .../Strategy/BeastMasteryHunterStrategy.cpp | 34 +-- .../GenericHunterNonCombatStrategy.cpp | 57 ++--- .../Hunter/Strategy/GenericHunterStrategy.cpp | 58 +---- .../Hunter/Strategy/GenericHunterStrategy.h | 9 - .../Hunter/Strategy/HunterBuffStrategies.cpp | 58 +++-- .../Hunter/Strategy/HunterBuffStrategies.h | 27 +-- .../Strategy/MarksmanshipHunterStrategy.cpp | 38 +-- .../Strategy/SurvivalHunterStrategy.cpp | 73 ++---- .../Class/Hunter/Trigger/HunterTriggers.cpp | 77 +++--- src/Ai/Class/Hunter/Trigger/HunterTriggers.h | 22 +- .../Multiplier/RaidIccMultipliers.cpp | 4 +- src/Bot/Factory/AiFactory.cpp | 2 +- src/Bot/Factory/PlayerbotFactory.cpp | 2 +- 16 files changed, 354 insertions(+), 438 deletions(-) diff --git a/src/Ai/Class/Hunter/Action/HunterActions.cpp b/src/Ai/Class/Hunter/Action/HunterActions.cpp index 18e5d590579..a49f9a10e63 100644 --- a/src/Ai/Class/Hunter/Action/HunterActions.cpp +++ b/src/Ai/Class/Hunter/Action/HunterActions.cpp @@ -16,18 +16,15 @@ bool CastViperStingAction::isUseful() AI_VALUE2(uint8, "mana", "current target") >= 30; } -bool CastAspectOfTheCheetahAction::isUseful() -{ - return !botAI->HasAnyAuraOf(GetTarget(), "aspect of the cheetah", "aspect of the pack", nullptr); -} - bool CastAspectOfTheHawkAction::isUseful() { Unit* target = GetTarget(); if (!target) return false; + if (bot->HasSpell(61846) || bot->HasSpell(61847)) // Aspect of the Dragonhawk spell IDs return false; + return true; } @@ -36,11 +33,14 @@ bool CastArcaneShotAction::isUseful() Unit* target = GetTarget(); if (!target) return false; - if (bot->HasSpell(53301) || bot->HasSpell(60051) || bot->HasSpell(60052) || bot->HasSpell(60053)) // Explosive Shot spell IDs + + if (bot->HasSpell(53301) || bot->HasSpell(60051) || + bot->HasSpell(60052) || bot->HasSpell(60053)) // Explosive Shot spell IDs return false; // Armor Penetration rating check - will not cast Arcane Shot above 435 ArP - int32 armorPenRating = bot->GetUInt32Value(PLAYER_FIELD_COMBAT_RATING_1) + bot->GetUInt32Value(CR_ARMOR_PENETRATION); + int32 armorPenRating = + bot->GetUInt32Value(PLAYER_FIELD_COMBAT_RATING_1) + bot->GetUInt32Value(CR_ARMOR_PENETRATION); if (armorPenRating > 435) return false; @@ -52,18 +52,26 @@ bool CastImmolationTrapAction::isUseful() Unit* target = GetTarget(); if (!target) return false; - if (bot->HasSpell(13813) || bot->HasSpell(14316) || bot->HasSpell(14317) || bot->HasSpell(27025) || bot->HasSpell(49066) || bot->HasSpell(49067)) // Explosive Trap spell IDs + + if (bot->HasSpell(13813) || bot->HasSpell(14316) || bot->HasSpell(14317) || bot->HasSpell(27025) || + bot->HasSpell(49066) || bot->HasSpell(49067)) // Explosive Trap spell IDs return false; + return true; } -Value* CastFreezingTrap::GetTargetValue() { return context->GetValue("cc target", "freezing trap"); } +Value* CastFreezingTrap::GetTargetValue() +{ + return context->GetValue("cc target", "freezing trap"); +} bool FeedPetAction::Execute(Event /*event*/) { - if (Pet* pet = bot->GetPet()) - if (pet->getPetType() == HUNTER_PET && pet->GetHappinessState() != HAPPY) - pet->SetPower(POWER_HAPPINESS, pet->GetMaxPower(Powers(POWER_HAPPINESS))); + if (Pet* pet = bot->GetPet(); pet && pet->getPetType() == HUNTER_PET && + pet->GetHappinessState() != HAPPY) + { + pet->SetPower(POWER_HAPPINESS, pet->GetMaxPower(Powers(POWER_HAPPINESS))); + } return true; } @@ -79,6 +87,7 @@ bool CastAutoShotAction::isUseful() { return false; } + return AI_VALUE(uint32, "active spell") != AI_VALUE2(uint32, "spell id", getName()); } @@ -87,6 +96,7 @@ bool CastDisengageAction::Execute(Event event) Unit* target = AI_VALUE(Unit*, "current target"); if (!target) return false; + // can cast spell check passed in isUseful() bot->SetOrientation(bot->GetAngle(target)); return CastSpellAction::Execute(event); @@ -97,11 +107,20 @@ bool CastDisengageAction::isUseful() return !botAI->HasStrategy("trap weave", BOT_STATE_COMBAT); } -Value* CastScareBeastCcAction::GetTargetValue() { return context->GetValue("cc target", "scare beast"); } +Value* CastScareBeastCcAction::GetTargetValue() +{ + return context->GetValue("cc target", "scare beast"); +} -bool CastScareBeastCcAction::Execute(Event /*event*/) { return botAI->CastSpell("scare beast", GetTarget()); } +bool CastScareBeastCcAction::Execute(Event /*event*/) +{ + return botAI->CastSpell("scare beast", GetTarget()); +} -bool CastWingClipAction::isUseful() { return CastSpellAction::isUseful() && !botAI->HasAura(spell, GetTarget()); } +bool CastWingClipAction::isUseful() +{ + return CastSpellAction::isUseful() && !botAI->HasAura(spell, GetTarget()); +} std::vector CastWingClipAction::getPrerequisites() { diff --git a/src/Ai/Class/Hunter/Action/HunterActions.h b/src/Ai/Class/Hunter/Action/HunterActions.h index 8e21135254d..a67f17780b5 100644 --- a/src/Ai/Class/Hunter/Action/HunterActions.h +++ b/src/Ai/Class/Hunter/Action/HunterActions.h @@ -19,52 +19,58 @@ class Unit; class CastTrueshotAuraAction : public CastBuffSpellAction { public: - CastTrueshotAuraAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "trueshot aura") {} + CastTrueshotAuraAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "trueshot aura") {} }; -class CastAspectOfTheHawkAction : public CastBuffSpellAction +class CastAspectOfTheDragonhawkAction : public CastBuffSpellAction { public: - CastAspectOfTheHawkAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the hawk") {} - bool isUseful() override; + CastAspectOfTheDragonhawkAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the dragonhawk") {} }; -class CastAspectOfTheMonkeyAction : public CastBuffSpellAction +class CastAspectOfTheHawkAction : public CastBuffSpellAction { public: - CastAspectOfTheMonkeyAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the monkey") {} + CastAspectOfTheHawkAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the hawk") {} + bool isUseful() override; }; -class CastAspectOfTheDragonhawkAction : public CastBuffSpellAction +class CastAspectOfTheMonkeyAction : public CastBuffSpellAction { public: - CastAspectOfTheDragonhawkAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the dragonhawk") {} + CastAspectOfTheMonkeyAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the monkey") {} }; class CastAspectOfTheWildAction : public CastBuffSpellAction { public: - CastAspectOfTheWildAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the wild") {} + CastAspectOfTheWildAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the wild") {} }; class CastAspectOfTheCheetahAction : public CastBuffSpellAction { public: - CastAspectOfTheCheetahAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the cheetah") {} - - bool isUseful() override; + CastAspectOfTheCheetahAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the cheetah") {} }; class CastAspectOfThePackAction : public CastBuffSpellAction { public: - CastAspectOfThePackAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the pack") {} + CastAspectOfThePackAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the pack") {} }; class CastAspectOfTheViperAction : public CastBuffSpellAction { public: - CastAspectOfTheViperAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the viper") {} + CastAspectOfTheViperAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the viper") {} }; // Cooldown Spells @@ -72,26 +78,28 @@ class CastAspectOfTheViperAction : public CastBuffSpellAction class CastRapidFireAction : public CastBuffSpellAction { public: - CastRapidFireAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "rapid fire") {} + CastRapidFireAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "rapid fire") {} }; class CastDeterrenceAction : public CastBuffSpellAction { public: - CastDeterrenceAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "deterrence") {} + CastDeterrenceAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "deterrence") {} }; class CastReadinessAction : public CastBuffSpellAction { public: - CastReadinessAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "readiness") {} + CastReadinessAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "readiness") {} }; class CastDisengageAction : public CastSpellAction { public: CastDisengageAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "disengage") {} - bool Execute(Event event) override; bool isUseful() override; }; @@ -101,14 +109,15 @@ class CastDisengageAction : public CastSpellAction class CastScareBeastAction : public CastSpellAction { public: - CastScareBeastAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "scare beast") {} + CastScareBeastAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "scare beast") {} }; class CastScareBeastCcAction : public CastSpellAction { public: - CastScareBeastCcAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "scare beast on cc") {} - + CastScareBeastCcAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "scare beast on cc") {} Value* GetTargetValue() override; bool Execute(Event event) override; }; @@ -116,34 +125,41 @@ class CastScareBeastCcAction : public CastSpellAction class CastFreezingTrap : public CastDebuffSpellAction { public: - CastFreezingTrap(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "freezing trap") {} - + CastFreezingTrap(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "freezing trap") {} Value* GetTargetValue() override; }; class CastWyvernStingAction : public CastDebuffSpellAction { public: - CastWyvernStingAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "wyvern sting", true) {} + CastWyvernStingAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "wyvern sting", true) {} }; class CastSilencingShotAction : public CastSpellAction { public: - CastSilencingShotAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "silencing shot") {} + CastSilencingShotAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "silencing shot") {} }; class CastConcussiveShotAction : public CastSnareSpellAction { public: - CastConcussiveShotAction(PlayerbotAI* botAI) : CastSnareSpellAction(botAI, "concussive shot") {} + CastConcussiveShotAction(PlayerbotAI* botAI) : + CastSnareSpellAction(botAI, "concussive shot") {} }; class CastIntimidationAction : public CastBuffSpellAction { public: - CastIntimidationAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "intimidation", false, 5000) {} - std::string const GetTargetName() override { return "pet target"; } + CastIntimidationAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "intimidation", false, 5000) {} + std::string const GetTargetName() override + { + return "pet target"; + } }; // Threat Spells @@ -151,19 +167,22 @@ class CastIntimidationAction : public CastBuffSpellAction class CastDistractingShotAction : public CastSpellAction { public: - CastDistractingShotAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "distracting shot") {} + CastDistractingShotAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "distracting shot") {} }; class CastMisdirectionOnMainTankAction : public BuffOnMainTankAction { public: - CastMisdirectionOnMainTankAction(PlayerbotAI* ai) : BuffOnMainTankAction(ai, "misdirection", true) {} + CastMisdirectionOnMainTankAction(PlayerbotAI* botAI) : + BuffOnMainTankAction(botAI, "misdirection", true) {} }; class CastFeignDeathAction : public CastBuffSpellAction { public: - CastFeignDeathAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "feign death") {} + CastFeignDeathAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "feign death") {} }; // Pet Spells @@ -172,7 +191,6 @@ class FeedPetAction : public Action { public: FeedPetAction(PlayerbotAI* botAI) : Action(botAI, "feed pet") {} - bool Execute(Event event) override; }; @@ -186,27 +204,39 @@ class CastMendPetAction : public CastAuraSpellAction { public: CastMendPetAction(PlayerbotAI* botAI) : CastAuraSpellAction(botAI, "mend pet") {} - std::string const GetTargetName() override { return "pet target"; } + std::string const GetTargetName() override + { + return "pet target"; + } }; class CastRevivePetAction : public CastBuffSpellAction { public: - CastRevivePetAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "revive pet") {} + CastRevivePetAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "revive pet") {} }; class CastKillCommandAction : public CastBuffSpellAction { public: - CastKillCommandAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "kill command", false, 5000) {} - std::string const GetTargetName() override { return "pet target"; } + CastKillCommandAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "kill command", false, 5000) {} + std::string const GetTargetName() override + { + return "pet target"; + } }; class CastBestialWrathAction : public CastBuffSpellAction { public: - CastBestialWrathAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "bestial wrath", false, 5000) {} - std::string const GetTargetName() override { return "pet target"; } + CastBestialWrathAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "bestial wrath", false, 5000) {} + std::string const GetTargetName() override + { + return "pet target"; + } }; // Direct Damage Spells @@ -215,14 +245,18 @@ class CastAutoShotAction : public CastSpellAction { public: CastAutoShotAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "auto shot") {} - ActionThreatType getThreatType() override { return ActionThreatType::None; } + ActionThreatType getThreatType() override + { + return ActionThreatType::None; + } bool isUseful() override; }; class CastArcaneShotAction : public CastSpellAction { public: - CastArcaneShotAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "arcane shot") {} + CastArcaneShotAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "arcane shot") {} bool isUseful() override; }; @@ -235,7 +269,8 @@ class CastAimedShotAction : public CastSpellAction class CastChimeraShotAction : public CastSpellAction { public: - CastChimeraShotAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "chimera shot") {} + CastChimeraShotAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "chimera shot") {} }; class CastSteadyShotAction : public CastSpellAction @@ -255,7 +290,8 @@ class CastKillShotAction : public CastSpellAction class CastHuntersMarkAction : public CastDebuffSpellAction { public: - CastHuntersMarkAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "hunter's mark") {} + CastHuntersMarkAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "hunter's mark") {} bool isUseful() override { // Bypass TTL check @@ -266,20 +302,23 @@ class CastHuntersMarkAction : public CastDebuffSpellAction class CastTranquilizingShotAction : public CastSpellAction { public: - CastTranquilizingShotAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "tranquilizing shot") {} + CastTranquilizingShotAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "tranquilizing shot") {} }; class CastViperStingAction : public CastDebuffSpellAction { public: - CastViperStingAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "viper sting", true) {} + CastViperStingAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "viper sting", true) {} bool isUseful() override; }; class CastSerpentStingAction : public CastDebuffSpellAction { public: - CastSerpentStingAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "serpent sting", true) {} + CastSerpentStingAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "serpent sting", true) {} bool isUseful() override { // Bypass TTL check @@ -290,7 +329,8 @@ class CastSerpentStingAction : public CastDebuffSpellAction class CastScorpidStingAction : public CastDebuffSpellAction { public: - CastScorpidStingAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "scorpid sting", true) {} + CastScorpidStingAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "scorpid sting", true) {} bool isUseful() override { // Bypass TTL check @@ -301,7 +341,8 @@ class CastScorpidStingAction : public CastDebuffSpellAction class CastSerpentStingOnAttackerAction : public CastDebuffSpellOnAttackerAction { public: - CastSerpentStingOnAttackerAction(PlayerbotAI* botAI) : CastDebuffSpellOnAttackerAction(botAI, "serpent sting", true) {} + CastSerpentStingOnAttackerAction(PlayerbotAI* botAI) + : CastDebuffSpellOnAttackerAction(botAI, "serpent sting", true) {} bool isUseful() override { // Bypass TTL check @@ -312,20 +353,23 @@ class CastSerpentStingOnAttackerAction : public CastDebuffSpellOnAttackerAction class CastImmolationTrapAction : public CastSpellAction { public: - CastImmolationTrapAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "immolation trap") {} + CastImmolationTrapAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "immolation trap") {} bool isUseful() override; }; class CastExplosiveTrapAction : public CastSpellAction { public: - CastExplosiveTrapAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "explosive trap") {} + CastExplosiveTrapAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "explosive trap") {} }; -class CastBlackArrow : public CastDebuffSpellAction +class CastBlackArrowAction : public CastDebuffSpellAction { public: - CastBlackArrow(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "black arrow", true) {} + CastBlackArrowAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "black arrow", true) {} bool isUseful() override { if (botAI->HasStrategy("trap weave", BOT_STATE_COMBAT)) @@ -335,77 +379,89 @@ class CastBlackArrow : public CastDebuffSpellAction } }; -class CastExplosiveShotAction : public CastDebuffSpellAction +class CastExplosiveShotBaseAction : public CastDebuffSpellAction { public: - CastExplosiveShotAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "explosive shot", true, 0.0f) {} - bool isUseful() override - { - // Bypass TTL check - return CastAuraSpellAction::isUseful(); - } + CastExplosiveShotBaseAction(PlayerbotAI* botAI) + : CastDebuffSpellAction(botAI, "explosive shot", true, 0.0f) {} }; // Rank 4 -class CastExplosiveShotRank4Action : public CastDebuffSpellAction +class CastExplosiveShotRank4Action : public CastExplosiveShotBaseAction { public: - CastExplosiveShotRank4Action(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "explosive shot", true, 0.0f) {} - - bool Execute(Event event) override { return botAI->CastSpell(60053, GetTarget()); } + CastExplosiveShotRank4Action(PlayerbotAI* botAI) : + CastExplosiveShotBaseAction(botAI) {} + bool Execute(Event event) override + { + return botAI->CastSpell(60053, GetTarget()); + } bool isUseful() override { Unit* target = GetTarget(); if (!target) return false; + return !target->HasAura(60053); } }; // Rank 3 -class CastExplosiveShotRank3Action : public CastDebuffSpellAction +class CastExplosiveShotRank3Action : public CastExplosiveShotBaseAction { public: - CastExplosiveShotRank3Action(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "explosive shot", true, 0.0f) {} - - bool Execute(Event event) override { return botAI->CastSpell(60052, GetTarget()); } + CastExplosiveShotRank3Action(PlayerbotAI* botAI) : + CastExplosiveShotBaseAction(botAI) {} + bool Execute(Event event) override + { + return botAI->CastSpell(60052, GetTarget()); + } bool isUseful() override { Unit* target = GetTarget(); if (!target) return false; + return !target->HasAura(60052); } }; // Rank 2 -class CastExplosiveShotRank2Action : public CastDebuffSpellAction +class CastExplosiveShotRank2Action : public CastExplosiveShotBaseAction { public: - CastExplosiveShotRank2Action(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "explosive shot", true, 0.0f) {} - - bool Execute(Event event) override { return botAI->CastSpell(60051, GetTarget()); } + CastExplosiveShotRank2Action(PlayerbotAI* botAI) : + CastExplosiveShotBaseAction(botAI) {} + bool Execute(Event event) override + { + return botAI->CastSpell(60051, GetTarget()); + } bool isUseful() override { Unit* target = GetTarget(); if (!target) return false; + return !target->HasAura(60051); } }; // Rank 1 -class CastExplosiveShotRank1Action : public CastDebuffSpellAction +class CastExplosiveShotRank1Action : public CastExplosiveShotBaseAction { public: - CastExplosiveShotRank1Action(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "explosive shot", true, 0.0f) {} - - bool Execute(Event event) override { return botAI->CastSpell(53301, GetTarget()); } + CastExplosiveShotRank1Action(PlayerbotAI* botAI) : + CastExplosiveShotBaseAction(botAI) {} + bool Execute(Event event) override + { + return botAI->CastSpell(53301, GetTarget()); + } bool isUseful() override { Unit* target = GetTarget(); if (!target) return false; + return !target->HasAura(53301); } }; @@ -415,8 +471,8 @@ class CastExplosiveShotRank1Action : public CastDebuffSpellAction class CastWingClipAction : public CastSpellAction { public: - CastWingClipAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "wing clip") {} - + CastWingClipAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "wing clip") {} bool isUseful() override; std::vector getPrerequisites() override; }; @@ -424,13 +480,15 @@ class CastWingClipAction : public CastSpellAction class CastRaptorStrikeAction : public CastSpellAction { public: - CastRaptorStrikeAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "raptor strike") {} + CastRaptorStrikeAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "raptor strike") {} }; class CastMongooseBiteAction : public CastSpellAction { public: - CastMongooseBiteAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "mongoose bite") {} + CastMongooseBiteAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "mongoose bite") {} }; // AoE Spells @@ -445,7 +503,10 @@ class CastVolleyAction : public CastSpellAction { public: CastVolleyAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "volley") {} - ActionThreatType getThreatType() override { return ActionThreatType::Aoe; } + ActionThreatType getThreatType() override + { + return ActionThreatType::Aoe; + } }; #endif diff --git a/src/Ai/Class/Hunter/HunterAiObjectContext.cpp b/src/Ai/Class/Hunter/HunterAiObjectContext.cpp index def1e469391..2a836ff5e14 100644 --- a/src/Ai/Class/Hunter/HunterAiObjectContext.cpp +++ b/src/Ai/Class/Hunter/HunterAiObjectContext.cpp @@ -22,7 +22,6 @@ class HunterStrategyFactoryInternal : public NamedObjectContext HunterStrategyFactoryInternal() { creators["nc"] = &HunterStrategyFactoryInternal::nc; - creators["boost"] = &HunterStrategyFactoryInternal::boost; creators["pet"] = &HunterStrategyFactoryInternal::pet; creators["cc"] = &HunterStrategyFactoryInternal::cc; creators["trap weave"] = &HunterStrategyFactoryInternal::trap_weave; @@ -34,7 +33,6 @@ class HunterStrategyFactoryInternal : public NamedObjectContext private: static Strategy* nc(PlayerbotAI* botAI) { return new GenericHunterNonCombatStrategy(botAI); } - static Strategy* boost(PlayerbotAI* botAI) { return new HunterBoostStrategy(botAI); } static Strategy* pet(PlayerbotAI* botAI) { return new HunterPetStrategy(botAI); } static Strategy* cc(PlayerbotAI* botAI) { return new HunterCcStrategy(botAI); } static Strategy* trap_weave(PlayerbotAI* botAI) { return new HunterTrapWeaveStrategy(botAI); } @@ -51,14 +49,12 @@ class HunterBuffStrategyFactoryInternal : public NamedObjectContext { creators["bspeed"] = &HunterBuffStrategyFactoryInternal::bspeed; creators["bdps"] = &HunterBuffStrategyFactoryInternal::bdps; - creators["bmana"] = &HunterBuffStrategyFactoryInternal::bmana; creators["rnature"] = &HunterBuffStrategyFactoryInternal::rnature; } private: static Strategy* bspeed(PlayerbotAI* botAI) { return new HunterBuffSpeedStrategy(botAI); } static Strategy* bdps(PlayerbotAI* botAI) { return new HunterBuffDpsStrategy(botAI); } - static Strategy* bmana(PlayerbotAI* botAI) { return new HunterBuffManaStrategy(botAI); } static Strategy* rnature(PlayerbotAI* botAI) { return new HunterNatureResistanceStrategy(botAI); } }; @@ -67,7 +63,6 @@ class HunterTriggerFactoryInternal : public NamedObjectContext public: HunterTriggerFactoryInternal() { - creators["aspect of the viper"] = &HunterTriggerFactoryInternal::aspect_of_the_viper; creators["black arrow"] = &HunterTriggerFactoryInternal::black_arrow; creators["no stings"] = &HunterTriggerFactoryInternal::NoStings; creators["hunters pet dead"] = &HunterTriggerFactoryInternal::hunters_pet_dead; @@ -75,10 +70,9 @@ class HunterTriggerFactoryInternal : public NamedObjectContext creators["hunters pet medium health"] = &HunterTriggerFactoryInternal::hunters_pet_medium_health; creators["hunter's mark"] = &HunterTriggerFactoryInternal::hunters_mark; creators["freezing trap"] = &HunterTriggerFactoryInternal::freezing_trap; - creators["aspect of the pack"] = &HunterTriggerFactoryInternal::aspect_of_the_pack; creators["rapid fire"] = &HunterTriggerFactoryInternal::rapid_fire; - creators["aspect of the hawk"] = &HunterTriggerFactoryInternal::aspect_of_the_hawk; - creators["aspect of the monkey"] = &HunterTriggerFactoryInternal::aspect_of_the_monkey; + creators["aspect of the pack"] = &HunterTriggerFactoryInternal::aspect_of_the_pack; + creators["aspect of the dragonhawk"] = &HunterTriggerFactoryInternal::aspect_of_the_dragonhawk; creators["aspect of the wild"] = &HunterTriggerFactoryInternal::aspect_of_the_wild; creators["aspect of the viper"] = &HunterTriggerFactoryInternal::aspect_of_the_viper; creators["trueshot aura"] = &HunterTriggerFactoryInternal::trueshot_aura; @@ -107,10 +101,8 @@ class HunterTriggerFactoryInternal : public NamedObjectContext private: static Trigger* auto_shot(PlayerbotAI* botAI) { return new AutoShotTrigger(botAI); } static Trigger* scare_beast(PlayerbotAI* botAI) { return new ScareBeastTrigger(botAI); } - static Trigger* concussive_shot_on_snare_target(PlayerbotAI* botAI) - { - return new ConsussiveShotSnareTrigger(botAI); - } + static Trigger* concussive_shot_on_snare_target(PlayerbotAI* botAI) { + return new ConcussiveShotOnSnareTargetTrigger(botAI); } static Trigger* pet_not_happy(PlayerbotAI* botAI) { return new HunterPetNotHappy(botAI); } static Trigger* serpent_sting_on_attacker(PlayerbotAI* botAI) { return new SerpentStingOnAttackerTrigger(botAI); } static Trigger* trueshot_aura(PlayerbotAI* botAI) { return new TrueshotAuraTrigger(botAI); } @@ -125,18 +117,17 @@ class HunterTriggerFactoryInternal : public NamedObjectContext static Trigger* freezing_trap(PlayerbotAI* botAI) { return new FreezingTrapTrigger(botAI); } static Trigger* aspect_of_the_pack(PlayerbotAI* botAI) { return new HunterAspectOfThePackTrigger(botAI); } static Trigger* rapid_fire(PlayerbotAI* botAI) { return new RapidFireTrigger(botAI); } - static Trigger* aspect_of_the_hawk(PlayerbotAI* botAI) { return new HunterAspectOfTheHawkTrigger(botAI); } - static Trigger* aspect_of_the_monkey(PlayerbotAI* botAI) { return new HunterAspectOfTheMonkeyTrigger(botAI); } + static Trigger* aspect_of_the_dragonhawk(PlayerbotAI* botAI) { return new HunterAspectOfTheDragonhawkTrigger(botAI); } static Trigger* aspect_of_the_wild(PlayerbotAI* botAI) { return new HunterAspectOfTheWildTrigger(botAI); } static Trigger* low_ammo(PlayerbotAI* botAI) { return new HunterLowAmmoTrigger(botAI); } static Trigger* no_ammo(PlayerbotAI* botAI) { return new HunterNoAmmoTrigger(botAI); } static Trigger* has_ammo(PlayerbotAI* botAI) { return new HunterHasAmmoTrigger(botAI); } static Trigger* switch_to_melee(PlayerbotAI* botAI) { return new SwitchToMeleeTrigger(botAI); } static Trigger* switch_to_ranged(PlayerbotAI* botAI) { return new SwitchToRangedTrigger(botAI); } - static Trigger* misdirection_on_main_tank(PlayerbotAI* ai) { return new MisdirectionOnMainTankTrigger(ai); } - static Trigger* remove_enrage(PlayerbotAI* ai) { return new TargetRemoveEnrageTrigger(ai); } - static Trigger* remove_magic(PlayerbotAI* ai) { return new TargetRemoveMagicTrigger(ai); } - static Trigger* immolation_trap_no_cd(PlayerbotAI* ai) { return new ImmolationTrapNoCdTrigger(ai); } + static Trigger* misdirection_on_main_tank(PlayerbotAI* botAI) { return new MisdirectionOnMainTankTrigger(botAI); } + static Trigger* remove_enrage(PlayerbotAI* botAI) { return new TargetRemoveEnrageTrigger(botAI); } + static Trigger* remove_magic(PlayerbotAI* botAI) { return new TargetRemoveMagicTrigger(botAI); } + static Trigger* immolation_trap_no_cd(PlayerbotAI* botAI) { return new ImmolationTrapNoCdTrigger(botAI); } static Trigger* kill_command(PlayerbotAI* botAI) { return new KillCommandTrigger(botAI); } static Trigger* explosive_shot(PlayerbotAI* botAI) { return new ExplosiveShotTrigger(botAI); } static Trigger* lock_and_load(PlayerbotAI* botAI) { return new LockAndLoadTrigger(botAI); } @@ -153,7 +144,6 @@ class HunterAiObjectContextInternal : public NamedObjectContext creators["auto shot"] = &HunterAiObjectContextInternal::auto_shot; creators["aimed shot"] = &HunterAiObjectContextInternal::aimed_shot; creators["chimera shot"] = &HunterAiObjectContextInternal::chimera_shot; - creators["explosive shot"] = &HunterAiObjectContextInternal::explosive_shot; creators["arcane shot"] = &HunterAiObjectContextInternal::arcane_shot; creators["concussive shot"] = &HunterAiObjectContextInternal::concussive_shot; creators["distracting shot"] = &HunterAiObjectContextInternal::distracting_shot; @@ -176,6 +166,7 @@ class HunterAiObjectContextInternal : public NamedObjectContext creators["deterrence"] = &HunterAiObjectContextInternal::deterrence; creators["readiness"] = &HunterAiObjectContextInternal::readiness; creators["aspect of the hawk"] = &HunterAiObjectContextInternal::aspect_of_the_hawk; + creators["aspect of the dragonhawk"] = &HunterAiObjectContextInternal::aspect_of_the_dragonhawk; creators["aspect of the monkey"] = &HunterAiObjectContextInternal::aspect_of_the_monkey; creators["aspect of the wild"] = &HunterAiObjectContextInternal::aspect_of_the_wild; creators["aspect of the viper"] = &HunterAiObjectContextInternal::aspect_of_the_viper; @@ -191,7 +182,6 @@ class HunterAiObjectContextInternal : public NamedObjectContext creators["bestial wrath"] = &HunterAiObjectContextInternal::bestial_wrath; creators["scare beast"] = &HunterAiObjectContextInternal::scare_beast; creators["scare beast on cc"] = &HunterAiObjectContextInternal::scare_beast_on_cc; - creators["aspect of the dragonhawk"] = &HunterAiObjectContextInternal::aspect_of_the_dragonhawk; creators["tranquilizing shot"] = &HunterAiObjectContextInternal::tranquilizing_shot; creators["steady shot"] = &HunterAiObjectContextInternal::steady_shot; creators["kill shot"] = &HunterAiObjectContextInternal::kill_shot; @@ -200,6 +190,7 @@ class HunterAiObjectContextInternal : public NamedObjectContext creators["disengage"] = &HunterAiObjectContextInternal::disengage; creators["immolation trap"] = &HunterAiObjectContextInternal::immolation_trap; creators["explosive trap"] = &HunterAiObjectContextInternal::explosive_trap; + creators["explosive shot base"] = &HunterAiObjectContextInternal::explosive_shot_base; creators["explosive shot rank 4"] = &HunterAiObjectContextInternal::explosive_shot_rank_4; creators["explosive shot rank 3"] = &HunterAiObjectContextInternal::explosive_shot_rank_3; creators["explosive shot rank 2"] = &HunterAiObjectContextInternal::explosive_shot_rank_2; @@ -218,7 +209,6 @@ class HunterAiObjectContextInternal : public NamedObjectContext static Action* auto_shot(PlayerbotAI* botAI) { return new CastAutoShotAction(botAI); } static Action* aimed_shot(PlayerbotAI* botAI) { return new CastAimedShotAction(botAI); } static Action* chimera_shot(PlayerbotAI* botAI) { return new CastChimeraShotAction(botAI); } - static Action* explosive_shot(PlayerbotAI* botAI) { return new CastExplosiveShotAction(botAI); } static Action* arcane_shot(PlayerbotAI* botAI) { return new CastArcaneShotAction(botAI); } static Action* concussive_shot(PlayerbotAI* botAI) { return new CastConcussiveShotAction(botAI); } static Action* distracting_shot(PlayerbotAI* botAI) { return new CastDistractingShotAction(botAI); } @@ -234,12 +224,13 @@ class HunterAiObjectContextInternal : public NamedObjectContext static Action* kill_command(PlayerbotAI* botAI) { return new CastKillCommandAction(botAI); } static Action* revive_pet(PlayerbotAI* botAI) { return new CastRevivePetAction(botAI); } static Action* call_pet(PlayerbotAI* botAI) { return new CastCallPetAction(botAI); } - static Action* black_arrow(PlayerbotAI* botAI) { return new CastBlackArrow(botAI); } + static Action* black_arrow(PlayerbotAI* botAI) { return new CastBlackArrowAction(botAI); } static Action* freezing_trap(PlayerbotAI* botAI) { return new CastFreezingTrap(botAI); } static Action* rapid_fire(PlayerbotAI* botAI) { return new CastRapidFireAction(botAI); } static Action* deterrence(PlayerbotAI* botAI) { return new CastDeterrenceAction(botAI); } static Action* readiness(PlayerbotAI* botAI) { return new CastReadinessAction(botAI); } static Action* aspect_of_the_hawk(PlayerbotAI* botAI) { return new CastAspectOfTheHawkAction(botAI); } + static Action* aspect_of_the_dragonhawk(PlayerbotAI* botAI) { return new CastAspectOfTheDragonhawkAction(botAI); } static Action* aspect_of_the_monkey(PlayerbotAI* botAI) { return new CastAspectOfTheMonkeyAction(botAI); } static Action* aspect_of_the_wild(PlayerbotAI* botAI) { return new CastAspectOfTheWildAction(botAI); } static Action* aspect_of_the_viper(PlayerbotAI* botAI) { return new CastAspectOfTheViperAction(botAI); } @@ -248,20 +239,20 @@ class HunterAiObjectContextInternal : public NamedObjectContext static Action* wing_clip(PlayerbotAI* botAI) { return new CastWingClipAction(botAI); } static Action* raptor_strike(PlayerbotAI* botAI) { return new CastRaptorStrikeAction(botAI); } static Action* mongoose_bite(PlayerbotAI* botAI) { return new CastMongooseBiteAction(botAI); } - static Action* aspect_of_the_dragonhawk(PlayerbotAI* ai) { return new CastAspectOfTheDragonhawkAction(ai); } - static Action* tranquilizing_shot(PlayerbotAI* ai) { return new CastTranquilizingShotAction(ai); } - static Action* steady_shot(PlayerbotAI* ai) { return new CastSteadyShotAction(ai); } - static Action* kill_shot(PlayerbotAI* ai) { return new CastKillShotAction(ai); } - static Action* misdirection_on_main_tank(PlayerbotAI* ai) { return new CastMisdirectionOnMainTankAction(ai); } - static Action* silencing_shot(PlayerbotAI* ai) { return new CastSilencingShotAction(ai); } - static Action* disengage(PlayerbotAI* ai) { return new CastDisengageAction(ai); } - static Action* immolation_trap(PlayerbotAI* ai) { return new CastImmolationTrapAction(ai); } - static Action* explosive_trap(PlayerbotAI* ai) { return new CastExplosiveTrapAction(ai); } - static Action* explosive_shot_rank_4(PlayerbotAI* ai) { return new CastExplosiveShotRank4Action(ai); } - static Action* explosive_shot_rank_3(PlayerbotAI* ai) { return new CastExplosiveShotRank3Action(ai); } - static Action* explosive_shot_rank_2(PlayerbotAI* ai) { return new CastExplosiveShotRank2Action(ai); } - static Action* explosive_shot_rank_1(PlayerbotAI* ai) { return new CastExplosiveShotRank1Action(ai); } - static Action* intimidation(PlayerbotAI* ai) { return new CastIntimidationAction(ai); } + static Action* tranquilizing_shot(PlayerbotAI* botAI) { return new CastTranquilizingShotAction(botAI); } + static Action* steady_shot(PlayerbotAI* botAI) { return new CastSteadyShotAction(botAI); } + static Action* kill_shot(PlayerbotAI* botAI) { return new CastKillShotAction(botAI); } + static Action* misdirection_on_main_tank(PlayerbotAI* botAI) { return new CastMisdirectionOnMainTankAction(botAI); } + static Action* silencing_shot(PlayerbotAI* botAI) { return new CastSilencingShotAction(botAI); } + static Action* disengage(PlayerbotAI* botAI) { return new CastDisengageAction(botAI); } + static Action* immolation_trap(PlayerbotAI* botAI) { return new CastImmolationTrapAction(botAI); } + static Action* explosive_trap(PlayerbotAI* botAI) { return new CastExplosiveTrapAction(botAI); } + static Action* explosive_shot_base(PlayerbotAI* botAI) { return new CastExplosiveShotBaseAction(botAI); } + static Action* explosive_shot_rank_4(PlayerbotAI* botAI) { return new CastExplosiveShotRank4Action(botAI); } + static Action* explosive_shot_rank_3(PlayerbotAI* botAI) { return new CastExplosiveShotRank3Action(botAI); } + static Action* explosive_shot_rank_2(PlayerbotAI* botAI) { return new CastExplosiveShotRank2Action(botAI); } + static Action* explosive_shot_rank_1(PlayerbotAI* botAI) { return new CastExplosiveShotRank1Action(botAI); } + static Action* intimidation(PlayerbotAI* botAI) { return new CastIntimidationAction(botAI); } }; SharedNamedObjectContextList HunterAiObjectContext::sharedStrategyContexts; diff --git a/src/Ai/Class/Hunter/Strategy/BeastMasteryHunterStrategy.cpp b/src/Ai/Class/Hunter/Strategy/BeastMasteryHunterStrategy.cpp index a2c302d379b..124da0b135f 100644 --- a/src/Ai/Class/Hunter/Strategy/BeastMasteryHunterStrategy.cpp +++ b/src/Ai/Class/Hunter/Strategy/BeastMasteryHunterStrategy.cpp @@ -6,41 +6,9 @@ #include "BeastMasteryHunterStrategy.h" #include "Playerbots.h" -// ===== Action Node Factory ===== -class BeastMasteryHunterStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - BeastMasteryHunterStrategyActionNodeFactory() - { - creators["auto shot"] = &auto_shot; - creators["kill command"] = &kill_command; - creators["kill shot"] = &kill_shot; - creators["viper sting"] = &viper_sting; - creators["serpent sting"] = serpent_sting; - creators["aimed shot"] = &aimed_shot; - creators["arcane shot"] = &arcane_shot; - creators["steady shot"] = &steady_shot; - creators["multi-shot"] = &multi_shot; - creators["volley"] = &volley; - } - -private: - static ActionNode* auto_shot(PlayerbotAI*) { return new ActionNode("auto shot", {}, {}, {}); } - static ActionNode* kill_command(PlayerbotAI*) { return new ActionNode("kill command", {}, {}, {}); } - static ActionNode* kill_shot(PlayerbotAI*) { return new ActionNode("kill shot", {}, {}, {}); } - static ActionNode* viper_sting(PlayerbotAI*) { return new ActionNode("viper sting", {}, {}, {}); } - static ActionNode* serpent_sting(PlayerbotAI*) { return new ActionNode("serpent sting", {}, {}, {}); } - static ActionNode* aimed_shot(PlayerbotAI*) { return new ActionNode("aimed shot", {}, {}, {}); } - static ActionNode* arcane_shot(PlayerbotAI*) { return new ActionNode("arcane shot", {}, {}, {}); } - static ActionNode* steady_shot(PlayerbotAI*) { return new ActionNode("steady shot", {}, {}, {}); } - static ActionNode* multi_shot(PlayerbotAI*) { return new ActionNode("multi shot", {}, {}, {}); } - static ActionNode* volley(PlayerbotAI*) { return new ActionNode("volley", {}, {}, {}); } -}; - -// ===== Single Target Strategy ===== BeastMasteryHunterStrategy::BeastMasteryHunterStrategy(PlayerbotAI* botAI) : GenericHunterStrategy(botAI) { - actionNodeFactories.Add(new BeastMasteryHunterStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } // ===== Default Actions ===== diff --git a/src/Ai/Class/Hunter/Strategy/GenericHunterNonCombatStrategy.cpp b/src/Ai/Class/Hunter/Strategy/GenericHunterNonCombatStrategy.cpp index 00c35dc2a46..0093a490b3e 100644 --- a/src/Ai/Class/Hunter/Strategy/GenericHunterNonCombatStrategy.cpp +++ b/src/Ai/Class/Hunter/Strategy/GenericHunterNonCombatStrategy.cpp @@ -4,62 +4,31 @@ */ #include "GenericHunterNonCombatStrategy.h" - #include "Playerbots.h" -class GenericHunterNonCombatStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - GenericHunterNonCombatStrategyActionNodeFactory() - { - creators["rapid fire"] = &rapid_fire; - creators["boost"] = &rapid_fire; - creators["aspect of the pack"] = &aspect_of_the_pack; - } - -private: - static ActionNode* rapid_fire([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("rapid fire", - /*P*/ {}, - /*A*/ { NextAction("readiness")}, - /*C*/ {}); - } - - static ActionNode* aspect_of_the_pack([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("aspect of the pack", - /*P*/ {}, - /*A*/ { NextAction("aspect of the cheetah")}, - /*C*/ {}); - } -}; - GenericHunterNonCombatStrategy::GenericHunterNonCombatStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) { - actionNodeFactories.Add(new GenericHunterNonCombatStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } void GenericHunterNonCombatStrategy::InitTriggers(std::vector& triggers) { NonCombatStrategy::InitTriggers(triggers); - triggers.push_back(new TriggerNode("trueshot aura", { NextAction("trueshot aura", 2.0f)})); - triggers.push_back(new TriggerNode("often", { - NextAction("apply stone", 1.0f), - NextAction("apply oil", 1.0f), - })); - triggers.push_back(new TriggerNode("low ammo", { NextAction("say::low ammo", ACTION_NORMAL)})); - triggers.push_back(new TriggerNode("no track", { NextAction("track humanoids", ACTION_NORMAL)})); - triggers.push_back(new TriggerNode("no ammo", { NextAction("equip upgrades packet action", ACTION_HIGH + 1)})); + triggers.push_back(new TriggerNode("trueshot aura", { NextAction("trueshot aura", 2.0f) })); + triggers.push_back(new TriggerNode("often", { NextAction("apply stone", 1.0f), + NextAction("apply oil", 1.0f) })); + triggers.push_back(new TriggerNode("low ammo", { NextAction("say::low ammo", ACTION_NORMAL) })); + triggers.push_back(new TriggerNode("no track", { NextAction("track humanoids", ACTION_NORMAL) })); + triggers.push_back(new TriggerNode("no ammo", { NextAction("equip upgrades packet action", ACTION_HIGH + 1) })); } void HunterPetStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode("no pet", { NextAction("call pet", 60.0f)})); - triggers.push_back(new TriggerNode("has pet", { NextAction("toggle pet spell", 60.0f)})); - triggers.push_back(new TriggerNode("new pet", { NextAction("set pet stance", 60.0f)})); - triggers.push_back(new TriggerNode("pet not happy", { NextAction("feed pet", 60.0f)})); - triggers.push_back(new TriggerNode("hunters pet medium health", { NextAction("mend pet", 60.0f)})); - triggers.push_back(new TriggerNode("hunters pet dead", { NextAction("revive pet", 60.0f)})); + triggers.push_back(new TriggerNode("no pet", { NextAction("call pet", 60.0f) })); + triggers.push_back(new TriggerNode("has pet", { NextAction("toggle pet spell", 60.0f) })); + triggers.push_back(new TriggerNode("new pet", { NextAction("set pet stance", 60.0f) })); + triggers.push_back(new TriggerNode("pet not happy", { NextAction("feed pet", 60.0f) })); + triggers.push_back(new TriggerNode("hunters pet medium health", { NextAction("mend pet", 60.0f) })); + triggers.push_back(new TriggerNode("hunters pet dead", { NextAction("revive pet", 60.0f) })); } diff --git a/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.cpp b/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.cpp index 0a9e88a5405..eb3907dd71b 100644 --- a/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.cpp +++ b/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.cpp @@ -11,11 +11,6 @@ class GenericHunterStrategyActionNodeFactory : public NamedObjectFactory& triggers) triggers.push_back(new TriggerNode("hunter's mark", { NextAction("hunter's mark", 29.5f) })); triggers.push_back(new TriggerNode("rapid fire", { NextAction("rapid fire", 29.0f) })); triggers.push_back(new TriggerNode("aspect of the viper", { NextAction("aspect of the viper", 28.0f) })); - triggers.push_back(new TriggerNode("aspect of the hawk", { NextAction("aspect of the dragonhawk", 27.5f) })); // Aggro/Threat/Defensive Triggers triggers.push_back(new TriggerNode("has aggro", { NextAction("concussive shot", 20.0f) })); @@ -118,14 +76,12 @@ void GenericHunterStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("tranquilizing shot magic", { NextAction("tranquilizing shot", 61.0f) })); // Ranged-based Triggers - triggers.push_back(new TriggerNode("enemy within melee", { - NextAction("explosive trap", 37.0f), - NextAction("mongoose bite", 22.0f), - NextAction("wing clip", 21.0f) })); + triggers.push_back(new TriggerNode("enemy within melee", { NextAction("explosive trap", 37.0f), + NextAction("mongoose bite", 22.0f), + NextAction("wing clip", 21.0f) })); - triggers.push_back(new TriggerNode("enemy too close for auto shot", { - NextAction("disengage", 35.0f), - NextAction("flee", 34.0f) })); + triggers.push_back(new TriggerNode("enemy too close for auto shot", { NextAction("disengage", 35.0f), + NextAction("flee", 34.0f) })); } // ===== AoE Strategy, 2/3+ enemies ===== @@ -138,10 +94,6 @@ void AoEHunterStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("light aoe", { NextAction("multi-shot", 21.0f) })); } -void HunterBoostStrategy::InitTriggers(std::vector& triggers) -{ -} - void HunterCcStrategy::InitTriggers(std::vector& triggers) { triggers.push_back(new TriggerNode("scare beast", { NextAction("scare beast on cc", 23.0f) })); diff --git a/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.h b/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.h index 01aef4cecca..b3a2248c377 100644 --- a/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.h +++ b/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.h @@ -30,15 +30,6 @@ class AoEHunterStrategy : public CombatStrategy std::string const getName() override { return "aoe"; } }; -class HunterBoostStrategy : public Strategy -{ -public: - HunterBoostStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} - - std::string const getName() override { return "boost"; } - void InitTriggers(std::vector& triggers) override; -}; - class HunterCcStrategy : public Strategy { public: diff --git a/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.cpp b/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.cpp index 3601d771198..6333f184c38 100644 --- a/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.cpp +++ b/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.cpp @@ -10,9 +10,21 @@ class BuffHunterStrategyActionNodeFactory : public NamedObjectFactory { public: - BuffHunterStrategyActionNodeFactory() { creators["aspect of the hawk"] = &aspect_of_the_hawk; } + BuffHunterStrategyActionNodeFactory() + { + creators["aspect of the dragonhawk"] = &aspect_of_the_dragonhawk; + creators["aspect of the hawk"] = &aspect_of_the_hawk; + creators["aspect of the pack"] = &aspect_of_the_pack; + } private: + static ActionNode* aspect_of_the_dragonhawk([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode("aspect of the dragonhawk", + /*P*/ {}, + /*A*/ { NextAction("aspect of the hawk") }, + /*C*/ {}); + } static ActionNode* aspect_of_the_hawk([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode("aspect of the hawk", @@ -20,9 +32,16 @@ class BuffHunterStrategyActionNodeFactory : public NamedObjectFactory& triggers) { triggers.push_back( - new TriggerNode("aspect of the hawk", { NextAction("aspect of the dragonhawk", 20.1f), - NextAction("aspect of the hawk", 20.0f) })); + new TriggerNode( + "aspect of the dragonhawk", + { + NextAction("aspect of the dragonhawk", ACTION_HIGH) + } + ) + ); } void HunterNatureResistanceStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode("aspect of the wild", - { NextAction("aspect of the wild", 20.0f) })); + triggers.push_back( + new TriggerNode( + "aspect of the wild", + { + NextAction("aspect of the wild", ACTION_HIGH) + } + ) + ); } void HunterBuffSpeedStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode("aspect of the pack", - { NextAction("aspect of the pack", 20.0f) })); -} - -void HunterBuffManaStrategy::InitTriggers(std::vector& triggers) -{ - triggers.push_back(new TriggerNode("aspect of the viper", - { NextAction("aspect of the viper", 20.0f) })); + triggers.push_back( + new TriggerNode( + "aspect of the pack", + { + NextAction("aspect of the pack", ACTION_HIGH) + } + ) + ); } diff --git a/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.h b/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.h index 7df8bc1bc38..93155caebbe 100644 --- a/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.h +++ b/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.h @@ -6,44 +6,35 @@ #ifndef _PLAYERBOT_HUNTERBUFFSTRATEGIES_H #define _PLAYERBOT_HUNTERBUFFSTRATEGIES_H -#include "NonCombatStrategy.h" +#include "Strategy.h" class PlayerbotAI; -class HunterBuffSpeedStrategy : public NonCombatStrategy +class HunterBuffSpeedStrategy : public Strategy { public: - HunterBuffSpeedStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) {} + HunterBuffSpeedStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} - std::string const getName() override { return "bspeed"; } - void InitTriggers(std::vector& triggers) override; -}; - -class HunterBuffManaStrategy : public NonCombatStrategy -{ -public: - HunterBuffManaStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) {} - - std::string const getName() override { return "bmana"; } void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "bspeed"; } }; -class HunterBuffDpsStrategy : public NonCombatStrategy +class HunterBuffDpsStrategy : public Strategy { public: HunterBuffDpsStrategy(PlayerbotAI* botAI); - std::string const getName() override { return "bdps"; } void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "bdps"; } }; -class HunterNatureResistanceStrategy : public NonCombatStrategy +class HunterNatureResistanceStrategy : public Strategy { public: - HunterNatureResistanceStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) {} + HunterNatureResistanceStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} - std::string const getName() override { return "rnature"; } void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "rnature"; } }; #endif diff --git a/src/Ai/Class/Hunter/Strategy/MarksmanshipHunterStrategy.cpp b/src/Ai/Class/Hunter/Strategy/MarksmanshipHunterStrategy.cpp index e4e8a9a4f44..8289b90bf89 100644 --- a/src/Ai/Class/Hunter/Strategy/MarksmanshipHunterStrategy.cpp +++ b/src/Ai/Class/Hunter/Strategy/MarksmanshipHunterStrategy.cpp @@ -6,45 +6,9 @@ #include "MarksmanshipHunterStrategy.h" #include "Playerbots.h" -// ===== Action Node Factory ===== -class MarksmanshipHunterStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - MarksmanshipHunterStrategyActionNodeFactory() - { - creators["auto shot"] = &auto_shot; - creators["silencing shot"] = &silencing_shot; - creators["kill command"] = &kill_command; - creators["kill shot"] = &kill_shot; - creators["viper sting"] = &viper_sting; - creators["serpent sting"] = serpent_sting; - creators["chimera shot"] = &chimera_shot; - creators["aimed shot"] = &aimed_shot; - creators["arcane shot"] = &arcane_shot; - creators["steady shot"] = &steady_shot; - creators["multi-shot"] = &multi_shot; - creators["volley"] = &volley; - } - -private: - static ActionNode* auto_shot(PlayerbotAI*) { return new ActionNode("auto shot", {}, {}, {}); } - static ActionNode* silencing_shot(PlayerbotAI*) { return new ActionNode("silencing shot", {}, {}, {}); } - static ActionNode* kill_command(PlayerbotAI*) { return new ActionNode("kill command", {}, {}, {}); } - static ActionNode* kill_shot(PlayerbotAI*) { return new ActionNode("kill shot", {}, {}, {}); } - static ActionNode* viper_sting(PlayerbotAI*) { return new ActionNode("viper sting", {}, {}, {}); } - static ActionNode* serpent_sting(PlayerbotAI*) { return new ActionNode("serpent sting", {}, {}, {}); } - static ActionNode* chimera_shot(PlayerbotAI*) { return new ActionNode("chimera shot", {}, {}, {}); } - static ActionNode* aimed_shot(PlayerbotAI*) { return new ActionNode("aimed shot", {}, {}, {}); } - static ActionNode* arcane_shot(PlayerbotAI*) { return new ActionNode("arcane shot", {}, {}, {}); } - static ActionNode* steady_shot(PlayerbotAI*) { return new ActionNode("steady shot", {}, {}, {}); } - static ActionNode* multi_shot(PlayerbotAI*) { return new ActionNode("multi shot", {}, {}, {}); } - static ActionNode* volley(PlayerbotAI*) { return new ActionNode("volley", {}, {}, {}); } -}; - -// ===== Single Target Strategy ===== MarksmanshipHunterStrategy::MarksmanshipHunterStrategy(PlayerbotAI* botAI) : GenericHunterStrategy(botAI) { - actionNodeFactories.Add(new MarksmanshipHunterStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } // ===== Default Actions ===== diff --git a/src/Ai/Class/Hunter/Strategy/SurvivalHunterStrategy.cpp b/src/Ai/Class/Hunter/Strategy/SurvivalHunterStrategy.cpp index 796891a9b0b..cc8f2a64c1b 100644 --- a/src/Ai/Class/Hunter/Strategy/SurvivalHunterStrategy.cpp +++ b/src/Ai/Class/Hunter/Strategy/SurvivalHunterStrategy.cpp @@ -12,36 +12,35 @@ class SurvivalHunterStrategyActionNodeFactory : public NamedObjectFactory& triggers) } ) ); - triggers.push_back( - new TriggerNode( - "lock and load", - { - NextAction("explosive shot rank 3", 27.5f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "lock and load", - { - NextAction("explosive shot rank 2", 27.0f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "lock and load", - { - NextAction("explosive shot rank 1", 26.5f) - } - ) - ); triggers.push_back( new TriggerNode( "kill command", diff --git a/src/Ai/Class/Hunter/Trigger/HunterTriggers.cpp b/src/Ai/Class/Hunter/Trigger/HunterTriggers.cpp index 972332d1a0a..733960bca3e 100644 --- a/src/Ai/Class/Hunter/Trigger/HunterTriggers.cpp +++ b/src/Ai/Class/Hunter/Trigger/HunterTriggers.cpp @@ -16,8 +16,7 @@ bool KillCommandTrigger::IsActive() { - Unit* target = GetTarget(); - return !botAI->HasAura("kill command", target); + return !botAI->HasAura("kill command", GetTarget()); } bool BlackArrowTrigger::IsActive() @@ -26,36 +25,46 @@ bool BlackArrowTrigger::IsActive() return false; return DebuffTrigger::IsActive(); - return BuffTrigger::IsActive(); } -bool HunterAspectOfTheHawkTrigger::IsActive() +bool HunterAspectOfTheDragonhawkTrigger::IsActive() { Unit* target = GetTarget(); - return SpellTrigger::IsActive() && !botAI->HasAura("aspect of the hawk", target) && - !botAI->HasAura("aspect of the dragonhawk", target) && - (!AI_VALUE2(bool, "has mana", "self target") || AI_VALUE2(uint8, "mana", "self target") > 70); + if (!target) + return false; + + if (!SpellTrigger::IsActive()) + return false; + + if (botAI->HasAura("aspect of the hawk", target) || + botAI->HasAura("aspect of the dragonhawk", target)) + return false; + + if (botAI->HasAura("aspect of the viper", target)) + return AI_VALUE2(uint8, "mana", "self target") >= 60; + + return true; } bool HunterNoStingsActiveTrigger::IsActive() { Unit* target = AI_VALUE(Unit*, "current target"); - return DebuffTrigger::IsActive() && target && !botAI->HasAura("serpent sting", target, false, true) && - !botAI->HasAura("scorpid sting", target, false, true) && !botAI->HasAura("viper sting", target, false, true); - return BuffTrigger::IsActive(); + return DebuffTrigger::IsActive() && target && + !botAI->HasAura("serpent sting", target, false, true) && + !botAI->HasAura("scorpid sting", target, false, true) && + !botAI->HasAura("viper sting", target, false, true); } bool HuntersPetDeadTrigger::IsActive() { - // Unit* pet = AI_VALUE(Unit*, "pet target"); - // return pet && AI_VALUE2(bool, "dead", "pet target") && !AI_VALUE2(bool, "mounted", "self target"); return AI_VALUE(bool, "pet dead") && !AI_VALUE2(bool, "mounted", "self target"); } bool HuntersPetLowHealthTrigger::IsActive() { Unit* pet = AI_VALUE(Unit*, "pet target"); - return pet && AI_VALUE2(uint8, "health", "pet target") < 40 && !AI_VALUE2(bool, "dead", "pet target") && + return pet && AI_VALUE2(uint8, "health", "pet target") < 40 && + !AI_VALUE2(bool, "dead", "pet target") && !AI_VALUE2(bool, "mounted", "self target"); } @@ -73,9 +82,14 @@ bool HunterPetNotHappy::IsActive() bool HunterAspectOfTheViperTrigger::IsActive() { - return SpellTrigger::IsActive() && !botAI->HasAura(spell, GetTarget()) && + if (botAI->HasStrategy("rnature", BotState::BOT_STATE_COMBAT) || + botAI->HasStrategy("rnature", BotState::BOT_STATE_NON_COMBAT) || + botAI->HasStrategy("bspeed", BotState::BOT_STATE_COMBAT) || + botAI->HasStrategy("bspeed", BotState::BOT_STATE_NON_COMBAT)) + return false; + + return BuffTrigger::IsActive() && AI_VALUE2(uint8, "mana", "self target") < (sPlayerbotAIConfig.lowMana / 2); - ; } bool HunterAspectOfThePackTrigger::IsActive() @@ -85,11 +99,14 @@ bool HunterAspectOfThePackTrigger::IsActive() bool HunterLowAmmoTrigger::IsActive() { - return bot->GetGroup() && (AI_VALUE2(uint32, "item count", "ammo") < 100) && - (AI_VALUE2(uint32, "item count", "ammo") > 0); + uint32 ammoCount = AI_VALUE2(uint32, "item count", "ammo"); + return bot->GetGroup() && ammoCount > 0 && ammoCount < 100; } -bool HunterHasAmmoTrigger::IsActive() { return !AmmoCountTrigger::IsActive(); } +bool HunterHasAmmoTrigger::IsActive() +{ + return !AmmoCountTrigger::IsActive(); +} bool SwitchToRangedTrigger::IsActive() { @@ -131,6 +148,7 @@ bool NoTrackTrigger::IsActive() if (botAI->HasAura(track, bot)) return false; } + return true; } @@ -138,17 +156,17 @@ bool SerpentStingOnAttackerTrigger::IsActive() { if (!DebuffOnAttackerTrigger::IsActive()) return false; + Unit* target = GetTarget(); if (!target) - { return false; - } + return !botAI->HasAura("scorpid sting", target, false, true) && !botAI->HasAura("viper sting", target, false, true); - return BuffTrigger::IsActive(); } -const std::set VolleyChannelCheckTrigger::VOLLEY_SPELL_IDS = { +const std::set VolleyChannelCheckTrigger::VOLLEY_SPELL_IDS = +{ 1510, // Volley Rank 1 14294, // Volley Rank 2 14295, // Volley Rank 3 @@ -159,19 +177,12 @@ const std::set VolleyChannelCheckTrigger::VOLLEY_SPELL_IDS = { bool VolleyChannelCheckTrigger::IsActive() { - Player* bot = botAI->GetBot(); - - // Check if the bot is channeling a spell - if (Spell* spell = bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL)) + if (Spell* spell = bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL); + spell && VOLLEY_SPELL_IDS.count(spell->m_spellInfo->Id)) { - // Only trigger if the spell being channeled is Volley - if (VOLLEY_SPELL_IDS.count(spell->m_spellInfo->Id)) - { - uint8 attackerCount = AI_VALUE(uint8, "attacker count"); - return attackerCount < minEnemies; - } + uint8 attackerCount = AI_VALUE(uint8, "attacker count"); + return attackerCount < minEnemies; } - // Not channeling Volley return false; } diff --git a/src/Ai/Class/Hunter/Trigger/HunterTriggers.h b/src/Ai/Class/Hunter/Trigger/HunterTriggers.h index 6bc5f3c44aa..7459f94bbc1 100644 --- a/src/Ai/Class/Hunter/Trigger/HunterTriggers.h +++ b/src/Ai/Class/Hunter/Trigger/HunterTriggers.h @@ -16,16 +16,10 @@ class PlayerbotAI; // Buff and Out of Combat Triggers -class HunterAspectOfTheMonkeyTrigger : public BuffTrigger +class HunterAspectOfTheDragonhawkTrigger : public BuffTrigger { public: - HunterAspectOfTheMonkeyTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "aspect of the monkey") {} -}; - -class HunterAspectOfTheHawkTrigger : public BuffTrigger -{ -public: - HunterAspectOfTheHawkTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "aspect of the hawk") {} + HunterAspectOfTheDragonhawkTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "aspect of the dragonhawk") {} bool IsActive() override; }; @@ -130,10 +124,10 @@ class FreezingTrapTrigger : public HasCcTargetTrigger FreezingTrapTrigger(PlayerbotAI* botAI) : HasCcTargetTrigger(botAI, "freezing trap") {} }; -class ConsussiveShotSnareTrigger : public SnareTargetTrigger +class ConcussiveShotOnSnareTargetTrigger : public SnareTargetTrigger { public: - ConsussiveShotSnareTrigger(PlayerbotAI* botAI) : SnareTargetTrigger(botAI, "concussive shot") {} + ConcussiveShotOnSnareTargetTrigger(PlayerbotAI* botAI) : SnareTargetTrigger(botAI, "concussive shot") {} }; class ScareBeastTrigger : public HasCcTargetTrigger @@ -212,25 +206,25 @@ class SwitchToMeleeTrigger : public Trigger class MisdirectionOnMainTankTrigger : public BuffOnMainTankTrigger { public: - MisdirectionOnMainTankTrigger(PlayerbotAI* ai) : BuffOnMainTankTrigger(ai, "misdirection", true) {} + MisdirectionOnMainTankTrigger(PlayerbotAI* botAI) : BuffOnMainTankTrigger(botAI, "misdirection", true) {} }; class TargetRemoveEnrageTrigger : public TargetAuraDispelTrigger { public: - TargetRemoveEnrageTrigger(PlayerbotAI* ai) : TargetAuraDispelTrigger(ai, "tranquilizing shot", DISPEL_ENRAGE) {} + TargetRemoveEnrageTrigger(PlayerbotAI* botAI) : TargetAuraDispelTrigger(botAI, "tranquilizing shot", DISPEL_ENRAGE) {} }; class TargetRemoveMagicTrigger : public TargetAuraDispelTrigger { public: - TargetRemoveMagicTrigger(PlayerbotAI* ai) : TargetAuraDispelTrigger(ai, "tranquilizing shot", DISPEL_MAGIC) {} + TargetRemoveMagicTrigger(PlayerbotAI* botAI) : TargetAuraDispelTrigger(botAI, "tranquilizing shot", DISPEL_MAGIC) {} }; class ImmolationTrapNoCdTrigger : public SpellNoCooldownTrigger { public: - ImmolationTrapNoCdTrigger(PlayerbotAI* ai) : SpellNoCooldownTrigger(ai, "immolation trap") {} + ImmolationTrapNoCdTrigger(PlayerbotAI* botAI) : SpellNoCooldownTrigger(botAI, "immolation trap") {} }; BEGIN_TRIGGER(HuntersPetDeadTrigger, Trigger) diff --git a/src/Ai/Raid/Icecrown/Multiplier/RaidIccMultipliers.cpp b/src/Ai/Raid/Icecrown/Multiplier/RaidIccMultipliers.cpp index c7f25e33174..710c15e0a03 100644 --- a/src/Ai/Raid/Icecrown/Multiplier/RaidIccMultipliers.cpp +++ b/src/Ai/Raid/Icecrown/Multiplier/RaidIccMultipliers.cpp @@ -647,7 +647,7 @@ float IccSindragosaMultiplier::GetValue(Action* action) dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action)) + dynamic_cast(action)) return 0.0f; } @@ -774,7 +774,7 @@ float IccLichKingAddsMultiplier::GetValue(Action* action) dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action)) + dynamic_cast(action) || dynamic_cast(action)) return 0.0f; } diff --git a/src/Bot/Factory/AiFactory.cpp b/src/Bot/Factory/AiFactory.cpp index 84b3a7dc53a..6121789a940 100644 --- a/src/Bot/Factory/AiFactory.cpp +++ b/src/Bot/Factory/AiFactory.cpp @@ -363,7 +363,7 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa else engine->addStrategiesNoInit("surv", nullptr); - engine->addStrategiesNoInit("cc", "dps assist", "aoe", nullptr); + engine->addStrategiesNoInit("cc", "dps assist", "aoe", "bdps", nullptr); break; case CLASS_ROGUE: if (tab == ROGUE_TAB_ASSASSINATION || tab == ROGUE_TAB_SUBTLETY) diff --git a/src/Bot/Factory/PlayerbotFactory.cpp b/src/Bot/Factory/PlayerbotFactory.cpp index 506ad9154fd..b66623c5825 100644 --- a/src/Bot/Factory/PlayerbotFactory.cpp +++ b/src/Bot/Factory/PlayerbotFactory.cpp @@ -3307,7 +3307,7 @@ void PlayerbotFactory::InitReagents() break; case CLASS_PALADIN: if (level >= 52) - items.push_back({21177, 80}); // Symbol of Kings + items.push_back({21177, 100}); // Symbol of Kings break; case CLASS_PRIEST: if (level >= 48 && level < 56) From 496d6c9e4c21a5bb8aa8890ea8d02ffcf0d01cda Mon Sep 17 00:00:00 2001 From: dillyns <49765217+dillyns@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:24:55 -0400 Subject: [PATCH 02/18] Blood dk rotation update (#2227) ## Pull Request Description Improve the blood DK rotation, add hysteria use, improve blood pact use. Basic Blood priority should be: 1. Keep diseases up 2. Use rune strike whenever possible 3. Use frost and death runes on Icy Touch (highest threat ability) 4. Use unholy runes on death strike to trigger more death runes 5. use blood runes on heart/blood strike Hysteria should be used on a physical dps, or a tank if no physical dps is available. Never a caster or healer. Summon ghoul should be saved for death pact usage. They are honestly a liability that aggros everything in range without the unholy talent that turns them into a pet anyways. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. Removed plague strike from the general rotation. It should only be used for disease application. Added death strike on "high unholy rune" trigger instead. Remove raise dead from generic dk. Only cast it right before death pact. Add hysteria to bloods default actions. - Describe the **processing cost** when this logic executes across many bots. Minimal change ## How to Test the Changes To test rotation changes: Use a combat parsing addon such as Details! or watch the combat log of a Blood DK bot. They should now use death strike instead of using unholy runes on plague strike. To test hysteria: Ungrouped blood dk bots can be observed using it on themselves. Create a group with the blood dk + casters/healers only. Observe that they never use hysteria on the casters. Create a group with blood dk + physical dps. Observe that they use hysteria on a dps and not themselves. To test blood pact: I used GM command .damage to get my Blood DK bot into "critical health" trigger. They will use raise dead then blood pact. ## 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? - - [x] No - - [ ] Yes (**explain why**) - 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**) Used for brainstorming and exploring the codebase to find similar patterns. ## 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 --- src/Ai/Class/Dk/Action/DKActions.cpp | 52 +++++++++++++++++++ src/Ai/Class/Dk/Action/DKActions.h | 7 +++ src/Ai/Class/Dk/DKAiObjectContext.cpp | 6 ++- src/Ai/Class/Dk/Strategy/BloodDKStrategy.cpp | 32 +++++++++--- src/Ai/Class/Dk/Strategy/FrostDKStrategy.cpp | 1 - .../Strategy/GenericDKNonCombatStrategy.cpp | 6 --- .../Class/Dk/Strategy/GenericDKStrategy.cpp | 16 +++--- src/Ai/Class/Dk/Strategy/UnholyDKStrategy.cpp | 14 ++--- src/Ai/Class/Dk/Trigger/DKTriggers.h | 6 +++ 9 files changed, 109 insertions(+), 31 deletions(-) diff --git a/src/Ai/Class/Dk/Action/DKActions.cpp b/src/Ai/Class/Dk/Action/DKActions.cpp index 7788e481907..5cd475e81a6 100644 --- a/src/Ai/Class/Dk/Action/DKActions.cpp +++ b/src/Ai/Class/Dk/Action/DKActions.cpp @@ -48,3 +48,55 @@ bool CastRaiseDeadAction::Execute(Event event) return true; } + +Unit* CastHysteriaAction::GetTarget() +{ + Group* group = bot->GetGroup(); + if (!group) + { + if (!bot->HasAura(49016)) + return bot; + return nullptr; + } + + Unit* rangedDps = nullptr; + Unit* tank = nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (member->GetMap() != bot->GetMap() || bot->GetDistance(member) > sPlayerbotAIConfig.spellDistance) + continue; + + // Skip if already has hysteria + if (member->HasAura(49016)) + continue; + + // Priority 1: Melee DPS + if (botAI->IsMelee(member) && botAI->IsDps(member)) + return member; + + // Priority 2: Ranged DPS (physical, not casters) + if (!rangedDps && botAI->IsRanged(member) && botAI->IsDps(member) && !botAI->IsCaster(member)) + rangedDps = member; + + // Priority 3: Tank + if (!tank && botAI->IsTank(member)) + tank = member; + } + + if (rangedDps) + return rangedDps; + + if (tank) + return tank; + + // Fallback to self if no hysteria + if (!bot->HasAura(49016)) + return bot; + + return nullptr; +} diff --git a/src/Ai/Class/Dk/Action/DKActions.h b/src/Ai/Class/Dk/Action/DKActions.h index 74e066cd514..5e8bf968fe1 100644 --- a/src/Ai/Class/Dk/Action/DKActions.h +++ b/src/Ai/Class/Dk/Action/DKActions.h @@ -340,4 +340,11 @@ class CastBloodTapAction : public CastMeleeSpellAction CastBloodTapAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "blood tap") {} }; +class CastHysteriaAction : public CastSpellAction +{ +public: + CastHysteriaAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "hysteria") {} + Unit* GetTarget() override; +}; + #endif diff --git a/src/Ai/Class/Dk/DKAiObjectContext.cpp b/src/Ai/Class/Dk/DKAiObjectContext.cpp index ac80c7dc46d..9a8271aa704 100644 --- a/src/Ai/Class/Dk/DKAiObjectContext.cpp +++ b/src/Ai/Class/Dk/DKAiObjectContext.cpp @@ -100,6 +100,7 @@ class DeathKnightTriggerFactoryInternal : public NamedObjectContext creators["dd cd and no desolation"] = &DeathKnightTriggerFactoryInternal::dd_cd_and_no_desolation; creators["death and decay cooldown"] = &DeathKnightTriggerFactoryInternal::death_and_decay_cooldown; creators["army of the dead"] = &DeathKnightTriggerFactoryInternal::army_of_the_dead; + creators["hysteria no cd"] = &DeathKnightTriggerFactoryInternal::hysteria_no_cd; } private: @@ -152,6 +153,7 @@ class DeathKnightTriggerFactoryInternal : public NamedObjectContext } static Trigger* death_and_decay_cooldown(PlayerbotAI* botAI) { return new DeathAndDecayCooldownTrigger(botAI); } static Trigger* army_of_the_dead(PlayerbotAI* botAI) { return new ArmyOfTheDeadTrigger(botAI); } + static Trigger* hysteria_no_cd(PlayerbotAI* botAI) { return new HysteriaNoCooldownTrigger(botAI); } }; class DeathKnightAiObjectContextInternal : public NamedObjectContext @@ -209,7 +211,7 @@ class DeathKnightAiObjectContextInternal : public NamedObjectContext creators["vampiric blood"] = &DeathKnightAiObjectContextInternal::vampiric_blood; creators["death pact"] = &DeathKnightAiObjectContextInternal::death_pact; creators["death rune_mastery"] = &DeathKnightAiObjectContextInternal::death_rune_mastery; - // creators["hysteria"] = &DeathKnightAiObjectContextInternal::hysteria; + creators["hysteria"] = &DeathKnightAiObjectContextInternal::hysteria; creators["dancing rune weapon"] = &DeathKnightAiObjectContextInternal::dancing_rune_weapon; creators["dark command"] = &DeathKnightAiObjectContextInternal::dark_command; } @@ -265,7 +267,7 @@ class DeathKnightAiObjectContextInternal : public NamedObjectContext static Action* vampiric_blood(PlayerbotAI* botAI) { return new CastVampiricBloodAction(botAI); } static Action* death_pact(PlayerbotAI* botAI) { return new CastDeathPactAction(botAI); } static Action* death_rune_mastery(PlayerbotAI* botAI) { return new CastDeathRuneMasteryAction(botAI); } - // static Action* hysteria(PlayerbotAI* botAI) { return new CastHysteriaAction(botAI); } + static Action* hysteria(PlayerbotAI* botAI) { return new CastHysteriaAction(botAI); } static Action* dancing_rune_weapon(PlayerbotAI* botAI) { return new CastDancingRuneWeaponAction(botAI); } static Action* dark_command(PlayerbotAI* botAI) { return new CastDarkCommandAction(botAI); } static Action* mind_freeze_on_enemy_healer(PlayerbotAI* botAI) diff --git a/src/Ai/Class/Dk/Strategy/BloodDKStrategy.cpp b/src/Ai/Class/Dk/Strategy/BloodDKStrategy.cpp index 344b3dc0957..9ced88ac7f3 100644 --- a/src/Ai/Class/Dk/Strategy/BloodDKStrategy.cpp +++ b/src/Ai/Class/Dk/Strategy/BloodDKStrategy.cpp @@ -50,7 +50,9 @@ class BloodDKStrategyActionNodeFactory : public NamedObjectFactory { NextAction("frost presence") }, - /*A*/ {}, + /*A*/ { + NextAction("blood strike") + }, /*C*/ {} ); } @@ -89,13 +91,11 @@ BloodDKStrategy::BloodDKStrategy(PlayerbotAI* botAI) : GenericDKStrategy(botAI) std::vector BloodDKStrategy::getDefaultActions() { return { - NextAction("rune strike", ACTION_DEFAULT + 0.8f), - NextAction("icy touch", ACTION_DEFAULT + 0.7f), - NextAction("heart strike", ACTION_DEFAULT + 0.6f), - NextAction("blood strike", ACTION_DEFAULT + 0.5f), - NextAction("dancing rune weapon", ACTION_DEFAULT + 0.4f), - NextAction("death coil", ACTION_DEFAULT + 0.3f), - NextAction("plague strike", ACTION_DEFAULT + 0.2f), + NextAction("rune strike", ACTION_DEFAULT + 0.6f), + NextAction("icy touch", ACTION_DEFAULT + 0.5f), + NextAction("heart strike", ACTION_DEFAULT + 0.4f), + NextAction("dancing rune weapon", ACTION_DEFAULT + 0.3f), + NextAction("death coil", ACTION_DEFAULT + 0.2f), NextAction("horn of winter", ACTION_DEFAULT + 0.1f), NextAction("melee", ACTION_DEFAULT) }; @@ -105,6 +105,14 @@ void BloodDKStrategy::InitTriggers(std::vector& triggers) { GenericDKStrategy::InitTriggers(triggers); + triggers.push_back( + new TriggerNode( + "hysteria no cd", + { + NextAction("hysteria", ACTION_NORMAL + 4) + } + ) + ); triggers.push_back( new TriggerNode( "rune strike", @@ -162,4 +170,12 @@ void BloodDKStrategy::InitTriggers(std::vector& triggers) } ) ); + triggers.push_back( + new TriggerNode( + "high unholy rune", + { + NextAction("death strike", ACTION_HIGH + 1) + } + ) + ); } diff --git a/src/Ai/Class/Dk/Strategy/FrostDKStrategy.cpp b/src/Ai/Class/Dk/Strategy/FrostDKStrategy.cpp index d0b0ee2037f..48a61a5ffcf 100644 --- a/src/Ai/Class/Dk/Strategy/FrostDKStrategy.cpp +++ b/src/Ai/Class/Dk/Strategy/FrostDKStrategy.cpp @@ -91,7 +91,6 @@ std::vector FrostDKStrategy::getDefaultActions() return { NextAction("obliterate", ACTION_DEFAULT + 0.7f), NextAction("frost strike", ACTION_DEFAULT + 0.4f), - NextAction("empower rune weapon", ACTION_DEFAULT + 0.3f), NextAction("horn of winter", ACTION_DEFAULT + 0.1f), NextAction("melee", ACTION_DEFAULT) }; diff --git a/src/Ai/Class/Dk/Strategy/GenericDKNonCombatStrategy.cpp b/src/Ai/Class/Dk/Strategy/GenericDKNonCombatStrategy.cpp index d358d437054..0d3a43b7953 100644 --- a/src/Ai/Class/Dk/Strategy/GenericDKNonCombatStrategy.cpp +++ b/src/Ai/Class/Dk/Strategy/GenericDKNonCombatStrategy.cpp @@ -41,16 +41,10 @@ void GenericDKNonCombatStrategy::InitTriggers(std::vector& trigger { NonCombatStrategy::InitTriggers(triggers); - triggers.push_back( - new TriggerNode("no pet", { NextAction("raise dead", ACTION_NORMAL + 1) })); triggers.push_back( new TriggerNode("horn of winter", { NextAction("horn of winter", 21.0f) })); triggers.push_back( new TriggerNode("bone shield", { NextAction("bone shield", 21.0f) })); - triggers.push_back( - new TriggerNode("has pet", { NextAction("toggle pet spell", 60.0f) })); - triggers.push_back( - new TriggerNode("new pet", { NextAction("set pet stance", 60.0f) })); } void DKBuffDpsStrategy::InitTriggers(std::vector& triggers) diff --git a/src/Ai/Class/Dk/Strategy/GenericDKStrategy.cpp b/src/Ai/Class/Dk/Strategy/GenericDKStrategy.cpp index 61ff8c74897..10934161245 100644 --- a/src/Ai/Class/Dk/Strategy/GenericDKStrategy.cpp +++ b/src/Ai/Class/Dk/Strategy/GenericDKStrategy.cpp @@ -165,12 +165,6 @@ void GenericDKStrategy::InitTriggers(std::vector& triggers) { MeleeCombatStrategy::InitTriggers(triggers); - triggers.push_back( - new TriggerNode("no pet", { NextAction("raise dead", ACTION_NORMAL + 5) })); - triggers.push_back( - new TriggerNode("has pet", { NextAction("toggle pet spell", 60.0f) })); - triggers.push_back( - new TriggerNode("new pet", { NextAction("set pet stance", 60.0f) })); triggers.push_back( new TriggerNode("mind freeze", { NextAction("mind freeze", ACTION_HIGH + 1) })); triggers.push_back( @@ -179,7 +173,8 @@ void GenericDKStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode( "horn of winter", { NextAction("horn of winter", ACTION_NORMAL + 1) })); triggers.push_back(new TriggerNode("critical health", - { NextAction("death pact", ACTION_HIGH + 5) })); + { NextAction("raise dead", ACTION_HIGH + 6), + NextAction("death pact", ACTION_HIGH + 5) })); triggers.push_back( new TriggerNode("low health", { NextAction("icebound fortitude", ACTION_HIGH + 5), @@ -190,4 +185,11 @@ void GenericDKStrategy::InitTriggers(std::vector& triggers) NextAction("blood boil", ACTION_NORMAL + 3) })); triggers.push_back( new TriggerNode("pestilence glyph", { NextAction("pestilence", ACTION_HIGH + 9) })); + triggers.push_back( + new TriggerNode("no rune", + { + NextAction("empower rune weapon", ACTION_HIGH + 1) + } + ) + ); } diff --git a/src/Ai/Class/Dk/Strategy/UnholyDKStrategy.cpp b/src/Ai/Class/Dk/Strategy/UnholyDKStrategy.cpp index d94a94ec313..40a0ac0417d 100644 --- a/src/Ai/Class/Dk/Strategy/UnholyDKStrategy.cpp +++ b/src/Ai/Class/Dk/Strategy/UnholyDKStrategy.cpp @@ -87,6 +87,13 @@ void UnholyDKStrategy::InitTriggers(std::vector& triggers) { GenericDKStrategy::InitTriggers(triggers); + triggers.push_back( + new TriggerNode("no pet", { NextAction("raise dead", ACTION_NORMAL + 5) })); + triggers.push_back( + new TriggerNode("has pet", { NextAction("toggle pet spell", 60.0f) })); + triggers.push_back( + new TriggerNode("new pet", { NextAction("set pet stance", 60.0f) })); + triggers.push_back( new TriggerNode( "death and decay cooldown", @@ -146,13 +153,6 @@ void UnholyDKStrategy::InitTriggers(std::vector& triggers) } ) ); - triggers.push_back( - new TriggerNode("no rune", - { - NextAction("empower rune weapon", ACTION_HIGH + 1) - } - ) - ); triggers.push_back( new TriggerNode( "army of the dead", diff --git a/src/Ai/Class/Dk/Trigger/DKTriggers.h b/src/Ai/Class/Dk/Trigger/DKTriggers.h index c46fbfe37fa..98070d85f5e 100644 --- a/src/Ai/Class/Dk/Trigger/DKTriggers.h +++ b/src/Ai/Class/Dk/Trigger/DKTriggers.h @@ -198,4 +198,10 @@ class ArmyOfTheDeadTrigger : public BoostTrigger ArmyOfTheDeadTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "army of the dead") {} }; +class HysteriaNoCooldownTrigger : public SpellNoCooldownTrigger +{ +public: + HysteriaNoCooldownTrigger(PlayerbotAI* botAI) : SpellNoCooldownTrigger(botAI, "hysteria") {} +}; + #endif From 76dd91c4fafdb6a690cf2db7da0036299c0a9d09 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Fri, 3 Apr 2026 22:25:29 +0200 Subject: [PATCH 03/18] Hand of Freedom support (#2233) ## Pull Request Description Added Hand of Freedom action for paladin. Related with: #2002 ## How to Test the Changes - invite paladin bot to party - start fight (can use dummy target) - apply some snare effect to bot or yourself (for example `.aura 1715`) - bot should use hand of freedom ## 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**) Yes, paladin bots start using Hand of Freedom - 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**) OpenCode, as helper to create and review code ## 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 --- .../Value/PartyMemberSnaredTargetValue.cpp | 64 +++++++++++++++++++ .../Base/Value/PartyMemberSnaredTargetValue.h | 24 +++++++ src/Ai/Base/ValueContext.h | 3 + .../Class/Paladin/Action/PaladinActions.cpp | 34 ++++++++++ src/Ai/Class/Paladin/Action/PaladinActions.h | 13 ++++ .../Class/Paladin/PaladinAiObjectContext.cpp | 4 ++ .../Strategy/GenericPaladinStrategy.cpp | 3 + .../Class/Paladin/Trigger/PaladinTriggers.cpp | 35 ++++++++++ .../Class/Paladin/Trigger/PaladinTriggers.h | 10 +++ src/Ai/Class/Paladin/Util/PaladinHelper.h | 43 +++++++++++++ src/Bot/PlayerbotAI.cpp | 5 ++ src/Bot/PlayerbotAI.h | 1 + 12 files changed, 239 insertions(+) create mode 100644 src/Ai/Base/Value/PartyMemberSnaredTargetValue.cpp create mode 100644 src/Ai/Base/Value/PartyMemberSnaredTargetValue.h create mode 100644 src/Ai/Class/Paladin/Util/PaladinHelper.h diff --git a/src/Ai/Base/Value/PartyMemberSnaredTargetValue.cpp b/src/Ai/Base/Value/PartyMemberSnaredTargetValue.cpp new file mode 100644 index 00000000000..02f726b0fa1 --- /dev/null +++ b/src/Ai/Base/Value/PartyMemberSnaredTargetValue.cpp @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "PartyMemberSnaredTargetValue.h" + +#include + +#include "PlayerbotAIAware.h" +#include "Playerbots.h" + +class PartyMemberSnaredTargetPredicate : public FindPlayerPredicate, public PlayerbotAIAware +{ +public: + PartyMemberSnaredTargetPredicate(PlayerbotAI* botAI) + : PlayerbotAIAware(botAI) + { + } + + bool Check(Unit* unit) override + { + if (!unit || !unit->IsAlive() || !unit->IsInWorld() || unit == botAI->GetBot()) + return false; + + if (unit->GetMapId() != botAI->GetBot()->GetMapId()) + return false; + + if (!botAI->GetBot()->IsWithinLOSInMap(unit)) + return false; + + return botAI->IsMovementImpaired(unit); + } +}; + +Unit* PartyMemberSnaredTargetValue::Calculate() +{ + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + PartyMemberSnaredTargetPredicate predicate(botAI); + Player* bestTarget = nullptr; + float closestDistanceSq = std::numeric_limits::max(); + + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) + { + Player* member = gref->GetSource(); + if (!member) + continue; + + if (!predicate.Check(member)) + continue; + + float const distanceSq = bot->GetExactDist2dSq(member->GetPositionX(), member->GetPositionY()); + if (distanceSq < closestDistanceSq) + { + closestDistanceSq = distanceSq; + bestTarget = member; + } + } + + return bestTarget; +} diff --git a/src/Ai/Base/Value/PartyMemberSnaredTargetValue.h b/src/Ai/Base/Value/PartyMemberSnaredTargetValue.h new file mode 100644 index 00000000000..d7b517e38a3 --- /dev/null +++ b/src/Ai/Base/Value/PartyMemberSnaredTargetValue.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_PARTYMEMBERSNAREDTARGETVALUE_H +#define _PLAYERBOT_PARTYMEMBERSNAREDTARGETVALUE_H + +#include "NamedObjectContext.h" +#include "PartyMemberValue.h" + +class PartyMemberSnaredTargetValue : public PartyMemberValue +{ +public: + PartyMemberSnaredTargetValue(PlayerbotAI* botAI, std::string const name = "party member snared target") + : PartyMemberValue(botAI, name) + { + } + +protected: + Unit* Calculate() override; +}; + +#endif diff --git a/src/Ai/Base/ValueContext.h b/src/Ai/Base/ValueContext.h index d1b07b88cce..14cbd185c24 100644 --- a/src/Ai/Base/ValueContext.h +++ b/src/Ai/Base/ValueContext.h @@ -66,6 +66,7 @@ #include "PartyMemberToDispel.h" #include "PartyMemberToHeal.h" #include "PartyMemberToResurrect.h" +#include "PartyMemberSnaredTargetValue.h" #include "PartyMemberWithoutAuraValue.h" #include "PartyMemberWithoutItemValue.h" #include "PetTargetValue.h" @@ -152,6 +153,7 @@ class ValueContext : public NamedObjectContext creators["duel target"] = &ValueContext::duel_target; creators["party member to dispel"] = &ValueContext::party_member_to_dispel; creators["party member to protect"] = &ValueContext::party_member_to_protect; + creators["party member snared target"] = &ValueContext::party_member_snared_target; creators["health"] = &ValueContext::health; creators["rage"] = &ValueContext::rage; creators["energy"] = &ValueContext::energy; @@ -450,6 +452,7 @@ class ValueContext : public NamedObjectContext static UntypedValue* party_member_to_resurrect(PlayerbotAI* botAI) { return new PartyMemberToResurrect(botAI); } static UntypedValue* party_member_to_dispel(PlayerbotAI* botAI) { return new PartyMemberToDispel(botAI); } static UntypedValue* party_member_to_protect(PlayerbotAI* botAI) { return new PartyMemberToProtect(botAI); } + static UntypedValue* party_member_snared_target(PlayerbotAI* botAI) { return new PartyMemberSnaredTargetValue(botAI); } static UntypedValue* current_target(PlayerbotAI* botAI) { return new CurrentTargetValue(botAI); } static UntypedValue* old_target(PlayerbotAI* botAI) { return new CurrentTargetValue(botAI); } static UntypedValue* self_target(PlayerbotAI* botAI) { return new SelfTargetValue(botAI); } diff --git a/src/Ai/Class/Paladin/Action/PaladinActions.cpp b/src/Ai/Class/Paladin/Action/PaladinActions.cpp index 944b6e68601..38e17af1e6e 100644 --- a/src/Ai/Class/Paladin/Action/PaladinActions.cpp +++ b/src/Ai/Class/Paladin/Action/PaladinActions.cpp @@ -7,6 +7,7 @@ #include "AiFactory.h" #include "Event.h" +#include "PaladinHelper.h" #include "PlayerbotAI.h" #include "Playerbots.h" #include "SharedDefines.h" @@ -468,6 +469,39 @@ bool CastSealSpellAction::isUseful() { return AI_VALUE2(bool, "combat", "self ta Value* CastTurnUndeadAction::GetTargetValue() { return context->GetValue("cc target", getName()); } +Unit* CastHandOfFreedomOnPartyAction::GetTarget() +{ + bool const selfImpaired = botAI->IsMovementImpaired(bot); + bool const hasSelfHand = selfImpaired && ai::paladin::HasAnyPaladinHandFromCaster(bot, bot); + + if (!bot->GetGroup()) + { + if (selfImpaired && !hasSelfHand) + return bot; + + return nullptr; + } + + if (selfImpaired && !hasSelfHand) + return bot; + + return CastBuffSpellAction::GetTarget(); +} + +Value* CastHandOfFreedomOnPartyAction::GetTargetValue() +{ + return context->GetValue("party member snared target"); +} + +bool CastHandOfFreedomOnPartyAction::isUseful() +{ + Unit* target = GetTarget(); + if (!target) + return false; + + return CastBuffSpellAction::isUseful() && !ai::paladin::HasAnyPaladinHandFromCaster(target, bot); +} + Unit* CastRighteousDefenseAction::GetTarget() { Unit* current_target = AI_VALUE(Unit*, "current target"); diff --git a/src/Ai/Class/Paladin/Action/PaladinActions.h b/src/Ai/Class/Paladin/Action/PaladinActions.h index c58c3209d65..75b0637a4bc 100644 --- a/src/Ai/Class/Paladin/Action/PaladinActions.h +++ b/src/Ai/Class/Paladin/Action/PaladinActions.h @@ -371,6 +371,19 @@ class CastTurnUndeadAction : public CastBuffSpellAction Value* GetTargetValue() override; }; +class CastHandOfFreedomOnPartyAction : public CastBuffSpellAction, public PartyMemberActionNameSupport +{ +public: + CastHandOfFreedomOnPartyAction(PlayerbotAI* botAI) + : CastBuffSpellAction(botAI, "hand of freedom"), PartyMemberActionNameSupport("hand of freedom") {} + + Unit* GetTarget() override; + Value* GetTargetValue() override; + std::string const GetTargetName() override { return "party member snared target"; } + std::string const getName() override { return PartyMemberActionNameSupport::getName(); } + bool isUseful() override; +}; + PROTECT_ACTION(CastBlessingOfProtectionProtectAction, "blessing of protection"); class CastDivinePleaAction : public CastBuffSpellAction diff --git a/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp b/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp index ed8f4931b38..3c92e57c33c 100644 --- a/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp +++ b/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp @@ -142,6 +142,7 @@ class PaladinTriggerFactoryInternal : public NamedObjectContext creators["repentance interrupt"] = &PaladinTriggerFactoryInternal::repentance_interrupt; creators["beacon of light on main tank"] = &PaladinTriggerFactoryInternal::beacon_of_light_on_main_tank; creators["sacred shield on main tank"] = &PaladinTriggerFactoryInternal::sacred_shield_on_main_tank; + creators["hand of freedom on party"] = &PaladinTriggerFactoryInternal::hand_of_freedom_on_party; creators["blessing of kings on party"] = &PaladinTriggerFactoryInternal::blessing_of_kings_on_party; creators["blessing of wisdom on party"] = &PaladinTriggerFactoryInternal::blessing_of_wisdom_on_party; @@ -207,6 +208,7 @@ class PaladinTriggerFactoryInternal : public NamedObjectContext static Trigger* repentance_interrupt(PlayerbotAI* botAI) { return new RepentanceInterruptTrigger(botAI); } static Trigger* beacon_of_light_on_main_tank(PlayerbotAI* ai) { return new BeaconOfLightOnMainTankTrigger(ai); } static Trigger* sacred_shield_on_main_tank(PlayerbotAI* ai) { return new SacredShieldOnMainTankTrigger(ai); } + static Trigger* hand_of_freedom_on_party(PlayerbotAI* botAI) { return new HandOfFreedomOnPartyTrigger(botAI); } static Trigger* blessing_of_kings_on_party(PlayerbotAI* botAI) { return new BlessingOfKingsOnPartyTrigger(botAI); } static Trigger* blessing_of_wisdom_on_party(PlayerbotAI* botAI) @@ -308,6 +310,7 @@ class PaladinAiObjectContextInternal : public NamedObjectContext creators["divine illumination"] = &PaladinAiObjectContextInternal::divine_illumination; creators["divine sacrifice"] = &PaladinAiObjectContextInternal::divine_sacrifice; creators["cancel divine sacrifice"] = &PaladinAiObjectContextInternal::cancel_divine_sacrifice; + creators["hand of freedom on party"] = &PaladinAiObjectContextInternal::hand_of_freedom_on_party; } private: @@ -414,6 +417,7 @@ class PaladinAiObjectContextInternal : public NamedObjectContext static Action* divine_illumination(PlayerbotAI* ai) { return new CastDivineIlluminationAction(ai); } static Action* divine_sacrifice(PlayerbotAI* ai) { return new CastDivineSacrificeAction(ai); } static Action* cancel_divine_sacrifice(PlayerbotAI* ai) { return new CastCancelDivineSacrificeAction(ai); } + static Action* hand_of_freedom_on_party(PlayerbotAI* ai) { return new CastHandOfFreedomOnPartyAction(ai); } }; SharedNamedObjectContextList PaladinAiObjectContext::sharedStrategyContexts; diff --git a/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp b/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp index 49143f9aef8..f03207216ec 100644 --- a/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp +++ b/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp @@ -35,6 +35,9 @@ void GenericPaladinStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode( "protect party member", { NextAction("blessing of protection on party", ACTION_EMERGENCY + 2) })); + triggers.push_back(new TriggerNode( + "hand of freedom on party", + { NextAction("hand of freedom on party", ACTION_HIGH + 4) })); triggers.push_back( new TriggerNode("high mana", { NextAction("divine plea", ACTION_HIGH) })); } diff --git a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp index 71328c4dcf7..e3367aaef3b 100644 --- a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp +++ b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp @@ -8,6 +8,7 @@ #include "PaladinActions.h" #include "PlayerbotAIConfig.h" #include "Playerbots.h" +#include "PaladinHelper.h" bool SealTrigger::IsActive() { @@ -31,6 +32,40 @@ bool BlessingTrigger::IsActive() "blessing of kings", "blessing of sanctuary", nullptr); } +Unit* HandOfFreedomOnPartyTrigger::GetTarget() +{ + bool const selfImpaired = botAI->IsMovementImpaired(bot); + bool const hasSelfHand = selfImpaired && ai::paladin::HasAnyPaladinHandFromCaster(bot, bot); + + if (!bot->GetGroup()) + { + if (selfImpaired && !hasSelfHand) + return bot; + + return nullptr; + } + + if (selfImpaired && !hasSelfHand) + return bot; + + return Trigger::GetTarget(); +} + +bool HandOfFreedomOnPartyTrigger::IsActive() +{ + Unit* target = GetTarget(); + if (!target) + return false; + + if (target != bot && bot->GetExactDist2dSq(target->GetPositionX(), target->GetPositionY()) > 30.0f * 30.0f) + return false; + + if (!botAI->CanCastSpell("hand of freedom", target)) + return false; + + return !ai::paladin::HasAnyPaladinHandFromCaster(target, bot) && botAI->IsMovementImpaired(target); +} + bool NotSensingUndeadTrigger::IsActive() { return !botAI->HasAura("sense undead", bot); diff --git a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h index f33b66890ad..cc6ceddcf4c 100644 --- a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h +++ b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h @@ -242,6 +242,16 @@ class BlessingOfSanctuaryOnPartyTrigger : public BuffOnPartyTrigger : BuffOnPartyTrigger(botAI, "blessing of sanctuary", 2 * 2000) {} }; +class HandOfFreedomOnPartyTrigger : public Trigger +{ +public: + HandOfFreedomOnPartyTrigger(PlayerbotAI* botAI) : Trigger(botAI, "hand of freedom on party", 1) {} + + Unit* GetTarget() override; + std::string const GetTargetName() override { return "party member snared target"; } + bool IsActive() override; +}; + class AvengingWrathTrigger : public BoostTrigger { public: diff --git a/src/Ai/Class/Paladin/Util/PaladinHelper.h b/src/Ai/Class/Paladin/Util/PaladinHelper.h new file mode 100644 index 00000000000..64b88b73103 --- /dev/null +++ b/src/Ai/Class/Paladin/Util/PaladinHelper.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_PALADINHELPER_H +#define _PLAYERBOT_PALADINHELPER_H + +#include + +#include "Unit.h" + +class Player; + +namespace ai::paladin +{ +static constexpr uint32 SPELL_HAND_OF_PROTECTION = 1022; +static constexpr uint32 SPELL_HAND_OF_SALVATION = 1038; +static constexpr uint32 SPELL_HAND_OF_FREEDOM = 1044; +static constexpr uint32 SPELL_HAND_OF_SACRIFICE = 6940; + +inline bool HasHandFromCaster(Unit* target, Player* caster, std::initializer_list spellIds) +{ + if (!target || !caster) + return false; + + for (uint32 spellId : spellIds) + { + if (target->HasAura(spellId, caster->GetGUID())) + return true; + } + + return false; +} + +inline bool HasAnyPaladinHandFromCaster(Unit* target, Player* caster) +{ + return HasHandFromCaster(target, caster, + { SPELL_HAND_OF_PROTECTION, SPELL_HAND_OF_SALVATION, SPELL_HAND_OF_FREEDOM, SPELL_HAND_OF_SACRIFICE }); +} +} + +#endif diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 4a746009ac0..446ca8c40f1 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -1962,6 +1962,11 @@ bool PlayerbotAI::HasAggro(Unit* unit) return false; } +bool PlayerbotAI::IsMovementImpaired(Unit* unit) +{ + return unit && (unit->HasAuraType(SPELL_AURA_MOD_ROOT) || unit->IsRooted() || unit->GetSpeedRate(MOVE_RUN) < 1.0f); +} + int32 PlayerbotAI::GetAssistTankIndex(Player* player) { Group* group = player->GetGroup(); diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index 5d64bf15946..9c417561fec 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -430,6 +430,7 @@ class PlayerbotAI : public PlayerbotAIBase static bool IsAssistHealOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers = false); static bool IsAssistRangedDpsOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers = false); bool HasAggro(Unit* unit); + bool IsMovementImpaired(Unit* unit); static int32 GetAssistTankIndex(Player* player); int32 GetGroupSlotIndex(Player* player); int32 GetRangedIndex(Player* player); From 15bf0ab427a91b35e31ffd5551bef8d90c0d6e1a Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 3 Apr 2026 15:25:40 -0500 Subject: [PATCH 04/18] Fix Shaman Weapon Enchants & Cure Toxins/Cleanse Spirit (#2234) ## Pull Request Description 1. I've been having persistent issues with Enhancement Shamans sometimes applying Rockbiter to both weapons instead of MH Windfury and OH Flametongue. Rockbiter is the alternative for Flametongue and, through Flametongue, the alternative for Windfury. But there seemed to be no obvious reason why a Shaman that had all three abilities would ever use Rockbiter, which costs more mana than Windfury and Flametongue. Claude's take on it is that there is instability from ItemForSpellValue related to its poor way of distinguishing handedness, in addition to it having a 1-second cache, which can cause in some scenarios stale caches for the action running on each hand back-to-back. I still can't say I fully understand why the issue exists, but the most straightforward fix that should prevent this from happening is to just have separate mainhand and offhand actions for each enchant. So that's what this PR does. The relevant ActionNodes are now: - The MH-specific chain for Enhancement is WF -> FT -> RB. In practice, Enhancement should never apply RB because all Shamans under level 10 (when FT is learned) are considered Elemental. The FT -> RB node is just for Elemental. - The MH-specific Resto chain (not that Resto can dual-wield) is EL -> FT -> RB. Againt, FT -> RB is just for Elemental. - OH for Enhancement is only FT. You cannot be Enhancement before level 10, nor can Enhancement dual-wield before level 40, so no alternative is needed. 3. I commented out Frostbrand Weapon actions/triggers because the ability is not included in any strategy. I didn't delete the code because in the future somebody might want to implement it as I understand it can be useful for Enhancement Shamans in PvP. 4. Shamans are coded to use "cure poison" and "cure disease", which do not exist in WotLK, having been combined into Cure Toxins. Wishmaster has PR #1844 that has been open on this for a long time, but I decided to correct the abilities here anyway as he was going for a more limited approach, and I decided to rename all the actions and redo the structure to rely on ActionNode alternatives, which is pretty much the exact framework that should be used for this type of situation w/r/t bots. Now, Shamans prefer Cleanse Spirit (Resto talent, which costs the same as Cure Toxins and also dispels curses), with an alternative of Cure Toxins (for poisons and disease only). I tested this and it seems to work well. 5. I deleted empty ActionNodes. 6. I did some cleanup of formatting and such, but this is not intended to be a comprehensive refactor. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. I've followed the general intended structure of class strategies with triggers and actions. The same triggers exist, just different actions are called based on the trigger that fires, so I don't think there should be any impact on performance. ## How to Test the Changes The best way to get a grasp on if things work is probably to just do group play for a while with Shamans and make sure they apply the right enchants and properly cast Cleanse Spirit and Cure Toxins. ## 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**) Shamans previously did not cure poisons or disease at all, and now they do with the default "cure" strategy applied. - Does this change add new decision branches or increase maintenance complexity? - - [ ] No - - [x] Yes (**explain below**) One might say having separate actions per hand for enchants is somewhat more complex, but ultimately I think it is less confusing to keep those paths separate. ## 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 had Claude try to diagnose the weapon enchant issue. It proposed and provided the separate MH/OH WF/FT actions. The other things were easy enough for me to do. ## 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 --- src/Ai/Base/Actions/GenericSpellActions.cpp | 33 +- src/Ai/Base/Actions/GenericSpellActions.h | 14 + src/Ai/Class/Shaman/Action/ShamanActions.cpp | 25 +- src/Ai/Class/Shaman/Action/ShamanActions.h | 352 ++++++++++-------- src/Ai/Class/Shaman/ShamanAiObjectContext.cpp | 194 +++++----- .../Strategy/ElementalShamanStrategy.cpp | 33 +- .../Strategy/EnhancementShamanStrategy.cpp | 17 - .../Shaman/Strategy/GenericShamanStrategy.cpp | 61 +-- .../Shaman/Strategy/RestoShamanStrategy.cpp | 97 +---- .../Strategy/ShamanNonCombatStrategy.cpp | 110 +++--- .../Shaman/Strategy/TotemsShamanStrategy.cpp | 19 +- .../Class/Shaman/Trigger/ShamanTriggers.cpp | 6 +- src/Ai/Class/Shaman/Trigger/ShamanTriggers.h | 137 +++---- 13 files changed, 519 insertions(+), 579 deletions(-) diff --git a/src/Ai/Base/Actions/GenericSpellActions.cpp b/src/Ai/Base/Actions/GenericSpellActions.cpp index 82bdb74d2a2..41911f62d8e 100644 --- a/src/Ai/Base/Actions/GenericSpellActions.cpp +++ b/src/Ai/Base/Actions/GenericSpellActions.cpp @@ -151,7 +151,9 @@ bool CastMeleeSpellAction::isUseful() return CastSpellAction::isUseful(); } -CastMeleeDebuffSpellAction::CastMeleeDebuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool isOwner, float needLifeTime) : CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) +CastMeleeDebuffSpellAction::CastMeleeDebuffSpellAction( + PlayerbotAI* botAI, std::string const spell, bool isOwner, float needLifeTime) : + CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) { range = ATTACK_DISTANCE; } @@ -203,6 +205,35 @@ bool CastEnchantItemAction::isPossible() return spellId && AI_VALUE2(Item*, "item for spell", spellId); } +CastEnchantItemMainHandAction::CastEnchantItemMainHandAction(PlayerbotAI* botAI, std::string const spell) + : CastEnchantItemAction(botAI, spell) {} + +bool CastEnchantItemMainHandAction::isPossible() +{ + if (!CastEnchantItemAction::isPossible()) + return false; + + Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND); + return item && !item->GetEnchantmentId(TEMP_ENCHANTMENT_SLOT) && + item->GetTemplate()->Class == ITEM_CLASS_WEAPON; +} + +CastEnchantItemOffHandAction::CastEnchantItemOffHandAction(PlayerbotAI* botAI, std::string const spell) + : CastEnchantItemAction(botAI, spell) {} + +bool CastEnchantItemOffHandAction::isPossible() +{ + if (!CastEnchantItemAction::isPossible()) + return false; + + Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND); + if (!item || item->GetEnchantmentId(TEMP_ENCHANTMENT_SLOT)) + return false; + + uint32 invType = item->GetTemplate()->InventoryType; + return invType == INVTYPE_WEAPON || invType == INVTYPE_WEAPONOFFHAND; +} + CastHealingSpellAction::CastHealingSpellAction(PlayerbotAI* botAI, std::string const spell, uint8 estAmount, HealingManaEfficiency manaEfficiency, bool isOwner) : CastAuraSpellAction(botAI, spell, isOwner), estAmount(estAmount), manaEfficiency(manaEfficiency) diff --git a/src/Ai/Base/Actions/GenericSpellActions.h b/src/Ai/Base/Actions/GenericSpellActions.h index b15c4894a3b..dc07857138d 100644 --- a/src/Ai/Base/Actions/GenericSpellActions.h +++ b/src/Ai/Base/Actions/GenericSpellActions.h @@ -130,6 +130,20 @@ class CastEnchantItemAction : public CastSpellAction std::string const GetTargetName() override { return "self target"; } }; +class CastEnchantItemMainHandAction : public CastEnchantItemAction +{ +public: + CastEnchantItemMainHandAction(PlayerbotAI* botAI, std::string const spell); + bool isPossible() override; +}; + +class CastEnchantItemOffHandAction : public CastEnchantItemAction +{ +public: + CastEnchantItemOffHandAction(PlayerbotAI* botAI, std::string const spell); + bool isPossible() override; +}; + class CastHealingSpellAction : public CastAuraSpellAction { public: diff --git a/src/Ai/Class/Shaman/Action/ShamanActions.cpp b/src/Ai/Class/Shaman/Action/ShamanActions.cpp index 3bb0ae86f4d..47634c4e070 100644 --- a/src/Ai/Class/Shaman/Action/ShamanActions.cpp +++ b/src/Ai/Class/Shaman/Action/ShamanActions.cpp @@ -57,7 +57,8 @@ bool CastLavaBurstAction::isUseful() if (!target) return false; - static const uint32 FLAME_SHOCK_SPELL_IDS[] = {8050, 8052, 8053, 10447, 10448, 29228, 25457, 49232, 49233}; + static const uint32 FLAME_SHOCK_SPELL_IDS[] = + {8050, 8052, 8053, 10447, 10448, 29228, 25457, 49232, 49233}; ObjectGuid botGuid = bot->GetGUID(); for (uint32 spellId : FLAME_SHOCK_SPELL_IDS) @@ -65,6 +66,7 @@ bool CastLavaBurstAction::isUseful() if (target->HasAura(spellId, botGuid)) return true; } + return false; } @@ -77,15 +79,13 @@ bool CastSpiritWalkAction::Execute(Event /*event*/) for (Unit* unit : bot->m_Controlled) { - if (unit->GetEntry() == SPIRIT_WOLF) + if (unit->GetEntry() == SPIRIT_WOLF && unit->HasSpell(SPIRIT_WALK_SPELL)) { - if (unit->HasSpell(SPIRIT_WALK_SPELL)) - { - unit->CastSpell(unit, SPIRIT_WALK_SPELL, false); - return true; - } + unit->CastSpell(unit, SPIRIT_WALK_SPELL, false); + return true; } } + return false; } @@ -105,16 +105,13 @@ bool SetTotemAction::Execute(Event /*event*/) } if (!totemSpell) - { return false; - } - if (const ActionButton* button = bot->GetActionButton(actionButtonId)) + if (const ActionButton* button = bot->GetActionButton(actionButtonId); + button && button->GetType() == ACTION_BUTTON_SPELL && + button->GetAction() == totemSpell) { - if (button->GetType() == ACTION_BUTTON_SPELL && button->GetAction() == totemSpell) - { - return false; - } + return false; } bot->addActionButton(actionButtonId, totemSpell, ACTION_BUTTON_SPELL); diff --git a/src/Ai/Class/Shaman/Action/ShamanActions.h b/src/Ai/Class/Shaman/Action/ShamanActions.h index 69f29f0496e..cdd661561ec 100644 --- a/src/Ai/Class/Shaman/Action/ShamanActions.h +++ b/src/Ai/Class/Shaman/Action/ShamanActions.h @@ -18,73 +18,92 @@ class PlayerbotAI; class CastWaterShieldAction : public CastBuffSpellAction { public: - CastWaterShieldAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "water shield") {} + CastWaterShieldAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "water shield") {} }; class CastLightningShieldAction : public CastBuffSpellAction { public: - CastLightningShieldAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "lightning shield") {} + CastLightningShieldAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "lightning shield") {} }; -class CastEarthlivingWeaponAction : public CastEnchantItemAction +class CastRockbiterWeaponMainHandAction : public CastEnchantItemMainHandAction { public: - CastEarthlivingWeaponAction(PlayerbotAI* botAI) : CastEnchantItemAction(botAI, "earthliving weapon") {} + CastRockbiterWeaponMainHandAction(PlayerbotAI* botAI) : + CastEnchantItemMainHandAction(botAI, "rockbiter weapon") {} }; -class CastRockbiterWeaponAction : public CastEnchantItemAction +class CastFlametongueWeaponMainHandAction : public CastEnchantItemMainHandAction { public: - CastRockbiterWeaponAction(PlayerbotAI* botAI) : CastEnchantItemAction(botAI, "rockbiter weapon") {} + CastFlametongueWeaponMainHandAction(PlayerbotAI* botAI) : + CastEnchantItemMainHandAction(botAI, "flametongue weapon") {} }; -class CastFlametongueWeaponAction : public CastEnchantItemAction +class CastFlametongueWeaponOffHandAction : public CastEnchantItemOffHandAction { public: - CastFlametongueWeaponAction(PlayerbotAI* botAI) : CastEnchantItemAction(botAI, "flametongue weapon") {} + CastFlametongueWeaponOffHandAction(PlayerbotAI* botAI) : + CastEnchantItemOffHandAction(botAI, "flametongue weapon") {} }; -class CastFrostbrandWeaponAction : public CastEnchantItemAction +/* class CastFrostbrandWeaponOffHandAction : public CastEnchantItemOffHandAction { public: - CastFrostbrandWeaponAction(PlayerbotAI* botAI) : CastEnchantItemAction(botAI, "frostbrand weapon") {} + CastFrostbrandWeaponOffHandAction(PlayerbotAI* botAI) : + CastEnchantItemOffHandAction(botAI, "frostbrand weapon") {} +}; */ + +class CastEarthlivingWeaponMainHandAction : public CastEnchantItemMainHandAction +{ +public: + CastEarthlivingWeaponMainHandAction(PlayerbotAI* botAI) : + CastEnchantItemMainHandAction(botAI, "earthliving weapon") {} }; -class CastWindfuryWeaponAction : public CastEnchantItemAction +class CastWindfuryWeaponMainHandAction : public CastEnchantItemMainHandAction { public: - CastWindfuryWeaponAction(PlayerbotAI* botAI) : CastEnchantItemAction(botAI, "windfury weapon") {} + CastWindfuryWeaponMainHandAction(PlayerbotAI* botAI) : + CastEnchantItemMainHandAction(botAI, "windfury weapon") {} }; class CastAncestralSpiritAction : public ResurrectPartyMemberAction { public: - CastAncestralSpiritAction(PlayerbotAI* botAI) : ResurrectPartyMemberAction(botAI, "ancestral spirit") {} + CastAncestralSpiritAction(PlayerbotAI* botAI) : + ResurrectPartyMemberAction(botAI, "ancestral spirit") {} }; class CastWaterBreathingAction : public CastBuffSpellAction { public: - CastWaterBreathingAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "water breathing") {} + CastWaterBreathingAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "water breathing") {} }; class CastWaterWalkingAction : public CastBuffSpellAction { public: - CastWaterWalkingAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "water walking") {} + CastWaterWalkingAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "water walking") {} }; class CastWaterBreathingOnPartyAction : public BuffOnPartyAction { public: - CastWaterBreathingOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "water breathing") {} + CastWaterBreathingOnPartyAction(PlayerbotAI* botAI) : + BuffOnPartyAction(botAI, "water breathing") {} }; class CastWaterWalkingOnPartyAction : public BuffOnPartyAction { public: - CastWaterWalkingOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "water walking") {} + CastWaterWalkingOnPartyAction(PlayerbotAI* botAI) : + BuffOnPartyAction(botAI, "water walking") {} }; // Boost Actions @@ -92,31 +111,36 @@ class CastWaterWalkingOnPartyAction : public BuffOnPartyAction class CastHeroismAction : public CastBuffSpellAction { public: - CastHeroismAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "heroism") {} + CastHeroismAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "heroism") {} }; class CastBloodlustAction : public CastBuffSpellAction { public: - CastBloodlustAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "bloodlust") {} + CastBloodlustAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "bloodlust") {} }; class CastElementalMasteryAction : public CastBuffSpellAction { public: - CastElementalMasteryAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "elemental mastery") {} + CastElementalMasteryAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "elemental mastery") {} }; class CastShamanisticRageAction : public CastBuffSpellAction { public: - CastShamanisticRageAction(PlayerbotAI* ai) : CastBuffSpellAction(ai, "shamanistic rage") {} + CastShamanisticRageAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "shamanistic rage") {} }; class CastFeralSpiritAction : public CastSpellAction { public: - CastFeralSpiritAction(PlayerbotAI* ai) : CastSpellAction(ai, "feral spirit") {} + CastFeralSpiritAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "feral spirit") {} }; class CastSpiritWalkAction : public Action @@ -138,7 +162,8 @@ class CastWindShearAction : public CastSpellAction class CastWindShearOnEnemyHealerAction : public CastSpellOnEnemyHealerAction { public: - CastWindShearOnEnemyHealerAction(PlayerbotAI* botAI) : CastSpellOnEnemyHealerAction(botAI, "wind shear") {} + CastWindShearOnEnemyHealerAction(PlayerbotAI* botAI) : + CastSpellOnEnemyHealerAction(botAI, "wind shear") {} }; class CastPurgeAction : public CastSpellAction @@ -150,16 +175,15 @@ class CastPurgeAction : public CastSpellAction class CastCleanseSpiritAction : public CastCureSpellAction { public: - CastCleanseSpiritAction(PlayerbotAI* botAI) : CastCureSpellAction(botAI, "cleanse spirit") {} + CastCleanseSpiritAction(PlayerbotAI* botAI) : + CastCureSpellAction(botAI, "cleanse spirit") {} }; class CastCleanseSpiritPoisonOnPartyAction : public CurePartyMemberAction { public: - CastCleanseSpiritPoisonOnPartyAction(PlayerbotAI* botAI) - : CurePartyMemberAction(botAI, "cleanse spirit", DISPEL_POISON) - { - } + CastCleanseSpiritPoisonOnPartyAction(PlayerbotAI* botAI) : + CurePartyMemberAction(botAI, "cleanse spirit", DISPEL_POISON) {} std::string const getName() override { return "cleanse spirit poison on party"; } }; @@ -167,10 +191,8 @@ class CastCleanseSpiritPoisonOnPartyAction : public CurePartyMemberAction class CastCleanseSpiritCurseOnPartyAction : public CurePartyMemberAction { public: - CastCleanseSpiritCurseOnPartyAction(PlayerbotAI* botAI) - : CurePartyMemberAction(botAI, "cleanse spirit", DISPEL_CURSE) - { - } + CastCleanseSpiritCurseOnPartyAction(PlayerbotAI* botAI) : + CurePartyMemberAction(botAI, "cleanse spirit", DISPEL_CURSE) {} std::string const getName() override { return "cleanse spirit curse on party"; } }; @@ -178,42 +200,35 @@ class CastCleanseSpiritCurseOnPartyAction : public CurePartyMemberAction class CastCleanseSpiritDiseaseOnPartyAction : public CurePartyMemberAction { public: - CastCleanseSpiritDiseaseOnPartyAction(PlayerbotAI* botAI) - : CurePartyMemberAction(botAI, "cleanse spirit", DISPEL_DISEASE) - { - } + CastCleanseSpiritDiseaseOnPartyAction(PlayerbotAI* botAI) : + CurePartyMemberAction(botAI, "cleanse spirit", DISPEL_DISEASE) {} std::string const getName() override { return "cleanse spirit disease on party"; } }; -class CastCurePoisonActionSham : public CastCureSpellAction +class CastCureToxinsActionSham : public CastCureSpellAction { public: - CastCurePoisonActionSham(PlayerbotAI* botAI) : CastCureSpellAction(botAI, "cure poison") {} + CastCureToxinsActionSham(PlayerbotAI* botAI) : + CastCureSpellAction(botAI, "cure toxins") {} }; -class CastCurePoisonOnPartyActionSham : public CurePartyMemberAction +class CastCureToxinsPoisonOnPartyActionSham : public CurePartyMemberAction { public: - CastCurePoisonOnPartyActionSham(PlayerbotAI* botAI) : CurePartyMemberAction(botAI, "cure poison", DISPEL_POISON) {} - - std::string const getName() override { return "cure poison on party"; } -}; + CastCureToxinsPoisonOnPartyActionSham(PlayerbotAI* botAI) : + CurePartyMemberAction(botAI, "cure toxins", DISPEL_POISON) {} -class CastCureDiseaseActionSham : public CastCureSpellAction -{ -public: - CastCureDiseaseActionSham(PlayerbotAI* botAI) : CastCureSpellAction(botAI, "cure disease") {} + std::string const getName() override { return "cure toxins poison on party"; } }; -class CastCureDiseaseOnPartyActionSham : public CurePartyMemberAction +class CastCureToxinsDiseaseOnPartyActionSham : public CurePartyMemberAction { public: - CastCureDiseaseOnPartyActionSham(PlayerbotAI* botAI) : CurePartyMemberAction(botAI, "cure disease", DISPEL_DISEASE) - { - } + CastCureToxinsDiseaseOnPartyActionSham(PlayerbotAI* botAI) : + CurePartyMemberAction(botAI, "cure toxins", DISPEL_DISEASE) {} - std::string const getName() override { return "cure disease on party"; } + std::string const getName() override { return "cure toxins disease on party"; } }; // Damage and Debuff Actions @@ -221,68 +236,77 @@ class CastCureDiseaseOnPartyActionSham : public CurePartyMemberAction class CastFireNovaAction : public CastMeleeSpellAction { public: - CastFireNovaAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "fire nova") {} + CastFireNovaAction(PlayerbotAI* botAI) : + CastMeleeSpellAction(botAI, "fire nova") {} + bool isUseful() override; }; class CastStormstrikeAction : public CastMeleeSpellAction { public: - CastStormstrikeAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "stormstrike") {} + CastStormstrikeAction(PlayerbotAI* botAI) : + CastMeleeSpellAction(botAI, "stormstrike") {} }; class CastLavaLashAction : public CastMeleeSpellAction { public: - CastLavaLashAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "lava lash") {} + CastLavaLashAction(PlayerbotAI* botAI) : + CastMeleeSpellAction(botAI, "lava lash") {} }; class CastFlameShockAction : public CastDebuffSpellAction { public: - CastFlameShockAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "flame shock", true, 6.0f) {} - bool isUseful() override - { - // Bypass TTL check - return CastAuraSpellAction::isUseful(); - } + CastFlameShockAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "flame shock", true, 6.0f) {} + + bool isUseful() override { return CastAuraSpellAction::isUseful(); } }; class CastEarthShockAction : public CastSpellAction { public: - CastEarthShockAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "earth shock") {} + CastEarthShockAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "earth shock") {} }; class CastFrostShockAction : public CastSnareSpellAction { public: - CastFrostShockAction(PlayerbotAI* botAI) : CastSnareSpellAction(botAI, "frost shock") {} + CastFrostShockAction(PlayerbotAI* botAI) : + CastSnareSpellAction(botAI, "frost shock") {} }; class CastChainLightningAction : public CastSpellAction { public: - CastChainLightningAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "chain lightning") {} + CastChainLightningAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "chain lightning") {} + ActionThreatType getThreatType() override { return ActionThreatType::Aoe; } }; class CastLightningBoltAction : public CastSpellAction { public: - CastLightningBoltAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "lightning bolt") {} + CastLightningBoltAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "lightning bolt") {} }; class CastThunderstormAction : public CastSpellAction { public: - CastThunderstormAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "thunderstorm") {} + CastThunderstormAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "thunderstorm") {} }; class CastLavaBurstAction : public CastSpellAction { public: - CastLavaBurstAction(PlayerbotAI* ai) : CastSpellAction(ai, "lava burst") {} + CastLavaBurstAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "lava burst") {} bool isUseful() override; }; @@ -291,73 +315,71 @@ class CastLavaBurstAction : public CastSpellAction class CastLesserHealingWaveAction : public CastHealingSpellAction { public: - CastLesserHealingWaveAction(PlayerbotAI* botAI) : CastHealingSpellAction(botAI, "lesser healing wave") {} + CastLesserHealingWaveAction(PlayerbotAI* botAI) : + CastHealingSpellAction(botAI, "lesser healing wave") {} }; class CastLesserHealingWaveOnPartyAction : public HealPartyMemberAction { public: - CastLesserHealingWaveOnPartyAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "lesser healing wave", 25.0f, HealingManaEfficiency::LOW) - { - } + CastLesserHealingWaveOnPartyAction(PlayerbotAI* botAI) : + HealPartyMemberAction(botAI, "lesser healing wave", 25.0f, HealingManaEfficiency::LOW) {} }; class CastHealingWaveAction : public CastHealingSpellAction { public: - CastHealingWaveAction(PlayerbotAI* botAI) : CastHealingSpellAction(botAI, "healing wave") {} + CastHealingWaveAction(PlayerbotAI* botAI) : + CastHealingSpellAction(botAI, "healing wave") {} }; class CastHealingWaveOnPartyAction : public HealPartyMemberAction { public: - CastHealingWaveOnPartyAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "healing wave", 50.0f, HealingManaEfficiency::MEDIUM) - { - } + CastHealingWaveOnPartyAction(PlayerbotAI* botAI) : + HealPartyMemberAction(botAI, "healing wave", 50.0f, HealingManaEfficiency::MEDIUM) {} }; class CastChainHealAction : public HealPartyMemberAction { public: - CastChainHealAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "chain heal", 15.0f, HealingManaEfficiency::HIGH) - { - } + CastChainHealAction(PlayerbotAI* botAI) : + HealPartyMemberAction(botAI, "chain heal", 15.0f, HealingManaEfficiency::HIGH) {} }; class CastRiptideAction : public CastHealingSpellAction { public: - CastRiptideAction(PlayerbotAI* botAI) : CastHealingSpellAction(botAI, "riptide") {} + CastRiptideAction(PlayerbotAI* botAI) : + CastHealingSpellAction(botAI, "riptide") {} }; class CastRiptideOnPartyAction : public HealPartyMemberAction { public: - CastRiptideOnPartyAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "riptide", 15.0f, HealingManaEfficiency::VERY_HIGH) - { - } + CastRiptideOnPartyAction(PlayerbotAI* botAI) : + HealPartyMemberAction(botAI, "riptide", 15.0f, HealingManaEfficiency::VERY_HIGH) {} }; class CastEarthShieldAction : public CastBuffSpellAction { public: - CastEarthShieldAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "earth shield") {} + CastEarthShieldAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "earth shield") {} }; class CastEarthShieldOnPartyAction : public BuffOnPartyAction { public: - CastEarthShieldOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "earth shield") {} + CastEarthShieldOnPartyAction(PlayerbotAI* botAI) : + BuffOnPartyAction(botAI, "earth shield") {} }; class CastEarthShieldOnMainTankAction : public BuffOnMainTankAction { public: - CastEarthShieldOnMainTankAction(PlayerbotAI* ai) : BuffOnMainTankAction(ai, "earth shield", false) {} + CastEarthShieldOnMainTankAction(PlayerbotAI* botAI) : + BuffOnMainTankAction(botAI, "earth shield", false) {} }; // Totem Spells @@ -365,8 +387,9 @@ class CastEarthShieldOnMainTankAction : public BuffOnMainTankAction class CastTotemAction : public CastBuffSpellAction { public: - CastTotemAction(PlayerbotAI* botAI, std::string const spell, std::string const buffName = "") - : CastBuffSpellAction(botAI, spell) + CastTotemAction( + PlayerbotAI* botAI, std::string const spell, + std::string const buffName = "") : CastBuffSpellAction(botAI, spell) { buff = (buffName == "") ? spell : buffName; } @@ -380,56 +403,66 @@ class CastTotemAction : public CastBuffSpellAction class CastCallOfTheElementsAction : public CastSpellAction { public: - CastCallOfTheElementsAction(PlayerbotAI* ai) : CastSpellAction(ai, "call of the elements") {} + CastCallOfTheElementsAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "call of the elements") {} }; class CastTotemicRecallAction : public CastSpellAction { public: - CastTotemicRecallAction(PlayerbotAI* ai) : CastSpellAction(ai, "totemic recall") {} + CastTotemicRecallAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "totemic recall") {} }; class CastStrengthOfEarthTotemAction : public CastTotemAction { public: - CastStrengthOfEarthTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "strength of earth totem", "strength of earth") {} + CastStrengthOfEarthTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "strength of earth totem", "strength of earth") {} }; class CastStoneskinTotemAction : public CastTotemAction { public: - CastStoneskinTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "stoneskin totem", "stoneskin") {} + CastStoneskinTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "stoneskin totem", "stoneskin") {} }; class CastTremorTotemAction : public CastTotemAction { public: - CastTremorTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "tremor totem", "") {} + CastTremorTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "tremor totem", "") {} }; class CastEarthbindTotemAction : public CastTotemAction { public: - CastEarthbindTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "earthbind totem", "") {} + CastEarthbindTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "earthbind totem", "") {} }; class CastStoneclawTotemAction : public CastTotemAction { public: - CastStoneclawTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "stoneclaw totem", "") {} + CastStoneclawTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "stoneclaw totem", "") {} bool isUseful() override; }; class CastSearingTotemAction : public CastTotemAction { public: - CastSearingTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "searing totem", "") {} + CastSearingTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "searing totem", "") {} }; class CastMagmaTotemAction : public CastTotemAction { public: - CastMagmaTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "magma totem", "") {} + CastMagmaTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "magma totem", "") {} + std::string const GetTargetName() override { return "self target"; } bool isUseful() override; }; @@ -437,26 +470,30 @@ class CastMagmaTotemAction : public CastTotemAction class CastFlametongueTotemAction : public CastTotemAction { public: - CastFlametongueTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "flametongue totem", "flametongue totem") {} + CastFlametongueTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "flametongue totem", "flametongue totem") {} }; class CastTotemOfWrathAction : public CastTotemAction { public: - CastTotemOfWrathAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "totem of wrath", "totem of wrath") {} + CastTotemOfWrathAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "totem of wrath", "totem of wrath") {} }; class CastFrostResistanceTotemAction : public CastTotemAction { public: - CastFrostResistanceTotemAction(PlayerbotAI* botAI) - : CastTotemAction(botAI, "frost resistance totem", "frost resistance") {} + CastFrostResistanceTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "frost resistance totem", "frost resistance") {} }; class CastFireElementalTotemAction : public CastTotemAction { public: - CastFireElementalTotemAction(PlayerbotAI* ai) : CastTotemAction(ai, "fire elemental totem", "") {} + CastFireElementalTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "fire elemental totem", "") {} + virtual std::string const GetTargetName() override { return "self target"; } virtual bool isUseful() override { return CastTotemAction::isUseful(); } }; @@ -464,7 +501,9 @@ class CastFireElementalTotemAction : public CastTotemAction class CastFireElementalTotemMeleeAction : public CastTotemAction { public: - CastFireElementalTotemMeleeAction(PlayerbotAI* ai) : CastTotemAction(ai, "fire elemental totem", "") {} + CastFireElementalTotemMeleeAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "fire elemental totem", "") {} + virtual std::string const GetTargetName() override { return "self target"; } virtual bool isUseful() override { @@ -478,51 +517,60 @@ class CastFireElementalTotemMeleeAction : public CastTotemAction class CastHealingStreamTotemAction : public CastTotemAction { public: - CastHealingStreamTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "healing stream totem", "") {} + CastHealingStreamTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "healing stream totem", "") {} }; class CastManaSpringTotemAction : public CastTotemAction { public: - CastManaSpringTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "mana spring totem", "mana spring") {} + CastManaSpringTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "mana spring totem", "mana spring") {} }; class CastCleansingTotemAction : public CastTotemAction { public: - CastCleansingTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "cleansing totem", "") {} + CastCleansingTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "cleansing totem", "") {} virtual bool isUseful(); }; class CastManaTideTotemAction : public CastTotemAction { public: - CastManaTideTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "mana tide totem", "") {} + CastManaTideTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "mana tide totem", "") {} + std::string const GetTargetName() override { return "self target"; } }; class CastFireResistanceTotemAction : public CastTotemAction { public: - CastFireResistanceTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "fire resistance totem", "fire resistance") {} + CastFireResistanceTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "fire resistance totem", "fire resistance") {} }; class CastWrathOfAirTotemAction : public CastTotemAction { public: - CastWrathOfAirTotemAction(PlayerbotAI* ai) : CastTotemAction(ai, "wrath of air totem", "wrath of air totem") {} + CastWrathOfAirTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "wrath of air totem", "wrath of air totem") {} }; class CastWindfuryTotemAction : public CastTotemAction { public: - CastWindfuryTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "windfury totem", "windfury totem") {} + CastWindfuryTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "windfury totem", "windfury totem") {} }; class CastNatureResistanceTotemAction : public CastTotemAction { public: - CastNatureResistanceTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "nature resistance totem", "nature resistance") {} + CastNatureResistanceTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "nature resistance totem", "nature resistance") {} }; // Set Strategy Assigned Totems @@ -532,12 +580,8 @@ class SetTotemAction : public Action public: // Template constructor: infers N (size of the id array) at compile time template - SetTotemAction(PlayerbotAI* botAI, std::string const& totemName, const uint32 (&ids)[N], int actionButtonId) - : Action(botAI, "set " + totemName) - , totemSpellIds(ids) - , totemSpellIdsCount(N) - , actionButtonId(actionButtonId) - {} + SetTotemAction(PlayerbotAI* botAI, std::string const& totemName, const uint32 (&ids)[N], int actionButtonId) : + Action(botAI, "set " + totemName), totemSpellIds(ids), totemSpellIdsCount(N), actionButtonId(actionButtonId) {} bool Execute(Event event) override; uint32 const* totemSpellIds; @@ -548,120 +592,120 @@ class SetTotemAction : public Action class SetStrengthOfEarthTotemAction : public SetTotemAction { public: - SetStrengthOfEarthTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "strength of earth totem", STRENGTH_OF_EARTH_TOTEM, TOTEM_BAR_SLOT_EARTH) {} + SetStrengthOfEarthTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "strength of earth totem", STRENGTH_OF_EARTH_TOTEM, TOTEM_BAR_SLOT_EARTH) {} }; class SetStoneskinTotemAction : public SetTotemAction { public: - SetStoneskinTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "stoneskin totem", STONESKIN_TOTEM, TOTEM_BAR_SLOT_EARTH) {} + SetStoneskinTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "stoneskin totem", STONESKIN_TOTEM, TOTEM_BAR_SLOT_EARTH) {} }; class SetTremorTotemAction : public SetTotemAction { public: - SetTremorTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "tremor totem", TREMOR_TOTEM, TOTEM_BAR_SLOT_EARTH) {} + SetTremorTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "tremor totem", TREMOR_TOTEM, TOTEM_BAR_SLOT_EARTH) {} }; class SetEarthbindTotemAction : public SetTotemAction { public: - SetEarthbindTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "earthbind totem", EARTHBIND_TOTEM, TOTEM_BAR_SLOT_EARTH) {} + SetEarthbindTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "earthbind totem", EARTHBIND_TOTEM, TOTEM_BAR_SLOT_EARTH) {} }; class SetSearingTotemAction : public SetTotemAction { public: - SetSearingTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "searing totem", SEARING_TOTEM, TOTEM_BAR_SLOT_FIRE) {} + SetSearingTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "searing totem", SEARING_TOTEM, TOTEM_BAR_SLOT_FIRE) {} }; class SetMagmaTotemAction : public SetTotemAction { public: - SetMagmaTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "magma totem", MAGMA_TOTEM, TOTEM_BAR_SLOT_FIRE) {} + SetMagmaTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "magma totem", MAGMA_TOTEM, TOTEM_BAR_SLOT_FIRE) {} }; class SetFlametongueTotemAction : public SetTotemAction { public: - SetFlametongueTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "flametongue totem", FLAMETONGUE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} + SetFlametongueTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "flametongue totem", FLAMETONGUE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} }; class SetTotemOfWrathAction : public SetTotemAction { public: - SetTotemOfWrathAction(PlayerbotAI* ai) - : SetTotemAction(ai, "totem of wrath", TOTEM_OF_WRATH, TOTEM_BAR_SLOT_FIRE) {} + SetTotemOfWrathAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "totem of wrath", TOTEM_OF_WRATH, TOTEM_BAR_SLOT_FIRE) {} }; class SetFrostResistanceTotemAction : public SetTotemAction { public: - SetFrostResistanceTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "frost resistance totem", FROST_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} + SetFrostResistanceTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "frost resistance totem", FROST_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} }; class SetHealingStreamTotemAction : public SetTotemAction { public: - SetHealingStreamTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "healing stream totem", HEALING_STREAM_TOTEM, TOTEM_BAR_SLOT_WATER) {} + SetHealingStreamTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "healing stream totem", HEALING_STREAM_TOTEM, TOTEM_BAR_SLOT_WATER) {} }; class SetManaSpringTotemAction : public SetTotemAction { public: - SetManaSpringTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "mana spring totem", MANA_SPRING_TOTEM, TOTEM_BAR_SLOT_WATER) {} + SetManaSpringTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "mana spring totem", MANA_SPRING_TOTEM, TOTEM_BAR_SLOT_WATER) {} }; class SetCleansingTotemAction : public SetTotemAction { public: - SetCleansingTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "cleansing totem", CLEANSING_TOTEM, TOTEM_BAR_SLOT_WATER) {} + SetCleansingTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "cleansing totem", CLEANSING_TOTEM, TOTEM_BAR_SLOT_WATER) {} }; class SetFireResistanceTotemAction : public SetTotemAction { public: - SetFireResistanceTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "fire resistance totem", FIRE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_WATER) {} + SetFireResistanceTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "fire resistance totem", FIRE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_WATER) {} }; class SetWrathOfAirTotemAction : public SetTotemAction { public: - SetWrathOfAirTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "wrath of air totem", WRATH_OF_AIR_TOTEM, TOTEM_BAR_SLOT_AIR) {} + SetWrathOfAirTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "wrath of air totem", WRATH_OF_AIR_TOTEM, TOTEM_BAR_SLOT_AIR) {} }; class SetWindfuryTotemAction : public SetTotemAction { public: - SetWindfuryTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "windfury totem", WINDFURY_TOTEM, TOTEM_BAR_SLOT_AIR) {} + SetWindfuryTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "windfury totem", WINDFURY_TOTEM, TOTEM_BAR_SLOT_AIR) {} }; class SetNatureResistanceTotemAction : public SetTotemAction { public: - SetNatureResistanceTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "nature resistance totem", NATURE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_AIR) {} + SetNatureResistanceTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "nature resistance totem", NATURE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_AIR) {} }; class SetGroundingTotemAction : public SetTotemAction { public: - SetGroundingTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "grounding totem", GROUNDING_TOTEM, TOTEM_BAR_SLOT_AIR) {} + SetGroundingTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "grounding totem", GROUNDING_TOTEM, TOTEM_BAR_SLOT_AIR) {} }; #endif diff --git a/src/Ai/Class/Shaman/ShamanAiObjectContext.cpp b/src/Ai/Class/Shaman/ShamanAiObjectContext.cpp index 08ee3638d97..73c41c4b6c7 100644 --- a/src/Ai/Class/Shaman/ShamanAiObjectContext.cpp +++ b/src/Ai/Class/Shaman/ShamanAiObjectContext.cpp @@ -158,10 +158,6 @@ class ShamanATriggerFactoryInternal : public NamedObjectContext creators["bloodlust"] = &ShamanATriggerFactoryInternal::bloodlust; creators["elemental mastery"] = &ShamanATriggerFactoryInternal::elemental_mastery; creators["wind shear on enemy healer"] = &ShamanATriggerFactoryInternal::wind_shear_on_enemy_healer; - creators["cure poison"] = &ShamanATriggerFactoryInternal::cure_poison; - creators["party member cure poison"] = &ShamanATriggerFactoryInternal::party_member_cure_poison; - creators["cure disease"] = &ShamanATriggerFactoryInternal::cure_disease; - creators["party member cure disease"] = &ShamanATriggerFactoryInternal::party_member_cure_disease; creators["earth shield on main tank"] = &ShamanATriggerFactoryInternal::earth_shield_on_main_tank; creators["maelstrom weapon 3"] = &ShamanATriggerFactoryInternal::maelstrom_weapon_3; creators["maelstrom weapon 4"] = &ShamanATriggerFactoryInternal::maelstrom_weapon_4; @@ -225,42 +221,38 @@ class ShamanATriggerFactoryInternal : public NamedObjectContext static Trigger* shock(PlayerbotAI* botAI) { return new ShockTrigger(botAI); } static Trigger* frost_shock_snare(PlayerbotAI* botAI) { return new FrostShockSnareTrigger(botAI); } static Trigger* wind_shear_on_enemy_healer(PlayerbotAI* botAI) { return new WindShearInterruptEnemyHealerSpellTrigger(botAI); } - static Trigger* cure_poison(PlayerbotAI* botAI) { return new CurePoisonTrigger(botAI); } - static Trigger* party_member_cure_poison(PlayerbotAI* botAI) { return new PartyMemberCurePoisonTrigger(botAI); } - static Trigger* cure_disease(PlayerbotAI* botAI) { return new CureDiseaseTrigger(botAI); } - static Trigger* party_member_cure_disease(PlayerbotAI* botAI) { return new PartyMemberCureDiseaseTrigger(botAI); } - static Trigger* earth_shield_on_main_tank(PlayerbotAI* ai) { return new EarthShieldOnMainTankTrigger(ai); } - static Trigger* flame_shock(PlayerbotAI* ai) { return new FlameShockTrigger(ai); } + static Trigger* earth_shield_on_main_tank(PlayerbotAI* botAI) { return new EarthShieldOnMainTankTrigger(botAI); } + static Trigger* flame_shock(PlayerbotAI* botAI) { return new FlameShockTrigger(botAI); } static Trigger* fire_elemental_totem(PlayerbotAI* botAI) { return new FireElementalTotemTrigger(botAI); } static Trigger* earth_shock_execute(PlayerbotAI* botAI) { return new EarthShockExecuteTrigger(botAI); } - static Trigger* spirit_walk_ready(PlayerbotAI* ai) { return new SpiritWalkTrigger(ai); } - static Trigger* chain_lightning_no_cd(PlayerbotAI* ai) { return new ChainLightningNoCdTrigger(ai); } - static Trigger* call_of_the_elements_and_enemy_within_melee(PlayerbotAI* ai) { return new CallOfTheElementsAndEnemyWithinMeleeTrigger(ai); } - static Trigger* maelstrom_weapon_5_and_medium_aoe(PlayerbotAI* ai) { return new MaelstromWeapon5AndMediumAoeTrigger(ai); } - static Trigger* maelstrom_weapon_4_and_medium_aoe(PlayerbotAI* ai) { return new MaelstromWeapon4AndMediumAoeTrigger(ai); } - static Trigger* call_of_the_elements(PlayerbotAI* ai) { return new CallOfTheElementsTrigger(ai); } - static Trigger* totemic_recall(PlayerbotAI* ai) { return new TotemicRecallTrigger(ai); } - static Trigger* no_earth_totem(PlayerbotAI* ai) { return new NoEarthTotemTrigger(ai); } - static Trigger* no_fire_totem(PlayerbotAI* ai) { return new NoFireTotemTrigger(ai); } - static Trigger* no_water_totem(PlayerbotAI* ai) { return new NoWaterTotemTrigger(ai); } - static Trigger* no_air_totem(PlayerbotAI* ai) { return new NoAirTotemTrigger(ai); } - static Trigger* set_strength_of_earth_totem(PlayerbotAI* ai) { return new SetStrengthOfEarthTotemTrigger(ai); } - static Trigger* set_stoneskin_totem(PlayerbotAI* ai) { return new SetStoneskinTotemTrigger(ai); } - static Trigger* set_tremor_totem(PlayerbotAI* ai) { return new SetTremorTotemTrigger(ai); } - static Trigger* set_earthbind_totem(PlayerbotAI* ai) { return new SetEarthbindTotemTrigger(ai); } - static Trigger* set_searing_totem(PlayerbotAI* ai) { return new SetSearingTotemTrigger(ai); } - static Trigger* set_magma_totem(PlayerbotAI* ai) { return new SetMagmaTotemTrigger(ai); } - static Trigger* set_flametongue_totem(PlayerbotAI* ai) { return new SetFlametongueTotemTrigger(ai); } - static Trigger* set_totem_of_wrath(PlayerbotAI* ai) { return new SetTotemOfWrathTrigger(ai); } - static Trigger* set_frost_resistance_totem(PlayerbotAI* ai) { return new SetFrostResistanceTotemTrigger(ai); } - static Trigger* set_healing_stream_totem(PlayerbotAI* ai) { return new SetHealingStreamTotemTrigger(ai); } - static Trigger* set_mana_spring_totem(PlayerbotAI* ai) { return new SetManaSpringTotemTrigger(ai); } - static Trigger* set_cleansing_totem(PlayerbotAI* ai) { return new SetCleansingTotemTrigger(ai); } - static Trigger* set_fire_resistance_totem(PlayerbotAI* ai) { return new SetFireResistanceTotemTrigger(ai); } - static Trigger* set_wrath_of_air_totem(PlayerbotAI* ai) { return new SetWrathOfAirTotemTrigger(ai); } - static Trigger* set_windfury_totem(PlayerbotAI* ai) { return new SetWindfuryTotemTrigger(ai); } - static Trigger* set_nature_resistance_totem(PlayerbotAI* ai) { return new SetNatureResistanceTotemTrigger(ai); } - static Trigger* set_grounding_totem(PlayerbotAI* ai) { return new SetGroundingTotemTrigger(ai); } + static Trigger* spirit_walk_ready(PlayerbotAI* botAI) { return new SpiritWalkTrigger(botAI); } + static Trigger* chain_lightning_no_cd(PlayerbotAI* botAI) { return new ChainLightningNoCdTrigger(botAI); } + static Trigger* call_of_the_elements_and_enemy_within_melee(PlayerbotAI* botAI) { return new CallOfTheElementsAndEnemyWithinMeleeTrigger(botAI); } + static Trigger* maelstrom_weapon_5_and_medium_aoe(PlayerbotAI* botAI) { return new MaelstromWeapon5AndMediumAoeTrigger(botAI); } + static Trigger* maelstrom_weapon_4_and_medium_aoe(PlayerbotAI* botAI) { return new MaelstromWeapon4AndMediumAoeTrigger(botAI); } + static Trigger* call_of_the_elements(PlayerbotAI* botAI) { return new CallOfTheElementsTrigger(botAI); } + static Trigger* totemic_recall(PlayerbotAI* botAI) { return new TotemicRecallTrigger(botAI); } + static Trigger* no_earth_totem(PlayerbotAI* botAI) { return new NoEarthTotemTrigger(botAI); } + static Trigger* no_fire_totem(PlayerbotAI* botAI) { return new NoFireTotemTrigger(botAI); } + static Trigger* no_water_totem(PlayerbotAI* botAI) { return new NoWaterTotemTrigger(botAI); } + static Trigger* no_air_totem(PlayerbotAI* botAI) { return new NoAirTotemTrigger(botAI); } + static Trigger* set_strength_of_earth_totem(PlayerbotAI* botAI) { return new SetStrengthOfEarthTotemTrigger(botAI); } + static Trigger* set_stoneskin_totem(PlayerbotAI* botAI) { return new SetStoneskinTotemTrigger(botAI); } + static Trigger* set_tremor_totem(PlayerbotAI* botAI) { return new SetTremorTotemTrigger(botAI); } + static Trigger* set_earthbind_totem(PlayerbotAI* botAI) { return new SetEarthbindTotemTrigger(botAI); } + static Trigger* set_searing_totem(PlayerbotAI* botAI) { return new SetSearingTotemTrigger(botAI); } + static Trigger* set_magma_totem(PlayerbotAI* botAI) { return new SetMagmaTotemTrigger(botAI); } + static Trigger* set_flametongue_totem(PlayerbotAI* botAI) { return new SetFlametongueTotemTrigger(botAI); } + static Trigger* set_totem_of_wrath(PlayerbotAI* botAI) { return new SetTotemOfWrathTrigger(botAI); } + static Trigger* set_frost_resistance_totem(PlayerbotAI* botAI) { return new SetFrostResistanceTotemTrigger(botAI); } + static Trigger* set_healing_stream_totem(PlayerbotAI* botAI) { return new SetHealingStreamTotemTrigger(botAI); } + static Trigger* set_mana_spring_totem(PlayerbotAI* botAI) { return new SetManaSpringTotemTrigger(botAI); } + static Trigger* set_cleansing_totem(PlayerbotAI* botAI) { return new SetCleansingTotemTrigger(botAI); } + static Trigger* set_fire_resistance_totem(PlayerbotAI* botAI) { return new SetFireResistanceTotemTrigger(botAI); } + static Trigger* set_wrath_of_air_totem(PlayerbotAI* botAI) { return new SetWrathOfAirTotemTrigger(botAI); } + static Trigger* set_windfury_totem(PlayerbotAI* botAI) { return new SetWindfuryTotemTrigger(botAI); } + static Trigger* set_nature_resistance_totem(PlayerbotAI* botAI) { return new SetNatureResistanceTotemTrigger(botAI); } + static Trigger* set_grounding_totem(PlayerbotAI* botAI) { return new SetGroundingTotemTrigger(botAI); } }; class ShamanAiObjectContextInternal : public NamedObjectContext @@ -272,11 +264,12 @@ class ShamanAiObjectContextInternal : public NamedObjectContext creators["lightning shield"] = &ShamanAiObjectContextInternal::lightning_shield; creators["wind shear"] = &ShamanAiObjectContextInternal::wind_shear; creators["wind shear on enemy healer"] = &ShamanAiObjectContextInternal::wind_shear_on_enemy_healer; - creators["rockbiter weapon"] = &ShamanAiObjectContextInternal::rockbiter_weapon; - creators["flametongue weapon"] = &ShamanAiObjectContextInternal::flametongue_weapon; - creators["frostbrand weapon"] = &ShamanAiObjectContextInternal::frostbrand_weapon; - creators["windfury weapon"] = &ShamanAiObjectContextInternal::windfury_weapon; - creators["earthliving weapon"] = &ShamanAiObjectContextInternal::earthliving_weapon; + creators["rockbiter weapon main hand"] = &ShamanAiObjectContextInternal::rockbiter_weapon_main_hand; + creators["flametongue weapon main hand"] = &ShamanAiObjectContextInternal::flametongue_weapon_main_hand; + creators["flametongue weapon off hand"] = &ShamanAiObjectContextInternal::flametongue_weapon_off_hand; + // creators["frostbrand weapon off hand"] = &ShamanAiObjectContextInternal::frostbrand_weapon_off_hand; + creators["windfury weapon main hand"] = &ShamanAiObjectContextInternal::windfury_weapon_main_hand; + creators["earthliving weapon main hand"] = &ShamanAiObjectContextInternal::earthliving_weapon_main_hand; creators["purge"] = &ShamanAiObjectContextInternal::purge; creators["healing wave"] = &ShamanAiObjectContextInternal::healing_wave; creators["lesser healing wave"] = &ShamanAiObjectContextInternal::lesser_healing_wave; @@ -308,10 +301,9 @@ class ShamanAiObjectContextInternal : public NamedObjectContext creators["heroism"] = &ShamanAiObjectContextInternal::heroism; creators["bloodlust"] = &ShamanAiObjectContextInternal::bloodlust; creators["elemental mastery"] = &ShamanAiObjectContextInternal::elemental_mastery; - creators["cure disease"] = &ShamanAiObjectContextInternal::cure_disease; - creators["cure disease on party"] = &ShamanAiObjectContextInternal::cure_disease_on_party; - creators["cure poison"] = &ShamanAiObjectContextInternal::cure_poison; - creators["cure poison on party"] = &ShamanAiObjectContextInternal::cure_poison_on_party; + creators["cure toxins"] = &ShamanAiObjectContextInternal::cure_toxins; + creators["cure toxins poison on party"] = &ShamanAiObjectContextInternal::cure_toxins_poison_on_party; + creators["cure toxins disease on party"] = &ShamanAiObjectContextInternal::cure_toxins_disease_on_party; creators["lava burst"] = &ShamanAiObjectContextInternal::lava_burst; creators["earth shield on main tank"] = &ShamanAiObjectContextInternal::earth_shield_on_main_tank; creators["shamanistic rage"] = &ShamanAiObjectContextInternal::shamanistic_rage; @@ -368,10 +360,10 @@ class ShamanAiObjectContextInternal : public NamedObjectContext static Action* frost_shock(PlayerbotAI* botAI) { return new CastFrostShockAction(botAI); } static Action* earth_shock(PlayerbotAI* botAI) { return new CastEarthShockAction(botAI); } static Action* flame_shock(PlayerbotAI* botAI) { return new CastFlameShockAction(botAI); } + static Action* cleanse_spirit(PlayerbotAI* botAI) { return new CastCleanseSpiritAction(botAI); } static Action* cleanse_spirit_poison_on_party(PlayerbotAI* botAI) { return new CastCleanseSpiritPoisonOnPartyAction(botAI); } static Action* cleanse_spirit_disease_on_party(PlayerbotAI* botAI) { return new CastCleanseSpiritDiseaseOnPartyAction(botAI); } static Action* cleanse_spirit_curse_on_party(PlayerbotAI* botAI) { return new CastCleanseSpiritCurseOnPartyAction(botAI); } - static Action* cleanse_spirit(PlayerbotAI* botAI) { return new CastCleanseSpiritAction(botAI); } static Action* water_walking(PlayerbotAI* botAI) { return new CastWaterWalkingAction(botAI); } static Action* water_breathing(PlayerbotAI* botAI) { return new CastWaterBreathingAction(botAI); } static Action* water_walking_on_party(PlayerbotAI* botAI) { return new CastWaterWalkingOnPartyAction(botAI); } @@ -380,11 +372,12 @@ class ShamanAiObjectContextInternal : public NamedObjectContext static Action* lightning_shield(PlayerbotAI* botAI) { return new CastLightningShieldAction(botAI); } static Action* fire_nova(PlayerbotAI* botAI) { return new CastFireNovaAction(botAI); } static Action* wind_shear(PlayerbotAI* botAI) { return new CastWindShearAction(botAI); } - static Action* rockbiter_weapon(PlayerbotAI* botAI) { return new CastRockbiterWeaponAction(botAI); } - static Action* flametongue_weapon(PlayerbotAI* botAI) { return new CastFlametongueWeaponAction(botAI); } - static Action* frostbrand_weapon(PlayerbotAI* botAI) { return new CastFrostbrandWeaponAction(botAI); } - static Action* windfury_weapon(PlayerbotAI* botAI) { return new CastWindfuryWeaponAction(botAI); } - static Action* earthliving_weapon(PlayerbotAI* botAI) { return new CastEarthlivingWeaponAction(botAI); } + static Action* rockbiter_weapon_main_hand(PlayerbotAI* botAI) { return new CastRockbiterWeaponMainHandAction(botAI); } + static Action* flametongue_weapon_main_hand(PlayerbotAI* botAI) { return new CastFlametongueWeaponMainHandAction(botAI); } + static Action* flametongue_weapon_off_hand(PlayerbotAI* botAI) { return new CastFlametongueWeaponOffHandAction(botAI); } + // static Action* frostbrand_weapon_off_hand(PlayerbotAI* botAI) { return new CastFrostbrandWeaponOffHandAction(botAI); } + static Action* earthliving_weapon_main_hand(PlayerbotAI* botAI) { return new CastEarthlivingWeaponMainHandAction(botAI); } + static Action* windfury_weapon_main_hand(PlayerbotAI* botAI) { return new CastWindfuryWeaponMainHandAction(botAI); } static Action* purge(PlayerbotAI* botAI) { return new CastPurgeAction(botAI); } static Action* healing_wave(PlayerbotAI* botAI) { return new CastHealingWaveAction(botAI); } static Action* lesser_healing_wave(PlayerbotAI* botAI) { return new CastLesserHealingWaveAction(botAI); } @@ -399,54 +392,53 @@ class ShamanAiObjectContextInternal : public NamedObjectContext static Action* lava_lash(PlayerbotAI* botAI) { return new CastLavaLashAction(botAI); } static Action* ancestral_spirit(PlayerbotAI* botAI) { return new CastAncestralSpiritAction(botAI); } static Action* wind_shear_on_enemy_healer(PlayerbotAI* botAI) { return new CastWindShearOnEnemyHealerAction(botAI); } - static Action* cure_poison(PlayerbotAI* botAI) { return new CastCurePoisonActionSham(botAI); } - static Action* cure_poison_on_party(PlayerbotAI* botAI) { return new CastCurePoisonOnPartyActionSham(botAI); } - static Action* cure_disease(PlayerbotAI* botAI) { return new CastCureDiseaseActionSham(botAI); } - static Action* cure_disease_on_party(PlayerbotAI* botAI) { return new CastCureDiseaseOnPartyActionSham(botAI); } - static Action* lava_burst(PlayerbotAI* ai) { return new CastLavaBurstAction(ai); } - static Action* earth_shield_on_main_tank(PlayerbotAI* ai) { return new CastEarthShieldOnMainTankAction(ai); } - static Action* shamanistic_rage(PlayerbotAI* ai) { return new CastShamanisticRageAction(ai); } - static Action* feral_spirit(PlayerbotAI* ai) { return new CastFeralSpiritAction(ai); } - static Action* spirit_walk(PlayerbotAI* ai) { return new CastSpiritWalkAction(ai); } - static Action* call_of_the_elements(PlayerbotAI* ai) { return new CastCallOfTheElementsAction(ai); } - static Action* totemic_recall(PlayerbotAI* ai) { return new CastTotemicRecallAction(ai); } - static Action* strength_of_earth_totem(PlayerbotAI* ai) { return new CastStrengthOfEarthTotemAction(ai); } - static Action* stoneskin_totem(PlayerbotAI* ai) { return new CastStoneskinTotemAction(ai); } - static Action* tremor_totem(PlayerbotAI* ai) { return new CastTremorTotemAction(ai); } - static Action* earthbind_totem(PlayerbotAI* ai) { return new CastEarthbindTotemAction(ai); } - static Action* stoneclaw_totem(PlayerbotAI* ai) { return new CastStoneclawTotemAction(ai); } - static Action* searing_totem(PlayerbotAI* ai) { return new CastSearingTotemAction(ai); } - static Action* magma_totem(PlayerbotAI* ai) { return new CastMagmaTotemAction(ai); } - static Action* flametongue_totem(PlayerbotAI* ai) { return new CastFlametongueTotemAction(ai); } - static Action* totem_of_wrath(PlayerbotAI* ai) { return new CastTotemOfWrathAction(ai); } - static Action* frost_resistance_totem(PlayerbotAI* ai) { return new CastFrostResistanceTotemAction(ai); } - static Action* fire_elemental_totem(PlayerbotAI* ai) { return new CastFireElementalTotemAction(ai); } - static Action* fire_elemental_totem_melee(PlayerbotAI* ai) { return new CastFireElementalTotemMeleeAction(ai); } - static Action* healing_stream_totem(PlayerbotAI* ai) { return new CastHealingStreamTotemAction(ai); } - static Action* mana_spring_totem(PlayerbotAI* ai) { return new CastManaSpringTotemAction(ai); } - static Action* cleansing_totem(PlayerbotAI* ai) { return new CastCleansingTotemAction(ai); } - static Action* mana_tide_totem(PlayerbotAI* ai) { return new CastManaTideTotemAction(ai); } - static Action* fire_resistance_totem(PlayerbotAI* ai) { return new CastFireResistanceTotemAction(ai); } - static Action* wrath_of_air_totem(PlayerbotAI* ai) { return new CastWrathOfAirTotemAction(ai); } - static Action* windfury_totem(PlayerbotAI* ai) { return new CastWindfuryTotemAction(ai); } - static Action* nature_resistance_totem(PlayerbotAI* ai) { return new CastNatureResistanceTotemAction(ai); } - static Action* set_strength_of_earth_totem(PlayerbotAI* ai) { return new SetStrengthOfEarthTotemAction(ai); } - static Action* set_stoneskin_totem(PlayerbotAI* ai) { return new SetStoneskinTotemAction(ai); } - static Action* set_tremor_totem(PlayerbotAI* ai) { return new SetTremorTotemAction(ai); } - static Action* set_earthbind_totem(PlayerbotAI* ai) { return new SetEarthbindTotemAction(ai); } - static Action* set_searing_totem(PlayerbotAI* ai) { return new SetSearingTotemAction(ai); } - static Action* set_magma_totem(PlayerbotAI* ai) { return new SetMagmaTotemAction(ai); } - static Action* set_flametongue_totem(PlayerbotAI* ai) { return new SetFlametongueTotemAction(ai); } - static Action* set_totem_of_wrath(PlayerbotAI* ai) { return new SetTotemOfWrathAction(ai); } - static Action* set_frost_resistance_totem(PlayerbotAI* ai) { return new SetFrostResistanceTotemAction(ai); } - static Action* set_healing_stream_totem(PlayerbotAI* ai) { return new SetHealingStreamTotemAction(ai); } - static Action* set_mana_spring_totem(PlayerbotAI* ai) { return new SetManaSpringTotemAction(ai); } - static Action* set_cleansing_totem(PlayerbotAI* ai) { return new SetCleansingTotemAction(ai); } - static Action* set_fire_resistance_totem(PlayerbotAI* ai) { return new SetFireResistanceTotemAction(ai); } - static Action* set_wrath_of_air_totem(PlayerbotAI* ai) { return new SetWrathOfAirTotemAction(ai); } - static Action* set_windfury_totem(PlayerbotAI* ai) { return new SetWindfuryTotemAction(ai); } - static Action* set_nature_resistance_totem(PlayerbotAI* ai) { return new SetNatureResistanceTotemAction(ai); } - static Action* set_grounding_totem(PlayerbotAI* ai) { return new SetGroundingTotemAction(ai); } + static Action* cure_toxins(PlayerbotAI* botAI) { return new CastCureToxinsActionSham(botAI); } + static Action* cure_toxins_poison_on_party(PlayerbotAI* botAI) { return new CastCureToxinsPoisonOnPartyActionSham(botAI); } + static Action* cure_toxins_disease_on_party(PlayerbotAI* botAI) { return new CastCureToxinsDiseaseOnPartyActionSham(botAI); } + static Action* lava_burst(PlayerbotAI* botAI) { return new CastLavaBurstAction(botAI); } + static Action* earth_shield_on_main_tank(PlayerbotAI* botAI) { return new CastEarthShieldOnMainTankAction(botAI); } + static Action* shamanistic_rage(PlayerbotAI* botAI) { return new CastShamanisticRageAction(botAI); } + static Action* feral_spirit(PlayerbotAI* botAI) { return new CastFeralSpiritAction(botAI); } + static Action* spirit_walk(PlayerbotAI* botAI) { return new CastSpiritWalkAction(botAI); } + static Action* call_of_the_elements(PlayerbotAI* botAI) { return new CastCallOfTheElementsAction(botAI); } + static Action* totemic_recall(PlayerbotAI* botAI) { return new CastTotemicRecallAction(botAI); } + static Action* strength_of_earth_totem(PlayerbotAI* botAI) { return new CastStrengthOfEarthTotemAction(botAI); } + static Action* stoneskin_totem(PlayerbotAI* botAI) { return new CastStoneskinTotemAction(botAI); } + static Action* tremor_totem(PlayerbotAI* botAI) { return new CastTremorTotemAction(botAI); } + static Action* earthbind_totem(PlayerbotAI* botAI) { return new CastEarthbindTotemAction(botAI); } + static Action* stoneclaw_totem(PlayerbotAI* botAI) { return new CastStoneclawTotemAction(botAI); } + static Action* searing_totem(PlayerbotAI* botAI) { return new CastSearingTotemAction(botAI); } + static Action* magma_totem(PlayerbotAI* botAI) { return new CastMagmaTotemAction(botAI); } + static Action* flametongue_totem(PlayerbotAI* botAI) { return new CastFlametongueTotemAction(botAI); } + static Action* totem_of_wrath(PlayerbotAI* botAI) { return new CastTotemOfWrathAction(botAI); } + static Action* frost_resistance_totem(PlayerbotAI* botAI) { return new CastFrostResistanceTotemAction(botAI); } + static Action* fire_elemental_totem(PlayerbotAI* botAI) { return new CastFireElementalTotemAction(botAI); } + static Action* fire_elemental_totem_melee(PlayerbotAI* botAI) { return new CastFireElementalTotemMeleeAction(botAI); } + static Action* healing_stream_totem(PlayerbotAI* botAI) { return new CastHealingStreamTotemAction(botAI); } + static Action* mana_spring_totem(PlayerbotAI* botAI) { return new CastManaSpringTotemAction(botAI); } + static Action* cleansing_totem(PlayerbotAI* botAI) { return new CastCleansingTotemAction(botAI); } + static Action* mana_tide_totem(PlayerbotAI* botAI) { return new CastManaTideTotemAction(botAI); } + static Action* fire_resistance_totem(PlayerbotAI* botAI) { return new CastFireResistanceTotemAction(botAI); } + static Action* wrath_of_air_totem(PlayerbotAI* botAI) { return new CastWrathOfAirTotemAction(botAI); } + static Action* windfury_totem(PlayerbotAI* botAI) { return new CastWindfuryTotemAction(botAI); } + static Action* nature_resistance_totem(PlayerbotAI* botAI) { return new CastNatureResistanceTotemAction(botAI); } + static Action* set_strength_of_earth_totem(PlayerbotAI* botAI) { return new SetStrengthOfEarthTotemAction(botAI); } + static Action* set_stoneskin_totem(PlayerbotAI* botAI) { return new SetStoneskinTotemAction(botAI); } + static Action* set_tremor_totem(PlayerbotAI* botAI) { return new SetTremorTotemAction(botAI); } + static Action* set_earthbind_totem(PlayerbotAI* botAI) { return new SetEarthbindTotemAction(botAI); } + static Action* set_searing_totem(PlayerbotAI* botAI) { return new SetSearingTotemAction(botAI); } + static Action* set_magma_totem(PlayerbotAI* botAI) { return new SetMagmaTotemAction(botAI); } + static Action* set_flametongue_totem(PlayerbotAI* botAI) { return new SetFlametongueTotemAction(botAI); } + static Action* set_totem_of_wrath(PlayerbotAI* botAI) { return new SetTotemOfWrathAction(botAI); } + static Action* set_frost_resistance_totem(PlayerbotAI* botAI) { return new SetFrostResistanceTotemAction(botAI); } + static Action* set_healing_stream_totem(PlayerbotAI* botAI) { return new SetHealingStreamTotemAction(botAI); } + static Action* set_mana_spring_totem(PlayerbotAI* botAI) { return new SetManaSpringTotemAction(botAI); } + static Action* set_cleansing_totem(PlayerbotAI* botAI) { return new SetCleansingTotemAction(botAI); } + static Action* set_fire_resistance_totem(PlayerbotAI* botAI) { return new SetFireResistanceTotemAction(botAI); } + static Action* set_wrath_of_air_totem(PlayerbotAI* botAI) { return new SetWrathOfAirTotemAction(botAI); } + static Action* set_windfury_totem(PlayerbotAI* botAI) { return new SetWindfuryTotemAction(botAI); } + static Action* set_nature_resistance_totem(PlayerbotAI* botAI) { return new SetNatureResistanceTotemAction(botAI); } + static Action* set_grounding_totem(PlayerbotAI* botAI) { return new SetGroundingTotemAction(botAI); } }; SharedNamedObjectContextList ShamanAiObjectContext::sharedStrategyContexts; diff --git a/src/Ai/Class/Shaman/Strategy/ElementalShamanStrategy.cpp b/src/Ai/Class/Shaman/Strategy/ElementalShamanStrategy.cpp index c1295687cdd..5ef0d3f55c5 100644 --- a/src/Ai/Class/Shaman/Strategy/ElementalShamanStrategy.cpp +++ b/src/Ai/Class/Shaman/Strategy/ElementalShamanStrategy.cpp @@ -4,42 +4,11 @@ */ #include "ElementalShamanStrategy.h" - #include "Playerbots.h" -// ===== Action Node Factory ===== -class ElementalShamanStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - ElementalShamanStrategyActionNodeFactory() - { - creators["flame shock"] = &flame_shock; - creators["earth shock"] = &earth_shock; - creators["lava burst"] = &lava_burst; - creators["lightning bolt"] = &lightning_bolt; - creators["call of the elements"] = &call_of_the_elements; - creators["elemental mastery"] = &elemental_mastery; - creators["stoneclaw totem"] = &stoneclaw_totem; - creators["water shield"] = &water_shield; - creators["thunderstorm"] = &thunderstorm; - } - -private: - static ActionNode* flame_shock(PlayerbotAI*) { return new ActionNode("flame shock", {}, {}, {}); } - static ActionNode* earth_shock(PlayerbotAI*) { return new ActionNode("earth shock", {}, {}, {}); } - static ActionNode* lava_burst(PlayerbotAI*) { return new ActionNode("lava burst", {}, {}, {}); } - static ActionNode* lightning_bolt(PlayerbotAI*) { return new ActionNode("lightning bolt", {}, {}, {}); } - static ActionNode* call_of_the_elements(PlayerbotAI*) { return new ActionNode("call of the elements", {}, {}, {}); } - static ActionNode* elemental_mastery(PlayerbotAI*) { return new ActionNode("elemental mastery", {}, {}, {}); } - static ActionNode* stoneclaw_totem(PlayerbotAI*) { return new ActionNode("stoneclaw totem", {}, {}, {}); } - static ActionNode* water_shield(PlayerbotAI*) { return new ActionNode("water shield", {}, {}, {}); } - static ActionNode* thunderstorm(PlayerbotAI*) { return new ActionNode("thunderstorm", {}, {}, {}); } -}; - -// ===== Single Target Strategy ===== ElementalShamanStrategy::ElementalShamanStrategy(PlayerbotAI* botAI) : GenericShamanStrategy(botAI) { - actionNodeFactories.Add(new ElementalShamanStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } // ===== Default Actions ===== diff --git a/src/Ai/Class/Shaman/Strategy/EnhancementShamanStrategy.cpp b/src/Ai/Class/Shaman/Strategy/EnhancementShamanStrategy.cpp index 4b7fb7159e0..0eb548993d5 100644 --- a/src/Ai/Class/Shaman/Strategy/EnhancementShamanStrategy.cpp +++ b/src/Ai/Class/Shaman/Strategy/EnhancementShamanStrategy.cpp @@ -4,7 +4,6 @@ */ #include "EnhancementShamanStrategy.h" - #include "Playerbots.h" // ===== Action Node Factory ===== @@ -13,19 +12,10 @@ class EnhancementShamanStrategyActionNodeFactory : public NamedObjectFactory& triggers) triggers.push_back(new TriggerNode("wind shear", { NextAction("wind shear", 23.0f), })); triggers.push_back(new TriggerNode("wind shear on enemy healer", { NextAction("wind shear on enemy healer", 23.0f), })); triggers.push_back(new TriggerNode("purge", { NextAction("purge", ACTION_DISPEL), })); - triggers.push_back(new TriggerNode("medium mana", { NextAction("mana potion", ACTION_DISPEL), })); triggers.push_back(new TriggerNode("new pet", { NextAction("set pet stance", 65.0f), })); } void ShamanCureStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode("cure poison", { NextAction("cure poison", 21.0f), })); - triggers.push_back(new TriggerNode("party member cure poison", { NextAction("cure poison on party", 21.0f), })); triggers.push_back(new TriggerNode("cleanse spirit poison", { NextAction("cleanse spirit", 24.0f), })); triggers.push_back(new TriggerNode("party member cleanse spirit poison", { NextAction("cleanse spirit poison on party", 23.0f), })); - triggers.push_back(new TriggerNode("cure disease", { NextAction("cure disease", 31.0f), })); - triggers.push_back(new TriggerNode("party member cure disease", { NextAction("cure disease on party", 30.0f), })); triggers.push_back(new TriggerNode("cleanse spirit disease", { NextAction("cleanse spirit", 24.0f), })); triggers.push_back(new TriggerNode("party member cleanse spirit disease", { NextAction("cleanse spirit disease on party", 23.0f), })); triggers.push_back(new TriggerNode("cleanse spirit curse", { NextAction("cleanse spirit", 24.0f), })); @@ -133,11 +138,11 @@ void ShamanBoostStrategy::InitTriggers(std::vector& triggers) Player* bot = botAI->GetBot(); int tab = AiFactory::GetPlayerSpecTab(bot); - if (tab == 0) // Elemental + if (tab == SHAMAN_TAB_ELEMENTAL) { triggers.push_back(new TriggerNode("fire elemental totem", { NextAction("fire elemental totem", 23.0f), })); } - else if (tab == 1) // Enhancement + else if (tab == SHAMAN_TAB_ENHANCEMENT) { triggers.push_back(new TriggerNode("fire elemental totem", { NextAction("fire elemental totem melee", 24.0f), })); } @@ -149,23 +154,19 @@ void ShamanAoeStrategy::InitTriggers(std::vector& triggers) Player* bot = botAI->GetBot(); int tab = AiFactory::GetPlayerSpecTab(bot); - if (tab == 0) // Elemental + if (tab == SHAMAN_TAB_ELEMENTAL) { triggers.push_back(new TriggerNode("medium aoe",{ NextAction("fire nova", 23.0f), })); triggers.push_back(new TriggerNode("chain lightning no cd", { NextAction("chain lightning", 5.6f), })); } - else if (tab == 1) // Enhancement + else if (tab == SHAMAN_TAB_ENHANCEMENT) { - triggers.push_back(new TriggerNode("medium aoe",{ - NextAction("magma totem", 24.0f), - NextAction("fire nova", 23.0f), })); + triggers.push_back(new TriggerNode("medium aoe",{ NextAction("magma totem", 24.0f), + NextAction("fire nova", 23.0f), })); triggers.push_back(new TriggerNode("maelstrom weapon 5 and medium aoe", { NextAction("chain lightning", 22.0f), })); triggers.push_back(new TriggerNode("maelstrom weapon 4 and medium aoe", { NextAction("chain lightning", 21.0f), })); triggers.push_back(new TriggerNode("enemy within melee", { NextAction("fire nova", 5.1f), })); } - else if (tab == 2) // Restoration - { - // Handled by "Healer DPS" Strategy - } + // Resto AoE handled by "Healer DPS" Strategy } diff --git a/src/Ai/Class/Shaman/Strategy/RestoShamanStrategy.cpp b/src/Ai/Class/Shaman/Strategy/RestoShamanStrategy.cpp index 698531a858c..37f55c6c6c8 100644 --- a/src/Ai/Class/Shaman/Strategy/RestoShamanStrategy.cpp +++ b/src/Ai/Class/Shaman/Strategy/RestoShamanStrategy.cpp @@ -4,64 +4,11 @@ */ #include "RestoShamanStrategy.h" - #include "Playerbots.h" -// ===== Action Node Factory ===== -class RestoShamanStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - RestoShamanStrategyActionNodeFactory() - { - creators["mana tide totem"] = &mana_tide_totem; - creators["call of the elements"] = &call_of_the_elements; - creators["stoneclaw totem"] = &stoneclaw_totem; - creators["riptide on party"] = &riptide_on_party; - creators["chain heal on party"] = &chain_heal_on_party; - creators["healing wave on party"] = &healing_wave_on_party; - creators["lesser healing wave on party"] = &lesser_healing_wave_on_party; - creators["earth shield on main tank"] = &earth_shield_on_main_tank; - creators["cleanse spirit poison on party"] = &cleanse_spirit_poison_on_party; - creators["cleanse spirit disease on party"] = &cleanse_spirit_disease_on_party; - creators["cleanse spirit curse on party"] = &cleanse_spirit_curse_on_party; - creators["cleansing totem"] = &cleansing_totem; - creators["water shield"] = &water_shield; - creators["flame shock"] = &flame_shock; - creators["lava burst"] = &lava_burst; - creators["lightning bolt"] = &lightning_bolt; - creators["chain lightning"] = &chain_lightning; - } - -private: - static ActionNode* mana_tide_totem([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("mana tide totem", - /*P*/ {}, - /*A*/ { NextAction("mana potion") }, - /*C*/ {}); - } - static ActionNode* call_of_the_elements(PlayerbotAI*) { return new ActionNode("call of the elements", {}, {}, {}); } - static ActionNode* stoneclaw_totem(PlayerbotAI*) { return new ActionNode("stoneclaw totem", {}, {}, {}); } - static ActionNode* riptide_on_party(PlayerbotAI*) { return new ActionNode("riptide on party", {}, {}, {}); } - static ActionNode* chain_heal_on_party(PlayerbotAI*) { return new ActionNode("chain heal on party", {}, {}, {}); } - static ActionNode* healing_wave_on_party(PlayerbotAI*) { return new ActionNode("healing wave on party", {}, {}, {}); } - static ActionNode* lesser_healing_wave_on_party(PlayerbotAI*) { return new ActionNode("lesser healing wave on party", {}, {}, {}); } - static ActionNode* earth_shield_on_main_tank(PlayerbotAI*) { return new ActionNode("earth shield on main tank", {}, {}, {}); } - static ActionNode* cleanse_spirit_poison_on_party(PlayerbotAI*) { return new ActionNode("cleanse spirit poison on party", {}, {}, {}); } - static ActionNode* cleanse_spirit_disease_on_party(PlayerbotAI*) { return new ActionNode("cleanse spirit disease on party", {}, {}, {}); } - static ActionNode* cleanse_spirit_curse_on_party(PlayerbotAI*) { return new ActionNode("cleanse spirit curse on party", {}, {}, {}); } - static ActionNode* cleansing_totem(PlayerbotAI*) { return new ActionNode("cleansing totem", {}, {}, {}); } - static ActionNode* water_shield(PlayerbotAI*) { return new ActionNode("water shield", {}, {}, {}); } - static ActionNode* flame_shock(PlayerbotAI*) { return new ActionNode("flame shock", {}, {}, {}); } - static ActionNode* lava_burst(PlayerbotAI*) { return new ActionNode("lava burst", {}, {}, {}); } - static ActionNode* lightning_bolt(PlayerbotAI*) { return new ActionNode("lightning bolt", {}, {}, {}); } - static ActionNode* chain_lightning(PlayerbotAI*) { return new ActionNode("chain lightning", {}, {}, {}); } -}; - -// ===== Single Target Strategy ===== RestoShamanStrategy::RestoShamanStrategy(PlayerbotAI* botAI) : GenericShamanStrategy(botAI) { - actionNodeFactories.Add(new RestoShamanStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } // ===== Trigger Initialization === @@ -75,28 +22,23 @@ void RestoShamanStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("medium mana", { NextAction("mana tide totem", ACTION_HIGH + 5) })); // Healing Triggers - triggers.push_back(new TriggerNode("group heal setting", { - NextAction("riptide on party", 27.0f), - NextAction("chain heal on party", 26.0f) })); + triggers.push_back(new TriggerNode("group heal setting", { NextAction("riptide on party", 27.0f), + NextAction("chain heal on party", 26.0f) })); - triggers.push_back(new TriggerNode("party member critical health", { - NextAction("riptide on party", 25.0f), - NextAction("healing wave on party", 24.0f), - NextAction("lesser healing wave on party", 23.0f) })); + triggers.push_back(new TriggerNode("party member critical health", { NextAction("riptide on party", 25.0f), + NextAction("healing wave on party", 24.0f), + NextAction("lesser healing wave on party", 23.0f) })); - triggers.push_back(new TriggerNode("party member low health", { - NextAction("riptide on party", 19.0f), - NextAction("healing wave on party", 18.0f), - NextAction("lesser healing wave on party", 17.0f) })); + triggers.push_back(new TriggerNode("party member low health", { NextAction("riptide on party", 19.0f), + NextAction("healing wave on party", 18.0f), + NextAction("lesser healing wave on party", 17.0f) })); - triggers.push_back(new TriggerNode("party member medium health", { - NextAction("riptide on party", 16.0f), - NextAction("healing wave on party", 15.0f), - NextAction("lesser healing wave on party", 14.0f) })); + triggers.push_back(new TriggerNode("party member medium health", { NextAction("riptide on party", 16.0f), + NextAction("healing wave on party", 15.0f), + NextAction("lesser healing wave on party", 14.0f) })); - triggers.push_back(new TriggerNode("party member almost full health", { - NextAction("riptide on party", 12.0f), - NextAction("lesser healing wave on party", 11.0f) })); + triggers.push_back(new TriggerNode("party member almost full health", { NextAction("riptide on party", 12.0f), + NextAction("lesser healing wave on party", 11.0f) })); triggers.push_back(new TriggerNode("earth shield on main tank", { NextAction("earth shield on main tank", ACTION_HIGH + 7) })); @@ -113,12 +55,9 @@ void RestoShamanStrategy::InitTriggers(std::vector& triggers) void ShamanHealerDpsStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode("healer should attack", - { NextAction("flame shock", ACTION_DEFAULT + 0.2f), - NextAction("lava burst", ACTION_DEFAULT + 0.1f), - NextAction("lightning bolt", ACTION_DEFAULT) })); + triggers.push_back(new TriggerNode("healer should attack", { NextAction("flame shock", ACTION_DEFAULT + 0.2f), + NextAction("lava burst", ACTION_DEFAULT + 0.1f), + NextAction("lightning bolt", ACTION_DEFAULT) })); - triggers.push_back( - new TriggerNode("medium aoe and healer should attack", - { NextAction("chain lightning", ACTION_DEFAULT + 0.3f) })); + triggers.push_back( new TriggerNode("medium aoe and healer should attack", { NextAction("chain lightning", ACTION_DEFAULT + 0.3f) })); } diff --git a/src/Ai/Class/Shaman/Strategy/ShamanNonCombatStrategy.cpp b/src/Ai/Class/Shaman/Strategy/ShamanNonCombatStrategy.cpp index c72000539c7..1e10b46c9b7 100644 --- a/src/Ai/Class/Shaman/Strategy/ShamanNonCombatStrategy.cpp +++ b/src/Ai/Class/Shaman/Strategy/ShamanNonCombatStrategy.cpp @@ -13,45 +13,65 @@ class ShamanNonCombatStrategyActionNodeFactory : public NamedObjectFactory& triggers) NonCombatStrategy::InitTriggers(triggers); // Totemic Recall - triggers.push_back(new TriggerNode("totemic recall", { NextAction("totemic recall", 60.0f), })); + triggers.push_back(new TriggerNode("totemic recall", { NextAction("totemic recall", 60.0f) })); // Healing/Resurrect Triggers - triggers.push_back(new TriggerNode("party member dead", { NextAction("ancestral spirit", ACTION_CRITICAL_HEAL + 10), })); - triggers.push_back(new TriggerNode("party member critical health", { - NextAction("riptide on party", 31.0f), - NextAction("healing wave on party", 30.0f) })); - triggers.push_back(new TriggerNode("party member low health",{ - NextAction("riptide on party", 29.0f), - NextAction("healing wave on party", 28.0f) })); - triggers.push_back(new TriggerNode("party member medium health",{ - NextAction("riptide on party", 27.0f), - NextAction("healing wave on party", 26.0f) })); - triggers.push_back(new TriggerNode("party member almost full health",{ - NextAction("riptide on party", 25.0f), - NextAction("lesser healing wave on party", 24.0f) })); - triggers.push_back(new TriggerNode("group heal setting",{ NextAction("chain heal on party", 27.0f) })); + triggers.push_back(new TriggerNode("party member dead", { NextAction("ancestral spirit", ACTION_CRITICAL_HEAL + 10) })); + triggers.push_back(new TriggerNode("party member critical health", { NextAction("riptide on party", 31.0f), + NextAction("healing wave on party", 30.0f) })); + triggers.push_back(new TriggerNode("party member low health", { NextAction("riptide on party", 29.0f), + NextAction("healing wave on party", 28.0f) })); + triggers.push_back(new TriggerNode("party member medium health", { NextAction("riptide on party", 27.0f), + NextAction("healing wave on party", 26.0f) })); + triggers.push_back(new TriggerNode("party member almost full health", { NextAction("riptide on party", 25.0f), + NextAction("lesser healing wave on party", 24.0f) })); + triggers.push_back(new TriggerNode("group heal setting", { NextAction("chain heal on party", 27.0f) })); // Cure Triggers - triggers.push_back(new TriggerNode("cure poison", { NextAction("cure poison", 21.0f), })); - triggers.push_back(new TriggerNode("party member cure poison", { NextAction("cure poison on party", 21.0f), })); - triggers.push_back(new TriggerNode("cure disease", { NextAction("cure disease", 31.0f), })); - triggers.push_back(new TriggerNode("party member cure disease", { NextAction("cure disease on party", 30.0f), })); + triggers.push_back(new TriggerNode("cleanse spirit poison", { NextAction("cleanse spirit", 24.0f) })); + triggers.push_back(new TriggerNode("party member cleanse spirit poison", { NextAction("cleanse spirit poison on party", 23.0f) })); + triggers.push_back(new TriggerNode("cleanse spirit disease", { NextAction("cleanse spirit", 24.0f) })); + triggers.push_back(new TriggerNode("party member cleanse spirit disease", { NextAction("cleanse spirit disease on party", 23.0f) })); + triggers.push_back(new TriggerNode("cleanse spirit curse", { NextAction("cleanse spirit", 24.0f) })); + triggers.push_back(new TriggerNode("party member cleanse spirit curse", { NextAction("cleanse spirit curse on party", 23.0f) })); // Out of Combat Buff Triggers Player* bot = botAI->GetBot(); int tab = AiFactory::GetPlayerSpecTab(bot); - if (tab == 0) // Elemental + if (tab == SHAMAN_TAB_ELEMENTAL) { - triggers.push_back(new TriggerNode("main hand weapon no imbue", { NextAction("flametongue weapon", 22.0f), })); + triggers.push_back(new TriggerNode("main hand weapon no imbue", { NextAction("flametongue weapon main hand", 22.0f), })); triggers.push_back(new TriggerNode("water shield", { NextAction("water shield", 21.0f), })); } - else if (tab == 1) // Enhancement + else if (tab == SHAMAN_TAB_ENHANCEMENT) { - triggers.push_back(new TriggerNode("main hand weapon no imbue", { NextAction("windfury weapon", 22.0f), })); - triggers.push_back(new TriggerNode("off hand weapon no imbue", { NextAction("flametongue weapon", 21.0f), })); + triggers.push_back(new TriggerNode("main hand weapon no imbue", { NextAction("windfury weapon main hand", 22.0f), })); + triggers.push_back(new TriggerNode("off hand weapon no imbue", { NextAction("flametongue weapon off hand", 21.0f), })); triggers.push_back(new TriggerNode("lightning shield", { NextAction("lightning shield", 20.0f), })); } - else if (tab == 2) // Restoration + else if (tab == SHAMAN_TAB_RESTORATION) { - triggers.push_back(new TriggerNode("main hand weapon no imbue",{ NextAction("earthliving weapon", 22.0f), })); + triggers.push_back(new TriggerNode("main hand weapon no imbue", { NextAction("earthliving weapon main hand", 22.0f), })); triggers.push_back(new TriggerNode("water shield", { NextAction("water shield", 20.0f), })); } diff --git a/src/Ai/Class/Shaman/Strategy/TotemsShamanStrategy.cpp b/src/Ai/Class/Shaman/Strategy/TotemsShamanStrategy.cpp index d00cc5e6cb7..c5ae222c16f 100644 --- a/src/Ai/Class/Shaman/Strategy/TotemsShamanStrategy.cpp +++ b/src/Ai/Class/Shaman/Strategy/TotemsShamanStrategy.cpp @@ -75,13 +75,9 @@ void TotemOfWrathStrategy::InitTriggers(std::vector& triggers) // If the bot hasn't learned Totem of Wrath yet, set Flametongue Totem instead. Player* bot = botAI->GetBot(); if (bot->HasSpell(30706)) - { triggers.push_back(new TriggerNode("set totem of wrath", { NextAction("set totem of wrath", 60.0f) })); - } else if (bot->HasSpell(8227)) - { triggers.push_back(new TriggerNode("set flametongue totem", { NextAction("set flametongue totem", 60.0f) })); - } triggers.push_back(new TriggerNode("no fire totem", { NextAction("totem of wrath", 55.0f) })); } @@ -117,13 +113,9 @@ void CleansingTotemStrategy::InitTriggers(std::vector& triggers) // If the bot hasn't learned Cleansing Totem yet, set Mana Spring Totem instead. Player* bot = botAI->GetBot(); if (bot->HasSpell(8170)) - { triggers.push_back(new TriggerNode("set cleansing totem", { NextAction("set cleansing totem", 60.0f) })); - } else if (bot->HasSpell(5675)) - { triggers.push_back(new TriggerNode("set mana spring totem", { NextAction("set mana spring totem", 60.0f) })); - } triggers.push_back(new TriggerNode("no water totem", { NextAction("cleansing totem", 55.0f) })); } @@ -143,15 +135,10 @@ void WrathOfAirTotemStrategy::InitTriggers(std::vector& triggers) // If the bot hasn't learned Wrath of Air Totem yet, set Grounding Totem instead. Player* bot = botAI->GetBot(); if (bot->HasSpell(3738)) - { triggers.push_back(new TriggerNode("set wrath of air totem", { NextAction("set wrath of air totem", 60.0f) })); - } else if (bot->HasSpell(8177)) - { triggers.push_back(new TriggerNode("set grounding totem", { NextAction("set grounding totem", 60.0f) })); - } - triggers.push_back( - new TriggerNode("no air totem", { NextAction("wrath of air totem", 55.0f) })); + triggers.push_back( new TriggerNode("no air totem", { NextAction("wrath of air totem", 55.0f) })); } WindfuryTotemStrategy::WindfuryTotemStrategy(PlayerbotAI* botAI) : GenericShamanStrategy(botAI) {} @@ -161,13 +148,9 @@ void WindfuryTotemStrategy::InitTriggers(std::vector& triggers) // If the bot hasn't learned Windfury Totem yet, set Grounding Totem instead. Player* bot = botAI->GetBot(); if (bot->HasSpell(8512)) - { triggers.push_back(new TriggerNode("set windfury totem", { NextAction("set windfury totem", 60.0f) })); - } else if (bot->HasSpell(8177)) - { triggers.push_back(new TriggerNode("set grounding totem", { NextAction("set grounding totem", 60.0f) })); - } triggers.push_back(new TriggerNode("no air totem", { NextAction("windfury totem", 55.0f) })); } diff --git a/src/Ai/Class/Shaman/Trigger/ShamanTriggers.cpp b/src/Ai/Class/Shaman/Trigger/ShamanTriggers.cpp index c8b36135a97..e7bcbfe4cf2 100644 --- a/src/Ai/Class/Shaman/Trigger/ShamanTriggers.cpp +++ b/src/Ai/Class/Shaman/Trigger/ShamanTriggers.cpp @@ -261,13 +261,13 @@ bool TotemicRecallTrigger::IsActive() } // Find the active totem strategy for this slot, and return the highest-rank spellId the bot knows for it -static uint32 GetRequiredTotemSpellId(PlayerbotAI* ai, const char* strategies[], +static uint32 GetRequiredTotemSpellId(PlayerbotAI* botAI, const char* strategies[], const uint32* spellList[], const size_t spellCounts[], size_t numStrategies) { - Player* bot = ai->GetBot(); + Player* bot = botAI->GetBot(); for (size_t i = 0; i < numStrategies; ++i) { - if (ai->HasStrategy(strategies[i], BOT_STATE_COMBAT)) + if (botAI->HasStrategy(strategies[i], BOT_STATE_COMBAT)) { // Find the highest-rank spell the bot knows for (size_t j = 0; j < spellCounts[i]; ++j) diff --git a/src/Ai/Class/Shaman/Trigger/ShamanTriggers.h b/src/Ai/Class/Shaman/Trigger/ShamanTriggers.h index 9e1a86aace0..800dc834269 100644 --- a/src/Ai/Class/Shaman/Trigger/ShamanTriggers.h +++ b/src/Ai/Class/Shaman/Trigger/ShamanTriggers.h @@ -41,14 +41,14 @@ const uint32 SPELL_CALL_OF_THE_ELEMENTS = 66842; class MainHandWeaponNoImbueTrigger : public BuffTrigger { public: - MainHandWeaponNoImbueTrigger(PlayerbotAI* ai) : BuffTrigger(ai, "main hand", 1) {} + MainHandWeaponNoImbueTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "main hand", 1) {} virtual bool IsActive(); }; class OffHandWeaponNoImbueTrigger : public BuffTrigger { public: - OffHandWeaponNoImbueTrigger(PlayerbotAI* ai) : BuffTrigger(ai, "off hand", 1) {} + OffHandWeaponNoImbueTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "off hand", 1) {} virtual bool IsActive(); }; @@ -121,7 +121,7 @@ class ElementalMasteryTrigger : public BuffTrigger class SpiritWalkTrigger : public Trigger { public: - SpiritWalkTrigger(PlayerbotAI* ai) : Trigger(ai, "spirit walk ready") {} + SpiritWalkTrigger(PlayerbotAI* botAI) : Trigger(botAI, "spirit walk ready") {} bool IsActive() override; @@ -165,9 +165,7 @@ class PartyMemberCleanseSpiritPoisonTrigger : public PartyMemberNeedCureTrigger { public: PartyMemberCleanseSpiritPoisonTrigger(PlayerbotAI* botAI) - : PartyMemberNeedCureTrigger(botAI, "cleanse spirit", DISPEL_POISON) - { - } + : PartyMemberNeedCureTrigger(botAI, "cleanse spirit", DISPEL_POISON) {} }; class CleanseSpiritCurseTrigger : public NeedCureTrigger @@ -180,9 +178,7 @@ class PartyMemberCleanseSpiritCurseTrigger : public PartyMemberNeedCureTrigger { public: PartyMemberCleanseSpiritCurseTrigger(PlayerbotAI* botAI) - : PartyMemberNeedCureTrigger(botAI, "cleanse spirit", DISPEL_CURSE) - { - } + : PartyMemberNeedCureTrigger(botAI, "cleanse spirit", DISPEL_CURSE) {} }; class CleanseSpiritDiseaseTrigger : public NeedCureTrigger @@ -195,34 +191,7 @@ class PartyMemberCleanseSpiritDiseaseTrigger : public PartyMemberNeedCureTrigger { public: PartyMemberCleanseSpiritDiseaseTrigger(PlayerbotAI* botAI) - : PartyMemberNeedCureTrigger(botAI, "cleanse spirit", DISPEL_DISEASE) - { - } -}; - -class CurePoisonTrigger : public NeedCureTrigger -{ -public: - CurePoisonTrigger(PlayerbotAI* botAI) : NeedCureTrigger(botAI, "cure poison", DISPEL_POISON) {} -}; - -class PartyMemberCurePoisonTrigger : public PartyMemberNeedCureTrigger -{ -public: - PartyMemberCurePoisonTrigger(PlayerbotAI* botAI) : PartyMemberNeedCureTrigger(botAI, "cure poison", DISPEL_POISON) {} -}; - -class CureDiseaseTrigger : public NeedCureTrigger -{ -public: - CureDiseaseTrigger(PlayerbotAI* botAI) : NeedCureTrigger(botAI, "cure disease", DISPEL_DISEASE) {} -}; - -class PartyMemberCureDiseaseTrigger : public PartyMemberNeedCureTrigger -{ -public: - PartyMemberCureDiseaseTrigger(PlayerbotAI* botAI) - : PartyMemberNeedCureTrigger(botAI, "cure disease", DISPEL_DISEASE) {} + : PartyMemberNeedCureTrigger(botAI, "cleanse spirit", DISPEL_DISEASE) {} }; // Damage and Debuff Triggers @@ -250,7 +219,7 @@ class MaelstromWeaponTrigger : public HasAuraStackTrigger class FlameShockTrigger : public DebuffTrigger { public: - FlameShockTrigger(PlayerbotAI* ai) : DebuffTrigger(ai, "flame shock", 1, true, 6.0f) {} + FlameShockTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "flame shock", 1, true, 6.0f) {} bool IsActive() override { return BuffTrigger::IsActive(); } }; @@ -265,19 +234,19 @@ class EarthShockExecuteTrigger : public Trigger class MaelstromWeapon5AndMediumAoeTrigger : public TwoTriggers { public: - MaelstromWeapon5AndMediumAoeTrigger(PlayerbotAI* ai) : TwoTriggers(ai, "maelstrom weapon 5", "medium aoe") {} + MaelstromWeapon5AndMediumAoeTrigger(PlayerbotAI* botAI) : TwoTriggers(botAI, "maelstrom weapon 5", "medium aoe") {} }; class MaelstromWeapon4AndMediumAoeTrigger : public TwoTriggers { public: - MaelstromWeapon4AndMediumAoeTrigger(PlayerbotAI* ai) : TwoTriggers(ai, "maelstrom weapon 4", "medium aoe") {} + MaelstromWeapon4AndMediumAoeTrigger(PlayerbotAI* botAI) : TwoTriggers(botAI, "maelstrom weapon 4", "medium aoe") {} }; class ChainLightningNoCdTrigger : public SpellNoCooldownTrigger { public: - ChainLightningNoCdTrigger(PlayerbotAI* ai) : SpellNoCooldownTrigger(ai, "chain lightning") {} + ChainLightningNoCdTrigger(PlayerbotAI* botAI) : SpellNoCooldownTrigger(botAI, "chain lightning") {} }; // Healing Triggers @@ -307,49 +276,49 @@ class TotemTrigger : public Trigger class CallOfTheElementsTrigger : public Trigger { public: - CallOfTheElementsTrigger(PlayerbotAI* ai) : Trigger(ai, "call of the elements") {} + CallOfTheElementsTrigger(PlayerbotAI* botAI) : Trigger(botAI, "call of the elements") {} bool IsActive() override; }; class TotemicRecallTrigger : public Trigger { public: - TotemicRecallTrigger(PlayerbotAI* ai) : Trigger(ai, "totemic recall") {} + TotemicRecallTrigger(PlayerbotAI* botAI) : Trigger(botAI, "totemic recall") {} bool IsActive() override; }; class NoEarthTotemTrigger : public Trigger { public: - NoEarthTotemTrigger(PlayerbotAI* ai) : Trigger(ai, "no earth totem") {} + NoEarthTotemTrigger(PlayerbotAI* botAI) : Trigger(botAI, "no earth totem") {} bool IsActive() override; }; class NoFireTotemTrigger : public Trigger { public: - NoFireTotemTrigger(PlayerbotAI* ai) : Trigger(ai, "no fire totem") {} + NoFireTotemTrigger(PlayerbotAI* botAI) : Trigger(botAI, "no fire totem") {} bool IsActive() override; }; class NoWaterTotemTrigger : public Trigger { public: - NoWaterTotemTrigger(PlayerbotAI* ai) : Trigger(ai, "no water totem") {} + NoWaterTotemTrigger(PlayerbotAI* botAI) : Trigger(botAI, "no water totem") {} bool IsActive() override; }; class NoAirTotemTrigger : public Trigger { public: - NoAirTotemTrigger(PlayerbotAI* ai) : Trigger(ai, "no air totem") {} + NoAirTotemTrigger(PlayerbotAI* botAI) : Trigger(botAI, "no air totem") {} bool IsActive() override; }; class CallOfTheElementsAndEnemyWithinMeleeTrigger : public TwoTriggers { public: - CallOfTheElementsAndEnemyWithinMeleeTrigger(PlayerbotAI* ai) : TwoTriggers(ai, "call of the elements", "enemy within melee") {} + CallOfTheElementsAndEnemyWithinMeleeTrigger(PlayerbotAI* botAI) : TwoTriggers(botAI, "call of the elements", "enemy within melee") {} }; // Set Strategy Assigned Totems @@ -359,8 +328,8 @@ class SetTotemTrigger : public Trigger public: // Template constructor: infers N (size of the id array) at compile time template - SetTotemTrigger(PlayerbotAI* ai, std::string const& spellName, const uint32 (&ids)[N], int actionButtonId) - : Trigger(ai, "set " + spellName) + SetTotemTrigger(PlayerbotAI* botAI, std::string const& spellName, const uint32 (&ids)[N], int actionButtonId) + : Trigger(botAI, "set " + spellName) , totemSpellIds(ids) , totemSpellIdsCount(N) , actionButtonId(actionButtonId) @@ -376,120 +345,120 @@ class SetTotemTrigger : public Trigger class SetStrengthOfEarthTotemTrigger : public SetTotemTrigger { public: - SetStrengthOfEarthTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "strength of earth totem", STRENGTH_OF_EARTH_TOTEM, TOTEM_BAR_SLOT_EARTH) {} + SetStrengthOfEarthTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "strength of earth totem", STRENGTH_OF_EARTH_TOTEM, TOTEM_BAR_SLOT_EARTH) {} }; class SetStoneskinTotemTrigger : public SetTotemTrigger { public: - SetStoneskinTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "stoneskin totem", STONESKIN_TOTEM, TOTEM_BAR_SLOT_EARTH) {} + SetStoneskinTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "stoneskin totem", STONESKIN_TOTEM, TOTEM_BAR_SLOT_EARTH) {} }; class SetTremorTotemTrigger : public SetTotemTrigger { public: - SetTremorTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "tremor totem", TREMOR_TOTEM, TOTEM_BAR_SLOT_EARTH) {} + SetTremorTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "tremor totem", TREMOR_TOTEM, TOTEM_BAR_SLOT_EARTH) {} }; class SetEarthbindTotemTrigger : public SetTotemTrigger { public: - SetEarthbindTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "earthbind totem", EARTHBIND_TOTEM, TOTEM_BAR_SLOT_EARTH) {} + SetEarthbindTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "earthbind totem", EARTHBIND_TOTEM, TOTEM_BAR_SLOT_EARTH) {} }; class SetSearingTotemTrigger : public SetTotemTrigger { public: - SetSearingTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "searing totem", SEARING_TOTEM, TOTEM_BAR_SLOT_FIRE) {} + SetSearingTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "searing totem", SEARING_TOTEM, TOTEM_BAR_SLOT_FIRE) {} }; class SetMagmaTotemTrigger : public SetTotemTrigger { public: - SetMagmaTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "magma totem", MAGMA_TOTEM, TOTEM_BAR_SLOT_FIRE) {} + SetMagmaTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "magma totem", MAGMA_TOTEM, TOTEM_BAR_SLOT_FIRE) {} }; class SetFlametongueTotemTrigger : public SetTotemTrigger { public: - SetFlametongueTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "flametongue totem", FLAMETONGUE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} + SetFlametongueTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "flametongue totem", FLAMETONGUE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} }; class SetTotemOfWrathTrigger : public SetTotemTrigger { public: - SetTotemOfWrathTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "totem of wrath", TOTEM_OF_WRATH, TOTEM_BAR_SLOT_FIRE) {} + SetTotemOfWrathTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "totem of wrath", TOTEM_OF_WRATH, TOTEM_BAR_SLOT_FIRE) {} }; class SetFrostResistanceTotemTrigger : public SetTotemTrigger { public: - SetFrostResistanceTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "frost resistance totem", FROST_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} + SetFrostResistanceTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "frost resistance totem", FROST_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} }; class SetHealingStreamTotemTrigger : public SetTotemTrigger { public: - SetHealingStreamTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "healing stream totem", HEALING_STREAM_TOTEM, TOTEM_BAR_SLOT_WATER) {} + SetHealingStreamTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "healing stream totem", HEALING_STREAM_TOTEM, TOTEM_BAR_SLOT_WATER) {} }; class SetManaSpringTotemTrigger : public SetTotemTrigger { public: - SetManaSpringTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "mana spring totem", MANA_SPRING_TOTEM, TOTEM_BAR_SLOT_WATER) {} + SetManaSpringTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "mana spring totem", MANA_SPRING_TOTEM, TOTEM_BAR_SLOT_WATER) {} }; class SetCleansingTotemTrigger : public SetTotemTrigger { public: - SetCleansingTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "cleansing totem", CLEANSING_TOTEM, TOTEM_BAR_SLOT_WATER) {} + SetCleansingTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "cleansing totem", CLEANSING_TOTEM, TOTEM_BAR_SLOT_WATER) {} }; class SetFireResistanceTotemTrigger : public SetTotemTrigger { public: - SetFireResistanceTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "fire resistance totem", FIRE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_WATER) {} + SetFireResistanceTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "fire resistance totem", FIRE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_WATER) {} }; class SetWrathOfAirTotemTrigger : public SetTotemTrigger { public: - SetWrathOfAirTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "wrath of air totem", WRATH_OF_AIR_TOTEM, TOTEM_BAR_SLOT_AIR) {} + SetWrathOfAirTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "wrath of air totem", WRATH_OF_AIR_TOTEM, TOTEM_BAR_SLOT_AIR) {} }; class SetWindfuryTotemTrigger : public SetTotemTrigger { public: - SetWindfuryTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "windfury totem", WINDFURY_TOTEM, TOTEM_BAR_SLOT_AIR) {} + SetWindfuryTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "windfury totem", WINDFURY_TOTEM, TOTEM_BAR_SLOT_AIR) {} }; class SetNatureResistanceTotemTrigger : public SetTotemTrigger { public: - SetNatureResistanceTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "nature resistance totem", NATURE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_AIR) {} + SetNatureResistanceTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "nature resistance totem", NATURE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_AIR) {} }; class SetGroundingTotemTrigger : public SetTotemTrigger { public: - SetGroundingTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "grounding totem", GROUNDING_TOTEM, TOTEM_BAR_SLOT_AIR) {} + SetGroundingTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "grounding totem", GROUNDING_TOTEM, TOTEM_BAR_SLOT_AIR) {} }; #endif From 579f9726665385825278879b65f504c8d523592f Mon Sep 17 00:00:00 2001 From: Chris Lacy <129199071+SI-ChrisL@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:29:23 -0500 Subject: [PATCH 05/18] fix: invert NoRtiTrigger condition so mark rti strategy works (#2256) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - `NoRtiTrigger::IsActive()` returns `target != nullptr`, meaning it only fires when a raid icon is **already** assigned — creating a chicken-and-egg problem where the bot never places the first mark - Invert to `target == nullptr` so the trigger fires when **no** mark exists, prompting `MarkRtiAction` to mark the lowest-HP unmarked attacker - Once a mark is placed, the trigger stops firing until the marked target dies ## Test plan - [ ] Add `mark rti` strategy to a tank bot via `co +mark rti` - [ ] Enter combat with multiple mobs — bot should auto-mark lowest HP target with skull - [ ] Verify mark persists until target dies, then bot marks next target - [ ] Verify no marking occurs out of combat 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar Co-authored-by: Claude Opus 4.6 (1M context) --- src/Ai/Base/Trigger/RtiTriggers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ai/Base/Trigger/RtiTriggers.cpp b/src/Ai/Base/Trigger/RtiTriggers.cpp index 6158bc4d600..c7dc737cf33 100644 --- a/src/Ai/Base/Trigger/RtiTriggers.cpp +++ b/src/Ai/Base/Trigger/RtiTriggers.cpp @@ -16,5 +16,5 @@ bool NoRtiTrigger::IsActive() return false; Unit* target = AI_VALUE(Unit*, "rti target"); - return target != nullptr; + return target == nullptr; } From f76c286353c41661c47e095a14a2836725c636a8 Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 3 Apr 2026 15:31:30 -0500 Subject: [PATCH 06/18] Fix Destruction Warlock Glyphs, Take Two (#2278) ## Pull Request Description When I previously "fixed" the default glyphs for destro pve, I accidentally put Life Tap twice. This PR replaces the second Life Tap with Incinerate (which is what I intended). I also fixed a typo in the config. ## How to Test the Changes Use maintenance on a level 80 Warlock with destro pve spec. Their major glyphs should be, in order, Life Tap, Conflagrate, and Incinerate. ## 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**) The default glyphs are wrong. - 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? - - [x] No - - [ ] Yes (**explain below**) ## 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 --- conf/playerbots.conf.dist | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index c1c06b8b717..5df2b0a9784 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -582,7 +582,7 @@ AiPlayerbot.AutoGearScoreLimit = 0 AiPlayerbot.BotCheats = "food,taxi,raid" # List of attunement quests (comma-separated list of quest IDs) that are automatically completed for all bots. -# While mod-playerbots does not restore removed attunement requirements, although other mods, such as mod-individual-progression, may do so. +# While mod-playerbots does not restore removed attunement requirements, other mods, such as mod-individual-progression, may do so. # This is meant to exclude bots from such requirements. # # Default: @@ -1687,7 +1687,7 @@ AiPlayerbot.PremadeSpecLink.9.1.60 = -003203301135112530135201051 AiPlayerbot.PremadeSpecLink.9.1.70 = -003203301135112530135201051-55 AiPlayerbot.PremadeSpecLink.9.1.80 = -003203301135112530135221351-55000005 AiPlayerbot.PremadeSpecName.9.2 = destro pve -AiPlayerbot.PremadeSpecGlyph.9.2 = 45785,43390,42454,43394,43393,45785 +AiPlayerbot.PremadeSpecGlyph.9.2 = 45785,43390,42454,43394,43393,42453 AiPlayerbot.PremadeSpecLink.9.2.60 = --05203215200231051305031151 AiPlayerbot.PremadeSpecLink.9.2.80 = 23-0302-05203215220331051335231351 AiPlayerbot.PremadeSpecName.9.3 = affli pvp From c0390a24fd9b0ab622d7ea246b57b16c3cc0f5d8 Mon Sep 17 00:00:00 2001 From: bashermens <31279994+hermensbas@users.noreply.github.com> Date: Fri, 3 Apr 2026 23:54:46 +0200 Subject: [PATCH 07/18] feat(Performance): BotActiveAlone activity interval fixes and default settings for avg player (#2250) ## Pull Request Description - Bugfix the jittering on/off of botAlone activity - BotActiveAlone activity duration configurable - Updated the default config values for general user for a smoother experience - Added offset jittering for the check allowedActivity and check next AI delay to prevent cpu spikes (disabled WhenIsFriend can cause race conditions) ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [ ] No, not at all - - [ ] Minimal impact (**explain below**) - - [x] Moderate impact (**explain below**) In a positive way, bots in your zone and 150 radius will always be active, meanwhile other bots will be active 40% of the time with intervals for 60 seconds per bot. With much lower latencies. All configurable without say. 40% and 60 seconds for more balance for those who seek bots create world feel more natural and live vs bots leveling without killing the server performance. Why not 50 due activity of bots itself 40% will result more into 45-50% like behavior and 50% prolly more 55%-60%. This it not something we want incorporate when calculating the value since it depends on various config and situation. But 40% is good base with default config. - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - 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? - - [x] No - - [ ] Yes (**explain below**) ## 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: Revision Co-authored-by: kadeshar --- conf/playerbots.conf.dist | 28 +++++++---- src/Bot/Engine/PlayerbotAIBase.cpp | 10 ++-- src/Bot/Engine/PlayerbotAIBase.h | 3 +- src/Bot/PlayerbotAI.cpp | 76 +++++++++++++++--------------- src/Bot/PlayerbotAI.h | 3 +- src/PlayerbotAIConfig.cpp | 5 +- src/PlayerbotAIConfig.h | 1 + 7 files changed, 72 insertions(+), 54 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 5df2b0a9784..ff1b393b58b 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -866,22 +866,32 @@ AiPlayerbot.ExcludedHunterPetFamilies = "" # #################################################################################################### - #################################################################################################### -# ACTIVITIES +# ACTIVITY # +# BotActiveAlone (%) +# - Determines the percentage of bots that remain active when no real players are nearby. +# - Default is 40% (which is practise kinda translates into 45-50%). +# - If `botActiveAloneSmartScale` is enabled, automatically temporarily down scale activity based on latency. +# - 40% will be activated in a random rotation for the amount of seconds specified in . +# - There are multiple conditions when bots will forced to be active e.g. when in BG/instance/attacked, some are configurable below. +#- When 100$ all bots will be active without any rotation or logic applied but comes with performance hit. # -# Specify percent of active bots -# The default is 100% but will be automatically adjusted if botActiveAloneSmartScale -# is enabled. Regardless, this value is only applied to inactive areas where no real players -# are detected. When real players are nearby, the value is always enforced to 100% -AiPlayerbot.BotActiveAlone = 100 +AiPlayerbot.BotActiveAlone = 10 +AiPlayerbot.BotActiveAloneDurationSeconds = 30 -# Force botActiveAlone when bot is within the specified distance of a real player +# Some additional rules that enforces the bot to be active +# +# - bot is within this distance from a real player. +# - bot is in the same zone as a real player. +# - bot is in the same continent as a real player. +# - bot is a real player's friend. +# - bot is in a real player's guild. +# AiPlayerbot.BotActiveAloneForceWhenInRadius = 150 AiPlayerbot.BotActiveAloneForceWhenInZone = 1 AiPlayerbot.BotActiveAloneForceWhenInMap = 0 -AiPlayerbot.BotActiveAloneForceWhenIsFriend = 1 +AiPlayerbot.BotActiveAloneForceWhenIsFriend = 0 AiPlayerbot.BotActiveAloneForceWhenInGuild = 1 # SmartScale (automatic scaling of percentage of active bots based on latency) diff --git a/src/Bot/Engine/PlayerbotAIBase.cpp b/src/Bot/Engine/PlayerbotAIBase.cpp index cf4ad172cc9..46e8c0bf508 100644 --- a/src/Bot/Engine/PlayerbotAIBase.cpp +++ b/src/Bot/Engine/PlayerbotAIBase.cpp @@ -25,7 +25,7 @@ void PlayerbotAIBase::UpdateAI(uint32 elapsed, bool minimal) return; UpdateAIInternal(elapsed, minimal); - YieldThread(); + YieldThread(nullptr); } void PlayerbotAIBase::SetNextCheckDelay(uint32 const delay) @@ -49,10 +49,14 @@ void PlayerbotAIBase::IncreaseNextCheckDelay(uint32 delay) bool PlayerbotAIBase::CanUpdateAI() { return nextAICheckDelay == 0; } -void PlayerbotAIBase::YieldThread(uint32 delay) +void PlayerbotAIBase::YieldThread(Player* bot, uint32 delay) { if (nextAICheckDelay < delay) - nextAICheckDelay = delay; + { + // Adding a deterministic per-bot slight offset (0–200 ms) to stagger updates and prevent cpu spikes. + uint32 offset = bot ? (bot->GetGUID().GetCounter() % 201) : 0; + nextAICheckDelay = delay + offset; + } } bool PlayerbotAIBase::IsActive() { return nextAICheckDelay < sPlayerbotAIConfig.maxWaitForMove; } diff --git a/src/Bot/Engine/PlayerbotAIBase.h b/src/Bot/Engine/PlayerbotAIBase.h index d0e0b775b20..2d6ab31ce41 100644 --- a/src/Bot/Engine/PlayerbotAIBase.h +++ b/src/Bot/Engine/PlayerbotAIBase.h @@ -8,6 +8,7 @@ #include "Define.h" #include "PlayerbotAIConfig.h" +#include "Player.h" class PlayerbotAIBase { @@ -17,7 +18,7 @@ class PlayerbotAIBase bool CanUpdateAI(); void SetNextCheckDelay(uint32 const delay); void IncreaseNextCheckDelay(uint32 delay); - void YieldThread(uint32 delay = sPlayerbotAIConfig.reactDelay); + void YieldThread(Player* bot, uint32 delay = sPlayerbotAIConfig.reactDelay); virtual void UpdateAI(uint32 elapsed, bool minimal = false); virtual void UpdateAIInternal(uint32 elapsed, bool minimal = false) = 0; bool IsActive(); diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 446ca8c40f1..5ea8b3323f8 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -279,7 +279,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) if (spellTarget && !spellTarget->IsAlive() && !spellInfo->IsAllowingDeadTarget()) { InterruptSpell(); - YieldThread(GetReactDelay()); + YieldThread(bot, GetReactDelay()); return; } @@ -288,7 +288,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) if (goSpellTarget && !goSpellTarget->isSpawned()) { InterruptSpell(); - YieldThread(GetReactDelay()); + YieldThread(bot, GetReactDelay()); return; } @@ -320,7 +320,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) if (isHeal && isSingleTarget && spellTarget && spellTarget->IsFullHealth()) { InterruptSpell(); - YieldThread(GetReactDelay()); + YieldThread(bot, GetReactDelay()); return; } @@ -332,7 +332,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) } // Wait for spell cast - YieldThread(GetReactDelay()); + YieldThread(bot, GetReactDelay()); return; } } @@ -368,7 +368,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) // Update internal AI UpdateAIInternal(elapsed, minimal); - YieldThread(GetReactDelay()); + YieldThread(bot, GetReactDelay()); } // Helper function for UpdateAI to check group membership and handle removal if necessary @@ -4377,21 +4377,27 @@ Player* PlayerbotAI::GetGroupLeader() return master; } -uint32 PlayerbotAI::GetFixedBotNumer(uint32 maxNum, float cyclePerMin) +uint32 PlayerbotAI::GetFixedBotNumber(uint32 maxNum) { - uint32 randseed = rand32(); // Seed random number - uint32 randnum = bot->GetGUID().GetCounter() + randseed; // Semi-random but fixed number for each bot. + if (maxNum == 0) + return 0; - if (cyclePerMin > 0) - { - uint32 cycle = floor(getMSTime() / (1000)); // Semi-random number adds 1 each second. - cycle = cycle * cyclePerMin / 60; // Cycles cyclePerMin per minute. - randnum += cycle; // Make the random number cylce. - } + // Deterministic pseudo-random hash based on the bot GUID evenly distributed across active slots + uint32 id = bot->GetGUID().GetCounter(); + uint32 h = id; + h ^= h >> 16; + h *= 0x7feb352d; + h ^= h >> 15; + h *= 0x846ca68b; + h ^= h >> 16; - randnum = - (randnum % (maxNum + 1)); // Loops the randomnumber at maxNum. Bassically removes all the numbers above 99. - return randnum; // Now we have a number unique for each bot between 0 and maxNum that increases by cyclePerMin. + // Current time slot + uint32 timeSlot = (getMSTime() / 1000) / sPlayerbotAIConfig.BotActiveAloneDurationSeconds; + + // Mix timeSlot into the hash to reshuffle every rotation window + uint32 mixed = h ^ (timeSlot * 0x9e3779b9); // with multiplicative constant + + return mixed % maxNum; } /* @@ -4408,7 +4414,7 @@ enum GrouperType GrouperType PlayerbotAI::GetGrouperType() { - uint32 grouperNumber = GetFixedBotNumer(100, 0); + uint32 grouperNumber = GetFixedBotNumber(100); if (grouperNumber < 20 && !HasRealPlayerMaster()) return GrouperType::SOLO; @@ -4430,7 +4436,7 @@ GrouperType PlayerbotAI::GetGrouperType() GuilderType PlayerbotAI::GetGuilderType() { - uint32 grouperNumber = GetFixedBotNumer(100, 0); + uint32 grouperNumber = GetFixedBotNumber(100); if (grouperNumber < 20 && !HasRealPlayerMaster()) return GuilderType::SOLO; @@ -4754,44 +4760,40 @@ bool PlayerbotAI::AllowActive(ActivityType activityType) // situations are usable for scaling when enabled. // ####################################################################################### - // Below is code to have a specified % of bots active at all times. - // The default is 100%. With 1% of all bots going active or inactive each minute. + // Base percentage of bots to be active uint32 mod = sPlayerbotAIConfig.botActiveAlone > 100 ? 100 : sPlayerbotAIConfig.botActiveAlone; + + // Apply SmartScale if enabled if (sPlayerbotAIConfig.botActiveAloneSmartScale && bot->GetLevel() >= sPlayerbotAIConfig.botActiveAloneSmartScaleWhenMinLevel && bot->GetLevel() <= sPlayerbotAIConfig.botActiveAloneSmartScaleWhenMaxLevel) { - mod = AutoScaleActivity(mod); + mod = AutoScaleActivity(mod); // mod reflects on latency throttling } - uint32 ActivityNumber = - GetFixedBotNumer(100, sPlayerbotAIConfig.botActiveAlone * static_cast(mod) / 100 * 0.01f); + // Get deterministic bucket + timeSlot + uint32 ActivityNumber = GetFixedBotNumber(100); - return ActivityNumber <= - (sPlayerbotAIConfig.botActiveAlone * mod) / - 100; // The given percentage of bots should be active and rotate 1% of those active bots each minute. + // Check if this bot is in the active set + return ActivityNumber < mod; // mod is directly the number of bots active (0–100) } bool PlayerbotAI::AllowActivity(ActivityType activityType, bool checkNow) { const int activityIndex = static_cast(activityType); - // Unknown/out-of-range avoid blocking, added logging for further analysing should not happen in the first place. - if (activityIndex <= 0 || activityIndex >= MAX_ACTIVITY_TYPE) - { - LOG_ERROR("playerbots", "AllowActivity received invalid activity type value: {}", activityIndex); - return true; - } - if (!allowActiveCheckTimer[activityIndex]) - allowActiveCheckTimer[activityIndex] = time(nullptr); + allowActiveCheckTimer[activityIndex] = getMSTime(); + + // 4500ms base + 0–499ms per-bot offset = 4500–4999ms, capping at just under 5 seconds + uint32 offset = bot->GetGUID().GetCounter() % 500; - if (!checkNow && time(nullptr) < (allowActiveCheckTimer[activityIndex] + 5)) + if (!checkNow && getMSTime() < (allowActiveCheckTimer[activityIndex] + 4500 + offset)) return allowActive[activityIndex]; const bool allowed = AllowActive(activityType); allowActive[activityIndex] = allowed; - allowActiveCheckTimer[activityIndex] = time(nullptr); + allowActiveCheckTimer[activityIndex] = getMSTime(); return allowed; } diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index 9c417561fec..1829e9175d4 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -541,8 +541,7 @@ class PlayerbotAI : public PlayerbotAIBase // Checks if the bot is summoned as alt of a player bool IsAlt(); Player* GetGroupLeader(); - // Returns a semi-random (cycling) number that is fixed for each bot. - uint32 GetFixedBotNumer(uint32 maxNum = 100, float cyclePerMin = 1); + uint32 GetFixedBotNumber(uint32 maxNum = 100); GrouperType GetGrouperType(); GuilderType GetGuilderType(); bool HasPlayerNearby(WorldPosition* pos, float range = sPlayerbotAIConfig.reactDistance); diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index 6a8d60129e2..f150f7af198 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -595,11 +595,12 @@ bool PlayerbotAIConfig::Initialize() randomBotHordeRatio = sConfigMgr->GetOption("AiPlayerbot.RandomBotHordeRatio", 50); disableDeathKnightLogin = sConfigMgr->GetOption("AiPlayerbot.DisableDeathKnightLogin", 0); limitTalentsExpansion = sConfigMgr->GetOption("AiPlayerbot.LimitTalentsExpansion", 0); - botActiveAlone = sConfigMgr->GetOption("AiPlayerbot.BotActiveAlone", 100); + botActiveAlone = sConfigMgr->GetOption("AiPlayerbot.BotActiveAlone", 10); + BotActiveAloneDurationSeconds = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneDurationSeconds", 30); BotActiveAloneForceWhenInRadius = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenInRadius", 150); BotActiveAloneForceWhenInZone = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenInZone", 1); BotActiveAloneForceWhenInMap = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenInMap", 0); - BotActiveAloneForceWhenIsFriend = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenIsFriend", 1); + BotActiveAloneForceWhenIsFriend = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenIsFriend", 0); BotActiveAloneForceWhenInGuild = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenInGuild", 1); botActiveAloneSmartScale = sConfigMgr->GetOption("AiPlayerbot.botActiveAloneSmartScale", 1); botActiveAloneSmartScaleDiffLimitfloor = sConfigMgr->GetOption("AiPlayerbot.botActiveAloneSmartScaleDiffLimitfloor", 50); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 7b9b2fe8112..7b6c1eb6f57 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -334,6 +334,7 @@ class PlayerbotAIConfig bool disableDeathKnightLogin; bool limitTalentsExpansion; uint32 botActiveAlone; + uint32 BotActiveAloneDurationSeconds; uint32 BotActiveAloneForceWhenInRadius; bool BotActiveAloneForceWhenInZone; bool BotActiveAloneForceWhenInMap; From d07ddb14d091111ae01157149c8f2997fc52a304 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 4 Apr 2026 07:31:17 +0200 Subject: [PATCH 08/18] Paladin use bubble heal strategy (#2244) ## Pull Request Description Added support for bubble heal strategy. Adjusted emergency action order ## How to Test the Changes - invite paladin bot to party - check that bot dont have `healer dps` strategy - start combat (with for example dummy) - use `.damage 20000` where 20000 is near max health of bot - use `.unaura 25771` until bot use Divine Shield - bot should heal himself ## 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**) Paladin bot heal himself without `healer dps` while Divine Shield - 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? - - [x] No - - [ ] Yes (**explain below**) ## 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 --- .../Class/Paladin/PaladinAiObjectContext.cpp | 2 ++ .../Strategy/GenericPaladinStrategy.cpp | 22 +++++++++++-------- .../Class/Paladin/Trigger/PaladinTriggers.cpp | 5 +++++ .../Class/Paladin/Trigger/PaladinTriggers.h | 8 +++++++ 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp b/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp index 3c92e57c33c..7edbf5c8f49 100644 --- a/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp +++ b/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp @@ -134,6 +134,7 @@ class PaladinTriggerFactoryInternal : public NamedObjectContext &PaladinTriggerFactoryInternal::hammer_of_justice_on_snare_target; creators["not sensing undead"] = &PaladinTriggerFactoryInternal::not_sensing_undead; creators["divine favor"] = &PaladinTriggerFactoryInternal::divine_favor; + creators["divine shield low health"] = &PaladinTriggerFactoryInternal::divine_shield_low_health; creators["turn undead"] = &PaladinTriggerFactoryInternal::turn_undead; creators["avenger's shield"] = &PaladinTriggerFactoryInternal::avenger_shield; creators["consecration"] = &PaladinTriggerFactoryInternal::consecration; @@ -156,6 +157,7 @@ class PaladinTriggerFactoryInternal : public NamedObjectContext static Trigger* not_sensing_undead(PlayerbotAI* botAI) { return new NotSensingUndeadTrigger(botAI); } static Trigger* turn_undead(PlayerbotAI* botAI) { return new TurnUndeadTrigger(botAI); } static Trigger* divine_favor(PlayerbotAI* botAI) { return new DivineFavorTrigger(botAI); } + static Trigger* divine_shield_low_health(PlayerbotAI* botAI) { return new DivineShieldLowHealthTrigger(botAI); } static Trigger* holy_shield(PlayerbotAI* botAI) { return new HolyShieldTrigger(botAI); } static Trigger* righteous_fury(PlayerbotAI* botAI) { return new RighteousFuryTrigger(botAI); } static Trigger* judgement(PlayerbotAI* botAI) { return new JudgementTrigger(botAI); } diff --git a/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp b/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp index f03207216ec..9a197c601c6 100644 --- a/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp +++ b/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp @@ -16,17 +16,21 @@ void GenericPaladinStrategy::InitTriggers(std::vector& triggers) { CombatStrategy::InitTriggers(triggers); - triggers.push_back(new TriggerNode("critical health", { NextAction("divine shield", - ACTION_HIGH + 5) })); - triggers.push_back( - new TriggerNode("hammer of justice interrupt", - { NextAction("hammer of justice", ACTION_INTERRUPT) })); - triggers.push_back(new TriggerNode( - "hammer of justice on enemy healer", + triggers.push_back(new TriggerNode("hammer of justice interrupt", + { NextAction("hammer of justice", ACTION_INTERRUPT) })); + triggers.push_back(new TriggerNode("hammer of justice on enemy healer", { NextAction("hammer of justice on enemy healer", ACTION_INTERRUPT) })); - triggers.push_back(new TriggerNode( - "hammer of justice on snare target", + triggers.push_back(new TriggerNode("hammer of justice on snare target", { NextAction("hammer of justice on snare target", ACTION_INTERRUPT) })); + triggers.push_back(new TriggerNode("critical health", { NextAction("divine shield", ACTION_EMERGENCY) })); + triggers.push_back(new TriggerNode("critical health", { NextAction("lay on hands", ACTION_EMERGENCY + 1) })); + triggers.push_back(new TriggerNode("party member critical health", + { NextAction("lay on hands on party", ACTION_EMERGENCY + 2) })); + triggers.push_back(new TriggerNode("divine shield low health", + { NextAction("flash of light", ACTION_EMERGENCY + 3), NextAction("holy light", ACTION_EMERGENCY + 2)})); + triggers.push_back(new TriggerNode("protect party member", + { NextAction("blessing of protection on party", ACTION_EMERGENCY + 3) })); + triggers.push_back(new TriggerNode("high mana", { NextAction("divine plea", ACTION_HIGH) })); triggers.push_back(new TriggerNode( "critical health", { NextAction("lay on hands", ACTION_EMERGENCY) })); triggers.push_back( diff --git a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp index e3367aaef3b..46a2d8a9427 100644 --- a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp +++ b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp @@ -32,6 +32,11 @@ bool BlessingTrigger::IsActive() "blessing of kings", "blessing of sanctuary", nullptr); } +bool DivineShieldLowHealthTrigger::IsActive() +{ + return botAI->HasAura("divine shield", bot) && AI_VALUE2(uint8, "health", "self target") < 80; +} + Unit* HandOfFreedomOnPartyTrigger::GetTarget() { bool const selfImpaired = botAI->IsMovementImpaired(bot); diff --git a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h index cc6ceddcf4c..d11c8024fca 100644 --- a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h +++ b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h @@ -185,6 +185,14 @@ class DivineFavorTrigger : public BuffTrigger DivineFavorTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "divine favor") {} }; +class DivineShieldLowHealthTrigger : public Trigger +{ +public: + DivineShieldLowHealthTrigger(PlayerbotAI* botAI) : Trigger(botAI, "divine shield low health") {} + + bool IsActive() override; +}; + class NotSensingUndeadTrigger : public BuffTrigger { public: From 30c142aacab881335e1e717996a6a5ef2995fe82 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 4 Apr 2026 07:31:42 +0200 Subject: [PATCH 09/18] Challenging Roar support (#2238) ## Pull Request Description Added Challenging Roar support to druid taunt spell Related with: #2002 ## How to Test the Changes - run dungeon with druid tank and monitor spell usage ## 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**) Druid also use Challenging Roar as taunt spell - 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? - - [x] No - - [ ] Yes (**explain below**) ## 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 --- src/Ai/Class/Druid/Action/DruidBearActions.h | 6 ++++++ src/Ai/Class/Druid/DruidAiObjectContext.cpp | 2 ++ src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.cpp | 1 + 3 files changed, 9 insertions(+) diff --git a/src/Ai/Class/Druid/Action/DruidBearActions.h b/src/Ai/Class/Druid/Action/DruidBearActions.h index caf369be339..d5354b7e6c5 100644 --- a/src/Ai/Class/Druid/Action/DruidBearActions.h +++ b/src/Ai/Class/Druid/Action/DruidBearActions.h @@ -23,6 +23,12 @@ class CastGrowlAction : public CastSpellAction CastGrowlAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "growl") {} }; +class CastChallengingRoarAction : public CastMeleeDebuffSpellAction +{ +public: + CastChallengingRoarAction(PlayerbotAI* botAI) : CastMeleeDebuffSpellAction(botAI, "challenging roar") {} +}; + class CastMaulAction : public CastMeleeSpellAction { public: diff --git a/src/Ai/Class/Druid/DruidAiObjectContext.cpp b/src/Ai/Class/Druid/DruidAiObjectContext.cpp index 3a638eedc31..17b561d19f4 100644 --- a/src/Ai/Class/Druid/DruidAiObjectContext.cpp +++ b/src/Ai/Class/Druid/DruidAiObjectContext.cpp @@ -183,6 +183,7 @@ class DruidAiObjectContextInternal : public NamedObjectContext creators["bash"] = &DruidAiObjectContextInternal::bash; creators["swipe"] = &DruidAiObjectContextInternal::swipe; creators["growl"] = &DruidAiObjectContextInternal::growl; + creators["challenging roar"] = &DruidAiObjectContextInternal::challenging_roar; creators["demoralizing roar"] = &DruidAiObjectContextInternal::demoralizing_roar; creators["hibernate"] = &DruidAiObjectContextInternal::hibernate; creators["entangling roots"] = &DruidAiObjectContextInternal::entangling_roots; @@ -277,6 +278,7 @@ class DruidAiObjectContextInternal : public NamedObjectContext static Action* bash(PlayerbotAI* botAI) { return new CastBashAction(botAI); } static Action* swipe(PlayerbotAI* botAI) { return new CastSwipeAction(botAI); } static Action* growl(PlayerbotAI* botAI) { return new CastGrowlAction(botAI); } + static Action* challenging_roar(PlayerbotAI* botAI) { return new CastChallengingRoarAction(botAI); } static Action* demoralizing_roar(PlayerbotAI* botAI) { return new CastDemoralizingRoarAction(botAI); } static Action* moonkin_form(PlayerbotAI* botAI) { return new CastMoonkinFormAction(botAI); } static Action* hibernate(PlayerbotAI* botAI) { return new CastHibernateAction(botAI); } diff --git a/src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.cpp b/src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.cpp index 4a60fbd3eac..13af635c3d3 100644 --- a/src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.cpp +++ b/src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.cpp @@ -212,6 +212,7 @@ void BearTankDruidStrategy::InitTriggers(std::vector& triggers) } ) ); + triggers.push_back(new TriggerNode("high aoe", {NextAction("challenging roar", ACTION_HIGH + 8)})); triggers.push_back( new TriggerNode( "lose aggro", From a87999bef55c213ea093af842ff1213a2eefe9f8 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 4 Apr 2026 07:32:32 +0200 Subject: [PATCH 10/18] 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 68f5a523909..27dd5999cbf 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 f78728cd5e8..7a44112f36f 100644 --- a/src/Ai/Base/Trigger/GenericTriggers.h +++ b/src/Ai/Base/Trigger/GenericTriggers.h @@ -762,6 +762,14 @@ class FearCharmSleepTrigger : public Trigger 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 63f9be4049f..c77df3a31ba 100644 --- a/src/Ai/Base/TriggerContext.h +++ b/src/Ai/Base/TriggerContext.h @@ -62,6 +62,7 @@ class TriggerContext : public NamedObjectContext 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 @@ class TriggerContext : public NamedObjectContext 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 9f15cf76731..20a42c21997 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 7910fc0d8ed..da004fc70a9 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 091aed2b84c..05a6c2c63ca 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 27643258356..160d8df8fcc 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 178984de9cb..18781ef31f8 100644 --- a/src/Ai/Class/Warrior/Strategy/GenericWarriorStrategy.cpp +++ b/src/Ai/Class/Warrior/Strategy/GenericWarriorStrategy.cpp @@ -7,9 +7,26 @@ #include "Playerbots.h" -GenericWarriorStrategy::GenericWarriorStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) +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 From f5363c94655e293d530e0e05ba28a73bdcb2729e Mon Sep 17 00:00:00 2001 From: Crow Date: Sat, 4 Apr 2026 05:59:45 -0500 Subject: [PATCH 11/18] 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 5aa419c9276..f6e5bd2345b 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 563d7076987..8a9ed924837 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 @@ class ShatteringThrowTrigger : public Trigger // public: // SlamTrigger(PlayerbotAI* ai) : HasAuraTrigger(ai, "slam!") {} // }; + #endif From ca54cff6f522bd70eba5078f3ea804abf4309e94 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Sat, 4 Apr 2026 04:00:01 -0700 Subject: [PATCH 12/18] Bug fix. Edge case where bots would get stuck in cities. (#2269) ## Pull Request Description When I refactored flight destinations, I wanted to make where bots go more intentional. so I made it dependent on the allianceHubsPerLevelCache and hodeHubsPerLevelCache. This system relied on there being an innkeeper in each area that the bots would fly to. However, not every zone has an innkeeper, and so there was an odd situation where bots had nowhere to fly to. (Most notably at level 53.) This solves that by hardcoding the flightmasters in those areas into the cache. I also put back in the city teleport probability check which was forcing every bot to teleport to a city on level up. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## 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? - - [x] No - - [ ] Yes (**explain why**) - 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**) Debugging and comments. ## 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 --- src/Bot/RandomPlayerbotMgr.cpp | 13 ++++++++----- src/Mgr/Travel/TravelMgr.cpp | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/Bot/RandomPlayerbotMgr.cpp b/src/Bot/RandomPlayerbotMgr.cpp index 3fea369a52b..a772136e771 100644 --- a/src/Bot/RandomPlayerbotMgr.cpp +++ b/src/Bot/RandomPlayerbotMgr.cpp @@ -1768,13 +1768,16 @@ void RandomPlayerbotMgr::RandomTeleportForLevel(Player* bot) if (bot->InBattleground()) return; - std::vector locs = sTravelMgr.GetCityLocations(bot); - if (!locs.empty()) + if (bot->GetLevel() >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) { - RandomTeleport(bot, locs, true); - return; + std::vector locs = sTravelMgr.GetCityLocations(bot); + if (!locs.empty()) + { + RandomTeleport(bot, locs, true); + return; + } } - locs = sTravelMgr.GetTeleportLocations(bot); + std::vector locs = sTravelMgr.GetTeleportLocations(bot); if (!locs.empty()) { RandomTeleport(bot, locs, false); diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp index 7a5ac4f2e47..1868bc2e386 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -4419,6 +4419,7 @@ std::vector> TravelMgr::GetOptimalFlightDestinations(Player* bot->GetTeamId()); if (!fromNode) return validDestinations; + std::vector candidateLocations; if (bot->GetLevel() >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) candidateLocations = GetCityLocations(bot); @@ -4673,6 +4674,31 @@ void TravelMgr::PrepareDestinationCache() if (forAlliance) allianceFlightMasterCache[guid] = pos; flightMastersCount++; + + // Zones that have flight masters but no innkeepers — use flight master as hub + static const std::set zonesWithoutInnkeeper = { + 4, // Blasted Lands (52-57) + 16, // Azshara (45-52) + 28, // Western Plaguelands (50-60) + 46, // Burning Steppes (51-60) + 51, // Searing Gorge (45-51) + 361, // Felwood (47-57) + 490, // Un'Goro Crater (49-56) + 2817, // Crystalsong Forest (77-80) + 4197 // Wintergrasp (79-80) + }; + if (zonesWithoutInnkeeper.count(areaId)) + { + LevelBracket bracket = zone2LevelBracket[areaId]; + WorldPosition loc(mapId, x + cos(orient) * 5.0f, y + sin(orient) * 5.0f, z + 0.5f, orient + M_PI); + for (int i = bracket.low; i <= bracket.high; i++) + { + if (forHorde) + hordeHubsPerLevelCache[i].push_back(loc); + if (forAlliance) + allianceHubsPerLevelCache[i].push_back(loc); + } + } } else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) { From 4ba05962c9c85400020bf8ab74f9cf23feb5da10 Mon Sep 17 00:00:00 2001 From: dillyns <49765217+dillyns@users.noreply.github.com> Date: Sat, 4 Apr 2026 07:00:26 -0400 Subject: [PATCH 13/18] Add fireball fallback for frostfire bolt for frost mage (#2271) ## Pull Request Description Low level frost mages who do not yet have Frostfire Bolt never use Brain Freeze procs, since it is trying to use Frostfire Bolt. This adds Fireball as the fallback for Frostfire Bolt, so frost mages without frostfire bolt will still use Fireball when they get Brain Freeze ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes Get a frost mage without Frostfire Bolt who has the Brain Freeze talent. A level 60 frost mage works for example. Have them attack something until Brain Freeze procs. They should now use Fireball instead of ignoring it. ## 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? - - [x] No - - [ ] Yes (**explain why**) - 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? - - [x] No - - [ ] Yes (**explain below**) ## 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 --- src/Ai/Class/Mage/Strategy/FrostMageStrategy.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ai/Class/Mage/Strategy/FrostMageStrategy.cpp b/src/Ai/Class/Mage/Strategy/FrostMageStrategy.cpp index 34ed81dba1f..fe703f354d2 100644 --- a/src/Ai/Class/Mage/Strategy/FrostMageStrategy.cpp +++ b/src/Ai/Class/Mage/Strategy/FrostMageStrategy.cpp @@ -35,7 +35,7 @@ class FrostMageStrategyActionNodeFactory : public NamedObjectFactory static ActionNode* ice_lance(PlayerbotAI*) { return new ActionNode("ice lance", {}, {}, {}); } static ActionNode* fire_blast(PlayerbotAI*) { return new ActionNode("fire blast", {}, {}, {}); } static ActionNode* fireball(PlayerbotAI*) { return new ActionNode("fireball", {}, {}, {}); } - static ActionNode* frostfire_bolt(PlayerbotAI*) { return new ActionNode("frostfire bolt", {}, {}, {}); } + static ActionNode* frostfire_bolt(PlayerbotAI*) { return new ActionNode("frostfire bolt", {}, { NextAction("fireball") }, {}); } }; // ===== Single Target Strategy ===== From 4c9b0adb727e6e03288a340ed924cd3dd7d0e9ba Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 4 Apr 2026 13:00:51 +0200 Subject: [PATCH 14/18] Fire mage cc (#2281) ## Pull Request Description Added support for fire mage cc spells like: Dragon's Breath (disorient) and Blast Wave (knockback) ## How to Test the Changes 1. Invite fire mage to party 2. Add strategy `nc +duel` 3. Start duel and go near bot 4. Bot should use frost nova/dragon's breath/blast wave depends of cooldowns ## 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**) Mages cc by default - 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**) OpenCode, for research differences between CcStrategy implementation between cmangos and ac playerbots ## 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 --- src/Ai/Class/Mage/Strategy/GenericMageStrategy.cpp | 8 ++++++++ src/Bot/Factory/AiFactory.cpp | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Ai/Class/Mage/Strategy/GenericMageStrategy.cpp b/src/Ai/Class/Mage/Strategy/GenericMageStrategy.cpp index 75a63efa473..0e26c692d95 100644 --- a/src/Ai/Class/Mage/Strategy/GenericMageStrategy.cpp +++ b/src/Ai/Class/Mage/Strategy/GenericMageStrategy.cpp @@ -237,6 +237,14 @@ void MageBoostStrategy::InitTriggers(std::vector& triggers) void MageCcStrategy::InitTriggers(std::vector& triggers) { triggers.push_back(new TriggerNode("polymorph", { NextAction("polymorph", 30.0f) })); + + Player* bot = botAI->GetBot(); + int tab = AiFactory::GetPlayerSpecTab(bot); + if (tab == MAGE_TAB_FIRE) + { + triggers.push_back(new TriggerNode("enemy too close for spell", {NextAction("dragon's breath", ACTION_INTERRUPT + 1)})); + triggers.push_back(new TriggerNode("enemy is close", {NextAction("blast wave", ACTION_INTERRUPT)})); + } } void MageAoeStrategy::InitTriggers(std::vector& triggers) diff --git a/src/Bot/Factory/AiFactory.cpp b/src/Bot/Factory/AiFactory.cpp index 6121789a940..a821886f97e 100644 --- a/src/Bot/Factory/AiFactory.cpp +++ b/src/Bot/Factory/AiFactory.cpp @@ -311,7 +311,7 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa else engine->addStrategiesNoInit("frost", nullptr); - engine->addStrategiesNoInit("dps", "dps assist", "cure", "aoe", nullptr); + engine->addStrategiesNoInit("dps", "dps assist", "cure", "cc", "aoe", nullptr); break; case CLASS_WARRIOR: if (tab == WARRIOR_TAB_PROTECTION) From 7b04c569562c29f82662fe2b7a56210607946d0a Mon Sep 17 00:00:00 2001 From: Benjamin Jackson <38561765+heyitsbench@users.noreply.github.com> Date: Sat, 4 Apr 2026 07:22:27 -0400 Subject: [PATCH 15/18] Add default case to mount initialization for bots. (#2276) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Pull Request Description This PR adds a default case for the mount initialization function for player bots, allowing custom race additions to not crash when added to an AzerothCore server (such as [`mod-worgoblin`](https://github.com/heyitsbench/mod-worgoblin)). ## Feature Evaluation This feature ideally does not get touched in standard `mod-playerbots` usage, as it only adds a case to a switch statement, and would not be getting taken with Blizzlike data (which the vast majority of users likely use). ## How to Test the Changes 1. Add custom races to your AzerothCore instance (most easily accomplished with the above linked module). 2. Start the server and configure it to create random player bots (that fall under the added custom races). 3. Observe no crash. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [X] No, not at all - Does this change modify default bot behavior? - - [X] No - Does this change add new decision branches or increase maintenance complexity? - - [X] Yes (**explain below**) This does add another path in a switch statement. It also creates another set of data for that case. This could have been avoided by consolidating the human/orc cases into the default case, but I elected not to do that in case players make their own changes. If preferred, I can definitely consolidate the cases to not have those redundant data points. ## Messages to Translate - Does this change add bot messages to translate? - - [X] No ## AI Assistance - Was AI assistance used while working on this change? - - [X] No ## Final Checklist - - [X] Stability is not compromised. - - [X] Performance impact is understood, tested, and acceptable. - - [X] Added logic complexity is justified and explained. - - [ ] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers Pretty please? 🥹 --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> --- src/Bot/Factory/PlayerbotFactory.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Bot/Factory/PlayerbotFactory.cpp b/src/Bot/Factory/PlayerbotFactory.cpp index b66623c5825..11f301feb4f 100644 --- a/src/Bot/Factory/PlayerbotFactory.cpp +++ b/src/Bot/Factory/PlayerbotFactory.cpp @@ -3032,6 +3032,17 @@ void PlayerbotFactory::InitMounts() slow = {33660, 35020, 35022, 35018}; fast = {35025, 35025, 35027}; break; + default: + if (bot->GetTeamId() == TEAM_HORDE) + { // Orc mounts + slow = {470, 6648, 458, 472}; + fast = {23228, 23227, 23229}; + } + else // Human mounts + { + slow = {6654, 6653, 580}; + fast = {23250, 23252, 23251}; + } } switch (bot->GetTeamId()) From a4a3a3d964aa87605c534336ef847da797000279 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 4 Apr 2026 22:10:14 +0200 Subject: [PATCH 16/18] Fix for github action translation labeling (#2285) Maintenance PR --- .github/workflows/label_translation-pr.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/label_translation-pr.yml b/.github/workflows/label_translation-pr.yml index 5dbe92fc5c6..cadb0a46e7e 100644 --- a/.github/workflows/label_translation-pr.yml +++ b/.github/workflows/label_translation-pr.yml @@ -35,4 +35,5 @@ jobs: GH_TOKEN: ${{ github.token }} run: | gh pr edit ${{ github.event.pull_request.number }} \ + --repo ${{ github.repository }} \ --add-label "Added translation" From 9ebccc23a2df0e0a56932c7b74f3d0f81b67ccbb Mon Sep 17 00:00:00 2001 From: bash Date: Sun, 5 Apr 2026 11:25:53 +0200 Subject: [PATCH 17/18] clearified the ac --- conf/playerbots.conf.dist | 67 +++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index ff1b393b58b..ccb392e1aef 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -865,28 +865,43 @@ AiPlayerbot.ExcludedHunterPetFamilies = "" # # #################################################################################################### - #################################################################################################### # ACTIVITY # -# BotActiveAlone (%) -# - Determines the percentage of bots that remain active when no real players are nearby. -# - Default is 40% (which is practise kinda translates into 45-50%). -# - If `botActiveAloneSmartScale` is enabled, automatically temporarily down scale activity based on latency. -# - 40% will be activated in a random rotation for the amount of seconds specified in . -# - There are multiple conditions when bots will forced to be active e.g. when in BG/instance/attacked, some are configurable below. -#- When 100$ all bots will be active without any rotation or logic applied but comes with performance hit. +# BotActiveAlone +# - Controls how many bots are active when no real players are nearby. +# - Think of it as a rough percentage: 10 means approximately 10% of bots will be active. +# Not exact — the actual number may vary slightly per rotation cycle. +# - The active bots rotate: every a different set of bots takes a turn. +# - The real number of active bots will always be higher than this value, because bots in +# combat, dungeons, battlegrounds, LFG queue, groups with real players, etc. are always +# forced active on top of this (see force rules below). +# - Set to 100 (with SmartScale off) = all bots always active. Maximum server load. +# - Set to 0 = only bots that match a force rule below will be active. +# +# BotActiveAloneDurationSeconds +# - How often the active roster rotates (in seconds). A different group of bots wakes up +# and the previous group may go idle. +# - This is a minimum, not exact. If a bot is in combat or meets any force rule when the +# rotation happens, it stays active until those conditions end — it won't be cut off +# mid-fight just because its turn expired. # AiPlayerbot.BotActiveAlone = 10 AiPlayerbot.BotActiveAloneDurationSeconds = 30 -# Some additional rules that enforces the bot to be active # -# - bot is within this distance from a real player. -# - bot is in the same zone as a real player. -# - bot is in the same continent as a real player. -# - bot is a real player's friend. -# - bot is in a real player's guild. +# Force-active rules (1 = on, 0 = off) +# These override the percentage above. If any of these conditions is true, the bot stays active. +# +# InRadius - A real player is within this many yards (set to 0 to disable). +# InZone - A real player is in the same zone (e.g. Elwynn Forest). +# InMap - A real player is on the same continent (e.g. Eastern Kingdoms). +# IsFriend - A real player has this bot on their friends list. +# InGuild - This bot is in a guild that has a real player in it. +# +# Bots are also always forced active (not configurable) when: +# in combat, inside a dungeon/raid/BG, in a BG or LFG queue, +# grouped with a real player, or controlled by a real player. # AiPlayerbot.BotActiveAloneForceWhenInRadius = 150 AiPlayerbot.BotActiveAloneForceWhenInZone = 1 @@ -894,15 +909,21 @@ AiPlayerbot.BotActiveAloneForceWhenInMap = 0 AiPlayerbot.BotActiveAloneForceWhenIsFriend = 0 AiPlayerbot.BotActiveAloneForceWhenInGuild = 1 -# SmartScale (automatic scaling of percentage of active bots based on latency) -# The default is 1. When enabled (smart) scales the 'BotActiveAlone' value. -# (The scaling will be overruled by the BotActiveAloneForceWhen...rules) -# -# Limitfloor - when DIFF (latency) is above floor, activity scaling begins -# LimitCeiling - when DIFF (latency) is above ceiling, activity is 0% -# -# MinLevel - only apply scaling when level is above or equal to min(bot)Level -# MaxLevel - only apply scaling when level is lower or equal of max(bot)Level +# SmartScale — automatically reduces active bots when the server is struggling. +# Monitors the server's update time (how long each server tick takes in milliseconds). +# When the server slows down, fewer bots are kept active to reduce load. +# +# Floor (default 50ms) - Below this, no reduction. Server is running fine. +# Ceiling (default 200ms) - At or above this, all non-forced bots are paused. +# Between floor and ceiling, activity scales down gradually. +# Example: BotActiveAlone=10, floor=50, ceiling=200 +# Server at 50ms → ~10% active (no reduction) +# Server at 125ms → ~5% active (half reduction) +# Server at 200ms → 0% active (only forced bots remain) +# +# MinLevel/MaxLevel — only bots within this level range are affected by SmartScale. +# Bots outside the range always use the full BotActiveAlone value. +# Force rules always win over SmartScale. # AiPlayerbot.botActiveAloneSmartScale = 1 AiPlayerbot.botActiveAloneSmartScaleDiffLimitfloor = 50 From 5fccb8cb57dfb5fe0375070db357c77cd7b54bc3 Mon Sep 17 00:00:00 2001 From: bash Date: Sun, 5 Apr 2026 21:13:47 +0200 Subject: [PATCH 18/18] Activity timing init fix, some activity performances fix and additional minor bug fixes --- src/Bot/PlayerbotAI.cpp | 208 +++++++++++++++++----------------------- 1 file changed, 86 insertions(+), 122 deletions(-) diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 5ea8b3323f8..87f18e5b97b 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -119,7 +119,7 @@ PlayerbotAI::PlayerbotAI() for (uint8 i = 0; i < MAX_ACTIVITY_TYPE; i++) { - allowActiveCheckTimer[i] = time(nullptr); + allowActiveCheckTimer[i] = 0; allowActive[i] = false; } } @@ -137,19 +137,20 @@ PlayerbotAI::PlayerbotAI(Player* bot) for (uint8 i = 0; i < MAX_ACTIVITY_TYPE; i++) { - allowActiveCheckTimer[i] = time(nullptr); + allowActiveCheckTimer[i] = 0; allowActive[i] = false; } accountId = bot->GetSession()->GetAccountId(); - aiObjectContext = AiFactory::createAiObjectContext(bot, this); engines[BOT_STATE_COMBAT] = AiFactory::createCombatEngine(bot, this, aiObjectContext); engines[BOT_STATE_NON_COMBAT] = AiFactory::createNonCombatEngine(bot, this, aiObjectContext); engines[BOT_STATE_DEAD] = AiFactory::createDeadEngine(bot, this, aiObjectContext); + if (sPlayerbotAIConfig.applyInstanceStrategies) ApplyInstanceStrategies(bot->GetMapId()); + currentEngine = engines[BOT_STATE_NON_COMBAT]; currentState = BOT_STATE_NON_COMBAT; @@ -445,9 +446,11 @@ void PlayerbotAI::UpdateAIInternal([[maybe_unused]] uint32 elapsed, bool minimal if (!bot->GetMap()) return; // instances are created and destroyed on demand + // kinda expensive call to make on every single updateAI, do we really need this information? std::string const mapString = WorldPosition(bot).isOverworld() ? std::to_string(bot->GetMapId()) : "I"; PerfMonitorOperation* pmo = sPerfMonitor.start(PERF_MON_TOTAL, "PlayerbotAI::UpdateAIInternal " + mapString); + ExternalEventHelper helper(aiObjectContext); // chat replies @@ -1202,23 +1205,18 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet) if (HasRealPlayerMaster() && guid1 != GetMaster()->GetGUID()) return; + auto itemIds = GetChatHelper()->ExtractAllItemIds(message); if (message.starts_with(sPlayerbotAIConfig.toxicLinksPrefix) && - (GetChatHelper()->ExtractAllItemIds(message).size() > 0 || - GetChatHelper()->ExtractAllQuestIds(message).size() > 0) && + (itemIds.size() > 0 || GetChatHelper()->ExtractAllQuestIds(message).size() > 0) && sPlayerbotAIConfig.toxicLinksRepliesChance) { if (urand(0, 50) > 0 || urand(1, 100) > sPlayerbotAIConfig.toxicLinksRepliesChance) - { return; - } } - else if ((GetChatHelper()->ExtractAllItemIds(message).count(19019) && - sPlayerbotAIConfig.thunderfuryRepliesChance)) + else if (itemIds.count(19019) && sPlayerbotAIConfig.thunderfuryRepliesChance) { if (urand(0, 60) > 0 || urand(1, 100) > sPlayerbotAIConfig.thunderfuryRepliesChance) - { return; - } } else { @@ -4459,7 +4457,6 @@ GuilderType PlayerbotAI::GetGuilderType() bool PlayerbotAI::HasPlayerNearby(WorldPosition* pos, float range) { float sqRange = range * range; - bool nearPlayer = false; for (auto& player : sRandomPlayerbotMgr.GetPlayers()) { if (!player->IsGameMaster() || player->isGMVisible()) @@ -4468,19 +4465,18 @@ bool PlayerbotAI::HasPlayerNearby(WorldPosition* pos, float range) continue; if (pos->sqDistance(WorldPosition(player)) < sqRange) - nearPlayer = true; + return true; - // if player is far check farsight/cinematic camera WorldObject* viewObj = player->GetViewpoint(); if (viewObj && viewObj != player) { if (pos->sqDistance(WorldPosition(viewObj)) < sqRange) - nearPlayer = true; + return true; } } } - return nearPlayer; + return false; } bool PlayerbotAI::HasPlayerNearby(float range) @@ -4491,12 +4487,12 @@ bool PlayerbotAI::HasPlayerNearby(float range) bool PlayerbotAI::HasManyPlayersNearby(uint32 trigerrValue, float range) { - float sqRange = range * range; uint32 found = 0; for (auto& player : sRandomPlayerbotMgr.GetPlayers()) { - if ((!player->IsGameMaster() || player->isGMVisible()) && ServerFacade::instance().GetDistance2d(player, bot) < sqRange) + if ((!player->IsGameMaster() || player->isGMVisible()) && + ServerFacade::instance().GetDistance2d(player, bot) < range) { found++; @@ -4536,11 +4532,8 @@ inline bool HasRealPlayers(Map* map) inline bool ZoneHasRealPlayers(Player* bot) { - Map* map = bot->GetMap(); - if (!bot || !map) - { + if (!bot) return false; - } for (Player* player : sRandomPlayerbotMgr.GetPlayers()) { @@ -4548,17 +4541,13 @@ inline bool ZoneHasRealPlayers(Player* bot) continue; if (player->IsGameMaster() && !player->IsVisible()) - { continue; - } if (player->GetZoneId() == bot->GetZoneId()) { PlayerbotAI* botAI = GET_PLAYERBOT_AI(player); if (!botAI || botAI->IsRealPlayer() || botAI->HasRealPlayerMaster()) - { return true; - } } } @@ -4567,95 +4556,53 @@ inline bool ZoneHasRealPlayers(Player* bot) bool PlayerbotAI::AllowActive(ActivityType activityType) { - // Early return if bot is in invalid state if (!bot || !bot->GetSession() || !bot->IsInWorld() || bot->IsBeingTeleported() || bot->GetSession()->isLogingOut() || bot->IsDuringRemoveFromWorld()) return false; - // when botActiveAlone is 100% and smartScale disabled if (sPlayerbotAIConfig.botActiveAlone >= 100 && !sPlayerbotAIConfig.botActiveAloneSmartScale) - { return true; - } - // Is in combat. Always defend yourself. if (activityType != OUT_OF_PARTY_ACTIVITY && activityType != PACKET_ACTIVITY) { if (bot->IsInCombat()) - { return true; - } } - // only keep updating till initializing time has completed, - // which prevents unneeded expensive GameTime calls. if (_isBotInitializing) { _isBotInitializing = GameTime::GetUptime().count() < sPlayerbotAIConfig.maxRandomBots * 0.11; - - // no activity allowed during bot initialization if (_isBotInitializing) - { return false; - } } - // General exceptions if (activityType == PACKET_ACTIVITY) - { return true; - } - // bg, raid, dungeon if (!WorldPosition(bot).isOverworld()) - { return true; - } - // bot map has active players. + // bot map has active players (uses map->GetPlayers(), separate list) if (sPlayerbotAIConfig.BotActiveAloneForceWhenInMap) { if (HasRealPlayers(bot->GetMap())) - { return true; - } - } - - // bot zone has active players. - if (sPlayerbotAIConfig.BotActiveAloneForceWhenInZone) - { - if (ZoneHasRealPlayers(bot)) - { - return true; - } } - // when in real guild + // when in real guild (no player iteration, cheap lookup) if (sPlayerbotAIConfig.BotActiveAloneForceWhenInGuild) { if (IsInRealGuild()) - { return true; - } - } - - // Player is near. Always active. - if (HasPlayerNearby(sPlayerbotAIConfig.BotActiveAloneForceWhenInRadius)) - { - return true; } - // Has player master. Always active. if (GetMaster()) { PlayerbotAI* masterBotAI = GET_PLAYERBOT_AI(GetMaster()); if (!masterBotAI || masterBotAI->IsRealPlayer()) - { return true; - } } - // if grouped up Group* group = bot->GetGroup(); if (group) { @@ -4666,116 +4613,133 @@ bool PlayerbotAI::AllowActive(ActivityType activityType) continue; if (member == bot) - { continue; - } PlayerbotAI* memberBotAI = GET_PLAYERBOT_AI(member); - { - if (!memberBotAI || memberBotAI->HasRealPlayerMaster()) - { - return true; - } - } + if (!memberBotAI || memberBotAI->HasRealPlayerMaster()) + return true; if (group->IsLeader(member->GetGUID())) { if (!memberBotAI->AllowActivity(PARTY_ACTIVITY)) - { return false; - } } } } - // In bg queue. Speed up bg queue/join. if (bot->InBattlegroundQueue()) - { return true; - } bool isLFG = false; if (group) { if (sLFGMgr->GetState(group->GetGUID()) != lfg::LFG_STATE_NONE) - { isLFG = true; - } } if (sLFGMgr->GetState(bot->GetGUID()) != lfg::LFG_STATE_NONE) - { isLFG = true; - } + if (isLFG) - { return true; - } - // HasFriend - if (sPlayerbotAIConfig.BotActiveAloneForceWhenIsFriend) + // ========================================================================= + // Combined player loop + // ========================================================================= + bool checkZone = sPlayerbotAIConfig.BotActiveAloneForceWhenInZone; + bool checkRadius = sPlayerbotAIConfig.BotActiveAloneForceWhenInRadius > 0; + bool checkFriend = sPlayerbotAIConfig.BotActiveAloneForceWhenIsFriend; + + // Original IsFriend guard: force inactive if GUID is invalid + if (checkFriend && !bot->GetGUID()) + return false; + + if (checkZone || checkRadius || checkFriend) { - // shouldnt be needed analyse in future - if (!bot->GetGUID()) - return false; + uint32 botMapId = bot->GetMapId(); + uint32 botZoneId = checkZone ? bot->GetZoneId() : 0; + + float sqRange = 0.0f; + WorldPosition botPos(bot); + if (checkRadius) + { + float range = static_cast(sPlayerbotAIConfig.BotActiveAloneForceWhenInRadius); + sqRange = range * range; + } for (auto& player : sRandomPlayerbotMgr.GetPlayers()) { - if (!player || !player->GetSession() || !player->IsInWorld() || player->IsDuringRemoveFromWorld() || - player->GetSession()->isLogingOut()) + if (!player) continue; - PlayerbotAI* playerAI = GET_PLAYERBOT_AI(player); - if (!playerAI || !playerAI->IsRealPlayer()) - continue; + bool sameMap = (player->GetMapId() == botMapId); + bool isGM = player->IsGameMaster(); - // if a real player has the bot as a friend - PlayerSocial* social = player->GetSocial(); - if (social && social->HasFriend(bot->GetGUID())) - return true; + if (sameMap) + { + // Zone check (original filter: IsVisible, real-player check) + if (checkZone && !(isGM && !player->IsVisible()) && player->GetZoneId() == botZoneId) + { + PlayerbotAI* ai = GET_PLAYERBOT_AI(player); + if (!ai || ai->IsRealPlayer() || ai->HasRealPlayerMaster()) + return true; + } + + // Radius check (original filter: isGMVisible, no real-player check) + if (checkRadius && (!isGM || player->isGMVisible())) + { + if (botPos.sqDistance(WorldPosition(player)) < sqRange) + return true; // FIX: original never early-returned + + WorldObject* viewObj = player->GetViewpoint(); + if (viewObj && viewObj != player && botPos.sqDistance(WorldPosition(viewObj)) < sqRange) + return true; // FIX: original never early-returned + } + } + + // Friend check (original filter: no GM check, real-player only, cross-map) + if (checkFriend) + { + if (!player->GetSession() || !player->IsInWorld() || player->IsDuringRemoveFromWorld() || + player->GetSession()->isLogingOut()) + continue; + + PlayerbotAI* ai = GET_PLAYERBOT_AI(player); + if (!ai || ai->IsRealPlayer()) + { + PlayerSocial* social = player->GetSocial(); + if (social && social->HasFriend(bot->GetGUID())) + return true; + } + } } } - // Force the bots to spread + // Force the bots to spread if (activityType == OUT_OF_PARTY_ACTIVITY || activityType == GRIND_ACTIVITY) { if (HasManyPlayersNearby(10, 40)) - { return true; - } } - // Bots don't need react to PathGenerator activities if (activityType == DETAILED_MOVE_ACTIVITY) - { return false; - } if (sPlayerbotAIConfig.botActiveAlone <= 0) - { return false; - } // ####################################################################################### - // All mandatory conditations are checked to be active or not, from here the remaining - // situations are usable for scaling when enabled. + // Scaling logic // ####################################################################################### - - // Base percentage of bots to be active uint32 mod = sPlayerbotAIConfig.botActiveAlone > 100 ? 100 : sPlayerbotAIConfig.botActiveAlone; - - // Apply SmartScale if enabled if (sPlayerbotAIConfig.botActiveAloneSmartScale && bot->GetLevel() >= sPlayerbotAIConfig.botActiveAloneSmartScaleWhenMinLevel && bot->GetLevel() <= sPlayerbotAIConfig.botActiveAloneSmartScaleWhenMaxLevel) { - mod = AutoScaleActivity(mod); // mod reflects on latency throttling + mod = AutoScaleActivity(mod); } - // Get deterministic bucket + timeSlot uint32 ActivityNumber = GetFixedBotNumber(100); - - // Check if this bot is in the active set - return ActivityNumber < mod; // mod is directly the number of bots active (0–100) + return ActivityNumber < mod; } bool PlayerbotAI::AllowActivity(ActivityType activityType, bool checkNow)