From bebac60c51cb764f65cdf29878a77470accc223b Mon Sep 17 00:00:00 2001 From: kadeshar Date: Fri, 6 Feb 2026 20:55:28 +0100 Subject: [PATCH 01/11] test-staging alignment (#2121) # Pull Request Describe what this change does and why it is needed... --- ## Design Philosophy We prioritize **stability, performance, and predictability** over behavioral realism. Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and long-term robustness. Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all participants. Because every action and decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and negatively affect both players and world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a project goal. Increased behavioral realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and maintained continuously as the system evolves. If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision model**. More complex behavior should only be available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. Principles: - **Stability before intelligence** A stable system is always preferred over a smarter one. - **Performance is a shared resource** Any increase in bot cost affects all players and all bots. - **Simple logic scales better than smart logic** Predictable behavior under load is more valuable than perfect decisions. - **Complexity must justify itself** If a feature cannot clearly explain its cost, it should not exist. - **Defaults must be cheap** Expensive behavior must always be optional and clearly communicated. - **Bots should look reasonable, not perfect** The goal is believable behavior, not human simulation. Before submitting, confirm that this change aligns with those principles. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? - Describe the **cheapest implementation** that produces an acceptable result? - Describe the **runtime cost** when this logic executes across many bots? --- ## How to Test the Changes - Step-by-step instructions to test the change - Any required setup (e.g. multiple players, bots, specific configuration) - Expected behavior and how to verify it ## Complexity & Impact Does this change add new decision branches? - - [ ] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [ ] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [ ] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [ ] No - - [ ] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - - [ ] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [ ] No - - [ ] Yes (**explain below**) If yes, please specify: - AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) - Which parts of the change were influenced or generated - Whether the result was manually reviewed and adapted AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest about what they do and do not understand. --- ## Final Checklist - - [ ] Stability is not compromised - - [ ] Performance impact is understood, tested, and acceptable - - [ ] Added logic complexity is justified and explained - - [ ] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --------- Co-authored-by: Crow --- PULL_REQUEST_TEMPLATE.md | 44 +++++++++++++--------------------- conf/playerbots.conf.dist | 2 +- src/Bot/RandomPlayerbotMgr.cpp | 2 +- 3 files changed, 18 insertions(+), 30 deletions(-) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index a23a7e67f..d26b1b1cd 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -67,46 +67,34 @@ Please answer the following: ## Complexity & Impact Does this change add new decision branches? -``` -[ ] No -[ ] Yes (**explain below**) -``` +- - [ ] No +- - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? -``` -[ ] No -[ ] Yes (**describe and justify impact**) -``` +- - [ ] No +- - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? -``` -[ ] No -[ ] Yes (**explain why**) -``` +- - [ ] No +- - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? -``` -[ ] No -[ ] Yes (**explain why**) -``` +- - [ ] No +- - [ ] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: -``` -[ ] Lightweight mode remains the default -[ ] More complex behavior is optional and thereby configurable -``` +- - [ ] Lightweight mode remains the default +- - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? -``` -[ ] No -[ ] Yes (**explain below**) -``` +- - [ ] No +- - [ ] Yes (**explain below**) If yes, please specify: @@ -123,10 +111,10 @@ about what they do and do not understand. ## Final Checklist -- [ ] Stability is not compromised -- [ ] Performance impact is understood, tested, and acceptable -- [ ] Added logic complexity is justified and explained -- [ ] Documentation updated if needed +- - [ ] Stability is not compromised +- - [ ] Performance impact is understood, tested, and acceptable +- - [ ] Added logic complexity is justified and explained +- - [ ] Documentation updated if needed --- diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 22615b407..907377316 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -990,7 +990,7 @@ AiPlayerbot.ZoneBracket.3433 = 10,22 AiPlayerbot.ZoneBracket.3525 = 10,21 # Classic WoW - High-level zones: -# Deadwind Pass (Zone ID: 10 Default Min,Max: 19,33) +# Duskwood (Zone ID: 10 Default Min,Max: 19,33) # Wetlands (Zone ID: 11 Default Min,Max: 21,30) # Redridge Mountains (Zone ID: 44 Default Min,Max: 16,28) # Hillsbrad Foothills (Zone ID: 267 Default Min,Max: 20,34) diff --git a/src/Bot/RandomPlayerbotMgr.cpp b/src/Bot/RandomPlayerbotMgr.cpp index 4707d854f..892368c66 100644 --- a/src/Bot/RandomPlayerbotMgr.cpp +++ b/src/Bot/RandomPlayerbotMgr.cpp @@ -1784,7 +1784,7 @@ void RandomPlayerbotMgr::PrepareZone2LevelBracket() zone2LevelBracket[3525] = {10, 21}; // Bloodmyst Isle // Classic WoW - High - level zones - zone2LevelBracket[10] = {19, 33}; // Deadwind Pass + zone2LevelBracket[10] = {19, 33}; // Duskwood zone2LevelBracket[11] = {21, 30}; // Wetlands zone2LevelBracket[44] = {16, 28}; // Redridge Mountains zone2LevelBracket[267] = {20, 34}; // Hillsbrad Foothills From b31bda85eedc70cd0c7e2618fda3916b1ca17f0c Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 6 Feb 2026 13:55:43 -0600 Subject: [PATCH 02/11] Refactor raid strategy framework (#2069) # Pull Request The purposes of this PR are to (1) establish a general raid helper framework for the benefit of future raid strategies and (2) make some improvements to problematic areas of the raid strategy code. List of changes: 1. Added new RaidBossHelpers.cpp and RaidBossHelpers.h files in the Raid folder. 3. Moved reused helpers from Karazhan, Gruul, and Magtheridon strategies to the new helper files. 4. Modified the prior function that assigned a DPS bot to store and erase timers and trackers in associative containers--the function now includes parameters for mapId (so a bot that is not in the instance will not be assigned) and for the ability to exclude a bot (useful for excluding particular important roles, such as a Warlock tank, so they are not bogged down by these extra tasks at critical moments). I also renamed it from IsInstanceTimerManager to IsMechanicTrackerBot. 5. Moved all helper files in raid strategies to Util folders (was needed for ICC, MC, and Ulduar). 6. Renamed and reordered includes of Ulduar files in AiObjectContext.cpp to match other raid strategies. a. This initially caused compile errors which made me realize that the existing code had several problems with missing includes and was compiling only due to the prior ordering in AiObjectContext.cpp. Therefore, I added the missing includes to Molten Core, Ulduar, and Vault of Archavon strategies. b. Ulduar and Old Kingdom were also using the same constant name for a spell--the reordering caused a compile error here as well, which just highlighted an existing problem that was being hidden. I renamed the constant for Ulduar to fix this, but I think the better approach going forward would be to use a namespace or enum class. But that is for another time and probably another person. 7. Several changes with respect to Ulduar files: a. The position constants and enums for spells and NPCs and such were in the trigger header file. I did not think that made sense so moved them to existing helper files. b. Since the strategy does not use multipliers, I removed all files and references to multipliers in it. c. I removed some unneeded includes. I did not do a detailed review to determine what else could be removed--I just took some out that I could tell right away were not needed. d. I renamed the ingame strategy name from "uld" to "ulduar," which I think is clearer and is still plenty short. 8. Partial refactor of Gruul and Magtheridon strategies: a. I did not due a full refactoring but made some quick changes to things I did previously that were rather stupid like repeating calculations, having useless logic like pointless IsAlive() checks for creatures already on the hostile references list, and not using the existing Position class for coordinates. b. There were a few substantive changes, such as allowing players to pick Maulgar mage and moonkin tanks with the assistant flag, but a greater refactoring of the strategies themselves is beyond this PR. c. I was clearing some containers used for Gruul and Magtheridon strategies; the methods are now fixed to erase only the applicable keys so that in the unlikely event that one server has multiple groups running Gruul or Magtheridon at the same time, there won't be timer or position tracker conflicts. ## How to Test the Changes 1. Enter any raid instance that has any code impacted by this PR 2. Engage bosses and observe if any strategies are now broken I personally tested Maulgar, Gruul, and Magtheridon and confirmed that they still work as intended. ## Complexity & Impact I do not expect this PR to have any relevant changes to in-game performance, but I will defer to those more knowledgeable than I if there are concerns in this area. As I've mentioned before, you can consider me to be like a person who has taken half an intro C++ course at best. ## AI Assistance None beyond autocomplete of repetitive changes. --------- Co-authored-by: bashermens <31279994+hermensbas@users.noreply.github.com> --- .../Action/RaidGruulsLairActions.cpp | 218 +++++------ .../GruulsLair/Action/RaidGruulsLairActions.h | 4 +- .../Multiplier/RaidGruulsLairMultipliers.cpp | 25 +- .../GruulsLair/RaidGruulsLairActionContext.h | 4 +- .../GruulsLair/RaidGruulsLairTriggerContext.h | 8 +- .../Strategy/RaidGruulsLairStrategy.cpp | 6 +- .../Trigger/RaidGruulsLairTriggers.cpp | 45 ++- .../Trigger/RaidGruulsLairTriggers.h | 8 +- .../GruulsLair/Util/RaidGruulsLairHelpers.cpp | 145 +++----- .../GruulsLair/Util/RaidGruulsLairHelpers.h | 39 +- .../Raid/Icecrown/{ => Util}/RaidIccScripts.h | 0 .../Karazhan/Action/RaidKarazhanActions.cpp | 27 +- .../Multiplier/RaidKarazhanMultipliers.cpp | 4 + .../Karazhan/Trigger/RaidKarazhanTriggers.cpp | 13 +- .../Karazhan/Util/RaidKarazhanHelpers.cpp | 108 ------ .../Raid/Karazhan/Util/RaidKarazhanHelpers.h | 19 +- .../Action/RaidMagtheridonActions.cpp | 160 ++++---- .../Action/RaidMagtheridonActions.h | 6 +- .../Multiplier/RaidMagtheridonMultipliers.cpp | 20 +- .../Trigger/RaidMagtheridonTriggers.cpp | 22 +- .../Util/RaidMagtheridonHelpers.cpp | 91 +---- .../Magtheridon/Util/RaidMagtheridonHelpers.h | 35 +- src/Ai/Raid/MoltenCore/RaidMcActionContext.h | 1 + src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h | 1 + .../MoltenCore/{ => Util}/RaidMcHelpers.h | 0 src/Ai/Raid/RaidBossHelpers.cpp | 142 ++++++++ src/Ai/Raid/RaidBossHelpers.h | 21 ++ src/Ai/Raid/RaidStrategyContext.h | 4 +- .../Raid/Ulduar/Action/RaidUlduarActions.cpp | 3 - .../Multiplier/RaidUlduarMultipliers.cpp | 28 -- .../Ulduar/Multiplier/RaidUlduarMultipliers.h | 17 - src/Ai/Raid/Ulduar/RaidUlduarBossHelper.h | 169 --------- .../Ulduar/Strategy/RaidUlduarStrategy.cpp | 7 - .../Raid/Ulduar/Strategy/RaidUlduarStrategy.h | 4 +- .../Ulduar/Trigger/RaidUlduarTriggers.cpp | 2 +- .../Raid/Ulduar/Trigger/RaidUlduarTriggers.h | 178 --------- .../{ => Util}/RaidUlduarBossHelper.cpp | 39 +- .../Raid/Ulduar/Util/RaidUlduarBossHelper.h | 341 ++++++++++++++++++ .../Ulduar/{ => Util}/RaidUlduarScripts.h | 0 .../VaultOfArchavon/RaidVoAActionContext.h | 1 + .../VaultOfArchavon/RaidVoATriggerContext.h | 1 + src/Bot/Engine/AiObjectContext.cpp | 4 +- src/Bot/PlayerbotAI.cpp | 2 +- 43 files changed, 891 insertions(+), 1081 deletions(-) rename src/Ai/Raid/Icecrown/{ => Util}/RaidIccScripts.h (100%) rename src/Ai/Raid/MoltenCore/{ => Util}/RaidMcHelpers.h (100%) create mode 100644 src/Ai/Raid/RaidBossHelpers.cpp create mode 100644 src/Ai/Raid/RaidBossHelpers.h delete mode 100644 src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.cpp delete mode 100644 src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.h delete mode 100644 src/Ai/Raid/Ulduar/RaidUlduarBossHelper.h rename src/Ai/Raid/Ulduar/{ => Util}/RaidUlduarBossHelper.cpp (63%) create mode 100644 src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.h rename src/Ai/Raid/Ulduar/{ => Util}/RaidUlduarScripts.h (100%) diff --git a/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.cpp b/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.cpp index 1a98135ca..2af82ebe9 100644 --- a/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.cpp +++ b/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.cpp @@ -2,6 +2,7 @@ #include "RaidGruulsLairHelpers.h" #include "CreatureAI.h" #include "Playerbots.h" +#include "RaidBossHelpers.h" #include "Unit.h" using namespace GruulsLairHelpers; @@ -12,6 +13,8 @@ using namespace GruulsLairHelpers; bool HighKingMaulgarMainTankAttackMaulgarAction::Execute(Event event) { Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); + if (!maulgar) + return false; MarkTargetWithSquare(bot, maulgar); SetRtiTarget(botAI, "square", maulgar); @@ -21,31 +24,20 @@ bool HighKingMaulgarMainTankAttackMaulgarAction::Execute(Event event) if (maulgar->GetVictim() == bot) { - const Location& tankPosition = GruulsLairLocations::MaulgarTankPosition; + const Position& position = MAULGAR_TANK_POSITION; const float maxDistance = 3.0f; - float distanceToTankPosition = bot->GetExactDist2d(tankPosition.x, tankPosition.y); + float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - if (distanceToTankPosition > maxDistance) + if (distanceToPosition > maxDistance) { - float dX = tankPosition.x - bot->GetPositionX(); - float dY = tankPosition.y - bot->GetPositionY(); - float dist = sqrt(dX * dX + dY * dY); - float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; - float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; - return MoveTo(bot->GetMapId(), moveX, moveY, tankPosition.z, false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveX = bot->GetPositionX() + (dX / distanceToPosition) * maxDistance; + float moveY = bot->GetPositionY() + (dY / distanceToPosition) * maxDistance; + return MoveTo(GRUULS_LAIR_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, true); } - - float orientation = atan2(maulgar->GetPositionY() - bot->GetPositionY(), - maulgar->GetPositionX() - bot->GetPositionX()); - bot->SetFacingTo(orientation); - } - else if (!bot->IsWithinMeleeRange(maulgar)) - { - return MoveTo(maulgar->GetMapId(), maulgar->GetPositionX(), maulgar->GetPositionY(), - maulgar->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); } return false; @@ -55,6 +47,8 @@ bool HighKingMaulgarMainTankAttackMaulgarAction::Execute(Event event) bool HighKingMaulgarFirstAssistTankAttackOlmAction::Execute(Event event) { Unit* olm = AI_VALUE2(Unit*, "find target", "olm the summoner"); + if (!olm) + return false; MarkTargetWithCircle(bot, olm); SetRtiTarget(botAI, "circle", olm); @@ -64,29 +58,22 @@ bool HighKingMaulgarFirstAssistTankAttackOlmAction::Execute(Event event) if (olm->GetVictim() == bot) { - const Location& tankPosition = GruulsLairLocations::OlmTankPosition; + const Position& position = OLM_TANK_POSITION; const float maxDistance = 3.0f; const float olmTankLeeway = 30.0f; - float distanceOlmToTankPosition = olm->GetExactDist2d(tankPosition.x, tankPosition.y); + float distanceOlmToPosition = olm->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - if (distanceOlmToTankPosition > olmTankLeeway) + if (distanceOlmToPosition > olmTankLeeway) { - float dX = tankPosition.x - bot->GetPositionX(); - float dY = tankPosition.y - bot->GetPositionY(); - float dist = sqrt(dX * dX + dY * dY); - float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; - float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; - return MoveTo(bot->GetMapId(), moveX, moveY, tankPosition.z, false, false, false, false, + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveX = bot->GetPositionX() + (dX / distanceOlmToPosition) * maxDistance; + float moveY = bot->GetPositionY() + (dY / distanceOlmToPosition) * maxDistance; + return MoveTo(GRUULS_LAIR_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } - else if (!bot->IsWithinMeleeRange(olm)) - { - return MoveTo(olm->GetMapId(), olm->GetPositionX(), olm->GetPositionY(), - olm->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); - } return false; } @@ -95,6 +82,8 @@ bool HighKingMaulgarFirstAssistTankAttackOlmAction::Execute(Event event) bool HighKingMaulgarSecondAssistTankAttackBlindeyeAction::Execute(Event event) { Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); + if (!blindeye) + return false; MarkTargetWithStar(bot, blindeye); SetRtiTarget(botAI, "star", blindeye); @@ -104,31 +93,20 @@ bool HighKingMaulgarSecondAssistTankAttackBlindeyeAction::Execute(Event event) if (blindeye->GetVictim() == bot) { - const Location& tankPosition = GruulsLairLocations::BlindeyeTankPosition; + const Position& position = BLINDEYE_TANK_POSITION; const float maxDistance = 3.0f; - float distanceToTankPosition = bot->GetExactDist2d(tankPosition.x, tankPosition.y); + float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - if (distanceToTankPosition > maxDistance) + if (distanceToPosition > maxDistance) { - float dX = tankPosition.x - bot->GetPositionX(); - float dY = tankPosition.y - bot->GetPositionY(); - float dist = sqrt(dX * dX + dY * dY); - float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; - float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; - return MoveTo(bot->GetMapId(), moveX, moveY, tankPosition.z, false, false, false, false, + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveX = bot->GetPositionX() + (dX / distanceToPosition) * maxDistance; + float moveY = bot->GetPositionY() + (dY / distanceToPosition) * maxDistance; + return MoveTo(GRUULS_LAIR_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } - - float orientation = atan2(blindeye->GetPositionY() - bot->GetPositionY(), - blindeye->GetPositionX() - bot->GetPositionX()); - bot->SetFacingTo(orientation); - } - else if (!bot->IsWithinMeleeRange(blindeye)) - { - return MoveTo(blindeye->GetMapId(), blindeye->GetPositionX(), blindeye->GetPositionY(), - blindeye->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); } return false; @@ -138,6 +116,8 @@ bool HighKingMaulgarSecondAssistTankAttackBlindeyeAction::Execute(Event event) bool HighKingMaulgarMageTankAttackKroshAction::Execute(Event event) { Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); + if (!krosh) + return false; MarkTargetWithTriangle(bot, krosh); SetRtiTarget(botAI, "triangle", krosh); @@ -149,25 +129,22 @@ bool HighKingMaulgarMageTankAttackKroshAction::Execute(Event event) return botAI->CastSpell("fire ward", bot); if (bot->GetTarget() != krosh->GetGUID()) - { - bot->SetSelection(krosh->GetGUID()); - return true; - } + return Attack(krosh); if (krosh->GetVictim() == bot) { - const Location& tankPosition = GruulsLairLocations::KroshTankPosition; - float distanceToKrosh = krosh->GetExactDist2d(tankPosition.x, tankPosition.y); + const Position& position = KROSH_TANK_POSITION; + float distanceToKrosh = krosh->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); const float minDistance = 16.0f; const float maxDistance = 29.0f; const float tankPositionLeeway = 1.0f; if (distanceToKrosh > minDistance && distanceToKrosh < maxDistance) { - if (!bot->IsWithinDist2d(tankPosition.x, tankPosition.y, tankPositionLeeway)) + if (!bot->IsWithinDist2d(position.GetPositionX(), position.GetPositionY(), tankPositionLeeway)) { - return MoveTo(bot->GetMapId(), tankPosition.x, tankPosition.y, tankPosition.z, false, - false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + return MoveTo(GRUULS_LAIR_MAP_ID, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } float orientation = atan2(krosh->GetPositionY() - bot->GetPositionY(), @@ -179,7 +156,7 @@ bool HighKingMaulgarMageTankAttackKroshAction::Execute(Event event) Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { - return MoveTo(krosh->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } @@ -192,20 +169,19 @@ bool HighKingMaulgarMageTankAttackKroshAction::Execute(Event event) bool HighKingMaulgarMoonkinTankAttackKigglerAction::Execute(Event event) { Unit* kiggler = AI_VALUE2(Unit*, "find target", "kiggler the crazed"); + if (!kiggler) + return false; MarkTargetWithDiamond(bot, kiggler); SetRtiTarget(botAI, "diamond", kiggler); if (bot->GetTarget() != kiggler->GetGUID()) - { - bot->SetSelection(kiggler->GetGUID()); - return true; - } + return Attack(kiggler); Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { - return MoveTo(kiggler->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } @@ -216,120 +192,105 @@ bool HighKingMaulgarAssignDPSPriorityAction::Execute(Event event) { // Target priority 1: Blindeye Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); - if (blindeye && blindeye->IsAlive()) + if (blindeye) { Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(blindeye->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } SetRtiTarget(botAI, "star", blindeye); if (bot->GetTarget() != blindeye->GetGUID()) - { - bot->SetSelection(blindeye->GetGUID()); return Attack(blindeye); - } return false; } // Target priority 2: Olm Unit* olm = AI_VALUE2(Unit*, "find target", "olm the summoner"); - if (olm && olm->IsAlive()) + if (olm) { Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(olm->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } SetRtiTarget(botAI, "circle", olm); if (bot->GetTarget() != olm->GetGUID()) - { - bot->SetSelection(olm->GetGUID()); return Attack(olm); - } return false; } // Target priority 3a: Krosh (ranged only) Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); - if (krosh && krosh->IsAlive() && botAI->IsRanged(bot)) + if (krosh && botAI->IsRanged(bot)) { Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(krosh->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } SetRtiTarget(botAI, "triangle", krosh); if (bot->GetTarget() != krosh->GetGUID()) - { - bot->SetSelection(krosh->GetGUID()); return Attack(krosh); - } return false; } // Target priority 3b: Kiggler Unit* kiggler = AI_VALUE2(Unit*, "find target", "kiggler the crazed"); - if (kiggler && kiggler->IsAlive()) + if (kiggler) { Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(kiggler->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } SetRtiTarget(botAI, "diamond", kiggler); if (bot->GetTarget() != kiggler->GetGUID()) - { - bot->SetSelection(kiggler->GetGUID()); return Attack(kiggler); - } return false; } // Target priority 4: Maulgar Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); - if (maulgar && maulgar->IsAlive()) + if (maulgar) { Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(maulgar->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } SetRtiTarget(botAI, "square", maulgar); if (bot->GetTarget() != maulgar->GetGUID()) - { - bot->SetSelection(maulgar->GetGUID()); return Attack(maulgar); - } } return false; @@ -338,22 +299,22 @@ bool HighKingMaulgarAssignDPSPriorityAction::Execute(Event event) // Avoid Whirlwind and Blast Wave and generally try to stay near the center of the room bool HighKingMaulgarHealerFindSafePositionAction::Execute(Event event) { - const Location& fightCenter = GruulsLairLocations::MaulgarRoomCenter; - const float maxDistanceFromFight = 50.0f; - float distToFight = bot->GetExactDist2d(fightCenter.x, fightCenter.y); + const Position& center = MAULGAR_ROOM_CENTER; + const float maxDistanceFromCenter = 50.0f; + float distToCenter = bot->GetExactDist2d(center.GetPositionX(), center.GetPositionY()); - if (distToFight > maxDistanceFromFight) + if (distToCenter > maxDistanceFromCenter) { - float angle = atan2(bot->GetPositionY() - fightCenter.y, bot->GetPositionX() - fightCenter.x); - float destX = fightCenter.x + 40.0f * cos(angle); - float destY = fightCenter.y + 40.0f * sin(angle); - float destZ = fightCenter.z; + float angle = atan2(bot->GetPositionY() - center.GetPositionY(), bot->GetPositionX() - center.GetPositionX()); + float destX = center.GetPositionX() + 40.0f * cos(angle); + float destY = center.GetPositionY() + 40.0f * sin(angle); + float destZ = center.GetPositionZ(); if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), destX, destY, destZ)) return false; - return MoveTo(bot->GetMapId(), destX, destY, destZ, false, false, false, false, + return MoveTo(GRUULS_LAIR_MAP_ID, destX, destY, destZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } @@ -362,7 +323,7 @@ bool HighKingMaulgarHealerFindSafePositionAction::Execute(Event event) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(bot->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } @@ -373,6 +334,8 @@ bool HighKingMaulgarHealerFindSafePositionAction::Execute(Event event) bool HighKingMaulgarRunAwayFromWhirlwindAction::Execute(Event event) { Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); + if (!maulgar) + return false; const float safeDistance = 10.0f; float distance = bot->GetExactDist2d(maulgar); @@ -395,7 +358,7 @@ bool HighKingMaulgarRunAwayFromWhirlwindAction::Execute(Event event) { bot->AttackStop(); bot->InterruptNonMeleeSpells(true); - return MoveTo(maulgar->GetMapId(), destX, destY, destZ, false, false, false, false, + return MoveTo(GRUULS_LAIR_MAP_ID, destX, destY, destZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } @@ -439,7 +402,7 @@ bool HighKingMaulgarBanishFelstalkerAction::Execute(Event event) if (warlockIndex >= 0 && warlockIndex < felStalkers.size()) { Unit* assignedFelStalker = felStalkers[warlockIndex]; - if (!assignedFelStalker->HasAura(SPELL_BANISH) && botAI->CanCastSpell(SPELL_BANISH, assignedFelStalker, true)) + if (!botAI->HasAura("banish", assignedFelStalker) && botAI->CanCastSpell("banish", assignedFelStalker)) return botAI->CastSpell("banish", assignedFelStalker); } @@ -528,40 +491,33 @@ bool HighKingMaulgarMisdirectOlmAndBlindeyeAction::Execute(Event event) // Gruul the Dragonkiller Actions // Position in center of the room -bool GruulTheDragonkillerMainTankPositionBossAction::Execute(Event event) +bool GruulTheDragonkillerTanksPositionBossAction::Execute(Event event) { Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); + if (!gruul) + return false; if (bot->GetVictim() != gruul) return Attack(gruul); if (gruul->GetVictim() == bot) { - const Location& tankPosition = GruulsLairLocations::GruulTankPosition; - const float maxDistance = 3.0f; + const Position& position = GRUUL_TANK_POSITION; + const float maxDistance = 5.0f; - float dX = tankPosition.x - bot->GetPositionX(); - float dY = tankPosition.y - bot->GetPositionY(); - float distanceToTankPosition = bot->GetExactDist2d(tankPosition.x, tankPosition.y); + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float distanceToTankPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); if (distanceToTankPosition > maxDistance) { float step = std::min(maxDistance, distanceToTankPosition); float moveX = bot->GetPositionX() + (dX / distanceToTankPosition) * maxDistance; float moveY = bot->GetPositionY() + (dY / distanceToTankPosition) * maxDistance; - const float moveZ = tankPosition.z; - return MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, false, + const float moveZ = position.GetPositionZ(); + return MoveTo(GRUULS_LAIR_MAP_ID, moveX, moveY, moveZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } - - float orientation = atan2(gruul->GetPositionY() - bot->GetPositionY(), - gruul->GetPositionX() - bot->GetPositionX()); - bot->SetFacingTo(orientation); - } - else if (!bot->IsWithinMeleeRange(gruul)) - { - return MoveTo(gruul->GetMapId(), gruul->GetPositionX(), gruul->GetPositionY(), gruul->GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } return false; @@ -579,16 +535,16 @@ bool GruulTheDragonkillerSpreadRangedAction::Execute(Event event) static std::unordered_map hasReachedInitialPosition; Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); - if (gruul && gruul->IsAlive() && gruul->GetHealth() == gruul->GetMaxHealth()) + if (gruul && gruul->GetHealth() == gruul->GetMaxHealth()) { - initialPositions.clear(); - hasReachedInitialPosition.clear(); + initialPositions.erase(bot->GetGUID()); + hasReachedInitialPosition.erase(bot->GetGUID()); } - const Location& tankPosition = GruulsLairLocations::GruulTankPosition; - const float centerX = tankPosition.x; - const float centerY = tankPosition.y; - float centerZ = bot->GetPositionZ(); + const Position& position = GRUUL_TANK_POSITION; + const float centerX = position.GetPositionX(); + const float centerY = position.GetPositionY(); + const float centerZ = position.GetPositionZ(); const float minRadius = 25.0f; const float maxRadius = 40.0f; @@ -642,7 +598,7 @@ bool GruulTheDragonkillerSpreadRangedAction::Execute(Event event) bot->GetPositionY(), bot->GetPositionZ(), destX, destY, destZ)) return false; - return MoveTo(bot->GetMapId(), destX, destY, destZ, false, false, false, false, + return MoveTo(GRUULS_LAIR_MAP_ID, destX, destY, destZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } diff --git a/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.h b/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.h index 6faf7ed3e..525e0ee6d 100644 --- a/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.h +++ b/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.h @@ -85,10 +85,10 @@ public: bool Execute(Event event) override; }; -class GruulTheDragonkillerMainTankPositionBossAction : public AttackAction +class GruulTheDragonkillerTanksPositionBossAction : public AttackAction { public: - GruulTheDragonkillerMainTankPositionBossAction(PlayerbotAI* botAI, std::string const name = "gruul the dragonkiller main tank position boss") : AttackAction(botAI, name) {}; + GruulTheDragonkillerTanksPositionBossAction(PlayerbotAI* botAI, std::string const name = "gruul the dragonkiller tanks position boss") : AttackAction(botAI, name) {}; bool Execute(Event event) override; }; diff --git a/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.cpp b/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.cpp index 5ca2de932..6604fc525 100644 --- a/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.cpp +++ b/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.cpp @@ -8,18 +8,11 @@ #include "HunterActions.h" #include "MageActions.h" #include "Playerbots.h" +#include "ReachTargetActions.h" #include "WarriorActions.h" using namespace GruulsLairHelpers; -static bool IsChargeAction(Action* action) -{ - return dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action); -} - float HighKingMaulgarDisableTankAssistMultiplier::GetValue(Action* action) { if (IsAnyOgreBossAlive(botAI) && dynamic_cast(action)) @@ -38,12 +31,10 @@ float HighKingMaulgarAvoidWhirlwindMultiplier::GetValue(Action* action) Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); if (maulgar && maulgar->HasAura(SPELL_WHIRLWIND) && - (!kiggler || !kiggler->IsAlive()) && - (!krosh || !krosh->IsAlive()) && - (!olm || !olm->IsAlive()) && - (!blindeye || !blindeye->IsAlive())) + !kiggler && !krosh && !olm && !blindeye) { - if (IsChargeAction(action) || (dynamic_cast(action) && + if (dynamic_cast(action) || + (dynamic_cast(action) && !dynamic_cast(action))) return 0.0f; } @@ -57,7 +48,8 @@ float HighKingMaulgarDisableArcaneShotOnKroshMultiplier::GetValue(Action* action Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); Unit* target = AI_VALUE(Unit*, "current target"); - if (krosh && target && target->GetGUID() == krosh->GetGUID() && dynamic_cast(action)) + if (krosh && target && target->GetGUID() == krosh->GetGUID() && + dynamic_cast(action)) return 0.0f; return 1.0f; @@ -101,8 +93,9 @@ float GruulTheDragonkillerGroundSlamMultiplier::GetValue(Action* action) if (bot->HasAura(SPELL_GROUND_SLAM_1) || bot->HasAura(SPELL_GROUND_SLAM_2)) { - if ((dynamic_cast(action) && !dynamic_cast(action)) || - IsChargeAction(action)) + if ((dynamic_cast(action) && + !dynamic_cast(action)) || + dynamic_cast(action)) return 0.0f; } diff --git a/src/Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h b/src/Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h index 809fadf03..3850f58c6 100644 --- a/src/Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h +++ b/src/Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h @@ -22,7 +22,7 @@ public: creators["high king maulgar misdirect olm and blindeye"] = &RaidGruulsLairActionContext::high_king_maulgar_misdirect_olm_and_blindeye; // Gruul the Dragonkiller - creators["gruul the dragonkiller main tank position boss"] = &RaidGruulsLairActionContext::gruul_the_dragonkiller_main_tank_position_boss; + creators["gruul the dragonkiller tanks position boss"] = &RaidGruulsLairActionContext::gruul_the_dragonkiller_tanks_position_boss; creators["gruul the dragonkiller spread ranged"] = &RaidGruulsLairActionContext::gruul_the_dragonkiller_spread_ranged; creators["gruul the dragonkiller shatter spread"] = &RaidGruulsLairActionContext::gruul_the_dragonkiller_shatter_spread; } @@ -41,7 +41,7 @@ private: static Action* high_king_maulgar_misdirect_olm_and_blindeye(PlayerbotAI* botAI) { return new HighKingMaulgarMisdirectOlmAndBlindeyeAction(botAI); } // Gruul the Dragonkiller - static Action* gruul_the_dragonkiller_main_tank_position_boss(PlayerbotAI* botAI) { return new GruulTheDragonkillerMainTankPositionBossAction(botAI); } + static Action* gruul_the_dragonkiller_tanks_position_boss(PlayerbotAI* botAI) { return new GruulTheDragonkillerTanksPositionBossAction(botAI); } static Action* gruul_the_dragonkiller_spread_ranged(PlayerbotAI* botAI) { return new GruulTheDragonkillerSpreadRangedAction(botAI); } static Action* gruul_the_dragonkiller_shatter_spread(PlayerbotAI* botAI) { return new GruulTheDragonkillerShatterSpreadAction(botAI); } }; diff --git a/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h b/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h index fa8e76f58..d12b0ce46 100644 --- a/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h +++ b/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h @@ -22,8 +22,8 @@ public: creators["high king maulgar pulling olm and blindeye"] = &RaidGruulsLairTriggerContext::high_king_maulgar_pulling_olm_and_blindeye; // Gruul the Dragonkiller - creators["gruul the dragonkiller boss engaged by main tank"] = &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_boss_engaged_by_main_tank; - creators["gruul the dragonkiller boss engaged by range"] = &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_boss_engaged_by_range; + creators["gruul the dragonkiller boss engaged by tanks"] = &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_boss_engaged_by_tanks; + creators["gruul the dragonkiller boss engaged by ranged"] = &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_boss_engaged_by_ranged; creators["gruul the dragonkiller incoming shatter"] = &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_incoming_shatter; } @@ -41,8 +41,8 @@ private: static Trigger* high_king_maulgar_pulling_olm_and_blindeye(PlayerbotAI* botAI) { return new HighKingMaulgarPullingOlmAndBlindeyeTrigger(botAI); } // Gruul the Dragonkiller - static Trigger* gruul_the_dragonkiller_boss_engaged_by_main_tank(PlayerbotAI* botAI) { return new GruulTheDragonkillerBossEngagedByMainTankTrigger(botAI); } - static Trigger* gruul_the_dragonkiller_boss_engaged_by_range(PlayerbotAI* botAI) { return new GruulTheDragonkillerBossEngagedByRangeTrigger(botAI); } + static Trigger* gruul_the_dragonkiller_boss_engaged_by_tanks(PlayerbotAI* botAI) { return new GruulTheDragonkillerBossEngagedByTanksTrigger(botAI); } + static Trigger* gruul_the_dragonkiller_boss_engaged_by_ranged(PlayerbotAI* botAI) { return new GruulTheDragonkillerBossEngagedByRangedTrigger(botAI); } static Trigger* gruul_the_dragonkiller_incoming_shatter(PlayerbotAI* botAI) { return new GruulTheDragonkillerIncomingShatterTrigger(botAI); } }; diff --git a/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.cpp b/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.cpp index 9ec264ea0..249c8e8a8 100644 --- a/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.cpp +++ b/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.cpp @@ -35,10 +35,10 @@ void RaidGruulsLairStrategy::InitTriggers(std::vector& triggers) NextAction("high king maulgar misdirect olm and blindeye", ACTION_RAID + 2) })); // Gruul the Dragonkiller - triggers.push_back(new TriggerNode("gruul the dragonkiller boss engaged by main tank", { - NextAction("gruul the dragonkiller main tank position boss", ACTION_RAID + 1) })); + triggers.push_back(new TriggerNode("gruul the dragonkiller boss engaged by tanks", { + NextAction("gruul the dragonkiller tanks position boss", ACTION_RAID + 1) })); - triggers.push_back(new TriggerNode("gruul the dragonkiller boss engaged by range", { + triggers.push_back(new TriggerNode("gruul the dragonkiller boss engaged by ranged", { NextAction("gruul the dragonkiller spread ranged", ACTION_RAID + 1) })); triggers.push_back(new TriggerNode("gruul the dragonkiller incoming shatter", { diff --git a/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.cpp b/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.cpp index 35d9f9a1d..4bc5efe99 100644 --- a/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.cpp +++ b/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.cpp @@ -10,35 +10,35 @@ bool HighKingMaulgarIsMainTankTrigger::IsActive() { Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); - return botAI->IsMainTank(bot) && maulgar && maulgar->IsAlive(); + return botAI->IsMainTank(bot) && maulgar; } bool HighKingMaulgarIsFirstAssistTankTrigger::IsActive() { Unit* olm = AI_VALUE2(Unit*, "find target", "olm the summoner"); - return botAI->IsAssistTankOfIndex(bot, 0) && olm && olm->IsAlive(); + return botAI->IsAssistTankOfIndex(bot, 0, false) && olm; } bool HighKingMaulgarIsSecondAssistTankTrigger::IsActive() { Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); - return botAI->IsAssistTankOfIndex(bot, 1) && blindeye && blindeye->IsAlive(); + return botAI->IsAssistTankOfIndex(bot, 1, false) && blindeye; } bool HighKingMaulgarIsMageTankTrigger::IsActive() { Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); - return IsKroshMageTank(botAI, bot) && krosh && krosh->IsAlive(); + return IsKroshMageTank(botAI, bot) && krosh; } bool HighKingMaulgarIsMoonkinTankTrigger::IsActive() { Unit* kiggler = AI_VALUE2(Unit*, "find target", "kiggler the crazed"); - return IsKigglerMoonkinTank(botAI, bot) && kiggler && kiggler->IsAlive(); + return IsKigglerMoonkinTank(botAI, bot) && kiggler; } bool HighKingMaulgarDeterminingKillOrderTrigger::IsActive() @@ -50,11 +50,11 @@ bool HighKingMaulgarDeterminingKillOrderTrigger::IsActive() Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); return (botAI->IsDps(bot) || botAI->IsTank(bot)) && - !(botAI->IsMainTank(bot) && maulgar && maulgar->IsAlive()) && - !(botAI->IsAssistTankOfIndex(bot, 0) && olm && olm->IsAlive()) && - !(botAI->IsAssistTankOfIndex(bot, 1) && blindeye && blindeye->IsAlive()) && - !(IsKroshMageTank(botAI, bot) && krosh && krosh->IsAlive()) && - !(IsKigglerMoonkinTank(botAI, bot) && kiggler && kiggler->IsAlive()); + !(botAI->IsMainTank(bot) && maulgar) && + !(botAI->IsAssistTankOfIndex(bot, 0, false) && olm) && + !(botAI->IsAssistTankOfIndex(bot, 1, false) && blindeye) && + !(IsKroshMageTank(botAI, bot) && krosh) && + !(IsKigglerMoonkinTank(botAI, bot) && kiggler); } bool HighKingMaulgarHealerInDangerTrigger::IsActive() @@ -66,7 +66,7 @@ bool HighKingMaulgarBossChannelingWhirlwindTrigger::IsActive() { Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); - return maulgar && maulgar->IsAlive() && maulgar->HasAura(SPELL_WHIRLWIND) && + return maulgar && maulgar->HasAura(SPELL_WHIRLWIND) && !botAI->IsMainTank(bot); } @@ -74,7 +74,7 @@ bool HighKingMaulgarWildFelstalkerSpawnedTrigger::IsActive() { Unit* felStalker = AI_VALUE2(Unit*, "find target", "wild fel stalker"); - return felStalker && felStalker->IsAlive() && bot->getClass() == CLASS_WARLOCK; + return felStalker && bot->getClass() == CLASS_WARLOCK; } bool HighKingMaulgarPullingOlmAndBlindeyeTrigger::IsActive() @@ -120,12 +120,12 @@ bool HighKingMaulgarPullingOlmAndBlindeyeTrigger::IsActive() switch (hunterIndex) { case 0: - return olm && olm->IsAlive() && olm->GetHealthPct() > 98.0f && - olmTank && olmTank->IsAlive() && botAI->CanCastSpell("misdirection", olmTank); + return olm && olm->GetHealthPct() > 98.0f && + olmTank && botAI->CanCastSpell("misdirection", olmTank); case 1: - return blindeye && blindeye->IsAlive() && blindeye->GetHealthPct() > 90.0f && - blindeyeTank && blindeyeTank->IsAlive() && botAI->CanCastSpell("misdirection", blindeyeTank); + return blindeye && blindeye->GetHealthPct() > 90.0f && + blindeyeTank && botAI->CanCastSpell("misdirection", blindeyeTank); default: break; @@ -136,25 +136,24 @@ bool HighKingMaulgarPullingOlmAndBlindeyeTrigger::IsActive() // Gruul the Dragonkiller Triggers -bool GruulTheDragonkillerBossEngagedByMainTankTrigger::IsActive() +bool GruulTheDragonkillerBossEngagedByTanksTrigger::IsActive() { Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); - return gruul && gruul->IsAlive() && botAI->IsMainTank(bot); + return gruul && botAI->IsTank(bot); } -bool GruulTheDragonkillerBossEngagedByRangeTrigger::IsActive() +bool GruulTheDragonkillerBossEngagedByRangedTrigger::IsActive() { Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); - return gruul && gruul->IsAlive() && botAI->IsRanged(bot); + return gruul && botAI->IsRanged(bot); } bool GruulTheDragonkillerIncomingShatterTrigger::IsActive() { Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); - return gruul && gruul->IsAlive() && - (bot->HasAura(SPELL_GROUND_SLAM_1) || - bot->HasAura(SPELL_GROUND_SLAM_2)); + return gruul && (bot->HasAura(SPELL_GROUND_SLAM_1) || + bot->HasAura(SPELL_GROUND_SLAM_2)); } diff --git a/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.h b/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.h index f3f328536..583b8b75f 100644 --- a/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.h +++ b/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.h @@ -73,17 +73,17 @@ public: bool IsActive() override; }; -class GruulTheDragonkillerBossEngagedByMainTankTrigger : public Trigger +class GruulTheDragonkillerBossEngagedByTanksTrigger : public Trigger { public: - GruulTheDragonkillerBossEngagedByMainTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "gruul the dragonkiller boss engaged by main tank") {} + GruulTheDragonkillerBossEngagedByTanksTrigger(PlayerbotAI* botAI) : Trigger(botAI, "gruul the dragonkiller boss engaged by tanks") {} bool IsActive() override; }; -class GruulTheDragonkillerBossEngagedByRangeTrigger : public Trigger +class GruulTheDragonkillerBossEngagedByRangedTrigger : public Trigger { public: - GruulTheDragonkillerBossEngagedByRangeTrigger(PlayerbotAI* botAI) : Trigger(botAI, "gruul the dragonkiller boss engaged by range") {} + GruulTheDragonkillerBossEngagedByRangedTrigger(PlayerbotAI* botAI) : Trigger(botAI, "gruul the dragonkiller boss engaged by ranged") {} bool IsActive() override; }; diff --git a/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.cpp b/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.cpp index 0c8a23a19..27c703aec 100644 --- a/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.cpp +++ b/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.cpp @@ -6,19 +6,16 @@ namespace GruulsLairHelpers { - namespace GruulsLairLocations - { - // Olm does not chase properly due to the Core's caster movement issues - // Thus, the below "OlmTankPosition" is beyond the actual desired tanking location - // It is the spot to which the OlmTank runs to to pull Olm to a decent tanking location - // "MaulgarRoomCenter" is to keep healers in a centralized location - const Location MaulgarTankPosition = { 90.686f, 167.047f, -13.234f }; - const Location OlmTankPosition = { 87.485f, 234.942f, -3.635f }; - const Location BlindeyeTankPosition = { 99.681f, 213.989f, -10.345f }; - const Location KroshTankPosition = { 116.880f, 166.208f, -14.231f }; - const Location MaulgarRoomCenter = { 88.754f, 150.759f, -11.569f }; - const Location GruulTankPosition = { 241.238f, 365.025f, -4.220f }; - } + // Olm does not chase properly due to the Core's caster movement issues + // Thus, the below "OlmTankPosition" is beyond the actual desired tanking location + // It is the spot to which the OlmTank runs to to pull Olm to a decent tanking location + // "MaulgarRoomCenter" is to keep healers in a centralized location + const Position MAULGAR_TANK_POSITION = { 90.686f, 167.047f, -13.234f }; + const Position OLM_TANK_POSITION = { 87.485f, 234.942f, -3.635f }; + const Position BLINDEYE_TANK_POSITION = { 99.681f, 213.989f, -10.345f }; + const Position KROSH_TANK_POSITION = { 116.880f, 166.208f, -14.231f }; + const Position MAULGAR_ROOM_CENTER = { 88.754f, 150.759f, -11.569f }; + const Position GRUUL_TANK_POSITION = { 241.238f, 365.025f, -4.220f }; bool IsAnyOgreBossAlive(PlayerbotAI* botAI) { @@ -42,84 +39,43 @@ namespace GruulsLairHelpers return false; } - void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId) - { - Group* group = bot->GetGroup(); - if (!target || !group) - return; - - ObjectGuid currentGuid = group->GetTargetIcon(iconId); - if (currentGuid != target->GetGUID()) - { - group->SetTargetIcon(iconId, bot->GetGUID(), target->GetGUID()); - } - } - - void MarkTargetWithSquare(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::squareIndex); - } - - void MarkTargetWithStar(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::starIndex); - } - - void MarkTargetWithCircle(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::circleIndex); - } - - void MarkTargetWithDiamond(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::diamondIndex); - } - - void MarkTargetWithTriangle(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::triangleIndex); - } - - void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target) - { - if (!target) - return; - - std::string currentRti = botAI->GetAiObjectContext()->GetValue("rti")->Get(); - Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("rti target")->Get(); - - if (currentRti != rtiName || currentTarget != target) - { - botAI->GetAiObjectContext()->GetValue("rti")->Set(rtiName); - botAI->GetAiObjectContext()->GetValue("rti target")->Set(target); - } - } - bool IsKroshMageTank(PlayerbotAI* botAI, Player* bot) { Group* group = bot->GetGroup(); if (!group) return false; - Player* highestHpMage = nullptr; - uint32 highestHp = 0; + // (1) First loop: Return the first assistant Mage (real player or bot) for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member)) + if (!member || !member->IsAlive() || member->getClass() != CLASS_MAGE) continue; - if (member->getClass() == CLASS_MAGE) + if (group->IsAssistant(member->GetGUID())) + return member == bot; + } + + // (2) Fall back to bot Mage with highest HP + Player* highestHpMage = nullptr; + uint32 highestHp = 0; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || + member->getClass() != CLASS_MAGE) + continue; + + uint32 hp = member->GetMaxHealth(); + if (!highestHpMage || hp > highestHp) { - uint32 hp = member->GetMaxHealth(); - if (!highestHpMage || hp > highestHp) - { - highestHpMage = member; - highestHp = hp; - } + highestHpMage = member; + highestHp = hp; } } + // (3) Return the found Mage tank, or nullptr if none found return highestHpMage == bot; } @@ -129,30 +85,37 @@ namespace GruulsLairHelpers if (!group) return false; - Player* highestHpMoonkin = nullptr; - uint32 highestHp = 0; - + // (1) First loop: Return the first assistant Moonkin (real player or bot) for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member)) + if (!member || !member->IsAlive() || member->getClass() != CLASS_DRUID) continue; - if (member->getClass() == CLASS_DRUID) + if (group->IsAssistant(member->GetGUID()) && + AiFactory::GetPlayerSpecTab(member) == DRUID_TAB_BALANCE) + return member == bot; + } + + // (2) Fall back to bot Moonkin with highest HP + Player* highestHpMoonkin = nullptr; + uint32 highestHp = 0; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->getClass() != CLASS_DRUID || + !GET_PLAYERBOT_AI(member) || AiFactory::GetPlayerSpecTab(member) != DRUID_TAB_BALANCE) + continue; + + uint32 hp = member->GetMaxHealth(); + if (!highestHpMoonkin || hp > highestHp) { - int tab = AiFactory::GetPlayerSpecTab(member); - if (tab == DRUID_TAB_BALANCE) - { - uint32 hp = member->GetMaxHealth(); - if (!highestHpMoonkin || hp > highestHp) - { - highestHpMoonkin = member; - highestHp = hp; - } - } + highestHpMoonkin = member; + highestHp = hp; } } + // (3) Return the found Moonkin tank, or nullptr if none found return highestHpMoonkin == bot; } diff --git a/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.h b/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.h index c7becc836..4615a9b7a 100644 --- a/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.h +++ b/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.h @@ -2,23 +2,19 @@ #define RAID_GRUULSLAIRHELPERS_H #include "PlayerbotAI.h" -#include "RtiTargetValue.h" namespace GruulsLairHelpers { enum GruulsLairSpells { // High King Maulgar - SPELL_WHIRLWIND = 33238, + SPELL_WHIRLWIND = 33238, // Krosh Firehand - SPELL_SPELL_SHIELD = 33054, + SPELL_SPELL_SHIELD = 33054, // Hunter - SPELL_MISDIRECTION = 35079, - - // Warlock - SPELL_BANISH = 18647, // Rank 2 + SPELL_MISDIRECTION = 35079, // Gruul the Dragonkiller SPELL_GROUND_SLAM_1 = 33525, @@ -30,33 +26,20 @@ namespace GruulsLairHelpers NPC_WILD_FEL_STALKER = 18847, }; + constexpr uint32 GRUULS_LAIR_MAP_ID = 565; + bool IsAnyOgreBossAlive(PlayerbotAI* botAI); - void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId); - void MarkTargetWithSquare(Player* bot, Unit* target); - void MarkTargetWithStar(Player* bot, Unit* target); - void MarkTargetWithCircle(Player* bot, Unit* target); - void MarkTargetWithDiamond(Player* bot, Unit* target); - void MarkTargetWithTriangle(Player* bot, Unit* target); - void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target); bool IsKroshMageTank(PlayerbotAI* botAI, Player* bot); bool IsKigglerMoonkinTank(PlayerbotAI* botAI, Player* bot); bool IsPositionSafe(PlayerbotAI* botAI, Player* bot, Position pos); bool TryGetNewSafePosition(PlayerbotAI* botAI, Player* bot, Position& outPos); - struct Location - { - float x, y, z; - }; - - namespace GruulsLairLocations - { - extern const Location MaulgarTankPosition; - extern const Location OlmTankPosition; - extern const Location BlindeyeTankPosition; - extern const Location KroshTankPosition; - extern const Location MaulgarRoomCenter; - extern const Location GruulTankPosition; - } + extern const Position MAULGAR_TANK_POSITION; + extern const Position OLM_TANK_POSITION; + extern const Position BLINDEYE_TANK_POSITION; + extern const Position KROSH_TANK_POSITION; + extern const Position MAULGAR_ROOM_CENTER; + extern const Position GRUUL_TANK_POSITION; } #endif diff --git a/src/Ai/Raid/Icecrown/RaidIccScripts.h b/src/Ai/Raid/Icecrown/Util/RaidIccScripts.h similarity index 100% rename from src/Ai/Raid/Icecrown/RaidIccScripts.h rename to src/Ai/Raid/Icecrown/Util/RaidIccScripts.h diff --git a/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp b/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp index adf0eac48..4d40a9eed 100644 --- a/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp +++ b/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp @@ -2,6 +2,7 @@ #include "RaidKarazhanHelpers.h" #include "Playerbots.h" #include "PlayerbotTextMgr.h" +#include "RaidBossHelpers.h" using namespace KarazhanHelpers; @@ -44,7 +45,7 @@ bool AttumenTheHuntsmanMarkTargetAction::Execute(Event event) Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); if (attumenMounted) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) MarkTargetWithStar(bot, attumenMounted); SetRtiTarget(botAI, "star", attumenMounted); @@ -57,7 +58,7 @@ bool AttumenTheHuntsmanMarkTargetAction::Execute(Event event) } else if (Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight")) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) MarkTargetWithStar(bot, midnight); if (!botAI->IsAssistTankOfIndex(bot, 0)) @@ -180,7 +181,7 @@ bool MoroesMarkTargetAction::Execute(Event event) if (target) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) MarkTargetWithSkull(bot, target); SetRtiTarget(botAI, "skull", target); @@ -405,7 +406,7 @@ bool TheCuratorMarkAstralFlareAction::Execute(Event event) if (!flare) return false; - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) MarkTargetWithSkull(bot, flare); SetRtiTarget(botAI, "skull", flare); @@ -469,11 +470,11 @@ bool TheCuratorSpreadRangedAction::Execute(Event event) // Prioritize (1) Demon Chains, (2) Kil'rek, (3) Illhoof bool TerestianIllhoofMarkTargetAction::Execute(Event event) { - Unit* demonChains = AI_VALUE2(Unit*, "find target", "demon chains"); - Unit* kilrek = AI_VALUE2(Unit*, "find target", "kil'rek"); + Unit* demonChains = GetFirstAliveUnitByEntry(botAI, NPC_DEMON_CHAINS); + Unit* kilrek = GetFirstAliveUnitByEntry(botAI, NPC_KILREK); Unit* illhoof = AI_VALUE2(Unit*, "find target", "terestian illhoof"); - Unit* target = GetFirstAliveUnit({demonChains, kilrek, illhoof}); + Unit* target = GetFirstAliveUnit({demonChains, kilrek, illhoof}); if (target) MarkTargetWithSkull(bot, target); @@ -1007,7 +1008,7 @@ bool NetherspiteManageTimersAndTrackersAction::Execute(Event event) if (netherspite->GetHealth() == netherspite->GetMaxHealth() && !netherspite->HasAura(SPELL_GREEN_BEAM_HEAL)) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) netherspiteDpsWaitTimer.insert_or_assign(instanceId, now); if (botAI->IsTank(bot) && !bot->HasAura(SPELL_RED_BEAM_DEBUFF)) @@ -1018,7 +1019,7 @@ bool NetherspiteManageTimersAndTrackersAction::Execute(Event event) } else if (netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) netherspiteDpsWaitTimer.erase(instanceId); if (botAI->IsTank(bot)) @@ -1029,7 +1030,7 @@ bool NetherspiteManageTimersAndTrackersAction::Execute(Event event) } else if (!netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) netherspiteDpsWaitTimer.try_emplace(instanceId, now); if (botAI->IsTank(bot) && bot->HasAura(SPELL_RED_BEAM_DEBUFF)) @@ -1458,7 +1459,7 @@ bool NightbaneManageTimersAndTrackersAction::Execute(Event event) if (botAI->IsRanged(bot)) nightbaneRangedStep.erase(botGuid); - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) nightbaneDpsWaitTimer.erase(instanceId); } // Erase flight phase timer and Rain of Bones tracker on ground phase and start DPS wait timer @@ -1466,7 +1467,7 @@ bool NightbaneManageTimersAndTrackersAction::Execute(Event event) { nightbaneRainOfBonesHit.erase(botGuid); - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) { nightbaneFlightPhaseStartTimer.erase(instanceId); nightbaneDpsWaitTimer.try_emplace(instanceId, now); @@ -1482,7 +1483,7 @@ bool NightbaneManageTimersAndTrackersAction::Execute(Event event) if (botAI->IsRanged(bot)) nightbaneRangedStep.erase(botGuid); - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) { nightbaneDpsWaitTimer.erase(instanceId); nightbaneFlightPhaseStartTimer.try_emplace(instanceId, now); diff --git a/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.cpp b/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.cpp index 117a17f38..06a3335c3 100644 --- a/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.cpp +++ b/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.cpp @@ -10,6 +10,7 @@ #include "MageActions.h" #include "Playerbots.h" #include "PriestActions.h" +#include "RaidBossHelpers.h" #include "ReachTargetActions.h" #include "RogueActions.h" #include "ShamanActions.h" @@ -242,6 +243,9 @@ float PrinceMalchezaarEnfeebleKeepDistanceMultiplier::GetValue(Action* action) if (bot->HasAura(SPELL_ENFEEBLE)) { + if (dynamic_cast(action)) + return 0.0f; + if (dynamic_cast(action) && !dynamic_cast(action)) return 0.0f; diff --git a/src/Ai/Raid/Karazhan/Trigger/RaidKarazhanTriggers.cpp b/src/Ai/Raid/Karazhan/Trigger/RaidKarazhanTriggers.cpp index 2fb7d5af0..3c43aa898 100644 --- a/src/Ai/Raid/Karazhan/Trigger/RaidKarazhanTriggers.cpp +++ b/src/Ai/Raid/Karazhan/Trigger/RaidKarazhanTriggers.cpp @@ -2,6 +2,7 @@ #include "RaidKarazhanHelpers.h" #include "RaidKarazhanActions.h" #include "Playerbots.h" +#include "RaidBossHelpers.h" using namespace KarazhanHelpers; @@ -40,7 +41,7 @@ bool AttumenTheHuntsmanAttumenIsMountedTrigger::IsActive() bool AttumenTheHuntsmanBossWipesAggroWhenMountingTrigger::IsActive() { - if (!IsInstanceTimerManager(botAI, bot)) + if (!IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) return false; Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"); @@ -110,7 +111,7 @@ bool BigBadWolfBossIsChasingLittleRedRidingHoodTrigger::IsActive() bool RomuloAndJulianneBothBossesRevivedTrigger::IsActive() { - if (!IsInstanceTimerManager(botAI, bot)) + if (!IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) return false; Unit* romulo = AI_VALUE2(Unit*, "find target", "romulo"); @@ -126,7 +127,7 @@ bool RomuloAndJulianneBothBossesRevivedTrigger::IsActive() bool WizardOfOzNeedTargetPriorityTrigger::IsActive() { - if (!IsInstanceTimerManager(botAI, bot)) + if (!IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) return false; Unit* dorothee = AI_VALUE2(Unit*, "find target", "dorothee"); @@ -178,7 +179,7 @@ bool TheCuratorBossAstralFlaresCastArcingSearTrigger::IsActive() bool TerestianIllhoofNeedTargetPriorityTrigger::IsActive() { - if (!IsInstanceTimerManager(botAI, bot)) + if (!IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) return false; Unit* illhoof = AI_VALUE2(Unit*, "find target", "terestian illhoof"); @@ -202,7 +203,7 @@ bool ShadeOfAranFlameWreathIsActiveTrigger::IsActive() // Exclusion of Banish is so the player may Banish elementals if they wish bool ShadeOfAranConjuredElementalsSummonedTrigger::IsActive() { - if (!IsInstanceTimerManager(botAI, bot)) + if (!IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) return false; Unit* elemental = AI_VALUE2(Unit*, "find target", "conjured elemental"); @@ -279,7 +280,7 @@ bool NetherspiteBossIsBanishedTrigger::IsActive() bool NetherspiteNeedToManageTimersAndTrackersTrigger::IsActive() { - if (!botAI->IsTank(bot) && !IsInstanceTimerManager(botAI, bot)) + if (!botAI->IsTank(bot) && !IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) return false; Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); diff --git a/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.cpp b/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.cpp index 821cc6701..ea989dcf3 100644 --- a/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.cpp +++ b/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.cpp @@ -1,7 +1,6 @@ #include "RaidKarazhanHelpers.h" #include "RaidKarazhanActions.h" #include "Playerbots.h" -#include "RtiTargetValue.h" namespace KarazhanHelpers { @@ -52,75 +51,6 @@ namespace KarazhanHelpers const Position NIGHTBANE_FLIGHT_STACK_POSITION = { -11159.555f, -1893.526f, 91.473f }; // Broken Barrel const Position NIGHTBANE_RAIN_OF_BONES_POSITION = { -11165.233f, -1911.123f, 91.473f }; - void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId) - { - if (!target) - return; - - if (Group* group = bot->GetGroup()) - { - ObjectGuid currentGuid = group->GetTargetIcon(iconId); - if (currentGuid != target->GetGUID()) - group->SetTargetIcon(iconId, bot->GetGUID(), target->GetGUID()); - } - } - - void MarkTargetWithSkull(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::skullIndex); - } - - void MarkTargetWithSquare(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::squareIndex); - } - - void MarkTargetWithStar(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::starIndex); - } - - void MarkTargetWithCircle(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::circleIndex); - } - - void MarkTargetWithMoon(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::moonIndex); - } - - void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target) - { - if (!target) - return; - - std::string currentRti = botAI->GetAiObjectContext()->GetValue("rti")->Get(); - Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("rti target")->Get(); - - if (currentRti != rtiName || currentTarget != target) - { - botAI->GetAiObjectContext()->GetValue("rti")->Set(rtiName); - botAI->GetAiObjectContext()->GetValue("rti target")->Set(target); - } - } - - // Only one bot is needed to set/reset instance-wide timers - bool IsInstanceTimerManager(PlayerbotAI* botAI, Player* bot) - { - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsDps(member) && GET_PLAYERBOT_AI(member)) - return member == bot; - } - } - - return false; - } - Unit* GetFirstAliveUnit(const std::vector& units) { for (Unit* unit : units) @@ -132,44 +62,6 @@ namespace KarazhanHelpers return nullptr; } - Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry) - { - const GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); - for (auto const& npcGuid : npcs) - { - Unit* unit = botAI->GetUnit(npcGuid); - if (unit && unit->IsAlive() && unit->GetEntry() == entry) - return unit; - } - - return nullptr; - } - - Unit* GetNearestPlayerInRadius(Player* bot, float radius) - { - Unit* nearestPlayer = nullptr; - float nearestDistance = radius; - - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref != nullptr; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || member == bot) - continue; - - float distance = bot->GetExactDist2d(member); - if (distance < nearestDistance) - { - nearestDistance = distance; - nearestPlayer = member; - } - } - } - - return nearestPlayer; - } - bool IsFlameWreathActive(PlayerbotAI* botAI, Player* bot) { Unit* aran = botAI->GetAiObjectContext()->GetValue("find target", "shade of aran")->Get(); diff --git a/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h b/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h index 394693b2e..ad7e8c3d5 100644 --- a/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h +++ b/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h @@ -61,6 +61,11 @@ namespace KarazhanHelpers NPC_ATTUMEN_THE_HUNTSMAN = 15550, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED = 16152, + // Terestian Illhoof + NPC_TERESTIAN_ILLHOOF = 15688, + NPC_DEMON_CHAINS = 17248, + NPC_KILREK = 17229, + // Shade of Aran NPC_CONJURED_ELEMENTAL = 17167, @@ -74,8 +79,8 @@ namespace KarazhanHelpers NPC_NETHERSPITE_INFERNAL = 17646, }; - const uint32 KARAZHAN_MAP_ID = 532; - const float NIGHTBANE_FLIGHT_Z = 95.0f; + constexpr uint32 KARAZHAN_MAP_ID = 532; + constexpr float NIGHTBANE_FLIGHT_Z = 95.0f; // Attumen the Huntsman extern std::unordered_map attumenDpsWaitTimer; @@ -105,17 +110,7 @@ namespace KarazhanHelpers extern const Position NIGHTBANE_FLIGHT_STACK_POSITION; extern const Position NIGHTBANE_RAIN_OF_BONES_POSITION; - void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId); - void MarkTargetWithSkull(Player* bot, Unit* target); - void MarkTargetWithSquare(Player* bot, Unit* target); - void MarkTargetWithStar(Player* bot, Unit* target); - void MarkTargetWithCircle(Player* bot, Unit* target); - void MarkTargetWithMoon(Player* bot, Unit* target); - void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target); - bool IsInstanceTimerManager(PlayerbotAI* botAI, Player* bot); Unit* GetFirstAliveUnit(const std::vector& units); - Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry); - Unit* GetNearestPlayerInRadius(Player* bot, float radius); bool IsFlameWreathActive(PlayerbotAI* botAI, Player* bot); std::vector GetRedBlockers(PlayerbotAI* botAI, Player* bot); std::vector GetBlueBlockers(PlayerbotAI* botAI, Player* bot); diff --git a/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.cpp b/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.cpp index 69fc86244..dab7efab9 100644 --- a/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.cpp +++ b/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.cpp @@ -4,6 +4,7 @@ #include "ObjectAccessor.h" #include "ObjectGuid.h" #include "Playerbots.h" +#include "RaidBossHelpers.h" using namespace MagtheridonHelpers; @@ -14,46 +15,45 @@ bool MagtheridonMainTankAttackFirstThreeChannelersAction::Execute(Event event) return false; Creature* channelerSquare = GetChanneler(bot, SOUTH_CHANNELER); - if (channelerSquare && channelerSquare->IsAlive()) + if (channelerSquare) MarkTargetWithSquare(bot, channelerSquare); Creature* channelerStar = GetChanneler(bot, WEST_CHANNELER); - if (channelerStar && channelerStar->IsAlive()) + if (channelerStar) MarkTargetWithStar(bot, channelerStar); Creature* channelerCircle = GetChanneler(bot, EAST_CHANNELER); - if (channelerCircle && channelerCircle->IsAlive()) + if (channelerCircle) MarkTargetWithCircle(bot, channelerCircle); // After first three channelers are dead, wait for Magtheridon to activate - if ((!channelerSquare || !channelerSquare->IsAlive()) && - (!channelerStar || !channelerStar->IsAlive()) && - (!channelerCircle || !channelerCircle->IsAlive())) + if (!channelerSquare && !channelerStar && !channelerCircle) { - const Location& position = MagtheridonsLairLocations::WaitingForMagtheridonPosition; - if (!bot->IsWithinDist2d(position.x, position.y, 2.0f)) + const Position& position = WAITING_FOR_MAGTHERIDON_POSITION; + if (!bot->IsWithinDist2d(position.GetPositionX(), position.GetPositionY(), 2.0f)) { - return MoveTo(bot->GetMapId(), position.x, position.y, position.z, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } - bot->SetFacingTo(position.orientation); + bot->SetFacingTo(position.GetOrientation()); return true; } Creature* currentTarget = nullptr; std::string rtiName; - if (channelerSquare && channelerSquare->IsAlive()) + if (channelerSquare) { currentTarget = channelerSquare; rtiName = "square"; } - else if (channelerStar && channelerStar->IsAlive()) + else if (channelerStar) { currentTarget = channelerStar; rtiName = "star"; } - else if (channelerCircle && channelerCircle->IsAlive()) + else if (channelerCircle) { currentTarget = channelerCircle; rtiName = "circle"; @@ -70,7 +70,7 @@ bool MagtheridonMainTankAttackFirstThreeChannelersAction::Execute(Event event) bool MagtheridonFirstAssistTankAttackNWChannelerAction::Execute(Event event) { Creature* channelerDiamond = GetChanneler(bot, NORTHWEST_CHANNELER); - if (!channelerDiamond || !channelerDiamond->IsAlive()) + if (!channelerDiamond) return false; MarkTargetWithDiamond(bot, channelerDiamond); @@ -81,18 +81,18 @@ bool MagtheridonFirstAssistTankAttackNWChannelerAction::Execute(Event event) if (channelerDiamond->GetVictim() == bot) { - const Location& position = MagtheridonsLairLocations::NWChannelerTankPosition; + const Position& position = NW_CHANNELER_TANK_POSITION; const float maxDistance = 3.0f; + float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - if (bot->GetExactDist2d(position.x, position.y) > maxDistance) + if (distanceToPosition > maxDistance) { - float dX = position.x - bot->GetPositionX(); - float dY = position.y - bot->GetPositionY(); - float dist = sqrt(dX * dX + dY * dY); - float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; - float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveX = bot->GetPositionX() + (dX / distanceToPosition) * maxDistance; + float moveY = bot->GetPositionY() + (dY / distanceToPosition) * maxDistance; - return MoveTo(bot->GetMapId(), moveX, moveY, position.z, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } @@ -103,7 +103,7 @@ bool MagtheridonFirstAssistTankAttackNWChannelerAction::Execute(Event event) bool MagtheridonSecondAssistTankAttackNEChannelerAction::Execute(Event event) { Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); - if (!channelerTriangle || !channelerTriangle->IsAlive()) + if (!channelerTriangle) return false; MarkTargetWithTriangle(bot, channelerTriangle); @@ -114,18 +114,18 @@ bool MagtheridonSecondAssistTankAttackNEChannelerAction::Execute(Event event) if (channelerTriangle->GetVictim() == bot) { - const Location& position = MagtheridonsLairLocations::NEChannelerTankPosition; + const Position& position = NE_CHANNELER_TANK_POSITION; const float maxDistance = 3.0f; + float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - if (bot->GetExactDist2d(position.x, position.y) > maxDistance) + if (distanceToPosition > maxDistance) { - float dX = position.x - bot->GetPositionX(); - float dY = position.y - bot->GetPositionY(); - float dist = sqrt(dX * dX + dY * dY); - float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; - float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveX = bot->GetPositionX() + (dX / distanceToPosition) * maxDistance; + float moveY = bot->GetPositionY() + (dY / distanceToPosition) * maxDistance; - return MoveTo(bot->GetMapId(), moveX, moveY, position.z, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } @@ -175,7 +175,7 @@ bool MagtheridonMisdirectHellfireChannelers::Execute(Event event) switch (hunterIndex) { case 0: - if (mainTank && channelerStar && channelerStar->IsAlive() && + if (mainTank && channelerStar && channelerStar->GetVictim() != mainTank) { if (botAI->CanCastSpell("misdirection", mainTank)) @@ -190,7 +190,7 @@ bool MagtheridonMisdirectHellfireChannelers::Execute(Event event) break; case 1: - if (mainTank && channelerCircle && channelerCircle->IsAlive() && + if (mainTank && channelerCircle && channelerCircle->GetVictim() != mainTank) { if (botAI->CanCastSpell("misdirection", mainTank)) @@ -215,90 +215,69 @@ bool MagtheridonAssignDPSPriorityAction::Execute(Event event) { // Listed in order of priority Creature* channelerSquare = GetChanneler(bot, SOUTH_CHANNELER); - if (channelerSquare && channelerSquare->IsAlive()) + if (channelerSquare) { SetRtiTarget(botAI, "square", channelerSquare); if (bot->GetTarget() != channelerSquare->GetGUID()) - { - bot->SetSelection(channelerSquare->GetGUID()); return Attack(channelerSquare); - } return false; } Creature* channelerStar = GetChanneler(bot, WEST_CHANNELER); - if (channelerStar && channelerStar->IsAlive()) + if (channelerStar) { SetRtiTarget(botAI, "star", channelerStar); if (bot->GetTarget() != channelerStar->GetGUID()) - { - bot->SetSelection(channelerStar->GetGUID()); return Attack(channelerStar); - } return false; } Creature* channelerCircle = GetChanneler(bot, EAST_CHANNELER); - if (channelerCircle && channelerCircle->IsAlive()) + if (channelerCircle) { SetRtiTarget(botAI, "circle", channelerCircle); if (bot->GetTarget() != channelerCircle->GetGUID()) - { - bot->SetSelection(channelerCircle->GetGUID()); return Attack(channelerCircle); - } return false; } Creature* channelerDiamond = GetChanneler(bot, NORTHWEST_CHANNELER); - if (channelerDiamond && channelerDiamond->IsAlive()) + if (channelerDiamond) { SetRtiTarget(botAI, "diamond", channelerDiamond); if (bot->GetTarget() != channelerDiamond->GetGUID()) - { - bot->SetSelection(channelerDiamond->GetGUID()); return Attack(channelerDiamond); - } return false; } Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); - if (channelerTriangle && channelerTriangle->IsAlive()) + if (channelerTriangle) { SetRtiTarget(botAI, "triangle", channelerTriangle); if (bot->GetTarget() != channelerTriangle->GetGUID()) - { - bot->SetSelection(channelerTriangle->GetGUID()); return Attack(channelerTriangle); - } return false; } Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); if (magtheridon && !magtheridon->HasAura(SPELL_SHADOW_CAGE) && - (!channelerSquare || !channelerSquare->IsAlive()) && - (!channelerStar || !channelerStar->IsAlive()) && - (!channelerCircle || !channelerCircle->IsAlive()) && - (!channelerDiamond || !channelerDiamond->IsAlive()) && - (!channelerTriangle || !channelerTriangle->IsAlive())) + !channelerSquare && !channelerStar && !channelerCircle && + !channelerDiamond && !channelerTriangle) { SetRtiTarget(botAI, "cross", magtheridon); if (bot->GetTarget() != magtheridon->GetGUID()) - { - bot->SetSelection(magtheridon->GetGUID()); return Attack(magtheridon); - } } return false; @@ -343,15 +322,15 @@ bool MagtheridonWarlockCCBurningAbyssalAction::Execute(Event event) if (warlockIndex >= 0 && warlockIndex < abyssals.size()) { Unit* assignedAbyssal = abyssals[warlockIndex]; - if (!assignedAbyssal->HasAura(SPELL_BANISH) && botAI->CanCastSpell(SPELL_BANISH, assignedAbyssal, true)) + if (!botAI->HasAura("banish", assignedAbyssal) && botAI->CanCastSpell("banish", assignedAbyssal)) return botAI->CastSpell("banish", assignedAbyssal); } for (size_t i = warlocks.size(); i < abyssals.size(); ++i) { Unit* excessAbyssal = abyssals[i]; - if (!excessAbyssal->HasAura(SPELL_BANISH) && !excessAbyssal->HasAura(SPELL_FEAR) && - botAI->CanCastSpell(SPELL_FEAR, excessAbyssal, true)) + if (!botAI->HasAura("banish", excessAbyssal) && !botAI->HasAura("fear", excessAbyssal) && + botAI->CanCastSpell("fear", excessAbyssal)) return botAI->CastSpell("fear", excessAbyssal); } @@ -373,22 +352,20 @@ bool MagtheridonMainTankPositionBossAction::Execute(Event event) if (magtheridon->GetVictim() == bot) { - const Location& position = MagtheridonsLairLocations::MagtheridonTankPosition; + const Position& position = MAGTHERIDON_TANK_POSITION; const float maxDistance = 2.0f; + float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - if (bot->GetExactDist2d(position.x, position.y) > maxDistance) + if (distanceToPosition > maxDistance) { - float dX = position.x - bot->GetPositionX(); - float dY = position.y - bot->GetPositionY(); - float dist = sqrt(dX * dX + dY * dY); - float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; - float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveX = bot->GetPositionX() + (dX / distanceToPosition) * maxDistance; + float moveY = bot->GetPositionY() + (dY / distanceToPosition) * maxDistance; - return MoveTo(bot->GetMapId(), moveX, moveY, position.z, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, true); } - - bot->SetFacingTo(position.orientation); } return false; @@ -440,13 +417,13 @@ bool MagtheridonSpreadRangedAction::Execute(Event event) } bool isHealer = botAI->IsHeal(bot); - const Location& center = isHealer - ? MagtheridonsLairLocations::HealerSpreadPosition - : MagtheridonsLairLocations::RangedSpreadPosition; + const Position& center = isHealer + ? HEALER_SPREAD_POSITION + : RANGED_SPREAD_POSITION; float maxSpreadRadius = isHealer ? 15.0f : 20.0f; - float centerX = center.x; - float centerY = center.y; - float centerZ = bot->GetPositionZ(); + float centerX = center.GetPositionX(); + float centerY = center.GetPositionY(); + float centerZ = center.GetPositionZ(); const float radiusBuffer = 3.0f; if (!initialPositions.count(bot->GetGUID())) @@ -479,7 +456,7 @@ bool MagtheridonSpreadRangedAction::Execute(Event event) bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(bot->GetMapId(), destX, destY, destZ, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, destX, destY, destZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } hasReachedInitialPosition[bot->GetGUID()] = true; @@ -499,7 +476,7 @@ bool MagtheridonSpreadRangedAction::Execute(Event event) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(bot->GetMapId(), targetX, targetY, centerZ, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, targetX, targetY, centerZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } @@ -593,7 +570,7 @@ bool MagtheridonUseManticronCubeAction::HandleWaitingPhase(const CubeInfo& cubeI { bot->AttackStop(); bot->InterruptNonMeleeSpells(true); - return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, targetX, targetY, targetZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } @@ -603,7 +580,7 @@ bool MagtheridonUseManticronCubeAction::HandleWaitingPhase(const CubeInfo& cubeI float fallbackY = cubeInfo.y + sin(angle) * safeWaitDistance; float fallbackZ = bot->GetPositionZ(); - return MoveTo(bot->GetMapId(), fallbackX, fallbackY, fallbackZ, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, fallbackX, fallbackY, fallbackZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } @@ -638,7 +615,7 @@ bool MagtheridonUseManticronCubeAction::HandleCubeInteraction(const CubeInfo& cu bot->AttackStop(); bot->InterruptNonMeleeSpells(true); - return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, targetX, targetY, targetZ, false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); } @@ -663,14 +640,14 @@ bool MagtheridonManageTimersAndAssignmentsAction::Execute(Event event) magtheridon->FindCurrentSpellBySpellId(SPELL_BLAST_NOVA); bool lastBlastNova = lastBlastNovaState[instanceId]; - if (lastBlastNova && !blastNovaActive && IsInstanceTimerManager(botAI, bot)) + if (lastBlastNova && !blastNovaActive && IsMechanicTrackerBot(botAI, bot, MAGTHERIDON_MAP_ID, nullptr)) blastNovaTimer[instanceId] = now; lastBlastNovaState[instanceId] = blastNovaActive; if (!magtheridon->HasAura(SPELL_SHADOW_CAGE)) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, MAGTHERIDON_MAP_ID, nullptr)) { spreadWaitTimer.try_emplace(instanceId, now); blastNovaTimer.try_emplace(instanceId, now); @@ -679,11 +656,12 @@ bool MagtheridonManageTimersAndAssignmentsAction::Execute(Event event) } else { - MagtheridonSpreadRangedAction::initialPositions.clear(); - MagtheridonSpreadRangedAction::hasReachedInitialPosition.clear(); - botToCubeAssignment.clear(); + ObjectGuid guid = bot->GetGUID(); + MagtheridonSpreadRangedAction::initialPositions.erase(guid); + MagtheridonSpreadRangedAction::hasReachedInitialPosition.erase(guid); + botToCubeAssignment.erase(guid); - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, MAGTHERIDON_MAP_ID, nullptr)) { spreadWaitTimer.erase(instanceId); blastNovaTimer.erase(instanceId); diff --git a/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h b/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h index 6c4ed84c2..d47d06459 100644 --- a/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h +++ b/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h @@ -6,8 +6,6 @@ #include "AttackAction.h" #include "MovementActions.h" -using namespace MagtheridonHelpers; - class MagtheridonMainTankAttackFirstThreeChannelersAction : public AttackAction { public: @@ -85,8 +83,8 @@ public: private: bool HandleCubeRelease(Unit* magtheridon, GameObject* cube); bool ShouldActivateCubeLogic(Unit* magtheridon); - bool HandleWaitingPhase(const CubeInfo& cubeInfo); - bool HandleCubeInteraction(const CubeInfo& cubeInfo, GameObject* cube); + bool HandleWaitingPhase(const MagtheridonHelpers::CubeInfo& cubeInfo); + bool HandleCubeInteraction(const MagtheridonHelpers::CubeInfo& cubeInfo, GameObject* cube); }; class MagtheridonManageTimersAndAssignmentsAction : public Action diff --git a/src/Ai/Raid/Magtheridon/Multiplier/RaidMagtheridonMultipliers.cpp b/src/Ai/Raid/Magtheridon/Multiplier/RaidMagtheridonMultipliers.cpp index 9580fd923..55aaf90e9 100644 --- a/src/Ai/Raid/Magtheridon/Multiplier/RaidMagtheridonMultipliers.cpp +++ b/src/Ai/Raid/Magtheridon/Multiplier/RaidMagtheridonMultipliers.cpp @@ -8,6 +8,7 @@ #include "GenericSpellActions.h" #include "Playerbots.h" #include "WarlockActions.h" +#include "WipeAction.h" using namespace MagtheridonHelpers; @@ -24,10 +25,10 @@ float MagtheridonUseManticronCubeMultiplier::GetValue(Action* action) auto it = botToCubeAssignment.find(bot->GetGUID()); if (it != botToCubeAssignment.end()) { - if (dynamic_cast(action)) + if (dynamic_cast(action)) return 1.0f; - - return 0.0f; + else if (!dynamic_cast(action)) + return 0.0f; } } @@ -41,28 +42,31 @@ float MagtheridonWaitToAttackMultiplier::GetValue(Action* action) if (!magtheridon || magtheridon->HasAura(SPELL_SHADOW_CAGE)) return 1.0f; + if (botAI->IsMainTank(bot)) + return 1.0f; + const uint8 dpsWaitSeconds = 6; auto it = dpsWaitTimer.find(magtheridon->GetMap()->GetInstanceId()); if (it == dpsWaitTimer.end() || (time(nullptr) - it->second) < dpsWaitSeconds) { - if (!botAI->IsMainTank(bot) && (dynamic_cast(action) || - (!botAI->IsHeal(bot) && dynamic_cast(action)))) + if (dynamic_cast(action) || + (!botAI->IsHeal(bot) && dynamic_cast(action))) return 0.0f; } return 1.0f; } -// No tank assist for offtanks during the channeler phase -// So they don't try to pull channelers from each other or the main tank float MagtheridonDisableOffTankAssistMultiplier::GetValue(Action* action) { Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - Unit* channeler = AI_VALUE2(Unit*, "find target", "hellfire channeler"); if (!magtheridon) return 1.0f; + if (bot->GetVictim() == nullptr) + return 1.0f; + if ((botAI->IsAssistTankOfIndex(bot, 0) || botAI->IsAssistTankOfIndex(bot, 1)) && dynamic_cast(action)) return 0.0f; diff --git a/src/Ai/Raid/Magtheridon/Trigger/RaidMagtheridonTriggers.cpp b/src/Ai/Raid/Magtheridon/Trigger/RaidMagtheridonTriggers.cpp index 35442df6e..43aa3361f 100644 --- a/src/Ai/Raid/Magtheridon/Trigger/RaidMagtheridonTriggers.cpp +++ b/src/Ai/Raid/Magtheridon/Trigger/RaidMagtheridonTriggers.cpp @@ -18,7 +18,7 @@ bool MagtheridonNWChannelerEngagedByFirstAssistTankTrigger::IsActive() Creature* channelerDiamond = GetChanneler(bot, NORTHWEST_CHANNELER); return magtheridon && botAI->IsAssistTankOfIndex(bot, 0) && - channelerDiamond && channelerDiamond->IsAlive(); + channelerDiamond; } bool MagtheridonNEChannelerEngagedBySecondAssistTankTrigger::IsActive() @@ -27,7 +27,7 @@ bool MagtheridonNEChannelerEngagedBySecondAssistTankTrigger::IsActive() Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); return magtheridon && botAI->IsAssistTankOfIndex(bot, 1) && - channelerTriangle && channelerTriangle->IsAlive(); + channelerTriangle; } bool MagtheridonPullingWestAndEastChannelersTrigger::IsActive() @@ -38,8 +38,7 @@ bool MagtheridonPullingWestAndEastChannelersTrigger::IsActive() Creature* channelerCircle = GetChanneler(bot, EAST_CHANNELER); return magtheridon && bot->getClass() == CLASS_HUNTER && - ((channelerStar && channelerStar->IsAlive()) || - (channelerCircle && channelerCircle->IsAlive())); + (channelerStar || channelerCircle); } bool MagtheridonDeterminingKillOrderTrigger::IsActive() @@ -51,12 +50,11 @@ bool MagtheridonDeterminingKillOrderTrigger::IsActive() Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); if (!magtheridon || botAI->IsHeal(bot) || botAI->IsMainTank(bot) || - (botAI->IsAssistTankOfIndex(bot, 0) && channelerDiamond && channelerDiamond->IsAlive()) || - (botAI->IsAssistTankOfIndex(bot, 1) && channelerTriangle && channelerTriangle->IsAlive())) + (botAI->IsAssistTankOfIndex(bot, 0) && channelerDiamond) || + (botAI->IsAssistTankOfIndex(bot, 1) && channelerTriangle)) return false; - return (channeler && channeler->IsAlive()) || (magtheridon && - !magtheridon->HasAura(SPELL_SHADOW_CAGE)); + return channeler || (magtheridon && !magtheridon->HasAura(SPELL_SHADOW_CAGE)); } bool MagtheridonBurningAbyssalSpawnedTrigger::IsActive() @@ -84,10 +82,8 @@ bool MagtheridonBossEngagedByMainTankTrigger::IsActive() bool MagtheridonBossEngagedByRangedTrigger::IsActive() { Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - Unit* channeler = AI_VALUE2(Unit*, "find target", "hellfire channeler"); - return magtheridon && botAI->IsRanged(bot) && - !(channeler && channeler->IsAlive()); + return magtheridon && !magtheridon->HasAura(SPELL_SHADOW_CAGE) && botAI->IsRanged(bot); } bool MagtheridonIncomingBlastNovaTrigger::IsActive() @@ -122,7 +118,5 @@ bool MagtheridonIncomingBlastNovaTrigger::IsActive() bool MagtheridonNeedToManageTimersAndAssignmentsTrigger::IsActive() { - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - - return magtheridon; + return AI_VALUE2(Unit*, "find target", "magtheridon"); } diff --git a/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.cpp b/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.cpp index dc88d2a19..a56d4b85b 100644 --- a/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.cpp +++ b/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.cpp @@ -1,22 +1,18 @@ #include "RaidMagtheridonHelpers.h" #include "Creature.h" #include "GameObject.h" -#include "GroupReference.h" #include "Map.h" #include "ObjectGuid.h" #include "Playerbots.h" namespace MagtheridonHelpers { - namespace MagtheridonsLairLocations - { - const Location WaitingForMagtheridonPosition = { 1.359f, 2.048f, -0.406f, 3.135f }; - const Location MagtheridonTankPosition = { 22.827f, 2.105f, -0.406f, 3.135f }; - const Location NWChannelerTankPosition = { -11.764f, 30.818f, -0.411f, 0.0f }; - const Location NEChannelerTankPosition = { -12.490f, -26.211f, -0.411f, 0.0f }; - const Location RangedSpreadPosition = { -14.890f, 1.995f, -0.406f, 0.0f }; - const Location HealerSpreadPosition = { -2.265f, 1.874f, -0.404f, 0.0f }; - } + const Position WAITING_FOR_MAGTHERIDON_POSITION = { 1.359f, 2.048f, -0.406f, 3.135f }; + const Position MAGTHERIDON_TANK_POSITION = { 22.827f, 2.105f, -0.406f, 3.135f }; + const Position NW_CHANNELER_TANK_POSITION = { -11.764f, 30.818f, -0.411f, 0.0f }; + const Position NE_CHANNELER_TANK_POSITION = { -12.490f, -26.211f, -0.411f, 0.0f }; + const Position RANGED_SPREAD_POSITION = { -14.890f, 1.995f, -0.406f, 0.0f }; + const Position HEALER_SPREAD_POSITION = { -2.265f, 1.874f, -0.404f, 0.0f }; // Identify channelers by their database GUIDs Creature* GetChanneler(Player* bot, uint32 dbGuid) @@ -29,63 +25,11 @@ namespace MagtheridonHelpers if (it == map->GetCreatureBySpawnIdStore().end()) return nullptr; - return it->second; - } + Creature* channeler = it->second; + if (!channeler->IsAlive()) + return nullptr; - void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId) - { - Group* group = bot->GetGroup(); - if (!target || !group) - return; - - ObjectGuid currentGuid = group->GetTargetIcon(iconId); - if (currentGuid != target->GetGUID()) - group->SetTargetIcon(iconId, bot->GetGUID(), target->GetGUID()); - } - - void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target) - { - if (!target) - return; - - std::string currentRti = botAI->GetAiObjectContext()->GetValue("rti")->Get(); - Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("rti target")->Get(); - - if (currentRti != rtiName || currentTarget != target) - { - botAI->GetAiObjectContext()->GetValue("rti")->Set(rtiName); - botAI->GetAiObjectContext()->GetValue("rti target")->Set(target); - } - } - - void MarkTargetWithSquare(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::squareIndex); - } - - void MarkTargetWithStar(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::starIndex); - } - - void MarkTargetWithCircle(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::circleIndex); - } - - void MarkTargetWithDiamond(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::diamondIndex); - } - - void MarkTargetWithTriangle(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::triangleIndex); - } - - void MarkTargetWithCross(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::crossIndex); + return channeler; } const std::vector MANTICRON_CUBE_DB_GUIDS = { 43157, 43158, 43159, 43160, 43161 }; @@ -208,19 +152,4 @@ namespace MagtheridonHelpers return true; } - - bool IsInstanceTimerManager(PlayerbotAI* botAI, Player* bot) - { - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsDps(member) && GET_PLAYERBOT_AI(member)) - return member == bot; - } - } - - return true; - } } diff --git a/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.h b/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.h index f9f514de3..1335aff46 100644 --- a/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.h +++ b/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.h @@ -8,7 +8,6 @@ #include "Group.h" #include "ObjectGuid.h" #include "PlayerbotAI.h" -#include "RtiTargetValue.h" namespace MagtheridonHelpers { @@ -19,10 +18,6 @@ namespace MagtheridonHelpers SPELL_BLAST_NOVA = 30616, SPELL_SHADOW_GRASP = 30410, - // Warlock - SPELL_BANISH = 18647, - SPELL_FEAR = 6215, - // Hunter SPELL_MISDIRECTION = 35079, }; @@ -38,6 +33,7 @@ namespace MagtheridonHelpers GO_BLAZE = 181832, }; + constexpr uint32 MAGTHERIDON_MAP_ID = 544; constexpr uint32 SOUTH_CHANNELER = 90978; constexpr uint32 WEST_CHANNELER = 90979; constexpr uint32 NORTHWEST_CHANNELER = 90980; @@ -45,31 +41,14 @@ namespace MagtheridonHelpers constexpr uint32 NORTHEAST_CHANNELER = 90981; Creature* GetChanneler(Player* bot, uint32 dbGuid); - void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId); - void MarkTargetWithSquare(Player* bot, Unit* target); - void MarkTargetWithStar(Player* bot, Unit* target); - void MarkTargetWithCircle(Player* bot, Unit* target); - void MarkTargetWithDiamond(Player* bot, Unit* target); - void MarkTargetWithTriangle(Player* bot, Unit* target); - void MarkTargetWithCross(Player* bot, Unit* target); - void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target); bool IsSafeFromMagtheridonHazards(PlayerbotAI* botAI, Player* bot, float x, float y, float z); - bool IsInstanceTimerManager(PlayerbotAI* botAI, Player* bot); - struct Location - { - float x, y, z, orientation; - }; - - namespace MagtheridonsLairLocations - { - extern const Location WaitingForMagtheridonPosition; - extern const Location MagtheridonTankPosition; - extern const Location NWChannelerTankPosition; - extern const Location NEChannelerTankPosition; - extern const Location RangedSpreadPosition; - extern const Location HealerSpreadPosition; - } + extern const Position WAITING_FOR_MAGTHERIDON_POSITION; + extern const Position MAGTHERIDON_TANK_POSITION; + extern const Position NW_CHANNELER_TANK_POSITION; + extern const Position NE_CHANNELER_TANK_POSITION; + extern const Position RANGED_SPREAD_POSITION; + extern const Position HEALER_SPREAD_POSITION; struct CubeInfo { diff --git a/src/Ai/Raid/MoltenCore/RaidMcActionContext.h b/src/Ai/Raid/MoltenCore/RaidMcActionContext.h index 79a4a95a8..aaccb80d3 100644 --- a/src/Ai/Raid/MoltenCore/RaidMcActionContext.h +++ b/src/Ai/Raid/MoltenCore/RaidMcActionContext.h @@ -2,6 +2,7 @@ #define _PLAYERBOT_RAIDMCACTIONCONTEXT_H #include "Action.h" +#include "BossAuraActions.h" #include "NamedObjectContext.h" #include "RaidMcActions.h" diff --git a/src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h b/src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h index b74958919..a62d851dc 100644 --- a/src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h +++ b/src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h @@ -2,6 +2,7 @@ #define _PLAYERBOT_RAIDMCTRIGGERCONTEXT_H #include "AiObjectContext.h" +#include "BossAuraTriggers.h" #include "NamedObjectContext.h" #include "RaidMcTriggers.h" diff --git a/src/Ai/Raid/MoltenCore/RaidMcHelpers.h b/src/Ai/Raid/MoltenCore/Util/RaidMcHelpers.h similarity index 100% rename from src/Ai/Raid/MoltenCore/RaidMcHelpers.h rename to src/Ai/Raid/MoltenCore/Util/RaidMcHelpers.h diff --git a/src/Ai/Raid/RaidBossHelpers.cpp b/src/Ai/Raid/RaidBossHelpers.cpp new file mode 100644 index 000000000..bcb48294f --- /dev/null +++ b/src/Ai/Raid/RaidBossHelpers.cpp @@ -0,0 +1,142 @@ +#include "RaidBossHelpers.h" +#include "Playerbots.h" +#include "RtiTargetValue.h" + +// Functions to mark targets with raid target icons +// Note that these functions do not allow the player to change the icon during the encounter +void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId) +{ + if (!target) + return; + + if (Group* group = bot->GetGroup()) + { + ObjectGuid currentGuid = group->GetTargetIcon(iconId); + if (currentGuid != target->GetGUID()) + group->SetTargetIcon(iconId, bot->GetGUID(), target->GetGUID()); + } +} + +void MarkTargetWithSkull(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::skullIndex); +} + +void MarkTargetWithSquare(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::squareIndex); +} + +void MarkTargetWithStar(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::starIndex); +} + +void MarkTargetWithCircle(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::circleIndex); +} + +void MarkTargetWithDiamond(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::diamondIndex); +} + +void MarkTargetWithTriangle(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::triangleIndex); +} + +void MarkTargetWithCross(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::crossIndex); +} + +void MarkTargetWithMoon(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::moonIndex); +} + +// For bots to set their raid target icon to the specified icon on the specified target +void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target) +{ + if (!target) + return; + + std::string currentRti = botAI->GetAiObjectContext()->GetValue("rti")->Get(); + Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("rti target")->Get(); + + if (currentRti != rtiName || currentTarget != target) + { + botAI->GetAiObjectContext()->GetValue("rti")->Set(rtiName); + botAI->GetAiObjectContext()->GetValue("rti target")->Set(target); + } +} + +// Return the first alive DPS bot in the specified instance map, excluding any specified bot +// 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 (member != exclude) + 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) +{ + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("nearest npcs")->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 nullptr; +} + +// Return the nearest alive player (human or bot) within the specified radius +Unit* GetNearestPlayerInRadius(Player* bot, float radius) +{ + Unit* nearestPlayer = nullptr; + float nearestDistance = radius; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref != nullptr; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; + + float distance = bot->GetExactDist2d(member); + if (distance < nearestDistance) + { + nearestDistance = distance; + nearestPlayer = member; + } + } + } + + return nearestPlayer; +} diff --git a/src/Ai/Raid/RaidBossHelpers.h b/src/Ai/Raid/RaidBossHelpers.h new file mode 100644 index 000000000..15c60353e --- /dev/null +++ b/src/Ai/Raid/RaidBossHelpers.h @@ -0,0 +1,21 @@ +#ifndef _PLAYERBOT_RAIDBOSSHELPERS_H_ +#define _PLAYERBOT_RAIDBOSSHELPERS_H_ + +#include "AiObject.h" +#include "Unit.h" + +void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId); +void MarkTargetWithSkull(Player* bot, Unit* target); +void MarkTargetWithSquare(Player* bot, Unit* target); +void MarkTargetWithStar(Player* bot, Unit* target); +void MarkTargetWithCircle(Player* bot, Unit* target); +void MarkTargetWithDiamond(Player* bot, Unit* target); +void MarkTargetWithTriangle(Player* bot, Unit* target); +void MarkTargetWithCross(Player* bot, Unit* target); +void 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); +Unit* GetNearestPlayerInRadius(Player* bot, float radius); + +#endif diff --git a/src/Ai/Raid/RaidStrategyContext.h b/src/Ai/Raid/RaidStrategyContext.h index 4f7a63c7a..3c7971fb6 100644 --- a/src/Ai/Raid/RaidStrategyContext.h +++ b/src/Ai/Raid/RaidStrategyContext.h @@ -29,7 +29,7 @@ public: creators["wotlk-os"] = &RaidStrategyContext::wotlk_os; creators["wotlk-eoe"] = &RaidStrategyContext::wotlk_eoe; creators["voa"] = &RaidStrategyContext::voa; - creators["uld"] = &RaidStrategyContext::uld; + creators["ulduar"] = &RaidStrategyContext::ulduar; creators["onyxia"] = &RaidStrategyContext::onyxia; creators["icc"] = &RaidStrategyContext::icc; } @@ -45,7 +45,7 @@ private: static Strategy* wotlk_eoe(PlayerbotAI* botAI) { return new RaidEoEStrategy(botAI); } static Strategy* voa(PlayerbotAI* botAI) { return new RaidVoAStrategy(botAI); } static Strategy* onyxia(PlayerbotAI* botAI) { return new RaidOnyxiaStrategy(botAI); } - static Strategy* uld(PlayerbotAI* botAI) { return new RaidUlduarStrategy(botAI); } + static Strategy* ulduar(PlayerbotAI* botAI) { return new RaidUlduarStrategy(botAI); } static Strategy* icc(PlayerbotAI* botAI) { return new RaidIccStrategy(botAI); } }; diff --git a/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp b/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp index b20425d22..f7eca38db 100644 --- a/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp +++ b/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp @@ -11,7 +11,6 @@ #include "GameObject.h" #include "Group.h" #include "LastMovementValue.h" -#include "ObjectDefines.h" #include "ObjectGuid.h" #include "PlayerbotAI.h" #include "PlayerbotAIConfig.h" @@ -19,11 +18,9 @@ #include "Position.h" #include "RaidUlduarBossHelper.h" #include "RaidUlduarScripts.h" -#include "RaidUlduarStrategy.h" #include "RtiValue.h" #include "ScriptedCreature.h" #include "ServerFacade.h" -#include "SharedDefines.h" #include "Unit.h" #include "Vehicle.h" #include diff --git a/src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.cpp b/src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.cpp deleted file mode 100644 index 0a51ca407..000000000 --- a/src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.cpp +++ /dev/null @@ -1,28 +0,0 @@ -#include "RaidUlduarMultipliers.h" - -#include "ChooseTargetActions.h" -#include "DKActions.h" -#include "DruidActions.h" -#include "DruidBearActions.h" -#include "FollowActions.h" -#include "GenericActions.h" -#include "GenericSpellActions.h" -#include "HunterActions.h" -#include "MageActions.h" -#include "MovementActions.h" -#include "PaladinActions.h" -#include "PriestActions.h" -#include "RaidUlduarActions.h" -#include "ReachTargetActions.h" -#include "RogueActions.h" -#include "ScriptedCreature.h" -#include "ShamanActions.h" -#include "UseMeetingStoneAction.h" -#include "WarriorActions.h" - -float FlameLeviathanMultiplier::GetValue(Action* action) -{ - // if (dynamic_cast(action)) - // return 0.0f; - return 1.0f; -} diff --git a/src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.h b/src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.h deleted file mode 100644 index 6c9a468fc..000000000 --- a/src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.h +++ /dev/null @@ -1,17 +0,0 @@ - -#ifndef _PLAYERRBOT_RAIDULDUARMULTIPLIERS_H_ -#define _PLAYERRBOT_RAIDULDUARMULTIPLIERS_H_ - -#include "Multiplier.h" -#include "Ai/Raid/Ulduar/RaidUlduarBossHelper.h" - -class FlameLeviathanMultiplier : public Multiplier -{ -public: - FlameLeviathanMultiplier(PlayerbotAI* ai) : Multiplier(ai, "flame leviathan") {} - -public: - virtual float GetValue(Action* action); -}; - -#endif diff --git a/src/Ai/Raid/Ulduar/RaidUlduarBossHelper.h b/src/Ai/Raid/Ulduar/RaidUlduarBossHelper.h deleted file mode 100644 index 592fbc80e..000000000 --- a/src/Ai/Raid/Ulduar/RaidUlduarBossHelper.h +++ /dev/null @@ -1,169 +0,0 @@ -#ifndef _PLAYERBOT_RAIDULDUARBOSSHELPER_H -#define _PLAYERBOT_RAIDULDUARBOSSHELPER_H - -#include -#include -#include -#include -#include -#include - -#include "AiObject.h" -#include "AiObjectContext.h" -#include "EventMap.h" -#include "Log.h" -#include "ObjectGuid.h" -#include "Player.h" -#include "PlayerbotAI.h" -#include "Playerbots.h" -#include "ScriptedCreature.h" -#include "SharedDefines.h" - -const uint32 ULDUAR_MAP_ID = 603; - -class RazorscaleBossHelper : public AiObject -{ -public: - // Enums and constants specific to Razorscale - enum RazorscaleUnits : uint32 - { - UNIT_RAZORSCALE = 33186, - UNIT_DARK_RUNE_SENTINEL = 33846, - UNIT_DARK_RUNE_WATCHER = 33453, - UNIT_DARK_RUNE_GUARDIAN = 33388, - UNIT_DEVOURING_FLAME = 34188, - }; - - enum RazorscaleGameObjects : uint32 - { - GO_RAZORSCALE_HARPOON_1 = 194519, - GO_RAZORSCALE_HARPOON_2 = 194541, - GO_RAZORSCALE_HARPOON_3 = 194542, - GO_RAZORSCALE_HARPOON_4 = 194543, - }; - - enum RazorscaleSpells : uint32 - { - SPELL_CHAIN_1 = 49679, - SPELL_CHAIN_2 = 49682, - SPELL_CHAIN_3 = 49683, - SPELL_CHAIN_4 = 49684, - SPELL_SENTINEL_WHIRLWIND = 63806, - SPELL_STUN_AURA = 62794, - SPELL_FUSEARMOR = 64771 - }; - - static constexpr uint32 FUSEARMOR_THRESHOLD = 2; - - // Constants for arena parameters - static constexpr float RAZORSCALE_FLYING_Z_THRESHOLD = 440.0f; - static constexpr float RAZORSCALE_ARENA_CENTER_X = 587.54f; - static constexpr float RAZORSCALE_ARENA_CENTER_Y = -175.04f; - static constexpr float RAZORSCALE_ARENA_RADIUS = 30.0f; - - // Harpoon cooldown (seconds) - static constexpr time_t HARPOON_COOLDOWN_DURATION = 5; - - // Structure for harpoon data - struct HarpoonData - { - uint32 gameObjectEntry; - uint32 chainSpellId; - }; - - explicit RazorscaleBossHelper(PlayerbotAI* botAI) - : AiObject(botAI), _boss(nullptr) {} - - bool UpdateBossAI(); - Unit* GetBoss() const; - - bool IsGroundPhase() const; - bool IsFlyingPhase() const; - - bool IsHarpoonFired(uint32 chainSpellId) const; - static bool IsHarpoonReady(GameObject* harpoonGO); - static void SetHarpoonOnCooldown(GameObject* harpoonGO); - GameObject* FindNearestHarpoon(float x, float y, float z) const; - - static const std::vector& GetHarpoonData(); - - void AssignRolesBasedOnHealth(); - bool AreRolesAssigned() const; - bool CanSwapRoles() const; - -private: - Unit* _boss; - - // A map to track the last role swap *per bot* by their GUID - static std::unordered_map _lastRoleSwapTime; - - // The cooldown that applies to every bot - static const std::time_t _roleSwapCooldown = 10; - - static std::unordered_map _harpoonCooldowns; -}; - -// template -// class GenericBossHelper : public AiObject -// { -// public: -// GenericBossHelper(PlayerbotAI* botAI, std::string name) : AiObject(botAI), _name(name) {} -// virtual bool UpdateBossAI() -// { -// if (!bot->IsInCombat()) -// { -// _unit = nullptr; -// } -// if (_unit && (!_unit->IsInWorld() || !_unit->IsAlive())) -// { -// _unit = nullptr; -// } -// if (!_unit) -// { -// _unit = AI_VALUE2(Unit*, "find target", _name); -// if (!_unit) -// { -// return false; -// } -// _target = _unit->ToCreature(); -// if (!_target) -// { -// return false; -// } -// _ai = dynamic_cast(_target->GetAI()); -// if (!_ai) -// { -// return false; -// } -// _event_map = &_ai->events; -// if (!_event_map) -// { -// return false; -// } -// } -// if (!_event_map) -// { -// return false; -// } -// _timer = _event_map->GetTimer(); -// return true; -// } -// virtual void Reset() -// { -// _unit = nullptr; -// _target = nullptr; -// _ai = nullptr; -// _event_map = nullptr; -// _timer = 0; -// } - -// protected: -// std::string _name; -// Unit* _unit = nullptr; -// Creature* _target = nullptr; -// BossAiType* _ai = nullptr; -// EventMap* _event_map = nullptr; -// uint32 _timer = 0; -// }; - -#endif diff --git a/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.cpp b/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.cpp index 3b9a426cc..0a1b76a40 100644 --- a/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.cpp +++ b/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.cpp @@ -1,7 +1,5 @@ #include "RaidUlduarStrategy.h" -#include "RaidUlduarMultipliers.h" - void RaidUlduarStrategy::InitTriggers(std::vector& triggers) { // @@ -316,8 +314,3 @@ void RaidUlduarStrategy::InitTriggers(std::vector& triggers) "yogg-saron phase 3 positioning trigger", { NextAction("yogg-saron phase 3 positioning action", ACTION_RAID) })); } - -void RaidUlduarStrategy::InitMultipliers(std::vector& multipliers) -{ - multipliers.push_back(new FlameLeviathanMultiplier(botAI)); -} diff --git a/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h b/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h index 81bb93c3a..bb2feefe4 100644 --- a/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h +++ b/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h @@ -3,16 +3,14 @@ #define _PLAYERBOT_RAIDULDUARSTRATEGY_H #include "AiObjectContext.h" -#include "Multiplier.h" #include "Strategy.h" class RaidUlduarStrategy : public Strategy { public: RaidUlduarStrategy(PlayerbotAI* ai) : Strategy(ai) {} - virtual std::string const getName() override { return "uld"; } + virtual std::string const getName() override { return "ulduar"; } virtual void InitTriggers(std::vector& triggers) override; - virtual void InitMultipliers(std::vector& multipliers) override; }; #endif diff --git a/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.cpp b/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.cpp index a4bc2cf9a..f14a51311 100644 --- a/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.cpp +++ b/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.cpp @@ -1634,7 +1634,7 @@ bool VezaxShadowCrashTrigger::IsActive() return false; } - return botAI->HasAura(SPELL_SHADOW_CRASH, bot); + return botAI->HasAura(SPELL_VEZAX_SHADOW_CRASH, bot); } bool VezaxMarkOfTheFacelessTrigger::IsActive() diff --git a/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.h b/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.h index 129c3d4db..7f8cb51a8 100644 --- a/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.h +++ b/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.h @@ -3,187 +3,9 @@ #include "EventMap.h" #include "GenericTriggers.h" -#include "PlayerbotAIConfig.h" #include "RaidUlduarBossHelper.h" #include "Trigger.h" -enum UlduarIDs -{ - // Iron Assembly - SPELL_LIGHTNING_TENDRILS_10_MAN = 61887, - SPELL_LIGHTNING_TENDRILS_25_MAN = 63486, - SPELL_OVERLOAD_10_MAN = 61869, - SPELL_OVERLOAD_25_MAN = 63481, - SPELL_OVERLOAD_10_MAN_2 = 63485, - SPELL_OVERLOAD_25_MAN_2 = 61886, - SPELL_RUNE_OF_POWER = 64320, - - // Kologarn - NPC_RIGHT_ARM = 32934, - NPC_RUBBLE = 33768, - SPELL_CRUNCH_ARMOR = 64002, - - SPELL_FOCUSED_EYEBEAM_10_2 = 63346, - SPELL_FOCUSED_EYEBEAM_10 = 63347, - SPELL_FOCUSED_EYEBEAM_25_2 = 63976, - SPELL_FOCUSED_EYEBEAM_25 = 63977, - - // Hodir - NPC_SNOWPACKED_ICICLE = 33174, - NPC_TOASTY_FIRE = 33342, - SPELL_FLASH_FREEZE = 61968, - SPELL_BITING_COLD_PLAYER_AURA = 62039, - - // Freya - NPC_SNAPLASHER = 32916, - NPC_STORM_LASHER = 32919, - NPC_DETONATING_LASHER = 32918, - NPC_ANCIENT_WATER_SPIRIT = 33202, - NPC_ANCIENT_CONSERVATOR = 33203, - NPC_HEALTHY_SPORE = 33215, - NPC_EONARS_GIFT = 33228, - GOBJECT_NATURE_BOMB = 194902, - - // Thorim - NPC_DARK_RUNE_ACOLYTE_I = 32886, - NPC_CAPTURED_MERCENARY_SOLDIER_ALLY = 32885, - NPC_CAPTURED_MERCENARY_SOLDIER_HORDE = 32883, - NPC_CAPTURED_MERCENARY_CAPTAIN_ALLY = 32908, - NPC_CAPTURED_MERCENARY_CAPTAIN_HORDE = 32907, - NPC_JORMUNGAR_BEHEMOT = 32882, - NPC_DARK_RUNE_WARBRINGER = 32877, - NPC_DARK_RUNE_EVOKER = 32878, - NPC_DARK_RUNE_CHAMPION = 32876, - NPC_DARK_RUNE_COMMONER = 32904, - NPC_IRON_RING_GUARD = 32874, - NPC_RUNIC_COLOSSUS = 32872, - NPC_ANCIENT_RUNE_GIANT = 32873, - NPC_DARK_RUNE_ACOLYTE_G = 33110, - NPC_IRON_HONOR_GUARD = 32875, - SPELL_UNBALANCING_STRIKE = 62130, - - // Mimiron - NPC_LEVIATHAN_MKII = 33432, - NPC_VX001 = 33651, - NPC_AERIAL_COMMAND_UNIT = 33670, - NPC_BOMB_BOT = 33836, - NPC_ROCKET_STRIKE_N = 34047, - NPC_ASSAULT_BOT = 34057, - NPC_PROXIMITY_MINE = 34362, - SPELL_P3WX2_LASER_BARRAGE_1 = 63293, - SPELL_P3WX2_LASER_BARRAGE_2 = 63297, - SPELL_SPINNING_UP = 63414, - SPELL_SHOCK_BLAST = 63631, - SPELL_P3WX2_LASER_BARRAGE_3 = 64042, - SPELL_P3WX2_LASER_BARRAGE_AURA_1 = 63274, - SPELL_P3WX2_LASER_BARRAGE_AURA_2 = 63300, - - // General Vezax - SPELL_MARK_OF_THE_FACELESS = 63276, - SPELL_SHADOW_CRASH = 63277, - - // Yogg-Saron - ACTION_ILLUSION_DRAGONS = 1, - ACTION_ILLUSION_ICECROWN = 2, - ACTION_ILLUSION_STORMWIND = 3, - NPC_GUARDIAN_OF_YS = 33136, - NPC_YOGG_SARON = 33288, - NPC_OMINOUS_CLOUD = 33292, - NPC_RUBY_CONSORT = 33716, - NPC_AZURE_CONSORT = 33717, - NPC_BRONZE_CONSORT = 33718, - NPC_EMERALD_CONSORT = 33719, - NPC_OBSIDIAN_CONSORT = 33720, - NPC_ALEXTRASZA = 33536, - NPC_MALYGOS_ILLUSION = 33535, - NPC_NELTHARION = 33523, - NPC_YSERA = 33495, - GO_DRAGON_SOUL = 194462, - NPC_SARA_PHASE_1 = 33134, - NPC_LICH_KING_ILLUSION = 33441, - NPC_IMMOLATED_CHAMPION = 33442, - NPC_SUIT_OF_ARMOR = 33433, - NPC_GARONA = 33436, - NPC_KING_LLANE = 33437, - NPC_DEATHSWORN_ZEALOT = 33567, - NPC_INFLUENCE_TENTACLE = 33943, - NPC_DEATH_ORB = 33882, - NPC_BRAIN = 33890, - NPC_CRUSHER_TENTACLE = 33966, - NPC_CONSTRICTOR_TENTACLE = 33983, - NPC_CORRUPTOR_TENTACLE = 33985, - NPC_IMMORTAL_GUARDIAN = 33988, - NPC_LAUGHING_SKULL = 33990, - NPC_SANITY_WELL = 33991, - NPC_DESCEND_INTO_MADNESS = 34072, - NPC_MARKED_IMMORTAL_GUARDIAN = 36064, - SPELL_SANITY = 63050, - SPELL_BRAIN_LINK = 63802, - SPELL_MALADY_OF_THE_MIND = 63830, - SPELL_SHADOW_BARRIER = 63894, - SPELL_TELEPORT_TO_CHAMBER = 63997, - SPELL_TELEPORT_TO_ICECROWN = 63998, - SPELL_TELEPORT_TO_STORMWIND = 63989, - SPELL_TELEPORT_BACK = 63992, - SPELL_CANCEL_ILLUSION_AURA = 63993, - SPELL_INDUCE_MADNESS = 64059, - SPELL_LUNATIC_GAZE_YS = 64163, - GO_FLEE_TO_THE_SURFACE_PORTAL = 194625, - - // Buffs - SPELL_FROST_TRAP = 13809 -}; - -const float ULDUAR_KOLOGARN_AXIS_Z_PATHING_ISSUE_DETECT = 420.0f; -const float ULDUAR_KOLOGARN_EYEBEAM_RADIUS = 3.0f; -const float ULDUAR_THORIM_AXIS_Z_FLOOR_THRESHOLD = 429.6094f; -const float ULDUAR_THORIM_AXIS_Z_PATHING_ISSUE_DETECT = 410.0f; -const float ULDUAR_AURIAYA_AXIS_Z_PATHING_ISSUE_DETECT = 410.0f; -const float ULDUAR_YOGG_SARON_BOSS_ROOM_AXIS_Z_PATHING_ISSUE_DETECT = 300.0f; -const float ULDUAR_YOGG_SARON_BRAIN_ROOM_AXIS_Z_PATHING_ISSUE_DETECT = 200.0f; -const float ULDUAR_YOGG_SARON_STORMWIND_KEEPER_RADIUS = 150.0f; -const float ULDUAR_YOGG_SARON_ICECROWN_CITADEL_RADIUS = 150.0f; -const float ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_RADIUS = 150.0f; -const float ULDUAR_YOGG_SARON_BRAIN_ROOM_RADIUS = 50.0f; - -const Position ULDUAR_THORIM_NEAR_ARENA_CENTER = Position(2134.9854f, -263.11853f, 419.8465f); -const Position ULDUAR_THORIM_NEAR_ENTRANCE_POSITION = Position(2172.4355f, -258.27957f, 418.47162f); -const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_6_YARDS_1 = Position(2237.6187f, -265.08844f, 412.17548f); -const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_6_YARDS_2 = Position(2237.2498f, -275.81122f, 412.17548f); -const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_5_YARDS_1 = Position(2236.895f, -294.62448f, 412.1348f); -const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_1 = Position(2242.1162f, -310.15308f, 412.1348f); -const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_2 = Position(2242.018f, -318.66003f, 412.1348f); -const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_3 = Position(2242.1904f, -329.0533f, 412.1348f); -const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_6_YARDS_1 = Position(2219.5417f, -264.77167f, 412.17548f); -const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_6_YARDS_2 = Position(2217.446f, -275.85248f, 412.17548f); -const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_5_YARDS_1 = Position(2217.8877f, -295.01193f, 412.13434f); -const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_1 = Position(2212.193f, -307.44992f, 412.1348f); -const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_2 = Position(2212.1353f, -318.20795f, 412.1348f); -const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_3 = Position(2212.1956f, -328.0144f, 412.1348f); -const Position ULDUAR_THORIM_JUMP_END_POINT = Position(2137.8818f, -278.18942f, 419.66653f); -const Position ULDUAR_THORIM_PHASE2_TANK_SPOT = Position(2134.8572f, -287.0291f, 419.4935f); -const Position ULDUAR_THORIM_PHASE2_RANGE1_SPOT = Position(2112.8752f, -267.69305f, 419.52814f); -const Position ULDUAR_THORIM_PHASE2_RANGE2_SPOT = Position(2134.1296f, -257.3316f, 419.8462f); -const Position ULDUAR_THORIM_PHASE2_RANGE3_SPOT = Position(2156.798f, -267.57434f, 419.52722f); -const Position ULDUAR_MIMIRON_PHASE2_SIDE1RANGE_SPOT = Position(2753.708f, 2583.9617f, 364.31357f); -const Position ULDUAR_MIMIRON_PHASE2_SIDE1MELEE_SPOT = Position(2746.9792f, 2573.6716f, 364.31357f); -const Position ULDUAR_MIMIRON_PHASE2_SIDE2RANGE_SPOT = Position(2727.7224f, 2569.527f, 364.31357f); -const Position ULDUAR_MIMIRON_PHASE2_SIDE2MELEE_SPOT = Position(2739.4746f, 2569.4106f, 364.31357f); -const Position ULDUAR_MIMIRON_PHASE2_SIDE3RANGE_SPOT = Position(2754.1294f, 2553.9954f, 364.31357f); -const Position ULDUAR_MIMIRON_PHASE2_SIDE3MELEE_SPOT = Position(2746.8513f, 2565.4263f, 364.31357f); -const Position ULDUAR_MIMIRON_PHASE4_TANK_SPOT = Position(2744.5754f, 2570.8657f, 364.3138f); -const Position ULDUAR_VEZAX_MARK_OF_THE_FACELESS_SPOT = Position(1913.6501f, 122.93989f, 342.38083f); -const Position ULDUAR_YOGG_SARON_MIDDLE = Position(1980.28f, -25.5868f, 329.397f); -const Position ULDUAR_YOGG_SARON_STORMWIND_KEEPER_MIDDLE = Position(1927.1511f, 68.507256f, 242.37657f); -const Position ULDUAR_YOGG_SARON_ICECROWN_CITADEL_MIDDLE = Position(1925.6553f, -121.59296f, 239.98965f); -const Position ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_MIDDLE = Position(2104.5667f, -25.509348f, 242.64679f); -const Position ULDUAR_YOGG_SARON_BRAIN_ROOM_MIDDLE = Position(1980.1971f, -27.854689f, 236.06789f); -const Position ULDUAR_YOGG_SARON_STORMWIND_KEEPER_ENTRANCE = Position(1954.06f, 21.66f, 239.71f); -const Position ULDUAR_YOGG_SARON_ICECROWN_CITADEL_ENTRANCE = Position(1950.11f, -79.284f, 239.98982f); -const Position ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_ENTRANCE = Position(2048.63f, -25.5f, 239.72f); -const Position ULDUAR_YOGG_SARON_PHASE_3_MELEE_SPOT = Position(1998.5377f, -22.90317f, 324.8895f); -const Position ULDUAR_YOGG_SARON_PHASE_3_RANGED_SPOT = Position(2018.7628f, -18.896868f, 327.07245f); - // // Flame Levi // diff --git a/src/Ai/Raid/Ulduar/RaidUlduarBossHelper.cpp b/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.cpp similarity index 63% rename from src/Ai/Raid/Ulduar/RaidUlduarBossHelper.cpp rename to src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.cpp index 72333a079..fd6711cf0 100644 --- a/src/Ai/Raid/Ulduar/RaidUlduarBossHelper.cpp +++ b/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.cpp @@ -1,4 +1,3 @@ -#include "ChatHelper.h" #include "RaidUlduarBossHelper.h" #include "ObjectAccessor.h" #include "GameObject.h" @@ -9,6 +8,44 @@ #include "Playerbots.h" #include "World.h" +const Position ULDUAR_THORIM_NEAR_ARENA_CENTER = Position(2134.9854f, -263.11853f, 419.8465f); +const Position ULDUAR_THORIM_NEAR_ENTRANCE_POSITION = Position(2172.4355f, -258.27957f, 418.47162f); +const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_6_YARDS_1 = Position(2237.6187f, -265.08844f, 412.17548f); +const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_6_YARDS_2 = Position(2237.2498f, -275.81122f, 412.17548f); +const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_5_YARDS_1 = Position(2236.895f, -294.62448f, 412.1348f); +const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_1 = Position(2242.1162f, -310.15308f, 412.1348f); +const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_2 = Position(2242.018f, -318.66003f, 412.1348f); +const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_3 = Position(2242.1904f, -329.0533f, 412.1348f); +const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_6_YARDS_1 = Position(2219.5417f, -264.77167f, 412.17548f); +const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_6_YARDS_2 = Position(2217.446f, -275.85248f, 412.17548f); +const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_5_YARDS_1 = Position(2217.8877f, -295.01193f, 412.13434f); +const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_1 = Position(2212.193f, -307.44992f, 412.1348f); +const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_2 = Position(2212.1353f, -318.20795f, 412.1348f); +const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_3 = Position(2212.1956f, -328.0144f, 412.1348f); +const Position ULDUAR_THORIM_JUMP_END_POINT = Position(2137.8818f, -278.18942f, 419.66653f); +const Position ULDUAR_THORIM_PHASE2_TANK_SPOT = Position(2134.8572f, -287.0291f, 419.4935f); +const Position ULDUAR_THORIM_PHASE2_RANGE1_SPOT = Position(2112.8752f, -267.69305f, 419.52814f); +const Position ULDUAR_THORIM_PHASE2_RANGE2_SPOT = Position(2134.1296f, -257.3316f, 419.8462f); +const Position ULDUAR_THORIM_PHASE2_RANGE3_SPOT = Position(2156.798f, -267.57434f, 419.52722f); +const Position ULDUAR_MIMIRON_PHASE2_SIDE1RANGE_SPOT = Position(2753.708f, 2583.9617f, 364.31357f); +const Position ULDUAR_MIMIRON_PHASE2_SIDE1MELEE_SPOT = Position(2746.9792f, 2573.6716f, 364.31357f); +const Position ULDUAR_MIMIRON_PHASE2_SIDE2RANGE_SPOT = Position(2727.7224f, 2569.527f, 364.31357f); +const Position ULDUAR_MIMIRON_PHASE2_SIDE2MELEE_SPOT = Position(2739.4746f, 2569.4106f, 364.31357f); +const Position ULDUAR_MIMIRON_PHASE2_SIDE3RANGE_SPOT = Position(2754.1294f, 2553.9954f, 364.31357f); +const Position ULDUAR_MIMIRON_PHASE2_SIDE3MELEE_SPOT = Position(2746.8513f, 2565.4263f, 364.31357f); +const Position ULDUAR_MIMIRON_PHASE4_TANK_SPOT = Position(2744.5754f, 2570.8657f, 364.3138f); +const Position ULDUAR_VEZAX_MARK_OF_THE_FACELESS_SPOT = Position(1913.6501f, 122.93989f, 342.38083f); +const Position ULDUAR_YOGG_SARON_MIDDLE = Position(1980.28f, -25.5868f, 329.397f); +const Position ULDUAR_YOGG_SARON_STORMWIND_KEEPER_MIDDLE = Position(1927.1511f, 68.507256f, 242.37657f); +const Position ULDUAR_YOGG_SARON_ICECROWN_CITADEL_MIDDLE = Position(1925.6553f, -121.59296f, 239.98965f); +const Position ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_MIDDLE = Position(2104.5667f, -25.509348f, 242.64679f); +const Position ULDUAR_YOGG_SARON_BRAIN_ROOM_MIDDLE = Position(1980.1971f, -27.854689f, 236.06789f); +const Position ULDUAR_YOGG_SARON_STORMWIND_KEEPER_ENTRANCE = Position(1954.06f, 21.66f, 239.71f); +const Position ULDUAR_YOGG_SARON_ICECROWN_CITADEL_ENTRANCE = Position(1950.11f, -79.284f, 239.98982f); +const Position ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_ENTRANCE = Position(2048.63f, -25.5f, 239.72f); +const Position ULDUAR_YOGG_SARON_PHASE_3_MELEE_SPOT = Position(1998.5377f, -22.90317f, 324.8895f); +const Position ULDUAR_YOGG_SARON_PHASE_3_RANGED_SPOT = Position(2018.7628f, -18.896868f, 327.07245f); + // Prevent harpoon spam std::unordered_map RazorscaleBossHelper::_harpoonCooldowns; // Prevent role assignment spam diff --git a/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.h b/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.h new file mode 100644 index 000000000..b3d49ff58 --- /dev/null +++ b/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.h @@ -0,0 +1,341 @@ +#ifndef _PLAYERBOT_RAIDULDUARBOSSHELPER_H +#define _PLAYERBOT_RAIDULDUARBOSSHELPER_H + +#include +#include +#include + +#include "AiObject.h" +#include "AiObjectContext.h" +#include "EventMap.h" +#include "ObjectGuid.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "ScriptedCreature.h" + +constexpr uint32 ULDUAR_MAP_ID = 603; + +enum UlduarIDs +{ + // Iron Assembly + SPELL_LIGHTNING_TENDRILS_10_MAN = 61887, + SPELL_LIGHTNING_TENDRILS_25_MAN = 63486, + SPELL_OVERLOAD_10_MAN = 61869, + SPELL_OVERLOAD_25_MAN = 63481, + SPELL_OVERLOAD_10_MAN_2 = 63485, + SPELL_OVERLOAD_25_MAN_2 = 61886, + SPELL_RUNE_OF_POWER = 64320, + + // Kologarn + NPC_RIGHT_ARM = 32934, + NPC_RUBBLE = 33768, + SPELL_CRUNCH_ARMOR = 64002, + + SPELL_FOCUSED_EYEBEAM_10_2 = 63346, + SPELL_FOCUSED_EYEBEAM_10 = 63347, + SPELL_FOCUSED_EYEBEAM_25_2 = 63976, + SPELL_FOCUSED_EYEBEAM_25 = 63977, + + // Hodir + NPC_SNOWPACKED_ICICLE = 33174, + NPC_TOASTY_FIRE = 33342, + SPELL_FLASH_FREEZE = 61968, + SPELL_BITING_COLD_PLAYER_AURA = 62039, + + // Freya + NPC_SNAPLASHER = 32916, + NPC_STORM_LASHER = 32919, + NPC_DETONATING_LASHER = 32918, + NPC_ANCIENT_WATER_SPIRIT = 33202, + NPC_ANCIENT_CONSERVATOR = 33203, + NPC_HEALTHY_SPORE = 33215, + NPC_EONARS_GIFT = 33228, + GOBJECT_NATURE_BOMB = 194902, + + // Thorim + NPC_DARK_RUNE_ACOLYTE_I = 32886, + NPC_CAPTURED_MERCENARY_SOLDIER_ALLY = 32885, + NPC_CAPTURED_MERCENARY_SOLDIER_HORDE = 32883, + NPC_CAPTURED_MERCENARY_CAPTAIN_ALLY = 32908, + NPC_CAPTURED_MERCENARY_CAPTAIN_HORDE = 32907, + NPC_JORMUNGAR_BEHEMOT = 32882, + NPC_DARK_RUNE_WARBRINGER = 32877, + NPC_DARK_RUNE_EVOKER = 32878, + NPC_DARK_RUNE_CHAMPION = 32876, + NPC_DARK_RUNE_COMMONER = 32904, + NPC_IRON_RING_GUARD = 32874, + NPC_RUNIC_COLOSSUS = 32872, + NPC_ANCIENT_RUNE_GIANT = 32873, + NPC_DARK_RUNE_ACOLYTE_G = 33110, + NPC_IRON_HONOR_GUARD = 32875, + SPELL_UNBALANCING_STRIKE = 62130, + + // Mimiron + NPC_LEVIATHAN_MKII = 33432, + NPC_VX001 = 33651, + NPC_AERIAL_COMMAND_UNIT = 33670, + NPC_BOMB_BOT = 33836, + NPC_ROCKET_STRIKE_N = 34047, + NPC_ASSAULT_BOT = 34057, + NPC_PROXIMITY_MINE = 34362, + SPELL_P3WX2_LASER_BARRAGE_1 = 63293, + SPELL_P3WX2_LASER_BARRAGE_2 = 63297, + SPELL_SPINNING_UP = 63414, + SPELL_SHOCK_BLAST = 63631, + SPELL_P3WX2_LASER_BARRAGE_3 = 64042, + SPELL_P3WX2_LASER_BARRAGE_AURA_1 = 63274, + SPELL_P3WX2_LASER_BARRAGE_AURA_2 = 63300, + + // General Vezax + SPELL_MARK_OF_THE_FACELESS = 63276, + SPELL_VEZAX_SHADOW_CRASH = 63277, + + // Yogg-Saron + ACTION_ILLUSION_DRAGONS = 1, + ACTION_ILLUSION_ICECROWN = 2, + ACTION_ILLUSION_STORMWIND = 3, + NPC_GUARDIAN_OF_YS = 33136, + NPC_YOGG_SARON = 33288, + NPC_OMINOUS_CLOUD = 33292, + NPC_RUBY_CONSORT = 33716, + NPC_AZURE_CONSORT = 33717, + NPC_BRONZE_CONSORT = 33718, + NPC_EMERALD_CONSORT = 33719, + NPC_OBSIDIAN_CONSORT = 33720, + NPC_ALEXTRASZA = 33536, + NPC_MALYGOS_ILLUSION = 33535, + NPC_NELTHARION = 33523, + NPC_YSERA = 33495, + GO_DRAGON_SOUL = 194462, + NPC_SARA_PHASE_1 = 33134, + NPC_LICH_KING_ILLUSION = 33441, + NPC_IMMOLATED_CHAMPION = 33442, + NPC_SUIT_OF_ARMOR = 33433, + NPC_GARONA = 33436, + NPC_KING_LLANE = 33437, + NPC_DEATHSWORN_ZEALOT = 33567, + NPC_INFLUENCE_TENTACLE = 33943, + NPC_DEATH_ORB = 33882, + NPC_BRAIN = 33890, + NPC_CRUSHER_TENTACLE = 33966, + NPC_CONSTRICTOR_TENTACLE = 33983, + NPC_CORRUPTOR_TENTACLE = 33985, + NPC_IMMORTAL_GUARDIAN = 33988, + NPC_LAUGHING_SKULL = 33990, + NPC_SANITY_WELL = 33991, + NPC_DESCEND_INTO_MADNESS = 34072, + NPC_MARKED_IMMORTAL_GUARDIAN = 36064, + SPELL_SANITY = 63050, + SPELL_BRAIN_LINK = 63802, + SPELL_MALADY_OF_THE_MIND = 63830, + SPELL_SHADOW_BARRIER = 63894, + SPELL_TELEPORT_TO_CHAMBER = 63997, + SPELL_TELEPORT_TO_ICECROWN = 63998, + SPELL_TELEPORT_TO_STORMWIND = 63989, + SPELL_TELEPORT_BACK = 63992, + SPELL_CANCEL_ILLUSION_AURA = 63993, + SPELL_INDUCE_MADNESS = 64059, + SPELL_LUNATIC_GAZE_YS = 64163, + GO_FLEE_TO_THE_SURFACE_PORTAL = 194625, + + // Buffs + SPELL_FROST_TRAP = 13809 +}; + +constexpr float ULDUAR_KOLOGARN_AXIS_Z_PATHING_ISSUE_DETECT = 420.0f; +constexpr float ULDUAR_KOLOGARN_EYEBEAM_RADIUS = 3.0f; +constexpr float ULDUAR_THORIM_AXIS_Z_FLOOR_THRESHOLD = 429.6094f; +constexpr float ULDUAR_THORIM_AXIS_Z_PATHING_ISSUE_DETECT = 410.0f; +constexpr float ULDUAR_AURIAYA_AXIS_Z_PATHING_ISSUE_DETECT = 410.0f; +constexpr float ULDUAR_YOGG_SARON_BOSS_ROOM_AXIS_Z_PATHING_ISSUE_DETECT = 300.0f; +constexpr float ULDUAR_YOGG_SARON_BRAIN_ROOM_AXIS_Z_PATHING_ISSUE_DETECT = 200.0f; +constexpr float ULDUAR_YOGG_SARON_STORMWIND_KEEPER_RADIUS = 150.0f; +constexpr float ULDUAR_YOGG_SARON_ICECROWN_CITADEL_RADIUS = 150.0f; +constexpr float ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_RADIUS = 150.0f; +constexpr float ULDUAR_YOGG_SARON_BRAIN_ROOM_RADIUS = 50.0f; + +extern const Position ULDUAR_THORIM_NEAR_ARENA_CENTER; +extern const Position ULDUAR_THORIM_NEAR_ENTRANCE_POSITION; +extern const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_6_YARDS_1; +extern const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_6_YARDS_2; +extern const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_5_YARDS_1; +extern const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_1; +extern const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_2; +extern const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_3; +extern const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_6_YARDS_1; +extern const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_6_YARDS_2; +extern const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_5_YARDS_1; +extern const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_1; +extern const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_2; +extern const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_3; +extern const Position ULDUAR_THORIM_JUMP_END_POINT; +extern const Position ULDUAR_THORIM_PHASE2_TANK_SPOT; +extern const Position ULDUAR_THORIM_PHASE2_RANGE1_SPOT; +extern const Position ULDUAR_THORIM_PHASE2_RANGE2_SPOT; +extern const Position ULDUAR_THORIM_PHASE2_RANGE3_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE2_SIDE1RANGE_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE2_SIDE1MELEE_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE2_SIDE2RANGE_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE2_SIDE2MELEE_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE2_SIDE3RANGE_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE2_SIDE3MELEE_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE4_TANK_SPOT; +extern const Position ULDUAR_VEZAX_MARK_OF_THE_FACELESS_SPOT; +extern const Position ULDUAR_YOGG_SARON_MIDDLE; +extern const Position ULDUAR_YOGG_SARON_STORMWIND_KEEPER_MIDDLE; +extern const Position ULDUAR_YOGG_SARON_ICECROWN_CITADEL_MIDDLE; +extern const Position ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_MIDDLE; +extern const Position ULDUAR_YOGG_SARON_BRAIN_ROOM_MIDDLE; +extern const Position ULDUAR_YOGG_SARON_STORMWIND_KEEPER_ENTRANCE; +extern const Position ULDUAR_YOGG_SARON_ICECROWN_CITADEL_ENTRANCE; +extern const Position ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_ENTRANCE; +extern const Position ULDUAR_YOGG_SARON_PHASE_3_MELEE_SPOT; +extern const Position ULDUAR_YOGG_SARON_PHASE_3_RANGED_SPOT; + +class RazorscaleBossHelper : public AiObject +{ +public: + // Enums and constants specific to Razorscale + enum RazorscaleUnits : uint32 + { + UNIT_RAZORSCALE = 33186, + UNIT_DARK_RUNE_SENTINEL = 33846, + UNIT_DARK_RUNE_WATCHER = 33453, + UNIT_DARK_RUNE_GUARDIAN = 33388, + UNIT_DEVOURING_FLAME = 34188, + }; + + enum RazorscaleGameObjects : uint32 + { + GO_RAZORSCALE_HARPOON_1 = 194519, + GO_RAZORSCALE_HARPOON_2 = 194541, + GO_RAZORSCALE_HARPOON_3 = 194542, + GO_RAZORSCALE_HARPOON_4 = 194543, + }; + + enum RazorscaleSpells : uint32 + { + SPELL_CHAIN_1 = 49679, + SPELL_CHAIN_2 = 49682, + SPELL_CHAIN_3 = 49683, + SPELL_CHAIN_4 = 49684, + SPELL_SENTINEL_WHIRLWIND = 63806, + SPELL_STUN_AURA = 62794, + SPELL_FUSEARMOR = 64771 + }; + + static constexpr uint32 FUSEARMOR_THRESHOLD = 2; + + // Constants for arena parameters + static constexpr float RAZORSCALE_FLYING_Z_THRESHOLD = 440.0f; + static constexpr float RAZORSCALE_ARENA_CENTER_X = 587.54f; + static constexpr float RAZORSCALE_ARENA_CENTER_Y = -175.04f; + static constexpr float RAZORSCALE_ARENA_RADIUS = 30.0f; + + // Harpoon cooldown (seconds) + static constexpr time_t HARPOON_COOLDOWN_DURATION = 5; + + // Structure for harpoon data + struct HarpoonData + { + uint32 gameObjectEntry; + uint32 chainSpellId; + }; + + explicit RazorscaleBossHelper(PlayerbotAI* botAI) + : AiObject(botAI), _boss(nullptr) {} + + bool UpdateBossAI(); + Unit* GetBoss() const; + + bool IsGroundPhase() const; + bool IsFlyingPhase() const; + + bool IsHarpoonFired(uint32 chainSpellId) const; + static bool IsHarpoonReady(GameObject* harpoonGO); + static void SetHarpoonOnCooldown(GameObject* harpoonGO); + GameObject* FindNearestHarpoon(float x, float y, float z) const; + + static const std::vector& GetHarpoonData(); + + void AssignRolesBasedOnHealth(); + bool AreRolesAssigned() const; + bool CanSwapRoles() const; + +private: + Unit* _boss; + + // A map to track the last role swap *per bot* by their GUID + static std::unordered_map _lastRoleSwapTime; + + // The cooldown that applies to every bot + static const std::time_t _roleSwapCooldown = 10; + + static std::unordered_map _harpoonCooldowns; +}; + +// template +// class GenericBossHelper : public AiObject +// { +// public: +// GenericBossHelper(PlayerbotAI* botAI, std::string name) : AiObject(botAI), _name(name) {} +// virtual bool UpdateBossAI() +// { +// if (!bot->IsInCombat()) +// { +// _unit = nullptr; +// } +// if (_unit && (!_unit->IsInWorld() || !_unit->IsAlive())) +// { +// _unit = nullptr; +// } +// if (!_unit) +// { +// _unit = AI_VALUE2(Unit*, "find target", _name); +// if (!_unit) +// { +// return false; +// } +// _target = _unit->ToCreature(); +// if (!_target) +// { +// return false; +// } +// _ai = dynamic_cast(_target->GetAI()); +// if (!_ai) +// { +// return false; +// } +// _event_map = &_ai->events; +// if (!_event_map) +// { +// return false; +// } +// } +// if (!_event_map) +// { +// return false; +// } +// _timer = _event_map->GetTimer(); +// return true; +// } +// virtual void Reset() +// { +// _unit = nullptr; +// _target = nullptr; +// _ai = nullptr; +// _event_map = nullptr; +// _timer = 0; +// } + +// protected: +// std::string _name; +// Unit* _unit = nullptr; +// Creature* _target = nullptr; +// BossAiType* _ai = nullptr; +// EventMap* _event_map = nullptr; +// uint32 _timer = 0; +// }; + +#endif diff --git a/src/Ai/Raid/Ulduar/RaidUlduarScripts.h b/src/Ai/Raid/Ulduar/Util/RaidUlduarScripts.h similarity index 100% rename from src/Ai/Raid/Ulduar/RaidUlduarScripts.h rename to src/Ai/Raid/Ulduar/Util/RaidUlduarScripts.h diff --git a/src/Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h b/src/Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h index b304826b8..263d03d9b 100644 --- a/src/Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h +++ b/src/Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h @@ -7,6 +7,7 @@ #define _PLAYERBOT_RAIDVOAACTIONCONTEXT_H #include "Action.h" +#include "BossAuraActions.h" #include "NamedObjectContext.h" #include "RaidVoAActions.h" #include "PlayerbotAI.h" diff --git a/src/Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h b/src/Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h index 9fe078f80..6566793fd 100644 --- a/src/Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h +++ b/src/Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h @@ -7,6 +7,7 @@ #define _PLAYERBOT_RAIDVOATRIGGERCONTEXT_H #include "AiObjectContext.h" +#include "BossAuraTriggers.h" #include "NamedObjectContext.h" #include "RaidVoATriggers.h" diff --git a/src/Bot/Engine/AiObjectContext.cpp b/src/Bot/Engine/AiObjectContext.cpp index b6d5f7de0..250a39296 100644 --- a/src/Bot/Engine/AiObjectContext.cpp +++ b/src/Bot/Engine/AiObjectContext.cpp @@ -15,8 +15,6 @@ #include "PaladinAiObjectContext.h" #include "Playerbots.h" #include "PriestAiObjectContext.h" -#include "RaidUlduarActionContext.h" -#include "RaidUlduarTriggerContext.h" #include "RogueAiObjectContext.h" #include "ShamanAiObjectContext.h" #include "SharedValueContext.h" @@ -49,6 +47,8 @@ #include "Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h" #include "Ai/Raid/ObsidianSanctum/RaidOsActionContext.h" #include "Ai/Raid/ObsidianSanctum/RaidOsTriggerContext.h" +#include "Ai/Raid/Ulduar/RaidUlduarActionContext.h" +#include "Ai/Raid/Ulduar/RaidUlduarTriggerContext.h" #include "Ai/Raid/Onyxia/RaidOnyxiaActionContext.h" #include "Ai/Raid/Onyxia/RaidOnyxiaTriggerContext.h" #include "Ai/Raid/Icecrown/RaidIccActionContext.h" diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 437e62fc6..56c2cd39b 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -1585,7 +1585,7 @@ void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) strategyName = "wotlk-hol"; // Halls of Lightning break; case 603: - strategyName = "uld"; // Ulduar + strategyName = "ulduar"; // Ulduar break; case 604: strategyName = "wotlk-gd"; // Gundrak From 026df0dabead6e200462f7dbb7594a6d0fee9d37 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Fri, 6 Feb 2026 20:57:48 +0100 Subject: [PATCH 03/11] Chilton wand fix (#2115) # Pull Request Added Chilton wand to excluded to equipment items for bots and unified 2 exclusion lists to single one. Resolves: https://github.com/mod-playerbots/mod-playerbots/issues/2093 --- ## How to Test the Changes Couldnt reproduce Chilton wand bug then testing sound impossible. Someone can try getting this items on shaman. ## Complexity & Impact Does this change add new decision branches? - - [x] No - - [ ] Yes Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes Could this logic scale poorly under load? - - [x] No - - [ ] Yes --- ## Defaults & Configuration Does this change modify default bot behavior? - - [x] No - - [ ] Yes If this introduces more advanced or AI-heavy logic: - - [ ] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [x] No - - [ ] Yes --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- --- src/Bot/Factory/PlayerbotFactory.cpp | 4 ---- src/Mgr/Item/RandomItemMgr.cpp | 9 ++++++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Bot/Factory/PlayerbotFactory.cpp b/src/Bot/Factory/PlayerbotFactory.cpp index dfefd323c..ca1f4d180 100644 --- a/src/Bot/Factory/PlayerbotFactory.cpp +++ b/src/Bot/Factory/PlayerbotFactory.cpp @@ -1799,10 +1799,6 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) { for (uint32 itemId : sRandomItemMgr.GetCachedEquipments(requiredLevel, inventoryType)) { - if (itemId == 46978) // shaman earth ring totem - { - continue; - } uint32 skipProb = 25; if (urand(1, 100) <= skipProb) continue; diff --git a/src/Mgr/Item/RandomItemMgr.cpp b/src/Mgr/Item/RandomItemMgr.cpp index 5c0e8c94a..1053e542e 100644 --- a/src/Mgr/Item/RandomItemMgr.cpp +++ b/src/Mgr/Item/RandomItemMgr.cpp @@ -2256,10 +2256,13 @@ void RandomItemMgr::BuildEquipCacheNew() { continue; } - if (itemId == 22784) - { // Sunwell Orb + + // Unobtainable or unusable items + if (itemId == 12468 || // Chilton Wand + itemId == 22784 || // Sunwell Orb + itemId == 46978) // Totem of the Earthen Ring continue; - } + equipCacheNew[proto->RequiredLevel][proto->InventoryType].push_back(itemId); } } From 76b6df9ea363912f47355a9984c5c18ec4be05e0 Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Fri, 6 Feb 2026 23:20:31 +0100 Subject: [PATCH 04/11] Extend SummonWhenGroup to auto-added bots (#2034) ### Summary Extend AiPlayerbot.SummonWhenGroup to apply when bots are auto-added to a group (e.g., addclass bots or raidus style auto invites). ### Motivation Bots added automatically to a group never accept a normal invite, so they do not trigger the summon-on-accept path. When SummonWhenGroup is enabled, these bots should also be teleported next to the master to match expected behavior. ### Implementation details Hook the summon behavior right after automatic group addition. --- src/Ai/Base/Actions/UseMeetingStoneAction.h | 2 +- src/Script/WorldThr/PlayerbotOperations.h | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Ai/Base/Actions/UseMeetingStoneAction.h b/src/Ai/Base/Actions/UseMeetingStoneAction.h index ff1282284..fd2a2c5f2 100644 --- a/src/Ai/Base/Actions/UseMeetingStoneAction.h +++ b/src/Ai/Base/Actions/UseMeetingStoneAction.h @@ -17,9 +17,9 @@ public: SummonAction(PlayerbotAI* botAI, std::string const name = "summon") : MovementAction(botAI, name) {} bool Execute(Event event) override; + bool Teleport(Player* summoner, Player* player, bool preserveAuras); protected: - bool Teleport(Player* summoner, Player* player, bool preserveAuras); bool SummonUsingGos(Player* summoner, Player* player, bool preserveAuras); bool SummonUsingNpcs(Player* summoner, Player* player, bool preserveAuras); }; diff --git a/src/Script/WorldThr/PlayerbotOperations.h b/src/Script/WorldThr/PlayerbotOperations.h index a80321d53..ee6443cd8 100644 --- a/src/Script/WorldThr/PlayerbotOperations.h +++ b/src/Script/WorldThr/PlayerbotOperations.h @@ -14,9 +14,11 @@ #include "PlayerbotOperation.h" #include "Player.h" #include "PlayerbotAI.h" +#include "PlayerbotAIConfig.h" #include "PlayerbotMgr.h" #include "PlayerbotRepository.h" #include "RandomPlayerbotMgr.h" +#include "UseMeetingStoneAction.h" #include "WorldSession.h" #include "WorldSessionMgr.h" @@ -74,6 +76,15 @@ public: if (group->AddMember(target)) { LOG_DEBUG("playerbots", "GroupInviteOperation: Successfully added {} to group", target->GetName()); + if (sPlayerbotAIConfig.summonWhenGroup && target->GetDistance(bot) > sPlayerbotAIConfig.sightDistance) + { + PlayerbotAI* targetAI = sPlayerbotsMgr.GetPlayerbotAI(target); + if (targetAI) + { + SummonAction summonAction(targetAI, "group summon"); + summonAction.Teleport(bot, target, true); + } + } return true; } else From 6ed3f24ecb13158a5a856e2b5112266d5479799b Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 7 Feb 2026 16:34:15 +0100 Subject: [PATCH 05/11] Enforce test fix (#2122) CI/CD PR --------- Co-authored-by: Crow --- .github/workflows/check_pr_source.yml | 4 +++- .github/workflows/code_style.yml | 4 ++-- .github/workflows/core_build.yml | 4 ++-- .github/workflows/macos_build.yml | 4 ++-- .github/workflows/windows_build.yml | 4 ++-- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/check_pr_source.yml b/.github/workflows/check_pr_source.yml index 877ab85c7..5b354f6f7 100644 --- a/.github/workflows/check_pr_source.yml +++ b/.github/workflows/check_pr_source.yml @@ -1,13 +1,15 @@ -name: Enforce test-staging → main +name: Enforce test-staging → master on: pull_request: branches: - master + - test-staging jobs: require-test-staging: runs-on: ubuntu-22.04 + if: github.event.pull_request.base.ref == 'master' steps: - name: Ensure PR source is test-staging run: | diff --git a/.github/workflows/code_style.yml b/.github/workflows/code_style.yml index 7b1688fb6..83b78c392 100644 --- a/.github/workflows/code_style.yml +++ b/.github/workflows/code_style.yml @@ -2,9 +2,9 @@ name: Codestyle on: push: - branches: [ "master" ] + branches: [ "master", "test-staging" ] pull_request: - branches: [ "master" ] + branches: [ "master", "test-staging" ] concurrency: group: "codestyle-${{ github.event.pull_request.number }}" diff --git a/.github/workflows/core_build.yml b/.github/workflows/core_build.yml index 2927716f1..ac6ee60d8 100644 --- a/.github/workflows/core_build.yml +++ b/.github/workflows/core_build.yml @@ -2,9 +2,9 @@ name: ubuntu-build on: push: - branches: [ "master" ] + branches: [ "master", "test-staging" ] pull_request: - branches: [ "master" ] + branches: [ "master", "test-staging" ] concurrency: group: "core-build-${{ github.event.pull_request.number }}" diff --git a/.github/workflows/macos_build.yml b/.github/workflows/macos_build.yml index 30a558c63..ab4d83daa 100644 --- a/.github/workflows/macos_build.yml +++ b/.github/workflows/macos_build.yml @@ -1,9 +1,9 @@ name: macos-build on: push: - branches: [ "master" ] + branches: [ "master", "test-staging" ] pull_request: - branches: [ "master" ] + branches: [ "master", "test-staging" ] concurrency: group: "macos-build-${{ github.event.pull_request.number }}" diff --git a/.github/workflows/windows_build.yml b/.github/workflows/windows_build.yml index 49678c97a..121d97751 100644 --- a/.github/workflows/windows_build.yml +++ b/.github/workflows/windows_build.yml @@ -1,9 +1,9 @@ name: windows-build on: push: - branches: [ "master" ] + branches: [ "master", "test-staging" ] pull_request: - branches: [ "master" ] + branches: [ "master", "test-staging" ] concurrency: group: "windows-build-${{ github.event.pull_request.number }}" From 79fb3a5bbc9e27752a3133a83d56a9c72dc44ed6 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 7 Feb 2026 17:53:55 +0100 Subject: [PATCH 06/11] - Fixed Oculus drake mounting --- .../Dungeon/Oculus/Action/OculusActions.cpp | 2 +- src/Bot/PlayerbotAI.cpp | 34 ++++++++++++++++--- src/Bot/PlayerbotAI.h | 3 +- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/Ai/Dungeon/Oculus/Action/OculusActions.cpp b/src/Ai/Dungeon/Oculus/Action/OculusActions.cpp index 414acc4bc..bbb94d110 100644 --- a/src/Ai/Dungeon/Oculus/Action/OculusActions.cpp +++ b/src/Ai/Dungeon/Oculus/Action/OculusActions.cpp @@ -62,7 +62,7 @@ bool MountDrakeAction::Execute(Event event) break; } - std::vector players = botAI->GetPlayersInGroup(); + std::vector players = botAI->GetAllPlayersInGroup(); for (Player* player : players) { if (!player || !player->IsInWorld() || player->IsDuringRemoveFromWorld()) diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 437e62fc6..8d95981e4 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -2580,7 +2580,7 @@ std::string PlayerbotAI::GetLocalizedGameObjectName(uint32 entry) return name; } -std::vector PlayerbotAI::GetPlayersInGroup() +std::vector PlayerbotAI::GetRealPlayersInGroup() { std::vector members; @@ -2607,6 +2607,30 @@ std::vector PlayerbotAI::GetPlayersInGroup() return members; } +std::vector PlayerbotAI::GetAllPlayersInGroup() +{ + std::vector members; + + Group* group = bot->GetGroup(); + + if (!group) + return members; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + + if (!member) + { + continue; + } + + members.push_back(ref->GetSource()); + } + + return members; +} + bool PlayerbotAI::SayToGuild(const std::string& msg) { if (msg.empty()) @@ -2715,9 +2739,9 @@ bool PlayerbotAI::SayToParty(const std::string& msg) ChatHandler::BuildChatPacket(data, CHAT_MSG_PARTY, msg.c_str(), LANG_UNIVERSAL, CHAT_TAG_NONE, bot->GetGUID(), bot->GetName()); - for (auto reciever : GetPlayersInGroup()) + for (auto receiver : GetRealPlayersInGroup()) { - ServerFacade::instance().SendPacket(reciever, &data); + ServerFacade::instance().SendPacket(receiver, &data); } return true; @@ -2732,9 +2756,9 @@ bool PlayerbotAI::SayToRaid(const std::string& msg) ChatHandler::BuildChatPacket(data, CHAT_MSG_RAID, msg.c_str(), LANG_UNIVERSAL, CHAT_TAG_NONE, bot->GetGUID(), bot->GetName()); - for (auto reciever : GetPlayersInGroup()) + for (auto receiver : GetRealPlayersInGroup()) { - ServerFacade::instance().SendPacket(reciever, &data); + ServerFacade::instance().SendPacket(receiver, &data); } return true; diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index c2d4aeb75..3364b31ac 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -446,7 +446,8 @@ public: GameObject* GetGameObject(ObjectGuid guid); // static GameObject* GetGameObject(GameObjectData const* gameObjectData); WorldObject* GetWorldObject(ObjectGuid guid); - std::vector GetPlayersInGroup(); + std::vector GetAllPlayersInGroup(); + std::vector GetRealPlayersInGroup(); const AreaTableEntry* GetCurrentArea(); const AreaTableEntry* GetCurrentZone(); static std::string GetLocalizedAreaName(const AreaTableEntry* entry); From 8585f10f48e470c495338fc5b237ca5cadcdcfaa Mon Sep 17 00:00:00 2001 From: Crow Date: Sun, 8 Feb 2026 05:31:23 -0600 Subject: [PATCH 07/11] Implement Serpentshrine Cavern Strategies (#1888) Edited: Below description of methods were brought up to date as of the PR coming off of draft. ### General I've starting leveraging, to the extent possible, an out-of-combat method to erase map keys. This is mostly useful for timers that need to start upon the pull because I dislike having to rely on a check for a boss to be at 100% HP (or 99.9% or whatever) because it can be unreliable sometimes. ### Trash Underbog Colossi: Some Colossi leave behind a lake of toxin when they die that quickly kills any player that is standing in it. The pool is a dynamic-object-generated AoE, and bots will not avoid it on their own (I think because the AoE is out of combat, plus the radius is much larger than the default avoidance radius in the config). The method does not require bots to be in combat, and simply gets bots to run out of the toxin. You will probably still get a couple of idiots who drink in the middle of it, but in my experience, the vast majority of the raid gets out, and healers that escape can easily keep up a couple of fools until they've drank to full. Greyheart Tidecallers: Bots will mark and destroy Water Elemental Totems immediately. ### Hydross the Unstable The strategy uses 2 tanks, with the main tank assigned to the frost phase and the 1st assistant tank assigned to the nature phase. - The main tank will tank the frost phase, and the first assistant tank will tank the nature phase. They each have designated spots and will wait at their spots twiddling their thumbs while Hydross is in the other phase. - Hunters will misdirect to the applicable tank upon the pull and after each phase change. - The phase change process begins 1 second after Hydross reaches 100% Marks. The current tank will begin moving to the next phase tank's spot for the next tank to take over as soon as Hydross transitions. - DPS is ordered to stop after Hydross reaches 100% Marks until 5 seconds after he transitions. - Bots will prioritize the elementals adds after every phase change, unless Hydross is under 10% HP, in which case they should ignore the adds and burn the boss. - Ranged bots should spread during the frost phase to mitigate the impact of Water Tombs. ### The Lurker Below - There is a designated spot for the main tank. - Ranged DPS will fan out over a 120-degree arc that is centered directly across from the tank spot (to try to spread to reduce Geyser damage while also keeping them behind Lurker). - When Spout begins, all bots will run around behind Lurker. The intent is to keep a distance with a radius of 20 or 21 yards and within 45 degrees (either side) of directly behind him. Movement is specifically tangential along an arc so bots don't run in front of Lurker. - Spout's duration is tracked by a timer. The mechanics of the spell itself are rather unique and don't involve a continuous cast or aura to track easily so I settled for the timer. - If you have 3 (or more) tanks, each of the first 3 tanks will be assigned to one of the 3 Coilfang Guardians during the submerge phase. ### Leotheras the Blind The fight is designed for a Warlock tank. You can choose the Warlock tank by giving a Warlock the Assistant flag. If you don't do that, your highest HP Warlock will be picked. Do NOT switch the Warlock tank to a co +tank strategy--the designated Warlock is hardcoded to spam Searing Pain on Demon Leo and otherwise will engage in normal DPS strategies. If you don't have a Warlock at all, the strategy has some methods built in to try to make things work as best as possible with a melee tank. - The Spellbinders get marked with skulls and killed in order. - There is no designated spot or designated tank for the human phase. Your tanks will fight for aggro. Ranged bots will attempt to keep some distance, and when Whirlwind starts, everybody will run away from Leotheras. - During the demon phase, your melee tanks should take a backseat to your Warlock tank, who will receive help in the form of Misdirection. Bots will get the hell away from the Warlock tank so the Warlock tank should be taking every Chaos Blast alone. - During the final phase, your regular tanks will tank Leotheras, and the Warlock tank will tank his Shadow. The melee tanks will attempt to separate Leotheras from his Shadow so bots can focus down Leotheras without getting hit with Chaos Blasts. - Bots will wait 5 seconds to DPS after every transition into human phase, 12 seconds to DPS after every transition into demon phase, and 8 seconds to DPS after the transition into the final phase. There is no waiting on DPS after Whirlwinds, even though it would be ideal. It's not a big deal to live without, and for various reasons, it would have been a pain in the ass to deal with. - Bots will save Bloodlust/Heroism until after Spellbinders are down. - To deal with the Inner Demons, I disabled DPS assist for bots who are targeted and force them to focus only on their Inner Demons. This is sufficient in my experience for all DPS bots and Protection Warriors and Paladins to kill their Inner Demons, even at 50% damage. Feral Tank Druids and Healers still need help, so the strategy hardcodes their actions while fighting Inner Demons. For example, Resto Druids are coded to shift out of Tree Form, cast Barkskin on themselves, and just spam Wrath until the Inner Demon is dead. There are no bot strategy changes used for this method. ### Fathom-Lord Karathress You will need 4 tanks. Your main tank will tank Karathress, and an assistant tank will tank each Fathom Guard. If you have fewer than 4 tanks, then the priority order for tank assignment will be Karathress, Caribdis, Sharkkis, and then Tidalvess. - Roughly, the tank spots are (1) for Karathress, near where he starts but closer to the ledge for LoS reasons, (2) for Sharkkis, North from his starting location on the other side of the ramp, (3) for Tidalvess, Northwest from his starting location near the pillar, and (4) for Caribdis, far to the West of her starting position, near the corner. - Note that the tanks will probably clip through the terrain a bit when going to their positions. This is due to me implementing a forced MoveTo to the tank position coordinates. There is something weird about the maps in Karathress's room, and the tanks will take some really screwed up paths without making them go directly to the exact coordinates. So this looks stupid but is necessary. - One healer will be assigned to heal the Caribdis tank. Because AC Playerbots does not yet have a focus heal strategy, this just means that such healer has a designated location near the Caribdis tank's location. This healer can be selected with the Assistant flag. - Hunters will misdirect the Fathom Guards onto their applicable tanks. If you don't have three Hunters, the priority is Caribdis, Tidalvess, then Sharkkis. - DPS will wait 12 seconds to begin attacking. After that, they will prioritize targets as follows: - (1): Melee will always prioritize Spitfire Totems as soon as they spawn. This will continue through the duration of the fight. - (2): All bots will kill Tidalvess first. - (3): Melee bots will move to Sharkkis, and ranged bots will move to Caribdis. I understand this is not the standard kill order for players, which would have the entire raid kill Sharkkis next. The reasons I have done this differently are because melee DPS is much stronger with 3.3.5 talents vs. in retail TBC, and because bots get really thrown off by Cyclones and therefore they struggle to kill Caribdis quickly. You do not want Karathress below 75% HP before all Fathom-Guards are dead or he gets a huge damage buff. - (4) If Caribdis dies first, ranged bots will help with Sharkkis. - (5) Everybody kills Sharkkis's pet. - (6) Everybody kills Karathress. ### Morogrim Tidewalker - The main tank will pull the boss to the Northeast pillar, with the tank's back against the pillar. - A hunter will misdirect the boss onto the main tank upon the pull. - When the boss gets to 26% HP, the main tank will begin moving the boss to the Northeast corner of the room in preparation for Phase 2 (which begins at 25%). The tank will move in two steps to get around the pillar. - When the boss gets to 25% HP, ranged will follow the main tank to the corner and stack up right behind the boss. They will also move in two steps. - There is no method for melee since they will just naturally follow the boss anyway. ### Lady Vashj **Phase 1**: - The main tank will tank Vashj in the center of the arena. - If a Shaman is in the main tank's group, that Shaman will attempt to keep a Grounding Totem down in range of the main tank to absorb Shock Blast. This should continue in Phase 3. - Ranged bots will spread out in a semicircle around the center of the arena. - If any bot other than the main tank gets Static Charge, it will run away from other bots. If the main tank gets Static Charge, other bots will run away from the main tank. This method should continue in Phase 3. - If any bot is Entangled and has Static Charge, the bot will attempt to use Cloak of Shadows if it is a Rogue, and Paladins will attempt to use Hand of Freedom. This method should continue in Phase 3 (with some modifications). - Bots will not use Bloodlust or Heroism (saved for Phase 3). Bots will not use any other major cooldowns, either, such as Metamorphosis (saved for Phase 2 and 3). **Phase 2**: There are two central mechanics to this phase, both of which were challenging to get bots to execute properly. First is the system of prioritizing adds. The large playing field and multiple types of adds coming from random directions make this phase not doable with realistic DPS under the standard Playerbots target selection system. Therefore, I took inspiration from liyunfan's Naxx strategy for Phase 1 of Kel'Thuzad to disable dps assist and create a custom target selection system. First, a cheat with respect to the Coilfang Striders: - Tanks will permanently have the Fear Ward aura applied to them if you have raid cheats enabled. This allows them to tank the Coilfang Striders. The standard strategy was to have an Elemental Shaman kite the Strider around the perimeter of the arena, with ranged players (including healers) spamming DoTs on the Strider. If you can make bots do this, then great, but it's far beyond my capabilities. Therefore, with the cheat, the first assistant tank is responsible for tanking Striders and keeping them away from Core passers (described below) and Vashj. Evidently it was (and is, in TBC Classic) possible to tank (and melee DPS) Striders by wearing a Dire Maul Ogre Suit, which would give you enough reach to stay out of the Strider's fear. I actually tried that, and it does not work, either because AC's radiuses are not the same or just because bots do not maintain the same level of precise positioning. But anyway, the point is that technically the Striders are tankable by real players, so maybe that will make you feel better about using this cheat (it's fine enough rationalizing for me). I found this fight to be unmanageable without this cheat (i.e., using a method that would only have bots try to run away from Striders) because each Strider was guaranteed to wipe out a couple of bots, and you really cannot afford to lose anyone. YMMV though. - If cheats are enabled for Striders, Hunters will attempt to Misdirect the Striders to the first assist tank. - If cheats are not enabled, bots will attempt to use slows/roots to stop the Striders. I have some logic for them to use Netherweave Nets, but I suspect it does not actually work so I may remove it instead of trying to get it to function properly. Target priority is as follows: - Hunters and Mages: Enchanted Elementals, Coilfang Striders, Coilfang Elites. - Other Ranged Bots: Elites, Striders, Elementals. - Melee DPS: Elementals, Elites. - Tanks: Elites, Elementals (except if cheats are enabled, the first assistant tank will instead prioritize Striders and then Elementals) - Everybody else (basically means healers): Elementals, Elites, Striders - If there is more than one of the same target, bots will prioritize the one that is closer to Vashj. - In all cases, the valid attack ranged is limited so that bots should not leave the central platform. - If somehow a bot ends up too far from the center of the room and is not actively attacking anything, there is logic to make them run back. Handling Tainted Elementals and the Tainted Core: I will make another post about this later. It is easily the most complicated strategy I've ever worked on (far beyond anything on Kael'thas even) so will necessitate a long explanation. The tl;dr is that there is a chain of two-to-four bots that receive/pass the Tainted Core before using it on a Shield Generator, and if you are playing by yourself, you probably need to turn raid cheats on, in which case there will also be a bot that teleports to, kills, and loots the Tainted Elementals (i.e., the bots will then handle the entire sequence of shutting down Shield Generators). **Phase 3**: - The main tank will pick up Vashj immediately and try to keep her away from Enchanted Elementals. - DPS will burn down residual adds from Phase 2 in the order of (1) elementals, (2) strider for ranged only (if you have more than one up, you're dead), and (3) elites (hopefully you have only one up, but two with one almost dead is possible). - Hunters will kill Toxic Sporebats. This works quite well, but they (and anybody else if ordered to target Sporebats) have a tendency to levitate up into the pipes at the top of the room when killing the Sporebats. To counteract this, a method forcibly teleports bots to the ground if they get more than 2 yards above the ground. - The Phase 1 Cloak of Shadows/Hand of Freedom method is now expanded to include bots Entangled in the Sporebat poison pools (with Hand of Freedom usage prioritized on the main tank). - There is a specific method to avoid the Sporebat poison pools. The Vashj tank will move backwards when avoiding poison. --------- Co-authored-by: kadeshar --- conf/playerbots.conf.dist | 2 +- src/Ai/Raid/RaidStrategyContext.h | 3 + .../Action/RaidSSCActions.cpp | 3128 +++++++++++++++++ .../Action/RaidSSCActions.h | 457 +++ .../Multiplier/RaidSSCMultipliers.cpp | 799 +++++ .../Multiplier/RaidSSCMultipliers.h | 236 ++ .../RaidSSCActionContext.h | 337 ++ .../RaidSSCTriggerContext.h | 325 ++ .../Strategy/RaidSSCStrategy.cpp | 206 ++ .../Strategy/RaidSSCStrategy.h | 18 + .../Trigger/RaidSSCTriggers.cpp | 670 ++++ .../Trigger/RaidSSCTriggers.h | 414 +++ .../Util/RaidSSCHelpers.cpp | 583 +++ .../SerpentshrineCavern/Util/RaidSSCHelpers.h | 189 + src/Bot/Engine/AiObjectContext.cpp | 4 + src/Bot/PlayerbotAI.cpp | 3 + 16 files changed, 7373 insertions(+), 1 deletion(-) create mode 100644 src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.cpp create mode 100644 src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h create mode 100644 src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.cpp create mode 100644 src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.h create mode 100644 src/Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h create mode 100644 src/Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h create mode 100644 src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.cpp create mode 100644 src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.h create mode 100644 src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.cpp create mode 100644 src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.h create mode 100644 src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.cpp create mode 100644 src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.h diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 907377316..18a6addf6 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -558,7 +558,7 @@ AiPlayerbot.AutoGearScoreLimit = 0 # "mana" (bots have infinite mana) # "power" (bots have infinite energy, rage, and runic power) # "taxi" (bots may use all flight paths, though they will not actually learn them) -# "raid" (bots use cheats implemented into raid strategies (currently only for Ulduar)) +# "raid" (bots use cheats implemented into raid strategies (currently only for SSC and Ulduar)) # To use multiple cheats, separate them by commas below (e.g., to enable all, use "gold,health,mana,power,raid,taxi") # Default: food, taxi, and raid are enabled AiPlayerbot.BotCheats = "food,taxi,raid" diff --git a/src/Ai/Raid/RaidStrategyContext.h b/src/Ai/Raid/RaidStrategyContext.h index 3c7971fb6..4a040985e 100644 --- a/src/Ai/Raid/RaidStrategyContext.h +++ b/src/Ai/Raid/RaidStrategyContext.h @@ -8,6 +8,7 @@ #include "RaidKarazhanStrategy.h" #include "RaidMagtheridonStrategy.h" #include "RaidGruulsLairStrategy.h" +#include "RaidSSCStrategy.h" #include "RaidOsStrategy.h" #include "RaidEoEStrategy.h" #include "RaidVoAStrategy.h" @@ -26,6 +27,7 @@ public: creators["karazhan"] = &RaidStrategyContext::karazhan; creators["magtheridon"] = &RaidStrategyContext::magtheridon; creators["gruulslair"] = &RaidStrategyContext::gruulslair; + creators["ssc"] = &RaidStrategyContext::ssc; creators["wotlk-os"] = &RaidStrategyContext::wotlk_os; creators["wotlk-eoe"] = &RaidStrategyContext::wotlk_eoe; creators["voa"] = &RaidStrategyContext::voa; @@ -41,6 +43,7 @@ private: 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* ssc(PlayerbotAI* botAI) { return new RaidSSCStrategy(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/SerpentshrineCavern/Action/RaidSSCActions.cpp b/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.cpp new file mode 100644 index 000000000..7aa3eda35 --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.cpp @@ -0,0 +1,3128 @@ +#include "RaidSSCActions.h" +#include "RaidSSCHelpers.h" +#include "AiFactory.h" +#include "Corpse.h" +#include "LootAction.h" +#include "LootObjectStack.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" +#include "RtiTargetValue.h" + +using namespace SerpentShrineCavernHelpers; + +// General + +bool SerpentShrineCavernEraseTimersAndTrackersAction::Execute(Event /*event*/) +{ + const uint32 instanceId = bot->GetMap()->GetInstanceId(); + const ObjectGuid guid = bot->GetGUID(); + + bool erased = false; + if (!AI_VALUE2(Unit*, "find target", "hydross the unstable")) + { + if (hydrossChangeToNaturePhaseTimer.erase(instanceId) > 0) + erased = true; + if (hydrossChangeToFrostPhaseTimer.erase(instanceId) > 0) + erased = true; + if (hydrossNatureDpsWaitTimer.erase(instanceId) > 0) + erased = true; + if (hydrossFrostDpsWaitTimer.erase(instanceId) > 0) + erased = true; + } + if (!AI_VALUE2(Unit*, "find target", "the lurker below")) + { + if (lurkerRangedPositions.erase(guid) > 0) + erased = true; + if (lurkerSpoutTimer.erase(instanceId) > 0) + erased = true; + } + if (!AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) + { + if (karathressDpsWaitTimer.erase(instanceId) > 0) + erased = true; + } + if (!AI_VALUE2(Unit*, "find target", "morogrim tidewalker")) + { + if (tidewalkerTankStep.erase(guid) > 0) + erased = true; + if (tidewalkerRangedStep.erase(guid) > 0) + erased = true; + } + if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + { + if (vashjRangedPositions.erase(guid) > 0) + erased = true; + if (hasReachedVashjRangedPosition.erase(guid) > 0) + erased = true; + } + + return erased; +} + +// Trash Mobs + +// Move out of toxic pool left behind by some colossi upon death +bool UnderbogColossusEscapeToxicPoolAction::Execute(Event /*event*/) +{ + Aura* aura = bot->GetAura(SPELL_TOXIC_POOL); + if (!aura) + return false; + + DynamicObject* dynObj = aura->GetDynobjOwner(); + if (!dynObj) + return false; + + float radius = dynObj->GetRadius(); + if (radius <= 0.0f) + { + const SpellInfo* sInfo = sSpellMgr->GetSpellInfo(dynObj->GetSpellId()); + if (sInfo) + { + for (int e = 0; e < MAX_SPELL_EFFECTS; ++e) + { + auto const& eff = sInfo->Effects[e]; + if (eff.Effect == SPELL_EFFECT_SCHOOL_DAMAGE || + (eff.Effect == SPELL_EFFECT_APPLY_AURA && + eff.ApplyAuraName == SPELL_AURA_PERIODIC_DAMAGE)) + { + radius = eff.CalcRadius(); + break; + } + } + } + } + + if (radius <= 0.0f) + return false; + + constexpr float bufferDist = 3.0f; + constexpr float centerThreshold = 1.0f; + + float dx = bot->GetPositionX() - dynObj->GetPositionX(); + float dy = bot->GetPositionY() - dynObj->GetPositionY(); + + float distToObj = bot->GetExactDist2d(dynObj->GetPositionX(), dynObj->GetPositionY()); + const float insideThresh = radius + centerThreshold; + + if (distToObj > insideThresh) + return false; + + float safeDist = radius + bufferDist; + float moveX, moveY; + + if (distToObj == 0.0f) + { + float angle = frand(0.0f, static_cast(M_PI * 2.0)); + moveX = dynObj->GetPositionX() + std::cos(angle) * safeDist; + moveY = dynObj->GetPositionY() + std::sin(angle) * safeDist; + } + else + { + float invDist = 1.0f / distToObj; + moveX = dynObj->GetPositionX() + (dx * invDist) * safeDist; + moveY = dynObj->GetPositionY() + (dy * invDist) * safeDist; + } + + botAI->Reset(); + return MoveTo(SSC_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, false, + true, MovementPriority::MOVEMENT_FORCED, true, false); +} + +bool GreyheartTidecallerMarkWaterElementalTotemAction::Execute(Event /*event*/) +{ + if (Unit* totem = GetFirstAliveUnitByEntry(botAI, NPC_WATER_ELEMENTAL_TOTEM)) + MarkTargetWithSkull(bot, totem); + + return false; +} + +// Hydross the Unstable + +// (1) When tanking, move to designated tanking spot on frost side +// (2) 1 second after 100% Mark of Hydross, move to nature tank's spot to hand off boss +// (3) When Hydross is in nature form, move back to frost tank spot and wait for transition +bool HydrossTheUnstablePositionFrostTankAction::Execute(Event /*event*/) +{ + Unit* hydross = AI_VALUE2(Unit*, "find target", "hydross the unstable"); + if (!hydross) + return false; + + if (!hydross->HasAura(SPELL_CORRUPTION) && !HasMarkOfHydrossAt100Percent(bot)) + { + MarkTargetWithSquare(bot, hydross); + SetRtiTarget(botAI, "square", hydross); + + if (bot->GetTarget() != hydross->GetGUID()) + return Attack(hydross); + + if (hydross->GetVictim() == bot && bot->IsWithinMeleeRange(hydross)) + { + const Position& position = HYDROSS_FROST_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(SSC_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + } + + if (!hydross->HasAura(SPELL_CORRUPTION) && HasMarkOfHydrossAt100Percent(bot) && + hydross->GetVictim() == bot && bot->IsWithinMeleeRange(hydross)) + { + const time_t now = std::time(nullptr); + auto it = hydrossChangeToNaturePhaseTimer.find(hydross->GetMap()->GetInstanceId()); + + if (it != hydrossChangeToNaturePhaseTimer.end() && (now - it->second) >= 1) + { + const Position& position = HYDROSS_NATURE_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(SSC_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT, true, true); + } + else + { + botAI->Reset(); + return true; + } + } + } + + if (hydross->HasAura(SPELL_CORRUPTION)) + { + const Position& position = HYDROSS_FROST_TANK_POSITION; + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) + { + return MoveTo(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +// (1) When tanking, move to designated tanking spot on nature side +// (2) 1 second after 100% Mark of Corruption, move to frost tank's spot to hand off boss +// (3) When Hydross is in frost form, move back to nature tank spot and wait for transition +bool HydrossTheUnstablePositionNatureTankAction::Execute(Event /*event*/) +{ + Unit* hydross = AI_VALUE2(Unit*, "find target", "hydross the unstable"); + if (!hydross) + return false; + + if (hydross->HasAura(SPELL_CORRUPTION) && !HasMarkOfCorruptionAt100Percent(bot)) + { + MarkTargetWithTriangle(bot, hydross); + SetRtiTarget(botAI, "triangle", hydross); + + if (bot->GetTarget() != hydross->GetGUID()) + return Attack(hydross); + + if (hydross->GetVictim() == bot && bot->IsWithinMeleeRange(hydross)) + { + const Position& position = HYDROSS_NATURE_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(SSC_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + } + + if (hydross->HasAura(SPELL_CORRUPTION) && HasMarkOfCorruptionAt100Percent(bot) && + hydross->GetVictim() == bot && bot->IsWithinMeleeRange(hydross)) + { + const time_t now = std::time(nullptr); + auto it = hydrossChangeToFrostPhaseTimer.find(hydross->GetMap()->GetInstanceId()); + + if (it != hydrossChangeToFrostPhaseTimer.end() && (now - it->second) >= 1) + { + const Position& position = HYDROSS_FROST_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(SSC_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT, true, true); + } + else + { + botAI->Reset(); + return true; + } + } + } + + if (!hydross->HasAura(SPELL_CORRUPTION)) + { + const Position& position = HYDROSS_NATURE_TANK_POSITION; + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) + { + return MoveTo(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +bool HydrossTheUnstablePrioritizeElementalAddsAction::Execute(Event /*event*/) +{ + Unit* waterElemental = GetFirstAliveUnitByEntry(botAI, NPC_PURE_SPAWN_OF_HYDROSS); + if (waterElemental) + { + if (IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr)) + MarkTargetWithSkull(bot, waterElemental); + + SetRtiTarget(botAI, "skull", waterElemental); + + if (bot->GetTarget() != waterElemental->GetGUID()) + return Attack(waterElemental); + } + else if (Unit* natureElemental = GetFirstAliveUnitByEntry(botAI, NPC_TAINTED_SPAWN_OF_HYDROSS)) + { + if (IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr)) + MarkTargetWithSkull(bot, natureElemental); + + SetRtiTarget(botAI, "skull", natureElemental); + + if (bot->GetTarget() != natureElemental->GetGUID()) + return Attack(natureElemental); + } + + return false; +} + +// To mitigate the effect of Water Tomb +bool HydrossTheUnstableFrostPhaseSpreadOutAction::Execute(Event /*event*/) +{ + if (!AI_VALUE2(Unit*, "find target", "hydross the unstable")) + return false; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot || !member->IsAlive()) + continue; + + constexpr float safeDistance = 6.0f; + constexpr uint32 minInterval = 1000; + if (bot->GetExactDist2d(member) < safeDistance) + return FleePosition(member->GetPosition(), safeDistance, minInterval); + } + } + + return false; +} + +bool HydrossTheUnstableMisdirectBossToTankAction::Execute(Event /*event*/) +{ + Unit* hydross = AI_VALUE2(Unit*, "find target", "hydross the unstable"); + if (!hydross) + return false; + + if (Group* group = bot->GetGroup()) + { + if (TryMisdirectToFrostTank(hydross, group)) + return true; + + if (TryMisdirectToNatureTank(hydross, group)) + return true; + } + + return false; +} + +bool HydrossTheUnstableMisdirectBossToTankAction::TryMisdirectToFrostTank( + Unit* hydross, Group* group) +{ + Player* frostTank = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsMainTank(member)) + { + frostTank = member; + break; + } + } + + if (HasNoMarkOfHydross(bot) && !hydross->HasAura(SPELL_CORRUPTION) && frostTank) + { + if (botAI->CanCastSpell("misdirection", frostTank)) + return botAI->CastSpell("misdirection", frostTank); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", hydross)) + return botAI->CastSpell("steady shot", hydross); + } + + return false; +} + +bool HydrossTheUnstableMisdirectBossToTankAction::TryMisdirectToNatureTank( + Unit* hydross, Group* group) +{ + Player* natureTank = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && botAI->IsAssistTankOfIndex(member, 0, true)) + { + natureTank = member; + break; + } + } + + if (HasNoMarkOfCorruption(bot) && hydross->HasAura(SPELL_CORRUPTION) && natureTank) + { + if (botAI->CanCastSpell("misdirection", natureTank)) + return botAI->CastSpell("misdirection", natureTank); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", hydross)) + return botAI->CastSpell("steady shot", hydross); + } + + return false; +} + +bool HydrossTheUnstableStopDpsUponPhaseChangeAction::Execute(Event /*event*/) +{ + Unit* hydross = AI_VALUE2(Unit*, "find target", "hydross the unstable"); + if (!hydross) + return false; + + const uint32 instanceId = hydross->GetMap()->GetInstanceId(); + const time_t now = std::time(nullptr); + constexpr uint8 phaseStartStopSeconds = 5; + constexpr uint8 phaseEndStopSeconds = 1; + + bool shouldStopDps = false; + + // 1 second after 100% Mark of Hydross, stop DPS + auto itNature = hydrossChangeToNaturePhaseTimer.find(instanceId); + if (itNature != hydrossChangeToNaturePhaseTimer.end() && + (now - itNature->second) >= phaseEndStopSeconds) + shouldStopDps = true; + + // Keep DPS stopped for 5 seconds after transition into nature phase + auto itNatureDps = hydrossNatureDpsWaitTimer.find(instanceId); + if (itNatureDps != hydrossNatureDpsWaitTimer.end() && + (now - itNatureDps->second) < phaseStartStopSeconds) + shouldStopDps = true; + + // 1 second after 100% Mark of Corruption, stop DPS + auto itFrost = hydrossChangeToFrostPhaseTimer.find(instanceId); + if (itFrost != hydrossChangeToFrostPhaseTimer.end() && + (now - itFrost->second) >= phaseEndStopSeconds) + shouldStopDps = true; + + // Keep DPS stopped for 5 seconds after transition into frost phase + auto itFrostDps = hydrossFrostDpsWaitTimer.find(instanceId); + if (itFrostDps != hydrossFrostDpsWaitTimer.end() && + (now - itFrostDps->second) < phaseStartStopSeconds) + shouldStopDps = true; + + if (shouldStopDps) + { + botAI->Reset(); + return true; + } + + return false; +} + +bool HydrossTheUnstableManageTimersAction::Execute(Event /*event*/) +{ + Unit* hydross = AI_VALUE2(Unit*, "find target", "hydross the unstable"); + if (!hydross) + return false; + + const uint32 instanceId = hydross->GetMap()->GetInstanceId(); + const time_t now = std::time(nullptr); + + bool changed = false; + if (!hydross->HasAura(SPELL_CORRUPTION)) + { + if (hydrossFrostDpsWaitTimer.try_emplace(instanceId, now).second) + changed = true; + if (hydrossNatureDpsWaitTimer.erase(instanceId) > 0) + changed = true; + if (hydrossChangeToFrostPhaseTimer.erase(instanceId) > 0) + changed = true; + if (HasMarkOfHydrossAt100Percent(bot)) + { + if (hydrossChangeToNaturePhaseTimer.try_emplace(instanceId, now).second) + changed = true; + } + } + else + { + if (hydrossNatureDpsWaitTimer.try_emplace(instanceId, now).second) + changed = true; + if (hydrossFrostDpsWaitTimer.erase(instanceId) > 0) + changed = true; + if (hydrossChangeToNaturePhaseTimer.erase(instanceId) > 0) + changed = true; + if (HasMarkOfCorruptionAt100Percent(bot)) + { + if (hydrossChangeToFrostPhaseTimer.try_emplace(instanceId, now).second) + changed = true; + } + } + + return changed; +} + +// The Lurker Below + +// Run around behind Lurker during Spout +bool TheLurkerBelowRunAroundBehindBossAction::Execute(Event /*event*/) +{ + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker) + return false; + + float radius = frand(20.0f, 21.0f); + float botAngle = std::atan2( + bot->GetPositionY() - lurker->GetPositionY(), bot->GetPositionX() - lurker->GetPositionX()); + float relativeAngle = Position::NormalizeOrientation(botAngle - lurker->GetOrientation()); + constexpr float safeArc = M_PI / 2.0f; + + if (std::fabs(Position::NormalizeOrientation(relativeAngle - M_PI)) > safeArc / 2.0f) + { + float tangentAngle = botAngle + (relativeAngle > M_PI ? -0.1f : 0.1f); + float moveX = lurker->GetPositionX() + radius * std::cos(tangentAngle); + float moveY = lurker->GetPositionY() + radius * std::sin(tangentAngle); + botAI->Reset(); + return MoveTo(SSC_MAP_ID, moveX, moveY, lurker->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + else + { + float behindAngle = lurker->GetOrientation() + M_PI + frand(-0.5f, 0.5f) * safeArc; + float targetX = lurker->GetPositionX() + radius * std::cos(behindAngle); + float targetY = lurker->GetPositionY() + radius * std::sin(behindAngle); + if (bot->GetExactDist2d(targetX, targetY) > 2.0f) + { + botAI->Reset(); + return MoveTo(SSC_MAP_ID, targetX, targetY, lurker->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + } + + return false; +} + +bool TheLurkerBelowPositionMainTankAction::Execute(Event /*event*/) +{ + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker) + return false; + + if (bot->GetTarget() != lurker->GetGUID()) + return Attack(lurker); + + const Position& position = LURKER_MAIN_TANK_POSITION; + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 0.2f) + { + return MoveTo(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +// Assign ranged positions within a 120-degree arc behind Lurker +bool TheLurkerBelowSpreadRangedInArcAction::Execute(Event /*event*/) +{ + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker) + return false; + + std::vector rangedMembers; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !botAI->IsRanged(member)) + continue; + + rangedMembers.push_back(member); + } + } + + if (rangedMembers.empty()) + return false; + + const ObjectGuid guid = bot->GetGUID(); + + auto it = lurkerRangedPositions.find(guid); + if (it == lurkerRangedPositions.end()) + { + size_t count = rangedMembers.size(); + auto findIt = std::find(rangedMembers.begin(), rangedMembers.end(), bot); + size_t botIndex = (findIt != rangedMembers.end()) ? + std::distance(rangedMembers.begin(), findIt) : 0; + + constexpr float arcSpan = 2.0f * M_PI / 3.0f; + constexpr float arcCenter = 2.262f; + constexpr float arcStart = arcCenter - arcSpan / 2.0f; + + float angle = (count == 1) ? arcCenter : + (arcStart + arcSpan * static_cast(botIndex) / static_cast(count - 1)); + float radius = 28.0f; + + float targetX = lurker->GetPositionX() + radius * std::sin(angle); + float targetY = lurker->GetPositionY() + radius * std::cos(angle); + + lurkerRangedPositions.try_emplace(guid, Position(targetX, targetY, lurker->GetPositionZ())); + it = lurkerRangedPositions.find(guid); + } + + if (it == lurkerRangedPositions.end()) + return false; + + const Position& position = it->second; + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) + { + return MoveTo(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +// During the submerge phase, if there are >= 3 tanks in the raid, +// the first 3 will each pick up 1 Guardian +bool TheLurkerBelowTanksPickUpAddsAction::Execute(Event /*event*/) +{ + Player* mainTank = nullptr; + Player* firstAssistTank = nullptr; + Player* secondAssistTank = nullptr; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (!mainTank && botAI->IsMainTank(member)) + mainTank = member; + else if (!firstAssistTank && botAI->IsAssistTankOfIndex(member, 0, true)) + firstAssistTank = member; + else if (!secondAssistTank && botAI->IsAssistTankOfIndex(member, 1, true)) + secondAssistTank = member; + } + } + + if (!mainTank || !firstAssistTank || !secondAssistTank) + return false; + + std::vector guardians; + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + for (auto guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_COILFANG_GUARDIAN) + guardians.push_back(unit); + } + + if (guardians.size() < 3) + return false; + + std::vector tanks = { mainTank, firstAssistTank, secondAssistTank }; + std::vector rtiIndices = + { + RtiTargetValue::starIndex, + RtiTargetValue::circleIndex, + RtiTargetValue::diamondIndex + }; + std::vector rtiNames = { "star", "circle", "diamond" }; + + for (size_t i = 0; i < 3; ++i) + { + Player* tank = tanks[i]; + Unit* guardian = guardians[i]; + if (bot == tank) + { + MarkTargetWithIcon(bot, guardian, rtiIndices[i]); + SetRtiTarget(botAI, rtiNames[i], guardian); + if (bot->GetTarget() != guardian->GetGUID()) + return Attack(guardian); + } + } + + return false; +} + +bool TheLurkerBelowManageSpoutTimerAction::Execute(Event /*event*/) +{ + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker) + return false; + + const uint32 instanceId = lurker->GetMap()->GetInstanceId(); + const time_t now = std::time(nullptr); + + auto it = lurkerSpoutTimer.find(instanceId); + if (it != lurkerSpoutTimer.end() && it->second <= now) + { + lurkerSpoutTimer.erase(it); + it = lurkerSpoutTimer.end(); + } + + const time_t spoutCastTime = 20; + if (IsLurkerCastingSpout(lurker) && it == lurkerSpoutTimer.end()) + lurkerSpoutTimer.try_emplace(instanceId, now + spoutCastTime); + + return false; +} + +// Leotheras the Blind + +bool LeotherasTheBlindTargetSpellbindersAction::Execute(Event /*event*/) +{ + if (Unit* spellbinder = GetFirstAliveUnitByEntry(botAI, NPC_GREYHEART_SPELLBINDER)) + MarkTargetWithSkull(bot, spellbinder); + + return false; +} + +// Warlock tank action--see GetLeotherasDemonFormTank in RaidSSCHelpers.cpp +// Use tank strategy for Demon Form and DPS strategy for Human Form +bool LeotherasTheBlindDemonFormTankAttackBossAction::Execute(Event /*event*/) +{ + Unit* innerDemon = nullptr; + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + Creature* creature = unit ? unit->ToCreature() : nullptr; + if (creature && creature->GetEntry() == NPC_INNER_DEMON + && creature->GetSummonerGUID() == bot->GetGUID()) + { + innerDemon = creature; + break; + } + } + + if (innerDemon) + return false; + + if (Unit* leotherasDemon = GetActiveLeotherasDemon(botAI)) + { + MarkTargetWithSquare(bot, leotherasDemon); + SetRtiTarget(botAI, "square", leotherasDemon); + + if (botAI->CanCastSpell("searing pain", leotherasDemon)) + return botAI->CastSpell("searing pain", leotherasDemon); + } + + return false; +} + +// Stop melee tanks from attacking upon transformation so they don't take aggro +// Applies only if there is a Warlock tank present +bool LeotherasTheBlindMeleeTanksDontAttackDemonFormAction::Execute(Event /*event*/) +{ + bot->AttackStop(); + botAI->Reset(); + return true; +} + +// Intent is to keep enough distance from Leotheras and spread to prepare for Whirlwind +// And stay away from the Warlock tank to avoid Chaos Blasts +bool LeotherasTheBlindPositionRangedAction::Execute(Event /*event*/) +{ + constexpr float safeDistFromBoss = 15.0f; + Unit* leotherasHuman = GetLeotherasHuman(botAI); + if (leotherasHuman && bot->GetExactDist2d(leotherasHuman) < safeDistFromBoss && + leotherasHuman->GetVictim() != bot) + { + constexpr uint32 minInterval = 500; + return FleePosition(leotherasHuman->GetPosition(), safeDistFromBoss, minInterval); + } + + Group* group = bot->GetGroup(); + if (!group) + return false; + + if (GetActiveLeotherasDemon(botAI)) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot || !member->IsAlive()) + continue; + + constexpr uint32 minInterval = 0; + if (GetLeotherasDemonFormTank(bot) == member) + { + constexpr float safeDistFromTank = 10.0f; + if (bot->GetExactDist2d(member) < safeDistFromTank) + return FleePosition(member->GetPosition(), safeDistFromTank, minInterval); + } + else + { + constexpr float safeDistFromMember = 6.0f; + if (bot->GetExactDist2d(member) < safeDistFromMember) + return FleePosition(member->GetPosition(), safeDistFromMember, minInterval); + } + } + } + + return false; +} + +bool LeotherasTheBlindRunAwayFromWhirlwindAction::Execute(Event /*event*/) +{ + if (Unit* leotherasHuman = GetLeotherasHuman(botAI)) + { + float currentDistance = bot->GetExactDist2d(leotherasHuman); + constexpr float safeDistance = 25.0f; + if (currentDistance < safeDistance) + { + botAI->Reset(); + return MoveAway(leotherasHuman, safeDistance - currentDistance); + } + } + + return false; +} + +// This method is likely unnecessary unless the player does not use a Warlock tank +// If a melee tank is used, other melee needs to run away after too many Chaos Blast stacks +bool LeotherasTheBlindMeleeDpsRunAwayFromBossAction::Execute(Event /*event*/) +{ + if (botAI->CanCastSpell("cloak of shadows", bot)) + return botAI->CastSpell("cloak of shadows", bot); + + Unit* leotheras = GetPhase2LeotherasDemon(botAI); + if (!leotheras) + return false; + + Unit* demonVictim = leotheras->GetVictim(); + if (!demonVictim) + return false; + + float currentDistance = bot->GetExactDist2d(demonVictim); + constexpr float safeDistance = 10.0f; + if (currentDistance < safeDistance) + { + botAI->Reset(); + if (demonVictim != bot) + return MoveAway(demonVictim, safeDistance - currentDistance); + } + + return false; +} + +// Hardcoded actions for healers and bear tanks to kill Inner Demons +bool LeotherasTheBlindDestroyInnerDemonAction::Execute(Event /*event*/) +{ + Unit* innerDemon = nullptr; + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + Creature* creature = unit ? unit->ToCreature() : nullptr; + if (creature && creature->GetEntry() == NPC_INNER_DEMON + && creature->GetSummonerGUID() == bot->GetGUID()) + { + innerDemon = creature; + break; + } + } + + if (innerDemon) + { + if (botAI->IsTank(bot) && bot->getClass() == CLASS_DRUID) + return HandleFeralTankStrategy(innerDemon); + + if (botAI->IsHeal(bot)) + return HandleHealerStrategy(innerDemon); + + // Roles without a strategy need to affirmatively attack their Inner Demons + // Because DPS assist is disabled via multipliers + if (bot->GetTarget() != innerDemon->GetGUID()) + return Attack(innerDemon); + } + + return false; +} + +// At 50% nerfed damage, bears have trouble killing their Inner Demons without a specific strategy +// Warrior and Paladin tanks have no trouble in my experience (Prot Warriors have high DPS, and +// Prot Paladins have an advantage in that Inner Demons are weak to Holy) +bool LeotherasTheBlindDestroyInnerDemonAction::HandleFeralTankStrategy(Unit* innerDemon) +{ + if (bot->HasAura(SPELL_DIRE_BEAR_FORM)) + bot->RemoveAura(SPELL_DIRE_BEAR_FORM); + + if (bot->HasAura(SPELL_BEAR_FORM)) + bot->RemoveAura(SPELL_BEAR_FORM); + + bool casted = false; + if (!bot->HasAura(SPELL_CAT_FORM) && botAI->CanCastSpell("cat form", bot)) + { + if (botAI->CastSpell("cat form", bot)) + casted = true; + } + if (botAI->CanCastSpell("berserk", bot)) + { + if (botAI->CastSpell("berserk", bot)) + casted = true; + } + if (bot->GetPower(POWER_ENERGY) < 30 && botAI->CanCastSpell("tiger's fury", bot)) + { + if (botAI->CastSpell("tiger's fury", bot)) + casted = true; + } + if (bot->GetComboPoints() >= 4 && botAI->CanCastSpell("ferocious bite", innerDemon)) + { + if (botAI->CastSpell("ferocious bite", innerDemon)) + casted = true; + } + if (bot->GetComboPoints() == 0 && innerDemon->GetHealthPct() > 25.0f && + botAI->CanCastSpell("rake", innerDemon)) + { + if (botAI->CastSpell("rake", innerDemon)) + casted = true; + } + if (botAI->CanCastSpell("mangle (cat)", innerDemon)) + { + if (botAI->CastSpell("mangle (cat)", innerDemon)) + casted = true; + } + + return casted; +} + +bool LeotherasTheBlindDestroyInnerDemonAction::HandleHealerStrategy(Unit* innerDemon) +{ + if (bot->getClass() == CLASS_DRUID) + { + if (bot->HasAura(SPELL_TREE_OF_LIFE)) + bot->RemoveAura(SPELL_TREE_OF_LIFE); + + bool casted = false; + if (botAI->CanCastSpell("barkskin", bot)) + { + if (botAI->CastSpell("barkskin", bot)) + casted = true; + } + if (botAI->CanCastSpell("wrath", innerDemon)) + { + if (botAI->CastSpell("wrath", innerDemon)) + casted = true; + } + + return casted; + } + else if (bot->getClass() == CLASS_PALADIN) + { + bool casted = false; + if (botAI->CanCastSpell("avenging wrath", bot)) + { + if (botAI->CastSpell("avenging wrath", bot)) + casted = true; + } + if (botAI->CanCastSpell("consecration", bot)) + { + if (botAI->CastSpell("consecration", bot)) + casted = true; + } + if (botAI->CanCastSpell("exorcism", innerDemon)) + { + if (botAI->CastSpell("exorcism", innerDemon)) + casted = true; + } + if (botAI->CanCastSpell("hammer of wrath", innerDemon)) + { + if (botAI->CastSpell("hammer of wrath", innerDemon)) + casted = true; + } + if (botAI->CanCastSpell("holy shock", innerDemon)) + { + if (botAI->CastSpell("holy shock", innerDemon)) + casted = true; + } + if (botAI->CanCastSpell("judgment of light", innerDemon)) + { + if (botAI->CastSpell("judgment of light", innerDemon)) + casted = true; + } + + return casted; + } + else if (bot->getClass() == CLASS_PRIEST) + { + if (botAI->CanCastSpell("smite", innerDemon)) + return botAI->CastSpell("smite", innerDemon); + } + else if (bot->getClass() == CLASS_SHAMAN) + { + bool casted = false; + if (botAI->CanCastSpell("earth shock", innerDemon)) + { + if (botAI->CastSpell("earth shock", innerDemon)) + casted = true; + } + if (botAI->CanCastSpell("chain lightning", innerDemon)) + { + if (botAI->CastSpell("chain lightning", innerDemon)) + casted = true; + } + if (botAI->CanCastSpell("lightning bolt", innerDemon)) + { + if (botAI->CastSpell("lightning bolt", innerDemon)) + casted = true; + } + + return casted; + } + + return false; +} + +// Everybody except the Warlock tank should focus on Leotheras in Phase 3 +bool LeotherasTheBlindFinalPhaseAssignDpsPriorityAction::Execute(Event /*event*/) +{ + Unit* leotherasHuman = GetLeotherasHuman(botAI); + if (!leotherasHuman) + return false; + + MarkTargetWithStar(bot, leotherasHuman); + SetRtiTarget(botAI, "star", leotherasHuman); + + if (bot->GetTarget() != leotherasHuman->GetGUID()) + return Attack(leotherasHuman); + + Unit* leotherasDemon = GetPhase3LeotherasDemon(botAI); + if (leotherasDemon) + { + if (leotherasHuman->GetVictim() == bot) + { + Unit* demonTarget = leotherasDemon->GetVictim(); + if (demonTarget && leotherasHuman->GetExactDist2d(demonTarget) < 20.0f) + { + float angle = atan2(bot->GetPositionY() - demonTarget->GetPositionY(), + bot->GetPositionX() - demonTarget->GetPositionX()); + float targetX = bot->GetPositionX() + 25.0f * std::cos(angle); + float targetY = bot->GetPositionY() + 25.0f * std::sin(angle); + + return MoveTo(SSC_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + } + } + + return false; +} + +// Misdirect to Warlock tank or to main tank if there is no Warlock tank +bool LeotherasTheBlindMisdirectBossToDemonFormTankAction::Execute(Event /*event*/) +{ + Unit* leotherasDemon = GetActiveLeotherasDemon(botAI); + if (!leotherasDemon) + return false; + + Player* demonFormTank = GetLeotherasDemonFormTank(bot); + Player* targetTank = demonFormTank; + + if (!targetTank) + { + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsMainTank(member)) + { + targetTank = member; + break; + } + } + } + } + + if (!targetTank) + return false; + + if (botAI->CanCastSpell("misdirection", targetTank)) + return botAI->CastSpell("misdirection", targetTank); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", leotherasDemon)) + return botAI->CastSpell("steady shot", leotherasDemon); + + return false; +} + +// This does not pause DPS after a Whirlwind, which is also an aggro wipe +bool LeotherasTheBlindManageDpsWaitTimersAction::Execute(Event /*event*/) +{ + Unit* leotheras = AI_VALUE2(Unit*, "find target", "leotheras the blind"); + if (!leotheras) + return false; + + const uint32 instanceId = leotheras->GetMap()->GetInstanceId(); + const time_t now = std::time(nullptr); + + bool changed = false; + // Encounter start/reset: clear all timers + if (leotheras->HasAura(SPELL_LEOTHERAS_BANISHED)) + { + if (leotherasHumanFormDpsWaitTimer.erase(instanceId) > 0) + changed = true; + if (leotherasDemonFormDpsWaitTimer.erase(instanceId) > 0) + changed = true; + if (leotherasFinalPhaseDpsWaitTimer.erase(instanceId) > 0) + changed = true; + } + + // Human Phase + Unit* leotherasHuman = GetLeotherasHuman(botAI); + Unit* leotherasPhase3Demon = GetPhase3LeotherasDemon(botAI); + if (leotherasHuman && !leotherasPhase3Demon) + { + if (leotherasHumanFormDpsWaitTimer.try_emplace(instanceId, now).second) + changed = true; + if (leotherasDemonFormDpsWaitTimer.erase(instanceId) > 0) + changed = true; + } + // Demon Phase + else if (Unit* leotherasPhase2Demon = GetPhase2LeotherasDemon(botAI)) + { + if (leotherasDemonFormDpsWaitTimer.try_emplace(instanceId, now).second) + changed = true; + if (leotherasHumanFormDpsWaitTimer.erase(instanceId) > 0) + changed = true; + } + // Final Phase (<15% HP) + else if (leotherasHuman && leotherasPhase3Demon) + { + if (leotherasFinalPhaseDpsWaitTimer.try_emplace(instanceId, now).second) + changed = true; + if (leotherasHumanFormDpsWaitTimer.erase(instanceId) > 0) + changed = true; + if (leotherasDemonFormDpsWaitTimer.erase(instanceId) > 0) + changed = true; + } + + return changed; +} + +// Fathom-Lord Karathress +// Note: 4 tanks are required for the full strategy, and having at least 2 +// is crucial to separate Caribdis from the others + +// Karathress is tanked near his starting position +bool FathomLordKarathressMainTankPositionBossAction::Execute(Event /*event*/) +{ + Unit* karathress = AI_VALUE2(Unit*, "find target", "fathom-lord karathress"); + if (!karathress) + return false; + + MarkTargetWithTriangle(bot, karathress); + SetRtiTarget(botAI, "triangle", karathress); + + if (bot->GetTarget() != karathress->GetGUID()) + return Attack(karathress); + + if (karathress->GetVictim() == bot && bot->IsWithinMeleeRange(karathress)) + { + const Position& position = KARATHRESS_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 3.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(SSC_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +// Caribdis is pulled far to the West in the corner +// Best to use a Warrior or Druid tank for interrupts +bool FathomLordKarathressFirstAssistTankPositionCaribdisAction::Execute(Event /*event*/) +{ + Unit* caribdis = AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); + if (!caribdis) + return false; + + MarkTargetWithDiamond(bot, caribdis); + SetRtiTarget(botAI, "diamond", caribdis); + + if (bot->GetTarget() != caribdis->GetGUID()) + return Attack(caribdis); + + if (caribdis->GetVictim() == bot) + { + const Position& position = CARIBDIS_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 3.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +// Sharkkis is pulled North to the other side of the ramp +bool FathomLordKarathressSecondAssistTankPositionSharkkisAction::Execute(Event /*event*/) +{ + Unit* sharkkis = AI_VALUE2(Unit*, "find target", "fathom-guard sharkkis"); + if (!sharkkis) + return false; + + MarkTargetWithStar(bot, sharkkis); + SetRtiTarget(botAI, "star", sharkkis); + + if (bot->GetTarget() != sharkkis->GetGUID()) + return Attack(sharkkis); + + if (sharkkis->GetVictim() == bot && bot->IsWithinMeleeRange(sharkkis)) + { + const Position& position = SHARKKIS_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 3.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +// Tidalvess is pulled Northwest near the pillar +bool FathomLordKarathressThirdAssistTankPositionTidalvessAction::Execute(Event /*event*/) +{ + Unit* tidalvess = AI_VALUE2(Unit*, "find target", "fathom-guard tidalvess"); + if (!tidalvess) + return false; + + MarkTargetWithCircle(bot, tidalvess); + SetRtiTarget(botAI, "circle", tidalvess); + + if (bot->GetTarget() != tidalvess->GetGUID()) + return Attack(tidalvess); + + if (tidalvess->GetVictim() == bot && bot->IsWithinMeleeRange(tidalvess)) + { + const Position& position = TIDALVESS_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 3.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +// Caribdis's tank spot is far away so a dedicated healer is needed +// Use the assistant flag to select the healer +bool FathomLordKarathressPositionCaribdisTankHealerAction::Execute(Event /*event*/) +{ + Unit* caribdis = AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); + if (!caribdis) + return false; + + const Position& position = CARIBDIS_HEALER_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 3.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +// Misdirect priority: (1) Caribdis tank, (2) Tidalvess tank, (3) Sharkkis tank +bool FathomLordKarathressMisdirectBossesToTanksAction::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; + } + + int 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* bossTarget = nullptr; + Player* tankTarget = nullptr; + if (hunterIndex == 0) + { + bossTarget = AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsAssistTankOfIndex(member, 0, false)) + { + tankTarget = member; + break; + } + } + } + else if (hunterIndex == 1) + { + bossTarget = AI_VALUE2(Unit*, "find target", "fathom-guard tidalvess"); + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsAssistTankOfIndex(member, 2, false)) + { + tankTarget = member; + break; + } + } + } + else if (hunterIndex == 2) + { + bossTarget = AI_VALUE2(Unit*, "find target", "fathom-guard sharkkis"); + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsAssistTankOfIndex(member, 1, false)) + { + tankTarget = member; + break; + } + } + } + + if (!bossTarget || !tankTarget) + return false; + + if (botAI->CanCastSpell("misdirection", tankTarget)) + return botAI->CastSpell("misdirection", tankTarget); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", bossTarget)) + return botAI->CastSpell("steady shot", bossTarget); + + return false; +} + +// Kill order is non-standard because bots handle Cyclones poorly and need more time +// to get her down than real players (standard is ranged DPS help with Sharkkis first) +bool FathomLordKarathressAssignDpsPriorityAction::Execute(Event /*event*/) +{ + // Target priority 1: Spitfire Totems for melee dps + Unit* totem = GetFirstAliveUnitByEntry(botAI, NPC_SPITFIRE_TOTEM); + if (totem && botAI->IsMelee(bot) && botAI->IsDps(bot)) + { + MarkTargetWithSkull(bot, totem); + SetRtiTarget(botAI, "skull", totem); + + if (bot->GetTarget() != totem->GetGUID()) + return Attack(totem); + + // Direct movement order due to path between Sharkkis and totem sometimes being screwy + if (!bot->IsWithinMeleeRange(totem)) + { + return MoveTo(SSC_MAP_ID, totem->GetPositionX(), totem->GetPositionY(), + totem->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; + } + + // Target priority 2: Tidalvess for all dps + Unit* tidalvess = AI_VALUE2(Unit*, "find target", "fathom-guard tidalvess"); + if (tidalvess) + { + MarkTargetWithCircle(bot, tidalvess); + SetRtiTarget(botAI, "circle", tidalvess); + + if (bot->GetTarget() != tidalvess->GetGUID()) + return Attack(tidalvess); + + return false; + } + + // Target priority 3: Caribdis for ranged dps + Unit* caribdis = AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); + if (botAI->IsRangedDps(bot) && caribdis) + { + MarkTargetWithDiamond(bot, caribdis); + SetRtiTarget(botAI, "diamond", caribdis); + + const Position& position = CARIBDIS_RANGED_DPS_POSITION; + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) + { + return MoveInside(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), 8.0f, MovementPriority::MOVEMENT_COMBAT); + } + + if (bot->GetTarget() != caribdis->GetGUID()) + return Attack(caribdis); + + return false; + } + + // Target priority 4: Sharkkis for melee dps and, after Caribdis is down, ranged dps also + Unit* sharkkis = AI_VALUE2(Unit*, "find target", "fathom-guard sharkkis"); + if (sharkkis) + { + MarkTargetWithStar(bot, sharkkis); + SetRtiTarget(botAI, "star", sharkkis); + + if (bot->GetTarget() != sharkkis->GetGUID()) + return Attack(sharkkis); + + return false; + } + + // Target priority 5: Sharkkis pets for all dps + Unit* fathomSporebat = AI_VALUE2(Unit*, "find target", "fathom sporebat"); + if (fathomSporebat && botAI->IsMelee(bot)) + { + MarkTargetWithCross(bot, fathomSporebat); + SetRtiTarget(botAI, "cross", fathomSporebat); + + if (bot->GetTarget() != fathomSporebat->GetGUID()) + return Attack(fathomSporebat); + + return false; + } + + Unit* fathomLurker = AI_VALUE2(Unit*, "find target", "fathom lurker"); + if (fathomLurker && botAI->IsMelee(bot)) + { + MarkTargetWithSquare(bot, fathomLurker); + SetRtiTarget(botAI, "square", fathomLurker); + + if (bot->GetTarget() != fathomLurker->GetGUID()) + return Attack(fathomLurker); + + return false; + } + + // Target priority 6: Karathress for all dps + Unit* karathress = AI_VALUE2(Unit*, "find target", "fathom-lord karathress"); + if (karathress) + { + MarkTargetWithTriangle(bot, karathress); + SetRtiTarget(botAI, "triangle", karathress); + + if (bot->GetTarget() != karathress->GetGUID()) + return Attack(karathress); + } + + return false; +} + +bool FathomLordKarathressManageDpsTimerAction::Execute(Event /*event*/) +{ + Unit* karathress = AI_VALUE2(Unit*, "find target", "fathom-lord karathress"); + if (!karathress) + return false; + + karathressDpsWaitTimer.try_emplace( + karathress->GetMap()->GetInstanceId(), std::time(nullptr)); + + return false; +} + +// Morogrim Tidewalker + +bool MorogrimTidewalkerMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* tidewalker = AI_VALUE2(Unit*, "find target", "morogrim tidewalker"); + if (!tidewalker) + return false; + + Player* mainTank = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + } + + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", tidewalker)) + return botAI->CastSpell("steady shot", tidewalker); + + return false; +} + +// Separate tanking positions are used for phase 1 and phase 2 to address the +// Water Globule mechanic in phase 2 +bool MorogrimTidewalkerMoveBossToTankPositionAction::Execute(Event /*event*/) +{ + Unit* tidewalker = AI_VALUE2(Unit*, "find target", "morogrim tidewalker"); + if (!tidewalker) + return false; + + if (bot->GetTarget() != tidewalker->GetGUID()) + return Attack(tidewalker); + + if (tidewalker->GetVictim() == bot && bot->IsWithinMeleeRange(tidewalker)) + { + if (tidewalker->GetHealthPct() > 26.0f) + return MoveToPhase1TankPosition(tidewalker); + else + return MoveToPhase2TankPosition(tidewalker); + } + + return false; +} + +// Phase 1: tank position is up against the Northeast pillar +bool MorogrimTidewalkerMoveBossToTankPositionAction::MoveToPhase1TankPosition(Unit* tidewalker) +{ + const Position& phase1 = TIDEWALKER_PHASE_1_TANK_POSITION; + float distToPhase1 = bot->GetExactDist2d(phase1.GetPositionX(), phase1.GetPositionY()); + if (distToPhase1 > 1.0f) + { + float dX = phase1.GetPositionX() - bot->GetPositionX(); + float dY = phase1.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPhase1); + float moveX = bot->GetPositionX() + (dX / distToPhase1) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPhase1) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, phase1.GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + + return false; +} + +// Phase 2: move in two steps to get around the pillar and back up into the Northeast corner +bool MorogrimTidewalkerMoveBossToTankPositionAction::MoveToPhase2TankPosition(Unit* tidewalker) +{ + const Position& phase2 = TIDEWALKER_PHASE_2_TANK_POSITION; + const Position& transition = TIDEWALKER_PHASE_TRANSITION_WAYPOINT; + + auto itStep = tidewalkerTankStep.find(bot->GetGUID()); + uint8 step = (itStep != tidewalkerTankStep.end()) ? itStep->second : 0; + + if (step == 0) + { + float distToTransition = + bot->GetExactDist2d(transition.GetPositionX(), transition.GetPositionY()); + + if (distToTransition > 2.0f) + { + float dX = transition.GetPositionX() - bot->GetPositionX(); + float dY = transition.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToTransition); + float moveX = bot->GetPositionX() + (dX / distToTransition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToTransition) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, transition.GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + else + tidewalkerTankStep.try_emplace(bot->GetGUID(), 1); + } + + if (step == 1) + { + float distToPhase2 = + bot->GetExactDist2d(phase2.GetPositionX(), phase2.GetPositionY()); + + if (distToPhase2 > 1.0f) + { + float dX = phase2.GetPositionX() - bot->GetPositionX(); + float dY = phase2.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPhase2); + float moveX = bot->GetPositionX() + (dX / distToPhase2) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPhase2) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, phase2.GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +// Ranged stack behind the boss in the Northeast corner in phase 2 +// No corresponding method for melee since they will do so anyway +bool MorogrimTidewalkerPhase2RepositionRangedAction::Execute(Event /*event*/) +{ + Unit* tidewalker = AI_VALUE2(Unit*, "find target", "morogrim tidewalker"); + if (!tidewalker) + return false; + + const Position& phase2 = TIDEWALKER_PHASE_2_RANGED_POSITION; + const Position& transition = TIDEWALKER_PHASE_TRANSITION_WAYPOINT; + + auto itStep = tidewalkerRangedStep.find(bot->GetGUID()); + uint8 step = (itStep != tidewalkerRangedStep.end()) ? itStep->second : 0; + + if (step == 0) + { + float distToTransition = + bot->GetExactDist2d(transition.GetPositionX(), transition.GetPositionY()); + + if (distToTransition > 2.0f) + { + float dX = transition.GetPositionX() - bot->GetPositionX(); + float dY = transition.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToTransition); + float moveX = bot->GetPositionX() + (dX / distToTransition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToTransition) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, transition.GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + else + { + tidewalkerRangedStep.try_emplace(bot->GetGUID(), 1); + step = 1; + } + } + + if (step == 1) + { + float distToPhase2 = + bot->GetExactDist2d(phase2.GetPositionX(), phase2.GetPositionY()); + + if (distToPhase2 > 1.0f) + { + float dX = phase2.GetPositionX() - bot->GetPositionX(); + float dY = phase2.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPhase2); + float moveX = bot->GetPositionX() + (dX / distToPhase2) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPhase2) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, phase2.GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +// Lady Vashj + +bool LadyVashjMainTankPositionBossAction::Execute(Event /*event*/) +{ + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + return false; + + if (bot->GetTarget() != vashj->GetGUID()) + return Attack(vashj); + + if (vashj->GetVictim() == bot && bot->IsWithinMeleeRange(vashj)) + { + // Phase 1: Position Vashj in the center of the platform + if (IsLadyVashjInPhase1(botAI)) + { + const Position& position = VASHJ_PLATFORM_CENTER_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(SSC_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + // Phase 3: No fixed position, but move Vashj away from Enchanted Elementals + else if (IsLadyVashjInPhase3(botAI)) + { + Unit* enchanted = AI_VALUE2(Unit*, "find target", "enchanted elemental"); + if (enchanted) + { + float currentDistance = bot->GetExactDist2d(enchanted); + constexpr float safeDistance = 10.0f; + if (currentDistance < safeDistance) + return MoveAway(enchanted, safeDistance - currentDistance); + } + } + } + + return false; +} + +// Semicircle around center of the room (to allow escape paths by Static Charged bots) +bool LadyVashjPhase1SpreadRangedInArcAction::Execute(Event /*event*/) +{ + std::vector spreadMembers; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && GET_PLAYERBOT_AI(member)) + { + if (botAI->IsRanged(member)) + spreadMembers.push_back(member); + } + } + } + + const ObjectGuid guid = bot->GetGUID(); + + auto itPos = vashjRangedPositions.find(guid); + auto itReached = hasReachedVashjRangedPosition.find(guid); + if (itPos == vashjRangedPositions.end()) + { + auto it = std::find(spreadMembers.begin(), spreadMembers.end(), bot); + size_t botIndex = (it != spreadMembers.end()) ? + std::distance(spreadMembers.begin(), it) : 0; + size_t count = spreadMembers.size(); + if (count == 0) + return false; + + const Position& center = VASHJ_PLATFORM_CENTER_POSITION; + constexpr float minRadius = 20.0f; + constexpr float maxRadius = 30.0f; + + constexpr float arcCenter = M_PI / 2.0f; // North + constexpr float arcSpan = M_PI; // 180° + constexpr float arcStart = arcCenter - arcSpan / 2.0f; + + float angle; + if (count == 1) + angle = arcCenter; + else + angle = arcStart + (static_cast(botIndex) / (count - 1)) * arcSpan; + + float radius = frand(minRadius, maxRadius); + float targetX = center.GetPositionX() + radius * std::cos(angle); + float targetY = center.GetPositionY() + radius * std::sin(angle); + + auto res = vashjRangedPositions.try_emplace(guid, Position(targetX, targetY, center.GetPositionZ())); + itPos = res.first; + hasReachedVashjRangedPosition.try_emplace(guid, false); + itReached = hasReachedVashjRangedPosition.find(guid); + } + + if (itPos == vashjRangedPositions.end()) + return false; + + Position position = itPos->second; + if (itReached == hasReachedVashjRangedPosition.end() || !(itReached->second)) + { + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) + { + return MoveTo(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + if (itReached != hasReachedVashjRangedPosition.end()) + itReached->second = true; + } + + return false; +} + +// For absorbing Shock Burst +bool LadyVashjSetGroundingTotemInMainTankGroupAction::Execute(Event /*event*/) +{ + Player* mainTank = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + } + + if (!mainTank) + return false; + + if (bot->GetExactDist2d(mainTank) > 25.0f) + { + return MoveInside(SSC_MAP_ID, mainTank->GetPositionX(), mainTank->GetPositionY(), + mainTank->GetPositionZ(), 20.0f, MovementPriority::MOVEMENT_COMBAT); + } + + if (!botAI->HasStrategy("grounding", BotState::BOT_STATE_COMBAT)) + botAI->ChangeStrategy("+grounding", BotState::BOT_STATE_COMBAT); + + if (!bot->HasAura(SPELL_GROUNDING_TOTEM_EFFECT) && + botAI->CanCastSpell("grounding totem", bot)) + return botAI->CastSpell("grounding totem", bot); + + return false; +} + +bool LadyVashjMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + return false; + + Player* mainTank = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + } + + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", vashj)) + return botAI->CastSpell("steady shot", vashj); + + return false; +} + +bool LadyVashjStaticChargeMoveAwayFromGroupAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + // If the main tank has Static Charge, other group members should move away + Player* mainTank = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsMainTank(member) && + member->HasAura(SPELL_STATIC_CHARGE)) + { + mainTank = member; + break; + } + } + + if (mainTank && bot != mainTank) + { + float currentDistance = bot->GetExactDist2d(mainTank); + constexpr float safeDistance = 11.0f; + if (currentDistance < safeDistance) + return MoveAway(mainTank, safeDistance - currentDistance); + } + + // If any other bot has Static Charge, it should move away from other group members + if (!botAI->IsMainTank(bot) && bot->HasAura(SPELL_STATIC_CHARGE)) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; + + float currentDistance = bot->GetExactDist2d(member); + constexpr float safeDistance = 11.0f; + if (currentDistance < safeDistance) + return MoveFromGroup(safeDistance); + } + } + + return false; +} + +bool LadyVashjAssignPhase2AndPhase3DpsPriorityAction::Execute(Event /*event*/) +{ + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + return false; + + const Position& center = VASHJ_PLATFORM_CENTER_POSITION; + float platformZ = center.GetPositionZ(); + if (bot->GetPositionZ() - platformZ > 2.0f) + { + // This block is needed to prevent bots from floating into the air to attack sporebats + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + bot->StopMoving(); + bot->GetMotionMaster()->Clear(); + bot->TeleportTo(SSC_MAP_ID, bot->GetPositionX(), bot->GetPositionY(), + platformZ, bot->GetOrientation()); + return true; + } + + auto const& attackers = + botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + Unit* target = nullptr; + Unit* enchanted = nullptr; + Unit* elite = nullptr; + Unit* strider = nullptr; + Unit* sporebat = nullptr; + + // Search and attack radius are intended to keep bots from going down the stairs + const float maxSearchRange = + botAI->IsRanged(bot) ? 60.0f : 55.0f; + const float maxPursueRange = maxSearchRange - 5.0f; + + for (auto guid : attackers) + { + Unit* unit = botAI->GetUnit(guid); + if (!IsValidLadyVashjCombatNpc(unit, botAI)) + continue; + + float distFromCenter = unit->GetExactDist2d(center.GetPositionX(), center.GetPositionY()); + if (IsLadyVashjInPhase2(botAI) && distFromCenter > maxSearchRange) + continue; + + switch (unit->GetEntry()) + { + case NPC_ENCHANTED_ELEMENTAL: + if (!enchanted || vashj->GetExactDist2d(unit) < vashj->GetExactDist2d(enchanted)) + enchanted = unit; + break; + + case NPC_COILFANG_ELITE: + if (!elite || unit->GetHealthPct() < elite->GetHealthPct()) + elite = unit; + break; + + case NPC_COILFANG_STRIDER: + if (!strider || unit->GetHealthPct() < strider->GetHealthPct()) + strider = unit; + break; + + case NPC_TOXIC_SPOREBAT: + if (!sporebat || bot->GetExactDist2d(unit) < bot->GetExactDist2d(sporebat)) + sporebat = unit; + break; + + case NPC_LADY_VASHJ: + vashj = unit; + break; + + default: + break; + } + } + + std::vector targets; + if (IsLadyVashjInPhase2(botAI)) + { + if (botAI->IsRanged(bot)) + { + // Hunters and Mages prioritize Enchanted Elementals, + // while other ranged DPS prioritize Striders + if (bot->getClass() == CLASS_HUNTER || bot->getClass() == CLASS_MAGE) + targets = { enchanted, strider, elite }; + else + targets = { strider, elite, enchanted }; + } + else if (botAI->IsMelee(bot) && botAI->IsDps(bot)) + targets = { enchanted, elite }; + else if (botAI->IsTank(bot)) + { + if (botAI->HasCheat(BotCheatMask::raid) && botAI->IsAssistTankOfIndex(bot, 0, true)) + targets = { strider, elite, enchanted }; + else + targets = { elite, strider, enchanted }; + } + else + targets = { enchanted, elite, strider }; + } + + if (IsLadyVashjInPhase3(botAI)) + { + if (botAI->IsTank(bot)) + { + if (botAI->IsMainTank(bot)) + { + MarkTargetWithDiamond(bot, vashj); + SetRtiTarget(botAI, "diamond", vashj); + targets = { vashj }; + } + else if (botAI->IsAssistTankOfIndex(bot, 0, true)) + { + if (botAI->HasCheat(BotCheatMask::raid)) + targets = { strider, elite, enchanted, vashj }; + } + else + targets = { elite, strider, enchanted, vashj }; + } + else if (botAI->IsRanged(bot)) + { + // Hunters are assigned to kill Sporebats in Phase 3 + if (bot->getClass() == CLASS_HUNTER) + targets = { sporebat, enchanted, strider, elite, vashj }; + else + targets = { enchanted, strider, elite, vashj }; + } + else if (botAI->IsMelee(bot) && botAI->IsDps(bot)) + targets = { enchanted, elite, vashj }; + else + targets = { enchanted, elite, strider, vashj }; + } + + for (Unit* candidate : targets) + { + if (candidate && bot->GetExactDist2d(candidate) <= maxPursueRange) + { + target = candidate; + break; + } + } + + Unit* currentTarget = context->GetValue("current target")->Get(); + + if (currentTarget && !IsValidLadyVashjCombatNpc(currentTarget, botAI)) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + context->GetValue("current target")->Set(nullptr); + bot->SetTarget(ObjectGuid::Empty); + bot->SetSelection(ObjectGuid()); + currentTarget = nullptr; + } + + if (target && currentTarget != target && bot->GetTarget() != target->GetGUID()) + return Attack(target); + + // If bots have wandered too far from the center, move them back + if (bot->GetExactDist2d(center.GetPositionX(), center.GetPositionY()) > 55.0f) + { + Player* designatedLooter = GetDesignatedCoreLooter(bot->GetGroup(), botAI); + Player* firstCorePasser = GetFirstTaintedCorePasser(bot->GetGroup(), botAI); + // A bot will not move back to the middle if (1) there is a Tainted Elemental, and + // (2) the bot is either the designated looter or the first core passer + if (Unit* tainted = AI_VALUE2(Unit*, "find target", "tainted elemental")) + { + if ((designatedLooter && designatedLooter == bot) || + (firstCorePasser && firstCorePasser == bot)) + return false; + } + + return MoveInside(SSC_MAP_ID, center.GetPositionX(), center.GetPositionY(), + center.GetPositionZ(), 40.0f, MovementPriority::MOVEMENT_COMBAT); + } + + return false; +} + +bool LadyVashjMisdirectStriderToFirstAssistTankAction::Execute(Event /*event*/) +{ + // Striders are not tankable without a cheat to block Fear so there is + // no point in misdirecting if raid cheats are not enabled + if (!botAI->HasCheat(BotCheatMask::raid)) + return false; + + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* strider = GetFirstAliveUnitByEntry(botAI, NPC_COILFANG_STRIDER); + if (!strider) + return false; + + Player* firstAssistTank = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && botAI->IsAssistTankOfIndex(member, 0, true)) + { + firstAssistTank = member; + break; + } + } + } + + if (!firstAssistTank || strider->GetVictim() == firstAssistTank) + return false; + + if (botAI->CanCastSpell("misdirection", firstAssistTank)) + return botAI->CastSpell("misdirection", firstAssistTank); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", strider)) + return botAI->CastSpell("steady shot", strider); + + return false; +} + +bool LadyVashjTankAttackAndMoveAwayStriderAction::Execute(Event /*event*/) +{ + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + return false; + + Unit* strider = GetFirstAliveUnitByEntry(botAI, NPC_COILFANG_STRIDER); + if (!strider) + return false; + + // Raid cheat automatically applies Fear Ward to tanks to make Strider tankable + // This simulates the real-life strategy where the Strider can be meleed by + // players wearing an Ogre Suit (due to the extended combat reach) + if (botAI->HasCheat(BotCheatMask::raid) && botAI->IsTank(bot)) + { + if (!bot->HasAura(SPELL_FEAR_WARD)) + bot->AddAura(SPELL_FEAR_WARD, bot); + + if (botAI->IsAssistTankOfIndex(bot, 0, true) && + bot->GetTarget() != strider->GetGUID()) + return Attack(strider); + + if (strider->GetVictim() == bot) + { + float currentDistance = bot->GetExactDist2d(vashj); + constexpr float safeDistance = 28.0f; + + if (currentDistance < safeDistance) + return MoveAway(vashj, safeDistance - currentDistance); + } + + return false; + } + + // Don't move away if raid cheats are enabled, or in any case if the bot is a tank + if (!botAI->HasCheat(BotCheatMask::raid) || !botAI->IsTank(bot)) + { + float currentDistance = bot->GetExactDist2d(strider); + constexpr float safeDistance = 20.0f; + if (currentDistance < safeDistance) + return MoveAway(strider, safeDistance - currentDistance); + } + + // Try to root/slow the Strider if it is not tankable (poor man's kiting strategy) + if (!botAI->HasCheat(BotCheatMask::raid)) + { + if (!strider->HasAura(SPELL_HEAVY_NETHERWEAVE_NET)) + { + Item* net = bot->GetItemByEntry(ITEM_HEAVY_NETHERWEAVE_NET); + if (net && botAI->HasItemInInventory(ITEM_HEAVY_NETHERWEAVE_NET) && + botAI->CanCastSpell("heavy netherweave net", strider)) + return botAI->CastSpell("heavy netherweave net", strider); + } + + if (!botAI->HasAura("frost shock", strider) && bot->getClass() == CLASS_SHAMAN && + botAI->CanCastSpell("frost shock", strider)) + return botAI->CastSpell("frost shock", strider); + + if (!strider->HasAura(SPELL_CURSE_OF_EXHAUSTION) && bot->getClass() == CLASS_WARLOCK && + botAI->CanCastSpell("curse of exhaustion", strider)) + return botAI->CastSpell("curse of exhaustion", strider); + + if (!strider->HasAura(SPELL_SLOW) && bot->getClass() == CLASS_MAGE && + botAI->CanCastSpell("slow", strider)) + return botAI->CastSpell("slow", strider); + } + + return false; +} + +// If cheats are enabled, the first returned melee DPS bot will teleport to Tainted Elementals +// Such bot will recover HP and remove the Poison Bolt debuff while attacking the elemental +bool LadyVashjTeleportToTaintedElementalAction::Execute(Event /*event*/) +{ + Unit* tainted = AI_VALUE2(Unit*, "find target", "tainted elemental"); + if (!tainted) + return false; + + if (bot->GetExactDist2d(tainted) >= 10.0f) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + bot->TeleportTo(SSC_MAP_ID, tainted->GetPositionX(), tainted->GetPositionY(), + tainted->GetPositionZ(), tainted->GetOrientation()); + } + + if (bot->GetTarget() != tainted->GetGUID()) + { + MarkTargetWithStar(bot, tainted); + SetRtiTarget(botAI, "star", tainted); + return Attack(tainted); + } + + if (bot->GetExactDist2d(tainted) < 5.0f) + { + bot->SetFullHealth(); + bot->RemoveAura(SPELL_POISON_BOLT); + } + + return false; +} + +bool LadyVashjLootTaintedCoreAction::Execute(Event) +{ + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + return false; + + auto const& corpses = context->GetValue("nearest corpses")->Get(); + const float maxLootRange = sPlayerbotAIConfig.lootDistance; + + for (auto const& guid : corpses) + { + LootObject loot(bot, guid); + if (!loot.IsLootPossible(bot)) + continue; + + WorldObject* object = loot.GetWorldObject(bot); + if (!object) + continue; + + Creature* creature = object->ToCreature(); + if (!creature || creature->GetEntry() != NPC_TAINTED_ELEMENTAL || creature->IsAlive()) + continue; + + context->GetValue("loot target")->Set(loot); + + float dist = bot->GetDistance(object); + if (dist > maxLootRange) + return MoveTo(object, 2.0f, MovementPriority::MOVEMENT_FORCED); + + OpenLootAction open(botAI); + if (!open.Execute(Event())) + return false; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->HasItemCount(ITEM_TAINTED_CORE, 1, false)) + return true; + } + } + + const ObjectGuid botGuid = bot->GetGUID(); + const ObjectGuid corpseGuid = guid; + constexpr uint8 coreIndex = 0; + + botAI->AddTimedEvent([botGuid, corpseGuid, coreIndex, vashj]() + { + Player* receiver = botGuid.IsEmpty() ? nullptr : ObjectAccessor::FindPlayer(botGuid); + if (!receiver) + return; + + if (Group* group = receiver->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->HasItemCount(ITEM_TAINTED_CORE, 1, false)) + return; + } + } + + receiver->SetLootGUID(corpseGuid); + + WorldPacket* packet = new WorldPacket(CMSG_AUTOSTORE_LOOT_ITEM, 1); + *packet << coreIndex; + receiver->GetSession()->QueuePacket(packet); + + const uint32 instanceId = vashj->GetMap()->GetInstanceId(); + const time_t now = std::time(nullptr); + lastCoreInInventoryTime.insert_or_assign(instanceId, now); + }, 600); + + return true; + } + + return false; +} + +bool LadyVashjPassTheTaintedCoreAction::Execute(Event /*event*/) +{ + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); + if (!designatedLooter) + return false; + + Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); + Player* secondCorePasser = GetSecondTaintedCorePasser(group, botAI); + Player* thirdCorePasser = GetThirdTaintedCorePasser(group, botAI); + Player* fourthCorePasser = GetFourthTaintedCorePasser(group, botAI); + const uint32 instanceId = vashj->GetMap()->GetInstanceId(); + + Unit* closestTrigger = nullptr; + if (Unit* tainted = AI_VALUE2(Unit*, "find target", "tainted elemental")) + { + closestTrigger = GetNearestActiveShieldGeneratorTriggerByEntry(tainted); + if (closestTrigger) + nearestTriggerGuid.insert_or_assign(instanceId, closestTrigger->GetGUID()); + } + + auto itSnap = nearestTriggerGuid.find(instanceId); + if (itSnap != nearestTriggerGuid.end() && !itSnap->second.IsEmpty()) + { + Unit* snapUnit = botAI->GetUnit(itSnap->second); + if (snapUnit) + closestTrigger = snapUnit; + else + nearestTriggerGuid.erase(instanceId); + } + + if (!firstCorePasser || !secondCorePasser || !thirdCorePasser || + !fourthCorePasser || !closestTrigger) + return false; + + // Not gated behind CheatMask because the auto application of Fear Ward is necessary + // to address an issue with bot movement, which is that bots cannot be rooted and + // therefore will move when feared while holding the Tainted Core + if (!bot->HasAura(SPELL_FEAR_WARD)) + bot->AddAura(SPELL_FEAR_WARD, bot); + + Item* item = bot->GetItemByEntry(ITEM_TAINTED_CORE); + if (!item || !botAI->HasItemInInventory(ITEM_TAINTED_CORE)) + { + // Passer order: HealAssistantOfIndex 0, 1, 2, then RangedDpsAssistantOfIndex 0 + if (bot == firstCorePasser) + { + if (LineUpFirstCorePasser(designatedLooter, closestTrigger)) + return true; + } + else if (bot == secondCorePasser) + { + if (LineUpSecondCorePasser(firstCorePasser, closestTrigger)) + return true; + } + else if (bot == thirdCorePasser) + { + if (LineUpThirdCorePasser(designatedLooter, firstCorePasser, + secondCorePasser, closestTrigger)) + return true; + } + else if (bot == fourthCorePasser) + { + if (LineUpFourthCorePasser(firstCorePasser, secondCorePasser, + thirdCorePasser, closestTrigger)) + return true; + } + } + else if (item && botAI->HasItemInInventory(ITEM_TAINTED_CORE)) + { + // Designated core looter logic + // Applicable only if cheat mode is on and thus looter is a bot + if (bot == designatedLooter) + { + if (IsFirstCorePasserInIntendedPosition( + designatedLooter, firstCorePasser, closestTrigger)) + { + const time_t now = std::time(nullptr); + auto it = lastImbueAttempt.find(instanceId); + if (it == lastImbueAttempt.end() || (now - it->second) >= 2) + { + lastImbueAttempt.insert_or_assign(instanceId, now); + lastCoreInInventoryTime.insert_or_assign(instanceId, now); + botAI->ImbueItem(item, firstCorePasser); + intendedLineup.erase(bot->GetGUID()); + ScheduleTransferCoreAfterImbue(botAI, bot, firstCorePasser); + return true; + } + } + } + // First core passer: receive core from looter at the top of the stairs, + // pass to second core passer + else if (bot == firstCorePasser) + { + const time_t now = std::time(nullptr); + auto it = lastImbueAttempt.find(instanceId); + if (it == lastImbueAttempt.end() || (now - it->second) >= 2) + { + lastImbueAttempt.insert_or_assign(instanceId, now); + lastCoreInInventoryTime.insert_or_assign(instanceId, now); + botAI->ImbueItem(item, secondCorePasser); + intendedLineup.erase(bot->GetGUID()); + ScheduleTransferCoreAfterImbue(botAI, bot, secondCorePasser); + return true; + } + } + // Second core passer: if closest usable generator is within passing distance + // of the first passer, move to the generator; otherwise, move as close as + // possible to the generator while staying in passing range + else if (bot == secondCorePasser) + { + if (!UseCoreOnNearestGenerator(instanceId)) + { + if (IsThirdCorePasserInIntendedPosition( + secondCorePasser, thirdCorePasser, closestTrigger)) + { + const time_t now = std::time(nullptr); + auto it = lastImbueAttempt.find(instanceId); + if (it == lastImbueAttempt.end() || (now - it->second) >= 2) + { + lastImbueAttempt.insert_or_assign(instanceId, now); + lastCoreInInventoryTime.insert_or_assign(instanceId, now); + botAI->ImbueItem(item, thirdCorePasser); + intendedLineup.erase(bot->GetGUID()); + ScheduleTransferCoreAfterImbue(botAI, bot, thirdCorePasser); + return true; + } + } + } + } + // Third core passer: if closest usable generator is within passing distance + // of the second passer, move to the generator; otherwise, move as close as + // possible to the generator while staying in passing range + else if (bot == thirdCorePasser) + { + if (!UseCoreOnNearestGenerator(instanceId)) + { + if (IsFourthCorePasserInIntendedPosition( + thirdCorePasser, fourthCorePasser, closestTrigger)) + { + const time_t now = std::time(nullptr); + auto it = lastImbueAttempt.find(instanceId); + if (it == lastImbueAttempt.end() || (now - it->second) >= 2) + { + lastImbueAttempt.insert_or_assign(instanceId, now); + lastCoreInInventoryTime.insert_or_assign(instanceId, now); + botAI->ImbueItem(item, fourthCorePasser); + intendedLineup.erase(bot->GetGUID()); + ScheduleTransferCoreAfterImbue(botAI, bot, fourthCorePasser); + return true; + } + } + } + } + // Fourth core passer: the fourth passer is rarely needed and no more than + // four ever should be, so it should use the Core on the nearest generator + else if (bot == fourthCorePasser) + UseCoreOnNearestGenerator(instanceId); + } + + return false; +} + +bool LadyVashjPassTheTaintedCoreAction::LineUpFirstCorePasser( + Player* designatedLooter, Unit* closestTrigger) +{ + const float centerX = VASHJ_PLATFORM_CENTER_POSITION.GetPositionX(); + const float centerY = VASHJ_PLATFORM_CENTER_POSITION.GetPositionY(); + constexpr float radius = 57.5f; + + float mx = designatedLooter->GetPositionX(); + float my = designatedLooter->GetPositionY(); + float angle = atan2(my - centerY, mx - centerX); + + float targetX = centerX + radius * std::cos(angle); + float targetY = centerY + radius * std::sin(angle); + constexpr float targetZ = 41.097f; + + intendedLineup.insert_or_assign(bot->GetGUID(), Position(targetX, targetY, targetZ)); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(SSC_MAP_ID, targetX, targetY, targetZ, false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); +} + +bool LadyVashjPassTheTaintedCoreAction::LineUpSecondCorePasser( + Player* firstCorePasser, Unit* closestTrigger) +{ + float fx = firstCorePasser->GetPositionX(); + float fy = firstCorePasser->GetPositionY(); + + float dx = closestTrigger->GetPositionX() - fx; + float dy = closestTrigger->GetPositionY() - fy; + float distToTrigger = firstCorePasser->GetExactDist2d(closestTrigger); + + if (distToTrigger == 0.0f) + return false; + + dx /= distToTrigger; dy /= distToTrigger; + + // Target is on a line between firstCorePasser and closestTrigger + float targetX, targetY, targetZ; + // If firstCorePasser is within thresholdDist of closestTrigger, + // go to nearTriggerDist short of closestTrigger + constexpr float thresholdDist = 40.0f; + constexpr float nearTriggerDist = 1.5f; + // If firstCorePasser is not thresholdDist yards from closestTrigger, + // go to farDistance from firstCorePasser + constexpr float farDistance = 38.0f; + + if (distToTrigger <= thresholdDist) + { + float moveDist = std::max(distToTrigger - nearTriggerDist, 0.0f); + targetX = fx + dx * moveDist; + targetY = fy + dy * moveDist; + } + else + { + targetX = fx + dx * farDistance; + targetY = fy + dy * farDistance; + } + + intendedLineup.insert_or_assign(bot->GetGUID(), Position(targetX, targetY, VASHJ_PLATFORM_Z)); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(SSC_MAP_ID, targetX, targetY, VASHJ_PLATFORM_Z, false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); +} + +bool LadyVashjPassTheTaintedCoreAction::LineUpThirdCorePasser( + Player* designatedLooter, Player* firstCorePasser, Player* secondCorePasser, Unit* closestTrigger) +{ + // Wait to move until it is clear that a third passer is needed + bool needThird = + (IsFirstCorePasserInIntendedPosition(designatedLooter, firstCorePasser, closestTrigger) && + firstCorePasser->GetExactDist2d(closestTrigger) > 42.0f) || + (IsSecondCorePasserInIntendedPosition(firstCorePasser, secondCorePasser, closestTrigger) && + secondCorePasser->GetExactDist2d(closestTrigger) > 4.0f); + + if (!needThird) + return false; + + float sx = secondCorePasser->GetPositionX(); + float sy = secondCorePasser->GetPositionY(); + + float dx = closestTrigger->GetPositionX() - sx; + float dy = closestTrigger->GetPositionY() - sy; + float distToTrigger = secondCorePasser->GetExactDist2d(closestTrigger); + + if (distToTrigger == 0.0f) + return false; + + dx /= distToTrigger; dy /= distToTrigger; + + float targetX, targetY, targetZ; + constexpr float thresholdDist = 40.0f; + constexpr float nearTriggerDist = 1.5f; + constexpr float farDistance = 38.0f; + + if (distToTrigger <= thresholdDist) + { + float moveDist = std::max(distToTrigger - nearTriggerDist, 0.0f); + targetX = sx + dx * moveDist; + targetY = sy + dy * moveDist; + } + else + { + targetX = sx + dx * farDistance; + targetY = sy + dy * farDistance; + } + + intendedLineup.insert_or_assign(bot->GetGUID(), Position(targetX, targetY, VASHJ_PLATFORM_Z)); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(SSC_MAP_ID, targetX, targetY, VASHJ_PLATFORM_Z, false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + + return false; +} + +bool LadyVashjPassTheTaintedCoreAction::LineUpFourthCorePasser( + Player* firstCorePasser, Player* secondCorePasser, Player* thirdCorePasser, Unit* closestTrigger) +{ + // Wait to move until it is clear that a fourth passer is needed + bool needFourth = + (IsSecondCorePasserInIntendedPosition(firstCorePasser, secondCorePasser, closestTrigger) && + secondCorePasser->GetExactDist2d(closestTrigger) > 42.0f) || + (IsThirdCorePasserInIntendedPosition(secondCorePasser, thirdCorePasser, closestTrigger) && + thirdCorePasser->GetExactDist2d(closestTrigger) > 4.0f); + + if (!needFourth) + return false; + + float sx = thirdCorePasser->GetPositionX(); + float sy = thirdCorePasser->GetPositionY(); + + float tx = closestTrigger->GetPositionX(); + float ty = closestTrigger->GetPositionY(); + + float dx = tx - sx; + float dy = ty - sy; + float distToTrigger = thirdCorePasser->GetExactDist2d(closestTrigger); + + if (distToTrigger == 0.0f) + return false; + + dx /= distToTrigger; dy /= distToTrigger; + + constexpr float nearTriggerDist = 1.5f; + float targetX = tx - dx * nearTriggerDist; + float targetY = ty - dy * nearTriggerDist; + + intendedLineup.insert_or_assign(bot->GetGUID(), Position(targetX, targetY, VASHJ_PLATFORM_Z)); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(SSC_MAP_ID, targetX, targetY, VASHJ_PLATFORM_Z, false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); +} + +// The next four functions check if the respective passer is <= 2 yards of their intended +// position and are used to determine when the prior bot in the chain can pass the core +bool LadyVashjPassTheTaintedCoreAction::IsFirstCorePasserInIntendedPosition( + Player* designatedLooter, Player* firstCorePasser, Unit* closestTrigger) +{ + auto itSnap = intendedLineup.find(firstCorePasser->GetGUID()); + if (itSnap != intendedLineup.end()) + { + float dist2d = firstCorePasser->GetExactDist2d(itSnap->second.GetPositionX(), + itSnap->second.GetPositionY()); + return dist2d <= 2.0f; + } + + return false; +} + +bool LadyVashjPassTheTaintedCoreAction::IsSecondCorePasserInIntendedPosition( + Player* firstCorePasser, Player* secondCorePasser, Unit* closestTrigger) +{ + auto itSnap = intendedLineup.find(secondCorePasser->GetGUID()); + if (itSnap != intendedLineup.end()) + { + float dist2d = secondCorePasser->GetExactDist2d(itSnap->second.GetPositionX(), + itSnap->second.GetPositionY()); + return dist2d <= 2.0f; + } + + return false; +} + +bool LadyVashjPassTheTaintedCoreAction::IsThirdCorePasserInIntendedPosition( + Player* secondCorePasser, Player* thirdCorePasser, Unit* closestTrigger) +{ + auto itSnap = intendedLineup.find(thirdCorePasser->GetGUID()); + if (itSnap != intendedLineup.end()) + { + float dist2d = thirdCorePasser->GetExactDist2d(itSnap->second.GetPositionX(), + itSnap->second.GetPositionY()); + return dist2d <= 2.0f; + } + + return false; +} + +bool LadyVashjPassTheTaintedCoreAction::IsFourthCorePasserInIntendedPosition( + Player* thirdCorePasser, Player* fourthCorePasser, Unit* closestTrigger) +{ + auto itSnap = intendedLineup.find(fourthCorePasser->GetGUID()); + if (itSnap != intendedLineup.end()) + { + float dist2d = fourthCorePasser->GetExactDist2d(itSnap->second.GetPositionX(), + itSnap->second.GetPositionY()); + return dist2d <= 2.0f; + } + + return false; +} + +// ImbueItem() is inconsistent in causing the receiver bot to receive the core and the giver +// bot to remove the core, so ScheduleTransferCoreAfterImbue() creates the core on the receiver +// and removes it from the giver, with ImbueItem() called primarily for the throwing animation +void LadyVashjPassTheTaintedCoreAction::ScheduleTransferCoreAfterImbue( + PlayerbotAI* botAI, Player* giver, Player* receiver) +{ + if (!receiver || !giver) + return; + + constexpr uint32 delayMs = 1500; + const ObjectGuid receiverGuid = receiver->GetGUID(); + const ObjectGuid giverGuid = giver->GetGUID(); + + botAI->AddTimedEvent([receiverGuid, giverGuid]() + { + Player* receiverPlayer = + receiverGuid.IsEmpty() ? nullptr : ObjectAccessor::FindPlayer(receiverGuid); + Player* giverPlayer = + giverGuid.IsEmpty() ? nullptr : ObjectAccessor::FindPlayer(giverGuid); + + if (!receiverPlayer) + return; + + if (!receiverPlayer->HasItemCount(ITEM_TAINTED_CORE, 1, false)) + { + ItemPosCountVec dest; + uint32 count = 1; + int canStore = + receiverPlayer->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, ITEM_TAINTED_CORE, count); + + if (canStore == EQUIP_ERR_OK) + { + receiverPlayer->StoreNewItem(dest, ITEM_TAINTED_CORE, true, + Item::GenerateItemRandomPropertyId(ITEM_TAINTED_CORE)); + } + } + + if (giverPlayer) + { + Item* item = giverPlayer->GetItemByEntry(ITEM_TAINTED_CORE); + if (item && giverPlayer->HasItemCount(ITEM_TAINTED_CORE, 1, false)) + giverPlayer->DestroyItem(item->GetBagSlot(), item->GetSlot(), true); + } + }, delayMs); +} + +bool LadyVashjPassTheTaintedCoreAction::UseCoreOnNearestGenerator(const uint32 instanceId) +{ + auto const& generators = + GetAllGeneratorInfosByDbGuids(bot->GetMap(), SHIELD_GENERATOR_DB_GUIDS); + const GeneratorInfo* nearestGen = GetNearestGeneratorToBot(bot, generators); + if (!nearestGen) + return false; + + GameObject* generator = botAI->GetGameObject(nearestGen->guid); + if (!generator) + return false; + + if (bot->GetExactDist2d(generator) > 4.5f) + return false; + + Item* core = bot->GetItemByEntry(ITEM_TAINTED_CORE); + if (!core) + return false; + + if (bot->CanUseItem(core) != EQUIP_ERR_OK) + return false; + + if (bot->IsNonMeleeSpellCast(false)) + return false; + + const uint8 bagIndex = core->GetBagSlot(); + const uint8 slot = core->GetSlot(); + constexpr uint8 cast_count = 0; + uint32 spellId = 0; + + for (uint8 i = 0; i < MAX_ITEM_PROTO_SPELLS; ++i) + { + if (core->GetTemplate()->Spells[i].SpellId > 0) + { + spellId = core->GetTemplate()->Spells[i].SpellId; + break; + } + } + + const ObjectGuid item_guid = core->GetGUID(); + constexpr uint32 glyphIndex = 0; + constexpr uint8 castFlags = 0; + + WorldPacket packet(CMSG_USE_ITEM); + packet << bagIndex; + packet << slot; + packet << cast_count; + packet << spellId; + packet << item_guid; + packet << glyphIndex; + packet << castFlags; + packet << (uint32)TARGET_FLAG_GAMEOBJECT; + packet << generator->GetGUID().WriteAsPacked(); + + bot->GetSession()->HandleUseItemOpcode(packet); + nearestTriggerGuid.erase(instanceId); + lastImbueAttempt.erase(instanceId); + lastCoreInInventoryTime.erase(instanceId); + return true; +} + +// Fallback for residual cores to be destroyed in Phase 3 in case +// ScheduleTransferCoreAfterImbue() fails to remove the core from the giver +bool LadyVashjDestroyTaintedCoreAction::Execute(Event /*event*/) +{ + if (Item* core = bot->GetItemByEntry(ITEM_TAINTED_CORE)) + { + bot->DestroyItem(core->GetBagSlot(), core->GetSlot(), true); + return true; + } + + return false; +} + +// This needs to be separate from the general map erasing logic because +// Bots may end up out of combat during the Vashj encounter +bool LadyVashjEraseCorePassingTrackersAction::Execute(Event /*event*/) +{ + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + return false; + + const uint32 instanceId = vashj->GetMap()->GetInstanceId(); + + bool erased = false; + if (nearestTriggerGuid.erase(instanceId) > 0) + erased = true; + if (lastImbueAttempt.erase(instanceId) > 0) + erased = true; + if (lastCoreInInventoryTime.erase(instanceId) > 0) + erased = true; + if (intendedLineup.erase(bot->GetGUID()) > 0) + erased = true; + + return erased; +} + +// The standard "avoid aoe" strategy does work for Toxic Spores, but this method +// provides more buffer distance and limits the area in which bots can move +// so that they do not go down the stairs +bool LadyVashjAvoidToxicSporesAction::Execute(Event /*event*/) +{ + auto const& spores = GetAllSporeDropTriggers(botAI, bot); + if (spores.empty()) + return false; + + constexpr float hazardRadius = 7.0f; + bool inDanger = false; + for (Unit* spore : spores) + { + if (bot->GetExactDist2d(spore) < hazardRadius) + { + inDanger = true; + break; + } + } + + if (!inDanger) + return false; + + const Position& vashjCenter = VASHJ_PLATFORM_CENTER_POSITION; + constexpr float maxRadius = 60.0f; + + Position safestPos = FindSafestNearbyPosition(spores, vashjCenter, maxRadius, hazardRadius); + + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + bool backwards = (vashj && vashj->GetVictim() == bot); + return MoveTo(SSC_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), + safestPos.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, backwards); +} + +Position LadyVashjAvoidToxicSporesAction::FindSafestNearbyPosition( + const std::vector& spores, const Position& vashjCenter, + float maxRadius, float hazardRadius) +{ + constexpr float searchStep = M_PI / 8.0f; + constexpr float minDistance = 2.0f; + constexpr float maxDistance = 40.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) + { + float x = bot->GetPositionX() + distance * std::cos(angle); + float y = bot->GetPositionY() + distance * std::sin(angle); + float z = bot->GetPositionZ(); + + if (vashjCenter.GetExactDist2d(x, y) > maxRadius) + continue; + + bool isSafe = true; + for (Unit* spore : spores) + { + if (spore->GetExactDist2d(x, y) < hazardRadius) + { + isSafe = false; + break; + } + } + + if (!isSafe) + continue; + + Position testPos(x, y, z); + + bool pathSafe = + IsPathSafeFromSpores(bot->GetPosition(), testPos, spores, 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 LadyVashjAvoidToxicSporesAction::IsPathSafeFromSpores(const Position& start, + const Position& end, const std::vector& spores, 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* spore : spores) + { + float distToSpore = spore->GetExactDist2d(checkX, checkY); + if (distToSpore < hazardRadius) + return false; + } + } + + return true; +} + +// When Toxic Sporebats spit poison, they summon "Spore Drop Trigger" NPCs +// that create the toxic pools +std::vector LadyVashjAvoidToxicSporesAction::GetAllSporeDropTriggers( + PlayerbotAI* botAI, Player* bot) +{ + std::vector sporeDropTriggers; + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); + for (auto const& npcGuid : npcs) + { + constexpr float maxSearchRadius = 40.0f; + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->GetEntry() == NPC_SPORE_DROP_TRIGGER && + bot->GetExactDist2d(unit) < maxSearchRadius) + sporeDropTriggers.push_back(unit); + } + + return sporeDropTriggers; +} + +bool LadyVashjUseFreeActionAbilitiesAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + auto const& spores = + LadyVashjAvoidToxicSporesAction::GetAllSporeDropTriggers(botAI, bot); + constexpr float toxicSporeRadius = 6.0f; + + // If Rogues are Entangled and either have Static Charge or + // are near a spore, use Cloak of Shadows + if (bot->getClass() == CLASS_ROGUE && bot->HasAura(SPELL_ENTANGLE)) + { + bool nearSpore = false; + for (Unit* spore : spores) + { + if (bot->GetExactDist2d(spore) < toxicSporeRadius) + { + nearSpore = true; + break; + } + } + if (bot->HasAura(SPELL_STATIC_CHARGE) || nearSpore) + { + if (botAI->CanCastSpell("cloak of shadows", bot)) + return botAI->CastSpell("cloak of shadows", bot); + } + } + + // The remainder of the logic is for Paladins to use Hand of Freedom + Player* mainTankToxic = nullptr; + Player* anyToxic = nullptr; + Player* mainTankStatic = nullptr; + Player* anyStatic = nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !member->HasAura(SPELL_ENTANGLE) || + !botAI->IsMelee(member)) + continue; + + bool nearToxicSpore = false; + for (Unit* spore : spores) + { + if (member->GetExactDist2d(spore) < toxicSporeRadius) + { + nearToxicSpore = true; + break; + } + } + + if (nearToxicSpore) + { + if (botAI->IsMainTank(member)) + mainTankToxic = member; + + if (!anyToxic) + anyToxic = member; + } + + if (member->HasAura(SPELL_STATIC_CHARGE)) + { + if (botAI->IsMainTank(member)) + mainTankStatic = member; + + if (!anyStatic) + anyStatic = member; + } + } + + if (bot->getClass() == CLASS_PALADIN) + { + // Priority 1: Entangled in Toxic Spores (prefer main tank) + Player* toxicTarget = mainTankToxic ? mainTankToxic : anyToxic; + if (toxicTarget) + { + if (botAI->CanCastSpell("hand of freedom", toxicTarget)) + return botAI->CastSpell("hand of freedom", toxicTarget); + } + + // Priority 2: Entangled with Static Charge (prefer main tank) + Player* staticTarget = mainTankStatic ? mainTankStatic : anyStatic; + if (staticTarget) + { + if (botAI->CanCastSpell("hand of freedom", staticTarget)) + return botAI->CastSpell("hand of freedom", staticTarget); + } + } + + return false; +} diff --git a/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h b/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h new file mode 100644 index 000000000..cbd237402 --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h @@ -0,0 +1,457 @@ +#ifndef _PLAYERBOT_RAIDSSCACTIONS_H +#define _PLAYERBOT_RAIDSSCACTIONS_H + +#include "Action.h" +#include "AttackAction.h" +#include "MovementActions.h" + +// General + +class SerpentShrineCavernEraseTimersAndTrackersAction : public Action +{ +public: + SerpentShrineCavernEraseTimersAndTrackersAction( + PlayerbotAI* botAI, std::string const name = "serpent shrine cavern erase timers and trackers") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Trash + +class UnderbogColossusEscapeToxicPoolAction : public MovementAction +{ +public: + UnderbogColossusEscapeToxicPoolAction( + PlayerbotAI* botAI, std::string const name = "underbog colossus escape toxic pool") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class GreyheartTidecallerMarkWaterElementalTotemAction : public Action +{ +public: + GreyheartTidecallerMarkWaterElementalTotemAction( + PlayerbotAI* botAI, std::string const name = "greyheart tidecaller mark water elemental totem") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Hydross the Unstable + +class HydrossTheUnstablePositionFrostTankAction : public AttackAction +{ +public: + HydrossTheUnstablePositionFrostTankAction( + PlayerbotAI* botAI, std::string const name = "hydross the unstable position frost tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HydrossTheUnstablePositionNatureTankAction : public AttackAction +{ +public: + HydrossTheUnstablePositionNatureTankAction( + PlayerbotAI* botAI, std::string const name = "hydross the unstable position nature tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HydrossTheUnstablePrioritizeElementalAddsAction : public AttackAction +{ +public: + HydrossTheUnstablePrioritizeElementalAddsAction( + PlayerbotAI* botAI, std::string const name = "hydross the unstable prioritize elemental adds") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HydrossTheUnstableFrostPhaseSpreadOutAction : public MovementAction +{ +public: + HydrossTheUnstableFrostPhaseSpreadOutAction( + PlayerbotAI* botAI, std::string const name = "hydross the unstable frost phase spread out") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HydrossTheUnstableMisdirectBossToTankAction : public Action +{ +public: + HydrossTheUnstableMisdirectBossToTankAction( + PlayerbotAI* botAI, std::string const name = "hydross the unstable misdirect boss to tank") : Action(botAI, name) {} + bool Execute(Event event) override; + +private: + bool TryMisdirectToFrostTank(Unit* hydross, Group* group); + bool TryMisdirectToNatureTank(Unit* hydross, Group* group); +}; + +class HydrossTheUnstableStopDpsUponPhaseChangeAction : public Action +{ +public: + HydrossTheUnstableStopDpsUponPhaseChangeAction( + PlayerbotAI* botAI, std::string const name = "hydross the unstable stop dps upon phase change") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class HydrossTheUnstableManageTimersAction : public Action +{ +public: + HydrossTheUnstableManageTimersAction( + PlayerbotAI* botAI, std::string const name = "hydross the unstable manage timers") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// The Lurker Below + +class TheLurkerBelowRunAroundBehindBossAction : public MovementAction +{ +public: + TheLurkerBelowRunAroundBehindBossAction( + PlayerbotAI* botAI, std::string const name = "the lurker below run around behind boss") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class TheLurkerBelowPositionMainTankAction : public AttackAction +{ +public: + TheLurkerBelowPositionMainTankAction( + PlayerbotAI* botAI, std::string const name = "the lurker below position main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class TheLurkerBelowSpreadRangedInArcAction : public MovementAction +{ +public: + TheLurkerBelowSpreadRangedInArcAction( + PlayerbotAI* botAI, std::string const name = "the lurker below spread ranged in arc") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class TheLurkerBelowTanksPickUpAddsAction : public AttackAction +{ +public: + TheLurkerBelowTanksPickUpAddsAction( + PlayerbotAI* botAI, std::string const name = "the lurker below tanks pick up adds") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class TheLurkerBelowManageSpoutTimerAction : public Action +{ +public: + TheLurkerBelowManageSpoutTimerAction( + PlayerbotAI* botAI, std::string const name = "the lurker below manage spout timer") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Leotheras the Blind + +class LeotherasTheBlindTargetSpellbindersAction : public Action +{ +public: + LeotherasTheBlindTargetSpellbindersAction( + PlayerbotAI* botAI, std::string const name = "leotheras the blind target spellbinders") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class LeotherasTheBlindPositionRangedAction : public MovementAction +{ +public: + LeotherasTheBlindPositionRangedAction( + PlayerbotAI* botAI, std::string const name = "leotheras the blind position ranged") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LeotherasTheBlindDemonFormTankAttackBossAction : public AttackAction +{ +public: + LeotherasTheBlindDemonFormTankAttackBossAction( + PlayerbotAI* botAI, std::string const name = "leotheras the blind demon form tank attack boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LeotherasTheBlindMeleeTanksDontAttackDemonFormAction : public Action +{ +public: + LeotherasTheBlindMeleeTanksDontAttackDemonFormAction( + PlayerbotAI* botAI, std::string const name = "leotheras the blind melee tanks don't attack demon form") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class LeotherasTheBlindRunAwayFromWhirlwindAction : public MovementAction +{ +public: + LeotherasTheBlindRunAwayFromWhirlwindAction( + PlayerbotAI* botAI, std::string const name = "leotheras the blind run away from whirlwind") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LeotherasTheBlindMeleeDpsRunAwayFromBossAction : public MovementAction +{ +public: + LeotherasTheBlindMeleeDpsRunAwayFromBossAction( + PlayerbotAI* botAI, std::string const name = "leotheras the blind melee dps run away from boss") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LeotherasTheBlindDestroyInnerDemonAction : public AttackAction +{ +public: + LeotherasTheBlindDestroyInnerDemonAction( + PlayerbotAI* botAI, std::string const name = "leotheras the blind destroy inner demon") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool HandleFeralTankStrategy(Unit* innerDemon); + bool HandleHealerStrategy(Unit* innerDemon); +}; + +class LeotherasTheBlindFinalPhaseAssignDpsPriorityAction : public AttackAction +{ +public: + LeotherasTheBlindFinalPhaseAssignDpsPriorityAction( + PlayerbotAI* botAI, std::string const name = "leotheras the blind final phase assign dps priority") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LeotherasTheBlindMisdirectBossToDemonFormTankAction : public AttackAction +{ +public: + LeotherasTheBlindMisdirectBossToDemonFormTankAction( + PlayerbotAI* botAI, std::string const name = "leotheras the blind misdirect boss to demon form tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LeotherasTheBlindManageDpsWaitTimersAction : public Action +{ +public: + LeotherasTheBlindManageDpsWaitTimersAction( + PlayerbotAI* botAI, std::string const name = "leotheras the blind manage dps wait timers") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Fathom-Lord Karathress + +class FathomLordKarathressMainTankPositionBossAction : public AttackAction +{ +public: + FathomLordKarathressMainTankPositionBossAction( + PlayerbotAI* botAI, std::string const name = "fathom-lord karathress main tank position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class FathomLordKarathressFirstAssistTankPositionCaribdisAction : public AttackAction +{ +public: + FathomLordKarathressFirstAssistTankPositionCaribdisAction( + PlayerbotAI* botAI, std::string const name = "fathom-lord karathress first assist tank position caribdis") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class FathomLordKarathressSecondAssistTankPositionSharkkisAction : public AttackAction +{ +public: + FathomLordKarathressSecondAssistTankPositionSharkkisAction( + PlayerbotAI* botAI, std::string const name = "fathom-lord karathress second assist tank position sharkkis") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class FathomLordKarathressThirdAssistTankPositionTidalvessAction : public AttackAction +{ +public: + FathomLordKarathressThirdAssistTankPositionTidalvessAction( + PlayerbotAI* botAI, std::string const name = "fathom-lord karathress third assist tank position tidalvess") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class FathomLordKarathressPositionCaribdisTankHealerAction : public MovementAction +{ +public: + FathomLordKarathressPositionCaribdisTankHealerAction( + PlayerbotAI* botAI, std::string const name = "fathom-lord karathress position caribdis tank healer") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class FathomLordKarathressMisdirectBossesToTanksAction : public AttackAction +{ +public: + FathomLordKarathressMisdirectBossesToTanksAction( + PlayerbotAI* botAI, std::string const name = "fathom-lord karathress misdirect bosses to tanks") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class FathomLordKarathressAssignDpsPriorityAction : public AttackAction +{ +public: + FathomLordKarathressAssignDpsPriorityAction( + PlayerbotAI* botAI, std::string const name = "fathom-lord karathress assign dps priority") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class FathomLordKarathressManageDpsTimerAction : public Action +{ +public: + FathomLordKarathressManageDpsTimerAction( + PlayerbotAI* botAI, std::string const name = "fathom-lord karathress manage dps timer") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Morogrim Tidewalker + +class MorogrimTidewalkerMisdirectBossToMainTankAction : public AttackAction +{ +public: + MorogrimTidewalkerMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "morogrim tidewalker misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class MorogrimTidewalkerMoveBossToTankPositionAction : public AttackAction +{ +public: + MorogrimTidewalkerMoveBossToTankPositionAction( + PlayerbotAI* botAI, std::string const name = "morogrim tidewalker move boss to tank position") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool MoveToPhase1TankPosition(Unit* tidewalker); + bool MoveToPhase2TankPosition(Unit* tidewalker); +}; + +class MorogrimTidewalkerPhase2RepositionRangedAction : public MovementAction +{ +public: + MorogrimTidewalkerPhase2RepositionRangedAction( + PlayerbotAI* botAI, std::string const name = "morogrim tidewalker phase 2 reposition ranged") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +// Lady Vashj + +class LadyVashjMainTankPositionBossAction : public AttackAction +{ +public: + LadyVashjMainTankPositionBossAction( + PlayerbotAI* botAI, std::string const name = "lady vashj main tank position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjPhase1SpreadRangedInArcAction : public MovementAction +{ +public: + LadyVashjPhase1SpreadRangedInArcAction( + PlayerbotAI* botAI, std::string const name = "lady vashj phase 1 spread ranged in arc") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjSetGroundingTotemInMainTankGroupAction : public MovementAction +{ +public: + LadyVashjSetGroundingTotemInMainTankGroupAction( + PlayerbotAI* botAI, std::string const name = "lady vashj set grounding totem in main tank group") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjStaticChargeMoveAwayFromGroupAction : public MovementAction +{ +public: + LadyVashjStaticChargeMoveAwayFromGroupAction( + PlayerbotAI* botAI, std::string const name = "lady vashj static charge move away from group") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjMisdirectBossToMainTankAction : public AttackAction +{ +public: + LadyVashjMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "lady vashj misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjAssignPhase2AndPhase3DpsPriorityAction : public AttackAction +{ +public: + LadyVashjAssignPhase2AndPhase3DpsPriorityAction( + PlayerbotAI* botAI, std::string const name = "lady vashj assign phase 2 and phase 3 dps priority") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjMisdirectStriderToFirstAssistTankAction : public AttackAction +{ +public: + LadyVashjMisdirectStriderToFirstAssistTankAction( + PlayerbotAI* botAI, std::string const name = "lady vashj misdirect strider to first assist tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjTankAttackAndMoveAwayStriderAction : public AttackAction +{ +public: + LadyVashjTankAttackAndMoveAwayStriderAction( + PlayerbotAI* botAI, std::string const name = "lady vashj tank attack and move away strider") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjTeleportToTaintedElementalAction : public AttackAction +{ +public: + LadyVashjTeleportToTaintedElementalAction( + PlayerbotAI* botAI, std::string const name = "lady vashj teleport to tainted elemental") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjLootTaintedCoreAction : public MovementAction +{ +public: + LadyVashjLootTaintedCoreAction( + PlayerbotAI* botAI, std::string const name = "lady vashj loot tainted core") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjPassTheTaintedCoreAction : public MovementAction +{ +public: + LadyVashjPassTheTaintedCoreAction( + PlayerbotAI* botAI, std::string const name = "lady vashj pass the tainted core") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool LineUpFirstCorePasser(Player* designatedLooter, Unit* closestTrigger); + bool LineUpSecondCorePasser(Player* firstCorePasser, Unit* closestTrigger); + bool LineUpThirdCorePasser(Player* designatedLooter, Player* firstCorePasser, Player* secondCorePasser, Unit* closestTrigger); + bool LineUpFourthCorePasser(Player* firstCorePasser, Player* secondCorePasser, Player* thirdCorePasser, Unit* closestTrigger); + bool IsFirstCorePasserInIntendedPosition(Player* designatedLooter, Player* firstCorePasser, Unit* closestTrigger); + bool IsSecondCorePasserInIntendedPosition(Player* firstCorePasser, Player* secondCorePasser, Unit* closestTrigger); + bool IsThirdCorePasserInIntendedPosition(Player* secondCorePasser, Player* thirdCorePasser, Unit* closestTrigger); + bool IsFourthCorePasserInIntendedPosition(Player* thirdCorePasser, Player* fourthCorePasser, Unit* closestTrigger); + void ScheduleTransferCoreAfterImbue(PlayerbotAI* botAI, Player* giver, Player* receiver); + bool UseCoreOnNearestGenerator(const uint32 instanceId); +}; + +class LadyVashjDestroyTaintedCoreAction : public Action +{ +public: + LadyVashjDestroyTaintedCoreAction(PlayerbotAI* botAI, std::string const name = "lady vashj destroy tainted core") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjEraseCorePassingTrackersAction : public Action +{ +public: + LadyVashjEraseCorePassingTrackersAction(PlayerbotAI* botAI, std::string const name = "lady vashj erase core passing trackers") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjAvoidToxicSporesAction : public MovementAction +{ +public: + LadyVashjAvoidToxicSporesAction(PlayerbotAI* botAI, std::string const name = "lady vashj avoid toxic spores") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + static std::vector GetAllSporeDropTriggers(PlayerbotAI* botAI, Player* bot); + +private: + Position FindSafestNearbyPosition(const std::vector& spores, const Position& position, float maxRadius, float hazardRadius); + bool IsPathSafeFromSpores(const Position& start, const Position& end, const std::vector& spores, float hazardRadius); +}; + +class LadyVashjUseFreeActionAbilitiesAction : public Action +{ +public: + LadyVashjUseFreeActionAbilitiesAction(PlayerbotAI* botAI, std::string const name = "lady vashj use free action abilities") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +#endif diff --git a/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.cpp b/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.cpp new file mode 100644 index 000000000..c99cafa3c --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.cpp @@ -0,0 +1,799 @@ +#include "RaidSSCMultipliers.h" +#include "RaidSSCActions.h" +#include "RaidSSCHelpers.h" +#include "ChooseTargetActions.h" +#include "DestroyItemAction.h" +#include "DKActions.h" +#include "DruidActions.h" +#include "DruidBearActions.h" +#include "DruidCatActions.h" +#include "DruidShapeshiftActions.h" +#include "FollowActions.h" +#include "GenericSpellActions.h" +#include "HunterActions.h" +#include "LootAction.h" +#include "MageActions.h" +#include "PaladinActions.h" +#include "Playerbots.h" +#include "ReachTargetActions.h" +#include "RogueActions.h" +#include "ShamanActions.h" +#include "WarlockActions.h" +#include "WarriorActions.h" +#include "WipeAction.h" + +using namespace SerpentShrineCavernHelpers; + +// Trash + +float UnderbogColossusEscapeToxicPoolMultiplier::GetValue(Action* action) +{ + if (bot->HasAura(SPELL_TOXIC_POOL)) + { + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Hydross the Unstable + +float HydrossTheUnstableDisableTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0, true)) + return 1.0f; + + Unit* hydross = AI_VALUE2(Unit*, "find target", "hydross the unstable"); + if (!hydross) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action))) + { + if ((botAI->IsMainTank(bot) && hydross->HasAura(SPELL_CORRUPTION)) || + (botAI->IsAssistTankOfIndex(bot, 0, true) && !hydross->HasAura(SPELL_CORRUPTION))) + return 0.0f; + } + + return 1.0f; +} + +float HydrossTheUnstableWaitForDpsMultiplier::GetValue(Action* action) +{ + Unit* hydross = AI_VALUE2(Unit*, "find target", "hydross the unstable"); + if (!hydross) + return 1.0f; + + Unit* waterElemental = AI_VALUE2(Unit*, "find target", "pure spawn of hydross"); + Unit* natureElemental = AI_VALUE2(Unit*, "find target", "tainted spawn of hydross"); + if (botAI->IsAssistTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0, true) && + (waterElemental || natureElemental)) + return 1.0f; + + if (dynamic_cast(action)) + return 1.0f; + + const uint32 instanceId = hydross->GetMap()->GetInstanceId(); + const time_t now = std::time(nullptr); + constexpr uint8 phaseChangeWaitSeconds = 1; + constexpr uint8 dpsWaitSeconds = 5; + + if (!hydross->HasAura(SPELL_CORRUPTION) && !botAI->IsMainTank(bot)) + { + auto itDps = hydrossFrostDpsWaitTimer.find(instanceId); + auto itPhase = hydrossChangeToFrostPhaseTimer.find(instanceId); + + bool justChanged = (itDps == hydrossFrostDpsWaitTimer.end() || + (now - itDps->second) < dpsWaitSeconds); + bool aboutToChange = (itPhase != hydrossChangeToFrostPhaseTimer.end() && + (now - itPhase->second) > phaseChangeWaitSeconds); + + if (justChanged || aboutToChange) + { + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + } + } + + if (hydross->HasAura(SPELL_CORRUPTION) && !botAI->IsAssistTankOfIndex(bot, 0, true)) + { + auto itDps = hydrossNatureDpsWaitTimer.find(instanceId); + auto itPhase = hydrossChangeToNaturePhaseTimer.find(instanceId); + + bool justChanged = (itDps == hydrossNatureDpsWaitTimer.end() || + (now - itDps->second) < dpsWaitSeconds); + bool aboutToChange = (itPhase != hydrossChangeToNaturePhaseTimer.end() && + (now - itPhase->second) > phaseChangeWaitSeconds); + + if (justChanged || aboutToChange) + { + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + } + } + + return 1.0f; +} + +float HydrossTheUnstableControlMisdirectionMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_HUNTER) + return 1.0f; + + if (AI_VALUE2(Unit*, "find target", "hydross the unstable")) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// The Lurker Below + +float TheLurkerBelowStayAwayFromSpoutMultiplier::GetValue(Action* action) +{ + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker) + return 1.0f; + + const time_t now = std::time(nullptr); + + auto it = lurkerSpoutTimer.find(lurker->GetMap()->GetInstanceId()); + if (it != lurkerSpoutTimer.end() && it->second > now) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float TheLurkerBelowMaintainRangedSpreadMultiplier::GetValue(Action* action) +{ + if (!botAI->IsRanged(bot)) + return 1.0f; + + if (AI_VALUE2(Unit*, "find target", "the lurker below")) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Disable tank assist during Submerge only if there are 3 or more tanks in the raid +float TheLurkerBelowDisableTankAssistMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + if (bot->GetVictim() == nullptr) + return 1.0f; + + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker || lurker->getStandState() != UNIT_STAND_STATE_SUBMERGED) + return 1.0f; + + Group* group = bot->GetGroup(); + if (!group) + return 1.0f; + + uint8 tankCount = 0; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (botAI->IsTank(member)) + ++tankCount; + } + + if (tankCount >= 3) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Leotheras the Blind + +float LeotherasTheBlindAvoidWhirlwindMultiplier::GetValue(Action* action) +{ + if (botAI->IsTank(bot)) + return 1.0f; + + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return 1.0f; + + Unit* leotherasHuman = GetLeotherasHuman(botAI); + if (!leotherasHuman) + return 1.0f; + + if (!leotherasHuman->HasAura(SPELL_LEOTHERAS_BANISHED) && + (leotherasHuman->HasAura(SPELL_WHIRLWIND) || + leotherasHuman->HasAura(SPELL_WHIRLWIND_CHANNEL))) + { + if (dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float LeotherasTheBlindDisableTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot) || bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "leotheras the blind")) + return 1.0f; + + if (GetPhase2LeotherasDemon(botAI) && dynamic_cast(action)) + return 0.0f; + + if (!GetPhase3LeotherasDemon(botAI) && dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float LeotherasTheBlindFocusOnInnerDemonMultiplier::GetValue(Action* action) +{ + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float LeotherasTheBlindMeleeDpsAvoidChaosBlastMultiplier::GetValue(Action* action) +{ + if (botAI->IsRanged(bot) || botAI->IsTank(bot)) + return 1.0f; + + if (!GetPhase2LeotherasDemon(botAI)) + return 1.0f; + + Aura* chaosBlast = bot->GetAura(SPELL_CHAOS_BLAST); + if (chaosBlast && chaosBlast->GetStackAmount() >= 5) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float LeotherasTheBlindWaitForDpsMultiplier::GetValue(Action* action) +{ + Unit* leotheras = AI_VALUE2(Unit*, "find target", "leotheras the blind"); + if (!leotheras) + return 1.0f; + + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return 1.0f; + + if (dynamic_cast(action)) + return 1.0f; + + const uint32 instanceId = leotheras->GetMap()->GetInstanceId(); + const time_t now = std::time(nullptr); + + constexpr uint8 dpsWaitSecondsPhase1 = 5; + Unit* leotherasHuman = GetLeotherasHuman(botAI); + Unit* leotherasPhase3Demon = GetPhase3LeotherasDemon(botAI); + if (leotherasHuman && !leotherasHuman->HasAura(SPELL_LEOTHERAS_BANISHED) && + !leotherasPhase3Demon) + { + if (botAI->IsTank(bot)) + return 1.0f; + + auto it = leotherasHumanFormDpsWaitTimer.find(instanceId); + if (it == leotherasHumanFormDpsWaitTimer.end() || + (now - it->second) < dpsWaitSecondsPhase1) + { + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + } + } + + constexpr uint8 dpsWaitSecondsPhase2 = 12; + Unit* leotherasPhase2Demon = GetPhase2LeotherasDemon(botAI); + Player* demonFormTank = GetLeotherasDemonFormTank(bot); + if (leotherasPhase2Demon) + { + if (demonFormTank && demonFormTank == bot) + return 1.0f; + + if (!demonFormTank && botAI->IsTank(bot)) + return 1.0f; + + auto it = leotherasDemonFormDpsWaitTimer.find(instanceId); + if (it == leotherasDemonFormDpsWaitTimer.end() || + (now - it->second) < dpsWaitSecondsPhase2) + { + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + } + } + + constexpr uint8 dpsWaitSecondsPhase3 = 8; + if (leotherasPhase3Demon) + { + if ((demonFormTank && demonFormTank == bot) || botAI->IsTank(bot)) + return 1.0f; + + auto it = leotherasFinalPhaseDpsWaitTimer.find(instanceId); + if (it == leotherasFinalPhaseDpsWaitTimer.end() || + (now - it->second) < dpsWaitSecondsPhase3) + { + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + } + } + + return 1.0f; +} + +// Don't use Bloodlust/Heroism during the Channeler phase +float LeotherasTheBlindDelayBloodlustAndHeroismMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_SHAMAN) + return 1.0f; + + Unit* leotheras = AI_VALUE2(Unit*, "find target", "leotheras the blind"); + if (leotheras && leotheras->HasAura(SPELL_LEOTHERAS_BANISHED)) + { + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Fathom-Lord Karathress + +float FathomLordKarathressDisableTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) + return 1.0f; + + if (bot->GetVictim() != nullptr && dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float FathomLordKarathressDisableAoeMultiplier::GetValue(Action* action) +{ + if (!botAI->IsDps(bot)) + return 1.0f; + + if (AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) + { + if (auto castSpellAction = dynamic_cast(action)) + { + if (castSpellAction->getThreatType() == Action::ActionThreatType::Aoe) + return 0.0f; + } + } + + return 1.0f; +} + +float FathomLordKarathressControlMisdirectionMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_HUNTER) + return 1.0f; + + if (AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float FathomLordKarathressWaitForDpsMultiplier::GetValue(Action* action) +{ + if (botAI->IsTank(bot)) + return 1.0f; + + Unit* karathress = AI_VALUE2(Unit*, "find target", "fathom-lord karathress"); + if (!karathress) + return 1.0f; + + if (dynamic_cast(action)) + return 1.0f; + + const time_t now = std::time(nullptr); + constexpr uint8 dpsWaitSeconds = 12; + + auto it = karathressDpsWaitTimer.find(karathress->GetMap()->GetInstanceId()); + if (it == karathressDpsWaitTimer.end() || (now - it->second) < dpsWaitSeconds) + { + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + } + + return 1.0f; +} + +float FathomLordKarathressCaribdisTankHealerMaintainPositionMultiplier::GetValue(Action* action) +{ + if (!botAI->IsAssistHealOfIndex(bot, 0, true)) + return 1.0f; + + if (AI_VALUE2(Unit*, "find target", "fathom-guard caribdis")) + { + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Morogrim Tidewalker + +// Use Bloodlust/Heroism after the first Murloc spawn +float MorogrimTidewalkerDelayBloodlustAndHeroismMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_SHAMAN) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "morogrim tidewalker")) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "tidewalker lurker")) + { + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float MorogrimTidewalkerDisableTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsMainTank(bot)) + return 1.0f; + + if (AI_VALUE2(Unit*, "find target", "morogrim tidewalker")) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float MorogrimTidewalkerMaintainPhase2StackingMultiplier::GetValue(Action* action) +{ + if (!botAI->IsRanged(bot)) + return 1.0f; + + Unit* tidewalker = AI_VALUE2(Unit*, "find target", "morogrim tidewalker"); + if (!tidewalker) + return 1.0f; + + if (tidewalker->GetHealthPct() < 25.0f) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Lady Vashj + +// Wait until phase 3 to use Bloodlust/Heroism +// Don't use other major cooldowns in Phase 1, either +float LadyVashjDelayCooldownsMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + return 1.0f; + + if (bot->getClass() == CLASS_SHAMAN) + { + if (IsLadyVashjInPhase3(botAI)) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + if (botAI->IsDps(bot) && IsLadyVashjInPhase1(botAI)) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + 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 LadyVashjMaintainPhase1RangedSpreadMultiplier::GetValue(Action* action) +{ + if (!botAI->IsRanged(bot)) + return 1.0f; + + if (AI_VALUE2(Unit*, "find target", "lady vashj") && + IsLadyVashjInPhase1(botAI)) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float LadyVashjStaticChargeStayAwayFromGroupMultiplier::GetValue(Action* action) +{ + if (botAI->IsMainTank(bot) || !bot->HasAura(SPELL_STATIC_CHARGE)) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Bots should not loot the core with normal looting logic +float LadyVashjDoNotLootTheTaintedCoreMultiplier::GetValue(Action* action) +{ + if (AI_VALUE2(Unit*, "find target", "lady vashj")) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float LadyVashjCorePassersPrioritizePositioningMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "lady vashj") || + !IsLadyVashjInPhase2(botAI)) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 1.0f; + + Group* group = bot->GetGroup(); + if (!group) + return 1.0f; + + Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); + Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); + Player* secondCorePasser = GetSecondTaintedCorePasser(group, botAI); + Player* thirdCorePasser = GetThirdTaintedCorePasser(group, botAI); + Player* fourthCorePasser = GetFourthTaintedCorePasser(group, botAI); + + auto hasCore = [](Player* player) + { + return player && player->HasItemCount(ITEM_TAINTED_CORE, 1, false); + }; + + if (hasCore(bot)) + { + if (!dynamic_cast(action)) + return 0.0f; + } + + if (bot == designatedLooter) + { + if (!hasCore(bot)) + return 1.0f; + } + else if (bot == firstCorePasser) + { + if (hasCore(secondCorePasser) || hasCore(thirdCorePasser) || + hasCore(fourthCorePasser)) + return 1.0f; + } + else if (bot == secondCorePasser) + { + if (hasCore(thirdCorePasser) || hasCore(fourthCorePasser)) + return 1.0f; + } + else if (bot == thirdCorePasser) + { + if (hasCore(fourthCorePasser)) + return 1.0f; + } + else if (bot != fourthCorePasser) + return 1.0f; + + if (AI_VALUE2(Unit*, "find target", "tainted elemental") && + (bot == firstCorePasser || bot == secondCorePasser)) + { + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + } + + if (AnyRecentCoreInInventory(group, botAI)) + { + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// All of phases 2 and 3 require a custom movement and targeting system +// So the standard target selection system must be disabled +float LadyVashjDisableAutomaticTargetingAndMovementModifier::GetValue(Action *action) +{ + if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (IsLadyVashjInPhase2(botAI)) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (!botAI->IsHeal(bot) && dynamic_cast(action)) + return 0.0f; + + Unit* enchanted = AI_VALUE2(Unit*, "find target", "enchanted elemental"); + if (enchanted && bot->GetVictim() == enchanted) + { + if (dynamic_cast(action)) + return 0.0f; + } + } + + if (IsLadyVashjInPhase3(botAI)) + { + if (dynamic_cast(action)) + return 0.0f; + + Unit* enchanted = AI_VALUE2(Unit*, "find target", "enchanted elemental"); + Unit* strider = AI_VALUE2(Unit*, "find target", "coilfang strider"); + Unit* elite = AI_VALUE2(Unit*, "find target", "coilfang elite"); + if (enchanted || strider || elite) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (enchanted && bot->GetVictim() == enchanted) + { + if (dynamic_cast(action)) + return 0.0f; + } + } + else if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} diff --git a/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.h b/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.h new file mode 100644 index 000000000..6630dc206 --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.h @@ -0,0 +1,236 @@ +#ifndef _PLAYERBOT_RAIDSSCMULTIPLIERS_H +#define _PLAYERBOT_RAIDSSCMULTIPLIERS_H + +#include "Multiplier.h" + +// Trash + +class UnderbogColossusEscapeToxicPoolMultiplier : public Multiplier +{ +public: + UnderbogColossusEscapeToxicPoolMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "underbog colossus escape toxic pool") {} + virtual float GetValue(Action* action); +}; + +// Hydross the Unstable + +class HydrossTheUnstableDisableTankActionsMultiplier : public Multiplier +{ +public: + HydrossTheUnstableDisableTankActionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "hydross the unstable disable tank actions") {} + virtual float GetValue(Action* action); +}; + +class HydrossTheUnstableWaitForDpsMultiplier : public Multiplier +{ +public: + HydrossTheUnstableWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "hydross the unstable wait for dps") {} + virtual float GetValue(Action* action); +}; + +class HydrossTheUnstableControlMisdirectionMultiplier : public Multiplier +{ +public: + HydrossTheUnstableControlMisdirectionMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "hydross the unstable control misdirection") {} + virtual float GetValue(Action* action); +}; + +// The Lurker Below + +class TheLurkerBelowStayAwayFromSpoutMultiplier : public Multiplier +{ +public: + TheLurkerBelowStayAwayFromSpoutMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "the lurker below stay away from spout") {} + virtual float GetValue(Action* action); +}; + +class TheLurkerBelowMaintainRangedSpreadMultiplier : public Multiplier +{ +public: + TheLurkerBelowMaintainRangedSpreadMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "the lurker below maintain ranged spread") {} + virtual float GetValue(Action* action); +}; + +class TheLurkerBelowDisableTankAssistMultiplier : public Multiplier +{ +public: + TheLurkerBelowDisableTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "the lurker below disable tank assist") {} + virtual float GetValue(Action* action); +}; + +// Leotheras the Blind + +class LeotherasTheBlindAvoidWhirlwindMultiplier : public Multiplier +{ +public: + LeotherasTheBlindAvoidWhirlwindMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "leotheras the blind avoid whirlwind") {} + virtual float GetValue(Action* action); +}; + +class LeotherasTheBlindDisableTankActionsMultiplier : public Multiplier +{ +public: + LeotherasTheBlindDisableTankActionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "leotheras the blind disable tank actions") {} + virtual float GetValue(Action* action); +}; + +class LeotherasTheBlindMeleeDpsAvoidChaosBlastMultiplier : public Multiplier +{ +public: + LeotherasTheBlindMeleeDpsAvoidChaosBlastMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "leotheras the blind melee dps avoid chaos blast") {} + virtual float GetValue(Action* action); +}; + +class LeotherasTheBlindFocusOnInnerDemonMultiplier : public Multiplier +{ +public: + LeotherasTheBlindFocusOnInnerDemonMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "leotheras the blind focus on inner demon") {} + virtual float GetValue(Action* action); +}; + +class LeotherasTheBlindWaitForDpsMultiplier : public Multiplier +{ +public: + LeotherasTheBlindWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "leotheras the blind wait for dps") {} + virtual float GetValue(Action* action); +}; + +class LeotherasTheBlindDelayBloodlustAndHeroismMultiplier : public Multiplier +{ +public: + LeotherasTheBlindDelayBloodlustAndHeroismMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "leotheras the blind delay bloodlust and heroism") {} + virtual float GetValue(Action* action); +}; + +// Fathom-Lord Karathress + +class FathomLordKarathressDisableTankActionsMultiplier : public Multiplier +{ +public: + FathomLordKarathressDisableTankActionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "fathom-lord karathress disable tank actions") {} + virtual float GetValue(Action* action); +}; + +class FathomLordKarathressDisableAoeMultiplier : public Multiplier +{ +public: + FathomLordKarathressDisableAoeMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "fathom-lord karathress disable aoe") {} + virtual float GetValue(Action* action); +}; + +class FathomLordKarathressControlMisdirectionMultiplier : public Multiplier +{ +public: + FathomLordKarathressControlMisdirectionMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "fathom-lord karathress control misdirection") {} + virtual float GetValue(Action* action); +}; + +class FathomLordKarathressWaitForDpsMultiplier : public Multiplier +{ +public: + FathomLordKarathressWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "fathom-lord karathress wait for dps") {} + virtual float GetValue(Action* action); +}; + +class FathomLordKarathressCaribdisTankHealerMaintainPositionMultiplier : public Multiplier +{ +public: + FathomLordKarathressCaribdisTankHealerMaintainPositionMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "fathom-lord karathress caribdis tank healer maintain position") {} + virtual float GetValue(Action* action); +}; + +// Morogrim Tidewalker + +class MorogrimTidewalkerDelayBloodlustAndHeroismMultiplier : public Multiplier +{ +public: + MorogrimTidewalkerDelayBloodlustAndHeroismMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "morogrim tidewalker delay bloodlust and heroism") {} + virtual float GetValue(Action* action); +}; + +class MorogrimTidewalkerDisableTankActionsMultiplier : public Multiplier +{ +public: + MorogrimTidewalkerDisableTankActionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "morogrim tidewalker disable tank actions") {} + virtual float GetValue(Action* action); +}; + +class MorogrimTidewalkerMaintainPhase2StackingMultiplier : public Multiplier +{ +public: + MorogrimTidewalkerMaintainPhase2StackingMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "morogrim tidewalker maintain phase2 stacking") {} + virtual float GetValue(Action* action); +}; + +// Lady Vashj + +class LadyVashjDelayCooldownsMultiplier : public Multiplier +{ +public: + LadyVashjDelayCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "lady vashj delay cooldowns") {} + virtual float GetValue(Action* action); +}; + +class LadyVashjMaintainPhase1RangedSpreadMultiplier : public Multiplier +{ +public: + LadyVashjMaintainPhase1RangedSpreadMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "lady vashj maintain phase1 ranged spread") {} + virtual float GetValue(Action* action); +}; + +class LadyVashjStaticChargeStayAwayFromGroupMultiplier : public Multiplier +{ +public: + LadyVashjStaticChargeStayAwayFromGroupMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "lady vashj static charge stay away from group") {} + virtual float GetValue(Action* action); +}; + +class LadyVashjDoNotLootTheTaintedCoreMultiplier : public Multiplier +{ +public: + LadyVashjDoNotLootTheTaintedCoreMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "lady vashj do not loot the tainted core") {} + virtual float GetValue(Action* action); +}; + +class LadyVashjCorePassersPrioritizePositioningMultiplier : public Multiplier +{ +public: + LadyVashjCorePassersPrioritizePositioningMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "lady vashj core passers prioritize positioning") {} + virtual float GetValue(Action* action); +}; + +class LadyVashjDisableAutomaticTargetingAndMovementModifier : public Multiplier +{ +public: + LadyVashjDisableAutomaticTargetingAndMovementModifier( + PlayerbotAI* botAI) : Multiplier(botAI, "lady vashj disable automatic targeting and movement") {} + virtual float GetValue(Action* action); +}; + +#endif diff --git a/src/Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h b/src/Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h new file mode 100644 index 000000000..e6dce1694 --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h @@ -0,0 +1,337 @@ +#ifndef _PLAYERBOT_RAIDSSCACTIONCONTEXT_H +#define _PLAYERBOT_RAIDSSCACTIONCONTEXT_H + +#include "RaidSSCActions.h" +#include "NamedObjectContext.h" + +class RaidSSCActionContext : public NamedObjectContext +{ +public: + RaidSSCActionContext() + { + // General + creators["serpent shrine cavern erase timers and trackers"] = + &RaidSSCActionContext::serpent_shrine_cavern_erase_timers_and_trackers; + + // Trash + creators["underbog colossus escape toxic pool"] = + &RaidSSCActionContext::underbog_colossus_escape_toxic_pool; + + creators["greyheart tidecaller mark water elemental totem"] = + &RaidSSCActionContext::greyheart_tidecaller_mark_water_elemental_totem; + + // Hydross the Unstable + creators["hydross the unstable position frost tank"] = + &RaidSSCActionContext::hydross_the_unstable_position_frost_tank; + + creators["hydross the unstable position nature tank"] = + &RaidSSCActionContext::hydross_the_unstable_position_nature_tank; + + creators["hydross the unstable prioritize elemental adds"] = + &RaidSSCActionContext::hydross_the_unstable_prioritize_elemental_adds; + + creators["hydross the unstable frost phase spread out"] = + &RaidSSCActionContext::hydross_the_unstable_frost_phase_spread_out; + + creators["hydross the unstable misdirect boss to tank"] = + &RaidSSCActionContext::hydross_the_unstable_misdirect_boss_to_tank; + + creators["hydross the unstable stop dps upon phase change"] = + &RaidSSCActionContext::hydross_the_unstable_stop_dps_upon_phase_change; + + creators["hydross the unstable manage timers"] = + &RaidSSCActionContext::hydross_the_unstable_manage_timers; + + // The Lurker Below + creators["the lurker below run around behind boss"] = + &RaidSSCActionContext::the_lurker_below_run_around_behind_boss; + + creators["the lurker below position main tank"] = + &RaidSSCActionContext::the_lurker_below_position_main_tank; + + creators["the lurker below spread ranged in arc"] = + &RaidSSCActionContext::the_lurker_below_spread_ranged_in_arc; + + creators["the lurker below tanks pick up adds"] = + &RaidSSCActionContext::the_lurker_below_tanks_pick_up_adds; + + creators["the lurker below manage spout timer"] = + &RaidSSCActionContext::the_lurker_below_manage_spout_timer; + + // Leotheras the Blind + creators["leotheras the blind target spellbinders"] = + &RaidSSCActionContext::leotheras_the_blind_target_spellbinders; + + creators["leotheras the blind demon form tank attack boss"] = + &RaidSSCActionContext::leotheras_the_blind_demon_form_tank_attack_boss; + + creators["leotheras the blind melee tanks don't attack demon form"] = + &RaidSSCActionContext::leotheras_the_blind_melee_tanks_dont_attack_demon_form; + + creators["leotheras the blind position ranged"] = + &RaidSSCActionContext::leotheras_the_blind_position_ranged; + + creators["leotheras the blind run away from whirlwind"] = + &RaidSSCActionContext::leotheras_the_blind_run_away_from_whirlwind; + + creators["leotheras the blind melee dps run away from boss"] = + &RaidSSCActionContext::leotheras_the_blind_melee_dps_run_away_from_boss; + + creators["leotheras the blind destroy inner demon"] = + &RaidSSCActionContext::leotheras_the_blind_destroy_inner_demon; + + creators["leotheras the blind final phase assign dps priority"] = + &RaidSSCActionContext::leotheras_the_blind_final_phase_assign_dps_priority; + + creators["leotheras the blind misdirect boss to demon form tank"] = + &RaidSSCActionContext::leotheras_the_blind_misdirect_boss_to_demon_form_tank; + + creators["leotheras the blind manage dps wait timers"] = + &RaidSSCActionContext::leotheras_the_blind_manage_dps_wait_timers; + + // Fathom-Lord Karathress + creators["fathom-lord karathress main tank position boss"] = + &RaidSSCActionContext::fathom_lord_karathress_main_tank_position_boss; + + creators["fathom-lord karathress first assist tank position caribdis"] = + &RaidSSCActionContext::fathom_lord_karathress_first_assist_tank_position_caribdis; + + creators["fathom-lord karathress second assist tank position sharkkis"] = + &RaidSSCActionContext::fathom_lord_karathress_second_assist_tank_position_sharkkis; + + creators["fathom-lord karathress third assist tank position tidalvess"] = + &RaidSSCActionContext::fathom_lord_karathress_third_assist_tank_position_tidalvess; + + creators["fathom-lord karathress position caribdis tank healer"] = + &RaidSSCActionContext::fathom_lord_karathress_position_caribdis_tank_healer; + + creators["fathom-lord karathress misdirect bosses to tanks"] = + &RaidSSCActionContext::fathom_lord_karathress_misdirect_bosses_to_tanks; + + creators["fathom-lord karathress assign dps priority"] = + &RaidSSCActionContext::fathom_lord_karathress_assign_dps_priority; + + creators["fathom-lord karathress manage dps timer"] = + &RaidSSCActionContext::fathom_lord_karathress_manage_dps_timer; + + // Morogrim Tidewalker + creators["morogrim tidewalker misdirect boss to main tank"] = + &RaidSSCActionContext::morogrim_tidewalker_misdirect_boss_to_main_tank; + + creators["morogrim tidewalker move boss to tank position"] = + &RaidSSCActionContext::morogrim_tidewalker_move_boss_to_tank_position; + + creators["morogrim tidewalker phase 2 reposition ranged"] = + &RaidSSCActionContext::morogrim_tidewalker_phase_2_reposition_ranged; + + // Lady Vashj + creators["lady vashj main tank position boss"] = + &RaidSSCActionContext::lady_vashj_main_tank_position_boss; + + creators["lady vashj phase 1 spread ranged in arc"] = + &RaidSSCActionContext::lady_vashj_phase_1_spread_ranged_in_arc; + + creators["lady vashj set grounding totem in main tank group"] = + &RaidSSCActionContext::lady_vashj_set_grounding_totem_in_main_tank_group; + + creators["lady vashj static charge move away from group"] = + &RaidSSCActionContext::lady_vashj_static_charge_move_away_from_group; + + creators["lady vashj misdirect boss to main tank"] = + &RaidSSCActionContext::lady_vashj_misdirect_boss_to_main_tank; + + creators["lady vashj assign phase 2 and phase 3 dps priority"] = + &RaidSSCActionContext::lady_vashj_assign_phase_2_and_phase_3_dps_priority; + + creators["lady vashj misdirect strider to first assist tank"] = + &RaidSSCActionContext::lady_vashj_misdirect_strider_to_first_assist_tank; + + creators["lady vashj tank attack and move away strider"] = + &RaidSSCActionContext::lady_vashj_tank_attack_and_move_away_strider; + + creators["lady vashj loot tainted core"] = + &RaidSSCActionContext::lady_vashj_loot_tainted_core; + + creators["lady vashj teleport to tainted elemental"] = + &RaidSSCActionContext::lady_vashj_teleport_to_tainted_elemental; + + creators["lady vashj pass the tainted core"] = + &RaidSSCActionContext::lady_vashj_pass_the_tainted_core; + + creators["lady vashj destroy tainted core"] = + &RaidSSCActionContext::lady_vashj_destroy_tainted_core; + + creators["lady vashj erase core passing trackers"] = + &RaidSSCActionContext::lady_vashj_erase_core_passing_trackers; + + creators["lady vashj avoid toxic spores"] = + &RaidSSCActionContext::lady_vashj_avoid_toxic_spores; + + creators["lady vashj use free action abilities"] = + &RaidSSCActionContext::lady_vashj_use_free_action_abilities; + } + +private: + // General + static Action* serpent_shrine_cavern_erase_timers_and_trackers( + PlayerbotAI* botAI) { return new SerpentShrineCavernEraseTimersAndTrackersAction(botAI); } + + // Trash + static Action* underbog_colossus_escape_toxic_pool( + PlayerbotAI* botAI) { return new UnderbogColossusEscapeToxicPoolAction(botAI); } + + static Action* greyheart_tidecaller_mark_water_elemental_totem( + PlayerbotAI* botAI) { return new GreyheartTidecallerMarkWaterElementalTotemAction(botAI); } + + // Hydross the Unstable + static Action* hydross_the_unstable_position_frost_tank( + PlayerbotAI* botAI) { return new HydrossTheUnstablePositionFrostTankAction(botAI); } + + static Action* hydross_the_unstable_position_nature_tank( + PlayerbotAI* botAI) { return new HydrossTheUnstablePositionNatureTankAction(botAI); } + + static Action* hydross_the_unstable_prioritize_elemental_adds( + PlayerbotAI* botAI) { return new HydrossTheUnstablePrioritizeElementalAddsAction(botAI); } + + static Action* hydross_the_unstable_frost_phase_spread_out( + PlayerbotAI* botAI) { return new HydrossTheUnstableFrostPhaseSpreadOutAction(botAI); } + + static Action* hydross_the_unstable_misdirect_boss_to_tank( + PlayerbotAI* botAI) { return new HydrossTheUnstableMisdirectBossToTankAction(botAI); } + + static Action* hydross_the_unstable_stop_dps_upon_phase_change( + PlayerbotAI* botAI) { return new HydrossTheUnstableStopDpsUponPhaseChangeAction(botAI); } + + static Action* hydross_the_unstable_manage_timers( + PlayerbotAI* botAI) { return new HydrossTheUnstableManageTimersAction(botAI); } + + // The Lurker Below + static Action* the_lurker_below_run_around_behind_boss( + PlayerbotAI* botAI) { return new TheLurkerBelowRunAroundBehindBossAction(botAI); } + + static Action* the_lurker_below_position_main_tank( + PlayerbotAI* botAI) { return new TheLurkerBelowPositionMainTankAction(botAI); } + + static Action* the_lurker_below_spread_ranged_in_arc( + PlayerbotAI* botAI) { return new TheLurkerBelowSpreadRangedInArcAction(botAI); } + + static Action* the_lurker_below_tanks_pick_up_adds( + PlayerbotAI* botAI) { return new TheLurkerBelowTanksPickUpAddsAction(botAI); } + + static Action* the_lurker_below_manage_spout_timer( + PlayerbotAI* botAI) { return new TheLurkerBelowManageSpoutTimerAction(botAI); } + + // Leotheras the Blind + static Action* leotheras_the_blind_target_spellbinders( + PlayerbotAI* botAI) { return new LeotherasTheBlindTargetSpellbindersAction(botAI); } + + static Action* leotheras_the_blind_demon_form_tank_attack_boss( + PlayerbotAI* botAI) { return new LeotherasTheBlindDemonFormTankAttackBossAction(botAI); } + + static Action* leotheras_the_blind_melee_tanks_dont_attack_demon_form( + PlayerbotAI* botAI) { return new LeotherasTheBlindMeleeTanksDontAttackDemonFormAction(botAI); } + + static Action* leotheras_the_blind_position_ranged( + PlayerbotAI* botAI) { return new LeotherasTheBlindPositionRangedAction(botAI); } + + static Action* leotheras_the_blind_run_away_from_whirlwind( + PlayerbotAI* botAI) { return new LeotherasTheBlindRunAwayFromWhirlwindAction(botAI); } + + static Action* leotheras_the_blind_melee_dps_run_away_from_boss( + PlayerbotAI* botAI) { return new LeotherasTheBlindMeleeDpsRunAwayFromBossAction(botAI); } + + static Action* leotheras_the_blind_destroy_inner_demon( + PlayerbotAI* botAI) { return new LeotherasTheBlindDestroyInnerDemonAction(botAI); } + + static Action* leotheras_the_blind_misdirect_boss_to_demon_form_tank( + PlayerbotAI* botAI) { return new LeotherasTheBlindMisdirectBossToDemonFormTankAction(botAI); } + + static Action* leotheras_the_blind_final_phase_assign_dps_priority( + PlayerbotAI* botAI) { return new LeotherasTheBlindFinalPhaseAssignDpsPriorityAction(botAI); } + + static Action* leotheras_the_blind_manage_dps_wait_timers( + PlayerbotAI* botAI) { return new LeotherasTheBlindManageDpsWaitTimersAction(botAI); } + + // Fathom-Lord Karathress + static Action* fathom_lord_karathress_main_tank_position_boss( + PlayerbotAI* botAI) { return new FathomLordKarathressMainTankPositionBossAction(botAI); } + + static Action* fathom_lord_karathress_first_assist_tank_position_caribdis( + PlayerbotAI* botAI) { return new FathomLordKarathressFirstAssistTankPositionCaribdisAction(botAI); } + + static Action* fathom_lord_karathress_second_assist_tank_position_sharkkis( + PlayerbotAI* botAI) { return new FathomLordKarathressSecondAssistTankPositionSharkkisAction(botAI); } + + static Action* fathom_lord_karathress_third_assist_tank_position_tidalvess( + PlayerbotAI* botAI) { return new FathomLordKarathressThirdAssistTankPositionTidalvessAction(botAI); } + + static Action* fathom_lord_karathress_position_caribdis_tank_healer( + PlayerbotAI* botAI) { return new FathomLordKarathressPositionCaribdisTankHealerAction(botAI); } + + static Action* fathom_lord_karathress_misdirect_bosses_to_tanks( + PlayerbotAI* botAI) { return new FathomLordKarathressMisdirectBossesToTanksAction(botAI); } + + static Action* fathom_lord_karathress_assign_dps_priority( + PlayerbotAI* botAI) { return new FathomLordKarathressAssignDpsPriorityAction(botAI); } + + static Action* fathom_lord_karathress_manage_dps_timer( + PlayerbotAI* botAI) { return new FathomLordKarathressManageDpsTimerAction(botAI); } + + // Morogrim Tidewalker + static Action* morogrim_tidewalker_misdirect_boss_to_main_tank( + PlayerbotAI* botAI) { return new MorogrimTidewalkerMisdirectBossToMainTankAction(botAI); } + + static Action* morogrim_tidewalker_move_boss_to_tank_position( + PlayerbotAI* botAI) { return new MorogrimTidewalkerMoveBossToTankPositionAction(botAI); } + + static Action* morogrim_tidewalker_phase_2_reposition_ranged( + PlayerbotAI* botAI) { return new MorogrimTidewalkerPhase2RepositionRangedAction(botAI); } + + // Lady Vashj + static Action* lady_vashj_main_tank_position_boss( + PlayerbotAI* botAI) { return new LadyVashjMainTankPositionBossAction(botAI); } + + static Action* lady_vashj_phase_1_spread_ranged_in_arc( + PlayerbotAI* botAI) { return new LadyVashjPhase1SpreadRangedInArcAction(botAI); } + + static Action* lady_vashj_set_grounding_totem_in_main_tank_group( + PlayerbotAI* botAI) { return new LadyVashjSetGroundingTotemInMainTankGroupAction(botAI); } + + static Action* lady_vashj_static_charge_move_away_from_group( + PlayerbotAI* botAI) { return new LadyVashjStaticChargeMoveAwayFromGroupAction(botAI); } + + static Action* lady_vashj_misdirect_boss_to_main_tank( + PlayerbotAI* botAI) { return new LadyVashjMisdirectBossToMainTankAction(botAI); } + + static Action* lady_vashj_assign_phase_2_and_phase_3_dps_priority( + PlayerbotAI* botAI) { return new LadyVashjAssignPhase2AndPhase3DpsPriorityAction(botAI); } + + static Action* lady_vashj_misdirect_strider_to_first_assist_tank( + PlayerbotAI* botAI) { return new LadyVashjMisdirectStriderToFirstAssistTankAction(botAI); } + + static Action* lady_vashj_tank_attack_and_move_away_strider( + PlayerbotAI* botAI) { return new LadyVashjTankAttackAndMoveAwayStriderAction(botAI); } + + static Action* lady_vashj_teleport_to_tainted_elemental( + PlayerbotAI* botAI) { return new LadyVashjTeleportToTaintedElementalAction(botAI); } + + static Action* lady_vashj_loot_tainted_core( + PlayerbotAI* botAI) { return new LadyVashjLootTaintedCoreAction(botAI); } + + static Action* lady_vashj_pass_the_tainted_core( + PlayerbotAI* botAI) { return new LadyVashjPassTheTaintedCoreAction(botAI); } + + static Action* lady_vashj_destroy_tainted_core( + PlayerbotAI* botAI) { return new LadyVashjDestroyTaintedCoreAction(botAI); } + + static Action* lady_vashj_erase_core_passing_trackers( + PlayerbotAI* botAI) { return new LadyVashjEraseCorePassingTrackersAction(botAI); } + + static Action* lady_vashj_avoid_toxic_spores( + PlayerbotAI* botAI) { return new LadyVashjAvoidToxicSporesAction(botAI); } + + static Action* lady_vashj_use_free_action_abilities( + PlayerbotAI* botAI) { return new LadyVashjUseFreeActionAbilitiesAction(botAI); } +}; + +#endif diff --git a/src/Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h b/src/Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h new file mode 100644 index 000000000..13135bf3e --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h @@ -0,0 +1,325 @@ +#ifndef _PLAYERBOT_RAIDSSCTRIGGERCONTEXT_H +#define _PLAYERBOT_RAIDSSCTRIGGERCONTEXT_H + +#include "RaidSSCTriggers.h" +#include "AiObjectContext.h" + +class RaidSSCTriggerContext : public NamedObjectContext +{ +public: + RaidSSCTriggerContext() + { + // General + creators["serpent shrine cavern bot is not in combat"] = + &RaidSSCTriggerContext::serpent_shrine_cavern_bot_is_not_in_combat; + + // Trash + creators["underbog colossus spawned toxic pool after death"] = + &RaidSSCTriggerContext::underbog_colossus_spawned_toxic_pool_after_death; + + creators["greyheart tidecaller water elemental totem spawned"] = + &RaidSSCTriggerContext::greyheart_tidecaller_water_elemental_totem_spawned; + + // Hydross the Unstable + creators["hydross the unstable bot is frost tank"] = + &RaidSSCTriggerContext::hydross_the_unstable_bot_is_frost_tank; + + creators["hydross the unstable bot is nature tank"] = + &RaidSSCTriggerContext::hydross_the_unstable_bot_is_nature_tank; + + creators["hydross the unstable elementals spawned"] = + &RaidSSCTriggerContext::hydross_the_unstable_elementals_spawned; + + creators["hydross the unstable danger from water tombs"] = + &RaidSSCTriggerContext::hydross_the_unstable_danger_from_water_tombs; + + creators["hydross the unstable tank needs aggro upon phase change"] = + &RaidSSCTriggerContext::hydross_the_unstable_tank_needs_aggro_upon_phase_change; + + creators["hydross the unstable aggro resets upon phase change"] = + &RaidSSCTriggerContext::hydross_the_unstable_aggro_resets_upon_phase_change; + + creators["hydross the unstable need to manage timers"] = + &RaidSSCTriggerContext::hydross_the_unstable_need_to_manage_timers; + + // The Lurker Below + creators["the lurker below spout is active"] = + &RaidSSCTriggerContext::the_lurker_below_spout_is_active; + + creators["the lurker below boss is active for main tank"] = + &RaidSSCTriggerContext::the_lurker_below_boss_is_active_for_main_tank; + + creators["the lurker below boss casts geyser"] = + &RaidSSCTriggerContext::the_lurker_below_boss_casts_geyser; + + creators["the lurker below boss is submerged"] = + &RaidSSCTriggerContext::the_lurker_below_boss_is_submerged; + + creators["the lurker below need to prepare timer for spout"] = + &RaidSSCTriggerContext::the_lurker_below_need_to_prepare_timer_for_spout; + + // Leotheras the Blind + creators["leotheras the blind boss is inactive"] = + &RaidSSCTriggerContext::leotheras_the_blind_boss_is_inactive; + + creators["leotheras the blind boss transformed into demon form"] = + &RaidSSCTriggerContext::leotheras_the_blind_boss_transformed_into_demon_form; + + creators["leotheras the blind only warlock should tank demon form"] = + &RaidSSCTriggerContext::leotheras_the_blind_only_warlock_should_tank_demon_form; + + creators["leotheras the blind boss engaged by ranged"] = + &RaidSSCTriggerContext::leotheras_the_blind_boss_engaged_by_ranged; + + creators["leotheras the blind boss channeling whirlwind"] = + &RaidSSCTriggerContext::leotheras_the_blind_boss_channeling_whirlwind; + + creators["leotheras the blind bot has too many chaos blast stacks"] = + &RaidSSCTriggerContext::leotheras_the_blind_bot_has_too_many_chaos_blast_stacks; + + creators["leotheras the blind inner demon has awakened"] = + &RaidSSCTriggerContext::leotheras_the_blind_inner_demon_has_awakened; + + creators["leotheras the blind entered final phase"] = + &RaidSSCTriggerContext::leotheras_the_blind_entered_final_phase; + + creators["leotheras the blind demon form tank needs aggro"] = + &RaidSSCTriggerContext::leotheras_the_blind_demon_form_tank_needs_aggro; + + creators["leotheras the blind boss wipes aggro upon phase change"] = + &RaidSSCTriggerContext::leotheras_the_blind_boss_wipes_aggro_upon_phase_change; + + // Fathom-Lord Karathress + creators["fathom-lord karathress boss engaged by main tank"] = + &RaidSSCTriggerContext::fathom_lord_karathress_boss_engaged_by_main_tank; + + creators["fathom-lord karathress caribdis engaged by first assist tank"] = + &RaidSSCTriggerContext::fathom_lord_karathress_caribdis_engaged_by_first_assist_tank; + + creators["fathom-lord karathress sharkkis engaged by second assist tank"] = + &RaidSSCTriggerContext::fathom_lord_karathress_sharkkis_engaged_by_second_assist_tank; + + creators["fathom-lord karathress tidalvess engaged by third assist tank"] = + &RaidSSCTriggerContext::fathom_lord_karathress_tidalvess_engaged_by_third_assist_tank; + + creators["fathom-lord karathress caribdis tank needs dedicated healer"] = + &RaidSSCTriggerContext::fathom_lord_karathress_caribdis_tank_needs_dedicated_healer; + + creators["fathom-lord karathress pulling bosses"] = + &RaidSSCTriggerContext::fathom_lord_karathress_pulling_bosses; + + creators["fathom-lord karathress determining kill order"] = + &RaidSSCTriggerContext::fathom_lord_karathress_determining_kill_order; + + creators["fathom-lord karathress tanks need to establish aggro"] = + &RaidSSCTriggerContext::fathom_lord_karathress_tanks_need_to_establish_aggro; + + // Morogrim Tidewalker + creators["morogrim tidewalker boss engaged by main tank"] = + &RaidSSCTriggerContext::morogrim_tidewalker_boss_engaged_by_main_tank; + + creators["morogrim tidewalker pulling boss"] = + &RaidSSCTriggerContext::morogrim_tidewalker_pulling_boss; + + creators["morogrim tidewalker water globules are incoming"] = + &RaidSSCTriggerContext::morogrim_tidewalker_water_globules_are_incoming; + + // Lady Vashj + creators["lady vashj boss engaged by main tank"] = + &RaidSSCTriggerContext::lady_vashj_boss_engaged_by_main_tank; + + creators["lady vashj boss engaged by ranged in phase 1"] = + &RaidSSCTriggerContext::lady_vashj_boss_engaged_by_ranged_in_phase_1; + + creators["lady vashj casts shock blast on highest aggro"] = + &RaidSSCTriggerContext::lady_vashj_casts_shock_blast_on_highest_aggro; + + creators["lady vashj bot has static charge"] = + &RaidSSCTriggerContext::lady_vashj_bot_has_static_charge; + + creators["lady vashj pulling boss in phase 1 and phase 3"] = + &RaidSSCTriggerContext::lady_vashj_pulling_boss_in_phase_1_and_phase_3; + + creators["lady vashj adds spawn in phase 2 and phase 3"] = + &RaidSSCTriggerContext::lady_vashj_adds_spawn_in_phase_2_and_phase_3; + + creators["lady vashj coilfang strider is approaching"] = + &RaidSSCTriggerContext::lady_vashj_coilfang_strider_is_approaching; + + creators["lady vashj tainted elemental cheat"] = + &RaidSSCTriggerContext::lady_vashj_tainted_elemental_cheat; + + creators["lady vashj tainted core was looted"] = + &RaidSSCTriggerContext::lady_vashj_tainted_core_was_looted; + + creators["lady vashj tainted core is unusable"] = + &RaidSSCTriggerContext::lady_vashj_tainted_core_is_unusable; + + creators["lady vashj need to reset core passing trackers"] = + &RaidSSCTriggerContext::lady_vashj_need_to_reset_core_passing_trackers; + + creators["lady vashj toxic sporebats are spewing poison clouds"] = + &RaidSSCTriggerContext::lady_vashj_toxic_sporebats_are_spewing_poison_clouds; + + creators["lady vashj bot is entangled in toxic spores or static charge"] = + &RaidSSCTriggerContext::lady_vashj_bot_is_entangled_in_toxic_spores_or_static_charge; + } + +private: + // General + static Trigger* serpent_shrine_cavern_bot_is_not_in_combat( + PlayerbotAI* botAI) { return new SerpentShrineCavernBotIsNotInCombatTrigger(botAI); } + + // Trash + static Trigger* underbog_colossus_spawned_toxic_pool_after_death( + PlayerbotAI* botAI) { return new UnderbogColossusSpawnedToxicPoolAfterDeathTrigger(botAI); } + + static Trigger* greyheart_tidecaller_water_elemental_totem_spawned( + PlayerbotAI* botAI) { return new GreyheartTidecallerWaterElementalTotemSpawnedTrigger(botAI); } + + // Hydross the Unstable + static Trigger* hydross_the_unstable_bot_is_frost_tank( + PlayerbotAI* botAI) { return new HydrossTheUnstableBotIsFrostTankTrigger(botAI); } + + static Trigger* hydross_the_unstable_bot_is_nature_tank( + PlayerbotAI* botAI) { return new HydrossTheUnstableBotIsNatureTankTrigger(botAI); } + + static Trigger* hydross_the_unstable_elementals_spawned( + PlayerbotAI* botAI) { return new HydrossTheUnstableElementalsSpawnedTrigger(botAI); } + + static Trigger* hydross_the_unstable_danger_from_water_tombs( + PlayerbotAI* botAI) { return new HydrossTheUnstableDangerFromWaterTombsTrigger(botAI); } + + static Trigger* hydross_the_unstable_tank_needs_aggro_upon_phase_change( + PlayerbotAI* botAI) { return new HydrossTheUnstableTankNeedsAggroUponPhaseChangeTrigger(botAI); } + + static Trigger* hydross_the_unstable_aggro_resets_upon_phase_change( + PlayerbotAI* botAI) { return new HydrossTheUnstableAggroResetsUponPhaseChangeTrigger(botAI); } + + static Trigger* hydross_the_unstable_need_to_manage_timers( + PlayerbotAI* botAI) { return new HydrossTheUnstableNeedToManageTimersTrigger(botAI); } + + // The Lurker Below + static Trigger* the_lurker_below_spout_is_active( + PlayerbotAI* botAI) { return new TheLurkerBelowSpoutIsActiveTrigger(botAI); } + + static Trigger* the_lurker_below_boss_is_active_for_main_tank( + PlayerbotAI* botAI) { return new TheLurkerBelowBossIsActiveForMainTankTrigger(botAI); } + + static Trigger* the_lurker_below_boss_casts_geyser( + PlayerbotAI* botAI) { return new TheLurkerBelowBossCastsGeyserTrigger(botAI); } + + static Trigger* the_lurker_below_boss_is_submerged( + PlayerbotAI* botAI) { return new TheLurkerBelowBossIsSubmergedTrigger(botAI); } + + static Trigger* the_lurker_below_need_to_prepare_timer_for_spout( + PlayerbotAI* botAI) { return new TheLurkerBelowNeedToPrepareTimerForSpoutTrigger(botAI); } + + // Leotheras the Blind + static Trigger* leotheras_the_blind_boss_is_inactive( + PlayerbotAI* botAI) { return new LeotherasTheBlindBossIsInactiveTrigger(botAI); } + + static Trigger* leotheras_the_blind_boss_transformed_into_demon_form( + PlayerbotAI* botAI) { return new LeotherasTheBlindBossTransformedIntoDemonFormTrigger(botAI); } + + static Trigger* leotheras_the_blind_only_warlock_should_tank_demon_form( + PlayerbotAI* botAI) { return new LeotherasTheBlindOnlyWarlockShouldTankDemonFormTrigger(botAI); } + + static Trigger* leotheras_the_blind_boss_engaged_by_ranged( + PlayerbotAI* botAI) { return new LeotherasTheBlindBossEngagedByRangedTrigger(botAI); } + + static Trigger* leotheras_the_blind_boss_channeling_whirlwind( + PlayerbotAI* botAI) { return new LeotherasTheBlindBossChannelingWhirlwindTrigger(botAI); } + + static Trigger* leotheras_the_blind_bot_has_too_many_chaos_blast_stacks( + PlayerbotAI* botAI) { return new LeotherasTheBlindBotHasTooManyChaosBlastStacksTrigger(botAI); } + + static Trigger* leotheras_the_blind_inner_demon_has_awakened( + PlayerbotAI* botAI) { return new LeotherasTheBlindInnerDemonHasAwakenedTrigger(botAI); } + + static Trigger* leotheras_the_blind_entered_final_phase( + PlayerbotAI* botAI) { return new LeotherasTheBlindEnteredFinalPhaseTrigger(botAI); } + + static Trigger* leotheras_the_blind_demon_form_tank_needs_aggro( + PlayerbotAI* botAI) { return new LeotherasTheBlindDemonFormTankNeedsAggro(botAI); } + + static Trigger* leotheras_the_blind_boss_wipes_aggro_upon_phase_change( + PlayerbotAI* botAI) { return new LeotherasTheBlindBossWipesAggroUponPhaseChangeTrigger(botAI); } + + // Fathom-Lord Karathress + static Trigger* fathom_lord_karathress_boss_engaged_by_main_tank( + PlayerbotAI* botAI) { return new FathomLordKarathressBossEngagedByMainTankTrigger(botAI); } + + static Trigger* fathom_lord_karathress_caribdis_engaged_by_first_assist_tank( + PlayerbotAI* botAI) { return new FathomLordKarathressCaribdisEngagedByFirstAssistTankTrigger(botAI); } + + static Trigger* fathom_lord_karathress_sharkkis_engaged_by_second_assist_tank( + PlayerbotAI* botAI) { return new FathomLordKarathressSharkkisEngagedBySecondAssistTankTrigger(botAI); } + + static Trigger* fathom_lord_karathress_tidalvess_engaged_by_third_assist_tank( + PlayerbotAI* botAI) { return new FathomLordKarathressTidalvessEngagedByThirdAssistTankTrigger(botAI); } + + static Trigger* fathom_lord_karathress_caribdis_tank_needs_dedicated_healer( + PlayerbotAI* botAI) { return new FathomLordKarathressCaribdisTankNeedsDedicatedHealerTrigger(botAI); } + + static Trigger* fathom_lord_karathress_pulling_bosses( + PlayerbotAI* botAI) { return new FathomLordKarathressPullingBossesTrigger(botAI); } + + static Trigger* fathom_lord_karathress_determining_kill_order( + PlayerbotAI* botAI) { return new FathomLordKarathressDeterminingKillOrderTrigger(botAI); } + + static Trigger* fathom_lord_karathress_tanks_need_to_establish_aggro( + PlayerbotAI* botAI) { return new FathomLordKarathressTanksNeedToEstablishAggroTrigger(botAI); } + + // Morogrim Tidewalker + static Trigger* morogrim_tidewalker_boss_engaged_by_main_tank( + PlayerbotAI* botAI) { return new MorogrimTidewalkerBossEngagedByMainTankTrigger(botAI); } + + static Trigger* morogrim_tidewalker_pulling_boss( + PlayerbotAI* botAI) { return new MorogrimTidewalkerPullingBossTrigger(botAI); } + + static Trigger* morogrim_tidewalker_water_globules_are_incoming( + PlayerbotAI* botAI) { return new MorogrimTidewalkerWaterGlobulesAreIncomingTrigger(botAI); } + + // Lady Vashj + static Trigger* lady_vashj_boss_engaged_by_main_tank( + PlayerbotAI* botAI) { return new LadyVashjBossEngagedByMainTankTrigger(botAI); } + + static Trigger* lady_vashj_boss_engaged_by_ranged_in_phase_1( + PlayerbotAI* botAI) { return new LadyVashjBossEngagedByRangedInPhase1Trigger(botAI); } + + static Trigger* lady_vashj_casts_shock_blast_on_highest_aggro( + PlayerbotAI* botAI) { return new LadyVashjCastsShockBlastOnHighestAggroTrigger(botAI); } + + static Trigger* lady_vashj_bot_has_static_charge( + PlayerbotAI* botAI) { return new LadyVashjBotHasStaticChargeTrigger(botAI); } + + static Trigger* lady_vashj_pulling_boss_in_phase_1_and_phase_3( + PlayerbotAI* botAI) { return new LadyVashjPullingBossInPhase1AndPhase3Trigger(botAI); } + + static Trigger* lady_vashj_adds_spawn_in_phase_2_and_phase_3( + PlayerbotAI* botAI) { return new LadyVashjAddsSpawnInPhase2AndPhase3Trigger(botAI); } + + static Trigger* lady_vashj_coilfang_strider_is_approaching( + PlayerbotAI* botAI) { return new LadyVashjCoilfangStriderIsApproachingTrigger(botAI); } + + static Trigger* lady_vashj_tainted_elemental_cheat( + PlayerbotAI* botAI) { return new LadyVashjTaintedElementalCheatTrigger(botAI); } + + static Trigger* lady_vashj_tainted_core_was_looted( + PlayerbotAI* botAI) { return new LadyVashjTaintedCoreWasLootedTrigger(botAI); } + + static Trigger* lady_vashj_tainted_core_is_unusable( + PlayerbotAI* botAI) { return new LadyVashjTaintedCoreIsUnusableTrigger(botAI); } + + static Trigger* lady_vashj_need_to_reset_core_passing_trackers( + PlayerbotAI* botAI) { return new LadyVashjNeedToResetCorePassingTrackersTrigger(botAI); } + + static Trigger* lady_vashj_toxic_sporebats_are_spewing_poison_clouds( + PlayerbotAI* botAI) { return new LadyVashjToxicSporebatsAreSpewingPoisonCloudsTrigger(botAI); } + + static Trigger* lady_vashj_bot_is_entangled_in_toxic_spores_or_static_charge( + PlayerbotAI* botAI) { return new LadyVashjBotIsEntangledInToxicSporesOrStaticChargeTrigger(botAI); } +}; + +#endif diff --git a/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.cpp b/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.cpp new file mode 100644 index 000000000..139667dc6 --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.cpp @@ -0,0 +1,206 @@ +#include "RaidSSCStrategy.h" +#include "RaidSSCMultipliers.h" + +void RaidSSCStrategy::InitTriggers(std::vector& triggers) +{ + // General + triggers.push_back(new TriggerNode("serpent shrine cavern bot is not in combat", { + NextAction("serpent shrine cavern erase timers and trackers", ACTION_EMERGENCY + 11) })); + + // Trash Mobs + triggers.push_back(new TriggerNode("underbog colossus spawned toxic pool after death", { + NextAction("underbog colossus escape toxic pool", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("greyheart tidecaller water elemental totem spawned", { + NextAction("greyheart tidecaller mark water elemental totem", ACTION_RAID + 1) })); + + // Hydross the Unstable + triggers.push_back(new TriggerNode("hydross the unstable bot is frost tank", { + NextAction("hydross the unstable position frost tank", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("hydross the unstable bot is nature tank", { + NextAction("hydross the unstable position nature tank", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("hydross the unstable elementals spawned", { + NextAction("hydross the unstable prioritize elemental adds", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("hydross the unstable danger from water tombs", { + NextAction("hydross the unstable frost phase spread out", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("hydross the unstable tank needs aggro upon phase change", { + NextAction("hydross the unstable misdirect boss to tank", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("hydross the unstable aggro resets upon phase change", { + NextAction("hydross the unstable stop dps upon phase change", ACTION_EMERGENCY + 9) })); + + triggers.push_back(new TriggerNode("hydross the unstable need to manage timers", { + NextAction("hydross the unstable manage timers", ACTION_EMERGENCY + 10) })); + + // The Lurker Below + triggers.push_back(new TriggerNode("the lurker below spout is active", { + NextAction("the lurker below run around behind boss", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("the lurker below boss is active for main tank", { + NextAction("the lurker below position main tank", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("the lurker below boss casts geyser", { + NextAction("the lurker below spread ranged in arc", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("the lurker below boss is submerged", { + NextAction("the lurker below tanks pick up adds", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("the lurker below need to prepare timer for spout", { + NextAction("the lurker below manage spout timer", ACTION_EMERGENCY + 10) })); + + // Leotheras the Blind + triggers.push_back(new TriggerNode("leotheras the blind boss is inactive", { + NextAction("leotheras the blind target spellbinders", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("leotheras the blind boss transformed into demon form", { + NextAction("leotheras the blind demon form tank attack boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("leotheras the blind only warlock should tank demon form", { + NextAction("leotheras the blind melee tanks don't attack demon form", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("leotheras the blind boss engaged by ranged", { + NextAction("leotheras the blind position ranged", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("leotheras the blind boss channeling whirlwind", { + NextAction("leotheras the blind run away from whirlwind", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("leotheras the blind bot has too many chaos blast stacks", { + NextAction("leotheras the blind melee dps run away from boss", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("leotheras the blind inner demon has awakened", { + NextAction("leotheras the blind destroy inner demon", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("leotheras the blind entered final phase", { + NextAction("leotheras the blind final phase assign dps priority", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("leotheras the blind demon form tank needs aggro", { + NextAction("leotheras the blind misdirect boss to demon form tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("leotheras the blind boss wipes aggro upon phase change", { + NextAction("leotheras the blind manage dps wait timers", ACTION_EMERGENCY + 10) })); + + // Fathom-Lord Karathress + triggers.push_back(new TriggerNode("fathom-lord karathress boss engaged by main tank", { + NextAction("fathom-lord karathress main tank position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("fathom-lord karathress caribdis engaged by first assist tank", { + NextAction("fathom-lord karathress first assist tank position caribdis", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("fathom-lord karathress sharkkis engaged by second assist tank", { + NextAction("fathom-lord karathress second assist tank position sharkkis", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("fathom-lord karathress tidalvess engaged by third assist tank", { + NextAction("fathom-lord karathress third assist tank position tidalvess", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("fathom-lord karathress caribdis tank needs dedicated healer", { + NextAction("fathom-lord karathress position caribdis tank healer", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("fathom-lord karathress pulling bosses", { + NextAction("fathom-lord karathress misdirect bosses to tanks", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("fathom-lord karathress determining kill order", { + NextAction("fathom-lord karathress assign dps priority", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("fathom-lord karathress tanks need to establish aggro", { + NextAction("fathom-lord karathress manage dps timer", ACTION_EMERGENCY + 10) })); + + // Morogrim Tidewalker + triggers.push_back(new TriggerNode("morogrim tidewalker boss engaged by main tank", { + NextAction("morogrim tidewalker move boss to tank position", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("morogrim tidewalker water globules are incoming", { + NextAction("morogrim tidewalker phase 2 reposition ranged", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("morogrim tidewalker pulling boss", { + NextAction("morogrim tidewalker misdirect boss to main tank", ACTION_RAID + 1) })); + + // Lady Vashj + triggers.push_back(new TriggerNode("lady vashj boss engaged by main tank", { + NextAction("lady vashj main tank position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("lady vashj boss engaged by ranged in phase 1", { + NextAction("lady vashj phase 1 spread ranged in arc", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("lady vashj casts shock blast on highest aggro", { + NextAction("lady vashj set grounding totem in main tank group", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("lady vashj bot has static charge", { + NextAction("lady vashj static charge move away from group", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("lady vashj pulling boss in phase 1 and phase 3", { + NextAction("lady vashj misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("lady vashj tainted elemental cheat", { + NextAction("lady vashj teleport to tainted elemental", ACTION_EMERGENCY + 10), + NextAction("lady vashj loot tainted core", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("lady vashj tainted core was looted", { + NextAction("lady vashj pass the tainted core", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("lady vashj tainted core is unusable", { + NextAction("lady vashj destroy tainted core", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("lady vashj need to reset core passing trackers", { + NextAction("lady vashj erase core passing trackers", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("lady vashj adds spawn in phase 2 and phase 3", { + NextAction("lady vashj assign phase 2 and phase 3 dps priority", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("lady vashj coilfang strider is approaching", { + NextAction("lady vashj misdirect strider to first assist tank", ACTION_EMERGENCY + 2), + NextAction("lady vashj tank attack and move away strider", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("lady vashj toxic sporebats are spewing poison clouds", { + NextAction("lady vashj avoid toxic spores", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("lady vashj bot is entangled in toxic spores or static charge", { + NextAction("lady vashj use free action abilities", ACTION_EMERGENCY + 7) })); +} + +void RaidSSCStrategy::InitMultipliers(std::vector& multipliers) +{ + // Trash Mobs + multipliers.push_back(new UnderbogColossusEscapeToxicPoolMultiplier(botAI)); + + // Hydross the Unstable + multipliers.push_back(new HydrossTheUnstableDisableTankActionsMultiplier(botAI)); + multipliers.push_back(new HydrossTheUnstableWaitForDpsMultiplier(botAI)); + multipliers.push_back(new HydrossTheUnstableControlMisdirectionMultiplier(botAI)); + + // The Lurker Below + multipliers.push_back(new TheLurkerBelowStayAwayFromSpoutMultiplier(botAI)); + multipliers.push_back(new TheLurkerBelowMaintainRangedSpreadMultiplier(botAI)); + multipliers.push_back(new TheLurkerBelowDisableTankAssistMultiplier(botAI)); + + // Leotheras the Blind + multipliers.push_back(new LeotherasTheBlindAvoidWhirlwindMultiplier(botAI)); + multipliers.push_back(new LeotherasTheBlindDisableTankActionsMultiplier(botAI)); + multipliers.push_back(new LeotherasTheBlindMeleeDpsAvoidChaosBlastMultiplier(botAI)); + multipliers.push_back(new LeotherasTheBlindFocusOnInnerDemonMultiplier(botAI)); + multipliers.push_back(new LeotherasTheBlindWaitForDpsMultiplier(botAI)); + multipliers.push_back(new LeotherasTheBlindDelayBloodlustAndHeroismMultiplier(botAI)); + + // Fathom-Lord Karathress + multipliers.push_back(new FathomLordKarathressDisableTankActionsMultiplier(botAI)); + multipliers.push_back(new FathomLordKarathressDisableAoeMultiplier(botAI)); + multipliers.push_back(new FathomLordKarathressControlMisdirectionMultiplier(botAI)); + multipliers.push_back(new FathomLordKarathressWaitForDpsMultiplier(botAI)); + multipliers.push_back(new FathomLordKarathressCaribdisTankHealerMaintainPositionMultiplier(botAI)); + + // Morogrim Tidewalker + multipliers.push_back(new MorogrimTidewalkerDelayBloodlustAndHeroismMultiplier(botAI)); + multipliers.push_back(new MorogrimTidewalkerDisableTankActionsMultiplier(botAI)); + multipliers.push_back(new MorogrimTidewalkerMaintainPhase2StackingMultiplier(botAI)); + + // Lady Vashj + multipliers.push_back(new LadyVashjDelayCooldownsMultiplier(botAI)); + multipliers.push_back(new LadyVashjMaintainPhase1RangedSpreadMultiplier(botAI)); + multipliers.push_back(new LadyVashjStaticChargeStayAwayFromGroupMultiplier(botAI)); + multipliers.push_back(new LadyVashjDoNotLootTheTaintedCoreMultiplier(botAI)); + multipliers.push_back(new LadyVashjCorePassersPrioritizePositioningMultiplier(botAI)); + multipliers.push_back(new LadyVashjDisableAutomaticTargetingAndMovementModifier(botAI)); +} diff --git a/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.h b/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.h new file mode 100644 index 000000000..3c2c05f58 --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.h @@ -0,0 +1,18 @@ +#ifndef _PLAYERBOT_RAIDSSCSTRATEGY_H_ +#define _PLAYERBOT_RAIDSSCSTRATEGY_H_ + +#include "Strategy.h" +#include "Multiplier.h" + +class RaidSSCStrategy : public Strategy +{ +public: + RaidSSCStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + std::string const getName() override { return "ssc"; } + + void InitTriggers(std::vector& triggers) override; + void InitMultipliers(std::vector& multipliers) override; +}; + +#endif diff --git a/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.cpp b/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.cpp new file mode 100644 index 000000000..e77e63642 --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.cpp @@ -0,0 +1,670 @@ +#include "RaidSSCTriggers.h" +#include "RaidSSCHelpers.h" +#include "RaidSSCActions.h" +#include "AiFactory.h" +#include "Corpse.h" +#include "LootObjectStack.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +using namespace SerpentShrineCavernHelpers; + +// General +bool SerpentShrineCavernBotIsNotInCombatTrigger::IsActive() +{ + return !bot->IsInCombat(); +} + +// Trash Mobs + +bool UnderbogColossusSpawnedToxicPoolAfterDeathTrigger::IsActive() +{ + return bot->HasAura(SPELL_TOXIC_POOL); +} + +bool GreyheartTidecallerWaterElementalTotemSpawnedTrigger::IsActive() +{ + return botAI->IsDps(bot) && + GetFirstAliveUnitByEntry(botAI, NPC_WATER_ELEMENTAL_TOTEM); +} + +// Hydross the Unstable + +bool HydrossTheUnstableBotIsFrostTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "hydross the unstable") && + botAI->IsMainTank(bot); +} + +bool HydrossTheUnstableBotIsNatureTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "hydross the unstable") && + botAI->IsAssistTankOfIndex(bot, 0, true); +} + +bool HydrossTheUnstableElementalsSpawnedTrigger::IsActive() +{ + Unit* hydross = AI_VALUE2(Unit*, "find target", "hydross the unstable"); + if (hydross && hydross->GetHealthPct() < 10.0f) + return false; + + if (!AI_VALUE2(Unit*, "find target", "pure spawn of hydross") && + !AI_VALUE2(Unit*, "find target", "tainted spawn of hydross")) + return false; + + return !botAI->IsHeal(bot) && !botAI->IsMainTank(bot) && + !botAI->IsAssistTankOfIndex(bot, 0, true); +} + +bool HydrossTheUnstableDangerFromWaterTombsTrigger::IsActive() +{ + return botAI->IsRanged(bot) && + AI_VALUE2(Unit*, "find target", "hydross the unstable"); +} + +bool HydrossTheUnstableTankNeedsAggroUponPhaseChangeTrigger::IsActive() +{ + return bot->getClass() == CLASS_HUNTER && + AI_VALUE2(Unit*, "find target", "hydross the unstable"); +} + +bool HydrossTheUnstableAggroResetsUponPhaseChangeTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "hydross the unstable")) + return false; + + return bot->getClass() != CLASS_HUNTER && + !botAI->IsHeal(bot) && + !botAI->IsMainTank(bot) && + !botAI->IsAssistTankOfIndex(bot, 0, true); +} + +bool HydrossTheUnstableNeedToManageTimersTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "hydross the unstable") && + IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr); +} + +// The Lurker Below + +bool TheLurkerBelowSpoutIsActiveTrigger::IsActive() +{ + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker) + return false; + + const time_t now = std::time(nullptr); + + auto it = lurkerSpoutTimer.find(lurker->GetMap()->GetInstanceId()); + return it != lurkerSpoutTimer.end() && it->second > now; +} + +bool TheLurkerBelowBossIsActiveForMainTankTrigger::IsActive() +{ + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker) + return false; + + if (!botAI->IsMainTank(bot)) + return false; + + const time_t now = std::time(nullptr); + + auto it = lurkerSpoutTimer.find(lurker->GetMap()->GetInstanceId()); + return lurker->getStandState() != UNIT_STAND_STATE_SUBMERGED && + (it == lurkerSpoutTimer.end() || it->second <= now); +} + +bool TheLurkerBelowBossCastsGeyserTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker) + return false; + + const time_t now = std::time(nullptr); + + auto it = lurkerSpoutTimer.find(lurker->GetMap()->GetInstanceId()); + return lurker->getStandState() != UNIT_STAND_STATE_SUBMERGED && + (it == lurkerSpoutTimer.end() || it->second <= now); +} + +// Trigger will be active only if there are at least 3 tanks in the raid +bool TheLurkerBelowBossIsSubmergedTrigger::IsActive() +{ + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker || lurker->getStandState() != UNIT_STAND_STATE_SUBMERGED) + return false; + + Player* mainTank = nullptr; + Player* firstAssistTank = nullptr; + Player* secondAssistTank = nullptr; + + 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()) + continue; + + PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); + if (!memberAI) + continue; + + if (!mainTank && memberAI->IsMainTank(member)) + mainTank = member; + else if (!firstAssistTank && memberAI->IsAssistTankOfIndex(member, 0, true)) + firstAssistTank = member; + else if (!secondAssistTank && memberAI->IsAssistTankOfIndex(member, 1, true)) + secondAssistTank = member; + } + + if (!mainTank || !firstAssistTank || !secondAssistTank) + return false; + + return bot == mainTank || bot == firstAssistTank || bot == secondAssistTank; +} + +bool TheLurkerBelowNeedToPrepareTimerForSpoutTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "the lurker below") && + IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr); +} + +// Leotheras the Blind + +bool LeotherasTheBlindBossIsInactiveTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "greyheart spellbinder"); +} + +bool LeotherasTheBlindBossTransformedIntoDemonFormTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "leotheras the blind")) + return false; + + if (GetLeotherasDemonFormTank(bot) != bot) + return false; + + return GetActiveLeotherasDemon(botAI); +} + +bool LeotherasTheBlindOnlyWarlockShouldTankDemonFormTrigger::IsActive() +{ + if (botAI->IsRanged(bot) || !botAI->IsTank(bot)) + return false; + + if (!AI_VALUE2(Unit*, "find target", "leotheras the blind")) + return false; + + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return false; + + if (!GetLeotherasDemonFormTank(bot)) + return false; + + return GetPhase2LeotherasDemon(botAI); +} + +bool LeotherasTheBlindBossEngagedByRangedTrigger::IsActive() +{ + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return false; + + if (!botAI->IsRanged(bot)) + return false; + + Unit* leotheras = AI_VALUE2(Unit*, "find target", "leotheras the blind"); + if (!leotheras) + return false; + + return !leotheras->HasAura(SPELL_LEOTHERAS_BANISHED) && + !leotheras->HasAura(SPELL_WHIRLWIND) && + !leotheras->HasAura(SPELL_WHIRLWIND_CHANNEL); +} + +bool LeotherasTheBlindBossChannelingWhirlwindTrigger::IsActive() +{ + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return false; + + if (botAI->IsTank(bot) && botAI->IsMelee(bot)) + return false; + + Unit* leotheras = AI_VALUE2(Unit*, "find target", "leotheras the blind"); + if (!leotheras || leotheras->HasAura(SPELL_LEOTHERAS_BANISHED)) + return false; + + return leotheras->HasAura(SPELL_WHIRLWIND) || + leotheras->HasAura(SPELL_WHIRLWIND_CHANNEL); +} + +bool LeotherasTheBlindBotHasTooManyChaosBlastStacksTrigger::IsActive() +{ + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return false; + + if (botAI->IsRanged(bot)) + return false; + + Aura* chaosBlast = bot->GetAura(SPELL_CHAOS_BLAST); + if (!chaosBlast || chaosBlast->GetStackAmount() < 5) + return false; + + if (!GetLeotherasDemonFormTank(bot) && botAI->IsMainTank(bot)) + return false; + + return GetPhase2LeotherasDemon(botAI); +} + +bool LeotherasTheBlindInnerDemonHasAwakenedTrigger::IsActive() +{ + return bot->HasAura(SPELL_INSIDIOUS_WHISPER) && + GetLeotherasDemonFormTank(bot) != bot; +} + +bool LeotherasTheBlindEnteredFinalPhaseTrigger::IsActive() +{ + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return false; + + if (botAI->IsHeal(bot)) + return false; + + if (GetLeotherasDemonFormTank(bot) == bot) + return false; + + return GetPhase3LeotherasDemon(botAI) && + GetLeotherasHuman(botAI); +} + +bool LeotherasTheBlindDemonFormTankNeedsAggro::IsActive() +{ + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return false; + + if (bot->getClass() != CLASS_HUNTER) + return false; + + return AI_VALUE2(Unit*, "find target", "leotheras the blind"); +} + +bool LeotherasTheBlindBossWipesAggroUponPhaseChangeTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "leotheras the blind") && + IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr); +} + +// Fathom-Lord Karathress + +bool FathomLordKarathressBossEngagedByMainTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "fathom-lord karathress") && + botAI->IsMainTank(bot); +} + +bool FathomLordKarathressCaribdisEngagedByFirstAssistTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "fathom-guard caribdis") && + botAI->IsAssistTankOfIndex(bot, 0, false); +} + +bool FathomLordKarathressSharkkisEngagedBySecondAssistTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "fathom-guard sharkkis") && + botAI->IsAssistTankOfIndex(bot, 1, false); +} + +bool FathomLordKarathressTidalvessEngagedByThirdAssistTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "fathom-guard tidalvess") && + botAI->IsAssistTankOfIndex(bot, 2, false); +} + +bool FathomLordKarathressCaribdisTankNeedsDedicatedHealerTrigger::IsActive() +{ + Unit* caribdis = AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); + if (!caribdis) + return false; + + if (!botAI->IsAssistHealOfIndex(bot, 0, true)) + return false; + + Player* firstAssistTank = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (botAI->IsAssistTankOfIndex(member, 0, false)) + { + firstAssistTank = member; + break; + } + } + } + + return firstAssistTank; +} + +bool FathomLordKarathressPullingBossesTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* karathress = AI_VALUE2(Unit*, "find target", "fathom-lord karathress"); + return karathress && karathress->GetHealthPct() > 98.0f; +} + +bool FathomLordKarathressDeterminingKillOrderTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) + return false; + + if (botAI->IsHeal(bot)) + return false; + + if (botAI->IsDps(bot)) + return true; + else if (botAI->IsAssistTankOfIndex(bot, 0, false)) + return !AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); + else if (botAI->IsAssistTankOfIndex(bot, 1, false)) + return !AI_VALUE2(Unit*, "find target", "fathom-guard sharkkis"); + else if (botAI->IsAssistTankOfIndex(bot, 2, false)) + return !AI_VALUE2(Unit*, "find target", "fathom-guard tidalvess"); + else + return false; +} + +bool FathomLordKarathressTanksNeedToEstablishAggroTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "fathom-lord karathress") && + IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr); +} + +// Morogrim Tidewalker + +bool MorogrimTidewalkerPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* tidewalker = AI_VALUE2(Unit*, "find target", "morogrim tidewalker"); + return tidewalker && tidewalker->GetHealthPct() > 95.0f; +} + +bool MorogrimTidewalkerBossEngagedByMainTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "morogrim tidewalker") && + botAI->IsMainTank(bot); +} + +bool MorogrimTidewalkerWaterGlobulesAreIncomingTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* tidewalker = AI_VALUE2(Unit*, "find target", "morogrim tidewalker"); + return tidewalker && tidewalker->GetHealthPct() < 25.0f; +} + +// Lady Vashj + +bool LadyVashjBossEngagedByMainTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "lady vashj") && + !IsLadyVashjInPhase2(botAI) && botAI->IsMainTank(bot); +} + +bool LadyVashjBossEngagedByRangedInPhase1Trigger::IsActive() +{ + return botAI->IsRanged(bot) && IsLadyVashjInPhase1(botAI); +} + +bool LadyVashjCastsShockBlastOnHighestAggroTrigger::IsActive() +{ + if (bot->getClass() != CLASS_SHAMAN) + return false; + + if (!AI_VALUE2(Unit*, "find target", "lady vashj") || + IsLadyVashjInPhase2(botAI)) + return false; + + if (!IsMainTankInSameSubgroup(bot)) + return false; + + return true; +} + +bool LadyVashjBotHasStaticChargeTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + return false; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->HasAura(SPELL_STATIC_CHARGE)) + return true; + } + } + + return false; +} + +bool LadyVashjPullingBossInPhase1AndPhase3Trigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + return false; + + return (vashj->GetHealthPct() <= 100.0f && vashj->GetHealthPct() > 90.0f) || + (!vashj->HasUnitState(UNIT_STATE_ROOT) && vashj->GetHealthPct() <= 50.0f && + vashj->GetHealthPct() > 40.0f); +} + +bool LadyVashjAddsSpawnInPhase2AndPhase3Trigger::IsActive() +{ + if (botAI->IsHeal(bot)) + return false; + + return AI_VALUE2(Unit*, "find target", "lady vashj") && + !IsLadyVashjInPhase1(botAI); +} + +bool LadyVashjCoilfangStriderIsApproachingTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "coilfang strider"); +} + +bool LadyVashjTaintedElementalCheatTrigger::IsActive() +{ + if (!botAI->HasCheat(BotCheatMask::raid)) + return false; + + if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + return false; + + bool taintedPresent = false; + Unit* taintedUnit = AI_VALUE2(Unit*, "find target", "tainted elemental"); + if (taintedUnit) + taintedPresent = true; + else + { + GuidVector corpses = AI_VALUE(GuidVector, "nearest corpses"); + for (auto const& guid : corpses) + { + LootObject loot(bot, guid); + WorldObject* object = loot.GetWorldObject(bot); + if (!object) + continue; + + if (Creature* creature = object->ToCreature()) + { + if (creature->GetEntry() == NPC_TAINTED_ELEMENTAL && !creature->IsAlive()) + { + taintedPresent = true; + break; + } + } + } + } + + if (!taintedPresent) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + return (GetDesignatedCoreLooter(group, botAI) == bot && + !bot->HasItemCount(ITEM_TAINTED_CORE, 1, false)); +} + +bool LadyVashjTaintedCoreWasLootedTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "lady vashj") || !IsLadyVashjInPhase2(botAI)) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); + Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); + Player* secondCorePasser = GetSecondTaintedCorePasser(group, botAI); + Player* thirdCorePasser = GetThirdTaintedCorePasser(group, botAI); + Player* fourthCorePasser = GetFourthTaintedCorePasser(group, botAI); + + auto hasCore = [](Player* player) -> bool + { + return player && player->HasItemCount(ITEM_TAINTED_CORE, 1, false); + }; + + if (bot == designatedLooter) + { + if (!hasCore(bot)) + return false; + } + else if (bot == firstCorePasser) + { + if (hasCore(secondCorePasser) || hasCore(thirdCorePasser) || + hasCore(fourthCorePasser)) + return false; + } + else if (bot == secondCorePasser) + { + if (hasCore(thirdCorePasser) || hasCore(fourthCorePasser)) + return false; + } + else if (bot == thirdCorePasser) + { + if (hasCore(fourthCorePasser)) + return false; + } + else if (bot != fourthCorePasser) + return false; + + if (AnyRecentCoreInInventory(group, botAI)) + return true; + + // First and second passers move to positions as soon as the elemental appears + if (AI_VALUE2(Unit*, "find target", "tainted elemental") && + (bot == firstCorePasser || bot == secondCorePasser)) + return true; + + return false; +} + +bool LadyVashjTaintedCoreIsUnusableTrigger::IsActive() +{ + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + return false; + + if (!IsLadyVashjInPhase2(botAI)) + return bot->HasItemCount(ITEM_TAINTED_CORE, 1, false); + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* coreHandlers[] = + { + GetDesignatedCoreLooter(group, botAI), + GetFirstTaintedCorePasser(group, botAI), + GetSecondTaintedCorePasser(group, botAI), + GetThirdTaintedCorePasser(group, botAI), + GetFourthTaintedCorePasser(group, botAI) + }; + + if (bot->HasItemCount(ITEM_TAINTED_CORE, 1, false)) + { + for (Player* coreHandler : coreHandlers) + { + if (coreHandler && bot == coreHandler) + return false; + } + return true; + } + + return false; +} + +bool LadyVashjNeedToResetCorePassingTrackersTrigger::IsActive() +{ + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj || IsLadyVashjInPhase2(botAI)) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + return IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr) || + GetDesignatedCoreLooter(group, botAI) == bot || + GetFirstTaintedCorePasser(group, botAI) == bot || + GetSecondTaintedCorePasser(group, botAI) == bot || + GetThirdTaintedCorePasser(group, botAI) == bot || + GetFourthTaintedCorePasser(group, botAI) == bot; +} + +bool LadyVashjToxicSporebatsAreSpewingPoisonCloudsTrigger::IsActive() +{ + return IsLadyVashjInPhase3(botAI); +} + +bool LadyVashjBotIsEntangledInToxicSporesOrStaticChargeTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + return false; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->HasAura(SPELL_ENTANGLE)) + continue; + + if (botAI->IsMelee(member)) + return true; + } + } + + return false; +} diff --git a/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.h b/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.h new file mode 100644 index 000000000..e106b58f3 --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.h @@ -0,0 +1,414 @@ +#ifndef _PLAYERBOT_RAIDSSCTRIGGERS_H +#define _PLAYERBOT_RAIDSSCTRIGGERS_H + +#include "Trigger.h" + +// General + +class SerpentShrineCavernBotIsNotInCombatTrigger : public Trigger +{ +public: + SerpentShrineCavernBotIsNotInCombatTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "serpent shrine cavern bot is not in combat") {} + bool IsActive() override; +}; + +// Trash + +class UnderbogColossusSpawnedToxicPoolAfterDeathTrigger : public Trigger +{ +public: + UnderbogColossusSpawnedToxicPoolAfterDeathTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "underbog colossus spawned toxic pool after death") {} + bool IsActive() override; +}; + +class GreyheartTidecallerWaterElementalTotemSpawnedTrigger : public Trigger +{ +public: + GreyheartTidecallerWaterElementalTotemSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "greyheart tidecaller water elemental totem spawned") {} + bool IsActive() override; +}; + +// Hydross the Unstable + +class HydrossTheUnstableBotIsFrostTankTrigger : public Trigger +{ +public: + HydrossTheUnstableBotIsFrostTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hydross the unstable bot is frost tank") {} + bool IsActive() override; +}; + +class HydrossTheUnstableBotIsNatureTankTrigger : public Trigger +{ +public: + HydrossTheUnstableBotIsNatureTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hydross the unstable bot is nature tank") {} + bool IsActive() override; +}; + +class HydrossTheUnstableElementalsSpawnedTrigger : public Trigger +{ +public: + HydrossTheUnstableElementalsSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hydross the unstable elementals spawned") {} + bool IsActive() override; +}; + +class HydrossTheUnstableDangerFromWaterTombsTrigger : public Trigger +{ +public: + HydrossTheUnstableDangerFromWaterTombsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hydross the unstable danger from water tombs") {} + bool IsActive() override; +}; + +class HydrossTheUnstableTankNeedsAggroUponPhaseChangeTrigger : public Trigger +{ +public: + HydrossTheUnstableTankNeedsAggroUponPhaseChangeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hydross the unstable tank needs aggro upon phase change") {} + bool IsActive() override; +}; + +class HydrossTheUnstableAggroResetsUponPhaseChangeTrigger : public Trigger +{ +public: + HydrossTheUnstableAggroResetsUponPhaseChangeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hydross the unstable aggro resets upon phase change") {} + bool IsActive() override; +}; + +class HydrossTheUnstableNeedToManageTimersTrigger : public Trigger +{ +public: + HydrossTheUnstableNeedToManageTimersTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hydross the unstable need to manage timers") {} + bool IsActive() override; +}; + +// The Lurker Below + +class TheLurkerBelowSpoutIsActiveTrigger : public Trigger +{ +public: + TheLurkerBelowSpoutIsActiveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "the lurker below spout is active") {} + bool IsActive() override; +}; + +class TheLurkerBelowBossIsActiveForMainTankTrigger : public Trigger +{ +public: + TheLurkerBelowBossIsActiveForMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "the lurker below boss is active for main tank") {} + bool IsActive() override; +}; + +class TheLurkerBelowBossCastsGeyserTrigger : public Trigger +{ +public: + TheLurkerBelowBossCastsGeyserTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "the lurker below boss casts geyser") {} + bool IsActive() override; +}; + +class TheLurkerBelowBossIsSubmergedTrigger : public Trigger +{ +public: + TheLurkerBelowBossIsSubmergedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "the lurker below boss is submerged") {} + bool IsActive() override; +}; + +class TheLurkerBelowNeedToPrepareTimerForSpoutTrigger : public Trigger +{ +public: + TheLurkerBelowNeedToPrepareTimerForSpoutTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "the lurker below need to prepare timer for spout") {} + bool IsActive() override; +}; + +// Leotheras the Blind + +class LeotherasTheBlindBossIsInactiveTrigger : public Trigger +{ +public: + LeotherasTheBlindBossIsInactiveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "leotheras the blind boss is inactive") {} + bool IsActive() override; +}; + +class LeotherasTheBlindOnlyWarlockShouldTankDemonFormTrigger : public Trigger +{ +public: + LeotherasTheBlindOnlyWarlockShouldTankDemonFormTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "leotheras the blind only warlock should tank demon form") {} + bool IsActive() override; +}; + +class LeotherasTheBlindBossTransformedIntoDemonFormTrigger : public Trigger +{ +public: + LeotherasTheBlindBossTransformedIntoDemonFormTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "leotheras the blind boss transformed into demon form") {} + bool IsActive() override; +}; + +class LeotherasTheBlindBossEngagedByRangedTrigger : public Trigger +{ +public: + LeotherasTheBlindBossEngagedByRangedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "leotheras the blind boss engaged by ranged") {} + bool IsActive() override; +}; + +class LeotherasTheBlindBossChannelingWhirlwindTrigger : public Trigger +{ +public: + LeotherasTheBlindBossChannelingWhirlwindTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "leotheras the blind boss channeling whirlwind") {} + bool IsActive() override; +}; + +class LeotherasTheBlindBotHasTooManyChaosBlastStacksTrigger : public Trigger +{ +public: + LeotherasTheBlindBotHasTooManyChaosBlastStacksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "leotheras the blind bot has too many chaos blast stacks") {} + bool IsActive() override; +}; + +class LeotherasTheBlindInnerDemonHasAwakenedTrigger : public Trigger +{ +public: + LeotherasTheBlindInnerDemonHasAwakenedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "leotheras the blind inner demon has awakened") {} + bool IsActive() override; +}; + +class LeotherasTheBlindEnteredFinalPhaseTrigger : public Trigger +{ +public: + LeotherasTheBlindEnteredFinalPhaseTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "leotheras the blind entered final phase") {} + bool IsActive() override; +}; + +class LeotherasTheBlindDemonFormTankNeedsAggro : public Trigger +{ +public: + LeotherasTheBlindDemonFormTankNeedsAggro( + PlayerbotAI* botAI) : Trigger(botAI, "leotheras the blind demon form tank needs aggro") {} + bool IsActive() override; +}; + +class LeotherasTheBlindBossWipesAggroUponPhaseChangeTrigger : public Trigger +{ +public: + LeotherasTheBlindBossWipesAggroUponPhaseChangeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "leotheras the blind boss wipes aggro upon phase change") {} + bool IsActive() override; +}; + +// Fathom-Lord Karathress + +class FathomLordKarathressBossEngagedByMainTankTrigger : public Trigger +{ +public: + FathomLordKarathressBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "fathom-lord karathress boss engaged by main tank") {} + bool IsActive() override; +}; + +class FathomLordKarathressCaribdisEngagedByFirstAssistTankTrigger : public Trigger +{ +public: + FathomLordKarathressCaribdisEngagedByFirstAssistTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "fathom-lord karathress caribdis engaged by first assist tank") {} + bool IsActive() override; +}; + +class FathomLordKarathressSharkkisEngagedBySecondAssistTankTrigger : public Trigger +{ +public: + FathomLordKarathressSharkkisEngagedBySecondAssistTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "fathom-lord karathress sharkkis engaged by second assist tank") {} + bool IsActive() override; +}; + +class FathomLordKarathressTidalvessEngagedByThirdAssistTankTrigger : public Trigger +{ +public: + FathomLordKarathressTidalvessEngagedByThirdAssistTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "fathom-lord karathress tidalvess engaged by third assist tank") {} + bool IsActive() override; +}; + +class FathomLordKarathressCaribdisTankNeedsDedicatedHealerTrigger : public Trigger +{ +public: + FathomLordKarathressCaribdisTankNeedsDedicatedHealerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "fathom-lord karathress caribdis tank needs dedicated healer") {} + bool IsActive() override; +}; + +class FathomLordKarathressPullingBossesTrigger : public Trigger +{ +public: + FathomLordKarathressPullingBossesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "fathom-lord karathress pulling bosses") {} + bool IsActive() override; +}; + +class FathomLordKarathressDeterminingKillOrderTrigger : public Trigger +{ +public: + FathomLordKarathressDeterminingKillOrderTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "fathom-lord karathress determining kill order") {} + bool IsActive() override; +}; + +class FathomLordKarathressTanksNeedToEstablishAggroTrigger : public Trigger +{ +public: + FathomLordKarathressTanksNeedToEstablishAggroTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "fathom-lord karathress tanks need to establish aggro") {} + bool IsActive() override; +}; + +// Morogrim Tidewalker + +class MorogrimTidewalkerPullingBossTrigger : public Trigger +{ +public: + MorogrimTidewalkerPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "morogrim tidewalker pulling boss") {} + bool IsActive() override; +}; + +class MorogrimTidewalkerBossEngagedByMainTankTrigger : public Trigger +{ +public: + MorogrimTidewalkerBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "morogrim tidewalker boss engaged by main tank") {} + bool IsActive() override; +}; + +class MorogrimTidewalkerWaterGlobulesAreIncomingTrigger : public Trigger +{ +public: + MorogrimTidewalkerWaterGlobulesAreIncomingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "morogrim tidewalker water globules are incoming") {} + bool IsActive() override; +}; + +// Lady Vashj + +class LadyVashjBossEngagedByMainTankTrigger : public Trigger +{ +public: + LadyVashjBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj boss engaged by main tank") {} + bool IsActive() override; +}; + +class LadyVashjBossEngagedByRangedInPhase1Trigger : public Trigger +{ +public: + LadyVashjBossEngagedByRangedInPhase1Trigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj boss engaged by ranged in phase 1") {} + bool IsActive() override; +}; + +class LadyVashjCastsShockBlastOnHighestAggroTrigger : public Trigger +{ +public: + LadyVashjCastsShockBlastOnHighestAggroTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj casts shock blast on highest aggro") {} + bool IsActive() override; +}; + +class LadyVashjBotHasStaticChargeTrigger : public Trigger +{ +public: + LadyVashjBotHasStaticChargeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj bot has static charge") {} + bool IsActive() override; +}; + +class LadyVashjPullingBossInPhase1AndPhase3Trigger : public Trigger +{ +public: + LadyVashjPullingBossInPhase1AndPhase3Trigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj pulling boss in phase 1 and phase 3") {} + bool IsActive() override; +}; + +class LadyVashjAddsSpawnInPhase2AndPhase3Trigger : public Trigger +{ +public: + LadyVashjAddsSpawnInPhase2AndPhase3Trigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj adds spawn in phase 2 and phase 3") {} + bool IsActive() override; +}; + +class LadyVashjCoilfangStriderIsApproachingTrigger : public Trigger +{ +public: + LadyVashjCoilfangStriderIsApproachingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj coilfang strider is approaching") {} + bool IsActive() override; +}; + +class LadyVashjTaintedElementalCheatTrigger : public Trigger +{ +public: + LadyVashjTaintedElementalCheatTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj tainted elemental cheat") {} + bool IsActive() override; +}; + +class LadyVashjTaintedCoreWasLootedTrigger : public Trigger +{ +public: + LadyVashjTaintedCoreWasLootedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj tainted core was looted") {} + bool IsActive() override; +}; + +class LadyVashjTaintedCoreIsUnusableTrigger : public Trigger +{ +public: + LadyVashjTaintedCoreIsUnusableTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj tainted core is unusable") {} + bool IsActive() override; +}; + +class LadyVashjNeedToResetCorePassingTrackersTrigger : public Trigger +{ +public: + LadyVashjNeedToResetCorePassingTrackersTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj need to reset core passing trackers") {} + bool IsActive() override; +}; + +class LadyVashjToxicSporebatsAreSpewingPoisonCloudsTrigger : public Trigger +{ +public: + LadyVashjToxicSporebatsAreSpewingPoisonCloudsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj toxic sporebats are spewing poison clouds") {} + bool IsActive() override; +}; + +class LadyVashjBotIsEntangledInToxicSporesOrStaticChargeTrigger : public Trigger +{ +public: + LadyVashjBotIsEntangledInToxicSporesOrStaticChargeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj bot is entangled in toxic spores or static charge") {} + bool IsActive() override; +}; + +#endif diff --git a/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.cpp b/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.cpp new file mode 100644 index 000000000..7bda085be --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.cpp @@ -0,0 +1,583 @@ +#include "RaidSSCHelpers.h" +#include "AiFactory.h" +#include "Creature.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +namespace SerpentShrineCavernHelpers +{ + // Hydross the Unstable + + const Position HYDROSS_FROST_TANK_POSITION = { -236.669f, -358.352f, -0.828f }; + const Position HYDROSS_NATURE_TANK_POSITION = { -225.471f, -327.790f, -3.682f }; + + std::unordered_map hydrossFrostDpsWaitTimer; + std::unordered_map hydrossNatureDpsWaitTimer; + std::unordered_map hydrossChangeToFrostPhaseTimer; + std::unordered_map hydrossChangeToNaturePhaseTimer; + + bool HasMarkOfHydrossAt100Percent(Player* bot) + { + return bot->HasAura(SPELL_MARK_OF_HYDROSS_100) || + bot->HasAura(SPELL_MARK_OF_HYDROSS_250) || + bot->HasAura(SPELL_MARK_OF_HYDROSS_500); + } + + bool HasNoMarkOfHydross(Player* bot) + { + return !bot->HasAura(SPELL_MARK_OF_HYDROSS_10) && + !bot->HasAura(SPELL_MARK_OF_HYDROSS_25) && + !bot->HasAura(SPELL_MARK_OF_HYDROSS_50) && + !bot->HasAura(SPELL_MARK_OF_HYDROSS_100) && + !bot->HasAura(SPELL_MARK_OF_HYDROSS_250) && + !bot->HasAura(SPELL_MARK_OF_HYDROSS_500); + } + + bool HasMarkOfCorruptionAt100Percent(Player* bot) + { + return bot->HasAura(SPELL_MARK_OF_CORRUPTION_100) || + bot->HasAura(SPELL_MARK_OF_CORRUPTION_250) || + bot->HasAura(SPELL_MARK_OF_CORRUPTION_500); + } + + bool HasNoMarkOfCorruption(Player* bot) + { + return !bot->HasAura(SPELL_MARK_OF_CORRUPTION_10) && + !bot->HasAura(SPELL_MARK_OF_CORRUPTION_25) && + !bot->HasAura(SPELL_MARK_OF_CORRUPTION_50) && + !bot->HasAura(SPELL_MARK_OF_CORRUPTION_100) && + !bot->HasAura(SPELL_MARK_OF_CORRUPTION_250) && + !bot->HasAura(SPELL_MARK_OF_CORRUPTION_500); + } + + // The Lurker Below + + const Position LURKER_MAIN_TANK_POSITION = { 23.706f, -406.038f, -19.686f }; + + std::unordered_map lurkerSpoutTimer; + std::unordered_map lurkerRangedPositions; + + bool IsLurkerCastingSpout(Unit* lurker) + { + if (!lurker || !lurker->HasUnitState(UNIT_STATE_CASTING)) + return false; + + Spell* currentSpell = lurker->GetCurrentSpell(CURRENT_GENERIC_SPELL); + if (!currentSpell) + return false; + + uint32 spellId = currentSpell->m_spellInfo->Id; + bool isSpout = spellId == SPELL_SPOUT_VISUAL; + + return isSpout; + } + + // Leotheras the Blind + + std::unordered_map leotherasHumanFormDpsWaitTimer; + std::unordered_map leotherasDemonFormDpsWaitTimer; + std::unordered_map leotherasFinalPhaseDpsWaitTimer; + + Unit* GetLeotherasHuman(PlayerbotAI* botAI) + { + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->GetEntry() == NPC_LEOTHERAS_THE_BLIND && + unit->IsInCombat() && !unit->HasAura(SPELL_METAMORPHOSIS)) + return unit; + } + return nullptr; + } + + Unit* GetPhase2LeotherasDemon(PlayerbotAI* botAI) + { + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->GetEntry() == NPC_LEOTHERAS_THE_BLIND && + unit->HasAura(SPELL_METAMORPHOSIS)) + return unit; + } + return nullptr; + } + + Unit* GetPhase3LeotherasDemon(PlayerbotAI* botAI) + { + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->GetEntry() == NPC_SHADOW_OF_LEOTHERAS) + return unit; + } + return nullptr; + } + + Unit* GetActiveLeotherasDemon(PlayerbotAI* botAI) + { + Unit* phase2 = GetPhase2LeotherasDemon(botAI); + Unit* phase3 = GetPhase3LeotherasDemon(botAI); + return phase2 ? phase2 : phase3; + } + + Player* GetLeotherasDemonFormTank(Player* bot) + { + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + // (1) First loop: Return the first assistant Warlock (real player or bot) + 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; + } + + // (2) Fall back to first found bot Warlock + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || + member->getClass() != CLASS_WARLOCK) + continue; + + return member; + } + + // (3) Return nullptr if none found + return nullptr; + } + + // Fathom-Lord Karathress + + const Position KARATHRESS_TANK_POSITION = { 474.403f, -531.118f, -7.548f }; + const Position TIDALVESS_TANK_POSITION = { 511.282f, -501.162f, -13.158f }; + const Position SHARKKIS_TANK_POSITION = { 508.057f, -541.109f, -10.133f }; + const Position CARIBDIS_TANK_POSITION = { 464.462f, -475.820f, -13.158f }; + const Position CARIBDIS_HEALER_POSITION = { 466.203f, -503.201f, -13.158f }; + const Position CARIBDIS_RANGED_DPS_POSITION = { 463.197f, -501.190f, -13.158f }; + + std::unordered_map karathressDpsWaitTimer; + + // Morogrim Tidewalker + + const Position TIDEWALKER_PHASE_1_TANK_POSITION = { 410.925f, -741.916f, -7.146f }; + const Position TIDEWALKER_PHASE_TRANSITION_WAYPOINT = { 407.035f, -759.479f, -7.168f }; + const Position TIDEWALKER_PHASE_2_TANK_POSITION = { 446.571f, -767.155f, -7.144f }; + const Position TIDEWALKER_PHASE_2_RANGED_POSITION = { 432.595f, -766.288f, -7.145f }; + + std::unordered_map tidewalkerTankStep; + std::unordered_map tidewalkerRangedStep; + + // Lady Vashj + + const Position VASHJ_PLATFORM_CENTER_POSITION = { 29.634f, -923.541f, 42.985f }; + + std::unordered_map vashjRangedPositions; + std::unordered_map hasReachedVashjRangedPosition; + std::unordered_map nearestTriggerGuid; + std::unordered_map intendedLineup; + std::unordered_map lastImbueAttempt; + std::unordered_map lastCoreInInventoryTime; + + bool IsMainTankInSameSubgroup(Player* bot) + { + Group* group = bot->GetGroup(); + if (!group || !group->isRaidGroup()) + return false; + + uint8 botSubGroup = group->GetMemberGroup(bot->GetGUID()); + if (botSubGroup >= MAX_RAID_SUBGROUPS) + return false; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot || !member->IsAlive()) + continue; + + if (group->GetMemberGroup(member->GetGUID()) != botSubGroup) + continue; + + if (PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member)) + { + if (memberAI->IsMainTank(member)) + return true; + } + } + + return false; + } + + bool IsLadyVashjInPhase1(PlayerbotAI* botAI) + { + Unit* vashj = + botAI->GetAiObjectContext()->GetValue("find target", "lady vashj")->Get(); + if (!vashj) + return false; + + Creature* vashjCreature = vashj->ToCreature(); + return vashjCreature && vashjCreature->GetHealthPct() > 70.0f && + vashjCreature->GetReactState() != REACT_PASSIVE; + } + + bool IsLadyVashjInPhase2(PlayerbotAI* botAI) + { + Unit* vashj = + botAI->GetAiObjectContext()->GetValue("find target", "lady vashj")->Get(); + if (!vashj) + return false; + + Creature* vashjCreature = vashj->ToCreature(); + return vashjCreature && vashjCreature->GetReactState() == REACT_PASSIVE; + } + + bool IsLadyVashjInPhase3(PlayerbotAI* botAI) + { + Unit* vashj = + botAI->GetAiObjectContext()->GetValue("find target", "lady vashj")->Get(); + if (!vashj) + return false; + + Creature* vashjCreature = vashj->ToCreature(); + return vashjCreature && vashjCreature->GetHealthPct() <= 50.0f && + vashjCreature->GetReactState() != REACT_PASSIVE; + } + + bool IsValidLadyVashjCombatNpc(Unit* unit, PlayerbotAI* botAI) + { + if (!unit || !unit->IsAlive()) + return false; + + uint32 entry = unit->GetEntry(); + + if (IsLadyVashjInPhase2(botAI)) + { + return entry == NPC_TAINTED_ELEMENTAL || entry == NPC_ENCHANTED_ELEMENTAL || + entry == NPC_COILFANG_ELITE || entry == NPC_COILFANG_STRIDER; + } + else if (IsLadyVashjInPhase3(botAI)) + { + return entry == NPC_TAINTED_ELEMENTAL || entry == NPC_ENCHANTED_ELEMENTAL || + entry == NPC_COILFANG_ELITE || entry == NPC_COILFANG_STRIDER || + entry == NPC_TOXIC_SPOREBAT || entry == NPC_LADY_VASHJ; + } + + return false; + } + + bool AnyRecentCoreInInventory(Group* group, PlayerbotAI* botAI, uint32 graceSeconds) + { + Unit* vashj = + botAI->GetAiObjectContext()->GetValue("find target", "lady vashj")->Get(); + if (!vashj) + return false; + + if (group) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->HasItemCount(ITEM_TAINTED_CORE, 1, false)) + return true; + } + } + + const uint32 instanceId = vashj->GetMap()->GetInstanceId(); + const time_t now = std::time(nullptr); + + auto it = lastCoreInInventoryTime.find(instanceId); + if (it != lastCoreInInventoryTime.end()) + { + if ((now - it->second) <= static_cast(graceSeconds)) + return true; + } + + return false; + } + + Player* GetDesignatedCoreLooter(Group* group, PlayerbotAI* botAI) + { + if (!group) + return nullptr; + + Player* leader = nullptr; + ObjectGuid leaderGuid = group->GetLeaderGUID(); + if (!leaderGuid.IsEmpty()) + leader = ObjectAccessor::FindPlayer(leaderGuid); + + if (!botAI->HasCheat(BotCheatMask::raid)) + return leader; + + Player* fallback = leader; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == leader) + continue; + + PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); + if (!memberAI) + continue; + + if (memberAI->IsMelee(member) && memberAI->IsDps(member)) + return member; + + if (!fallback && memberAI->IsRangedDps(member)) + fallback = member; + } + + return fallback ? fallback : leader; + } + + Player* GetFirstTaintedCorePasser(Group* group, PlayerbotAI* botAI) + { + if (!group) + return nullptr; + + Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == designatedLooter) + continue; + + PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); + if (!memberAI) + continue; + + if (memberAI->IsAssistHealOfIndex(member, 0, true)) + return member; + } + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || + botAI->IsTank(member) || member == designatedLooter) + continue; + return member; + } + + return nullptr; + } + + Player* GetSecondTaintedCorePasser(Group* group, PlayerbotAI* botAI) + { + if (!group) + return nullptr; + + Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); + Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == designatedLooter || + member == firstCorePasser) + continue; + + PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); + if (!memberAI) + continue; + + if (memberAI->IsAssistHealOfIndex(member, 1, true)) + return member; + } + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || + botAI->IsTank(member) || member == designatedLooter || + member == firstCorePasser) + continue; + return member; + } + + return nullptr; + } + + Player* GetThirdTaintedCorePasser(Group* group, PlayerbotAI* botAI) + { + if (!group) + return nullptr; + + Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); + Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); + Player* secondCorePasser = GetSecondTaintedCorePasser(group, botAI); + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == designatedLooter || + member == firstCorePasser || member == secondCorePasser) + continue; + + PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); + if (!memberAI) + continue; + + if (memberAI->IsAssistHealOfIndex(member, 2, true)) + return member; + } + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || + botAI->IsTank(member) || member == designatedLooter || + member == firstCorePasser || member == secondCorePasser) + continue; + return member; + } + + return nullptr; + } + + Player* GetFourthTaintedCorePasser(Group* group, PlayerbotAI* botAI) + { + if (!group) + return nullptr; + + Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); + Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); + Player* secondCorePasser = GetSecondTaintedCorePasser(group, botAI); + Player* thirdCorePasser = GetThirdTaintedCorePasser(group, botAI); + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == designatedLooter || + member == firstCorePasser || member == secondCorePasser || + member == thirdCorePasser) + continue; + + PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); + if (!memberAI) + continue; + + if (memberAI->IsAssistRangedDpsOfIndex(member, 0, true)) + return member; + } + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || + botAI->IsTank(member) || member == designatedLooter || + member == firstCorePasser || member == secondCorePasser || + member == thirdCorePasser) + continue; + return member; + } + + return nullptr; + } + + const std::vector SHIELD_GENERATOR_DB_GUIDS = + { + 47482, // NW + 47483, // NE + 47484, // SE + 47485 // SW + }; + + // Get the positions of all active Shield Generators by their database GUIDs + std::vector GetAllGeneratorInfosByDbGuids( + Map* map, const std::vector& generatorDbGuids) + { + std::vector generators; + if (!map) + return generators; + + for (uint32 dbGuid : generatorDbGuids) + { + auto bounds = map->GetGameObjectBySpawnIdStore().equal_range(dbGuid); + if (bounds.first == bounds.second) + continue; + + GameObject* go = bounds.first->second; + if (!go) + continue; + + if (go->GetGoState() != GO_STATE_READY) + continue; + + GeneratorInfo info; + info.guid = go->GetGUID(); + info.x = go->GetPositionX(); + info.y = go->GetPositionY(); + info.z = go->GetPositionZ(); + generators.push_back(info); + } + + return generators; + } + + // Returns the nearest active Shield Generator to the bot + // Active generators are powered by NPC_WORLD_INVISIBLE_TRIGGER creatures, + // which depawn after use + Unit* GetNearestActiveShieldGeneratorTriggerByEntry(Unit* reference) + { + if (!reference) + return nullptr; + + std::list triggers; + constexpr float searchRange = 150.0f; + reference->GetCreatureListWithEntryInGrid( + triggers, NPC_WORLD_INVISIBLE_TRIGGER, searchRange); + + Creature* nearest = nullptr; + float minDist = std::numeric_limits::max(); + + for (Creature* creature : triggers) + { + if (!creature->IsAlive()) + continue; + + float dist = reference->GetDistance(creature); + if (dist < minDist) + { + minDist = dist; + nearest = creature; + } + } + + return nearest; + } + + const GeneratorInfo* GetNearestGeneratorToBot( + Player* bot, const std::vector& generators) + { + if (!bot || generators.empty()) + return nullptr; + + const GeneratorInfo* nearest = nullptr; + float minDist = std::numeric_limits::max(); + + for (auto const& gen : generators) + { + float dist = bot->GetExactDist(gen.x, gen.y, gen.z); + if (dist < minDist) + { + minDist = dist; + nearest = &gen; + } + } + + return nearest; + } +} diff --git a/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.h b/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.h new file mode 100644 index 000000000..a725e28fc --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.h @@ -0,0 +1,189 @@ +#ifndef _PLAYERBOT_RAIDSSCHELPERS_H_ +#define _PLAYERBOT_RAIDSSCHELPERS_H_ + +#include +#include + +#include "AiObject.h" +#include "Position.h" +#include "Unit.h" + +namespace SerpentShrineCavernHelpers +{ + enum SerpentShrineCavernSpells + { + // Trash Mobs + SPELL_TOXIC_POOL = 38718, + + // Hydross the Unstable + SPELL_MARK_OF_HYDROSS_10 = 38215, + SPELL_MARK_OF_HYDROSS_25 = 38216, + SPELL_MARK_OF_HYDROSS_50 = 38217, + SPELL_MARK_OF_HYDROSS_100 = 38218, + SPELL_MARK_OF_HYDROSS_250 = 38231, + SPELL_MARK_OF_HYDROSS_500 = 40584, + SPELL_MARK_OF_CORRUPTION_10 = 38219, + SPELL_MARK_OF_CORRUPTION_25 = 38220, + SPELL_MARK_OF_CORRUPTION_50 = 38221, + SPELL_MARK_OF_CORRUPTION_100 = 38222, + SPELL_MARK_OF_CORRUPTION_250 = 38230, + SPELL_MARK_OF_CORRUPTION_500 = 40583, + SPELL_CORRUPTION = 37961, + + // The Lurker Below + SPELL_SPOUT_VISUAL = 37431, + + // Leotheras the Blind + SPELL_LEOTHERAS_BANISHED = 37546, + SPELL_WHIRLWIND = 37640, + SPELL_WHIRLWIND_CHANNEL = 37641, + SPELL_METAMORPHOSIS = 37673, + SPELL_CHAOS_BLAST = 37675, + SPELL_INSIDIOUS_WHISPER = 37676, + + // Lady Vashj + SPELL_FEAR_WARD = 6346, + SPELL_POISON_BOLT = 38253, + SPELL_STATIC_CHARGE = 38280, + SPELL_ENTANGLE = 38316, + + // Druid + SPELL_CAT_FORM = 768, + SPELL_BEAR_FORM = 5487, + SPELL_DIRE_BEAR_FORM = 9634, + SPELL_TREE_OF_LIFE = 33891, + + // Hunter + SPELL_MISDIRECTION = 35079, + + // Mage + SPELL_SLOW = 31589, + + // Shaman + SPELL_GROUNDING_TOTEM_EFFECT = 8178, + + // Warlock + SPELL_CURSE_OF_EXHAUSTION = 18223, + + // Item + SPELL_HEAVY_NETHERWEAVE_NET = 31368, + }; + + enum SerpentShrineCavernNPCs + { + // Trash Mobs + NPC_WATER_ELEMENTAL_TOTEM = 22236, + + // Hydross the Unstable + NPC_PURE_SPAWN_OF_HYDROSS = 22035, + NPC_TAINTED_SPAWN_OF_HYDROSS = 22036, + + // The Lurker Below + NPC_COILFANG_GUARDIAN = 21873, + + // Leotheras the Blind + NPC_LEOTHERAS_THE_BLIND = 21215, + NPC_GREYHEART_SPELLBINDER = 21806, + NPC_INNER_DEMON = 21857, + NPC_SHADOW_OF_LEOTHERAS = 21875, + + // Fathom-Lord Karathress + NPC_SPITFIRE_TOTEM = 22091, + + // Lady Vashj + NPC_WORLD_INVISIBLE_TRIGGER = 12999, + NPC_LADY_VASHJ = 21212, + NPC_ENCHANTED_ELEMENTAL = 21958, + NPC_TAINTED_ELEMENTAL = 22009, + NPC_COILFANG_ELITE = 22055, + NPC_COILFANG_STRIDER = 22056, + NPC_TOXIC_SPOREBAT = 22140, + NPC_SPORE_DROP_TRIGGER = 22207, + }; + + enum SerpentShrineCavernItems + { + // Lady Vashj + ITEM_TAINTED_CORE = 31088, + + // Tailoring + ITEM_HEAVY_NETHERWEAVE_NET = 24269, + }; + + constexpr uint32 SSC_MAP_ID = 548; + + // Hydross the Unstable + extern const Position HYDROSS_FROST_TANK_POSITION; + extern const Position HYDROSS_NATURE_TANK_POSITION; + extern std::unordered_map hydrossFrostDpsWaitTimer; + extern std::unordered_map hydrossNatureDpsWaitTimer; + extern std::unordered_map hydrossChangeToFrostPhaseTimer; + extern std::unordered_map hydrossChangeToNaturePhaseTimer; + bool HasMarkOfHydrossAt100Percent(Player* bot); + bool HasNoMarkOfHydross(Player* bot); + bool HasMarkOfCorruptionAt100Percent(Player* bot); + bool HasNoMarkOfCorruption(Player* bot); + + // The Lurker Below + extern const Position LURKER_MAIN_TANK_POSITION; + extern std::unordered_map lurkerSpoutTimer; + extern std::unordered_map lurkerRangedPositions; + bool IsLurkerCastingSpout(Unit* lurker); + + // Leotheras the Blind + extern std::unordered_map leotherasHumanFormDpsWaitTimer; + extern std::unordered_map leotherasDemonFormDpsWaitTimer; + extern std::unordered_map leotherasFinalPhaseDpsWaitTimer; + Unit* GetLeotherasHuman(PlayerbotAI* botAI); + Unit* GetPhase2LeotherasDemon(PlayerbotAI* botAI); + Unit* GetPhase3LeotherasDemon(PlayerbotAI* botAI); + Unit* GetActiveLeotherasDemon(PlayerbotAI* botAI); + Player* GetLeotherasDemonFormTank(Player* bot); + + // Fathom-Lord Karathress + extern const Position KARATHRESS_TANK_POSITION; + extern const Position TIDALVESS_TANK_POSITION; + extern const Position SHARKKIS_TANK_POSITION; + extern const Position CARIBDIS_TANK_POSITION; + extern const Position CARIBDIS_HEALER_POSITION; + extern const Position CARIBDIS_RANGED_DPS_POSITION; + extern std::unordered_map karathressDpsWaitTimer; + + // Morogrim Tidewalker + extern const Position TIDEWALKER_PHASE_1_TANK_POSITION; + extern const Position TIDEWALKER_PHASE_TRANSITION_WAYPOINT; + extern const Position TIDEWALKER_PHASE_2_TANK_POSITION; + extern const Position TIDEWALKER_PHASE_2_RANGED_POSITION; + extern std::unordered_map tidewalkerTankStep; + extern std::unordered_map tidewalkerRangedStep; + + // Lady Vashj + constexpr float VASHJ_PLATFORM_Z = 42.985f; + extern const Position VASHJ_PLATFORM_CENTER_POSITION; + extern std::unordered_map vashjRangedPositions; + extern std::unordered_map hasReachedVashjRangedPosition; + extern std::unordered_map nearestTriggerGuid; + extern std::unordered_map intendedLineup; + extern std::unordered_map lastImbueAttempt; + extern std::unordered_map lastCoreInInventoryTime; + bool IsMainTankInSameSubgroup(Player* bot); + bool IsLadyVashjInPhase1(PlayerbotAI* botAI); + bool IsLadyVashjInPhase2(PlayerbotAI* botAI); + bool IsLadyVashjInPhase3(PlayerbotAI* botAI); + bool IsValidLadyVashjCombatNpc(Unit* unit, PlayerbotAI* botAI); + bool AnyRecentCoreInInventory(Group* group, PlayerbotAI* botAI, uint32 graceSeconds = 3); + Player* GetDesignatedCoreLooter(Group* group, PlayerbotAI* botAI); + Player* GetFirstTaintedCorePasser(Group* group, PlayerbotAI* botAI); + Player* GetSecondTaintedCorePasser(Group* group, PlayerbotAI* botAI); + Player* GetThirdTaintedCorePasser(Group* group, PlayerbotAI* botAI); + Player* GetFourthTaintedCorePasser(Group* group, PlayerbotAI* botAI); + struct GeneratorInfo { ObjectGuid guid; float x, y, z; }; + extern const std::vector SHIELD_GENERATOR_DB_GUIDS; + std::vector GetAllGeneratorInfosByDbGuids( + Map* map, const std::vector& generatorDbGuids); + Unit* GetNearestActiveShieldGeneratorTriggerByEntry(Unit* reference); + const GeneratorInfo* GetNearestGeneratorToBot( + Player* bot, const std::vector& generators); +} + +#endif diff --git a/src/Bot/Engine/AiObjectContext.cpp b/src/Bot/Engine/AiObjectContext.cpp index 250a39296..3a2037a08 100644 --- a/src/Bot/Engine/AiObjectContext.cpp +++ b/src/Bot/Engine/AiObjectContext.cpp @@ -41,6 +41,8 @@ #include "Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h" #include "Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h" #include "Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h" +#include "Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h" +#include "Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h" #include "Ai/Raid/EyeOfEternity/RaidEoEActionContext.h" #include "Ai/Raid/EyeOfEternity/RaidEoETriggerContext.h" #include "Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h" @@ -115,6 +117,7 @@ void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextList Date: Sun, 8 Feb 2026 03:41:33 -0800 Subject: [PATCH 08/11] Refactor of EquipActions (#1994) #PR Description The root cause of issue #1987 was the AI Value item usage becoming a very expensive call when bots gained professions accidentally. My original approach was to eliminate it entirely, but after inputs and testing I decided to introduce a more focused Ai value "Item upgrade" that only checks equipment and ammo inheriting directly from item usage, so the logic is unified between them. Upgrades are now only assessed when receiving an item that can be equipped. Additionally, I noticed that winning loot rolls did not trigger the upgrade action, so I added a new package handler for that. Performance needs to be re-evaluated, but I expect a reduction in calls and in the cost of each call. I tested with bots and selfbot in deadmines and ahadowfang keep. --------- Co-authored-by: bashermens <31279994+hermensbas@users.noreply.github.com> --- conf/playerbots.conf.dist | 2 +- src/Ai/Base/Actions/EquipAction.cpp | 98 ++++----- src/Ai/Base/Actions/EquipAction.h | 8 +- src/Ai/Base/ChatActionContext.h | 3 +- .../Strategy/WorldPacketHandlerStrategy.cpp | 1 + src/Ai/Base/Value/ItemUsageValue.cpp | 186 ++++++++++-------- src/Ai/Base/Value/ItemUsageValue.h | 22 ++- src/Ai/Base/ValueContext.h | 2 + src/Ai/Base/WorldPacketTriggerContext.h | 2 + src/Bot/PlayerbotAI.cpp | 1 + 10 files changed, 176 insertions(+), 149 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 18a6addf6..a2f06885c 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -2182,4 +2182,4 @@ AiPlayerbot.SummonAtInnkeepersEnabled = 1 # 30% more damage, 40% damage reduction (tank bots), increased all resistances, reduced threat for non tank bots, increased threat for tank bots. # Buffs will be applied on PP, Sindragosa and Lich King -AiPlayerbot.EnableICCBuffs = 1 +AiPlayerbot.EnableICCBuffs = 1 \ No newline at end of file diff --git a/src/Ai/Base/Actions/EquipAction.cpp b/src/Ai/Base/Actions/EquipAction.cpp index 32508ef2e..a8b262692 100644 --- a/src/Ai/Base/Actions/EquipAction.cpp +++ b/src/Ai/Base/Actions/EquipAction.cpp @@ -328,7 +328,43 @@ void EquipAction::EquipItem(Item* item) botAI->TellMaster(out); } -bool EquipUpgradesAction::Execute(Event event) +ItemIds EquipAction::SelectInventoryItemsToEquip() +{ + CollectItemsVisitor visitor; + IterateItems(&visitor, ITERATE_ITEMS_IN_BAGS); + + ItemIds items; + for (auto i = visitor.items.begin(); i != visitor.items.end(); ++i) + { + Item* item = *i; + if (!item) + continue; + + ItemTemplate const* itemTemplate = item->GetTemplate(); + if (!itemTemplate) + continue; + + //TODO Expand to Glyphs and Gems, that can be placed in equipment + //Pre-filter non-equipable items + if (itemTemplate->InventoryType == INVTYPE_NON_EQUIP) + continue; + + int32 randomProperty = item->GetItemRandomPropertyId(); + uint32 itemId = item->GetTemplate()->ItemId; + std::string itemUsageParam; + if (randomProperty != 0) + itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty); + else + itemUsageParam = std::to_string(itemId); + + ItemUsage usage = AI_VALUE2(ItemUsage, "item upgrade", itemUsageParam); + if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_BAD_EQUIP) + items.insert(itemId); + } + return items; +} + +bool EquipUpgradesTriggeredAction::Execute(Event event) { if (!sPlayerbotAIConfig.autoEquipUpgradeLoot && !sRandomPlayerbotMgr.IsRandomBot(bot)) return false; @@ -361,72 +397,18 @@ bool EquipUpgradesAction::Execute(Event event) p >> itemId; ItemTemplate const* item = sObjectMgr->GetItemTemplate(itemId); - if (item->Class == ITEM_CLASS_TRADE_GOODS && item->SubClass == ITEM_SUBCLASS_MEAT) + if (item->InventoryType == INVTYPE_NON_EQUIP) return false; } - CollectItemsVisitor visitor; - IterateItems(&visitor, ITERATE_ITEMS_IN_BAGS); - - ItemIds items; - for (auto i = visitor.items.begin(); i != visitor.items.end(); ++i) - { - Item* item = *i; - if (!item) - break; - int32 randomProperty = item->GetItemRandomPropertyId(); - uint32 itemId = item->GetTemplate()->ItemId; - std::string itemUsageParam; - if (randomProperty != 0) - { - itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty); - } - else - { - itemUsageParam = std::to_string(itemId); - } - ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemUsageParam); - - if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_BAD_EQUIP) - { - items.insert(itemId); - } - } - + ItemIds items = SelectInventoryItemsToEquip(); EquipItems(items); return true; } bool EquipUpgradeAction::Execute(Event event) { - CollectItemsVisitor visitor; - IterateItems(&visitor, ITERATE_ITEMS_IN_BAGS); - - ItemIds items; - for (auto i = visitor.items.begin(); i != visitor.items.end(); ++i) - { - Item* item = *i; - if (!item) - break; - int32 randomProperty = item->GetItemRandomPropertyId(); - uint32 itemId = item->GetTemplate()->ItemId; - std::string itemUsageParam; - if (randomProperty != 0) - { - itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty); - } - else - { - itemUsageParam = std::to_string(itemId); - } - ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemUsageParam); - - if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_BAD_EQUIP) - { - items.insert(itemId); - } - } - + ItemIds items = SelectInventoryItemsToEquip(); EquipItems(items); return true; } diff --git a/src/Ai/Base/Actions/EquipAction.h b/src/Ai/Base/Actions/EquipAction.h index 518d39c8e..4f84f942b 100644 --- a/src/Ai/Base/Actions/EquipAction.h +++ b/src/Ai/Base/Actions/EquipAction.h @@ -8,6 +8,7 @@ #include "ChatHelper.h" #include "InventoryAction.h" +#include "Item.h" class FindItemVisitor; class Item; @@ -20,6 +21,7 @@ public: bool Execute(Event event) override; void EquipItems(ItemIds ids); + ItemIds SelectInventoryItemsToEquip(); private: void EquipItem(FindItemVisitor* visitor); @@ -27,10 +29,10 @@ private: void EquipItem(Item* item); }; -class EquipUpgradesAction : public EquipAction +class EquipUpgradesTriggeredAction : public EquipAction { public: - EquipUpgradesAction(PlayerbotAI* botAI, std::string const name = "equip upgrades") : EquipAction(botAI, name) {} + explicit EquipUpgradesTriggeredAction(PlayerbotAI* botAI, std::string const name = "equip upgrades") : EquipAction(botAI, name) {} bool Execute(Event event) override; }; @@ -38,7 +40,7 @@ public: class EquipUpgradeAction : public EquipAction { public: - EquipUpgradeAction(PlayerbotAI* botAI, std::string const name = "equip upgrade") : EquipAction(botAI, name) {} + explicit EquipUpgradeAction(PlayerbotAI* botAI, std::string const name = "equip upgrade") : EquipAction(botAI, name) {} bool Execute(Event event) override; }; diff --git a/src/Ai/Base/ChatActionContext.h b/src/Ai/Base/ChatActionContext.h index 5e215e123..8e4ed9ccf 100644 --- a/src/Ai/Base/ChatActionContext.h +++ b/src/Ai/Base/ChatActionContext.h @@ -120,7 +120,7 @@ public: creators["use"] = &ChatActionContext::use; creators["item count"] = &ChatActionContext::item_count; creators["equip"] = &ChatActionContext::equip; - creators["equip upgrades"] = &ChatActionContext::equip_upgrades; + creators["equip upgrades"] = &ChatActionContext::equip_upgrade; creators["unequip"] = &ChatActionContext::unequip; creators["sell"] = &ChatActionContext::sell; creators["buy"] = &ChatActionContext::buy; @@ -258,7 +258,6 @@ private: static Action* talents(PlayerbotAI* botAI) { return new ChangeTalentsAction(botAI); } static Action* equip(PlayerbotAI* botAI) { return new EquipAction(botAI); } - static Action* equip_upgrades(PlayerbotAI* botAI) { return new EquipUpgradesAction(botAI); } static Action* unequip(PlayerbotAI* botAI) { return new UnequipAction(botAI); } static Action* sell(PlayerbotAI* botAI) { return new SellAction(botAI); } static Action* buy(PlayerbotAI* botAI) { return new BuyAction(botAI); } diff --git a/src/Ai/Base/Strategy/WorldPacketHandlerStrategy.cpp b/src/Ai/Base/Strategy/WorldPacketHandlerStrategy.cpp index cb37d2717..2debc44cb 100644 --- a/src/Ai/Base/Strategy/WorldPacketHandlerStrategy.cpp +++ b/src/Ai/Base/Strategy/WorldPacketHandlerStrategy.cpp @@ -42,6 +42,7 @@ void WorldPacketHandlerStrategy::InitTriggers(std::vector& trigger NextAction("query item usage", relevance), NextAction("equip upgrades", relevance) })); triggers.push_back(new TriggerNode("item push result", { NextAction("quest item push result", relevance) })); + triggers.push_back(new TriggerNode("loot roll won", { NextAction("equip upgrades", relevance) })); triggers.push_back(new TriggerNode("ready check finished", { NextAction("finish ready check", relevance) })); // triggers.push_back(new TriggerNode("often", { NextAction("security check", relevance), NextAction("check mail", relevance) })); triggers.push_back(new TriggerNode("guild invite", { NextAction("guild accept", relevance) })); diff --git a/src/Ai/Base/Value/ItemUsageValue.cpp b/src/Ai/Base/Value/ItemUsageValue.cpp index 6f48fa973..c6183e30f 100644 --- a/src/Ai/Base/Value/ItemUsageValue.cpp +++ b/src/Ai/Base/Value/ItemUsageValue.cpp @@ -19,19 +19,9 @@ ItemUsage ItemUsageValue::Calculate() { - uint32 itemId = 0; - uint32 randomPropertyId = 0; - size_t pos = qualifier.find(","); - if (pos != std::string::npos) - { - itemId = atoi(qualifier.substr(0, pos).c_str()); - randomPropertyId = atoi(qualifier.substr(pos + 1).c_str()); - } - else - { - itemId = atoi(qualifier.c_str()); - } - + ParsedItemUsage const parsed = GetItemIdFromQualifier(); + uint32 itemId = parsed.itemId; + uint32 randomPropertyId = parsed.randomPropertyId; if (!itemId) return ITEM_USAGE_NONE; @@ -142,96 +132,30 @@ ItemUsage ItemUsageValue::Calculate() // If the loot is from an item in the bot’s bags, ignore syncQuestWithPlayer if (isLootFromItem && botNeedsItemForQuest) - { return ITEM_USAGE_QUEST; - } // If the bot is NOT acting alone and the master needs this quest item, defer to the master if (!isSelfBot && masterNeedsItemForQuest) - { return ITEM_USAGE_NONE; - } // If the bot itself needs the item for a quest, allow looting if (botNeedsItemForQuest) - { return ITEM_USAGE_QUEST; - } if (proto->Class == ITEM_CLASS_PROJECTILE && bot->CanUseItem(proto) == EQUIP_ERR_OK) { - if (bot->getClass() == CLASS_HUNTER || bot->getClass() == CLASS_ROGUE || bot->getClass() == CLASS_WARRIOR) - { - Item* rangedWeapon = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED); - uint32 requiredSubClass = 0; - - if (rangedWeapon) - { - switch (rangedWeapon->GetTemplate()->SubClass) - { - case ITEM_SUBCLASS_WEAPON_GUN: - requiredSubClass = ITEM_SUBCLASS_BULLET; - break; - case ITEM_SUBCLASS_WEAPON_BOW: - case ITEM_SUBCLASS_WEAPON_CROSSBOW: - requiredSubClass = ITEM_SUBCLASS_ARROW; - break; - } - } - - // Ensure the item is the correct ammo type for the equipped ranged weapon - if (proto->SubClass == requiredSubClass) - { - float ammoCount = BetterStacks(proto, "ammo"); - float requiredAmmo = (bot->getClass() == CLASS_HUNTER) ? 8 : 2; // Hunters get 8 stacks, others 2 - uint32 currentAmmoId = bot->GetUInt32Value(PLAYER_AMMO_ID); - - // Check if the bot has an ammo type assigned - if (currentAmmoId == 0) - { - return ITEM_USAGE_EQUIP; // Equip the ammo if no ammo - } - // Compare new ammo vs current equipped ammo - ItemTemplate const* currentAmmoProto = sObjectMgr->GetItemTemplate(currentAmmoId); - if (currentAmmoProto) - { - uint32 currentAmmoDPS = (currentAmmoProto->Damage[0].DamageMin + currentAmmoProto->Damage[0].DamageMax) * 1000 / 2; - uint32 newAmmoDPS = (proto->Damage[0].DamageMin + proto->Damage[0].DamageMax) * 1000 / 2; - - if (newAmmoDPS > currentAmmoDPS) // New ammo meets upgrade condition - { - return ITEM_USAGE_EQUIP; - } - if (newAmmoDPS < currentAmmoDPS) // New ammo is worse - { - return ITEM_USAGE_NONE; - } - } - // Ensure we have enough ammo in the inventory - if (ammoCount < requiredAmmo) - { - ammoCount += CurrentStacks(proto); - - if (ammoCount < requiredAmmo) // Buy ammo to reach the proper supply - return ITEM_USAGE_AMMO; - else if (ammoCount < requiredAmmo + 1) - return ITEM_USAGE_KEEP; // Keep the ammo if we don't have too much. - } - } - } + ItemUsage ammoUsage = QueryItemUsageForAmmo(proto); + if (ammoUsage != ITEM_USAGE_NONE) + return ammoUsage; } - // Need to add something like free bagspace or item value. if (proto->SellPrice > 0) { if (proto->Quality >= ITEM_QUALITY_NORMAL && !isSoulbound) - { return ITEM_USAGE_AH; - } + else - { return ITEM_USAGE_VENDOR; - } } return ITEM_USAGE_NONE; @@ -480,6 +404,80 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto, return ITEM_USAGE_NONE; } +ItemUsage ItemUsageValue::QueryItemUsageForAmmo(ItemTemplate const* proto) +{ + if (bot->getClass() != CLASS_HUNTER || bot->getClass() != CLASS_ROGUE || bot->getClass() != CLASS_WARRIOR) + return ITEM_USAGE_NONE; + + Item* rangedWeapon = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED); + uint32 requiredSubClass = 0; + + if (rangedWeapon) + { + switch (rangedWeapon->GetTemplate()->SubClass) + { + case ITEM_SUBCLASS_WEAPON_GUN: + requiredSubClass = ITEM_SUBCLASS_BULLET; + break; + case ITEM_SUBCLASS_WEAPON_BOW: + case ITEM_SUBCLASS_WEAPON_CROSSBOW: + requiredSubClass = ITEM_SUBCLASS_ARROW; + break; + } + } + + // Ensure the item is the correct ammo type for the equipped ranged weapon + if (proto->SubClass == requiredSubClass) + { + float ammoCount = BetterStacks(proto, "ammo"); + float requiredAmmo = (bot->getClass() == CLASS_HUNTER) ? 8 : 2; // Hunters get 8 stacks, others 2 + uint32 currentAmmoId = bot->GetUInt32Value(PLAYER_AMMO_ID); + + // Check if the bot has an ammo type assigned + if (currentAmmoId == 0) + return ITEM_USAGE_EQUIP; // Equip the ammo if no ammo + // Compare new ammo vs current equipped ammo + ItemTemplate const* currentAmmoProto = sObjectMgr->GetItemTemplate(currentAmmoId); + if (currentAmmoProto) + { + uint32 currentAmmoDPS = (currentAmmoProto->Damage[0].DamageMin + currentAmmoProto->Damage[0].DamageMax) * 1000 / 2; + uint32 newAmmoDPS = (proto->Damage[0].DamageMin + proto->Damage[0].DamageMax) * 1000 / 2; + + if (newAmmoDPS > currentAmmoDPS) // New ammo meets upgrade condition + return ITEM_USAGE_EQUIP; + + if (newAmmoDPS < currentAmmoDPS) // New ammo is worse + return ITEM_USAGE_NONE; + } + // Ensure we have enough ammo in the inventory + if (ammoCount < requiredAmmo) + { + ammoCount += CurrentStacks(proto); + + if (ammoCount < requiredAmmo) // Buy ammo to reach the proper supply + return ITEM_USAGE_AMMO; + else if (ammoCount < requiredAmmo + 1) + return ITEM_USAGE_KEEP; // Keep the ammo if we don't have too much. + } + } + return ITEM_USAGE_NONE; +} + +ParsedItemUsage ItemUsageValue::GetItemIdFromQualifier() +{ + ParsedItemUsage parsed; + + size_t const pos = qualifier.find(","); + if (pos != std::string::npos) + { + parsed.itemId = atoi(qualifier.substr(0, pos).c_str()); + parsed.randomPropertyId = atoi(qualifier.substr(pos + 1).c_str()); + return parsed; + } + else + parsed.itemId = atoi(qualifier.c_str()); + return parsed; +} // Return smaltest bag size equipped uint32 ItemUsageValue::GetSmallestBagSize() { @@ -913,3 +911,25 @@ std::string const ItemUsageValue::GetConsumableType(ItemTemplate const* proto, b return ""; } + +ItemUsage ItemUpgradeValue::Calculate() +{ + ParsedItemUsage parsed = GetItemIdFromQualifier(); + uint32 itemId = parsed.itemId; + uint32 randomPropertyId = parsed.randomPropertyId; + if (!itemId) + return ITEM_USAGE_NONE; + + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); + if (!proto) + return ITEM_USAGE_NONE; + + ItemUsage equip = QueryItemUsageForEquip(proto, randomPropertyId); + if (equip != ITEM_USAGE_NONE) + return equip; + + if (proto->Class == ITEM_CLASS_PROJECTILE && bot->CanUseItem(proto) == EQUIP_ERR_OK) + return QueryItemUsageForAmmo(proto); + + return ITEM_USAGE_NONE; +} diff --git a/src/Ai/Base/Value/ItemUsageValue.h b/src/Ai/Base/Value/ItemUsageValue.h index d50ea1b06..b759bab6a 100644 --- a/src/Ai/Base/Value/ItemUsageValue.h +++ b/src/Ai/Base/Value/ItemUsageValue.h @@ -14,7 +14,11 @@ class Player; class PlayerbotAI; struct ItemTemplate; - +struct ParsedItemUsage +{ + uint32 itemId = 0; + int32 randomPropertyId = 0; +}; enum ItemUsage : uint32 { ITEM_USAGE_NONE = 0, @@ -42,8 +46,12 @@ public: ItemUsage Calculate() override; -private: +protected: ItemUsage QueryItemUsageForEquip(ItemTemplate const* proto, int32 randomPropertyId = 0); + ItemUsage QueryItemUsageForAmmo(ItemTemplate const* proto); + ParsedItemUsage GetItemIdFromQualifier(); + +private: uint32 GetSmallestBagSize(); bool IsItemUsefulForQuest(Player* player, ItemTemplate const* proto); bool IsItemNeededForSkill(ItemTemplate const* proto); @@ -61,4 +69,14 @@ public: static std::string const GetConsumableType(ItemTemplate const* proto, bool hasMana); }; +class ItemUpgradeValue : public ItemUsageValue +{ +public: + ItemUpgradeValue(PlayerbotAI* botAI, std::string const name = "item upgrade") : ItemUsageValue(botAI, name) + { + } + + ItemUsage Calculate() override; +}; + #endif diff --git a/src/Ai/Base/ValueContext.h b/src/Ai/Base/ValueContext.h index ab93bcea4..b67aee9bb 100644 --- a/src/Ai/Base/ValueContext.h +++ b/src/Ai/Base/ValueContext.h @@ -216,6 +216,7 @@ public: creators["formation"] = &ValueContext::formation; creators["stance"] = &ValueContext::stance; creators["item usage"] = &ValueContext::item_usage; + creators["item upgrade"] = &ValueContext::item_upgrade; creators["speed"] = &ValueContext::speed; creators["last said"] = &ValueContext::last_said; creators["last emote"] = &ValueContext::last_emote; @@ -341,6 +342,7 @@ private: static UntypedValue* already_seen_players(PlayerbotAI* botAI) { return new AlreadySeenPlayersValue(botAI); } static UntypedValue* new_player_nearby(PlayerbotAI* botAI) { return new NewPlayerNearbyValue(botAI); } static UntypedValue* item_usage(PlayerbotAI* botAI) { return new ItemUsageValue(botAI); } + static UntypedValue* item_upgrade(PlayerbotAI* botAI) { return new ItemUpgradeValue(botAI); } static UntypedValue* formation(PlayerbotAI* botAI) { return new FormationValue(botAI); } static UntypedValue* stance(PlayerbotAI* botAI) { return new StanceValue(botAI); } static UntypedValue* mana_save_level(PlayerbotAI* botAI) { return new ManaSaveLevelValue(botAI); } diff --git a/src/Ai/Base/WorldPacketTriggerContext.h b/src/Ai/Base/WorldPacketTriggerContext.h index 1e7309524..62afafd1a 100644 --- a/src/Ai/Base/WorldPacketTriggerContext.h +++ b/src/Ai/Base/WorldPacketTriggerContext.h @@ -46,6 +46,7 @@ public: creators["questgiver quest details"] = &WorldPacketTriggerContext::questgiver_quest_details; creators["item push result"] = &WorldPacketTriggerContext::item_push_result; + creators["loot roll won"] = &WorldPacketTriggerContext::loot_roll_won; creators["party command"] = &WorldPacketTriggerContext::party_command; creators["taxi done"] = &WorldPacketTriggerContext::taxi_done; creators["cast failed"] = &WorldPacketTriggerContext::cast_failed; @@ -92,6 +93,7 @@ private: static Trigger* taxi_done(PlayerbotAI* botAI) { return new WorldPacketTrigger(botAI, "taxi done"); } static Trigger* party_command(PlayerbotAI* botAI) { return new WorldPacketTrigger(botAI, "party command"); } static Trigger* item_push_result(PlayerbotAI* botAI) { return new WorldPacketTrigger(botAI, "item push result"); } + static Trigger* loot_roll_won(PlayerbotAI* botAI) { return new WorldPacketTrigger(botAI, "loot roll won"); } // quest static Trigger* quest_update_add_kill(PlayerbotAI* ai) { return new WorldPacketTrigger(ai, "quest update add kill"); } diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index f4be1d43e..cc91f3ce9 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -185,6 +185,7 @@ PlayerbotAI::PlayerbotAI(Player* bot) botOutgoingPacketHandlers.AddHandler(SMSG_TRADE_STATUS_EXTENDED, "trade status extended"); botOutgoingPacketHandlers.AddHandler(SMSG_LOOT_RESPONSE, "loot response"); botOutgoingPacketHandlers.AddHandler(SMSG_ITEM_PUSH_RESULT, "item push result"); + botOutgoingPacketHandlers.AddHandler(SMSG_LOOT_ROLL_WON, "loot roll won"); botOutgoingPacketHandlers.AddHandler(SMSG_PARTY_COMMAND_RESULT, "party command"); botOutgoingPacketHandlers.AddHandler(SMSG_LEVELUP_INFO, "levelup"); botOutgoingPacketHandlers.AddHandler(SMSG_LOG_XPGAIN, "xpgain"); From e9e79ad696c053e1c1b7fad0c5943ba9d99d5ad5 Mon Sep 17 00:00:00 2001 From: Hokken Date: Sun, 8 Feb 2026 11:45:03 +0000 Subject: [PATCH 09/11] Fix LootRollLevel=1 to match documented 'greed' behavior (#2068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes `AiPlayerbot.LootRollLevel = 1` to actually behave as "greed" mode per the config documentation. ## Problem The config documentation states: ```conf # Bots' loot roll level (0 = pass, 1 = greed, 2 = need) # Default: 1 (greed) AiPlayerbot.LootRollLevel = 1 ``` However, level 1 was converting **all GREED votes to PASS**, causing bots to pass on almost everything: | Item Type | AI Decision | Level 1 Behavior (Before) | Expected | |-----------|-------------|---------------------------|----------| | Gear upgrade | NEED | GREED ✓ | GREED | | Usable gear (not upgrade) | GREED | **PASS** ✗ | GREED | | Crafting materials | GREED | **PASS** ✗ | GREED | | Recipes, consumables | GREED | **PASS** ✗ | GREED | The only items bots would greed on were direct gear upgrades (originally NEED, downgraded to GREED). ## Root Cause In `LootRollAction.cpp`, lines 104-107 were converting GREED to PASS: ```cpp else if (vote == GREED) { vote = PASS; // This breaks "greed" mode } ``` ## Fix Remove the GREED→PASS conversion. Level 1 now only downgrades NEED to GREED (as intended), preserving GREED votes for useful items. ## Behavior After Fix | Level | Description | Behavior | |-------|-------------|----------| | 0 | Pass | Always pass on all items | | 1 | Greed | Greed on useful items, never need | | 2 | Need | Full AI logic (need/greed/pass) | ## Test Plan - [ ] Set `AiPlayerbot.LootRollLevel = 1` - [ ] Kill mobs that drop crafting materials, recipes, or non-upgrade gear - [ ] Verify bots greed on useful items instead of passing - [ ] Verify bots still pass on junk items - [ ] Verify bots never roll need (only greed) Co-authored-by: Hokken --- src/Ai/Base/Actions/LootRollAction.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Ai/Base/Actions/LootRollAction.cpp b/src/Ai/Base/Actions/LootRollAction.cpp index e600f1f20..9a21c8139 100644 --- a/src/Ai/Base/Actions/LootRollAction.cpp +++ b/src/Ai/Base/Actions/LootRollAction.cpp @@ -90,6 +90,8 @@ bool LootRollAction::Execute(Event event) } else if (sPlayerbotAIConfig.lootRollLevel == 1) { + // Level 1 = "greed" mode: bots greed on useful items but never need + // Only downgrade NEED to GREED, preserve GREED votes as-is if (vote == NEED) { if (RollUniqueCheck(proto, bot)) @@ -101,10 +103,6 @@ bool LootRollAction::Execute(Event event) vote = GREED; } } - else if (vote == GREED) - { - vote = PASS; - } } switch (group->GetLootMethod()) { From c9c936d5c1483d99af2b0967a8c68c88322874fb Mon Sep 17 00:00:00 2001 From: dillyns <49765217+dillyns@users.noreply.github.com> Date: Sun, 8 Feb 2026 12:36:27 -0500 Subject: [PATCH 10/11] Add Unending Breath to Warlock NonCombat Strat (#2074) # Pull Request Adds actions and triggers for Warlock class to cast Unending Breath when swimming, following the existing implementation for Shaman Water Breathing. --- ## Feature Evaluation Add triggers for Warlock noncombat strategy for Unending Breath on self and party. Triggers should only be active while swimming. Minimal runtime cost on Warlock bots trigger processing. --- ## How to Test the Changes - Bring a Warlock bot into water - It should cast Unending Breath on itself and anyone in the party ## Complexity & Impact - Does this change add new decision branches? - [ ] No - [x] Yes (**explain below**) It adds triggers to Warlock to decide when to cast Unending Breath on self or party members. - Does this change increase per-bot or per-tick processing? - [ ] No - [x] Yes (**describe and justify impact**) Minimal additional processing for Warlock triggers, same as already existing triggers for Shaman. - Could this logic scale poorly under load? - [x] No - [ ] Yes (**explain why**) --- ## Defaults & Configuration - Does this change modify default bot behavior? - [x] No - [ ] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - [ ] Lightweight mode remains the default - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance - Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - [ ] No - [x] Yes (**explain below**) Claude was used to explore the codebase to find similar implementations that already existed. --- ## Final Checklist - [x] Stability is not compromised - [x] Performance impact is understood, tested, and acceptable - [x] Added logic complexity is justified and explained - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. Co-authored-by: bashermens <31279994+hermensbas@users.noreply.github.com> --- src/Ai/Class/Warlock/Action/WarlockActions.h | 12 ++++++++++++ .../Strategy/GenericWarlockNonCombatStrategy.cpp | 2 ++ src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp | 10 ++++++++++ src/Ai/Class/Warlock/Trigger/WarlockTriggers.h | 14 ++++++++++++++ src/Ai/Class/Warlock/WarlockAiObjectContext.cpp | 8 ++++++++ 5 files changed, 46 insertions(+) diff --git a/src/Ai/Class/Warlock/Action/WarlockActions.h b/src/Ai/Class/Warlock/Action/WarlockActions.h index 787b518d6..09f346370 100644 --- a/src/Ai/Class/Warlock/Action/WarlockActions.h +++ b/src/Ai/Class/Warlock/Action/WarlockActions.h @@ -41,6 +41,18 @@ public: std::string const GetTargetName() override { return "pet target"; } }; +class CastUnendingBreathAction : public CastBuffSpellAction +{ +public: + CastUnendingBreathAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "unending breath") {} +}; + +class CastUnendingBreathOnPartyAction : public BuffOnPartyAction +{ +public: + CastUnendingBreathOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "unending breath") {} +}; + class CreateSoulShardAction : public Action { public: diff --git a/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.cpp b/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.cpp index a78ff0a98..d22abe3ca 100644 --- a/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.cpp +++ b/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.cpp @@ -85,6 +85,8 @@ void GenericWarlockNonCombatStrategy::InitTriggers(std::vector& tr triggers.push_back(new TriggerNode("too many soul shards", { NextAction("destroy soul shard", 60.0f) })); triggers.push_back(new TriggerNode("soul link", { NextAction("soul link", 28.0f) })); triggers.push_back(new TriggerNode("demon armor", { NextAction("fel armor", 27.0f) })); + triggers.push_back(new TriggerNode("unending breath", { NextAction("unending breath", 12.0f) })); + triggers.push_back(new TriggerNode("unending breath on party", { NextAction("unending breath on party", 11.0f) })); triggers.push_back(new TriggerNode("no healthstone", { NextAction("create healthstone", 26.0f) })); triggers.push_back(new TriggerNode("no soulstone", { NextAction("create soulstone", 25.0f) })); triggers.push_back(new TriggerNode("life tap", { NextAction("life tap", 23.0f) })); diff --git a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp index 53e77669b..1df531e7f 100644 --- a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp +++ b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp @@ -79,6 +79,16 @@ bool SoulLinkTrigger::IsActive() return !botAI->HasAura("soul link", target); } +bool UnendingBreathTrigger::IsActive() +{ + return BuffTrigger::IsActive() && AI_VALUE2(bool, "swimming", "self target"); +} + +bool UnendingBreathOnPartyTrigger::IsActive() +{ + return BuffOnPartyTrigger::IsActive() && AI_VALUE2(bool, "swimming", "self target"); +} + bool DemonicEmpowermentTrigger::IsActive() { Pet* pet = bot->GetPet(); diff --git a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h index 851254901..d66fe537e 100644 --- a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h +++ b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h @@ -32,6 +32,20 @@ public: bool IsActive() override; }; +class UnendingBreathTrigger : public BuffTrigger +{ +public: + UnendingBreathTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "unending breath", 5 * 2000) {} + bool IsActive() override; +}; + +class UnendingBreathOnPartyTrigger : public BuffOnPartyTrigger +{ +public: + UnendingBreathOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "unending breath on party", 2 * 2000) {} + bool IsActive() override; +}; + class OutOfSoulShardsTrigger : public Trigger { public: diff --git a/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp b/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp index a70327b6c..27b9166c6 100644 --- a/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp +++ b/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp @@ -143,6 +143,8 @@ public: creators["shadow trance"] = &WarlockTriggerFactoryInternal::shadow_trance; creators["demon armor"] = &WarlockTriggerFactoryInternal::demon_armor; creators["soul link"] = &WarlockTriggerFactoryInternal::soul_link; + creators["unending breath"] = &WarlockTriggerFactoryInternal::unending_breath; + creators["unending breath on party"] = &WarlockTriggerFactoryInternal::unending_breath_on_party; creators["no soul shard"] = &WarlockTriggerFactoryInternal::no_soul_shard; creators["too many soul shards"] = &WarlockTriggerFactoryInternal::too_many_soul_shards; creators["no healthstone"] = &WarlockTriggerFactoryInternal::HasHealthstone; @@ -189,6 +191,8 @@ private: static Trigger* shadow_trance(PlayerbotAI* botAI) { return new ShadowTranceTrigger(botAI); } static Trigger* demon_armor(PlayerbotAI* botAI) { return new DemonArmorTrigger(botAI); } static Trigger* soul_link(PlayerbotAI* botAI) { return new SoulLinkTrigger(botAI); } + static Trigger* unending_breath(PlayerbotAI* botAI) { return new UnendingBreathTrigger(botAI); } + static Trigger* unending_breath_on_party(PlayerbotAI* botAI) { return new UnendingBreathOnPartyTrigger(botAI); } static Trigger* no_soul_shard(PlayerbotAI* botAI) { return new OutOfSoulShardsTrigger(botAI); } static Trigger* too_many_soul_shards(PlayerbotAI* botAI) { return new TooManySoulShardsTrigger(botAI); } static Trigger* HasHealthstone(PlayerbotAI* botAI) { return new HasHealthstoneTrigger(botAI); } @@ -240,6 +244,8 @@ public: creators["demon armor"] = &WarlockAiObjectContextInternal::demon_armor; creators["demon skin"] = &WarlockAiObjectContextInternal::demon_skin; creators["soul link"] = &WarlockAiObjectContextInternal::soul_link; + creators["unending breath"] = &WarlockAiObjectContextInternal::unending_breath; + creators["unending breath on party"] = &WarlockAiObjectContextInternal::unending_breath_on_party; creators["create soul shard"] = &WarlockAiObjectContextInternal::create_soul_shard; creators["destroy soul shard"] = &WarlockAiObjectContextInternal::destroy_soul_shard; creators["create healthstone"] = &WarlockAiObjectContextInternal::create_healthstone; @@ -313,6 +319,8 @@ private: static Action* demon_armor(PlayerbotAI* botAI) { return new CastDemonArmorAction(botAI); } static Action* demon_skin(PlayerbotAI* botAI) { return new CastDemonSkinAction(botAI); } static Action* soul_link(PlayerbotAI* botAI) { return new CastSoulLinkAction(botAI); } + static Action* unending_breath(PlayerbotAI* botAI) { return new CastUnendingBreathAction(botAI); } + static Action* unending_breath_on_party(PlayerbotAI* botAI) { return new CastUnendingBreathOnPartyAction(botAI); } static Action* create_soul_shard(PlayerbotAI* botAI) { return new CreateSoulShardAction(botAI); } static Action* destroy_soul_shard(PlayerbotAI* botAI) { return new DestroySoulShardAction(botAI); } static Action* create_healthstone(PlayerbotAI* botAI) { return new CastCreateHealthstoneAction(botAI); } From 610fdc16d7e24fccbc336509831879d4c08db600 Mon Sep 17 00:00:00 2001 From: privatecore Date: Sun, 8 Feb 2026 18:36:56 +0100 Subject: [PATCH 11/11] Fix bug with GetCreature + GetGameObject = use ObjectAccessor's methods instead (#2105) # Pull Request https://en.cppreference.com/w/cpp/algorithm/equal_range.html > second is an iterator to the first element of the range [first, last) ordered after value (or last if no such element is found). The original code uses `return bounds.second->second`, which causes the wrong creature/gameobject to be returned. Instead, both methods (`GetCreature` and `GetGameObject`) now utilize ObjectAccessor's methods to retrieve the correct entities. These built-in methods offer a safer way to access objects. Additionally, `GetUnit` no longer includes redundant creature processing before checks and now has the same logic as the `ObjectAccessor::GetUnit` method. Furthermore, `GuidPosition::isDead` method has been renamed to `GuidPosition::IsCreatureOrGOAccessible` and updated, as it is used only for creatures (NOT units) and gameobjects. --- ## Design Philosophy We prioritize **stability, performance, and predictability** over behavioral realism. Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and long-term robustness. Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all participants. Because every action and decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and negatively affect both players and world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a project goal. Increased behavioral realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and maintained continuously as the system evolves. If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision model**. More complex behavior should only be available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. Principles: - **Stability before intelligence** A stable system is always preferred over a smarter one. - **Performance is a shared resource** Any increase in bot cost affects all players and all bots. - **Simple logic scales better than smart logic** Predictable behavior under load is more valuable than perfect decisions. - **Complexity must justify itself** If a feature cannot clearly explain its cost, it should not exist. - **Defaults must be cheap** Expensive behavior must always be optional and clearly communicated. - **Bots should look reasonable, not perfect** The goal is believable behavior, not human simulation. Before submitting, confirm that this change aligns with those principles. --- ## How to Test the Changes The behavior has not changed after all. ## Complexity & Impact - Does this change add new decision branches? - [x] No - [ ] Yes (**explain below**) - Does this change increase per-bot or per-tick processing? - [x] No - [ ] Yes (**describe and justify impact**) - Could this logic scale poorly under load? - [x] No - [ ] Yes (**explain why**) --- ## Defaults & Configuration - Does this change modify default bot behavior? - [x] No - [ ] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - [ ] Lightweight mode remains the default - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance - Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - [x] No - [ ] Yes (**explain below**) If yes, please specify: - AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) - Which parts of the change were influenced or generated - Whether the result was manually reviewed and adapted AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest about what they do and do not understand. --- ## Final Checklist - [x] Stability is not compromised - [x] Performance impact is understood, tested, and acceptable - [x] Added logic complexity is justified and explained - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --------- Co-authored-by: bashermens <31279994+hermensbas@users.noreply.github.com> --- src/Ai/Base/Value/QuestValues.cpp | 6 +- src/Mgr/Travel/TravelMgr.cpp | 188 ++++++++++++++---------------- src/Mgr/Travel/TravelMgr.h | 7 +- 3 files changed, 92 insertions(+), 109 deletions(-) diff --git a/src/Ai/Base/Value/QuestValues.cpp b/src/Ai/Base/Value/QuestValues.cpp index 172477850..f556f443d 100644 --- a/src/Ai/Base/Value/QuestValues.cpp +++ b/src/Ai/Base/Value/QuestValues.cpp @@ -173,7 +173,7 @@ std::vector ActiveQuestGiversValue::Calculate() continue; } - if (guidp.isDead()) + if (!guidp.IsCreatureOrGOAccessible()) continue; retQuestGivers.push_back(guidp); @@ -231,7 +231,7 @@ std::vector ActiveQuestTakersValue::Calculate() for (auto& guidp : entry.second) { - if (guidp.isDead()) + if (!guidp.IsCreatureOrGOAccessible()) continue; retQuestTakers.push_back(guidp); @@ -298,7 +298,7 @@ std::vector ActiveQuestObjectivesValue::Calculate() { for (auto& guidp : entry.second) { - if (guidp.isDead()) + if (!guidp.IsCreatureOrGOAccessible()) continue; retQuestObjectives.push_back(guidp); diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp index 84eef9b99..748f929aa 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -881,107 +881,6 @@ std::vector WorldPosition::getGameObjectsNear(float radiu return worker.GetResult(); } -Creature* GuidPosition::GetCreature() -{ - if (!*this) - return nullptr; - - if (loadedFromDB) - { - auto creatureBounds = getMap()->GetCreatureBySpawnIdStore().equal_range(GetCounter()); - if (creatureBounds.first != creatureBounds.second) - return creatureBounds.second->second; - - return nullptr; - } - - return getMap()->GetCreature(*this); -} - -Unit* GuidPosition::GetUnit() -{ - if (!*this) - return nullptr; - - if (loadedFromDB) - { - auto creatureBounds = getMap()->GetCreatureBySpawnIdStore().equal_range(GetCounter()); - if (creatureBounds.first != creatureBounds.second) - return creatureBounds.second->second; - - return nullptr; - } - - if (IsPlayer()) - return ObjectAccessor::FindPlayer(*this); - - if (IsPet()) - return getMap()->GetPet(*this); - - return GetCreature(); -} - -GameObject* GuidPosition::GetGameObject() -{ - if (!*this) - return nullptr; - - if (loadedFromDB) - { - auto gameobjectBounds = getMap()->GetGameObjectBySpawnIdStore().equal_range(GetCounter()); - if (gameobjectBounds.first != gameobjectBounds.second) - return gameobjectBounds.second->second; - - return nullptr; - } - - return getMap()->GetGameObject(*this); -} - -Player* GuidPosition::GetPlayer() -{ - if (!*this) - return nullptr; - - if (IsPlayer()) - return ObjectAccessor::FindPlayer(*this); - - return nullptr; -} - -bool GuidPosition::isDead() -{ - if (!getMap()) - return false; - - if (!getMap()->IsGridLoaded(getX(), getY())) - return false; - - if (IsUnit() && GetUnit() && GetUnit()->IsInWorld() && GetUnit()->IsAlive()) - return false; - - if (IsGameObject() && GetGameObject() && GetGameObject()->IsInWorld()) - return false; - - return true; -} - -GuidPosition::GuidPosition(WorldObject* wo) : ObjectGuid(wo->GetGUID()), WorldPosition(wo), loadedFromDB(false) {} - -GuidPosition::GuidPosition(CreatureData const& creData) - : ObjectGuid(HighGuid::Unit, creData.id1, creData.spawnId), - WorldPosition(creData.mapid, creData.posX, creData.posY, creData.posZ, creData.orientation) -{ - loadedFromDB = true; -} - -GuidPosition::GuidPosition(GameObjectData const& goData) - : ObjectGuid(HighGuid::GameObject, goData.id), - WorldPosition(goData.mapid, goData.posX, goData.posY, goData.posZ, goData.orientation) -{ - loadedFromDB = true; -} - CreatureTemplate const* GuidPosition::GetCreatureTemplate() { return IsCreature() ? sObjectMgr->GetCreatureTemplate(GetEntry()) : nullptr; @@ -1000,7 +899,7 @@ WorldObject* GuidPosition::GetWorldObject() switch (GetHigh()) { case HighGuid::Player: - return ObjectAccessor::FindPlayer(*this); + return GetPlayer(); case HighGuid::Transport: case HighGuid::Mo_Transport: case HighGuid::GameObject: @@ -1021,8 +920,93 @@ WorldObject* GuidPosition::GetWorldObject() return nullptr; } +GameObject* GuidPosition::GetGameObject() +{ + if (!*this) + return nullptr; + + if (loadedFromDB) + return ObjectAccessor::GetSpawnedGameObjectByDBGUID(GetMapId(), GetCounter()); + + return getMap()->GetGameObject(*this); // fallback +} + +Unit* GuidPosition::GetUnit() +{ + if (!*this) + return nullptr; + + if (IsPlayer()) + return GetPlayer(); + + if (IsPet()) + return getMap()->GetPet(*this); + + return GetCreature(); +} + +Creature* GuidPosition::GetCreature() +{ + if (!*this) + return nullptr; + + if (loadedFromDB) + return ObjectAccessor::GetSpawnedCreatureByDBGUID(GetMapId(), GetCounter()); + + return getMap()->GetCreature(*this); // fallback +} + +Player* GuidPosition::GetPlayer() +{ + if (!*this) + return nullptr; + + if (IsPlayer()) + return ObjectAccessor::FindPlayer(*this); + + return nullptr; +} + bool GuidPosition::HasNpcFlag(NPCFlags flag) { return IsCreature() && GetCreatureTemplate()->npcflag & flag; } +bool GuidPosition::IsCreatureOrGOAccessible() +{ + Map* map = getMap(); + if (!map || !map->IsGridLoaded(GetPositionX(), GetPositionY())) + return false; + + if (IsCreature()) + { + Creature* creature = GetCreature(); + if (creature && creature->IsInWorld() && creature->IsAlive()) + return true; + } + else if (IsGameObject()) + { + GameObject* go = GetGameObject(); + if (go && go->IsInWorld()) + return true; + } + + return false; +} + +GuidPosition::GuidPosition(WorldObject* wo) : ObjectGuid(wo->GetGUID()), WorldPosition(wo), loadedFromDB(false) {} + +GuidPosition::GuidPosition(CreatureData const& creData) + : ObjectGuid(HighGuid::Unit, creData.id1, creData.spawnId), + WorldPosition(creData.mapid, creData.posX, creData.posY, creData.posZ, creData.orientation) +{ + loadedFromDB = true; +} + +GuidPosition::GuidPosition(GameObjectData const& goData) + : ObjectGuid(HighGuid::GameObject, goData.id), + WorldPosition(goData.mapid, goData.posX, goData.posY, goData.posZ, goData.orientation) +{ + loadedFromDB = true; +} + std::vector TravelDestination::getPoints(bool ignoreFull) { if (ignoreFull) diff --git a/src/Mgr/Travel/TravelMgr.h b/src/Mgr/Travel/TravelMgr.h index 3223444bf..251313736 100644 --- a/src/Mgr/Travel/TravelMgr.h +++ b/src/Mgr/Travel/TravelMgr.h @@ -416,14 +416,13 @@ public: GameObjectTemplate const* GetGameObjectTemplate(); WorldObject* GetWorldObject(); - Creature* GetCreature(); - Unit* GetUnit(); GameObject* GetGameObject(); + Unit* GetUnit(); + Creature* GetCreature(); Player* GetPlayer(); bool HasNpcFlag(NPCFlags flag); - - bool isDead(); // For loaded grids check if the unit/object is unloaded/dead. + bool IsCreatureOrGOAccessible(); // For loaded grids check if the creature/gameobject is in world + alive operator bool() const { return !IsEmpty(); } bool operator==(ObjectGuid const& guid) const { return GetRawValue() == guid.GetRawValue(); }