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