Implement Magtheridon strategy (#1721)

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>
This commit is contained in:
Crow
2025-11-05 07:53:16 -06:00
committed by GitHub
parent 80dbd22ba1
commit d02d61e690
15 changed files with 1565 additions and 0 deletions

View File

@@ -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;

View File

@@ -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<Act
actionContexts.Add(new RaidMcActionContext());
actionContexts.Add(new RaidBwlActionContext());
actionContexts.Add(new RaidKarazhanActionContext());
actionContexts.Add(new RaidMagtheridonActionContext());
actionContexts.Add(new RaidGruulsLairActionContext());
actionContexts.Add(new RaidNaxxActionContext());
actionContexts.Add(new RaidOsActionContext());
@@ -147,6 +150,7 @@ void AiObjectContext::BuildSharedTriggerContexts(SharedNamedObjectContextList<Tr
triggerContexts.Add(new RaidMcTriggerContext());
triggerContexts.Add(new RaidBwlTriggerContext());
triggerContexts.Add(new RaidKarazhanTriggerContext());
triggerContexts.Add(new RaidMagtheridonTriggerContext());
triggerContexts.Add(new RaidGruulsLairTriggerContext());
triggerContexts.Add(new RaidNaxxTriggerContext());
triggerContexts.Add(new RaidOsTriggerContext());

View File

@@ -6,6 +6,7 @@
#include "RaidMcStrategy.h"
#include "RaidBwlStrategy.h"
#include "RaidKarazhanStrategy.h"
#include "RaidMagtheridonStrategy.h"
#include "RaidGruulsLairStrategy.h"
#include "RaidNaxxStrategy.h"
#include "RaidOsStrategy.h"
@@ -24,6 +25,7 @@ public:
creators["mc"] = &RaidStrategyContext::mc;
creators["bwl"] = &RaidStrategyContext::bwl;
creators["karazhan"] = &RaidStrategyContext::karazhan;
creators["magtheridon"] = &RaidStrategyContext::magtheridon;
creators["gruulslair"] = &RaidStrategyContext::gruulslair;
creators["naxx"] = &RaidStrategyContext::naxx;
creators["wotlk-os"] = &RaidStrategyContext::wotlk_os;
@@ -39,6 +41,7 @@ private:
static Strategy* mc(PlayerbotAI* botAI) { return new RaidMcStrategy(botAI); }
static Strategy* bwl(PlayerbotAI* botAI) { return new RaidBwlStrategy(botAI); }
static Strategy* karazhan(PlayerbotAI* botAI) { return new RaidKarazhanStrategy(botAI); }
static Strategy* magtheridon(PlayerbotAI* botAI) { return new RaidMagtheridonStrategy(botAI); }
static Strategy* gruulslair(PlayerbotAI* botAI) { return new RaidGruulsLairStrategy(botAI); }
static Strategy* naxx(PlayerbotAI* botAI) { return new RaidNaxxStrategy(botAI); }
static Strategy* wotlk_os(PlayerbotAI* botAI) { return new RaidOsStrategy(botAI); }

View File

@@ -0,0 +1,37 @@
#ifndef _PLAYERBOT_RAIDMAGTHERIDONACTIONCONTEXT_H
#define _PLAYERBOT_RAIDMAGTHERIDONACTIONCONTEXT_H
#include "RaidMagtheridonActions.h"
#include "NamedObjectContext.h"
class RaidMagtheridonActionContext : public NamedObjectContext<Action>
{
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

View File

@@ -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<Player*> 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<int>(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<Unit*> 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<Player*> 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<int>(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<ObjectGuid, Position> MagtheridonSpreadRangedAction::initialPositions;
std::unordered_map<ObjectGuid, bool> 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<Player*> 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<float>(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<float>(rand()) / RAND_MAX * 2.0f * M_PI;
float radius = static_cast<float>(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<float>(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;
}

View File

@@ -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<ObjectGuid, Position> initialPositions;
static std::unordered_map<ObjectGuid, bool> 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

View File

@@ -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<std::string>("rti")->Get();
Unit* currentTarget = botAI->GetAiObjectContext()->GetValue<Unit*>("rti target")->Get();
if (currentRti != rtiName || currentTarget != target)
{
botAI->GetAiObjectContext()->GetValue<std::string>("rti")->Set(rtiName);
botAI->GetAiObjectContext()->GetValue<Unit*>("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<uint32> MANTICRON_CUBE_DB_GUIDS = { 43157, 43158, 43159, 43160, 43161 };
// Get the positions of all Manticron Cubes by their database GUIDs
std::vector<CubeInfo> GetAllCubeInfosByDbGuids(Map* map, const std::vector<uint32>& cubeDbGuids)
{
std::vector<CubeInfo> 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<ObjectGuid, CubeInfo> botToCubeAssignment;
void AssignBotsToCubesByGuidAndCoords(Group* group, const std::vector<CubeInfo>& cubes, PlayerbotAI* botAI)
{
botToCubeAssignment.clear();
if (!group)
return;
size_t cubeIndex = 0;
std::vector<Player*> 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<uint32, bool> lastBlastNovaState;
std::unordered_map<uint32, time_t> magtheridonBlastNovaTimer;
std::unordered_map<uint32, time_t> magtheridonSpreadWaitTimer;
std::unordered_map<uint32, time_t> magtheridonAggroWaitTimer;
bool IsSafeFromMagtheridonHazards(PlayerbotAI* botAI, Player* bot, float x, float y, float z)
{
// Debris
std::vector<Unit*> debrisHazards;
const GuidVector npcs = botAI->GetAiObjectContext()->GetValue<GuidVector>("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<GuidVector>("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;
}
}

View File

@@ -0,0 +1,90 @@
#ifndef _PLAYERBOT_RAIDMAGTHERIDONHELPERS_H
#define _PLAYERBOT_RAIDMAGTHERIDONHELPERS_H
#include <ctime>
#include <unordered_map>
#include <vector>
#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<uint32> MANTICRON_CUBE_DB_GUIDS;
extern std::unordered_map<ObjectGuid, CubeInfo> botToCubeAssignment;
std::vector<CubeInfo> GetAllCubeInfosByDbGuids(Map* map, const std::vector<uint32>& cubeDbGuids);
void AssignBotsToCubesByGuidAndCoords(Group* group, const std::vector<CubeInfo>& cubes, PlayerbotAI* botAI);
extern std::unordered_map<uint32, bool> lastBlastNovaState;
extern std::unordered_map<uint32, time_t> magtheridonBlastNovaTimer;
extern std::unordered_map<uint32, time_t> magtheridonSpreadWaitTimer;
extern std::unordered_map<uint32, time_t> magtheridonAggroWaitTimer;
}
#endif

View File

@@ -0,0 +1,71 @@
#include <unordered_map>
#include <ctime>
#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<MagtheridonUseManticronCubeAction*>(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<AttackAction*>(action) ||
(!botAI->IsHeal(bot) && dynamic_cast<CastSpellAction*>(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<TankAssistAction*>(action))
return 0.0f;
return 1.0f;
}

View File

@@ -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

View File

@@ -0,0 +1,42 @@
#include "RaidMagtheridonStrategy.h"
#include "RaidMagtheridonMultipliers.h"
void RaidMagtheridonStrategy::InitTriggers(std::vector<TriggerNode*>& 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<Multiplier*>& multipliers)
{
multipliers.push_back(new MagtheridonUseManticronCubeMultiplier(botAI));
multipliers.push_back(new MagtheridonWaitToAttackMultiplier(botAI));
multipliers.push_back(new MagtheridonDisableOffTankAssistMultiplier(botAI));
}

View File

@@ -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<TriggerNode*>& triggers) override;
void InitMultipliers(std::vector<Multiplier*>& multipliers) override;
};
#endif

View File

@@ -0,0 +1,37 @@
#ifndef _PLAYERBOT_RAIDMAGTHERIDONTRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDMAGTHERIDONTRIGGERCONTEXT_H
#include "RaidMagtheridonTriggers.h"
#include "AiObjectContext.h"
class RaidMagtheridonTriggerContext : public NamedObjectContext<Trigger>
{
public:
RaidMagtheridonTriggerContext() : NamedObjectContext<Trigger>()
{
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

View File

@@ -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<CubeInfo> 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;
}

View File

@@ -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