From ee245f73b5b80165508cceab997e1ef3f037904d Mon Sep 17 00:00:00 2001 From: ThePenguinMan96 Date: Sat, 26 Jul 2025 01:49:49 -0700 Subject: [PATCH 1/5] Mage Overhaul Hello everyone, Back again with another class overhaul. Here is a list of what changes have been made: 1. Consolidated the AoE strategies into "aoe". For light aoe (2+ enemies) the mage will use Cone of Cold (frost)/Arcane Explosion (Arcane)/Multi-Dot with Living Bomb (Fire/Frostfire). For medium aoe (3+ enemies) they will use Flamestrike -> Blizzard. Also, the mage will automatically cancel channeling their blizzard if there is less than 2 enemies around. This is huge, since the mage would often stand there and finish their entire channel during a boss fight after the adds died. 2. Organized actions, triggers, and the aiobjectcontext 3. Enabled Deep Freeze to be casted on bosses regardless of their immune status. Big benefit for frost dps on boss fights. 4. Slight tweaks in the conf so Arcane gets Arcane Barrage and Frostfire gets 2/2 Firestarter 5. Streamlined Arcane DPS to use Missile Barrage proc when at 4 stacks of Arcane Blast 5. Streamlined Fire/Frostfire DPS to keep Improved Scorch active (5% spell crit) unless there is a debuff of equal type 6. Added "firestarter" strategy, that utilizes the Fire talent Firestarter better. The mage will multi-dot Living Bomb while running towards melee, and cast Dragon's Breath -> instant cast Flamestrike -> Blast Wave -> instant cast Flamestrike -> Blizzard for bonkers damage. Disabled by default - not everyone wants their mages running into melee. Enable by typing "co +firestarter" on fire and frostfire mages. 7. Streamlined Frost DPS by finally adding support for Cold Snap for mages. It will proc when both Icy Veins and Deep Freeze are on cooldown. There is an exception to this - if the mage is level 30-59, it will not check for Deep Freeze - only Icy Veins. 8. Added Conjure Mana Gem support in the generic non-combat strategy and Use Mana Gem support in the generic combat strategy. This might be the biggest benefit of the overhaul - the gem has a 90 second cooldown, not shared with mana potions. It really prevents the mage from gassing out in longer fights. And the mana gem has 3 charges! 9. Added Mana Shield ability, which triggers on low health. 10. Changed Mirror Image from a boost ability to an anti-threat tool. Not many people know this, but it's best use in PvE is it's anti-threat modifier: "Mod Total Threat - Temporary Value: -90000000". It also doesn't do good damage, and is essentially used best as a pre-pull spell. But until the mages know how to react to a pull-timer, it's going to be used to reduce threat. Let me know what y'all think! --- conf/playerbots.conf.dist | 6 +- src/AiFactory.cpp | 10 +- src/PlayerbotAI.cpp | 13 +- src/strategy/mage/ArcaneMageStrategy.cpp | 75 +-- src/strategy/mage/ArcaneMageStrategy.h | 9 - src/strategy/mage/FireMageStrategy.cpp | 82 ++- src/strategy/mage/FireMageStrategy.h | 9 +- src/strategy/mage/FrostFireMageStrategy.cpp | 63 ++- src/strategy/mage/FrostFireMageStrategy.h | 11 +- src/strategy/mage/FrostMageStrategy.cpp | 121 ++--- src/strategy/mage/FrostMageStrategy.h | 9 - .../mage/GenericMageNonCombatStrategy.cpp | 26 +- src/strategy/mage/GenericMageStrategy.cpp | 107 ++-- src/strategy/mage/GenericMageStrategy.h | 9 + src/strategy/mage/MageActions.cpp | 50 +- src/strategy/mage/MageActions.h | 486 +++++++++++------- src/strategy/mage/MageAiObjectContext.cpp | 79 ++- src/strategy/mage/MageTriggers.cpp | 137 ++++- src/strategy/mage/MageTriggers.h | 230 +++++++-- 19 files changed, 985 insertions(+), 547 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index b64454ce..a0d1df51 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -1427,8 +1427,8 @@ AiPlayerbot.PremadeSpecLink.7.5.80 = -023222301004-05032331331013501120331251 AiPlayerbot.PremadeSpecName.8.0 = arcane pve AiPlayerbot.PremadeSpecGlyph.8.0 = 42735,43339,44955,43364,43361,42751 -AiPlayerbot.PremadeSpecLink.8.0.60 = 23000503110033014032310150532 -AiPlayerbot.PremadeSpecLink.8.0.80 = 23000523310033015032310250532-03-203203001 +AiPlayerbot.PremadeSpecLink.8.0.60 = 230005231100330150323102500321 +AiPlayerbot.PremadeSpecLink.8.0.80 = 230005231100330150323102505321-03-203303001 AiPlayerbot.PremadeSpecName.8.1 = fire pve AiPlayerbot.PremadeSpecGlyph.8.1 = 42739,43339,45737,43364,44920,42751 AiPlayerbot.PremadeSpecLink.8.1.60 = -0055030011302231053120321341 @@ -1440,7 +1440,7 @@ AiPlayerbot.PremadeSpecLink.8.2.80 = 23002303110003--053303031320310003015223135 AiPlayerbot.PremadeSpecName.8.3 = frostfire pve AiPlayerbot.PremadeSpecGlyph.8.3 = 44684,44920,42751,43339,43364,45737 AiPlayerbot.PremadeSpecLink.8.3.60 = -2305032012303331053120300051 -AiPlayerbot.PremadeSpecLink.8.3.80 = -2305032012303331053120311351-023303031 +AiPlayerbot.PremadeSpecLink.8.3.80 = -2305032012303331053120321351-023302031 AiPlayerbot.PremadeSpecName.8.4 = arcane pvp AiPlayerbot.PremadeSpecGlyph.8.4 = 42735,43364,42738,43360,43357,42752 AiPlayerbot.PremadeSpecLink.8.4.60 = 205323200122032103303102015221 diff --git a/src/AiFactory.cpp b/src/AiFactory.cpp index 7c18070b..3bab43ae 100644 --- a/src/AiFactory.cpp +++ b/src/AiFactory.cpp @@ -302,22 +302,22 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa break; case CLASS_MAGE: if (tab == 0) - engine->addStrategiesNoInit("arcane", "arcane aoe", nullptr); + engine->addStrategiesNoInit("arcane", nullptr); else if (tab == 1) { if (player->HasSpell(44614) /*Frostfire Bolt*/ && player->HasAura(15047) /*Ice Shards*/) { - engine->addStrategiesNoInit("frostfire", "frostfire aoe", nullptr); + engine->addStrategiesNoInit("frostfire", nullptr); } else { - engine->addStrategiesNoInit("fire", "fire aoe", nullptr); + engine->addStrategiesNoInit("fire", nullptr); } } else - engine->addStrategiesNoInit("frost", "frost aoe", nullptr); + engine->addStrategiesNoInit("frost", nullptr); - engine->addStrategiesNoInit("dps", "dps assist", "cure", nullptr); + engine->addStrategiesNoInit("dps", "dps assist", "cure", "aoe", nullptr); break; case CLASS_WARRIOR: if (tab == 2) diff --git a/src/PlayerbotAI.cpp b/src/PlayerbotAI.cpp index d2e0f293..1d406345 100644 --- a/src/PlayerbotAI.cpp +++ b/src/PlayerbotAI.cpp @@ -2949,14 +2949,19 @@ bool PlayerbotAI::CanCastSpell(uint32 spellid, Unit* target, bool checkHasSpell, if (!itemTarget) { + // Exception for Deep Freeze (44572) - allow cast for damage on immune targets (e.g., bosses) if (target->IsImmunedToSpell(spellInfo)) { - if (!sPlayerbotAIConfig->logInGroupOnly || (bot->GetGroup() && HasRealPlayerMaster())) + if (spellid != 44572) // Deep Freeze { - LOG_DEBUG("playerbots", "target is immuned to spell - target name: {}, spellid: {}, bot name: {}", - target->GetName(), spellid, bot->GetName()); + if (!sPlayerbotAIConfig->logInGroupOnly || (bot->GetGroup() && HasRealPlayerMaster())) + { + LOG_DEBUG("playerbots", "target is immuned to spell - target name: {}, spellid: {}, bot name: {}", + target->GetName(), spellid, bot->GetName()); + } + return false; } - return false; + // Otherwise, allow Deep Freeze even if immune } if (bot != target && sServerFacade->GetDistance2d(bot, target) > sPlayerbotAIConfig->sightDistance) diff --git a/src/strategy/mage/ArcaneMageStrategy.cpp b/src/strategy/mage/ArcaneMageStrategy.cpp index 5b585721..2b2fb7e8 100644 --- a/src/strategy/mage/ArcaneMageStrategy.cpp +++ b/src/strategy/mage/ArcaneMageStrategy.cpp @@ -4,9 +4,9 @@ */ #include "ArcaneMageStrategy.h" - #include "Playerbots.h" +// ===== Action Node Factory ===== class ArcaneMageStrategyActionNodeFactory : public NamedObjectFactory { public: @@ -15,69 +15,48 @@ public: creators["arcane blast"] = &arcane_blast; creators["arcane barrage"] = &arcane_barrage; creators["arcane missiles"] = &arcane_missiles; - // creators["firebolt"] = &firebolt; + creators["fire blast"] = &fire_blast; + creators["frostbolt"] = &frostbolt; + creators["arcane power"] = &arcane_power; + creators["icy veins"] = &icy_veins; } private: - static ActionNode* arcane_blast([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("arcane blast", - /*P*/ nullptr, - /*A*/ NextAction::array(0, new NextAction("arcane missiles"), nullptr), - /*C*/ nullptr); - } - - static ActionNode* arcane_barrage([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("arcane barrage", - /*P*/ nullptr, - /*A*/ nullptr, - /*C*/ nullptr); - } - - static ActionNode* arcane_missiles([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("arcane missiles", - /*P*/ nullptr, - /*A*/ NextAction::array(0, new NextAction("fireball"), nullptr), - /*C*/ nullptr); - } - - // static ActionNode* firebolt([[maybe_unused]] PlayerbotAI* botAI) - // { - // return new ActionNode ("firebolt", - // /*P*/ nullptr, - // /*A*/ NextAction::array(0, new NextAction("shoot"), nullptr), - // /*C*/ nullptr); - // } + static ActionNode* arcane_blast(PlayerbotAI*) { return new ActionNode("arcane blast", nullptr, nullptr, nullptr); } + static ActionNode* arcane_barrage(PlayerbotAI*) { return new ActionNode("arcane barrage", nullptr, nullptr, nullptr); } + static ActionNode* arcane_missiles(PlayerbotAI*) { return new ActionNode("arcane missiles", nullptr, nullptr, nullptr); } + static ActionNode* fire_blast(PlayerbotAI*) { return new ActionNode("fire blast", nullptr, nullptr, nullptr); } + static ActionNode* frostbolt(PlayerbotAI*) { return new ActionNode("frostbolt", nullptr, nullptr, nullptr); } + static ActionNode* arcane_power(PlayerbotAI*) { return new ActionNode("arcane power", nullptr, nullptr, nullptr); } + static ActionNode* icy_veins(PlayerbotAI*) { return new ActionNode("icy veins", nullptr, nullptr, nullptr); } }; +// ===== Single Target Strategy ===== ArcaneMageStrategy::ArcaneMageStrategy(PlayerbotAI* botAI) : GenericMageStrategy(botAI) { actionNodeFactories.Add(new ArcaneMageStrategyActionNodeFactory()); } +// ===== Default Actions ===== NextAction** ArcaneMageStrategy::getDefaultActions() { - return NextAction::array(0, new NextAction("arcane blast", ACTION_DEFAULT + 0.3f), - new NextAction("frostbolt", ACTION_DEFAULT + 0.2f), // arcane immune target - new NextAction("fire blast", ACTION_DEFAULT + 0.1f), // cast during movement - new NextAction("shoot", ACTION_DEFAULT), nullptr); + return NextAction::array(0, new NextAction("arcane blast", 5.6f), + new NextAction("arcane missiles", 5.5f), + new NextAction("arcane barrage", 5.4f), // cast while moving + new NextAction("fire blast", 5.3f), // cast while moving if arcane barrage isn't available/learned + new NextAction("frostbolt", 5.2f), // for arcane immune targets + new NextAction("shoot", 5.1f), nullptr); } +// ===== Trigger Initialization === void ArcaneMageStrategy::InitTriggers(std::vector& triggers) { GenericMageStrategy::InitTriggers(triggers); - triggers.push_back( - new TriggerNode("arcane blast stack", NextAction::array(0, new NextAction("arcane missiles", 15.0f), NULL))); + // Cooldown Triggers + triggers.push_back(new TriggerNode("arcane power", NextAction::array(0, new NextAction("arcane power", 29.0f), nullptr))); + triggers.push_back(new TriggerNode("icy veins", NextAction::array(0, new NextAction("icy veins", 28.5f), nullptr))); + + // Proc Trigger + triggers.push_back(new TriggerNode("arcane blast 4 stacks and missile barrage", NextAction::array(0, new NextAction("arcane missiles", 15.0f), nullptr))); } - -void ArcaneMageAoeStrategy::InitTriggers(std::vector& triggers) -{ - // triggers.push_back(new TriggerNode( - // "high aoe", - // NextAction::array(0, new NextAction("arcane explosion", 39.0f), NULL))); - - triggers.push_back(new TriggerNode("medium aoe", NextAction::array(0, new NextAction("blizzard", 40.0f), NULL))); -} \ No newline at end of file diff --git a/src/strategy/mage/ArcaneMageStrategy.h b/src/strategy/mage/ArcaneMageStrategy.h index adcc51cf..f6adf23f 100644 --- a/src/strategy/mage/ArcaneMageStrategy.h +++ b/src/strategy/mage/ArcaneMageStrategy.h @@ -20,13 +20,4 @@ public: NextAction** getDefaultActions() override; }; -class ArcaneMageAoeStrategy : public CombatStrategy -{ -public: - ArcaneMageAoeStrategy(PlayerbotAI* ai) : CombatStrategy(ai) {} - -public: - virtual void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "arcane aoe"; } -}; #endif diff --git a/src/strategy/mage/FireMageStrategy.cpp b/src/strategy/mage/FireMageStrategy.cpp index 9d81beae..0501d4a0 100644 --- a/src/strategy/mage/FireMageStrategy.cpp +++ b/src/strategy/mage/FireMageStrategy.cpp @@ -4,42 +4,74 @@ */ #include "FireMageStrategy.h" - #include "Playerbots.h" #include "Strategy.h" -NextAction** FireMageStrategy::getDefaultActions() +// ===== Action Node Factory ===== +class FireMageStrategyActionNodeFactory : public NamedObjectFactory { - return NextAction::array(0, new NextAction("fireball", ACTION_DEFAULT + 0.3f), - new NextAction("frostbolt", ACTION_DEFAULT + 0.2f), // fire immune target - new NextAction("fire blast", ACTION_DEFAULT + 0.1f), // cast during movement - new NextAction("shoot", ACTION_DEFAULT), NULL); +public: + FireMageStrategyActionNodeFactory() + { + creators["fireball"] = &fireball; + creators["frostbolt"] = &frostbolt; + creators["fire blast"] = &fire_blast; + creators["pyroblast"] = &pyroblast; + creators["scorch"] = &scorch; + creators["living bomb"] = &living_bomb; + creators["combustion"] = &combustion; + } + +private: + static ActionNode* fireball(PlayerbotAI*) { return new ActionNode("fireball", nullptr, nullptr, nullptr); } + static ActionNode* frostbolt(PlayerbotAI*) { return new ActionNode("frostbolt", nullptr, nullptr, nullptr); } + static ActionNode* fire_blast(PlayerbotAI*) { return new ActionNode("fire blast", nullptr, nullptr, nullptr); } + static ActionNode* pyroblast(PlayerbotAI*) { return new ActionNode("pyroblast", nullptr, nullptr, nullptr); } + static ActionNode* scorch(PlayerbotAI*) { return new ActionNode("scorch", nullptr, nullptr, nullptr); } + static ActionNode* living_bomb(PlayerbotAI*) { return new ActionNode("living bomb", nullptr, nullptr, nullptr); } + static ActionNode* combustion(PlayerbotAI*) { return new ActionNode("combustion", nullptr, nullptr, nullptr); } +}; + +// ===== Single Target Strategy ===== +FireMageStrategy::FireMageStrategy(PlayerbotAI* botAI) : GenericMageStrategy(botAI) +{ + actionNodeFactories.Add(new FireMageStrategyActionNodeFactory()); } +// ===== Default Actions ===== +NextAction** FireMageStrategy::getDefaultActions() +{ + return NextAction::array(0, new NextAction("fireball", 5.3f), + new NextAction("frostbolt", 5.2f), // fire immune target + new NextAction("fire blast", 5.1f), // cast during movement + new NextAction("shoot", 5.0f), nullptr); +} + +// ===== Trigger Initialization ===== void FireMageStrategy::InitTriggers(std::vector& triggers) { GenericMageStrategy::InitTriggers(triggers); - // triggers.push_back(new TriggerNode("pyroblast", NextAction::array(0, new NextAction("pyroblast", 10.0f), - // nullptr))); - triggers.push_back( - new TriggerNode("hot streak", NextAction::array(0, new NextAction("pyroblast", 25.0f), nullptr))); - triggers.push_back( - new TriggerNode("combustion", NextAction::array(0, new NextAction("combustion", 50.0f), nullptr))); - triggers.push_back( - new TriggerNode("living bomb", NextAction::array(0, new NextAction("living bomb", 19.0f), nullptr))); - // triggers.push_back(new TriggerNode("enemy too close for spell", NextAction::array(0, new NextAction("dragon's - // breath", 70.0f), nullptr))); + // Cooldown Trigger + triggers.push_back(new TriggerNode("combustion", NextAction::array(0, new NextAction("combustion", 18.0f), nullptr))); + + // Debuff Triggers + triggers.push_back(new TriggerNode("improved scorch", NextAction::array(0, new NextAction("scorch", 19.0f), nullptr))); + triggers.push_back(new TriggerNode("living bomb", NextAction::array(0, new NextAction("living bomb", 18.5f), nullptr))); + + // Proc Trigger + triggers.push_back(new TriggerNode("hot streak", NextAction::array(0, new NextAction("pyroblast", 25.0f), nullptr))); } -void FireMageAoeStrategy::InitTriggers(std::vector& triggers) +// Combat strategy to run to melee for Dragon's Breath and Blast Wave +// Disabled by default for the Fire/Frostfire spec +// To enable, type "co +firestarter" +// To disable, type "co -firestarter" +FirestarterStrategy::FirestarterStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {} + +void FirestarterStrategy::InitTriggers(std::vector& triggers) { - // higher priority to cast before move away - triggers.push_back( - new TriggerNode("medium aoe", NextAction::array(0, - new NextAction("dragon's breath", ACTION_MOVE + 9), - new NextAction("flamestrike", ACTION_MOVE + 8), - new NextAction("blast wave", ACTION_MOVE + 7), - new NextAction("living bomb on attackers", 21.0f), - new NextAction("blizzard", 20.0f), nullptr))); + triggers.push_back(new TriggerNode( + "blast wave off cd and medium aoe", + NextAction::array(0, new NextAction("reach melee", 25.5f), nullptr))); } diff --git a/src/strategy/mage/FireMageStrategy.h b/src/strategy/mage/FireMageStrategy.h index 4200fbe0..4574a345 100644 --- a/src/strategy/mage/FireMageStrategy.h +++ b/src/strategy/mage/FireMageStrategy.h @@ -13,20 +13,19 @@ class PlayerbotAI; class FireMageStrategy : public GenericMageStrategy { public: - FireMageStrategy(PlayerbotAI* botAI) : GenericMageStrategy(botAI) {} + FireMageStrategy(PlayerbotAI* botAI); void InitTriggers(std::vector& triggers) override; std::string const getName() override { return "fire"; } NextAction** getDefaultActions() override; }; -class FireMageAoeStrategy : public CombatStrategy +class FirestarterStrategy : public CombatStrategy { public: - FireMageAoeStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {} + FirestarterStrategy(PlayerbotAI* botAI); void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "fire aoe"; } + std::string const getName() override { return "firestarter"; } }; - #endif diff --git a/src/strategy/mage/FrostFireMageStrategy.cpp b/src/strategy/mage/FrostFireMageStrategy.cpp index dc01684f..106427c8 100644 --- a/src/strategy/mage/FrostFireMageStrategy.cpp +++ b/src/strategy/mage/FrostFireMageStrategy.cpp @@ -4,31 +4,60 @@ */ #include "FrostFireMageStrategy.h" - #include "Playerbots.h" -NextAction** FrostFireMageStrategy::getDefaultActions() +// ===== Action Node Factory ===== +class FrostFireMageStrategyActionNodeFactory : public NamedObjectFactory { - return NextAction::array(0, new NextAction("frostfire bolt", ACTION_DEFAULT + 0.1f), - new NextAction("shoot", ACTION_DEFAULT), NULL); +public: + FrostFireMageStrategyActionNodeFactory() + { + creators["frostfire bolt"] = &frostfire_bolt; + creators["fire blast"] = &fire_blast; + creators["pyroblast"] = &pyroblast; + creators["combustion"] = &combustion; + creators["icy veins"] = &icy_veins; + creators["scorch"] = &scorch; + creators["living bomb"] = &living_bomb; + } + +private: + static ActionNode* frostfire_bolt(PlayerbotAI*) { return new ActionNode("frostfire bolt", nullptr, nullptr, nullptr); } + static ActionNode* fire_blast(PlayerbotAI*) { return new ActionNode("fire blast", nullptr, nullptr, nullptr); } + static ActionNode* pyroblast(PlayerbotAI*) { return new ActionNode("pyroblast", nullptr, nullptr, nullptr); } + static ActionNode* combustion(PlayerbotAI*) { return new ActionNode("combustion", nullptr, nullptr, nullptr); } + static ActionNode* icy_veins(PlayerbotAI*) { return new ActionNode("icy veins", nullptr, nullptr, nullptr); } + static ActionNode* scorch(PlayerbotAI*) { return new ActionNode("scorch", nullptr, nullptr, nullptr); } + static ActionNode* living_bomb(PlayerbotAI*) { return new ActionNode("living bomb", nullptr, nullptr, nullptr); } +}; + +// ===== Single Target Strategy ===== +FrostFireMageStrategy::FrostFireMageStrategy(PlayerbotAI* botAI) : GenericMageStrategy(botAI) +{ + actionNodeFactories.Add(new FrostFireMageStrategyActionNodeFactory()); } +// ===== Default Actions ===== +NextAction** FrostFireMageStrategy::getDefaultActions() +{ + return NextAction::array(0, new NextAction("frostfire bolt", 5.2f), + new NextAction("fire blast", 5.1f), // cast during movement + new NextAction("shoot", 5.0f), nullptr); +} + +// ===== Trigger Initialization ===== void FrostFireMageStrategy::InitTriggers(std::vector& triggers) { GenericMageStrategy::InitTriggers(triggers); - triggers.push_back( - new TriggerNode("hot streak", NextAction::array(0, new NextAction("pyroblast", 25.0f), nullptr))); - triggers.push_back( - new TriggerNode("combustion", NextAction::array(0, new NextAction("combustion", 50.0f), nullptr))); - triggers.push_back( - new TriggerNode("icy veins", NextAction::array(0, new NextAction("icy veins", 60.0f), nullptr))); -} + // Cooldown Triggers + triggers.push_back(new TriggerNode("combustion", NextAction::array(0, new NextAction("combustion", 18.0f), nullptr))); + triggers.push_back(new TriggerNode("icy veins", NextAction::array(0, new NextAction("icy veins", 17.5f), nullptr))); -void FrostFireMageAoeStrategy::InitTriggers(std::vector& triggers) -{ - triggers.push_back( - new TriggerNode("medium aoe", NextAction::array(0, new NextAction("flamestrike", 20.0f), nullptr))); - triggers.push_back( - new TriggerNode("living bomb", NextAction::array(0, new NextAction("living bomb", 25.0f), nullptr))); + // Debuff Triggers + triggers.push_back(new TriggerNode("improved scorch", NextAction::array(0, new NextAction("scorch", 19.0f), nullptr))); + triggers.push_back(new TriggerNode("living bomb", NextAction::array(0, new NextAction("living bomb", 18.5f), nullptr))); + + // Proc Trigger + triggers.push_back(new TriggerNode("hot streak", NextAction::array(0, new NextAction("pyroblast", 25.0f), nullptr))); } diff --git a/src/strategy/mage/FrostFireMageStrategy.h b/src/strategy/mage/FrostFireMageStrategy.h index ef6cd9fc..0db6f934 100644 --- a/src/strategy/mage/FrostFireMageStrategy.h +++ b/src/strategy/mage/FrostFireMageStrategy.h @@ -13,20 +13,11 @@ class PlayerbotAI; class FrostFireMageStrategy : public GenericMageStrategy { public: - FrostFireMageStrategy(PlayerbotAI* botAI) : GenericMageStrategy(botAI) {} + FrostFireMageStrategy(PlayerbotAI* botAI); void InitTriggers(std::vector& triggers) override; std::string const getName() override { return "frostfire"; } NextAction** getDefaultActions() override; }; -class FrostFireMageAoeStrategy : public CombatStrategy -{ -public: - FrostFireMageAoeStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {} - - void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "frostfire aoe"; } -}; - #endif diff --git a/src/strategy/mage/FrostMageStrategy.cpp b/src/strategy/mage/FrostMageStrategy.cpp index 55d84ad0..eef36c92 100644 --- a/src/strategy/mage/FrostMageStrategy.cpp +++ b/src/strategy/mage/FrostMageStrategy.cpp @@ -7,6 +7,7 @@ #include "Playerbots.h" +// ===== Action Node Factory ===== class FrostMageStrategyActionNodeFactory : public NamedObjectFactory { public: @@ -16,96 +17,70 @@ public: creators["ice barrier"] = &ice_barrier; creators["summon water elemental"] = &summon_water_elemental; creators["deep freeze"] = &deep_freeze; + creators["icy veins"] = &icy_veins; + creators["frostbolt"] = &frostbolt; + creators["ice lance"] = &ice_lance; + creators["fire blast"] = &fire_blast; + creators["fireball"] = &fireball; + creators["frostfire bolt"] = &frostfire_bolt; } private: - static ActionNode* cold_snap([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("cold snap", - /*P*/ nullptr, - /*A*/ nullptr, - /*C*/ nullptr); - } - - static ActionNode* ice_barrier([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("ice barrier", - /*P*/ nullptr, - /*A*/ nullptr, - /*C*/ nullptr); - } - - static ActionNode* summon_water_elemental([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("summon water elemental", - /*P*/ nullptr, - /*A*/ nullptr, - /*C*/ nullptr); - } - - static ActionNode* deep_freeze([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("deep freeze", - /*P*/ nullptr, - /*A*/ NextAction::array(0, new NextAction("ice lance"), nullptr), - /*C*/ nullptr); - } + static ActionNode* cold_snap(PlayerbotAI*) { return new ActionNode("cold snap", nullptr, nullptr, nullptr); } + static ActionNode* ice_barrier(PlayerbotAI*) { return new ActionNode("ice barrier", nullptr, nullptr, nullptr); } + static ActionNode* summon_water_elemental(PlayerbotAI*) { return new ActionNode("summon water elemental", nullptr, nullptr, nullptr); } + static ActionNode* deep_freeze(PlayerbotAI*) { return new ActionNode("deep freeze", nullptr, nullptr, nullptr); } + static ActionNode* icy_veins(PlayerbotAI*) { return new ActionNode("icy veins", nullptr, nullptr, nullptr); } + static ActionNode* frostbolt(PlayerbotAI*) { return new ActionNode("frostbolt", nullptr, nullptr, nullptr); } + static ActionNode* ice_lance(PlayerbotAI*) { return new ActionNode("ice lance", nullptr, nullptr, nullptr); } + static ActionNode* fire_blast(PlayerbotAI*) { return new ActionNode("fire blast", nullptr, nullptr, nullptr); } + static ActionNode* fireball(PlayerbotAI*) { return new ActionNode("fireball", nullptr, nullptr, nullptr); } + static ActionNode* frostfire_bolt(PlayerbotAI*) { return new ActionNode("frostfire bolt", nullptr, nullptr, nullptr); } }; +// ===== Single Target Strategy ===== FrostMageStrategy::FrostMageStrategy(PlayerbotAI* botAI) : GenericMageStrategy(botAI) { actionNodeFactories.Add(new FrostMageStrategyActionNodeFactory()); } +// ===== Default Actions ===== NextAction** FrostMageStrategy::getDefaultActions() { - return NextAction::array(0, new NextAction("frostbolt", ACTION_DEFAULT + 0.3f), - new NextAction("fire blast", ACTION_DEFAULT + 0.2f), // cast during movement - new NextAction("shoot", ACTION_DEFAULT + 0.1f), - new NextAction("fireball", ACTION_DEFAULT), nullptr); + return NextAction::array(0, new NextAction("frostbolt", 5.4f), + new NextAction("ice lance", 5.3f), // cast during movement + new NextAction("fire blast", 5.2f), // cast during movement if ice lance is not learned + new NextAction("shoot", 5.1f), + new NextAction("fireball", 5.0f), nullptr); } +// ===== Trigger Initialization === void FrostMageStrategy::InitTriggers(std::vector& triggers) { GenericMageStrategy::InitTriggers(triggers); - triggers.push_back(new TriggerNode("icy veins", NextAction::array(0, new NextAction("icy veins", 50.0f), nullptr))); - // No logic currently for cold snap usage.. possibly use right after icy veins drops off? - // triggers.push_back(new TriggerNode("cold snap", NextAction::array(0, new NextAction("cold snap", 50.0f), - // nullptr))); - triggers.push_back(new TriggerNode( - "no pet", NextAction::array(0, new NextAction("summon water elemental", ACTION_HIGH), nullptr))); - triggers.push_back( - new TriggerNode("has pet", NextAction::array(0, new NextAction("toggle pet spell", ACTION_HIGH + 1), nullptr))); - triggers.push_back( - new TriggerNode("medium health", NextAction::array(0, new NextAction("ice barrier", ACTION_NORMAL), nullptr))); - triggers.push_back( - new TriggerNode("being attacked", NextAction::array(0, new NextAction("ice barrier", ACTION_HIGH + 1), nullptr))); - triggers.push_back(new TriggerNode( - "brain freeze", NextAction::array(0, new NextAction("frostfire bolt", ACTION_NORMAL + 3), nullptr))); - // Combo cast the last charge of fingers of frost for double crits. - // Should only do this on the final charge of FoF. - triggers.push_back(new TriggerNode("fingers of frost single", - NextAction::array(0, new NextAction("frostbolt", ACTION_NORMAL + 2), - new NextAction("deep freeze", ACTION_NORMAL + 1), nullptr))); - // May not need this, frostbolt is the default action so probably don't need to specify. - // Maybe uncomment if you find the mage is prioritising auxillary spells while this buff is up, and wasting the - // proc. triggers.push_back(new TriggerNode("fingers of frost double", NextAction::array(0, new - // NextAction("frostbolt", ACTION_NORMAL), nullptr))); + // Cooldown triggers + triggers.push_back(new TriggerNode("cold snap", NextAction::array(0, new NextAction("cold snap", 28.0f), nullptr))); + triggers.push_back(new TriggerNode("icy veins", NextAction::array(0, new NextAction("icy veins", 27.5f), nullptr))); - // Same 2-spell combo for various freeze procs - triggers.push_back(new TriggerNode("frost nova on target", - NextAction::array(0, new NextAction("frostbolt", ACTION_NORMAL + 2), - new NextAction("deep freeze", ACTION_NORMAL + 1), nullptr))); - triggers.push_back(new TriggerNode("frostbite on target", - NextAction::array(0, new NextAction("frostbolt", ACTION_NORMAL + 2), - new NextAction("deep freeze", ACTION_NORMAL + 1), nullptr))); -} - -void FrostMageAoeStrategy::InitTriggers(std::vector& triggers) -{ - triggers.push_back( - new TriggerNode("medium aoe", NextAction::array(0, new NextAction("blizzard", ACTION_HIGH), nullptr))); - triggers.push_back( - new TriggerNode("light aoe", NextAction::array(0, new NextAction("cone of cold", ACTION_HIGH + 1), nullptr))); + // Pet/Defensive triggers + triggers.push_back(new TriggerNode("no pet", NextAction::array(0, new NextAction("summon water elemental", 30.0f), nullptr))); + triggers.push_back(new TriggerNode("has pet", NextAction::array(0, new NextAction("toggle pet spell", 29.5f), nullptr))); + triggers.push_back(new TriggerNode("medium health", NextAction::array(0, new NextAction("ice barrier", 29.0f), nullptr))); + triggers.push_back(new TriggerNode("being attacked", NextAction::array(0, new NextAction("ice barrier", 29.0f), nullptr))); + + // Proc/Freeze triggers + triggers.push_back(new TriggerNode("brain freeze", NextAction::array(0, new NextAction("frostfire bolt", 19.5f), nullptr))); + triggers.push_back(new TriggerNode("fingers of frost", NextAction::array(0, + new NextAction("deep freeze", 19.0f), + new NextAction("frostbolt", 18.0f), nullptr))); + + triggers.push_back(new TriggerNode("frostbite on target", NextAction::array(0, + new NextAction("deep freeze", 19.0f), + new NextAction("frostbolt", 18.0f), nullptr))); + + triggers.push_back(new TriggerNode("frost nova on target", NextAction::array(0, + new NextAction("deep freeze", 19.0f), + new NextAction("frostbolt", 18.0f), nullptr))); + } diff --git a/src/strategy/mage/FrostMageStrategy.h b/src/strategy/mage/FrostMageStrategy.h index 17a1fe40..d54c04d3 100644 --- a/src/strategy/mage/FrostMageStrategy.h +++ b/src/strategy/mage/FrostMageStrategy.h @@ -20,13 +20,4 @@ public: NextAction** getDefaultActions() override; }; -class FrostMageAoeStrategy : public CombatStrategy -{ -public: - FrostMageAoeStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {} - - void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "frost aoe"; } -}; - #endif diff --git a/src/strategy/mage/GenericMageNonCombatStrategy.cpp b/src/strategy/mage/GenericMageNonCombatStrategy.cpp index 8e7a475e..24da5125 100644 --- a/src/strategy/mage/GenericMageNonCombatStrategy.cpp +++ b/src/strategy/mage/GenericMageNonCombatStrategy.cpp @@ -4,7 +4,7 @@ */ #include "GenericMageNonCombatStrategy.h" - +#include "AiFactory.h" #include "Playerbots.h" class GenericMageNonCombatStrategyActionNodeFactory : public NamedObjectFactory @@ -52,35 +52,23 @@ void GenericMageNonCombatStrategy::InitTriggers(std::vector& trigg { NonCombatStrategy::InitTriggers(triggers); - triggers.push_back( - new TriggerNode("arcane intellect", NextAction::array(0, new NextAction("arcane intellect", 21.0f), nullptr))); - - triggers.push_back( - new TriggerNode("no focus magic", NextAction::array(0, new NextAction("focus magic on party", 19.0f), nullptr))); - // triggers.push_back(new TriggerNode("no drink", NextAction::array(0, new NextAction("conjure water", 16.0f), - // nullptr))); triggers.push_back(new TriggerNode("no food", NextAction::array(0, new NextAction("conjure - // food", 15.0f), nullptr))); + triggers.push_back(new TriggerNode("arcane intellect", NextAction::array(0, new NextAction("arcane intellect", 21.0f), nullptr))); + triggers.push_back(new TriggerNode("no focus magic", NextAction::array(0, new NextAction("focus magic on party", 19.0f), nullptr))); triggers.push_back(new TriggerNode("often", NextAction::array(0, new NextAction("apply oil", 1.0f), nullptr))); + triggers.push_back(new TriggerNode("no mana gem", NextAction::array(0, new NextAction("conjure mana gem", 20.0f), nullptr))); } void MageBuffManaStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back( - new TriggerNode("mage armor", NextAction::array(0, new NextAction("mage armor", 19.0f), nullptr))); + triggers.push_back(new TriggerNode("mage armor", NextAction::array(0, new NextAction("mage armor", 19.0f), nullptr))); } void MageBuffDpsStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back( - new TriggerNode("mage armor", NextAction::array(0, new NextAction("molten armor", 19.0f), nullptr))); + triggers.push_back(new TriggerNode("mage armor", NextAction::array(0, new NextAction("molten armor", 19.0f), nullptr))); } void MageBuffStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back( - new TriggerNode("arcane intellect on party", - NextAction::array(0, new NextAction("arcane intellect on party", 20.0f), nullptr))); - // triggers.push_back(new TriggerNode("give water", NextAction::array(0, new NextAction("give water", 14.0f), - // nullptr))); triggers.push_back(new TriggerNode("give food", NextAction::array(0, new NextAction("give - // food", 13.0f), nullptr))); + triggers.push_back(new TriggerNode("arcane intellect on party", NextAction::array(0, new NextAction("arcane intellect on party", 20.0f), nullptr))); } diff --git a/src/strategy/mage/GenericMageStrategy.cpp b/src/strategy/mage/GenericMageStrategy.cpp index 8821a9e7..53494898 100644 --- a/src/strategy/mage/GenericMageStrategy.cpp +++ b/src/strategy/mage/GenericMageStrategy.cpp @@ -4,7 +4,7 @@ */ #include "GenericMageStrategy.h" - +#include "AiFactory.h" #include "Playerbots.h" #include "RangedCombatStrategy.h" @@ -160,49 +160,90 @@ void GenericMageStrategy::InitTriggers(std::vector& triggers) { RangedCombatStrategy::InitTriggers(triggers); - // triggers.push_back(new TriggerNode("enemy out of spell", NextAction::array(0, new NextAction("reach spell", - // ACTION_MOVE + 9), nullptr))); - triggers.push_back( - new TriggerNode("enemy is close", NextAction::array(0, new NextAction("frost nova", 50.0f), nullptr))); - triggers.push_back( - new TriggerNode("counterspell on enemy healer", - NextAction::array(0, new NextAction("counterspell on enemy healer", 40.0f), nullptr))); - triggers.push_back( - new TriggerNode("critical health", NextAction::array(0, new NextAction("ice block", 80.0f), nullptr))); - triggers.push_back( - new TriggerNode("spellsteal", NextAction::array(0, new NextAction("spellsteal", 40.0f), nullptr))); - triggers.push_back( - new TriggerNode("medium threat", NextAction::array(0, new NextAction("invisibility", 60.0f), nullptr))); - triggers.push_back( - new TriggerNode("low mana", NextAction::array(0, new NextAction("evocation", ACTION_EMERGENCY + 5), nullptr))); - triggers.push_back( - new TriggerNode("fire ward", NextAction::array(0, new NextAction("fire ward", ACTION_EMERGENCY), nullptr))); - triggers.push_back( - new TriggerNode("frost ward", NextAction::array(0, new NextAction("frost ward", ACTION_EMERGENCY), nullptr))); - - triggers.push_back(new TriggerNode("enemy too close for spell", - NextAction::array(0, new NextAction("blink back", ACTION_MOVE + 5), nullptr))); + // Threat Triggers + triggers.push_back(new TriggerNode("high threat", NextAction::array(0, new NextAction("mirror image", 60.0f), nullptr))); + triggers.push_back(new TriggerNode("medium threat", NextAction::array(0, new NextAction("invisibility", 30.0f), nullptr))); + + // Defensive Triggers + triggers.push_back(new TriggerNode("critical health", NextAction::array(0, new NextAction("ice block", 90.0f), nullptr))); + triggers.push_back(new TriggerNode("low health", NextAction::array(0, new NextAction("mana shield", 85.0f), nullptr))); + triggers.push_back(new TriggerNode("fire ward", NextAction::array(0, new NextAction("fire ward", 90.0f), nullptr))); + triggers.push_back(new TriggerNode("frost ward", NextAction::array(0, new NextAction("frost ward", 90.0f), nullptr))); + triggers.push_back(new TriggerNode("enemy is close and no firestarter strategy", NextAction::array(0, new NextAction("frost nova", 50.0f), nullptr))); + triggers.push_back(new TriggerNode("enemy too close for spell and no firestarter strategy", NextAction::array(0, new NextAction("blink back", 35.0f), nullptr))); + + // Mana Threshold Triggers + Player* bot = botAI->GetBot(); + if (bot->HasSpell(42985)) // Mana Sapphire + triggers.push_back(new TriggerNode("high mana", NextAction::array(0, new NextAction("use mana sapphire", 90.0f), nullptr))); + else if (bot->HasSpell(27101)) // Mana Emerald + triggers.push_back(new TriggerNode("high mana", NextAction::array(0, new NextAction("use mana emerald", 90.0f), nullptr))); + else if (bot->HasSpell(10054)) // Mana Ruby + triggers.push_back(new TriggerNode("high mana", NextAction::array(0, new NextAction("use mana ruby", 90.0f), nullptr))); + else if (bot->HasSpell(10053)) // Mana Citrine + triggers.push_back(new TriggerNode("high mana", NextAction::array(0, new NextAction("use mana citrine", 90.0f), nullptr))); + else if (bot->HasSpell(3552)) // Mana Jade + triggers.push_back(new TriggerNode("high mana", NextAction::array(0, new NextAction("use mana jade", 90.0f), nullptr))); + else if (bot->HasSpell(759)) // Mana Agate + triggers.push_back(new TriggerNode("high mana", NextAction::array(0, new NextAction("use mana agate", 90.0f), nullptr))); + + triggers.push_back(new TriggerNode("medium mana", NextAction::array(0, new NextAction("mana potion", 90.0f), nullptr))); + triggers.push_back(new TriggerNode("low mana", NextAction::array(0, new NextAction("evocation", 90.0f), nullptr))); + + // Counterspell / Spellsteal Triggers + triggers.push_back(new TriggerNode("spellsteal", NextAction::array(0, new NextAction("spellsteal", 40.0f), nullptr))); + triggers.push_back(new TriggerNode("counterspell on enemy healer", NextAction::array(0, new NextAction("counterspell on enemy healer", 40.0f), nullptr))); } void MageCureStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back( - new TriggerNode("remove curse", NextAction::array(0, new NextAction("remove curse", 41.0f), nullptr))); - triggers.push_back(new TriggerNode("remove curse on party", - NextAction::array(0, new NextAction("remove curse on party", 40.0f), nullptr))); + triggers.push_back(new TriggerNode("remove curse", NextAction::array(0, new NextAction("remove curse", 41.0f), nullptr))); + triggers.push_back(new TriggerNode("remove curse on party", NextAction::array(0, new NextAction("remove curse on party", 40.0f), nullptr))); } void MageBoostStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode("icy veins", NextAction::array(0, new NextAction("icy veins", 50.0f), nullptr))); - triggers.push_back( - new TriggerNode("presence of mind", NextAction::array(0, new NextAction("presence of mind", 42.0f), nullptr))); - // triggers.push_back(new TriggerNode("arcane power", NextAction::array(0, new NextAction("arcane power", 41.0f), nullptr))); - triggers.push_back( - new TriggerNode("mirror image", NextAction::array(0, new NextAction("mirror image", 41.0f), nullptr))); } void MageCcStrategy::InitTriggers(std::vector& triggers) { triggers.push_back(new TriggerNode("polymorph", NextAction::array(0, new NextAction("polymorph", 30.0f), nullptr))); } + +void MageAoeStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back(new TriggerNode("blizzard channel check", NextAction::array(0, new NextAction("cancel channel", 26.0f), nullptr))); + + Player* bot = botAI->GetBot(); + int tab = AiFactory::GetPlayerSpecTab(bot); + + if (tab == 0) // Arcane + { + triggers.push_back(new TriggerNode("flamestrike active and medium aoe", NextAction::array(0, new NextAction("blizzard", 24.0f), nullptr))); + triggers.push_back(new TriggerNode("medium aoe", NextAction::array(0, + new NextAction("flamestrike", 23.0f), + new NextAction("blizzard", 22.0f), nullptr))); + triggers.push_back(new TriggerNode("light aoe", NextAction::array(0, new NextAction("arcane explosion", 21.0f), nullptr))); + } + else if (tab == 1) // Fire and Frostfire + { + triggers.push_back( + new TriggerNode("medium aoe", NextAction::array(0, + new NextAction("dragon's breath", 39.0f), + new NextAction("blast wave", 38.0f), + new NextAction("flamestrike", 23.0f), + new NextAction("blizzard", 22.0f), nullptr))); + + triggers.push_back(new TriggerNode("flamestrike active and medium aoe", NextAction::array(0, new NextAction("blizzard", 24.0f), nullptr))); + triggers.push_back(new TriggerNode("firestarter", NextAction::array(0, new NextAction("flamestrike", 40.0f), nullptr))); + triggers.push_back(new TriggerNode("living bomb on attackers", NextAction::array(0, new NextAction("living bomb on attackers", 21.0f), nullptr))); + } + else if (tab == 2) // Frost + { + triggers.push_back(new TriggerNode("flamestrike active and medium aoe", NextAction::array(0, new NextAction("blizzard", 24.0f), nullptr))); + triggers.push_back(new TriggerNode("medium aoe", NextAction::array(0, + new NextAction("flamestrike", 23.0f), + new NextAction("blizzard", 22.0f), nullptr))); + triggers.push_back(new TriggerNode("light aoe", NextAction::array(0, new NextAction("cone of cold", 21.0f), nullptr))); + } +} diff --git a/src/strategy/mage/GenericMageStrategy.h b/src/strategy/mage/GenericMageStrategy.h index 4657eb95..66ac8de5 100644 --- a/src/strategy/mage/GenericMageStrategy.h +++ b/src/strategy/mage/GenericMageStrategy.h @@ -51,4 +51,13 @@ public: std::string const getName() override { return "cc"; } }; +class MageAoeStrategy : public CombatStrategy +{ +public: + MageAoeStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {} + + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "aoe"; } +}; + #endif diff --git a/src/strategy/mage/MageActions.cpp b/src/strategy/mage/MageActions.cpp index b32c77fa..a2fa4045 100644 --- a/src/strategy/mage/MageActions.cpp +++ b/src/strategy/mage/MageActions.cpp @@ -5,7 +5,7 @@ #include "MageActions.h" #include - +#include "UseItemAction.h" #include "PlayerbotAIConfig.h" #include "Playerbots.h" #include "ServerFacade.h" @@ -13,6 +13,42 @@ Value* CastPolymorphAction::GetTargetValue() { return context->GetValue("cc target", getName()); } +bool UseManaSapphireAction::isUseful() +{ + Player* bot = botAI->GetBot(); + return AI_VALUE2(bool, "combat", "self target") && bot->GetItemCount(33312, false) > 0; // Mana Sapphire +} + +bool UseManaEmeraldAction::isUseful() +{ + Player* bot = botAI->GetBot(); + return AI_VALUE2(bool, "combat", "self target") && bot->GetItemCount(22044, false) > 0; // Mana Emerald +} + +bool UseManaRubyAction::isUseful() +{ + Player* bot = botAI->GetBot(); + return AI_VALUE2(bool, "combat", "self target") && bot->GetItemCount(8008, false) > 0; // Mana Ruby +} + +bool UseManaCitrineAction::isUseful() +{ + Player* bot = botAI->GetBot(); + return AI_VALUE2(bool, "combat", "self target") && bot->GetItemCount(8007, false) > 0; // Mana Citrine +} + +bool UseManaJadeAction::isUseful() +{ + Player* bot = botAI->GetBot(); + return AI_VALUE2(bool, "combat", "self target") && bot->GetItemCount(5513, false) > 0; // Mana Jade +} + +bool UseManaAgateAction::isUseful() +{ + Player* bot = botAI->GetBot(); + return AI_VALUE2(bool, "combat", "self target") && bot->GetItemCount(5514, false) > 0; // Mana Agate +} + bool CastFrostNovaAction::isUseful() { Unit* target = AI_VALUE(Unit*, "current target"); @@ -105,4 +141,14 @@ bool CastBlinkBackAction::Execute(Event event) // can cast spell check passed in isUseful() bot->SetOrientation(bot->GetAngle(target) + M_PI); return CastSpellAction::Execute(event); -} \ No newline at end of file +} + +bool CancelChannelAction::Execute(Event event) +{ + if (bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL)) + { + bot->InterruptSpell(CURRENT_CHANNELED_SPELL); + return true; + } + return false; +} diff --git a/src/strategy/mage/MageActions.h b/src/strategy/mage/MageActions.h index 48efd9e8..cf52a9a8 100644 --- a/src/strategy/mage/MageActions.h +++ b/src/strategy/mage/MageActions.h @@ -8,11 +8,280 @@ #include "GenericSpellActions.h" #include "SharedDefines.h" +#include "UseItemAction.h" class PlayerbotAI; -BUFF_ACTION(CastFireWardAction, "fire ward"); -BUFF_ACTION(CastFrostWardAction, "frost ward"); +// Buff and Out of Combat Actions + +class CastMoltenArmorAction : public CastBuffSpellAction +{ +public: + CastMoltenArmorAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "molten armor") {} +}; + +class CastMageArmorAction : public CastBuffSpellAction +{ +public: + CastMageArmorAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "mage armor") {} +}; + +class CastIceArmorAction : public CastBuffSpellAction +{ +public: + CastIceArmorAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "ice armor") {} +}; + +class CastFrostArmorAction : public CastBuffSpellAction +{ +public: + CastFrostArmorAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "frost armor") {} +}; + +class CastArcaneIntellectAction : public CastBuffSpellAction +{ +public: + CastArcaneIntellectAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "arcane intellect") {} +}; + +class CastArcaneIntellectOnPartyAction : public BuffOnPartyAction +{ +public: + CastArcaneIntellectOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "arcane intellect") {} +}; + +class CastFocusMagicOnPartyAction : public CastSpellAction +{ +public: + CastFocusMagicOnPartyAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "focus magic") {} + Unit* GetTarget() override; +}; + +class CastSummonWaterElementalAction : public CastBuffSpellAction +{ +public: + CastSummonWaterElementalAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "summon water elemental") {} +}; + +// Boost Actions + +class CastCombustionAction : public CastBuffSpellAction +{ +public: + CastCombustionAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "combustion") {} +}; + +class CastArcanePowerAction : public CastBuffSpellAction +{ +public: + CastArcanePowerAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "arcane power") {} +}; + +class CastPresenceOfMindAction : public CastBuffSpellAction +{ +public: + CastPresenceOfMindAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "presence of mind") {} +}; + +class CastIcyVeinsAction : public CastBuffSpellAction +{ +public: + CastIcyVeinsAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "icy veins") {} +}; + +class CastColdSnapAction : public CastBuffSpellAction +{ +public: + CastColdSnapAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "cold snap") {} +}; + +// Defensive Actions + +class CastFireWardAction : public CastBuffSpellAction +{ +public: + CastFireWardAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "fire ward") {} +}; + +class CastFrostWardAction : public CastBuffSpellAction +{ +public: + CastFrostWardAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "frost ward") {} +}; + +class CastIceBarrierAction : public CastBuffSpellAction +{ +public: + CastIceBarrierAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "ice barrier") {} +}; + +class CastInvisibilityAction : public CastBuffSpellAction +{ +public: + CastInvisibilityAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "invisibility") {} +}; +class CastIceBlockAction : public CastBuffSpellAction +{ +public: + CastIceBlockAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "ice block") {} +}; + +class CastMirrorImageAction : public CastBuffSpellAction +{ +public: + CastMirrorImageAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "mirror image") {} +}; + +class CastBlinkBackAction : public CastSpellAction +{ +public: + CastBlinkBackAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "blink") {} + bool Execute(Event event) override; +}; + +class CastManaShieldAction : public CastBuffSpellAction +{ +public: + CastManaShieldAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "mana shield") {} +}; + +// Utility Actions + +class CastEvocationAction : public CastSpellAction +{ +public: + CastEvocationAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "evocation") {} + std::string const GetTargetName() override { return "self target"; } +}; + +class CastConjureManaGemAction : public CastBuffSpellAction +{ +public: + CastConjureManaGemAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "conjure mana gem") {} +}; + +class CastConjureFoodAction : public CastBuffSpellAction +{ +public: + CastConjureFoodAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "conjure food") {} +}; + +class CastConjureWaterAction : public CastBuffSpellAction +{ +public: + CastConjureWaterAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "conjure water") {} +}; + +class UseManaSapphireAction : public UseItemAction +{ +public: + UseManaSapphireAction(PlayerbotAI* botAI) : UseItemAction(botAI, "mana sapphire") {} + bool isUseful() override; +}; +class UseManaEmeraldAction : public UseItemAction +{ +public: + UseManaEmeraldAction(PlayerbotAI* botAI) : UseItemAction(botAI, "mana emerald") {} + bool isUseful() override; +}; + +class UseManaRubyAction : public UseItemAction +{ +public: + UseManaRubyAction(PlayerbotAI* botAI) : UseItemAction(botAI, "mana ruby") {} + bool isUseful() override; +}; + +class UseManaCitrineAction : public UseItemAction +{ +public: + UseManaCitrineAction(PlayerbotAI* botAI) : UseItemAction(botAI, "mana citrine") {} + bool isUseful() override; +}; + +class UseManaJadeAction : public UseItemAction +{ +public: + UseManaJadeAction(PlayerbotAI* botAI) : UseItemAction(botAI, "mana jade") {} + bool isUseful() override; +}; + +class UseManaAgateAction : public UseItemAction +{ +public: + UseManaAgateAction(PlayerbotAI* botAI) : UseItemAction(botAI, "mana agate") {} + bool isUseful() override; +}; + +// CC, Interrupt, and Dispel Actions + +class CastPolymorphAction : public CastBuffSpellAction +{ +public: + CastPolymorphAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "polymorph") {} + Value* GetTargetValue() override; +}; + +class CastSpellstealAction : public CastSpellAction +{ +public: + CastSpellstealAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "spellsteal") {} +}; + +class CastCounterspellAction : public CastSpellAction +{ +public: + CastCounterspellAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "counterspell") {} +}; + +class CastCounterspellOnEnemyHealerAction : public CastSpellOnEnemyHealerAction +{ +public: + CastCounterspellOnEnemyHealerAction(PlayerbotAI* botAI) : CastSpellOnEnemyHealerAction(botAI, "counterspell") {} +}; + +class CastFrostNovaAction : public CastSpellAction +{ +public: + CastFrostNovaAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "frost nova") {} + bool isUseful() override; +}; + +class CastDeepFreezeAction : public CastSpellAction +{ +public: + CastDeepFreezeAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "deep freeze") {} + bool isPossible() override { return true; } +}; + +class CastRemoveCurseAction : public CastCureSpellAction +{ +public: + CastRemoveCurseAction(PlayerbotAI* botAI) : CastCureSpellAction(botAI, "remove curse") {} +}; + +class CastRemoveLesserCurseAction : public CastCureSpellAction +{ +public: + CastRemoveLesserCurseAction(PlayerbotAI* botAI) : CastCureSpellAction(botAI, "remove lesser curse") {} +}; + +class CastRemoveCurseOnPartyAction : public CurePartyMemberAction +{ +public: + CastRemoveCurseOnPartyAction(PlayerbotAI* botAI) : CurePartyMemberAction(botAI, "remove curse", DISPEL_CURSE) {} +}; + +class CastRemoveLesserCurseOnPartyAction : public CurePartyMemberAction +{ +public: + CastRemoveLesserCurseOnPartyAction(PlayerbotAI* botAI) + : CurePartyMemberAction(botAI, "remove lesser curse", DISPEL_CURSE) + { + } +}; + +// Damage and Debuff Actions class CastFireballAction : public CastSpellAction { @@ -57,18 +326,26 @@ public: CastPyroblastAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "pyroblast") {} }; -class CastFlamestrikeAction : public CastDebuffSpellAction +class CastLivingBombAction : public CastDebuffSpellAction { public: - CastFlamestrikeAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "flamestrike", true, 0.0f) {} - ActionThreatType getThreatType() override { return ActionThreatType::Aoe; } + CastLivingBombAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "living bomb", true) {} + bool isUseful() override + { + // Bypass TTL check + return CastAuraSpellAction::isUseful(); + } }; -class CastFrostNovaAction : public CastSpellAction +class CastLivingBombOnAttackersAction : public CastDebuffSpellOnAttackerAction { public: - CastFrostNovaAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "frost nova") {} - bool isUseful() override; + CastLivingBombOnAttackersAction(PlayerbotAI* botAI) : CastDebuffSpellOnAttackerAction(botAI, "living bomb", true) {} + bool isUseful() override + { + // Bypass TTL check + return CastAuraSpellAction::isUseful(); + } }; class CastFrostboltAction : public CastSpellAction @@ -89,12 +366,6 @@ public: CastIceLanceAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "ice lance") {} }; -class CastDeepFreezeAction : public CastSpellAction -{ -public: - CastDeepFreezeAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "deep freeze") {} -}; - class CastBlizzardAction : public CastSpellAction { public: @@ -110,143 +381,11 @@ public: bool isUseful() override; }; -class CastArcaneIntellectAction : public CastBuffSpellAction +class CastFlamestrikeAction : public CastDebuffSpellAction { public: - CastArcaneIntellectAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "arcane intellect") {} -}; - -class CastArcaneIntellectOnPartyAction : public BuffOnPartyAction -{ -public: - CastArcaneIntellectOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "arcane intellect") {} -}; - -class CastRemoveCurseAction : public CastCureSpellAction -{ -public: - CastRemoveCurseAction(PlayerbotAI* botAI) : CastCureSpellAction(botAI, "remove curse") {} -}; - -class CastRemoveLesserCurseAction : public CastCureSpellAction -{ -public: - CastRemoveLesserCurseAction(PlayerbotAI* botAI) : CastCureSpellAction(botAI, "remove lesser curse") {} -}; - -class CastIcyVeinsAction : public CastBuffSpellAction -{ -public: - CastIcyVeinsAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "icy veins") {} -}; - -class CastColdSnapAction : public CastBuffSpellAction -{ -public: - CastColdSnapAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "cold snap") {} -}; - -class CastIceBarrierAction : public CastBuffSpellAction -{ -public: - CastIceBarrierAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "ice barrier") {} -}; - -class CastSummonWaterElementalAction : public CastBuffSpellAction -{ -public: - CastSummonWaterElementalAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "summon water elemental") {} -}; - -class CastCombustionAction : public CastBuffSpellAction -{ -public: - CastCombustionAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "combustion") {} -}; - -BEGIN_SPELL_ACTION(CastCounterspellAction, "counterspell") -END_SPELL_ACTION() - -class CastRemoveCurseOnPartyAction : public CurePartyMemberAction -{ -public: - CastRemoveCurseOnPartyAction(PlayerbotAI* botAI) : CurePartyMemberAction(botAI, "remove curse", DISPEL_CURSE) {} -}; - -class CastRemoveLesserCurseOnPartyAction : public CurePartyMemberAction -{ -public: - CastRemoveLesserCurseOnPartyAction(PlayerbotAI* botAI) - : CurePartyMemberAction(botAI, "remove lesser curse", DISPEL_CURSE) - { - } -}; - -class CastConjureFoodAction : public CastBuffSpellAction -{ -public: - CastConjureFoodAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "conjure food") {} -}; - -class CastConjureWaterAction : public CastBuffSpellAction -{ -public: - CastConjureWaterAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "conjure water") {} -}; - -class CastIceBlockAction : public CastBuffSpellAction -{ -public: - CastIceBlockAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "ice block") {} -}; - -class CastMoltenArmorAction : public CastBuffSpellAction -{ -public: - CastMoltenArmorAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "molten armor") {} -}; - -class CastMageArmorAction : public CastBuffSpellAction -{ -public: - CastMageArmorAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "mage armor") {} -}; - -class CastIceArmorAction : public CastBuffSpellAction -{ -public: - CastIceArmorAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "ice armor") {} -}; - -class CastFrostArmorAction : public CastBuffSpellAction -{ -public: - CastFrostArmorAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "frost armor") {} -}; - -class CastPolymorphAction : public CastBuffSpellAction -{ -public: - CastPolymorphAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "polymorph") {} - Value* GetTargetValue() override; -}; - -class CastSpellstealAction : public CastSpellAction -{ -public: - CastSpellstealAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "spellsteal") {} -}; - -class CastLivingBombAction : public CastDebuffSpellAction -{ -public: - CastLivingBombAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "living bomb", true) {} -}; - -class CastLivingBombOnAttackersAction : public CastDebuffSpellOnAttackerAction -{ -public: - CastLivingBombOnAttackersAction(PlayerbotAI* botAI) : CastDebuffSpellOnAttackerAction(botAI, "living bomb", true) {} + CastFlamestrikeAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "flamestrike", true, 0.0f) {} + ActionThreatType getThreatType() override { return ActionThreatType::Aoe; } }; class CastDragonsBreathAction : public CastSpellAction @@ -265,55 +404,12 @@ public: bool isUseful() override; }; -class CastInvisibilityAction : public CastBuffSpellAction +class CancelChannelAction : public Action { public: - CastInvisibilityAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "invisibility") {} -}; + CancelChannelAction(PlayerbotAI* botAI) : Action(botAI, "cancel channel") {} -class CastEvocationAction : public CastSpellAction -{ -public: - CastEvocationAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "evocation") {} - std::string const GetTargetName() override { return "self target"; } -}; - -class CastCounterspellOnEnemyHealerAction : public CastSpellOnEnemyHealerAction -{ -public: - CastCounterspellOnEnemyHealerAction(PlayerbotAI* botAI) : CastSpellOnEnemyHealerAction(botAI, "counterspell") {} -}; - -class CastArcanePowerAction : public CastBuffSpellAction -{ -public: - CastArcanePowerAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "arcane power") {} -}; - -class CastPresenceOfMindAction : public CastBuffSpellAction -{ -public: - CastPresenceOfMindAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "presence of mind") {} -}; - -class CastMirrorImageAction : public CastBuffSpellAction -{ -public: - CastMirrorImageAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "mirror image") {} -}; - -class CastFocusMagicOnPartyAction : public CastSpellAction -{ -public: - CastFocusMagicOnPartyAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "focus magic") {} - Unit* GetTarget() override; -}; - -class CastBlinkBackAction : public CastSpellAction -{ -public: - CastBlinkBackAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "blink") {} - bool Execute(Event event) override; }; + #endif diff --git a/src/strategy/mage/MageAiObjectContext.cpp b/src/strategy/mage/MageAiObjectContext.cpp index ba1e4e2d..83de0705 100644 --- a/src/strategy/mage/MageAiObjectContext.cpp +++ b/src/strategy/mage/MageAiObjectContext.cpp @@ -4,7 +4,6 @@ */ #include "MageAiObjectContext.h" - #include "ArcaneMageStrategy.h" #include "FireMageStrategy.h" #include "FrostFireMageStrategy.h" @@ -23,27 +22,23 @@ public: { creators["nc"] = &MageStrategyFactoryInternal::nc; creators["pull"] = &MageStrategyFactoryInternal::pull; - creators["fire aoe"] = &MageStrategyFactoryInternal::fire_aoe; - creators["frostfire aoe"] = &MageStrategyFactoryInternal::frostfire_aoe; - creators["frost aoe"] = &MageStrategyFactoryInternal::frost_aoe; - creators["arcane aoe"] = &MageStrategyFactoryInternal::arcane_aoe; + creators["aoe"] = &MageStrategyFactoryInternal::aoe; creators["cure"] = &MageStrategyFactoryInternal::cure; creators["buff"] = &MageStrategyFactoryInternal::buff; creators["boost"] = &MageStrategyFactoryInternal::boost; creators["cc"] = &MageStrategyFactoryInternal::cc; + creators["firestarter"] = &MageStrategyFactoryInternal::firestarter; } private: static Strategy* nc(PlayerbotAI* botAI) { return new GenericMageNonCombatStrategy(botAI); } static Strategy* pull(PlayerbotAI* botAI) { return new PullStrategy(botAI, "shoot"); } - static Strategy* fire_aoe(PlayerbotAI* botAI) { return new FireMageAoeStrategy(botAI); } - static Strategy* frostfire_aoe(PlayerbotAI* botAI) { return new FrostFireMageAoeStrategy(botAI); } - static Strategy* frost_aoe(PlayerbotAI* botAI) { return new FrostMageAoeStrategy(botAI); } - static Strategy* arcane_aoe(PlayerbotAI* botAI) { return new ArcaneMageAoeStrategy(botAI); } + static Strategy* aoe(PlayerbotAI* botAI) { return new MageAoeStrategy(botAI); } static Strategy* cure(PlayerbotAI* botAI) { return new MageCureStrategy(botAI); } static Strategy* buff(PlayerbotAI* botAI) { return new MageBuffStrategy(botAI); } static Strategy* boost(PlayerbotAI* botAI) { return new MageBoostStrategy(botAI); } static Strategy* cc(PlayerbotAI* botAI) { return new MageCcStrategy(botAI); } + static Strategy* firestarter(PlayerbotAI* botAI) { return new FirestarterStrategy(botAI); } }; class MageCombatStrategyFactoryInternal : public NamedObjectContext @@ -86,8 +81,7 @@ public: creators["fireball"] = &MageTriggerFactoryInternal::fireball; creators["pyroblast"] = &MageTriggerFactoryInternal::pyroblast; creators["combustion"] = &MageTriggerFactoryInternal::combustion; - creators["fingers of frost single"] = &MageTriggerFactoryInternal::fingers_of_frost_single; - creators["fingers of frost double"] = &MageTriggerFactoryInternal::fingers_of_frost_double; + creators["fingers of frost"] = &MageTriggerFactoryInternal::fingers_of_frost; creators["brain freeze"] = &MageTriggerFactoryInternal::brain_freeze; creators["icy veins"] = &MageTriggerFactoryInternal::icy_veins; creators["cold snap"] = &MageTriggerFactoryInternal::cold_snap; @@ -102,6 +96,7 @@ public: creators["spellsteal"] = &MageTriggerFactoryInternal::spellsteal; creators["hot streak"] = &MageTriggerFactoryInternal::hot_streak; creators["living bomb"] = &MageTriggerFactoryInternal::living_bomb; + creators["living bomb on attackers"] = &MageTriggerFactoryInternal::living_bomb_on_attackers; creators["missile barrage"] = &MageTriggerFactoryInternal::missile_barrage; creators["arcane blast"] = &MageTriggerFactoryInternal::arcane_blast; creators["counterspell on enemy healer"] = &MageTriggerFactoryInternal::counterspell_enemy_healer; @@ -115,6 +110,20 @@ public: creators["frostbite on target"] = &MageTriggerFactoryInternal::frostbite_on_target; creators["no focus magic"] = &MageTriggerFactoryInternal::no_focus_magic; creators["frostfire bolt"] = &MageTriggerFactoryInternal::frostfire_bolt; + creators["firestarter"] = &MageTriggerFactoryInternal::firestarter; + creators["improved scorch"] = &MageTriggerFactoryInternal::improved_scorch; + creators["flamestrike nearby"] = &MageTriggerFactoryInternal::flamestrike_nearby; + creators["flamestrike active and medium aoe"] = &MageTriggerFactoryInternal::flamestrike_blizzard; + creators["arcane blast 4 stacks and missile barrage"] = &MageTriggerFactoryInternal::arcane_blast_4_stacks_and_missile_barrage; + creators["icy veins on cd"] = &MageTriggerFactoryInternal::icy_veins_on_cd; + creators["deep freeze on cd"] = &MageTriggerFactoryInternal::deep_freeze_on_cd; + creators["no mana gem"] = &MageTriggerFactoryInternal::NoManaGem; + creators["blizzard channel check"] = &MageTriggerFactoryInternal::blizzard_channel_check; + creators["blast wave off cd"] = &MageTriggerFactoryInternal::blast_wave_off_cd; + creators["blast wave off cd and medium aoe"] = &MageTriggerFactoryInternal::blast_wave_off_cd_and_medium_aoe; + creators["no firestarter strategy"] = &MageTriggerFactoryInternal::no_firestarter_strategy; + creators["enemy is close and no firestarter strategy"] = &MageTriggerFactoryInternal::enemy_is_close_and_no_firestarter_strategy; + creators["enemy too close for spell and no firestarter strategy"] = &MageTriggerFactoryInternal::enemy_too_close_for_spell_and_no_firestarter_strategy; } private: @@ -126,8 +135,7 @@ private: static Trigger* fireball(PlayerbotAI* botAI) { return new FireballTrigger(botAI); } static Trigger* pyroblast(PlayerbotAI* botAI) { return new PyroblastTrigger(botAI); } static Trigger* combustion(PlayerbotAI* botAI) { return new CombustionTrigger(botAI); } - static Trigger* fingers_of_frost_single(PlayerbotAI* botAI) { return new FingersOfFrostSingleTrigger(botAI); } - static Trigger* fingers_of_frost_double(PlayerbotAI* botAI) { return new FingersOfFrostDoubleTrigger(botAI); } + static Trigger* fingers_of_frost(PlayerbotAI* botAI) { return new FingersOfFrostTrigger(botAI); } static Trigger* brain_freeze(PlayerbotAI* botAI) { return new BrainFreezeTrigger(botAI); } static Trigger* icy_veins(PlayerbotAI* botAI) { return new IcyVeinsTrigger(botAI); } static Trigger* cold_snap(PlayerbotAI* botAI) { return new ColdSnapTrigger(botAI); } @@ -141,6 +149,7 @@ private: static Trigger* polymorph(PlayerbotAI* botAI) { return new PolymorphTrigger(botAI); } static Trigger* spellsteal(PlayerbotAI* botAI) { return new SpellstealTrigger(botAI); } static Trigger* living_bomb(PlayerbotAI* botAI) { return new LivingBombTrigger(botAI); } + static Trigger* living_bomb_on_attackers(PlayerbotAI* botAI) { return new LivingBombOnAttackersTrigger(botAI); } static Trigger* missile_barrage(PlayerbotAI* botAI) { return new MissileBarrageTrigger(botAI); } static Trigger* arcane_blast(PlayerbotAI* botAI) { return new ArcaneBlastTrigger(botAI); } static Trigger* counterspell_enemy_healer(PlayerbotAI* botAI) { return new CounterspellEnemyHealerTrigger(botAI); } @@ -150,6 +159,20 @@ private: static Trigger* frostbite_on_target(PlayerbotAI* botAI) { return new FrostbiteOnTargetTrigger(botAI); } static Trigger* no_focus_magic(PlayerbotAI* botAI) { return new NoFocusMagicTrigger(botAI); } static Trigger* frostfire_bolt(PlayerbotAI* botAI) { return new FrostfireBoltTrigger(botAI); } + static Trigger* improved_scorch(PlayerbotAI* botAI) { return new ImprovedScorchTrigger(botAI); } + static Trigger* firestarter(PlayerbotAI* botAI) { return new FirestarterTrigger(botAI); } + static Trigger* flamestrike_nearby(PlayerbotAI* botAI) { return new FlamestrikeNearbyTrigger(botAI); } + static Trigger* flamestrike_blizzard(PlayerbotAI* botAI) { return new FlamestrikeBlizzardTrigger(botAI); } + static Trigger* arcane_blast_4_stacks_and_missile_barrage(PlayerbotAI* botAI) { return new ArcaneBlast4StacksAndMissileBarrageTrigger(botAI); } + static Trigger* icy_veins_on_cd(PlayerbotAI* botAI) { return new IcyVeinsCooldownTrigger(botAI); } + static Trigger* deep_freeze_on_cd(PlayerbotAI* botAI) { return new DeepFreezeCooldownTrigger(botAI); } + static Trigger* NoManaGem(PlayerbotAI* botAI) { return new NoManaGemTrigger(botAI); } + static Trigger* blizzard_channel_check(PlayerbotAI* botAI) { return new BlizzardChannelCheckTrigger(botAI); } + static Trigger* blast_wave_off_cd(PlayerbotAI* botAI) { return new BlastWaveOffCdTrigger(botAI); } + static Trigger* blast_wave_off_cd_and_medium_aoe(PlayerbotAI* botAI) { return new BlastWaveOffCdTriggerAndMediumAoeTrigger(botAI); } + static Trigger* no_firestarter_strategy(PlayerbotAI* botAI) { return new NoFirestarterStrategyTrigger(botAI); } + static Trigger* enemy_is_close_and_no_firestarter_strategy(PlayerbotAI* botAI) { return new EnemyIsCloseAndNoFirestarterStrategyTrigger(botAI); } + static Trigger* enemy_too_close_for_spell_and_no_firestarter_strategy(PlayerbotAI* botAI) { return new EnemyTooCloseForSpellAndNoFirestarterStrategyTrigger(botAI); } }; class MageAiObjectContextInternal : public NamedObjectContext @@ -170,6 +193,7 @@ public: creators["arcane intellect on party"] = &MageAiObjectContextInternal::arcane_intellect_on_party; creators["conjure water"] = &MageAiObjectContextInternal::conjure_water; creators["conjure food"] = &MageAiObjectContextInternal::conjure_food; + creators["conjure mana gem"] = &MageAiObjectContextInternal::conjure_mana_gem; creators["molten armor"] = &MageAiObjectContextInternal::molten_armor; creators["mage armor"] = &MageAiObjectContextInternal::mage_armor; creators["ice armor"] = &MageAiObjectContextInternal::ice_armor; @@ -207,6 +231,14 @@ public: creators["mirror image"] = &MageAiObjectContextInternal::mirror_image; creators["focus magic on party"] = &MageAiObjectContextInternal::focus_magic_on_party; creators["blink back"] = &MageAiObjectContextInternal::blink_back; + creators["use mana sapphire"] = &MageAiObjectContextInternal::use_mana_sapphire; + creators["use mana emerald"] = &MageAiObjectContextInternal::use_mana_emerald; + creators["use mana ruby"] = &MageAiObjectContextInternal::use_mana_ruby; + creators["use mana citrine"] = &MageAiObjectContextInternal::use_mana_citrine; + creators["use mana jade"] = &MageAiObjectContextInternal::use_mana_jade; + creators["use mana agate"] = &MageAiObjectContextInternal::use_mana_agate; + creators["cancel channel"] = &MageAiObjectContextInternal::cancel_channel; + creators["mana shield"] = &MageAiObjectContextInternal::mana_shield; } private: @@ -228,6 +260,7 @@ private: static Action* arcane_intellect_on_party(PlayerbotAI* botAI) { return new CastArcaneIntellectOnPartyAction(botAI); } static Action* conjure_water(PlayerbotAI* botAI) { return new CastConjureWaterAction(botAI); } static Action* conjure_food(PlayerbotAI* botAI) { return new CastConjureFoodAction(botAI); } + static Action* conjure_mana_gem(PlayerbotAI* botAI) { return new CastConjureManaGemAction(botAI); } static Action* molten_armor(PlayerbotAI* botAI) { return new CastMoltenArmorAction(botAI); } static Action* mage_armor(PlayerbotAI* botAI) { return new CastMageArmorAction(botAI); } static Action* ice_armor(PlayerbotAI* botAI) { return new CastIceArmorAction(botAI); } @@ -241,10 +274,7 @@ private: static Action* remove_curse(PlayerbotAI* botAI) { return new CastRemoveCurseAction(botAI); } static Action* remove_curse_on_party(PlayerbotAI* botAI) { return new CastRemoveCurseOnPartyAction(botAI); } static Action* remove_lesser_curse(PlayerbotAI* botAI) { return new CastRemoveLesserCurseAction(botAI); } - static Action* remove_lesser_curse_on_party(PlayerbotAI* botAI) - { - return new CastRemoveLesserCurseOnPartyAction(botAI); - } + static Action* remove_lesser_curse_on_party(PlayerbotAI* botAI) { return new CastRemoveLesserCurseOnPartyAction(botAI); } static Action* icy_veins(PlayerbotAI* botAI) { return new CastIcyVeinsAction(botAI); } static Action* cold_snap(PlayerbotAI* botAI) { return new CastColdSnapAction(botAI); } static Action* ice_barrier(PlayerbotAI* botAI) { return new CastIceBarrierAction(botAI); } @@ -259,13 +289,18 @@ private: static Action* blast_wave(PlayerbotAI* botAI) { return new CastBlastWaveAction(botAI); } static Action* invisibility(PlayerbotAI* botAI) { return new CastInvisibilityAction(botAI); } static Action* evocation(PlayerbotAI* botAI) { return new CastEvocationAction(botAI); } - static Action* counterspell_on_enemy_healer(PlayerbotAI* botAI) - { - return new CastCounterspellOnEnemyHealerAction(botAI); - } + static Action* counterspell_on_enemy_healer(PlayerbotAI* botAI) { return new CastCounterspellOnEnemyHealerAction(botAI); } static Action* mirror_image(PlayerbotAI* botAI) { return new CastMirrorImageAction(botAI); } static Action* focus_magic_on_party(PlayerbotAI* botAI) { return new CastFocusMagicOnPartyAction(botAI); } static Action* blink_back(PlayerbotAI* botAI) { return new CastBlinkBackAction(botAI); } + static Action* use_mana_sapphire(PlayerbotAI* botAI) { return new UseManaSapphireAction(botAI); } + static Action* use_mana_emerald(PlayerbotAI* botAI) { return new UseManaEmeraldAction(botAI); } + static Action* use_mana_ruby(PlayerbotAI* botAI) { return new UseManaRubyAction(botAI); } + static Action* use_mana_citrine(PlayerbotAI* botAI) { return new UseManaCitrineAction(botAI); } + static Action* use_mana_jade(PlayerbotAI* botAI) { return new UseManaJadeAction(botAI); } + static Action* use_mana_agate(PlayerbotAI* botAI) { return new UseManaAgateAction(botAI); } + static Action* cancel_channel(PlayerbotAI* botAI) { return new CancelChannelAction(botAI); } + static Action* mana_shield(PlayerbotAI* botAI) { return new CastManaShieldAction(botAI); } }; SharedNamedObjectContextList MageAiObjectContext::sharedStrategyContexts; @@ -309,4 +344,4 @@ void MageAiObjectContext::BuildSharedTriggerContexts(SharedNamedObjectContextLis void MageAiObjectContext::BuildSharedValueContexts(SharedNamedObjectContextList& valueContexts) { AiObjectContext::BuildSharedValueContexts(valueContexts); -} \ No newline at end of file +} diff --git a/src/strategy/mage/MageTriggers.cpp b/src/strategy/mage/MageTriggers.cpp index 956fa752..4d3a4945 100644 --- a/src/strategy/mage/MageTriggers.cpp +++ b/src/strategy/mage/MageTriggers.cpp @@ -4,9 +4,33 @@ */ #include "MageTriggers.h" - #include "MageActions.h" #include "Playerbots.h" +#include "Player.h" +#include "Spell.h" +#include "DynamicObject.h" +#include "Value.h" +#include "SpellAuraEffects.h" +#include "ServerFacade.h" + +bool NoManaGemTrigger::IsActive() +{ + static const std::vector gemIds = { + 33312, // Mana Sapphire + 22044, // Mana Emerald + 8008, // Mana Ruby + 8007, // Mana Citrine + 5513, // Mana Jade + 5514 // Mana Agate + }; + Player* bot = botAI->GetBot(); + for (uint32 gemId : gemIds) + { + if (bot->GetItemCount(gemId, false) > 0) // false = only in bags + return false; + } + return true; +} bool ArcaneIntellectOnPartyTrigger::IsActive() { @@ -25,25 +49,6 @@ bool MageArmorTrigger::IsActive() !botAI->HasAura("molten armor", target) && !botAI->HasAura("mage armor", target); } -bool FingersOfFrostSingleTrigger::IsActive() -{ - // Fingers of Frost "stack" count is always 1. - // The value is instead stored in the charges. - Aura* aura = botAI->GetAura("fingers of frost", bot, false, true, -1); - return (aura && aura->GetCharges() == 1); -} - -bool ArcaneBlastStackTrigger::IsActive() -{ - Aura* aura = botAI->GetAura(getName(), GetTarget(), false, true, 3); - if (!aura) - return false; - if (aura->GetStackAmount() >= 4) - return true; - bool hasMissileBarrage = botAI->HasAura(44401, bot); - return hasMissileBarrage; -} - bool FrostNovaOnTargetTrigger::IsActive() { Unit* target = GetTarget(); @@ -84,3 +89,95 @@ bool NoFocusMagicTrigger::IsActive() } return true; } + +bool DeepFreezeCooldownTrigger::IsActive() +{ + Player* bot = botAI->GetBot(); + static const uint32 DEEP_FREEZE_SPELL_ID = 44572; + + // If the bot does NOT have Deep Freeze, treat as "on cooldown" + if (!bot->HasSpell(DEEP_FREEZE_SPELL_ID)) + return true; + + // Otherwise, use the default cooldown logic + return SpellCooldownTrigger::IsActive(); +} + +const std::set FlamestrikeNearbyTrigger::FLAMESTRIKE_SPELL_IDS = {2120, 2121, 8422, 8423, 10215, + 10216, 27086, 42925, 42926}; + +bool FlamestrikeNearbyTrigger::IsActive() +{ + Player* bot = botAI->GetBot(); + + for (uint32 spellId : FLAMESTRIKE_SPELL_IDS) + { + Aura* aura = bot->GetAura(spellId, bot->GetGUID()); + if (!aura) + continue; + + DynamicObject* dynObj = aura->GetDynobjOwner(); + if (!dynObj) + continue; + + float dist = bot->GetDistance2d(dynObj->GetPositionX(), dynObj->GetPositionY()); + if (dist <= radius) + return true; + } + return false; +} + +bool ImprovedScorchTrigger::IsActive() +{ + Unit* target = GetTarget(); + if (!target || !target->IsAlive() || !target->IsInWorld()) + return false; + + // List of all spell IDs for Improved Scorch, Winter's Chill, and Shadow Mastery + static const uint32 ImprovedScorchExclusiveDebuffs[] = {// Shadow Mastery + 17794, 17797, 17798, 17799, 17800, + // Winter's Chill + 12579, + // Improved Scorch + 22959}; + + for (uint32 spellId : ImprovedScorchExclusiveDebuffs) + { + if (target->HasAura(spellId)) + return false; + } + + // Use default DebuffTrigger logic for the rest (only trigger if debuff is missing or expiring) + return DebuffTrigger::IsActive(); +} + +const std::set BlizzardChannelCheckTrigger::BLIZZARD_SPELL_IDS = { + 10, // Blizzard Rank 1 + 6141, // Blizzard Rank 2 + 8427, // Blizzard Rank 3 + 10185, // Blizzard Rank 4 + 10186, // Blizzard Rank 5 + 10187, // Blizzard Rank 6 + 27085, // Blizzard Rank 7 + 42938, // Blizzard Rank 8 + 42939 // Blizzard Rank 9 +}; + +bool BlizzardChannelCheckTrigger::IsActive() +{ + Player* bot = botAI->GetBot(); + + // Check if the bot is channeling a spell + if (Spell* spell = bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL)) + { + // Only trigger if the spell being channeled is Blizzard + if (BLIZZARD_SPELL_IDS.count(spell->m_spellInfo->Id)) + { + uint8 attackerCount = AI_VALUE(uint8, "attacker count"); + return attackerCount < minEnemies; + } + } + + // Not channeling Blizzard + return false; +} diff --git a/src/strategy/mage/MageTriggers.h b/src/strategy/mage/MageTriggers.h index 7adf92ec..772cb926 100644 --- a/src/strategy/mage/MageTriggers.h +++ b/src/strategy/mage/MageTriggers.h @@ -9,11 +9,15 @@ #include "CureTriggers.h" #include "GenericTriggers.h" #include "SharedDefines.h" +#include "Trigger.h" +#include "Playerbots.h" +#include "PlayerbotAI.h" +#include +#include class PlayerbotAI; -DEFLECT_TRIGGER(FireWardTrigger, "fire ward"); -DEFLECT_TRIGGER(FrostWardTrigger, "frost ward"); +// Buff and Out of Combat Triggers class ArcaneIntellectOnPartyTrigger : public BuffOnPartyTrigger { @@ -37,30 +41,53 @@ public: bool IsActive() override; }; -class LivingBombTrigger : public DebuffTrigger +class NoFocusMagicTrigger : public Trigger { public: - LivingBombTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "living bomb", 1, true) {} + NoFocusMagicTrigger(PlayerbotAI* botAI) : Trigger(botAI, "no focus magic") {} + bool IsActive() override; }; -class FireballTrigger : public DebuffTrigger +class IceBarrierTrigger : public BuffTrigger { public: - FireballTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "fireball", 1, true) {} + IceBarrierTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "ice barrier") {} }; -class PyroblastTrigger : public DebuffTrigger +class NoManaGemTrigger : public Trigger { public: - PyroblastTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "pyroblast", 1, true) {} + NoManaGemTrigger(PlayerbotAI* botAI) : Trigger(botAI, "no mana gem") {} + + bool IsActive() override; }; +class FireWardTrigger : public DeflectSpellTrigger +{ +public: + FireWardTrigger(PlayerbotAI* botAI) : DeflectSpellTrigger(botAI, "fire ward") {} +}; + +class FrostWardTrigger : public DeflectSpellTrigger +{ +public: + FrostWardTrigger(PlayerbotAI* botAI) : DeflectSpellTrigger(botAI, "frost ward") {} +}; + +// Proc and Boost Triggers + class HotStreakTrigger : public HasAuraTrigger { public: HotStreakTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "hot streak") {} }; +class FirestarterTrigger : public HasAuraTrigger +{ +public: + FirestarterTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "firestarter") {} +}; + class MissileBarrageTrigger : public HasAuraTrigger { public: @@ -73,55 +100,71 @@ public: ArcaneBlastTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "arcane blast") {} }; -class FingersOfFrostSingleTrigger : public HasAuraStackTrigger +class ArcaneBlastStackTrigger : public HasAuraStackTrigger { public: - FingersOfFrostSingleTrigger(PlayerbotAI* ai) : HasAuraStackTrigger(ai, "fingers of frost", 1, 1) {} + ArcaneBlastStackTrigger(PlayerbotAI* botAI) : HasAuraStackTrigger(botAI, "arcane blast", 4, 1) {} +}; + +class ArcaneBlast4StacksAndMissileBarrageTrigger : public TwoTriggers +{ +public: + ArcaneBlast4StacksAndMissileBarrageTrigger(PlayerbotAI* ai) + : TwoTriggers(ai, "arcane blast stack", "missile barrage") + { + } +}; + +class CombustionTrigger : public BuffTrigger +{ +public: + CombustionTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "combustion") {} +}; + +class IcyVeinsCooldownTrigger : public SpellCooldownTrigger +{ +public: + IcyVeinsCooldownTrigger(PlayerbotAI* botAI) : SpellCooldownTrigger(botAI, "icy veins") {} +}; + +class DeepFreezeCooldownTrigger : public SpellCooldownTrigger +{ +public: + DeepFreezeCooldownTrigger(PlayerbotAI* botAI) : SpellCooldownTrigger(botAI, "deep freeze") {} + bool IsActive() override; }; -class FingersOfFrostDoubleTrigger : public HasAuraStackTrigger +class ColdSnapTrigger : public TwoTriggers { public: - FingersOfFrostDoubleTrigger(PlayerbotAI* ai) : HasAuraStackTrigger(ai, "fingers of frost", 2, 1) {} - // bool IsActive() override; + ColdSnapTrigger(PlayerbotAI* ai) : TwoTriggers(ai, "icy veins on cd", "deep freeze on cd") {} }; -class BrainFreezeTrigger : public HasAuraTrigger +class MirrorImageTrigger : public BuffTrigger { public: - BrainFreezeTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "fireball!") {} + MirrorImageTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "mirror image") {} }; -class CounterspellInterruptSpellTrigger : public InterruptSpellTrigger +class IcyVeinsTrigger : public BuffTrigger { public: - CounterspellInterruptSpellTrigger(PlayerbotAI* botAI) : InterruptSpellTrigger(botAI, "counterspell") {} + IcyVeinsTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "icy veins") {} }; -class CombustionTrigger : public BoostTrigger +class ArcanePowerTrigger : public BuffTrigger { public: - CombustionTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "combustion") {} + ArcanePowerTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "arcane power") {} +}; +class PresenceOfMindTrigger : public BuffTrigger +{ +public: + PresenceOfMindTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "presence of mind") {} }; -class IcyVeinsTrigger : public BoostTrigger -{ -public: - IcyVeinsTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "icy veins") {} -}; - -class ColdSnapTrigger : public BoostTrigger -{ -public: - ColdSnapTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "cold snap") {} -}; - -class IceBarrierTrigger : public BuffTrigger -{ -public: - IceBarrierTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "ice barrier") {} -}; +// CC, Interrupt, and Dispel Triggers class PolymorphTrigger : public HasCcTargetTrigger { @@ -155,29 +198,63 @@ public: CounterspellEnemyHealerTrigger(PlayerbotAI* botAI) : InterruptEnemyHealerTrigger(botAI, "counterspell") {} }; -class ArcanePowerTrigger : public BuffTrigger +class CounterspellInterruptSpellTrigger : public InterruptSpellTrigger { public: - ArcanePowerTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "arcane power") {} + CounterspellInterruptSpellTrigger(PlayerbotAI* botAI) : InterruptSpellTrigger(botAI, "counterspell") {} }; -class PresenceOfMindTrigger : public BuffTrigger +// Damage and Debuff Triggers + +class LivingBombTrigger : public DebuffTrigger { public: - PresenceOfMindTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "presence of mind") {} + LivingBombTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "living bomb", 1, true) {} + bool IsActive() override { return BuffTrigger::IsActive(); } }; -class ArcaneBlastStackTrigger : public HasAuraStackTrigger +class LivingBombOnAttackersTrigger : public DebuffOnAttackerTrigger { public: - ArcaneBlastStackTrigger(PlayerbotAI* botAI) : HasAuraStackTrigger(botAI, "arcane blast", 3, 1) {} + LivingBombOnAttackersTrigger(PlayerbotAI* ai) : DebuffOnAttackerTrigger(ai, "living bomb", true) {} + bool IsActive() override { return BuffTrigger::IsActive(); } +}; + +class FireballTrigger : public DebuffTrigger +{ +public: + FireballTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "fireball", 1, true) {} +}; + +class ImprovedScorchTrigger : public DebuffTrigger +{ +public: + ImprovedScorchTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "improved scorch", 1, true, 0.5f) {} bool IsActive() override; }; -class MirrorImageTrigger : public BoostTrigger +class PyroblastTrigger : public DebuffTrigger { public: - MirrorImageTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "mirror image") {} + PyroblastTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "pyroblast", 1, true) {} +}; + +class FrostfireBoltTrigger : public DebuffTrigger +{ +public: + FrostfireBoltTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "frostfire bolt", 1, true) {} +}; + +class FingersOfFrostTrigger : public HasAuraTrigger +{ +public: + FingersOfFrostTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "fingers of frost") {} +}; + +class BrainFreezeTrigger : public HasAuraTrigger +{ +public: + BrainFreezeTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "fireball!") {} }; class FrostNovaOnTargetTrigger : public DebuffTrigger @@ -194,17 +271,74 @@ public: bool IsActive() override; }; -class NoFocusMagicTrigger : public Trigger +class FlamestrikeNearbyTrigger : public Trigger { public: - NoFocusMagicTrigger(PlayerbotAI* botAI) : Trigger(botAI, "no focus magic") {} + FlamestrikeNearbyTrigger(PlayerbotAI* botAI, float radius = 30.0f) + : Trigger(botAI, "flamestrike nearby"), radius(radius) + { + } bool IsActive() override; + +protected: + float radius; + static const std::set FLAMESTRIKE_SPELL_IDS; }; -class FrostfireBoltTrigger : public DebuffTrigger +class FlamestrikeBlizzardTrigger : public TwoTriggers { public: - FrostfireBoltTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "frostfire bolt", 1, true) {} + FlamestrikeBlizzardTrigger(PlayerbotAI* ai) : TwoTriggers(ai, "flamestrike nearby", "medium aoe") {} +}; + +class BlizzardChannelCheckTrigger : public Trigger +{ +public: + BlizzardChannelCheckTrigger(PlayerbotAI* botAI, uint32 minEnemies = 2) + : Trigger(botAI, "blizzard channel check"), minEnemies(minEnemies) {} + + bool IsActive() override; + +protected: + uint32 minEnemies; + static const std::set BLIZZARD_SPELL_IDS; +}; + +class BlastWaveOffCdTrigger : public SpellNoCooldownTrigger +{ +public: + BlastWaveOffCdTrigger(PlayerbotAI* botAI) : SpellNoCooldownTrigger(botAI, "blast wave") {} +}; + +class BlastWaveOffCdTriggerAndMediumAoeTrigger : public TwoTriggers +{ +public: + BlastWaveOffCdTriggerAndMediumAoeTrigger(PlayerbotAI* ai) : TwoTriggers(ai, "blast wave off cd", "medium aoe") {} +}; + +class NoFirestarterStrategyTrigger : public Trigger +{ +public: + NoFirestarterStrategyTrigger(PlayerbotAI* botAI) : Trigger(botAI, "no firestarter strategy") {} + + bool IsActive() override + { + return !botAI->HasStrategy("firestarter", BOT_STATE_COMBAT); + } +}; + +class EnemyIsCloseAndNoFirestarterStrategyTrigger : public TwoTriggers +{ +public: + EnemyIsCloseAndNoFirestarterStrategyTrigger(PlayerbotAI* botAI) + : TwoTriggers(botAI, "enemy is close", "no firestarter strategy") {} +}; + +class EnemyTooCloseForSpellAndNoFirestarterStrategyTrigger : public TwoTriggers +{ +public: + EnemyTooCloseForSpellAndNoFirestarterStrategyTrigger(PlayerbotAI* botAI) + : TwoTriggers(botAI, "enemy too close for spell", "no firestarter strategy") {} }; #endif From a632fa21947d31a6ae8006651a3df18e54aa1279 Mon Sep 17 00:00:00 2001 From: ThePenguinMan96 Date: Sat, 26 Jul 2025 13:06:41 -0700 Subject: [PATCH 2/5] Migrate important cooldowns over to BoostStrategy This commit focuses on the changes requested - Migrate important cooldowns over to BoostStrategy. Just tested it, and it is working fine - The player has more control over when they can boost. Also, added Mirror Image to the Boost Strategy, after other cooldowns are used - this way the mirror images get the buffs. --- src/strategy/mage/ArcaneMageStrategy.cpp | 4 --- src/strategy/mage/FireMageStrategy.cpp | 3 --- src/strategy/mage/FrostFireMageStrategy.cpp | 4 --- src/strategy/mage/FrostMageStrategy.cpp | 4 --- src/strategy/mage/GenericMageStrategy.cpp | 29 +++++++++++++++++++++ src/strategy/mage/MageTriggers.h | 20 +++++++------- 6 files changed, 39 insertions(+), 25 deletions(-) diff --git a/src/strategy/mage/ArcaneMageStrategy.cpp b/src/strategy/mage/ArcaneMageStrategy.cpp index 2b2fb7e8..30c17d54 100644 --- a/src/strategy/mage/ArcaneMageStrategy.cpp +++ b/src/strategy/mage/ArcaneMageStrategy.cpp @@ -53,10 +53,6 @@ void ArcaneMageStrategy::InitTriggers(std::vector& triggers) { GenericMageStrategy::InitTriggers(triggers); - // Cooldown Triggers - triggers.push_back(new TriggerNode("arcane power", NextAction::array(0, new NextAction("arcane power", 29.0f), nullptr))); - triggers.push_back(new TriggerNode("icy veins", NextAction::array(0, new NextAction("icy veins", 28.5f), nullptr))); - // Proc Trigger triggers.push_back(new TriggerNode("arcane blast 4 stacks and missile barrage", NextAction::array(0, new NextAction("arcane missiles", 15.0f), nullptr))); } diff --git a/src/strategy/mage/FireMageStrategy.cpp b/src/strategy/mage/FireMageStrategy.cpp index 0501d4a0..f87fe882 100644 --- a/src/strategy/mage/FireMageStrategy.cpp +++ b/src/strategy/mage/FireMageStrategy.cpp @@ -52,9 +52,6 @@ void FireMageStrategy::InitTriggers(std::vector& triggers) { GenericMageStrategy::InitTriggers(triggers); - // Cooldown Trigger - triggers.push_back(new TriggerNode("combustion", NextAction::array(0, new NextAction("combustion", 18.0f), nullptr))); - // Debuff Triggers triggers.push_back(new TriggerNode("improved scorch", NextAction::array(0, new NextAction("scorch", 19.0f), nullptr))); triggers.push_back(new TriggerNode("living bomb", NextAction::array(0, new NextAction("living bomb", 18.5f), nullptr))); diff --git a/src/strategy/mage/FrostFireMageStrategy.cpp b/src/strategy/mage/FrostFireMageStrategy.cpp index 106427c8..4af7a869 100644 --- a/src/strategy/mage/FrostFireMageStrategy.cpp +++ b/src/strategy/mage/FrostFireMageStrategy.cpp @@ -50,10 +50,6 @@ void FrostFireMageStrategy::InitTriggers(std::vector& triggers) { GenericMageStrategy::InitTriggers(triggers); - // Cooldown Triggers - triggers.push_back(new TriggerNode("combustion", NextAction::array(0, new NextAction("combustion", 18.0f), nullptr))); - triggers.push_back(new TriggerNode("icy veins", NextAction::array(0, new NextAction("icy veins", 17.5f), nullptr))); - // Debuff Triggers triggers.push_back(new TriggerNode("improved scorch", NextAction::array(0, new NextAction("scorch", 19.0f), nullptr))); triggers.push_back(new TriggerNode("living bomb", NextAction::array(0, new NextAction("living bomb", 18.5f), nullptr))); diff --git a/src/strategy/mage/FrostMageStrategy.cpp b/src/strategy/mage/FrostMageStrategy.cpp index eef36c92..37a4551b 100644 --- a/src/strategy/mage/FrostMageStrategy.cpp +++ b/src/strategy/mage/FrostMageStrategy.cpp @@ -59,10 +59,6 @@ void FrostMageStrategy::InitTriggers(std::vector& triggers) { GenericMageStrategy::InitTriggers(triggers); - // Cooldown triggers - triggers.push_back(new TriggerNode("cold snap", NextAction::array(0, new NextAction("cold snap", 28.0f), nullptr))); - triggers.push_back(new TriggerNode("icy veins", NextAction::array(0, new NextAction("icy veins", 27.5f), nullptr))); - // Pet/Defensive triggers triggers.push_back(new TriggerNode("no pet", NextAction::array(0, new NextAction("summon water elemental", 30.0f), nullptr))); triggers.push_back(new TriggerNode("has pet", NextAction::array(0, new NextAction("toggle pet spell", 29.5f), nullptr))); diff --git a/src/strategy/mage/GenericMageStrategy.cpp b/src/strategy/mage/GenericMageStrategy.cpp index 53494898..921f09ef 100644 --- a/src/strategy/mage/GenericMageStrategy.cpp +++ b/src/strategy/mage/GenericMageStrategy.cpp @@ -203,6 +203,35 @@ void MageCureStrategy::InitTriggers(std::vector& triggers) void MageBoostStrategy::InitTriggers(std::vector& triggers) { + Player* bot = botAI->GetBot(); + int tab = AiFactory::GetPlayerSpecTab(bot); + + if (tab == 0) // Arcane + { + triggers.push_back(new TriggerNode("arcane power", NextAction::array(0, new NextAction("arcane power", 29.0f), nullptr))); + triggers.push_back(new TriggerNode("icy veins", NextAction::array(0, new NextAction("icy veins", 28.5f), nullptr))); + triggers.push_back(new TriggerNode("mirror image", NextAction::array(0, new NextAction("mirror image", 28.0f), nullptr))); + } + else if (tab == 1) + { + if (bot->HasSpell(44614) /*Frostfire Bolt*/ && bot->HasAura(15047) /*Ice Shards*/) + { // Frostfire + triggers.push_back(new TriggerNode("combustion", NextAction::array(0, new NextAction("combustion", 18.0f), nullptr))); + triggers.push_back(new TriggerNode("icy veins", NextAction::array(0, new NextAction("icy veins", 17.5f), nullptr))); + triggers.push_back(new TriggerNode("mirror image", NextAction::array(0, new NextAction("mirror image", 17.0f), nullptr))); + } + else + { // Fire + triggers.push_back(new TriggerNode("combustion", NextAction::array(0, new NextAction("combustion", 18.0f), nullptr))); + triggers.push_back(new TriggerNode("mirror image", NextAction::array(0, new NextAction("mirror image", 17.5f), nullptr))); + } + } + else if (tab == 2) // Frost + { + triggers.push_back(new TriggerNode("cold snap", NextAction::array(0, new NextAction("cold snap", 28.0f), nullptr))); + triggers.push_back(new TriggerNode("icy veins", NextAction::array(0, new NextAction("icy veins", 27.5f), nullptr))); + triggers.push_back(new TriggerNode("mirror image", NextAction::array(0, new NextAction("mirror image", 26.0f), nullptr))); + } } void MageCcStrategy::InitTriggers(std::vector& triggers) diff --git a/src/strategy/mage/MageTriggers.h b/src/strategy/mage/MageTriggers.h index 772cb926..a8e06317 100644 --- a/src/strategy/mage/MageTriggers.h +++ b/src/strategy/mage/MageTriggers.h @@ -115,10 +115,10 @@ public: } }; -class CombustionTrigger : public BuffTrigger +class CombustionTrigger : public BoostTrigger { public: - CombustionTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "combustion") {} + CombustionTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "combustion") {} }; class IcyVeinsCooldownTrigger : public SpellCooldownTrigger @@ -141,27 +141,27 @@ public: ColdSnapTrigger(PlayerbotAI* ai) : TwoTriggers(ai, "icy veins on cd", "deep freeze on cd") {} }; -class MirrorImageTrigger : public BuffTrigger +class MirrorImageTrigger : public BoostTrigger { public: - MirrorImageTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "mirror image") {} + MirrorImageTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "mirror image") {} }; -class IcyVeinsTrigger : public BuffTrigger +class IcyVeinsTrigger : public BoostTrigger { public: - IcyVeinsTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "icy veins") {} + IcyVeinsTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "icy veins") {} }; -class ArcanePowerTrigger : public BuffTrigger +class ArcanePowerTrigger : public BoostTrigger { public: - ArcanePowerTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "arcane power") {} + ArcanePowerTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "arcane power") {} }; -class PresenceOfMindTrigger : public BuffTrigger +class PresenceOfMindTrigger : public BoostTrigger { public: - PresenceOfMindTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "presence of mind") {} + PresenceOfMindTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "presence of mind") {} }; // CC, Interrupt, and Dispel Triggers From e59bad26c4ef7b2d46293c359ce6fbf5d813c3fb Mon Sep 17 00:00:00 2001 From: Yunfan Li Date: Sun, 27 Jul 2025 11:30:23 +0800 Subject: [PATCH 3/5] Change drink aura for free food --- src/strategy/actions/NonCombatActions.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/strategy/actions/NonCombatActions.cpp b/src/strategy/actions/NonCombatActions.cpp index 6ee977a2..128e44ab 100644 --- a/src/strategy/actions/NonCombatActions.cpp +++ b/src/strategy/actions/NonCombatActions.cpp @@ -40,13 +40,13 @@ bool DrinkAction::Execute(Event event) float delay; if (!bot->InBattleground()) - delay = 27000.0f * (100 - p) / 100.0f; + delay = 18000.0f * (100 - p) / 100.0f; else - delay = 20000.0f * (100 - p) / 100.0f; + delay = 12000.0f * (100 - p) / 100.0f; botAI->SetNextCheckDelay(delay); - bot->AddAura(24707, bot); + bot->AddAura(25990, bot); return true; // return botAI->CastSpell(24707, bot); } @@ -90,13 +90,13 @@ bool EatAction::Execute(Event event) float delay; if (!bot->InBattleground()) - delay = 27000.0f * (100 - p) / 100.0f; + delay = 18000.0f * (100 - p) / 100.0f; else - delay = 20000.0f * (100 - p) / 100.0f; + delay = 12000.0f * (100 - p) / 100.0f; botAI->SetNextCheckDelay(delay); - bot->AddAura(24707, bot); + bot->AddAura(25990, bot); return true; } From 1e33b28abecbc6ac24d8fc54749434218f3a0cdd Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sun, 27 Jul 2025 07:51:45 +0200 Subject: [PATCH 4/5] - Added raid cheat to configuration to add posibility to turn off (#1465) - Added General Vezax strategy --- conf/playerbots.conf.dist | 3 +- src/PlayerbotAIConfig.cpp | 4 +- src/PlayerbotAIConfig.h | 3 +- src/strategy/actions/CheatAction.cpp | 5 + .../raids/ulduar/RaidUlduarActionContext.h | 6 + .../raids/ulduar/RaidUlduarActions.cpp | 228 +++++++++++------- src/strategy/raids/ulduar/RaidUlduarActions.h | 24 ++ .../raids/ulduar/RaidUlduarStrategy.cpp | 15 ++ .../raids/ulduar/RaidUlduarTriggerContext.h | 6 + .../raids/ulduar/RaidUlduarTriggers.cpp | 62 +++++ .../raids/ulduar/RaidUlduarTriggers.h | 30 +++ 11 files changed, 299 insertions(+), 87 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index b64454ce..b8263ee1 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -501,9 +501,10 @@ AiPlayerbot.AutoGearScoreLimit = 0 # "mana" (bots have infinite mana) # "power" (bots have infinite energy, rage, and runic power) # "taxi" (bots may use all flight paths, though they will not actually learn them) +# "raid" (bots use cheats implemented into raid strategies) # To use multiple cheats, separate them by commas below (e.g., to enable all, use "gold,health,mana,power,taxi") # Default: taxi is enabled -AiPlayerbot.BotCheats = "taxi" +AiPlayerbot.BotCheats = "taxi,raid" # # diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index db1419a4..7899de84 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -448,7 +448,7 @@ bool PlayerbotAIConfig::Initialize() } botCheats.clear(); - LoadListString>(sConfigMgr->GetOption("AiPlayerbot.BotCheats", "taxi"), + LoadListString>(sConfigMgr->GetOption("AiPlayerbot.BotCheats", "taxi,raid"), botCheats); botCheatMask = 0; @@ -463,6 +463,8 @@ bool PlayerbotAIConfig::Initialize() botCheatMask |= (uint32)BotCheatMask::mana; if (std::find(botCheats.begin(), botCheats.end(), "power") != botCheats.end()) botCheatMask |= (uint32)BotCheatMask::power; + if (std::find(botCheats.begin(), botCheats.end(), "raid") != botCheats.end()) + botCheatMask |= (uint32)BotCheatMask::raid; LoadListString>(sConfigMgr->GetOption("AiPlayerbot.AllowedLogFiles", ""), allowedLogFiles); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 65b99c5d..cfdf70ea 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -22,7 +22,8 @@ enum class BotCheatMask : uint32 health = 4, mana = 8, power = 16, - maxMask = 32 + raid = 32, + maxMask = 64 }; enum class HealingManaEfficiency : uint8 diff --git a/src/strategy/actions/CheatAction.cpp b/src/strategy/actions/CheatAction.cpp index 1092ca34..f627105f 100644 --- a/src/strategy/actions/CheatAction.cpp +++ b/src/strategy/actions/CheatAction.cpp @@ -53,6 +53,9 @@ BotCheatMask CheatAction::GetCheatMask(std::string const cheat) if (cheat == "power") return BotCheatMask::power; + if (cheat == "raid") + return BotCheatMask::raid; + return BotCheatMask::none; } @@ -70,6 +73,8 @@ std::string const CheatAction::GetCheatName(BotCheatMask cheatMask) return "mana"; case BotCheatMask::power: return "power"; + case BotCheatMask::raid: + return "raid"; default: return "none"; } diff --git a/src/strategy/raids/ulduar/RaidUlduarActionContext.h b/src/strategy/raids/ulduar/RaidUlduarActionContext.h index da16537a..c27a2580 100644 --- a/src/strategy/raids/ulduar/RaidUlduarActionContext.h +++ b/src/strategy/raids/ulduar/RaidUlduarActionContext.h @@ -62,6 +62,9 @@ public: creators["mimiron rocket strike action"] = &RaidUlduarActionContext::mimiron_rocket_strike_action; creators["mimiron phase 4 mark dps action"] = &RaidUlduarActionContext::mimiron_phase_4_mark_dps_action; creators["mimiron cheat action"] = &RaidUlduarActionContext::mimiron_cheat_action; + creators["vezax cheat action"] = &RaidUlduarActionContext::vezax_cheat_action; + creators["vezax shadow crash action"] = &RaidUlduarActionContext::vezax_shadow_crash_action; + creators["vezax mark of the faceless action"] = &RaidUlduarActionContext::vezax_mark_of_the_faceless_action; } private: @@ -111,6 +114,9 @@ private: static Action* mimiron_rocket_strike_action(PlayerbotAI* ai) { return new MimironRocketStrikeAction(ai); } static Action* mimiron_phase_4_mark_dps_action(PlayerbotAI* ai) { return new MimironPhase4MarkDpsAction(ai); } static Action* mimiron_cheat_action(PlayerbotAI* ai) { return new MimironCheatAction(ai); } + static Action* vezax_cheat_action(PlayerbotAI* ai) { return new VezaxCheatAction(ai); } + static Action* vezax_shadow_crash_action(PlayerbotAI* ai) { return new VezaxShadowCrashAction(ai); } + static Action* vezax_mark_of_the_faceless_action(PlayerbotAI* ai) { return new VezaxMarkOfTheFacelessAction(ai); } }; #endif diff --git a/src/strategy/raids/ulduar/RaidUlduarActions.cpp b/src/strategy/raids/ulduar/RaidUlduarActions.cpp index 901713be..3d180820 100644 --- a/src/strategy/raids/ulduar/RaidUlduarActions.cpp +++ b/src/strategy/raids/ulduar/RaidUlduarActions.cpp @@ -42,7 +42,6 @@ const Position ULDUAR_KOLOGARN_RESTORE_POSITION = Position(1764.3749f, -24.02903 const Position ULDUAR_KOLOGARN_EYEBEAM_LEFT_POSITION = Position(1781.2051f, 9.34402f, 449.0f, 0.00087690353f); const Position ULDUAR_KOLOGARN_EYEBEAM_RIGHT_POSITION = Position(1763.2561f, -24.44305f, 449.0f, 0.00087690353f); const Position ULDUAR_THORIM_JUMP_START_POINT = Position(2137.137f, -291.19025f, 438.24753f, 1.7059844f); -const Position ULDUAR_THORIM_JUMP_END_POINT = Position(2137.8818f, -278.18942f, 419.66653f); bool FlameLeviathanVehicleAction::Execute(Event event) { @@ -484,12 +483,12 @@ bool RazorscaleAvoidDevouringFlameAction::isUseful() float distance = bot->GetDistance2d(unit); if (distance < safeDistance) { - return true; // Bot is within the danger distance + return true; // Bot is within the danger distance } } } - return false; // No nearby flames or bot is at a safe distance + return false; // No nearby flames or bot is at a safe distance } bool RazorscaleAvoidSentinelAction::Execute(Event event) @@ -529,17 +528,17 @@ bool RazorscaleAvoidSentinelAction::Execute(Event event) Unit* mainTankUnit = AI_VALUE(Unit*, "main tank"); Player* mainTank = mainTankUnit ? mainTankUnit->ToPlayer() : nullptr; - if (mainTank && !GET_PLAYERBOT_AI(mainTank)) // Main tank is a real player + if (mainTank && !GET_PLAYERBOT_AI(mainTank)) // Main tank is a real player { // Iterate through the first 3 bot tanks to assign the Skull marker for (int i = 0; i < 3; ++i) { - if (botAI->IsAssistTankOfIndex(bot, i) && GET_PLAYERBOT_AI(bot)) // Bot is a valid tank + if (botAI->IsAssistTankOfIndex(bot, i) && GET_PLAYERBOT_AI(bot)) // Bot is a valid tank { Group* group = bot->GetGroup(); if (group && lowestHealthSentinel) { - int8 skullIndex = 7; // Skull + int8 skullIndex = 7; // Skull ObjectGuid currentSkullTarget = group->GetTargetIcon(skullIndex); // If there's no skull set yet, or the skull is on a different target, set the sentinel @@ -548,16 +547,16 @@ bool RazorscaleAvoidSentinelAction::Execute(Event event) group->SetTargetIcon(skullIndex, bot->GetGUID(), lowestHealthSentinel->GetGUID()); } } - break; // Stop after finding the first valid bot tank + break; // Stop after finding the first valid bot tank } } } - else if (isMainTank && lowestHealthSentinel) // Bot is the main tank + else if (isMainTank && lowestHealthSentinel) // Bot is the main tank { Group* group = bot->GetGroup(); if (group) { - int8 skullIndex = 7; // Skull + int8 skullIndex = 7; // Skull ObjectGuid currentSkullTarget = group->GetTargetIcon(skullIndex); // If there's no skull set yet, or the skull is on a different target, set the sentinel @@ -568,8 +567,7 @@ bool RazorscaleAvoidSentinelAction::Execute(Event event) } } - - return movedAway; // Return true if moved + return movedAway; // Return true if moved } bool RazorscaleAvoidSentinelAction::isUseful() @@ -585,13 +583,13 @@ bool RazorscaleAvoidSentinelAction::isUseful() } // If the main tank is a human, check if this bot is one of the first three valid bot tanks - if (mainTank && !GET_PLAYERBOT_AI(mainTank)) // Main tank is a human player + if (mainTank && !GET_PLAYERBOT_AI(mainTank)) // Main tank is a human player { for (int i = 0; i < 3; ++i) { - if (botAI->IsAssistTankOfIndex(bot, i) && GET_PLAYERBOT_AI(bot)) // Bot is a valid tank + if (botAI->IsAssistTankOfIndex(bot, i) && GET_PLAYERBOT_AI(bot)) // Bot is a valid tank { - return true; // This bot should assist with marking + return true; // This bot should assist with marking } } } @@ -654,7 +652,8 @@ bool RazorscaleAvoidWhirlwindAction::isUseful() Unit* unit = botAI->GetUnit(npc); if (unit && unit->GetEntry() == RazorscaleBossHelper::UNIT_DARK_RUNE_SENTINEL) { - if (unit->HasAura(RazorscaleBossHelper::SPELL_SENTINEL_WHIRLWIND) || unit->GetCurrentSpell(CURRENT_CHANNELED_SPELL)) + if (unit->HasAura(RazorscaleBossHelper::SPELL_SENTINEL_WHIRLWIND) || + unit->GetCurrentSpell(CURRENT_CHANNELED_SPELL)) { if (bot->GetDistance2d(unit) < radius) { @@ -679,11 +678,11 @@ bool RazorscaleIgnoreBossAction::isUseful() if (boss->GetPositionZ() >= RazorscaleBossHelper::RAZORSCALE_FLYING_Z_THRESHOLD) { // Check if the bot is outside the designated area - if (bot->GetDistance2d( - RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, - RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y) > RazorscaleBossHelper::RAZORSCALE_ARENA_RADIUS + 25.0f) + if (bot->GetDistance2d(RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, + RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y) > + RazorscaleBossHelper::RAZORSCALE_ARENA_RADIUS + 25.0f) { - return true; // Movement to the center is the top priority for all bots + return true; // Movement to the center is the top priority for all bots } if (!botAI->IsTank(bot)) @@ -698,11 +697,11 @@ bool RazorscaleIgnoreBossAction::isUseful() } // Check if the boss is already set as the moon marker - int8 moonIndex = 4; // Moon marker index + int8 moonIndex = 4; // Moon marker index ObjectGuid currentMoonTarget = group->GetTargetIcon(moonIndex); if (currentMoonTarget == boss->GetGUID()) { - return false; // Moon marker is already correctly set, no further action needed + return false; // Moon marker is already correctly set, no further action needed } // Proceed to tank-specific logic @@ -716,13 +715,13 @@ bool RazorscaleIgnoreBossAction::isUseful() } // If the main tank is a human, check if this bot is the lowest-indexed bot tank - if (mainTank && !GET_PLAYERBOT_AI(mainTank)) // Main tank is a human player + if (mainTank && !GET_PLAYERBOT_AI(mainTank)) // Main tank is a human player { - for (int i = 0; i < 3; ++i) // Only iterate through the first 3 indexes + for (int i = 0; i < 3; ++i) // Only iterate through the first 3 indexes { - if (botAI->IsAssistTankOfIndex(bot, i) && GET_PLAYERBOT_AI(bot)) // Valid bot tank + if (botAI->IsAssistTankOfIndex(bot, i) && GET_PLAYERBOT_AI(bot)) // Valid bot tank { - return true; // This bot should assign the marker + return true; // This bot should assign the marker } } } @@ -751,18 +750,13 @@ bool RazorscaleIgnoreBossAction::Execute(Event event) } // Check if the bot is outside the designated area and move inside first - if (bot->GetDistance2d( - RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, - RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y) > RazorscaleBossHelper::RAZORSCALE_ARENA_RADIUS + 25.0f) + if (bot->GetDistance2d(RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, + RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y) > + RazorscaleBossHelper::RAZORSCALE_ARENA_RADIUS + 25.0f) { - return MoveInside( - ULDUAR_MAP_ID, - RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, - RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y, - bot->GetPositionZ(), - RazorscaleBossHelper::RAZORSCALE_ARENA_RADIUS - 10.0f, - MovementPriority::MOVEMENT_NORMAL - ); + return MoveInside(ULDUAR_MAP_ID, RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, + RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y, bot->GetPositionZ(), + RazorscaleBossHelper::RAZORSCALE_ARENA_RADIUS - 10.0f, MovementPriority::MOVEMENT_NORMAL); } if (!botAI->IsTank(bot)) @@ -775,7 +769,7 @@ bool RazorscaleIgnoreBossAction::Execute(Event event) ObjectGuid currentMoonTarget = group->GetTargetIcon(moonIndex); if (currentMoonTarget == boss->GetGUID()) { - return false; // Moon marker is already correctly set + return false; // Moon marker is already correctly set } // Get the main tank and determine role @@ -783,33 +777,28 @@ bool RazorscaleIgnoreBossAction::Execute(Event event) Player* mainTank = mainTankUnit ? mainTankUnit->ToPlayer() : nullptr; // If the main tank is a human, assign the moon marker using the lowest-indexed bot tank - if (mainTank && !GET_PLAYERBOT_AI(mainTank)) // Main tank is a real player + if (mainTank && !GET_PLAYERBOT_AI(mainTank)) // Main tank is a real player { - for (int i = 0; i < 3; ++i) // Only iterate through the first 3 indexes + for (int i = 0; i < 3; ++i) // Only iterate through the first 3 indexes { - if (botAI->IsAssistTankOfIndex(bot, i) && GET_PLAYERBOT_AI(bot)) // Bot is a valid tank + if (botAI->IsAssistTankOfIndex(bot, i) && GET_PLAYERBOT_AI(bot)) // Bot is a valid tank { group->SetTargetIcon(moonIndex, bot->GetGUID(), boss->GetGUID()); SetNextMovementDelay(1000); - break; // Assign the moon marker and stop + break; // Assign the moon marker and stop } } } - else if (mainTankUnit == bot) // If this bot is the main tank + else if (mainTankUnit == bot) // If this bot is the main tank { group->SetTargetIcon(moonIndex, bot->GetGUID(), boss->GetGUID()); SetNextMovementDelay(1000); } // Tanks move inside the arena - return MoveInside( - ULDUAR_MAP_ID, - RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, - RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y, - bot->GetPositionZ(), - RazorscaleBossHelper::RAZORSCALE_ARENA_RADIUS - 10.0f, - MovementPriority::MOVEMENT_NORMAL - ); + return MoveInside(ULDUAR_MAP_ID, RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, + RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y, bot->GetPositionZ(), + RazorscaleBossHelper::RAZORSCALE_ARENA_RADIUS - 10.0f, MovementPriority::MOVEMENT_NORMAL); } bool RazorscaleGroundedAction::isUseful() @@ -875,9 +864,8 @@ bool RazorscaleGroundedAction::isUseful() float bossY = boss->GetPositionY(); float bossZ = boss->GetPositionZ(); - bool atInitialLandingPosition = (fabs(bossX - landingX) < 2.0f) && - (fabs(bossY - landingY) < 2.0f) && - (fabs(bossZ - landingZ) < 1.0f); + bool atInitialLandingPosition = + (fabs(bossX - landingX) < 2.0f) && (fabs(bossY - landingY) < 2.0f) && (fabs(bossZ - landingZ) < 1.0f); constexpr float initialLandingRadius = 14.0f; constexpr float normalRadius = 12.0f; @@ -891,7 +879,8 @@ bool RazorscaleGroundedAction::isUseful() return distanceToAdjustedCenter > initialLandingRadius; } - float distanceToCenter = bot->GetDistance2d(RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y); + float distanceToCenter = bot->GetDistance2d(RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, + RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y); return distanceToCenter > normalRadius; } @@ -911,12 +900,12 @@ bool RazorscaleGroundedAction::Execute(Event event) Unit* mainTankUnit = AI_VALUE(Unit*, "main tank"); Player* mainTank = mainTankUnit ? mainTankUnit->ToPlayer() : nullptr; - if (mainTank && !GET_PLAYERBOT_AI(mainTank)) // Main tank is a human player + if (mainTank && !GET_PLAYERBOT_AI(mainTank)) // Main tank is a human player { // Iterate through the first 3 bot tanks to handle the moon marker for (int i = 0; i < 3; ++i) { - if (botAI->IsAssistTankOfIndex(bot, i) && GET_PLAYERBOT_AI(bot)) // Bot is a valid tank + if (botAI->IsAssistTankOfIndex(bot, i) && GET_PLAYERBOT_AI(bot)) // Bot is a valid tank { int8 moonIndex = 4; ObjectGuid currentMoonTarget = group->GetTargetIcon(moonIndex); @@ -931,7 +920,7 @@ bool RazorscaleGroundedAction::Execute(Event event) } } } - else if (botAI->IsMainTank(bot)) // Bot is the main tank + else if (botAI->IsMainTank(bot)) // Bot is the main tank { int8 moonIndex = 4; ObjectGuid currentMoonTarget = group->GetTargetIcon(moonIndex); @@ -961,33 +950,22 @@ bool RazorscaleGroundedAction::Execute(Event event) float bossY = boss->GetPositionY(); float bossZ = boss->GetPositionZ(); - bool atInitialLandingPosition = (fabs(bossX - landingX) < 2.0f) && - (fabs(bossY - landingY) < 2.0f) && - (fabs(bossZ - landingZ) < 1.0f); + bool atInitialLandingPosition = + (fabs(bossX - landingX) < 2.0f) && (fabs(bossY - landingY) < 2.0f) && (fabs(bossZ - landingZ) < 1.0f); if (atInitialLandingPosition) { // If at the initial landing position, use 12-yard radius with a // 20 yard offset on the Y axis so everyone is behind the boss - return MoveInside( - ULDUAR_MAP_ID, - RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, - RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y - 20.0f, - bot->GetPositionZ(), - RazorscaleBossHelper::RAZORSCALE_ARENA_RADIUS - 12.0f, - MovementPriority::MOVEMENT_COMBAT - ); + return MoveInside(ULDUAR_MAP_ID, RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, + RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y - 20.0f, bot->GetPositionZ(), + RazorscaleBossHelper::RAZORSCALE_ARENA_RADIUS - 12.0f, MovementPriority::MOVEMENT_COMBAT); } // Otherwise, move inside a 12-yard radius around the arena center - return MoveInside( - ULDUAR_MAP_ID, - RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, - RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y, - bot->GetPositionZ(), - 12.0f, - MovementPriority::MOVEMENT_COMBAT - ); + return MoveInside(ULDUAR_MAP_ID, RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, + RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y, bot->GetPositionZ(), 12.0f, + MovementPriority::MOVEMENT_COMBAT); } return false; } @@ -1064,9 +1042,7 @@ bool RazorscaleHarpoonAction::Execute(Event event) float botDist = bot->GetDistance(closestHarpoon); if (botDist > INTERACTION_DISTANCE - 1.0f) { - return MoveTo(bot->GetMapId(), - closestHarpoon->GetPositionX(), - closestHarpoon->GetPositionY(), + return MoveTo(bot->GetMapId(), closestHarpoon->GetPositionX(), closestHarpoon->GetPositionY(), closestHarpoon->GetPositionZ()); } @@ -1453,7 +1429,12 @@ bool KologarnEyebeamAction::Execute(Event event) bool KologarnEyebeamAction::isUseful() { KologarnEyebeamTrigger kologarnEyebeamTrigger(botAI); - return kologarnEyebeamTrigger.IsActive(); + if (!kologarnEyebeamTrigger.IsActive()) + { + return false; + } + + return botAI->HasCheat(BotCheatMask::raid); } bool KologarnRtiTargetAction::isUseful() @@ -1477,7 +1458,12 @@ bool KologarnRtiTargetAction::Execute(Event event) bool KologarnCrunchArmorAction::isUseful() { KologarnCrunchArmorTrigger kologarnCrunchArmorTrigger(botAI); - return kologarnCrunchArmorTrigger.IsActive(); + if (!kologarnCrunchArmorTrigger.IsActive()) + { + return false; + } + + return botAI->HasCheat(BotCheatMask::raid); } bool KologarnCrunchArmorAction::Execute(Event event) @@ -1576,6 +1562,11 @@ bool HodirBitingColdJumpAction::Execute(Event event) // return true; } +bool HodirBitingColdJumpAction::isUseful() +{ + return botAI->HasCheat(BotCheatMask::raid); +} + bool FreyaMoveAwayNatureBombAction::isUseful() { // Check boss and it is alive @@ -1798,7 +1789,12 @@ bool FreyaMoveToHealingSporeAction::Execute(Event event) bool ThorimUnbalancingStrikeAction::isUseful() { ThorimUnbalancingStrikeTrigger thorimUnbalancingStrikeTrigger(botAI); - return thorimUnbalancingStrikeTrigger.IsActive(); + if (!thorimUnbalancingStrikeTrigger.IsActive()) + { + return false; + } + + return botAI->HasCheat(BotCheatMask::raid); } bool ThorimUnbalancingStrikeAction::Execute(Event event) @@ -2245,7 +2241,7 @@ bool MimironShockBlastAction::Execute(Event event) float dy = bot->GetPositionY() + sin(angle) * distance; float dz = bot->GetPositionZ(); if (bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(), - bot->GetPositionZ(), dx, dy, dz)) + bot->GetPositionZ(), dx, dy, dz)) { bot->TeleportTo(target->GetMapId(), dx, dy, dz, target->GetOrientation()); return true; @@ -2448,7 +2444,7 @@ bool MimironAerialCommandUnitAction::Execute(Event event) { group->SetTargetIcon(crossIndex, bot->GetGUID(), boss->GetGUID()); } - + if (assaultBot) { ObjectGuid skullTarget = group->GetTargetIcon(skullIndex); @@ -2647,3 +2643,67 @@ bool MimironCheatAction::Execute(Event event) return true; } + +bool VezaxCheatAction::Execute(Event event) +{ + // Restore bot's mana to full + uint32 maxMana = bot->GetMaxPower(POWER_MANA); + if (maxMana > 0) + { + bot->SetPower(POWER_MANA, maxMana); + } + + return true; +} + +bool VezaxShadowCrashAction::Execute(Event event) +{ + // Find General Vezax boss + Unit* boss = AI_VALUE2(Unit*, "find target", "general vezax"); + if (!boss || !boss->IsAlive()) + { + return false; + } + + // Get bot's current position relative to boss + float bossX = boss->GetPositionX(); + float bossY = boss->GetPositionY(); + float bossZ = boss->GetPositionZ(); + + float botX = bot->GetPositionX(); + float botY = bot->GetPositionY(); + + // Calculate current angle and distance from boss + float currentAngle = atan2(botY - bossY, botX - bossX); + float currentDistance = bot->GetDistance2d(boss); + + // Set desired distance from boss (stay close enough for melee, far enough for ranged) + float desiredDistance = 15.0f; + + // If too close or too far, adjust distance first + if (currentDistance < desiredDistance - 2.0f || currentDistance > desiredDistance + 2.0f) + { + currentDistance = desiredDistance; + } + + // Calculate movement increment - move in increments around the boss + float angleIncrement = M_PI / 10; + float newAngle = currentAngle + angleIncrement; + + // Calculate new position + float newX = bossX + currentDistance * cos(newAngle); + float newY = bossY + currentDistance * sin(newAngle); + float newZ = bossZ; // Keep same Z level as boss + + // Move to the new position + return MoveTo(boss->GetMapId(), newX, newY, newZ, false, false, false, true, MovementPriority::MOVEMENT_COMBAT, + true); +} + +bool VezaxMarkOfTheFacelessAction::Execute(Event event) +{ + return MoveTo(bot->GetMapId(), ULDUAR_VEZAX_MARK_OF_THE_FACELESS_SPOT.GetPositionX(), + ULDUAR_VEZAX_MARK_OF_THE_FACELESS_SPOT.GetPositionY(), + ULDUAR_VEZAX_MARK_OF_THE_FACELESS_SPOT.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); +} diff --git a/src/strategy/raids/ulduar/RaidUlduarActions.h b/src/strategy/raids/ulduar/RaidUlduarActions.h index d39c6a1b..6fef3367 100644 --- a/src/strategy/raids/ulduar/RaidUlduarActions.h +++ b/src/strategy/raids/ulduar/RaidUlduarActions.h @@ -198,6 +198,7 @@ class HodirBitingColdJumpAction : public MovementAction public: HodirBitingColdJumpAction(PlayerbotAI* ai) : MovementAction(ai, "hodir biting cold jump") {} bool Execute(Event event) override; + bool isUseful() override; }; class FreyaMoveAwayNatureBombAction : public MovementAction @@ -353,5 +354,28 @@ public: bool Execute(Event event) override; }; +class VezaxCheatAction : public Action +{ +public: + VezaxCheatAction(PlayerbotAI* ai) : Action(ai, "vezax cheat action") {} + + bool Execute(Event event) override; +}; + +class VezaxShadowCrashAction : public MovementAction +{ +public: + VezaxShadowCrashAction(PlayerbotAI* ai) : MovementAction(ai, "vezax shadow crash action") {} + + bool Execute(Event event) override; +}; + +class VezaxMarkOfTheFacelessAction : public MovementAction +{ +public: + VezaxMarkOfTheFacelessAction(PlayerbotAI* ai) : MovementAction(ai, "vezax mark of the faceless action") {} + + bool Execute(Event event) override; +}; #endif diff --git a/src/strategy/raids/ulduar/RaidUlduarStrategy.cpp b/src/strategy/raids/ulduar/RaidUlduarStrategy.cpp index d7e805c0..b7f0ba41 100644 --- a/src/strategy/raids/ulduar/RaidUlduarStrategy.cpp +++ b/src/strategy/raids/ulduar/RaidUlduarStrategy.cpp @@ -225,6 +225,21 @@ void RaidUlduarStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode( "mimiron cheat trigger", NextAction::array(0, new NextAction("mimiron cheat action", ACTION_RAID), nullptr))); + + // + // General Vezax + // + triggers.push_back(new TriggerNode( + "vezax cheat trigger", + NextAction::array(0, new NextAction("vezax cheat action", ACTION_RAID), nullptr))); + + triggers.push_back(new TriggerNode( + "vezax shadow crash trigger", + NextAction::array(0, new NextAction("vezax shadow crash action", ACTION_RAID), nullptr))); + + triggers.push_back(new TriggerNode( + "vezax mark of the faceless trigger", + NextAction::array(0, new NextAction("vezax mark of the faceless action", ACTION_RAID), nullptr))); } void RaidUlduarStrategy::InitMultipliers(std::vector& multipliers) diff --git a/src/strategy/raids/ulduar/RaidUlduarTriggerContext.h b/src/strategy/raids/ulduar/RaidUlduarTriggerContext.h index 84cd1104..a412748b 100644 --- a/src/strategy/raids/ulduar/RaidUlduarTriggerContext.h +++ b/src/strategy/raids/ulduar/RaidUlduarTriggerContext.h @@ -64,6 +64,9 @@ public: creators["mimiron rocket strike trigger"] = &RaidUlduarTriggerContext::mimiron_rocket_strike_trigger; creators["mimiron phase 4 mark dps trigger"] = &RaidUlduarTriggerContext::mimiron_phase_4_mark_dps_trigger; creators["mimiron cheat trigger"] = &RaidUlduarTriggerContext::mimiron_cheat_trigger; + creators["vezax cheat trigger"] = &RaidUlduarTriggerContext::vezax_cheat_trigger; + creators["vezax shadow crash trigger"] = &RaidUlduarTriggerContext::vezax_shadow_crash_trigger; + creators["vezax mark of the faceless trigger"] = &RaidUlduarTriggerContext::vezax_mark_of_the_faceless_trigger; } private: @@ -115,6 +118,9 @@ private: static Trigger* mimiron_rocket_strike_trigger(PlayerbotAI* ai) { return new MimironRocketStrikeTrigger(ai); } static Trigger* mimiron_phase_4_mark_dps_trigger(PlayerbotAI* ai) { return new MimironPhase4MarkDpsTrigger(ai); } static Trigger* mimiron_cheat_trigger(PlayerbotAI* ai) { return new MimironCheatTrigger(ai); } + static Trigger* vezax_cheat_trigger(PlayerbotAI* ai) { return new VezaxCheatTrigger(ai); } + static Trigger* vezax_shadow_crash_trigger(PlayerbotAI* ai) { return new VezaxShadowCrashTrigger(ai); } + static Trigger* vezax_mark_of_the_faceless_trigger(PlayerbotAI* ai) { return new VezaxMarkOfTheFacelessTrigger(ai); } }; #endif diff --git a/src/strategy/raids/ulduar/RaidUlduarTriggers.cpp b/src/strategy/raids/ulduar/RaidUlduarTriggers.cpp index 6deb508c..7e726eef 100644 --- a/src/strategy/raids/ulduar/RaidUlduarTriggers.cpp +++ b/src/strategy/raids/ulduar/RaidUlduarTriggers.cpp @@ -1533,6 +1533,11 @@ bool MimironPhase4MarkDpsTrigger::IsActive() bool MimironCheatTrigger::IsActive() { + if (!botAI->HasCheat(BotCheatMask::raid)) + { + return false; + } + if (!botAI->IsMainTank(bot)) { return false; @@ -1557,3 +1562,60 @@ bool MimironCheatTrigger::IsActive() return false; } + +bool VezaxCheatTrigger::IsActive() +{ + if (!botAI->HasCheat(BotCheatMask::raid)) + { + return false; + } + + Unit* boss = AI_VALUE2(Unit*, "find target", "general vezax"); + + // Check boss and it is alive + if (!boss || !boss->IsAlive()) + { + return false; + } + + if (!AI_VALUE2(bool, "has mana", "self target")) + { + return false; + } + + return AI_VALUE2(uint8, "mana", "self target") < sPlayerbotAIConfig->lowMana; +} + +bool VezaxShadowCrashTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "general vezax"); + + // Check boss and it is alive + if (!boss || !boss->IsAlive()) + { + return false; + } + + return botAI->HasAura(SPELL_SHADOW_CRASH, bot); +} + +bool VezaxMarkOfTheFacelessTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "general vezax"); + + // Check boss and it is alive + if (!boss || !boss->IsAlive()) + { + return false; + } + + if (!botAI->HasAura(SPELL_MARK_OF_THE_FACELESS, bot)) + { + return false; + } + + float distance = bot->GetDistance2d(ULDUAR_VEZAX_MARK_OF_THE_FACELESS_SPOT.GetPositionX(), + ULDUAR_VEZAX_MARK_OF_THE_FACELESS_SPOT.GetPositionY()); + + return distance > 2.0f; +} diff --git a/src/strategy/raids/ulduar/RaidUlduarTriggers.h b/src/strategy/raids/ulduar/RaidUlduarTriggers.h index 987c1ce4..6ebf1a7a 100644 --- a/src/strategy/raids/ulduar/RaidUlduarTriggers.h +++ b/src/strategy/raids/ulduar/RaidUlduarTriggers.h @@ -77,6 +77,10 @@ enum UlduarIDs SPELL_P3WX2_LASER_BARRAGE_3 = 64042, SPELL_P3WX2_LASER_BARRAGE_AURA_1 = 63274, SPELL_P3WX2_LASER_BARRAGE_AURA_2 = 63300, + + //General Vezax + SPELL_MARK_OF_THE_FACELESS = 63276, + SPELL_SHADOW_CRASH = 63277, // Buffs SPELL_FROST_TRAP = 13809 @@ -106,6 +110,7 @@ const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_5_YARDS_1 = Position(2217.8877f const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_1 = Position(2212.193f, -307.44992f, 412.1348f); const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_2 = Position(2212.1353f, -318.20795f, 412.1348f); const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_3 = Position(2212.1956f, -328.0144f, 412.1348f); +const Position ULDUAR_THORIM_JUMP_END_POINT = Position(2137.8818f, -278.18942f, 419.66653f); const Position ULDUAR_THORIM_PHASE2_TANK_SPOT = Position(2134.8572f, -287.0291f, 419.4935f); const Position ULDUAR_THORIM_PHASE2_RANGE1_SPOT = Position(2112.8752f, -267.69305f, 419.52814f); const Position ULDUAR_THORIM_PHASE2_RANGE2_SPOT = Position(2134.1296f, -257.3316f, 419.8462f); @@ -117,6 +122,7 @@ const Position ULDUAR_MIMIRON_PHASE2_SIDE2MELEE_SPOT = Position(2739.4746f, 2569 const Position ULDUAR_MIMIRON_PHASE2_SIDE3RANGE_SPOT = Position(2754.1294f, 2553.9954f, 364.31357f); const Position ULDUAR_MIMIRON_PHASE2_SIDE3MELEE_SPOT = Position(2746.8513f, 2565.4263f, 364.31357f); const Position ULDUAR_MIMIRON_PHASE4_TANK_SPOT = Position(2744.5754f, 2570.8657f, 364.3138f); +const Position ULDUAR_VEZAX_MARK_OF_THE_FACELESS_SPOT = Position(1913.6501f, 122.93989f, 342.38083f); // // Flame Levi @@ -418,4 +424,28 @@ public: bool IsActive() override; }; +// +// General Vezax +// +class VezaxCheatTrigger : public Trigger +{ +public: + VezaxCheatTrigger(PlayerbotAI* ai) : Trigger(ai, "vezax cheat trigger") {} + bool IsActive() override; +}; + +class VezaxShadowCrashTrigger : public Trigger +{ +public: + VezaxShadowCrashTrigger(PlayerbotAI* ai) : Trigger(ai, "vezax shadow crash trigger") {} + bool IsActive() override; +}; + +class VezaxMarkOfTheFacelessTrigger : public Trigger +{ +public: + VezaxMarkOfTheFacelessTrigger(PlayerbotAI* ai) : Trigger(ai, "vezax mark of the faceless trigger") {} + bool IsActive() override; +}; + #endif From db7a17ffde8fcf43aca03aaff7661ececfb00991 Mon Sep 17 00:00:00 2001 From: NoxMax <50133316+NoxMax@users.noreply.github.com> Date: Sun, 27 Jul 2025 00:13:20 -0600 Subject: [PATCH 5/5] Fix: Properly track RNDbot and AddClass accounts, and login faction balance issue (#1434) * AssignAccountTypes & AddRandomBots Fix: Properly track RNDbot and AddClass accounts, and login faction balance issue * code style edits * fix addclass init on first build of playerbots_account_type --- .../base/playerbots_account_type.sql | 8 + .../2025_07_01_00_account_type.sql | 9 + src/PlayerbotAIConfig.cpp | 10 +- src/PlayerbotMgr.cpp | 4 +- src/RandomPlayerbotFactory.cpp | 110 ++++- src/RandomPlayerbotMgr.cpp | 417 +++++++++++++----- src/RandomPlayerbotMgr.h | 18 +- 7 files changed, 449 insertions(+), 127 deletions(-) create mode 100644 data/sql/playerbots/base/playerbots_account_type.sql create mode 100644 data/sql/playerbots/updates/db_playerbots/2025_07_01_00_account_type.sql diff --git a/data/sql/playerbots/base/playerbots_account_type.sql b/data/sql/playerbots/base/playerbots_account_type.sql new file mode 100644 index 00000000..d9ce9074 --- /dev/null +++ b/data/sql/playerbots/base/playerbots_account_type.sql @@ -0,0 +1,8 @@ +DROP TABLE IF EXISTS `playerbots_account_type`; +CREATE TABLE `playerbots_account_type` ( + `account_id` int unsigned NOT NULL, + `account_type` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '0 = unassigned, 1 = RNDbot, 2 = AddClass', + `assignment_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`account_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Playerbot account type assignments'; + diff --git a/data/sql/playerbots/updates/db_playerbots/2025_07_01_00_account_type.sql b/data/sql/playerbots/updates/db_playerbots/2025_07_01_00_account_type.sql new file mode 100644 index 00000000..1bee566f --- /dev/null +++ b/data/sql/playerbots/updates/db_playerbots/2025_07_01_00_account_type.sql @@ -0,0 +1,9 @@ +-- Create playerbots_account_type table for tracking accounts assignments +DROP TABLE IF EXISTS `playerbots_account_type`; +CREATE TABLE `playerbots_account_type` ( + `account_id` int unsigned NOT NULL, + `account_type` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '0 = unassigned, 1 = RNDbot, 2 = AddClass', + `assignment_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`account_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Playerbot account type assignments'; + diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index 7899de84..86bf734b 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -118,7 +118,6 @@ bool PlayerbotAIConfig::Initialize() tellWhenAvoidAoe = sConfigMgr->GetOption("AiPlayerbot.TellWhenAvoidAoe", false); randomGearLoweringChance = sConfigMgr->GetOption("AiPlayerbot.RandomGearLoweringChance", 0.0f); - incrementalGearInit = sConfigMgr->GetOption("AiPlayerbot.IncrementalGearInit", true); randomGearQualityLimit = sConfigMgr->GetOption("AiPlayerbot.RandomGearQualityLimit", 3); randomGearScoreLimit = sConfigMgr->GetOption("AiPlayerbot.RandomGearScoreLimit", 0); @@ -157,7 +156,7 @@ bool PlayerbotAIConfig::Initialize() LoadList>( sConfigMgr->GetOption("AiPlayerbot.RandomBotQuestIds", "7848,3802,5505,6502,7761"), randomBotQuestIds); - + LoadSet>(sConfigMgr->GetOption("AiPlayerbot.DisallowedGameObjects", "176213,17155"), disallowedGameObjects); botAutologin = sConfigMgr->GetOption("AiPlayerbot.BotAutologin", false); @@ -190,7 +189,7 @@ bool PlayerbotAIConfig::Initialize() maxRandomBotsPriceChangeInterval = sConfigMgr->GetOption("AiPlayerbot.MaxRandomBotsPriceChangeInterval", 48 * HOUR); randomBotJoinLfg = sConfigMgr->GetOption("AiPlayerbot.RandomBotJoinLfg", true); - + //////////////////////////// ICC EnableICCBuffs = sConfigMgr->GetOption("AiPlayerbot.EnableICCBuffs", true); @@ -346,7 +345,7 @@ bool PlayerbotAIConfig::Initialize() { std::string setting = "AiPlayerbot.ZoneBracket." + std::to_string(zoneId); std::string value = sConfigMgr->GetOption(setting, ""); - + if (!value.empty()) { size_t commaPos = value.find(','); @@ -618,6 +617,9 @@ bool PlayerbotAIConfig::Initialize() return true; } + // Assign account types after accounts are created + sRandomPlayerbotMgr->AssignAccountTypes(); + if (sPlayerbotAIConfig->enabled) { sRandomPlayerbotMgr->Init(); diff --git a/src/PlayerbotMgr.cpp b/src/PlayerbotMgr.cpp index 72489431..ee6c6926 100644 --- a/src/PlayerbotMgr.cpp +++ b/src/PlayerbotMgr.cpp @@ -584,8 +584,8 @@ void PlayerbotHolder::OnBotLogin(Player* const bot) } bot->SaveToDB(false, false); - bool addClassBot = sRandomPlayerbotMgr->IsAddclassBot(bot->GetGUID().GetCounter()); - if (addClassBot && master && isRandomAccount && abs((int)master->GetLevel() - (int)bot->GetLevel()) > 3) + bool addClassBot = sRandomPlayerbotMgr->IsAccountType(accountId, 2); + if (addClassBot && master && abs((int)master->GetLevel() - (int)bot->GetLevel()) > 3) { // PlayerbotFactory factory(bot, master->GetLevel()); // factory.Randomize(false); diff --git a/src/RandomPlayerbotFactory.cpp b/src/RandomPlayerbotFactory.cpp index d411ffc9..a3d6a2c7 100644 --- a/src/RandomPlayerbotFactory.cpp +++ b/src/RandomPlayerbotFactory.cpp @@ -393,37 +393,118 @@ std::string const RandomPlayerbotFactory::CreateRandomBotName(NameRaceAndGender return std::move(botName); } +// Calculates the total number of required accounts, either using the specified randomBotAccountCount +// or determining it dynamically based on MaxRandomBots, EnablePeriodicOnlineOffline and its ratio, +// and AddClassAccountPoolSize. The system also factors in the types of existing account, as assigned by +// AssignAccountTypes() uint32 RandomPlayerbotFactory::CalculateTotalAccountCount() { - // Calculates the total number of required accounts, either using the specified randomBotAccountCount - // or determining it dynamically based on the WOTLK condition, max random bots, rotation pool size, - // and additional class account pool size. + // Reset account types if features are disabled + // Reset is done here to precede needed accounts calculations + if (sPlayerbotAIConfig->maxRandomBots == 0 || sPlayerbotAIConfig->addClassAccountPoolSize == 0) + { + if (sPlayerbotAIConfig->maxRandomBots == 0) + { + PlayerbotsDatabase.Execute("UPDATE playerbots_account_type SET account_type = 0 WHERE account_type = 1"); + LOG_INFO("playerbots", "MaxRandomBots set to 0, any RNDbot accounts (type 1) will be unassigned (type 0)"); + } + if (sPlayerbotAIConfig->addClassAccountPoolSize == 0) + { + PlayerbotsDatabase.Execute("UPDATE playerbots_account_type SET account_type = 0 WHERE account_type = 2"); + LOG_INFO("playerbots", "AddClassAccountPoolSize set to 0, any AddClass accounts (type 2) will be unassigned (type 0)"); + } + + // Wait for DB to reflect the change, up to 1 second max. This is needed to make sure other logs don't show wrong info + for (int waited = 0; waited < 1000; waited += 50) + { + QueryResult res = PlayerbotsDatabase.Query("SELECT COUNT(*) FROM playerbots_account_type WHERE account_type IN ({}, {})", + sPlayerbotAIConfig->maxRandomBots == 0 ? 1 : -1, + sPlayerbotAIConfig->addClassAccountPoolSize == 0 ? 2 : -1); + + if (!res || res->Fetch()[0].Get() == 0) + { + break; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); // Extra 50ms fixed delay for safety. + } + } // Checks if randomBotAccountCount is set, otherwise calculate it dynamically. if (sPlayerbotAIConfig->randomBotAccountCount > 0) return sPlayerbotAIConfig->randomBotAccountCount; - // Avoid creating accounts if both maxRandom & ClassBots are set to zero. - if (sPlayerbotAIConfig->maxRandomBots == 0 && - sPlayerbotAIConfig->addClassAccountPoolSize == 0) - return 0; + // Check existing account types + uint32 existingRndBotAccounts = 0; + uint32 existingAddClassAccounts = 0; + uint32 existingUnassignedAccounts = 0; - //bool isWOTLK = sWorld->getIntConfig(CONFIG_EXPANSION) == EXPANSION_WRATH_OF_THE_LICH_KING; //not used, line marked for removal. + QueryResult typeCheck = PlayerbotsDatabase.Query("SELECT account_type, COUNT(*) FROM playerbots_account_type GROUP BY account_type"); + if (typeCheck) + { + do + { + Field* fields = typeCheck->Fetch(); + uint8 accountType = fields[0].Get(); + uint32 count = fields[1].Get(); - // Determine divisor based on WOTLK condition + if (accountType == 0) existingUnassignedAccounts = count; + else if (accountType == 1) existingRndBotAccounts = count; + else if (accountType == 2) existingAddClassAccounts = count; + } while (typeCheck->NextRow()); + } + + // Determine divisor based on Death Knight login eligibility and requested A&H faction ratio int divisor = CalculateAvailableCharsPerAccount(); // Calculate max bots int maxBots = sPlayerbotAIConfig->maxRandomBots; - // Take perodic online - offline into account + // Take periodic online - offline into account if (sPlayerbotAIConfig->enablePeriodicOnlineOffline) { maxBots *= sPlayerbotAIConfig->periodicOnlineOfflineRatio; } - // Calculate base accounts, add class account pool size, and add 1 as a fixed offset - uint32 baseAccounts = maxBots / divisor; - return baseAccounts + sPlayerbotAIConfig->addClassAccountPoolSize + 1; + // Calculate number of accounts needed for RNDbots + // Result is rounded up for maxBots not cleanly divisible by the divisor + uint32 neededRndBotAccounts = (maxBots + divisor - 1) / divisor; + uint32 neededAddClassAccounts = sPlayerbotAIConfig->addClassAccountPoolSize; + + // Start with existing total + uint32 existingTotal = existingRndBotAccounts + existingAddClassAccounts + existingUnassignedAccounts; + + // Calculate shortfalls after using unassigned accounts + uint32 availableUnassigned = existingUnassignedAccounts; + uint32 additionalAccountsNeeded = 0; + + // Check RNDbot needs + if (neededRndBotAccounts > existingRndBotAccounts) + { + uint32 rndBotShortfall = neededRndBotAccounts - existingRndBotAccounts; + if (rndBotShortfall <= availableUnassigned) + availableUnassigned -= rndBotShortfall; + else + { + additionalAccountsNeeded += (rndBotShortfall - availableUnassigned); + availableUnassigned = 0; + } + } + + // Check AddClass needs + if (neededAddClassAccounts > existingAddClassAccounts) + { + uint32 addClassShortfall = neededAddClassAccounts - existingAddClassAccounts; + if (addClassShortfall <= availableUnassigned) + availableUnassigned -= addClassShortfall; + else + { + additionalAccountsNeeded += (addClassShortfall - availableUnassigned); + availableUnassigned = 0; + } + } + + // Return existing total plus any additional accounts needed + return existingTotal + additionalAccountsNeeded; } uint32 RandomPlayerbotFactory::CalculateAvailableCharsPerAccount() @@ -475,8 +556,9 @@ void RandomPlayerbotFactory::CreateRandomBots() LOG_INFO("playerbots", "Deleting all random bot characters and accounts..."); // First execute all the cleanup SQL commands - // Clear playerbots_random_bots table + // Clear playerbots_random_bots and playerbots_account_type PlayerbotsDatabase.Execute("DELETE FROM playerbots_random_bots"); + PlayerbotsDatabase.Execute("DELETE FROM playerbots_account_type"); // Get the database names dynamically std::string loginDBName = LoginDatabase.GetConnectionInfo()->database; diff --git a/src/RandomPlayerbotMgr.cpp b/src/RandomPlayerbotMgr.cpp index fb9504a9..fd29b1df 100644 --- a/src/RandomPlayerbotMgr.cpp +++ b/src/RandomPlayerbotMgr.cpp @@ -515,15 +515,174 @@ void RandomPlayerbotMgr::UpdateAIInternal(uint32 elapsed, bool /*minimal*/) // setActivityPercentage(activityPercentage); // } +// Assigns accounts as RNDbot accounts (type 1) based on MaxRandomBots and EnablePeriodicOnlineOffline and its ratio, +// and assigns accounts as AddClass accounts (type 2) based AddClassAccountPoolSize. Type 1 and 2 assignments are +// permenant, unless MaxRandomBots or AddClassAccountPoolSize are set to 0. If so, their associated accounts will +// be unassigned (type 0) +void RandomPlayerbotMgr::AssignAccountTypes() +{ + LOG_INFO("playerbots", "Assigning account types for random bot accounts..."); + + // Clear existing filtered lists + rndBotTypeAccounts.clear(); + addClassTypeAccounts.clear(); + + // First, get ALL randombot accounts from the database + std::vector allRandomBotAccounts; + QueryResult allAccounts = LoginDatabase.Query( + "SELECT id FROM account WHERE username LIKE '{}%%' ORDER BY id", + sPlayerbotAIConfig->randomBotAccountPrefix.c_str()); + + if (allAccounts) + { + do + { + Field* fields = allAccounts->Fetch(); + uint32 accountId = fields[0].Get(); + allRandomBotAccounts.push_back(accountId); + } while (allAccounts->NextRow()); + } + + LOG_INFO("playerbots", "Found {} total randombot accounts in database", allRandomBotAccounts.size()); + + // Check existing assignments + QueryResult existingAssignments = PlayerbotsDatabase.Query("SELECT account_id, account_type FROM playerbots_account_type"); + std::map currentAssignments; + + if (existingAssignments) + { + do + { + Field* fields = existingAssignments->Fetch(); + uint32 accountId = fields[0].Get(); + uint8 accountType = fields[1].Get(); + currentAssignments[accountId] = accountType; + } while (existingAssignments->NextRow()); + } + + // Mark ALL randombot accounts as unassigned if not already assigned + for (uint32 accountId : allRandomBotAccounts) + { + if (currentAssignments.find(accountId) == currentAssignments.end()) + { + PlayerbotsDatabase.Execute("INSERT INTO playerbots_account_type (account_id, account_type) VALUES ({}, 0) ON DUPLICATE KEY UPDATE account_type = account_type", accountId); + currentAssignments[accountId] = 0; + } + } + + // Calculate needed RNDbot accounts + uint32 neededRndBotAccounts = 0; + if (sPlayerbotAIConfig->maxRandomBots > 0) + { + int divisor = RandomPlayerbotFactory::CalculateAvailableCharsPerAccount(); + int maxBots = sPlayerbotAIConfig->maxRandomBots; + + // Take periodic online-offline into account + if (sPlayerbotAIConfig->enablePeriodicOnlineOffline) + { + maxBots *= sPlayerbotAIConfig->periodicOnlineOfflineRatio; + } + + // Calculate base accounts needed for RNDbots, ensuring round up for maxBots not cleanly divisible by the divisor + neededRndBotAccounts = (maxBots + divisor - 1) / divisor; + } + + // Count existing assigned accounts + uint32 existingRndBotAccounts = 0; + uint32 existingAddClassAccounts = 0; + + for (const auto& [accountId, accountType] : currentAssignments) + { + if (accountType == 1) existingRndBotAccounts++; + else if (accountType == 2) existingAddClassAccounts++; + } + + // Assign RNDbot accounts from lowest position if needed + if (existingRndBotAccounts < neededRndBotAccounts) + { + uint32 toAssign = neededRndBotAccounts - existingRndBotAccounts; + uint32 assigned = 0; + + for (uint32 i = 0; i < allRandomBotAccounts.size() && assigned < toAssign; i++) + { + uint32 accountId = allRandomBotAccounts[i]; + if (currentAssignments[accountId] == 0) // Unassigned + { + PlayerbotsDatabase.Execute("UPDATE playerbots_account_type SET account_type = 1, assignment_date = NOW() WHERE account_id = {}", accountId); + currentAssignments[accountId] = 1; + assigned++; + } + } + + if (assigned < toAssign) + { + LOG_ERROR("playerbots", "Not enough unassigned accounts to fulfill RNDbot requirements. Need {} more accounts.", toAssign - assigned); + } + } + + // Assign AddClass accounts from highest position if needed + uint32 neededAddClassAccounts = sPlayerbotAIConfig->addClassAccountPoolSize; + + if (existingAddClassAccounts < neededAddClassAccounts) + { + uint32 toAssign = neededAddClassAccounts - existingAddClassAccounts; + uint32 assigned = 0; + + for (int i = allRandomBotAccounts.size() - 1; i >= 0 && assigned < toAssign; i--) + { + uint32 accountId = allRandomBotAccounts[i]; + if (currentAssignments[accountId] == 0) // Unassigned + { + PlayerbotsDatabase.Execute("UPDATE playerbots_account_type SET account_type = 2, assignment_date = NOW() WHERE account_id = {}", accountId); + currentAssignments[accountId] = 2; + assigned++; + } + } + + if (assigned < toAssign) + { + LOG_ERROR("playerbots", "Not enough unassigned accounts to fulfill AddClass requirements. Need {} more accounts.", toAssign - assigned); + } + } + + // Populate filtered account lists with ALL accounts of each type + for (const auto& [accountId, accountType] : currentAssignments) + { + if (accountType == 1) rndBotTypeAccounts.push_back(accountId); + else if (accountType == 2) addClassTypeAccounts.push_back(accountId); + } + + LOG_INFO("playerbots", "Account type assignment complete: {} RNDbot accounts, {} AddClass accounts, {} unassigned", + rndBotTypeAccounts.size(), addClassTypeAccounts.size(), + currentAssignments.size() - rndBotTypeAccounts.size() - addClassTypeAccounts.size()); +} + +bool RandomPlayerbotMgr::IsAccountType(uint32 accountId, uint8 accountType) +{ + QueryResult result = PlayerbotsDatabase.Query("SELECT 1 FROM playerbots_account_type WHERE account_id = {} AND account_type = {}", accountId, accountType); + return result != nullptr; +} + +// Logs-in bots in 4 phases. Phase 1 logs Alliance bots up to how much is expected according to the faction ratio, +// and Phase 2 logs-in the remainder Horde bots to reach the total maxAllowedBotCount. If maxAllowedBotCount is not +// reached after Phase 2, the function goes back to log-in Alliance bots and reach maxAllowedBotCount. This is done +// because not every account is guaranteed 5A/5H bots, so the true ratio might be skewed by few percentages. Finally, +// Phase 4 is reached if and only if the value of RandomBotAccountCount is lower than it should. uint32 RandomPlayerbotMgr::AddRandomBots() { uint32 maxAllowedBotCount = GetEventValue(0, "bot_count"); + static time_t missingBotsTimer = 0; if (currentBots.size() < maxAllowedBotCount) { + // Calculate how many bots to add maxAllowedBotCount -= currentBots.size(); maxAllowedBotCount = std::min(sPlayerbotAIConfig->randomBotsPerInterval, maxAllowedBotCount); + // Single RNG instance for all shuffling + std::mt19937 rng(std::chrono::steady_clock::now().time_since_epoch().count()); + + // Only need to track the Alliance count, as it's in Phase 1 uint32 totalRatio = sPlayerbotAIConfig->randomBotAllianceRatio + sPlayerbotAIConfig->randomBotHordeRatio; uint32 allowedAllianceCount = maxAllowedBotCount * (sPlayerbotAIConfig->randomBotAllianceRatio) / totalRatio; @@ -535,26 +694,42 @@ uint32 RandomPlayerbotMgr::AddRandomBots() allowedAllianceCount++; } - uint32 allowedHordeCount = maxAllowedBotCount - allowedAllianceCount; - - for (std::vector::iterator i = sPlayerbotAIConfig->randomBotAccounts.begin(); - i != sPlayerbotAIConfig->randomBotAccounts.end(); i++) + // Determine which accounts to use based on EnablePeriodicOnlineOffline + std::vector accountsToUse; + if (sPlayerbotAIConfig->enablePeriodicOnlineOffline) { - uint32 accountId = *i; - if (sPlayerbotAIConfig->enablePeriodicOnlineOffline) - { - // minus addclass bots account - int32 baseAccount = - RandomPlayerbotFactory::CalculateTotalAccountCount() - sPlayerbotAIConfig->addClassAccountPoolSize; - if (baseAccount <= 0 || baseAccount > sPlayerbotAIConfig->randomBotAccounts.size()) - { - LOG_ERROR("playerbots", "Account calculation error with PeriodicOnlineOffline"); - return 0; - } - uint32 index = urand(0, baseAccount - 1); - accountId = sPlayerbotAIConfig->randomBotAccounts[index]; + // Calculate how many accounts can be used + // With enablePeriodicOnlineOffline, don't use all of rndBotTypeAccounts right away. Fraction results are rounded up + uint32 accountsToUseCount = (rndBotTypeAccounts.size() + sPlayerbotAIConfig->periodicOnlineOfflineRatio - 1) + / sPlayerbotAIConfig->periodicOnlineOfflineRatio; + + // Randomly select accounts + std::vector shuffledAccounts = rndBotTypeAccounts; + std::shuffle(shuffledAccounts.begin(), shuffledAccounts.end(), rng); + + for (uint32 i = 0; i < accountsToUseCount && i < shuffledAccounts.size(); i++) + { + accountsToUse.push_back(shuffledAccounts[i]); } + } + else + { + accountsToUse = rndBotTypeAccounts; + } + + // Pre-map all characters from selected accounts + struct CharacterInfo + { + uint32 guid; + uint8 rClass; + uint8 rRace; + uint32 accountId; + }; + std::vector allCharacters; + + for (uint32 accountId : accountsToUse) + { CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARS_BY_ACCOUNT_ID); stmt->SetData(0, accountId); @@ -562,87 +737,115 @@ uint32 RandomPlayerbotMgr::AddRandomBots() if (!result) continue; - std::vector allGuidInfos; - do { Field* fields = result->Fetch(); - GuidClassRaceInfo info; + CharacterInfo info; info.guid = fields[0].Get(); info.rClass = fields[1].Get(); info.rRace = fields[2].Get(); - allGuidInfos.push_back(info); + info.accountId = accountId; + allCharacters.push_back(info); } while (result->NextRow()); - - // random shuffle for class balance - std::mt19937 rnd(time(0)); - std::shuffle(allGuidInfos.begin(), allGuidInfos.end(), rnd); - - std::vector guids; - for (const auto& info : allGuidInfos) - { - ObjectGuid::LowType guid = info.guid; - uint32 rClass = info.rClass; - uint32 rRace = info.rRace; - - if (GetEventValue(guid, "add")) - continue; - - if (GetEventValue(guid, "logout")) - continue; - - if (GetPlayerBot(guid)) - continue; - - if (std::find(currentBots.begin(), currentBots.end(), guid) != currentBots.end()) - continue; - - if (sPlayerbotAIConfig->disableDeathKnightLogin) - { - if (rClass == CLASS_DEATH_KNIGHT) - { - continue; - } - } - - uint32 isAlliance = IsAlliance(rRace); - bool factionNotAllowed = (!allowedAllianceCount && isAlliance) || (!allowedHordeCount && !isAlliance); - - if (factionNotAllowed) - continue; - - if (isAlliance) - { - allowedAllianceCount--; - } - else - { - allowedHordeCount--; - } - - uint32 add_time = sPlayerbotAIConfig->enablePeriodicOnlineOffline - ? urand(sPlayerbotAIConfig->minRandomBotInWorldTime, - sPlayerbotAIConfig->maxRandomBotInWorldTime) - : sPlayerbotAIConfig->permanantlyInWorldTime; - - SetEventValue(guid, "add", 1, add_time); - SetEventValue(guid, "logout", 0, 0); - currentBots.push_back(guid); - - maxAllowedBotCount--; - if (!maxAllowedBotCount) - break; - } - - if (!maxAllowedBotCount) - break; } + // Shuffle for class balance + std::shuffle(allCharacters.begin(), allCharacters.end(), rng); + + // Separate characters by faction for phased login + std::vector allianceChars; + std::vector hordeChars; + + for (const auto& charInfo : allCharacters) + { + if (IsAlliance(charInfo.rRace)) + allianceChars.push_back(charInfo); + + else + hordeChars.push_back(charInfo); + } + + // Lambda to handle bot login logic + auto tryLoginBot = [&](const CharacterInfo& charInfo) -> bool + { + if (GetEventValue(charInfo.guid, "add") || + GetEventValue(charInfo.guid, "logout") || + GetPlayerBot(charInfo.guid) || + std::find(currentBots.begin(), currentBots.end(), charInfo.guid) != currentBots.end() || + (sPlayerbotAIConfig->disableDeathKnightLogin && charInfo.rClass == CLASS_DEATH_KNIGHT)) + { + return false; + } + + uint32 add_time = sPlayerbotAIConfig->enablePeriodicOnlineOffline + ? urand(sPlayerbotAIConfig->minRandomBotInWorldTime, + sPlayerbotAIConfig->maxRandomBotInWorldTime) + : sPlayerbotAIConfig->permanantlyInWorldTime; + + SetEventValue(charInfo.guid, "add", 1, add_time); + SetEventValue(charInfo.guid, "logout", 0, 0); + currentBots.push_back(charInfo.guid); + + return true; + }; + + // PHASE 1: Log-in Alliance bots up to allowedAllianceCount + for (const auto& charInfo : allianceChars) + { + if (!allowedAllianceCount) + break; + + if (tryLoginBot(charInfo)) + { + maxAllowedBotCount--; + allowedAllianceCount--; + } + } + + // PHASE 2: Log-in Horde bots up to maxAllowedBotCount + for (const auto& charInfo : hordeChars) + { + if (!maxAllowedBotCount) + break; + + if (tryLoginBot(charInfo)) + maxAllowedBotCount--; + } + + // PHASE 3: If maxAllowedBotCount wasn't reached, log-in more Alliance bots + for (const auto& charInfo : allianceChars) + { + if (!maxAllowedBotCount) + break; + + if (tryLoginBot(charInfo)) + maxAllowedBotCount--; + } + + // PHASE 4: An error is given if maxAllowedBotCount is still not reached if (maxAllowedBotCount) - LOG_ERROR("playerbots", - "Not enough random bot accounts available. Try to increase RandomBotAccountCount " - "in your conf file", - ceil(maxAllowedBotCount / 10)); + { + if (missingBotsTimer == 0) + missingBotsTimer = time(nullptr); + + if (time(nullptr) - missingBotsTimer >= 10) + { + int divisor = RandomPlayerbotFactory::CalculateAvailableCharsPerAccount(); + uint32 moreAccountsNeeded = (maxAllowedBotCount + divisor - 1) / divisor; + LOG_ERROR("playerbots", + "Can't log-in all the requested bots. Try increasing RandomBotAccountCount in your conf file.\n" + "{} more accounts needed.", moreAccountsNeeded); + missingBotsTimer = 0; // Reset timer so error is not spammed every tick + } + } + else + { + missingBotsTimer = 0; // Reset timer if logins for this interval were successful + } + } + else + { + missingBotsTimer = 0; // Reset timer if there's enough bots } return currentBots.size(); @@ -1165,7 +1368,6 @@ void RandomPlayerbotMgr::ScheduleChangeStrategy(uint32 bot, uint32 time) bool RandomPlayerbotMgr::ProcessBot(uint32 bot) { ObjectGuid botGUID = ObjectGuid::Create(bot); - Player* player = GetPlayerBot(botGUID); PlayerbotAI* botAI = player ? GET_PLAYERBOT_AI(player) : nullptr; @@ -1875,24 +2077,21 @@ void RandomPlayerbotMgr::PrepareTeleportCache() void RandomPlayerbotMgr::PrepareAddclassCache() { - /// @FIXME: Modifying RandomBotAccountCount may cause the original addclass bots to be converted into rndbots, - // which needs to be fixed by separating the two accounts in implementation - size_t poolSize = sPlayerbotAIConfig->addClassAccountPoolSize; - size_t start = sPlayerbotAIConfig->randomBotAccounts.size() > poolSize - ? sPlayerbotAIConfig->randomBotAccounts.size() - poolSize - : 0; + // Using accounts marked as type 2 (AddClass) int32 collected = 0; - for (size_t i = start; i < sPlayerbotAIConfig->randomBotAccounts.size(); i++) + + for (uint32 accountId : addClassTypeAccounts) { for (uint8 claz = CLASS_WARRIOR; claz <= CLASS_DRUID; claz++) { if (claz == 10) continue; + QueryResult results = CharacterDatabase.Query( "SELECT guid, race FROM characters " - "WHERE account = {} AND class = '{}' AND online = 0 " - "ORDER BY account DESC", - sPlayerbotAIConfig->randomBotAccounts[i], claz); + "WHERE account = {} AND class = '{}' AND online = 0", + accountId, claz); + if (results) { do @@ -1907,7 +2106,8 @@ void RandomPlayerbotMgr::PrepareAddclassCache() } } } - LOG_INFO("playerbots", ">> {} characters collected for addclass command.", collected); + + LOG_INFO("playerbots", ">> {} characters collected for addclass command from {} AddClass accounts.", collected, addClassTypeAccounts.size()); } void RandomPlayerbotMgr::Init() @@ -2286,10 +2486,13 @@ bool RandomPlayerbotMgr::IsRandomBot(ObjectGuid::LowType bot) ObjectGuid guid = ObjectGuid::Create(bot); if (!sPlayerbotAIConfig->IsInRandomAccountList(sCharacterCache->GetCharacterAccountIdByGuid(guid))) return false; + if (std::find(currentBots.begin(), currentBots.end(), bot) != currentBots.end()) return true; + return false; } + bool RandomPlayerbotMgr::IsAddclassBot(Player* bot) { if (bot && GET_PLAYERBOT_AI(bot)) @@ -2301,23 +2504,37 @@ bool RandomPlayerbotMgr::IsAddclassBot(Player* bot) { return IsAddclassBot(bot->GetGUID().GetCounter()); } + return false; } bool RandomPlayerbotMgr::IsAddclassBot(ObjectGuid::LowType bot) { ObjectGuid guid = ObjectGuid::Create(bot); + + // Check the cache with faction considerations for (uint8 claz = CLASS_WARRIOR; claz <= CLASS_DRUID; claz++) { if (claz == 10) continue; + for (uint8 isAlliance = 0; isAlliance <= 1; isAlliance++) { if (addclassCache[GetTeamClassIdx(isAlliance, claz)].find(guid) != addclassCache[GetTeamClassIdx(isAlliance, claz)].end()) + { return true; + } } } + + // If not in cache, check the account type + uint32 accountId = sCharacterCache->GetCharacterAccountIdByGuid(guid); + if (accountId && IsAccountType(accountId, 2)) // Type 2 = AddClass + { + return true; + } + return false; } diff --git a/src/RandomPlayerbotMgr.h b/src/RandomPlayerbotMgr.h index 02d0ff9d..6a62a68b 100644 --- a/src/RandomPlayerbotMgr.h +++ b/src/RandomPlayerbotMgr.h @@ -60,7 +60,6 @@ public: bool IsEmpty() { return !lastChangeTime; } -public: uint32 value; uint32 lastChangeTime; uint32 validIn; @@ -104,10 +103,6 @@ public: void LogPlayerLocation(); void UpdateAIInternal(uint32 elapsed, bool minimal = false) override; -private: - //void ScaleBotActivity(); - -public: uint32 activeBots = 0; static bool HandlePlayerbotConsoleCommand(ChatHandler* handler, char const* args); bool IsRandomBot(Player* bot); @@ -189,6 +184,11 @@ public: }; std::map zone2LevelBracket; std::map> bankerLocsPerLevelCache; + + // Account type management + void AssignAccountTypes(); + bool IsAccountType(uint32 accountId, uint8 accountType); + protected: void OnBotLoginInternal(Player* const bot) override; @@ -218,10 +218,8 @@ private: void RandomTeleport(Player* bot, std::vector& locs, bool hearth = false); uint32 GetZoneLevel(uint16 mapId, float teleX, float teleY, float teleZ); typedef void (RandomPlayerbotMgr::*ConsoleCommandHandler)(Player*); - std::vector players; uint32 processTicks; - // std::map> rpgLocsCache; std::map>> rpgLocsCacheLevel; @@ -230,6 +228,12 @@ private: std::list currentBots; uint32 bgBotsCount; uint32 playersLevel; + + // Account lists + std::vector rndBotTypeAccounts; // Accounts marked as RNDbot (type 1) + std::vector addClassTypeAccounts; // Accounts marked as AddClass (type 2) + + //void ScaleBotActivity(); // Deprecated function }; #define sRandomPlayerbotMgr RandomPlayerbotMgr::instance()