From f4b4d8967fd5136a199663b717414274161e8dc4 Mon Sep 17 00:00:00 2001 From: Crow Date: Wed, 10 Dec 2025 14:08:25 -0600 Subject: [PATCH] Karazhan Refactor + Nightbane Strategy (#1847) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR completely refactors the Karazhan raid strategies to clean up the code and polish up/introduce new strategies, including for Nightbane. General changes with respect to the code: - Moved gating checks for action methods to triggers instead of relying on isUseful functions - Got rid of the nonsensical helper class I made and switched to a helper namespace - Broke up some longer action classes into separate methods and/or private members - Deleted/consolidated some excess code - Made greater use of early returns - Renamed methods, multipliers, and triggers to make them easier to understand - Generally made edits to conform with AC code standards Below are the implemented strategies. I’m including all of them, not just new/modified ones, because I don’t think the first set of strategies was ever tested. So each boss could probably use complete testing. ### **Trash** I added strategies for a trash mob that I find particularly annoying: - Mana Warp: These are, IMO, the most annoying trash mobs in Karazhan. They blow up when low on health, and having two blow up in quick succession is enough to take out a decent chunk of your raid. The strategy directs bots to use pretty much every stun in the book on Mana Warps when they’re low on HP, which is the only way to prevent the explosion. ### **Attumen** - During the first phase, bots will focus on Midnight (taking either Midnight or Attumen to 25% starts the next phase). When Attumen spawns, the assist tank will pick him up and move him away so that bots don’t get cleaved. - When Attumen mounts Midnight, starting phase 2, threat is wiped, and bots will pause DPS for a few seconds to allow the main tank to get aggro. All bots, other than the main tank and any bot that pulls aggro, will stack behind Attumen (~6 yards for ranged so Hunters can still attack). ### **Moroes** - As before, bots will mark and prioritize adds and the boss in the recommended kill order: Dorothea, Catriona, Keira, Rafe, Robin, Crispin, and Moroes. In practice, the enemies will probably be stacked up, and the bots will AoE them down in accordance with their typical AoE strategies, but classes without AoE capabilities should still prioritize the skull. - Based on testing feedback, added a method for the main tank to prioritize Moroes ### **Maiden of Virtue** I’ve made only minor changes to Revision’s original strategy here. - The tank with aggro will position Maiden in the middle of the room and move her to a healer when Repentance is cast so that the Holy Ground will break the healer’s stun. - Ranged bots have assigned positions between the columns around the middle of the room (to prevent chain damage from Holy Wrath). ### **The Big Bad Wolf** - The tank with aggro brings the boss to the front left of the stage. - If a bot gets turned into Little Red Riding Hood, it will run around the stage in a counter-clockwise rectangle until the transformation fades. I tweaked this strategy a bit; it's still not perfect, but it works better than before. ### **Romulo and Julianne** There are no substantive changes to this strategy. As before, in phase 3, when both bosses are active, the bots switch back-and-forth between the bosses by alternating the skull icon based on which boss has lower HP (10% differential). ### **The Wizard of Oz** There are no substantive changes to this strategy. As before, bots mark the bosses with a skull icon in their recommended kill order: Dorothee, Tito (assuming he spawns before you kill Dorothee), Roar, Strawman, Tinhead, and the Crone. Additionally, Mages will spam Scorch on Strawman to daze him. ### **The Curator** - The tank will drag the boss to a fixed spot down the hallway. Ranged bots will spread out to avoid chain damage from Arcing Sear, and bots will mark Astral Flares with the skull icon to prioritize them down. - Those strategies already existed, but now I made the assist tank also focus on the boss to try to stay second in aggro and therefore absorb Hateful Bolts. - Added a multiplier to save Bloodlust/Heroism until Evocation. ### **Terestian Illhoof** There are no substantive changes to this strategy. The bots will mark targets with the skull icon in the following order: Demonic Chains, Kil'rek, and Illhoof. ### **Shade of Aran** I redid the strategies a bit, and I think (hope) they work better. - Flame Wreath: Bots will stop moving until the aura fades. There is a bug in which Flame Wreath will sometimes persist long beyond its supposed 20-second duration. If Aran casts Arcane Explosion during this time, you will almost certainly wipe, so that’s frustrating. I made it so that bots will stay in place still and eat the Arcane Explosion because it’s the lesser of two evils, and if you are overgeared, you may be able to survive. In the previous strategy, bots would stop actions entirely, but now they should keep attacking/casting without moving. - Arcane Explosion: No substantive changes here--bots will run out immediately and stay out of range until the cast finishes. - Conjured Elementals: No substantive changes here--they will be marked one-by-one by the skull icon, except that the marking will skip any elemental that is banished. - Ranged Positioning: I redid this strategy. Ranged bots will now maintain a distance between 11 and 15 yards from the boss. This keeps them out of the 10-yard radius in which Aran silences while also keeping them from getting too far away and getting stuck in the alcoves. ### **Netherspite** I significantly refactored the action methods here, but substantively the original strategy remains mostly intact. - Red (Tank) Beam: One tank will be assigned to block the beam for each Portal Phase. The assigned tank will dance in and out of the beam (5 seconds in, 5 seconds out). Tanks intentionally do not avoid Void Zones (it was the lesser of two evils for them to take that damage vs. trying to dynamically avoid them, moving the boss, and possibly getting everybody out of position. - Blue (DPS) Beam: DPS other than Rogues and Warriors are eligible to be assigned (one-by-one) to block this beam. When the assigned blocker reaches 25 stacks of the debuff, they will leave the beam, and the next assigned blocker will take their place. If a Void Zone drops under the assigned blocker, the bot will move along the beam to get out of the Void Zone so that they do not stop blocking. - Green (Healer) Beam: This works the same way as the Blue Beam, except that eligible blockers are healers, Rogues, and DPS Warriors. Healers that are assigned to block will swap in the same way as Blue Beam blockers. If a Rogue or DPS Warrior is the assigned blocker, however, they will stand in the beam for the entire Portal Phase since they do not suffer any adverse effects from the beam. In this PR, I made the strategy prioritize Rogues and DPS Warriors over healers to try to avoid the need for bots to swap (and to avoid the irritating scenario in which a healer would block the beam for the first half of a phase and then tag in a Rogue or DPS Warrior, which would be wasted by blocking only half of a phase). - Non-Blockers: They will stay at least 5 yards away from each beam until called to be an assigned blocker. They will also avoid Void Zones. - Banish Phase: The only strategy I implemented was for bots to avoid residual Void Zones from the Portal Phase. - Phase Transitions: Bots should pause DPS at the beginning of the encounter and whenever Netherspite transitions back from the Banish Phase to the Portal Phase (which is an aggro reset). Note that this doesn't wipe DOTs, and there's not much I can do about that. ### **Prince Malchezaar** The action methods are significantly refactored, but the strategy substantively is not changed very much. - Bots will maintain distance from Infernals. The tank has a larger avoidance radius to give DPS a little bit of margin to work with. Depending on Infernal placement, it is possible for bots to get stuck in some bad positions. In that case, the best solution is to put them on “flee” and lead them to a better position. - Bots that get Enfeebled will run out of Shadow Nova range. They should pick a path that does not cross within any Infernal's Hellfire radius. - Added a multiplier to save Bloodlust/Heroism until Phase 3. ### **Nightbane** **Disclaimer**: Bots are terrible at this encounter, in large part because the map is awful (even before the recent Core changes). So the strategies are not ideal because they need to operate within the bots’ limitations. I STRONGLY suggest you clear the entire Livery Stables (including the upper level) because the mobs in them have a high risk of pulling through the floor of the Master’s Terrace. Ideally, you should clear out the Scullery too. The strategy uses waypoints toward the Northeastern door to the Master’s Terrace. I tried several different locations, and that worked best for me based on where Nightbane lands (note that he has a different landing spot for the encounter start vs. the start subsequent ground phases). - Ground Phase, main tank: The main tank uses two waypoints after it picks up the boss—the movement pattern should be kind of like a reverse checkmark, where the tank moves back along the inner edge of the terrace, then pivots and moves at a bit of an angle to the outer edge. I did this as a way to get the tank to face Nightbane sideways across the terrace, which is how he’s supposed to be tanked. The main tank will not get out of Charred Earth. This is intended. The tank cannot move dynamically enough to avoid it while also not turning the boss and wiping the raid. - Ground phase, ranged: Ranged bots rotate between three waypoints. They start stacked at the same position. If Charred Earth is dropped on that position, the bots will rotate to the second position. If Charred Earth is dropped on that position, they will rotate to the third position. The maximum number of Charred Earths that can be active is two, so if one is dropped on the third position, the ranged bots should rotate back to the first position. - Ground Phase, other melee: Melee bots have no coded Charred Earth avoidance strategy. They do decently enough with the general “avoid aoe” strategy. - Flight Phase: Bots move to Nightbane’s flight position—Nightbane is bugged and cannot be attacked in the air in AC, but all bots still need to stay near him or he will wipe the raid with Fireball Barrage. Bots stack on the same position; when Rain of Bones is cast (one time, snapshotted on the target’s location), all bots will move away to a second nearby position. They will then kill the Restless Skeletons. The Flight Phase lasts for 45 seconds, but Nightbane emotes after 35 seconds and goes to land—during the final 10-second period, bots are freed from all strategies and will follow the master. You need to lead them back to the Northeastern part of the terrace before Nightbane lands so that bots can get immediately positioned when he lands. - Phase Changes: Whenever Nightbane lands, bots should pause DPS for the main tank to get aggro. - Managing bots/pets: During the Flight Phase, bots and pets have a tendency to chase Nightbane outside of the boundaries of the map and pull mobs from other parts of the instance as well as cause Restless Skeletons to spawn out of bounds (and then further cause bots/pets to go out of bounds to attack the skeletons). The strategy should solve for this, but if there are still issues, it should be noted. My resolution was to implement the following: (1) when Nightbane takes flight, bots stop attacking and mark him with the moon icon, and Hunters and Warlocks put pets on passive and recall them (they will put pets on defensive again when Nightbane lands), and (2) all temporary pets are disabled for the duration of the fight (Water Elementals, Shaman wolves, Druid treants, etc.). **Known Issues:** - The approach to getting Nightbane to turn sideways is not always spot on since it’s a workaround to account for what should be dynamic movement. He can end up at a bit of an angle. I’ve tweaked the positions such that if he is at an angle, it should not be a situation in which any ranged position is exposed to Smoldering Breath or Cleave. That does increase the risk that poor positioning may expose a ranged position to Tail Sweep. This is obviously suboptimal, but that attack is much more survivable. - Ranged bots move between the three waypoints by reading the presence of the Charred Earth aura on themselves. Sometimes, a bot may reach the next waypoint with the Charred Earth aura from the previous waypoint still on them (depending on timing of ticks), in which case the bot will keep moving to the next waypoint. This can cause an issue in which the ranged group gets split. So you could have Charred Earth dropped on position 1, and some ranged bots will go to position 2 but others will go to position 3, and then if the second Charred Earth is dropped on position 3, the bots at position 3 will move back to position 1 (which is still in Charred Earth) before then moving to position 2. Balancing spacing between waypoints, positioning with respect to the boss, line of sight, and maximum distance is very difficult to do. I messed with the positioning a lot, and I am not sure if this possibility cannot be entirely avoided without creating other issues. I have at least reached a result in which I have not seen any bots cycle indefinitely (though if testing observes this, it is obviously a problem). Ultimately, I wouldn’t say I’m totally satisfied with the strategy. Wipes are still possible on account of bad RNG. But I think it does make the fight a lot more manageable, as it is a fight that is very difficult with IP nerfs in the absence of any strategy. Previously, I needed to bring 3 healers, and they would still all be out of mana by the first Flight Phase and reliant on MP5 for the remainder of the fight, and I would wipe multiple times before a kill. --- .../karazhan/RaidKarazhanActionContext.h | 277 ++- .../raids/karazhan/RaidKarazhanActions.cpp | 1853 +++++++++-------- .../raids/karazhan/RaidKarazhanActions.h | 359 ++-- .../raids/karazhan/RaidKarazhanHelpers.cpp | 695 ++++--- .../raids/karazhan/RaidKarazhanHelpers.h | 185 +- .../karazhan/RaidKarazhanMultipliers.cpp | 512 +++-- .../raids/karazhan/RaidKarazhanMultipliers.h | 110 +- .../raids/karazhan/RaidKarazhanStrategy.cpp | 203 +- .../raids/karazhan/RaidKarazhanStrategy.h | 2 +- .../karazhan/RaidKarazhanTriggerContext.h | 270 ++- .../raids/karazhan/RaidKarazhanTriggers.cpp | 367 +++- .../raids/karazhan/RaidKarazhanTriggers.h | 262 ++- 12 files changed, 3355 insertions(+), 1740 deletions(-) diff --git a/src/strategy/raids/karazhan/RaidKarazhanActionContext.h b/src/strategy/raids/karazhan/RaidKarazhanActionContext.h index 2c1f7168..34f276a9 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanActionContext.h +++ b/src/strategy/raids/karazhan/RaidKarazhanActionContext.h @@ -1,5 +1,5 @@ -#ifndef _PLAYERBOT_RAIDKARAZHANACTIONS_CONTEXT_H -#define _PLAYERBOT_RAIDKARAZHANACTIONS_CONTEXT_H +#ifndef _PLAYERBOT_RAIDKARAZHANACTIONCONTEXT_H +#define _PLAYERBOT_RAIDKARAZHANACTIONCONTEXT_H #include "RaidKarazhanActions.h" #include "NamedObjectContext.h" @@ -9,77 +9,254 @@ class RaidKarazhanActionContext : public NamedObjectContext public: RaidKarazhanActionContext() { - creators["karazhan attumen the huntsman stack behind"] = &RaidKarazhanActionContext::karazhan_attumen_the_huntsman_stack_behind; + // Trash + creators["mana warp stun creature before warp breach"] = + &RaidKarazhanActionContext::mana_warp_stun_creature_before_warp_breach; - creators["karazhan moroes mark target"] = &RaidKarazhanActionContext::karazhan_moroes_mark_target; + // Attumen the Huntsman + creators["attumen the huntsman mark target"] = + &RaidKarazhanActionContext::attumen_the_huntsman_mark_target; - creators["karazhan maiden of virtue position boss"] = &RaidKarazhanActionContext::karazhan_maiden_of_virtue_position_boss; - creators["karazhan maiden of virtue position ranged"] = &RaidKarazhanActionContext::karazhan_maiden_of_virtue_position_ranged; + creators["attumen the huntsman split bosses"] = + &RaidKarazhanActionContext::attumen_the_huntsman_split_bosses; - creators["karazhan big bad wolf position boss"] = &RaidKarazhanActionContext::karazhan_big_bad_wolf_position_boss; - creators["karazhan big bad wolf run away"] = &RaidKarazhanActionContext::karazhan_big_bad_wolf_run_away; + creators["attumen the huntsman stack behind"] = + &RaidKarazhanActionContext::attumen_the_huntsman_stack_behind; - creators["karazhan romulo and julianne mark target"] = &RaidKarazhanActionContext::karazhan_romulo_and_julianne_mark_target; + creators["attumen the huntsman manage dps timer"] = + &RaidKarazhanActionContext::attumen_the_huntsman_manage_dps_timer; - creators["karazhan wizard of oz mark target"] = &RaidKarazhanActionContext::karazhan_wizard_of_oz_mark_target; - creators["karazhan wizard of oz scorch strawman"] = &RaidKarazhanActionContext::karazhan_wizard_of_oz_scorch_strawman; + // Moroes + creators["moroes main tank attack boss"] = + &RaidKarazhanActionContext::moroes_main_tank_attack_boss; - creators["karazhan the curator mark target"] = &RaidKarazhanActionContext::karazhan_the_curator_mark_target; - creators["karazhan the curator position boss"] = &RaidKarazhanActionContext::karazhan_the_curator_position_boss; - creators["karazhan the curator spread ranged"] = &RaidKarazhanActionContext::karazhan_the_curator_spread_ranged; + creators["moroes mark target"] = + &RaidKarazhanActionContext::moroes_mark_target; - creators["karazhan terestian illhoof mark target"] = &RaidKarazhanActionContext::karazhan_terestian_illhoof_mark_target; + // Maiden of Virtue + creators["maiden of virtue move boss to healer"] = + &RaidKarazhanActionContext::maiden_of_virtue_move_boss_to_healer; - creators["karazhan shade of aran arcane explosion run away"] = &RaidKarazhanActionContext::karazhan_shade_of_aran_arcane_explosion_run_away; - creators["karazhan shade of aran flame wreath stop movement"] = &RaidKarazhanActionContext::karazhan_shade_of_aran_flame_wreath_stop_movement; - creators["karazhan shade of aran mark conjured elemental"] = &RaidKarazhanActionContext::karazhan_shade_of_aran_mark_conjured_elemental; - creators["karazhan shade of aran spread ranged"] = &RaidKarazhanActionContext::karazhan_shade_of_aran_spread_ranged; + creators["maiden of virtue position ranged"] = + &RaidKarazhanActionContext::maiden_of_virtue_position_ranged; - creators["karazhan netherspite block red beam"] = &RaidKarazhanActionContext::karazhan_netherspite_block_red_beam; - creators["karazhan netherspite block blue beam"] = &RaidKarazhanActionContext::karazhan_netherspite_block_blue_beam; - creators["karazhan netherspite block green beam"] = &RaidKarazhanActionContext::karazhan_netherspite_block_green_beam; - creators["karazhan netherspite avoid beam and void zone"] = &RaidKarazhanActionContext::karazhan_netherspite_avoid_beam_and_void_zone; - creators["karazhan netherspite banish phase avoid void zone"] = &RaidKarazhanActionContext::karazhan_netherspite_banish_phase_avoid_void_zone; + // The Big Bad Wolf + creators["big bad wolf position boss"] = + &RaidKarazhanActionContext::big_bad_wolf_position_boss; - creators["karazhan prince malchezaar non tank avoid hazard"] = &RaidKarazhanActionContext::karazhan_prince_malchezaar_non_tank_avoid_hazard; - creators["karazhan prince malchezaar tank avoid hazard"] = &RaidKarazhanActionContext::karazhan_prince_malchezaar_tank_avoid_hazard; + creators["big bad wolf run away from boss"] = + &RaidKarazhanActionContext::big_bad_wolf_run_away_from_boss; + + // Romulo and Julianne + creators["romulo and julianne mark target"] = + &RaidKarazhanActionContext::romulo_and_julianne_mark_target; + + // The Wizard of Oz + creators["wizard of oz mark target"] = + &RaidKarazhanActionContext::wizard_of_oz_mark_target; + creators["wizard of oz scorch strawman"] = + &RaidKarazhanActionContext::wizard_of_oz_scorch_strawman; + + // The Curator + creators["the curator mark astral flare"] = + &RaidKarazhanActionContext::the_curator_mark_astral_flare; + + creators["the curator position boss"] = + &RaidKarazhanActionContext::the_curator_position_boss; + + creators["the curator spread ranged"] = + &RaidKarazhanActionContext::the_curator_spread_ranged; + + // Terestian Illhoof + creators["terestian illhoof mark target"] = + &RaidKarazhanActionContext::terestian_illhoof_mark_target; + + // Shade of Aran + creators["shade of aran run away from arcane explosion"] = + &RaidKarazhanActionContext::shade_of_aran_run_away_from_arcane_explosion; + + creators["shade of aran stop moving during flame wreath"] = + &RaidKarazhanActionContext::shade_of_aran_stop_moving_during_flame_wreath; + + creators["shade of aran mark conjured elemental"] = + &RaidKarazhanActionContext::shade_of_aran_mark_conjured_elemental; + + creators["shade of aran ranged maintain distance"] = + &RaidKarazhanActionContext::shade_of_aran_ranged_maintain_distance; + + // Netherspite + creators["netherspite block red beam"] = + &RaidKarazhanActionContext::netherspite_block_red_beam; + + creators["netherspite block blue beam"] = + &RaidKarazhanActionContext::netherspite_block_blue_beam; + + creators["netherspite block green beam"] = + &RaidKarazhanActionContext::netherspite_block_green_beam; + + creators["netherspite avoid beam and void zone"] = + &RaidKarazhanActionContext::netherspite_avoid_beam_and_void_zone; + + creators["netherspite banish phase avoid void zone"] = + &RaidKarazhanActionContext::netherspite_banish_phase_avoid_void_zone; + + creators["netherspite manage timers and trackers"] = + &RaidKarazhanActionContext::netherspite_manage_timers_and_trackers; + + // Prince Malchezaar + creators["prince malchezaar enfeebled avoid hazard"] = + &RaidKarazhanActionContext::prince_malchezaar_enfeebled_avoid_hazard; + + creators["prince malchezaar non tank avoid infernal"] = + &RaidKarazhanActionContext::prince_malchezaar_non_tank_avoid_infernal; + + creators["prince malchezaar main tank movement"] = + &RaidKarazhanActionContext::prince_malchezaar_main_tank_movement; + + // Nightbane + creators["nightbane ground phase position boss"] = + &RaidKarazhanActionContext::nightbane_ground_phase_position_boss; + + creators["nightbane ground phase rotate ranged positions"] = + &RaidKarazhanActionContext::nightbane_ground_phase_rotate_ranged_positions; + + creators["nightbane cast fear ward on main tank"] = + &RaidKarazhanActionContext::nightbane_cast_fear_ward_on_main_tank; + + creators["nightbane control pet aggression"] = + &RaidKarazhanActionContext::nightbane_control_pet_aggression; + + creators["nightbane flight phase movement"] = + &RaidKarazhanActionContext::nightbane_flight_phase_movement; + + creators["nightbane manage timers and trackers"] = + &RaidKarazhanActionContext::nightbane_manage_timers_and_trackers; } private: - static Action* karazhan_attumen_the_huntsman_stack_behind(PlayerbotAI* botAI) { return new KarazhanAttumenTheHuntsmanStackBehindAction(botAI); } + // Trash + static Action* mana_warp_stun_creature_before_warp_breach( + PlayerbotAI* botAI) { return new ManaWarpStunCreatureBeforeWarpBreachAction(botAI); } - static Action* karazhan_moroes_mark_target(PlayerbotAI* botAI) { return new KarazhanMoroesMarkTargetAction(botAI); } + // Attumen the Huntsman + static Action* attumen_the_huntsman_mark_target( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanMarkTargetAction(botAI); } - static Action* karazhan_maiden_of_virtue_position_boss(PlayerbotAI* botAI) { return new KarazhanMaidenOfVirtuePositionBossAction(botAI); } - static Action* karazhan_maiden_of_virtue_position_ranged(PlayerbotAI* botAI) { return new KarazhanMaidenOfVirtuePositionRangedAction(botAI); } + static Action* attumen_the_huntsman_split_bosses( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanSplitBossesAction(botAI); } - static Action* karazhan_big_bad_wolf_position_boss(PlayerbotAI* botAI) { return new KarazhanBigBadWolfPositionBossAction(botAI); } - static Action* karazhan_big_bad_wolf_run_away(PlayerbotAI* botAI) { return new KarazhanBigBadWolfRunAwayAction(botAI); } + static Action* attumen_the_huntsman_stack_behind( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanStackBehindAction(botAI); } - static Action* karazhan_romulo_and_julianne_mark_target(PlayerbotAI* botAI) { return new KarazhanRomuloAndJulianneMarkTargetAction(botAI); } + static Action* attumen_the_huntsman_manage_dps_timer( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanManageDpsTimerAction(botAI); } - static Action* karazhan_wizard_of_oz_mark_target(PlayerbotAI* botAI) { return new KarazhanWizardOfOzMarkTargetAction(botAI); } - static Action* karazhan_wizard_of_oz_scorch_strawman(PlayerbotAI* botAI) { return new KarazhanWizardOfOzScorchStrawmanAction(botAI); } + // Moroes + static Action* moroes_main_tank_attack_boss( + PlayerbotAI* botAI) { return new MoroesMainTankAttackBossAction(botAI); } - static Action* karazhan_the_curator_mark_target(PlayerbotAI* botAI) { return new KarazhanTheCuratorMarkTargetAction(botAI); } - static Action* karazhan_the_curator_position_boss(PlayerbotAI* botAI) { return new KarazhanTheCuratorPositionBossAction(botAI); } - static Action* karazhan_the_curator_spread_ranged(PlayerbotAI* botAI) { return new KarazhanTheCuratorSpreadRangedAction(botAI); } + static Action* moroes_mark_target( + PlayerbotAI* botAI) { return new MoroesMarkTargetAction(botAI); } - static Action* karazhan_terestian_illhoof_mark_target(PlayerbotAI* botAI) { return new KarazhanTerestianIllhoofMarkTargetAction(botAI); } + // Maiden of Virtue + static Action* maiden_of_virtue_move_boss_to_healer( + PlayerbotAI* botAI) { return new MaidenOfVirtueMoveBossToHealerAction(botAI); } - static Action* karazhan_shade_of_aran_arcane_explosion_run_away(PlayerbotAI* botAI) { return new KarazhanShadeOfAranArcaneExplosionRunAwayAction(botAI); } - static Action* karazhan_shade_of_aran_flame_wreath_stop_movement(PlayerbotAI* botAI) { return new KarazhanShadeOfAranFlameWreathStopMovementAction(botAI); } - static Action* karazhan_shade_of_aran_mark_conjured_elemental(PlayerbotAI* botAI) { return new KarazhanShadeOfAranMarkConjuredElementalAction(botAI); } - static Action* karazhan_shade_of_aran_spread_ranged(PlayerbotAI* botAI) { return new KarazhanShadeOfAranSpreadRangedAction(botAI); } + static Action* maiden_of_virtue_position_ranged( + PlayerbotAI* botAI) { return new MaidenOfVirtuePositionRangedAction(botAI); } - static Action* karazhan_netherspite_block_red_beam(PlayerbotAI* botAI) { return new KarazhanNetherspiteBlockRedBeamAction(botAI); } - static Action* karazhan_netherspite_block_blue_beam(PlayerbotAI* botAI) { return new KarazhanNetherspiteBlockBlueBeamAction(botAI); } - static Action* karazhan_netherspite_block_green_beam(PlayerbotAI* botAI) { return new KarazhanNetherspiteBlockGreenBeamAction(botAI); } - static Action* karazhan_netherspite_avoid_beam_and_void_zone(PlayerbotAI* botAI) { return new KarazhanNetherspiteAvoidBeamAndVoidZoneAction(botAI); } - static Action* karazhan_netherspite_banish_phase_avoid_void_zone(PlayerbotAI* botAI) { return new KarazhanNetherspiteBanishPhaseAvoidVoidZoneAction(botAI); } + // The Big Bad Wolf + static Action* big_bad_wolf_position_boss( + PlayerbotAI* botAI) { return new BigBadWolfPositionBossAction(botAI); } - static Action* karazhan_prince_malchezaar_non_tank_avoid_hazard(PlayerbotAI* botAI) { return new KarazhanPrinceMalchezaarNonTankAvoidHazardAction(botAI); } - static Action* karazhan_prince_malchezaar_tank_avoid_hazard(PlayerbotAI* botAI) { return new KarazhanPrinceMalchezaarTankAvoidHazardAction(botAI); } + static Action* big_bad_wolf_run_away_from_boss( + PlayerbotAI* botAI) { return new BigBadWolfRunAwayFromBossAction(botAI); } + + // Romulo and Julianne + static Action* romulo_and_julianne_mark_target( + PlayerbotAI* botAI) { return new RomuloAndJulianneMarkTargetAction(botAI); } + + // The Wizard of Oz + static Action* wizard_of_oz_mark_target( + PlayerbotAI* botAI) { return new WizardOfOzMarkTargetAction(botAI); } + + static Action* wizard_of_oz_scorch_strawman( + PlayerbotAI* botAI) { return new WizardOfOzScorchStrawmanAction(botAI); } + + // The Curator + static Action* the_curator_mark_astral_flare( + PlayerbotAI* botAI) { return new TheCuratorMarkAstralFlareAction(botAI); } + + static Action* the_curator_position_boss( + PlayerbotAI* botAI) { return new TheCuratorPositionBossAction(botAI); } + + static Action* the_curator_spread_ranged( + PlayerbotAI* botAI) { return new TheCuratorSpreadRangedAction(botAI); } + + // Terestian Illhoof + static Action* terestian_illhoof_mark_target( + PlayerbotAI* botAI) { return new TerestianIllhoofMarkTargetAction(botAI); } + + // Shade of Aran + static Action* shade_of_aran_run_away_from_arcane_explosion( + PlayerbotAI* botAI) { return new ShadeOfAranRunAwayFromArcaneExplosionAction(botAI); } + + static Action* shade_of_aran_stop_moving_during_flame_wreath( + PlayerbotAI* botAI) { return new ShadeOfAranStopMovingDuringFlameWreathAction(botAI); } + + static Action* shade_of_aran_mark_conjured_elemental( + PlayerbotAI* botAI) { return new ShadeOfAranMarkConjuredElementalAction(botAI); } + + static Action* shade_of_aran_ranged_maintain_distance( + PlayerbotAI* botAI) { return new ShadeOfAranRangedMaintainDistanceAction(botAI); } + + // Netherspite + static Action* netherspite_block_red_beam( + PlayerbotAI* botAI) { return new NetherspiteBlockRedBeamAction(botAI); } + + static Action* netherspite_block_blue_beam( + PlayerbotAI* botAI) { return new NetherspiteBlockBlueBeamAction(botAI); } + + static Action* netherspite_block_green_beam( + PlayerbotAI* botAI) { return new NetherspiteBlockGreenBeamAction(botAI); } + + static Action* netherspite_avoid_beam_and_void_zone( + PlayerbotAI* botAI) { return new NetherspiteAvoidBeamAndVoidZoneAction(botAI); } + + static Action* netherspite_banish_phase_avoid_void_zone( + PlayerbotAI* botAI) { return new NetherspiteBanishPhaseAvoidVoidZoneAction(botAI); } + + static Action* netherspite_manage_timers_and_trackers( + PlayerbotAI* botAI) { return new NetherspiteManageTimersAndTrackersAction(botAI); } + + // Prince Malchezaar + static Action* prince_malchezaar_enfeebled_avoid_hazard( + PlayerbotAI* botAI) { return new PrinceMalchezaarEnfeebledAvoidHazardAction(botAI); } + + static Action* prince_malchezaar_non_tank_avoid_infernal( + PlayerbotAI* botAI) { return new PrinceMalchezaarNonTankAvoidInfernalAction(botAI); } + + static Action* prince_malchezaar_main_tank_movement( + PlayerbotAI* botAI) { return new PrinceMalchezaarMainTankMovementAction(botAI); } + + // Nightbane + static Action* nightbane_ground_phase_position_boss( + PlayerbotAI* botAI) { return new NightbaneGroundPhasePositionBossAction(botAI); } + + static Action* nightbane_ground_phase_rotate_ranged_positions( + PlayerbotAI* botAI) { return new NightbaneGroundPhaseRotateRangedPositionsAction(botAI); } + + static Action* nightbane_cast_fear_ward_on_main_tank( + PlayerbotAI* botAI) { return new NightbaneCastFearWardOnMainTankAction(botAI); } + + static Action* nightbane_control_pet_aggression( + PlayerbotAI* botAI) { return new NightbaneControlPetAggressionAction(botAI); } + + static Action* nightbane_flight_phase_movement( + PlayerbotAI* botAI) { return new NightbaneFlightPhaseMovementAction(botAI); } + + static Action* nightbane_manage_timers_and_trackers( + PlayerbotAI* botAI) { return new NightbaneManageTimersAndTrackersAction(botAI); } }; #endif diff --git a/src/strategy/raids/karazhan/RaidKarazhanActions.cpp b/src/strategy/raids/karazhan/RaidKarazhanActions.cpp index e45d63f7..e452cf45 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanActions.cpp +++ b/src/strategy/raids/karazhan/RaidKarazhanActions.cpp @@ -1,81 +1,215 @@ #include "RaidKarazhanActions.h" #include "RaidKarazhanHelpers.h" -#include "AiObjectContext.h" -#include "PlayerbotAI.h" -#include "PlayerbotMgr.h" -#include "PlayerbotTextMgr.h" #include "Playerbots.h" -#include "Position.h" +#include "PlayerbotTextMgr.h" -namespace +using namespace KarazhanHelpers; + +// Trash + +// Mana Warps blow up when they die for massive raid damage +// But they cannot cast the ability if they are stunned +bool ManaWarpStunCreatureBeforeWarpBreachAction::Execute(Event event) { - // Big Bad Wolf - static int currentIndex = 0; - // Netherspite - static std::map beamMoveTimes; - static std::map lastBeamMoveSideways; -} + Unit* manaWarp = GetFirstAliveUnitByEntry(botAI, NPC_MANA_WARP); + if (!manaWarp) + return false; -bool KarazhanAttumenTheHuntsmanStackBehindAction::Execute(Event event) -{ - RaidKarazhanHelpers karazhanHelper(botAI); - Unit* boss = karazhanHelper.GetFirstAliveUnitByEntry(NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); - - float distance = 5.0f; - float orientation = boss->GetOrientation() + M_PI; - float x = boss->GetPositionX(); - float y = boss->GetPositionY(); - float z = boss->GetPositionZ(); - float rx = x + cos(orientation) * distance; - float ry = y + sin(orientation) * distance; - - if (bot->GetExactDist2d(rx, ry) > 1.0f) + static const std::array spells = { - return MoveTo(bot->GetMapId(), rx, ry, z, false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + "bash", + "concussion blow", + "hammer of justice", + "kidney shot", + "maim", + "revenge stun", + "shadowfury", + "shockwave" + }; + + for (const char* spell : spells) + { + if (botAI->CanCastSpell(spell, manaWarp)) + return botAI->CastSpell(spell, manaWarp); } return false; } -bool KarazhanAttumenTheHuntsmanStackBehindAction::isUseful() -{ - RaidKarazhanHelpers karazhanHelper(botAI); - Unit* boss = karazhanHelper.GetFirstAliveUnitByEntry(NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); +// Attumen the Huntsman - return boss && !(botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot); +// Prioritize Midnight until Attumen is mounted +bool AttumenTheHuntsmanMarkTargetAction::Execute(Event event) +{ + Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + if (attumenMounted) + { + if (IsMapIDTimerManager(botAI, bot)) + MarkTargetWithStar(bot, attumenMounted); + + SetRtiTarget(botAI, "star", attumenMounted); + + if (bot->GetTarget() != attumenMounted->GetGUID()) + { + bot->SetTarget(attumenMounted->GetGUID()); + return Attack(attumenMounted); + } + } + else if (Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight")) + { + if (IsMapIDTimerManager(botAI, bot)) + MarkTargetWithStar(bot, midnight); + + if (!botAI->IsAssistTankOfIndex(bot, 0)) + { + SetRtiTarget(botAI, "star", midnight); + + if (bot->GetTarget() != midnight->GetGUID()) + { + bot->SetTarget(midnight->GetGUID()); + return Attack(midnight); + } + } + } + + return false; } -bool KarazhanMoroesMarkTargetAction::Execute(Event event) +// Off tank should move Attumen out of the way so he doesn't cleave bots +bool AttumenTheHuntsmanSplitBossesAction::Execute(Event event) { - RaidKarazhanHelpers karazhanHelper(botAI); + Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"); + if (!midnight) + return false; + Unit* attumen = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN); + if (!attumen) + return false; + + MarkTargetWithSquare(bot, attumen); + SetRtiTarget(botAI, "square", attumen); + + if (bot->GetVictim() != attumen) + return Attack(attumen); + + if (attumen->GetVictim() == bot && midnight->GetVictim() != bot) + { + const float safeDistance = 6.0f; + Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance); + if (nearestPlayer && attumen->GetExactDist2d(nearestPlayer) < safeDistance) + return MoveFromGroup(safeDistance + 2.0f); + } + + return false; +} + +// Stack behind mounted Attumen (inside minimum range of Berserker Charge) +bool AttumenTheHuntsmanStackBehindAction::Execute(Event event) +{ + Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + if (!attumenMounted) + return false; + + const float distanceBehind = botAI->IsRanged(bot) ? 6.0f : 2.0f; + float orientation = attumenMounted->GetOrientation() + M_PI; + float rearX = attumenMounted->GetPositionX() + std::cos(orientation) * distanceBehind; + float rearY = attumenMounted->GetPositionY() + std::sin(orientation) * distanceBehind; + + if (bot->GetExactDist2d(rearX, rearY) > 1.0f) + { + return MoveTo(KARAZHAN_MAP_ID, rearX, rearY, attumenMounted->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +// Reset timer for bots to pause DPS when Attumen mounts Midnight +bool AttumenTheHuntsmanManageDpsTimerAction::Execute(Event event) +{ + Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"); + if (!midnight) + return false; + + if (midnight && midnight->GetHealth() == midnight->GetMaxHealth()) + attumenDpsWaitTimer.erase(KARAZHAN_MAP_ID); + + // Midnight is still present as a separate (invisible) unit after Attumen mounts + // So this block can be reached + Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + if (!attumenMounted) + return false; + + const time_t now = std::time(nullptr); + + if (attumenMounted) + attumenDpsWaitTimer.try_emplace(KARAZHAN_MAP_ID, now); + + return false; +} + +// Moroes + +bool MoroesMainTankAttackBossAction::Execute(Event event) +{ + Unit* moroes = AI_VALUE2(Unit*, "find target", "moroes"); + if (!moroes) + return false; + + MarkTargetWithCircle(bot, moroes); + SetRtiTarget(botAI, "circle", moroes); + + if (bot->GetVictim() != moroes) + return Attack(moroes); + + return false; +} + +// Mark targets with skull in the recommended kill order +bool MoroesMarkTargetAction::Execute(Event event) +{ Unit* dorothea = AI_VALUE2(Unit*, "find target", "baroness dorothea millstipe"); Unit* catriona = AI_VALUE2(Unit*, "find target", "lady catriona von'indi"); Unit* keira = AI_VALUE2(Unit*, "find target", "lady keira berrybuck"); Unit* rafe = AI_VALUE2(Unit*, "find target", "baron rafe dreuger"); Unit* robin = AI_VALUE2(Unit*, "find target", "lord robin daris"); Unit* crispin = AI_VALUE2(Unit*, "find target", "lord crispin ference"); - Unit* target = karazhanHelper.GetFirstAliveUnit({dorothea, catriona, keira, rafe, robin, crispin}); + Unit* target = GetFirstAliveUnit({dorothea, catriona, keira, rafe, robin, crispin}); - karazhanHelper.MarkTargetWithSkull(target); + if (target) + { + if (IsMapIDTimerManager(botAI, bot)) + MarkTargetWithSkull(bot, target); + + SetRtiTarget(botAI, "skull", target); + } return false; } -bool KarazhanMaidenOfVirtuePositionBossAction::Execute(Event event) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "maiden of virtue"); - Unit* healer = nullptr; +// Maiden of Virtue +// Tank the boss in the center of the room +// Move to healers after Repentenace to break the stun +bool MaidenOfVirtueMoveBossToHealerAction::Execute(Event event) +{ + Unit* maiden = AI_VALUE2(Unit*, "find target", "maiden of virtue"); + if (!maiden) + return false; + + if (bot->GetVictim() != maiden) + return Attack(maiden); + + Unit* healer = nullptr; if (Group* group = bot->GetGroup()) { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || !botAI->IsHeal(member) || !member->HasAura(SPELL_REPENTANCE)) - { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !botAI->IsHeal(member) || + !member->HasAura(SPELL_REPENTANCE)) continue; - } + healer = member; break; } @@ -84,580 +218,534 @@ bool KarazhanMaidenOfVirtuePositionBossAction::Execute(Event event) if (healer) { float angle = healer->GetOrientation(); - float targetX = healer->GetPositionX() + cos(angle) * 6.0f; - float targetY = healer->GetPositionY() + sin(angle) * 6.0f; - float targetZ = healer->GetPositionZ(); + float targetX = healer->GetPositionX() + std::cos(angle) * 6.0f; + float targetY = healer->GetPositionY() + std::sin(angle) * 6.0f; { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); + return MoveTo(KARAZHAN_MAP_ID, targetX, targetY, healer->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); } } - const float maxDistance = 3.0f; - const float distanceToBossPosition = boss->GetExactDist2d(KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION); - - if (distanceToBossPosition > maxDistance) + const Position& position = MAIDEN_OF_VIRTUE_BOSS_POSITION; + const float maxDistance = 2.0f; + float distanceToPosition = maiden->GetExactDist2d(position); + if (distanceToPosition > maxDistance) { - float dX = KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION.GetPositionX() - boss->GetPositionX(); - float dY = KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION.GetPositionY() - boss->GetPositionY(); - float mX = KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION.GetPositionX() + (dX / distanceToBossPosition) * maxDistance; - float mY = KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION.GetPositionY() + (dY / distanceToBossPosition) * maxDistance; + float dX = position.GetPositionX() - maiden->GetPositionX(); + float dY = position.GetPositionY() - maiden->GetPositionY(); + float mX = position.GetPositionX() + (dX / distanceToPosition) * maxDistance; + float mY = position.GetPositionY() + (dY / distanceToPosition) * maxDistance; { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), mX, mY, - bot->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + return MoveTo(KARAZHAN_MAP_ID, mX, mY, position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); } } return false; } -bool KarazhanMaidenOfVirtuePositionBossAction::isUseful() +// Spread out ranged DPS between the pillars +bool MaidenOfVirtuePositionRangedAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "maiden of virtue"); + const uint8 maxIndex = 7; + uint8 index = 0; - return boss && botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot; -} - -bool KarazhanMaidenOfVirtuePositionRangedAction::Execute(Event event) -{ - int maxIndex = 7; - int index = 0; - - const GuidVector members = AI_VALUE(GuidVector, "group members"); - - for (auto const& memberGuid : members) + if (Group* group = bot->GetGroup()) { - Unit* member = botAI->GetUnit(memberGuid); - - if (!member || !botAI->IsRanged(member->ToPlayer())) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - continue; - } + Player* member = ref->GetSource(); + if (!member || !botAI->IsRanged(member)) + continue; - if (member == bot) - break; + if (member == bot) + break; - if (index >= maxIndex) - { - index = 0; - continue; + if (index >= maxIndex) + { + index = 0; + continue; + } + index++; } - index++; } - float distance = bot->GetExactDist2d(KARAZHAN_MAIDEN_OF_VIRTUE_RANGED_POSITION[index]); - const float maxDistance = 2.0f; - if (distance > maxDistance) + const Position& position = MAIDEN_OF_VIRTUE_RANGED_POSITION[index]; + if (bot->GetExactDist2d(position) > 2.0f) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), KARAZHAN_MAIDEN_OF_VIRTUE_RANGED_POSITION[index].GetPositionX(), - KARAZHAN_MAIDEN_OF_VIRTUE_RANGED_POSITION[index].GetPositionY(), bot->GetPositionZ(), false, - false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } return false; } -bool KarazhanMaidenOfVirtuePositionRangedAction::isUseful() +// The Big Bad Wolf + +// Tank the boss at the front left corner of the stage +bool BigBadWolfPositionBossAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "maiden of virtue"); - - return boss && botAI->IsRanged(bot); -} - -bool KarazhanBigBadWolfPositionBossAction::Execute(Event event) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "the big bad wolf"); - - const float maxDistance = 3.0f; - const float distanceToBossPosition = boss->GetExactDist2d(KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION); - - if (distanceToBossPosition > maxDistance) - { - float dX = KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION.GetPositionX() - boss->GetPositionX(); - float dY = KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION.GetPositionY() - boss->GetPositionY(); - - float mX = KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION.GetPositionX() + (dX / distanceToBossPosition) * maxDistance; - float mY = KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION.GetPositionY() + (dY / distanceToBossPosition) * maxDistance; - - float moveDistance = bot->GetExactDist2d(mX, mY); - if (moveDistance < 0.5f) - { - return false; - } - - return MoveTo(bot->GetMapId(), mX, mY, bot->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); - } - - return false; -} - -bool KarazhanBigBadWolfPositionBossAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "the big bad wolf"); - - return boss && botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot && - !bot->HasAura(SPELL_LITTLE_RED_RIDING_HOOD); -} - -bool KarazhanBigBadWolfRunAwayAction::Execute(Event event) -{ - constexpr float threshold = 1.0f; - Position target = KARAZHAN_BIG_BAD_WOLF_RUN_POSITION[currentIndex]; - - while (bot->GetExactDist2d(target.GetPositionX(), target.GetPositionY()) < threshold) - { - currentIndex = (currentIndex + 1) % 4; - target = KARAZHAN_BIG_BAD_WOLF_RUN_POSITION[currentIndex]; - } - - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), target.GetPositionX(), target.GetPositionY(), target.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED); -} - -bool KarazhanBigBadWolfRunAwayAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "the big bad wolf"); - - return boss && bot->HasAura(SPELL_LITTLE_RED_RIDING_HOOD); -} - -bool KarazhanRomuloAndJulianneMarkTargetAction::Execute(Event event) -{ - Unit* target = nullptr; - Unit* romulo = AI_VALUE2(Unit*, "find target", "romulo"); - Unit* julianne = AI_VALUE2(Unit*, "find target", "julianne"); - - const int maxPctDifference = 10; - if (julianne->GetHealthPct() + maxPctDifference < romulo->GetHealthPct() || julianne->GetHealthPct() < 1.0f) - { - target = romulo; - } - else if (romulo->GetHealthPct() + maxPctDifference < julianne->GetHealthPct() || romulo->GetHealthPct() < 1.0f) - { - target = julianne; - } - if (!target) - { + Unit* wolf = AI_VALUE2(Unit*, "find target", "the big bad wolf"); + if (!wolf) return false; - } - RaidKarazhanHelpers karazhanHelper(botAI); - karazhanHelper.MarkTargetWithSkull(target); + if (bot->GetVictim() != wolf) + return Attack(wolf); + + if (wolf->GetVictim() == bot) + { + const Position& position = BIG_BAD_WOLF_BOSS_POSITION; + const float maxStep = 2.0f; + float dist = wolf->GetExactDist2d(position); + + if (dist > 0.0f && dist > maxStep) + { + float dX = position.GetPositionX() - wolf->GetPositionX(); + float dY = position.GetPositionY() - wolf->GetPositionY(); + float moveDist = std::min(maxStep, dist); + float moveX = wolf->GetPositionX() + (dX / dist) * moveDist; + float moveY = wolf->GetPositionY() + (dY / dist) * moveDist; + + return MoveTo(KARAZHAN_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } return false; } -bool KarazhanWizardOfOzMarkTargetAction::Execute(Event event) +// Run away, little girl, run away +bool BigBadWolfRunAwayFromBossAction::Execute(Event event) +{ + const ObjectGuid botGuid = bot->GetGUID(); + uint8 index = bigBadWolfRunIndex.count(botGuid) ? bigBadWolfRunIndex[botGuid] : 0; + + while (bot->GetExactDist2d(BIG_BAD_WOLF_RUN_POSITION[index].GetPositionX(), + BIG_BAD_WOLF_RUN_POSITION[index].GetPositionY()) < 1.0f) + { + index = (index + 1) % 4; + } + + bigBadWolfRunIndex[botGuid] = index; + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + + const Position& position = BIG_BAD_WOLF_RUN_POSITION[index]; + return MoveTo(KARAZHAN_MAP_ID, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); +} + +// Romulo and Julianne + +// Keep the couple within 10% HP of each other +bool RomuloAndJulianneMarkTargetAction::Execute(Event event) +{ + Unit* romulo = AI_VALUE2(Unit*, "find target", "romulo"); + if (!romulo) + return false; + + Unit* julianne = AI_VALUE2(Unit*, "find target", "julianne"); + if (!julianne) + return false; + + Unit* target = nullptr; + const float maxPctDifference = 10.0f; + + if (julianne->GetHealthPct() + maxPctDifference < romulo->GetHealthPct() || julianne->GetHealthPct() < 1.0f) + target = romulo; + else if (romulo->GetHealthPct() + maxPctDifference < julianne->GetHealthPct() || romulo->GetHealthPct() < 1.0f) + target = julianne; + else + target = (romulo->GetHealthPct() >= julianne->GetHealthPct()) ? romulo : julianne; + + if (target) + MarkTargetWithSkull(bot, target); + + return false; +} + +// The Wizard of Oz + +// Mark targets with skull in the recommended kill order +bool WizardOfOzMarkTargetAction::Execute(Event event) { - RaidKarazhanHelpers karazhanHelper(botAI); Unit* dorothee = AI_VALUE2(Unit*, "find target", "dorothee"); Unit* tito = AI_VALUE2(Unit*, "find target", "tito"); Unit* roar = AI_VALUE2(Unit*, "find target", "roar"); Unit* strawman = AI_VALUE2(Unit*, "find target", "strawman"); Unit* tinhead = AI_VALUE2(Unit*, "find target", "tinhead"); Unit* crone = AI_VALUE2(Unit*, "find target", "the crone"); - Unit* target = karazhanHelper.GetFirstAliveUnit({dorothee, tito, roar, strawman, tinhead, crone}); + Unit* target = GetFirstAliveUnit({dorothee, tito, roar, strawman, tinhead, crone}); - karazhanHelper.MarkTargetWithSkull(target); + if (target) + MarkTargetWithSkull(bot, target); return false; } -bool KarazhanWizardOfOzScorchStrawmanAction::Execute(Event event) +// Mages spam Scorch on Strawman to disorient him +bool WizardOfOzScorchStrawmanAction::Execute(Event event) { Unit* strawman = AI_VALUE2(Unit*, "find target", "strawman"); - if (!strawman || !strawman->IsAlive() || bot->getClass() != CLASS_MAGE) - { - return false; - } - - if (botAI->CanCastSpell("scorch", strawman)) - { - botAI->CastSpell("scorch", strawman); - } + if (strawman && botAI->CanCastSpell("scorch", strawman)) + return botAI->CastSpell("scorch", strawman); return false; } -bool KarazhanTheCuratorMarkTargetAction::Execute(Event event) -{ - Unit* target = AI_VALUE2(Unit*, "find target", "astral flare"); - if (!target || !target->IsAlive()) - { - return false; - } +// The Curator - RaidKarazhanHelpers karazhanHelper(botAI); - karazhanHelper.MarkTargetWithSkull(target); +// Prioritize destroying Astral Flares +bool TheCuratorMarkAstralFlareAction::Execute(Event event) +{ + Unit* flare = AI_VALUE2(Unit*, "find target", "astral flare"); + if (!flare) + return false; + + if (IsMapIDTimerManager(botAI, bot)) + MarkTargetWithSkull(bot, flare); + + SetRtiTarget(botAI, "skull", flare); return false; } -bool KarazhanTheCuratorPositionBossAction::Execute(Event event) +// Tank the boss in the center of the hallway near the Guardian's Library +// Main tank and off tank will attack the boss; others will focus on Astral Flares +bool TheCuratorPositionBossAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "the curator"); - const float maxDistance = 3.0f; - const float distanceToBossPosition = boss->GetExactDist2d(KARAZHAN_THE_CURATOR_BOSS_POSITION); + Unit* curator = AI_VALUE2(Unit*, "find target", "the curator"); + if (!curator) + return false; - if (distanceToBossPosition > maxDistance) + MarkTargetWithCircle(bot, curator); + SetRtiTarget(botAI, "circle", curator); + + if (bot->GetVictim() != curator) + return Attack(curator); + + if (curator->GetVictim() == bot) { - float dX = KARAZHAN_THE_CURATOR_BOSS_POSITION.GetPositionX() - boss->GetPositionX(); - float dY = KARAZHAN_THE_CURATOR_BOSS_POSITION.GetPositionY() - boss->GetPositionY(); - float mX = KARAZHAN_THE_CURATOR_BOSS_POSITION.GetPositionX() + (dX / distanceToBossPosition) * maxDistance; - float mY = KARAZHAN_THE_CURATOR_BOSS_POSITION.GetPositionY() + (dY / distanceToBossPosition) * maxDistance; + const Position& position = THE_CURATOR_BOSS_POSITION; + const float maxDistance = 3.0f; + float distanceToBossPosition = curator->GetExactDist2d(position); + + if (distanceToBossPosition > maxDistance) { - return MoveTo(bot->GetMapId(), mX, mY, - bot->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, - false); + float dX = position.GetPositionX() - curator->GetPositionX(); + float dY = position.GetPositionY() - curator->GetPositionY(); + float mX = position.GetPositionX() + (dX / distanceToBossPosition) * maxDistance; + float mY = position.GetPositionY() + (dY / distanceToBossPosition) * maxDistance; + + return MoveTo(KARAZHAN_MAP_ID, mX, mY, position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); } } return false; } -bool KarazhanTheCuratorPositionBossAction::isUseful() +// Spread out ranged DPS to avoid Arcing Sear damage +bool TheCuratorSpreadRangedAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "the curator"); - - return boss && botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot; -} - -bool KarazhanTheCuratorSpreadRangedAction::Execute(Event event) -{ - RaidKarazhanHelpers karazhanHelper(botAI); const float minDistance = 5.0f; - Unit* nearestPlayer = karazhanHelper.GetNearestPlayerInRadius(minDistance); + Unit* nearestPlayer = GetNearestPlayerInRadius(bot, minDistance); if (nearestPlayer) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - + bot->InterruptNonMeleeSpells(true); return FleePosition(nearestPlayer->GetPosition(), minDistance); } return false; } -bool KarazhanTheCuratorSpreadRangedAction::isUseful() +// Terestian Illhoof + +// Prioritize (1) Demon Chains, (2) Kil'rek, (3) Illhoof +bool TerestianIllhoofMarkTargetAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "the curator"); + Unit* demonChains = AI_VALUE2(Unit*, "find target", "demon chains"); + Unit* kilrek = AI_VALUE2(Unit*, "find target", "kil'rek"); + Unit* illhoof = AI_VALUE2(Unit*, "find target", "terestian illhoof"); + Unit* target = GetFirstAliveUnit({demonChains, kilrek, illhoof}); - return boss && botAI->IsRanged(bot); -} - -bool KarazhanTerestianIllhoofMarkTargetAction::Execute(Event event) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "terestian illhoof"); - if (!boss) - { - return false; - } - - RaidKarazhanHelpers karazhanHelper(botAI); - Unit* target = karazhanHelper.GetFirstAliveUnitByEntry(NPC_DEMON_CHAINS); - if (!target || !target->IsAlive()) - { - target = karazhanHelper.GetFirstAliveUnitByEntry(NPC_KILREK); - if (!target || !target->IsAlive()) - { - target = boss; - } - } - karazhanHelper.MarkTargetWithSkull(target); + if (target) + MarkTargetWithSkull(bot, target); return false; } -bool KarazhanShadeOfAranArcaneExplosionRunAwayAction::Execute(Event event) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); - const float safeDistance = 20.0f; - const float distance = bot->GetDistance2d(boss); +// Shade of Aran +// Run to the edge of the room to avoid Arcane Explosion +bool ShadeOfAranRunAwayFromArcaneExplosionAction::Execute(Event event) +{ + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + if (!aran) + return false; + + const float safeDistance = 20.0f; + float distance = bot->GetDistance2d(aran); if (distance < safeDistance) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveAway(boss, safeDistance - distance); + bot->InterruptNonMeleeSpells(true); + return MoveAway(aran, safeDistance - distance); } return false; } -bool KarazhanShadeOfAranArcaneExplosionRunAwayAction::isUseful() +// I will not move when Flame Wreath is cast or the raid blows up +bool ShadeOfAranStopMovingDuringFlameWreathAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); + AI_VALUE(LastMovement&, "last movement").Set(nullptr); - return boss && boss->IsAlive() && boss->HasUnitState(UNIT_STATE_CASTING) && - boss->FindCurrentSpellBySpellId(SPELL_ARCANE_EXPLOSION); -} - -bool KarazhanShadeOfAranFlameWreathStopMovementAction::Execute(Event event) -{ - RaidKarazhanHelpers karazhanHelper(botAI); - if (karazhanHelper.IsFlameWreathActive()) + if (bot->isMoving()) { - AI_VALUE(LastMovement&, "last movement").Set(nullptr); bot->GetMotionMaster()->Clear(); - if (bot->isMoving()) - { - bot->StopMoving(); - } + bot->StopMoving(); return true; } return false; } -bool KarazhanShadeOfAranMarkConjuredElementalAction::Execute(Event event) +// Mark Conjured Elementals with skull so DPS can burn them down +bool ShadeOfAranMarkConjuredElementalAction::Execute(Event event) { - RaidKarazhanHelpers karazhanHelper(botAI); - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); - Unit* target = karazhanHelper.GetFirstAliveUnitByEntry(NPC_CONJURED_ELEMENTAL); + Unit* elemental = GetFirstAliveUnitByEntry(botAI, NPC_CONJURED_ELEMENTAL); - if (!boss || !boss->IsAlive() || - !target || !target->IsAlive() || target->HasAura(SPELL_WARLOCK_BANISH)) - { - return false; - } - - karazhanHelper.MarkTargetWithSkull(target); + if (elemental) + MarkTargetWithSkull(bot, elemental); return false; } -bool KarazhanShadeOfAranSpreadRangedAction::Execute(Event event) +// Don't get closer than 11 yards to Aran to avoid counterspell +// Don't get farther than 15 yards from Aran to avoid getting stuck in alcoves +bool ShadeOfAranRangedMaintainDistanceAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + if (!aran) + return false; - const float maxBossDistance = 12.0f; - float bossDistance = bot->GetExactDist2d(boss); - if (bossDistance > maxBossDistance) + Group* group = bot->GetGroup(); + if (!group) + return false; + + const float minDist = 11.0f; + const float maxDist = 15.0f; + const float ringIncrement = M_PI / 8; + const float distIncrement = 0.5f; + + float bestX = 0, bestY = 0, bestMoveDist = std::numeric_limits::max(); + bool found = false; + + for (float dist = minDist; dist <= maxDist; dist += distIncrement) { - float dX = bot->GetPositionX() - boss->GetPositionX(); - float dY = bot->GetPositionY() - boss->GetPositionY(); - float length = std::sqrt(dX * dX + dY * dY); - dX /= length; - dY /= length; - float tX = boss->GetPositionX() + dX * maxBossDistance; - float tY = boss->GetPositionY() + dY * maxBossDistance; + for (float angle = 0; angle < 2 * M_PI; angle += ringIncrement) { - return MoveTo(bot->GetMapId(), tX, tY, bot->GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); + float x = aran->GetPositionX() + std::cos(angle) * dist; + float y = aran->GetPositionY() + std::sin(angle) * dist; + + bool tooClose = false; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot || !member->IsAlive()) + continue; + if (member->GetExactDist2d(x, y) < 3.0f) + { + tooClose = true; + break; + } + } + if (tooClose) + continue; + + float moveDist = bot->GetExactDist2d(x, y); + if (moveDist < bestMoveDist) + { + bestMoveDist = moveDist; + bestX = x; + bestY = y; + found = true; + } } } - const float minDistance = 5.0f; - RaidKarazhanHelpers karazhanHelper(botAI); - Unit* nearestPlayer = karazhanHelper.GetNearestPlayerInRadius(minDistance); - if (nearestPlayer) + if (found && bestMoveDist > 0.5f) { - return FleePosition(nearestPlayer->GetPosition(), minDistance); + return MoveTo(KARAZHAN_MAP_ID, bestX, bestY, bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); } return false; } -bool KarazhanShadeOfAranSpreadRangedAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); - RaidKarazhanHelpers karazhanHelper(botAI); - - return boss && boss->IsAlive() && botAI->IsRanged(bot) && !karazhanHelper.IsFlameWreathActive() && - !(boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_ARCANE_EXPLOSION)); -} +// Netherspite // One tank bot per phase will dance in and out of the red beam (5 seconds in, 5 seconds out) -// Tank bots will ignore void zones--their positioning is too important -bool KarazhanNetherspiteBlockRedBeamAction::Execute(Event event) +// Tank bots will ignore void zones--their positioning is too important to risk losing beam control +bool NetherspiteBlockRedBeamAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite) + return false; + Unit* redPortal = bot->FindNearestCreature(NPC_RED_PORTAL, 150.0f); + if (!redPortal) + return false; - RaidKarazhanHelpers karazhanHelper(botAI); - static std::map wasBlockingRedBeam; - ObjectGuid botGuid = bot->GetGUID(); - auto [redBlocker, greenBlocker, blueBlocker] = karazhanHelper.GetCurrentBeamBlockers(); + const ObjectGuid botGuid = bot->GetGUID(); + auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot); bool isBlockingNow = (bot == redBlocker); - bool wasBlocking = wasBlockingRedBeam[botGuid]; - Position beamPos = karazhanHelper.GetPositionOnBeam(boss, redPortal, 18.0f); + auto it = _wasBlockingRedBeam.find(botGuid); + bool wasBlocking = (it != _wasBlockingRedBeam.end()) ? it->second : false; + + Position beamPos = GetPositionOnBeam(netherspite, redPortal, 18.0f); if (isBlockingNow) { if (!wasBlocking) { - std::map ph; - ph["%player"] = bot->GetName(); + std::map placeholders{{"%player", bot->GetName()}}; std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( - "netherspite_beam_blocking_red", "%player is moving to block the red beam!", ph); + "netherspite_beam_blocking_red", "%player is moving to block the red beam!", placeholders); bot->Yell(text, LANG_UNIVERSAL); } - wasBlockingRedBeam[botGuid] = true; + _wasBlockingRedBeam[botGuid] = true; - uint32 intervalSecs = 5; - - if (beamMoveTimes[botGuid] == 0) - { - beamMoveTimes[botGuid] = time(nullptr); - lastBeamMoveSideways[botGuid] = false; - } - if (time(nullptr) - beamMoveTimes[botGuid] >= intervalSecs) + const uint8 intervalSecs = 5; + if (std::time(nullptr) - redBeamMoveTimer[botGuid] >= intervalSecs) { lastBeamMoveSideways[botGuid] = !lastBeamMoveSideways[botGuid]; - beamMoveTimes[botGuid] = time(nullptr); + redBeamMoveTimer[botGuid] = std::time(nullptr); } if (!lastBeamMoveSideways[botGuid]) - { - return MoveTo(bot->GetMapId(), beamPos.GetPositionX(), beamPos.GetPositionY(), beamPos.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED); - } + return MoveTo(KARAZHAN_MAP_ID, beamPos.GetPositionX(), beamPos.GetPositionY(), beamPos.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); else { - float bx = boss->GetPositionX(); - float by = boss->GetPositionY(); - float px = redPortal->GetPositionX(); - float py = redPortal->GetPositionY(); - float dx = px - bx; - float dy = py - by; - float length = sqrt(dx*dx + dy*dy); + float length = netherspite->GetExactDist2d(redPortal); if (length == 0.0f) - { return false; - } - dx /= length; - dy /= length; + float dx = (redPortal->GetPositionX() - netherspite->GetPositionX()) / length; + float dy = (redPortal->GetPositionY() - netherspite->GetPositionY()) / length; float perpDx = -dy; float perpDy = dx; float sideX = beamPos.GetPositionX() + perpDx * 3.0f; float sideY = beamPos.GetPositionY() + perpDy * 3.0f; float sideZ = beamPos.GetPositionZ(); - return MoveTo(bot->GetMapId(), sideX, sideY, sideZ, false, false, false, true, - MovementPriority::MOVEMENT_FORCED); + return MoveTo(KARAZHAN_MAP_ID, sideX, sideY, sideZ, false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); } } - wasBlockingRedBeam[botGuid] = false; + _wasBlockingRedBeam[botGuid] = false; return false; } -bool KarazhanNetherspiteBlockRedBeamAction::isUseful() +Position NetherspiteBlockRedBeamAction::GetPositionOnBeam(Unit* netherspite, Unit* portal, float distanceFromBoss) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - Unit* redPortal = bot->FindNearestCreature(NPC_RED_PORTAL, 150.0f); + float bx = netherspite->GetPositionX(); + float by = netherspite->GetPositionY(); + float bz = netherspite->GetPositionZ(); + float px = portal->GetPositionX(); + float py = portal->GetPositionY(); - ObjectGuid botGuid = bot->GetGUID(); - static std::map lastBossBanishState; - bool bossIsBanished = boss && boss->HasAura(SPELL_NETHERSPITE_BANISHED); + float dx = px - bx; + float dy = py - by; + float length = netherspite->GetExactDist2d(px, py); + if (length == 0.0f) + return Position(bx, by, bz); - if (lastBossBanishState[botGuid] != bossIsBanished) - { - if (!bossIsBanished) - { - beamMoveTimes[botGuid] = 0; - lastBeamMoveSideways[botGuid] = false; - } - lastBossBanishState[botGuid] = bossIsBanished; - } + dx /= length; + dy /= length; + float targetX = bx + dx * distanceFromBoss; + float targetY = by + dy * distanceFromBoss; + float targetZ = bz; - return boss && redPortal && !bossIsBanished; + return Position(targetX, targetY, targetZ); } -// Two non-Rogue/Warrior DPS bots will block the blue beam for each phase (swap at 26 debuff stacks) +// Two non-Rogue/Warrior DPS bots will block the blue beam for each phase (swap at 25 debuff stacks) // When avoiding void zones, blocking bots will move along the beam to continue blocking -bool KarazhanNetherspiteBlockBlueBeamAction::Execute(Event event) +bool NetherspiteBlockBlueBeamAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - Unit* bluePortal = bot->FindNearestCreature(NPC_BLUE_PORTAL, 150.0f); + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite) + return false; - RaidKarazhanHelpers karazhanHelper(botAI); - static std::map wasBlockingBlueBeam; - ObjectGuid botGuid = bot->GetGUID(); - auto [redBlocker, greenBlocker, blueBlocker] = karazhanHelper.GetCurrentBeamBlockers(); + Unit* bluePortal = bot->FindNearestCreature(NPC_BLUE_PORTAL, 150.0f); + if (!bluePortal) + return false; + + const ObjectGuid botGuid = bot->GetGUID(); + auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot); bool isBlockingNow = (bot == blueBlocker); - bool wasBlocking = wasBlockingBlueBeam[botGuid]; + + auto it = _wasBlockingBlueBeam.find(botGuid); + bool wasBlocking = (it != _wasBlockingBlueBeam.end()) ? it->second : false; if (wasBlocking && !isBlockingNow) { - std::map ph; - ph["%player"] = bot->GetName(); + std::map placeholders{{"%player", bot->GetName()}}; std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( - "netherspite_beam_leaving_blue", "%player is leaving the blue beam--next blocker up!", ph); + "netherspite_beam_leaving_blue", "%player is leaving the blue beam--next blocker up!", placeholders); bot->Yell(text, LANG_UNIVERSAL); - wasBlockingBlueBeam[botGuid] = false; - - return false; + _wasBlockingBlueBeam[botGuid] = false; } if (isBlockingNow) { if (!wasBlocking) { - std::map ph; - ph["%player"] = bot->GetName(); + std::map placeholders{{"%player", bot->GetName()}}; std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( - "netherspite_beam_blocking_blue", "%player is moving to block the blue beam!", ph); + "netherspite_beam_blocking_blue", "%player is moving to block the blue beam!", placeholders); bot->Yell(text, LANG_UNIVERSAL); } - wasBlockingBlueBeam[botGuid] = true; + _wasBlockingBlueBeam[botGuid] = true; - std::vector voidZones = karazhanHelper.GetAllVoidZones(); - float bx = boss->GetPositionX(); - float by = boss->GetPositionY(); - float bz = boss->GetPositionZ(); + float idealDistance = botAI->IsRanged(bot) ? 25.0f : 18.0f; + std::vector voidZones = GetAllVoidZones(botAI, bot); + + float bx = netherspite->GetPositionX(); + float by = netherspite->GetPositionY(); + float bz = netherspite->GetPositionZ(); float px = bluePortal->GetPositionX(); float py = bluePortal->GetPositionY(); + float dx = px - bx; float dy = py - by; - float length = sqrt(dx*dx + dy*dy); + float length = netherspite->GetExactDist2d(bluePortal); if (length == 0.0f) - { return false; - } dx /= length; dy /= length; float bestDist = 150.0f; Position bestPos; bool found = false; + for (float dist = 18.0f; dist <= 30.0f; dist += 0.5f) { float candidateX = bx + dx * dist; float candidateY = by + dy * dist; float candidateZ = bz; - bool outsideAllVoidZones = true; - for (Unit* voidZone : voidZones) - { - float voidZoneDist = sqrt(pow(candidateX - voidZone->GetPositionX(), 2) + - pow(candidateY - voidZone->GetPositionY(), 2)); - if (voidZoneDist < 4.0f) - { - outsideAllVoidZones = false; - break; - } - } - if (!outsideAllVoidZones) - { + if (!IsSafePosition(candidateX, candidateY, candidateZ, voidZones, 4.0f)) continue; - } - float distToIdeal = fabs(dist - 18.0f); + + float distToIdeal = fabs(dist - idealDistance); if (!found || distToIdeal < bestDist) { bestDist = distToIdeal; @@ -665,108 +753,90 @@ bool KarazhanNetherspiteBlockBlueBeamAction::Execute(Event event) found = true; } } + if (found) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); } return false; } - wasBlockingBlueBeam[botGuid] = false; + _wasBlockingBlueBeam[botGuid] = false; return false; } -bool KarazhanNetherspiteBlockBlueBeamAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - Unit* bluePortal = bot->FindNearestCreature(NPC_BLUE_PORTAL, 150.0f); - - return boss && bluePortal && !boss->HasAura(SPELL_NETHERSPITE_BANISHED); -} - -// Two healer bots will block the green beam for each phase (swap at 26 debuff stacks) +// Two healer bots will block the green beam for each phase (swap at 25 debuff stacks) // OR one rogue or DPS warrior bot will block the green beam for an entire phase (if they begin the phase as the blocker) // When avoiding void zones, blocking bots will move along the beam to continue blocking -bool KarazhanNetherspiteBlockGreenBeamAction::Execute(Event event) +bool NetherspiteBlockGreenBeamAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - Unit* greenPortal = bot->FindNearestCreature(NPC_GREEN_PORTAL, 150.0f); + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite) + return false; - RaidKarazhanHelpers karazhanHelper(botAI); - static std::map wasBlockingGreenBeam; - ObjectGuid botGuid = bot->GetGUID(); - auto [redBlocker, greenBlocker, blueBlocker] = karazhanHelper.GetCurrentBeamBlockers(); + Unit* greenPortal = bot->FindNearestCreature(NPC_GREEN_PORTAL, 150.0f); + if (!greenPortal) + return false; + + const ObjectGuid botGuid = bot->GetGUID(); + auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot); bool isBlockingNow = (bot == greenBlocker); - bool wasBlocking = wasBlockingGreenBeam[botGuid]; + + auto it = _wasBlockingGreenBeam.find(botGuid); + bool wasBlocking = (it != _wasBlockingGreenBeam.end()) ? it->second : false; if (wasBlocking && !isBlockingNow) { - std::map ph; - ph["%player"] = bot->GetName(); + std::map placeholders{{"%player", bot->GetName()}}; std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( - "netherspite_beam_leaving_green", "%player is leaving the green beam--next blocker up!", ph); + "netherspite_beam_leaving_green", "%player is leaving the green beam--next blocker up!", placeholders); bot->Yell(text, LANG_UNIVERSAL); - wasBlockingGreenBeam[botGuid] = false; - - return false; + _wasBlockingGreenBeam[botGuid] = false; } if (isBlockingNow) { if (!wasBlocking) { - std::map ph; - ph["%player"] = bot->GetName(); + std::map placeholders{{"%player", bot->GetName()}}; std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( - "netherspite_beam_blocking_green", "%player is moving to block the green beam!", ph); + "netherspite_beam_blocking_green", "%player is moving to block the green beam!", placeholders); bot->Yell(text, LANG_UNIVERSAL); } - wasBlockingGreenBeam[botGuid] = true; + _wasBlockingGreenBeam[botGuid] = true; - std::vector voidZones = karazhanHelper.GetAllVoidZones(); - float bx = boss->GetPositionX(); - float by = boss->GetPositionY(); - float bz = boss->GetPositionZ(); + std::vector voidZones = GetAllVoidZones(botAI, bot); + + float bx = netherspite->GetPositionX(); + float by = netherspite->GetPositionY(); + float bz = netherspite->GetPositionZ(); float px = greenPortal->GetPositionX(); float py = greenPortal->GetPositionY(); + float dx = px - bx; float dy = py - by; - float length = sqrt(dx*dx + dy*dy); + float length = netherspite->GetExactDist2d(greenPortal); if (length == 0.0f) - { return false; - } dx /= length; dy /= length; float bestDist = 150.0f; Position bestPos; bool found = false; + for (float dist = 18.0f; dist <= 30.0f; dist += 0.5f) { float candidateX = bx + dx * dist; float candidateY = by + dy * dist; float candidateZ = bz; - bool outsideAllVoidZones = true; - for (Unit* voidZone : voidZones) - { - float voidZoneDist = sqrt(pow(candidateX - voidZone->GetPositionX(), 2) + - pow(candidateY - voidZone->GetPositionY(), 2)); - if (voidZoneDist < 4.0f) - { - outsideAllVoidZones = false; - break; - } - } - if (!outsideAllVoidZones) - { + if (!IsSafePosition(candidateX, candidateY, candidateZ, voidZones, 4.0f)) continue; - } + float distToIdeal = fabs(dist - 18.0f); if (!found || distToIdeal < bestDist) { @@ -775,380 +845,362 @@ bool KarazhanNetherspiteBlockGreenBeamAction::Execute(Event event) found = true; } } + if (found) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); } return false; } - wasBlockingGreenBeam[botGuid] = false; + _wasBlockingGreenBeam[botGuid] = false; return false; } -bool KarazhanNetherspiteBlockGreenBeamAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - Unit* greenPortal = bot->FindNearestCreature(NPC_GREEN_PORTAL, 150.0f); - - return boss && greenPortal && !boss->HasAura(SPELL_NETHERSPITE_BANISHED); -} - // All bots not currently blocking a beam will avoid beams and void zones -bool KarazhanNetherspiteAvoidBeamAndVoidZoneAction::Execute(Event event) +bool NetherspiteAvoidBeamAndVoidZoneAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - RaidKarazhanHelpers karazhanHelper(botAI); - auto [redBlocker, greenBlocker, blueBlocker] = karazhanHelper.GetCurrentBeamBlockers(); - std::vector voidZones = karazhanHelper.GetAllVoidZones(); - bool nearVoidZone = false; - for (Unit* vz : voidZones) - { - if (bot->GetExactDist2d(vz) < 4.0f) - { - nearVoidZone = true; - break; - } - } - struct BeamAvoid { Unit* portal; float minDist, maxDist; }; + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite) + return false; + + auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot); + std::vector voidZones = GetAllVoidZones(botAI, bot); + + bool nearVoidZone = !IsSafePosition(bot->GetPositionX(), bot->GetPositionY(), + bot->GetPositionZ(), voidZones, 4.0f); + std::vector beams; Unit* redPortal = bot->FindNearestCreature(NPC_RED_PORTAL, 150.0f); Unit* bluePortal = bot->FindNearestCreature(NPC_BLUE_PORTAL, 150.0f); Unit* greenPortal = bot->FindNearestCreature(NPC_GREEN_PORTAL, 150.0f); + if (redPortal) { - float bx = boss->GetPositionX(), by = boss->GetPositionY(); - float px = redPortal->GetPositionX(), py = redPortal->GetPositionY(); - float dx = px - bx, dy = py - by; - float length = sqrt(dx*dx + dy*dy); + float length = netherspite->GetExactDist2d(redPortal); beams.push_back({redPortal, 0.0f, length}); } + if (bluePortal) { - float bx = boss->GetPositionX(), by = boss->GetPositionY(); - float px = bluePortal->GetPositionX(), py = bluePortal->GetPositionY(); - float dx = px - bx, dy = py - by; - float length = sqrt(dx*dx + dy*dy); + float length = netherspite->GetExactDist2d(bluePortal); beams.push_back({bluePortal, 0.0f, length}); } + if (greenPortal) { - float bx = boss->GetPositionX(), by = boss->GetPositionY(); - float px = greenPortal->GetPositionX(), py = greenPortal->GetPositionY(); - float dx = px - bx, dy = py - by; - float length = sqrt(dx*dx + dy*dy); + float length = netherspite->GetExactDist2d(greenPortal); beams.push_back({greenPortal, 0.0f, length}); } - bool nearBeam = false; - for (auto const& beam : beams) - { - float bx = boss->GetPositionX(), by = boss->GetPositionY(); - float px = beam.portal->GetPositionX(), py = beam.portal->GetPositionY(); - float dx = px - bx, dy = py - by; - float length = sqrt(dx*dx + dy*dy); - if (length == 0.0f) - { - continue; - } - dx /= length; dy /= length; - float botdx = bot->GetPositionX() - bx, botdy = bot->GetPositionY() - by; - float t = (botdx * dx + botdy * dy); - float beamX = bx + dx * t, beamY = by + dy * t; - float distToBeam = sqrt(pow(bot->GetPositionX() - beamX, 2) + pow(bot->GetPositionY() - beamY, 2)); - if (distToBeam < 5.0f && t > beam.minDist && t < beam.maxDist) - { - nearBeam = true; - break; - } - } - if (!nearVoidZone && !nearBeam) - { - return false; - } - const float minMoveDist = 2.0f, maxSearchDist = 30.0f, stepAngle = M_PI/18.0f, stepDist = 0.5f; - float bossZ = boss->GetPositionZ(); + bool nearBeam = !IsAwayFromBeams(bot->GetPositionX(), bot->GetPositionY(), beams, netherspite); + + if (!nearVoidZone && !nearBeam) + return false; + + const float minMoveDist = 2.0f; + const float minMoveDistSq = minMoveDist * minMoveDist; + const float maxSearchDist = 30.0f, stepAngle = M_PI/18.0f, stepDist = 0.5f; + float netherspiteZ = netherspite->GetPositionZ(); Position bestCandidate; - float bestDist = 0.0f; + float bestDistSq = std::numeric_limits::max(); bool found = false; + + const float botX = bot->GetPositionX(); + const float botY = bot->GetPositionY(); + for (float angle = 0; angle < 2 * M_PI; angle += stepAngle) { for (float dist = 2.0f; dist <= maxSearchDist; dist += stepDist) { - float cx = bot->GetPositionX() + cos(angle) * dist; - float cy = bot->GetPositionY() + sin(angle) * dist; - float cz = bossZ; - if (std::any_of(voidZones.begin(), voidZones.end(), [&](Unit* vz){ return Position(cx, cy, cz).GetExactDist2d(vz) < 4.0f; })) - { + float cx = botX + std::cos(angle) * dist; + float cy = botY + std::sin(angle) * dist; + float cz = netherspiteZ; + + if (!IsSafePosition(cx, cy, cz, voidZones, 4.0f) || + !IsAwayFromBeams(cx, cy, beams, netherspite)) continue; - } - bool tooCloseToBeam = false; - for (auto const& beam : beams) - { - float bx = boss->GetPositionX(), by = boss->GetPositionY(); - float px = beam.portal->GetPositionX(), py = beam.portal->GetPositionY(); - float dx = px - bx, dy = py - by; - float length = sqrt(dx*dx + dy*dy); - if (length == 0.0f) - { - continue; - } - dx /= length; dy /= length; - float botdx = cx - bx, botdy = cy - by; - float t = (botdx * dx + botdy * dy); - float beamX = bx + dx * t, beamY = by + dy * t; - float distToBeam = sqrt(pow(cx - beamX, 2) + pow(cy - beamY, 2)); - if (distToBeam < 5.0f && t > beam.minDist && t < beam.maxDist) - { - tooCloseToBeam = true; - break; - } - } - if (tooCloseToBeam) - { + + float dx = cx - botX; + float dy = cy - botY; + float moveDistSq = dx*dx + dy*dy; + if (moveDistSq < minMoveDistSq) continue; - } - float moveDist = sqrt(pow(cx - bot->GetPositionX(), 2) + pow(cy - bot->GetPositionY(), 2)); - if (moveDist < minMoveDist) - { - continue; - } - if (!found || moveDist < bestDist) + + if (!found || moveDistSq < bestDistSq) { bestCandidate = Position(cx, cy, cz); - bestDist = moveDist; + bestDistSq = moveDistSq; found = true; } } } - if (found && karazhanHelper.IsSafePosition(bestCandidate.GetPositionX(), - bestCandidate.GetPositionY(), bestCandidate.GetPositionZ(), - voidZones, 4.0f)) + + if (found) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), bestCandidate.GetPositionX(), bestCandidate.GetPositionY(), - bestCandidate.GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, bestCandidate.GetPositionX(), bestCandidate.GetPositionY(), + bestCandidate.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); } return false; } -bool KarazhanNetherspiteAvoidBeamAndVoidZoneAction::isUseful() +bool NetherspiteAvoidBeamAndVoidZoneAction::IsAwayFromBeams( + float x, float y, const std::vector& beams, Unit* netherspite) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - if (!boss || boss->HasAura(SPELL_NETHERSPITE_BANISHED)) + for (auto const& beam : beams) { - return false; - } + float bx = netherspite->GetPositionX(), by = netherspite->GetPositionY(); + float px = beam.portal->GetPositionX(), py = beam.portal->GetPositionY(); + float dx = px - bx, dy = py - by; + float length = netherspite->GetExactDist2d(beam.portal); - RaidKarazhanHelpers karazhanHelper(botAI); - auto [redBlocker, greenBlocker, blueBlocker] = karazhanHelper.GetCurrentBeamBlockers(); - if (bot == redBlocker || bot == blueBlocker || bot == greenBlocker) - { - return false; + if (length == 0.0f) + continue; + + dx /= length; dy /= length; + float botdx = x - bx, botdy = y - by; + float distanceAlongBeam = (botdx * dx + botdy * dy); + float beamX = bx + dx * distanceAlongBeam, beamY = by + dy * distanceAlongBeam; + float distToBeam = sqrt((x - beamX) * (x - beamX) + (y - beamY) * (y - beamY)); + + if (distToBeam < 5.0f && distanceAlongBeam > beam.minDist && distanceAlongBeam < beam.maxDist) + return false; } return true; } -bool KarazhanNetherspiteBanishPhaseAvoidVoidZoneAction::Execute(Event event) +bool NetherspiteBanishPhaseAvoidVoidZoneAction::Execute(Event event) { - RaidKarazhanHelpers karazhanHelper(botAI); - std::vector voidZones = karazhanHelper.GetAllVoidZones(); + std::vector voidZones = GetAllVoidZones(botAI, bot); for (Unit* vz : voidZones) { if (vz->GetEntry() == NPC_VOID_ZONE && bot->GetExactDist2d(vz) < 4.0f) - { return FleePosition(vz->GetPosition(), 4.0f); - } } return false; } -bool KarazhanNetherspiteBanishPhaseAvoidVoidZoneAction::isUseful() +bool NetherspiteManageTimersAndTrackersAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - if (!boss || !boss->HasAura(SPELL_NETHERSPITE_BANISHED)) - { + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite) return false; - } - RaidKarazhanHelpers karazhanHelper(botAI); - std::vector voidZones = karazhanHelper.GetAllVoidZones(); - for (Unit* vz : voidZones) + const ObjectGuid botGuid = bot->GetGUID(); + const time_t now = std::time(nullptr); + + // DpsWaitTimer is for pausing DPS during phase transitions + // redBeamMoveTimer and lastBeamMoveSideways are for tank dancing in/out of the red beam + if (netherspite->GetHealth() == netherspite->GetMaxHealth() && + !netherspite->HasAura(SPELL_GREEN_BEAM_HEAL)) { - if (bot->GetExactDist2d(vz) < 4.0f) + if (IsMapIDTimerManager(botAI, bot)) + netherspiteDpsWaitTimer.insert_or_assign(KARAZHAN_MAP_ID, now); + + if (botAI->IsTank(bot) && !bot->HasAura(SPELL_RED_BEAM_DEBUFF)) { - return true; + redBeamMoveTimer.erase(botGuid); + lastBeamMoveSideways.erase(botGuid); + } + } + else if (netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + { + if (IsMapIDTimerManager(botAI, bot)) + netherspiteDpsWaitTimer.erase(KARAZHAN_MAP_ID); + + if (botAI->IsTank(bot)) + { + redBeamMoveTimer.erase(botGuid); + lastBeamMoveSideways.erase(botGuid); + } + } + else if (!netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + { + if (IsMapIDTimerManager(botAI, bot)) + netherspiteDpsWaitTimer.try_emplace(KARAZHAN_MAP_ID, now); + + if (botAI->IsTank(bot) && bot->HasAura(SPELL_RED_BEAM_DEBUFF)) + { + redBeamMoveTimer.try_emplace(botGuid, now); + lastBeamMoveSideways.try_emplace(botGuid, false); } } return false; } -bool KarazhanPrinceMalchezaarNonTankAvoidHazardAction::Execute(Event event) +// Move away from the boss to avoid Shadow Nova when Enfeebled +// Do not cross within Infernal Hellfire radius while doing so +bool PrinceMalchezaarEnfeebledAvoidHazardAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "prince malchezaar"); - RaidKarazhanHelpers karazhanHelper(botAI); - std::vector infernals = karazhanHelper.GetSpawnedInfernals(); + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + if (!malchezaar) + return false; + + std::vector infernals = GetSpawnedInfernals(botAI); const float minSafeBossDistance = 34.0f; + const float minSafeBossDistanceSq = minSafeBossDistance * minSafeBossDistance; const float maxSafeBossDistance = 60.0f; const float safeInfernalDistance = 23.0f; - const float stepSize = 0.5f; - const int numAngles = 64; + const float safeInfernalDistanceSq = safeInfernalDistance * safeInfernalDistance; + const float distIncrement = 0.5f; + const uint8 numAngles = 64; + float bx = bot->GetPositionX(); float by = bot->GetPositionY(); float bz = bot->GetPositionZ(); - float bossX = boss->GetPositionX(); - float bossY = boss->GetPositionY(); - float bossZ = boss->GetPositionZ(); - float bestMoveDist = std::numeric_limits::max(); + float malchezaarX = malchezaar->GetPositionX(); + float malchezaarY = malchezaar->GetPositionY(); + float malchezaarZ = malchezaar->GetPositionZ(); + float bestMoveDistSq = std::numeric_limits::max(); float bestDestX = 0.0f, bestDestY = 0.0f, bestDestZ = bz; bool found = false; - if (bot->HasAura(SPELL_ENFEEBLE)) + for (int i = 0; i < numAngles; ++i) { - for (int i = 0; i < numAngles; ++i) + float angle = (2 * M_PI * i) / numAngles; + float dx = std::cos(angle); + float dy = std::sin(angle); + + for (float dist = minSafeBossDistance; dist <= maxSafeBossDistance; dist += distIncrement) { - float angle = (2 * M_PI * i) / numAngles; - float dx = cos(angle); - float dy = sin(angle); - for (float dist = minSafeBossDistance; dist <= maxSafeBossDistance; dist += stepSize) + float x = malchezaarX + dx * dist; + float y = malchezaarY + dy * dist; + float destZ = malchezaarZ; + float destX = x, destY = y; + if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bx, by, bz, destX, destY, destZ, true)) + continue; + + float ddx = destX - malchezaarX; + float ddy = destY - malchezaarY; + float distFromBossSq = ddx*ddx + ddy*ddy; + if (distFromBossSq < minSafeBossDistanceSq) + continue; + + bool pathSafe = IsStraightPathSafe(Position(bx, by, bz), Position(destX, destY, destZ), + infernals, safeInfernalDistance, distIncrement); + float mdx = destX - bx; + float mdy = destY - by; + float moveDistSq = mdx*mdx + mdy*mdy; + + if (pathSafe && moveDistSq < bestMoveDistSq) { - float x = bossX + dx * dist; - float y = bossY + dy * dist; - float destZ = bossZ; - float destX = x, destY = y, destZ2 = destZ; - if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bx, by, bz, destX, destY, destZ2, true)) - { - continue; - } - float distFromBoss = sqrt(pow(destX - bossX, 2) + pow(destY - bossY, 2)); - if (distFromBoss < minSafeBossDistance) - { - continue; - } - bool pathSafe = karazhanHelper.IsStraightPathSafe(Position(bx, by, bz), Position(destX, destY, destZ2), - infernals, safeInfernalDistance, stepSize); - float moveDist = sqrt(pow(destX - bx, 2) + pow(destY - by, 2)); - if (pathSafe && moveDist < bestMoveDist) - { - bestMoveDist = moveDist; - bestDestX = destX; - bestDestY = destY; - bestDestZ = destZ2; - found = true; - } + bestMoveDistSq = moveDistSq; + bestDestX = destX; + bestDestY = destY; + bestDestZ = destZ; + found = true; } } + } + + if (found) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, bestDestX, bestDestY, bestDestZ, false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +// Move away from infernals while staying within range of the boss +// Prioritize finding a safe path to the new location, but will fallback to just finding a safe location if needed +bool PrinceMalchezaarNonTankAvoidInfernalAction::Execute(Event event) +{ + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + if (!malchezaar) + return false; + + std::vector infernals = GetSpawnedInfernals(botAI); + + const float safeInfernalDistance = 23.0f; + const float safeInfernalDistanceSq = safeInfernalDistance * safeInfernalDistance; + const float maxSafeBossDistance = 35.0f; + + float bx = bot->GetPositionX(); + float by = bot->GetPositionY(); + float bz = bot->GetPositionZ(); + float malchezaarX = malchezaar->GetPositionX(); + float malchezaarY = malchezaar->GetPositionY(); + float malchezaarZ = malchezaar->GetPositionZ(); + + bool nearInfernal = false; + for (Unit* infernal : infernals) + { + float dx = bx - infernal->GetPositionX(); + float dy = by - infernal->GetPositionY(); + float infernalDistSq = dx*dx + dy*dy; + if (infernalDistSq < safeInfernalDistanceSq) + { + nearInfernal = true; + break; + } + } + + float bestDestX = bx, bestDestY = by, bestDestZ = bz; + bool found = false; + + if (nearInfernal) + { + const float distIncrement = 0.5f; + const uint8 numAngles = 64; + + // 1. Try to find a safe position with a safe path + found = TryFindSafePositionWithSafePath(bot, bx, by, bz, malchezaarX, malchezaarY, malchezaarZ, + infernals, safeInfernalDistance, distIncrement, numAngles, maxSafeBossDistance, + true, bestDestX, bestDestY, bestDestZ); + + // 2. Fallback: try to find a safe position (ignore path safety) + if (!found) + { + found = TryFindSafePositionWithSafePath(bot, bx, by, bz, malchezaarX, malchezaarY, malchezaarZ, + infernals, safeInfernalDistance, distIncrement, numAngles, maxSafeBossDistance, + false, bestDestX, bestDestY, bestDestZ); + } + if (found) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), bestDestX, bestDestY, bestDestZ, false, false, false, true, - MovementPriority::MOVEMENT_FORCED); - } - - return false; - } - - if (!bot->HasAura(SPELL_ENFEEBLE)) - { - bool nearInfernal = false; - for (Unit* infernal : infernals) - { - float infernalDist = sqrt(pow(bx - infernal->GetPositionX(), 2) + pow(by - infernal->GetPositionY(), 2)); - if (infernalDist < safeInfernalDistance) - { - nearInfernal = true; - break; - } - } - if (nearInfernal) - { - float bestMoveDist = std::numeric_limits::max(); - float bestDestX = bx, bestDestY = by, bestDestZ = bz; - bool found = false; - for (int i = 0; i < numAngles; ++i) - { - float angle = (2 * M_PI * i) / numAngles; - float dx = cos(angle); - float dy = sin(angle); - for (float dist = stepSize; dist <= maxSafeBossDistance; dist += stepSize) - { - float x = bossX + dx * dist; - float y = bossY + dy * dist; - float destZ = bossZ; - float destX = x, destY = y, destZ2 = destZ; - if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bossX, bossY, bossZ, destX, destY, destZ2, true)) - { - continue; - } - bool destSafe = true; - for (Unit* infernal : infernals) - { - float infernalDist = sqrt(pow(destX - infernal->GetPositionX(), 2) + pow(destY - infernal->GetPositionY(), 2)); - if (infernalDist < safeInfernalDistance) - { - destSafe = false; - break; - } - } - if (!destSafe) - continue; - float moveDist = sqrt(pow(destX - bx, 2) + pow(destY - by, 2)); - if (moveDist < bestMoveDist) - { - bestMoveDist = moveDist; - bestDestX = destX; - bestDestY = destY; - bestDestZ = destZ2; - found = true; - } - } - } - if (found) - { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), bestDestX, bestDestY, bestDestZ, false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); - } + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, bestDestX, bestDestY, bestDestZ, false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); } } return false; } -bool KarazhanPrinceMalchezaarNonTankAvoidHazardAction::isUseful() +// This is similar to the non-tank avoid infernal action, but the movement is based on the bot's location +// And the safe distance from infernals is larger to give melee more room to maneuver +bool PrinceMalchezaarMainTankMovementAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + if (!malchezaar) + return false; - return boss && !(botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot); -} + if (bot->GetVictim() != malchezaar) + return Attack(malchezaar); -bool KarazhanPrinceMalchezaarTankAvoidHazardAction::Execute(Event event) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "prince malchezaar"); - RaidKarazhanHelpers karazhanHelper(botAI); - std::vector infernals = karazhanHelper.GetSpawnedInfernals(); + std::vector infernals = GetSpawnedInfernals(botAI); + + const float safeInfernalDistance = 30.0f; + const float safeInfernalDistanceSq = safeInfernalDistance * safeInfernalDistance; + const float maxSampleDist = 75.0f; - const float safeInfernalDistance = 28.0f; - const float stepSize = 0.5f; - const int numAngles = 64; - const float maxSampleDist = 60.0f; float bx = bot->GetPositionX(); float by = bot->GetPositionY(); float bz = bot->GetPositionZ(); @@ -1156,116 +1208,283 @@ bool KarazhanPrinceMalchezaarTankAvoidHazardAction::Execute(Event event) bool nearInfernal = false; for (Unit* infernal : infernals) { - float infernalDist = sqrt(pow(bx - infernal->GetPositionX(), 2) + pow(by - infernal->GetPositionY(), 2)); - if (infernalDist < safeInfernalDistance) + float dx = bx - infernal->GetPositionX(); + float dy = by - infernal->GetPositionY(); + float infernalDistSq = dx*dx + dy*dy; + if (infernalDistSq < safeInfernalDistanceSq) { nearInfernal = true; break; } } - float bestMoveDist = std::numeric_limits::max(); float bestDestX = bx, bestDestY = by, bestDestZ = bz; bool found = false; if (nearInfernal) { - for (int i = 0; i < numAngles; ++i) - { - float angle = (2 * M_PI * i) / numAngles; - float dx = cos(angle); - float dy = sin(angle); - for (float dist = stepSize; dist <= maxSampleDist; dist += stepSize) - { - float x = bx + dx * dist; - float y = by + dy * dist; - float z = bz; + const float distIncrement = 0.5f; + const uint8 numAngles = 64; - float destX = x, destY = y, destZ = z; - if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bx, by, bz, destX, destY, destZ, true)) - continue; + // 1. Try to find a safe position with a safe path + found = TryFindSafePositionWithSafePath( bot, bx, by, bz, bx, by, bz, + infernals, safeInfernalDistance, distIncrement, numAngles, maxSampleDist, + true, bestDestX, bestDestY, bestDestZ); - bool destSafe = true; - for (Unit* infernal : infernals) - { - float infernalDist = sqrt(pow(destX - infernal->GetPositionX(), 2) + pow(destY - infernal->GetPositionY(), 2)); - if (infernalDist < safeInfernalDistance) - { - destSafe = false; - break; - } - } - if (!destSafe) - continue; - - bool pathSafe = karazhanHelper.IsStraightPathSafe(Position(bx, by, bz), Position(destX, destY, destZ), - infernals, safeInfernalDistance, stepSize); - float moveDist = sqrt(pow(destX - bx, 2) + pow(destY - by, 2)); - if (pathSafe && moveDist < bestMoveDist) - { - bestMoveDist = moveDist; - bestDestX = destX; - bestDestY = destY; - bestDestZ = destZ; - found = true; - } - } - } + // 2. Fallback: try to find a safe position (ignore path safety) if (!found) { - for (int i = 0; i < numAngles; ++i) - { - float angle = (2 * M_PI * i) / numAngles; - float dx = cos(angle); - float dy = sin(angle); - for (float dist = stepSize; dist <= maxSampleDist; dist += stepSize) - { - float x = bx + dx * dist; - float y = by + dy * dist; - float z = bz; - - float destX = x, destY = y, destZ = z; - if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bx, by, bz, destX, destY, destZ, true)) - continue; - - bool destSafe = true; - for (Unit* infernal : infernals) - { - float infernalDist = sqrt(pow(destX - infernal->GetPositionX(), 2) + pow(destY - infernal->GetPositionY(), 2)); - if (infernalDist < safeInfernalDistance) - { - destSafe = false; - break; - } - } - float moveDist = sqrt(pow(destX - bx, 2) + pow(destY - by, 2)); - if (destSafe && moveDist < bestMoveDist) - { - bestMoveDist = moveDist; - bestDestX = destX; - bestDestY = destY; - bestDestZ = destZ; - found = true; - } - } - } + found = TryFindSafePositionWithSafePath( bot, bx, by, bz, bx, by, bz, + infernals, safeInfernalDistance, distIncrement, numAngles, maxSampleDist, + false, bestDestX, bestDestY, bestDestZ); } + if (found) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), bestDestX, bestDestY, bestDestZ, false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); + return MoveTo(KARAZHAN_MAP_ID, bestDestX, bestDestY, bestDestZ, false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); } } return false; } -bool KarazhanPrinceMalchezaarTankAvoidHazardAction::isUseful() +// The tank position is near the Southeastern area of the Master's Terrace +// The tank moves Nightbane into position in two steps to try to get Nightbane to face sideways to the raid +bool NightbaneGroundPhasePositionBossAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + return false; - return boss && botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot; + MarkTargetWithSkull(bot, nightbane); + + if (bot->GetVictim() != nightbane) + return Attack(nightbane); + + const ObjectGuid botGuid = bot->GetGUID(); + uint8 step = nightbaneTankStep.count(botGuid) ? nightbaneTankStep[botGuid] : 0; + + if (nightbane->GetVictim() == bot) + { + const Position tankPositions[2] = + { + NIGHTBANE_TRANSITION_BOSS_POSITION, + NIGHTBANE_FINAL_BOSS_POSITION + }; + const Position& position = tankPositions[step]; + const float maxDistance = 0.5f; + float distanceToTarget = bot->GetExactDist2d(position); + + if ((distanceToTarget > maxDistance) && bot->IsWithinMeleeRange(nightbane)) + return MoveTo(KARAZHAN_MAP_ID, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, true); + + if (step == 0 && distanceToTarget <= maxDistance) + nightbaneTankStep[botGuid] = 1; + + if (step == 1 && distanceToTarget <= maxDistance) + { + float orientation = atan2(nightbane->GetPositionY() - bot->GetPositionY(), + nightbane->GetPositionX() - bot->GetPositionX()); + bot->SetFacingTo(orientation); + } + } + + return false; +} + +// Ranged bots rotate between 3 positions to avoid standing in Charred Earth, which lasts for +// 30s and has a minimum cooldown of 18s (so there can be 2 active at once) +// Ranged positions are near the Northeastern door to the tower +bool NightbaneGroundPhaseRotateRangedPositionsAction::Execute(Event event) +{ + const ObjectGuid botGuid = bot->GetGUID(); + uint8 index = nightbaneRangedStep.count(botGuid) ? nightbaneRangedStep[botGuid] : 0; + + const Position rangedPositions[3] = + { + NIGHTBANE_RANGED_POSITION1, + NIGHTBANE_RANGED_POSITION2, + NIGHTBANE_RANGED_POSITION3 + }; + const Position& position = rangedPositions[index]; + const float maxDistance = 2.0f; + float distanceToTarget = bot->GetExactDist2d(position); + + if (distanceToTarget <= maxDistance && + bot->HasAura(SPELL_CHARRED_EARTH) && !bot->HasAura(SPELL_BELLOWING_ROAR)) + { + index = (index + 1) % 3; + nightbaneRangedStep[botGuid] = index; + const Position& newPosition = rangedPositions[index]; + float newDistanceToTarget = bot->GetExactDist2d(newPosition); + if (newDistanceToTarget > maxDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, newPosition.GetPositionX(), newPosition.GetPositionY(), newPosition.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + return false; + } + + if (distanceToTarget > maxDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +// For countering Bellowing Roars during the ground phase +bool NightbaneCastFearWardOnMainTankAction::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; +} + +// Put pets on passive during the flight phase so they don't try to chase Nightbane off the map +bool NightbaneControlPetAggressionAction::Execute(Event event) +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + return false; + + Pet* pet = bot->GetPet(); + if (!pet) + return false; + + if (nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z && pet->GetReactState() == REACT_PASSIVE) + pet->SetReactState(REACT_DEFENSIVE); + + if (nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z && pet->GetReactState() != REACT_PASSIVE) + { + pet->AttackStop(); + pet->SetReactState(REACT_PASSIVE); + } + + return false; +} + +// 1. Stack at the "Flight Stack Position" near Nightbane so he doesn't use Fireball Barrage +// 2. Once Rain of Bones hits, the whole party moves to a new stack position +// This action lasts for the first 35 seconds of the flight phase, after which Nightbane gets +// ready to land, and the player will need to lead the bots over near the ground phase position +bool NightbaneFlightPhaseMovementAction::Execute(Event event) +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane || nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z) + return false; + + MarkTargetWithMoon(bot, nightbane); + + Unit* botTarget = botAI->GetUnit(bot->GetTarget()); + if (botTarget && botTarget == nightbane) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + } + + const ObjectGuid botGuid = bot->GetGUID(); + bool hasRainOfBones = bot->HasAura(SPELL_RAIN_OF_BONES); + + if (hasRainOfBones) + nightbaneRainOfBonesHit[botGuid] = true; + + float destX, destY, destZ; + if (nightbaneRainOfBonesHit[botGuid]) + { + destX = NIGHTBANE_RAIN_OF_BONES_POSITION.GetPositionX(); + destY = NIGHTBANE_RAIN_OF_BONES_POSITION.GetPositionY(); + destZ = NIGHTBANE_RAIN_OF_BONES_POSITION.GetPositionZ(); + } + else + { + destX = NIGHTBANE_FLIGHT_STACK_POSITION.GetPositionX(); + destY = NIGHTBANE_FLIGHT_STACK_POSITION.GetPositionY(); + destZ = NIGHTBANE_FLIGHT_STACK_POSITION.GetPositionZ(); + } + + if (bot->GetExactDist2d(destX, destY) > 2.0f) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, destX, destY, destZ, false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool NightbaneManageTimersAndTrackersAction::Execute(Event event) +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + return false; + + const ObjectGuid botGuid = bot->GetGUID(); + const time_t now = std::time(nullptr); + + // Erase DPS wait timer and tank and ranged position tracking on encounter reset + if (nightbane->GetHealth() == nightbane->GetMaxHealth()) + { + if (botAI->IsMainTank(bot)) + nightbaneTankStep.erase(botGuid); + + if (botAI->IsRanged(bot)) + nightbaneRangedStep.erase(botGuid); + + if (IsMapIDTimerManager(botAI, bot)) + nightbaneDpsWaitTimer.erase(KARAZHAN_MAP_ID); + } + // Erase flight phase timer and Rain of Bones tracker on ground phase and start DPS wait timer + else if (nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z) + { + nightbaneRainOfBonesHit.erase(botGuid); + + if (IsMapIDTimerManager(botAI, bot)) + { + nightbaneFlightPhaseStartTimer.erase(KARAZHAN_MAP_ID); + nightbaneDpsWaitTimer.try_emplace(KARAZHAN_MAP_ID, now); + } + } + // Erase DPS wait timer and tank and ranged position tracking and start flight phase timer + // at beginning of flight phase + else if (nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z) + { + if (botAI->IsMainTank(bot)) + nightbaneTankStep.erase(botGuid); + + if (botAI->IsRanged(bot)) + nightbaneRangedStep.erase(botGuid); + + if (IsMapIDTimerManager(botAI, bot)) + { + nightbaneDpsWaitTimer.erase(KARAZHAN_MAP_ID); + nightbaneFlightPhaseStartTimer.try_emplace(KARAZHAN_MAP_ID, now); + } + } + + return false; } diff --git a/src/strategy/raids/karazhan/RaidKarazhanActions.h b/src/strategy/raids/karazhan/RaidKarazhanActions.h index 4ab24ed9..2d50944c 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanActions.h +++ b/src/strategy/raids/karazhan/RaidKarazhanActions.h @@ -2,217 +2,322 @@ #define _PLAYERBOT_RAIDKARAZHANACTIONS_H #include "Action.h" +#include "AttackAction.h" #include "MovementActions.h" -class KarazhanAttumenTheHuntsmanStackBehindAction : public MovementAction +class ManaWarpStunCreatureBeforeWarpBreachAction : public AttackAction { public: - KarazhanAttumenTheHuntsmanStackBehindAction(PlayerbotAI* botAI, std::string const name = "karazhan attumen the huntsman stack behind") : MovementAction(botAI, name) {} - - bool Execute(Event event) override; - bool isUseful() override; -}; - -class KarazhanMoroesMarkTargetAction : public Action -{ -public: - KarazhanMoroesMarkTargetAction(PlayerbotAI* botAI, std::string const name = "karazhan moroes mark target") : Action(botAI, name) {} + ManaWarpStunCreatureBeforeWarpBreachAction( + PlayerbotAI* botAI, std::string const name = "mana warp stun creature before warp breach") : AttackAction(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanMaidenOfVirtuePositionBossAction : public MovementAction +class AttumenTheHuntsmanMarkTargetAction : public AttackAction { public: - KarazhanMaidenOfVirtuePositionBossAction(PlayerbotAI* botAI, std::string const name = "karazhan maiden of virtue position boss") : MovementAction(botAI, name) {} - + AttumenTheHuntsmanMarkTargetAction( + PlayerbotAI* botAI, std::string const name = "attumen the huntsman mark target") : AttackAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanMaidenOfVirtuePositionRangedAction : public MovementAction +class AttumenTheHuntsmanSplitBossesAction : public AttackAction { public: - KarazhanMaidenOfVirtuePositionRangedAction(PlayerbotAI* botAI, std::string const name = "karazhan maiden of virtue position ranged") : MovementAction(botAI, name) {} - + AttumenTheHuntsmanSplitBossesAction( + PlayerbotAI* botAI, std::string const name = "attumen the huntsman split bosses") : AttackAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanBigBadWolfPositionBossAction : public MovementAction +class AttumenTheHuntsmanStackBehindAction : public MovementAction { public: - KarazhanBigBadWolfPositionBossAction(PlayerbotAI* botAI, std::string const name = "karazhan big bad wolf position boss") : MovementAction(botAI, name) {} - + AttumenTheHuntsmanStackBehindAction( + PlayerbotAI* botAI, std::string const name = "attumen the huntsman stack behind") : MovementAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanBigBadWolfRunAwayAction : public MovementAction +class AttumenTheHuntsmanManageDpsTimerAction : public Action { public: - KarazhanBigBadWolfRunAwayAction(PlayerbotAI* botAI, std::string const name = "karazhan big bad wolf run away") : MovementAction(botAI, name) {} - + AttumenTheHuntsmanManageDpsTimerAction( + PlayerbotAI* botAI, std::string const name = "attumen the huntsman manage dps timer") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class MoroesMainTankAttackBossAction : public AttackAction +{ +public: + MoroesMainTankAttackBossAction( + PlayerbotAI* botAI, std::string const name = "moroes main tank attack boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class MoroesMarkTargetAction : public Action +{ +public: + MoroesMarkTargetAction( + PlayerbotAI* botAI, std::string const name = "moroes mark target") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class MaidenOfVirtueMoveBossToHealerAction : public AttackAction +{ +public: + MaidenOfVirtueMoveBossToHealerAction( + PlayerbotAI* botAI, std::string const name = "maiden of virtue move boss to healer") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class MaidenOfVirtuePositionRangedAction : public MovementAction +{ +public: + MaidenOfVirtuePositionRangedAction( + PlayerbotAI* botAI, std::string const name = "maiden of virtue position ranged") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class BigBadWolfPositionBossAction : public AttackAction +{ +public: + BigBadWolfPositionBossAction( + PlayerbotAI* botAI, std::string const name = "big bad wolf position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class BigBadWolfRunAwayFromBossAction : public MovementAction +{ +public: + BigBadWolfRunAwayFromBossAction( + PlayerbotAI* botAI, std::string const name = "big bad wolf run away from boss") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class RomuloAndJulianneMarkTargetAction : public Action +{ +public: + RomuloAndJulianneMarkTargetAction( + PlayerbotAI* botAI, std::string const name = "romulo and julianne mark target") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class WizardOfOzMarkTargetAction : public Action +{ +public: + WizardOfOzMarkTargetAction( + PlayerbotAI* botAI, std::string const name = "wizard of oz mark target") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class WizardOfOzScorchStrawmanAction : public Action +{ +public: + WizardOfOzScorchStrawmanAction( + PlayerbotAI* botAI, std::string const name = "wizard of oz scorch strawman") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class TheCuratorMarkAstralFlareAction : public Action +{ +public: + TheCuratorMarkAstralFlareAction( + PlayerbotAI* botAI, std::string const name = "the curator mark astral flare") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class TheCuratorPositionBossAction : public AttackAction +{ +public: + TheCuratorPositionBossAction( + PlayerbotAI* botAI, std::string const name = "the curator position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class TheCuratorSpreadRangedAction : public MovementAction +{ +public: + TheCuratorSpreadRangedAction( + PlayerbotAI* botAI, std::string const name = "the curator spread ranged") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class TerestianIllhoofMarkTargetAction : public Action +{ +public: + TerestianIllhoofMarkTargetAction( + PlayerbotAI* botAI, std::string const name = "terestian illhoof mark target") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class ShadeOfAranRunAwayFromArcaneExplosionAction : public MovementAction +{ +public: + ShadeOfAranRunAwayFromArcaneExplosionAction( + PlayerbotAI* botAI, std::string const name = "shade of aran run away from arcane explosion") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class ShadeOfAranStopMovingDuringFlameWreathAction : public MovementAction +{ +public: + ShadeOfAranStopMovingDuringFlameWreathAction( + PlayerbotAI* botAI, std::string const name = "shade of aran stop moving during flame wreath") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class ShadeOfAranMarkConjuredElementalAction : public Action +{ +public: + ShadeOfAranMarkConjuredElementalAction( + PlayerbotAI* botAI, std::string const name = "shade of aran mark conjured elemental") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class ShadeOfAranRangedMaintainDistanceAction : public MovementAction +{ +public: + ShadeOfAranRangedMaintainDistanceAction( + PlayerbotAI* botAI, std::string const name = "shade of aran ranged maintain distance") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class NetherspiteBlockRedBeamAction : public MovementAction +{ +public: + NetherspiteBlockRedBeamAction( + PlayerbotAI* botAI, std::string const name = "netherspite block red beam") : MovementAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; private: - size_t currentIndex = 0; + Position GetPositionOnBeam(Unit* netherspite, Unit* portal, float distanceFromBoss); + std::unordered_map _wasBlockingRedBeam; }; -class KarazhanRomuloAndJulianneMarkTargetAction : public Action +class NetherspiteBlockBlueBeamAction : public MovementAction { public: - KarazhanRomuloAndJulianneMarkTargetAction(PlayerbotAI* botAI, std::string const name = "karazhan romulo and julianne mark target") : Action(botAI, name) {} + NetherspiteBlockBlueBeamAction( + PlayerbotAI* botAI, std::string const name = "netherspite block blue beam") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +private: + std::unordered_map _wasBlockingBlueBeam; +}; + +class NetherspiteBlockGreenBeamAction : public MovementAction +{ +public: + NetherspiteBlockGreenBeamAction( + PlayerbotAI* botAI, std::string const name = "netherspite block green beam") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + std::unordered_map _wasBlockingGreenBeam; +}; + +class NetherspiteAvoidBeamAndVoidZoneAction : public MovementAction +{ +public: + NetherspiteAvoidBeamAndVoidZoneAction( + PlayerbotAI* botAI, std::string const name = "netherspite avoid beam and void zone") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + struct BeamAvoid + { + Unit* portal; + float minDist, maxDist; + }; + bool IsAwayFromBeams(float x, float y, const std::vector& beams, Unit* netherspite); +}; + +class NetherspiteBanishPhaseAvoidVoidZoneAction : public MovementAction +{ +public: + NetherspiteBanishPhaseAvoidVoidZoneAction( + PlayerbotAI* botAI, std::string const name = "netherspite banish phase avoid void zone") : MovementAction(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanWizardOfOzMarkTargetAction : public Action +class NetherspiteManageTimersAndTrackersAction : public Action { public: - KarazhanWizardOfOzMarkTargetAction(PlayerbotAI* botAI, std::string const name = "karazhan wizard of oz mark target") : Action(botAI, name) {} - + NetherspiteManageTimersAndTrackersAction( + PlayerbotAI* botAI, std::string const name = "netherspite manage timers and trackers") : Action(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanWizardOfOzScorchStrawmanAction : public Action +class PrinceMalchezaarEnfeebledAvoidHazardAction : public MovementAction { public: - KarazhanWizardOfOzScorchStrawmanAction(PlayerbotAI* botAI, std::string const name = "karazhan wizard of oz scorch strawman") : Action(botAI, name) {} - + PrinceMalchezaarEnfeebledAvoidHazardAction( + PlayerbotAI* botAI, std::string const name = "prince malchezaar enfeebled avoid hazard") : MovementAction(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanTheCuratorMarkTargetAction : public Action +class PrinceMalchezaarNonTankAvoidInfernalAction : public MovementAction { public: - KarazhanTheCuratorMarkTargetAction(PlayerbotAI* botAI, std::string const name = "karazhan the curator mark target") : Action(botAI, name) {} - + PrinceMalchezaarNonTankAvoidInfernalAction( + PlayerbotAI* botAI, std::string const name = "prince malchezaar non tank avoid infernal") : MovementAction(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanTheCuratorPositionBossAction : public MovementAction +class PrinceMalchezaarMainTankMovementAction : public AttackAction { public: - KarazhanTheCuratorPositionBossAction(PlayerbotAI* botAI, std::string const name = "karazhan the curator position boss") : MovementAction(botAI, name) {} - - bool Execute(Event event) override; - bool isUseful() override; -}; - -class KarazhanTheCuratorSpreadRangedAction : public MovementAction -{ -public: - KarazhanTheCuratorSpreadRangedAction(PlayerbotAI* botAI, std::string const name = "karazhan the curator spread ranged") : MovementAction(botAI, name) {} - - bool Execute(Event event) override; - bool isUseful() override; -}; - -class KarazhanTerestianIllhoofMarkTargetAction : public Action -{ -public: - KarazhanTerestianIllhoofMarkTargetAction(PlayerbotAI* botAI, std::string const name = "karazhan terestian illhoof mark target") : Action(botAI, name) {} - + PrinceMalchezaarMainTankMovementAction( + PlayerbotAI* botAI, std::string const name = "prince malchezaar main tank movement") : AttackAction(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanShadeOfAranArcaneExplosionRunAwayAction : public MovementAction +class NightbaneGroundPhasePositionBossAction : public AttackAction { public: - KarazhanShadeOfAranArcaneExplosionRunAwayAction(PlayerbotAI* botAI, std::string const name = "karazhan shade of aran arcane explosion run away") : MovementAction(botAI, name) {} - - bool Execute(Event event) override; - bool isUseful() override; -}; - -class KarazhanShadeOfAranFlameWreathStopMovementAction : public MovementAction -{ -public: - KarazhanShadeOfAranFlameWreathStopMovementAction(PlayerbotAI* botAI, std::string const name = "karazhan shade of aran flame wreath stop bot") : MovementAction(botAI, name) {} - + NightbaneGroundPhasePositionBossAction( + PlayerbotAI* botAI, std::string const name = "nightbane ground phase position boss") : AttackAction(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanShadeOfAranMarkConjuredElementalAction : public Action +class NightbaneGroundPhaseRotateRangedPositionsAction : public MovementAction { public: - KarazhanShadeOfAranMarkConjuredElementalAction(PlayerbotAI* botAI, std::string const name = "karazhan shade of aran mark conjured elemental") : Action(botAI, name) {} - + NightbaneGroundPhaseRotateRangedPositionsAction( + PlayerbotAI* botAI, std::string const name = "nightbane ground phase rotate ranged positions") : MovementAction(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanShadeOfAranSpreadRangedAction : public MovementAction +class NightbaneCastFearWardOnMainTankAction : public Action { public: - KarazhanShadeOfAranSpreadRangedAction(PlayerbotAI* botAI, std::string const name = "karazhan shade of aran spread ranged") : MovementAction(botAI, name) {} - + NightbaneCastFearWardOnMainTankAction( + PlayerbotAI* botAI, std::string const name = "nightbane cast fear ward on main tank") : Action(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanNetherspiteBlockRedBeamAction : public MovementAction +class NightbaneControlPetAggressionAction : public Action { public: - KarazhanNetherspiteBlockRedBeamAction(PlayerbotAI* botAI, std::string const name = "karazhan netherspite block red beam") : MovementAction(botAI, name) {} - + NightbaneControlPetAggressionAction( + PlayerbotAI* botAI, std::string const name = "nightbane control pet aggression") : Action(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanNetherspiteBlockBlueBeamAction : public MovementAction +class NightbaneFlightPhaseMovementAction : public MovementAction { public: - KarazhanNetherspiteBlockBlueBeamAction(PlayerbotAI* botAI, std::string const name = "karazhan netherspite block blue beam") : MovementAction(botAI, name) {} - + NightbaneFlightPhaseMovementAction( + PlayerbotAI* botAI, std::string const name = "nightbane flight phase movement") : MovementAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanNetherspiteBlockGreenBeamAction : public MovementAction +class NightbaneManageTimersAndTrackersAction : public Action { public: - KarazhanNetherspiteBlockGreenBeamAction(PlayerbotAI* botAI, std::string const name = "karazhan netherspite block green beam") : MovementAction(botAI, name) {} - + NightbaneManageTimersAndTrackersAction( + PlayerbotAI* botAI, std::string const name = "nightbane manage timers and trackers") : Action(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; -}; - -class KarazhanNetherspiteAvoidBeamAndVoidZoneAction : public MovementAction -{ -public: - KarazhanNetherspiteAvoidBeamAndVoidZoneAction(PlayerbotAI* botAI, std::string const name = "karazhan netherspite avoid beam and void zone") : MovementAction(botAI, name) {} - - bool Execute(Event event) override; - bool isUseful() override; -}; - -class KarazhanNetherspiteBanishPhaseAvoidVoidZoneAction : public MovementAction -{ -public: - KarazhanNetherspiteBanishPhaseAvoidVoidZoneAction(PlayerbotAI* botAI, std::string const name = "karazhan netherspite banish phase avoid void zone") : MovementAction(botAI, name) {} - - bool Execute(Event event) override; - bool isUseful() override; -}; - -class KarazhanPrinceMalchezaarNonTankAvoidHazardAction : public MovementAction -{ -public: - KarazhanPrinceMalchezaarNonTankAvoidHazardAction(PlayerbotAI* botAI, std::string const name = "karazhan prince malchezaar non-tank avoid hazard") : MovementAction(botAI, name) {} - - bool Execute(Event event) override; - bool isUseful() override; -}; - -class KarazhanPrinceMalchezaarTankAvoidHazardAction : public MovementAction -{ -public: - KarazhanPrinceMalchezaarTankAvoidHazardAction(PlayerbotAI* botAI, std::string const name = "karazhan prince malchezaar tank avoid hazard") : MovementAction(botAI, name) {} - - bool Execute(Event event) override; - bool isUseful() override; }; #endif diff --git a/src/strategy/raids/karazhan/RaidKarazhanHelpers.cpp b/src/strategy/raids/karazhan/RaidKarazhanHelpers.cpp index 29634174..9ca9f7b3 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanHelpers.cpp +++ b/src/strategy/raids/karazhan/RaidKarazhanHelpers.cpp @@ -1,316 +1,356 @@ -#include -#include - #include "RaidKarazhanHelpers.h" #include "RaidKarazhanActions.h" -#include "AiObjectContext.h" -#include "PlayerbotMgr.h" -#include "Position.h" -#include "Spell.h" +#include "Playerbots.h" +#include "RtiTargetValue.h" -const Position KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION = Position(-10945.881f, -2103.782f, 92.712f); -const Position KARAZHAN_MAIDEN_OF_VIRTUE_RANGED_POSITION[8] = +namespace KarazhanHelpers { - { -10931.178f, -2116.580f, 92.179f }, - { -10925.828f, -2102.425f, 92.180f }, - { -10933.089f, -2088.5017f, 92.180f }, - { -10947.59f, -2082.8147f, 92.180f }, - { -10960.912f, -2090.4368f, 92.179f }, - { -10966.017f, -2105.288f, 92.175f }, - { -10959.242f, -2119.6172f, 92.180f }, - { -10944.495f, -2123.857f, 92.180f }, -}; + // Attumen the Huntsman + std::unordered_map attumenDpsWaitTimer; + // Big Bad Wolf + std::unordered_map bigBadWolfRunIndex; + // Netherspite + std::unordered_map netherspiteDpsWaitTimer; + std::unordered_map redBeamMoveTimer; + std::unordered_map lastBeamMoveSideways; + // Nightbane + std::unordered_map nightbaneDpsWaitTimer; + std::unordered_map nightbaneTankStep; + std::unordered_map nightbaneRangedStep; + std::unordered_map nightbaneFlightPhaseStartTimer; + std::unordered_map nightbaneRainOfBonesHit; -const Position KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION = Position(-10913.391f, -1773.508f, 90.477f); -const Position KARAZHAN_BIG_BAD_WOLF_RUN_POSITION[4] = -{ - { -10875.456f, -1779.036f, 90.477f }, - { -10872.281f, -1751.638f, 90.477f }, - { -10910.492f, -1747.401f, 90.477f }, - { -10913.391f, -1773.508f, 90.477f }, -}; - -const Position KARAZHAN_THE_CURATOR_BOSS_POSITION = Position(-11139.463f, -1884.645f, 165.765f); - -void RaidKarazhanHelpers::MarkTargetWithSkull(Unit* target) -{ - if (!target) + const Position MAIDEN_OF_VIRTUE_BOSS_POSITION = { -10945.881f, -2103.782f, 92.712f }; + const Position MAIDEN_OF_VIRTUE_RANGED_POSITION[8] = { - return; - } + { -10931.178f, -2116.580f, 92.179f }, + { -10925.828f, -2102.425f, 92.180f }, + { -10933.089f, -2088.502f, 92.180f }, + { -10947.590f, -2082.815f, 92.180f }, + { -10960.912f, -2090.437f, 92.179f }, + { -10966.017f, -2105.288f, 92.175f }, + { -10959.242f, -2119.617f, 92.180f }, + { -10944.495f, -2123.857f, 92.180f }, + }; - if (Group* group = bot->GetGroup()) + const Position BIG_BAD_WOLF_BOSS_POSITION = { -10913.391f, -1773.508f, 90.477f }; + const Position BIG_BAD_WOLF_RUN_POSITION[4] = { - constexpr uint8_t skullIconId = 7; - ObjectGuid skullGuid = group->GetTargetIcon(skullIconId); + { -10875.456f, -1779.036f, 90.477f }, + { -10872.281f, -1751.638f, 90.477f }, + { -10910.492f, -1747.401f, 90.477f }, + { -10913.391f, -1773.508f, 90.477f }, + }; - if (skullGuid != target->GetGUID()) + const Position THE_CURATOR_BOSS_POSITION = { -11139.463f, -1884.645f, 165.765f }; + + const Position NIGHTBANE_TRANSITION_BOSS_POSITION = { -11160.646f, -1932.773f, 91.473f }; // near some ribs + const Position NIGHTBANE_FINAL_BOSS_POSITION = { -11173.530f, -1940.707f, 91.473f }; + const Position NIGHTBANE_RANGED_POSITION1 = { -11145.949f, -1970.927f, 91.473f }; + const Position NIGHTBANE_RANGED_POSITION2 = { -11143.594f, -1954.981f, 91.473f }; + const Position NIGHTBANE_RANGED_POSITION3 = { -11159.778f, -1961.031f, 91.473f }; + 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()) { - group->SetTargetIcon(skullIconId, bot->GetGUID(), target->GetGUID()); - } - } -} - -Unit* RaidKarazhanHelpers::GetFirstAliveUnit(const std::vector& units) -{ - for (Unit* unit : units) - { - if (unit && unit->IsAlive()) - { - return unit; + ObjectGuid currentGuid = group->GetTargetIcon(iconId); + if (currentGuid != target->GetGUID()) + group->SetTargetIcon(iconId, bot->GetGUID(), target->GetGUID()); } } - return nullptr; -} - -Unit* RaidKarazhanHelpers::GetFirstAliveUnitByEntry(uint32 entry) -{ - const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - - for (auto const& npcGuid : npcs) + void MarkTargetWithSkull(Player* bot, Unit* target) { - Unit* unit = botAI->GetUnit(npcGuid); + MarkTargetWithIcon(bot, target, RtiTargetValue::skullIndex); + } - if (unit && unit->IsAlive() && unit->GetEntry() == entry) + 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) { - return unit; + botAI->GetAiObjectContext()->GetValue("rti")->Set(rtiName); + botAI->GetAiObjectContext()->GetValue("rti target")->Set(target); } } - return nullptr; -} - -Unit* RaidKarazhanHelpers::GetNearestPlayerInRadius(float radius) -{ - if (Group* group = bot->GetGroup()) + // Only one bot is needed to set/reset mapwide timers + bool IsMapIDTimerManager(PlayerbotAI* botAI, Player* bot) { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + if (Group* group = bot->GetGroup()) { - Player* member = itr->GetSource(); - - if (!member || !member->IsAlive() || member == bot) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - continue; - } - - if (bot->GetExactDist2d(member) < radius) - { - return member; + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsDps(member) && GET_PLAYERBOT_AI(member)) + return member == bot; } } + + return false; } - return nullptr; -} - -bool RaidKarazhanHelpers::IsFlameWreathActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); - Spell* currentSpell = boss ? boss->GetCurrentSpell(CURRENT_GENERIC_SPELL) : nullptr; - if (currentSpell && currentSpell->m_spellInfo && currentSpell->m_spellInfo->Id == SPELL_FLAME_WREATH) + Unit* GetFirstAliveUnit(const std::vector& units) { - return true; - } - - if (Group* group = bot->GetGroup()) - { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + for (Unit* unit : units) { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive()) + if (unit && unit->IsAlive()) + return unit; + } + + 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()) { - continue; - } - if (member->HasAura(SPELL_AURA_FLAME_WREATH)) - { - return true; + 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; } - return false; -} - -// Red beam blockers: tank bots, no Nether Exhaustion Red -std::vector RaidKarazhanHelpers::GetRedBlockers() -{ - std::vector redBlockers; - if (Group* group = bot->GetGroup()) + bool IsFlameWreathActive(PlayerbotAI* botAI, Player* bot) { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || !botAI->IsTank(member) || !GET_PLAYERBOT_AI(member) || - member->HasAura(SPELL_NETHER_EXHAUSTION_RED)) - { - continue; - } - redBlockers.push_back(member); - } - } + Unit* aran = botAI->GetAiObjectContext()->GetValue("find target", "shade of aran")->Get(); + Spell* currentSpell = aran ? aran->GetCurrentSpell(CURRENT_GENERIC_SPELL) : nullptr; - return redBlockers; -} + if (currentSpell && currentSpell->m_spellInfo && + currentSpell->m_spellInfo->Id == SPELL_FLAME_WREATH_CAST) + return true; -// Blue beam blockers: non-Rogue/Warrior DPS bots, no Nether Exhaustion Blue and ≤25 stacks of Blue Beam debuff -std::vector RaidKarazhanHelpers::GetBlueBlockers() -{ - std::vector blueBlockers; - if (Group* group = bot->GetGroup()) - { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + if (Group* group = bot->GetGroup()) { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member)) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - continue; - } - bool isDps = botAI->IsDps(member); - bool isWarrior = member->getClass() == CLASS_WARRIOR; - bool isRogue = member->getClass() == CLASS_ROGUE; - bool hasExhaustion = member->HasAura(SPELL_NETHER_EXHAUSTION_BLUE); - Aura* blueBuff = member->GetAura(SPELL_BLUE_BEAM_DEBUFF); - bool overStack = blueBuff && blueBuff->GetStackAmount() >= 26; - if (isDps && !isWarrior && !isRogue && !hasExhaustion && !overStack) - { - blueBlockers.push_back(member); + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (member->HasAura(SPELL_FLAME_WREATH_AURA)) + return true; } } + + return false; } - return blueBlockers; -} - -// Green beam blockers: -// (1) Rogue and non-tank Warrior bots, no Nether Exhaustion Green -// (2) Healer bots, no Nether Exhaustion Green and ≤25 stacks of Green Beam debuff -std::vector RaidKarazhanHelpers::GetGreenBlockers() -{ - std::vector greenBlockers; - if (Group* group = bot->GetGroup()) + // Red beam blockers: tank bots, no Nether Exhaustion Red + std::vector GetRedBlockers(PlayerbotAI* botAI, Player* bot) { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + std::vector redBlockers; + if (Group* group = bot->GetGroup()) { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member)) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - continue; - } - bool hasExhaustion = member->HasAura(SPELL_NETHER_EXHAUSTION_GREEN); - Aura* greenBuff = member->GetAura(SPELL_GREEN_BEAM_DEBUFF); - bool overStack = greenBuff && greenBuff->GetStackAmount() >= 26; - bool isRogue = member->getClass() == CLASS_ROGUE; - bool isDpsWarrior = member->getClass() == CLASS_WARRIOR && botAI->IsDps(member); - bool eligibleRogueWarrior = (isRogue || isDpsWarrior) && !hasExhaustion; - bool isHealer = botAI->IsHeal(member); - bool eligibleHealer = isHealer && !hasExhaustion && !overStack; - if (eligibleRogueWarrior || eligibleHealer) - { - greenBlockers.push_back(member); + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !botAI->IsTank(member) || !GET_PLAYERBOT_AI(member) || + member->HasAura(SPELL_NETHER_EXHAUSTION_RED)) + continue; + + redBlockers.push_back(member); } } + + return redBlockers; } - return greenBlockers; -} - -Position RaidKarazhanHelpers::GetPositionOnBeam(Unit* boss, Unit* portal, float distanceFromBoss) -{ - float bx = boss->GetPositionX(); - float by = boss->GetPositionY(); - float bz = boss->GetPositionZ(); - float px = portal->GetPositionX(); - float py = portal->GetPositionY(); - - float dx = px - bx; - float dy = py - by; - float length = sqrt(dx*dx + dy*dy); - if (length == 0.0f) + // Blue beam blockers: non-Rogue/Warrior DPS bots, no Nether Exhaustion Blue and <24 stacks of Blue Beam debuff + std::vector GetBlueBlockers(PlayerbotAI* botAI, Player* bot) { - return Position(bx, by, bz); + std::vector blueBlockers; + 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)) + continue; + + bool hasExhaustion = member->HasAura(SPELL_NETHER_EXHAUSTION_BLUE); + Aura* blueBuff = member->GetAura(SPELL_BLUE_BEAM_DEBUFF); + bool overStack = blueBuff && blueBuff->GetStackAmount() >= 24; + + bool isDps = botAI->IsDps(member); + bool isWarrior = member->getClass() == CLASS_WARRIOR; + bool isRogue = member->getClass() == CLASS_ROGUE; + + if (isDps && !isWarrior && !isRogue && !hasExhaustion && !overStack) + blueBlockers.push_back(member); + } + } + + return blueBlockers; } - dx /= length; - dy /= length; - float targetX = bx + dx * distanceFromBoss; - float targetY = by + dy * distanceFromBoss; - float targetZ = bz; - - return Position(targetX, targetY, targetZ); -} - -std::tuple RaidKarazhanHelpers::GetCurrentBeamBlockers() -{ - static ObjectGuid currentRedBlocker; - static ObjectGuid currentGreenBlocker; - static ObjectGuid currentBlueBlocker; - - Player* redBlocker = nullptr; - Player* greenBlocker = nullptr; - Player* blueBlocker = nullptr; - - std::vector redBlockers = GetRedBlockers(); - if (!redBlockers.empty()) + // Green beam blockers: + // (1) Prioritize Rogues and non-tank Warrior bots, no Nether Exhaustion Green + // (2) Then assign Healer bots, no Nether Exhaustion Green and <24 stacks of Green Beam debuff + std::vector GetGreenBlockers(PlayerbotAI* botAI, Player* bot) { - auto it = std::find_if(redBlockers.begin(), redBlockers.end(), [](Player* p) + std::vector greenBlockers; + if (Group* group = bot->GetGroup()) { - return p && p->GetGUID() == currentRedBlocker; - }); - if (it != redBlockers.end()) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member)) + continue; + + bool hasExhaustion = member->HasAura(SPELL_NETHER_EXHAUSTION_GREEN); + bool isRogue = member->getClass() == CLASS_ROGUE; + bool isDpsWarrior = member->getClass() == CLASS_WARRIOR && botAI->IsDps(member); + bool eligibleRogueWarrior = (isRogue || isDpsWarrior) && !hasExhaustion; + + if (eligibleRogueWarrior) + greenBlockers.push_back(member); + } + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member)) + continue; + + bool hasExhaustion = member->HasAura(SPELL_NETHER_EXHAUSTION_GREEN); + Aura* greenBuff = member->GetAura(SPELL_GREEN_BEAM_DEBUFF); + bool overStack = greenBuff && greenBuff->GetStackAmount() >= 24; + bool isHealer = botAI->IsHeal(member); + bool eligibleHealer = isHealer && !hasExhaustion && !overStack; + + if (eligibleHealer) + greenBlockers.push_back(member); + } + } + + return greenBlockers; + } + + std::tuple GetCurrentBeamBlockers(PlayerbotAI* botAI, Player* bot) + { + static ObjectGuid currentRedBlocker; + static ObjectGuid currentGreenBlocker; + static ObjectGuid currentBlueBlocker; + + Player* redBlocker = nullptr; + Player* greenBlocker = nullptr; + Player* blueBlocker = nullptr; + + std::vector redBlockers = GetRedBlockers(botAI, bot); + if (!redBlockers.empty()) { - redBlocker = *it; + auto it = std::find_if(redBlockers.begin(), redBlockers.end(), [](Player* player) + { + return player && player->GetGUID() == currentRedBlocker; + }); + + if (it != redBlockers.end()) + redBlocker = *it; + else + redBlocker = redBlockers.front(); + + currentRedBlocker = redBlocker ? redBlocker->GetGUID() : ObjectGuid::Empty; } else { - redBlocker = redBlockers.front(); + currentRedBlocker = ObjectGuid::Empty; + redBlocker = nullptr; } - currentRedBlocker = redBlocker ? redBlocker->GetGUID() : ObjectGuid::Empty; - } - else - { - currentRedBlocker = ObjectGuid::Empty; - redBlocker = nullptr; - } - std::vector greenBlockers = GetGreenBlockers(); - if (!greenBlockers.empty()) - { - auto it = std::find_if(greenBlockers.begin(), greenBlockers.end(), [](Player* p) + std::vector greenBlockers = GetGreenBlockers(botAI, bot); + if (!greenBlockers.empty()) { - return p && p->GetGUID() == currentGreenBlocker; - }); - if (it != greenBlockers.end()) - { - greenBlocker = *it; + auto it = std::find_if(greenBlockers.begin(), greenBlockers.end(), [](Player* player) + { + return player && player->GetGUID() == currentGreenBlocker; + }); + + if (it != greenBlockers.end()) + greenBlocker = *it; + else + greenBlocker = greenBlockers.front(); + + currentGreenBlocker = greenBlocker ? greenBlocker->GetGUID() : ObjectGuid::Empty; } else { - greenBlocker = greenBlockers.front(); + currentGreenBlocker = ObjectGuid::Empty; + greenBlocker = nullptr; } - currentGreenBlocker = greenBlocker ? greenBlocker->GetGUID() : ObjectGuid::Empty; - } - else - { - currentGreenBlocker = ObjectGuid::Empty; - greenBlocker = nullptr; - } - std::vector blueBlockers = GetBlueBlockers(); + std::vector blueBlockers = GetBlueBlockers(botAI, bot); if (!blueBlockers.empty()) { - auto it = std::find_if(blueBlockers.begin(), blueBlockers.end(), [](Player* p) + auto it = std::find_if(blueBlockers.begin(), blueBlockers.end(), [](Player* player) { - return p && p->GetGUID() == currentBlueBlocker; + return player && player->GetGUID() == currentBlueBlocker; }); + if (it != blueBlockers.end()) - { blueBlocker = *it; - } else - { blueBlocker = blueBlockers.front(); - } + currentBlueBlocker = blueBlocker ? blueBlocker->GetGUID() : ObjectGuid::Empty; } else @@ -319,91 +359,132 @@ std::tuple RaidKarazhanHelpers::GetCurrentBeamBlocker blueBlocker = nullptr; } - return std::make_tuple(redBlocker, greenBlocker, blueBlocker); -} - -std::vector RaidKarazhanHelpers::GetAllVoidZones() -{ - std::vector voidZones; - const float radius = 30.0f; - const GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); - for (auto const& npcGuid : npcs) - { - Unit* unit = botAI->GetUnit(npcGuid); - if (!unit || unit->GetEntry() != NPC_VOID_ZONE) - { - continue; - } - float dist = bot->GetExactDist2d(unit); - if (dist < radius) - { - voidZones.push_back(unit); - } + return std::make_tuple(redBlocker, greenBlocker, blueBlocker); } - return voidZones; -} - -bool RaidKarazhanHelpers::IsSafePosition(float x, float y, float z, - const std::vector& hazards, float hazardRadius) -{ - for (Unit* hazard : hazards) + std::vector GetAllVoidZones(PlayerbotAI* botAI, Player* bot) { - float dist = std::sqrt(std::pow(x - hazard->GetPositionX(), 2) + std::pow(y - hazard->GetPositionY(), 2)); - if (dist < hazardRadius) + std::vector voidZones; + const float radius = 30.0f; + const GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); + for (auto const& npcGuid : npcs) { - return false; + Unit* unit = botAI->GetUnit(npcGuid); + if (!unit || unit->GetEntry() != NPC_VOID_ZONE) + continue; + + float dist = bot->GetExactDist2d(unit); + if (dist < radius) + voidZones.push_back(unit); } + + return voidZones; } - return true; -} - -std::vector RaidKarazhanHelpers::GetSpawnedInfernals() const -{ - std::vector infernals; - const GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); - for (auto const& npcGuid : npcs) + bool IsSafePosition(float x, float y, float z, const std::vector& hazards, float hazardRadius) { - Unit* unit = botAI->GetUnit(npcGuid); - if (unit && unit->GetEntry() == NPC_NETHERSPITE_INFERNAL) + for (Unit* hazard : hazards) { - infernals.push_back(unit); + float dist = hazard->GetExactDist2d(x, y); + if (dist < hazardRadius) + return false; } - } - return infernals; -} - -bool RaidKarazhanHelpers::IsStraightPathSafe(const Position& start, const Position& target, const std::vector& hazards, float hazardRadius, float stepSize) -{ - float sx = start.GetPositionX(); - float sy = start.GetPositionY(); - float sz = start.GetPositionZ(); - float tx = target.GetPositionX(); - float ty = target.GetPositionY(); - float tz = target.GetPositionZ(); - float totalDist = std::sqrt(std::pow(tx - sx, 2) + std::pow(ty - sy, 2)); - if (totalDist == 0.0f) - { return true; } - for (float checkDist = 0.0f; checkDist <= totalDist; checkDist += stepSize) + std::vector GetSpawnedInfernals(PlayerbotAI* botAI) { - float t = checkDist / totalDist; - float checkX = sx + (tx - sx) * t; - float checkY = sy + (ty - sy) * t; - float checkZ = sz + (tz - sz) * t; - for (Unit* hazard : hazards) + std::vector infernals; + const GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); + for (auto const& npcGuid : npcs) { - float hazardDist = std::sqrt(std::pow(checkX - hazard->GetPositionX(), 2) + std::pow(checkY - hazard->GetPositionY(), 2)); - if (hazardDist < hazardRadius) - { - return false; - } + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->GetEntry() == NPC_NETHERSPITE_INFERNAL) + infernals.push_back(unit); } + + return infernals; } - return true; + bool IsStraightPathSafe(const Position& start, const Position& target, const std::vector& hazards, + float hazardRadius, float stepSize) + { + float sx = start.GetPositionX(); + float sy = start.GetPositionY(); + float sz = start.GetPositionZ(); + float tx = target.GetPositionX(); + float ty = target.GetPositionY(); + float tz = target.GetPositionZ(); + + const float totalDist = start.GetExactDist2d(target.GetPositionX(), target.GetPositionY()); + if (totalDist == 0.0f) + return true; + + for (float checkDist = 0.0f; checkDist <= totalDist; checkDist += stepSize) + { + float t = checkDist / totalDist; + float checkX = sx + (tx - sx) * t; + float checkY = sy + (ty - sy) * t; + float checkZ = sz + (tz - sz) * t; + for (Unit* hazard : hazards) + { + const float hx = checkX - hazard->GetPositionX(); + const float hy = checkY - hazard->GetPositionY(); + if ((hx*hx + hy*hy) < hazardRadius * hazardRadius) + return false; + } + } + + return true; + } + + bool TryFindSafePositionWithSafePath( + Player* bot, float originX, float originY, float originZ, float centerX, float centerY, float centerZ, + const std::vector& hazards, float safeDistance, float stepSize, uint8 numAngles, + float maxSampleDist, bool requireSafePath, float& bestDestX, float& bestDestY, float& bestDestZ) + { + float bestMoveDist = std::numeric_limits::max(); + bool found = false; + + for (int i = 0; i < numAngles; ++i) + { + float angle = (2.0f * M_PI * i) / numAngles; + float dx = cos(angle); + float dy = sin(angle); + + for (float dist = stepSize; dist <= maxSampleDist; dist += stepSize) + { + float x = centerX + dx * dist; + float y = centerY + dy * dist; + float z = centerZ; + float destX = x, destY = y, destZ = z; + if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, centerX, centerY, centerZ, + destX, destY, destZ, true)) + continue; + + if (!IsSafePosition(destX, destY, destZ, hazards, safeDistance)) + continue; + + if (requireSafePath) + { + if (!IsStraightPathSafe(Position(originX, originY, originZ), Position(destX, destY, destZ), + hazards, safeDistance, stepSize)) + continue; + } + + const float moveDist = Position(originX, originY, originZ).GetExactDist2d(destX, destY); + if (moveDist < bestMoveDist) + { + bestMoveDist = moveDist; + bestDestX = destX; + bestDestY = destY; + bestDestZ = destZ; + found = true; + } + } + } + + return found; + } } diff --git a/src/strategy/raids/karazhan/RaidKarazhanHelpers.h b/src/strategy/raids/karazhan/RaidKarazhanHelpers.h index d26d8501..fb08333a 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanHelpers.h +++ b/src/strategy/raids/karazhan/RaidKarazhanHelpers.h @@ -1,85 +1,136 @@ #ifndef _PLAYERBOT_RAIDKARAZHANHELPERS_H_ #define _PLAYERBOT_RAIDKARAZHANHELPERS_H_ +#include +#include + #include "AiObject.h" -#include "Playerbots.h" #include "Position.h" +#include "Unit.h" -enum KarazhanSpells +namespace KarazhanHelpers { - // Maiden of Virtue - SPELL_REPENTANCE = 29511, + enum KarazhanSpells + { + // Maiden of Virtue + SPELL_REPENTANCE = 29511, - // Opera Event - SPELL_LITTLE_RED_RIDING_HOOD = 30756, + // Opera Event + SPELL_LITTLE_RED_RIDING_HOOD = 30756, - // Shade of Aran - SPELL_FLAME_WREATH = 30004, - SPELL_AURA_FLAME_WREATH = 29946, - SPELL_ARCANE_EXPLOSION = 29973, - SPELL_WARLOCK_BANISH = 18647, // Rank 2 + // The Curator + SPELL_CURATOR_EVOCATION = 30254, - // Netherspite - SPELL_GREEN_BEAM_DEBUFF = 30422, - SPELL_BLUE_BEAM_DEBUFF = 30423, - SPELL_NETHER_EXHAUSTION_RED = 38637, - SPELL_NETHER_EXHAUSTION_GREEN = 38638, - SPELL_NETHER_EXHAUSTION_BLUE = 38639, - SPELL_NETHERSPITE_BANISHED = 39833, + // Shade of Aran + SPELL_FLAME_WREATH_CAST = 30004, + SPELL_FLAME_WREATH_AURA = 29946, + SPELL_ARCANE_EXPLOSION = 29973, - // Prince Malchezaar - SPELL_ENFEEBLE = 30843, -}; + // Netherspite + SPELL_RED_BEAM_DEBUFF = 30421, // "Nether Portal - Perseverance" (player aura) + SPELL_GREEN_BEAM_DEBUFF = 30422, // "Nether Portal - Serenity" (player aura) + SPELL_BLUE_BEAM_DEBUFF = 30423, // "Nether Portal - Dominance" (player aura) + SPELL_GREEN_BEAM_HEAL = 30467, // "Nether Portal - Serenity" (Netherspite aura) + SPELL_NETHER_EXHAUSTION_RED = 38637, + SPELL_NETHER_EXHAUSTION_GREEN = 38638, + SPELL_NETHER_EXHAUSTION_BLUE = 38639, + SPELL_NETHERSPITE_BANISHED = 39833, // "Vortex Shade Black" + + // Prince Malchezaar + SPELL_ENFEEBLE = 30843, + + // Nightbane + SPELL_CHARRED_EARTH = 30129, + SPELL_BELLOWING_ROAR = 36922, + SPELL_RAIN_OF_BONES = 37091, + + // Warlock + SPELL_WARLOCK_BANISH = 18647, + + // Priest + SPELL_FEAR_WARD = 6346, + }; + + enum KarazhanNPCs + { + // Trash + NPC_SPECTRAL_RETAINER = 16410, + NPC_MANA_WARP = 16530, + + // Attumen the Huntsman + NPC_ATTUMEN_THE_HUNTSMAN = 15550, + NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED = 16152, + + // Shade of Aran + NPC_CONJURED_ELEMENTAL = 17167, + + // Netherspite + NPC_VOID_ZONE = 16697, + NPC_GREEN_PORTAL = 17367, // "Nether Portal - Serenity " + NPC_BLUE_PORTAL = 17368, // "Nether Portal - Dominance " + NPC_RED_PORTAL = 17369, // "Nether Portal - Perseverance " + + // Prince Malchezaar + NPC_NETHERSPITE_INFERNAL = 17646, + }; + + const uint32 KARAZHAN_MAP_ID = 532; + const float NIGHTBANE_FLIGHT_Z = 95.0f; -enum KarazhanNpcs -{ // Attumen the Huntsman - NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED = 16152, - - // Terestian Illhoof - NPC_KILREK = 17229, - NPC_DEMON_CHAINS = 17248, - - // Shade of Aran - NPC_CONJURED_ELEMENTAL = 17167, - + extern std::unordered_map attumenDpsWaitTimer; + // Big Bad Wolf + extern std::unordered_map bigBadWolfRunIndex; // Netherspite - NPC_VOID_ZONE = 16697, - NPC_RED_PORTAL = 17369, - NPC_BLUE_PORTAL = 17368, - NPC_GREEN_PORTAL = 17367, + extern std::unordered_map netherspiteDpsWaitTimer; + extern std::unordered_map redBeamMoveTimer; + extern std::unordered_map lastBeamMoveSideways; + // Nightbane + extern std::unordered_map nightbaneDpsWaitTimer; + extern std::unordered_map nightbaneTankStep; + extern std::unordered_map nightbaneRangedStep; + extern std::unordered_map nightbaneFlightPhaseStartTimer; + extern std::unordered_map nightbaneRainOfBonesHit; - // Prince Malchezaar - NPC_NETHERSPITE_INFERNAL = 17646, -}; + extern const Position MAIDEN_OF_VIRTUE_BOSS_POSITION; + extern const Position MAIDEN_OF_VIRTUE_RANGED_POSITION[8]; + extern const Position BIG_BAD_WOLF_BOSS_POSITION; + extern const Position BIG_BAD_WOLF_RUN_POSITION[4]; + extern const Position THE_CURATOR_BOSS_POSITION; + extern const Position NIGHTBANE_TRANSITION_BOSS_POSITION; + extern const Position NIGHTBANE_FINAL_BOSS_POSITION; + extern const Position NIGHTBANE_RANGED_POSITION1; + extern const Position NIGHTBANE_RANGED_POSITION2; + extern const Position NIGHTBANE_RANGED_POSITION3; + extern const Position NIGHTBANE_FLIGHT_STACK_POSITION; + extern const Position NIGHTBANE_RAIN_OF_BONES_POSITION; -extern const Position KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION; -extern const Position KARAZHAN_MAIDEN_OF_VIRTUE_RANGED_POSITION[8]; -extern const Position KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION; -extern const Position KARAZHAN_BIG_BAD_WOLF_RUN_POSITION[4]; -extern const Position KARAZHAN_THE_CURATOR_BOSS_POSITION; - -class RaidKarazhanHelpers : public AiObject -{ -public: - explicit RaidKarazhanHelpers(PlayerbotAI* botAI) : AiObject(botAI) {} - - void MarkTargetWithSkull(Unit* /*target*/); - Unit* GetFirstAliveUnit(const std::vector& /*units*/); - Unit* GetFirstAliveUnitByEntry(uint32 /*entry*/); - Unit* GetNearestPlayerInRadius(float /*radius*/ = 5.0f); - bool IsFlameWreathActive(); - Position GetPositionOnBeam(Unit* boss, Unit* portal, float distanceFromBoss); - std::vector GetRedBlockers(); - std::vector GetBlueBlockers(); - std::vector GetGreenBlockers(); - std::tuple GetCurrentBeamBlockers(); - std::vector GetAllVoidZones(); - bool IsSafePosition (float x, float y, float z, - const std::vector& hazards, float hazardRadius); - std::vector GetSpawnedInfernals() const; - bool IsStraightPathSafe(const Position& start, const Position& target, - const std::vector& hazards, float hazardRadius, float stepSize); -}; + 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 IsMapIDTimerManager(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); + std::vector GetGreenBlockers(PlayerbotAI* botAI, Player* bot); + std::tuple GetCurrentBeamBlockers(PlayerbotAI* botAI, Player* bot); + std::vector GetAllVoidZones(PlayerbotAI *botAI, Player* bot); + bool IsSafePosition (float x, float y, float z, const std::vector& hazards, float hazardRadius); + std::vector GetSpawnedInfernals(PlayerbotAI* botAI); + bool IsStraightPathSafe( + const Position& start, const Position& target, + const std::vector& hazards, float hazardRadius, float stepSize); + bool TryFindSafePositionWithSafePath( + Player* bot, float originX, float originY, float originZ, float centerX, float centerY, float centerZ, + const std::vector& hazards, float safeDistance, float stepSize, uint8 numAngles, + float maxSampleDist, bool requireSafePath, float& bestDestX, float& bestDestY, float& bestDestZ); +} #endif diff --git a/src/strategy/raids/karazhan/RaidKarazhanMultipliers.cpp b/src/strategy/raids/karazhan/RaidKarazhanMultipliers.cpp index f68cbe8b..cd64ea98 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanMultipliers.cpp +++ b/src/strategy/raids/karazhan/RaidKarazhanMultipliers.cpp @@ -1,265 +1,359 @@ #include "RaidKarazhanMultipliers.h" #include "RaidKarazhanActions.h" #include "RaidKarazhanHelpers.h" -#include "AiObjectContext.h" #include "AttackAction.h" -#include "DruidBearActions.h" -#include "DruidCatActions.h" +#include "ChooseTargetActions.h" +#include "DruidActions.h" +#include "FollowActions.h" +#include "GenericActions.h" +#include "HunterActions.h" +#include "MageActions.h" +#include "Playerbots.h" +#include "PriestActions.h" +#include "ReachTargetActions.h" #include "RogueActions.h" -#include "WarriorActions.h" +#include "ShamanActions.h" -static bool IsChargeAction(Action* action) -{ - return dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action); -} +using namespace KarazhanHelpers; -float KarazhanAttumenTheHuntsmanMultiplier::GetValue(Action* action) +// Keep tanks from jumping back and forth between Attumen and Midnight +float AttumenTheHuntsmanDisableTankAssistMultiplier::GetValue(Action* action) { - RaidKarazhanHelpers karazhanHelper(botAI); - Unit* boss = karazhanHelper.GetFirstAliveUnitByEntry(NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); - if (boss && !(botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot) && - (dynamic_cast(action) && - !dynamic_cast(action))) - { + Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"); + if (!midnight) + return 1.0f; + + Unit* attumen = AI_VALUE2(Unit*, "find target", "attumen the huntsman"); + if (!attumen) + return 1.0f; + + if (bot->GetVictim() != nullptr && dynamic_cast(action)) return 0.0f; + + return 1.0f; +} + +// Try to get rid of jittering when bots are stacked behind Attumen +float AttumenTheHuntsmanStayStackedMultiplier::GetValue(Action* action) +{ + Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + if (!attumenMounted) + return 1.0f; + + if (!botAI->IsMainTank(bot) && attumenMounted->GetVictim() != bot) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; } return 1.0f; } -float KarazhanBigBadWolfMultiplier::GetValue(Action* action) +// Give the main tank 8 seconds to grab aggro when Attumen mounts Midnight +// In reality it's shorter because it takes Attumen a few seconds to aggro after mounting +float AttumenTheHuntsmanWaitForDpsMultiplier::GetValue(Action* action) { - Unit* boss = AI_VALUE2(Unit*, "find target", "the big bad wolf"); - if (!boss) - { + Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + if (!attumenMounted) return 1.0f; - } - if (bot->HasAura(SPELL_LITTLE_RED_RIDING_HOOD)) + const time_t now = std::time(nullptr); + const uint8 dpsWaitSeconds = 8; + + auto it = attumenDpsWaitTimer.find(KARAZHAN_MAP_ID); + if (it == attumenDpsWaitTimer.end() || (now - it->second) < dpsWaitSeconds) { - if ((dynamic_cast(action) && !dynamic_cast(action)) || - (dynamic_cast(action))) + if (!botAI->IsMainTank(bot)) { - return 0.0f; - } - } - - return 1.0f; -} - -float KarazhanShadeOfAranMultiplier::GetValue(Action* action) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); - if (!boss) - { - return 1.0f; - } - - if (boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_ARCANE_EXPLOSION)) - { - if (IsChargeAction(action)) - { - return 0.0f; - } - - if (dynamic_cast(action)) - { - const float safeDistance = 20.0f; - if (bot->GetDistance2d(boss) >= safeDistance) - { + if (dynamic_cast(action) || (dynamic_cast(action) && + !dynamic_cast(action))) return 0.0f; - } } } - bool flameWreathActive = boss->HasAura(SPELL_FLAME_WREATH); - if (!flameWreathActive && bot->GetGroup()) + return 1.0f; +} + +// The assist tank should stay on the boss to be 2nd on aggro and tank Hateful Bolts +float TheCuratorDisableTankAssistMultiplier::GetValue(Action* action) +{ + Unit* curator = AI_VALUE2(Unit*, "find target", "the curator"); + if (!curator) + return 1.0f; + + if (bot->GetVictim() != nullptr && dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Save Bloodlust/Heroism for Evocation (100% increased damage) +float TheCuratorDelayBloodlustAndHeroismMultiplier::GetValue(Action* action) +{ + Unit* curator = AI_VALUE2(Unit*, "find target", "the curator"); + if (!curator) + return 1.0f; + + if (!curator->HasAura(SPELL_CURATOR_EVOCATION)) { - for (GroupReference* itr = bot->GetGroup()->GetFirstMember(); itr != nullptr; itr = itr->next()) + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Don't charge back in when running from Arcane Explosion +float ShadeOfAranArcaneExplosionDisableChargeMultiplier::GetValue(Action* action) +{ + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + if (!aran) + return 1.0f; + + if (aran->HasUnitState(UNIT_STATE_CASTING) && + aran->FindCurrentSpellBySpellId(SPELL_ARCANE_EXPLOSION)) + { + if (dynamic_cast(action)) + return 0.0f; + + if (bot->GetDistance2d(aran) >= 20.0f) { - Player* member = itr->GetSource(); - if (member && member->HasAura(SPELL_AURA_FLAME_WREATH)) - { - flameWreathActive = true; - break; - } + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; } } - if (flameWreathActive) + + return 1.0f; +} + +// I will not move when Flame Wreath is cast or the raid blows up +float ShadeOfAranFlameWreathDisableMovementMultiplier::GetValue(Action* action) +{ + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + if (!aran) + return 1.0f; + + if (IsFlameWreathActive(botAI, bot)) { - if (dynamic_cast(action) || IsChargeAction(action)) + 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)) + return 0.0f; + } + + return 1.0f; +} + +// Try to rid of the jittering when blocking beams +float NetherspiteKeepBlockingBeamMultiplier::GetValue(Action* action) +{ + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + return 1.0f; + + auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot); + + if (bot == redBlocker) + { + if (dynamic_cast(action)) + return 0.0f; + } + + if (bot == blueBlocker) + { + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + if (bot == greenBlocker) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Give tanks 5 seconds to get aggro during phase transitions +float NetherspiteWaitForDpsMultiplier::GetValue(Action* action) +{ + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + return 1.0f; + + const time_t now = std::time(nullptr); + const uint8 dpsWaitSeconds = 5; + + auto it = netherspiteDpsWaitTimer.find(KARAZHAN_MAP_ID); + if (it == netherspiteDpsWaitTimer.end() || (now - it->second) < dpsWaitSeconds) + { + if (!botAI->IsTank(bot)) { + if (dynamic_cast(action) || (dynamic_cast(action) && + !dynamic_cast(action))) return 0.0f; } } - return 1.0f; + return 1.0f; } -float KarazhanNetherspiteBlueAndGreenBeamMultiplier::GetValue(Action* action) +// Disable standard "avoid aoe" strategy, which may interfere with scripted avoidance +float PrinceMalchezaarDisableAvoidAoeMultiplier::GetValue(Action* action) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - if (!boss || !boss->IsAlive()) - { + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + if (!malchezaar) return 1.0f; - } - - if (dynamic_cast(action) || dynamic_cast(action)) - { - return 0.0f; - } - - RaidKarazhanHelpers karazhanHelper(botAI); - auto [redBlocker /*unused*/, greenBlocker, blueBlocker] = karazhanHelper.GetCurrentBeamBlockers(); - bool isBlocker = (bot == greenBlocker || bot == blueBlocker); - if (isBlocker) - { - Unit* bluePortal = bot->FindNearestCreature(NPC_BLUE_PORTAL, 150.0f); - Unit* greenPortal = bot->FindNearestCreature(NPC_GREEN_PORTAL, 150.0f); - bool inBeam = false; - for (Unit* portal : {bluePortal, greenPortal}) - { - if (!portal) - { - continue; - } - float bx = boss->GetPositionX(), by = boss->GetPositionY(); - float px = portal->GetPositionX(), py = portal->GetPositionY(); - float dx = px - bx, dy = py - by; - float length = sqrt(dx*dx + dy*dy); - if (length == 0.0f) - { - continue; - } - dx /= length; dy /= length; - float botdx = bot->GetPositionX() - bx, botdy = bot->GetPositionY() - by; - float t = (botdx * dx + botdy * dy); - float beamX = bx + dx * t, beamY = by + dy * t; - float distToBeam = sqrt(pow(bot->GetPositionX() - beamX, 2) + pow(bot->GetPositionY() - beamY, 2)); - if (distToBeam < 0.3f && t > 0.0f && t < length) - { - inBeam = true; - break; - } - } - if (inBeam) - { - std::vector voidZones = karazhanHelper.GetAllVoidZones(); - bool inVoidZone = false; - for (Unit* vz : voidZones) - { - if (bot->GetExactDist2d(vz) < 4.0f) - { - inVoidZone = true; - break; - } - } - if (!inVoidZone) - { - if (dynamic_cast(action) || IsChargeAction(action)) - { - return 0.0f; - } - } - } - } - - return 1.0f; -} - -float KarazhanNetherspiteRedBeamMultiplier::GetValue(Action* action) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - if (!boss || !boss->IsAlive()) - { - return 1.0f; - } if (dynamic_cast(action)) - { return 0.0f; + + return 1.0f; +} + +// Don't run back into Shadow Nova when Enfeebled +float PrinceMalchezaarEnfeebleKeepDistanceMultiplier::GetValue(Action* action) +{ + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + if (!malchezaar) + return 1.0f; + + if (bot->HasAura(SPELL_ENFEEBLE)) + { + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; } - RaidKarazhanHelpers karazhanHelper(botAI); - auto [redBlocker, greenBlocker /*unused*/, blueBlocker /*unused*/] = karazhanHelper.GetCurrentBeamBlockers(); - static std::map beamMoveTimes; - static std::map lastBeamMoveSideways; - ObjectGuid botGuid = bot->GetGUID(); - Unit* redPortal = bot->FindNearestCreature(NPC_RED_PORTAL, 150.0f); - if (bot == redBlocker && boss && redPortal) - { - Position blockingPos = karazhanHelper.GetPositionOnBeam(boss, redPortal, 18.0f); - float bx = boss->GetPositionX(); - float by = boss->GetPositionY(); - float px = redPortal->GetPositionX(); - float py = redPortal->GetPositionY(); - float dx = px - bx; - float dy = py - by; - float length = sqrt(dx*dx + dy*dy); - if (length != 0.0f) - { - dx /= length; - dy /= length; - float perpDx = -dy; - float perpDy = dx; - Position sidewaysPos(blockingPos.GetPositionX() + perpDx * 3.0f, - blockingPos.GetPositionY() + perpDy * 3.0f, - blockingPos.GetPositionZ()); + return 1.0f; +} - uint32 intervalSecs = 5; - if (beamMoveTimes[botGuid] == 0) - { - beamMoveTimes[botGuid] = time(nullptr); - lastBeamMoveSideways[botGuid] = false; - } - if (time(nullptr) - beamMoveTimes[botGuid] >= intervalSecs) - { - lastBeamMoveSideways[botGuid] = !lastBeamMoveSideways[botGuid]; - beamMoveTimes[botGuid] = time(nullptr); - } - Position targetPos = lastBeamMoveSideways[botGuid] ? sidewaysPos : blockingPos; - float distToTarget = bot->GetExactDist2d(targetPos.GetPositionX(), targetPos.GetPositionY()); - const float positionTolerance = 1.5f; - if (distToTarget < positionTolerance) - { - if (dynamic_cast(action) || IsChargeAction(action)) - { - return 0.0f; - } - } +// Wait until Phase 3 to use Bloodlust/Heroism +float PrinceMalchezaarDelayBloodlustAndHeroismMultiplier::GetValue(Action* action) +{ + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + if (!malchezaar) + return 1.0f; + + if (malchezaar->GetHealthPct() > 30.0f) + { + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Pets tend to run out of bounds and cause skeletons to spawn off the map +// Pets also tend to pull adds from inside of the tower through the floor +// This multiplier DOES NOT impact Hunter and Warlock pets +// Hunter and Warlock pets are addressed in ControlPetAggressionAction +float NightbaneDisablePetsMultiplier::GetValue(Action* action) +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Give the main tank 8 seconds to get aggro during phase transitions +float NightbaneWaitForDpsMultiplier::GetValue(Action* action) +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane || nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z) + return 1.0f; + + const time_t now = std::time(nullptr); + const uint8 dpsWaitSeconds = 8; + + auto it = nightbaneDpsWaitTimer.find(KARAZHAN_MAP_ID); + if (it == nightbaneDpsWaitTimer.end() || (now - it->second) < dpsWaitSeconds) + { + if (!botAI->IsMainTank(bot)) + { + if (dynamic_cast(action) || (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; } } return 1.0f; } -float KarazhanPrinceMalchezaarMultiplier::GetValue(Action* action) +// The "avoid aoe" strategy must be disabled for the main tank +// Otherwise, the main tank will spin Nightbane to avoid Charred Earth and wipe the raid +// It is also disabled for all bots during the flight phase +float NightbaneDisableAvoidAoeMultiplier::GetValue(Action* action) { - Unit* boss = AI_VALUE2(Unit*, "find target", "prince malchezaar"); - if (!boss || !boss->IsAlive()) - { + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) return 1.0f; - } - if (dynamic_cast(action)) + if (nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z || botAI->IsMainTank(bot)) { - return 0.0f; - } - - if (botAI->IsMelee(bot) && bot->HasAura(SPELL_ENFEEBLE) && - !dynamic_cast(action)) - { - return 0.0f; - } - - if (botAI->IsRanged(bot) && bot->HasAura(SPELL_ENFEEBLE) && - (dynamic_cast(action) && - !dynamic_cast(action))) - { - return 0.0f; + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Disable some movement actions that conflict with the strategies +float NightbaneDisableMovementMultiplier::GetValue(Action* action) +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + // Disable CombatFormationMoveAction for all bots except: + // (1) main tank and (2) only during the ground phase, other melee + if (botAI->IsRanged(bot) || + (botAI->IsMelee(bot) && !botAI->IsMainTank(bot) && + nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z)) + { + if (dynamic_cast(action)) + return 0.0f; } return 1.0f; diff --git a/src/strategy/raids/karazhan/RaidKarazhanMultipliers.h b/src/strategy/raids/karazhan/RaidKarazhanMultipliers.h index c8a4ba37..8ad5a1d4 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanMultipliers.h +++ b/src/strategy/raids/karazhan/RaidKarazhanMultipliers.h @@ -3,45 +3,131 @@ #include "Multiplier.h" -class KarazhanAttumenTheHuntsmanMultiplier : public Multiplier +class AttumenTheHuntsmanDisableTankAssistMultiplier : public Multiplier { public: - KarazhanAttumenTheHuntsmanMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "karazhan attumen the huntsman multiplier") {} + AttumenTheHuntsmanDisableTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "attumen the huntsman disable tank assist multiplier") {} virtual float GetValue(Action* action); }; -class KarazhanBigBadWolfMultiplier : public Multiplier +class AttumenTheHuntsmanStayStackedMultiplier : public Multiplier { public: - KarazhanBigBadWolfMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "karazhan big bad wolf multiplier") {} + AttumenTheHuntsmanStayStackedMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "attumen the huntsman stay stacked multiplier") {} virtual float GetValue(Action* action); }; -class KarazhanShadeOfAranMultiplier : public Multiplier +class AttumenTheHuntsmanWaitForDpsMultiplier : public Multiplier { public: - KarazhanShadeOfAranMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "karazhan shade of aran multiplier") {} + AttumenTheHuntsmanWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "attumen the huntsman wait for dps multiplier") {} virtual float GetValue(Action* action); }; -class KarazhanNetherspiteBlueAndGreenBeamMultiplier : public Multiplier +class TheCuratorDisableTankAssistMultiplier : public Multiplier { public: - KarazhanNetherspiteBlueAndGreenBeamMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "karazhan netherspite blue and green beam multiplier") {} + TheCuratorDisableTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "the curator disable tank assist multiplier") {} virtual float GetValue(Action* action); }; -class KarazhanNetherspiteRedBeamMultiplier : public Multiplier +class TheCuratorDelayBloodlustAndHeroismMultiplier : public Multiplier { public: - KarazhanNetherspiteRedBeamMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "karazhan netherspite red beam multiplier") {} + TheCuratorDelayBloodlustAndHeroismMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "the curator delay bloodlust and heroism multiplier") {} virtual float GetValue(Action* action); }; -class KarazhanPrinceMalchezaarMultiplier : public Multiplier +class ShadeOfAranArcaneExplosionDisableChargeMultiplier : public Multiplier { public: - KarazhanPrinceMalchezaarMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "karazhan prince malchezaar multiplier") {} + ShadeOfAranArcaneExplosionDisableChargeMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "shade of aran arcane explosion disable charge multiplier") {} + virtual float GetValue(Action* action); +}; + +class ShadeOfAranFlameWreathDisableMovementMultiplier : public Multiplier +{ +public: + ShadeOfAranFlameWreathDisableMovementMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "shade of aran flame wreath disable movement multiplier") {} + virtual float GetValue(Action* action); +}; + +class NetherspiteKeepBlockingBeamMultiplier : public Multiplier +{ +public: + NetherspiteKeepBlockingBeamMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "netherspite keep blocking beam multiplier") {} + virtual float GetValue(Action* action); +}; + +class NetherspiteWaitForDpsMultiplier : public Multiplier +{ +public: + NetherspiteWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "netherspite wait for dps multiplier") {} + virtual float GetValue(Action* action); +}; + +class PrinceMalchezaarDisableAvoidAoeMultiplier : public Multiplier +{ +public: + PrinceMalchezaarDisableAvoidAoeMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "prince malchezaar disable avoid aoe multiplier") {} + virtual float GetValue(Action* action); +}; + +class PrinceMalchezaarEnfeebleKeepDistanceMultiplier : public Multiplier +{ +public: + PrinceMalchezaarEnfeebleKeepDistanceMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "prince malchezaar enfeeble keep distance multiplier") {} + virtual float GetValue(Action* action); +}; + +class PrinceMalchezaarDelayBloodlustAndHeroismMultiplier : public Multiplier +{ +public: + PrinceMalchezaarDelayBloodlustAndHeroismMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "prince malchezaar delay bloodlust and heroism multiplier") {} + virtual float GetValue(Action* action); +}; + +class NightbaneDisablePetsMultiplier : public Multiplier +{ +public: + NightbaneDisablePetsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "nightbane disable pets multiplier") {} + virtual float GetValue(Action* action); +}; + +class NightbaneWaitForDpsMultiplier : public Multiplier +{ +public: + NightbaneWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "nightbane wait for dps multiplier") {} + virtual float GetValue(Action* action); +}; + +class NightbaneDisableAvoidAoeMultiplier : public Multiplier +{ +public: + NightbaneDisableAvoidAoeMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "nightbane disable avoid aoe multiplier") {} + virtual float GetValue(Action* action); +}; + +class NightbaneDisableMovementMultiplier : public Multiplier +{ +public: + NightbaneDisableMovementMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "nightbane disable movement multiplier") {} virtual float GetValue(Action* action); }; diff --git a/src/strategy/raids/karazhan/RaidKarazhanStrategy.cpp b/src/strategy/raids/karazhan/RaidKarazhanStrategy.cpp index f93c923c..041651af 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanStrategy.cpp +++ b/src/strategy/raids/karazhan/RaidKarazhanStrategy.cpp @@ -3,79 +3,160 @@ void RaidKarazhanStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode( - "karazhan attumen the huntsman", NextAction::array(0, - new NextAction("karazhan attumen the huntsman stack behind", ACTION_RAID + 1), - nullptr))); + // Trash + triggers.push_back(new TriggerNode("mana warp is about to explode", + NextAction::array(0, new NextAction("mana warp stun creature before warp breach", ACTION_EMERGENCY + 6), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan moroes", NextAction::array(0, - new NextAction("karazhan moroes mark target", ACTION_RAID + 1), - nullptr))); + // Attumen the Huntsman + triggers.push_back(new TriggerNode("attumen the huntsman need target priority", + NextAction::array(0, new NextAction("attumen the huntsman mark target", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("attumen the huntsman attumen spawned", + NextAction::array(0, new NextAction("attumen the huntsman split bosses", ACTION_RAID + 2), nullptr) + )); + triggers.push_back(new TriggerNode("attumen the huntsman attumen is mounted", + NextAction::array(0, new NextAction("attumen the huntsman stack behind", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("attumen the huntsman boss wipes aggro when mounting", + NextAction::array(0, new NextAction("attumen the huntsman manage dps timer", ACTION_RAID + 2), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan maiden of virtue", NextAction::array(0, - new NextAction("karazhan maiden of virtue position ranged", ACTION_RAID + 1), - new NextAction("karazhan maiden of virtue position boss", ACTION_RAID + 1), - nullptr))); + // Moroes + triggers.push_back(new TriggerNode("moroes boss engaged by main tank", + NextAction::array(0, new NextAction("moroes main tank attack boss", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("moroes need target priority", + NextAction::array(0, new NextAction("moroes mark target", ACTION_RAID + 1), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan big bad wolf", NextAction::array(0, - new NextAction("karazhan big bad wolf run away", ACTION_EMERGENCY + 6), - new NextAction("karazhan big bad wolf position boss", ACTION_RAID + 1), - nullptr))); + // Maiden of Virtue + triggers.push_back(new TriggerNode("maiden of virtue healers are stunned by repentance", + NextAction::array(0, new NextAction("maiden of virtue move boss to healer", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("maiden of virtue holy wrath deals chain damage", + NextAction::array(0, new NextAction("maiden of virtue position ranged", ACTION_RAID + 1), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan romulo and julianne", NextAction::array(0, - new NextAction("karazhan romulo and julianne mark target", ACTION_RAID + 1), - nullptr))); + // The Big Bad Wolf + triggers.push_back(new TriggerNode("big bad wolf boss is chasing little red riding hood", + NextAction::array(0, new NextAction("big bad wolf run away from boss", ACTION_EMERGENCY + 6), nullptr) + )); + triggers.push_back(new TriggerNode("big bad wolf boss engaged by tank", + NextAction::array(0, new NextAction("big bad wolf position boss", ACTION_RAID + 1), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan wizard of oz", NextAction::array(0, - new NextAction("karazhan wizard of oz scorch strawman", ACTION_RAID + 2), - new NextAction("karazhan wizard of oz mark target", ACTION_RAID + 1), - nullptr))); + // Romulo and Julianne + triggers.push_back(new TriggerNode("romulo and julianne both bosses revived", + NextAction::array(0, new NextAction("romulo and julianne mark target", ACTION_RAID + 1), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan the curator", NextAction::array(0, - new NextAction("karazhan the curator spread ranged", ACTION_RAID + 2), - new NextAction("karazhan the curator position boss", ACTION_RAID + 2), - new NextAction("karazhan the curator mark target", ACTION_RAID + 1), - nullptr))); + // The Wizard of Oz + triggers.push_back(new TriggerNode("wizard of oz need target priority", + NextAction::array(0, new NextAction("wizard of oz mark target", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("wizard of oz strawman is vulnerable to fire", + NextAction::array(0, new NextAction("wizard of oz scorch strawman", ACTION_RAID + 2), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan terestian illhoof", NextAction::array(0, - new NextAction("karazhan terestian illhoof mark target", ACTION_RAID + 1), - nullptr))); + // The Curator + triggers.push_back(new TriggerNode("the curator astral flare spawned", + NextAction::array(0, new NextAction("the curator mark astral flare", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("the curator boss engaged by tanks", + NextAction::array(0, new NextAction("the curator position boss", ACTION_RAID + 2), nullptr) + )); + triggers.push_back(new TriggerNode("the curator astral flares cast arcing sear", + NextAction::array(0, new NextAction("the curator spread ranged", ACTION_RAID + 2), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan shade of aran", NextAction::array(0, - new NextAction("karazhan shade of aran flame wreath stop movement", ACTION_EMERGENCY + 7), - new NextAction("karazhan shade of aran arcane explosion run away", ACTION_EMERGENCY + 6), - new NextAction("karazhan shade of aran spread ranged", ACTION_RAID + 2), - new NextAction("karazhan shade of aran mark conjured elemental", ACTION_RAID + 1), - nullptr))); + // Terestian Illhoof + triggers.push_back(new TriggerNode("terestian illhoof need target priority", + NextAction::array(0, new NextAction("terestian illhoof mark target", ACTION_RAID + 1), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan netherspite", NextAction::array(0, - new NextAction("karazhan netherspite block red beam", ACTION_EMERGENCY + 8), - new NextAction("karazhan netherspite block blue beam", ACTION_EMERGENCY + 8), - new NextAction("karazhan netherspite block green beam", ACTION_EMERGENCY + 8), - new NextAction("karazhan netherspite avoid beam and void zone", ACTION_EMERGENCY + 7), - new NextAction("karazhan netherspite banish phase avoid void zone", ACTION_RAID + 1), - nullptr))); + // Shade of Aran + triggers.push_back(new TriggerNode("shade of aran arcane explosion is casting", + NextAction::array(0, new NextAction("shade of aran run away from arcane explosion", ACTION_EMERGENCY + 6), nullptr) + )); + triggers.push_back(new TriggerNode("shade of aran flame wreath is active", + NextAction::array(0, new NextAction("shade of aran stop moving during flame wreath", ACTION_EMERGENCY + 7), nullptr) + )); + triggers.push_back(new TriggerNode("shade of aran conjured elementals summoned", + NextAction::array(0, new NextAction("shade of aran mark conjured elemental", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("shade of aran boss uses counterspell and blizzard", + NextAction::array(0, new NextAction("shade of aran ranged maintain distance", ACTION_RAID + 2), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan prince malchezaar", NextAction::array(0, - new NextAction("karazhan prince malchezaar non tank avoid hazard", ACTION_EMERGENCY + 6), - new NextAction("karazhan prince malchezaar tank avoid hazard", ACTION_EMERGENCY + 6), - nullptr))); + // Netherspite + triggers.push_back(new TriggerNode("netherspite red beam is active", + NextAction::array(0, new NextAction("netherspite block red beam", ACTION_EMERGENCY + 8), nullptr) + )); + triggers.push_back(new TriggerNode("netherspite blue beam is active", + NextAction::array(0, new NextAction("netherspite block blue beam", ACTION_EMERGENCY + 8), nullptr) + )); + triggers.push_back(new TriggerNode("netherspite green beam is active", + NextAction::array(0, new NextAction("netherspite block green beam", ACTION_EMERGENCY + 8), nullptr) + )); + triggers.push_back(new TriggerNode("netherspite bot is not beam blocker", + NextAction::array(0, new NextAction("netherspite avoid beam and void zone", ACTION_EMERGENCY + 7), nullptr) + )); + triggers.push_back(new TriggerNode("netherspite boss is banished", + NextAction::array(0, new NextAction("netherspite banish phase avoid void zone", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("netherspite need to manage timers and trackers", + NextAction::array(0, new NextAction("netherspite manage timers and trackers", ACTION_EMERGENCY + 10), nullptr) + )); + + // Prince Malchezaar + triggers.push_back(new TriggerNode("prince malchezaar bot is enfeebled", + NextAction::array(0, new NextAction("prince malchezaar enfeebled avoid hazard", ACTION_EMERGENCY + 6), nullptr) + )); + triggers.push_back(new TriggerNode("prince malchezaar infernals are spawned", + NextAction::array(0, new NextAction("prince malchezaar non tank avoid infernal", ACTION_EMERGENCY + 1), nullptr) + )); + triggers.push_back(new TriggerNode("prince malchezaar boss engaged by main tank", + NextAction::array(0, new NextAction("prince malchezaar main tank movement", ACTION_EMERGENCY + 6), nullptr) + )); + + // Nightbane + triggers.push_back(new TriggerNode("nightbane boss engaged by main tank", + NextAction::array(0, new NextAction("nightbane ground phase position boss", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("nightbane ranged bots are in charred earth", + NextAction::array(0, new NextAction("nightbane ground phase rotate ranged positions", ACTION_EMERGENCY + 1), nullptr) + )); + triggers.push_back(new TriggerNode("nightbane main tank is susceptible to fear", + NextAction::array(0, new NextAction("nightbane cast fear ward on main tank", ACTION_RAID + 2), nullptr) + )); + triggers.push_back(new TriggerNode("nightbane pets ignore collision to chase flying boss", + NextAction::array(0, new NextAction("nightbane control pet aggression", ACTION_RAID + 2), nullptr) + )); + triggers.push_back(new TriggerNode("nightbane boss is flying", + NextAction::array(0, new NextAction("nightbane flight phase movement", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("nightbane need to manage timers and trackers", + NextAction::array(0, new NextAction("nightbane manage timers and trackers", ACTION_EMERGENCY + 10), nullptr) + )); } void RaidKarazhanStrategy::InitMultipliers(std::vector& multipliers) { - multipliers.push_back(new KarazhanShadeOfAranMultiplier(botAI)); - multipliers.push_back(new KarazhanNetherspiteBlueAndGreenBeamMultiplier(botAI)); - multipliers.push_back(new KarazhanNetherspiteRedBeamMultiplier(botAI)); - multipliers.push_back(new KarazhanPrinceMalchezaarMultiplier(botAI)); + multipliers.push_back(new AttumenTheHuntsmanDisableTankAssistMultiplier(botAI)); + multipliers.push_back(new AttumenTheHuntsmanStayStackedMultiplier(botAI)); + multipliers.push_back(new AttumenTheHuntsmanWaitForDpsMultiplier(botAI)); + multipliers.push_back(new TheCuratorDisableTankAssistMultiplier(botAI)); + multipliers.push_back(new TheCuratorDelayBloodlustAndHeroismMultiplier(botAI)); + multipliers.push_back(new ShadeOfAranArcaneExplosionDisableChargeMultiplier(botAI)); + multipliers.push_back(new ShadeOfAranFlameWreathDisableMovementMultiplier(botAI)); + multipliers.push_back(new NetherspiteKeepBlockingBeamMultiplier(botAI)); + multipliers.push_back(new NetherspiteWaitForDpsMultiplier(botAI)); + multipliers.push_back(new PrinceMalchezaarDisableAvoidAoeMultiplier(botAI)); + multipliers.push_back(new PrinceMalchezaarEnfeebleKeepDistanceMultiplier(botAI)); + multipliers.push_back(new PrinceMalchezaarDelayBloodlustAndHeroismMultiplier(botAI)); + multipliers.push_back(new NightbaneDisablePetsMultiplier(botAI)); + multipliers.push_back(new NightbaneWaitForDpsMultiplier(botAI)); + multipliers.push_back(new NightbaneDisableAvoidAoeMultiplier(botAI)); + multipliers.push_back(new NightbaneDisableMovementMultiplier(botAI)); } diff --git a/src/strategy/raids/karazhan/RaidKarazhanStrategy.h b/src/strategy/raids/karazhan/RaidKarazhanStrategy.h index c03e4285..7d6b16de 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanStrategy.h +++ b/src/strategy/raids/karazhan/RaidKarazhanStrategy.h @@ -7,7 +7,7 @@ class RaidKarazhanStrategy : public Strategy { public: - RaidKarazhanStrategy(PlayerbotAI* ai) : Strategy(ai) {} + RaidKarazhanStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} std::string const getName() override { return "karazhan"; } diff --git a/src/strategy/raids/karazhan/RaidKarazhanTriggerContext.h b/src/strategy/raids/karazhan/RaidKarazhanTriggerContext.h index 0b6a29e8..fa2b7d37 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanTriggerContext.h +++ b/src/strategy/raids/karazhan/RaidKarazhanTriggerContext.h @@ -9,31 +9,255 @@ class RaidKarazhanTriggerContext : public NamedObjectContext public: RaidKarazhanTriggerContext() { - creators["karazhan attumen the huntsman"] = &RaidKarazhanTriggerContext::karazhan_attumen_the_huntsman; - creators["karazhan moroes"] = &RaidKarazhanTriggerContext::karazhan_moroes; - creators["karazhan maiden of virtue"] = &RaidKarazhanTriggerContext::karazhan_maiden_of_virtue; - creators["karazhan big bad wolf"] = &RaidKarazhanTriggerContext::karazhan_big_bad_wolf; - creators["karazhan romulo and julianne"] = &RaidKarazhanTriggerContext::karazhan_romulo_and_julianne; - creators["karazhan wizard of oz"] = &RaidKarazhanTriggerContext::karazhan_wizard_of_oz; - creators["karazhan the curator"] = &RaidKarazhanTriggerContext::karazhan_the_curator; - creators["karazhan terestian illhoof"] = &RaidKarazhanTriggerContext::karazhan_terestian_illhoof; - creators["karazhan shade of aran"] = &RaidKarazhanTriggerContext::karazhan_shade_of_aran; - creators["karazhan netherspite"] = &RaidKarazhanTriggerContext::karazhan_netherspite; - creators["karazhan prince malchezaar"] = &RaidKarazhanTriggerContext::karazhan_prince_malchezaar; + // Trash + creators["mana warp is about to explode"] = + &RaidKarazhanTriggerContext::mana_warp_is_about_to_explode; + + // Attumen the Huntsman + creators["attumen the huntsman need target priority"] = + &RaidKarazhanTriggerContext::attumen_the_huntsman_need_target_priority; + + creators["attumen the huntsman attumen spawned"] = + &RaidKarazhanTriggerContext::attumen_the_huntsman_attumen_spawned; + + creators["attumen the huntsman attumen is mounted"] = + &RaidKarazhanTriggerContext::attumen_the_huntsman_attumen_is_mounted; + + creators["attumen the huntsman boss wipes aggro when mounting"] = + &RaidKarazhanTriggerContext::attumen_the_huntsman_boss_wipes_aggro_when_mounting; + + // Moroes + creators["moroes boss engaged by main tank"] = + &RaidKarazhanTriggerContext::moroes_boss_engaged_by_main_tank; + + creators["moroes need target priority"] = + &RaidKarazhanTriggerContext::moroes_need_target_priority; + + // Maiden of Virtue + creators["maiden of virtue healers are stunned by repentance"] = + &RaidKarazhanTriggerContext::maiden_of_virtue_healers_are_stunned_by_repentance; + + creators["maiden of virtue holy wrath deals chain damage"] = + &RaidKarazhanTriggerContext::maiden_of_virtue_holy_wrath_deals_chain_damage; + + // The Big Bad Wolf + creators["big bad wolf boss engaged by tank"] = + &RaidKarazhanTriggerContext::big_bad_wolf_boss_engaged_by_tank; + + creators["big bad wolf boss is chasing little red riding hood"] = + &RaidKarazhanTriggerContext::big_bad_wolf_boss_is_chasing_little_red_riding_hood; + + // Romulo and Julianne + creators["romulo and julianne both bosses revived"] = + &RaidKarazhanTriggerContext::romulo_and_julianne_both_bosses_revived; + + // The Wizard of Oz + creators["wizard of oz need target priority"] = + &RaidKarazhanTriggerContext::wizard_of_oz_need_target_priority; + + creators["wizard of oz strawman is vulnerable to fire"] = + &RaidKarazhanTriggerContext::wizard_of_oz_strawman_is_vulnerable_to_fire; + + // The Curator + creators["the curator astral flare spawned"] = + &RaidKarazhanTriggerContext::the_curator_astral_flare_spawned; + + creators["the curator boss engaged by tanks"] = + &RaidKarazhanTriggerContext::the_curator_boss_engaged_by_tanks; + + creators["the curator astral flares cast arcing sear"] = + &RaidKarazhanTriggerContext::the_curator_astral_flares_cast_arcing_sear; + + // Terestian Illhoof + creators["terestian illhoof need target priority"] = + &RaidKarazhanTriggerContext::terestian_illhoof_need_target_priority; + + // Shade of Aran + creators["shade of aran arcane explosion is casting"] = + &RaidKarazhanTriggerContext::shade_of_aran_arcane_explosion_is_casting; + + creators["shade of aran flame wreath is active"] = + &RaidKarazhanTriggerContext::shade_of_aran_flame_wreath_is_active; + + creators["shade of aran conjured elementals summoned"] = + &RaidKarazhanTriggerContext::shade_of_aran_conjured_elementals_summoned; + + creators["shade of aran boss uses counterspell and blizzard"] = + &RaidKarazhanTriggerContext::shade_of_aran_boss_uses_counterspell_and_blizzard; + + // Netherspite + creators["netherspite red beam is active"] = + &RaidKarazhanTriggerContext::netherspite_red_beam_is_active; + + creators["netherspite blue beam is active"] = + &RaidKarazhanTriggerContext::netherspite_blue_beam_is_active; + + creators["netherspite green beam is active"] = + &RaidKarazhanTriggerContext::netherspite_green_beam_is_active; + + creators["netherspite bot is not beam blocker"] = + &RaidKarazhanTriggerContext::netherspite_bot_is_not_beam_blocker; + + creators["netherspite boss is banished"] = + &RaidKarazhanTriggerContext::netherspite_boss_is_banished; + + creators["netherspite need to manage timers and trackers"] = + &RaidKarazhanTriggerContext::netherspite_need_to_manage_timers_and_trackers; + + // Prince Malchezaar + creators["prince malchezaar bot is enfeebled"] = + &RaidKarazhanTriggerContext::prince_malchezaar_bot_is_enfeebled; + + creators["prince malchezaar infernals are spawned"] = + &RaidKarazhanTriggerContext::prince_malchezaar_infernals_are_spawned; + + creators["prince malchezaar boss engaged by main tank"] = + &RaidKarazhanTriggerContext::prince_malchezaar_boss_engaged_by_main_tank; + + // Nightbane + creators["nightbane boss engaged by main tank"] = + &RaidKarazhanTriggerContext::nightbane_boss_engaged_by_main_tank; + + creators["nightbane ranged bots are in charred earth"] = + &RaidKarazhanTriggerContext::nightbane_ranged_bots_are_in_charred_earth; + + creators["nightbane main tank is susceptible to fear"] = + &RaidKarazhanTriggerContext::nightbane_main_tank_is_susceptible_to_fear; + + creators["nightbane pets ignore collision to chase flying boss"] = + &RaidKarazhanTriggerContext::nightbane_pets_ignore_collision_to_chase_flying_boss; + + creators["nightbane boss is flying"] = + &RaidKarazhanTriggerContext::nightbane_boss_is_flying; + + creators["nightbane need to manage timers and trackers"] = + &RaidKarazhanTriggerContext::nightbane_need_to_manage_timers_and_trackers; } private: - static Trigger* karazhan_attumen_the_huntsman(PlayerbotAI* botAI) { return new KarazhanAttumenTheHuntsmanTrigger(botAI); } - static Trigger* karazhan_moroes(PlayerbotAI* botAI) { return new KarazhanMoroesTrigger(botAI); } - static Trigger* karazhan_maiden_of_virtue(PlayerbotAI* botAI) { return new KarazhanMaidenOfVirtueTrigger(botAI); } - static Trigger* karazhan_big_bad_wolf(PlayerbotAI* botAI) { return new KarazhanBigBadWolfTrigger(botAI); } - static Trigger* karazhan_romulo_and_julianne(PlayerbotAI* botAI) { return new KarazhanRomuloAndJulianneTrigger(botAI); } - static Trigger* karazhan_wizard_of_oz(PlayerbotAI* botAI) { return new KarazhanWizardOfOzTrigger(botAI); } - static Trigger* karazhan_the_curator(PlayerbotAI* botAI) { return new KarazhanTheCuratorTrigger(botAI); } - static Trigger* karazhan_terestian_illhoof(PlayerbotAI* botAI) { return new KarazhanTerestianIllhoofTrigger(botAI); } - static Trigger* karazhan_shade_of_aran(PlayerbotAI* botAI) { return new KarazhanShadeOfAranTrigger(botAI); } - static Trigger* karazhan_netherspite(PlayerbotAI* botAI) { return new KarazhanNetherspiteTrigger(botAI); } - static Trigger* karazhan_prince_malchezaar(PlayerbotAI* botAI) { return new KarazhanPrinceMalchezaarTrigger(botAI); } + // Trash + static Trigger* mana_warp_is_about_to_explode( + PlayerbotAI* botAI) { return new ManaWarpIsAboutToExplodeTrigger(botAI); } + + // Attumen the Huntsman + static Trigger* attumen_the_huntsman_need_target_priority( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanNeedTargetPriorityTrigger(botAI); } + + static Trigger* attumen_the_huntsman_attumen_spawned( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanAttumenSpawnedTrigger(botAI); } + + static Trigger* attumen_the_huntsman_attumen_is_mounted( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanAttumenIsMountedTrigger(botAI); } + + static Trigger* attumen_the_huntsman_boss_wipes_aggro_when_mounting( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanBossWipesAggroWhenMountingTrigger(botAI); } + + // Moroes + static Trigger* moroes_boss_engaged_by_main_tank( + PlayerbotAI* botAI) { return new MoroesBossEngagedByMainTankTrigger(botAI); } + + static Trigger* moroes_need_target_priority( + PlayerbotAI* botAI) { return new MoroesNeedTargetPriorityTrigger(botAI); } + + // Maiden of Virtue + static Trigger* maiden_of_virtue_healers_are_stunned_by_repentance( + PlayerbotAI* botAI) { return new MaidenOfVirtueHealersAreStunnedByRepentanceTrigger(botAI); } + + static Trigger* maiden_of_virtue_holy_wrath_deals_chain_damage( + PlayerbotAI* botAI) { return new MaidenOfVirtueHolyWrathDealsChainDamageTrigger(botAI); } + + // The Big Bad Wolf + static Trigger* big_bad_wolf_boss_engaged_by_tank( + PlayerbotAI* botAI) { return new BigBadWolfBossEngagedByTankTrigger(botAI); } + + static Trigger* big_bad_wolf_boss_is_chasing_little_red_riding_hood( + PlayerbotAI* botAI) { return new BigBadWolfBossIsChasingLittleRedRidingHoodTrigger(botAI); } + + // Romulo and Julianne + static Trigger* romulo_and_julianne_both_bosses_revived( + PlayerbotAI* botAI) { return new RomuloAndJulianneBothBossesRevivedTrigger(botAI); } + + // The Wizard of Oz + static Trigger* wizard_of_oz_need_target_priority( + PlayerbotAI* botAI) { return new WizardOfOzNeedTargetPriorityTrigger(botAI); } + + static Trigger* wizard_of_oz_strawman_is_vulnerable_to_fire( + PlayerbotAI* botAI) { return new WizardOfOzStrawmanIsVulnerableToFireTrigger(botAI); } + + // The Curator + static Trigger* the_curator_astral_flare_spawned( + PlayerbotAI* botAI) { return new TheCuratorAstralFlareSpawnedTrigger(botAI); } + + static Trigger* the_curator_boss_engaged_by_tanks( + PlayerbotAI* botAI) { return new TheCuratorBossEngagedByTanksTrigger(botAI); } + + static Trigger* the_curator_astral_flares_cast_arcing_sear( + PlayerbotAI* botAI) { return new TheCuratorBossAstralFlaresCastArcingSearTrigger(botAI); } + + // Terestian Illhoof + static Trigger* terestian_illhoof_need_target_priority( + PlayerbotAI* botAI) { return new TerestianIllhoofNeedTargetPriorityTrigger(botAI); } + + // Shade of Aran + static Trigger* shade_of_aran_arcane_explosion_is_casting( + PlayerbotAI* botAI) { return new ShadeOfAranArcaneExplosionIsCastingTrigger(botAI); } + + static Trigger* shade_of_aran_flame_wreath_is_active( + PlayerbotAI* botAI) { return new ShadeOfAranFlameWreathIsActiveTrigger(botAI); } + + static Trigger* shade_of_aran_conjured_elementals_summoned( + PlayerbotAI* botAI) { return new ShadeOfAranConjuredElementalsSummonedTrigger(botAI); } + + static Trigger* shade_of_aran_boss_uses_counterspell_and_blizzard( + PlayerbotAI* botAI) { return new ShadeOfAranBossUsesCounterspellAndBlizzardTrigger(botAI); } + + // Netherspite + static Trigger* netherspite_red_beam_is_active( + PlayerbotAI* botAI) { return new NetherspiteRedBeamIsActiveTrigger(botAI); } + + static Trigger* netherspite_blue_beam_is_active( + PlayerbotAI* botAI) { return new NetherspiteBlueBeamIsActiveTrigger(botAI); } + + static Trigger* netherspite_green_beam_is_active( + PlayerbotAI* botAI) { return new NetherspiteGreenBeamIsActiveTrigger(botAI); } + + static Trigger* netherspite_bot_is_not_beam_blocker( + PlayerbotAI* botAI) { return new NetherspiteBotIsNotBeamBlockerTrigger(botAI); } + + static Trigger* netherspite_boss_is_banished( + PlayerbotAI* botAI) { return new NetherspiteBossIsBanishedTrigger(botAI); } + + static Trigger* netherspite_need_to_manage_timers_and_trackers( + PlayerbotAI* botAI) { return new NetherspiteNeedToManageTimersAndTrackersTrigger(botAI); } + + // Prince Malchezaar + static Trigger* prince_malchezaar_bot_is_enfeebled( + PlayerbotAI* botAI) { return new PrinceMalchezaarBotIsEnfeebledTrigger(botAI); } + + static Trigger* prince_malchezaar_infernals_are_spawned( + PlayerbotAI* botAI) { return new PrinceMalchezaarInfernalsAreSpawnedTrigger(botAI); } + + static Trigger* prince_malchezaar_boss_engaged_by_main_tank( + PlayerbotAI* botAI) { return new PrinceMalchezaarBossEngagedByMainTankTrigger(botAI); } + + // Nightbane + static Trigger* nightbane_boss_engaged_by_main_tank( + PlayerbotAI* botAI) { return new NightbaneBossEngagedByMainTankTrigger(botAI); } + + static Trigger* nightbane_ranged_bots_are_in_charred_earth( + PlayerbotAI* botAI) { return new NightbaneRangedBotsAreInCharredEarthTrigger(botAI); } + + static Trigger* nightbane_main_tank_is_susceptible_to_fear( + PlayerbotAI* botAI) { return new NightbaneMainTankIsSusceptibleToFearTrigger(botAI); } + + static Trigger* nightbane_pets_ignore_collision_to_chase_flying_boss( + PlayerbotAI* botAI) { return new NightbanePetsIgnoreCollisionToChaseFlyingBossTrigger(botAI); } + + static Trigger* nightbane_boss_is_flying( + PlayerbotAI* botAI) { return new NightbaneBossIsFlyingTrigger(botAI); } + + static Trigger* nightbane_need_to_manage_timers_and_trackers( + PlayerbotAI* botAI) { return new NightbaneNeedToManageTimersAndTrackersTrigger(botAI); } }; -#endif +#endif \ No newline at end of file diff --git a/src/strategy/raids/karazhan/RaidKarazhanTriggers.cpp b/src/strategy/raids/karazhan/RaidKarazhanTriggers.cpp index f70ae70f..e39006f7 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanTriggers.cpp +++ b/src/strategy/raids/karazhan/RaidKarazhanTriggers.cpp @@ -3,17 +3,64 @@ #include "RaidKarazhanActions.h" #include "Playerbots.h" -bool KarazhanAttumenTheHuntsmanTrigger::IsActive() -{ - RaidKarazhanHelpers helpers(botAI); - Unit* boss = helpers.GetFirstAliveUnitByEntry(NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); +using namespace KarazhanHelpers; - return boss && boss->IsAlive(); +bool ManaWarpIsAboutToExplodeTrigger::IsActive() +{ + Unit* manaWarp = AI_VALUE2(Unit*, "find target", "mana warp"); + return manaWarp && manaWarp->GetHealthPct() < 15; } -bool KarazhanMoroesTrigger::IsActive() +bool AttumenTheHuntsmanNeedTargetPriorityTrigger::IsActive() { + if (botAI->IsHeal(bot)) + return false; + + Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"); + return midnight != nullptr; +} + +bool AttumenTheHuntsmanAttumenSpawnedTrigger::IsActive() +{ + if (!botAI->IsAssistTankOfIndex(bot, 0)) + return false; + + Unit* attumen = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN); + return attumen != nullptr; +} + +bool AttumenTheHuntsmanAttumenIsMountedTrigger::IsActive() +{ + if (botAI->IsMainTank(bot)) + return false; + + Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + return attumenMounted && attumenMounted->GetVictim() != bot; +} + +bool AttumenTheHuntsmanBossWipesAggroWhenMountingTrigger::IsActive() +{ + if (!IsMapIDTimerManager(botAI, bot)) + return false; + + Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"); + return midnight != nullptr; +} + +bool MoroesBossEngagedByMainTankTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot)) + return false; + Unit* moroes = AI_VALUE2(Unit*, "find target", "moroes"); + return moroes != nullptr; +} + +bool MoroesNeedTargetPriorityTrigger::IsActive() +{ + if (!botAI->IsDps(bot)) + return false; + Unit* dorothea = AI_VALUE2(Unit*, "find target", "baroness dorothea millstipe"); Unit* catriona = AI_VALUE2(Unit*, "find target", "lady catriona von'indi"); Unit* keira = AI_VALUE2(Unit*, "find target", "lady keira berrybuck"); @@ -21,39 +68,67 @@ bool KarazhanMoroesTrigger::IsActive() Unit* robin = AI_VALUE2(Unit*, "find target", "lord robin daris"); Unit* crispin = AI_VALUE2(Unit*, "find target", "lord crispin ference"); - return ((moroes && moroes->IsAlive()) || - (dorothea && dorothea->IsAlive()) || - (catriona && catriona->IsAlive()) || - (keira && keira->IsAlive()) || - (rafe && rafe->IsAlive()) || - (robin && robin->IsAlive()) || - (crispin && crispin->IsAlive())); + Unit* target = GetFirstAliveUnit({ dorothea, catriona, keira, rafe, robin, crispin }); + return target != nullptr; } -bool KarazhanMaidenOfVirtueTrigger::IsActive() +bool MaidenOfVirtueHealersAreStunnedByRepentanceTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "maiden of virtue"); + if (!botAI->IsTank(bot)) + return false; - return boss && boss->IsAlive(); + Unit* maiden = AI_VALUE2(Unit*, "find target", "maiden of virtue"); + return maiden && maiden->GetVictim() == bot; } -bool KarazhanBigBadWolfTrigger::IsActive() +bool MaidenOfVirtueHolyWrathDealsChainDamageTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "the big bad wolf"); + if (!botAI->IsRanged(bot)) + return false; - return boss && boss->IsAlive(); + Unit* maiden = AI_VALUE2(Unit*, "find target", "maiden of virtue"); + return maiden != nullptr; } -bool KarazhanRomuloAndJulianneTrigger::IsActive() +bool BigBadWolfBossEngagedByTankTrigger::IsActive() { - Unit* julianne = AI_VALUE2(Unit*, "find target", "julianne"); + if (!botAI->IsTank(bot) || bot->HasAura(SPELL_LITTLE_RED_RIDING_HOOD)) + return false; + + Unit* wolf = AI_VALUE2(Unit*, "find target", "the big bad wolf"); + return wolf != nullptr; +} + +bool BigBadWolfBossIsChasingLittleRedRidingHoodTrigger::IsActive() +{ + if (!bot->HasAura(SPELL_LITTLE_RED_RIDING_HOOD)) + return false; + + Unit* wolf = AI_VALUE2(Unit*, "find target", "the big bad wolf"); + return wolf != nullptr; +} + +bool RomuloAndJulianneBothBossesRevivedTrigger::IsActive() +{ + if (!IsMapIDTimerManager(botAI, bot)) + return false; + Unit* romulo = AI_VALUE2(Unit*, "find target", "romulo"); + if (!romulo) + return false; - return julianne && julianne->IsAlive() && romulo && romulo->IsAlive(); + Unit* julianne = AI_VALUE2(Unit*, "find target", "julianne"); + if (!julianne) + return false; + + return true; } -bool KarazhanWizardOfOzTrigger::IsActive() +bool WizardOfOzNeedTargetPriorityTrigger::IsActive() { + if (!IsMapIDTimerManager(botAI, bot)) + return false; + Unit* dorothee = AI_VALUE2(Unit*, "find target", "dorothee"); Unit* tito = AI_VALUE2(Unit*, "find target", "tito"); Unit* roar = AI_VALUE2(Unit*, "find target", "roar"); @@ -61,45 +136,249 @@ bool KarazhanWizardOfOzTrigger::IsActive() Unit* tinhead = AI_VALUE2(Unit*, "find target", "tinhead"); Unit* crone = AI_VALUE2(Unit*, "find target", "the crone"); - return ((dorothee && dorothee->IsAlive()) || - (tito && tito->IsAlive()) || - (roar && roar->IsAlive()) || - (strawman && strawman->IsAlive()) || - (tinhead && tinhead->IsAlive()) || - (crone && crone->IsAlive())); + Unit* target = GetFirstAliveUnit({ dorothee, tito, roar, strawman, tinhead, crone }); + return target != nullptr; } -bool KarazhanTheCuratorTrigger::IsActive() +bool WizardOfOzStrawmanIsVulnerableToFireTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "the curator"); + if (bot->getClass() != CLASS_MAGE) + return false; - return boss && boss->IsAlive(); + Unit* strawman = AI_VALUE2(Unit*, "find target", "strawman"); + return strawman && strawman->IsAlive(); } -bool KarazhanTerestianIllhoofTrigger::IsActive() +bool TheCuratorAstralFlareSpawnedTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "terestian illhoof"); + if (!botAI->IsDps(bot)) + return false; - return boss && boss->IsAlive(); + Unit* flare = AI_VALUE2(Unit*, "find target", "astral flare"); + return flare != nullptr; } -bool KarazhanShadeOfAranTrigger::IsActive() +bool TheCuratorBossEngagedByTanksTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); + if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0)) + return false; - return boss && boss->IsAlive(); + Unit* curator = AI_VALUE2(Unit*, "find target", "the curator"); + return curator != nullptr; } -bool KarazhanNetherspiteTrigger::IsActive() +bool TheCuratorBossAstralFlaresCastArcingSearTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!botAI->IsRanged(bot)) + return false; - return boss && boss->IsAlive(); + Unit* curator = AI_VALUE2(Unit*, "find target", "the curator"); + return curator != nullptr; } -bool KarazhanPrinceMalchezaarTrigger::IsActive() +bool TerestianIllhoofNeedTargetPriorityTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + if (!IsMapIDTimerManager(botAI, bot)) + return false; - return boss && boss->IsAlive(); + Unit* illhoof = AI_VALUE2(Unit*, "find target", "terestian illhoof"); + return illhoof != nullptr; +} + +bool ShadeOfAranArcaneExplosionIsCastingTrigger::IsActive() +{ + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + return aran && aran->HasUnitState(UNIT_STATE_CASTING) && + aran->FindCurrentSpellBySpellId(SPELL_ARCANE_EXPLOSION) && + !IsFlameWreathActive(botAI, bot); +} + +bool ShadeOfAranFlameWreathIsActiveTrigger::IsActive() +{ + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + return aran && IsFlameWreathActive(botAI, bot); +} + +// Exclusion of Banish is so the player may Banish elementals if they wish +bool ShadeOfAranConjuredElementalsSummonedTrigger::IsActive() +{ + if (!IsMapIDTimerManager(botAI, bot)) + return false; + + Unit* elemental = AI_VALUE2(Unit*, "find target", "conjured elemental"); + return elemental && elemental->IsAlive() && + !elemental->HasAura(SPELL_WARLOCK_BANISH); +} + +bool ShadeOfAranBossUsesCounterspellAndBlizzardTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + return aran && !(aran->HasUnitState(UNIT_STATE_CASTING) && + aran->FindCurrentSpellBySpellId(SPELL_ARCANE_EXPLOSION)) && + !IsFlameWreathActive(botAI, bot); +} + +bool NetherspiteRedBeamIsActiveTrigger::IsActive() +{ + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + return false; + + Unit* redPortal = bot->FindNearestCreature(NPC_RED_PORTAL, 150.0f); + return redPortal != nullptr; +} + +bool NetherspiteBlueBeamIsActiveTrigger::IsActive() +{ + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + return false; + + Unit* bluePortal = bot->FindNearestCreature(NPC_BLUE_PORTAL, 150.0f); + return bluePortal != nullptr; +} + +bool NetherspiteGreenBeamIsActiveTrigger::IsActive() +{ + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + return false; + + Unit* greenPortal = bot->FindNearestCreature(NPC_GREEN_PORTAL, 150.0f); + return greenPortal != nullptr; +} + +bool NetherspiteBotIsNotBeamBlockerTrigger::IsActive() +{ + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + return false; + + auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot); + return bot != redBlocker && bot != blueBlocker && bot != greenBlocker; +} + +bool NetherspiteBossIsBanishedTrigger::IsActive() +{ + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || !netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + return false; + + std::vector voidZones = GetAllVoidZones(botAI, bot); + for (Unit* vz : voidZones) + { + if (bot->GetExactDist2d(vz) < 4.0f) + return true; + } + + return false; +} + +bool NetherspiteNeedToManageTimersAndTrackersTrigger::IsActive() +{ + if (!botAI->IsTank(bot) && !IsMapIDTimerManager(botAI, bot)) + return false; + + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + return netherspite != nullptr; +} + +bool PrinceMalchezaarBotIsEnfeebledTrigger::IsActive() +{ + return bot->HasAura(SPELL_ENFEEBLE); +} + +bool PrinceMalchezaarInfernalsAreSpawnedTrigger::IsActive() +{ + if (botAI->IsMainTank(bot) || bot->HasAura(SPELL_ENFEEBLE)) + return false; + + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + return malchezaar != nullptr; +} + +bool PrinceMalchezaarBossEngagedByMainTankTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + return malchezaar != nullptr; +} + +bool NightbaneBossEngagedByMainTankTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + return nightbane && nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z; +} + +bool NightbaneRangedBotsAreInCharredEarthTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + return nightbane && nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z; +} + +bool NightbaneMainTankIsSusceptibleToFearTrigger::IsActive() +{ + if (bot->getClass() != CLASS_PRIEST) + return false; + + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + 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; + } + } + } + + return mainTank && !mainTank->HasAura(SPELL_FEAR_WARD) && + botAI->CanCastSpell("fear ward", mainTank); +} + +bool NightbanePetsIgnoreCollisionToChaseFlyingBossTrigger::IsActive() +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + return false; + + Pet* pet = bot->GetPet(); + return pet && pet->IsAlive(); +} + +bool NightbaneBossIsFlyingTrigger::IsActive() +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane || nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z) + return false; + + const time_t now = std::time(nullptr); + const uint8 flightPhaseDurationSeconds = 35; + + return nightbaneFlightPhaseStartTimer.find(KARAZHAN_MAP_ID) != nightbaneFlightPhaseStartTimer.end() && + (now - nightbaneFlightPhaseStartTimer[KARAZHAN_MAP_ID] < flightPhaseDurationSeconds); +} + +bool NightbaneNeedToManageTimersAndTrackersTrigger::IsActive() +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + return nightbane != nullptr; } diff --git a/src/strategy/raids/karazhan/RaidKarazhanTriggers.h b/src/strategy/raids/karazhan/RaidKarazhanTriggers.h index 69f44152..e3465ef1 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanTriggers.h +++ b/src/strategy/raids/karazhan/RaidKarazhanTriggers.h @@ -3,80 +3,298 @@ #include "Trigger.h" -class KarazhanAttumenTheHuntsmanTrigger : public Trigger +class ManaWarpIsAboutToExplodeTrigger : public Trigger { public: - KarazhanAttumenTheHuntsmanTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan attumen the huntsman") {} + ManaWarpIsAboutToExplodeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "mana warp is about to explode") {} bool IsActive() override; }; -class KarazhanMoroesTrigger : public Trigger +class AttumenTheHuntsmanNeedTargetPriorityTrigger : public Trigger { public: - KarazhanMoroesTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan moroes") {} + AttumenTheHuntsmanNeedTargetPriorityTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "attumen the huntsman need target priority") {} bool IsActive() override; }; -class KarazhanMaidenOfVirtueTrigger : public Trigger +class AttumenTheHuntsmanAttumenSpawnedTrigger : public Trigger { public: - KarazhanMaidenOfVirtueTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan maiden of virtue") {} + AttumenTheHuntsmanAttumenSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "attumen the huntsman attumen spawned") {} bool IsActive() override; }; -class KarazhanBigBadWolfTrigger : public Trigger +class AttumenTheHuntsmanAttumenIsMountedTrigger : public Trigger { public: - KarazhanBigBadWolfTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan big bad wolf") {} + AttumenTheHuntsmanAttumenIsMountedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "attumen the huntsman attumen is mounted") {} bool IsActive() override; }; -class KarazhanRomuloAndJulianneTrigger : public Trigger +class AttumenTheHuntsmanBossWipesAggroWhenMountingTrigger : public Trigger { public: - KarazhanRomuloAndJulianneTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan romulo and julianne") {} + AttumenTheHuntsmanBossWipesAggroWhenMountingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "attumen the huntsman boss wipes aggro when mounting") {} bool IsActive() override; }; -class KarazhanWizardOfOzTrigger : public Trigger +class MoroesBossEngagedByMainTankTrigger : public Trigger { public: - KarazhanWizardOfOzTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan wizard of oz") {} + MoroesBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "moroes boss engaged by main tank") {} bool IsActive() override; }; -class KarazhanTheCuratorTrigger : public Trigger +class MoroesNeedTargetPriorityTrigger : public Trigger { public: - KarazhanTheCuratorTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan the curator") {} + MoroesNeedTargetPriorityTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "moroes need target priority") {} bool IsActive() override; }; -class KarazhanTerestianIllhoofTrigger : public Trigger +class MaidenOfVirtueHealersAreStunnedByRepentanceTrigger : public Trigger { public: - KarazhanTerestianIllhoofTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan terestian illhoof") {} + MaidenOfVirtueHealersAreStunnedByRepentanceTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "maiden of virtue healers are stunned by repentance") {} bool IsActive() override; }; -class KarazhanShadeOfAranTrigger : public Trigger +class MaidenOfVirtueHolyWrathDealsChainDamageTrigger : public Trigger { public: - KarazhanShadeOfAranTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan shade of aran") {} + MaidenOfVirtueHolyWrathDealsChainDamageTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "maiden of virtue holy wrath deals chain damage") {} bool IsActive() override; }; -class KarazhanNetherspiteTrigger : public Trigger +class BigBadWolfBossEngagedByTankTrigger : public Trigger { public: - KarazhanNetherspiteTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan netherspite") {} + BigBadWolfBossEngagedByTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "big bad wolf boss engaged by tank") {} bool IsActive() override; }; -class KarazhanPrinceMalchezaarTrigger : public Trigger +class BigBadWolfBossIsChasingLittleRedRidingHoodTrigger : public Trigger { public: - KarazhanPrinceMalchezaarTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan prince malchezaar") {} + BigBadWolfBossIsChasingLittleRedRidingHoodTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "big bad wolf boss is chasing little red riding hood") {} + bool IsActive() override; +}; + +class RomuloAndJulianneBothBossesRevivedTrigger : public Trigger +{ +public: + RomuloAndJulianneBothBossesRevivedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "romulo and julianne both bosses revived") {} + bool IsActive() override; +}; + +class WizardOfOzNeedTargetPriorityTrigger : public Trigger +{ +public: + WizardOfOzNeedTargetPriorityTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "wizard of oz need target priority") {} + bool IsActive() override; +}; + +class WizardOfOzStrawmanIsVulnerableToFireTrigger : public Trigger +{ +public: + WizardOfOzStrawmanIsVulnerableToFireTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "wizard of oz strawman is vulnerable to fire") {} + bool IsActive() override; +}; +class TheCuratorAstralFlareSpawnedTrigger : public Trigger +{ +public: + TheCuratorAstralFlareSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "the curator astral flare spawned") {} + bool IsActive() override; +}; + +class TheCuratorBossEngagedByTanksTrigger : public Trigger +{ +public: + TheCuratorBossEngagedByTanksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "the curator boss engaged by tanks") {} + bool IsActive() override; +}; + +class TheCuratorBossAstralFlaresCastArcingSearTrigger : public Trigger +{ +public: + TheCuratorBossAstralFlaresCastArcingSearTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "the curator astral flares cast arcing sear") {} + bool IsActive() override; +}; + +class TerestianIllhoofNeedTargetPriorityTrigger : public Trigger +{ +public: + TerestianIllhoofNeedTargetPriorityTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "terestian illhoof need target priority") {} + bool IsActive() override; +}; + +class ShadeOfAranArcaneExplosionIsCastingTrigger : public Trigger +{ +public: + ShadeOfAranArcaneExplosionIsCastingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "shade of aran arcane explosion is casting") {} + bool IsActive() override; +}; + +class ShadeOfAranFlameWreathIsActiveTrigger : public Trigger +{ +public: + ShadeOfAranFlameWreathIsActiveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "shade of aran flame wreath is active") {} + bool IsActive() override; +}; + +class ShadeOfAranConjuredElementalsSummonedTrigger : public Trigger +{ +public: + ShadeOfAranConjuredElementalsSummonedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "shade of aran conjured elementals summoned") {} + bool IsActive() override; +}; + +class ShadeOfAranBossUsesCounterspellAndBlizzardTrigger : public Trigger +{ +public: + ShadeOfAranBossUsesCounterspellAndBlizzardTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "shade of aran boss uses counterspell and blizzard") {} + bool IsActive() override; +}; + +class NetherspiteRedBeamIsActiveTrigger : public Trigger +{ +public: + NetherspiteRedBeamIsActiveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "netherspite red beam is active") {} + bool IsActive() override; +}; + +class NetherspiteBlueBeamIsActiveTrigger : public Trigger +{ +public: + NetherspiteBlueBeamIsActiveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "netherspite blue beam is active") {} + bool IsActive() override; +}; + +class NetherspiteGreenBeamIsActiveTrigger : public Trigger +{ +public: + NetherspiteGreenBeamIsActiveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "netherspite green beam is active") {} + bool IsActive() override; +}; + +class NetherspiteBotIsNotBeamBlockerTrigger : public Trigger +{ +public: + NetherspiteBotIsNotBeamBlockerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "netherspite bot is not beam blocker") {} + bool IsActive() override; +}; + +class NetherspiteBossIsBanishedTrigger : public Trigger +{ +public: + NetherspiteBossIsBanishedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "netherspite boss is banished") {} + bool IsActive() override; +}; + +class NetherspiteNeedToManageTimersAndTrackersTrigger : public Trigger +{ +public: + NetherspiteNeedToManageTimersAndTrackersTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "netherspite need to manage timers and trackers") {} + bool IsActive() override; +}; + +class PrinceMalchezaarBotIsEnfeebledTrigger : public Trigger +{ +public: + PrinceMalchezaarBotIsEnfeebledTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "prince malchezaar bot is enfeebled") {} + bool IsActive() override; +}; + +class PrinceMalchezaarInfernalsAreSpawnedTrigger : public Trigger +{ +public: + PrinceMalchezaarInfernalsAreSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "prince malchezaar infernals are spawned") {} + bool IsActive() override; +}; + +class PrinceMalchezaarBossEngagedByMainTankTrigger : public Trigger +{ +public: + PrinceMalchezaarBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "prince malchezaar boss engaged by main tank") {} + bool IsActive() override; +}; + +class NightbaneBossEngagedByMainTankTrigger : public Trigger +{ +public: + NightbaneBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nightbane boss engaged by main tank") {} + bool IsActive() override; +}; + +class NightbaneRangedBotsAreInCharredEarthTrigger : public Trigger +{ +public: + NightbaneRangedBotsAreInCharredEarthTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nightbane ranged bots are in charred earth") {} + bool IsActive() override; +}; + +class NightbaneMainTankIsSusceptibleToFearTrigger : public Trigger +{ +public: + NightbaneMainTankIsSusceptibleToFearTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nightbane main tank is susceptible to fear") {} + bool IsActive() override; +}; + +class NightbanePetsIgnoreCollisionToChaseFlyingBossTrigger : public Trigger +{ +public: + NightbanePetsIgnoreCollisionToChaseFlyingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nightbane pets ignore collision to chase flying boss") {} + bool IsActive() override; +}; + +class NightbaneBossIsFlyingTrigger : public Trigger +{ +public: + NightbaneBossIsFlyingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nightbane boss is flying") {} + bool IsActive() override; +}; + +class NightbaneNeedToManageTimersAndTrackersTrigger : public Trigger +{ +public: + NightbaneNeedToManageTimersAndTrackersTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nightbane need to manage timers and trackers") {} bool IsActive() override; };