From 05e8f4d82c429ecf4f144b4321ad785bfeb20cce Mon Sep 17 00:00:00 2001 From: Crow Date: Sat, 16 May 2026 01:26:55 -0500 Subject: [PATCH] Implement Black Temple Strategies (#2381) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Pull Request Description This PR implements strategies for all bosses in the Black Temple. As always, I’ve written these with the intent that all bosses be completable with appropriate gear and 50/50 IP nerfs and boss HP returned to TBC levels. Illidan is difficult at those parameters, but he is certainly doable. You just won’t be able to roll a bunch of meme specs or shitty compositions. Probably. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. My goal with raid strategies is the same—I’m not just trying to make encounters doable but am trying to allow people to experience them in a way that feels like they are part of a coordinated raid of real players. To me, being able to achieve that is the minimum, and that requires getting bots to respond to all major mechanics, even if they can technically just be powered through. ## How to Test the Changes Run the Black Temple raid and see if my descriptions of strategies in the next post check out. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [ ] No, not at all - - [x] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) The performance impact exists only when the “blacktemple” strategy is on. I’ve tested with pmon and done my best to properly gate checks to limit the performance impact even during the instance and boss encounters. - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) The triggers and multipliers will be evaluated as long as the "blacktemple" strategy is active, and it will be applied automatically in the instance (and removed automatically when changing maps outside the instance). - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) GPT-5.4, mainly for calculations and things that are more intermediate concepts that I’m not proficient with like lambdas. ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- conf/playerbots.conf.dist | 2 +- .../Action/RaidBlackTempleActions.cpp | 3041 +++++++++++++++++ .../Action/RaidBlackTempleActions.h | 565 +++ .../Multiplier/RaidBlackTempleMultipliers.cpp | 785 +++++ .../Multiplier/RaidBlackTempleMultipliers.h | 267 ++ .../RaidBlackTempleActionContext.h | 406 +++ .../RaidBlackTempleTriggerContext.h | 406 +++ .../Strategy/RaidBlackTempleStrategy.cpp | 253 ++ .../Strategy/RaidBlackTempleStrategy.h | 22 + .../Trigger/RaidBlackTempleTriggers.cpp | 863 +++++ .../Trigger/RaidBlackTempleTriggers.h | 518 +++ .../Util/RaidBlackTempleHelpers.cpp | 468 +++ .../BlackTemple/Util/RaidBlackTempleHelpers.h | 216 ++ src/Ai/Raid/RaidStrategyContext.h | 3 + src/Bot/Engine/BuildSharedActionContexts.cpp | 2 + src/Bot/Engine/BuildSharedTriggerContexts.cpp | 2 + src/Bot/PlayerbotAI.cpp | 9 +- 17 files changed, 7824 insertions(+), 4 deletions(-) create mode 100644 src/Ai/Raid/BlackTemple/Action/RaidBlackTempleActions.cpp create mode 100644 src/Ai/Raid/BlackTemple/Action/RaidBlackTempleActions.h create mode 100644 src/Ai/Raid/BlackTemple/Multiplier/RaidBlackTempleMultipliers.cpp create mode 100644 src/Ai/Raid/BlackTemple/Multiplier/RaidBlackTempleMultipliers.h create mode 100644 src/Ai/Raid/BlackTemple/RaidBlackTempleActionContext.h create mode 100644 src/Ai/Raid/BlackTemple/RaidBlackTempleTriggerContext.h create mode 100644 src/Ai/Raid/BlackTemple/Strategy/RaidBlackTempleStrategy.cpp create mode 100644 src/Ai/Raid/BlackTemple/Strategy/RaidBlackTempleStrategy.h create mode 100644 src/Ai/Raid/BlackTemple/Trigger/RaidBlackTempleTriggers.cpp create mode 100644 src/Ai/Raid/BlackTemple/Trigger/RaidBlackTempleTriggers.h create mode 100644 src/Ai/Raid/BlackTemple/Util/RaidBlackTempleHelpers.cpp create mode 100644 src/Ai/Raid/BlackTemple/Util/RaidBlackTempleHelpers.h diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index ec5ec4a15..01a6a4600 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -576,7 +576,7 @@ AiPlayerbot.AutoGearScoreLimit = 0 # "mana" (bots have infinite mana) # "power" (bots have infinite energy, rage, and runic power) # "taxi" (bots may use all flight paths, though they will not actually learn them) -# "raid" (bots use cheats implemented into raid strategies (currently only for SSC and Ulduar)) +# "raid" (bots use certain cheats implemented into raid strategies) # To use multiple cheats, separate them by commas below (e.g., to enable all, use "gold,health,mana,power,raid,taxi") # Default: food, taxi, and raid are enabled AiPlayerbot.BotCheats = "food,taxi,raid" diff --git a/src/Ai/Raid/BlackTemple/Action/RaidBlackTempleActions.cpp b/src/Ai/Raid/BlackTemple/Action/RaidBlackTempleActions.cpp new file mode 100644 index 000000000..0d47dbf07 --- /dev/null +++ b/src/Ai/Raid/BlackTemple/Action/RaidBlackTempleActions.cpp @@ -0,0 +1,3041 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "RaidBlackTempleActions.h" + +#include + +#include "CreatureAI.h" +#include "Playerbots.h" +#include "RaidBlackTempleHelpers.h" +#include "RaidBossHelpers.h" + +using namespace BlackTempleHelpers; + +// General + +bool BlackTempleEraseTimersAndTrackersAction::Execute(Event /*event*/) +{ + const ObjectGuid guid = bot->GetGUID(); + const uint32 instanceId = bot->GetMap()->GetInstanceId(); + + if (botAI->IsTank(bot)) + { + bool erased = false; + if (!AI_VALUE2(Unit*, "find target", "illidan stormrage") && + !AI_VALUE2(Unit*, "find target", "flame of azzinoth")) + { + if (illidanBossDpsWaitTimer.erase(instanceId) > 0) + erased = true; + if (illidanFlameDpsWaitTimer.erase(instanceId) > 0) + erased = true; + if (illidanLastPhase.erase(instanceId) > 0) + erased = true; + if (illidanShadowTrapGuid.erase(guid) > 0) + erased = true; + if (illidanShadowTrapDestination.erase(guid) > 0) + erased = true; + if (flameTankWaypointIndex.erase(guid) > 0) + erased = true; + if (westFlameGuid.erase(instanceId) > 0) + erased = true; + if (eastFlameGuid.erase(instanceId) > 0) + erased = true; + } + if (!AI_VALUE2(Unit*, "find target", "gathios the shatterer")) + { + if (councilDpsWaitTimer.erase(instanceId) > 0) + erased = true; + if (gathiosTankStep.erase(guid) > 0) + erased = true; + } + if (!AI_VALUE2(Unit*, "find target", "mother shahraz") && + shahrazTankStep.erase(guid) > 0) + { + erased = true; + } + return erased; + } + else if (botAI->IsHeal(bot)) + { + if (zerevorHealStep.erase(guid) > 0) + return true; + else + return false; + } + else + { + bool erased = false; + if (!AI_VALUE2(Unit*, "find target", "supremus") && + supremusPhaseTimer.erase(instanceId) > 0) + { + erased = true; + } + if (!AI_VALUE2(Unit*, "find target", "ashtongue channeler") && + hasReachedAkamaChannelerPosition.erase(guid) > 0) + { + erased = true; + } + if (!AI_VALUE2(Unit*, "find target", "gurtogg bloodboil") && + gurtoggPhaseTimer.erase(instanceId) > 0) + { + erased = true; + } + return erased; + } +} + +// High Warlord Naj'entus + +bool HighWarlordNajentusMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* najentus = AI_VALUE2(Unit*, "find target", "high warlord naj'entus"); + if (!najentus) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", najentus)) + { + return botAI->CastSpell("steady shot", najentus); + } + + return false; +} + +bool HighWarlordNajentusTanksPositionBossAction::Execute(Event /*event*/) +{ + Unit* najentus = AI_VALUE2(Unit*, "find target", "high warlord naj'entus"); + if (!najentus) + return false; + + if (AI_VALUE(Unit*, "current target") != najentus) + return Attack(najentus); + + if (najentus->GetVictim() == bot && bot->IsWithinMeleeRange(najentus)) + { + const Position& position = NAJENTUS_TANK_POSITION; + const float distToPosition = bot->GetExactDist2d(position.GetPositionX(), + position.GetPositionY()); + if (distToPosition > 3.0f) + { + const float dX = position.GetPositionX() - bot->GetPositionX(); + const float dY = position.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(5.0f, distToPosition); + const float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(BLACK_TEMPLE_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +bool HighWarlordNajentusDisperseRangedAction::Execute(Event /*event*/) +{ + Unit* najentus = AI_VALUE2(Unit*, "find target", "high warlord naj'entus"); + if (!najentus) + return false; + + constexpr uint32 minInterval = 1000; + + constexpr float safeDistFromBoss = 10.0f; + if (bot->GetExactDist2d(najentus) < safeDistFromBoss && + FleePosition(najentus->GetPosition(), safeDistFromBoss, minInterval)) + { + return true; + } + + constexpr float safeDistFromPlayer = 7.0f; + if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistFromPlayer)) + return FleePosition(nearestPlayer->GetPosition(), safeDistFromPlayer, minInterval); + + return false; +} + +bool HighWarlordNajentusRemoveImpalingSpineAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* impaledPlayer = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (member->HasAura(static_cast(BlackTempleSpells::SPELL_IMPALING_SPINE))) + { + impaledPlayer = member; + break; + } + } + if (!impaledPlayer) + return false; + + constexpr float searchRadius = 30.0f; + GameObject* spineGo = bot->FindNearestGameObject( + static_cast(BlackTempleObjects::GO_NAJENTUS_SPINE), searchRadius, true); + if (!spineGo) + return false; + + if (bot->GetExactDist2d(spineGo) > 3.0f) + { + const uint32 delay = urand(2000, 3000); + const ObjectGuid spineGuid = spineGo->GetGUID(); + + botAI->AddTimedEvent( + [this, spineGuid]() + { + if (GameObject* targetSpine = botAI->GetGameObject(spineGuid)) + { + MoveTo(BLACK_TEMPLE_MAP_ID, targetSpine->GetPositionX(), + targetSpine->GetPositionY(), bot->GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, + true, false); + } + }, + delay); + + return true; + } + else + { + const uint32 delay = urand(1000, 2000); + const ObjectGuid spineGuid = spineGo->GetGUID(); + + botAI->AddTimedEvent( + [this, spineGuid]() + { + if (GameObject* targetSpine = botAI->GetGameObject(spineGuid)) + targetSpine->Use(bot); + }, + delay); + + return true; + } + + return false; +} + +bool HighWarlordNajentusThrowImpalingSpineAction::Execute(Event /*event*/) +{ + Unit* najentus = AI_VALUE2(Unit*, "find target", "high warlord naj'entus"); + if (!najentus) + return false; + + if (bot->GetExactDist2d(najentus) > 24.0f) + { + const float angle = atan2(bot->GetPositionY() - najentus->GetPositionY(), + bot->GetPositionX() - najentus->GetPositionX()); + const float targetX = najentus->GetPositionX() + 23.0f * std::cos(angle); + const float targetY = najentus->GetPositionY() + 23.0f * std::sin(angle); + + return MoveTo(BLACK_TEMPLE_MAP_ID, targetX, targetY, bot->GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, + true, false); + } + + if (bot->GetItemByEntry(static_cast(BlackTempleItems::ITEM_NAJENTUS_SPINE))) + { + const uint32 delay = urand(500, 1500); + const ObjectGuid najentusGuid = najentus->GetGUID(); + + botAI->AddTimedEvent( + [this, najentusGuid]() + { + Item* targetSpine = bot->GetItemByEntry( + static_cast(BlackTempleItems::ITEM_NAJENTUS_SPINE)); + Unit* targetNajentus = botAI->GetUnit(najentusGuid); + if (targetSpine && targetNajentus) + botAI->ImbueItem(targetSpine, targetNajentus); + }, + delay); + + return true; + } + + return false; +} + +// Supremus + +bool SupremusMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* supremus = AI_VALUE2(Unit*, "find target", "supremus"); + if (!supremus) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector hunters; + for (GroupReference* ref = group->GetFirstMember(); ref && hunters.size() < 3; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && member->getClass() == CLASS_HUNTER) + hunters.push_back(member); + } + + if (hunters.empty()) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + Player* firstAssistTank = GetGroupAssistTank(botAI, bot, 0); + Player* secondAssistTank = GetGroupAssistTank(botAI, bot, 1); + + Player* misdirectTarget = nullptr; + if (bot == hunters[0] && mainTank) + misdirectTarget = mainTank; + else if (hunters.size() > 1 && bot == hunters[1] && firstAssistTank) + misdirectTarget = firstAssistTank; + else if (hunters.size() > 2 && bot == hunters[2] && secondAssistTank) + misdirectTarget = secondAssistTank; + + if (!misdirectTarget) + return false; + + if (botAI->CanCastSpell("misdirection", misdirectTarget)) + return botAI->CastSpell("misdirection", misdirectTarget); + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", supremus)) + { + return botAI->CastSpell("steady shot", supremus); + } + + return false; +} + +bool SupremusDisperseRangedAction::Execute(Event /*event*/) +{ + constexpr float safeDistance = 8.0f; + constexpr uint32 minInterval = 1000; + if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) + return FleePosition(nearestPlayer->GetPosition(), safeDistance, minInterval); + + return false; +} + +bool SupremusKiteBossAction::Execute(Event /*event*/) +{ + Unit* supremus = AI_VALUE2(Unit*, "find target", "supremus"); + if (!supremus) + return false; + + constexpr float safeDistance = 25.0f; + const float currentDistance = bot->GetDistance2d(supremus); + if (currentDistance < safeDistance) + return MoveAway(supremus, safeDistance - currentDistance); + + return false; +} + +bool SupremusMoveAwayFromVolcanosAction::Execute(Event /*event*/) +{ + auto const& volcanos = GetAllSupremusVolcanos(); + if (volcanos.empty()) + return false; + + constexpr float hazardRadius = 16.0f; + bool inDanger = false; + for (Unit* volcano : volcanos) + { + if (bot->GetDistance2d(volcano) < hazardRadius) + { + inDanger = true; + break; + } + } + + if (!inDanger) + return false; + + constexpr float maxRadius = 40.0f; + Position safestPos = FindSafestNearbyPosition(volcanos, maxRadius, hazardRadius); + + return MoveTo(BLACK_TEMPLE_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); +} + +Position SupremusMoveAwayFromVolcanosAction::FindSafestNearbyPosition( + const std::vector& volcanos, float maxRadius, float hazardRadius) +{ + constexpr float searchStep = M_PI / 8.0f; + constexpr float distanceStep = 1.0f; + + Position bestPos; + float minMoveDistance = std::numeric_limits::max(); + bool foundSafe = false; + + for (float distance = 0.0f; + distance <= maxRadius; distance += distanceStep) + { + for (float angle = 0.0f; angle < 2 * M_PI; angle += searchStep) + { + float x = bot->GetPositionX() + distance * std::cos(angle); + float y = bot->GetPositionY() + distance * std::sin(angle); + + bool isSafe = true; + for (Unit* volcano : volcanos) + { + if (volcano->GetDistance2d(x, y) < hazardRadius) + { + isSafe = false; + break; + } + } + + if (!isSafe) + continue; + + Position testPos(x, y, bot->GetPositionZ()); + + bool pathSafe = + IsPathSafeFromVolcanos(bot->GetPosition(), testPos, volcanos, hazardRadius); + if (pathSafe || !foundSafe) + { + float moveDistance = bot->GetExactDist2d(x, y); + + if (pathSafe && (!foundSafe || moveDistance < minMoveDistance)) + { + bestPos = testPos; + minMoveDistance = moveDistance; + foundSafe = true; + } + else if (!foundSafe && moveDistance < minMoveDistance) + { + bestPos = testPos; + minMoveDistance = moveDistance; + } + } + } + + if (foundSafe) + break; + } + + return bestPos; +} + +bool SupremusMoveAwayFromVolcanosAction::IsPathSafeFromVolcanos(const Position& start, + const Position& end, const std::vector& volcanos, float hazardRadius) +{ + constexpr uint8 numChecks = 10; + float dx = end.GetPositionX() - start.GetPositionX(); + float dy = end.GetPositionY() - start.GetPositionY(); + + for (uint8 i = 1; i <= numChecks; ++i) + { + float ratio = static_cast(i) / numChecks; + float checkX = start.GetPositionX() + dx * ratio; + float checkY = start.GetPositionY() + dy * ratio; + + for (Unit* volcano : volcanos) + { + float distToVol = volcano->GetDistance2d(checkX, checkY); + if (distToVol < hazardRadius) + return false; + } + } + + return true; +} + +std::vector SupremusMoveAwayFromVolcanosAction::GetAllSupremusVolcanos() +{ + std::vector volcanos; + constexpr float searchRadius = 40.0f; + + std::list creatureList; + bot->GetCreatureListWithEntryInGrid(creatureList, static_cast( + BlackTempleNpcs::NPC_SUPREMUS_VOLCANO), searchRadius); + + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + volcanos.push_back(creature); + } + + return volcanos; +} + +bool SupremusManagePhaseTimerAction::Execute(Event /*event*/) +{ + Unit* supremus = AI_VALUE2(Unit*, "find target", "supremus"); + if (!supremus) + return false; + + supremusPhaseTimer.try_emplace( + supremus->GetMap()->GetInstanceId(), std::time(nullptr)); + + return false; +} + +// Shade of Akama + +bool ShadeOfAkamaMeleeDpsPrioritizeChannelersAction::Execute(Event /*event*/) +{ + if (!hasReachedAkamaChannelerPosition.count(bot->GetGUID())) + { + const Position &position = AKAMA_CHANNELER_POSITION; + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, position.GetPositionX(), position.GetPositionY(), + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + else + { + hasReachedAkamaChannelerPosition.insert(bot->GetGUID()); + } + } + + constexpr float searchRadius = 30.0f; + std::list creatureList; + bot->GetCreatureListWithEntryInGrid( + creatureList, static_cast(BlackTempleNpcs::NPC_ASHTONGUE_CHANNELER), searchRadius); + + std::vector channelers; + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + channelers.push_back(creature); + } + + if (channelers.empty()) + return false; + + std::sort(channelers.begin(), channelers.end(), + [](Creature* first, Creature* second) { return first->GetGUID() < second->GetGUID(); }); + + Creature* const channeler = channelers.front(); + + MarkTargetWithSkull(bot, channeler); + + if (AI_VALUE(Unit*, "current target") != channeler) + return Attack(channeler); + + return false; +} + +// Teron Gorefiend + +bool TeronGorefiendMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* gorefiend = AI_VALUE2(Unit*, "find target", "teron gorefiend"); + if (!gorefiend) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", gorefiend)) + { + return botAI->CastSpell("steady shot", gorefiend); + } + + return false; +} + +bool TeronGorefiendTanksPositionBossAction::Execute(Event /*event*/) +{ + Unit* gorefiend = AI_VALUE2(Unit*, "find target", "teron gorefiend"); + if (!gorefiend) + return false; + + MarkTargetWithSkull(bot, gorefiend); + + if (AI_VALUE(Unit*, "current target") != gorefiend) + return Attack(gorefiend); + + if (gorefiend->GetVictim() == bot && bot->IsWithinMeleeRange(gorefiend)) + { + const Position& position = GOREFIEND_TANK_POSITION; + const float distToPosition = bot->GetExactDist2d(position.GetPositionX(), + position.GetPositionY()); + if (distToPosition > 3.0f) + { + const float dX = position.GetPositionX() - bot->GetPositionX(); + const float dY = position.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(5.0f, distToPosition); + const float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(BLACK_TEMPLE_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +// Assume positions in arc at the edge of the balcony (farthest from Constructs) +bool TeronGorefiendPositionRangedOnBalconyAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector rangedMembers; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !botAI->IsRanged(member)) + continue; + + rangedMembers.push_back(member); + } + + if (rangedMembers.empty()) + return false; + + size_t count = rangedMembers.size(); + auto findIt = std::find(rangedMembers.begin(), rangedMembers.end(), bot); + size_t botIndex = (findIt != rangedMembers.end()) ? + std::distance(rangedMembers.begin(), findIt) : 0; + + constexpr float arcSpan = 2.0f * M_PI / 5.0f; + constexpr float arcCenter = 6.279f; + constexpr float arcStart = arcCenter - arcSpan / 2.0f; + + constexpr float radius = 12.0f; + const float angle = (count == 1) ? arcCenter : + (arcStart + arcSpan * static_cast(botIndex) / static_cast(count - 1)); + + const float targetX = GOREFIEND_TANK_POSITION.GetPositionX() + radius * std::cos(angle); + const float targetY = GOREFIEND_TANK_POSITION.GetPositionY() + radius * std::sin(angle); + + if (bot->GetExactDist2d(targetX, targetY) > 1.0f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool TeronGorefiendAvoidShadowOfDeathAction::Execute(Event /*event*/) +{ + switch (bot->getClass()) + { + case CLASS_HUNTER: + return botAI->CanCastSpell("feign death", bot) && + botAI->CastSpell("feign death", bot); + + case CLASS_MAGE: + return botAI->CanCastSpell("ice block", bot) && + botAI->CastSpell("ice block", bot); + + case CLASS_PALADIN: + return botAI->CanCastSpell("divine shield", bot) && + botAI->CastSpell("divine shield", bot); + + case CLASS_ROGUE: + return botAI->CanCastSpell("vanish", bot) && + botAI->CastSpell("vanish", bot); + + default: + return false; + } +} + +bool TeronGorefiendMoveToCornerToDieAction::Execute(Event /*event*/) +{ + const Position& position = GOREFIEND_DIE_POSITION; + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, position.GetPositionX(), position.GetPositionY(), + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool TeronGorefiendControlAndDestroyShadowyConstructsAction::Execute(Event /*event*/) +{ + Unit* gorefiend = AI_VALUE2(Unit*, "find target", "teron gorefiend"); + if (!gorefiend) + return false; + + Unit* spirit = bot->GetCharm(); + if (!spirit) + return false; + + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get(); + Unit* priorityTarget = nullptr; + uint32 highestHp = std::numeric_limits::min(); + + float closestToGorefiend = std::numeric_limits::max(); + + for (auto guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive() || + unit->GetEntry() != static_cast(BlackTempleNpcs::NPC_SHADOWY_CONSTRUCT)) + continue; + + uint32 hp = unit->GetHealth(); + float distToGorefiend = gorefiend->GetExactDist2d(unit); + + if (hp > highestHp) + { + highestHp = hp; + priorityTarget = unit; + closestToGorefiend = distToGorefiend; + } + else if ((hp == highestHp) && (distToGorefiend < closestToGorefiend)) + { + priorityTarget = unit; + closestToGorefiend = distToGorefiend; + } + } + + if (priorityTarget) + { + const float distToTarget = spirit->GetExactDist2d(priorityTarget); + constexpr float desiredDist = 10.0f; + if (distToTarget > desiredDist) + { + const float moveDist = distToTarget - desiredDist + 2.0f; + const float dX = priorityTarget->GetPositionX() - spirit->GetPositionX(); + const float dY = priorityTarget->GetPositionY() - spirit->GetPositionY(); + const float moveX = spirit->GetPositionX() + (dX / distToTarget) * moveDist; + const float moveY = spirit->GetPositionY() + (dY / distToTarget) * moveDist; + + spirit->GetMotionMaster()->MovePoint(0, moveX, moveY, spirit->GetPositionZ()); + return true; + } + + // Adding cooldowns manually is needed due to the charmed creature not observing cooldowns, + // including the GCD. The ordering, including repeating some spells, is the product of testing + // to try to keep the bot from breaking chains with volley, which tends to happen when volley + // is cast before chains (maybe due to projectile travel time?) + if (!spirit->HasSpellCooldown(static_cast(BlackTempleSpells::SPELL_SPIRIT_CHAINS)) && + priorityTarget->GetHealthPct() == 100.0f) + { + spirit->CastSpell( + priorityTarget, static_cast(BlackTempleSpells::SPELL_SPIRIT_CHAINS), true); + spirit->AddSpellCooldown( + static_cast(BlackTempleSpells::SPELL_SPIRIT_CHAINS), 0, 15000); + return true; + } + else if (!spirit->HasSpellCooldown(static_cast(BlackTempleSpells::SPELL_SPIRIT_LANCE))) + { + spirit->CastSpell( + priorityTarget, static_cast(BlackTempleSpells::SPELL_SPIRIT_LANCE), true); + spirit->AddSpellCooldown( + static_cast(BlackTempleSpells::SPELL_SPIRIT_LANCE), 0, 1000); + return true; + } + else if (!spirit->HasSpellCooldown(static_cast(BlackTempleSpells::SPELL_SPIRIT_CHAINS))) + { + spirit->CastSpell( + priorityTarget, static_cast(BlackTempleSpells::SPELL_SPIRIT_CHAINS), true); + spirit->AddSpellCooldown( + static_cast(BlackTempleSpells::SPELL_SPIRIT_CHAINS), 0, 15000); + return true; + } + else if (!spirit->HasSpellCooldown(static_cast(BlackTempleSpells::SPELL_SPIRIT_LANCE))) + { + spirit->CastSpell + (priorityTarget, static_cast(BlackTempleSpells::SPELL_SPIRIT_LANCE), true); + spirit->AddSpellCooldown( + static_cast(BlackTempleSpells::SPELL_SPIRIT_LANCE), 0, 1000); + return true; + } + else if (!spirit->HasSpellCooldown(static_cast(BlackTempleSpells::SPELL_SPIRIT_VOLLEY)) && + !priorityTarget->HasAura(static_cast(BlackTempleSpells::SPELL_SPIRIT_CHAINS))) + { + spirit->CastSpell + (priorityTarget, static_cast(BlackTempleSpells::SPELL_SPIRIT_VOLLEY), true); + spirit->AddSpellCooldown( + static_cast(BlackTempleSpells::SPELL_SPIRIT_VOLLEY), 0, 15000); + return true; + } + } + else + { + const float distToGorefiend = spirit->GetExactDist2d(gorefiend); + constexpr float targetDist = 5.0f; + if (distToGorefiend > targetDist) + { + const float moveDist = distToGorefiend - targetDist; + const float dX = gorefiend->GetPositionX() - spirit->GetPositionX(); + const float dY = gorefiend->GetPositionY() - spirit->GetPositionY(); + const float moveX = spirit->GetPositionX() + (dX / distToGorefiend) * moveDist; + const float moveY = spirit->GetPositionY() + (dY / distToGorefiend) * moveDist; + + spirit->GetMotionMaster()->MovePoint(0, moveX, moveY, spirit->GetPositionZ()); + return true; + } + else if (!spirit->HasSpellCooldown(static_cast(BlackTempleSpells::SPELL_SPIRIT_STRIKE))) + { + spirit->CastSpell( + gorefiend, static_cast(BlackTempleSpells::SPELL_SPIRIT_STRIKE), true); + spirit->AddSpellCooldown( + static_cast(BlackTempleSpells::SPELL_SPIRIT_STRIKE), 0, 1000); + return true; + } + } + + return false; +} + +// Gurtogg Bloodboil + +bool GurtoggBloodboilMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* gurtogg = AI_VALUE2(Unit*, "find target", "gurtogg bloodboil"); + if (!gurtogg) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", gurtogg)) + { + return botAI->CastSpell("steady shot", gurtogg); + } + + return false; +} + +bool GurtoggBloodboilTanksPositionBossAction::Execute(Event /*event*/) +{ + Unit* gurtogg = AI_VALUE2(Unit*, "find target", "gurtogg bloodboil"); + if (!gurtogg) + return false; + + if (AI_VALUE(Unit*, "current target") != gurtogg) + return Attack(gurtogg); + + Unit* victim = gurtogg->GetVictim(); + Player* playerVictim = victim ? victim->ToPlayer() : nullptr; + if (playerVictim && botAI->IsTank(playerVictim) && bot->IsWithinMeleeRange(gurtogg)) + { + const Position& position = GURTOGG_TANK_POSITION; + const float distToPosition = bot->GetExactDist2d(position.GetPositionX(), + position.GetPositionY()); + if (distToPosition > 2.0f) + { + const float dX = position.GetPositionX() - bot->GetPositionX(); + const float dY = position.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(5.0f, distToPosition); + const float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(BLACK_TEMPLE_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +bool GurtoggBloodboilRotateRangedGroupsAction::Execute(Event /*event*/) +{ + Unit* gurtogg = AI_VALUE2(Unit*, "find target", "gurtogg bloodboil"); + if (!gurtogg) + return false; + + auto groups = GetGurtoggRangedRotationGroups(bot); + int activeGroup = GetGurtoggActiveRotationGroup(gurtogg); + + bool inActiveGroup = false; + if (activeGroup >= 0 && activeGroup < groups.size()) + { + auto const& group = groups[activeGroup]; + inActiveGroup = std::find(group.begin(), group.end(), bot) != group.end(); + } + + const Position& nearPosition = GURTOGG_RANGED_POSITION; + const Position& farPosition = GURTOGG_SOAKER_POSITION; + constexpr float distFromPos = 2.0f; + + if (inActiveGroup && bot->GetExactDist2d(farPosition) > distFromPos) + { + return MoveInside(BLACK_TEMPLE_MAP_ID, farPosition.GetPositionX(), + farPosition.GetPositionY(), bot->GetPositionZ(), + distFromPos, MovementPriority::MOVEMENT_FORCED); + } + else if (!inActiveGroup && bot->GetExactDist2d(nearPosition) > distFromPos) + { + return MoveInside(BLACK_TEMPLE_MAP_ID, nearPosition.GetPositionX(), + nearPosition.GetPositionY(), bot->GetPositionZ(), + distFromPos, MovementPriority::MOVEMENT_FORCED); + } + + return false; +} + +bool GurtoggBloodboilRangedMoveAwayFromEnragedPlayerAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* enragedPlayer = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->HasAura( + static_cast(BlackTempleSpells::SPELL_PLAYER_FEL_RAGE))) + { + enragedPlayer = member; + break; + } + } + + constexpr float safeDistance = 20.0f; + constexpr uint32 minInterval = 0; + if (enragedPlayer && bot->GetExactDist2d(enragedPlayer) < safeDistance) + return FleePosition(enragedPlayer->GetPosition(), safeDistance, minInterval); + + return false; +} + +bool GurtoggBloodboilManagePhaseTimerAction::Execute(Event /*event*/) +{ + Unit* gurtogg = AI_VALUE2(Unit*, "find target", "gurtogg bloodboil"); + if (!gurtogg) + return false; + + const time_t now = std::time(nullptr); + const uint32 instanceId = gurtogg->GetMap()->GetInstanceId(); + + if (gurtogg->HasAura(static_cast(BlackTempleSpells::SPELL_BOSS_FEL_RAGE))) + { + return gurtoggPhaseTimer.erase(instanceId) > 0; + } + else + { + auto [it, inserted] = gurtoggPhaseTimer.try_emplace(instanceId, now); + return inserted; + } +} + +// Reliquary of Souls + +bool ReliquaryOfSoulsMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* desire = AI_VALUE2(Unit*, "find target", "essence of desire"); + Unit* anger = AI_VALUE2(Unit*, "find target", "essence of anger"); + + if (!desire && !anger) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + Unit* target = desire ? desire : anger; + + if (target->GetHealthPct() > 95.0f) + { + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", target)) + { + return botAI->CastSpell("steady shot", target); + } + } + + return false; +} + +bool ReliquaryOfSoulsAdjustDistanceFromSufferingAction::Execute(Event /*event*/) +{ + Unit* suffering = AI_VALUE2(Unit*, "find target", "essence of suffering"); + if (!suffering) + return false; + + if (botAI->IsTank(bot) && bot->GetHealthPct() > 25.0f) + return TanksMoveToMinimumRange(suffering); + else if (botAI->IsMelee(bot) && bot->GetVictim() != suffering) + return MeleeDpsStayAtMaximumRange(suffering); + else if (botAI->IsRanged(bot)) + return RangedMoveAwayFromBoss(suffering); + + return false; +} + +bool ReliquaryOfSoulsAdjustDistanceFromSufferingAction::TanksMoveToMinimumRange(Unit* suffering) +{ + const float distanceToBoss = bot->GetExactDist2d(suffering); + if (distanceToBoss > 2.0f) + { + const float dX = suffering->GetPositionX() - bot->GetPositionX(); + const float dY = suffering->GetPositionY() - bot->GetPositionY(); + const float targetX = bot->GetPositionX() + (dX / distanceToBoss); + const float targetY = bot->GetPositionY() + (dY / distanceToBoss); + + return MoveTo(BLACK_TEMPLE_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool ReliquaryOfSoulsAdjustDistanceFromSufferingAction::MeleeDpsStayAtMaximumRange(Unit* suffering) +{ + const float desiredDist = bot->GetMeleeRange(suffering); + const float behindAngle = Position::NormalizeOrientation(suffering->GetOrientation() + M_PI); + const float targetX = suffering->GetPositionX() + desiredDist * std::cos(behindAngle); + const float targetY = suffering->GetPositionY() + desiredDist * std::sin(behindAngle); + + if (bot->GetExactDist2d(targetX, targetY) > 0.25f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool ReliquaryOfSoulsAdjustDistanceFromSufferingAction::RangedMoveAwayFromBoss(Unit* suffering) +{ + constexpr float safeDistance = 15.0f; + constexpr uint32 minInterval = 1000; + if (bot->GetExactDist2d(suffering) < safeDistance) + return FleePosition(suffering->GetPosition(), safeDistance, minInterval); + + return false; +} + +bool ReliquaryOfSoulsHealersDpsSufferingAction::Execute(Event /*event*/) +{ + Unit* suffering = AI_VALUE2(Unit*, "find target", "essence of suffering"); + if (!suffering) + return false; + + if (bot->getClass() == CLASS_DRUID) + { + if (botAI->HasAura("tree of life", bot)) + botAI->RemoveAura("tree of life"); + + bool casted = false; + + if (botAI->CanCastSpell("barkskin", bot) && + botAI->CastSpell("barkskin", bot)) + casted = true; + + if (botAI->CanCastSpell("wrath", suffering) && + botAI->CastSpell("wrath", suffering)) + casted = true; + + return casted; + } + else if (bot->getClass() == CLASS_PALADIN) + { + bool casted = false; + + if (botAI->CanCastSpell("avenging wrath", bot) && + botAI->CastSpell("avenging wrath", bot)) + casted = true; + + if (botAI->CanCastSpell("consecration", bot) && + botAI->CastSpell("consecration", bot)) + casted = true; + + if (botAI->CanCastSpell("exorcism", suffering) && + botAI->CastSpell("exorcism", suffering)) + casted = true; + + if (botAI->CanCastSpell("hammer of wrath", suffering) && + botAI->CastSpell("hammer of wrath", suffering)) + casted = true; + + if (botAI->CanCastSpell("holy shock", suffering) && + botAI->CastSpell("holy shock", suffering)) + casted = true; + + if (botAI->CanCastSpell("judgement of light", suffering) && + botAI->CastSpell("judgement of light", suffering)) + casted = true; + + return casted; + } + else if (bot->getClass() == CLASS_PRIEST) + { + if (botAI->CanCastSpell("smite", suffering)) + return botAI->CastSpell("smite", suffering); + } + else if (bot->getClass() == CLASS_SHAMAN) + { + bool casted = false; + + if (botAI->CanCastSpell("earth shock", suffering) && + botAI->CastSpell("earth shock", suffering)) + casted = true; + + if (botAI->CanCastSpell("chain lightning", suffering) && + botAI->CastSpell("chain lightning", suffering)) + casted = true; + + if (botAI->CanCastSpell("lightning bolt", suffering) && + botAI->CastSpell("lightning bolt", suffering)) + casted = true; + + return casted; + } + + return false; +} + +bool ReliquaryOfSoulsSpellstealRuneShieldAction::Execute(Event /*event*/) +{ + if (Unit* desire = AI_VALUE2(Unit*, "find target", "essence of desire"); + desire && botAI->CanCastSpell("spellsteal", desire)) + { + return botAI->CastSpell("spellsteal", desire); + } + + return false; +} + +bool ReliquaryOfSoulsSpellReflectDeadenAction::Execute(Event /*event*/) +{ + if (botAI->CanCastSpell("spell reflection", bot)) + return botAI->CastSpell("spell reflection", bot); + + return false; +} + +// Mother Shahraz + +bool MotherShahrazMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* shahraz = AI_VALUE2(Unit*, "find target", "mother shahraz"); + if (!shahraz) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", shahraz)) + { + return botAI->CastSpell("steady shot", shahraz); + } + + return false; +} + +bool MotherShahrazTanksPositionBossUnderPillarAction::Execute(Event /*event*/) +{ + Unit* shahraz = AI_VALUE2(Unit*, "find target", "mother shahraz"); + if (!shahraz) + return false; + + if (AI_VALUE(Unit*, "current target") != shahraz) + return Attack(shahraz); + + Unit* victim = shahraz->GetVictim(); + Player* playerVictim = victim ? victim->ToPlayer() : nullptr; + if (playerVictim && botAI->IsTank(playerVictim)) + { + const ObjectGuid guid = bot->GetGUID(); + auto it = shahrazTankStep.try_emplace( + guid, TankPositionState::MovingToTransition).first; + TankPositionState state = it->second; + + constexpr float maxDistance = 0.5f; + const Position& position = state == TankPositionState::MovingToTransition ? + SHAHRAZ_TRANSITION_POSITION : SHAHRAZ_TANK_POSITION; + const float distToPosition = bot->GetExactDist2d(position); + + if (distToPosition > maxDistance && bot->IsWithinMeleeRange(shahraz)) + { + const bool backwards = (shahraz->GetVictim() == bot); + return MoveTo(BLACK_TEMPLE_MAP_ID, position.GetPositionX(), position.GetPositionY(), + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, backwards); + } + + if (state == TankPositionState::MovingToTransition && distToPosition <= maxDistance) + shahrazTankStep[guid] = TankPositionState::MovingToFinal; + + if (state != TankPositionState::MovingToTransition && distToPosition <= maxDistance) + { + const float orientation = atan2(shahraz->GetPositionY() - bot->GetPositionY(), + shahraz->GetPositionX() - bot->GetPositionX()); + bot->SetFacingTo(orientation); + shahrazTankStep[guid] = TankPositionState::Positioned; + } + } + + return false; +} + +bool MotherShahrazMeleeDpsWaitAtSafePositionAction::Execute(Event /*event*/) +{ + return MoveTo(BLACK_TEMPLE_MAP_ID, SHAHRAZ_RANGED_POSITION.GetPositionX(), + SHAHRAZ_RANGED_POSITION.GetPositionY(), bot->GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); +} + +// This doesn't matter for bots since they don't take fall damage, and it's actually easier +// to tank her closer to her starting position, but I want to simulate a player strategy +bool MotherShahrazPositionRangedUnderPillarAction::Execute(Event /*event*/) +{ + const Position& position = SHAHRAZ_RANGED_POSITION; + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 1.0f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool MotherShahrazRunAwayToBreakFatalAttractionAction::Execute(Event /*event*/) +{ + std::vector attractedPlayers = GetAttractedPlayers(); + if (attractedPlayers.size() < 2) + return false; + + float centerX = 0.0f, centerY = 0.0f; + for (Player* member : attractedPlayers) + { + centerX += member->GetPositionX(); + centerY += member->GetPositionY(); + } + centerX /= attractedPlayers.size(); + centerY /= attractedPlayers.size(); + + auto botIt = std::find(attractedPlayers.begin(), attractedPlayers.end(), bot); + if (botIt == attractedPlayers.end()) + return false; + + const float spreadAngle = + 2.0f * M_PI * std::distance(attractedPlayers.begin(), botIt) / attractedPlayers.size(); + + constexpr float maxSpreadDistance = 35.0f; + constexpr float distanceStep = 1.0f; + float lastValidX = bot->GetPositionX(); + float lastValidY = bot->GetPositionY(); + float lastValidZ = bot->GetPositionZ(); + + for (float currentDistance = distanceStep; + currentDistance <= maxSpreadDistance; + currentDistance += distanceStep) + { + float testX = centerX + std::cos(spreadAngle) * currentDistance; + float testY = centerY + std::sin(spreadAngle) * currentDistance; + float testZ = lastValidZ; + + if (!bot->GetMap()->CheckCollisionAndGetValidCoords( + bot, bot->GetPositionX(), bot->GetPositionY(), + bot->GetPositionZ(), testX, testY, testZ)) + { + break; + } + + lastValidX = testX; + lastValidY = testY; + lastValidZ = testZ; + } + + if (MoveTo(BLACK_TEMPLE_MAP_ID, lastValidX, lastValidY, lastValidZ, false, false, + false, false, MovementPriority::MOVEMENT_FORCED, true, false)) + { + return true; + } + else + { + // In case bots get stuck, try a 5-yard random move + const float angle = frand(0.0f, 2.0f * M_PI); + constexpr float dist = 5.0f; + float randX = bot->GetPositionX() + std::cos(angle) * dist; + float randY = bot->GetPositionY() + std::sin(angle) * dist; + float randZ = lastValidZ; + bot->GetMap()->CheckCollisionAndGetValidCoords( + bot, bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), + randX, randY, randZ); + + return MoveTo(BLACK_TEMPLE_MAP_ID, randX, randY, randZ, false, false, false, + false, MovementPriority::MOVEMENT_FORCED, true, false); + } +} + +std::vector MotherShahrazRunAwayToBreakFatalAttractionAction::GetAttractedPlayers() +{ + std::vector attractedPlayers; + Group* group = bot->GetGroup(); + if (!group) + return attractedPlayers; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->HasAura(static_cast( + BlackTempleSpells::SPELL_FATAL_ATTRACTION))) + { + attractedPlayers.push_back(member); + } + } + + std::sort(attractedPlayers.begin(), attractedPlayers.end(), + [](Player* a, Player* b) { + return a->GetGUID().GetCounter() < b->GetGUID().GetCounter(); + }); + + return attractedPlayers; +} + +// Illidari Council + +bool IllidariCouncilMisdirectBossesToTanksAction::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); + } + + if (hunters.size() >= 4) + break; + } + + int8 hunterIndex = -1; + for (size_t i = 0; i < hunters.size(); ++i) + { + if (hunters[i] == bot) + { + hunterIndex = static_cast(i); + break; + } + } + if (hunterIndex == -1) + return false; + + Unit* councilTarget = nullptr; + Player* tankTarget = nullptr; + if (hunterIndex == 0) + { + councilTarget = AI_VALUE2(Unit*, "find target", "high nethermancer zerevor"); + tankTarget = GetZerevorMageTank(bot); + } + else if (hunterIndex == 1) + { + councilTarget = AI_VALUE2(Unit*, "find target", "lady malande"); + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + if (Player* member = GetGroupAssistTank(botAI, bot, 0)) + { + tankTarget = member; + break; + } + } + } + else if (hunterIndex == 2) + { + councilTarget = AI_VALUE2(Unit*, "find target", "gathios the shatterer"); + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + if (Player* member = GetGroupMainTank(botAI, bot)) + { + tankTarget = member; + break; + } + } + } + else if (hunterIndex == 3) + { + councilTarget = AI_VALUE2(Unit*, "find target", "veras darkshadow"); + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + if (Player* member = GetGroupAssistTank(botAI, bot, 1)) + { + tankTarget = member; + break; + } + } + } + + if (!councilTarget || !tankTarget || !tankTarget->IsAlive()) + return false; + + if (botAI->CanCastSpell("misdirection", tankTarget)) + return botAI->CastSpell("misdirection", tankTarget); + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", councilTarget)) + { + return botAI->CastSpell("steady shot", councilTarget); + } + + return false; +} + +bool IllidariCouncilMainTankPositionGathiosAction::Execute(Event /*event*/) +{ + Unit* gathios = AI_VALUE2(Unit*, "find target", "gathios the shatterer"); + if (!gathios) + return false; + + // Failsafe for if bot falls through the floor, which tends to happen upon the pull + if (bot->GetPositionZ() < COUNCIL_FLOOR_Z_THRESHOLD) + { + bot->TeleportTo(BLACK_TEMPLE_MAP_ID, gathios->GetPositionX(), gathios->GetPositionY(), + gathios->GetPositionZ(), bot->GetOrientation()); + } + + MarkTargetWithSquare(bot, gathios); + SetRtiTarget(botAI, "square", gathios); + + if (AI_VALUE(Unit*, "current target") != gathios) + return Attack(gathios); + + const ObjectGuid guid = bot->GetGUID(); + uint8 index = gathiosTankStep.count(guid) ? gathiosTankStep[guid] : 0; + + const Position& position = GATHIOS_TANK_POSITIONS[index]; + + constexpr float maxDistance = 2.0f; + float distToPosition = bot->GetExactDist2d(position); + + if (gathios->GetVictim() == bot && bot->IsWithinMeleeRange(gathios)) + { + if (distToPosition <= maxDistance && HasDangerousCouncilAura(bot)) + { + index = (index + 1) % 4; + gathiosTankStep[guid] = index; + const Position& newPosition = GATHIOS_TANK_POSITIONS[index]; + const float newDistToPosition = bot->GetExactDist2d(newPosition); + if (newDistToPosition > maxDistance) + { + const float dX = newPosition.GetPositionX() - bot->GetPositionX(); + const float dY = newPosition.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(5.0f, newDistToPosition); + const float moveX = bot->GetPositionX() + (dX / newDistToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / newDistToPosition) * moveDist; + + return MoveTo(BLACK_TEMPLE_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + else if (distToPosition > maxDistance) + { + const float dX = position.GetPositionX() - bot->GetPositionX(); + const float dY = position.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(5.0f, distToPosition); + const float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(BLACK_TEMPLE_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +bool IllidariCouncilMainTankReflectJudgementOfCommandAction::Execute(Event /*event*/) +{ + if (botAI->CanCastSpell("spell reflection", bot)) + return botAI->CastSpell("spell reflection", bot); + + return false; +} + +bool IllidariCouncilFirstAssistTankFocusMalandeAction::Execute(Event /*event*/) +{ + Unit* malande = AI_VALUE2(Unit*, "find target", "lady malande"); + if (!malande) + return false; + + // Failsafe for if bot falls through the floor, which tends to happen upon the pull + if (bot->GetPositionZ() < COUNCIL_FLOOR_Z_THRESHOLD) + { + bot->TeleportTo(BLACK_TEMPLE_MAP_ID, malande->GetPositionX(), malande->GetPositionY(), + malande->GetPositionZ(), bot->GetOrientation()); + } + + MarkTargetWithStar(bot, malande); + SetRtiTarget(botAI, "star", malande); + + if (AI_VALUE(Unit*, "current target") != malande) + return Attack(malande); + + return false; +} + +bool IllidariCouncilSecondAssistTankPositionDarkshadowAction::Execute(Event /*event*/) +{ + Unit* darkshadow = AI_VALUE2(Unit*, "find target", "veras darkshadow"); + if (!darkshadow) + return false; + + // Failsafe for if bot falls through the floor, which tends to happen upon the pull + if (bot->GetPositionZ() < COUNCIL_FLOOR_Z_THRESHOLD) + { + bot->TeleportTo(BLACK_TEMPLE_MAP_ID, darkshadow->GetPositionX(), darkshadow->GetPositionY(), + darkshadow->GetPositionZ(), bot->GetOrientation()); + } + + MarkTargetWithCircle(bot, darkshadow); + SetRtiTarget(botAI, "circle", darkshadow); + + if (AI_VALUE(Unit*, "current target") != darkshadow) + return Attack(darkshadow); + + if (darkshadow->GetVictim() == bot) + { + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + const float distToPosition = bot->GetExactDist2d(mainTank->GetPositionX(), + mainTank->GetPositionY()); + if (distToPosition > 2.0f) + { + const float dX = mainTank->GetPositionX() - bot->GetPositionX(); + const float dY = mainTank->GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(5.0f, distToPosition); + const float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + const bool backwards = bot->GetExactDist2d(mainTank) < 10.0f; + return MoveTo(BLACK_TEMPLE_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, backwards); + } + } + + return false; +} + +bool IllidariCouncilMageTankPositionZerevorAction::Execute(Event /*event*/) +{ + Unit* zerevor = AI_VALUE2(Unit*, "find target", "high nethermancer zerevor"); + if (!zerevor) + return false; + + if (zerevor->HasAura(static_cast(BlackTempleSpells::SPELL_DAMPEN_MAGIC)) && + botAI->CanCastSpell("spellsteal", zerevor)) + { + return botAI->CastSpell("spellsteal", zerevor); + } + + MarkTargetWithTriangle(bot, zerevor); + SetRtiTarget(botAI, "triangle", zerevor); + + if (AI_VALUE(Unit*, "current target") != zerevor) + return Attack(zerevor); + + if (zerevor->GetVictim() == bot) + { + const Position& position = ZEREVOR_TANK_POSITION; + const float distToPosition = bot->GetExactDist2d(position.GetPositionX(), + position.GetPositionY()); + if (distToPosition > 2.0f) + { + const float dX = position.GetPositionX() - bot->GetPositionX(); + const float dY = position.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(10.0f, distToPosition); + const float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(BLACK_TEMPLE_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +bool IllidariCouncilPositionMageTankHealerAction::Execute(Event /*event*/) +{ + Player* mageTank = GetZerevorMageTank(bot); + if (!mageTank) + return false; + + Unit* zerevor = AI_VALUE2(Unit*, "find target", "high nethermancer zerevor"); + if (!zerevor || zerevor->GetVictim() != mageTank) + return false; + + const ObjectGuid guid = bot->GetGUID(); + uint8 index = zerevorHealStep.count(guid) ? zerevorHealStep[guid] : 0; + + const Position& position = ZEREVOR_HEALER_POSITIONS[index]; + + constexpr float maxDistance = 1.0f; + const float distToPosition = bot->GetExactDist2d(position); + + if (distToPosition <= maxDistance && HasDangerousCouncilAura(bot)) + { + index = (index + 1) % 2; + zerevorHealStep[guid] = index; + const Position& newPosition = ZEREVOR_HEALER_POSITIONS[index]; + const float newDistToPosition = bot->GetExactDist2d(newPosition); + if (newDistToPosition > maxDistance) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, newPosition.GetPositionX(), + newPosition.GetPositionY(), bot->GetPositionZ(), + false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + else if (distToPosition > maxDistance) + { + const float dX = position.GetPositionX() - bot->GetPositionX(); + const float dY = position.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(10.0f, distToPosition); + const float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(BLACK_TEMPLE_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +bool IllidariCouncilDisperseRangedAction::Execute(Event /*event*/) +{ + constexpr float safeDistance = 4.0f; + constexpr uint32 minInterval = 1000; + if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) + return FleePosition(nearestPlayer->GetPosition(), safeDistance, minInterval); + + return false; +} + +bool IllidariCouncilCommandPetsToAttackGathiosAction::Execute(Event /*event*/) +{ + Unit* gathios = AI_VALUE2(Unit*, "find target", "gathios the shatterer"); + if (!gathios) + return false; + + Pet* pet = bot->GetPet(); + if (pet && pet->IsAlive() && pet->GetVictim() != gathios) + { + pet->ClearUnitState(UNIT_STATE_FOLLOW); + pet->AttackStop(); + pet->SetTarget(gathios->GetGUID()); + + if (pet->GetCharmInfo()) + { + pet->GetCharmInfo()->SetIsCommandAttack(true); + pet->GetCharmInfo()->SetIsAtStay(false); + pet->GetCharmInfo()->SetIsFollowing(false); + pet->GetCharmInfo()->SetIsCommandFollow(false); + pet->GetCharmInfo()->SetIsReturning(false); + + pet->AI()->AttackStart(gathios); + return true; + } + } + + return false; +} + +bool IllidariCouncilAssignDpsTargetsAction::Execute(Event /*event*/) +{ + Unit* malande = AI_VALUE2(Unit*, "find target", "lady malande"); + if (!malande) + return false; + + bool shouldAttackMalande = false; + Unit* zerevor = AI_VALUE2(Unit*, "find target", "high nethermancer zerevor"); + if (zerevor && zerevor->GetExactDist2d(malande) < 15.0f) + { + shouldAttackMalande = false; + } + else if (bot->getClass() == CLASS_ROGUE || + (bot->getClass() == CLASS_WARRIOR && botAI->IsDps(bot))) + { + shouldAttackMalande = !malande->HasAura( + static_cast(BlackTempleSpells::SPELL_BLESSING_OF_PROTECTION)); + } + else if (bot->getClass() == CLASS_SHAMAN && botAI->IsDps(bot)) + { + shouldAttackMalande = !malande->HasAura( + static_cast(BlackTempleSpells::SPELL_BLESSING_OF_SPELL_WARDING)); + } + + if (shouldAttackMalande) + { + SetRtiTarget(botAI, "star", malande); + + if (AI_VALUE(Unit*, "current target") != malande) + return Attack(malande); + } + else if (Unit* darkshadow = AI_VALUE2(Unit*, "find target", "veras darkshadow"); + darkshadow && !darkshadow->HasAura( + static_cast(BlackTempleSpells::SPELL_VANISH))) + { + SetRtiTarget(botAI, "circle", darkshadow); + + if (AI_VALUE(Unit*, "current target") != darkshadow) + return Attack(darkshadow); + } + else if (Unit* gathios = AI_VALUE2(Unit*, "find target", "gathios the shatterer")) + { + SetRtiTarget(botAI, "square", gathios); + + if (AI_VALUE(Unit*, "current target") != gathios) + return Attack(gathios); + } + + return false; +} + +bool IllidariCouncilManageDpsTimerAction::Execute(Event /*event*/) +{ + if (Unit* gathios = AI_VALUE2(Unit*, "find target", "gathios the shatterer")) + { + return councilDpsWaitTimer.try_emplace( + gathios->GetMap()->GetInstanceId(), std::time(nullptr)).second; + } + + return false; +} + +// Illidan Stormrage + +bool IllidanStormrageMisdirectToTankAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + int phase = GetIllidanPhase(illidan); + + if (phase == 2 && TryMisdirectToFlameTanks(group)) + return true; + + return phase == 4 && TryMisdirectToWarlockTank(illidan); +} + +bool IllidanStormrageMisdirectToTankAction::TryMisdirectToFlameTanks(Group* group) +{ + 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); + } + + if (hunters.size() >= 2) + break; + } + + int8 hunterIndex = -1; + for (size_t i = 0; i < hunters.size(); ++i) + { + if (hunters[i] == bot) + { + hunterIndex = static_cast(i); + break; + } + } + if (hunterIndex == -1) + return false; + + auto [eastFlame, westFlame] = GetFlamesOfAzzinoth(bot); + if (!eastFlame || !westFlame || eastFlame == westFlame) + return false; + + Player* firstAssistTank = GetGroupAssistTank(botAI, bot, 0); + Player* secondAssistTank = GetGroupAssistTank(botAI, bot, 1); + if (!firstAssistTank || !secondAssistTank) + return false; + + if (hunters.size() == 1) + { + if (eastFlame->GetHealthPct() < 99.0f) + return false; + + if (botAI->CanCastSpell("misdirection", secondAssistTank)) + return botAI->CastSpell("misdirection", secondAssistTank); + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", eastFlame)) + { + return botAI->CastSpell("steady shot", eastFlame); + } + + return false; + } + + Player* tankTarget = nullptr; + Unit* flame = nullptr; + + if (hunterIndex == 0) + { + tankTarget = secondAssistTank; + flame = eastFlame; + } + else if (hunterIndex == 1) + { + tankTarget = firstAssistTank; + flame = westFlame; + } + else + return false; + + if (!tankTarget || !tankTarget->IsAlive() || flame->GetHealthPct() < 90.0f) + return false; + + if (botAI->CanCastSpell("misdirection", tankTarget)) + return botAI->CastSpell("misdirection", tankTarget); + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", flame)) + { + return botAI->CastSpell("steady shot", flame); + } + + return false; +} + +bool IllidanStormrageMisdirectToTankAction::TryMisdirectToWarlockTank(Unit* illidan) +{ + Player* warlockTank = GetIllidanWarlockTank(bot); + if (!warlockTank) + return false; + + if (botAI->CanCastSpell("misdirection", warlockTank)) + return botAI->CastSpell("misdirection", warlockTank); + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", illidan)) + { + return botAI->CastSpell("steady shot", illidan); + } + + return false; +} + +bool IllidanStormrageMainTankRepositionBossAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + if (AI_VALUE(Unit*, "current target") != illidan) + return Attack(illidan); + + if (GetIllidanPhase(illidan) == 5) + { + GameObject* trap = FindNearestTrap(botAI, bot); + if (trap && bot->GetExactDist2d(trap) < 40.0f && illidan->GetVictim() == bot) + return MoveToShadowTrap(trap); + } + else + { + illidanShadowTrapGuid.erase(bot->GetGUID()); + illidanShadowTrapDestination.erase(bot->GetGUID()); + } + + if (illidan->GetVictim() != bot) + { + illidanShadowTrapGuid.erase(bot->GetGUID()); + illidanShadowTrapDestination.erase(bot->GetGUID()); + return false; + } + + auto const& flameCrashes = GetAllFlameCrashes(bot); + if (flameCrashes.empty()) + return false; + + constexpr float hazardRadius = 12.0f; + bool inDanger = false; + for (Unit* flameCrash : flameCrashes) + { + if (bot->GetDistance2d(flameCrash) < hazardRadius) + { + inDanger = true; + break; + } + } + + if (!inDanger) + return false; + + constexpr float maxRadius = 30.0f; + Position safestPos = FindSafestNearbyPosition(flameCrashes, maxRadius, hazardRadius); + + return MoveTo(BLACK_TEMPLE_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, true); +} + +bool IllidanStormrageMainTankRepositionBossAction::MoveToShadowTrap(GameObject* trap) +{ + if (!trap) + return false; + + ObjectGuid const botGuid = bot->GetGUID(); + ObjectGuid const trapGuid = trap->GetGUID(); + Position target; + + auto const cachedTrapIt = illidanShadowTrapGuid.find(botGuid); + auto const cachedDestinationIt = illidanShadowTrapDestination.find(botGuid); + if (cachedTrapIt != illidanShadowTrapGuid.end() && + cachedDestinationIt != illidanShadowTrapDestination.end() && + cachedTrapIt->second == trapGuid) + { + target = cachedDestinationIt->second; + } + else + { + const float trapX = trap->GetPositionX(); + const float trapY = trap->GetPositionY(); + + const float distToTrap = trap->GetExactDist2d(bot); + if (distToTrap <= 0.0f) + return false; + + constexpr float distBeyondTrap = 4.0f; + + const float dx = trapX - bot->GetPositionX(); + const float dy = trapY - bot->GetPositionY(); + const float targetX = trapX + (dx / distToTrap) * distBeyondTrap; + const float targetY = trapY + (dy / distToTrap) * distBeyondTrap; + + target = Position(targetX, targetY, trap->GetPositionZ()); + illidanShadowTrapGuid[botGuid] = trapGuid; + illidanShadowTrapDestination[botGuid] = target; + } + + const float targetDist = bot->GetExactDist2d(target); + + if (targetDist > 2.0f && bot->GetHealthPct() > 50.0f) + { + const float dX = target.GetPositionX() - bot->GetPositionX(); + const float dY = target.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(5.0f, targetDist); + const float moveX = bot->GetPositionX() + (dX / targetDist) * moveDist; + const float moveY = bot->GetPositionY() + (dY / targetDist) * moveDist; + + return MoveTo(BLACK_TEMPLE_MAP_ID, moveX, moveY, trap->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + + return false; +} + +Position IllidanStormrageMainTankRepositionBossAction::FindSafestNearbyPosition( + const std::vector& flameCrashes, float maxRadius, float hazardRadius) +{ + constexpr float searchStep = M_PI / 16.0f; + constexpr float minDistance = 2.0f; + constexpr float distanceStep = 1.0f; + + float backwardsAngle = Position::NormalizeOrientation(bot->GetOrientation() + M_PI); + + Position bestPos; + float bestAngleDiff = M_PI * 2.0f; + float bestDistance = std::numeric_limits::max(); + bool foundSafe = false; + + for (float distance = minDistance; distance <= maxRadius; distance += distanceStep) + { + for (float angleOffset = 0.0f; angleOffset < 2 * M_PI; angleOffset += searchStep) + { + for (int sign = -1; sign <= 1; sign += 2) + { + const float testAngle = + Position::NormalizeOrientation(backwardsAngle + sign * angleOffset); + const float x = bot->GetPositionX() + distance * std::cos(testAngle); + const float y = bot->GetPositionY() + distance * std::sin(testAngle); + + Position testPos(x, y, bot->GetPositionZ()); + + bool isSafe = true; + for (Unit* flameCrash : flameCrashes) + { + if (flameCrash->GetDistance2d(x, y) < hazardRadius) + { + isSafe = false; + break; + } + } + if (!isSafe) + continue; + + bool pathSafe = IsPathSafeFromFlameCrashes(bot->GetPosition(), testPos, + flameCrashes, hazardRadius); + + float angleDiff = std::abs(Position::NormalizeOrientation( + testAngle - backwardsAngle)); + if (angleDiff > M_PI) + angleDiff = 2 * M_PI - angleDiff; + + if (pathSafe && (!foundSafe || angleDiff < bestAngleDiff || + (angleDiff == bestAngleDiff && distance < bestDistance))) + { + bestPos = testPos; + bestAngleDiff = angleDiff; + bestDistance = distance; + foundSafe = true; + } + else if (!foundSafe && angleDiff < bestAngleDiff) + { + bestPos = testPos; + bestAngleDiff = angleDiff; + bestDistance = distance; + } + } + if (foundSafe) + break; + } + if (foundSafe) + break; + } + + return bestPos; +} + +bool IllidanStormrageMainTankRepositionBossAction::IsPathSafeFromFlameCrashes( + const Position& start, const Position& end, const std::vector& flameCrashes, + float hazardRadius) +{ + constexpr uint8 numChecks = 10; + float dx = end.GetPositionX() - start.GetPositionX(); + float dy = end.GetPositionY() - start.GetPositionY(); + + for (uint8 i = 1; i <= numChecks; ++i) + { + float ratio = static_cast(i) / numChecks; + float checkX = start.GetPositionX() + dx * ratio; + float checkY = start.GetPositionY() + dy * ratio; + + for (Unit* flameCrash : flameCrashes) + { + float distToFlameCrash = flameCrash->GetDistance2d(checkX, checkY); + if (distToFlameCrash < hazardRadius) + return false; + } + } + + return true; +} + +bool IllidanStormrageIsolateBotWithParasiteAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + int phase = GetIllidanPhase(illidan); + + if (phase == 1) + { + constexpr float safeDistance = 15.0f; + if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) + { + const float currentDistance = bot->GetExactDist2d(nearestPlayer); + if (currentDistance < safeDistance) + return MoveAway(nearestPlayer, safeDistance - currentDistance); + } + } + else + { + const float angle = illidan->GetOrientation() + M_PI; + constexpr float distBehindIllidan = 35.0f; + + const float targetX = illidan->GetPositionX() + std::cos(angle) * distBehindIllidan; + const float targetY = illidan->GetPositionY() + std::sin(angle) * distBehindIllidan; + const Position target(targetX, targetY, bot->GetPositionZ()); + + if (HasParasiticShadowfiend(bot)) + return InfectedBotMoveFromGroup(illidan, target); + + if (GetIllidanTrapperHunter(bot) == bot) + return FreezeTrapShadowfiend(bot, illidan, target); + } + + return false; +} + +bool IllidanStormrageIsolateBotWithParasiteAction::InfectedBotMoveFromGroup( + Unit* illidan, const Position& target) +{ + if (bot->GetExactDist2d(target) < 1.0f) + return false; + + return MoveTo(BLACK_TEMPLE_MAP_ID, target.GetPositionX(), target.GetPositionY(), + target.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); +} + +bool IllidanStormrageIsolateBotWithParasiteAction::FreezeTrapShadowfiend( + Player* bot, Unit* illidan, const Position& target) +{ + if (bot->HasSpellCooldown(static_cast(BlackTempleSpells::SPELL_FROST_TRAP))) + return false; + + Player* infected = GetBotWithParasiticShadowfiend(bot); + if (!infected) + return false; + + if (bot->GetExactDist2d(target) > 2.0f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, target.GetPositionX(), target.GetPositionY(), + target.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + else if (bot->GetExactDist2d(infected) < 2.0f && + botAI->CanCastSpell(static_cast(BlackTempleSpells::SPELL_FROST_TRAP), bot)) + { + return botAI->CastSpell(static_cast(BlackTempleSpells::SPELL_FROST_TRAP), bot); + } + + return false; +} + +bool IllidanStormrageSetEarthbindTotemAction::Execute(Event /*event*/) +{ + return botAI->CanCastSpell("earthbind totem", bot) && + botAI->CastSpell("earthbind totem", bot); +} + +bool IllidanStormrageAssistTanksHandleFlamesOfAzzinothAction::Execute(Event /*event*/) +{ + auto [eastFlame, westFlame] = GetFlamesOfAzzinoth(bot); + // The second assist tank's flame is killed first; this is so that if the tank + // for the second flame dies after the first flame is down, the dead flame's + // tank will become the first assist tank and take over the remaining flame + if (botAI->IsAssistTankOfIndex(bot, 1, true)) + { + if (eastFlame && westFlame) + { + if (AI_VALUE(Unit*, "current target") != eastFlame) + return Attack(eastFlame); + + if (eastFlame->GetVictim() != bot) + { + if (!bot->IsWithinMeleeRange(eastFlame)) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, eastFlame->GetPositionX(), + eastFlame->GetPositionY(), eastFlame->GetPositionZ(), + false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + return false; + } + } + else if (!eastFlame && !westFlame) + { + // (1) Before flames spawn, go to the waiting position + // (2) If both flames are dead and the waiting position is too close to hazards, + // move to a grate position + std::list demonFires; + constexpr float searchRadius = 40.0f; + bot->GetCreatureListWithEntryInGrid( + demonFires, static_cast(BlackTempleNpcs::NPC_DEMON_FIRE), searchRadius); + + const Position& pos = demonFires.empty() ? + ILLIDAN_E_GLAIVE_WAITING_POSITION : ILLIDAN_E_GRATE_POSITION; + + if (bot->GetExactDist2d(pos.GetPositionX(), pos.GetPositionY()) > 0.5f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, pos.GetPositionX(), pos.GetPositionY(), + pos.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + // After the first flame dies, its tank waits with other bots + else if (!eastFlame && westFlame) + { + const Position& pos = ILLIDAN_E_GRATE_POSITION; + if (bot->GetExactDist2d(pos.GetPositionX(), pos.GetPositionY()) > 0.5f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, pos.GetPositionX(), pos.GetPositionY(), + pos.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + } + else if (botAI->IsAssistTankOfIndex(bot, 0, true)) + { + if (westFlame) + { + if (AI_VALUE(Unit*, "current target") != westFlame) + return Attack(westFlame); + + if (westFlame->GetVictim() != bot) + { + if (!bot->IsWithinMeleeRange(westFlame)) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, westFlame->GetPositionX(), + westFlame->GetPositionY(), westFlame->GetPositionZ(), + false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + return false; + } + } + else + { + // (1) Before flames spawn, go to the waiting position + // (2) If both flames are dead and the waiting position is too close to hazards, + // move to a grate position + std::list demonFires; + constexpr float searchRadius = 40.0f; + bot->GetCreatureListWithEntryInGrid( + demonFires, static_cast(BlackTempleNpcs::NPC_DEMON_FIRE), searchRadius); + + const Position& pos = demonFires.empty() ? + ILLIDAN_W_GLAIVE_WAITING_POSITION : ILLIDAN_W_GRATE_POSITION; + + if (bot->GetExactDist2d(pos.GetPositionX(), pos.GetPositionY()) > 0.5f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, pos.GetPositionX(), pos.GetPositionY(), + pos.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + } + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + EyeBlastDangerArea dangerArea = GetEyeBlastDangerArea(bot); + + // Only consider the eye blast if its trigger NPC is within 30 yards of the tank + constexpr float eyeBlastTriggerRadius = 30.0f; + if (dangerArea.width > 0.0f && + bot->GetExactDist2d(dangerArea.start) <= eyeBlastTriggerRadius) + { + return RepositionToAvoidEyeBlast(illidan, dangerArea); + } + else + { + return RepositionToAvoidBlaze(eastFlame, westFlame); + } + + return false; +} + +bool IllidanStormrageAssistTanksHandleFlamesOfAzzinothAction::RepositionToAvoidEyeBlast( + Unit* illidan, const EyeBlastDangerArea& dangerArea) +{ + if (!IsPositionInEyeBlastDangerArea(bot->GetPosition(), dangerArea)) + return false; + + const float dx = dangerArea.end.GetPositionX() - dangerArea.start.GetPositionX(); + const float dy = dangerArea.end.GetPositionY() - dangerArea.start.GetPositionY(); + const float length = dangerArea.start.GetExactDist2d(dangerArea.end); + + const float px = bot->GetPositionX(); + const float py = bot->GetPositionY(); + const float sx = dangerArea.start.GetPositionX(); + const float sy = dangerArea.start.GetPositionY(); + + const float projection = std::clamp( + ((px - sx) * dx + (py - sy) * dy) / (length * length), 0.0f, 1.0f); + const float closestX = sx + projection * dx; + const float closestY = sy + projection * dy; + + const float distToLine = bot->GetExactDist2d(closestX, closestY); + const float moveDist = (dangerArea.width - distToLine) + 0.5f; + if (moveDist <= 0.0f) + return false; + + const float rawDirX = px - closestX; + const float rawDirY = py - closestY; + const float rawDirLength = std::sqrt(rawDirX * rawDirX + rawDirY * rawDirY); + const float dirX = rawDirLength == 0.0f ? -(dy / length) : rawDirX / rawDirLength; + const float dirY = rawDirLength == 0.0f ? dx / length : rawDirY / rawDirLength; + + const float safeX = px + dirX * moveDist; + const float safeY = py + dirY * moveDist; + const float safeZ = bot->GetPositionZ(); + const Position safePosition(safeX, safeY, safeZ); + + constexpr float minGrateDistance = 10.0f; + const bool tooCloseToNorthGrate = + safePosition.GetExactDist2d(ILLIDAN_N_GRATE_POSITION) < minGrateDistance; + const bool tooCloseToEastGrate = + safePosition.GetExactDist2d(ILLIDAN_E_GRATE_POSITION) < minGrateDistance; + const bool tooCloseToWestGrate = + safePosition.GetExactDist2d(ILLIDAN_W_GRATE_POSITION) < minGrateDistance; + + if (tooCloseToNorthGrate || tooCloseToEastGrate || tooCloseToWestGrate) + return false; + + return MoveTo(BLACK_TEMPLE_MAP_ID, safeX, safeY, safeZ, false, false, false, + false, MovementPriority::MOVEMENT_FORCED, true, false); +} + +bool IllidanStormrageAssistTanksHandleFlamesOfAzzinothAction::RepositionToAvoidBlaze( + Unit* eastFlame, Unit* westFlame) +{ + const std::array* waypoints = nullptr; + constexpr size_t numWaypoints = 7; + + if (botAI->IsAssistTankOfIndex(bot, 1, true)) + { + if (!eastFlame || eastFlame->GetVictim() != bot || + !bot->IsWithinMeleeRange(eastFlame)) + { + return false; + } + waypoints = &E_GLAIVE_TANK_POSITIONS; + } + else if (botAI->IsAssistTankOfIndex(bot, 0, true)) + { + if (!westFlame || westFlame->GetVictim() != bot || + !bot->IsWithinMeleeRange(westFlame)) + { + return false; + } + waypoints = &W_GLAIVE_TANK_POSITIONS; + } + + if (!waypoints) + return false; + + size_t& waypointIndex = flameTankWaypointIndex[bot->GetGUID()]; + const Position& target = (*waypoints)[waypointIndex]; + + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("possible triggers")->Get(); + + bool blazeNearby = false; + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->GetEntry() == static_cast(BlackTempleNpcs::NPC_BLAZE) && + bot->GetDistance2d(unit) <= 8.0f) + { + blazeNearby = true; + break; + } + } + + float distToPosition = + bot->GetExactDist2d(target.GetPositionX(), target.GetPositionY()); + if (blazeNearby && distToPosition <= 0.2f) + { + waypointIndex = (waypointIndex + 1) % numWaypoints; + const Position& newTarget = (*waypoints)[waypointIndex]; + const float distToNewPosition = + bot->GetExactDist2d(newTarget.GetPositionX(), newTarget.GetPositionY()); + + if (distToNewPosition > 0.2f) + { + const float dX = newTarget.GetPositionX() - bot->GetPositionX(); + const float dY = newTarget.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(5.0f, distToNewPosition); + const float moveX = bot->GetPositionX() + (dX / distToNewPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToNewPosition) * moveDist; + + return MoveTo(BLACK_TEMPLE_MAP_ID, newTarget.GetPositionX(), newTarget.GetPositionY(), + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + else if (distToPosition > 0.2f) + { + const float dX = target.GetPositionX() - bot->GetPositionX(); + const float dY = target.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(3.0f, distToPosition); + const float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(BLACK_TEMPLE_MAP_ID, target.GetPositionX(), target.GetPositionY(), + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, true); + } + + return false; +} + +// Pets grab aggro right away during Phase 2 and wipe the raid if not put on passive +// Just like players, pets cannot melee Illidan during Phase 4 +bool IllidanStormrageControlPetAggressionAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + Pet* pet = bot->GetPet(); + if (!pet) + return false; + + int phase = GetIllidanPhase(illidan); + + if ((phase == 2 || phase == 4) && + pet->GetReactState() != REACT_PASSIVE) + { + pet->AttackStop(); + pet->SetReactState(REACT_PASSIVE); + } + else if (pet->GetReactState() == REACT_PASSIVE) + { + pet->SetReactState(REACT_DEFENSIVE); + } + + return false; +} + +bool IllidanStormragePositionAboveGrateAction::Execute(Event /*event*/) +{ + const std::array& gratePositions = GRATE_POSITIONS; + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector bots; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && !botAI->IsAssistTankOfIndex(member, 0, true) && + !botAI->IsAssistTankOfIndex(member, 1, true)) + { + bots.push_back(member); + } + } + + if (bots.empty()) + return false; + + std::sort(bots.begin(), bots.end(), + [](Player* a, Player* b) { return a->GetGUID() < b->GetGUID(); }); + + auto it = std::find(bots.begin(), bots.end(), bot); + if (it == bots.end()) + return false; + + const size_t botIndex = std::distance(bots.begin(), it); + const uint8 index = botIndex % gratePositions.size(); + + const Position& position = gratePositions[index]; + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 0.2f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool IllidanStormrageRemoveDarkBarrageAction::Execute(Event /*event*/) +{ + switch (bot->getClass()) + { + case CLASS_MAGE: + return botAI->CanCastSpell("ice block", bot) && + botAI->CastSpell("ice block", bot); + + case CLASS_PALADIN: + return botAI->CanCastSpell("divine shield", bot) && + botAI->CastSpell("divine shield", bot); + + case CLASS_ROGUE: + return botAI->CanCastSpell("cloak of shadows", bot) && + botAI->CastSpell("cloak of shadows", bot); + + default: + return false; + } +} + +bool IllidanStormrageMoveAwayFromLandingPointAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + constexpr float safeDistance = 20.0f; + const float currentDistance = bot->GetExactDist2d(illidan); + if (currentDistance < safeDistance) + return MoveAway(illidan, safeDistance - currentDistance); + + return false; +} + +// NOTE: Illidan's bounding radius is 0.459f, and combatreach is 7.5f +bool IllidanStormrageDisperseRangedAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + int phase = GetIllidanPhase(illidan); + + if (phase == 4) + { + return SpreadInCircleInDemonPhase(illidan, group); + } + else if (GetBotWithParasiticShadowfiend(bot) == bot || + (GetIllidanTrapperHunter(bot) == bot && + GetBotWithParasiticShadowfiend(bot))) + { + return false; + } + else + { + return FanOutBehindInHumanPhase(illidan, group); + } +} + +bool IllidanStormrageDisperseRangedAction::FanOutBehindInHumanPhase( + Unit* illidan, Group* group) +{ + auto const& flameCrashes = GetAllFlameCrashes(bot); + + std::vector healers; + std::vector rangedDps; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !botAI->IsRanged(member)) + continue; + + if (botAI->IsHeal(member)) + healers.push_back(member); + else + rangedDps.push_back(member); + } + + constexpr float arcSpan = M_PI; + const float arcCenter = illidan->GetOrientation() + M_PI; + const float arcStart = arcCenter - arcSpan / 2.0f; + + const float radius = botAI->IsHeal(bot) ? 18.0f : 25.0f; + auto& bots = botAI->IsHeal(bot) ? healers : rangedDps; + const size_t count = bots.size(); + auto findIt = std::find(bots.begin(), bots.end(), bot); + const size_t botIndex = (findIt != bots.end()) ? + std::distance(bots.begin(), findIt) : 0; + + const float angle = (count == 1) ? arcCenter : + (arcStart + arcSpan * static_cast(botIndex) / + static_cast(count - 1)); + + const float targetX = illidan->GetPositionX() + radius * std::cos(angle); + const float targetY = illidan->GetPositionY() + radius * std::sin(angle); + + constexpr float hazardRadius = 12.0f; + bool safe = true; + for (Unit* flameCrash : flameCrashes) + { + if (flameCrash->GetDistance2d(targetX, targetY) < hazardRadius) + { + safe = false; + break; + } + } + + if (!safe) + return false; + + if (bot->GetExactDist2d(targetX, targetY) > 1.0f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, targetX, targetY, bot->GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_COMBAT, + true, false); + } + + return false; +} + +bool IllidanStormrageDisperseRangedAction::SpreadInCircleInDemonPhase( + Unit* illidan, Group* group) +{ + Player* warlockTank = GetIllidanWarlockTank(bot); + if (!warlockTank) + { + constexpr float safeDistFromBoss = 24.0f; + if (bot->GetExactDist2d(illidan) < safeDistFromBoss) + { + constexpr uint32 minInterval = 0; + if (FleePosition(illidan->GetPosition(), safeDistFromBoss, minInterval)) + return true; + } + + constexpr float safeDistFromPlayer = 6.0f; + constexpr uint32 minInterval = 1000; + if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistFromPlayer)) + return FleePosition(nearestPlayer->GetPosition(), safeDistFromPlayer, minInterval); + + return false; + } + + std::vector rangedBots; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !botAI->IsRanged(member)) + continue; + + rangedBots.push_back(member); + } + + if (rangedBots.empty()) + return false; + + const size_t count = rangedBots.size(); + auto findIt = std::find(rangedBots.begin(), rangedBots.end(), bot); + const size_t botIndex = (findIt != rangedBots.end()) ? + std::distance(rangedBots.begin(), findIt) : 0; + + const float dx = warlockTank->GetPositionX() - illidan->GetPositionX(); + const float dy = warlockTank->GetPositionY() - illidan->GetPositionY(); + const float warlockAngle = std::atan2(dy, dx); + + constexpr float forbiddenArc = (2.0f / 3.0f) * M_PI; + constexpr float allowedArc = (4.0f / 3.0f) * M_PI; + + const float arcStart = Position::NormalizeOrientation(warlockAngle + forbiddenArc / 2.0f); + constexpr float radius = 25.0f; + + const float angle = (count == 1) ? + Position::NormalizeOrientation(arcStart + allowedArc / 2.0f) : + Position::NormalizeOrientation( + arcStart + allowedArc * static_cast(botIndex) / + static_cast(count - 1)); + + const float targetX = illidan->GetPositionX() + radius * std::cos(angle); + const float targetY = illidan->GetPositionY() + radius * std::sin(angle); + + if (bot->GetExactDist2d(targetX, targetY) > 1.0f) + { + if (MoveTo(BLACK_TEMPLE_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false)) + { + return true; + } + else + { + constexpr float safeDistFromTank = 25.0f; + const float currentDistFromTank = bot->GetExactDist2d(warlockTank); + if (currentDistFromTank < safeDistFromTank) + return MoveAway(warlockTank, safeDistFromTank - currentDistFromTank); + } + } + + return false; +} + +// Melee cannot attack Demon Form Illidan +bool IllidanStormrageMeleeGoSomewhereToNotDieAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + constexpr float demonSearchRadius = 25.0f; + constexpr float shadowfiendSearchRadius = 15.0f; + + Unit* illidanVictim = illidan->GetVictim(); + // But they can attack Shadow Demons and Shadowfiends, if far enough from Illidan + Unit* shadowDemon = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_SHADOW_DEMON), demonSearchRadius, true); + + if (shadowDemon && shadowDemon->GetDistance2d(illidan) > 15.0f && + (!illidanVictim || shadowDemon->GetDistance2d(illidanVictim) > 24.0f)) + { + return false; + } + else + { + Unit* shadowfiend = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_PARASITIC_SHADOWFIEND), + shadowfiendSearchRadius, true); + + if (shadowfiend && shadowfiend->GetDistance2d(illidan) > 15.0f && + shadowfiend->GetHealthPct() < 30.0f && + (!illidanVictim || shadowfiend->GetDistance2d(illidanVictim) > 24.0f)) + { + return false; + } + } + + // 30y is closer than ideal but is a compromise to allow melee to reach targets in time + constexpr float safeDistFromBoss = 30.0f; + const float currentDistFromBoss = bot->GetExactDist2d(illidan); + if (currentDistFromBoss < safeDistFromBoss) + MoveAway(illidan, safeDistFromBoss - currentDistFromBoss); + + if (Player* warlockTank = GetIllidanWarlockTank(bot)) + { + constexpr float safeDistFromTank = 25.0f; + const float currentDistFromTank = bot->GetExactDist2d(warlockTank); + if (currentDistFromTank < safeDistFromTank) + MoveAway(warlockTank, safeDistFromTank - currentDistFromTank); + } + + constexpr float safeDistFromPlayer = 6.0f; + if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistFromPlayer)) + MoveAway(nearestPlayer, safeDistFromPlayer - bot->GetDistance2d(nearestPlayer)); + + return true; +} + +bool IllidanStormrageWarlockTankHandleDemonBossAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + constexpr float safeDistance = 24.0f; + const float currentDistance = bot->GetExactDist2d(illidan); + if (currentDistance < safeDistance && + MoveAway(illidan, safeDistance - currentDistance)) + { + return true; + } + + if (botAI->CanCastSpell("shadow ward", bot) && + botAI->CastSpell("shadow ward", bot)) + { + return true; + } + + if (botAI->CanCastSpell("searing pain", illidan)) + return botAI->CastSpell("searing pain", illidan); + + return false; +} + +bool IllidanStormrageDpsPrioritizeAddsAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + int phase = GetIllidanPhase(illidan); + + std::vector targets; + + if (phase == 4) + { + constexpr float searchRadius = 35.0f; + + Unit* shadowDemon = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_SHADOW_DEMON), searchRadius, true); + + if (GetIllidanWarlockTank(bot) == bot) + { + targets = { shadowDemon, illidan }; + } + else + { + Unit* shadowfiend = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_PARASITIC_SHADOWFIEND), + searchRadius, true); + + if (botAI->IsRanged(bot)) + { + if (shadowDemon) + targets = { shadowDemon }; + else if (shadowfiend && bot->GetDistance2d(shadowfiend) > 10.0f) + targets = { shadowfiend }; + else + targets = { illidan }; + } + else if (botAI->IsMelee(bot)) + { + targets = { shadowDemon, shadowfiend }; + } + } + } + else if (botAI->IsRanged(bot)) + { + if (phase == 1 || phase == 3 || phase == 5) + { + constexpr float searchRadius = 35.0f; + Unit* shadowfiend = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_PARASITIC_SHADOWFIEND), + searchRadius, true); + + if (shadowfiend && bot->GetDistance2d(shadowfiend) > 10.0f) + targets = { shadowfiend }; + else + targets = { illidan }; + } + else if (phase == 2) + { + constexpr float searchRadius = 20.0f; + Unit* shadowfiend = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_PARASITIC_SHADOWFIEND), + searchRadius, true); + + if (shadowfiend && bot->GetDistance2d(shadowfiend) > 5.0f) + { + targets = { shadowfiend }; + } + else + { + auto [eastFlame, westFlame] = GetFlamesOfAzzinoth(bot); + targets = { eastFlame, westFlame }; + } + } + } + + for (Unit* candidate : targets) + { + if (candidate && candidate->IsAlive()) + { + if (AI_VALUE(Unit*, "current target") != candidate) + return Attack(candidate); + + return false; + } + } + + return false; +} + +bool IllidanStormrageUseShadowTrapAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + GameObject* trap = FindNearestTrap(botAI, bot); + if (!trap || illidan->GetExactDist2d(trap) >= 4.0f) + return false; + + if (bot->GetExactDist2d(trap) < 3.0f) + { + trap->Use(bot); + return true; + } + else + { + return MoveTo(BLACK_TEMPLE_MAP_ID, trap->GetPositionX(), trap->GetPositionY(), + trap->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool IllidanStormrageManageDpsTimerAndRtiAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + const time_t now = std::time(nullptr); + const uint32 instanceId = illidan->GetMap()->GetInstanceId(); + + bool updated = false; + const int phase = GetIllidanPhase(illidan); + int lastPhase = -1; + if (auto const it = illidanLastPhase.find(instanceId); it != illidanLastPhase.end()) + lastPhase = it->second; + const bool phaseChanged = lastPhase != phase; + illidanLastPhase[instanceId] = phase; + + if (phaseChanged) + { + if (phase == 1 || phase == 3 || phase == 4 || phase == 5) + { + illidanBossDpsWaitTimer[instanceId] = now; + updated = true; + } + else if (phase == 2) + { + if (illidanBossDpsWaitTimer.erase(instanceId) > 0) + updated = true; + } + + if (phase != 2 && illidanFlameDpsWaitTimer.erase(instanceId) > 0) + updated = true; + } + + if (phase == 2) + { + if (eastFlameGuid.find(instanceId) == eastFlameGuid.end() && + westFlameGuid.find(instanceId) == westFlameGuid.end()) + { + std::list creatureList; + constexpr float searchRadius = 50.0f; + illidan->GetCreatureListWithEntryInGrid(creatureList, static_cast( + BlackTempleNpcs::NPC_FLAME_OF_AZZINOTH), searchRadius); + + std::vector flames; + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + flames.push_back(creature); + } + + if (flames.size() == 2) + { + const float eastDist0 = + flames[0]->GetExactDist2d(ILLIDAN_E_GLAIVE_WAITING_POSITION); + const float eastDist1 = + flames[1]->GetExactDist2d(ILLIDAN_E_GLAIVE_WAITING_POSITION); + + if (eastDist0 < eastDist1) + { + eastFlameGuid[instanceId] = flames[0]->GetGUID(); + westFlameGuid[instanceId] = flames[1]->GetGUID(); + } + else + { + eastFlameGuid[instanceId] = flames[1]->GetGUID(); + westFlameGuid[instanceId] = flames[0]->GetGUID(); + } + + illidanFlameDpsWaitTimer[instanceId] = now; + + updated = true; + } + } + } + else + { + if (eastFlameGuid.erase(instanceId) > 0) + updated = true; + if (westFlameGuid.erase(instanceId) > 0) + updated = true; + } + + return updated; +} + +bool IllidanStormrageDestroyHazardsAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + int phase = GetIllidanPhase(illidan); + constexpr float searchRadius = 50.0f; + std::list hazards; + std::vector entries; + + if (phase == 2) + { + entries = { + static_cast(BlackTempleNpcs::NPC_FLAME_CRASH) + }; + } + else if (phase == 4) + { + entries = { + static_cast(BlackTempleNpcs::NPC_FLAME_CRASH) + }; + } + else if (phase == 0) + { + entries = { + static_cast(BlackTempleNpcs::NPC_DEMON_FIRE), + static_cast(BlackTempleNpcs::NPC_BLAZE) + }; + } + + if (!entries.empty()) + bot->GetCreatureListWithEntryInGrid(hazards, entries, searchRadius); + + for (Creature* creature : hazards) + { + if (creature && creature->IsAlive()) + { + creature->Kill(bot, creature); + return true; + } + } + + return false; +} + +// Reduce Shadow Demon to 25% health and kill residual Shadowfiends in Phase 2 +bool IllidanStormrageHandleAddsCheatAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + if (GetIllidanPhase(illidan) == 2) + { + constexpr float searchRadius = 20.0f; + if (Unit* shadowfiend = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_PARASITIC_SHADOWFIEND), + searchRadius, true)) + { + shadowfiend->Kill(bot, shadowfiend); + return true; + } + } + else + { + constexpr float searchRadius = 75.0f; + Unit* shadowDemon = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_SHADOW_DEMON), searchRadius, true); + + if (shadowDemon && shadowDemon->GetHealthPct() > 25.0f) + { + uint32 desiredDamage = 0; + const uint32 quarterHealth = shadowDemon->GetMaxHealth() / 4; + if (shadowDemon->GetHealth() > quarterHealth) + desiredDamage = shadowDemon->GetHealth() - quarterHealth; + + Unit::DealDamage(bot, shadowDemon, desiredDamage, nullptr, DIRECT_DAMAGE, + SPELL_SCHOOL_MASK_NORMAL, nullptr, false, false, nullptr); + return true; + } + } + + return false; +} diff --git a/src/Ai/Raid/BlackTemple/Action/RaidBlackTempleActions.h b/src/Ai/Raid/BlackTemple/Action/RaidBlackTempleActions.h new file mode 100644 index 000000000..d4bc41d23 --- /dev/null +++ b/src/Ai/Raid/BlackTemple/Action/RaidBlackTempleActions.h @@ -0,0 +1,565 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_RAIDBLACKTEMPLEACTIONS_H +#define _PLAYERBOT_RAIDBLACKTEMPLEACTIONS_H + +#include "Action.h" +#include "AttackAction.h" +#include "MovementActions.h" +#include "RaidBlackTempleHelpers.h" + +namespace BlackTempleHelpers +{ + struct EyeBlastDangerArea; +} + +// General + +class BlackTempleEraseTimersAndTrackersAction : public Action +{ +public: + BlackTempleEraseTimersAndTrackersAction( + PlayerbotAI* botAI) : Action(botAI, "black temple erase timers and trackers") {} + bool Execute(Event event) override; +}; + +// High Warlord Naj'entus + +class HighWarlordNajentusMisdirectBossToMainTankAction : public AttackAction +{ +public: + HighWarlordNajentusMisdirectBossToMainTankAction( + PlayerbotAI* botAI) : AttackAction(botAI, "high warlord naj'entus misdirect boss to main tank") {} + bool Execute(Event event) override; +}; + +class HighWarlordNajentusTanksPositionBossAction : public AttackAction +{ +public: + HighWarlordNajentusTanksPositionBossAction( + PlayerbotAI* botAI) : AttackAction(botAI, "high warlord naj'entus tanks position boss") {} + bool Execute(Event event) override; +}; + +class HighWarlordNajentusDisperseRangedAction : public MovementAction +{ +public: + HighWarlordNajentusDisperseRangedAction( + PlayerbotAI* botAI) : MovementAction(botAI, "naj'entus disperse ranged") {} + bool Execute(Event event) override; +}; + +class HighWarlordNajentusRemoveImpalingSpineAction : public MovementAction +{ +public: + HighWarlordNajentusRemoveImpalingSpineAction( + PlayerbotAI* botAI) : MovementAction(botAI, "high warlord naj'entus remove impaling spine") {} + bool Execute(Event event) override; +}; + +class HighWarlordNajentusThrowImpalingSpineAction : public MovementAction +{ +public: + HighWarlordNajentusThrowImpalingSpineAction( + PlayerbotAI* botAI) : MovementAction(botAI, "high warlord naj'entus throw impaling spine") {} + bool Execute(Event event) override; +}; + +// Supremus + +class SupremusMisdirectBossToMainTankAction : public AttackAction +{ +public: + SupremusMisdirectBossToMainTankAction( + PlayerbotAI* botAI) : AttackAction(botAI, "supremus misdirect boss to main tank") {} + bool Execute(Event event) override; +}; + +class SupremusDisperseRangedAction : public MovementAction +{ +public: + SupremusDisperseRangedAction( + PlayerbotAI* botAI) : MovementAction(botAI, "supremus disperse ranged") {} + bool Execute(Event event) override; +}; + +class SupremusKiteBossAction : public MovementAction +{ +public: + SupremusKiteBossAction( + PlayerbotAI* botAI) : MovementAction(botAI, "supremus kite boss") {} + bool Execute(Event event) override; +}; + +class SupremusMoveAwayFromVolcanosAction : public MovementAction +{ +public: + SupremusMoveAwayFromVolcanosAction( + PlayerbotAI* botAI) : MovementAction(botAI, "supremus move away from volcanos") {} + bool Execute(Event event) override; + +private: + Position FindSafestNearbyPosition( + const std::vector& volcanos, float maxRadius, float hazardRadius); + bool IsPathSafeFromVolcanos(const Position& start, + const Position& end, const std::vector& volcanos, float hazardRadius); + std::vector GetAllSupremusVolcanos(); +}; + +class SupremusManagePhaseTimerAction : public Action +{ +public: + SupremusManagePhaseTimerAction( + PlayerbotAI* botAI) : Action(botAI, "supremus manage phase timer") {} + bool Execute(Event event) override; +}; + +// Shade of Akama + +class ShadeOfAkamaMeleeDpsPrioritizeChannelersAction : public AttackAction +{ +public: + ShadeOfAkamaMeleeDpsPrioritizeChannelersAction( + PlayerbotAI* botAI) : AttackAction(botAI, "shade of akama melee dps prioritize channelers") {} + bool Execute(Event event) override; +}; + +// Teron Gorefiend + +class TeronGorefiendMisdirectBossToMainTankAction : public AttackAction +{ +public: + TeronGorefiendMisdirectBossToMainTankAction( + PlayerbotAI* botAI) : AttackAction(botAI, "teron gorefiend misdirect boss to main tank") {} + bool Execute(Event event) override; +}; + +class TeronGorefiendTanksPositionBossAction : public AttackAction +{ +public: + TeronGorefiendTanksPositionBossAction( + PlayerbotAI* botAI) : AttackAction(botAI, "teron gorefiend tanks position boss") {} + bool Execute(Event event) override; +}; + +class TeronGorefiendPositionRangedOnBalconyAction : public MovementAction +{ +public: + TeronGorefiendPositionRangedOnBalconyAction( + PlayerbotAI* botAI) : MovementAction(botAI, "teron gorefiend position ranged on balcony") {} + bool Execute(Event event) override; +}; + +class TeronGorefiendAvoidShadowOfDeathAction : public Action +{ +public: + TeronGorefiendAvoidShadowOfDeathAction( + PlayerbotAI* botAI) : Action(botAI, "teron gorefiend avoid shadow of death") {} + bool Execute(Event event) override; +}; + +class TeronGorefiendMoveToCornerToDieAction : public MovementAction +{ +public: + TeronGorefiendMoveToCornerToDieAction( + PlayerbotAI* botAI) : MovementAction(botAI, "teron gorefiend move to corner to die") {} + bool Execute(Event event) override; +}; + +class TeronGorefiendControlAndDestroyShadowyConstructsAction : public MovementAction +{ +public: + TeronGorefiendControlAndDestroyShadowyConstructsAction( + PlayerbotAI* botAI) : MovementAction(botAI, "teron gorefiend control and destroy shadowy constructs") {} + bool Execute(Event event) override; +}; + +// Gurtogg Bloodboil + +class GurtoggBloodboilMisdirectBossToMainTankAction : public AttackAction +{ +public: + GurtoggBloodboilMisdirectBossToMainTankAction( + PlayerbotAI* botAI) : AttackAction(botAI, "gurtogg bloodboil misdirect boss to main tank") {} + bool Execute(Event event) override; +}; + +class GurtoggBloodboilTanksPositionBossAction : public AttackAction +{ +public: + GurtoggBloodboilTanksPositionBossAction( + PlayerbotAI* botAI) : AttackAction(botAI, "gurtogg bloodboil tanks position boss") {} + bool Execute(Event event) override; +}; + +class GurtoggBloodboilRotateRangedGroupsAction : public MovementAction +{ +public: + GurtoggBloodboilRotateRangedGroupsAction( + PlayerbotAI* botAI) : MovementAction(botAI, "gurtogg bloodboil rotate ranged groups") {} + bool Execute(Event event) override; +}; + +class GurtoggBloodboilRangedMoveAwayFromEnragedPlayerAction : public MovementAction +{ +public: + GurtoggBloodboilRangedMoveAwayFromEnragedPlayerAction( + PlayerbotAI* botAI) : MovementAction(botAI, "gurtogg bloodboil ranged move away from enraged player") {} + bool Execute(Event event) override; +}; + +class GurtoggBloodboilManagePhaseTimerAction : public Action +{ +public: + GurtoggBloodboilManagePhaseTimerAction( + PlayerbotAI* botAI) : Action(botAI, "gurtogg bloodboil manage phase timer") {} + bool Execute(Event event) override; +}; + +// Reliquary of Souls + +class ReliquaryOfSoulsMisdirectBossToMainTankAction : public AttackAction +{ +public: + ReliquaryOfSoulsMisdirectBossToMainTankAction( + PlayerbotAI* botAI) : AttackAction(botAI, "reliquary of souls misdirect boss to main tank") {} + bool Execute(Event event) override; +}; + +class ReliquaryOfSoulsAdjustDistanceFromSufferingAction : public MovementAction +{ +public: + ReliquaryOfSoulsAdjustDistanceFromSufferingAction( + PlayerbotAI* botAI) : MovementAction(botAI, "reliquary of souls adjust distance from suffering") {} + bool Execute(Event event) override; + +private: + bool TanksMoveToMinimumRange(Unit* suffering); + bool MeleeDpsStayAtMaximumRange(Unit* suffering); + bool RangedMoveAwayFromBoss(Unit* suffering); +}; + +class ReliquaryOfSoulsHealersDpsSufferingAction : public Action +{ +public: + ReliquaryOfSoulsHealersDpsSufferingAction( + PlayerbotAI* botAI) : Action(botAI, "reliquary of souls healers dps suffering") {} + bool Execute(Event event) override; +}; + +class ReliquaryOfSoulsSpellstealRuneShieldAction : public Action +{ +public: + ReliquaryOfSoulsSpellstealRuneShieldAction( + PlayerbotAI* botAI) : Action(botAI, "reliquary of souls spellsteal rune shield") {} + bool Execute(Event event) override; +}; + +class ReliquaryOfSoulsSpellReflectDeadenAction : public Action +{ +public: + ReliquaryOfSoulsSpellReflectDeadenAction( + PlayerbotAI* botAI) : Action(botAI, "reliquary of souls spell reflect deaden") {} + bool Execute(Event event) override; +}; + +// Mother Shahraz + +class MotherShahrazMisdirectBossToMainTankAction : public AttackAction +{ +public: + MotherShahrazMisdirectBossToMainTankAction( + PlayerbotAI* botAI) : AttackAction(botAI, "mother shahraz misdirect boss to main tank") {} + bool Execute(Event event) override; +}; + +class MotherShahrazTanksPositionBossUnderPillarAction : public AttackAction +{ +public: + MotherShahrazTanksPositionBossUnderPillarAction( + PlayerbotAI* botAI) : AttackAction(botAI, "mother shahraz tanks position boss under pillar") {} + bool Execute(Event event) override; +}; + +class MotherShahrazMeleeDpsWaitAtSafePositionAction : public MovementAction +{ +public: + MotherShahrazMeleeDpsWaitAtSafePositionAction( + PlayerbotAI* botAI) : MovementAction(botAI, "mother shahraz melee dps wait at safe position") {} + bool Execute(Event event) override; +}; + +class MotherShahrazPositionRangedUnderPillarAction : public MovementAction +{ +public: + MotherShahrazPositionRangedUnderPillarAction( + PlayerbotAI* botAI) : MovementAction(botAI, "mother shahraz position ranged under pillar") {} + bool Execute(Event event) override; +}; + +class MotherShahrazRunAwayToBreakFatalAttractionAction : public MovementAction +{ +public: + MotherShahrazRunAwayToBreakFatalAttractionAction( + PlayerbotAI* botAI) : MovementAction(botAI, "mother shahraz run away to break fatal attraction") {} + bool Execute(Event event) override; + +private: + std::vector GetAttractedPlayers(); +}; + +// Illidari Council + +class IllidariCouncilMisdirectBossesToTanksAction : public AttackAction +{ +public: + IllidariCouncilMisdirectBossesToTanksAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidari council misdirect bosses to tanks") {} + bool Execute(Event event) override; +}; + +class IllidariCouncilMainTankPositionGathiosAction : public AttackAction +{ +public: + IllidariCouncilMainTankPositionGathiosAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidari council main tank position gathios") {} + bool Execute(Event event) override; +}; + +class IllidariCouncilMainTankReflectJudgementOfCommandAction : public Action +{ +public: + IllidariCouncilMainTankReflectJudgementOfCommandAction( + PlayerbotAI* botAI) : Action(botAI, "illidari council main tank reflect judgement of command") {} + bool Execute(Event event) override; +}; + +class IllidariCouncilFirstAssistTankFocusMalandeAction : public AttackAction +{ +public: + IllidariCouncilFirstAssistTankFocusMalandeAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidari council first assist tank focus malande") {} + bool Execute(Event event) override; +}; + +class IllidariCouncilSecondAssistTankPositionDarkshadowAction : public AttackAction +{ +public: + IllidariCouncilSecondAssistTankPositionDarkshadowAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidari council second assist tank position darkshadow") {} + bool Execute(Event event) override; +}; + +class IllidariCouncilMageTankPositionZerevorAction : public AttackAction +{ +public: + IllidariCouncilMageTankPositionZerevorAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidari council mage tank position zerevor") {} + bool Execute(Event event) override; +}; + +class IllidariCouncilPositionMageTankHealerAction : public AttackAction +{ +public: + IllidariCouncilPositionMageTankHealerAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidari council position mage tank healer") {} + bool Execute(Event event) override; +}; + +class IllidariCouncilAssignDpsTargetsAction : public AttackAction +{ +public: + IllidariCouncilAssignDpsTargetsAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidari council assign dps targets") {} + bool Execute(Event event) override; +}; + +class IllidariCouncilDisperseRangedAction : public MovementAction +{ +public: + IllidariCouncilDisperseRangedAction( + PlayerbotAI* botAI) : MovementAction(botAI, "illidari council disperse ranged") {} + bool Execute(Event event) override; +}; + +class IllidariCouncilCommandPetsToAttackGathiosAction : public AttackAction +{ +public: + IllidariCouncilCommandPetsToAttackGathiosAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidari council command pets to attack gathios") {} + bool Execute(Event event) override; +}; + +class IllidariCouncilManageDpsTimerAction : public Action +{ +public: + IllidariCouncilManageDpsTimerAction( + PlayerbotAI* botAI) : Action(botAI, "illidari council manage dps timer") {} + bool Execute(Event event) override; +}; + +// Illidan Stormrage + +class IllidanStormrageMisdirectToTankAction : public AttackAction +{ +public: + IllidanStormrageMisdirectToTankAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidan stormrage misdirect to tank") {} + bool Execute(Event event) override; + +private: + bool TryMisdirectToFlameTanks(Group* group); + bool TryMisdirectToWarlockTank(Unit* illidan); +}; + +class IllidanStormrageMainTankRepositionBossAction : public AttackAction +{ +public: + IllidanStormrageMainTankRepositionBossAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidan stormrage main tank reposition boss") {} + bool Execute(Event event) override; + +private: + bool MoveToShadowTrap(GameObject* trap); + Position FindSafestNearbyPosition( + const std::vector& flameCrashes, float maxRadius, float hazardRadius); + bool IsPathSafeFromFlameCrashes(const Position& start, + const Position& end, const std::vector& flameCrashes, float hazardRadius); +}; + +class IllidanStormrageIsolateBotWithParasiteAction : public MovementAction +{ +public: + IllidanStormrageIsolateBotWithParasiteAction( + PlayerbotAI* botAI) : MovementAction(botAI, "illidan stormrage isolate bot with parasite") {} + bool Execute(Event event) override; + +private: + bool InfectedBotMoveFromGroup(Unit* illidan, const Position& targetPos); + bool FreezeTrapShadowfiend(Player* bot, Unit* illidan, const Position& targetPos); +}; + +class IllidanStormrageSetEarthbindTotemAction : public Action +{ +public: + IllidanStormrageSetEarthbindTotemAction( + PlayerbotAI* botAI) : Action(botAI, "illidan stormrage set earthbind totem") {} + bool Execute(Event event) override; +}; + +class IllidanStormrageAssistTanksHandleFlamesOfAzzinothAction : public AttackAction +{ +public: + IllidanStormrageAssistTanksHandleFlamesOfAzzinothAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidan stormrage assist tanks handle flames of azzinoth") {} + bool Execute(Event event) override; + +private: + bool RepositionToAvoidEyeBlast(Unit* illidan, const BlackTempleHelpers::EyeBlastDangerArea& dangerArea); + bool RepositionToAvoidBlaze(Unit* eastFlame, Unit* westFlame); +}; + +class IllidanStormrageControlPetAggressionAction : public Action +{ +public: + IllidanStormrageControlPetAggressionAction( + PlayerbotAI* botAI) : Action(botAI, "illidan stormrage control pet aggression") {} + bool Execute(Event event) override; +}; + +class IllidanStormragePositionAboveGrateAction : public MovementAction +{ +public: + IllidanStormragePositionAboveGrateAction( + PlayerbotAI* botAI) : MovementAction(botAI, "illidan stormrage position above grate") {} + bool Execute(Event event) override; +}; + +class IllidanStormrageRemoveDarkBarrageAction : public Action +{ +public: + IllidanStormrageRemoveDarkBarrageAction( + PlayerbotAI* botAI) : Action(botAI, "illidan stormrage remove dark barrage") {} + bool Execute(Event event) override; +}; + +class IllidanStormrageMoveAwayFromLandingPointAction : public MovementAction +{ +public: + IllidanStormrageMoveAwayFromLandingPointAction( + PlayerbotAI* botAI) : MovementAction(botAI, "illidan stormrage move away from landing point") {} + bool Execute(Event event) override; +}; + +class IllidanStormrageDisperseRangedAction : public MovementAction +{ +public: + IllidanStormrageDisperseRangedAction( + PlayerbotAI* botAI) : MovementAction(botAI, "illidan stormrage disperse ranged") {} + bool Execute(Event event) override; + +private: + bool FanOutBehindInHumanPhase(Unit* illidan, Group* group); + bool SpreadInCircleInDemonPhase(Unit* illidan, Group* group); +}; + +class IllidanStormrageMeleeGoSomewhereToNotDieAction : public MovementAction +{ +public: + IllidanStormrageMeleeGoSomewhereToNotDieAction( + PlayerbotAI* botAI) : MovementAction(botAI, "illidan stormrage melee go somewhere to not die") {} + bool Execute(Event event) override; +}; + +class IllidanStormrageWarlockTankHandleDemonBossAction : public AttackAction +{ +public: + IllidanStormrageWarlockTankHandleDemonBossAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidan stormrage warlock tank handle demon boss") {} + bool Execute(Event event) override; +}; + +class IllidanStormrageDpsPrioritizeAddsAction : public AttackAction +{ +public: + IllidanStormrageDpsPrioritizeAddsAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidan stormrage dps prioritize adds") {} + bool Execute(Event event) override; +}; + +class IllidanStormrageUseShadowTrapAction : public MovementAction +{ +public: + IllidanStormrageUseShadowTrapAction( + PlayerbotAI* botAI) : MovementAction(botAI, "illidan stormrage use shadow trap") {} + bool Execute(Event event) override; +}; + +class IllidanStormrageManageDpsTimerAndRtiAction : public Action +{ +public: + IllidanStormrageManageDpsTimerAndRtiAction( + PlayerbotAI* botAI) : Action(botAI, "illidan stormrage manage dps timer and rti") {} + bool Execute(Event event) override; +}; + +class IllidanStormrageDestroyHazardsAction : public Action +{ +public: + IllidanStormrageDestroyHazardsAction( + PlayerbotAI* botAI) : Action(botAI, "illidan stormrage destroy hazards") {} + bool Execute(Event event) override; +}; + +class IllidanStormrageHandleAddsCheatAction : public Action +{ +public: + IllidanStormrageHandleAddsCheatAction( + PlayerbotAI* botAI) : Action(botAI, "illidan stormrage handle adds cheat") {} + bool Execute(Event event) override; +}; + +#endif diff --git a/src/Ai/Raid/BlackTemple/Multiplier/RaidBlackTempleMultipliers.cpp b/src/Ai/Raid/BlackTemple/Multiplier/RaidBlackTempleMultipliers.cpp new file mode 100644 index 000000000..94e5143a6 --- /dev/null +++ b/src/Ai/Raid/BlackTemple/Multiplier/RaidBlackTempleMultipliers.cpp @@ -0,0 +1,785 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "RaidBlackTempleMultipliers.h" + +#include "ChooseTargetActions.h" +#include "DKActions.h" +#include "DruidActions.h" +#include "DruidBearActions.h" +#include "DruidShapeshiftActions.h" +#include "FollowActions.h" +#include "HunterActions.h" +#include "MageActions.h" +#include "PaladinActions.h" +#include "PriestActions.h" +#include "RaidBlackTempleActions.h" +#include "RaidBlackTempleHelpers.h" +#include "ReachTargetActions.h" +#include "RogueActions.h" +#include "ShamanActions.h" +#include "WarlockActions.h" +#include "WarriorActions.h" +#include "WipeAction.h" + +using namespace BlackTempleHelpers; + +static bool IsDpsCooldownAction(Action* action) +{ + return dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action); +} + +// High Warlord Naj'entus + +float HighWarlordNajentusDelayDpsCooldownsMultiplier::GetValue(Action* action) +{ + Unit* najentus = AI_VALUE2(Unit*, "find target", "high warlord naj'entus"); + if (!najentus || najentus->GetHealthPct() < 95.0f) + return 1.0f; + + if (IsDpsCooldownAction(action) || + (botAI->IsDps(bot) && dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +float HighWarlordNajentusDisableCombatFormationMoveMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "high warlord naj'entus")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +// Supremus + +float SupremusDelayDpsCooldownsMultiplier::GetValue(Action* action) +{ + Unit* supremus = AI_VALUE2(Unit*, "find target", "supremus"); + if (!supremus || supremus->GetHealthPct() < 95.0f) + return 1.0f; + + if (IsDpsCooldownAction(action) || + (botAI->IsDps(bot) && dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +float SupremusFocusOnAvoidanceInPhase2Multiplier::GetValue(Action* action) +{ + Unit* supremus = AI_VALUE2(Unit*, "find target", "supremus"); + if (!supremus || supremus->GetVictim() != bot || + !supremus->HasAura(static_cast(BlackTempleSpells::SPELL_SNARE_SELF))) + { + return 1.0f; + } + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +float SupremusHitboxIsBuggedMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_ROGUE || + !AI_VALUE2(Unit*, "find target", "supremus")) + { + return 1.0f; + } + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Teron Gorefiend + +float TeronGorefiendDelayDpsCooldownsMultiplier::GetValue(Action* action) +{ + Unit* gorefiend = AI_VALUE2(Unit*, "find target", "teron gorefiend"); + if (!gorefiend || gorefiend->GetHealthPct() < 95.0f) + return 1.0f; + + if (IsDpsCooldownAction(action) || + (botAI->IsDps(bot) && dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +float TeronGorefiendControlMovementMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "teron gorefiend")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + { + return 0.0f; + } + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + if (botAI->IsRanged(bot) && dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float TeronGorefiendMarkedBotOnlyMoveToDieMultiplier::GetValue(Action* action) +{ + Aura* aura = bot->GetAura( + static_cast(BlackTempleSpells::SPELL_SHADOW_OF_DEATH)); + if (!aura || aura->GetDuration() >= 15000) + return 1.0f; + + if (dynamic_cast(action)) + return 1.0f; + else if (!dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float TeronGorefiendSpiritsAttackOnlyShadowyConstructsMultiplier::GetValue(Action* action) +{ + if (!bot->HasAura(static_cast(BlackTempleSpells::SPELL_SPIRITUAL_VENGEANCE)) || + dynamic_cast(action)) + { + return 1.0f; + } + + if (!dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float TeronGorefiendDisableAttackingConstructsMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "teron gorefiend")) + return 1.0f; + + if (bot->GetVictim() != nullptr && + dynamic_cast(action)) + { + return 0.0f; + } + + if (!botAI->IsRangedDps(bot)) + return 1.0f; + + auto castSpellAction = dynamic_cast(action); + if (castSpellAction && + castSpellAction->getThreatType() == Action::ActionThreatType::Aoe) + { + return 0.0f; + } + + return 1.0f; +} + +// Gurtogg Bloodboil + +float GurtoggBloodboilDelayDpsCooldownsMultiplier::GetValue(Action* action) +{ + Unit* gurtogg = AI_VALUE2(Unit*, "find target", "gurtogg bloodboil"); + if (!gurtogg || gurtogg->GetHealthPct() < 95.0f) + return 1.0f; + + if (IsDpsCooldownAction(action) || + (botAI->IsDps(bot) && dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +float GurtoggBloodboilControlMovementMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "gurtogg bloodboil")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + { + return 0.0f; + } + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_PLAYER_FEL_RAGE)) && + (dynamic_cast(action) && + !dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +// Reliquary of Souls + +float ReliquaryOfSoulsDelayDpsCooldownsMultiplier::GetValue(Action* action) +{ + Unit* suffering = AI_VALUE2(Unit*, "find target", "essence of suffering"); + if (!suffering || suffering->GetHealthPct() < 95.0f) + return 1.0f; + + if (IsDpsCooldownAction(action) || + (botAI->IsDps(bot) && dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +float ReliquaryOfSoulsDontWasteHealingMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "essence of suffering")) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + { + return 1.0f; + } + + if (dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +// Mother Shahraz + +float MotherShahrazDelayDpsCooldownsMultiplier::GetValue(Action* action) +{ + Unit* shahraz = AI_VALUE2(Unit*, "find target", "mother shahraz"); + if (!shahraz || shahraz->GetHealthPct() < 90.0f) + return 1.0f; + + if (IsDpsCooldownAction(action) || + (botAI->IsDps(bot) && dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +float MotherShahrazControlMovementMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "mother shahraz")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + { + return 0.0f; + } + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +float MotherShahrazBotsWithFatalAttractionOnlyRunAwayMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "mother shahraz") || + !bot->HasAura(static_cast(BlackTempleSpells::SPELL_FATAL_ATTRACTION))) + { + return 1.0f; + } + + if (dynamic_cast(action)) + return 1.0f; + + if (!dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Illidari Council + +float IllidariCouncilDelayDpsCooldownsMultiplier::GetValue(Action* action) +{ + Unit* gathios = AI_VALUE2(Unit*, "find target", "gathios the shatterer"); + if (!gathios || gathios->GetHealthPct() < 90.0f) + return 1.0f; + + if (IsDpsCooldownAction(action) || + (botAI->IsDps(bot) && dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +float IllidariCouncilDisableTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot) || + !AI_VALUE2(Unit*, "find target", "gathios the shatterer")) + { + return 1.0f; + } + + if (bot->GetVictim() != nullptr && dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +float IllidariCouncilControlMovementMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "high nethermancer zerevor")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + { + return 0.0f; + } + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + if (botAI->IsAssistHealOfIndex(bot, 0, true) && + (dynamic_cast(action) && + !dynamic_cast(action))) + { + return 0.0f; + } + + if (!botAI->IsAssistTankOfIndex(bot, 0, false) && + dynamic_cast(action)) + { + return 0.0f; + } + + if ((botAI->IsMainTank(bot) || + botAI->IsAssistTankOfIndex(bot, 0, false) || + botAI->IsAssistTankOfIndex(bot, 1, false) || + GetZerevorMageTank(bot) == bot) && + dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +float IllidariCouncilControlMisdirectionMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_HUNTER || + !AI_VALUE2(Unit*, "find target", "high nethermancer zerevor")) + { + return 1.0f; + } + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float IllidariCouncilDisableIceBlockMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_MAGE || + !AI_VALUE2(Unit*, "find target", "high nethermancer zerevor")) + { + return 1.0f; + } + + if (GetZerevorMageTank(bot) != bot) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float IllidariCouncilDisableArcaneShotOnZerevorMultiplier::GetValue(Action* action) +{ + Unit* zerevor = AI_VALUE2(Unit*, "find target", "high nethermancer zerevor"); + if (!zerevor) + return 1.0f; + + Unit* target = AI_VALUE(Unit*, "current target"); + if (!target || target->GetGUID() != zerevor->GetGUID()) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float IllidariCouncilWaitForDpsMultiplier::GetValue(Action* action) +{ + if (botAI->IsMainTank(bot) || + botAI->IsAssistTankOfIndex(bot, 0, false) || + botAI->IsAssistTankOfIndex(bot, 1, false) || + GetZerevorMageTank(bot) == bot) + { + return 1.0f; + } + + Unit* gathios = AI_VALUE2(Unit*, "find target", "gathios the shatterer"); + if (!gathios) + return 1.0f; + + if (dynamic_cast(action)) + return 1.0f; + + const time_t now = std::time(nullptr); + constexpr uint8 dpsWaitSeconds = 5; + + auto it = councilDpsWaitTimer.find(gathios->GetMap()->GetInstanceId()); + if (it == councilDpsWaitTimer.end() || (now - it->second) >= dpsWaitSeconds) + return 1.0f; + + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +// Illidan Stormrage + +float IllidanStormrageDelayDpsCooldownsMultiplier::GetValue(Action* action) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return 1.0f; + + if (illidan->GetHealthPct() > 62.0f && + (dynamic_cast(action) || + dynamic_cast(action))) + { + return 0.0f; + } + + if (illidan->GetHealthPct() <= 62.0f || illidan->GetHealthPct() > 95.0f) + return 1.0f; + + if (IsDpsCooldownAction(action) || + (botAI->IsDps(bot) && dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +float IllidanStormrageControlTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->GetHealth() == 1) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (GetIllidanPhase(illidan) != 2) + return 1.0f; + + if (botAI->IsMainTank(bot)) + { + if (dynamic_cast(action) && + !dynamic_cast(action)) + { + return 0.0f; + } + + if (dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + } + else if (botAI->IsAssistTankOfIndex(bot, 0, false) || + botAI->IsAssistTankOfIndex(bot, 1, false)) + { + if (dynamic_cast(action) && + !dynamic_cast(action)) + { + return 0.0f; + } + + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float IllidanStormrageDisableDefaultTargetingMultiplier::GetValue(Action* action) +{ + if (bot->GetVictim() == nullptr) + return 1.0f; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->GetHealth() == 1) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + int phase = GetIllidanPhase(illidan); + + if (phase == 4 && dynamic_cast(action)) + return 0.0f; + + if (botAI->IsRangedDps(bot)) + { + if (phase != 2) + context->GetValue("neglect threat")->Set(true); + + if (dynamic_cast(action)) + return 0.0f; + } + + constexpr float searchRadius = 40.0f; + Unit* shadowDemon = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_SHADOW_DEMON), searchRadius); + Unit* shadowfiend = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_PARASITIC_SHADOWFIEND), searchRadius); + + if (((shadowDemon && bot->GetTarget() == shadowDemon->GetGUID()) || + (shadowfiend && bot->GetTarget() == shadowfiend->GetGUID())) && + dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +float IllidanStormrageControlNonTankMovementMultiplier::GetValue(Action* action) +{ + if (botAI->IsTank(bot)) + return 1.0f; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->GetHealth() == 1) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + { + return 0.0f; + } + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + int phase = GetIllidanPhase(illidan); + + if (phase == 2 && + (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action))) + { + return 0.0f; + } + + if (phase == 4 && botAI->IsHeal(bot) && + dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +float IllidanStormrageUseEarthbindTotemMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_SHAMAN) + return 1.0f; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || GetIllidanPhase(illidan) == 2) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +float IllidanStormrageWaitForDpsMultiplier::GetValue(Action* action) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return 1.0f; + + if (dynamic_cast(action)) + return 1.0f; + + const time_t now = std::time(nullptr); + const uint32 instanceId = illidan->GetMap()->GetInstanceId(); + + int phase = GetIllidanPhase(illidan); + + if ((phase == 1 || phase == 3 || phase == 5) && + !botAI->IsMainTank(bot)) + { + constexpr uint8 humanoidPhaseDpsWaitSeconds = 3; + auto it = illidanBossDpsWaitTimer.find(instanceId); + + if ((it == illidanBossDpsWaitTimer.end() || + (now - it->second) < humanoidPhaseDpsWaitSeconds) && + (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action)))) + { + return 0.0f; + } + } + + if (phase == 4 && GetIllidanWarlockTank(bot) != bot) + { + constexpr uint8 demonPhaseDpsWaitSeconds = 8; + auto it = illidanBossDpsWaitTimer.find(instanceId); + + if ((it == illidanBossDpsWaitTimer.end() || + (now - it->second) < demonPhaseDpsWaitSeconds) && + (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action)))) + { + return 0.0f; + } + } + + if (AI_VALUE2(Unit*, "find target", "flame of azzinoth") && + !botAI->IsAssistTankOfIndex(bot, 0, true) && + !botAI->IsAssistTankOfIndex(bot, 1, true)) + { + constexpr uint8 flamePhaseDpsWaitSeconds = 6; + auto it = illidanFlameDpsWaitTimer.find(instanceId); + + if ((it == illidanFlameDpsWaitTimer.end() || + (now - it->second) < flamePhaseDpsWaitSeconds) && + (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action)))) + { + return 0.0f; + } + } + + return 1.0f; +} diff --git a/src/Ai/Raid/BlackTemple/Multiplier/RaidBlackTempleMultipliers.h b/src/Ai/Raid/BlackTemple/Multiplier/RaidBlackTempleMultipliers.h new file mode 100644 index 000000000..ba82e0100 --- /dev/null +++ b/src/Ai/Raid/BlackTemple/Multiplier/RaidBlackTempleMultipliers.h @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_RAIDBLACKTEMPLEMULTIPLIERS_H +#define _PLAYERBOT_RAIDBLACKTEMPLEMULTIPLIERS_H + +#include "Multiplier.h" + +// High Warlord Naj'entus + +class HighWarlordNajentusDelayDpsCooldownsMultiplier : public Multiplier +{ +public: + HighWarlordNajentusDelayDpsCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "high warlord naj'entus delay dps cooldowns multiplier") {} + virtual float GetValue(Action* action); +}; + +class HighWarlordNajentusDisableCombatFormationMoveMultiplier : public Multiplier +{ +public: + HighWarlordNajentusDisableCombatFormationMoveMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "high warlord naj'entus disable combat formation move multiplier") {} + virtual float GetValue(Action* action); +}; + +// Supremus + +class SupremusDelayDpsCooldownsMultiplier : public Multiplier +{ +public: + SupremusDelayDpsCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "supremus delay dps cooldowns multiplier") {} + virtual float GetValue(Action* action); +}; + +class SupremusFocusOnAvoidanceInPhase2Multiplier : public Multiplier +{ +public: + SupremusFocusOnAvoidanceInPhase2Multiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "supremus focus on avoidance in phase 2 multiplier") {} + virtual float GetValue(Action* action); +}; + +class SupremusHitboxIsBuggedMultiplier : public Multiplier +{ +public: + SupremusHitboxIsBuggedMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "supremus hitbox is bugged multiplier") {} + virtual float GetValue(Action* action); +}; + +// Teron Gorefiend + +class TeronGorefiendDelayDpsCooldownsMultiplier : public Multiplier +{ +public: + TeronGorefiendDelayDpsCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "teron gorefiend delay dps cooldowns multiplier") {} + virtual float GetValue(Action* action); +}; + +class TeronGorefiendControlMovementMultiplier : public Multiplier +{ +public: + TeronGorefiendControlMovementMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "teron gorefiend control movement multiplier") {} + virtual float GetValue(Action* action); +}; + +class TeronGorefiendMarkedBotOnlyMoveToDieMultiplier : public Multiplier +{ +public: + TeronGorefiendMarkedBotOnlyMoveToDieMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "teron gorefiend marked bot only move to die multiplier") {} + virtual float GetValue(Action* action); +}; + +class TeronGorefiendSpiritsAttackOnlyShadowyConstructsMultiplier : public Multiplier +{ +public: + TeronGorefiendSpiritsAttackOnlyShadowyConstructsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "teron gorefiend spirits attack only shadowy constructs multiplier") {} + virtual float GetValue(Action* action); +}; + +class TeronGorefiendDisableAttackingConstructsMultiplier : public Multiplier +{ +public: + TeronGorefiendDisableAttackingConstructsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "teron gorefiend disable attacking constructs multiplier") {} + virtual float GetValue(Action* action); +}; + +// Gurtogg Bloodboil + +class GurtoggBloodboilDelayDpsCooldownsMultiplier : public Multiplier +{ +public: + GurtoggBloodboilDelayDpsCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "gurtogg bloodboil delay dps cooldowns multiplier") {} + virtual float GetValue(Action* action); +}; + +class GurtoggBloodboilControlMovementMultiplier : public Multiplier +{ +public: + GurtoggBloodboilControlMovementMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "gurtogg bloodboil control movement multiplier") {} + virtual float GetValue(Action* action); +}; + +// Reliquary of Souls + +class ReliquaryOfSoulsDelayDpsCooldownsMultiplier : public Multiplier +{ +public: + ReliquaryOfSoulsDelayDpsCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "reliquary of souls delay dps cooldowns multiplier") {} + virtual float GetValue(Action* action); +}; + +class ReliquaryOfSoulsDontWasteHealingMultiplier : public Multiplier +{ +public: + ReliquaryOfSoulsDontWasteHealingMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "reliquary of souls don't waste healing multiplier") {} + virtual float GetValue(Action* action); +}; + +// Mother Shahraz + +class MotherShahrazDelayDpsCooldownsMultiplier : public Multiplier +{ +public: + MotherShahrazDelayDpsCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "mother shahraz delay dps cooldowns multiplier") {} + virtual float GetValue(Action* action); +}; + +class MotherShahrazControlMovementMultiplier : public Multiplier +{ +public: + MotherShahrazControlMovementMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "mother shahraz control movement multiplier") {} + virtual float GetValue(Action* action); +}; + +class MotherShahrazBotsWithFatalAttractionOnlyRunAwayMultiplier : public Multiplier +{ +public: + MotherShahrazBotsWithFatalAttractionOnlyRunAwayMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "mother shahraz bots with fatal attraction only run away multiplier") {} + virtual float GetValue(Action* action); +}; + +// Illidari Council + +class IllidariCouncilDelayDpsCooldownsMultiplier : public Multiplier +{ +public: + IllidariCouncilDelayDpsCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidari council delay dps cooldowns multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidariCouncilDisableTankActionsMultiplier : public Multiplier +{ +public: + IllidariCouncilDisableTankActionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidari council disable tank actions multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidariCouncilControlMovementMultiplier : public Multiplier +{ +public: + IllidariCouncilControlMovementMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidari council control movement multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidariCouncilControlMisdirectionMultiplier : public Multiplier +{ +public: + IllidariCouncilControlMisdirectionMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidari council control misdirection multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidariCouncilDisableArcaneShotOnZerevorMultiplier : public Multiplier +{ +public: + IllidariCouncilDisableArcaneShotOnZerevorMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidari council disable arcane shot on zerevor multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidariCouncilDisableIceBlockMultiplier : public Multiplier +{ +public: + IllidariCouncilDisableIceBlockMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidari council disable ice block multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidariCouncilWaitForDpsMultiplier : public Multiplier +{ +public: + IllidariCouncilWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidari council wait for dps multiplier") {} + virtual float GetValue(Action* action); +}; + +// Illidan Stormrage + +class IllidanStormrageDelayDpsCooldownsMultiplier : public Multiplier +{ +public: + IllidanStormrageDelayDpsCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidan stormrage delay dps cooldowns multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidanStormrageControlTankActionsMultiplier : public Multiplier +{ +public: + IllidanStormrageControlTankActionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidan stormrage control tank actions multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidanStormrageDisableDefaultTargetingMultiplier : public Multiplier +{ +public: + IllidanStormrageDisableDefaultTargetingMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidan stormrage disable default targeting multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidanStormrageControlNonTankMovementMultiplier : public Multiplier +{ +public: + IllidanStormrageControlNonTankMovementMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidan stormrage control non-tank movement multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidanStormrageUseEarthbindTotemMultiplier : public Multiplier +{ +public: + IllidanStormrageUseEarthbindTotemMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidan stormrage use earthbind totem multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidanStormrageWaitForDpsMultiplier : public Multiplier +{ +public: + IllidanStormrageWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidan stormrage wait for dps multiplier") {} + virtual float GetValue(Action* action); +}; + +#endif diff --git a/src/Ai/Raid/BlackTemple/RaidBlackTempleActionContext.h b/src/Ai/Raid/BlackTemple/RaidBlackTempleActionContext.h new file mode 100644 index 000000000..8271449b5 --- /dev/null +++ b/src/Ai/Raid/BlackTemple/RaidBlackTempleActionContext.h @@ -0,0 +1,406 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_RAIDBLACKTEMPLEACTIONCONTEXT_H +#define _PLAYERBOT_RAIDBLACKTEMPLEACTIONCONTEXT_H + +#include "NamedObjectContext.h" +#include "RaidBlackTempleActions.h" + +class RaidBlackTempleActionContext : public NamedObjectContext +{ +public: + RaidBlackTempleActionContext() + { + // General + creators["black temple erase timers and trackers"] = + &RaidBlackTempleActionContext::black_temple_erase_timers_and_trackers; + + // High Warlord Naj'entus + creators["high warlord naj'entus misdirect boss to main tank"] = + &RaidBlackTempleActionContext::high_warlord_najentus_misdirect_boss_to_main_tank; + + creators["high warlord naj'entus tanks position boss"] = + &RaidBlackTempleActionContext::high_warlord_najentus_tanks_position_boss; + + creators["high warlord naj'entus disperse ranged"] = + &RaidBlackTempleActionContext::high_warlord_najentus_disperse_ranged; + + creators["high warlord naj'entus remove impaling spine"] = + &RaidBlackTempleActionContext::high_warlord_najentus_remove_impaling_spine; + + creators["high warlord naj'entus throw impaling spine"] = + &RaidBlackTempleActionContext::high_warlord_najentus_throw_impaling_spine; + + // Supremus + creators["supremus misdirect boss to main tank"] = + &RaidBlackTempleActionContext::supremus_misdirect_boss_to_main_tank; + + creators["supremus disperse ranged"] = + &RaidBlackTempleActionContext::supremus_disperse_ranged; + + creators["supremus kite boss"] = + &RaidBlackTempleActionContext::supremus_kite_boss; + + creators["supremus move away from volcanos"] = + &RaidBlackTempleActionContext::supremus_move_away_from_volcanos; + + creators["supremus manage phase timer"] = + &RaidBlackTempleActionContext::supremus_manage_phase_timer; + + // Shade of Akama + creators["shade of akama melee dps prioritize channelers"] = + &RaidBlackTempleActionContext::shade_of_akama_melee_dps_prioritize_channelers; + + // Teron Gorefiend + creators["teron gorefiend misdirect boss to main tank"] = + &RaidBlackTempleActionContext::teron_gorefiend_misdirect_boss_to_main_tank; + + creators["teron gorefiend tanks position boss"] = + &RaidBlackTempleActionContext::teron_gorefiend_tanks_position_boss; + + creators["teron gorefiend position ranged on balcony"] = + &RaidBlackTempleActionContext::teron_gorefiend_position_ranged_on_balcony; + + creators["teron gorefiend avoid shadow of death"] = + &RaidBlackTempleActionContext::teron_gorefiend_avoid_shadow_of_death; + + creators["teron gorefiend move to corner to die"] = + &RaidBlackTempleActionContext::teron_gorefiend_move_to_corner_to_die; + + creators["teron gorefiend control and destroy shadowy constructs"] = + &RaidBlackTempleActionContext::teron_gorefiend_control_and_destroy_shadowy_constructs; + + // Gurtogg Bloodboil + creators["gurtogg bloodboil misdirect boss to main tank"] = + &RaidBlackTempleActionContext::gurtogg_bloodboil_misdirect_boss_to_main_tank; + + creators["gurtogg bloodboil tanks position boss"] = + &RaidBlackTempleActionContext::gurtogg_bloodboil_tanks_position_boss; + + creators["gurtogg bloodboil rotate ranged groups"] = + &RaidBlackTempleActionContext::gurtogg_bloodboil_rotate_ranged_groups; + + creators["gurtogg bloodboil ranged move away from enraged player"] = + &RaidBlackTempleActionContext::gurtogg_bloodboil_ranged_move_away_from_enraged_player; + + creators["gurtogg bloodboil manage phase timer"] = + &RaidBlackTempleActionContext::gurtogg_bloodboil_manage_phase_timer; + + // Reliquary of Souls + creators["reliquary of souls misdirect boss to main tank"] = + &RaidBlackTempleActionContext::reliquary_of_souls_misdirect_boss_to_main_tank; + + creators["reliquary of souls adjust distance from suffering"] = + &RaidBlackTempleActionContext::reliquary_of_souls_adjust_distance_from_suffering; + + creators["reliquary of souls healers dps suffering"] = + &RaidBlackTempleActionContext::reliquary_of_souls_healers_dps_suffering; + + creators["reliquary of souls spellsteal rune shield"] = + &RaidBlackTempleActionContext::reliquary_of_souls_spellsteal_rune_shield; + + creators["reliquary of souls spell reflect deaden"] = + &RaidBlackTempleActionContext::reliquary_of_souls_spell_reflect_deaden; + + // Mother Shahraz + creators["mother shahraz misdirect boss to main tank"] = + &RaidBlackTempleActionContext::mother_shahraz_misdirect_boss_to_main_tank; + + creators["mother shahraz tanks position boss under pillar"] = + &RaidBlackTempleActionContext::mother_shahraz_tanks_position_boss_under_pillar; + + creators["mother shahraz melee dps wait at safe position"] = + &RaidBlackTempleActionContext::mother_shahraz_melee_dps_wait_at_safe_position; + + creators["mother shahraz position ranged under pillar"] = + &RaidBlackTempleActionContext::mother_shahraz_position_ranged_under_pillar; + + creators["mother shahraz run away to break fatal attraction"] = + &RaidBlackTempleActionContext::mother_shahraz_run_away_to_break_fatal_attraction; + + // Illidari Council + creators["illidari council misdirect bosses to tanks"] = + &RaidBlackTempleActionContext::illidari_council_misdirect_bosses_to_tanks; + + creators["illidari council main tank position gathios"] = + &RaidBlackTempleActionContext::illidari_council_main_tank_position_gathios; + + creators["illidari council main tank reflect judgement of command"] = + &RaidBlackTempleActionContext::illidari_council_main_tank_reflect_judgement_of_command; + + creators["illidari council first assist tank focus malande"] = + &RaidBlackTempleActionContext::illidari_council_first_assist_tank_focus_malande; + + creators["illidari council second assist tank position darkshadow"] = + &RaidBlackTempleActionContext::illidari_council_second_assist_tank_position_darkshadow; + + creators["illidari council mage tank position zerevor"] = + &RaidBlackTempleActionContext::illidari_council_mage_tank_position_zerevor; + + creators["illidari council position mage tank healer"] = + &RaidBlackTempleActionContext::illidari_council_position_mage_tank_healer; + + creators["illidari council disperse ranged"] = + &RaidBlackTempleActionContext::illidari_council_disperse_ranged; + + creators["illidari council command pets to attack gathios"] = + &RaidBlackTempleActionContext::illidari_council_command_pets_to_attack_gathios; + + creators["illidari council assign dps targets"] = + &RaidBlackTempleActionContext::illidari_council_assign_dps_targets; + + creators["illidari council manage dps timer"] = + &RaidBlackTempleActionContext::illidari_council_manage_dps_timer; + + // Illidan Stormrage + creators["illidan stormrage misdirect to tank"] = + &RaidBlackTempleActionContext::illidan_stormrage_misdirect_to_tank; + + creators["illidan stormrage main tank reposition boss"] = + &RaidBlackTempleActionContext::illidan_stormrage_main_tank_reposition_boss; + + creators["illidan stormrage isolate bot with parasite"] = + &RaidBlackTempleActionContext::illidan_stormrage_isolate_bot_with_parasite; + + creators["illidan stormrage set earthbind totem"] = + &RaidBlackTempleActionContext::illidan_stormrage_set_earthbind_totem; + + creators["illidan stormrage assist tanks handle flames of azzinoth"] = + &RaidBlackTempleActionContext::illidan_stormrage_assist_tanks_handle_flames_of_azzinoth; + + creators["illidan stormrage control pet aggression"] = + &RaidBlackTempleActionContext::illidan_stormrage_control_pet_aggression; + + creators["illidan stormrage position above grate"] = + &RaidBlackTempleActionContext::illidan_stormrage_position_above_grate; + + creators["illidan stormrage remove dark barrage"] = + &RaidBlackTempleActionContext::illidan_stormrage_remove_dark_barrage; + + creators["illidan stormrage move away from landing point"] = + &RaidBlackTempleActionContext::illidan_stormrage_move_away_from_landing_point; + + creators["illidan stormrage disperse ranged"] = + &RaidBlackTempleActionContext::illidan_stormrage_disperse_ranged; + + creators["illidan stormrage melee go somewhere to not die"] = + &RaidBlackTempleActionContext::illidan_stormrage_melee_go_somewhere_to_not_die; + + creators["illidan stormrage warlock tank handle demon boss"] = + &RaidBlackTempleActionContext::illidan_stormrage_warlock_tank_handle_demon_boss; + + creators["illidan stormrage dps prioritize adds"] = + &RaidBlackTempleActionContext::illidan_stormrage_dps_prioritize_adds; + + creators["illidan stormrage use shadow trap"] = + &RaidBlackTempleActionContext::illidan_stormrage_use_shadow_trap; + + creators["illidan stormrage manage dps timer and rti"] = + &RaidBlackTempleActionContext::illidan_stormrage_manage_dps_timer_and_rti; + + creators["illidan stormrage destroy hazards"] = + &RaidBlackTempleActionContext::illidan_stormrage_destroy_hazards; + + creators["illidan stormrage handle adds cheat"] = + &RaidBlackTempleActionContext::illidan_stormrage_handle_adds_cheat; + } + +private: + // General + static Action* black_temple_erase_timers_and_trackers( + PlayerbotAI* botAI) { return new BlackTempleEraseTimersAndTrackersAction(botAI); } + + // High Warlord Naj'entus + static Action* high_warlord_najentus_misdirect_boss_to_main_tank( + PlayerbotAI* botAI) { return new HighWarlordNajentusMisdirectBossToMainTankAction(botAI); } + + static Action* high_warlord_najentus_tanks_position_boss( + PlayerbotAI* botAI) { return new HighWarlordNajentusTanksPositionBossAction(botAI); } + + static Action* high_warlord_najentus_disperse_ranged( + PlayerbotAI* botAI) { return new HighWarlordNajentusDisperseRangedAction(botAI); } + + static Action* high_warlord_najentus_remove_impaling_spine( + PlayerbotAI* botAI) { return new HighWarlordNajentusRemoveImpalingSpineAction(botAI); } + + static Action* high_warlord_najentus_throw_impaling_spine( + PlayerbotAI* botAI) { return new HighWarlordNajentusThrowImpalingSpineAction(botAI); } + + // Supremus + static Action* supremus_misdirect_boss_to_main_tank( + PlayerbotAI* botAI) { return new SupremusMisdirectBossToMainTankAction(botAI); } + + static Action* supremus_disperse_ranged( + PlayerbotAI* botAI) { return new SupremusDisperseRangedAction(botAI); } + + static Action* supremus_kite_boss( + PlayerbotAI* botAI) { return new SupremusKiteBossAction(botAI); } + + static Action* supremus_move_away_from_volcanos( + PlayerbotAI* botAI) { return new SupremusMoveAwayFromVolcanosAction(botAI); } + + static Action* supremus_manage_phase_timer( + PlayerbotAI* botAI) { return new SupremusManagePhaseTimerAction(botAI); } + + // Shade of Akama + static Action* shade_of_akama_melee_dps_prioritize_channelers( + PlayerbotAI* botAI) { return new ShadeOfAkamaMeleeDpsPrioritizeChannelersAction(botAI); } + + // Teron Gorefiend + static Action* teron_gorefiend_misdirect_boss_to_main_tank( + PlayerbotAI* botAI) { return new TeronGorefiendMisdirectBossToMainTankAction(botAI); } + + static Action* teron_gorefiend_tanks_position_boss( + PlayerbotAI* botAI) { return new TeronGorefiendTanksPositionBossAction(botAI); } + + static Action* teron_gorefiend_position_ranged_on_balcony( + PlayerbotAI* botAI) { return new TeronGorefiendPositionRangedOnBalconyAction(botAI); } + + static Action* teron_gorefiend_avoid_shadow_of_death( + PlayerbotAI* botAI) { return new TeronGorefiendAvoidShadowOfDeathAction(botAI); } + + static Action* teron_gorefiend_move_to_corner_to_die( + PlayerbotAI* botAI) { return new TeronGorefiendMoveToCornerToDieAction(botAI); } + + static Action* teron_gorefiend_control_and_destroy_shadowy_constructs( + PlayerbotAI* botAI) { return new TeronGorefiendControlAndDestroyShadowyConstructsAction(botAI); } + + // Gurtogg Bloodboil + static Action* gurtogg_bloodboil_misdirect_boss_to_main_tank( + PlayerbotAI* botAI) { return new GurtoggBloodboilMisdirectBossToMainTankAction(botAI); } + + static Action* gurtogg_bloodboil_tanks_position_boss( + PlayerbotAI* botAI) { return new GurtoggBloodboilTanksPositionBossAction(botAI); } + + static Action* gurtogg_bloodboil_rotate_ranged_groups( + PlayerbotAI* botAI) { return new GurtoggBloodboilRotateRangedGroupsAction(botAI); } + + static Action* gurtogg_bloodboil_ranged_move_away_from_enraged_player( + PlayerbotAI* botAI) { return new GurtoggBloodboilRangedMoveAwayFromEnragedPlayerAction(botAI); } + + static Action* gurtogg_bloodboil_manage_phase_timer( + PlayerbotAI* botAI) { return new GurtoggBloodboilManagePhaseTimerAction(botAI); } + + // Reliquary of Souls + static Action* reliquary_of_souls_misdirect_boss_to_main_tank( + PlayerbotAI* botAI) { return new ReliquaryOfSoulsMisdirectBossToMainTankAction(botAI); } + + static Action* reliquary_of_souls_adjust_distance_from_suffering( + PlayerbotAI* botAI) { return new ReliquaryOfSoulsAdjustDistanceFromSufferingAction(botAI); } + + static Action* reliquary_of_souls_healers_dps_suffering( + PlayerbotAI* botAI) { return new ReliquaryOfSoulsHealersDpsSufferingAction(botAI); } + + static Action* reliquary_of_souls_spellsteal_rune_shield( + PlayerbotAI* botAI) { return new ReliquaryOfSoulsSpellstealRuneShieldAction(botAI); } + + static Action* reliquary_of_souls_spell_reflect_deaden( + PlayerbotAI* botAI) { return new ReliquaryOfSoulsSpellReflectDeadenAction(botAI); } + + // Mother Shahraz + static Action* mother_shahraz_misdirect_boss_to_main_tank( + PlayerbotAI* botAI) { return new MotherShahrazMisdirectBossToMainTankAction(botAI); } + + static Action* mother_shahraz_tanks_position_boss_under_pillar( + PlayerbotAI* botAI) { return new MotherShahrazTanksPositionBossUnderPillarAction(botAI); } + + static Action* mother_shahraz_melee_dps_wait_at_safe_position( + PlayerbotAI* botAI) { return new MotherShahrazMeleeDpsWaitAtSafePositionAction(botAI); } + + static Action* mother_shahraz_position_ranged_under_pillar( + PlayerbotAI* botAI) { return new MotherShahrazPositionRangedUnderPillarAction(botAI); } + + static Action* mother_shahraz_run_away_to_break_fatal_attraction( + PlayerbotAI* botAI) { return new MotherShahrazRunAwayToBreakFatalAttractionAction(botAI); } + + // Illidari Council + static Action* illidari_council_misdirect_bosses_to_tanks( + PlayerbotAI* botAI) { return new IllidariCouncilMisdirectBossesToTanksAction(botAI); } + + static Action* illidari_council_main_tank_position_gathios( + PlayerbotAI* botAI) { return new IllidariCouncilMainTankPositionGathiosAction(botAI); } + + static Action* illidari_council_main_tank_reflect_judgement_of_command( + PlayerbotAI* botAI) { return new IllidariCouncilMainTankReflectJudgementOfCommandAction(botAI); } + + static Action* illidari_council_first_assist_tank_focus_malande( + PlayerbotAI* botAI) { return new IllidariCouncilFirstAssistTankFocusMalandeAction(botAI); } + + static Action* illidari_council_second_assist_tank_position_darkshadow( + PlayerbotAI* botAI) { return new IllidariCouncilSecondAssistTankPositionDarkshadowAction(botAI); } + + static Action* illidari_council_mage_tank_position_zerevor( + PlayerbotAI* botAI) { return new IllidariCouncilMageTankPositionZerevorAction(botAI); } + + static Action* illidari_council_position_mage_tank_healer( + PlayerbotAI* botAI) { return new IllidariCouncilPositionMageTankHealerAction(botAI); } + + static Action* illidari_council_disperse_ranged( + PlayerbotAI* botAI) { return new IllidariCouncilDisperseRangedAction(botAI); } + + static Action* illidari_council_command_pets_to_attack_gathios( + PlayerbotAI* botAI) { return new IllidariCouncilCommandPetsToAttackGathiosAction(botAI); } + + static Action* illidari_council_assign_dps_targets( + PlayerbotAI* botAI) { return new IllidariCouncilAssignDpsTargetsAction(botAI); } + + static Action* illidari_council_manage_dps_timer( + PlayerbotAI* botAI) { return new IllidariCouncilManageDpsTimerAction(botAI); } + + // Illidan Stormrage + static Action* illidan_stormrage_misdirect_to_tank( + PlayerbotAI* botAI) { return new IllidanStormrageMisdirectToTankAction(botAI); } + + static Action* illidan_stormrage_main_tank_reposition_boss( + PlayerbotAI* botAI) { return new IllidanStormrageMainTankRepositionBossAction(botAI); } + + static Action* illidan_stormrage_isolate_bot_with_parasite( + PlayerbotAI* botAI) { return new IllidanStormrageIsolateBotWithParasiteAction(botAI); } + + static Action* illidan_stormrage_set_earthbind_totem( + PlayerbotAI* botAI) { return new IllidanStormrageSetEarthbindTotemAction(botAI); } + + static Action* illidan_stormrage_assist_tanks_handle_flames_of_azzinoth( + PlayerbotAI* botAI) { return new IllidanStormrageAssistTanksHandleFlamesOfAzzinothAction(botAI); } + + static Action* illidan_stormrage_control_pet_aggression( + PlayerbotAI* botAI) { return new IllidanStormrageControlPetAggressionAction(botAI); } + + static Action* illidan_stormrage_position_above_grate( + PlayerbotAI* botAI) { return new IllidanStormragePositionAboveGrateAction(botAI); } + + static Action* illidan_stormrage_remove_dark_barrage( + PlayerbotAI* botAI) { return new IllidanStormrageRemoveDarkBarrageAction(botAI); } + + static Action* illidan_stormrage_move_away_from_landing_point( + PlayerbotAI* botAI) { return new IllidanStormrageMoveAwayFromLandingPointAction(botAI); } + + static Action* illidan_stormrage_disperse_ranged( + PlayerbotAI* botAI) { return new IllidanStormrageDisperseRangedAction(botAI); } + + static Action* illidan_stormrage_melee_go_somewhere_to_not_die( + PlayerbotAI* botAI) { return new IllidanStormrageMeleeGoSomewhereToNotDieAction(botAI); } + + static Action* illidan_stormrage_warlock_tank_handle_demon_boss( + PlayerbotAI* botAI) { return new IllidanStormrageWarlockTankHandleDemonBossAction(botAI); } + + static Action* illidan_stormrage_dps_prioritize_adds( + PlayerbotAI* botAI) { return new IllidanStormrageDpsPrioritizeAddsAction(botAI); } + + static Action* illidan_stormrage_use_shadow_trap( + PlayerbotAI* botAI) { return new IllidanStormrageUseShadowTrapAction(botAI); } + + static Action* illidan_stormrage_manage_dps_timer_and_rti( + PlayerbotAI* botAI) { return new IllidanStormrageManageDpsTimerAndRtiAction(botAI); } + + static Action* illidan_stormrage_destroy_hazards( + PlayerbotAI* botAI) { return new IllidanStormrageDestroyHazardsAction(botAI); } + + static Action* illidan_stormrage_handle_adds_cheat( + PlayerbotAI* botAI) { return new IllidanStormrageHandleAddsCheatAction(botAI); } +}; + +#endif diff --git a/src/Ai/Raid/BlackTemple/RaidBlackTempleTriggerContext.h b/src/Ai/Raid/BlackTemple/RaidBlackTempleTriggerContext.h new file mode 100644 index 000000000..d9cc09489 --- /dev/null +++ b/src/Ai/Raid/BlackTemple/RaidBlackTempleTriggerContext.h @@ -0,0 +1,406 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_RAIDBLACKTEMPLETRIGGERCONTEXT_H +#define _PLAYERBOT_RAIDBLACKTEMPLETRIGGERCONTEXT_H + +#include "NamedObjectContext.h" +#include "RaidBlackTempleTriggers.h" + +class RaidBlackTempleTriggerContext : public NamedObjectContext +{ +public: + RaidBlackTempleTriggerContext() + { + // General + creators["black temple bot is not in combat"] = + &RaidBlackTempleTriggerContext::black_temple_bot_is_not_in_combat; + + // High Warlord Naj'entus + creators["high warlord naj'entus pulling boss"] = + &RaidBlackTempleTriggerContext::high_warlord_najentus_pulling_boss; + + creators["high warlord naj'entus boss engaged by tanks"] = + &RaidBlackTempleTriggerContext::high_warlord_najentus_boss_engaged_by_tanks; + + creators["high warlord naj'entus casts needle spines"] = + &RaidBlackTempleTriggerContext::high_warlord_najentus_casts_needle_spines; + + creators["high warlord naj'entus player is impaled"] = + &RaidBlackTempleTriggerContext::high_warlord_najentus_player_is_impaled; + + creators["high warlord naj'entus boss has tidal shield"] = + &RaidBlackTempleTriggerContext::high_warlord_najentus_boss_has_tidal_shield; + + // Supremus + creators["supremus pulling boss or changing phase"] = + &RaidBlackTempleTriggerContext::supremus_pulling_boss_or_changing_phase; + + creators["supremus boss engaged by ranged"] = + &RaidBlackTempleTriggerContext::supremus_boss_engaged_by_ranged; + + creators["supremus boss is fixated on bot"] = + &RaidBlackTempleTriggerContext::supremus_boss_is_fixated_on_bot; + + creators["supremus volcano is nearby"] = + &RaidBlackTempleTriggerContext::supremus_volcano_is_nearby; + + creators["supremus need to manage phase timer"] = + &RaidBlackTempleTriggerContext::supremus_need_to_manage_phase_timer; + + // Shade of Akama + creators["shade of akama killing channelers starts phase 2"] = + &RaidBlackTempleTriggerContext::shade_of_akama_killing_channelers_starts_phase_2; + + // Teron Gorefiend + creators["teron gorefiend pulling boss"] = + &RaidBlackTempleTriggerContext::teron_gorefiend_pulling_boss; + + creators["teron gorefiend boss engaged by tanks"] = + &RaidBlackTempleTriggerContext::teron_gorefiend_boss_engaged_by_tanks; + + creators["teron gorefiend boss engaged by ranged"] = + &RaidBlackTempleTriggerContext::teron_gorefiend_boss_engaged_by_ranged; + + creators["teron gorefiend boss is casting shadow of death"] = + &RaidBlackTempleTriggerContext::teron_gorefiend_boss_is_casting_shadow_of_death; + + creators["teron gorefiend bot has shadow of death"] = + &RaidBlackTempleTriggerContext::teron_gorefiend_bot_has_shadow_of_death; + + creators["teron gorefiend bot transformed into vengeful spirit"] = + &RaidBlackTempleTriggerContext::teron_gorefiend_bot_transformed_into_vengeful_spirit; + + // Gurtogg Bloodboil + creators["gurtogg bloodboil pulling boss"] = + &RaidBlackTempleTriggerContext::gurtogg_bloodboil_pulling_boss; + + creators["gurtogg bloodboil boss engaged by tanks"] = + &RaidBlackTempleTriggerContext::gurtogg_bloodboil_boss_engaged_by_tanks; + + creators["gurtogg bloodboil boss casts bloodboil"] = + &RaidBlackTempleTriggerContext::gurtogg_bloodboil_boss_casts_bloodboil; + + creators["gurtogg bloodboil bot has fel rage"] = + &RaidBlackTempleTriggerContext::gurtogg_bloodboil_bot_has_fel_rage; + + creators["gurtogg bloodboil need to manage phase timer"] = + &RaidBlackTempleTriggerContext::gurtogg_bloodboil_need_to_manage_phase_timer; + + // Reliquary of Souls + creators["reliquary of souls aggro resets upon phase change"] = + &RaidBlackTempleTriggerContext::reliquary_of_souls_aggro_resets_upon_phase_change; + + creators["reliquary of souls essence of suffering fixates on closest target"] = + &RaidBlackTempleTriggerContext::reliquary_of_souls_essence_of_suffering_fixates_on_closest_target; + + creators["reliquary of souls essence of suffering disables healing"] = + &RaidBlackTempleTriggerContext::reliquary_of_souls_essence_of_suffering_disables_healing; + + creators["reliquary of souls essence of desire has rune shield"] = + &RaidBlackTempleTriggerContext::reliquary_of_souls_essence_of_desire_has_rune_shield; + + creators["reliquary of souls essence of desire casting deaden"] = + &RaidBlackTempleTriggerContext::reliquary_of_souls_essence_of_desire_casting_deaden; + + // Mother Shahraz + creators["mother shahraz pulling boss"] = + &RaidBlackTempleTriggerContext::mother_shahraz_pulling_boss; + + creators["mother shahraz boss engaged by tanks"] = + &RaidBlackTempleTriggerContext::mother_shahraz_boss_engaged_by_tanks; + + creators["mother shahraz tanks are positioning boss"] = + &RaidBlackTempleTriggerContext::mother_shahraz_tanks_are_positioning_boss; + + creators["mother shahraz sinister beam knocks back players"] = + &RaidBlackTempleTriggerContext::mother_shahraz_sinister_beam_knocks_back_players; + + creators["mother shahraz bots are linked by fatal attraction"] = + &RaidBlackTempleTriggerContext::mother_shahraz_bots_are_linked_by_fatal_attraction; + + // Illidari Council + creators["illidari council pulling bosses"] = + &RaidBlackTempleTriggerContext::illidari_council_pulling_bosses; + + creators["illidari council gathios engaged by main tank"] = + &RaidBlackTempleTriggerContext::illidari_council_gathios_engaged_by_main_tank; + + creators["illidari council gathios casting judgement of command"] = + &RaidBlackTempleTriggerContext::illidari_council_gathios_casting_judgement_of_command; + + creators["illidari council malande engaged by first assist tank"] = + &RaidBlackTempleTriggerContext::illidari_council_malande_engaged_by_first_assist_tank; + + creators["illidari council darkshadow engaged by second assist tank"] = + &RaidBlackTempleTriggerContext::illidari_council_darkshadow_engaged_by_second_assist_tank; + + creators["illidari council zerevor engaged by mage tank"] = + &RaidBlackTempleTriggerContext::illidari_council_zerevor_engaged_by_mage_tank; + + creators["illidari council mage tank needs dedicated healer"] = + &RaidBlackTempleTriggerContext::illidari_council_mage_tank_needs_dedicated_healer; + + creators["illidari council zerevor casts dangerous aoes"] = + &RaidBlackTempleTriggerContext::illidari_council_zerevor_casts_dangerous_aoes; + + creators["illidari council pets screw up the pull"] = + &RaidBlackTempleTriggerContext::illidari_council_pets_screw_up_the_pull; + + creators["illidari council determining dps assignments"] = + &RaidBlackTempleTriggerContext::illidari_council_determining_dps_assignments; + + creators["illidari council need to manage dps timer"] = + &RaidBlackTempleTriggerContext::illidari_council_need_to_manage_dps_timer; + + // Illidan Stormrage + creators["illidan stormrage tank needs aggro"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_tank_needs_aggro; + + creators["illidan stormrage boss casts flame crash in front of main tank"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_boss_casts_flame_crash_in_front_of_main_tank; + + creators["illidan stormrage bot has parasitic shadowfiend"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_bot_has_parasitic_shadowfiend; + + creators["illidan stormrage parasitic shadowfiends run wild"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_parasitic_shadowfiends_run_wild; + + creators["illidan stormrage boss summoned flames of azzinoth"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_boss_summoned_flames_of_azzinoth; + + creators["illidan stormrage pets die to fire"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_pets_die_to_fire; + + creators["illidan stormrage grate is safe from flames"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_grate_is_safe_from_flames; + + creators["illidan stormrage bot struck by dark barrage"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_bot_struck_by_dark_barrage; + + creators["illidan stormrage boss is preparing to land"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_boss_is_preparing_to_land; + + creators["illidan stormrage boss deals splash damage"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_boss_deals_splash_damage; + + creators["illidan stormrage this expansion hates melee"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_this_expansion_hates_melee; + + creators["illidan stormrage boss transforms into demon"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_boss_transforms_into_demon; + + creators["illidan stormrage boss spawns adds"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_boss_spawns_adds; + + creators["illidan stormrage maiev placed shadow trap"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_maiev_placed_shadow_trap; + + creators["illidan stormrage need to manage dps timer and rti"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_need_to_manage_dps_timer_and_rti; + + creators["illidan stormrage need to clear hazards between phases"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_need_to_clear_hazards_between_phases; + + creators["illidan stormrage cheat"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_cheat; + } + +private: + // General + static Trigger* black_temple_bot_is_not_in_combat( + PlayerbotAI* botAI) { return new BlackTempleBotIsNotInCombatTrigger(botAI); } + + // High Warlord Naj'entus + static Trigger* high_warlord_najentus_pulling_boss( + PlayerbotAI* botAI) { return new HighWarlordNajentusPullingBossTrigger(botAI); } + + static Trigger* high_warlord_najentus_boss_engaged_by_tanks( + PlayerbotAI* botAI) { return new HighWarlordNajentusBossEngagedByTanksTrigger(botAI); } + + static Trigger* high_warlord_najentus_casts_needle_spines( + PlayerbotAI* botAI) { return new HighWarlordNajentusCastsNeedleSpinesTrigger(botAI); } + + static Trigger* high_warlord_najentus_player_is_impaled( + PlayerbotAI* botAI) { return new HighWarlordNajentusPlayerIsImpaledTrigger(botAI); } + + static Trigger* high_warlord_najentus_boss_has_tidal_shield( + PlayerbotAI* botAI) { return new HighWarlordNajentusBossHasTidalShieldTrigger(botAI); } + + // Supremus + static Trigger* supremus_pulling_boss_or_changing_phase( + PlayerbotAI* botAI) { return new SupremusPullingBossOrChangingPhaseTrigger(botAI); } + + static Trigger* supremus_boss_engaged_by_ranged( + PlayerbotAI* botAI) { return new SupremusBossEngagedByRangedTrigger(botAI); } + + static Trigger* supremus_boss_is_fixated_on_bot( + PlayerbotAI* botAI) { return new SupremusBossIsFixatedOnBotTrigger(botAI); } + + static Trigger* supremus_volcano_is_nearby( + PlayerbotAI* botAI) { return new SupremusVolcanoIsNearbyTrigger(botAI); } + + static Trigger* supremus_need_to_manage_phase_timer( + PlayerbotAI* botAI) { return new SupremusNeedToManagePhaseTimerTrigger(botAI); } + + // Shade of Akama + static Trigger* shade_of_akama_killing_channelers_starts_phase_2( + PlayerbotAI* botAI) { return new ShadeOfAkamaKillingChannelersStartsPhase2Trigger(botAI); } + + // Teron Gorefiend + static Trigger* teron_gorefiend_pulling_boss( + PlayerbotAI* botAI) { return new TeronGorefiendPullingBossTrigger(botAI); } + + static Trigger* teron_gorefiend_boss_engaged_by_tanks( + PlayerbotAI* botAI) { return new TeronGorefiendBossEngagedByTanksTrigger(botAI); } + + static Trigger* teron_gorefiend_boss_engaged_by_ranged( + PlayerbotAI* botAI) { return new TeronGorefiendBossEngagedByRangedTrigger(botAI); } + + static Trigger* teron_gorefiend_boss_is_casting_shadow_of_death( + PlayerbotAI* botAI) { return new TeronGorefiendBossIsCastingShadowOfDeathTrigger(botAI); } + + static Trigger* teron_gorefiend_bot_has_shadow_of_death( + PlayerbotAI* botAI) { return new TeronGorefiendBotHasShadowOfDeathTrigger(botAI); } + + static Trigger* teron_gorefiend_bot_transformed_into_vengeful_spirit( + PlayerbotAI* botAI) { return new TeronGorefiendBotTransformedIntoVengefulSpiritTrigger(botAI); } + + // Gurtogg Bloodboil + static Trigger* gurtogg_bloodboil_pulling_boss( + PlayerbotAI* botAI) { return new GurtoggBloodboilPullingBossTrigger(botAI); } + + static Trigger* gurtogg_bloodboil_boss_engaged_by_tanks( + PlayerbotAI* botAI) { return new GurtoggBloodboilBossEngagedByTanksTrigger(botAI); } + + static Trigger* gurtogg_bloodboil_boss_casts_bloodboil( + PlayerbotAI* botAI) { return new GurtoggBloodboilBossCastsBloodboilTrigger(botAI); } + + static Trigger* gurtogg_bloodboil_bot_has_fel_rage( + PlayerbotAI* botAI) { return new GurtoggBloodboilBotHasFelRageTrigger(botAI); } + + static Trigger* gurtogg_bloodboil_need_to_manage_phase_timer( + PlayerbotAI* botAI) { return new GurtoggBloodboilNeedToManagePhaseTimerTrigger(botAI); } + + // Reliquary of Souls + static Trigger* reliquary_of_souls_aggro_resets_upon_phase_change( + PlayerbotAI* botAI) { return new ReliquaryOfSoulsAggroResetsUponPhaseChangeTrigger(botAI); } + + static Trigger* reliquary_of_souls_essence_of_suffering_fixates_on_closest_target( + PlayerbotAI* botAI) { return new ReliquaryOfSoulsEssenceOfSufferingFixatesOnClosestTargetTrigger(botAI); } + + static Trigger* reliquary_of_souls_essence_of_suffering_disables_healing( + PlayerbotAI* botAI) { return new ReliquaryOfSoulsEssenceOfSufferingDisablesHealingTrigger(botAI); } + + static Trigger* reliquary_of_souls_essence_of_desire_has_rune_shield( + PlayerbotAI* botAI) { return new ReliquaryOfSoulsEssenceOfDesireHasRuneShieldTrigger(botAI); } + + static Trigger* reliquary_of_souls_essence_of_desire_casting_deaden( + PlayerbotAI* botAI) { return new ReliquaryOfSoulsEssenceOfDesireCastingDeadenTrigger(botAI); } + + // Mother Shahraz + static Trigger* mother_shahraz_pulling_boss( + PlayerbotAI* botAI) { return new MotherShahrazPullingBossTrigger(botAI); } + + static Trigger* mother_shahraz_boss_engaged_by_tanks( + PlayerbotAI* botAI) { return new MotherShahrazBossEngagedByTanksTrigger(botAI); } + + static Trigger* mother_shahraz_tanks_are_positioning_boss( + PlayerbotAI* botAI) { return new MotherShahrazTanksArePositioningBossTrigger(botAI); } + + static Trigger* mother_shahraz_sinister_beam_knocks_back_players( + PlayerbotAI* botAI) { return new MotherShahrazSinisterBeamKnocksBackPlayersTrigger(botAI); } + + static Trigger* mother_shahraz_bots_are_linked_by_fatal_attraction( + PlayerbotAI* botAI) { return new MotherShahrazBotsAreLinkedByFatalAttractionTrigger(botAI); } + + // Illidari Council + static Trigger* illidari_council_pulling_bosses( + PlayerbotAI* botAI) { return new IllidariCouncilPullingBossesTrigger(botAI); } + + static Trigger* illidari_council_gathios_engaged_by_main_tank( + PlayerbotAI* botAI) { return new IllidariCouncilGathiosEngagedByMainTankTrigger(botAI); } + + static Trigger* illidari_council_gathios_casting_judgement_of_command( + PlayerbotAI* botAI) { return new IllidariCouncilGathiosCastingJudgementOfCommandTrigger(botAI); } + + static Trigger* illidari_council_malande_engaged_by_first_assist_tank( + PlayerbotAI* botAI) { return new IllidariCouncilMalandeEngagedByFirstAssistTankTrigger(botAI); } + + static Trigger* illidari_council_darkshadow_engaged_by_second_assist_tank( + PlayerbotAI* botAI) { return new IllidariCouncilDarkshadowEngagedBySecondAssistTankTrigger(botAI); } + + static Trigger* illidari_council_zerevor_engaged_by_mage_tank( + PlayerbotAI* botAI) { return new IllidariCouncilZerevorEngagedByMageTankTrigger(botAI); } + + static Trigger* illidari_council_mage_tank_needs_dedicated_healer( + PlayerbotAI* botAI) { return new IllidariCouncilMageTankNeedsDedicatedHealerTrigger(botAI); } + + static Trigger* illidari_council_zerevor_casts_dangerous_aoes( + PlayerbotAI* botAI) { return new IllidariCouncilZerevorCastsDangerousAoesTrigger(botAI); } + + static Trigger* illidari_council_pets_screw_up_the_pull( + PlayerbotAI* botAI) { return new IllidariCouncilPetsScrewUpThePullTrigger(botAI); } + + static Trigger* illidari_council_determining_dps_assignments( + PlayerbotAI* botAI) { return new IllidariCouncilDeterminingDpsAssignmentsTrigger(botAI); } + + static Trigger* illidari_council_need_to_manage_dps_timer( + PlayerbotAI* botAI) { return new IllidariCouncilNeedToManageDpsTimerTrigger(botAI); } + + // Illidan Stormrage + static Trigger* illidan_stormrage_tank_needs_aggro( + PlayerbotAI* botAI) { return new IllidanStormrageTankNeedsAggroTrigger(botAI); } + + static Trigger* illidan_stormrage_boss_casts_flame_crash_in_front_of_main_tank( + PlayerbotAI* botAI) { return new IllidanStormrageBossCastsFlameCrashInFrontOfMainTankTrigger(botAI); } + + static Trigger* illidan_stormrage_bot_has_parasitic_shadowfiend( + PlayerbotAI* botAI) { return new IllidanStormrageBotHasParasiticShadowfiendTrigger(botAI); } + + static Trigger* illidan_stormrage_parasitic_shadowfiends_run_wild( + PlayerbotAI* botAI) { return new IllidanStormrageParasiticShadowfiendsRunWildTrigger(botAI); } + + static Trigger* illidan_stormrage_boss_summoned_flames_of_azzinoth( + PlayerbotAI* botAI) { return new IllidanStormrageBossSummonedFlamesOfAzzinothTrigger(botAI); } + + static Trigger* illidan_stormrage_pets_die_to_fire( + PlayerbotAI* botAI) { return new IllidanStormragePetsDieToFireTrigger(botAI); } + + static Trigger* illidan_stormrage_grate_is_safe_from_flames( + PlayerbotAI* botAI) { return new IllidanStormrageGrateIsSafeFromFlamesTrigger(botAI); } + + static Trigger* illidan_stormrage_bot_struck_by_dark_barrage( + PlayerbotAI* botAI) { return new IllidanStormrageBotStruckByDarkBarrageTrigger(botAI); } + + static Trigger* illidan_stormrage_boss_is_preparing_to_land( + PlayerbotAI* botAI) { return new IllidanStormrageBossIsPreparingToLandTrigger(botAI); } + + static Trigger* illidan_stormrage_boss_deals_splash_damage( + PlayerbotAI* botAI) { return new IllidanStormrageBossDealsSplashDamageTrigger(botAI); } + + static Trigger* illidan_stormrage_this_expansion_hates_melee( + PlayerbotAI* botAI) { return new IllidanStormrageThisExpansionHatesMeleeTrigger(botAI); } + + static Trigger* illidan_stormrage_boss_transforms_into_demon( + PlayerbotAI* botAI) { return new IllidanStormrageBossTransformsIntoDemonTrigger(botAI); } + + static Trigger* illidan_stormrage_boss_spawns_adds( + PlayerbotAI* botAI) { return new IllidanStormrageBossSpawnsAddsTrigger(botAI); } + + static Trigger* illidan_stormrage_maiev_placed_shadow_trap( + PlayerbotAI* botAI) { return new IllidanStormrageMaievPlacedShadowTrapTrigger(botAI); } + + static Trigger* illidan_stormrage_need_to_manage_dps_timer_and_rti( + PlayerbotAI* botAI) { return new IllidanStormrageNeedToManageDpsTimerAndRtiTrigger(botAI); } + + static Trigger* illidan_stormrage_need_to_clear_hazards_between_phases( + PlayerbotAI* botAI) { return new IllidanStormrageNeedToClearHazardsBetweenPhasesTrigger(botAI); } + + static Trigger* illidan_stormrage_cheat( + PlayerbotAI* botAI) { return new IllidanStormrageCheatTrigger(botAI); } +}; + +#endif diff --git a/src/Ai/Raid/BlackTemple/Strategy/RaidBlackTempleStrategy.cpp b/src/Ai/Raid/BlackTemple/Strategy/RaidBlackTempleStrategy.cpp new file mode 100644 index 000000000..157469895 --- /dev/null +++ b/src/Ai/Raid/BlackTemple/Strategy/RaidBlackTempleStrategy.cpp @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "RaidBlackTempleStrategy.h" + +#include "RaidBlackTempleMultipliers.h" + +void RaidBlackTempleStrategy::InitTriggers(std::vector& triggers) +{ + // General + triggers.push_back(new TriggerNode("black temple bot is not in combat", { + NextAction("black temple erase timers and trackers", ACTION_EMERGENCY + 11) })); + + // High Warlord Naj'entus + triggers.push_back(new TriggerNode("high warlord naj'entus pulling boss", { + NextAction("high warlord naj'entus misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("high warlord naj'entus boss engaged by tanks", { + NextAction("high warlord naj'entus tanks position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("high warlord naj'entus casts needle spines", { + NextAction("high warlord naj'entus disperse ranged", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("high warlord naj'entus player is impaled", { + NextAction("high warlord naj'entus remove impaling spine", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("high warlord naj'entus boss has tidal shield", { + NextAction("high warlord naj'entus throw impaling spine", ACTION_RAID + 2) })); + + // Supremus + triggers.push_back(new TriggerNode("supremus pulling boss or changing phase", { + NextAction("supremus misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("supremus boss engaged by ranged", { + NextAction("supremus disperse ranged", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("supremus boss is fixated on bot", { + NextAction("supremus kite boss", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("supremus volcano is nearby", { + NextAction("supremus move away from volcanos", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("supremus need to manage phase timer", { + NextAction("supremus manage phase timer", ACTION_EMERGENCY + 10) })); + + // Shade of Akama + triggers.push_back(new TriggerNode("shade of akama killing channelers starts phase 2", { + NextAction("shade of akama melee dps prioritize channelers", ACTION_RAID + 1) })); + + // Teron Gorefiend + triggers.push_back(new TriggerNode("teron gorefiend pulling boss", { + NextAction("teron gorefiend misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("teron gorefiend boss engaged by tanks", { + NextAction("teron gorefiend tanks position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("teron gorefiend boss engaged by ranged", { + NextAction("teron gorefiend position ranged on balcony", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("teron gorefiend boss is casting shadow of death", { + NextAction("teron gorefiend avoid shadow of death", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("teron gorefiend bot has shadow of death", { + NextAction("teron gorefiend move to corner to die", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("teron gorefiend bot transformed into vengeful spirit", { + NextAction("teron gorefiend control and destroy shadowy constructs", ACTION_EMERGENCY + 10) })); + + // Gurtogg Bloodboil + triggers.push_back(new TriggerNode("gurtogg bloodboil pulling boss", { + NextAction("gurtogg bloodboil misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("gurtogg bloodboil boss engaged by tanks", { + NextAction("gurtogg bloodboil tanks position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("gurtogg bloodboil boss casts bloodboil", { + NextAction("gurtogg bloodboil rotate ranged groups", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("gurtogg bloodboil bot has fel rage", { + NextAction("gurtogg bloodboil ranged move away from enraged player", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("gurtogg bloodboil need to manage phase timer", { + NextAction("gurtogg bloodboil manage phase timer", ACTION_EMERGENCY + 10) })); + + // Reliquary of Souls + triggers.push_back(new TriggerNode("reliquary of souls aggro resets upon phase change", { + NextAction("reliquary of souls misdirect boss to main tank", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("reliquary of souls essence of suffering fixates on closest target", { + NextAction("reliquary of souls adjust distance from suffering", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("reliquary of souls essence of suffering disables healing", { + NextAction("reliquary of souls healers dps suffering", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("reliquary of souls essence of desire has rune shield", { + NextAction("reliquary of souls spellsteal rune shield", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("reliquary of souls essence of desire casting deaden", { + NextAction("reliquary of souls spell reflect deaden", ACTION_EMERGENCY + 6) })); + + // Mother Shahraz + triggers.push_back(new TriggerNode("mother shahraz pulling boss", { + NextAction("mother shahraz misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("mother shahraz boss engaged by tanks", { + NextAction("mother shahraz tanks position boss under pillar", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("mother shahraz tanks are positioning boss", { + NextAction("mother shahraz melee dps wait at safe position", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("mother shahraz sinister beam knocks back players", { + NextAction("mother shahraz position ranged under pillar", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("mother shahraz bots are linked by fatal attraction", { + NextAction("mother shahraz run away to break fatal attraction", ACTION_EMERGENCY + 10) })); + + // Illidari Council + triggers.push_back(new TriggerNode("illidari council pulling bosses", { + NextAction("illidari council misdirect bosses to tanks", ACTION_RAID + 4) })); + + triggers.push_back(new TriggerNode("illidari council gathios engaged by main tank", { + NextAction("illidari council main tank position gathios", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("illidari council gathios casting judgement of command", { + NextAction("illidari council main tank reflect judgement of command", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("illidari council malande engaged by first assist tank", { + NextAction("illidari council first assist tank focus malande", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("illidari council darkshadow engaged by second assist tank", { + NextAction("illidari council second assist tank position darkshadow", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("illidari council zerevor engaged by mage tank", { + NextAction("illidari council mage tank position zerevor", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("illidari council mage tank needs dedicated healer", { + NextAction("illidari council position mage tank healer", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("illidari council zerevor casts dangerous aoes", { + NextAction("illidari council disperse ranged", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("illidari council pets screw up the pull", { + NextAction("illidari council command pets to attack gathios", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("illidari council determining dps assignments", { + NextAction("illidari council assign dps targets", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("illidari council need to manage dps timer", { + NextAction("illidari council manage dps timer", ACTION_EMERGENCY + 10) })); + + // Illidan Stormrage + triggers.push_back(new TriggerNode("illidan stormrage tank needs aggro", { + NextAction("illidan stormrage misdirect to tank", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("illidan stormrage boss casts flame crash in front of main tank", { + NextAction("illidan stormrage main tank reposition boss", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("illidan stormrage bot has parasitic shadowfiend", { + NextAction("illidan stormrage isolate bot with parasite", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("illidan stormrage parasitic shadowfiends run wild", { + NextAction("illidan stormrage set earthbind totem", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("illidan stormrage boss summoned flames of azzinoth", { + NextAction("illidan stormrage assist tanks handle flames of azzinoth", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("illidan stormrage pets die to fire", { + NextAction("illidan stormrage control pet aggression", ACTION_RAID + 4) })); + + triggers.push_back(new TriggerNode("illidan stormrage grate is safe from flames", { + NextAction("illidan stormrage position above grate", ACTION_EMERGENCY + 2) })); + + triggers.push_back(new TriggerNode("illidan stormrage bot struck by dark barrage", { + NextAction("illidan stormrage remove dark barrage", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("illidan stormrage boss is preparing to land", { + NextAction("illidan stormrage move away from landing point", ACTION_EMERGENCY + 3) })); + + triggers.push_back(new TriggerNode("illidan stormrage boss deals splash damage", { + NextAction("illidan stormrage disperse ranged", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("illidan stormrage this expansion hates melee", { + NextAction("illidan stormrage melee go somewhere to not die", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("illidan stormrage boss transforms into demon", { + NextAction("illidan stormrage warlock tank handle demon boss", ACTION_EMERGENCY + 9) })); + + triggers.push_back(new TriggerNode("illidan stormrage boss spawns adds", { + NextAction("illidan stormrage dps prioritize adds", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("illidan stormrage maiev placed shadow trap", { + NextAction("illidan stormrage use shadow trap", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("illidan stormrage need to manage dps timer and rti", { + NextAction("illidan stormrage manage dps timer and rti", ACTION_EMERGENCY + 11) })); + + triggers.push_back(new TriggerNode("illidan stormrage need to clear hazards between phases", { + NextAction("illidan stormrage destroy hazards", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("illidan stormrage cheat", { + NextAction("illidan stormrage handle adds cheat", ACTION_EMERGENCY + 10) })); +} + +void RaidBlackTempleStrategy::InitMultipliers(std::vector& multipliers) +{ + // High Warlord Naj'entus + multipliers.push_back(new HighWarlordNajentusDelayDpsCooldownsMultiplier(botAI)); + multipliers.push_back(new HighWarlordNajentusDisableCombatFormationMoveMultiplier(botAI)); + + // Supremus + multipliers.push_back(new SupremusDelayDpsCooldownsMultiplier(botAI)); + multipliers.push_back(new SupremusFocusOnAvoidanceInPhase2Multiplier(botAI)); + multipliers.push_back(new SupremusHitboxIsBuggedMultiplier(botAI)); + + // Teron Gorefiend + multipliers.push_back(new TeronGorefiendDelayDpsCooldownsMultiplier(botAI)); + multipliers.push_back(new TeronGorefiendControlMovementMultiplier(botAI)); + multipliers.push_back(new TeronGorefiendMarkedBotOnlyMoveToDieMultiplier(botAI)); + multipliers.push_back(new TeronGorefiendSpiritsAttackOnlyShadowyConstructsMultiplier(botAI)); + multipliers.push_back(new TeronGorefiendDisableAttackingConstructsMultiplier(botAI)); + + // Gurtogg Bloodboil + multipliers.push_back(new GurtoggBloodboilDelayDpsCooldownsMultiplier(botAI)); + multipliers.push_back(new GurtoggBloodboilControlMovementMultiplier(botAI)); + + // Reliquary of Souls + multipliers.push_back(new ReliquaryOfSoulsDelayDpsCooldownsMultiplier(botAI)); + multipliers.push_back(new ReliquaryOfSoulsDontWasteHealingMultiplier(botAI)); + + // Mother Shahraz + multipliers.push_back(new MotherShahrazDelayDpsCooldownsMultiplier(botAI)); + multipliers.push_back(new MotherShahrazControlMovementMultiplier(botAI)); + multipliers.push_back(new MotherShahrazBotsWithFatalAttractionOnlyRunAwayMultiplier(botAI)); + + // Illidari Council + multipliers.push_back(new IllidariCouncilDelayDpsCooldownsMultiplier(botAI)); + multipliers.push_back(new IllidariCouncilDisableTankActionsMultiplier(botAI)); + multipliers.push_back(new IllidariCouncilControlMovementMultiplier(botAI)); + multipliers.push_back(new IllidariCouncilControlMisdirectionMultiplier(botAI)); + multipliers.push_back(new IllidariCouncilDisableArcaneShotOnZerevorMultiplier(botAI)); + multipliers.push_back(new IllidariCouncilDisableIceBlockMultiplier(botAI)); + multipliers.push_back(new IllidariCouncilWaitForDpsMultiplier(botAI)); + + // Illidan Stormrage + multipliers.push_back(new IllidanStormrageDelayDpsCooldownsMultiplier(botAI)); + multipliers.push_back(new IllidanStormrageControlTankActionsMultiplier(botAI)); + multipliers.push_back(new IllidanStormrageDisableDefaultTargetingMultiplier(botAI)); + multipliers.push_back(new IllidanStormrageControlNonTankMovementMultiplier(botAI)); + multipliers.push_back(new IllidanStormrageUseEarthbindTotemMultiplier(botAI)); + multipliers.push_back(new IllidanStormrageWaitForDpsMultiplier(botAI)); +} diff --git a/src/Ai/Raid/BlackTemple/Strategy/RaidBlackTempleStrategy.h b/src/Ai/Raid/BlackTemple/Strategy/RaidBlackTempleStrategy.h new file mode 100644 index 000000000..2f1cc4205 --- /dev/null +++ b/src/Ai/Raid/BlackTemple/Strategy/RaidBlackTempleStrategy.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_RAIDBLACKTEMPLESTRATEGY_H_ +#define _PLAYERBOT_RAIDBLACKTEMPLESTRATEGY_H_ + +#include "Strategy.h" + +class RaidBlackTempleStrategy : public Strategy +{ +public: + RaidBlackTempleStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + std::string const getName() override { return "blacktemple"; } + + void InitTriggers(std::vector& triggers) override; + void InitMultipliers(std::vector& multipliers) override; +}; + +#endif diff --git a/src/Ai/Raid/BlackTemple/Trigger/RaidBlackTempleTriggers.cpp b/src/Ai/Raid/BlackTemple/Trigger/RaidBlackTempleTriggers.cpp new file mode 100644 index 000000000..388e28d8b --- /dev/null +++ b/src/Ai/Raid/BlackTemple/Trigger/RaidBlackTempleTriggers.cpp @@ -0,0 +1,863 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "RaidBlackTempleTriggers.h" + +#include "AiFactory.h" +#include "Playerbots.h" +#include "RaidBlackTempleActions.h" +#include "RaidBlackTempleHelpers.h" +#include "RaidBossHelpers.h" +#include "SharedDefines.h" + +using namespace BlackTempleHelpers; + +// General + +bool BlackTempleBotIsNotInCombatTrigger::IsActive() +{ + return !bot->IsInCombat() && bot->GetMapId() == BLACK_TEMPLE_MAP_ID; +} + +// High Warlord Naj'entus + +bool HighWarlordNajentusPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* najentus = AI_VALUE2(Unit*, "find target", "high warlord naj'entus"); + return najentus && najentus->GetHealthPct() > 95.0f; +} + +bool HighWarlordNajentusBossEngagedByTanksTrigger::IsActive() +{ + return botAI->IsTank(bot) && + AI_VALUE2(Unit*, "find target", "high warlord naj'entus"); +} + +bool HighWarlordNajentusCastsNeedleSpinesTrigger::IsActive() +{ + return botAI->IsRanged(bot) && + AI_VALUE2(Unit*, "find target", "high warlord naj'entus"); +} + +bool HighWarlordNajentusPlayerIsImpaledTrigger::IsActive() +{ + if (botAI->IsTank(bot)) + return false; + + if (!AI_VALUE2(Unit*, "find target", "high warlord naj'entus")) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* impaledPlayer = nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot) + continue; + + if (member->HasAura( + static_cast(BlackTempleSpells::SPELL_IMPALING_SPINE))) + { + impaledPlayer = member; + break; + } + } + + Player* closestBot = nullptr; + float closestDist = std::numeric_limits::max(); + + if (impaledPlayer) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == impaledPlayer || + !GET_PLAYERBOT_AI(member) || botAI->IsTank(member)) + { + continue; + } + + float dist = member->GetDistance(impaledPlayer); + if (dist < closestDist) + { + closestDist = dist; + closestBot = member; + } + } + } + + return closestBot == bot; +} + +bool HighWarlordNajentusBossHasTidalShieldTrigger::IsActive() +{ + Unit* najentus = AI_VALUE2(Unit*, "find target", "high warlord naj'entus"); + if (!najentus || !najentus->HasAura( + static_cast(BlackTempleSpells::SPELL_TIDAL_SHIELD))) + { + return false; + } + + return botAI->HasItemInInventory( + static_cast(BlackTempleItems::ITEM_NAJENTUS_SPINE)); +} + +// Supremus + +bool SupremusPullingBossOrChangingPhaseTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* supremus = AI_VALUE2(Unit*, "find target", "supremus"); + if (!supremus) + return false; + + auto it = supremusPhaseTimer.find(supremus->GetMap()->GetInstanceId()); + if (it == supremusPhaseTimer.end()) + return false; + + const time_t now = time(nullptr); + const time_t elapsed = now - it->second; + + // Active during first 10 seconds, or during 60-70, 120-130, etc. + return (elapsed < 10) || ((elapsed % 60) < 10 && elapsed >= 60); +} + +bool SupremusBossEngagedByRangedTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* supremus = AI_VALUE2(Unit*, "find target", "supremus"); + return supremus && !supremus->HasAura( + static_cast(BlackTempleSpells::SPELL_SNARE_SELF)); +} + +bool SupremusBossIsFixatedOnBotTrigger::IsActive() +{ + Unit* supremus = AI_VALUE2(Unit*, "find target", "supremus"); + return supremus && supremus->GetVictim() == bot && + supremus->HasAura(static_cast( + BlackTempleSpells::SPELL_SNARE_SELF)); +} + +bool SupremusVolcanoIsNearbyTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "supremus") && + HasSupremusVolcanoNearby(botAI, bot); +} + +bool SupremusNeedToManagePhaseTimerTrigger::IsActive() +{ + if (!botAI->IsDps(bot)) + return false; + + if (!AI_VALUE2(Unit*, "find target", "supremus")) + return false; + + return IsMechanicTrackerBot(botAI, bot, BLACK_TEMPLE_MAP_ID); +} + +// Shade of Akama + +bool ShadeOfAkamaKillingChannelersStartsPhase2Trigger::IsActive() +{ + if (!botAI->IsDps(bot) || !botAI->IsMelee(bot)) + return false; + + constexpr float searchRadius = 30.0f; + Unit* channeler = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_ASHTONGUE_CHANNELER), + searchRadius, true); + + return channeler && !channeler->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE); +} + +// Teron Gorefiend + +bool TeronGorefiendPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* gorefiend = + AI_VALUE2(Unit*, "find target", "teron gorefiend"); + + return gorefiend && gorefiend->GetHealthPct() > 95.0f; +} + +bool TeronGorefiendBossEngagedByTanksTrigger::IsActive() +{ + return botAI->IsTank(bot) && + AI_VALUE2(Unit*, "find target", "teron gorefiend"); +} + +bool TeronGorefiendBossEngagedByRangedTrigger::IsActive() +{ + return botAI->IsRanged(bot) && + AI_VALUE2(Unit*, "find target", "teron gorefiend"); +} + +bool TeronGorefiendBossIsCastingShadowOfDeathTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER && bot->getClass() != CLASS_MAGE && + bot->getClass() != CLASS_PALADIN && bot->getClass() != CLASS_ROGUE) + { + return false; + } + + Unit* gorefiend = AI_VALUE2(Unit*, "find target", "teron gorefiend"); + if (!gorefiend) + return false; + + if (botAI->HasAura("feign death", bot)) + { + botAI->RemoveAura("feign death"); + return true; + } + else if (botAI->HasAura("ice block", bot)) + { + botAI->RemoveAura("ice block"); + return true; + } + else if (!botAI->IsHeal(bot) && botAI->HasAura("divine shield", bot)) + { + botAI->RemoveAura("divine shield"); + return true; + } + + if (!gorefiend->HasUnitState(UNIT_STATE_CASTING)) + return false; + + Spell* spell = gorefiend->GetCurrentSpell(CURRENT_GENERIC_SPELL); + if (!spell || spell->m_spellInfo->Id != + static_cast(BlackTempleSpells::SPELL_SHADOW_OF_DEATH)) + { + return false; + } + + Unit* target = spell->m_targets.GetUnitTarget(); + return target && target->GetGUID() == bot->GetGUID(); +} + +bool TeronGorefiendBotHasShadowOfDeathTrigger::IsActive() +{ + Aura* aura = bot->GetAura( + static_cast(BlackTempleSpells::SPELL_SHADOW_OF_DEATH)); + return aura && aura->GetDuration() < 12000; +} + +bool TeronGorefiendBotTransformedIntoVengefulSpiritTrigger::IsActive() +{ + return bot->HasAura( + static_cast(BlackTempleSpells::SPELL_SPIRITUAL_VENGEANCE)); +} + +// Gurtogg Bloodboil + +bool GurtoggBloodboilPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* gurtogg = AI_VALUE2(Unit*, "find target", "gurtogg bloodboil"); + if (!gurtogg) + return false; + + auto it = gurtoggPhaseTimer.find(gurtogg->GetMap()->GetInstanceId()); + if (it == gurtoggPhaseTimer.end()) + return false; + + const time_t elapsed = std::time(nullptr) - it->second; + return elapsed < 10; +} + +bool GurtoggBloodboilBossEngagedByTanksTrigger::IsActive() +{ + if (!botAI->IsTank(bot)) + return false; + + Unit* gurtogg = AI_VALUE2(Unit*, "find target", "gurtogg bloodboil"); + return gurtogg && !gurtogg->HasAura( + static_cast(BlackTempleSpells::SPELL_BOSS_FEL_RAGE)); +} + +bool GurtoggBloodboilBossCastsBloodboilTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* gurtogg = AI_VALUE2(Unit*, "find target", "gurtogg bloodboil"); + return gurtogg && !gurtogg->HasAura( + static_cast(BlackTempleSpells::SPELL_BOSS_FEL_RAGE)); +} + +bool GurtoggBloodboilBotHasFelRageTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* gurtogg = AI_VALUE2(Unit*, "find target", "gurtogg bloodboil"); + if (!gurtogg || !gurtogg->HasAura( + static_cast(BlackTempleSpells::SPELL_BOSS_FEL_RAGE))) + { + return false; + } + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->HasAura + (static_cast(BlackTempleSpells::SPELL_PLAYER_FEL_RAGE))) + { + return true; + } + } + } + + return false; +} + +bool GurtoggBloodboilNeedToManagePhaseTimerTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "gurtogg bloodboil") && + IsMechanicTrackerBot(botAI, bot, BLACK_TEMPLE_MAP_ID); +} + +// Reliquary of Souls + +bool ReliquaryOfSoulsAggroResetsUponPhaseChangeTrigger::IsActive() +{ + return bot->getClass() == CLASS_HUNTER && + AI_VALUE2(Unit*, "find target", "reliquary of the lost"); +} + +bool ReliquaryOfSoulsEssenceOfSufferingFixatesOnClosestTargetTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "essence of suffering"); +} + +bool ReliquaryOfSoulsEssenceOfSufferingDisablesHealingTrigger::IsActive() +{ + if (!botAI->IsHeal(bot)) + return false; + + if (bot->getClass() == CLASS_PRIEST && + AiFactory::GetPlayerSpecTab(bot) == PRIEST_TAB_DISCIPLINE) + { + return false; + } + + return AI_VALUE2(Unit*, "find target", "essence of suffering"); +} + +bool ReliquaryOfSoulsEssenceOfDesireHasRuneShieldTrigger::IsActive() +{ + if (bot->getClass() != CLASS_MAGE) + return false; + + Unit* desire = AI_VALUE2(Unit*, "find target", "essence of desire"); + return desire && desire->HasAura( + static_cast(BlackTempleSpells::SPELL_RUNE_SHIELD)); +} + +bool ReliquaryOfSoulsEssenceOfDesireCastingDeadenTrigger::IsActive() +{ + if (!botAI->IsTank(bot) || bot->getClass() != CLASS_WARRIOR) + return false; + + Unit* desire = AI_VALUE2(Unit*, "find target", "essence of desire"); + if (!desire || !desire->HasUnitState(UNIT_STATE_CASTING)) + return false; + + Spell* spell = desire->GetCurrentSpell(CURRENT_GENERIC_SPELL); + if (!spell || spell->m_spellInfo->Id != + static_cast(BlackTempleSpells::SPELL_DEADEN)) + { + return false; + } + + Unit* target = spell->m_targets.GetUnitTarget(); + return target && target->GetGUID() == bot->GetGUID(); +} + +// Mother Shahraz + +bool MotherShahrazPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* shahraz = AI_VALUE2(Unit*, "find target", "mother shahraz"); + return shahraz && shahraz->GetHealthPct() > 95.0f; +} + +bool MotherShahrazBossEngagedByTanksTrigger::IsActive() +{ + if (!botAI->IsTank(bot)) + return false; + + if (!AI_VALUE2(Unit*, "find target", "mother shahraz")) + return false; + + return !bot->HasAura( + static_cast(BlackTempleSpells::SPELL_FATAL_ATTRACTION)); +} + +bool MotherShahrazTanksArePositioningBossTrigger::IsActive() +{ + if (!botAI->IsMelee(bot) || !botAI->IsDps(bot)) + return false; + + Unit* shahraz = AI_VALUE2(Unit*, "find target", "mother shahraz"); + if (!shahraz || shahraz->GetHealthPct() < 90.0f) + return false; + + TankPositionState tankState = GetShahrazTankPositionState(botAI, bot); + return tankState != TankPositionState::Positioned; +} + +bool MotherShahrazSinisterBeamKnocksBackPlayersTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + if (!AI_VALUE2(Unit*, "find target", "mother shahraz")) + return false; + + return !bot->HasAura( + static_cast(BlackTempleSpells::SPELL_FATAL_ATTRACTION)); +} + +bool MotherShahrazBotsAreLinkedByFatalAttractionTrigger::IsActive() +{ + return bot->HasAura( + static_cast(BlackTempleSpells::SPELL_FATAL_ATTRACTION)); +} + +// Illidari Council + +bool IllidariCouncilPullingBossesTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* gathios = AI_VALUE2(Unit*, "find target", "gathios the shatterer"); + return gathios && gathios->GetHealthPct() > 95.0f; +} + +bool IllidariCouncilGathiosEngagedByMainTankTrigger::IsActive() +{ + return botAI->IsMainTank(bot) && + AI_VALUE2(Unit*, "find target", "gathios the shatterer"); +} + +bool IllidariCouncilGathiosCastingJudgementOfCommandTrigger::IsActive() +{ + if (bot->getClass() != CLASS_WARRIOR || !botAI->IsMainTank(bot)) + return false; + + Unit* gathios = AI_VALUE2(Unit*, "find target", "gathios the shatterer"); + if (!gathios || !gathios->HasUnitState(UNIT_STATE_CASTING) || !gathios->HasAura( + static_cast(BlackTempleSpells::SPELL_SEAL_OF_COMMAND))) + { + return false; + } + + Spell* spell = gathios->GetCurrentSpell(CURRENT_GENERIC_SPELL); + if (!spell || spell->m_spellInfo->Id != + static_cast(BlackTempleSpells::SPELL_JUDGEMENT)) + { + return false; + } + + Unit* target = spell->m_targets.GetUnitTarget(); + return target && target->GetGUID() == bot->GetGUID(); +} + +bool IllidariCouncilMalandeEngagedByFirstAssistTankTrigger::IsActive() +{ + return botAI->IsAssistTankOfIndex(bot, 0, false) && + AI_VALUE2(Unit*, "find target", "lady malande"); +} + +bool IllidariCouncilDarkshadowEngagedBySecondAssistTankTrigger::IsActive() +{ + if (!botAI->IsAssistTankOfIndex(bot, 1, false)) + return false; + + Unit* darkshadow = AI_VALUE2(Unit*, "find target", "veras darkshadow"); + return darkshadow && !darkshadow->HasAura( + static_cast(BlackTempleSpells::SPELL_VANISH)); +} + +bool IllidariCouncilZerevorEngagedByMageTankTrigger::IsActive() +{ + if (bot->getClass() != CLASS_MAGE || GetZerevorMageTank(bot) != bot) + return false; + + return AI_VALUE2(Unit*, "find target", "high nethermancer zerevor"); +} + +bool IllidariCouncilMageTankNeedsDedicatedHealerTrigger::IsActive() +{ + return botAI->IsAssistHealOfIndex(bot, 0, true) && + AI_VALUE2(Unit*, "find target", "high nethermancer zerevor"); +} + +bool IllidariCouncilZerevorCastsDangerousAoesTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + if (!AI_VALUE2(Unit*, "find target", "high nethermancer zerevor")) + return false; + + return !HasDangerousCouncilAura(bot); +} + +bool IllidariCouncilPetsScrewUpThePullTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER && bot->getClass() != CLASS_WARLOCK) + return false; + + Pet* pet = bot->GetPet(); + if (!pet || !pet->IsAlive()) + return false; + + return AI_VALUE2(Unit*, "find target", "gathios the shatterer"); +} + +bool IllidariCouncilDeterminingDpsAssignmentsTrigger::IsActive() +{ + if (botAI->IsHeal(bot)) + return false; + + if (!AI_VALUE2(Unit*, "find target", "gathios the shatterer")) + return false; + + if (botAI->IsMainTank(bot) || botAI->IsAssistTankOfIndex(bot, 0, false) || + GetZerevorMageTank(bot) == bot) + { + return false; + } + + Unit* darkshadow = AI_VALUE2(Unit*, "find target", "veras darkshadow"); + if (botAI->IsTank(bot) && botAI->IsAssistTankOfIndex(bot, 1, false) && + darkshadow && !darkshadow->HasAura( + static_cast(BlackTempleSpells::SPELL_VANISH))) + { + return false; + } + + return true; +} + +bool IllidariCouncilNeedToManageDpsTimerTrigger::IsActive() +{ + if (!botAI->IsDps(bot)) + return false; + + if (!AI_VALUE2(Unit*, "find target", "gathios the shatterer")) + return false; + + return IsMechanicTrackerBot( + botAI, bot, BLACK_TEMPLE_MAP_ID, GetZerevorMageTank(bot)); +} + +// Illidan Stormrage + +bool IllidanStormrageTankNeedsAggroTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + return illidan && illidan->GetHealth() > 1; +} + +bool IllidanStormrageBossCastsFlameCrashInFrontOfMainTankTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + int phase = GetIllidanPhase(illidan); + return phase == 1 || phase == 3 || phase == 5; +} + +bool IllidanStormrageBotHasParasiticShadowfiendTrigger::IsActive() +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->GetHealth() == 1 || + illidan->GetVictim() == bot) + { + return false; + } + + int phase = GetIllidanPhase(illidan); + if (phase == 2 || phase == 4) + return false; + + if (botAI->IsMainTank(bot)) + return false; + + if (phase == 5 && FindNearestTrap(botAI, bot)) + return false; + + Player* infected = GetBotWithParasiticShadowfiend(bot); + if (!infected) + return false; + + if (infected == bot || + (phase != 1 && bot->getClass() == CLASS_HUNTER)) + { + return true; + } + + return false; +} + +bool IllidanStormrageParasiticShadowfiendsRunWildTrigger::IsActive() +{ + if (bot->getClass() != CLASS_SHAMAN) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->GetHealth() == 1 || GetIllidanPhase(illidan) == 2) + return false; + + ObjectGuid guid = bot->m_SummonSlot[SUMMON_SLOT_TOTEM_EARTH]; + if (guid.IsEmpty()) + return true; + + Creature* totem = bot->GetMap()->GetCreature(guid); + return !totem || totem->GetDistance(bot) > 20.0f || + totem->GetUInt32Value(UNIT_CREATED_BY_SPELL) != + static_cast(BlackTempleSpells::SPELL_EARTHBIND_TOTEM); +} + +bool IllidanStormrageBossSummonedFlamesOfAzzinothTrigger::IsActive() +{ + if (!botAI->IsAssistTankOfIndex(bot, 0, true) && + !botAI->IsAssistTankOfIndex(bot, 1, true)) + { + return false; + } + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + return illidan && GetIllidanPhase(illidan) == 2; +} + +bool IllidanStormragePetsDieToFireTrigger::IsActive() +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->GetHealth() == 1) + return false; + + Pet* pet = bot->GetPet(); + return pet && pet->IsAlive(); +} + +bool IllidanStormrageGrateIsSafeFromFlamesTrigger::IsActive() +{ + if (botAI->IsAssistTankOfIndex(bot, 0, true) || + botAI->IsAssistTankOfIndex(bot, 1, true)) + { + return false; + } + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + return illidan && GetIllidanPhase(illidan) == 2; +} + +bool IllidanStormrageBotStruckByDarkBarrageTrigger::IsActive() +{ + if (bot->getClass() != CLASS_MAGE && bot->getClass() != CLASS_PALADIN && + bot->getClass() != CLASS_ROGUE) + { + return false; + } + + if (!AI_VALUE2(Unit*, "find target", "illidan stormrage")) + return false; + + if (botAI->HasAura("ice block", bot)) + { + botAI->RemoveAura("ice block"); + return true; + } + else if (!botAI->IsHeal(bot) && botAI->HasAura("divine shield", bot)) + { + botAI->RemoveAura("divine shield"); + return true; + } + + return bot->HasAura( + static_cast(BlackTempleSpells::SPELL_DARK_BARRAGE)); +} + +bool IllidanStormrageBossIsPreparingToLandTrigger::IsActive() +{ + if (botAI->IsMainTank(bot)) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + return illidan && GetIllidanPhase(illidan) == 0; +} + +bool IllidanStormrageBossDealsSplashDamageTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->HasAura + (static_cast(BlackTempleSpells::SPELL_CAGED))) + { + return false; + } + + int phase = GetIllidanPhase(illidan); + + if (phase == 4 && GetIllidanWarlockTank(bot) == bot) + return false; + + return phase == 3 || phase == 4 || phase == 5; +} + +bool IllidanStormrageThisExpansionHatesMeleeTrigger::IsActive() +{ + if (!botAI->IsMelee(bot)) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + return illidan && GetIllidanPhase(illidan) == 4; +} + +bool IllidanStormrageBossTransformsIntoDemonTrigger::IsActive() +{ + if (bot->getClass() != CLASS_WARLOCK) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || GetIllidanPhase(illidan) != 4) + return false; + + return GetIllidanWarlockTank(bot) == bot; +} + +bool IllidanStormrageBossSpawnsAddsTrigger::IsActive() +{ + if (botAI->IsHeal(bot)) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->GetHealth() == 1) + return false; + + if (botAI->IsTank(bot) && GetIllidanPhase(illidan) != 4) + return false; + + return true; +} + +bool IllidanStormrageMaievPlacedShadowTrapTrigger::IsActive() +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->GetHealth() == 1 || + GetIllidanPhase(illidan) != 5) + { + return false; + } + + GameObject* trap = FindNearestTrap(botAI, bot); + if (!trap) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* closestBot = nullptr; + float closestDist = std::numeric_limits::max(); + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || illidan->GetVictim() == member || + !GET_PLAYERBOT_AI(member) || botAI->IsMainTank(member)) + { + continue; + } + + float dist = member->GetDistance(trap); + if (dist < closestDist) + { + closestDist = dist; + closestBot = member; + } + } + + return closestBot == bot; +} + +bool IllidanStormrageNeedToManageDpsTimerAndRtiTrigger::IsActive() +{ + if (!botAI->IsDps(bot)) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->GetHealth() == 1) + return false; + + return IsMechanicTrackerBot( + botAI, bot, BLACK_TEMPLE_MAP_ID, GetIllidanWarlockTank(bot)); +} + +// Destroying hazards behind phases is not gated behind CheatMask +// The strategy simply cannot work without doing this +bool IllidanStormrageNeedToClearHazardsBetweenPhasesTrigger::IsActive() +{ + if (!botAI->IsDps(bot)) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->GetHealth() == 1) + return false; + + int phase = GetIllidanPhase(illidan); + if (phase != 0 && phase != 2 && phase != 4) + return false; + + return IsMechanicTrackerBot( + botAI, bot, BLACK_TEMPLE_MAP_ID, GetIllidanWarlockTank(bot)); +} + +bool IllidanStormrageCheatTrigger::IsActive() +{ + if (!botAI->HasCheat(BotCheatMask::raid) || !botAI->IsDps(bot)) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + int phase = GetIllidanPhase(illidan); + return phase == 2 || phase == 4; +} diff --git a/src/Ai/Raid/BlackTemple/Trigger/RaidBlackTempleTriggers.h b/src/Ai/Raid/BlackTemple/Trigger/RaidBlackTempleTriggers.h new file mode 100644 index 000000000..0113b4b14 --- /dev/null +++ b/src/Ai/Raid/BlackTemple/Trigger/RaidBlackTempleTriggers.h @@ -0,0 +1,518 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_RAIDBLACKTEMPLETRIGGERS_H +#define _PLAYERBOT_RAIDBLACKTEMPLETRIGGERS_H + +#include "Trigger.h" + +// General + +class BlackTempleBotIsNotInCombatTrigger : public Trigger +{ +public: + BlackTempleBotIsNotInCombatTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "black temple bot is not in combat") {} + bool IsActive() override; +}; + +// High Warlord Naj'entus + +class HighWarlordNajentusPullingBossTrigger : public Trigger +{ +public: + HighWarlordNajentusPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high warlord naj'entus pulling boss") {} + bool IsActive() override; +}; + +class HighWarlordNajentusBossEngagedByTanksTrigger : public Trigger +{ +public: + HighWarlordNajentusBossEngagedByTanksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high warlord naj'entus boss engaged by tanks") {} + bool IsActive() override; +}; + +class HighWarlordNajentusCastsNeedleSpinesTrigger : public Trigger +{ +public: + HighWarlordNajentusCastsNeedleSpinesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high warlord naj'entus casts needle spines") {} + bool IsActive() override; +}; + +class HighWarlordNajentusPlayerIsImpaledTrigger : public Trigger +{ +public: + HighWarlordNajentusPlayerIsImpaledTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high warlord naj'entus player is impaled") {} + bool IsActive() override; +}; + +class HighWarlordNajentusBossHasTidalShieldTrigger : public Trigger +{ +public: + HighWarlordNajentusBossHasTidalShieldTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high warlord naj'entus boss has tidal shield") {} + bool IsActive() override; +}; + +// Supremus + +class SupremusPullingBossOrChangingPhaseTrigger : public Trigger +{ +public: + SupremusPullingBossOrChangingPhaseTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "supremus pulling boss or changing phase") {} + bool IsActive() override; +}; + +class SupremusBossEngagedByRangedTrigger : public Trigger +{ +public: + SupremusBossEngagedByRangedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "supremus boss engaged by ranged") {} + bool IsActive() override; +}; + +class SupremusBossIsFixatedOnBotTrigger : public Trigger +{ +public: + SupremusBossIsFixatedOnBotTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "supremus boss is fixated on bot") {} + bool IsActive() override; +}; + +class SupremusVolcanoIsNearbyTrigger : public Trigger +{ +public: + SupremusVolcanoIsNearbyTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "supremus volcano is nearby") {} + bool IsActive() override; +}; + +class SupremusNeedToManagePhaseTimerTrigger : public Trigger +{ +public: + SupremusNeedToManagePhaseTimerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "supremus need to manage phase timer") {} + bool IsActive() override; +}; + +// Shade of Akama + +class ShadeOfAkamaKillingChannelersStartsPhase2Trigger : public Trigger +{ +public: + ShadeOfAkamaKillingChannelersStartsPhase2Trigger( + PlayerbotAI* botAI) : Trigger(botAI, "shade of akama killing channelers starts phase 2") {} + bool IsActive() override; +}; + +// Teron Gorefiend +class TeronGorefiendPullingBossTrigger : public Trigger +{ +public: + TeronGorefiendPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "teron gorefiend pulling boss") {} + bool IsActive() override; +}; + +class TeronGorefiendBossEngagedByTanksTrigger : public Trigger +{ +public: + TeronGorefiendBossEngagedByTanksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "teron gorefiend boss engaged by tanks") {} + bool IsActive() override; +}; + +class TeronGorefiendBossEngagedByRangedTrigger : public Trigger +{ +public: + TeronGorefiendBossEngagedByRangedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "teron gorefiend boss engaged by ranged") {} + bool IsActive() override; +}; + +class TeronGorefiendBossIsCastingShadowOfDeathTrigger : public Trigger +{ +public: + TeronGorefiendBossIsCastingShadowOfDeathTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "teron gorefiend boss is casting shadow of death") {} + bool IsActive() override; +}; + +class TeronGorefiendBotHasShadowOfDeathTrigger : public Trigger +{ +public: + TeronGorefiendBotHasShadowOfDeathTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "teron gorefiend bot has shadow of death") {} + bool IsActive() override; +}; + +class TeronGorefiendBotTransformedIntoVengefulSpiritTrigger : public Trigger +{ +public: + TeronGorefiendBotTransformedIntoVengefulSpiritTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "teron gorefiend bot transformed into vengeful spirit") {} + bool IsActive() override; +}; + +// Gurtogg Bloodboil + +class GurtoggBloodboilPullingBossTrigger : public Trigger +{ +public: + GurtoggBloodboilPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "gurtogg bloodboil pulling boss") {} + bool IsActive() override; +}; + +class GurtoggBloodboilBossEngagedByTanksTrigger : public Trigger +{ +public: + GurtoggBloodboilBossEngagedByTanksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "gurtogg bloodboil boss engaged by tanks") {} + bool IsActive() override; +}; + +class GurtoggBloodboilBossCastsBloodboilTrigger : public Trigger +{ +public: + GurtoggBloodboilBossCastsBloodboilTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "gurtogg bloodboil boss casts bloodboil") {} + bool IsActive() override; +}; + +class GurtoggBloodboilBotHasFelRageTrigger : public Trigger +{ +public: + GurtoggBloodboilBotHasFelRageTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "gurtogg bloodboil bot has fel rage") {} + bool IsActive() override; +}; + +class GurtoggBloodboilNeedToManagePhaseTimerTrigger : public Trigger +{ +public: + GurtoggBloodboilNeedToManagePhaseTimerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "gurtogg bloodboil need to manage phase timer") {} + bool IsActive() override; +}; + +// Reliquary of Souls + +class ReliquaryOfSoulsAggroResetsUponPhaseChangeTrigger : public Trigger +{ +public: + ReliquaryOfSoulsAggroResetsUponPhaseChangeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "reliquary of souls aggro resets upon phase change") {} + bool IsActive() override; +}; + +class ReliquaryOfSoulsEssenceOfSufferingFixatesOnClosestTargetTrigger : public Trigger +{ +public: + ReliquaryOfSoulsEssenceOfSufferingFixatesOnClosestTargetTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "reliquary of souls essence of suffering fixates on closest target") {} + bool IsActive() override; +}; + +class ReliquaryOfSoulsEssenceOfSufferingDisablesHealingTrigger : public Trigger +{ +public: + ReliquaryOfSoulsEssenceOfSufferingDisablesHealingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "reliquary of souls essence of suffering disables healing") {} + bool IsActive() override; +}; + +class ReliquaryOfSoulsEssenceOfDesireHasRuneShieldTrigger : public Trigger +{ +public: + ReliquaryOfSoulsEssenceOfDesireHasRuneShieldTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "reliquary of souls essence of desire has rune shield") {} + bool IsActive() override; +}; + +class ReliquaryOfSoulsEssenceOfDesireCastingDeadenTrigger : public Trigger +{ +public: + ReliquaryOfSoulsEssenceOfDesireCastingDeadenTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "reliquary of souls essence of desire casting deaden") {} + bool IsActive() override; +}; + +// Mother Shahraz + +class MotherShahrazPullingBossTrigger : public Trigger +{ +public: + MotherShahrazPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "mother shahraz pulling boss") {} + bool IsActive() override; +}; + +class MotherShahrazBossEngagedByTanksTrigger : public Trigger +{ +public: + MotherShahrazBossEngagedByTanksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "mother shahraz boss engaged by tanks") {} + bool IsActive() override; +}; + +class MotherShahrazTanksArePositioningBossTrigger : public Trigger +{ +public: + MotherShahrazTanksArePositioningBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "mother shahraz tanks are positioning boss") {} + bool IsActive() override; +}; + +class MotherShahrazSinisterBeamKnocksBackPlayersTrigger : public Trigger +{ +public: + MotherShahrazSinisterBeamKnocksBackPlayersTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "mother shahraz sinister beam knocks back players") {} + bool IsActive() override; +}; + +class MotherShahrazBotsAreLinkedByFatalAttractionTrigger : public Trigger +{ +public: + MotherShahrazBotsAreLinkedByFatalAttractionTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "mother shahraz bots are linked by fatal attraction") {} + bool IsActive() override; +}; + +// Illidari Council + +class IllidariCouncilPullingBossesTrigger : public Trigger +{ +public: + IllidariCouncilPullingBossesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council pulling bosses") {} + bool IsActive() override; +}; + +class IllidariCouncilGathiosEngagedByMainTankTrigger : public Trigger +{ +public: + IllidariCouncilGathiosEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council gathios engaged by main tank") {} + bool IsActive() override; +}; + +class IllidariCouncilGathiosCastingJudgementOfCommandTrigger : public Trigger +{ +public: + IllidariCouncilGathiosCastingJudgementOfCommandTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council gathios casting judgement of command") {} + bool IsActive() override; +}; + +class IllidariCouncilMalandeEngagedByFirstAssistTankTrigger : public Trigger +{ +public: + IllidariCouncilMalandeEngagedByFirstAssistTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council malande engaged by first assist tank") {} + bool IsActive() override; +}; + +class IllidariCouncilDarkshadowEngagedBySecondAssistTankTrigger : public Trigger +{ +public: + IllidariCouncilDarkshadowEngagedBySecondAssistTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council darkshadow engaged by second assist tank") {} + bool IsActive() override; +}; + +class IllidariCouncilZerevorEngagedByMageTankTrigger : public Trigger +{ +public: + IllidariCouncilZerevorEngagedByMageTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council zerevor engaged by mage tank") {} + bool IsActive() override; +}; + +class IllidariCouncilMageTankNeedsDedicatedHealerTrigger : public Trigger +{ +public: + IllidariCouncilMageTankNeedsDedicatedHealerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council mage tank needs dedicated healer") {} + bool IsActive() override; +}; + +class IllidariCouncilZerevorCastsDangerousAoesTrigger : public Trigger +{ +public: + IllidariCouncilZerevorCastsDangerousAoesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council zerevor casts dangerous aoes") {} + bool IsActive() override; +}; + +class IllidariCouncilPetsScrewUpThePullTrigger : public Trigger +{ +public: + IllidariCouncilPetsScrewUpThePullTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council pets screw up the pull") {} + bool IsActive() override; +}; + +class IllidariCouncilNeedToManageDpsTimerTrigger : public Trigger +{ +public: + IllidariCouncilNeedToManageDpsTimerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council need to manage dps timer") {} + bool IsActive() override; +}; + +class IllidariCouncilDeterminingDpsAssignmentsTrigger : public Trigger +{ +public: + IllidariCouncilDeterminingDpsAssignmentsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council determining dps assignments") {} + bool IsActive() override; +}; + +// Illidan Stormrage + +class IllidanStormrageTankNeedsAggroTrigger : public Trigger +{ +public: + IllidanStormrageTankNeedsAggroTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage tank needs aggro") {} + bool IsActive() override; +}; + +class IllidanStormrageBossCastsFlameCrashInFrontOfMainTankTrigger : public Trigger +{ +public: + IllidanStormrageBossCastsFlameCrashInFrontOfMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage boss casts flame crash in front of main tank") {} + bool IsActive() override; +}; + +class IllidanStormrageBotHasParasiticShadowfiendTrigger : public Trigger +{ +public: + IllidanStormrageBotHasParasiticShadowfiendTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage bot has parasitic shadowfiend") {} + bool IsActive() override; +}; + +class IllidanStormrageParasiticShadowfiendsRunWildTrigger : public Trigger +{ +public: + IllidanStormrageParasiticShadowfiendsRunWildTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage parasitic shadowfiends run wild") {} + bool IsActive() override; +}; + +class IllidanStormrageBossSummonedFlamesOfAzzinothTrigger : public Trigger +{ +public: + IllidanStormrageBossSummonedFlamesOfAzzinothTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage boss summoned flames of azzinoth") {} + bool IsActive() override; +}; + +class IllidanStormragePetsDieToFireTrigger : public Trigger +{ +public: + IllidanStormragePetsDieToFireTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage pets die to fire") {} + bool IsActive() override; +}; + +class IllidanStormrageGrateIsSafeFromFlamesTrigger : public Trigger +{ +public: + IllidanStormrageGrateIsSafeFromFlamesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage grate is safe from flames") {} + bool IsActive() override; +}; + +class IllidanStormrageBotStruckByDarkBarrageTrigger : public Trigger +{ +public: + IllidanStormrageBotStruckByDarkBarrageTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage bot struck by dark barrage") {} + bool IsActive() override; +}; + +class IllidanStormrageBossIsPreparingToLandTrigger : public Trigger +{ +public: + IllidanStormrageBossIsPreparingToLandTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage boss is preparing to land") {} + bool IsActive() override; +}; + +class IllidanStormrageBossDealsSplashDamageTrigger : public Trigger +{ +public: + IllidanStormrageBossDealsSplashDamageTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage boss deals splash damage") {} + bool IsActive() override; +}; + +class IllidanStormrageThisExpansionHatesMeleeTrigger : public Trigger +{ +public: + IllidanStormrageThisExpansionHatesMeleeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage this expansion hates melee") {} + bool IsActive() override; +}; + +class IllidanStormrageBossTransformsIntoDemonTrigger : public Trigger +{ +public: + IllidanStormrageBossTransformsIntoDemonTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage boss transforms into demon") {} + bool IsActive() override; +}; + +class IllidanStormrageBossSpawnsAddsTrigger : public Trigger +{ +public: + IllidanStormrageBossSpawnsAddsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage boss spawns adds") {} + bool IsActive() override; +}; + +class IllidanStormrageMaievPlacedShadowTrapTrigger : public Trigger +{ +public: + IllidanStormrageMaievPlacedShadowTrapTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage maiev placed shadow trap") {} + bool IsActive() override; +}; + +class IllidanStormrageNeedToManageDpsTimerAndRtiTrigger : public Trigger +{ +public: + IllidanStormrageNeedToManageDpsTimerAndRtiTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage need to manage dps timer and rti") {} + bool IsActive() override; +}; + +class IllidanStormrageNeedToClearHazardsBetweenPhasesTrigger : public Trigger +{ +public: + IllidanStormrageNeedToClearHazardsBetweenPhasesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage need to clear hazards between phases") {} + bool IsActive() override; +}; + +class IllidanStormrageCheatTrigger : public Trigger +{ +public: + IllidanStormrageCheatTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage cheat") {} + bool IsActive() override; +}; + +#endif diff --git a/src/Ai/Raid/BlackTemple/Util/RaidBlackTempleHelpers.cpp b/src/Ai/Raid/BlackTemple/Util/RaidBlackTempleHelpers.cpp new file mode 100644 index 000000000..75b0be0ba --- /dev/null +++ b/src/Ai/Raid/BlackTemple/Util/RaidBlackTempleHelpers.cpp @@ -0,0 +1,468 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "RaidBlackTempleHelpers.h" + +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +namespace BlackTempleHelpers +{ + // High Warlord Naj'entus + const Position NAJENTUS_TANK_POSITION = { 438.515f, 772.436f, 11.931f }; + + // Supremus + std::unordered_map supremusPhaseTimer; + + bool HasSupremusVolcanoNearby(PlayerbotAI* botAI, Player* bot) + { + constexpr float searchRadius = 20.0f; + std::list creatureList; + bot->GetCreatureListWithEntryInGrid( + creatureList, static_cast( + BlackTempleNpcs::NPC_SUPREMUS_VOLCANO), searchRadius); + + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + return true; + } + + return false; + } + + // Shade of Akama + const Position AKAMA_CHANNELER_POSITION = { 467.851f, 401.622f, 118.538f }; + + std::unordered_set hasReachedAkamaChannelerPosition; + + // Teron Gorefiend + const Position GOREFIEND_TANK_POSITION = { 597.653f, 402.284f, 187.090f }; + const Position GOREFIEND_DIE_POSITION = { 525.709f, 377.177f, 193.203f }; + + // Gurtogg Bloodboil + const Position GURTOGG_TANK_POSITION = { 735.987f, 272.451f, 063.554f }; + const Position GURTOGG_RANGED_POSITION = { 762.265f, 277.183f, 063.781f }; + const Position GURTOGG_SOAKER_POSITION = { 769.348f, 280.116f, 063.780f }; + + std::unordered_map gurtoggPhaseTimer; + + std::vector> GetGurtoggRangedRotationGroups(Player* bot) + { + Group* group = bot->GetGroup(); + std::vector rangedMembers; + std::vector> groups(3); + + if (!group) + return groups; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive()) + { + PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); + if (memberAI && memberAI->IsRanged(member)) + rangedMembers.push_back(member); + } + } + + for (size_t i = 0; i < rangedMembers.size(); ++i) + { + groups[i / 5].push_back(rangedMembers[i]); + if (groups[2].size() == 5) + break; + } + + return groups; + } + + int GetGurtoggActiveRotationGroup(Unit* gurtogg) + { + if (!gurtogg) + return -1; + + auto it = gurtoggPhaseTimer.find(gurtogg->GetMap()->GetInstanceId()); + if (it == gurtoggPhaseTimer.end()) + return -1; + + const time_t now = std::time(nullptr); + const time_t elapsed = now - it->second; + const int groupIndex = (elapsed % 30) / 10; // 3 groups, swapping every 10 seconds + + return groupIndex; + } + + // Mother Shahraz + const Position SHAHRAZ_TANK_POSITION = { 960.438f, 178.989f, 192.826f }; + const Position SHAHRAZ_TRANSITION_POSITION = { 951.327f, 179.550f, 192.550f }; + const Position SHAHRAZ_RANGED_POSITION = { 935.267f, 175.459f, 192.821f }; + std::unordered_map shahrazTankStep; + + TankPositionState GetShahrazTankPositionState(PlayerbotAI* botAI, Player* bot) + { + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return TankPositionState::Unknown; + + auto it = shahrazTankStep.find(mainTank->GetGUID()); + if (it != shahrazTankStep.end()) + return it->second; + + return TankPositionState::Unknown; + } + + // Illidari Council + const std::array GATHIOS_TANK_POSITIONS = {{ + { 662.977f, 296.246f, 271.688f }, + { 636.238f, 283.719f, 271.629f }, + { 655.571f, 261.377f, 271.687f }, + { 673.789f, 274.139f, 271.689f } + }}; + const Position ZEREVOR_TANK_POSITION = { 686.219f, 377.644f, 271.689f }; + const std::array ZEREVOR_HEALER_POSITIONS = {{ + { 661.385f, 351.219f, 271.690f }, + { 667.003f, 363.768f, 271.690f } + }}; + const Position MALANDE_TANK_POSITION = { 690.590f, 299.790f, 277.443f }; + + std::unordered_map councilDpsWaitTimer; + std::unordered_map gathiosTankStep; + std::unordered_map zerevorHealStep; + + // (1) First priority is an assistant Mage (real player or bot) + // (2) If no assistant Mage, then look for any Mage bot + Player* GetZerevorMageTank(Player* bot) + { + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + Player* fallbackMage = nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->getClass() != CLASS_MAGE) + continue; + + if (group->IsAssistant(member->GetGUID())) + return member; + + if (!fallbackMage && GET_PLAYERBOT_AI(member)) + fallbackMage = member; + } + + return fallbackMage; + } + + bool HasDangerousCouncilAura(Unit* unit) + { + static const std::array dangerousAuras = + { + static_cast(BlackTempleSpells::SPELL_CONSECRATION), + static_cast(BlackTempleSpells::SPELL_BLIZZARD), + static_cast(BlackTempleSpells::SPELL_FLAMESTRIKE) + }; + + for (uint32 aura : dangerousAuras) + { + if (unit->HasAura(aura)) + return true; + } + + return false; + } + + // Illidan Stormrage + const Position ILLIDAN_LANDING_POSITION = { 676.648f, 304.761f, 354.189f }; + const Position ILLIDAN_N_GRATE_POSITION = { 682.100f, 306.000f, 353.192f }; + const Position ILLIDAN_E_GRATE_POSITION = { 673.500f, 298.500f, 353.192f }; + const Position ILLIDAN_W_GRATE_POSITION = { 672.400f, 312.500f, 353.192f }; + const std::array GRATE_POSITIONS = {{ + ILLIDAN_N_GRATE_POSITION, + ILLIDAN_E_GRATE_POSITION, + ILLIDAN_W_GRATE_POSITION + }}; + + const Position ILLIDAN_E_GLAIVE_WAITING_POSITION = { 677.656f, 294.066f, 353.192f }; + const std::array E_GLAIVE_TANK_POSITIONS = {{ + { 683.000f, 295.000f, 354.000f }, + { 696.969f, 300.982f, 354.302f }, + { 691.112f, 287.461f, 354.363f }, + { 676.674f, 280.797f, 354.268f }, + { 664.414f, 284.834f, 354.271f }, + { 656.826f, 295.113f, 354.165f }, + { 665.000f, 304.000f, 354.000f } + }}; + + const Position ILLIDAN_W_GLAIVE_WAITING_POSITION = { 676.102f, 316.305f, 353.192f }; + const std::array W_GLAIVE_TANK_POSITIONS = {{ + { 697.208f, 313.475f, 354.234f }, + { 681.000f, 318.000f, 354.000f }, + { 664.000f, 307.000f, 354.000f }, + { 656.161f, 314.132f, 354.092f }, + { 665.080f, 326.905f, 354.128f }, + { 678.809f, 329.968f, 354.387f }, + { 690.889f, 324.277f, 354.204f } + }}; + + std::unordered_map flameTankWaypointIndex; + std::unordered_map illidanShadowTrapGuid; + std::unordered_map illidanShadowTrapDestination; + std::unordered_map illidanLastPhase; + std::unordered_map illidanBossDpsWaitTimer; + std::unordered_map illidanFlameDpsWaitTimer; + std::unordered_map eastFlameGuid; + std::unordered_map westFlameGuid; + + int GetIllidanPhase(Unit* illidan) + { + if (!illidan || illidan->GetHealth() == 1 || illidan->HasAura( + static_cast(BlackTempleSpells::SPELL_SHADOW_PRISON))) + { + return -1; + } + + // Transitioning from Phase 2 to Phase 3 + float x, y, z; + illidan->GetMotionMaster()->GetDestination(x, y, z); + Position dest(x, y, z); + if ((dest.GetExactDist2d(ILLIDAN_LANDING_POSITION) < 0.2f || + illidan->GetExactDist2d(ILLIDAN_LANDING_POSITION) < 0.2f) && + illidan->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + { + return 0; + } + + // Phase 2: Flying + if (illidan->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + return 2; + + // Phase 1: Health > 65% + if (illidan->GetHealthPct() > 65.0f) + return 1; + + // Phase 4: Demon Form + if (!illidan->HasAura(static_cast(BlackTempleSpells::SPELL_CAGED)) && + (illidan->HasAura(static_cast(BlackTempleSpells::SPELL_DEMON_FORM)) || + illidan->HasAura(static_cast(BlackTempleSpells::SPELL_DEMON_TRANSFORM_1)) || + illidan->HasAura(static_cast(BlackTempleSpells::SPELL_DEMON_TRANSFORM_2)) || + illidan->HasAura(static_cast(BlackTempleSpells::SPELL_DEMON_TRANSFORM_3)))) + { + return 4; + } + + // Phase 3: Normal (ground, 65-30%, not demon) + if (illidan->GetHealthPct() > 30.0f) + return 3; + + // Phase 5: Health <= 30% + if (illidan->GetHealthPct() <= 30.0f) + return 5; + + return -1; + } + + std::vector GetAllFlameCrashes(Player* bot) + { + std::vector flameCrashes; + std::list creatureList; + constexpr float searchRadius = 30.0f; + bot->GetCreatureListWithEntryInGrid( + creatureList, static_cast(BlackTempleNpcs::NPC_FLAME_CRASH), searchRadius); + + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + flameCrashes.push_back(creature); + } + + return flameCrashes; + } + + std::pair GetFlamesOfAzzinoth(Player* bot) + { + Unit* eastFlame = nullptr; + Unit* westFlame = nullptr; + + const uint32 instanceId = bot->GetMap()->GetInstanceId(); + + if (eastFlameGuid.find(instanceId) != eastFlameGuid.end()) + { + if (Unit* unit = ObjectAccessor::GetUnit(*bot, eastFlameGuid[instanceId])) + { + if (unit->IsAlive()) + eastFlame = unit; + } + } + + if (westFlameGuid.find(instanceId) != westFlameGuid.end()) + { + if (Unit* unit = ObjectAccessor::GetUnit(*bot, westFlameGuid[instanceId])) + { + if (unit->IsAlive()) + westFlame = unit; + } + } + + return { eastFlame, westFlame }; + } + + // (1) First priority is an assistant Warlock (real player or bot) + // (2) If no assistant Warlock, then look for any Warlock bot + Player* GetIllidanWarlockTank(Player* bot) + { + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + Player* fallbackWarlock = nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->getClass() != CLASS_WARLOCK) + continue; + + if (group->IsAssistant(member->GetGUID())) + return member; + + if (!fallbackWarlock && GET_PLAYERBOT_AI(member)) + fallbackWarlock = member; + } + + return fallbackWarlock; + } + + bool HasParasiticShadowfiend(Player* member) + { + if (!member) + return false; + + constexpr uint32 shadowfiendAura1 = + static_cast(BlackTempleSpells::SPELL_PARASITIC_SHADOWFIEND_1); + constexpr uint32 shadowfiendAura2 = + static_cast(BlackTempleSpells::SPELL_PARASITIC_SHADOWFIEND_2); + + return member->HasAura(shadowfiendAura1) || member->HasAura(shadowfiendAura2); + } + + // Get the first bot hunter that doesn't have Parasitic Shadowfiend + Player* GetIllidanTrapperHunter(Player* bot) + { + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + 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) && !HasParasiticShadowfiend(member)) + { + return member; + } + } + + return nullptr; + } + + Player* GetBotWithParasiticShadowfiend(Player* bot) + { + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && GET_PLAYERBOT_AI(member) && + HasParasiticShadowfiend(member)) + { + return member; + } + } + + return nullptr; + } + + EyeBlastDangerArea GetEyeBlastDangerArea(Player* bot) + { + constexpr float searchRadius = 100.0f; + std::list creatureList; + bot->GetCreatureListWithEntryInGrid( + creatureList, static_cast(BlackTempleNpcs::NPC_ILLIDAN_DB_TARGET), searchRadius); + + Creature* eyeBlastTrigger = nullptr; + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + { + eyeBlastTrigger = creature; + break; + } + } + + if (!eyeBlastTrigger) + return {}; + + Position startPos = Position(eyeBlastTrigger->GetPositionX(), eyeBlastTrigger->GetPositionY(), + eyeBlastTrigger->GetPositionZ()); + + float destX, destY, destZ; + eyeBlastTrigger->GetMotionMaster()->GetDestination(destX, destY, destZ); + Position endPos(destX, destY, destZ); + + if (startPos.GetExactDist2d(endPos) < 0.1f) + return {}; + + constexpr float eyeBlastWidth = 9.0f; + return { startPos, endPos, eyeBlastWidth }; + } + + bool IsPositionInEyeBlastDangerArea(const Position& pos, const EyeBlastDangerArea& area) + { + const float dx = area.end.GetPositionX() - area.start.GetPositionX(); + const float dy = area.end.GetPositionY() - area.start.GetPositionY(); + const float length = area.start.GetExactDist2d(area.end.GetPositionX(), area.end.GetPositionY()); + + if (length < 0.1f) + return false; + + const float projectionFactor = ( + (pos.GetPositionX() - area.start.GetPositionX()) * dx + ( + pos.GetPositionY() - area.start.GetPositionY()) * dy) / (length * length); + + const float clampedProjectionFactor = std::clamp(projectionFactor, 0.0f, 1.0f); + + const float closestX = area.start.GetPositionX() + clampedProjectionFactor * dx; + const float closestY = area.start.GetPositionY() + clampedProjectionFactor * dy; + + const float distToLine = pos.GetExactDist2d(closestX, closestY); + + return distToLine < area.width; + } + + GameObject* FindNearestTrap(PlayerbotAI* botAI, Player* bot) + { + GuidVector const& gos = + botAI->GetAiObjectContext()->GetValue("nearest game objects")->Get(); + + GameObject* nearestTrap = nullptr; + for (ObjectGuid const& guid : gos) + { + GameObject* go = botAI->GetGameObject(guid); + if (go && go->isSpawned() && + go->GetEntry() == static_cast(BlackTempleObjects::GO_SHADOW_TRAP)) + { + nearestTrap = go; + break; + } + } + + return nearestTrap; + } +} diff --git a/src/Ai/Raid/BlackTemple/Util/RaidBlackTempleHelpers.h b/src/Ai/Raid/BlackTemple/Util/RaidBlackTempleHelpers.h new file mode 100644 index 000000000..9ea6cb7df --- /dev/null +++ b/src/Ai/Raid/BlackTemple/Util/RaidBlackTempleHelpers.h @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_RAIDBLACKTEMPLEHELPERS_H_ +#define _PLAYERBOT_RAIDBLACKTEMPLEHELPERS_H_ + +#include +#include +#include +#include +#include + +#include "Common.h" +#include "ObjectGuid.h" +#include "Position.h" + +class GameObject; +class Player; +class PlayerbotAI; +class Unit; + +namespace BlackTempleHelpers +{ + +enum class BlackTempleSpells : uint32 +{ + // High Warlord Naj'entus + SPELL_IMPALING_SPINE = 39837, + SPELL_TIDAL_SHIELD = 39872, + + // Supremus + SPELL_SNARE_SELF = 41922, + + // Teron Gorefiend + SPELL_SHADOW_OF_DEATH = 40251, + SPELL_SPIRITUAL_VENGEANCE = 40268, + + SPELL_SPIRIT_LANCE = 40157, + SPELL_SPIRIT_CHAINS = 40175, + SPELL_SPIRIT_VOLLEY = 40314, + SPELL_SPIRIT_STRIKE = 40325, + + // Gurtogg Bloodboil + SPELL_BOSS_FEL_RAGE = 40594, + SPELL_PLAYER_FEL_RAGE = 40604, + SPELL_BLOODBOIL = 42005, + + // Reliquary of Souls + SPELL_DEADEN = 41410, + SPELL_RUNE_SHIELD = 41431, + + // Mother Shahraz + SPELL_FATAL_ATTRACTION = 41001, + + // Gathios the Shatterer + SPELL_BLESSING_OF_PROTECTION = 41450, + SPELL_BLESSING_OF_SPELL_WARDING = 41451, + SPELL_JUDGEMENT = 41467, + SPELL_SEAL_OF_COMMAND = 41469, + SPELL_CONSECRATION = 41541, + + // Veras Darkshadow + SPELL_VANISH = 41476, + + // High Nethermancer Zerevor + SPELL_DAMPEN_MAGIC = 41478, + SPELL_FLAMESTRIKE = 41481, + SPELL_BLIZZARD = 41482, + + // Illidan Stormrage + SPELL_DEMON_TRANSFORM_1 = 40511, + SPELL_DEMON_TRANSFORM_2 = 40398, + SPELL_DEMON_TRANSFORM_3 = 40510, + SPELL_DEMON_FORM = 40506, + SPELL_DARK_BARRAGE = 40585, + SPELL_SHADOW_PRISON = 40647, + SPELL_CAGED = 40695, + SPELL_PARASITIC_SHADOWFIEND_1 = 41917, // cast by Illidan (primary infection) + SPELL_PARASITIC_SHADOWFIEND_2 = 41914, // cast by Shadowfiend on contact (secondary infection) + + // Hunter + SPELL_FROST_TRAP = 13809, + SPELL_MISDIRECTION = 35079, + + // Shaman + SPELL_EARTHBIND_TOTEM = 2484, +}; + +enum class BlackTempleNpcs : uint32 +{ + // Supremus + NPC_SUPREMUS_VOLCANO = 23085, + + // Shade of Akama + NPC_ASHTONGUE_CHANNELER = 23421, + + // Teron Gorefiend + NPC_SHADOWY_CONSTRUCT = 23111, + + // Illidan Stormrage + NPC_FLAME_OF_AZZINOTH = 22997, + NPC_DEMON_FIRE = 23069, + NPC_ILLIDAN_DB_TARGET = 23070, + NPC_BLAZE = 23259, + NPC_FLAME_CRASH = 23336, + NPC_SHADOW_DEMON = 23375, + NPC_PARASITIC_SHADOWFIEND = 23498, +}; + +enum class BlackTempleItems : uint32 +{ + // High Warlord Naj'entus + ITEM_NAJENTUS_SPINE = 32408, +}; + +enum class BlackTempleObjects : uint32 +{ + // High Warlord Naj'entus + GO_NAJENTUS_SPINE = 185584, + + // Illidan Stormrage + GO_SHADOW_TRAP = 185916, +}; + +enum class TankPositionState : uint8 +{ + MovingToTransition = 0, + MovingToFinal = 1, + Positioned = 2, + Unknown = 255, +}; + +constexpr uint32 BLACK_TEMPLE_MAP_ID = 564; + +// High Warlord Naj'entus +extern const Position NAJENTUS_TANK_POSITION; + +// Supremus +extern std::unordered_map supremusPhaseTimer; +bool HasSupremusVolcanoNearby(PlayerbotAI* botAI, Player* bot); + +// Shade of Akama +extern const Position AKAMA_CHANNELER_POSITION; +extern std::unordered_set hasReachedAkamaChannelerPosition; + +// Teron Gorefiend +extern const Position GOREFIEND_TANK_POSITION; +extern const Position GOREFIEND_DIE_POSITION; + +// Gurtogg Bloodboil +extern const Position GURTOGG_TANK_POSITION; +extern const Position GURTOGG_RANGED_POSITION; +extern const Position GURTOGG_SOAKER_POSITION; +extern std::unordered_map gurtoggPhaseTimer; +std::vector> GetGurtoggRangedRotationGroups(Player* bot); +int GetGurtoggActiveRotationGroup(Unit* gurtogg); + +// Mother Shahraz +extern const Position SHAHRAZ_TANK_POSITION; +extern const Position SHAHRAZ_TRANSITION_POSITION; +extern const Position SHAHRAZ_RANGED_POSITION; +extern std::unordered_map shahrazTankStep; +TankPositionState GetShahrazTankPositionState(PlayerbotAI* botAI, Player* bot); + +// Illidari Council +constexpr float COUNCIL_FLOOR_Z_THRESHOLD = 270.000f; +extern const std::array GATHIOS_TANK_POSITIONS; +extern const Position MALANDE_TANK_POSITION; +extern const Position ZEREVOR_TANK_POSITION; +extern const std::array ZEREVOR_HEALER_POSITIONS; +extern std::unordered_map councilDpsWaitTimer; +extern std::unordered_map gathiosTankStep; +extern std::unordered_map zerevorHealStep; +Player* GetZerevorMageTank(Player* bot); +bool HasDangerousCouncilAura(Unit* unit); + +// Illidan Stormrage +extern const Position ILLIDAN_LANDING_POSITION; +extern const Position ILLIDAN_N_GRATE_POSITION; +extern const Position ILLIDAN_E_GRATE_POSITION; +extern const Position ILLIDAN_W_GRATE_POSITION; +extern const std::array GRATE_POSITIONS; +extern const Position ILLIDAN_E_GLAIVE_WAITING_POSITION; +extern const std::array E_GLAIVE_TANK_POSITIONS; +extern const Position ILLIDAN_W_GLAIVE_WAITING_POSITION; +extern const std::array W_GLAIVE_TANK_POSITIONS; +extern std::unordered_map flameTankWaypointIndex; +extern std::unordered_map illidanShadowTrapGuid; +extern std::unordered_map illidanShadowTrapDestination; +extern std::unordered_map illidanLastPhase; +extern std::unordered_map illidanBossDpsWaitTimer; +extern std::unordered_map illidanFlameDpsWaitTimer; +extern std::unordered_map eastFlameGuid; +extern std::unordered_map westFlameGuid; +int GetIllidanPhase(Unit* illidan); +std::vector GetAllFlameCrashes(Player* bot); +std::pair GetFlamesOfAzzinoth(Player* bot); +Player* GetIllidanWarlockTank(Player* bot); +bool HasParasiticShadowfiend(Player* member); +Player* GetIllidanTrapperHunter(Player* bot); +Player* GetBotWithParasiticShadowfiend(Player* bot); +struct EyeBlastDangerArea +{ + Position start; + Position end; + float width; +}; +EyeBlastDangerArea GetEyeBlastDangerArea(Player* bot); +bool IsPositionInEyeBlastDangerArea(const Position& pos, const EyeBlastDangerArea& area); +GameObject* FindNearestTrap(PlayerbotAI* botAI, Player* bot); + +} + +#endif diff --git a/src/Ai/Raid/RaidStrategyContext.h b/src/Ai/Raid/RaidStrategyContext.h index bdc76c4a7..f307c994f 100644 --- a/src/Ai/Raid/RaidStrategyContext.h +++ b/src/Ai/Raid/RaidStrategyContext.h @@ -12,6 +12,7 @@ #include "RaidSSCStrategy.h" #include "RaidTempestKeepStrategy.h" #include "RaidHyjalSummitStrategy.h" +#include "RaidBlackTempleStrategy.h" #include "RaidZulAmanStrategy.h" #include "RaidOsStrategy.h" #include "RaidEoEStrategy.h" @@ -35,6 +36,7 @@ public: creators["ssc"] = &RaidStrategyContext::ssc; creators["tempestkeep"] = &RaidStrategyContext::tempestkeep; creators["hyjal"] = &RaidStrategyContext::hyjal; + creators["blacktemple"] = &RaidStrategyContext::blacktemple; creators["zulaman"] = &RaidStrategyContext::zulaman; creators["wotlk-os"] = &RaidStrategyContext::wotlk_os; creators["wotlk-eoe"] = &RaidStrategyContext::wotlk_eoe; @@ -55,6 +57,7 @@ private: static Strategy* ssc(PlayerbotAI* botAI) { return new RaidSSCStrategy(botAI); } static Strategy* tempestkeep(PlayerbotAI* botAI) { return new RaidTempestKeepStrategy(botAI); } static Strategy* hyjal(PlayerbotAI* botAI) { return new RaidHyjalSummitStrategy(botAI); } + static Strategy* blacktemple(PlayerbotAI* botAI) { return new RaidBlackTempleStrategy(botAI); } static Strategy* zulaman(PlayerbotAI* botAI) { return new RaidZulAmanStrategy(botAI); } static Strategy* wotlk_os(PlayerbotAI* botAI) { return new RaidOsStrategy(botAI); } static Strategy* wotlk_eoe(PlayerbotAI* botAI) { return new RaidEoEStrategy(botAI); } diff --git a/src/Bot/Engine/BuildSharedActionContexts.cpp b/src/Bot/Engine/BuildSharedActionContexts.cpp index 4c2c8fad3..3cd68a20a 100644 --- a/src/Bot/Engine/BuildSharedActionContexts.cpp +++ b/src/Bot/Engine/BuildSharedActionContexts.cpp @@ -12,6 +12,7 @@ #include "Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h" #include "Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h" #include "Ai/Raid/HyjalSummit/RaidHyjalSummitActionContext.h" +#include "Ai/Raid/BlackTemple/RaidBlackTempleActionContext.h" #include "Ai/Raid/ZulAman/RaidZulAmanActionContext.h" #include "Ai/Raid/ObsidianSanctum/RaidOsActionContext.h" #include "Ai/Raid/EyeOfEternity/RaidEoEActionContext.h" @@ -36,6 +37,7 @@ void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextList allInstanceStrategies = { - "aq20", "bwl", "karazhan", "gruulslair", "hyjal", "icc", "magtheridon", - "moltencore", "naxx", "onyxia", "ssc", "tbc-ac", "tempestkeep", "ulduar", - "voa", "wotlk-an", "wotlk-cos", "wotlk-dtk", "wotlk-eoe", "wotlk-fos", + "aq20", "blacktemple", "bwl", "gruulslair", "hyjal", "icc", "karazhan", + "magtheridon", "moltencore", "naxx", "onyxia", "ssc", "tbc-ac", "tempestkeep", + "ulduar", "voa", "wotlk-an", "wotlk-cos", "wotlk-dtk", "wotlk-eoe", "wotlk-fos", "wotlk-gd", "wotlk-hol", "wotlk-hor", "wotlk-hos", "wotlk-nex", "wotlk-occ", "wotlk-ok", "wotlk-os", "wotlk-pos", "wotlk-toc", "wotlk-uk", "wotlk-up", "wotlk-vh", "zulaman" @@ -1669,6 +1669,9 @@ void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) case 558: strategyName = "tbc-ac"; // Auchindoun: Auchenai Crypts break; + case 564: + strategyName = "blacktemple"; // Black Temple + break; case 565: strategyName = "gruulslair"; // Gruul's Lair break;