diff --git a/src/Ai/Raid/RaidBossHelpers.cpp b/src/Ai/Raid/RaidBossHelpers.cpp index bcb48294..40dbae5d 100644 --- a/src/Ai/Raid/RaidBossHelpers.cpp +++ b/src/Ai/Raid/RaidBossHelpers.cpp @@ -77,39 +77,101 @@ void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target) // Intended for purposes of storing and erasing timers and trackers in associative containers bool IsMechanicTrackerBot(PlayerbotAI* botAI, Player* bot, uint32 mapId, Player* exclude) { - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || member->GetMapId() != mapId || - !GET_PLAYERBOT_AI(member) || !botAI->IsDps(member)) - continue; + if (!botAI->IsDps(bot) || !bot->IsAlive() || bot->GetMapId() != mapId) + return false; - if (member != exclude) - return member == bot; - } + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->GetMapId() != mapId || member == exclude) + continue; + + PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); + if (!memberAI || !memberAI->IsDps(member)) + continue; + + return member == bot; } return false; } -// Return the first matching alive unit from a cell search of nearby npcs -// More responsive than "find target," but performance cost is much higher -// Re: using the third parameter (false by default), some units are never considered -// to be in combat (e.g., totems) -Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry, bool requireInCombat) +// Requires the main tank to be alive +// Note that IsMainTank() will return the player with the main tank flag, even if dead +Player* GetGroupMainTank(PlayerbotAI* botAI, 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()) + continue; + + if (botAI->IsMainTank(member)) + return member; + } + + return nullptr; +} + +// Returns the alive assist tank of the specified index (0 = first, 1 = second, etc.) +// Priority: Assistants first, then Non-Assistants. +Player* GetGroupAssistTank(PlayerbotAI* botAI, Player* bot, uint8 index) +{ + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + uint8 assistantCount = 0; + std::vector nonAssistantTanks; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (botAI->IsAssistTank(member)) + { + if (group->IsAssistant(member->GetGUID())) + { + if (assistantCount == index) + return member; + + assistantCount++; + } + else + { + nonAssistantTanks.push_back(member); + } + } + } + + // If the index wasn't found among assistants, check the non-assistants that were saved + uint8 nonAssistantIndex = index - assistantCount; + if (nonAssistantIndex < nonAssistantTanks.size()) + return nonAssistantTanks[nonAssistantIndex]; + + return nullptr; +} + +// Return the first matching alive unit from PossibleTargetsValue within sightDistance from config +Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry) { auto const& npcs = - botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get(); for (auto const& npcGuid : npcs) { Unit* unit = botAI->GetUnit(npcGuid); if (unit && unit->IsAlive() && unit->GetEntry() == entry) - { - if (!requireInCombat || unit->IsInCombat()) - return unit; - } + return unit; } return nullptr; diff --git a/src/Ai/Raid/RaidBossHelpers.h b/src/Ai/Raid/RaidBossHelpers.h index 15c60353..31df8c3a 100644 --- a/src/Ai/Raid/RaidBossHelpers.h +++ b/src/Ai/Raid/RaidBossHelpers.h @@ -15,7 +15,10 @@ void MarkTargetWithCross(Player* bot, Unit* target); void MarkTargetWithMoon(Player* bot, Unit* target); void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target); bool IsMechanicTrackerBot(PlayerbotAI* botAI, Player* bot, uint32 mapId, Player* exclude = nullptr); -Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry, bool requireInCombat = false); +Player* GetGroupMainTank(PlayerbotAI* botAI, Player* bot); +Player* GetGroupAssistTank(PlayerbotAI* botAI, Player* bot, uint8 index); +Unit* GetFirstAliveUnitByEntry( + PlayerbotAI* botAI, uint32 entry); Unit* GetNearestPlayerInRadius(Player* bot, float radius); #endif diff --git a/src/Ai/Raid/RaidStrategyContext.h b/src/Ai/Raid/RaidStrategyContext.h index 4a040985..4d87c5ac 100644 --- a/src/Ai/Raid/RaidStrategyContext.h +++ b/src/Ai/Raid/RaidStrategyContext.h @@ -6,9 +6,10 @@ #include "RaidMcStrategy.h" #include "RaidBwlStrategy.h" #include "RaidKarazhanStrategy.h" -#include "RaidMagtheridonStrategy.h" #include "RaidGruulsLairStrategy.h" +#include "RaidMagtheridonStrategy.h" #include "RaidSSCStrategy.h" +#include "RaidTempestKeepStrategy.h" #include "RaidOsStrategy.h" #include "RaidEoEStrategy.h" #include "RaidVoAStrategy.h" @@ -25,9 +26,10 @@ public: creators["moltencore"] = &RaidStrategyContext::moltencore; creators["bwl"] = &RaidStrategyContext::bwl; creators["karazhan"] = &RaidStrategyContext::karazhan; - creators["magtheridon"] = &RaidStrategyContext::magtheridon; creators["gruulslair"] = &RaidStrategyContext::gruulslair; + creators["magtheridon"] = &RaidStrategyContext::magtheridon; creators["ssc"] = &RaidStrategyContext::ssc; + creators["tempestkeep"] = &RaidStrategyContext::tempestkeep; creators["wotlk-os"] = &RaidStrategyContext::wotlk_os; creators["wotlk-eoe"] = &RaidStrategyContext::wotlk_eoe; creators["voa"] = &RaidStrategyContext::voa; @@ -41,9 +43,10 @@ private: static Strategy* moltencore(PlayerbotAI* botAI) { return new RaidMcStrategy(botAI); } static Strategy* bwl(PlayerbotAI* botAI) { return new RaidBwlStrategy(botAI); } static Strategy* karazhan(PlayerbotAI* botAI) { return new RaidKarazhanStrategy(botAI); } - static Strategy* magtheridon(PlayerbotAI* botAI) { return new RaidMagtheridonStrategy(botAI); } static Strategy* gruulslair(PlayerbotAI* botAI) { return new RaidGruulsLairStrategy(botAI); } + static Strategy* magtheridon(PlayerbotAI* botAI) { return new RaidMagtheridonStrategy(botAI); } static Strategy* ssc(PlayerbotAI* botAI) { return new RaidSSCStrategy(botAI); } + static Strategy* tempestkeep(PlayerbotAI* botAI) { return new RaidTempestKeepStrategy(botAI); } static Strategy* wotlk_os(PlayerbotAI* botAI) { return new RaidOsStrategy(botAI); } static Strategy* wotlk_eoe(PlayerbotAI* botAI) { return new RaidEoEStrategy(botAI); } static Strategy* voa(PlayerbotAI* botAI) { return new RaidVoAStrategy(botAI); } diff --git a/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp new file mode 100644 index 00000000..edcc7ad5 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp @@ -0,0 +1,2050 @@ +#include "RaidTempestKeepActions.h" +#include "RaidTempestKeepHelpers.h" +#include "RaidTempestKeepKaelthasBossAI.h" +#include "AiFactory.h" +#include "EquipAction.h" +#include "LootAction.h" +#include "LootObjectStack.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +using namespace TempestKeepHelpers; + +// Trash + +bool CrimsonHandCenturionCastPolymorphAction::Execute(Event /*event*/) +{ + Unit* centurion = AI_VALUE2(Unit*, "find target", "crimson hand centurion"); + if (!centurion) + return false; + + if (centurion->GetHealth() == centurion->GetMaxHealth() && + !botAI->HasAura("polymorph", centurion) && + botAI->CanCastSpell("polymorph", centurion)) + { + return botAI->CastSpell("polymorph", centurion); + } + else if (centurion->HasAura(SPELL_ARCANE_FLURRY)) + { + botAI->Reset(); + return botAI->CastSpell("polymorph", centurion); + } + + return false; +} + +// Al'ar + +bool AlarMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (mainTank && botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", alar)) + return botAI->CastSpell("steady shot", alar); + + return false; +} + +bool AlarBossTanksMoveBetweenPlatformsAction::Execute(Event /*event*/) +{ + if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0, true)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + MarkTargetWithStar(bot, alar); + SetRtiTarget(botAI, "star", alar); + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (botAI->IsMainTank(bot)) + return PositionMainTank(alar, locationIndex); + else + return PositionAssistTank(alar, locationIndex); +} + +bool AlarBossTanksMoveBetweenPlatformsAction::PositionMainTank( + Unit* alar, int8 locationIndex) +{ + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& target = + (locationIndex == PLATFORM_0_IDX || locationIndex == PLATFORM_3_IDX) + ? PLATFORM_POSITIONS[0] : PLATFORM_POSITIONS[2]; + + if (bot->GetExactDist2d(target.GetPositionX(), target.GetPositionY()) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, target.GetPositionX(), target.GetPositionY(), + target.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + else if ((locationIndex == PLATFORM_0_IDX || locationIndex == PLATFORM_2_IDX) && + bot->GetTarget() != alar->GetGUID()) + return Attack(alar); + } + + return false; +} + +bool AlarBossTanksMoveBetweenPlatformsAction::PositionAssistTank( + Unit* alar, int8 locationIndex) +{ + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& target = + (locationIndex == PLATFORM_0_IDX || locationIndex == PLATFORM_1_IDX) + ? PLATFORM_POSITIONS[1] : PLATFORM_POSITIONS[3]; + + if (bot->GetExactDist2d(target.GetPositionX(), target.GetPositionY()) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, target.GetPositionX(), target.GetPositionY(), + target.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + else if ((locationIndex == PLATFORM_1_IDX || locationIndex == PLATFORM_3_IDX) && + bot->GetTarget() != alar->GetGUID()) + return Attack(alar); + } + + return false; +} + +bool AlarMeleeDpsMoveBetweenPlatformsAction::Execute(Event /*event*/) +{ + if (!botAI->IsMelee(bot) || !botAI->IsDps(bot)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + SetRtiTarget(botAI, "star", alar); + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& target = PLATFORM_POSITIONS[locationIndex]; + + if (bot->GetExactDist2d(target.GetPositionX(), target.GetPositionY()) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, target.GetPositionX(), target.GetPositionY(), + target.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + if (bot->GetTarget() != alar->GetGUID()) + return Attack(alar); + } + + return false; +} + +bool AlarRangedAndEmberTankMoveUnderPlatformsAction::Execute(Event /*event*/) +{ + if (!botAI->IsRanged(bot) && !botAI->IsAssistTankOfIndex(bot, 1, true)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& groundTarget = GROUND_POSITIONS[locationIndex]; + + constexpr float distRangedFromTarget = 8.0f; + constexpr float distTankFromTarget = 20.0f; + if (botAI->IsRanged(bot) && bot->GetExactDist2d( + groundTarget.GetPositionX(), groundTarget.GetPositionY()) > distRangedFromTarget) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, groundTarget.GetPositionX(), + groundTarget.GetPositionY(), groundTarget.GetPositionZ(), + distRangedFromTarget, MovementPriority::MOVEMENT_COMBAT); + } + else if (botAI->IsAssistTankOfIndex(bot, 1, true) && + !AI_VALUE2(Unit*, "find target", "ember of al'ar") && bot->GetExactDist2d( + groundTarget.GetPositionX(), groundTarget.GetPositionY()) > distTankFromTarget) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, groundTarget.GetPositionX(), + groundTarget.GetPositionY(), groundTarget.GetPositionZ(), + distTankFromTarget, MovementPriority::MOVEMENT_COMBAT); + } + } + + return false; +} + +bool AlarAssistTanksPickUpEmbersAction::Execute(Event /*event*/) +{ + if (!botAI->IsTank(bot)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (!isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return HandlePhase1Embers(alar); + else + return HandlePhase2Embers(alar); +} + +// Embers will be tanked by only the second assist tank in Phase 1 +bool AlarAssistTanksPickUpEmbersAction::HandlePhase1Embers(Unit* alar) +{ + if (!botAI->IsAssistTankOfIndex(bot, 1, true)) + return false; + + if (Unit* ember = AI_VALUE2(Unit*, "find target", "ember of al'ar")) + { + MarkTargetWithSquare(bot, ember); + SetRtiTarget(botAI, "square", ember); + + if (bot->GetTarget() != ember->GetGUID()) + return Attack(ember); + + if (ember->GetVictim() == bot) + { + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& groundTarget = GROUND_POSITIONS[locationIndex]; + const Position& center = ALAR_POINT_MIDDLE; + float dx = center.GetPositionX() - groundTarget.GetPositionX(); + float dy = center.GetPositionY() - groundTarget.GetPositionY(); + float distToCenter = + groundTarget.GetExactDist2d(center.GetPositionX(), center.GetPositionY()); + + constexpr float moveDist = 25.0f; + float targetX = groundTarget.GetPositionX() + (dx / distToCenter) * moveDist; + float targetY = groundTarget.GetPositionY() + (dy / distToCenter) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, targetX, targetY, groundTarget.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + else + { + constexpr float safeDistance = 16.0f; + if (GetNearestPlayerInRadius(bot, safeDistance)) + return MoveFromGroup(safeDistance); + } + } + else if (!bot->IsWithinMeleeRange(ember)) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, ember->GetPositionX(), ember->GetPositionY(), + ember->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +// One Ember will be tanked by the second assist tank in Phase 2, and the other by +// the main tank or first assist tank (whichever is not tanking Al'ar) +bool AlarAssistTanksPickUpEmbersAction::HandlePhase2Embers(Unit* alar) +{ + auto [firstEmber, secondEmber] = GetFirstTwoEmbersOfAlar(botAI); + + if (botAI->IsAssistTankOfIndex(bot, 1, true) && firstEmber) + { + MarkTargetWithSquare(bot, firstEmber); + SetRtiTarget(botAI, "square", firstEmber); + + if (firstEmber->GetVictim() != bot) + { + if (bot->GetTarget() != firstEmber->GetGUID()) + return Attack(firstEmber); + + return botAI->DoSpecificAction("taunt spell", Event(), true); + } + else if (bot->IsWithinMeleeRange(firstEmber)) + { + constexpr float safeDistance = 16.0f; + if (GetNearestNonTankPlayerInRadius(botAI, bot, safeDistance)) + return MoveFromGroup(safeDistance); + } + } + else if (GetSecondEmberTank(botAI) == bot && secondEmber) + { + MarkTargetWithCircle(bot, secondEmber); + SetRtiTarget(botAI, "circle", secondEmber); + + if (secondEmber->GetVictim() != bot) + { + if (bot->GetTarget() != secondEmber->GetGUID()) + return Attack(secondEmber); + + return botAI->DoSpecificAction("taunt spell", Event(), true); + } + else if (bot->IsWithinMeleeRange(secondEmber)) + { + constexpr float safeDistance = 16.0f; + if (GetNearestNonTankPlayerInRadius(botAI, bot, safeDistance)) + return MoveFromGroup(safeDistance); + } + } + + return false; +} + +bool AlarRangedDpsPrioritizeEmbersAction::Execute(Event /*event*/) +{ + auto [firstEmber, secondEmber] = GetFirstTwoEmbersOfAlar(botAI); + + constexpr float safeDistance = 16.0f; + if (firstEmber) + { + if (bot->GetDistance2d(firstEmber) < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(firstEmber, safeDistance - bot->GetDistance2d(firstEmber)); + } + + SetRtiTarget(botAI, "square", firstEmber); + if (bot->GetTarget() != firstEmber->GetGUID()) + return Attack(firstEmber); + } + else if (secondEmber) + { + if (bot->GetDistance2d(secondEmber) < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(secondEmber, safeDistance - bot->GetDistance2d(secondEmber)); + } + + SetRtiTarget(botAI, "circle", secondEmber); + if (bot->GetTarget() != secondEmber->GetGUID()) + return Attack(secondEmber); + } + else if (Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar")) + { + SetRtiTarget(botAI, "star", alar); + if (bot->GetTarget() != alar->GetGUID()) + return Attack(alar); + } + + return false; +} + +// Jump from platform during Flame Quills and wait at assigned position after landing +bool AlarJumpFromPlatformAction::Execute(Event /*event*/) +{ + if (bot->GetPositionZ() > ALAR_BALCONY_Z) + { + int8 closestPlatform; + Position ground; + GetClosestPlatformAndGround(bot->GetPosition(), closestPlatform, ground); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return JumpTo(TEMPEST_KEEP_MAP_ID, ground.GetPositionX(), ground.GetPositionY(), + ground.GetPositionZ(), MovementPriority::MOVEMENT_FORCED); + } + else + { + constexpr float distAlarTankFromPosition = 5.0f; + constexpr float distEmberTankFromPos = 25.0f; + constexpr float distMeleeDpsFromPos = 5.0f; + constexpr float distRangedFromPos = 10.0f; + + if (botAI->IsMainTank(bot) && + bot->GetExactDist2d(ALAR_SW_RAMP_BASE.GetPositionX(), + ALAR_SW_RAMP_BASE.GetPositionY()) > distAlarTankFromPosition) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, ALAR_SW_RAMP_BASE.GetPositionX(), + ALAR_SW_RAMP_BASE.GetPositionY(), ALAR_SW_RAMP_BASE.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + else if (botAI->IsAssistTankOfIndex(bot, 0, true) && + bot->GetExactDist2d(ALAR_SE_RAMP_BASE.GetPositionX(), + ALAR_SE_RAMP_BASE.GetPositionY()) > distAlarTankFromPosition) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, ALAR_SE_RAMP_BASE.GetPositionX(), + ALAR_SE_RAMP_BASE.GetPositionY(), ALAR_SE_RAMP_BASE.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + else if (botAI->IsAssistTankOfIndex(bot, 1, true) && + bot->GetExactDist2d(ALAR_POINT_MIDDLE.GetPositionX(), + ALAR_POINT_MIDDLE.GetPositionY()) > distEmberTankFromPos) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, ALAR_POINT_MIDDLE.GetPositionX(), + ALAR_POINT_MIDDLE.GetPositionY(), ALAR_POINT_MIDDLE.GetPositionZ(), + distEmberTankFromPos, MovementPriority::MOVEMENT_FORCED); + } + else if (botAI->IsMelee(bot) && + bot->GetExactDist2d(ALAR_ROOM_S_CENTER.GetPositionX(), + ALAR_ROOM_S_CENTER.GetPositionY()) > distMeleeDpsFromPos) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, ALAR_ROOM_S_CENTER.GetPositionX(), + ALAR_ROOM_S_CENTER.GetPositionY(), ALAR_ROOM_S_CENTER.GetPositionZ(), + distMeleeDpsFromPos, MovementPriority::MOVEMENT_FORCED); + } + else if (botAI->IsRanged(bot) && + bot->GetExactDist2d(ALAR_POINT_MIDDLE.GetPositionX(), + ALAR_POINT_MIDDLE.GetPositionY()) > distRangedFromPos) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, ALAR_POINT_MIDDLE.GetPositionX(), + ALAR_POINT_MIDDLE.GetPositionY(), ALAR_POINT_MIDDLE.GetPositionZ(), + distRangedFromPos, MovementPriority::MOVEMENT_FORCED); + } + } + + return false; +} + +bool AlarMoveAwayFromRebirthAction::Execute(Event /*event*/) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (bot->GetPositionZ() > ALAR_BALCONY_Z) + { + int8 closestPlatform; + Position ground; + GetClosestPlatformAndGround(bot->GetPosition(), closestPlatform, ground); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return JumpTo(TEMPEST_KEEP_MAP_ID, ground.GetPositionX(), ground.GetPositionY(), + ground.GetPositionZ(), MovementPriority::MOVEMENT_FORCED); + } + else + { + float currentDistance = bot->GetDistance2d(alar); + constexpr float safeDistance = 20.0f; + if (currentDistance < safeDistance) + return MoveAway(alar, safeDistance - currentDistance); + } + + return false; +} + +// Main tank and first assist tank will swap tanking Al'ar when Melt Armor is applied +bool AlarSwapTanksOnBossAction::Execute(Event /*event*/) +{ + if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0, true)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (alar->GetHealth() == alar->GetMaxHealth()) + { + SetRtiTarget(botAI, "star", alar); + if (bot->GetTarget() != alar->GetGUID()) + return Attack(alar); + } + + Player* secondEmberTank = GetSecondEmberTank(botAI); + if (secondEmberTank && secondEmberTank != bot) + { + SetRtiTarget(botAI, "star", alar); + if (bot->GetTarget() != alar->GetGUID()) + return Attack(alar); + else if (alar->GetVictim() != bot) + return botAI->DoSpecificAction("taunt spell", Event(), true); + } + + return false; +} + +bool AlarAvoidFlamePatchesAndDiveBombsAction::Execute(Event /*event*/) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + return AvoidFlamePatch() || HandleDiveBomb(alar); +} + +bool AlarAvoidFlamePatchesAndDiveBombsAction::AvoidFlamePatch() +{ + constexpr float searchRadius = 40.0f; + constexpr float hazardRadius = 8.0f; + + std::vector flamePatches = + GetAllHazardTriggers(bot, NPC_FLAME_PATCH, searchRadius); + + for (Unit* flamePatch : flamePatches) + { + if (bot->GetExactDist2d(flamePatch) < hazardRadius) + { + Position safestPos = FindSafestNearbyPosition(bot, flamePatches, hazardRadius); + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(TEMPEST_KEEP_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), + safestPos.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + + return false; +} + +bool AlarAvoidFlamePatchesAndDiveBombsAction::HandleDiveBomb(Unit* alar) +{ + if ((alar->HasUnitState(UNIT_STATE_CASTING) && + alar->FindCurrentSpellBySpellId(SPELL_REBIRTH_DIVE)) || + !alar->IsVisible()) + { + float currentDistance = bot->GetDistance2d(alar); + constexpr float safeDistance = 20.0f; + if (currentDistance < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(alar, safeDistance - currentDistance); + } + } + else + { + Position dest; + if (GetAlarCurrentLocationIndex(alar) == POINT_QUILL_OR_DIVE_IDX || + GetAlarDestinationLocationIndex(alar, dest) == POINT_QUILL_OR_DIVE_IDX) + { + constexpr float safeDistance = 10.0f; + constexpr uint32 minInterval = 0; + if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) + return FleePosition(nearestPlayer->GetPosition(), safeDistance, minInterval); + } + } + + return false; +} + +// For Phase 2, ensure that bots don't get too far away and become inactive +bool AlarReturnToRoomCenterAction::Execute(Event /*event*/) +{ + constexpr float distFromCenter = 45.0f; + const Position& center = ALAR_ROOM_CENTER; + if (bot->GetVictim() == nullptr && + bot->GetExactDist2d(center.GetPositionX(), center.GetPositionY()) > distFromCenter) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, center.GetPositionX(), center.GetPositionY(), + center.GetPositionZ(), distFromCenter - 5.0f, + MovementPriority::MOVEMENT_COMBAT); + } + + return false; +} + +bool AlarManagePhaseTrackerAction::Execute(Event /*event*/) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + const uint32 instanceId = alar->GetMap()->GetInstanceId(); + + if (alar->GetHealthPct() > 99.5f && alar->GetPositionZ() >= ALAR_BALCONY_Z) + { + isAlarInPhase2.erase(instanceId); + lastRebirthState.erase(instanceId); + } + + bool rebirthActive = alar->HasUnitState(UNIT_STATE_CASTING) && + alar->FindCurrentSpellBySpellId(SPELL_REBIRTH_PHASE2); + bool lastRebirth = lastRebirthState[instanceId]; + + if (lastRebirth && !rebirthActive) + isAlarInPhase2[instanceId] = true; + + lastRebirthState[instanceId] = rebirthActive; + + return false; +} + +// Void Reaver + +bool VoidReaverTanksPositionBossAction::Execute(Event /*event*/) +{ + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (!voidReaver) + return false; + + const Position& position = VOID_REAVER_TANK_POSITION; + + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float distToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (bot->IsWithinMeleeRange(voidReaver) && distToPosition > 2.0f) + { + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + + return false; +} + +bool VoidReaverUseAggroDumpAbilityAction::Execute(Event /*event*/) +{ + botAI->Reset(); + static const std::array spells = + { + "divine protection", + "fade", + "feign death", + "ice block", + "soulshatter", + "vanish", + }; + for (const char* spell : spells) + { + if (botAI->CanCastSpell(spell, bot) && + botAI->CastSpell(spell, bot)) + return true; + } + + return false; +} + +bool VoidReaverSpreadRangedAction::Execute(Event /*event*/) +{ + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (!voidReaver) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + ObjectGuid guid = bot->GetGUID(); + + if (!hasReachedVoidReaverPosition[guid]) + { + int healerCount = 0, rangedDpsCount = 0; + int healerIndex = GetHealerIndex(group, healerCount); + int rangedDpsIndex = GetRangedDpsIndex(group, rangedDpsCount); + + // Void Reaver's hitbox is 15 yards (GetDistance2d() of 16.5 yards for non-Tauren) + constexpr float radius = 45.0f; + float targetX = 0.0f; + float targetY = 0.0f; + + if (healerIndex != -1 && healerCount > 0) + { + float angle = 2 * M_PI * healerIndex / healerCount; + targetX = voidReaver->GetPositionX() + radius * std::cos(angle); + targetY = voidReaver->GetPositionY() + radius * std::sin(angle); + } + else if (rangedDpsIndex != -1 && rangedDpsCount > 0) + { + float angle = 2 * M_PI * rangedDpsIndex / rangedDpsCount; + if (healerCount > 0) + angle += M_PI / rangedDpsCount; + + targetX = voidReaver->GetPositionX() + radius * std::cos(angle); + targetY = voidReaver->GetPositionY() + radius * std::sin(angle); + } + + if (bot->GetExactDist2d(targetX, targetY) > 2.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + else + { + hasReachedVoidReaverPosition[guid] = true; + } + } + else + { + constexpr float safeDistance = 20.0f; + constexpr uint32 minInterval = 1000; + if (bot->GetDistance2d(voidReaver) < safeDistance) + return FleePosition(voidReaver->GetPosition(), safeDistance, minInterval); + } + + return false; +} + +int VoidReaverSpreadRangedAction::GetHealerIndex(Group* group, int& healerCount) +{ + std::vector healers; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !botAI->IsHeal(member)) + continue; + + healers.push_back(member); + } + + healerCount = healers.size(); + auto it = std::find(healers.begin(), healers.end(), bot); + return (it != healers.end()) ? std::distance(healers.begin(), it) : -1; +} + +int VoidReaverSpreadRangedAction::GetRangedDpsIndex(Group* group, int& rangedDpsCount) +{ + std::vector rangedDps; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !botAI->IsRanged(member) || botAI->IsHeal(member)) + continue; + + rangedDps.push_back(member); + } + + rangedDpsCount = rangedDps.size(); + auto it = std::find(rangedDps.begin(), rangedDps.end(), bot); + return (it != rangedDps.end()) ? std::distance(rangedDps.begin(), it) : -1; +} + +bool VoidReaverAvoidArcaneOrbAction::Execute(Event /*event*/) +{ + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (!voidReaver) + return false; + + auto it = voidReaverArcaneOrbs.find(bot->GetMap()->GetInstanceId()); + if (it == voidReaverArcaneOrbs.end() || it->second.empty()) + return false; + + uint32 currentTime = getMSTime(); + constexpr uint32 orbDuration = 7000; + constexpr float safeDistance = 22.0f; + bool shouldFlee = false; + Position fleeDest; + + for (auto const& orb : it->second) + { + if (getMSTimeDiff(orb.castTime, currentTime) <= orbDuration) + { + if (bot->GetExactDist2d(orb.destination.GetPositionX(), + orb.destination.GetPositionY()) < safeDistance) + { + shouldFlee = true; + fleeDest = orb.destination; + break; + } + } + } + + it->second.erase(std::remove_if(it->second.begin(), it->second.end(), + [currentTime](const ArcaneOrbData& orb) { + return getMSTimeDiff(orb.castTime, currentTime) > orbDuration; + }), it->second.end()); + + if (shouldFlee) + { + constexpr uint32 minInterval = 0; + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return FleePosition(fleeDest, safeDistance, minInterval); + } + + return false; +} + +bool VoidReaverEraseTrackersAction::Execute(Event /*event*/) +{ + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (voidReaver) + return false; + + bool erased = false; + + if (voidReaverArcaneOrbs.erase(bot->GetMap()->GetInstanceId())) + erased = true; + + if (hasReachedVoidReaverPosition.erase(bot->GetGUID())) + erased = true; + + return erased; +} + +// High Astromancer Solarian + +bool HighAstromancerSolarianRangedLeaveSpaceForMeleeAction::Execute(Event /*event*/) +{ + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + if (!astromancer) + return false; + + float currentDistance = bot->GetExactDist2d(astromancer); + constexpr float minDistance = 20.0f; + if (currentDistance < minDistance) + return MoveAway(astromancer, minDistance - currentDistance); + + return false; +} + +bool HighAstromancerSolarianMoveAwayFromGroupAction::Execute(Event /*event*/) +{ + constexpr float safeDistance = 15.0f; + if (GetNearestPlayerInRadius(bot, safeDistance)) + { + botAI->Reset(); + return MoveFromGroup(safeDistance); + } + + return false; +} + +bool HighAstromancerSolarianStackForAoeAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* stackTarget = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsRanged(member)) + { + stackTarget = member; + break; + } + } + + if (stackTarget && bot != stackTarget && bot->GetExactDist2d(stackTarget) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, stackTarget->GetPositionX(), stackTarget->GetPositionY(), + stackTarget->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +// Split melee into two groups, one on each Solarium Priest +bool HighAstromancerSolarianTargetSolariumPriestsAction::Execute(Event /*event*/) +{ + auto priestsPair = GetSolariumPriests(botAI); + if (!priestsPair.first || !priestsPair.second) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + auto meleeMembers = GetMeleeBots(group); + if (meleeMembers.empty()) + return false; + + Unit* targetPriest = AssignSolariumPriestsToBots(priestsPair, meleeMembers); + if (!targetPriest) + return false; + + auto it = std::find(meleeMembers.begin(), meleeMembers.end(), bot); + if (it == meleeMembers.end()) + return false; + + if (targetPriest == priestsPair.first) + { + MarkTargetWithSquare(bot, targetPriest); + SetRtiTarget(botAI, "square", targetPriest); + } + else + { + MarkTargetWithStar(bot, targetPriest); + SetRtiTarget(botAI, "star", targetPriest); + } + + if (bot->GetTarget() != targetPriest->GetGUID()) + return Attack(targetPriest); + + return false; +} + +std::pair HighAstromancerSolarianTargetSolariumPriestsAction::GetSolariumPriests(PlayerbotAI* botAI) +{ + Unit* lowest = nullptr; + Unit* highest = nullptr; + + for (auto const& guid : + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get()) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->GetEntry() == NPC_SOLARIUM_PRIEST) + { + if (!lowest || unit->GetGUID().GetRawValue() < lowest->GetGUID().GetRawValue()) + lowest = unit; + + if (!highest || unit->GetGUID().GetRawValue() > highest->GetGUID().GetRawValue()) + highest = unit; + } + } + + return {lowest, highest}; +} + +std::vector HighAstromancerSolarianTargetSolariumPriestsAction::GetMeleeBots(Group* group) +{ + std::vector meleeMembers; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsMelee(member) && GET_PLAYERBOT_AI(member)) + meleeMembers.push_back(member); + } + + return meleeMembers; +} + +Unit* HighAstromancerSolarianTargetSolariumPriestsAction::AssignSolariumPriestsToBots( + const std::pair& priestsPair, const std::vector& meleeMembers) +{ + if (!priestsPair.first || !priestsPair.second || meleeMembers.empty()) + return nullptr; + + auto it = std::find(meleeMembers.begin(), meleeMembers.end(), bot); + if (it == meleeMembers.end()) + return nullptr; + + size_t botIndex = std::distance(meleeMembers.begin(), it); + size_t totalMelee = meleeMembers.size(); + + if (totalMelee == 1) + return priestsPair.first; + + size_t split = totalMelee / 2; + + if (botIndex < split) + return priestsPair.first; + else + return priestsPair.second; +} + +bool HighAstromancerSolarianCastFearWardOnMainTankAction::Execute(Event /*event*/) +{ + Player* mainTank = GetGroupMainTank(botAI, bot); + if (mainTank && botAI->CanCastSpell("fear ward", mainTank)) + return botAI->CastSpell("fear ward", mainTank); + + return false; +} + +// Kael'thas Sunstrider + +bool KaelthasSunstriderKiteThaladredAction::Execute(Event /*event*/) +{ + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + if (!thaladred) + return false; + + float currentDistance = bot->GetExactDist2d(thaladred); + constexpr float safeDistance = 25.0f; + if (currentDistance < safeDistance) + return MoveAway(thaladred, safeDistance - currentDistance); + + return false; +} + +// Misdirect order: (1) Capernian, (2) Telonicus, (3) Capernian (again for good measure) +bool KaelthasSunstriderMisdirectAdvisorsToTanksAction::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() >= 3) + 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* advisorTarget = nullptr; + Player* tankTarget = nullptr; + if (hunterIndex == 0 || hunterIndex == 2) + { + advisorTarget = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + tankTarget = GetCapernianTank(bot); + } + else if (hunterIndex == 1) + { + advisorTarget = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + tankTarget = GetGroupAssistTank(botAI, bot, 0); + } + + if (!advisorTarget || + advisorTarget->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || + advisorTarget->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE) || + advisorTarget->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + return false; + + if (!tankTarget || !tankTarget->IsAlive()) + return false; + + if (botAI->CanCastSpell("misdirection", tankTarget)) + return botAI->CastSpell("misdirection", tankTarget); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", advisorTarget)) + return botAI->CastSpell("steady shot", advisorTarget); + + return false; +} + +bool KaelthasSunstriderMainTankPositionSanguinarAction::Execute(Event /*event*/) +{ + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + if (!sanguinar) + return false; + + MarkTargetWithStar(bot, sanguinar); + SetRtiTarget(botAI, "star", sanguinar); + + if (bot->GetTarget() != sanguinar->GetGUID()) + return Attack(sanguinar); + + if (sanguinar->GetVictim() == bot && bot->IsWithinMeleeRange(sanguinar)) + { + const Position& position = SANGUINAR_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +bool KaelthasSunstriderCastFearWardOnSanguinarTankAction::Execute(Event /*event*/) +{ + Player* mainTank = GetGroupMainTank(botAI, bot); + if (mainTank && botAI->CanCastSpell("fear ward", mainTank)) + return botAI->CastSpell("fear ward", mainTank); + + return false; +} + +bool KaelthasSunstriderWarlockTankPositionCapernianAction::Execute(Event /*event*/) +{ + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (!capernian) + return false; + + MarkTargetWithCircle(bot, capernian); + SetRtiTarget(botAI, "circle", capernian); + + if (bot->GetTarget() != capernian->GetGUID() && + botAI->CanCastSpell("searing pain", capernian) && + botAI->CastSpell("searing pain", capernian)) + return true; + + if (capernian->GetVictim() == bot) + { + float currentDist = bot->GetDistance2d(capernian); + constexpr float minDistance = 28.0f; + if (currentDist < minDistance) + return MoveAway(capernian, minDistance - currentDist); + } + + if (botAI->CanCastSpell("searing pain", capernian)) + return botAI->CastSpell("searing pain", capernian); + + return false; +} + +bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::Execute(Event /*event*/) +{ + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (!capernian) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return false; + + if (botAI->IsRanged(bot) && capernian->GetVictim() != bot && + RangedBotsDisperse(kaelAI, capernian)) + { + return true; + } + else if (botAI->IsMelee(bot) && kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR && + MeleeStayBackFromCapernian(capernian)) + { + return true; + } + + return false; +} + +bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::RangedBotsDisperse(boss_kaelthas* kaelAI, Unit* capernian) +{ + if (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR) + { + Group* group = bot->GetGroup(); + if (!group) + return false; + + 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); + } + + if (healers.empty() && rangedDps.empty()) + return false; + + size_t count = healers.size() + rangedDps.size(); + size_t botIndex = 0; + float radius = 0.0f; + float angle = 0.0f; + + // Spread is 90-degree arc for healers and 120-degree arc for ranged DPS + float arcSpan = botAI->IsHeal(bot) ? M_PI / 2.0f : 2.0f * M_PI / 3.0f; + constexpr float arcCenter = 2.9f; + float arcStart = arcCenter - arcSpan / 2.0f; + + // Capernian's hitbox is 4.5 yards (GetDistance2d of 6.0f for non-Tauren) + if (botAI->IsHeal(bot)) + { + auto findIt = std::find(healers.begin(), healers.end(), bot); + botIndex = (findIt != healers.end()) ? std::distance(healers.begin(), findIt) : 0; + radius = 42.0f; + count = healers.size(); + } + else + { + auto findIt = std::find(rangedDps.begin(), rangedDps.end(), bot); + botIndex = (findIt != rangedDps.end()) ? std::distance(rangedDps.begin(), findIt) : 0; + radius = 34.0f; + count = rangedDps.size(); + } + + angle = (count == 1) ? arcCenter : + (arcStart + arcSpan * static_cast(botIndex) / static_cast(count - 1)); + + float targetX = capernian->GetPositionX() + radius * std::cos(angle); + float targetY = capernian->GetPositionY() + radius * std::sin(angle); + + if (bot->GetExactDist2d(targetX, targetY) > 1.0f) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(TEMPEST_KEEP_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_FORCED, true, false); + } + } + else + { + if (AI_VALUE2(Unit*, "find target", "thaladred the darkener")) + return false; + + const float safeDistance = 6.0f; + constexpr uint32 minInterval = 1000; + if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) + return FleePosition(nearestPlayer->GetPosition(), safeDistance, minInterval); + } + + return false; +} + +bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::MeleeStayBackFromCapernian(Unit* capernian) +{ + // Main tank purposely stays in range to bait Conflagration in Phase 1 + if (botAI->IsMainTank(bot)) + { + // MoveTo called for a WorldObj is a GetDistance() check so both hitboxes are account for + constexpr float desiredDist = 15.0f; + botAI->Reset(); + return MoveTo(capernian, desiredDist, MovementPriority::MOVEMENT_FORCED); + } + else + { + constexpr float safeDistance = 42.0f; + float currentDistance = bot->GetDistance2d(capernian); + if (currentDistance < safeDistance) + { + botAI->Reset(); + return MoveAway(capernian, safeDistance - currentDistance); + } + else + { + return true; + } + } +} + +bool KaelthasSunstriderFirstAssistTankPositionTelonicusAction::Execute(Event /*event*/) +{ + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + if (!telonicus) + return false; + + MarkTargetWithTriangle(bot, telonicus); + SetRtiTarget(botAI, "triangle", telonicus); + + if (bot->GetTarget() != telonicus->GetGUID()) + return Attack(telonicus); + + if (telonicus->GetVictim() == bot && bot->IsWithinMeleeRange(telonicus)) + { + const Position& position = TELONICUS_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +bool KaelthasSunstriderHandleAdvisorRolesInPhase3Action::Execute(Event /*event*/) +{ + const Position* movePosition = nullptr; + if (botAI->IsAssistHealOfIndex(bot, 0, true)) + { + movePosition = &ADVISOR_HEAL_POSITION; + } + else if (botAI->IsMainTank(bot)) + { + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + if (sanguinar && sanguinar->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + movePosition = &SANGUINAR_WAITING_POSITION; + } + else if (botAI->IsAssistTankOfIndex(bot, 0, true)) + { + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + if (telonicus && telonicus->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + movePosition = &TELONICUS_WAITING_POSITION; + } + else if (GetCapernianTank(bot) == bot) + { + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (capernian && capernian->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + movePosition = &CAPERNIAN_WAITING_POSITION; + } + + if (movePosition && + bot->GetExactDist2d(movePosition->GetPositionX(), movePosition->GetPositionY()) > 2.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, movePosition->GetPositionX(), movePosition->GetPositionY(), + movePosition->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool KaelthasSunstriderReequipGearAction::Execute(Event /*event*/) +{ + return botAI->DoSpecificAction("equip upgrade", Event(), true); +} + +bool KaelthasSunstriderAssignAdvisorDpsPriorityAction::Execute(Event /*event*/) +{ + // Target priority 1: Thaladred, except Capernian tank + Player* capernianTank = GetCapernianTank(bot); + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + + if ((!capernianTank || bot != capernianTank) && + thaladred && !thaladred->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !thaladred->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + { + MarkTargetWithSquare(bot, thaladred); + SetRtiTarget(botAI, "square", thaladred); + + if (bot->GetTarget() != thaladred->GetGUID()) + return Attack(thaladred); + + return false; + } + + // Target priority 2: Capernian for ranged only (excluding longbow tank) + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + + if (botAI->IsRangedDps(bot) && !IsDebuffHunter(bot) && + capernian && !capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + { + SetRtiTarget(botAI, "circle", capernian); + + if (bot->GetTarget() != capernian->GetGUID()) + return Attack(capernian); + + return false; + } + + // Target priority 3: Sanguinar (debuff hunter and melee move here after Thaladred) + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + + if (sanguinar && !sanguinar->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !sanguinar->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + { + SetRtiTarget(botAI, "star", sanguinar); + + if (bot->GetTarget() != sanguinar->GetGUID()) + return Attack(sanguinar); + + return false; + } + + // Target priority 4: Telonicus + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + + if (telonicus && !telonicus->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !telonicus->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + { + SetRtiTarget(botAI, "triangle", telonicus); + if (bot->GetTarget() != telonicus->GetGUID()) + return Attack(telonicus); + + // Melee DPS need to stay at max-ish melee range behind Telonicus to avoid bombs + if (botAI->IsMelee(bot) && botAI->IsDps(bot) && telonicus->GetVictim() != bot) + { + float desiredDist = bot->GetMeleeRange(telonicus); + float behindAngle = Position::NormalizeOrientation(telonicus->GetOrientation() + M_PI); + float targetX = telonicus->GetPositionX() + desiredDist * std::cos(behindAngle); + float targetY = telonicus->GetPositionY() + desiredDist * std::sin(behindAngle); + + if (bot->GetExactDist2d(targetX, targetY) > 0.25f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, targetX, targetY, telonicus->GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + } + } + + return false; +} + +bool KaelthasSunstriderManageAdvisorDpsTimerAction::Execute(Event /*event*/) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + static const std::array advisorNames = + { + "grand astromancer capernian", + "master engineer telonicus", + "lord sanguinar" + }; + + for (const char* name : advisorNames) + { + Unit* advisor = AI_VALUE2(Unit*, "find target", name); + if (!advisor) + continue; + + if (advisor->GetHealth() == advisor->GetMaxHealth() && + !advisor->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE)) + { + const time_t now = std::time(nullptr); + advisorDpsWaitTimer.insert_or_assign(kaelthas->GetMap()->GetInstanceId(), now); + return true; + } + } + + return false; +} + +bool KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction::Execute(Event /*event*/) +{ + if (botAI->IsAssistTank(bot)) + SetRtiTarget(botAI, "moon", nullptr); + + // Priority 0: Everybody other than the main tank needs to stay away from the axe + // But this applies to assist tanks only after they get aggro on the mace, dagger, or sword + Unit* axe = AI_VALUE2(Unit*, "find target", "devastation"); + Unit* mace = AI_VALUE2(Unit*, "find target", "cosmic infuser"); + Unit* dagger = AI_VALUE2(Unit*, "find target", "infinity blades"); + Unit* sword = AI_VALUE2(Unit*, "find target", "warp slicer"); + + if (axe) + { + bool hasAggroFromWeapon = mace && mace->GetVictim() == bot || + dagger && dagger->GetVictim() == bot || + sword && sword->GetVictim() == bot; + if (!botAI->IsTank(bot) || + (botAI->IsAssistTank(bot) && hasAggroFromWeapon)) + { + float currentDistance = bot->GetExactDist2d(axe); + const float safeDistance = botAI->IsAssistTank(bot) ? 17.0f : 13.0f; + if (currentDistance < safeDistance) + return MoveAway(axe, safeDistance - currentDistance); + } + } + + if (botAI->IsDps(bot)) + { + // Priority 1: Staff of Disintegration (Skull) + if (Unit* staff = AI_VALUE2(Unit*, "find target", "staff of disintegration")) + { + MarkTargetWithSkull(bot, staff); + SetRtiTarget(botAI, "skull", staff); + + if (bot->GetTarget() != staff->GetGUID()) + return Attack(staff); + } + // Priority 2: Cosmic Infuser (Skull) + else if (mace) + { + MarkTargetWithSkull(bot, mace); + SetRtiTarget(botAI, "skull", mace); + + if (bot->GetTarget() != mace->GetGUID()) + return Attack(mace); + } + // Priority 3: Warp Slicer (Skull) + else if (sword) + { + MarkTargetWithSkull(bot, sword); + SetRtiTarget(botAI, "skull", sword); + + if (bot->GetTarget() != sword->GetGUID()) + return Attack(sword); + } + // Priority 4: Infinity Blades (Skull) + else if (dagger) + { + MarkTargetWithSkull(bot, dagger); + SetRtiTarget(botAI, "skull", dagger); + + if (bot->GetTarget() != dagger->GetGUID()) + return Attack(dagger); + } + // Priority 5: Devastation - ranged only (Diamond--marked in other method by main tank) + else if (axe && botAI->IsRangedDps(bot)) + { + SetRtiTarget(botAI, "diamond", axe); + + if (bot->GetTarget() != axe->GetGUID()) + return Attack(axe); + } + // Priority 6: Netherstrand Longbow (Skull) + else if (Unit* longbow = AI_VALUE2(Unit*, "find target", "netherstrand longbow")) + { + MarkTargetWithSkull(bot, longbow); + SetRtiTarget(botAI, "skull", longbow); + + if (bot->GetTarget() != longbow->GetGUID()) + return Attack(longbow); + } + // Priority 7: Phaseshift Bulwark (Skull) + else if (Unit* shield = AI_VALUE2(Unit*, "find target", "phaseshift bulwark")) + { + MarkTargetWithSkull(bot, shield); + SetRtiTarget(botAI, "skull", shield); + + if (bot->GetTarget() != shield->GetGUID()) + return Attack(shield); + } + } + + return false; +} + +bool KaelthasSunstriderMoveDevastationAwayAction::Execute(Event /*event*/) +{ + Unit* axe = AI_VALUE2(Unit*, "find target", "devastation"); + if (!axe) + return false; + + MarkTargetWithDiamond(bot, axe); + SetRtiTarget(botAI, "diamond", axe); + + if (bot->GetTarget() != axe->GetGUID()) + return Attack(axe); + + constexpr float safeDistance = 13.0f; + if (axe->GetVictim() == bot && GetNearestNonTankPlayerInRadius(botAI, bot, safeDistance)) + return MoveFromGroup(safeDistance); + + return false; +} + +bool KaelthasSunstriderLootLegendaryWeaponsAction::Execute(Event /*event*/) +{ + struct WeaponInfo + { + uint32 npcEntry; + uint32 itemId; + }; + + static const std::array weapons = + { + WeaponInfo{ NPC_NETHERSTRAND_LONGBOW, ITEM_NETHERSTRAND_LONGBOW }, + WeaponInfo{ NPC_COSMIC_INFUSER, ITEM_COSMIC_INFUSER }, + WeaponInfo{ NPC_DEVASTATION, ITEM_DEVASTATION }, + WeaponInfo{ NPC_INFINITY_BLADES, ITEM_INFINITY_BLADE }, + WeaponInfo{ NPC_WARP_SLICER, ITEM_WARP_SLICER }, + WeaponInfo{ NPC_STAFF_OF_DISINTEGRATION, ITEM_STAFF_OF_DISINTEGRATION }, + WeaponInfo{ NPC_PHASESHIFT_BULWARK, ITEM_PHASESHIFT_BULWARK } + }; + + for (auto const& weapon : weapons) + { + if (ShouldBotLootWeapon(weapon.npcEntry)) + { + if (bot->HasItemCount(weapon.itemId, 1, false)) + { + EquipAction* equipAction = + dynamic_cast(botAI->GetAiObjectContext()->GetAction("equip")); + if (equipAction) + { + ItemIds ids; + ids.insert(weapon.itemId); + equipAction->EquipItems(ids); + } + continue; + } + return LootWeapon(weapon.npcEntry, weapon.itemId); + } + } + + return false; +} + +bool KaelthasSunstriderLootLegendaryWeaponsAction::ShouldBotLootWeapon(uint32 weaponEntry) +{ + uint8 tab = AiFactory::GetPlayerSpecTab(bot); + switch (weaponEntry) + { + case NPC_NETHERSTRAND_LONGBOW: + return bot->getClass() == CLASS_HUNTER; + + case NPC_COSMIC_INFUSER: + return botAI->IsHeal(bot); + + // Fury Warriors could use the axe, but their DPS is terrible at appropriate gear levels + // So IMO they're better off looting only the dagger to MH it and break MCs + case NPC_DEVASTATION: + return (bot->getClass() == CLASS_WARRIOR && tab == WARRIOR_TAB_ARMS) || + (bot->getClass() == CLASS_PALADIN && tab == PALADIN_TAB_RETRIBUTION) || + (botAI->IsDps(bot) && bot->getClass() == CLASS_DEATH_KNIGHT); + + case NPC_INFINITY_BLADES: + return bot->getClass() == CLASS_ROGUE || + bot->getClass() == CLASS_HUNTER || + (bot->getClass() == CLASS_SHAMAN && tab == SHAMAN_TAB_ENHANCEMENT) || + (bot->getClass() == CLASS_WARRIOR && tab != WARRIOR_TAB_ARMS); + + case NPC_WARP_SLICER: + return bot->getClass() == CLASS_ROGUE && tab != ROGUE_TAB_ASSASSINATION || + (botAI->IsTank(bot) && + (bot->getClass() == CLASS_DEATH_KNIGHT || + bot->getClass() == CLASS_PALADIN)); + + case NPC_STAFF_OF_DISINTEGRATION: + return (botAI->IsRangedDps(bot) && bot->getClass() != CLASS_HUNTER) || + (bot->getClass() == CLASS_DRUID && tab == DRUID_TAB_FERAL); + + case NPC_PHASESHIFT_BULWARK: + return botAI->IsTank(bot) && + (bot->getClass() == CLASS_PALADIN || + bot->getClass() == CLASS_WARRIOR || + bot->getClass() == CLASS_DEATH_KNIGHT); + + default: + return false; + } +} + +bool KaelthasSunstriderLootLegendaryWeaponsAction::LootWeapon( + uint32 weaponEntry, uint32 itemId) +{ + constexpr float searchRadius = 150.0f; + Creature* weapon = bot->FindNearestCreature(weaponEntry, searchRadius, false); + + if (!weapon || weapon->IsAlive()) + return false; + + LootObject loot(bot, weapon->GetGUID()); + if (!loot.IsLootPossible(bot)) + return false; + + context->GetValue("loot target")->Set(loot); + + const float maxLootRange = sPlayerbotAIConfig.lootDistance; + constexpr float distFromObject = 2.0f; + + if (bot->GetDistance(weapon) > maxLootRange) + return MoveTo(weapon, distFromObject, MovementPriority::MOVEMENT_COMBAT); + + OpenLootAction open(botAI); + bool opened = open.Execute(Event()); + if (!opened) + return opened; + + if (bot->HasItemCount(itemId, 1, false)) + return false; + + bot->SetLootGUID(weapon->GetGUID()); + + constexpr uint8 weaponIndex = 0; + WorldPacket* packet = new WorldPacket(CMSG_AUTOSTORE_LOOT_ITEM, 1); + *packet << weaponIndex; + bot->GetSession()->QueuePacket(packet); + + return true; +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::Execute(Event /*event*/) +{ + return UsePhaseshiftBulwark() || + UseStaffOfDisintegration() || + UseNetherstrandLongbow(); +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::UsePhaseshiftBulwark() +{ + Item* offHand = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND); + if (!offHand || offHand->GetEntry() != ITEM_PHASESHIFT_BULWARK) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas || !kaelthas->HasAura(SPELL_SHOCK_BARRIER)) + return false; + + if (bot->HasAura(SPELL_ARCANE_BARRIER) || bot->CanUseItem(offHand) != EQUIP_ERR_OK) + return false; + + return UseEquippedItemWithPacket(offHand); +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::UseStaffOfDisintegration() +{ + Item* mainHand = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND); + if (!mainHand || mainHand->GetEntry() != ITEM_STAFF_OF_DISINTEGRATION) + return false; + + if (bot->HasAura(SPELL_MENTAL_PROTECTION_FIELD) || + bot->CanUseItem(mainHand) != EQUIP_ERR_OK) + return false; + + return UseEquippedItemWithPacket(mainHand); +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::UseNetherstrandLongbow() +{ + Item* ranged = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED); + if (!ranged || ranged->GetEntry() != ITEM_NETHERSTRAND_LONGBOW) + return false; + + if (bot->HasItemCount(ITEM_NETHER_SPIKES, 1, false) || + bot->CanUseItem(ranged) != EQUIP_ERR_OK) + return false; + + return UseEquippedItemWithPacket(ranged); +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::UseEquippedItemWithPacket(Item* item) +{ + if (!item || bot->CanUseItem(item) != EQUIP_ERR_OK || bot->IsNonMeleeSpellCast(true)) + return false; + + uint8 bagIndex = item->GetBagSlot(); + uint8 slot = item->GetSlot(); + uint8 cast_count = 1; + ObjectGuid item_guid = item->GetGUID(); + uint32 glyphIndex = 0; + uint8 castFlags = 0; + uint32 spellId = 0; + + for (uint8 i = 0; i < MAX_ITEM_PROTO_SPELLS; ++i) + { + if (item->GetTemplate()->Spells[i].SpellId > 0 && + item->GetTemplate()->Spells[i].SpellTrigger == ITEM_SPELLTRIGGER_ON_USE) + { + spellId = item->GetTemplate()->Spells[i].SpellId; + break; + } + } + + if (!spellId) + return false; + + WorldPacket packet(CMSG_USE_ITEM); + packet << bagIndex << slot << cast_count << spellId << item_guid << glyphIndex << castFlags; + + uint32 targetFlag = TARGET_FLAG_UNIT; + packet << targetFlag << bot->GetPackGUID(); + + bot->GetSession()->HandleUseItemOpcode(packet); + return true; +} + +bool KaelthasSunstriderMainTankPositionBossAction::Execute(Event /*event*/) +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + MarkTargetWithStar(bot, kaelthas); + SetRtiTarget(botAI, "star", kaelthas); + + if (bot->GetTarget() != kaelthas->GetGUID()) + return Attack(kaelthas); + + if (kaelthas->GetVictim() == bot && bot->IsWithinMeleeRange(kaelthas)) + { + const Position& position = KAELTHAS_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 4.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +bool KaelthasSunstriderAvoidFlameStrikeAction::Execute(Event /*event*/) +{ + constexpr float searchRadius = 40.0f; + std::vector flameStrikes = + GetAllHazardTriggers(bot, NPC_FLAME_STRIKE_TRIGGER, searchRadius); + + if (flameStrikes.empty()) + return false; + + constexpr float hazardRadius = 12.0f; + bool inDanger = false; + for (Unit* flameStrike : flameStrikes) + { + if (bot->GetExactDist2d(flameStrike) < hazardRadius) + { + inDanger = true; + break; + } + } + + if (!inDanger) + return false; + + Position safestPos = FindSafestNearbyPosition(bot, flameStrikes, hazardRadius); + + botAI->Reset(); + return MoveTo(TEMPEST_KEEP_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), + safestPos.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); +} + +bool KaelthasSunstriderHandlePhoenixesAndEggsAction::Execute(Event /*event*/) +{ + if (botAI->IsAssistTankOfIndex(bot, 0, true) || botAI->IsAssistTankOfIndex(bot, 1, true)) + return AssistTanksPickUpPhoenixes(); + else + return NonTanksDestroyEggsAndAvoidPhoenixes(); +} + +bool KaelthasSunstriderHandlePhoenixesAndEggsAction::AssistTanksPickUpPhoenixes() +{ + std::vector phoenixes; + auto const& npcs = botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get(); + for (auto const& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->GetEntry() == NPC_PHOENIX && unit->IsAlive()) + phoenixes.push_back(unit); + } + + if (phoenixes.empty()) + return false; + + std::sort(phoenixes.begin(), phoenixes.end(), + [](Unit* first, Unit* second) { return first->GetGUID() < second->GetGUID(); }); + + Unit* targetPhoenix = nullptr; + if (botAI->IsAssistTankOfIndex(bot, 0, true)) + { + targetPhoenix = phoenixes[0]; + MarkTargetWithSquare(bot, targetPhoenix); + SetRtiTarget(botAI, "square", targetPhoenix); + } + else if (botAI->IsAssistTankOfIndex(bot, 1, true) && phoenixes.size() >= 2) + { + targetPhoenix = phoenixes[1]; + MarkTargetWithCircle(bot, targetPhoenix); + SetRtiTarget(botAI, "circle", targetPhoenix); + } + + if (!targetPhoenix) + return false; + + if (bot->GetTarget() != targetPhoenix->GetGUID()) + return Attack(targetPhoenix); + + constexpr float safeDistance = 12.0f; + if (targetPhoenix->GetVictim() == bot && + GetNearestNonTankPlayerInRadius(botAI, bot, safeDistance)) + return MoveFromGroup(safeDistance); + + return false; +} + +bool KaelthasSunstriderHandlePhoenixesAndEggsAction::NonTanksDestroyEggsAndAvoidPhoenixes() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (botAI->IsDps(bot) && !kaelthas->HasAura(SPELL_SHOCK_BARRIER)) + { + if (Unit* phoenixEgg = GetFirstAliveUnitByEntry(botAI, NPC_PHOENIX_EGG)) + { + MarkTargetWithDiamond(bot, phoenixEgg); + SetRtiTarget(botAI, "diamond", phoenixEgg); + + if (bot->GetTarget() != phoenixEgg->GetGUID()) + return Attack(phoenixEgg); + } + } + else if (botAI->IsDps(bot)) + return false; + + if (Unit* phoenix = AI_VALUE2(Unit*, "find target", "phoenix")) + { + float currentDistance = bot->GetExactDist2d(phoenix); + constexpr float safeDistance = 12.0f; + if (currentDistance < safeDistance) + return MoveAway(phoenix, safeDistance - currentDistance); + } + + return false; +} + +bool KaelthasSunstriderBreakMindControlAction::Execute(Event /*event*/) +{ + Player* mcTarget = nullptr; + float closestDist = std::numeric_limits::max(); + + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot) + continue; + + if (member->HasAura(SPELL_KAELTHAS_MIND_CONTROL)) + { + float dist = bot->GetExactDist2d(member); + if (dist < closestDist) + { + closestDist = dist; + mcTarget = member; + } + } + } + + if (!mcTarget) + return false; + + if (!bot->IsWithinMeleeRange(mcTarget)) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, mcTarget->GetPositionX(), mcTarget->GetPositionY(), + mcTarget->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + if (bot->getClass() == CLASS_ROGUE && + AiFactory::GetPlayerSpecTab(bot) != ROGUE_TAB_COMBAT && + botAI->CanCastSpell("sinister strike", mcTarget)) + { + return botAI->CastSpell("sinister strike", mcTarget); + } + else + { + static const std::array spells = + { + "hamstring", + "wing clip", + "shiv", + "stormstrike" + }; + for (const char* spell : spells) + { + if (botAI->CanCastSpell(spell, mcTarget)) + return botAI->CastSpell(spell, mcTarget); + } + } + + return false; +} + +// Shock Barrier needs to be #1 focus, even if there is a Phoenix Egg up +bool KaelthasSunstriderBreakThroughShockBarrierAction::Execute(Event /*event*/) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (!kaelthas->HasAura(SPELL_SHOCK_BARRIER)) + { + static const std::array spells = + { + "bash", + "counterspell", + "kick", + "mind freeze", + "pummel", + "shield bash", + "silencing shot", + "wind shear", + }; + for (const char* spell : spells) + { + if (botAI->CanCastSpell(spell, kaelthas)) + return botAI->CastSpell(spell, kaelthas); + } + } + else if (bot->GetTarget() != kaelthas->GetGUID()) + { + SetRtiTarget(botAI, "star", kaelthas); + return Attack(kaelthas); + } + + return false; +} + +bool KaelthasSunstriderSpreadOutInMidairAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + constexpr float minSpreadDistance = 16.0f; + + std::vector nearbyPlayers; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot || !member->IsAlive()) + continue; + + if (bot->IsWithinDist3d(member, minSpreadDistance * 1.0f)) + nearbyPlayers.push_back(member); + } + + if (nearbyPlayers.empty()) + return false; + + Player* closestPlayer = nullptr; + float closestDist = std::numeric_limits::max(); + for (Player* player : nearbyPlayers) + { + float distToPlayer = bot->GetExactDist(player); + if (distToPlayer < closestDist) + { + closestDist = distToPlayer; + closestPlayer = player; + } + } + + if (closestPlayer && closestDist < minSpreadDistance) + { + float angle = bot->GetAngle(closestPlayer) + M_PI; + float distance = minSpreadDistance - closestDist; + + float x = bot->GetPositionX() + std::cos(angle) * distance; + float y = bot->GetPositionY() + std::sin(angle) * distance; + + return MoveTo(TEMPEST_KEEP_MAP_ID, x, y, bot->GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} diff --git a/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h new file mode 100644 index 00000000..8b021304 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h @@ -0,0 +1,413 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPACTIONS_H +#define _PLAYERBOT_RAIDTEMPESTKEEPACTIONS_H + +#include "RaidTempestKeepHelpers.h" +#include "RaidTempestKeepKaelthasBossAI.h" +#include "Action.h" +#include "AttackAction.h" +#include "MovementActions.h" + +using namespace TempestKeepHelpers; + +// Trash + +class CrimsonHandCenturionCastPolymorphAction : public Action +{ +public: + CrimsonHandCenturionCastPolymorphAction( + PlayerbotAI* botAI, std::string const name = "crimson hand centurion cast polymorph") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Al'ar + +class AlarMisdirectBossToMainTankAction : public AttackAction +{ +public: + AlarMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "al'ar misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarBossTanksMoveBetweenPlatformsAction : public AttackAction +{ +public: + AlarBossTanksMoveBetweenPlatformsAction( + PlayerbotAI* botAI, std::string const name = "al'ar boss tanks move between platforms") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool PositionMainTank(Unit* alar, int8 locationIndex); + bool PositionAssistTank(Unit* alar, int8 locationIndex); +}; + +class AlarMeleeDpsMoveBetweenPlatformsAction : public AttackAction +{ +public: + AlarMeleeDpsMoveBetweenPlatformsAction( + PlayerbotAI* botAI, std::string const name = "al'ar melee dps move between platforms") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarRangedAndEmberTankMoveUnderPlatformsAction : public AttackAction +{ +public: + AlarRangedAndEmberTankMoveUnderPlatformsAction( + PlayerbotAI* botAI, std::string const name = "al'ar ranged and ember tank move under platforms") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarAssistTanksPickUpEmbersAction : public AttackAction +{ +public: + AlarAssistTanksPickUpEmbersAction( + PlayerbotAI* botAI, std::string const name = "al'ar assist tanks pick up embers") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool HandlePhase1Embers(Unit* alar); + bool HandlePhase2Embers(Unit* alar); +}; + +class AlarRangedDpsPrioritizeEmbersAction : public AttackAction +{ +public: + AlarRangedDpsPrioritizeEmbersAction( + PlayerbotAI* botAI, std::string const name = "al'ar ranged dps prioritize embers") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarJumpFromPlatformAction : public MovementAction +{ +public: + AlarJumpFromPlatformAction( + PlayerbotAI* botAI, std::string const name = "al'ar jump from platform") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarMoveAwayFromRebirthAction : public MovementAction +{ +public: + AlarMoveAwayFromRebirthAction( + PlayerbotAI* botAI, std::string const name = "al'ar move away from rebirth") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarSwapTanksOnBossAction : public AttackAction +{ +public: + AlarSwapTanksOnBossAction( + PlayerbotAI* botAI, std::string const name = "al'ar swap tanks on boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarAvoidFlamePatchesAndDiveBombsAction : public MovementAction +{ +public: + AlarAvoidFlamePatchesAndDiveBombsAction( + PlayerbotAI* botAI, std::string const name = "al'ar avoid flame patches and dive bombs") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool AvoidFlamePatch(); + bool HandleDiveBomb(Unit* alar); +}; + +class AlarReturnToRoomCenterAction : public MovementAction +{ +public: + AlarReturnToRoomCenterAction( + PlayerbotAI* botAI, std::string const name = "al'ar return to room center") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarManagePhaseTrackerAction : public Action +{ +public: + AlarManagePhaseTrackerAction( + PlayerbotAI* botAI, std::string const name = "al'ar manage phase tracker") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Void Reaver + +class VoidReaverTanksPositionBossAction : public AttackAction +{ +public: + VoidReaverTanksPositionBossAction( + PlayerbotAI* botAI, std::string const name = "void reaver tanks position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class VoidReaverUseAggroDumpAbilityAction : public Action +{ +public: + VoidReaverUseAggroDumpAbilityAction( + PlayerbotAI* botAI, std::string const name = "void reaver use aggro dump ability") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class VoidReaverSpreadRangedAction : public MovementAction +{ +public: + VoidReaverSpreadRangedAction( + PlayerbotAI* botAI, std::string const name = "void reaver spread ranged") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + int GetHealerIndex(Group* group, int& healerCount); + int GetRangedDpsIndex(Group* group, int& rangedDpsCount); +}; + +class VoidReaverAvoidArcaneOrbAction : public MovementAction +{ +public: + VoidReaverAvoidArcaneOrbAction( + PlayerbotAI* botAI, std::string const name = "void reaver avoid arcane orb") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class VoidReaverEraseTrackersAction : public Action +{ +public: + VoidReaverEraseTrackersAction( + PlayerbotAI* botAI, std::string const name = "void reaver erase trackers") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// High Astromancer Solarian + +class HighAstromancerSolarianRangedLeaveSpaceForMeleeAction : public MovementAction +{ +public: + HighAstromancerSolarianRangedLeaveSpaceForMeleeAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian ranged leave space for melee") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HighAstromancerSolarianMoveAwayFromGroupAction : public MovementAction +{ +public: + HighAstromancerSolarianMoveAwayFromGroupAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian move away from group") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HighAstromancerSolarianStackForAoeAction : public MovementAction +{ +public: + HighAstromancerSolarianStackForAoeAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian stack for aoe") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HighAstromancerSolarianTargetSolariumPriestsAction : public AttackAction +{ +public: + HighAstromancerSolarianTargetSolariumPriestsAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian target solarium priests") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + std::pair GetSolariumPriests(PlayerbotAI* botAI); + std::vector GetMeleeBots(Group* group); + Unit* AssignSolariumPriestsToBots(const std::pair& priestsPair, const std::vector& meleeMembers); +}; + +class HighAstromancerSolarianCastFearWardOnMainTankAction : public Action +{ +public: + HighAstromancerSolarianCastFearWardOnMainTankAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian cast fear ward on main tank") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Kael'thas Sunstrider + +class KaelthasSunstriderKiteThaladredAction : public MovementAction +{ +public: + KaelthasSunstriderKiteThaladredAction( + PlayerbotAI* botAI) : MovementAction(botAI, "kael'thas sunstrider kite thaladred") {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderMisdirectAdvisorsToTanksAction : public AttackAction +{ +public: + KaelthasSunstriderMisdirectAdvisorsToTanksAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider misdirect advisors to tanks") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderMainTankPositionSanguinarAction : public AttackAction +{ +public: + KaelthasSunstriderMainTankPositionSanguinarAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider main tank position sanguinar") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderCastFearWardOnSanguinarTankAction : public Action +{ +public: + KaelthasSunstriderCastFearWardOnSanguinarTankAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider cast fear ward on sanguinar tank") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderWarlockTankPositionCapernianAction : public AttackAction +{ +public: + KaelthasSunstriderWarlockTankPositionCapernianAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider warlock tank position capernian") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction : public MovementAction +{ +public: + KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider spread and move away from capernian") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool RangedBotsDisperse(boss_kaelthas* kaelAI, Unit* capernian); + bool MeleeStayBackFromCapernian(Unit* capernian); +}; + +class KaelthasSunstriderFirstAssistTankPositionTelonicusAction : public AttackAction +{ +public: + KaelthasSunstriderFirstAssistTankPositionTelonicusAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider first assist tank position telonicus") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderHandleAdvisorRolesInPhase3Action : public MovementAction +{ +public: + KaelthasSunstriderHandleAdvisorRolesInPhase3Action( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider handle advisor roles in phase 3") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderAssignAdvisorDpsPriorityAction : public AttackAction +{ +public: + KaelthasSunstriderAssignAdvisorDpsPriorityAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider assign advisor dps priority") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderManageAdvisorDpsTimerAction : public Action +{ +public: + KaelthasSunstriderManageAdvisorDpsTimerAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider manage advisor dps timer") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction : public AttackAction +{ +public: + KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider assign legendary weapon dps priority") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderMoveDevastationAwayAction : public AttackAction +{ +public: + KaelthasSunstriderMoveDevastationAwayAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider move devastation away") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderLootLegendaryWeaponsAction : public MovementAction +{ +public: + KaelthasSunstriderLootLegendaryWeaponsAction( + PlayerbotAI* botAI) : MovementAction(botAI, "kael'thas sunstrider loot legendary weapons") {} + bool Execute(Event event) override; + +private: + bool ShouldBotLootWeapon(uint32 weaponEntry); + bool LootWeapon(uint32 weaponEntry, uint32 itemId); +}; + +class KaelthasSunstriderUseLegendaryWeaponsAction : public Action +{ +public: + KaelthasSunstriderUseLegendaryWeaponsAction( + PlayerbotAI* botAI) : Action(botAI, "kael'thas sunstrider use legendary weapons") {} + bool Execute(Event event) override; + +private: + bool UsePhaseshiftBulwark(); + bool UseStaffOfDisintegration(); + bool UseNetherstrandLongbow(); + bool UseEquippedItemWithPacket(Item* item); +}; + +class KaelthasSunstriderReequipGearAction : public Action +{ +public: + KaelthasSunstriderReequipGearAction( + PlayerbotAI* botAI) : Action(botAI, "kael'thas sunstrider reequip gear") {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderMainTankPositionBossAction : public AttackAction +{ +public: + KaelthasSunstriderMainTankPositionBossAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider main tank position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderAvoidFlameStrikeAction : public MovementAction +{ +public: + KaelthasSunstriderAvoidFlameStrikeAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider avoid flame strike") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderHandlePhoenixesAndEggsAction : public AttackAction +{ +public: + KaelthasSunstriderHandlePhoenixesAndEggsAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider handle phoenixes and eggs") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool AssistTanksPickUpPhoenixes(); + bool NonTanksDestroyEggsAndAvoidPhoenixes(); +}; + +class KaelthasSunstriderBreakMindControlAction : public AttackAction +{ +public: + KaelthasSunstriderBreakMindControlAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider break mind control") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderBreakThroughShockBarrierAction : public AttackAction +{ +public: + KaelthasSunstriderBreakThroughShockBarrierAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider break through shock barrier") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderSpreadOutInMidairAction : public MovementAction +{ +public: + KaelthasSunstriderSpreadOutInMidairAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider spread out in midair") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +#endif diff --git a/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp new file mode 100644 index 00000000..201bbc76 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp @@ -0,0 +1,422 @@ +#include "RaidTempestKeepMultipliers.h" +#include "RaidTempestKeepActions.h" +#include "RaidTempestKeepHelpers.h" +#include "RaidTempestKeepKaelthasBossAI.h" +#include "ChooseTargetActions.h" +#include "DKActions.h" +#include "DruidActions.h" +#include "DruidBearActions.h" +#include "EquipAction.h" +#include "FollowActions.h" +#include "HunterActions.h" +#include "MageActions.h" +#include "PaladinActions.h" +#include "Playerbots.h" +#include "RogueActions.h" +#include "ShamanActions.h" +#include "WarlockActions.h" +#include "WarriorActions.h" + +// Al'ar + +float AlarMoveBetweenPlatformsMultiplier::GetValue(Action* action) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return 1.0f; + + if (isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (botAI->IsDps(bot) && + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float AlarDisableDisperseMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "al'ar")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float AlarDisableTankAssistMultiplier::GetValue(Action* action) +{ + if (bot->GetVictim() == nullptr) + return 1.0f; + + if (!botAI->IsTank(bot)) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "al'ar")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float AlarStayAwayFromRebirthMultiplier::GetValue(Action* action) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return 1.0f; + + Creature* alarCreature = alar->ToCreature(); + if (!alarCreature || alarCreature->GetReactState() != REACT_PASSIVE) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float AlarPhase2NoTankingIfArmorMeltedMultiplier::GetValue(Action* action) +{ + if (!bot->HasAura(SPELL_MELT_ARMOR)) + return 1.0f; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar || bot->GetTarget() != alar->GetGUID()) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Void Reaver + +float VoidReaverMaintainPositionsMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "void reaver")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// High Astromancer Solarian + +float HighAstromancerSolarianMaintainPositionMultiplier::GetValue(Action* action) +{ + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + if (!astromancer || astromancer->HasAura(SPELL_SOLARIAN_TRANSFORM)) + return 1.0f; + + if (botAI->IsRanged(bot) && + (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action))) + return 0.0f; + + if (!bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER)) + return 1.0f; + + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + + return 1.0f; +} + +float HighAstromancerSolarianDisableTankAssistMultiplier::GetValue(Action* action) +{ + if (bot->GetVictim() == nullptr) + return 1.0f; + + if (!botAI->IsTank(bot)) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "solarium priest")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Kael'thas Sunstrider + +float KaelthasSunstriderWaitForDpsMultiplier::GetValue(Action* action) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || kaelAI->GetPhase() != PHASE_SINGLE_ADVISOR) + return 1.0f; + + if (dynamic_cast(action)) + return 1.0f; + + const time_t now = std::time(nullptr); + constexpr uint8 dpsWaitSeconds = 10; + + auto it = advisorDpsWaitTimer.find(kaelthas->GetMap()->GetInstanceId()); + if (it == advisorDpsWaitTimer.end() || (now - it->second) < dpsWaitSeconds) + { + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + + auto isAdvisorActive = [](Unit* advisor) + { + return advisor && !advisor->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !advisor->HasAura(SPELL_PERMANENT_FEIGN_DEATH); + }; + + if ((isAdvisorActive(sanguinar) && botAI->IsMainTank(bot)) || + (isAdvisorActive(telonicus) && botAI->IsAssistTankOfIndex(bot, 0, true)) || + (isAdvisorActive(capernian) && (botAI->IsMainTank(bot) || GetCapernianTank(bot) == bot))) + return 1.0f; + + bool shouldHoldDps = + (isAdvisorActive(sanguinar) && !botAI->IsMainTank(bot)) || + (isAdvisorActive(telonicus) && !botAI->IsAssistTankOfIndex(bot, 0, true)) || + (isAdvisorActive(capernian) && !botAI->IsMainTank(bot) && GetCapernianTank(bot) != bot); + + if (shouldHoldDps && + (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action)))) + return 0.0f; + } + + return 1.0f; +} + +float KaelthasSunstriderKiteThaladredMultiplier::GetValue(Action* action) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return 1.0f; + + if (botAI->IsTank(bot) && kaelAI->GetPhase() == PHASE_ALL_ADVISORS) + return 1.0f; + + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + if (!thaladred || thaladred->GetVictim() != bot || + thaladred->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float KaelthasSunstriderControlMisdirectionMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_HUNTER) + return 1.0f; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || kaelAI->GetPhase() == PHASE_FINAL) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float KaelthasSunstriderKeepDistanceFromCapernianMultiplier::GetValue(Action* action) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || kaelAI->GetPhase() != PHASE_SINGLE_ADVISOR) + return 1.0f; + + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (!capernian || capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || + capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float KaelthasSunstriderManageWeaponTankingMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return 1.0f; + + if (kaelAI->GetPhase() != PHASE_WEAPONS && + dynamic_cast(action)) + return 0.0f; + + if (!botAI->IsMainTank(bot)) + return 1.0f; + + // Try to keep main tank from grabbing aggro on any weapon other than the axe + if (kaelAI->GetPhase() == PHASE_WEAPONS && + (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))) + return 0.0f; + + return 1.0f; +} + +float KaelthasSunstriderDisableAdvisorTankAssistMultiplier::GetValue(Action* action) +{ + if (bot->GetVictim() == nullptr || !botAI->IsTank(bot)) + return 1.0f; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return 1.0f; + + if (kaelAI->GetPhase() != PHASE_SINGLE_ADVISOR && + kaelAI->GetPhase() != PHASE_ALL_ADVISORS) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float KaelthasSunstriderDisableDisperseMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Bloodlust/Heroism and other major cooldowns should be used at the start of Phase 3 +float KaelthasSunstriderDelayCooldownsMultiplier::GetValue(Action* action) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || kaelAI->GetPhase() == PHASE_ALL_ADVISORS || + kaelAI->GetPhase() == PHASE_FINAL) + return 1.0f; + + if (bot->getClass() == CLASS_SHAMAN && + (dynamic_cast(action) || + dynamic_cast(action))) + return 0.0f; + + if (botAI->IsDps(bot) && + (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))) + return 0.0f; + + return 1.0f; +} + +float KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier::GetValue(Action* action) +{ + if (!bot->HasAura(SPELL_GRAVITY_LAPSE)) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} diff --git a/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h new file mode 100644 index 00000000..d43d4a63 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h @@ -0,0 +1,150 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPMULTIPLIERS_H +#define _PLAYERBOT_RAIDTEMPESTKEEPMULTIPLIERS_H + +#include "Multiplier.h" + +// Al'ar + +class AlarMoveBetweenPlatformsMultiplier : public Multiplier +{ +public: + AlarMoveBetweenPlatformsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar move between platforms multiplier") {} + virtual float GetValue(Action* action); +}; + +class AlarDisableDisperseMultiplier : public Multiplier +{ +public: + AlarDisableDisperseMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar disable disperse multiplier") {} + virtual float GetValue(Action* action); +}; + +class AlarDisableTankAssistMultiplier : public Multiplier +{ +public: + AlarDisableTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar disable tank assist multiplier") {} + virtual float GetValue(Action* action); +}; + +class AlarStayAwayFromRebirthMultiplier : public Multiplier +{ +public: + AlarStayAwayFromRebirthMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar stay away from rebirth multiplier") {} + virtual float GetValue(Action* action); +}; + +class AlarPhase2NoTankingIfArmorMeltedMultiplier : public Multiplier +{ +public: + AlarPhase2NoTankingIfArmorMeltedMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar phase 2 no tanking if armor melted multiplier") {} + virtual float GetValue(Action* action); +}; + +// Void Reaver + +class VoidReaverMaintainPositionsMultiplier : public Multiplier +{ +public: + VoidReaverMaintainPositionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "void reaver maintain positions multiplier") {} + virtual float GetValue(Action* action); +}; + +// High Astromancer Solarian + +class HighAstromancerSolarianDisableTankAssistMultiplier : public Multiplier +{ +public: + HighAstromancerSolarianDisableTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "high astromancer solarian disable tank assist multiplier") {} + virtual float GetValue(Action* action); +}; + +class HighAstromancerSolarianMaintainPositionMultiplier : public Multiplier +{ +public: + HighAstromancerSolarianMaintainPositionMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "high astromancer solarian maintain position multiplier") {} + virtual float GetValue(Action* action); +}; + +// Kael'thas Sunstrider + +class KaelthasSunstriderWaitForDpsMultiplier : public Multiplier +{ +public: + KaelthasSunstriderWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider wait for dps multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderKiteThaladredMultiplier : public Multiplier +{ +public: + KaelthasSunstriderKiteThaladredMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider kite thaladred multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderControlMisdirectionMultiplier : public Multiplier +{ +public: + KaelthasSunstriderControlMisdirectionMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider control misdirection multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderKeepDistanceFromCapernianMultiplier : public Multiplier +{ +public: + KaelthasSunstriderKeepDistanceFromCapernianMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider keep distance from capernian multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderManageWeaponTankingMultiplier : public Multiplier +{ +public: + KaelthasSunstriderManageWeaponTankingMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider manage weapon tanking multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderDisableAdvisorTankAssistMultiplier : public Multiplier +{ +public: + KaelthasSunstriderDisableAdvisorTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider disable advisor tank assist multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderDisableDisperseMultiplier : public Multiplier +{ +public: + KaelthasSunstriderDisableDisperseMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider disable disperse multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderDelayCooldownsMultiplier : public Multiplier +{ +public: + KaelthasSunstriderDelayCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider delay cooldowns multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier : public Multiplier +{ +public: + KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider stay spread during gravity lapse multiplier") {} + virtual float GetValue(Action* action); +}; + +#endif diff --git a/src/Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h b/src/Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h new file mode 100644 index 00000000..50b2de1f --- /dev/null +++ b/src/Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h @@ -0,0 +1,289 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPACTIONCONTEXT_H +#define _PLAYERBOT_RAIDTEMPESTKEEPACTIONCONTEXT_H + +#include "RaidTempestKeepActions.h" +#include "NamedObjectContext.h" + +class RaidTempestKeepActionContext : public NamedObjectContext +{ +public: + RaidTempestKeepActionContext() + { + // Trash + creators["crimson hand centurion cast polymorph"] = + &RaidTempestKeepActionContext::crimson_hand_centurion_cast_polymorph; + + // Al'ar + creators["al'ar misdirect boss to main tank"] = + &RaidTempestKeepActionContext::alar_misdirect_boss_to_main_tank; + + creators["al'ar boss tanks move between platforms"] = + &RaidTempestKeepActionContext::alar_boss_tanks_move_between_platforms; + + creators["al'ar melee dps move between platforms"] = + &RaidTempestKeepActionContext::alar_melee_dps_move_between_platforms; + + creators["al'ar ranged and ember tank move under platforms"] = + &RaidTempestKeepActionContext::alar_ranged_and_ember_tank_move_under_platforms; + + creators["al'ar assist tanks pick up embers"] = + &RaidTempestKeepActionContext::alar_assist_tanks_pick_up_embers; + + creators["al'ar ranged dps prioritize embers"] = + &RaidTempestKeepActionContext::alar_ranged_dps_prioritize_embers; + + creators["al'ar jump from platform"] = + &RaidTempestKeepActionContext::alar_jump_from_platform; + + creators["al'ar move away from rebirth"] = + &RaidTempestKeepActionContext::alar_move_away_from_rebirth; + + creators["al'ar swap tanks on boss"] = + &RaidTempestKeepActionContext::alar_swap_tanks_on_boss; + + creators["al'ar avoid flame patches and dive bombs"] = + &RaidTempestKeepActionContext::alar_avoid_flame_patches_and_dive_bombs; + + creators["al'ar return to room center"] = + &RaidTempestKeepActionContext::alar_return_to_room_center; + + creators["al'ar manage phase tracker"] = + &RaidTempestKeepActionContext::alar_manage_phase_tracker; + + // Void Reaver + creators["void reaver tanks position boss"] = + &RaidTempestKeepActionContext::void_reaver_tanks_position_boss; + + creators["void reaver use aggro dump ability"] = + &RaidTempestKeepActionContext::void_reaver_use_aggro_dump_ability; + + creators["void reaver spread ranged"] = + &RaidTempestKeepActionContext::void_reaver_spread_ranged; + + creators["void reaver avoid arcane orb"] = + &RaidTempestKeepActionContext::void_reaver_avoid_arcane_orb; + + creators["void reaver erase trackers"] = + &RaidTempestKeepActionContext::void_reaver_erase_trackers; + + // High Astromancer Solarian + creators["high astromancer solarian ranged leave space for melee"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_ranged_leave_space_for_melee; + + creators["high astromancer solarian move away from group"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_move_away_from_group; + + creators["high astromancer solarian stack for aoe"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_stack_for_aoe; + + creators["high astromancer solarian target solarium priests"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_target_solarium_priests; + + creators["high astromancer solarian cast fear ward on main tank"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_cast_fear_ward_on_main_tank; + + // Kael'thas Sunstrider + creators["kael'thas sunstrider kite thaladred"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_kite_thaladred; + + creators["kael'thas sunstrider misdirect advisors to tanks"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_misdirect_advisors_to_tanks; + + creators["kael'thas sunstrider main tank position sanguinar"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_main_tank_position_sanguinar; + + creators["kael'thas sunstrider cast fear ward on sanguinar tank"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_cast_fear_ward_on_sanguinar_tank; + + creators["kael'thas sunstrider warlock tank position capernian"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_warlock_tank_position_capernian; + + creators["kael'thas sunstrider spread and move away from capernian"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_spread_and_move_away_from_capernian; + + creators["kael'thas sunstrider first assist tank position telonicus"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_first_assist_tank_position_telonicus; + + creators["kael'thas sunstrider handle advisor roles in phase 3"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_handle_advisor_roles_in_phase_3; + + creators["kael'thas sunstrider assign advisor dps priority"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_assign_advisor_dps_priority; + + creators["kael'thas sunstrider manage advisor dps timer"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_manage_advisor_dps_timer; + + creators["kael'thas sunstrider assign legendary weapon dps priority"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_assign_legendary_weapon_dps_priority; + + creators["kael'thas sunstrider main tank move devastation away"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_main_tank_move_devastation_away; + + creators["kael'thas sunstrider loot legendary weapons"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_loot_legendary_weapons; + + creators["kael'thas sunstrider use legendary weapons"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_use_legendary_weapons; + + creators["kael'thas sunstrider reequip gear"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_reequip_gear; + + creators["kael'thas sunstrider main tank position boss"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_main_tank_position_boss; + + creators["kael'thas sunstrider avoid flame strike"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_avoid_flame_strike; + + creators["kael'thas sunstrider handle phoenixes and eggs"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_handle_phoenixes_and_eggs; + + creators["kael'thas sunstrider break mind control"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_break_mind_control; + + creators["kael'thas sunstrider break through shock barrier"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_break_through_shock_barrier; + + creators["kael'thas sunstrider spread out in midair"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_spread_out_in_midair; + } + +private: + // Trash + static Action* crimson_hand_centurion_cast_polymorph( + PlayerbotAI* botAI) { return new CrimsonHandCenturionCastPolymorphAction(botAI); } + + // Al'ar + static Action* alar_misdirect_boss_to_main_tank( + PlayerbotAI* botAI) { return new AlarMisdirectBossToMainTankAction(botAI); } + + static Action* alar_boss_tanks_move_between_platforms( + PlayerbotAI* botAI) { return new AlarBossTanksMoveBetweenPlatformsAction(botAI); } + + static Action* alar_melee_dps_move_between_platforms( + PlayerbotAI* botAI) { return new AlarMeleeDpsMoveBetweenPlatformsAction(botAI); } + + static Action* alar_ranged_and_ember_tank_move_under_platforms( + PlayerbotAI* botAI) { return new AlarRangedAndEmberTankMoveUnderPlatformsAction(botAI); } + + static Action* alar_assist_tanks_pick_up_embers( + PlayerbotAI* botAI) { return new AlarAssistTanksPickUpEmbersAction(botAI); } + + static Action* alar_ranged_dps_prioritize_embers( + PlayerbotAI* botAI) { return new AlarRangedDpsPrioritizeEmbersAction(botAI); } + + static Action* alar_jump_from_platform( + PlayerbotAI* botAI) { return new AlarJumpFromPlatformAction(botAI); } + + static Action* alar_move_away_from_rebirth( + PlayerbotAI* botAI) { return new AlarMoveAwayFromRebirthAction(botAI); } + + static Action* alar_swap_tanks_on_boss( + PlayerbotAI* botAI) { return new AlarSwapTanksOnBossAction(botAI); } + + static Action* alar_avoid_flame_patches_and_dive_bombs( + PlayerbotAI* botAI) { return new AlarAvoidFlamePatchesAndDiveBombsAction(botAI); } + + static Action* alar_return_to_room_center( + PlayerbotAI* botAI) { return new AlarReturnToRoomCenterAction(botAI); } + + static Action* alar_manage_phase_tracker( + PlayerbotAI* botAI) { return new AlarManagePhaseTrackerAction(botAI); } + + // Void Reaver + static Action* void_reaver_tanks_position_boss( + PlayerbotAI* botAI) { return new VoidReaverTanksPositionBossAction(botAI); } + + static Action* void_reaver_use_aggro_dump_ability( + PlayerbotAI* botAI) { return new VoidReaverUseAggroDumpAbilityAction(botAI); } + + static Action* void_reaver_spread_ranged( + PlayerbotAI* botAI) { return new VoidReaverSpreadRangedAction(botAI); } + + static Action* void_reaver_avoid_arcane_orb( + PlayerbotAI* botAI) { return new VoidReaverAvoidArcaneOrbAction(botAI); } + + static Action* void_reaver_erase_trackers( + PlayerbotAI* botAI) { return new VoidReaverEraseTrackersAction(botAI); } + + // High Astromancer Solarian + static Action* high_astromancer_solarian_ranged_leave_space_for_melee( + PlayerbotAI* botAI) { return new HighAstromancerSolarianRangedLeaveSpaceForMeleeAction(botAI); } + + static Action* high_astromancer_solarian_move_away_from_group( + PlayerbotAI* botAI) { return new HighAstromancerSolarianMoveAwayFromGroupAction(botAI); } + + static Action* high_astromancer_solarian_stack_for_aoe( + PlayerbotAI* botAI) { return new HighAstromancerSolarianStackForAoeAction(botAI); } + + static Action* high_astromancer_solarian_target_solarium_priests( + PlayerbotAI* botAI) { return new HighAstromancerSolarianTargetSolariumPriestsAction(botAI); } + + static Action* high_astromancer_solarian_cast_fear_ward_on_main_tank( + PlayerbotAI* botAI) { return new HighAstromancerSolarianCastFearWardOnMainTankAction(botAI); } + + // Kael'thas Sunstrider + static Action* kaelthas_sunstrider_kite_thaladred( + PlayerbotAI* botAI) { return new KaelthasSunstriderKiteThaladredAction(botAI); } + + static Action* kaelthas_sunstrider_misdirect_advisors_to_tanks( + PlayerbotAI* botAI) { return new KaelthasSunstriderMisdirectAdvisorsToTanksAction(botAI); } + + static Action* kaelthas_sunstrider_main_tank_position_sanguinar( + PlayerbotAI* botAI) { return new KaelthasSunstriderMainTankPositionSanguinarAction(botAI); } + + static Action* kaelthas_sunstrider_cast_fear_ward_on_sanguinar_tank( + PlayerbotAI* botAI) { return new KaelthasSunstriderCastFearWardOnSanguinarTankAction(botAI); } + + static Action* kaelthas_sunstrider_warlock_tank_position_capernian( + PlayerbotAI* botAI) { return new KaelthasSunstriderWarlockTankPositionCapernianAction(botAI); } + + static Action* kaelthas_sunstrider_spread_and_move_away_from_capernian( + PlayerbotAI* botAI) { return new KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction(botAI); } + + static Action* kaelthas_sunstrider_first_assist_tank_position_telonicus( + PlayerbotAI* botAI) { return new KaelthasSunstriderFirstAssistTankPositionTelonicusAction(botAI); } + + static Action* kaelthas_sunstrider_handle_advisor_roles_in_phase_3( + PlayerbotAI* botAI) { return new KaelthasSunstriderHandleAdvisorRolesInPhase3Action(botAI); } + + static Action* kaelthas_sunstrider_assign_advisor_dps_priority( + PlayerbotAI* botAI) { return new KaelthasSunstriderAssignAdvisorDpsPriorityAction(botAI); } + + static Action* kaelthas_sunstrider_manage_advisor_dps_timer( + PlayerbotAI* botAI) { return new KaelthasSunstriderManageAdvisorDpsTimerAction(botAI); } + + static Action* kaelthas_sunstrider_assign_legendary_weapon_dps_priority( + PlayerbotAI* botAI) { return new KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction(botAI); } + + static Action* kaelthas_sunstrider_main_tank_move_devastation_away( + PlayerbotAI* botAI) { return new KaelthasSunstriderMoveDevastationAwayAction(botAI); } + + static Action* kaelthas_sunstrider_loot_legendary_weapons( + PlayerbotAI* botAI) { return new KaelthasSunstriderLootLegendaryWeaponsAction(botAI); } + + static Action* kaelthas_sunstrider_use_legendary_weapons( + PlayerbotAI* botAI) { return new KaelthasSunstriderUseLegendaryWeaponsAction(botAI); } + + static Action* kaelthas_sunstrider_reequip_gear( + PlayerbotAI* botAI) { return new KaelthasSunstriderReequipGearAction(botAI); } + + static Action* kaelthas_sunstrider_main_tank_position_boss( + PlayerbotAI* botAI) { return new KaelthasSunstriderMainTankPositionBossAction(botAI); } + + static Action* kaelthas_sunstrider_avoid_flame_strike( + PlayerbotAI* botAI) { return new KaelthasSunstriderAvoidFlameStrikeAction(botAI); } + + static Action* kaelthas_sunstrider_handle_phoenixes_and_eggs( + PlayerbotAI* botAI) { return new KaelthasSunstriderHandlePhoenixesAndEggsAction(botAI); } + + static Action* kaelthas_sunstrider_break_mind_control( + PlayerbotAI* botAI) { return new KaelthasSunstriderBreakMindControlAction(botAI); } + + static Action* kaelthas_sunstrider_break_through_shock_barrier( + PlayerbotAI* botAI) { return new KaelthasSunstriderBreakThroughShockBarrierAction(botAI); } + + static Action* kaelthas_sunstrider_spread_out_in_midair( + PlayerbotAI* botAI) { return new KaelthasSunstriderSpreadOutInMidairAction(botAI); } +}; + +#endif diff --git a/src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h b/src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h new file mode 100644 index 00000000..c6b4922d --- /dev/null +++ b/src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h @@ -0,0 +1,265 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPTRIGGERCONTEXT_H +#define _PLAYERBOT_RAIDTEMPESTKEEPTRIGGERCONTEXT_H + +#include "RaidTempestKeepTriggers.h" +#include "AiObjectContext.h" + +class RaidTempestKeepTriggerContext : public NamedObjectContext +{ +public: + RaidTempestKeepTriggerContext() + { + // Trash + creators["crimson hand centurion casts arcane volley"] = + &RaidTempestKeepTriggerContext::crimson_hand_centurion_casts_arcane_volley; + + // Al'ar + creators["al'ar pulling boss"] = + &RaidTempestKeepTriggerContext::alar_pulling_boss; + + creators["al'ar boss is flying between platforms"] = + &RaidTempestKeepTriggerContext::alar_boss_is_flying_between_platforms; + + creators["al'ar embers of al'ar explode upon death"] = + &RaidTempestKeepTriggerContext::alar_embers_of_alar_explode_upon_death; + + creators["al'ar killing embers of al'ar damages boss"] = + &RaidTempestKeepTriggerContext::alar_killing_embers_of_alar_damages_boss; + + creators["al'ar incoming flame quills"] = + &RaidTempestKeepTriggerContext::alar_incoming_flame_quills; + + creators["al'ar rising from the ashes"] = + &RaidTempestKeepTriggerContext::alar_rising_from_the_ashes; + + creators["al'ar everything is on fire in phase 2"] = + &RaidTempestKeepTriggerContext::alar_everything_is_on_fire_in_phase_2; + + creators["al'ar phase 2 encounter is at room center"] = + &RaidTempestKeepTriggerContext::alar_phase_2_encounter_is_at_room_center; + + creators["al'ar strategy changes between phases"] = + &RaidTempestKeepTriggerContext::alar_strategy_changes_between_phases; + + // Void Reaver + creators["void reaver boss casts pounding"] = + &RaidTempestKeepTriggerContext::void_reaver_boss_casts_pounding; + + creators["void reaver knock away reduces tank aggro"] = + &RaidTempestKeepTriggerContext::void_reaver_knock_away_reduces_tank_aggro; + + creators["void reaver boss launches arcane orbs"] = + &RaidTempestKeepTriggerContext::void_reaver_boss_launches_arcane_orbs; + + creators["void reaver arcane orb is incoming"] = + &RaidTempestKeepTriggerContext::void_reaver_arcane_orb_is_incoming; + + creators["void reaver bot is not in combat"] = + &RaidTempestKeepTriggerContext::void_reaver_bot_is_not_in_combat; + + // High Astromancer Solarian + creators["high astromancer solarian boss casts wrath of the astromancer"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_boss_casts_wrath_of_the_astromancer; + + creators["high astromancer solarian bot has wrath of the astromancer"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_bot_has_wrath_of_the_astromancer; + + creators["high astromancer solarian boss has vanished"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_boss_has_vanished; + + creators["high astromancer solarian solarium priests spawned"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_solarium_priests_spawned; + + creators["high astromancer solarian boss casts psychic scream"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_boss_casts_psychic_scream; + + // Kael'thas Sunstrider + creators["kael'thas sunstrider thaladred is fixated on bot"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_thaladred_is_fixated_on_bot; + + creators["kael'thas sunstrider pulling tankable advisors"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_pulling_tankable_advisors; + + creators["kael'thas sunstrider sanguinar engaged by main tank"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_sanguinar_engaged_by_main_tank; + + creators["kael'thas sunstrider sanguinar casts bellowing roar"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_sanguinar_casts_bellowing_roar; + + creators["kael'thas sunstrider capernian should be tanked by a warlock"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_capernian_should_be_tanked_by_a_warlock; + + creators["kael'thas sunstrider capernian casts arcane burst and conflagration"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_capernian_casts_arcane_burst_and_conflagration; + + creators["kael'thas sunstrider telonicus engaged by first assist tank"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_telonicus_engaged_by_first_assist_tank; + + creators["kael'thas sunstrider bots have specific roles in phase 3"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_bots_have_specific_roles_in_phase_3; + + creators["kael'thas sunstrider determining advisor kill order"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_determining_advisor_kill_order; + + creators["kael'thas sunstrider waiting for tanks to get aggro on advisors"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_waiting_for_tanks_to_get_aggro_on_advisors; + + creators["kael'thas sunstrider legendary weapons are alive"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_are_alive; + + creators["kael'thas sunstrider legendary axe casts whirlwind"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_axe_casts_whirlwind; + + creators["kael'thas sunstrider legendary weapons are dead and lootable"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_are_dead_and_lootable; + + creators["kael'thas sunstrider legendary weapons are equipped"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_are_equipped; + + creators["kael'thas sunstrider legendary weapons were lost"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_were_lost; + + creators["kael'thas sunstrider boss has entered the fight"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_boss_has_entered_the_fight; + + creators["kael'thas sunstrider raid member is mind controlled"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_raid_member_is_mind_controlled; + + creators["kael'thas sunstrider phoenixes and eggs are spawning"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_phoenixes_and_eggs_are_spawning; + + creators["kael'thas sunstrider boss is casting pyroblast"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_boss_is_casting_pyroblast; + + creators["kael'thas sunstrider boss is manipulating gravity"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_boss_is_manipulating_gravity; + } + +private: + // Trash + static Trigger* crimson_hand_centurion_casts_arcane_volley( + PlayerbotAI* botAI) { return new CrimsonHandCenturionCastsArcaneVolleyTrigger(botAI); } + + // Al'ar + static Trigger* alar_pulling_boss( + PlayerbotAI* botAI) { return new AlarPullingBossTrigger(botAI); } + + static Trigger* alar_boss_is_flying_between_platforms( + PlayerbotAI* botAI) { return new AlarBossIsFlyingBetweenPlatformsTrigger(botAI); } + + static Trigger* alar_embers_of_alar_explode_upon_death( + PlayerbotAI* botAI) { return new AlarEmbersOfAlarExplodeUponDeathTrigger(botAI); } + + static Trigger* alar_killing_embers_of_alar_damages_boss( + PlayerbotAI* botAI) { return new AlarKillingEmbersOfAlarDamagesBossTrigger(botAI); } + + static Trigger* alar_incoming_flame_quills( + PlayerbotAI* botAI) { return new AlarIncomingFlameQuillsTrigger(botAI); } + + static Trigger* alar_rising_from_the_ashes( + PlayerbotAI* botAI) { return new AlarRisingFromTheAshesTrigger(botAI); } + + static Trigger* alar_everything_is_on_fire_in_phase_2( + PlayerbotAI* botAI) { return new AlarEverythingIsOnFireInPhase2Trigger(botAI); } + + static Trigger* alar_phase_2_encounter_is_at_room_center( + PlayerbotAI* botAI) { return new AlarPhase2EncounterIsAtRoomCenterTrigger(botAI); } + + static Trigger* alar_strategy_changes_between_phases( + PlayerbotAI* botAI) { return new AlarStrategyChangesBetweenPhasesTrigger(botAI); } + + // Void Reaver + static Trigger* void_reaver_boss_casts_pounding( + PlayerbotAI* botAI) { return new VoidReaverBossCastsPoundingTrigger(botAI); } + + static Trigger* void_reaver_knock_away_reduces_tank_aggro( + PlayerbotAI* botAI) { return new VoidReaverKnockAwayReducesTankAggroTrigger(botAI); } + + static Trigger* void_reaver_boss_launches_arcane_orbs( + PlayerbotAI* botAI) { return new VoidReaverBossLaunchesArcaneOrbsTrigger(botAI); } + + static Trigger* void_reaver_arcane_orb_is_incoming( + PlayerbotAI* botAI) { return new VoidReaverArcaneOrbIsIncomingTrigger(botAI); } + + static Trigger* void_reaver_bot_is_not_in_combat( + PlayerbotAI* botAI) { return new VoidReaverBotIsNotInCombatTrigger(botAI); } + + // High Astromancer Solarian + static Trigger* high_astromancer_solarian_boss_casts_wrath_of_the_astromancer( + PlayerbotAI* botAI) { return new HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger(botAI); } + + static Trigger* high_astromancer_solarian_bot_has_wrath_of_the_astromancer( + PlayerbotAI* botAI) { return new HighAstromancerSolarianBotHasWrathOfTheAstromancerTrigger(botAI); } + + static Trigger* high_astromancer_solarian_boss_has_vanished( + PlayerbotAI* botAI) { return new HighAstromancerSolarianBossHasVanishedTrigger(botAI); } + + static Trigger* high_astromancer_solarian_solarium_priests_spawned( + PlayerbotAI* botAI) { return new HighAstromancerSolarianSolariumPriestsSpawnedTrigger(botAI); } + + static Trigger* high_astromancer_solarian_boss_casts_psychic_scream( + PlayerbotAI* botAI) { return new HighAstromancerSolarianBossCastsPsychicScreamTrigger(botAI); } + + // Kael'thas Sunstrider + static Trigger* kaelthas_sunstrider_thaladred_is_fixated_on_bot( + PlayerbotAI* botAI) { return new KaelthasSunstriderThaladredIsFixatedOnBotTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_pulling_tankable_advisors( + PlayerbotAI* botAI) { return new KaelthasSunstriderPullingTankableAdvisorsTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_sanguinar_engaged_by_main_tank( + PlayerbotAI* botAI) { return new KaelthasSunstriderSanguinarEngagedByMainTankTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_sanguinar_casts_bellowing_roar( + PlayerbotAI* botAI) { return new KaelthasSunstriderSanguinarCastsBellowingRoarTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_capernian_should_be_tanked_by_a_warlock( + PlayerbotAI* botAI) { return new KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_capernian_casts_arcane_burst_and_conflagration( + PlayerbotAI* botAI) { return new KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_telonicus_engaged_by_first_assist_tank( + PlayerbotAI* botAI) { return new KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_bots_have_specific_roles_in_phase_3( + PlayerbotAI* botAI) { return new KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger(botAI); } + + static Trigger* kaelthas_sunstrider_determining_advisor_kill_order( + PlayerbotAI* botAI) { return new KaelthasSunstriderDeterminingAdvisorKillOrderTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_waiting_for_tanks_to_get_aggro_on_advisors( + PlayerbotAI* botAI) { return new KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_legendary_weapons_are_alive( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryWeaponsAreAliveTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_legendary_axe_casts_whirlwind( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_legendary_weapons_are_dead_and_lootable( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_legendary_weapons_are_equipped( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_legendary_weapons_were_lost( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryWeaponsWereLostTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_boss_has_entered_the_fight( + PlayerbotAI* botAI) { return new KaelthasSunstriderBossHasEnteredTheFightTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_raid_member_is_mind_controlled( + PlayerbotAI* botAI) { return new KaelthasSunstriderRaidMemberIsMindControlledTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_phoenixes_and_eggs_are_spawning( + PlayerbotAI* botAI) { return new KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_boss_is_casting_pyroblast( + PlayerbotAI* botAI) { return new KaelthasSunstriderBossIsCastingPyroblastTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_boss_is_manipulating_gravity( + PlayerbotAI* botAI) { return new KaelthasSunstriderBossIsManipulatingGravityTrigger(botAI); } +}; + +#endif diff --git a/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp b/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp new file mode 100644 index 00000000..74950c3a --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp @@ -0,0 +1,162 @@ +#include "RaidTempestKeepStrategy.h" +#include "RaidTempestKeepMultipliers.h" + +void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) +{ + // Trash + triggers.push_back(new TriggerNode("crimson hand centurion casts arcane volley", { + NextAction("crimson hand centurion cast polymorph", ACTION_RAID + 1) })); + + // Al'ar + triggers.push_back(new TriggerNode("al'ar pulling boss", { + NextAction("al'ar misdirect boss to main tank", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("al'ar boss is flying between platforms", { + NextAction("al'ar boss tanks move between platforms", ACTION_RAID + 1), + NextAction("al'ar melee dps move between platforms", ACTION_RAID + 1), + NextAction("al'ar ranged and ember tank move under platforms", ACTION_RAID + 4) })); + + triggers.push_back(new TriggerNode("al'ar embers of al'ar explode upon death", { + NextAction("al'ar assist tanks pick up embers", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("al'ar killing embers of al'ar damages boss", { + NextAction("al'ar ranged dps prioritize embers", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("al'ar incoming flame quills", { + NextAction("al'ar jump from platform", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("al'ar rising from the ashes", { + NextAction("al'ar move away from rebirth", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("al'ar everything is on fire in phase 2", { + NextAction("al'ar swap tanks on boss", ACTION_EMERGENCY + 2), + NextAction("al'ar avoid flame patches and dive bombs", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("al'ar phase 2 encounter is at room center", { + NextAction("al'ar return to room center", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("al'ar strategy changes between phases", { + NextAction("al'ar manage phase tracker", ACTION_EMERGENCY + 10) })); + + // Void Reaver + triggers.push_back(new TriggerNode("void reaver boss casts pounding", { + NextAction("void reaver tanks position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("void reaver knock away reduces tank aggro", { + NextAction("void reaver use aggro dump ability", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("void reaver boss launches arcane orbs", { + NextAction("void reaver spread ranged", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("void reaver arcane orb is incoming", { + NextAction("void reaver avoid arcane orb", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("void reaver bot is not in combat", { + NextAction("void reaver erase trackers", ACTION_EMERGENCY + 11) })); + + // High Astromancer Solarian + triggers.push_back(new TriggerNode("high astromancer solarian boss casts wrath of the astromancer", { + NextAction("high astromancer solarian ranged leave space for melee", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("high astromancer solarian bot has wrath of the astromancer", { + NextAction("high astromancer solarian move away from group", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("high astromancer solarian boss has vanished", { + NextAction("high astromancer solarian stack for aoe", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("high astromancer solarian solarium priests spawned", { + NextAction("high astromancer solarian target solarium priests", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("high astromancer solarian boss casts psychic scream", { + NextAction("high astromancer solarian cast fear ward on main tank", ACTION_RAID + 2) })); + + // Kael'thas Sunstrider + triggers.push_back(new TriggerNode("kael'thas sunstrider thaladred is fixated on bot", { + NextAction("kael'thas sunstrider kite thaladred", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider pulling tankable advisors", { + NextAction("kael'thas sunstrider misdirect advisors to tanks", ACTION_EMERGENCY + 2) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider sanguinar engaged by main tank", { + NextAction("kael'thas sunstrider main tank position sanguinar", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider sanguinar casts bellowing roar", { + NextAction("kael'thas sunstrider cast fear ward on sanguinar tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider capernian should be tanked by a warlock", { + NextAction("kael'thas sunstrider warlock tank position capernian", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider capernian casts arcane burst and conflagration", { + NextAction("kael'thas sunstrider spread and move away from capernian", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider telonicus engaged by first assist tank", { + NextAction("kael'thas sunstrider first assist tank position telonicus", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider bots have specific roles in phase 3", { + NextAction("kael'thas sunstrider handle advisor roles in phase 3", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider determining advisor kill order", { + NextAction("kael'thas sunstrider assign advisor dps priority", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider waiting for tanks to get aggro on advisors", { + NextAction("kael'thas sunstrider manage advisor dps timer", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are alive", { + NextAction("kael'thas sunstrider assign legendary weapon dps priority", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary axe casts whirlwind", { + NextAction("kael'thas sunstrider main tank move devastation away", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are dead and lootable", { + NextAction("kael'thas sunstrider loot legendary weapons", ACTION_RAID) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are equipped", { + NextAction("kael'thas sunstrider use legendary weapons", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons were lost", { + NextAction("kael'thas sunstrider reequip gear", ACTION_EMERGENCY + 11) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider boss has entered the fight", { + NextAction("kael'thas sunstrider main tank position boss", ACTION_RAID + 1), + NextAction("kael'thas sunstrider avoid flame strike", ACTION_EMERGENCY + 8) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider phoenixes and eggs are spawning", { + NextAction("kael'thas sunstrider handle phoenixes and eggs", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider raid member is mind controlled", { + NextAction("kael'thas sunstrider break mind control", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider boss is casting pyroblast", { + NextAction("kael'thas sunstrider break through shock barrier", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider boss is manipulating gravity", { + NextAction("kael'thas sunstrider spread out in midair", ACTION_EMERGENCY + 1) })); +} + +void RaidTempestKeepStrategy::InitMultipliers(std::vector& multipliers) +{ + // Alar + multipliers.push_back(new AlarMoveBetweenPlatformsMultiplier(botAI)); + multipliers.push_back(new AlarDisableDisperseMultiplier(botAI)); + multipliers.push_back(new AlarDisableTankAssistMultiplier(botAI)); + multipliers.push_back(new AlarStayAwayFromRebirthMultiplier(botAI)); + multipliers.push_back(new AlarPhase2NoTankingIfArmorMeltedMultiplier(botAI)); + + // Void Reaver + multipliers.push_back(new VoidReaverMaintainPositionsMultiplier(botAI)); + + // High Astromancer Solarian + multipliers.push_back(new HighAstromancerSolarianDisableTankAssistMultiplier(botAI)); + multipliers.push_back(new HighAstromancerSolarianMaintainPositionMultiplier(botAI)); + + // Kael'thas Sunstrider + multipliers.push_back(new KaelthasSunstriderWaitForDpsMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderKiteThaladredMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderControlMisdirectionMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderKeepDistanceFromCapernianMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderManageWeaponTankingMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderDisableAdvisorTankAssistMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderDisableDisperseMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderDelayCooldownsMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier(botAI)); +} diff --git a/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.h b/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.h new file mode 100644 index 00000000..77fd29c3 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.h @@ -0,0 +1,18 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPSTRATEGY_H_ +#define _PLAYERBOT_RAIDTEMPESTKEEPSTRATEGY_H_ + +#include "Strategy.h" +#include "Multiplier.h" + +class RaidTempestKeepStrategy : public Strategy +{ +public: + RaidTempestKeepStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + std::string const getName() override { return "tempestkeep"; } + + void InitTriggers(std::vector& triggers) override; + void InitMultipliers(std::vector& multipliers) override; +}; + +#endif diff --git a/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp new file mode 100644 index 00000000..b0b399b1 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp @@ -0,0 +1,527 @@ +#include "RaidTempestKeepTriggers.h" +#include "RaidTempestKeepHelpers.h" +#include "RaidTempestKeepActions.h" +#include "RaidTempestKeepKaelthasBossAI.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +using namespace TempestKeepHelpers; + +// Trash + +bool CrimsonHandCenturionCastsArcaneVolleyTrigger::IsActive() +{ + return bot->getClass() == CLASS_MAGE && + AI_VALUE2(Unit*, "find target", "crimson hand centurion"); +} + +// Al'ar + +bool AlarPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + return alar && alar->GetHealthPct() > 98.0f; +} + +bool AlarBossIsFlyingBetweenPlatformsTrigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar || !alar->IsVisible()) + return false; + + if (isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return false; + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + return locationIndex != POINT_QUILL_OR_DIVE_IDX && + locationIndex != POINT_MIDDLE_IDX; +} + +bool AlarEmbersOfAlarExplodeUponDeathTrigger::IsActive() +{ + return botAI->IsTank(bot) && AI_VALUE2(Unit*, "find target", "ember of al'ar"); +} + +bool AlarKillingEmbersOfAlarDamagesBossTrigger::IsActive() +{ + return botAI->IsRangedDps(bot) && + AI_VALUE2(Unit*, "find target", "ember of al'ar"); +} + +bool AlarIncomingFlameQuillsTrigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar || isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return false; + + Position dest; + return GetAlarCurrentLocationIndex(alar) == POINT_QUILL_OR_DIVE_IDX || + GetAlarDestinationLocationIndex(alar, dest) == POINT_QUILL_OR_DIVE_IDX; +} + +bool AlarRisingFromTheAshesTrigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return false; + + Creature* alarCreature = alar->ToCreature(); + return alarCreature && alarCreature->GetReactState() == REACT_PASSIVE; +} + +bool AlarEverythingIsOnFireInPhase2Trigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + return alar && isAlarInPhase2[alar->GetMap()->GetInstanceId()]; +} + +bool AlarPhase2EncounterIsAtRoomCenterTrigger::IsActive() +{ + if (bot->GetVictim()) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar || !isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return false; + + Creature* alarCreature = alar->ToCreature(); + if (alarCreature && alarCreature->GetReactState() == REACT_PASSIVE) + return false; + + Position dest; + return GetAlarCurrentLocationIndex(alar) != POINT_QUILL_OR_DIVE_IDX && + GetAlarDestinationLocationIndex(alar, dest) != POINT_QUILL_OR_DIVE_IDX; +} + +bool AlarStrategyChangesBetweenPhasesTrigger::IsActive() +{ + return botAI->IsDps(bot) && IsMechanicTrackerBot(botAI, bot, TEMPEST_KEEP_MAP_ID) && + AI_VALUE2(Unit*, "find target", "al'ar"); +} + +// Void Reaver + +bool VoidReaverBossCastsPoundingTrigger::IsActive() +{ + if (!botAI->IsTank(bot)) + return false; + + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + return voidReaver && voidReaver->GetVictim() == bot; +} + +bool VoidReaverKnockAwayReducesTankAggroTrigger::IsActive() +{ + if (botAI->IsTank(bot)) + return false; + + if (bot->getClass() == CLASS_DEATH_KNIGHT || + bot->getClass() == CLASS_DRUID || + bot->getClass() == CLASS_SHAMAN || + bot->getClass() == CLASS_WARRIOR) + return false; + + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + return voidReaver && voidReaver->GetVictim() == bot; +} + +bool VoidReaverBossLaunchesArcaneOrbsTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + return voidReaver && voidReaver->GetVictim() != bot; +} + +bool VoidReaverArcaneOrbIsIncomingTrigger::IsActive() +{ + if (botAI->IsTank(bot)) + return false; + + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (!voidReaver || voidReaver->GetVictim() == bot) + return false; + + auto it = voidReaverArcaneOrbs.find(bot->GetMap()->GetInstanceId()); + if (it == voidReaverArcaneOrbs.end() || it->second.empty()) + return false; + + uint32 currentTime = getMSTime(); + constexpr uint32 orbDuration = 7000; + constexpr float safeDistance = 22.0f; + + for (auto const& orb : it->second) + { + if (getMSTimeDiff(orb.castTime, currentTime) <= orbDuration && + bot->GetExactDist2d(orb.destination.GetPositionX(), + orb.destination.GetPositionY()) < safeDistance) + return true; + } + + return false; +} + +bool VoidReaverBotIsNotInCombatTrigger::IsActive() +{ + return !bot->IsInCombat(); +} + +// High Astromancer Solarian + +bool HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger::IsActive() +{ + if (bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER)) + return false; + + if (!botAI->IsRanged(bot)) + return false; + + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + return astromancer && !astromancer->HasAura(SPELL_SOLARIAN_TRANSFORM); +} + +bool HighAstromancerSolarianBotHasWrathOfTheAstromancerTrigger::IsActive() +{ + return bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER); +} + +bool HighAstromancerSolarianBossHasVanishedTrigger::IsActive() +{ + if (bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER)) + return false; + + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + if (!astromancer) + return false; + + Creature* astromancerCreature = astromancer->ToCreature(); + return astromancerCreature && + astromancerCreature->GetReactState() == REACT_PASSIVE; +} + +bool HighAstromancerSolarianSolariumPriestsSpawnedTrigger::IsActive() +{ + return botAI->IsMelee(bot) && AI_VALUE2(Unit*, "find target", "solarium priest"); +} + +bool HighAstromancerSolarianBossCastsPsychicScreamTrigger::IsActive() +{ + if (bot->getClass() != CLASS_PRIEST) + return false; + + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + return astromancer && astromancer->HasAura(SPELL_SOLARIAN_TRANSFORM); +} + +// Kael'thas Sunstrider + +bool KaelthasSunstriderThaladredIsFixatedOnBotTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + if (!thaladred || thaladred->GetVictim() != bot) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return false; + + return !(botAI->IsTank(bot) && kaelAI->GetPhase() == PHASE_ALL_ADVISORS); +} + +bool KaelthasSunstriderPullingTankableAdvisorsTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR || + kaelAI->GetPhase() == PHASE_ALL_ADVISORS); +} + +bool KaelthasSunstriderSanguinarEngagedByMainTankTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + return sanguinar && !sanguinar->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !sanguinar->HasAura(SPELL_PERMANENT_FEIGN_DEATH); +} + +bool KaelthasSunstriderSanguinarCastsBellowingRoarTrigger::IsActive() +{ + if (bot->getClass() != CLASS_PRIEST) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return false; + + if (kaelAI->GetPhase() != PHASE_SINGLE_ADVISOR && + kaelAI->GetPhase() != PHASE_TRANSITION && + kaelAI->GetPhase() != PHASE_ALL_ADVISORS) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank || mainTank->HasAura(SPELL_FEAR_WARD)) + return false; + + return botAI->CanCastSpell("fear ward", mainTank); +} + +bool KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger::IsActive() +{ + if (bot->getClass() != CLASS_WARLOCK) + return false; + + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (!capernian || capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || + capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + return false; + + return GetCapernianTank(bot) == bot; +} + +bool KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger::IsActive() +{ + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (!capernian || capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || + capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + return false; + + return GetCapernianTank(bot) != bot; +} + +bool KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger::IsActive() +{ + if (!botAI->IsAssistTankOfIndex(bot, 0, false)) + return false; + + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + return telonicus && !telonicus->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !telonicus->HasAura(SPELL_PERMANENT_FEIGN_DEATH); +} + +bool KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (!AI_VALUE2(Unit*, "find target", "master engineer telonicus") && + !AI_VALUE2(Unit*, "find target", "lord sanguinar")) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || kaelAI->GetPhase() != PHASE_ALL_ADVISORS) + return false; + + return botAI->IsAssistHealOfIndex(bot, 0, true) || + botAI->IsMainTank(bot) || botAI->IsAssistTankOfIndex(bot, 0, true) || + (bot->getClass() == CLASS_WARLOCK && GetCapernianTank(bot) == bot); +} + +bool KaelthasSunstriderDeterminingAdvisorKillOrderTrigger::IsActive() +{ + if (botAI->IsHeal(bot) || + botAI->IsMainTank(bot) || + botAI->IsAssistTankOfIndex(bot, 0, true)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR || + kaelAI->GetPhase() == PHASE_ALL_ADVISORS); +} + +bool KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger::IsActive() +{ + if (!botAI->IsDps(bot)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || kaelAI->GetPhase() != PHASE_SINGLE_ADVISOR) + return false; + + return IsMechanicTrackerBot(botAI, bot, TEMPEST_KEEP_MAP_ID, GetCapernianTank(bot)); +} + +bool KaelthasSunstriderLegendaryWeaponsAreAliveTrigger::IsActive() +{ + if (botAI->IsMainTank(bot)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && kaelAI->GetPhase() == PHASE_WEAPONS; +} + +bool KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger::IsActive() +{ + return botAI->IsMainTank(bot) && + AI_VALUE2(Unit*, "find target", "devastation"); +} + +bool KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || + (kaelAI->GetPhase() != PHASE_WEAPONS && kaelAI->GetPhase() != PHASE_ALL_ADVISORS)) + return false; + + Unit* axe = AI_VALUE2(Unit*, "find target", "devastation"); + if (axe && axe->GetVictim() == bot) + return false; + + return IsAnyLegendaryWeaponDead(bot); +} + +bool KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) + return false; + + return bot->HasItemCount(ITEM_STAFF_OF_DISINTEGRATION, 1, false) || + bot->HasItemCount(ITEM_NETHERSTRAND_LONGBOW, 1, false) || + bot->HasItemCount(ITEM_PHASESHIFT_BULWARK, 1, false); +} + +bool KaelthasSunstriderLegendaryWeaponsWereLostTrigger::IsActive() +{ + if (bot->GetMapId() != TEMPEST_KEEP_MAP_ID) + return false; + + Map* map = bot->GetMap(); + if (!map) + return false; + + constexpr uint32 KAELTHAS_DB_GUID = 158218; + auto const& creatureStore = map->GetCreatureBySpawnIdStore(); + auto it = creatureStore.find(KAELTHAS_DB_GUID); + if (it == creatureStore.end()) + return false; + + Creature* kaelthas = it->second; + if (!kaelthas || bot->GetExactDist2d(kaelthas) > 150.0f) + return false; + + const std::array weaponSlots = + { + EQUIPMENT_SLOT_MAINHAND, + EQUIPMENT_SLOT_OFFHAND, + EQUIPMENT_SLOT_RANGED + }; + + for (uint8 slot : weaponSlots) + { + if (!bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot) && + HasEquippableItemForSlot(bot, slot)) + return true; + } + + return false; +} + +bool KaelthasSunstriderBossHasEnteredTheFightTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && kaelAI->GetPhase() == PHASE_FINAL; +} + +bool KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (botAI->IsTank(bot) && kaelthas->GetVictim() == bot) + return false; + + return AI_VALUE2(Unit*, "find target", "phoenix") || + AI_VALUE2(Unit*, "find target", "phoenix egg"); +} + +bool KaelthasSunstriderRaidMemberIsMindControlledTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (botAI->IsTank(bot) && kaelthas->GetVictim() == bot) + return false; + + if (!bot->HasItemCount(ITEM_INFINITY_BLADE, 1, true)) + return false; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (member->HasAura(SPELL_KAELTHAS_MIND_CONTROL)) + return true; + } + } + + return false; +} + +bool KaelthasSunstriderBossIsCastingPyroblastTrigger::IsActive() +{ + if (!botAI->IsDps(bot)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + return kaelthas && kaelthas->HasAura(SPELL_SHOCK_BARRIER); +} + +bool KaelthasSunstriderBossIsManipulatingGravityTrigger::IsActive() +{ + return bot->HasAura(SPELL_GRAVITY_LAPSE); +} diff --git a/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.h b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.h new file mode 100644 index 00000000..13b94c68 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.h @@ -0,0 +1,338 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPTRIGGERS_H +#define _PLAYERBOT_RAIDTEMPESTKEEPTRIGGERS_H + +#include "Trigger.h" + +// General + +// Trash + +class CrimsonHandCenturionCastsArcaneVolleyTrigger : public Trigger +{ +public: + CrimsonHandCenturionCastsArcaneVolleyTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "crimson hand centurion casts arcane volley") {} + bool IsActive() override; +}; + +// Al'ar + +class AlarPullingBossTrigger : public Trigger +{ +public: + AlarPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar pulling boss") {} + bool IsActive() override; +}; + +class AlarBossIsFlyingBetweenPlatformsTrigger : public Trigger +{ +public: + AlarBossIsFlyingBetweenPlatformsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar boss is flying between platforms") {} + bool IsActive() override; +}; + +class AlarEmbersOfAlarExplodeUponDeathTrigger : public Trigger +{ +public: + AlarEmbersOfAlarExplodeUponDeathTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar embers of al'ar explode upon death") {} + bool IsActive() override; +}; + +class AlarKillingEmbersOfAlarDamagesBossTrigger : public Trigger +{ +public: + AlarKillingEmbersOfAlarDamagesBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar killing embers of al'ar damages boss") {} + bool IsActive() override; +}; + +class AlarIncomingFlameQuillsTrigger : public Trigger +{ +public: + AlarIncomingFlameQuillsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar incoming flame quills") {} + bool IsActive() override; +}; + +class AlarRisingFromTheAshesTrigger : public Trigger +{ +public: + AlarRisingFromTheAshesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar rising from the ashes") {} + bool IsActive() override; +}; + +class AlarEverythingIsOnFireInPhase2Trigger : public Trigger +{ +public: + AlarEverythingIsOnFireInPhase2Trigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar everything is on fire in phase 2") {} + bool IsActive() override; +}; + +class AlarPhase2EncounterIsAtRoomCenterTrigger : public Trigger +{ +public: + AlarPhase2EncounterIsAtRoomCenterTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar phase 2 encounter is at room center") {} + bool IsActive() override; +}; + +class AlarStrategyChangesBetweenPhasesTrigger : public Trigger +{ +public: + AlarStrategyChangesBetweenPhasesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar strategy changes between phases") {} + bool IsActive() override; +}; + +// Void Reaver + +class VoidReaverBossCastsPoundingTrigger : public Trigger +{ +public: + VoidReaverBossCastsPoundingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver boss casts pounding") {} + bool IsActive() override; +}; + +class VoidReaverKnockAwayReducesTankAggroTrigger : public Trigger +{ +public: + VoidReaverKnockAwayReducesTankAggroTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver knock away reduces tank aggro") {} + bool IsActive() override; +}; + +class VoidReaverBossLaunchesArcaneOrbsTrigger : public Trigger +{ +public: + VoidReaverBossLaunchesArcaneOrbsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver boss launches arcane orbs") {} + bool IsActive() override; +}; + +class VoidReaverArcaneOrbIsIncomingTrigger : public Trigger +{ +public: + VoidReaverArcaneOrbIsIncomingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver arcane orb is incoming") {} + bool IsActive() override; +}; + +class VoidReaverBotIsNotInCombatTrigger : public Trigger +{ +public: + VoidReaverBotIsNotInCombatTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver bot is not in combat") {} + bool IsActive() override; +}; + +// High Astromancer Solarian + +class HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger : public Trigger +{ +public: + HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian boss casts wrath of the astromancer") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianBotHasWrathOfTheAstromancerTrigger : public Trigger +{ +public: + HighAstromancerSolarianBotHasWrathOfTheAstromancerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian bot has wrath of the astromancer") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianBossHasVanishedTrigger : public Trigger +{ +public: + HighAstromancerSolarianBossHasVanishedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian boss has vanished") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianSolariumPriestsSpawnedTrigger : public Trigger +{ +public: + HighAstromancerSolarianSolariumPriestsSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian solarium priests spawned") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianBossCastsPsychicScreamTrigger : public Trigger +{ +public: + HighAstromancerSolarianBossCastsPsychicScreamTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer boss casts psychic scream") {} + bool IsActive() override; +}; + +// Kael'thas Sunstrider + +class KaelthasSunstriderThaladredIsFixatedOnBotTrigger : public Trigger +{ +public: + KaelthasSunstriderThaladredIsFixatedOnBotTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider thaladred is fixated on bot") {} + bool IsActive() override; +}; + +class KaelthasSunstriderPullingTankableAdvisorsTrigger : public Trigger +{ +public: + KaelthasSunstriderPullingTankableAdvisorsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider pulling tankable advisors") {} + bool IsActive() override; +}; + +class KaelthasSunstriderSanguinarEngagedByMainTankTrigger : public Trigger +{ +public: + KaelthasSunstriderSanguinarEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider sanguinar engaged by main tank") {} + bool IsActive() override; +}; + +class KaelthasSunstriderSanguinarCastsBellowingRoarTrigger : public Trigger +{ +public: + KaelthasSunstriderSanguinarCastsBellowingRoarTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider sanguinar casts bellowing roar") {} + bool IsActive() override; +}; + +class KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger : public Trigger +{ +public: + KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider capernian should be tanked by a warlock") {} + bool IsActive() override; +}; + +class KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger : public Trigger +{ +public: + KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider capernian casts arcane burst and conflagration") {} + bool IsActive() override; +}; + +class KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger : public Trigger +{ +public: + KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider telonicus engaged by first assist tank") {} + bool IsActive() override; +}; + +class KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger : public Trigger +{ +public: + KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider bots have specific roles in phase 3") {} + bool IsActive() override; +}; + +class KaelthasSunstriderDeterminingAdvisorKillOrderTrigger : public Trigger +{ +public: + KaelthasSunstriderDeterminingAdvisorKillOrderTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider determining advisor kill order") {} + bool IsActive() override; +}; + +class KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger : public Trigger +{ +public: + KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider waiting for tanks to get aggro on advisors") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryWeaponsAreAliveTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryWeaponsAreAliveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary weapons are alive") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary axe casts whirlwind") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary weapons are dead and lootable") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary weapons are equipped") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryWeaponsWereLostTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryWeaponsWereLostTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary weapons were lost") {} + bool IsActive() override; +}; + +class KaelthasSunstriderBossHasEnteredTheFightTrigger : public Trigger +{ +public: + KaelthasSunstriderBossHasEnteredTheFightTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider boss has entered the fight") {} + bool IsActive() override; +}; + +class KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger : public Trigger +{ +public: + KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider phoenixes and eggs are spawning") {} + bool IsActive() override; +}; + +class KaelthasSunstriderRaidMemberIsMindControlledTrigger : public Trigger +{ +public: + KaelthasSunstriderRaidMemberIsMindControlledTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider raid member is mind controlled") {} + bool IsActive() override; +}; + +class KaelthasSunstriderBossIsCastingPyroblastTrigger : public Trigger +{ +public: + KaelthasSunstriderBossIsCastingPyroblastTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider boss is casting pyroblast") {} + bool IsActive() override; +}; + +class KaelthasSunstriderBossIsManipulatingGravityTrigger : public Trigger +{ +public: + KaelthasSunstriderBossIsManipulatingGravityTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider boss is manipulating gravity") {} + bool IsActive() override; +}; + +#endif diff --git a/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.cpp b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.cpp new file mode 100644 index 00000000..30327771 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.cpp @@ -0,0 +1,425 @@ +#include "RaidTempestKeepHelpers.h" +#include "RaidTempestKeepActions.h" +#include "LootObjectStack.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +namespace TempestKeepHelpers +{ + // General + + Unit* GetNearestNonTankPlayerInRadius(PlayerbotAI* botAI, Player* bot, float radius) + { + Unit* nearestPlayer = nullptr; + float nearestDistance = radius; + + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref != nullptr; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == bot || botAI->IsTank(member)) + continue; + + float distance = bot->GetExactDist2d(member); + if (distance < nearestDistance) + { + nearestDistance = distance; + nearestPlayer = member; + } + } + + return nearestPlayer; + } + + std::vector GetAllHazardTriggers(Player* bot, uint32 npcEntry, float searchRadius) + { + std::vector hazardTriggers; + + std::list creatureList; + bot->GetCreatureListWithEntryInGrid(creatureList, npcEntry, searchRadius); + + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + hazardTriggers.push_back(creature); + } + + return hazardTriggers; + } + + Position FindSafestNearbyPosition(Player* bot, const std::vector& hazards, + float hazardRadius, const Position* center) + { + constexpr float searchStep = M_PI / 8.0f; + constexpr float minDistance = 2.0f; + constexpr float maxDistance = 30.0f; + constexpr float distanceStep = 1.0f; + + Position bestPos; + float minMoveDistance = std::numeric_limits::max(); + bool foundSafe = false; + + for (float distance = minDistance; distance <= maxDistance; distance += distanceStep) + { + for (float angle = 0.0f; angle < 2 * M_PI; angle += searchStep) + { + const Position& searchCenter = center ? *center : bot->GetPosition(); + float x = searchCenter.GetPositionX() + distance * std::cos(angle); + float y = searchCenter.GetPositionY() + distance * std::sin(angle); + + bool isSafe = true; + for (Unit* hazard : hazards) + { + if (hazard->GetExactDist2d(x, y) < hazardRadius) + { + isSafe = false; + break; + } + } + + if (!isSafe) + continue; + + Position testPos(x, y, bot->GetPositionZ()); + + bool pathSafe = IsPathSafeFromHazards(bot->GetPosition(), testPos, hazards, 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 IsPathSafeFromHazards( + const Position& start, const Position& end, const std::vector& hazards, 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* hazard : hazards) + { + float distToHazard = hazard->GetExactDist2d(checkX, checkY); + if (distToHazard < hazardRadius) + return false; + } + } + + return true; + } + + // Al'ar + + const Position ALAR_PLATFORM_0 = { 335.638f, 59.4879f, 17.9319f }; // West Platform + const Position ALAR_PLATFORM_1 = { 388.751f, 31.7312f, 20.2636f }; // Northwest Platform + const Position ALAR_PLATFORM_2 = { 388.791f, -33.1059f, 20.2636f }; // Northeast Platform + const Position ALAR_PLATFORM_3 = { 332.723f, -61.159f, 17.9791f }; // East Platform + const std::array PLATFORM_POSITIONS = + { + ALAR_PLATFORM_0, + ALAR_PLATFORM_1, + ALAR_PLATFORM_2, + ALAR_PLATFORM_3 + }; + const Position ALAR_GROUND_0 = { 336.439f, 48.181f, -2.389f }; // Ground counterpart to West Platform + const Position ALAR_GROUND_1 = { 379.122f, 25.146f, -2.385f }; // Ground counterpart to Northwest Platform + const Position ALAR_GROUND_2 = { 378.583f, -27.481f, -2.385f }; // Ground counterpart to Northeast Platform + const Position ALAR_GROUND_3 = { 331.631f, -49.716f, -2.389f }; // Ground counterpart to East Platform + const std::array GROUND_POSITIONS = + { + ALAR_GROUND_0, + ALAR_GROUND_1, + ALAR_GROUND_2, + ALAR_GROUND_3 + }; + const Position ALAR_ROOM_CENTER = { 330.611f, -2.540f, -2.389f }; + const Position ALAR_POINT_QUILL_OR_DIVE = { 332.000f, 0.010f, 43.000f }; + const Position ALAR_POINT_MIDDLE = { 331.000f, 0.010f, -2.380f }; + const Position ALAR_SE_RAMP_BASE = { 281.064f, -36.590f, -2.389f }; + const Position ALAR_SW_RAMP_BASE = { 281.064f, 36.590f, -2.389f }; + const Position ALAR_ROOM_S_CENTER = { 281.064f, 0.000f, -2.389f }; + + std::unordered_map lastRebirthState; + std::unordered_map isAlarInPhase2; + + int8 GetAlarDestinationLocationIndex(Unit* alar, Position& dest) + { + if (!alar) + return LOCATION_NONE; + + float x, y, z; + if (!alar->GetMotionMaster()->GetDestination(x, y, z)) + return LOCATION_NONE; + + dest.Relocate(x, y, z); + + const std::array locations = + { + ALAR_PLATFORM_0, + ALAR_PLATFORM_1, + ALAR_PLATFORM_2, + ALAR_PLATFORM_3, + ALAR_POINT_QUILL_OR_DIVE, + ALAR_POINT_MIDDLE, + }; + + float minDist = std::numeric_limits::max(); + int8 locationIndex = LOCATION_NONE; + for (int8 i = 0; i < TOTAL_ALAR_LOCATIONS; ++i) + { + float dist = dest.GetExactDist2d(&locations[i]); + if (dist < minDist) + { + minDist = dist; + locationIndex = i; + } + } + if (minDist > 0.1f) + return LOCATION_NONE; + + return locationIndex; + } + + int8 GetAlarCurrentLocationIndex(Unit* alar) + { + if (!alar) + return LOCATION_NONE; + + const std::array locations = + { + ALAR_PLATFORM_0, + ALAR_PLATFORM_1, + ALAR_PLATFORM_2, + ALAR_PLATFORM_3, + ALAR_POINT_QUILL_OR_DIVE, + ALAR_POINT_MIDDLE, + }; + + float minDist = std::numeric_limits::max(); + int8 locationIndex = LOCATION_NONE; + for (int8 i = 0; i < TOTAL_ALAR_LOCATIONS; ++i) + { + float dist = alar->GetPosition().GetExactDist2d(&locations[i]); + if (dist < minDist) + { + minDist = dist; + locationIndex = i; + } + } + if (minDist > 0.1f) + return LOCATION_NONE; + + return locationIndex; + } + + void GetClosestPlatformAndGround(const Position& botPos, int8& closestPlatform, Position& ground) + { + float minDist = std::numeric_limits::max(); + closestPlatform = -1; + for (int8 i = 0; i < 4; ++i) + { + float dist = botPos.GetExactDist2d(&PLATFORM_POSITIONS[i]); + if (dist < minDist) + { + minDist = dist; + closestPlatform = i; + } + } + ground = GROUND_POSITIONS[closestPlatform]; + } + + std::pair GetFirstTwoEmbersOfAlar(PlayerbotAI* botAI) + { + Unit* firstEmber = nullptr; + Unit* secondEmber = nullptr; + + for (auto const& guid : + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get()) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_EMBER_OF_ALAR) + { + if (!firstEmber) + { + firstEmber = unit; + } + else if (!secondEmber) + { + secondEmber = unit; + break; + } + } + } + + return { firstEmber, secondEmber }; + } + + Player* GetSecondEmberTank(PlayerbotAI* botAI) + { + Player* mainTank = GetGroupMainTank(botAI, botAI->GetBot()); + Player* assistTank = GetGroupAssistTank(botAI, botAI->GetBot(), 0); + + bool mainTankHasMelt = mainTank && mainTank->HasAura(SPELL_MELT_ARMOR); + bool assistTankHasMelt = assistTank && assistTank->HasAura(SPELL_MELT_ARMOR); + + if (mainTankHasMelt) + return mainTank; + + if (assistTankHasMelt || (!mainTankHasMelt && !assistTankHasMelt)) + return assistTank; + + return nullptr; + } + + // Void Reaver + + const Position VOID_REAVER_TANK_POSITION = { 423.845f, 371.733f, 14.897f }; + + std::unordered_map hasReachedVoidReaverPosition; + std::unordered_map> voidReaverArcaneOrbs; + + // Kael'thas Sunstrider + + const Position SANGUINAR_TANK_POSITION = { 775.478f, 39.888f, 46.780f }; + const Position SANGUINAR_WAITING_POSITION = { 761.850f, 27.459f, 46.779f }; + const Position TELONICUS_TANK_POSITION = { 773.717f, 44.091f, 46.780f }; + const Position TELONICUS_WAITING_POSITION = { 754.347f, 31.739f, 46.796f }; + const Position ADVISOR_HEAL_POSITION = { 752.171f, 19.494f, 46.779f }; + const Position CAPERNIAN_WAITING_POSITION = { 743.897f, -11.575f, 46.779f }; + const Position KAELTHAS_TANK_POSITION = { 799.390f, -0.837f, 48.729f }; + + std::unordered_map advisorDpsWaitTimer; + + // (1) First priority is an assistant Warlock (real player or bot) + // (2) If no assistant Warlock, then look for any Warlock bot + Player* GetCapernianTank(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; + } + + // One Hunter will start on Sanguinar in Phase 3 with Melee to apply Armor Disruption + // (1) First priority is an assistant Hunter (real player or bot) + // (2) If no assistant Hunter, then look for any Hunter bot + bool IsDebuffHunter(Player* bot) + { + if (bot->getClass() != CLASS_HUNTER || !bot->IsAlive()) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* fallbackHunter = nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->getClass() != CLASS_HUNTER) + continue; + + if (group->IsAssistant(member->GetGUID())) + return member == bot; + + if (!fallbackHunter && GET_PLAYERBOT_AI(member)) + fallbackHunter = member; + } + + return fallbackHunter == bot; + } + + bool IsAnyLegendaryWeaponDead(Player* bot) + { + static const std::array weaponEntries = + { + NPC_STAFF_OF_DISINTEGRATION, + NPC_COSMIC_INFUSER, + NPC_INFINITY_BLADES, + NPC_WARP_SLICER, + NPC_PHASESHIFT_BULWARK, + NPC_NETHERSTRAND_LONGBOW, + NPC_DEVASTATION + }; + + constexpr float searchRadius = 100.0f; + + for (uint32 entry : weaponEntries) + { + Creature* weapon = bot->FindNearestCreature(entry, searchRadius, false); + + if (weapon && !weapon->IsAlive()) + return true; + } + + return false; + } + + bool HasEquippableItemForSlot(Player* bot, uint8 slot) + { + for (uint8 i = 0; i < 5; ++i) + { + uint8 bag = (i == 0) ? INVENTORY_SLOT_BAG_0 : (INVENTORY_SLOT_BAG_START + i - 1); + uint8 startSlot = (bag == INVENTORY_SLOT_BAG_0) ? INVENTORY_SLOT_ITEM_START : 0; + uint8 endSlot = (bag == INVENTORY_SLOT_BAG_0) ? INVENTORY_SLOT_ITEM_END : + (bot->GetBagByPos(bag) ? bot->GetBagByPos(bag)->GetBagSize() : 0); + + for (uint8 bagSlot = startSlot; bagSlot < endSlot; ++bagSlot) + { + Item* item = bot->GetItemByPos(bag, bagSlot); + if (!item || !item->GetTemplate()) + continue; + + uint16 dest = 0; + if (bot->CanEquipItem(slot, dest, item, false) == EQUIP_ERR_OK) + return true; + } + } + + return false; + } +} diff --git a/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.h b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.h new file mode 100644 index 00000000..e99781f1 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.h @@ -0,0 +1,158 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPHELPERS_H_ +#define _PLAYERBOT_RAIDTEMPESTKEEPHELPERS_H_ + +#include +#include +#include + +#include "AiObject.h" +#include "Position.h" +#include "Unit.h" + +namespace TempestKeepHelpers +{ + enum TempestKeepSpells + { + // Trash + SPELL_ARCANE_FLURRY = 37268, + + // Al'ar + SPELL_REBIRTH_PHASE2 = 34342, + SPELL_REBIRTH_DIVE = 35369, + SPELL_MELT_ARMOR = 35410, + + // Void Reaver + SPELL_ARCANE_ORB = 34172, + + // High Astromancer Solarian + SPELL_SOLARIAN_TRANSFORM = 39117, + SPELL_WRATH_OF_THE_ASTROMANCER = 42783, + + // Kael'thas Sunstrider + SPELL_PERMANENT_FEIGN_DEATH = 29266, + SPELL_GRAVITY_LAPSE = 39432, + SPELL_KAEL_FULL_POWER = 36187, + SPELL_MENTAL_PROTECTION_FIELD = 36480, // Staff of Disintegration + SPELL_ARCANE_BARRIER = 36481, // Phaseshift Bulwark + SPELL_KAELTHAS_MIND_CONTROL = 36797, + SPELL_SHOCK_BARRIER = 36815, + SPELL_STAFF_FROSTBOLT = 36990, + + // Hunter + SPELL_MISDIRECTION = 35079, + + // Priest + SPELL_FEAR_WARD = 6346, + }; + + enum TempestKeepNPCs + { + // Al'ar + NPC_EMBER_OF_ALAR = 19551, + NPC_FLAME_PATCH = 20602, + + // High Astromancer Solarian + NPC_SOLARIUM_PRIEST = 18806, + + // Kael'thas Sunstrider + NPC_KAELTHAS_SUNSTRIDER = 19622, + NPC_NETHERSTRAND_LONGBOW = 21268, + NPC_DEVASTATION = 21269, + NPC_COSMIC_INFUSER = 21270, + NPC_INFINITY_BLADES = 21271, // Item is singular, but NPC is plural + NPC_WARP_SLICER = 21272, + NPC_PHASESHIFT_BULWARK = 21273, + NPC_STAFF_OF_DISINTEGRATION = 21274, + // NPC_NETHER_VAPOR = 21002, // Unimplemented in AC; method needed if fixed + NPC_PHOENIX = 21362, + NPC_PHOENIX_EGG = 21364, + NPC_FLAME_STRIKE_TRIGGER = 21369, + }; + + enum TempestKeepItems + { + // Kael'thas Sunstrider + ITEM_WARP_SLICER = 30311, + ITEM_INFINITY_BLADE = 30312, + ITEM_STAFF_OF_DISINTEGRATION = 30313, + ITEM_PHASESHIFT_BULWARK = 30314, + ITEM_DEVASTATION = 30316, + ITEM_COSMIC_INFUSER = 30317, + ITEM_NETHERSTRAND_LONGBOW = 30318, + ITEM_NETHER_SPIKES = 30319, + }; + + // General + constexpr uint32 TEMPEST_KEEP_MAP_ID = 550; + Unit* GetNearestNonTankPlayerInRadius(PlayerbotAI* botAI, Player* bot, float radius); + std::vector GetAllHazardTriggers(Player* bot, uint32 npcEntry, float searchRadius); + Position FindSafestNearbyPosition(Player* bot, const std::vector& hazards, + float hazardRadius, const Position* center = nullptr); + bool IsPathSafeFromHazards( + const Position& start, const Position& end, const std::vector& hazards, + float hazardRadius); + + // Al'ar + enum AlarLocationIndex + { + PLATFORM_0_IDX, + PLATFORM_1_IDX, + PLATFORM_2_IDX, + PLATFORM_3_IDX, + POINT_QUILL_OR_DIVE_IDX, + POINT_MIDDLE_IDX, + LOCATION_NONE = -1 + }; + constexpr float ALAR_BALCONY_Z = 17.0f; + extern const Position ALAR_PLATFORM_0; + extern const Position ALAR_PLATFORM_1; + extern const Position ALAR_PLATFORM_2; + extern const Position ALAR_PLATFORM_3; + extern const std::array PLATFORM_POSITIONS; + extern const Position ALAR_GROUND_0; + extern const Position ALAR_GROUND_1; + extern const Position ALAR_GROUND_2; + extern const Position ALAR_GROUND_3; + extern const std::array GROUND_POSITIONS; + extern const Position ALAR_ROOM_CENTER; + extern const Position ALAR_POINT_QUILL_OR_DIVE; + extern const Position ALAR_POINT_MIDDLE; + extern const Position ALAR_SE_RAMP_BASE; + extern const Position ALAR_SW_RAMP_BASE; + extern const Position ALAR_ROOM_S_CENTER; + constexpr uint8 TOTAL_ALAR_LOCATIONS = 6; + extern std::unordered_map lastRebirthState; + extern std::unordered_map isAlarInPhase2; + int8 GetAlarDestinationLocationIndex(Unit* alar, Position& dest); + int8 GetAlarCurrentLocationIndex(Unit* alar); + void GetClosestPlatformAndGround( + const Position& botPos, int8& closestPlatform, Position& ground); + std::pair GetFirstTwoEmbersOfAlar(PlayerbotAI* botAI); + Player* GetSecondEmberTank(PlayerbotAI* botAI); + + // Void Reaver + extern const Position VOID_REAVER_TANK_POSITION; + extern std::unordered_map hasReachedVoidReaverPosition; + struct ArcaneOrbData + { + Position destination; + uint32 castTime; + }; + extern std::unordered_map> voidReaverArcaneOrbs; + + // Kael'thas Sunstrider + extern const Position SANGUINAR_TANK_POSITION; + extern const Position SANGUINAR_WAITING_POSITION; + extern const Position TELONICUS_TANK_POSITION; + extern const Position TELONICUS_WAITING_POSITION; + extern const Position CAPERNIAN_WAITING_POSITION; + extern const Position ADVISOR_HEAL_POSITION; + extern const Position KAELTHAS_TANK_POSITION; + extern std::unordered_map advisorDpsWaitTimer; + Player* GetCapernianTank(Player* bot); + bool IsDebuffHunter(Player* bot); + bool IsAnyLegendaryWeaponDead(Player* bot); + bool HasEquippableItemForSlot(Player* bot, uint8 slot); +} + +#endif diff --git a/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepKaelthasBossAI.h b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepKaelthasBossAI.h new file mode 100644 index 00000000..598c8061 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepKaelthasBossAI.h @@ -0,0 +1,54 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPKAELTHASBOSSAI_H_ +#define _PLAYERBOT_RAIDTEMPESTKEEPKAELTHASBOSSAI_H_ + +#include "ScriptedCreature.h" + +enum KTYells +{ +}; + +enum KTPhases +{ + PHASE_NONE = 0, + PHASE_SINGLE_ADVISOR = 1, + PHASE_WEAPONS = 2, + PHASE_TRANSITION = 3, + PHASE_ALL_ADVISORS = 4, + PHASE_FINAL = 5 +}; + +enum KTActions +{ +}; + +struct boss_kaelthas : public BossAI +{ + boss_kaelthas(Creature* creature); + + void PrepareAdvisors(); + void SetRoomState(GOState state); + void Reset() override; + void AttackStart(Unit* who) override; + void MoveInLineOfSight(Unit* who) override; + void KilledUnit(Unit* victim) override; + void JustSummoned(Creature* summon) override; + void SpellHit(Unit* caster, SpellInfo const* spell) override; + void MovementInform(uint32 type, uint32 point) override; + void ExecuteMiddleEvent(); + void IntroduceNewAdvisor(KTYells talkIntroduction, KTActions kaelAction); + void PhaseEnchantedWeaponsExecute(); + void PhaseAllAdvisorsExecute(); + void PhaseKaelExecute(); + void UpdateAI(uint32 diff) override; + bool CheckEvadeIfOutOfCombatArea() const override; + void JustDied(Unit* killer) override; + + uint32 GetPhase() const { return _phase; } // This is the only addition to the existing class + +private: + uint32 _phase; + uint8 _advisorsAlive; + bool _transitionSceneReached = false; +}; + +#endif diff --git a/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepScripts.cpp b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepScripts.cpp new file mode 100644 index 00000000..ff26d150 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepScripts.cpp @@ -0,0 +1,47 @@ +#include "RaidTempestKeepHelpers.h" +#include "ObjectAccessor.h" +#include "Player.h" +#include "ScriptMgr.h" +#include "Spell.h" +#include "Timer.h" + +using namespace TempestKeepHelpers; + +class BossListenerScript : public AllSpellScript +{ +public: + BossListenerScript() : AllSpellScript("BossListenerScript") { } + + void OnSpellCast(Spell* spell, Unit* caster, SpellInfo const* spellInfo, bool /*skipCheck*/) override + { + if (spellInfo->Id != SPELL_ARCANE_ORB) + return; + + std::list const& targets = *spell->GetUniqueTargetInfo(); + if (targets.empty()) + return; + + Player* target = ObjectAccessor::GetPlayer(*caster, targets.front().targetGUID); + if (!target) + return; + + auto& orbs = voidReaverArcaneOrbs[caster->GetMap()->GetInstanceId()]; + uint32 currentTime = getMSTime(); + + ArcaneOrbData orbData; + orbData.destination = target->GetPosition(); + orbData.castTime = currentTime; + + orbs.push_back(orbData); + + orbs.erase(std::remove_if(orbs.begin(), orbs.end(), + [currentTime](const ArcaneOrbData& orb) { + return getMSTimeDiff(orb.castTime, currentTime) > 5000; + }), orbs.end()); + } +}; + +void AddSC_TempestKeepBotScripts() +{ + new BossListenerScript(); +} diff --git a/src/Bot/Engine/AiObjectContext.cpp b/src/Bot/Engine/AiObjectContext.cpp index 3a2037a0..ff54b971 100644 --- a/src/Bot/Engine/AiObjectContext.cpp +++ b/src/Bot/Engine/AiObjectContext.cpp @@ -37,12 +37,14 @@ #include "Ai/Raid/BlackwingLair/RaidBwlTriggerContext.h" #include "Ai/Raid/Karazhan/RaidKarazhanActionContext.h" #include "Ai/Raid/Karazhan/RaidKarazhanTriggerContext.h" -#include "Ai/Raid/Magtheridon/RaidMagtheridonActionContext.h" -#include "Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h" #include "Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h" #include "Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h" +#include "Ai/Raid/Magtheridon/RaidMagtheridonActionContext.h" +#include "Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h" #include "Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h" #include "Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h" +#include "Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h" +#include "Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h" #include "Ai/Raid/EyeOfEternity/RaidEoEActionContext.h" #include "Ai/Raid/EyeOfEternity/RaidEoETriggerContext.h" #include "Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h" @@ -115,9 +117,10 @@ void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextListIsAlive()) + return false; + Group* group = player->GetGroup(); if (!group) return false; - int counter = 0; + uint8 totalAssistants = 0; + uint8 assistantsBeforePlayer = 0; + uint8 nonAssistantsBeforePlayer = 0; + bool playerFound = false; - // First, assistants for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member) + if (!member || (ignoreDeadPlayers && !member->IsAlive()) || !IsHeal(member)) continue; - if (ignoreDeadPlayers && !member->IsAlive()) - continue; + bool isAssistant = group->IsAssistant(member->GetGUID()); - if (group->IsAssistant(member->GetGUID()) && IsHeal(member)) + if (isAssistant) + totalAssistants++; + + if (member == player) + playerFound = true; + else if (!playerFound) { - if (index == counter) - return player == member; - counter++; + if (isAssistant) + assistantsBeforePlayer++; + else + nonAssistantsBeforePlayer++; } } - // If not enough assistants, get non-assistants - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member) - continue; + if (!playerFound) + return false; - if (ignoreDeadPlayers && !member->IsAlive()) - continue; + // If the player is an assistant, their index is just the number of assistants before them. + // If they are a non-assistant, their index is shifted by the total number of assistants. + uint8 playerIndex = group->IsAssistant(player->GetGUID()) + ? assistantsBeforePlayer : (totalAssistants + nonAssistantsBeforePlayer); - if (!group->IsAssistant(member->GetGUID()) && IsHeal(member)) - { - if (index == counter) - return player == member; - counter++; - } - } - - return false; + return playerIndex == index; } -bool PlayerbotAI::IsAssistRangedDpsOfIndex(Player* player, int index, bool ignoreDeadPlayers) +bool PlayerbotAI::IsAssistRangedDpsOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers) { + if (!IsRangedDps(player)) + return false; + + if (ignoreDeadPlayers && !player->IsAlive()) + return false; + Group* group = player->GetGroup(); if (!group) return false; - int counter = 0; + uint8 totalAssistants = 0; + uint8 assistantsBeforePlayer = 0; + uint8 nonAssistantsBeforePlayer = 0; + bool playerFound = false; - // First, assistants for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member) + if (!member || (ignoreDeadPlayers && !member->IsAlive()) || !IsRangedDps(member)) continue; - if (ignoreDeadPlayers && !member->IsAlive()) - continue; + bool isAssistant = group->IsAssistant(member->GetGUID()); - if (group->IsAssistant(member->GetGUID()) && IsRangedDps(member)) + if (isAssistant) + totalAssistants++; + + if (member == player) + playerFound = true; + else if (!playerFound) { - if (index == counter) - return player == member; - counter++; + if (isAssistant) + assistantsBeforePlayer++; + else + nonAssistantsBeforePlayer++; } } - // If not enough assistants, get non-assistants - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member) - continue; + if (!playerFound) + return false; - if (ignoreDeadPlayers && !member->IsAlive()) - continue; + // If the player is an assistant, their index is just the number of assistants before them. + // If they are a non-assistant, their index is shifted by the total number of assistants. + uint8 playerIndex = group->IsAssistant(player->GetGUID()) + ? assistantsBeforePlayer : (totalAssistants + nonAssistantsBeforePlayer); - if (!group->IsAssistant(member->GetGUID()) && IsRangedDps(member)) - { - if (index == counter) - return player == member; - counter++; - } - } - - return false; + return playerIndex == index; } bool PlayerbotAI::HasAggro(Unit* unit) @@ -2226,43 +2235,44 @@ bool PlayerbotAI::IsDps(Player* player, bool bySpec) return false; } -bool PlayerbotAI::IsMainTank(Player* player) +bool PlayerbotAI::IsMainTank(Player* player, bool ignoreMemberFlag) { Group* group = player->GetGroup(); if (!group) - { return IsTank(player); - } ObjectGuid mainTank = ObjectGuid(); - Group::MemberSlotList const& slots = group->GetMemberSlots(); - for (Group::member_citerator itr = slots.begin(); itr != slots.end(); ++itr) + // (1) Check for main tank flag (any class or spec) + if (!ignoreMemberFlag) { - if (itr->flags & MEMBER_FLAG_MAINTANK) + Group::MemberSlotList const& slots = group->GetMemberSlots(); + + for (Group::member_citerator itr = slots.begin(); itr != slots.end(); ++itr) { - mainTank = itr->guid; - break; + if (itr->flags & MEMBER_FLAG_MAINTANK) + { + mainTank = itr->guid; + break; + } } + + if (mainTank != ObjectGuid::Empty) + return player->GetGUID() == mainTank; } - if (mainTank != ObjectGuid::Empty) - { - return player->GetGUID() == mainTank; - } + // (2) If no main tank flag, return the first tank + if (!IsTank(player) || !player->IsAlive()) + return false; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member) - { continue; - } if (IsTank(member) && member->IsAlive()) - { return player->GetGUID() == member->GetGUID(); - } } return false; @@ -2281,47 +2291,31 @@ bool PlayerbotAI::IsBotMainTank(Player* player) return false; if (IsMainTank(player)) - { return true; - } Group* group = player->GetGroup(); if (!group) - { - return true; // If no group, consider the bot as main tank - } + return true; int32 botAssistTankIndex = GetAssistTankIndex(player); if (botAssistTankIndex == -1) - { return false; - } for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) { Player* member = gref->GetSource(); if (!member) - { continue; - } int32 memberAssistTankIndex = GetAssistTankIndex(member); if (memberAssistTankIndex == -1) - { continue; - } if (memberAssistTankIndex == botAssistTankIndex && player == member) - { return true; - } if (memberAssistTankIndex < botAssistTankIndex && member->GetSession()->IsBot()) - { return false; - } - - return false; } return false; @@ -2331,73 +2325,76 @@ uint32 PlayerbotAI::GetGroupTankNum(Player* player) { Group* group = player->GetGroup(); if (!group) - { return 0; - } + uint32 result = 0; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member) - { continue; - } if (IsTank(member) && member->IsAlive()) - { result++; - } } + return result; } -bool PlayerbotAI::IsAssistTank(Player* player) { return IsTank(player) && !IsMainTank(player); } - -bool PlayerbotAI::IsAssistTankOfIndex(Player* player, int index, bool ignoreDeadPlayers) +bool PlayerbotAI::IsAssistTank(Player* player) { + return IsTank(player) && !IsMainTank(player); +} + +bool PlayerbotAI::IsAssistTankOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers) +{ + if (!IsAssistTank(player)) + return false; + + if (ignoreDeadPlayers && !player->IsAlive()) + return false; + Group* group = player->GetGroup(); if (!group) return false; - int counter = 0; + uint8 totalAssistants = 0; + uint8 assistantsBeforePlayer = 0; + uint8 nonAssistantsBeforePlayer = 0; + bool playerFound = false; - // First, assistants for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member) + if (!member || (ignoreDeadPlayers && !member->IsAlive()) || !IsAssistTank(member)) continue; - if (ignoreDeadPlayers && !member->IsAlive()) - continue; + bool isAssistant = group->IsAssistant(member->GetGUID()); - if (group->IsAssistant(member->GetGUID()) && IsAssistTank(member)) + if (isAssistant) + totalAssistants++; + + if (member == player) + playerFound = true; + else if (!playerFound) { - if (index == counter) - return player == member; - counter++; + if (isAssistant) + assistantsBeforePlayer++; + else + nonAssistantsBeforePlayer++; } } - // If not enough assistants, get non-assistants - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member) - continue; + if (!playerFound) + return false; - if (ignoreDeadPlayers && !member->IsAlive()) - continue; + // If the player is an assistant, their index is just the number of assistants before them. + // If they are a non-assistant, their index is shifted by the total number of assistants. + uint8 playerIndex = group->IsAssistant(player->GetGUID()) + ? assistantsBeforePlayer : (totalAssistants + nonAssistantsBeforePlayer); - if (!group->IsAssistant(member->GetGUID()) && IsAssistTank(member)) - { - if (index == counter) - return player == member; - counter++; - } - } - return false; + return playerIndex == index; } namespace acore diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index 05710548..b3a51b15 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -423,12 +423,12 @@ public: static bool IsRangedDps(Player* player, bool bySpec = false); static bool IsCombo(Player* player); static bool IsBotMainTank(Player* player); - static bool IsMainTank(Player* player); + static bool IsMainTank(Player* player, bool ignoreMemberFlag = false); static uint32 GetGroupTankNum(Player* player); static bool IsAssistTank(Player* player); - static bool IsAssistTankOfIndex(Player* player, int index, bool ignoreDeadPlayers = false); - static bool IsAssistHealOfIndex(Player* player, int index, bool ignoreDeadPlayers = false); - static bool IsAssistRangedDpsOfIndex(Player* player, int index, bool ignoreDeadPlayers = false); + static bool IsAssistTankOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers = false); + static bool IsAssistHealOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers = false); + static bool IsAssistRangedDpsOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers = false); bool HasAggro(Unit* unit); static int32 GetAssistTankIndex(Player* player); int32 GetGroupSlotIndex(Player* player); diff --git a/src/Script/Playerbots.cpp b/src/Script/Playerbots.cpp index 94b8eb1b..ab6d64f9 100644 --- a/src/Script/Playerbots.cpp +++ b/src/Script/Playerbots.cpp @@ -520,6 +520,8 @@ public: void AddPlayerbotsSecureLoginScripts(); +void AddSC_TempestKeepBotScripts(); + void AddPlayerbotsScripts() { new PlayerbotsDatabaseScript(); @@ -532,4 +534,5 @@ void AddPlayerbotsScripts() AddPlayerbotsSecureLoginScripts(); AddPlayerbotsCommandscripts(); PlayerBotsGuildValidationScript(); + AddSC_TempestKeepBotScripts(); }