mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
# Pull Request Most of the changes are not functional but are to modify style based on comments received to the TK PR (e.g., eliminate nesting of if statements) and leverage general boss helpers. There is some reordering of returns and other changes to try to consolidate or clean-up the code (such as removing unnecessary parameters). The strategies themselves have only minor changes. - Main tank no longer uses tangential movement for Lurker Spout, unlike other bots. The MT will just call moves directly to a position behind Lurker. This is because I found tangential movement was taking too long for the MT to get in place since it starts right in front of Lurker. - Vashj MT group Shaman will now use Grounding Totem without actually switching to the Grounding Totem strategy. I have now eliminated all strategy swaps, which I dislike because they persist after the encounter, and it's better not to mess with player strategies since presumably people are generally using Windfury or Wrath of Air for the Air Totem. - I made a ton of changes to Vashj core passing as I noticed the existing logic is nonfunctional in several ways. It generally works fine ingame, but the changes should make things much smoother. For example, the storing of the nearest trigger NPC for generators in the existing strategy is useless because it relies on insert_or_assign for an unordered map that will continue to run during the course of the core passing logic, and a similar issue exists with respect to the map to store the last time a bot held a core. The result is that there is slight movement of bots when the core is flying through the air and not held by any bot because the trigger for core passing does not fire during that period. In practice, the time is brief enough that the sequence works OK, but it looks stupid because the bots should not be moving at all. So that should be fixed. There is a known issue re: core passing that would take extreme effort to fix and I am not going to do it because it is a fringe situation. There are a couple of spots where the Tainted Elemental can rarely spawn that can result in a straight-line passing sequence to the nearest generator that is blocked by LoS. Fixing this would be extremely difficult and niche, so what you will need to do if this happens is to just command your bots to destroy the core and try again with the next spawn. --- ## 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? I have attempted to streamline methods and even remove some. The strategy is admittedly somewhat performance heavy due to the need for function calls such as iterating over inventory items. However, the new version should be less performance intensive than the merged strategy--for example, there were places where all members of the raid would have their inventory checked, but I've now limited the check to only the 5 core handler bots. I've run the instance with pmon on, and there are no methods that stand out as particularly resource intensive when not in a boss encounter, and I view that as the most important thing (though I make effort to reduce performance impact during encounters also). Expensive checks that are unavoidable for the strategy to work such as grid searches are gated behind cheaper checks. --- ## 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 Enter SSC with a raid group and run the instance, including all bosses. Every boss should be killable, and every major mechanic should be addressed by bots. I will work with Dreathean to get the Wiki up soon so that should be a reference for testing strategies. ## Complexity & Impact Does this change add new decision branches? - - [ ] No - - [x] Yes (**explain below**) Only within the context of strategies, which are basically all new decision branches, and there are some tweaks here to what is currently merged. Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [ ] No - - [x] Yes (**explain why**) I'm sure if you have a large server, with multiple raid groups running the instance at the same time, the performance impact could be significant. But I have done my best to limit it, and I think some notable performance impact is unavoidable with the current framework for raid strategies. --- ## Defaults & Configuration Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Only for strategies in the instance. If this introduces more advanced or AI-heavy logic: - - [ ] Lightweight mode remains the default - - [x] More complex behavior is optional and thereby configurable There should be no impact if co +ssc and nc +ssc are not added to bots. Because raid strategies are currently not removed after leaving an instance, players should manually remove them (or reset botAI). This is a general issue that needs to be addressed with the module. --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [ ] No - - [x] 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. GPT-4 because I don't like to use up my premium requests in CoPilot and I generally like it better than GPT-5 =P I use LLMs to draft code snippets but do review everything and have become less-and-less reliant over time. I don't use agent mode, only ask. For this PR, I had it do the updated version of AnyRecentCoreInInventory(), which is more complicated than before and uses indexing for each bot to consider status of only prior bots in the passing chain. Everything else either I wrote or could have written but had the AI help and just edited afterward to save time. --- ## 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: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash <hermensb@gmail.com> Co-authored-by: Revision <tkn963@gmail.com> Co-authored-by: kadeshar <kadeshar@gmail.com>
583 lines
16 KiB
C++
583 lines
16 KiB
C++
/*
|
|
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
|
|
* and/or modify it under version 3 of the License, or (at your option), any later version.
|
|
*/
|
|
|
|
#include "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) &&
|
|
AI_VALUE2(Unit*, "find target", "greyheart tidecaller");
|
|
}
|
|
|
|
// Hydross the Unstable <Duke of Currents>
|
|
|
|
bool HydrossTheUnstableBotIsFrostTankTrigger::IsActive()
|
|
{
|
|
return botAI->IsMainTank(bot) &&
|
|
AI_VALUE2(Unit*, "find target", "hydross the unstable");
|
|
}
|
|
|
|
bool HydrossTheUnstableBotIsNatureTankTrigger::IsActive()
|
|
{
|
|
return botAI->IsAssistTankOfIndex(bot, 0, true) &&
|
|
AI_VALUE2(Unit*, "find target", "hydross the unstable");
|
|
}
|
|
|
|
bool HydrossTheUnstableElementalsSpawnedTrigger::IsActive()
|
|
{
|
|
if (botAI->IsHeal(bot))
|
|
return false;
|
|
|
|
Unit* hydross = AI_VALUE2(Unit*, "find target", "hydross the unstable");
|
|
if (!hydross || hydross->GetHealthPct() < 10.0f)
|
|
return false;
|
|
|
|
if (botAI->IsMainTank(bot) || botAI->IsAssistTankOfIndex(bot, 0, true))
|
|
return false;
|
|
|
|
return AI_VALUE2(Unit*, "find target", "pure spawn of hydross") ||
|
|
AI_VALUE2(Unit*, "find target", "tainted spawn of hydross");
|
|
}
|
|
|
|
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 (bot->getClass() == CLASS_HUNTER ||
|
|
botAI->IsHeal(bot) ||
|
|
botAI->IsMainTank(bot) ||
|
|
botAI->IsAssistTankOfIndex(bot, 0, true))
|
|
return false;
|
|
|
|
return AI_VALUE2(Unit*, "find target", "hydross the unstable");
|
|
}
|
|
|
|
bool HydrossTheUnstableNeedToManageTimersTrigger::IsActive()
|
|
{
|
|
return IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID) &&
|
|
AI_VALUE2(Unit*, "find target", "hydross the unstable");
|
|
}
|
|
|
|
// 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()
|
|
{
|
|
if (!botAI->IsMainTank(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);
|
|
}
|
|
|
|
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()
|
|
{
|
|
if (!botAI->IsTank(bot))
|
|
return false;
|
|
|
|
Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below");
|
|
if (!lurker || lurker->getStandState() != UNIT_STAND_STATE_SUBMERGED)
|
|
return false;
|
|
|
|
Player* mainTank = GetGroupMainTank(botAI, bot);
|
|
Player* firstAssistTank = GetGroupAssistTank(botAI, bot, 0);
|
|
Player* secondAssistTank = GetGroupAssistTank(botAI, bot, 1);
|
|
|
|
if (!mainTank || !firstAssistTank || !secondAssistTank)
|
|
return false;
|
|
|
|
return bot == mainTank || bot == firstAssistTank || bot == secondAssistTank;
|
|
}
|
|
|
|
bool TheLurkerBelowNeedToPrepareTimerForSpoutTrigger::IsActive()
|
|
{
|
|
return IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID) &&
|
|
AI_VALUE2(Unit*, "find target", "the lurker below");
|
|
}
|
|
|
|
// Leotheras the Blind
|
|
|
|
bool LeotherasTheBlindBossIsInactiveTrigger::IsActive()
|
|
{
|
|
return IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID) &&
|
|
AI_VALUE2(Unit*, "find target", "greyheart spellbinder");
|
|
}
|
|
|
|
bool LeotherasTheBlindBossTransformedIntoDemonFormTrigger::IsActive()
|
|
{
|
|
if (bot->getClass() != CLASS_WARLOCK)
|
|
return false;
|
|
|
|
if (!AI_VALUE2(Unit*, "find target", "leotheras the blind"))
|
|
return false;
|
|
|
|
if (GetLeotherasDemonFormTank(bot) != bot)
|
|
return false;
|
|
|
|
return GetActiveLeotherasDemon(bot);
|
|
}
|
|
|
|
bool LeotherasTheBlindOnlyWarlockShouldTankDemonFormTrigger::IsActive()
|
|
{
|
|
if (!botAI->IsTank(bot))
|
|
return false;
|
|
|
|
if (bot->HasAura(SPELL_INSIDIOUS_WHISPER))
|
|
return false;
|
|
|
|
if (!AI_VALUE2(Unit*, "find target", "leotheras the blind"))
|
|
return false;
|
|
|
|
if (!GetLeotherasDemonFormTank(bot))
|
|
return false;
|
|
|
|
return GetPhase2LeotherasDemon(bot);
|
|
}
|
|
|
|
bool LeotherasTheBlindBossEngagedByRangedTrigger::IsActive()
|
|
{
|
|
if (!botAI->IsRanged(bot))
|
|
return false;
|
|
|
|
if (bot->HasAura(SPELL_INSIDIOUS_WHISPER))
|
|
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 (botAI->IsTank(bot))
|
|
return false;
|
|
|
|
Unit* leotheras = AI_VALUE2(Unit*, "find target", "leotheras the blind");
|
|
if (!leotheras)
|
|
return false;
|
|
|
|
if (bot->HasAura(SPELL_INSIDIOUS_WHISPER))
|
|
return false;
|
|
|
|
return leotheras->HasAura(SPELL_WHIRLWIND) ||
|
|
leotheras->HasAura(SPELL_WHIRLWIND_CHANNEL);
|
|
}
|
|
|
|
bool LeotherasTheBlindBotHasTooManyChaosBlastStacksTrigger::IsActive()
|
|
{
|
|
if (botAI->IsRanged(bot))
|
|
return false;
|
|
|
|
if (!AI_VALUE2(Unit*, "find target", "leotheras the blind"))
|
|
return false;
|
|
|
|
if (bot->HasAura(SPELL_INSIDIOUS_WHISPER))
|
|
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(bot);
|
|
}
|
|
|
|
bool LeotherasTheBlindInnerDemonHasAwakenedTrigger::IsActive()
|
|
{
|
|
return bot->HasAura(SPELL_INSIDIOUS_WHISPER) &&
|
|
GetLeotherasDemonFormTank(bot) != bot;
|
|
}
|
|
|
|
bool LeotherasTheBlindEnteredFinalPhaseTrigger::IsActive()
|
|
{
|
|
if (botAI->IsHeal(bot))
|
|
return false;
|
|
|
|
if (!AI_VALUE2(Unit*, "find target", "leotheras the blind"))
|
|
return false;
|
|
|
|
if (bot->HasAura(SPELL_INSIDIOUS_WHISPER))
|
|
return false;
|
|
|
|
if (bot->getClass() == CLASS_WARLOCK && GetLeotherasDemonFormTank(bot) == bot)
|
|
return false;
|
|
|
|
return GetPhase3LeotherasDemon(bot);
|
|
}
|
|
|
|
bool LeotherasTheBlindDemonFormTankNeedsAggro::IsActive()
|
|
{
|
|
if (bot->getClass() != CLASS_HUNTER)
|
|
return false;
|
|
|
|
if (!AI_VALUE2(Unit*, "find target", "leotheras the blind"))
|
|
return false;
|
|
|
|
return !bot->HasAura(SPELL_INSIDIOUS_WHISPER);
|
|
}
|
|
|
|
bool LeotherasTheBlindBossWipesAggroUponPhaseChangeTrigger::IsActive()
|
|
{
|
|
return IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID) &&
|
|
AI_VALUE2(Unit*, "find target", "leotheras the blind");
|
|
}
|
|
|
|
// Fathom-Lord Karathress
|
|
|
|
bool FathomLordKarathressBossEngagedByMainTankTrigger::IsActive()
|
|
{
|
|
return botAI->IsMainTank(bot) &&
|
|
AI_VALUE2(Unit*, "find target", "fathom-lord karathress");
|
|
}
|
|
|
|
bool FathomLordKarathressCaribdisEngagedByFirstAssistTankTrigger::IsActive()
|
|
{
|
|
return botAI->IsAssistTankOfIndex(bot, 0, false) &&
|
|
AI_VALUE2(Unit*, "find target", "fathom-guard caribdis");
|
|
}
|
|
|
|
bool FathomLordKarathressSharkkisEngagedBySecondAssistTankTrigger::IsActive()
|
|
{
|
|
return botAI->IsAssistTankOfIndex(bot, 1, false) &&
|
|
AI_VALUE2(Unit*, "find target", "fathom-guard sharkkis");
|
|
}
|
|
|
|
bool FathomLordKarathressTidalvessEngagedByThirdAssistTankTrigger::IsActive()
|
|
{
|
|
return botAI->IsAssistTankOfIndex(bot, 2, false) &&
|
|
AI_VALUE2(Unit*, "find target", "fathom-guard tidalvess");
|
|
}
|
|
|
|
bool FathomLordKarathressCaribdisTankNeedsDedicatedHealerTrigger::IsActive()
|
|
{
|
|
return botAI->IsAssistHealOfIndex(bot, 0, true) &&
|
|
AI_VALUE2(Unit*, "find target", "fathom-guard caribdis");
|
|
}
|
|
|
|
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 (botAI->IsHeal(bot))
|
|
return false;
|
|
|
|
if (!AI_VALUE2(Unit*, "find target", "fathom-lord karathress"))
|
|
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 IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID) &&
|
|
AI_VALUE2(Unit*, "find target", "fathom-lord karathress");
|
|
}
|
|
|
|
// 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 botAI->IsMainTank(bot) &&
|
|
AI_VALUE2(Unit*, "find target", "morogrim tidewalker");
|
|
}
|
|
|
|
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 <Coilfang Matron>
|
|
|
|
bool LadyVashjBossEngagedByMainTankTrigger::IsActive()
|
|
{
|
|
if (!botAI->IsMainTank(bot))
|
|
return false;
|
|
|
|
return AI_VALUE2(Unit*, "find target", "lady vashj") &&
|
|
!IsLadyVashjInPhase2(botAI);
|
|
}
|
|
|
|
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;
|
|
|
|
return IsMainTankInSameSubgroup(botAI, bot);
|
|
}
|
|
|
|
bool LadyVashjBotHasStaticChargeTrigger::IsActive()
|
|
{
|
|
if (!AI_VALUE2(Unit*, "find target", "lady vashj"))
|
|
return false;
|
|
|
|
Group* group = bot->GetGroup();
|
|
if (!group)
|
|
return false;
|
|
|
|
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;
|
|
if (AI_VALUE2(Unit*, "find target", "tainted elemental"))
|
|
{
|
|
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();
|
|
creature->GetEntry() == NPC_TAINTED_ELEMENTAL && !creature->IsAlive())
|
|
{
|
|
taintedPresent = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!taintedPresent)
|
|
return false;
|
|
|
|
return GetDesignatedCoreLooter(botAI, bot) == bot &&
|
|
!bot->HasItemCount(ITEM_TAINTED_CORE, 1, false);
|
|
}
|
|
|
|
bool LadyVashjTaintedCoreWasLootedTrigger::IsActive()
|
|
{
|
|
if (!AI_VALUE2(Unit*, "find target", "lady vashj") || !IsLadyVashjInPhase2(botAI))
|
|
return false;
|
|
|
|
auto coreHandlers = GetCoreHandlers(botAI, bot);
|
|
|
|
bool isCoreHandler = false;
|
|
for (Player* handler : coreHandlers)
|
|
if (handler == bot)
|
|
isCoreHandler = true;
|
|
|
|
if (!isCoreHandler)
|
|
return false;
|
|
|
|
// First and second passers move to positions as soon as the elemental appears
|
|
Unit* tainted = AI_VALUE2(Unit*, "find target", "tainted elemental");
|
|
if (tainted && coreHandlers[0]->GetExactDist2d(tainted) < 5.0f &&
|
|
(bot == coreHandlers[1] || bot == coreHandlers[2]))
|
|
return true;
|
|
|
|
// Main logic: run if core is in play for this bot or a prior handler
|
|
return AnyRecentCoreInInventory(botAI, bot);
|
|
}
|
|
|
|
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);
|
|
|
|
auto coreHandlers = GetCoreHandlers(botAI, bot);
|
|
|
|
if (bot->HasItemCount(ITEM_TAINTED_CORE, 1, false))
|
|
{
|
|
for (Player* coreHandler : coreHandlers)
|
|
{
|
|
if (coreHandler && bot == coreHandler)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool LadyVashjToxicSporebatsAreSpewingPoisonCloudsTrigger::IsActive()
|
|
{
|
|
return IsLadyVashjInPhase3(botAI);
|
|
}
|
|
|
|
bool LadyVashjBotIsEntangledInToxicSporesOrStaticChargeTrigger::IsActive()
|
|
{
|
|
if (!AI_VALUE2(Unit*, "find target", "lady vashj"))
|
|
return false;
|
|
|
|
Group* group = bot->GetGroup();
|
|
if (!group)
|
|
return false;
|
|
|
|
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;
|
|
}
|