From d02d61e6903e0f62dc41216d62448a73fe6c8f2a Mon Sep 17 00:00:00 2001 From: Crow Date: Wed, 5 Nov 2025 07:53:16 -0600 Subject: [PATCH] Implement Magtheridon strategy (#1721) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I'm marking this as a draft for now because I haven't done a detailed review of the code, but I'm posting it now in case anybody wants to give it a try. Here's what the strategy (should) do. ### **Channeler Phase** While you can probably AoE down all five Hellfire Channelers, that's more dicey with IP nerfs and it's no fun, so the strategy takes what would have still been considered an aggressive approach back in the day by (1) assigning the Main Tank to the first Channeler, (2) having Hunters misdirect two more Channelers to the MT, and (3) one Off Tank picking up each of the fourth and fifth Channelers and dragging them out of Shadow Volley Range from the main group. Sometimes the pull gets a little wonky and one of the OTs might end up with one of the Channelers that was intended for the MT, but it should work out in the end. DPS will move through Channelers from Square -> Star -> Circle -> Diamond -> Triangle. Once Square, Star, and Circle are down, the MT will go sit by Magtheridon and wait for him to become active instead of helping with the last two Channelers. I could have made the MT help with the fourth Channeler too, but it's not needed, and positioning to pick up Magtheridon after the third Channeler is a failsafe for low DPS groups that aren't able to get four down before Magtheridon breaks free. The top priority for Warlocks is to banish/fear the Burning Abyssals, and they will continue to do so even after Magtheridon becomes active (you are not supposed to kill the Abyssals; they have a gazillion HP and automatically despawn after a minute). Their next priority is to put Curse of Tongues on the Channelers. ### **Magtheridon Positioning** The MT will pick up Magtheridon and pull him (moving backwards because Magtheridon kind of hits like a bus) to a position up against the far wall. Ranged DPS will spread out from a point roughly in the center of the room, and Healers will spread out from a point that is a little closer to Magtheridon. I have not built in aoe avoidance (except for cube clickers, see below) because the general avoid aoe strategy seems to work fine for this fight. ### **Clicking Manticron Cubes** Now, the fun part. Bots will be assigned to clicking cubes by standard group selection order (reverse join order), but assignment is done via two passes. The first pass will look to select five ranged DPS bots that are _not_ Warlocks. This is because Warlocks may still be needed to keep Abyssals banished/feared and because Warlocks of all three specs put out by far the most damage of all ranged DPS at level 70 in pre-raid/T4 gear. If there are not five non-Warlock ranged DPS bots available, then the logic goes to the second pass, which can pick any bot that is not a Tank. Cube clicking works on a timer: 1. 48 seconds after Magtheridon breaks free, assigned cube clickers move near their cubes (but a few yards outside of interact distance). During this time, they should move around still to avoid Debris (by maintaining distance from the triggering NPC) and Conflagration (by maintaining distance from the triggering gameobject). Blast Nova is on a 54.35s to 55.4s timer, and I found 48s to always be ample time to get to the cubes, but YMMV so this is a good thing to test. Going to a cube too early not only takes away DPS but also risks more hazards appearing on/around the cube that will then cause problems when the cube needs to be clicked. 2. Blast Nova is a 2s cast, followed by a 10s channel (if not interrupted by the cubes). As soon as the cast begins, bots will move into interaction range and click the cube. Well, there is a randomized delay between 200ms (about the fastest possible human reaction time to visual stimuli) and 1500ms. It didn’t happen to me in a few runs, but it may be possible that the delay causes the raid to eat one tick of Blast Nova (I’m not sure if the first blast comes as soon as the channel starts). Again, another good thing to test, but also one tick is not going to kill anybody, and it’s arguably good to introduce some degree of imperfection. 3. Once Blast Nova stops channeling (i.e., all five cubes have been clicked and channeled simultaneously), bots will interrupt their cube clicking and go back to regularly scheduled activities. Again, I’ve introduced a randomized delay, this time between 200ms and 3000ms. Note that bots can easily be perfect at this—if I don’t do the randomized delay, they click and let go so fast that you can barely even see the beams appear. It’s so atrocious for immersion that I consider the lack of any randomization to be totally unacceptable for this strategy. 4. 48s after Blast Nova, bots will go back to their near-cube positions, rinse and repeat. If an assigned cube clicker dies, another bot should be immediately assigned. All bots in the raid track the same timer so the new bot should step into the prior bot’s shoes. Of course, if Blast Nova is about to go off and a clicker dies next to a cube, you’re probably wiping because I didn’t assign backups to stand in place. That’s too much of a dad guild-level strategy even for me. And that’s about it. Figuring out the cubes was a tremendous pain in the ass, but I’ve really enjoyed the learning process. --------- Co-authored-by: bash <31279994+hermensbas@users.noreply.github.com> --- src/PlayerbotAI.cpp | 2 + src/strategy/AiObjectContext.cpp | 4 + src/strategy/raids/RaidStrategyContext.h | 3 + .../RaidMagtheridonActionContext.h | 37 + .../magtheridon/RaidMagtheridonActions.cpp | 703 ++++++++++++++++++ .../magtheridon/RaidMagtheridonActions.h | 100 +++ .../magtheridon/RaidMagtheridonHelpers.cpp | 226 ++++++ .../magtheridon/RaidMagtheridonHelpers.h | 90 +++ .../RaidMagtheridonMultipliers.cpp | 71 ++ .../magtheridon/RaidMagtheridonMultipliers.h | 27 + .../magtheridon/RaidMagtheridonStrategy.cpp | 42 ++ .../magtheridon/RaidMagtheridonStrategy.h | 18 + .../RaidMagtheridonTriggerContext.h | 37 + .../magtheridon/RaidMagtheridonTriggers.cpp | 128 ++++ .../magtheridon/RaidMagtheridonTriggers.h | 77 ++ 15 files changed, 1565 insertions(+) create mode 100644 src/strategy/raids/magtheridon/RaidMagtheridonActionContext.h create mode 100644 src/strategy/raids/magtheridon/RaidMagtheridonActions.cpp create mode 100644 src/strategy/raids/magtheridon/RaidMagtheridonActions.h create mode 100644 src/strategy/raids/magtheridon/RaidMagtheridonHelpers.cpp create mode 100644 src/strategy/raids/magtheridon/RaidMagtheridonHelpers.h create mode 100644 src/strategy/raids/magtheridon/RaidMagtheridonMultipliers.cpp create mode 100644 src/strategy/raids/magtheridon/RaidMagtheridonMultipliers.h create mode 100644 src/strategy/raids/magtheridon/RaidMagtheridonStrategy.cpp create mode 100644 src/strategy/raids/magtheridon/RaidMagtheridonStrategy.h create mode 100644 src/strategy/raids/magtheridon/RaidMagtheridonTriggerContext.h create mode 100644 src/strategy/raids/magtheridon/RaidMagtheridonTriggers.cpp create mode 100644 src/strategy/raids/magtheridon/RaidMagtheridonTriggers.h diff --git a/src/PlayerbotAI.cpp b/src/PlayerbotAI.cpp index bf6b0a34..48a259e1 100644 --- a/src/PlayerbotAI.cpp +++ b/src/PlayerbotAI.cpp @@ -1534,6 +1534,8 @@ void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) case 533: strategyName = "naxx"; // Naxxramas break; + case 544: + strategyName = "magtheridon"; // Magtheridon's Lair case 565: strategyName = "gruulslair"; // Gruul's Lair break; diff --git a/src/strategy/AiObjectContext.cpp b/src/strategy/AiObjectContext.cpp index 4c49fe4e..7a1da0f8 100644 --- a/src/strategy/AiObjectContext.cpp +++ b/src/strategy/AiObjectContext.cpp @@ -39,6 +39,8 @@ #include "raids/blackwinglair/RaidBwlTriggerContext.h" #include "raids/karazhan/RaidKarazhanActionContext.h" #include "raids/karazhan/RaidKarazhanTriggerContext.h" +#include "raids/magtheridon/RaidMagtheridonActionContext.h" +#include "raids/magtheridon/RaidMagtheridonTriggerContext.h" #include "raids/gruulslair/RaidGruulsLairActionContext.h" #include "raids/gruulslair/RaidGruulsLairTriggerContext.h" #include "raids/naxxramas/RaidNaxxActionContext.h" @@ -113,6 +115,7 @@ void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextList +{ +public: + RaidMagtheridonActionContext() + { + creators["magtheridon main tank attack first three channelers"] = &RaidMagtheridonActionContext::magtheridon_main_tank_attack_first_three_channelers; + creators["magtheridon first assist tank attack nw channeler"] = &RaidMagtheridonActionContext::magtheridon_first_assist_tank_attack_nw_channeler; + creators["magtheridon second assist tank attack ne channeler"] = &RaidMagtheridonActionContext::magtheridon_second_assist_tank_attack_ne_channeler; + creators["magtheridon misdirect hellfire channelers"] = &RaidMagtheridonActionContext::magtheridon_misdirect_hellfire_channelers; + creators["magtheridon assign dps priority"] = &RaidMagtheridonActionContext::magtheridon_assign_dps_priority; + creators["magtheridon warlock cc burning abyssal"] = &RaidMagtheridonActionContext::magtheridon_warlock_cc_burning_abyssal; + creators["magtheridon main tank position boss"] = &RaidMagtheridonActionContext::magtheridon_main_tank_position_boss; + creators["magtheridon spread ranged"] = &RaidMagtheridonActionContext::magtheridon_spread_ranged; + creators["magtheridon use manticron cube"] = &RaidMagtheridonActionContext::magtheridon_use_manticron_cube; + creators["magtheridon manage timers and assignments"] = &RaidMagtheridonActionContext::magtheridon_manage_timers_and_assignments; + } + +private: + static Action* magtheridon_main_tank_attack_first_three_channelers(PlayerbotAI* botAI) { return new MagtheridonMainTankAttackFirstThreeChannelersAction(botAI); } + static Action* magtheridon_first_assist_tank_attack_nw_channeler(PlayerbotAI* botAI) { return new MagtheridonFirstAssistTankAttackNWChannelerAction(botAI); } + static Action* magtheridon_second_assist_tank_attack_ne_channeler(PlayerbotAI* botAI) { return new MagtheridonSecondAssistTankAttackNEChannelerAction(botAI); } + static Action* magtheridon_misdirect_hellfire_channelers(PlayerbotAI* botAI) { return new MagtheridonMisdirectHellfireChannelers(botAI); } + static Action* magtheridon_assign_dps_priority(PlayerbotAI* botAI) { return new MagtheridonAssignDPSPriorityAction(botAI); } + static Action* magtheridon_warlock_cc_burning_abyssal(PlayerbotAI* botAI) { return new MagtheridonWarlockCCBurningAbyssalAction(botAI); } + static Action* magtheridon_main_tank_position_boss(PlayerbotAI* botAI) { return new MagtheridonMainTankPositionBossAction(botAI); } + static Action* magtheridon_spread_ranged(PlayerbotAI* botAI) { return new MagtheridonSpreadRangedAction(botAI); } + static Action* magtheridon_use_manticron_cube(PlayerbotAI* botAI) { return new MagtheridonUseManticronCubeAction(botAI); } + static Action* magtheridon_manage_timers_and_assignments(PlayerbotAI* botAI) { return new MagtheridonManageTimersAndAssignmentsAction(botAI); } +}; + +#endif diff --git a/src/strategy/raids/magtheridon/RaidMagtheridonActions.cpp b/src/strategy/raids/magtheridon/RaidMagtheridonActions.cpp new file mode 100644 index 00000000..8efc0e06 --- /dev/null +++ b/src/strategy/raids/magtheridon/RaidMagtheridonActions.cpp @@ -0,0 +1,703 @@ +#include "RaidMagtheridonActions.h" +#include "RaidMagtheridonHelpers.h" +#include "Creature.h" +#include "ObjectAccessor.h" +#include "ObjectGuid.h" +#include "Playerbots.h" + +using namespace MagtheridonHelpers; + +bool MagtheridonMainTankAttackFirstThreeChannelersAction::Execute(Event event) +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + if (!magtheridon) + return false; + + Creature* channelerSquare = GetChanneler(bot, SOUTH_CHANNELER); + if (channelerSquare && channelerSquare->IsAlive()) + MarkTargetWithSquare(bot, channelerSquare); + + Creature* channelerStar = GetChanneler(bot, WEST_CHANNELER); + if (channelerStar && channelerStar->IsAlive()) + MarkTargetWithStar(bot, channelerStar); + + Creature* channelerCircle = GetChanneler(bot, EAST_CHANNELER); + if (channelerCircle && channelerCircle->IsAlive()) + MarkTargetWithCircle(bot, channelerCircle); + + // After first three channelers are dead, wait for Magtheridon to activate + if ((!channelerSquare || !channelerSquare->IsAlive()) && + (!channelerStar || !channelerStar->IsAlive()) && + (!channelerCircle || !channelerCircle->IsAlive())) + { + const Location& position = MagtheridonsLairLocations::WaitingForMagtheridonPosition; + if (!bot->IsWithinDist2d(position.x, position.y, 2.0f)) + { + return MoveTo(bot->GetMapId(), position.x, position.y, position.z, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + bot->SetFacingTo(position.orientation); + return true; + } + + Creature* currentTarget = nullptr; + std::string rtiName; + if (channelerSquare && channelerSquare->IsAlive()) + { + currentTarget = channelerSquare; + rtiName = "square"; + } + else if (channelerStar && channelerStar->IsAlive()) + { + currentTarget = channelerStar; + rtiName = "star"; + } + else if (channelerCircle && channelerCircle->IsAlive()) + { + currentTarget = channelerCircle; + rtiName = "circle"; + } + + SetRtiTarget(botAI, rtiName, currentTarget); + + if (currentTarget && bot->GetVictim() != currentTarget) + return Attack(currentTarget); + + return false; +} + +bool MagtheridonFirstAssistTankAttackNWChannelerAction::Execute(Event event) +{ + Creature* channelerDiamond = GetChanneler(bot, NORTHWEST_CHANNELER); + if (!channelerDiamond || !channelerDiamond->IsAlive()) + return false; + + MarkTargetWithDiamond(bot, channelerDiamond); + SetRtiTarget(botAI, "diamond", channelerDiamond); + + if (bot->GetVictim() != channelerDiamond) + return Attack(channelerDiamond); + + if (channelerDiamond->GetVictim() == bot) + { + const Location& position = MagtheridonsLairLocations::NWChannelerTankPosition; + const float maxDistance = 3.0f; + + if (bot->GetExactDist2d(position.x, position.y) > maxDistance) + { + float dX = position.x - bot->GetPositionX(); + float dY = position.y - bot->GetPositionY(); + float dist = sqrt(dX * dX + dY * dY); + float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; + float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; + + return MoveTo(bot->GetMapId(), moveX, moveY, position.z, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +bool MagtheridonSecondAssistTankAttackNEChannelerAction::Execute(Event event) +{ + Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); + if (!channelerTriangle || !channelerTriangle->IsAlive()) + return false; + + MarkTargetWithTriangle(bot, channelerTriangle); + SetRtiTarget(botAI, "triangle", channelerTriangle); + + if (bot->GetVictim() != channelerTriangle) + return Attack(channelerTriangle); + + if (channelerTriangle->GetVictim() == bot) + { + const Location& position = MagtheridonsLairLocations::NEChannelerTankPosition; + const float maxDistance = 3.0f; + + if (bot->GetExactDist2d(position.x, position.y) > maxDistance) + { + float dX = position.x - bot->GetPositionX(); + float dY = position.y - bot->GetPositionY(); + float dist = sqrt(dX * dX + dY * dY); + float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; + float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; + + return MoveTo(bot->GetMapId(), moveX, moveY, position.z, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +// Misdirect West & East Channelers to Main Tank +bool MagtheridonMisdirectHellfireChannelers::Execute(Event event) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector hunters; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && member->getClass() == CLASS_HUNTER && GET_PLAYERBOT_AI(member)) + hunters.push_back(member); + } + + int hunterIndex = -1; + for (size_t i = 0; i < hunters.size(); ++i) + { + if (hunters[i] == bot) + { + hunterIndex = static_cast(i); + break; + } + } + + Player* mainTank = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + + Creature* channelerStar = GetChanneler(bot, WEST_CHANNELER); + Creature* channelerCircle = GetChanneler(bot, EAST_CHANNELER); + + switch (hunterIndex) + { + case 0: + if (mainTank && channelerStar && channelerStar->IsAlive() && + channelerStar->GetVictim() != mainTank) + { + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (!bot->HasAura(SPELL_MISDIRECTION)) + return false; + + if (botAI->CanCastSpell("steady shot", channelerStar)) + return botAI->CastSpell("steady shot", channelerStar); + } + break; + + case 1: + if (mainTank && channelerCircle && channelerCircle->IsAlive() && + channelerCircle->GetVictim() != mainTank) + { + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (!bot->HasAura(SPELL_MISDIRECTION)) + return false; + + if (botAI->CanCastSpell("steady shot", channelerCircle)) + return botAI->CastSpell("steady shot", channelerCircle); + } + break; + + default: + break; + } + + return false; +} + +bool MagtheridonAssignDPSPriorityAction::Execute(Event event) +{ + // Listed in order of priority + Creature* channelerSquare = GetChanneler(bot, SOUTH_CHANNELER); + if (channelerSquare && channelerSquare->IsAlive()) + { + SetRtiTarget(botAI, "square", channelerSquare); + + if (bot->GetTarget() != channelerSquare->GetGUID()) + { + bot->SetSelection(channelerSquare->GetGUID()); + return Attack(channelerSquare); + } + + return false; + } + + Creature* channelerStar = GetChanneler(bot, WEST_CHANNELER); + if (channelerStar && channelerStar->IsAlive()) + { + SetRtiTarget(botAI, "star", channelerStar); + + if (bot->GetTarget() != channelerStar->GetGUID()) + { + bot->SetSelection(channelerStar->GetGUID()); + return Attack(channelerStar); + } + + return false; + } + + Creature* channelerCircle = GetChanneler(bot, EAST_CHANNELER); + if (channelerCircle && channelerCircle->IsAlive()) + { + SetRtiTarget(botAI, "circle", channelerCircle); + + if (bot->GetTarget() != channelerCircle->GetGUID()) + { + bot->SetSelection(channelerCircle->GetGUID()); + return Attack(channelerCircle); + } + + return false; + } + + Creature* channelerDiamond = GetChanneler(bot, NORTHWEST_CHANNELER); + if (channelerDiamond && channelerDiamond->IsAlive()) + { + SetRtiTarget(botAI, "diamond", channelerDiamond); + + if (bot->GetTarget() != channelerDiamond->GetGUID()) + { + bot->SetSelection(channelerDiamond->GetGUID()); + return Attack(channelerDiamond); + } + + return false; + } + + Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); + if (channelerTriangle && channelerTriangle->IsAlive()) + { + SetRtiTarget(botAI, "triangle", channelerTriangle); + + if (bot->GetTarget() != channelerTriangle->GetGUID()) + { + bot->SetSelection(channelerTriangle->GetGUID()); + return Attack(channelerTriangle); + } + + return false; + } + + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + if (magtheridon && !magtheridon->HasAura(SPELL_SHADOW_CAGE) && + (!channelerSquare || !channelerSquare->IsAlive()) && + (!channelerStar || !channelerStar->IsAlive()) && + (!channelerCircle || !channelerCircle->IsAlive()) && + (!channelerDiamond || !channelerDiamond->IsAlive()) && + (!channelerTriangle || !channelerTriangle->IsAlive())) + { + SetRtiTarget(botAI, "cross", magtheridon); + + if (bot->GetTarget() != magtheridon->GetGUID()) + { + bot->SetSelection(magtheridon->GetGUID()); + return Attack(magtheridon); + } + } + + return false; +} + +// Assign Burning Abyssals to Warlocks to Banish +// Burning Abyssals in excess of Warlocks in party will be Feared +bool MagtheridonWarlockCCBurningAbyssalAction::Execute(Event event) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + const GuidVector& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + std::vector abyssals; + for (auto const& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (unit && unit->GetEntry() == NPC_BURNING_ABYSSAL && unit->IsAlive()) + abyssals.push_back(unit); + } + + std::vector warlocks; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && member->getClass() == CLASS_WARLOCK && GET_PLAYERBOT_AI(member)) + warlocks.push_back(member); + } + + int warlockIndex = -1; + for (size_t i = 0; i < warlocks.size(); ++i) + { + if (warlocks[i] == bot) + { + warlockIndex = static_cast(i); + break; + } + } + + if (warlockIndex >= 0 && warlockIndex < abyssals.size()) + { + Unit* assignedAbyssal = abyssals[warlockIndex]; + if (!assignedAbyssal->HasAura(SPELL_BANISH) && botAI->CanCastSpell(SPELL_BANISH, assignedAbyssal, true)) + return botAI->CastSpell("banish", assignedAbyssal); + } + + for (size_t i = warlocks.size(); i < abyssals.size(); ++i) + { + Unit* excessAbyssal = abyssals[i]; + if (!excessAbyssal->HasAura(SPELL_BANISH) && !excessAbyssal->HasAura(SPELL_FEAR) && + botAI->CanCastSpell(SPELL_FEAR, excessAbyssal, true)) + return botAI->CastSpell("fear", excessAbyssal); + } + + return false; +} + +// Main tank will back up to the Northern point of the room +bool MagtheridonMainTankPositionBossAction::Execute(Event event) +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + if (!magtheridon) + return false; + + MarkTargetWithCross(bot, magtheridon); + SetRtiTarget(botAI, "cross", magtheridon); + + if (bot->GetVictim() != magtheridon) + return Attack(magtheridon); + + if (magtheridon->GetVictim() == bot) + { + const Location& position = MagtheridonsLairLocations::MagtheridonTankPosition; + const float maxDistance = 2.0f; + + if (bot->GetExactDist2d(position.x, position.y) > maxDistance) + { + float dX = position.x - bot->GetPositionX(); + float dY = position.y - bot->GetPositionY(); + float dist = sqrt(dX * dX + dY * dY); + float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; + float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; + + return MoveTo(bot->GetMapId(), moveX, moveY, position.z, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, true); + } + + bot->SetFacingTo(position.orientation); + } + + return false; +} + +// Ranged DPS will remain within 25 yards of the center of the room +// Healers will remain within 15 yards of a position that is between ranged DPS and the boss +std::unordered_map MagtheridonSpreadRangedAction::initialPositions; +std::unordered_map MagtheridonSpreadRangedAction::hasReachedInitialPosition; + +bool MagtheridonSpreadRangedAction::Execute(Event event) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + // Wait for 6 seconds after Magtheridon activates to spread + const uint8 spreadWaitSeconds = 6; + auto it = magtheridonSpreadWaitTimer.find(bot->GetMapId()); + if (it == magtheridonSpreadWaitTimer.end() || + (time(nullptr) - it->second) < spreadWaitSeconds) + return false; + + auto cubeIt = botToCubeAssignment.find(bot->GetGUID()); + if (cubeIt != botToCubeAssignment.end()) + { + time_t now = time(nullptr); + auto timerIt = magtheridonBlastNovaTimer.find(bot->GetMapId()); + if (timerIt != magtheridonBlastNovaTimer.end()) + { + time_t lastBlastNova = timerIt->second; + if (now - lastBlastNova >= 49) + return false; + } + } + + std::vector members; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive()) + members.push_back(member); + } + + bool isHealer = botAI->IsHeal(bot); + const Location& center = isHealer + ? MagtheridonsLairLocations::HealerSpreadPosition + : MagtheridonsLairLocations::RangedSpreadPosition; + float maxSpreadRadius = isHealer ? 15.0f : 20.0f; + float centerX = center.x; + float centerY = center.y; + float centerZ = bot->GetPositionZ(); + const float radiusBuffer = 3.0f; + + if (!initialPositions.count(bot->GetGUID())) + { + auto it = std::find(members.begin(), members.end(), bot); + uint8 botIndex = (it != members.end()) ? std::distance(members.begin(), it) : 0; + uint8 count = members.size(); + + float angle = 2 * M_PI * botIndex / count; + float radius = static_cast(rand()) / RAND_MAX * maxSpreadRadius; + float targetX = centerX + radius * cos(angle); + float targetY = centerY + radius * sin(angle); + + initialPositions[bot->GetGUID()] = Position(targetX, targetY, centerZ); + hasReachedInitialPosition[bot->GetGUID()] = false; + } + + Position targetPosition = initialPositions[bot->GetGUID()]; + if (!hasReachedInitialPosition[bot->GetGUID()]) + { + if (!bot->IsWithinDist2d(targetPosition.GetPositionX(), targetPosition.GetPositionY(), 2.0f)) + { + float destX = targetPosition.GetPositionX(); + float destY = targetPosition.GetPositionY(); + float destZ = targetPosition.GetPositionZ(); + + if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), + bot->GetPositionY(), bot->GetPositionZ(), destX, destY, destZ)) + return false; + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(false); + return MoveTo(bot->GetMapId(), destX, destY, destZ, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + hasReachedInitialPosition[bot->GetGUID()] = true; + } + + float distToCenter = bot->GetExactDist2d(centerX, centerY); + + if (distToCenter > maxSpreadRadius + radiusBuffer) + { + float angle = static_cast(rand()) / RAND_MAX * 2.0f * M_PI; + float radius = static_cast(rand()) / RAND_MAX * maxSpreadRadius; + float targetX = centerX + radius * cos(angle); + float targetY = centerY + radius * sin(angle); + + if (bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(), + bot->GetPositionZ(), targetX, targetY, centerZ)) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(false); + return MoveTo(bot->GetMapId(), targetX, targetY, centerZ, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +// For bots that are assigned to click cubes +// Magtheridon casts Blast Nova every 54.35 to 55.40s, with a 2s cast time +bool MagtheridonUseManticronCubeAction::Execute(Event event) +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + if (!magtheridon) + return false; + + auto it = botToCubeAssignment.find(bot->GetGUID()); + const CubeInfo& cubeInfo = it->second; + GameObject* cube = botAI->GetGameObject(cubeInfo.guid); + if (!cube) + return false; + + // Release cubes after Blast Nova is interrupted + if (HandleCubeRelease(magtheridon, cube)) + return true; + + // Check if cube logic should be active (49+ second rule) + if (!ShouldActivateCubeLogic(magtheridon)) + return false; + + // Handle active cube logic based on Blast Nova casting state + bool blastNovaActive = magtheridon->HasUnitState(UNIT_STATE_CASTING) && + magtheridon->FindCurrentSpellBySpellId(SPELL_BLAST_NOVA); + + if (!blastNovaActive) + // After 49 seconds, wait at safe distance from cube + return HandleWaitingPhase(cubeInfo); + else + // Blast Nova is casting - move to and click cube + return HandleCubeInteraction(cubeInfo, cube); + + return false; +} + +bool MagtheridonUseManticronCubeAction::HandleCubeRelease(Unit* magtheridon, GameObject* cube) +{ + if (bot->HasAura(SPELL_SHADOW_GRASP) && + !(magtheridon->HasUnitState(UNIT_STATE_CASTING) && + magtheridon->FindCurrentSpellBySpellId(SPELL_BLAST_NOVA))) + { + uint32 delay = urand(200, 3000); + botAI->AddTimedEvent( + [this] + { + botAI->Reset(); + }, + delay); + botAI->SetNextCheckDelay(delay + 50); + return true; + } + + return false; +} + +bool MagtheridonUseManticronCubeAction::ShouldActivateCubeLogic(Unit* magtheridon) +{ + auto timerIt = magtheridonBlastNovaTimer.find(bot->GetMapId()); + if (timerIt == magtheridonBlastNovaTimer.end()) + return false; + + time_t now = time(nullptr); + time_t lastBlastNova = timerIt->second; + + return (now - lastBlastNova >= 49); +} + +bool MagtheridonUseManticronCubeAction::HandleWaitingPhase(const CubeInfo& cubeInfo) +{ + const float safeWaitDistance = 8.0f; + float cubeDist = bot->GetExactDist2d(cubeInfo.x, cubeInfo.y); + + if (fabs(cubeDist - safeWaitDistance) > 1.0f) + { + for (int i = 0; i < 12; ++i) + { + float angle = i * M_PI / 6.0f; + float targetX = cubeInfo.x + cos(angle) * safeWaitDistance; + float targetY = cubeInfo.y + sin(angle) * safeWaitDistance; + float targetZ = bot->GetPositionZ(); + + if (IsSafeFromMagtheridonHazards(botAI, bot, targetX, targetY, targetZ)) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + float angle = static_cast(rand()) / RAND_MAX * 2.0f * M_PI; + float fallbackX = cubeInfo.x + cos(angle) * safeWaitDistance; + float fallbackY = cubeInfo.y + sin(angle) * safeWaitDistance; + float fallbackZ = bot->GetPositionZ(); + + return MoveTo(bot->GetMapId(), fallbackX, fallbackY, fallbackZ, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return true; +} + +bool MagtheridonUseManticronCubeAction::HandleCubeInteraction(const CubeInfo& cubeInfo, GameObject* cube) +{ + const float interactDistance = 1.0f; + float cubeDist = bot->GetExactDist2d(cubeInfo.x, cubeInfo.y); + + if (cubeDist > interactDistance) + { + if (cubeDist <= interactDistance + 1.0f) + { + uint32 delay = urand(200, 1500); + botAI->AddTimedEvent( + [this, cube] + { + bot->StopMoving(); + cube->Use(bot); + }, + delay); + botAI->SetNextCheckDelay(delay + 50); + return true; + } + + float angle = atan2(cubeInfo.y - bot->GetPositionY(), cubeInfo.x - bot->GetPositionX()); + float targetX = cubeInfo.x - cos(angle) * interactDistance; + float targetY = cubeInfo.y - sin(angle) * interactDistance; + float targetZ = bot->GetPositionZ(); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +// The Blast Nova timer resets when Magtheridon stops casting it, which is needed to ensure that bots use cubes. +// However, Magtheridon's Blast Nova cooldown actually runs from when he starts casting it. This means that if a Blast Nova +// is not interrupted or takes too long to interrupt, the timer will be thrown off for the rest of the encounter. +// Correcting this issue is complicated and probably would need some rewriting--I have not done so and +// and view the current solution as sufficient since in TBC a missed Blast Nova would be a guaranteed wipe anyway. +bool MagtheridonManageTimersAndAssignmentsAction::Execute(Event event) +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + if (!magtheridon) + return false; + + uint32 mapId = magtheridon->GetMapId(); + + bool blastNovaActive = magtheridon->HasUnitState(UNIT_STATE_CASTING) && + magtheridon->FindCurrentSpellBySpellId(SPELL_BLAST_NOVA); + bool lastBlastNova = lastBlastNovaState[mapId]; + + if (lastBlastNova && !blastNovaActive && IsMapIDTimerManager(botAI, bot)) + magtheridonBlastNovaTimer[mapId] = time(nullptr); + lastBlastNovaState[mapId] = blastNovaActive; + + if (IsMapIDTimerManager(botAI, bot)) + { + if (!magtheridon->HasAura(SPELL_SHADOW_CAGE)) + { + if (magtheridonSpreadWaitTimer.find(mapId) == magtheridonSpreadWaitTimer.end()) + magtheridonSpreadWaitTimer[mapId] = time(nullptr); + + if (magtheridonBlastNovaTimer.find(mapId) == magtheridonBlastNovaTimer.end()) + magtheridonBlastNovaTimer[mapId] = time(nullptr); + + if (magtheridonAggroWaitTimer.find(mapId) == magtheridonAggroWaitTimer.end()) + magtheridonAggroWaitTimer[mapId] = time(nullptr); + } + } + + if (magtheridon->HasAura(SPELL_SHADOW_CAGE)) + { + if (!MagtheridonSpreadRangedAction::initialPositions.empty()) + MagtheridonSpreadRangedAction::initialPositions.clear(); + + if (!MagtheridonSpreadRangedAction::hasReachedInitialPosition.empty()) + MagtheridonSpreadRangedAction::hasReachedInitialPosition.clear(); + + if (!botToCubeAssignment.empty()) + botToCubeAssignment.clear(); + + if (IsMapIDTimerManager(botAI, bot)) + { + if (magtheridonSpreadWaitTimer.find(mapId) != magtheridonSpreadWaitTimer.end()) + magtheridonSpreadWaitTimer.erase(mapId); + + if (magtheridonBlastNovaTimer.find(mapId) != magtheridonBlastNovaTimer.end()) + magtheridonBlastNovaTimer.erase(mapId); + + if (magtheridonAggroWaitTimer.find(mapId) != magtheridonAggroWaitTimer.end()) + magtheridonAggroWaitTimer.erase(mapId); + } + } + + return false; +} diff --git a/src/strategy/raids/magtheridon/RaidMagtheridonActions.h b/src/strategy/raids/magtheridon/RaidMagtheridonActions.h new file mode 100644 index 00000000..6c4ed84c --- /dev/null +++ b/src/strategy/raids/magtheridon/RaidMagtheridonActions.h @@ -0,0 +1,100 @@ +#ifndef _PLAYERBOT_RAIDMAGTHERIDONACTIONS_H +#define _PLAYERBOT_RAIDMAGTHERIDONACTIONS_H + +#include "RaidMagtheridonHelpers.h" +#include "Action.h" +#include "AttackAction.h" +#include "MovementActions.h" + +using namespace MagtheridonHelpers; + +class MagtheridonMainTankAttackFirstThreeChannelersAction : public AttackAction +{ +public: + MagtheridonMainTankAttackFirstThreeChannelersAction(PlayerbotAI* botAI, std::string const name = "magtheridon main tank attack first three channelers") : AttackAction(botAI, name) {}; + + bool Execute(Event event) override; +}; + +class MagtheridonFirstAssistTankAttackNWChannelerAction : public AttackAction +{ +public: + MagtheridonFirstAssistTankAttackNWChannelerAction(PlayerbotAI* botAI, std::string const name = "magtheridon first assist tank attack nw channeler") : AttackAction(botAI, name) {}; + + bool Execute(Event event) override; +}; + +class MagtheridonSecondAssistTankAttackNEChannelerAction : public AttackAction +{ +public: + MagtheridonSecondAssistTankAttackNEChannelerAction(PlayerbotAI* botAI, std::string const name = "magtheridon second assist tank attack ne channeler") : AttackAction(botAI, name) {}; + + bool Execute(Event event) override; +}; + +class MagtheridonMisdirectHellfireChannelers : public AttackAction +{ +public: + MagtheridonMisdirectHellfireChannelers(PlayerbotAI* botAI, std::string const name = "magtheridon misdirect hellfire channelers") : AttackAction(botAI, name) {}; + + bool Execute(Event event) override; +}; + +class MagtheridonAssignDPSPriorityAction : public AttackAction +{ +public: + MagtheridonAssignDPSPriorityAction(PlayerbotAI* botAI, std::string const name = "magtheridon assign dps priority") : AttackAction(botAI, name) {}; + + bool Execute(Event event) override; +}; + +class MagtheridonWarlockCCBurningAbyssalAction : public AttackAction +{ +public: + MagtheridonWarlockCCBurningAbyssalAction(PlayerbotAI* botAI, std::string const name = "magtheridon warlock cc burning abyssal") : AttackAction(botAI, name) {}; + + bool Execute(Event event) override; +}; + +class MagtheridonMainTankPositionBossAction : public AttackAction +{ +public: + MagtheridonMainTankPositionBossAction(PlayerbotAI* botAI, std::string const name = "magtheridon main tank position boss") : AttackAction(botAI, name) {}; + + bool Execute(Event event) override; +}; + +class MagtheridonSpreadRangedAction : public MovementAction +{ +public: + static std::unordered_map initialPositions; + static std::unordered_map hasReachedInitialPosition; + + MagtheridonSpreadRangedAction(PlayerbotAI* botAI, std::string const name = "magtheridon spread ranged") : MovementAction(botAI, name) {}; + + bool Execute(Event event) override; +}; + +class MagtheridonUseManticronCubeAction : public MovementAction +{ +public: + MagtheridonUseManticronCubeAction(PlayerbotAI* botAI, std::string const name = "magtheridon use manticron cube") : MovementAction(botAI, name) {}; + + bool Execute(Event event) override; + +private: + bool HandleCubeRelease(Unit* magtheridon, GameObject* cube); + bool ShouldActivateCubeLogic(Unit* magtheridon); + bool HandleWaitingPhase(const CubeInfo& cubeInfo); + bool HandleCubeInteraction(const CubeInfo& cubeInfo, GameObject* cube); +}; + +class MagtheridonManageTimersAndAssignmentsAction : public Action +{ +public: + MagtheridonManageTimersAndAssignmentsAction(PlayerbotAI* botAI, std::string const name = "magtheridon manage timers and assignments") : Action(botAI, name) {}; + + bool Execute(Event event) override; +}; + +#endif diff --git a/src/strategy/raids/magtheridon/RaidMagtheridonHelpers.cpp b/src/strategy/raids/magtheridon/RaidMagtheridonHelpers.cpp new file mode 100644 index 00000000..8a0d693c --- /dev/null +++ b/src/strategy/raids/magtheridon/RaidMagtheridonHelpers.cpp @@ -0,0 +1,226 @@ +#include "RaidMagtheridonHelpers.h" +#include "Creature.h" +#include "GameObject.h" +#include "GroupReference.h" +#include "Map.h" +#include "ObjectGuid.h" +#include "Playerbots.h" + +namespace MagtheridonHelpers +{ + namespace MagtheridonsLairLocations + { + const Location WaitingForMagtheridonPosition = { 1.359f, 2.048f, -0.406f, 3.135f }; + const Location MagtheridonTankPosition = { 22.827f, 2.105f, -0.406f, 3.135f }; + const Location NWChannelerTankPosition = { -11.764f, 30.818f, -0.411f, 0.0f }; + const Location NEChannelerTankPosition = { -12.490f, -26.211f, -0.411f, 0.0f }; + const Location RangedSpreadPosition = { -14.890f, 1.995f, -0.406f, 0.0f }; + const Location HealerSpreadPosition = { -2.265f, 1.874f, -0.404f, 0.0f }; + } + + // Identify channelers by their database GUIDs + Creature* GetChanneler(Player* bot, uint32 dbGuid) + { + Map* map = bot->GetMap(); + if (!map) + return nullptr; + + auto it = map->GetCreatureBySpawnIdStore().find(dbGuid); + if (it == map->GetCreatureBySpawnIdStore().end()) + return nullptr; + + return it->second; + } + + void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId) + { + Group* group = bot->GetGroup(); + if (!target || !group) + return; + + ObjectGuid currentGuid = group->GetTargetIcon(iconId); + if (currentGuid != target->GetGUID()) + group->SetTargetIcon(iconId, bot->GetGUID(), target->GetGUID()); + } + + void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target) + { + if (!target) + return; + + std::string currentRti = botAI->GetAiObjectContext()->GetValue("rti")->Get(); + Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("rti target")->Get(); + + if (currentRti != rtiName || currentTarget != target) + { + botAI->GetAiObjectContext()->GetValue("rti")->Set(rtiName); + botAI->GetAiObjectContext()->GetValue("rti target")->Set(target); + } + } + + void MarkTargetWithSquare(Player* bot, Unit* target) + { + MarkTargetWithIcon(bot, target, RtiTargetValue::squareIndex); + } + + void MarkTargetWithStar(Player* bot, Unit* target) + { + MarkTargetWithIcon(bot, target, RtiTargetValue::starIndex); + } + + void MarkTargetWithCircle(Player* bot, Unit* target) + { + MarkTargetWithIcon(bot, target, RtiTargetValue::circleIndex); + } + + void MarkTargetWithDiamond(Player* bot, Unit* target) + { + MarkTargetWithIcon(bot, target, RtiTargetValue::diamondIndex); + } + + void MarkTargetWithTriangle(Player* bot, Unit* target) + { + MarkTargetWithIcon(bot, target, RtiTargetValue::triangleIndex); + } + + void MarkTargetWithCross(Player* bot, Unit* target) + { + MarkTargetWithIcon(bot, target, RtiTargetValue::crossIndex); + } + + const std::vector MANTICRON_CUBE_DB_GUIDS = { 43157, 43158, 43159, 43160, 43161 }; + + // Get the positions of all Manticron Cubes by their database GUIDs + std::vector GetAllCubeInfosByDbGuids(Map* map, const std::vector& cubeDbGuids) + { + std::vector cubes; + if (!map) + return cubes; + + for (uint32 dbGuid : cubeDbGuids) + { + auto bounds = map->GetGameObjectBySpawnIdStore().equal_range(dbGuid); + if (bounds.first == bounds.second) + continue; + + GameObject* go = bounds.first->second; + if (!go) + continue; + + CubeInfo info; + info.guid = go->GetGUID(); + info.x = go->GetPositionX(); + info.y = go->GetPositionY(); + info.z = go->GetPositionZ(); + cubes.push_back(info); + } + + return cubes; + } + + std::unordered_map botToCubeAssignment; + + void AssignBotsToCubesByGuidAndCoords(Group* group, const std::vector& cubes, PlayerbotAI* botAI) + { + botToCubeAssignment.clear(); + if (!group) + return; + + size_t cubeIndex = 0; + std::vector candidates; + + // Assign ranged DPS (excluding Warlocks) to cubes first + for (GroupReference* ref = group->GetFirstMember(); ref && cubeIndex < cubes.size(); ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !botAI->IsRangedDps(member, true) || + member->getClass() == CLASS_WARLOCK || !GET_PLAYERBOT_AI(member)) + continue; + + candidates.push_back(member); + if (candidates.size() >= cubes.size()) + break; + } + + // If there are still cubes left, assign any other non-tank bots + if (candidates.size() < cubes.size()) + { + for (GroupReference* ref = group->GetFirstMember(); + ref && candidates.size() < cubes.size(); ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || botAI->IsTank(member)) + continue; + + if (std::find(candidates.begin(), candidates.end(), member) == candidates.end()) + candidates.push_back(member); + } + } + + for (Player* member : candidates) + { + if (cubeIndex >= cubes.size()) + break; + + if (!member || !member->IsAlive()) + continue; + + botToCubeAssignment[member->GetGUID()] = cubes[cubeIndex++]; + } + } + + std::unordered_map lastBlastNovaState; + std::unordered_map magtheridonBlastNovaTimer; + std::unordered_map magtheridonSpreadWaitTimer; + std::unordered_map magtheridonAggroWaitTimer; + + bool IsSafeFromMagtheridonHazards(PlayerbotAI* botAI, Player* bot, float x, float y, float z) + { + // Debris + std::vector debrisHazards; + const GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); + for (auto const& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (!unit || unit->GetEntry() != NPC_TARGET_TRIGGER) + continue; + debrisHazards.push_back(unit); + } + for (Unit* hazard : debrisHazards) + { + float dist = std::sqrt(std::pow(x - hazard->GetPositionX(), 2) + std::pow(y - hazard->GetPositionY(), 2)); + if (dist < 9.0f) + return false; + } + + // Conflagration + GuidVector gos = *botAI->GetAiObjectContext()->GetValue("nearest game objects"); + for (auto const& goGuid : gos) + { + GameObject* go = botAI->GetGameObject(goGuid); + if (!go || go->GetEntry() != GO_BLAZE) + continue; + + float dist = std::sqrt(std::pow(x - go->GetPositionX(), 2) + std::pow(y - go->GetPositionY(), 2)); + if (dist < 5.0f) + return false; + } + + return true; + } + + bool IsMapIDTimerManager(PlayerbotAI* botAI, Player* bot) + { + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && !botAI->IsMainTank(member) && GET_PLAYERBOT_AI(member)) + return member == bot; + } + } + + return true; + } +} diff --git a/src/strategy/raids/magtheridon/RaidMagtheridonHelpers.h b/src/strategy/raids/magtheridon/RaidMagtheridonHelpers.h new file mode 100644 index 00000000..21fd9d45 --- /dev/null +++ b/src/strategy/raids/magtheridon/RaidMagtheridonHelpers.h @@ -0,0 +1,90 @@ +#ifndef _PLAYERBOT_RAIDMAGTHERIDONHELPERS_H +#define _PLAYERBOT_RAIDMAGTHERIDONHELPERS_H + +#include +#include +#include + +#include "Group.h" +#include "ObjectGuid.h" +#include "PlayerbotAI.h" +#include "RtiTargetValue.h" + +namespace MagtheridonHelpers +{ + enum MagtheridonSpells + { + // Magtheridon + SPELL_SHADOW_CAGE = 30205, + SPELL_BLAST_NOVA = 30616, + SPELL_SHADOW_GRASP = 30410, + + // Warlock + SPELL_BANISH = 18647, + SPELL_FEAR = 6215, + + // Hunter + SPELL_MISDIRECTION = 34477, + }; + + enum MagtheridonNPCs + { + NPC_BURNING_ABYSSAL = 17454, + NPC_TARGET_TRIGGER = 17474, + }; + + enum MagtheridonObjects + { + GO_BLAZE = 181832, + }; + + constexpr uint32 SOUTH_CHANNELER = 90978; + constexpr uint32 WEST_CHANNELER = 90979; + constexpr uint32 NORTHWEST_CHANNELER = 90980; + constexpr uint32 EAST_CHANNELER = 90982; + constexpr uint32 NORTHEAST_CHANNELER = 90981; + + Creature* GetChanneler(Player* bot, uint32 dbGuid); + void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId); + void MarkTargetWithSquare(Player* bot, Unit* target); + void MarkTargetWithStar(Player* bot, Unit* target); + void MarkTargetWithCircle(Player* bot, Unit* target); + void MarkTargetWithDiamond(Player* bot, Unit* target); + void MarkTargetWithTriangle(Player* bot, Unit* target); + void MarkTargetWithCross(Player* bot, Unit* target); + void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target); + bool IsSafeFromMagtheridonHazards(PlayerbotAI* botAI, Player* bot, float x, float y, float z); + bool IsMapIDTimerManager(PlayerbotAI* botAI, Player* bot); + + struct Location + { + float x, y, z, orientation; + }; + + namespace MagtheridonsLairLocations + { + extern const Location WaitingForMagtheridonPosition; + extern const Location MagtheridonTankPosition; + extern const Location NWChannelerTankPosition; + extern const Location NEChannelerTankPosition; + extern const Location RangedSpreadPosition; + extern const Location HealerSpreadPosition; + } + + struct CubeInfo + { + ObjectGuid guid; + float x, y, z; + }; + + extern const std::vector MANTICRON_CUBE_DB_GUIDS; + extern std::unordered_map botToCubeAssignment; + std::vector GetAllCubeInfosByDbGuids(Map* map, const std::vector& cubeDbGuids); + void AssignBotsToCubesByGuidAndCoords(Group* group, const std::vector& cubes, PlayerbotAI* botAI); + extern std::unordered_map lastBlastNovaState; + extern std::unordered_map magtheridonBlastNovaTimer; + extern std::unordered_map magtheridonSpreadWaitTimer; + extern std::unordered_map magtheridonAggroWaitTimer; +} + +#endif diff --git a/src/strategy/raids/magtheridon/RaidMagtheridonMultipliers.cpp b/src/strategy/raids/magtheridon/RaidMagtheridonMultipliers.cpp new file mode 100644 index 00000000..c32ce152 --- /dev/null +++ b/src/strategy/raids/magtheridon/RaidMagtheridonMultipliers.cpp @@ -0,0 +1,71 @@ +#include +#include + +#include "RaidMagtheridonMultipliers.h" +#include "RaidMagtheridonActions.h" +#include "RaidMagtheridonHelpers.h" +#include "ChooseTargetActions.h" +#include "GenericSpellActions.h" +#include "Playerbots.h" +#include "WarlockActions.h" + +using namespace MagtheridonHelpers; + +// Don't do anything other than clicking cubes when Magtheridon is casting Blast Nova +float MagtheridonUseManticronCubeMultiplier::GetValue(Action* action) +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + if (!magtheridon) + return 1.0f; + + if (magtheridon->HasUnitState(UNIT_STATE_CASTING) && + magtheridon->FindCurrentSpellBySpellId(SPELL_BLAST_NOVA)) + { + auto it = botToCubeAssignment.find(bot->GetGUID()); + if (it != botToCubeAssignment.end()) + { + if (dynamic_cast(action)) + return 1.0f; + + return 0.0f; + } + } + + return 1.0f; +} + +// Bots will wait for 6 seconds after Magtheridon becomes attackable before engaging +float MagtheridonWaitToAttackMultiplier::GetValue(Action* action) +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + if (!magtheridon || magtheridon->HasAura(SPELL_SHADOW_CAGE)) + return 1.0f; + + const uint8 aggroWaitSeconds = 6; + auto it = magtheridonAggroWaitTimer.find(bot->GetMapId()); + if (it == magtheridonAggroWaitTimer.end() || + (time(nullptr) - it->second) < aggroWaitSeconds) + { + if (!botAI->IsMainTank(bot) && (dynamic_cast(action) || + (!botAI->IsHeal(bot) && dynamic_cast(action)))) + return 0.0f; + } + + return 1.0f; +} + +// No tank assist for offtanks during the channeler phase +// So they don't try to pull channelers from each other or the main tank +float MagtheridonDisableOffTankAssistMultiplier::GetValue(Action* action) +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + Unit* channeler = AI_VALUE2(Unit*, "find target", "hellfire channeler"); + if (!magtheridon) + return 1.0f; + + if ((botAI->IsAssistTankOfIndex(bot, 0) || botAI->IsAssistTankOfIndex(bot, 1)) && + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} diff --git a/src/strategy/raids/magtheridon/RaidMagtheridonMultipliers.h b/src/strategy/raids/magtheridon/RaidMagtheridonMultipliers.h new file mode 100644 index 00000000..2cce516b --- /dev/null +++ b/src/strategy/raids/magtheridon/RaidMagtheridonMultipliers.h @@ -0,0 +1,27 @@ +#ifndef _PLAYERBOT_RAIDMAGTHERIDONMULTIPLIERS_H +#define _PLAYERBOT_RAIDMAGTHERIDONMULTIPLIERS_H + +#include "Multiplier.h" + +class MagtheridonUseManticronCubeMultiplier : public Multiplier +{ +public: + MagtheridonUseManticronCubeMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "magtheridon use manticron cube multiplier") {} + float GetValue(Action* action) override; +}; + +class MagtheridonWaitToAttackMultiplier : public Multiplier +{ +public: + MagtheridonWaitToAttackMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "magtheridon wait to attack multiplier") {} + float GetValue(Action* action) override; +}; + +class MagtheridonDisableOffTankAssistMultiplier : public Multiplier +{ +public: + MagtheridonDisableOffTankAssistMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "magtheridon disable off tank assist multiplier") {} + float GetValue(Action* action) override; +}; + +#endif diff --git a/src/strategy/raids/magtheridon/RaidMagtheridonStrategy.cpp b/src/strategy/raids/magtheridon/RaidMagtheridonStrategy.cpp new file mode 100644 index 00000000..27281dde --- /dev/null +++ b/src/strategy/raids/magtheridon/RaidMagtheridonStrategy.cpp @@ -0,0 +1,42 @@ +#include "RaidMagtheridonStrategy.h" +#include "RaidMagtheridonMultipliers.h" + +void RaidMagtheridonStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back(new TriggerNode("magtheridon incoming blast nova", NextAction::array(0, + new NextAction("magtheridon use manticron cube", ACTION_EMERGENCY + 10), nullptr))); + + triggers.push_back(new TriggerNode("magtheridon need to manage timers and assignments", NextAction::array(0, + new NextAction("magtheridon manage timers and assignments", ACTION_EMERGENCY + 1), nullptr))); + + triggers.push_back(new TriggerNode("magtheridon burning abyssal spawned", NextAction::array(0, + new NextAction("magtheridon warlock cc burning abyssal", ACTION_RAID + 3), nullptr))); + + triggers.push_back(new TriggerNode("magtheridon boss engaged by ranged", NextAction::array(0, + new NextAction("magtheridon spread ranged", ACTION_RAID + 2), nullptr))); + + triggers.push_back(new TriggerNode("magtheridon pulling west and east channelers", NextAction::array(0, + new NextAction("magtheridon misdirect hellfire channelers", ACTION_RAID + 2), nullptr))); + + triggers.push_back(new TriggerNode("magtheridon boss engaged by main tank", NextAction::array(0, + new NextAction("magtheridon main tank position boss", ACTION_RAID + 2), nullptr))); + + triggers.push_back(new TriggerNode("magtheridon first three channelers engaged by main tank", NextAction::array(0, + new NextAction("magtheridon main tank attack first three channelers", ACTION_RAID + 1), nullptr))); + + triggers.push_back(new TriggerNode("magtheridon nw channeler engaged by first assist tank", NextAction::array(0, + new NextAction("magtheridon first assist tank attack nw channeler", ACTION_RAID + 1), nullptr))); + + triggers.push_back(new TriggerNode("magtheridon ne channeler engaged by second assist tank", NextAction::array(0, + new NextAction("magtheridon second assist tank attack ne channeler", ACTION_RAID + 1), nullptr))); + + triggers.push_back(new TriggerNode("magtheridon determining kill order", NextAction::array(0, + new NextAction("magtheridon assign dps priority", ACTION_RAID + 1), nullptr))); +} + +void RaidMagtheridonStrategy::InitMultipliers(std::vector& multipliers) +{ + multipliers.push_back(new MagtheridonUseManticronCubeMultiplier(botAI)); + multipliers.push_back(new MagtheridonWaitToAttackMultiplier(botAI)); + multipliers.push_back(new MagtheridonDisableOffTankAssistMultiplier(botAI)); +} diff --git a/src/strategy/raids/magtheridon/RaidMagtheridonStrategy.h b/src/strategy/raids/magtheridon/RaidMagtheridonStrategy.h new file mode 100644 index 00000000..7b8ab8f9 --- /dev/null +++ b/src/strategy/raids/magtheridon/RaidMagtheridonStrategy.h @@ -0,0 +1,18 @@ +#ifndef _PLAYERBOT_RAIDMAGTHERIDONSTRATEGY_H +#define _PLAYERBOT_RAIDMAGTHERIDONSTRATEGY_H + +#include "Strategy.h" +#include "Multiplier.h" + +class RaidMagtheridonStrategy : public Strategy +{ +public: + RaidMagtheridonStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + std::string const getName() override { return "magtheridon"; } + + void InitTriggers(std::vector& triggers) override; + void InitMultipliers(std::vector& multipliers) override; +}; + +#endif diff --git a/src/strategy/raids/magtheridon/RaidMagtheridonTriggerContext.h b/src/strategy/raids/magtheridon/RaidMagtheridonTriggerContext.h new file mode 100644 index 00000000..525fe496 --- /dev/null +++ b/src/strategy/raids/magtheridon/RaidMagtheridonTriggerContext.h @@ -0,0 +1,37 @@ +#ifndef _PLAYERBOT_RAIDMAGTHERIDONTRIGGERCONTEXT_H +#define _PLAYERBOT_RAIDMAGTHERIDONTRIGGERCONTEXT_H + +#include "RaidMagtheridonTriggers.h" +#include "AiObjectContext.h" + +class RaidMagtheridonTriggerContext : public NamedObjectContext +{ +public: + RaidMagtheridonTriggerContext() : NamedObjectContext() + { + creators["magtheridon first three channelers engaged by main tank"] = &RaidMagtheridonTriggerContext::magtheridon_first_three_channelers_engaged_by_main_tank; + creators["magtheridon nw channeler engaged by first assist tank"] = &RaidMagtheridonTriggerContext::magtheridon_nw_channeler_engaged_by_first_assist_tank; + creators["magtheridon ne channeler engaged by second assist tank"] = &RaidMagtheridonTriggerContext::magtheridon_ne_channeler_engaged_by_second_assist_tank; + creators["magtheridon pulling west and east channelers"] = &RaidMagtheridonTriggerContext::magtheridon_pull_west_and_east_channelers; + creators["magtheridon determining kill order"] = &RaidMagtheridonTriggerContext::magtheridon_determining_kill_order; + creators["magtheridon burning abyssal spawned"] = &RaidMagtheridonTriggerContext::magtheridon_burning_abyssal_spawned; + creators["magtheridon boss engaged by main tank"] = &RaidMagtheridonTriggerContext::magtheridon_boss_engaged_by_main_tank; + creators["magtheridon boss engaged by ranged"] = &RaidMagtheridonTriggerContext::magtheridon_boss_engaged_by_ranged; + creators["magtheridon incoming blast nova"] = &RaidMagtheridonTriggerContext::magtheridon_incoming_blast_nova; + creators["magtheridon need to manage timers and assignments"] = &RaidMagtheridonTriggerContext::magtheridon_need_to_manage_timers_and_assignments; + } + +private: + static Trigger* magtheridon_first_three_channelers_engaged_by_main_tank(PlayerbotAI* botAI) { return new MagtheridonFirstThreeChannelersEngagedByMainTankTrigger(botAI); } + static Trigger* magtheridon_nw_channeler_engaged_by_first_assist_tank(PlayerbotAI* botAI) { return new MagtheridonNWChannelerEngagedByFirstAssistTankTrigger(botAI); } + static Trigger* magtheridon_ne_channeler_engaged_by_second_assist_tank(PlayerbotAI* botAI) { return new MagtheridonNEChannelerEngagedBySecondAssistTankTrigger(botAI); } + static Trigger* magtheridon_pull_west_and_east_channelers(PlayerbotAI* botAI) { return new MagtheridonPullingWestAndEastChannelersTrigger(botAI); } + static Trigger* magtheridon_determining_kill_order(PlayerbotAI* botAI) { return new MagtheridonDeterminingKillOrderTrigger(botAI); } + static Trigger* magtheridon_burning_abyssal_spawned(PlayerbotAI* botAI) { return new MagtheridonBurningAbyssalSpawnedTrigger(botAI); } + static Trigger* magtheridon_boss_engaged_by_main_tank(PlayerbotAI* botAI) { return new MagtheridonBossEngagedByMainTankTrigger(botAI); } + static Trigger* magtheridon_boss_engaged_by_ranged(PlayerbotAI* botAI) { return new MagtheridonBossEngagedByRangedTrigger(botAI); } + static Trigger* magtheridon_incoming_blast_nova(PlayerbotAI* botAI) { return new MagtheridonIncomingBlastNovaTrigger(botAI); } + static Trigger* magtheridon_need_to_manage_timers_and_assignments(PlayerbotAI* botAI) { return new MagtheridonNeedToManageTimersAndAssignmentsTrigger(botAI); } +}; + +#endif diff --git a/src/strategy/raids/magtheridon/RaidMagtheridonTriggers.cpp b/src/strategy/raids/magtheridon/RaidMagtheridonTriggers.cpp new file mode 100644 index 00000000..35442df6 --- /dev/null +++ b/src/strategy/raids/magtheridon/RaidMagtheridonTriggers.cpp @@ -0,0 +1,128 @@ +#include "RaidMagtheridonTriggers.h" +#include "RaidMagtheridonHelpers.h" +#include "Playerbots.h" + +using namespace MagtheridonHelpers; + +bool MagtheridonFirstThreeChannelersEngagedByMainTankTrigger::IsActive() +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + + return magtheridon && botAI->IsMainTank(bot) && + magtheridon->HasAura(SPELL_SHADOW_CAGE); +} + +bool MagtheridonNWChannelerEngagedByFirstAssistTankTrigger::IsActive() +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + Creature* channelerDiamond = GetChanneler(bot, NORTHWEST_CHANNELER); + + return magtheridon && botAI->IsAssistTankOfIndex(bot, 0) && + channelerDiamond && channelerDiamond->IsAlive(); +} + +bool MagtheridonNEChannelerEngagedBySecondAssistTankTrigger::IsActive() +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); + + return magtheridon && botAI->IsAssistTankOfIndex(bot, 1) && + channelerTriangle && channelerTriangle->IsAlive(); +} + +bool MagtheridonPullingWestAndEastChannelersTrigger::IsActive() +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + + Creature* channelerStar = GetChanneler(bot, WEST_CHANNELER); + Creature* channelerCircle = GetChanneler(bot, EAST_CHANNELER); + + return magtheridon && bot->getClass() == CLASS_HUNTER && + ((channelerStar && channelerStar->IsAlive()) || + (channelerCircle && channelerCircle->IsAlive())); +} + +bool MagtheridonDeterminingKillOrderTrigger::IsActive() +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + Unit* channeler = AI_VALUE2(Unit*, "find target", "hellfire channeler"); + + Creature* channelerDiamond = GetChanneler(bot, NORTHWEST_CHANNELER); + Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); + + if (!magtheridon || botAI->IsHeal(bot) || botAI->IsMainTank(bot) || + (botAI->IsAssistTankOfIndex(bot, 0) && channelerDiamond && channelerDiamond->IsAlive()) || + (botAI->IsAssistTankOfIndex(bot, 1) && channelerTriangle && channelerTriangle->IsAlive())) + return false; + + return (channeler && channeler->IsAlive()) || (magtheridon && + !magtheridon->HasAura(SPELL_SHADOW_CAGE)); +} + +bool MagtheridonBurningAbyssalSpawnedTrigger::IsActive() +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + if (!magtheridon || bot->getClass() != CLASS_WARLOCK) + return false; + + const GuidVector& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + return std::any_of(npcs.begin(), npcs.end(), [this](const ObjectGuid& npc) + { + Unit* unit = botAI->GetUnit(npc); + return unit && unit->GetEntry() == NPC_BURNING_ABYSSAL; + }); +} + +bool MagtheridonBossEngagedByMainTankTrigger::IsActive() +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + + return magtheridon && botAI->IsMainTank(bot) && + !magtheridon->HasAura(SPELL_SHADOW_CAGE); +} + +bool MagtheridonBossEngagedByRangedTrigger::IsActive() +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + Unit* channeler = AI_VALUE2(Unit*, "find target", "hellfire channeler"); + + return magtheridon && botAI->IsRanged(bot) && + !(channeler && channeler->IsAlive()); +} + +bool MagtheridonIncomingBlastNovaTrigger::IsActive() +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + Group* group = bot->GetGroup(); + if (!group || !magtheridon || magtheridon->HasAura(SPELL_SHADOW_CAGE)) + return false; + + bool needsReassign = botToCubeAssignment.empty(); + if (!needsReassign) + { + for (auto const& pair : botToCubeAssignment) + { + Player* assigned = ObjectAccessor::FindPlayer(pair.first); + if (!assigned || !assigned->IsAlive()) + { + needsReassign = true; + break; + } + } + } + + if (needsReassign) + { + std::vector cubes = GetAllCubeInfosByDbGuids(bot->GetMap(), MANTICRON_CUBE_DB_GUIDS); + AssignBotsToCubesByGuidAndCoords(group, cubes, botAI); + } + + return botToCubeAssignment.find(bot->GetGUID()) != botToCubeAssignment.end(); +} + +bool MagtheridonNeedToManageTimersAndAssignmentsTrigger::IsActive() +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + + return magtheridon; +} diff --git a/src/strategy/raids/magtheridon/RaidMagtheridonTriggers.h b/src/strategy/raids/magtheridon/RaidMagtheridonTriggers.h new file mode 100644 index 00000000..0039c4e2 --- /dev/null +++ b/src/strategy/raids/magtheridon/RaidMagtheridonTriggers.h @@ -0,0 +1,77 @@ +#ifndef _PLAYERBOT_RAIDMAGTHERIDONTRIGGERS_H +#define _PLAYERBOT_RAIDMAGTHERIDONTRIGGERS_H + +#include "Trigger.h" +#include "PlayerbotAI.h" + +class MagtheridonFirstThreeChannelersEngagedByMainTankTrigger : public Trigger +{ +public: + MagtheridonFirstThreeChannelersEngagedByMainTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon first three channelers engaged by main tank") {}; + bool IsActive() override; +}; + +class MagtheridonNWChannelerEngagedByFirstAssistTankTrigger : public Trigger +{ +public: + MagtheridonNWChannelerEngagedByFirstAssistTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon nw channeler engaged by first assist tank") {}; + bool IsActive() override; +}; + +class MagtheridonNEChannelerEngagedBySecondAssistTankTrigger : public Trigger +{ +public: + MagtheridonNEChannelerEngagedBySecondAssistTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon ne channeler engaged by second assist tank") {}; + bool IsActive() override; +}; + +class MagtheridonPullingWestAndEastChannelersTrigger : public Trigger +{ +public: + MagtheridonPullingWestAndEastChannelersTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon pulling west and east channelers") {}; + bool IsActive() override; +}; + +class MagtheridonDeterminingKillOrderTrigger : public Trigger +{ +public: + MagtheridonDeterminingKillOrderTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon determining kill order") {}; + bool IsActive() override; +}; + +class MagtheridonBurningAbyssalSpawnedTrigger : public Trigger +{ +public: + MagtheridonBurningAbyssalSpawnedTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon burning abyssal spawned") {}; + bool IsActive() override; +}; + +class MagtheridonBossEngagedByMainTankTrigger : public Trigger +{ +public: + MagtheridonBossEngagedByMainTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon boss engaged by main tank") {}; + bool IsActive() override; +}; + +class MagtheridonBossEngagedByRangedTrigger : public Trigger +{ +public: + MagtheridonBossEngagedByRangedTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon boss engaged by ranged") {}; + bool IsActive() override; +}; + +class MagtheridonIncomingBlastNovaTrigger : public Trigger +{ +public: + MagtheridonIncomingBlastNovaTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon incoming blast nova") {}; + bool IsActive() override; +}; + +class MagtheridonNeedToManageTimersAndAssignmentsTrigger : public Trigger +{ +public: + MagtheridonNeedToManageTimersAndAssignmentsTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon need to manage timers and assignments") {}; + bool IsActive() override; +}; + +#endif