From 7eff13a9f8a32b3d6711c53feb2f053842974f98 Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 12 Jun 2026 16:57:49 -0500 Subject: [PATCH] Implement remaining racials* + minor modifications to racials strategy (#2456) ## Pull Request Description *The title is a lie because I did not implement Cannibalize. I don't think that one is ever going to be worth the processing cost to find nearby corpses. Anyway: - Implemented Stoneform and PoisonDiseaseBleedTrigger. - Implemented Escape Artist and MovementImpairedTrigger. The trigger excludes Stealth and Prowl, like with Hand of Freedom. I realize Gnomes cannot normally be Druids, but I have left Prowl in, in case somebody uses a mod to make it happen. I could easily be persuaded to remove the Prowl exclusion, though. - Implemented Rogue version of Arcane Torrent, used based on the low energy trigger. - Implemented DK version of Arcane Torrent; I don't know shit about DKs, and it doesn't look like there is any runic-energy-related trigger, so I just made this a generic boost trigger. That means it's used on cooldown while the boost strategy is active and the encounter trips the balance that causes boost trigger to fire. It's not great, but it's better than not using the ability at all. - All racials, plus Lifeblood (which is also under the racials strategy), are gated behind HasSpell() checks. This stops bots from evaluating racials they don't have and reduces log spam. - Removed the ActionNodeFactory for racials, which was used only for Lifeblood with Gift of the Naaru alternative. I split those abilities into their own TriggerNodes, behind their own spell checks. - Increased threshold for Lifeblood and Gift of the Naaru use to medium health instead of low health. These are not strong heals, plus they are HoTs, so bots may as well get more use out of them. Gift of the Naaru can be used on allies as well, but I did not implement it (because of the effort and because I don't think it's worth the processing cost anyway for such an insignificant heal). - Removed spell checks from EMFH and WoTF IsPossible() since they are checked before the trigger is evaluated. - Removed IsUseful() overrides from EMFH and WoTF since the checks are already in the triggers. If we should have the checks done twice anyway, LMK. ## 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 I tested: - EMFH with Fear - Escape Artist with Entangling Roots - Stoneform with Rupture - Arcane Torrent (Rogue version) I did not test the DK version of Arcane Torrent. I did not check all possible conditions for the CC breaks, but it can be done most easily by self-botting and using the .aura GM command. The racials strategy is a default combat strategy only so for testing at least you may want to add nc +racials to make it easier. ## 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**) New triggers always add more processing, but this stuff is insignificant. - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Racials is a default combat strategy so new racials/changes to existing racials modifies default behavior. - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## 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] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Ai/Base/ActionContext.h | 4 + src/Ai/Base/Actions/GenericSpellActions.cpp | 23 +--- src/Ai/Base/Actions/GenericSpellActions.h | 18 ++- src/Ai/Base/Strategy/RacialsStrategy.cpp | 129 ++++++++++++++------ src/Ai/Base/Trigger/GenericTriggers.cpp | 13 ++ src/Ai/Base/Trigger/GenericTriggers.h | 16 +++ src/Ai/Base/TriggerContext.h | 4 + 7 files changed, 149 insertions(+), 58 deletions(-) diff --git a/src/Ai/Base/ActionContext.h b/src/Ai/Base/ActionContext.h index 3026cfd50..7cde4dbfd 100644 --- a/src/Ai/Base/ActionContext.h +++ b/src/Ai/Base/ActionContext.h @@ -175,6 +175,8 @@ public: creators["berserking"] = &ActionContext::berserking; creators["every man for himself"] = &ActionContext::every_man_for_himself; creators["will of the forsaken"] = &ActionContext::will_of_the_forsaken; + creators["stoneform"] = &ActionContext::stoneform; + creators["escape artist"] = &ActionContext::escape_artist; creators["use trinket"] = &ActionContext::use_trinket; creators["auto talents"] = &ActionContext::auto_talents; creators["auto share quest"] = &ActionContext::auto_share_quest; @@ -380,6 +382,8 @@ private: static Action* berserking(PlayerbotAI* botAI) { return new CastBerserkingAction(botAI); } static Action* every_man_for_himself(PlayerbotAI* botAI) { return new CastEveryManForHimselfAction(botAI); } static Action* will_of_the_forsaken(PlayerbotAI* botAI) { return new CastWillOfTheForsakenAction(botAI); } + static Action* stoneform(PlayerbotAI* botAI) { return new CastStoneformAction(botAI); } + static Action* escape_artist(PlayerbotAI* botAI) { return new CastEscapeArtistAction(botAI); } static Action* use_trinket(PlayerbotAI* botAI) { return new UseTrinketAction(botAI); } static Action* auto_talents(PlayerbotAI* botAI) { return new AutoSetTalentsAction(botAI); } static Action* auto_share_quest(PlayerbotAI* ai) { return new AutoShareQuestAction(ai); } diff --git a/src/Ai/Base/Actions/GenericSpellActions.cpp b/src/Ai/Base/Actions/GenericSpellActions.cpp index 657fda7cd..50621484f 100644 --- a/src/Ai/Base/Actions/GenericSpellActions.cpp +++ b/src/Ai/Base/Actions/GenericSpellActions.cpp @@ -491,32 +491,13 @@ bool CastVehicleSpellAction::Execute(Event /*event*/) bool CastEveryManForHimselfAction::isPossible() { uint32 spellId = AI_VALUE2(uint32, "spell id", spell); - return spellId && bot->HasSpell(spellId) && !HasSpellOrCategoryCooldown(bot, spellId); -} - -bool CastEveryManForHimselfAction::isUseful() -{ - return (bot->HasAuraType(SPELL_AURA_MOD_STUN) || - bot->HasAuraType(SPELL_AURA_MOD_FEAR) || - bot->HasAuraType(SPELL_AURA_MOD_ROOT) || - bot->HasAuraType(SPELL_AURA_MOD_CONFUSE) || - bot->HasAuraType(SPELL_AURA_MOD_CHARM)) - && CastSpellAction::isUseful(); + return spellId && !HasSpellOrCategoryCooldown(bot, spellId); } bool CastWillOfTheForsakenAction::isPossible() { uint32 spellId = AI_VALUE2(uint32, "spell id", spell); - return spellId && bot->HasSpell(spellId) && !HasSpellOrCategoryCooldown(bot, spellId); -} - -bool CastWillOfTheForsakenAction::isUseful() -{ - return (bot->HasAuraType(SPELL_AURA_MOD_FEAR) || - bot->HasAuraType(SPELL_AURA_MOD_CHARM) || - bot->HasAuraType(SPELL_AURA_AOE_CHARM) || - bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP)) - && CastSpellAction::isUseful(); + return spellId && !HasSpellOrCategoryCooldown(bot, spellId); } bool UseTrinketAction::Execute(Event /*event*/) diff --git a/src/Ai/Base/Actions/GenericSpellActions.h b/src/Ai/Base/Actions/GenericSpellActions.h index c17d96907..1d6e464ef 100644 --- a/src/Ai/Base/Actions/GenericSpellActions.h +++ b/src/Ai/Base/Actions/GenericSpellActions.h @@ -313,7 +313,6 @@ public: std::string const GetTargetName() override { return "self target"; } bool isPossible() override; - bool isUseful() override; }; class CastWillOfTheForsakenAction : public CastSpellAction @@ -323,7 +322,22 @@ public: std::string const GetTargetName() override { return "self target"; } bool isPossible() override; - bool isUseful() override; +}; + +class CastStoneformAction : public CastSpellAction +{ +public: + CastStoneformAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "stoneform") {} + + std::string const GetTargetName() override { return "self target"; } +}; + +class CastEscapeArtistAction : public CastSpellAction +{ +public: + CastEscapeArtistAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "escape artist") {} + + std::string const GetTargetName() override { return "self target"; } }; class UseTrinketAction : public Action diff --git a/src/Ai/Base/Strategy/RacialsStrategy.cpp b/src/Ai/Base/Strategy/RacialsStrategy.cpp index b5a84bbce..3382d3291 100644 --- a/src/Ai/Base/Strategy/RacialsStrategy.cpp +++ b/src/Ai/Base/Strategy/RacialsStrategy.cpp @@ -4,45 +4,104 @@ */ #include "RacialsStrategy.h" +#include "Playerbots.h" -class RacialsStrategyActionNodeFactory : public NamedObjectFactory +namespace { -public: - RacialsStrategyActionNodeFactory() { creators["lifeblood"] = &lifeblood; } - -private: - static ActionNode* lifeblood(PlayerbotAI* /*botAI*/) - { - return new ActionNode("lifeblood", - /*P*/ {}, - /*A*/ { NextAction("gift of the naaru") }, - /*C*/ {}); - } -}; - -void RacialsStrategy::InitTriggers(std::vector& triggers) -{ - triggers.push_back( - new TriggerNode("low health", { NextAction("lifeblood", ACTION_NORMAL + 5) })); - triggers.push_back( - new TriggerNode("medium aoe", { NextAction("war stomp", ACTION_NORMAL + 5) })); - triggers.push_back(new TriggerNode( - "low mana", { NextAction("arcane torrent", ACTION_NORMAL + 5) })); - - triggers.push_back(new TriggerNode( - "generic boost", { NextAction("blood fury", ACTION_NORMAL + 5), - NextAction("berserking", ACTION_NORMAL + 5), - NextAction("use trinket", ACTION_NORMAL + 4) })); - - triggers.push_back(new TriggerNode( - "loss of control", { NextAction("every man for himself", ACTION_EMERGENCY + 1) })); - - triggers.push_back(new TriggerNode( - "fear charm sleep", { NextAction("will of the forsaken", ACTION_EMERGENCY + 1) })); - + constexpr uint32 SPELL_ARCANE_TORRENT_ENERGY = 25046; + constexpr uint32 SPELL_ARCANE_TORRENT_MANA = 28730; + constexpr uint32 SPELL_ARCANE_TORRENT_RUNIC_POWER = 50613; + constexpr uint32 SPELL_WAR_STOMP = 20549; + constexpr uint32 SPELL_BERSERKING = 26297; + constexpr uint32 SPELL_EVERY_MAN_FOR_HIMSELF = 59752; + constexpr uint32 SPELL_WILL_OF_THE_FORSAKEN = 7744; + constexpr uint32 SPELL_STONEFORM = 20594; + constexpr uint32 SPELL_ESCAPE_ARTIST = 20589; } RacialsStrategy::RacialsStrategy(PlayerbotAI* botAI) : Strategy(botAI) { - actionNodeFactories.Add(new RacialsStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed +} + +void RacialsStrategy::InitTriggers(std::vector& triggers) +{ + Player* bot = botAI->GetBot(); + + if (bot->HasSpell(SPELL_ARCANE_TORRENT_MANA)) + { + triggers.push_back(new TriggerNode( + "low mana", { NextAction("arcane torrent", ACTION_NORMAL + 5) })); + } + + if (bot->HasSpell(SPELL_ARCANE_TORRENT_ENERGY)) + { + triggers.push_back(new TriggerNode( + "low energy", { NextAction("arcane torrent", ACTION_NORMAL + 5) })); + } + + if (bot->HasSpell(SPELL_ARCANE_TORRENT_RUNIC_POWER)) + { + // No low runic power trigger exists; this trigger should be modified if one is added + triggers.push_back(new TriggerNode( + "generic boost", { NextAction("arcane torrent", ACTION_NORMAL + 5) })); + } + + if (bot->HasSpell(SPELL_WAR_STOMP)) + { + triggers.push_back(new TriggerNode( + "medium aoe", { NextAction("war stomp", ACTION_NORMAL + 5) })); + } + + if (bot->HasSpell(SPELL_BERSERKING)) + { + triggers.push_back(new TriggerNode( + "generic boost", { NextAction("berserking", ACTION_NORMAL + 5) })); + } + + if (bot->HasSpell(SPELL_EVERY_MAN_FOR_HIMSELF)) + { + triggers.push_back(new TriggerNode( + "loss of control", { NextAction("every man for himself", ACTION_EMERGENCY + 1) })); + } + + if (bot->HasSpell(SPELL_WILL_OF_THE_FORSAKEN)) + { + triggers.push_back(new TriggerNode( + "fear charm sleep", { NextAction("will of the forsaken", ACTION_EMERGENCY + 1) })); + } + + if (bot->HasSpell(SPELL_STONEFORM)) + { + triggers.push_back(new TriggerNode( + "poison disease bleed", { NextAction("stoneform", ACTION_DISPEL) })); + } + + if (bot->HasSpell(SPELL_ESCAPE_ARTIST)) + { + triggers.push_back(new TriggerNode( + "movement impaired", { NextAction("escape artist", ACTION_EMERGENCY + 1) })); + } + + if (botAI->HasSpell("blood fury")) + { + triggers.push_back(new TriggerNode( + "generic boost", { NextAction("blood fury", ACTION_NORMAL + 5) })); + } + + if (botAI->HasSpell("gift of the naaru")) + { + // Currently targets self only + triggers.push_back(new TriggerNode( + "medium health", { NextAction("gift of the naaru", ACTION_LIGHT_HEAL + 5) })); + } + + if (botAI->HasSpell("lifeblood")) + { + triggers.push_back(new TriggerNode( + "medium health", { NextAction("lifeblood", ACTION_LIGHT_HEAL + 5) })); + } + + triggers.push_back(new TriggerNode( + "generic boost", { NextAction("use trinket", ACTION_NORMAL + 4) })); } diff --git a/src/Ai/Base/Trigger/GenericTriggers.cpp b/src/Ai/Base/Trigger/GenericTriggers.cpp index aac920b5b..308146467 100644 --- a/src/Ai/Base/Trigger/GenericTriggers.cpp +++ b/src/Ai/Base/Trigger/GenericTriggers.cpp @@ -491,6 +491,19 @@ bool FearSleepSapTrigger::IsActive() bot->HasAuraWithMechanic(1 << MECHANIC_SAPPED); } +bool PoisonDiseaseBleedTrigger::IsActive() +{ + return botAI->HasAuraToDispel(bot, DISPEL_POISON) || + botAI->HasAuraToDispel(bot, DISPEL_DISEASE) || + bot->HasAuraWithMechanic(1 << MECHANIC_BLEED); +} + +bool MovementImpairedTrigger::IsActive() +{ + return botAI->IsMovementImpaired(bot) && + !botAI->HasAnyAuraOf(bot, "stealth", "prowl", nullptr); +} + bool HasAuraStackTrigger::IsActive() { return botAI->GetAura(getName(), GetTarget(), false, true, stack); diff --git a/src/Ai/Base/Trigger/GenericTriggers.h b/src/Ai/Base/Trigger/GenericTriggers.h index 3e662eb3b..418949a85 100644 --- a/src/Ai/Base/Trigger/GenericTriggers.h +++ b/src/Ai/Base/Trigger/GenericTriggers.h @@ -752,6 +752,22 @@ public: bool IsActive() override; }; +class PoisonDiseaseBleedTrigger : public Trigger +{ +public: + PoisonDiseaseBleedTrigger(PlayerbotAI* botAI) : Trigger(botAI, "poison disease bleed", 1) {} + + bool IsActive() override; +}; + +class MovementImpairedTrigger : public Trigger +{ +public: + MovementImpairedTrigger(PlayerbotAI* botAI) : Trigger(botAI, "movement impaired", 1) {} + + bool IsActive() override; +}; + class IsSwimmingTrigger : public Trigger { public: diff --git a/src/Ai/Base/TriggerContext.h b/src/Ai/Base/TriggerContext.h index d440e5b5f..111597620 100644 --- a/src/Ai/Base/TriggerContext.h +++ b/src/Ai/Base/TriggerContext.h @@ -66,6 +66,8 @@ public: 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["poison disease bleed"] = &TriggerContext::poison_disease_bleed; + creators["movement impaired"] = &TriggerContext::movement_impaired; creators["protect party member"] = &TriggerContext::protect_party_member; @@ -382,6 +384,8 @@ private: 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* poison_disease_bleed(PlayerbotAI* botAI) { return new PoisonDiseaseBleedTrigger(botAI); } + static Trigger* movement_impaired(PlayerbotAI* botAI) { return new MovementImpairedTrigger(botAI); } static Trigger* PartyMemberCriticalHealth(PlayerbotAI* botAI) { return new PartyMemberCriticalHealthTrigger(botAI);