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