#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) { Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); if (!magtheridon) return false; Group* group = bot->GetGroup(); if (!group) return false; const uint32 instanceId = magtheridon->GetMap()->GetInstanceId(); // Wait for 6 seconds after Magtheridon activates to spread const uint8 spreadWaitSeconds = 6; auto it = spreadWaitTimer.find(instanceId); if (it == spreadWaitTimer.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 = blastNovaTimer.find(instanceId); if (timerIt != blastNovaTimer.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 = blastNovaTimer.find(magtheridon->GetMap()->GetInstanceId()); if (timerIt == blastNovaTimer.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; const uint32 instanceId = magtheridon->GetMap()->GetInstanceId(); const time_t now = time(nullptr); bool blastNovaActive = magtheridon->HasUnitState(UNIT_STATE_CASTING) && magtheridon->FindCurrentSpellBySpellId(SPELL_BLAST_NOVA); bool lastBlastNova = lastBlastNovaState[instanceId]; if (lastBlastNova && !blastNovaActive && IsInstanceTimerManager(botAI, bot)) blastNovaTimer[instanceId] = now; lastBlastNovaState[instanceId] = blastNovaActive; if (!magtheridon->HasAura(SPELL_SHADOW_CAGE)) { if (IsInstanceTimerManager(botAI, bot)) { spreadWaitTimer.try_emplace(instanceId, now); blastNovaTimer.try_emplace(instanceId, now); dpsWaitTimer.try_emplace(instanceId, now); } } else { MagtheridonSpreadRangedAction::initialPositions.clear(); MagtheridonSpreadRangedAction::hasReachedInitialPosition.clear(); botToCubeAssignment.clear(); if (IsInstanceTimerManager(botAI, bot)) { spreadWaitTimer.erase(instanceId); blastNovaTimer.erase(instanceId); dpsWaitTimer.erase(instanceId); } } return false; }