From be2cf436eaacdcb24709b6f3a63cced6b424e32f Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 27 Feb 2026 18:04:10 -0600 Subject: [PATCH] Implement Tempest Keep: The Eye Strategies (#1943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Edit: Descriptions of methods are out of date right now. To be updated. This one comes with the same caveats as SSC about requiring ownership from somebody with C++ knowledge, except I think the matter is even more acute here because these strategies incorporate a novel approach proposed by Timberpoes. By redeclaring the entire bossai class for Kael’thas, it was possible to add new member functions to the class in order to access its private member variables. This allows bots to have visibility into boss mechanics beyond what they could do with ordinary techniques and is similar in approach to what was done by the Naxx strategies, except that this approach does not require any modifications to the core. I used it for only one mechanic, which was to detect Kael’thas’s phase. That was very helpful because the fight is divided into 5 phases, and distinguishing between them with traditional techniques requires lookups of a dozen NPCs and comparisons of their various unit states, react states, and auras; by accessing his bossai, this can all be avoided. However, there is far more potential beyond this if the approach is an acceptable one. On with the (shit)show. ### Trash In a perfect world, there would be many strategies for TK trash, which is easily more difficult than two of the bosses. It’s a real pain to do though because to solve the biggest issues properly, each pack would have to be handled a little differently. So the only thing I’ve included is for Mages to cast polymorph on the Crimson Hand Centurions when they are channeling Arcane Flurry. The purpose is not to actually keep them CC’d but to interrupt their channel. ### Al’ar This fight sucked so much to write a strategy for. The only silver lining is that being the post-nerf version, the boss moves between only 4 platform locations (instead of 6), and movement between them is on a fixed rotation (interrupted by Flame Quills) instead of being random. Thus, a strategy can be consistently replicated, and the fight can be done with only 3 tanks (2 on the platforms for the boss and 1 below for adds). **Phase 1:** I’m going to call the platform that Al’ar lands at after the pull “platform 0” because that reflects the indices in the code. In a clockwise direction, the remaining platforms will be referred to as platforms 1, 2, and 3, respectively. The best way to pull is to first put all ranged, as well as tanks other than your main tank and first assistant tank, on nc +stay below platform 0. Then, go up the ramp to platform 0 with your main tank, first assistant tank, and melee dps following you, then hit Al’ar with any ranged attack or spell to start the fight. - Your main tank will start at platform 0, and your first assistant tank will immediately move to platform 1. When Al’ar moves to platform 1, your main tank will move to platform 2. When Al’ar moves to platform 2, your first assistant tank will move to platform 3. When Al’ar moves to platform 3, your main tank will move back to platform 0. This assures a tank is available to receive Al’ar after every platform movement (every 30 seconds). - Melee DPS will follow Al’ar as it moves between platforms. - Each platform is mapped to a corresponding ground location below it. Ranged DPS and healers will follow Al’ar by moving to the corresponding ground location as it flies between platforms. - After each platform move, an Ember of Al’ar will spawn. Your second assistant tank will pick up the Ember and move it to the point that is 25 yards away from the ground position corresponding to Al’ar’s platform (on an invisible line between such ground position and the middle of the room). Ranged DPS will then focus down the Ember before switching back to Al’ar (this positioning is so that ranged are not hit by the Ember Blast explosion that happens whenever an Ember dies). - Each time Al’ar leaves a platform, it has a chance to instead fly up high in the middle of the room to perform Flame Quills, which will one-shot anybody on the upper level or ramps. When Al’ar begins the Flame Quills sequence, all bots on the top level will jump off. FYI, Al’ar’s usage of Flame Quills is not entirely random: there is a 20% chance for it to do so after the first platform move, and the chance increases by another 20% after each subsequent platform move that does not trigger Flame Quills (reset after each Flame Quills sequence). - After Flame Quills, Al’ar will randomly land at either platform 0 or 3. To prepare for this, bots will move to assigned positions during the Flame Quills sequence: - Ranged and the second assistant tank will wait in the middle of the room. - Melee DPS will wait at a point that is between the base of each ramp. - The main tank will wait at the base of the ramp to platform 0. - The first assistant tank will wait at the base of the ramp to platform 3. - Once Al’ar lands, the regular Phase 1 strategies resume. - When Al’ar “dies,” it disappears and moves to the center of the room, where it casts Rebirth and returns to full HP. Bots will wait outside of the radius of the Rebirth explosion for Phase 2 to start. Phase 2: - Your main tank will tank Al’ar initially. When Al’ar casts Melt Armor, your first assistant tank will taunt Al’ar and take over. The tank swaps will continue back and forth every time Melt Armor is cast. - Bots will avoid Flame Patches. FWIW, the standard co +avoid aoe strategy does work for Flame Patches, but avoid aoe provides no buffer distance so as you’ve probably noticed, it doesn’t provide for preemptive avoidance. Also, avoid aoe does not consider multiple hazards together so it can be an issue when movement needs to take into account more than one hazard, plus when a strategy requires particular bot movement, it’s better to account for the hazards within that movement strategy instead of relying on separate methods that can create conflicts. - When Al’ar takes to the sky to perform Dive Bomb, bots will spread out (and continue to avoid Flame Patches). After the Dive Bomb, Al’ar does another Rebirth explosion. I have tried a million different things to properly detect this full sequence (even accessing the bossAI like I did with Kael’thas) and cannot get it to work properly. Ultimately, all I’ve been able to get to work at all with respect to the final explosion is for bots to detect the 2-second cast of the Rebirth and run out. It is not enough time for bots that are too close when the cast happens so some bots may get hit, but if you have adequate gear, they should survive. - After each Dive Bomb, 2 Embers will spawn. Your second assistant tank will tank one Ember, and either the main tank or first assistant tank, whichever one is not tanking Al’ar at the time, will tank the other Ember. They will both move the Embers away from bots, and ranged DPS will focus both Embers down before switching back to Al’ar. - Because the room is so large, it is possible for bots to get too far away from active combat (particularly if they are thrown across the room by Ember Blast) so there is also a method for them to run back toward the center if they get too far away. ### Void Reaver Ironically, what was often considered the easiest boss in 25-player content in TBC is the only boss with an ability (Arcane Orb) that I do not believe can be avoided by bots, even with access to Void Reaver’s boss script. Therefore, every single Arcane Orb is going to hit its target, so the strategy can only try to limit the damage by spreading ranged bots in two rings around Void Reaver (one for healers and one for ranged DPS, to try to ensure sufficient distribution of healers). The tanks will all fight for aggro (necessary due to Knock Away) and try to keep Void Reaver in the middle of the room. Bots that can wipe aggro or otherwise gain invulnerability are directed to use the applicable abilities as soon as they pick up aggro (e.g., Soulshatter). He’s still easy, but if you have IP nerfs, it’s a little bit of a gear check. ### High Astromancer Solarian No boss was hit harder by nerfs in TBC than Solarian, whose encounter went from a totally unique fight that required arcane resistance to a fight that is kind of just an easier Baron Geddon. IMO, she is the easiest boss in TBC 25-player raids. - Ranged bots stack up at a distance from Solarian; this leaves all bots with plenty of space to run away from other bots when they get Wrath of the Astromancer. - When Solarian vanishes, all bots will stack to AoE down the Solarium Agents that spawn. - When Solarian returns with two Solarium Priests, melee will divide into two groups, with one focused on each Solarium Priest. I think this method is not working correctly right now because when one Priest dies, the bots still on the second Priest are leaving it. I’ll need to decide whether I want to figure it out or just get rid of it because this fight is so easy regardless. - Priest bots will cast Fear Ward on the main tank to block the Psychic Scream during the final tank-and-spank Voidwalker phase, and the main tank will pick up Voidwalker Solarian as soon as she transforms. Note that the bots will not be knocked into the air by Wrath of the Astromancer. The issue is due to the presence of a check for knockbacks in Playerbots that causes bots to ignore knockbacks that would launch them at a velocity beyond a hardcoded value. I’ve increased that velocity limit on my own fork, and it does allow Wrath of the Astromancer (and other knockbacks that otherwise don’t work) to work on bots. But that’s obviously a broader issue and not addressed in this PR, and bots don’t take fall damage in any case. ### Kael’thas Sunstrider So this strategy has 23(!) action methods. But like in retail, this is actually an easy fight once it is learned because it is highly scripted. Unlike in other strategies I’ve done, the bots probably cannot do this fight by themselves unless they are way overgeared. This is because there are a few windows during which bots need to position themselves properly based on dynamic factors. But no RTSC is needed—you just need to have bots follow you to the right locations. Also note that the gear check for this strategy is higher than in retail because you have to get all of the legendary weapons down and looted before the advisors aggro in Phase 3, or it’s going to be an absolute shitshow (with human players, you can deal with there still being a couple of weapons up). For a point of reference, when I was first working on this strategy with damage reduced to 50% and bots pretty close to T4 BiS, I had almost no margin of error (I would usually get the weapons down with barely a second to spare). You will need at least 2 tanks, but 3 is better. Your main tank will need to be able to equip the legendary shield so you must use a Warrior or Paladin. However, it is ideal for the first assistant tank to be a Druid because they can equip the legendary staff. **Phase 1:** Fun fact—when you “kill” the advisors in this phase, they don’t actually die but get an aura applied called “Permanent Feign Death” (nice oxymoron). - _Thaladred_: You’re supposed to kite him, and bots can’t really kite, so the method is a poor man’s method of having the bot move away from him in a straight line when fixated. You want him to die in the far Southern part of the room. If he dies in a bad location, you may as well call a wipe and restart. What will work best for you will depend on your DPS since you don’t want to kill him before he gets to the location you want but also don’t want bots to be trapped up against a wall since they can’t properly kite him. The way that works best for me is to have bots stay back while I aggro the boss, and wait until right before Thaladred switches to his second fixate target before attacking. Note that if you do put bots on stay, when you put them back on follow, the bot that is then being fixated will remain on stay (because they need to disregard movement orders other than running away from Thaladred). So after Thaladred dies, make sure to manually type /follow or the bot that was fixated when you took the bots off of stay will not rejoin the fight. - _Sanguinar_: He will be tanked by your main tank, who will be targeted by your Priests for Fear Ward. Bots will wait to engage him; I made it a very generous time (12 seconds) because there is absolutely no rush in Phase 1. There’s no sense in being aggressive. During that time, the main tank will drag Sanguinar to the West wall. - _Capernian_: This is the first make-or-break part of the fight. Phase 1 Capernian was the most frequent cause for wipes for me. - She should be tanked by a Warlock. If you want to pick your Warlock tank, you can do so by the assistant flag, but if you don’t, the strategy will just pick your highest HP Warlock. If you raid without a Warlock, then you’re insane, but at least there’s a guard so your server won’t crash? - You do not need to add the tank strategy to your Warlock. There is a method that will automatically switch your selected tank Warlock between DPS and tank strategies at appropriate times because you need to squeeze out every drop of DPS you can get, particularly for Phase 2, where you’ll need your Warlock to be blowing up weapons with Seed of Corruption instead of spamming Searing Pain. You’ll want your Warlock to start with a DPS strategy as usual (since they should be DPSing Thaladred). - To engage Capernian, start running East right before Sanguinar dies. She will activate quickly, and you want to try to get in front of her (but not too close) before she aggros. - When Capernian aggros, your Warlock tank will immediately switch to the tank strategy and attack. Your main tank will run toward Capernian but not actually attack; their purpose will be to bait her Conflagration to reduce the chance that it hits your Warlock tank. Other melee will not engage Capernian. Ranged DPS will be idle for 12 seconds; during this time, you should run South to make sure they are not in range of Capernian. After 12 seconds, your ranged DPS will activate, move into range and spread out, and attack (it doesn’t seem possible to outrange Conflagrate, so if bots don’t spread, she will annihilate the entire ranged group with a single cast). Ideally, you kill her not too far from her starting position. If she ends up in the middle of the room, you should probably wipe and start over. - _Telonicus_: He is very easy in retail but actually is a big risk for wipes with respect to bots because his bombs will one-shot any non-tank, and bots will stupidly stand in front of him without a proper strategy. You should keep some distance from him before he aggros. Your first assistant tank will pick him up and move him to the West wall near Sanguinar. Again, there is a 12-second delay before DPS starts. Your melee DPS are coded to stay directly behind him and not get too close so they don’t get hit by bombs. **Phase 2:** Kael’thas will summon all weapons immediately after Telonicus is down. Just before Telonicus is down, you should move to the platform where the advisors originally were—you’ll be in better position for the raid to AoE down the weapons. - Your main tank will pick up the axe and move it away from the group. The axe is the biggest threat during this phase and can easily one-shot casters if not pulled away. - One of your Hunters will attempt to get aggro on the bow and move away from the group (as a hacky way of trying to turn the bow away from the group because you can’t really get a bot to do that directly). This method is hit or miss, but it shouldn’t be that big of a deal if your Hunter doesn’t pull it off properly. - Everybody else will prioritize weapons in the following order (but most damage will come from AoE, which is what you want or you will not beat the timer): staff, mace, sword, dagger, axe (ranged only), bow, and shield. - As weapons are defeated, bots will loot and equip them. If you have not disabled bot announcements in your config, you get to see your entire raid go nuts because they looted legendary items. - Here is what weapons bots will loot and equip. I don't know anything about DKs, having never played WotLK, so tell me if anything is wrong for them. - _Healers:_ Mace (if a healer normally uses a staff, it's best if they keep an OH in their bags for this fight) - _Tanks:_ Shield and sword for Paladins and DK, shield and dagger for warriors, staff for Druid - _Offensive_ casters: Staff - _Rogues:_ Sword and dagger if Combat or Subtlety, dagger only if Assassination - _DPS Death Knights, Retribution Paladins, Arms Warriors_: Axe - _Fury Warriors_: Dagger. I understand that due to Titan Grip, they should also have the Axe for best DPS; however, Fury Warriors have awful DPS (we’re talking barely above Prot-level) at this stage. Thus, my view is it is better to give them only the dagger so they will MH it and help break MC in Phase 4, since they will contribute hardly any DPS regardless. - _Cat Druids_: Staff - _Enhancement Shamans_: Dagger - _Hunters:_ Bow and dagger. Note that I do NOT have them loot the sword because they need the dagger in their mainhand to use to break MC in Phase 4; whatever marginal benefit they get from the sword as a stat stick is not worth losing this capability. If your Hunter uses a 2H, it is best to have them carry a 1H in their inventory so they can put something in the OH after they equip the dagger. - After looting weapons, bots with the staff will use it (once) to activate the Mental Protection Field. Hunters will use the bow to generate the legendary arrows and equip those (and will continue to do so during the fight if they use up the arrows). - If you wipe from this point forward, everybody will lose their legendary weapons, and by default, most bots will not automatically reequip their own weapons until a loot event occurs. This was extremely annoying, and therefore there is a noncombat method implemented that causes everybody to equip upgrades when they get within 150 yards of Kael’thas. I considered applying this to the whole instance, but I’m not sure if some people would not like that so I decided to limit things to the Kael’thas encounter. **Phase 3:** I highly recommend you have your Shamans drop Tremor Totems (co +tremor) during this phase. Doing so is not coded because I wanted to leave flexibility, but I think it is very helpful for Sanguinar. After the weapons die, you want to move your bots to a central location between the advisors. If Thaladred died closer to the middle of the room, ideally you position to the side of Thaladred so when he fixates he will not chase bots North into the other advisors. - Shamans will immediately use Heroism/Bloodlust. - Your melee tanks will bring Sanguinar and Telonicus to their tanking positions (same as Phase 1). If your first assistant tank is a Druid, they will be immune to Telonicus’s Remote Toy due to having the legendary staff’s aura activated and will also make your main tank immune. - One healer will stay by the Sanguinar and Telonicus tanking positions to heal the tanks. Once IsHealAssistantOfIndex() is fixed, you will be able to select this healer with the assistant flag. Right now, this will just be the last healer that joined your raid (per standard AC logic). - DPS priority will be Thaladred, Capernian (ranged only), Sanguinar, Telonicus. As with retail, the most chaotic period will be before Thaladred is killed, particularly if he chases bots into other advisors. I don’t have a great solution for this, but Capernian is significantly less dangerous during this phase thanks to the legendary staff. This is the last true breakpoint—if you get Thaladred down with your raid mostly intact, you are very likely to get the kill. **Phase 4:** Kael’thas will aggro immediately after all advisors are dead. - Your main tank will position Kael’thas at his original position. - Bots will move out of Flame Strikes. - Assist tanks will pick up Phoenixes. Since they die over time anyway, bots will not waste time attacking them. When Phoenixes die, they turn into an Egg—at that point, bots will switch to the Egg to destroy it before the Phoenix is reborn. - When Kael’thas puts up Shock Barrier and starts casting Pyroblast on your main tank (a one-shot), all bots will focus DPS on him (even if there is an egg up). You have 4 seconds to break the barrier (80K HP) and interrupt his Pyroblast. It is likely that you will not be able to if you are playing with IP nerfs and are in T4 gear. However, the main tank will use the legendary shield’s ability, which will allow them to absorb one cast, giving you 8 seconds to break the barrier and interrupt Pyroblast. Bots will put top priority on interrupting Pyroblast as soon as the barrier is down. - If a bot (or player) is mind controlled, bots with the legendary dagger (other than tanks) will move to MC’d players and use the following attacks to break MC: Shiv (Rogues), Hamstring (Warriors), Wing Clip (Hunters), and Stormstrike (Shamans). **Phase 5:** At 50% HP, Kael’thas enters a long RP sequence. This is a good time to kill any remaining Phoenixes and/or Eggs. - Kael’thas stops casting Pyroblast and Mind Control. - His main new ability is Gravity Lapse, and it doesn’t work properly on bots... He sucks in the entire raid then knocks everybody back in a different direction. What is supposed to happen is that players will end up floating in midair in different directions and at different heights. However, bots will immediately fall to the ground after getting knocked back. They will not actually hit the ground though and instead remain in a flying state right above the floor. - If you could move in 3D space, Netherbeam would be very easy to deal with. However, because that is not available to bots, they can spread only in 2D space and thus need to move farther to get properly spread, and they waste the first moments falling straight down. As a result, the damage from Netherbeam can be quite high, and the beginning of Gravity Lapse requires a lot of healing. I don’t really have a better way of dealing with this. - FWIW, I don’t think there is any existing method to make bots disperse in 3D anyway. - Kael’thas is supposed to use Nether Void when players are in midair, which creates clouds that reduce your max HP and thus make it more challenging to maneuver, but AC is bugged and he doesn’t use the ability at all (there’s been an open issue about this forever). For fuck's sake, that's all. --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision --- src/Ai/Raid/RaidBossHelpers.cpp | 104 +- src/Ai/Raid/RaidBossHelpers.h | 5 +- src/Ai/Raid/RaidStrategyContext.h | 9 +- .../Action/RaidTempestKeepActions.cpp | 2050 +++++++++++++++++ .../Action/RaidTempestKeepActions.h | 413 ++++ .../Multiplier/RaidTempestKeepMultipliers.cpp | 422 ++++ .../Multiplier/RaidTempestKeepMultipliers.h | 150 ++ .../RaidTempestKeepActionContext.h | 289 +++ .../RaidTempestKeepTriggerContext.h | 265 +++ .../Strategy/RaidTempestKeepStrategy.cpp | 162 ++ .../Strategy/RaidTempestKeepStrategy.h | 18 + .../Trigger/RaidTempestKeepTriggers.cpp | 527 +++++ .../Trigger/RaidTempestKeepTriggers.h | 338 +++ .../Util/RaidTempestKeepHelpers.cpp | 425 ++++ .../TempestKeep/Util/RaidTempestKeepHelpers.h | 158 ++ .../Util/RaidTempestKeepKaelthasBossAI.h | 54 + .../Util/RaidTempestKeepScripts.cpp | 47 + src/Bot/Engine/AiObjectContext.cpp | 12 +- src/Bot/PlayerbotAI.cpp | 239 +- src/Bot/PlayerbotAI.h | 8 +- src/Script/Playerbots.cpp | 3 + 21 files changed, 5544 insertions(+), 154 deletions(-) create mode 100644 src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp create mode 100644 src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h create mode 100644 src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp create mode 100644 src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h create mode 100644 src/Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h create mode 100644 src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h create mode 100644 src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp create mode 100644 src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.h create mode 100644 src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp create mode 100644 src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.h create mode 100644 src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.cpp create mode 100644 src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.h create mode 100644 src/Ai/Raid/TempestKeep/Util/RaidTempestKeepKaelthasBossAI.h create mode 100644 src/Ai/Raid/TempestKeep/Util/RaidTempestKeepScripts.cpp diff --git a/src/Ai/Raid/RaidBossHelpers.cpp b/src/Ai/Raid/RaidBossHelpers.cpp index bcb48294..40dbae5d 100644 --- a/src/Ai/Raid/RaidBossHelpers.cpp +++ b/src/Ai/Raid/RaidBossHelpers.cpp @@ -77,39 +77,101 @@ void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target) // Intended for purposes of storing and erasing timers and trackers in associative containers bool IsMechanicTrackerBot(PlayerbotAI* botAI, Player* bot, uint32 mapId, Player* exclude) { - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || member->GetMapId() != mapId || - !GET_PLAYERBOT_AI(member) || !botAI->IsDps(member)) - continue; + if (!botAI->IsDps(bot) || !bot->IsAlive() || bot->GetMapId() != mapId) + return false; - if (member != exclude) - return member == bot; - } + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->GetMapId() != mapId || member == exclude) + continue; + + PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); + if (!memberAI || !memberAI->IsDps(member)) + continue; + + return member == bot; } return false; } -// Return the first matching alive unit from a cell search of nearby npcs -// More responsive than "find target," but performance cost is much higher -// Re: using the third parameter (false by default), some units are never considered -// to be in combat (e.g., totems) -Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry, bool requireInCombat) +// Requires the main tank to be alive +// Note that IsMainTank() will return the player with the main tank flag, even if dead +Player* GetGroupMainTank(PlayerbotAI* botAI, Player* bot) +{ + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (botAI->IsMainTank(member)) + return member; + } + + return nullptr; +} + +// Returns the alive assist tank of the specified index (0 = first, 1 = second, etc.) +// Priority: Assistants first, then Non-Assistants. +Player* GetGroupAssistTank(PlayerbotAI* botAI, Player* bot, uint8 index) +{ + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + uint8 assistantCount = 0; + std::vector nonAssistantTanks; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (botAI->IsAssistTank(member)) + { + if (group->IsAssistant(member->GetGUID())) + { + if (assistantCount == index) + return member; + + assistantCount++; + } + else + { + nonAssistantTanks.push_back(member); + } + } + } + + // If the index wasn't found among assistants, check the non-assistants that were saved + uint8 nonAssistantIndex = index - assistantCount; + if (nonAssistantIndex < nonAssistantTanks.size()) + return nonAssistantTanks[nonAssistantIndex]; + + return nullptr; +} + +// Return the first matching alive unit from PossibleTargetsValue within sightDistance from config +Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry) { auto const& npcs = - botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get(); for (auto const& npcGuid : npcs) { Unit* unit = botAI->GetUnit(npcGuid); if (unit && unit->IsAlive() && unit->GetEntry() == entry) - { - if (!requireInCombat || unit->IsInCombat()) - return unit; - } + return unit; } return nullptr; diff --git a/src/Ai/Raid/RaidBossHelpers.h b/src/Ai/Raid/RaidBossHelpers.h index 15c60353..31df8c3a 100644 --- a/src/Ai/Raid/RaidBossHelpers.h +++ b/src/Ai/Raid/RaidBossHelpers.h @@ -15,7 +15,10 @@ void MarkTargetWithCross(Player* bot, Unit* target); void MarkTargetWithMoon(Player* bot, Unit* target); void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target); bool IsMechanicTrackerBot(PlayerbotAI* botAI, Player* bot, uint32 mapId, Player* exclude = nullptr); -Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry, bool requireInCombat = false); +Player* GetGroupMainTank(PlayerbotAI* botAI, Player* bot); +Player* GetGroupAssistTank(PlayerbotAI* botAI, Player* bot, uint8 index); +Unit* GetFirstAliveUnitByEntry( + PlayerbotAI* botAI, uint32 entry); Unit* GetNearestPlayerInRadius(Player* bot, float radius); #endif diff --git a/src/Ai/Raid/RaidStrategyContext.h b/src/Ai/Raid/RaidStrategyContext.h index 4a040985..4d87c5ac 100644 --- a/src/Ai/Raid/RaidStrategyContext.h +++ b/src/Ai/Raid/RaidStrategyContext.h @@ -6,9 +6,10 @@ #include "RaidMcStrategy.h" #include "RaidBwlStrategy.h" #include "RaidKarazhanStrategy.h" -#include "RaidMagtheridonStrategy.h" #include "RaidGruulsLairStrategy.h" +#include "RaidMagtheridonStrategy.h" #include "RaidSSCStrategy.h" +#include "RaidTempestKeepStrategy.h" #include "RaidOsStrategy.h" #include "RaidEoEStrategy.h" #include "RaidVoAStrategy.h" @@ -25,9 +26,10 @@ public: creators["moltencore"] = &RaidStrategyContext::moltencore; creators["bwl"] = &RaidStrategyContext::bwl; creators["karazhan"] = &RaidStrategyContext::karazhan; - creators["magtheridon"] = &RaidStrategyContext::magtheridon; creators["gruulslair"] = &RaidStrategyContext::gruulslair; + creators["magtheridon"] = &RaidStrategyContext::magtheridon; creators["ssc"] = &RaidStrategyContext::ssc; + creators["tempestkeep"] = &RaidStrategyContext::tempestkeep; creators["wotlk-os"] = &RaidStrategyContext::wotlk_os; creators["wotlk-eoe"] = &RaidStrategyContext::wotlk_eoe; creators["voa"] = &RaidStrategyContext::voa; @@ -41,9 +43,10 @@ private: static Strategy* moltencore(PlayerbotAI* botAI) { return new RaidMcStrategy(botAI); } static Strategy* bwl(PlayerbotAI* botAI) { return new RaidBwlStrategy(botAI); } static Strategy* karazhan(PlayerbotAI* botAI) { return new RaidKarazhanStrategy(botAI); } - static Strategy* magtheridon(PlayerbotAI* botAI) { return new RaidMagtheridonStrategy(botAI); } static Strategy* gruulslair(PlayerbotAI* botAI) { return new RaidGruulsLairStrategy(botAI); } + static Strategy* magtheridon(PlayerbotAI* botAI) { return new RaidMagtheridonStrategy(botAI); } static Strategy* ssc(PlayerbotAI* botAI) { return new RaidSSCStrategy(botAI); } + static Strategy* tempestkeep(PlayerbotAI* botAI) { return new RaidTempestKeepStrategy(botAI); } static Strategy* wotlk_os(PlayerbotAI* botAI) { return new RaidOsStrategy(botAI); } static Strategy* wotlk_eoe(PlayerbotAI* botAI) { return new RaidEoEStrategy(botAI); } static Strategy* voa(PlayerbotAI* botAI) { return new RaidVoAStrategy(botAI); } diff --git a/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp new file mode 100644 index 00000000..edcc7ad5 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp @@ -0,0 +1,2050 @@ +#include "RaidTempestKeepActions.h" +#include "RaidTempestKeepHelpers.h" +#include "RaidTempestKeepKaelthasBossAI.h" +#include "AiFactory.h" +#include "EquipAction.h" +#include "LootAction.h" +#include "LootObjectStack.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +using namespace TempestKeepHelpers; + +// Trash + +bool CrimsonHandCenturionCastPolymorphAction::Execute(Event /*event*/) +{ + Unit* centurion = AI_VALUE2(Unit*, "find target", "crimson hand centurion"); + if (!centurion) + return false; + + if (centurion->GetHealth() == centurion->GetMaxHealth() && + !botAI->HasAura("polymorph", centurion) && + botAI->CanCastSpell("polymorph", centurion)) + { + return botAI->CastSpell("polymorph", centurion); + } + else if (centurion->HasAura(SPELL_ARCANE_FLURRY)) + { + botAI->Reset(); + return botAI->CastSpell("polymorph", centurion); + } + + return false; +} + +// Al'ar + +bool AlarMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (mainTank && botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", alar)) + return botAI->CastSpell("steady shot", alar); + + return false; +} + +bool AlarBossTanksMoveBetweenPlatformsAction::Execute(Event /*event*/) +{ + if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0, true)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + MarkTargetWithStar(bot, alar); + SetRtiTarget(botAI, "star", alar); + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (botAI->IsMainTank(bot)) + return PositionMainTank(alar, locationIndex); + else + return PositionAssistTank(alar, locationIndex); +} + +bool AlarBossTanksMoveBetweenPlatformsAction::PositionMainTank( + Unit* alar, int8 locationIndex) +{ + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& target = + (locationIndex == PLATFORM_0_IDX || locationIndex == PLATFORM_3_IDX) + ? PLATFORM_POSITIONS[0] : PLATFORM_POSITIONS[2]; + + if (bot->GetExactDist2d(target.GetPositionX(), target.GetPositionY()) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, target.GetPositionX(), target.GetPositionY(), + target.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + else if ((locationIndex == PLATFORM_0_IDX || locationIndex == PLATFORM_2_IDX) && + bot->GetTarget() != alar->GetGUID()) + return Attack(alar); + } + + return false; +} + +bool AlarBossTanksMoveBetweenPlatformsAction::PositionAssistTank( + Unit* alar, int8 locationIndex) +{ + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& target = + (locationIndex == PLATFORM_0_IDX || locationIndex == PLATFORM_1_IDX) + ? PLATFORM_POSITIONS[1] : PLATFORM_POSITIONS[3]; + + if (bot->GetExactDist2d(target.GetPositionX(), target.GetPositionY()) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, target.GetPositionX(), target.GetPositionY(), + target.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + else if ((locationIndex == PLATFORM_1_IDX || locationIndex == PLATFORM_3_IDX) && + bot->GetTarget() != alar->GetGUID()) + return Attack(alar); + } + + return false; +} + +bool AlarMeleeDpsMoveBetweenPlatformsAction::Execute(Event /*event*/) +{ + if (!botAI->IsMelee(bot) || !botAI->IsDps(bot)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + SetRtiTarget(botAI, "star", alar); + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& target = PLATFORM_POSITIONS[locationIndex]; + + if (bot->GetExactDist2d(target.GetPositionX(), target.GetPositionY()) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, target.GetPositionX(), target.GetPositionY(), + target.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + if (bot->GetTarget() != alar->GetGUID()) + return Attack(alar); + } + + return false; +} + +bool AlarRangedAndEmberTankMoveUnderPlatformsAction::Execute(Event /*event*/) +{ + if (!botAI->IsRanged(bot) && !botAI->IsAssistTankOfIndex(bot, 1, true)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& groundTarget = GROUND_POSITIONS[locationIndex]; + + constexpr float distRangedFromTarget = 8.0f; + constexpr float distTankFromTarget = 20.0f; + if (botAI->IsRanged(bot) && bot->GetExactDist2d( + groundTarget.GetPositionX(), groundTarget.GetPositionY()) > distRangedFromTarget) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, groundTarget.GetPositionX(), + groundTarget.GetPositionY(), groundTarget.GetPositionZ(), + distRangedFromTarget, MovementPriority::MOVEMENT_COMBAT); + } + else if (botAI->IsAssistTankOfIndex(bot, 1, true) && + !AI_VALUE2(Unit*, "find target", "ember of al'ar") && bot->GetExactDist2d( + groundTarget.GetPositionX(), groundTarget.GetPositionY()) > distTankFromTarget) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, groundTarget.GetPositionX(), + groundTarget.GetPositionY(), groundTarget.GetPositionZ(), + distTankFromTarget, MovementPriority::MOVEMENT_COMBAT); + } + } + + return false; +} + +bool AlarAssistTanksPickUpEmbersAction::Execute(Event /*event*/) +{ + if (!botAI->IsTank(bot)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (!isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return HandlePhase1Embers(alar); + else + return HandlePhase2Embers(alar); +} + +// Embers will be tanked by only the second assist tank in Phase 1 +bool AlarAssistTanksPickUpEmbersAction::HandlePhase1Embers(Unit* alar) +{ + if (!botAI->IsAssistTankOfIndex(bot, 1, true)) + return false; + + if (Unit* ember = AI_VALUE2(Unit*, "find target", "ember of al'ar")) + { + MarkTargetWithSquare(bot, ember); + SetRtiTarget(botAI, "square", ember); + + if (bot->GetTarget() != ember->GetGUID()) + return Attack(ember); + + if (ember->GetVictim() == bot) + { + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& groundTarget = GROUND_POSITIONS[locationIndex]; + const Position& center = ALAR_POINT_MIDDLE; + float dx = center.GetPositionX() - groundTarget.GetPositionX(); + float dy = center.GetPositionY() - groundTarget.GetPositionY(); + float distToCenter = + groundTarget.GetExactDist2d(center.GetPositionX(), center.GetPositionY()); + + constexpr float moveDist = 25.0f; + float targetX = groundTarget.GetPositionX() + (dx / distToCenter) * moveDist; + float targetY = groundTarget.GetPositionY() + (dy / distToCenter) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, targetX, targetY, groundTarget.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + else + { + constexpr float safeDistance = 16.0f; + if (GetNearestPlayerInRadius(bot, safeDistance)) + return MoveFromGroup(safeDistance); + } + } + else if (!bot->IsWithinMeleeRange(ember)) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, ember->GetPositionX(), ember->GetPositionY(), + ember->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +// One Ember will be tanked by the second assist tank in Phase 2, and the other by +// the main tank or first assist tank (whichever is not tanking Al'ar) +bool AlarAssistTanksPickUpEmbersAction::HandlePhase2Embers(Unit* alar) +{ + auto [firstEmber, secondEmber] = GetFirstTwoEmbersOfAlar(botAI); + + if (botAI->IsAssistTankOfIndex(bot, 1, true) && firstEmber) + { + MarkTargetWithSquare(bot, firstEmber); + SetRtiTarget(botAI, "square", firstEmber); + + if (firstEmber->GetVictim() != bot) + { + if (bot->GetTarget() != firstEmber->GetGUID()) + return Attack(firstEmber); + + return botAI->DoSpecificAction("taunt spell", Event(), true); + } + else if (bot->IsWithinMeleeRange(firstEmber)) + { + constexpr float safeDistance = 16.0f; + if (GetNearestNonTankPlayerInRadius(botAI, bot, safeDistance)) + return MoveFromGroup(safeDistance); + } + } + else if (GetSecondEmberTank(botAI) == bot && secondEmber) + { + MarkTargetWithCircle(bot, secondEmber); + SetRtiTarget(botAI, "circle", secondEmber); + + if (secondEmber->GetVictim() != bot) + { + if (bot->GetTarget() != secondEmber->GetGUID()) + return Attack(secondEmber); + + return botAI->DoSpecificAction("taunt spell", Event(), true); + } + else if (bot->IsWithinMeleeRange(secondEmber)) + { + constexpr float safeDistance = 16.0f; + if (GetNearestNonTankPlayerInRadius(botAI, bot, safeDistance)) + return MoveFromGroup(safeDistance); + } + } + + return false; +} + +bool AlarRangedDpsPrioritizeEmbersAction::Execute(Event /*event*/) +{ + auto [firstEmber, secondEmber] = GetFirstTwoEmbersOfAlar(botAI); + + constexpr float safeDistance = 16.0f; + if (firstEmber) + { + if (bot->GetDistance2d(firstEmber) < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(firstEmber, safeDistance - bot->GetDistance2d(firstEmber)); + } + + SetRtiTarget(botAI, "square", firstEmber); + if (bot->GetTarget() != firstEmber->GetGUID()) + return Attack(firstEmber); + } + else if (secondEmber) + { + if (bot->GetDistance2d(secondEmber) < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(secondEmber, safeDistance - bot->GetDistance2d(secondEmber)); + } + + SetRtiTarget(botAI, "circle", secondEmber); + if (bot->GetTarget() != secondEmber->GetGUID()) + return Attack(secondEmber); + } + else if (Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar")) + { + SetRtiTarget(botAI, "star", alar); + if (bot->GetTarget() != alar->GetGUID()) + return Attack(alar); + } + + return false; +} + +// Jump from platform during Flame Quills and wait at assigned position after landing +bool AlarJumpFromPlatformAction::Execute(Event /*event*/) +{ + if (bot->GetPositionZ() > ALAR_BALCONY_Z) + { + int8 closestPlatform; + Position ground; + GetClosestPlatformAndGround(bot->GetPosition(), closestPlatform, ground); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return JumpTo(TEMPEST_KEEP_MAP_ID, ground.GetPositionX(), ground.GetPositionY(), + ground.GetPositionZ(), MovementPriority::MOVEMENT_FORCED); + } + else + { + constexpr float distAlarTankFromPosition = 5.0f; + constexpr float distEmberTankFromPos = 25.0f; + constexpr float distMeleeDpsFromPos = 5.0f; + constexpr float distRangedFromPos = 10.0f; + + if (botAI->IsMainTank(bot) && + bot->GetExactDist2d(ALAR_SW_RAMP_BASE.GetPositionX(), + ALAR_SW_RAMP_BASE.GetPositionY()) > distAlarTankFromPosition) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, ALAR_SW_RAMP_BASE.GetPositionX(), + ALAR_SW_RAMP_BASE.GetPositionY(), ALAR_SW_RAMP_BASE.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + else if (botAI->IsAssistTankOfIndex(bot, 0, true) && + bot->GetExactDist2d(ALAR_SE_RAMP_BASE.GetPositionX(), + ALAR_SE_RAMP_BASE.GetPositionY()) > distAlarTankFromPosition) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, ALAR_SE_RAMP_BASE.GetPositionX(), + ALAR_SE_RAMP_BASE.GetPositionY(), ALAR_SE_RAMP_BASE.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + else if (botAI->IsAssistTankOfIndex(bot, 1, true) && + bot->GetExactDist2d(ALAR_POINT_MIDDLE.GetPositionX(), + ALAR_POINT_MIDDLE.GetPositionY()) > distEmberTankFromPos) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, ALAR_POINT_MIDDLE.GetPositionX(), + ALAR_POINT_MIDDLE.GetPositionY(), ALAR_POINT_MIDDLE.GetPositionZ(), + distEmberTankFromPos, MovementPriority::MOVEMENT_FORCED); + } + else if (botAI->IsMelee(bot) && + bot->GetExactDist2d(ALAR_ROOM_S_CENTER.GetPositionX(), + ALAR_ROOM_S_CENTER.GetPositionY()) > distMeleeDpsFromPos) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, ALAR_ROOM_S_CENTER.GetPositionX(), + ALAR_ROOM_S_CENTER.GetPositionY(), ALAR_ROOM_S_CENTER.GetPositionZ(), + distMeleeDpsFromPos, MovementPriority::MOVEMENT_FORCED); + } + else if (botAI->IsRanged(bot) && + bot->GetExactDist2d(ALAR_POINT_MIDDLE.GetPositionX(), + ALAR_POINT_MIDDLE.GetPositionY()) > distRangedFromPos) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, ALAR_POINT_MIDDLE.GetPositionX(), + ALAR_POINT_MIDDLE.GetPositionY(), ALAR_POINT_MIDDLE.GetPositionZ(), + distRangedFromPos, MovementPriority::MOVEMENT_FORCED); + } + } + + return false; +} + +bool AlarMoveAwayFromRebirthAction::Execute(Event /*event*/) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (bot->GetPositionZ() > ALAR_BALCONY_Z) + { + int8 closestPlatform; + Position ground; + GetClosestPlatformAndGround(bot->GetPosition(), closestPlatform, ground); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return JumpTo(TEMPEST_KEEP_MAP_ID, ground.GetPositionX(), ground.GetPositionY(), + ground.GetPositionZ(), MovementPriority::MOVEMENT_FORCED); + } + else + { + float currentDistance = bot->GetDistance2d(alar); + constexpr float safeDistance = 20.0f; + if (currentDistance < safeDistance) + return MoveAway(alar, safeDistance - currentDistance); + } + + return false; +} + +// Main tank and first assist tank will swap tanking Al'ar when Melt Armor is applied +bool AlarSwapTanksOnBossAction::Execute(Event /*event*/) +{ + if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0, true)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (alar->GetHealth() == alar->GetMaxHealth()) + { + SetRtiTarget(botAI, "star", alar); + if (bot->GetTarget() != alar->GetGUID()) + return Attack(alar); + } + + Player* secondEmberTank = GetSecondEmberTank(botAI); + if (secondEmberTank && secondEmberTank != bot) + { + SetRtiTarget(botAI, "star", alar); + if (bot->GetTarget() != alar->GetGUID()) + return Attack(alar); + else if (alar->GetVictim() != bot) + return botAI->DoSpecificAction("taunt spell", Event(), true); + } + + return false; +} + +bool AlarAvoidFlamePatchesAndDiveBombsAction::Execute(Event /*event*/) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + return AvoidFlamePatch() || HandleDiveBomb(alar); +} + +bool AlarAvoidFlamePatchesAndDiveBombsAction::AvoidFlamePatch() +{ + constexpr float searchRadius = 40.0f; + constexpr float hazardRadius = 8.0f; + + std::vector flamePatches = + GetAllHazardTriggers(bot, NPC_FLAME_PATCH, searchRadius); + + for (Unit* flamePatch : flamePatches) + { + if (bot->GetExactDist2d(flamePatch) < hazardRadius) + { + Position safestPos = FindSafestNearbyPosition(bot, flamePatches, hazardRadius); + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(TEMPEST_KEEP_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), + safestPos.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + + return false; +} + +bool AlarAvoidFlamePatchesAndDiveBombsAction::HandleDiveBomb(Unit* alar) +{ + if ((alar->HasUnitState(UNIT_STATE_CASTING) && + alar->FindCurrentSpellBySpellId(SPELL_REBIRTH_DIVE)) || + !alar->IsVisible()) + { + float currentDistance = bot->GetDistance2d(alar); + constexpr float safeDistance = 20.0f; + if (currentDistance < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(alar, safeDistance - currentDistance); + } + } + else + { + Position dest; + if (GetAlarCurrentLocationIndex(alar) == POINT_QUILL_OR_DIVE_IDX || + GetAlarDestinationLocationIndex(alar, dest) == POINT_QUILL_OR_DIVE_IDX) + { + constexpr float safeDistance = 10.0f; + constexpr uint32 minInterval = 0; + if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) + return FleePosition(nearestPlayer->GetPosition(), safeDistance, minInterval); + } + } + + return false; +} + +// For Phase 2, ensure that bots don't get too far away and become inactive +bool AlarReturnToRoomCenterAction::Execute(Event /*event*/) +{ + constexpr float distFromCenter = 45.0f; + const Position& center = ALAR_ROOM_CENTER; + if (bot->GetVictim() == nullptr && + bot->GetExactDist2d(center.GetPositionX(), center.GetPositionY()) > distFromCenter) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, center.GetPositionX(), center.GetPositionY(), + center.GetPositionZ(), distFromCenter - 5.0f, + MovementPriority::MOVEMENT_COMBAT); + } + + return false; +} + +bool AlarManagePhaseTrackerAction::Execute(Event /*event*/) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + const uint32 instanceId = alar->GetMap()->GetInstanceId(); + + if (alar->GetHealthPct() > 99.5f && alar->GetPositionZ() >= ALAR_BALCONY_Z) + { + isAlarInPhase2.erase(instanceId); + lastRebirthState.erase(instanceId); + } + + bool rebirthActive = alar->HasUnitState(UNIT_STATE_CASTING) && + alar->FindCurrentSpellBySpellId(SPELL_REBIRTH_PHASE2); + bool lastRebirth = lastRebirthState[instanceId]; + + if (lastRebirth && !rebirthActive) + isAlarInPhase2[instanceId] = true; + + lastRebirthState[instanceId] = rebirthActive; + + return false; +} + +// Void Reaver + +bool VoidReaverTanksPositionBossAction::Execute(Event /*event*/) +{ + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (!voidReaver) + return false; + + const Position& position = VOID_REAVER_TANK_POSITION; + + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float distToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (bot->IsWithinMeleeRange(voidReaver) && distToPosition > 2.0f) + { + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + + return false; +} + +bool VoidReaverUseAggroDumpAbilityAction::Execute(Event /*event*/) +{ + botAI->Reset(); + static const std::array spells = + { + "divine protection", + "fade", + "feign death", + "ice block", + "soulshatter", + "vanish", + }; + for (const char* spell : spells) + { + if (botAI->CanCastSpell(spell, bot) && + botAI->CastSpell(spell, bot)) + return true; + } + + return false; +} + +bool VoidReaverSpreadRangedAction::Execute(Event /*event*/) +{ + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (!voidReaver) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + ObjectGuid guid = bot->GetGUID(); + + if (!hasReachedVoidReaverPosition[guid]) + { + int healerCount = 0, rangedDpsCount = 0; + int healerIndex = GetHealerIndex(group, healerCount); + int rangedDpsIndex = GetRangedDpsIndex(group, rangedDpsCount); + + // Void Reaver's hitbox is 15 yards (GetDistance2d() of 16.5 yards for non-Tauren) + constexpr float radius = 45.0f; + float targetX = 0.0f; + float targetY = 0.0f; + + if (healerIndex != -1 && healerCount > 0) + { + float angle = 2 * M_PI * healerIndex / healerCount; + targetX = voidReaver->GetPositionX() + radius * std::cos(angle); + targetY = voidReaver->GetPositionY() + radius * std::sin(angle); + } + else if (rangedDpsIndex != -1 && rangedDpsCount > 0) + { + float angle = 2 * M_PI * rangedDpsIndex / rangedDpsCount; + if (healerCount > 0) + angle += M_PI / rangedDpsCount; + + targetX = voidReaver->GetPositionX() + radius * std::cos(angle); + targetY = voidReaver->GetPositionY() + radius * std::sin(angle); + } + + if (bot->GetExactDist2d(targetX, targetY) > 2.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + else + { + hasReachedVoidReaverPosition[guid] = true; + } + } + else + { + constexpr float safeDistance = 20.0f; + constexpr uint32 minInterval = 1000; + if (bot->GetDistance2d(voidReaver) < safeDistance) + return FleePosition(voidReaver->GetPosition(), safeDistance, minInterval); + } + + return false; +} + +int VoidReaverSpreadRangedAction::GetHealerIndex(Group* group, int& healerCount) +{ + std::vector healers; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !botAI->IsHeal(member)) + continue; + + healers.push_back(member); + } + + healerCount = healers.size(); + auto it = std::find(healers.begin(), healers.end(), bot); + return (it != healers.end()) ? std::distance(healers.begin(), it) : -1; +} + +int VoidReaverSpreadRangedAction::GetRangedDpsIndex(Group* group, int& rangedDpsCount) +{ + std::vector rangedDps; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !botAI->IsRanged(member) || botAI->IsHeal(member)) + continue; + + rangedDps.push_back(member); + } + + rangedDpsCount = rangedDps.size(); + auto it = std::find(rangedDps.begin(), rangedDps.end(), bot); + return (it != rangedDps.end()) ? std::distance(rangedDps.begin(), it) : -1; +} + +bool VoidReaverAvoidArcaneOrbAction::Execute(Event /*event*/) +{ + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (!voidReaver) + return false; + + auto it = voidReaverArcaneOrbs.find(bot->GetMap()->GetInstanceId()); + if (it == voidReaverArcaneOrbs.end() || it->second.empty()) + return false; + + uint32 currentTime = getMSTime(); + constexpr uint32 orbDuration = 7000; + constexpr float safeDistance = 22.0f; + bool shouldFlee = false; + Position fleeDest; + + for (auto const& orb : it->second) + { + if (getMSTimeDiff(orb.castTime, currentTime) <= orbDuration) + { + if (bot->GetExactDist2d(orb.destination.GetPositionX(), + orb.destination.GetPositionY()) < safeDistance) + { + shouldFlee = true; + fleeDest = orb.destination; + break; + } + } + } + + it->second.erase(std::remove_if(it->second.begin(), it->second.end(), + [currentTime](const ArcaneOrbData& orb) { + return getMSTimeDiff(orb.castTime, currentTime) > orbDuration; + }), it->second.end()); + + if (shouldFlee) + { + constexpr uint32 minInterval = 0; + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return FleePosition(fleeDest, safeDistance, minInterval); + } + + return false; +} + +bool VoidReaverEraseTrackersAction::Execute(Event /*event*/) +{ + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (voidReaver) + return false; + + bool erased = false; + + if (voidReaverArcaneOrbs.erase(bot->GetMap()->GetInstanceId())) + erased = true; + + if (hasReachedVoidReaverPosition.erase(bot->GetGUID())) + erased = true; + + return erased; +} + +// High Astromancer Solarian + +bool HighAstromancerSolarianRangedLeaveSpaceForMeleeAction::Execute(Event /*event*/) +{ + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + if (!astromancer) + return false; + + float currentDistance = bot->GetExactDist2d(astromancer); + constexpr float minDistance = 20.0f; + if (currentDistance < minDistance) + return MoveAway(astromancer, minDistance - currentDistance); + + return false; +} + +bool HighAstromancerSolarianMoveAwayFromGroupAction::Execute(Event /*event*/) +{ + constexpr float safeDistance = 15.0f; + if (GetNearestPlayerInRadius(bot, safeDistance)) + { + botAI->Reset(); + return MoveFromGroup(safeDistance); + } + + return false; +} + +bool HighAstromancerSolarianStackForAoeAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* stackTarget = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsRanged(member)) + { + stackTarget = member; + break; + } + } + + if (stackTarget && bot != stackTarget && bot->GetExactDist2d(stackTarget) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, stackTarget->GetPositionX(), stackTarget->GetPositionY(), + stackTarget->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +// Split melee into two groups, one on each Solarium Priest +bool HighAstromancerSolarianTargetSolariumPriestsAction::Execute(Event /*event*/) +{ + auto priestsPair = GetSolariumPriests(botAI); + if (!priestsPair.first || !priestsPair.second) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + auto meleeMembers = GetMeleeBots(group); + if (meleeMembers.empty()) + return false; + + Unit* targetPriest = AssignSolariumPriestsToBots(priestsPair, meleeMembers); + if (!targetPriest) + return false; + + auto it = std::find(meleeMembers.begin(), meleeMembers.end(), bot); + if (it == meleeMembers.end()) + return false; + + if (targetPriest == priestsPair.first) + { + MarkTargetWithSquare(bot, targetPriest); + SetRtiTarget(botAI, "square", targetPriest); + } + else + { + MarkTargetWithStar(bot, targetPriest); + SetRtiTarget(botAI, "star", targetPriest); + } + + if (bot->GetTarget() != targetPriest->GetGUID()) + return Attack(targetPriest); + + return false; +} + +std::pair HighAstromancerSolarianTargetSolariumPriestsAction::GetSolariumPriests(PlayerbotAI* botAI) +{ + Unit* lowest = nullptr; + Unit* highest = nullptr; + + for (auto const& guid : + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get()) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->GetEntry() == NPC_SOLARIUM_PRIEST) + { + if (!lowest || unit->GetGUID().GetRawValue() < lowest->GetGUID().GetRawValue()) + lowest = unit; + + if (!highest || unit->GetGUID().GetRawValue() > highest->GetGUID().GetRawValue()) + highest = unit; + } + } + + return {lowest, highest}; +} + +std::vector HighAstromancerSolarianTargetSolariumPriestsAction::GetMeleeBots(Group* group) +{ + std::vector meleeMembers; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsMelee(member) && GET_PLAYERBOT_AI(member)) + meleeMembers.push_back(member); + } + + return meleeMembers; +} + +Unit* HighAstromancerSolarianTargetSolariumPriestsAction::AssignSolariumPriestsToBots( + const std::pair& priestsPair, const std::vector& meleeMembers) +{ + if (!priestsPair.first || !priestsPair.second || meleeMembers.empty()) + return nullptr; + + auto it = std::find(meleeMembers.begin(), meleeMembers.end(), bot); + if (it == meleeMembers.end()) + return nullptr; + + size_t botIndex = std::distance(meleeMembers.begin(), it); + size_t totalMelee = meleeMembers.size(); + + if (totalMelee == 1) + return priestsPair.first; + + size_t split = totalMelee / 2; + + if (botIndex < split) + return priestsPair.first; + else + return priestsPair.second; +} + +bool HighAstromancerSolarianCastFearWardOnMainTankAction::Execute(Event /*event*/) +{ + Player* mainTank = GetGroupMainTank(botAI, bot); + if (mainTank && botAI->CanCastSpell("fear ward", mainTank)) + return botAI->CastSpell("fear ward", mainTank); + + return false; +} + +// Kael'thas Sunstrider + +bool KaelthasSunstriderKiteThaladredAction::Execute(Event /*event*/) +{ + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + if (!thaladred) + return false; + + float currentDistance = bot->GetExactDist2d(thaladred); + constexpr float safeDistance = 25.0f; + if (currentDistance < safeDistance) + return MoveAway(thaladred, safeDistance - currentDistance); + + return false; +} + +// Misdirect order: (1) Capernian, (2) Telonicus, (3) Capernian (again for good measure) +bool KaelthasSunstriderMisdirectAdvisorsToTanksAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector hunters; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && member->getClass() == CLASS_HUNTER && GET_PLAYERBOT_AI(member)) + hunters.push_back(member); + + if (hunters.size() >= 3) + break; + } + + int8 hunterIndex = -1; + for (size_t i = 0; i < hunters.size(); ++i) + { + if (hunters[i] == bot) + { + hunterIndex = static_cast(i); + break; + } + } + if (hunterIndex == -1) + return false; + + Unit* advisorTarget = nullptr; + Player* tankTarget = nullptr; + if (hunterIndex == 0 || hunterIndex == 2) + { + advisorTarget = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + tankTarget = GetCapernianTank(bot); + } + else if (hunterIndex == 1) + { + advisorTarget = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + tankTarget = GetGroupAssistTank(botAI, bot, 0); + } + + if (!advisorTarget || + advisorTarget->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || + advisorTarget->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE) || + advisorTarget->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + return false; + + if (!tankTarget || !tankTarget->IsAlive()) + return false; + + if (botAI->CanCastSpell("misdirection", tankTarget)) + return botAI->CastSpell("misdirection", tankTarget); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", advisorTarget)) + return botAI->CastSpell("steady shot", advisorTarget); + + return false; +} + +bool KaelthasSunstriderMainTankPositionSanguinarAction::Execute(Event /*event*/) +{ + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + if (!sanguinar) + return false; + + MarkTargetWithStar(bot, sanguinar); + SetRtiTarget(botAI, "star", sanguinar); + + if (bot->GetTarget() != sanguinar->GetGUID()) + return Attack(sanguinar); + + if (sanguinar->GetVictim() == bot && bot->IsWithinMeleeRange(sanguinar)) + { + const Position& position = SANGUINAR_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +bool KaelthasSunstriderCastFearWardOnSanguinarTankAction::Execute(Event /*event*/) +{ + Player* mainTank = GetGroupMainTank(botAI, bot); + if (mainTank && botAI->CanCastSpell("fear ward", mainTank)) + return botAI->CastSpell("fear ward", mainTank); + + return false; +} + +bool KaelthasSunstriderWarlockTankPositionCapernianAction::Execute(Event /*event*/) +{ + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (!capernian) + return false; + + MarkTargetWithCircle(bot, capernian); + SetRtiTarget(botAI, "circle", capernian); + + if (bot->GetTarget() != capernian->GetGUID() && + botAI->CanCastSpell("searing pain", capernian) && + botAI->CastSpell("searing pain", capernian)) + return true; + + if (capernian->GetVictim() == bot) + { + float currentDist = bot->GetDistance2d(capernian); + constexpr float minDistance = 28.0f; + if (currentDist < minDistance) + return MoveAway(capernian, minDistance - currentDist); + } + + if (botAI->CanCastSpell("searing pain", capernian)) + return botAI->CastSpell("searing pain", capernian); + + return false; +} + +bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::Execute(Event /*event*/) +{ + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (!capernian) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return false; + + if (botAI->IsRanged(bot) && capernian->GetVictim() != bot && + RangedBotsDisperse(kaelAI, capernian)) + { + return true; + } + else if (botAI->IsMelee(bot) && kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR && + MeleeStayBackFromCapernian(capernian)) + { + return true; + } + + return false; +} + +bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::RangedBotsDisperse(boss_kaelthas* kaelAI, Unit* capernian) +{ + if (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR) + { + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector healers; + std::vector rangedDps; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !botAI->IsRanged(member)) + continue; + + if (botAI->IsHeal(member)) + healers.push_back(member); + else + rangedDps.push_back(member); + } + + if (healers.empty() && rangedDps.empty()) + return false; + + size_t count = healers.size() + rangedDps.size(); + size_t botIndex = 0; + float radius = 0.0f; + float angle = 0.0f; + + // Spread is 90-degree arc for healers and 120-degree arc for ranged DPS + float arcSpan = botAI->IsHeal(bot) ? M_PI / 2.0f : 2.0f * M_PI / 3.0f; + constexpr float arcCenter = 2.9f; + float arcStart = arcCenter - arcSpan / 2.0f; + + // Capernian's hitbox is 4.5 yards (GetDistance2d of 6.0f for non-Tauren) + if (botAI->IsHeal(bot)) + { + auto findIt = std::find(healers.begin(), healers.end(), bot); + botIndex = (findIt != healers.end()) ? std::distance(healers.begin(), findIt) : 0; + radius = 42.0f; + count = healers.size(); + } + else + { + auto findIt = std::find(rangedDps.begin(), rangedDps.end(), bot); + botIndex = (findIt != rangedDps.end()) ? std::distance(rangedDps.begin(), findIt) : 0; + radius = 34.0f; + count = rangedDps.size(); + } + + angle = (count == 1) ? arcCenter : + (arcStart + arcSpan * static_cast(botIndex) / static_cast(count - 1)); + + float targetX = capernian->GetPositionX() + radius * std::cos(angle); + float targetY = capernian->GetPositionY() + radius * std::sin(angle); + + if (bot->GetExactDist2d(targetX, targetY) > 1.0f) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(TEMPEST_KEEP_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_FORCED, true, false); + } + } + else + { + if (AI_VALUE2(Unit*, "find target", "thaladred the darkener")) + return false; + + const float safeDistance = 6.0f; + constexpr uint32 minInterval = 1000; + if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) + return FleePosition(nearestPlayer->GetPosition(), safeDistance, minInterval); + } + + return false; +} + +bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::MeleeStayBackFromCapernian(Unit* capernian) +{ + // Main tank purposely stays in range to bait Conflagration in Phase 1 + if (botAI->IsMainTank(bot)) + { + // MoveTo called for a WorldObj is a GetDistance() check so both hitboxes are account for + constexpr float desiredDist = 15.0f; + botAI->Reset(); + return MoveTo(capernian, desiredDist, MovementPriority::MOVEMENT_FORCED); + } + else + { + constexpr float safeDistance = 42.0f; + float currentDistance = bot->GetDistance2d(capernian); + if (currentDistance < safeDistance) + { + botAI->Reset(); + return MoveAway(capernian, safeDistance - currentDistance); + } + else + { + return true; + } + } +} + +bool KaelthasSunstriderFirstAssistTankPositionTelonicusAction::Execute(Event /*event*/) +{ + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + if (!telonicus) + return false; + + MarkTargetWithTriangle(bot, telonicus); + SetRtiTarget(botAI, "triangle", telonicus); + + if (bot->GetTarget() != telonicus->GetGUID()) + return Attack(telonicus); + + if (telonicus->GetVictim() == bot && bot->IsWithinMeleeRange(telonicus)) + { + const Position& position = TELONICUS_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +bool KaelthasSunstriderHandleAdvisorRolesInPhase3Action::Execute(Event /*event*/) +{ + const Position* movePosition = nullptr; + if (botAI->IsAssistHealOfIndex(bot, 0, true)) + { + movePosition = &ADVISOR_HEAL_POSITION; + } + else if (botAI->IsMainTank(bot)) + { + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + if (sanguinar && sanguinar->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + movePosition = &SANGUINAR_WAITING_POSITION; + } + else if (botAI->IsAssistTankOfIndex(bot, 0, true)) + { + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + if (telonicus && telonicus->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + movePosition = &TELONICUS_WAITING_POSITION; + } + else if (GetCapernianTank(bot) == bot) + { + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (capernian && capernian->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + movePosition = &CAPERNIAN_WAITING_POSITION; + } + + if (movePosition && + bot->GetExactDist2d(movePosition->GetPositionX(), movePosition->GetPositionY()) > 2.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, movePosition->GetPositionX(), movePosition->GetPositionY(), + movePosition->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool KaelthasSunstriderReequipGearAction::Execute(Event /*event*/) +{ + return botAI->DoSpecificAction("equip upgrade", Event(), true); +} + +bool KaelthasSunstriderAssignAdvisorDpsPriorityAction::Execute(Event /*event*/) +{ + // Target priority 1: Thaladred, except Capernian tank + Player* capernianTank = GetCapernianTank(bot); + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + + if ((!capernianTank || bot != capernianTank) && + thaladred && !thaladred->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !thaladred->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + { + MarkTargetWithSquare(bot, thaladred); + SetRtiTarget(botAI, "square", thaladred); + + if (bot->GetTarget() != thaladred->GetGUID()) + return Attack(thaladred); + + return false; + } + + // Target priority 2: Capernian for ranged only (excluding longbow tank) + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + + if (botAI->IsRangedDps(bot) && !IsDebuffHunter(bot) && + capernian && !capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + { + SetRtiTarget(botAI, "circle", capernian); + + if (bot->GetTarget() != capernian->GetGUID()) + return Attack(capernian); + + return false; + } + + // Target priority 3: Sanguinar (debuff hunter and melee move here after Thaladred) + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + + if (sanguinar && !sanguinar->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !sanguinar->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + { + SetRtiTarget(botAI, "star", sanguinar); + + if (bot->GetTarget() != sanguinar->GetGUID()) + return Attack(sanguinar); + + return false; + } + + // Target priority 4: Telonicus + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + + if (telonicus && !telonicus->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !telonicus->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + { + SetRtiTarget(botAI, "triangle", telonicus); + if (bot->GetTarget() != telonicus->GetGUID()) + return Attack(telonicus); + + // Melee DPS need to stay at max-ish melee range behind Telonicus to avoid bombs + if (botAI->IsMelee(bot) && botAI->IsDps(bot) && telonicus->GetVictim() != bot) + { + float desiredDist = bot->GetMeleeRange(telonicus); + float behindAngle = Position::NormalizeOrientation(telonicus->GetOrientation() + M_PI); + float targetX = telonicus->GetPositionX() + desiredDist * std::cos(behindAngle); + float targetY = telonicus->GetPositionY() + desiredDist * std::sin(behindAngle); + + if (bot->GetExactDist2d(targetX, targetY) > 0.25f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, targetX, targetY, telonicus->GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + } + } + + return false; +} + +bool KaelthasSunstriderManageAdvisorDpsTimerAction::Execute(Event /*event*/) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + static const std::array advisorNames = + { + "grand astromancer capernian", + "master engineer telonicus", + "lord sanguinar" + }; + + for (const char* name : advisorNames) + { + Unit* advisor = AI_VALUE2(Unit*, "find target", name); + if (!advisor) + continue; + + if (advisor->GetHealth() == advisor->GetMaxHealth() && + !advisor->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE)) + { + const time_t now = std::time(nullptr); + advisorDpsWaitTimer.insert_or_assign(kaelthas->GetMap()->GetInstanceId(), now); + return true; + } + } + + return false; +} + +bool KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction::Execute(Event /*event*/) +{ + if (botAI->IsAssistTank(bot)) + SetRtiTarget(botAI, "moon", nullptr); + + // Priority 0: Everybody other than the main tank needs to stay away from the axe + // But this applies to assist tanks only after they get aggro on the mace, dagger, or sword + Unit* axe = AI_VALUE2(Unit*, "find target", "devastation"); + Unit* mace = AI_VALUE2(Unit*, "find target", "cosmic infuser"); + Unit* dagger = AI_VALUE2(Unit*, "find target", "infinity blades"); + Unit* sword = AI_VALUE2(Unit*, "find target", "warp slicer"); + + if (axe) + { + bool hasAggroFromWeapon = mace && mace->GetVictim() == bot || + dagger && dagger->GetVictim() == bot || + sword && sword->GetVictim() == bot; + if (!botAI->IsTank(bot) || + (botAI->IsAssistTank(bot) && hasAggroFromWeapon)) + { + float currentDistance = bot->GetExactDist2d(axe); + const float safeDistance = botAI->IsAssistTank(bot) ? 17.0f : 13.0f; + if (currentDistance < safeDistance) + return MoveAway(axe, safeDistance - currentDistance); + } + } + + if (botAI->IsDps(bot)) + { + // Priority 1: Staff of Disintegration (Skull) + if (Unit* staff = AI_VALUE2(Unit*, "find target", "staff of disintegration")) + { + MarkTargetWithSkull(bot, staff); + SetRtiTarget(botAI, "skull", staff); + + if (bot->GetTarget() != staff->GetGUID()) + return Attack(staff); + } + // Priority 2: Cosmic Infuser (Skull) + else if (mace) + { + MarkTargetWithSkull(bot, mace); + SetRtiTarget(botAI, "skull", mace); + + if (bot->GetTarget() != mace->GetGUID()) + return Attack(mace); + } + // Priority 3: Warp Slicer (Skull) + else if (sword) + { + MarkTargetWithSkull(bot, sword); + SetRtiTarget(botAI, "skull", sword); + + if (bot->GetTarget() != sword->GetGUID()) + return Attack(sword); + } + // Priority 4: Infinity Blades (Skull) + else if (dagger) + { + MarkTargetWithSkull(bot, dagger); + SetRtiTarget(botAI, "skull", dagger); + + if (bot->GetTarget() != dagger->GetGUID()) + return Attack(dagger); + } + // Priority 5: Devastation - ranged only (Diamond--marked in other method by main tank) + else if (axe && botAI->IsRangedDps(bot)) + { + SetRtiTarget(botAI, "diamond", axe); + + if (bot->GetTarget() != axe->GetGUID()) + return Attack(axe); + } + // Priority 6: Netherstrand Longbow (Skull) + else if (Unit* longbow = AI_VALUE2(Unit*, "find target", "netherstrand longbow")) + { + MarkTargetWithSkull(bot, longbow); + SetRtiTarget(botAI, "skull", longbow); + + if (bot->GetTarget() != longbow->GetGUID()) + return Attack(longbow); + } + // Priority 7: Phaseshift Bulwark (Skull) + else if (Unit* shield = AI_VALUE2(Unit*, "find target", "phaseshift bulwark")) + { + MarkTargetWithSkull(bot, shield); + SetRtiTarget(botAI, "skull", shield); + + if (bot->GetTarget() != shield->GetGUID()) + return Attack(shield); + } + } + + return false; +} + +bool KaelthasSunstriderMoveDevastationAwayAction::Execute(Event /*event*/) +{ + Unit* axe = AI_VALUE2(Unit*, "find target", "devastation"); + if (!axe) + return false; + + MarkTargetWithDiamond(bot, axe); + SetRtiTarget(botAI, "diamond", axe); + + if (bot->GetTarget() != axe->GetGUID()) + return Attack(axe); + + constexpr float safeDistance = 13.0f; + if (axe->GetVictim() == bot && GetNearestNonTankPlayerInRadius(botAI, bot, safeDistance)) + return MoveFromGroup(safeDistance); + + return false; +} + +bool KaelthasSunstriderLootLegendaryWeaponsAction::Execute(Event /*event*/) +{ + struct WeaponInfo + { + uint32 npcEntry; + uint32 itemId; + }; + + static const std::array weapons = + { + WeaponInfo{ NPC_NETHERSTRAND_LONGBOW, ITEM_NETHERSTRAND_LONGBOW }, + WeaponInfo{ NPC_COSMIC_INFUSER, ITEM_COSMIC_INFUSER }, + WeaponInfo{ NPC_DEVASTATION, ITEM_DEVASTATION }, + WeaponInfo{ NPC_INFINITY_BLADES, ITEM_INFINITY_BLADE }, + WeaponInfo{ NPC_WARP_SLICER, ITEM_WARP_SLICER }, + WeaponInfo{ NPC_STAFF_OF_DISINTEGRATION, ITEM_STAFF_OF_DISINTEGRATION }, + WeaponInfo{ NPC_PHASESHIFT_BULWARK, ITEM_PHASESHIFT_BULWARK } + }; + + for (auto const& weapon : weapons) + { + if (ShouldBotLootWeapon(weapon.npcEntry)) + { + if (bot->HasItemCount(weapon.itemId, 1, false)) + { + EquipAction* equipAction = + dynamic_cast(botAI->GetAiObjectContext()->GetAction("equip")); + if (equipAction) + { + ItemIds ids; + ids.insert(weapon.itemId); + equipAction->EquipItems(ids); + } + continue; + } + return LootWeapon(weapon.npcEntry, weapon.itemId); + } + } + + return false; +} + +bool KaelthasSunstriderLootLegendaryWeaponsAction::ShouldBotLootWeapon(uint32 weaponEntry) +{ + uint8 tab = AiFactory::GetPlayerSpecTab(bot); + switch (weaponEntry) + { + case NPC_NETHERSTRAND_LONGBOW: + return bot->getClass() == CLASS_HUNTER; + + case NPC_COSMIC_INFUSER: + return botAI->IsHeal(bot); + + // Fury Warriors could use the axe, but their DPS is terrible at appropriate gear levels + // So IMO they're better off looting only the dagger to MH it and break MCs + case NPC_DEVASTATION: + return (bot->getClass() == CLASS_WARRIOR && tab == WARRIOR_TAB_ARMS) || + (bot->getClass() == CLASS_PALADIN && tab == PALADIN_TAB_RETRIBUTION) || + (botAI->IsDps(bot) && bot->getClass() == CLASS_DEATH_KNIGHT); + + case NPC_INFINITY_BLADES: + return bot->getClass() == CLASS_ROGUE || + bot->getClass() == CLASS_HUNTER || + (bot->getClass() == CLASS_SHAMAN && tab == SHAMAN_TAB_ENHANCEMENT) || + (bot->getClass() == CLASS_WARRIOR && tab != WARRIOR_TAB_ARMS); + + case NPC_WARP_SLICER: + return bot->getClass() == CLASS_ROGUE && tab != ROGUE_TAB_ASSASSINATION || + (botAI->IsTank(bot) && + (bot->getClass() == CLASS_DEATH_KNIGHT || + bot->getClass() == CLASS_PALADIN)); + + case NPC_STAFF_OF_DISINTEGRATION: + return (botAI->IsRangedDps(bot) && bot->getClass() != CLASS_HUNTER) || + (bot->getClass() == CLASS_DRUID && tab == DRUID_TAB_FERAL); + + case NPC_PHASESHIFT_BULWARK: + return botAI->IsTank(bot) && + (bot->getClass() == CLASS_PALADIN || + bot->getClass() == CLASS_WARRIOR || + bot->getClass() == CLASS_DEATH_KNIGHT); + + default: + return false; + } +} + +bool KaelthasSunstriderLootLegendaryWeaponsAction::LootWeapon( + uint32 weaponEntry, uint32 itemId) +{ + constexpr float searchRadius = 150.0f; + Creature* weapon = bot->FindNearestCreature(weaponEntry, searchRadius, false); + + if (!weapon || weapon->IsAlive()) + return false; + + LootObject loot(bot, weapon->GetGUID()); + if (!loot.IsLootPossible(bot)) + return false; + + context->GetValue("loot target")->Set(loot); + + const float maxLootRange = sPlayerbotAIConfig.lootDistance; + constexpr float distFromObject = 2.0f; + + if (bot->GetDistance(weapon) > maxLootRange) + return MoveTo(weapon, distFromObject, MovementPriority::MOVEMENT_COMBAT); + + OpenLootAction open(botAI); + bool opened = open.Execute(Event()); + if (!opened) + return opened; + + if (bot->HasItemCount(itemId, 1, false)) + return false; + + bot->SetLootGUID(weapon->GetGUID()); + + constexpr uint8 weaponIndex = 0; + WorldPacket* packet = new WorldPacket(CMSG_AUTOSTORE_LOOT_ITEM, 1); + *packet << weaponIndex; + bot->GetSession()->QueuePacket(packet); + + return true; +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::Execute(Event /*event*/) +{ + return UsePhaseshiftBulwark() || + UseStaffOfDisintegration() || + UseNetherstrandLongbow(); +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::UsePhaseshiftBulwark() +{ + Item* offHand = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND); + if (!offHand || offHand->GetEntry() != ITEM_PHASESHIFT_BULWARK) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas || !kaelthas->HasAura(SPELL_SHOCK_BARRIER)) + return false; + + if (bot->HasAura(SPELL_ARCANE_BARRIER) || bot->CanUseItem(offHand) != EQUIP_ERR_OK) + return false; + + return UseEquippedItemWithPacket(offHand); +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::UseStaffOfDisintegration() +{ + Item* mainHand = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND); + if (!mainHand || mainHand->GetEntry() != ITEM_STAFF_OF_DISINTEGRATION) + return false; + + if (bot->HasAura(SPELL_MENTAL_PROTECTION_FIELD) || + bot->CanUseItem(mainHand) != EQUIP_ERR_OK) + return false; + + return UseEquippedItemWithPacket(mainHand); +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::UseNetherstrandLongbow() +{ + Item* ranged = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED); + if (!ranged || ranged->GetEntry() != ITEM_NETHERSTRAND_LONGBOW) + return false; + + if (bot->HasItemCount(ITEM_NETHER_SPIKES, 1, false) || + bot->CanUseItem(ranged) != EQUIP_ERR_OK) + return false; + + return UseEquippedItemWithPacket(ranged); +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::UseEquippedItemWithPacket(Item* item) +{ + if (!item || bot->CanUseItem(item) != EQUIP_ERR_OK || bot->IsNonMeleeSpellCast(true)) + return false; + + uint8 bagIndex = item->GetBagSlot(); + uint8 slot = item->GetSlot(); + uint8 cast_count = 1; + ObjectGuid item_guid = item->GetGUID(); + uint32 glyphIndex = 0; + uint8 castFlags = 0; + uint32 spellId = 0; + + for (uint8 i = 0; i < MAX_ITEM_PROTO_SPELLS; ++i) + { + if (item->GetTemplate()->Spells[i].SpellId > 0 && + item->GetTemplate()->Spells[i].SpellTrigger == ITEM_SPELLTRIGGER_ON_USE) + { + spellId = item->GetTemplate()->Spells[i].SpellId; + break; + } + } + + if (!spellId) + return false; + + WorldPacket packet(CMSG_USE_ITEM); + packet << bagIndex << slot << cast_count << spellId << item_guid << glyphIndex << castFlags; + + uint32 targetFlag = TARGET_FLAG_UNIT; + packet << targetFlag << bot->GetPackGUID(); + + bot->GetSession()->HandleUseItemOpcode(packet); + return true; +} + +bool KaelthasSunstriderMainTankPositionBossAction::Execute(Event /*event*/) +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + MarkTargetWithStar(bot, kaelthas); + SetRtiTarget(botAI, "star", kaelthas); + + if (bot->GetTarget() != kaelthas->GetGUID()) + return Attack(kaelthas); + + if (kaelthas->GetVictim() == bot && bot->IsWithinMeleeRange(kaelthas)) + { + const Position& position = KAELTHAS_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 4.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +bool KaelthasSunstriderAvoidFlameStrikeAction::Execute(Event /*event*/) +{ + constexpr float searchRadius = 40.0f; + std::vector flameStrikes = + GetAllHazardTriggers(bot, NPC_FLAME_STRIKE_TRIGGER, searchRadius); + + if (flameStrikes.empty()) + return false; + + constexpr float hazardRadius = 12.0f; + bool inDanger = false; + for (Unit* flameStrike : flameStrikes) + { + if (bot->GetExactDist2d(flameStrike) < hazardRadius) + { + inDanger = true; + break; + } + } + + if (!inDanger) + return false; + + Position safestPos = FindSafestNearbyPosition(bot, flameStrikes, hazardRadius); + + botAI->Reset(); + return MoveTo(TEMPEST_KEEP_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), + safestPos.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); +} + +bool KaelthasSunstriderHandlePhoenixesAndEggsAction::Execute(Event /*event*/) +{ + if (botAI->IsAssistTankOfIndex(bot, 0, true) || botAI->IsAssistTankOfIndex(bot, 1, true)) + return AssistTanksPickUpPhoenixes(); + else + return NonTanksDestroyEggsAndAvoidPhoenixes(); +} + +bool KaelthasSunstriderHandlePhoenixesAndEggsAction::AssistTanksPickUpPhoenixes() +{ + std::vector phoenixes; + auto const& npcs = botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get(); + for (auto const& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->GetEntry() == NPC_PHOENIX && unit->IsAlive()) + phoenixes.push_back(unit); + } + + if (phoenixes.empty()) + return false; + + std::sort(phoenixes.begin(), phoenixes.end(), + [](Unit* first, Unit* second) { return first->GetGUID() < second->GetGUID(); }); + + Unit* targetPhoenix = nullptr; + if (botAI->IsAssistTankOfIndex(bot, 0, true)) + { + targetPhoenix = phoenixes[0]; + MarkTargetWithSquare(bot, targetPhoenix); + SetRtiTarget(botAI, "square", targetPhoenix); + } + else if (botAI->IsAssistTankOfIndex(bot, 1, true) && phoenixes.size() >= 2) + { + targetPhoenix = phoenixes[1]; + MarkTargetWithCircle(bot, targetPhoenix); + SetRtiTarget(botAI, "circle", targetPhoenix); + } + + if (!targetPhoenix) + return false; + + if (bot->GetTarget() != targetPhoenix->GetGUID()) + return Attack(targetPhoenix); + + constexpr float safeDistance = 12.0f; + if (targetPhoenix->GetVictim() == bot && + GetNearestNonTankPlayerInRadius(botAI, bot, safeDistance)) + return MoveFromGroup(safeDistance); + + return false; +} + +bool KaelthasSunstriderHandlePhoenixesAndEggsAction::NonTanksDestroyEggsAndAvoidPhoenixes() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (botAI->IsDps(bot) && !kaelthas->HasAura(SPELL_SHOCK_BARRIER)) + { + if (Unit* phoenixEgg = GetFirstAliveUnitByEntry(botAI, NPC_PHOENIX_EGG)) + { + MarkTargetWithDiamond(bot, phoenixEgg); + SetRtiTarget(botAI, "diamond", phoenixEgg); + + if (bot->GetTarget() != phoenixEgg->GetGUID()) + return Attack(phoenixEgg); + } + } + else if (botAI->IsDps(bot)) + return false; + + if (Unit* phoenix = AI_VALUE2(Unit*, "find target", "phoenix")) + { + float currentDistance = bot->GetExactDist2d(phoenix); + constexpr float safeDistance = 12.0f; + if (currentDistance < safeDistance) + return MoveAway(phoenix, safeDistance - currentDistance); + } + + return false; +} + +bool KaelthasSunstriderBreakMindControlAction::Execute(Event /*event*/) +{ + Player* mcTarget = nullptr; + float closestDist = std::numeric_limits::max(); + + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot) + continue; + + if (member->HasAura(SPELL_KAELTHAS_MIND_CONTROL)) + { + float dist = bot->GetExactDist2d(member); + if (dist < closestDist) + { + closestDist = dist; + mcTarget = member; + } + } + } + + if (!mcTarget) + return false; + + if (!bot->IsWithinMeleeRange(mcTarget)) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, mcTarget->GetPositionX(), mcTarget->GetPositionY(), + mcTarget->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + if (bot->getClass() == CLASS_ROGUE && + AiFactory::GetPlayerSpecTab(bot) != ROGUE_TAB_COMBAT && + botAI->CanCastSpell("sinister strike", mcTarget)) + { + return botAI->CastSpell("sinister strike", mcTarget); + } + else + { + static const std::array spells = + { + "hamstring", + "wing clip", + "shiv", + "stormstrike" + }; + for (const char* spell : spells) + { + if (botAI->CanCastSpell(spell, mcTarget)) + return botAI->CastSpell(spell, mcTarget); + } + } + + return false; +} + +// Shock Barrier needs to be #1 focus, even if there is a Phoenix Egg up +bool KaelthasSunstriderBreakThroughShockBarrierAction::Execute(Event /*event*/) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (!kaelthas->HasAura(SPELL_SHOCK_BARRIER)) + { + static const std::array spells = + { + "bash", + "counterspell", + "kick", + "mind freeze", + "pummel", + "shield bash", + "silencing shot", + "wind shear", + }; + for (const char* spell : spells) + { + if (botAI->CanCastSpell(spell, kaelthas)) + return botAI->CastSpell(spell, kaelthas); + } + } + else if (bot->GetTarget() != kaelthas->GetGUID()) + { + SetRtiTarget(botAI, "star", kaelthas); + return Attack(kaelthas); + } + + return false; +} + +bool KaelthasSunstriderSpreadOutInMidairAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + constexpr float minSpreadDistance = 16.0f; + + std::vector nearbyPlayers; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot || !member->IsAlive()) + continue; + + if (bot->IsWithinDist3d(member, minSpreadDistance * 1.0f)) + nearbyPlayers.push_back(member); + } + + if (nearbyPlayers.empty()) + return false; + + Player* closestPlayer = nullptr; + float closestDist = std::numeric_limits::max(); + for (Player* player : nearbyPlayers) + { + float distToPlayer = bot->GetExactDist(player); + if (distToPlayer < closestDist) + { + closestDist = distToPlayer; + closestPlayer = player; + } + } + + if (closestPlayer && closestDist < minSpreadDistance) + { + float angle = bot->GetAngle(closestPlayer) + M_PI; + float distance = minSpreadDistance - closestDist; + + float x = bot->GetPositionX() + std::cos(angle) * distance; + float y = bot->GetPositionY() + std::sin(angle) * distance; + + return MoveTo(TEMPEST_KEEP_MAP_ID, x, y, bot->GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} diff --git a/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h new file mode 100644 index 00000000..8b021304 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h @@ -0,0 +1,413 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPACTIONS_H +#define _PLAYERBOT_RAIDTEMPESTKEEPACTIONS_H + +#include "RaidTempestKeepHelpers.h" +#include "RaidTempestKeepKaelthasBossAI.h" +#include "Action.h" +#include "AttackAction.h" +#include "MovementActions.h" + +using namespace TempestKeepHelpers; + +// Trash + +class CrimsonHandCenturionCastPolymorphAction : public Action +{ +public: + CrimsonHandCenturionCastPolymorphAction( + PlayerbotAI* botAI, std::string const name = "crimson hand centurion cast polymorph") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Al'ar + +class AlarMisdirectBossToMainTankAction : public AttackAction +{ +public: + AlarMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "al'ar misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarBossTanksMoveBetweenPlatformsAction : public AttackAction +{ +public: + AlarBossTanksMoveBetweenPlatformsAction( + PlayerbotAI* botAI, std::string const name = "al'ar boss tanks move between platforms") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool PositionMainTank(Unit* alar, int8 locationIndex); + bool PositionAssistTank(Unit* alar, int8 locationIndex); +}; + +class AlarMeleeDpsMoveBetweenPlatformsAction : public AttackAction +{ +public: + AlarMeleeDpsMoveBetweenPlatformsAction( + PlayerbotAI* botAI, std::string const name = "al'ar melee dps move between platforms") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarRangedAndEmberTankMoveUnderPlatformsAction : public AttackAction +{ +public: + AlarRangedAndEmberTankMoveUnderPlatformsAction( + PlayerbotAI* botAI, std::string const name = "al'ar ranged and ember tank move under platforms") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarAssistTanksPickUpEmbersAction : public AttackAction +{ +public: + AlarAssistTanksPickUpEmbersAction( + PlayerbotAI* botAI, std::string const name = "al'ar assist tanks pick up embers") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool HandlePhase1Embers(Unit* alar); + bool HandlePhase2Embers(Unit* alar); +}; + +class AlarRangedDpsPrioritizeEmbersAction : public AttackAction +{ +public: + AlarRangedDpsPrioritizeEmbersAction( + PlayerbotAI* botAI, std::string const name = "al'ar ranged dps prioritize embers") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarJumpFromPlatformAction : public MovementAction +{ +public: + AlarJumpFromPlatformAction( + PlayerbotAI* botAI, std::string const name = "al'ar jump from platform") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarMoveAwayFromRebirthAction : public MovementAction +{ +public: + AlarMoveAwayFromRebirthAction( + PlayerbotAI* botAI, std::string const name = "al'ar move away from rebirth") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarSwapTanksOnBossAction : public AttackAction +{ +public: + AlarSwapTanksOnBossAction( + PlayerbotAI* botAI, std::string const name = "al'ar swap tanks on boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarAvoidFlamePatchesAndDiveBombsAction : public MovementAction +{ +public: + AlarAvoidFlamePatchesAndDiveBombsAction( + PlayerbotAI* botAI, std::string const name = "al'ar avoid flame patches and dive bombs") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool AvoidFlamePatch(); + bool HandleDiveBomb(Unit* alar); +}; + +class AlarReturnToRoomCenterAction : public MovementAction +{ +public: + AlarReturnToRoomCenterAction( + PlayerbotAI* botAI, std::string const name = "al'ar return to room center") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarManagePhaseTrackerAction : public Action +{ +public: + AlarManagePhaseTrackerAction( + PlayerbotAI* botAI, std::string const name = "al'ar manage phase tracker") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Void Reaver + +class VoidReaverTanksPositionBossAction : public AttackAction +{ +public: + VoidReaverTanksPositionBossAction( + PlayerbotAI* botAI, std::string const name = "void reaver tanks position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class VoidReaverUseAggroDumpAbilityAction : public Action +{ +public: + VoidReaverUseAggroDumpAbilityAction( + PlayerbotAI* botAI, std::string const name = "void reaver use aggro dump ability") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class VoidReaverSpreadRangedAction : public MovementAction +{ +public: + VoidReaverSpreadRangedAction( + PlayerbotAI* botAI, std::string const name = "void reaver spread ranged") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + int GetHealerIndex(Group* group, int& healerCount); + int GetRangedDpsIndex(Group* group, int& rangedDpsCount); +}; + +class VoidReaverAvoidArcaneOrbAction : public MovementAction +{ +public: + VoidReaverAvoidArcaneOrbAction( + PlayerbotAI* botAI, std::string const name = "void reaver avoid arcane orb") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class VoidReaverEraseTrackersAction : public Action +{ +public: + VoidReaverEraseTrackersAction( + PlayerbotAI* botAI, std::string const name = "void reaver erase trackers") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// High Astromancer Solarian + +class HighAstromancerSolarianRangedLeaveSpaceForMeleeAction : public MovementAction +{ +public: + HighAstromancerSolarianRangedLeaveSpaceForMeleeAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian ranged leave space for melee") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HighAstromancerSolarianMoveAwayFromGroupAction : public MovementAction +{ +public: + HighAstromancerSolarianMoveAwayFromGroupAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian move away from group") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HighAstromancerSolarianStackForAoeAction : public MovementAction +{ +public: + HighAstromancerSolarianStackForAoeAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian stack for aoe") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HighAstromancerSolarianTargetSolariumPriestsAction : public AttackAction +{ +public: + HighAstromancerSolarianTargetSolariumPriestsAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian target solarium priests") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + std::pair GetSolariumPriests(PlayerbotAI* botAI); + std::vector GetMeleeBots(Group* group); + Unit* AssignSolariumPriestsToBots(const std::pair& priestsPair, const std::vector& meleeMembers); +}; + +class HighAstromancerSolarianCastFearWardOnMainTankAction : public Action +{ +public: + HighAstromancerSolarianCastFearWardOnMainTankAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian cast fear ward on main tank") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Kael'thas Sunstrider + +class KaelthasSunstriderKiteThaladredAction : public MovementAction +{ +public: + KaelthasSunstriderKiteThaladredAction( + PlayerbotAI* botAI) : MovementAction(botAI, "kael'thas sunstrider kite thaladred") {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderMisdirectAdvisorsToTanksAction : public AttackAction +{ +public: + KaelthasSunstriderMisdirectAdvisorsToTanksAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider misdirect advisors to tanks") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderMainTankPositionSanguinarAction : public AttackAction +{ +public: + KaelthasSunstriderMainTankPositionSanguinarAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider main tank position sanguinar") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderCastFearWardOnSanguinarTankAction : public Action +{ +public: + KaelthasSunstriderCastFearWardOnSanguinarTankAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider cast fear ward on sanguinar tank") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderWarlockTankPositionCapernianAction : public AttackAction +{ +public: + KaelthasSunstriderWarlockTankPositionCapernianAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider warlock tank position capernian") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction : public MovementAction +{ +public: + KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider spread and move away from capernian") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool RangedBotsDisperse(boss_kaelthas* kaelAI, Unit* capernian); + bool MeleeStayBackFromCapernian(Unit* capernian); +}; + +class KaelthasSunstriderFirstAssistTankPositionTelonicusAction : public AttackAction +{ +public: + KaelthasSunstriderFirstAssistTankPositionTelonicusAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider first assist tank position telonicus") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderHandleAdvisorRolesInPhase3Action : public MovementAction +{ +public: + KaelthasSunstriderHandleAdvisorRolesInPhase3Action( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider handle advisor roles in phase 3") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderAssignAdvisorDpsPriorityAction : public AttackAction +{ +public: + KaelthasSunstriderAssignAdvisorDpsPriorityAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider assign advisor dps priority") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderManageAdvisorDpsTimerAction : public Action +{ +public: + KaelthasSunstriderManageAdvisorDpsTimerAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider manage advisor dps timer") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction : public AttackAction +{ +public: + KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider assign legendary weapon dps priority") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderMoveDevastationAwayAction : public AttackAction +{ +public: + KaelthasSunstriderMoveDevastationAwayAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider move devastation away") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderLootLegendaryWeaponsAction : public MovementAction +{ +public: + KaelthasSunstriderLootLegendaryWeaponsAction( + PlayerbotAI* botAI) : MovementAction(botAI, "kael'thas sunstrider loot legendary weapons") {} + bool Execute(Event event) override; + +private: + bool ShouldBotLootWeapon(uint32 weaponEntry); + bool LootWeapon(uint32 weaponEntry, uint32 itemId); +}; + +class KaelthasSunstriderUseLegendaryWeaponsAction : public Action +{ +public: + KaelthasSunstriderUseLegendaryWeaponsAction( + PlayerbotAI* botAI) : Action(botAI, "kael'thas sunstrider use legendary weapons") {} + bool Execute(Event event) override; + +private: + bool UsePhaseshiftBulwark(); + bool UseStaffOfDisintegration(); + bool UseNetherstrandLongbow(); + bool UseEquippedItemWithPacket(Item* item); +}; + +class KaelthasSunstriderReequipGearAction : public Action +{ +public: + KaelthasSunstriderReequipGearAction( + PlayerbotAI* botAI) : Action(botAI, "kael'thas sunstrider reequip gear") {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderMainTankPositionBossAction : public AttackAction +{ +public: + KaelthasSunstriderMainTankPositionBossAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider main tank position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderAvoidFlameStrikeAction : public MovementAction +{ +public: + KaelthasSunstriderAvoidFlameStrikeAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider avoid flame strike") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderHandlePhoenixesAndEggsAction : public AttackAction +{ +public: + KaelthasSunstriderHandlePhoenixesAndEggsAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider handle phoenixes and eggs") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool AssistTanksPickUpPhoenixes(); + bool NonTanksDestroyEggsAndAvoidPhoenixes(); +}; + +class KaelthasSunstriderBreakMindControlAction : public AttackAction +{ +public: + KaelthasSunstriderBreakMindControlAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider break mind control") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderBreakThroughShockBarrierAction : public AttackAction +{ +public: + KaelthasSunstriderBreakThroughShockBarrierAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider break through shock barrier") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderSpreadOutInMidairAction : public MovementAction +{ +public: + KaelthasSunstriderSpreadOutInMidairAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider spread out in midair") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +#endif diff --git a/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp new file mode 100644 index 00000000..201bbc76 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp @@ -0,0 +1,422 @@ +#include "RaidTempestKeepMultipliers.h" +#include "RaidTempestKeepActions.h" +#include "RaidTempestKeepHelpers.h" +#include "RaidTempestKeepKaelthasBossAI.h" +#include "ChooseTargetActions.h" +#include "DKActions.h" +#include "DruidActions.h" +#include "DruidBearActions.h" +#include "EquipAction.h" +#include "FollowActions.h" +#include "HunterActions.h" +#include "MageActions.h" +#include "PaladinActions.h" +#include "Playerbots.h" +#include "RogueActions.h" +#include "ShamanActions.h" +#include "WarlockActions.h" +#include "WarriorActions.h" + +// Al'ar + +float AlarMoveBetweenPlatformsMultiplier::GetValue(Action* action) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return 1.0f; + + if (isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (botAI->IsDps(bot) && + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float AlarDisableDisperseMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "al'ar")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float AlarDisableTankAssistMultiplier::GetValue(Action* action) +{ + if (bot->GetVictim() == nullptr) + return 1.0f; + + if (!botAI->IsTank(bot)) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "al'ar")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float AlarStayAwayFromRebirthMultiplier::GetValue(Action* action) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return 1.0f; + + Creature* alarCreature = alar->ToCreature(); + if (!alarCreature || alarCreature->GetReactState() != REACT_PASSIVE) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float AlarPhase2NoTankingIfArmorMeltedMultiplier::GetValue(Action* action) +{ + if (!bot->HasAura(SPELL_MELT_ARMOR)) + return 1.0f; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar || bot->GetTarget() != alar->GetGUID()) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Void Reaver + +float VoidReaverMaintainPositionsMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "void reaver")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// High Astromancer Solarian + +float HighAstromancerSolarianMaintainPositionMultiplier::GetValue(Action* action) +{ + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + if (!astromancer || astromancer->HasAura(SPELL_SOLARIAN_TRANSFORM)) + return 1.0f; + + if (botAI->IsRanged(bot) && + (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action))) + return 0.0f; + + if (!bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER)) + return 1.0f; + + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + + return 1.0f; +} + +float HighAstromancerSolarianDisableTankAssistMultiplier::GetValue(Action* action) +{ + if (bot->GetVictim() == nullptr) + return 1.0f; + + if (!botAI->IsTank(bot)) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "solarium priest")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Kael'thas Sunstrider + +float KaelthasSunstriderWaitForDpsMultiplier::GetValue(Action* action) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || kaelAI->GetPhase() != PHASE_SINGLE_ADVISOR) + return 1.0f; + + if (dynamic_cast(action)) + return 1.0f; + + const time_t now = std::time(nullptr); + constexpr uint8 dpsWaitSeconds = 10; + + auto it = advisorDpsWaitTimer.find(kaelthas->GetMap()->GetInstanceId()); + if (it == advisorDpsWaitTimer.end() || (now - it->second) < dpsWaitSeconds) + { + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + + auto isAdvisorActive = [](Unit* advisor) + { + return advisor && !advisor->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !advisor->HasAura(SPELL_PERMANENT_FEIGN_DEATH); + }; + + if ((isAdvisorActive(sanguinar) && botAI->IsMainTank(bot)) || + (isAdvisorActive(telonicus) && botAI->IsAssistTankOfIndex(bot, 0, true)) || + (isAdvisorActive(capernian) && (botAI->IsMainTank(bot) || GetCapernianTank(bot) == bot))) + return 1.0f; + + bool shouldHoldDps = + (isAdvisorActive(sanguinar) && !botAI->IsMainTank(bot)) || + (isAdvisorActive(telonicus) && !botAI->IsAssistTankOfIndex(bot, 0, true)) || + (isAdvisorActive(capernian) && !botAI->IsMainTank(bot) && GetCapernianTank(bot) != bot); + + if (shouldHoldDps && + (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action)))) + return 0.0f; + } + + return 1.0f; +} + +float KaelthasSunstriderKiteThaladredMultiplier::GetValue(Action* action) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return 1.0f; + + if (botAI->IsTank(bot) && kaelAI->GetPhase() == PHASE_ALL_ADVISORS) + return 1.0f; + + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + if (!thaladred || thaladred->GetVictim() != bot || + thaladred->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float KaelthasSunstriderControlMisdirectionMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_HUNTER) + return 1.0f; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || kaelAI->GetPhase() == PHASE_FINAL) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float KaelthasSunstriderKeepDistanceFromCapernianMultiplier::GetValue(Action* action) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || kaelAI->GetPhase() != PHASE_SINGLE_ADVISOR) + return 1.0f; + + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (!capernian || capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || + capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float KaelthasSunstriderManageWeaponTankingMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return 1.0f; + + if (kaelAI->GetPhase() != PHASE_WEAPONS && + dynamic_cast(action)) + return 0.0f; + + if (!botAI->IsMainTank(bot)) + return 1.0f; + + // Try to keep main tank from grabbing aggro on any weapon other than the axe + if (kaelAI->GetPhase() == PHASE_WEAPONS && + (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action))) + return 0.0f; + + return 1.0f; +} + +float KaelthasSunstriderDisableAdvisorTankAssistMultiplier::GetValue(Action* action) +{ + if (bot->GetVictim() == nullptr || !botAI->IsTank(bot)) + return 1.0f; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return 1.0f; + + if (kaelAI->GetPhase() != PHASE_SINGLE_ADVISOR && + kaelAI->GetPhase() != PHASE_ALL_ADVISORS) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float KaelthasSunstriderDisableDisperseMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Bloodlust/Heroism and other major cooldowns should be used at the start of Phase 3 +float KaelthasSunstriderDelayCooldownsMultiplier::GetValue(Action* action) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || kaelAI->GetPhase() == PHASE_ALL_ADVISORS || + kaelAI->GetPhase() == PHASE_FINAL) + return 1.0f; + + if (bot->getClass() == CLASS_SHAMAN && + (dynamic_cast(action) || + dynamic_cast(action))) + return 0.0f; + + if (botAI->IsDps(bot) && + (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action))) + return 0.0f; + + return 1.0f; +} + +float KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier::GetValue(Action* action) +{ + if (!bot->HasAura(SPELL_GRAVITY_LAPSE)) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} diff --git a/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h new file mode 100644 index 00000000..d43d4a63 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h @@ -0,0 +1,150 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPMULTIPLIERS_H +#define _PLAYERBOT_RAIDTEMPESTKEEPMULTIPLIERS_H + +#include "Multiplier.h" + +// Al'ar + +class AlarMoveBetweenPlatformsMultiplier : public Multiplier +{ +public: + AlarMoveBetweenPlatformsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar move between platforms multiplier") {} + virtual float GetValue(Action* action); +}; + +class AlarDisableDisperseMultiplier : public Multiplier +{ +public: + AlarDisableDisperseMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar disable disperse multiplier") {} + virtual float GetValue(Action* action); +}; + +class AlarDisableTankAssistMultiplier : public Multiplier +{ +public: + AlarDisableTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar disable tank assist multiplier") {} + virtual float GetValue(Action* action); +}; + +class AlarStayAwayFromRebirthMultiplier : public Multiplier +{ +public: + AlarStayAwayFromRebirthMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar stay away from rebirth multiplier") {} + virtual float GetValue(Action* action); +}; + +class AlarPhase2NoTankingIfArmorMeltedMultiplier : public Multiplier +{ +public: + AlarPhase2NoTankingIfArmorMeltedMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar phase 2 no tanking if armor melted multiplier") {} + virtual float GetValue(Action* action); +}; + +// Void Reaver + +class VoidReaverMaintainPositionsMultiplier : public Multiplier +{ +public: + VoidReaverMaintainPositionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "void reaver maintain positions multiplier") {} + virtual float GetValue(Action* action); +}; + +// High Astromancer Solarian + +class HighAstromancerSolarianDisableTankAssistMultiplier : public Multiplier +{ +public: + HighAstromancerSolarianDisableTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "high astromancer solarian disable tank assist multiplier") {} + virtual float GetValue(Action* action); +}; + +class HighAstromancerSolarianMaintainPositionMultiplier : public Multiplier +{ +public: + HighAstromancerSolarianMaintainPositionMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "high astromancer solarian maintain position multiplier") {} + virtual float GetValue(Action* action); +}; + +// Kael'thas Sunstrider + +class KaelthasSunstriderWaitForDpsMultiplier : public Multiplier +{ +public: + KaelthasSunstriderWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider wait for dps multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderKiteThaladredMultiplier : public Multiplier +{ +public: + KaelthasSunstriderKiteThaladredMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider kite thaladred multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderControlMisdirectionMultiplier : public Multiplier +{ +public: + KaelthasSunstriderControlMisdirectionMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider control misdirection multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderKeepDistanceFromCapernianMultiplier : public Multiplier +{ +public: + KaelthasSunstriderKeepDistanceFromCapernianMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider keep distance from capernian multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderManageWeaponTankingMultiplier : public Multiplier +{ +public: + KaelthasSunstriderManageWeaponTankingMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider manage weapon tanking multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderDisableAdvisorTankAssistMultiplier : public Multiplier +{ +public: + KaelthasSunstriderDisableAdvisorTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider disable advisor tank assist multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderDisableDisperseMultiplier : public Multiplier +{ +public: + KaelthasSunstriderDisableDisperseMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider disable disperse multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderDelayCooldownsMultiplier : public Multiplier +{ +public: + KaelthasSunstriderDelayCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider delay cooldowns multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier : public Multiplier +{ +public: + KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider stay spread during gravity lapse multiplier") {} + virtual float GetValue(Action* action); +}; + +#endif diff --git a/src/Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h b/src/Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h new file mode 100644 index 00000000..50b2de1f --- /dev/null +++ b/src/Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h @@ -0,0 +1,289 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPACTIONCONTEXT_H +#define _PLAYERBOT_RAIDTEMPESTKEEPACTIONCONTEXT_H + +#include "RaidTempestKeepActions.h" +#include "NamedObjectContext.h" + +class RaidTempestKeepActionContext : public NamedObjectContext +{ +public: + RaidTempestKeepActionContext() + { + // Trash + creators["crimson hand centurion cast polymorph"] = + &RaidTempestKeepActionContext::crimson_hand_centurion_cast_polymorph; + + // Al'ar + creators["al'ar misdirect boss to main tank"] = + &RaidTempestKeepActionContext::alar_misdirect_boss_to_main_tank; + + creators["al'ar boss tanks move between platforms"] = + &RaidTempestKeepActionContext::alar_boss_tanks_move_between_platforms; + + creators["al'ar melee dps move between platforms"] = + &RaidTempestKeepActionContext::alar_melee_dps_move_between_platforms; + + creators["al'ar ranged and ember tank move under platforms"] = + &RaidTempestKeepActionContext::alar_ranged_and_ember_tank_move_under_platforms; + + creators["al'ar assist tanks pick up embers"] = + &RaidTempestKeepActionContext::alar_assist_tanks_pick_up_embers; + + creators["al'ar ranged dps prioritize embers"] = + &RaidTempestKeepActionContext::alar_ranged_dps_prioritize_embers; + + creators["al'ar jump from platform"] = + &RaidTempestKeepActionContext::alar_jump_from_platform; + + creators["al'ar move away from rebirth"] = + &RaidTempestKeepActionContext::alar_move_away_from_rebirth; + + creators["al'ar swap tanks on boss"] = + &RaidTempestKeepActionContext::alar_swap_tanks_on_boss; + + creators["al'ar avoid flame patches and dive bombs"] = + &RaidTempestKeepActionContext::alar_avoid_flame_patches_and_dive_bombs; + + creators["al'ar return to room center"] = + &RaidTempestKeepActionContext::alar_return_to_room_center; + + creators["al'ar manage phase tracker"] = + &RaidTempestKeepActionContext::alar_manage_phase_tracker; + + // Void Reaver + creators["void reaver tanks position boss"] = + &RaidTempestKeepActionContext::void_reaver_tanks_position_boss; + + creators["void reaver use aggro dump ability"] = + &RaidTempestKeepActionContext::void_reaver_use_aggro_dump_ability; + + creators["void reaver spread ranged"] = + &RaidTempestKeepActionContext::void_reaver_spread_ranged; + + creators["void reaver avoid arcane orb"] = + &RaidTempestKeepActionContext::void_reaver_avoid_arcane_orb; + + creators["void reaver erase trackers"] = + &RaidTempestKeepActionContext::void_reaver_erase_trackers; + + // High Astromancer Solarian + creators["high astromancer solarian ranged leave space for melee"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_ranged_leave_space_for_melee; + + creators["high astromancer solarian move away from group"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_move_away_from_group; + + creators["high astromancer solarian stack for aoe"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_stack_for_aoe; + + creators["high astromancer solarian target solarium priests"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_target_solarium_priests; + + creators["high astromancer solarian cast fear ward on main tank"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_cast_fear_ward_on_main_tank; + + // Kael'thas Sunstrider + creators["kael'thas sunstrider kite thaladred"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_kite_thaladred; + + creators["kael'thas sunstrider misdirect advisors to tanks"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_misdirect_advisors_to_tanks; + + creators["kael'thas sunstrider main tank position sanguinar"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_main_tank_position_sanguinar; + + creators["kael'thas sunstrider cast fear ward on sanguinar tank"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_cast_fear_ward_on_sanguinar_tank; + + creators["kael'thas sunstrider warlock tank position capernian"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_warlock_tank_position_capernian; + + creators["kael'thas sunstrider spread and move away from capernian"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_spread_and_move_away_from_capernian; + + creators["kael'thas sunstrider first assist tank position telonicus"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_first_assist_tank_position_telonicus; + + creators["kael'thas sunstrider handle advisor roles in phase 3"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_handle_advisor_roles_in_phase_3; + + creators["kael'thas sunstrider assign advisor dps priority"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_assign_advisor_dps_priority; + + creators["kael'thas sunstrider manage advisor dps timer"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_manage_advisor_dps_timer; + + creators["kael'thas sunstrider assign legendary weapon dps priority"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_assign_legendary_weapon_dps_priority; + + creators["kael'thas sunstrider main tank move devastation away"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_main_tank_move_devastation_away; + + creators["kael'thas sunstrider loot legendary weapons"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_loot_legendary_weapons; + + creators["kael'thas sunstrider use legendary weapons"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_use_legendary_weapons; + + creators["kael'thas sunstrider reequip gear"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_reequip_gear; + + creators["kael'thas sunstrider main tank position boss"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_main_tank_position_boss; + + creators["kael'thas sunstrider avoid flame strike"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_avoid_flame_strike; + + creators["kael'thas sunstrider handle phoenixes and eggs"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_handle_phoenixes_and_eggs; + + creators["kael'thas sunstrider break mind control"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_break_mind_control; + + creators["kael'thas sunstrider break through shock barrier"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_break_through_shock_barrier; + + creators["kael'thas sunstrider spread out in midair"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_spread_out_in_midair; + } + +private: + // Trash + static Action* crimson_hand_centurion_cast_polymorph( + PlayerbotAI* botAI) { return new CrimsonHandCenturionCastPolymorphAction(botAI); } + + // Al'ar + static Action* alar_misdirect_boss_to_main_tank( + PlayerbotAI* botAI) { return new AlarMisdirectBossToMainTankAction(botAI); } + + static Action* alar_boss_tanks_move_between_platforms( + PlayerbotAI* botAI) { return new AlarBossTanksMoveBetweenPlatformsAction(botAI); } + + static Action* alar_melee_dps_move_between_platforms( + PlayerbotAI* botAI) { return new AlarMeleeDpsMoveBetweenPlatformsAction(botAI); } + + static Action* alar_ranged_and_ember_tank_move_under_platforms( + PlayerbotAI* botAI) { return new AlarRangedAndEmberTankMoveUnderPlatformsAction(botAI); } + + static Action* alar_assist_tanks_pick_up_embers( + PlayerbotAI* botAI) { return new AlarAssistTanksPickUpEmbersAction(botAI); } + + static Action* alar_ranged_dps_prioritize_embers( + PlayerbotAI* botAI) { return new AlarRangedDpsPrioritizeEmbersAction(botAI); } + + static Action* alar_jump_from_platform( + PlayerbotAI* botAI) { return new AlarJumpFromPlatformAction(botAI); } + + static Action* alar_move_away_from_rebirth( + PlayerbotAI* botAI) { return new AlarMoveAwayFromRebirthAction(botAI); } + + static Action* alar_swap_tanks_on_boss( + PlayerbotAI* botAI) { return new AlarSwapTanksOnBossAction(botAI); } + + static Action* alar_avoid_flame_patches_and_dive_bombs( + PlayerbotAI* botAI) { return new AlarAvoidFlamePatchesAndDiveBombsAction(botAI); } + + static Action* alar_return_to_room_center( + PlayerbotAI* botAI) { return new AlarReturnToRoomCenterAction(botAI); } + + static Action* alar_manage_phase_tracker( + PlayerbotAI* botAI) { return new AlarManagePhaseTrackerAction(botAI); } + + // Void Reaver + static Action* void_reaver_tanks_position_boss( + PlayerbotAI* botAI) { return new VoidReaverTanksPositionBossAction(botAI); } + + static Action* void_reaver_use_aggro_dump_ability( + PlayerbotAI* botAI) { return new VoidReaverUseAggroDumpAbilityAction(botAI); } + + static Action* void_reaver_spread_ranged( + PlayerbotAI* botAI) { return new VoidReaverSpreadRangedAction(botAI); } + + static Action* void_reaver_avoid_arcane_orb( + PlayerbotAI* botAI) { return new VoidReaverAvoidArcaneOrbAction(botAI); } + + static Action* void_reaver_erase_trackers( + PlayerbotAI* botAI) { return new VoidReaverEraseTrackersAction(botAI); } + + // High Astromancer Solarian + static Action* high_astromancer_solarian_ranged_leave_space_for_melee( + PlayerbotAI* botAI) { return new HighAstromancerSolarianRangedLeaveSpaceForMeleeAction(botAI); } + + static Action* high_astromancer_solarian_move_away_from_group( + PlayerbotAI* botAI) { return new HighAstromancerSolarianMoveAwayFromGroupAction(botAI); } + + static Action* high_astromancer_solarian_stack_for_aoe( + PlayerbotAI* botAI) { return new HighAstromancerSolarianStackForAoeAction(botAI); } + + static Action* high_astromancer_solarian_target_solarium_priests( + PlayerbotAI* botAI) { return new HighAstromancerSolarianTargetSolariumPriestsAction(botAI); } + + static Action* high_astromancer_solarian_cast_fear_ward_on_main_tank( + PlayerbotAI* botAI) { return new HighAstromancerSolarianCastFearWardOnMainTankAction(botAI); } + + // Kael'thas Sunstrider + static Action* kaelthas_sunstrider_kite_thaladred( + PlayerbotAI* botAI) { return new KaelthasSunstriderKiteThaladredAction(botAI); } + + static Action* kaelthas_sunstrider_misdirect_advisors_to_tanks( + PlayerbotAI* botAI) { return new KaelthasSunstriderMisdirectAdvisorsToTanksAction(botAI); } + + static Action* kaelthas_sunstrider_main_tank_position_sanguinar( + PlayerbotAI* botAI) { return new KaelthasSunstriderMainTankPositionSanguinarAction(botAI); } + + static Action* kaelthas_sunstrider_cast_fear_ward_on_sanguinar_tank( + PlayerbotAI* botAI) { return new KaelthasSunstriderCastFearWardOnSanguinarTankAction(botAI); } + + static Action* kaelthas_sunstrider_warlock_tank_position_capernian( + PlayerbotAI* botAI) { return new KaelthasSunstriderWarlockTankPositionCapernianAction(botAI); } + + static Action* kaelthas_sunstrider_spread_and_move_away_from_capernian( + PlayerbotAI* botAI) { return new KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction(botAI); } + + static Action* kaelthas_sunstrider_first_assist_tank_position_telonicus( + PlayerbotAI* botAI) { return new KaelthasSunstriderFirstAssistTankPositionTelonicusAction(botAI); } + + static Action* kaelthas_sunstrider_handle_advisor_roles_in_phase_3( + PlayerbotAI* botAI) { return new KaelthasSunstriderHandleAdvisorRolesInPhase3Action(botAI); } + + static Action* kaelthas_sunstrider_assign_advisor_dps_priority( + PlayerbotAI* botAI) { return new KaelthasSunstriderAssignAdvisorDpsPriorityAction(botAI); } + + static Action* kaelthas_sunstrider_manage_advisor_dps_timer( + PlayerbotAI* botAI) { return new KaelthasSunstriderManageAdvisorDpsTimerAction(botAI); } + + static Action* kaelthas_sunstrider_assign_legendary_weapon_dps_priority( + PlayerbotAI* botAI) { return new KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction(botAI); } + + static Action* kaelthas_sunstrider_main_tank_move_devastation_away( + PlayerbotAI* botAI) { return new KaelthasSunstriderMoveDevastationAwayAction(botAI); } + + static Action* kaelthas_sunstrider_loot_legendary_weapons( + PlayerbotAI* botAI) { return new KaelthasSunstriderLootLegendaryWeaponsAction(botAI); } + + static Action* kaelthas_sunstrider_use_legendary_weapons( + PlayerbotAI* botAI) { return new KaelthasSunstriderUseLegendaryWeaponsAction(botAI); } + + static Action* kaelthas_sunstrider_reequip_gear( + PlayerbotAI* botAI) { return new KaelthasSunstriderReequipGearAction(botAI); } + + static Action* kaelthas_sunstrider_main_tank_position_boss( + PlayerbotAI* botAI) { return new KaelthasSunstriderMainTankPositionBossAction(botAI); } + + static Action* kaelthas_sunstrider_avoid_flame_strike( + PlayerbotAI* botAI) { return new KaelthasSunstriderAvoidFlameStrikeAction(botAI); } + + static Action* kaelthas_sunstrider_handle_phoenixes_and_eggs( + PlayerbotAI* botAI) { return new KaelthasSunstriderHandlePhoenixesAndEggsAction(botAI); } + + static Action* kaelthas_sunstrider_break_mind_control( + PlayerbotAI* botAI) { return new KaelthasSunstriderBreakMindControlAction(botAI); } + + static Action* kaelthas_sunstrider_break_through_shock_barrier( + PlayerbotAI* botAI) { return new KaelthasSunstriderBreakThroughShockBarrierAction(botAI); } + + static Action* kaelthas_sunstrider_spread_out_in_midair( + PlayerbotAI* botAI) { return new KaelthasSunstriderSpreadOutInMidairAction(botAI); } +}; + +#endif diff --git a/src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h b/src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h new file mode 100644 index 00000000..c6b4922d --- /dev/null +++ b/src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h @@ -0,0 +1,265 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPTRIGGERCONTEXT_H +#define _PLAYERBOT_RAIDTEMPESTKEEPTRIGGERCONTEXT_H + +#include "RaidTempestKeepTriggers.h" +#include "AiObjectContext.h" + +class RaidTempestKeepTriggerContext : public NamedObjectContext +{ +public: + RaidTempestKeepTriggerContext() + { + // Trash + creators["crimson hand centurion casts arcane volley"] = + &RaidTempestKeepTriggerContext::crimson_hand_centurion_casts_arcane_volley; + + // Al'ar + creators["al'ar pulling boss"] = + &RaidTempestKeepTriggerContext::alar_pulling_boss; + + creators["al'ar boss is flying between platforms"] = + &RaidTempestKeepTriggerContext::alar_boss_is_flying_between_platforms; + + creators["al'ar embers of al'ar explode upon death"] = + &RaidTempestKeepTriggerContext::alar_embers_of_alar_explode_upon_death; + + creators["al'ar killing embers of al'ar damages boss"] = + &RaidTempestKeepTriggerContext::alar_killing_embers_of_alar_damages_boss; + + creators["al'ar incoming flame quills"] = + &RaidTempestKeepTriggerContext::alar_incoming_flame_quills; + + creators["al'ar rising from the ashes"] = + &RaidTempestKeepTriggerContext::alar_rising_from_the_ashes; + + creators["al'ar everything is on fire in phase 2"] = + &RaidTempestKeepTriggerContext::alar_everything_is_on_fire_in_phase_2; + + creators["al'ar phase 2 encounter is at room center"] = + &RaidTempestKeepTriggerContext::alar_phase_2_encounter_is_at_room_center; + + creators["al'ar strategy changes between phases"] = + &RaidTempestKeepTriggerContext::alar_strategy_changes_between_phases; + + // Void Reaver + creators["void reaver boss casts pounding"] = + &RaidTempestKeepTriggerContext::void_reaver_boss_casts_pounding; + + creators["void reaver knock away reduces tank aggro"] = + &RaidTempestKeepTriggerContext::void_reaver_knock_away_reduces_tank_aggro; + + creators["void reaver boss launches arcane orbs"] = + &RaidTempestKeepTriggerContext::void_reaver_boss_launches_arcane_orbs; + + creators["void reaver arcane orb is incoming"] = + &RaidTempestKeepTriggerContext::void_reaver_arcane_orb_is_incoming; + + creators["void reaver bot is not in combat"] = + &RaidTempestKeepTriggerContext::void_reaver_bot_is_not_in_combat; + + // High Astromancer Solarian + creators["high astromancer solarian boss casts wrath of the astromancer"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_boss_casts_wrath_of_the_astromancer; + + creators["high astromancer solarian bot has wrath of the astromancer"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_bot_has_wrath_of_the_astromancer; + + creators["high astromancer solarian boss has vanished"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_boss_has_vanished; + + creators["high astromancer solarian solarium priests spawned"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_solarium_priests_spawned; + + creators["high astromancer solarian boss casts psychic scream"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_boss_casts_psychic_scream; + + // Kael'thas Sunstrider + creators["kael'thas sunstrider thaladred is fixated on bot"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_thaladred_is_fixated_on_bot; + + creators["kael'thas sunstrider pulling tankable advisors"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_pulling_tankable_advisors; + + creators["kael'thas sunstrider sanguinar engaged by main tank"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_sanguinar_engaged_by_main_tank; + + creators["kael'thas sunstrider sanguinar casts bellowing roar"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_sanguinar_casts_bellowing_roar; + + creators["kael'thas sunstrider capernian should be tanked by a warlock"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_capernian_should_be_tanked_by_a_warlock; + + creators["kael'thas sunstrider capernian casts arcane burst and conflagration"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_capernian_casts_arcane_burst_and_conflagration; + + creators["kael'thas sunstrider telonicus engaged by first assist tank"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_telonicus_engaged_by_first_assist_tank; + + creators["kael'thas sunstrider bots have specific roles in phase 3"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_bots_have_specific_roles_in_phase_3; + + creators["kael'thas sunstrider determining advisor kill order"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_determining_advisor_kill_order; + + creators["kael'thas sunstrider waiting for tanks to get aggro on advisors"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_waiting_for_tanks_to_get_aggro_on_advisors; + + creators["kael'thas sunstrider legendary weapons are alive"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_are_alive; + + creators["kael'thas sunstrider legendary axe casts whirlwind"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_axe_casts_whirlwind; + + creators["kael'thas sunstrider legendary weapons are dead and lootable"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_are_dead_and_lootable; + + creators["kael'thas sunstrider legendary weapons are equipped"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_are_equipped; + + creators["kael'thas sunstrider legendary weapons were lost"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_were_lost; + + creators["kael'thas sunstrider boss has entered the fight"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_boss_has_entered_the_fight; + + creators["kael'thas sunstrider raid member is mind controlled"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_raid_member_is_mind_controlled; + + creators["kael'thas sunstrider phoenixes and eggs are spawning"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_phoenixes_and_eggs_are_spawning; + + creators["kael'thas sunstrider boss is casting pyroblast"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_boss_is_casting_pyroblast; + + creators["kael'thas sunstrider boss is manipulating gravity"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_boss_is_manipulating_gravity; + } + +private: + // Trash + static Trigger* crimson_hand_centurion_casts_arcane_volley( + PlayerbotAI* botAI) { return new CrimsonHandCenturionCastsArcaneVolleyTrigger(botAI); } + + // Al'ar + static Trigger* alar_pulling_boss( + PlayerbotAI* botAI) { return new AlarPullingBossTrigger(botAI); } + + static Trigger* alar_boss_is_flying_between_platforms( + PlayerbotAI* botAI) { return new AlarBossIsFlyingBetweenPlatformsTrigger(botAI); } + + static Trigger* alar_embers_of_alar_explode_upon_death( + PlayerbotAI* botAI) { return new AlarEmbersOfAlarExplodeUponDeathTrigger(botAI); } + + static Trigger* alar_killing_embers_of_alar_damages_boss( + PlayerbotAI* botAI) { return new AlarKillingEmbersOfAlarDamagesBossTrigger(botAI); } + + static Trigger* alar_incoming_flame_quills( + PlayerbotAI* botAI) { return new AlarIncomingFlameQuillsTrigger(botAI); } + + static Trigger* alar_rising_from_the_ashes( + PlayerbotAI* botAI) { return new AlarRisingFromTheAshesTrigger(botAI); } + + static Trigger* alar_everything_is_on_fire_in_phase_2( + PlayerbotAI* botAI) { return new AlarEverythingIsOnFireInPhase2Trigger(botAI); } + + static Trigger* alar_phase_2_encounter_is_at_room_center( + PlayerbotAI* botAI) { return new AlarPhase2EncounterIsAtRoomCenterTrigger(botAI); } + + static Trigger* alar_strategy_changes_between_phases( + PlayerbotAI* botAI) { return new AlarStrategyChangesBetweenPhasesTrigger(botAI); } + + // Void Reaver + static Trigger* void_reaver_boss_casts_pounding( + PlayerbotAI* botAI) { return new VoidReaverBossCastsPoundingTrigger(botAI); } + + static Trigger* void_reaver_knock_away_reduces_tank_aggro( + PlayerbotAI* botAI) { return new VoidReaverKnockAwayReducesTankAggroTrigger(botAI); } + + static Trigger* void_reaver_boss_launches_arcane_orbs( + PlayerbotAI* botAI) { return new VoidReaverBossLaunchesArcaneOrbsTrigger(botAI); } + + static Trigger* void_reaver_arcane_orb_is_incoming( + PlayerbotAI* botAI) { return new VoidReaverArcaneOrbIsIncomingTrigger(botAI); } + + static Trigger* void_reaver_bot_is_not_in_combat( + PlayerbotAI* botAI) { return new VoidReaverBotIsNotInCombatTrigger(botAI); } + + // High Astromancer Solarian + static Trigger* high_astromancer_solarian_boss_casts_wrath_of_the_astromancer( + PlayerbotAI* botAI) { return new HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger(botAI); } + + static Trigger* high_astromancer_solarian_bot_has_wrath_of_the_astromancer( + PlayerbotAI* botAI) { return new HighAstromancerSolarianBotHasWrathOfTheAstromancerTrigger(botAI); } + + static Trigger* high_astromancer_solarian_boss_has_vanished( + PlayerbotAI* botAI) { return new HighAstromancerSolarianBossHasVanishedTrigger(botAI); } + + static Trigger* high_astromancer_solarian_solarium_priests_spawned( + PlayerbotAI* botAI) { return new HighAstromancerSolarianSolariumPriestsSpawnedTrigger(botAI); } + + static Trigger* high_astromancer_solarian_boss_casts_psychic_scream( + PlayerbotAI* botAI) { return new HighAstromancerSolarianBossCastsPsychicScreamTrigger(botAI); } + + // Kael'thas Sunstrider + static Trigger* kaelthas_sunstrider_thaladred_is_fixated_on_bot( + PlayerbotAI* botAI) { return new KaelthasSunstriderThaladredIsFixatedOnBotTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_pulling_tankable_advisors( + PlayerbotAI* botAI) { return new KaelthasSunstriderPullingTankableAdvisorsTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_sanguinar_engaged_by_main_tank( + PlayerbotAI* botAI) { return new KaelthasSunstriderSanguinarEngagedByMainTankTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_sanguinar_casts_bellowing_roar( + PlayerbotAI* botAI) { return new KaelthasSunstriderSanguinarCastsBellowingRoarTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_capernian_should_be_tanked_by_a_warlock( + PlayerbotAI* botAI) { return new KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_capernian_casts_arcane_burst_and_conflagration( + PlayerbotAI* botAI) { return new KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_telonicus_engaged_by_first_assist_tank( + PlayerbotAI* botAI) { return new KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_bots_have_specific_roles_in_phase_3( + PlayerbotAI* botAI) { return new KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger(botAI); } + + static Trigger* kaelthas_sunstrider_determining_advisor_kill_order( + PlayerbotAI* botAI) { return new KaelthasSunstriderDeterminingAdvisorKillOrderTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_waiting_for_tanks_to_get_aggro_on_advisors( + PlayerbotAI* botAI) { return new KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_legendary_weapons_are_alive( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryWeaponsAreAliveTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_legendary_axe_casts_whirlwind( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_legendary_weapons_are_dead_and_lootable( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_legendary_weapons_are_equipped( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_legendary_weapons_were_lost( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryWeaponsWereLostTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_boss_has_entered_the_fight( + PlayerbotAI* botAI) { return new KaelthasSunstriderBossHasEnteredTheFightTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_raid_member_is_mind_controlled( + PlayerbotAI* botAI) { return new KaelthasSunstriderRaidMemberIsMindControlledTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_phoenixes_and_eggs_are_spawning( + PlayerbotAI* botAI) { return new KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_boss_is_casting_pyroblast( + PlayerbotAI* botAI) { return new KaelthasSunstriderBossIsCastingPyroblastTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_boss_is_manipulating_gravity( + PlayerbotAI* botAI) { return new KaelthasSunstriderBossIsManipulatingGravityTrigger(botAI); } +}; + +#endif diff --git a/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp b/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp new file mode 100644 index 00000000..74950c3a --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp @@ -0,0 +1,162 @@ +#include "RaidTempestKeepStrategy.h" +#include "RaidTempestKeepMultipliers.h" + +void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) +{ + // Trash + triggers.push_back(new TriggerNode("crimson hand centurion casts arcane volley", { + NextAction("crimson hand centurion cast polymorph", ACTION_RAID + 1) })); + + // Al'ar + triggers.push_back(new TriggerNode("al'ar pulling boss", { + NextAction("al'ar misdirect boss to main tank", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("al'ar boss is flying between platforms", { + NextAction("al'ar boss tanks move between platforms", ACTION_RAID + 1), + NextAction("al'ar melee dps move between platforms", ACTION_RAID + 1), + NextAction("al'ar ranged and ember tank move under platforms", ACTION_RAID + 4) })); + + triggers.push_back(new TriggerNode("al'ar embers of al'ar explode upon death", { + NextAction("al'ar assist tanks pick up embers", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("al'ar killing embers of al'ar damages boss", { + NextAction("al'ar ranged dps prioritize embers", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("al'ar incoming flame quills", { + NextAction("al'ar jump from platform", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("al'ar rising from the ashes", { + NextAction("al'ar move away from rebirth", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("al'ar everything is on fire in phase 2", { + NextAction("al'ar swap tanks on boss", ACTION_EMERGENCY + 2), + NextAction("al'ar avoid flame patches and dive bombs", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("al'ar phase 2 encounter is at room center", { + NextAction("al'ar return to room center", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("al'ar strategy changes between phases", { + NextAction("al'ar manage phase tracker", ACTION_EMERGENCY + 10) })); + + // Void Reaver + triggers.push_back(new TriggerNode("void reaver boss casts pounding", { + NextAction("void reaver tanks position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("void reaver knock away reduces tank aggro", { + NextAction("void reaver use aggro dump ability", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("void reaver boss launches arcane orbs", { + NextAction("void reaver spread ranged", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("void reaver arcane orb is incoming", { + NextAction("void reaver avoid arcane orb", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("void reaver bot is not in combat", { + NextAction("void reaver erase trackers", ACTION_EMERGENCY + 11) })); + + // High Astromancer Solarian + triggers.push_back(new TriggerNode("high astromancer solarian boss casts wrath of the astromancer", { + NextAction("high astromancer solarian ranged leave space for melee", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("high astromancer solarian bot has wrath of the astromancer", { + NextAction("high astromancer solarian move away from group", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("high astromancer solarian boss has vanished", { + NextAction("high astromancer solarian stack for aoe", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("high astromancer solarian solarium priests spawned", { + NextAction("high astromancer solarian target solarium priests", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("high astromancer solarian boss casts psychic scream", { + NextAction("high astromancer solarian cast fear ward on main tank", ACTION_RAID + 2) })); + + // Kael'thas Sunstrider + triggers.push_back(new TriggerNode("kael'thas sunstrider thaladred is fixated on bot", { + NextAction("kael'thas sunstrider kite thaladred", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider pulling tankable advisors", { + NextAction("kael'thas sunstrider misdirect advisors to tanks", ACTION_EMERGENCY + 2) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider sanguinar engaged by main tank", { + NextAction("kael'thas sunstrider main tank position sanguinar", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider sanguinar casts bellowing roar", { + NextAction("kael'thas sunstrider cast fear ward on sanguinar tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider capernian should be tanked by a warlock", { + NextAction("kael'thas sunstrider warlock tank position capernian", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider capernian casts arcane burst and conflagration", { + NextAction("kael'thas sunstrider spread and move away from capernian", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider telonicus engaged by first assist tank", { + NextAction("kael'thas sunstrider first assist tank position telonicus", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider bots have specific roles in phase 3", { + NextAction("kael'thas sunstrider handle advisor roles in phase 3", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider determining advisor kill order", { + NextAction("kael'thas sunstrider assign advisor dps priority", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider waiting for tanks to get aggro on advisors", { + NextAction("kael'thas sunstrider manage advisor dps timer", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are alive", { + NextAction("kael'thas sunstrider assign legendary weapon dps priority", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary axe casts whirlwind", { + NextAction("kael'thas sunstrider main tank move devastation away", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are dead and lootable", { + NextAction("kael'thas sunstrider loot legendary weapons", ACTION_RAID) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are equipped", { + NextAction("kael'thas sunstrider use legendary weapons", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons were lost", { + NextAction("kael'thas sunstrider reequip gear", ACTION_EMERGENCY + 11) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider boss has entered the fight", { + NextAction("kael'thas sunstrider main tank position boss", ACTION_RAID + 1), + NextAction("kael'thas sunstrider avoid flame strike", ACTION_EMERGENCY + 8) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider phoenixes and eggs are spawning", { + NextAction("kael'thas sunstrider handle phoenixes and eggs", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider raid member is mind controlled", { + NextAction("kael'thas sunstrider break mind control", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider boss is casting pyroblast", { + NextAction("kael'thas sunstrider break through shock barrier", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider boss is manipulating gravity", { + NextAction("kael'thas sunstrider spread out in midair", ACTION_EMERGENCY + 1) })); +} + +void RaidTempestKeepStrategy::InitMultipliers(std::vector& multipliers) +{ + // Alar + multipliers.push_back(new AlarMoveBetweenPlatformsMultiplier(botAI)); + multipliers.push_back(new AlarDisableDisperseMultiplier(botAI)); + multipliers.push_back(new AlarDisableTankAssistMultiplier(botAI)); + multipliers.push_back(new AlarStayAwayFromRebirthMultiplier(botAI)); + multipliers.push_back(new AlarPhase2NoTankingIfArmorMeltedMultiplier(botAI)); + + // Void Reaver + multipliers.push_back(new VoidReaverMaintainPositionsMultiplier(botAI)); + + // High Astromancer Solarian + multipliers.push_back(new HighAstromancerSolarianDisableTankAssistMultiplier(botAI)); + multipliers.push_back(new HighAstromancerSolarianMaintainPositionMultiplier(botAI)); + + // Kael'thas Sunstrider + multipliers.push_back(new KaelthasSunstriderWaitForDpsMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderKiteThaladredMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderControlMisdirectionMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderKeepDistanceFromCapernianMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderManageWeaponTankingMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderDisableAdvisorTankAssistMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderDisableDisperseMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderDelayCooldownsMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier(botAI)); +} diff --git a/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.h b/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.h new file mode 100644 index 00000000..77fd29c3 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.h @@ -0,0 +1,18 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPSTRATEGY_H_ +#define _PLAYERBOT_RAIDTEMPESTKEEPSTRATEGY_H_ + +#include "Strategy.h" +#include "Multiplier.h" + +class RaidTempestKeepStrategy : public Strategy +{ +public: + RaidTempestKeepStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + std::string const getName() override { return "tempestkeep"; } + + void InitTriggers(std::vector& triggers) override; + void InitMultipliers(std::vector& multipliers) override; +}; + +#endif diff --git a/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp new file mode 100644 index 00000000..b0b399b1 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp @@ -0,0 +1,527 @@ +#include "RaidTempestKeepTriggers.h" +#include "RaidTempestKeepHelpers.h" +#include "RaidTempestKeepActions.h" +#include "RaidTempestKeepKaelthasBossAI.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +using namespace TempestKeepHelpers; + +// Trash + +bool CrimsonHandCenturionCastsArcaneVolleyTrigger::IsActive() +{ + return bot->getClass() == CLASS_MAGE && + AI_VALUE2(Unit*, "find target", "crimson hand centurion"); +} + +// Al'ar + +bool AlarPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + return alar && alar->GetHealthPct() > 98.0f; +} + +bool AlarBossIsFlyingBetweenPlatformsTrigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar || !alar->IsVisible()) + return false; + + if (isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return false; + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + return locationIndex != POINT_QUILL_OR_DIVE_IDX && + locationIndex != POINT_MIDDLE_IDX; +} + +bool AlarEmbersOfAlarExplodeUponDeathTrigger::IsActive() +{ + return botAI->IsTank(bot) && AI_VALUE2(Unit*, "find target", "ember of al'ar"); +} + +bool AlarKillingEmbersOfAlarDamagesBossTrigger::IsActive() +{ + return botAI->IsRangedDps(bot) && + AI_VALUE2(Unit*, "find target", "ember of al'ar"); +} + +bool AlarIncomingFlameQuillsTrigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar || isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return false; + + Position dest; + return GetAlarCurrentLocationIndex(alar) == POINT_QUILL_OR_DIVE_IDX || + GetAlarDestinationLocationIndex(alar, dest) == POINT_QUILL_OR_DIVE_IDX; +} + +bool AlarRisingFromTheAshesTrigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return false; + + Creature* alarCreature = alar->ToCreature(); + return alarCreature && alarCreature->GetReactState() == REACT_PASSIVE; +} + +bool AlarEverythingIsOnFireInPhase2Trigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + return alar && isAlarInPhase2[alar->GetMap()->GetInstanceId()]; +} + +bool AlarPhase2EncounterIsAtRoomCenterTrigger::IsActive() +{ + if (bot->GetVictim()) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar || !isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return false; + + Creature* alarCreature = alar->ToCreature(); + if (alarCreature && alarCreature->GetReactState() == REACT_PASSIVE) + return false; + + Position dest; + return GetAlarCurrentLocationIndex(alar) != POINT_QUILL_OR_DIVE_IDX && + GetAlarDestinationLocationIndex(alar, dest) != POINT_QUILL_OR_DIVE_IDX; +} + +bool AlarStrategyChangesBetweenPhasesTrigger::IsActive() +{ + return botAI->IsDps(bot) && IsMechanicTrackerBot(botAI, bot, TEMPEST_KEEP_MAP_ID) && + AI_VALUE2(Unit*, "find target", "al'ar"); +} + +// Void Reaver + +bool VoidReaverBossCastsPoundingTrigger::IsActive() +{ + if (!botAI->IsTank(bot)) + return false; + + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + return voidReaver && voidReaver->GetVictim() == bot; +} + +bool VoidReaverKnockAwayReducesTankAggroTrigger::IsActive() +{ + if (botAI->IsTank(bot)) + return false; + + if (bot->getClass() == CLASS_DEATH_KNIGHT || + bot->getClass() == CLASS_DRUID || + bot->getClass() == CLASS_SHAMAN || + bot->getClass() == CLASS_WARRIOR) + return false; + + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + return voidReaver && voidReaver->GetVictim() == bot; +} + +bool VoidReaverBossLaunchesArcaneOrbsTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + return voidReaver && voidReaver->GetVictim() != bot; +} + +bool VoidReaverArcaneOrbIsIncomingTrigger::IsActive() +{ + if (botAI->IsTank(bot)) + return false; + + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (!voidReaver || voidReaver->GetVictim() == bot) + return false; + + auto it = voidReaverArcaneOrbs.find(bot->GetMap()->GetInstanceId()); + if (it == voidReaverArcaneOrbs.end() || it->second.empty()) + return false; + + uint32 currentTime = getMSTime(); + constexpr uint32 orbDuration = 7000; + constexpr float safeDistance = 22.0f; + + for (auto const& orb : it->second) + { + if (getMSTimeDiff(orb.castTime, currentTime) <= orbDuration && + bot->GetExactDist2d(orb.destination.GetPositionX(), + orb.destination.GetPositionY()) < safeDistance) + return true; + } + + return false; +} + +bool VoidReaverBotIsNotInCombatTrigger::IsActive() +{ + return !bot->IsInCombat(); +} + +// High Astromancer Solarian + +bool HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger::IsActive() +{ + if (bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER)) + return false; + + if (!botAI->IsRanged(bot)) + return false; + + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + return astromancer && !astromancer->HasAura(SPELL_SOLARIAN_TRANSFORM); +} + +bool HighAstromancerSolarianBotHasWrathOfTheAstromancerTrigger::IsActive() +{ + return bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER); +} + +bool HighAstromancerSolarianBossHasVanishedTrigger::IsActive() +{ + if (bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER)) + return false; + + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + if (!astromancer) + return false; + + Creature* astromancerCreature = astromancer->ToCreature(); + return astromancerCreature && + astromancerCreature->GetReactState() == REACT_PASSIVE; +} + +bool HighAstromancerSolarianSolariumPriestsSpawnedTrigger::IsActive() +{ + return botAI->IsMelee(bot) && AI_VALUE2(Unit*, "find target", "solarium priest"); +} + +bool HighAstromancerSolarianBossCastsPsychicScreamTrigger::IsActive() +{ + if (bot->getClass() != CLASS_PRIEST) + return false; + + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + return astromancer && astromancer->HasAura(SPELL_SOLARIAN_TRANSFORM); +} + +// Kael'thas Sunstrider + +bool KaelthasSunstriderThaladredIsFixatedOnBotTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + if (!thaladred || thaladred->GetVictim() != bot) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return false; + + return !(botAI->IsTank(bot) && kaelAI->GetPhase() == PHASE_ALL_ADVISORS); +} + +bool KaelthasSunstriderPullingTankableAdvisorsTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR || + kaelAI->GetPhase() == PHASE_ALL_ADVISORS); +} + +bool KaelthasSunstriderSanguinarEngagedByMainTankTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + return sanguinar && !sanguinar->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !sanguinar->HasAura(SPELL_PERMANENT_FEIGN_DEATH); +} + +bool KaelthasSunstriderSanguinarCastsBellowingRoarTrigger::IsActive() +{ + if (bot->getClass() != CLASS_PRIEST) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return false; + + if (kaelAI->GetPhase() != PHASE_SINGLE_ADVISOR && + kaelAI->GetPhase() != PHASE_TRANSITION && + kaelAI->GetPhase() != PHASE_ALL_ADVISORS) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank || mainTank->HasAura(SPELL_FEAR_WARD)) + return false; + + return botAI->CanCastSpell("fear ward", mainTank); +} + +bool KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger::IsActive() +{ + if (bot->getClass() != CLASS_WARLOCK) + return false; + + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (!capernian || capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || + capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + return false; + + return GetCapernianTank(bot) == bot; +} + +bool KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger::IsActive() +{ + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (!capernian || capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || + capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + return false; + + return GetCapernianTank(bot) != bot; +} + +bool KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger::IsActive() +{ + if (!botAI->IsAssistTankOfIndex(bot, 0, false)) + return false; + + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + return telonicus && !telonicus->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !telonicus->HasAura(SPELL_PERMANENT_FEIGN_DEATH); +} + +bool KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (!AI_VALUE2(Unit*, "find target", "master engineer telonicus") && + !AI_VALUE2(Unit*, "find target", "lord sanguinar")) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || kaelAI->GetPhase() != PHASE_ALL_ADVISORS) + return false; + + return botAI->IsAssistHealOfIndex(bot, 0, true) || + botAI->IsMainTank(bot) || botAI->IsAssistTankOfIndex(bot, 0, true) || + (bot->getClass() == CLASS_WARLOCK && GetCapernianTank(bot) == bot); +} + +bool KaelthasSunstriderDeterminingAdvisorKillOrderTrigger::IsActive() +{ + if (botAI->IsHeal(bot) || + botAI->IsMainTank(bot) || + botAI->IsAssistTankOfIndex(bot, 0, true)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR || + kaelAI->GetPhase() == PHASE_ALL_ADVISORS); +} + +bool KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger::IsActive() +{ + if (!botAI->IsDps(bot)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || kaelAI->GetPhase() != PHASE_SINGLE_ADVISOR) + return false; + + return IsMechanicTrackerBot(botAI, bot, TEMPEST_KEEP_MAP_ID, GetCapernianTank(bot)); +} + +bool KaelthasSunstriderLegendaryWeaponsAreAliveTrigger::IsActive() +{ + if (botAI->IsMainTank(bot)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && kaelAI->GetPhase() == PHASE_WEAPONS; +} + +bool KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger::IsActive() +{ + return botAI->IsMainTank(bot) && + AI_VALUE2(Unit*, "find target", "devastation"); +} + +bool KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || + (kaelAI->GetPhase() != PHASE_WEAPONS && kaelAI->GetPhase() != PHASE_ALL_ADVISORS)) + return false; + + Unit* axe = AI_VALUE2(Unit*, "find target", "devastation"); + if (axe && axe->GetVictim() == bot) + return false; + + return IsAnyLegendaryWeaponDead(bot); +} + +bool KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) + return false; + + return bot->HasItemCount(ITEM_STAFF_OF_DISINTEGRATION, 1, false) || + bot->HasItemCount(ITEM_NETHERSTRAND_LONGBOW, 1, false) || + bot->HasItemCount(ITEM_PHASESHIFT_BULWARK, 1, false); +} + +bool KaelthasSunstriderLegendaryWeaponsWereLostTrigger::IsActive() +{ + if (bot->GetMapId() != TEMPEST_KEEP_MAP_ID) + return false; + + Map* map = bot->GetMap(); + if (!map) + return false; + + constexpr uint32 KAELTHAS_DB_GUID = 158218; + auto const& creatureStore = map->GetCreatureBySpawnIdStore(); + auto it = creatureStore.find(KAELTHAS_DB_GUID); + if (it == creatureStore.end()) + return false; + + Creature* kaelthas = it->second; + if (!kaelthas || bot->GetExactDist2d(kaelthas) > 150.0f) + return false; + + const std::array weaponSlots = + { + EQUIPMENT_SLOT_MAINHAND, + EQUIPMENT_SLOT_OFFHAND, + EQUIPMENT_SLOT_RANGED + }; + + for (uint8 slot : weaponSlots) + { + if (!bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot) && + HasEquippableItemForSlot(bot, slot)) + return true; + } + + return false; +} + +bool KaelthasSunstriderBossHasEnteredTheFightTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && kaelAI->GetPhase() == PHASE_FINAL; +} + +bool KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (botAI->IsTank(bot) && kaelthas->GetVictim() == bot) + return false; + + return AI_VALUE2(Unit*, "find target", "phoenix") || + AI_VALUE2(Unit*, "find target", "phoenix egg"); +} + +bool KaelthasSunstriderRaidMemberIsMindControlledTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (botAI->IsTank(bot) && kaelthas->GetVictim() == bot) + return false; + + if (!bot->HasItemCount(ITEM_INFINITY_BLADE, 1, true)) + return false; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (member->HasAura(SPELL_KAELTHAS_MIND_CONTROL)) + return true; + } + } + + return false; +} + +bool KaelthasSunstriderBossIsCastingPyroblastTrigger::IsActive() +{ + if (!botAI->IsDps(bot)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + return kaelthas && kaelthas->HasAura(SPELL_SHOCK_BARRIER); +} + +bool KaelthasSunstriderBossIsManipulatingGravityTrigger::IsActive() +{ + return bot->HasAura(SPELL_GRAVITY_LAPSE); +} diff --git a/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.h b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.h new file mode 100644 index 00000000..13b94c68 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.h @@ -0,0 +1,338 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPTRIGGERS_H +#define _PLAYERBOT_RAIDTEMPESTKEEPTRIGGERS_H + +#include "Trigger.h" + +// General + +// Trash + +class CrimsonHandCenturionCastsArcaneVolleyTrigger : public Trigger +{ +public: + CrimsonHandCenturionCastsArcaneVolleyTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "crimson hand centurion casts arcane volley") {} + bool IsActive() override; +}; + +// Al'ar + +class AlarPullingBossTrigger : public Trigger +{ +public: + AlarPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar pulling boss") {} + bool IsActive() override; +}; + +class AlarBossIsFlyingBetweenPlatformsTrigger : public Trigger +{ +public: + AlarBossIsFlyingBetweenPlatformsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar boss is flying between platforms") {} + bool IsActive() override; +}; + +class AlarEmbersOfAlarExplodeUponDeathTrigger : public Trigger +{ +public: + AlarEmbersOfAlarExplodeUponDeathTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar embers of al'ar explode upon death") {} + bool IsActive() override; +}; + +class AlarKillingEmbersOfAlarDamagesBossTrigger : public Trigger +{ +public: + AlarKillingEmbersOfAlarDamagesBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar killing embers of al'ar damages boss") {} + bool IsActive() override; +}; + +class AlarIncomingFlameQuillsTrigger : public Trigger +{ +public: + AlarIncomingFlameQuillsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar incoming flame quills") {} + bool IsActive() override; +}; + +class AlarRisingFromTheAshesTrigger : public Trigger +{ +public: + AlarRisingFromTheAshesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar rising from the ashes") {} + bool IsActive() override; +}; + +class AlarEverythingIsOnFireInPhase2Trigger : public Trigger +{ +public: + AlarEverythingIsOnFireInPhase2Trigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar everything is on fire in phase 2") {} + bool IsActive() override; +}; + +class AlarPhase2EncounterIsAtRoomCenterTrigger : public Trigger +{ +public: + AlarPhase2EncounterIsAtRoomCenterTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar phase 2 encounter is at room center") {} + bool IsActive() override; +}; + +class AlarStrategyChangesBetweenPhasesTrigger : public Trigger +{ +public: + AlarStrategyChangesBetweenPhasesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar strategy changes between phases") {} + bool IsActive() override; +}; + +// Void Reaver + +class VoidReaverBossCastsPoundingTrigger : public Trigger +{ +public: + VoidReaverBossCastsPoundingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver boss casts pounding") {} + bool IsActive() override; +}; + +class VoidReaverKnockAwayReducesTankAggroTrigger : public Trigger +{ +public: + VoidReaverKnockAwayReducesTankAggroTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver knock away reduces tank aggro") {} + bool IsActive() override; +}; + +class VoidReaverBossLaunchesArcaneOrbsTrigger : public Trigger +{ +public: + VoidReaverBossLaunchesArcaneOrbsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver boss launches arcane orbs") {} + bool IsActive() override; +}; + +class VoidReaverArcaneOrbIsIncomingTrigger : public Trigger +{ +public: + VoidReaverArcaneOrbIsIncomingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver arcane orb is incoming") {} + bool IsActive() override; +}; + +class VoidReaverBotIsNotInCombatTrigger : public Trigger +{ +public: + VoidReaverBotIsNotInCombatTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver bot is not in combat") {} + bool IsActive() override; +}; + +// High Astromancer Solarian + +class HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger : public Trigger +{ +public: + HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian boss casts wrath of the astromancer") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianBotHasWrathOfTheAstromancerTrigger : public Trigger +{ +public: + HighAstromancerSolarianBotHasWrathOfTheAstromancerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian bot has wrath of the astromancer") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianBossHasVanishedTrigger : public Trigger +{ +public: + HighAstromancerSolarianBossHasVanishedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian boss has vanished") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianSolariumPriestsSpawnedTrigger : public Trigger +{ +public: + HighAstromancerSolarianSolariumPriestsSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian solarium priests spawned") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianBossCastsPsychicScreamTrigger : public Trigger +{ +public: + HighAstromancerSolarianBossCastsPsychicScreamTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer boss casts psychic scream") {} + bool IsActive() override; +}; + +// Kael'thas Sunstrider + +class KaelthasSunstriderThaladredIsFixatedOnBotTrigger : public Trigger +{ +public: + KaelthasSunstriderThaladredIsFixatedOnBotTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider thaladred is fixated on bot") {} + bool IsActive() override; +}; + +class KaelthasSunstriderPullingTankableAdvisorsTrigger : public Trigger +{ +public: + KaelthasSunstriderPullingTankableAdvisorsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider pulling tankable advisors") {} + bool IsActive() override; +}; + +class KaelthasSunstriderSanguinarEngagedByMainTankTrigger : public Trigger +{ +public: + KaelthasSunstriderSanguinarEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider sanguinar engaged by main tank") {} + bool IsActive() override; +}; + +class KaelthasSunstriderSanguinarCastsBellowingRoarTrigger : public Trigger +{ +public: + KaelthasSunstriderSanguinarCastsBellowingRoarTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider sanguinar casts bellowing roar") {} + bool IsActive() override; +}; + +class KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger : public Trigger +{ +public: + KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider capernian should be tanked by a warlock") {} + bool IsActive() override; +}; + +class KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger : public Trigger +{ +public: + KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider capernian casts arcane burst and conflagration") {} + bool IsActive() override; +}; + +class KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger : public Trigger +{ +public: + KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider telonicus engaged by first assist tank") {} + bool IsActive() override; +}; + +class KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger : public Trigger +{ +public: + KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider bots have specific roles in phase 3") {} + bool IsActive() override; +}; + +class KaelthasSunstriderDeterminingAdvisorKillOrderTrigger : public Trigger +{ +public: + KaelthasSunstriderDeterminingAdvisorKillOrderTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider determining advisor kill order") {} + bool IsActive() override; +}; + +class KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger : public Trigger +{ +public: + KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider waiting for tanks to get aggro on advisors") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryWeaponsAreAliveTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryWeaponsAreAliveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary weapons are alive") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary axe casts whirlwind") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary weapons are dead and lootable") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary weapons are equipped") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryWeaponsWereLostTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryWeaponsWereLostTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary weapons were lost") {} + bool IsActive() override; +}; + +class KaelthasSunstriderBossHasEnteredTheFightTrigger : public Trigger +{ +public: + KaelthasSunstriderBossHasEnteredTheFightTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider boss has entered the fight") {} + bool IsActive() override; +}; + +class KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger : public Trigger +{ +public: + KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider phoenixes and eggs are spawning") {} + bool IsActive() override; +}; + +class KaelthasSunstriderRaidMemberIsMindControlledTrigger : public Trigger +{ +public: + KaelthasSunstriderRaidMemberIsMindControlledTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider raid member is mind controlled") {} + bool IsActive() override; +}; + +class KaelthasSunstriderBossIsCastingPyroblastTrigger : public Trigger +{ +public: + KaelthasSunstriderBossIsCastingPyroblastTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider boss is casting pyroblast") {} + bool IsActive() override; +}; + +class KaelthasSunstriderBossIsManipulatingGravityTrigger : public Trigger +{ +public: + KaelthasSunstriderBossIsManipulatingGravityTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider boss is manipulating gravity") {} + bool IsActive() override; +}; + +#endif diff --git a/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.cpp b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.cpp new file mode 100644 index 00000000..30327771 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.cpp @@ -0,0 +1,425 @@ +#include "RaidTempestKeepHelpers.h" +#include "RaidTempestKeepActions.h" +#include "LootObjectStack.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +namespace TempestKeepHelpers +{ + // General + + Unit* GetNearestNonTankPlayerInRadius(PlayerbotAI* botAI, Player* bot, float radius) + { + Unit* nearestPlayer = nullptr; + float nearestDistance = radius; + + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref != nullptr; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == bot || botAI->IsTank(member)) + continue; + + float distance = bot->GetExactDist2d(member); + if (distance < nearestDistance) + { + nearestDistance = distance; + nearestPlayer = member; + } + } + + return nearestPlayer; + } + + std::vector GetAllHazardTriggers(Player* bot, uint32 npcEntry, float searchRadius) + { + std::vector hazardTriggers; + + std::list creatureList; + bot->GetCreatureListWithEntryInGrid(creatureList, npcEntry, searchRadius); + + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + hazardTriggers.push_back(creature); + } + + return hazardTriggers; + } + + Position FindSafestNearbyPosition(Player* bot, const std::vector& hazards, + float hazardRadius, const Position* center) + { + constexpr float searchStep = M_PI / 8.0f; + constexpr float minDistance = 2.0f; + constexpr float maxDistance = 30.0f; + constexpr float distanceStep = 1.0f; + + Position bestPos; + float minMoveDistance = std::numeric_limits::max(); + bool foundSafe = false; + + for (float distance = minDistance; distance <= maxDistance; distance += distanceStep) + { + for (float angle = 0.0f; angle < 2 * M_PI; angle += searchStep) + { + const Position& searchCenter = center ? *center : bot->GetPosition(); + float x = searchCenter.GetPositionX() + distance * std::cos(angle); + float y = searchCenter.GetPositionY() + distance * std::sin(angle); + + bool isSafe = true; + for (Unit* hazard : hazards) + { + if (hazard->GetExactDist2d(x, y) < hazardRadius) + { + isSafe = false; + break; + } + } + + if (!isSafe) + continue; + + Position testPos(x, y, bot->GetPositionZ()); + + bool pathSafe = IsPathSafeFromHazards(bot->GetPosition(), testPos, hazards, hazardRadius); + if (pathSafe || !foundSafe) + { + float moveDistance = bot->GetExactDist2d(x, y); + + if (pathSafe && (!foundSafe || moveDistance < minMoveDistance)) + { + bestPos = testPos; + minMoveDistance = moveDistance; + foundSafe = true; + } + else if (!foundSafe && moveDistance < minMoveDistance) + { + bestPos = testPos; + minMoveDistance = moveDistance; + } + } + } + + if (foundSafe) + break; + } + + return bestPos; + } + + bool IsPathSafeFromHazards( + const Position& start, const Position& end, const std::vector& hazards, float hazardRadius) + { + constexpr uint8 numChecks = 10; + float dx = end.GetPositionX() - start.GetPositionX(); + float dy = end.GetPositionY() - start.GetPositionY(); + + for (uint8 i = 1; i <= numChecks; ++i) + { + float ratio = static_cast(i) / numChecks; + float checkX = start.GetPositionX() + dx * ratio; + float checkY = start.GetPositionY() + dy * ratio; + + for (Unit* hazard : hazards) + { + float distToHazard = hazard->GetExactDist2d(checkX, checkY); + if (distToHazard < hazardRadius) + return false; + } + } + + return true; + } + + // Al'ar + + const Position ALAR_PLATFORM_0 = { 335.638f, 59.4879f, 17.9319f }; // West Platform + const Position ALAR_PLATFORM_1 = { 388.751f, 31.7312f, 20.2636f }; // Northwest Platform + const Position ALAR_PLATFORM_2 = { 388.791f, -33.1059f, 20.2636f }; // Northeast Platform + const Position ALAR_PLATFORM_3 = { 332.723f, -61.159f, 17.9791f }; // East Platform + const std::array PLATFORM_POSITIONS = + { + ALAR_PLATFORM_0, + ALAR_PLATFORM_1, + ALAR_PLATFORM_2, + ALAR_PLATFORM_3 + }; + const Position ALAR_GROUND_0 = { 336.439f, 48.181f, -2.389f }; // Ground counterpart to West Platform + const Position ALAR_GROUND_1 = { 379.122f, 25.146f, -2.385f }; // Ground counterpart to Northwest Platform + const Position ALAR_GROUND_2 = { 378.583f, -27.481f, -2.385f }; // Ground counterpart to Northeast Platform + const Position ALAR_GROUND_3 = { 331.631f, -49.716f, -2.389f }; // Ground counterpart to East Platform + const std::array GROUND_POSITIONS = + { + ALAR_GROUND_0, + ALAR_GROUND_1, + ALAR_GROUND_2, + ALAR_GROUND_3 + }; + const Position ALAR_ROOM_CENTER = { 330.611f, -2.540f, -2.389f }; + const Position ALAR_POINT_QUILL_OR_DIVE = { 332.000f, 0.010f, 43.000f }; + const Position ALAR_POINT_MIDDLE = { 331.000f, 0.010f, -2.380f }; + const Position ALAR_SE_RAMP_BASE = { 281.064f, -36.590f, -2.389f }; + const Position ALAR_SW_RAMP_BASE = { 281.064f, 36.590f, -2.389f }; + const Position ALAR_ROOM_S_CENTER = { 281.064f, 0.000f, -2.389f }; + + std::unordered_map lastRebirthState; + std::unordered_map isAlarInPhase2; + + int8 GetAlarDestinationLocationIndex(Unit* alar, Position& dest) + { + if (!alar) + return LOCATION_NONE; + + float x, y, z; + if (!alar->GetMotionMaster()->GetDestination(x, y, z)) + return LOCATION_NONE; + + dest.Relocate(x, y, z); + + const std::array locations = + { + ALAR_PLATFORM_0, + ALAR_PLATFORM_1, + ALAR_PLATFORM_2, + ALAR_PLATFORM_3, + ALAR_POINT_QUILL_OR_DIVE, + ALAR_POINT_MIDDLE, + }; + + float minDist = std::numeric_limits::max(); + int8 locationIndex = LOCATION_NONE; + for (int8 i = 0; i < TOTAL_ALAR_LOCATIONS; ++i) + { + float dist = dest.GetExactDist2d(&locations[i]); + if (dist < minDist) + { + minDist = dist; + locationIndex = i; + } + } + if (minDist > 0.1f) + return LOCATION_NONE; + + return locationIndex; + } + + int8 GetAlarCurrentLocationIndex(Unit* alar) + { + if (!alar) + return LOCATION_NONE; + + const std::array locations = + { + ALAR_PLATFORM_0, + ALAR_PLATFORM_1, + ALAR_PLATFORM_2, + ALAR_PLATFORM_3, + ALAR_POINT_QUILL_OR_DIVE, + ALAR_POINT_MIDDLE, + }; + + float minDist = std::numeric_limits::max(); + int8 locationIndex = LOCATION_NONE; + for (int8 i = 0; i < TOTAL_ALAR_LOCATIONS; ++i) + { + float dist = alar->GetPosition().GetExactDist2d(&locations[i]); + if (dist < minDist) + { + minDist = dist; + locationIndex = i; + } + } + if (minDist > 0.1f) + return LOCATION_NONE; + + return locationIndex; + } + + void GetClosestPlatformAndGround(const Position& botPos, int8& closestPlatform, Position& ground) + { + float minDist = std::numeric_limits::max(); + closestPlatform = -1; + for (int8 i = 0; i < 4; ++i) + { + float dist = botPos.GetExactDist2d(&PLATFORM_POSITIONS[i]); + if (dist < minDist) + { + minDist = dist; + closestPlatform = i; + } + } + ground = GROUND_POSITIONS[closestPlatform]; + } + + std::pair GetFirstTwoEmbersOfAlar(PlayerbotAI* botAI) + { + Unit* firstEmber = nullptr; + Unit* secondEmber = nullptr; + + for (auto const& guid : + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get()) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_EMBER_OF_ALAR) + { + if (!firstEmber) + { + firstEmber = unit; + } + else if (!secondEmber) + { + secondEmber = unit; + break; + } + } + } + + return { firstEmber, secondEmber }; + } + + Player* GetSecondEmberTank(PlayerbotAI* botAI) + { + Player* mainTank = GetGroupMainTank(botAI, botAI->GetBot()); + Player* assistTank = GetGroupAssistTank(botAI, botAI->GetBot(), 0); + + bool mainTankHasMelt = mainTank && mainTank->HasAura(SPELL_MELT_ARMOR); + bool assistTankHasMelt = assistTank && assistTank->HasAura(SPELL_MELT_ARMOR); + + if (mainTankHasMelt) + return mainTank; + + if (assistTankHasMelt || (!mainTankHasMelt && !assistTankHasMelt)) + return assistTank; + + return nullptr; + } + + // Void Reaver + + const Position VOID_REAVER_TANK_POSITION = { 423.845f, 371.733f, 14.897f }; + + std::unordered_map hasReachedVoidReaverPosition; + std::unordered_map> voidReaverArcaneOrbs; + + // Kael'thas Sunstrider + + const Position SANGUINAR_TANK_POSITION = { 775.478f, 39.888f, 46.780f }; + const Position SANGUINAR_WAITING_POSITION = { 761.850f, 27.459f, 46.779f }; + const Position TELONICUS_TANK_POSITION = { 773.717f, 44.091f, 46.780f }; + const Position TELONICUS_WAITING_POSITION = { 754.347f, 31.739f, 46.796f }; + const Position ADVISOR_HEAL_POSITION = { 752.171f, 19.494f, 46.779f }; + const Position CAPERNIAN_WAITING_POSITION = { 743.897f, -11.575f, 46.779f }; + const Position KAELTHAS_TANK_POSITION = { 799.390f, -0.837f, 48.729f }; + + std::unordered_map advisorDpsWaitTimer; + + // (1) First priority is an assistant Warlock (real player or bot) + // (2) If no assistant Warlock, then look for any Warlock bot + Player* GetCapernianTank(Player* bot) + { + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + Player* fallbackWarlock = nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->getClass() != CLASS_WARLOCK) + continue; + + if (group->IsAssistant(member->GetGUID())) + return member; + + if (!fallbackWarlock && GET_PLAYERBOT_AI(member)) + fallbackWarlock = member; + } + + return fallbackWarlock; + } + + // One Hunter will start on Sanguinar in Phase 3 with Melee to apply Armor Disruption + // (1) First priority is an assistant Hunter (real player or bot) + // (2) If no assistant Hunter, then look for any Hunter bot + bool IsDebuffHunter(Player* bot) + { + if (bot->getClass() != CLASS_HUNTER || !bot->IsAlive()) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* fallbackHunter = nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->getClass() != CLASS_HUNTER) + continue; + + if (group->IsAssistant(member->GetGUID())) + return member == bot; + + if (!fallbackHunter && GET_PLAYERBOT_AI(member)) + fallbackHunter = member; + } + + return fallbackHunter == bot; + } + + bool IsAnyLegendaryWeaponDead(Player* bot) + { + static const std::array weaponEntries = + { + NPC_STAFF_OF_DISINTEGRATION, + NPC_COSMIC_INFUSER, + NPC_INFINITY_BLADES, + NPC_WARP_SLICER, + NPC_PHASESHIFT_BULWARK, + NPC_NETHERSTRAND_LONGBOW, + NPC_DEVASTATION + }; + + constexpr float searchRadius = 100.0f; + + for (uint32 entry : weaponEntries) + { + Creature* weapon = bot->FindNearestCreature(entry, searchRadius, false); + + if (weapon && !weapon->IsAlive()) + return true; + } + + return false; + } + + bool HasEquippableItemForSlot(Player* bot, uint8 slot) + { + for (uint8 i = 0; i < 5; ++i) + { + uint8 bag = (i == 0) ? INVENTORY_SLOT_BAG_0 : (INVENTORY_SLOT_BAG_START + i - 1); + uint8 startSlot = (bag == INVENTORY_SLOT_BAG_0) ? INVENTORY_SLOT_ITEM_START : 0; + uint8 endSlot = (bag == INVENTORY_SLOT_BAG_0) ? INVENTORY_SLOT_ITEM_END : + (bot->GetBagByPos(bag) ? bot->GetBagByPos(bag)->GetBagSize() : 0); + + for (uint8 bagSlot = startSlot; bagSlot < endSlot; ++bagSlot) + { + Item* item = bot->GetItemByPos(bag, bagSlot); + if (!item || !item->GetTemplate()) + continue; + + uint16 dest = 0; + if (bot->CanEquipItem(slot, dest, item, false) == EQUIP_ERR_OK) + return true; + } + } + + return false; + } +} diff --git a/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.h b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.h new file mode 100644 index 00000000..e99781f1 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.h @@ -0,0 +1,158 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPHELPERS_H_ +#define _PLAYERBOT_RAIDTEMPESTKEEPHELPERS_H_ + +#include +#include +#include + +#include "AiObject.h" +#include "Position.h" +#include "Unit.h" + +namespace TempestKeepHelpers +{ + enum TempestKeepSpells + { + // Trash + SPELL_ARCANE_FLURRY = 37268, + + // Al'ar + SPELL_REBIRTH_PHASE2 = 34342, + SPELL_REBIRTH_DIVE = 35369, + SPELL_MELT_ARMOR = 35410, + + // Void Reaver + SPELL_ARCANE_ORB = 34172, + + // High Astromancer Solarian + SPELL_SOLARIAN_TRANSFORM = 39117, + SPELL_WRATH_OF_THE_ASTROMANCER = 42783, + + // Kael'thas Sunstrider + SPELL_PERMANENT_FEIGN_DEATH = 29266, + SPELL_GRAVITY_LAPSE = 39432, + SPELL_KAEL_FULL_POWER = 36187, + SPELL_MENTAL_PROTECTION_FIELD = 36480, // Staff of Disintegration + SPELL_ARCANE_BARRIER = 36481, // Phaseshift Bulwark + SPELL_KAELTHAS_MIND_CONTROL = 36797, + SPELL_SHOCK_BARRIER = 36815, + SPELL_STAFF_FROSTBOLT = 36990, + + // Hunter + SPELL_MISDIRECTION = 35079, + + // Priest + SPELL_FEAR_WARD = 6346, + }; + + enum TempestKeepNPCs + { + // Al'ar + NPC_EMBER_OF_ALAR = 19551, + NPC_FLAME_PATCH = 20602, + + // High Astromancer Solarian + NPC_SOLARIUM_PRIEST = 18806, + + // Kael'thas Sunstrider + NPC_KAELTHAS_SUNSTRIDER = 19622, + NPC_NETHERSTRAND_LONGBOW = 21268, + NPC_DEVASTATION = 21269, + NPC_COSMIC_INFUSER = 21270, + NPC_INFINITY_BLADES = 21271, // Item is singular, but NPC is plural + NPC_WARP_SLICER = 21272, + NPC_PHASESHIFT_BULWARK = 21273, + NPC_STAFF_OF_DISINTEGRATION = 21274, + // NPC_NETHER_VAPOR = 21002, // Unimplemented in AC; method needed if fixed + NPC_PHOENIX = 21362, + NPC_PHOENIX_EGG = 21364, + NPC_FLAME_STRIKE_TRIGGER = 21369, + }; + + enum TempestKeepItems + { + // Kael'thas Sunstrider + ITEM_WARP_SLICER = 30311, + ITEM_INFINITY_BLADE = 30312, + ITEM_STAFF_OF_DISINTEGRATION = 30313, + ITEM_PHASESHIFT_BULWARK = 30314, + ITEM_DEVASTATION = 30316, + ITEM_COSMIC_INFUSER = 30317, + ITEM_NETHERSTRAND_LONGBOW = 30318, + ITEM_NETHER_SPIKES = 30319, + }; + + // General + constexpr uint32 TEMPEST_KEEP_MAP_ID = 550; + Unit* GetNearestNonTankPlayerInRadius(PlayerbotAI* botAI, Player* bot, float radius); + std::vector GetAllHazardTriggers(Player* bot, uint32 npcEntry, float searchRadius); + Position FindSafestNearbyPosition(Player* bot, const std::vector& hazards, + float hazardRadius, const Position* center = nullptr); + bool IsPathSafeFromHazards( + const Position& start, const Position& end, const std::vector& hazards, + float hazardRadius); + + // Al'ar + enum AlarLocationIndex + { + PLATFORM_0_IDX, + PLATFORM_1_IDX, + PLATFORM_2_IDX, + PLATFORM_3_IDX, + POINT_QUILL_OR_DIVE_IDX, + POINT_MIDDLE_IDX, + LOCATION_NONE = -1 + }; + constexpr float ALAR_BALCONY_Z = 17.0f; + extern const Position ALAR_PLATFORM_0; + extern const Position ALAR_PLATFORM_1; + extern const Position ALAR_PLATFORM_2; + extern const Position ALAR_PLATFORM_3; + extern const std::array PLATFORM_POSITIONS; + extern const Position ALAR_GROUND_0; + extern const Position ALAR_GROUND_1; + extern const Position ALAR_GROUND_2; + extern const Position ALAR_GROUND_3; + extern const std::array GROUND_POSITIONS; + extern const Position ALAR_ROOM_CENTER; + extern const Position ALAR_POINT_QUILL_OR_DIVE; + extern const Position ALAR_POINT_MIDDLE; + extern const Position ALAR_SE_RAMP_BASE; + extern const Position ALAR_SW_RAMP_BASE; + extern const Position ALAR_ROOM_S_CENTER; + constexpr uint8 TOTAL_ALAR_LOCATIONS = 6; + extern std::unordered_map lastRebirthState; + extern std::unordered_map isAlarInPhase2; + int8 GetAlarDestinationLocationIndex(Unit* alar, Position& dest); + int8 GetAlarCurrentLocationIndex(Unit* alar); + void GetClosestPlatformAndGround( + const Position& botPos, int8& closestPlatform, Position& ground); + std::pair GetFirstTwoEmbersOfAlar(PlayerbotAI* botAI); + Player* GetSecondEmberTank(PlayerbotAI* botAI); + + // Void Reaver + extern const Position VOID_REAVER_TANK_POSITION; + extern std::unordered_map hasReachedVoidReaverPosition; + struct ArcaneOrbData + { + Position destination; + uint32 castTime; + }; + extern std::unordered_map> voidReaverArcaneOrbs; + + // Kael'thas Sunstrider + extern const Position SANGUINAR_TANK_POSITION; + extern const Position SANGUINAR_WAITING_POSITION; + extern const Position TELONICUS_TANK_POSITION; + extern const Position TELONICUS_WAITING_POSITION; + extern const Position CAPERNIAN_WAITING_POSITION; + extern const Position ADVISOR_HEAL_POSITION; + extern const Position KAELTHAS_TANK_POSITION; + extern std::unordered_map advisorDpsWaitTimer; + Player* GetCapernianTank(Player* bot); + bool IsDebuffHunter(Player* bot); + bool IsAnyLegendaryWeaponDead(Player* bot); + bool HasEquippableItemForSlot(Player* bot, uint8 slot); +} + +#endif diff --git a/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepKaelthasBossAI.h b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepKaelthasBossAI.h new file mode 100644 index 00000000..598c8061 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepKaelthasBossAI.h @@ -0,0 +1,54 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPKAELTHASBOSSAI_H_ +#define _PLAYERBOT_RAIDTEMPESTKEEPKAELTHASBOSSAI_H_ + +#include "ScriptedCreature.h" + +enum KTYells +{ +}; + +enum KTPhases +{ + PHASE_NONE = 0, + PHASE_SINGLE_ADVISOR = 1, + PHASE_WEAPONS = 2, + PHASE_TRANSITION = 3, + PHASE_ALL_ADVISORS = 4, + PHASE_FINAL = 5 +}; + +enum KTActions +{ +}; + +struct boss_kaelthas : public BossAI +{ + boss_kaelthas(Creature* creature); + + void PrepareAdvisors(); + void SetRoomState(GOState state); + void Reset() override; + void AttackStart(Unit* who) override; + void MoveInLineOfSight(Unit* who) override; + void KilledUnit(Unit* victim) override; + void JustSummoned(Creature* summon) override; + void SpellHit(Unit* caster, SpellInfo const* spell) override; + void MovementInform(uint32 type, uint32 point) override; + void ExecuteMiddleEvent(); + void IntroduceNewAdvisor(KTYells talkIntroduction, KTActions kaelAction); + void PhaseEnchantedWeaponsExecute(); + void PhaseAllAdvisorsExecute(); + void PhaseKaelExecute(); + void UpdateAI(uint32 diff) override; + bool CheckEvadeIfOutOfCombatArea() const override; + void JustDied(Unit* killer) override; + + uint32 GetPhase() const { return _phase; } // This is the only addition to the existing class + +private: + uint32 _phase; + uint8 _advisorsAlive; + bool _transitionSceneReached = false; +}; + +#endif diff --git a/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepScripts.cpp b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepScripts.cpp new file mode 100644 index 00000000..ff26d150 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepScripts.cpp @@ -0,0 +1,47 @@ +#include "RaidTempestKeepHelpers.h" +#include "ObjectAccessor.h" +#include "Player.h" +#include "ScriptMgr.h" +#include "Spell.h" +#include "Timer.h" + +using namespace TempestKeepHelpers; + +class BossListenerScript : public AllSpellScript +{ +public: + BossListenerScript() : AllSpellScript("BossListenerScript") { } + + void OnSpellCast(Spell* spell, Unit* caster, SpellInfo const* spellInfo, bool /*skipCheck*/) override + { + if (spellInfo->Id != SPELL_ARCANE_ORB) + return; + + std::list const& targets = *spell->GetUniqueTargetInfo(); + if (targets.empty()) + return; + + Player* target = ObjectAccessor::GetPlayer(*caster, targets.front().targetGUID); + if (!target) + return; + + auto& orbs = voidReaverArcaneOrbs[caster->GetMap()->GetInstanceId()]; + uint32 currentTime = getMSTime(); + + ArcaneOrbData orbData; + orbData.destination = target->GetPosition(); + orbData.castTime = currentTime; + + orbs.push_back(orbData); + + orbs.erase(std::remove_if(orbs.begin(), orbs.end(), + [currentTime](const ArcaneOrbData& orb) { + return getMSTimeDiff(orb.castTime, currentTime) > 5000; + }), orbs.end()); + } +}; + +void AddSC_TempestKeepBotScripts() +{ + new BossListenerScript(); +} diff --git a/src/Bot/Engine/AiObjectContext.cpp b/src/Bot/Engine/AiObjectContext.cpp index 3a2037a0..ff54b971 100644 --- a/src/Bot/Engine/AiObjectContext.cpp +++ b/src/Bot/Engine/AiObjectContext.cpp @@ -37,12 +37,14 @@ #include "Ai/Raid/BlackwingLair/RaidBwlTriggerContext.h" #include "Ai/Raid/Karazhan/RaidKarazhanActionContext.h" #include "Ai/Raid/Karazhan/RaidKarazhanTriggerContext.h" -#include "Ai/Raid/Magtheridon/RaidMagtheridonActionContext.h" -#include "Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h" #include "Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h" #include "Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h" +#include "Ai/Raid/Magtheridon/RaidMagtheridonActionContext.h" +#include "Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h" #include "Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h" #include "Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h" +#include "Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h" +#include "Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h" #include "Ai/Raid/EyeOfEternity/RaidEoEActionContext.h" #include "Ai/Raid/EyeOfEternity/RaidEoETriggerContext.h" #include "Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h" @@ -115,9 +117,10 @@ void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextListIsAlive()) + return false; + Group* group = player->GetGroup(); if (!group) return false; - int counter = 0; + uint8 totalAssistants = 0; + uint8 assistantsBeforePlayer = 0; + uint8 nonAssistantsBeforePlayer = 0; + bool playerFound = false; - // First, assistants for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member) + if (!member || (ignoreDeadPlayers && !member->IsAlive()) || !IsHeal(member)) continue; - if (ignoreDeadPlayers && !member->IsAlive()) - continue; + bool isAssistant = group->IsAssistant(member->GetGUID()); - if (group->IsAssistant(member->GetGUID()) && IsHeal(member)) + if (isAssistant) + totalAssistants++; + + if (member == player) + playerFound = true; + else if (!playerFound) { - if (index == counter) - return player == member; - counter++; + if (isAssistant) + assistantsBeforePlayer++; + else + nonAssistantsBeforePlayer++; } } - // If not enough assistants, get non-assistants - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member) - continue; + if (!playerFound) + return false; - if (ignoreDeadPlayers && !member->IsAlive()) - continue; + // If the player is an assistant, their index is just the number of assistants before them. + // If they are a non-assistant, their index is shifted by the total number of assistants. + uint8 playerIndex = group->IsAssistant(player->GetGUID()) + ? assistantsBeforePlayer : (totalAssistants + nonAssistantsBeforePlayer); - if (!group->IsAssistant(member->GetGUID()) && IsHeal(member)) - { - if (index == counter) - return player == member; - counter++; - } - } - - return false; + return playerIndex == index; } -bool PlayerbotAI::IsAssistRangedDpsOfIndex(Player* player, int index, bool ignoreDeadPlayers) +bool PlayerbotAI::IsAssistRangedDpsOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers) { + if (!IsRangedDps(player)) + return false; + + if (ignoreDeadPlayers && !player->IsAlive()) + return false; + Group* group = player->GetGroup(); if (!group) return false; - int counter = 0; + uint8 totalAssistants = 0; + uint8 assistantsBeforePlayer = 0; + uint8 nonAssistantsBeforePlayer = 0; + bool playerFound = false; - // First, assistants for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member) + if (!member || (ignoreDeadPlayers && !member->IsAlive()) || !IsRangedDps(member)) continue; - if (ignoreDeadPlayers && !member->IsAlive()) - continue; + bool isAssistant = group->IsAssistant(member->GetGUID()); - if (group->IsAssistant(member->GetGUID()) && IsRangedDps(member)) + if (isAssistant) + totalAssistants++; + + if (member == player) + playerFound = true; + else if (!playerFound) { - if (index == counter) - return player == member; - counter++; + if (isAssistant) + assistantsBeforePlayer++; + else + nonAssistantsBeforePlayer++; } } - // If not enough assistants, get non-assistants - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member) - continue; + if (!playerFound) + return false; - if (ignoreDeadPlayers && !member->IsAlive()) - continue; + // If the player is an assistant, their index is just the number of assistants before them. + // If they are a non-assistant, their index is shifted by the total number of assistants. + uint8 playerIndex = group->IsAssistant(player->GetGUID()) + ? assistantsBeforePlayer : (totalAssistants + nonAssistantsBeforePlayer); - if (!group->IsAssistant(member->GetGUID()) && IsRangedDps(member)) - { - if (index == counter) - return player == member; - counter++; - } - } - - return false; + return playerIndex == index; } bool PlayerbotAI::HasAggro(Unit* unit) @@ -2226,43 +2235,44 @@ bool PlayerbotAI::IsDps(Player* player, bool bySpec) return false; } -bool PlayerbotAI::IsMainTank(Player* player) +bool PlayerbotAI::IsMainTank(Player* player, bool ignoreMemberFlag) { Group* group = player->GetGroup(); if (!group) - { return IsTank(player); - } ObjectGuid mainTank = ObjectGuid(); - Group::MemberSlotList const& slots = group->GetMemberSlots(); - for (Group::member_citerator itr = slots.begin(); itr != slots.end(); ++itr) + // (1) Check for main tank flag (any class or spec) + if (!ignoreMemberFlag) { - if (itr->flags & MEMBER_FLAG_MAINTANK) + Group::MemberSlotList const& slots = group->GetMemberSlots(); + + for (Group::member_citerator itr = slots.begin(); itr != slots.end(); ++itr) { - mainTank = itr->guid; - break; + if (itr->flags & MEMBER_FLAG_MAINTANK) + { + mainTank = itr->guid; + break; + } } + + if (mainTank != ObjectGuid::Empty) + return player->GetGUID() == mainTank; } - if (mainTank != ObjectGuid::Empty) - { - return player->GetGUID() == mainTank; - } + // (2) If no main tank flag, return the first tank + if (!IsTank(player) || !player->IsAlive()) + return false; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member) - { continue; - } if (IsTank(member) && member->IsAlive()) - { return player->GetGUID() == member->GetGUID(); - } } return false; @@ -2281,47 +2291,31 @@ bool PlayerbotAI::IsBotMainTank(Player* player) return false; if (IsMainTank(player)) - { return true; - } Group* group = player->GetGroup(); if (!group) - { - return true; // If no group, consider the bot as main tank - } + return true; int32 botAssistTankIndex = GetAssistTankIndex(player); if (botAssistTankIndex == -1) - { return false; - } for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) { Player* member = gref->GetSource(); if (!member) - { continue; - } int32 memberAssistTankIndex = GetAssistTankIndex(member); if (memberAssistTankIndex == -1) - { continue; - } if (memberAssistTankIndex == botAssistTankIndex && player == member) - { return true; - } if (memberAssistTankIndex < botAssistTankIndex && member->GetSession()->IsBot()) - { return false; - } - - return false; } return false; @@ -2331,73 +2325,76 @@ uint32 PlayerbotAI::GetGroupTankNum(Player* player) { Group* group = player->GetGroup(); if (!group) - { return 0; - } + uint32 result = 0; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member) - { continue; - } if (IsTank(member) && member->IsAlive()) - { result++; - } } + return result; } -bool PlayerbotAI::IsAssistTank(Player* player) { return IsTank(player) && !IsMainTank(player); } - -bool PlayerbotAI::IsAssistTankOfIndex(Player* player, int index, bool ignoreDeadPlayers) +bool PlayerbotAI::IsAssistTank(Player* player) { + return IsTank(player) && !IsMainTank(player); +} + +bool PlayerbotAI::IsAssistTankOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers) +{ + if (!IsAssistTank(player)) + return false; + + if (ignoreDeadPlayers && !player->IsAlive()) + return false; + Group* group = player->GetGroup(); if (!group) return false; - int counter = 0; + uint8 totalAssistants = 0; + uint8 assistantsBeforePlayer = 0; + uint8 nonAssistantsBeforePlayer = 0; + bool playerFound = false; - // First, assistants for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member) + if (!member || (ignoreDeadPlayers && !member->IsAlive()) || !IsAssistTank(member)) continue; - if (ignoreDeadPlayers && !member->IsAlive()) - continue; + bool isAssistant = group->IsAssistant(member->GetGUID()); - if (group->IsAssistant(member->GetGUID()) && IsAssistTank(member)) + if (isAssistant) + totalAssistants++; + + if (member == player) + playerFound = true; + else if (!playerFound) { - if (index == counter) - return player == member; - counter++; + if (isAssistant) + assistantsBeforePlayer++; + else + nonAssistantsBeforePlayer++; } } - // If not enough assistants, get non-assistants - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member) - continue; + if (!playerFound) + return false; - if (ignoreDeadPlayers && !member->IsAlive()) - continue; + // If the player is an assistant, their index is just the number of assistants before them. + // If they are a non-assistant, their index is shifted by the total number of assistants. + uint8 playerIndex = group->IsAssistant(player->GetGUID()) + ? assistantsBeforePlayer : (totalAssistants + nonAssistantsBeforePlayer); - if (!group->IsAssistant(member->GetGUID()) && IsAssistTank(member)) - { - if (index == counter) - return player == member; - counter++; - } - } - return false; + return playerIndex == index; } namespace acore diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index 05710548..b3a51b15 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -423,12 +423,12 @@ public: static bool IsRangedDps(Player* player, bool bySpec = false); static bool IsCombo(Player* player); static bool IsBotMainTank(Player* player); - static bool IsMainTank(Player* player); + static bool IsMainTank(Player* player, bool ignoreMemberFlag = false); static uint32 GetGroupTankNum(Player* player); static bool IsAssistTank(Player* player); - static bool IsAssistTankOfIndex(Player* player, int index, bool ignoreDeadPlayers = false); - static bool IsAssistHealOfIndex(Player* player, int index, bool ignoreDeadPlayers = false); - static bool IsAssistRangedDpsOfIndex(Player* player, int index, bool ignoreDeadPlayers = false); + static bool IsAssistTankOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers = false); + static bool IsAssistHealOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers = false); + static bool IsAssistRangedDpsOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers = false); bool HasAggro(Unit* unit); static int32 GetAssistTankIndex(Player* player); int32 GetGroupSlotIndex(Player* player); diff --git a/src/Script/Playerbots.cpp b/src/Script/Playerbots.cpp index 94b8eb1b..ab6d64f9 100644 --- a/src/Script/Playerbots.cpp +++ b/src/Script/Playerbots.cpp @@ -520,6 +520,8 @@ public: void AddPlayerbotsSecureLoginScripts(); +void AddSC_TempestKeepBotScripts(); + void AddPlayerbotsScripts() { new PlayerbotsDatabaseScript(); @@ -532,4 +534,5 @@ void AddPlayerbotsScripts() AddPlayerbotsSecureLoginScripts(); AddPlayerbotsCommandscripts(); PlayerBotsGuildValidationScript(); + AddSC_TempestKeepBotScripts(); }