From c9c936d5c1483d99af2b0967a8c68c88322874fb Mon Sep 17 00:00:00 2001 From: dillyns <49765217+dillyns@users.noreply.github.com> Date: Sun, 8 Feb 2026 12:36:27 -0500 Subject: [PATCH] Add Unending Breath to Warlock NonCombat Strat (#2074) # Pull Request Adds actions and triggers for Warlock class to cast Unending Breath when swimming, following the existing implementation for Shaman Water Breathing. --- ## Feature Evaluation Add triggers for Warlock noncombat strategy for Unending Breath on self and party. Triggers should only be active while swimming. Minimal runtime cost on Warlock bots trigger processing. --- ## How to Test the Changes - Bring a Warlock bot into water - It should cast Unending Breath on itself and anyone in the party ## Complexity & Impact - Does this change add new decision branches? - [ ] No - [x] Yes (**explain below**) It adds triggers to Warlock to decide when to cast Unending Breath on self or party members. - Does this change increase per-bot or per-tick processing? - [ ] No - [x] Yes (**describe and justify impact**) Minimal additional processing for Warlock triggers, same as already existing triggers for Shaman. - Could this logic scale poorly under load? - [x] No - [ ] Yes (**explain why**) --- ## Defaults & Configuration - Does this change modify default bot behavior? - [x] No - [ ] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - [ ] 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**) Claude was used to explore the codebase to find similar implementations that already existed. --- ## 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: bashermens <31279994+hermensbas@users.noreply.github.com> --- src/Ai/Class/Warlock/Action/WarlockActions.h | 12 ++++++++++++ .../Strategy/GenericWarlockNonCombatStrategy.cpp | 2 ++ src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp | 10 ++++++++++ src/Ai/Class/Warlock/Trigger/WarlockTriggers.h | 14 ++++++++++++++ src/Ai/Class/Warlock/WarlockAiObjectContext.cpp | 8 ++++++++ 5 files changed, 46 insertions(+) diff --git a/src/Ai/Class/Warlock/Action/WarlockActions.h b/src/Ai/Class/Warlock/Action/WarlockActions.h index 787b518d6..09f346370 100644 --- a/src/Ai/Class/Warlock/Action/WarlockActions.h +++ b/src/Ai/Class/Warlock/Action/WarlockActions.h @@ -41,6 +41,18 @@ public: std::string const GetTargetName() override { return "pet target"; } }; +class CastUnendingBreathAction : public CastBuffSpellAction +{ +public: + CastUnendingBreathAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "unending breath") {} +}; + +class CastUnendingBreathOnPartyAction : public BuffOnPartyAction +{ +public: + CastUnendingBreathOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "unending breath") {} +}; + class CreateSoulShardAction : public Action { public: diff --git a/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.cpp b/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.cpp index a78ff0a98..d22abe3ca 100644 --- a/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.cpp +++ b/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.cpp @@ -85,6 +85,8 @@ void GenericWarlockNonCombatStrategy::InitTriggers(std::vector& tr triggers.push_back(new TriggerNode("too many soul shards", { NextAction("destroy soul shard", 60.0f) })); triggers.push_back(new TriggerNode("soul link", { NextAction("soul link", 28.0f) })); triggers.push_back(new TriggerNode("demon armor", { NextAction("fel armor", 27.0f) })); + triggers.push_back(new TriggerNode("unending breath", { NextAction("unending breath", 12.0f) })); + triggers.push_back(new TriggerNode("unending breath on party", { NextAction("unending breath on party", 11.0f) })); triggers.push_back(new TriggerNode("no healthstone", { NextAction("create healthstone", 26.0f) })); triggers.push_back(new TriggerNode("no soulstone", { NextAction("create soulstone", 25.0f) })); triggers.push_back(new TriggerNode("life tap", { NextAction("life tap", 23.0f) })); diff --git a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp index 53e77669b..1df531e7f 100644 --- a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp +++ b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp @@ -79,6 +79,16 @@ bool SoulLinkTrigger::IsActive() return !botAI->HasAura("soul link", target); } +bool UnendingBreathTrigger::IsActive() +{ + return BuffTrigger::IsActive() && AI_VALUE2(bool, "swimming", "self target"); +} + +bool UnendingBreathOnPartyTrigger::IsActive() +{ + return BuffOnPartyTrigger::IsActive() && AI_VALUE2(bool, "swimming", "self target"); +} + bool DemonicEmpowermentTrigger::IsActive() { Pet* pet = bot->GetPet(); diff --git a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h index 851254901..d66fe537e 100644 --- a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h +++ b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h @@ -32,6 +32,20 @@ public: bool IsActive() override; }; +class UnendingBreathTrigger : public BuffTrigger +{ +public: + UnendingBreathTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "unending breath", 5 * 2000) {} + bool IsActive() override; +}; + +class UnendingBreathOnPartyTrigger : public BuffOnPartyTrigger +{ +public: + UnendingBreathOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "unending breath on party", 2 * 2000) {} + bool IsActive() override; +}; + class OutOfSoulShardsTrigger : public Trigger { public: diff --git a/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp b/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp index a70327b6c..27b9166c6 100644 --- a/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp +++ b/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp @@ -143,6 +143,8 @@ public: creators["shadow trance"] = &WarlockTriggerFactoryInternal::shadow_trance; creators["demon armor"] = &WarlockTriggerFactoryInternal::demon_armor; creators["soul link"] = &WarlockTriggerFactoryInternal::soul_link; + creators["unending breath"] = &WarlockTriggerFactoryInternal::unending_breath; + creators["unending breath on party"] = &WarlockTriggerFactoryInternal::unending_breath_on_party; creators["no soul shard"] = &WarlockTriggerFactoryInternal::no_soul_shard; creators["too many soul shards"] = &WarlockTriggerFactoryInternal::too_many_soul_shards; creators["no healthstone"] = &WarlockTriggerFactoryInternal::HasHealthstone; @@ -189,6 +191,8 @@ private: static Trigger* shadow_trance(PlayerbotAI* botAI) { return new ShadowTranceTrigger(botAI); } static Trigger* demon_armor(PlayerbotAI* botAI) { return new DemonArmorTrigger(botAI); } static Trigger* soul_link(PlayerbotAI* botAI) { return new SoulLinkTrigger(botAI); } + static Trigger* unending_breath(PlayerbotAI* botAI) { return new UnendingBreathTrigger(botAI); } + static Trigger* unending_breath_on_party(PlayerbotAI* botAI) { return new UnendingBreathOnPartyTrigger(botAI); } static Trigger* no_soul_shard(PlayerbotAI* botAI) { return new OutOfSoulShardsTrigger(botAI); } static Trigger* too_many_soul_shards(PlayerbotAI* botAI) { return new TooManySoulShardsTrigger(botAI); } static Trigger* HasHealthstone(PlayerbotAI* botAI) { return new HasHealthstoneTrigger(botAI); } @@ -240,6 +244,8 @@ public: creators["demon armor"] = &WarlockAiObjectContextInternal::demon_armor; creators["demon skin"] = &WarlockAiObjectContextInternal::demon_skin; creators["soul link"] = &WarlockAiObjectContextInternal::soul_link; + creators["unending breath"] = &WarlockAiObjectContextInternal::unending_breath; + creators["unending breath on party"] = &WarlockAiObjectContextInternal::unending_breath_on_party; creators["create soul shard"] = &WarlockAiObjectContextInternal::create_soul_shard; creators["destroy soul shard"] = &WarlockAiObjectContextInternal::destroy_soul_shard; creators["create healthstone"] = &WarlockAiObjectContextInternal::create_healthstone; @@ -313,6 +319,8 @@ private: static Action* demon_armor(PlayerbotAI* botAI) { return new CastDemonArmorAction(botAI); } static Action* demon_skin(PlayerbotAI* botAI) { return new CastDemonSkinAction(botAI); } static Action* soul_link(PlayerbotAI* botAI) { return new CastSoulLinkAction(botAI); } + static Action* unending_breath(PlayerbotAI* botAI) { return new CastUnendingBreathAction(botAI); } + static Action* unending_breath_on_party(PlayerbotAI* botAI) { return new CastUnendingBreathOnPartyAction(botAI); } static Action* create_soul_shard(PlayerbotAI* botAI) { return new CreateSoulShardAction(botAI); } static Action* destroy_soul_shard(PlayerbotAI* botAI) { return new DestroySoulShardAction(botAI); } static Action* create_healthstone(PlayerbotAI* botAI) { return new CastCreateHealthstoneAction(botAI); }