From 0af8788a99bb0989916fdab4629430bf3f92d7d8 Mon Sep 17 00:00:00 2001 From: crow Date: Tue, 23 Dec 2025 12:17:02 -0600 Subject: [PATCH 01/29] commit #1 for PR --- src/PlayerbotAI.cpp | 3 + src/strategy/AiObjectContext.cpp | 4 + src/strategy/raids/RaidStrategyContext.h | 3 + .../RaidTempestKeepActionContext.h | 295 +++ .../tempestkeep/RaidTempestKeepActions.cpp | 2286 +++++++++++++++++ .../tempestkeep/RaidTempestKeepActions.h | 417 +++ .../tempestkeep/RaidTempestKeepHelpers.cpp | 610 +++++ .../tempestkeep/RaidTempestKeepHelpers.h | 175 ++ .../RaidTempestKeepKaelthasBossAI.h | 54 + .../RaidTempestKeepMultipliers.cpp | 474 ++++ .../tempestkeep/RaidTempestKeepMultipliers.h | 158 ++ .../tempestkeep/RaidTempestKeepStrategy.cpp | 166 ++ .../tempestkeep/RaidTempestKeepStrategy.h | 18 + .../RaidTempestKeepTriggerContext.h | 247 ++ .../tempestkeep/RaidTempestKeepTriggers.cpp | 566 ++++ .../tempestkeep/RaidTempestKeepTriggers.h | 312 +++ 16 files changed, 5788 insertions(+) create mode 100644 src/strategy/raids/tempestkeep/RaidTempestKeepActionContext.h create mode 100644 src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp create mode 100644 src/strategy/raids/tempestkeep/RaidTempestKeepActions.h create mode 100644 src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp create mode 100644 src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.h create mode 100644 src/strategy/raids/tempestkeep/RaidTempestKeepKaelthasBossAI.h create mode 100644 src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.cpp create mode 100644 src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.h create mode 100644 src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp create mode 100644 src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.h create mode 100644 src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h create mode 100644 src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp create mode 100644 src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h diff --git a/src/PlayerbotAI.cpp b/src/PlayerbotAI.cpp index c1878632c6a..58a5f070e7c 100644 --- a/src/PlayerbotAI.cpp +++ b/src/PlayerbotAI.cpp @@ -1473,6 +1473,9 @@ void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) case 544: strategyName = "magtheridon"; // Magtheridon's Lair break; + case 550: + strategyName = "tempestkeep"; // Tempest Keep + break; case 565: strategyName = "gruulslair"; // Gruul's Lair break; diff --git a/src/strategy/AiObjectContext.cpp b/src/strategy/AiObjectContext.cpp index 7a1da0f8a63..4cd83419c22 100644 --- a/src/strategy/AiObjectContext.cpp +++ b/src/strategy/AiObjectContext.cpp @@ -43,6 +43,8 @@ #include "raids/magtheridon/RaidMagtheridonTriggerContext.h" #include "raids/gruulslair/RaidGruulsLairActionContext.h" #include "raids/gruulslair/RaidGruulsLairTriggerContext.h" +#include "raids/tempestkeep/RaidTempestKeepActionContext.h" +#include "raids/tempestkeep/RaidTempestKeepTriggerContext.h" #include "raids/naxxramas/RaidNaxxActionContext.h" #include "raids/naxxramas/RaidNaxxTriggerContext.h" #include "raids/eyeofeternity/RaidEoEActionContext.h" @@ -117,6 +119,7 @@ void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextList creators["karazhan"] = &RaidStrategyContext::karazhan; creators["magtheridon"] = &RaidStrategyContext::magtheridon; creators["gruulslair"] = &RaidStrategyContext::gruulslair; + creators["tempestkeep"] = &RaidStrategyContext::tempestkeep; creators["naxx"] = &RaidStrategyContext::naxx; creators["wotlk-os"] = &RaidStrategyContext::wotlk_os; creators["wotlk-eoe"] = &RaidStrategyContext::wotlk_eoe; @@ -43,6 +45,7 @@ class RaidStrategyContext : public NamedObjectContext 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* tempestkeep(PlayerbotAI* botAI) { return new RaidTempestKeepStrategy(botAI); } static Strategy* naxx(PlayerbotAI* botAI) { return new RaidNaxxStrategy(botAI); } static Strategy* wotlk_os(PlayerbotAI* botAI) { return new RaidOsStrategy(botAI); } static Strategy* wotlk_eoe(PlayerbotAI* botAI) { return new RaidEoEStrategy(botAI); } diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActionContext.h b/src/strategy/raids/tempestkeep/RaidTempestKeepActionContext.h new file mode 100644 index 00000000000..e4bf7c3908c --- /dev/null +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepActionContext.h @@ -0,0 +1,295 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPACTIONCONTEXT_H +#define _PLAYERBOT_RAIDTEMPESTKEEPACTIONCONTEXT_H + +#include "RaidTempestKeepActions.h" +#include "NamedObjectContext.h" + +class RaidTempestKeepActionContext : public NamedObjectContext +{ +public: + RaidTempestKeepActionContext() + { + // Trash + creators["crimson hand centurion cast polymorph"] = + &RaidTempestKeepActionContext::crimson_hand_centurion_cast_polymorph; + + // Al'ar + creators["al'ar misdirect boss to main tank"] = + &RaidTempestKeepActionContext::alar_misdirect_boss_to_main_tank; + + creators["al'ar boss tanks move between platforms"] = + &RaidTempestKeepActionContext::alar_boss_tanks_move_between_platforms; + + creators["al'ar melee dps move between platforms"] = + &RaidTempestKeepActionContext::alar_melee_dps_move_between_platforms; + + creators["al'ar ranged and ember tank move under platforms"] = + &RaidTempestKeepActionContext::alar_ranged_and_ember_tank_move_under_platforms; + + creators["al'ar assist tanks pick up embers"] = + &RaidTempestKeepActionContext::alar_assist_tanks_pick_up_embers; + + creators["al'ar ranged dps prioritize embers"] = + &RaidTempestKeepActionContext::alar_ranged_dps_prioritize_embers; + + creators["al'ar jump from platform"] = + &RaidTempestKeepActionContext::alar_jump_from_platform; + + creators["al'ar move away from rebirth"] = + &RaidTempestKeepActionContext::alar_move_away_from_rebirth; + + creators["al'ar swap tanks on boss"] = + &RaidTempestKeepActionContext::alar_swap_tanks_on_boss; + + creators["al'ar avoid flame patches and dive bombs"] = + &RaidTempestKeepActionContext::alar_avoid_flame_patches_and_dive_bombs; + + creators["al'ar return to room center"] = + &RaidTempestKeepActionContext::alar_return_to_room_center; + + creators["al'ar manage phase tracker"] = + &RaidTempestKeepActionContext::alar_manage_phase_tracker; + + // Void Reaver + creators["void reaver tanks position boss"] = + &RaidTempestKeepActionContext::void_reaver_tanks_position_boss; + + creators["void reaver ranged use aggro dump ability"] = + &RaidTempestKeepActionContext::void_reaver_ranged_use_aggro_dump_ability; + + creators["void reaver spread ranged"] = + &RaidTempestKeepActionContext::void_reaver_spread_ranged; + + // High Astromancer Solarian + creators["high astromancer solarian ranged leave space for melee"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_ranged_leave_space_for_melee; + + creators["high astromancer solarian move away from group"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_move_away_from_group; + + creators["high astromancer solarian stack for aoe"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_stack_for_aoe; + + creators["high astromancer solarian target solarium priests"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_target_solarium_priests; + + creators["high astromancer solarian tank voidwalker"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_tank_voidwalker; + + creators["high astromancer solarian cast fear ward on main tank"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_cast_fear_ward_on_main_tank; + + // Kael'thas Sunstrider + creators["kael'thas sunstrider kite thaladred"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_kite_thaladred; + + creators["kael'thas sunstrider misdirect advisors to tanks"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_misdirect_advisors_to_tanks; + + creators["kael'thas sunstrider main tank position sanguinar"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_main_tank_position_sanguinar; + + creators["kael'thas sunstrider cast fear ward on sanguinar tank"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_cast_fear_ward_on_sanguinar_tank; + + creators["kael'thas sunstrider manage warlock tank strategy"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_manage_warlock_tank_strategy; + + creators["kael'thas sunstrider warlock tank position capernian"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_warlock_tank_position_capernian; + + creators["kael'thas sunstrider spread and move away from capernian"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_spread_and_move_away_from_capernian; + + creators["kael'thas sunstrider first assist tank position telonicus"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_first_assist_tank_position_telonicus; + + creators["kael'thas sunstrider position phase 3 tank healer"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_position_phase_3_tank_healer; + + creators["kael'thas sunstrider assign advisor dps priority"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_assign_advisor_dps_priority; + + creators["kael'thas sunstrider manage advisor dps timer"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_manage_advisor_dps_timer; + + creators["kael'thas sunstrider assign legendary weapon dps priority"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_assign_legendary_weapon_dps_priority; + + creators["kael'thas sunstrider main tank move devastation away"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_main_tank_move_devastation_away; + + creators["kael'thas sunstrider hunter turn away netherstrand longbow"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_hunter_turn_away_netherstrand_longbow; + + creators["kael'thas sunstrider loot legendary weapons"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_loot_legendary_weapons; + + creators["kael'thas sunstrider use legendary weapons"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_use_legendary_weapons; + + creators["kael'thas sunstrider reequip gear"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_reequip_gear; + + creators["kael'thas sunstrider main tank position boss"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_main_tank_position_boss; + + creators["kael'thas sunstrider avoid flame strike"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_avoid_flame_strike; + + creators["kael'thas sunstrider round up phoenixes and focus down eggs"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_round_up_phoenixes_and_focus_down_eggs; + + creators["kael'thas sunstrider break mind control with infinity blade"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_break_mind_control_with_infinity_blade; + + creators["kael'thas sunstrider break through shock barrier"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_break_through_shock_barrier; + + creators["kael'thas sunstrider spread out in midair"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_spread_out_in_midair; + } + +private: + // Trash + static Action* crimson_hand_centurion_cast_polymorph( + PlayerbotAI* botAI) { return new CrimsonHandCenturionCastPolymorphAction(botAI); } + + // Al'ar + static Action* alar_misdirect_boss_to_main_tank( + PlayerbotAI* botAI) { return new AlarMisdirectBossToMainTankAction(botAI); } + + static Action* alar_boss_tanks_move_between_platforms( + PlayerbotAI* botAI) { return new AlarBossTanksMoveBetweenPlatformsAction(botAI); } + + static Action* alar_melee_dps_move_between_platforms( + PlayerbotAI* botAI) { return new AlarMeleeDpsMoveBetweenPlatformsAction(botAI); } + + static Action* alar_ranged_and_ember_tank_move_under_platforms( + PlayerbotAI* botAI) { return new AlarRangedAndEmberTankMoveUnderPlatformsAction(botAI); } + + static Action* alar_assist_tanks_pick_up_embers( + PlayerbotAI* botAI) { return new AlarAssistTanksPickUpEmbersAction(botAI); } + + static Action* alar_ranged_dps_prioritize_embers( + PlayerbotAI* botAI) { return new AlarRangedDpsPrioritizeEmbersAction(botAI); } + + static Action* alar_jump_from_platform( + PlayerbotAI* botAI) { return new AlarJumpFromPlatformAction(botAI); } + + static Action* alar_move_away_from_rebirth( + PlayerbotAI* botAI) { return new AlarMoveAwayFromRebirthAction(botAI); } + + static Action* alar_swap_tanks_on_boss( + PlayerbotAI* botAI) { return new AlarSwapTanksOnBossAction(botAI); } + + static Action* alar_avoid_flame_patches_and_dive_bombs( + PlayerbotAI* botAI) { return new AlarAvoidFlamePatchesAndDiveBombsAction(botAI); } + + static Action* alar_return_to_room_center( + PlayerbotAI* botAI) { return new AlarReturnToRoomCenterAction(botAI); } + + static Action* alar_manage_phase_tracker( + PlayerbotAI* botAI) { return new AlarManagePhaseTrackerAction(botAI); } + + // Void Reaver + static Action* void_reaver_tanks_position_boss( + PlayerbotAI* botAI) { return new VoidReaverTanksPositionBossAction(botAI); } + + static Action* void_reaver_ranged_use_aggro_dump_ability( + PlayerbotAI* botAI) { return new VoidReaverRangedUseAggroDumpAbilityAction(botAI); } + + static Action* void_reaver_spread_ranged( + PlayerbotAI* botAI) { return new VoidReaverSpreadRangedAction(botAI); } + + // High Astromancer Solarian + static Action* high_astromancer_solarian_ranged_leave_space_for_melee( + PlayerbotAI* botAI) { return new HighAstromancerSolarianRangedLeaveSpaceForMeleeAction(botAI); } + + static Action* high_astromancer_solarian_move_away_from_group( + PlayerbotAI* botAI) { return new HighAstromancerSolarianMoveAwayFromGroupAction(botAI); } + + static Action* high_astromancer_solarian_stack_for_aoe( + PlayerbotAI* botAI) { return new HighAstromancerSolarianStackForAoeAction(botAI); } + + static Action* high_astromancer_solarian_target_solarium_priests( + PlayerbotAI* botAI) { return new HighAstromancerSolarianTargetSolariumPriestsAction(botAI); } + + static Action* high_astromancer_solarian_tank_voidwalker( + PlayerbotAI* botAI) { return new HighAstromancerSolarianTankVoidwalkerAction(botAI); } + + static Action* high_astromancer_solarian_cast_fear_ward_on_main_tank( + PlayerbotAI* botAI) { return new HighAstromancerSolarianCastFearWardOnMainTankAction(botAI); } + + // Kael'thas Sunstrider + static Action* kaelthas_sunstrider_kite_thaladred( + PlayerbotAI* botAI) { return new KaelthasSunstriderKiteThaladredAction(botAI); } + + static Action* kaelthas_sunstrider_misdirect_advisors_to_tanks( + PlayerbotAI* botAI) { return new KaelthasSunstriderMisdirectAdvisorsToTanksAction(botAI); } + + static Action* kaelthas_sunstrider_main_tank_position_sanguinar( + PlayerbotAI* botAI) { return new KaelthasSunstriderMainTankPositionSanguinarAction(botAI); } + + static Action* kaelthas_sunstrider_manage_warlock_tank_strategy( + PlayerbotAI* botAI) { return new KaelthasSunstriderManageWarlockTankStrategyAction(botAI); } + + static Action* kaelthas_sunstrider_cast_fear_ward_on_sanguinar_tank( + PlayerbotAI* botAI) { return new KaelthasSunstriderCastFearWardOnSanguinarTankAction(botAI); } + + static Action* kaelthas_sunstrider_warlock_tank_position_capernian( + PlayerbotAI* botAI) { return new KaelthasSunstriderWarlockTankPositionCapernianAction(botAI); } + + static Action* kaelthas_sunstrider_spread_and_move_away_from_capernian( + PlayerbotAI* botAI) { return new KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction(botAI); } + + static Action* kaelthas_sunstrider_first_assist_tank_position_telonicus( + PlayerbotAI* botAI) { return new KaelthasSunstriderFirstAssistTankPositionTelonicusAction(botAI); } + + static Action* kaelthas_sunstrider_position_phase_3_tank_healer( + PlayerbotAI* botAI) { return new KaelthasSunstriderPositionPhase3TankHealerAction(botAI); } + + static Action* kaelthas_sunstrider_assign_advisor_dps_priority( + PlayerbotAI* botAI) { return new KaelthasSunstriderAssignAdvisorDpsPriorityAction(botAI); } + + static Action* kaelthas_sunstrider_manage_advisor_dps_timer( + PlayerbotAI* botAI) { return new KaelthasSunstriderManageAdvisorDpsTimerAction(botAI); } + + static Action* kaelthas_sunstrider_assign_legendary_weapon_dps_priority( + PlayerbotAI* botAI) { return new KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction(botAI); } + + static Action* kaelthas_sunstrider_main_tank_move_devastation_away( + PlayerbotAI* botAI) { return new KaelthasSunstriderMoveDevastationAwayAction(botAI); } + + static Action* kaelthas_sunstrider_hunter_turn_away_netherstrand_longbow( + PlayerbotAI* botAI) { return new KaelthasSunstriderHunterTurnAwayNetherstrandLongbowAction(botAI); } + + static Action* kaelthas_sunstrider_loot_legendary_weapons( + PlayerbotAI* botAI) { return new KaelthasSunstriderLootLegendaryWeaponsAction(botAI); } + + static Action* kaelthas_sunstrider_use_legendary_weapons( + PlayerbotAI* botAI) { return new KaelthasSunstriderUseLegendaryWeaponsAction(botAI); } + + static Action* kaelthas_sunstrider_reequip_gear( + PlayerbotAI* botAI) { return new KaelthasSunstriderReequipGearAction(botAI); } + + static Action* kaelthas_sunstrider_main_tank_position_boss( + PlayerbotAI* botAI) { return new KaelthasSunstriderMainTankPositionBossAction(botAI); } + + static Action* kaelthas_sunstrider_avoid_flame_strike( + PlayerbotAI* botAI) { return new KaelthasSunstriderAvoidFlameStrikeAction(botAI); } + + static Action* kaelthas_sunstrider_round_up_phoenixes_and_focus_down_eggs( + PlayerbotAI* botAI) { return new KaelthasSunstriderRoundUpPhoenixesAndFocusDownEggsAction(botAI); } + + static Action* kaelthas_sunstrider_break_mind_control_with_infinity_blade( + PlayerbotAI* botAI) { return new KaelthasSunstriderBreakMindControlWithInfinityBladeAction(botAI); } + + static Action* kaelthas_sunstrider_break_through_shock_barrier( + PlayerbotAI* botAI) { return new KaelthasSunstriderBreakThroughShockBarrierAction(botAI); } + + static Action* kaelthas_sunstrider_spread_out_in_midair( + PlayerbotAI* botAI) { return new KaelthasSunstriderSpreadOutInMidairAction(botAI); } +}; + +#endif diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp new file mode 100644 index 00000000000..9541570a249 --- /dev/null +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp @@ -0,0 +1,2286 @@ +#include "RaidTempestKeepActions.h" +#include "RaidTempestKeepHelpers.h" +#include "RaidTempestKeepKaelthasBossAI.h" +#include "AiFactory.h" +#include "LootAction.h" +#include "LootObjectStack.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "SharedDefines.h" + +using namespace TempestKeepHelpers; + +// Trash + +bool CrimsonHandCenturionCastPolymorphAction::Execute(Event event) +{ + Unit* centurion = AI_VALUE2(Unit*, "find target", "crimson hand centurion"); + if (!centurion) + return false; + + if (centurion->GetHealth() == centurion->GetMaxHealth() && + !centurion->HasAura(SPELL_POLYMORPH_SHEEP) && + !centurion->HasAura(SPELL_POLYMORPH_TURTLE) && + !centurion->HasAura(SPELL_POLYMORPH_PIG)) + { + return botAI->CastSpell("polymorph", centurion); + } + else if (botAI->CanCastSpell("polymorph", centurion)) + return botAI->CastSpell("polymorph", centurion); + + return false; +} + +// Al'ar + +bool AlarMisdirectBossToMainTankAction::Execute(Event event) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + Player* mainTank = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + } + + if (mainTank && botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", alar)) + return botAI->CastSpell("steady shot", alar); + + return false; +} + +bool AlarBossTanksMoveBetweenPlatformsAction::Execute(Event event) +{ + if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + MarkTargetWithStar(bot, alar); + SetRtiTarget(botAI, "star", alar); + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + bool mtAction = PositionMainTank(botAI->IsMainTank(bot) + ? bot : nullptr, alar, locationIndex); + bool atAction = PositionAssistTank(botAI->IsAssistTankOfIndex(bot, 0) + ? bot : nullptr, alar, locationIndex); + + return mtAction || atAction; +} + +bool AlarBossTanksMoveBetweenPlatformsAction::PositionMainTank( + Player* mainTank, Unit* alar, int8 locationIndex) +{ + if (!mainTank) + return false; + + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& target = + (locationIndex == PLATFORM_0_IDX || locationIndex == PLATFORM_3_IDX) + ? PLATFORM_POSITIONS[0] : PLATFORM_POSITIONS[2]; + + if (mainTank->GetExactDist2d(target.GetPositionX(), target.GetPositionY()) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, target.GetPositionX(), target.GetPositionY(), + target.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + else if (mainTank->GetTarget() != alar->GetGUID()) + { + return Attack(alar); + } + else if (alar->GetVictim() != mainTank) + { + const char* taunts[] = { "taunt", "growl", "hand of reckoning", "dark command" }; + for (const char* spellName : taunts) + { + if (botAI->CanCastSpell(spellName, alar)) + return botAI->CastSpell(spellName, alar); + } + } + } + + return false; +} + +bool AlarBossTanksMoveBetweenPlatformsAction::PositionAssistTank( + Player* assistTank, Unit* alar, int8 locationIndex) +{ + if (!assistTank) + return false; + + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& target = + (locationIndex == PLATFORM_0_IDX || locationIndex == PLATFORM_1_IDX) + ? PLATFORM_POSITIONS[1] : PLATFORM_POSITIONS[3]; + + if (assistTank->GetExactDist2d(target.GetPositionX(), target.GetPositionY()) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, target.GetPositionX(), target.GetPositionY(), + target.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + else if (assistTank->GetTarget() != alar->GetGUID()) + { + return Attack(alar); + } + else if (alar->GetVictim() != assistTank) + { + const char* taunts[] = { "taunt", "growl", "hand of reckoning", "dark command" }; + for (const char* spellName : taunts) + { + if (botAI->CanCastSpell(spellName, alar)) + return botAI->CastSpell(spellName, alar); + } + } + } + + return false; +} + +bool AlarMeleeDpsMoveBetweenPlatformsAction::Execute(Event event) +{ + if (!botAI->IsMelee(bot) || !botAI->IsDps(bot)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + SetRtiTarget(botAI, "star", alar); + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& target = PLATFORM_POSITIONS[locationIndex]; + + if (bot->GetExactDist2d(target.GetPositionX(), target.GetPositionY()) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, target.GetPositionX(), target.GetPositionY(), + target.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + if (bot->GetTarget() != alar->GetGUID()) + return Attack(alar); + } + + return false; +} + +bool AlarRangedAndEmberTankMoveUnderPlatformsAction::Execute(Event event) +{ + if (!botAI->IsRanged(bot) && !botAI->IsAssistTankOfIndex(bot, 1)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position groundPositions[] = + {ALAR_GROUND_0, ALAR_GROUND_1, ALAR_GROUND_2, ALAR_GROUND_3}; + const Position& groundTarget = groundPositions[locationIndex]; + + if (botAI->IsRanged(bot)) + { + if (bot->GetExactDist2d( + groundTarget.GetPositionX(), groundTarget.GetPositionY()) > 8.0f) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, groundTarget.GetPositionX(), + groundTarget.GetPositionY(), groundTarget.GetPositionZ(), + 8.0f, MovementPriority::MOVEMENT_COMBAT); + } + } + else if (botAI->IsAssistTankOfIndex(bot, 1)) + { + Unit* ember = AI_VALUE2(Unit*, "find target", "ember of al'ar"); + if (!ember && bot->GetExactDist2d( + groundTarget.GetPositionX(), groundTarget.GetPositionY()) > 20.0f) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, groundTarget.GetPositionX(), + groundTarget.GetPositionY(), groundTarget.GetPositionZ(), + 20.0f, MovementPriority::MOVEMENT_COMBAT); + } + } + } + + return false; +} + +bool AlarAssistTanksPickUpEmbersAction::Execute(Event event) +{ + if (!botAI->IsTank(bot)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (!isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return HandlePhase1Embers(alar); + else + return HandlePhase2Embers(alar); + + return false; +} + +// Embers will be tanked by only the second assist tank in Phase 1 +bool AlarAssistTanksPickUpEmbersAction::HandlePhase1Embers(Unit* alar) +{ + if (!botAI->IsAssistTankOfIndex(bot, 1)) + return false; + + Unit* ember = AI_VALUE2(Unit*, "find target", "ember of al'ar"); + if (ember) + { + MarkTargetWithSquare(bot, ember); + SetRtiTarget(botAI, "square", ember); + + if (bot->GetTarget() != ember->GetGUID()) + return Attack(ember); + + if (ember->GetVictim() == bot) + { + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& groundTarget = GROUND_POSITIONS[locationIndex]; + const Position& center = ALAR_POINT_MIDDLE; + float dx = center.GetPositionX() - groundTarget.GetPositionX(); + float dy = center.GetPositionY() - groundTarget.GetPositionY(); + float distToCenter = groundTarget.GetExactDist2d(center.GetPositionX(), center.GetPositionY()); + + float moveDist = 25.0f; + float targetX = groundTarget.GetPositionX() + (dx / distToCenter) * moveDist; + float targetY = groundTarget.GetPositionY() + (dy / distToCenter) * moveDist; + float targetZ = groundTarget.GetPositionZ(); + + return MoveTo(TEMPEST_KEEP_MAP_ID, targetX, targetY, targetZ, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + else + { + const float safeDistance = 15.0f; + Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance); + if (nearestPlayer) + return MoveFromGroup(safeDistance + 1.0f); + } + } + else if (!bot->IsWithinMeleeRange(ember)) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, ember->GetPositionX(), ember->GetPositionY(), + ember->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +// One Ember will be tanked by the second assist tank in Phase 2, and the other by +// the main tank or first assist tank (whichever is not tanking Al'ar) +bool AlarAssistTanksPickUpEmbersAction::HandlePhase2Embers(Unit* alar) +{ + auto [firstEmber, secondEmber] = GetFirstTwoEmbersOfAlar(botAI); + + if (botAI->IsAssistTankOfIndex(bot, 1) && firstEmber) + { + MarkTargetWithSquare(bot, firstEmber); + SetRtiTarget(botAI, "square", firstEmber); + + if (firstEmber->GetVictim() != bot) + { + if (bot->GetTarget() != firstEmber->GetGUID()) + return Attack(firstEmber); + + const char* taunts[] = { "taunt", "growl", "hand of reckoning", "dark command" }; + for (const char* spellName : taunts) + { + if (botAI->CanCastSpell(spellName, firstEmber)) + return botAI->CastSpell(spellName, firstEmber); + } + } + else if (bot->IsWithinMeleeRange(firstEmber)) + { + const float safeDistance = 15.0f; + Unit* nearestPlayer = GetNearestNonTankPlayerInRadius(bot, safeDistance); + if (nearestPlayer) + return MoveFromGroup(safeDistance + 1.0f); + } + } + else + { + Player* secondEmberTank = GetSecondEmberTank(botAI, alar); + if (secondEmberTank && bot == secondEmberTank && secondEmber) + { + MarkTargetWithCircle(bot, secondEmber); + SetRtiTarget(botAI, "circle", secondEmber); + + if (secondEmber->GetVictim() != bot) + { + if (bot->GetTarget() != secondEmber->GetGUID()) + return Attack(secondEmber); + + const char* taunts[] = { "taunt", "growl", "hand of reckoning", "dark command" }; + for (const char* spellName : taunts) + { + if (botAI->CanCastSpell(spellName, secondEmber)) + return botAI->CastSpell(spellName, secondEmber); + } + } + else if (bot->IsWithinMeleeRange(secondEmber)) + { + const float safeDistance = 15.0f; + Unit* nearestPlayer = GetNearestNonTankPlayerInRadius(bot, safeDistance); + if (nearestPlayer) + return MoveFromGroup(safeDistance + 1.0f); + } + } + } + + return false; +} + +bool AlarRangedDpsPrioritizeEmbersAction::Execute(Event event) +{ + if (!botAI->IsRangedDps(bot)) + return false; + + auto [firstEmber, secondEmber] = GetFirstTwoEmbersOfAlar(botAI); + + const float safeDistance = 15.0f; + if (firstEmber) + { + if (bot->GetDistance2d(firstEmber) < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(firstEmber, safeDistance - bot->GetDistance2d(firstEmber)); + } + SetRtiTarget(botAI, "square", firstEmber); + if (bot->GetTarget() != firstEmber->GetGUID()) + return Attack(firstEmber); + } + else if (secondEmber) + { + if (bot->GetDistance2d(secondEmber) < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(secondEmber, safeDistance - bot->GetDistance2d(secondEmber)); + } + SetRtiTarget(botAI, "circle", secondEmber); + if (bot->GetTarget() != secondEmber->GetGUID()) + return Attack(secondEmber); + } + else if (Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar")) + { + SetRtiTarget(botAI, "star", alar); + if (bot->GetTarget() != alar->GetGUID()) + return Attack(alar); + } + + return false; +} + +// Jump from platform during Flame Quills and wait at assigned position after landing +bool AlarJumpFromPlatformAction::Execute(Event event) +{ + if (bot->GetPositionZ() > ALAR_BALCONY_Z) + { + int8 closestPlatform; + Position ground; + GetClosestPlatformAndGround(bot->GetPosition(), closestPlatform, ground); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return JumpTo(TEMPEST_KEEP_MAP_ID, ground.GetPositionX(), ground.GetPositionY(), + ground.GetPositionZ(), MovementPriority::MOVEMENT_FORCED); + } + else if (botAI->IsMainTank(bot)) + { + if (bot->GetExactDist2d(ALAR_SW_RAMP_BASE.GetPositionX(), ALAR_SW_RAMP_BASE.GetPositionY()) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, ALAR_SW_RAMP_BASE.GetPositionX(), ALAR_SW_RAMP_BASE.GetPositionY(), + ALAR_SW_RAMP_BASE.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + else if (botAI->IsAssistTankOfIndex(bot, 0)) + { + if (bot->GetExactDist2d(ALAR_SE_RAMP_BASE.GetPositionX(), ALAR_SE_RAMP_BASE.GetPositionY()) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, ALAR_SE_RAMP_BASE.GetPositionX(), ALAR_SE_RAMP_BASE.GetPositionY(), + ALAR_SE_RAMP_BASE.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + else if (botAI->IsAssistTankOfIndex(bot, 1)) + { + if (bot->GetExactDist2d(ALAR_POINT_MIDDLE.GetPositionX(), ALAR_POINT_MIDDLE.GetPositionY()) > 25.0f) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, ALAR_POINT_MIDDLE.GetPositionX(), ALAR_POINT_MIDDLE.GetPositionY(), + ALAR_POINT_MIDDLE.GetPositionZ(), 25.0f, MovementPriority::MOVEMENT_FORCED); + } + } + else if (botAI->IsMelee(bot)) + { + if (bot->GetExactDist2d(ALAR_ROOM_S_CENTER.GetPositionX(), ALAR_ROOM_S_CENTER.GetPositionY()) > 5.0f) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, ALAR_ROOM_S_CENTER.GetPositionX(), ALAR_ROOM_S_CENTER.GetPositionY(), + ALAR_ROOM_S_CENTER.GetPositionZ(), 5.0f, MovementPriority::MOVEMENT_FORCED); + } + } + else if (botAI->IsRanged(bot)) + { + if (bot->GetExactDist2d(ALAR_POINT_MIDDLE.GetPositionX(), ALAR_POINT_MIDDLE.GetPositionY()) > 10.0f) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, ALAR_POINT_MIDDLE.GetPositionX(), ALAR_POINT_MIDDLE.GetPositionY(), + ALAR_POINT_MIDDLE.GetPositionZ(), 10.0f, MovementPriority::MOVEMENT_FORCED); + } + } + + return false; +} + +bool AlarMoveAwayFromRebirthAction::Execute(Event event) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (bot->GetPositionZ() > ALAR_BALCONY_Z) + { + int8 closestPlatform; + Position ground; + GetClosestPlatformAndGround(bot->GetPosition(), closestPlatform, ground); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return JumpTo(TEMPEST_KEEP_MAP_ID, ground.GetPositionX(), ground.GetPositionY(), + ground.GetPositionZ(), MovementPriority::MOVEMENT_FORCED); + } + + float currentDistance = bot->GetDistance2d(alar); + const float safeDistance = 20.0f; + if (currentDistance < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(alar, safeDistance - currentDistance); + } + + return false; +} + +// Main tank and first assist tank will swap tanking Al'ar when Melt Armor is applied +bool AlarSwapTanksOnBossAction::Execute(Event event) +{ + if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + Player* mainTank = nullptr; + Player* assistTank = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member) + continue; + + if (botAI->IsMainTank(member)) + mainTank = member; + + if (botAI->IsAssistTankOfIndex(member, 0)) + assistTank = member; + } + } + + if (alar->GetHealth() == alar->GetMaxHealth()) + { + SetRtiTarget(botAI, "star", alar); + + if (bot->GetTarget() != alar->GetGUID()) + return Attack(alar); + } + + if (mainTank && assistTank && alar->GetVictim() == mainTank && + mainTank->HasAura(SPELL_MELT_ARMOR) && bot == assistTank) + { + SetRtiTarget(botAI, "star", alar); + + const char* taunts[] = { "taunt", "growl", "hand of reckoning", "dark command" }; + for (const char* spellName : taunts) + { + if (botAI->CanCastSpell(spellName, alar)) + return botAI->CastSpell(spellName, alar); + } + + if (bot->GetTarget() != alar->GetGUID()) + return Attack(alar); + } + + if (mainTank && assistTank && alar->GetVictim() == assistTank && + assistTank->HasAura(SPELL_MELT_ARMOR) && bot == mainTank) + { + SetRtiTarget(botAI, "star", alar); + + const char* taunts[] = { "taunt", "growl", "hand of reckoning", "dark command" }; + for (const char* spellName : taunts) + { + if (botAI->CanCastSpell(spellName, alar)) + return botAI->CastSpell(spellName, alar); + } + + if (bot->GetTarget() != alar->GetGUID()) + return Attack(alar); + } + + return false; +} + +bool AlarAvoidFlamePatchesAndDiveBombsAction::Execute(Event event) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (AvoidFlamePatch()) + return true; + + if (HandleDiveBomb(alar)) + return true; + + return false; +} + +bool AlarAvoidFlamePatchesAndDiveBombsAction::AvoidFlamePatch() +{ + std::vector flamePatches = GetAllHazardTriggers(botAI, bot, NPC_FLAME_PATCH, 40.0f); + const float hazardRadius = 8.0f; + for (Unit* flamePatch : flamePatches) + { + if (bot->GetExactDist2d(flamePatch) < hazardRadius) + { + Position safestPos = FindSafestNearbyPosition(bot, flamePatches, 30.0f, hazardRadius); + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(TEMPEST_KEEP_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), + safestPos.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +bool AlarAvoidFlamePatchesAndDiveBombsAction::HandleDiveBomb(Unit* alar) +{ + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (locationIndex == POINT_QUILL_OR_DIVE_IDX) + { + Unit* nearestPlayer = GetNearestPlayerInRadius(bot, 10.0f); + if (nearestPlayer) + { + return FleePosition(Position(nearestPlayer->GetPositionX(), nearestPlayer->GetPositionY(), + nearestPlayer->GetPositionZ()), 11.0f); + } + } + else if (alar->HasUnitState(UNIT_STATE_CASTING) && + alar->FindCurrentSpellBySpellId(SPELL_REBIRTH_DIVE)) + { + float currentDistance = bot->GetDistance2d(alar); + const float safeDistance = 20.0f; + if (currentDistance < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(alar, safeDistance - currentDistance); + } + } + + return false; +} + +// For Phase 2, ensure that bots don't get too far away and become inactive +bool AlarReturnToRoomCenterAction::Execute(Event event) +{ + const Position& center = ALAR_ROOM_CENTER; + if (bot->GetVictim() == nullptr && + bot->GetExactDist2d(center.GetPositionX(), center.GetPositionY()) > 50.0f) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, center.GetPositionX(), center.GetPositionY(), + center.GetPositionZ(), 50.0f, MovementPriority::MOVEMENT_COMBAT); + } + + return false; +} + +bool AlarManagePhaseTrackerAction::Execute(Event event) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + const uint32 instanceId = alar->GetMap()->GetInstanceId(); + + if (alar->GetHealthPct() > 99.5f && alar->GetPositionZ() >= ALAR_BALCONY_Z) + isAlarInPhase2[instanceId] = false; + + bool rebirthActive = alar->HasUnitState(UNIT_STATE_CASTING) && + alar->FindCurrentSpellBySpellId(SPELL_REBIRTH_PHASE2); + bool lastRebirth = lastRebirthState[instanceId]; + + if (lastRebirth && !rebirthActive) + isAlarInPhase2[instanceId] = true; + + lastRebirthState[instanceId] = rebirthActive; + + return false; +} + +// Void Reaver + +bool VoidReaverTanksPositionBossAction::Execute(Event event) +{ + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (!voidReaver) + return false; + + const Position& tankPosition = VOID_REAVER_TANK_POSITION; + + float dX = tankPosition.GetPositionX() - bot->GetPositionX(); + float dY = tankPosition.GetPositionY() - bot->GetPositionY(); + float distanceToTankPosition = bot->GetExactDist2d(tankPosition.GetPositionX(), + tankPosition.GetPositionY()); + + if (bot->IsWithinMeleeRange(voidReaver) && distanceToTankPosition > 2.0f) + { + float moveDist = std::min(5.0f, distanceToTankPosition); + float moveX = bot->GetPositionX() + (dX / distanceToTankPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distanceToTankPosition) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, tankPosition.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_FORCED, true, true); + } + + return false; +} + +bool VoidReaverRangedUseAggroDumpAbilityAction::Execute(Event event) +{ + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (!voidReaver) + return false; + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + static const std::array spells = + { + "divine protection", + "fade", + "feign death", + "ice block", + "soulshatter", + "vanish", + }; + + for (const char* spell : spells) + { + if (botAI->CanCastSpell(spell, bot)) + return botAI->CastSpell(spell, bot); + } + + return false; +} + +// As far as I can tell, it is not possible for bots to detect Arcane Orbs +// Therefore, this spreads out the ranged bots so as few of them as possible get hit +bool VoidReaverSpreadRangedAction::Execute(Event event) +{ + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (!voidReaver) + return false; + + if (voidReaver->GetHealth() == voidReaver->GetMaxHealth()) + { + initialVoidReaverPositions.clear(); + hasReachedInitialVoidReaverPosition.clear(); + } + + if (initialVoidReaverPositions.empty()) + { + std::vector healers; + std::vector rangedDps; + 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; + if (botAI->IsHeal(member)) + healers.push_back(member); + else + rangedDps.push_back(member); + } + } + + const Position& tankPosition = VOID_REAVER_TANK_POSITION; + const float radius = 30.0f; + const float offsetArc = 1.0f; + const uint8 botsPerRing = 8; + + std::vector rangedBots = healers; + rangedBots.insert(rangedBots.end(), rangedDps.begin(), rangedDps.end()); + + for (size_t i = 0; i < rangedBots.size(); ++i) + { + Player* ranged = rangedBots[i]; + uint8 ringIndex = i / botsPerRing; + uint8 posInRing = i % botsPerRing; + float ringRadius = radius + (ringIndex * offsetArc); + float angle = 2 * M_PI * posInRing / botsPerRing; + + float targetX = tankPosition.GetPositionX() + ringRadius * std::cos(angle); + float targetY = tankPosition.GetPositionY() + ringRadius * std::sin(angle); + + Position pos(targetX, targetY, ranged->GetPositionZ()); + initialVoidReaverPositions[ranged->GetGUID()] = pos; + hasReachedInitialVoidReaverPosition[ranged->GetGUID()] = false; + } + } + + Position targetPosition = initialVoidReaverPositions[bot->GetGUID()]; + float destX = targetPosition.GetPositionX(); + float destY = targetPosition.GetPositionY(); + + if (bot->GetExactDist2d(destX, destY) > 1.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, destX, destY, targetPosition.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +Position VoidReaverSpreadRangedAction::GetRangedBotPosition(const Position& center, + float radius, uint8 botsPerRing, float offsetArc, uint8 botIndex, float botZ) +{ + float angleOffset = (offsetArc / radius); + uint8 ringIndex = botIndex / botsPerRing; + uint8 posInRing = botIndex % botsPerRing; + float baseAngle = 2 * M_PI * posInRing / botsPerRing; + float angle = baseAngle + (ringIndex == 1 ? angleOffset : 0); + + if (ringIndex > 1) + { + angle = 2 * M_PI * (botIndex % botsPerRing) / botsPerRing; + ringIndex = 0; + } + + float targetX = center.GetPositionX() + radius * cos(angle); + float targetY = center.GetPositionY() + radius * sin(angle); + + return Position(targetX, targetY, botZ); +} + +// High Astromancer Solarian + +bool HighAstromancerSolarianRangedLeaveSpaceForMeleeAction::Execute(Event event) +{ + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + if (!astromancer) + return false; + + float currentDistance = bot->GetExactDist2d(astromancer); + const float minDistance = 20.0f; + if (currentDistance < minDistance) + return MoveAway(astromancer, minDistance - currentDistance + 2.0f); + + return false; +} + +bool HighAstromancerSolarianMoveAwayFromGroupAction::Execute(Event event) +{ + const float safeDistance = 15.0f; + Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance); + if (nearestPlayer) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveFromGroup(safeDistance + 1.0f); + } + + return false; +} + +bool HighAstromancerSolarianStackForAoeAction::Execute(Event event) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* stackTarget = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsRanged(member)) + { + stackTarget = member; + break; + } + } + + if (stackTarget && bot != stackTarget && bot->GetExactDist2d(stackTarget) >= 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, stackTarget->GetPositionX(), stackTarget->GetPositionY(), + stackTarget->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +// Split melee into two groups, one on each Solarium Priest +bool HighAstromancerSolarianTargetSolariumPriestsAction::Execute(Event event) +{ + Unit* solariumPriest = AI_VALUE2(Unit*, "find target", "solarium priest"); + if (!solariumPriest) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + auto solariumPriests = GetSolariumPriests(); + auto meleeMembers = GetMeleeBots(group); + Unit* targetPriest = AssignSolariumPriestsToBots(solariumPriests, meleeMembers); + if (!targetPriest) + return false; + + auto it = std::find(meleeMembers.begin(), meleeMembers.end(), bot); + size_t botIndex = std::distance(meleeMembers.begin(), it); + size_t totalMelee = meleeMembers.size(); + if (botIndex < totalMelee / 2) + { + MarkTargetWithSquare(bot, targetPriest); + SetRtiTarget(botAI, "square", targetPriest); + } + else + { + MarkTargetWithStar(bot, targetPriest); + SetRtiTarget(botAI, "star", targetPriest); + } + + if (bot->GetTarget() != targetPriest->GetGUID()) + return Attack(targetPriest); + + return false; +} + +std::vector HighAstromancerSolarianTargetSolariumPriestsAction::GetSolariumPriests() +{ + std::vector solariumPriests; + auto const& npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); + for (auto const& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->GetEntry() == NPC_SOLARIUM_PRIEST && unit->IsAlive()) + solariumPriests.push_back(unit); + } + + return solariumPriests; +} + +std::vector HighAstromancerSolarianTargetSolariumPriestsAction::GetMeleeBots(Group* group) +{ + std::vector meleeMembers; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsMelee(member) && GET_PLAYERBOT_AI(member)) + meleeMembers.push_back(member); + } + + std::sort(meleeMembers.begin(), meleeMembers.end(), + [](Player* left, Player* right) { return left->GetGUID() < right->GetGUID(); }); + + return meleeMembers; +} + +Unit* HighAstromancerSolarianTargetSolariumPriestsAction::AssignSolariumPriestsToBots( + const std::vector& solariumPriests, const std::vector& meleeMembers) +{ + if (solariumPriests.size() < 2 || meleeMembers.empty()) + return nullptr; + + std::vector sortedPriests = solariumPriests; + std::sort(sortedPriests.begin(), sortedPriests.end(), + [](Unit* left, Unit* right) { return left->GetGUID() < right->GetGUID(); }); + + auto it = std::find(meleeMembers.begin(), meleeMembers.end(), bot); + if (it == meleeMembers.end()) + return nullptr; + + size_t botIndex = std::distance(meleeMembers.begin(), it); + size_t totalMelee = meleeMembers.size(); + + if (botIndex < totalMelee / 2) + return sortedPriests[0]; + else + return sortedPriests[1]; +} + +bool HighAstromancerSolarianTankVoidwalkerAction::Execute(Event event) +{ + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + if (!astromancer) + return false; + + if (bot->GetTarget() != astromancer->GetGUID()) + return Attack(astromancer); + + if (astromancer->GetVictim() != bot) + { + const char* taunts[] = { "taunt", "growl", "hand of reckoning", "dark command" }; + for (const char* spellName : taunts) + { + if (botAI->CanCastSpell(spellName, astromancer)) + return botAI->CastSpell(spellName, astromancer); + } + } + + return false; +} + +bool HighAstromancerSolarianCastFearWardOnMainTankAction::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 && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + } + + if (mainTank && botAI->CanCastSpell("fear ward", mainTank)) + return botAI->CastSpell("fear ward", mainTank); + + return false; +} + +// Kael'thas Sunstrider + +bool KaelthasSunstriderKiteThaladredAction::Execute(Event event) +{ + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + if (!thaladred) + return false; + + float currentDistance = bot->GetExactDist2d(thaladred); + const float safeDistance = 25.0f; + if (currentDistance < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(thaladred, safeDistance - currentDistance + 5.0f); + } + + return false; +} + +// Misdirect order: (1) Capernian, (2) Telonicus, (3) Capernian (again for good measure) +bool KaelthasSunstriderMisdirectAdvisorsToTanksAction::Execute(Event event) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector hunters; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && member->getClass() == CLASS_HUNTER && GET_PLAYERBOT_AI(member)) + hunters.push_back(member); + + if (hunters.size() >= 3) + break; + } + + int8 hunterIndex = -1; + for (size_t i = 0; i < hunters.size(); ++i) + { + if (hunters[i] == bot) + { + hunterIndex = static_cast(i); + break; + } + } + if (hunterIndex == -1) + return false; + + Unit* advisorTarget = nullptr; + Player* tankTarget = nullptr; + if (hunterIndex == 0) + { + advisorTarget = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + tankTarget = GetCapernianTank(botAI, bot); + } + else if (hunterIndex == 1) + { + advisorTarget = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && GET_PLAYERBOT_AI(member)->IsAssistTankOfIndex(member, 0)) + { + tankTarget = member; + break; + } + } + } + else if (hunterIndex == 2) + { + advisorTarget = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + tankTarget = GetCapernianTank(botAI, bot); + } + + if (!advisorTarget || + advisorTarget->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || + advisorTarget->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE) || + advisorTarget->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + return false; + + if (!tankTarget || !tankTarget->IsAlive()) + return false; + + if (botAI->CanCastSpell("misdirection", tankTarget)) + return botAI->CastSpell("misdirection", tankTarget); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", advisorTarget)) + return botAI->CastSpell("steady shot", advisorTarget); + + return false; +} + +bool KaelthasSunstriderMainTankPositionSanguinarAction::Execute(Event event) +{ + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + if (!sanguinar) + return false; + + MarkTargetWithStar(bot, sanguinar); + SetRtiTarget(botAI, "star", sanguinar); + + if (bot->GetTarget() != sanguinar->GetGUID()) + return Attack(sanguinar); + + if (sanguinar->GetVictim() == bot && bot->IsWithinMeleeRange(sanguinar)) + { + const Position& position = SANGUINAR_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +bool KaelthasSunstriderCastFearWardOnSanguinarTankAction::Execute(Event event) +{ + Player* mainTank = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + } + + if (mainTank && botAI->CanCastSpell("fear ward", mainTank)) + return botAI->CastSpell("fear ward", mainTank); + + return false; +} + +// Use tank strategy only when necessary to tank Capernian; otherwise, use DPS strategies +bool KaelthasSunstriderManageWarlockTankStrategyAction::Execute(Event event) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return false; + + bool currentlyTank = botAI->HasStrategy("tank", BotState::BOT_STATE_COMBAT); + + // Phase 1: Switch to tank after Sanguinar is "dead" (feign death) + if (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR) + { + if (!currentlyTank) + { + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + if (sanguinar && sanguinar->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + botAI->ChangeStrategy("+tank", BotState::BOT_STATE_COMBAT); + } + return false; + } + + // Phase 2: Switch to DPS + if (kaelAI->GetPhase() == PHASE_WEAPONS) + { + if (currentlyTank) + botAI->ResetStrategies(false); + return false; + } + + // Phase 2→3 Transition: Weapons dead, waiting for advisors - switch to tank + if (kaelAI->GetPhase() == PHASE_TRANSITION) + { + if (!currentlyTank) + botAI->ChangeStrategy("+tank", BotState::BOT_STATE_COMBAT); + return false; + } + + // Phase 3 + if (kaelAI->GetPhase() == PHASE_ALL_ADVISORS) + { + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + + // If Capernian is alive, switch to tank strategy + // (fallback in case not all weapons were down before Phase 3) + if (capernian && !currentlyTank) + botAI->ChangeStrategy("+tank", BotState::BOT_STATE_COMBAT); + // If Capernian is dead, reset to DPS for remainder of encounter + else if (!capernian && currentlyTank) + botAI->ResetStrategies(false); + } + + return false; +} + +bool KaelthasSunstriderWarlockTankPositionCapernianAction::Execute(Event event) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return false; + + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (!capernian) + return false; + + MarkTargetWithCircle(bot, capernian); + SetRtiTarget(botAI, "circle", capernian); + + if (bot->GetTarget() != capernian->GetGUID()) + return Attack(capernian); + + if (capernian->GetVictim() == bot && kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR) + { + float currentDist = bot->GetDistance2d(capernian); + if (currentDist == 0.0f) + return false; + + const float minDistance = 20.0f; + if (currentDist < minDistance) + return MoveAway(capernian, minDistance - currentDist + 1.0f); + } + + return false; +} + +bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::Execute(Event event) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return false; + + if (botAI->IsRanged(bot)) + { + if (RangedBotsDisperse(kaelAI)) + return true; + } + + if (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR) + { + if (StayBackFromCapernian()) + return true; + } + + return false; +} + +bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::RangedBotsDisperse(boss_kaelthas* kaelAI) +{ + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + if (kaelAI->GetPhase() == PHASE_ALL_ADVISORS) + { + if (thaladred) + return false; + } + + const float safeDistance = 6.0f; + Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance); + if (nearestPlayer) + { + const uint32 minInterval = 1000; + return FleePosition(Position(nearestPlayer->GetPositionX(), nearestPlayer->GetPositionY(), + nearestPlayer->GetPositionZ()), safeDistance, minInterval); + } + + return false; +} + +bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::StayBackFromCapernian() +{ + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (!capernian) + return false; + + // Main tank purposely stays in range to bait Conflagration in Phase 1 + if (botAI->IsMainTank(bot)) + { + const float desiredDist = 15.0f; + const float tolerance = 2.0f; + + if (fabs(bot->GetExactDist2d(capernian) - desiredDist) > tolerance) + { + float dx = bot->GetPositionX() - capernian->GetPositionX(); + float dy = bot->GetPositionY() - capernian->GetPositionY(); + float distanceToCapernian = bot->GetExactDist2d(capernian); + if (distanceToCapernian == 0.0f) + return false; + + float nx = dx / distanceToCapernian; + float ny = dy / distanceToCapernian; + + float targetX = capernian->GetPositionX() + nx * desiredDist; + float targetY = capernian->GetPositionY() + ny * desiredDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, targetX, targetY, capernian->GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return true; + } + + float safeDistance; + if (botAI->IsMelee(bot)) + safeDistance = 45.0f; + else if (botAI->IsRangedDps(bot)) + safeDistance = 25.0f; + else if (botAI->IsHeal(bot)) + safeDistance = 40.0f; + + float currentDistance = bot->GetExactDist2d(capernian); + if (currentDistance < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(capernian, safeDistance - currentDistance + 1.0f); + } + + if (botAI->IsMelee(bot)) + { + bot->SetTarget(ObjectGuid::Empty); + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return true; + } + + return false; +} + +bool KaelthasSunstriderFirstAssistTankPositionTelonicusAction::Execute(Event event) +{ + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + if (!telonicus) + return false; + + MarkTargetWithTriangle(bot, telonicus); + SetRtiTarget(botAI, "triangle", telonicus); + + if (bot->GetTarget() != telonicus->GetGUID()) + return Attack(telonicus); + + if (telonicus->GetVictim() == bot && bot->IsWithinMeleeRange(telonicus)) + { + const Position& position = TELONICUS_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +bool KaelthasSunstriderPositionPhase3TankHealerAction::Execute(Event event) +{ + const Position& position = ADVISOR_HEAL_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(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +bool KaelthasSunstriderReequipGearAction::Execute(Event event) +{ + return botAI->DoSpecificAction("equip upgrades", Event(), true); +} + +bool KaelthasSunstriderAssignAdvisorDpsPriorityAction::Execute(Event event) +{ + // Target priority 1: Thaladred, except Capernian tank + Player* capernianTank = GetCapernianTank(botAI, bot); + bool isCapernianTank = capernianTank && bot == capernianTank; + + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + if ((!capernianTank || bot != capernianTank) && thaladred && thaladred->IsAlive() && + !thaladred->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && !thaladred->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + { + MarkTargetWithSquare(bot, thaladred); + SetRtiTarget(botAI, "square", thaladred); + + if (bot->GetTarget() != thaladred->GetGUID()) + return Attack(thaladred); + + return false; + } + + // Target priority 2: Capernian for ranged only (excluding longbow tank) + Player* longBowTank = GetNetherstrandLongbowTank(botAI, bot); + + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (botAI->IsRangedDps(bot) && (!longBowTank || bot != longBowTank) && capernian && capernian->IsAlive() + && !capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && !capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + { + SetRtiTarget(botAI, "circle", capernian); + + if (bot->GetTarget() != capernian->GetGUID()) + return Attack(capernian); + + return false; + } + + // Target priority 3: Sanguinar (longbow tank and melee move here after Thaladred) + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + if (sanguinar && sanguinar->IsAlive() && + !sanguinar->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && !sanguinar->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + { + SetRtiTarget(botAI, "star", sanguinar); + + if (bot->GetTarget() != sanguinar->GetGUID()) + return Attack(sanguinar); + + return false; + } + + // Target priority 4: Telonicus + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + if (telonicus && telonicus->IsAlive() && + !telonicus->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && !telonicus->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + { + SetRtiTarget(botAI, "triangle", telonicus); + + if (bot->GetTarget() != telonicus->GetGUID()) + return Attack(telonicus); + + // Melee DPS need to stay at max-ish melee range behind Telonicus (god damn bombs) + if (botAI->IsMelee(bot) && botAI->IsDps(bot) && telonicus->GetVictim() != bot) + { + float maxMeleeRange = bot->GetMeleeRange(telonicus); + const float meleeRangeBuffer = 0.5f; + const float tolerance = 0.75f; + + float desiredDist = std::max(2.0f, maxMeleeRange - meleeRangeBuffer); + float currentDist = bot->GetExactDist2d(telonicus); + + if (fabs(currentDist - desiredDist) > tolerance) + { + float behindAngle = Position::NormalizeOrientation(telonicus->GetOrientation() + M_PI); + float targetX = telonicus->GetPositionX() + desiredDist * std::cos(behindAngle); + float targetY = telonicus->GetPositionY() + desiredDist * std::sin(behindAngle); + + if (bot->GetExactDist2d(targetX, targetY) > 1.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, targetX, targetY, telonicus->GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + } + } + } + + return false; +} + +bool KaelthasSunstriderManageAdvisorDpsTimerAction::Execute(Event event) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + const char* advisorNames[] = + { + "grand astromancer capernian", + "master engineer telonicus", + "lord sanguinar" + }; + + for (const char* name : advisorNames) + { + Unit* advisor = AI_VALUE2(Unit*, "find target", name); + if (!advisor) + continue; + + if (advisor->GetHealth() == advisor->GetMaxHealth() && + !advisor->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE)) + { + const time_t now = std::time(nullptr); + advisorDpsWaitTimer.insert_or_assign(kaelthas->GetMap()->GetInstanceId(), now); + return true; + } + } + + return false; +} + +bool KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction::Execute(Event event) +{ + if (botAI->IsMainTank(bot)) + return false; + + Player* longBowTank = GetNetherstrandLongbowTank(botAI, bot); + if (longBowTank && longBowTank == bot) + return false; + + if (botAI->IsAssistTank(bot)) + SetRtiTarget(botAI, "moon", nullptr); + + // Priority 0: Everybody other than the main tank needs to stay away from the axe + // But this applies to assist tanks only after they get aggro on the mace, dagger, or sword + Unit* axe = AI_VALUE2(Unit*, "find target", "devastation"); + Unit* mace = AI_VALUE2(Unit*, "find target", "cosmic infuser"); + Unit* dagger = AI_VALUE2(Unit*, "find target", "infinity blades"); + Unit* sword = AI_VALUE2(Unit*, "find target", "warp slicer"); + if (axe) + { + if (botAI->IsDps(bot) || botAI->IsHeal(bot) || + (botAI->IsAssistTank(bot) && (mace && mace->GetVictim() == bot || + dagger && dagger->GetVictim() == bot || sword && sword->GetVictim() == bot))) + { + const float safeDistance = botAI->IsAssistTank(bot) ? 15.0f : 10.0f; + float currentDistance = bot->GetExactDist2d(axe); + if (currentDistance < safeDistance) + return MoveAway(axe, safeDistance - currentDistance + 1.0f); + } + } + + if (botAI->IsDps(bot)) + { + // Priority 1: Staff of Disintegration (Skull) + if (Unit* staff = AI_VALUE2(Unit*, "find target", "staff of disintegration")) + { + MarkTargetWithSkull(bot, staff); + SetRtiTarget(botAI, "skull", staff); + + if (staff->HasUnitState(UNIT_STATE_CASTING) && + staff->FindCurrentSpellBySpellId(SPELL_STAFF_FROSTBOLT)) + { + if (bot->getClass() == CLASS_MAGE && botAI->CanCastSpell("counterspell", staff)) + return botAI->CastSpell("counterspell", staff); + else if (bot->getClass() == CLASS_SHAMAN && botAI->CanCastSpell("wind shear", staff)) + return botAI->CastSpell("wind shear", staff); + } + + if (bot->GetTarget() != staff->GetGUID()) + return Attack(staff); + + return false; + } + // Priority 2: Cosmic Infuser (Skull) + if (mace) + { + MarkTargetWithSkull(bot, mace); + SetRtiTarget(botAI, "skull", mace); + + if (bot->GetTarget() != mace->GetGUID()) + return Attack(mace); + + return false; + } + // Priority 3: Warp Slicer (Skull) + if (sword) + { + MarkTargetWithSkull(bot, sword); + SetRtiTarget(botAI, "skull", sword); + + if (bot->GetTarget() != sword->GetGUID()) + return Attack(sword); + + return false; + } + // Priority 4: Infinity Blades (Skull) + if (dagger) + { + MarkTargetWithSkull(bot, dagger); + SetRtiTarget(botAI, "skull", dagger); + + if (bot->GetTarget() != dagger->GetGUID()) + return Attack(dagger); + + return false; + } + // Priority 5: Devastation - ranged only (Diamond--marked in other method by main tank) + if (axe && botAI->IsRangedDps(bot)) + { + SetRtiTarget(botAI, "diamond", axe); + + if (bot->GetTarget() != axe->GetGUID()) + return Attack(axe); + + return false; + } + // Priority 6: Netherstrand Longbow (Cross--marked in other method by longbow tank) + if (Unit* longbow = AI_VALUE2(Unit*, "find target", "netherstrand longbow")) + { + SetRtiTarget(botAI, "cross", longbow); + + if (bot->GetTarget() != longbow->GetGUID()) + return Attack(longbow); + + return false; + } + // Priority 7: Phaseshift Bulwark (Skull) + if (Unit* shield = AI_VALUE2(Unit*, "find target", "phaseshift bulwark")) + { + MarkTargetWithSkull(bot, shield); + SetRtiTarget(botAI, "skull", shield); + + if (bot->GetTarget() != shield->GetGUID()) + return Attack(shield); + } + } + + return false; +} + +bool KaelthasSunstriderMoveDevastationAwayAction::Execute(Event event) +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* axe = AI_VALUE2(Unit*, "find target", "devastation"); + if (!axe) + return false; + + MarkTargetWithDiamond(bot, axe); + SetRtiTarget(botAI, "diamond", axe); + + if (bot->GetTarget() != axe->GetGUID()) + return Attack(axe); + + if (axe->GetVictim() == bot) + { + const float safeDistance = 10.0f; + Unit* nearestPlayer = GetNearestNonTankPlayerInRadius(bot, safeDistance); + if (nearestPlayer) + { + float currentDistance = bot->GetExactDist2d(nearestPlayer); + return MoveFromGroup(safeDistance + 1.0f); + } + } + + return false; +} + +bool KaelthasSunstriderHunterTurnAwayNetherstrandLongbowAction::Execute(Event event) +{ + Player* longBowTank = GetNetherstrandLongbowTank(botAI, bot); + if (!longBowTank || longBowTank != bot) + return false; + + Unit* longbow = AI_VALUE2(Unit*, "find target", "netherstrand longbow"); + if (!longbow || !longbow->IsAlive()) + return false; + + MarkTargetWithCross(bot, longbow); + SetRtiTarget(botAI, "cross", longbow); + + if (bot->GetTarget() != longbow->GetGUID()) + return Attack(longbow); + + if (longbow->GetVictim() == bot) + { + const float dangerZone = 15.0f; + Unit* nearestPlayer = GetNearestNonTankPlayerInRadius(bot, dangerZone); + if (!nearestPlayer) + return false; + + float currentDistance = bot->GetExactDist2d(nearestPlayer); + if (currentDistance < dangerZone) + return MoveFromGroup(dangerZone); + } + + return false; +} + +bool KaelthasSunstriderLootLegendaryWeaponsAction::Execute(Event event) +{ + struct WeaponInfo + { + uint32 npcEntry; + uint32 itemId; + const char* name; + }; + + const WeaponInfo weapons[] = + { + { NPC_NETHERSTRAND_LONGBOW, ITEM_NETHERSTRAND_LONGBOW, "netherstrand longbow" }, + { NPC_COSMIC_INFUSER, ITEM_COSMIC_INFUSER, "cosmic infuser" }, + { NPC_DEVASTATION, ITEM_DEVASTATION, "devastation" }, + { NPC_INFINITY_BLADES, ITEM_INFINITY_BLADE, "infinity blade" }, + { NPC_WARP_SLICER, ITEM_WARP_SLICER, "warp slicer" }, + { NPC_STAFF_OF_DISINTEGRATION, ITEM_STAFF_OF_DISINTEGRATION, "staff of disintegration" }, + { NPC_PHASESHIFT_BULWARK, ITEM_PHASESHIFT_BULWARK, "phaseshift bulwark" } + }; + + for (auto const& weapon : weapons) + { + if (ShouldBotLootWeapon(weapon.npcEntry)) + { + if (bot->HasItemCount(weapon.itemId, 1, false)) + continue; + + return LootWeapon(weapon.npcEntry, weapon.itemId, weapon.name); + } + } + + return false; +} + +bool KaelthasSunstriderLootLegendaryWeaponsAction::ShouldBotLootWeapon(uint32 weaponEntry) +{ + uint8 tab = AiFactory::GetPlayerSpecTab(bot); + + switch (weaponEntry) + { + case NPC_NETHERSTRAND_LONGBOW: + return bot->getClass() == CLASS_HUNTER; + + case NPC_COSMIC_INFUSER: + return botAI->IsHeal(bot); + + // Fury Warriors could use the axe, but their DPS is terrible at appropriate gear levels + // So they're better off looting only the dagger to MH it and break MCs + case NPC_DEVASTATION: + return (bot->getClass() == CLASS_WARRIOR && tab == WARRIOR_TAB_ARMS) || + (bot->getClass() == CLASS_PALADIN && tab == PALADIN_TAB_RETRIBUTION) || + (botAI->IsDps(bot) && bot->getClass() == CLASS_DEATH_KNIGHT); + + case NPC_INFINITY_BLADES: + return bot->getClass() == CLASS_ROGUE || + bot->getClass() == CLASS_HUNTER || + (bot->getClass() == CLASS_SHAMAN && tab == SHAMAN_TAB_ENHANCEMENT) || + (bot->getClass() == CLASS_WARRIOR && tab != WARRIOR_TAB_ARMS); + + case NPC_WARP_SLICER: + return bot->getClass() == CLASS_ROGUE && tab != ROGUE_TAB_ASSASSINATION || + (botAI->IsTank(bot) && + (bot->getClass() == CLASS_DEATH_KNIGHT || + bot->getClass() == CLASS_PALADIN)); + + case NPC_STAFF_OF_DISINTEGRATION: + return (botAI->IsRangedDps(bot) && bot->getClass() != CLASS_HUNTER) || + (bot->getClass() == CLASS_DRUID && tab == DRUID_TAB_FERAL); + + case NPC_PHASESHIFT_BULWARK: + return botAI->IsTank(bot) && + (bot->getClass() == CLASS_PALADIN || + bot->getClass() == CLASS_WARRIOR || + bot->getClass() == CLASS_DEATH_KNIGHT); + + default: + return false; + } +} + +bool KaelthasSunstriderLootLegendaryWeaponsAction::LootWeapon( + uint32 weaponEntry, uint32 itemId, const char* weaponName) +{ + 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) + continue; + + if (creature->GetEntry() != weaponEntry || creature->IsAlive()) + continue; + + context->GetValue("loot target")->Set(loot); + + float distToObject = bot->GetDistance(object); + + if (distToObject > maxLootRange) + return MoveTo(object, 2.0f, MovementPriority::MOVEMENT_FORCED); + + OpenLootAction open(botAI); + bool opened = open.Execute(Event()); + + if (!opened) + return opened; + + const ObjectGuid botGuid = bot->GetGUID(); + const ObjectGuid corpseGuid = guid; + const uint8 weaponIndex = 0; + + botAI->AddTimedEvent([this, botGuid, corpseGuid, weaponIndex, itemId, weaponName]() + { + Player* receiver = botGuid.IsEmpty() ? nullptr : ObjectAccessor::FindPlayer(botGuid); + if (!receiver || !receiver->IsInWorld()) + return; + + if (receiver->HasItemCount(itemId, 1, false)) + return; + + receiver->SetLootGUID(corpseGuid); + + WorldPacket* packet = new WorldPacket(CMSG_AUTOSTORE_LOOT_ITEM, 1); + *packet << weaponIndex; + receiver->GetSession()->QueuePacket(packet); + }, 600); + + botAI->DoSpecificAction("equip upgrades", Event(), true); + return true; + } + + return false; +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::Execute(Event event) +{ + if (UsePhaseshiftBulwark()) + return true; + + if (UseStaffOfDisintegration()) + return true; + + if (UseNetherstrandLongbow()) + return true; + + return false; +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::UsePhaseshiftBulwark() +{ + Item* offHand = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND); + if (!offHand || offHand->GetEntry() != ITEM_PHASESHIFT_BULWARK) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (!kaelthas->HasAura(SPELL_SHOCK_BARRIER)) + return false; + + if (bot->HasAura(SPELL_ARCANE_BARRIER)) + return false; + + if (bot->CanUseItem(offHand) != EQUIP_ERR_OK) + return false; + + return UseEquippedItemWithPacket(offHand); +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::UseStaffOfDisintegration() +{ + Item* mainHand = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND); + if (!mainHand || mainHand->GetEntry() != ITEM_STAFF_OF_DISINTEGRATION) + return false; + + if (bot->HasAura(SPELL_MENTAL_PROTECTION_FIELD)) + return false; + + return UseEquippedItemWithPacket(mainHand); +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::UseNetherstrandLongbow() +{ + Item* ranged = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED); + if (!ranged || ranged->GetEntry() != ITEM_NETHERSTRAND_LONGBOW) + return false; + + if (bot->HasItemCount(ITEM_NETHER_SPIKES, 401, false)) + return false; + + return UseEquippedItemWithPacket(ranged); +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::UseEquippedItemWithPacket(Item* item) +{ + if (!item) + return false; + + if (bot->CanUseItem(item) != EQUIP_ERR_OK) + return false; + + if (bot->IsNonMeleeSpellCast(true)) + return false; + + uint8 bagIndex = item->GetBagSlot(); + uint8 slot = item->GetSlot(); + uint8 cast_count = 1; + ObjectGuid item_guid = item->GetGUID(); + uint32 glyphIndex = 0; + uint8 castFlags = 0; + uint32 spellId = 0; + + for (uint8 i = 0; i < MAX_ITEM_PROTO_SPELLS; ++i) + { + if (item->GetTemplate()->Spells[i].SpellId > 0 && + item->GetTemplate()->Spells[i].SpellTrigger == ITEM_SPELLTRIGGER_ON_USE) + { + spellId = item->GetTemplate()->Spells[i].SpellId; + break; + } + } + + if (!spellId) + return false; + + WorldPacket packet(CMSG_USE_ITEM); + packet << bagIndex << slot << cast_count << spellId << item_guid << glyphIndex << castFlags; + + uint32 targetFlag = TARGET_FLAG_UNIT; + packet << targetFlag << bot->GetPackGUID(); + + bot->GetSession()->HandleUseItemOpcode(packet); + return true; +} + +bool KaelthasSunstriderMainTankPositionBossAction::Execute(Event event) +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + MarkTargetWithStar(bot, kaelthas); + SetRtiTarget(botAI, "star", kaelthas); + + if (bot->GetTarget() != kaelthas->GetGUID()) + return Attack(kaelthas); + + if (kaelthas->GetVictim() == bot && bot->IsWithinMeleeRange(kaelthas)) + { + const Position& position = KAELTHAS_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 4.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +bool KaelthasSunstriderAvoidFlameStrikeAction::Execute(Event event) +{ + std::vector flameStrikes = + GetAllHazardTriggers(botAI, bot, NPC_FLAME_STRIKE_TRIGGER, 40.0f); + if (flameStrikes.empty()) + return false; + + const float hazardRadius = 12.0f; + bool inDanger = false; + for (Unit* flameStrike : flameStrikes) + { + if (bot->GetExactDist2d(flameStrike) < hazardRadius) + { + inDanger = true; + break; + } + } + + if (!inDanger) + return false; + + Position safestPos = FindSafestNearbyPosition(bot, flameStrikes, 30.0f, hazardRadius); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(TEMPEST_KEEP_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), + safestPos.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); +} + +bool KaelthasSunstriderRoundUpPhoenixesAndFocusDownEggsAction::Execute(Event event) +{ + if (botAI->IsAssistTankOfIndex(bot, 0) || botAI->IsAssistTankOfIndex(bot, 1)) + { + std::vector phoenixes; + auto const& npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); + for (auto const& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->GetEntry() == NPC_PHOENIX && unit->IsAlive()) + phoenixes.push_back(unit); + } + + if (phoenixes.empty()) + return false; + + std::sort(phoenixes.begin(), phoenixes.end(), + [](Unit* a, Unit* b) { return a->GetGUID() < b->GetGUID(); }); + + Unit* targetPhoenix = nullptr; + if (botAI->IsAssistTankOfIndex(bot, 0)) + { + targetPhoenix = phoenixes[0]; + MarkTargetWithSquare(bot, targetPhoenix); + SetRtiTarget(botAI, "square", targetPhoenix); + } + else if (botAI->IsAssistTankOfIndex(bot, 1) && phoenixes.size() >= 2) + { + targetPhoenix = phoenixes[1]; + MarkTargetWithCircle(bot, targetPhoenix); + SetRtiTarget(botAI, "circle", targetPhoenix); + } + + if (!targetPhoenix) + return false; + + if (bot->GetTarget() != targetPhoenix->GetGUID()) + return Attack(targetPhoenix); + + if (targetPhoenix->GetVictim() == bot) + { + const float safeDistance = 10.0f; + + Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance); + if (Group* group = bot->GetGroup()) + { + if (nearestPlayer && group) + { + PlayerbotAI* nearestAI = GET_PLAYERBOT_AI(nearestPlayer->ToPlayer()); + if (nearestAI && (nearestAI->IsAssistTankOfIndex(nearestPlayer->ToPlayer(), 0) || + nearestAI->IsAssistTankOfIndex(nearestPlayer->ToPlayer(), 1))) + { + nearestPlayer = nullptr; + } + } + } + + if (nearestPlayer) + { + float closestDist = bot->GetExactDist2d(nearestPlayer); + if (closestDist < safeDistance) + return MoveFromGroup(safeDistance + 2.0f); + } + } + + return false; + } + + if (botAI->IsMelee(bot) && botAI->IsDps(bot)) + { + Unit* phoenix = AI_VALUE2(Unit*, "find target", "phoenix"); + if (!phoenix) + return false; + + float currentDistance = bot->GetExactDist2d(phoenix); + const float safeDistance = 10.0f; + if (currentDistance < safeDistance) + return MoveAway(phoenix, safeDistance - currentDistance + 2.0f); + } + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (botAI->IsRangedDps(bot) || + (botAI->IsMelee(bot) && botAI->IsDps(bot) && kaelthas && + kaelthas->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE) && kaelthas->GetHealthPct() <= 50.0f)) + { + Unit* phoenixEgg = AI_VALUE2(Unit*, "find target", "phoenix egg"); + if (!phoenixEgg) + return false; + + MarkTargetWithDiamond(bot, phoenixEgg); + SetRtiTarget(botAI, "diamond", phoenixEgg); + + if (bot->GetTarget() != phoenixEgg->GetGUID()) + return Attack (phoenixEgg); + } + + return false; +} + +bool KaelthasSunstriderBreakMindControlWithInfinityBladeAction::Execute(Event event) +{ + Player* mcTarget = nullptr; + float closestDist = std::numeric_limits::max(); + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; + + if (member->HasAura(SPELL_KAELTHAS_MIND_CONTROL)) + { + float dist = bot->GetExactDist2d(member); + if (dist < closestDist) + { + closestDist = dist; + mcTarget = member; + } + } + } + } + + if (!mcTarget) + return false; + + if (!bot->IsWithinMeleeRange(mcTarget)) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, mcTarget->GetPositionX(), mcTarget->GetPositionY(), + mcTarget->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + static const std::array spells = + { + "hamstring", + "wing clip", + "shiv", + "stormstrike" + }; + + for (const char* spell : spells) + { + if (botAI->CanCastSpell(spell, mcTarget)) + return botAI->CastSpell(spell, mcTarget); + } + + return false; +} + +// Shock Barrier needs to be #1 focus, even if there is a Phoenix Egg up +bool KaelthasSunstriderBreakThroughShockBarrierAction::Execute(Event event) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (!kaelthas->HasAura(SPELL_SHOCK_BARRIER)) + { + static const std::array spells = + { + "bash", + "counterspell", + "kick", + "mind freeze", + "pummel", + "shield bash", + "silencing shot", + "wind shear", + }; + + for (const char* spell : spells) + { + if (botAI->CanCastSpell(spell, kaelthas)) + return botAI->CastSpell(spell, kaelthas); + } + } + else if (bot->GetTarget() != kaelthas->GetGUID()) + { + SetRtiTarget(botAI, "star", kaelthas); + return Attack(kaelthas); + } + + return false; +} + +// Bots immediately fall to the ground after Gravity Lapse, so this action +// name is kind of a misnomer (though bots are still in a flying state) +bool KaelthasSunstriderSpreadOutInMidairAction::Execute(Event event) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + const float minSpreadDistance = 15.0f; + + std::vector nearbyPlayers; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot || !member->IsAlive()) + continue; + + if (bot->IsWithinDist3d(member, minSpreadDistance * 1.0f)) + nearbyPlayers.push_back(member); + } + + if (nearbyPlayers.empty()) + return false; + + Player* closestPlayer = nullptr; + float closestDist = std::numeric_limits::max(); + for (Player* player : nearbyPlayers) + { + float distToPlayer = bot->GetExactDist(player); + if (distToPlayer < closestDist) + { + closestDist = distToPlayer; + closestPlayer = player; + } + } + + if (closestPlayer && closestDist < minSpreadDistance) + { + float angle = bot->GetAngle(closestPlayer) + M_PI; + float distance = minSpreadDistance - closestDist; + + float x = bot->GetPositionX() + std::cos(angle) * distance; + float y = bot->GetPositionY() + std::sin(angle) * distance; + + return MoveTo(TEMPEST_KEEP_MAP_ID, x, y, bot->GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.h b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.h new file mode 100644 index 00000000000..976dcf7e8aa --- /dev/null +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.h @@ -0,0 +1,417 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPACTIONS_H +#define _PLAYERBOT_RAIDTEMPESTKEEPACTIONS_H + +#include "RaidTempestKeepHelpers.h" +#include "RaidTempestKeepKaelthasBossAI.h" +#include "Action.h" +#include "AttackAction.h" +#include "MovementActions.h" + +using namespace TempestKeepHelpers; + +// Trash + +class CrimsonHandCenturionCastPolymorphAction : public Action +{ +public: + CrimsonHandCenturionCastPolymorphAction( + PlayerbotAI* botAI, std::string const name = "crimson hand centurion cast polymorph") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Al'ar + +class AlarMisdirectBossToMainTankAction : public AttackAction +{ +public: + AlarMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "al'ar misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarBossTanksMoveBetweenPlatformsAction : public AttackAction +{ +public: + AlarBossTanksMoveBetweenPlatformsAction( + PlayerbotAI* botAI, std::string const name = "al'ar boss tanks move between platforms") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool PositionMainTank(Player* mainTank, Unit* alar, int8 locationIndex); + bool PositionAssistTank(Player* assistTank, Unit* alar, int8 locationIndex); +}; + +class AlarMeleeDpsMoveBetweenPlatformsAction : public AttackAction +{ +public: + AlarMeleeDpsMoveBetweenPlatformsAction( + PlayerbotAI* botAI, std::string const name = "al'ar melee dps move between platforms") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarRangedAndEmberTankMoveUnderPlatformsAction : public AttackAction +{ +public: + AlarRangedAndEmberTankMoveUnderPlatformsAction( + PlayerbotAI* botAI, std::string const name = "al'ar ranged and ember tank move under platforms") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarAssistTanksPickUpEmbersAction : public AttackAction +{ +public: + AlarAssistTanksPickUpEmbersAction( + PlayerbotAI* botAI, std::string const name = "al'ar assist tanks pick up embers") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool HandlePhase1Embers(Unit* alar); + bool HandlePhase2Embers(Unit* alar); +}; + +class AlarRangedDpsPrioritizeEmbersAction : public AttackAction +{ +public: + AlarRangedDpsPrioritizeEmbersAction( + PlayerbotAI* botAI, std::string const name = "al'ar ranged dps prioritize embers") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarJumpFromPlatformAction : public MovementAction +{ +public: + AlarJumpFromPlatformAction( + PlayerbotAI* botAI, std::string const name = "al'ar jump from platform") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarMoveAwayFromRebirthAction : public MovementAction +{ +public: + AlarMoveAwayFromRebirthAction( + PlayerbotAI* botAI, std::string const name = "al'ar move away from rebirth") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarSwapTanksOnBossAction : public AttackAction +{ +public: + AlarSwapTanksOnBossAction( + PlayerbotAI* botAI, std::string const name = "al'ar swap tanks on boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarAvoidFlamePatchesAndDiveBombsAction : public MovementAction +{ +public: + AlarAvoidFlamePatchesAndDiveBombsAction( + PlayerbotAI* botAI, std::string const name = "al'ar avoid flame patches and dive bombs") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool AvoidFlamePatch(); + bool HandleDiveBomb(Unit* alar); +}; + +class AlarReturnToRoomCenterAction : public MovementAction +{ +public: + AlarReturnToRoomCenterAction( + PlayerbotAI* botAI, std::string const name = "al'ar return to room center") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarManagePhaseTrackerAction : public Action +{ +public: + AlarManagePhaseTrackerAction( + PlayerbotAI* botAI, std::string const name = "al'ar manage phase tracker") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Void Reaver + +class VoidReaverTanksPositionBossAction : public AttackAction +{ +public: + VoidReaverTanksPositionBossAction( + PlayerbotAI* botAI, std::string const name = "void reaver tanks position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class VoidReaverRangedUseAggroDumpAbilityAction : public Action +{ +public: + VoidReaverRangedUseAggroDumpAbilityAction( + PlayerbotAI* botAI, std::string const name = "void reaver ranged use aggro dump ability") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class VoidReaverSpreadRangedAction : public MovementAction +{ +public: + VoidReaverSpreadRangedAction( + PlayerbotAI* botAI, std::string const name = "void reaver spread ranged") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + Position GetRangedBotPosition( + const Position& center, float radius, uint8 botsPerRing, float offsetArc, uint8 botIndex, float botZ); +}; + +// High Astromancer Solarian + +class HighAstromancerSolarianRangedLeaveSpaceForMeleeAction : public MovementAction +{ +public: + HighAstromancerSolarianRangedLeaveSpaceForMeleeAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian ranged leave space for melee") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HighAstromancerSolarianMoveAwayFromGroupAction : public MovementAction +{ +public: + HighAstromancerSolarianMoveAwayFromGroupAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian move away from group") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HighAstromancerSolarianStackForAoeAction : public MovementAction +{ +public: + HighAstromancerSolarianStackForAoeAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian stack for aoe") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HighAstromancerSolarianTargetSolariumPriestsAction : public AttackAction +{ +public: + HighAstromancerSolarianTargetSolariumPriestsAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian target solarium priests") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + std::vector GetSolariumPriests(); + std::vector GetMeleeBots(Group* group); + Unit* AssignSolariumPriestsToBots(const std::vector& solariumPriests, const std::vector& meleeMembers); +}; + +class HighAstromancerSolarianTankVoidwalkerAction : public AttackAction +{ +public: + HighAstromancerSolarianTankVoidwalkerAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian tank voidwalker") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HighAstromancerSolarianCastFearWardOnMainTankAction : public Action +{ +public: + HighAstromancerSolarianCastFearWardOnMainTankAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian cast fear ward on main tank") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Kael'thas Sunstrider + +class KaelthasSunstriderKiteThaladredAction : public MovementAction +{ +public: + KaelthasSunstriderKiteThaladredAction( + PlayerbotAI* botAI) : MovementAction(botAI, "kael'thas sunstrider kite thaladred") {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderMisdirectAdvisorsToTanksAction : public AttackAction +{ +public: + KaelthasSunstriderMisdirectAdvisorsToTanksAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider misdirect advisors to tanks") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderMainTankPositionSanguinarAction : public AttackAction +{ +public: + KaelthasSunstriderMainTankPositionSanguinarAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider main tank position sanguinar") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderCastFearWardOnSanguinarTankAction : public Action +{ +public: + KaelthasSunstriderCastFearWardOnSanguinarTankAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider cast fear ward on sanguinar tank") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderManageWarlockTankStrategyAction : public Action +{ +public: + KaelthasSunstriderManageWarlockTankStrategyAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider manage warlock tank strategy") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderWarlockTankPositionCapernianAction : public AttackAction +{ +public: + KaelthasSunstriderWarlockTankPositionCapernianAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider warlock tank position capernian") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction : public MovementAction +{ +public: + KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider spread and move away from capernian") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool RangedBotsDisperse(boss_kaelthas* kaelAI); + bool StayBackFromCapernian(); +}; + +class KaelthasSunstriderFirstAssistTankPositionTelonicusAction : public AttackAction +{ +public: + KaelthasSunstriderFirstAssistTankPositionTelonicusAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider first assist tank position telonicus") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderPositionPhase3TankHealerAction : public AttackAction +{ +public: + KaelthasSunstriderPositionPhase3TankHealerAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider position phase 3 tank healer") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderAssignAdvisorDpsPriorityAction : public AttackAction +{ +public: + KaelthasSunstriderAssignAdvisorDpsPriorityAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider assign advisor dps priority") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderManageAdvisorDpsTimerAction : public Action +{ +public: + KaelthasSunstriderManageAdvisorDpsTimerAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider manage advisor dps timer") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction : public AttackAction +{ +public: + KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider assign legendary weapon dps priority") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderMoveDevastationAwayAction : public AttackAction +{ +public: + KaelthasSunstriderMoveDevastationAwayAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider move devastation away") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderHunterTurnAwayNetherstrandLongbowAction : public AttackAction +{ +public: + KaelthasSunstriderHunterTurnAwayNetherstrandLongbowAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider hunter turn away netherstrand longbow") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderLootLegendaryWeaponsAction : public MovementAction +{ +public: + KaelthasSunstriderLootLegendaryWeaponsAction( + PlayerbotAI* botAI) : MovementAction(botAI, "kael'thas sunstrider loot legendary weapons") {} + bool Execute(Event event) override; + +private: + bool ShouldBotLootWeapon(uint32 weaponEntry); + bool LootWeapon(uint32 weaponEntry, uint32 itemId, const char* weaponName); +}; + +class KaelthasSunstriderUseLegendaryWeaponsAction : public Action +{ +public: + KaelthasSunstriderUseLegendaryWeaponsAction( + PlayerbotAI* botAI) : Action(botAI, "kael'thas sunstrider use legendary weapons") {} + bool Execute(Event event) override; + +private: + bool UsePhaseshiftBulwark(); + bool UseStaffOfDisintegration(); + bool UseNetherstrandLongbow(); + bool UseEquippedItemWithPacket(Item* item); +}; + +class KaelthasSunstriderReequipGearAction : public Action +{ +public: + KaelthasSunstriderReequipGearAction( + PlayerbotAI* botAI) : Action(botAI, "kael'thas sunstrider reequip gear") {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderMainTankPositionBossAction : public AttackAction +{ +public: + KaelthasSunstriderMainTankPositionBossAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider main tank position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderAvoidFlameStrikeAction : public MovementAction +{ +public: + KaelthasSunstriderAvoidFlameStrikeAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider avoid flame strike") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderRoundUpPhoenixesAndFocusDownEggsAction : public AttackAction +{ +public: + KaelthasSunstriderRoundUpPhoenixesAndFocusDownEggsAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider round up phoenixes and focus down eggs") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderBreakMindControlWithInfinityBladeAction : public AttackAction +{ +public: + KaelthasSunstriderBreakMindControlWithInfinityBladeAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider break mind control with infinity blade") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderBreakThroughShockBarrierAction : public AttackAction +{ +public: + KaelthasSunstriderBreakThroughShockBarrierAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider break through shock barrier") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderSpreadOutInMidairAction : public MovementAction +{ +public: + KaelthasSunstriderSpreadOutInMidairAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider spread out in midair") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +#endif diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp new file mode 100644 index 00000000000..43352125011 --- /dev/null +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp @@ -0,0 +1,610 @@ +#include + +#include "RaidTempestKeepHelpers.h" +#include "RaidTempestKeepActions.h" +#include "LootObjectStack.h" +#include "Playerbots.h" +#include "RtiTargetValue.h" + +namespace TempestKeepHelpers +{ + // General Helpers + + void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId) + { + if (!target) + return; + + if (Group* group = bot->GetGroup()) + { + ObjectGuid currentGuid = group->GetTargetIcon(iconId); + if (currentGuid != target->GetGUID()) + group->SetTargetIcon(iconId, bot->GetGUID(), target->GetGUID()); + } + } + + void MarkTargetWithSkull(Player* bot, Unit* target) + { + MarkTargetWithIcon(bot, target, RtiTargetValue::skullIndex); + } + + void MarkTargetWithSquare(Player* bot, Unit* target) + { + MarkTargetWithIcon(bot, target, RtiTargetValue::squareIndex); + } + + void MarkTargetWithStar(Player* bot, Unit* target) + { + MarkTargetWithIcon(bot, target, RtiTargetValue::starIndex); + } + + void MarkTargetWithCircle(Player* bot, Unit* target) + { + MarkTargetWithIcon(bot, target, RtiTargetValue::circleIndex); + } + + void MarkTargetWithTriangle(Player* bot, Unit* target) + { + MarkTargetWithIcon(bot, target, RtiTargetValue::triangleIndex); + } + + void MarkTargetWithDiamond(Player* bot, Unit* target) + { + MarkTargetWithIcon(bot, target, RtiTargetValue::diamondIndex); + } + + void MarkTargetWithCross(Player* bot, Unit* target) + { + MarkTargetWithIcon(bot, target, RtiTargetValue::crossIndex); + } + + void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target) + { + std::string currentRti = botAI->GetAiObjectContext()->GetValue("rti")->Get(); + Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("rti target")->Get(); + + if (currentRti != rtiName || currentTarget != target) + { + botAI->GetAiObjectContext()->GetValue("rti")->Set(rtiName); + botAI->GetAiObjectContext()->GetValue("rti target")->Set(target); + } + } + + Unit* GetNearestPlayerInRadius(Player* bot, float radius) + { + Unit* nearestPlayer = nullptr; + float nearestDistance = radius; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref != nullptr; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; + + float distance = bot->GetExactDist2d(member); + if (distance < nearestDistance) + { + nearestDistance = distance; + nearestPlayer = member; + } + } + } + + return nearestPlayer; + } + + Unit* GetNearestNonTankPlayerInRadius(Player* bot, float radius) + { + Unit* nearestPlayer = nullptr; + float nearestDistance = radius; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref != nullptr; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; + + if (PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member)) + { + if (memberAI->IsTank(member)) + continue; + } + + float distance = bot->GetExactDist2d(member); + if (distance < nearestDistance) + { + nearestDistance = distance; + nearestPlayer = member; + } + } + } + + return nearestPlayer; + } + + std::vector GetAllHazardTriggers( + PlayerbotAI* botAI, Player* bot, uint32 npcEntry, float maxSearchRadius) + { + std::vector hazardTriggers; + auto const& npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); + for (auto const& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->GetEntry() == npcEntry && bot->GetExactDist2d(unit) < maxSearchRadius) + hazardTriggers.push_back(unit); + } + + return hazardTriggers; + } + + Position FindSafestNearbyPosition(Player* bot, const std::vector& hazards, + float maxRadius, float hazardRadius, const Position* center) + { + const float searchStep = M_PI / 8.0f; + const float minDistance = 2.0f; + const float maxDistance = 30.0f; + const float distanceStep = 1.0f; + + Position bestPos; + float minMoveDistance = std::numeric_limits::max(); + bool foundSafe = false; + + for (float distance = minDistance; distance <= maxDistance; distance += distanceStep) + { + for (float angle = 0.0f; angle < 2 * M_PI; angle += searchStep) + { + const Position& searchCenter = center ? *center : bot->GetPosition(); + float x = searchCenter.GetPositionX() + distance * std::cos(angle); + float y = searchCenter.GetPositionY() + distance * std::sin(angle); + + bool isSafe = true; + for (Unit* hazard : hazards) + { + if (hazard->GetExactDist2d(x, y) < hazardRadius) + { + isSafe = false; + break; + } + } + + if (!isSafe) + continue; + + Position testPos(x, y, bot->GetPositionZ()); + + bool pathSafe = IsPathSafeFromHazards(bot->GetPosition(), testPos, hazards, hazardRadius); + if (pathSafe || !foundSafe) + { + float moveDistance = bot->GetExactDist2d(x, y); + + if (pathSafe && (!foundSafe || moveDistance < minMoveDistance)) + { + bestPos = testPos; + minMoveDistance = moveDistance; + foundSafe = true; + } + else if (!foundSafe && moveDistance < minMoveDistance) + { + bestPos = testPos; + minMoveDistance = moveDistance; + } + } + } + + if (foundSafe) + break; + } + + return bestPos; + } + + bool IsPathSafeFromHazards( + const Position& start, const Position& end, const std::vector& hazards, float hazardRadius) + { + const uint8 numChecks = 10; + float dx = end.GetPositionX() - start.GetPositionX(); + float dy = end.GetPositionY() - start.GetPositionY(); + + for (uint8 i = 1; i <= numChecks; ++i) + { + float ratio = static_cast(i) / numChecks; + float checkX = start.GetPositionX() + dx * ratio; + float checkY = start.GetPositionY() + dy * ratio; + + for (Unit* hazard : hazards) + { + float distToHazard = hazard->GetExactDist2d(checkX, checkY); + if (distToHazard < hazardRadius) + return false; + } + } + + return true; + } + + // Al'ar + + const Position ALAR_PLATFORM_0 = { 335.638f, 59.4879f, 17.9319f }; // West Platform + const Position ALAR_PLATFORM_1 = { 388.751f, 31.7312f, 20.2636f }; // Northwest Platform + const Position ALAR_PLATFORM_2 = { 388.791f, -33.1059f, 20.2636f }; // Northeast Platform + const Position ALAR_PLATFORM_3 = { 332.723f, -61.159f, 17.9791f }; // East Platform + const Position PLATFORM_POSITIONS[] = + { + ALAR_PLATFORM_0, + ALAR_PLATFORM_1, + ALAR_PLATFORM_2, + ALAR_PLATFORM_3 + }; + const Position ALAR_GROUND_0 = { 336.439f, 48.181f, -2.389f }; // Ground counterpart to West Platform + const Position ALAR_GROUND_1 = { 379.122f, 25.146f, -2.385f }; // Ground counterpart to Northwest Platform + const Position ALAR_GROUND_2 = { 378.583f, -27.481f, -2.385f }; // Ground counterpart to Northeast Platform + const Position ALAR_GROUND_3 = { 331.631f, -49.716f, -2.389f }; // Ground counterpart to East Platform + const Position GROUND_POSITIONS[] = + { + ALAR_GROUND_0, + ALAR_GROUND_1, + ALAR_GROUND_2, + ALAR_GROUND_3 + }; + const Position ALAR_ROOM_CENTER = { 330.611f, -2.540f, -2.389f }; + const Position ALAR_POINT_QUILL_OR_DIVE = {332.0f, 0.01f, 43.0f, 0.0f}; + const Position ALAR_POINT_MIDDLE = {331.0f, 0.01f, -2.38f, 0.0f}; + const Position ALAR_SE_RAMP_BASE = { 281.064f, -36.590f, -2.389f }; + const Position ALAR_SW_RAMP_BASE = { 281.064f, 36.590f, -2.389f }; + const Position ALAR_ROOM_S_CENTER = { 281.064f, 0.0f, -2.389f }; + + std::unordered_map lastRebirthState; + std::unordered_map isAlarInPhase2; + + int8 GetAlarDestinationLocationIndex(Unit* alar, Position& dest) + { + if (!alar) + return LOCATION_NONE; + + float x, y, z; + if (!alar->GetMotionMaster()->GetDestination(x, y, z)) + return LOCATION_NONE; + + dest.Relocate(x, y, z); + + const Position locations[] = + { + ALAR_PLATFORM_0, + ALAR_PLATFORM_1, + ALAR_PLATFORM_2, + ALAR_PLATFORM_3, + ALAR_POINT_QUILL_OR_DIVE, + ALAR_POINT_MIDDLE, + }; + + float minDist = std::numeric_limits::max(); + int8 locationIndex = LOCATION_NONE; + for (int8 i = 0; i < 7; ++i) + { + float dist = dest.GetExactDist2d(&locations[i]); + if (dist < minDist) + { + minDist = dist; + locationIndex = i; + } + } + if (minDist > 0.1f) + return LOCATION_NONE; + + return locationIndex; + } + + int8 GetAlarCurrentLocationIndex(Unit* alar) + { + if (!alar) + return LOCATION_NONE; + + const Position locations[] = + { + ALAR_PLATFORM_0, + ALAR_PLATFORM_1, + ALAR_PLATFORM_2, + ALAR_PLATFORM_3, + ALAR_POINT_QUILL_OR_DIVE, + ALAR_POINT_MIDDLE, + }; + + float minDist = std::numeric_limits::max(); + int8 locationIndex = LOCATION_NONE; + for (int8 i = 0; i < 7; ++i) + { + float dist = alar->GetPosition().GetExactDist2d(&locations[i]); + if (dist < minDist) + { + minDist = dist; + locationIndex = i; + } + } + if (minDist > 0.1f) + return LOCATION_NONE; + + return locationIndex; + } + + void GetClosestPlatformAndGround(const Position& botPos, int8& closestPlatform, Position& ground) + { + float minDist = std::numeric_limits::max(); + closestPlatform = -1; + for (int8 i = 0; i < 4; ++i) + { + float dist = botPos.GetExactDist2d(&PLATFORM_POSITIONS[i]); + if (dist < minDist) + { + minDist = dist; + closestPlatform = i; + } + } + ground = GROUND_POSITIONS[closestPlatform]; + } + + std::pair GetFirstTwoEmbersOfAlar(PlayerbotAI* botAI) + { + Unit* firstEmber = nullptr; + Unit* secondEmber = nullptr; + + for (auto const& guid : + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get()) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_EMBER_OF_ALAR) + { + if (!firstEmber) + firstEmber = unit; + else if (!secondEmber) + { + secondEmber = unit; + break; + } + } + } + + return { firstEmber, secondEmber }; + } + + Player* GetSecondEmberTank(PlayerbotAI* botAI, Unit* alar) + { + Player* mainTank = nullptr; + Player* assistTank = nullptr; + + if (Group* group = botAI->GetBot()->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member) + continue; + if (botAI->IsMainTank(member)) + mainTank = member; + if (botAI->IsAssistTankOfIndex(member, 0)) + assistTank = member; + } + } + + bool mainTankHasMelt = mainTank && mainTank->HasAura(SPELL_MELT_ARMOR); + bool assistTankHasMelt = assistTank && assistTank->HasAura(SPELL_MELT_ARMOR); + + if (!mainTankHasMelt && !assistTankHasMelt) + return assistTank; + if (mainTankHasMelt) + return mainTank; + if (assistTankHasMelt) + return assistTank; + + return nullptr; + } + + // Void Reaver + + const Position VOID_REAVER_TANK_POSITION = { 423.845f, 371.733f, 14.897f }; + + std::unordered_map initialVoidReaverPositions; + std::unordered_map hasReachedInitialVoidReaverPosition; + + // High Astromancer Solarian + // N/A + + // Kael'thas Sunstrider + + const Position SANGUINAR_TANK_POSITION = { 775.478f, 39.888f, 46.780f }; + const Position TELONICUS_TANK_POSITION = { 773.717f, 44.091f, 46.780f }; + const Position ADVISOR_HEAL_POSITION = { 749.443f, 26.927f, 46.780f }; + const Position KAELTHAS_WEAPON_STACK_POSITION = { 775.296f, -0.822f, 48.729f }; + const Position KAELTHAS_AXE_TANK_POSITION = { 775.621f, 20.717f, 48.729f }; + const Position KAELTHAS_BOW_TANK_POSITION = { 777.713f, -28.857f, 48.729f }; + const Position KAELTHAS_TANK_POSITION = { 799.390f, -0.837f, 48.729f }; + + std::unordered_map advisorDpsWaitTimer; + + bool IsKaelthasInstanceTimerManager(PlayerbotAI* botAI, Player* bot) + { + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !botAI->IsDps(member) || + !GET_PLAYERBOT_AI(member)) + continue; + + Player* capernianTank = GetCapernianTank(botAI, member); + if (capernianTank && capernianTank == member) + continue; + + return member == bot; + } + } + + return false; + } + + Player* GetCapernianTank(PlayerbotAI* botAI, Player* bot) + { + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + // (1) Look for an 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 bot Warlock with highest HP + Player* highestHpWarlock = nullptr; + uint32 highestHp = 0; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || + member->getClass() != CLASS_WARLOCK) + continue; + + uint32 hp = member->GetMaxHealth(); + if (!highestHpWarlock || hp > highestHp) + { + highestHpWarlock = member; + highestHp = hp; + } + } + + // (3) Return the found Warlock tank, or nullptr if none found + return highestHpWarlock; + } + + Player* GetNetherstrandLongbowTank(PlayerbotAI* botAI, Player* bot) + { + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + // (1) Look for an assistant Hunter (real player or bot) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->getClass() != CLASS_HUNTER) + continue; + + if (group->IsAssistant(member->GetGUID())) + return member; + } + + // (2) Fall back to any bot Hunter + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || + member->getClass() != CLASS_HUNTER) + continue; + + return member; + } + + return nullptr; + } + + bool IsAnyLegendaryWeaponDead(PlayerbotAI* botAI, Player* bot) + { + const std::pair weapons[] = + { + { "staff of disintegration", NPC_STAFF_OF_DISINTEGRATION }, + { "cosmic infuser", NPC_COSMIC_INFUSER }, + { "infinity blade", NPC_INFINITY_BLADES }, + { "warp slicer", NPC_WARP_SLICER }, + { "phaseshift bulwark", NPC_PHASESHIFT_BULWARK }, + { "netherstrand longbow", NPC_NETHERSTRAND_LONGBOW }, + { "devastation", NPC_DEVASTATION }, + }; + + for (auto const& [name, entry] : weapons) + { + Unit* weapon = + botAI->GetAiObjectContext()->GetValue("find target", name)->Get(); + if (weapon && weapon->IsAlive()) + continue; + + auto const& corpses = + botAI->GetAiObjectContext()->GetValue("nearest corpses")->Get(); + 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() == entry && !creature->IsAlive()) + return true; + } + } + } + + return false; + } + + bool HasEquippableItemForSlot(Player* bot, uint8 slot) + { + for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag) + { + for (uint8 bagSlot = 0; bagSlot < MAX_BAG_SIZE; ++bagSlot) + { + Item* item = bot->GetItemByPos(bag, bagSlot); + if (!item) + continue; + + ItemTemplate const* proto = item->GetTemplate(); + if (!proto) + continue; + + uint16 dest = 0; + if (bot->CanEquipItem(slot, dest, item, false)) + return true; + } + } + + return false; + } + + bool HasEquippableOffhand(Player* bot) + { + for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag) + { + for (uint8 bagSlot = 0; bagSlot < MAX_BAG_SIZE; ++bagSlot) + { + Item* item = bot->GetItemByPos(bag, bagSlot); + if (!item) + continue; + + ItemTemplate const* proto = item->GetTemplate(); + if (!proto) + continue; + + if (proto->InventoryType == INVTYPE_SHIELD || + proto->InventoryType == INVTYPE_WEAPONOFFHAND || + proto->InventoryType == INVTYPE_HOLDABLE) + { + uint16 dest = 0; + if (bot->CanEquipItem(EQUIPMENT_SLOT_OFFHAND, dest, item, false)) + return true; + } + } + } + + return false; + } +} diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.h b/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.h new file mode 100644 index 00000000000..90acbb8bb02 --- /dev/null +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.h @@ -0,0 +1,175 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPHELPERS_H_ +#define _PLAYERBOT_RAIDTEMPESTKEEPHELPERS_H_ + +#include +#include +#include + +#include "AiObject.h" +#include "Position.h" +#include "Unit.h" + +namespace TempestKeepHelpers +{ + enum TempestKeepSpells + { + // Trash + SPELL_ARCANE_FLURRY = 37268, + + // Al'ar + SPELL_REBIRTH_PHASE2 = 34342, + SPELL_REBIRTH_DIVE = 35369, + SPELL_MELT_ARMOR = 35410, + + // High Astromancer Solarian + SPELL_SOLARIAN_TRANSFORM = 39117, + SPELL_WRATH_OF_THE_ASTROMANCER = 42783, + + // Kael'thas Sunstrider + SPELL_PERMANENT_FEIGN_DEATH = 29266, + SPELL_GRAVITY_LAPSE = 39432, + SPELL_KAEL_FULL_POWER = 36187, + SPELL_MENTAL_PROTECTION_FIELD = 36480, // Staff of Disintegration + SPELL_ARCANE_BARRIER = 36481, // Phaseshift Bulwark + SPELL_KAELTHAS_MIND_CONTROL = 36797, + SPELL_SHOCK_BARRIER = 36815, + SPELL_STAFF_FROSTBOLT = 36990, + + // Hunter + SPELL_MISDIRECTION = 35079, + + // Mage + SPELL_POLYMORPH_SHEEP = 12826, + SPELL_POLYMORPH_TURTLE = 28271, + SPELL_POLYMORPH_PIG = 28272, + + // Priest + SPELL_FEAR_WARD = 6346, + }; + + enum TempestKeepNPCs + { + // Al'ar + NPC_EMBER_OF_ALAR = 19551, + NPC_FLAME_PATCH = 20602, + + // High Astromancer Solarian + NPC_SOLARIUM_PRIEST = 18806, + + // Kael'thas Sunstrider + NPC_KAELTHAS_SUNSTRIDER = 19622, + NPC_LORD_SANGUINAR = 20060, + NPC_GRAND_ASTROMANCER_CAPERNIAN = 20062, + NPC_MASTER_ENGINEER_TELONICUS = 20063, + NPC_THALADRED_THE_DARKENER = 20064, + NPC_NETHERSTRAND_LONGBOW = 21268, + NPC_DEVASTATION = 21269, + NPC_COSMIC_INFUSER = 21270, + NPC_INFINITY_BLADES = 21271, // Item is singular, but NPC is plural + NPC_WARP_SLICER = 21272, + NPC_PHASESHIFT_BULWARK = 21273, + NPC_STAFF_OF_DISINTEGRATION = 21274, + // NPC_NETHER_VAPOR = 21002, // Unimplemented in AC; method needed if fixed + NPC_PHOENIX = 21362, + NPC_FLAME_STRIKE_TRIGGER = 21369, + }; + + enum TempestKeepItems + { + // Kael'thas Sunstrider + ITEM_WARP_SLICER = 30311, + ITEM_INFINITY_BLADE = 30312, + ITEM_STAFF_OF_DISINTEGRATION = 30313, + ITEM_PHASESHIFT_BULWARK = 30314, + ITEM_DEVASTATION = 30316, + ITEM_COSMIC_INFUSER = 30317, + ITEM_NETHERSTRAND_LONGBOW = 30318, + ITEM_NETHER_SPIKES = 30319, + }; + + enum AlarLocationIndex + { + PLATFORM_0_IDX, + PLATFORM_1_IDX, + PLATFORM_2_IDX, + PLATFORM_3_IDX, + POINT_QUILL_OR_DIVE_IDX, + POINT_MIDDLE_IDX, + LOCATION_NONE = -1 + }; + + // General + const uint32 TEMPEST_KEEP_MAP_ID = 550; + void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId); + void MarkTargetWithSkull(Player* bot, Unit* target); + void MarkTargetWithSquare(Player* bot, Unit* target); + void MarkTargetWithStar(Player* bot, Unit* target); + void MarkTargetWithCircle(Player* bot, Unit* target); + void MarkTargetWithTriangle(Player* bot, Unit* target); + void MarkTargetWithDiamond(Player* bot, Unit* target); + void MarkTargetWithCross(Player* bot, Unit* target); + void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target); + Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry); + Unit* GetNearestPlayerInRadius(Player* bot, float radius); + Unit* GetNearestNonTankPlayerInRadius(Player* bot, float radius); + std::vector GetAllHazardTriggers( + PlayerbotAI* botAI, Player* bot, uint32 npcEntry, float maxSearchRadius); + Position FindSafestNearbyPosition(Player* bot, const std::vector& hazards, + float maxRadius, float hazardRadius, const Position* center = nullptr); + bool IsPathSafeFromHazards( + const Position& start, const Position& end, const std::vector& hazards, + float hazardRadius); + + // Al'ar + const float ALAR_BALCONY_Z = 17.0f; + extern const Position ALAR_PLATFORM_0; + extern const Position ALAR_PLATFORM_1; + extern const Position ALAR_PLATFORM_2; + extern const Position ALAR_PLATFORM_3; + extern const Position PLATFORM_POSITIONS[4]; + extern const Position ALAR_GROUND_0; + extern const Position ALAR_GROUND_1; + extern const Position ALAR_GROUND_2; + extern const Position ALAR_GROUND_3; + extern const Position GROUND_POSITIONS[4]; + extern const Position ALAR_ROOM_CENTER; + extern const Position ALAR_POINT_QUILL_OR_DIVE; + extern const Position ALAR_POINT_MIDDLE; + extern const Position ALAR_SE_RAMP_BASE; + extern const Position ALAR_SW_RAMP_BASE; + extern const Position ALAR_ROOM_S_CENTER; + extern std::unordered_map lastRebirthState; + extern std::unordered_map isAlarInPhase2; + int8 GetAlarDestinationLocationIndex(Unit* alar, Position& dest); + int8 GetAlarCurrentLocationIndex(Unit* alar); + void GetClosestPlatformAndGround( + const Position& botPos, int8& closestPlatform, Position& ground); + std::pair GetFirstTwoEmbersOfAlar(PlayerbotAI* botAI); + Player* GetSecondEmberTank(PlayerbotAI* botAI, Unit* alar); + + // Void Reaver + extern const Position VOID_REAVER_TANK_POSITION; + extern std::unordered_map initialVoidReaverPositions; + extern std::unordered_map hasReachedInitialVoidReaverPosition; + + // High Astromancer Solarian + // N/A + + // Kael'thas Sunstrider + extern const Position SANGUINAR_TANK_POSITION; + extern const Position TELONICUS_TANK_POSITION; + extern const Position ADVISOR_HEAL_POSITION; + extern const Position KAELTHAS_WEAPON_STACK_POSITION; + extern const Position KAELTHAS_AXE_TANK_POSITION; + extern const Position KAELTHAS_BOW_TANK_POSITION; + extern const Position KAELTHAS_TANK_POSITION; + extern std::unordered_map advisorDpsWaitTimer; + bool IsKaelthasInstanceTimerManager(PlayerbotAI* botAI, Player* bot); + Player* GetCapernianTank(PlayerbotAI* botAI, Player* bot); + Player* GetNetherstrandLongbowTank(PlayerbotAI* botAI, Player* bot); + bool IsAnyLegendaryWeaponDead(PlayerbotAI* botAI, Player* bot); + bool HasEquippableItemForSlot(Player* bot, uint8 slot); + bool HasEquippableOffhand(Player* bot); +} + +#endif diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepKaelthasBossAI.h b/src/strategy/raids/tempestkeep/RaidTempestKeepKaelthasBossAI.h new file mode 100644 index 00000000000..598c80614a5 --- /dev/null +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepKaelthasBossAI.h @@ -0,0 +1,54 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPKAELTHASBOSSAI_H_ +#define _PLAYERBOT_RAIDTEMPESTKEEPKAELTHASBOSSAI_H_ + +#include "ScriptedCreature.h" + +enum KTYells +{ +}; + +enum KTPhases +{ + PHASE_NONE = 0, + PHASE_SINGLE_ADVISOR = 1, + PHASE_WEAPONS = 2, + PHASE_TRANSITION = 3, + PHASE_ALL_ADVISORS = 4, + PHASE_FINAL = 5 +}; + +enum KTActions +{ +}; + +struct boss_kaelthas : public BossAI +{ + boss_kaelthas(Creature* creature); + + void PrepareAdvisors(); + void SetRoomState(GOState state); + void Reset() override; + void AttackStart(Unit* who) override; + void MoveInLineOfSight(Unit* who) override; + void KilledUnit(Unit* victim) override; + void JustSummoned(Creature* summon) override; + void SpellHit(Unit* caster, SpellInfo const* spell) override; + void MovementInform(uint32 type, uint32 point) override; + void ExecuteMiddleEvent(); + void IntroduceNewAdvisor(KTYells talkIntroduction, KTActions kaelAction); + void PhaseEnchantedWeaponsExecute(); + void PhaseAllAdvisorsExecute(); + void PhaseKaelExecute(); + void UpdateAI(uint32 diff) override; + bool CheckEvadeIfOutOfCombatArea() const override; + void JustDied(Unit* killer) override; + + uint32 GetPhase() const { return _phase; } // This is the only addition to the existing class + +private: + uint32 _phase; + uint8 _advisorsAlive; + bool _transitionSceneReached = false; +}; + +#endif diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.cpp new file mode 100644 index 00000000000..4091f6ff704 --- /dev/null +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.cpp @@ -0,0 +1,474 @@ +#include "RaidTempestKeepMultipliers.h" +#include "RaidTempestKeepActions.h" +#include "RaidTempestKeepHelpers.h" +#include "RaidTempestKeepKaelthasBossAI.h" +#include "ChooseTargetActions.h" +#include "DKActions.h" +#include "DruidBearActions.h" +#include "EquipAction.h" +#include "FollowActions.h" +#include "HunterActions.h" +#include "MageActions.h" +#include "PaladinActions.h" +#include "Playerbots.h" +#include "RogueActions.h" +#include "ShamanActions.h" +#include "WarlockActions.h" +#include "WarriorActions.h" + +// Al'ar + +float AlarMoveBetweenPlatformsMultiplier::GetValue(Action* action) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return 1.0f; + + if (isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (botAI->IsDps(bot)) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float AlarDisableDisperseMultiplier::GetValue(Action* action) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float AlarDisableTankAssistMultiplier::GetValue(Action* action) +{ + if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0)) + return 1.0f; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return 1.0f; + + if (bot->GetVictim() != nullptr) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float AlarStayAwayFromRebirthMultiplier::GetValue(Action* action) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return 1.0f; + + Creature* alarCreature = alar->ToCreature(); + if (alarCreature && alarCreature->GetReactState() == REACT_PASSIVE) + { + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float AlarPhase2NoTankingIfArmorMeltedMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot) || !bot->HasAura(SPELL_MELT_ARMOR)) + return 1.0f; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (alar) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Void Reaver + +float VoidReaverDisableTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (voidReaver) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float VoidReaverMaintainPositionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsRanged(bot)) + return 1.0f; + + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (voidReaver) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// High Astromancer Solarian + +float HighAstromancerSolarianMaintainPositionMultiplier::GetValue(Action* action) +{ + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + if (!astromancer || astromancer->HasAura(SPELL_SOLARIAN_TRANSFORM)) + return 1.0f; + + if (botAI->IsRanged(bot)) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + if (bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER)) + { + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + } + + return 1.0f; +} + +float HighAstromancerSolarianDisableTankAssistMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + Unit* solariumPriest = AI_VALUE2(Unit*, "find target", "solarium priest"); + if (solariumPriest && bot->GetVictim() != nullptr) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Kael'thas Sunstrider + +float KaelthasSunstriderWaitForDpsMultiplier::GetValue(Action* action) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || kaelAI->GetPhase() != PHASE_SINGLE_ADVISOR) + return 1.0f; + + if (dynamic_cast(action)) + return 1.0f; + + const time_t now = std::time(nullptr); + const uint8 dpsWaitSeconds = 10; + + auto it = advisorDpsWaitTimer.find(kaelthas->GetMap()->GetInstanceId()); + if (it == advisorDpsWaitTimer.end() || (now - it->second) < dpsWaitSeconds) + { + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + + auto isAdvisorActive = [](Unit* advisor) + { + return advisor && advisor->IsAlive() && + !advisor->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !advisor->HasAura(SPELL_PERMANENT_FEIGN_DEATH); + }; + + if (isAdvisorActive(sanguinar) && !botAI->IsMainTank(bot)) + { + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + } + + if (isAdvisorActive(telonicus) && !botAI->IsAssistTankOfIndex(bot, 0)) + { + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + } + + Player* capernianTank = GetCapernianTank(botAI, bot); + if (isAdvisorActive(capernian) && capernianTank && capernianTank != bot && + !botAI->IsMainTank(bot)) + { + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + } + } + + return 1.0f; +} + +float KaelthasSunstriderKiteThaladredMultiplier::GetValue(Action* action) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return 1.0f; + + if (botAI->IsTank(bot) && kaelAI->GetPhase() == PHASE_ALL_ADVISORS) + return 1.0f; + + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + if (!thaladred || thaladred->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + return 1.0f; + + if (thaladred->GetVictim() == bot) + { + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float KaelthasSunstriderControlMisdirectionMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_HUNTER) + return 1.0f; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (kaelAI && kaelAI->GetPhase() != PHASE_FINAL) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// For disabling Capernian Tank's Shadow Ward, which is part of the standard +// tank strategy for Warlocks (made for Twin Emps but useless here) +float KaelthasSunstriderDisableShadowWardMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_WARLOCK) + return 1.0f; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float KaelthasSunstriderManageTankTargetingMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return 1.0f; + + // Try to keep main tank from grabbing aggro on any weapon other than the axe + if (botAI->IsMainTank(bot) && kaelAI->GetPhase() == PHASE_WEAPONS) + { + 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)) + return 0.0f; + } + + if (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR || + kaelAI->GetPhase() == PHASE_ALL_ADVISORS) + { + if (bot->GetVictim() != nullptr) + { + if (dynamic_cast(action)) + return 0.0f; + } + } + + return 1.0f; +} + +float KaelthasSunstriderDisableDisperseMultiplier::GetValue(Action* action) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Bloodlust/Heroism should be used at the start of Phase 3 +float KaelthasSunstriderDelayBloodlustAndHeroismMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_SHAMAN) + return 1.0f; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (kaelAI && + kaelAI->GetPhase() != PHASE_ALL_ADVISORS && kaelAI->GetPhase() != PHASE_FINAL) + { + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float KaelthasSunstriderTryNonfatalBreakingOfMindControlMultiplier::GetValue(Action* action) +{ + if (botAI->IsTank(bot) || !bot->HasItemCount(ITEM_INFINITY_BLADE, 1, true)) + return 1.0f; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + Group* group = bot->GetGroup(); + if (!group) + return 1.0f; + + bool hasMindControlledPlayer = false; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (member->HasAura(SPELL_KAELTHAS_MIND_CONTROL)) + { + hasMindControlledPlayer = true; + break; + } + } + + if (hasMindControlledPlayer) + { + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float KaelthasSunstriderAllDpsOnBossDuringPyroblastMultiplier::GetValue(Action* action) +{ + if (!botAI->IsDps(bot)) + return 1.0f; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas || !kaelthas->HasUnitState(UNIT_STATE_CASTING)) + return 1.0f; + + if (kaelthas->HasAura(SPELL_SHOCK_BARRIER)) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier::GetValue(Action* action) +{ + if (!bot->HasAura(SPELL_GRAVITY_LAPSE)) + return 1.0f; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (kaelthas) + { + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.h b/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.h new file mode 100644 index 00000000000..e2a7ade443a --- /dev/null +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.h @@ -0,0 +1,158 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPMULTIPLIERS_H +#define _PLAYERBOT_RAIDTEMPESTKEEPMULTIPLIERS_H + +#include "Multiplier.h" + +class AlarMoveBetweenPlatformsMultiplier : public Multiplier +{ +public: + AlarMoveBetweenPlatformsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar move between platforms multiplier") {} + virtual float GetValue(Action* action); +}; + +class AlarDisableDisperseMultiplier : public Multiplier +{ +public: + AlarDisableDisperseMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar disable disperse multiplier") {} + virtual float GetValue(Action* action); +}; + +class AlarDisableTankAssistMultiplier : public Multiplier +{ +public: + AlarDisableTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar disable tank assist multiplier") {} + virtual float GetValue(Action* action); +}; + +class AlarStayAwayFromRebirthMultiplier : public Multiplier +{ +public: + AlarStayAwayFromRebirthMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar stay away from rebirth multiplier") {} + virtual float GetValue(Action* action); +}; + +class AlarPhase2NoTankingIfArmorMeltedMultiplier : public Multiplier +{ +public: + AlarPhase2NoTankingIfArmorMeltedMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar phase 2 no tanking if armor melted multiplier") {} + virtual float GetValue(Action* action); +}; + +class VoidReaverDisableTankActionsMultiplier : public Multiplier +{ +public: + VoidReaverDisableTankActionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "void reaver disable tank actions multiplier") {} + virtual float GetValue(Action* action); +}; + +class VoidReaverMaintainPositionsMultiplier : public Multiplier +{ +public: + VoidReaverMaintainPositionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "void reaver maintain positions multiplier") {} + virtual float GetValue(Action* action); +}; + +class HighAstromancerSolarianDisableTankAssistMultiplier : public Multiplier +{ +public: + HighAstromancerSolarianDisableTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "high astromancer solarian disable tank assist multiplier") {} + virtual float GetValue(Action* action); +}; + +class HighAstromancerSolarianMaintainPositionMultiplier : public Multiplier +{ +public: + HighAstromancerSolarianMaintainPositionMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "high astromancer solarian maintain position multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderKiteThaladredMultiplier : public Multiplier +{ +public: + KaelthasSunstriderKiteThaladredMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider kite thaladred multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderWaitForDpsMultiplier : public Multiplier +{ +public: + KaelthasSunstriderWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider wait for dps multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderControlMisdirectionMultiplier : public Multiplier +{ +public: + KaelthasSunstriderControlMisdirectionMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider control misdirection multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderDisableShadowWardMultiplier : public Multiplier +{ +public: + KaelthasSunstriderDisableShadowWardMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider disable shadow ward multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderManageTankTargetingMultiplier : public Multiplier +{ +public: + KaelthasSunstriderManageTankTargetingMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider manage tank targeting multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderDisableDisperseMultiplier : public Multiplier +{ +public: + KaelthasSunstriderDisableDisperseMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider disable disperse multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderDelayBloodlustAndHeroismMultiplier : public Multiplier +{ +public: + KaelthasSunstriderDelayBloodlustAndHeroismMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider delay bloodlust and heroism multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderTryNonfatalBreakingOfMindControlMultiplier : public Multiplier +{ +public: + KaelthasSunstriderTryNonfatalBreakingOfMindControlMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider try nonfatal breaking of mind control multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderAllDpsOnBossDuringPyroblastMultiplier : public Multiplier +{ +public: + KaelthasSunstriderAllDpsOnBossDuringPyroblastMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider all dps on boss during pyroblast multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier : public Multiplier +{ +public: + KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider stay spread during gravity lapse multiplier") {} + virtual float GetValue(Action* action); +}; + +#endif diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp new file mode 100644 index 00000000000..21a9231f68e --- /dev/null +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp @@ -0,0 +1,166 @@ +#include "RaidTempestKeepStrategy.h" +#include "RaidTempestKeepMultipliers.h" + +void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) +{ + // Trash + triggers.push_back(new TriggerNode("crimson hand centurion casts arcane volley", + NextAction::array(0, new NextAction("crimson hand centurion cast polymorph", ACTION_RAID + 1), nullptr) + )); + + // Al'ar + triggers.push_back(new TriggerNode("al'ar pulling boss", + NextAction::array(0, new NextAction("al'ar misdirect boss to main tank", ACTION_EMERGENCY + 1), nullptr) + )); + triggers.push_back(new TriggerNode("al'ar boss is flying between platforms", + NextAction::array(0, + new NextAction("al'ar boss tanks move between platforms", ACTION_RAID + 1), + new NextAction("al'ar melee dps move between platforms", ACTION_RAID + 1), + new NextAction("al'ar ranged and ember tank move under platforms", ACTION_RAID + 4), nullptr) + )); + triggers.push_back(new TriggerNode("al'ar boss spawns embers of alar", + NextAction::array(0, + new NextAction("al'ar assist tanks pick up embers", ACTION_RAID + 3), + new NextAction("al'ar ranged dps prioritize embers", ACTION_RAID + 2), nullptr) + )); + triggers.push_back(new TriggerNode("al'ar incoming flame quills", + NextAction::array(0, new NextAction("al'ar jump from platform", ACTION_EMERGENCY + 7), nullptr) + )); + triggers.push_back(new TriggerNode("al'ar rising from the ashes", + NextAction::array(0, new NextAction("al'ar move away from rebirth", ACTION_EMERGENCY + 7), nullptr) + )); + triggers.push_back(new TriggerNode("al'ar everything is on fire in phase 2", + NextAction::array(0, + new NextAction("al'ar swap tanks on boss", ACTION_EMERGENCY + 2), + new NextAction("al'ar avoid flame patches and dive bombs", ACTION_EMERGENCY + 1), nullptr) + )); + triggers.push_back(new TriggerNode("al'ar phase 2 encounter is at room center", + NextAction::array(0, new NextAction("al'ar return to room center", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("al'ar strategy changes between phases", + NextAction::array(0, new NextAction("al'ar manage phase tracker", ACTION_EMERGENCY + 10), nullptr) + )); + + // Void Reaver + triggers.push_back(new TriggerNode("void reaver knock away reduces tank aggro", + NextAction::array(0, + new NextAction("void reaver tanks position boss", ACTION_RAID + 1), + new NextAction("void reaver ranged use aggro dump ability", ACTION_EMERGENCY + 6), nullptr) + )); + triggers.push_back(new TriggerNode("void reaver boss launches arcane orbs", + NextAction::array(0, new NextAction("void reaver spread ranged", ACTION_RAID + 1), nullptr) + )); + + // High Astromancer Solarian + triggers.push_back(new TriggerNode("high astromancer solarian boss casts wrath of the astromancer", + NextAction::array(0, new NextAction("high astromancer solarian ranged leave space for melee", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("high astromancer solarian bot has wrath of the astromancer", + NextAction::array(0, new NextAction("high astromancer solarian move away from group", ACTION_EMERGENCY + 6), nullptr) + )); + triggers.push_back(new TriggerNode("high astromancer solarian boss has vanished", + NextAction::array(0, new NextAction("high astromancer solarian stack for aoe", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("high astromancer solarian solarium priests spawned", + NextAction::array(0, new NextAction("high astromancer solarian target solarium priests", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("high astromancer solarian transformed into voidwalker", + NextAction::array(0, new NextAction("high astromancer solarian tank voidwalker", ACTION_EMERGENCY + 1), nullptr) + )); + triggers.push_back(new TriggerNode("high astromancer solarian boss casts psychic scream", + NextAction::array(0, new NextAction("high astromancer solarian cast fear ward on main tank", ACTION_RAID + 2), nullptr) + )); + + // Kael'thas Sunstrider + triggers.push_back(new TriggerNode("kael'thas sunstrider thaladred is fixated on bot", + NextAction::array(0, new NextAction("kael'thas sunstrider kite thaladred", ACTION_EMERGENCY + 6), nullptr) + )); + triggers.push_back(new TriggerNode("kael'thas sunstrider pulling tankable advisors", + NextAction::array(0, new NextAction("kael'thas sunstrider misdirect advisors to tanks", ACTION_EMERGENCY + 2), nullptr) + )); + triggers.push_back(new TriggerNode("kael'thas sunstrider sanguinar engaged by main tank", + NextAction::array(0, new NextAction("kael'thas sunstrider main tank position sanguinar", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("kael'thas sunstrider sanguinar casts bellowing roar", + NextAction::array(0, new NextAction("kael'thas sunstrider cast fear ward on sanguinar tank", ACTION_RAID + 2), nullptr) + )); + triggers.push_back(new TriggerNode("kael'thas sunstrider capernian requires a warlock tank", + NextAction::array(0, new NextAction("kael'thas sunstrider manage warlock tank strategy", ACTION_EMERGENCY + 10), nullptr) + )); + triggers.push_back(new TriggerNode("kael'thas sunstrider capernian engaged by warlock tank", + NextAction::array(0, new NextAction("kael'thas sunstrider warlock tank position capernian", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("kael'thas sunstrider capernian casts arcane burst and conflagration", + NextAction::array(0, new NextAction("kael'thas sunstrider spread and move away from capernian", ACTION_RAID + 3), nullptr) + )); + triggers.push_back(new TriggerNode("kael'thas sunstrider telonicus engaged by first assist tank", + NextAction::array(0, new NextAction("kael'thas sunstrider first assist tank position telonicus", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("kael'thas sunstrider melee tanks need dedicated healer in phase 3", + NextAction::array(0, new NextAction("kael'thas sunstrider position phase 3 tank healer", ACTION_RAID + 2), nullptr) + )); + triggers.push_back(new TriggerNode("kael'thas sunstrider determining advisor kill order", + NextAction::array(0, new NextAction("kael'thas sunstrider assign advisor dps priority", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("kael'thas sunstrider waiting for tanks to get aggro on advisors", + NextAction::array(0, new NextAction("kael'thas sunstrider manage advisor dps timer", ACTION_EMERGENCY + 10), nullptr) + )); + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are alive", + NextAction::array(0, + new NextAction("kael'thas sunstrider assign legendary weapon dps priority", ACTION_RAID + 1), + new NextAction("kael'thas sunstrider main tank move devastation away", ACTION_EMERGENCY + 1), + new NextAction("kael'thas sunstrider hunter turn away netherstrand longbow", ACTION_EMERGENCY + 1), nullptr) + )); + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are dead and lootable", + NextAction::array(0, new NextAction("kael'thas sunstrider loot legendary weapons", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are equipped", + NextAction::array(0, new NextAction("kael'thas sunstrider use legendary weapons", ACTION_EMERGENCY + 6), nullptr) + )); + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons were lost", + NextAction::array(0, new NextAction("kael'thas sunstrider reequip gear", ACTION_EMERGENCY + 11), nullptr) + )); + triggers.push_back(new TriggerNode("kael'thas sunstrider boss has entered the fight", + NextAction::array(0, + new NextAction("kael'thas sunstrider main tank position boss", ACTION_RAID + 1), + new NextAction("kael'thas sunstrider avoid flame strike", ACTION_EMERGENCY + 8), nullptr) + )); + triggers.push_back(new TriggerNode("kael'thas sunstrider phoenixes and eggs are spawning", + NextAction::array(0, new NextAction("kael'thas sunstrider round up phoenixes and focus down eggs", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("kael'thas sunstrider raid member is mind controlled", + NextAction::array(0, new NextAction("kael'thas sunstrider break mind control with infinity blade", ACTION_EMERGENCY + 1), nullptr) + )); + triggers.push_back(new TriggerNode("kael'thas sunstrider boss is casting pyroblast", + NextAction::array(0, new NextAction("kael'thas sunstrider break through shock barrier", ACTION_EMERGENCY + 7), nullptr) + )); + triggers.push_back(new TriggerNode("kael'thas sunstrider boss is manipulating gravity", + NextAction::array(0, new NextAction("kael'thas sunstrider spread out in midair", ACTION_EMERGENCY + 1), nullptr) + )); +} + +void RaidTempestKeepStrategy::InitMultipliers(std::vector& multipliers) +{ + multipliers.push_back(new AlarMoveBetweenPlatformsMultiplier(botAI)); + multipliers.push_back(new AlarDisableDisperseMultiplier(botAI)); + multipliers.push_back(new AlarDisableTankAssistMultiplier(botAI)); + multipliers.push_back(new AlarStayAwayFromRebirthMultiplier(botAI)); + // multipliers.push_back(new AlarPhase2NoTankingIfArmorMeltedMultiplier(botAI)); + + multipliers.push_back(new VoidReaverDisableTankActionsMultiplier(botAI)); + multipliers.push_back(new VoidReaverMaintainPositionsMultiplier(botAI)); + + multipliers.push_back(new HighAstromancerSolarianDisableTankAssistMultiplier(botAI)); + multipliers.push_back(new HighAstromancerSolarianMaintainPositionMultiplier(botAI)); + + multipliers.push_back(new KaelthasSunstriderKiteThaladredMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderWaitForDpsMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderControlMisdirectionMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderDisableShadowWardMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderManageTankTargetingMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderDisableDisperseMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderDelayBloodlustAndHeroismMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderTryNonfatalBreakingOfMindControlMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderAllDpsOnBossDuringPyroblastMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier(botAI)); +} diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.h b/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.h new file mode 100644 index 00000000000..77fd29c360a --- /dev/null +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.h @@ -0,0 +1,18 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPSTRATEGY_H_ +#define _PLAYERBOT_RAIDTEMPESTKEEPSTRATEGY_H_ + +#include "Strategy.h" +#include "Multiplier.h" + +class RaidTempestKeepStrategy : public Strategy +{ +public: + RaidTempestKeepStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + std::string const getName() override { return "tempestkeep"; } + + void InitTriggers(std::vector& triggers) override; + void InitMultipliers(std::vector& multipliers) override; +}; + +#endif diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h new file mode 100644 index 00000000000..525372c4c16 --- /dev/null +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h @@ -0,0 +1,247 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPTRIGGERCONTEXT_H +#define _PLAYERBOT_RAIDTEMPESTKEEPTRIGGERCONTEXT_H + +#include "RaidTempestKeepTriggers.h" +#include "AiObjectContext.h" + +class RaidTempestKeepTriggerContext : public NamedObjectContext +{ +public: + RaidTempestKeepTriggerContext() + { + // Trash + creators["crimson hand centurion casts arcane volley"] = + &RaidTempestKeepTriggerContext::crimson_hand_centurion_casts_arcane_volley; + + // Al'ar + creators["al'ar pulling boss"] = + &RaidTempestKeepTriggerContext::alar_pulling_boss; + + creators["al'ar boss is flying between platforms"] = + &RaidTempestKeepTriggerContext::alar_boss_is_flying_between_platforms; + + creators["al'ar boss spawns embers of alar"] = + &RaidTempestKeepTriggerContext::alar_boss_spawns_embers_of_alar; + + creators["al'ar incoming flame quills"] = + &RaidTempestKeepTriggerContext::alar_incoming_flame_quills; + + creators["al'ar rising from the ashes"] = + &RaidTempestKeepTriggerContext::alar_rising_from_the_ashes; + + creators["al'ar everything is on fire in phase 2"] = + &RaidTempestKeepTriggerContext::alar_everything_is_on_fire_in_phase_2; + + creators["al'ar phase 2 encounter is at room center"] = + &RaidTempestKeepTriggerContext::alar_phase_2_encounter_is_at_room_center; + + creators["al'ar strategy changes between phases"] = + &RaidTempestKeepTriggerContext::alar_strategy_changes_between_phases; + + // Void Reaver + creators["void reaver knock away reduces tank aggro"] = + &RaidTempestKeepTriggerContext::void_reaver_knock_away_reduces_tank_aggro; + + creators["void reaver boss launches arcane orbs"] = + &RaidTempestKeepTriggerContext::void_reaver_boss_launches_arcane_orbs; + + // High Astromancer Solarian + creators["high astromancer solarian boss casts wrath of the astromancer"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_boss_casts_wrath_of_the_astromancer; + + creators["high astromancer solarian bot has wrath of the astromancer"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_bot_has_wrath_of_the_astromancer; + + creators["high astromancer solarian boss has vanished"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_boss_has_vanished; + + creators["high astromancer solarian solarium priests spawned"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_solarium_priests_spawned; + + creators["high astromancer solarian transformed into voidwalker"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_transformed_into_voidwalker; + + creators["high astromancer solarian boss casts psychic scream"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_boss_casts_psychic_scream; + + // Kael'thas Sunstrider + creators["kael'thas sunstrider thaladred is fixated on bot"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_thaladred_is_fixated_on_bot; + + creators["kael'thas sunstrider pulling tankable advisors"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_pulling_tankable_advisors; + + creators["kael'thas sunstrider sanguinar engaged by main tank"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_sanguinar_engaged_by_main_tank; + + creators["kael'thas sunstrider sanguinar casts bellowing roar"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_sanguinar_casts_bellowing_roar; + + creators["kael'thas sunstrider capernian requires a warlock tank"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_capernian_requires_a_warlock_tank; + + creators["kael'thas sunstrider capernian engaged by warlock tank"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_capernian_engaged_by_warlock_tank; + + creators["kael'thas sunstrider capernian casts arcane burst and conflagration"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_capernian_casts_arcane_burst_and_conflagration; + + creators["kael'thas sunstrider telonicus engaged by first assist tank"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_telonicus_engaged_by_first_assist_tank; + + creators["kael'thas sunstrider melee tanks need dedicated healer in phase 3"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_melee_tanks_need_dedicated_healer_in_phase_3; + + creators["kael'thas sunstrider determining advisor kill order"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_determining_advisor_kill_order; + + creators["kael'thas sunstrider waiting for tanks to get aggro on advisors"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_waiting_for_tanks_to_get_aggro_on_advisors; + + creators["kael'thas sunstrider legendary weapons are alive"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_are_alive; + + creators["kael'thas sunstrider legendary weapons are dead and lootable"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_are_dead_and_lootable; + + creators["kael'thas sunstrider legendary weapons are equipped"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_are_equipped; + + creators["kael'thas sunstrider legendary weapons were lost"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_were_lost; + + creators["kael'thas sunstrider boss has entered the fight"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_boss_has_entered_the_fight; + + creators["kael'thas sunstrider raid member is mind controlled"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_raid_member_is_mind_controlled; + + creators["kael'thas sunstrider phoenixes and eggs are spawning"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_phoenixes_and_eggs_are_spawning; + + creators["kael'thas sunstrider boss is casting pyroblast"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_boss_is_casting_pyroblast; + + creators["kael'thas sunstrider boss is manipulating gravity"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_boss_is_manipulating_gravity; + } + +private: + // Trash + static Trigger* crimson_hand_centurion_casts_arcane_volley( + PlayerbotAI* botAI) { return new CrimsonHandCenturionCastsArcaneVolleyTrigger(botAI); } + + // Al'ar + static Trigger* alar_pulling_boss( + PlayerbotAI* botAI) { return new AlarPullingBossTrigger(botAI); } + + static Trigger* alar_boss_is_flying_between_platforms( + PlayerbotAI* botAI) { return new AlarBossIsFlyingBetweenPlatformsTrigger(botAI); } + + static Trigger* alar_boss_spawns_embers_of_alar( + PlayerbotAI* botAI) { return new AlarBossSpawnsEmbersOfAlarTrigger(botAI); } + + static Trigger* alar_incoming_flame_quills( + PlayerbotAI* botAI) { return new AlarIncomingFlameQuillsTrigger(botAI); } + + static Trigger* alar_rising_from_the_ashes( + PlayerbotAI* botAI) { return new AlarRisingFromTheAshesTrigger(botAI); } + + static Trigger* alar_everything_is_on_fire_in_phase_2( + PlayerbotAI* botAI) { return new AlarEverythingIsOnFireInPhase2Trigger(botAI); } + + static Trigger* alar_phase_2_encounter_is_at_room_center( + PlayerbotAI* botAI) { return new AlarPhase2EncounterIsAtRoomCenterTrigger(botAI); } + + static Trigger* alar_strategy_changes_between_phases( + PlayerbotAI* botAI) { return new AlarStrategyChangesBetweenPhasesTrigger(botAI); } + + // Void Reaver + static Trigger* void_reaver_knock_away_reduces_tank_aggro( + PlayerbotAI* botAI) { return new VoidReaverKnockAwayReducesTankAggroTrigger(botAI); } + + static Trigger* void_reaver_boss_launches_arcane_orbs( + PlayerbotAI* botAI) { return new VoidReaverBossLaunchesArcaneOrbsTrigger(botAI); } + + // High Astromancer Solarian + static Trigger* high_astromancer_solarian_boss_casts_wrath_of_the_astromancer( + PlayerbotAI* botAI) { return new HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger(botAI); } + + static Trigger* high_astromancer_solarian_bot_has_wrath_of_the_astromancer( + PlayerbotAI* botAI) { return new HighAstromancerSolarianBotHasWrathOfTheAstromancerTrigger(botAI); } + + static Trigger* high_astromancer_solarian_boss_has_vanished( + PlayerbotAI* botAI) { return new HighAstromancerSolarianBossHasVanishedTrigger(botAI); } + + static Trigger* high_astromancer_solarian_solarium_priests_spawned( + PlayerbotAI* botAI) { return new HighAstromancerSolarianSolariumPriestsSpawnedTrigger(botAI); } + + static Trigger* high_astromancer_solarian_transformed_into_voidwalker( + PlayerbotAI* botAI) { return new HighAstromancerSolarianTransformedIntoVoidwalkerTrigger(botAI); } + + static Trigger* high_astromancer_solarian_boss_casts_psychic_scream( + PlayerbotAI* botAI) { return new HighAstromancerSolarianBossCastsPsychicScreamTrigger(botAI); } + + // Kael'thas Sunstrider + static Trigger* kaelthas_sunstrider_thaladred_is_fixated_on_bot( + PlayerbotAI* botAI) { return new KaelthasSunstriderThaladredIsFixatedOnBotTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_pulling_tankable_advisors( + PlayerbotAI* botAI) { return new KaelthasSunstriderPullingTankableAdvisorsTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_sanguinar_engaged_by_main_tank( + PlayerbotAI* botAI) { return new KaelthasSunstriderSanguinarEngagedByMainTankTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_sanguinar_casts_bellowing_roar( + PlayerbotAI* botAI) { return new KaelthasSunstriderSanguinarCastsBellowingRoarTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_capernian_requires_a_warlock_tank( + PlayerbotAI* botAI) { return new KaelthasSunstriderCapernianRequiresAWarlockTankTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_capernian_engaged_by_warlock_tank( + PlayerbotAI* botAI) { return new KaelthasSunstriderCapernianEngagedByWarlockTankTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_capernian_casts_arcane_burst_and_conflagration( + PlayerbotAI* botAI) { return new KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_telonicus_engaged_by_first_assist_tank( + PlayerbotAI* botAI) { return new KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_melee_tanks_need_dedicated_healer_in_phase_3( + PlayerbotAI* botAI) { return new KaelthasSunstriderMeleeTanksNeedDedicatedHealerInPhase3Trigger(botAI); } + + static Trigger* kaelthas_sunstrider_determining_advisor_kill_order( + PlayerbotAI* botAI) { return new KaelthasSunstriderDeterminingAdvisorKillOrderTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_waiting_for_tanks_to_get_aggro_on_advisors( + PlayerbotAI* botAI) { return new KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_legendary_weapons_are_alive( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryWeaponsAreAliveTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_legendary_weapons_are_dead_and_lootable( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_legendary_weapons_are_equipped( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_legendary_weapons_were_lost( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryWeaponsWereLostTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_boss_has_entered_the_fight( + PlayerbotAI* botAI) { return new KaelthasSunstriderBossHasEnteredTheFightTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_raid_member_is_mind_controlled( + PlayerbotAI* botAI) { return new KaelthasSunstriderRaidMemberIsMindControlledTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_phoenixes_and_eggs_are_spawning( + PlayerbotAI* botAI) { return new KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_boss_is_casting_pyroblast( + PlayerbotAI* botAI) { return new KaelthasSunstriderBossIsCastingPyroblastTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_boss_is_manipulating_gravity( + PlayerbotAI* botAI) { return new KaelthasSunstriderBossIsManipulatingGravityTrigger(botAI); } +}; + +#endif diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp new file mode 100644 index 00000000000..cdf49aa0e9e --- /dev/null +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp @@ -0,0 +1,566 @@ +#include "RaidTempestKeepTriggers.h" +#include "RaidTempestKeepHelpers.h" +#include "RaidTempestKeepActions.h" +#include "RaidTempestKeepKaelthasBossAI.h" +#include "Playerbots.h" + +using namespace TempestKeepHelpers; + +// Trash + +bool CrimsonHandCenturionCastsArcaneVolleyTrigger::IsActive() +{ + if (bot->getClass() != CLASS_MAGE) + return false; + + Unit* centurion = AI_VALUE2(Unit*, "find target", "crimson hand centurion"); + return centurion && centurion->HasAura(SPELL_ARCANE_FLURRY); +} + +// Al'ar + +bool AlarPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + return alar && alar->GetHealthPct() > 98.0f; +} + +bool AlarBossIsFlyingBetweenPlatformsTrigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar || !alar->IsVisible()) + return false; + + if (isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return false; + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (locationIndex == POINT_QUILL_OR_DIVE_IDX || + locationIndex == POINT_MIDDLE_IDX) + return false; + + return true; +} + +bool AlarBossSpawnsEmbersOfAlarTrigger::IsActive() +{ + if (!botAI->IsTank(bot) && + !botAI->IsRangedDps(bot)) + return false; + + Unit* ember = AI_VALUE2(Unit*, "find target", "ember of al'ar"); + return ember != nullptr; +} + +bool AlarIncomingFlameQuillsTrigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return false; + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + return locationIndex == POINT_QUILL_OR_DIVE_IDX; +} + +bool AlarRisingFromTheAshesTrigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return false; + + Creature* alarCreature = alar->ToCreature(); + return alarCreature && alarCreature->GetReactState() == REACT_PASSIVE; +} + +bool AlarEverythingIsOnFireInPhase2Trigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + return isAlarInPhase2[alar->GetMap()->GetInstanceId()]; +} + +bool AlarPhase2EncounterIsAtRoomCenterTrigger::IsActive() +{ + if (bot->GetVictim()) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (!isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return false; + + Creature* alarCreature = alar->ToCreature(); + if (alarCreature && alarCreature->GetReactState() == REACT_PASSIVE) + return false; + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + return locationIndex != POINT_QUILL_OR_DIVE_IDX; +} + +bool AlarStrategyChangesBetweenPhasesTrigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsDps(member) && + GET_PLAYERBOT_AI(member)) + return member == bot; + } + } + + return false; +} + +// Void Reaver + +bool VoidReaverKnockAwayReducesTankAggroTrigger::IsActive() +{ + if (!botAI->IsRanged(bot) && !botAI->IsTank(bot)) + return false; + + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + return voidReaver && voidReaver->GetVictim() == bot; +} + +bool VoidReaverBossLaunchesArcaneOrbsTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + return voidReaver != nullptr; +} + +// High Astromancer Solarian + +bool HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + if (bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER)) + return false; + + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + return astromancer && !astromancer->HasAura(SPELL_SOLARIAN_TRANSFORM); +} + +bool HighAstromancerSolarianBotHasWrathOfTheAstromancerTrigger::IsActive() +{ + return bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER); +} + +bool HighAstromancerSolarianBossHasVanishedTrigger::IsActive() +{ + if (bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER)) + return false; + + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + if (!astromancer) + return false; + + Creature* astromancerCreature = astromancer->ToCreature(); + return astromancerCreature && + astromancerCreature->GetReactState() == REACT_PASSIVE; +} + +bool HighAstromancerSolarianSolariumPriestsSpawnedTrigger::IsActive() +{ + if (!botAI->IsMelee(bot)) + return false; + + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + if (!astromancer) + return false; + + Unit* solariumPriest = AI_VALUE2(Unit*, "find target", "solarium priest"); + return solariumPriest != nullptr; +} + +bool HighAstromancerSolarianTransformedIntoVoidwalkerTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + return astromancer && astromancer->HasAura(SPELL_SOLARIAN_TRANSFORM); +} + +bool HighAstromancerSolarianBossCastsPsychicScreamTrigger::IsActive() +{ + if (bot->getClass() != CLASS_PRIEST) + return false; + + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + if (!astromancer || !astromancer->HasAura(SPELL_SOLARIAN_TRANSFORM)) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* mainTank = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + + return mainTank && !mainTank->HasAura(SPELL_FEAR_WARD) && + botAI->CanCastSpell("fear ward", mainTank); +} + +// Kael'thas Sunstrider + +bool KaelthasSunstriderThaladredIsFixatedOnBotTrigger::IsActive() +{ + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + if (!thaladred || thaladred->GetVictim() != bot) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return false; + + if (botAI->IsTank(bot) && kaelAI->GetPhase() == PHASE_ALL_ADVISORS) + return false; + + return true; +} + +bool KaelthasSunstriderPullingTankableAdvisorsTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR || + kaelAI->GetPhase() == PHASE_ALL_ADVISORS); +} + +bool KaelthasSunstriderSanguinarEngagedByMainTankTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + return sanguinar && !sanguinar->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !sanguinar->HasAura(SPELL_PERMANENT_FEIGN_DEATH); +} + +bool KaelthasSunstriderSanguinarCastsBellowingRoarTrigger::IsActive() +{ + if (bot->getClass() != CLASS_PRIEST) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return false; + + if (kaelAI->GetPhase() != PHASE_SINGLE_ADVISOR && + kaelAI->GetPhase() != PHASE_ALL_ADVISORS) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* mainTank = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + + return mainTank && !mainTank->HasAura(SPELL_FEAR_WARD) && + botAI->CanCastSpell("fear ward", mainTank); +} + +bool KaelthasSunstriderCapernianRequiresAWarlockTankTrigger::IsActive() +{ + if (bot->getClass() != CLASS_WARLOCK) + return false; + + Player* capernianTank = GetCapernianTank(botAI, bot); + if (!capernianTank || capernianTank != bot) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + return kaelthas != nullptr; +} + +bool KaelthasSunstriderCapernianEngagedByWarlockTankTrigger::IsActive() +{ + if (bot->getClass() != CLASS_WARLOCK) + return false; + + Player* capernianTank = GetCapernianTank(botAI, bot); + if (!capernianTank || capernianTank != bot) + return false; + + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + return capernian && !capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH); +} + +bool KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger::IsActive() +{ + Player* capernianTank = GetCapernianTank(botAI, bot); + if (capernianTank && capernianTank == bot) + return false; + + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + return capernian && !capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH); +} + +bool KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger::IsActive() +{ + if (!botAI->IsAssistTankOfIndex(bot, 0)) + return false; + + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + return telonicus && !telonicus->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !telonicus->HasAura(SPELL_PERMANENT_FEIGN_DEATH); +} + +bool KaelthasSunstriderMeleeTanksNeedDedicatedHealerInPhase3Trigger::IsActive() +{ + if (!botAI->IsHealAssistantOfIndex(bot, 0)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || kaelAI->GetPhase() != PHASE_ALL_ADVISORS) + return false; + + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + if (telonicus && telonicus->IsAlive()) + return true; + + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + if (sanguinar && sanguinar->IsAlive()) + return true; + + return false; +} + +bool KaelthasSunstriderDeterminingAdvisorKillOrderTrigger::IsActive() +{ + if (botAI->IsHeal(bot) || botAI->IsMainTank(bot) || + botAI->IsAssistTankOfIndex(bot, 0)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR || + kaelAI->GetPhase() == PHASE_ALL_ADVISORS); +} + +bool KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger::IsActive() +{ + if (!IsKaelthasInstanceTimerManager(botAI, bot)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR; +} + +bool KaelthasSunstriderLegendaryWeaponsAreAliveTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && kaelAI->GetPhase() == PHASE_WEAPONS; +} + +bool KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + return kaelthas && IsAnyLegendaryWeaponDead(botAI, bot); +} + +bool KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + return bot->HasItemCount(ITEM_STAFF_OF_DISINTEGRATION, 1, false) || + bot->HasItemCount(ITEM_NETHERSTRAND_LONGBOW, 1, false) || + bot->HasItemCount(ITEM_PHASESHIFT_BULWARK, 1, false); +} + +bool KaelthasSunstriderLegendaryWeaponsWereLostTrigger::IsActive() +{ + if (bot->GetMapId() != TEMPEST_KEEP_MAP_ID) + return false; + + const uint32 KAELTHAS_DB_GUID = 158218; + Map* map = bot->GetMap(); + if (!map) + return false; + + auto it = map->GetCreatureBySpawnIdStore().find(KAELTHAS_DB_GUID); + if (it == map->GetCreatureBySpawnIdStore().end()) + return false; + + Creature* kaelthas = it->second; + if (!kaelthas) + return false; + + if (bot->GetExactDist2d(kaelthas) > 150.0f) + return false; + + Item* mainHand = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND); + bool has2HWeapon = mainHand && mainHand->GetTemplate()->InventoryType == INVTYPE_2HWEAPON; + + for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; ++slot) + { + if (slot == EQUIPMENT_SLOT_BODY || slot == EQUIPMENT_SLOT_TABARD) + continue; + + if (slot == EQUIPMENT_SLOT_OFFHAND && has2HWeapon) + continue; + + if (!bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) + { + if (slot == EQUIPMENT_SLOT_OFFHAND) + { + if (!mainHand || mainHand->GetTemplate()->InventoryType != INVTYPE_WEAPON) + continue; + if (HasEquippableOffhand(bot)) + return true; + } + else if (HasEquippableItemForSlot(bot, slot)) + return true; + } + } + + return false; +} + +bool KaelthasSunstriderBossHasEnteredTheFightTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && kaelAI->GetPhase() == PHASE_FINAL; +} + +bool KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + Unit* phoenix = AI_VALUE2(Unit*, "find target", "phoenix"); + Unit* phoenixEgg = AI_VALUE2(Unit*, "find target", "phoenix egg"); + return phoenix != nullptr || phoenixEgg != nullptr; +} + +bool KaelthasSunstriderRaidMemberIsMindControlledTrigger::IsActive() +{ + if (!bot->HasItemCount(ITEM_INFINITY_BLADE, 1, true) || botAI->IsTank(bot)) + return false; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (member->HasAura(SPELL_KAELTHAS_MIND_CONTROL)) + return true; + } + } + + return false; +} + +bool KaelthasSunstriderBossIsCastingPyroblastTrigger::IsActive() +{ + if (!botAI->IsDps(bot)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + return kaelthas && kaelthas->HasAura(SPELL_SHOCK_BARRIER); +} + +bool KaelthasSunstriderBossIsManipulatingGravityTrigger::IsActive() +{ + return bot->HasAura(SPELL_GRAVITY_LAPSE); +} diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h new file mode 100644 index 00000000000..98352ce5be4 --- /dev/null +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h @@ -0,0 +1,312 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPTRIGGERS_H +#define _PLAYERBOT_RAIDTEMPESTKEEPTRIGGERS_H + +#include "Trigger.h" + +// Trash + +class CrimsonHandCenturionCastsArcaneVolleyTrigger : public Trigger +{ +public: + CrimsonHandCenturionCastsArcaneVolleyTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "crimson hand centurion casts arcane volley") {} + bool IsActive() override; +}; + +// Al'ar + +class AlarPullingBossTrigger : public Trigger +{ +public: + AlarPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar pulling boss") {} + bool IsActive() override; +}; + +class AlarBossIsFlyingBetweenPlatformsTrigger : public Trigger +{ +public: + AlarBossIsFlyingBetweenPlatformsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar boss is flying between platforms") {} + bool IsActive() override; +}; + +class AlarBossSpawnsEmbersOfAlarTrigger : public Trigger +{ +public: + AlarBossSpawnsEmbersOfAlarTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar boss spawns embers of alar") {} + bool IsActive() override; +}; + +class AlarIncomingFlameQuillsTrigger : public Trigger +{ +public: + AlarIncomingFlameQuillsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar incoming flame quills") {} + bool IsActive() override; +}; + +class AlarRisingFromTheAshesTrigger : public Trigger +{ +public: + AlarRisingFromTheAshesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar rising from the ashes") {} + bool IsActive() override; +}; + +class AlarEverythingIsOnFireInPhase2Trigger : public Trigger +{ +public: + AlarEverythingIsOnFireInPhase2Trigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar everything is on fire in phase 2") {} + bool IsActive() override; +}; + +class AlarPhase2EncounterIsAtRoomCenterTrigger : public Trigger +{ +public: + AlarPhase2EncounterIsAtRoomCenterTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar phase 2 encounter is at room center") {} + bool IsActive() override; +}; + +class AlarStrategyChangesBetweenPhasesTrigger : public Trigger +{ +public: + AlarStrategyChangesBetweenPhasesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar strategy changes between phases") {} + bool IsActive() override; +}; + +// Void Reaver + +class VoidReaverKnockAwayReducesTankAggroTrigger : public Trigger +{ +public: + VoidReaverKnockAwayReducesTankAggroTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver knock away reduces tank aggro") {} + bool IsActive() override; +}; + +class VoidReaverBossLaunchesArcaneOrbsTrigger : public Trigger +{ +public: + VoidReaverBossLaunchesArcaneOrbsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver boss launches arcane orbs") {} + bool IsActive() override; +}; + +// High Astromancer Solarian + +class HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger : public Trigger +{ +public: + HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian boss casts wrath of the astromancer") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianBotHasWrathOfTheAstromancerTrigger : public Trigger +{ +public: + HighAstromancerSolarianBotHasWrathOfTheAstromancerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian bot has wrath of the astromancer") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianBossHasVanishedTrigger : public Trigger +{ +public: + HighAstromancerSolarianBossHasVanishedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian boss has vanished") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianSolariumPriestsSpawnedTrigger : public Trigger +{ +public: + HighAstromancerSolarianSolariumPriestsSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian solarium priests spawned") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianTransformedIntoVoidwalkerTrigger : public Trigger +{ +public: + HighAstromancerSolarianTransformedIntoVoidwalkerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian transformed into voidwalker") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianBossCastsPsychicScreamTrigger : public Trigger +{ +public: + HighAstromancerSolarianBossCastsPsychicScreamTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian boss casts psychic scream") {} + bool IsActive() override; +}; + +// Kael'thas Sunstrider + +class KaelthasSunstriderThaladredIsFixatedOnBotTrigger : public Trigger +{ +public: + KaelthasSunstriderThaladredIsFixatedOnBotTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider thaladred is fixated on bot") {} + bool IsActive() override; +}; + +class KaelthasSunstriderPullingTankableAdvisorsTrigger : public Trigger +{ +public: + KaelthasSunstriderPullingTankableAdvisorsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider pulling tankable advisors") {} + bool IsActive() override; +}; + +class KaelthasSunstriderSanguinarEngagedByMainTankTrigger : public Trigger +{ +public: + KaelthasSunstriderSanguinarEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider sanguinar engaged by main tank") {} + bool IsActive() override; +}; + +class KaelthasSunstriderSanguinarCastsBellowingRoarTrigger : public Trigger +{ +public: + KaelthasSunstriderSanguinarCastsBellowingRoarTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider sanguinar casts bellowing roar") {} + bool IsActive() override; +}; + +class KaelthasSunstriderCapernianRequiresAWarlockTankTrigger : public Trigger +{ +public: + KaelthasSunstriderCapernianRequiresAWarlockTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider capernian requires a warlock tank") {} + bool IsActive() override; +}; + +class KaelthasSunstriderCapernianEngagedByWarlockTankTrigger : public Trigger +{ +public: + KaelthasSunstriderCapernianEngagedByWarlockTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider capernian engaged by warlock tank") {} + bool IsActive() override; +}; + +class KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger : public Trigger +{ +public: + KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider capernian casts arcane burst and conflagration") {} + bool IsActive() override; +}; + +class KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger : public Trigger +{ +public: + KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider telonicus engaged by first assist tank") {} + bool IsActive() override; +}; + +class KaelthasSunstriderMeleeTanksNeedDedicatedHealerInPhase3Trigger : public Trigger +{ +public: + KaelthasSunstriderMeleeTanksNeedDedicatedHealerInPhase3Trigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider melee tanks need dedicated healer in phase 3") {} + bool IsActive() override; +}; + +class KaelthasSunstriderDeterminingAdvisorKillOrderTrigger : public Trigger +{ +public: + KaelthasSunstriderDeterminingAdvisorKillOrderTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider determining advisor kill order") {} + bool IsActive() override; +}; + +class KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger : public Trigger +{ +public: + KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider waiting for tanks to get aggro on advisors") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryWeaponsAreAliveTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryWeaponsAreAliveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary weapons are alive") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary weapons are dead and lootable") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary weapons are equipped") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryWeaponsWereLostTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryWeaponsWereLostTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary weapons were lost") {} + bool IsActive() override; +}; + +class KaelthasSunstriderBossHasEnteredTheFightTrigger : public Trigger +{ +public: + KaelthasSunstriderBossHasEnteredTheFightTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider boss has entered the fight") {} + bool IsActive() override; +}; + +class KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger : public Trigger +{ +public: + KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider phoenixes and eggs are spawning") {} + bool IsActive() override; +}; + +class KaelthasSunstriderRaidMemberIsMindControlledTrigger : public Trigger +{ +public: + KaelthasSunstriderRaidMemberIsMindControlledTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider raid member is mind controlled") {} + bool IsActive() override; +}; + +class KaelthasSunstriderBossIsCastingPyroblastTrigger : public Trigger +{ +public: + KaelthasSunstriderBossIsCastingPyroblastTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider boss is casting pyroblast") {} + bool IsActive() override; +}; + +class KaelthasSunstriderBossIsManipulatingGravityTrigger : public Trigger +{ +public: + KaelthasSunstriderBossIsManipulatingGravityTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider boss is manipulating gravity") {} + bool IsActive() override; +}; + +#endif From 373ee5c8b9f04e5f1cfbba56fbc98dc76eb3f0a8 Mon Sep 17 00:00:00 2001 From: crow Date: Tue, 23 Dec 2025 13:48:52 -0600 Subject: [PATCH 02/29] initialize variable --- src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp index 9541570a249..bf20f157fd8 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp @@ -1352,7 +1352,7 @@ bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::StayBackFromCaperni return true; } - float safeDistance; + float safeDistance = 0.0f; if (botAI->IsMelee(bot)) safeDistance = 45.0f; else if (botAI->IsRangedDps(bot)) From 2d51da0a2d96bb6d0e51bf5b377a17242a2ad736 Mon Sep 17 00:00:00 2001 From: crow Date: Thu, 25 Dec 2025 18:16:42 -0600 Subject: [PATCH 03/29] Fix Al'ar error --- .../tempestkeep/RaidTempestKeepActions.cpp | 71 +++++-------------- .../tempestkeep/RaidTempestKeepHelpers.cpp | 4 ++ 2 files changed, 22 insertions(+), 53 deletions(-) diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp index bf20f157fd8..0f64ed9dac4 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp @@ -527,24 +527,6 @@ bool AlarSwapTanksOnBossAction::Execute(Event event) if (!alar) return false; - Player* mainTank = nullptr; - Player* assistTank = nullptr; - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member) - continue; - - if (botAI->IsMainTank(member)) - mainTank = member; - - if (botAI->IsAssistTankOfIndex(member, 0)) - assistTank = member; - } - } - if (alar->GetHealth() == alar->GetMaxHealth()) { SetRtiTarget(botAI, "star", alar); @@ -553,36 +535,22 @@ bool AlarSwapTanksOnBossAction::Execute(Event event) return Attack(alar); } - if (mainTank && assistTank && alar->GetVictim() == mainTank && - mainTank->HasAura(SPELL_MELT_ARMOR) && bot == assistTank) + Player* secondEmberTank = GetSecondEmberTank(botAI, alar); + if (secondEmberTank && secondEmberTank != bot) { SetRtiTarget(botAI, "star", alar); - const char* taunts[] = { "taunt", "growl", "hand of reckoning", "dark command" }; - for (const char* spellName : taunts) + if (alar->GetVictim() != bot) { - if (botAI->CanCastSpell(spellName, alar)) - return botAI->CastSpell(spellName, alar); - } - - if (bot->GetTarget() != alar->GetGUID()) - return Attack(alar); - } - - if (mainTank && assistTank && alar->GetVictim() == assistTank && - assistTank->HasAura(SPELL_MELT_ARMOR) && bot == mainTank) - { - SetRtiTarget(botAI, "star", alar); - - const char* taunts[] = { "taunt", "growl", "hand of reckoning", "dark command" }; - for (const char* spellName : taunts) - { - if (botAI->CanCastSpell(spellName, alar)) - return botAI->CastSpell(spellName, alar); + const char* taunts[] = { "taunt", "growl", "hand of reckoning", "dark command" }; + for (const char* spellName : taunts) + { + if (botAI->CanCastSpell(spellName, alar)) + return botAI->CastSpell(spellName, alar); + } } - - if (bot->GetTarget() != alar->GetGUID()) - return Attack(alar); + else if (bot->GetTarget() != alar->GetGUID()) + return Attack(alar); } return false; @@ -662,10 +630,10 @@ bool AlarReturnToRoomCenterAction::Execute(Event event) { const Position& center = ALAR_ROOM_CENTER; if (bot->GetVictim() == nullptr && - bot->GetExactDist2d(center.GetPositionX(), center.GetPositionY()) > 50.0f) + bot->GetExactDist2d(center.GetPositionX(), center.GetPositionY()) > 45.0f) { return MoveInside(TEMPEST_KEEP_MAP_ID, center.GetPositionX(), center.GetPositionY(), - center.GetPositionZ(), 50.0f, MovementPriority::MOVEMENT_COMBAT); + center.GetPositionZ(), 40.0f, MovementPriority::MOVEMENT_COMBAT); } return false; @@ -728,8 +696,7 @@ bool VoidReaverRangedUseAggroDumpAbilityAction::Execute(Event event) if (!voidReaver) return false; - bot->AttackStop(); - bot->InterruptNonMeleeSpells(true); + botAI->Reset(); static const std::array spells = { "divine protection", @@ -862,8 +829,7 @@ bool HighAstromancerSolarianMoveAwayFromGroupAction::Execute(Event event) Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance); if (nearestPlayer) { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(true); + botAI->Reset(); return MoveFromGroup(safeDistance + 1.0f); } @@ -1040,12 +1006,11 @@ bool KaelthasSunstriderKiteThaladredAction::Execute(Event event) return false; float currentDistance = bot->GetExactDist2d(thaladred); - const float safeDistance = 25.0f; + const float safeDistance = 20.0f; if (currentDistance < safeDistance) { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(true); - return MoveAway(thaladred, safeDistance - currentDistance + 5.0f); + botAI->Reset(); + return MoveAway(thaladred, safeDistance - currentDistance); } return false; diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp index 43352125011..3d50187aba2 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp @@ -382,8 +382,10 @@ namespace TempestKeepHelpers Player* member = ref->GetSource(); if (!member) continue; + if (botAI->IsMainTank(member)) mainTank = member; + if (botAI->IsAssistTankOfIndex(member, 0)) assistTank = member; } @@ -394,8 +396,10 @@ namespace TempestKeepHelpers if (!mainTankHasMelt && !assistTankHasMelt) return assistTank; + if (mainTankHasMelt) return mainTank; + if (assistTankHasMelt) return assistTank; From 376d2909da8fffedfe2c0be7047a9247e8102cf3 Mon Sep 17 00:00:00 2001 From: crow Date: Tue, 30 Dec 2025 09:38:50 -0600 Subject: [PATCH 04/29] Some streamlining + fix Warlock strategy swap --- .../tempestkeep/RaidTempestKeepActions.cpp | 39 +++++++----- .../RaidTempestKeepMultipliers.cpp | 47 ++++++++------ .../tempestkeep/RaidTempestKeepStrategy.cpp | 19 +++--- .../RaidTempestKeepTriggerContext.h | 20 ++---- .../tempestkeep/RaidTempestKeepTriggers.cpp | 62 +++---------------- .../tempestkeep/RaidTempestKeepTriggers.h | 22 +------ 6 files changed, 74 insertions(+), 135 deletions(-) diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp index 0f64ed9dac4..38e254ef820 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp @@ -402,6 +402,7 @@ bool AlarRangedDpsPrioritizeEmbersAction::Execute(Event event) return MoveAway(firstEmber, safeDistance - bot->GetDistance2d(firstEmber)); } SetRtiTarget(botAI, "square", firstEmber); + if (bot->GetTarget() != firstEmber->GetGUID()) return Attack(firstEmber); } @@ -414,12 +415,14 @@ bool AlarRangedDpsPrioritizeEmbersAction::Execute(Event event) return MoveAway(secondEmber, safeDistance - bot->GetDistance2d(secondEmber)); } SetRtiTarget(botAI, "circle", secondEmber); + if (bot->GetTarget() != secondEmber->GetGUID()) return Attack(secondEmber); } else if (Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar")) { SetRtiTarget(botAI, "star", alar); + if (bot->GetTarget() != alar->GetGUID()) return Attack(alar); } @@ -509,8 +512,7 @@ bool AlarMoveAwayFromRebirthAction::Execute(Event event) const float safeDistance = 20.0f; if (currentDistance < safeDistance) { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(true); + botAI->Reset(); return MoveAway(alar, safeDistance - currentDistance); } @@ -540,7 +542,11 @@ bool AlarSwapTanksOnBossAction::Execute(Event event) { SetRtiTarget(botAI, "star", alar); - if (alar->GetVictim() != bot) + if (bot->GetTarget() != alar->GetGUID()) + { + return Attack(alar); + } + else if (alar->GetVictim() != bot) { const char* taunts[] = { "taunt", "growl", "hand of reckoning", "dark command" }; for (const char* spellName : taunts) @@ -549,8 +555,6 @@ bool AlarSwapTanksOnBossAction::Execute(Event event) return botAI->CastSpell(spellName, alar); } } - else if (bot->GetTarget() != alar->GetGUID()) - return Attack(alar); } return false; @@ -955,6 +959,9 @@ Unit* HighAstromancerSolarianTargetSolariumPriestsAction::AssignSolariumPriestsT bool HighAstromancerSolarianTankVoidwalkerAction::Execute(Event event) { + if (!botAI->IsMainTank(bot)) + return false; + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); if (!astromancer) return false; @@ -977,6 +984,9 @@ bool HighAstromancerSolarianTankVoidwalkerAction::Execute(Event event) bool HighAstromancerSolarianCastFearWardOnMainTankAction::Execute(Event event) { + if (bot->getClass() != CLASS_PRIEST) + return false; + Player* mainTank = nullptr; if (Group* group = bot->GetGroup()) { @@ -1175,7 +1185,7 @@ bool KaelthasSunstriderManageWarlockTankStrategyAction::Execute(Event event) if (kaelAI->GetPhase() == PHASE_WEAPONS) { if (currentlyTank) - botAI->ResetStrategies(false); + botAI->ChangeStrategy("-tank", BotState::BOT_STATE_COMBAT); return false; } @@ -1198,7 +1208,7 @@ bool KaelthasSunstriderManageWarlockTankStrategyAction::Execute(Event event) botAI->ChangeStrategy("+tank", BotState::BOT_STATE_COMBAT); // If Capernian is dead, reset to DPS for remainder of encounter else if (!capernian && currentlyTank) - botAI->ResetStrategies(false); + botAI->ChangeStrategy("-tank", BotState::BOT_STATE_COMBAT); } return false; @@ -1215,7 +1225,8 @@ bool KaelthasSunstriderWarlockTankPositionCapernianAction::Execute(Event event) return false; Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); - if (!capernian) + if (!capernian || capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || + capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) return false; MarkTargetWithCircle(bot, capernian); @@ -1328,16 +1339,13 @@ bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::StayBackFromCaperni float currentDistance = bot->GetExactDist2d(capernian); if (currentDistance < safeDistance) { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(true); + botAI->Reset(); return MoveAway(capernian, safeDistance - currentDistance + 1.0f); } if (botAI->IsMelee(bot)) { - bot->SetTarget(ObjectGuid::Empty); - bot->AttackStop(); - bot->InterruptNonMeleeSpells(true); + botAI->Reset(); return true; } @@ -2005,8 +2013,7 @@ bool KaelthasSunstriderAvoidFlameStrikeAction::Execute(Event event) Position safestPos = FindSafestNearbyPosition(bot, flameStrikes, 30.0f, hazardRadius); - bot->AttackStop(); - bot->InterruptNonMeleeSpells(true); + botAI->Reset(); return MoveTo(TEMPEST_KEEP_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), safestPos.GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_COMBAT, true, false); @@ -2207,7 +2214,7 @@ bool KaelthasSunstriderSpreadOutInMidairAction::Execute(Event event) if (!group) return false; - const float minSpreadDistance = 15.0f; + const float minSpreadDistance = 16.0f; std::vector nearbyPlayers; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.cpp index 4091f6ff704..0038e69c71f 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.cpp @@ -103,7 +103,7 @@ float AlarPhase2NoTankingIfArmorMeltedMultiplier::GetValue(Action* action) return 1.0f; Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); - if (alar) + if (alar && bot->GetTarget() == alar->GetGUID()) { if (dynamic_cast(action) || dynamic_cast(action) || @@ -333,27 +333,29 @@ float KaelthasSunstriderManageTankTargetingMultiplier::GetValue(Action* action) return 1.0f; // Try to keep main tank from grabbing aggro on any weapon other than the axe - if (botAI->IsMainTank(bot) && kaelAI->GetPhase() == PHASE_WEAPONS) + if (kaelAI->GetPhase() == PHASE_WEAPONS) { - 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)) - return 0.0f; + if (botAI->IsMainTank(bot)) + { + 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)) + return 0.0f; + } } - - if (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR || + else if (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR || kaelAI->GetPhase() == PHASE_ALL_ADVISORS) { if (bot->GetVictim() != nullptr) @@ -362,6 +364,11 @@ float KaelthasSunstriderManageTankTargetingMultiplier::GetValue(Action* action) return 0.0f; } } + else if (kaelAI->GetPhase() == PHASE_FINAL) + { + if (dynamic_cast(action)) + return 0.0f; + } return 1.0f; } diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp index 21a9231f68e..b0b21450103 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp @@ -65,12 +65,10 @@ void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) NextAction::array(0, new NextAction("high astromancer solarian target solarium priests", ACTION_RAID + 1), nullptr) )); triggers.push_back(new TriggerNode("high astromancer solarian transformed into voidwalker", - NextAction::array(0, new NextAction("high astromancer solarian tank voidwalker", ACTION_EMERGENCY + 1), nullptr) - )); - triggers.push_back(new TriggerNode("high astromancer solarian boss casts psychic scream", - NextAction::array(0, new NextAction("high astromancer solarian cast fear ward on main tank", ACTION_RAID + 2), nullptr) + NextAction::array(0, + new NextAction("high astromancer solarian tank voidwalker", ACTION_EMERGENCY + 1), + new NextAction("high astromancer solarian cast fear ward on main tank", ACTION_RAID + 2), nullptr) )); - // Kael'thas Sunstrider triggers.push_back(new TriggerNode("kael'thas sunstrider thaladred is fixated on bot", NextAction::array(0, new NextAction("kael'thas sunstrider kite thaladred", ACTION_EMERGENCY + 6), nullptr) @@ -84,11 +82,10 @@ void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("kael'thas sunstrider sanguinar casts bellowing roar", NextAction::array(0, new NextAction("kael'thas sunstrider cast fear ward on sanguinar tank", ACTION_RAID + 2), nullptr) )); - triggers.push_back(new TriggerNode("kael'thas sunstrider capernian requires a warlock tank", - NextAction::array(0, new NextAction("kael'thas sunstrider manage warlock tank strategy", ACTION_EMERGENCY + 10), nullptr) - )); - triggers.push_back(new TriggerNode("kael'thas sunstrider capernian engaged by warlock tank", - NextAction::array(0, new NextAction("kael'thas sunstrider warlock tank position capernian", ACTION_RAID + 1), nullptr) + triggers.push_back(new TriggerNode("kael'thas sunstrider capernian should be tanked by a warlock", + NextAction::array(0, + new NextAction("kael'thas sunstrider manage warlock tank strategy", ACTION_EMERGENCY + 10), + new NextAction("kael'thas sunstrider warlock tank position capernian", ACTION_RAID + 1), nullptr) )); triggers.push_back(new TriggerNode("kael'thas sunstrider capernian casts arcane burst and conflagration", NextAction::array(0, new NextAction("kael'thas sunstrider spread and move away from capernian", ACTION_RAID + 3), nullptr) @@ -145,7 +142,7 @@ void RaidTempestKeepStrategy::InitMultipliers(std::vector& multipli multipliers.push_back(new AlarDisableDisperseMultiplier(botAI)); multipliers.push_back(new AlarDisableTankAssistMultiplier(botAI)); multipliers.push_back(new AlarStayAwayFromRebirthMultiplier(botAI)); - // multipliers.push_back(new AlarPhase2NoTankingIfArmorMeltedMultiplier(botAI)); + multipliers.push_back(new AlarPhase2NoTankingIfArmorMeltedMultiplier(botAI)); multipliers.push_back(new VoidReaverDisableTankActionsMultiplier(botAI)); multipliers.push_back(new VoidReaverMaintainPositionsMultiplier(botAI)); diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h index 525372c4c16..0068dc35b78 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h @@ -61,9 +61,6 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext creators["high astromancer solarian transformed into voidwalker"] = &RaidTempestKeepTriggerContext::high_astromancer_solarian_transformed_into_voidwalker; - creators["high astromancer solarian boss casts psychic scream"] = - &RaidTempestKeepTriggerContext::high_astromancer_solarian_boss_casts_psychic_scream; - // Kael'thas Sunstrider creators["kael'thas sunstrider thaladred is fixated on bot"] = &RaidTempestKeepTriggerContext::kaelthas_sunstrider_thaladred_is_fixated_on_bot; @@ -77,11 +74,8 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext creators["kael'thas sunstrider sanguinar casts bellowing roar"] = &RaidTempestKeepTriggerContext::kaelthas_sunstrider_sanguinar_casts_bellowing_roar; - creators["kael'thas sunstrider capernian requires a warlock tank"] = - &RaidTempestKeepTriggerContext::kaelthas_sunstrider_capernian_requires_a_warlock_tank; - - creators["kael'thas sunstrider capernian engaged by warlock tank"] = - &RaidTempestKeepTriggerContext::kaelthas_sunstrider_capernian_engaged_by_warlock_tank; + creators["kael'thas sunstrider capernian should be tanked by a warlock"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_capernian_should_be_tanked_by_a_warlock; creators["kael'thas sunstrider capernian casts arcane burst and conflagration"] = &RaidTempestKeepTriggerContext::kaelthas_sunstrider_capernian_casts_arcane_burst_and_conflagration; @@ -179,9 +173,6 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext static Trigger* high_astromancer_solarian_transformed_into_voidwalker( PlayerbotAI* botAI) { return new HighAstromancerSolarianTransformedIntoVoidwalkerTrigger(botAI); } - static Trigger* high_astromancer_solarian_boss_casts_psychic_scream( - PlayerbotAI* botAI) { return new HighAstromancerSolarianBossCastsPsychicScreamTrigger(botAI); } - // Kael'thas Sunstrider static Trigger* kaelthas_sunstrider_thaladred_is_fixated_on_bot( PlayerbotAI* botAI) { return new KaelthasSunstriderThaladredIsFixatedOnBotTrigger(botAI); } @@ -195,11 +186,8 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext static Trigger* kaelthas_sunstrider_sanguinar_casts_bellowing_roar( PlayerbotAI* botAI) { return new KaelthasSunstriderSanguinarCastsBellowingRoarTrigger(botAI); } - static Trigger* kaelthas_sunstrider_capernian_requires_a_warlock_tank( - PlayerbotAI* botAI) { return new KaelthasSunstriderCapernianRequiresAWarlockTankTrigger(botAI); } - - static Trigger* kaelthas_sunstrider_capernian_engaged_by_warlock_tank( - PlayerbotAI* botAI) { return new KaelthasSunstriderCapernianEngagedByWarlockTankTrigger(botAI); } + static Trigger* kaelthas_sunstrider_capernian_should_be_tanked_by_a_warlock( + PlayerbotAI* botAI) { return new KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger(botAI); } static Trigger* kaelthas_sunstrider_capernian_casts_arcane_burst_and_conflagration( PlayerbotAI* botAI) { return new KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger(botAI); } diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp index cdf49aa0e9e..a2ccaddeaeb 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp @@ -206,51 +206,19 @@ bool HighAstromancerSolarianSolariumPriestsSpawnedTrigger::IsActive() if (!botAI->IsMelee(bot)) return false; - Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); - if (!astromancer) - return false; - Unit* solariumPriest = AI_VALUE2(Unit*, "find target", "solarium priest"); return solariumPriest != nullptr; } bool HighAstromancerSolarianTransformedIntoVoidwalkerTrigger::IsActive() { - if (!botAI->IsMainTank(bot)) + if (!botAI->IsMainTank(bot) && bot->getClass() != CLASS_PRIEST) return false; Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); return astromancer && astromancer->HasAura(SPELL_SOLARIAN_TRANSFORM); } -bool HighAstromancerSolarianBossCastsPsychicScreamTrigger::IsActive() -{ - if (bot->getClass() != CLASS_PRIEST) - return false; - - Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); - if (!astromancer || !astromancer->HasAura(SPELL_SOLARIAN_TRANSFORM)) - return false; - - Group* group = bot->GetGroup(); - if (!group) - return false; - - Player* mainTank = nullptr; - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && botAI->IsMainTank(member)) - { - mainTank = member; - break; - } - } - - return mainTank && !mainTank->HasAura(SPELL_FEAR_WARD) && - botAI->CanCastSpell("fear ward", mainTank); -} - // Kael'thas Sunstrider bool KaelthasSunstriderThaladredIsFixatedOnBotTrigger::IsActive() @@ -333,7 +301,7 @@ bool KaelthasSunstriderSanguinarCastsBellowingRoarTrigger::IsActive() botAI->CanCastSpell("fear ward", mainTank); } -bool KaelthasSunstriderCapernianRequiresAWarlockTankTrigger::IsActive() +bool KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger::IsActive() { if (bot->getClass() != CLASS_WARLOCK) return false; @@ -346,20 +314,6 @@ bool KaelthasSunstriderCapernianRequiresAWarlockTankTrigger::IsActive() return kaelthas != nullptr; } -bool KaelthasSunstriderCapernianEngagedByWarlockTankTrigger::IsActive() -{ - if (bot->getClass() != CLASS_WARLOCK) - return false; - - Player* capernianTank = GetCapernianTank(botAI, bot); - if (!capernianTank || capernianTank != bot) - return false; - - Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); - return capernian && !capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && - !capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH); -} - bool KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger::IsActive() { Player* capernianTank = GetCapernianTank(botAI, bot); @@ -521,13 +475,15 @@ bool KaelthasSunstriderBossHasEnteredTheFightTrigger::IsActive() bool KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger::IsActive() { - Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); - if (!kaelthas) - return false; - Unit* phoenix = AI_VALUE2(Unit*, "find target", "phoenix"); + if (phoenix != nullptr) + return true; + Unit* phoenixEgg = AI_VALUE2(Unit*, "find target", "phoenix egg"); - return phoenix != nullptr || phoenixEgg != nullptr; + if (phoenixEgg != nullptr) + return true; + + return false; } bool KaelthasSunstriderRaidMemberIsMindControlledTrigger::IsActive() diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h index 98352ce5be4..b8daff2e582 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h @@ -139,14 +139,6 @@ class HighAstromancerSolarianTransformedIntoVoidwalkerTrigger : public Trigger bool IsActive() override; }; -class HighAstromancerSolarianBossCastsPsychicScreamTrigger : public Trigger -{ -public: - HighAstromancerSolarianBossCastsPsychicScreamTrigger( - PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian boss casts psychic scream") {} - bool IsActive() override; -}; - // Kael'thas Sunstrider class KaelthasSunstriderThaladredIsFixatedOnBotTrigger : public Trigger @@ -181,19 +173,11 @@ class KaelthasSunstriderSanguinarCastsBellowingRoarTrigger : public Trigger bool IsActive() override; }; -class KaelthasSunstriderCapernianRequiresAWarlockTankTrigger : public Trigger -{ -public: - KaelthasSunstriderCapernianRequiresAWarlockTankTrigger( - PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider capernian requires a warlock tank") {} - bool IsActive() override; -}; - -class KaelthasSunstriderCapernianEngagedByWarlockTankTrigger : public Trigger +class KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger : public Trigger { public: - KaelthasSunstriderCapernianEngagedByWarlockTankTrigger( - PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider capernian engaged by warlock tank") {} + KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider capernian should be tanked by a warlock") {} bool IsActive() override; }; From 6497790e4fb5ae345975fba1d09a722ea095d051 Mon Sep 17 00:00:00 2001 From: crow Date: Thu, 1 Jan 2026 20:00:18 -0600 Subject: [PATCH 05/29] bunch of tweaks Fix Centurion method Refactor Ember handling Add new KT method for tanks + cleanups --- .../RaidTempestKeepActionContext.h | 24 +- .../tempestkeep/RaidTempestKeepActions.cpp | 273 ++++++++---------- .../tempestkeep/RaidTempestKeepActions.h | 18 +- .../tempestkeep/RaidTempestKeepHelpers.cpp | 22 +- .../RaidTempestKeepMultipliers.cpp | 94 ++---- .../tempestkeep/RaidTempestKeepMultipliers.h | 14 +- .../tempestkeep/RaidTempestKeepStrategy.cpp | 18 +- .../RaidTempestKeepTriggerContext.h | 8 +- .../tempestkeep/RaidTempestKeepTriggers.cpp | 50 ++-- .../tempestkeep/RaidTempestKeepTriggers.h | 6 +- 10 files changed, 226 insertions(+), 301 deletions(-) diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActionContext.h b/src/strategy/raids/tempestkeep/RaidTempestKeepActionContext.h index e4bf7c3908c..fc80ff75fcf 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepActionContext.h +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepActionContext.h @@ -104,8 +104,8 @@ class RaidTempestKeepActionContext : public NamedObjectContext creators["kael'thas sunstrider first assist tank position telonicus"] = &RaidTempestKeepActionContext::kaelthas_sunstrider_first_assist_tank_position_telonicus; - creators["kael'thas sunstrider position phase 3 tank healer"] = - &RaidTempestKeepActionContext::kaelthas_sunstrider_position_phase_3_tank_healer; + creators["kael'thas sunstrider handle sanguinar and telonicus in phase 3"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_handle_sanguinar_and_telonicus_in_phase_3; creators["kael'thas sunstrider assign advisor dps priority"] = &RaidTempestKeepActionContext::kaelthas_sunstrider_assign_advisor_dps_priority; @@ -137,11 +137,11 @@ class RaidTempestKeepActionContext : public NamedObjectContext creators["kael'thas sunstrider avoid flame strike"] = &RaidTempestKeepActionContext::kaelthas_sunstrider_avoid_flame_strike; - creators["kael'thas sunstrider round up phoenixes and focus down eggs"] = - &RaidTempestKeepActionContext::kaelthas_sunstrider_round_up_phoenixes_and_focus_down_eggs; + creators["kael'thas sunstrider handle phoenixes and eggs"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_handle_phoenixes_and_eggs; - creators["kael'thas sunstrider break mind control with infinity blade"] = - &RaidTempestKeepActionContext::kaelthas_sunstrider_break_mind_control_with_infinity_blade; + creators["kael'thas sunstrider break mind control"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_break_mind_control; creators["kael'thas sunstrider break through shock barrier"] = &RaidTempestKeepActionContext::kaelthas_sunstrider_break_through_shock_barrier; @@ -246,8 +246,8 @@ class RaidTempestKeepActionContext : public NamedObjectContext static Action* kaelthas_sunstrider_first_assist_tank_position_telonicus( PlayerbotAI* botAI) { return new KaelthasSunstriderFirstAssistTankPositionTelonicusAction(botAI); } - static Action* kaelthas_sunstrider_position_phase_3_tank_healer( - PlayerbotAI* botAI) { return new KaelthasSunstriderPositionPhase3TankHealerAction(botAI); } + static Action* kaelthas_sunstrider_handle_sanguinar_and_telonicus_in_phase_3( + PlayerbotAI* botAI) { return new KaelthasSunstriderHandleSanguinarAndTelonicusInPhase3Action(botAI); } static Action* kaelthas_sunstrider_assign_advisor_dps_priority( PlayerbotAI* botAI) { return new KaelthasSunstriderAssignAdvisorDpsPriorityAction(botAI); } @@ -279,11 +279,11 @@ class RaidTempestKeepActionContext : public NamedObjectContext static Action* kaelthas_sunstrider_avoid_flame_strike( PlayerbotAI* botAI) { return new KaelthasSunstriderAvoidFlameStrikeAction(botAI); } - static Action* kaelthas_sunstrider_round_up_phoenixes_and_focus_down_eggs( - PlayerbotAI* botAI) { return new KaelthasSunstriderRoundUpPhoenixesAndFocusDownEggsAction(botAI); } + static Action* kaelthas_sunstrider_handle_phoenixes_and_eggs( + PlayerbotAI* botAI) { return new KaelthasSunstriderHandlePhoenixesAndEggsAction(botAI); } - static Action* kaelthas_sunstrider_break_mind_control_with_infinity_blade( - PlayerbotAI* botAI) { return new KaelthasSunstriderBreakMindControlWithInfinityBladeAction(botAI); } + static Action* kaelthas_sunstrider_break_mind_control( + PlayerbotAI* botAI) { return new KaelthasSunstriderBreakMindControlAction(botAI); } static Action* kaelthas_sunstrider_break_through_shock_barrier( PlayerbotAI* botAI) { return new KaelthasSunstriderBreakThroughShockBarrierAction(botAI); } diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp index 38e254ef820..68904c76ba4 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp @@ -18,15 +18,21 @@ bool CrimsonHandCenturionCastPolymorphAction::Execute(Event event) if (!centurion) return false; - if (centurion->GetHealth() == centurion->GetMaxHealth() && - !centurion->HasAura(SPELL_POLYMORPH_SHEEP) && - !centurion->HasAura(SPELL_POLYMORPH_TURTLE) && - !centurion->HasAura(SPELL_POLYMORPH_PIG)) + if (centurion->GetHealth() == centurion->GetMaxHealth()) { - return botAI->CastSpell("polymorph", centurion); + if (!centurion->HasAura(SPELL_POLYMORPH_SHEEP) && + !centurion->HasAura(SPELL_POLYMORPH_TURTLE) && + !centurion->HasAura(SPELL_POLYMORPH_PIG)) + { + if (botAI->CanCastSpell("polymorph", centurion)) + return botAI->CastSpell("polymorph", centurion); + } + } + else if (centurion->HasAura(SPELL_ARCANE_FLURRY)) + { + if (botAI->CanCastSpell("polymorph", centurion)) + return botAI->CastSpell("polymorph", centurion); } - else if (botAI->CanCastSpell("polymorph", centurion)) - return botAI->CastSpell("polymorph", centurion); return false; } @@ -81,12 +87,13 @@ bool AlarBossTanksMoveBetweenPlatformsAction::Execute(Event event) locationIndex = GetAlarDestinationLocationIndex(alar, dest); } - bool mtAction = PositionMainTank(botAI->IsMainTank(bot) - ? bot : nullptr, alar, locationIndex); - bool atAction = PositionAssistTank(botAI->IsAssistTankOfIndex(bot, 0) - ? bot : nullptr, alar, locationIndex); + if (botAI->IsMainTank(bot) && PositionMainTank(bot, alar, locationIndex)) + return true; - return mtAction || atAction; + if (botAI->IsAssistTankOfIndex(bot, 0) && PositionAssistTank(bot, alar, locationIndex)) + return true; + + return false; } bool AlarBossTanksMoveBetweenPlatformsAction::PositionMainTank( @@ -352,34 +359,30 @@ bool AlarAssistTanksPickUpEmbersAction::HandlePhase2Embers(Unit* alar) return MoveFromGroup(safeDistance + 1.0f); } } - else + else if (GetSecondEmberTank(botAI, alar) == bot && secondEmber) { - Player* secondEmberTank = GetSecondEmberTank(botAI, alar); - if (secondEmberTank && bot == secondEmberTank && secondEmber) - { - MarkTargetWithCircle(bot, secondEmber); - SetRtiTarget(botAI, "circle", secondEmber); + MarkTargetWithCircle(bot, secondEmber); + SetRtiTarget(botAI, "circle", secondEmber); - if (secondEmber->GetVictim() != bot) - { - if (bot->GetTarget() != secondEmber->GetGUID()) - return Attack(secondEmber); + if (secondEmber->GetVictim() != bot) + { + if (bot->GetTarget() != secondEmber->GetGUID()) + return Attack(secondEmber); - const char* taunts[] = { "taunt", "growl", "hand of reckoning", "dark command" }; - for (const char* spellName : taunts) - { - if (botAI->CanCastSpell(spellName, secondEmber)) - return botAI->CastSpell(spellName, secondEmber); - } - } - else if (bot->IsWithinMeleeRange(secondEmber)) + const char* taunts[] = { "taunt", "growl", "hand of reckoning", "dark command" }; + for (const char* spellName : taunts) { - const float safeDistance = 15.0f; - Unit* nearestPlayer = GetNearestNonTankPlayerInRadius(bot, safeDistance); - if (nearestPlayer) - return MoveFromGroup(safeDistance + 1.0f); + if (botAI->CanCastSpell(spellName, secondEmber)) + return botAI->CastSpell(spellName, secondEmber); } } + else if (bot->IsWithinMeleeRange(secondEmber)) + { + const float safeDistance = 15.0f; + Unit* nearestPlayer = GetNearestNonTankPlayerInRadius(bot, safeDistance); + if (nearestPlayer) + return MoveFromGroup(safeDistance + 1.0f); + } } return false; @@ -588,7 +591,7 @@ bool AlarAvoidFlamePatchesAndDiveBombsAction::AvoidFlamePatch() bot->InterruptNonMeleeSpells(true); return MoveTo(TEMPEST_KEEP_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), safestPos.GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_COMBAT, true, false); + MovementPriority::MOVEMENT_FORCED, true, false); } } @@ -674,20 +677,20 @@ bool VoidReaverTanksPositionBossAction::Execute(Event event) if (!voidReaver) return false; - const Position& tankPosition = VOID_REAVER_TANK_POSITION; + const Position& position = VOID_REAVER_TANK_POSITION; - float dX = tankPosition.GetPositionX() - bot->GetPositionX(); - float dY = tankPosition.GetPositionY() - bot->GetPositionY(); - float distanceToTankPosition = bot->GetExactDist2d(tankPosition.GetPositionX(), - tankPosition.GetPositionY()); + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), + position.GetPositionY()); - if (bot->IsWithinMeleeRange(voidReaver) && distanceToTankPosition > 2.0f) + if (bot->IsWithinMeleeRange(voidReaver) && distanceToPosition > 2.0f) { - float moveDist = std::min(5.0f, distanceToTankPosition); - float moveX = bot->GetPositionX() + (dX / distanceToTankPosition) * moveDist; - float moveY = bot->GetPositionY() + (dY / distanceToTankPosition) * moveDist; + float moveDist = std::min(5.0f, distanceToPosition); + float moveX = bot->GetPositionX() + (dX / distanceToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distanceToPosition) * moveDist; - return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, tankPosition.GetPositionZ(), false, + return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, true); } @@ -728,7 +731,7 @@ bool VoidReaverSpreadRangedAction::Execute(Event event) if (!voidReaver) return false; - if (voidReaver->GetHealth() == voidReaver->GetMaxHealth()) + if (voidReaver->GetHealthPct() > 99.8f) { initialVoidReaverPositions.clear(); hasReachedInitialVoidReaverPosition.clear(); @@ -752,7 +755,7 @@ bool VoidReaverSpreadRangedAction::Execute(Event event) } } - const Position& tankPosition = VOID_REAVER_TANK_POSITION; + const Position& position = VOID_REAVER_TANK_POSITION; const float radius = 30.0f; const float offsetArc = 1.0f; const uint8 botsPerRing = 8; @@ -768,8 +771,8 @@ bool VoidReaverSpreadRangedAction::Execute(Event event) float ringRadius = radius + (ringIndex * offsetArc); float angle = 2 * M_PI * posInRing / botsPerRing; - float targetX = tankPosition.GetPositionX() + ringRadius * std::cos(angle); - float targetY = tankPosition.GetPositionY() + ringRadius * std::sin(angle); + float targetX = position.GetPositionX() + ringRadius * std::cos(angle); + float targetY = position.GetPositionY() + ringRadius * std::sin(angle); Position pos(targetX, targetY, ranged->GetPositionZ()); initialVoidReaverPositions[ranged->GetGUID()] = pos; @@ -1016,7 +1019,7 @@ bool KaelthasSunstriderKiteThaladredAction::Execute(Event event) return false; float currentDistance = bot->GetExactDist2d(thaladred); - const float safeDistance = 20.0f; + const float safeDistance = 22.0f; if (currentDistance < safeDistance) { botAI->Reset(); @@ -1178,30 +1181,23 @@ bool KaelthasSunstriderManageWarlockTankStrategyAction::Execute(Event event) if (sanguinar && sanguinar->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) botAI->ChangeStrategy("+tank", BotState::BOT_STATE_COMBAT); } - return false; } - // Phase 2: Switch to DPS - if (kaelAI->GetPhase() == PHASE_WEAPONS) + else if (kaelAI->GetPhase() == PHASE_WEAPONS) { if (currentlyTank) botAI->ChangeStrategy("-tank", BotState::BOT_STATE_COMBAT); - return false; } - // Phase 2→3 Transition: Weapons dead, waiting for advisors - switch to tank - if (kaelAI->GetPhase() == PHASE_TRANSITION) + else if (kaelAI->GetPhase() == PHASE_TRANSITION) { if (!currentlyTank) botAI->ChangeStrategy("+tank", BotState::BOT_STATE_COMBAT); - return false; } - // Phase 3 - if (kaelAI->GetPhase() == PHASE_ALL_ADVISORS) + else if (kaelAI->GetPhase() == PHASE_ALL_ADVISORS) { Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); - // If Capernian is alive, switch to tank strategy // (fallback in case not all weapons were down before Phase 3) if (capernian && !currentlyTank) @@ -1276,10 +1272,9 @@ bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::Execute(Event event bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::RangedBotsDisperse(boss_kaelthas* kaelAI) { - Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); if (kaelAI->GetPhase() == PHASE_ALL_ADVISORS) { - if (thaladred) + if (AI_VALUE2(Unit*, "find target", "thaladred the darkener")) return false; } @@ -1305,9 +1300,7 @@ bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::StayBackFromCaperni if (botAI->IsMainTank(bot)) { const float desiredDist = 15.0f; - const float tolerance = 2.0f; - - if (fabs(bot->GetExactDist2d(capernian) - desiredDist) > tolerance) + if (fabs(bot->GetExactDist2d(capernian) - desiredDist)) { float dx = bot->GetPositionX() - capernian->GetPositionX(); float dy = bot->GetPositionY() - capernian->GetPositionY(); @@ -1340,7 +1333,7 @@ bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::StayBackFromCaperni if (currentDistance < safeDistance) { botAI->Reset(); - return MoveAway(capernian, safeDistance - currentDistance + 1.0f); + return MoveAway(capernian, safeDistance - currentDistance); } if (botAI->IsMelee(bot)) @@ -1386,22 +1379,38 @@ bool KaelthasSunstriderFirstAssistTankPositionTelonicusAction::Execute(Event eve return false; } -bool KaelthasSunstriderPositionPhase3TankHealerAction::Execute(Event event) +// This is a little confusing, but I combined a couple of different actions into a single method +// because from a code perspective, they are almost identical +bool KaelthasSunstriderHandleSanguinarAndTelonicusInPhase3Action::Execute(Event event) { - const Position& position = ADVISOR_HEAL_POSITION; - float distToPosition = - bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + bool shouldMove = false; + // The heal assistant moves to heal position next to Sanguinar and Telonicus tank positions + // And remains there to heal the main tank and first assistant tank + if (botAI->IsHealAssistantOfIndex(bot, 0)) + shouldMove = true; + // The main tank and first assistant tank move to the heal position at the beginning of Phase 3 + // Before the advisors aggro so they are available to pick up Sanguinar and Telonicus + // Once they aggro, the tanks move to their respective tank positions (per the dedicated tanking actions above) + else if (botAI->IsTank(bot)) + { + // No Telonicus check is included since only a single advisor needs to be checked to + // determine if Phase 3 has started but advisors have not aggroed yet + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + if (sanguinar && sanguinar->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + shouldMove = true; + } - if (distToPosition > 2.0f) + if (shouldMove) { - 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; + const Position& position = ADVISOR_HEAL_POSITION; + float distToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, - false, true, MovementPriority::MOVEMENT_COMBAT, true, false); + if (distToPosition > 2.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } } return false; @@ -1416,11 +1425,10 @@ bool KaelthasSunstriderAssignAdvisorDpsPriorityAction::Execute(Event event) { // Target priority 1: Thaladred, except Capernian tank Player* capernianTank = GetCapernianTank(botAI, bot); - bool isCapernianTank = capernianTank && bot == capernianTank; - Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); - if ((!capernianTank || bot != capernianTank) && thaladred && thaladred->IsAlive() && - !thaladred->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && !thaladred->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + if ((!capernianTank || bot != capernianTank) && + thaladred && !thaladred->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !thaladred->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) { MarkTargetWithSquare(bot, thaladred); SetRtiTarget(botAI, "square", thaladred); @@ -1435,8 +1443,9 @@ bool KaelthasSunstriderAssignAdvisorDpsPriorityAction::Execute(Event event) Player* longBowTank = GetNetherstrandLongbowTank(botAI, bot); Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); - if (botAI->IsRangedDps(bot) && (!longBowTank || bot != longBowTank) && capernian && capernian->IsAlive() - && !capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && !capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + if (botAI->IsRangedDps(bot) && (!longBowTank || bot != longBowTank) && + capernian && !capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) { SetRtiTarget(botAI, "circle", capernian); @@ -1448,8 +1457,8 @@ bool KaelthasSunstriderAssignAdvisorDpsPriorityAction::Execute(Event event) // Target priority 3: Sanguinar (longbow tank and melee move here after Thaladred) Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); - if (sanguinar && sanguinar->IsAlive() && - !sanguinar->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && !sanguinar->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + if (sanguinar && !sanguinar->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !sanguinar->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) { SetRtiTarget(botAI, "star", sanguinar); @@ -1461,8 +1470,8 @@ bool KaelthasSunstriderAssignAdvisorDpsPriorityAction::Execute(Event event) // Target priority 4: Telonicus Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); - if (telonicus && telonicus->IsAlive() && - !telonicus->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && !telonicus->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + if (telonicus && !telonicus->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !telonicus->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) { SetRtiTarget(botAI, "triangle", telonicus); @@ -1485,7 +1494,7 @@ bool KaelthasSunstriderAssignAdvisorDpsPriorityAction::Execute(Event event) float targetX = telonicus->GetPositionX() + desiredDist * std::cos(behindAngle); float targetY = telonicus->GetPositionY() + desiredDist * std::sin(behindAngle); - if (bot->GetExactDist2d(targetX, targetY) > 1.0f) + if (bot->GetExactDist2d(targetX, targetY) > tolerance) { return MoveTo(TEMPEST_KEEP_MAP_ID, targetX, targetY, telonicus->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); @@ -1530,11 +1539,7 @@ bool KaelthasSunstriderManageAdvisorDpsTimerAction::Execute(Event event) bool KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction::Execute(Event event) { - if (botAI->IsMainTank(bot)) - return false; - - Player* longBowTank = GetNetherstrandLongbowTank(botAI, bot); - if (longBowTank && longBowTank == bot) + if (botAI->IsMainTank(bot) || GetNetherstrandLongbowTank(botAI, bot) == bot) return false; if (botAI->IsAssistTank(bot)) @@ -1567,75 +1572,54 @@ bool KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction::Execute(Event eve MarkTargetWithSkull(bot, staff); SetRtiTarget(botAI, "skull", staff); - if (staff->HasUnitState(UNIT_STATE_CASTING) && - staff->FindCurrentSpellBySpellId(SPELL_STAFF_FROSTBOLT)) - { - if (bot->getClass() == CLASS_MAGE && botAI->CanCastSpell("counterspell", staff)) - return botAI->CastSpell("counterspell", staff); - else if (bot->getClass() == CLASS_SHAMAN && botAI->CanCastSpell("wind shear", staff)) - return botAI->CastSpell("wind shear", staff); - } - if (bot->GetTarget() != staff->GetGUID()) return Attack(staff); - - return false; } // Priority 2: Cosmic Infuser (Skull) - if (mace) + else if (mace) { MarkTargetWithSkull(bot, mace); SetRtiTarget(botAI, "skull", mace); if (bot->GetTarget() != mace->GetGUID()) return Attack(mace); - - return false; } // Priority 3: Warp Slicer (Skull) - if (sword) + else if (sword) { MarkTargetWithSkull(bot, sword); SetRtiTarget(botAI, "skull", sword); if (bot->GetTarget() != sword->GetGUID()) return Attack(sword); - - return false; } // Priority 4: Infinity Blades (Skull) - if (dagger) + else if (dagger) { MarkTargetWithSkull(bot, dagger); SetRtiTarget(botAI, "skull", dagger); if (bot->GetTarget() != dagger->GetGUID()) return Attack(dagger); - - return false; } // Priority 5: Devastation - ranged only (Diamond--marked in other method by main tank) - if (axe && botAI->IsRangedDps(bot)) + else if (axe && botAI->IsRangedDps(bot)) { SetRtiTarget(botAI, "diamond", axe); if (bot->GetTarget() != axe->GetGUID()) return Attack(axe); - - return false; } // Priority 6: Netherstrand Longbow (Cross--marked in other method by longbow tank) - if (Unit* longbow = AI_VALUE2(Unit*, "find target", "netherstrand longbow")) + else if (Unit* longbow = AI_VALUE2(Unit*, "find target", "netherstrand longbow")) { SetRtiTarget(botAI, "cross", longbow); if (bot->GetTarget() != longbow->GetGUID()) return Attack(longbow); - - return false; } // Priority 7: Phaseshift Bulwark (Skull) - if (Unit* shield = AI_VALUE2(Unit*, "find target", "phaseshift bulwark")) + else if (Unit* shield = AI_VALUE2(Unit*, "find target", "phaseshift bulwark")) { MarkTargetWithSkull(bot, shield); SetRtiTarget(botAI, "skull", shield); @@ -1665,7 +1649,7 @@ bool KaelthasSunstriderMoveDevastationAwayAction::Execute(Event event) if (axe->GetVictim() == bot) { - const float safeDistance = 10.0f; + const float safeDistance = 12.0f; Unit* nearestPlayer = GetNearestNonTankPlayerInRadius(bot, safeDistance); if (nearestPlayer) { @@ -1697,12 +1681,12 @@ bool KaelthasSunstriderHunterTurnAwayNetherstrandLongbowAction::Execute(Event ev { const float dangerZone = 15.0f; Unit* nearestPlayer = GetNearestNonTankPlayerInRadius(bot, dangerZone); - if (!nearestPlayer) - return false; - - float currentDistance = bot->GetExactDist2d(nearestPlayer); - if (currentDistance < dangerZone) - return MoveFromGroup(dangerZone); + if (nearestPlayer) + { + float currentDistance = bot->GetExactDist2d(nearestPlayer); + if (currentDistance < dangerZone) + return MoveFromGroup(dangerZone); + } } return false; @@ -1853,16 +1837,9 @@ bool KaelthasSunstriderLootLegendaryWeaponsAction::LootWeapon( bool KaelthasSunstriderUseLegendaryWeaponsAction::Execute(Event event) { - if (UsePhaseshiftBulwark()) - return true; - - if (UseStaffOfDisintegration()) - return true; - - if (UseNetherstrandLongbow()) - return true; - - return false; + return UsePhaseshiftBulwark() || + UseStaffOfDisintegration() || + UseNetherstrandLongbow(); } bool KaelthasSunstriderUseLegendaryWeaponsAction::UsePhaseshiftBulwark() @@ -1872,10 +1849,7 @@ bool KaelthasSunstriderUseLegendaryWeaponsAction::UsePhaseshiftBulwark() return false; Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); - if (!kaelthas) - return false; - - if (!kaelthas->HasAura(SPELL_SHOCK_BARRIER)) + if (!kaelthas || !kaelthas->HasAura(SPELL_SHOCK_BARRIER)) return false; if (bot->HasAura(SPELL_ARCANE_BARRIER)) @@ -2019,7 +1993,7 @@ bool KaelthasSunstriderAvoidFlameStrikeAction::Execute(Event event) MovementPriority::MOVEMENT_COMBAT, true, false); } -bool KaelthasSunstriderRoundUpPhoenixesAndFocusDownEggsAction::Execute(Event event) +bool KaelthasSunstriderHandlePhoenixesAndEggsAction::Execute(Event event) { if (botAI->IsAssistTankOfIndex(bot, 0) || botAI->IsAssistTankOfIndex(bot, 1)) { @@ -2086,8 +2060,7 @@ bool KaelthasSunstriderRoundUpPhoenixesAndFocusDownEggsAction::Execute(Event eve return false; } - - if (botAI->IsMelee(bot) && botAI->IsDps(bot)) + else if (!botAI->IsTank(bot)) { Unit* phoenix = AI_VALUE2(Unit*, "find target", "phoenix"); if (!phoenix) @@ -2098,11 +2071,7 @@ bool KaelthasSunstriderRoundUpPhoenixesAndFocusDownEggsAction::Execute(Event eve if (currentDistance < safeDistance) return MoveAway(phoenix, safeDistance - currentDistance + 2.0f); } - - Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); - if (botAI->IsRangedDps(bot) || - (botAI->IsMelee(bot) && botAI->IsDps(bot) && kaelthas && - kaelthas->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE) && kaelthas->GetHealthPct() <= 50.0f)) + else if (botAI->IsRangedDps(bot)) { Unit* phoenixEgg = AI_VALUE2(Unit*, "find target", "phoenix egg"); if (!phoenixEgg) @@ -2118,7 +2087,7 @@ bool KaelthasSunstriderRoundUpPhoenixesAndFocusDownEggsAction::Execute(Event eve return false; } -bool KaelthasSunstriderBreakMindControlWithInfinityBladeAction::Execute(Event event) +bool KaelthasSunstriderBreakMindControlAction::Execute(Event event) { Player* mcTarget = nullptr; float closestDist = std::numeric_limits::max(); @@ -2128,7 +2097,7 @@ bool KaelthasSunstriderBreakMindControlWithInfinityBladeAction::Execute(Event ev for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || member == bot) + if (!member || member == bot) continue; if (member->HasAura(SPELL_KAELTHAS_MIND_CONTROL)) diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.h b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.h index 976dcf7e8aa..d9c531a6528 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.h +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.h @@ -284,11 +284,11 @@ class KaelthasSunstriderFirstAssistTankPositionTelonicusAction : public AttackAc bool Execute(Event event) override; }; -class KaelthasSunstriderPositionPhase3TankHealerAction : public AttackAction +class KaelthasSunstriderHandleSanguinarAndTelonicusInPhase3Action : public MovementAction { public: - KaelthasSunstriderPositionPhase3TankHealerAction( - PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider position phase 3 tank healer") : AttackAction(botAI, name) {} + KaelthasSunstriderHandleSanguinarAndTelonicusInPhase3Action( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider handle sanguinar and telonicus in phase 3") : MovementAction(botAI, name) {} bool Execute(Event event) override; }; @@ -382,19 +382,19 @@ class KaelthasSunstriderAvoidFlameStrikeAction : public MovementAction bool Execute(Event event) override; }; -class KaelthasSunstriderRoundUpPhoenixesAndFocusDownEggsAction : public AttackAction +class KaelthasSunstriderHandlePhoenixesAndEggsAction : public AttackAction { public: - KaelthasSunstriderRoundUpPhoenixesAndFocusDownEggsAction( - PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider round up phoenixes and focus down eggs") : AttackAction(botAI, name) {} + KaelthasSunstriderHandlePhoenixesAndEggsAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider handle phoenixes and eggs") : AttackAction(botAI, name) {} bool Execute(Event event) override; }; -class KaelthasSunstriderBreakMindControlWithInfinityBladeAction : public AttackAction +class KaelthasSunstriderBreakMindControlAction : public AttackAction { public: - KaelthasSunstriderBreakMindControlWithInfinityBladeAction( - PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider break mind control with infinity blade") : AttackAction(botAI, name) {} + KaelthasSunstriderBreakMindControlAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider break mind control") : AttackAction(botAI, name) {} bool Execute(Event event) override; }; diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp index 3d50187aba2..ca625516d4e 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp @@ -228,10 +228,10 @@ namespace TempestKeepHelpers // Al'ar - const Position ALAR_PLATFORM_0 = { 335.638f, 59.4879f, 17.9319f }; // West Platform - const Position ALAR_PLATFORM_1 = { 388.751f, 31.7312f, 20.2636f }; // Northwest Platform + const Position ALAR_PLATFORM_0 = { 335.638f, 59.4879f, 17.9319f }; // West Platform + const Position ALAR_PLATFORM_1 = { 388.751f, 31.7312f, 20.2636f }; // Northwest Platform const Position ALAR_PLATFORM_2 = { 388.791f, -33.1059f, 20.2636f }; // Northeast Platform - const Position ALAR_PLATFORM_3 = { 332.723f, -61.159f, 17.9791f }; // East Platform + const Position ALAR_PLATFORM_3 = { 332.723f, -61.159f, 17.9791f }; // East Platform const Position PLATFORM_POSITIONS[] = { ALAR_PLATFORM_0, @@ -239,8 +239,8 @@ namespace TempestKeepHelpers ALAR_PLATFORM_2, ALAR_PLATFORM_3 }; - const Position ALAR_GROUND_0 = { 336.439f, 48.181f, -2.389f }; // Ground counterpart to West Platform - const Position ALAR_GROUND_1 = { 379.122f, 25.146f, -2.385f }; // Ground counterpart to Northwest Platform + const Position ALAR_GROUND_0 = { 336.439f, 48.181f, -2.389f }; // Ground counterpart to West Platform + const Position ALAR_GROUND_1 = { 379.122f, 25.146f, -2.385f }; // Ground counterpart to Northwest Platform const Position ALAR_GROUND_2 = { 378.583f, -27.481f, -2.385f }; // Ground counterpart to Northeast Platform const Position ALAR_GROUND_3 = { 331.631f, -49.716f, -2.389f }; // Ground counterpart to East Platform const Position GROUND_POSITIONS[] = @@ -250,12 +250,12 @@ namespace TempestKeepHelpers ALAR_GROUND_2, ALAR_GROUND_3 }; - const Position ALAR_ROOM_CENTER = { 330.611f, -2.540f, -2.389f }; - const Position ALAR_POINT_QUILL_OR_DIVE = {332.0f, 0.01f, 43.0f, 0.0f}; - const Position ALAR_POINT_MIDDLE = {331.0f, 0.01f, -2.38f, 0.0f}; - const Position ALAR_SE_RAMP_BASE = { 281.064f, -36.590f, -2.389f }; - const Position ALAR_SW_RAMP_BASE = { 281.064f, 36.590f, -2.389f }; - const Position ALAR_ROOM_S_CENTER = { 281.064f, 0.0f, -2.389f }; + const Position ALAR_ROOM_CENTER = { 330.611f, -2.540f, -2.389f }; + const Position ALAR_POINT_QUILL_OR_DIVE = { 332.000f, 0.010f, 43.000f }; + const Position ALAR_POINT_MIDDLE = { 331.000f, 0.010f, -2.380f }; + const Position ALAR_SE_RAMP_BASE = { 281.064f, -36.590f, -2.389f }; + const Position ALAR_SW_RAMP_BASE = { 281.064f, 36.590f, -2.389f }; + const Position ALAR_ROOM_S_CENTER = { 281.064f, 0.000f, -2.389f }; std::unordered_map lastRebirthState; std::unordered_map isAlarInPhase2; diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.cpp index 0038e69c71f..ad08770b24b 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.cpp @@ -45,8 +45,7 @@ float AlarMoveBetweenPlatformsMultiplier::GetValue(Action* action) float AlarDisableDisperseMultiplier::GetValue(Action* action) { - Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); - if (!alar) + if (!AI_VALUE2(Unit*, "find target", "al'ar")) return 1.0f; if (dynamic_cast(action) && @@ -63,11 +62,10 @@ float AlarDisableDisperseMultiplier::GetValue(Action* action) float AlarDisableTankAssistMultiplier::GetValue(Action* action) { - if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0)) + if (!botAI->IsTank(bot)) return 1.0f; - Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); - if (!alar) + if (!AI_VALUE2(Unit*, "find target", "al'ar")) return 1.0f; if (bot->GetVictim() != nullptr) @@ -99,7 +97,7 @@ float AlarStayAwayFromRebirthMultiplier::GetValue(Action* action) float AlarPhase2NoTankingIfArmorMeltedMultiplier::GetValue(Action* action) { - if (!botAI->IsTank(bot) || !bot->HasAura(SPELL_MELT_ARMOR)) + if (!bot->HasAura(SPELL_MELT_ARMOR)) return 1.0f; Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); @@ -117,28 +115,18 @@ float AlarPhase2NoTankingIfArmorMeltedMultiplier::GetValue(Action* action) // Void Reaver -float VoidReaverDisableTankActionsMultiplier::GetValue(Action* action) +float VoidReaverMaintainPositionsMultiplier::GetValue(Action* action) { - if (!botAI->IsTank(bot)) + if (!AI_VALUE2(Unit*, "find target", "void reaver")) return 1.0f; - Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); - if (voidReaver) + if (botAI->IsTank(bot)) { if (dynamic_cast(action)) return 0.0f; } - return 1.0f; -} - -float VoidReaverMaintainPositionsMultiplier::GetValue(Action* action) -{ - if (!botAI->IsRanged(bot)) - return 1.0f; - - Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); - if (voidReaver) + if (botAI->IsRanged(bot)) { if (dynamic_cast(action) || dynamic_cast(action) || @@ -184,8 +172,8 @@ float HighAstromancerSolarianDisableTankAssistMultiplier::GetValue(Action* actio if (!botAI->IsTank(bot)) return 1.0f; - Unit* solariumPriest = AI_VALUE2(Unit*, "find target", "solarium priest"); - if (solariumPriest && bot->GetVictim() != nullptr) + if (AI_VALUE2(Unit*, "find target", "solarium priest") && + bot->GetVictim() != nullptr) { if (dynamic_cast(action)) return 0.0f; @@ -226,25 +214,9 @@ float KaelthasSunstriderWaitForDpsMultiplier::GetValue(Action* action) !advisor->HasAura(SPELL_PERMANENT_FEIGN_DEATH); }; - if (isAdvisorActive(sanguinar) && !botAI->IsMainTank(bot)) - { - if (dynamic_cast(action) || - (dynamic_cast(action) && - !dynamic_cast(action))) - return 0.0f; - } - - if (isAdvisorActive(telonicus) && !botAI->IsAssistTankOfIndex(bot, 0)) - { - if (dynamic_cast(action) || - (dynamic_cast(action) && - !dynamic_cast(action))) - return 0.0f; - } - - Player* capernianTank = GetCapernianTank(botAI, bot); - if (isAdvisorActive(capernian) && capernianTank && capernianTank != bot && - !botAI->IsMainTank(bot)) + if ((isAdvisorActive(sanguinar) && !botAI->IsMainTank(bot)) || + (isAdvisorActive(telonicus) && !botAI->IsAssistTankOfIndex(bot, 0)) || + (isAdvisorActive(capernian) && !botAI->IsMainTank(bot) && GetCapernianTank(botAI, bot) != bot)) { if (dynamic_cast(action) || (dynamic_cast(action) && @@ -309,12 +281,11 @@ float KaelthasSunstriderDisableShadowWardMultiplier::GetValue(Action* action) if (bot->getClass() != CLASS_WARLOCK) return 1.0f; - Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); - if (!kaelthas) - return 1.0f; - - if (dynamic_cast(action)) - return 0.0f; + if (AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) + { + if (dynamic_cast(action)) + return 0.0f; + } return 1.0f; } @@ -355,7 +326,13 @@ float KaelthasSunstriderManageTankTargetingMultiplier::GetValue(Action* action) return 0.0f; } } - else if (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR || + else + { + if (dynamic_cast(action)) + return 0.0f; + } + + if (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR || kaelAI->GetPhase() == PHASE_ALL_ADVISORS) { if (bot->GetVictim() != nullptr) @@ -364,19 +341,13 @@ float KaelthasSunstriderManageTankTargetingMultiplier::GetValue(Action* action) return 0.0f; } } - else if (kaelAI->GetPhase() == PHASE_FINAL) - { - if (dynamic_cast(action)) - return 0.0f; - } return 1.0f; } float KaelthasSunstriderDisableDisperseMultiplier::GetValue(Action* action) { - Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); - if (!kaelthas) + if (!AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) return 1.0f; if (dynamic_cast(action) && @@ -414,8 +385,7 @@ float KaelthasSunstriderTryNonfatalBreakingOfMindControlMultiplier::GetValue(Act if (botAI->IsTank(bot) || !bot->HasItemCount(ITEM_INFINITY_BLADE, 1, true)) return 1.0f; - Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); - if (!kaelthas) + if (!AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) return 1.0f; Group* group = bot->GetGroup(); @@ -439,7 +409,7 @@ float KaelthasSunstriderTryNonfatalBreakingOfMindControlMultiplier::GetValue(Act if (hasMindControlledPlayer) { if (dynamic_cast(action) && - !dynamic_cast(action)) + !dynamic_cast(action)) return 0.0f; } @@ -457,7 +427,7 @@ float KaelthasSunstriderAllDpsOnBossDuringPyroblastMultiplier::GetValue(Action* if (kaelthas->HasAura(SPELL_SHOCK_BARRIER)) { - if (dynamic_cast(action)) + if (dynamic_cast(action)) return 0.0f; } @@ -466,11 +436,7 @@ float KaelthasSunstriderAllDpsOnBossDuringPyroblastMultiplier::GetValue(Action* float KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier::GetValue(Action* action) { - if (!bot->HasAura(SPELL_GRAVITY_LAPSE)) - return 1.0f; - - Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); - if (kaelthas) + if (bot->HasAura(SPELL_GRAVITY_LAPSE)) { if (dynamic_cast(action) && !dynamic_cast(action)) diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.h b/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.h index e2a7ade443a..0069c74f85c 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.h +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.h @@ -3,6 +3,8 @@ #include "Multiplier.h" +// Al'ar + class AlarMoveBetweenPlatformsMultiplier : public Multiplier { public: @@ -43,13 +45,7 @@ class AlarPhase2NoTankingIfArmorMeltedMultiplier : public Multiplier virtual float GetValue(Action* action); }; -class VoidReaverDisableTankActionsMultiplier : public Multiplier -{ -public: - VoidReaverDisableTankActionsMultiplier( - PlayerbotAI* botAI) : Multiplier(botAI, "void reaver disable tank actions multiplier") {} - virtual float GetValue(Action* action); -}; +// Void Reaver class VoidReaverMaintainPositionsMultiplier : public Multiplier { @@ -59,6 +55,8 @@ class VoidReaverMaintainPositionsMultiplier : public Multiplier virtual float GetValue(Action* action); }; +// High Astromancer Solarian + class HighAstromancerSolarianDisableTankAssistMultiplier : public Multiplier { public: @@ -75,6 +73,8 @@ class HighAstromancerSolarianMaintainPositionMultiplier : public Multiplier virtual float GetValue(Action* action); }; +// Kael'thas Sunstrider + class KaelthasSunstriderKiteThaladredMultiplier : public Multiplier { public: diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp index b0b21450103..95880e65ac6 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp @@ -8,7 +8,7 @@ void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) NextAction::array(0, new NextAction("crimson hand centurion cast polymorph", ACTION_RAID + 1), nullptr) )); - // Al'ar + // Al'ar triggers.push_back(new TriggerNode("al'ar pulling boss", NextAction::array(0, new NextAction("al'ar misdirect boss to main tank", ACTION_EMERGENCY + 1), nullptr) )); @@ -69,6 +69,7 @@ void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) new NextAction("high astromancer solarian tank voidwalker", ACTION_EMERGENCY + 1), new NextAction("high astromancer solarian cast fear ward on main tank", ACTION_RAID + 2), nullptr) )); + // Kael'thas Sunstrider triggers.push_back(new TriggerNode("kael'thas sunstrider thaladred is fixated on bot", NextAction::array(0, new NextAction("kael'thas sunstrider kite thaladred", ACTION_EMERGENCY + 6), nullptr) @@ -93,10 +94,10 @@ void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("kael'thas sunstrider telonicus engaged by first assist tank", NextAction::array(0, new NextAction("kael'thas sunstrider first assist tank position telonicus", ACTION_RAID + 1), nullptr) )); - triggers.push_back(new TriggerNode("kael'thas sunstrider melee tanks need dedicated healer in phase 3", - NextAction::array(0, new NextAction("kael'thas sunstrider position phase 3 tank healer", ACTION_RAID + 2), nullptr) + triggers.push_back(new TriggerNode("kael'thas sunstrider need dedicated bots for sanguinar and telonicus in phase 3", + NextAction::array(0, new NextAction("kael'thas sunstrider handle sanguinar and telonicus in phase 3", ACTION_RAID + 2), nullptr) )); - triggers.push_back(new TriggerNode("kael'thas sunstrider determining advisor kill order", + triggers.push_back(new TriggerNode("kael'thas sunstrider determining advisor kill order", NextAction::array(0, new NextAction("kael'thas sunstrider assign advisor dps priority", ACTION_RAID + 1), nullptr) )); triggers.push_back(new TriggerNode("kael'thas sunstrider waiting for tanks to get aggro on advisors", @@ -123,10 +124,10 @@ void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) new NextAction("kael'thas sunstrider avoid flame strike", ACTION_EMERGENCY + 8), nullptr) )); triggers.push_back(new TriggerNode("kael'thas sunstrider phoenixes and eggs are spawning", - NextAction::array(0, new NextAction("kael'thas sunstrider round up phoenixes and focus down eggs", ACTION_RAID + 1), nullptr) + NextAction::array(0, new NextAction("kael'thas sunstrider handle phoenixes and eggs", ACTION_RAID + 1), nullptr) )); triggers.push_back(new TriggerNode("kael'thas sunstrider raid member is mind controlled", - NextAction::array(0, new NextAction("kael'thas sunstrider break mind control with infinity blade", ACTION_EMERGENCY + 1), nullptr) + NextAction::array(0, new NextAction("kael'thas sunstrider break mind control", ACTION_EMERGENCY + 1), nullptr) )); triggers.push_back(new TriggerNode("kael'thas sunstrider boss is casting pyroblast", NextAction::array(0, new NextAction("kael'thas sunstrider break through shock barrier", ACTION_EMERGENCY + 7), nullptr) @@ -138,18 +139,21 @@ void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) void RaidTempestKeepStrategy::InitMultipliers(std::vector& multipliers) { + // Alar multipliers.push_back(new AlarMoveBetweenPlatformsMultiplier(botAI)); multipliers.push_back(new AlarDisableDisperseMultiplier(botAI)); multipliers.push_back(new AlarDisableTankAssistMultiplier(botAI)); multipliers.push_back(new AlarStayAwayFromRebirthMultiplier(botAI)); multipliers.push_back(new AlarPhase2NoTankingIfArmorMeltedMultiplier(botAI)); - multipliers.push_back(new VoidReaverDisableTankActionsMultiplier(botAI)); + // Void Reaver multipliers.push_back(new VoidReaverMaintainPositionsMultiplier(botAI)); + // High Astromancer Solarian multipliers.push_back(new HighAstromancerSolarianDisableTankAssistMultiplier(botAI)); multipliers.push_back(new HighAstromancerSolarianMaintainPositionMultiplier(botAI)); + // Kael'thas Sunstrider multipliers.push_back(new KaelthasSunstriderKiteThaladredMultiplier(botAI)); multipliers.push_back(new KaelthasSunstriderWaitForDpsMultiplier(botAI)); multipliers.push_back(new KaelthasSunstriderControlMisdirectionMultiplier(botAI)); diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h index 0068dc35b78..0fc35d2b94d 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h @@ -83,8 +83,8 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext creators["kael'thas sunstrider telonicus engaged by first assist tank"] = &RaidTempestKeepTriggerContext::kaelthas_sunstrider_telonicus_engaged_by_first_assist_tank; - creators["kael'thas sunstrider melee tanks need dedicated healer in phase 3"] = - &RaidTempestKeepTriggerContext::kaelthas_sunstrider_melee_tanks_need_dedicated_healer_in_phase_3; + creators["kael'thas sunstrider need dedicated bots for sanguinar and telonicus in phase 3"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_need_dedicated_bots_for_sanguinar_and_telonicus_in_phase_3; creators["kael'thas sunstrider determining advisor kill order"] = &RaidTempestKeepTriggerContext::kaelthas_sunstrider_determining_advisor_kill_order; @@ -195,8 +195,8 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext static Trigger* kaelthas_sunstrider_telonicus_engaged_by_first_assist_tank( PlayerbotAI* botAI) { return new KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger(botAI); } - static Trigger* kaelthas_sunstrider_melee_tanks_need_dedicated_healer_in_phase_3( - PlayerbotAI* botAI) { return new KaelthasSunstriderMeleeTanksNeedDedicatedHealerInPhase3Trigger(botAI); } + static Trigger* kaelthas_sunstrider_need_dedicated_bots_for_sanguinar_and_telonicus_in_phase_3( + PlayerbotAI* botAI) { return new KaelthasSunstriderNeedDedicatedBotsForSanguinarAndTelonicusInPhase3Trigger(botAI); } static Trigger* kaelthas_sunstrider_determining_advisor_kill_order( PlayerbotAI* botAI) { return new KaelthasSunstriderDeterminingAdvisorKillOrderTrigger(botAI); } diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp index a2ccaddeaeb..3ef409932db 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp @@ -13,8 +13,7 @@ bool CrimsonHandCenturionCastsArcaneVolleyTrigger::IsActive() if (bot->getClass() != CLASS_MAGE) return false; - Unit* centurion = AI_VALUE2(Unit*, "find target", "crimson hand centurion"); - return centurion && centurion->HasAura(SPELL_ARCANE_FLURRY); + return AI_VALUE2(Unit*, "find target", "crimson hand centurion"); } // Al'ar @@ -53,12 +52,10 @@ bool AlarBossIsFlyingBetweenPlatformsTrigger::IsActive() bool AlarBossSpawnsEmbersOfAlarTrigger::IsActive() { - if (!botAI->IsTank(bot) && - !botAI->IsRangedDps(bot)) + if (!botAI->IsTank(bot) && !botAI->IsRangedDps(bot)) return false; - Unit* ember = AI_VALUE2(Unit*, "find target", "ember of al'ar"); - return ember != nullptr; + return AI_VALUE2(Unit*, "find target", "ember of al'ar"); } bool AlarIncomingFlameQuillsTrigger::IsActive() @@ -130,8 +127,7 @@ bool AlarPhase2EncounterIsAtRoomCenterTrigger::IsActive() bool AlarStrategyChangesBetweenPhasesTrigger::IsActive() { - Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); - if (!alar) + if (!AI_VALUE2(Unit*, "find target", "al'ar")) return false; if (Group* group = bot->GetGroup()) @@ -164,8 +160,7 @@ bool VoidReaverBossLaunchesArcaneOrbsTrigger::IsActive() if (!botAI->IsRanged(bot)) return false; - Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); - return voidReaver != nullptr; + return AI_VALUE2(Unit*, "find target", "void reaver"); } // High Astromancer Solarian @@ -206,8 +201,7 @@ bool HighAstromancerSolarianSolariumPriestsSpawnedTrigger::IsActive() if (!botAI->IsMelee(bot)) return false; - Unit* solariumPriest = AI_VALUE2(Unit*, "find target", "solarium priest"); - return solariumPriest != nullptr; + return AI_VALUE2(Unit*, "find target", "solarium priest"); } bool HighAstromancerSolarianTransformedIntoVoidwalkerTrigger::IsActive() @@ -306,18 +300,15 @@ bool KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger::IsActive() if (bot->getClass() != CLASS_WARLOCK) return false; - Player* capernianTank = GetCapernianTank(botAI, bot); - if (!capernianTank || capernianTank != bot) + if (GetCapernianTank(botAI, bot) != bot) return false; - Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); - return kaelthas != nullptr; + return AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); } bool KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger::IsActive() { - Player* capernianTank = GetCapernianTank(botAI, bot); - if (capernianTank && capernianTank == bot) + if (GetCapernianTank(botAI, bot) == bot) return false; Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); @@ -335,9 +326,11 @@ bool KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger::IsActive() !telonicus->HasAura(SPELL_PERMANENT_FEIGN_DEATH); } -bool KaelthasSunstriderMeleeTanksNeedDedicatedHealerInPhase3Trigger::IsActive() +bool KaelthasSunstriderNeedDedicatedBotsForSanguinarAndTelonicusInPhase3Trigger::IsActive() { - if (!botAI->IsHealAssistantOfIndex(bot, 0)) + if (!botAI->IsHealAssistantOfIndex(bot, 0) && + !botAI->IsMainTank(bot) && + !botAI->IsAssistTankOfIndex(bot, 0)) return false; Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); @@ -348,12 +341,8 @@ bool KaelthasSunstriderMeleeTanksNeedDedicatedHealerInPhase3Trigger::IsActive() if (!kaelAI || kaelAI->GetPhase() != PHASE_ALL_ADVISORS) return false; - Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); - if (telonicus && telonicus->IsAlive()) - return true; - - Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); - if (sanguinar && sanguinar->IsAlive()) + if (AI_VALUE2(Unit*, "find target", "master engineer telonicus") || + AI_VALUE2(Unit*, "find target", "lord sanguinar")) return true; return false; @@ -405,8 +394,7 @@ bool KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger::IsActive() bool KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger::IsActive() { - Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); - if (!kaelthas) + if (!AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) return false; return bot->HasItemCount(ITEM_STAFF_OF_DISINTEGRATION, 1, false) || @@ -475,12 +463,10 @@ bool KaelthasSunstriderBossHasEnteredTheFightTrigger::IsActive() bool KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger::IsActive() { - Unit* phoenix = AI_VALUE2(Unit*, "find target", "phoenix"); - if (phoenix != nullptr) + if (AI_VALUE2(Unit*, "find target", "phoenix")) return true; - Unit* phoenixEgg = AI_VALUE2(Unit*, "find target", "phoenix egg"); - if (phoenixEgg != nullptr) + if (AI_VALUE2(Unit*, "find target", "phoenix egg")) return true; return false; diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h index b8daff2e582..db0bf25df31 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h @@ -197,11 +197,11 @@ class KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger : public Trigge bool IsActive() override; }; -class KaelthasSunstriderMeleeTanksNeedDedicatedHealerInPhase3Trigger : public Trigger +class KaelthasSunstriderNeedDedicatedBotsForSanguinarAndTelonicusInPhase3Trigger : public Trigger { public: - KaelthasSunstriderMeleeTanksNeedDedicatedHealerInPhase3Trigger( - PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider melee tanks need dedicated healer in phase 3") {} + KaelthasSunstriderNeedDedicatedBotsForSanguinarAndTelonicusInPhase3Trigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider need dedicated bots for sanguinar and telonicus in phase 3") {} bool IsActive() override; }; From 2702860fe51b4443888fe3853455c16eda388948 Mon Sep 17 00:00:00 2001 From: crow Date: Tue, 6 Jan 2026 09:13:50 -0600 Subject: [PATCH 06/29] update to reflect PR #1923 and change vector to pair --- .../tempestkeep/RaidTempestKeepActions.cpp | 58 +++-- .../tempestkeep/RaidTempestKeepActions.h | 4 +- .../tempestkeep/RaidTempestKeepStrategy.cpp | 233 +++++++++--------- 3 files changed, 148 insertions(+), 147 deletions(-) diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp index 68904c76ba4..cb165075ba5 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp @@ -873,24 +873,30 @@ bool HighAstromancerSolarianStackForAoeAction::Execute(Event event) // Split melee into two groups, one on each Solarium Priest bool HighAstromancerSolarianTargetSolariumPriestsAction::Execute(Event event) { - Unit* solariumPriest = AI_VALUE2(Unit*, "find target", "solarium priest"); - if (!solariumPriest) + auto priestsPair = GetSolariumPriests(botAI); + if (!priestsPair.first || !priestsPair.second) return false; Group* group = bot->GetGroup(); if (!group) return false; - auto solariumPriests = GetSolariumPriests(); auto meleeMembers = GetMeleeBots(group); - Unit* targetPriest = AssignSolariumPriestsToBots(solariumPriests, meleeMembers); + if (meleeMembers.empty()) + return false; + + Unit* targetPriest = AssignSolariumPriestsToBots(priestsPair, meleeMembers); if (!targetPriest) return false; auto it = std::find(meleeMembers.begin(), meleeMembers.end(), bot); + if (it == meleeMembers.end()) + return false; + size_t botIndex = std::distance(meleeMembers.begin(), it); size_t totalMelee = meleeMembers.size(); - if (botIndex < totalMelee / 2) + + if (targetPriest == priestsPair.first) { MarkTargetWithSquare(bot, targetPriest); SetRtiTarget(botAI, "square", targetPriest); @@ -907,18 +913,26 @@ bool HighAstromancerSolarianTargetSolariumPriestsAction::Execute(Event event) return false; } -std::vector HighAstromancerSolarianTargetSolariumPriestsAction::GetSolariumPriests() +std::pair HighAstromancerSolarianTargetSolariumPriestsAction::GetSolariumPriests(PlayerbotAI* botAI) { - std::vector solariumPriests; - auto const& npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); - for (auto const& npcGuid : npcs) + Unit* lowest = nullptr; + Unit* highest = nullptr; + + for (auto const& guid : + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get()) { - Unit* unit = botAI->GetUnit(npcGuid); - if (unit && unit->GetEntry() == NPC_SOLARIUM_PRIEST && unit->IsAlive()) - solariumPriests.push_back(unit); + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->GetEntry() == NPC_SOLARIUM_PRIEST) + { + if (!lowest || unit->GetGUID().GetRawValue() < lowest->GetGUID().GetRawValue()) + lowest = unit; + + if (!highest || unit->GetGUID().GetRawValue() > highest->GetGUID().GetRawValue()) + highest = unit; + } } - return solariumPriests; + return {lowest, highest}; } std::vector HighAstromancerSolarianTargetSolariumPriestsAction::GetMeleeBots(Group* group) @@ -938,15 +952,11 @@ std::vector HighAstromancerSolarianTargetSolariumPriestsAction::GetMele } Unit* HighAstromancerSolarianTargetSolariumPriestsAction::AssignSolariumPriestsToBots( - const std::vector& solariumPriests, const std::vector& meleeMembers) + const std::pair& priestsPair, const std::vector& meleeMembers) { - if (solariumPriests.size() < 2 || meleeMembers.empty()) + if (!priestsPair.first || !priestsPair.second || meleeMembers.empty()) return nullptr; - std::vector sortedPriests = solariumPriests; - std::sort(sortedPriests.begin(), sortedPriests.end(), - [](Unit* left, Unit* right) { return left->GetGUID() < right->GetGUID(); }); - auto it = std::find(meleeMembers.begin(), meleeMembers.end(), bot); if (it == meleeMembers.end()) return nullptr; @@ -954,10 +964,14 @@ Unit* HighAstromancerSolarianTargetSolariumPriestsAction::AssignSolariumPriestsT size_t botIndex = std::distance(meleeMembers.begin(), it); size_t totalMelee = meleeMembers.size(); - if (botIndex < totalMelee / 2) - return sortedPriests[0]; + if (totalMelee == 1) + return priestsPair.first; + + size_t split = totalMelee / 2; + if (botIndex < split) + return priestsPair.first; else - return sortedPriests[1]; + return priestsPair.second; } bool HighAstromancerSolarianTankVoidwalkerAction::Execute(Event event) diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.h b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.h index d9c531a6528..e5fbc91bf1c 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.h +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.h @@ -193,9 +193,9 @@ class HighAstromancerSolarianTargetSolariumPriestsAction : public AttackAction bool Execute(Event event) override; private: - std::vector GetSolariumPriests(); + std::pair GetSolariumPriests(PlayerbotAI* botAI); std::vector GetMeleeBots(Group* group); - Unit* AssignSolariumPriestsToBots(const std::vector& solariumPriests, const std::vector& meleeMembers); + Unit* AssignSolariumPriestsToBots(const std::pair& priestsPair, const std::vector& meleeMembers); }; class HighAstromancerSolarianTankVoidwalkerAction : public AttackAction diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp index 95880e65ac6..4c4ffd6b159 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp @@ -4,137 +4,124 @@ void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) { // Trash - triggers.push_back(new TriggerNode("crimson hand centurion casts arcane volley", - NextAction::array(0, new NextAction("crimson hand centurion cast polymorph", ACTION_RAID + 1), nullptr) - )); + triggers.push_back(new TriggerNode("crimson hand centurion casts arcane volley", { + NextAction("crimson hand centurion cast polymorph", ACTION_RAID + 1) })); // Al'ar - triggers.push_back(new TriggerNode("al'ar pulling boss", - NextAction::array(0, new NextAction("al'ar misdirect boss to main tank", ACTION_EMERGENCY + 1), nullptr) - )); - triggers.push_back(new TriggerNode("al'ar boss is flying between platforms", - NextAction::array(0, - new NextAction("al'ar boss tanks move between platforms", ACTION_RAID + 1), - new NextAction("al'ar melee dps move between platforms", ACTION_RAID + 1), - new NextAction("al'ar ranged and ember tank move under platforms", ACTION_RAID + 4), nullptr) - )); - triggers.push_back(new TriggerNode("al'ar boss spawns embers of alar", - NextAction::array(0, - new NextAction("al'ar assist tanks pick up embers", ACTION_RAID + 3), - new NextAction("al'ar ranged dps prioritize embers", ACTION_RAID + 2), nullptr) - )); - triggers.push_back(new TriggerNode("al'ar incoming flame quills", - NextAction::array(0, new NextAction("al'ar jump from platform", ACTION_EMERGENCY + 7), nullptr) - )); - triggers.push_back(new TriggerNode("al'ar rising from the ashes", - NextAction::array(0, new NextAction("al'ar move away from rebirth", ACTION_EMERGENCY + 7), nullptr) - )); - triggers.push_back(new TriggerNode("al'ar everything is on fire in phase 2", - NextAction::array(0, - new NextAction("al'ar swap tanks on boss", ACTION_EMERGENCY + 2), - new NextAction("al'ar avoid flame patches and dive bombs", ACTION_EMERGENCY + 1), nullptr) - )); - triggers.push_back(new TriggerNode("al'ar phase 2 encounter is at room center", - NextAction::array(0, new NextAction("al'ar return to room center", ACTION_RAID + 1), nullptr) - )); - triggers.push_back(new TriggerNode("al'ar strategy changes between phases", - NextAction::array(0, new NextAction("al'ar manage phase tracker", ACTION_EMERGENCY + 10), nullptr) - )); + triggers.push_back(new TriggerNode("al'ar pulling boss", { + NextAction("al'ar misdirect boss to main tank", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("al'ar boss is flying between platforms", { + NextAction("al'ar boss tanks move between platforms", ACTION_RAID + 1), + NextAction("al'ar melee dps move between platforms", ACTION_RAID + 1), + NextAction("al'ar ranged and ember tank move under platforms", ACTION_RAID + 4) })); + + triggers.push_back(new TriggerNode("al'ar boss spawns embers of alar", { + NextAction("al'ar assist tanks pick up embers", ACTION_RAID + 3), + NextAction("al'ar ranged dps prioritize embers", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("al'ar incoming flame quills", { + NextAction("al'ar jump from platform", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("al'ar rising from the ashes", { + NextAction("al'ar move away from rebirth", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("al'ar everything is on fire in phase 2", { + NextAction("al'ar swap tanks on boss", ACTION_EMERGENCY + 2), + NextAction("al'ar avoid flame patches and dive bombs", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("al'ar phase 2 encounter is at room center", { + NextAction("al'ar return to room center", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("al'ar strategy changes between phases", { + NextAction("al'ar manage phase tracker", ACTION_EMERGENCY + 10) })); // Void Reaver - triggers.push_back(new TriggerNode("void reaver knock away reduces tank aggro", - NextAction::array(0, - new NextAction("void reaver tanks position boss", ACTION_RAID + 1), - new NextAction("void reaver ranged use aggro dump ability", ACTION_EMERGENCY + 6), nullptr) - )); - triggers.push_back(new TriggerNode("void reaver boss launches arcane orbs", - NextAction::array(0, new NextAction("void reaver spread ranged", ACTION_RAID + 1), nullptr) - )); + triggers.push_back(new TriggerNode("void reaver knock away reduces tank aggro", { + NextAction("void reaver tanks position boss", ACTION_RAID + 1), + NextAction("void reaver ranged use aggro dump ability", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("void reaver boss launches arcane orbs", { + NextAction("void reaver spread ranged", ACTION_RAID + 1) })); // High Astromancer Solarian - triggers.push_back(new TriggerNode("high astromancer solarian boss casts wrath of the astromancer", - NextAction::array(0, new NextAction("high astromancer solarian ranged leave space for melee", ACTION_RAID + 1), nullptr) - )); - triggers.push_back(new TriggerNode("high astromancer solarian bot has wrath of the astromancer", - NextAction::array(0, new NextAction("high astromancer solarian move away from group", ACTION_EMERGENCY + 6), nullptr) - )); - triggers.push_back(new TriggerNode("high astromancer solarian boss has vanished", - NextAction::array(0, new NextAction("high astromancer solarian stack for aoe", ACTION_RAID + 1), nullptr) - )); - triggers.push_back(new TriggerNode("high astromancer solarian solarium priests spawned", - NextAction::array(0, new NextAction("high astromancer solarian target solarium priests", ACTION_RAID + 1), nullptr) - )); - triggers.push_back(new TriggerNode("high astromancer solarian transformed into voidwalker", - NextAction::array(0, - new NextAction("high astromancer solarian tank voidwalker", ACTION_EMERGENCY + 1), - new NextAction("high astromancer solarian cast fear ward on main tank", ACTION_RAID + 2), nullptr) - )); + triggers.push_back(new TriggerNode("high astromancer solarian boss casts wrath of the astromancer", { + NextAction("high astromancer solarian ranged leave space for melee", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("high astromancer solarian bot has wrath of the astromancer", { + NextAction("high astromancer solarian move away from group", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("high astromancer solarian boss has vanished", { + NextAction("high astromancer solarian stack for aoe", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("high astromancer solarian solarium priests spawned", { + NextAction("high astromancer solarian target solarium priests", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("high astromancer solarian transformed into voidwalker", { + NextAction("high astromancer solarian tank voidwalker", ACTION_EMERGENCY + 1), + NextAction("high astromancer solarian cast fear ward on main tank", ACTION_RAID + 2) })); // Kael'thas Sunstrider - triggers.push_back(new TriggerNode("kael'thas sunstrider thaladred is fixated on bot", - NextAction::array(0, new NextAction("kael'thas sunstrider kite thaladred", ACTION_EMERGENCY + 6), nullptr) - )); - triggers.push_back(new TriggerNode("kael'thas sunstrider pulling tankable advisors", - NextAction::array(0, new NextAction("kael'thas sunstrider misdirect advisors to tanks", ACTION_EMERGENCY + 2), nullptr) - )); - triggers.push_back(new TriggerNode("kael'thas sunstrider sanguinar engaged by main tank", - NextAction::array(0, new NextAction("kael'thas sunstrider main tank position sanguinar", ACTION_RAID + 1), nullptr) - )); - triggers.push_back(new TriggerNode("kael'thas sunstrider sanguinar casts bellowing roar", - NextAction::array(0, new NextAction("kael'thas sunstrider cast fear ward on sanguinar tank", ACTION_RAID + 2), nullptr) - )); - triggers.push_back(new TriggerNode("kael'thas sunstrider capernian should be tanked by a warlock", - NextAction::array(0, - new NextAction("kael'thas sunstrider manage warlock tank strategy", ACTION_EMERGENCY + 10), - new NextAction("kael'thas sunstrider warlock tank position capernian", ACTION_RAID + 1), nullptr) - )); - triggers.push_back(new TriggerNode("kael'thas sunstrider capernian casts arcane burst and conflagration", - NextAction::array(0, new NextAction("kael'thas sunstrider spread and move away from capernian", ACTION_RAID + 3), nullptr) - )); - triggers.push_back(new TriggerNode("kael'thas sunstrider telonicus engaged by first assist tank", - NextAction::array(0, new NextAction("kael'thas sunstrider first assist tank position telonicus", ACTION_RAID + 1), nullptr) - )); - triggers.push_back(new TriggerNode("kael'thas sunstrider need dedicated bots for sanguinar and telonicus in phase 3", - NextAction::array(0, new NextAction("kael'thas sunstrider handle sanguinar and telonicus in phase 3", ACTION_RAID + 2), nullptr) - )); - triggers.push_back(new TriggerNode("kael'thas sunstrider determining advisor kill order", - NextAction::array(0, new NextAction("kael'thas sunstrider assign advisor dps priority", ACTION_RAID + 1), nullptr) - )); - triggers.push_back(new TriggerNode("kael'thas sunstrider waiting for tanks to get aggro on advisors", - NextAction::array(0, new NextAction("kael'thas sunstrider manage advisor dps timer", ACTION_EMERGENCY + 10), nullptr) - )); - triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are alive", - NextAction::array(0, - new NextAction("kael'thas sunstrider assign legendary weapon dps priority", ACTION_RAID + 1), - new NextAction("kael'thas sunstrider main tank move devastation away", ACTION_EMERGENCY + 1), - new NextAction("kael'thas sunstrider hunter turn away netherstrand longbow", ACTION_EMERGENCY + 1), nullptr) - )); - triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are dead and lootable", - NextAction::array(0, new NextAction("kael'thas sunstrider loot legendary weapons", ACTION_RAID + 1), nullptr) - )); - triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are equipped", - NextAction::array(0, new NextAction("kael'thas sunstrider use legendary weapons", ACTION_EMERGENCY + 6), nullptr) - )); - triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons were lost", - NextAction::array(0, new NextAction("kael'thas sunstrider reequip gear", ACTION_EMERGENCY + 11), nullptr) - )); - triggers.push_back(new TriggerNode("kael'thas sunstrider boss has entered the fight", - NextAction::array(0, - new NextAction("kael'thas sunstrider main tank position boss", ACTION_RAID + 1), - new NextAction("kael'thas sunstrider avoid flame strike", ACTION_EMERGENCY + 8), nullptr) - )); - triggers.push_back(new TriggerNode("kael'thas sunstrider phoenixes and eggs are spawning", - NextAction::array(0, new NextAction("kael'thas sunstrider handle phoenixes and eggs", ACTION_RAID + 1), nullptr) - )); - triggers.push_back(new TriggerNode("kael'thas sunstrider raid member is mind controlled", - NextAction::array(0, new NextAction("kael'thas sunstrider break mind control", ACTION_EMERGENCY + 1), nullptr) - )); - triggers.push_back(new TriggerNode("kael'thas sunstrider boss is casting pyroblast", - NextAction::array(0, new NextAction("kael'thas sunstrider break through shock barrier", ACTION_EMERGENCY + 7), nullptr) - )); - triggers.push_back(new TriggerNode("kael'thas sunstrider boss is manipulating gravity", - NextAction::array(0, new NextAction("kael'thas sunstrider spread out in midair", ACTION_EMERGENCY + 1), nullptr) - )); + triggers.push_back(new TriggerNode("kael'thas sunstrider thaladred is fixated on bot", { + NextAction("kael'thas sunstrider kite thaladred", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider pulling tankable advisors", { + NextAction("kael'thas sunstrider misdirect advisors to tanks", ACTION_EMERGENCY + 2) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider sanguinar engaged by main tank", { + NextAction("kael'thas sunstrider main tank position sanguinar", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider sanguinar casts bellowing roar", { + NextAction("kael'thas sunstrider cast fear ward on sanguinar tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider capernian should be tanked by a warlock", { + NextAction("kael'thas sunstrider manage warlock tank strategy", ACTION_EMERGENCY + 10), + NextAction("kael'thas sunstrider warlock tank position capernian", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider capernian casts arcane burst and conflagration", { + NextAction("kael'thas sunstrider spread and move away from capernian", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider telonicus engaged by first assist tank", { + NextAction("kael'thas sunstrider first assist tank position telonicus", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider need dedicated bots for sanguinar and telonicus in phase 3", { + NextAction("kael'thas sunstrider handle sanguinar and telonicus in phase 3", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider determining advisor kill order", { + NextAction("kael'thas sunstrider assign advisor dps priority", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider waiting for tanks to get aggro on advisors", { + NextAction("kael'thas sunstrider manage advisor dps timer", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are alive", { + NextAction("kael'thas sunstrider assign legendary weapon dps priority", ACTION_RAID + 1), + NextAction("kael'thas sunstrider main tank move devastation away", ACTION_EMERGENCY + 1), + NextAction("kael'thas sunstrider hunter turn away netherstrand longbow", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are dead and lootable", { + NextAction("kael'thas sunstrider loot legendary weapons", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are equipped", { + NextAction("kael'thas sunstrider use legendary weapons", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons were lost", { + NextAction("kael'thas sunstrider reequip gear", ACTION_EMERGENCY + 11) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider boss has entered the fight", { + NextAction("kael'thas sunstrider main tank position boss", ACTION_RAID + 1), + NextAction("kael'thas sunstrider avoid flame strike", ACTION_EMERGENCY + 8) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider phoenixes and eggs are spawning", { + NextAction("kael'thas sunstrider handle phoenixes and eggs", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider raid member is mind controlled", { + NextAction("kael'thas sunstrider break mind control", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider boss is casting pyroblast", { + NextAction("kael'thas sunstrider break through shock barrier", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider boss is manipulating gravity", { + NextAction("kael'thas sunstrider spread out in midair", ACTION_EMERGENCY + 1) })); } void RaidTempestKeepStrategy::InitMultipliers(std::vector& multipliers) From cdb93361c938ab4058fca124fba80fe31d72f14b Mon Sep 17 00:00:00 2001 From: crow Date: Tue, 6 Jan 2026 09:20:28 -0600 Subject: [PATCH 07/29] address merge conflicts --- src/strategy/AiObjectContext.cpp | 4 ---- src/strategy/raids/RaidStrategyContext.h | 3 --- 2 files changed, 7 deletions(-) diff --git a/src/strategy/AiObjectContext.cpp b/src/strategy/AiObjectContext.cpp index 4cd83419c22..60894e16eaf 100644 --- a/src/strategy/AiObjectContext.cpp +++ b/src/strategy/AiObjectContext.cpp @@ -45,8 +45,6 @@ #include "raids/gruulslair/RaidGruulsLairTriggerContext.h" #include "raids/tempestkeep/RaidTempestKeepActionContext.h" #include "raids/tempestkeep/RaidTempestKeepTriggerContext.h" -#include "raids/naxxramas/RaidNaxxActionContext.h" -#include "raids/naxxramas/RaidNaxxTriggerContext.h" #include "raids/eyeofeternity/RaidEoEActionContext.h" #include "raids/eyeofeternity/RaidEoETriggerContext.h" #include "raids/vaultofarchavon/RaidVoAActionContext.h" @@ -120,7 +118,6 @@ void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextList creators["magtheridon"] = &RaidStrategyContext::magtheridon; creators["gruulslair"] = &RaidStrategyContext::gruulslair; creators["tempestkeep"] = &RaidStrategyContext::tempestkeep; - creators["naxx"] = &RaidStrategyContext::naxx; creators["wotlk-os"] = &RaidStrategyContext::wotlk_os; creators["wotlk-eoe"] = &RaidStrategyContext::wotlk_eoe; creators["voa"] = &RaidStrategyContext::voa; @@ -46,7 +44,6 @@ class RaidStrategyContext : public NamedObjectContext static Strategy* magtheridon(PlayerbotAI* botAI) { return new RaidMagtheridonStrategy(botAI); } static Strategy* gruulslair(PlayerbotAI* botAI) { return new RaidGruulsLairStrategy(botAI); } static Strategy* tempestkeep(PlayerbotAI* botAI) { return new RaidTempestKeepStrategy(botAI); } - static Strategy* naxx(PlayerbotAI* botAI) { return new RaidNaxxStrategy(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); } From 6f992e4fa9faa1af24083db7d950993e22134773 Mon Sep 17 00:00:00 2001 From: crow Date: Thu, 8 Jan 2026 09:37:36 -0600 Subject: [PATCH 08/29] minor performance improvement --- src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp index 3ef409932db..ca0594d7903 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp @@ -477,6 +477,10 @@ bool KaelthasSunstriderRaidMemberIsMindControlledTrigger::IsActive() if (!bot->HasItemCount(ITEM_INFINITY_BLADE, 1, true) || botAI->IsTank(bot)) return false; + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + if (Group* group = bot->GetGroup()) { for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) From 20f57ea34b18e45b073f58a9c8be72476e95894d Mon Sep 17 00:00:00 2001 From: crow Date: Mon, 12 Jan 2026 11:07:40 -0600 Subject: [PATCH 09/29] switch to out of combat map clearing & split some triggers --- .../RaidTempestKeepActionContext.h | 8 ++ .../tempestkeep/RaidTempestKeepActions.cpp | 96 +++++++------- .../tempestkeep/RaidTempestKeepActions.h | 10 ++ .../tempestkeep/RaidTempestKeepHelpers.cpp | 18 +++ .../tempestkeep/RaidTempestKeepHelpers.h | 1 + .../tempestkeep/RaidTempestKeepStrategy.cpp | 20 ++- .../RaidTempestKeepTriggerContext.h | 42 +++++-- .../tempestkeep/RaidTempestKeepTriggers.cpp | 117 +++++++++++------- .../tempestkeep/RaidTempestKeepTriggers.h | 46 ++++++- 9 files changed, 252 insertions(+), 106 deletions(-) diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActionContext.h b/src/strategy/raids/tempestkeep/RaidTempestKeepActionContext.h index fc80ff75fcf..923fec30480 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepActionContext.h +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepActionContext.h @@ -9,6 +9,10 @@ class RaidTempestKeepActionContext : public NamedObjectContext public: RaidTempestKeepActionContext() { + // General + creators["tempest keep clear timers and trackers"] = + &RaidTempestKeepActionContext::tempest_keep_clear_timers_and_trackers; + // Trash creators["crimson hand centurion cast polymorph"] = &RaidTempestKeepActionContext::crimson_hand_centurion_cast_polymorph; @@ -151,6 +155,10 @@ class RaidTempestKeepActionContext : public NamedObjectContext } private: + // General + static Action* tempest_keep_clear_timers_and_trackers( + PlayerbotAI* botAI) { return new TempestKeepClearTimersAndTrackersAction(botAI); } + // Trash static Action* crimson_hand_centurion_cast_polymorph( PlayerbotAI* botAI) { return new CrimsonHandCenturionCastPolymorphAction(botAI); } diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp index cb165075ba5..4010a95bb0f 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp @@ -10,6 +10,33 @@ using namespace TempestKeepHelpers; +// General + +bool TempestKeepClearTimersAndTrackersAction::Execute(Event event) +{ + bool cleared = false; + + if (!initialVoidReaverPositions.empty()) + { + initialVoidReaverPositions.clear(); + cleared = true; + } + + if (!hasReachedInitialVoidReaverPosition.empty()) + { + hasReachedInitialVoidReaverPosition.clear(); + cleared = true; + } + + if (!advisorDpsWaitTimer.empty()) + { + advisorDpsWaitTimer.clear(); + cleared = true; + } + + return cleared; +} + // Trash bool CrimsonHandCenturionCastPolymorphAction::Execute(Event event) @@ -390,9 +417,6 @@ bool AlarAssistTanksPickUpEmbersAction::HandlePhase2Embers(Unit* alar) bool AlarRangedDpsPrioritizeEmbersAction::Execute(Event event) { - if (!botAI->IsRangedDps(bot)) - return false; - auto [firstEmber, secondEmber] = GetFirstTwoEmbersOfAlar(botAI); const float safeDistance = 15.0f; @@ -510,13 +534,15 @@ bool AlarMoveAwayFromRebirthAction::Execute(Event event) return JumpTo(TEMPEST_KEEP_MAP_ID, ground.GetPositionX(), ground.GetPositionY(), ground.GetPositionZ(), MovementPriority::MOVEMENT_FORCED); } - - float currentDistance = bot->GetDistance2d(alar); - const float safeDistance = 20.0f; - if (currentDistance < safeDistance) + else { - botAI->Reset(); - return MoveAway(alar, safeDistance - currentDistance); + float currentDistance = bot->GetDistance2d(alar); + const float safeDistance = 20.0f; + if (currentDistance < safeDistance) + { + botAI->Reset(); + return MoveAway(alar, safeDistance - currentDistance); + } } return false; @@ -600,24 +626,8 @@ bool AlarAvoidFlamePatchesAndDiveBombsAction::AvoidFlamePatch() bool AlarAvoidFlamePatchesAndDiveBombsAction::HandleDiveBomb(Unit* alar) { - int8 locationIndex = GetAlarCurrentLocationIndex(alar); - if (locationIndex == LOCATION_NONE) - { - Position dest; - locationIndex = GetAlarDestinationLocationIndex(alar, dest); - } - - if (locationIndex == POINT_QUILL_OR_DIVE_IDX) - { - Unit* nearestPlayer = GetNearestPlayerInRadius(bot, 10.0f); - if (nearestPlayer) - { - return FleePosition(Position(nearestPlayer->GetPositionX(), nearestPlayer->GetPositionY(), - nearestPlayer->GetPositionZ()), 11.0f); - } - } - else if (alar->HasUnitState(UNIT_STATE_CASTING) && - alar->FindCurrentSpellBySpellId(SPELL_REBIRTH_DIVE)) + if (alar->HasUnitState(UNIT_STATE_CASTING) && + alar->FindCurrentSpellBySpellId(SPELL_REBIRTH_DIVE)) { float currentDistance = bot->GetDistance2d(alar); const float safeDistance = 20.0f; @@ -628,6 +638,22 @@ bool AlarAvoidFlamePatchesAndDiveBombsAction::HandleDiveBomb(Unit* alar) return MoveAway(alar, safeDistance - currentDistance); } } + else + { + Position dest; + if (GetAlarCurrentLocationIndex(alar) == POINT_QUILL_OR_DIVE_IDX || + GetAlarDestinationLocationIndex(alar, dest) == POINT_QUILL_OR_DIVE_IDX) + { + const float safeDistance = 10.0f; + Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance); + if (nearestPlayer) + { + const uint32 minInterval = 0; + return FleePosition(Position(nearestPlayer->GetPositionX(), nearestPlayer->GetPositionY(), + nearestPlayer->GetPositionZ()), safeDistance, minInterval); + } + } + } return false; } @@ -699,10 +725,6 @@ bool VoidReaverTanksPositionBossAction::Execute(Event event) bool VoidReaverRangedUseAggroDumpAbilityAction::Execute(Event event) { - Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); - if (!voidReaver) - return false; - botAI->Reset(); static const std::array spells = { @@ -731,12 +753,6 @@ bool VoidReaverSpreadRangedAction::Execute(Event event) if (!voidReaver) return false; - if (voidReaver->GetHealthPct() > 99.8f) - { - initialVoidReaverPositions.clear(); - hasReachedInitialVoidReaverPosition.clear(); - } - if (initialVoidReaverPositions.empty()) { std::vector healers; @@ -976,9 +992,6 @@ Unit* HighAstromancerSolarianTargetSolariumPriestsAction::AssignSolariumPriestsT bool HighAstromancerSolarianTankVoidwalkerAction::Execute(Event event) { - if (!botAI->IsMainTank(bot)) - return false; - Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); if (!astromancer) return false; @@ -1001,9 +1014,6 @@ bool HighAstromancerSolarianTankVoidwalkerAction::Execute(Event event) bool HighAstromancerSolarianCastFearWardOnMainTankAction::Execute(Event event) { - if (bot->getClass() != CLASS_PRIEST) - return false; - Player* mainTank = nullptr; if (Group* group = bot->GetGroup()) { diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.h b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.h index e5fbc91bf1c..1d20040f79b 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.h +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.h @@ -9,6 +9,16 @@ using namespace TempestKeepHelpers; +// General + +class TempestKeepClearTimersAndTrackersAction : public Action +{ +public: + TempestKeepClearTimersAndTrackersAction( + PlayerbotAI* botAI, std::string const name = "tempest keep clear timers and trackers") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + // Trash class CrimsonHandCenturionCastPolymorphAction : public Action diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp index ca625516d4e..613ddaa7b7e 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp @@ -10,6 +10,24 @@ namespace TempestKeepHelpers { // General Helpers + bool IsInstanceTimerManager(PlayerbotAI* botAI, Player* bot) + { + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !botAI->IsDps(member) || + !GET_PLAYERBOT_AI(member)) + continue; + + return member == bot; + } + } + + return false; + } + void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId) { if (!target) diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.h b/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.h index 90acbb8bb02..62de43a5e4d 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.h +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.h @@ -100,6 +100,7 @@ namespace TempestKeepHelpers // General const uint32 TEMPEST_KEEP_MAP_ID = 550; + bool IsInstanceTimerManager(PlayerbotAI* botAI, Player* bot); void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId); void MarkTargetWithSkull(Player* bot, Unit* target); void MarkTargetWithSquare(Player* bot, Unit* target); diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp index 4c4ffd6b159..b1241eba2a8 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp @@ -3,6 +3,10 @@ void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) { + // General + triggers.push_back(new TriggerNode("tempest keep timer bot is not in combat", { + NextAction("tempest keep clear timers and trackers", ACTION_EMERGENCY + 11) })); + // Trash triggers.push_back(new TriggerNode("crimson hand centurion casts arcane volley", { NextAction("crimson hand centurion cast polymorph", ACTION_RAID + 1) })); @@ -16,8 +20,10 @@ void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) NextAction("al'ar melee dps move between platforms", ACTION_RAID + 1), NextAction("al'ar ranged and ember tank move under platforms", ACTION_RAID + 4) })); - triggers.push_back(new TriggerNode("al'ar boss spawns embers of alar", { - NextAction("al'ar assist tanks pick up embers", ACTION_RAID + 3), + triggers.push_back(new TriggerNode("al'ar embers of al'ar explode upon death", { + NextAction("al'ar assist tanks pick up embers", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("al'ar killing embers of al'ar damages boss", { NextAction("al'ar ranged dps prioritize embers", ACTION_RAID + 2) })); triggers.push_back(new TriggerNode("al'ar incoming flame quills", { @@ -37,8 +43,10 @@ void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) NextAction("al'ar manage phase tracker", ACTION_EMERGENCY + 10) })); // Void Reaver + triggers.push_back(new TriggerNode("void reaver boss casts pounding", { + NextAction("void reaver tanks position boss", ACTION_RAID + 1) })); + triggers.push_back(new TriggerNode("void reaver knock away reduces tank aggro", { - NextAction("void reaver tanks position boss", ACTION_RAID + 1), NextAction("void reaver ranged use aggro dump ability", ACTION_EMERGENCY + 6) })); triggers.push_back(new TriggerNode("void reaver boss launches arcane orbs", { @@ -57,8 +65,10 @@ void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("high astromancer solarian solarium priests spawned", { NextAction("high astromancer solarian target solarium priests", ACTION_RAID + 1) })); - triggers.push_back(new TriggerNode("high astromancer solarian transformed into voidwalker", { - NextAction("high astromancer solarian tank voidwalker", ACTION_EMERGENCY + 1), + triggers.push_back(new TriggerNode("high astromancer solarian boss transformed into voidwalker", { + NextAction("high astromancer solarian tank voidwalker", ACTION_EMERGENCY + 1), })); + + triggers.push_back(new TriggerNode("high astromancer solarian boss casts psychic scream", { NextAction("high astromancer solarian cast fear ward on main tank", ACTION_RAID + 2) })); // Kael'thas Sunstrider diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h index 0fc35d2b94d..a017bcbfb52 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h @@ -9,6 +9,10 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext public: RaidTempestKeepTriggerContext() { + // General + creators["tempest keep timer bot is not in combat"] = + &RaidTempestKeepTriggerContext::tempest_keep_timer_bot_is_not_in_combat; + // Trash creators["crimson hand centurion casts arcane volley"] = &RaidTempestKeepTriggerContext::crimson_hand_centurion_casts_arcane_volley; @@ -20,8 +24,11 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext creators["al'ar boss is flying between platforms"] = &RaidTempestKeepTriggerContext::alar_boss_is_flying_between_platforms; - creators["al'ar boss spawns embers of alar"] = - &RaidTempestKeepTriggerContext::alar_boss_spawns_embers_of_alar; + creators["al'ar embers of al'ar explode upon death"] = + &RaidTempestKeepTriggerContext::alar_embers_of_alar_explode_upon_death; + + creators["al'ar killing embers of al'ar damages boss"] = + &RaidTempestKeepTriggerContext::alar_killing_embers_of_alar_damages_boss; creators["al'ar incoming flame quills"] = &RaidTempestKeepTriggerContext::alar_incoming_flame_quills; @@ -39,6 +46,9 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext &RaidTempestKeepTriggerContext::alar_strategy_changes_between_phases; // Void Reaver + creators["void reaver boss casts pounding"] = + &RaidTempestKeepTriggerContext::void_reaver_boss_casts_pounding; + creators["void reaver knock away reduces tank aggro"] = &RaidTempestKeepTriggerContext::void_reaver_knock_away_reduces_tank_aggro; @@ -58,8 +68,11 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext creators["high astromancer solarian solarium priests spawned"] = &RaidTempestKeepTriggerContext::high_astromancer_solarian_solarium_priests_spawned; - creators["high astromancer solarian transformed into voidwalker"] = - &RaidTempestKeepTriggerContext::high_astromancer_solarian_transformed_into_voidwalker; + creators["high astromancer solarian boss transformed into voidwalker"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_boss_transformed_into_voidwalker; + + creators["high astromancer solarian boss casts psychic scream"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_boss_casts_psychic_scream; // Kael'thas Sunstrider creators["kael'thas sunstrider thaladred is fixated on bot"] = @@ -121,6 +134,10 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext } private: + // General + static Trigger* tempest_keep_timer_bot_is_not_in_combat( + PlayerbotAI* botAI) { return new TempestKeepTimerBotIsNotInCombatTrigger(botAI); } + // Trash static Trigger* crimson_hand_centurion_casts_arcane_volley( PlayerbotAI* botAI) { return new CrimsonHandCenturionCastsArcaneVolleyTrigger(botAI); } @@ -132,8 +149,11 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext static Trigger* alar_boss_is_flying_between_platforms( PlayerbotAI* botAI) { return new AlarBossIsFlyingBetweenPlatformsTrigger(botAI); } - static Trigger* alar_boss_spawns_embers_of_alar( - PlayerbotAI* botAI) { return new AlarBossSpawnsEmbersOfAlarTrigger(botAI); } + static Trigger* alar_embers_of_alar_explode_upon_death( + PlayerbotAI* botAI) { return new AlarEmbersOfAlarExplodeUponDeathTrigger(botAI); } + + static Trigger* alar_killing_embers_of_alar_damages_boss( + PlayerbotAI* botAI) { return new AlarKillingEmbersOfAlarDamagesBossTrigger(botAI); } static Trigger* alar_incoming_flame_quills( PlayerbotAI* botAI) { return new AlarIncomingFlameQuillsTrigger(botAI); } @@ -151,6 +171,9 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext PlayerbotAI* botAI) { return new AlarStrategyChangesBetweenPhasesTrigger(botAI); } // Void Reaver + static Trigger* void_reaver_boss_casts_pounding( + PlayerbotAI* botAI) { return new VoidReaverBossCastsPoundingTrigger(botAI); } + static Trigger* void_reaver_knock_away_reduces_tank_aggro( PlayerbotAI* botAI) { return new VoidReaverKnockAwayReducesTankAggroTrigger(botAI); } @@ -170,8 +193,11 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext static Trigger* high_astromancer_solarian_solarium_priests_spawned( PlayerbotAI* botAI) { return new HighAstromancerSolarianSolariumPriestsSpawnedTrigger(botAI); } - static Trigger* high_astromancer_solarian_transformed_into_voidwalker( - PlayerbotAI* botAI) { return new HighAstromancerSolarianTransformedIntoVoidwalkerTrigger(botAI); } + static Trigger* high_astromancer_solarian_boss_transformed_into_voidwalker( + PlayerbotAI* botAI) { return new HighAstromancerSolarianBossTransformedIntoVoidwalkerTrigger(botAI); } + + static Trigger* high_astromancer_solarian_boss_casts_psychic_scream( + PlayerbotAI* botAI) { return new HighAstromancerSolarianBossCastsPsychicScreamTrigger(botAI); } // Kael'thas Sunstrider static Trigger* kaelthas_sunstrider_thaladred_is_fixated_on_bot( diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp index ca0594d7903..c45846f5731 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp @@ -6,6 +6,13 @@ using namespace TempestKeepHelpers; +// General + +bool TempestKeepTimerBotIsNotInCombatTrigger::IsActive() +{ + return IsInstanceTimerManager(botAI, bot) && !bot->IsInCombat(); +} + // Trash bool CrimsonHandCenturionCastsArcaneVolleyTrigger::IsActive() @@ -50,9 +57,17 @@ bool AlarBossIsFlyingBetweenPlatformsTrigger::IsActive() return true; } -bool AlarBossSpawnsEmbersOfAlarTrigger::IsActive() +bool AlarEmbersOfAlarExplodeUponDeathTrigger::IsActive() { - if (!botAI->IsTank(bot) && !botAI->IsRangedDps(bot)) + if (!botAI->IsTank(bot)) + return false; + + return AI_VALUE2(Unit*, "find target", "ember of al'ar"); +} + +bool AlarKillingEmbersOfAlarDamagesBossTrigger::IsActive() +{ + if (!botAI->IsRangedDps(bot)) return false; return AI_VALUE2(Unit*, "find target", "ember of al'ar"); @@ -67,14 +82,9 @@ bool AlarIncomingFlameQuillsTrigger::IsActive() if (isAlarInPhase2[alar->GetMap()->GetInstanceId()]) return false; - int8 locationIndex = GetAlarCurrentLocationIndex(alar); - if (locationIndex == LOCATION_NONE) - { - Position dest; - locationIndex = GetAlarDestinationLocationIndex(alar, dest); - } - - return locationIndex == POINT_QUILL_OR_DIVE_IDX; + Position dest; + return GetAlarCurrentLocationIndex(alar) == POINT_QUILL_OR_DIVE_IDX || + GetAlarDestinationLocationIndex(alar, dest) == POINT_QUILL_OR_DIVE_IDX; } bool AlarRisingFromTheAshesTrigger::IsActive() @@ -115,14 +125,9 @@ bool AlarPhase2EncounterIsAtRoomCenterTrigger::IsActive() if (alarCreature && alarCreature->GetReactState() == REACT_PASSIVE) return false; - int8 locationIndex = GetAlarCurrentLocationIndex(alar); - if (locationIndex == LOCATION_NONE) - { - Position dest; - locationIndex = GetAlarDestinationLocationIndex(alar, dest); - } - - return locationIndex != POINT_QUILL_OR_DIVE_IDX; + Position dest; + return GetAlarCurrentLocationIndex(alar) != POINT_QUILL_OR_DIVE_IDX && + GetAlarDestinationLocationIndex(alar, dest) != POINT_QUILL_OR_DIVE_IDX; } bool AlarStrategyChangesBetweenPhasesTrigger::IsActive() @@ -146,9 +151,18 @@ bool AlarStrategyChangesBetweenPhasesTrigger::IsActive() // Void Reaver +bool VoidReaverBossCastsPoundingTrigger::IsActive() +{ + if (!botAI->IsTank(bot)) + return false; + + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + return voidReaver && voidReaver->GetVictim() == bot; +} + bool VoidReaverKnockAwayReducesTankAggroTrigger::IsActive() { - if (!botAI->IsRanged(bot) && !botAI->IsTank(bot)) + if (!botAI->IsRanged(bot)) return false; Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); @@ -204,9 +218,18 @@ bool HighAstromancerSolarianSolariumPriestsSpawnedTrigger::IsActive() return AI_VALUE2(Unit*, "find target", "solarium priest"); } -bool HighAstromancerSolarianTransformedIntoVoidwalkerTrigger::IsActive() +bool HighAstromancerSolarianBossTransformedIntoVoidwalkerTrigger::IsActive() { - if (!botAI->IsMainTank(bot) && bot->getClass() != CLASS_PRIEST) + if (!botAI->IsMainTank(bot)) + return false; + + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + return astromancer && astromancer->HasAura(SPELL_SOLARIAN_TRANSFORM); +} + +bool HighAstromancerSolarianBossCastsPsychicScreamTrigger::IsActive() +{ + if (bot->getClass() != CLASS_PRIEST) return false; Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); @@ -251,12 +274,12 @@ bool KaelthasSunstriderPullingTankableAdvisorsTrigger::IsActive() bool KaelthasSunstriderSanguinarEngagedByMainTankTrigger::IsActive() { - if (!botAI->IsMainTank(bot)) + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + if (!sanguinar || sanguinar->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || + sanguinar->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) return false; - Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); - return sanguinar && !sanguinar->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && - !sanguinar->HasAura(SPELL_PERMANENT_FEIGN_DEATH); + return botAI->IsMainTank(bot); } bool KaelthasSunstriderSanguinarCastsBellowingRoarTrigger::IsActive() @@ -300,43 +323,43 @@ bool KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger::IsActive() if (bot->getClass() != CLASS_WARLOCK) return false; - if (GetCapernianTank(botAI, bot) != bot) + if (!AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) return false; - return AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + return GetCapernianTank(botAI, bot) == bot; } bool KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger::IsActive() { - if (GetCapernianTank(botAI, bot) == bot) + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (!capernian || capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || + capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) return false; - Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); - return capernian && !capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && - !capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH); + return GetCapernianTank(botAI, bot) != bot; } bool KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger::IsActive() { - if (!botAI->IsAssistTankOfIndex(bot, 0)) + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + if (!telonicus || telonicus->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || + telonicus->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) return false; - Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); - return telonicus && !telonicus->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && - !telonicus->HasAura(SPELL_PERMANENT_FEIGN_DEATH); + return botAI->IsAssistTankOfIndex(bot, 0); } bool KaelthasSunstriderNeedDedicatedBotsForSanguinarAndTelonicusInPhase3Trigger::IsActive() { + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + if (!botAI->IsHealAssistantOfIndex(bot, 0) && !botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0)) return false; - Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); - if (!kaelthas) - return false; - boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); if (!kaelAI || kaelAI->GetPhase() != PHASE_ALL_ADVISORS) return false; @@ -350,14 +373,17 @@ bool KaelthasSunstriderNeedDedicatedBotsForSanguinarAndTelonicusInPhase3Trigger: bool KaelthasSunstriderDeterminingAdvisorKillOrderTrigger::IsActive() { - if (botAI->IsHeal(bot) || botAI->IsMainTank(bot) || - botAI->IsAssistTankOfIndex(bot, 0)) + if (botAI->IsHeal(bot)) return false; Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); if (!kaelthas) return false; + if (botAI->IsMainTank(bot) || + botAI->IsAssistTankOfIndex(bot, 0)) + return false; + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); return kaelAI && (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR || kaelAI->GetPhase() == PHASE_ALL_ADVISORS); @@ -474,13 +500,16 @@ bool KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger::IsActive() bool KaelthasSunstriderRaidMemberIsMindControlledTrigger::IsActive() { - if (!bot->HasItemCount(ITEM_INFINITY_BLADE, 1, true) || botAI->IsTank(bot)) - return false; - Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); if (!kaelthas) return false; + if (botAI->IsTank(bot) && kaelthas->GetVictim() == bot) + return false; + + if (!bot->HasItemCount(ITEM_INFINITY_BLADE, 1, true)) + return false; + if (Group* group = bot->GetGroup()) { for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h index db0bf25df31..cc2a3310346 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h @@ -3,6 +3,16 @@ #include "Trigger.h" +// General + +class TempestKeepTimerBotIsNotInCombatTrigger : public Trigger +{ +public: + TempestKeepTimerBotIsNotInCombatTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "tempest keep timer bot is not in combat") {} + bool IsActive() override; +}; + // Trash class CrimsonHandCenturionCastsArcaneVolleyTrigger : public Trigger @@ -31,11 +41,19 @@ class AlarBossIsFlyingBetweenPlatformsTrigger : public Trigger bool IsActive() override; }; -class AlarBossSpawnsEmbersOfAlarTrigger : public Trigger +class AlarEmbersOfAlarExplodeUponDeathTrigger : public Trigger +{ +public: + AlarEmbersOfAlarExplodeUponDeathTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar embers of al'ar explode upon death") {} + bool IsActive() override; +}; + +class AlarKillingEmbersOfAlarDamagesBossTrigger : public Trigger { public: - AlarBossSpawnsEmbersOfAlarTrigger( - PlayerbotAI* botAI) : Trigger(botAI, "al'ar boss spawns embers of alar") {} + AlarKillingEmbersOfAlarDamagesBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar killing embers of al'ar damages boss") {} bool IsActive() override; }; @@ -81,6 +99,14 @@ class AlarStrategyChangesBetweenPhasesTrigger : public Trigger // Void Reaver +class VoidReaverBossCastsPoundingTrigger : public Trigger +{ +public: + VoidReaverBossCastsPoundingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver boss casts pounding") {} + bool IsActive() override; +}; + class VoidReaverKnockAwayReducesTankAggroTrigger : public Trigger { public: @@ -131,11 +157,19 @@ class HighAstromancerSolarianSolariumPriestsSpawnedTrigger : public Trigger bool IsActive() override; }; -class HighAstromancerSolarianTransformedIntoVoidwalkerTrigger : public Trigger +class HighAstromancerSolarianBossTransformedIntoVoidwalkerTrigger : public Trigger +{ +public: + HighAstromancerSolarianBossTransformedIntoVoidwalkerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian boss transformed into voidwalker") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianBossCastsPsychicScreamTrigger : public Trigger { public: - HighAstromancerSolarianTransformedIntoVoidwalkerTrigger( - PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian transformed into voidwalker") {} + HighAstromancerSolarianBossCastsPsychicScreamTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer boss casts psychic scream") {} bool IsActive() override; }; From 29eb1937bdb40b3aa81c54a037f8659004fd2d38 Mon Sep 17 00:00:00 2001 From: crow Date: Thu, 15 Jan 2026 02:05:52 -0600 Subject: [PATCH 10/29] fixed a bunch of stupid crap --- .../RaidTempestKeepActionContext.h | 16 +-- .../tempestkeep/RaidTempestKeepActions.cpp | 116 +++++++++--------- .../tempestkeep/RaidTempestKeepActions.h | 12 +- .../tempestkeep/RaidTempestKeepHelpers.cpp | 67 ++++------ .../tempestkeep/RaidTempestKeepHelpers.h | 6 +- .../RaidTempestKeepMultipliers.cpp | 49 ++++++-- .../tempestkeep/RaidTempestKeepStrategy.cpp | 16 ++- .../RaidTempestKeepTriggerContext.h | 28 +++-- .../tempestkeep/RaidTempestKeepTriggers.cpp | 43 ++++--- .../tempestkeep/RaidTempestKeepTriggers.h | 28 ++++- 10 files changed, 220 insertions(+), 161 deletions(-) diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActionContext.h b/src/strategy/raids/tempestkeep/RaidTempestKeepActionContext.h index 923fec30480..93160985a04 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepActionContext.h +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepActionContext.h @@ -10,8 +10,8 @@ class RaidTempestKeepActionContext : public NamedObjectContext RaidTempestKeepActionContext() { // General - creators["tempest keep clear timers and trackers"] = - &RaidTempestKeepActionContext::tempest_keep_clear_timers_and_trackers; + creators["tempest keep erase timers and trackers"] = + &RaidTempestKeepActionContext::tempest_keep_erase_timers_and_trackers; // Trash creators["crimson hand centurion cast polymorph"] = @@ -108,8 +108,8 @@ class RaidTempestKeepActionContext : public NamedObjectContext creators["kael'thas sunstrider first assist tank position telonicus"] = &RaidTempestKeepActionContext::kaelthas_sunstrider_first_assist_tank_position_telonicus; - creators["kael'thas sunstrider handle sanguinar and telonicus in phase 3"] = - &RaidTempestKeepActionContext::kaelthas_sunstrider_handle_sanguinar_and_telonicus_in_phase_3; + creators["kael'thas sunstrider handle advisor roles in phase 3"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_handle_advisor_roles_in_phase_3; creators["kael'thas sunstrider assign advisor dps priority"] = &RaidTempestKeepActionContext::kaelthas_sunstrider_assign_advisor_dps_priority; @@ -156,8 +156,8 @@ class RaidTempestKeepActionContext : public NamedObjectContext private: // General - static Action* tempest_keep_clear_timers_and_trackers( - PlayerbotAI* botAI) { return new TempestKeepClearTimersAndTrackersAction(botAI); } + static Action* tempest_keep_erase_timers_and_trackers( + PlayerbotAI* botAI) { return new TempestKeepEraseTimersAndTrackersAction(botAI); } // Trash static Action* crimson_hand_centurion_cast_polymorph( @@ -254,8 +254,8 @@ class RaidTempestKeepActionContext : public NamedObjectContext static Action* kaelthas_sunstrider_first_assist_tank_position_telonicus( PlayerbotAI* botAI) { return new KaelthasSunstriderFirstAssistTankPositionTelonicusAction(botAI); } - static Action* kaelthas_sunstrider_handle_sanguinar_and_telonicus_in_phase_3( - PlayerbotAI* botAI) { return new KaelthasSunstriderHandleSanguinarAndTelonicusInPhase3Action(botAI); } + static Action* kaelthas_sunstrider_handle_advisor_roles_in_phase_3( + PlayerbotAI* botAI) { return new KaelthasSunstriderHandleAdvisorRolesInPhase3Action(botAI); } static Action* kaelthas_sunstrider_assign_advisor_dps_priority( PlayerbotAI* botAI) { return new KaelthasSunstriderAssignAdvisorDpsPriorityAction(botAI); } diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp index 4010a95bb0f..4b2cbf77601 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp @@ -12,29 +12,33 @@ using namespace TempestKeepHelpers; // General -bool TempestKeepClearTimersAndTrackersAction::Execute(Event event) +bool TempestKeepEraseTimersAndTrackersAction::Execute(Event event) { - bool cleared = false; + const ObjectGuid guid = bot->GetGUID(); + const uint32 instanceId = bot->GetMap()->GetInstanceId(); - if (!initialVoidReaverPositions.empty()) + bool erased = false; + if (!AI_VALUE2(Unit*, "find target", "al'ar")) { - initialVoidReaverPositions.clear(); - cleared = true; + if (lastRebirthState.erase(instanceId)) + erased = true; + if (isAlarInPhase2.erase(instanceId)) + erased = true; } - - if (!hasReachedInitialVoidReaverPosition.empty()) + else if (!AI_VALUE2(Unit*, "find target", "void reaver")) { - hasReachedInitialVoidReaverPosition.clear(); - cleared = true; + if (initialVoidReaverPositions.erase(guid)) + erased = true; + if (hasReachedInitialVoidReaverPosition.erase(guid)) + erased = true; } - - if (!advisorDpsWaitTimer.empty()) + else if (!AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) { - advisorDpsWaitTimer.clear(); - cleared = true; + if (advisorDpsWaitTimer.erase(instanceId)) + erased = true; } - return cleared; + return erased; } // Trash @@ -57,8 +61,8 @@ bool CrimsonHandCenturionCastPolymorphAction::Execute(Event event) } else if (centurion->HasAura(SPELL_ARCANE_FLURRY)) { - if (botAI->CanCastSpell("polymorph", centurion)) - return botAI->CastSpell("polymorph", centurion); + botAI->Reset(); + return botAI->CastSpell("polymorph", centurion); } return false; @@ -626,8 +630,9 @@ bool AlarAvoidFlamePatchesAndDiveBombsAction::AvoidFlamePatch() bool AlarAvoidFlamePatchesAndDiveBombsAction::HandleDiveBomb(Unit* alar) { - if (alar->HasUnitState(UNIT_STATE_CASTING) && - alar->FindCurrentSpellBySpellId(SPELL_REBIRTH_DIVE)) + if ((alar->HasUnitState(UNIT_STATE_CASTING) && + alar->FindCurrentSpellBySpellId(SPELL_REBIRTH_DIVE)) || + !alar->IsVisible()) { float currentDistance = bot->GetDistance2d(alar); const float safeDistance = 20.0f; @@ -680,9 +685,6 @@ bool AlarManagePhaseTrackerAction::Execute(Event event) const uint32 instanceId = alar->GetMap()->GetInstanceId(); - if (alar->GetHealthPct() > 99.5f && alar->GetPositionZ() >= ALAR_BALCONY_Z) - isAlarInPhase2[instanceId] = false; - bool rebirthActive = alar->HasUnitState(UNIT_STATE_CASTING) && alar->FindCurrentSpellBySpellId(SPELL_REBIRTH_PHASE2); bool lastRebirth = lastRebirthState[instanceId]; @@ -1403,36 +1405,37 @@ bool KaelthasSunstriderFirstAssistTankPositionTelonicusAction::Execute(Event eve return false; } -// This is a little confusing, but I combined a couple of different actions into a single method -// because from a code perspective, they are almost identical -bool KaelthasSunstriderHandleSanguinarAndTelonicusInPhase3Action::Execute(Event event) +bool KaelthasSunstriderHandleAdvisorRolesInPhase3Action::Execute(Event event) { - bool shouldMove = false; - // The heal assistant moves to heal position next to Sanguinar and Telonicus tank positions - // And remains there to heal the main tank and first assistant tank + const Position* movePosition = nullptr; + if (botAI->IsHealAssistantOfIndex(bot, 0)) - shouldMove = true; - // The main tank and first assistant tank move to the heal position at the beginning of Phase 3 - // Before the advisors aggro so they are available to pick up Sanguinar and Telonicus - // Once they aggro, the tanks move to their respective tank positions (per the dedicated tanking actions above) - else if (botAI->IsTank(bot)) - { - // No Telonicus check is included since only a single advisor needs to be checked to - // determine if Phase 3 has started but advisors have not aggroed yet + movePosition = &ADVISOR_HEAL_POSITION; + else if (botAI->IsMainTank(bot)) + { Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); if (sanguinar && sanguinar->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) - shouldMove = true; + movePosition = &SANGUINAR_WAITING_POSITION; } - - if (shouldMove) + else if (botAI->IsAssistTankOfIndex(bot, 0)) + { + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + if (telonicus && telonicus->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + movePosition = &TELONICUS_WAITING_POSITION; + } + else if (GetCapernianTank(botAI, bot) == bot) { - const Position& position = ADVISOR_HEAL_POSITION; - float distToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (capernian && capernian->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + movePosition = &CAPERNIAN_WAITING_POSITION; + } - if (distToPosition > 2.0f) + if (movePosition) + { + if (bot->GetExactDist2d(movePosition->GetPositionX(), movePosition->GetPositionY()) > 2.0f) { - return MoveTo(TEMPEST_KEEP_MAP_ID, position.GetPositionX(), position.GetPositionY(), - position.GetPositionZ(), false, false, false, false, + return MoveTo(TEMPEST_KEEP_MAP_ID, movePosition->GetPositionX(), movePosition->GetPositionY(), + movePosition->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); } } @@ -1563,9 +1566,6 @@ bool KaelthasSunstriderManageAdvisorDpsTimerAction::Execute(Event event) bool KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction::Execute(Event event) { - if (botAI->IsMainTank(bot) || GetNetherstrandLongbowTank(botAI, bot) == bot) - return false; - if (botAI->IsAssistTank(bot)) SetRtiTarget(botAI, "moon", nullptr); @@ -1658,9 +1658,6 @@ bool KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction::Execute(Event eve bool KaelthasSunstriderMoveDevastationAwayAction::Execute(Event event) { - if (!botAI->IsMainTank(bot)) - return false; - Unit* axe = AI_VALUE2(Unit*, "find target", "devastation"); if (!axe) return false; @@ -1687,12 +1684,8 @@ bool KaelthasSunstriderMoveDevastationAwayAction::Execute(Event event) bool KaelthasSunstriderHunterTurnAwayNetherstrandLongbowAction::Execute(Event event) { - Player* longBowTank = GetNetherstrandLongbowTank(botAI, bot); - if (!longBowTank || longBowTank != bot) - return false; - Unit* longbow = AI_VALUE2(Unit*, "find target", "netherstrand longbow"); - if (!longbow || !longbow->IsAlive()) + if (!longbow) return false; MarkTargetWithCross(bot, longbow); @@ -2141,9 +2134,18 @@ bool KaelthasSunstriderBreakMindControlAction::Execute(Event event) if (!bot->IsWithinMeleeRange(mcTarget)) { - return MoveTo(TEMPEST_KEEP_MAP_ID, mcTarget->GetPositionX(), mcTarget->GetPositionY(), - mcTarget->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); + uint32 delay = urand(1500, 2500); // 1.5 to 2.5 seconds + float x = mcTarget->GetPositionX(); + float y = mcTarget->GetPositionY(); + float z = mcTarget->GetPositionZ(); + botAI->AddTimedEvent( + [this, x, y, z]() { + MoveTo(TEMPEST_KEEP_MAP_ID, x, y, z, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + }, + delay); + botAI->SetNextCheckDelay(delay + 50); + return true; } static const std::array spells = diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.h b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.h index 1d20040f79b..8665c028c00 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.h +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.h @@ -11,11 +11,11 @@ using namespace TempestKeepHelpers; // General -class TempestKeepClearTimersAndTrackersAction : public Action +class TempestKeepEraseTimersAndTrackersAction : public Action { public: - TempestKeepClearTimersAndTrackersAction( - PlayerbotAI* botAI, std::string const name = "tempest keep clear timers and trackers") : Action(botAI, name) {} + TempestKeepEraseTimersAndTrackersAction( + PlayerbotAI* botAI, std::string const name = "tempest keep erase timers and trackers") : Action(botAI, name) {} bool Execute(Event event) override; }; @@ -294,11 +294,11 @@ class KaelthasSunstriderFirstAssistTankPositionTelonicusAction : public AttackAc bool Execute(Event event) override; }; -class KaelthasSunstriderHandleSanguinarAndTelonicusInPhase3Action : public MovementAction +class KaelthasSunstriderHandleAdvisorRolesInPhase3Action : public MovementAction { public: - KaelthasSunstriderHandleSanguinarAndTelonicusInPhase3Action( - PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider handle sanguinar and telonicus in phase 3") : MovementAction(botAI, name) {} + KaelthasSunstriderHandleAdvisorRolesInPhase3Action( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider handle advisor roles in phase 3") : MovementAction(botAI, name) {} bool Execute(Event event) override; }; diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp index 613ddaa7b7e..e52cac619d3 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp @@ -10,24 +10,6 @@ namespace TempestKeepHelpers { // General Helpers - bool IsInstanceTimerManager(PlayerbotAI* botAI, Player* bot) - { - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !botAI->IsDps(member) || - !GET_PLAYERBOT_AI(member)) - continue; - - return member == bot; - } - } - - return false; - } - void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId) { if (!target) @@ -88,6 +70,28 @@ namespace TempestKeepHelpers } } + bool IsInstanceTimerManager(PlayerbotAI* botAI, Player* bot) + { + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !botAI->IsDps(member) || + !GET_PLAYERBOT_AI(member)) + continue; + + Player* capernianTank = GetCapernianTank(botAI, member); + if (capernianTank && capernianTank == member) + continue; + + return member == bot; + } + } + + return false; + } + Unit* GetNearestPlayerInRadius(Player* bot, float radius) { Unit* nearestPlayer = nullptr; @@ -437,8 +441,11 @@ namespace TempestKeepHelpers // Kael'thas Sunstrider const Position SANGUINAR_TANK_POSITION = { 775.478f, 39.888f, 46.780f }; + const Position SANGUINAR_WAITING_POSITION = { 761.850f, 27.459f, 46.779f }; const Position TELONICUS_TANK_POSITION = { 773.717f, 44.091f, 46.780f }; - const Position ADVISOR_HEAL_POSITION = { 749.443f, 26.927f, 46.780f }; + const Position TELONICUS_WAITING_POSITION = { 754.347f, 31.739f, 46.796f }; + const Position ADVISOR_HEAL_POSITION = { 757.425f, 13.011f, 46.779f }; + const Position CAPERNIAN_WAITING_POSITION = { 743.897f, -11.575f, 46.779f }; const Position KAELTHAS_WEAPON_STACK_POSITION = { 775.296f, -0.822f, 48.729f }; const Position KAELTHAS_AXE_TANK_POSITION = { 775.621f, 20.717f, 48.729f }; const Position KAELTHAS_BOW_TANK_POSITION = { 777.713f, -28.857f, 48.729f }; @@ -446,28 +453,6 @@ namespace TempestKeepHelpers std::unordered_map advisorDpsWaitTimer; - bool IsKaelthasInstanceTimerManager(PlayerbotAI* botAI, Player* bot) - { - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !botAI->IsDps(member) || - !GET_PLAYERBOT_AI(member)) - continue; - - Player* capernianTank = GetCapernianTank(botAI, member); - if (capernianTank && capernianTank == member) - continue; - - return member == bot; - } - } - - return false; - } - Player* GetCapernianTank(PlayerbotAI* botAI, Player* bot) { Group* group = bot->GetGroup(); diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.h b/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.h index 62de43a5e4d..6c5098b03cc 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.h +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.h @@ -100,7 +100,6 @@ namespace TempestKeepHelpers // General const uint32 TEMPEST_KEEP_MAP_ID = 550; - bool IsInstanceTimerManager(PlayerbotAI* botAI, Player* bot); void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId); void MarkTargetWithSkull(Player* bot, Unit* target); void MarkTargetWithSquare(Player* bot, Unit* target); @@ -110,6 +109,7 @@ namespace TempestKeepHelpers void MarkTargetWithDiamond(Player* bot, Unit* target); void MarkTargetWithCross(Player* bot, Unit* target); void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target); + bool IsInstanceTimerManager(PlayerbotAI* botAI, Player* bot); Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry); Unit* GetNearestPlayerInRadius(Player* bot, float radius); Unit* GetNearestNonTankPlayerInRadius(Player* bot, float radius); @@ -158,14 +158,16 @@ namespace TempestKeepHelpers // Kael'thas Sunstrider extern const Position SANGUINAR_TANK_POSITION; + extern const Position SANGUINAR_WAITING_POSITION; extern const Position TELONICUS_TANK_POSITION; + extern const Position TELONICUS_WAITING_POSITION; + extern const Position CAPERNIAN_WAITING_POSITION; extern const Position ADVISOR_HEAL_POSITION; extern const Position KAELTHAS_WEAPON_STACK_POSITION; extern const Position KAELTHAS_AXE_TANK_POSITION; extern const Position KAELTHAS_BOW_TANK_POSITION; extern const Position KAELTHAS_TANK_POSITION; extern std::unordered_map advisorDpsWaitTimer; - bool IsKaelthasInstanceTimerManager(PlayerbotAI* botAI, Player* bot); Player* GetCapernianTank(PlayerbotAI* botAI, Player* bot); Player* GetNetherstrandLongbowTank(PlayerbotAI* botAI, Player* bot); bool IsAnyLegendaryWeaponDead(PlayerbotAI* botAI, Player* bot); diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.cpp index ad08770b24b..73166789569 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.cpp @@ -4,6 +4,7 @@ #include "RaidTempestKeepKaelthasBossAI.h" #include "ChooseTargetActions.h" #include "DKActions.h" +#include "DruidActions.h" #include "DruidBearActions.h" #include "EquipAction.h" #include "FollowActions.h" @@ -358,23 +359,51 @@ float KaelthasSunstriderDisableDisperseMultiplier::GetValue(Action* action) return 1.0f; } -// Bloodlust/Heroism should be used at the start of Phase 3 -float KaelthasSunstriderDelayBloodlustAndHeroismMultiplier::GetValue(Action* action) +// Bloodlust/Heroism and other major cooldowns should be used at the start of Phase 3 +float KaelthasSunstriderDelayCooldownsMultiplier::GetValue(Action* action) { - if (bot->getClass() != CLASS_SHAMAN) - return 1.0f; - Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); if (!kaelthas) return 1.0f; boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); - if (kaelAI && - kaelAI->GetPhase() != PHASE_ALL_ADVISORS && kaelAI->GetPhase() != PHASE_FINAL) + if (!kaelAI) + return 1.0f; + + if (kaelAI->GetPhase() != PHASE_ALL_ADVISORS && kaelAI->GetPhase() != PHASE_FINAL) { - if (dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; + if (bot->getClass() == CLASS_SHAMAN) + { + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + if (botAI->IsDps(bot)) + { + 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; diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp index b1241eba2a8..e8311be4322 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp @@ -4,8 +4,8 @@ void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) { // General - triggers.push_back(new TriggerNode("tempest keep timer bot is not in combat", { - NextAction("tempest keep clear timers and trackers", ACTION_EMERGENCY + 11) })); + triggers.push_back(new TriggerNode("tempest keep bot is not in combat", { + NextAction("tempest keep erase timers and trackers", ACTION_EMERGENCY + 11) })); // Trash triggers.push_back(new TriggerNode("crimson hand centurion casts arcane volley", { @@ -94,8 +94,8 @@ void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("kael'thas sunstrider telonicus engaged by first assist tank", { NextAction("kael'thas sunstrider first assist tank position telonicus", ACTION_RAID + 1) })); - triggers.push_back(new TriggerNode("kael'thas sunstrider need dedicated bots for sanguinar and telonicus in phase 3", { - NextAction("kael'thas sunstrider handle sanguinar and telonicus in phase 3", ACTION_RAID + 2) })); + triggers.push_back(new TriggerNode("kael'thas sunstrider bots have specific roles in phase 3", { + NextAction("kael'thas sunstrider handle advisor roles in phase 3", ACTION_RAID + 2) })); triggers.push_back(new TriggerNode("kael'thas sunstrider determining advisor kill order", { NextAction("kael'thas sunstrider assign advisor dps priority", ACTION_RAID + 1) })); @@ -104,8 +104,12 @@ void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) NextAction("kael'thas sunstrider manage advisor dps timer", ACTION_EMERGENCY + 10) })); triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are alive", { - NextAction("kael'thas sunstrider assign legendary weapon dps priority", ACTION_RAID + 1), - NextAction("kael'thas sunstrider main tank move devastation away", ACTION_EMERGENCY + 1), + NextAction("kael'thas sunstrider assign legendary weapon dps priority", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary axe casts whirlwind", { + NextAction("kael'thas sunstrider main tank move devastation away", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary bow casts multishot", { NextAction("kael'thas sunstrider hunter turn away netherstrand longbow", ACTION_EMERGENCY + 1) })); triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are dead and lootable", { diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h index a017bcbfb52..659c03a5f5e 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h @@ -10,8 +10,8 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext RaidTempestKeepTriggerContext() { // General - creators["tempest keep timer bot is not in combat"] = - &RaidTempestKeepTriggerContext::tempest_keep_timer_bot_is_not_in_combat; + creators["tempest keep bot is not in combat"] = + &RaidTempestKeepTriggerContext::tempest_keep_bot_is_not_in_combat; // Trash creators["crimson hand centurion casts arcane volley"] = @@ -96,8 +96,8 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext creators["kael'thas sunstrider telonicus engaged by first assist tank"] = &RaidTempestKeepTriggerContext::kaelthas_sunstrider_telonicus_engaged_by_first_assist_tank; - creators["kael'thas sunstrider need dedicated bots for sanguinar and telonicus in phase 3"] = - &RaidTempestKeepTriggerContext::kaelthas_sunstrider_need_dedicated_bots_for_sanguinar_and_telonicus_in_phase_3; + creators["kael'thas sunstrider bots have specific roles in phase 3"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_bots_have_specific_roles_in_phase_3; creators["kael'thas sunstrider determining advisor kill order"] = &RaidTempestKeepTriggerContext::kaelthas_sunstrider_determining_advisor_kill_order; @@ -108,6 +108,12 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext creators["kael'thas sunstrider legendary weapons are alive"] = &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_are_alive; + creators["kael'thas sunstrider legendary axe casts whirlwind"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_axe_casts_whirlwind; + + creators["kael'thas sunstrider legendary bow casts multishot"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_bow_casts_multishot; + creators["kael'thas sunstrider legendary weapons are dead and lootable"] = &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_are_dead_and_lootable; @@ -135,8 +141,8 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext private: // General - static Trigger* tempest_keep_timer_bot_is_not_in_combat( - PlayerbotAI* botAI) { return new TempestKeepTimerBotIsNotInCombatTrigger(botAI); } + static Trigger* tempest_keep_bot_is_not_in_combat( + PlayerbotAI* botAI) { return new TempestKeepBotIsNotInCombatTrigger(botAI); } // Trash static Trigger* crimson_hand_centurion_casts_arcane_volley( @@ -221,8 +227,8 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext static Trigger* kaelthas_sunstrider_telonicus_engaged_by_first_assist_tank( PlayerbotAI* botAI) { return new KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger(botAI); } - static Trigger* kaelthas_sunstrider_need_dedicated_bots_for_sanguinar_and_telonicus_in_phase_3( - PlayerbotAI* botAI) { return new KaelthasSunstriderNeedDedicatedBotsForSanguinarAndTelonicusInPhase3Trigger(botAI); } + static Trigger* kaelthas_sunstrider_bots_have_specific_roles_in_phase_3( + PlayerbotAI* botAI) { return new KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger(botAI); } static Trigger* kaelthas_sunstrider_determining_advisor_kill_order( PlayerbotAI* botAI) { return new KaelthasSunstriderDeterminingAdvisorKillOrderTrigger(botAI); } @@ -233,6 +239,12 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext static Trigger* kaelthas_sunstrider_legendary_weapons_are_alive( PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryWeaponsAreAliveTrigger(botAI); } + static Trigger* kaelthas_sunstrider_legendary_axe_casts_whirlwind( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_legendary_bow_casts_multishot( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryBowCastsMultiShotTrigger(botAI); } + static Trigger* kaelthas_sunstrider_legendary_weapons_are_dead_and_lootable( PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger(botAI); } diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp index c45846f5731..6f8564a27fd 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp @@ -8,9 +8,9 @@ using namespace TempestKeepHelpers; // General -bool TempestKeepTimerBotIsNotInCombatTrigger::IsActive() +bool TempestKeepBotIsNotInCombatTrigger::IsActive() { - return IsInstanceTimerManager(botAI, bot) && !bot->IsInCombat(); + return !bot->IsInCombat(); } // Trash @@ -135,18 +135,7 @@ bool AlarStrategyChangesBetweenPhasesTrigger::IsActive() if (!AI_VALUE2(Unit*, "find target", "al'ar")) return false; - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsDps(member) && - GET_PLAYERBOT_AI(member)) - return member == bot; - } - } - - return false; + return IsInstanceTimerManager(botAI, bot); } // Void Reaver @@ -349,7 +338,7 @@ bool KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger::IsActive() return botAI->IsAssistTankOfIndex(bot, 0); } -bool KaelthasSunstriderNeedDedicatedBotsForSanguinarAndTelonicusInPhase3Trigger::IsActive() +bool KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger::IsActive() { Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); if (!kaelthas) @@ -357,7 +346,8 @@ bool KaelthasSunstriderNeedDedicatedBotsForSanguinarAndTelonicusInPhase3Trigger: if (!botAI->IsHealAssistantOfIndex(bot, 0) && !botAI->IsMainTank(bot) && - !botAI->IsAssistTankOfIndex(bot, 0)) + !botAI->IsAssistTankOfIndex(bot, 0) && + GetCapernianTank(botAI, bot) != bot) return false; boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); @@ -391,7 +381,7 @@ bool KaelthasSunstriderDeterminingAdvisorKillOrderTrigger::IsActive() bool KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger::IsActive() { - if (!IsKaelthasInstanceTimerManager(botAI, bot)) + if (!IsInstanceTimerManager(botAI, bot)) return false; Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); @@ -408,10 +398,29 @@ bool KaelthasSunstriderLegendaryWeaponsAreAliveTrigger::IsActive() if (!kaelthas) return false; + if (botAI->IsMainTank(bot) || GetNetherstrandLongbowTank(botAI, bot) == bot) + return false; + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); return kaelAI && kaelAI->GetPhase() == PHASE_WEAPONS; } +bool KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "devastation")) + return false; + + return botAI->IsMainTank(bot); +} + +bool KaelthasSunstriderLegendaryBowCastsMultiShotTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "netherstrand longbow")) + return false; + + return GetNetherstrandLongbowTank(botAI, bot) == bot; +} + bool KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger::IsActive() { Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h index cc2a3310346..580f80e53cb 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h @@ -5,11 +5,11 @@ // General -class TempestKeepTimerBotIsNotInCombatTrigger : public Trigger +class TempestKeepBotIsNotInCombatTrigger : public Trigger { public: - TempestKeepTimerBotIsNotInCombatTrigger( - PlayerbotAI* botAI) : Trigger(botAI, "tempest keep timer bot is not in combat") {} + TempestKeepBotIsNotInCombatTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "tempest keep bot is not in combat") {} bool IsActive() override; }; @@ -231,11 +231,11 @@ class KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger : public Trigge bool IsActive() override; }; -class KaelthasSunstriderNeedDedicatedBotsForSanguinarAndTelonicusInPhase3Trigger : public Trigger +class KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger : public Trigger { public: - KaelthasSunstriderNeedDedicatedBotsForSanguinarAndTelonicusInPhase3Trigger( - PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider need dedicated bots for sanguinar and telonicus in phase 3") {} + KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider bots have specific roles in phase 3") {} bool IsActive() override; }; @@ -263,6 +263,22 @@ class KaelthasSunstriderLegendaryWeaponsAreAliveTrigger : public Trigger bool IsActive() override; }; +class KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary axe casts whirlwind") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryBowCastsMultiShotTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryBowCastsMultiShotTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary bow casts multi shot") {} + bool IsActive() override; +}; + class KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger : public Trigger { public: From 1efba4f5234c6feacf1716b20a19b3d3e8d19bd1 Mon Sep 17 00:00:00 2001 From: crow Date: Thu, 15 Jan 2026 02:10:24 -0600 Subject: [PATCH 11/29] oops --- src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.h | 6 +++--- src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.h b/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.h index 0069c74f85c..23e801d6453 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.h +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.h @@ -123,11 +123,11 @@ class KaelthasSunstriderDisableDisperseMultiplier : public Multiplier virtual float GetValue(Action* action); }; -class KaelthasSunstriderDelayBloodlustAndHeroismMultiplier : public Multiplier +class KaelthasSunstriderDelayCooldownsMultiplier : public Multiplier { public: - KaelthasSunstriderDelayBloodlustAndHeroismMultiplier( - PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider delay bloodlust and heroism multiplier") {} + KaelthasSunstriderDelayCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider delay cooldowns multiplier") {} virtual float GetValue(Action* action); }; diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp index e8311be4322..780d01b9b9e 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp @@ -161,7 +161,7 @@ void RaidTempestKeepStrategy::InitMultipliers(std::vector& multipli multipliers.push_back(new KaelthasSunstriderDisableShadowWardMultiplier(botAI)); multipliers.push_back(new KaelthasSunstriderManageTankTargetingMultiplier(botAI)); multipliers.push_back(new KaelthasSunstriderDisableDisperseMultiplier(botAI)); - multipliers.push_back(new KaelthasSunstriderDelayBloodlustAndHeroismMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderDelayCooldownsMultiplier(botAI)); multipliers.push_back(new KaelthasSunstriderTryNonfatalBreakingOfMindControlMultiplier(botAI)); multipliers.push_back(new KaelthasSunstriderAllDpsOnBossDuringPyroblastMultiplier(botAI)); multipliers.push_back(new KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier(botAI)); From ad4a81225f5325fcd9593a0d2ac32d581c946af5 Mon Sep 17 00:00:00 2001 From: crow Date: Fri, 16 Jan 2026 01:54:46 -0600 Subject: [PATCH 12/29] timer fix --- .../tempestkeep/RaidTempestKeepActions.cpp | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp index 4b2cbf77601..ba8b30dea8d 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp @@ -20,23 +20,16 @@ bool TempestKeepEraseTimersAndTrackersAction::Execute(Event event) bool erased = false; if (!AI_VALUE2(Unit*, "find target", "al'ar")) { - if (lastRebirthState.erase(instanceId)) - erased = true; - if (isAlarInPhase2.erase(instanceId)) - erased = true; + erased |= lastRebirthState.erase(instanceId) > 0; + erased |= isAlarInPhase2.erase(instanceId) > 0; } - else if (!AI_VALUE2(Unit*, "find target", "void reaver")) + if (!AI_VALUE2(Unit*, "find target", "void reaver")) { - if (initialVoidReaverPositions.erase(guid)) - erased = true; - if (hasReachedInitialVoidReaverPosition.erase(guid)) - erased = true; - } - else if (!AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) - { - if (advisorDpsWaitTimer.erase(instanceId)) - erased = true; + erased |= initialVoidReaverPositions.erase(guid) > 0; + erased |= hasReachedInitialVoidReaverPosition.erase(guid) > 0; } + if (!AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) + erased |= advisorDpsWaitTimer.erase(instanceId) > 0; return erased; } From c18085af343d89b2ac2b55067a50e6843cd67b00 Mon Sep 17 00:00:00 2001 From: crow Date: Fri, 16 Jan 2026 18:13:52 -0600 Subject: [PATCH 13/29] clean up some triggers --- .../tempestkeep/RaidTempestKeepTriggers.cpp | 116 ++++++------------ 1 file changed, 37 insertions(+), 79 deletions(-) diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp index 6f8564a27fd..0cc9b7394a0 100644 --- a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp +++ b/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp @@ -17,10 +17,8 @@ bool TempestKeepBotIsNotInCombatTrigger::IsActive() bool CrimsonHandCenturionCastsArcaneVolleyTrigger::IsActive() { - if (bot->getClass() != CLASS_MAGE) - return false; - - return AI_VALUE2(Unit*, "find target", "crimson hand centurion"); + return bot->getClass() == CLASS_MAGE && + AI_VALUE2(Unit*, "find target", "crimson hand centurion"); } // Al'ar @@ -50,36 +48,26 @@ bool AlarBossIsFlyingBetweenPlatformsTrigger::IsActive() locationIndex = GetAlarDestinationLocationIndex(alar, dest); } - if (locationIndex == POINT_QUILL_OR_DIVE_IDX || - locationIndex == POINT_MIDDLE_IDX) - return false; - - return true; + return locationIndex != POINT_QUILL_OR_DIVE_IDX && + locationIndex != POINT_MIDDLE_IDX; } bool AlarEmbersOfAlarExplodeUponDeathTrigger::IsActive() { - if (!botAI->IsTank(bot)) - return false; - - return AI_VALUE2(Unit*, "find target", "ember of al'ar"); + return !botAI->IsTank(bot) && + AI_VALUE2(Unit*, "find target", "ember of al'ar"); } bool AlarKillingEmbersOfAlarDamagesBossTrigger::IsActive() { - if (!botAI->IsRangedDps(bot)) - return false; - - return AI_VALUE2(Unit*, "find target", "ember of al'ar"); + return botAI->IsRangedDps(bot) && + AI_VALUE2(Unit*, "find target", "ember of al'ar"); } bool AlarIncomingFlameQuillsTrigger::IsActive() { Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); - if (!alar) - return false; - - if (isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + if (!alar || isAlarInPhase2[alar->GetMap()->GetInstanceId()]) return false; Position dest; @@ -103,10 +91,7 @@ bool AlarRisingFromTheAshesTrigger::IsActive() bool AlarEverythingIsOnFireInPhase2Trigger::IsActive() { Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); - if (!alar) - return false; - - return isAlarInPhase2[alar->GetMap()->GetInstanceId()]; + return alar && isAlarInPhase2[alar->GetMap()->GetInstanceId()]; } bool AlarPhase2EncounterIsAtRoomCenterTrigger::IsActive() @@ -115,10 +100,7 @@ bool AlarPhase2EncounterIsAtRoomCenterTrigger::IsActive() return false; Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); - if (!alar) - return false; - - if (!isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + if (!alar || !isAlarInPhase2[alar->GetMap()->GetInstanceId()]) return false; Creature* alarCreature = alar->ToCreature(); @@ -132,10 +114,8 @@ bool AlarPhase2EncounterIsAtRoomCenterTrigger::IsActive() bool AlarStrategyChangesBetweenPhasesTrigger::IsActive() { - if (!AI_VALUE2(Unit*, "find target", "al'ar")) - return false; - - return IsInstanceTimerManager(botAI, bot); + return AI_VALUE2(Unit*, "find target", "al'ar") && + IsInstanceTimerManager(botAI, bot); } // Void Reaver @@ -160,20 +140,18 @@ bool VoidReaverKnockAwayReducesTankAggroTrigger::IsActive() bool VoidReaverBossLaunchesArcaneOrbsTrigger::IsActive() { - if (!botAI->IsRanged(bot)) - return false; - - return AI_VALUE2(Unit*, "find target", "void reaver"); + return botAI->IsRanged(bot) && + AI_VALUE2(Unit*, "find target", "void reaver"); } // High Astromancer Solarian bool HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger::IsActive() { - if (!botAI->IsRanged(bot)) + if (bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER)) return false; - if (bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER)) + if (!botAI->IsRanged(bot)) return false; Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); @@ -201,10 +179,8 @@ bool HighAstromancerSolarianBossHasVanishedTrigger::IsActive() bool HighAstromancerSolarianSolariumPriestsSpawnedTrigger::IsActive() { - if (!botAI->IsMelee(bot)) - return false; - - return AI_VALUE2(Unit*, "find target", "solarium priest"); + return botAI->IsMelee(bot) && + AI_VALUE2(Unit*, "find target", "solarium priest"); } bool HighAstromancerSolarianBossTransformedIntoVoidwalkerTrigger::IsActive() @@ -241,10 +217,7 @@ bool KaelthasSunstriderThaladredIsFixatedOnBotTrigger::IsActive() if (!kaelAI) return false; - if (botAI->IsTank(bot) && kaelAI->GetPhase() == PHASE_ALL_ADVISORS) - return false; - - return true; + return !(botAI->IsTank(bot) && kaelAI->GetPhase() == PHASE_ALL_ADVISORS); } bool KaelthasSunstriderPullingTankableAdvisorsTrigger::IsActive() @@ -344,6 +317,10 @@ bool KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger::IsActive() if (!kaelthas) return false; + if (!AI_VALUE2(Unit*, "find target", "master engineer telonicus") && + !AI_VALUE2(Unit*, "find target", "lord sanguinar")) + return false; + if (!botAI->IsHealAssistantOfIndex(bot, 0) && !botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0) && @@ -351,14 +328,7 @@ bool KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger::IsActive() return false; boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); - if (!kaelAI || kaelAI->GetPhase() != PHASE_ALL_ADVISORS) - return false; - - if (AI_VALUE2(Unit*, "find target", "master engineer telonicus") || - AI_VALUE2(Unit*, "find target", "lord sanguinar")) - return true; - - return false; + return kaelAI && kaelAI->GetPhase() == PHASE_ALL_ADVISORS; } bool KaelthasSunstriderDeterminingAdvisorKillOrderTrigger::IsActive() @@ -381,13 +351,13 @@ bool KaelthasSunstriderDeterminingAdvisorKillOrderTrigger::IsActive() bool KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger::IsActive() { - if (!IsInstanceTimerManager(botAI, bot)) - return false; - Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); if (!kaelthas) return false; + if (!IsInstanceTimerManager(botAI, bot)) + return false; + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); return kaelAI && kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR; } @@ -407,24 +377,20 @@ bool KaelthasSunstriderLegendaryWeaponsAreAliveTrigger::IsActive() bool KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger::IsActive() { - if (!AI_VALUE2(Unit*, "find target", "devastation")) - return false; - - return botAI->IsMainTank(bot); + return AI_VALUE2(Unit*, "find target", "devastation") && + botAI->IsMainTank(bot); } bool KaelthasSunstriderLegendaryBowCastsMultiShotTrigger::IsActive() { - if (!AI_VALUE2(Unit*, "find target", "netherstrand longbow")) - return false; - - return GetNetherstrandLongbowTank(botAI, bot) == bot; + return AI_VALUE2(Unit*, "find target", "netherstrand longbow") && + GetNetherstrandLongbowTank(botAI, bot) == bot; } bool KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger::IsActive() { - Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); - return kaelthas && IsAnyLegendaryWeaponDead(botAI, bot); + return AI_VALUE2(Unit*, "find target", "kael'thas sunstrider") && + IsAnyLegendaryWeaponDead(botAI, bot); } bool KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger::IsActive() @@ -452,10 +418,7 @@ bool KaelthasSunstriderLegendaryWeaponsWereLostTrigger::IsActive() return false; Creature* kaelthas = it->second; - if (!kaelthas) - return false; - - if (bot->GetExactDist2d(kaelthas) > 150.0f) + if (!kaelthas || bot->GetExactDist2d(kaelthas) > 150.0f) return false; Item* mainHand = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND); @@ -498,13 +461,8 @@ bool KaelthasSunstriderBossHasEnteredTheFightTrigger::IsActive() bool KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger::IsActive() { - if (AI_VALUE2(Unit*, "find target", "phoenix")) - return true; - - if (AI_VALUE2(Unit*, "find target", "phoenix egg")) - return true; - - return false; + return AI_VALUE2(Unit*, "find target", "phoenix") || + AI_VALUE2(Unit*, "find target", "phoenix egg"); } bool KaelthasSunstriderRaidMemberIsMindControlledTrigger::IsActive() From 61d0b7f72309b6a5d9e832dc7623c9aa624acdd4 Mon Sep 17 00:00:00 2001 From: crow Date: Sun, 18 Jan 2026 09:22:57 -0600 Subject: [PATCH 14/29] Update to reflect new module structure --- .../RaidAi/TempestKeep/Action}/RaidTempestKeepActions.cpp | 0 .../RaidAi/TempestKeep/Action}/RaidTempestKeepActions.h | 0 .../RaidAi/TempestKeep/Multiplier}/RaidTempestKeepMultipliers.cpp | 0 .../RaidAi/TempestKeep/Multiplier}/RaidTempestKeepMultipliers.h | 0 .../RaidAi/TempestKeep}/RaidTempestKeepActionContext.h | 0 .../RaidAi/TempestKeep}/RaidTempestKeepTriggerContext.h | 0 .../RaidAi/TempestKeep/Strategy}/RaidTempestKeepStrategy.cpp | 0 .../RaidAi/TempestKeep/Strategy}/RaidTempestKeepStrategy.h | 0 .../RaidAi/TempestKeep/Trigger}/RaidTempestKeepTriggers.cpp | 0 .../RaidAi/TempestKeep/Trigger}/RaidTempestKeepTriggers.h | 0 .../RaidAi/TempestKeep/Util}/RaidTempestKeepHelpers.cpp | 0 .../RaidAi/TempestKeep/Util}/RaidTempestKeepHelpers.h | 0 .../RaidAi/TempestKeep/Util}/RaidTempestKeepKaelthasBossAI.h | 0 13 files changed, 0 insertions(+), 0 deletions(-) rename src/{strategy/raids/tempestkeep => Scenario/RaidAi/TempestKeep/Action}/RaidTempestKeepActions.cpp (100%) rename src/{strategy/raids/tempestkeep => Scenario/RaidAi/TempestKeep/Action}/RaidTempestKeepActions.h (100%) rename src/{strategy/raids/tempestkeep => Scenario/RaidAi/TempestKeep/Multiplier}/RaidTempestKeepMultipliers.cpp (100%) rename src/{strategy/raids/tempestkeep => Scenario/RaidAi/TempestKeep/Multiplier}/RaidTempestKeepMultipliers.h (100%) rename src/{strategy/raids/tempestkeep => Scenario/RaidAi/TempestKeep}/RaidTempestKeepActionContext.h (100%) rename src/{strategy/raids/tempestkeep => Scenario/RaidAi/TempestKeep}/RaidTempestKeepTriggerContext.h (100%) rename src/{strategy/raids/tempestkeep => Scenario/RaidAi/TempestKeep/Strategy}/RaidTempestKeepStrategy.cpp (100%) rename src/{strategy/raids/tempestkeep => Scenario/RaidAi/TempestKeep/Strategy}/RaidTempestKeepStrategy.h (100%) rename src/{strategy/raids/tempestkeep => Scenario/RaidAi/TempestKeep/Trigger}/RaidTempestKeepTriggers.cpp (100%) rename src/{strategy/raids/tempestkeep => Scenario/RaidAi/TempestKeep/Trigger}/RaidTempestKeepTriggers.h (100%) rename src/{strategy/raids/tempestkeep => Scenario/RaidAi/TempestKeep/Util}/RaidTempestKeepHelpers.cpp (100%) rename src/{strategy/raids/tempestkeep => Scenario/RaidAi/TempestKeep/Util}/RaidTempestKeepHelpers.h (100%) rename src/{strategy/raids/tempestkeep => Scenario/RaidAi/TempestKeep/Util}/RaidTempestKeepKaelthasBossAI.h (100%) diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp b/src/Scenario/RaidAi/TempestKeep/Action/RaidTempestKeepActions.cpp similarity index 100% rename from src/strategy/raids/tempestkeep/RaidTempestKeepActions.cpp rename to src/Scenario/RaidAi/TempestKeep/Action/RaidTempestKeepActions.cpp diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActions.h b/src/Scenario/RaidAi/TempestKeep/Action/RaidTempestKeepActions.h similarity index 100% rename from src/strategy/raids/tempestkeep/RaidTempestKeepActions.h rename to src/Scenario/RaidAi/TempestKeep/Action/RaidTempestKeepActions.h diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.cpp b/src/Scenario/RaidAi/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp similarity index 100% rename from src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.cpp rename to src/Scenario/RaidAi/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.h b/src/Scenario/RaidAi/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h similarity index 100% rename from src/strategy/raids/tempestkeep/RaidTempestKeepMultipliers.h rename to src/Scenario/RaidAi/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepActionContext.h b/src/Scenario/RaidAi/TempestKeep/RaidTempestKeepActionContext.h similarity index 100% rename from src/strategy/raids/tempestkeep/RaidTempestKeepActionContext.h rename to src/Scenario/RaidAi/TempestKeep/RaidTempestKeepActionContext.h diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h b/src/Scenario/RaidAi/TempestKeep/RaidTempestKeepTriggerContext.h similarity index 100% rename from src/strategy/raids/tempestkeep/RaidTempestKeepTriggerContext.h rename to src/Scenario/RaidAi/TempestKeep/RaidTempestKeepTriggerContext.h diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp b/src/Scenario/RaidAi/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp similarity index 100% rename from src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.cpp rename to src/Scenario/RaidAi/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.h b/src/Scenario/RaidAi/TempestKeep/Strategy/RaidTempestKeepStrategy.h similarity index 100% rename from src/strategy/raids/tempestkeep/RaidTempestKeepStrategy.h rename to src/Scenario/RaidAi/TempestKeep/Strategy/RaidTempestKeepStrategy.h diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp b/src/Scenario/RaidAi/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp similarity index 100% rename from src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.cpp rename to src/Scenario/RaidAi/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h b/src/Scenario/RaidAi/TempestKeep/Trigger/RaidTempestKeepTriggers.h similarity index 100% rename from src/strategy/raids/tempestkeep/RaidTempestKeepTriggers.h rename to src/Scenario/RaidAi/TempestKeep/Trigger/RaidTempestKeepTriggers.h diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp b/src/Scenario/RaidAi/TempestKeep/Util/RaidTempestKeepHelpers.cpp similarity index 100% rename from src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.cpp rename to src/Scenario/RaidAi/TempestKeep/Util/RaidTempestKeepHelpers.cpp diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.h b/src/Scenario/RaidAi/TempestKeep/Util/RaidTempestKeepHelpers.h similarity index 100% rename from src/strategy/raids/tempestkeep/RaidTempestKeepHelpers.h rename to src/Scenario/RaidAi/TempestKeep/Util/RaidTempestKeepHelpers.h diff --git a/src/strategy/raids/tempestkeep/RaidTempestKeepKaelthasBossAI.h b/src/Scenario/RaidAi/TempestKeep/Util/RaidTempestKeepKaelthasBossAI.h similarity index 100% rename from src/strategy/raids/tempestkeep/RaidTempestKeepKaelthasBossAI.h rename to src/Scenario/RaidAi/TempestKeep/Util/RaidTempestKeepKaelthasBossAI.h From 8cee1dcdfd408e9bb4bc8ca088f5cf992d0836ef Mon Sep 17 00:00:00 2001 From: crow Date: Tue, 20 Jan 2026 20:06:09 -0600 Subject: [PATCH 15/29] update to reflect new module structure and make several fixes --- .../Action/RaidTempestKeepActions.cpp | 19 +++++++++---------- .../Action/RaidTempestKeepActions.h | 0 .../Multiplier/RaidTempestKeepMultipliers.cpp | 0 .../Multiplier/RaidTempestKeepMultipliers.h | 0 .../RaidTempestKeepActionContext.h | 0 .../TempestKeep}/RaidTempestKeepHelpers.cpp | 0 .../TempestKeep}/RaidTempestKeepHelpers.h | 5 ----- .../RaidTempestKeepKaelthasBossAI.h | 0 .../RaidTempestKeepTriggerContext.h | 0 .../Strategy/RaidTempestKeepStrategy.cpp | 0 .../Strategy/RaidTempestKeepStrategy.h | 0 .../Trigger/RaidTempestKeepTriggers.cpp | 2 +- .../Trigger/RaidTempestKeepTriggers.h | 0 13 files changed, 10 insertions(+), 16 deletions(-) rename src/{Scenario/RaidAi => Ai/Raid}/TempestKeep/Action/RaidTempestKeepActions.cpp (99%) rename src/{Scenario/RaidAi => Ai/Raid}/TempestKeep/Action/RaidTempestKeepActions.h (100%) rename src/{Scenario/RaidAi => Ai/Raid}/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp (100%) rename src/{Scenario/RaidAi => Ai/Raid}/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h (100%) rename src/{Scenario/RaidAi => Ai/Raid}/TempestKeep/RaidTempestKeepActionContext.h (100%) rename src/{Scenario/RaidAi/TempestKeep/Util => Ai/Raid/TempestKeep}/RaidTempestKeepHelpers.cpp (100%) rename src/{Scenario/RaidAi/TempestKeep/Util => Ai/Raid/TempestKeep}/RaidTempestKeepHelpers.h (97%) rename src/{Scenario/RaidAi/TempestKeep/Util => Ai/Raid/TempestKeep}/RaidTempestKeepKaelthasBossAI.h (100%) rename src/{Scenario/RaidAi => Ai/Raid}/TempestKeep/RaidTempestKeepTriggerContext.h (100%) rename src/{Scenario/RaidAi => Ai/Raid}/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp (100%) rename src/{Scenario/RaidAi => Ai/Raid}/TempestKeep/Strategy/RaidTempestKeepStrategy.h (100%) rename src/{Scenario/RaidAi => Ai/Raid}/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp (99%) rename src/{Scenario/RaidAi => Ai/Raid}/TempestKeep/Trigger/RaidTempestKeepTriggers.h (100%) diff --git a/src/Scenario/RaidAi/TempestKeep/Action/RaidTempestKeepActions.cpp b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp similarity index 99% rename from src/Scenario/RaidAi/TempestKeep/Action/RaidTempestKeepActions.cpp rename to src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp index ba8b30dea8d..f1f150a38ab 100644 --- a/src/Scenario/RaidAi/TempestKeep/Action/RaidTempestKeepActions.cpp +++ b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp @@ -18,11 +18,6 @@ bool TempestKeepEraseTimersAndTrackersAction::Execute(Event event) const uint32 instanceId = bot->GetMap()->GetInstanceId(); bool erased = false; - if (!AI_VALUE2(Unit*, "find target", "al'ar")) - { - erased |= lastRebirthState.erase(instanceId) > 0; - erased |= isAlarInPhase2.erase(instanceId) > 0; - } if (!AI_VALUE2(Unit*, "find target", "void reaver")) { erased |= initialVoidReaverPositions.erase(guid) > 0; @@ -44,9 +39,7 @@ bool CrimsonHandCenturionCastPolymorphAction::Execute(Event event) if (centurion->GetHealth() == centurion->GetMaxHealth()) { - if (!centurion->HasAura(SPELL_POLYMORPH_SHEEP) && - !centurion->HasAura(SPELL_POLYMORPH_TURTLE) && - !centurion->HasAura(SPELL_POLYMORPH_PIG)) + if (!botAI->HasAura("polymorph", centurion)) { if (botAI->CanCastSpell("polymorph", centurion)) return botAI->CastSpell("polymorph", centurion); @@ -678,6 +671,12 @@ bool AlarManagePhaseTrackerAction::Execute(Event event) const uint32 instanceId = alar->GetMap()->GetInstanceId(); + if (alar->GetHealthPct() > 99.5f && alar->GetPositionZ() >= ALAR_BALCONY_Z) + { + isAlarInPhase2.erase(instanceId); + lastRebirthState.erase(instanceId); + } + bool rebirthActive = alar->HasUnitState(UNIT_STATE_CASTING) && alar->FindCurrentSpellBySpellId(SPELL_REBIRTH_PHASE2); bool lastRebirth = lastRebirthState[instanceId]; @@ -1438,7 +1437,7 @@ bool KaelthasSunstriderHandleAdvisorRolesInPhase3Action::Execute(Event event) bool KaelthasSunstriderReequipGearAction::Execute(Event event) { - return botAI->DoSpecificAction("equip upgrades", Event(), true); + return botAI->DoSpecificAction("equip upgrade", Event(), true); } bool KaelthasSunstriderAssignAdvisorDpsPriorityAction::Execute(Event event) @@ -1838,7 +1837,7 @@ bool KaelthasSunstriderLootLegendaryWeaponsAction::LootWeapon( receiver->GetSession()->QueuePacket(packet); }, 600); - botAI->DoSpecificAction("equip upgrades", Event(), true); + botAI->DoSpecificAction("equip upgrade", Event(), true); return true; } diff --git a/src/Scenario/RaidAi/TempestKeep/Action/RaidTempestKeepActions.h b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h similarity index 100% rename from src/Scenario/RaidAi/TempestKeep/Action/RaidTempestKeepActions.h rename to src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h diff --git a/src/Scenario/RaidAi/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp similarity index 100% rename from src/Scenario/RaidAi/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp rename to src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp diff --git a/src/Scenario/RaidAi/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h similarity index 100% rename from src/Scenario/RaidAi/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h rename to src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h diff --git a/src/Scenario/RaidAi/TempestKeep/RaidTempestKeepActionContext.h b/src/Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h similarity index 100% rename from src/Scenario/RaidAi/TempestKeep/RaidTempestKeepActionContext.h rename to src/Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h diff --git a/src/Scenario/RaidAi/TempestKeep/Util/RaidTempestKeepHelpers.cpp b/src/Ai/Raid/TempestKeep/RaidTempestKeepHelpers.cpp similarity index 100% rename from src/Scenario/RaidAi/TempestKeep/Util/RaidTempestKeepHelpers.cpp rename to src/Ai/Raid/TempestKeep/RaidTempestKeepHelpers.cpp diff --git a/src/Scenario/RaidAi/TempestKeep/Util/RaidTempestKeepHelpers.h b/src/Ai/Raid/TempestKeep/RaidTempestKeepHelpers.h similarity index 97% rename from src/Scenario/RaidAi/TempestKeep/Util/RaidTempestKeepHelpers.h rename to src/Ai/Raid/TempestKeep/RaidTempestKeepHelpers.h index 6c5098b03cc..5b1d99d2e20 100644 --- a/src/Scenario/RaidAi/TempestKeep/Util/RaidTempestKeepHelpers.h +++ b/src/Ai/Raid/TempestKeep/RaidTempestKeepHelpers.h @@ -38,11 +38,6 @@ namespace TempestKeepHelpers // Hunter SPELL_MISDIRECTION = 35079, - // Mage - SPELL_POLYMORPH_SHEEP = 12826, - SPELL_POLYMORPH_TURTLE = 28271, - SPELL_POLYMORPH_PIG = 28272, - // Priest SPELL_FEAR_WARD = 6346, }; diff --git a/src/Scenario/RaidAi/TempestKeep/Util/RaidTempestKeepKaelthasBossAI.h b/src/Ai/Raid/TempestKeep/RaidTempestKeepKaelthasBossAI.h similarity index 100% rename from src/Scenario/RaidAi/TempestKeep/Util/RaidTempestKeepKaelthasBossAI.h rename to src/Ai/Raid/TempestKeep/RaidTempestKeepKaelthasBossAI.h diff --git a/src/Scenario/RaidAi/TempestKeep/RaidTempestKeepTriggerContext.h b/src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h similarity index 100% rename from src/Scenario/RaidAi/TempestKeep/RaidTempestKeepTriggerContext.h rename to src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h diff --git a/src/Scenario/RaidAi/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp b/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp similarity index 100% rename from src/Scenario/RaidAi/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp rename to src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp diff --git a/src/Scenario/RaidAi/TempestKeep/Strategy/RaidTempestKeepStrategy.h b/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.h similarity index 100% rename from src/Scenario/RaidAi/TempestKeep/Strategy/RaidTempestKeepStrategy.h rename to src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.h diff --git a/src/Scenario/RaidAi/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp similarity index 99% rename from src/Scenario/RaidAi/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp rename to src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp index 0cc9b7394a0..7ffac770714 100644 --- a/src/Scenario/RaidAi/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp +++ b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp @@ -54,7 +54,7 @@ bool AlarBossIsFlyingBetweenPlatformsTrigger::IsActive() bool AlarEmbersOfAlarExplodeUponDeathTrigger::IsActive() { - return !botAI->IsTank(bot) && + return botAI->IsTank(bot) && AI_VALUE2(Unit*, "find target", "ember of al'ar"); } diff --git a/src/Scenario/RaidAi/TempestKeep/Trigger/RaidTempestKeepTriggers.h b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.h similarity index 100% rename from src/Scenario/RaidAi/TempestKeep/Trigger/RaidTempestKeepTriggers.h rename to src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.h From 23270ef9ab011383d3d3fbfb4a9a7e313e8de5b3 Mon Sep 17 00:00:00 2001 From: crow Date: Sat, 24 Jan 2026 17:39:45 -0600 Subject: [PATCH 16/29] Update for assistant function commit --- .../Action/RaidTempestKeepActions.cpp | 69 ++++++++++--------- .../Multiplier/RaidTempestKeepMultipliers.cpp | 2 +- .../TempestKeep/RaidTempestKeepHelpers.cpp | 2 +- .../Trigger/RaidTempestKeepTriggers.cpp | 8 +-- 4 files changed, 43 insertions(+), 38 deletions(-) diff --git a/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp index f1f150a38ab..f77a75637b1 100644 --- a/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp +++ b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp @@ -20,11 +20,16 @@ bool TempestKeepEraseTimersAndTrackersAction::Execute(Event event) bool erased = false; if (!AI_VALUE2(Unit*, "find target", "void reaver")) { - erased |= initialVoidReaverPositions.erase(guid) > 0; - erased |= hasReachedInitialVoidReaverPosition.erase(guid) > 0; + if (initialVoidReaverPositions.erase(guid) > 0) + erased = true; + if (hasReachedInitialVoidReaverPosition.erase(guid) > 0) + erased = true; } if (!AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) - erased |= advisorDpsWaitTimer.erase(instanceId) > 0; + { + if (advisorDpsWaitTimer.erase(instanceId) > 0) + erased = true; + } return erased; } @@ -87,7 +92,7 @@ bool AlarMisdirectBossToMainTankAction::Execute(Event event) bool AlarBossTanksMoveBetweenPlatformsAction::Execute(Event event) { - if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0)) + if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0, true)) return false; Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); @@ -104,11 +109,10 @@ bool AlarBossTanksMoveBetweenPlatformsAction::Execute(Event event) locationIndex = GetAlarDestinationLocationIndex(alar, dest); } - if (botAI->IsMainTank(bot) && PositionMainTank(bot, alar, locationIndex)) - return true; - - if (botAI->IsAssistTankOfIndex(bot, 0) && PositionAssistTank(bot, alar, locationIndex)) - return true; + if (botAI->IsMainTank(bot)) + return PositionMainTank(bot, alar, locationIndex); + else if (botAI->IsAssistTankOfIndex(bot, 0, true)) + return PositionAssistTank(bot, alar, locationIndex); return false; } @@ -223,7 +227,7 @@ bool AlarMeleeDpsMoveBetweenPlatformsAction::Execute(Event event) bool AlarRangedAndEmberTankMoveUnderPlatformsAction::Execute(Event event) { - if (!botAI->IsRanged(bot) && !botAI->IsAssistTankOfIndex(bot, 1)) + if (!botAI->IsRanged(bot) && !botAI->IsAssistTankOfIndex(bot, 1, true)) return false; Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); @@ -253,7 +257,7 @@ bool AlarRangedAndEmberTankMoveUnderPlatformsAction::Execute(Event event) 8.0f, MovementPriority::MOVEMENT_COMBAT); } } - else if (botAI->IsAssistTankOfIndex(bot, 1)) + else if (botAI->IsAssistTankOfIndex(bot, 1, true)) { Unit* ember = AI_VALUE2(Unit*, "find target", "ember of al'ar"); if (!ember && bot->GetExactDist2d( @@ -289,7 +293,7 @@ bool AlarAssistTanksPickUpEmbersAction::Execute(Event event) // Embers will be tanked by only the second assist tank in Phase 1 bool AlarAssistTanksPickUpEmbersAction::HandlePhase1Embers(Unit* alar) { - if (!botAI->IsAssistTankOfIndex(bot, 1)) + if (!botAI->IsAssistTankOfIndex(bot, 1, true)) return false; Unit* ember = AI_VALUE2(Unit*, "find target", "ember of al'ar"); @@ -351,7 +355,7 @@ bool AlarAssistTanksPickUpEmbersAction::HandlePhase2Embers(Unit* alar) { auto [firstEmber, secondEmber] = GetFirstTwoEmbersOfAlar(botAI); - if (botAI->IsAssistTankOfIndex(bot, 1) && firstEmber) + if (botAI->IsAssistTankOfIndex(bot, 1, true) && firstEmber) { MarkTargetWithSquare(bot, firstEmber); SetRtiTarget(botAI, "square", firstEmber); @@ -470,7 +474,7 @@ bool AlarJumpFromPlatformAction::Execute(Event event) MovementPriority::MOVEMENT_FORCED, true, false); } } - else if (botAI->IsAssistTankOfIndex(bot, 0)) + else if (botAI->IsAssistTankOfIndex(bot, 0, true)) { if (bot->GetExactDist2d(ALAR_SE_RAMP_BASE.GetPositionX(), ALAR_SE_RAMP_BASE.GetPositionY()) > 5.0f) { @@ -479,7 +483,7 @@ bool AlarJumpFromPlatformAction::Execute(Event event) MovementPriority::MOVEMENT_FORCED, true, false); } } - else if (botAI->IsAssistTankOfIndex(bot, 1)) + else if (botAI->IsAssistTankOfIndex(bot, 1, true)) { if (bot->GetExactDist2d(ALAR_POINT_MIDDLE.GetPositionX(), ALAR_POINT_MIDDLE.GetPositionY()) > 25.0f) { @@ -541,7 +545,7 @@ bool AlarMoveAwayFromRebirthAction::Execute(Event event) // Main tank and first assist tank will swap tanking Al'ar when Melt Armor is applied bool AlarSwapTanksOnBossAction::Execute(Event event) { - if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0)) + if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0, true)) return false; Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); @@ -636,12 +640,11 @@ bool AlarAvoidFlamePatchesAndDiveBombsAction::HandleDiveBomb(Unit* alar) GetAlarDestinationLocationIndex(alar, dest) == POINT_QUILL_OR_DIVE_IDX) { const float safeDistance = 10.0f; - Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance); - if (nearestPlayer) + if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) { const uint32 minInterval = 0; - return FleePosition(Position(nearestPlayer->GetPositionX(), nearestPlayer->GetPositionY(), - nearestPlayer->GetPositionZ()), safeDistance, minInterval); + return FleePosition(Position(nearestPlayer->GetPosition()), + safeDistance, minInterval); } } } @@ -1090,7 +1093,8 @@ bool KaelthasSunstriderMisdirectAdvisorsToTanksAction::Execute(Event event) for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (member && member->IsAlive() && GET_PLAYERBOT_AI(member)->IsAssistTankOfIndex(member, 0)) + if (member && member->IsAlive() && + GET_PLAYERBOT_AI(member)->IsAssistTankOfIndex(member, 0, true)) { tankTarget = member; break; @@ -1297,12 +1301,11 @@ bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::RangedBotsDisperse( } const float safeDistance = 6.0f; - Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance); - if (nearestPlayer) + if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) { const uint32 minInterval = 1000; - return FleePosition(Position(nearestPlayer->GetPositionX(), nearestPlayer->GetPositionY(), - nearestPlayer->GetPositionZ()), safeDistance, minInterval); + return FleePosition(Position(nearestPlayer->GetPosition()), + safeDistance, minInterval); } return false; @@ -1401,7 +1404,7 @@ bool KaelthasSunstriderHandleAdvisorRolesInPhase3Action::Execute(Event event) { const Position* movePosition = nullptr; - if (botAI->IsHealAssistantOfIndex(bot, 0)) + if (botAI->IsAssistHealOfIndex(bot, 0, true)) movePosition = &ADVISOR_HEAL_POSITION; else if (botAI->IsMainTank(bot)) { @@ -1409,7 +1412,7 @@ bool KaelthasSunstriderHandleAdvisorRolesInPhase3Action::Execute(Event event) if (sanguinar && sanguinar->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) movePosition = &SANGUINAR_WAITING_POSITION; } - else if (botAI->IsAssistTankOfIndex(bot, 0)) + else if (botAI->IsAssistTankOfIndex(bot, 0, true)) { Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); if (telonicus && telonicus->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) @@ -2004,7 +2007,8 @@ bool KaelthasSunstriderAvoidFlameStrikeAction::Execute(Event event) bool KaelthasSunstriderHandlePhoenixesAndEggsAction::Execute(Event event) { - if (botAI->IsAssistTankOfIndex(bot, 0) || botAI->IsAssistTankOfIndex(bot, 1)) + if (botAI->IsAssistTankOfIndex(bot, 0, true) || + botAI->IsAssistTankOfIndex(bot, 1, true)) { std::vector phoenixes; auto const& npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); @@ -2022,13 +2026,13 @@ bool KaelthasSunstriderHandlePhoenixesAndEggsAction::Execute(Event event) [](Unit* a, Unit* b) { return a->GetGUID() < b->GetGUID(); }); Unit* targetPhoenix = nullptr; - if (botAI->IsAssistTankOfIndex(bot, 0)) + if (botAI->IsAssistTankOfIndex(bot, 0, true)) { targetPhoenix = phoenixes[0]; MarkTargetWithSquare(bot, targetPhoenix); SetRtiTarget(botAI, "square", targetPhoenix); } - else if (botAI->IsAssistTankOfIndex(bot, 1) && phoenixes.size() >= 2) + else if (botAI->IsAssistTankOfIndex(bot, 1, true) && phoenixes.size() >= 2) { targetPhoenix = phoenixes[1]; MarkTargetWithCircle(bot, targetPhoenix); @@ -2051,8 +2055,9 @@ bool KaelthasSunstriderHandlePhoenixesAndEggsAction::Execute(Event event) if (nearestPlayer && group) { PlayerbotAI* nearestAI = GET_PLAYERBOT_AI(nearestPlayer->ToPlayer()); - if (nearestAI && (nearestAI->IsAssistTankOfIndex(nearestPlayer->ToPlayer(), 0) || - nearestAI->IsAssistTankOfIndex(nearestPlayer->ToPlayer(), 1))) + if (nearestAI && + (nearestAI->IsAssistTankOfIndex(nearestPlayer->ToPlayer(), 0, true) || + nearestAI->IsAssistTankOfIndex(nearestPlayer->ToPlayer(), 1, true))) { nearestPlayer = nullptr; } diff --git a/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp index 73166789569..4d67dd3bd61 100644 --- a/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp +++ b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp @@ -216,7 +216,7 @@ float KaelthasSunstriderWaitForDpsMultiplier::GetValue(Action* action) }; if ((isAdvisorActive(sanguinar) && !botAI->IsMainTank(bot)) || - (isAdvisorActive(telonicus) && !botAI->IsAssistTankOfIndex(bot, 0)) || + (isAdvisorActive(telonicus) && !botAI->IsAssistTankOfIndex(bot, 0, true)) || (isAdvisorActive(capernian) && !botAI->IsMainTank(bot) && GetCapernianTank(botAI, bot) != bot)) { if (dynamic_cast(action) || diff --git a/src/Ai/Raid/TempestKeep/RaidTempestKeepHelpers.cpp b/src/Ai/Raid/TempestKeep/RaidTempestKeepHelpers.cpp index e52cac619d3..e638d606c54 100644 --- a/src/Ai/Raid/TempestKeep/RaidTempestKeepHelpers.cpp +++ b/src/Ai/Raid/TempestKeep/RaidTempestKeepHelpers.cpp @@ -408,7 +408,7 @@ namespace TempestKeepHelpers if (botAI->IsMainTank(member)) mainTank = member; - if (botAI->IsAssistTankOfIndex(member, 0)) + if (botAI->IsAssistTankOfIndex(member, 0, false)) assistTank = member; } } diff --git a/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp index 7ffac770714..833374bcd97 100644 --- a/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp +++ b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp @@ -308,7 +308,7 @@ bool KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger::IsActive() telonicus->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) return false; - return botAI->IsAssistTankOfIndex(bot, 0); + return botAI->IsAssistTankOfIndex(bot, 0, true); } bool KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger::IsActive() @@ -321,9 +321,9 @@ bool KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger::IsActive() !AI_VALUE2(Unit*, "find target", "lord sanguinar")) return false; - if (!botAI->IsHealAssistantOfIndex(bot, 0) && + if (!botAI->IsAssistHealOfIndex(bot, 0, true) && !botAI->IsMainTank(bot) && - !botAI->IsAssistTankOfIndex(bot, 0) && + !botAI->IsAssistTankOfIndex(bot, 0, true) && GetCapernianTank(botAI, bot) != bot) return false; @@ -341,7 +341,7 @@ bool KaelthasSunstriderDeterminingAdvisorKillOrderTrigger::IsActive() return false; if (botAI->IsMainTank(bot) || - botAI->IsAssistTankOfIndex(bot, 0)) + botAI->IsAssistTankOfIndex(bot, 0, true)) return false; boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); From bebac60c51cb764f65cdf29878a77470accc223b Mon Sep 17 00:00:00 2001 From: kadeshar Date: Fri, 6 Feb 2026 20:55:28 +0100 Subject: [PATCH 17/29] test-staging alignment (#2121) # Pull Request Describe what this change does and why it is needed... --- ## Design Philosophy We prioritize **stability, performance, and predictability** over behavioral realism. Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and long-term robustness. Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all participants. Because every action and decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and negatively affect both players and world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a project goal. Increased behavioral realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and maintained continuously as the system evolves. If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision model**. More complex behavior should only be available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. Principles: - **Stability before intelligence** A stable system is always preferred over a smarter one. - **Performance is a shared resource** Any increase in bot cost affects all players and all bots. - **Simple logic scales better than smart logic** Predictable behavior under load is more valuable than perfect decisions. - **Complexity must justify itself** If a feature cannot clearly explain its cost, it should not exist. - **Defaults must be cheap** Expensive behavior must always be optional and clearly communicated. - **Bots should look reasonable, not perfect** The goal is believable behavior, not human simulation. Before submitting, confirm that this change aligns with those principles. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? - Describe the **cheapest implementation** that produces an acceptable result? - Describe the **runtime cost** when this logic executes across many bots? --- ## How to Test the Changes - Step-by-step instructions to test the change - Any required setup (e.g. multiple players, bots, specific configuration) - Expected behavior and how to verify it ## Complexity & Impact Does this change add new decision branches? - - [ ] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [ ] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [ ] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [ ] No - - [ ] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - - [ ] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [ ] No - - [ ] Yes (**explain below**) If yes, please specify: - AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) - Which parts of the change were influenced or generated - Whether the result was manually reviewed and adapted AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest about what they do and do not understand. --- ## Final Checklist - - [ ] Stability is not compromised - - [ ] Performance impact is understood, tested, and acceptable - - [ ] Added logic complexity is justified and explained - - [ ] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --------- Co-authored-by: Crow --- PULL_REQUEST_TEMPLATE.md | 44 +++++++++++++--------------------- conf/playerbots.conf.dist | 2 +- src/Bot/RandomPlayerbotMgr.cpp | 2 +- 3 files changed, 18 insertions(+), 30 deletions(-) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index a23a7e67fa4..d26b1b1cd79 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -67,46 +67,34 @@ Please answer the following: ## Complexity & Impact Does this change add new decision branches? -``` -[ ] No -[ ] Yes (**explain below**) -``` +- - [ ] No +- - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? -``` -[ ] No -[ ] Yes (**describe and justify impact**) -``` +- - [ ] No +- - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? -``` -[ ] No -[ ] Yes (**explain why**) -``` +- - [ ] No +- - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? -``` -[ ] No -[ ] Yes (**explain why**) -``` +- - [ ] No +- - [ ] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: -``` -[ ] Lightweight mode remains the default -[ ] More complex behavior is optional and thereby configurable -``` +- - [ ] Lightweight mode remains the default +- - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? -``` -[ ] No -[ ] Yes (**explain below**) -``` +- - [ ] No +- - [ ] Yes (**explain below**) If yes, please specify: @@ -123,10 +111,10 @@ about what they do and do not understand. ## Final Checklist -- [ ] Stability is not compromised -- [ ] Performance impact is understood, tested, and acceptable -- [ ] Added logic complexity is justified and explained -- [ ] Documentation updated if needed +- - [ ] Stability is not compromised +- - [ ] Performance impact is understood, tested, and acceptable +- - [ ] Added logic complexity is justified and explained +- - [ ] Documentation updated if needed --- diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 22615b4071a..90737731672 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -990,7 +990,7 @@ AiPlayerbot.ZoneBracket.3433 = 10,22 AiPlayerbot.ZoneBracket.3525 = 10,21 # Classic WoW - High-level zones: -# Deadwind Pass (Zone ID: 10 Default Min,Max: 19,33) +# Duskwood (Zone ID: 10 Default Min,Max: 19,33) # Wetlands (Zone ID: 11 Default Min,Max: 21,30) # Redridge Mountains (Zone ID: 44 Default Min,Max: 16,28) # Hillsbrad Foothills (Zone ID: 267 Default Min,Max: 20,34) diff --git a/src/Bot/RandomPlayerbotMgr.cpp b/src/Bot/RandomPlayerbotMgr.cpp index 4707d854ff9..892368c66f1 100644 --- a/src/Bot/RandomPlayerbotMgr.cpp +++ b/src/Bot/RandomPlayerbotMgr.cpp @@ -1784,7 +1784,7 @@ void RandomPlayerbotMgr::PrepareZone2LevelBracket() zone2LevelBracket[3525] = {10, 21}; // Bloodmyst Isle // Classic WoW - High - level zones - zone2LevelBracket[10] = {19, 33}; // Deadwind Pass + zone2LevelBracket[10] = {19, 33}; // Duskwood zone2LevelBracket[11] = {21, 30}; // Wetlands zone2LevelBracket[44] = {16, 28}; // Redridge Mountains zone2LevelBracket[267] = {20, 34}; // Hillsbrad Foothills From b31bda85eedc70cd0c7e2618fda3916b1ca17f0c Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 6 Feb 2026 13:55:43 -0600 Subject: [PATCH 18/29] Refactor raid strategy framework (#2069) # Pull Request The purposes of this PR are to (1) establish a general raid helper framework for the benefit of future raid strategies and (2) make some improvements to problematic areas of the raid strategy code. List of changes: 1. Added new RaidBossHelpers.cpp and RaidBossHelpers.h files in the Raid folder. 3. Moved reused helpers from Karazhan, Gruul, and Magtheridon strategies to the new helper files. 4. Modified the prior function that assigned a DPS bot to store and erase timers and trackers in associative containers--the function now includes parameters for mapId (so a bot that is not in the instance will not be assigned) and for the ability to exclude a bot (useful for excluding particular important roles, such as a Warlock tank, so they are not bogged down by these extra tasks at critical moments). I also renamed it from IsInstanceTimerManager to IsMechanicTrackerBot. 5. Moved all helper files in raid strategies to Util folders (was needed for ICC, MC, and Ulduar). 6. Renamed and reordered includes of Ulduar files in AiObjectContext.cpp to match other raid strategies. a. This initially caused compile errors which made me realize that the existing code had several problems with missing includes and was compiling only due to the prior ordering in AiObjectContext.cpp. Therefore, I added the missing includes to Molten Core, Ulduar, and Vault of Archavon strategies. b. Ulduar and Old Kingdom were also using the same constant name for a spell--the reordering caused a compile error here as well, which just highlighted an existing problem that was being hidden. I renamed the constant for Ulduar to fix this, but I think the better approach going forward would be to use a namespace or enum class. But that is for another time and probably another person. 7. Several changes with respect to Ulduar files: a. The position constants and enums for spells and NPCs and such were in the trigger header file. I did not think that made sense so moved them to existing helper files. b. Since the strategy does not use multipliers, I removed all files and references to multipliers in it. c. I removed some unneeded includes. I did not do a detailed review to determine what else could be removed--I just took some out that I could tell right away were not needed. d. I renamed the ingame strategy name from "uld" to "ulduar," which I think is clearer and is still plenty short. 8. Partial refactor of Gruul and Magtheridon strategies: a. I did not due a full refactoring but made some quick changes to things I did previously that were rather stupid like repeating calculations, having useless logic like pointless IsAlive() checks for creatures already on the hostile references list, and not using the existing Position class for coordinates. b. There were a few substantive changes, such as allowing players to pick Maulgar mage and moonkin tanks with the assistant flag, but a greater refactoring of the strategies themselves is beyond this PR. c. I was clearing some containers used for Gruul and Magtheridon strategies; the methods are now fixed to erase only the applicable keys so that in the unlikely event that one server has multiple groups running Gruul or Magtheridon at the same time, there won't be timer or position tracker conflicts. ## How to Test the Changes 1. Enter any raid instance that has any code impacted by this PR 2. Engage bosses and observe if any strategies are now broken I personally tested Maulgar, Gruul, and Magtheridon and confirmed that they still work as intended. ## Complexity & Impact I do not expect this PR to have any relevant changes to in-game performance, but I will defer to those more knowledgeable than I if there are concerns in this area. As I've mentioned before, you can consider me to be like a person who has taken half an intro C++ course at best. ## AI Assistance None beyond autocomplete of repetitive changes. --------- Co-authored-by: bashermens <31279994+hermensbas@users.noreply.github.com> --- .../Action/RaidGruulsLairActions.cpp | 218 +++++------ .../GruulsLair/Action/RaidGruulsLairActions.h | 4 +- .../Multiplier/RaidGruulsLairMultipliers.cpp | 25 +- .../GruulsLair/RaidGruulsLairActionContext.h | 4 +- .../GruulsLair/RaidGruulsLairTriggerContext.h | 8 +- .../Strategy/RaidGruulsLairStrategy.cpp | 6 +- .../Trigger/RaidGruulsLairTriggers.cpp | 45 ++- .../Trigger/RaidGruulsLairTriggers.h | 8 +- .../GruulsLair/Util/RaidGruulsLairHelpers.cpp | 135 +++---- .../GruulsLair/Util/RaidGruulsLairHelpers.h | 39 +- .../Raid/Icecrown/{ => Util}/RaidIccScripts.h | 0 .../Karazhan/Action/RaidKarazhanActions.cpp | 27 +- .../Multiplier/RaidKarazhanMultipliers.cpp | 4 + .../Karazhan/Trigger/RaidKarazhanTriggers.cpp | 13 +- .../Karazhan/Util/RaidKarazhanHelpers.cpp | 108 ------ .../Raid/Karazhan/Util/RaidKarazhanHelpers.h | 19 +- .../Action/RaidMagtheridonActions.cpp | 160 ++++---- .../Action/RaidMagtheridonActions.h | 6 +- .../Multiplier/RaidMagtheridonMultipliers.cpp | 20 +- .../Trigger/RaidMagtheridonTriggers.cpp | 22 +- .../Util/RaidMagtheridonHelpers.cpp | 91 +---- .../Magtheridon/Util/RaidMagtheridonHelpers.h | 35 +- src/Ai/Raid/MoltenCore/RaidMcActionContext.h | 1 + src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h | 1 + .../MoltenCore/{ => Util}/RaidMcHelpers.h | 0 src/Ai/Raid/RaidBossHelpers.cpp | 142 ++++++++ src/Ai/Raid/RaidBossHelpers.h | 21 ++ src/Ai/Raid/RaidStrategyContext.h | 4 +- .../Raid/Ulduar/Action/RaidUlduarActions.cpp | 3 - .../Multiplier/RaidUlduarMultipliers.cpp | 28 -- .../Ulduar/Multiplier/RaidUlduarMultipliers.h | 17 - src/Ai/Raid/Ulduar/RaidUlduarBossHelper.h | 169 --------- .../Ulduar/Strategy/RaidUlduarStrategy.cpp | 7 - .../Raid/Ulduar/Strategy/RaidUlduarStrategy.h | 4 +- .../Ulduar/Trigger/RaidUlduarTriggers.cpp | 2 +- .../Raid/Ulduar/Trigger/RaidUlduarTriggers.h | 178 --------- .../{ => Util}/RaidUlduarBossHelper.cpp | 39 +- .../Raid/Ulduar/Util/RaidUlduarBossHelper.h | 341 ++++++++++++++++++ .../Ulduar/{ => Util}/RaidUlduarScripts.h | 0 .../VaultOfArchavon/RaidVoAActionContext.h | 1 + .../VaultOfArchavon/RaidVoATriggerContext.h | 1 + src/Bot/Engine/AiObjectContext.cpp | 4 +- src/Bot/PlayerbotAI.cpp | 2 +- 43 files changed, 886 insertions(+), 1076 deletions(-) rename src/Ai/Raid/Icecrown/{ => Util}/RaidIccScripts.h (100%) rename src/Ai/Raid/MoltenCore/{ => Util}/RaidMcHelpers.h (100%) create mode 100644 src/Ai/Raid/RaidBossHelpers.cpp create mode 100644 src/Ai/Raid/RaidBossHelpers.h delete mode 100644 src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.cpp delete mode 100644 src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.h delete mode 100644 src/Ai/Raid/Ulduar/RaidUlduarBossHelper.h rename src/Ai/Raid/Ulduar/{ => Util}/RaidUlduarBossHelper.cpp (63%) create mode 100644 src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.h rename src/Ai/Raid/Ulduar/{ => Util}/RaidUlduarScripts.h (100%) diff --git a/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.cpp b/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.cpp index 1a98135cac3..2af82ebe99f 100644 --- a/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.cpp +++ b/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.cpp @@ -2,6 +2,7 @@ #include "RaidGruulsLairHelpers.h" #include "CreatureAI.h" #include "Playerbots.h" +#include "RaidBossHelpers.h" #include "Unit.h" using namespace GruulsLairHelpers; @@ -12,6 +13,8 @@ using namespace GruulsLairHelpers; bool HighKingMaulgarMainTankAttackMaulgarAction::Execute(Event event) { Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); + if (!maulgar) + return false; MarkTargetWithSquare(bot, maulgar); SetRtiTarget(botAI, "square", maulgar); @@ -21,31 +24,20 @@ bool HighKingMaulgarMainTankAttackMaulgarAction::Execute(Event event) if (maulgar->GetVictim() == bot) { - const Location& tankPosition = GruulsLairLocations::MaulgarTankPosition; + const Position& position = MAULGAR_TANK_POSITION; const float maxDistance = 3.0f; - float distanceToTankPosition = bot->GetExactDist2d(tankPosition.x, tankPosition.y); + float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - if (distanceToTankPosition > maxDistance) + if (distanceToPosition > maxDistance) { - float dX = tankPosition.x - bot->GetPositionX(); - float dY = tankPosition.y - bot->GetPositionY(); - float dist = sqrt(dX * dX + dY * dY); - float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; - float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; - return MoveTo(bot->GetMapId(), moveX, moveY, tankPosition.z, false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveX = bot->GetPositionX() + (dX / distanceToPosition) * maxDistance; + float moveY = bot->GetPositionY() + (dY / distanceToPosition) * maxDistance; + return MoveTo(GRUULS_LAIR_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, true); } - - float orientation = atan2(maulgar->GetPositionY() - bot->GetPositionY(), - maulgar->GetPositionX() - bot->GetPositionX()); - bot->SetFacingTo(orientation); - } - else if (!bot->IsWithinMeleeRange(maulgar)) - { - return MoveTo(maulgar->GetMapId(), maulgar->GetPositionX(), maulgar->GetPositionY(), - maulgar->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); } return false; @@ -55,6 +47,8 @@ bool HighKingMaulgarMainTankAttackMaulgarAction::Execute(Event event) bool HighKingMaulgarFirstAssistTankAttackOlmAction::Execute(Event event) { Unit* olm = AI_VALUE2(Unit*, "find target", "olm the summoner"); + if (!olm) + return false; MarkTargetWithCircle(bot, olm); SetRtiTarget(botAI, "circle", olm); @@ -64,29 +58,22 @@ bool HighKingMaulgarFirstAssistTankAttackOlmAction::Execute(Event event) if (olm->GetVictim() == bot) { - const Location& tankPosition = GruulsLairLocations::OlmTankPosition; + const Position& position = OLM_TANK_POSITION; const float maxDistance = 3.0f; const float olmTankLeeway = 30.0f; - float distanceOlmToTankPosition = olm->GetExactDist2d(tankPosition.x, tankPosition.y); + float distanceOlmToPosition = olm->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - if (distanceOlmToTankPosition > olmTankLeeway) + if (distanceOlmToPosition > olmTankLeeway) { - float dX = tankPosition.x - bot->GetPositionX(); - float dY = tankPosition.y - bot->GetPositionY(); - float dist = sqrt(dX * dX + dY * dY); - float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; - float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; - return MoveTo(bot->GetMapId(), moveX, moveY, tankPosition.z, false, false, false, false, + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveX = bot->GetPositionX() + (dX / distanceOlmToPosition) * maxDistance; + float moveY = bot->GetPositionY() + (dY / distanceOlmToPosition) * maxDistance; + return MoveTo(GRUULS_LAIR_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } - else if (!bot->IsWithinMeleeRange(olm)) - { - return MoveTo(olm->GetMapId(), olm->GetPositionX(), olm->GetPositionY(), - olm->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); - } return false; } @@ -95,6 +82,8 @@ bool HighKingMaulgarFirstAssistTankAttackOlmAction::Execute(Event event) bool HighKingMaulgarSecondAssistTankAttackBlindeyeAction::Execute(Event event) { Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); + if (!blindeye) + return false; MarkTargetWithStar(bot, blindeye); SetRtiTarget(botAI, "star", blindeye); @@ -104,31 +93,20 @@ bool HighKingMaulgarSecondAssistTankAttackBlindeyeAction::Execute(Event event) if (blindeye->GetVictim() == bot) { - const Location& tankPosition = GruulsLairLocations::BlindeyeTankPosition; + const Position& position = BLINDEYE_TANK_POSITION; const float maxDistance = 3.0f; - float distanceToTankPosition = bot->GetExactDist2d(tankPosition.x, tankPosition.y); + float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - if (distanceToTankPosition > maxDistance) + if (distanceToPosition > maxDistance) { - float dX = tankPosition.x - bot->GetPositionX(); - float dY = tankPosition.y - bot->GetPositionY(); - float dist = sqrt(dX * dX + dY * dY); - float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; - float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; - return MoveTo(bot->GetMapId(), moveX, moveY, tankPosition.z, false, false, false, false, + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveX = bot->GetPositionX() + (dX / distanceToPosition) * maxDistance; + float moveY = bot->GetPositionY() + (dY / distanceToPosition) * maxDistance; + return MoveTo(GRUULS_LAIR_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } - - float orientation = atan2(blindeye->GetPositionY() - bot->GetPositionY(), - blindeye->GetPositionX() - bot->GetPositionX()); - bot->SetFacingTo(orientation); - } - else if (!bot->IsWithinMeleeRange(blindeye)) - { - return MoveTo(blindeye->GetMapId(), blindeye->GetPositionX(), blindeye->GetPositionY(), - blindeye->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); } return false; @@ -138,6 +116,8 @@ bool HighKingMaulgarSecondAssistTankAttackBlindeyeAction::Execute(Event event) bool HighKingMaulgarMageTankAttackKroshAction::Execute(Event event) { Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); + if (!krosh) + return false; MarkTargetWithTriangle(bot, krosh); SetRtiTarget(botAI, "triangle", krosh); @@ -149,25 +129,22 @@ bool HighKingMaulgarMageTankAttackKroshAction::Execute(Event event) return botAI->CastSpell("fire ward", bot); if (bot->GetTarget() != krosh->GetGUID()) - { - bot->SetSelection(krosh->GetGUID()); - return true; - } + return Attack(krosh); if (krosh->GetVictim() == bot) { - const Location& tankPosition = GruulsLairLocations::KroshTankPosition; - float distanceToKrosh = krosh->GetExactDist2d(tankPosition.x, tankPosition.y); + const Position& position = KROSH_TANK_POSITION; + float distanceToKrosh = krosh->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); const float minDistance = 16.0f; const float maxDistance = 29.0f; const float tankPositionLeeway = 1.0f; if (distanceToKrosh > minDistance && distanceToKrosh < maxDistance) { - if (!bot->IsWithinDist2d(tankPosition.x, tankPosition.y, tankPositionLeeway)) + if (!bot->IsWithinDist2d(position.GetPositionX(), position.GetPositionY(), tankPositionLeeway)) { - return MoveTo(bot->GetMapId(), tankPosition.x, tankPosition.y, tankPosition.z, false, - false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + return MoveTo(GRUULS_LAIR_MAP_ID, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } float orientation = atan2(krosh->GetPositionY() - bot->GetPositionY(), @@ -179,7 +156,7 @@ bool HighKingMaulgarMageTankAttackKroshAction::Execute(Event event) Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { - return MoveTo(krosh->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } @@ -192,20 +169,19 @@ bool HighKingMaulgarMageTankAttackKroshAction::Execute(Event event) bool HighKingMaulgarMoonkinTankAttackKigglerAction::Execute(Event event) { Unit* kiggler = AI_VALUE2(Unit*, "find target", "kiggler the crazed"); + if (!kiggler) + return false; MarkTargetWithDiamond(bot, kiggler); SetRtiTarget(botAI, "diamond", kiggler); if (bot->GetTarget() != kiggler->GetGUID()) - { - bot->SetSelection(kiggler->GetGUID()); - return true; - } + return Attack(kiggler); Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { - return MoveTo(kiggler->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } @@ -216,120 +192,105 @@ bool HighKingMaulgarAssignDPSPriorityAction::Execute(Event event) { // Target priority 1: Blindeye Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); - if (blindeye && blindeye->IsAlive()) + if (blindeye) { Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(blindeye->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } SetRtiTarget(botAI, "star", blindeye); if (bot->GetTarget() != blindeye->GetGUID()) - { - bot->SetSelection(blindeye->GetGUID()); return Attack(blindeye); - } return false; } // Target priority 2: Olm Unit* olm = AI_VALUE2(Unit*, "find target", "olm the summoner"); - if (olm && olm->IsAlive()) + if (olm) { Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(olm->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } SetRtiTarget(botAI, "circle", olm); if (bot->GetTarget() != olm->GetGUID()) - { - bot->SetSelection(olm->GetGUID()); return Attack(olm); - } return false; } // Target priority 3a: Krosh (ranged only) Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); - if (krosh && krosh->IsAlive() && botAI->IsRanged(bot)) + if (krosh && botAI->IsRanged(bot)) { Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(krosh->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } SetRtiTarget(botAI, "triangle", krosh); if (bot->GetTarget() != krosh->GetGUID()) - { - bot->SetSelection(krosh->GetGUID()); return Attack(krosh); - } return false; } // Target priority 3b: Kiggler Unit* kiggler = AI_VALUE2(Unit*, "find target", "kiggler the crazed"); - if (kiggler && kiggler->IsAlive()) + if (kiggler) { Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(kiggler->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } SetRtiTarget(botAI, "diamond", kiggler); if (bot->GetTarget() != kiggler->GetGUID()) - { - bot->SetSelection(kiggler->GetGUID()); return Attack(kiggler); - } return false; } // Target priority 4: Maulgar Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); - if (maulgar && maulgar->IsAlive()) + if (maulgar) { Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(maulgar->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } SetRtiTarget(botAI, "square", maulgar); if (bot->GetTarget() != maulgar->GetGUID()) - { - bot->SetSelection(maulgar->GetGUID()); return Attack(maulgar); - } } return false; @@ -338,22 +299,22 @@ bool HighKingMaulgarAssignDPSPriorityAction::Execute(Event event) // Avoid Whirlwind and Blast Wave and generally try to stay near the center of the room bool HighKingMaulgarHealerFindSafePositionAction::Execute(Event event) { - const Location& fightCenter = GruulsLairLocations::MaulgarRoomCenter; - const float maxDistanceFromFight = 50.0f; - float distToFight = bot->GetExactDist2d(fightCenter.x, fightCenter.y); + const Position& center = MAULGAR_ROOM_CENTER; + const float maxDistanceFromCenter = 50.0f; + float distToCenter = bot->GetExactDist2d(center.GetPositionX(), center.GetPositionY()); - if (distToFight > maxDistanceFromFight) + if (distToCenter > maxDistanceFromCenter) { - float angle = atan2(bot->GetPositionY() - fightCenter.y, bot->GetPositionX() - fightCenter.x); - float destX = fightCenter.x + 40.0f * cos(angle); - float destY = fightCenter.y + 40.0f * sin(angle); - float destZ = fightCenter.z; + float angle = atan2(bot->GetPositionY() - center.GetPositionY(), bot->GetPositionX() - center.GetPositionX()); + float destX = center.GetPositionX() + 40.0f * cos(angle); + float destY = center.GetPositionY() + 40.0f * sin(angle); + float destZ = center.GetPositionZ(); if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), destX, destY, destZ)) return false; - return MoveTo(bot->GetMapId(), destX, destY, destZ, false, false, false, false, + return MoveTo(GRUULS_LAIR_MAP_ID, destX, destY, destZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } @@ -362,7 +323,7 @@ bool HighKingMaulgarHealerFindSafePositionAction::Execute(Event event) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(bot->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } @@ -373,6 +334,8 @@ bool HighKingMaulgarHealerFindSafePositionAction::Execute(Event event) bool HighKingMaulgarRunAwayFromWhirlwindAction::Execute(Event event) { Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); + if (!maulgar) + return false; const float safeDistance = 10.0f; float distance = bot->GetExactDist2d(maulgar); @@ -395,7 +358,7 @@ bool HighKingMaulgarRunAwayFromWhirlwindAction::Execute(Event event) { bot->AttackStop(); bot->InterruptNonMeleeSpells(true); - return MoveTo(maulgar->GetMapId(), destX, destY, destZ, false, false, false, false, + return MoveTo(GRUULS_LAIR_MAP_ID, destX, destY, destZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } @@ -439,7 +402,7 @@ bool HighKingMaulgarBanishFelstalkerAction::Execute(Event event) if (warlockIndex >= 0 && warlockIndex < felStalkers.size()) { Unit* assignedFelStalker = felStalkers[warlockIndex]; - if (!assignedFelStalker->HasAura(SPELL_BANISH) && botAI->CanCastSpell(SPELL_BANISH, assignedFelStalker, true)) + if (!botAI->HasAura("banish", assignedFelStalker) && botAI->CanCastSpell("banish", assignedFelStalker)) return botAI->CastSpell("banish", assignedFelStalker); } @@ -528,40 +491,33 @@ bool HighKingMaulgarMisdirectOlmAndBlindeyeAction::Execute(Event event) // Gruul the Dragonkiller Actions // Position in center of the room -bool GruulTheDragonkillerMainTankPositionBossAction::Execute(Event event) +bool GruulTheDragonkillerTanksPositionBossAction::Execute(Event event) { Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); + if (!gruul) + return false; if (bot->GetVictim() != gruul) return Attack(gruul); if (gruul->GetVictim() == bot) { - const Location& tankPosition = GruulsLairLocations::GruulTankPosition; - const float maxDistance = 3.0f; + const Position& position = GRUUL_TANK_POSITION; + const float maxDistance = 5.0f; - float dX = tankPosition.x - bot->GetPositionX(); - float dY = tankPosition.y - bot->GetPositionY(); - float distanceToTankPosition = bot->GetExactDist2d(tankPosition.x, tankPosition.y); + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float distanceToTankPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); if (distanceToTankPosition > maxDistance) { float step = std::min(maxDistance, distanceToTankPosition); float moveX = bot->GetPositionX() + (dX / distanceToTankPosition) * maxDistance; float moveY = bot->GetPositionY() + (dY / distanceToTankPosition) * maxDistance; - const float moveZ = tankPosition.z; - return MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, false, + const float moveZ = position.GetPositionZ(); + return MoveTo(GRUULS_LAIR_MAP_ID, moveX, moveY, moveZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } - - float orientation = atan2(gruul->GetPositionY() - bot->GetPositionY(), - gruul->GetPositionX() - bot->GetPositionX()); - bot->SetFacingTo(orientation); - } - else if (!bot->IsWithinMeleeRange(gruul)) - { - return MoveTo(gruul->GetMapId(), gruul->GetPositionX(), gruul->GetPositionY(), gruul->GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } return false; @@ -579,16 +535,16 @@ bool GruulTheDragonkillerSpreadRangedAction::Execute(Event event) static std::unordered_map hasReachedInitialPosition; Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); - if (gruul && gruul->IsAlive() && gruul->GetHealth() == gruul->GetMaxHealth()) + if (gruul && gruul->GetHealth() == gruul->GetMaxHealth()) { - initialPositions.clear(); - hasReachedInitialPosition.clear(); + initialPositions.erase(bot->GetGUID()); + hasReachedInitialPosition.erase(bot->GetGUID()); } - const Location& tankPosition = GruulsLairLocations::GruulTankPosition; - const float centerX = tankPosition.x; - const float centerY = tankPosition.y; - float centerZ = bot->GetPositionZ(); + const Position& position = GRUUL_TANK_POSITION; + const float centerX = position.GetPositionX(); + const float centerY = position.GetPositionY(); + const float centerZ = position.GetPositionZ(); const float minRadius = 25.0f; const float maxRadius = 40.0f; @@ -642,7 +598,7 @@ bool GruulTheDragonkillerSpreadRangedAction::Execute(Event event) bot->GetPositionY(), bot->GetPositionZ(), destX, destY, destZ)) return false; - return MoveTo(bot->GetMapId(), destX, destY, destZ, false, false, false, false, + return MoveTo(GRUULS_LAIR_MAP_ID, destX, destY, destZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } diff --git a/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.h b/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.h index 6faf7ed3ecb..525e0ee6dff 100644 --- a/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.h +++ b/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.h @@ -85,10 +85,10 @@ class HighKingMaulgarMisdirectOlmAndBlindeyeAction : public AttackAction bool Execute(Event event) override; }; -class GruulTheDragonkillerMainTankPositionBossAction : public AttackAction +class GruulTheDragonkillerTanksPositionBossAction : public AttackAction { public: - GruulTheDragonkillerMainTankPositionBossAction(PlayerbotAI* botAI, std::string const name = "gruul the dragonkiller main tank position boss") : AttackAction(botAI, name) {}; + GruulTheDragonkillerTanksPositionBossAction(PlayerbotAI* botAI, std::string const name = "gruul the dragonkiller tanks position boss") : AttackAction(botAI, name) {}; bool Execute(Event event) override; }; diff --git a/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.cpp b/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.cpp index 5ca2de9327d..6604fc5250b 100644 --- a/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.cpp +++ b/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.cpp @@ -8,18 +8,11 @@ #include "HunterActions.h" #include "MageActions.h" #include "Playerbots.h" +#include "ReachTargetActions.h" #include "WarriorActions.h" using namespace GruulsLairHelpers; -static bool IsChargeAction(Action* action) -{ - return dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action); -} - float HighKingMaulgarDisableTankAssistMultiplier::GetValue(Action* action) { if (IsAnyOgreBossAlive(botAI) && dynamic_cast(action)) @@ -38,12 +31,10 @@ float HighKingMaulgarAvoidWhirlwindMultiplier::GetValue(Action* action) Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); if (maulgar && maulgar->HasAura(SPELL_WHIRLWIND) && - (!kiggler || !kiggler->IsAlive()) && - (!krosh || !krosh->IsAlive()) && - (!olm || !olm->IsAlive()) && - (!blindeye || !blindeye->IsAlive())) + !kiggler && !krosh && !olm && !blindeye) { - if (IsChargeAction(action) || (dynamic_cast(action) && + if (dynamic_cast(action) || + (dynamic_cast(action) && !dynamic_cast(action))) return 0.0f; } @@ -57,7 +48,8 @@ float HighKingMaulgarDisableArcaneShotOnKroshMultiplier::GetValue(Action* action Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); Unit* target = AI_VALUE(Unit*, "current target"); - if (krosh && target && target->GetGUID() == krosh->GetGUID() && dynamic_cast(action)) + if (krosh && target && target->GetGUID() == krosh->GetGUID() && + dynamic_cast(action)) return 0.0f; return 1.0f; @@ -101,8 +93,9 @@ float GruulTheDragonkillerGroundSlamMultiplier::GetValue(Action* action) if (bot->HasAura(SPELL_GROUND_SLAM_1) || bot->HasAura(SPELL_GROUND_SLAM_2)) { - if ((dynamic_cast(action) && !dynamic_cast(action)) || - IsChargeAction(action)) + if ((dynamic_cast(action) && + !dynamic_cast(action)) || + dynamic_cast(action)) return 0.0f; } diff --git a/src/Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h b/src/Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h index 809fadf034e..3850f58c6d7 100644 --- a/src/Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h +++ b/src/Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h @@ -22,7 +22,7 @@ class RaidGruulsLairActionContext : public NamedObjectContext creators["high king maulgar misdirect olm and blindeye"] = &RaidGruulsLairActionContext::high_king_maulgar_misdirect_olm_and_blindeye; // Gruul the Dragonkiller - creators["gruul the dragonkiller main tank position boss"] = &RaidGruulsLairActionContext::gruul_the_dragonkiller_main_tank_position_boss; + creators["gruul the dragonkiller tanks position boss"] = &RaidGruulsLairActionContext::gruul_the_dragonkiller_tanks_position_boss; creators["gruul the dragonkiller spread ranged"] = &RaidGruulsLairActionContext::gruul_the_dragonkiller_spread_ranged; creators["gruul the dragonkiller shatter spread"] = &RaidGruulsLairActionContext::gruul_the_dragonkiller_shatter_spread; } @@ -41,7 +41,7 @@ class RaidGruulsLairActionContext : public NamedObjectContext static Action* high_king_maulgar_misdirect_olm_and_blindeye(PlayerbotAI* botAI) { return new HighKingMaulgarMisdirectOlmAndBlindeyeAction(botAI); } // Gruul the Dragonkiller - static Action* gruul_the_dragonkiller_main_tank_position_boss(PlayerbotAI* botAI) { return new GruulTheDragonkillerMainTankPositionBossAction(botAI); } + static Action* gruul_the_dragonkiller_tanks_position_boss(PlayerbotAI* botAI) { return new GruulTheDragonkillerTanksPositionBossAction(botAI); } static Action* gruul_the_dragonkiller_spread_ranged(PlayerbotAI* botAI) { return new GruulTheDragonkillerSpreadRangedAction(botAI); } static Action* gruul_the_dragonkiller_shatter_spread(PlayerbotAI* botAI) { return new GruulTheDragonkillerShatterSpreadAction(botAI); } }; diff --git a/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h b/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h index fa8e76f58bc..d12b0ce4672 100644 --- a/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h +++ b/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h @@ -22,8 +22,8 @@ class RaidGruulsLairTriggerContext : public NamedObjectContext creators["high king maulgar pulling olm and blindeye"] = &RaidGruulsLairTriggerContext::high_king_maulgar_pulling_olm_and_blindeye; // Gruul the Dragonkiller - creators["gruul the dragonkiller boss engaged by main tank"] = &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_boss_engaged_by_main_tank; - creators["gruul the dragonkiller boss engaged by range"] = &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_boss_engaged_by_range; + creators["gruul the dragonkiller boss engaged by tanks"] = &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_boss_engaged_by_tanks; + creators["gruul the dragonkiller boss engaged by ranged"] = &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_boss_engaged_by_ranged; creators["gruul the dragonkiller incoming shatter"] = &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_incoming_shatter; } @@ -41,8 +41,8 @@ class RaidGruulsLairTriggerContext : public NamedObjectContext static Trigger* high_king_maulgar_pulling_olm_and_blindeye(PlayerbotAI* botAI) { return new HighKingMaulgarPullingOlmAndBlindeyeTrigger(botAI); } // Gruul the Dragonkiller - static Trigger* gruul_the_dragonkiller_boss_engaged_by_main_tank(PlayerbotAI* botAI) { return new GruulTheDragonkillerBossEngagedByMainTankTrigger(botAI); } - static Trigger* gruul_the_dragonkiller_boss_engaged_by_range(PlayerbotAI* botAI) { return new GruulTheDragonkillerBossEngagedByRangeTrigger(botAI); } + static Trigger* gruul_the_dragonkiller_boss_engaged_by_tanks(PlayerbotAI* botAI) { return new GruulTheDragonkillerBossEngagedByTanksTrigger(botAI); } + static Trigger* gruul_the_dragonkiller_boss_engaged_by_ranged(PlayerbotAI* botAI) { return new GruulTheDragonkillerBossEngagedByRangedTrigger(botAI); } static Trigger* gruul_the_dragonkiller_incoming_shatter(PlayerbotAI* botAI) { return new GruulTheDragonkillerIncomingShatterTrigger(botAI); } }; diff --git a/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.cpp b/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.cpp index 9ec264ea00a..249c8e8a804 100644 --- a/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.cpp +++ b/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.cpp @@ -35,10 +35,10 @@ void RaidGruulsLairStrategy::InitTriggers(std::vector& triggers) NextAction("high king maulgar misdirect olm and blindeye", ACTION_RAID + 2) })); // Gruul the Dragonkiller - triggers.push_back(new TriggerNode("gruul the dragonkiller boss engaged by main tank", { - NextAction("gruul the dragonkiller main tank position boss", ACTION_RAID + 1) })); + triggers.push_back(new TriggerNode("gruul the dragonkiller boss engaged by tanks", { + NextAction("gruul the dragonkiller tanks position boss", ACTION_RAID + 1) })); - triggers.push_back(new TriggerNode("gruul the dragonkiller boss engaged by range", { + triggers.push_back(new TriggerNode("gruul the dragonkiller boss engaged by ranged", { NextAction("gruul the dragonkiller spread ranged", ACTION_RAID + 1) })); triggers.push_back(new TriggerNode("gruul the dragonkiller incoming shatter", { diff --git a/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.cpp b/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.cpp index 35d9f9a1dac..4bc5efe99cb 100644 --- a/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.cpp +++ b/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.cpp @@ -10,35 +10,35 @@ bool HighKingMaulgarIsMainTankTrigger::IsActive() { Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); - return botAI->IsMainTank(bot) && maulgar && maulgar->IsAlive(); + return botAI->IsMainTank(bot) && maulgar; } bool HighKingMaulgarIsFirstAssistTankTrigger::IsActive() { Unit* olm = AI_VALUE2(Unit*, "find target", "olm the summoner"); - return botAI->IsAssistTankOfIndex(bot, 0) && olm && olm->IsAlive(); + return botAI->IsAssistTankOfIndex(bot, 0, false) && olm; } bool HighKingMaulgarIsSecondAssistTankTrigger::IsActive() { Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); - return botAI->IsAssistTankOfIndex(bot, 1) && blindeye && blindeye->IsAlive(); + return botAI->IsAssistTankOfIndex(bot, 1, false) && blindeye; } bool HighKingMaulgarIsMageTankTrigger::IsActive() { Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); - return IsKroshMageTank(botAI, bot) && krosh && krosh->IsAlive(); + return IsKroshMageTank(botAI, bot) && krosh; } bool HighKingMaulgarIsMoonkinTankTrigger::IsActive() { Unit* kiggler = AI_VALUE2(Unit*, "find target", "kiggler the crazed"); - return IsKigglerMoonkinTank(botAI, bot) && kiggler && kiggler->IsAlive(); + return IsKigglerMoonkinTank(botAI, bot) && kiggler; } bool HighKingMaulgarDeterminingKillOrderTrigger::IsActive() @@ -50,11 +50,11 @@ bool HighKingMaulgarDeterminingKillOrderTrigger::IsActive() Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); return (botAI->IsDps(bot) || botAI->IsTank(bot)) && - !(botAI->IsMainTank(bot) && maulgar && maulgar->IsAlive()) && - !(botAI->IsAssistTankOfIndex(bot, 0) && olm && olm->IsAlive()) && - !(botAI->IsAssistTankOfIndex(bot, 1) && blindeye && blindeye->IsAlive()) && - !(IsKroshMageTank(botAI, bot) && krosh && krosh->IsAlive()) && - !(IsKigglerMoonkinTank(botAI, bot) && kiggler && kiggler->IsAlive()); + !(botAI->IsMainTank(bot) && maulgar) && + !(botAI->IsAssistTankOfIndex(bot, 0, false) && olm) && + !(botAI->IsAssistTankOfIndex(bot, 1, false) && blindeye) && + !(IsKroshMageTank(botAI, bot) && krosh) && + !(IsKigglerMoonkinTank(botAI, bot) && kiggler); } bool HighKingMaulgarHealerInDangerTrigger::IsActive() @@ -66,7 +66,7 @@ bool HighKingMaulgarBossChannelingWhirlwindTrigger::IsActive() { Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); - return maulgar && maulgar->IsAlive() && maulgar->HasAura(SPELL_WHIRLWIND) && + return maulgar && maulgar->HasAura(SPELL_WHIRLWIND) && !botAI->IsMainTank(bot); } @@ -74,7 +74,7 @@ bool HighKingMaulgarWildFelstalkerSpawnedTrigger::IsActive() { Unit* felStalker = AI_VALUE2(Unit*, "find target", "wild fel stalker"); - return felStalker && felStalker->IsAlive() && bot->getClass() == CLASS_WARLOCK; + return felStalker && bot->getClass() == CLASS_WARLOCK; } bool HighKingMaulgarPullingOlmAndBlindeyeTrigger::IsActive() @@ -120,12 +120,12 @@ bool HighKingMaulgarPullingOlmAndBlindeyeTrigger::IsActive() switch (hunterIndex) { case 0: - return olm && olm->IsAlive() && olm->GetHealthPct() > 98.0f && - olmTank && olmTank->IsAlive() && botAI->CanCastSpell("misdirection", olmTank); + return olm && olm->GetHealthPct() > 98.0f && + olmTank && botAI->CanCastSpell("misdirection", olmTank); case 1: - return blindeye && blindeye->IsAlive() && blindeye->GetHealthPct() > 90.0f && - blindeyeTank && blindeyeTank->IsAlive() && botAI->CanCastSpell("misdirection", blindeyeTank); + return blindeye && blindeye->GetHealthPct() > 90.0f && + blindeyeTank && botAI->CanCastSpell("misdirection", blindeyeTank); default: break; @@ -136,25 +136,24 @@ bool HighKingMaulgarPullingOlmAndBlindeyeTrigger::IsActive() // Gruul the Dragonkiller Triggers -bool GruulTheDragonkillerBossEngagedByMainTankTrigger::IsActive() +bool GruulTheDragonkillerBossEngagedByTanksTrigger::IsActive() { Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); - return gruul && gruul->IsAlive() && botAI->IsMainTank(bot); + return gruul && botAI->IsTank(bot); } -bool GruulTheDragonkillerBossEngagedByRangeTrigger::IsActive() +bool GruulTheDragonkillerBossEngagedByRangedTrigger::IsActive() { Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); - return gruul && gruul->IsAlive() && botAI->IsRanged(bot); + return gruul && botAI->IsRanged(bot); } bool GruulTheDragonkillerIncomingShatterTrigger::IsActive() { Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); - return gruul && gruul->IsAlive() && - (bot->HasAura(SPELL_GROUND_SLAM_1) || - bot->HasAura(SPELL_GROUND_SLAM_2)); + return gruul && (bot->HasAura(SPELL_GROUND_SLAM_1) || + bot->HasAura(SPELL_GROUND_SLAM_2)); } diff --git a/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.h b/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.h index f3f32853613..583b8b75f7b 100644 --- a/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.h +++ b/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.h @@ -73,17 +73,17 @@ class HighKingMaulgarPullingOlmAndBlindeyeTrigger : public Trigger bool IsActive() override; }; -class GruulTheDragonkillerBossEngagedByMainTankTrigger : public Trigger +class GruulTheDragonkillerBossEngagedByTanksTrigger : public Trigger { public: - GruulTheDragonkillerBossEngagedByMainTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "gruul the dragonkiller boss engaged by main tank") {} + GruulTheDragonkillerBossEngagedByTanksTrigger(PlayerbotAI* botAI) : Trigger(botAI, "gruul the dragonkiller boss engaged by tanks") {} bool IsActive() override; }; -class GruulTheDragonkillerBossEngagedByRangeTrigger : public Trigger +class GruulTheDragonkillerBossEngagedByRangedTrigger : public Trigger { public: - GruulTheDragonkillerBossEngagedByRangeTrigger(PlayerbotAI* botAI) : Trigger(botAI, "gruul the dragonkiller boss engaged by range") {} + GruulTheDragonkillerBossEngagedByRangedTrigger(PlayerbotAI* botAI) : Trigger(botAI, "gruul the dragonkiller boss engaged by ranged") {} bool IsActive() override; }; diff --git a/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.cpp b/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.cpp index 0c8a23a19c8..27c703aecc9 100644 --- a/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.cpp +++ b/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.cpp @@ -6,19 +6,16 @@ namespace GruulsLairHelpers { - namespace GruulsLairLocations - { - // Olm does not chase properly due to the Core's caster movement issues - // Thus, the below "OlmTankPosition" is beyond the actual desired tanking location - // It is the spot to which the OlmTank runs to to pull Olm to a decent tanking location - // "MaulgarRoomCenter" is to keep healers in a centralized location - const Location MaulgarTankPosition = { 90.686f, 167.047f, -13.234f }; - const Location OlmTankPosition = { 87.485f, 234.942f, -3.635f }; - const Location BlindeyeTankPosition = { 99.681f, 213.989f, -10.345f }; - const Location KroshTankPosition = { 116.880f, 166.208f, -14.231f }; - const Location MaulgarRoomCenter = { 88.754f, 150.759f, -11.569f }; - const Location GruulTankPosition = { 241.238f, 365.025f, -4.220f }; - } + // Olm does not chase properly due to the Core's caster movement issues + // Thus, the below "OlmTankPosition" is beyond the actual desired tanking location + // It is the spot to which the OlmTank runs to to pull Olm to a decent tanking location + // "MaulgarRoomCenter" is to keep healers in a centralized location + const Position MAULGAR_TANK_POSITION = { 90.686f, 167.047f, -13.234f }; + const Position OLM_TANK_POSITION = { 87.485f, 234.942f, -3.635f }; + const Position BLINDEYE_TANK_POSITION = { 99.681f, 213.989f, -10.345f }; + const Position KROSH_TANK_POSITION = { 116.880f, 166.208f, -14.231f }; + const Position MAULGAR_ROOM_CENTER = { 88.754f, 150.759f, -11.569f }; + const Position GRUUL_TANK_POSITION = { 241.238f, 365.025f, -4.220f }; bool IsAnyOgreBossAlive(PlayerbotAI* botAI) { @@ -42,84 +39,43 @@ namespace GruulsLairHelpers return false; } - void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId) + bool IsKroshMageTank(PlayerbotAI* botAI, Player* bot) { Group* group = bot->GetGroup(); - if (!target || !group) - return; + if (!group) + return false; - ObjectGuid currentGuid = group->GetTargetIcon(iconId); - if (currentGuid != target->GetGUID()) + // (1) First loop: Return the first assistant Mage (real player or bot) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - group->SetTargetIcon(iconId, bot->GetGUID(), target->GetGUID()); - } - } - - void MarkTargetWithSquare(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::squareIndex); - } - - void MarkTargetWithStar(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::starIndex); - } - - void MarkTargetWithCircle(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::circleIndex); - } - - void MarkTargetWithDiamond(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::diamondIndex); - } - - void MarkTargetWithTriangle(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::triangleIndex); - } - - void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target) - { - if (!target) - return; - - std::string currentRti = botAI->GetAiObjectContext()->GetValue("rti")->Get(); - Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("rti target")->Get(); + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->getClass() != CLASS_MAGE) + continue; - if (currentRti != rtiName || currentTarget != target) - { - botAI->GetAiObjectContext()->GetValue("rti")->Set(rtiName); - botAI->GetAiObjectContext()->GetValue("rti target")->Set(target); + if (group->IsAssistant(member->GetGUID())) + return member == bot; } - } - - bool IsKroshMageTank(PlayerbotAI* botAI, Player* bot) - { - Group* group = bot->GetGroup(); - if (!group) - return false; + // (2) Fall back to bot Mage with highest HP Player* highestHpMage = nullptr; uint32 highestHp = 0; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member)) + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || + member->getClass() != CLASS_MAGE) continue; - if (member->getClass() == CLASS_MAGE) + uint32 hp = member->GetMaxHealth(); + if (!highestHpMage || hp > highestHp) { - uint32 hp = member->GetMaxHealth(); - if (!highestHpMage || hp > highestHp) - { - highestHpMage = member; - highestHp = hp; - } + highestHpMage = member; + highestHp = hp; } } + // (3) Return the found Mage tank, or nullptr if none found return highestHpMage == bot; } @@ -129,30 +85,37 @@ namespace GruulsLairHelpers if (!group) return false; + // (1) First loop: Return the first assistant Moonkin (real player or bot) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->getClass() != CLASS_DRUID) + continue; + + if (group->IsAssistant(member->GetGUID()) && + AiFactory::GetPlayerSpecTab(member) == DRUID_TAB_BALANCE) + return member == bot; + } + + // (2) Fall back to bot Moonkin with highest HP Player* highestHpMoonkin = nullptr; uint32 highestHp = 0; - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member)) + if (!member || !member->IsAlive() || member->getClass() != CLASS_DRUID || + !GET_PLAYERBOT_AI(member) || AiFactory::GetPlayerSpecTab(member) != DRUID_TAB_BALANCE) continue; - if (member->getClass() == CLASS_DRUID) + uint32 hp = member->GetMaxHealth(); + if (!highestHpMoonkin || hp > highestHp) { - int tab = AiFactory::GetPlayerSpecTab(member); - if (tab == DRUID_TAB_BALANCE) - { - uint32 hp = member->GetMaxHealth(); - if (!highestHpMoonkin || hp > highestHp) - { - highestHpMoonkin = member; - highestHp = hp; - } - } + highestHpMoonkin = member; + highestHp = hp; } } + // (3) Return the found Moonkin tank, or nullptr if none found return highestHpMoonkin == bot; } diff --git a/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.h b/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.h index c7becc83625..4615a9b7acd 100644 --- a/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.h +++ b/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.h @@ -2,23 +2,19 @@ #define RAID_GRUULSLAIRHELPERS_H #include "PlayerbotAI.h" -#include "RtiTargetValue.h" namespace GruulsLairHelpers { enum GruulsLairSpells { // High King Maulgar - SPELL_WHIRLWIND = 33238, + SPELL_WHIRLWIND = 33238, // Krosh Firehand - SPELL_SPELL_SHIELD = 33054, + SPELL_SPELL_SHIELD = 33054, // Hunter - SPELL_MISDIRECTION = 35079, - - // Warlock - SPELL_BANISH = 18647, // Rank 2 + SPELL_MISDIRECTION = 35079, // Gruul the Dragonkiller SPELL_GROUND_SLAM_1 = 33525, @@ -30,33 +26,20 @@ namespace GruulsLairHelpers NPC_WILD_FEL_STALKER = 18847, }; + constexpr uint32 GRUULS_LAIR_MAP_ID = 565; + bool IsAnyOgreBossAlive(PlayerbotAI* botAI); - void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId); - void MarkTargetWithSquare(Player* bot, Unit* target); - void MarkTargetWithStar(Player* bot, Unit* target); - void MarkTargetWithCircle(Player* bot, Unit* target); - void MarkTargetWithDiamond(Player* bot, Unit* target); - void MarkTargetWithTriangle(Player* bot, Unit* target); - void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target); bool IsKroshMageTank(PlayerbotAI* botAI, Player* bot); bool IsKigglerMoonkinTank(PlayerbotAI* botAI, Player* bot); bool IsPositionSafe(PlayerbotAI* botAI, Player* bot, Position pos); bool TryGetNewSafePosition(PlayerbotAI* botAI, Player* bot, Position& outPos); - struct Location - { - float x, y, z; - }; - - namespace GruulsLairLocations - { - extern const Location MaulgarTankPosition; - extern const Location OlmTankPosition; - extern const Location BlindeyeTankPosition; - extern const Location KroshTankPosition; - extern const Location MaulgarRoomCenter; - extern const Location GruulTankPosition; - } + extern const Position MAULGAR_TANK_POSITION; + extern const Position OLM_TANK_POSITION; + extern const Position BLINDEYE_TANK_POSITION; + extern const Position KROSH_TANK_POSITION; + extern const Position MAULGAR_ROOM_CENTER; + extern const Position GRUUL_TANK_POSITION; } #endif diff --git a/src/Ai/Raid/Icecrown/RaidIccScripts.h b/src/Ai/Raid/Icecrown/Util/RaidIccScripts.h similarity index 100% rename from src/Ai/Raid/Icecrown/RaidIccScripts.h rename to src/Ai/Raid/Icecrown/Util/RaidIccScripts.h diff --git a/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp b/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp index adf0eac488d..4d40a9eedb5 100644 --- a/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp +++ b/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp @@ -2,6 +2,7 @@ #include "RaidKarazhanHelpers.h" #include "Playerbots.h" #include "PlayerbotTextMgr.h" +#include "RaidBossHelpers.h" using namespace KarazhanHelpers; @@ -44,7 +45,7 @@ bool AttumenTheHuntsmanMarkTargetAction::Execute(Event event) Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); if (attumenMounted) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) MarkTargetWithStar(bot, attumenMounted); SetRtiTarget(botAI, "star", attumenMounted); @@ -57,7 +58,7 @@ bool AttumenTheHuntsmanMarkTargetAction::Execute(Event event) } else if (Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight")) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) MarkTargetWithStar(bot, midnight); if (!botAI->IsAssistTankOfIndex(bot, 0)) @@ -180,7 +181,7 @@ bool MoroesMarkTargetAction::Execute(Event event) if (target) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) MarkTargetWithSkull(bot, target); SetRtiTarget(botAI, "skull", target); @@ -405,7 +406,7 @@ bool TheCuratorMarkAstralFlareAction::Execute(Event event) if (!flare) return false; - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) MarkTargetWithSkull(bot, flare); SetRtiTarget(botAI, "skull", flare); @@ -469,11 +470,11 @@ bool TheCuratorSpreadRangedAction::Execute(Event event) // Prioritize (1) Demon Chains, (2) Kil'rek, (3) Illhoof bool TerestianIllhoofMarkTargetAction::Execute(Event event) { - Unit* demonChains = AI_VALUE2(Unit*, "find target", "demon chains"); - Unit* kilrek = AI_VALUE2(Unit*, "find target", "kil'rek"); + Unit* demonChains = GetFirstAliveUnitByEntry(botAI, NPC_DEMON_CHAINS); + Unit* kilrek = GetFirstAliveUnitByEntry(botAI, NPC_KILREK); Unit* illhoof = AI_VALUE2(Unit*, "find target", "terestian illhoof"); - Unit* target = GetFirstAliveUnit({demonChains, kilrek, illhoof}); + Unit* target = GetFirstAliveUnit({demonChains, kilrek, illhoof}); if (target) MarkTargetWithSkull(bot, target); @@ -1007,7 +1008,7 @@ bool NetherspiteManageTimersAndTrackersAction::Execute(Event event) if (netherspite->GetHealth() == netherspite->GetMaxHealth() && !netherspite->HasAura(SPELL_GREEN_BEAM_HEAL)) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) netherspiteDpsWaitTimer.insert_or_assign(instanceId, now); if (botAI->IsTank(bot) && !bot->HasAura(SPELL_RED_BEAM_DEBUFF)) @@ -1018,7 +1019,7 @@ bool NetherspiteManageTimersAndTrackersAction::Execute(Event event) } else if (netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) netherspiteDpsWaitTimer.erase(instanceId); if (botAI->IsTank(bot)) @@ -1029,7 +1030,7 @@ bool NetherspiteManageTimersAndTrackersAction::Execute(Event event) } else if (!netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) netherspiteDpsWaitTimer.try_emplace(instanceId, now); if (botAI->IsTank(bot) && bot->HasAura(SPELL_RED_BEAM_DEBUFF)) @@ -1458,7 +1459,7 @@ bool NightbaneManageTimersAndTrackersAction::Execute(Event event) if (botAI->IsRanged(bot)) nightbaneRangedStep.erase(botGuid); - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) nightbaneDpsWaitTimer.erase(instanceId); } // Erase flight phase timer and Rain of Bones tracker on ground phase and start DPS wait timer @@ -1466,7 +1467,7 @@ bool NightbaneManageTimersAndTrackersAction::Execute(Event event) { nightbaneRainOfBonesHit.erase(botGuid); - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) { nightbaneFlightPhaseStartTimer.erase(instanceId); nightbaneDpsWaitTimer.try_emplace(instanceId, now); @@ -1482,7 +1483,7 @@ bool NightbaneManageTimersAndTrackersAction::Execute(Event event) if (botAI->IsRanged(bot)) nightbaneRangedStep.erase(botGuid); - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) { nightbaneDpsWaitTimer.erase(instanceId); nightbaneFlightPhaseStartTimer.try_emplace(instanceId, now); diff --git a/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.cpp b/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.cpp index 117a17f38a2..06a3335c392 100644 --- a/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.cpp +++ b/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.cpp @@ -10,6 +10,7 @@ #include "MageActions.h" #include "Playerbots.h" #include "PriestActions.h" +#include "RaidBossHelpers.h" #include "ReachTargetActions.h" #include "RogueActions.h" #include "ShamanActions.h" @@ -242,6 +243,9 @@ float PrinceMalchezaarEnfeebleKeepDistanceMultiplier::GetValue(Action* action) if (bot->HasAura(SPELL_ENFEEBLE)) { + if (dynamic_cast(action)) + return 0.0f; + if (dynamic_cast(action) && !dynamic_cast(action)) return 0.0f; diff --git a/src/Ai/Raid/Karazhan/Trigger/RaidKarazhanTriggers.cpp b/src/Ai/Raid/Karazhan/Trigger/RaidKarazhanTriggers.cpp index 2fb7d5af0f0..3c43aa898ca 100644 --- a/src/Ai/Raid/Karazhan/Trigger/RaidKarazhanTriggers.cpp +++ b/src/Ai/Raid/Karazhan/Trigger/RaidKarazhanTriggers.cpp @@ -2,6 +2,7 @@ #include "RaidKarazhanHelpers.h" #include "RaidKarazhanActions.h" #include "Playerbots.h" +#include "RaidBossHelpers.h" using namespace KarazhanHelpers; @@ -40,7 +41,7 @@ bool AttumenTheHuntsmanAttumenIsMountedTrigger::IsActive() bool AttumenTheHuntsmanBossWipesAggroWhenMountingTrigger::IsActive() { - if (!IsInstanceTimerManager(botAI, bot)) + if (!IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) return false; Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"); @@ -110,7 +111,7 @@ bool BigBadWolfBossIsChasingLittleRedRidingHoodTrigger::IsActive() bool RomuloAndJulianneBothBossesRevivedTrigger::IsActive() { - if (!IsInstanceTimerManager(botAI, bot)) + if (!IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) return false; Unit* romulo = AI_VALUE2(Unit*, "find target", "romulo"); @@ -126,7 +127,7 @@ bool RomuloAndJulianneBothBossesRevivedTrigger::IsActive() bool WizardOfOzNeedTargetPriorityTrigger::IsActive() { - if (!IsInstanceTimerManager(botAI, bot)) + if (!IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) return false; Unit* dorothee = AI_VALUE2(Unit*, "find target", "dorothee"); @@ -178,7 +179,7 @@ bool TheCuratorBossAstralFlaresCastArcingSearTrigger::IsActive() bool TerestianIllhoofNeedTargetPriorityTrigger::IsActive() { - if (!IsInstanceTimerManager(botAI, bot)) + if (!IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) return false; Unit* illhoof = AI_VALUE2(Unit*, "find target", "terestian illhoof"); @@ -202,7 +203,7 @@ bool ShadeOfAranFlameWreathIsActiveTrigger::IsActive() // Exclusion of Banish is so the player may Banish elementals if they wish bool ShadeOfAranConjuredElementalsSummonedTrigger::IsActive() { - if (!IsInstanceTimerManager(botAI, bot)) + if (!IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) return false; Unit* elemental = AI_VALUE2(Unit*, "find target", "conjured elemental"); @@ -279,7 +280,7 @@ bool NetherspiteBossIsBanishedTrigger::IsActive() bool NetherspiteNeedToManageTimersAndTrackersTrigger::IsActive() { - if (!botAI->IsTank(bot) && !IsInstanceTimerManager(botAI, bot)) + if (!botAI->IsTank(bot) && !IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) return false; Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); diff --git a/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.cpp b/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.cpp index 821cc670199..ea989dcf31f 100644 --- a/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.cpp +++ b/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.cpp @@ -1,7 +1,6 @@ #include "RaidKarazhanHelpers.h" #include "RaidKarazhanActions.h" #include "Playerbots.h" -#include "RtiTargetValue.h" namespace KarazhanHelpers { @@ -52,75 +51,6 @@ namespace KarazhanHelpers const Position NIGHTBANE_FLIGHT_STACK_POSITION = { -11159.555f, -1893.526f, 91.473f }; // Broken Barrel const Position NIGHTBANE_RAIN_OF_BONES_POSITION = { -11165.233f, -1911.123f, 91.473f }; - void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId) - { - if (!target) - return; - - if (Group* group = bot->GetGroup()) - { - ObjectGuid currentGuid = group->GetTargetIcon(iconId); - if (currentGuid != target->GetGUID()) - group->SetTargetIcon(iconId, bot->GetGUID(), target->GetGUID()); - } - } - - void MarkTargetWithSkull(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::skullIndex); - } - - void MarkTargetWithSquare(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::squareIndex); - } - - void MarkTargetWithStar(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::starIndex); - } - - void MarkTargetWithCircle(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::circleIndex); - } - - void MarkTargetWithMoon(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::moonIndex); - } - - void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target) - { - if (!target) - return; - - std::string currentRti = botAI->GetAiObjectContext()->GetValue("rti")->Get(); - Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("rti target")->Get(); - - if (currentRti != rtiName || currentTarget != target) - { - botAI->GetAiObjectContext()->GetValue("rti")->Set(rtiName); - botAI->GetAiObjectContext()->GetValue("rti target")->Set(target); - } - } - - // Only one bot is needed to set/reset instance-wide timers - bool IsInstanceTimerManager(PlayerbotAI* botAI, Player* bot) - { - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsDps(member) && GET_PLAYERBOT_AI(member)) - return member == bot; - } - } - - return false; - } - Unit* GetFirstAliveUnit(const std::vector& units) { for (Unit* unit : units) @@ -132,44 +62,6 @@ namespace KarazhanHelpers return nullptr; } - Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry) - { - const GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); - for (auto const& npcGuid : npcs) - { - Unit* unit = botAI->GetUnit(npcGuid); - if (unit && unit->IsAlive() && unit->GetEntry() == entry) - return unit; - } - - return nullptr; - } - - Unit* GetNearestPlayerInRadius(Player* bot, float radius) - { - Unit* nearestPlayer = nullptr; - float nearestDistance = radius; - - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref != nullptr; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || member == bot) - continue; - - float distance = bot->GetExactDist2d(member); - if (distance < nearestDistance) - { - nearestDistance = distance; - nearestPlayer = member; - } - } - } - - return nearestPlayer; - } - bool IsFlameWreathActive(PlayerbotAI* botAI, Player* bot) { Unit* aran = botAI->GetAiObjectContext()->GetValue("find target", "shade of aran")->Get(); diff --git a/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h b/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h index 394693b2e4a..ad7e8c3d5fa 100644 --- a/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h +++ b/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h @@ -61,6 +61,11 @@ namespace KarazhanHelpers NPC_ATTUMEN_THE_HUNTSMAN = 15550, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED = 16152, + // Terestian Illhoof + NPC_TERESTIAN_ILLHOOF = 15688, + NPC_DEMON_CHAINS = 17248, + NPC_KILREK = 17229, + // Shade of Aran NPC_CONJURED_ELEMENTAL = 17167, @@ -74,8 +79,8 @@ namespace KarazhanHelpers NPC_NETHERSPITE_INFERNAL = 17646, }; - const uint32 KARAZHAN_MAP_ID = 532; - const float NIGHTBANE_FLIGHT_Z = 95.0f; + constexpr uint32 KARAZHAN_MAP_ID = 532; + constexpr float NIGHTBANE_FLIGHT_Z = 95.0f; // Attumen the Huntsman extern std::unordered_map attumenDpsWaitTimer; @@ -105,17 +110,7 @@ namespace KarazhanHelpers extern const Position NIGHTBANE_FLIGHT_STACK_POSITION; extern const Position NIGHTBANE_RAIN_OF_BONES_POSITION; - void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId); - void MarkTargetWithSkull(Player* bot, Unit* target); - void MarkTargetWithSquare(Player* bot, Unit* target); - void MarkTargetWithStar(Player* bot, Unit* target); - void MarkTargetWithCircle(Player* bot, Unit* target); - void MarkTargetWithMoon(Player* bot, Unit* target); - void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target); - bool IsInstanceTimerManager(PlayerbotAI* botAI, Player* bot); Unit* GetFirstAliveUnit(const std::vector& units); - Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry); - Unit* GetNearestPlayerInRadius(Player* bot, float radius); bool IsFlameWreathActive(PlayerbotAI* botAI, Player* bot); std::vector GetRedBlockers(PlayerbotAI* botAI, Player* bot); std::vector GetBlueBlockers(PlayerbotAI* botAI, Player* bot); diff --git a/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.cpp b/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.cpp index 69fc8624429..dab7efab97e 100644 --- a/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.cpp +++ b/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.cpp @@ -4,6 +4,7 @@ #include "ObjectAccessor.h" #include "ObjectGuid.h" #include "Playerbots.h" +#include "RaidBossHelpers.h" using namespace MagtheridonHelpers; @@ -14,46 +15,45 @@ bool MagtheridonMainTankAttackFirstThreeChannelersAction::Execute(Event event) return false; Creature* channelerSquare = GetChanneler(bot, SOUTH_CHANNELER); - if (channelerSquare && channelerSquare->IsAlive()) + if (channelerSquare) MarkTargetWithSquare(bot, channelerSquare); Creature* channelerStar = GetChanneler(bot, WEST_CHANNELER); - if (channelerStar && channelerStar->IsAlive()) + if (channelerStar) MarkTargetWithStar(bot, channelerStar); Creature* channelerCircle = GetChanneler(bot, EAST_CHANNELER); - if (channelerCircle && channelerCircle->IsAlive()) + if (channelerCircle) MarkTargetWithCircle(bot, channelerCircle); // After first three channelers are dead, wait for Magtheridon to activate - if ((!channelerSquare || !channelerSquare->IsAlive()) && - (!channelerStar || !channelerStar->IsAlive()) && - (!channelerCircle || !channelerCircle->IsAlive())) + if (!channelerSquare && !channelerStar && !channelerCircle) { - const Location& position = MagtheridonsLairLocations::WaitingForMagtheridonPosition; - if (!bot->IsWithinDist2d(position.x, position.y, 2.0f)) + const Position& position = WAITING_FOR_MAGTHERIDON_POSITION; + if (!bot->IsWithinDist2d(position.GetPositionX(), position.GetPositionY(), 2.0f)) { - return MoveTo(bot->GetMapId(), position.x, position.y, position.z, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } - bot->SetFacingTo(position.orientation); + bot->SetFacingTo(position.GetOrientation()); return true; } Creature* currentTarget = nullptr; std::string rtiName; - if (channelerSquare && channelerSquare->IsAlive()) + if (channelerSquare) { currentTarget = channelerSquare; rtiName = "square"; } - else if (channelerStar && channelerStar->IsAlive()) + else if (channelerStar) { currentTarget = channelerStar; rtiName = "star"; } - else if (channelerCircle && channelerCircle->IsAlive()) + else if (channelerCircle) { currentTarget = channelerCircle; rtiName = "circle"; @@ -70,7 +70,7 @@ bool MagtheridonMainTankAttackFirstThreeChannelersAction::Execute(Event event) bool MagtheridonFirstAssistTankAttackNWChannelerAction::Execute(Event event) { Creature* channelerDiamond = GetChanneler(bot, NORTHWEST_CHANNELER); - if (!channelerDiamond || !channelerDiamond->IsAlive()) + if (!channelerDiamond) return false; MarkTargetWithDiamond(bot, channelerDiamond); @@ -81,18 +81,18 @@ bool MagtheridonFirstAssistTankAttackNWChannelerAction::Execute(Event event) if (channelerDiamond->GetVictim() == bot) { - const Location& position = MagtheridonsLairLocations::NWChannelerTankPosition; + const Position& position = NW_CHANNELER_TANK_POSITION; const float maxDistance = 3.0f; + float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - if (bot->GetExactDist2d(position.x, position.y) > maxDistance) + if (distanceToPosition > maxDistance) { - float dX = position.x - bot->GetPositionX(); - float dY = position.y - bot->GetPositionY(); - float dist = sqrt(dX * dX + dY * dY); - float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; - float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveX = bot->GetPositionX() + (dX / distanceToPosition) * maxDistance; + float moveY = bot->GetPositionY() + (dY / distanceToPosition) * maxDistance; - return MoveTo(bot->GetMapId(), moveX, moveY, position.z, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } @@ -103,7 +103,7 @@ bool MagtheridonFirstAssistTankAttackNWChannelerAction::Execute(Event event) bool MagtheridonSecondAssistTankAttackNEChannelerAction::Execute(Event event) { Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); - if (!channelerTriangle || !channelerTriangle->IsAlive()) + if (!channelerTriangle) return false; MarkTargetWithTriangle(bot, channelerTriangle); @@ -114,18 +114,18 @@ bool MagtheridonSecondAssistTankAttackNEChannelerAction::Execute(Event event) if (channelerTriangle->GetVictim() == bot) { - const Location& position = MagtheridonsLairLocations::NEChannelerTankPosition; + const Position& position = NE_CHANNELER_TANK_POSITION; const float maxDistance = 3.0f; + float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - if (bot->GetExactDist2d(position.x, position.y) > maxDistance) + if (distanceToPosition > maxDistance) { - float dX = position.x - bot->GetPositionX(); - float dY = position.y - bot->GetPositionY(); - float dist = sqrt(dX * dX + dY * dY); - float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; - float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveX = bot->GetPositionX() + (dX / distanceToPosition) * maxDistance; + float moveY = bot->GetPositionY() + (dY / distanceToPosition) * maxDistance; - return MoveTo(bot->GetMapId(), moveX, moveY, position.z, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } @@ -175,7 +175,7 @@ bool MagtheridonMisdirectHellfireChannelers::Execute(Event event) switch (hunterIndex) { case 0: - if (mainTank && channelerStar && channelerStar->IsAlive() && + if (mainTank && channelerStar && channelerStar->GetVictim() != mainTank) { if (botAI->CanCastSpell("misdirection", mainTank)) @@ -190,7 +190,7 @@ bool MagtheridonMisdirectHellfireChannelers::Execute(Event event) break; case 1: - if (mainTank && channelerCircle && channelerCircle->IsAlive() && + if (mainTank && channelerCircle && channelerCircle->GetVictim() != mainTank) { if (botAI->CanCastSpell("misdirection", mainTank)) @@ -215,90 +215,69 @@ bool MagtheridonAssignDPSPriorityAction::Execute(Event event) { // Listed in order of priority Creature* channelerSquare = GetChanneler(bot, SOUTH_CHANNELER); - if (channelerSquare && channelerSquare->IsAlive()) + if (channelerSquare) { SetRtiTarget(botAI, "square", channelerSquare); if (bot->GetTarget() != channelerSquare->GetGUID()) - { - bot->SetSelection(channelerSquare->GetGUID()); return Attack(channelerSquare); - } return false; } Creature* channelerStar = GetChanneler(bot, WEST_CHANNELER); - if (channelerStar && channelerStar->IsAlive()) + if (channelerStar) { SetRtiTarget(botAI, "star", channelerStar); if (bot->GetTarget() != channelerStar->GetGUID()) - { - bot->SetSelection(channelerStar->GetGUID()); return Attack(channelerStar); - } return false; } Creature* channelerCircle = GetChanneler(bot, EAST_CHANNELER); - if (channelerCircle && channelerCircle->IsAlive()) + if (channelerCircle) { SetRtiTarget(botAI, "circle", channelerCircle); if (bot->GetTarget() != channelerCircle->GetGUID()) - { - bot->SetSelection(channelerCircle->GetGUID()); return Attack(channelerCircle); - } return false; } Creature* channelerDiamond = GetChanneler(bot, NORTHWEST_CHANNELER); - if (channelerDiamond && channelerDiamond->IsAlive()) + if (channelerDiamond) { SetRtiTarget(botAI, "diamond", channelerDiamond); if (bot->GetTarget() != channelerDiamond->GetGUID()) - { - bot->SetSelection(channelerDiamond->GetGUID()); return Attack(channelerDiamond); - } return false; } Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); - if (channelerTriangle && channelerTriangle->IsAlive()) + if (channelerTriangle) { SetRtiTarget(botAI, "triangle", channelerTriangle); if (bot->GetTarget() != channelerTriangle->GetGUID()) - { - bot->SetSelection(channelerTriangle->GetGUID()); return Attack(channelerTriangle); - } return false; } Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); if (magtheridon && !magtheridon->HasAura(SPELL_SHADOW_CAGE) && - (!channelerSquare || !channelerSquare->IsAlive()) && - (!channelerStar || !channelerStar->IsAlive()) && - (!channelerCircle || !channelerCircle->IsAlive()) && - (!channelerDiamond || !channelerDiamond->IsAlive()) && - (!channelerTriangle || !channelerTriangle->IsAlive())) + !channelerSquare && !channelerStar && !channelerCircle && + !channelerDiamond && !channelerTriangle) { SetRtiTarget(botAI, "cross", magtheridon); if (bot->GetTarget() != magtheridon->GetGUID()) - { - bot->SetSelection(magtheridon->GetGUID()); return Attack(magtheridon); - } } return false; @@ -343,15 +322,15 @@ bool MagtheridonWarlockCCBurningAbyssalAction::Execute(Event event) if (warlockIndex >= 0 && warlockIndex < abyssals.size()) { Unit* assignedAbyssal = abyssals[warlockIndex]; - if (!assignedAbyssal->HasAura(SPELL_BANISH) && botAI->CanCastSpell(SPELL_BANISH, assignedAbyssal, true)) + if (!botAI->HasAura("banish", assignedAbyssal) && botAI->CanCastSpell("banish", assignedAbyssal)) return botAI->CastSpell("banish", assignedAbyssal); } for (size_t i = warlocks.size(); i < abyssals.size(); ++i) { Unit* excessAbyssal = abyssals[i]; - if (!excessAbyssal->HasAura(SPELL_BANISH) && !excessAbyssal->HasAura(SPELL_FEAR) && - botAI->CanCastSpell(SPELL_FEAR, excessAbyssal, true)) + if (!botAI->HasAura("banish", excessAbyssal) && !botAI->HasAura("fear", excessAbyssal) && + botAI->CanCastSpell("fear", excessAbyssal)) return botAI->CastSpell("fear", excessAbyssal); } @@ -373,22 +352,20 @@ bool MagtheridonMainTankPositionBossAction::Execute(Event event) if (magtheridon->GetVictim() == bot) { - const Location& position = MagtheridonsLairLocations::MagtheridonTankPosition; + const Position& position = MAGTHERIDON_TANK_POSITION; const float maxDistance = 2.0f; + float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - if (bot->GetExactDist2d(position.x, position.y) > maxDistance) + if (distanceToPosition > maxDistance) { - float dX = position.x - bot->GetPositionX(); - float dY = position.y - bot->GetPositionY(); - float dist = sqrt(dX * dX + dY * dY); - float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; - float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveX = bot->GetPositionX() + (dX / distanceToPosition) * maxDistance; + float moveY = bot->GetPositionY() + (dY / distanceToPosition) * maxDistance; - return MoveTo(bot->GetMapId(), moveX, moveY, position.z, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, true); } - - bot->SetFacingTo(position.orientation); } return false; @@ -440,13 +417,13 @@ bool MagtheridonSpreadRangedAction::Execute(Event event) } bool isHealer = botAI->IsHeal(bot); - const Location& center = isHealer - ? MagtheridonsLairLocations::HealerSpreadPosition - : MagtheridonsLairLocations::RangedSpreadPosition; + const Position& center = isHealer + ? HEALER_SPREAD_POSITION + : RANGED_SPREAD_POSITION; float maxSpreadRadius = isHealer ? 15.0f : 20.0f; - float centerX = center.x; - float centerY = center.y; - float centerZ = bot->GetPositionZ(); + float centerX = center.GetPositionX(); + float centerY = center.GetPositionY(); + float centerZ = center.GetPositionZ(); const float radiusBuffer = 3.0f; if (!initialPositions.count(bot->GetGUID())) @@ -479,7 +456,7 @@ bool MagtheridonSpreadRangedAction::Execute(Event event) bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(bot->GetMapId(), destX, destY, destZ, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, destX, destY, destZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } hasReachedInitialPosition[bot->GetGUID()] = true; @@ -499,7 +476,7 @@ bool MagtheridonSpreadRangedAction::Execute(Event event) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(bot->GetMapId(), targetX, targetY, centerZ, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, targetX, targetY, centerZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } @@ -593,7 +570,7 @@ bool MagtheridonUseManticronCubeAction::HandleWaitingPhase(const CubeInfo& cubeI { bot->AttackStop(); bot->InterruptNonMeleeSpells(true); - return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, targetX, targetY, targetZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } @@ -603,7 +580,7 @@ bool MagtheridonUseManticronCubeAction::HandleWaitingPhase(const CubeInfo& cubeI float fallbackY = cubeInfo.y + sin(angle) * safeWaitDistance; float fallbackZ = bot->GetPositionZ(); - return MoveTo(bot->GetMapId(), fallbackX, fallbackY, fallbackZ, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, fallbackX, fallbackY, fallbackZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } @@ -638,7 +615,7 @@ bool MagtheridonUseManticronCubeAction::HandleCubeInteraction(const CubeInfo& cu bot->AttackStop(); bot->InterruptNonMeleeSpells(true); - return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, targetX, targetY, targetZ, false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); } @@ -663,14 +640,14 @@ bool MagtheridonManageTimersAndAssignmentsAction::Execute(Event event) magtheridon->FindCurrentSpellBySpellId(SPELL_BLAST_NOVA); bool lastBlastNova = lastBlastNovaState[instanceId]; - if (lastBlastNova && !blastNovaActive && IsInstanceTimerManager(botAI, bot)) + if (lastBlastNova && !blastNovaActive && IsMechanicTrackerBot(botAI, bot, MAGTHERIDON_MAP_ID, nullptr)) blastNovaTimer[instanceId] = now; lastBlastNovaState[instanceId] = blastNovaActive; if (!magtheridon->HasAura(SPELL_SHADOW_CAGE)) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, MAGTHERIDON_MAP_ID, nullptr)) { spreadWaitTimer.try_emplace(instanceId, now); blastNovaTimer.try_emplace(instanceId, now); @@ -679,11 +656,12 @@ bool MagtheridonManageTimersAndAssignmentsAction::Execute(Event event) } else { - MagtheridonSpreadRangedAction::initialPositions.clear(); - MagtheridonSpreadRangedAction::hasReachedInitialPosition.clear(); - botToCubeAssignment.clear(); + ObjectGuid guid = bot->GetGUID(); + MagtheridonSpreadRangedAction::initialPositions.erase(guid); + MagtheridonSpreadRangedAction::hasReachedInitialPosition.erase(guid); + botToCubeAssignment.erase(guid); - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, MAGTHERIDON_MAP_ID, nullptr)) { spreadWaitTimer.erase(instanceId); blastNovaTimer.erase(instanceId); diff --git a/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h b/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h index 6c4ed84c22f..d47d06459ab 100644 --- a/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h +++ b/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h @@ -6,8 +6,6 @@ #include "AttackAction.h" #include "MovementActions.h" -using namespace MagtheridonHelpers; - class MagtheridonMainTankAttackFirstThreeChannelersAction : public AttackAction { public: @@ -85,8 +83,8 @@ class MagtheridonUseManticronCubeAction : public MovementAction private: bool HandleCubeRelease(Unit* magtheridon, GameObject* cube); bool ShouldActivateCubeLogic(Unit* magtheridon); - bool HandleWaitingPhase(const CubeInfo& cubeInfo); - bool HandleCubeInteraction(const CubeInfo& cubeInfo, GameObject* cube); + bool HandleWaitingPhase(const MagtheridonHelpers::CubeInfo& cubeInfo); + bool HandleCubeInteraction(const MagtheridonHelpers::CubeInfo& cubeInfo, GameObject* cube); }; class MagtheridonManageTimersAndAssignmentsAction : public Action diff --git a/src/Ai/Raid/Magtheridon/Multiplier/RaidMagtheridonMultipliers.cpp b/src/Ai/Raid/Magtheridon/Multiplier/RaidMagtheridonMultipliers.cpp index 9580fd923a9..55aaf90e94f 100644 --- a/src/Ai/Raid/Magtheridon/Multiplier/RaidMagtheridonMultipliers.cpp +++ b/src/Ai/Raid/Magtheridon/Multiplier/RaidMagtheridonMultipliers.cpp @@ -8,6 +8,7 @@ #include "GenericSpellActions.h" #include "Playerbots.h" #include "WarlockActions.h" +#include "WipeAction.h" using namespace MagtheridonHelpers; @@ -24,10 +25,10 @@ float MagtheridonUseManticronCubeMultiplier::GetValue(Action* action) auto it = botToCubeAssignment.find(bot->GetGUID()); if (it != botToCubeAssignment.end()) { - if (dynamic_cast(action)) + if (dynamic_cast(action)) return 1.0f; - - return 0.0f; + else if (!dynamic_cast(action)) + return 0.0f; } } @@ -41,28 +42,31 @@ float MagtheridonWaitToAttackMultiplier::GetValue(Action* action) if (!magtheridon || magtheridon->HasAura(SPELL_SHADOW_CAGE)) return 1.0f; + if (botAI->IsMainTank(bot)) + return 1.0f; + const uint8 dpsWaitSeconds = 6; auto it = dpsWaitTimer.find(magtheridon->GetMap()->GetInstanceId()); if (it == dpsWaitTimer.end() || (time(nullptr) - it->second) < dpsWaitSeconds) { - if (!botAI->IsMainTank(bot) && (dynamic_cast(action) || - (!botAI->IsHeal(bot) && dynamic_cast(action)))) + if (dynamic_cast(action) || + (!botAI->IsHeal(bot) && dynamic_cast(action))) return 0.0f; } return 1.0f; } -// No tank assist for offtanks during the channeler phase -// So they don't try to pull channelers from each other or the main tank float MagtheridonDisableOffTankAssistMultiplier::GetValue(Action* action) { Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - Unit* channeler = AI_VALUE2(Unit*, "find target", "hellfire channeler"); if (!magtheridon) return 1.0f; + if (bot->GetVictim() == nullptr) + return 1.0f; + if ((botAI->IsAssistTankOfIndex(bot, 0) || botAI->IsAssistTankOfIndex(bot, 1)) && dynamic_cast(action)) return 0.0f; diff --git a/src/Ai/Raid/Magtheridon/Trigger/RaidMagtheridonTriggers.cpp b/src/Ai/Raid/Magtheridon/Trigger/RaidMagtheridonTriggers.cpp index 35442df6e0e..43aa3361f3f 100644 --- a/src/Ai/Raid/Magtheridon/Trigger/RaidMagtheridonTriggers.cpp +++ b/src/Ai/Raid/Magtheridon/Trigger/RaidMagtheridonTriggers.cpp @@ -18,7 +18,7 @@ bool MagtheridonNWChannelerEngagedByFirstAssistTankTrigger::IsActive() Creature* channelerDiamond = GetChanneler(bot, NORTHWEST_CHANNELER); return magtheridon && botAI->IsAssistTankOfIndex(bot, 0) && - channelerDiamond && channelerDiamond->IsAlive(); + channelerDiamond; } bool MagtheridonNEChannelerEngagedBySecondAssistTankTrigger::IsActive() @@ -27,7 +27,7 @@ bool MagtheridonNEChannelerEngagedBySecondAssistTankTrigger::IsActive() Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); return magtheridon && botAI->IsAssistTankOfIndex(bot, 1) && - channelerTriangle && channelerTriangle->IsAlive(); + channelerTriangle; } bool MagtheridonPullingWestAndEastChannelersTrigger::IsActive() @@ -38,8 +38,7 @@ bool MagtheridonPullingWestAndEastChannelersTrigger::IsActive() Creature* channelerCircle = GetChanneler(bot, EAST_CHANNELER); return magtheridon && bot->getClass() == CLASS_HUNTER && - ((channelerStar && channelerStar->IsAlive()) || - (channelerCircle && channelerCircle->IsAlive())); + (channelerStar || channelerCircle); } bool MagtheridonDeterminingKillOrderTrigger::IsActive() @@ -51,12 +50,11 @@ bool MagtheridonDeterminingKillOrderTrigger::IsActive() Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); if (!magtheridon || botAI->IsHeal(bot) || botAI->IsMainTank(bot) || - (botAI->IsAssistTankOfIndex(bot, 0) && channelerDiamond && channelerDiamond->IsAlive()) || - (botAI->IsAssistTankOfIndex(bot, 1) && channelerTriangle && channelerTriangle->IsAlive())) + (botAI->IsAssistTankOfIndex(bot, 0) && channelerDiamond) || + (botAI->IsAssistTankOfIndex(bot, 1) && channelerTriangle)) return false; - return (channeler && channeler->IsAlive()) || (magtheridon && - !magtheridon->HasAura(SPELL_SHADOW_CAGE)); + return channeler || (magtheridon && !magtheridon->HasAura(SPELL_SHADOW_CAGE)); } bool MagtheridonBurningAbyssalSpawnedTrigger::IsActive() @@ -84,10 +82,8 @@ bool MagtheridonBossEngagedByMainTankTrigger::IsActive() bool MagtheridonBossEngagedByRangedTrigger::IsActive() { Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - Unit* channeler = AI_VALUE2(Unit*, "find target", "hellfire channeler"); - return magtheridon && botAI->IsRanged(bot) && - !(channeler && channeler->IsAlive()); + return magtheridon && !magtheridon->HasAura(SPELL_SHADOW_CAGE) && botAI->IsRanged(bot); } bool MagtheridonIncomingBlastNovaTrigger::IsActive() @@ -122,7 +118,5 @@ bool MagtheridonIncomingBlastNovaTrigger::IsActive() bool MagtheridonNeedToManageTimersAndAssignmentsTrigger::IsActive() { - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - - return magtheridon; + return AI_VALUE2(Unit*, "find target", "magtheridon"); } diff --git a/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.cpp b/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.cpp index dc88d2a1927..a56d4b85bc3 100644 --- a/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.cpp +++ b/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.cpp @@ -1,22 +1,18 @@ #include "RaidMagtheridonHelpers.h" #include "Creature.h" #include "GameObject.h" -#include "GroupReference.h" #include "Map.h" #include "ObjectGuid.h" #include "Playerbots.h" namespace MagtheridonHelpers { - namespace MagtheridonsLairLocations - { - const Location WaitingForMagtheridonPosition = { 1.359f, 2.048f, -0.406f, 3.135f }; - const Location MagtheridonTankPosition = { 22.827f, 2.105f, -0.406f, 3.135f }; - const Location NWChannelerTankPosition = { -11.764f, 30.818f, -0.411f, 0.0f }; - const Location NEChannelerTankPosition = { -12.490f, -26.211f, -0.411f, 0.0f }; - const Location RangedSpreadPosition = { -14.890f, 1.995f, -0.406f, 0.0f }; - const Location HealerSpreadPosition = { -2.265f, 1.874f, -0.404f, 0.0f }; - } + const Position WAITING_FOR_MAGTHERIDON_POSITION = { 1.359f, 2.048f, -0.406f, 3.135f }; + const Position MAGTHERIDON_TANK_POSITION = { 22.827f, 2.105f, -0.406f, 3.135f }; + const Position NW_CHANNELER_TANK_POSITION = { -11.764f, 30.818f, -0.411f, 0.0f }; + const Position NE_CHANNELER_TANK_POSITION = { -12.490f, -26.211f, -0.411f, 0.0f }; + const Position RANGED_SPREAD_POSITION = { -14.890f, 1.995f, -0.406f, 0.0f }; + const Position HEALER_SPREAD_POSITION = { -2.265f, 1.874f, -0.404f, 0.0f }; // Identify channelers by their database GUIDs Creature* GetChanneler(Player* bot, uint32 dbGuid) @@ -29,63 +25,11 @@ namespace MagtheridonHelpers if (it == map->GetCreatureBySpawnIdStore().end()) return nullptr; - return it->second; - } - - void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId) - { - Group* group = bot->GetGroup(); - if (!target || !group) - return; - - ObjectGuid currentGuid = group->GetTargetIcon(iconId); - if (currentGuid != target->GetGUID()) - group->SetTargetIcon(iconId, bot->GetGUID(), target->GetGUID()); - } - - void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target) - { - if (!target) - return; - - std::string currentRti = botAI->GetAiObjectContext()->GetValue("rti")->Get(); - Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("rti target")->Get(); - - if (currentRti != rtiName || currentTarget != target) - { - botAI->GetAiObjectContext()->GetValue("rti")->Set(rtiName); - botAI->GetAiObjectContext()->GetValue("rti target")->Set(target); - } - } - - void MarkTargetWithSquare(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::squareIndex); - } - - void MarkTargetWithStar(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::starIndex); - } - - void MarkTargetWithCircle(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::circleIndex); - } - - void MarkTargetWithDiamond(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::diamondIndex); - } - - void MarkTargetWithTriangle(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::triangleIndex); - } + Creature* channeler = it->second; + if (!channeler->IsAlive()) + return nullptr; - void MarkTargetWithCross(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::crossIndex); + return channeler; } const std::vector MANTICRON_CUBE_DB_GUIDS = { 43157, 43158, 43159, 43160, 43161 }; @@ -208,19 +152,4 @@ namespace MagtheridonHelpers return true; } - - bool IsInstanceTimerManager(PlayerbotAI* botAI, Player* bot) - { - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsDps(member) && GET_PLAYERBOT_AI(member)) - return member == bot; - } - } - - return true; - } } diff --git a/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.h b/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.h index f9f514de3ed..1335aff4655 100644 --- a/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.h +++ b/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.h @@ -8,7 +8,6 @@ #include "Group.h" #include "ObjectGuid.h" #include "PlayerbotAI.h" -#include "RtiTargetValue.h" namespace MagtheridonHelpers { @@ -19,10 +18,6 @@ namespace MagtheridonHelpers SPELL_BLAST_NOVA = 30616, SPELL_SHADOW_GRASP = 30410, - // Warlock - SPELL_BANISH = 18647, - SPELL_FEAR = 6215, - // Hunter SPELL_MISDIRECTION = 35079, }; @@ -38,6 +33,7 @@ namespace MagtheridonHelpers GO_BLAZE = 181832, }; + constexpr uint32 MAGTHERIDON_MAP_ID = 544; constexpr uint32 SOUTH_CHANNELER = 90978; constexpr uint32 WEST_CHANNELER = 90979; constexpr uint32 NORTHWEST_CHANNELER = 90980; @@ -45,31 +41,14 @@ namespace MagtheridonHelpers constexpr uint32 NORTHEAST_CHANNELER = 90981; Creature* GetChanneler(Player* bot, uint32 dbGuid); - void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId); - void MarkTargetWithSquare(Player* bot, Unit* target); - void MarkTargetWithStar(Player* bot, Unit* target); - void MarkTargetWithCircle(Player* bot, Unit* target); - void MarkTargetWithDiamond(Player* bot, Unit* target); - void MarkTargetWithTriangle(Player* bot, Unit* target); - void MarkTargetWithCross(Player* bot, Unit* target); - void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target); bool IsSafeFromMagtheridonHazards(PlayerbotAI* botAI, Player* bot, float x, float y, float z); - bool IsInstanceTimerManager(PlayerbotAI* botAI, Player* bot); - struct Location - { - float x, y, z, orientation; - }; - - namespace MagtheridonsLairLocations - { - extern const Location WaitingForMagtheridonPosition; - extern const Location MagtheridonTankPosition; - extern const Location NWChannelerTankPosition; - extern const Location NEChannelerTankPosition; - extern const Location RangedSpreadPosition; - extern const Location HealerSpreadPosition; - } + extern const Position WAITING_FOR_MAGTHERIDON_POSITION; + extern const Position MAGTHERIDON_TANK_POSITION; + extern const Position NW_CHANNELER_TANK_POSITION; + extern const Position NE_CHANNELER_TANK_POSITION; + extern const Position RANGED_SPREAD_POSITION; + extern const Position HEALER_SPREAD_POSITION; struct CubeInfo { diff --git a/src/Ai/Raid/MoltenCore/RaidMcActionContext.h b/src/Ai/Raid/MoltenCore/RaidMcActionContext.h index 79a4a95a856..aaccb80d337 100644 --- a/src/Ai/Raid/MoltenCore/RaidMcActionContext.h +++ b/src/Ai/Raid/MoltenCore/RaidMcActionContext.h @@ -2,6 +2,7 @@ #define _PLAYERBOT_RAIDMCACTIONCONTEXT_H #include "Action.h" +#include "BossAuraActions.h" #include "NamedObjectContext.h" #include "RaidMcActions.h" diff --git a/src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h b/src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h index b74958919cd..a62d851dc0d 100644 --- a/src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h +++ b/src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h @@ -2,6 +2,7 @@ #define _PLAYERBOT_RAIDMCTRIGGERCONTEXT_H #include "AiObjectContext.h" +#include "BossAuraTriggers.h" #include "NamedObjectContext.h" #include "RaidMcTriggers.h" diff --git a/src/Ai/Raid/MoltenCore/RaidMcHelpers.h b/src/Ai/Raid/MoltenCore/Util/RaidMcHelpers.h similarity index 100% rename from src/Ai/Raid/MoltenCore/RaidMcHelpers.h rename to src/Ai/Raid/MoltenCore/Util/RaidMcHelpers.h diff --git a/src/Ai/Raid/RaidBossHelpers.cpp b/src/Ai/Raid/RaidBossHelpers.cpp new file mode 100644 index 00000000000..bcb48294f89 --- /dev/null +++ b/src/Ai/Raid/RaidBossHelpers.cpp @@ -0,0 +1,142 @@ +#include "RaidBossHelpers.h" +#include "Playerbots.h" +#include "RtiTargetValue.h" + +// Functions to mark targets with raid target icons +// Note that these functions do not allow the player to change the icon during the encounter +void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId) +{ + if (!target) + return; + + if (Group* group = bot->GetGroup()) + { + ObjectGuid currentGuid = group->GetTargetIcon(iconId); + if (currentGuid != target->GetGUID()) + group->SetTargetIcon(iconId, bot->GetGUID(), target->GetGUID()); + } +} + +void MarkTargetWithSkull(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::skullIndex); +} + +void MarkTargetWithSquare(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::squareIndex); +} + +void MarkTargetWithStar(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::starIndex); +} + +void MarkTargetWithCircle(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::circleIndex); +} + +void MarkTargetWithDiamond(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::diamondIndex); +} + +void MarkTargetWithTriangle(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::triangleIndex); +} + +void MarkTargetWithCross(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::crossIndex); +} + +void MarkTargetWithMoon(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::moonIndex); +} + +// For bots to set their raid target icon to the specified icon on the specified target +void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target) +{ + if (!target) + return; + + std::string currentRti = botAI->GetAiObjectContext()->GetValue("rti")->Get(); + Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("rti target")->Get(); + + if (currentRti != rtiName || currentTarget != target) + { + botAI->GetAiObjectContext()->GetValue("rti")->Set(rtiName); + botAI->GetAiObjectContext()->GetValue("rti target")->Set(target); + } +} + +// Return the first alive DPS bot in the specified instance map, excluding any specified bot +// Intended for purposes of storing and erasing timers and trackers in associative containers +bool IsMechanicTrackerBot(PlayerbotAI* botAI, Player* bot, uint32 mapId, Player* exclude) +{ + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->GetMapId() != mapId || + !GET_PLAYERBOT_AI(member) || !botAI->IsDps(member)) + continue; + + if (member != exclude) + return member == bot; + } + } + + return false; +} + +// Return the first matching alive unit from a cell search of nearby npcs +// More responsive than "find target," but performance cost is much higher +// Re: using the third parameter (false by default), some units are never considered +// to be in combat (e.g., totems) +Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry, bool requireInCombat) +{ + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); + for (auto const& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->IsAlive() && unit->GetEntry() == entry) + { + if (!requireInCombat || unit->IsInCombat()) + return unit; + } + } + + return nullptr; +} + +// Return the nearest alive player (human or bot) within the specified radius +Unit* GetNearestPlayerInRadius(Player* bot, float radius) +{ + Unit* nearestPlayer = nullptr; + float nearestDistance = radius; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref != nullptr; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; + + float distance = bot->GetExactDist2d(member); + if (distance < nearestDistance) + { + nearestDistance = distance; + nearestPlayer = member; + } + } + } + + return nearestPlayer; +} diff --git a/src/Ai/Raid/RaidBossHelpers.h b/src/Ai/Raid/RaidBossHelpers.h new file mode 100644 index 00000000000..15c60353e50 --- /dev/null +++ b/src/Ai/Raid/RaidBossHelpers.h @@ -0,0 +1,21 @@ +#ifndef _PLAYERBOT_RAIDBOSSHELPERS_H_ +#define _PLAYERBOT_RAIDBOSSHELPERS_H_ + +#include "AiObject.h" +#include "Unit.h" + +void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId); +void MarkTargetWithSkull(Player* bot, Unit* target); +void MarkTargetWithSquare(Player* bot, Unit* target); +void MarkTargetWithStar(Player* bot, Unit* target); +void MarkTargetWithCircle(Player* bot, Unit* target); +void MarkTargetWithDiamond(Player* bot, Unit* target); +void MarkTargetWithTriangle(Player* bot, Unit* target); +void MarkTargetWithCross(Player* bot, Unit* target); +void MarkTargetWithMoon(Player* bot, Unit* target); +void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target); +bool IsMechanicTrackerBot(PlayerbotAI* botAI, Player* bot, uint32 mapId, Player* exclude = nullptr); +Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry, bool requireInCombat = false); +Unit* GetNearestPlayerInRadius(Player* bot, float radius); + +#endif diff --git a/src/Ai/Raid/RaidStrategyContext.h b/src/Ai/Raid/RaidStrategyContext.h index 4f7a63c7adb..3c7971fb6f4 100644 --- a/src/Ai/Raid/RaidStrategyContext.h +++ b/src/Ai/Raid/RaidStrategyContext.h @@ -29,7 +29,7 @@ class RaidStrategyContext : public NamedObjectContext creators["wotlk-os"] = &RaidStrategyContext::wotlk_os; creators["wotlk-eoe"] = &RaidStrategyContext::wotlk_eoe; creators["voa"] = &RaidStrategyContext::voa; - creators["uld"] = &RaidStrategyContext::uld; + creators["ulduar"] = &RaidStrategyContext::ulduar; creators["onyxia"] = &RaidStrategyContext::onyxia; creators["icc"] = &RaidStrategyContext::icc; } @@ -45,7 +45,7 @@ class RaidStrategyContext : public NamedObjectContext static Strategy* wotlk_eoe(PlayerbotAI* botAI) { return new RaidEoEStrategy(botAI); } static Strategy* voa(PlayerbotAI* botAI) { return new RaidVoAStrategy(botAI); } static Strategy* onyxia(PlayerbotAI* botAI) { return new RaidOnyxiaStrategy(botAI); } - static Strategy* uld(PlayerbotAI* botAI) { return new RaidUlduarStrategy(botAI); } + static Strategy* ulduar(PlayerbotAI* botAI) { return new RaidUlduarStrategy(botAI); } static Strategy* icc(PlayerbotAI* botAI) { return new RaidIccStrategy(botAI); } }; diff --git a/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp b/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp index b20425d224e..f7eca38db1e 100644 --- a/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp +++ b/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp @@ -11,7 +11,6 @@ #include "GameObject.h" #include "Group.h" #include "LastMovementValue.h" -#include "ObjectDefines.h" #include "ObjectGuid.h" #include "PlayerbotAI.h" #include "PlayerbotAIConfig.h" @@ -19,11 +18,9 @@ #include "Position.h" #include "RaidUlduarBossHelper.h" #include "RaidUlduarScripts.h" -#include "RaidUlduarStrategy.h" #include "RtiValue.h" #include "ScriptedCreature.h" #include "ServerFacade.h" -#include "SharedDefines.h" #include "Unit.h" #include "Vehicle.h" #include diff --git a/src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.cpp b/src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.cpp deleted file mode 100644 index 0a51ca407b7..00000000000 --- a/src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.cpp +++ /dev/null @@ -1,28 +0,0 @@ -#include "RaidUlduarMultipliers.h" - -#include "ChooseTargetActions.h" -#include "DKActions.h" -#include "DruidActions.h" -#include "DruidBearActions.h" -#include "FollowActions.h" -#include "GenericActions.h" -#include "GenericSpellActions.h" -#include "HunterActions.h" -#include "MageActions.h" -#include "MovementActions.h" -#include "PaladinActions.h" -#include "PriestActions.h" -#include "RaidUlduarActions.h" -#include "ReachTargetActions.h" -#include "RogueActions.h" -#include "ScriptedCreature.h" -#include "ShamanActions.h" -#include "UseMeetingStoneAction.h" -#include "WarriorActions.h" - -float FlameLeviathanMultiplier::GetValue(Action* action) -{ - // if (dynamic_cast(action)) - // return 0.0f; - return 1.0f; -} diff --git a/src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.h b/src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.h deleted file mode 100644 index 6c9a468fcd6..00000000000 --- a/src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.h +++ /dev/null @@ -1,17 +0,0 @@ - -#ifndef _PLAYERRBOT_RAIDULDUARMULTIPLIERS_H_ -#define _PLAYERRBOT_RAIDULDUARMULTIPLIERS_H_ - -#include "Multiplier.h" -#include "Ai/Raid/Ulduar/RaidUlduarBossHelper.h" - -class FlameLeviathanMultiplier : public Multiplier -{ -public: - FlameLeviathanMultiplier(PlayerbotAI* ai) : Multiplier(ai, "flame leviathan") {} - -public: - virtual float GetValue(Action* action); -}; - -#endif diff --git a/src/Ai/Raid/Ulduar/RaidUlduarBossHelper.h b/src/Ai/Raid/Ulduar/RaidUlduarBossHelper.h deleted file mode 100644 index 592fbc80eaa..00000000000 --- a/src/Ai/Raid/Ulduar/RaidUlduarBossHelper.h +++ /dev/null @@ -1,169 +0,0 @@ -#ifndef _PLAYERBOT_RAIDULDUARBOSSHELPER_H -#define _PLAYERBOT_RAIDULDUARBOSSHELPER_H - -#include -#include -#include -#include -#include -#include - -#include "AiObject.h" -#include "AiObjectContext.h" -#include "EventMap.h" -#include "Log.h" -#include "ObjectGuid.h" -#include "Player.h" -#include "PlayerbotAI.h" -#include "Playerbots.h" -#include "ScriptedCreature.h" -#include "SharedDefines.h" - -const uint32 ULDUAR_MAP_ID = 603; - -class RazorscaleBossHelper : public AiObject -{ -public: - // Enums and constants specific to Razorscale - enum RazorscaleUnits : uint32 - { - UNIT_RAZORSCALE = 33186, - UNIT_DARK_RUNE_SENTINEL = 33846, - UNIT_DARK_RUNE_WATCHER = 33453, - UNIT_DARK_RUNE_GUARDIAN = 33388, - UNIT_DEVOURING_FLAME = 34188, - }; - - enum RazorscaleGameObjects : uint32 - { - GO_RAZORSCALE_HARPOON_1 = 194519, - GO_RAZORSCALE_HARPOON_2 = 194541, - GO_RAZORSCALE_HARPOON_3 = 194542, - GO_RAZORSCALE_HARPOON_4 = 194543, - }; - - enum RazorscaleSpells : uint32 - { - SPELL_CHAIN_1 = 49679, - SPELL_CHAIN_2 = 49682, - SPELL_CHAIN_3 = 49683, - SPELL_CHAIN_4 = 49684, - SPELL_SENTINEL_WHIRLWIND = 63806, - SPELL_STUN_AURA = 62794, - SPELL_FUSEARMOR = 64771 - }; - - static constexpr uint32 FUSEARMOR_THRESHOLD = 2; - - // Constants for arena parameters - static constexpr float RAZORSCALE_FLYING_Z_THRESHOLD = 440.0f; - static constexpr float RAZORSCALE_ARENA_CENTER_X = 587.54f; - static constexpr float RAZORSCALE_ARENA_CENTER_Y = -175.04f; - static constexpr float RAZORSCALE_ARENA_RADIUS = 30.0f; - - // Harpoon cooldown (seconds) - static constexpr time_t HARPOON_COOLDOWN_DURATION = 5; - - // Structure for harpoon data - struct HarpoonData - { - uint32 gameObjectEntry; - uint32 chainSpellId; - }; - - explicit RazorscaleBossHelper(PlayerbotAI* botAI) - : AiObject(botAI), _boss(nullptr) {} - - bool UpdateBossAI(); - Unit* GetBoss() const; - - bool IsGroundPhase() const; - bool IsFlyingPhase() const; - - bool IsHarpoonFired(uint32 chainSpellId) const; - static bool IsHarpoonReady(GameObject* harpoonGO); - static void SetHarpoonOnCooldown(GameObject* harpoonGO); - GameObject* FindNearestHarpoon(float x, float y, float z) const; - - static const std::vector& GetHarpoonData(); - - void AssignRolesBasedOnHealth(); - bool AreRolesAssigned() const; - bool CanSwapRoles() const; - -private: - Unit* _boss; - - // A map to track the last role swap *per bot* by their GUID - static std::unordered_map _lastRoleSwapTime; - - // The cooldown that applies to every bot - static const std::time_t _roleSwapCooldown = 10; - - static std::unordered_map _harpoonCooldowns; -}; - -// template -// class GenericBossHelper : public AiObject -// { -// public: -// GenericBossHelper(PlayerbotAI* botAI, std::string name) : AiObject(botAI), _name(name) {} -// virtual bool UpdateBossAI() -// { -// if (!bot->IsInCombat()) -// { -// _unit = nullptr; -// } -// if (_unit && (!_unit->IsInWorld() || !_unit->IsAlive())) -// { -// _unit = nullptr; -// } -// if (!_unit) -// { -// _unit = AI_VALUE2(Unit*, "find target", _name); -// if (!_unit) -// { -// return false; -// } -// _target = _unit->ToCreature(); -// if (!_target) -// { -// return false; -// } -// _ai = dynamic_cast(_target->GetAI()); -// if (!_ai) -// { -// return false; -// } -// _event_map = &_ai->events; -// if (!_event_map) -// { -// return false; -// } -// } -// if (!_event_map) -// { -// return false; -// } -// _timer = _event_map->GetTimer(); -// return true; -// } -// virtual void Reset() -// { -// _unit = nullptr; -// _target = nullptr; -// _ai = nullptr; -// _event_map = nullptr; -// _timer = 0; -// } - -// protected: -// std::string _name; -// Unit* _unit = nullptr; -// Creature* _target = nullptr; -// BossAiType* _ai = nullptr; -// EventMap* _event_map = nullptr; -// uint32 _timer = 0; -// }; - -#endif diff --git a/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.cpp b/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.cpp index 3b9a426cc67..0a1b76a40f1 100644 --- a/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.cpp +++ b/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.cpp @@ -1,7 +1,5 @@ #include "RaidUlduarStrategy.h" -#include "RaidUlduarMultipliers.h" - void RaidUlduarStrategy::InitTriggers(std::vector& triggers) { // @@ -316,8 +314,3 @@ void RaidUlduarStrategy::InitTriggers(std::vector& triggers) "yogg-saron phase 3 positioning trigger", { NextAction("yogg-saron phase 3 positioning action", ACTION_RAID) })); } - -void RaidUlduarStrategy::InitMultipliers(std::vector& multipliers) -{ - multipliers.push_back(new FlameLeviathanMultiplier(botAI)); -} diff --git a/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h b/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h index 81bb93c3a59..bb2feefe4af 100644 --- a/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h +++ b/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h @@ -3,16 +3,14 @@ #define _PLAYERBOT_RAIDULDUARSTRATEGY_H #include "AiObjectContext.h" -#include "Multiplier.h" #include "Strategy.h" class RaidUlduarStrategy : public Strategy { public: RaidUlduarStrategy(PlayerbotAI* ai) : Strategy(ai) {} - virtual std::string const getName() override { return "uld"; } + virtual std::string const getName() override { return "ulduar"; } virtual void InitTriggers(std::vector& triggers) override; - virtual void InitMultipliers(std::vector& multipliers) override; }; #endif diff --git a/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.cpp b/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.cpp index a4bc2cf9a27..f14a513118c 100644 --- a/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.cpp +++ b/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.cpp @@ -1634,7 +1634,7 @@ bool VezaxShadowCrashTrigger::IsActive() return false; } - return botAI->HasAura(SPELL_SHADOW_CRASH, bot); + return botAI->HasAura(SPELL_VEZAX_SHADOW_CRASH, bot); } bool VezaxMarkOfTheFacelessTrigger::IsActive() diff --git a/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.h b/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.h index 129c3d4dbae..7f8cb51a83d 100644 --- a/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.h +++ b/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.h @@ -3,187 +3,9 @@ #include "EventMap.h" #include "GenericTriggers.h" -#include "PlayerbotAIConfig.h" #include "RaidUlduarBossHelper.h" #include "Trigger.h" -enum UlduarIDs -{ - // Iron Assembly - SPELL_LIGHTNING_TENDRILS_10_MAN = 61887, - SPELL_LIGHTNING_TENDRILS_25_MAN = 63486, - SPELL_OVERLOAD_10_MAN = 61869, - SPELL_OVERLOAD_25_MAN = 63481, - SPELL_OVERLOAD_10_MAN_2 = 63485, - SPELL_OVERLOAD_25_MAN_2 = 61886, - SPELL_RUNE_OF_POWER = 64320, - - // Kologarn - NPC_RIGHT_ARM = 32934, - NPC_RUBBLE = 33768, - SPELL_CRUNCH_ARMOR = 64002, - - SPELL_FOCUSED_EYEBEAM_10_2 = 63346, - SPELL_FOCUSED_EYEBEAM_10 = 63347, - SPELL_FOCUSED_EYEBEAM_25_2 = 63976, - SPELL_FOCUSED_EYEBEAM_25 = 63977, - - // Hodir - NPC_SNOWPACKED_ICICLE = 33174, - NPC_TOASTY_FIRE = 33342, - SPELL_FLASH_FREEZE = 61968, - SPELL_BITING_COLD_PLAYER_AURA = 62039, - - // Freya - NPC_SNAPLASHER = 32916, - NPC_STORM_LASHER = 32919, - NPC_DETONATING_LASHER = 32918, - NPC_ANCIENT_WATER_SPIRIT = 33202, - NPC_ANCIENT_CONSERVATOR = 33203, - NPC_HEALTHY_SPORE = 33215, - NPC_EONARS_GIFT = 33228, - GOBJECT_NATURE_BOMB = 194902, - - // Thorim - NPC_DARK_RUNE_ACOLYTE_I = 32886, - NPC_CAPTURED_MERCENARY_SOLDIER_ALLY = 32885, - NPC_CAPTURED_MERCENARY_SOLDIER_HORDE = 32883, - NPC_CAPTURED_MERCENARY_CAPTAIN_ALLY = 32908, - NPC_CAPTURED_MERCENARY_CAPTAIN_HORDE = 32907, - NPC_JORMUNGAR_BEHEMOT = 32882, - NPC_DARK_RUNE_WARBRINGER = 32877, - NPC_DARK_RUNE_EVOKER = 32878, - NPC_DARK_RUNE_CHAMPION = 32876, - NPC_DARK_RUNE_COMMONER = 32904, - NPC_IRON_RING_GUARD = 32874, - NPC_RUNIC_COLOSSUS = 32872, - NPC_ANCIENT_RUNE_GIANT = 32873, - NPC_DARK_RUNE_ACOLYTE_G = 33110, - NPC_IRON_HONOR_GUARD = 32875, - SPELL_UNBALANCING_STRIKE = 62130, - - // Mimiron - NPC_LEVIATHAN_MKII = 33432, - NPC_VX001 = 33651, - NPC_AERIAL_COMMAND_UNIT = 33670, - NPC_BOMB_BOT = 33836, - NPC_ROCKET_STRIKE_N = 34047, - NPC_ASSAULT_BOT = 34057, - NPC_PROXIMITY_MINE = 34362, - SPELL_P3WX2_LASER_BARRAGE_1 = 63293, - SPELL_P3WX2_LASER_BARRAGE_2 = 63297, - SPELL_SPINNING_UP = 63414, - SPELL_SHOCK_BLAST = 63631, - SPELL_P3WX2_LASER_BARRAGE_3 = 64042, - SPELL_P3WX2_LASER_BARRAGE_AURA_1 = 63274, - SPELL_P3WX2_LASER_BARRAGE_AURA_2 = 63300, - - // General Vezax - SPELL_MARK_OF_THE_FACELESS = 63276, - SPELL_SHADOW_CRASH = 63277, - - // Yogg-Saron - ACTION_ILLUSION_DRAGONS = 1, - ACTION_ILLUSION_ICECROWN = 2, - ACTION_ILLUSION_STORMWIND = 3, - NPC_GUARDIAN_OF_YS = 33136, - NPC_YOGG_SARON = 33288, - NPC_OMINOUS_CLOUD = 33292, - NPC_RUBY_CONSORT = 33716, - NPC_AZURE_CONSORT = 33717, - NPC_BRONZE_CONSORT = 33718, - NPC_EMERALD_CONSORT = 33719, - NPC_OBSIDIAN_CONSORT = 33720, - NPC_ALEXTRASZA = 33536, - NPC_MALYGOS_ILLUSION = 33535, - NPC_NELTHARION = 33523, - NPC_YSERA = 33495, - GO_DRAGON_SOUL = 194462, - NPC_SARA_PHASE_1 = 33134, - NPC_LICH_KING_ILLUSION = 33441, - NPC_IMMOLATED_CHAMPION = 33442, - NPC_SUIT_OF_ARMOR = 33433, - NPC_GARONA = 33436, - NPC_KING_LLANE = 33437, - NPC_DEATHSWORN_ZEALOT = 33567, - NPC_INFLUENCE_TENTACLE = 33943, - NPC_DEATH_ORB = 33882, - NPC_BRAIN = 33890, - NPC_CRUSHER_TENTACLE = 33966, - NPC_CONSTRICTOR_TENTACLE = 33983, - NPC_CORRUPTOR_TENTACLE = 33985, - NPC_IMMORTAL_GUARDIAN = 33988, - NPC_LAUGHING_SKULL = 33990, - NPC_SANITY_WELL = 33991, - NPC_DESCEND_INTO_MADNESS = 34072, - NPC_MARKED_IMMORTAL_GUARDIAN = 36064, - SPELL_SANITY = 63050, - SPELL_BRAIN_LINK = 63802, - SPELL_MALADY_OF_THE_MIND = 63830, - SPELL_SHADOW_BARRIER = 63894, - SPELL_TELEPORT_TO_CHAMBER = 63997, - SPELL_TELEPORT_TO_ICECROWN = 63998, - SPELL_TELEPORT_TO_STORMWIND = 63989, - SPELL_TELEPORT_BACK = 63992, - SPELL_CANCEL_ILLUSION_AURA = 63993, - SPELL_INDUCE_MADNESS = 64059, - SPELL_LUNATIC_GAZE_YS = 64163, - GO_FLEE_TO_THE_SURFACE_PORTAL = 194625, - - // Buffs - SPELL_FROST_TRAP = 13809 -}; - -const float ULDUAR_KOLOGARN_AXIS_Z_PATHING_ISSUE_DETECT = 420.0f; -const float ULDUAR_KOLOGARN_EYEBEAM_RADIUS = 3.0f; -const float ULDUAR_THORIM_AXIS_Z_FLOOR_THRESHOLD = 429.6094f; -const float ULDUAR_THORIM_AXIS_Z_PATHING_ISSUE_DETECT = 410.0f; -const float ULDUAR_AURIAYA_AXIS_Z_PATHING_ISSUE_DETECT = 410.0f; -const float ULDUAR_YOGG_SARON_BOSS_ROOM_AXIS_Z_PATHING_ISSUE_DETECT = 300.0f; -const float ULDUAR_YOGG_SARON_BRAIN_ROOM_AXIS_Z_PATHING_ISSUE_DETECT = 200.0f; -const float ULDUAR_YOGG_SARON_STORMWIND_KEEPER_RADIUS = 150.0f; -const float ULDUAR_YOGG_SARON_ICECROWN_CITADEL_RADIUS = 150.0f; -const float ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_RADIUS = 150.0f; -const float ULDUAR_YOGG_SARON_BRAIN_ROOM_RADIUS = 50.0f; - -const Position ULDUAR_THORIM_NEAR_ARENA_CENTER = Position(2134.9854f, -263.11853f, 419.8465f); -const Position ULDUAR_THORIM_NEAR_ENTRANCE_POSITION = Position(2172.4355f, -258.27957f, 418.47162f); -const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_6_YARDS_1 = Position(2237.6187f, -265.08844f, 412.17548f); -const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_6_YARDS_2 = Position(2237.2498f, -275.81122f, 412.17548f); -const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_5_YARDS_1 = Position(2236.895f, -294.62448f, 412.1348f); -const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_1 = Position(2242.1162f, -310.15308f, 412.1348f); -const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_2 = Position(2242.018f, -318.66003f, 412.1348f); -const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_3 = Position(2242.1904f, -329.0533f, 412.1348f); -const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_6_YARDS_1 = Position(2219.5417f, -264.77167f, 412.17548f); -const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_6_YARDS_2 = Position(2217.446f, -275.85248f, 412.17548f); -const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_5_YARDS_1 = Position(2217.8877f, -295.01193f, 412.13434f); -const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_1 = Position(2212.193f, -307.44992f, 412.1348f); -const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_2 = Position(2212.1353f, -318.20795f, 412.1348f); -const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_3 = Position(2212.1956f, -328.0144f, 412.1348f); -const Position ULDUAR_THORIM_JUMP_END_POINT = Position(2137.8818f, -278.18942f, 419.66653f); -const Position ULDUAR_THORIM_PHASE2_TANK_SPOT = Position(2134.8572f, -287.0291f, 419.4935f); -const Position ULDUAR_THORIM_PHASE2_RANGE1_SPOT = Position(2112.8752f, -267.69305f, 419.52814f); -const Position ULDUAR_THORIM_PHASE2_RANGE2_SPOT = Position(2134.1296f, -257.3316f, 419.8462f); -const Position ULDUAR_THORIM_PHASE2_RANGE3_SPOT = Position(2156.798f, -267.57434f, 419.52722f); -const Position ULDUAR_MIMIRON_PHASE2_SIDE1RANGE_SPOT = Position(2753.708f, 2583.9617f, 364.31357f); -const Position ULDUAR_MIMIRON_PHASE2_SIDE1MELEE_SPOT = Position(2746.9792f, 2573.6716f, 364.31357f); -const Position ULDUAR_MIMIRON_PHASE2_SIDE2RANGE_SPOT = Position(2727.7224f, 2569.527f, 364.31357f); -const Position ULDUAR_MIMIRON_PHASE2_SIDE2MELEE_SPOT = Position(2739.4746f, 2569.4106f, 364.31357f); -const Position ULDUAR_MIMIRON_PHASE2_SIDE3RANGE_SPOT = Position(2754.1294f, 2553.9954f, 364.31357f); -const Position ULDUAR_MIMIRON_PHASE2_SIDE3MELEE_SPOT = Position(2746.8513f, 2565.4263f, 364.31357f); -const Position ULDUAR_MIMIRON_PHASE4_TANK_SPOT = Position(2744.5754f, 2570.8657f, 364.3138f); -const Position ULDUAR_VEZAX_MARK_OF_THE_FACELESS_SPOT = Position(1913.6501f, 122.93989f, 342.38083f); -const Position ULDUAR_YOGG_SARON_MIDDLE = Position(1980.28f, -25.5868f, 329.397f); -const Position ULDUAR_YOGG_SARON_STORMWIND_KEEPER_MIDDLE = Position(1927.1511f, 68.507256f, 242.37657f); -const Position ULDUAR_YOGG_SARON_ICECROWN_CITADEL_MIDDLE = Position(1925.6553f, -121.59296f, 239.98965f); -const Position ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_MIDDLE = Position(2104.5667f, -25.509348f, 242.64679f); -const Position ULDUAR_YOGG_SARON_BRAIN_ROOM_MIDDLE = Position(1980.1971f, -27.854689f, 236.06789f); -const Position ULDUAR_YOGG_SARON_STORMWIND_KEEPER_ENTRANCE = Position(1954.06f, 21.66f, 239.71f); -const Position ULDUAR_YOGG_SARON_ICECROWN_CITADEL_ENTRANCE = Position(1950.11f, -79.284f, 239.98982f); -const Position ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_ENTRANCE = Position(2048.63f, -25.5f, 239.72f); -const Position ULDUAR_YOGG_SARON_PHASE_3_MELEE_SPOT = Position(1998.5377f, -22.90317f, 324.8895f); -const Position ULDUAR_YOGG_SARON_PHASE_3_RANGED_SPOT = Position(2018.7628f, -18.896868f, 327.07245f); - // // Flame Levi // diff --git a/src/Ai/Raid/Ulduar/RaidUlduarBossHelper.cpp b/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.cpp similarity index 63% rename from src/Ai/Raid/Ulduar/RaidUlduarBossHelper.cpp rename to src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.cpp index 72333a079b3..fd6711cf0fc 100644 --- a/src/Ai/Raid/Ulduar/RaidUlduarBossHelper.cpp +++ b/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.cpp @@ -1,4 +1,3 @@ -#include "ChatHelper.h" #include "RaidUlduarBossHelper.h" #include "ObjectAccessor.h" #include "GameObject.h" @@ -9,6 +8,44 @@ #include "Playerbots.h" #include "World.h" +const Position ULDUAR_THORIM_NEAR_ARENA_CENTER = Position(2134.9854f, -263.11853f, 419.8465f); +const Position ULDUAR_THORIM_NEAR_ENTRANCE_POSITION = Position(2172.4355f, -258.27957f, 418.47162f); +const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_6_YARDS_1 = Position(2237.6187f, -265.08844f, 412.17548f); +const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_6_YARDS_2 = Position(2237.2498f, -275.81122f, 412.17548f); +const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_5_YARDS_1 = Position(2236.895f, -294.62448f, 412.1348f); +const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_1 = Position(2242.1162f, -310.15308f, 412.1348f); +const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_2 = Position(2242.018f, -318.66003f, 412.1348f); +const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_3 = Position(2242.1904f, -329.0533f, 412.1348f); +const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_6_YARDS_1 = Position(2219.5417f, -264.77167f, 412.17548f); +const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_6_YARDS_2 = Position(2217.446f, -275.85248f, 412.17548f); +const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_5_YARDS_1 = Position(2217.8877f, -295.01193f, 412.13434f); +const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_1 = Position(2212.193f, -307.44992f, 412.1348f); +const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_2 = Position(2212.1353f, -318.20795f, 412.1348f); +const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_3 = Position(2212.1956f, -328.0144f, 412.1348f); +const Position ULDUAR_THORIM_JUMP_END_POINT = Position(2137.8818f, -278.18942f, 419.66653f); +const Position ULDUAR_THORIM_PHASE2_TANK_SPOT = Position(2134.8572f, -287.0291f, 419.4935f); +const Position ULDUAR_THORIM_PHASE2_RANGE1_SPOT = Position(2112.8752f, -267.69305f, 419.52814f); +const Position ULDUAR_THORIM_PHASE2_RANGE2_SPOT = Position(2134.1296f, -257.3316f, 419.8462f); +const Position ULDUAR_THORIM_PHASE2_RANGE3_SPOT = Position(2156.798f, -267.57434f, 419.52722f); +const Position ULDUAR_MIMIRON_PHASE2_SIDE1RANGE_SPOT = Position(2753.708f, 2583.9617f, 364.31357f); +const Position ULDUAR_MIMIRON_PHASE2_SIDE1MELEE_SPOT = Position(2746.9792f, 2573.6716f, 364.31357f); +const Position ULDUAR_MIMIRON_PHASE2_SIDE2RANGE_SPOT = Position(2727.7224f, 2569.527f, 364.31357f); +const Position ULDUAR_MIMIRON_PHASE2_SIDE2MELEE_SPOT = Position(2739.4746f, 2569.4106f, 364.31357f); +const Position ULDUAR_MIMIRON_PHASE2_SIDE3RANGE_SPOT = Position(2754.1294f, 2553.9954f, 364.31357f); +const Position ULDUAR_MIMIRON_PHASE2_SIDE3MELEE_SPOT = Position(2746.8513f, 2565.4263f, 364.31357f); +const Position ULDUAR_MIMIRON_PHASE4_TANK_SPOT = Position(2744.5754f, 2570.8657f, 364.3138f); +const Position ULDUAR_VEZAX_MARK_OF_THE_FACELESS_SPOT = Position(1913.6501f, 122.93989f, 342.38083f); +const Position ULDUAR_YOGG_SARON_MIDDLE = Position(1980.28f, -25.5868f, 329.397f); +const Position ULDUAR_YOGG_SARON_STORMWIND_KEEPER_MIDDLE = Position(1927.1511f, 68.507256f, 242.37657f); +const Position ULDUAR_YOGG_SARON_ICECROWN_CITADEL_MIDDLE = Position(1925.6553f, -121.59296f, 239.98965f); +const Position ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_MIDDLE = Position(2104.5667f, -25.509348f, 242.64679f); +const Position ULDUAR_YOGG_SARON_BRAIN_ROOM_MIDDLE = Position(1980.1971f, -27.854689f, 236.06789f); +const Position ULDUAR_YOGG_SARON_STORMWIND_KEEPER_ENTRANCE = Position(1954.06f, 21.66f, 239.71f); +const Position ULDUAR_YOGG_SARON_ICECROWN_CITADEL_ENTRANCE = Position(1950.11f, -79.284f, 239.98982f); +const Position ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_ENTRANCE = Position(2048.63f, -25.5f, 239.72f); +const Position ULDUAR_YOGG_SARON_PHASE_3_MELEE_SPOT = Position(1998.5377f, -22.90317f, 324.8895f); +const Position ULDUAR_YOGG_SARON_PHASE_3_RANGED_SPOT = Position(2018.7628f, -18.896868f, 327.07245f); + // Prevent harpoon spam std::unordered_map RazorscaleBossHelper::_harpoonCooldowns; // Prevent role assignment spam diff --git a/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.h b/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.h new file mode 100644 index 00000000000..b3d49ff58cb --- /dev/null +++ b/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.h @@ -0,0 +1,341 @@ +#ifndef _PLAYERBOT_RAIDULDUARBOSSHELPER_H +#define _PLAYERBOT_RAIDULDUARBOSSHELPER_H + +#include +#include +#include + +#include "AiObject.h" +#include "AiObjectContext.h" +#include "EventMap.h" +#include "ObjectGuid.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "ScriptedCreature.h" + +constexpr uint32 ULDUAR_MAP_ID = 603; + +enum UlduarIDs +{ + // Iron Assembly + SPELL_LIGHTNING_TENDRILS_10_MAN = 61887, + SPELL_LIGHTNING_TENDRILS_25_MAN = 63486, + SPELL_OVERLOAD_10_MAN = 61869, + SPELL_OVERLOAD_25_MAN = 63481, + SPELL_OVERLOAD_10_MAN_2 = 63485, + SPELL_OVERLOAD_25_MAN_2 = 61886, + SPELL_RUNE_OF_POWER = 64320, + + // Kologarn + NPC_RIGHT_ARM = 32934, + NPC_RUBBLE = 33768, + SPELL_CRUNCH_ARMOR = 64002, + + SPELL_FOCUSED_EYEBEAM_10_2 = 63346, + SPELL_FOCUSED_EYEBEAM_10 = 63347, + SPELL_FOCUSED_EYEBEAM_25_2 = 63976, + SPELL_FOCUSED_EYEBEAM_25 = 63977, + + // Hodir + NPC_SNOWPACKED_ICICLE = 33174, + NPC_TOASTY_FIRE = 33342, + SPELL_FLASH_FREEZE = 61968, + SPELL_BITING_COLD_PLAYER_AURA = 62039, + + // Freya + NPC_SNAPLASHER = 32916, + NPC_STORM_LASHER = 32919, + NPC_DETONATING_LASHER = 32918, + NPC_ANCIENT_WATER_SPIRIT = 33202, + NPC_ANCIENT_CONSERVATOR = 33203, + NPC_HEALTHY_SPORE = 33215, + NPC_EONARS_GIFT = 33228, + GOBJECT_NATURE_BOMB = 194902, + + // Thorim + NPC_DARK_RUNE_ACOLYTE_I = 32886, + NPC_CAPTURED_MERCENARY_SOLDIER_ALLY = 32885, + NPC_CAPTURED_MERCENARY_SOLDIER_HORDE = 32883, + NPC_CAPTURED_MERCENARY_CAPTAIN_ALLY = 32908, + NPC_CAPTURED_MERCENARY_CAPTAIN_HORDE = 32907, + NPC_JORMUNGAR_BEHEMOT = 32882, + NPC_DARK_RUNE_WARBRINGER = 32877, + NPC_DARK_RUNE_EVOKER = 32878, + NPC_DARK_RUNE_CHAMPION = 32876, + NPC_DARK_RUNE_COMMONER = 32904, + NPC_IRON_RING_GUARD = 32874, + NPC_RUNIC_COLOSSUS = 32872, + NPC_ANCIENT_RUNE_GIANT = 32873, + NPC_DARK_RUNE_ACOLYTE_G = 33110, + NPC_IRON_HONOR_GUARD = 32875, + SPELL_UNBALANCING_STRIKE = 62130, + + // Mimiron + NPC_LEVIATHAN_MKII = 33432, + NPC_VX001 = 33651, + NPC_AERIAL_COMMAND_UNIT = 33670, + NPC_BOMB_BOT = 33836, + NPC_ROCKET_STRIKE_N = 34047, + NPC_ASSAULT_BOT = 34057, + NPC_PROXIMITY_MINE = 34362, + SPELL_P3WX2_LASER_BARRAGE_1 = 63293, + SPELL_P3WX2_LASER_BARRAGE_2 = 63297, + SPELL_SPINNING_UP = 63414, + SPELL_SHOCK_BLAST = 63631, + SPELL_P3WX2_LASER_BARRAGE_3 = 64042, + SPELL_P3WX2_LASER_BARRAGE_AURA_1 = 63274, + SPELL_P3WX2_LASER_BARRAGE_AURA_2 = 63300, + + // General Vezax + SPELL_MARK_OF_THE_FACELESS = 63276, + SPELL_VEZAX_SHADOW_CRASH = 63277, + + // Yogg-Saron + ACTION_ILLUSION_DRAGONS = 1, + ACTION_ILLUSION_ICECROWN = 2, + ACTION_ILLUSION_STORMWIND = 3, + NPC_GUARDIAN_OF_YS = 33136, + NPC_YOGG_SARON = 33288, + NPC_OMINOUS_CLOUD = 33292, + NPC_RUBY_CONSORT = 33716, + NPC_AZURE_CONSORT = 33717, + NPC_BRONZE_CONSORT = 33718, + NPC_EMERALD_CONSORT = 33719, + NPC_OBSIDIAN_CONSORT = 33720, + NPC_ALEXTRASZA = 33536, + NPC_MALYGOS_ILLUSION = 33535, + NPC_NELTHARION = 33523, + NPC_YSERA = 33495, + GO_DRAGON_SOUL = 194462, + NPC_SARA_PHASE_1 = 33134, + NPC_LICH_KING_ILLUSION = 33441, + NPC_IMMOLATED_CHAMPION = 33442, + NPC_SUIT_OF_ARMOR = 33433, + NPC_GARONA = 33436, + NPC_KING_LLANE = 33437, + NPC_DEATHSWORN_ZEALOT = 33567, + NPC_INFLUENCE_TENTACLE = 33943, + NPC_DEATH_ORB = 33882, + NPC_BRAIN = 33890, + NPC_CRUSHER_TENTACLE = 33966, + NPC_CONSTRICTOR_TENTACLE = 33983, + NPC_CORRUPTOR_TENTACLE = 33985, + NPC_IMMORTAL_GUARDIAN = 33988, + NPC_LAUGHING_SKULL = 33990, + NPC_SANITY_WELL = 33991, + NPC_DESCEND_INTO_MADNESS = 34072, + NPC_MARKED_IMMORTAL_GUARDIAN = 36064, + SPELL_SANITY = 63050, + SPELL_BRAIN_LINK = 63802, + SPELL_MALADY_OF_THE_MIND = 63830, + SPELL_SHADOW_BARRIER = 63894, + SPELL_TELEPORT_TO_CHAMBER = 63997, + SPELL_TELEPORT_TO_ICECROWN = 63998, + SPELL_TELEPORT_TO_STORMWIND = 63989, + SPELL_TELEPORT_BACK = 63992, + SPELL_CANCEL_ILLUSION_AURA = 63993, + SPELL_INDUCE_MADNESS = 64059, + SPELL_LUNATIC_GAZE_YS = 64163, + GO_FLEE_TO_THE_SURFACE_PORTAL = 194625, + + // Buffs + SPELL_FROST_TRAP = 13809 +}; + +constexpr float ULDUAR_KOLOGARN_AXIS_Z_PATHING_ISSUE_DETECT = 420.0f; +constexpr float ULDUAR_KOLOGARN_EYEBEAM_RADIUS = 3.0f; +constexpr float ULDUAR_THORIM_AXIS_Z_FLOOR_THRESHOLD = 429.6094f; +constexpr float ULDUAR_THORIM_AXIS_Z_PATHING_ISSUE_DETECT = 410.0f; +constexpr float ULDUAR_AURIAYA_AXIS_Z_PATHING_ISSUE_DETECT = 410.0f; +constexpr float ULDUAR_YOGG_SARON_BOSS_ROOM_AXIS_Z_PATHING_ISSUE_DETECT = 300.0f; +constexpr float ULDUAR_YOGG_SARON_BRAIN_ROOM_AXIS_Z_PATHING_ISSUE_DETECT = 200.0f; +constexpr float ULDUAR_YOGG_SARON_STORMWIND_KEEPER_RADIUS = 150.0f; +constexpr float ULDUAR_YOGG_SARON_ICECROWN_CITADEL_RADIUS = 150.0f; +constexpr float ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_RADIUS = 150.0f; +constexpr float ULDUAR_YOGG_SARON_BRAIN_ROOM_RADIUS = 50.0f; + +extern const Position ULDUAR_THORIM_NEAR_ARENA_CENTER; +extern const Position ULDUAR_THORIM_NEAR_ENTRANCE_POSITION; +extern const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_6_YARDS_1; +extern const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_6_YARDS_2; +extern const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_5_YARDS_1; +extern const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_1; +extern const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_2; +extern const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_3; +extern const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_6_YARDS_1; +extern const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_6_YARDS_2; +extern const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_5_YARDS_1; +extern const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_1; +extern const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_2; +extern const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_3; +extern const Position ULDUAR_THORIM_JUMP_END_POINT; +extern const Position ULDUAR_THORIM_PHASE2_TANK_SPOT; +extern const Position ULDUAR_THORIM_PHASE2_RANGE1_SPOT; +extern const Position ULDUAR_THORIM_PHASE2_RANGE2_SPOT; +extern const Position ULDUAR_THORIM_PHASE2_RANGE3_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE2_SIDE1RANGE_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE2_SIDE1MELEE_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE2_SIDE2RANGE_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE2_SIDE2MELEE_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE2_SIDE3RANGE_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE2_SIDE3MELEE_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE4_TANK_SPOT; +extern const Position ULDUAR_VEZAX_MARK_OF_THE_FACELESS_SPOT; +extern const Position ULDUAR_YOGG_SARON_MIDDLE; +extern const Position ULDUAR_YOGG_SARON_STORMWIND_KEEPER_MIDDLE; +extern const Position ULDUAR_YOGG_SARON_ICECROWN_CITADEL_MIDDLE; +extern const Position ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_MIDDLE; +extern const Position ULDUAR_YOGG_SARON_BRAIN_ROOM_MIDDLE; +extern const Position ULDUAR_YOGG_SARON_STORMWIND_KEEPER_ENTRANCE; +extern const Position ULDUAR_YOGG_SARON_ICECROWN_CITADEL_ENTRANCE; +extern const Position ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_ENTRANCE; +extern const Position ULDUAR_YOGG_SARON_PHASE_3_MELEE_SPOT; +extern const Position ULDUAR_YOGG_SARON_PHASE_3_RANGED_SPOT; + +class RazorscaleBossHelper : public AiObject +{ +public: + // Enums and constants specific to Razorscale + enum RazorscaleUnits : uint32 + { + UNIT_RAZORSCALE = 33186, + UNIT_DARK_RUNE_SENTINEL = 33846, + UNIT_DARK_RUNE_WATCHER = 33453, + UNIT_DARK_RUNE_GUARDIAN = 33388, + UNIT_DEVOURING_FLAME = 34188, + }; + + enum RazorscaleGameObjects : uint32 + { + GO_RAZORSCALE_HARPOON_1 = 194519, + GO_RAZORSCALE_HARPOON_2 = 194541, + GO_RAZORSCALE_HARPOON_3 = 194542, + GO_RAZORSCALE_HARPOON_4 = 194543, + }; + + enum RazorscaleSpells : uint32 + { + SPELL_CHAIN_1 = 49679, + SPELL_CHAIN_2 = 49682, + SPELL_CHAIN_3 = 49683, + SPELL_CHAIN_4 = 49684, + SPELL_SENTINEL_WHIRLWIND = 63806, + SPELL_STUN_AURA = 62794, + SPELL_FUSEARMOR = 64771 + }; + + static constexpr uint32 FUSEARMOR_THRESHOLD = 2; + + // Constants for arena parameters + static constexpr float RAZORSCALE_FLYING_Z_THRESHOLD = 440.0f; + static constexpr float RAZORSCALE_ARENA_CENTER_X = 587.54f; + static constexpr float RAZORSCALE_ARENA_CENTER_Y = -175.04f; + static constexpr float RAZORSCALE_ARENA_RADIUS = 30.0f; + + // Harpoon cooldown (seconds) + static constexpr time_t HARPOON_COOLDOWN_DURATION = 5; + + // Structure for harpoon data + struct HarpoonData + { + uint32 gameObjectEntry; + uint32 chainSpellId; + }; + + explicit RazorscaleBossHelper(PlayerbotAI* botAI) + : AiObject(botAI), _boss(nullptr) {} + + bool UpdateBossAI(); + Unit* GetBoss() const; + + bool IsGroundPhase() const; + bool IsFlyingPhase() const; + + bool IsHarpoonFired(uint32 chainSpellId) const; + static bool IsHarpoonReady(GameObject* harpoonGO); + static void SetHarpoonOnCooldown(GameObject* harpoonGO); + GameObject* FindNearestHarpoon(float x, float y, float z) const; + + static const std::vector& GetHarpoonData(); + + void AssignRolesBasedOnHealth(); + bool AreRolesAssigned() const; + bool CanSwapRoles() const; + +private: + Unit* _boss; + + // A map to track the last role swap *per bot* by their GUID + static std::unordered_map _lastRoleSwapTime; + + // The cooldown that applies to every bot + static const std::time_t _roleSwapCooldown = 10; + + static std::unordered_map _harpoonCooldowns; +}; + +// template +// class GenericBossHelper : public AiObject +// { +// public: +// GenericBossHelper(PlayerbotAI* botAI, std::string name) : AiObject(botAI), _name(name) {} +// virtual bool UpdateBossAI() +// { +// if (!bot->IsInCombat()) +// { +// _unit = nullptr; +// } +// if (_unit && (!_unit->IsInWorld() || !_unit->IsAlive())) +// { +// _unit = nullptr; +// } +// if (!_unit) +// { +// _unit = AI_VALUE2(Unit*, "find target", _name); +// if (!_unit) +// { +// return false; +// } +// _target = _unit->ToCreature(); +// if (!_target) +// { +// return false; +// } +// _ai = dynamic_cast(_target->GetAI()); +// if (!_ai) +// { +// return false; +// } +// _event_map = &_ai->events; +// if (!_event_map) +// { +// return false; +// } +// } +// if (!_event_map) +// { +// return false; +// } +// _timer = _event_map->GetTimer(); +// return true; +// } +// virtual void Reset() +// { +// _unit = nullptr; +// _target = nullptr; +// _ai = nullptr; +// _event_map = nullptr; +// _timer = 0; +// } + +// protected: +// std::string _name; +// Unit* _unit = nullptr; +// Creature* _target = nullptr; +// BossAiType* _ai = nullptr; +// EventMap* _event_map = nullptr; +// uint32 _timer = 0; +// }; + +#endif diff --git a/src/Ai/Raid/Ulduar/RaidUlduarScripts.h b/src/Ai/Raid/Ulduar/Util/RaidUlduarScripts.h similarity index 100% rename from src/Ai/Raid/Ulduar/RaidUlduarScripts.h rename to src/Ai/Raid/Ulduar/Util/RaidUlduarScripts.h diff --git a/src/Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h b/src/Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h index b304826b8ff..263d03d9b75 100644 --- a/src/Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h +++ b/src/Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h @@ -7,6 +7,7 @@ #define _PLAYERBOT_RAIDVOAACTIONCONTEXT_H #include "Action.h" +#include "BossAuraActions.h" #include "NamedObjectContext.h" #include "RaidVoAActions.h" #include "PlayerbotAI.h" diff --git a/src/Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h b/src/Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h index 9fe078f809b..6566793fde1 100644 --- a/src/Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h +++ b/src/Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h @@ -7,6 +7,7 @@ #define _PLAYERBOT_RAIDVOATRIGGERCONTEXT_H #include "AiObjectContext.h" +#include "BossAuraTriggers.h" #include "NamedObjectContext.h" #include "RaidVoATriggers.h" diff --git a/src/Bot/Engine/AiObjectContext.cpp b/src/Bot/Engine/AiObjectContext.cpp index b6d5f7de0dc..250a3929697 100644 --- a/src/Bot/Engine/AiObjectContext.cpp +++ b/src/Bot/Engine/AiObjectContext.cpp @@ -15,8 +15,6 @@ #include "PaladinAiObjectContext.h" #include "Playerbots.h" #include "PriestAiObjectContext.h" -#include "RaidUlduarActionContext.h" -#include "RaidUlduarTriggerContext.h" #include "RogueAiObjectContext.h" #include "ShamanAiObjectContext.h" #include "SharedValueContext.h" @@ -49,6 +47,8 @@ #include "Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h" #include "Ai/Raid/ObsidianSanctum/RaidOsActionContext.h" #include "Ai/Raid/ObsidianSanctum/RaidOsTriggerContext.h" +#include "Ai/Raid/Ulduar/RaidUlduarActionContext.h" +#include "Ai/Raid/Ulduar/RaidUlduarTriggerContext.h" #include "Ai/Raid/Onyxia/RaidOnyxiaActionContext.h" #include "Ai/Raid/Onyxia/RaidOnyxiaTriggerContext.h" #include "Ai/Raid/Icecrown/RaidIccActionContext.h" diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 437e62fc693..56c2cd39b09 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -1585,7 +1585,7 @@ void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) strategyName = "wotlk-hol"; // Halls of Lightning break; case 603: - strategyName = "uld"; // Ulduar + strategyName = "ulduar"; // Ulduar break; case 604: strategyName = "wotlk-gd"; // Gundrak From 026df0dabead6e200462f7dbb7594a6d0fee9d37 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Fri, 6 Feb 2026 20:57:48 +0100 Subject: [PATCH 19/29] Chilton wand fix (#2115) # Pull Request Added Chilton wand to excluded to equipment items for bots and unified 2 exclusion lists to single one. Resolves: https://github.com/mod-playerbots/mod-playerbots/issues/2093 --- ## How to Test the Changes Couldnt reproduce Chilton wand bug then testing sound impossible. Someone can try getting this items on shaman. ## Complexity & Impact Does this change add new decision branches? - - [x] No - - [ ] Yes Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes Could this logic scale poorly under load? - - [x] No - - [ ] Yes --- ## Defaults & Configuration Does this change modify default bot behavior? - - [x] No - - [ ] Yes If this introduces more advanced or AI-heavy logic: - - [ ] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [x] No - - [ ] Yes --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- --- src/Bot/Factory/PlayerbotFactory.cpp | 4 ---- src/Mgr/Item/RandomItemMgr.cpp | 9 ++++++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Bot/Factory/PlayerbotFactory.cpp b/src/Bot/Factory/PlayerbotFactory.cpp index dfefd323c27..ca1f4d1800d 100644 --- a/src/Bot/Factory/PlayerbotFactory.cpp +++ b/src/Bot/Factory/PlayerbotFactory.cpp @@ -1799,10 +1799,6 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) { for (uint32 itemId : sRandomItemMgr.GetCachedEquipments(requiredLevel, inventoryType)) { - if (itemId == 46978) // shaman earth ring totem - { - continue; - } uint32 skipProb = 25; if (urand(1, 100) <= skipProb) continue; diff --git a/src/Mgr/Item/RandomItemMgr.cpp b/src/Mgr/Item/RandomItemMgr.cpp index 5c0e8c94ab2..1053e542e61 100644 --- a/src/Mgr/Item/RandomItemMgr.cpp +++ b/src/Mgr/Item/RandomItemMgr.cpp @@ -2256,10 +2256,13 @@ void RandomItemMgr::BuildEquipCacheNew() { continue; } - if (itemId == 22784) - { // Sunwell Orb + + // Unobtainable or unusable items + if (itemId == 12468 || // Chilton Wand + itemId == 22784 || // Sunwell Orb + itemId == 46978) // Totem of the Earthen Ring continue; - } + equipCacheNew[proto->RequiredLevel][proto->InventoryType].push_back(itemId); } } From 76b6df9ea363912f47355a9984c5c18ec4be05e0 Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Fri, 6 Feb 2026 23:20:31 +0100 Subject: [PATCH 20/29] Extend SummonWhenGroup to auto-added bots (#2034) ### Summary Extend AiPlayerbot.SummonWhenGroup to apply when bots are auto-added to a group (e.g., addclass bots or raidus style auto invites). ### Motivation Bots added automatically to a group never accept a normal invite, so they do not trigger the summon-on-accept path. When SummonWhenGroup is enabled, these bots should also be teleported next to the master to match expected behavior. ### Implementation details Hook the summon behavior right after automatic group addition. --- src/Ai/Base/Actions/UseMeetingStoneAction.h | 2 +- src/Script/WorldThr/PlayerbotOperations.h | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Ai/Base/Actions/UseMeetingStoneAction.h b/src/Ai/Base/Actions/UseMeetingStoneAction.h index ff1282284ea..fd2a2c5f2da 100644 --- a/src/Ai/Base/Actions/UseMeetingStoneAction.h +++ b/src/Ai/Base/Actions/UseMeetingStoneAction.h @@ -17,9 +17,9 @@ class SummonAction : public MovementAction SummonAction(PlayerbotAI* botAI, std::string const name = "summon") : MovementAction(botAI, name) {} bool Execute(Event event) override; + bool Teleport(Player* summoner, Player* player, bool preserveAuras); protected: - bool Teleport(Player* summoner, Player* player, bool preserveAuras); bool SummonUsingGos(Player* summoner, Player* player, bool preserveAuras); bool SummonUsingNpcs(Player* summoner, Player* player, bool preserveAuras); }; diff --git a/src/Script/WorldThr/PlayerbotOperations.h b/src/Script/WorldThr/PlayerbotOperations.h index a80321d535c..ee6443cd8a1 100644 --- a/src/Script/WorldThr/PlayerbotOperations.h +++ b/src/Script/WorldThr/PlayerbotOperations.h @@ -14,9 +14,11 @@ #include "PlayerbotOperation.h" #include "Player.h" #include "PlayerbotAI.h" +#include "PlayerbotAIConfig.h" #include "PlayerbotMgr.h" #include "PlayerbotRepository.h" #include "RandomPlayerbotMgr.h" +#include "UseMeetingStoneAction.h" #include "WorldSession.h" #include "WorldSessionMgr.h" @@ -74,6 +76,15 @@ class GroupInviteOperation : public PlayerbotOperation if (group->AddMember(target)) { LOG_DEBUG("playerbots", "GroupInviteOperation: Successfully added {} to group", target->GetName()); + if (sPlayerbotAIConfig.summonWhenGroup && target->GetDistance(bot) > sPlayerbotAIConfig.sightDistance) + { + PlayerbotAI* targetAI = sPlayerbotsMgr.GetPlayerbotAI(target); + if (targetAI) + { + SummonAction summonAction(targetAI, "group summon"); + summonAction.Teleport(bot, target, true); + } + } return true; } else From 65368a19bb7856fe9afdd1547f91941779df61ed Mon Sep 17 00:00:00 2001 From: crow Date: Fri, 6 Feb 2026 19:27:48 -0600 Subject: [PATCH 21/29] majorish updates to KT and some to other bosses --- .../Action/RaidTempestKeepActions.cpp | 406 +++++++----------- .../Action/RaidTempestKeepActions.h | 18 +- .../Multiplier/RaidTempestKeepMultipliers.cpp | 76 +--- .../Multiplier/RaidTempestKeepMultipliers.h | 16 - .../RaidTempestKeepActionContext.h | 12 - .../RaidTempestKeepTriggerContext.h | 6 - .../Strategy/RaidTempestKeepStrategy.cpp | 6 - .../Trigger/RaidTempestKeepTriggers.cpp | 59 ++- .../Trigger/RaidTempestKeepTriggers.h | 8 - .../{ => Util}/RaidTempestKeepHelpers.cpp | 146 +------ .../{ => Util}/RaidTempestKeepHelpers.h | 50 +-- .../RaidTempestKeepKaelthasBossAI.h | 0 12 files changed, 217 insertions(+), 586 deletions(-) rename src/Ai/Raid/TempestKeep/{ => Util}/RaidTempestKeepHelpers.cpp (78%) rename src/Ai/Raid/TempestKeep/{ => Util}/RaidTempestKeepHelpers.h (80%) rename src/Ai/Raid/TempestKeep/{ => Util}/RaidTempestKeepKaelthasBossAI.h (100%) diff --git a/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp index f77a75637b1..60c3e833c42 100644 --- a/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp +++ b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp @@ -2,11 +2,12 @@ #include "RaidTempestKeepHelpers.h" #include "RaidTempestKeepKaelthasBossAI.h" #include "AiFactory.h" +#include "EquipAction.h" #include "LootAction.h" #include "LootObjectStack.h" #include "ObjectAccessor.h" #include "Playerbots.h" -#include "SharedDefines.h" +#include "RaidBossHelpers.h" using namespace TempestKeepHelpers; @@ -259,7 +260,7 @@ bool AlarRangedAndEmberTankMoveUnderPlatformsAction::Execute(Event event) } else if (botAI->IsAssistTankOfIndex(bot, 1, true)) { - Unit* ember = AI_VALUE2(Unit*, "find target", "ember of al'ar"); + Unit* ember = AI_VALUE2(Unit*, "find target", "ember of alar"); if (!ember && bot->GetExactDist2d( groundTarget.GetPositionX(), groundTarget.GetPositionY()) > 20.0f) { @@ -296,7 +297,7 @@ bool AlarAssistTanksPickUpEmbersAction::HandlePhase1Embers(Unit* alar) if (!botAI->IsAssistTankOfIndex(bot, 1, true)) return false; - Unit* ember = AI_VALUE2(Unit*, "find target", "ember of al'ar"); + Unit* ember = AI_VALUE2(Unit*, "find target", "ember of alar"); if (ember) { MarkTargetWithSquare(bot, ember); @@ -332,10 +333,9 @@ bool AlarAssistTanksPickUpEmbersAction::HandlePhase1Embers(Unit* alar) } else { - const float safeDistance = 15.0f; - Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance); - if (nearestPlayer) - return MoveFromGroup(safeDistance + 1.0f); + constexpr float safeDistance = 16.0f; + if (GetNearestPlayerInRadius(bot, safeDistance)) + return MoveFromGroup(safeDistance); } } else if (!bot->IsWithinMeleeRange(ember)) @@ -374,10 +374,9 @@ bool AlarAssistTanksPickUpEmbersAction::HandlePhase2Embers(Unit* alar) } else if (bot->IsWithinMeleeRange(firstEmber)) { - const float safeDistance = 15.0f; - Unit* nearestPlayer = GetNearestNonTankPlayerInRadius(bot, safeDistance); - if (nearestPlayer) - return MoveFromGroup(safeDistance + 1.0f); + constexpr float safeDistance = 16.0f; + if (GetNearestNonTankPlayerInRadius(bot, safeDistance)) + return MoveFromGroup(safeDistance); } } else if (GetSecondEmberTank(botAI, alar) == bot && secondEmber) @@ -399,10 +398,9 @@ bool AlarAssistTanksPickUpEmbersAction::HandlePhase2Embers(Unit* alar) } else if (bot->IsWithinMeleeRange(secondEmber)) { - const float safeDistance = 15.0f; - Unit* nearestPlayer = GetNearestNonTankPlayerInRadius(bot, safeDistance); - if (nearestPlayer) - return MoveFromGroup(safeDistance + 1.0f); + constexpr float safeDistance = 16.0f; + if (GetNearestNonTankPlayerInRadius(bot, safeDistance)) + return MoveFromGroup(safeDistance); } } @@ -413,7 +411,7 @@ bool AlarRangedDpsPrioritizeEmbersAction::Execute(Event event) { auto [firstEmber, secondEmber] = GetFirstTwoEmbersOfAlar(botAI); - const float safeDistance = 15.0f; + constexpr float safeDistance = 15.0f; if (firstEmber) { if (bot->GetDistance2d(firstEmber) < safeDistance) @@ -531,7 +529,7 @@ bool AlarMoveAwayFromRebirthAction::Execute(Event event) else { float currentDistance = bot->GetDistance2d(alar); - const float safeDistance = 20.0f; + constexpr float safeDistance = 20.0f; if (currentDistance < safeDistance) { botAI->Reset(); @@ -601,7 +599,7 @@ bool AlarAvoidFlamePatchesAndDiveBombsAction::Execute(Event event) bool AlarAvoidFlamePatchesAndDiveBombsAction::AvoidFlamePatch() { std::vector flamePatches = GetAllHazardTriggers(botAI, bot, NPC_FLAME_PATCH, 40.0f); - const float hazardRadius = 8.0f; + constexpr float hazardRadius = 8.0f; for (Unit* flamePatch : flamePatches) { if (bot->GetExactDist2d(flamePatch) < hazardRadius) @@ -625,7 +623,7 @@ bool AlarAvoidFlamePatchesAndDiveBombsAction::HandleDiveBomb(Unit* alar) !alar->IsVisible()) { float currentDistance = bot->GetDistance2d(alar); - const float safeDistance = 20.0f; + constexpr float safeDistance = 20.0f; if (currentDistance < safeDistance) { bot->AttackStop(); @@ -639,10 +637,10 @@ bool AlarAvoidFlamePatchesAndDiveBombsAction::HandleDiveBomb(Unit* alar) if (GetAlarCurrentLocationIndex(alar) == POINT_QUILL_OR_DIVE_IDX || GetAlarDestinationLocationIndex(alar, dest) == POINT_QUILL_OR_DIVE_IDX) { - const float safeDistance = 10.0f; + constexpr float safeDistance = 10.0f; if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) { - const uint32 minInterval = 0; + constexpr uint32 minInterval = 0; return FleePosition(Position(nearestPlayer->GetPosition()), safeDistance, minInterval); } @@ -704,14 +702,14 @@ bool VoidReaverTanksPositionBossAction::Execute(Event event) float dX = position.GetPositionX() - bot->GetPositionX(); float dY = position.GetPositionY() - bot->GetPositionY(); - float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), + float distToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - if (bot->IsWithinMeleeRange(voidReaver) && distanceToPosition > 2.0f) + if (bot->IsWithinMeleeRange(voidReaver) && distToPosition > 2.0f) { - float moveDist = std::min(5.0f, distanceToPosition); - float moveX = bot->GetPositionX() + (dX / distanceToPosition) * moveDist; - float moveY = bot->GetPositionY() + (dY / distanceToPosition) * moveDist; + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, true); @@ -732,7 +730,6 @@ bool VoidReaverRangedUseAggroDumpAbilityAction::Execute(Event event) "soulshatter", "vanish", }; - for (const char* spell : spells) { if (botAI->CanCastSpell(spell, bot)) @@ -769,9 +766,9 @@ bool VoidReaverSpreadRangedAction::Execute(Event event) } const Position& position = VOID_REAVER_TANK_POSITION; - const float radius = 30.0f; - const float offsetArc = 1.0f; - const uint8 botsPerRing = 8; + constexpr float radius = 30.0f; + constexpr float offsetArc = 1.0f; + constexpr uint8 botsPerRing = 8; std::vector rangedBots = healers; rangedBots.insert(rangedBots.end(), rangedDps.begin(), rangedDps.end()); @@ -836,21 +833,20 @@ bool HighAstromancerSolarianRangedLeaveSpaceForMeleeAction::Execute(Event event) return false; float currentDistance = bot->GetExactDist2d(astromancer); - const float minDistance = 20.0f; + constexpr float minDistance = 20.0f; if (currentDistance < minDistance) - return MoveAway(astromancer, minDistance - currentDistance + 2.0f); + return MoveAway(astromancer, minDistance - currentDistance); return false; } bool HighAstromancerSolarianMoveAwayFromGroupAction::Execute(Event event) { - const float safeDistance = 15.0f; - Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance); - if (nearestPlayer) + constexpr float safeDistance = 15.0f; + if (GetNearestPlayerInRadius(bot, safeDistance)) { botAI->Reset(); - return MoveFromGroup(safeDistance + 1.0f); + return MoveFromGroup(safeDistance); } return false; @@ -1040,7 +1036,7 @@ bool KaelthasSunstriderKiteThaladredAction::Execute(Event event) return false; float currentDistance = bot->GetExactDist2d(thaladred); - const float safeDistance = 22.0f; + constexpr float safeDistance = 23.0f; if (currentDistance < safeDistance) { botAI->Reset(); @@ -1161,17 +1157,18 @@ bool KaelthasSunstriderMainTankPositionSanguinarAction::Execute(Event event) bool KaelthasSunstriderCastFearWardOnSanguinarTankAction::Execute(Event event) { + Group* group = bot->GetGroup(); + if (!group) + return false; + Player* mainTank = nullptr; - if (Group* group = bot->GetGroup()) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + Player* member = ref->GetSource(); + if (member && botAI->IsMainTank(member)) { - Player* member = ref->GetSource(); - if (member && botAI->IsMainTank(member)) - { - mainTank = member; - break; - } + mainTank = member; + break; } } @@ -1181,89 +1178,32 @@ bool KaelthasSunstriderCastFearWardOnSanguinarTankAction::Execute(Event event) return false; } -// Use tank strategy only when necessary to tank Capernian; otherwise, use DPS strategies -bool KaelthasSunstriderManageWarlockTankStrategyAction::Execute(Event event) -{ - Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); - if (!kaelthas) - return false; - - boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); - if (!kaelAI) - return false; - - bool currentlyTank = botAI->HasStrategy("tank", BotState::BOT_STATE_COMBAT); - - // Phase 1: Switch to tank after Sanguinar is "dead" (feign death) - if (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR) - { - if (!currentlyTank) - { - Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); - if (sanguinar && sanguinar->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) - botAI->ChangeStrategy("+tank", BotState::BOT_STATE_COMBAT); - } - } - // Phase 2: Switch to DPS - else if (kaelAI->GetPhase() == PHASE_WEAPONS) - { - if (currentlyTank) - botAI->ChangeStrategy("-tank", BotState::BOT_STATE_COMBAT); - } - // Phase 2→3 Transition: Weapons dead, waiting for advisors - switch to tank - else if (kaelAI->GetPhase() == PHASE_TRANSITION) - { - if (!currentlyTank) - botAI->ChangeStrategy("+tank", BotState::BOT_STATE_COMBAT); - } - // Phase 3 - else if (kaelAI->GetPhase() == PHASE_ALL_ADVISORS) - { - Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); - // If Capernian is alive, switch to tank strategy - // (fallback in case not all weapons were down before Phase 3) - if (capernian && !currentlyTank) - botAI->ChangeStrategy("+tank", BotState::BOT_STATE_COMBAT); - // If Capernian is dead, reset to DPS for remainder of encounter - else if (!capernian && currentlyTank) - botAI->ChangeStrategy("-tank", BotState::BOT_STATE_COMBAT); - } - - return false; -} - bool KaelthasSunstriderWarlockTankPositionCapernianAction::Execute(Event event) { - Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); - if (!kaelthas) - return false; - - boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); - if (!kaelAI) - return false; - Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); - if (!capernian || capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || - capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + if (!capernian) return false; MarkTargetWithCircle(bot, capernian); SetRtiTarget(botAI, "circle", capernian); if (bot->GetTarget() != capernian->GetGUID()) - return Attack(capernian); + { + if (botAI->CanCastSpell("searing pain", capernian)) + botAI->CastSpell("searing pain", capernian); + } - if (capernian->GetVictim() == bot && kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR) + if (capernian->GetVictim() == bot) { float currentDist = bot->GetDistance2d(capernian); - if (currentDist == 0.0f) - return false; - - const float minDistance = 20.0f; + constexpr float minDistance = 20.0f; if (currentDist < minDistance) - return MoveAway(capernian, minDistance - currentDist + 1.0f); + return MoveAway(capernian, minDistance - currentDist); } + if (botAI->CanCastSpell("searing pain", capernian)) + return botAI->CastSpell("searing pain", capernian); + return false; } @@ -1300,10 +1240,10 @@ bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::RangedBotsDisperse( return false; } - const float safeDistance = 6.0f; + constexpr float safeDistance = 6.0f; if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) { - const uint32 minInterval = 1000; + constexpr uint32 minInterval = 1000; return FleePosition(Position(nearestPlayer->GetPosition()), safeDistance, minInterval); } @@ -1320,7 +1260,7 @@ bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::StayBackFromCaperni // Main tank purposely stays in range to bait Conflagration in Phase 1 if (botAI->IsMainTank(bot)) { - const float desiredDist = 15.0f; + constexpr float desiredDist = 15.0f; if (fabs(bot->GetExactDist2d(capernian) - desiredDist)) { float dx = bot->GetPositionX() - capernian->GetPositionX(); @@ -1403,7 +1343,6 @@ bool KaelthasSunstriderFirstAssistTankPositionTelonicusAction::Execute(Event eve bool KaelthasSunstriderHandleAdvisorRolesInPhase3Action::Execute(Event event) { const Position* movePosition = nullptr; - if (botAI->IsAssistHealOfIndex(bot, 0, true)) movePosition = &ADVISOR_HEAL_POSITION; else if (botAI->IsMainTank(bot)) @@ -1462,10 +1401,10 @@ bool KaelthasSunstriderAssignAdvisorDpsPriorityAction::Execute(Event event) } // Target priority 2: Capernian for ranged only (excluding longbow tank) - Player* longBowTank = GetNetherstrandLongbowTank(botAI, bot); + Player* debuffHunter = GetDebuffHunter(botAI, bot); Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); - if (botAI->IsRangedDps(bot) && (!longBowTank || bot != longBowTank) && + if (botAI->IsRangedDps(bot) && (!debuffHunter || bot != debuffHunter) && capernian && !capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && !capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) { @@ -1477,7 +1416,7 @@ bool KaelthasSunstriderAssignAdvisorDpsPriorityAction::Execute(Event event) return false; } - // Target priority 3: Sanguinar (longbow tank and melee move here after Thaladred) + // Target priority 3: Sanguinar (debuff hunter and melee move here after Thaladred) Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); if (sanguinar && !sanguinar->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && !sanguinar->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) @@ -1504,10 +1443,9 @@ bool KaelthasSunstriderAssignAdvisorDpsPriorityAction::Execute(Event event) if (botAI->IsMelee(bot) && botAI->IsDps(bot) && telonicus->GetVictim() != bot) { float maxMeleeRange = bot->GetMeleeRange(telonicus); - const float meleeRangeBuffer = 0.5f; - const float tolerance = 0.75f; + constexpr float tolerance = 0.25f; - float desiredDist = std::max(2.0f, maxMeleeRange - meleeRangeBuffer); + float desiredDist = std::max(2.0f, maxMeleeRange - tolerance); float currentDist = bot->GetExactDist2d(telonicus); if (fabs(currentDist - desiredDist) > tolerance) @@ -1576,10 +1514,10 @@ bool KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction::Execute(Event eve (botAI->IsAssistTank(bot) && (mace && mace->GetVictim() == bot || dagger && dagger->GetVictim() == bot || sword && sword->GetVictim() == bot))) { - const float safeDistance = botAI->IsAssistTank(bot) ? 15.0f : 10.0f; + const float safeDistance = botAI->IsAssistTank(bot) ? 17.0f : 13.0f; float currentDistance = bot->GetExactDist2d(axe); if (currentDistance < safeDistance) - return MoveAway(axe, safeDistance - currentDistance + 1.0f); + return MoveAway(axe, safeDistance - currentDistance); } } @@ -1629,9 +1567,10 @@ bool KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction::Execute(Event eve if (bot->GetTarget() != axe->GetGUID()) return Attack(axe); } - // Priority 6: Netherstrand Longbow (Cross--marked in other method by longbow tank) + // Priority 6: Netherstrand Longbow (Cross) else if (Unit* longbow = AI_VALUE2(Unit*, "find target", "netherstrand longbow")) { + MarkTargetWithCross(bot, longbow); SetRtiTarget(botAI, "cross", longbow); if (bot->GetTarget() != longbow->GetGUID()) @@ -1665,40 +1604,9 @@ bool KaelthasSunstriderMoveDevastationAwayAction::Execute(Event event) if (axe->GetVictim() == bot) { - const float safeDistance = 12.0f; - Unit* nearestPlayer = GetNearestNonTankPlayerInRadius(bot, safeDistance); - if (nearestPlayer) - { - float currentDistance = bot->GetExactDist2d(nearestPlayer); - return MoveFromGroup(safeDistance + 1.0f); - } - } - - return false; -} - -bool KaelthasSunstriderHunterTurnAwayNetherstrandLongbowAction::Execute(Event event) -{ - Unit* longbow = AI_VALUE2(Unit*, "find target", "netherstrand longbow"); - if (!longbow) - return false; - - MarkTargetWithCross(bot, longbow); - SetRtiTarget(botAI, "cross", longbow); - - if (bot->GetTarget() != longbow->GetGUID()) - return Attack(longbow); - - if (longbow->GetVictim() == bot) - { - const float dangerZone = 15.0f; - Unit* nearestPlayer = GetNearestNonTankPlayerInRadius(bot, dangerZone); - if (nearestPlayer) - { - float currentDistance = bot->GetExactDist2d(nearestPlayer); - if (currentDistance < dangerZone) - return MoveFromGroup(dangerZone); - } + constexpr float safeDistance = 13.0f; + if (GetNearestNonTankPlayerInRadius(bot, safeDistance)) + return MoveFromGroup(safeDistance); } return false; @@ -1710,18 +1618,17 @@ bool KaelthasSunstriderLootLegendaryWeaponsAction::Execute(Event event) { uint32 npcEntry; uint32 itemId; - const char* name; }; const WeaponInfo weapons[] = { - { NPC_NETHERSTRAND_LONGBOW, ITEM_NETHERSTRAND_LONGBOW, "netherstrand longbow" }, - { NPC_COSMIC_INFUSER, ITEM_COSMIC_INFUSER, "cosmic infuser" }, - { NPC_DEVASTATION, ITEM_DEVASTATION, "devastation" }, - { NPC_INFINITY_BLADES, ITEM_INFINITY_BLADE, "infinity blade" }, - { NPC_WARP_SLICER, ITEM_WARP_SLICER, "warp slicer" }, - { NPC_STAFF_OF_DISINTEGRATION, ITEM_STAFF_OF_DISINTEGRATION, "staff of disintegration" }, - { NPC_PHASESHIFT_BULWARK, ITEM_PHASESHIFT_BULWARK, "phaseshift bulwark" } + { NPC_NETHERSTRAND_LONGBOW, ITEM_NETHERSTRAND_LONGBOW }, + { NPC_COSMIC_INFUSER, ITEM_COSMIC_INFUSER }, + { NPC_DEVASTATION, ITEM_DEVASTATION }, + { NPC_INFINITY_BLADES, ITEM_INFINITY_BLADE }, + { NPC_WARP_SLICER, ITEM_WARP_SLICER }, + { NPC_STAFF_OF_DISINTEGRATION, ITEM_STAFF_OF_DISINTEGRATION }, + { NPC_PHASESHIFT_BULWARK, ITEM_PHASESHIFT_BULWARK } }; for (auto const& weapon : weapons) @@ -1729,9 +1636,19 @@ bool KaelthasSunstriderLootLegendaryWeaponsAction::Execute(Event event) if (ShouldBotLootWeapon(weapon.npcEntry)) { if (bot->HasItemCount(weapon.itemId, 1, false)) + { + EquipAction* equipAction = + dynamic_cast(botAI->GetAiObjectContext()->GetAction("equip")); + if (equipAction) + { + ItemIds ids; + ids.insert(weapon.itemId); + equipAction->EquipItems(ids); + } continue; + } - return LootWeapon(weapon.npcEntry, weapon.itemId, weapon.name); + return LootWeapon(weapon.npcEntry, weapon.itemId); } } @@ -1741,7 +1658,6 @@ bool KaelthasSunstriderLootLegendaryWeaponsAction::Execute(Event event) bool KaelthasSunstriderLootLegendaryWeaponsAction::ShouldBotLootWeapon(uint32 weaponEntry) { uint8 tab = AiFactory::GetPlayerSpecTab(bot); - switch (weaponEntry) { case NPC_NETHERSTRAND_LONGBOW: @@ -1751,7 +1667,7 @@ bool KaelthasSunstriderLootLegendaryWeaponsAction::ShouldBotLootWeapon(uint32 we return botAI->IsHeal(bot); // Fury Warriors could use the axe, but their DPS is terrible at appropriate gear levels - // So they're better off looting only the dagger to MH it and break MCs + // So IMO they're better off looting only the dagger to MH it and break MCs case NPC_DEVASTATION: return (bot->getClass() == CLASS_WARRIOR && tab == WARRIOR_TAB_ARMS) || (bot->getClass() == CLASS_PALADIN && tab == PALADIN_TAB_RETRIBUTION) || @@ -1785,10 +1701,10 @@ bool KaelthasSunstriderLootLegendaryWeaponsAction::ShouldBotLootWeapon(uint32 we } bool KaelthasSunstriderLootLegendaryWeaponsAction::LootWeapon( - uint32 weaponEntry, uint32 itemId, const char* weaponName) + uint32 weaponEntry, uint32 itemId) { auto const& corpses = context->GetValue("nearest corpses")->Get(); - const float maxLootRange = sPlayerbotAIConfig->lootDistance; + const float maxLootRange = sPlayerbotAIConfig.lootDistance; for (auto const& guid : corpses) { @@ -1822,9 +1738,9 @@ bool KaelthasSunstriderLootLegendaryWeaponsAction::LootWeapon( const ObjectGuid botGuid = bot->GetGUID(); const ObjectGuid corpseGuid = guid; - const uint8 weaponIndex = 0; + constexpr uint8 weaponIndex = 0; - botAI->AddTimedEvent([this, botGuid, corpseGuid, weaponIndex, itemId, weaponName]() + botAI->AddTimedEvent([this, botGuid, corpseGuid, weaponIndex, itemId]() { Player* receiver = botGuid.IsEmpty() ? nullptr : ObjectAccessor::FindPlayer(botGuid); if (!receiver || !receiver->IsInWorld()) @@ -1839,8 +1755,6 @@ bool KaelthasSunstriderLootLegendaryWeaponsAction::LootWeapon( *packet << weaponIndex; receiver->GetSession()->QueuePacket(packet); }, 600); - - botAI->DoSpecificAction("equip upgrade", Event(), true); return true; } @@ -1983,7 +1897,7 @@ bool KaelthasSunstriderAvoidFlameStrikeAction::Execute(Event event) if (flameStrikes.empty()) return false; - const float hazardRadius = 12.0f; + constexpr float hazardRadius = 12.0f; bool inDanger = false; for (Unit* flameStrike : flameStrikes) { @@ -2047,55 +1961,34 @@ bool KaelthasSunstriderHandlePhoenixesAndEggsAction::Execute(Event event) if (targetPhoenix->GetVictim() == bot) { - const float safeDistance = 10.0f; - - Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance); - if (Group* group = bot->GetGroup()) - { - if (nearestPlayer && group) - { - PlayerbotAI* nearestAI = GET_PLAYERBOT_AI(nearestPlayer->ToPlayer()); - if (nearestAI && - (nearestAI->IsAssistTankOfIndex(nearestPlayer->ToPlayer(), 0, true) || - nearestAI->IsAssistTankOfIndex(nearestPlayer->ToPlayer(), 1, true))) - { - nearestPlayer = nullptr; - } - } - } - - if (nearestPlayer) - { - float closestDist = bot->GetExactDist2d(nearestPlayer); - if (closestDist < safeDistance) - return MoveFromGroup(safeDistance + 2.0f); - } + constexpr float safeDistance = 12.0f; + if (GetNearestNonTankPlayerInRadius(bot, safeDistance)) + return MoveFromGroup(safeDistance); } return false; } else if (!botAI->IsTank(bot)) { - Unit* phoenix = AI_VALUE2(Unit*, "find target", "phoenix"); - if (!phoenix) - return false; - - float currentDistance = bot->GetExactDist2d(phoenix); - const float safeDistance = 10.0f; - if (currentDistance < safeDistance) - return MoveAway(phoenix, safeDistance - currentDistance + 2.0f); - } - else if (botAI->IsRangedDps(bot)) - { - Unit* phoenixEgg = AI_VALUE2(Unit*, "find target", "phoenix egg"); - if (!phoenixEgg) - return false; + if (Unit* phoenix = AI_VALUE2(Unit*, "find target", "phoenix")) + { + float currentDistance = bot->GetExactDist2d(phoenix); + constexpr float safeDistance = 12.0f; + if (currentDistance < safeDistance) + return MoveAway(phoenix, safeDistance - currentDistance); + } - MarkTargetWithDiamond(bot, phoenixEgg); - SetRtiTarget(botAI, "diamond", phoenixEgg); + if (botAI->IsDps(bot)) + { + if (Unit* phoenixEgg = GetFirstAliveUnitByEntry(botAI, NPC_PHOENIX_EGG)) + { + MarkTargetWithDiamond(bot, phoenixEgg); + SetRtiTarget(botAI, "diamond", phoenixEgg); - if (bot->GetTarget() != phoenixEgg->GetGUID()) - return Attack (phoenixEgg); + if (bot->GetTarget() != phoenixEgg->GetGUID()) + return Attack (phoenixEgg); + } + } } return false; @@ -2106,22 +1999,23 @@ bool KaelthasSunstriderBreakMindControlAction::Execute(Event event) Player* mcTarget = nullptr; float closestDist = std::numeric_limits::max(); - if (Group* group = bot->GetGroup()) + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || member == bot) - continue; + Player* member = ref->GetSource(); + if (!member || member == bot) + continue; - if (member->HasAura(SPELL_KAELTHAS_MIND_CONTROL)) + if (member->HasAura(SPELL_KAELTHAS_MIND_CONTROL)) + { + float dist = bot->GetExactDist2d(member); + if (dist < closestDist) { - float dist = bot->GetExactDist2d(member); - if (dist < closestDist) - { - closestDist = dist; - mcTarget = member; - } + closestDist = dist; + mcTarget = member; } } } @@ -2131,32 +2025,31 @@ bool KaelthasSunstriderBreakMindControlAction::Execute(Event event) if (!bot->IsWithinMeleeRange(mcTarget)) { - uint32 delay = urand(1500, 2500); // 1.5 to 2.5 seconds - float x = mcTarget->GetPositionX(); - float y = mcTarget->GetPositionY(); - float z = mcTarget->GetPositionZ(); - botAI->AddTimedEvent( - [this, x, y, z]() { - MoveTo(TEMPEST_KEEP_MAP_ID, x, y, z, false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); - }, - delay); - botAI->SetNextCheckDelay(delay + 50); - return true; + return MoveTo(TEMPEST_KEEP_MAP_ID, mcTarget->GetPositionX(), mcTarget->GetPositionY(), + mcTarget->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); } - static const std::array spells = + if (bot->getClass() == CLASS_ROGUE && + AiFactory::GetPlayerSpecTab(bot) != ROGUE_TAB_COMBAT) { - "hamstring", - "wing clip", - "shiv", - "stormstrike" - }; - - for (const char* spell : spells) + if (botAI->CanCastSpell("sinister strike", mcTarget)) + return botAI->CastSpell("sinister strike", mcTarget); + } + else { - if (botAI->CanCastSpell(spell, mcTarget)) - return botAI->CastSpell(spell, mcTarget); + static const std::array spells = + { + "hamstring", + "wing clip", + "shiv", + "stormstrike" + }; + for (const char* spell : spells) + { + if (botAI->CanCastSpell(spell, mcTarget)) + return botAI->CastSpell(spell, mcTarget); + } } return false; @@ -2182,7 +2075,6 @@ bool KaelthasSunstriderBreakThroughShockBarrierAction::Execute(Event event) "silencing shot", "wind shear", }; - for (const char* spell : spells) { if (botAI->CanCastSpell(spell, kaelthas)) @@ -2198,7 +2090,7 @@ bool KaelthasSunstriderBreakThroughShockBarrierAction::Execute(Event event) return false; } -// Bots immediately fall to the ground after Gravity Lapse, so this action +// Bots generally immediately fall to the ground after Gravity Lapse, so this action // name is kind of a misnomer (though bots are still in a flying state) bool KaelthasSunstriderSpreadOutInMidairAction::Execute(Event event) { @@ -2206,7 +2098,7 @@ bool KaelthasSunstriderSpreadOutInMidairAction::Execute(Event event) if (!group) return false; - const float minSpreadDistance = 16.0f; + constexpr float minSpreadDistance = 16.0f; std::vector nearbyPlayers; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) diff --git a/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h index 8665c028c00..bb30023d58c 100644 --- a/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h +++ b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h @@ -258,14 +258,6 @@ class KaelthasSunstriderCastFearWardOnSanguinarTankAction : public Action bool Execute(Event event) override; }; -class KaelthasSunstriderManageWarlockTankStrategyAction : public Action -{ -public: - KaelthasSunstriderManageWarlockTankStrategyAction( - PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider manage warlock tank strategy") : Action(botAI, name) {} - bool Execute(Event event) override; -}; - class KaelthasSunstriderWarlockTankPositionCapernianAction : public AttackAction { public: @@ -334,14 +326,6 @@ class KaelthasSunstriderMoveDevastationAwayAction : public AttackAction bool Execute(Event event) override; }; -class KaelthasSunstriderHunterTurnAwayNetherstrandLongbowAction : public AttackAction -{ -public: - KaelthasSunstriderHunterTurnAwayNetherstrandLongbowAction( - PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider hunter turn away netherstrand longbow") : AttackAction(botAI, name) {} - bool Execute(Event event) override; -}; - class KaelthasSunstriderLootLegendaryWeaponsAction : public MovementAction { public: @@ -351,7 +335,7 @@ class KaelthasSunstriderLootLegendaryWeaponsAction : public MovementAction private: bool ShouldBotLootWeapon(uint32 weaponEntry); - bool LootWeapon(uint32 weaponEntry, uint32 itemId, const char* weaponName); + bool LootWeapon(uint32 weaponEntry, uint32 itemId); }; class KaelthasSunstriderUseLegendaryWeaponsAction : public Action diff --git a/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp index 4d67dd3bd61..aa0cdba03c5 100644 --- a/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp +++ b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp @@ -173,8 +173,10 @@ float HighAstromancerSolarianDisableTankAssistMultiplier::GetValue(Action* actio if (!botAI->IsTank(bot)) return 1.0f; - if (AI_VALUE2(Unit*, "find target", "solarium priest") && - bot->GetVictim() != nullptr) + if (bot->GetVictim() == nullptr) + return 1.0f; + + if (AI_VALUE2(Unit*, "find target", "solarium priest")) { if (dynamic_cast(action)) return 0.0f; @@ -199,7 +201,7 @@ float KaelthasSunstriderWaitForDpsMultiplier::GetValue(Action* action) return 1.0f; const time_t now = std::time(nullptr); - const uint8 dpsWaitSeconds = 10; + constexpr uint8 dpsWaitSeconds = 10; auto it = advisorDpsWaitTimer.find(kaelthas->GetMap()->GetInstanceId()); if (it == advisorDpsWaitTimer.end() || (now - it->second) < dpsWaitSeconds) @@ -210,8 +212,7 @@ float KaelthasSunstriderWaitForDpsMultiplier::GetValue(Action* action) auto isAdvisorActive = [](Unit* advisor) { - return advisor && advisor->IsAlive() && - !advisor->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + return advisor && !advisor->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && !advisor->HasAura(SPELL_PERMANENT_FEIGN_DEATH); }; @@ -275,22 +276,6 @@ float KaelthasSunstriderControlMisdirectionMultiplier::GetValue(Action* action) return 1.0f; } -// For disabling Capernian Tank's Shadow Ward, which is part of the standard -// tank strategy for Warlocks (made for Twin Emps but useless here) -float KaelthasSunstriderDisableShadowWardMultiplier::GetValue(Action* action) -{ - if (bot->getClass() != CLASS_WARLOCK) - return 1.0f; - - if (AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) - { - if (dynamic_cast(action)) - return 0.0f; - } - - return 1.0f; -} - float KaelthasSunstriderManageTankTargetingMultiplier::GetValue(Action* action) { if (!botAI->IsTank(bot)) @@ -333,14 +318,13 @@ float KaelthasSunstriderManageTankTargetingMultiplier::GetValue(Action* action) return 0.0f; } - if (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR || - kaelAI->GetPhase() == PHASE_ALL_ADVISORS) + if (bot->GetVictim() == nullptr) + return 1.0f; + else if (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR || + kaelAI->GetPhase() == PHASE_ALL_ADVISORS) { - if (bot->GetVictim() != nullptr) - { - if (dynamic_cast(action)) - return 0.0f; - } + if (dynamic_cast(action)) + return 0.0f; } return 1.0f; @@ -409,42 +393,6 @@ float KaelthasSunstriderDelayCooldownsMultiplier::GetValue(Action* action) return 1.0f; } -float KaelthasSunstriderTryNonfatalBreakingOfMindControlMultiplier::GetValue(Action* action) -{ - if (botAI->IsTank(bot) || !bot->HasItemCount(ITEM_INFINITY_BLADE, 1, true)) - return 1.0f; - - if (!AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) - return 1.0f; - - Group* group = bot->GetGroup(); - if (!group) - return 1.0f; - - bool hasMindControlledPlayer = false; - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive()) - continue; - - if (member->HasAura(SPELL_KAELTHAS_MIND_CONTROL)) - { - hasMindControlledPlayer = true; - break; - } - } - - if (hasMindControlledPlayer) - { - if (dynamic_cast(action) && - !dynamic_cast(action)) - return 0.0f; - } - - return 1.0f; -} - float KaelthasSunstriderAllDpsOnBossDuringPyroblastMultiplier::GetValue(Action* action) { if (!botAI->IsDps(bot)) diff --git a/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h index 23e801d6453..ab0467db592 100644 --- a/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h +++ b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h @@ -99,14 +99,6 @@ class KaelthasSunstriderControlMisdirectionMultiplier : public Multiplier virtual float GetValue(Action* action); }; -class KaelthasSunstriderDisableShadowWardMultiplier : public Multiplier -{ -public: - KaelthasSunstriderDisableShadowWardMultiplier( - PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider disable shadow ward multiplier") {} - virtual float GetValue(Action* action); -}; - class KaelthasSunstriderManageTankTargetingMultiplier : public Multiplier { public: @@ -131,14 +123,6 @@ class KaelthasSunstriderDelayCooldownsMultiplier : public Multiplier virtual float GetValue(Action* action); }; -class KaelthasSunstriderTryNonfatalBreakingOfMindControlMultiplier : public Multiplier -{ -public: - KaelthasSunstriderTryNonfatalBreakingOfMindControlMultiplier( - PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider try nonfatal breaking of mind control multiplier") {} - virtual float GetValue(Action* action); -}; - class KaelthasSunstriderAllDpsOnBossDuringPyroblastMultiplier : public Multiplier { public: diff --git a/src/Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h b/src/Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h index 93160985a04..f20bd9f65c6 100644 --- a/src/Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h +++ b/src/Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h @@ -96,9 +96,6 @@ class RaidTempestKeepActionContext : public NamedObjectContext creators["kael'thas sunstrider cast fear ward on sanguinar tank"] = &RaidTempestKeepActionContext::kaelthas_sunstrider_cast_fear_ward_on_sanguinar_tank; - creators["kael'thas sunstrider manage warlock tank strategy"] = - &RaidTempestKeepActionContext::kaelthas_sunstrider_manage_warlock_tank_strategy; - creators["kael'thas sunstrider warlock tank position capernian"] = &RaidTempestKeepActionContext::kaelthas_sunstrider_warlock_tank_position_capernian; @@ -123,9 +120,6 @@ class RaidTempestKeepActionContext : public NamedObjectContext creators["kael'thas sunstrider main tank move devastation away"] = &RaidTempestKeepActionContext::kaelthas_sunstrider_main_tank_move_devastation_away; - creators["kael'thas sunstrider hunter turn away netherstrand longbow"] = - &RaidTempestKeepActionContext::kaelthas_sunstrider_hunter_turn_away_netherstrand_longbow; - creators["kael'thas sunstrider loot legendary weapons"] = &RaidTempestKeepActionContext::kaelthas_sunstrider_loot_legendary_weapons; @@ -239,9 +233,6 @@ class RaidTempestKeepActionContext : public NamedObjectContext static Action* kaelthas_sunstrider_main_tank_position_sanguinar( PlayerbotAI* botAI) { return new KaelthasSunstriderMainTankPositionSanguinarAction(botAI); } - static Action* kaelthas_sunstrider_manage_warlock_tank_strategy( - PlayerbotAI* botAI) { return new KaelthasSunstriderManageWarlockTankStrategyAction(botAI); } - static Action* kaelthas_sunstrider_cast_fear_ward_on_sanguinar_tank( PlayerbotAI* botAI) { return new KaelthasSunstriderCastFearWardOnSanguinarTankAction(botAI); } @@ -269,9 +260,6 @@ class RaidTempestKeepActionContext : public NamedObjectContext static Action* kaelthas_sunstrider_main_tank_move_devastation_away( PlayerbotAI* botAI) { return new KaelthasSunstriderMoveDevastationAwayAction(botAI); } - static Action* kaelthas_sunstrider_hunter_turn_away_netherstrand_longbow( - PlayerbotAI* botAI) { return new KaelthasSunstriderHunterTurnAwayNetherstrandLongbowAction(botAI); } - static Action* kaelthas_sunstrider_loot_legendary_weapons( PlayerbotAI* botAI) { return new KaelthasSunstriderLootLegendaryWeaponsAction(botAI); } diff --git a/src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h b/src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h index 659c03a5f5e..7fc85fe3b08 100644 --- a/src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h +++ b/src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h @@ -111,9 +111,6 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext creators["kael'thas sunstrider legendary axe casts whirlwind"] = &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_axe_casts_whirlwind; - creators["kael'thas sunstrider legendary bow casts multishot"] = - &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_bow_casts_multishot; - creators["kael'thas sunstrider legendary weapons are dead and lootable"] = &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_are_dead_and_lootable; @@ -242,9 +239,6 @@ class RaidTempestKeepTriggerContext : public NamedObjectContext static Trigger* kaelthas_sunstrider_legendary_axe_casts_whirlwind( PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger(botAI); } - static Trigger* kaelthas_sunstrider_legendary_bow_casts_multishot( - PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryBowCastsMultiShotTrigger(botAI); } - static Trigger* kaelthas_sunstrider_legendary_weapons_are_dead_and_lootable( PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger(botAI); } diff --git a/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp b/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp index 780d01b9b9e..65b9e8a9c38 100644 --- a/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp +++ b/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp @@ -85,7 +85,6 @@ void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) NextAction("kael'thas sunstrider cast fear ward on sanguinar tank", ACTION_RAID + 2) })); triggers.push_back(new TriggerNode("kael'thas sunstrider capernian should be tanked by a warlock", { - NextAction("kael'thas sunstrider manage warlock tank strategy", ACTION_EMERGENCY + 10), NextAction("kael'thas sunstrider warlock tank position capernian", ACTION_RAID + 1) })); triggers.push_back(new TriggerNode("kael'thas sunstrider capernian casts arcane burst and conflagration", { @@ -109,9 +108,6 @@ void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("kael'thas sunstrider legendary axe casts whirlwind", { NextAction("kael'thas sunstrider main tank move devastation away", ACTION_EMERGENCY + 1) })); - triggers.push_back(new TriggerNode("kael'thas sunstrider legendary bow casts multishot", { - NextAction("kael'thas sunstrider hunter turn away netherstrand longbow", ACTION_EMERGENCY + 1) })); - triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are dead and lootable", { NextAction("kael'thas sunstrider loot legendary weapons", ACTION_RAID + 1) })); @@ -158,11 +154,9 @@ void RaidTempestKeepStrategy::InitMultipliers(std::vector& multipli multipliers.push_back(new KaelthasSunstriderKiteThaladredMultiplier(botAI)); multipliers.push_back(new KaelthasSunstriderWaitForDpsMultiplier(botAI)); multipliers.push_back(new KaelthasSunstriderControlMisdirectionMultiplier(botAI)); - multipliers.push_back(new KaelthasSunstriderDisableShadowWardMultiplier(botAI)); multipliers.push_back(new KaelthasSunstriderManageTankTargetingMultiplier(botAI)); multipliers.push_back(new KaelthasSunstriderDisableDisperseMultiplier(botAI)); multipliers.push_back(new KaelthasSunstriderDelayCooldownsMultiplier(botAI)); - multipliers.push_back(new KaelthasSunstriderTryNonfatalBreakingOfMindControlMultiplier(botAI)); multipliers.push_back(new KaelthasSunstriderAllDpsOnBossDuringPyroblastMultiplier(botAI)); multipliers.push_back(new KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier(botAI)); } diff --git a/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp index 833374bcd97..84ed8bc27c1 100644 --- a/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp +++ b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp @@ -3,6 +3,7 @@ #include "RaidTempestKeepActions.h" #include "RaidTempestKeepKaelthasBossAI.h" #include "Playerbots.h" +#include "RaidBossHelpers.h" using namespace TempestKeepHelpers; @@ -55,13 +56,13 @@ bool AlarBossIsFlyingBetweenPlatformsTrigger::IsActive() bool AlarEmbersOfAlarExplodeUponDeathTrigger::IsActive() { return botAI->IsTank(bot) && - AI_VALUE2(Unit*, "find target", "ember of al'ar"); + AI_VALUE2(Unit*, "find target", "ember of alar"); } bool AlarKillingEmbersOfAlarDamagesBossTrigger::IsActive() { return botAI->IsRangedDps(bot) && - AI_VALUE2(Unit*, "find target", "ember of al'ar"); + AI_VALUE2(Unit*, "find target", "ember of alar"); } bool AlarIncomingFlameQuillsTrigger::IsActive() @@ -114,8 +115,8 @@ bool AlarPhase2EncounterIsAtRoomCenterTrigger::IsActive() bool AlarStrategyChangesBetweenPhasesTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "al'ar") && - IsInstanceTimerManager(botAI, bot); + return IsMechanicTrackerBot(botAI, bot, TEMPEST_KEEP_MAP_ID, nullptr) && + AI_VALUE2(Unit*, "find target", "al'ar"); } // Void Reaver @@ -205,14 +206,14 @@ bool HighAstromancerSolarianBossCastsPsychicScreamTrigger::IsActive() bool KaelthasSunstriderThaladredIsFixatedOnBotTrigger::IsActive() { - Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); - if (!thaladred || thaladred->GetVictim() != bot) - return false; - Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); if (!kaelthas) return false; + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + if (!thaladred || thaladred->GetVictim() != bot) + return false; + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); if (!kaelAI) return false; @@ -236,12 +237,12 @@ bool KaelthasSunstriderPullingTankableAdvisorsTrigger::IsActive() bool KaelthasSunstriderSanguinarEngagedByMainTankTrigger::IsActive() { - Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); - if (!sanguinar || sanguinar->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || - sanguinar->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + if (!botAI->IsMainTank(bot)) return false; - return botAI->IsMainTank(bot); + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + return sanguinar && !sanguinar->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !sanguinar->HasAura(SPELL_PERMANENT_FEIGN_DEATH); } bool KaelthasSunstriderSanguinarCastsBellowingRoarTrigger::IsActive() @@ -285,30 +286,32 @@ bool KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger::IsActive() if (bot->getClass() != CLASS_WARLOCK) return false; - if (!AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) + if (GetCapernianTank(botAI, bot) != bot) return false; - return GetCapernianTank(botAI, bot) == bot; + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + return capernian && !capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH); } bool KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger::IsActive() { - Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); - if (!capernian || capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || - capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + if (GetCapernianTank(botAI, bot) == bot) return false; - return GetCapernianTank(botAI, bot) != bot; + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + return capernian && !capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH); } bool KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger::IsActive() { - Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); - if (!telonicus || telonicus->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || - telonicus->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + if (!botAI->IsAssistTankOfIndex(bot, 0, true)) return false; - return botAI->IsAssistTankOfIndex(bot, 0, true); + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + return telonicus && !telonicus->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !telonicus->HasAura(SPELL_PERMANENT_FEIGN_DEATH); } bool KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger::IsActive() @@ -355,7 +358,7 @@ bool KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger::IsActive() if (!kaelthas) return false; - if (!IsInstanceTimerManager(botAI, bot)) + if (!IsMechanicTrackerBot(botAI, bot, TEMPEST_KEEP_MAP_ID, GetCapernianTank(botAI, bot))) return false; boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); @@ -368,7 +371,7 @@ bool KaelthasSunstriderLegendaryWeaponsAreAliveTrigger::IsActive() if (!kaelthas) return false; - if (botAI->IsMainTank(bot) || GetNetherstrandLongbowTank(botAI, bot) == bot) + if (botAI->IsMainTank(bot)) return false; boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); @@ -381,12 +384,6 @@ bool KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger::IsActive() botAI->IsMainTank(bot); } -bool KaelthasSunstriderLegendaryBowCastsMultiShotTrigger::IsActive() -{ - return AI_VALUE2(Unit*, "find target", "netherstrand longbow") && - GetNetherstrandLongbowTank(botAI, bot) == bot; -} - bool KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger::IsActive() { return AI_VALUE2(Unit*, "find target", "kael'thas sunstrider") && @@ -408,7 +405,7 @@ bool KaelthasSunstriderLegendaryWeaponsWereLostTrigger::IsActive() if (bot->GetMapId() != TEMPEST_KEEP_MAP_ID) return false; - const uint32 KAELTHAS_DB_GUID = 158218; + constexpr uint32 KAELTHAS_DB_GUID = 158218; Map* map = bot->GetMap(); if (!map) return false; diff --git a/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.h b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.h index 580f80e53cb..d3ac3664ebb 100644 --- a/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.h +++ b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.h @@ -271,14 +271,6 @@ class KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger : public Trigger bool IsActive() override; }; -class KaelthasSunstriderLegendaryBowCastsMultiShotTrigger : public Trigger -{ -public: - KaelthasSunstriderLegendaryBowCastsMultiShotTrigger( - PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary bow casts multi shot") {} - bool IsActive() override; -}; - class KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger : public Trigger { public: diff --git a/src/Ai/Raid/TempestKeep/RaidTempestKeepHelpers.cpp b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.cpp similarity index 78% rename from src/Ai/Raid/TempestKeep/RaidTempestKeepHelpers.cpp rename to src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.cpp index e638d606c54..20372c2304c 100644 --- a/src/Ai/Raid/TempestKeep/RaidTempestKeepHelpers.cpp +++ b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.cpp @@ -1,121 +1,11 @@ -#include - #include "RaidTempestKeepHelpers.h" #include "RaidTempestKeepActions.h" #include "LootObjectStack.h" #include "Playerbots.h" -#include "RtiTargetValue.h" namespace TempestKeepHelpers { - // General Helpers - - void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId) - { - if (!target) - return; - - if (Group* group = bot->GetGroup()) - { - ObjectGuid currentGuid = group->GetTargetIcon(iconId); - if (currentGuid != target->GetGUID()) - group->SetTargetIcon(iconId, bot->GetGUID(), target->GetGUID()); - } - } - - void MarkTargetWithSkull(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::skullIndex); - } - - void MarkTargetWithSquare(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::squareIndex); - } - - void MarkTargetWithStar(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::starIndex); - } - - void MarkTargetWithCircle(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::circleIndex); - } - - void MarkTargetWithTriangle(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::triangleIndex); - } - - void MarkTargetWithDiamond(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::diamondIndex); - } - - void MarkTargetWithCross(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::crossIndex); - } - - void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target) - { - std::string currentRti = botAI->GetAiObjectContext()->GetValue("rti")->Get(); - Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("rti target")->Get(); - - if (currentRti != rtiName || currentTarget != target) - { - botAI->GetAiObjectContext()->GetValue("rti")->Set(rtiName); - botAI->GetAiObjectContext()->GetValue("rti target")->Set(target); - } - } - - bool IsInstanceTimerManager(PlayerbotAI* botAI, Player* bot) - { - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !botAI->IsDps(member) || - !GET_PLAYERBOT_AI(member)) - continue; - - Player* capernianTank = GetCapernianTank(botAI, member); - if (capernianTank && capernianTank == member) - continue; - - return member == bot; - } - } - - return false; - } - - Unit* GetNearestPlayerInRadius(Player* bot, float radius) - { - Unit* nearestPlayer = nullptr; - float nearestDistance = radius; - - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref != nullptr; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || member == bot) - continue; - - float distance = bot->GetExactDist2d(member); - if (distance < nearestDistance) - { - nearestDistance = distance; - nearestPlayer = member; - } - } - } - - return nearestPlayer; - } + // General Unit* GetNearestNonTankPlayerInRadius(Player* bot, float radius) { @@ -166,10 +56,10 @@ namespace TempestKeepHelpers Position FindSafestNearbyPosition(Player* bot, const std::vector& hazards, float maxRadius, float hazardRadius, const Position* center) { - const float searchStep = M_PI / 8.0f; - const float minDistance = 2.0f; - const float maxDistance = 30.0f; - const float distanceStep = 1.0f; + constexpr float searchStep = M_PI / 8.0f; + constexpr float minDistance = 2.0f; + constexpr float maxDistance = 30.0f; + constexpr float distanceStep = 1.0f; Position bestPos; float minMoveDistance = std::numeric_limits::max(); @@ -227,7 +117,7 @@ namespace TempestKeepHelpers bool IsPathSafeFromHazards( const Position& start, const Position& end, const std::vector& hazards, float hazardRadius) { - const uint8 numChecks = 10; + constexpr uint8 numChecks = 10; float dx = end.GetPositionX() - start.GetPositionX(); float dy = end.GetPositionY() - start.GetPositionY(); @@ -446,9 +336,6 @@ namespace TempestKeepHelpers const Position TELONICUS_WAITING_POSITION = { 754.347f, 31.739f, 46.796f }; const Position ADVISOR_HEAL_POSITION = { 757.425f, 13.011f, 46.779f }; const Position CAPERNIAN_WAITING_POSITION = { 743.897f, -11.575f, 46.779f }; - const Position KAELTHAS_WEAPON_STACK_POSITION = { 775.296f, -0.822f, 48.729f }; - const Position KAELTHAS_AXE_TANK_POSITION = { 775.621f, 20.717f, 48.729f }; - const Position KAELTHAS_BOW_TANK_POSITION = { 777.713f, -28.857f, 48.729f }; const Position KAELTHAS_TANK_POSITION = { 799.390f, -0.837f, 48.729f }; std::unordered_map advisorDpsWaitTimer; @@ -470,10 +357,7 @@ namespace TempestKeepHelpers return member; } - // (2) Fall back to bot Warlock with highest HP - Player* highestHpWarlock = nullptr; - uint32 highestHp = 0; - + // (2) Fall back to first found bot Warlock for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); @@ -481,19 +365,15 @@ namespace TempestKeepHelpers member->getClass() != CLASS_WARLOCK) continue; - uint32 hp = member->GetMaxHealth(); - if (!highestHpWarlock || hp > highestHp) - { - highestHpWarlock = member; - highestHp = hp; - } + return member; } - // (3) Return the found Warlock tank, or nullptr if none found - return highestHpWarlock; + // (3) Return nullptr if none found + return nullptr; } - Player* GetNetherstrandLongbowTank(PlayerbotAI* botAI, Player* bot) + // One Hunter will start on Sanguinar in Phase 3 with Melee to apply Armor Disruption + Player* GetDebuffHunter(PlayerbotAI* botAI, Player* bot) { Group* group = bot->GetGroup(); if (!group) @@ -510,7 +390,7 @@ namespace TempestKeepHelpers return member; } - // (2) Fall back to any bot Hunter + // (2) Fall back to first found bot Hunter for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); diff --git a/src/Ai/Raid/TempestKeep/RaidTempestKeepHelpers.h b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.h similarity index 80% rename from src/Ai/Raid/TempestKeep/RaidTempestKeepHelpers.h rename to src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.h index 5b1d99d2e20..9d532a57758 100644 --- a/src/Ai/Raid/TempestKeep/RaidTempestKeepHelpers.h +++ b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.h @@ -53,10 +53,6 @@ namespace TempestKeepHelpers // Kael'thas Sunstrider NPC_KAELTHAS_SUNSTRIDER = 19622, - NPC_LORD_SANGUINAR = 20060, - NPC_GRAND_ASTROMANCER_CAPERNIAN = 20062, - NPC_MASTER_ENGINEER_TELONICUS = 20063, - NPC_THALADRED_THE_DARKENER = 20064, NPC_NETHERSTRAND_LONGBOW = 21268, NPC_DEVASTATION = 21269, NPC_COSMIC_INFUSER = 21270, @@ -66,6 +62,7 @@ namespace TempestKeepHelpers NPC_STAFF_OF_DISINTEGRATION = 21274, // NPC_NETHER_VAPOR = 21002, // Unimplemented in AC; method needed if fixed NPC_PHOENIX = 21362, + NPC_PHOENIX_EGG = 21364, NPC_FLAME_STRIKE_TRIGGER = 21369, }; @@ -82,31 +79,8 @@ namespace TempestKeepHelpers ITEM_NETHER_SPIKES = 30319, }; - enum AlarLocationIndex - { - PLATFORM_0_IDX, - PLATFORM_1_IDX, - PLATFORM_2_IDX, - PLATFORM_3_IDX, - POINT_QUILL_OR_DIVE_IDX, - POINT_MIDDLE_IDX, - LOCATION_NONE = -1 - }; - // General - const uint32 TEMPEST_KEEP_MAP_ID = 550; - void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId); - void MarkTargetWithSkull(Player* bot, Unit* target); - void MarkTargetWithSquare(Player* bot, Unit* target); - void MarkTargetWithStar(Player* bot, Unit* target); - void MarkTargetWithCircle(Player* bot, Unit* target); - void MarkTargetWithTriangle(Player* bot, Unit* target); - void MarkTargetWithDiamond(Player* bot, Unit* target); - void MarkTargetWithCross(Player* bot, Unit* target); - void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target); - bool IsInstanceTimerManager(PlayerbotAI* botAI, Player* bot); - Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry); - Unit* GetNearestPlayerInRadius(Player* bot, float radius); + constexpr uint32 TEMPEST_KEEP_MAP_ID = 550; Unit* GetNearestNonTankPlayerInRadius(Player* bot, float radius); std::vector GetAllHazardTriggers( PlayerbotAI* botAI, Player* bot, uint32 npcEntry, float maxSearchRadius); @@ -117,7 +91,17 @@ namespace TempestKeepHelpers float hazardRadius); // Al'ar - const float ALAR_BALCONY_Z = 17.0f; + enum AlarLocationIndex + { + PLATFORM_0_IDX, + PLATFORM_1_IDX, + PLATFORM_2_IDX, + PLATFORM_3_IDX, + POINT_QUILL_OR_DIVE_IDX, + POINT_MIDDLE_IDX, + LOCATION_NONE = -1 + }; + constexpr float ALAR_BALCONY_Z = 17.0f; extern const Position ALAR_PLATFORM_0; extern const Position ALAR_PLATFORM_1; extern const Position ALAR_PLATFORM_2; @@ -148,9 +132,6 @@ namespace TempestKeepHelpers extern std::unordered_map initialVoidReaverPositions; extern std::unordered_map hasReachedInitialVoidReaverPosition; - // High Astromancer Solarian - // N/A - // Kael'thas Sunstrider extern const Position SANGUINAR_TANK_POSITION; extern const Position SANGUINAR_WAITING_POSITION; @@ -158,13 +139,10 @@ namespace TempestKeepHelpers extern const Position TELONICUS_WAITING_POSITION; extern const Position CAPERNIAN_WAITING_POSITION; extern const Position ADVISOR_HEAL_POSITION; - extern const Position KAELTHAS_WEAPON_STACK_POSITION; - extern const Position KAELTHAS_AXE_TANK_POSITION; - extern const Position KAELTHAS_BOW_TANK_POSITION; extern const Position KAELTHAS_TANK_POSITION; extern std::unordered_map advisorDpsWaitTimer; Player* GetCapernianTank(PlayerbotAI* botAI, Player* bot); - Player* GetNetherstrandLongbowTank(PlayerbotAI* botAI, Player* bot); + Player* GetDebuffHunter(PlayerbotAI* botAI, Player* bot); bool IsAnyLegendaryWeaponDead(PlayerbotAI* botAI, Player* bot); bool HasEquippableItemForSlot(Player* bot, uint8 slot); bool HasEquippableOffhand(Player* bot); diff --git a/src/Ai/Raid/TempestKeep/RaidTempestKeepKaelthasBossAI.h b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepKaelthasBossAI.h similarity index 100% rename from src/Ai/Raid/TempestKeep/RaidTempestKeepKaelthasBossAI.h rename to src/Ai/Raid/TempestKeep/Util/RaidTempestKeepKaelthasBossAI.h From 6ed3f24ecb13158a5a856e2b5112266d5479799b Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 7 Feb 2026 16:34:15 +0100 Subject: [PATCH 22/29] Enforce test fix (#2122) CI/CD PR --------- Co-authored-by: Crow --- .github/workflows/check_pr_source.yml | 4 +++- .github/workflows/code_style.yml | 4 ++-- .github/workflows/core_build.yml | 4 ++-- .github/workflows/macos_build.yml | 4 ++-- .github/workflows/windows_build.yml | 4 ++-- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/check_pr_source.yml b/.github/workflows/check_pr_source.yml index 877ab85c739..5b354f6f74a 100644 --- a/.github/workflows/check_pr_source.yml +++ b/.github/workflows/check_pr_source.yml @@ -1,13 +1,15 @@ -name: Enforce test-staging → main +name: Enforce test-staging → master on: pull_request: branches: - master + - test-staging jobs: require-test-staging: runs-on: ubuntu-22.04 + if: github.event.pull_request.base.ref == 'master' steps: - name: Ensure PR source is test-staging run: | diff --git a/.github/workflows/code_style.yml b/.github/workflows/code_style.yml index 7b1688fb6db..83b78c39276 100644 --- a/.github/workflows/code_style.yml +++ b/.github/workflows/code_style.yml @@ -2,9 +2,9 @@ name: Codestyle on: push: - branches: [ "master" ] + branches: [ "master", "test-staging" ] pull_request: - branches: [ "master" ] + branches: [ "master", "test-staging" ] concurrency: group: "codestyle-${{ github.event.pull_request.number }}" diff --git a/.github/workflows/core_build.yml b/.github/workflows/core_build.yml index 2927716f125..ac6ee60d844 100644 --- a/.github/workflows/core_build.yml +++ b/.github/workflows/core_build.yml @@ -2,9 +2,9 @@ name: ubuntu-build on: push: - branches: [ "master" ] + branches: [ "master", "test-staging" ] pull_request: - branches: [ "master" ] + branches: [ "master", "test-staging" ] concurrency: group: "core-build-${{ github.event.pull_request.number }}" diff --git a/.github/workflows/macos_build.yml b/.github/workflows/macos_build.yml index 30a558c6398..ab4d83daa64 100644 --- a/.github/workflows/macos_build.yml +++ b/.github/workflows/macos_build.yml @@ -1,9 +1,9 @@ name: macos-build on: push: - branches: [ "master" ] + branches: [ "master", "test-staging" ] pull_request: - branches: [ "master" ] + branches: [ "master", "test-staging" ] concurrency: group: "macos-build-${{ github.event.pull_request.number }}" diff --git a/.github/workflows/windows_build.yml b/.github/workflows/windows_build.yml index 49678c97aa9..121d97751ff 100644 --- a/.github/workflows/windows_build.yml +++ b/.github/workflows/windows_build.yml @@ -1,9 +1,9 @@ name: windows-build on: push: - branches: [ "master" ] + branches: [ "master", "test-staging" ] pull_request: - branches: [ "master" ] + branches: [ "master", "test-staging" ] concurrency: group: "windows-build-${{ github.event.pull_request.number }}" From 79fb3a5bbc9e27752a3133a83d56a9c72dc44ed6 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 7 Feb 2026 17:53:55 +0100 Subject: [PATCH 23/29] - Fixed Oculus drake mounting --- .../Dungeon/Oculus/Action/OculusActions.cpp | 2 +- src/Bot/PlayerbotAI.cpp | 34 ++++++++++++++++--- src/Bot/PlayerbotAI.h | 3 +- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/Ai/Dungeon/Oculus/Action/OculusActions.cpp b/src/Ai/Dungeon/Oculus/Action/OculusActions.cpp index 414acc4bc90..bbb94d110a3 100644 --- a/src/Ai/Dungeon/Oculus/Action/OculusActions.cpp +++ b/src/Ai/Dungeon/Oculus/Action/OculusActions.cpp @@ -62,7 +62,7 @@ bool MountDrakeAction::Execute(Event event) break; } - std::vector players = botAI->GetPlayersInGroup(); + std::vector players = botAI->GetAllPlayersInGroup(); for (Player* player : players) { if (!player || !player->IsInWorld() || player->IsDuringRemoveFromWorld()) diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 437e62fc693..8d95981e406 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -2580,7 +2580,7 @@ std::string PlayerbotAI::GetLocalizedGameObjectName(uint32 entry) return name; } -std::vector PlayerbotAI::GetPlayersInGroup() +std::vector PlayerbotAI::GetRealPlayersInGroup() { std::vector members; @@ -2607,6 +2607,30 @@ std::vector PlayerbotAI::GetPlayersInGroup() return members; } +std::vector PlayerbotAI::GetAllPlayersInGroup() +{ + std::vector members; + + Group* group = bot->GetGroup(); + + if (!group) + return members; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + + if (!member) + { + continue; + } + + members.push_back(ref->GetSource()); + } + + return members; +} + bool PlayerbotAI::SayToGuild(const std::string& msg) { if (msg.empty()) @@ -2715,9 +2739,9 @@ bool PlayerbotAI::SayToParty(const std::string& msg) ChatHandler::BuildChatPacket(data, CHAT_MSG_PARTY, msg.c_str(), LANG_UNIVERSAL, CHAT_TAG_NONE, bot->GetGUID(), bot->GetName()); - for (auto reciever : GetPlayersInGroup()) + for (auto receiver : GetRealPlayersInGroup()) { - ServerFacade::instance().SendPacket(reciever, &data); + ServerFacade::instance().SendPacket(receiver, &data); } return true; @@ -2732,9 +2756,9 @@ bool PlayerbotAI::SayToRaid(const std::string& msg) ChatHandler::BuildChatPacket(data, CHAT_MSG_RAID, msg.c_str(), LANG_UNIVERSAL, CHAT_TAG_NONE, bot->GetGUID(), bot->GetName()); - for (auto reciever : GetPlayersInGroup()) + for (auto receiver : GetRealPlayersInGroup()) { - ServerFacade::instance().SendPacket(reciever, &data); + ServerFacade::instance().SendPacket(receiver, &data); } return true; diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index c2d4aeb75b0..3364b31ac5a 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -446,7 +446,8 @@ class PlayerbotAI : public PlayerbotAIBase GameObject* GetGameObject(ObjectGuid guid); // static GameObject* GetGameObject(GameObjectData const* gameObjectData); WorldObject* GetWorldObject(ObjectGuid guid); - std::vector GetPlayersInGroup(); + std::vector GetAllPlayersInGroup(); + std::vector GetRealPlayersInGroup(); const AreaTableEntry* GetCurrentArea(); const AreaTableEntry* GetCurrentZone(); static std::string GetLocalizedAreaName(const AreaTableEntry* entry); From a1b939ffc81c4aede267a3247b3a8c79a399859d Mon Sep 17 00:00:00 2001 From: crow Date: Sun, 8 Feb 2026 02:37:24 -0600 Subject: [PATCH 24/29] comment out/remove unused parameters --- .../Action/RaidTempestKeepActions.cpp | 98 +++++++++---------- .../Multiplier/RaidTempestKeepMultipliers.cpp | 2 +- .../Trigger/RaidTempestKeepTriggers.cpp | 8 +- .../Util/RaidTempestKeepHelpers.cpp | 4 +- .../TempestKeep/Util/RaidTempestKeepHelpers.h | 4 +- 5 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp index 60c3e833c42..6067cb9a1f3 100644 --- a/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp +++ b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp @@ -13,7 +13,7 @@ using namespace TempestKeepHelpers; // General -bool TempestKeepEraseTimersAndTrackersAction::Execute(Event event) +bool TempestKeepEraseTimersAndTrackersAction::Execute(Event /*event*/) { const ObjectGuid guid = bot->GetGUID(); const uint32 instanceId = bot->GetMap()->GetInstanceId(); @@ -37,7 +37,7 @@ bool TempestKeepEraseTimersAndTrackersAction::Execute(Event event) // Trash -bool CrimsonHandCenturionCastPolymorphAction::Execute(Event event) +bool CrimsonHandCenturionCastPolymorphAction::Execute(Event /*event*/) { Unit* centurion = AI_VALUE2(Unit*, "find target", "crimson hand centurion"); if (!centurion) @@ -62,7 +62,7 @@ bool CrimsonHandCenturionCastPolymorphAction::Execute(Event event) // Al'ar -bool AlarMisdirectBossToMainTankAction::Execute(Event event) +bool AlarMisdirectBossToMainTankAction::Execute(Event /*event*/) { Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); if (!alar) @@ -91,7 +91,7 @@ bool AlarMisdirectBossToMainTankAction::Execute(Event event) return false; } -bool AlarBossTanksMoveBetweenPlatformsAction::Execute(Event event) +bool AlarBossTanksMoveBetweenPlatformsAction::Execute(Event /*event*/) { if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0, true)) return false; @@ -190,7 +190,7 @@ bool AlarBossTanksMoveBetweenPlatformsAction::PositionAssistTank( return false; } -bool AlarMeleeDpsMoveBetweenPlatformsAction::Execute(Event event) +bool AlarMeleeDpsMoveBetweenPlatformsAction::Execute(Event /*event*/) { if (!botAI->IsMelee(bot) || !botAI->IsDps(bot)) return false; @@ -226,7 +226,7 @@ bool AlarMeleeDpsMoveBetweenPlatformsAction::Execute(Event event) return false; } -bool AlarRangedAndEmberTankMoveUnderPlatformsAction::Execute(Event event) +bool AlarRangedAndEmberTankMoveUnderPlatformsAction::Execute(Event /*event*/) { if (!botAI->IsRanged(bot) && !botAI->IsAssistTankOfIndex(bot, 1, true)) return false; @@ -274,7 +274,7 @@ bool AlarRangedAndEmberTankMoveUnderPlatformsAction::Execute(Event event) return false; } -bool AlarAssistTanksPickUpEmbersAction::Execute(Event event) +bool AlarAssistTanksPickUpEmbersAction::Execute(Event /*event*/) { if (!botAI->IsTank(bot)) return false; @@ -407,7 +407,7 @@ bool AlarAssistTanksPickUpEmbersAction::HandlePhase2Embers(Unit* alar) return false; } -bool AlarRangedDpsPrioritizeEmbersAction::Execute(Event event) +bool AlarRangedDpsPrioritizeEmbersAction::Execute(Event /*event*/) { auto [firstEmber, secondEmber] = GetFirstTwoEmbersOfAlar(botAI); @@ -450,7 +450,7 @@ bool AlarRangedDpsPrioritizeEmbersAction::Execute(Event event) } // Jump from platform during Flame Quills and wait at assigned position after landing -bool AlarJumpFromPlatformAction::Execute(Event event) +bool AlarJumpFromPlatformAction::Execute(Event /*event*/) { if (bot->GetPositionZ() > ALAR_BALCONY_Z) { @@ -509,7 +509,7 @@ bool AlarJumpFromPlatformAction::Execute(Event event) return false; } -bool AlarMoveAwayFromRebirthAction::Execute(Event event) +bool AlarMoveAwayFromRebirthAction::Execute(Event /*event*/) { Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); if (!alar) @@ -541,7 +541,7 @@ bool AlarMoveAwayFromRebirthAction::Execute(Event event) } // Main tank and first assist tank will swap tanking Al'ar when Melt Armor is applied -bool AlarSwapTanksOnBossAction::Execute(Event event) +bool AlarSwapTanksOnBossAction::Execute(Event /*event*/) { if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0, true)) return false; @@ -581,7 +581,7 @@ bool AlarSwapTanksOnBossAction::Execute(Event event) return false; } -bool AlarAvoidFlamePatchesAndDiveBombsAction::Execute(Event event) +bool AlarAvoidFlamePatchesAndDiveBombsAction::Execute(Event /*event*/) { Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); if (!alar) @@ -651,7 +651,7 @@ bool AlarAvoidFlamePatchesAndDiveBombsAction::HandleDiveBomb(Unit* alar) } // For Phase 2, ensure that bots don't get too far away and become inactive -bool AlarReturnToRoomCenterAction::Execute(Event event) +bool AlarReturnToRoomCenterAction::Execute(Event /*event*/) { const Position& center = ALAR_ROOM_CENTER; if (bot->GetVictim() == nullptr && @@ -664,7 +664,7 @@ bool AlarReturnToRoomCenterAction::Execute(Event event) return false; } -bool AlarManagePhaseTrackerAction::Execute(Event event) +bool AlarManagePhaseTrackerAction::Execute(Event /*event*/) { Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); if (!alar) @@ -692,7 +692,7 @@ bool AlarManagePhaseTrackerAction::Execute(Event event) // Void Reaver -bool VoidReaverTanksPositionBossAction::Execute(Event event) +bool VoidReaverTanksPositionBossAction::Execute(Event /*event*/) { Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); if (!voidReaver) @@ -718,7 +718,7 @@ bool VoidReaverTanksPositionBossAction::Execute(Event event) return false; } -bool VoidReaverRangedUseAggroDumpAbilityAction::Execute(Event event) +bool VoidReaverRangedUseAggroDumpAbilityAction::Execute(Event /*event*/) { botAI->Reset(); static const std::array spells = @@ -741,7 +741,7 @@ bool VoidReaverRangedUseAggroDumpAbilityAction::Execute(Event event) // As far as I can tell, it is not possible for bots to detect Arcane Orbs // Therefore, this spreads out the ranged bots so as few of them as possible get hit -bool VoidReaverSpreadRangedAction::Execute(Event event) +bool VoidReaverSpreadRangedAction::Execute(Event /*event*/) { Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); if (!voidReaver) @@ -826,7 +826,7 @@ Position VoidReaverSpreadRangedAction::GetRangedBotPosition(const Position& cent // High Astromancer Solarian -bool HighAstromancerSolarianRangedLeaveSpaceForMeleeAction::Execute(Event event) +bool HighAstromancerSolarianRangedLeaveSpaceForMeleeAction::Execute(Event /*event*/) { Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); if (!astromancer) @@ -840,7 +840,7 @@ bool HighAstromancerSolarianRangedLeaveSpaceForMeleeAction::Execute(Event event) return false; } -bool HighAstromancerSolarianMoveAwayFromGroupAction::Execute(Event event) +bool HighAstromancerSolarianMoveAwayFromGroupAction::Execute(Event /*event*/) { constexpr float safeDistance = 15.0f; if (GetNearestPlayerInRadius(bot, safeDistance)) @@ -852,7 +852,7 @@ bool HighAstromancerSolarianMoveAwayFromGroupAction::Execute(Event event) return false; } -bool HighAstromancerSolarianStackForAoeAction::Execute(Event event) +bool HighAstromancerSolarianStackForAoeAction::Execute(Event /*event*/) { Group* group = bot->GetGroup(); if (!group) @@ -880,7 +880,7 @@ bool HighAstromancerSolarianStackForAoeAction::Execute(Event event) } // Split melee into two groups, one on each Solarium Priest -bool HighAstromancerSolarianTargetSolariumPriestsAction::Execute(Event event) +bool HighAstromancerSolarianTargetSolariumPriestsAction::Execute(Event /*event*/) { auto priestsPair = GetSolariumPriests(botAI); if (!priestsPair.first || !priestsPair.second) @@ -983,7 +983,7 @@ Unit* HighAstromancerSolarianTargetSolariumPriestsAction::AssignSolariumPriestsT return priestsPair.second; } -bool HighAstromancerSolarianTankVoidwalkerAction::Execute(Event event) +bool HighAstromancerSolarianTankVoidwalkerAction::Execute(Event /*event*/) { Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); if (!astromancer) @@ -1005,7 +1005,7 @@ bool HighAstromancerSolarianTankVoidwalkerAction::Execute(Event event) return false; } -bool HighAstromancerSolarianCastFearWardOnMainTankAction::Execute(Event event) +bool HighAstromancerSolarianCastFearWardOnMainTankAction::Execute(Event /*event*/) { Player* mainTank = nullptr; if (Group* group = bot->GetGroup()) @@ -1029,7 +1029,7 @@ bool HighAstromancerSolarianCastFearWardOnMainTankAction::Execute(Event event) // Kael'thas Sunstrider -bool KaelthasSunstriderKiteThaladredAction::Execute(Event event) +bool KaelthasSunstriderKiteThaladredAction::Execute(Event /*event*/) { Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); if (!thaladred) @@ -1047,7 +1047,7 @@ bool KaelthasSunstriderKiteThaladredAction::Execute(Event event) } // Misdirect order: (1) Capernian, (2) Telonicus, (3) Capernian (again for good measure) -bool KaelthasSunstriderMisdirectAdvisorsToTanksAction::Execute(Event event) +bool KaelthasSunstriderMisdirectAdvisorsToTanksAction::Execute(Event /*event*/) { Group* group = bot->GetGroup(); if (!group) @@ -1081,7 +1081,7 @@ bool KaelthasSunstriderMisdirectAdvisorsToTanksAction::Execute(Event event) if (hunterIndex == 0) { advisorTarget = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); - tankTarget = GetCapernianTank(botAI, bot); + tankTarget = GetCapernianTank(bot); } else if (hunterIndex == 1) { @@ -1100,7 +1100,7 @@ bool KaelthasSunstriderMisdirectAdvisorsToTanksAction::Execute(Event event) else if (hunterIndex == 2) { advisorTarget = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); - tankTarget = GetCapernianTank(botAI, bot); + tankTarget = GetCapernianTank(bot); } if (!advisorTarget || @@ -1121,7 +1121,7 @@ bool KaelthasSunstriderMisdirectAdvisorsToTanksAction::Execute(Event event) return false; } -bool KaelthasSunstriderMainTankPositionSanguinarAction::Execute(Event event) +bool KaelthasSunstriderMainTankPositionSanguinarAction::Execute(Event /*event*/) { Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); if (!sanguinar) @@ -1155,7 +1155,7 @@ bool KaelthasSunstriderMainTankPositionSanguinarAction::Execute(Event event) return false; } -bool KaelthasSunstriderCastFearWardOnSanguinarTankAction::Execute(Event event) +bool KaelthasSunstriderCastFearWardOnSanguinarTankAction::Execute(Event /*event*/) { Group* group = bot->GetGroup(); if (!group) @@ -1178,7 +1178,7 @@ bool KaelthasSunstriderCastFearWardOnSanguinarTankAction::Execute(Event event) return false; } -bool KaelthasSunstriderWarlockTankPositionCapernianAction::Execute(Event event) +bool KaelthasSunstriderWarlockTankPositionCapernianAction::Execute(Event /*event*/) { Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); if (!capernian) @@ -1207,7 +1207,7 @@ bool KaelthasSunstriderWarlockTankPositionCapernianAction::Execute(Event event) return false; } -bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::Execute(Event event) +bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::Execute(Event /*event*/) { Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); if (!kaelthas) @@ -1306,7 +1306,7 @@ bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::StayBackFromCaperni return false; } -bool KaelthasSunstriderFirstAssistTankPositionTelonicusAction::Execute(Event event) +bool KaelthasSunstriderFirstAssistTankPositionTelonicusAction::Execute(Event /*event*/) { Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); if (!telonicus) @@ -1340,7 +1340,7 @@ bool KaelthasSunstriderFirstAssistTankPositionTelonicusAction::Execute(Event eve return false; } -bool KaelthasSunstriderHandleAdvisorRolesInPhase3Action::Execute(Event event) +bool KaelthasSunstriderHandleAdvisorRolesInPhase3Action::Execute(Event /*event*/) { const Position* movePosition = nullptr; if (botAI->IsAssistHealOfIndex(bot, 0, true)) @@ -1357,7 +1357,7 @@ bool KaelthasSunstriderHandleAdvisorRolesInPhase3Action::Execute(Event event) if (telonicus && telonicus->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) movePosition = &TELONICUS_WAITING_POSITION; } - else if (GetCapernianTank(botAI, bot) == bot) + else if (GetCapernianTank(bot) == bot) { Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); if (capernian && capernian->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) @@ -1377,15 +1377,15 @@ bool KaelthasSunstriderHandleAdvisorRolesInPhase3Action::Execute(Event event) return false; } -bool KaelthasSunstriderReequipGearAction::Execute(Event event) +bool KaelthasSunstriderReequipGearAction::Execute(Event /*event*/) { return botAI->DoSpecificAction("equip upgrade", Event(), true); } -bool KaelthasSunstriderAssignAdvisorDpsPriorityAction::Execute(Event event) +bool KaelthasSunstriderAssignAdvisorDpsPriorityAction::Execute(Event /*event*/) { // Target priority 1: Thaladred, except Capernian tank - Player* capernianTank = GetCapernianTank(botAI, bot); + Player* capernianTank = GetCapernianTank(bot); Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); if ((!capernianTank || bot != capernianTank) && thaladred && !thaladred->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && @@ -1401,7 +1401,7 @@ bool KaelthasSunstriderAssignAdvisorDpsPriorityAction::Execute(Event event) } // Target priority 2: Capernian for ranged only (excluding longbow tank) - Player* debuffHunter = GetDebuffHunter(botAI, bot); + Player* debuffHunter = GetDebuffHunter(bot); Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); if (botAI->IsRangedDps(bot) && (!debuffHunter || bot != debuffHunter) && @@ -1466,7 +1466,7 @@ bool KaelthasSunstriderAssignAdvisorDpsPriorityAction::Execute(Event event) return false; } -bool KaelthasSunstriderManageAdvisorDpsTimerAction::Execute(Event event) +bool KaelthasSunstriderManageAdvisorDpsTimerAction::Execute(Event /*event*/) { Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); if (!kaelthas) @@ -1497,7 +1497,7 @@ bool KaelthasSunstriderManageAdvisorDpsTimerAction::Execute(Event event) return false; } -bool KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction::Execute(Event event) +bool KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction::Execute(Event /*event*/) { if (botAI->IsAssistTank(bot)) SetRtiTarget(botAI, "moon", nullptr); @@ -1590,7 +1590,7 @@ bool KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction::Execute(Event eve return false; } -bool KaelthasSunstriderMoveDevastationAwayAction::Execute(Event event) +bool KaelthasSunstriderMoveDevastationAwayAction::Execute(Event /*event*/) { Unit* axe = AI_VALUE2(Unit*, "find target", "devastation"); if (!axe) @@ -1612,7 +1612,7 @@ bool KaelthasSunstriderMoveDevastationAwayAction::Execute(Event event) return false; } -bool KaelthasSunstriderLootLegendaryWeaponsAction::Execute(Event event) +bool KaelthasSunstriderLootLegendaryWeaponsAction::Execute(Event /*event*/) { struct WeaponInfo { @@ -1761,7 +1761,7 @@ bool KaelthasSunstriderLootLegendaryWeaponsAction::LootWeapon( return false; } -bool KaelthasSunstriderUseLegendaryWeaponsAction::Execute(Event event) +bool KaelthasSunstriderUseLegendaryWeaponsAction::Execute(Event /*event*/) { return UsePhaseshiftBulwark() || UseStaffOfDisintegration() || @@ -1853,7 +1853,7 @@ bool KaelthasSunstriderUseLegendaryWeaponsAction::UseEquippedItemWithPacket(Item return true; } -bool KaelthasSunstriderMainTankPositionBossAction::Execute(Event event) +bool KaelthasSunstriderMainTankPositionBossAction::Execute(Event /*event*/) { if (!botAI->IsMainTank(bot)) return false; @@ -1890,7 +1890,7 @@ bool KaelthasSunstriderMainTankPositionBossAction::Execute(Event event) return false; } -bool KaelthasSunstriderAvoidFlameStrikeAction::Execute(Event event) +bool KaelthasSunstriderAvoidFlameStrikeAction::Execute(Event /*event*/) { std::vector flameStrikes = GetAllHazardTriggers(botAI, bot, NPC_FLAME_STRIKE_TRIGGER, 40.0f); @@ -1919,7 +1919,7 @@ bool KaelthasSunstriderAvoidFlameStrikeAction::Execute(Event event) MovementPriority::MOVEMENT_COMBAT, true, false); } -bool KaelthasSunstriderHandlePhoenixesAndEggsAction::Execute(Event event) +bool KaelthasSunstriderHandlePhoenixesAndEggsAction::Execute(Event /*event*/) { if (botAI->IsAssistTankOfIndex(bot, 0, true) || botAI->IsAssistTankOfIndex(bot, 1, true)) @@ -1994,7 +1994,7 @@ bool KaelthasSunstriderHandlePhoenixesAndEggsAction::Execute(Event event) return false; } -bool KaelthasSunstriderBreakMindControlAction::Execute(Event event) +bool KaelthasSunstriderBreakMindControlAction::Execute(Event /*event*/) { Player* mcTarget = nullptr; float closestDist = std::numeric_limits::max(); @@ -2056,7 +2056,7 @@ bool KaelthasSunstriderBreakMindControlAction::Execute(Event event) } // Shock Barrier needs to be #1 focus, even if there is a Phoenix Egg up -bool KaelthasSunstriderBreakThroughShockBarrierAction::Execute(Event event) +bool KaelthasSunstriderBreakThroughShockBarrierAction::Execute(Event /*event*/) { Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); if (!kaelthas) @@ -2092,7 +2092,7 @@ bool KaelthasSunstriderBreakThroughShockBarrierAction::Execute(Event event) // Bots generally immediately fall to the ground after Gravity Lapse, so this action // name is kind of a misnomer (though bots are still in a flying state) -bool KaelthasSunstriderSpreadOutInMidairAction::Execute(Event event) +bool KaelthasSunstriderSpreadOutInMidairAction::Execute(Event /*event*/) { Group* group = bot->GetGroup(); if (!group) diff --git a/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp index aa0cdba03c5..b634cbfc9b9 100644 --- a/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp +++ b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp @@ -218,7 +218,7 @@ float KaelthasSunstriderWaitForDpsMultiplier::GetValue(Action* action) if ((isAdvisorActive(sanguinar) && !botAI->IsMainTank(bot)) || (isAdvisorActive(telonicus) && !botAI->IsAssistTankOfIndex(bot, 0, true)) || - (isAdvisorActive(capernian) && !botAI->IsMainTank(bot) && GetCapernianTank(botAI, bot) != bot)) + (isAdvisorActive(capernian) && !botAI->IsMainTank(bot) && GetCapernianTank(bot) != bot)) { if (dynamic_cast(action) || (dynamic_cast(action) && diff --git a/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp index 84ed8bc27c1..39103ab88bc 100644 --- a/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp +++ b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp @@ -286,7 +286,7 @@ bool KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger::IsActive() if (bot->getClass() != CLASS_WARLOCK) return false; - if (GetCapernianTank(botAI, bot) != bot) + if (GetCapernianTank(bot) != bot) return false; Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); @@ -296,7 +296,7 @@ bool KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger::IsActive() bool KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger::IsActive() { - if (GetCapernianTank(botAI, bot) == bot) + if (GetCapernianTank(bot) == bot) return false; Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); @@ -327,7 +327,7 @@ bool KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger::IsActive() if (!botAI->IsAssistHealOfIndex(bot, 0, true) && !botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0, true) && - GetCapernianTank(botAI, bot) != bot) + GetCapernianTank(bot) != bot) return false; boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); @@ -358,7 +358,7 @@ bool KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger::IsActive() if (!kaelthas) return false; - if (!IsMechanicTrackerBot(botAI, bot, TEMPEST_KEEP_MAP_ID, GetCapernianTank(botAI, bot))) + if (!IsMechanicTrackerBot(botAI, bot, TEMPEST_KEEP_MAP_ID, GetCapernianTank(bot))) return false; boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); diff --git a/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.cpp b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.cpp index 20372c2304c..5863928526e 100644 --- a/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.cpp +++ b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.cpp @@ -340,7 +340,7 @@ namespace TempestKeepHelpers std::unordered_map advisorDpsWaitTimer; - Player* GetCapernianTank(PlayerbotAI* botAI, Player* bot) + Player* GetCapernianTank(Player* bot) { Group* group = bot->GetGroup(); if (!group) @@ -373,7 +373,7 @@ namespace TempestKeepHelpers } // One Hunter will start on Sanguinar in Phase 3 with Melee to apply Armor Disruption - Player* GetDebuffHunter(PlayerbotAI* botAI, Player* bot) + Player* GetDebuffHunter(Player* bot) { Group* group = bot->GetGroup(); if (!group) diff --git a/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.h b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.h index 9d532a57758..521203cb8ce 100644 --- a/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.h +++ b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.h @@ -141,8 +141,8 @@ namespace TempestKeepHelpers extern const Position ADVISOR_HEAL_POSITION; extern const Position KAELTHAS_TANK_POSITION; extern std::unordered_map advisorDpsWaitTimer; - Player* GetCapernianTank(PlayerbotAI* botAI, Player* bot); - Player* GetDebuffHunter(PlayerbotAI* botAI, Player* bot); + Player* GetCapernianTank(Player* bot); + Player* GetDebuffHunter(Player* bot); bool IsAnyLegendaryWeaponDead(PlayerbotAI* botAI, Player* bot); bool HasEquippableItemForSlot(Player* bot, uint8 slot); bool HasEquippableOffhand(Player* bot); From 8585f10f48e470c495338fc5b237ca5cadcdcfaa Mon Sep 17 00:00:00 2001 From: Crow Date: Sun, 8 Feb 2026 05:31:23 -0600 Subject: [PATCH 25/29] Implement Serpentshrine Cavern Strategies (#1888) Edited: Below description of methods were brought up to date as of the PR coming off of draft. ### General I've starting leveraging, to the extent possible, an out-of-combat method to erase map keys. This is mostly useful for timers that need to start upon the pull because I dislike having to rely on a check for a boss to be at 100% HP (or 99.9% or whatever) because it can be unreliable sometimes. ### Trash Underbog Colossi: Some Colossi leave behind a lake of toxin when they die that quickly kills any player that is standing in it. The pool is a dynamic-object-generated AoE, and bots will not avoid it on their own (I think because the AoE is out of combat, plus the radius is much larger than the default avoidance radius in the config). The method does not require bots to be in combat, and simply gets bots to run out of the toxin. You will probably still get a couple of idiots who drink in the middle of it, but in my experience, the vast majority of the raid gets out, and healers that escape can easily keep up a couple of fools until they've drank to full. Greyheart Tidecallers: Bots will mark and destroy Water Elemental Totems immediately. ### Hydross the Unstable The strategy uses 2 tanks, with the main tank assigned to the frost phase and the 1st assistant tank assigned to the nature phase. - The main tank will tank the frost phase, and the first assistant tank will tank the nature phase. They each have designated spots and will wait at their spots twiddling their thumbs while Hydross is in the other phase. - Hunters will misdirect to the applicable tank upon the pull and after each phase change. - The phase change process begins 1 second after Hydross reaches 100% Marks. The current tank will begin moving to the next phase tank's spot for the next tank to take over as soon as Hydross transitions. - DPS is ordered to stop after Hydross reaches 100% Marks until 5 seconds after he transitions. - Bots will prioritize the elementals adds after every phase change, unless Hydross is under 10% HP, in which case they should ignore the adds and burn the boss. - Ranged bots should spread during the frost phase to mitigate the impact of Water Tombs. ### The Lurker Below - There is a designated spot for the main tank. - Ranged DPS will fan out over a 120-degree arc that is centered directly across from the tank spot (to try to spread to reduce Geyser damage while also keeping them behind Lurker). - When Spout begins, all bots will run around behind Lurker. The intent is to keep a distance with a radius of 20 or 21 yards and within 45 degrees (either side) of directly behind him. Movement is specifically tangential along an arc so bots don't run in front of Lurker. - Spout's duration is tracked by a timer. The mechanics of the spell itself are rather unique and don't involve a continuous cast or aura to track easily so I settled for the timer. - If you have 3 (or more) tanks, each of the first 3 tanks will be assigned to one of the 3 Coilfang Guardians during the submerge phase. ### Leotheras the Blind The fight is designed for a Warlock tank. You can choose the Warlock tank by giving a Warlock the Assistant flag. If you don't do that, your highest HP Warlock will be picked. Do NOT switch the Warlock tank to a co +tank strategy--the designated Warlock is hardcoded to spam Searing Pain on Demon Leo and otherwise will engage in normal DPS strategies. If you don't have a Warlock at all, the strategy has some methods built in to try to make things work as best as possible with a melee tank. - The Spellbinders get marked with skulls and killed in order. - There is no designated spot or designated tank for the human phase. Your tanks will fight for aggro. Ranged bots will attempt to keep some distance, and when Whirlwind starts, everybody will run away from Leotheras. - During the demon phase, your melee tanks should take a backseat to your Warlock tank, who will receive help in the form of Misdirection. Bots will get the hell away from the Warlock tank so the Warlock tank should be taking every Chaos Blast alone. - During the final phase, your regular tanks will tank Leotheras, and the Warlock tank will tank his Shadow. The melee tanks will attempt to separate Leotheras from his Shadow so bots can focus down Leotheras without getting hit with Chaos Blasts. - Bots will wait 5 seconds to DPS after every transition into human phase, 12 seconds to DPS after every transition into demon phase, and 8 seconds to DPS after the transition into the final phase. There is no waiting on DPS after Whirlwinds, even though it would be ideal. It's not a big deal to live without, and for various reasons, it would have been a pain in the ass to deal with. - Bots will save Bloodlust/Heroism until after Spellbinders are down. - To deal with the Inner Demons, I disabled DPS assist for bots who are targeted and force them to focus only on their Inner Demons. This is sufficient in my experience for all DPS bots and Protection Warriors and Paladins to kill their Inner Demons, even at 50% damage. Feral Tank Druids and Healers still need help, so the strategy hardcodes their actions while fighting Inner Demons. For example, Resto Druids are coded to shift out of Tree Form, cast Barkskin on themselves, and just spam Wrath until the Inner Demon is dead. There are no bot strategy changes used for this method. ### Fathom-Lord Karathress You will need 4 tanks. Your main tank will tank Karathress, and an assistant tank will tank each Fathom Guard. If you have fewer than 4 tanks, then the priority order for tank assignment will be Karathress, Caribdis, Sharkkis, and then Tidalvess. - Roughly, the tank spots are (1) for Karathress, near where he starts but closer to the ledge for LoS reasons, (2) for Sharkkis, North from his starting location on the other side of the ramp, (3) for Tidalvess, Northwest from his starting location near the pillar, and (4) for Caribdis, far to the West of her starting position, near the corner. - Note that the tanks will probably clip through the terrain a bit when going to their positions. This is due to me implementing a forced MoveTo to the tank position coordinates. There is something weird about the maps in Karathress's room, and the tanks will take some really screwed up paths without making them go directly to the exact coordinates. So this looks stupid but is necessary. - One healer will be assigned to heal the Caribdis tank. Because AC Playerbots does not yet have a focus heal strategy, this just means that such healer has a designated location near the Caribdis tank's location. This healer can be selected with the Assistant flag. - Hunters will misdirect the Fathom Guards onto their applicable tanks. If you don't have three Hunters, the priority is Caribdis, Tidalvess, then Sharkkis. - DPS will wait 12 seconds to begin attacking. After that, they will prioritize targets as follows: - (1): Melee will always prioritize Spitfire Totems as soon as they spawn. This will continue through the duration of the fight. - (2): All bots will kill Tidalvess first. - (3): Melee bots will move to Sharkkis, and ranged bots will move to Caribdis. I understand this is not the standard kill order for players, which would have the entire raid kill Sharkkis next. The reasons I have done this differently are because melee DPS is much stronger with 3.3.5 talents vs. in retail TBC, and because bots get really thrown off by Cyclones and therefore they struggle to kill Caribdis quickly. You do not want Karathress below 75% HP before all Fathom-Guards are dead or he gets a huge damage buff. - (4) If Caribdis dies first, ranged bots will help with Sharkkis. - (5) Everybody kills Sharkkis's pet. - (6) Everybody kills Karathress. ### Morogrim Tidewalker - The main tank will pull the boss to the Northeast pillar, with the tank's back against the pillar. - A hunter will misdirect the boss onto the main tank upon the pull. - When the boss gets to 26% HP, the main tank will begin moving the boss to the Northeast corner of the room in preparation for Phase 2 (which begins at 25%). The tank will move in two steps to get around the pillar. - When the boss gets to 25% HP, ranged will follow the main tank to the corner and stack up right behind the boss. They will also move in two steps. - There is no method for melee since they will just naturally follow the boss anyway. ### Lady Vashj **Phase 1**: - The main tank will tank Vashj in the center of the arena. - If a Shaman is in the main tank's group, that Shaman will attempt to keep a Grounding Totem down in range of the main tank to absorb Shock Blast. This should continue in Phase 3. - Ranged bots will spread out in a semicircle around the center of the arena. - If any bot other than the main tank gets Static Charge, it will run away from other bots. If the main tank gets Static Charge, other bots will run away from the main tank. This method should continue in Phase 3. - If any bot is Entangled and has Static Charge, the bot will attempt to use Cloak of Shadows if it is a Rogue, and Paladins will attempt to use Hand of Freedom. This method should continue in Phase 3 (with some modifications). - Bots will not use Bloodlust or Heroism (saved for Phase 3). Bots will not use any other major cooldowns, either, such as Metamorphosis (saved for Phase 2 and 3). **Phase 2**: There are two central mechanics to this phase, both of which were challenging to get bots to execute properly. First is the system of prioritizing adds. The large playing field and multiple types of adds coming from random directions make this phase not doable with realistic DPS under the standard Playerbots target selection system. Therefore, I took inspiration from liyunfan's Naxx strategy for Phase 1 of Kel'Thuzad to disable dps assist and create a custom target selection system. First, a cheat with respect to the Coilfang Striders: - Tanks will permanently have the Fear Ward aura applied to them if you have raid cheats enabled. This allows them to tank the Coilfang Striders. The standard strategy was to have an Elemental Shaman kite the Strider around the perimeter of the arena, with ranged players (including healers) spamming DoTs on the Strider. If you can make bots do this, then great, but it's far beyond my capabilities. Therefore, with the cheat, the first assistant tank is responsible for tanking Striders and keeping them away from Core passers (described below) and Vashj. Evidently it was (and is, in TBC Classic) possible to tank (and melee DPS) Striders by wearing a Dire Maul Ogre Suit, which would give you enough reach to stay out of the Strider's fear. I actually tried that, and it does not work, either because AC's radiuses are not the same or just because bots do not maintain the same level of precise positioning. But anyway, the point is that technically the Striders are tankable by real players, so maybe that will make you feel better about using this cheat (it's fine enough rationalizing for me). I found this fight to be unmanageable without this cheat (i.e., using a method that would only have bots try to run away from Striders) because each Strider was guaranteed to wipe out a couple of bots, and you really cannot afford to lose anyone. YMMV though. - If cheats are enabled for Striders, Hunters will attempt to Misdirect the Striders to the first assist tank. - If cheats are not enabled, bots will attempt to use slows/roots to stop the Striders. I have some logic for them to use Netherweave Nets, but I suspect it does not actually work so I may remove it instead of trying to get it to function properly. Target priority is as follows: - Hunters and Mages: Enchanted Elementals, Coilfang Striders, Coilfang Elites. - Other Ranged Bots: Elites, Striders, Elementals. - Melee DPS: Elementals, Elites. - Tanks: Elites, Elementals (except if cheats are enabled, the first assistant tank will instead prioritize Striders and then Elementals) - Everybody else (basically means healers): Elementals, Elites, Striders - If there is more than one of the same target, bots will prioritize the one that is closer to Vashj. - In all cases, the valid attack ranged is limited so that bots should not leave the central platform. - If somehow a bot ends up too far from the center of the room and is not actively attacking anything, there is logic to make them run back. Handling Tainted Elementals and the Tainted Core: I will make another post about this later. It is easily the most complicated strategy I've ever worked on (far beyond anything on Kael'thas even) so will necessitate a long explanation. The tl;dr is that there is a chain of two-to-four bots that receive/pass the Tainted Core before using it on a Shield Generator, and if you are playing by yourself, you probably need to turn raid cheats on, in which case there will also be a bot that teleports to, kills, and loots the Tainted Elementals (i.e., the bots will then handle the entire sequence of shutting down Shield Generators). **Phase 3**: - The main tank will pick up Vashj immediately and try to keep her away from Enchanted Elementals. - DPS will burn down residual adds from Phase 2 in the order of (1) elementals, (2) strider for ranged only (if you have more than one up, you're dead), and (3) elites (hopefully you have only one up, but two with one almost dead is possible). - Hunters will kill Toxic Sporebats. This works quite well, but they (and anybody else if ordered to target Sporebats) have a tendency to levitate up into the pipes at the top of the room when killing the Sporebats. To counteract this, a method forcibly teleports bots to the ground if they get more than 2 yards above the ground. - The Phase 1 Cloak of Shadows/Hand of Freedom method is now expanded to include bots Entangled in the Sporebat poison pools (with Hand of Freedom usage prioritized on the main tank). - There is a specific method to avoid the Sporebat poison pools. The Vashj tank will move backwards when avoiding poison. --------- Co-authored-by: kadeshar --- conf/playerbots.conf.dist | 2 +- src/Ai/Raid/RaidStrategyContext.h | 3 + .../Action/RaidSSCActions.cpp | 3128 +++++++++++++++++ .../Action/RaidSSCActions.h | 457 +++ .../Multiplier/RaidSSCMultipliers.cpp | 799 +++++ .../Multiplier/RaidSSCMultipliers.h | 236 ++ .../RaidSSCActionContext.h | 337 ++ .../RaidSSCTriggerContext.h | 325 ++ .../Strategy/RaidSSCStrategy.cpp | 206 ++ .../Strategy/RaidSSCStrategy.h | 18 + .../Trigger/RaidSSCTriggers.cpp | 670 ++++ .../Trigger/RaidSSCTriggers.h | 414 +++ .../Util/RaidSSCHelpers.cpp | 583 +++ .../SerpentshrineCavern/Util/RaidSSCHelpers.h | 189 + src/Bot/Engine/AiObjectContext.cpp | 4 + src/Bot/PlayerbotAI.cpp | 3 + 16 files changed, 7373 insertions(+), 1 deletion(-) create mode 100644 src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.cpp create mode 100644 src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h create mode 100644 src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.cpp create mode 100644 src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.h create mode 100644 src/Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h create mode 100644 src/Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h create mode 100644 src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.cpp create mode 100644 src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.h create mode 100644 src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.cpp create mode 100644 src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.h create mode 100644 src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.cpp create mode 100644 src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.h diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 90737731672..18a6addf6d7 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 3c7971fb6f4..4a040985eb4 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 @@ class RaidStrategyContext : public NamedObjectContext 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 @@ class RaidStrategyContext : public NamedObjectContext 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 00000000000..7aa3eda35bd --- /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 00000000000..cbd23740210 --- /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 00000000000..c99cafa3c7f --- /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 00000000000..6630dc2060c --- /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 00000000000..e6dce16949d --- /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 00000000000..13135bf3e51 --- /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 00000000000..139667dc668 --- /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 00000000000..3c2c05f586e --- /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 00000000000..e77e63642ed --- /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 00000000000..e106b58f3ee --- /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 00000000000..7bda085be12 --- /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 00000000000..a725e28fcd4 --- /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 250a3929697..3a2037a0827 100644 --- a/src/Bot/Engine/AiObjectContext.cpp +++ b/src/Bot/Engine/AiObjectContext.cpp @@ -41,6 +41,8 @@ #include "Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h" #include "Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h" #include "Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h" +#include "Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h" +#include "Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h" #include "Ai/Raid/EyeOfEternity/RaidEoEActionContext.h" #include "Ai/Raid/EyeOfEternity/RaidEoETriggerContext.h" #include "Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h" @@ -115,6 +117,7 @@ void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextList Date: Sun, 8 Feb 2026 03:41:33 -0800 Subject: [PATCH 26/29] Refactor of EquipActions (#1994) #PR Description The root cause of issue #1987 was the AI Value item usage becoming a very expensive call when bots gained professions accidentally. My original approach was to eliminate it entirely, but after inputs and testing I decided to introduce a more focused Ai value "Item upgrade" that only checks equipment and ammo inheriting directly from item usage, so the logic is unified between them. Upgrades are now only assessed when receiving an item that can be equipped. Additionally, I noticed that winning loot rolls did not trigger the upgrade action, so I added a new package handler for that. Performance needs to be re-evaluated, but I expect a reduction in calls and in the cost of each call. I tested with bots and selfbot in deadmines and ahadowfang keep. --------- Co-authored-by: bashermens <31279994+hermensbas@users.noreply.github.com> --- conf/playerbots.conf.dist | 2 +- src/Ai/Base/Actions/EquipAction.cpp | 98 ++++----- src/Ai/Base/Actions/EquipAction.h | 8 +- src/Ai/Base/ChatActionContext.h | 3 +- .../Strategy/WorldPacketHandlerStrategy.cpp | 1 + src/Ai/Base/Value/ItemUsageValue.cpp | 186 ++++++++++-------- src/Ai/Base/Value/ItemUsageValue.h | 22 ++- src/Ai/Base/ValueContext.h | 2 + src/Ai/Base/WorldPacketTriggerContext.h | 2 + src/Bot/PlayerbotAI.cpp | 1 + 10 files changed, 176 insertions(+), 149 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 18a6addf6d7..a2f06885c17 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -2182,4 +2182,4 @@ AiPlayerbot.SummonAtInnkeepersEnabled = 1 # 30% more damage, 40% damage reduction (tank bots), increased all resistances, reduced threat for non tank bots, increased threat for tank bots. # Buffs will be applied on PP, Sindragosa and Lich King -AiPlayerbot.EnableICCBuffs = 1 +AiPlayerbot.EnableICCBuffs = 1 \ No newline at end of file diff --git a/src/Ai/Base/Actions/EquipAction.cpp b/src/Ai/Base/Actions/EquipAction.cpp index 32508ef2e52..a8b262692b0 100644 --- a/src/Ai/Base/Actions/EquipAction.cpp +++ b/src/Ai/Base/Actions/EquipAction.cpp @@ -328,7 +328,43 @@ void EquipAction::EquipItem(Item* item) botAI->TellMaster(out); } -bool EquipUpgradesAction::Execute(Event event) +ItemIds EquipAction::SelectInventoryItemsToEquip() +{ + CollectItemsVisitor visitor; + IterateItems(&visitor, ITERATE_ITEMS_IN_BAGS); + + ItemIds items; + for (auto i = visitor.items.begin(); i != visitor.items.end(); ++i) + { + Item* item = *i; + if (!item) + continue; + + ItemTemplate const* itemTemplate = item->GetTemplate(); + if (!itemTemplate) + continue; + + //TODO Expand to Glyphs and Gems, that can be placed in equipment + //Pre-filter non-equipable items + if (itemTemplate->InventoryType == INVTYPE_NON_EQUIP) + continue; + + int32 randomProperty = item->GetItemRandomPropertyId(); + uint32 itemId = item->GetTemplate()->ItemId; + std::string itemUsageParam; + if (randomProperty != 0) + itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty); + else + itemUsageParam = std::to_string(itemId); + + ItemUsage usage = AI_VALUE2(ItemUsage, "item upgrade", itemUsageParam); + if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_BAD_EQUIP) + items.insert(itemId); + } + return items; +} + +bool EquipUpgradesTriggeredAction::Execute(Event event) { if (!sPlayerbotAIConfig.autoEquipUpgradeLoot && !sRandomPlayerbotMgr.IsRandomBot(bot)) return false; @@ -361,72 +397,18 @@ bool EquipUpgradesAction::Execute(Event event) p >> itemId; ItemTemplate const* item = sObjectMgr->GetItemTemplate(itemId); - if (item->Class == ITEM_CLASS_TRADE_GOODS && item->SubClass == ITEM_SUBCLASS_MEAT) + if (item->InventoryType == INVTYPE_NON_EQUIP) return false; } - CollectItemsVisitor visitor; - IterateItems(&visitor, ITERATE_ITEMS_IN_BAGS); - - ItemIds items; - for (auto i = visitor.items.begin(); i != visitor.items.end(); ++i) - { - Item* item = *i; - if (!item) - break; - int32 randomProperty = item->GetItemRandomPropertyId(); - uint32 itemId = item->GetTemplate()->ItemId; - std::string itemUsageParam; - if (randomProperty != 0) - { - itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty); - } - else - { - itemUsageParam = std::to_string(itemId); - } - ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemUsageParam); - - if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_BAD_EQUIP) - { - items.insert(itemId); - } - } - + ItemIds items = SelectInventoryItemsToEquip(); EquipItems(items); return true; } bool EquipUpgradeAction::Execute(Event event) { - CollectItemsVisitor visitor; - IterateItems(&visitor, ITERATE_ITEMS_IN_BAGS); - - ItemIds items; - for (auto i = visitor.items.begin(); i != visitor.items.end(); ++i) - { - Item* item = *i; - if (!item) - break; - int32 randomProperty = item->GetItemRandomPropertyId(); - uint32 itemId = item->GetTemplate()->ItemId; - std::string itemUsageParam; - if (randomProperty != 0) - { - itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty); - } - else - { - itemUsageParam = std::to_string(itemId); - } - ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemUsageParam); - - if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_BAD_EQUIP) - { - items.insert(itemId); - } - } - + ItemIds items = SelectInventoryItemsToEquip(); EquipItems(items); return true; } diff --git a/src/Ai/Base/Actions/EquipAction.h b/src/Ai/Base/Actions/EquipAction.h index 518d39c8ee4..4f84f942bd9 100644 --- a/src/Ai/Base/Actions/EquipAction.h +++ b/src/Ai/Base/Actions/EquipAction.h @@ -8,6 +8,7 @@ #include "ChatHelper.h" #include "InventoryAction.h" +#include "Item.h" class FindItemVisitor; class Item; @@ -20,6 +21,7 @@ class EquipAction : public InventoryAction bool Execute(Event event) override; void EquipItems(ItemIds ids); + ItemIds SelectInventoryItemsToEquip(); private: void EquipItem(FindItemVisitor* visitor); @@ -27,10 +29,10 @@ class EquipAction : public InventoryAction void EquipItem(Item* item); }; -class EquipUpgradesAction : public EquipAction +class EquipUpgradesTriggeredAction : public EquipAction { public: - EquipUpgradesAction(PlayerbotAI* botAI, std::string const name = "equip upgrades") : EquipAction(botAI, name) {} + explicit EquipUpgradesTriggeredAction(PlayerbotAI* botAI, std::string const name = "equip upgrades") : EquipAction(botAI, name) {} bool Execute(Event event) override; }; @@ -38,7 +40,7 @@ class EquipUpgradesAction : public EquipAction class EquipUpgradeAction : public EquipAction { public: - EquipUpgradeAction(PlayerbotAI* botAI, std::string const name = "equip upgrade") : EquipAction(botAI, name) {} + explicit EquipUpgradeAction(PlayerbotAI* botAI, std::string const name = "equip upgrade") : EquipAction(botAI, name) {} bool Execute(Event event) override; }; diff --git a/src/Ai/Base/ChatActionContext.h b/src/Ai/Base/ChatActionContext.h index 5e215e1237e..8e4ed9ccf84 100644 --- a/src/Ai/Base/ChatActionContext.h +++ b/src/Ai/Base/ChatActionContext.h @@ -120,7 +120,7 @@ class ChatActionContext : public NamedObjectContext creators["use"] = &ChatActionContext::use; creators["item count"] = &ChatActionContext::item_count; creators["equip"] = &ChatActionContext::equip; - creators["equip upgrades"] = &ChatActionContext::equip_upgrades; + creators["equip upgrades"] = &ChatActionContext::equip_upgrade; creators["unequip"] = &ChatActionContext::unequip; creators["sell"] = &ChatActionContext::sell; creators["buy"] = &ChatActionContext::buy; @@ -258,7 +258,6 @@ class ChatActionContext : public NamedObjectContext static Action* talents(PlayerbotAI* botAI) { return new ChangeTalentsAction(botAI); } static Action* equip(PlayerbotAI* botAI) { return new EquipAction(botAI); } - static Action* equip_upgrades(PlayerbotAI* botAI) { return new EquipUpgradesAction(botAI); } static Action* unequip(PlayerbotAI* botAI) { return new UnequipAction(botAI); } static Action* sell(PlayerbotAI* botAI) { return new SellAction(botAI); } static Action* buy(PlayerbotAI* botAI) { return new BuyAction(botAI); } diff --git a/src/Ai/Base/Strategy/WorldPacketHandlerStrategy.cpp b/src/Ai/Base/Strategy/WorldPacketHandlerStrategy.cpp index cb37d271790..2debc44cbd8 100644 --- a/src/Ai/Base/Strategy/WorldPacketHandlerStrategy.cpp +++ b/src/Ai/Base/Strategy/WorldPacketHandlerStrategy.cpp @@ -42,6 +42,7 @@ void WorldPacketHandlerStrategy::InitTriggers(std::vector& trigger NextAction("query item usage", relevance), NextAction("equip upgrades", relevance) })); triggers.push_back(new TriggerNode("item push result", { NextAction("quest item push result", relevance) })); + triggers.push_back(new TriggerNode("loot roll won", { NextAction("equip upgrades", relevance) })); triggers.push_back(new TriggerNode("ready check finished", { NextAction("finish ready check", relevance) })); // triggers.push_back(new TriggerNode("often", { NextAction("security check", relevance), NextAction("check mail", relevance) })); triggers.push_back(new TriggerNode("guild invite", { NextAction("guild accept", relevance) })); diff --git a/src/Ai/Base/Value/ItemUsageValue.cpp b/src/Ai/Base/Value/ItemUsageValue.cpp index 6f48fa97395..c6183e30fd8 100644 --- a/src/Ai/Base/Value/ItemUsageValue.cpp +++ b/src/Ai/Base/Value/ItemUsageValue.cpp @@ -19,19 +19,9 @@ ItemUsage ItemUsageValue::Calculate() { - uint32 itemId = 0; - uint32 randomPropertyId = 0; - size_t pos = qualifier.find(","); - if (pos != std::string::npos) - { - itemId = atoi(qualifier.substr(0, pos).c_str()); - randomPropertyId = atoi(qualifier.substr(pos + 1).c_str()); - } - else - { - itemId = atoi(qualifier.c_str()); - } - + ParsedItemUsage const parsed = GetItemIdFromQualifier(); + uint32 itemId = parsed.itemId; + uint32 randomPropertyId = parsed.randomPropertyId; if (!itemId) return ITEM_USAGE_NONE; @@ -142,96 +132,30 @@ ItemUsage ItemUsageValue::Calculate() // If the loot is from an item in the bot’s bags, ignore syncQuestWithPlayer if (isLootFromItem && botNeedsItemForQuest) - { return ITEM_USAGE_QUEST; - } // If the bot is NOT acting alone and the master needs this quest item, defer to the master if (!isSelfBot && masterNeedsItemForQuest) - { return ITEM_USAGE_NONE; - } // If the bot itself needs the item for a quest, allow looting if (botNeedsItemForQuest) - { return ITEM_USAGE_QUEST; - } if (proto->Class == ITEM_CLASS_PROJECTILE && bot->CanUseItem(proto) == EQUIP_ERR_OK) { - if (bot->getClass() == CLASS_HUNTER || bot->getClass() == CLASS_ROGUE || bot->getClass() == CLASS_WARRIOR) - { - Item* rangedWeapon = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED); - uint32 requiredSubClass = 0; - - if (rangedWeapon) - { - switch (rangedWeapon->GetTemplate()->SubClass) - { - case ITEM_SUBCLASS_WEAPON_GUN: - requiredSubClass = ITEM_SUBCLASS_BULLET; - break; - case ITEM_SUBCLASS_WEAPON_BOW: - case ITEM_SUBCLASS_WEAPON_CROSSBOW: - requiredSubClass = ITEM_SUBCLASS_ARROW; - break; - } - } - - // Ensure the item is the correct ammo type for the equipped ranged weapon - if (proto->SubClass == requiredSubClass) - { - float ammoCount = BetterStacks(proto, "ammo"); - float requiredAmmo = (bot->getClass() == CLASS_HUNTER) ? 8 : 2; // Hunters get 8 stacks, others 2 - uint32 currentAmmoId = bot->GetUInt32Value(PLAYER_AMMO_ID); - - // Check if the bot has an ammo type assigned - if (currentAmmoId == 0) - { - return ITEM_USAGE_EQUIP; // Equip the ammo if no ammo - } - // Compare new ammo vs current equipped ammo - ItemTemplate const* currentAmmoProto = sObjectMgr->GetItemTemplate(currentAmmoId); - if (currentAmmoProto) - { - uint32 currentAmmoDPS = (currentAmmoProto->Damage[0].DamageMin + currentAmmoProto->Damage[0].DamageMax) * 1000 / 2; - uint32 newAmmoDPS = (proto->Damage[0].DamageMin + proto->Damage[0].DamageMax) * 1000 / 2; - - if (newAmmoDPS > currentAmmoDPS) // New ammo meets upgrade condition - { - return ITEM_USAGE_EQUIP; - } - if (newAmmoDPS < currentAmmoDPS) // New ammo is worse - { - return ITEM_USAGE_NONE; - } - } - // Ensure we have enough ammo in the inventory - if (ammoCount < requiredAmmo) - { - ammoCount += CurrentStacks(proto); - - if (ammoCount < requiredAmmo) // Buy ammo to reach the proper supply - return ITEM_USAGE_AMMO; - else if (ammoCount < requiredAmmo + 1) - return ITEM_USAGE_KEEP; // Keep the ammo if we don't have too much. - } - } - } + ItemUsage ammoUsage = QueryItemUsageForAmmo(proto); + if (ammoUsage != ITEM_USAGE_NONE) + return ammoUsage; } - // Need to add something like free bagspace or item value. if (proto->SellPrice > 0) { if (proto->Quality >= ITEM_QUALITY_NORMAL && !isSoulbound) - { return ITEM_USAGE_AH; - } + else - { return ITEM_USAGE_VENDOR; - } } return ITEM_USAGE_NONE; @@ -480,6 +404,80 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto, return ITEM_USAGE_NONE; } +ItemUsage ItemUsageValue::QueryItemUsageForAmmo(ItemTemplate const* proto) +{ + if (bot->getClass() != CLASS_HUNTER || bot->getClass() != CLASS_ROGUE || bot->getClass() != CLASS_WARRIOR) + return ITEM_USAGE_NONE; + + Item* rangedWeapon = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED); + uint32 requiredSubClass = 0; + + if (rangedWeapon) + { + switch (rangedWeapon->GetTemplate()->SubClass) + { + case ITEM_SUBCLASS_WEAPON_GUN: + requiredSubClass = ITEM_SUBCLASS_BULLET; + break; + case ITEM_SUBCLASS_WEAPON_BOW: + case ITEM_SUBCLASS_WEAPON_CROSSBOW: + requiredSubClass = ITEM_SUBCLASS_ARROW; + break; + } + } + + // Ensure the item is the correct ammo type for the equipped ranged weapon + if (proto->SubClass == requiredSubClass) + { + float ammoCount = BetterStacks(proto, "ammo"); + float requiredAmmo = (bot->getClass() == CLASS_HUNTER) ? 8 : 2; // Hunters get 8 stacks, others 2 + uint32 currentAmmoId = bot->GetUInt32Value(PLAYER_AMMO_ID); + + // Check if the bot has an ammo type assigned + if (currentAmmoId == 0) + return ITEM_USAGE_EQUIP; // Equip the ammo if no ammo + // Compare new ammo vs current equipped ammo + ItemTemplate const* currentAmmoProto = sObjectMgr->GetItemTemplate(currentAmmoId); + if (currentAmmoProto) + { + uint32 currentAmmoDPS = (currentAmmoProto->Damage[0].DamageMin + currentAmmoProto->Damage[0].DamageMax) * 1000 / 2; + uint32 newAmmoDPS = (proto->Damage[0].DamageMin + proto->Damage[0].DamageMax) * 1000 / 2; + + if (newAmmoDPS > currentAmmoDPS) // New ammo meets upgrade condition + return ITEM_USAGE_EQUIP; + + if (newAmmoDPS < currentAmmoDPS) // New ammo is worse + return ITEM_USAGE_NONE; + } + // Ensure we have enough ammo in the inventory + if (ammoCount < requiredAmmo) + { + ammoCount += CurrentStacks(proto); + + if (ammoCount < requiredAmmo) // Buy ammo to reach the proper supply + return ITEM_USAGE_AMMO; + else if (ammoCount < requiredAmmo + 1) + return ITEM_USAGE_KEEP; // Keep the ammo if we don't have too much. + } + } + return ITEM_USAGE_NONE; +} + +ParsedItemUsage ItemUsageValue::GetItemIdFromQualifier() +{ + ParsedItemUsage parsed; + + size_t const pos = qualifier.find(","); + if (pos != std::string::npos) + { + parsed.itemId = atoi(qualifier.substr(0, pos).c_str()); + parsed.randomPropertyId = atoi(qualifier.substr(pos + 1).c_str()); + return parsed; + } + else + parsed.itemId = atoi(qualifier.c_str()); + return parsed; +} // Return smaltest bag size equipped uint32 ItemUsageValue::GetSmallestBagSize() { @@ -913,3 +911,25 @@ std::string const ItemUsageValue::GetConsumableType(ItemTemplate const* proto, b return ""; } + +ItemUsage ItemUpgradeValue::Calculate() +{ + ParsedItemUsage parsed = GetItemIdFromQualifier(); + uint32 itemId = parsed.itemId; + uint32 randomPropertyId = parsed.randomPropertyId; + if (!itemId) + return ITEM_USAGE_NONE; + + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); + if (!proto) + return ITEM_USAGE_NONE; + + ItemUsage equip = QueryItemUsageForEquip(proto, randomPropertyId); + if (equip != ITEM_USAGE_NONE) + return equip; + + if (proto->Class == ITEM_CLASS_PROJECTILE && bot->CanUseItem(proto) == EQUIP_ERR_OK) + return QueryItemUsageForAmmo(proto); + + return ITEM_USAGE_NONE; +} diff --git a/src/Ai/Base/Value/ItemUsageValue.h b/src/Ai/Base/Value/ItemUsageValue.h index d50ea1b0608..b759bab6af2 100644 --- a/src/Ai/Base/Value/ItemUsageValue.h +++ b/src/Ai/Base/Value/ItemUsageValue.h @@ -14,7 +14,11 @@ class Player; class PlayerbotAI; struct ItemTemplate; - +struct ParsedItemUsage +{ + uint32 itemId = 0; + int32 randomPropertyId = 0; +}; enum ItemUsage : uint32 { ITEM_USAGE_NONE = 0, @@ -42,8 +46,12 @@ class ItemUsageValue : public CalculatedValue, public Qualified ItemUsage Calculate() override; -private: +protected: ItemUsage QueryItemUsageForEquip(ItemTemplate const* proto, int32 randomPropertyId = 0); + ItemUsage QueryItemUsageForAmmo(ItemTemplate const* proto); + ParsedItemUsage GetItemIdFromQualifier(); + +private: uint32 GetSmallestBagSize(); bool IsItemUsefulForQuest(Player* player, ItemTemplate const* proto); bool IsItemNeededForSkill(ItemTemplate const* proto); @@ -61,4 +69,14 @@ class ItemUsageValue : public CalculatedValue, public Qualified static std::string const GetConsumableType(ItemTemplate const* proto, bool hasMana); }; +class ItemUpgradeValue : public ItemUsageValue +{ +public: + ItemUpgradeValue(PlayerbotAI* botAI, std::string const name = "item upgrade") : ItemUsageValue(botAI, name) + { + } + + ItemUsage Calculate() override; +}; + #endif diff --git a/src/Ai/Base/ValueContext.h b/src/Ai/Base/ValueContext.h index ab93bcea471..b67aee9bb43 100644 --- a/src/Ai/Base/ValueContext.h +++ b/src/Ai/Base/ValueContext.h @@ -216,6 +216,7 @@ class ValueContext : public NamedObjectContext creators["formation"] = &ValueContext::formation; creators["stance"] = &ValueContext::stance; creators["item usage"] = &ValueContext::item_usage; + creators["item upgrade"] = &ValueContext::item_upgrade; creators["speed"] = &ValueContext::speed; creators["last said"] = &ValueContext::last_said; creators["last emote"] = &ValueContext::last_emote; @@ -341,6 +342,7 @@ class ValueContext : public NamedObjectContext static UntypedValue* already_seen_players(PlayerbotAI* botAI) { return new AlreadySeenPlayersValue(botAI); } static UntypedValue* new_player_nearby(PlayerbotAI* botAI) { return new NewPlayerNearbyValue(botAI); } static UntypedValue* item_usage(PlayerbotAI* botAI) { return new ItemUsageValue(botAI); } + static UntypedValue* item_upgrade(PlayerbotAI* botAI) { return new ItemUpgradeValue(botAI); } static UntypedValue* formation(PlayerbotAI* botAI) { return new FormationValue(botAI); } static UntypedValue* stance(PlayerbotAI* botAI) { return new StanceValue(botAI); } static UntypedValue* mana_save_level(PlayerbotAI* botAI) { return new ManaSaveLevelValue(botAI); } diff --git a/src/Ai/Base/WorldPacketTriggerContext.h b/src/Ai/Base/WorldPacketTriggerContext.h index 1e730952447..62afafd1a92 100644 --- a/src/Ai/Base/WorldPacketTriggerContext.h +++ b/src/Ai/Base/WorldPacketTriggerContext.h @@ -46,6 +46,7 @@ class WorldPacketTriggerContext : public NamedObjectContext creators["questgiver quest details"] = &WorldPacketTriggerContext::questgiver_quest_details; creators["item push result"] = &WorldPacketTriggerContext::item_push_result; + creators["loot roll won"] = &WorldPacketTriggerContext::loot_roll_won; creators["party command"] = &WorldPacketTriggerContext::party_command; creators["taxi done"] = &WorldPacketTriggerContext::taxi_done; creators["cast failed"] = &WorldPacketTriggerContext::cast_failed; @@ -92,6 +93,7 @@ class WorldPacketTriggerContext : public NamedObjectContext static Trigger* taxi_done(PlayerbotAI* botAI) { return new WorldPacketTrigger(botAI, "taxi done"); } static Trigger* party_command(PlayerbotAI* botAI) { return new WorldPacketTrigger(botAI, "party command"); } static Trigger* item_push_result(PlayerbotAI* botAI) { return new WorldPacketTrigger(botAI, "item push result"); } + static Trigger* loot_roll_won(PlayerbotAI* botAI) { return new WorldPacketTrigger(botAI, "loot roll won"); } // quest static Trigger* quest_update_add_kill(PlayerbotAI* ai) { return new WorldPacketTrigger(ai, "quest update add kill"); } diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index f4be1d43ee5..cc91f3ce995 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -185,6 +185,7 @@ PlayerbotAI::PlayerbotAI(Player* bot) botOutgoingPacketHandlers.AddHandler(SMSG_TRADE_STATUS_EXTENDED, "trade status extended"); botOutgoingPacketHandlers.AddHandler(SMSG_LOOT_RESPONSE, "loot response"); botOutgoingPacketHandlers.AddHandler(SMSG_ITEM_PUSH_RESULT, "item push result"); + botOutgoingPacketHandlers.AddHandler(SMSG_LOOT_ROLL_WON, "loot roll won"); botOutgoingPacketHandlers.AddHandler(SMSG_PARTY_COMMAND_RESULT, "party command"); botOutgoingPacketHandlers.AddHandler(SMSG_LEVELUP_INFO, "levelup"); botOutgoingPacketHandlers.AddHandler(SMSG_LOG_XPGAIN, "xpgain"); From e9e79ad696c053e1c1b7fad0c5943ba9d99d5ad5 Mon Sep 17 00:00:00 2001 From: Hokken Date: Sun, 8 Feb 2026 11:45:03 +0000 Subject: [PATCH 27/29] Fix LootRollLevel=1 to match documented 'greed' behavior (#2068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes `AiPlayerbot.LootRollLevel = 1` to actually behave as "greed" mode per the config documentation. ## Problem The config documentation states: ```conf # Bots' loot roll level (0 = pass, 1 = greed, 2 = need) # Default: 1 (greed) AiPlayerbot.LootRollLevel = 1 ``` However, level 1 was converting **all GREED votes to PASS**, causing bots to pass on almost everything: | Item Type | AI Decision | Level 1 Behavior (Before) | Expected | |-----------|-------------|---------------------------|----------| | Gear upgrade | NEED | GREED ✓ | GREED | | Usable gear (not upgrade) | GREED | **PASS** ✗ | GREED | | Crafting materials | GREED | **PASS** ✗ | GREED | | Recipes, consumables | GREED | **PASS** ✗ | GREED | The only items bots would greed on were direct gear upgrades (originally NEED, downgraded to GREED). ## Root Cause In `LootRollAction.cpp`, lines 104-107 were converting GREED to PASS: ```cpp else if (vote == GREED) { vote = PASS; // This breaks "greed" mode } ``` ## Fix Remove the GREED→PASS conversion. Level 1 now only downgrades NEED to GREED (as intended), preserving GREED votes for useful items. ## Behavior After Fix | Level | Description | Behavior | |-------|-------------|----------| | 0 | Pass | Always pass on all items | | 1 | Greed | Greed on useful items, never need | | 2 | Need | Full AI logic (need/greed/pass) | ## Test Plan - [ ] Set `AiPlayerbot.LootRollLevel = 1` - [ ] Kill mobs that drop crafting materials, recipes, or non-upgrade gear - [ ] Verify bots greed on useful items instead of passing - [ ] Verify bots still pass on junk items - [ ] Verify bots never roll need (only greed) Co-authored-by: Hokken --- src/Ai/Base/Actions/LootRollAction.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Ai/Base/Actions/LootRollAction.cpp b/src/Ai/Base/Actions/LootRollAction.cpp index e600f1f2041..9a21c8139fd 100644 --- a/src/Ai/Base/Actions/LootRollAction.cpp +++ b/src/Ai/Base/Actions/LootRollAction.cpp @@ -90,6 +90,8 @@ bool LootRollAction::Execute(Event event) } else if (sPlayerbotAIConfig.lootRollLevel == 1) { + // Level 1 = "greed" mode: bots greed on useful items but never need + // Only downgrade NEED to GREED, preserve GREED votes as-is if (vote == NEED) { if (RollUniqueCheck(proto, bot)) @@ -101,10 +103,6 @@ bool LootRollAction::Execute(Event event) vote = GREED; } } - else if (vote == GREED) - { - vote = PASS; - } } switch (group->GetLootMethod()) { From c9c936d5c1483d99af2b0967a8c68c88322874fb Mon Sep 17 00:00:00 2001 From: dillyns <49765217+dillyns@users.noreply.github.com> Date: Sun, 8 Feb 2026 12:36:27 -0500 Subject: [PATCH 28/29] Add Unending Breath to Warlock NonCombat Strat (#2074) # Pull Request Adds actions and triggers for Warlock class to cast Unending Breath when swimming, following the existing implementation for Shaman Water Breathing. --- ## Feature Evaluation Add triggers for Warlock noncombat strategy for Unending Breath on self and party. Triggers should only be active while swimming. Minimal runtime cost on Warlock bots trigger processing. --- ## How to Test the Changes - Bring a Warlock bot into water - It should cast Unending Breath on itself and anyone in the party ## Complexity & Impact - Does this change add new decision branches? - [ ] No - [x] Yes (**explain below**) It adds triggers to Warlock to decide when to cast Unending Breath on self or party members. - Does this change increase per-bot or per-tick processing? - [ ] No - [x] Yes (**describe and justify impact**) Minimal additional processing for Warlock triggers, same as already existing triggers for Shaman. - Could this logic scale poorly under load? - [x] No - [ ] Yes (**explain why**) --- ## Defaults & Configuration - Does this change modify default bot behavior? - [x] No - [ ] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - [ ] Lightweight mode remains the default - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance - Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - [ ] No - [x] Yes (**explain below**) Claude was used to explore the codebase to find similar implementations that already existed. --- ## Final Checklist - [x] Stability is not compromised - [x] Performance impact is understood, tested, and acceptable - [x] Added logic complexity is justified and explained - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. Co-authored-by: bashermens <31279994+hermensbas@users.noreply.github.com> --- src/Ai/Class/Warlock/Action/WarlockActions.h | 12 ++++++++++++ .../Strategy/GenericWarlockNonCombatStrategy.cpp | 2 ++ src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp | 10 ++++++++++ src/Ai/Class/Warlock/Trigger/WarlockTriggers.h | 14 ++++++++++++++ src/Ai/Class/Warlock/WarlockAiObjectContext.cpp | 8 ++++++++ 5 files changed, 46 insertions(+) diff --git a/src/Ai/Class/Warlock/Action/WarlockActions.h b/src/Ai/Class/Warlock/Action/WarlockActions.h index 787b518d696..09f34637031 100644 --- a/src/Ai/Class/Warlock/Action/WarlockActions.h +++ b/src/Ai/Class/Warlock/Action/WarlockActions.h @@ -41,6 +41,18 @@ class CastSoulLinkAction : public CastBuffSpellAction std::string const GetTargetName() override { return "pet target"; } }; +class CastUnendingBreathAction : public CastBuffSpellAction +{ +public: + CastUnendingBreathAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "unending breath") {} +}; + +class CastUnendingBreathOnPartyAction : public BuffOnPartyAction +{ +public: + CastUnendingBreathOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "unending breath") {} +}; + class CreateSoulShardAction : public Action { public: diff --git a/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.cpp b/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.cpp index a78ff0a988c..d22abe3caba 100644 --- a/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.cpp +++ b/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.cpp @@ -85,6 +85,8 @@ void GenericWarlockNonCombatStrategy::InitTriggers(std::vector& tr triggers.push_back(new TriggerNode("too many soul shards", { NextAction("destroy soul shard", 60.0f) })); triggers.push_back(new TriggerNode("soul link", { NextAction("soul link", 28.0f) })); triggers.push_back(new TriggerNode("demon armor", { NextAction("fel armor", 27.0f) })); + triggers.push_back(new TriggerNode("unending breath", { NextAction("unending breath", 12.0f) })); + triggers.push_back(new TriggerNode("unending breath on party", { NextAction("unending breath on party", 11.0f) })); triggers.push_back(new TriggerNode("no healthstone", { NextAction("create healthstone", 26.0f) })); triggers.push_back(new TriggerNode("no soulstone", { NextAction("create soulstone", 25.0f) })); triggers.push_back(new TriggerNode("life tap", { NextAction("life tap", 23.0f) })); diff --git a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp index 53e77669b26..1df531e7fd7 100644 --- a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp +++ b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp @@ -79,6 +79,16 @@ bool SoulLinkTrigger::IsActive() return !botAI->HasAura("soul link", target); } +bool UnendingBreathTrigger::IsActive() +{ + return BuffTrigger::IsActive() && AI_VALUE2(bool, "swimming", "self target"); +} + +bool UnendingBreathOnPartyTrigger::IsActive() +{ + return BuffOnPartyTrigger::IsActive() && AI_VALUE2(bool, "swimming", "self target"); +} + bool DemonicEmpowermentTrigger::IsActive() { Pet* pet = bot->GetPet(); diff --git a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h index 8512549012d..d66fe537e1f 100644 --- a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h +++ b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h @@ -32,6 +32,20 @@ class SoulLinkTrigger : public BuffTrigger bool IsActive() override; }; +class UnendingBreathTrigger : public BuffTrigger +{ +public: + UnendingBreathTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "unending breath", 5 * 2000) {} + bool IsActive() override; +}; + +class UnendingBreathOnPartyTrigger : public BuffOnPartyTrigger +{ +public: + UnendingBreathOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "unending breath on party", 2 * 2000) {} + bool IsActive() override; +}; + class OutOfSoulShardsTrigger : public Trigger { public: diff --git a/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp b/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp index a70327b6c68..27b9166c658 100644 --- a/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp +++ b/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp @@ -143,6 +143,8 @@ class WarlockTriggerFactoryInternal : public NamedObjectContext creators["shadow trance"] = &WarlockTriggerFactoryInternal::shadow_trance; creators["demon armor"] = &WarlockTriggerFactoryInternal::demon_armor; creators["soul link"] = &WarlockTriggerFactoryInternal::soul_link; + creators["unending breath"] = &WarlockTriggerFactoryInternal::unending_breath; + creators["unending breath on party"] = &WarlockTriggerFactoryInternal::unending_breath_on_party; creators["no soul shard"] = &WarlockTriggerFactoryInternal::no_soul_shard; creators["too many soul shards"] = &WarlockTriggerFactoryInternal::too_many_soul_shards; creators["no healthstone"] = &WarlockTriggerFactoryInternal::HasHealthstone; @@ -189,6 +191,8 @@ class WarlockTriggerFactoryInternal : public NamedObjectContext static Trigger* shadow_trance(PlayerbotAI* botAI) { return new ShadowTranceTrigger(botAI); } static Trigger* demon_armor(PlayerbotAI* botAI) { return new DemonArmorTrigger(botAI); } static Trigger* soul_link(PlayerbotAI* botAI) { return new SoulLinkTrigger(botAI); } + static Trigger* unending_breath(PlayerbotAI* botAI) { return new UnendingBreathTrigger(botAI); } + static Trigger* unending_breath_on_party(PlayerbotAI* botAI) { return new UnendingBreathOnPartyTrigger(botAI); } static Trigger* no_soul_shard(PlayerbotAI* botAI) { return new OutOfSoulShardsTrigger(botAI); } static Trigger* too_many_soul_shards(PlayerbotAI* botAI) { return new TooManySoulShardsTrigger(botAI); } static Trigger* HasHealthstone(PlayerbotAI* botAI) { return new HasHealthstoneTrigger(botAI); } @@ -240,6 +244,8 @@ class WarlockAiObjectContextInternal : public NamedObjectContext creators["demon armor"] = &WarlockAiObjectContextInternal::demon_armor; creators["demon skin"] = &WarlockAiObjectContextInternal::demon_skin; creators["soul link"] = &WarlockAiObjectContextInternal::soul_link; + creators["unending breath"] = &WarlockAiObjectContextInternal::unending_breath; + creators["unending breath on party"] = &WarlockAiObjectContextInternal::unending_breath_on_party; creators["create soul shard"] = &WarlockAiObjectContextInternal::create_soul_shard; creators["destroy soul shard"] = &WarlockAiObjectContextInternal::destroy_soul_shard; creators["create healthstone"] = &WarlockAiObjectContextInternal::create_healthstone; @@ -313,6 +319,8 @@ class WarlockAiObjectContextInternal : public NamedObjectContext static Action* demon_armor(PlayerbotAI* botAI) { return new CastDemonArmorAction(botAI); } static Action* demon_skin(PlayerbotAI* botAI) { return new CastDemonSkinAction(botAI); } static Action* soul_link(PlayerbotAI* botAI) { return new CastSoulLinkAction(botAI); } + static Action* unending_breath(PlayerbotAI* botAI) { return new CastUnendingBreathAction(botAI); } + static Action* unending_breath_on_party(PlayerbotAI* botAI) { return new CastUnendingBreathOnPartyAction(botAI); } static Action* create_soul_shard(PlayerbotAI* botAI) { return new CreateSoulShardAction(botAI); } static Action* destroy_soul_shard(PlayerbotAI* botAI) { return new DestroySoulShardAction(botAI); } static Action* create_healthstone(PlayerbotAI* botAI) { return new CastCreateHealthstoneAction(botAI); } From 610fdc16d7e24fccbc336509831879d4c08db600 Mon Sep 17 00:00:00 2001 From: privatecore Date: Sun, 8 Feb 2026 18:36:56 +0100 Subject: [PATCH 29/29] Fix bug with GetCreature + GetGameObject = use ObjectAccessor's methods instead (#2105) # Pull Request https://en.cppreference.com/w/cpp/algorithm/equal_range.html > second is an iterator to the first element of the range [first, last) ordered after value (or last if no such element is found). The original code uses `return bounds.second->second`, which causes the wrong creature/gameobject to be returned. Instead, both methods (`GetCreature` and `GetGameObject`) now utilize ObjectAccessor's methods to retrieve the correct entities. These built-in methods offer a safer way to access objects. Additionally, `GetUnit` no longer includes redundant creature processing before checks and now has the same logic as the `ObjectAccessor::GetUnit` method. Furthermore, `GuidPosition::isDead` method has been renamed to `GuidPosition::IsCreatureOrGOAccessible` and updated, as it is used only for creatures (NOT units) and gameobjects. --- ## Design Philosophy We prioritize **stability, performance, and predictability** over behavioral realism. Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and long-term robustness. Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all participants. Because every action and decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and negatively affect both players and world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a project goal. Increased behavioral realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and maintained continuously as the system evolves. If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision model**. More complex behavior should only be available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. Principles: - **Stability before intelligence** A stable system is always preferred over a smarter one. - **Performance is a shared resource** Any increase in bot cost affects all players and all bots. - **Simple logic scales better than smart logic** Predictable behavior under load is more valuable than perfect decisions. - **Complexity must justify itself** If a feature cannot clearly explain its cost, it should not exist. - **Defaults must be cheap** Expensive behavior must always be optional and clearly communicated. - **Bots should look reasonable, not perfect** The goal is believable behavior, not human simulation. Before submitting, confirm that this change aligns with those principles. --- ## How to Test the Changes The behavior has not changed after all. ## Complexity & Impact - Does this change add new decision branches? - [x] No - [ ] Yes (**explain below**) - Does this change increase per-bot or per-tick processing? - [x] No - [ ] Yes (**describe and justify impact**) - Could this logic scale poorly under load? - [x] No - [ ] Yes (**explain why**) --- ## Defaults & Configuration - Does this change modify default bot behavior? - [x] No - [ ] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - [ ] Lightweight mode remains the default - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance - Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - [x] No - [ ] Yes (**explain below**) If yes, please specify: - AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) - Which parts of the change were influenced or generated - Whether the result was manually reviewed and adapted AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest about what they do and do not understand. --- ## Final Checklist - [x] Stability is not compromised - [x] Performance impact is understood, tested, and acceptable - [x] Added logic complexity is justified and explained - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --------- Co-authored-by: bashermens <31279994+hermensbas@users.noreply.github.com> --- src/Ai/Base/Value/QuestValues.cpp | 6 +- src/Mgr/Travel/TravelMgr.cpp | 136 +++++++++++++----------------- src/Mgr/Travel/TravelMgr.h | 7 +- 3 files changed, 66 insertions(+), 83 deletions(-) diff --git a/src/Ai/Base/Value/QuestValues.cpp b/src/Ai/Base/Value/QuestValues.cpp index 1724778509a..f556f443d36 100644 --- a/src/Ai/Base/Value/QuestValues.cpp +++ b/src/Ai/Base/Value/QuestValues.cpp @@ -173,7 +173,7 @@ std::vector ActiveQuestGiversValue::Calculate() continue; } - if (guidp.isDead()) + if (!guidp.IsCreatureOrGOAccessible()) continue; retQuestGivers.push_back(guidp); @@ -231,7 +231,7 @@ std::vector ActiveQuestTakersValue::Calculate() for (auto& guidp : entry.second) { - if (guidp.isDead()) + if (!guidp.IsCreatureOrGOAccessible()) continue; retQuestTakers.push_back(guidp); @@ -298,7 +298,7 @@ std::vector ActiveQuestObjectivesValue::Calculate() { for (auto& guidp : entry.second) { - if (guidp.isDead()) + if (!guidp.IsCreatureOrGOAccessible()) continue; retQuestObjectives.push_back(guidp); diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp index 84eef9b99ea..748f929aa70 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -881,39 +881,63 @@ std::vector WorldPosition::getGameObjectsNear(float radiu return worker.GetResult(); } -Creature* GuidPosition::GetCreature() +CreatureTemplate const* GuidPosition::GetCreatureTemplate() +{ + return IsCreature() ? sObjectMgr->GetCreatureTemplate(GetEntry()) : nullptr; +} + +GameObjectTemplate const* GuidPosition::GetGameObjectTemplate() +{ + return IsGameObject() ? sObjectMgr->GetGameObjectTemplate(GetEntry()) : nullptr; +} + +WorldObject* GuidPosition::GetWorldObject() { if (!*this) return nullptr; - if (loadedFromDB) + switch (GetHigh()) { - auto creatureBounds = getMap()->GetCreatureBySpawnIdStore().equal_range(GetCounter()); - if (creatureBounds.first != creatureBounds.second) - return creatureBounds.second->second; - - return nullptr; + case HighGuid::Player: + return GetPlayer(); + case HighGuid::Transport: + case HighGuid::Mo_Transport: + case HighGuid::GameObject: + return GetGameObject(); + case HighGuid::Vehicle: + case HighGuid::Unit: + return GetCreature(); + case HighGuid::Pet: + return getMap()->GetPet(*this); + case HighGuid::DynamicObject: + return getMap()->GetDynamicObject(*this); + case HighGuid::Corpse: + return getMap()->GetCorpse(*this); + default: + return nullptr; } - return getMap()->GetCreature(*this); + return nullptr; } -Unit* GuidPosition::GetUnit() +GameObject* GuidPosition::GetGameObject() { if (!*this) return nullptr; if (loadedFromDB) - { - auto creatureBounds = getMap()->GetCreatureBySpawnIdStore().equal_range(GetCounter()); - if (creatureBounds.first != creatureBounds.second) - return creatureBounds.second->second; + return ObjectAccessor::GetSpawnedGameObjectByDBGUID(GetMapId(), GetCounter()); + return getMap()->GetGameObject(*this); // fallback +} + +Unit* GuidPosition::GetUnit() +{ + if (!*this) return nullptr; - } if (IsPlayer()) - return ObjectAccessor::FindPlayer(*this); + return GetPlayer(); if (IsPet()) return getMap()->GetPet(*this); @@ -921,21 +945,15 @@ Unit* GuidPosition::GetUnit() return GetCreature(); } -GameObject* GuidPosition::GetGameObject() +Creature* GuidPosition::GetCreature() { if (!*this) return nullptr; if (loadedFromDB) - { - auto gameobjectBounds = getMap()->GetGameObjectBySpawnIdStore().equal_range(GetCounter()); - if (gameobjectBounds.first != gameobjectBounds.second) - return gameobjectBounds.second->second; + return ObjectAccessor::GetSpawnedCreatureByDBGUID(GetMapId(), GetCounter()); - return nullptr; - } - - return getMap()->GetGameObject(*this); + return getMap()->GetCreature(*this); // fallback } Player* GuidPosition::GetPlayer() @@ -949,21 +967,28 @@ Player* GuidPosition::GetPlayer() return nullptr; } -bool GuidPosition::isDead() -{ - if (!getMap()) - return false; - - if (!getMap()->IsGridLoaded(getX(), getY())) - return false; +bool GuidPosition::HasNpcFlag(NPCFlags flag) { return IsCreature() && GetCreatureTemplate()->npcflag & flag; } - if (IsUnit() && GetUnit() && GetUnit()->IsInWorld() && GetUnit()->IsAlive()) +bool GuidPosition::IsCreatureOrGOAccessible() +{ + Map* map = getMap(); + if (!map || !map->IsGridLoaded(GetPositionX(), GetPositionY())) return false; - if (IsGameObject() && GetGameObject() && GetGameObject()->IsInWorld()) - return false; + if (IsCreature()) + { + Creature* creature = GetCreature(); + if (creature && creature->IsInWorld() && creature->IsAlive()) + return true; + } + else if (IsGameObject()) + { + GameObject* go = GetGameObject(); + if (go && go->IsInWorld()) + return true; + } - return true; + return false; } GuidPosition::GuidPosition(WorldObject* wo) : ObjectGuid(wo->GetGUID()), WorldPosition(wo), loadedFromDB(false) {} @@ -982,47 +1007,6 @@ GuidPosition::GuidPosition(GameObjectData const& goData) loadedFromDB = true; } -CreatureTemplate const* GuidPosition::GetCreatureTemplate() -{ - return IsCreature() ? sObjectMgr->GetCreatureTemplate(GetEntry()) : nullptr; -} - -GameObjectTemplate const* GuidPosition::GetGameObjectTemplate() -{ - return IsGameObject() ? sObjectMgr->GetGameObjectTemplate(GetEntry()) : nullptr; -} - -WorldObject* GuidPosition::GetWorldObject() -{ - if (!*this) - return nullptr; - - switch (GetHigh()) - { - case HighGuid::Player: - return ObjectAccessor::FindPlayer(*this); - case HighGuid::Transport: - case HighGuid::Mo_Transport: - case HighGuid::GameObject: - return GetGameObject(); - case HighGuid::Vehicle: - case HighGuid::Unit: - return GetCreature(); - case HighGuid::Pet: - return getMap()->GetPet(*this); - case HighGuid::DynamicObject: - return getMap()->GetDynamicObject(*this); - case HighGuid::Corpse: - return getMap()->GetCorpse(*this); - default: - return nullptr; - } - - return nullptr; -} - -bool GuidPosition::HasNpcFlag(NPCFlags flag) { return IsCreature() && GetCreatureTemplate()->npcflag & flag; } - std::vector TravelDestination::getPoints(bool ignoreFull) { if (ignoreFull) diff --git a/src/Mgr/Travel/TravelMgr.h b/src/Mgr/Travel/TravelMgr.h index 3223444bfee..251313736aa 100644 --- a/src/Mgr/Travel/TravelMgr.h +++ b/src/Mgr/Travel/TravelMgr.h @@ -416,14 +416,13 @@ class GuidPosition : public ObjectGuid, public WorldPosition GameObjectTemplate const* GetGameObjectTemplate(); WorldObject* GetWorldObject(); - Creature* GetCreature(); - Unit* GetUnit(); GameObject* GetGameObject(); + Unit* GetUnit(); + Creature* GetCreature(); Player* GetPlayer(); bool HasNpcFlag(NPCFlags flag); - - bool isDead(); // For loaded grids check if the unit/object is unloaded/dead. + bool IsCreatureOrGOAccessible(); // For loaded grids check if the creature/gameobject is in world + alive operator bool() const { return !IsEmpty(); } bool operator==(ObjectGuid const& guid) const { return GetRawValue() == guid.GetRawValue(); }