From e739f7820b87a7ec38cfacd12475a4dfda252371 Mon Sep 17 00:00:00 2001 From: Noscopezz Date: Wed, 11 Jun 2025 23:27:44 +0200 Subject: [PATCH] ICC Refactor, All bosses improved NM/HC (#1370) * ICC PP WIP WIP * added mutated plague for PP * BPC added (kinetic and boss targeting need to be done by player) OT collects dark nucles, bots spread on vortex and other stuff they do ok on their own. Tested only on 10NM, should work on 25NM * Tank pos. correction * BQL, ranged spread, link, flame, bite, tanking tested 10NM to do (better fire spread, hc tacti, melee spread when in air) * LDW improved improved shadow logic, ranged spread for easier shadow handling * dbs update, fixed teleporting Bots should only go and teleport to the mage that is actually below zero now * DBS ranged fix Ranged should spread more quickly and freak out less * Festergut && DBS fixed ranged spread (both) fixed spore logic (fester) * Rotface fix Improved big ooze tanking (static pos, todo kiting) ooze explosion spread mechanic fix ooze pool fix Player needs to mark rotface with skull icon, oterwise bots try to attack oozes * BQL fixed for 25nm todo: better melee logic in air phase, better melee flame spread * VDW, Sister Svalna, Sindy update Sister Svalna, bots can pickup spears and throw at svalna when she has shield up VDW added healer strats to use portal and heal boss (atm druids are for raid healing only, so use druide + any other healer, ideally player should be healer) todo (focus on supressers, add healer rotations, atm they use quickest spell they can) Sindragosa Added tank boss manipulation (boss orientation and position) bots detect (buffet, unchained magic and chilled to the bone and act accordingly) bots detect frost beacon move to safe spot and los frost bombs around them, while dpsing tombs (todo stop dps if only one tomb is left, if we have frost bombs around, not a big deal atm since in nm they dont one shot) Last phase bots los behind tomb to loose buffet, tanks swap when they have hi buffet count. Player should tell bots with skull rti if they should kill tomb or focus boss. todo (dynamic tomb los in last phase so that healers can see tank but also hide behind tomb to break los from boss) Removed some debug messages, improved LM spike action (nearest bots also try to help kill it) Improved Lady Deathwshiper shade action (only targeted bots will run away instead of every bot that is near it) dbs improved tank switch I recommend to use 3 healers (just to be safe) and 2 paladin tanks (warr seems to struggle with agro) in 10 man 25 man 6-7 healers (just to be safe) Since most of the bosses are about survival and not dps * LK Update (doable) LK added Improved tank switching for all bosses Fixed PP gas cloud kiting Malleable goo todo (dont know how to detect it since its not object or npc) just summon ranged bots to safe position to bypass BPC fixed OT sometimes not tanking kele kinectic bombs todo (for now player should take care of them) Sindragosa fixed rare case when she is in air phase but tombs went to last phase position LK Bots can handle necrotic Bots can handle adds Bots should focus valkyre that actually grabbed someone (if unlucky and player just use attack command and summon bots to you if they are far away from you) if they grab bots you can either summon to make them useless or let bots cc them and do it legit way. Defile should be watched by player and once it was cast just summon bots to you Vile spirits for some reason go to the ground and get nuked by bots aoe spells so there is not much to be done **Player needs to be alive the whole LK fight since you will have to watch out for frost spehers (sometimes bots ignore them), summon bots when defile is up and summon ranged bots if they get stuck near shambling or raging spirits since their aoe will wipe you) all in all LK is doable both 10 and 25nm, player needs to have knowledge of lk fight and needs to know how to use multibot addon and make macros for eg summoning or commanding groups of bots or individual bots) Dont forget frost/shadow/nature resist auras in whole ICC since it will help alot I have done whole icc 10 and 25 with 2 pala tanks, 2/5 heals and rest dps, if you use +1 or +2 heals it should be easier (since I was testing I did close to 0 dmg in fights same with heals) * fixed changes made by mistake fix * Malleable fix (simple spread mechanic) Malleable mechanic added (simple spread for now) Gas cloud fixed (Bots sometimes got stuck between puddle and kite location) * Defile Update Bots detect and avoid defile (they struggle to find a way back to the boss around it tho, use summon to help them) Melee bots should be able to stand behind/to the flank of shambling/spirits * GS fixed bots not returning to their ship for A and H Bots will return back to ship after killing mage * PP gas cloud kiting improved PP gas cloud kiting improved * BPC targeting fixed Bots will mark valid prince with skull RTI now * BQL added melee spread in air pahse BQL added melee spread * VDW healing rotation improved Healers will now use strong heals and hots * Fixed Necrotic Plague Fixed issue with Necrotic where it would get dispelled too soon, or would not get dispelled at all * LK Update Refined defile logic Added 3 points for ranged and melee in winter phase (east, west, south when facing throne) fixed frost spheres targeting (hunter will focus them) Atm bots will reset z axis if they fall underground or if they get teleported by lk Better positioning in 1st phase * 10HC update until PP LK defile improved for 10nm (bots sometimes stood 2 close to defile until it grew few times) Improved rotface for HC PP remade for 10HC. Gas cloud is now properly kited Fixed a rare case of server crash when there were ooze and gas cloud alive at same time. Bots will move around puddles according to its size now. Bots that get unboud plague will simply move away from raid and die thus loosing it from raid. Volatile ooze improved stacking. Fixed ranged sometimes glitching thru walls when spreading out from other members. 10HC PP is now doable but its hard without summoning (summoning break gascloud and ooze targets so its easier to do). You need to watch boss so you dont phase 2 soon otherwise you will get 2x ooze and cloud which is almoust always a wipe. If abo is not played near perfection bots will struggle with oozes and gas clouds if they are not slowed on time. Always save energy to slow gas cloud since it will wipe the group if it reach its target. Bots will sometimes stand in puddle, just command them to move and they will figure out what to do. todo (proper malleable handling) * Up until Sindy 10HC BPC added shadow prison handling, bots stop moving if more than 12 stack, tanks more than 18 Improved spreading logic VDW fixed issue where bots in portal wold move at half speed compared to real player * fixed accidental change * LK 10HC update Added Shadow trap logic (if they stand in it, not a big deal since bots wont get yeeted only players will) When harvest soul, only player will be in another dimension (you must survive) **Sindy and LK can be done, but I must dissapoint purist, at my skill level I could not achieve to do HC without using summon or other "cheat" bot functions. Other bosses are all doable now in 10hc * ICC fixes GS, PP; BQL, SINDY Minor fixes, bite action improved * ICC improve Sindy Bots will now choose non beacon position based on difficulty, 10/25 * ICC fixed missing A/H buff Fixed missing ICC buff for A and H Buff will only be present when logged on and in ICC, once any bot or player leave ICC the buff is gone to prevent abuse. This will make ICC easier now and with recent DPS update and movement improvement bots will now actully do decent dps and even greater healing. Ally buff https://www.wowhead.com/wotlk/spell=73828/strength-of-wrynn Horde buff https://www.wowhead.com/wotlk/spell=73822/hellscreams-warsong * revert last change revert buff * ICC improve Rotface Bots will now mark Rotface with skull icon, which will make them focus boss instead of oozes automatically * ICC Festergut 10 Man fix There was a rare case in 10 Man when 2 tanks would get spores which made them both stack at melee spot. Now the code will check if any of the spored player is main tank, if it is, it will stay at melee and other spores will go at ranged spot since off tank doesn't really need to stand near main tank all the time. * ICC BPC major update, fix and improve Fixed main tank sometimes not tanking both bosses (vala and talda) Improved marking of current prince Empowered vortex: bots will now spread out when it is being cast, instead of always spreading(ranged). This will make melee also spread better now since bots will calculate and move to optimal positions. Added Kinetic bomb handling. Hunters will take care of bombs, if no hunter is present then any ranged dps will take care of kinetic bombs. * ICC BQL/VDW major update + minor fixes/improvements LDW improved spreading for ranged, tanks will move boss in the middle of the room now when shield is depleted which will make bots bug less around pillars GS Assist tank will not teleport to enemy ship and will keep tanking adds on our ship now DBS ranged spread improved (they will calcualte spot for each bot and stick with it reducing movment to minimum), fixed bug where in 25 man ranged bots would go behind walls, making them unable to dps boss, improved rune of blood tanking Festergut ranged spread improved (they will calcualte spot for each bot and stick with it reducing movment to minimum) BQL Melee spread improved, bite logic improved, added swarming shadows logic (not perfect but at least it wont be all over the room anymore), Tanks will properly handle blood mirror now VDW boss and raid healers will be automatically assinged depening by number of healers(if more than 3 healers in group, 2 will focus on raid and others will heal boss, otherwise one healer will heal raid). Druid will be assigned to raid healing if no druid present then some other random heal class. Added rotations for druid healers if they end up healing the boss. Raid healers will not use portals anymore. Healers will come to the ground now after using portals (they were stuck in air) * ICC LK minor update PP Removed pre stacking for ranged when volatile ooze spawn (they will kill it much faster now and only stack if someone actually gets targeted by ooze) LK Hunters will use trnaq shot to remove enrage from shamblings Improved valkyr cc Minimized ping-ponging during winter phase * ICC minor Sindy improve Reduced position tolerances and forced movement for beacon and non beacon positions which will make bots move to spot that they actually need to be at instead of randomly running to sindragosa or beaconed players. * ICC minor update GS Bots will mark mage with skull rti Rotface Fixed bots glitching thru walls and floors (added check if position is valid before moving) PP Bots will mark volatile ooze with skull rti now which will help them focus it and kill asap (usefull for heroic when both volatile ooze and gas cloud are present at the same time) VDW Added default group position in the middle if the room so that bots don't spread out too much which will force them to focus supressers more Fixed Boss healers not keeping themself alive when low on HP * ICC LK minor update Commented out z axis bypass since it was fixed with recent core updates. Bots no longer fall thru buggy platforms so its no longer needed which in return makes valkyrs works as they should. * ICC LM & LDW Improved LM Removed Attack Action since it was buggy and replaced it with RTI. (Improved DPS) Simplified trigger for spike action, since attack logic is handled by skull RTI now. Bots (tanks) will mark spikes with skull RTI, which will make bots instantly switch to Spike targets. If no spike is present, the boss will be marked. Added logic that will make non-tank bots move away from LM to a fixed position behind LM if they happened to be in front of LM (No more insta kills with cleave). If LM is casting Bone Storm, bots will ignore the above logic and do max dps. LDW Removed Attack Action since it was buggy and replaced it with RTI. (Improved DPS) Bots (tanks) will mark adds with skull RTI, which will make bots instantly switch to adds targets. If no spike is present, the boss will be marked, which will help with keeping allies alive when they get mind controlled by LDW. Moved 2nd phase position deeper into the room and reduced boss hp trigger to 95%, which will keep tanks where they should be during 1st phase (sometimes the boss dropped below 98% and tanks would ping pong around the room). Ranged bots will now spread closer to each other now and will ignore spreading if they are 2 far or 2 close to the boss so they can properly reposition. These changes should also fix recent bugs with bots not casting/standing still on LM and LDW. * ICC DBS Improve Replaced attack action with RTI. (Improved DPS) Ranged bots will move away from blood beast if targeted by it now. * ICC Sindragosa and LK Improve Sindragosa Improved tomb los for frost bombs. Bots will now mark tomb with moon rti to avoid killing it too soon. (If they still attack the tomb, simply reset and after bombs are over, simply attack the tomb to kill or mark with a skull.) Main Tank will reset mystic buffet stacks now to avoid tank swapping since it's really unreliable which will make sindy less frustrating to kill (other bots will need to do mechanics properly by hiding behind ice tombs). LK Necrotic plague action Bots will now check before moving to the Shambling if they are in front of the Shambling to avoid getting one shoted by shockwave. Improved multiplier, which will now properly handle dispelling of necrotic plague. Winter phase action Bots will properly move behind adds to avoid getting one shoted by shamblings or raging spirits. If there is a hunter in the grouphunter will focus on spheres; if not, any ranged DPS will focus spheres. Adds action Bots will now be in proper positions during the 1st phase, and when the off tank is done collecting shamblings, it will move away from the group. Bots (non-tanks) will move behind the shambling if they are in front of it to avoid getting one shoted in the 1st phase. Restored action for checking if bots are at ground level, since they were still sometimes glitching through the floor (valkrys will be able to carry bots and drop them off the edge if not killed in time). Improved defile calculations for 10 man and 25 man raids since defile was growing differently for 10 and 25, which would make bots in 25 man move very far away from somewhat small defile puddle. Bots will now properly CC any valkyr that wasn't CC'd, and they won't freak out anymore if multiple valkyrs are present. (They can't tell which valkyr is holding bots/ players, so we should command an attack on valkyrs that are holding someone.) Bots will now run away from Vile Spirits if they are targeted by them during the last phase. * ICC 25HC ldw shades fix Fixed bots could not detect shade if there were multiple present * ICC trigger optimization Optimized trigger and action for: KineticBomb OozePuddle * ICC LK Improve Improved Shadow Trap, Winter phase and 1st phase. Bots will now have fixed positions for 1st phase if no shadow trap is present. Tanks will now take care of taunting adds during winter phase. (Assist tank will collect adds and bring them to main tank, main tank will take over the adds so we have minimal chance of adds rotating towards the raid) Fixed shadow trap detection. * ToC dungeon update Added TOC dungeon strategies for 1st encounter. Other encounters are pretty easy and straightforward so they do not require strategies. They can be all done with follow, reset, summon, attack, skull icon etc... Bots (A/H) will automatically check if they have lance equiped or in inventory. If they don't have lance they will pick it up from nearest lance stand and equip it. Bots will mount nearest mount if they have lance equipped. Once mounted they will keep Defend aura at 3 stacks (top priority). In fight they will cast charge if more than 5f away, they will cast shield break if target has Defended aura active and they will use Thrust if they cant use anything else. In short they will melt any target and enemies wont stand a chance keeping their defend aura up. Once champions are defeated bot will automatically equip best weapon they have in inventory (weapon that was swapped with lance) * ICC minor update LM Bots will now move in smaller increments to get behind boss They will still move towards fixed position, but as soon they are not in front of boss, they will stop moving and do other actions. GS Fixed cannon trigger, now will return true if cannon is 100f from bots. It used to return true all the time. * ToC added Eadric strat bots will look away during radiance cast * ICC LK minor fix fixed crash in icclichkingsaddsaction * ICC GS fix While encountering edge case when playing horde, the logic was to detect mages, there are usually 2 around, but the longer the fight last the bigger the chance that cannons would kill all the mages thus once we kill the mage that was casting deep freeze bots could not execute action that would return them back to the ship, so the boss had the time to nuke them one by one. Even tanks have hard time surviving the boss. I have changed it now (both A/H) so that we keep track of the enemy boss (which is hard if not impossible to kill as main trigger) and we are keeping track of the mage that is actually casting deep freeze, we mark it with skull, and now as soon the mage is dead, bots will remove the mark so there is 0 confusion about what to do next on their side, and they will come back asap. * FoS Improved and fixed, ICC LM, LDW and PP updates FoS Improved/fixed triggers, Improved/fixed actions. Bronjahm Tank will properly kite soul fragment around the room Replaced attack action with skull rti to focus soul fragments Non-tank bots will only try to stay within 10f of boss only once we enter last phase, tank will be in the middle of the room if it is not kiting soul fragment Devourer of Souls DPS bots will stop dps if boss is casting mirrored soul Laser can be easily avoided with follow/summon ICC LM Tank will only move to tank position if it actually have the agro, it used to move there without agro and few of the bots would get killed since tank was unable to reach bot LDW Fixed edge case where tank would be target of shade and it would be unable to move away it from it due to tank position restrictions, now if Tank is targeted by shade it will be able to move and run away from it (in hc this could cause wipes since explosion is aoe + tons of dmg) PP When oozes spawn, tank will move towards green ooze spawn to keep raid near green ooze (faster kill) and far away from gas cloud (better chance to kill if abo didn't manage to slow it) * PoS Strategies Ick and Krick All boss mechanics implemented Forgemaster Garfrost Easily doable with follow/stay/summon Scourgelord Tyrannus Ranged bots will spread so that they all don't get frozen by dragon They avoid everything else on their own * ICC Refactor, All bosses improved NM and HC Major Update * Update playerbots.conf.dist --- conf/playerbots.conf.dist | 6 + src/PlayerbotAIConfig.cpp | 4 + src/PlayerbotAIConfig.h | 1 + .../raids/icecrown/RaidIccActionContext.h | 50 +- .../raids/icecrown/RaidIccActions.cpp | 10026 +++++++++++----- src/strategy/raids/icecrown/RaidIccActions.h | 316 +- .../raids/icecrown/RaidIccMultipliers.cpp | 780 +- .../raids/icecrown/RaidIccMultipliers.h | 61 +- .../raids/icecrown/RaidIccStrategy.cpp | 71 +- .../raids/icecrown/RaidIccTriggerContext.h | 64 +- .../raids/icecrown/RaidIccTriggers.cpp | 643 +- src/strategy/raids/icecrown/RaidIccTriggers.h | 288 +- 12 files changed, 8756 insertions(+), 3554 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index fb98c77c..0a2f3b68 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -1888,3 +1888,9 @@ AiPlayerbot.TargetPosRecalcDistance = 0.1 # Allow bots to be summoned near innkeepers AiPlayerbot.SummonAtInnkeepersEnabled = 1 + +# Enable buffs in ICC to make Heroic easier and more casual. +# 30% more damage, 40% damage reduction (tank bots), increased all resistances, reduced threat for non tank bots, increased threat for tank bots. +# Buffs will be applied on PP, Sindragosa and Lich King + +AiPlayerbot.EnableICCBuffs = 1 \ No newline at end of file diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index 326e2f5c..3bfd25ba 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -189,6 +189,10 @@ bool PlayerbotAIConfig::Initialize() maxRandomBotsPriceChangeInterval = sConfigMgr->GetOption("AiPlayerbot.MaxRandomBotsPriceChangeInterval", 48 * HOUR); randomBotJoinLfg = sConfigMgr->GetOption("AiPlayerbot.RandomBotJoinLfg", true); + + //////////////////////////// ICC + + EnableICCBuffs = sConfigMgr->GetOption("AiPlayerbot.EnableICCBuffs", true); //////////////////////////// CHAT enableBroadcasts = sConfigMgr->GetOption("AiPlayerbot.EnableBroadcasts", true); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 0045d69e..82ce3594 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -56,6 +56,7 @@ public: bool enabled; bool disabledWithoutRealPlayer; + bool EnableICCBuffs; bool allowAccountBots, allowGuildBots, allowTrustedAccountBots; bool randomBotGuildNearby, randomBotInvitePlayer, inviteChat; uint32 globalCoolDown, reactDelay, maxWaitForMove, disableMoveSplinePath, maxMovementSearchTime, expireActionTime, diff --git a/src/strategy/raids/icecrown/RaidIccActionContext.h b/src/strategy/raids/icecrown/RaidIccActionContext.h index 18097e6f..4bed40c1 100644 --- a/src/strategy/raids/icecrown/RaidIccActionContext.h +++ b/src/strategy/raids/icecrown/RaidIccActionContext.h @@ -12,40 +12,52 @@ public: { creators["icc lm tank position"] = &RaidIccActionContext::icc_lm_tank_position; creators["icc spike"] = &RaidIccActionContext::icc_spike; + creators["icc dark reckoning"] = &RaidIccActionContext::icc_dark_reckoning; creators["icc ranged position lady deathwhisper"] = &RaidIccActionContext::icc_ranged_position_lady_deathwhisper; creators["icc adds lady deathwhisper"] = &RaidIccActionContext::icc_adds_lady_deathwhisper; creators["icc shade lady deathwhisper"] = &RaidIccActionContext::icc_shade_lady_deathwhisper; + creators["icc rotting frost giant tank position"] = &RaidIccActionContext::icc_rotting_frost_giant_tank_position; creators["icc cannon fire"] = &RaidIccActionContext::icc_cannon_fire; creators["icc gunship enter cannon"] = &RaidIccActionContext::icc_gunship_enter_cannon; creators["icc gunship teleport ally"] = &RaidIccActionContext::icc_gunship_teleport_ally; creators["icc gunship teleport horde"] = &RaidIccActionContext::icc_gunship_teleport_horde; + creators["icc dbs tank position"] = &RaidIccActionContext::icc_dbs_tank_position; creators["icc adds dbs"] = &RaidIccActionContext::icc_adds_dbs; - creators["icc festergut tank position"] = &RaidIccActionContext::icc_festergut_tank_position; + + creators["icc festergut group position"] = &RaidIccActionContext::icc_festergut_group_position; creators["icc festergut spore"] = &RaidIccActionContext::icc_festergut_spore; + creators["icc rotface tank position"] = &RaidIccActionContext::icc_rotface_tank_position; creators["icc rotface group position"] = &RaidIccActionContext::icc_rotface_group_position; creators["icc rotface move away from explosion"] = &RaidIccActionContext::icc_rotface_move_away_from_explosion; + creators["icc putricide volatile ooze"] = &RaidIccActionContext::icc_putricide_volatile_ooze; creators["icc putricide gas cloud"] = &RaidIccActionContext::icc_putricide_gas_cloud; creators["icc putricide growing ooze puddle"] = &RaidIccActionContext::icc_putricide_growing_ooze_puddle; - creators["avoid malleable goo"] = &RaidIccActionContext::avoid_malleable_goo; + creators["icc putricide avoid malleable goo"] = &RaidIccActionContext::icc_putricide_avoid_malleable_goo; + creators["icc bpc keleseth tank"] = &RaidIccActionContext::icc_bpc_keleseth_tank; - creators["icc bpc nucleus"] = &RaidIccActionContext::icc_bpc_nucleus; creators["icc bpc main tank"] = &RaidIccActionContext::icc_bpc_main_tank; creators["icc bpc empowered vortex"] = &RaidIccActionContext::icc_bpc_empowered_vortex; creators["icc bpc kinetic bomb"] = &RaidIccActionContext::icc_bpc_kinetic_bomb; - creators["icc bql tank position"] = &RaidIccActionContext::icc_bql_tank_position; + creators["icc bpc ball of flame"] = &RaidIccActionContext::icc_bpc_ball_of_flame; + + creators["icc bql group position"] = &RaidIccActionContext::icc_bql_group_position; creators["icc bql pact of darkfallen"] = &RaidIccActionContext::icc_bql_pact_of_darkfallen; creators["icc bql vampiric bite"] = &RaidIccActionContext::icc_bql_vampiric_bite; + creators["icc valkyre spear"] = &RaidIccActionContext::icc_valkyre_spear; creators["icc sister svalna"] = &RaidIccActionContext::icc_sister_svalna; + + creators["icc valithria group"] = &RaidIccActionContext::icc_valithria_group; creators["icc valithria portal"] = &RaidIccActionContext::icc_valithria_portal; creators["icc valithria heal"] = &RaidIccActionContext::icc_valithria_heal; creators["icc valithria dream cloud"] = &RaidIccActionContext::icc_valithria_dream_cloud; - creators["icc sindragosa tank position"] = &RaidIccActionContext::icc_sindragosa_tank_position; + + creators["icc sindragosa group position"] = &RaidIccActionContext::icc_sindragosa_group_position; creators["icc sindragosa frost beacon"] = &RaidIccActionContext::icc_sindragosa_frost_beacon; creators["icc sindragosa blistering cold"] = &RaidIccActionContext::icc_sindragosa_blistering_cold; creators["icc sindragosa unchained magic"] = &RaidIccActionContext::icc_sindragosa_unchained_magic; @@ -53,6 +65,7 @@ public: creators["icc sindragosa mystic buffet"] = &RaidIccActionContext::icc_sindragosa_mystic_buffet; creators["icc sindragosa frost bomb"] = &RaidIccActionContext::icc_sindragosa_frost_bomb; creators["icc sindragosa tank swap position"] = &RaidIccActionContext::icc_sindragosa_tank_swap_position; + creators["icc lich king shadow trap"] = &RaidIccActionContext::icc_lich_king_shadow_trap; creators["icc lich king necrotic plague"] = &RaidIccActionContext::icc_lich_king_necrotic_plague; creators["icc lich king winter"] = &RaidIccActionContext::icc_lich_king_winter; @@ -62,40 +75,52 @@ public: private: static Action* icc_lm_tank_position(PlayerbotAI* ai) { return new IccLmTankPositionAction(ai); } static Action* icc_spike(PlayerbotAI* ai) { return new IccSpikeAction(ai); } + static Action* icc_dark_reckoning(PlayerbotAI* ai) { return new IccDarkReckoningAction(ai); } static Action* icc_ranged_position_lady_deathwhisper(PlayerbotAI* ai) { return new IccRangedPositionLadyDeathwhisperAction(ai); } static Action* icc_adds_lady_deathwhisper(PlayerbotAI* ai) { return new IccAddsLadyDeathwhisperAction(ai); } static Action* icc_shade_lady_deathwhisper(PlayerbotAI* ai) { return new IccShadeLadyDeathwhisperAction(ai); } + static Action* icc_rotting_frost_giant_tank_position(PlayerbotAI* ai) { return new IccRottingFrostGiantTankPositionAction(ai); } static Action* icc_cannon_fire(PlayerbotAI* ai) { return new IccCannonFireAction(ai); } static Action* icc_gunship_enter_cannon(PlayerbotAI* ai) { return new IccGunshipEnterCannonAction(ai); } static Action* icc_gunship_teleport_ally(PlayerbotAI* ai) { return new IccGunshipTeleportAllyAction(ai); } - static Action* icc_gunship_teleport_horde(PlayerbotAI* ai) { return new IccGunshipTeleportHordeAction(ai); } + static Action* icc_gunship_teleport_horde(PlayerbotAI* ai) { return new IccGunshipTeleportHordeAction(ai); } + static Action* icc_dbs_tank_position(PlayerbotAI* ai) { return new IccDbsTankPositionAction(ai); } static Action* icc_adds_dbs(PlayerbotAI* ai) { return new IccAddsDbsAction(ai); } - static Action* icc_festergut_tank_position(PlayerbotAI* ai) { return new IccFestergutTankPositionAction(ai); } + + static Action* icc_festergut_group_position(PlayerbotAI* ai) { return new IccFestergutGroupPositionAction(ai); } static Action* icc_festergut_spore(PlayerbotAI* ai) { return new IccFestergutSporeAction(ai); } + static Action* icc_rotface_tank_position(PlayerbotAI* ai) { return new IccRotfaceTankPositionAction(ai); } static Action* icc_rotface_group_position(PlayerbotAI* ai) { return new IccRotfaceGroupPositionAction(ai); } - static Action* icc_rotface_move_away_from_explosion(PlayerbotAI* ai) { return new IccRotfaceMoveAwayFromExplosionAction(ai); } + static Action* icc_rotface_move_away_from_explosion(PlayerbotAI* ai) { return new IccRotfaceMoveAwayFromExplosionAction(ai); } + static Action* icc_putricide_volatile_ooze(PlayerbotAI* ai) { return new IccPutricideVolatileOozeAction(ai); } static Action* icc_putricide_gas_cloud(PlayerbotAI* ai) { return new IccPutricideGasCloudAction(ai); } static Action* icc_putricide_growing_ooze_puddle(PlayerbotAI* ai) { return new IccPutricideGrowingOozePuddleAction(ai); } - static Action* avoid_malleable_goo(PlayerbotAI* ai) { return new AvoidMalleableGooAction(ai); } + static Action* icc_putricide_avoid_malleable_goo(PlayerbotAI* ai) { return new IccPutricideAvoidMalleableGooAction(ai); } + static Action* icc_bpc_keleseth_tank(PlayerbotAI* ai) { return new IccBpcKelesethTankAction(ai); } - static Action* icc_bpc_nucleus(PlayerbotAI* ai) { return new IccBpcNucleusAction(ai); } static Action* icc_bpc_main_tank(PlayerbotAI* ai) { return new IccBpcMainTankAction(ai); } static Action* icc_bpc_empowered_vortex(PlayerbotAI* ai) { return new IccBpcEmpoweredVortexAction(ai); } static Action* icc_bpc_kinetic_bomb(PlayerbotAI* ai) { return new IccBpcKineticBombAction(ai); } - static Action* icc_bql_tank_position(PlayerbotAI* ai) { return new IccBqlTankPositionAction(ai); } + static Action* icc_bpc_ball_of_flame(PlayerbotAI* ai) { return new IccBpcBallOfFlameAction(ai); } + + static Action* icc_bql_group_position(PlayerbotAI* ai) { return new IccBqlGroupPositionAction(ai); } static Action* icc_bql_pact_of_darkfallen(PlayerbotAI* ai) { return new IccBqlPactOfDarkfallenAction(ai); } static Action* icc_bql_vampiric_bite(PlayerbotAI* ai) { return new IccBqlVampiricBiteAction(ai); } + static Action* icc_valkyre_spear(PlayerbotAI* ai) { return new IccValkyreSpearAction(ai); } static Action* icc_sister_svalna(PlayerbotAI* ai) { return new IccSisterSvalnaAction(ai); } + + static Action* icc_valithria_group(PlayerbotAI* ai) { return new IccValithriaGroupAction(ai); } static Action* icc_valithria_portal(PlayerbotAI* ai) { return new IccValithriaPortalAction(ai); } static Action* icc_valithria_heal(PlayerbotAI* ai) { return new IccValithriaHealAction(ai); } static Action* icc_valithria_dream_cloud(PlayerbotAI* ai) { return new IccValithriaDreamCloudAction(ai); } - static Action* icc_sindragosa_tank_position(PlayerbotAI* ai) { return new IccSindragosaTankPositionAction(ai); } + + static Action* icc_sindragosa_group_position(PlayerbotAI* ai) { return new IccSindragosaGroupPositionAction(ai); } static Action* icc_sindragosa_frost_beacon(PlayerbotAI* ai) { return new IccSindragosaFrostBeaconAction(ai); } static Action* icc_sindragosa_blistering_cold(PlayerbotAI* ai) { return new IccSindragosaBlisteringColdAction(ai); } static Action* icc_sindragosa_unchained_magic(PlayerbotAI* ai) { return new IccSindragosaUnchainedMagicAction(ai); } @@ -103,6 +128,7 @@ private: static Action* icc_sindragosa_mystic_buffet(PlayerbotAI* ai) { return new IccSindragosaMysticBuffetAction(ai); } static Action* icc_sindragosa_frost_bomb(PlayerbotAI* ai) { return new IccSindragosaFrostBombAction(ai); } static Action* icc_sindragosa_tank_swap_position(PlayerbotAI* ai) { return new IccSindragosaTankSwapPositionAction(ai); } + static Action* icc_lich_king_shadow_trap(PlayerbotAI* ai) { return new IccLichKingShadowTrapAction(ai); } static Action* icc_lich_king_necrotic_plague(PlayerbotAI* ai) { return new IccLichKingNecroticPlagueAction(ai); } static Action* icc_lich_king_winter(PlayerbotAI* ai) { return new IccLichKingWinterAction(ai); } diff --git a/src/strategy/raids/icecrown/RaidIccActions.cpp b/src/strategy/raids/icecrown/RaidIccActions.cpp index a29d0a11..833e1d8a 100644 --- a/src/strategy/raids/icecrown/RaidIccActions.cpp +++ b/src/strategy/raids/icecrown/RaidIccActions.cpp @@ -5,71 +5,72 @@ #include "Playerbots.h" #include "Timer.h" #include "Vehicle.h" +#include "RtiValue.h" #include "GenericSpellActions.h" #include "GenericActions.h" #include +#include "RaidIccTriggers.h" +#include "Multiplier.h" -enum CreatureIds { - NPC_KOR_KRON_BATTLE_MAGE = 37117, - NPC_KOR_KRON_AXETHROWER = 36968, - NPC_KOR_KRON_ROCKETEER = 36982, - - NPC_SKYBREAKER_SORCERER = 37116, - NPC_SKYBREAKER_RIFLEMAN = 36969, - NPC_SKYBREAKER_MORTAR_SOLDIER = 36978, - - NPC_IGB_HIGH_OVERLORD_SAURFANG = 36939, - NPC_IGB_MURADIN_BRONZEBEARD = 36948, -}; - -const std::vector availableTargets = { - NPC_KOR_KRON_AXETHROWER, NPC_KOR_KRON_ROCKETEER, NPC_KOR_KRON_BATTLE_MAGE, - NPC_IGB_HIGH_OVERLORD_SAURFANG, NPC_SKYBREAKER_RIFLEMAN, NPC_SKYBREAKER_MORTAR_SOLDIER, - NPC_SKYBREAKER_SORCERER, NPC_IGB_MURADIN_BRONZEBEARD -}; - -static std::vector sporeOrder; - -//Lord Marrowgwar +// Lord Marrowgwar bool IccLmTankPositionAction::Execute(Event event) { Unit* boss = AI_VALUE2(Unit*, "find target", "lord marrowgar"); if (!boss) return false; - if (botAI->IsTank(bot)) + if (!botAI->IsTank(bot)) + return false; + + const bool isBossInBoneStorm = botAI->GetAura("Bone Storm", boss) != nullptr; + + if (isBossInBoneStorm) + return false; + + if (botAI->HasAggro(boss) && botAI->IsMainTank(bot)) { - if ((botAI->HasAggro(boss) && botAI->IsMainTank(bot)) || botAI->IsAssistTank(bot)) - { - float distance = bot->GetExactDist2d(ICC_LM_TANK_POSITION.GetPositionX(), ICC_LM_TANK_POSITION.GetPositionY()); - if (distance > 3.0f) - { - // Calculate direction vector - float dirX = ICC_LM_TANK_POSITION.GetPositionX() - bot->GetPositionX(); - float dirY = ICC_LM_TANK_POSITION.GetPositionY() - bot->GetPositionY(); - float length = sqrt(dirX * dirX + dirY * dirY); - dirX /= length; - dirY /= length; + const float maxDistanceThreshold = 3.0f; + const float distance = bot->GetExactDist2d(ICC_LM_TANK_POSITION.GetPositionX(), ICC_LM_TANK_POSITION.GetPositionY()); - // Move in increments of 3.0f - float moveX = bot->GetPositionX() + dirX * 3.0f; - float moveY = bot->GetPositionY() + dirY * 3.0f; - - return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT); - } - } + if (distance > maxDistanceThreshold) + return MoveTowardPosition(ICC_LM_TANK_POSITION, maxDistanceThreshold); } + if (botAI->IsAssistTank(bot)) + { + const float maxDistanceThreshold = 3.0f; + const float distance = + bot->GetExactDist2d(ICC_LM_TANK_POSITION.GetPositionX(), ICC_LM_TANK_POSITION.GetPositionY()); + + if (distance > maxDistanceThreshold) + return MoveTowardPosition(ICC_LM_TANK_POSITION, maxDistanceThreshold); + } return false; } +bool IccLmTankPositionAction::MoveTowardPosition(const Position& position, float incrementSize) +{ + // Calculate direction vector + const float dirX = position.GetPositionX() - bot->GetPositionX(); + const float dirY = position.GetPositionY() - bot->GetPositionY(); + const float length = std::sqrt(dirX * dirX + dirY * dirY); + + // Normalize direction vector + const float normalizedDirX = dirX / length; + const float normalizedDirY = dirY / length; + + // Calculate new position with increment + const float moveX = bot->GetPositionX() + normalizedDirX * incrementSize; + const float moveY = bot->GetPositionY() + normalizedDirY * incrementSize; + + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); +} + bool IccSpikeAction::Execute(Event event) { - Aura* aura = botAI->GetAura("Impaled", bot); - // If we're impaled, we can't do anything - if (aura) + if (botAI->GetAura("Impaled", bot)) return false; // Find the boss @@ -77,53 +78,51 @@ bool IccSpikeAction::Execute(Event event) if (!boss) return false; - Aura* bossaura = botAI->GetAura("Bone Storm", boss); + const bool isBossInBoneStorm = botAI->GetAura("Bone Storm", boss) != nullptr; + const bool shouldMoveToSafePosition = boss->isInFront(bot) && !botAI->IsTank(bot) && !isBossInBoneStorm; - if (boss->isInFront(bot) && !botAI->IsTank(bot) && !bossaura) + if (shouldMoveToSafePosition) { - float distance = bot->GetExactDist2d(-390.6757f, 2230.5283f); - if (distance > 3.0f) - { - // Calculate direction vector - float dirX = -390.6757f - bot->GetPositionX(); - float dirY = 2230.5283f - bot->GetPositionY(); - float length = sqrt(dirX * dirX + dirY * dirY); - dirX /= length; - dirY /= length; + const Position safePosition{-390.6757f, 2230.5283f, 0.0f}; // Z value to be overridden by actual bot Z + const float distance = bot->GetExactDist2d(safePosition.GetPositionX(), safePosition.GetPositionY()); + const float maxDistanceThreshold = 3.0f; - // Move in increments of 3.0f - float moveX = bot->GetPositionX() + dirX * 3.0f; - float moveY = bot->GetPositionY() + dirY * 3.0f; + if (distance > maxDistanceThreshold) + return MoveTowardPosition(safePosition, maxDistanceThreshold); - return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT); - } return false; } if (!botAI->IsTank(bot)) return false; - GuidVector spikes = AI_VALUE(GuidVector, "possible targets no los"); - const uint32 spikeEntries[] = {36619, 38711, 38712}; // spikes id's + return HandleSpikeTargeting(boss); +} + +bool IccSpikeAction::HandleSpikeTargeting(Unit* boss) +{ + static const std::array spikeEntries = {NPC_SPIKE1, NPC_SPIKE2, NPC_SPIKE3}; + const GuidVector spikes = AI_VALUE(GuidVector, "possible targets no los"); Unit* priorityTarget = nullptr; bool anySpikesExist = false; // First check for alive spikes - for (uint32 entry : spikeEntries) + for (const auto entry : spikeEntries) { - for (const ObjectGuid& guid : spikes) + for (const auto& guid : spikes) { - Unit* unit = botAI->GetUnit(guid); - if (unit && unit->GetEntry() == entry) // Check if spike exists + if (Unit* unit = botAI->GetUnit(guid)) { - anySpikesExist = true; // At least one spike exists - - if (unit->IsAlive()) // Only consider alive ones for targeting + if (unit->GetEntry() == entry) { - priorityTarget = unit; - break; + anySpikesExist = true; // At least one spike exists + + if (unit->IsAlive()) + { // Only consider alive ones for targeting + priorityTarget = unit; + break; + } } } } @@ -133,48 +132,66 @@ bool IccSpikeAction::Execute(Event event) // Only fallback to boss if NO spikes exist at all (alive or dead) if (!anySpikesExist && boss->IsAlive()) - { priorityTarget = boss; - } - // Update skull icon if needed + // Update raid target icon if needed if (priorityTarget) - { - if (Group* group = bot->GetGroup()) - { - ObjectGuid currentSkull = group->GetTargetIcon(7); - Unit* currentSkullUnit = botAI->GetUnit(currentSkull); - - bool needsUpdate = false; - if (!currentSkullUnit || !currentSkullUnit->IsAlive()) - { - needsUpdate = true; - } - else if (currentSkullUnit != priorityTarget) - { - needsUpdate = true; - } - - if (needsUpdate) - { - group->SetTargetIcon(7, bot->GetGUID(), priorityTarget->GetGUID()); - } - } - } + UpdateRaidTargetIcon(priorityTarget); return false; - } -//Lady +bool IccSpikeAction::MoveTowardPosition(const Position& position, float incrementSize) +{ + // Calculate direction vector + const float dirX = position.GetPositionX() - bot->GetPositionX(); + const float dirY = position.GetPositionY() - bot->GetPositionY(); + const float length = std::sqrt(dirX * dirX + dirY * dirY); + + // Normalize direction vector + const float normalizedDirX = dirX / length; + const float normalizedDirY = dirY / length; + + // Calculate new position with increment + const float moveX = bot->GetPositionX() + normalizedDirX * incrementSize; + const float moveY = bot->GetPositionY() + normalizedDirY * incrementSize; + + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); +} + +void IccSpikeAction::UpdateRaidTargetIcon(Unit* target) +{ + static constexpr uint8_t SKULL_ICON_INDEX = 7; + + if (Group* group = bot->GetGroup()) + { + const ObjectGuid currentSkull = group->GetTargetIcon(SKULL_ICON_INDEX); + Unit* currentSkullUnit = botAI->GetUnit(currentSkull); + + const bool needsUpdate = !currentSkullUnit || !currentSkullUnit->IsAlive() || currentSkullUnit != target; + + if (needsUpdate) + group->SetTargetIcon(SKULL_ICON_INDEX, bot->GetGUID(), target->GetGUID()); + } +} + +// Lady Deathwhisper bool IccDarkReckoningAction::Execute(Event event) { - if (bot->HasAura(69483) && bot->GetExactDist2d(ICC_DARK_RECKONING_SAFE_POSITION) > 2.0f) //dark reckoning spell id + constexpr float SAFE_DISTANCE_THRESHOLD = 2.0f; + + // Check if the bot needs to move to the safe position + if (bot->HasAura(SPELL_DARK_RECKONING) && + bot->GetExactDist2d(ICC_DARK_RECKONING_SAFE_POSITION) > SAFE_DISTANCE_THRESHOLD) { + // Move to the safe position with the same parameters as before return MoveTo(bot->GetMapId(), ICC_DARK_RECKONING_SAFE_POSITION.GetPositionX(), - ICC_DARK_RECKONING_SAFE_POSITION.GetPositionY(), ICC_DARK_RECKONING_SAFE_POSITION.GetPositionZ(), false, - false, false, true, MovementPriority::MOVEMENT_NORMAL); + ICC_DARK_RECKONING_SAFE_POSITION.GetPositionY(), + ICC_DARK_RECKONING_SAFE_POSITION.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_NORMAL); } + return false; } @@ -184,40 +201,98 @@ bool IccRangedPositionLadyDeathwhisperAction::Execute(Event event) if (!boss) return false; - float currentDistance = bot->GetDistance2d(boss); + const float currentDistance = bot->GetDistance2d(boss); + const float minDistance = 7.0f; + const float maxDistance = 30.0f; - if (currentDistance < 7.0f || currentDistance > 30.0f) + if (currentDistance < minDistance || currentDistance > maxDistance) return false; - if (botAI->IsRanged(bot) || botAI->IsHeal(bot)) - { - float radius = 3.0f; - float moveIncrement = 2.0f; - bool isRanged = botAI->IsRanged(bot); + if (!botAI->IsRanged(bot) && !botAI->IsHeal(bot)) + return false; - GuidVector members = AI_VALUE(GuidVector, "group members"); - if (isRanged) + return MaintainRangedSpacing(); +} + +bool IccRangedPositionLadyDeathwhisperAction::MaintainRangedSpacing() +{ + const float safeSpacingRadius = 3.0f; + const float moveIncrement = 2.0f; + const float maxMoveDistance = 5.0f; // Limit maximum movement distance + const bool isRanged = botAI->IsRanged(bot) || botAI->IsHeal(bot); + + if (!isRanged) + return false; + + // Ranged: spread from other members + const GuidVector members = AI_VALUE(GuidVector, "group members"); + + // Calculate a combined vector representing all nearby members' positions + float totalX = 0.0f; + float totalY = 0.0f; + int nearbyCount = 0; + + for (const auto& memberGuid : members) { - // Ranged: spread from other members - for (auto& member : members) + Unit* member = botAI->GetUnit(memberGuid); + if (!member || !member->IsAlive() || member == bot) { - Unit* unit = botAI->GetUnit(member); - if (!unit || !unit->IsAlive() || unit == bot) - continue; + continue; + } - float dist = bot->GetExactDist2d(unit); - if (dist < radius) + const float distance = bot->GetExactDist2d(member); + if (distance < safeSpacingRadius) + { + // Calculate vector from member to bot + float dx = bot->GetPositionX() - member->GetPositionX(); + float dy = bot->GetPositionY() - member->GetPositionY(); + + // Weight by inverse distance (closer members have more influence) + float weight = (safeSpacingRadius - distance) / safeSpacingRadius; + totalX += dx * weight; + totalY += dy * weight; + nearbyCount++; + } + } + + // If we have nearby members, move away in the combined direction + if (nearbyCount > 0) + { + // Normalize the combined vector + float magnitude = std::sqrt(totalX * totalX + totalY * totalY); + if (magnitude > 0.001f) // Avoid division by zero + { + totalX /= magnitude; + totalY /= magnitude; + + // Calculate move distance based on nearest member + float moveDistance = std::min(moveIncrement, maxMoveDistance); + + // Create target position in the combined direction + float targetX = bot->GetPositionX() + totalX * moveDistance; + float targetY = bot->GetPositionY() + totalY * moveDistance; + float targetZ = bot->GetPositionZ(); // Maintain current Z + + // Check if the target position is valid and move there + if (bot->IsWithinLOS(targetX, targetY, targetZ)) { - float moveDistance = std::min(moveIncrement, radius - dist + 1.0f); - return FleePosition(unit->GetPosition(), moveDistance, 250U); - // return MoveAway(unit, moveDistance); + Position targetPos(targetX, targetY, targetZ); + MoveTo(bot->GetMapId(), targetPos.GetPositionX(), targetPos.GetPositionY(), targetPos.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_NORMAL); + } + else + { + // If los check fails, try shorter distance + targetX = bot->GetPositionX() + totalX * (moveDistance * 0.5f); + targetY = bot->GetPositionY() + totalY * (moveDistance * 0.5f); + Position targetPos(targetX, targetY, targetZ); + MoveTo(bot->GetMapId(), targetPos.GetPositionX(), targetPos.GetPositionY(), targetPos.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_NORMAL); } } } - return false; // Everyone is in position - } - return false; + return false; // Everyone is properly spaced } bool IccAddsLadyDeathwhisperAction::Execute(Event event) @@ -226,54 +301,79 @@ bool IccAddsLadyDeathwhisperAction::Execute(Event event) if (!boss) return false; - if (botAI->IsTank(bot) && boss->GetHealthPct() < 95.0f) + if (botAI->HasAura("Dominate Mind", bot, false, false) && !bot->HasAura(SPELL_CYCLONE)) + bot->AddAura(SPELL_CYCLONE, bot); + else if (bot->HasAura(SPELL_CYCLONE) && !botAI->HasAura("Dominate Mind", bot, false, false)) + bot->RemoveAura(SPELL_CYCLONE); + + const uint32 shadeEntryId = NPC_SHADE; + + if (botAI->IsTank(bot) && boss->HealthBelowPct(95)) { - // Check if the bot is not the victim of a shade with entry 38222 - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto& npc : npcs) + // Check if the bot is not the victim of a shade + if (IsTargetedByShade(shadeEntryId)) + return false; + + const float maxDistanceToTankPosition = 20.0f; + const float moveIncrement = 3.0f; + + const float distance = + bot->GetExactDist2d(ICC_LDW_TANK_POSTION.GetPositionX(), ICC_LDW_TANK_POSTION.GetPositionY()); + + if (distance > maxDistanceToTankPosition) { - Unit* unit = botAI->GetUnit(npc); - if (unit && unit->GetEntry() == 38222 && unit->GetVictim() == bot) - { - return false; // Exit if the bot is the victim of the shade - } - } - - float distance = bot->GetExactDist2d(ICC_LDW_TANK_POSTION.GetPositionX(), ICC_LDW_TANK_POSTION.GetPositionY()); - if (distance > 20.0f) - { - // Calculate direction vector - float dirX = ICC_LDW_TANK_POSTION.GetPositionX() - bot->GetPositionX(); - float dirY = ICC_LDW_TANK_POSTION.GetPositionY() - bot->GetPositionY(); - float length = sqrt(dirX * dirX + dirY * dirY); - dirX /= length; - dirY /= length; - - // Move in increments of 3.0f - float moveX = bot->GetPositionX() + dirX * 3.0f; - float moveY = bot->GetPositionY() + dirY * 3.0f; - - return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT); + return MoveTowardPosition(ICC_LDW_TANK_POSTION, moveIncrement); } } if (!botAI->IsTank(bot)) return false; - Unit* currentTarget = AI_VALUE(Unit*, "current target"); - - GuidVector targets = AI_VALUE(GuidVector, "possible targets no los"); + return HandleAddTargeting(boss); +} - const uint32 targetsEntries[] = {37949, 38394, 38625, 38626, 38010, 38397, 39000, 39001, 38136, 38396, 38632, 38633, 37890, 38393, 38628, 38629, 38135, 38395, 38634, 38009, 38398, 38630, 38631}; //fanatics and adherents +bool IccAddsLadyDeathwhisperAction::IsTargetedByShade(uint32 shadeEntry) +{ + const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (const auto& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->GetEntry() == shadeEntry && unit->GetVictim() == bot) + return true; + } + return false; +} + +bool IccAddsLadyDeathwhisperAction::MoveTowardPosition(const Position& position, float incrementSize) +{ + // Calculate direction vector + const float dirX = position.GetPositionX() - bot->GetPositionX(); + const float dirY = position.GetPositionY() - bot->GetPositionY(); + const float length = std::sqrt(dirX * dirX + dirY * dirY); + + // Normalize direction vector + const float normalizedDirX = dirX / length; + const float normalizedDirY = dirY / length; + + // Calculate new position with increment + const float moveX = bot->GetPositionX() + normalizedDirX * incrementSize; + const float moveY = bot->GetPositionY() + normalizedDirY * incrementSize; + + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); +} + +bool IccAddsLadyDeathwhisperAction::HandleAddTargeting(Unit* boss) +{ + const GuidVector targets = AI_VALUE(GuidVector, "possible targets no los"); Unit* priorityTarget = nullptr; bool hasValidAdds = false; // First check for alive adds - for (uint32 entry : targetsEntries) + for (const auto& entry : addEntriesLady) { - for (const ObjectGuid& guid : targets) + for (const auto& guid : targets) { Unit* unit = botAI->GetUnit(guid); if (unit && unit->IsAlive() && unit->GetEntry() == entry) @@ -289,65 +389,64 @@ bool IccAddsLadyDeathwhisperAction::Execute(Event event) // Only fallback to boss if NO adds exist if (!hasValidAdds && boss->IsAlive()) - { priorityTarget = boss; - } // Update skull icon if needed if (priorityTarget) - { - if (Group* group = bot->GetGroup()) - { - ObjectGuid currentSkull = group->GetTargetIcon(7); - Unit* currentSkullUnit = botAI->GetUnit(currentSkull); - - bool needsUpdate = false; - if (!currentSkullUnit || !currentSkullUnit->IsAlive()) - { - needsUpdate = true; // No valid skull target - } - else if (currentSkullUnit != priorityTarget) - { - needsUpdate = true; // Different target than desired - } - - if (needsUpdate) - { - group->SetTargetIcon(7, bot->GetGUID(), priorityTarget->GetGUID()); - } - } - } + UpdateRaidTargetIcon(priorityTarget); return false; +} +void IccAddsLadyDeathwhisperAction::UpdateRaidTargetIcon(Unit* target) +{ + static constexpr uint8_t SKULL_ICON_INDEX = 7; + + if (Group* group = bot->GetGroup()) + { + const ObjectGuid currentSkull = group->GetTargetIcon(SKULL_ICON_INDEX); + Unit* currentSkullUnit = botAI->GetUnit(currentSkull); + + const bool needsUpdate = !currentSkullUnit || !currentSkullUnit->IsAlive() || currentSkullUnit != target; + + if (needsUpdate) + group->SetTargetIcon(SKULL_ICON_INDEX, bot->GetGUID(), target->GetGUID()); + } } bool IccShadeLadyDeathwhisperAction::Execute(Event event) { - const float radius = 12.0f; + static constexpr uint32 VENGEFUL_SHADE_ID = NPC_SHADE; + static constexpr float SAFE_DISTANCE = 12.0f; // Get the nearest hostile NPCs - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto& npc : npcs) + const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + for (const auto& npcGuid : npcs) { - Unit* unit = botAI->GetUnit(npc); - if (!unit || unit->GetEntry() != 38222) // vengeful shade ID + Unit* shade = botAI->GetUnit(npcGuid); + + // Skip if not a vengeful shade + if (!shade || shade->GetEntry() != VENGEFUL_SHADE_ID) continue; // Only run away if the shade is targeting us - // Check by GUID comparison to ensure we're accurately identifying the specific shade in 25HC multiple shades spawn. - if (unit->GetVictim() && unit->GetVictim()->GetGUID() == bot->GetGUID()) - { - float currentDistance = bot->GetDistance2d(unit); + // Check by GUID comparison to ensure we're accurately identifying the specific shade + // This is especially important in 25HC where multiple shades can spawn + if (!shade->GetVictim() || shade->GetVictim()->GetGUID() != bot->GetGUID()) + continue; - // Move away from the Vengeful Shade if the bot is too close - if (currentDistance < radius) - { - botAI->Reset(); // forces bot to stop channeling or getting locked by any other action - return MoveAway(unit, radius - currentDistance); - } + const float currentDistance = bot->GetDistance2d(shade); + + // Move away from the Vengeful Shade if the bot is too close + if (currentDistance < SAFE_DISTANCE) + { + // Forces bot to stop channeling or getting locked by any other action + botAI->Reset(); + return MoveAway(shade, SAFE_DISTANCE - currentDistance); } } + return false; } @@ -357,34 +456,220 @@ bool IccRottingFrostGiantTankPositionAction::Execute(Event event) if (!boss) return false; - bot->SetTarget(boss->GetGUID()); - if (botAI->IsTank(bot) || botAI->IsMainTank(bot) || botAI->IsAssistTank(bot)) - { + Aura* aura = botAI->GetAura("death plague", bot, false, false); + if (aura) + bot->RemoveAura(aura->GetId()); + +/* TODO: code works for handling plague, but atm script is bugged and one bot can have 2 plagues at the same time or when cured, which should not happen, and it is immpossible to handle plague atm the legit way. + const bool hasCure = botAI->GetAura("recently infected", bot) != nullptr; + + // Tank behavior - unchanged + if (botAI->IsTank(bot) && botAI->HasAggro(boss) && !isInfected) if (bot->GetExactDist2d(ICC_ROTTING_FROST_GIANT_TANK_POSITION) > 5.0f) return MoveTo(bot->GetMapId(), ICC_ROTTING_FROST_GIANT_TANK_POSITION.GetPositionX(), - ICC_ROTTING_FROST_GIANT_TANK_POSITION.GetPositionY(), ICC_ROTTING_FROST_GIANT_TANK_POSITION.GetPositionZ(), false, - false, false, true, MovementPriority::MOVEMENT_NORMAL); - } + ICC_ROTTING_FROST_GIANT_TANK_POSITION.GetPositionY(), + ICC_ROTTING_FROST_GIANT_TANK_POSITION.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_NORMAL); - float radius = 10.0f; - float distanceExtra = 2.0f; + if (botAI->IsTank(bot)) + return false; - GuidVector members = AI_VALUE(GuidVector, "group members"); - for (auto& member : members) + // Handle infected bot behavior - move near a non-infected, non-cured bot + if (isInfected) { - if (bot->GetGUID() == member) + const GuidVector members = AI_VALUE(GuidVector, "group members"); + + // Count how many bots are targeting each potential target + std::map targetCounts; + + // First, identify all infected bots and their current targets (approximate) + for (const auto& memberGuid : members) { - continue; + Unit* member = botAI->GetUnit(memberGuid); + if (!member || !member->IsAlive() || member == bot) + continue; + + const bool memberIsInfected = botAI->GetAura("death plague", member) != nullptr; + + if (memberIsInfected) + { + // Find the nearest non-infected bot to this infected bot (as a guess of its target) + float minDist = 5.0f; // Only count if they're close enough to likely be targeting + Unit* likelyTarget = nullptr; + + for (const auto& targetGuid : members) + { + Unit* potentialTarget = botAI->GetUnit(targetGuid); + if (!potentialTarget || !potentialTarget->IsAlive() || potentialTarget == member) + continue; + + const bool targetIsInfected = botAI->GetAura("death plague", potentialTarget) != nullptr; + const bool targetHasCure = botAI->GetAura("recently infected", potentialTarget) != nullptr; + + if (!targetIsInfected && !targetHasCure) + { + float dist = member->GetExactDist2d(potentialTarget); + if (dist < minDist) + { + minDist = dist; + likelyTarget = potentialTarget; + } + } + } + + if (likelyTarget) + { + targetCounts[likelyTarget->GetGUID()]++; + } + } } - Unit* unit = botAI->GetUnit(member); - if (unit && (botAI->IsHeal(bot) || botAI->IsDps(bot)) && bot->GetExactDist2d(unit) < radius) + // Find viable targets and score them based on various factors + std::vector> viableTargets; + + // First try to find ranged, non-infected, non-cured bots + for (const auto& memberGuid : members) { - return FleePosition(unit->GetPosition(), radius + distanceExtra - bot->GetExactDist2d(unit)); - // return MoveAway(unit, radius + distanceExtra - bot->GetExactDist2d(unit)); + Unit* member = botAI->GetUnit(memberGuid); + if (!member || !member->IsAlive() || member == bot) + continue; + + const bool memberHasCure = botAI->GetAura("recently infected", member) != nullptr; + const bool memberIsInfected = botAI->GetAura("death plague", member) != nullptr; + + if (!memberIsInfected && !memberHasCure) + { + // Base score is distance (lower is better) + float score = bot->GetExactDist2d(member); + + // Prefer ranged targets + if (botAI->IsRanged(bot)) + { + score *= 0.7f; // Bonus for ranged targets + } + + // Apply penalty based on how many other infected bots are targeting this one + int targetingCount = targetCounts[member->GetGUID()]; + score *= (1.0f + targetingCount * 0.5f); // Increase score (worse) for heavily targeted bots + + viableTargets.push_back(std::make_pair(member, score)); + } + } + + // Sort targets by score (lowest/best first) + std::sort(viableTargets.begin(), viableTargets.end(), + [](const std::pair& a, const std::pair& b) + { return a.second < b.second; }); + + // Choose the best target + Unit* targetBot = nullptr; + if (!viableTargets.empty()) + { + targetBot = viableTargets[0].first; + } + + // Move to target bot if found + if (targetBot) + { + // If we're already close enough (1 yard), no need to move + if (bot->GetExactDist2d(targetBot) > 1.0f) + { + // Calculate a unique angle based on bot's GUID to ensure different approach angles + // This helps spread infected bots around the target + uint32 guidLow = bot->GetGUID().GetCounter(); + float angleOffset = float(guidLow % 628) / 100.0f; // Random angle between 0 and 2π + + // Calculate position 1 yard away from target at our unique angle + float angle = targetBot->GetOrientation() + angleOffset; + float targetX = targetBot->GetPositionX() + cos(angle) * 1.0f; + float targetY = targetBot->GetPositionY() + sin(angle) * 1.0f; + float targetZ = targetBot->GetPositionZ(); + + return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + } + return true; // Already in position + } + // No suitable target found, continue with normal behavior + } + + // For ranged bots, only spread from non-infected bots + if (botAI->IsRanged(bot)) + { + const float safeSpacingRadius = 11.0f; + const float moveIncrement = 2.0f; + const float maxMoveDistance = 15.0f; + + const GuidVector members = AI_VALUE(GuidVector, "group members"); + + // Calculate a combined vector representing all nearby NON-INFECTED members' positions + float totalX = 0.0f; + float totalY = 0.0f; + int nearbyCount = 0; + + for (const auto& memberGuid : members) + { + Unit* member = botAI->GetUnit(memberGuid); + if (!member || !member->IsAlive() || member == bot) + continue; + + // Only spread from non-infected bots (can stay near infected or cured bots) + const bool memberIsInfected = botAI->GetAura("death plague", member) != nullptr; + if (memberIsInfected) + continue; + + const float distance = bot->GetExactDist2d(member); + if (distance < safeSpacingRadius) + { + // Calculate vector from member to bot + float dx = bot->GetPositionX() - member->GetPositionX(); + float dy = bot->GetPositionY() - member->GetPositionY(); + + // Weight by inverse distance (closer members have more influence) + float weight = (safeSpacingRadius - distance) / safeSpacingRadius; + totalX += dx * weight; + totalY += dy * weight; + nearbyCount++; + } + } + + // If we have nearby non-infected members, move away in the combined direction + if (nearbyCount > 0) + { + // Normalize the combined vector + float magnitude = std::sqrt(totalX * totalX + totalY * totalY); + if (magnitude > 0.001f) // Avoid division by zero + { + totalX /= magnitude; + totalY /= magnitude; + + // Calculate move distance based on nearest member + float moveDistance = std::min(moveIncrement, maxMoveDistance); + + // Create target position in the combined direction + float targetX = bot->GetPositionX() + totalX * moveDistance; + float targetY = bot->GetPositionY() + totalY * moveDistance; + float targetZ = bot->GetPositionZ(); // Maintain current Z + + // Check if the target position is valid and move there + if (bot->IsWithinLOS(targetX, targetY, targetZ)) + { + return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, + MovementPriority::MOVEMENT_NORMAL); + } + else + { + // If los check fails, try shorter distance + targetX = bot->GetPositionX() + totalX * (moveDistance * 0.5f); + targetY = bot->GetPositionY() + totalY * (moveDistance * 0.5f); + return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, + MovementPriority::MOVEMENT_NORMAL); + } + } } } - return Attack(boss); +*/ + return false; // No movement needed } //Gunship @@ -392,216 +677,295 @@ bool IccCannonFireAction::Execute(Event event) { Unit* vehicleBase = bot->GetVehicleBase(); Vehicle* vehicle = bot->GetVehicle(); + if (!vehicleBase || !vehicle) return false; - GuidVector attackers = AI_VALUE(GuidVector, "possible targets no los"); - - Unit* target = nullptr; - for (auto i = attackers.begin(); i != attackers.end(); ++i) - { - Unit* unit = botAI->GetUnit(*i); - if (!unit) - continue; - for (uint32 entry : availableTargets) - { - if (unit->GetEntry() == entry) { - target = unit; - break; - } - } - if (target) - break; - } + Unit* target = FindValidCannonTarget(); if (!target) return false; - if (vehicleBase->GetPower(POWER_ENERGY) >= 90) { - uint32 spellId = AI_VALUE2(uint32, "vehicle spell id", "incinerating blast"); - if (botAI->CanCastVehicleSpell(spellId, target) && botAI->CastVehicleSpell(spellId, target)) { - vehicleBase->AddSpellCooldown(spellId, 0, 1000); + // Try to cast Incinerating Blast if we have enough energy + const float energyThreshold = 90.0f; + if (vehicleBase->GetPower(POWER_ENERGY) >= energyThreshold) + { + const uint32 blastSpellId = AI_VALUE2(uint32, "vehicle spell id", "incinerating blast"); + if (TryCastCannonSpell(blastSpellId, target, vehicleBase)) return true; + } + + // Otherwise just use regular Cannon Blast + const uint32 cannonSpellId = AI_VALUE2(uint32, "vehicle spell id", "cannon blast"); + return TryCastCannonSpell(cannonSpellId, target, vehicleBase); +} + +Unit* IccCannonFireAction::FindValidCannonTarget() +{ + const GuidVector attackers = AI_VALUE(GuidVector, "possible targets no los"); + + for (const auto& attackerGuid : attackers) + { + Unit* unit = botAI->GetUnit(attackerGuid); + if (!unit) + continue; + + for (const uint32 entry : availableTargetsGS) + { + if (unit->GetEntry() == entry) + return unit; } } - uint32 spellId = AI_VALUE2(uint32, "vehicle spell id", "cannon blast"); - if (botAI->CanCastVehicleSpell(spellId, target) && botAI->CastVehicleSpell(spellId, target)) { - vehicleBase->AddSpellCooldown(spellId, 0, 1000); + + return nullptr; +} + +bool IccCannonFireAction::TryCastCannonSpell(uint32 spellId, Unit* target, Unit* vehicleBase) +{ + static constexpr uint32 cooldownMs = 1000; + + if (botAI->CanCastVehicleSpell(spellId, target) && botAI->CastVehicleSpell(spellId, target)) + { + vehicleBase->AddSpellCooldown(spellId, 0, cooldownMs); return true; } + return false; } bool IccGunshipEnterCannonAction::Execute(Event event) { - // do not switch vehicles yet + // Do not switch vehicles if already in one if (bot->GetVehicle()) return false; - Unit* vehicleToEnter = nullptr; - GuidVector npcs = AI_VALUE(GuidVector, "nearest vehicles"); - for (GuidVector::iterator i = npcs.begin(); i != npcs.end(); i++) + Unit* bestVehicle = FindBestAvailableCannon(); + if (!bestVehicle) + return false; + + return EnterVehicle(bestVehicle, true); +} + +Unit* IccGunshipEnterCannonAction::FindBestAvailableCannon() +{ + const uint32 validCannonEntries[] = {NPC_CANNONA, NPC_CANNONH}; + Unit* bestVehicle = nullptr; + + const GuidVector npcs = AI_VALUE(GuidVector, "nearest vehicles"); + for (const auto& npcGuid : npcs) { - Unit* vehicleBase = botAI->GetUnit(*i); - if (!vehicleBase) + Unit* vehicleBase = botAI->GetUnit(npcGuid); + if (!IsValidCannon(vehicleBase, validCannonEntries)) continue; - if (vehicleBase->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) - continue; - - if (!vehicleBase->IsFriendlyTo(bot)) - continue; - - if (!vehicleBase->GetVehicleKit() || !vehicleBase->GetVehicleKit()->GetAvailableSeatCount()) - continue; - - uint32 entry = vehicleBase->GetEntry(); - if (entry != 36838 && entry != 36839) - continue; - - if (vehicleBase->HasAura(69704) || vehicleBase->HasAura(69705)) - continue; - - if (!vehicleToEnter || bot->GetExactDist(vehicleToEnter) > bot->GetExactDist(vehicleBase)) - vehicleToEnter = vehicleBase; + // Choose the closest valid cannon + if (!bestVehicle || bot->GetExactDist(vehicleBase) < bot->GetExactDist(bestVehicle)) + bestVehicle = vehicleBase; } - if (!vehicleToEnter) - return false; - - if (EnterVehicle(vehicleToEnter, true)) - return true; + return bestVehicle; +} - return false; +bool IccGunshipEnterCannonAction::IsValidCannon(Unit* vehicle, const uint32 validEntries[]) +{ + if (!vehicle) + return false; + + // Must be selectable + if (vehicle->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + return false; + + // Must be friendly + if (!vehicle->IsFriendlyTo(bot)) + return false; + + // Must have available seats + if (!vehicle->GetVehicleKit() || !vehicle->GetVehicleKit()->GetAvailableSeatCount()) + return false; + + // Must be one of the cannon entries + const uint32 entry = vehicle->GetEntry(); + bool isValidEntry = false; + for (size_t i = 0; i < 2; ++i) + { // 2 is the size of validEntries + if (entry == validEntries[i]) + { + isValidEntry = true; + break; + } + } + + if (!isValidEntry) + return false; + + // Must not have these auras (frozen or disabled) + if (vehicle->HasAura(69704) || vehicle->HasAura(69705)) + return false; + + return true; } bool IccGunshipEnterCannonAction::EnterVehicle(Unit* vehicleBase, bool moveIfFar) { - float dist = bot->GetDistance(vehicleBase); - + const float dist = bot->GetDistance(vehicleBase); + if (dist > INTERACTION_DISTANCE && !moveIfFar) return false; if (dist > INTERACTION_DISTANCE) return MoveTo(vehicleBase); - + // Prepare for entering vehicle botAI->RemoveShapeshift(); - bot->GetMotionMaster()->Clear(); bot->StopMoving(); + + // Enter the vehicle vehicleBase->HandleSpellClick(bot); if (!bot->IsOnVehicle(vehicleBase)) return false; - // dismount because bots can enter vehicle on mount + // Dismount because bots can enter vehicle while mounted WorldPacket emptyPacket; bot->GetSession()->HandleCancelMountAuraOpcode(emptyPacket); + return true; } bool IccGunshipTeleportAllyAction::Execute(Event event) { + static constexpr float MAX_WAITING_DISTANCE = 45.0f; + static constexpr float MAX_ATTACK_DISTANCE = 15.0f; + static constexpr uint8_t SKULL_ICON_INDEX = 7; + // Find the Battle-Mage boss Unit* boss = AI_VALUE2(Unit*, "find target", "kor'kron battle-mage"); // Check if we need to remove skull icon when boss is dead - if (Group* group = bot->GetGroup()) - { - ObjectGuid currentSkullTarget = group->GetTargetIcon(7); - if (!currentSkullTarget.IsEmpty()) - { - // If the current skull target is dead or doesn't exist, remove the icon - if (Unit* skullTarget = ObjectAccessor::GetUnit(*bot, currentSkullTarget)) - { - if (!skullTarget->IsAlive()) - group->SetTargetIcon(7, bot->GetGUID(), ObjectGuid::Empty); - } - else - { - // Target not found, might have despawned, remove icon - group->SetTargetIcon(7, bot->GetGUID(), ObjectGuid::Empty); - } - } - } + CleanupSkullIcon(SKULL_ICON_INDEX); - // If no boss found or boss is dead, nothing more to do + // If no boss found or boss is dead or not casting, check waiting position if (!boss || !boss->IsAlive() || !boss->HasUnitState(UNIT_STATE_CASTING)) { // If we're too far from waiting position, go there - if (bot->GetExactDist2d(ICC_GUNSHIP_TELEPORT_ALLY2) > 45.0f) - return bot->TeleportTo(bot->GetMapId(), ICC_GUNSHIP_TELEPORT_ALLY2.GetPositionX(), - ICC_GUNSHIP_TELEPORT_ALLY2.GetPositionY(), - ICC_GUNSHIP_TELEPORT_ALLY2.GetPositionZ(), bot->GetOrientation()); + if (bot->GetExactDist2d(ICC_GUNSHIP_TELEPORT_ALLY2) > MAX_WAITING_DISTANCE) + return TeleportTo(ICC_GUNSHIP_TELEPORT_ALLY2); } - else if (boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(69705) && boss->IsAlive()) + else if (boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_BELOW_ZERO) && + boss->IsAlive()) { // Mark the boss with skull icon - if (Group* group = bot->GetGroup()) - if (group->GetTargetIcon(7) != boss->GetGUID()) - group->SetTargetIcon(7, bot->GetGUID(), boss->GetGUID()); + UpdateBossSkullIcon(boss, SKULL_ICON_INDEX); // Teleport non-tank bots to attack position if not already there - if (!botAI->IsAssistTank(bot) && bot->GetExactDist2d(ICC_GUNSHIP_TELEPORT_ALLY) > 15.0f) - return bot->TeleportTo(bot->GetMapId(), ICC_GUNSHIP_TELEPORT_ALLY.GetPositionX(), - ICC_GUNSHIP_TELEPORT_ALLY.GetPositionY(), ICC_GUNSHIP_TELEPORT_ALLY.GetPositionZ(), - bot->GetOrientation()); + if (!botAI->IsAssistTank(bot) && bot->GetExactDist2d(ICC_GUNSHIP_TELEPORT_ALLY) > MAX_ATTACK_DISTANCE) + return TeleportTo(ICC_GUNSHIP_TELEPORT_ALLY); } return false; } +bool IccGunshipTeleportAllyAction::TeleportTo(const Position& position) +{ + return bot->TeleportTo(bot->GetMapId(), position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), + bot->GetOrientation()); +} + +void IccGunshipTeleportAllyAction::CleanupSkullIcon(uint8_t SKULL_ICON_INDEX) +{ + if (Group* group = bot->GetGroup()) + { + const ObjectGuid currentSkullTarget = group->GetTargetIcon(SKULL_ICON_INDEX); + + if (!currentSkullTarget.IsEmpty()) + { + Unit* skullTarget = ObjectAccessor::GetUnit(*bot, currentSkullTarget); + + if (!skullTarget || !skullTarget->IsAlive()) + { + // Target is dead or doesn't exist, remove icon + group->SetTargetIcon(SKULL_ICON_INDEX, bot->GetGUID(), ObjectGuid::Empty); + } + } + } +} + +void IccGunshipTeleportAllyAction::UpdateBossSkullIcon(Unit* boss, uint8_t SKULL_ICON_INDEX) +{ + if (Group* group = bot->GetGroup()) + { + if (group->GetTargetIcon(SKULL_ICON_INDEX) != boss->GetGUID()) + group->SetTargetIcon(SKULL_ICON_INDEX, bot->GetGUID(), boss->GetGUID()); + } +} + bool IccGunshipTeleportHordeAction::Execute(Event event) { + static constexpr float MAX_WAITING_DISTANCE = 45.0f; + static constexpr float MAX_ATTACK_DISTANCE = 15.0f; + static constexpr uint8_t SKULL_ICON_INDEX = 7; + // Find the Sorcerer boss Unit* boss = AI_VALUE2(Unit*, "find target", "skybreaker sorcerer"); // Check if we need to remove skull icon when boss is dead - if (Group* group = bot->GetGroup()) - { - ObjectGuid currentSkullTarget = group->GetTargetIcon(7); - if (!currentSkullTarget.IsEmpty()) - { - // If the current skull target is dead or doesn't exist, remove the icon - if (Unit* skullTarget = ObjectAccessor::GetUnit(*bot, currentSkullTarget)) - { - if (!skullTarget->IsAlive()) - group->SetTargetIcon(7, bot->GetGUID(), ObjectGuid::Empty); - } - else - { - // Target not found, might have despawned, remove icon - group->SetTargetIcon(7, bot->GetGUID(), ObjectGuid::Empty); - } - } - } + CleanupSkullIcon(SKULL_ICON_INDEX); - // If no boss found or boss is dead, nothing more to do + // If no boss found or boss is dead or not casting, check waiting position if (!boss || !boss->IsAlive() || !boss->HasUnitState(UNIT_STATE_CASTING)) { // If we're too far from waiting position, go there - if (bot->GetExactDist2d(ICC_GUNSHIP_TELEPORT_HORDE2) > 45.0f) - return bot->TeleportTo(bot->GetMapId(), ICC_GUNSHIP_TELEPORT_HORDE2.GetPositionX(), - ICC_GUNSHIP_TELEPORT_HORDE2.GetPositionY(), - ICC_GUNSHIP_TELEPORT_HORDE2.GetPositionZ(), bot->GetOrientation()); + if (bot->GetExactDist2d(ICC_GUNSHIP_TELEPORT_HORDE2) > MAX_WAITING_DISTANCE) + return TeleportTo(ICC_GUNSHIP_TELEPORT_HORDE2); } - else if (boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(69705) && boss->IsAlive()) + else if (boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_BELOW_ZERO) && + boss->IsAlive()) { // Mark the boss with skull icon - if (Group* group = bot->GetGroup()) - if (group->GetTargetIcon(7) != boss->GetGUID()) - group->SetTargetIcon(7, bot->GetGUID(), boss->GetGUID()); + UpdateBossSkullIcon(boss, SKULL_ICON_INDEX); // Teleport non-tank bots to attack position if not already there - if (!botAI->IsAssistTank(bot) && bot->GetExactDist2d(ICC_GUNSHIP_TELEPORT_HORDE) > 15.0f) - return bot->TeleportTo(bot->GetMapId(), ICC_GUNSHIP_TELEPORT_HORDE.GetPositionX(), - ICC_GUNSHIP_TELEPORT_HORDE.GetPositionY(), ICC_GUNSHIP_TELEPORT_HORDE.GetPositionZ(), - bot->GetOrientation()); + if (!botAI->IsAssistTank(bot) && bot->GetExactDist2d(ICC_GUNSHIP_TELEPORT_HORDE) > MAX_ATTACK_DISTANCE) + return TeleportTo(ICC_GUNSHIP_TELEPORT_HORDE); } return false; } +bool IccGunshipTeleportHordeAction::TeleportTo(const Position& position) +{ + return bot->TeleportTo(bot->GetMapId(), position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), + bot->GetOrientation()); +} + +void IccGunshipTeleportHordeAction::CleanupSkullIcon(uint8_t SKULL_ICON_INDEX) +{ + if (Group* group = bot->GetGroup()) + { + const ObjectGuid currentSkullTarget = group->GetTargetIcon(SKULL_ICON_INDEX); + + if (!currentSkullTarget.IsEmpty()) + { + Unit* skullTarget = ObjectAccessor::GetUnit(*bot, currentSkullTarget); + + if (!skullTarget || !skullTarget->IsAlive()) + { + // Target is dead or doesn't exist, remove icon + group->SetTargetIcon(SKULL_ICON_INDEX, bot->GetGUID(), ObjectGuid::Empty); + } + } + } +} + +void IccGunshipTeleportHordeAction::UpdateBossSkullIcon(Unit* boss, uint8_t SKULL_ICON_INDEX) +{ + if (Group* group = bot->GetGroup()) + { + if (group->GetTargetIcon(SKULL_ICON_INDEX) != boss->GetGUID()) + group->SetTargetIcon(SKULL_ICON_INDEX, bot->GetGUID(), boss->GetGUID()); + } +} + //DBS bool IccDbsTankPositionAction::Execute(Event event) { @@ -609,683 +973,1266 @@ bool IccDbsTankPositionAction::Execute(Event event) if (!boss) return false; - bot->SetTarget(boss->GetGUID()); - if (botAI->IsTank(bot) || botAI->IsMainTank(bot) || botAI->IsAssistTank(bot)) + Unit* beast = AI_VALUE2(Unit*, "find target", "blood beast"); + + // Handle tank positioning + if (botAI->IsTank(bot) && !beast) { if (bot->GetExactDist2d(ICC_DBS_TANK_POSITION) > 5.0f) return MoveTo(bot->GetMapId(), ICC_DBS_TANK_POSITION.GetPositionX(), ICC_DBS_TANK_POSITION.GetPositionY(), ICC_DBS_TANK_POSITION.GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_NORMAL); + + + + // Early return if this tank has Rune of Blood + if (botAI->GetAura("Rune of Blood", bot)) + return true; } - if (botAI->GetAura("Rune of Blood", bot) && botAI->IsTank(bot)) - return true; + if (!botAI->IsTank(bot)) + { + if (CrowdControlBloodBeasts()) + return true; + } + // Handle ranged and healer positioning if (botAI->IsRanged(bot) || botAI->IsHeal(bot)) { - const float evasion = 12.0f; + // Handle evasion from blood beasts + if (EvadeBloodBeasts()) + return true; - // Get the nearest hostile NPCs - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (unit && (unit->GetEntry() == 38508 || unit->GetEntry() == 38596 || unit->GetEntry() == 38597 || - unit->GetEntry() == 38598)) // blood beast - { - // Only run away if the blood beast is targeting us - if (unit->GetVictim() == bot) - { - float currentDistance = bot->GetDistance2d(unit); - - // Move away from the blood beast if the bot is too close - if (currentDistance < evasion) - { - return MoveAway(unit, evasion - currentDistance); - } - } - } - } - - // Get group and position in group - Group* group = bot->GetGroup(); - if (!group) - return false; - - // Find this bot's position among ranged/healers in the group - int rangedIndex = -1; - int currentIndex = 0; - - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive()) - continue; - - if ((botAI->IsRanged(member) || botAI->IsHeal(member)) && !botAI->IsTank(member)) - { - if (member == bot) - { - rangedIndex = currentIndex; - break; - } - currentIndex++; - } - } - - if (rangedIndex == -1) - return false; - - // Fixed positions calculation - float tankToBossAngle = 3.14f; - const float minBossDistance = 11.0f; - const float spreadDistance = 10.0f; - - // Calculate position in a fixed grid (3 rows x 5 columns) - int row = rangedIndex / 5; - int col = rangedIndex % 5; - - // Calculate base position - float xOffset = (col - 2) * spreadDistance; // Center around tank position - float yOffset = minBossDistance + (row * spreadDistance); // Each row further back - - // Add zigzag offset for odd rows - if (row % 2 == 1) - xOffset += spreadDistance / 2; - - // Rotate position based on tank-to-boss angle - float finalX = ICC_DBS_TANK_POSITION.GetPositionX() + - (cos(tankToBossAngle) * yOffset - sin(tankToBossAngle) * xOffset); - float finalY = ICC_DBS_TANK_POSITION.GetPositionY() + - (sin(tankToBossAngle) * yOffset + cos(tankToBossAngle) * xOffset); - float finalZ = ICC_DBS_TANK_POSITION.GetPositionZ(); - - // Update Z coordinate - bot->UpdateAllowedPositionZ(finalX, finalY, finalZ); - - // Move if not in position - if (bot->GetExactDist2d(finalX, finalY) > 3.0f) - return MoveTo(bot->GetMapId(), finalX, finalY, finalZ, false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); - - return false; - } - - return false; + // Position in formation + return PositionInRangedFormation(); } + return false; +} + +bool IccDbsTankPositionAction::CrowdControlBloodBeasts() +{ + const std::array bloodBeastEntries = {NPC_BLOOD_BEAST1, NPC_BLOOD_BEAST2, NPC_BLOOD_BEAST3, + NPC_BLOOD_BEAST4}; + const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + bool appliedCC = false; + + for (const auto& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (!unit || !unit->IsAlive()) + continue; + + // Check if this is a blood beast + const bool isBloodBeast = + std::find(bloodBeastEntries.begin(), bloodBeastEntries.end(), unit->GetEntry()) != bloodBeastEntries.end(); + + if (!isBloodBeast) + continue; + + // Apply class-specific CC + switch (bot->getClass()) + { + case CLASS_MAGE: + if (!botAI->HasAura("Frost Nova", unit)) + { + botAI->CastSpell("Frost Nova", unit); + appliedCC = true; + } + break; + case CLASS_DRUID: + if (!botAI->HasAura("Entangling Roots", unit)) + { + botAI->CastSpell("Entangling Roots", unit); + appliedCC = true; + } + break; + case CLASS_PALADIN: + if (!botAI->HasAura("Hammer of Justice", unit)) + { + botAI->CastSpell("Hammer of Justice", unit); + appliedCC = true; + } + break; + case CLASS_WARRIOR: + if (!botAI->HasAura("Hamstring", unit)) + { + botAI->CastSpell("Hamstring", unit); + appliedCC = true; + } + break; + case CLASS_HUNTER: + if (!botAI->HasAura("Concussive Shot", unit)) + { + botAI->CastSpell("Concussive Shot", unit); + appliedCC = true; + } + break; + case CLASS_ROGUE: + if (!botAI->HasAura("Kidney Shot", unit)) + { + botAI->CastSpell("Kidney Shot", unit); + appliedCC = true; + } + break; + case CLASS_SHAMAN: + if (!botAI->HasAura("Frost Shock", unit)) + { + botAI->CastSpell("Frost Shock", unit); + appliedCC = true; + } + break; + case CLASS_DEATH_KNIGHT: + if (!botAI->HasAura("Chains of Ice", unit)) + { + botAI->CastSpell("Chains of Ice", unit); + appliedCC = true; + } + break; + case CLASS_PRIEST: + if (!botAI->HasAura("Psychic Scream", unit)) + { + botAI->CastSpell("Psychic Scream", unit); + appliedCC = true; + } + break; + case CLASS_WARLOCK: + if (!botAI->HasAura("Fear", unit)) + { + botAI->CastSpell("Fear", unit); + appliedCC = true; + } + break; + default: + break; + } + } + + return false; +} + +bool IccDbsTankPositionAction::EvadeBloodBeasts() +{ + const float evasionDistance = 12.0f; + const std::array bloodBeastEntries = {NPC_BLOOD_BEAST1, NPC_BLOOD_BEAST2, NPC_BLOOD_BEAST3, NPC_BLOOD_BEAST4}; + + // Get the nearest hostile NPCs + const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + for (const auto& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (!unit) + continue; + + // Check if this is a blood beast + const bool isBloodBeast = std::find(bloodBeastEntries.begin(), bloodBeastEntries.end(), unit->GetEntry()) != bloodBeastEntries.end(); + + // Only evade if it's a blood beast targeting us + if (isBloodBeast && unit->GetVictim() == bot) + { + float currentDistance = bot->GetDistance2d(unit); + + // Move away if too close + if (currentDistance < evasionDistance) + return MoveAway(unit, evasionDistance - currentDistance); + } + } + + return false; +} + +bool IccDbsTankPositionAction::PositionInRangedFormation() +{ + // Get group + Group* group = bot->GetGroup(); + if (!group) + return false; + + // Find this bot's position among ranged/healers in the group + int rangedIndex = -1; + int currentIndex = 0; + + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if ((botAI->IsRanged(member) || botAI->IsHeal(member)) && !botAI->IsTank(member)) + { + if (member == bot) + { + rangedIndex = currentIndex; + break; + } + currentIndex++; + } + } + + if (rangedIndex == -1) + return false; + + // Fixed positions calculation + constexpr float tankToBossAngle = 3.14f; + constexpr float minBossDistance = 11.0f; + constexpr float spreadDistance = 10.0f; + constexpr int columnsPerRow = 5; + + // Calculate position in a fixed grid (3 rows x 5 columns) + int row = rangedIndex / columnsPerRow; + int col = rangedIndex % columnsPerRow; + + // Calculate base position + float xOffset = (col - 2) * spreadDistance; // Center around tank position + float yOffset = minBossDistance + (row * spreadDistance); // Each row further back + + // Add zigzag offset for odd rows + if (row % 2 == 1) + xOffset += spreadDistance / 2; + + // Rotate position based on tank-to-boss angle + float finalX = + ICC_DBS_TANK_POSITION.GetPositionX() + (cos(tankToBossAngle) * yOffset - sin(tankToBossAngle) * xOffset); + float finalY = + ICC_DBS_TANK_POSITION.GetPositionY() + (sin(tankToBossAngle) * yOffset + cos(tankToBossAngle) * xOffset); + float finalZ = ICC_DBS_TANK_POSITION.GetPositionZ(); + + // Update Z coordinate + bot->UpdateAllowedPositionZ(finalX, finalY, finalZ); + + // Move if not in position + if (bot->GetExactDist2d(finalX, finalY) > 3.0f) + return MoveTo(bot->GetMapId(), finalX, finalY, finalZ, false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + + return false; +} + bool IccAddsDbsAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "deathbringer saurfang"); if (!boss) return false; + // This action is only for melee if (!botAI->IsMelee(bot)) return false; - Unit* currentTarget = AI_VALUE(Unit*, "current target"); + Unit* priorityTarget = FindPriorityTarget(boss); - GuidVector targets = AI_VALUE(GuidVector, "possible targets no los"); + // Update raid target icons if needed + UpdateSkullMarker(priorityTarget); - const uint32 targetsEntries[] = {38508, 38596, 38597, 38598}; // adds + return false; +} - Unit* priorityTarget = nullptr; - bool hasValidAdds = false; +Unit* IccAddsDbsAction::FindPriorityTarget(Unit* boss) +{ + const GuidVector targets = AI_VALUE(GuidVector, "possible targets no los"); + + // Blood beast entry IDs + constexpr std::array addEntries = {NPC_BLOOD_BEAST1, NPC_BLOOD_BEAST2, NPC_BLOOD_BEAST3, NPC_BLOOD_BEAST4}; // First check for alive adds - for (uint32 entry : targetsEntries) + for (uint32_t entry : addEntries) { for (const ObjectGuid& guid : targets) { Unit* unit = botAI->GetUnit(guid); if (unit && unit->IsAlive() && unit->GetEntry() == entry) - { - priorityTarget = unit; - hasValidAdds = true; - break; - } + return unit; } - if (priorityTarget) - break; } - // Only fallback to boss if NO adds exist - if (!hasValidAdds && boss->IsAlive()) + // Only fallback to boss if it's alive + return boss->IsAlive() ? const_cast(boss) : nullptr; +} + +void IccAddsDbsAction::UpdateSkullMarker(Unit* priorityTarget) +{ + if (!priorityTarget) + return; + + Group* group = bot->GetGroup(); + if (!group) + return; + + constexpr uint8_t skullIconId = 7; + + // Get current skull target + ObjectGuid currentSkull = group->GetTargetIcon(skullIconId); + Unit* currentSkullUnit = botAI->GetUnit(currentSkull); + + // Determine if skull marker needs updating + bool needsUpdate = !currentSkullUnit || !currentSkullUnit->IsAlive() || currentSkullUnit != priorityTarget; + + // Update if needed + if (needsUpdate) + group->SetTargetIcon(skullIconId, bot->GetGUID(), priorityTarget->GetGUID()); +} + +// Festergut +bool IccFestergutGroupPositionAction::Execute(Event event) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "festergut"); + if (!boss) + return false; + + bot->SetTarget(boss->GetGUID()); + + // Handle tank positioning + if ((botAI->HasAggro(boss) && botAI->IsMainTank(bot)) || botAI->IsAssistTank(bot)) { - priorityTarget = boss; + if (bot->GetExactDist2d(ICC_FESTERGUT_TANK_POSITION) > 5.0f) + return MoveTo(bot->GetMapId(), ICC_FESTERGUT_TANK_POSITION.GetPositionX(), + ICC_FESTERGUT_TANK_POSITION.GetPositionY(), ICC_FESTERGUT_TANK_POSITION.GetPositionZ(), false, + false, false, true, MovementPriority::MOVEMENT_NORMAL); } - // Update skull icon if needed - if (priorityTarget) + // Check for spores in the group + if (HasSporesInGroup()) + return false; + + // Position non-tank ranged and healers + return PositionNonTankMembers(); +} + +bool IccFestergutGroupPositionAction::HasSporesInGroup() +{ + const GuidVector members = AI_VALUE(GuidVector, "group members"); + + for (const auto& memberGuid : members) { - if (Group* group = bot->GetGroup()) - { - ObjectGuid currentSkull = group->GetTargetIcon(7); - Unit* currentSkullUnit = botAI->GetUnit(currentSkull); - - bool needsUpdate = false; - if (!currentSkullUnit || !currentSkullUnit->IsAlive()) - { - needsUpdate = true; // No valid skull target - } - else if (currentSkullUnit != priorityTarget) - { - needsUpdate = true; // Different target than desired - } - - if (needsUpdate) - { - group->SetTargetIcon(7, bot->GetGUID(), priorityTarget->GetGUID()); - } - } + Unit* unit = botAI->GetUnit(memberGuid); + if (unit && unit->HasAura(SPELL_GAS_SPORE)) + return true; } return false; } - //FESTERGUT -bool IccFestergutTankPositionAction::Execute(Event event) +bool IccFestergutGroupPositionAction::PositionNonTankMembers() { - Unit* boss = AI_VALUE2(Unit*, "find target", "festergut"); - if (!boss) + // Only position ranged and healers without spores + if (!(botAI->IsRanged(bot) || botAI->IsHeal(bot))) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + int positionIndex = CalculatePositionIndex(group); + if (positionIndex == -1) + return false; + + // Position calculation parameters + constexpr float tankToBossAngle = 4.58f; + constexpr float minBossDistance = 20.0f; + constexpr float spreadDistance = 10.0f; + constexpr int columnsPerRow = 5; + + // Calculate grid position + int row = positionIndex / columnsPerRow; + int col = positionIndex % columnsPerRow; + + // Calculate base position + float xOffset = (col - 2) * spreadDistance; // Center around tank position + float yOffset = minBossDistance + (row * spreadDistance); // Each row further back + + // Add zigzag offset for odd rows + if (row % 2 == 1) + xOffset += spreadDistance / 2; + + // Rotate position based on tank-to-boss angle + float finalX = + ICC_FESTERGUT_TANK_POSITION.GetPositionX() + (cos(tankToBossAngle) * yOffset - sin(tankToBossAngle) * xOffset); + float finalY = + ICC_FESTERGUT_TANK_POSITION.GetPositionY() + (sin(tankToBossAngle) * yOffset + cos(tankToBossAngle) * xOffset); + float finalZ = ICC_FESTERGUT_TANK_POSITION.GetPositionZ(); + + // Update Z coordinate + bot->UpdateAllowedPositionZ(finalX, finalY, finalZ); + + // Move if not in position + if (bot->GetExactDist2d(finalX, finalY) > 3.0f) + return MoveTo(bot->GetMapId(), finalX, finalY, finalZ, false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + return false; +} - bot->SetTarget(boss->GetGUID()); - if (botAI->IsTank(bot) || botAI->IsMainTank(bot) || botAI->IsAssistTank(bot)) - { - if (bot->GetExactDist2d(ICC_FESTERGUT_TANK_POSITION) > 5.0f) - return MoveTo(bot->GetMapId(), ICC_FESTERGUT_TANK_POSITION.GetPositionX(), - ICC_FESTERGUT_TANK_POSITION.GetPositionY(), ICC_FESTERGUT_TANK_POSITION.GetPositionZ(), false, - false, false, true, MovementPriority::MOVEMENT_NORMAL); - } +int IccFestergutGroupPositionAction::CalculatePositionIndex(Group* group) +{ + int healerIndex = -1; + int rangedDpsIndex = -1; + int currentHealerIndex = 0; + int currentRangedDpsIndex = 0; + int totalHealers = 0; - GuidVector members = AI_VALUE(GuidVector, "group members"); - bool sporesPresent = false; - for (auto& member : members) + // First pass: count total healers + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) { - Unit* unit = botAI->GetUnit(member); - if (!unit) + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || botAI->IsTank(member)) continue; - if (unit->HasAura(69279)) + if (botAI->IsHeal(member)) + totalHealers++; + } + + // Second pass: determine position + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || botAI->IsTank(member)) + continue; + + if (member == bot) { - sporesPresent = true; + if (botAI->IsHeal(bot)) + healerIndex = currentHealerIndex; + else if (botAI->IsRanged(bot)) + rangedDpsIndex = currentRangedDpsIndex; break; } + + if (botAI->IsHeal(member)) + currentHealerIndex++; + else if (botAI->IsRanged(member)) + currentRangedDpsIndex++; } - if (!sporesPresent && (botAI->IsRanged(bot) || botAI->IsHeal(bot))) + // Calculate final position index + if (healerIndex != -1) { - Group* group = bot->GetGroup(); - if (!group) - return false; - - // Separate counters for healers and ranged DPS - int healerIndex = -1; - int rangedDpsIndex = -1; - int currentHealerIndex = 0; - int currentRangedDpsIndex = 0; - - // First pass: count total healers and ranged - int totalHealers = 0; - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || botAI->IsTank(member)) - continue; - - if (botAI->IsHeal(member)) - totalHealers++; - } - - // Second pass: assign positions - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || botAI->IsTank(member)) - continue; - - if (member == bot) - { - if (botAI->IsHeal(bot)) - healerIndex = currentHealerIndex; - else if (botAI->IsRanged(bot)) - rangedDpsIndex = currentRangedDpsIndex; - break; - } - - if (botAI->IsHeal(member)) - currentHealerIndex++; - else if (botAI->IsRanged(member)) - currentRangedDpsIndex++; - } - - int positionIndex; - if (healerIndex != -1) - { - // Healers get positions in first two rows - int healersPerRow = (totalHealers + 1) / 2; // Round up - positionIndex = healerIndex; - // Ensure healer is in first two rows - if (positionIndex >= healersPerRow) - { - positionIndex = positionIndex - healersPerRow + 5; // Move to second row - } - } - else if (rangedDpsIndex != -1) - { - // Ranged DPS start from where healers end - positionIndex = totalHealers + rangedDpsIndex; - } - else - return false; - - // Fixed positions calculation - float tankToBossAngle = 4.58f; - const float minBossDistance = 20.0f; - const float spreadDistance = 10.0f; - - // Calculate position in a fixed grid (3 rows x 5 columns) - int row = positionIndex / 5; - int col = positionIndex % 5; - - // Calculate base position - float xOffset = (col - 2) * spreadDistance; // Center around tank position - float yOffset = minBossDistance + (row * spreadDistance); // Each row further back - - // Add zigzag offset for odd rows - if (row % 2 == 1) - xOffset += spreadDistance / 2; - - // Rotate position based on tank-to-boss angle - float finalX = ICC_FESTERGUT_TANK_POSITION.GetPositionX() + (cos(tankToBossAngle) * yOffset - sin(tankToBossAngle) * xOffset); - float finalY = ICC_FESTERGUT_TANK_POSITION.GetPositionY() + (sin(tankToBossAngle) * yOffset + cos(tankToBossAngle) * xOffset); - float finalZ = ICC_FESTERGUT_TANK_POSITION.GetPositionZ(); - - // Update Z coordinate - bot->UpdateAllowedPositionZ(finalX, finalY, finalZ); - - // Move if not in position - if (bot->GetExactDist2d(finalX, finalY) > 3.0f) - return MoveTo(bot->GetMapId(), finalX, finalY, finalZ, - false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + // Healers in first two rows + int healersPerRow = (totalHealers + 1) / 2; // Round up + healerIndex = (healerIndex < healersPerRow) ? healerIndex : healerIndex - healersPerRow + 5; + return healerIndex; } - return false; + else if (rangedDpsIndex != -1) + { + // Ranged DPS after healers + return totalHealers + rangedDpsIndex; + } + + return -1; } bool IccFestergutSporeAction::Execute(Event event) { - const float POSITION_TOLERANCE = 4.0f; - const float SPREAD_RADIUS = 2.0f; // How far apart ranged should spread + constexpr float POSITION_TOLERANCE = 4.0f; + constexpr float SPREAD_RADIUS = 2.0f; - bool hasSpore = bot->HasAura(69279); // gas spore - - // If bot has spore, stop attacking + // Check if bot has spore + bool hasSpore = bot->HasAura(SPELL_GAS_SPORE); // gas spore + + // Stop attacking if spored if (hasSpore) - { - bot->AttackStop(); - } + botAI->Reset(); - // Calculate a unique spread position for ranged - float angle = (bot->GetGUID().GetCounter() % 16) * (M_PI / 8); // Divide circle into 16 positions - Position spreadRangedPos = ICC_FESTERGUT_RANGED_SPORE; - spreadRangedPos.m_positionX += cos(angle) * SPREAD_RADIUS; - spreadRangedPos.m_positionY += sin(angle) * SPREAD_RADIUS; + // Calculate unique spread position for ranged + Position spreadRangedPos = CalculateSpreadPosition(); - // Find all spored players and the one with lowest GUID - ObjectGuid lowestGuid; - bool isFirst = true; - std::vector sporedPlayers; - - GuidVector members = AI_VALUE(GuidVector, "group members"); - for (auto& member : members) - { - Unit* unit = botAI->GetUnit(member); - if (!unit) - continue; + // Find spored players + SporeInfo sporeInfo = FindSporedPlayers(); - if (unit->HasAura(69279)) - { - sporedPlayers.push_back(unit); - if (isFirst || unit->GetGUID() < lowestGuid) - { - lowestGuid = unit->GetGUID(); - isFirst = false; - } - } - } + // Determine target position + Position targetPos = DetermineTargetPosition(hasSpore, sporeInfo, spreadRangedPos); - // If no spores present at all, return - if (sporedPlayers.empty()) - return false; - - Position targetPos; - if (hasSpore) - { - bool mainTankHasSpore = false; - GuidVector members = AI_VALUE(GuidVector, "group members"); - for (auto& member : members) - { - Unit* unit = botAI->GetUnit(member); - if (!unit) - continue; - - if (botAI->IsMainTank(unit->ToPlayer()) && unit->HasAura(69279)) - { - mainTankHasSpore = true; - break; - } - } - - // If bot is main tank, always go melee regardless of GUID - if (botAI->IsMainTank(bot)) - { - targetPos = ICC_FESTERGUT_MELEE_SPORE; - } - // If this bot has the lowest GUID among spored players AND is not a tank AND main tank is not spored - else if (bot->GetGUID() == lowestGuid && !botAI->IsTank(bot) && !mainTankHasSpore) - { - targetPos = ICC_FESTERGUT_MELEE_SPORE; - } - // All other spored players go ranged - else - { - targetPos = spreadRangedPos; - } - } - else - { - // If bot doesn't have spore, go to position based on role - targetPos = botAI->IsMelee(bot) ? ICC_FESTERGUT_MELEE_SPORE : spreadRangedPos; - } - - // Only move if we're not already at the target position + // Move to position if not already there if (bot->GetExactDist2d(targetPos) > POSITION_TOLERANCE) - { return MoveTo(bot->GetMapId(), targetPos.GetPositionX(), targetPos.GetPositionY(), targetPos.GetPositionZ(), - true, false, false, true, MovementPriority::MOVEMENT_FORCED); - } + true, false, false, true, MovementPriority::MOVEMENT_FORCED); return hasSpore; } -//ROTFACE +Position IccFestergutSporeAction::CalculateSpreadPosition() +{ + constexpr float SPREAD_RADIUS = 2.0f; + + // Unique angle based on bot's GUID + float angle = (bot->GetGUID().GetCounter() % 16) * (M_PI / 8); + + Position spreadRangedPos = ICC_FESTERGUT_RANGED_SPORE; + spreadRangedPos.m_positionX += cos(angle) * SPREAD_RADIUS; + spreadRangedPos.m_positionY += sin(angle) * SPREAD_RADIUS; + + return spreadRangedPos; +} + +IccFestergutSporeAction::SporeInfo IccFestergutSporeAction::FindSporedPlayers() +{ + SporeInfo info; + const GuidVector members = AI_VALUE(GuidVector, "group members"); + + for (const auto& memberGuid : members) + { + Unit* unit = botAI->GetUnit(memberGuid); + if (!unit) + continue; + + if (unit->HasAura(SPELL_GAS_SPORE)) + { + info.sporedPlayers.push_back(unit); + + if (!info.hasLowestGuid || unit->GetGUID() < info.lowestGuid) + { + info.lowestGuid = unit->GetGUID(); + info.hasLowestGuid = true; + } + } + } + + return info; +} + +Position IccFestergutSporeAction::DetermineTargetPosition(bool hasSpore, const SporeInfo& sporeInfo, const Position& spreadRangedPos) +{ + // No spores at all + if (sporeInfo.sporedPlayers.empty()) + return botAI->IsMelee(bot) ? ICC_FESTERGUT_MELEE_SPORE : spreadRangedPos; + + // Bot has no spore, go to standard position + if (!hasSpore) + return botAI->IsMelee(bot) ? ICC_FESTERGUT_MELEE_SPORE : spreadRangedPos; + + // Check if main tank has spore + bool mainTankHasSpore = CheckMainTankSpore(); + + // Determine position based on spore logic + if (botAI->IsMainTank(bot)) + return ICC_FESTERGUT_MELEE_SPORE; + + if (bot->GetGUID() == sporeInfo.lowestGuid && !botAI->IsTank(bot) && !mainTankHasSpore) + return ICC_FESTERGUT_MELEE_SPORE; + + return spreadRangedPos; +} + +bool IccFestergutSporeAction::CheckMainTankSpore() +{ + const GuidVector members = AI_VALUE(GuidVector, "group members"); + + for (const auto& memberGuid : members) + { + Unit* unit = botAI->GetUnit(memberGuid); + if (!unit) + continue; + + if (botAI->IsMainTank(unit->ToPlayer()) && unit->HasAura(SPELL_GAS_SPORE)) + return true; + } + + return false; +} + +// Rotface bool IccRotfaceTankPositionAction::Execute(Event event) { Unit* boss = AI_VALUE2(Unit*, "find target", "rotface"); if (!boss) - return false; + return false; - // Mark Rotface with skull if not already marked - if (Group* group = bot->GetGroup()) - { - ObjectGuid skullGuid = group->GetTargetIcon(7); // 7 = skull - if (!skullGuid || !botAI->GetUnit(skullGuid)) - { - group->SetTargetIcon(7, bot->GetGUID(), boss->GetGUID()); - } - } + Unit* smallOoze = AI_VALUE2(Unit*, "find target", "little ooze"); + bool victimOfSmallOoze = smallOoze && smallOoze->GetVictim() == bot; + // Mark Rotface with skull + MarkBossWithSkull(boss); - // Main tank positioning logic - if (botAI->IsMainTank(bot)) - { - if (bot->GetExactDist2d(ICC_ROTFACE_TANK_POSITION) > 7.0f) - return MoveTo(bot->GetMapId(), ICC_ROTFACE_TANK_POSITION.GetPositionX(), - ICC_ROTFACE_TANK_POSITION.GetPositionY(), ICC_ROTFACE_TANK_POSITION.GetPositionZ(), false, - false, false, true, MovementPriority::MOVEMENT_COMBAT); - } - - bool hasOozeFlood = botAI->HasAura("Ooze Flood", bot); + // Main tank positioning and melee positioning + if ((botAI->IsMainTank(bot) || botAI->IsMelee(bot)) && !botAI->IsAssistTank(bot) && !victimOfSmallOoze) + return PositionMainTankAndMelee(boss); // Assist tank positioning for big ooze if (botAI->IsAssistTank(bot)) + return HandleAssistTankPositioning(boss); + + return false; +} + +void IccRotfaceTankPositionAction::MarkBossWithSkull(Unit* boss) +{ + Group* group = bot->GetGroup(); + if (!group) + return; + + constexpr uint8_t skullIconId = 7; + ObjectGuid skullGuid = group->GetTargetIcon(skullIconId); + if (skullGuid != boss->GetGUID()) + group->SetTargetIcon(skullIconId, bot->GetGUID(), boss->GetGUID()); +} + +bool IccRotfaceTankPositionAction::PositionMainTankAndMelee(Unit* boss) +{ + bool isBossCasting = boss && boss->HasUnitState(UNIT_STATE_CASTING) && boss->GetCurrentSpell(SPELL_SLIME_SPRAY); + + if (bot->GetExactDist2d(ICC_ROTFACE_CENTER_POSITION) > 7.0f && botAI->HasAggro(boss) && botAI->IsMainTank(bot)) + MoveTo(bot->GetMapId(), ICC_ROTFACE_CENTER_POSITION.GetPositionX(), + ICC_ROTFACE_CENTER_POSITION.GetPositionY(), ICC_ROTFACE_CENTER_POSITION.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + + if (boss && isBossCasting && !botAI->IsTank(bot)) { - // If we have the ooze flood aura, move away - if (hasOozeFlood) + float x = boss->GetPositionX(); + float y = boss->GetPositionY(); + float z = boss->GetPositionZ(); + + // If not already close to the boss's position, move there + if (bot->GetExactDist2d(x, y) > 0.5f) { - return MoveTo(boss->GetMapId(), boss->GetPositionX() + 5.0f * cos(bot->GetAngle(boss)), - boss->GetPositionY() + 5.0f * sin(bot->GetAngle(boss)), bot->GetPositionZ(), false, - false, false, true, MovementPriority::MOVEMENT_COMBAT); + MoveTo(bot->GetMapId(), x, y, z, false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, + false); + } + // Otherwise, already at the correct position + return false; + } + + if (!isBossCasting && (bot->GetExactDist2d(ICC_ROTFACE_CENTER_POSITION) < 2.0f || bot->GetExactDist2d(ICC_ROTFACE_CENTER_POSITION) > 7.0f) && !botAI->IsTank(bot)) + { + MoveTo(bot->GetMapId(), ICC_ROTFACE_CENTER_POSITION.GetPositionX(), ICC_ROTFACE_CENTER_POSITION.GetPositionY(), + bot->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + } + + return false; +} + +bool IccRotfaceTankPositionAction::HandleAssistTankPositioning(Unit* boss) +{ + // Handle big ooze positioning + return HandleBigOozePositioning(boss); +} + + +bool IccRotfaceTankPositionAction::HandleBigOozePositioning(Unit* boss) +{ + // Find all big oozes + GuidVector bigOozes = AI_VALUE(GuidVector, "nearest hostile npcs"); + std::vector activeBigOozes; + + for (const auto& guid : bigOozes) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_BIG_OOZE && unit->IsVisible()) + activeBigOozes.push_back(unit); + } + + if (activeBigOozes.empty()) + return false; + + // Iterate through all big oozes and handle them + for (Unit* bigOoze : activeBigOozes) + { + // Taunt if not targeting us + if (bigOoze->GetVictim() != bot && bigOoze->IsAlive() && bigOoze->IsVisible()) + { + if (botAI->CastSpell("taunt", bigOoze)) + return true; + bot->SetTarget(bigOoze->GetGUID()); + bot->SetFacingToObject(bigOoze); + return Attack(bigOoze); } - Unit* bigOoze = AI_VALUE2(Unit*, "find target", "big ooze"); - if (bigOoze) + // Calculate distances + float oozeDistance = bot->GetExactDist2d(bigOoze); + + // Stop moving if ooze is far enough + if (oozeDistance > 12.0f) { - // Taunt if not targeting us - if (bigOoze->GetVictim() != bot) - { - if (botAI->CastSpell("taunt", bigOoze)) - return true; - return Attack(bigOoze); - } + bot->SetTarget(bigOoze->GetGUID()); + bot->SetFacingToObject(bigOoze); + return true; + } - // Keep big ooze at designated position - if (bigOoze->GetVictim() == bot) + // If we have the ooze's aggro, kite it in a larger circular pattern between 20f and 30f from the center + if (bigOoze->GetVictim() == bot) + { + const float minRadius = 17.0f; + const float maxRadius = 25.0f; + const float safeDistanceFromOoze = 13.0f; + const float puddleSafeDistance = 30.0f; + const Position centerPosition = ICC_ROTFACE_CENTER_POSITION; + + float currentDistance = bot->GetExactDist2d(centerPosition); + + // If too close or too far, adjust position + if (currentDistance < minRadius || currentDistance > maxRadius) { - if (bot->GetExactDist2d(ICC_ROTFACE_BIG_OOZE_POSITION) > 5.0f) + // Calculate direction vector from bot to center + float dirX = bot->GetPositionX() - centerPosition.GetPositionX(); + float dirY = bot->GetPositionY() - centerPosition.GetPositionY(); + float length = std::sqrt(dirX * dirX + dirY * dirY); + + // Normalize direction vector + dirX /= length; + dirY /= length; + + // Adjust position to stay within the desired radius + float targetX = centerPosition.GetPositionX() + dirX * maxRadius; + float targetY = centerPosition.GetPositionY() + dirY * maxRadius; + + // Ensure the position is at least 10f away from the ooze + if (bigOoze->GetExactDist2d(targetX, targetY) >= safeDistanceFromOoze) { - return MoveTo(bot->GetMapId(), ICC_ROTFACE_BIG_OOZE_POSITION.GetPositionX(), - ICC_ROTFACE_BIG_OOZE_POSITION.GetPositionY(), ICC_ROTFACE_BIG_OOZE_POSITION.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + return MoveTo(bot->GetMapId(), targetX, targetY, bot->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); } - return Attack(bigOoze); } - return Attack(bigOoze); + // If within the desired radius, continue kiting in a circular pattern + float currentAngle = atan2(bot->GetPositionY() - centerPosition.GetPositionY(), + bot->GetPositionX() - centerPosition.GetPositionX()); + + // Adjust rotation direction to find a safe position + for (int i = 0; i < 16; ++i) // Try 16 directions around the circle + { + float angleOffset = (i % 2 == 0 ? 1 : -1) * (M_PI / 16.0f) * (i / static_cast(2)); + float newAngle = currentAngle + angleOffset; + + // Calculate new position along the circle + float newX = centerPosition.GetPositionX() + maxRadius * cos(newAngle); + float newY = centerPosition.GetPositionY() + maxRadius * sin(newAngle); + + // Ensure the position is at least 10f away from the ooze + if (bigOoze->GetExactDist2d(newX, newY) >= safeDistanceFromOoze) + { + // Check if the position is at least 30f away from any puddle + GuidVector puddles = AI_VALUE(GuidVector, "nearest hostile npcs"); + bool isSafeFromPuddles = true; + + for (const auto& puddleGuid : puddles) + { + Unit* puddle = botAI->GetUnit(puddleGuid); + if (puddle && botAI->GetAura("Ooze Flood", puddle)) + { + float puddleDistance = std::sqrt(std::pow(newX - puddle->GetPositionX(), 2) + + std::pow(newY - puddle->GetPositionY(), 2)); + if (puddleDistance < puddleSafeDistance) + { + isSafeFromPuddles = false; + break; + } + } + } + + if (isSafeFromPuddles) + { + return MoveTo(bot->GetMapId(), newX, newY, bot->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + } + } + } } } return false; } + bool IccRotfaceGroupPositionAction::Execute(Event event) { - // Find Rotface Unit* boss = AI_VALUE2(Unit*, "find target", "rotface"); if (!boss) - return false; + return false; - + const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + bool floodPresent = false; - // Check for puddles and move away if too close - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - bool hasOozeFlood = botAI->HasAura("Ooze Flood", bot); - - for (auto& npc : npcs) + for (const auto& npc : npcs) { Unit* unit = botAI->GetUnit(npc); - if (unit) + if (!unit || !botAI->HasAura("Ooze Flood", unit)) + continue; + + float puddleDistance = bot->GetExactDist2d(unit); + + if (puddleDistance < 30.0f) + floodPresent = true; + } + + Unit* bigOoze = AI_VALUE2(Unit*, "find target", "big ooze"); + bool hasOozeFlood = botAI->HasAura("Ooze Flood", bot); + Unit* smallOoze = AI_VALUE2(Unit*, "find target", "little ooze"); + bool hasMutatedInfection = botAI->HasAura("Mutated Infection", bot); + + // Handle puddle avoidance + if (!botAI->IsTank(bot) && HandlePuddleAvoidance(boss)) + return true; + + // Handle little ooze or mutated infection + if (HandleOozeTargeting()) + return true; + + // Position ranged and healers + if (/*!floodPresent && */ !((smallOoze && smallOoze->GetVictim() == bot) || hasMutatedInfection) && !hasOozeFlood && PositionRangedAndHealers(boss, smallOoze)) + return true; + + //if (!hasOozeFlood && bigOoze && bigOoze->IsAlive() && MoveAwayFromBigOoze(bigOoze)) + //return true; + + return false; +} + +bool IccRotfaceGroupPositionAction::HandlePuddleAvoidance(Unit* boss) +{ + const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + for (const auto& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (!unit || !botAI->HasAura("Ooze Flood", unit)) + continue; + + float puddleDistance = bot->GetExactDist2d(unit); + float bossDistance = bot->GetExactDist2d(ICC_ROTFACE_CENTER_POSITION); + + if (bossDistance < 15.0f) // Reduced center distance threshold + return false; + + if (puddleDistance < 30.0f) + return MoveAwayFromPuddle(boss, unit, puddleDistance); + } + + return false; +} + +bool IccRotfaceGroupPositionAction::MoveAwayFromPuddle(Unit* boss, Unit* puddle, float puddleDistance) +{ + if (!boss || !puddle) + return false; + + // Calculate angle and move direction + float dx = puddle->GetPositionX() - bot->GetPositionX(); + float dy = puddle->GetPositionY() - bot->GetPositionY(); + float angle = atan2(dy, dx); + + // Try to find a valid position in 8 directions + const float increment = 5.0f; + const float minPuddleDistance = 30.0f; + const float minCenterDistance = 15.0f; // Reduced center distance threshold + const float maxCenterDistance = 25.0f; // New maximum center distance threshold + const int directions = 8; // Number of directions to check + float bestX = bot->GetPositionX(); + float bestY = bot->GetPositionY(); + float bestZ = bot->GetPositionZ(); + float maxSafetyScore = 0.0f; + + for (int i = 0; i < directions; ++i) + { + float testAngle = angle + (i * M_PI / 4); // 8 directions (45-degree increments) + for (float distance = increment; distance <= 35.0f; distance += increment) { - if (unit->GetEntry() == 37013) // puddle + float moveX = bot->GetPositionX() - distance * cos(testAngle); + float moveY = bot->GetPositionY() - distance * sin(testAngle); + float moveZ = bot->GetPositionZ(); + + // Check distances and line of sight + float newPuddleDistance = + sqrt(pow(moveX - puddle->GetPositionX(), 2) + pow(moveY - puddle->GetPositionY(), 2)); + float newCenterDistance = sqrt(pow(moveX - ICC_ROTFACE_CENTER_POSITION.GetPositionX(), 2) + + pow(moveY - ICC_ROTFACE_CENTER_POSITION.GetPositionY(), 2)); + + if (newPuddleDistance >= minPuddleDistance && newCenterDistance >= minCenterDistance && + newCenterDistance <= maxCenterDistance && bot->IsWithinLOS(moveX, moveY, moveZ)) { - float puddleDistance = bot->GetExactDist2d(unit); - - - if (puddleDistance < 30.0f && (hasOozeFlood)) + // Calculate safety score (favor positions farther from puddle and center) + float safetyScore = newPuddleDistance + newCenterDistance; + if (safetyScore > maxSafetyScore) { - float dx = boss->GetPositionX() - unit->GetPositionX(); - float dy = boss->GetPositionY() - unit->GetPositionY(); - float angle = atan2(dy, dx); - - // Move away from puddle in smaller increment - float moveDistance = std::min(35.0f - puddleDistance, 5.0f); - float moveX = boss->GetPositionX() + (moveDistance * cos(angle)); - float moveY = boss->GetPositionY() + (moveDistance * sin(angle)); - - // Check if position is in LoS before moving - if (!bot->IsWithinLOS(moveX, moveY, boss->GetPositionZ())) - return false; - - return MoveTo(boss->GetMapId(), moveX, moveY, boss->GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + maxSafetyScore = safetyScore; + bestX = moveX; + bestY = moveY; + bestZ = moveZ; } } } + + // If we are already in a valid position, stop moving + if (maxSafetyScore > 0.0f && bot->GetExactDist2d(bestX, bestY) <= increment) + return false; } - // Check if we're targeted by little ooze + // Move to the best position found + if (maxSafetyScore > 0.0f) + { + return MoveTo(bot->GetMapId(), bestX, bestY, bestZ, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); + } + + return false; +} + +bool IccRotfaceGroupPositionAction::HandleOozeTargeting() +{ Unit* smallOoze = AI_VALUE2(Unit*, "find target", "little ooze"); bool hasMutatedInfection = botAI->HasAura("Mutated Infection", bot); if ((smallOoze && smallOoze->GetVictim() == bot) || hasMutatedInfection) - { - if (bot->GetExactDist2d(ICC_ROTFACE_BIG_OOZE_POSITION) > 3.0f) - { - // Check if position is in LoS before moving - if (!bot->IsWithinLOS(ICC_ROTFACE_BIG_OOZE_POSITION.GetPositionX(), - ICC_ROTFACE_BIG_OOZE_POSITION.GetPositionY(), - ICC_ROTFACE_BIG_OOZE_POSITION.GetPositionZ())) - return false; - - return MoveTo(bot->GetMapId(), ICC_ROTFACE_BIG_OOZE_POSITION.GetPositionX(), - ICC_ROTFACE_BIG_OOZE_POSITION.GetPositionY(), ICC_ROTFACE_BIG_OOZE_POSITION.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_COMBAT); - } - return true; // Stay at position - } - - if(botAI->IsRanged(bot) || botAI->IsHeal(bot)) - { - if (!hasOozeFlood) - { - float radius = 10.0f; - Unit* closestMember = nullptr; - float minDist = radius; - GuidVector members = AI_VALUE(GuidVector, "group members"); - - for (auto& member : members) - { - Unit* unit = botAI->GetUnit(member); - if (!unit || bot->GetGUID() == member) - continue; - - // Skip distance check if the other unit is an assist tank - if (botAI->IsAssistTank(bot)) - continue; - - float dist = bot->GetExactDist2d(unit); - if (dist < minDist) - { - minDist = dist; - closestMember = unit; - } - } - - if (closestMember) - { - float distToCenter = bot->GetExactDist2d(ICC_ROTFACE_TANK_POSITION); - float moveDistance = (distToCenter > 25.0f) ? 2.0f : 3.0f; - // return MoveAway(closestMember, moveDistance); - return FleePosition(closestMember->GetPosition(), moveDistance, 250U); - } - - return false; - } - } + return HandleOozeMemberPositioning(); return false; } -bool IccRotfaceMoveAwayFromExplosionAction::Execute(Event event) +bool IccRotfaceGroupPositionAction::HandleOozeMemberPositioning() { - if (botAI->IsMainTank(bot) || bot->HasAura(71215)) - return false; + Unit* bigOoze = AI_VALUE2(Unit*, "find target", "big ooze"); - // Stop current actions first - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - // Generate random angle between 0 and 2π - float angle = frand(0, 2 * M_PI); - - // Calculate position 20 yards away in random direction - float moveX = bot->GetPositionX() + 20.0f * cos(angle); - float moveY = bot->GetPositionY() + 20.0f * sin(angle); - float moveZ = bot->GetPositionZ(); - - // Check if position is in LoS before moving - if (!bot->IsWithinLOS(moveX, moveY, moveZ)) - return false; - - // Move to the position - return MoveTo(bot->GetMapId(), moveX, moveY, moveZ, - false, false, false, false, MovementPriority::MOVEMENT_FORCED); + // First case: No big ooze exists or is not alive, move to designated position + if (!bigOoze || !bigOoze->IsAlive() || !bigOoze->IsVisible()) + { + if (bot->GetExactDist2d(ICC_ROTFACE_BIG_OOZE_POSITION) > 3.0f) + { + return MoveTo(bot->GetMapId(), ICC_ROTFACE_BIG_OOZE_POSITION.GetPositionX(), + ICC_ROTFACE_BIG_OOZE_POSITION.GetPositionY(), ICC_ROTFACE_BIG_OOZE_POSITION.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + } + } + // Second case: Big ooze exists and is alive, move to it for merging + else if (bot->GetExactDist2d(bigOoze) > 2.0f && bigOoze->IsAlive() && bigOoze->IsVisible()) + { + // Move to big ooze for merge in increments of 5 + float dx = bigOoze->GetPositionX() - bot->GetPositionX(); + float dy = bigOoze->GetPositionY() - bot->GetPositionY(); + float dz = bigOoze->GetPositionZ() - bot->GetPositionZ(); + float dist = std::sqrt(dx * dx + dy * dy); + if (dist > 5.0f) + { + dx /= dist; + dy /= dist; + float moveX = bot->GetPositionX() + dx * 5.0f; + float moveY = bot->GetPositionY() + dy * 5.0f; + float moveZ = bot->GetPositionZ() + (dz / dist) * 5.0f; + return MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + } + return MoveTo(bot->GetMapId(), bigOoze->GetPositionX(), bigOoze->GetPositionY(), bigOoze->GetPositionZ(), false, + false, false, true, MovementPriority::MOVEMENT_COMBAT); + + } + + return false; // Stay at position } -//PP +bool IccRotfaceGroupPositionAction::PositionRangedAndHealers(Unit* boss,Unit *smallOoze) +{ + // Only for ranged and healers + if (!(botAI->IsRanged(bot) || botAI->IsHeal(bot))) + return false; -bool IccPutricideGrowingOozePuddleAction::Execute(Event event) + Difficulty diff = bot->GetRaidDifficulty(); + bool isBossCasting = boss && boss->HasUnitState(UNIT_STATE_CASTING) && boss->GetCurrentSpell(SPELL_SLIME_SPRAY); + bool isHeroic = (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC); + + // Move to the exact same position as the boss during slime spray + if (boss && isBossCasting && !isHeroic) + { + float x = boss->GetPositionX(); + float y = boss->GetPositionY(); + float z = boss->GetPositionZ(); + + // If not already close to the boss's position, move there + if (bot->GetExactDist2d(x, y) > 0.5f) + { + MoveTo(bot->GetMapId(), x, y, z, false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, + false); + } + // Otherwise, already at the correct position + return false; + } + + if (!isHeroic && !isBossCasting && boss && !(bot->getClass() == CLASS_HUNTER) && + (bot->GetExactDist2d(boss->GetPositionX(), boss->GetPositionY()) < 2.0f || + bot->GetExactDist2d(boss->GetPositionX(), boss->GetPositionY()) > 5.0f)) + { + MoveTo(bot->GetMapId(), boss->GetPositionX(), boss->GetPositionY(), + bot->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + } + + if (!isHeroic) + return false; + + return FindAndMoveFromClosestMember(boss, smallOoze); +} + +bool IccRotfaceGroupPositionAction::FindAndMoveFromClosestMember(Unit* boss, Unit* smallOoze) { - // Constants moved outside to prevent recreation on each call - static const float BASE_RADIUS = 2.0f; - static const float STACK_MULTIPLIER = 0.5f; - static const float MIN_DISTANCE = 0.1f; - static const float BUFFER_DISTANCE = 2.0f; - static const uint32 GROWING_OOZE_PUDDLE_ID = 37690; - static const uint32 GROW_AURA_ID = 70347; + const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + Unit* puddle = nullptr; + + for (const auto& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (!unit || !botAI->HasAura("Ooze Flood", unit)) + continue; + + puddle = unit; + break; + } + + const float safeSpacingRadius = 10.0f; + const float moveIncrement = 2.0f; + const float maxMoveDistance = 12.0f; // Limit maximum movement distance + const float puddleSafeDistance = 30.0f; // Minimum distance to stay away from puddle + const float minCenterDistance = 20.0f; // Minimum distance from center position + const bool isRanged = botAI->IsRanged(bot) || botAI->IsHeal(bot); + + // Ranged: spread from other members + const GuidVector members = AI_VALUE(GuidVector, "group members"); + + // Calculate a combined vector representing all nearby members' positions + float totalX = 0.0f; + float totalY = 0.0f; + int nearbyCount = 0; + + for (const auto& memberGuid : members) + { + Unit* member = botAI->GetUnit(memberGuid); + if (!member || !member->IsAlive() || member == bot || (smallOoze && smallOoze->GetVictim() == member) || + (member->GetTypeId() == TYPEID_PLAYER && botAI->IsAssistTank(static_cast(member)))) + continue; + + const float distance = bot->GetExactDist2d(member); + if (distance < safeSpacingRadius) + { + // Calculate vector from member to bot + float dx = bot->GetPositionX() - member->GetPositionX(); + float dy = bot->GetPositionY() - member->GetPositionY(); + + // Weight by inverse distance (closer members have more influence) + float weight = (safeSpacingRadius - distance) / safeSpacingRadius; + totalX += dx * weight; + totalY += dy * weight; + nearbyCount++; + } + } + + // If we have nearby members, move away in the combined direction + if (nearbyCount > 0) + { + // Normalize the combined vector + float magnitude = std::sqrt(totalX * totalX + totalY * totalY); + if (magnitude > 0.001f) // Avoid division by zero + { + totalX /= magnitude; + totalY /= magnitude; + + // Calculate move distance based on nearest member + float moveDistance = std::min(moveIncrement, maxMoveDistance); + + // Create target position in the combined direction + float targetX = bot->GetPositionX() + totalX * moveDistance; + float targetY = bot->GetPositionY() + totalY * moveDistance; + float targetZ = bot->GetPositionZ(); // Maintain current Z + + // Ensure the target position is at least 30 yards away from the puddle + if (puddle) + { + float puddleDistance = std::sqrt(std::pow(targetX - puddle->GetPositionX(), 2) + + std::pow(targetY - puddle->GetPositionY(), 2)); + if (puddleDistance < puddleSafeDistance) + { + // Adjust the target position to move further away from the puddle + float dx = targetX - puddle->GetPositionX(); + float dy = targetY - puddle->GetPositionY(); + float adjustmentFactor = (puddleSafeDistance - puddleDistance) / puddleDistance; + targetX += dx * adjustmentFactor; + targetY += dy * adjustmentFactor; + } + } + + // Ensure the target position is at least 20 yards away from the center position + const float centerX = ICC_ROTFACE_CENTER_POSITION.GetPositionX(); + const float centerY = ICC_ROTFACE_CENTER_POSITION.GetPositionY(); + float centerDistance = std::sqrt(std::pow(targetX - centerX, 2) + std::pow(targetY - centerY, 2)); + if (centerDistance < minCenterDistance) + { + // Adjust the target position to move further away from the center + float dx = targetX - centerX; + float dy = targetY - centerY; + float adjustmentFactor = (minCenterDistance - centerDistance) / centerDistance; + targetX += dx * adjustmentFactor; + targetY += dy * adjustmentFactor; + } + + // Check if the target position is valid and move there + if (bot->IsWithinLOS(targetX, targetY, targetZ)) + { + Position targetPos(targetX, targetY, targetZ); + MoveTo(bot->GetMapId(), targetPos.GetPositionX(), targetPos.GetPositionY(), targetPos.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_NORMAL); + } + else + { + // If los check fails, try shorter distance + targetX = bot->GetPositionX() + totalX * (moveDistance * 0.5f); + targetY = bot->GetPositionY() + totalY * (moveDistance * 0.5f); + Position targetPos(targetX, targetY, targetZ); + MoveTo(bot->GetMapId(), targetPos.GetPositionX(), targetPos.GetPositionY(), targetPos.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_NORMAL); + } + } + } + + return false; // Everyone is properly spaced +} + +bool IccRotfaceMoveAwayFromExplosionAction::Execute(Event event) +{ + // Skip if main tank or ooze flood + if (botAI->IsMainTank(bot)) + return false; + + botAI->Reset(); + + return MoveToRandomSafeLocation(); +} + +bool IccRotfaceMoveAwayFromExplosionAction::MoveToRandomSafeLocation() +{ + // Generate random angle + float angle = frand(0, 2 * M_PI); + + // Calculate initial move position + float moveX = bot->GetPositionX() + 5.0f * cos(angle); + float moveY = bot->GetPositionY() + 5.0f * sin(angle); + float moveZ = bot->GetPositionZ(); + + // Ensure the position is at least 30 yards away from any puddle + const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (const auto& npc : npcs) + { + Unit* puddle = botAI->GetUnit(npc); + if (!puddle || !botAI->HasAura("Ooze Flood", puddle)) + continue; + + float puddleDistance = + std::sqrt(std::pow(moveX - puddle->GetPositionX(), 2) + std::pow(moveY - puddle->GetPositionY(), 2)); + if (puddleDistance < 30.0f) + { + // Adjust the position to move further away from the puddle + float dx = moveX - puddle->GetPositionX(); + float dy = moveY - puddle->GetPositionY(); + float adjustmentFactor = (30.0f - puddleDistance) / puddleDistance; + moveX += dx * adjustmentFactor; + moveY += dy * adjustmentFactor; + } + } + + // Ensure the position is at least 30 yards away from the center position + const Position centerPosition = ICC_ROTFACE_CENTER_POSITION; + float centerDistance = std::sqrt(std::pow(moveX - centerPosition.GetPositionX(), 2) + + std::pow(moveY - centerPosition.GetPositionY(), 2)); + if (centerDistance < 40.0f) + { + // Adjust the position to move further away from the center + float dx = moveX - centerPosition.GetPositionX(); + float dy = moveY - centerPosition.GetPositionY(); + float adjustmentFactor = (40.0f - centerDistance) / centerDistance; + moveX += dx * adjustmentFactor; + moveY += dy * adjustmentFactor; + } + + // Check line of sight + if (!bot->IsWithinLOS(moveX, moveY, moveZ)) + return false; + + // Move in increments of 5.0f towards the calculated position + float currentX = bot->GetPositionX(); + float currentY = bot->GetPositionY(); + float currentZ = bot->GetPositionZ(); + + float directionX = moveX - currentX; + float directionY = moveY - currentY; + float distance = std::sqrt(directionX * directionX + directionY * directionY); + + if (distance > 5.0f) + { + directionX /= distance; + directionY /= distance; + + moveX = currentX + directionX * 5.0f; + moveY = currentY + directionY * 5.0f; + } + + // Move to the position + return MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, false, MovementPriority::MOVEMENT_FORCED); +} + +// Proffesor Putricide +bool IccPutricideGrowingOozePuddleAction::Execute(Event event) +{ + Unit* closestPuddle = FindClosestThreateningPuddle(); + if (!closestPuddle) + return false; + + Position movePosition = CalculateSafeMovePosition(closestPuddle); + return MoveTo(bot->GetMapId(), movePosition.GetPositionX(), movePosition.GetPositionY(), + movePosition.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); +} + +Unit* IccPutricideGrowingOozePuddleAction::FindClosestThreateningPuddle() +{ + static const float BASE_RADIUS = 2.0f; + static const float STACK_MULTIPLIER = 0.6f; + static const float MIN_DISTANCE = 0.1f; + + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + if (npcs.empty()) + return nullptr; + + Unit* closestPuddle = nullptr; + float closestDistance = FLT_MAX; + float closestSafeDistance = BASE_RADIUS; + + for (const auto& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (!unit || unit->GetEntry() != NPC_GROWING_OOZE_PUDDLE) + continue; + + float currentDistance = std::max(MIN_DISTANCE, bot->GetExactDist(unit)); + float safeDistance = BASE_RADIUS; + + if (Aura* grow = unit->GetAura(SPELL_GROW_AURA)) + safeDistance += (grow->GetStackAmount() * STACK_MULTIPLIER); + + if (currentDistance < safeDistance && currentDistance < closestDistance) + { + closestDistance = currentDistance; + closestSafeDistance = safeDistance; + closestPuddle = unit; + } + } + + return closestPuddle; +} + +Position IccPutricideGrowingOozePuddleAction::CalculateSafeMovePosition(Unit* closestPuddle) +{ + static const float BASE_RADIUS = 2.0f; + static const float STACK_MULTIPLIER = 0.6f; + static const float BUFFER_DISTANCE = 2.0f; + static const float MIN_DISTANCE = 0.1f; + static const int NUM_ANGLES_TO_TEST = 8; - // Cache bot position to avoid multiple calls float botX = bot->GetPositionX(); float botY = bot->GetPositionY(); float botZ = bot->GetPositionZ(); - // Get the nearest hostile NPCs - only once and store locally - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - if (npcs.empty()) - return false; + // Calculate current distance and safe distance + float currentDistance = std::max(MIN_DISTANCE, bot->GetExactDist(closestPuddle)); + float safeDistance = BASE_RADIUS; + if (Aura* grow = closestPuddle->GetAura(SPELL_GROW_AURA)) + safeDistance += (grow->GetStackAmount() * STACK_MULTIPLIER); - // Find puddles and their safe distances in one pass - std::vector> puddles; // Unit*, distance, safeDistance - puddles.reserve(8); // Pre-allocate a reasonable size - - for (auto& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (!unit || unit->GetEntry() != GROWING_OOZE_PUDDLE_ID) - continue; - - float currentDistance = std::max(MIN_DISTANCE, bot->GetExactDist(unit)); - - float safeDistance = BASE_RADIUS; - if (Aura* grow = unit->GetAura(GROW_AURA_ID)) - safeDistance += (grow->GetStackAmount() * STACK_MULTIPLIER); - - puddles.emplace_back(unit, currentDistance, safeDistance); - } - - // If no puddles found, exit early - if (puddles.empty()) - return false; - - // Find the closest threatening puddle - Unit* closestPuddle = nullptr; - float closestDistance = FLT_MAX; - float closestSafeDistance = BASE_RADIUS; - bool needToMove = false; - - for (const auto& [puddle, distance, safeDistance] : puddles) - { - if (distance < safeDistance && distance < closestDistance) - { - closestDistance = distance; - closestSafeDistance = safeDistance; - closestPuddle = puddle; - needToMove = true; - } - } - - // If we don't need to move, exit early - if (!needToMove) - return false; - - // Calculate vector from puddle to bot + // Calculate direction vector float dx = botX - closestPuddle->GetPositionX(); float dy = botY - closestPuddle->GetPositionY(); float dist = std::max(MIN_DISTANCE, sqrt(dx * dx + dy * dy)); - // If we're too close or inside, pick a random direction to move if (dist < MIN_DISTANCE * 2) { float randomAngle = float(rand()) / float(RAND_MAX) * 2 * M_PI; @@ -1298,55 +2245,85 @@ bool IccPutricideGrowingOozePuddleAction::Execute(Event event) dy /= dist; } - // Calculate move distance once - float moveDistance = closestSafeDistance - closestDistance + BUFFER_DISTANCE; + float moveDistance = safeDistance - currentDistance + BUFFER_DISTANCE; - // Try different angles to find a safe path - const int numAngles = 8; - for (int i = 0; i < numAngles; i++) + // Try different angles to find safe position + for (int i = 0; i < NUM_ANGLES_TO_TEST; i++) { - float angle = (2 * M_PI * i) / numAngles; + float angle = (2 * M_PI * i) / NUM_ANGLES_TO_TEST; float rotatedDx = dx * cos(angle) - dy * sin(angle); float rotatedDy = dx * sin(angle) + dy * cos(angle); float testX = botX + rotatedDx * moveDistance; float testY = botY + rotatedDy * moveDistance; - // Skip LOS check if too close to other puddles - bool tooCloseToOtherPuddle = false; - for (const auto& [otherPuddle, _, otherSafeDistance] : puddles) + if (!IsPositionTooCloseToOtherPuddles(testX, testY, closestPuddle) && bot->IsWithinLOS(testX, testY, botZ)) { - if (otherPuddle == closestPuddle) - continue; - - float newDist = - sqrt(pow(testX - otherPuddle->GetPositionX(), 2) + pow(testY - otherPuddle->GetPositionY(), 2)); - - if (newDist < otherSafeDistance) + // If main tank, add 6f to calculated position in the direction away from the puddle + if (botAI->IsTank(bot)) { - tooCloseToOtherPuddle = true; - break; + float awayDx = testX - closestPuddle->GetPositionX(); + float awayDy = testY - closestPuddle->GetPositionY(); + float awayDist = std::sqrt(awayDx * awayDx + awayDy * awayDy); + if (awayDist > 0.001f) + { + awayDx /= awayDist; + awayDy /= awayDist; + testX += awayDx * 6.0f; + testY += awayDy * 6.0f; + } } - } - - if (!tooCloseToOtherPuddle && bot->IsWithinLOS(testX, testY, botZ)) - { - // Found a safe path, move there - return MoveTo(bot->GetMapId(), testX, testY, botZ, false, false, false, false, - MovementPriority::MOVEMENT_COMBAT); + return Position(testX, testY, botZ); } } - // If we couldn't find a safe path, at least try to move away from the closest puddle - return MoveTo(bot->GetMapId(), botX + dx * moveDistance, botY + dy * moveDistance, botZ, false, false, false, false, - MovementPriority::MOVEMENT_COMBAT); + // Fallback position if no safe position found + float fallbackX = botX + dx * moveDistance; + float fallbackY = botY + dy * moveDistance; + if (botAI->IsTank(bot)) + { + float awayDx = fallbackX - closestPuddle->GetPositionX(); + float awayDy = fallbackY - closestPuddle->GetPositionY(); + float awayDist = std::sqrt(awayDx * awayDx + awayDy * awayDy); + if (awayDist > 0.001f) + { + awayDx /= awayDist; + awayDy /= awayDist; + fallbackX += awayDx * 6.0f; + fallbackY += awayDy * 6.0f; + } + } + return Position(fallbackX, fallbackY, botZ); +} + +bool IccPutricideGrowingOozePuddleAction::IsPositionTooCloseToOtherPuddles(float x, float y, Unit* ignorePuddle) +{ + static const float BASE_RADIUS = 2.0f; + static const float STACK_MULTIPLIER = 0.6f; + + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (const auto& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (!unit || unit == ignorePuddle || unit->GetEntry() != NPC_GROWING_OOZE_PUDDLE) + continue; + + float safeDistance = BASE_RADIUS; + if (Aura* grow = unit->GetAura(SPELL_GROW_AURA)) + safeDistance += (grow->GetStackAmount() * STACK_MULTIPLIER); + + float dist = sqrt(pow(x - unit->GetPositionX(), 2) + pow(y - unit->GetPositionY(), 2)); + if (dist < safeDistance) + return true; + } + + return false; } bool IccPutricideVolatileOozeAction::Execute(Event event) { - const float STACK_DISTANCE = 8.0f; + static const float STACK_DISTANCE = 7.0f; - // Find the ooze Unit* ooze = AI_VALUE2(Unit*, "find target", "volatile ooze"); if (!ooze) return false; @@ -1355,607 +2332,969 @@ bool IccPutricideVolatileOozeAction::Execute(Event event) if (!boss) return false; - if (botAI->IsTank(bot) && bot->GetExactDist2d(ICC_PUTRICIDE_TANK_POSITION) > 20.0f && !boss->HealthBelowPct(36) && boss->GetVictim() == bot) - { - return MoveTo(bot->GetMapId(), ICC_PUTRICIDE_TANK_POSITION.GetPositionX(), - ICC_PUTRICIDE_TANK_POSITION.GetPositionY(), ICC_PUTRICIDE_TANK_POSITION.GetPositionZ(), false, false, - false, true, MovementPriority::MOVEMENT_COMBAT, true, false); - } + // Main tank handling + if (botAI->IsMainTank(bot) && bot->GetExactDist2d(ICC_PUTRICIDE_TANK_POSITION) > 20.0f && + !boss->HealthBelowPct(36) && boss->GetVictim() == bot) + return MoveTo(bot->GetMapId(), ICC_PUTRICIDE_TANK_POSITION.GetPositionX(), ICC_PUTRICIDE_TANK_POSITION.GetPositionY(), + ICC_PUTRICIDE_TANK_POSITION.GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_COMBAT, true, false); - bool botHasAura = botAI->HasAura("Volatile Ooze Adhesive", bot); - bool botHasAura2 = botAI->HasAura("Gaseous Bloat", bot); - bool botHasAura3 = botAI->HasAura("Unbound Plague", bot); - - if (botHasAura2 || botHasAura3) + // Skip if we have forbidden auras + if (botAI->HasAura("Gaseous Bloat", bot) || botAI->HasAura("Unbound Plague", bot)) return false; - // Mark Volatile Ooze with skull if not already marked - if (Group* group = bot->GetGroup()) + // Mark ooze with skull + MarkOozeWithSkull(ooze); + + // Melee handling (non-tanks) + if (botAI->IsMelee(bot) && !botAI->IsMainTank(bot)) { - ObjectGuid skullGuid = group->GetTargetIcon(7); // 7 = skull - Unit* markedUnit = botAI->GetUnit(skullGuid); - - // Clear mark if current marked target is dead - if (markedUnit && !markedUnit->IsAlive()) + bot->SetTarget(ooze->GetGUID()); + bot->SetFacingToObject(ooze); + if (bot->IsWithinMeleeRange(ooze)) + return Attack(ooze); + } + + // Ranged/healer handling + if (botAI->IsRanged(bot) || botAI->IsHeal(bot)) + { + Unit* stackTarget = FindAuraTarget(); + if (stackTarget && bot->GetDistance2d(stackTarget) > STACK_DISTANCE) { - group->SetTargetIcon(7, bot->GetGUID(), ObjectGuid::Empty); + return MoveTo(bot->GetMapId(), stackTarget->GetPositionX(), stackTarget->GetPositionY(), + stackTarget->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); } - // Mark new ooze if it exists and nothing is marked - if (ooze && ooze->IsAlive() && (!skullGuid || !markedUnit)) + if (ooze && !botAI->IsHeal(bot) && stackTarget && bot->GetDistance2d(stackTarget) <= STACK_DISTANCE) { - group->SetTargetIcon(7, bot->GetGUID(), ooze->GetGUID()); + bot->SetTarget(ooze->GetGUID()); + bot->SetFacingToObject(ooze); + if (bot->IsWithinRange(ooze, 25.0f)) + return Attack(ooze); } } - // Check for aura on any group member + return false; +} + +void IccPutricideVolatileOozeAction::MarkOozeWithSkull(Unit* ooze) +{ Group* group = bot->GetGroup(); if (!group) - return false; + return; - Unit* auraTarget = nullptr; - Unit* stackTarget = nullptr; - bool anyoneHasAura = false; + constexpr uint8_t skullIconId = 7; + ObjectGuid skullGuid = group->GetTargetIcon(skullIconId); + Unit* markedUnit = botAI->GetUnit(skullGuid); + + // Clear dead marks or marks that are not on ooze + if (markedUnit && (!markedUnit->IsAlive() || (ooze && markedUnit != ooze))) + group->SetTargetIcon(skullIconId, bot->GetGUID(), ObjectGuid::Empty); + + // Mark alive ooze if needed + if (ooze && ooze->IsAlive() && (!skullGuid || !markedUnit)) + group->SetTargetIcon(skullIconId, bot->GetGUID(), ooze->GetGUID()); +} + +Unit* IccPutricideVolatileOozeAction::FindAuraTarget() +{ + Group* group = bot->GetGroup(); + if (!group) + return nullptr; - // First, try to find someone with the aura for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) { Player* member = itr->GetSource(); if (!member || !member->IsAlive() || member == bot) continue; - + if (botAI->HasAura("Volatile Ooze Adhesive", member)) - { - anyoneHasAura = true; - auraTarget = member; - stackTarget = member; - break; - } + return member; } - // If no one has aura, find a ranged player to stack with - /*if (!anyoneHasAura && !stackTarget) - { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || member == bot || - botAI->IsTank(member) || botAI->HasAura("Gaseous Bloat", member) || - botAI->HasAura("Unbound Plague", member)) - continue; - - if (botAI->IsRanged(member) && ) - { - stackTarget = member; - break; - } - } - } - */ - - /* - // For melee old stacking - if (botAI->IsMelee(bot) && !botAI->IsMainTank(bot)) - { - // If ooze exists and someone has aura, attack the ooze - if (ooze && anyoneHasAura) - { - bot->SetTarget(ooze->GetGUID()); - return Attack(ooze); - } - // Otherwise stack with ranged - else if (stackTarget) - { - if (bot->GetDistance2d(stackTarget) > STACK_DISTANCE) - { - return MoveTo(bot->GetMapId(), stackTarget->GetPositionX(), - stackTarget->GetPositionY(), stackTarget->GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_COMBAT); - } - } - } - */ - - // For melee new only attacking - if (botAI->IsMelee(bot) && !botAI->IsMainTank(bot)) - { - // If ooze exists and someone has aura, attack the ooze - if (ooze) - { - bot->SetTarget(ooze->GetGUID()); - return Attack(ooze); - } - } - - // For ranged and healers - if (botAI->IsRanged(bot) || botAI->IsHeal(bot)) - { - // Always try to stack - if (stackTarget && bot->GetDistance2d(stackTarget) > STACK_DISTANCE) - { - return MoveTo(bot->GetMapId(), stackTarget->GetPositionX(), - stackTarget->GetPositionY(), stackTarget->GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_COMBAT); - } - - // If stacked and ooze exists, attack it (except healers) - if (ooze && !botAI->IsHeal(bot) && stackTarget && - bot->GetDistance2d(stackTarget) <= STACK_DISTANCE) - { - bot->SetTarget(ooze->GetGUID()); - return Attack(ooze); - } - else if (botAI->IsHeal(bot)) - { - return false; // Allow healer to continue with normal healing actions - } - } - - return false; + return nullptr; } + bool IccPutricideGasCloudAction::Execute(Event event) { - if (botAI->IsMainTank(bot)) - return false; - Unit* gasCloud = AI_VALUE2(Unit*, "find target", "gas cloud"); if (!gasCloud) return false; - Unit* volatileOoze = AI_VALUE2(Unit*, "find target", "volatile ooze"); - Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); if (!boss) return false; - bool botHasAura = botAI->HasAura("Gaseous Bloat", bot); - - if(!botHasAura && volatileOoze) - return false; - + // Tank positioning logic if (botAI->IsTank(bot) && bot->GetExactDist2d(ICC_PUTRICIDE_TANK_POSITION) > 20.0f && !boss->HealthBelowPct(36) && boss->GetVictim() == bot) - { return MoveTo(bot->GetMapId(), ICC_PUTRICIDE_TANK_POSITION.GetPositionX(), ICC_PUTRICIDE_TANK_POSITION.GetPositionY(), ICC_PUTRICIDE_TANK_POSITION.GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_COMBAT, true, false); + + if (botAI->IsMainTank(bot)) + return false; + + bool hasGaseousBloat = botAI->HasAura("Gaseous Bloat", bot); + Unit* volatileOoze = AI_VALUE2(Unit*, "find target", "volatile ooze"); + + // Skip if we have no aura but ooze exists + if (!hasGaseousBloat && volatileOoze) + return false; + + if (hasGaseousBloat) + return HandleGaseousBloatMovement(gasCloud); + + return HandleGroupAuraSituation(gasCloud); +} + +bool IccPutricideGasCloudAction::HandleGaseousBloatMovement(Unit* gasCloud) +{ + static const int NUM_ANGLES = 16; + static const float MIN_SAFE_DISTANCE = 25.0f; + static const float EMERGENCY_DISTANCE = 8.0f; + static const float GAS_BOMB_SAFE_DIST = 6.0f; + + Position botPos = bot->GetPosition(); + Position cloudPos = gasCloud->GetPosition(); + float cloudDist = gasCloud->GetExactDist2d(botPos); + + if (cloudDist >= MIN_SAFE_DISTANCE) + return false; + + // Gather all choking gas bombs + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + std::vector gasBombs; + for (const auto& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_CHOKING_GAS_BOMB) + gasBombs.push_back(unit); } - if (botHasAura) + // Calculate direction away from cloud + float dx = botPos.GetPositionX() - cloudPos.GetPositionX(); + float dy = botPos.GetPositionY() - cloudPos.GetPositionY(); + float dist = std::max(0.1f, sqrt(dx * dx + dy * dy)); + + if (dist <= 0) + return false; + + dx /= dist; + dy /= dist; + + // Try to find safe movement position + Position bestPos; + bool foundPath = false; + float bestGasBombDist = 0.0f; + + for (int i = 0; i < NUM_ANGLES; i++) { - float botX = bot->GetPositionX(); - float botY = bot->GetPositionY(); - float botZ = bot->GetPositionZ(); - - float cloudX = gasCloud->GetPositionX(); - float cloudY = gasCloud->GetPositionY(); - float cloudDist = gasCloud->GetExactDist2d(botX, botY); - - // Only move if cloud is close enough to be dangerous - if (cloudDist < 25.0f) + float angle = (2 * M_PI * i) / NUM_ANGLES; + float rotatedDx = dx * cos(angle) - dy * sin(angle); + float rotatedDy = dx * sin(angle) + dy * cos(angle); + + for (float testDist = 5.0f; testDist <= 15.0f; testDist += 5.0f) { - // Calculate vector from cloud to bot - float dx = botX - cloudX; - float dy = botY - cloudY; - float dist = sqrt(dx * dx + dy * dy); - - if (dist > 0) + float testX = botPos.GetPositionX() + rotatedDx * testDist; + float testY = botPos.GetPositionY() + rotatedDy * testDist; + float testZ = botPos.GetPositionZ(); + Position testPos(testX, testY, testZ); + + float newCloudDist = cloudPos.GetExactDist2d(testPos); + + // Check gas bomb distance + float minGasBombDist = FLT_MAX; + for (Unit* bomb : gasBombs) { - dx /= dist; - dy /= dist; + float bombDist = sqrt(pow(testX - bomb->GetPositionX(), 2) + pow(testY - bomb->GetPositionY(), 2)); + if (bombDist < minGasBombDist) + minGasBombDist = bombDist; + } - // Try different angles to find a safe path - const int numAngles = 16; // Increased for more precise movement - float bestMoveX = botX; - float bestMoveY = botY; - float bestDist = cloudDist; - bool foundPath = false; - - for (int i = 0; i < numAngles; i++) + if (newCloudDist > cloudDist && minGasBombDist >= GAS_BOMB_SAFE_DIST && + bot->IsWithinLOS(testX, testY, testZ)) + { + // Prefer positions with greater minGasBombDist + if (!foundPath || minGasBombDist > bestGasBombDist) { - float angle = (2 * M_PI * i) / numAngles; - float rotatedDx = dx * cos(angle) - dy * sin(angle); - float rotatedDy = dx * sin(angle) + dy * cos(angle); - - // Try different distances - for (float testDist = 5.0f; testDist <= 15.0f; testDist += 5.0f) - { - float testX = botX + rotatedDx * testDist; - float testY = botY + rotatedDy * testDist; - float testZ = botZ; - - float newCloudDist = gasCloud->GetExactDist2d(testX, testY); - - // Check if this position is better - if (newCloudDist > bestDist && bot->IsWithinLOS(testX, testY, testZ)) - { - bestMoveX = testX; - bestMoveY = testY; - bestDist = newCloudDist; - foundPath = true; - } - } - } - - if (foundPath) - { - return MoveTo(bot->GetMapId(), bestMoveX, bestMoveY, botZ, - false, false, false, false, MovementPriority::MOVEMENT_COMBAT); - } - else if (cloudDist < 8.0f) // Emergency move if very close and no good path found - { - // Try to move directly away - float emergencyX = botX + dx * 10.0f; - float emergencyY = botY + dy * 10.0f; - - if (bot->IsWithinLOS(emergencyX, emergencyY, botZ)) - { - return MoveTo(bot->GetMapId(), emergencyX, emergencyY, botZ, - false, false, false, false, MovementPriority::MOVEMENT_COMBAT); - } + bestPos = testPos; + bestGasBombDist = minGasBombDist; + foundPath = true; } } } } - else + if (foundPath) + return MoveTo(bot->GetMapId(), bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT); + + // Emergency movement if too close + if (cloudDist < EMERGENCY_DISTANCE) { - Group* group = bot->GetGroup(); - if (!group) - return false; - - bool someoneHasAura = false; - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + Position emergencyPos = CalculateEmergencyPosition(botPos, dx, dy); + // Also check gas bomb distance for emergency move + float minGasBombDist = FLT_MAX; + for (Unit* bomb : gasBombs) { - Player* member = itr->GetSource(); - if (!member) - continue; - - if (botAI->HasAura("Gaseous Bloat", member)) - { - someoneHasAura = true; - break; - } - } - - if (someoneHasAura && !botAI->IsHeal(bot)) - { - return Attack(gasCloud); + float bombDist = sqrt(pow(emergencyPos.GetPositionX() - bomb->GetPositionX(), 2) + + pow(emergencyPos.GetPositionY() - bomb->GetPositionY(), 2)); + if (bombDist < minGasBombDist) + minGasBombDist = bombDist; } + if (minGasBombDist >= GAS_BOMB_SAFE_DIST && + bot->IsWithinLOS(emergencyPos.GetPositionX(), emergencyPos.GetPositionY(), emergencyPos.GetPositionZ())) + return MoveTo(bot->GetMapId(), emergencyPos.GetPositionX(), emergencyPos.GetPositionY(), + emergencyPos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); } return false; } -bool AvoidMalleableGooAction::Execute(Event event) +bool IccPutricideGasCloudAction::FindSafeMovementPosition(const Position& botPos, const Position& cloudPos, float dx, + float dy, int numAngles, Position& resultPos) { + float bestDist = cloudPos.GetExactDist2d(botPos); + bool foundPath = false; + resultPos = botPos; - bool hasUnboundPlague = botAI->HasAura("Unbound Plague", bot); - const float UNBOUND_PLAGUE_DISTANCE = 15.0f; - - // If bot has unbound plague, keep away from all other players - if (hasUnboundPlague) + for (int i = 0; i < numAngles; i++) { - Group* group = bot->GetGroup(); - if (!group) - return false; + float angle = (2 * M_PI * i) / numAngles; + float rotatedDx = dx * cos(angle) - dy * sin(angle); + float rotatedDy = dx * sin(angle) + dy * cos(angle); - float closestDistance = UNBOUND_PLAGUE_DISTANCE; - Unit* closestPlayer = nullptr; - - // Find closest player - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + for (float testDist = 5.0f; testDist <= 15.0f; testDist += 5.0f) { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || member == bot) - continue; + Position testPos(botPos.GetPositionX() + rotatedDx * testDist, botPos.GetPositionY() + rotatedDy * testDist, + botPos.GetPositionZ()); - float dist = bot->GetDistance2d(member); - if (dist < closestDistance) + float newCloudDist = cloudPos.GetExactDist2d(testPos); + if (newCloudDist > bestDist && + bot->IsWithinLOS(testPos.GetPositionX(), testPos.GetPositionY(), testPos.GetPositionZ())) { - closestDistance = dist; - closestPlayer = member; - } - } - - // Move away from closest player if too close - if (closestPlayer) - { - float dx = bot->GetPositionX() - closestPlayer->GetPositionX(); - float dy = bot->GetPositionY() - closestPlayer->GetPositionY(); - float dist = sqrt(dx * dx + dy * dy); - - if (dist > 0) - { - dx /= dist; - dy /= dist; - float moveDistance = UNBOUND_PLAGUE_DISTANCE - closestDistance + 2.0f; - - float moveX = bot->GetPositionX() + dx * moveDistance; - float moveY = bot->GetPositionY() + dy * moveDistance; - - if (bot->IsWithinLOS(moveX, moveY, bot->GetPositionZ())) - { - return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_COMBAT); - } + resultPos = testPos; + bestDist = newCloudDist; + foundPath = true; } } } - if (botAI->IsRanged(bot) || botAI->IsHeal(bot)) + return foundPath; +} + +Position IccPutricideGasCloudAction::CalculateEmergencyPosition(const Position& botPos, float dx, float dy) +{ + return Position(botPos.GetPositionX() + dx * 10.0f, botPos.GetPositionY() + dy * 10.0f, botPos.GetPositionZ()); +} + +bool IccPutricideGasCloudAction::HandleGroupAuraSituation(Unit* gasCloud) +{ + Group* group = bot->GetGroup(); + if (!group || botAI->IsHeal(bot)) + return false; + + // Mark gas cloud with skull if no volatile ooze is present or alive + Unit* volatileOoze = AI_VALUE2(Unit*, "find target", "volatile ooze"); + if ((!volatileOoze || !volatileOoze->IsAlive()) && gasCloud && gasCloud->IsAlive()) { - float radius = 7.0f; - bool isRanged = botAI->IsRanged(bot); - - GuidVector members = AI_VALUE(GuidVector, "group members"); - if (isRanged) + Group* group = bot->GetGroup(); + if (group) { - for (auto& member : members) - { - Unit* unit = botAI->GetUnit(member); - if (!unit || !unit->IsAlive() || unit == bot || botAI->IsTank(bot) || botAI->IsMelee(bot)) - continue; + constexpr uint8_t skullIconId = 7; + ObjectGuid currentSkull = group->GetTargetIcon(skullIconId); + Unit* markedUnit = botAI->GetUnit(currentSkull); + if (!markedUnit || !markedUnit->IsAlive() || markedUnit != gasCloud) + group->SetTargetIcon(skullIconId, bot->GetGUID(), gasCloud->GetGUID()); + } + } - float dist = bot->GetExactDist2d(unit); - if (dist < radius) - { - float moveDistance = radius - dist + 1.0f; - - // Calculate potential new position - float angle = bot->GetAngle(unit); - float newX = bot->GetPositionX() + cos(angle + M_PI) * moveDistance; - float newY = bot->GetPositionY() + sin(angle + M_PI) * moveDistance; - - // Only move if we have line of sight - if (bot->IsWithinLOS(newX, newY, bot->GetPositionZ())) - { - return FleePosition(unit->GetPosition(), moveDistance); - // return MoveAway(unit, moveDistance); - } - } - } + if (GroupHasGaseousBloat(group) && gasCloud) + { + bot->SetTarget(gasCloud->GetGUID()); + bot->SetFacingToObject(gasCloud); + + // Added movement logic for ranged attackers + if (botAI->IsRanged(bot) && !botAI->IsHeal(bot)) + return Attack(gasCloud); + + // Added movement logic for melee attackers + if (botAI->IsMelee(bot) && !botAI->IsTank(bot)) + return Attack(gasCloud); + } + + return false; +} + +bool IccPutricideGasCloudAction::GroupHasGaseousBloat(Group* group) +{ + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && botAI->HasAura("Gaseous Bloat", member)) + return true; + } + return false; +} + +bool IccPutricideAvoidMalleableGooAction::Execute(Event event) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); + if (!boss) + return false; + + // Tank handling for choking gas bomb + if (HandleTankPositioning(boss)) + return true; + + // Skip if volatile ooze or gas cloud exists + if (AI_VALUE2(Unit*, "find target", "volatile ooze") || AI_VALUE2(Unit*, "find target", "gas cloud")) + return false; + + // Handle unbound plague movement + if (HandleUnboundPlague(boss)) + return true; + + // Handle ranged/melee positioning + return HandleBossPositioning(boss); +} + +bool IccPutricideAvoidMalleableGooAction::HandleTankPositioning(Unit* boss) +{ + if (!botAI->IsTank(bot)) + return false; + + Unit* bomb = bot->FindNearestCreature(NPC_CHOKING_GAS_BOMB, 100.0f); + if (!bomb) + return false; + + const float safeDistance = 15.0f; + float currentDistance = bot->GetDistance2d(bomb); + + if (currentDistance < safeDistance) + return MoveAway(bomb, safeDistance - currentDistance); + + return false; +} + +bool IccPutricideAvoidMalleableGooAction::HandleUnboundPlague(Unit* boss) +{ + if (boss && boss->HealthBelowPct(35)) + return false; + + if (!botAI->HasAura("Unbound Plague", bot)) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + const float UNBOUND_PLAGUE_DISTANCE = 20.0f; + float closestDistance = UNBOUND_PLAGUE_DISTANCE; + Unit* closestPlayer = nullptr; + + // Find closest player + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; + + float dist = bot->GetDistance2d(member); + if (dist < closestDistance) + { + closestDistance = dist; + closestPlayer = member; + } + } + + if (!closestPlayer || closestDistance >= UNBOUND_PLAGUE_DISTANCE) + return true; + + // Calculate move away position + float dx = bot->GetPositionX() - closestPlayer->GetPositionX(); + float dy = bot->GetPositionY() - closestPlayer->GetPositionY(); + float dist = sqrt(dx * dx + dy * dy); + + if (dist <= 0) + return false; + + dx /= dist; + dy /= dist; + float moveDistance = UNBOUND_PLAGUE_DISTANCE - closestDistance + 2.0f; + + float moveX = bot->GetPositionX() + dx * moveDistance; + float moveY = bot->GetPositionY() + dy * moveDistance; + + if (bot->IsWithinLOS(moveX, moveY, bot->GetPositionZ())) + { + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); + } + + return false; +} + + +bool IccPutricideAvoidMalleableGooAction::HandleBossPositioning(Unit* boss) +{ + if (botAI->IsTank(bot)) + return false; + + // If boss is close to putricide_bad_position, all non-tank bots should be 1f in front of boss + const float BAD_POS_THRESHOLD = 10.0f; + const float IN_FRONT_DISTANCE = 1.0f; + float bossToBadPos = boss->GetExactDist2d(ICC_PUTRICIDE_BAD_POSITION.GetPositionX(), ICC_PUTRICIDE_BAD_POSITION.GetPositionY()); + + if (bossToBadPos <= BAD_POS_THRESHOLD) + { + // Move to 1f in front of boss + float bossOrientation = boss->GetOrientation(); + float targetX = boss->GetPositionX() + cos(bossOrientation) * IN_FRONT_DISTANCE; + float targetY = boss->GetPositionY() + sin(bossOrientation) * IN_FRONT_DISTANCE; + float targetZ = boss->GetPositionZ(); + + // Only move if not already close enough + if (bot->GetExactDist2d(targetX, targetY) > 0.5f) + { + bot->SetFacingToObject(boss); + return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, botAI->IsRanged(bot), + MovementPriority::MOVEMENT_COMBAT); } return false; } + float distToBoss = bot->GetExactDist2d(boss); + bool isRanged = botAI->IsRanged(bot); + + // Calculate desired position in front of boss + float desiredDistance = + isRanged ? ((bot->getClass() == CLASS_HUNTER) ? 14.0f : 6.0f) : (distToBoss < 2.0f ? 3.0f : 5.0f); + + // Check if we need to move + if ((std::abs(distToBoss - desiredDistance) > 0.5f || !boss->isInFront(bot)) && + (!isRanged || (isRanged && !botAI->IsTank(bot)))) + { + Position targetPos = CalculateBossPosition(boss, desiredDistance); + + // Check for obstacles + if (HasObstacleBetween(bot->GetPosition(), targetPos)) + { + // Use arc movement to navigate around obstacles + Position arcPoint = CalculateArcPoint(bot->GetPosition(), targetPos, boss->GetPosition()); + + if (bot->GetExactDist2d(arcPoint) > 1.0f) + { + bot->SetFacingToObject(boss); + return MoveTo(bot->GetMapId(), arcPoint.GetPositionX(), arcPoint.GetPositionY(), + arcPoint.GetPositionZ(), false, false, false, isRanged, + MovementPriority::MOVEMENT_COMBAT); + } + } + else + { + // No obstacles, move in increments directly toward target + Position adjustedTarget = CalculateIncrementalMove(bot->GetPosition(), targetPos, 2.0f); + bot->SetFacingToObject(boss); + return MoveTo(bot->GetMapId(), adjustedTarget.GetPositionX(), adjustedTarget.GetPositionY(), + adjustedTarget.GetPositionZ(), false, false, false, isRanged, + MovementPriority::MOVEMENT_COMBAT); + } + } + return false; } -//BPC +Position IccPutricideAvoidMalleableGooAction::CalculateBossPosition(Unit* boss, float distance) +{ + float bossOrientation = boss->GetOrientation(); + return Position(boss->GetPositionX() + cos(bossOrientation) * distance, + boss->GetPositionY() + sin(bossOrientation) * distance, boss->GetPositionZ()); +} + +bool IccPutricideAvoidMalleableGooAction::HasObstacleBetween(const Position& from, const Position& to) +{ + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (const auto& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (!unit || !unit->IsAlive()) + continue; + + if (unit->GetEntry() == NPC_GROWING_OOZE_PUDDLE || unit->GetEntry() == NPC_CHOKING_GAS_BOMB) + { + if (IsOnPath(from, to, unit->GetPosition(), 3.0f)) + return true; + } + } + return false; +} + +bool IccPutricideAvoidMalleableGooAction::IsOnPath(const Position& from, const Position& to, const Position& point, + float threshold) +{ + float pathX = to.GetPositionX() - from.GetPositionX(); + float pathY = to.GetPositionY() - from.GetPositionY(); + float pathLen = std::sqrt(pathX * pathX + pathY * pathY); + + if (pathLen < 0.1f) + return false; + + float normX = pathX / pathLen; + float normY = pathY / pathLen; + + float toPointX = point.GetPositionX() - from.GetPositionX(); + float toPointY = point.GetPositionY() - from.GetPositionY(); + float proj = toPointX * normX + toPointY * normY; + + if (proj < 0 || proj > pathLen) + return false; + + float closestX = from.GetPositionX() + normX * proj; + float closestY = from.GetPositionY() + normY * proj; + float distToPath = std::sqrt((point.GetPositionX() - closestX) * (point.GetPositionX() - closestX) + + (point.GetPositionY() - closestY) * (point.GetPositionY() - closestY)); + + return distToPath < threshold; +} + +Position IccPutricideAvoidMalleableGooAction::CalculateArcPoint(const Position& current, const Position& target, const Position& center) +{ + // Calculate vectors from center to current position and target + float currentX = current.GetPositionX() - center.GetPositionX(); + float currentY = current.GetPositionY() - center.GetPositionY(); + float targetX = target.GetPositionX() - center.GetPositionX(); + float targetY = target.GetPositionY() - center.GetPositionY(); + + // Calculate distances + float currentDist = std::sqrt(currentX * currentX + currentY * currentY); + float targetDist = std::sqrt(targetX * targetX + targetY * targetY); + + // Normalize vectors + currentX /= currentDist; + currentY /= currentDist; + targetX /= targetDist; + targetY /= targetDist; + + // Calculate dot product to find the angle between vectors + float dotProduct = currentX * targetX + currentY * targetY; + dotProduct = std::max(-1.0f, std::min(1.0f, dotProduct)); // Clamp to [-1, 1] + float angle = std::acos(dotProduct); + + // Determine rotation direction (clockwise or counterclockwise) + float crossProduct = currentX * targetY - currentY * targetX; + float stepAngle = angle * 0.25f; // Move 25% along the arc + + if (crossProduct < 0) + stepAngle = -stepAngle; // Clockwise + + // Calculate rotation matrix components + float cos_a = std::cos(stepAngle); + float sin_a = std::sin(stepAngle); + + // Rotate current vector + float rotatedX = currentX * cos_a - currentY * sin_a; + float rotatedY = currentX * sin_a + currentY * cos_a; + + // Scale to match the target distance for smoother approach + float desiredDist = currentDist * 0.9f + targetDist * 0.1f; + + // Calculate the new position + return Position(center.GetPositionX() + rotatedX * desiredDist, center.GetPositionY() + rotatedY * desiredDist, + current.GetPositionZ()); +} + +Position IccPutricideAvoidMalleableGooAction::CalculateIncrementalMove(const Position& current, const Position& target, + float maxDistance) +{ + float dx = target.GetPositionX() - current.GetPositionX(); + float dy = target.GetPositionY() - current.GetPositionY(); + float distance = std::sqrt(dx * dx + dy * dy); + + if (distance <= maxDistance) + return target; + + dx /= distance; + dy /= distance; + + return Position(current.GetPositionX() + dx * maxDistance, current.GetPositionY() + dy * maxDistance, + target.GetPositionZ()); +} + +// BPC bool IccBpcKelesethTankAction::Execute(Event event) { if (!botAI->IsAssistTank(bot)) return false; + // Handle boss positioning Unit* boss = AI_VALUE2(Unit*, "find target", "prince keleseth"); if (!boss) return false; - // Check if we're not the victim of Keleseth's attack - if (!(boss->GetVictim() == bot)) - return Attack(boss); + bool isBossVictim = boss && boss->GetVictim() == bot; - // First check for any nucleus that needs to be picked up - bool isCollectingNuclei = false; - GuidVector targets = AI_VALUE(GuidVector, "possible targets"); - for (auto i = targets.begin(); i != targets.end(); ++i) + // If not actively tanking, attack the boss + if (boss->GetVictim() != bot) { - Unit* unit = botAI->GetUnit(*i); - if (unit && unit->IsAlive() && unit->GetEntry() == 38369) // Dark Nucleus entry + bot->SetTarget(boss->GetGUID()); + bot->SetFacingToObject(boss); + Attack(boss); + } + + // If tanking boss, check for Dark Nucleus logic - collect any nucleus not targeting us + if (boss->GetVictim() == bot) + { + GuidVector targets = AI_VALUE(GuidVector, "possible targets"); + for (const auto& targetGuid : targets) { - if (!unit->GetVictim() || unit->GetVictim() != bot) + Unit* nucleus = botAI->GetUnit(targetGuid); + if (nucleus && nucleus->IsAlive() && nucleus->GetEntry() == NPC_DARK_NUCLEUS) { - isCollectingNuclei = true; - return Attack(unit); // Pick up any nucleus that isn't targeting us + // Attack nucleus that are NOT targeting us (to collect them) + if (nucleus->GetVictim() != bot) + { + bot->SetTarget(nucleus->GetGUID()); + bot->SetFacingToObject(nucleus); + Attack(nucleus); + // Return early to focus on nucleus collection first + return false; + } } } } - // If not collecting nuclei, move to OT position - if (!isCollectingNuclei && bot->GetExactDist2d(ICC_BPC_OT_POSITION) > 20.0f) - return MoveTo(bot->GetMapId(), ICC_BPC_OT_POSITION.GetPositionX(), - ICC_BPC_OT_POSITION.GetPositionY(), ICC_BPC_OT_POSITION.GetPositionZ(), - false, true, false, true, MovementPriority::MOVEMENT_COMBAT); - - return Attack(boss); -} - -bool IccBpcNucleusAction::Execute(Event event) -{ - if (!botAI->IsAssistTank(bot)) - return false; - - // Actively look for any nucleus that isn't targeting us - GuidVector targets = AI_VALUE(GuidVector, "possible targets"); - for (auto i = targets.begin(); i != targets.end(); ++i) + // Positioning logic - only execute if no nucleus needs collecting + if (botAI->HasAura("Invocation of Blood", boss) && bot->GetExactDist2d(ICC_BPC_MT_POSITION) > 15.0f && isBossVictim) { - Unit* unit = botAI->GetUnit(*i); - if (unit && unit->IsAlive() && unit->GetEntry() == 38369) // Dark Nucleus entry + // Calculate direction vector + float dirX = ICC_BPC_MT_POSITION.GetPositionX() - bot->GetPositionX(); + float dirY = ICC_BPC_MT_POSITION.GetPositionY() - bot->GetPositionY(); + + // Calculate distance and normalize + float length = std::sqrt(dirX * dirX + dirY * dirY); + if (length > 0.001f) { - if (!unit->GetVictim() || unit->GetVictim() != bot) - return Attack(unit); // Pick up any nucleus that isn't targeting us + dirX /= length; + dirY /= length; + + // Calculate movement distance (max 3 yards at a time) + float moveDist = std::min(3.0f, length); + + // Calculate target position + float moveX = bot->GetPositionX() + dirX * moveDist; + float moveY = bot->GetPositionY() + dirY * moveDist; + float moveZ = bot->GetPositionZ(); + + MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT); } } + // Always attack boss when tanking (if no nucleus was prioritized) + if (boss->GetVictim() == bot) + { + bot->SetTarget(boss->GetGUID()); + bot->SetFacingToObject(boss); + Attack(boss); + } + return false; } bool IccBpcMainTankAction::Execute(Event event) { + // Main tank specific behavior (higher priority) if (botAI->IsMainTank(bot)) { - // Move to MT position if we're not there - if (bot->GetExactDist2d(ICC_BPC_MT_POSITION) > 20.0f) - return MoveTo(bot->GetMapId(), ICC_BPC_MT_POSITION.GetPositionX(), - ICC_BPC_MT_POSITION.GetPositionY(), ICC_BPC_MT_POSITION.GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + // Get target princes + auto* valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); + auto* taldaram = AI_VALUE2(Unit*, "find target", "prince taldaram"); - Unit* valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); - Unit* taldaram = AI_VALUE2(Unit*, "find target", "prince taldaram"); + // Check if we're the target of both princes + const bool isVictimOfValanar = valanar && valanar->GetVictim() == bot; + const bool isVictimOfTaldaram = taldaram && taldaram->GetVictim() == bot; - // Attack any prince that's not targeting us - if (valanar && valanar->IsAlive() && (!valanar->GetVictim() || valanar->GetVictim() != bot)) - return Attack(valanar); - if (taldaram && taldaram->IsAlive() && (!taldaram->GetVictim() || taldaram->GetVictim() != bot)) - return Attack(taldaram); - - // If both princes are targeting us or dead, maintain current target - Unit* currentTarget = AI_VALUE(Unit*, "current target"); - if (currentTarget && currentTarget->IsAlive() && - (currentTarget == valanar || currentTarget == taldaram)) - return Attack(currentTarget); - - return false; - } - - if (!botAI->IsTank(bot)) - { - Unit* currentTarget = AI_VALUE(Unit*, "current target"); - GuidVector targets = AI_VALUE(GuidVector, "possible targets"); - - // If no valid skull target, search for empowered prince - Unit* empoweredPrince = nullptr; - for (auto i = targets.begin(); i != targets.end(); ++i) + // Move to MT position if targeted by both princes and not already close + if (isVictimOfValanar && isVictimOfTaldaram && bot->GetExactDist2d(ICC_BPC_MT_POSITION) > 15.0f) { - Unit* unit = botAI->GetUnit(*i); - if (!unit || !unit->IsAlive()) - continue; + // Calculate direction vector + float dirX = ICC_BPC_MT_POSITION.GetPositionX() - bot->GetPositionX(); + float dirY = ICC_BPC_MT_POSITION.GetPositionY() - bot->GetPositionY(); - if (unit->HasAura(71596)) + // Calculate distance and normalize + float length = std::sqrt(dirX * dirX + dirY * dirY); + if (length > 0.001f) { - if (unit->GetEntry() == 37972 || // Keleseth - unit->GetEntry() == 37973 || // Taldaram - unit->GetEntry() == 37970) // Valanar - { - empoweredPrince = unit; + dirX /= length; + dirY /= length; - // Mark empowered prince with skull if in group and not already marked - if (Group* group = bot->GetGroup()) - { - ObjectGuid currentSkullGuid = group->GetTargetIcon(7); - if (currentSkullGuid.IsEmpty() || currentSkullGuid != unit->GetGUID()) - { - group->SetTargetIcon(7, bot->GetGUID(), unit->GetGUID()); // 7 = skull - } - } - break; - } + // Calculate movement distance (max 3 yards at a time) + float moveDist = std::min(3.0f, length); + + // Calculate target position + float moveX = bot->GetPositionX() + dirX * moveDist; + float moveY = bot->GetPositionY() + dirY * moveDist; + float moveZ = bot->GetPositionZ(); + + MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); + } + } + + // Attack prince that's not targeting us + if (valanar && !isVictimOfValanar) + { + bot->SetTarget(valanar->GetGUID()); + bot->SetFacingToObject(valanar); + Attack(valanar); + } + + if (taldaram && !isVictimOfTaldaram) + { + bot->SetTarget(taldaram->GetGUID()); + bot->SetFacingToObject(taldaram); + Attack(taldaram); + } + } + + // Target marking for all tanks, called after main tank priority actions + if (botAI->IsTank(bot)) + MarkEmpoweredPrince(); + + return false; +} + +void IccBpcMainTankAction::MarkEmpoweredPrince() +{ + static constexpr uint8_t SKULL_RAID_ICON = 7; + + // Find empowered prince (Invocation of Blood) + Unit* empoweredPrince = nullptr; + const GuidVector& targets = AI_VALUE(GuidVector, "possible targets"); + + for (const auto& targetGuid : targets) + { + Unit* unit = botAI->GetUnit(targetGuid); + if (!unit || !unit->IsAlive()) + continue; + + if (botAI->HasAura("Invocation of Blood", unit)) + { + const uint32 entry = unit->GetEntry(); + if (entry == NPC_PRINCE_KELESETH || entry == NPC_PRINCE_VALANAR || entry == NPC_PRINCE_TALDARAM) + { + empoweredPrince = unit; + break; + } + } + } + + // Handle marking if we found an empowered prince + if (empoweredPrince && empoweredPrince->IsAlive()) + { + Group* group = bot->GetGroup(); + if (group) + { + const ObjectGuid currentSkullGuid = group->GetTargetIcon(SKULL_RAID_ICON); + Unit* markedUnit = botAI->GetUnit(currentSkullGuid); + + // Clear dead marks or marks that are not on empowered prince + if (markedUnit && (!markedUnit->IsAlive() || markedUnit != empoweredPrince)) + { + group->SetTargetIcon(SKULL_RAID_ICON, bot->GetGUID(), ObjectGuid::Empty); + } + + // Mark alive empowered prince if needed + if (!currentSkullGuid || !markedUnit) + { + group->SetTargetIcon(SKULL_RAID_ICON, bot->GetGUID(), empoweredPrince->GetGUID()); } } } - return false; } bool IccBpcEmpoweredVortexAction::Execute(Event event) { Unit* valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); - if (!valanar || !valanar->HasUnitState(UNIT_STATE_CASTING)) + if (!valanar) return false; - float const MIN_SPREAD = 12.0f; - float const MOVE_INCREMENT = 10.0f; - - // Use MT position as reference point to move away from - Position const* mtPos = &ICC_BPC_MT_POSITION; - float centerX = mtPos->GetPositionX(); - float centerY = mtPos->GetPositionY(); - float centerZ = mtPos->GetPositionZ(); + // Check if boss is casting empowered vortex + bool isCastingVortex = valanar->HasUnitState(UNIT_STATE_CASTING) && + (valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX1) || + valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX2) || + valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX3) || + valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX4)); - Group* group = bot->GetGroup(); - if (!group) - return false; - - // Get all alive group members and sort by GUID for consistent movement directions - std::vector> sortedMembers; - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + if (isCastingVortex) { - Player* member = itr->GetSource(); - if (member && member->IsAlive() && !botAI->IsTank(member)) - { - sortedMembers.push_back(std::make_pair(member->GetGUID(), member)); - } + // Use complex positioning system for empowered vortex + return HandleEmpoweredVortexSpread(); } - std::sort(sortedMembers.begin(), sortedMembers.end()); - - // Find this bot's index to determine movement direction - int botIndex = -1; - for (size_t i = 0; i < sortedMembers.size(); ++i) - { - if (sortedMembers[i].first == bot->GetGUID()) - { - botIndex = i; - break; - } - } - - if (botIndex == -1) - return false; - - // Calculate base angle based on bot index (split into 12 directions) - float baseAngle = botIndex * (2.0f * M_PI / 12.0f); - - // Calculate current distance from MT position - float currentDist = bot->GetDistance2d(centerX, centerY); - - // If too close to others, move further out - bool needToMove = false; - if (currentDist < MIN_SPREAD) - needToMove = true; else { - // Check distance to other players - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || member == bot) - continue; - - if (bot->GetDistance2d(member) < MIN_SPREAD) - { - needToMove = true; - break; - } - } + // Use simple ranged spreading for non-vortex situations + return MaintainRangedSpacing(); } +} - if (!needToMove) +bool IccBpcEmpoweredVortexAction::MaintainRangedSpacing() +{ + const float safeSpacingRadius = 7.0f; + const float moveIncrement = 2.0f; + const float maxMoveDistance = 5.0f; + const bool isRanged = botAI->IsRanged(bot) || botAI->IsHeal(bot); + + if (!isRanged) return false; - // Calculate new position further out in our assigned direction - float moveDistance = std::max(MOVE_INCREMENT, currentDist + MOVE_INCREMENT); - float targetX = centerX + cos(baseAngle) * moveDistance; - float targetY = centerY + sin(baseAngle) * moveDistance; - float targetZ = centerZ; + // Get group members + const GuidVector members = AI_VALUE(GuidVector, "group members"); - // Update Z coordinate and check LOS - bot->UpdateAllowedPositionZ(targetX, targetY, targetZ); - if (!bot->IsWithinLOS(targetX, targetY, targetZ)) + // Calculate a combined vector representing all nearby members' positions + float totalX = 0.0f; + float totalY = 0.0f; + int nearbyCount = 0; + + for (const auto& memberGuid : members) { - // Try adjusting angle if LOS fails - for (float angleAdjust = -M_PI/6; angleAdjust <= M_PI/6; angleAdjust += M_PI/12) + Unit* member = botAI->GetUnit(memberGuid); + if (!member || !member->IsAlive() || member == bot) { - if (angleAdjust == 0) - continue; + continue; + } - float newX = centerX + cos(baseAngle + angleAdjust) * moveDistance; - float newY = centerY + sin(baseAngle + angleAdjust) * moveDistance; - float newZ = centerZ; - - bot->UpdateAllowedPositionZ(newX, newY, newZ); - if (bot->IsWithinLOS(newX, newY, newZ)) + const float distance = bot->GetExactDist2d(member); + if (distance < safeSpacingRadius) + { + // Calculate vector from member to bot + float dx = bot->GetPositionX() - member->GetPositionX(); + float dy = bot->GetPositionY() - member->GetPositionY(); + + // Weight by inverse distance (closer members have more influence) + float weight = (safeSpacingRadius - distance) / safeSpacingRadius; + totalX += dx * weight; + totalY += dy * weight; + nearbyCount++; + } + } + + // If we have nearby members, move away in the combined direction + if (nearbyCount > 0) + { + // Normalize the combined vector + float magnitude = std::sqrt(totalX * totalX + totalY * totalY); + if (magnitude > 0.001f) // Avoid division by zero + { + totalX /= magnitude; + totalY /= magnitude; + + // Calculate move distance + float moveDistance = std::min(moveIncrement, maxMoveDistance); + + // Create target position in the combined direction + float targetX = bot->GetPositionX() + totalX * moveDistance; + float targetY = bot->GetPositionY() + totalY * moveDistance; + float targetZ = bot->GetPositionZ(); + + // Check if the target position is valid and move there + if (bot->IsWithinLOS(targetX, targetY, targetZ)) { - targetX = newX; - targetY = newY; - targetZ = newZ; - break; + MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, + MovementPriority::MOVEMENT_NORMAL); + } + else + { + // If LOS check fails, try shorter distance + targetX = bot->GetPositionX() + totalX * (moveDistance * 0.5f); + targetY = bot->GetPositionY() + totalY * (moveDistance * 0.5f); + MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, + MovementPriority::MOVEMENT_NORMAL); } } } - return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, - false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + return false; // Everyone is properly spaced +} + +bool IccBpcEmpoweredVortexAction::HandleEmpoweredVortexSpread() +{ + const float safeSpacingRadius = 13.0f; + const float moveIncrement = 2.0f; + const float maxMoveDistance = 5.0f; + const bool isTank = botAI->IsTank(bot); + + if (isTank) + return false; + + // Get group members + const GuidVector members = AI_VALUE(GuidVector, "group members"); + + // Calculate a combined vector representing all nearby members' positions + float totalX = 0.0f; + float totalY = 0.0f; + int nearbyCount = 0; + + for (const auto& memberGuid : members) + { + Unit* member = botAI->GetUnit(memberGuid); + if (!member || !member->IsAlive() || member == bot) + { + continue; + } + + const float distance = bot->GetExactDist2d(member); + if (distance < safeSpacingRadius) + { + // Calculate vector from member to bot + float dx = bot->GetPositionX() - member->GetPositionX(); + float dy = bot->GetPositionY() - member->GetPositionY(); + + // Weight by inverse distance (closer members have more influence) + float weight = (safeSpacingRadius - distance) / safeSpacingRadius; + totalX += dx * weight; + totalY += dy * weight; + nearbyCount++; + } + } + + // If we have nearby members, move away in the combined direction + if (nearbyCount > 0) + { + // Normalize the combined vector + float magnitude = std::sqrt(totalX * totalX + totalY * totalY); + if (magnitude > 0.001f) // Avoid division by zero + { + totalX /= magnitude; + totalY /= magnitude; + + // Calculate move distance + float moveDistance = std::min(moveIncrement, maxMoveDistance); + + // Create target position in the combined direction + float targetX = bot->GetPositionX() + totalX * moveDistance; + float targetY = bot->GetPositionY() + totalY * moveDistance; + float targetZ = bot->GetPositionZ(); + + // Check if the target position is valid and move there + if (bot->IsWithinLOS(targetX, targetY, targetZ)) + { + MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, + MovementPriority::MOVEMENT_NORMAL); + } + else + { + // If LOS check fails, try shorter distance + targetX = bot->GetPositionX() + totalX * (moveDistance * 0.5f); + targetY = bot->GetPositionY() + totalY * (moveDistance * 0.5f); + MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, + MovementPriority::MOVEMENT_NORMAL); + } + } + } + + return false; // Everyone is properly spaced } bool IccBpcKineticBombAction::Execute(Event event) @@ -1964,339 +3303,1092 @@ bool IccBpcKineticBombAction::Execute(Event event) if (!botAI->IsRangedDps(bot)) return false; - // Static constants to avoid recreating them every call - static const float MAX_HEIGHT_DIFF = 25.0f; - static const float SAFE_HEIGHT = 371.16473f; - static const float TELEPORT_HEIGHT = 366.16473f; + // Static constants + static constexpr float MAX_HEIGHT_DIFF = 20.0f; + static constexpr float SAFE_HEIGHT = 371.16473f; + static constexpr float TELEPORT_HEIGHT = 366.16473f; + static constexpr std::array KINETIC_BOMB_ENTRIES = {NPC_KINETIC_BOMB1, NPC_KINETIC_BOMB2, + NPC_KINETIC_BOMB3, NPC_KINETIC_BOMB4}; - // Handle the edge case where bot is too high (prevent teleport to entrance) + // Handle edge case where bot is too high if (bot->GetPositionZ() > SAFE_HEIGHT) - return bot->TeleportTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), TELEPORT_HEIGHT, - bot->GetOrientation()); - - // Cache bot position once - float botZ = bot->GetPositionZ(); - - // Check if we're already handling a valid bomb - Unit* currentTarget = AI_VALUE(Unit*, "current target"); - if (currentTarget && currentTarget->IsAlive() && currentTarget->GetName() == "Kinetic Bomb") { - float heightDiff = currentTarget->GetPositionZ() - botZ; - if (heightDiff < MAX_HEIGHT_DIFF) - return false; // Continue current attack + bot->TeleportTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), TELEPORT_HEIGHT, + bot->GetOrientation()); } - // Get possible targets once - GuidVector targets = AI_VALUE(GuidVector, "possible targets"); - if (targets.empty()) - return false; - - // Cache group only once - Group* group = bot->GetGroup(); - - // Find the lowest reachable bomb - Unit* bestBomb = nullptr; - float lowestHeightDiff = MAX_HEIGHT_DIFF; - - for (auto& guid : targets) + // Check current target if valid + if (Unit* currentTarget = AI_VALUE(Unit*, "current target")) { - Unit* unit = botAI->GetUnit(guid); - if (!unit || !unit->IsAlive() || unit->GetName() != "Kinetic Bomb") - continue; - - float heightDiff = unit->GetPositionZ() - botZ; - if (heightDiff >= lowestHeightDiff) - continue; - - // Skip if bot is too far to realistically hit this bomb - if (bot->GetDistance(unit) > 30.0f) - continue; - - // Check if any closer ranged DPS is already handling this bomb - bool alreadyHandled = false; - - if (group) + if (currentTarget->IsAlive() && std::find(KINETIC_BOMB_ENTRIES.begin(), KINETIC_BOMB_ENTRIES.end(), + currentTarget->GetEntry()) != KINETIC_BOMB_ENTRIES.end()) { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member || member == bot || !member->IsAlive() || !botAI->IsRangedDps(member)) - continue; - - if (member->GetTarget() == unit->GetGUID() && member->GetDistance(unit) < bot->GetDistance(unit)) - { - alreadyHandled = true; - break; - } - } - } - - if (!alreadyHandled) - { - bestBomb = unit; - lowestHeightDiff = heightDiff; + const float heightDiff = currentTarget->GetPositionZ() - 361.18222f; + if (heightDiff < MAX_HEIGHT_DIFF) + return false; // Continue current attack } } - // Attack the best bomb if found - if (bestBomb) - return Attack(bestBomb); + // Find the best kinetic bomb to attack + if (Unit* bestBomb = FindOptimalKineticBomb()) + { + bot->SetTarget(bestBomb->GetGUID()); + bot->SetFacingToObject(bestBomb); + Attack(bestBomb); + } return false; } -//BQL - -bool IccBqlTankPositionAction::Execute(Event event) +Unit* IccBpcKineticBombAction::FindOptimalKineticBomb() { - Unit* boss = AI_VALUE2(Unit*, "find target", "blood-queen lana'thel"); - Aura* aura = botAI->GetAura("Frenzied Bloodthirst", bot); - Aura* aura2 = botAI->GetAura("Swarming Shadows", bot); + static constexpr float MAX_HEIGHT_DIFF = 20.0f; + static constexpr std::array KINETIC_BOMB_ENTRIES = {NPC_KINETIC_BOMB1, NPC_KINETIC_BOMB2, + NPC_KINETIC_BOMB3, NPC_KINETIC_BOMB4}; + const GuidVector targets = AI_VALUE(GuidVector, "possible targets"); + if (targets.empty()) + return nullptr; + + const float botZ = 361.18222f; + Group* group = bot->GetGroup(); + + // Gather all valid kinetic bombs + std::vector kineticBombs; + for (const auto& guid : targets) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + if (std::find(KINETIC_BOMB_ENTRIES.begin(), KINETIC_BOMB_ENTRIES.end(), unit->GetEntry()) == + KINETIC_BOMB_ENTRIES.end()) + continue; + kineticBombs.push_back(unit); + } + + if (kineticBombs.empty()) + return nullptr; + + // Sort bombs by Z ascending (lowest first), then by heightDiff ascending (closest to ground) + std::sort(kineticBombs.begin(), kineticBombs.end(), + [botZ](Unit* a, Unit* b) + { + if (a->GetPositionZ() != b->GetPositionZ()) + return a->GetPositionZ() < b->GetPositionZ(); + return std::abs(a->GetPositionZ() - botZ) < std::abs(b->GetPositionZ() - botZ); + }); + + // Assign each ranged DPS to a unique bomb (lowest Z first) + std::vector rangedDps; + if (group) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && member->IsAlive() && botAI->IsRangedDps(member)) + rangedDps.push_back(member); + } + // Sort by GUID for deterministic assignment + std::sort(rangedDps.begin(), rangedDps.end(), [](Player* a, Player* b) { return a->GetGUID() < b->GetGUID(); }); + } + else + { + rangedDps.push_back(bot); + } + + // Find this bot's index among ranged DPS + auto it = std::find(rangedDps.begin(), rangedDps.end(), bot); + if (it == rangedDps.end()) + return nullptr; + size_t botIndex = std::distance(rangedDps.begin(), it); + + // Assign bombs in order, skip bombs already handled by other ranged DPS + size_t bombCount = kineticBombs.size(); + for (size_t i = 0, assigned = 0; i < bombCount; ++i) + { + Unit* bomb = kineticBombs[i]; + // Check if bomb is already handled by another ranged DPS closer than this bot + if (IsBombAlreadyHandled(bomb, group)) + continue; + if (assigned == botIndex) + return bomb; + ++assigned; + } + + // Fallback: pick the lowest bomb not already handled + for (Unit* bomb : kineticBombs) + { + if (!IsBombAlreadyHandled(bomb, group)) + return bomb; + } + + return nullptr; +} + +bool IccBpcKineticBombAction::IsBombAlreadyHandled(Unit* bomb, Group* group) +{ + if (!group) + return false; + + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || member == bot || !member->IsAlive() || !botAI->IsRangedDps(member)) + continue; + + if (member->GetTarget() == bomb->GetGUID() && member->GetDistance(bomb) < bot->GetDistance(bomb)) + return true; + } + + return false; +} + + +bool IccBpcBallOfFlameAction::Execute(Event event) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "prince taldaram"); if (!boss) return false; - // If tank is not at position, move there - if ((botAI->IsTank(bot) || botAI->IsMainTank(bot) || botAI->IsAssistTank(bot)) && !(aura || aura2)) - { - if (bot->GetExactDist2d(ICC_BQL_TANK_POSITION) > 10.0f) - return MoveTo(bot->GetMapId(), ICC_BQL_TANK_POSITION.GetPositionX(), - ICC_BQL_TANK_POSITION.GetPositionY(), ICC_BQL_TANK_POSITION.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED); - } + Unit* flame1 = bot->FindNearestCreature(NPC_BALL_OF_FLAME, 100.0f); + Unit* flame2 = bot->FindNearestCreature(NPC_BALL_OF_INFERNO_FLAME, 100.0f); - // If assist tank and no blood mirror, move to extact postion of main tank - if (botAI->IsAssistTank(bot) && !botAI->GetAura("Blood Mirror", bot) && !(aura || aura2)) - { - Unit* mainTank = AI_VALUE(Unit*, "main tank"); - if (!mainTank) - return false; + bool ballOfFlame = flame1 && (flame1->GetVictim() == bot); + bool infernoFlame = flame2 && (flame2->GetVictim() == bot); - return MoveTo(bot->GetMapId(), mainTank->GetPositionX(), mainTank->GetPositionY(), mainTank->GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED); - } - - float radius = 8.0f; - float moveIncrement = 3.0f; - bool isRanged = botAI->IsRanged(bot); - bool isMelee = botAI->IsMelee(bot); - - //if bot has Swarming Shadows, move to the wall - if (aura2) - { - // Get current position and map - float currentX = bot->GetPositionX(); - float currentY = bot->GetPositionY(); - float currentZ = bot->GetPositionZ(); - Map* map = bot->GetMap(); - - float bestDist = 100.0f; - float bestX = currentX; - float bestY = currentY; - bool foundWall = false; - - // Check only east (0) and west (π) directions for walls - float angles[2] = {M_PI_2, -M_PI_2}; // East = π/2, West = -π/2 - for (float angle : angles) + if (flame2 && (flame2->GetDistance2d(boss) > 2.0f) && !(flame2->GetDistance2d(boss) > 10.0f) && !infernoFlame && + bot->getClass() != CLASS_HUNTER) { - float dx = cos(angle); - float dy = sin(angle); - - // Binary search to find the wall - float minDist = 5.0f; - float maxDist = 100.0f; - float wallDist = maxDist; - - for (int i = 0; i < 8; i++) + if (!botAI->IsTank(bot) && !(flame2->GetVictim() == bot)) { - float testDist = (minDist + maxDist) / 2; - float testX = currentX + dx * testDist; - float testY = currentY + dy * testDist; - float testZ = currentZ; - - bool heightFound = map->GetHeight(testX, testY, testZ); - if (!heightFound) - testZ = currentZ; - - bool hasLos = map->isInLineOfSight(currentX, currentY, currentZ + 2.0f, - testX, testY, testZ + 2.0f, - bot->GetPhaseMask(), LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::Nothing); - - if (hasLos) + float targetX = flame2->GetPositionX(); + float targetY = flame2->GetPositionY(); + float currentX = bot->GetPositionX(); + float currentY = bot->GetPositionY(); + + // Calculate direction vector + float dx = targetX - currentX; + float dy = targetY - currentY; + float distance = sqrt(dx * dx + dy * dy); + + // Normalize and scale to 5 units (or remaining distance if less than 5) + float step = std::min(5.0f, distance); + if (distance > 0.1) { - minDist = testDist; + dx = dx / distance * step; + dy = dy / distance * step; + } + + // Calculate intermediate position + float newX = currentX + dx; + float newY = currentY + dy; + + MoveTo(flame2->GetMapId(), newX, newY, bot->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + } + } + + // If victim of ball of flame, keep at least 15f from other party members + if (ballOfFlame || infernoFlame) + { + const float SAFE_DIST = 15.0f; + GuidVector members = AI_VALUE(GuidVector, "group members"); + for (const auto& memberGuid : members) + { + Unit* member = botAI->GetUnit(memberGuid); + if (!member || !member->IsAlive() || member == bot) + continue; + float dist = bot->GetExactDist2d(member); + if (dist < SAFE_DIST) + { + // Move away from this member + float dx = bot->GetPositionX() - member->GetPositionX(); + float dy = bot->GetPositionY() - member->GetPositionY(); + float len = std::sqrt(dx * dx + dy * dy); + if (len < 0.01f) + continue; + dx /= len; + dy /= len; + float moveX = bot->GetPositionX() + dx * (SAFE_DIST - dist + 1.0f); + float moveY = bot->GetPositionY() + dy * (SAFE_DIST - dist + 1.0f); + float moveZ = bot->GetPositionZ(); + if (bot->IsWithinLOS(moveX, moveY, moveZ)) + { + MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, true, + MovementPriority::MOVEMENT_FORCED); + } + } + } + } + + return false; +} + +// Blood Queen Lana'thel +bool IccBqlGroupPositionAction::Execute(Event event) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "blood-queen lana'thel"); + if (!boss) + return false; + + Aura* frenzyAura = botAI->GetAura("Frenzied Bloodthirst", bot); + Aura* shadowAura = botAI->GetAura("Swarming Shadows", bot); + bool isTank = botAI->IsTank(bot); + // Handle tank positioning + if (isTank && HandleTankPosition(boss, frenzyAura, shadowAura)) + return true; + + // Handle swarming shadows movement + if (shadowAura && HandleShadowsMovement()) + return true; + + // Handle group positioning + if (!frenzyAura && !shadowAura && HandleGroupPosition(boss, frenzyAura, shadowAura)) + return true; + + return false; +} + +bool IccBqlGroupPositionAction::HandleTankPosition(Unit* boss, Aura* frenzyAura, Aura* shadowAura) +{ + if (frenzyAura || shadowAura) + return false; + + // Main tank positioning + if (botAI->IsMainTank(bot) && botAI->HasAggro(boss)) + { + if (bot->GetExactDist2d(ICC_BQL_TANK_POSITION) > 3.0f) + { + MoveTo(bot->GetMapId(), ICC_BQL_TANK_POSITION.GetPositionX(), ICC_BQL_TANK_POSITION.GetPositionY(), + ICC_BQL_TANK_POSITION.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + } + } + + // Assist tank positioning + if (botAI->IsAssistTank(bot) && !botAI->GetAura("Blood Mirror", bot)) + { + if (Unit* mainTank = AI_VALUE(Unit*, "main tank")) + { + MoveTo(bot->GetMapId(), mainTank->GetPositionX(), mainTank->GetPositionY(), mainTank->GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + } + } + + return false; +} + +bool IccBqlGroupPositionAction::HandleShadowsMovement() +{ + const float SAFE_SHADOW_DIST = 4.0f; + const float ARC_STEP = 0.05f; + const float CURVE_SPACING = 15.0f; + const int MAX_CURVES = 3; + const float maxClosestDist = botAI->IsMelee(bot) ? 25.0f : 20.0f; + const Position& center = ICC_BQL_CENTER_POSITION; + const float OUTER_CURVE_PREFERENCE = 200.0f; // Strong preference for outer curves + const float CURVE_SWITCH_PENALTY = 50.0f; // Penalty for switching curves + const float DISTANCE_PENALTY_FACTOR = 100.0f; // Penalty per yard moved from current position + const float MAX_CURVE_JUMP_DIST = 5.0f; // Maximum distance for jumping between curves + + // Track current curve to avoid unnecessary switching + static std::map botCurrentCurve; + int currentCurve = botCurrentCurve.count(bot->GetGUID()) ? botCurrentCurve[bot->GetGUID()] : 0; + + // Find closest wall path + Position lwall[4] = {ICC_BQL_LWALL1_POSITION, AdjustControlPoint(ICC_BQL_LWALL2_POSITION, center, 1.30f), + AdjustControlPoint(ICC_BQL_LWALL3_POSITION, center, 1.30f), ICC_BQL_LRWALL4_POSITION}; + Position rwall[4] = {ICC_BQL_RWALL1_POSITION, AdjustControlPoint(ICC_BQL_RWALL2_POSITION, center, 1.30f), + AdjustControlPoint(ICC_BQL_RWALL3_POSITION, center, 1.30f), ICC_BQL_LRWALL4_POSITION}; + Position* basePath = (bot->GetExactDist2d(lwall[0]) < bot->GetExactDist2d(rwall[0])) ? lwall : rwall; + + // Find all swarming shadows + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + Unit* shadows[100]{}; // Reasonable max estimate + int shadowCount = 0; + for (int i = 0; i < npcs.size() && shadowCount < 100; i++) + { + Unit* unit = botAI->GetUnit(npcs[i]); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_SWARMING_SHADOWS) + shadows[shadowCount++] = unit; + } + + // Helper lambda to check if a position is inside a shadow + auto IsPositionInShadow = [&](const Position& pos) -> bool + { + for (int i = 0; i < shadowCount; ++i) + { + if (pos.GetExactDist2d(shadows[i]) < SAFE_SHADOW_DIST) + return true; + } + return false; + }; + + // If bot is at the 4th position (end of the wall), move towards 3rd position or center to avoid getting stuck + float distToL4 = bot->GetExactDist2d(lwall[3]); + float distToR4 = bot->GetExactDist2d(rwall[3]); + const float STUCK_DIST = 2.0f; // within 2 yards is considered stuck at the end + + if (distToL4 < STUCK_DIST || distToR4 < STUCK_DIST) + { + // Move towards 3rd position of the same wall, or towards center if blocked + Position target; + if (distToL4 < distToR4) + { + target = lwall[2]; + } + else + { + target = rwall[2]; + } + + float tx = target.GetPositionX(); + float ty = target.GetPositionY(); + float tz = target.GetPositionZ(); + bot->UpdateAllowedPositionZ(tx, ty, tz); + if (!bot->IsWithinLOS(tx, ty, tz) || IsPositionInShadow(Position(tx, ty, tz))) + { + tx = center.GetPositionX(); + ty = center.GetPositionY(); + tz = center.GetPositionZ(); + } + + if (bot->GetExactDist2d(tx, ty) > 1.0f) + { + MoveTo(bot->GetMapId(), tx, ty, tz, false, false, false, true, MovementPriority::MOVEMENT_FORCED, + true, false); + } + return false; + } + + CurveInfo bestCurve; + bestCurve.foundSafe = false; + bestCurve.score = FLT_MAX; + bool foundCurve = false; + + // Keep track of information about all curves for possible fallback + CurveInfo curveInfos[MAX_CURVES]; + for (int i = 0; i < MAX_CURVES; i++) + { + curveInfos[i].foundSafe = false; + curveInfos[i].score = FLT_MAX; + } + + // Evaluate all curves starting from outermost (lowest index) + for (int curveIdx = 0; curveIdx < MAX_CURVES; curveIdx++) + { + float curveShrink = float(curveIdx) * CURVE_SPACING; + float shrinkFactor = 1.30f - (curveShrink / 30.0f); + if (shrinkFactor < 1.0f) + shrinkFactor = 1.0f; + + Position path[4] = {basePath[0], AdjustControlPoint(basePath[1], center, shrinkFactor / 1.30f), + AdjustControlPoint(basePath[2], center, shrinkFactor / 1.30f), basePath[3]}; + + // Find closest point on curve + float minDist = 9999.0f; + float t_closest = 0.0f; + Position closestPoint = path[0]; + + for (float t = 0.0f; t <= 1.0f; t += ARC_STEP) + { + Position pt = CalculateBezierPoint(t, path); + float dist = bot->GetExactDist2d(pt); + if (dist < minDist) + { + minDist = dist; + t_closest = t; + closestPoint = pt; + } + } + + // Check if the closest point is safe + bool closestIsSafe = !IsPositionInShadow(closestPoint); + + // Find closest safe point by searching in both directions from closest point + Position safeMoveTarget = closestPoint; + float safeMoveTargetDist = FLT_MAX; + bool foundSafe = closestIsSafe; + + // Only search for safe spots if the closest point isn't already safe + if (!closestIsSafe) + { + // Find the nearest safe point along the curve, not by direct distance + // but by distance along the curve from the closest point + + // Search forward on curve from closest point + float forwardT = -1.0f; + Position forwardPt; + for (float t = t_closest + ARC_STEP; t <= 1.0f; t += ARC_STEP) + { + Position pt = CalculateBezierPoint(t, path); + if (!IsPositionInShadow(pt)) + { + forwardT = t; + forwardPt = pt; + break; + } + } + + // Search backward on curve from closest point + float backwardT = -1.0f; + Position backwardPt; + for (float t = t_closest - ARC_STEP; t >= 0.0f; t -= ARC_STEP) + { + Position pt = CalculateBezierPoint(t, path); + if (!IsPositionInShadow(pt)) + { + backwardT = t; + backwardPt = pt; + break; + } + } + + // Choose the closest safe point based on curve distance, not direct distance + if (forwardT >= 0 && backwardT >= 0) + { + // Both directions have safe points, choose the closer one by curve distance + if (std::abs(forwardT - t_closest) < std::abs(backwardT - t_closest)) + { + safeMoveTarget = forwardPt; + foundSafe = true; } else { - maxDist = testDist; - wallDist = testDist; - foundWall = true; + safeMoveTarget = backwardPt; + foundSafe = true; } } - - if (foundWall && wallDist < bestDist) + else if (forwardT >= 0) { - bestDist = wallDist; - bestX = currentX + dx * (wallDist - 2.0f); // Stay 2 yards from wall - bestY = currentY + dy * (wallDist - 2.0f); + safeMoveTarget = forwardPt; + foundSafe = true; + } + else if (backwardT >= 0) + { + safeMoveTarget = backwardPt; + foundSafe = true; } } - // Only move if we're too far from the wall - if (foundWall && bestDist > 10.0f) - { - // Verify we still have the aura before moving - if (!botAI->GetAura("Swarming Shadows", bot)) - return false; + // Score this curve + float distancePenalty = 0.0f; + float score = 0.0f; - return MoveTo(bot->GetMapId(), bestX, bestY, bot->GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED); + if (foundSafe) + { + // If we found a safe point, penalize based on travel distance along the curve to reach it + float stepsToCurve = minDist / 2.0f; // Approximate steps to reach the curve + float safeDist = bot->GetExactDist2d(safeMoveTarget); + + // Add distance penalty based on how far we need to move along the curve + distancePenalty = safeDist * (1.0f / DISTANCE_PENALTY_FACTOR); + score = safeDist + distancePenalty; + } + else + { + // No safe point found, assign a high score + distancePenalty = minDist * (1.0f / DISTANCE_PENALTY_FACTOR); + score = minDist + distancePenalty + 1000.0f; // Penalty for unsafe position + } + + // Apply strong penalty for curves that are too far + if (minDist > maxClosestDist) + score += 500.0f; + + // Apply penalty for unsafe curves + if (!foundSafe) + score += 1000.0f; + + // Apply curve index preference (strongly prefer outer curves) + score += curveIdx * OUTER_CURVE_PREFERENCE; + + // Apply curve switching penalty + if (curveIdx != currentCurve && currentCurve != 0) + score += CURVE_SWITCH_PENALTY; + + // MORE IMPORTANT: Apply additional curve switching penalty if the bot is far away + // from the target curve (prevent jumping between curves when far away) + if (curveIdx != currentCurve && minDist > MAX_CURVE_JUMP_DIST) + score += 2000.0f; // Strong penalty to prevent jumping between curves + + // Store this curve's info + curveInfos[curveIdx].moveTarget = foundSafe ? safeMoveTarget : closestPoint; + curveInfos[curveIdx].foundSafe = foundSafe; + curveInfos[curveIdx].minDist = minDist; + curveInfos[curveIdx].curveIdx = curveIdx; + curveInfos[curveIdx].score = score; + curveInfos[curveIdx].closestPoint = closestPoint; + curveInfos[curveIdx].t_closest = t_closest; + + // Only update if this curve is better than our current best + if (!foundCurve || score < bestCurve.score) + { + bestCurve = curveInfos[curveIdx]; + foundCurve = true; } } + // Fallback: If we're trying to switch to a far curve and we're not near any curve, + // find and use the closest curve instead of making a direct beeline + if (foundCurve && bestCurve.minDist > MAX_CURVE_JUMP_DIST && bestCurve.curveIdx != currentCurve) + { + // Look for the closest curve first + float closestDist = FLT_MAX; + int closestCurveIdx = -1; + + for (int i = 0; i < MAX_CURVES; i++) + { + if (curveInfos[i].minDist < closestDist) + { + closestDist = curveInfos[i].minDist; + closestCurveIdx = i; + } + } + + // If we found a closer curve, use that instead + if (closestCurveIdx >= 0 && closestCurveIdx != bestCurve.curveIdx) + { + bestCurve = curveInfos[closestCurveIdx]; + } + } + + // Remember the selected curve for next time + if (foundCurve) + { + botCurrentCurve[bot->GetGUID()] = bestCurve.curveIdx; + } + + // Create a move plan to guide the bot along the curve if necessary + if (foundCurve && bot->GetExactDist2d(bestCurve.moveTarget) > 1.0f) + { + // Final check: ensure we're not moving into a shadow + if (!IsPositionInShadow(bestCurve.moveTarget)) + { + // Get the curve + float curveShrink = float(bestCurve.curveIdx) * CURVE_SPACING; + float shrinkFactor = 1.30f - (curveShrink / 30.0f); + if (shrinkFactor < 1.0f) + shrinkFactor = 1.0f; + + Position path[4] = {basePath[0], AdjustControlPoint(basePath[1], center, shrinkFactor / 1.30f), + AdjustControlPoint(basePath[2], center, shrinkFactor / 1.30f), basePath[3]}; + + // CRITICAL CHANGE: First check if we need to move to the curve + float distToClosestPoint = bot->GetExactDist2d(bestCurve.closestPoint); + + // If we're not on the curve yet, first move to the closest point on the curve + if (distToClosestPoint > 2.0f) + { + botAI->Reset(); + return MoveTo(bot->GetMapId(), bestCurve.closestPoint.GetPositionX(), + bestCurve.closestPoint.GetPositionY(), bestCurve.closestPoint.GetPositionZ(), false, + false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); + } + + // Now we know we're on or very close to the curve, so we'll follow it properly + + // Find target point on curve (t_target parameter) + float t_target = 0.0f; + float targetMinDist = 9999.0f; + + for (float t = 0.0f; t <= 1.0f; t += ARC_STEP) + { + Position pt = CalculateBezierPoint(t, path); + float dist = bestCurve.moveTarget.GetExactDist2d(pt); + if (dist < targetMinDist) + { + targetMinDist = dist; + t_target = t; + } + } + + // Find an intermediate point along the curve between closest and target + float t_step = (t_target > bestCurve.t_closest) ? ARC_STEP : -ARC_STEP; + float t_intermediate = bestCurve.t_closest + t_step; + Position intermediateTarget; + bool foundValidIntermediate = false; + + // Limit the distance we move along the curve in one step + const float MAX_CURVE_MOVEMENT = 7.0f; // Max yards to move along curve + float curveDistanceMoved = 0.0f; + Position lastPos = bestCurve.closestPoint; + + while ((t_step > 0 && t_intermediate <= t_target) || (t_step < 0 && t_intermediate >= t_target)) + { + Position pt = CalculateBezierPoint(t_intermediate, path); + + // Check if this point is safe + if (!IsPositionInShadow(pt)) + { + // Calculate distance moved along curve so far + curveDistanceMoved += lastPos.GetExactDist2d(pt); + lastPos = pt; + + // If we've moved the maximum allowed distance, use this position + if (curveDistanceMoved >= MAX_CURVE_MOVEMENT) + { + intermediateTarget = pt; + foundValidIntermediate = true; + break; + } + + // Otherwise, continue moving along the curve + intermediateTarget = pt; + foundValidIntermediate = true; + } + else + { + // We've hit a shadow, stop here + break; + } + + t_intermediate += t_step; + } + + // If we found a valid intermediate point, use it + if (foundValidIntermediate) + { + botAI->Reset(); + MoveTo(bot->GetMapId(), intermediateTarget.GetPositionX(), intermediateTarget.GetPositionY(), + intermediateTarget.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + botAI->Reset(); + // Fallback to direct movement to the target point on the curve + MoveTo(bot->GetMapId(), bestCurve.moveTarget.GetPositionX(), bestCurve.moveTarget.GetPositionY(), + bestCurve.moveTarget.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + + return false; +} + +Position IccBqlGroupPositionAction::AdjustControlPoint(const Position& wall, const Position& center, float factor) +{ + float dx = wall.GetPositionX() - center.GetPositionX(); + float dy = wall.GetPositionY() - center.GetPositionY(); + float dz = wall.GetPositionZ() - center.GetPositionZ(); + return Position(center.GetPositionX() + dx * factor, center.GetPositionY() + dy * factor, + center.GetPositionZ() + dz * factor); +} + +Position IccBqlGroupPositionAction::CalculateBezierPoint(float t, const Position path[4]) +{ + float omt = 1 - t; + float omt2 = omt * omt; + float omt3 = omt2 * omt; + float t2 = t * t; + float t3 = t2 * t; + + float x = omt3 * path[0].GetPositionX() + 3 * omt2 * t * path[1].GetPositionX() + + 3 * omt * t2 * path[2].GetPositionX() + t3 * path[3].GetPositionX(); + + float y = omt3 * path[0].GetPositionY() + 3 * omt2 * t * path[1].GetPositionY() + + 3 * omt * t2 * path[2].GetPositionY() + t3 * path[3].GetPositionY(); + + float z = omt3 * path[0].GetPositionZ() + 3 * omt2 * t * path[1].GetPositionZ() + + 3 * omt * t2 * path[2].GetPositionZ() + t3 * path[3].GetPositionZ(); + + return Position(x, y, z); +} + +bool IccBqlGroupPositionAction::HandleGroupPosition(Unit* boss, Aura* frenzyAura, Aura* shadowAura) +{ + if (frenzyAura || shadowAura) + return false; + GuidVector members = AI_VALUE(GuidVector, "group members"); - - if (isRanged && !aura && !aura2) //frenzied bloodthrist - { - // Ranged: spread from other ranged - for (auto& member : members) - { - Unit* unit = botAI->GetUnit(member); - if (!unit || !unit->IsAlive() || unit == bot || botAI->GetAura("Frenzied Bloodthirst", unit) || botAI->GetAura("Uncontrollable Frenzy", unit)) - continue; + bool isRanged = botAI->IsRanged(bot); + bool isMelee = botAI->IsMelee(bot); - float dist = bot->GetExactDist2d(unit); - if (dist < radius) + if (isRanged && bot->GetExactDist2d(boss->GetPositionX(), boss->GetPositionY()) > 35.0f) + MoveTo(boss, 5.0f, MovementPriority::MOVEMENT_FORCED); + + if ((boss->GetExactDist2d(ICC_BQL_TANK_POSITION.GetPositionX(), ICC_BQL_TANK_POSITION.GetPositionY()) > 10.0f) && + isRanged && !((boss->GetPositionZ() - ICC_BQL_CENTER_POSITION.GetPositionZ()) > 5.0f) && + (bot->GetExactDist2d(ICC_BQL_CENTER_POSITION.GetPositionX(), ICC_BQL_CENTER_POSITION.GetPositionY()) > 10.0f)) + MoveTo(bot->GetMapId(), ICC_BQL_CENTER_POSITION.GetPositionX(), ICC_BQL_CENTER_POSITION.GetPositionY(), + ICC_BQL_CENTER_POSITION.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); + + // --- Ranged bots wall assignment logic --- + if (isRanged) + { + // Gather all ranged and healers, sort by GUID for deterministic assignment + std::vector rangedBots; + std::vector healers; + for (const auto& guid : members) + { + Unit* member = botAI->GetUnit(guid); + if (!member || !member->IsAlive()) + continue; + Player* player = member->ToPlayer(); + if (!player) + continue; + if (botAI->IsRanged(player)) + rangedBots.push_back(player); + if (botAI->IsHeal(player)) + healers.push_back(player); + } + // Remove duplicates (healer can be ranged) + std::sort(rangedBots.begin(), rangedBots.end(), + [](Player* a, Player* b) { return a->GetGUID() < b->GetGUID(); }); + std::sort(healers.begin(), healers.end(), [](Player* a, Player* b) { return a->GetGUID() < b->GetGUID(); }); + + // Assign at least one healer to each side, then balance the rest + std::vector leftSide, rightSide; + Position leftPos = ICC_BQL_LWALL2_POSITION; + Position rightPos = ICC_BQL_RWALL2_POSITION; + + // Assign healers first + if (!healers.empty()) + { + leftSide.push_back(healers[0]); + if (healers.size() > 1) + rightSide.push_back(healers[1]); + } + // If only one healer, assign to left, right will be filled by ranged DPS + + // Remove assigned healers from rangedBots + for (Player* h : leftSide) + rangedBots.erase(std::remove(rangedBots.begin(), rangedBots.end(), h), rangedBots.end()); + for (Player* h : rightSide) + rangedBots.erase(std::remove(rangedBots.begin(), rangedBots.end(), h), rangedBots.end()); + + // Distribute remaining ranged evenly + size_t totalRanged = leftSide.size() + rightSide.size() + rangedBots.size(); + size_t leftCount = leftSide.size(); + size_t rightCount = rightSide.size(); + for (Player* p : rangedBots) + { + if (leftCount <= rightCount) { - float moveDistance = std::min(moveIncrement, radius - dist + 1.0f); - return FleePosition(unit->GetPosition(), moveDistance, 250U); - //return MoveAway(unit, moveDistance); + leftSide.push_back(p); + leftCount++; + } + else + { + rightSide.push_back(p); + rightCount++; + } + } + + // Determine which side this bot is assigned to + bool isLeft = std::find(leftSide.begin(), leftSide.end(), bot) != leftSide.end(); + bool isRight = std::find(rightSide.begin(), rightSide.end(), bot) != rightSide.end(); + + // Move to assigned wall position if not already close + const float MAX_WALL_DIST = 30.0f; + const float MOVE_INCREMENT = 2.0f; + const float MAX_MOVE_DISTANCE = 7.0f; + const float SAFE_SPACING_RADIUS = 7.0f; + const float MIN_CENTER_DISTANCE = 10.0f; + + Position targetWall = isLeft ? leftPos : (isRight ? rightPos : Position()); + if (isLeft || isRight) + { + float distToWall = bot->GetExactDist2d(targetWall.GetPositionX(), targetWall.GetPositionY()); + if (distToWall > MAX_WALL_DIST) + { + // Move in increments toward wall + float dx = targetWall.GetPositionX() - bot->GetPositionX(); + float dy = targetWall.GetPositionY() - bot->GetPositionY(); + float len = std::sqrt(dx * dx + dy * dy); + if (len > 0.001f) + { + dx /= len; + dy /= len; + float moveDist = std::min(MOVE_INCREMENT, distToWall); + float targetX = bot->GetPositionX() + dx * moveDist; + float targetY = bot->GetPositionY() + dy * moveDist; + float targetZ = bot->GetPositionZ(); + if (!bot->IsWithinLOS(targetX, targetY, targetZ)) + { + targetX = bot->GetPositionX() + dx * (moveDist * 0.5f); + targetY = bot->GetPositionY() + dy * (moveDist * 0.5f); + } + botAI->Reset(); + MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + // Spread from other assigned members on the same side and from swarming shadows + float totalX = 0.0f, totalY = 0.0f; + int nearbyCount = 0; + const std::vector& mySide = isLeft ? leftSide : rightSide; + for (Player* member : mySide) + { + if (!member || !member->IsAlive() || member == bot) + continue; + float distance = bot->GetExactDist2d(member); + if (distance < SAFE_SPACING_RADIUS) + { + float dx = bot->GetPositionX() - member->GetPositionX(); + float dy = bot->GetPositionY() - member->GetPositionY(); + float weight = (SAFE_SPACING_RADIUS - distance) / SAFE_SPACING_RADIUS; + totalX += dx * weight; + totalY += dy * weight; + nearbyCount++; + } + } + // Also spread from swarming shadows + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (const auto& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_SWARMING_SHADOWS) + { + float distance = bot->GetExactDist2d(unit); + if (distance < SAFE_SPACING_RADIUS) + { + float dx = bot->GetPositionX() - unit->GetPositionX(); + float dy = bot->GetPositionY() - unit->GetPositionY(); + float weight = (SAFE_SPACING_RADIUS - distance) / SAFE_SPACING_RADIUS; + totalX += dx * weight; + totalY += dy * weight; + nearbyCount++; + } + } + } + if (nearbyCount > 0) + { + float magnitude = sqrt(totalX * totalX + totalY * totalY); + if (magnitude > 0.001f) + { + totalX /= magnitude; + totalY /= magnitude; + float moveDistance = std::min(MOVE_INCREMENT, MAX_MOVE_DISTANCE); + float targetX = bot->GetPositionX() + totalX * moveDistance; + float targetY = bot->GetPositionY() + totalY * moveDistance; + float targetZ = bot->GetPositionZ(); + if (!bot->IsWithinLOS(targetX, targetY, targetZ)) + { + targetX = bot->GetPositionX() + totalX * (moveDistance * 0.5f); + targetY = bot->GetPositionY() + totalY * (moveDistance * 0.5f); + } + botAI->Reset(); + MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + // Maintain minimum distance from center position (if too close to center, move out) + float centerX = ICC_BQL_CENTER_POSITION.GetPositionX(); + float centerY = ICC_BQL_CENTER_POSITION.GetPositionY(); + float centerDist = + std::sqrt(std::pow(bot->GetPositionX() - centerX, 2) + std::pow(bot->GetPositionY() - centerY, 2)); + if (centerDist < MIN_CENTER_DISTANCE && !((boss->GetPositionZ() - bot->GetPositionZ()) > 5.0f)) + { + float dx = bot->GetPositionX() - centerX; + float dy = bot->GetPositionY() - centerY; + float dist = std::sqrt(dx * dx + dy * dy); + if (dist > 0.001f) + { + dx /= dist; + dy /= dist; + float moveDistance = std::min(MIN_CENTER_DISTANCE - centerDist + 1.0f, MAX_MOVE_DISTANCE); + float targetX = bot->GetPositionX() + dx * moveDistance; + float targetY = bot->GetPositionY() + dy * moveDistance; + float targetZ = bot->GetPositionZ(); + if (!bot->IsWithinLOS(targetX, targetY, targetZ)) + { + targetX = bot->GetPositionX() + dx * (moveDistance * 0.5f); + targetY = bot->GetPositionY() + dy * (moveDistance * 0.5f); + } + botAI->Reset(); + MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); + } } } } - if (isMelee && !aura && !aura2 && ((boss->GetPositionZ() - bot->GetPositionZ()) > 5.0f)) // melee also spread + if (isMelee && ((boss->GetPositionZ() - ICC_BQL_CENTER_POSITION.GetPositionZ()) > 5.0f)) { - // Melee: spread from other melee - for (auto& member : members) + const float SAFE_SPACING_RADIUS = 7.0f; + const float MOVE_INCREMENT = 2.0f; + const float MAX_MOVE_DISTANCE = 7.0f; + + float totalX = 0.0f; + float totalY = 0.0f; + int nearbyCount = 0; + + // Find all swarming shadows + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + std::vector swarmingShadows; + for (int i = 0; i < npcs.size(); ++i) { - Unit* unit = botAI->GetUnit(member); - if (!unit || !unit->IsAlive() || unit == bot || botAI->GetAura("Frenzied Bloodthirst", unit) || botAI->GetAura("Uncontrollable Frenzy", unit)) + Unit* unit = botAI->GetUnit(npcs[i]); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_SWARMING_SHADOWS) + swarmingShadows.push_back(unit); + } + + for (int i = 0; i < members.size(); i++) + { + Unit* member = botAI->GetUnit(members[i]); + if (!member || !member->IsAlive() || member == bot || botAI->GetAura("Frenzied Bloodthirst", member) || + botAI->GetAura("Uncontrollable Frenzy", member)) continue; - float dist = bot->GetExactDist2d(unit); - if (dist < radius) + float distance = bot->GetExactDist2d(member); + if (distance < SAFE_SPACING_RADIUS) { - // Calculate direction away from nearby player - float dx = bot->GetPositionX() - unit->GetPositionX(); - float dy = bot->GetPositionY() - unit->GetPositionY(); - float angle = atan2(dy, dx); - - // Move 8 yards away from the player - float newX = unit->GetPositionX() + cos(angle) * 8.0f; - float newY = unit->GetPositionY() + sin(angle) * 8.0f; - - return MoveTo(bot->GetMapId(), newX, newY, bot->GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED); + float dx = bot->GetPositionX() - member->GetPositionX(); + float dy = bot->GetPositionY() - member->GetPositionY(); + float weight = (SAFE_SPACING_RADIUS - distance) / SAFE_SPACING_RADIUS; + totalX += dx * weight; + totalY += dy * weight; + nearbyCount++; } } + + // Also spread from swarming shadows + for (Unit* shadow : swarmingShadows) + { + float distance = bot->GetExactDist2d(shadow); + if (distance < SAFE_SPACING_RADIUS) + { + float dx = bot->GetPositionX() - shadow->GetPositionX(); + float dy = bot->GetPositionY() - shadow->GetPositionY(); + float weight = (SAFE_SPACING_RADIUS - distance) / SAFE_SPACING_RADIUS; + totalX += dx * weight; + totalY += dy * weight; + nearbyCount++; + } + } + + if (nearbyCount > 0) + { + float magnitude = sqrt(totalX * totalX + totalY * totalY); + if (magnitude > 0.001f) + { + totalX /= magnitude; + totalY /= magnitude; + float moveDistance = MOVE_INCREMENT < MAX_MOVE_DISTANCE ? MOVE_INCREMENT : MAX_MOVE_DISTANCE; + float targetX = bot->GetPositionX() + totalX * moveDistance; + float targetY = bot->GetPositionY() + totalY * moveDistance; + float targetZ = bot->GetPositionZ(); + + if (!bot->IsWithinLOS(targetX, targetY, targetZ)) + { + targetX = bot->GetPositionX() + totalX * (moveDistance * 0.5f); + targetY = bot->GetPositionY() + totalY * (moveDistance * 0.5f); + } + + MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + return true; } - return false; // Everyone is in position + + + return false; } bool IccBqlPactOfDarkfallenAction::Execute(Event event) { // Check if bot has Pact of the Darkfallen - Aura* aura = botAI->GetAura("Pact of the Darkfallen", bot); - if (!aura) + if (!botAI->GetAura("Pact of the Darkfallen", bot)) return false; - const float POSITION_TOLERANCE = 1.0f; // Within 1 yards to break the link - - // Find other players with Pact of the Darkfallen - std::vector playersWithAura; - Player* tankWithAura = nullptr; - Group* group = bot->GetGroup(); if (!group) return false; - // Gather all players with the aura + // Find other players with Pact of the Darkfallen + Player* tankWithAura = nullptr; + Player* otherPlayerWithAura = nullptr; + int auraCount = 0; + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) { Player* member = itr->GetSource(); - if (!member || member->GetGUID() == bot->GetGUID()) + if (!member || member == bot) continue; - if (botAI->GetAura("Pact of the Darkfallen", member)) //pact of darkfallen + if (botAI->GetAura("Pact of the Darkfallen", member)) { - playersWithAura.push_back(member); - // If this player is a tank, store them + auraCount++; if (botAI->IsTank(member)) tankWithAura = member; + else if (!otherPlayerWithAura) + otherPlayerWithAura = member; } } - // If we found other players with the aura - if (!playersWithAura.empty()) + if (auraCount == 0) + return false; + + // Determine target position + Position targetPos; + if (tankWithAura && !botAI->IsTank(bot)) { - Position targetPos; - - if (playersWithAura.size() >= 2) // 3 or more total (including this bot) - { - if (tankWithAura) - { - // Move to tank's position if we're not a tank - if (!botAI->IsTank(bot)) - { - targetPos.Relocate(tankWithAura); - } - else - { - // If we are the tank, stay put - return true; - } - } - else - { - // Calculate center position of all affected players - float sumX = bot->GetPositionX(); - float sumY = bot->GetPositionY(); - float sumZ = bot->GetPositionZ(); - int count = 1; // Start with 1 for this bot - - for (Player* player : playersWithAura) - { - sumX += player->GetPositionX(); - sumY += player->GetPositionY(); - sumZ += player->GetPositionZ(); - count++; - } - - targetPos.Relocate(sumX / count, sumY / count, sumZ / count); - } - } - else // Only one other player has aura - { - targetPos.Relocate(playersWithAura[0]); - } - - // Move to target position if we're not already there - if (bot->GetDistance(targetPos) > POSITION_TOLERANCE) - { - return MoveTo(bot->GetMapId(), targetPos.GetPositionX(), targetPos.GetPositionY(), targetPos.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED); - } + // Move to tank if we're not a tank + targetPos.Relocate(tankWithAura); + } + else if (auraCount >= 2) + { + // Calculate center position if multiple players have aura + CalculateCenterPosition(targetPos, otherPlayerWithAura); + } + else if (otherPlayerWithAura) + { + // Move to the single other player with aura + targetPos.Relocate(otherPlayerWithAura); + } + else + { + // No valid movement case found return true; } - // If no other players found with aura, move to center - if (bot->GetDistance(ICC_BQL_CENTER_POSITION) > POSITION_TOLERANCE) + // Move to target position if needed + return MoveToTargetPosition(targetPos, auraCount); +} + +void IccBqlPactOfDarkfallenAction::CalculateCenterPosition(Position& targetPos, Player* otherPlayer) +{ + float sumX = bot->GetPositionX() + otherPlayer->GetPositionX(); + float sumY = bot->GetPositionY() + otherPlayer->GetPositionY(); + float sumZ = bot->GetPositionZ() + otherPlayer->GetPositionZ(); + targetPos.Relocate(sumX / 2, sumY / 2, sumZ / 2); +} + +bool IccBqlPactOfDarkfallenAction::MoveToTargetPosition(const Position& targetPos, int auraCount) +{ + const float POSITION_TOLERANCE = 0.1f; + float distance = bot->GetDistance(targetPos); + + if (distance <= POSITION_TOLERANCE) + return true; + + // Calculate movement increment + float dx = targetPos.GetPositionX() - bot->GetPositionX(); + float dy = targetPos.GetPositionY() - bot->GetPositionY(); + float dz = targetPos.GetPositionZ() - bot->GetPositionZ(); + float len = sqrt(dx * dx + dy * dy); + + float moveX, moveY, moveZ; + if (len > 5.0f && auraCount<=2) { - botAI->SetNextCheckDelay(500); - return MoveTo(bot->GetMapId(), ICC_BQL_CENTER_POSITION.GetPositionX(), ICC_BQL_CENTER_POSITION.GetPositionY(), ICC_BQL_CENTER_POSITION.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED); + dx /= len; + dy /= len; + moveX = bot->GetPositionX() + dx * 5.0f; + moveY = bot->GetPositionY() + dy * 5.0f; + moveZ = bot->GetPositionZ() + (dz / distance) * 5.0f; } + else + { + moveX = targetPos.GetPositionX(); + moveY = targetPos.GetPositionY(); + moveZ = targetPos.GetPositionZ(); + } + + botAI->Reset(); + MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED); return false; } @@ -2304,143 +4396,150 @@ bool IccBqlPactOfDarkfallenAction::Execute(Event event) bool IccBqlVampiricBiteAction::Execute(Event event) { // Only act when bot has Frenzied Bloodthirst - Aura* aura = botAI->GetAura("Frenzied Bloodthirst", bot); - - if (!aura) + if (!botAI->GetAura("Frenzied Bloodthirst", bot)) return false; const float BITE_RANGE = 2.0f; - Player* target = nullptr; Group* group = bot->GetGroup(); if (!group) return false; - // Create lists for potential targets with their distances + // Find best target + Player* target = FindBestBiteTarget(group); + if (!target) + return false; + + // Handle movement or casting + float x = target->GetPositionX(); + float y = target->GetPositionY(); + float z = target->GetPositionZ(); + + if (bot->GetExactDist2d(target) > BITE_RANGE) + { + return MoveTowardsTarget(target); + } + else if (bot->IsWithinLOS(x, y, z)) + { + return CastVampiricBite(target); + } + + return false; +} + +Player* IccBqlVampiricBiteAction::FindBestBiteTarget(Group* group) +{ + std::set currentlyTargetedPlayers; std::vector> dpsTargets; std::vector> healTargets; - // Get list of players who are currently being targeted - std::set currentlyTargetedPlayers; + // Get currently targeted players for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) { Player* member = itr->GetSource(); if (!member || !member->IsAlive() || member == bot) continue; - if (botAI->GetAura("Frenzied Bloodthirst", member)) + if (botAI->GetAura("Frenzied Bloodthirst", member) && member->GetTarget()) { - if (ObjectGuid targetGuid = member->GetTarget()) - { - currentlyTargetedPlayers.insert(targetGuid); - } + currentlyTargetedPlayers.insert(member->GetTarget()); } } + // Evaluate potential targets for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) { Player* member = itr->GetSource(); if (!member || member == bot || !member->IsAlive()) continue; - // Skip if already has essence, frenzy, or is a tank, or uncontrollable frenzy - if (botAI->GetAura("Frenzied Bloodthirst", member) || botAI->GetAura("Essence of the Blood Queen", member) || botAI->GetAura("Uncontrollable Frenzy", member) || botAI->IsTank(member)) - { - continue; - } - - // Skip if this player is currently targeted by another bot - if (currentlyTargetedPlayers.find(member->GetGUID()) != currentlyTargetedPlayers.end()) + if (IsInvalidTarget(member) || currentlyTargetedPlayers.count(member->GetGUID())) continue; float distance = bot->GetDistance(member); - if (botAI->IsDps(member)) - dpsTargets.push_back(std::make_pair(member, distance)); + dpsTargets.push_back({member, distance}); else if (botAI->IsHeal(member)) - healTargets.push_back(std::make_pair(member, distance)); + healTargets.push_back({member, distance}); } - // Sort both vectors by distance - auto sortByDistance = [](const std::pair& a, const std::pair& b) { - return a.second < b.second; - }; - + // Sort by distance + auto sortByDistance = [](const auto& a, const auto& b) { return a.second < b.second; }; std::sort(dpsTargets.begin(), dpsTargets.end(), sortByDistance); std::sort(healTargets.begin(), healTargets.end(), sortByDistance); - // First try closest DPS target + // Return closest valid target if (!dpsTargets.empty()) - { - target = dpsTargets[0].first; - } - // If no DPS available, try closest healer - else if (!healTargets.empty()) - { - target = healTargets[0].first; - } + return dpsTargets[0].first; + if (!healTargets.empty()) + return healTargets[0].first; + return nullptr; +} - if (!target) - { +bool IccBqlVampiricBiteAction::IsInvalidTarget(Player* player) +{ + return botAI->GetAura("Frenzied Bloodthirst", player) || botAI->GetAura("Essence of the Blood Queen", player) || + botAI->GetAura("Uncontrollable Frenzy", player) || botAI->GetAura("Swarming Shadows", player) || + botAI->IsTank(player); +} + +bool IccBqlVampiricBiteAction::MoveTowardsTarget(Player* target) +{ + if (IsInvalidTarget(target) || !target->IsAlive()) return false; - } - // Double check target is still alive - if (!target->IsAlive() || (botAI->GetAura("Frenzied Bloodthirst", target) - || botAI->GetAura("Essence of the Blood Queen", target) - || botAI->GetAura("Uncontrollable Frenzy", target))) - { - return false; - } - - // Check if we can reach the target float x = target->GetPositionX(); float y = target->GetPositionY(); float z = target->GetPositionZ(); - if (bot->IsWithinLOS(x, y, z) && bot->GetExactDist2d(target) > BITE_RANGE - && !(botAI->GetAura("Frenzied Bloodthirst", target) - || botAI->GetAura("Essence of the Blood Queen", target) - || botAI->GetAura("Uncontrollable Frenzy", target))) + if (!bot->IsWithinLOS(x, y, z)) + return false; + + float dx = x - bot->GetPositionX(); + float dy = y - bot->GetPositionY(); + float dz = z - bot->GetPositionZ(); + float len = sqrt(dx * dx + dy * dy); + + float moveX, moveY, moveZ; + if (len > 5.0f) { - return MoveTo(target->GetMapId(), x, y, z, false, false, false, true, MovementPriority::MOVEMENT_FORCED); + dx /= len; + dy /= len; + moveX = bot->GetPositionX() + dx * 5.0f; + moveY = bot->GetPositionY() + dy * 5.0f; + moveZ = bot->GetPositionZ() + (dz / len) * 5.0f; + } + else + { + moveX = x; + moveY = y; + moveZ = z; } - // If in range and can see target, cast the bite - if (bot->IsWithinLOS(x, y, z) && bot->GetExactDist2d(target) <= BITE_RANGE) - { - // Final alive check before casting - if (!target->IsAlive() || (botAI->GetAura("Frenzied Bloodthirst", target) - || botAI->GetAura("Essence of the Blood Queen", target) - || botAI->GetAura("Uncontrollable Frenzy", target))) - { - return false; - } - - if (botAI->CanCastSpell("Vampiric Bite", target)) - { - return botAI->CastSpell("Vampiric Bite", target); - } - } + MoveTo(target->GetMapId(), moveX, moveY, moveZ, false, false, false, true, + MovementPriority::MOVEMENT_FORCED); return false; } -//VDW +bool IccBqlVampiricBiteAction::CastVampiricBite(Player* target) +{ + if (IsInvalidTarget(target) || !target->IsAlive()) + return false; -//Valkyre 38248 spear, 50307 spear in inv, Sister Svalna 37126, aether shield 71463 + return botAI->CanCastSpell("Vampiric Bite", target) && botAI->CastSpell("Vampiric Bite", target); +} +// Sister Svalna bool IccValkyreSpearAction::Execute(Event event) { // Find the nearest spear - Creature* spear = bot->FindNearestCreature(38248, 100.0f); + Creature* spear = bot->FindNearestCreature(NPC_SPEAR, 100.0f); if (!spear) return false; // Move to the spear if not in range if (!spear->IsWithinDistInMap(bot, INTERACTION_DISTANCE)) - { return MoveTo(spear, INTERACTION_DISTANCE); - } // Remove shapeshift forms botAI->RemoveShapeshift(); @@ -2460,18 +4559,18 @@ bool IccValkyreSpearAction::Execute(Event event) bool IccSisterSvalnaAction::Execute(Event event) { Unit* svalna = AI_VALUE2(Unit*, "find target", "sister svalna"); - if (!svalna || !svalna->HasAura(71463)) // Check for Aether Shield aura + if (!svalna || !svalna->HasAura(SPELL_AETHER_SHIELD)) // Check for Aether Shield aura return false; // Check if bot has the spear item - if (!botAI->HasItemInInventory(50307)) + if (!botAI->HasItemInInventory(ITEM_SPEAR)) return false; // Get all items from inventory std::vector items = botAI->GetInventoryItems(); for (Item* item : items) { - if (item->GetEntry() == 50307) // Spear ID + if (item->GetEntry() == ITEM_SPEAR) // Spear ID { botAI->ImbueItem(item, svalna); // Use spear on Svalna return false; @@ -2481,102 +4580,611 @@ bool IccSisterSvalnaAction::Execute(Event event) return false; } -bool IccValithriaPortalAction::Execute(Event event) +// VDW +bool IccValithriaGroupAction::Execute(Event event) { - //Added movement for non healers, didnt want to make another action just for this - if (!botAI->IsHeal(bot)) - return MoveTo(bot->GetMapId(), ICC_VDW_GROUP_POSITION.GetPositionX(), ICC_VDW_GROUP_POSITION.GetPositionY(), ICC_VDW_GROUP_POSITION.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_COMBAT); - - //Portal action - if (!botAI->IsHeal(bot) || bot->HasAura(70766)) - return false; - - // Find the nearest portal - Creature* portal = bot->FindNearestCreature(37945, 100.0f); // Dream Portal - if (!portal) - portal = bot->FindNearestCreature(38430, 100.0f); // Nightmare Portal - - if (!portal) - return false; - - // Move exactly to portal position - float portalX = portal->GetPositionX(); - float portalY = portal->GetPositionY(); - float portalZ = portal->GetPositionZ(); - - // If not at exact portal position, move there - if (bot->GetDistance2d(portalX, portalY) > 0.1f) + // Helper lambda to find nearest creature of given entries + auto findNearestCreature = [this](std::initializer_list entries, float range) -> Creature* { - return MoveTo(portal->GetMapId(), portalX, portalY, portalZ, false, false, false, true, MovementPriority::MOVEMENT_NORMAL); + for (uint32 entry : entries) + { + if (Creature* creature = bot->FindNearestCreature(entry, range)) + { + return creature; + } + } + return nullptr; + }; + + // Find portals and enemies + Creature* portal = findNearestCreature( + {NPC_DREAM_PORTAL, NPC_DREAM_PORTAL_PRE_EFFECT, NPC_NIGHTMARE_PORTAL, NPC_NIGHTMARE_PORTAL_PRE_EFFECT}, 100.0f); + + Creature* worm = bot->FindNearestCreature(NPC_ROT_WORM, 100.0f); + Creature* zombie = bot->FindNearestCreature(NPC_BLISTERING_ZOMBIE, 100.0f); + Creature* manaVoid = bot->FindNearestCreature(NPC_MANA_VOID, 100.0f); + + // Find column of frost units + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + std::vector columnOfFrost; + for (ObjectGuid guid : npcs) + { + if (Unit* unit = botAI->GetUnit(guid)) + { + if (unit->IsAlive() && unit->GetEntry() == NPC_COLUMN_OF_FROST) + { + columnOfFrost.push_back(unit); + } + } } - // Remove shapeshift forms - botAI->RemoveShapeshift(); + // Tank behavior + if (botAI->IsTank(bot)) + { + for (const auto& targetGuid : AI_VALUE(GuidVector, "possible targets")) + { + if (Unit* unit = botAI->GetUnit(targetGuid)) + { + if (unit->IsAlive() && + (unit->GetEntry() == NPC_GLUTTONOUS_ABOMINATION || unit->GetEntry() == NPC_ROT_WORM)) + { + // Skip if unit is already attacking any tank + if (Unit* victim = unit->GetVictim()) + { + if (victim->GetTypeId() == TYPEID_PLAYER && botAI->IsTank(static_cast(victim))) + { + continue; + } + } - // Stop movement and click the portal - bot->GetMotionMaster()->Clear(); - bot->StopMoving(); - portal->HandleSpellClick(bot); + // Only attack if not already targeting us + if (unit->GetVictim() != bot) + { + bot->SetTarget(unit->GetGUID()); + bot->SetFacingToObject(unit); + Attack(unit); + } + } + } + } + } - // Dismount if mounted - WorldPacket emptyPacket; - bot->GetSession()->HandleCancelMountAuraOpcode(emptyPacket); + // Healer movement logic + if (botAI->IsHeal(bot) && bot->GetExactDist2d(ICC_VDW_HEAL_POSITION) > 45.0f && !portal) + MoveTowardsPosition(ICC_VDW_HEAL_POSITION, 10.0f); + + // Avoidance behaviors + if (manaVoid && bot->GetExactDist2d(manaVoid) < 10.0f && + !(botAI->GetAura("Twisted Nightmares", bot) || botAI->GetAura("Emerald Vigor", bot))) + { + botAI->Reset(); + FleePosition(manaVoid->GetPosition(), 11.0f, 250U); + } + + for (Unit* column : columnOfFrost) + { + if (column && bot->GetExactDist2d(column) < 7.0f) + { + botAI->Reset(); + FleePosition(column->GetPosition(), 8.0f, 250U); + } + } + + if (worm && worm->IsAlive() && worm->GetVictim() == bot && !botAI->IsTank(bot)) + { + botAI->Reset(); + FleePosition(worm->GetPosition(), 10.0f, 250U); + } + + if (zombie && zombie->IsAlive() && zombie->GetVictim() == bot && !botAI->IsTank(bot) && + bot->GetExactDist2d(zombie) < 20.0f) + { + botAI->Reset(); + FleePosition(zombie->GetPosition(), 21.0f, 250U); + } + + // Crowd control logic + if (zombie && !botAI->IsMainTank(bot) && !botAI->IsHeal(bot) && zombie->GetVictim() != bot) + { + switch (bot->getClass()) + { + case CLASS_MAGE: + if (!botAI->HasAura("Frost Nova", zombie)) + botAI->CastSpell("Frost Nova", zombie); + break; + case CLASS_DRUID: + if (!botAI->HasAura("Entangling Roots", zombie)) + botAI->CastSpell("Entangling Roots", zombie); + break; + case CLASS_PALADIN: + if (!botAI->HasAura("Hammer of Justice", zombie)) + botAI->CastSpell("Hammer of Justice", zombie); + break; + case CLASS_WARRIOR: + if (!botAI->HasAura("Hamstring", zombie)) + botAI->CastSpell("Hamstring", zombie); + break; + case CLASS_HUNTER: + if (!botAI->HasAura("Concussive Shot", zombie)) + botAI->CastSpell("Concussive Shot", zombie); + break; + case CLASS_ROGUE: + if (!botAI->HasAura("Kidney Shot", zombie)) + botAI->CastSpell("Kidney Shot", zombie); + break; + case CLASS_SHAMAN: + if (!botAI->HasAura("Frost Shock", zombie)) + botAI->CastSpell("Frost Shock", zombie); + break; + case CLASS_DEATH_KNIGHT: + if (!botAI->HasAura("Chains of Ice", zombie)) + botAI->CastSpell("Chains of Ice", zombie); + break; + default: + break; + } + } + + // Group assignment and movement logic + Difficulty diff = bot->GetRaidDifficulty(); + Group* group = bot->GetGroup(); + + if (group && (diff == RAID_DIFFICULTY_25MAN_NORMAL || diff == RAID_DIFFICULTY_25MAN_HEROIC)) + return Handle25ManGroupLogic(); + else + return Handle10ManGroupLogic(); +} + +bool IccValithriaGroupAction::MoveTowardsPosition(const Position& pos, float increment) +{ + float dx = pos.GetPositionX() - bot->GetPositionX(); + float dy = pos.GetPositionY() - bot->GetPositionY(); + float dz = pos.GetPositionZ() - bot->GetPositionZ(); + float dist = std::hypot(dx, dy); + + float moveX, moveY, moveZ; + if (dist > increment) + { + dx /= dist; + dy /= dist; + moveX = bot->GetPositionX() + dx * increment; + moveY = bot->GetPositionY() + dy * increment; + moveZ = bot->GetPositionZ() + (dz / dist) * increment; + } + else + { + moveX = pos.GetPositionX(); + moveY = pos.GetPositionY(); + moveZ = pos.GetPositionZ(); + } + + MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, true, MovementPriority::MOVEMENT_COMBAT, + true, false); + + return false; +} + +bool IccValithriaGroupAction::Handle25ManGroupLogic() +{ + const Position group1Pos = ICC_VDW_GROUP1_POSITION; + const Position group2Pos = ICC_VDW_GROUP2_POSITION; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + // Collect group members + std::vector tanks, dps; + std::vector> nonHeals; + + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + if (Player* member = itr->GetSource()) + { + if (member->IsAlive() && !botAI->IsHeal(member)) + { + if (botAI->IsTank(member)) + { + tanks.push_back(member); + } + else + { + dps.push_back(member); + } + nonHeals.emplace_back(member->GetGUID(), member); + } + } + } + + // Sort by GUID for consistent ordering + std::sort(nonHeals.begin(), nonHeals.end(), [](const auto& a, const auto& b) { return a.first < b.first; }); + + // Assign to groups + std::vector group1, group2; + if (!tanks.empty()) + group1.push_back(tanks[0]); + + if (tanks.size() > 1) + group2.push_back(tanks[1]); + else if (tanks.size() == 1 && !dps.empty()) + group2.push_back(dps[0]); + + // Assign remaining DPS + std::set assigned; + for (Player* p : group1) + assigned.insert(p->GetGUID()); + for (Player* p : group2) + assigned.insert(p->GetGUID()); + + for (Player* p : dps) + { + if (assigned.find(p->GetGUID()) == assigned.end()) + { + (group1.size() <= group2.size() ? group1 : group2).push_back(p); + } + } + + // Check which group the bot is in + bool inGroup1 = std::any_of(group1.begin(), group1.end(), [this](Player* p) { return p == bot; }); + bool inGroup2 = std::any_of(group2.begin(), group2.end(), [this](Player* p) { return p == bot; }); + + // Marking logic for tanks and DPS + if (botAI->IsTank(bot) || botAI->IsDps(bot)) + HandleMarkingLogic(inGroup1, inGroup2, group1Pos, group2Pos); + + + // Movement logic for non-healers + if (!botAI->IsHeal(bot)) + { + if (inGroup1) + { + return MoveTowardsPosition(group1Pos, 5.0f); + } + else if (inGroup2) + { + return MoveTowardsPosition(group2Pos, 5.0f); + } + } + + return false; +} + +bool IccValithriaGroupAction::HandleMarkingLogic(bool inGroup1, bool inGroup2, const Position& group1Pos, + const Position& group2Pos) +{ + static constexpr uint8_t SKULL_ICON_INDEX = 7; + static constexpr uint8_t CROSS_ICON_INDEX = 6; + static const std::array addPriority = {NPC_BLAZING_SKELETON, NPC_SUPPRESSER, + NPC_RISEN_ARCHMAGE, NPC_BLISTERING_ZOMBIE, + NPC_GLUTTONOUS_ABOMINATION, NPC_ROT_WORM}; + + const Position* groupPos = nullptr; + uint8_t iconIndex = 0; + std::string rtiValue; + + if (inGroup1) + { + iconIndex = SKULL_ICON_INDEX; + groupPos = &group1Pos; + rtiValue = "skull"; + } + else if (inGroup2) + { + iconIndex = CROSS_ICON_INDEX; + groupPos = &group2Pos; + rtiValue = "cross"; + } + else + return false; + + context->GetValue("rti")->Set(rtiValue); + + // Find priority target + const GuidVector adds = AI_VALUE(GuidVector, "possible targets"); + Unit* priorityTarget = nullptr; + + for (uint32 entry : addPriority) + { + for (const auto& guid : adds) + { + if (Unit* unit = botAI->GetUnit(guid)) + { + if (unit->IsAlive() && unit->GetEntry() == entry && + unit->GetExactDist2d(groupPos->GetPositionX(), groupPos->GetPositionY()) <= 25.0f) + { + priorityTarget = unit; + break; + } + } + } + if (priorityTarget) + break; + } + + // Update target icon if needed + if (priorityTarget && bot->GetGroup()) + { + Group* group = bot->GetGroup(); + ObjectGuid currentIcon = group->GetTargetIcon(iconIndex); + Unit* currentIconUnit = botAI->GetUnit(currentIcon); + + if (!currentIconUnit || !currentIconUnit->IsAlive() || currentIconUnit != priorityTarget) + group->SetTargetIcon(iconIndex, bot->GetGUID(), priorityTarget->GetGUID()); + } + + return false; +} + +bool IccValithriaGroupAction::Handle10ManGroupLogic() +{ + static constexpr uint8_t DEFAULT_ICON_INDEX = 7; + static const std::array addPriority = {NPC_BLAZING_SKELETON, NPC_SUPPRESSER, + NPC_RISEN_ARCHMAGE, NPC_BLISTERING_ZOMBIE, + NPC_GLUTTONOUS_ABOMINATION, NPC_ROT_WORM}; + + // Marking logic + Group* group = bot->GetGroup(); + if (group) + { + const GuidVector adds = AI_VALUE(GuidVector, "possible targets"); + Unit* priorityTarget = nullptr; + + for (uint32 entry : addPriority) + { + for (const auto& guid : adds) + { + if (Unit* unit = botAI->GetUnit(guid)) + { + if (unit->IsAlive() && unit->GetEntry() == entry && + unit->GetExactDist2d(ICC_VDW_HEAL_POSITION.GetPositionX(), + ICC_VDW_HEAL_POSITION.GetPositionY()) <= 50.0f) + { + priorityTarget = unit; + break; + } + } + } + if (priorityTarget) + break; + } + + if (priorityTarget) + { + ObjectGuid currentIcon = group->GetTargetIcon(DEFAULT_ICON_INDEX); + Unit* currentIconUnit = botAI->GetUnit(currentIcon); + + if (!currentIconUnit || !currentIconUnit->IsAlive() || currentIconUnit != priorityTarget) + { + group->SetTargetIcon(DEFAULT_ICON_INDEX, bot->GetGUID(), priorityTarget->GetGUID()); + } + } + } + + // Movement logic + if (bot->GetExactDist2d(ICC_VDW_HEAL_POSITION.GetPositionX(), ICC_VDW_HEAL_POSITION.GetPositionY()) > 35.0f) + MoveTowardsPosition(ICC_VDW_HEAL_POSITION, 5.0f); + + return false; +} + +bool IccValithriaPortalAction::Execute(Event event) +{ + // Only healers should take portals, and not if already inside + if (!botAI->IsHeal(bot) || bot->HasAura(SPELL_DREAM_STATE)) + return false; + + // Gather all portals (pre-effect and real) using nearest npcs + GuidVector npcs = AI_VALUE(GuidVector, "nearest npcs"); + std::vector preEffectPortals; + std::vector realPortals; + for (const auto& guid : npcs) + { + Creature* c = dynamic_cast(botAI->GetUnit(guid)); + if (!c) + continue; + uint32 entry = c->GetEntry(); + if (entry == NPC_DREAM_PORTAL_PRE_EFFECT || entry == NPC_NIGHTMARE_PORTAL_PRE_EFFECT) + preEffectPortals.push_back(c); + else if (entry == NPC_DREAM_PORTAL || entry == NPC_NIGHTMARE_PORTAL) + realPortals.push_back(c); + } + + if (preEffectPortals.empty() && realPortals.empty()) + return false; + + // Remove duplicates (in case of overlap) + auto sortByGuid = [](Creature* a, Creature* b) { return a->GetGUID() < b->GetGUID(); }; + std::sort(preEffectPortals.begin(), preEffectPortals.end(), sortByGuid); + preEffectPortals.erase(std::unique(preEffectPortals.begin(), preEffectPortals.end()), preEffectPortals.end()); + std::sort(realPortals.begin(), realPortals.end(), sortByGuid); + realPortals.erase(std::unique(realPortals.begin(), realPortals.end()), realPortals.end()); + + // Gather all healers in group, sort by GUID for deterministic assignment + Group* group = bot->GetGroup(); + std::vector healers; + if (group) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && member->IsAlive() && botAI->IsHeal(member)) + healers.push_back(member); + } + std::sort(healers.begin(), healers.end(), [](Player* a, Player* b) { return a->GetGUID() < b->GetGUID(); }); + } + else + healers.push_back(bot); + + // Find this bot's index among healers + auto it = std::find(healers.begin(), healers.end(), bot); + if (it == healers.end()) + return false; + size_t healerIndex = std::distance(healers.begin(), it); + + // Assign each healer to a pre-effect portal by index (wrap if more healers than portals) + Creature* assignedPreEffect = nullptr; + if (!preEffectPortals.empty()) + assignedPreEffect = preEffectPortals[healerIndex % preEffectPortals.size()]; + + // Move to assigned pre-effect portal, stand at portal + if (assignedPreEffect) + { + float portalX = assignedPreEffect->GetPositionX(); + float portalY = assignedPreEffect->GetPositionY(); + float portalZ = assignedPreEffect->GetPositionZ(); + float dist = bot->GetDistance2d(portalX, portalY); + + if (dist > 0.5f) + { + // Move directly to the pre-effect portal position + MoveTo(assignedPreEffect->GetMapId(), portalX, portalY, portalZ, false, false, false, true, + MovementPriority::MOVEMENT_NORMAL); + } + // Remove shapeshift forms + botAI->RemoveShapeshift(); + + // Try to click the real portal if it is close enough + Creature* nearestRealPortal = nullptr; + float minDist = 9999.0f; + for (Creature* portal : realPortals) + { + float d = bot->GetDistance2d(portal); + if (d < 3.0f && d < minDist) + { + nearestRealPortal = portal; + minDist = d; + } + } + + if (nearestRealPortal) + { + botAI->RemoveShapeshift(); + bot->GetMotionMaster()->Clear(); + bot->StopMoving(); + bot->SetFacingToObject(nearestRealPortal); + nearestRealPortal->HandleSpellClick(bot); + return true; + } + + // If no real portal is close, wait at the position + return false; + } + + // If no pre-effect portals, try to find a real portal within 3f + Creature* nearestRealPortal = nullptr; + float minDist = 9999.0f; + for (Creature* portal : realPortals) + { + float d = bot->GetDistance2d(portal); + if (d < 3.0f && d < minDist) + { + nearestRealPortal = portal; + minDist = d; + } + } + + if (nearestRealPortal && minDist > 2.0f) + MoveTo(bot->GetMapId(), nearestRealPortal->GetPositionX(), nearestRealPortal->GetPositionY(), + nearestRealPortal->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_NORMAL); + + if (nearestRealPortal) + { + botAI->RemoveShapeshift(); + bot->GetMotionMaster()->Clear(); + bot->StopMoving(); + bot->SetFacingToObject(nearestRealPortal); + nearestRealPortal->HandleSpellClick(bot); + return true; + } return false; } bool IccValithriaHealAction::Execute(Event event) { - if (!botAI->IsHeal(bot)) + // Early validation checks + if (!botAI->IsHeal(bot) || bot->GetHealthPct() < 50.0f) return false; - if (bot->GetHealthPct() < 50.0f) - return false; - - if (!bot->HasAura(70766)) //dream state + // Handle movement speed when not in dream state + if (!bot->HasAura(SPELL_DREAM_STATE)) { - bot->SetSpeed(MOVE_RUN, 1.0f, true); - bot->SetSpeed(MOVE_WALK, 1.0f, true); - bot->SetSpeed(MOVE_FLIGHT, 1.0f, true); + constexpr float NORMAL_SPEED = 1.0f; + bot->SetSpeed(MOVE_RUN, NORMAL_SPEED, true); + bot->SetSpeed(MOVE_WALK, NORMAL_SPEED, true); + bot->SetSpeed(MOVE_FLIGHT, NORMAL_SPEED, true); } - // Find Valithria - - if (bot->GetPositionZ() > 367.961f) - return bot->TeleportTo(bot->GetMapId(), bot->GetPositionX(), - bot->GetPositionY(), 365.0f, bot->GetOrientation()); - if (Creature* valithria = bot->FindNearestCreature(36789, 100.0f)) + // Enforce Z-position limit + constexpr float MAX_Z_POSITION = 367.961f; + constexpr float TARGET_Z_POSITION = 365.0f; + if (bot->GetPositionZ() > MAX_Z_POSITION) + bot->TeleportTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), TARGET_Z_POSITION, + bot->GetOrientation()); + + // Find Valithria within range + Creature* valithria = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); + if (!valithria) + return false; + + // Execute class-specific healing logic + switch (bot->getClass()) { - switch (bot->getClass()) + case CLASS_DRUID: { - case CLASS_DRUID: - { - // Check for Rejuvenation (48441) - if (!valithria->HasAura(48441)) - return botAI->CastSpell(48441, valithria); + // Druid healing spell constants + constexpr uint32 SPELL_REJUVENATION = 48441; + constexpr uint32 SPELL_REGROWTH = 48443; + constexpr uint32 SPELL_LIFEBLOOM = 48451; + constexpr uint32 SPELL_WILD_GROWTH = 53251; + constexpr uint8 LIFEBLOOM_MAX_STACKS = 3; - // Check for Regrowth (48443) - if (!valithria->HasAura(48443)) - return botAI->CastSpell(48443, valithria); + // Apply Rejuvenation if missing + if (!valithria->HasAura(SPELL_REJUVENATION, bot->GetGUID())) + return botAI->CastSpell(SPELL_REJUVENATION, valithria); - // Check for Lifebloom stacks (48451) - Aura* lifebloom = valithria->GetAura(48451); - if (!lifebloom || lifebloom->GetStackAmount() < 3) - return botAI->CastSpell(48451, valithria); + // Apply Regrowth if missing + if (!valithria->HasAura(SPELL_REGROWTH, bot->GetGUID())) + return botAI->CastSpell(SPELL_REGROWTH, valithria); - // If all HoTs are up with full stacks, cast Wild Growth (53251) - return botAI->CastSpell(53251, valithria); - } - case CLASS_SHAMAN: - return valithria->HasAura(61301) ? botAI->CastSpell(49273, valithria) : botAI->CastSpell(61301, valithria); // Cast Healing Wave if Riptide is up, otherwise cast Riptide - case CLASS_PRIEST: - return valithria->HasAura(48068) ? botAI->CastSpell(48063, valithria) : botAI->CastSpell(48068, valithria); // Cast Greater Heal if Renew is up, otherwise cast Renew - case CLASS_PALADIN: - return valithria->HasAura(53563) ? botAI->CastSpell(48782, valithria) : botAI->CastSpell(53563, valithria); // Cast Holy Light if Beacon is up, otherwise cast Beacon of Light - default: - return false; + // Stack Lifebloom to maximum stacks + Aura* lifebloom = valithria->GetAura(SPELL_LIFEBLOOM, bot->GetGUID()); + if (!lifebloom || lifebloom->GetStackAmount() < LIFEBLOOM_MAX_STACKS) + return botAI->CastSpell(SPELL_LIFEBLOOM, valithria); + + // All HoTs active with full stacks - cast Wild Growth + return botAI->CastSpell(SPELL_WILD_GROWTH, valithria); } + case CLASS_SHAMAN: + { + constexpr uint32 SPELL_RIPTIDE = 61301; + constexpr uint32 SPELL_HEALING_WAVE = 49273; + + // Cast Healing Wave if Riptide is active, otherwise apply Riptide + return valithria->HasAura(SPELL_RIPTIDE, bot->GetGUID()) ? botAI->CastSpell(SPELL_HEALING_WAVE, valithria) + : botAI->CastSpell(SPELL_RIPTIDE, valithria); + } + case CLASS_PRIEST: + { + constexpr uint32 SPELL_RENEW = 48068; + constexpr uint32 SPELL_GREATER_HEAL = 48063; + + // Cast Greater Heal if Renew is active, otherwise apply Renew + return valithria->HasAura(SPELL_RENEW, bot->GetGUID()) ? botAI->CastSpell(SPELL_GREATER_HEAL, valithria) + : botAI->CastSpell(SPELL_RENEW, valithria); + } + case CLASS_PALADIN: + { + constexpr uint32 SPELL_BEACON_OF_LIGHT = 53563; + constexpr uint32 SPELL_HOLY_LIGHT = 48782; + + // Cast Holy Light if Beacon is active, otherwise apply Beacon of Light + return valithria->HasAura(SPELL_BEACON_OF_LIGHT, bot->GetGUID()) + ? botAI->CastSpell(SPELL_HOLY_LIGHT, valithria) + : botAI->CastSpell(SPELL_BEACON_OF_LIGHT, valithria); + } + default: + return false; } return false; @@ -2585,185 +5193,375 @@ bool IccValithriaHealAction::Execute(Event event) bool IccValithriaDreamCloudAction::Execute(Event event) { // Only execute if we're in dream state - if (!bot->HasAura(70766)) + if (!bot->HasAura(SPELL_DREAM_STATE)) return false; // Set speed to match players in dream state - if (bot->HasAura(70766)) - { - bot->SetSpeed(MOVE_RUN, 2.0f, true); - bot->SetSpeed(MOVE_WALK, 2.0f, true); - bot->SetSpeed(MOVE_FLIGHT, 2.0f, true); - } - + bot->SetSpeed(MOVE_RUN, 2.0f, true); + bot->SetSpeed(MOVE_WALK, 2.0f, true); + bot->SetSpeed(MOVE_FLIGHT, 2.0f, true); - // Find nearest cloud of either type that we haven't collected - Creature* dreamCloud = bot->FindNearestCreature(37985, 100.0f); - Creature* nightmareCloud = bot->FindNearestCreature(38421, 100.0f); - - // If we have emerald vigor, prioritize dream clouds - if (bot->HasAura(70873)) + // Gather all group members with dream state + const GuidVector members = AI_VALUE(GuidVector, "group members"); + std::vector dreamBots; + for (const auto& guid : members) { - if (dreamCloud) - return MoveTo(dreamCloud->GetMapId(), dreamCloud->GetPositionX(), dreamCloud->GetPositionY(), dreamCloud->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_NORMAL); - if (nightmareCloud) - return MoveTo(nightmareCloud->GetMapId(), nightmareCloud->GetPositionX(), nightmareCloud->GetPositionY(), nightmareCloud->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_NORMAL); + Unit* member = botAI->GetUnit(guid); + if (member && member->IsAlive() && member->HasAura(SPELL_DREAM_STATE)) + dreamBots.push_back(member); + } + + if (dreamBots.empty()) + return false; + + // Sort dreamBots by GUID (lowest first) + std::sort(dreamBots.begin(), dreamBots.end(), [](Unit* a, Unit* b) { return a->GetGUID() < b->GetGUID(); }); + + // Find this bot's index in the sorted list + auto it = std::find(dreamBots.begin(), dreamBots.end(), bot); + if (it == dreamBots.end()) + return false; + size_t myIndex = std::distance(dreamBots.begin(), it); + + // Check if all dream bots are stacked within 3f of the current leader (lowest guid) + constexpr float STACK_RADIUS = 5.0f; + Unit* leader = dreamBots.front(); + bool allStacked = true; + for (Unit* member : dreamBots) + { + // Only require stacking for bots, not real players + Player* player = member->ToPlayer(); + if (player && !player->GetSession()) // is a bot + { + if (member->GetExactDist2d(leader) > STACK_RADIUS) + { + allStacked = false; + break; + } + } + } + + // If not all stacked, everyone moves to the leader's position (clouds' position) + constexpr float PORTALSTART_TOLERANCE = 5.0f; + if (!allStacked) + { + if (bot != leader) + { + if (bot->GetExactDist2d(leader) > PORTALSTART_TOLERANCE) + { + MoveTo(bot->GetMapId(), leader->GetPositionX(), leader->GetPositionY(), leader->GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_NORMAL); + } + // Wait at leader's position until all are stacked + return true; + } + // Leader waits for others to stack + return true; + } + + // All stacked: leader (lowest guid) moves to next cloud, others follow and stack at leader's new position + Creature* dreamCloud = bot->FindNearestCreature(NPC_DREAM_CLOUD, 100.0f); + Creature* nightmareCloud = bot->FindNearestCreature(NPC_NIGHTMARE_CLOUD, 100.0f); + + // Only the leader moves to the next cloud + if (bot == leader) + { + // If we have emerald vigor, prioritize dream clouds + if (bot->HasAura(SPELL_EMERALD_VIGOR)) + { + if (dreamCloud && bot->GetExactDist2d(dreamCloud) > 2.0f) + MoveTo(dreamCloud->GetMapId(), dreamCloud->GetPositionX(), dreamCloud->GetPositionY(), + dreamCloud->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_NORMAL); + if (nightmareCloud && bot->GetExactDist2d(nightmareCloud) > 2.0f) + MoveTo(nightmareCloud->GetMapId(), nightmareCloud->GetPositionX(), + nightmareCloud->GetPositionY(), nightmareCloud->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_NORMAL); + } + // Otherwise prioritize nightmare clouds + else + { + if (nightmareCloud && bot->GetExactDist2d(nightmareCloud) > 2.0f) + MoveTo(nightmareCloud->GetMapId(), nightmareCloud->GetPositionX(), + nightmareCloud->GetPositionY(), nightmareCloud->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_NORMAL); + + if (dreamCloud && bot->GetExactDist2d(dreamCloud) > 2.0f) + MoveTo(dreamCloud->GetMapId(), dreamCloud->GetPositionX(), dreamCloud->GetPositionY(), + dreamCloud->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_NORMAL); + } } - // Otherwise prioritize nightmare clouds else { - if (nightmareCloud) - return MoveTo(nightmareCloud->GetMapId(), nightmareCloud->GetPositionX(), nightmareCloud->GetPositionY(), nightmareCloud->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_NORMAL); - if (dreamCloud) - return MoveTo(dreamCloud->GetMapId(), dreamCloud->GetPositionX(), dreamCloud->GetPositionY(), dreamCloud->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_NORMAL); + // Non-leader bots follow and stack at leader's position + if (bot->GetExactDist2d(leader) > PORTALSTART_TOLERANCE) + { + MoveTo(bot->GetMapId(), leader->GetPositionX(), leader->GetPositionY(), leader->GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_NORMAL); + } + return true; } return false; } -//Sindragosa - -bool IccSindragosaTankPositionAction::Execute(Event event) +// Sindragosa +bool IccSindragosaGroupPositionAction::Execute(Event event) { Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); if (!boss || boss->HasUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY)) return false; - if ((botAI->IsTank(bot) || botAI->IsMainTank(bot) || botAI->IsAssistTank(bot)) && (boss->GetVictim() == bot)) + Aura* aura = botAI->GetAura("mystic buffet", bot, false, true); + + if (aura && aura->GetStackAmount() >= 6 && botAI->IsMainTank(bot)) + return false; + + if (botAI->IsTank(bot) && boss->GetVictim() == bot) + return HandleTankPositioning(boss); + + if (boss && boss->GetVictim() != bot) + return HandleNonTankPositioning(); + + return false; +} + +bool IccSindragosaGroupPositionAction::HandleTankPositioning(Unit* boss) +{ + float distBossToCenter = boss->GetExactDist2d(ICC_SINDRAGOSA_CENTER_POSITION); + float distToTankPos = bot->GetExactDist2d(ICC_SINDRAGOSA_TANK_POSITION); + float targetOrientation = M_PI / 2; // We want boss to face east + float currentOrientation = boss->GetOrientation(); + + // Normalize both orientations to 0-2π range + currentOrientation = fmod(currentOrientation + 2 * M_PI, 2 * M_PI); + targetOrientation = fmod(targetOrientation + 2 * M_PI, 2 * M_PI); + + float orientationDiff = currentOrientation - targetOrientation; + + // Normalize the difference to be between -PI and PI + while (orientationDiff > M_PI) + orientationDiff -= 2 * M_PI; + while (orientationDiff < -M_PI) + orientationDiff += 2 * M_PI; + + // Stage 1: Move boss to center if too far + if (boss && boss->GetVictim() == bot && distBossToCenter > 16.0f && distToTankPos <= 20.0f) { - float distBossToCenter = boss->GetExactDist2d(ICC_SINDRAGOSA_CENTER_POSITION); - float distToTankPos = bot->GetExactDist2d(ICC_SINDRAGOSA_TANK_POSITION); - float targetOrientation = M_PI / 2; // We want boss to face east - float currentOrientation = boss->GetOrientation(); - - // Normalize both orientations to 0-2π range - currentOrientation = fmod(currentOrientation + 2 * M_PI, 2 * M_PI); - targetOrientation = fmod(targetOrientation + 2 * M_PI, 2 * M_PI); - - float orientationDiff = currentOrientation - targetOrientation; - - // Normalize the difference to be between -PI and PI - while (orientationDiff > M_PI) orientationDiff -= 2 * M_PI; - while (orientationDiff < -M_PI) orientationDiff += 2 * M_PI; - - - // Stage 1: Move boss to center if too far - if (distBossToCenter > 20.0f) + // Calculate direction vector from boss to center + float dirX = ICC_SINDRAGOSA_CENTER_POSITION.GetPositionX() - boss->GetPositionX(); + float dirY = ICC_SINDRAGOSA_CENTER_POSITION.GetPositionY() - boss->GetPositionY(); + + // Move 10 yards beyond center in the same direction + float moveX = ICC_SINDRAGOSA_CENTER_POSITION.GetPositionX() + (dirX / distBossToCenter) * 4.0f; + float moveY = ICC_SINDRAGOSA_CENTER_POSITION.GetPositionY() + (dirY / distBossToCenter) * 4.0f; + + return MoveTo(bot->GetMapId(), moveX, moveY, boss->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + // Stage 2: Move to tank position if too far + if (boss && boss->GetVictim() == bot && distToTankPos > 10.0f) + { + Position botPos = bot->GetPosition(); + Position tankPos = ICC_SINDRAGOSA_TANK_POSITION; + + float dx = tankPos.GetPositionX() - botPos.GetPositionX(); + float dy = tankPos.GetPositionY() - botPos.GetPositionY(); + + float distance = std::sqrt(dx * dx + dy * dy); + float step = 1.0f; + + // Normalize and scale direction vector + float scale = step / distance; + + float targetX = botPos.GetPositionX() + dx * scale; + float targetY = botPos.GetPositionY() + dy * scale; + + return MoveTo(bot->GetMapId(), targetX, targetY, bot->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + // Stage 3: Adjust orientation when in position + if (boss && boss->GetVictim() == bot && std::abs(orientationDiff) > 0.15f) + { + // Move in an arc (circle) north or south around the boss until the orientation matches + float currentX = bot->GetPositionX(); + float currentY = bot->GetPositionY(); + float centerX = boss->GetPositionX(); + float centerY = boss->GetPositionY(); + float radius = std::max(2.0f, bot->GetExactDist2d(centerX, centerY)); // keep at least 2 yards from boss + + // Calculate current angle from boss to bot + float angle = atan2(currentY - centerY, currentX - centerX); + + // Determine direction: negative diff = move counterclockwise (north), positive = clockwise (south) + float arcStep = 0.125f; // radians per move, adjust for smoothness + if (orientationDiff < 0) + angle += arcStep; // move north (counterclockwise) + else + angle -= arcStep; // move south (clockwise) + + // Calculate new position on the arc + float moveX = centerX + radius * cos(angle); + float moveY = centerY + radius * sin(angle); + + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + // Stage 4: Adjust Y-axis position if too far from tank position + float yDiff = std::abs(bot->GetPositionY() - ICC_SINDRAGOSA_TANK_POSITION.GetPositionY()); + if (boss && boss->GetVictim() == bot && yDiff > 2.0f) + { + Position botPos = bot->GetPosition(); + Position tankPos = ICC_SINDRAGOSA_TANK_POSITION; + + // Only adjust Y position, keep X and Z the same + float newY = botPos.GetPositionY() + (tankPos.GetPositionY() > botPos.GetPositionY() ? 1.0f : -1.0f); + + return MoveTo(bot->GetMapId(), botPos.GetPositionX(), newY, botPos.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool IccSindragosaGroupPositionAction::HandleNonTankPositioning() +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + // Collect all alive raid members + std::vector raidMembers; + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive()) + continue; + raidMembers.push_back(member); + } + + // Count members without aura 1111 + size_t membersWithoutAura = 0; + for (Player* member : raidMembers) + { + if (!botAI->GetAura("mystic buffet", member)) + membersWithoutAura++; + } + + // Calculate percentage without aura + size_t totalMembers = raidMembers.size(); + if (totalMembers == 0) + return false; + + double percentageWithoutAura = static_cast(membersWithoutAura) / totalMembers; + bool raidClear = (percentageWithoutAura >= 0.6); // 60% or more don't have aura 1111 + + if (raidClear && botAI->IsTank(bot)) + { + static const std::array tombEntries = {NPC_TOMB1, NPC_TOMB2, NPC_TOMB3, NPC_TOMB4}; + const GuidVector tombGuids = AI_VALUE(GuidVector, "possible targets no los"); + + Unit* nearestTomb = nullptr; + float minDist = 150.0f; + + for (const auto entry : tombEntries) { - - // Calculate direction vector from boss to center - float dirX = ICC_SINDRAGOSA_CENTER_POSITION.GetPositionX() - boss->GetPositionX(); - float dirY = ICC_SINDRAGOSA_CENTER_POSITION.GetPositionY() - boss->GetPositionY(); - - // Move 10 yards beyond center in the same direction - float moveX = ICC_SINDRAGOSA_CENTER_POSITION.GetPositionX() + (dirX / distBossToCenter) * 10.0f; - float moveY = ICC_SINDRAGOSA_CENTER_POSITION.GetPositionY() + (dirY / distBossToCenter) * 10.0f; - - return MoveTo(bot->GetMapId(), moveX, moveY, boss->GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); - } - - // Stage 2: Get to tank position when boss is centered - if (distToTankPos > 5.0f) - { - return MoveTo(bot->GetMapId(), ICC_SINDRAGOSA_TANK_POSITION.GetPositionX(), - ICC_SINDRAGOSA_TANK_POSITION.GetPositionY(), - ICC_SINDRAGOSA_TANK_POSITION.GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); - } - - // Stage 3: Adjust orientation when in position - bool needsOrientationAdjust = std::abs(orientationDiff) > 0.15f; - if (needsOrientationAdjust) - { - // When we have negative difference (currentOrientation < targetOrientation) - // We need to move south to make the orientation more positive - float currentX = bot->GetPositionX(); - float currentY = bot->GetPositionY(); - float moveX, moveY; - - // For negative difference (need to increase orientation) -> move south - // For positive difference (need to decrease orientation) -> move north - if (orientationDiff < 0) + for (const auto& guid : tombGuids) { - moveX = currentX - 2.0f; // Move south to increase orientation - moveY = currentY; + if (Unit* unit = botAI->GetUnit(guid)) + { + if (unit->GetEntry() == entry && unit->IsAlive()) + { + float dist = bot->GetDistance(unit); + if (dist < minDist) + { + minDist = dist; + nearestTomb = unit; + } + } + } } - else - { - moveX = currentX + 2.0f; // Move north to decrease orientation - moveY = currentY; - } - - return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); } + + static constexpr uint8_t SKULL_ICON_INDEX = 7; + + Group* group = bot->GetGroup(); + if (!group) + return false; // Cannot assign icon without group + + Unit* targetToMark = nearestTomb; + + // Fallback: mark boss if no tomb is found + if (!targetToMark) + { + Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); + if (boss && boss->IsAlive()) + targetToMark = boss; + } + + if (targetToMark) + { + const ObjectGuid currentSkull = group->GetTargetIcon(SKULL_ICON_INDEX); + Unit* currentSkullUnit = botAI->GetUnit(currentSkull); + + const bool needsUpdate = + !currentSkullUnit || !currentSkullUnit->IsAlive() || currentSkullUnit != targetToMark; + + if (needsUpdate) + group->SetTargetIcon(SKULL_ICON_INDEX, bot->GetGUID(), targetToMark->GetGUID()); + } + } + + context->GetValue("rti")->Set("skull"); + if (botAI->IsRanged(bot)) + { + const float TOLERANCE = 9.0f; + const float MAX_STEP = 5.0f; + + float distToTarget = bot->GetExactDist2d(ICC_SINDRAGOSA_RANGED_POSITION); + + // Only move if outside tolerance + if (distToTarget > TOLERANCE) + return MoveIncrementallyToPosition(ICC_SINDRAGOSA_RANGED_POSITION, MAX_STEP); + return false; } - - // Non-tanks should stay on the left flank to avoid both cleave and tail smash - if (boss && !(boss->GetVictim() == bot) /*&& !bot->HasAura(69762)*/) + else { - if (botAI->IsRanged(bot)) - { - float const TOLERANCE = 15.0f; // 15 yard tolerance - float const MAX_STEP = 5.0f; // Maximum distance to move in one step - - float distToTarget = bot->GetExactDist2d(ICC_SINDRAGOSA_RANGED_POSITION); + const float TOLERANCE = 10.0f; + const float MAX_STEP = 5.0f; - // Only move if outside tolerance - if (distToTarget > TOLERANCE) - { - // Calculate direction vector to target - float dirX = ICC_SINDRAGOSA_RANGED_POSITION.GetPositionX() - bot->GetPositionX(); - float dirY = ICC_SINDRAGOSA_RANGED_POSITION.GetPositionY() - bot->GetPositionY(); - - // Normalize direction vector - float length = sqrt(dirX * dirX + dirY * dirY); - dirX /= length; - dirY /= length; + float distToTarget = bot->GetExactDist2d(ICC_SINDRAGOSA_MELEE_POSITION); - // Calculate intermediate point - float stepSize = std::min(MAX_STEP, distToTarget); - float moveX = bot->GetPositionX() + dirX * stepSize; - float moveY = bot->GetPositionY() + dirY * stepSize; - - return MoveTo(bot->GetMapId(), moveX, moveY, ICC_SINDRAGOSA_RANGED_POSITION.GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_COMBAT); - } - return false; - } - else - { - float const TOLERANCE = 10.0f; // 10 yard tolerance for melee - float const MAX_STEP = 5.0f; // Maximum distance to move in one step + // Only move if outside tolerance + if (distToTarget > TOLERANCE) + return MoveIncrementallyToPosition(ICC_SINDRAGOSA_MELEE_POSITION, MAX_STEP); - float distToTarget = bot->GetExactDist2d(ICC_SINDRAGOSA_MELEE_POSITION); - - // Only move if outside tolerance - if (distToTarget > TOLERANCE) - { - // Calculate direction vector to target - float dirX = ICC_SINDRAGOSA_MELEE_POSITION.GetPositionX() - bot->GetPositionX(); - float dirY = ICC_SINDRAGOSA_MELEE_POSITION.GetPositionY() - bot->GetPositionY(); - - // Normalize direction vector - float length = sqrt(dirX * dirX + dirY * dirY); - dirX /= length; - dirY /= length; - - // Calculate intermediate point - float stepSize = std::min(MAX_STEP, distToTarget); - float moveX = bot->GetPositionX() + dirX * stepSize; - float moveY = bot->GetPositionY() + dirY * stepSize; - - return MoveTo(bot->GetMapId(), moveX, moveY, ICC_SINDRAGOSA_MELEE_POSITION.GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_COMBAT); - } - return false; - } + return false; } - return false; +} + +bool IccSindragosaGroupPositionAction::MoveIncrementallyToPosition(const Position& targetPos, float maxStep) +{ + // Calculate direction vector to target + float dirX = targetPos.GetPositionX() - bot->GetPositionX(); + float dirY = targetPos.GetPositionY() - bot->GetPositionY(); + + // Normalize direction vector + float length = sqrt(dirX * dirX + dirY * dirY); + dirX /= length; + dirY /= length; + + // Calculate intermediate point + float distToTarget = bot->GetExactDist2d(targetPos); + float stepSize = std::min(maxStep, distToTarget); + float moveX = bot->GetPositionX() + dirX * stepSize; + float moveY = bot->GetPositionY() + dirY * stepSize; + + return MoveTo(bot->GetMapId(), moveX, moveY, targetPos.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); } bool IccSindragosaTankSwapPositionAction::Execute(Event event) @@ -2792,183 +5590,233 @@ bool IccSindragosaTankSwapPositionAction::Execute(Event event) bool IccSindragosaFrostBeaconAction::Execute(Event event) { - float const POSITION_TOLERANCE = 1.0f; - - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); + const Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); if (!boss) return false; - - // Handle beaconed players - if (bot->HasAura(70126)) + + HandleSupportActions(); + + if (bot->HasAura(FROST_BEACON_AURA_ID)) { - if (boss && boss->HealthBelowPct(35) && !boss->HasUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY)) - { - // Only move if not already at position (with tolerance) - if (bot->GetExactDist2d(ICC_SINDRAGOSA_THOMBMB2_POSITION.GetPositionX(), ICC_SINDRAGOSA_THOMBMB2_POSITION.GetPositionY()) > POSITION_TOLERANCE) - { - return MoveTo(bot->GetMapId(), - ICC_SINDRAGOSA_THOMBMB2_POSITION.GetPositionX(), - ICC_SINDRAGOSA_THOMBMB2_POSITION.GetPositionY(), - ICC_SINDRAGOSA_THOMBMB2_POSITION.GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); - } - return false; - } - else - { - Position const* tombPosition; - uint8 beaconIndex = 0; - bool foundSelf = false; - - Group* group = bot->GetGroup(); - if (!group) - return false; - - // Find this bot's index among players with Frost Beacon - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || !member->HasAura(70126)) // Only count alive players with Frost Beacon - continue; - - if (member == bot) - { - foundSelf = true; - break; - } - beaconIndex++; - } - - if (!foundSelf) - return false; - - switch (beaconIndex) { - case 0: - tombPosition = &ICC_SINDRAGOSA_THOMB1_POSITION; - break; - case 1: - tombPosition = &ICC_SINDRAGOSA_THOMB2_POSITION; - break; - case 2: - tombPosition = &ICC_SINDRAGOSA_THOMB3_POSITION; - break; - case 3: - tombPosition = &ICC_SINDRAGOSA_THOMB4_POSITION; - break; - default: - tombPosition = &ICC_SINDRAGOSA_THOMB5_POSITION; - break; - } - - // Only move if not already at position (with tolerance) - float dist = bot->GetExactDist2d(tombPosition->GetPositionX(), tombPosition->GetPositionY()); - if (dist > POSITION_TOLERANCE) - { - return MoveTo(bot->GetMapId(), tombPosition->GetPositionX(), - tombPosition->GetPositionY(), - tombPosition->GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); - } - return false; - } + return HandleBeaconedPlayer(boss); } - // Handle non-beaconed players - else + + return HandleNonBeaconedPlayer(boss); +} + +void IccSindragosaFrostBeaconAction::HandleSupportActions() +{ + Group* group = bot->GetGroup(); + + // Tank support - Paladin Hand of Freedom + if (group && bot->getClass() == CLASS_PALADIN) { - float const MIN_SAFE_DISTANCE = 13.0f; - float const MAX_SAFE_DISTANCE = 30.0f; - float const MOVE_TOLERANCE = 2.0f; // Tolerance for movement to reduce jitter - - GuidVector members = AI_VALUE(GuidVector, "group members"); - std::vector beaconedPlayers; - - for (auto& member : members) + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) { - Unit* player = botAI->GetUnit(member); - if (!player || player->GetGUID() == bot->GetGUID()) + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || !botAI->IsTank(member)) + { continue; - - if (player->HasAura(70126)) // Frost Beacon - beaconedPlayers.push_back(player); - } - - if (beaconedPlayers.empty()) - return false; + } - // Different behavior for air phase - Difficulty diff = bot->GetRaidDifficulty(); - if (boss->HasUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY) && (diff == RAID_DIFFICULTY_25MAN_NORMAL || diff == RAID_DIFFICULTY_25MAN_HEROIC)) - { - if (!bot->HasAura(70126)) // If not beaconed, move to safe position + if (botAI->GetAura("Frost Breath", member) && !member->HasAura(HAND_OF_FREEDOM_SPELL_ID)) { - float dist = bot->GetExactDist2d(ICC_SINDRAGOSA_FBOMB_POSITION.GetPositionX(), - ICC_SINDRAGOSA_FBOMB_POSITION.GetPositionY()); - if (dist > POSITION_TOLERANCE) - { - return MoveTo(bot->GetMapId(), ICC_SINDRAGOSA_FBOMB_POSITION.GetPositionX(), - ICC_SINDRAGOSA_FBOMB_POSITION.GetPositionY(), - ICC_SINDRAGOSA_FBOMB_POSITION.GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); - } - } - return false; - } - else if (boss->HasUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY) && (diff == RAID_DIFFICULTY_10MAN_NORMAL || diff == RAID_DIFFICULTY_10MAN_HEROIC)) - { - if (!bot->HasAura(70126)) // If not beaconed, move to safe position - { - float dist = bot->GetExactDist2d(ICC_SINDRAGOSA_FBOMB10_POSITION.GetPositionX(), - ICC_SINDRAGOSA_FBOMB10_POSITION.GetPositionY()); - if (dist > POSITION_TOLERANCE) - { - return MoveTo(bot->GetMapId(), ICC_SINDRAGOSA_FBOMB10_POSITION.GetPositionX(), - ICC_SINDRAGOSA_FBOMB10_POSITION.GetPositionY(), - ICC_SINDRAGOSA_FBOMB10_POSITION.GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); - } - } - return false; - } - else - { - // Ground phase - use existing vector-based movement - bool needToMove = false; - float moveX = 0, moveY = 0; - - for (Unit* beaconedPlayer : beaconedPlayers) - { - float dist = bot->GetExactDist2d(beaconedPlayer); - if (dist < MIN_SAFE_DISTANCE + MOVE_TOLERANCE) - { - needToMove = true; - float angle = bot->GetAngle(beaconedPlayer); - float moveDistance = std::min(5.0f, MIN_SAFE_DISTANCE - dist + MOVE_TOLERANCE); - - moveX += cos(angle + M_PI) * moveDistance; - moveY += sin(angle + M_PI) * moveDistance; - } - } - - if (needToMove && !bot->HasAura(70126)) - { - float posX = bot->GetPositionX() + moveX; - float posY = bot->GetPositionY() + moveY; - float posZ = bot->GetPositionZ(); - bot->UpdateAllowedPositionZ(posX, posY, posZ); - - // Only move if the change in position is significant - if (std::abs(moveX) > MOVE_TOLERANCE || std::abs(moveY) > MOVE_TOLERANCE) - { - return MoveTo(bot->GetMapId(), posX, posY, posZ, - false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); - } + botAI->CastSpell(HAND_OF_FREEDOM_SPELL_ID, member); + break; } } } + // Healer support - Apply HoTs to beaconed players + if (botAI->IsHeal(bot) && !bot->HasAura(FROST_BEACON_AURA_ID)) + { + const auto members = AI_VALUE(GuidVector, "group members"); + for (const auto& memberGuid : members) + { + Unit* member = botAI->GetUnit(memberGuid); + if (!member || !member->IsAlive() || !member->HasAura(FROST_BEACON_AURA_ID)) + { + continue; + } + + // Apply class-specific HoT spells + uint32 spellId = 0; + switch (bot->getClass()) + { + case CLASS_PRIEST: + spellId = 48068; + break; // Renew + case CLASS_SHAMAN: + spellId = 61301; + break; // Riptide + case CLASS_DRUID: + spellId = 48441; + break; // Rejuvenation + default: + continue; + } + + if (!member->HasAura(spellId)) + { + botAI->CastSpell(spellId, member); + } + } + } +} + +bool IccSindragosaFrostBeaconAction::HandleBeaconedPlayer(const Unit* boss) +{ + // Phase 3 positioning (below 35% health, not flying) + if (boss->HealthBelowPct(35) && !IsBossFlying(boss)) + { + return MoveToPositionIfNeeded(ICC_SINDRAGOSA_THOMBMB2_POSITION, POSITION_TOLERANCE); + } + + // Regular beacon positioning using tomb spots + Group* group = bot->GetGroup(); + if (!group) + { + return false; + } + + // Collect and sort beaconed players by GUID for deterministic assignment + std::vector beaconedPlayers; + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && member->IsAlive() && member->HasAura(FROST_BEACON_AURA_ID)) + { + beaconedPlayers.push_back(member); + } + } + + std::sort(beaconedPlayers.begin(), beaconedPlayers.end(), + [](const Player* a, const Player* b) { return a->GetGUID() < b->GetGUID(); }); + + // Find this bot's index + const auto it = std::find(beaconedPlayers.begin(), beaconedPlayers.end(), bot); + if (it == beaconedPlayers.end()) + { + return false; + } + + const size_t myIndex = std::distance(beaconedPlayers.begin(), it); + const size_t beaconCount = beaconedPlayers.size(); + + // Calculate tomb spot based on beacon count + size_t spot = 0; + switch (beaconCount) + { + case 2: + spot = (myIndex == 0) ? 0 : 2; + break; + case 5: + spot = (myIndex < 2) ? 0 : ((myIndex == 2) ? 1 : 2); + break; + case 6: + spot = myIndex / 2; + break; + default: + spot = myIndex % 3; + break; + } + + // Get tomb position and move if needed + static constexpr std::array tombPositions = { + &ICC_SINDRAGOSA_THOMB1_POSITION, &ICC_SINDRAGOSA_THOMB2_POSITION, &ICC_SINDRAGOSA_THOMB3_POSITION}; + + const Position& tombPosition = *tombPositions[std::min(spot, tombPositions.size() - 1)]; + return MoveToPositionIfNeeded(tombPosition, TOMB_POSITION_TOLERANCE); +} + +bool IccSindragosaFrostBeaconAction::HandleNonBeaconedPlayer(const Unit* boss) +{ + // Collect beaconed players + std::vector beaconedPlayers; + const auto members = AI_VALUE(GuidVector, "group members"); + for (const auto& memberGuid : members) + { + Unit* player = botAI->GetUnit(memberGuid); + if (player && player->GetGUID() != bot->GetGUID() && player->HasAura(FROST_BEACON_AURA_ID)) + { + beaconedPlayers.push_back(player); + } + } + + if (beaconedPlayers.empty()) + { + return false; + } + + // Air phase positioning + if (IsBossFlying(boss)) + { + if (!bot->HasAura(FROST_BEACON_AURA_ID)) + { + const Difficulty diff = bot->GetRaidDifficulty(); + const bool is25Man = (diff == RAID_DIFFICULTY_25MAN_NORMAL) || (diff == RAID_DIFFICULTY_25MAN_HEROIC); + + const Position& safePosition = is25Man ? ICC_SINDRAGOSA_FBOMB_POSITION : ICC_SINDRAGOSA_FBOMB10_POSITION; + + const float dist = bot->GetExactDist2d(safePosition.GetPositionX(), safePosition.GetPositionY()); + if (dist > MOVE_TOLERANCE) + { + return MoveToPosition(safePosition); + } + } + return botAI->IsHeal(bot); // Continue for healers, wait for others + } + + // Ground phase - position based on role and avoid beaconed players + const bool isRanged = botAI->IsRanged(bot) || (bot->GetExactDist2d(ICC_SINDRAGOSA_RANGED_POSITION.GetPositionX(),ICC_SINDRAGOSA_RANGED_POSITION.GetPositionY()) < + bot->GetExactDist2d(ICC_SINDRAGOSA_MELEE_POSITION.GetPositionX(),ICC_SINDRAGOSA_MELEE_POSITION.GetPositionY())); + + const Position& targetPosition = isRanged ? ICC_SINDRAGOSA_RANGED_POSITION : ICC_SINDRAGOSA_MELEE_POSITION; + + const float deltaX = std::abs(targetPosition.GetPositionX() - bot->GetPositionX()); + const float deltaY = std::abs(targetPosition.GetPositionY() - bot->GetPositionY()); + if (boss && boss->GetVictim() != bot) + { + if ((deltaX > MOVE_TOLERANCE) || (deltaY > MOVE_TOLERANCE)) + { + if (bot->HasUnitState(UNIT_STATE_CASTING)) + { + botAI->Reset(); + } + return MoveToPosition(targetPosition); + } + } return false; } +bool IccSindragosaFrostBeaconAction::MoveToPositionIfNeeded(const Position& position, float tolerance) +{ + const float distance = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + if (distance > tolerance) + { + return MoveToPosition(position); + } + return distance <= tolerance; +} + +bool IccSindragosaFrostBeaconAction::MoveToPosition(const Position& position) +{ + float posX = position.GetPositionX(); + float posY = position.GetPositionY(); + float posZ = position.GetPositionZ(); + + bot->UpdateAllowedPositionZ(posX, posY, posZ); + + return MoveTo(bot->GetMapId(), posX, posY, posZ, false, false, false, false, MovementPriority::MOVEMENT_FORCED, + true, false); +} + +bool IccSindragosaFrostBeaconAction::IsBossFlying(const Unit* boss) +{ + return boss->GetExactDist2d(ICC_SINDRAGOSA_FLYING_POSITION.GetPositionX(), + ICC_SINDRAGOSA_FLYING_POSITION.GetPositionY()) < 30.0f; +} + bool IccSindragosaBlisteringColdAction::Execute(Event event) { Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); @@ -2979,28 +5827,24 @@ bool IccSindragosaBlisteringColdAction::Execute(Event event) if (botAI->IsMainTank(bot)) return false; - float dist = bot->GetDistance(boss); - - // If we're already at safe distance, no need to move - if (dist >= 30.0f) + float dist = bot->GetExactDist2d(boss->GetPositionX(), boss->GetPositionY()); + + if (dist >= 33.0f) return false; - // Check boss health to determine movement target - bool isPhaseThree = boss->GetHealthPct() <= 35; - Position const& targetPos = isPhaseThree ? ICC_SINDRAGOSA_LOS2_POSITION : ICC_SINDRAGOSA_BLISTERING_COLD_POSITION; + Position const& targetPos = ICC_SINDRAGOSA_BLISTERING_COLD_POSITION; // Only move if we're too close to the boss (< 30 yards) - if (dist < 30.0f) + if (dist < 33.0f) { - // If in phase 3, check if already at LOS2 position - if (isPhaseThree && bot->IsWithinDist2d(targetPos.GetPositionX(), targetPos.GetPositionY(), 1.0f)) - return false; - float const STEP_SIZE = 10.0f; + float const STEP_SIZE = 15.0f; float distToTarget = bot->GetDistance2d(targetPos.GetPositionX(), targetPos.GetPositionY()); if (distToTarget > 0.1f) // Avoid division by zero { + if (!bot->HasAura(SPELL_NITRO_BOOSTS)) + bot->AddAura(SPELL_NITRO_BOOSTS, bot); // Calculate direction vector float dirX = targetPos.GetPositionX() - bot->GetPositionX(); float dirY = targetPos.GetPositionY() - bot->GetPositionY(); @@ -3023,12 +5867,21 @@ bool IccSindragosaBlisteringColdAction::Execute(Event event) bool IccSindragosaUnchainedMagicAction::Execute(Event event) { - if (bot->HasAura(69762)) // unchained magic + Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); + if (!boss) + return false; - if (Aura* aura = bot->GetAura(69766)) + Aura* aura = botAI->GetAura("Unchained Magic", bot, false, true); + if (!aura) + return false; + + Aura* aura1 = botAI->GetAura("Instability", bot, false, true); + + Difficulty diff = bot->GetRaidDifficulty(); + if (aura && (diff == RAID_DIFFICULTY_10MAN_NORMAL || diff == RAID_DIFFICULTY_25MAN_NORMAL)) { - if (aura->GetStackAmount() >= 3) - return true; // Stop casting spells + if (aura1 && aura1->GetStackAmount() >= 6) + return true; // Stop casting spells } return false; @@ -3036,15 +5889,21 @@ bool IccSindragosaUnchainedMagicAction::Execute(Event event) bool IccSindragosaChilledToTheBoneAction::Execute(Event event) { - if (bot->HasAura(70106)) // Chilled to the Bone + Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); + if (!boss) + return false; + + Aura* aura = botAI->GetAura("Chilled to the Bone", bot, false, true); + if (!aura) + return false; + + if (aura) // Chilled to the Bone { - if (Aura* aura = bot->GetAura(70106)) + if (aura->GetStackAmount() >= 6) { - if (aura->GetStackAmount() >= 8) - { - bot->AttackStop(); - return true; // Stop casting spells - } + botAI->Reset(); + bot->AttackStop(); + return true; } } @@ -3053,8 +5912,6 @@ bool IccSindragosaChilledToTheBoneAction::Execute(Event event) bool IccSindragosaMysticBuffetAction::Execute(Event event) { - - // Get boss to check if it exists Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); if (!boss || !bot || !bot->IsAlive()) return false; @@ -3064,60 +5921,82 @@ bool IccSindragosaMysticBuffetAction::Execute(Event event) if (!aura) return false; + if (boss->GetVictim() == bot) + return false; + // Skip if we have Frost Beacon - if (bot->HasAura(70126)) + if (bot->HasAura(SPELL_FROST_BEACON)) return false; - uint8 stacks = aura->GetStackAmount(); - - // Check if already at LOS2 position - if (bot->IsWithinDist2d(ICC_SINDRAGOSA_LOS2_POSITION.GetPositionX(), - ICC_SINDRAGOSA_LOS2_POSITION.GetPositionY(), 1.0f)) - { - return false; - } - - // Find nearest ice tomb Group* group = bot->GetGroup(); if (!group) return false; + static const std::array tombEntries = {NPC_TOMB1, NPC_TOMB2, NPC_TOMB3, NPC_TOMB4}; + const GuidVector tombGuids = AI_VALUE(GuidVector, "possible targets no los"); + Unit* nearestTomb = nullptr; float minDist = 150.0f; - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + for (const auto entry : tombEntries) { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || member == bot) - continue; - - if (member->HasAura(70157)) // Ice Tomb + for (const auto& guid : tombGuids) { - float dist = bot->GetDistance(member); - if (dist < minDist) + if (Unit* unit = botAI->GetUnit(guid)) { - minDist = dist; - nearestTomb = member; + if (unit->GetEntry() == entry && unit->IsAlive()) + { + float dist = bot->GetDistance(unit); + if (dist < minDist) + { + minDist = dist; + nearestTomb = unit; + } + } } } } - // If no tombs found or tomb not at MB2 position, don't move - if (!nearestTomb || !nearestTomb->IsWithinDist2d(ICC_SINDRAGOSA_THOMBMB2_POSITION.GetPositionX(), - ICC_SINDRAGOSA_THOMBMB2_POSITION.GetPositionY(), 1.0f)) - return false; + // Check if anyone in group has Frost Beacon (SPELL_FROST_BEACON) + bool anyoneHasFrostBeacon = false; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && member->HasAura(SPELL_FROST_BEACON)) + { + anyoneHasFrostBeacon = true; + break; + } + } - // Move to LOS2 position - return MoveTo(bot->GetMapId(), - ICC_SINDRAGOSA_LOS2_POSITION.GetPositionX(), - ICC_SINDRAGOSA_LOS2_POSITION.GetPositionY(), - ICC_SINDRAGOSA_LOS2_POSITION.GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_FORCED); + bool tombPresent = nearestTomb != nullptr; + bool atLOS2 = bot->GetExactDist2d(ICC_SINDRAGOSA_LOS2_POSITION.GetPositionX(), + ICC_SINDRAGOSA_LOS2_POSITION.GetPositionY()) <= 2.0f; + + // Move to LOS2 position if: tomb is present and no one has Frost Beacon + bool shouldMoveLOS2 = tombPresent && !anyoneHasFrostBeacon; + + if (shouldMoveLOS2) + { + // If already at LOS2 and have 3+ stacks, stay still + if (atLOS2 && aura && !botAI->IsHeal(bot)) + { + return true; + } + + + botAI->Reset(); + // Move to LOS2 position + return MoveTo(bot->GetMapId(), ICC_SINDRAGOSA_LOS2_POSITION.GetPositionX(), + ICC_SINDRAGOSA_LOS2_POSITION.GetPositionY(), ICC_SINDRAGOSA_LOS2_POSITION.GetPositionZ(), false, + false, false, true, MovementPriority::MOVEMENT_FORCED); + } + return false; } bool IccSindragosaFrostBombAction::Execute(Event event) { - if (!bot || !bot->IsAlive() || bot->HasAura(70157)) // Skip if dead or in Ice Tomb + if (!bot || !bot->IsAlive() || bot->HasAura(SPELL_ICE_TOMB)) // Skip if dead or in Ice Tomb return false; Group* group = bot->GetGroup(); @@ -3126,18 +6005,24 @@ bool IccSindragosaFrostBombAction::Execute(Event event) // Find frost bomb marker and tombs GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - const uint32 tombEntries[] = {36980, 38320, 38321, 38322}; // tomb id's + const uint32 tombEntries[] = {NPC_TOMB1, NPC_TOMB2, NPC_TOMB3, NPC_TOMB4}; // tomb id's Unit* marker = nullptr; - GuidVector tombGuids; + std::vector tombs; + std::vector tombGuids; - // Process all NPCs - for (const ObjectGuid& guid : npcs) + // Manually search for units with frost bomb aura (SPELL_FROST_BOMB_VISUAL) using NearestHostileNpcsValue logic + std::list units; + float range = 200.0f; + Acore::AnyUnitInObjectRangeCheck u_check(bot, range); + Acore::UnitListSearcher searcher(bot, units, u_check); + Cell::VisitAllObjects(bot, searcher, range); + + for (Unit* unit : units) { - Unit* unit = botAI->GetUnit(guid); if (!unit || !unit->IsAlive()) continue; - if (unit->HasAura(70022)) // Frost bomb visual + if (unit->HasAura(SPELL_FROST_BOMB_VISUAL)) // Frost bomb visual marker = unit; // Check if unit is a tomb @@ -3145,231 +6030,393 @@ bool IccSindragosaFrostBombAction::Execute(Event event) { if (unit->GetEntry() == entry) { - tombGuids.push_back(guid); + tombs.push_back(unit); + tombGuids.push_back(unit->GetGUID()); break; } } } - // Exit if required elements aren't found - if (!marker || tombGuids.empty()) + if (!marker || tombs.empty()) + { + bot->AttackStop(); + return true; + } + + // Get persistent group assignment - use a static map to store assignments + static std::map persistentGroupAssignments; + static std::vector allGroupGuids; // All guids that have ever been in the raid + + // Gather all group members (alive and dead, including those with ice tomb) + std::vector currentGuids; + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member) + currentGuids.push_back(member->GetGUID()); + } + + // Add any new GUIDs to our persistent list + for (const ObjectGuid& guid : currentGuids) + { + if (std::find(allGroupGuids.begin(), allGroupGuids.end(), guid) == allGroupGuids.end()) + { + allGroupGuids.push_back(guid); + } + } + + // Sort the complete guid list for consistency + std::sort(allGroupGuids.begin(), allGroupGuids.end()); + + Difficulty diff = bot->GetRaidDifficulty(); + // Determine group count (2 for 10m, 3 for 25m) + int groupCount = (diff == RAID_DIFFICULTY_25MAN_NORMAL || diff == RAID_DIFFICULTY_25MAN_HEROIC) ? 3 : 2; + + // Assign group indices to GUIDs that don't have assignments yet + for (size_t i = 0; i < allGroupGuids.size(); ++i) + { + const ObjectGuid& guid = allGroupGuids[i]; + if (persistentGroupAssignments.find(guid) == persistentGroupAssignments.end()) + { + // Assign to group based on their position in the sorted list + persistentGroupAssignments[guid] = int(i) % groupCount; + } + } + + // Get this bot's group assignment + auto it = persistentGroupAssignments.find(bot->GetGUID()); + if (it == persistentGroupAssignments.end()) return false; - // Sort tomb GUIDs by health (highest first) - std::sort(tombGuids.begin(), tombGuids.end(), - [this](const ObjectGuid& a, const ObjectGuid& b) - { - Unit* unitA = botAI->GetUnit(a); - Unit* unitB = botAI->GetUnit(b); - if (!unitA || !unitB) - return false; - return unitA->GetHealthPct() > unitB->GetHealthPct(); - }); + int myGroupIndex = it->second; - // Get the tomb with highest HP for LOS - Unit* highestHPTomb = botAI->GetUnit(tombGuids[0]); - - // Handle moon marking (much simpler now) - ObjectGuid currentMoon = group->GetTargetIcon(4); - - // Check if current moon mark is still valid - bool moonMarkValid = false; - if (!currentMoon.IsEmpty()) + // Build group positions based on available tombs + std::vector groupPositions; + for (int i = 0; i < groupCount; ++i) { - for (const ObjectGuid& tombGuid : tombGuids) + if (i < int(tombs.size())) { - if (tombGuid == currentMoon) + groupPositions.push_back(tombs[i]->GetPosition()); + } + else + { + groupPositions.push_back(marker->GetPosition()); + } + } + + // PRIORITY 1: Check if there are any tombs near our current position (within 8 yards) + std::vector nearbyTombs; + for (Unit* tomb : tombs) + { + if (tomb->GetExactDist2d(bot) <= 8.0f) + { + nearbyTombs.push_back(tomb); + } + } + + // PRIORITY 2: If no tombs nearby, find tombs near our assigned group position + std::vector groupPositionTombs; + if (nearbyTombs.empty()) + { + for (Unit* tomb : tombs) + { + if (tomb->GetExactDist2d(groupPositions[myGroupIndex]) <= 8.0f) { - Unit* tomb = botAI->GetUnit(tombGuid); - if (tomb && tomb->IsAlive()) - { - moonMarkValid = true; - break; - } + groupPositionTombs.push_back(tomb); } } } - // If no valid moon mark exists, mark the first tomb - if (!moonMarkValid && !tombGuids.empty()) + // Select which tombs to use based on priority + std::vector myTombs; + std::vector myTombGuids; + + if (!nearbyTombs.empty()) { - Unit* tombToMark = botAI->GetUnit(tombGuids[0]); - if (tombToMark) + // Use tombs near current position (highest priority) + myTombs = nearbyTombs; + for (Unit* tomb : nearbyTombs) { - group->SetTargetIcon(4, bot->GetGUID(), tombToMark->GetGUID()); // Moon - // Reset bot's target after marking - bot->SetTarget(ObjectGuid::Empty); + myTombGuids.push_back(tomb->GetGUID()); } } - - // Use marked tomb or highest HP tomb for LOS check - Unit* losTarget = nullptr; - if (moonMarkValid) + else if (!groupPositionTombs.empty()) { - losTarget = botAI->GetUnit(currentMoon); + // Use tombs near group position (medium priority) + myTombs = groupPositionTombs; + for (Unit* tomb : groupPositionTombs) + { + myTombGuids.push_back(tomb->GetGUID()); + } } else { - losTarget = highestHPTomb; + // Fallback: use closest available tomb (lowest priority) + Unit* closestTomb = nullptr; + float closestDist = 999.0f; + for (Unit* tomb : tombs) + { + float dist = tomb->GetExactDist2d(bot); + if (dist < closestDist) + { + closestDist = dist; + closestTomb = tomb; + } + } + if (closestTomb) + { + myTombs.push_back(closestTomb); + myTombGuids.push_back(closestTomb->GetGUID()); + } } + if (myTombs.empty()) + return false; - // Position handling using highest HP tomb - float angle = marker->GetAngle(losTarget); - float posX = losTarget->GetPositionX() + cos(angle) * 1.0f; - float posY = losTarget->GetPositionY() + sin(angle) * 1.0f; - float posZ = losTarget->GetPositionZ(); - if (bot->GetDistance2d(posX, posY) > 2.0f) + // Pick the tomb with highest HP in our selection + size_t bestIdx = 0; + float bestHp = 0.0f; + for (size_t i = 0; i < myTombs.size(); ++i) + { + float hp = myTombs[i]->GetHealthPct(); + if (i == 0 || hp > bestHp) + { + bestHp = hp; + bestIdx = i; + } + } + Unit* losTomb = myTombs[bestIdx]; + ObjectGuid losTombGuid = myTombGuids[bestIdx]; + + // Calculate position for LOS (stand at least 6.5f behind the tomb from the bomb) + float angle = marker->GetAngle(losTomb); + float posX = losTomb->GetPositionX() + cos(angle) * 6.5f; + float posY = losTomb->GetPositionY() + sin(angle) * 6.5f; + float posZ = losTomb->GetPositionZ(); + + // Always move to exact LOS position for safety + float distToLosPos = bot->GetDistance2d(posX, posY); + if (distToLosPos > 0.01f) + { + botAI->Reset(); + bot->AttackStop(); return MoveTo(bot->GetMapId(), posX, posY, posZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED); + } + + // Check if we are in LOS of the bomb (must be very close to calculated position) + bool inLOS = (distToLosPos <= 0.01f); + + // RTI marker constants + static constexpr uint8_t SKULL_ICON_INDEX = 7; + static constexpr uint8_t CROSS_ICON_INDEX = 6; + static constexpr uint8_t STAR_ICON_INDEX = 0; + + // If in LOS, handle RTI marking for group's tombs + if (inLOS) + { + // Determine RTI marker for this group + uint8_t iconIndex = 0; + std::string rtiValue; + if (myGroupIndex == 0) + { + iconIndex = SKULL_ICON_INDEX; + rtiValue = "skull"; + } + else if (myGroupIndex == 1) + { + iconIndex = CROSS_ICON_INDEX; + rtiValue = "cross"; + } + else if (myGroupIndex == 2) + { + iconIndex = STAR_ICON_INDEX; + rtiValue = "star"; + } + else + return false; + + context->GetValue("rti")->Set(rtiValue); + + // Find a tomb in our group with 65% or more HP to mark + Unit* tombToMark = nullptr; + for (size_t i = 0; i < myTombs.size(); ++i) + { + Unit* tomb = myTombs[i]; + if (tomb->IsAlive() && tomb->HealthAbovePct(45)) + { + tombToMark = tomb; + break; + } + } + + if (tombToMark) + { + // Check if this tomb is already marked with our group's icon + ObjectGuid currentIcon = group->GetTargetIcon(iconIndex); + Unit* currentIconUnit = botAI->GetUnit(currentIcon); + if (!currentIconUnit || !currentIconUnit->IsAlive() || currentIconUnit != tombToMark) + { + // Mark the tomb with our group's target icon + group->SetTargetIcon(iconIndex, bot->GetGUID(), tombToMark->GetGUID()); + } + } + else + { + // No tombs above 65% HP, remove marker if one exists + ObjectGuid currentIcon = group->GetTargetIcon(iconIndex); + if (!currentIcon.IsEmpty()) + { + // Clear the marker for our group's icon + group->SetTargetIcon(iconIndex, bot->GetGUID(), ObjectGuid::Empty); + } + Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); + bot->AttackStop(); + return true; + } + } return false; } +// The Lich King bool IccLichKingShadowTrapAction::Execute(Event event) { Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); - if (!boss) + if (!boss || !botAI->IsTank(bot)) return false; - if (!botAI->IsRanged(bot)) - return false; + Difficulty diff = bot->GetRaidDifficulty(); - // search for all nearby traps + if (sPlayerbotAIConfig->EnableICCBuffs && diff && (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) + { + //-------CHEAT------- + if (!bot->HasAura(SPELL_EXPERIENCED)) + bot->AddAura(SPELL_EXPERIENCED, bot); + + if (!bot->HasAura(SPELL_AGEIS_OF_DALARAN)) + bot->AddAura(SPELL_AGEIS_OF_DALARAN, bot); + + if (!bot->HasAura(SPELL_NO_THREAT) && !botAI->IsTank(bot)) + bot->AddAura(SPELL_NO_THREAT, bot); + + if (!bot->HasAura(SPELL_PAIN_SUPPRESION)) + bot->AddAura(SPELL_PAIN_SUPPRESION, bot); + //-------CHEAT------- + } + + // Define ICC_LICH_POSITION and circle parameters + const float X = ICC_LICH_KING_ASSISTHC_POSITION.GetPositionX(); + const float Y = ICC_LICH_KING_ASSISTHC_POSITION.GetPositionY(); + const float Z = ICC_LICH_KING_ASSISTHC_POSITION.GetPositionZ(); + const float CIRCLE_RADIUS = 20.0f; + const float SAFE_DISTANCE = 12.0f; + const int TEST_POSITIONS = 16; + const float ANGLE_STEP = 2 * M_PI / TEST_POSITIONS; + + // Find all nearby shadow traps GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - std::vector nearbyTraps; - bool needToMove = false; - + std::vector trapGuids; for (auto& npc : npcs) { Unit* unit = botAI->GetUnit(npc); - if (!unit || !unit->IsAlive()) + if (!unit || !unit->IsAlive() || unit->GetEntry() != NPC_SHADOW_TRAP) continue; - - if (unit->GetEntry() == 39137) // shadow trap + if (bot->GetDistance(unit) < SAFE_DISTANCE + 5.0f) { - float distance = bot->GetDistance(unit); - if (distance < 7.0f) - { - needToMove = true; - nearbyTraps.push_back(unit); - - // Immediate danger check - if we're very close to any trap, move away faster - if (distance < 3.0f) - { - float angle = bot->GetAngle(unit); - float x = bot->GetPositionX() + cos(angle + M_PI) * 5.0f; // Move opposite direction - float y = bot->GetPositionY() + sin(angle + M_PI) * 5.0f; - float z = bot->GetPositionZ(); - - bot->UpdateAllowedPositionZ(x, y, z); - if (bot->IsWithinLOS(x, y, z)) - { - botAI->Reset(); - return MoveTo(bot->GetMapId(), x, y, z, false, false, false, true, - MovementPriority::MOVEMENT_FORCED, true, false); - } - } - } + trapGuids.push_back(npc); } } - if (!needToMove || nearbyTraps.empty()) + if (trapGuids.empty()) return false; - // Try different angles to find a safe spot - float const MOVE_DISTANCE = 5.0f; - float const ANGLE_INCREMENT = M_PI / 4; - float bestAngle = 0; - float maxMinDistance = 0; - bool foundSafeSpot = false; - - // Try 8 primary directions first (faster evaluation) - for (int i = 0; i < 8; i++) + // Check if current position is already safe + bool currentPositionSafe = true; + for (auto& trapGuid : trapGuids) { - float tryAngle = i * ANGLE_INCREMENT; - float testX = bot->GetPositionX() + cos(tryAngle) * MOVE_DISTANCE; - float testY = bot->GetPositionY() + sin(tryAngle) * MOVE_DISTANCE; - float testZ = bot->GetPositionZ(); // Use current Z as base + Unit* trap = botAI->GetUnit(trapGuid); + if (!trap) + continue; + if (bot->GetDistance(trap) < SAFE_DISTANCE) + { + currentPositionSafe = false; + break; + } + } + // If current position is safe, no need to move + if (currentPositionSafe) + return false; + + // Calculate current angle relative to ICC_LICH_POSITION + float currentX = bot->GetPositionX() - X; + float currentY = bot->GetPositionY() - Y; + float currentAngle = atan2(currentY, currentX); + + // Test clockwise positions first, then opposite position + std::vector testAngles; + // Add clockwise positions + for (int i = 1; i <= TEST_POSITIONS; ++i) + { + testAngles.push_back(currentAngle - (ANGLE_STEP * i)); + } + // Add opposite position as fallback + testAngles.push_back(currentAngle + M_PI); + + // Test all positions + for (float testAngle : testAngles) + { + // Calculate position on circle + float testX = X + cos(testAngle) * CIRCLE_RADIUS; + float testY = Y + sin(testAngle) * CIRCLE_RADIUS; + float testZ = Z; + + // Update Z coordinate for terrain bot->UpdateAllowedPositionZ(testX, testY, testZ); - // Skip invalid positions + // Check line of sight if (!bot->IsWithinLOS(testX, testY, testZ)) continue; - // Find minimum distance to any trap from this position - float minTrapDistance = FLT_MAX; - for (Unit* trap : nearbyTraps) + // Check if this position is safe from all traps + bool isSafe = true; + for (auto& trapGuid : trapGuids) { + Unit* trap = botAI->GetUnit(trapGuid); + if (!trap) + continue; float distToTrap = sqrt(pow(testX - trap->GetPositionX(), 2) + pow(testY - trap->GetPositionY(), 2)); - minTrapDistance = std::min(minTrapDistance, distToTrap); + if (distToTrap < SAFE_DISTANCE) + { + isSafe = false; + break; + } } - // If this position gets us to safety, move there immediately - if (minTrapDistance >= 7.0f) + // Found a safe spot - move there + if (isSafe) { - bestAngle = tryAngle; - foundSafeSpot = true; - break; - } - - // Otherwise track the best option - if (minTrapDistance > maxMinDistance) - { - maxMinDistance = minTrapDistance; - bestAngle = tryAngle; + // Remove botAI->Reset() as it might interfere with movement + MoveTo(bot->GetMapId(), testX, testY, testZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED, + true, false); } } - // If we didn't find a completely safe spot but can improve our situation - if (!foundSafeSpot && maxMinDistance > bot->GetDistance(nearbyTraps[0])) - { - foundSafeSpot = true; - } - - // Move to the selected position - if (foundSafeSpot) - { - float x = bot->GetPositionX() + cos(bestAngle) * MOVE_DISTANCE; - float y = bot->GetPositionY() + sin(bestAngle) * MOVE_DISTANCE; - float z = bot->GetPositionZ(); - - bot->UpdateAllowedPositionZ(x, y, z); - if (bot->IsWithinLOS(x, y, z)) - { - botAI->Reset(); - return MoveTo(bot->GetMapId(), x, y, z, false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, - false); - } - } - - // Fallback: Just move backward if all else fails - float angle = bot->GetAngle(nearbyTraps[0]); - float x = bot->GetPositionX() + cos(angle + M_PI) * 5.0f; - float y = bot->GetPositionY() + sin(angle + M_PI) * 5.0f; - float z = bot->GetPositionZ(); - - bot->UpdateAllowedPositionZ(x, y, z); - botAI->Reset(); - return MoveTo(bot->GetMapId(), x, y, z, false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); - + // No safe position found return false; } bool IccLichKingNecroticPlagueAction::Execute(Event event) { bool hasPlague = botAI->HasAura("Necrotic Plague", bot); - // Only execute if we have the plague if (!hasPlague) return false; - // Find closest shambling and nearest shadow trap + // Find closest shambling horror GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); Unit* closestHorror = nullptr; - Unit* nearestTrap = nullptr; float minHorrorDist = 100.0f; - float minTrapDist = 100.0f; for (auto& npc : npcs) { @@ -3378,7 +6425,8 @@ bool IccLichKingNecroticPlagueAction::Execute(Event event) continue; uint32 entry = unit->GetEntry(); - if (entry == 37698 || entry == 39299 || entry == 39300 || entry == 39301) //shambling horror + if (entry == NPC_SHAMBLING_HORROR1 || entry == NPC_SHAMBLING_HORROR2 || + entry == NPC_SHAMBLING_HORROR3 || entry == NPC_SHAMBLING_HORROR4) { float distance = bot->GetDistance(unit); if (distance < minHorrorDist) @@ -3387,53 +6435,26 @@ bool IccLichKingNecroticPlagueAction::Execute(Event event) closestHorror = unit; } } - else if (entry == 39137) //shadow trap - { - float distance = bot->GetDistance(unit); - if (distance < minTrapDist) - { - minTrapDist = distance; - nearestTrap = unit; - } - } } - // If we found a shambling and we're not close enough, move to it + // If we found a shambling horror, handle movement if (closestHorror) { - if (closestHorror->isInFront(bot)) - return FleePosition(closestHorror->GetPosition(), 2.0f, 250U); - - // If we're too far, run to it - if (minHorrorDist > 2.0f) + // If we're close enough, stop and return success + if (minHorrorDist <= 2.0f) { - // Check if there's a trap in our path - if (nearestTrap && minTrapDist < 9.0f) - { - // Calculate a safe position that avoids the trap - float angle = nearestTrap->GetAngle(closestHorror); - float safeX = nearestTrap->GetPositionX() + cos(angle + M_PI_2) * 10.0f; - float safeY = nearestTrap->GetPositionY() + sin(angle + M_PI_2) * 10.0f; - float safeZ = 840.857f; - - // Move to safe position first - botAI->Reset(); - return MoveTo(bot->GetMapId(), safeX, safeY, safeZ, - false, false, false, false, MovementPriority::MOVEMENT_FORCED); - } - - // No traps nearby, move directly to horror - botAI->Reset(); - return MoveTo(closestHorror, 3.0f, MovementPriority::MOVEMENT_FORCED); - } - else - { - // We're close enough, stop moving bot->StopMoving(); return true; } + + // We need to move to the horror + botAI->Reset(); + MoveTo(closestHorror, 2.0f, MovementPriority::MOVEMENT_FORCED); + + return false; // Still moving, not finished yet } + // No shambling horror found, but we have plague - this shouldn't happen normally return false; } @@ -3443,139 +6464,515 @@ bool IccLichKingWinterAction::Execute(Event event) if (!boss) return false; + Unit* iceSphere = AI_VALUE2(Unit*, "find target", "ice sphere"); + + bool isVictim = iceSphere && iceSphere->GetVictim() == bot && !botAI->IsTank(bot); + + // First priority: Get out of Defile if we're in one + if (!IsPositionSafeFromDefile(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), 3.0f)) + { + // Find nearest safe position (use tank position as fallback) + const Position* safePos = botAI->IsTank(bot) ? GetMainTankPosition() : GetMainTankRangedPosition(); + TryMoveToPosition(safePos->GetPositionX(), safePos->GetPositionY(), 840.857f, true); + return true; + } + float currentDistance = bot->GetDistance2d(boss); - // Move behind target if bot is not tank, is close, and is in front + + Difficulty diff = bot->GetRaidDifficulty(); + + if (sPlayerbotAIConfig->EnableICCBuffs && diff && (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) + { + //------CHEAT------- + if (!bot->HasAura(SPELL_EXPERIENCED)) + bot->AddAura(SPELL_EXPERIENCED, bot); + + if (!bot->HasAura(SPELL_AGEIS_OF_DALARAN)) + bot->AddAura(SPELL_AGEIS_OF_DALARAN, bot); + + if (!bot->HasAura(SPELL_NO_THREAT) && !botAI->IsTank(bot)) + bot->AddAura(SPELL_NO_THREAT, bot); + + if (!bot->HasAura(SPELL_PAIN_SUPPRESION)) + bot->AddAura(SPELL_PAIN_SUPPRESION, bot); + //------CHEAT------- + } + + if (currentDistance < 35.0f && !bot->HasAura(SPELL_NITRO_BOOSTS)) + bot->AddAura(SPELL_NITRO_BOOSTS, bot); + + // Handle group target management + if (Group* group = bot->GetGroup()) + { + const ObjectGuid currentSkullTarget = group->GetTargetIcon(7); + if (!currentSkullTarget.IsEmpty()) + { + Unit* skullTarget = ObjectAccessor::GetUnit(*bot, currentSkullTarget); + group->SetTargetIcon(7, bot->GetGUID(), ObjectGuid::Empty); + } + } + + if (isVictim) + MoveFromGroup(6.0f); + + if (!isVictim) + { + HandlePositionCorrection(); + + // Handle tank positioning and add management FIRST + HandleTankPositioning(); // New method that handles both main and assist tanks + + // Then handle other roles + HandleMeleePositioning(); + HandleRangedPositioning(); + } + + return false; +} + +void IccLichKingWinterAction::HandlePositionCorrection() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); Unit* currentTarget = AI_VALUE(Unit*, "current target"); - if (!currentTarget) - {} - else if (!botAI->IsRanged(bot) && currentTarget->isInFront(bot) && currentTarget->IsAlive() && !botAI->IsTank(bot) && currentTarget->GetEntry() != 36597 && - currentTarget->GetEntry() != 36633 && currentTarget->GetEntry() != 39305 && currentTarget->GetEntry() != 39306 && currentTarget->GetEntry() != 39307) - { - float orientation = currentTarget->GetOrientation() + M_PI + M_PI / 8; - float x = currentTarget->GetPositionX(); - float y = currentTarget->GetPositionY(); - float z = currentTarget->GetPositionZ(); - float rx = x + cos(orientation) * 4.0f; - float ry = y + sin(orientation) * 4.0f; - return MoveTo(bot->GetMapId(), rx, ry, z, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, - true, false); - } + // Fix underground bug + if (abs(bot->GetPositionZ() - 840.857f) > 1.0f) + bot->TeleportTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), 840.857f, bot->GetOrientation()); - if (botAI->IsRanged(bot)) + // Reset targeting for specific conditions + if (currentTarget && boss && currentTarget == boss) + botAI->Reset(); + + if (botAI->IsTank(bot) && currentTarget && + ((currentTarget->GetEntry() == NPC_ICE_SPHERE1 || currentTarget->GetEntry() == NPC_ICE_SPHERE2 || + currentTarget->GetEntry() == NPC_ICE_SPHERE3 || currentTarget->GetEntry() == NPC_ICE_SPHERE4))) + botAI->Reset(); +} + +const Position* IccLichKingWinterAction::GetMainTankPosition() +{ + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + if (!mainTank) { - // Calculate distances to group positions + // FIXED: When no main tank, use the bot with lowest GUID to determine position + // This ensures ALL bots make the same decision collectively + + Unit* referenceBot = nullptr; + ObjectGuid lowestGuid; + + // Find the bot with lowest GUID in the group + Group* group = bot->GetGroup(); + if (group) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && member->IsAlive() && member->IsInWorld()) + { + if (lowestGuid.IsEmpty() || member->GetGUID() < lowestGuid) + { + lowestGuid = member->GetGUID(); + referenceBot = member; + } + } + } + } + + // If no group or reference bot found, fall back to current bot + if (!referenceBot) + referenceBot = bot; + + // Use the reference bot's position to determine closest tank position float dist1 = - bot->GetDistance2d(ICC_LK_FROSTR1_POSITION.GetPositionX(), ICC_LK_FROSTR1_POSITION.GetPositionY()); + referenceBot->GetDistance2d(ICC_LK_FROST1_POSITION.GetPositionX(), ICC_LK_FROST1_POSITION.GetPositionY()); float dist2 = - bot->GetDistance2d(ICC_LK_FROSTR2_POSITION.GetPositionX(), ICC_LK_FROSTR2_POSITION.GetPositionY()); + referenceBot->GetDistance2d(ICC_LK_FROST2_POSITION.GetPositionX(), ICC_LK_FROST2_POSITION.GetPositionY()); float dist3 = - bot->GetDistance2d(ICC_LK_FROSTR3_POSITION.GetPositionX(), ICC_LK_FROSTR3_POSITION.GetPositionY()); + referenceBot->GetDistance2d(ICC_LK_FROST3_POSITION.GetPositionX(), ICC_LK_FROST3_POSITION.GetPositionY()); - const Position* targetPos = &ICC_LK_FROSTR1_POSITION; if (dist2 < dist1 && dist2 < dist3) - targetPos = &ICC_LK_FROSTR2_POSITION; + return &ICC_LK_FROST2_POSITION; else if (dist3 < dist1 && dist3 < dist2) - targetPos = &ICC_LK_FROSTR3_POSITION; - - float distToTarget = bot->GetDistance2d(targetPos->GetPositionX(), targetPos->GetPositionY()); - if (distToTarget > 3.0f) - { - float angle = bot->GetAngle(targetPos); - float posX = bot->GetPositionX() + cos(angle) * 2.0f; - float posY = bot->GetPositionY() + sin(angle) * 2.0f; - return MoveTo(bot->GetMapId(), posX, posY, 840.857f, false, false, false, true, - MovementPriority::MOVEMENT_FORCED, true, false); - } + return &ICC_LK_FROST3_POSITION; + else + return &ICC_LK_FROST1_POSITION; } + + // Calculate which position the main tank is closest to + float dist1 = mainTank->GetDistance2d(ICC_LK_FROST1_POSITION.GetPositionX(), ICC_LK_FROST1_POSITION.GetPositionY()); + float dist2 = mainTank->GetDistance2d(ICC_LK_FROST2_POSITION.GetPositionX(), ICC_LK_FROST2_POSITION.GetPositionY()); + float dist3 = mainTank->GetDistance2d(ICC_LK_FROST3_POSITION.GetPositionX(), ICC_LK_FROST3_POSITION.GetPositionY()); + + if (dist2 < dist1 && dist2 < dist3) + return &ICC_LK_FROST2_POSITION; + else if (dist3 < dist1 && dist3 < dist2) + return &ICC_LK_FROST3_POSITION; else + return &ICC_LK_FROST1_POSITION; +} + +const Position* IccLichKingWinterAction::GetMainTankRangedPosition() +{ + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + if (!mainTank) { - // Calculate distances to tank and melee positions - float dist1 = bot->GetDistance2d(ICC_LK_FROST1_POSITION.GetPositionX(), ICC_LK_FROST1_POSITION.GetPositionY()); - float dist2 = bot->GetDistance2d(ICC_LK_FROST2_POSITION.GetPositionX(), ICC_LK_FROST2_POSITION.GetPositionY()); - float dist3 = bot->GetDistance2d(ICC_LK_FROST3_POSITION.GetPositionX(), ICC_LK_FROST3_POSITION.GetPositionY()); + // FIXED: When no main tank, use the bot with lowest GUID to determine position + // This ensures ALL bots make the same decision collectively + + Unit* referenceBot = nullptr; + ObjectGuid lowestGuid; + + // Find the bot with lowest GUID in the group + Group* group = bot->GetGroup(); + if (group) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && member->IsAlive() && member->IsInWorld()) + { + if (lowestGuid.IsEmpty() || member->GetGUID() < lowestGuid) + { + lowestGuid = member->GetGUID(); + referenceBot = member; + } + } + } + } + + // If no group or reference bot found, fall back to current bot + if (!referenceBot) + referenceBot = bot; + + // Use the reference bot's position to determine closest ranged position + float dist1 = + referenceBot->GetDistance2d(ICC_LK_FROSTR1_POSITION.GetPositionX(), ICC_LK_FROSTR1_POSITION.GetPositionY()); + float dist2 = + referenceBot->GetDistance2d(ICC_LK_FROSTR2_POSITION.GetPositionX(), ICC_LK_FROSTR2_POSITION.GetPositionY()); + float dist3 = + referenceBot->GetDistance2d(ICC_LK_FROSTR3_POSITION.GetPositionX(), ICC_LK_FROSTR3_POSITION.GetPositionY()); - const Position* targetPos = &ICC_LK_FROST1_POSITION; if (dist2 < dist1 && dist2 < dist3) - targetPos = &ICC_LK_FROST2_POSITION; + return &ICC_LK_FROSTR2_POSITION; else if (dist3 < dist1 && dist3 < dist2) - targetPos = &ICC_LK_FROST3_POSITION; - - float distToTarget = bot->GetDistance2d(targetPos->GetPositionX(), targetPos->GetPositionY()); - if (distToTarget > 6.0f && !botAI->IsTank(bot)) - { - float angle = bot->GetAngle(targetPos); - float posX = bot->GetPositionX() + cos(angle) * 3.0f; - float posY = bot->GetPositionY() + sin(angle) * 3.0f; - return MoveTo(bot->GetMapId(), posX, posY, 840.857f, false, false, false, false, - MovementPriority::MOVEMENT_FORCED, true, false); - } - - if (botAI->IsAssistTank(bot) && distToTarget < 10) - { - // Actively look for any shambling/spirit/ghoul that isn't targeting our tank - GuidVector targets = AI_VALUE(GuidVector, "possible targets"); - for (auto i = targets.begin(); i != targets.end(); ++i) - { - Unit* unit = botAI->GetUnit(*i); - if (unit && unit->IsAlive() && - (unit->GetEntry() == 37698 || unit->GetEntry() == 39299 || unit->GetEntry() == 39300 || - unit->GetEntry() == 39301 || // Shambling entry - unit->GetEntry() == 36701 || unit->GetEntry() == 39302 || unit->GetEntry() == 39303 || - unit->GetEntry() == 39304 || // Spirits entry - unit->GetEntry() == 37695 || unit->GetEntry() == 39309 || unit->GetEntry() == 39310 || - unit->GetEntry() == 39311)) // Drudge Ghouls entry - { - Unit* mt = AI_VALUE(Unit*, "main tank"); - if (unit->GetVictim() != mt) - { - bot->SetFacingToObject(unit); - return Attack(unit); // Attack any shambling that isn't targeting a tank - } - } - } - } - - if (botAI->IsMainTank(bot)) - { - // Actively look for any shambling/spirit/ghoul that isn't targeting our tank - GuidVector targets = AI_VALUE(GuidVector, "possible targets"); - for (auto i = targets.begin(); i != targets.end(); ++i) - { - Unit* unit = botAI->GetUnit(*i); - if (unit && unit->IsAlive() && - (unit->GetEntry() == 37698 || unit->GetEntry() == 39299 || unit->GetEntry() == 39300 || - unit->GetEntry() == 39301 || // Shambling entry - unit->GetEntry() == 36701 || unit->GetEntry() == 39302 || unit->GetEntry() == 39303 || - unit->GetEntry() == 39304 || // Spirits entry - unit->GetEntry() == 37695 || unit->GetEntry() == 39309 || unit->GetEntry() == 39310 || - unit->GetEntry() == 39311)) // Drudge Ghouls entry - { - Unit* mt = AI_VALUE(Unit*, "main tank"); - if (unit->GetVictim() != mt && unit->GetDistance2d(targetPos->GetPositionX(), targetPos->GetPositionY()) < 3.0f) - { - bot->SetFacingToObject(unit); - return Attack(unit); // Attack any shambling that isn't targeting a tank - } - } - } - } - - if (distToTarget > 2.0f && botAI->IsTank(bot)) - { - float angle = bot->GetAngle(targetPos); - float posX = bot->GetPositionX() + cos(angle) * 1.5f; - float posY = bot->GetPositionY() + sin(angle) * 1.5f; - return MoveTo(bot->GetMapId(), posX, posY, 840.857f, false, false, false, true, - MovementPriority::MOVEMENT_FORCED, true, false); - } - + return &ICC_LK_FROSTR3_POSITION; + else + return &ICC_LK_FROSTR1_POSITION; } - // Check for spheres if we're at a safe distance + // Map main tank's melee position to corresponding ranged position + const Position* tankMeleePos = GetMainTankPosition(); + + if (tankMeleePos == &ICC_LK_FROST1_POSITION) + return &ICC_LK_FROSTR1_POSITION; + else if (tankMeleePos == &ICC_LK_FROST2_POSITION) + return &ICC_LK_FROSTR2_POSITION; + else + return &ICC_LK_FROSTR3_POSITION; +} + +bool IccLichKingWinterAction::IsPositionSafeFromDefile(float x, float y, float z, float minSafeDistance) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); + if (!boss) + return true; // No boss, assume safe + + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + const float BASE_RADIUS = 6.0f; + const float SAFETY_MARGIN = 3.0f; + + for (auto& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (unit && unit->IsAlive() && unit->GetEntry() == DEFILE_NPC_ID) + { + // Calculate current defile radius including growth + float currentRadius = BASE_RADIUS; + Aura* growAura = nullptr; + + // Find growth aura (you'll need to define DEFILE_AURAS array) + for (size_t i = 0; i < DEFILE_AURA_COUNT; i++) + { + growAura = unit->GetAura(DEFILE_AURAS[i]); + if (growAura) + break; + } + + if (growAura) + { + uint8 stacks = growAura->GetStackAmount(); + float growthMultiplier = (bot->GetRaidDifficulty() == RAID_DIFFICULTY_10MAN_HEROIC || + bot->GetRaidDifficulty() == RAID_DIFFICULTY_10MAN_NORMAL) + ? 1.4f + : 0.95f; + currentRadius = BASE_RADIUS + (stacks * growthMultiplier); + } + + float dx = x - unit->GetPositionX(); + float dy = y - unit->GetPositionY(); + float distance = sqrt(dx * dx + dy * dy); + + if (distance < (currentRadius + SAFETY_MARGIN + minSafeDistance)) + return false; + } + } + return true; +} + +bool IccLichKingWinterAction::TryMoveToPosition(float targetX, float targetY, float targetZ, bool isForced) +{ + float currentX = bot->GetPositionX(); + float currentY = bot->GetPositionY(); + float currentZ = bot->GetPositionZ(); + + float dx = targetX - currentX; + float dy = targetY - currentY; + float dz = targetZ - currentZ; + float distance = sqrtf(dx * dx + dy * dy + dz * dz); + + if (distance < 0.1f) + return true; // Already at the position + + dx /= distance; + dy /= distance; + + // First check if direct path is safe + if (bot->IsWithinLOS(targetX, targetY, targetZ) && IsPositionSafeFromDefile(targetX, targetY, targetZ, 3.0f)) + { + if (isForced) + botAI->Reset(); + + MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED, + true, false); + return true; + } + + // If direct path isn't safe, try to find a safe path around defiles + const int MAX_ATTEMPTS = 8; + const float ANGLE_STEP = M_PI / 4.0f; + float attemptDistance = std::min(10.0f, distance); + + for (int i = 0; i < MAX_ATTEMPTS; i++) + { + float angle = i * ANGLE_STEP; + float offsetX = attemptDistance * cos(angle); + float offsetY = attemptDistance * sin(angle); + + // Try positions clockwise and counter-clockwise + for (int direction = -1; direction <= 1; direction += 2) + { + if (i == 0 && direction == 1) + continue; // Skip duplicate first attempt + + float testX = currentX + dx * attemptDistance + offsetX * direction; + float testY = currentY + dy * attemptDistance + offsetY * direction; + float testZ = targetZ; + + if (bot->IsWithinLOS(testX, testY, testZ) && IsPositionSafeFromDefile(testX, testY, testZ, 3.0f)) + { + if (isForced) + botAI->Reset(); + + MoveTo(bot->GetMapId(), testX, testY, testZ, false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + return false; // Not at final position yet + } + } + } + + // If no safe path found, just move directly (better than standing in defile) + if (isForced) + botAI->Reset(); + + MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED, + true, false); + return false; +} + +// Helper function to check if a unit is a valid collectible add +bool IccLichKingWinterAction::IsValidCollectibleAdd(Unit* unit) +{ + if (!unit || !unit->IsAlive()) + return false; + + uint32 entry = unit->GetEntry(); + + // Only spirits, shambling horrors, and ghouls are valid collectible adds + return (entry == NPC_SHAMBLING_HORROR1 || entry == NPC_SHAMBLING_HORROR2 || entry == NPC_SHAMBLING_HORROR3 || + entry == NPC_SHAMBLING_HORROR4 || entry == NPC_RAGING_SPIRIT1 || entry == NPC_RAGING_SPIRIT2 || + entry == NPC_RAGING_SPIRIT3 || entry == NPC_RAGING_SPIRIT4 || entry == NPC_DRUDGE_GHOUL1 || + entry == NPC_DRUDGE_GHOUL2 || entry == NPC_DRUDGE_GHOUL3 || entry == NPC_DRUDGE_GHOUL4); +} + +// FIXED HandleTankPositioning method +void IccLichKingWinterAction::HandleTankPositioning() +{ + if (!botAI->IsTank(bot)) + return; + + Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); + if (!boss) + return; + + // Get the target position based on main tank's choice + const Position* targetPos = GetMainTankPosition(); + + // First check if current position is safe + if (!IsPositionSafeFromDefile(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), 3.0f)) + { + // If in defile, prioritize getting out + TryMoveToPosition(targetPos->GetPositionX(), targetPos->GetPositionY(), 840.857f, true); + return; + } + + float distToTarget = bot->GetDistance2d(targetPos->GetPositionX(), targetPos->GetPositionY()); + + // MAIN TANK: Always stay at tank position + if (botAI->IsMainTank(bot)) + { + // Main tank should always maintain position at tank spot + if (distToTarget > 2.0f) + { + float targetX = targetPos->GetPositionX(); + float targetY = targetPos->GetPositionY(); + float targetZ = 840.857f; + + TryMoveToPosition(targetX, targetY, targetZ, true); + return; // Don't do add management until in position + } + + // Once in position, handle add management from tank position + HandleMainTankAddManagement(boss, targetPos); + } + // ASSIST TANK: More flexible positioning based on add collection + else if (botAI->IsAssistTank(bot)) + { + // First ensure we're reasonably close to tank area + if (distToTarget > 15.0f) + { + float targetX = targetPos->GetPositionX() + 3.0f; // Slight offset from main tank + float targetY = targetPos->GetPositionY() + 2.0f; + float targetZ = 840.857f; + + TryMoveToPosition(targetX, targetY, targetZ, true); + return; + } + + // Handle assist tank add collection and positioning + HandleAssistTankAddManagement(boss, targetPos); + } +} + +// Updated HandleMeleePositioning method - only for non-tanks +void IccLichKingWinterAction::HandleMeleePositioning() +{ + // Skip if this is a tank - they have their own positioning logic + if (botAI->IsTank(bot)) + return; + Unit* currentTarget = AI_VALUE(Unit*, "current target"); + // Handle melee positioning behind target (for DPS only) + if (currentTarget && !botAI->IsRanged(bot) && currentTarget->isInFront(bot) && currentTarget->IsAlive() && + currentTarget->GetEntry() != NPC_THE_LICH_KING && currentTarget->GetEntry() != NPC_ICE_SPHERE1 && + currentTarget->GetEntry() != NPC_ICE_SPHERE2 && currentTarget->GetEntry() != NPC_ICE_SPHERE3 && currentTarget->GetEntry() != NPC_ICE_SPHERE4) + { + // Calculate desired position (4.0f behind the target) + float orientation = currentTarget->GetOrientation() + M_PI + M_PI / 8; + float x = currentTarget->GetPositionX(); + float y = currentTarget->GetPositionY(); + float z = bot->GetPositionZ(); + float targetX = x + cos(orientation) * 4.0f; + float targetY = y + sin(orientation) * 4.0f; + Position botPos = bot->GetPosition(); + float dx = targetX - botPos.GetPositionX(); + float dy = targetY - botPos.GetPositionY(); + float distance = sqrt(dx * dx + dy * dy); + + if (distance <= 1.0f) + return; + + // Move in increments of 2 yards toward the target position + float moveDistance = std::min(2.0f, distance); + float normalizedDx = dx / distance; + float normalizedDy = dy / distance; + + float newX = botPos.GetPositionX() + normalizedDx * moveDistance; + float newY = botPos.GetPositionY() + normalizedDy * moveDistance; + + TryMoveToPosition(newX, newY, z, false); + } + // Handle non-ranged DPS positioning - USE MAIN TANK'S POSITION + if (!botAI->IsRanged(bot)) + { + Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); + const Position* targetPos = GetMainTankPosition(); + float distToTarget = bot->GetDistance2d(targetPos->GetPositionX(), targetPos->GetPositionY()); + if (distToTarget > 8.0f) + { + float targetX = targetPos->GetPositionX() - 5.0f; + float targetY = targetPos->GetPositionY() + 5.0f; + float targetZ = 840.857f; + if (boss && !boss->HealthAbovePct(50)) + { + targetX = targetPos->GetPositionX(); + targetY = targetPos->GetPositionY(); + targetZ = 840.857f; + } + + // Move in increments of 2 yards toward the tank position + Position botPos = bot->GetPosition(); + float dx = targetX - botPos.GetPositionX(); + float dy = targetY - botPos.GetPositionY(); + float distance = sqrt(dx * dx + dy * dy); + + if (distance > 2.0f) + { + float normalizedDx = dx / distance; + float normalizedDy = dy / distance; + + float newX = botPos.GetPositionX() + normalizedDx * 2.0f; + float newY = botPos.GetPositionY() + normalizedDy * 2.0f; + + TryMoveToPosition(newX, newY, targetZ); + } + else + { + TryMoveToPosition(targetX, targetY, targetZ); + } + } + } +} + +// Updated HandleRangedPositioning method +void IccLichKingWinterAction::HandleRangedPositioning() +{ + if (!botAI->IsRanged(bot)) + return; + + // Get the ranged position based on main tank's choice + const Position* targetPos = GetMainTankRangedPosition(); + + // First check if current position is safe + if (!IsPositionSafeFromDefile(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), 3.0f)) + { + // If in defile, prioritize getting out + TryMoveToPosition(targetPos->GetPositionX(), targetPos->GetPositionY(), 840.857f, true); + return; + } + + float distToTarget = bot->GetDistance2d(targetPos->GetPositionX(), targetPos->GetPositionY()); + + if (distToTarget > 2.0f) + { + float targetX = targetPos->GetPositionX(); + float targetY = targetPos->GetPositionY(); + float targetZ = 840.857f; + + TryMoveToPosition(targetX, targetY, targetZ); + } + + // Handle sphere targeting for ranged DPS if (botAI->IsRangedDps(bot)) { - // First check if the group has a hunter bool hasHunter = false; Group* group = bot->GetGroup(); if (group) @@ -3591,19 +6988,30 @@ bool IccLichKingWinterAction::Execute(Event event) } } - // If the bot is a hunter or the group has no hunter, allow attacking spheres if (bot->getClass() == CLASS_HUNTER || !hasHunter) { + Unit* currentTarget = bot->GetVictim(); + if (currentTarget && currentTarget->IsAlive()) + { + uint32 entry = currentTarget->GetEntry(); + if (entry == NPC_ICE_SPHERE1 || entry == NPC_ICE_SPHERE2 || entry == NPC_ICE_SPHERE3 || entry == NPC_ICE_SPHERE4) + { + bot->SetFacingToObject(currentTarget); + Attack(currentTarget); + return; + } + } + Unit* closestSphere = nullptr; float closestDist = 100.0f; - GuidVector npcs = AI_VALUE(GuidVector, "nearest npcs"); + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); for (auto& npc : npcs) { Unit* unit = botAI->GetUnit(npc); if (!unit || !unit->IsAlive()) continue; uint32 entry = unit->GetEntry(); - if (entry == 36633 || entry == 39305 || entry == 39306 || entry == 39307) + if (entry == NPC_ICE_SPHERE1 || entry == NPC_ICE_SPHERE2 || entry == NPC_ICE_SPHERE3 || entry == NPC_ICE_SPHERE4) { float dist = bot->GetDistance(unit); if (!closestSphere || dist < closestDist) @@ -3615,85 +7023,709 @@ bool IccLichKingWinterAction::Execute(Event event) } if (closestSphere) { - botAI->Reset(); + bot->SetTarget(closestSphere->GetGUID()); bot->SetFacingToObject(closestSphere); - return Attack(closestSphere); + Attack(closestSphere); + } + } + } +} + +// NEW METHOD: Main tank add management - stays at position +void IccLichKingWinterAction::HandleMainTankAddManagement(Unit* boss, const Position* tankPos) +{ + if (!botAI->IsMainTank(bot)) + return; + + // Look for adds near tank position (within 5 yards) + Unit* nearbyAdd = nullptr; + float closestDist = 8.0f; // Only consider adds within 8 yards of tank position + + GuidVector targets = AI_VALUE(GuidVector, "possible targets"); + + for (auto i = targets.begin(); i != targets.end(); ++i) + { + Unit* unit = botAI->GetUnit(*i); + if (!IsValidCollectibleAdd(unit)) + continue; + + // Check distance from tank position (not bot position) + float distFromTankPos = unit->GetDistance2d(tankPos->GetPositionX(), tankPos->GetPositionY()); + + // Take adds that are near tank position + if (distFromTankPos < closestDist) + { + nearbyAdd = unit; + closestDist = distFromTankPos; + } + } + + // Attack nearby add or continue with current target + if (nearbyAdd) + { + bot->SetTarget(nearbyAdd->GetGUID()); + bot->SetFacingToObject(nearbyAdd); + Attack(nearbyAdd); + } + else + { + // No nearby adds, check current target + Unit* currentTarget = bot->GetVictim(); + if (currentTarget && IsValidCollectibleAdd(currentTarget)) + { + // Continue attacking current add if it's valid and reasonably close + if (bot->GetDistance(currentTarget) < 10.0f) + { + bot->SetFacingToObject(currentTarget); + Attack(currentTarget); + } + else + { + // Clear target if it's not a valid nearby add + bot->SetTarget(ObjectGuid::Empty); + } + } + } +} + +void IccLichKingWinterAction::HandleAssistTankAddManagement(Unit* boss, const Position* tankPos) +{ + if (!botAI->IsAssistTank(bot)) + return; + + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + if (!mainTank) + return; + + // Look for priority adds that need to be collected + Unit* targetAdd = nullptr; + float closestDist = FLT_MAX; + bool foundPriorityAdd = false; + + GuidVector targets = AI_VALUE(GuidVector, "possible targets"); + + // Priority 1: Adds attacking non-tanks (players/healers) + for (auto i = targets.begin(); i != targets.end(); ++i) + { + Unit* unit = botAI->GetUnit(*i); + if (!IsValidCollectibleAdd(unit)) + continue; + + Unit* addVictim = unit->GetVictim(); + if (addVictim && addVictim->IsPlayer() && !botAI->IsTank(addVictim->ToPlayer())) + { + float addDist = bot->GetDistance(unit); + if (addDist < closestDist) + { + targetAdd = unit; + closestDist = addDist; + foundPriorityAdd = true; } } } - return false; + // Priority 2: Adds not attacking main tank (if no priority adds found) + if (!foundPriorityAdd) + { + closestDist = FLT_MAX; + for (auto i = targets.begin(); i != targets.end(); ++i) + { + Unit* unit = botAI->GetUnit(*i); + if (!IsValidCollectibleAdd(unit)) + continue; + + // Only target adds that are NOT attacking the main tank + if (unit->GetVictim() != mainTank) + { + float addDist = bot->GetDistance(unit); + if (addDist < closestDist) + { + targetAdd = unit; + closestDist = addDist; + } + } + } + } + + if (targetAdd) + { + // Calculate position between add and main tank + float pullX = (targetAdd->GetPositionX() + tankPos->GetPositionX()) / 2; + float pullY = (targetAdd->GetPositionY() + tankPos->GetPositionY()) / 2; + + // Move to intercept position if add is far from main tank + if (targetAdd->GetDistance2d(tankPos->GetPositionX(), tankPos->GetPositionY()) > 10.0f) + { + if (bot->GetDistance2d(pullX, pullY) > 3.0f) + { + TryMoveToPosition(pullX, pullY, 840.857f, false); + } + } + // Otherwise move toward the add + else if (closestDist > 5.0f) + { + TryMoveToPosition(targetAdd->GetPositionX(), targetAdd->GetPositionY(), 840.857f, false); + } + + // Attack the add + bot->SetTarget(targetAdd->GetGUID()); + bot->SetFacingToObject(targetAdd); + Attack(targetAdd); + } + else + { + // No adds to collect, position near main tank + float distToTankPos = bot->GetDistance2d(tankPos->GetPositionX(), tankPos->GetPositionY()); + if (distToTankPos > 2.0f) + { + float X = mainTank->GetPositionX(); + float Y = mainTank->GetPositionY(); + TryMoveToPosition(X, Y, 840.857f, false); + } + + // Check current target validity + Unit* currentTarget = bot->GetVictim(); + if (currentTarget && !IsValidCollectibleAdd(currentTarget)) + { + bot->SetTarget(ObjectGuid::Empty); + } + } } bool IccLichKingAddsAction::Execute(Event event) { - if (bot->HasAura(68985) || !bot->IsAlive()) // Don't process actions if bot is picked up by Val'kyr or is dead + if (bot->HasAura(SPELL_HARVEST_SOUL_VALKYR)) // Don't process actions if bot is picked up by Val'kyr return false; + Difficulty diff = bot->GetRaidDifficulty(); + + if (sPlayerbotAIConfig->EnableICCBuffs && diff && (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) + { + //------CHEAT------- + if (!bot->HasAura(SPELL_EXPERIENCED)) + bot->AddAura(SPELL_EXPERIENCED, bot); + + if (!bot->HasAura(SPELL_AGEIS_OF_DALARAN)) + bot->AddAura(SPELL_AGEIS_OF_DALARAN, bot); + + if (!bot->HasAura(SPELL_NO_THREAT) && !botAI->IsTank(bot)) + bot->AddAura(SPELL_NO_THREAT, bot); + + if (!bot->HasAura(SPELL_PAIN_SUPPRESION)) + bot->AddAura(SPELL_PAIN_SUPPRESION, bot); + //------CHEAT------- + } + Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); Unit* spiritWarden = AI_VALUE2(Unit*, "find target", "spirit warden"); bool hasPlague = botAI->HasAura("Necrotic Plague", bot); + Unit* terenasMenethilHC = bot->FindNearestCreature(NPC_TERENAS_MENETHIL_HC, 55.0f); + Group* group = bot->GetGroup(); + if (group && boss && boss->HealthAbovePct(71)) + { + constexpr uint8_t skullIconId = 7; + ObjectGuid skullGuid = group->GetTargetIcon(skullIconId); + if (skullGuid != boss->GetGUID()) + group->SetTargetIcon(skullIconId, bot->GetGUID(), boss->GetGUID()); + } + + //-----------Valkyr bot suicide if group fails to kill Valkyr in time------------- comment out if you dont want it if (bot->HasAura(30440)) // Random aura tracking whether bot has fallen off edge / been thrown by Val'kyr { if (bot->GetPositionZ() > 779.0f) return JumpTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), 740.01f); else { - bot->KillPlayer(); // If bot has jumped past the kill Z (780), kill + bot->Kill(bot, bot); // If bot has jumped past the kill Z (780), **Now it is fixed and bots will actually die instead of beeing frozen** return true; } } - if (!boss) - {} - else + bool hasWinterAura = boss && (boss->HasAura(SPELL_REMORSELESS_WINTER1) || boss->HasAura(SPELL_REMORSELESS_WINTER2) || boss->HasAura(SPELL_REMORSELESS_WINTER3) || + boss->HasAura(SPELL_REMORSELESS_WINTER4)); + bool hasWinter2Aura = boss && (boss->HasAura(SPELL_REMORSELESS_WINTER5) || boss->HasAura(SPELL_REMORSELESS_WINTER6) || boss->HasAura(SPELL_REMORSELESS_WINTER7) || + boss->HasAura(SPELL_REMORSELESS_WINTER8)); + + if (boss && boss->GetHealthPct() < 70 && boss->GetHealthPct() > 40 && !hasWinterAura && + !hasWinter2Aura) // If boss is in p2, check if bot has been thrown off platform { + float dx = bot->GetPositionX() - 503.0f; + float dy = bot->GetPositionY() - (-2124.0f); + float distance = sqrt(dx * dx + dy * dy); // Calculate distance from the center of the platform - bool hasWinterAura = boss->HasAura(72259) || boss->HasAura(74273) || boss->HasAura(74274) || boss->HasAura(74275); - bool hasWinter2Aura = boss->HasAura(68981) || boss->HasAura(74270) || boss->HasAura(74271) || boss->HasAura(74272); - - if (boss && boss->GetHealthPct() < 70 && boss->GetHealthPct() > 40 && !hasWinterAura && - !hasWinter2Aura) // If boss is in p2, check if bot has been thrown off platform + if (distance > 52.0f && distance < 70.0f && + bot->GetPositionZ() > 844) // If bot has fallen off edge, distance is over 52 { - float dx = bot->GetPositionX() - 503.0f; - float dy = bot->GetPositionY() - (-2124.0f); - float distance = sqrt(dx * dx + dy * dy); // Calculate distance from the center of the platform - - if (distance > 52.0f && distance < 70.0f && - bot->GetPositionZ() > 844) // If bot has fallen off edge, distance is over 52 - { - bot->AddAura(30440, bot); // Apply random 30 sec aura to track that we've initiated a jump - return JumpTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), + bot->AddAura(30440, bot); // Apply random 30 sec aura to track that we've initiated a jump + return JumpTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), 740.01f); // Start jumping to the abyss + } + } + //-----------Valkyr bot suicide if group fails to kill Valkyr in time------------- comment out if you dont want it + + // Handle teleportation fixes + HandleTeleportationFixes(diff, terenasMenethilHC); + + // Handle heroic mode spirit bomb avoidance for main tank + if (HandleSpiritBombAvoidance(diff, terenasMenethilHC)) + return true; + + // Handle non-main tank positioning in heroic mode + HandleHeroicNonTankPositioning(diff, terenasMenethilHC); + + // Handle spirit marking and targeting in heroic mode + HandleSpiritMarkingAndTargeting(diff, terenasMenethilHC); + + if (terenasMenethilHC) + return false; + + // Handle quake mechanics + if (HandleQuakeMechanics(boss)) + return true; + + // Handle shambling horror interactions + HandleShamblingHorrors(boss, hasPlague); + + // Handle assist tank add management + if (HandleAssistTankAddManagement(boss, diff)) + return true; + + // Handle melee positioning + HandleMeleePositioning(boss, hasPlague, diff); + + // Handle main tank targeting in heroic + HandleMainTankTargeting(boss, diff); + + // Handle non-tank positioning in heroic + HandleNonTankHeroicPositioning(boss, diff, hasPlague); + + // Handle ranged positioning + HandleRangedPositioning(boss, hasPlague, diff); + + // Handle defile mechanics + HandleDefileMechanics(boss, diff); + + // Handle Val'kyr mechanics + HandleValkyrMechanics(diff); + + // Handle vile spirit mechanics + HandleVileSpiritMechanics(); + + return false; +} + +void IccLichKingAddsAction::HandleTeleportationFixes(Difficulty diff, Unit* terenasMenethilHC) +{ + // temp soultion for bots when they get teleport far away into another dimension (they are unable to attack spirit + // warden) in heroic they will be in safe spot while player is surviving vile spirits + if (!(diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC) && + abs(bot->GetPositionY() - -2095.7915f) > 200.0f) + { + bot->TeleportTo(bot->GetMapId(), ICC_LICH_KING_ADDS_POSITION.GetPositionX(), + ICC_LICH_KING_ADDS_POSITION.GetPositionY(), ICC_LICH_KING_ADDS_POSITION.GetPositionZ(), + bot->GetOrientation()); + } + + // temp solution for bots going underground due to buggy ice platfroms and adds that go underground + if (abs(bot->GetPositionZ() - 840.857f) > 1.0f && !botAI->GetAura("Harvest Soul", bot, false, false) && + !botAI->GetAura("Harvest Souls", bot, false, false)) + bot->TeleportTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), 840.857f, bot->GetOrientation()); + + if (abs(bot->GetPositionZ() - 1049.865f) > 5.0f && botAI->GetAura("Harvest Soul", bot, false, false) && + terenasMenethilHC) + bot->TeleportTo(bot->GetMapId(), terenasMenethilHC->GetPositionX(), terenasMenethilHC->GetPositionY(), 1049.865f, + bot->GetOrientation()); +} + +bool IccLichKingAddsAction::HandleSpiritBombAvoidance(Difficulty diff, Unit* terenasMenethilHC) +{ + if (!botAI->IsMainTank(bot) || !terenasMenethilHC || !diff || + !(diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) + return false; + + std::map spiritBombs; + + // Gather all spirit bombs using their GUIDs for reliable tracking + GuidVector npcs1 = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto& npcGuid : npcs1) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_SPIRIT_BOMB) + { + spiritBombs[npcGuid] = unit; + } + } + + // Only proceed if there are actually spirit bombs present + if (spiritBombs.empty()) + return false; + + const float SAFE_DISTANCE = 14.0f; // Minimum safe horizontal distance + const float SAFE_HEIGHT_DIFF = 12.0f; // Safe if Z difference is greater than this + const float BOMB_DENSITY_RADIUS = 10.0f; // Radius for counting nearby bombs + const float BOMB_COUNT_PENALTY = 10.0f; // Score penalty per bomb in vicinity + const float MAX_HEIGHT_DIFF = 8.0f; // Increased from 5.0f for more flexibility + + // First check if current position is already safe + bool currentPositionSafe = true; + float minDistanceToAnyBomb = std::numeric_limits::max(); + + // Clean up invalid bombs and check current position safety + auto it = spiritBombs.begin(); + while (it != spiritBombs.end()) + { + Unit* verifiedBomb = botAI->GetUnit(it->first); + if (!verifiedBomb || !verifiedBomb->IsAlive() || verifiedBomb->GetEntry() != NPC_SPIRIT_BOMB) + { + it = spiritBombs.erase(it); + continue; + } + + float dx = bot->GetPositionX() - verifiedBomb->GetPositionX(); + float dy = bot->GetPositionY() - verifiedBomb->GetPositionY(); + float dz = bot->GetPositionZ() - verifiedBomb->GetPositionZ(); + float horizontalDistance = sqrt(dx * dx + dy * dy); + float verticalDistance = fabs(dz); + + minDistanceToAnyBomb = std::min(minDistanceToAnyBomb, horizontalDistance); + + // Position is dangerous if horizontally close AND not high enough above/below + if (horizontalDistance < SAFE_DISTANCE && verticalDistance <= SAFE_HEIGHT_DIFF) + { + currentPositionSafe = false; + } + ++it; + } + + // If no valid bombs remain after cleanup, exit early + if (spiritBombs.empty()) + { + return false; + } + + // Only move if current position is unsafe + if (!currentPositionSafe) + { + float bestScore = -std::numeric_limits::max(); + float bestX = 0, bestY = 0, bestZ = 0; + bool foundSafePosition = false; + + // Multi-distance search to avoid getting stuck + std::vector searchDistances = {6.0f, 10.0f, 15.0f, 20.0f, 25.0f}; + + for (float searchDistance : searchDistances) + { + // Try 36 different angles for thorough coverage + for (int i = 0; i < 36; i++) + { + float testAngle = i * 2 * M_PI / 36; + + float testX = bot->GetPositionX() + searchDistance * cos(testAngle); + float testY = bot->GetPositionY() + searchDistance * sin(testAngle); + float testZ = bot->GetPositionZ(); + + bot->UpdateAllowedPositionZ(testX, testY, testZ); + + // More lenient LOS and height checks + bool validPosition = true; + float heightDiff = fabs(testZ - bot->GetPositionZ()); + + // Skip positions that are too high/low or not in LOS + if (heightDiff >= MAX_HEIGHT_DIFF) + { + validPosition = false; + } + + // Only check LOS if height difference is reasonable + if (validPosition && !bot->IsWithinLOS(testX, testY, testZ)) + { + validPosition = false; + } + + if (!validPosition) + continue; + + // Check position safety and bomb density + bool positionSafe = true; + float minDistAtPos = std::numeric_limits::max(); + int bombCountInVicinity = 0; + + for (const auto& bombPair : spiritBombs) + { + Unit* bomb = bombPair.second; + if (!bomb || !bomb->IsAlive()) + continue; + + float dx = testX - bomb->GetPositionX(); + float dy = testY - bomb->GetPositionY(); + float dz = testZ - bomb->GetPositionZ(); + float horizontalDist = sqrt(dx * dx + dy * dy); + float verticalDist = fabs(dz); + + // Track minimum distance to any bomb + minDistAtPos = std::min(minDistAtPos, horizontalDist); + + // Count bombs within density radius + if (horizontalDist < BOMB_DENSITY_RADIUS) + { + bombCountInVicinity++; + } + + // Check safety condition + if (horizontalDist < SAFE_DISTANCE && verticalDist <= SAFE_HEIGHT_DIFF) + { + positionSafe = false; + break; + } + } + + // Skip unsafe positions + if (!positionSafe) + continue; + + // Calculate composite score with distance bonus to prefer closer safe positions + float distanceBonus = std::max(0.0f, 30.0f - searchDistance); // Prefer closer positions + float score = minDistAtPos - (bombCountInVicinity * BOMB_COUNT_PENALTY) + distanceBonus; + + // Update best position if this one is better + if (score > bestScore) + { + bestScore = score; + bestX = testX; + bestY = testY; + bestZ = testZ; + foundSafePosition = true; + } + } + + // If we found a safe position at this distance, use it (prefer closer positions) + if (foundSafePosition && searchDistance <= 15.0f) + break; + } + + // Move to the best position found + if (foundSafePosition) + { + // Final validation before moving + if (bot->IsWithinLOS(bestX, bestY, bestZ) && fabs(bestZ - bot->GetPositionZ()) <= MAX_HEIGHT_DIFF) + { + MoveTo(bot->GetMapId(), bestX, bestY, bestZ, false, false, false, true, + MovementPriority::MOVEMENT_FORCED); + } + } + } + return true; +} + +void IccLichKingAddsAction::HandleHeroicNonTankPositioning(Difficulty diff, Unit* terenasMenethilHC) +{ + if (!terenasMenethilHC || botAI->IsMainTank(bot) || !diff || + !(diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) + return; + + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + // Only move if significantly far from main tank (increased threshold to reduce jittery movement) + if (mainTank && bot->GetExactDist2d(mainTank->GetPositionX(), mainTank->GetPositionY()) > 2.0f) + { + MoveTo(bot->GetMapId(), mainTank->GetPositionX(), mainTank->GetPositionY(), mainTank->GetPositionZ(), false, + false, false, true, MovementPriority::MOVEMENT_FORCED); + } +} + +void IccLichKingAddsAction::HandleSpiritMarkingAndTargeting(Difficulty diff, Unit* terenasMenethilHC) +{ + if (!terenasMenethilHC || botAI->IsMainTank(bot) || !diff || + !(diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) + return; + + Group* group = bot->GetGroup(); + if (!group) + return; + + static constexpr uint8_t STAR_ICON_INDEX = 0; + static constexpr float MAX_Z_DIFF = 20.0f; + + // Check if current marked target is still valid and threatening + Unit* currentMarkedTarget = botAI->GetUnit(group->GetTargetIcon(STAR_ICON_INDEX)); + bool needNewMark = !currentMarkedTarget || !currentMarkedTarget->IsAlive(); + + // Check if current marked spirit is targeting a group member + bool currentTargetingGroupMember = false; + if (currentMarkedTarget && currentMarkedTarget->IsAlive()) + { + Unit* spiritTarget = currentMarkedTarget->GetVictim(); + if (spiritTarget && spiritTarget->GetTypeId() == TYPEID_PLAYER) + { + if (Group* spiritTargetGroup = spiritTarget->ToPlayer()->GetGroup()) + { + if (spiritTargetGroup->GetGUID() == group->GetGUID()) + { + currentTargetingGroupMember = true; + } } } } - //temp solution for bots going underground due to buggy ice platfroms and adds that go underground - if (abs(bot->GetPositionZ() - 840.857f) > 1.0f) - return bot->TeleportTo(bot->GetMapId(), bot->GetPositionX(), - bot->GetPositionY(), 840.857f, bot->GetOrientation()); - - //temp soultion for bots when they get teleport far away into another dimension (they are unable to attack spirit warden) in heroic they will be in safe spot while player is surviving vile spirits - Difficulty diff = bot->GetRaidDifficulty(); - if (!(diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC) && abs(bot->GetPositionY() - -2095.7915f) > 200.0f) + // Only search for new target if we need to mark OR if we can find a higher priority target + if (needNewMark || !currentTargetingGroupMember) { - return bot->TeleportTo(bot->GetMapId(), ICC_LICH_KING_ADDS_POSITION.GetPositionX(), - ICC_LICH_KING_ADDS_POSITION.GetPositionY(), ICC_LICH_KING_ADDS_POSITION.GetPositionZ(), bot->GetOrientation()); + Unit* prioritySpirit = nullptr; // Spirit targeting group member + Unit* nearestSpirit = nullptr; // Fallback: nearest spirit + float priorityDist = 100.0f; + float nearestDist = 100.0f; + + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (!unit || !unit->IsAlive() || !unit->isTargetableForAttack()) + continue; + + uint32 entry = unit->GetEntry(); + if (entry == NPC_WICKED_SPIRIT1 || entry == NPC_WICKED_SPIRIT2 || entry == NPC_WICKED_SPIRIT3 || + entry == NPC_WICKED_SPIRIT4) + { + // Check Z-axis difference first + float zDiff = std::abs(unit->GetPositionZ() - bot->GetPositionZ()); + if (zDiff <= MAX_Z_DIFF) + { + float dist = bot->GetDistance(unit->GetPositionX(), unit->GetPositionY(), unit->GetPositionZ()); + + // Check if this spirit is targeting a group member + bool targetingGroupMember = false; + Unit* spiritTarget = unit->GetVictim(); + if (spiritTarget && spiritTarget->GetTypeId() == TYPEID_PLAYER) + { + if (Group* spiritTargetGroup = spiritTarget->ToPlayer()->GetGroup()) + { + if (spiritTargetGroup->GetGUID() == group->GetGUID()) + { + targetingGroupMember = true; + } + } + } + + // Priority: spirits targeting group members + if (targetingGroupMember) + { + if (!prioritySpirit || dist < priorityDist) + { + prioritySpirit = unit; + priorityDist = dist; + } + } + + // Fallback: track nearest spirit regardless of target + if (!nearestSpirit || dist < nearestDist) + { + nearestSpirit = unit; + nearestDist = dist; + } + } + } + } + + // Mark priority spirit if found, otherwise fall back to nearest + Unit* spiritToMark = prioritySpirit ? prioritySpirit : nearestSpirit; + + // Only mark if we found a better target or need a new mark + if (spiritToMark && (needNewMark || (prioritySpirit && !currentTargetingGroupMember))) + { + group->SetTargetIcon(STAR_ICON_INDEX, bot->GetGUID(), spiritToMark->GetGUID()); + } } - if (!boss) - {} - else if (boss && boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(72262) && bot->GetExactDist2d(boss) > 20.0f) //quake + // Only ranged DPS use star for RTI + if (botAI->IsRangedDps(bot)) { - float angle = bot->GetAngle(boss); - float posX = bot->GetPositionX() + cos(angle) * 10.0f; - float posY = bot->GetPositionY() + sin(angle) * 10.0f; - return MoveTo(bot->GetMapId(), posX, posY, boss->GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + context->GetValue("rti")->Set("star"); + Unit* starTarget = botAI->GetUnit(group->GetTargetIcon(STAR_ICON_INDEX)); + if (starTarget && starTarget->IsAlive()) + { + bot->SetTarget(starTarget->GetGUID()); + bot->SetFacingToObject(starTarget); + Attack(starTarget); + bot->Kill(bot, starTarget); //temp solution since bots struggle to kill spirits in time, they have to follow main tank closely so that they do not get hit by bomb, thus making them have very limited time to react + } + } +} + +bool IccLichKingAddsAction::HandleQuakeMechanics(Unit* boss) +{ + if (!boss || !boss->HasUnitState(UNIT_STATE_CASTING) || !boss->FindCurrentSpellBySpellId(SPELL_QUAKE)) + return false; + + float currentDistance = bot->GetExactDist2d(boss); + + // If already at ideal distance (40f), no need to move + if (currentDistance >= 35.0f && currentDistance <= 45.0f) + return false; + + if (bot->HasUnitState(UNIT_STATE_CASTING)) + botAI->Reset(); + + float botX = bot->GetPositionX(); + float botY = bot->GetPositionY(); + float targetX, targetY; + + if (!botAI->IsTank(bot)) + { + // Non-tanks: use offset position as direction guide + float offsetX = boss->GetPositionX() + 15.0f; + float offsetY = boss->GetPositionY() + 15.0f; + + // Calculate direction towards offset position + float dx = offsetX - botX; + float dy = offsetY - botY; + float distance = sqrt(dx * dx + dy * dy); + + if (distance > 0.0f) + { + // Move 10f towards offset position + float ratio = 10.0f / distance; + targetX = botX + dx * ratio; + targetY = botY + dy * ratio; + } + else + { + targetX = botX; + targetY = botY; + } + } + else + { + // Tanks: use offset position as direction guide + float offsetX = boss->GetPositionX() - 15.0f; + float offsetY = boss->GetPositionY() - 15.0f; + + // Calculate direction towards offset position + float dx = offsetX - botX; + float dy = offsetY - botY; + float distance = sqrt(dx * dx + dy * dy); + + if (distance > 0.0f) + { + // Move 10f towards offset position + float ratio = 10.0f / distance; + targetX = botX + dx * ratio; + targetY = botY + dy * ratio; + } + else + { + targetX = botX; + targetY = botY; + } } + MoveTo(bot->GetMapId(), targetX, targetY, boss->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); + + return false; +} + +void IccLichKingAddsAction::HandleShamblingHorrors(Unit* boss, bool hasPlague) +{ // Find closest shambling horror GuidVector npcs2 = AI_VALUE(GuidVector, "nearest hostile npcs"); Unit* closestHorror = nullptr; @@ -3703,8 +7735,8 @@ bool IccLichKingAddsAction::Execute(Event event) { Unit* unit = botAI->GetUnit(npc); if (unit && unit->IsAlive() && - (unit->GetEntry() == 37698 || unit->GetEntry() == 39299 || unit->GetEntry() == 39300 || - unit->GetEntry() == 39301)) // Shambling horror entries + (unit->GetEntry() == NPC_SHAMBLING_HORROR1 || unit->GetEntry() == NPC_SHAMBLING_HORROR2 || unit->GetEntry() == NPC_SHAMBLING_HORROR3 || + unit->GetEntry() == NPC_SHAMBLING_HORROR4)) // Shambling horror entries { float distance = bot->GetDistance(unit); if (distance < minHorrorDistance) @@ -3715,87 +7747,390 @@ bool IccLichKingAddsAction::Execute(Event event) } } + /* if (!closestHorror || hasPlague) - {} - else if (!hasPlague && closestHorror->isInFront(bot) && closestHorror->IsAlive() && !botAI->IsTank(bot) && bot->GetDistance2d(closestHorror) < 3.0f) + { + } + else if (!hasPlague && closestHorror->isInFront(bot) && closestHorror->IsAlive() && !botAI->IsTank(bot) && + bot->GetDistance2d(closestHorror) < 3.0f) return FleePosition(closestHorror->GetPosition(), 2.0f, 250U); + */ // If bot is hunter and shambling is enraged, use Tranquilizing Shot if (bot->getClass() == CLASS_HUNTER && closestHorror && botAI->HasAura("Enrage", closestHorror)) - return botAI->CastSpell("Tranquilizing Shot", closestHorror); + botAI->CastSpell("Tranquilizing Shot", closestHorror); +} - if (!boss) - {} - else if (botAI->IsAssistTank(bot) && !boss->HealthBelowPct(71)) +bool IccLichKingAddsAction::HandleAssistTankAddManagement(Unit* boss, Difficulty diff) +{ + if (!botAI->IsAssistTank(bot) || !boss || boss->HealthBelowPct(71)) + return false; + + // Find all adds and categorize them by targeting status + GuidVector targets = AI_VALUE(GuidVector, "possible targets"); + std::vector addsNotTargetingUs; + std::vector addsTargetingUs; + + for (auto i = targets.begin(); i != targets.end(); ++i) { - // Actively look for any shambling/spirit/ghoul that isn't targeting us - GuidVector targets = AI_VALUE(GuidVector, "possible targets"); - for (auto i = targets.begin(); i != targets.end(); ++i) + Unit* unit = botAI->GetUnit(*i); + if (unit && unit->IsAlive() && + (unit->GetEntry() == NPC_SHAMBLING_HORROR1 || unit->GetEntry() == NPC_SHAMBLING_HORROR2 || + unit->GetEntry() == NPC_SHAMBLING_HORROR3 || + unit->GetEntry() == NPC_SHAMBLING_HORROR4 || // Shambling entry + unit->GetEntry() == NPC_RAGING_SPIRIT1 || unit->GetEntry() == NPC_RAGING_SPIRIT2 || + unit->GetEntry() == NPC_RAGING_SPIRIT3 || unit->GetEntry() == NPC_RAGING_SPIRIT4 || // Spirits entry + unit->GetEntry() == NPC_DRUDGE_GHOUL1 || unit->GetEntry() == NPC_DRUDGE_GHOUL2 || + unit->GetEntry() == NPC_DRUDGE_GHOUL3 || unit->GetEntry() == NPC_DRUDGE_GHOUL4)) // Drudge Ghouls entry { - Unit* unit = botAI->GetUnit(*i); - if (unit && unit->IsAlive() && - (unit->GetEntry() == 37698 || unit->GetEntry() == 39299 || unit->GetEntry() == 39300 || - unit->GetEntry() == 39301 || // Shambling entry - unit->GetEntry() == 36701 || unit->GetEntry() == 39302 || unit->GetEntry() == 39303 || - unit->GetEntry() == 39304 || // Spirits entry - unit->GetEntry() == 37695 || unit->GetEntry() == 39309 || unit->GetEntry() == 39310 || - unit->GetEntry() == 39311)) // Drudge Ghouls entry + if (unit->GetVictim() == bot) { - if (!unit->GetVictim() || unit->GetVictim() != bot) + addsTargetingUs.push_back(unit->GetGUID()); + } + else + { + addsNotTargetingUs.push_back(unit->GetGUID()); + } + } + } + + // If there are adds not targeting us, we need to collect them all + if (!addsNotTargetingUs.empty()) + { + // Find the highest priority target (Shamblings first, then closest) + Unit* priorityTarget = nullptr; + Unit* closestAdd = nullptr; + float closestDist = 999.0f; + + for (const ObjectGuid& addGuid : addsNotTargetingUs) + { + Unit* add = botAI->GetUnit(addGuid); + if (add && add->IsAlive()) + { + // Shambling takes absolute priority regardless of distance + if (add->GetEntry() == NPC_SHAMBLING_HORROR1 || add->GetEntry() == NPC_SHAMBLING_HORROR2 || + add->GetEntry() == NPC_SHAMBLING_HORROR3 || add->GetEntry() == NPC_SHAMBLING_HORROR4) { - bot->SetFacingToObject(unit); - return Attack(unit); // Pick up any shambling that isn't targeting us + priorityTarget = add; + break; // Found shambling, stop looking + } + + // Track closest add as backup + float dist = bot->GetExactDist2d(add); + if (dist < closestDist) + { + closestDist = dist; + closestAdd = add; } } } - // Return to adds position if we're too far - if (bot->GetExactDist2d(ICC_LICH_KING_ADDS_POSITION) > 1.0f && !boss->HealthBelowPct(71)) - { - return MoveTo(bot->GetMapId(), ICC_LICH_KING_ADDS_POSITION.GetPositionX(), - ICC_LICH_KING_ADDS_POSITION.GetPositionY(), ICC_LICH_KING_ADDS_POSITION.GetPositionZ(), false, - false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); - } - - return false; // Stay in position and keep facing current target - } + // Choose target: Shambling first, then closest + Unit* targetToAttack = priorityTarget ? priorityTarget : closestAdd; - if (!boss) - {} - else if (botAI->IsMelee(bot) && !botAI->IsAssistTank(bot) && !boss->HealthBelowPct(71) && !hasPlague) + if (targetToAttack) + { + // Generate threat on ALL adds not targeting us using ranged abilities + for (const ObjectGuid& addGuid : addsNotTargetingUs) + { + Unit* add = botAI->GetUnit(addGuid); + if (add && add->IsAlive()) + { + float dist = bot->GetExactDist2d(add); + + // Use ranged threat generation if within range + if (dist <= 30.0f) + { + // Try taunt first if available + if (botAI->CastSpell("taunt", add)) + { + continue; + } + // Fall back to ranged attack + else if (botAI->CastSpell("shoot", add) || botAI->CastSpell("throw", add)) + { + continue; + } + // Last resort - basic attack state update + else + { + bot->AttackerStateUpdate(add); + } + } + } + } + + // Move towards and attack the priority target + float distToTarget = bot->GetExactDist2d(targetToAttack); + + // If we're too far from our priority target, move closer + if (distToTarget > 5.0f) + { + MoveTo(bot->GetMapId(), targetToAttack->GetPositionX(), targetToAttack->GetPositionY(), + targetToAttack->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_FORCED, + true, false); + } + else + { + // We're close enough, set target and attack + bot->SetTarget(targetToAttack->GetGUID()); + bot->SetFacingToObject(targetToAttack); + Attack(targetToAttack); + } + } + } + // If all adds are targeting us or there are no adds, maintain position based on difficulty + else { - float currentDist = bot->GetDistance(ICC_LICH_KING_MELEE_POSITION); - if (currentDist > 2.0f) + // In heroic mode, stay at melee position + if (diff && (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) { - return MoveTo(bot->GetMapId(), ICC_LICH_KING_MELEE_POSITION.GetPositionX(), - ICC_LICH_KING_MELEE_POSITION.GetPositionY(), ICC_LICH_KING_MELEE_POSITION.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + + if (bot->GetExactDist2d(ICC_LICH_KING_ASSISTHC_POSITION.GetPositionX(), + ICC_LICH_KING_ASSISTHC_POSITION.GetPositionY()) > 2.0f) + { + MoveTo(bot->GetMapId(), ICC_LICH_KING_ASSISTHC_POSITION.GetPositionX(), + ICC_LICH_KING_ASSISTHC_POSITION.GetPositionY(), ICC_LICH_KING_ASSISTHC_POSITION.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); + } + } + // In normal mode, stay at adds position + else + { + if (bot->GetExactDist2d(ICC_LICH_KING_ADDS_POSITION) > 2.0f) + { + MoveTo(bot->GetMapId(), ICC_LICH_KING_ADDS_POSITION.GetPositionX(), + ICC_LICH_KING_ADDS_POSITION.GetPositionY(), ICC_LICH_KING_ADDS_POSITION.GetPositionZ(), false, + false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); + } + } + + // If we have adds targeting us, attack them with stable target selection + if (!addsTargetingUs.empty()) + { + Unit* currentTarget = bot->GetVictim(); + bool needNewTarget = true; + + // Check if current target is still valid (alive and attacking us) + if (currentTarget && currentTarget->IsAlive()) + { + for (const ObjectGuid& addGuid : addsTargetingUs) + { + if (addGuid == currentTarget->GetGUID()) + { + needNewTarget = false; + break; + } + } + } + + // Only pick new target if current one is invalid + if (needNewTarget) + { + currentTarget = nullptr; + + // Priority 1: Shambling Horror + for (const ObjectGuid& addGuid : addsTargetingUs) + { + Unit* add = botAI->GetUnit(addGuid); + if (add && add->IsAlive()) + { + if (add->GetEntry() == NPC_SHAMBLING_HORROR1 || add->GetEntry() == NPC_SHAMBLING_HORROR2 || + add->GetEntry() == NPC_SHAMBLING_HORROR3 || add->GetEntry() == NPC_SHAMBLING_HORROR4) + { + currentTarget = add; + break; + } + } + } + + // Priority 2: Any other add if no Shambling Horror + if (!currentTarget) + { + for (const ObjectGuid& addGuid : addsTargetingUs) + { + Unit* add = botAI->GetUnit(addGuid); + if (add && add->IsAlive()) + { + currentTarget = add; + break; + } + } + } + } + + if (currentTarget) + { + bot->SetTarget(currentTarget->GetGUID()); + bot->SetFacingToObject(currentTarget); + Attack(currentTarget); + } } } + return false; +} - if (!boss) - {} - else if (botAI->IsRanged(bot) && !boss->HealthBelowPct(71) && !hasPlague && !(diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) +void IccLichKingAddsAction::HandleMeleePositioning(Unit* boss, bool hasPlague, Difficulty diff) +{ + if (!boss || !botAI->IsMelee(bot) || botAI->IsAssistTank(bot) || boss->HealthBelowPct(71) || hasPlague) + return; + + if (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC) + return; + + float currentDist = bot->GetDistance(ICC_LICH_KING_MELEE_POSITION); + + if (currentDist > 6.0f && !botAI->IsMainTank(bot)) { - float currentDist = bot->GetDistance(ICC_LICH_KING_RANGED_POSITION); - if (currentDist > 2.0f) + MoveTo(bot->GetMapId(), ICC_LICH_KING_MELEE_POSITION.GetPositionX(), + ICC_LICH_KING_MELEE_POSITION.GetPositionY(), ICC_LICH_KING_MELEE_POSITION.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_FORCED, true, false); + } + + if (currentDist > 6.0f && botAI->IsMainTank(bot) && boss && boss->GetVictim() == bot) + { + Position currentPos = bot->GetPosition(); + Position targetPos = ICC_LICH_KING_MELEE_POSITION; + + // Calculate direction vector + float dx = targetPos.GetPositionX() - currentPos.GetPositionX(); + float dy = targetPos.GetPositionY() - currentPos.GetPositionY(); + + // Calculate distance and normalize direction + float distance = sqrt(dx * dx + dy * dy); + if (distance > 0.1) { - return MoveTo(bot->GetMapId(), ICC_LICH_KING_RANGED_POSITION.GetPositionX(), - ICC_LICH_KING_RANGED_POSITION.GetPositionY(), ICC_LICH_KING_RANGED_POSITION.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); + dx /= distance; + dy /= distance; + } + + // Calculate intermediate position (3f towards target) + float step = std::min(3.0f, distance - 1.0f); // Don't overshoot the target + if (step > 0) + { + float intermediateX = currentPos.GetPositionX() + dx * step; + float intermediateY = currentPos.GetPositionY() + dy * step; + + MoveTo(bot->GetMapId(), intermediateX, intermediateY, bot->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + } + else + { + // If we're within 1.0f + 3.0f of the target, move directly to it + MoveTo(bot->GetMapId(), targetPos.GetPositionX(), targetPos.GetPositionY(), bot->GetPositionZ(), false, + false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); + } + } +} + +void IccLichKingAddsAction::HandleMainTankTargeting(Unit* boss, Difficulty diff) +{ + if (!botAI->IsMainTank(bot) || !boss) + return; + + if (!(diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) + return; + + if (boss->HealthBelowPct(71) || boss->GetVictim() == bot) + return; + + bot->SetTarget(boss->GetGUID()); + bot->SetFacingToObject(boss); + Attack(boss); +} + +void IccLichKingAddsAction::HandleNonTankHeroicPositioning(Unit* boss, Difficulty diff, bool hasPlague) +{ + if (botAI->IsTank(bot) || !boss) + return; + + if (!(diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) + return; + + if (boss->HealthBelowPct(71) || hasPlague) + return; + + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + if (!mainTank) + return; + + if (bot->GetDistance2d(mainTank->GetPositionX(), mainTank->GetPositionY()) > 20.0f && (bot->getClass() == CLASS_HUNTER)) + { + botAI->Reset(); + + // Calculate direction vector to main tank + float dx = mainTank->GetPositionX() - bot->GetPositionX(); + float dy = mainTank->GetPositionY() - bot->GetPositionY(); + + // Normalize and scale to 2f increments + float distance = sqrt(dx * dx + dy * dy); + if (distance > 0) + { + dx = dx / distance * 2.0f; + dy = dy / distance * 2.0f; + + // Calculate new position (2f closer to main tank) + float newX = bot->GetPositionX() + dx; + float newY = bot->GetPositionY() + dy; + + MoveTo(bot->GetMapId(), newX, newY, bot->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); } } + if (bot->GetDistance2d(mainTank->GetPositionX(), mainTank->GetPositionY()) > 1.0f && (bot->getClass() != CLASS_HUNTER)) + { + botAI->Reset(); + MoveTo(bot->GetMapId(), mainTank->GetPositionX(), mainTank->GetPositionY(), bot->GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_FORCED, true, false); + } +} + +void IccLichKingAddsAction::HandleRangedPositioning(Unit* boss, bool hasPlague, Difficulty diff) +{ + if (!boss || !botAI->IsRanged(bot) || boss->HealthBelowPct(71) || hasPlague) + return; + + if (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC) + return; + + float currentDist = bot->GetDistance(ICC_LICH_KING_RANGED_POSITION); + if (currentDist > 2.0f) + { + MoveTo(bot->GetMapId(), ICC_LICH_KING_RANGED_POSITION.GetPositionX(), + ICC_LICH_KING_RANGED_POSITION.GetPositionY(), ICC_LICH_KING_RANGED_POSITION.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_FORCED, true, false); + } +} + +void IccLichKingAddsAction::HandleDefileMechanics(Unit* boss, Difficulty diff) +{ + if (!boss) + return; + + // Constants + const float BASE_RADIUS = 6.0f; + const float SAFETY_MARGIN = 3.0f; + const float MOVE_DISTANCE = 5.0f; + const float SPREAD_DISTANCE = 12.0f; + const float FIXED_Z = 840.857f; + const float MAX_HEIGHT_DIFF = 5.0f; + const float MIN_PLAYER_SPACING = 5.0f; + const float MAX_BOSS_DISTANCE = 40.0f; + const int ANGLE_TESTS = 16; + const int MAX_ANGLE_OFFSETS = 8; + + // Gather all defile units std::vector defiles; Unit* closestDefile = nullptr; float closestDistance = std::numeric_limits::max(); - // First gather all defiles - GuidVector npcs1 = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto& npc : npcs1) + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto& npc : npcs) { Unit* unit = botAI->GetUnit(npc); - if (unit && unit->IsAlive() && unit->GetEntry() == 38757) + if (unit && unit->IsAlive() && unit->GetEntry() == DEFILE_NPC_ID) { defiles.push_back(unit); float dist = bot->GetDistance(unit); @@ -3806,81 +8141,75 @@ bool IccLichKingAddsAction::Execute(Event event) } } } + + // Only process defile avoidance if defiles exist if (!defiles.empty()) { - float baseRadius = 6.0f; - float safetyMargin = 3.0f; // Fixed 3-yard safety margin - - // First, find if we need to move from any defile + // Check if we need to move away from defiles bool needToMove = false; - float bestAngle = 0.0f; - float maxSafeDistance = 0.0f; + float botX = bot->GetPositionX(); + float botY = bot->GetPositionY(); - // Check all defiles first to see if we need to move for (Unit* defile : defiles) { if (!defile || !defile->IsAlive()) continue; - float currentRadius = baseRadius; - Aura* growAura = defile->GetAura(72756); - if (!growAura) + // Calculate current defile radius including growth + float currentRadius = BASE_RADIUS; + Aura* growAura = nullptr; + + // Find growth aura + for (size_t i = 0; i < DEFILE_AURA_COUNT; i++) { - growAura = defile->GetAura(74162); - if (!growAura) - growAura = defile->GetAura(74163); - if (!growAura) - growAura = defile->GetAura(74164); // 25hc mabye 74164 + growAura = defile->GetAura(DEFILE_AURAS[i]); + if (growAura) + break; } if (growAura) { uint8 stacks = growAura->GetStackAmount(); - if (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_10MAN_NORMAL) - currentRadius = baseRadius + (stacks * 1.4f); - else - currentRadius = baseRadius + (stacks * 0.95f); + float growthMultiplier = + (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_10MAN_NORMAL) ? 1.4f : 0.95f; + currentRadius = BASE_RADIUS + (stacks * growthMultiplier); } - float dx = bot->GetPositionX() - defile->GetPositionX(); - float dy = bot->GetPositionY() - defile->GetPositionY(); + // Check if bot is too close to this defile + float dx = botX - defile->GetPositionX(); + float dy = botY - defile->GetPositionY(); float distanceToCenter = sqrt(dx * dx + dy * dy); - if (distanceToCenter < (currentRadius + safetyMargin)) + if (distanceToCenter < (currentRadius + SAFETY_MARGIN)) { needToMove = true; break; } } - // If we need to move, find the safest direction - if (!boss) - {} - else if (needToMove) + // Move away from defiles if needed + if (needToMove) { - // Try 16 different angles for more precise movement - for (int i = 0; i < 16; i++) + float bestAngle = 0.0f; + float maxSafetyScore = 0.0f; + bool foundSafePosition = false; + + // Test multiple angles to find safest escape route + for (int i = 0; i < ANGLE_TESTS; i++) { float testAngle = i * M_PI / 8; - float moveDistance = 5.0f; // Move in 5-yard increments - - float testX = bot->GetPositionX() + moveDistance * cos(testAngle); - float testY = bot->GetPositionY() + moveDistance * sin(testAngle); - float testZ = 840.857f; - + float testX = botX + MOVE_DISTANCE * cos(testAngle); + float testY = botY + MOVE_DISTANCE * sin(testAngle); + float testZ = FIXED_Z; + bot->UpdateAllowedPositionZ(testX, testY, testZ); - // Skip if not in LOS or too much height difference - if (!bot->IsWithinLOS(testX, testY, testZ) || - fabs(testZ - bot->GetPositionZ()) >= 5.0f) - { + // Skip invalid positions (LOS and height check) + if (!bot->IsWithinLOS(testX, testY, testZ) || fabs(testZ - bot->GetPositionZ()) >= MAX_HEIGHT_DIFF) continue; - } // Calculate minimum distance to any defile from this position float minDefileDistance = std::numeric_limits::max(); - float distanceToBoss = boss->GetDistance2d(testX, testY); - for (Unit* defile : defiles) { if (!defile || !defile->IsAlive()) @@ -3892,281 +8221,514 @@ bool IccLichKingAddsAction::Execute(Event event) minDefileDistance = std::min(minDefileDistance, dist); } - // Favor positions that are both safe from defiles and closer to the boss + // Calculate scoring (safety + boss proximity) + float distanceToBoss = boss->GetDistance2d(testX, testY); float safetyScore = minDefileDistance; - float bossScore = 100.0f - std::min(100.0f, distanceToBoss); // Convert distance to a 0-100 score - float totalScore = safetyScore + (bossScore * 0.5f); // Weight safety more than boss proximity + float bossScore = 100.0f - std::min(100.0f, distanceToBoss); + float totalScore = safetyScore + (bossScore * 0.5f); - // If this position is better than our previous best, update it - if (totalScore > maxSafeDistance) + if (totalScore > maxSafetyScore) { - maxSafeDistance = totalScore; + maxSafetyScore = totalScore; bestAngle = testAngle; + foundSafePosition = true; } } - // Move in the best direction found - if (maxSafeDistance > 0) + // Execute movement if safe position found + if (foundSafePosition && maxSafetyScore > 0) { - float moveDistance = 5.0f; - float testX = bot->GetPositionX() + moveDistance * cos(bestAngle); - float testY = bot->GetPositionY() + moveDistance * sin(bestAngle); - float testZ = 840.857f; - - bot->UpdateAllowedPositionZ(testX, testY, testZ); - return MoveTo(bot->GetMapId(), testX, testY, testZ, - false, false, false, true, MovementPriority::MOVEMENT_FORCED); + float moveX = botX + MOVE_DISTANCE * cos(bestAngle); + float moveY = botY + MOVE_DISTANCE * sin(bestAngle); + float moveZ = FIXED_Z; + + if (bot->HasUnitState(UNIT_STATE_CASTING)) + botAI->Reset(); + + bot->UpdateAllowedPositionZ(moveX, moveY, moveZ); + MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, true, + MovementPriority::MOVEMENT_FORCED); } } } - // Check if LK is casting Defile - make bots spread - if (!boss) - {} - else if (boss && boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(72762)) + // Handle Defile cast - spread positioning + if (boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(DEFILE_CAST_ID)) { + // Count players and determine bot's index uint32 playerCount = 0; uint32 botIndex = 0; uint32 currentIndex = 0; - + + float botX = bot->GetPositionX(); + float botY = bot->GetPositionY(); + Map::PlayerList const& players = bot->GetMap()->GetPlayers(); for (Map::PlayerList::const_iterator itr = players.begin(); itr != players.end(); ++itr) { Player* player = itr->GetSource(); if (!player || !player->IsAlive()) continue; - + if (player == bot) botIndex = currentIndex; - + currentIndex++; playerCount++; } - // Calculate this bot's preferred angle based on its index + // Calculate preferred spread angle based on bot index float preferredAngle = (float(botIndex) / float(playerCount)) * 2 * M_PI; - float moveDistance = 12.0f; // Fixed distance for consistent spread - - // Start checking from preferred angle, then try adjacent angles if blocked - float bestAngle = preferredAngle; bool foundSafeSpot = false; - - // Try positions at increasing offsets from preferred angle - for (int offset = 0; offset <= 8; offset++) // Try up to 8 positions on each side - { - for (int direction = -1; direction <= 1; direction += 2) // Check both clockwise and counterclockwise - { - if (offset == 0 && direction > 0) // Skip second check of preferred angle - continue; - - float testAngle = preferredAngle + (direction * offset * M_PI / 16); - float testX = bot->GetPositionX() + moveDistance * cos(testAngle); - float testY = bot->GetPositionY() + moveDistance * sin(testAngle); - float testZ = 840.857f; - - bot->UpdateAllowedPositionZ(testX, testY, testZ); - - if (!bot->IsWithinLOS(testX, testY, testZ) || - fabs(testZ - bot->GetPositionZ()) >= 5.0f) - { - continue; - } + float bestSpreadAngle = preferredAngle; - // Check if position is safe from defiles - bool isSafeFromDefiles = true; + // Try positions starting from preferred angle, expanding outward + for (int offset = 0; offset <= MAX_ANGLE_OFFSETS && !foundSafeSpot; offset++) + { + for (int direction = -1; direction <= 1; direction += 2) + { + if (offset == 0 && direction > 0) // Skip duplicate check of preferred angle + continue; + + float testAngle = preferredAngle + (direction * offset * M_PI / 16); + float testX = botX + SPREAD_DISTANCE * cos(testAngle); + float testY = botY + SPREAD_DISTANCE * sin(testAngle); + float testZ = FIXED_Z; + + bot->UpdateAllowedPositionZ(testX, testY, testZ); + + // Validate position basics (LOS and height) + if (!bot->IsWithinLOS(testX, testY, testZ) || fabs(testZ - bot->GetPositionZ()) >= MAX_HEIGHT_DIFF) + continue; + + // Check boss distance + if (boss->GetDistance2d(testX, testY) > MAX_BOSS_DISTANCE) + continue; + + // Check safety from all defiles (only if defiles exist) + bool safeFromDefiles = true; for (Unit* defile : defiles) { if (!defile || !defile->IsAlive()) continue; - float currentRadius = 6.0f; - Aura* growAura = defile->GetAura(72756); - if (!growAura) + // Calculate current defile radius including growth + float currentRadius = BASE_RADIUS; + Aura* growAura = nullptr; + + // Find growth aura + for (size_t i = 0; i < DEFILE_AURA_COUNT; i++) { - growAura = defile->GetAura(74162); - if (!growAura) - growAura = defile->GetAura(74163); - if (!growAura) - growAura = defile->GetAura(74164); + growAura = defile->GetAura(DEFILE_AURAS[i]); + if (growAura) + break; } if (growAura) { uint8 stacks = growAura->GetStackAmount(); - if (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_10MAN_NORMAL) - currentRadius = 6.0f + (stacks * 1.4f); - else - currentRadius = 6.0f + (stacks * 0.95f); + float growthMultiplier = + (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_10MAN_NORMAL) ? 1.4f + : 0.95f; + currentRadius = BASE_RADIUS + (stacks * growthMultiplier); } float dx = testX - defile->GetPositionX(); float dy = testY - defile->GetPositionY(); float distToDefile = sqrt(dx * dx + dy * dy); - if (distToDefile < (currentRadius + 3.0f)) + if (distToDefile < (currentRadius + SAFETY_MARGIN)) { - isSafeFromDefiles = false; + safeFromDefiles = false; break; } } - if (!isSafeFromDefiles) + if (!safeFromDefiles) continue; - // Check distance to other players + // Check spacing from other players bool tooCloseToPlayers = false; - for (Map::PlayerList::const_iterator itr = players.begin(); itr != players.end(); ++itr) + for (Map::PlayerList::const_iterator playerItr = players.begin(); playerItr != players.end(); + ++playerItr) { - Player* player = itr->GetSource(); + Player* player = playerItr->GetSource(); if (!player || !player->IsAlive() || player == bot) continue; - + float dx = testX - player->GetPositionX(); float dy = testY - player->GetPositionY(); float dist = sqrt(dx * dx + dy * dy); - - if (dist < 5.0f) // Minimum spacing between players + + if (dist < MIN_PLAYER_SPACING) { tooCloseToPlayers = true; break; } } - + if (tooCloseToPlayers) continue; - float distanceToBoss = boss->GetDistance2d(testX, testY); - if (distanceToBoss > 40.0f) - continue; - - // We found a safe spot - bestAngle = testAngle; + // Found valid position + bestSpreadAngle = testAngle; foundSafeSpot = true; break; } - - if (foundSafeSpot) - break; } - + + // Execute spread movement if safe spot found if (foundSafeSpot) { - float testX = bot->GetPositionX() + moveDistance * cos(bestAngle); - float testY = bot->GetPositionY() + moveDistance * sin(bestAngle); - float testZ = 840.857f; - - bot->UpdateAllowedPositionZ(testX, testY, testZ); - return MoveTo(bot->GetMapId(), testX, testY, testZ, - false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + float spreadX = botX + SPREAD_DISTANCE * cos(bestSpreadAngle); + float spreadY = botY + SPREAD_DISTANCE * sin(bestSpreadAngle); + float spreadZ = FIXED_Z; + + bot->UpdateAllowedPositionZ(spreadX, spreadY, spreadZ); + MoveTo(bot->GetMapId(), spreadX, spreadY, spreadZ, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); } } +} - // Check for Val'kyr Shadowguards grabbing units +void IccLichKingAddsAction::HandleValkyrMechanics(Difficulty diff) +{ GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - Unit* highestPriorityValkyr = nullptr; - float closestValkyrDistance = std::numeric_limits::max(); + std::vector grabbingValkyrs; - // First pass: Find all Val'kyrs and identify the highest priority target + // Find grabbing Val'kyrs for (auto& npc : npcs) { Unit* unit = botAI->GetUnit(npc); if (!unit || !unit->IsAlive()) continue; - // Check if this is a Val'kyr Shadowguard - if (unit->GetEntry() == 36609 || unit->GetEntry() == 39120 || unit->GetEntry() == 39121 || - unit->GetEntry() == 39122) + if (unit->GetEntry() == NPC_VALKYR_SHADOWGUARD1 || unit->GetEntry() == NPC_VALKYR_SHADOWGUARD2 || + unit->GetEntry() == NPC_VALKYR_SHADOWGUARD3 || unit->GetEntry() == NPC_VALKYR_SHADOWGUARD4) { - float currentDistance = bot->GetDistance(unit); + bool isGrabbing = false; - // Check if this Val'kyr is grabbing someone (aura 68985) - bool isGrabbing = unit->HasAura(68985); - - // Prioritize Val'kyrs that are currently grabbing someone, then the closest one - if (!highestPriorityValkyr || - (isGrabbing && (!highestPriorityValkyr->HasAura(68985) || currentDistance < closestValkyrDistance)) || - (!highestPriorityValkyr->HasAura(68985) && currentDistance < closestValkyrDistance)) + if (diff && !(diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) { - highestPriorityValkyr = unit; - closestValkyrDistance = currentDistance; + if (unit->HasAura(SPELL_HARVEST_SOUL_VALKYR)) + isGrabbing = true; + } + else + { + if (unit->HasAura(SPELL_HARVEST_SOUL_VALKYR) && unit->HealthAbovePct(49)) + isGrabbing = true; + } + + if (isGrabbing) + grabbingValkyrs.push_back(unit); + } + } + + Group* group = bot->GetGroup(); + if (!group) + return; + + // If no grabbing Val'kyrs, mark the Lich King with skull + if (grabbingValkyrs.empty()) + { + // Find the Lich King + for (auto& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_THE_LICH_KING && unit->HealthBelowPct(65) && unit->HealthAbovePct(40)) + { + ObjectGuid currentSkull = group->GetTargetIcon(7); // Skull icon + if (currentSkull != unit->GetGUID()) + { + group->SetTargetIcon(7, bot->GetGUID(), unit->GetGUID()); + } + break; + } + } + return; + } + + if (botAI->IsMainTank(bot)) + return; + + // Filter out dead Val'kyrs to ensure accurate group calculation + std::vector aliveGrabbingValkyrs; + for (Unit* valkyr : grabbingValkyrs) + { + if (valkyr && valkyr->IsAlive()) + aliveGrabbingValkyrs.push_back(valkyr); + } + + if (aliveGrabbingValkyrs.empty()) + return; + + HandleValkyrMarking(aliveGrabbingValkyrs, diff); + HandleValkyrAssignment(aliveGrabbingValkyrs); +} + +void IccLichKingAddsAction::HandleValkyrMarking(const std::vector& grabbingValkyrs, Difficulty diff) +{ + Group* group = bot->GetGroup(); + if (!group) + return; + + // Sort Val'kyrs by their GUID to ensure consistent ordering + std::vector sortedValkyrs = grabbingValkyrs; + std::sort(sortedValkyrs.begin(), sortedValkyrs.end(), [](Unit* a, Unit* b) { return a->GetGUID() < b->GetGUID(); }); + + static constexpr uint8_t ICON_INDICES[] = {7, 6, 0}; // Skull, Cross, Star + static constexpr const char* ICON_NAMES[] = {"skull", "cross", "star"}; + + // In heroic mode, clean up invalid markers for all possible icons + if (diff && (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) + { + for (size_t i = 0; i < 3; ++i) + { + ObjectGuid currentIcon = group->GetTargetIcon(ICON_INDICES[i]); + Unit* currentIconUnit = botAI->GetUnit(currentIcon); + + if (currentIconUnit && IsValkyr(currentIconUnit)) + { + bool shouldRemoveMarker = !currentIconUnit->HasAura(SPELL_HARVEST_SOUL_VALKYR) || + std::abs(currentIconUnit->GetPositionZ() - bot->GetPositionZ()) > 5.0f; + + if (shouldRemoveMarker) + group->SetTargetIcon(ICON_INDICES[i], bot->GetGUID(), ObjectGuid::Empty); } } } - // Second pass: Handle the highest priority Val'kyr - if (highestPriorityValkyr && !botAI->IsMainTank(bot) && !botAI->IsHeal(bot)) + // Clear unused markers if we have fewer Val'kyrs than icons + for (size_t i = sortedValkyrs.size(); i < 3; ++i) { - // Try to CC the Val'kyr based on class priority - bool ccAttempted = false; - - if (bot->getClass() == CLASS_MAGE && !botAI->HasAura("Frost Nova", highestPriorityValkyr)) + ObjectGuid currentIcon = group->GetTargetIcon(ICON_INDICES[i]); + if (!currentIcon.IsEmpty()) { - ccAttempted = botAI->CastSpell("Frost Nova", highestPriorityValkyr); + group->SetTargetIcon(ICON_INDICES[i], bot->GetGUID(), ObjectGuid::Empty); } - else if (bot->getClass() == CLASS_DRUID && !botAI->HasAura("Entangling Roots", highestPriorityValkyr)) - { - ccAttempted = botAI->CastSpell("Entangling Roots", highestPriorityValkyr); - } - else if (bot->getClass() == CLASS_PALADIN && !botAI->HasAura("Hammer of Justice", highestPriorityValkyr)) - { - ccAttempted = botAI->CastSpell("Hammer of Justice", highestPriorityValkyr); - } - else if (bot->getClass() == CLASS_WARRIOR && !botAI->HasAura("Hamstring", highestPriorityValkyr)) - { - ccAttempted = botAI->CastSpell("Hamstring", highestPriorityValkyr); - } - else if (bot->getClass() == CLASS_HUNTER && !botAI->HasAura("Concussive Shot", highestPriorityValkyr)) - { - ccAttempted = botAI->CastSpell("Concussive Shot", highestPriorityValkyr); - } - else if (bot->getClass() == CLASS_ROGUE && !botAI->HasAura("Kidney Shot", highestPriorityValkyr)) - { - ccAttempted = botAI->CastSpell("Kidney Shot", highestPriorityValkyr); - } - else if (bot->getClass() == CLASS_SHAMAN && !botAI->HasAura("Frost Shock", highestPriorityValkyr)) - { - ccAttempted = botAI->CastSpell("Frost Shock", highestPriorityValkyr); - } - else if (bot->getClass() == CLASS_DEATH_KNIGHT && !botAI->HasAura("Chains of Ice", highestPriorityValkyr)) - { - ccAttempted = botAI->CastSpell("Chains of Ice", highestPriorityValkyr); - } - - // If CC was attempted (success or fail) or if no CC available, attack the Val'kyr - if (ccAttempted) - { - bot->SetFacingToObject(highestPriorityValkyr); - return Attack(highestPriorityValkyr); - } - return false; - } - const float radiusvile = 12.0f; + // Mark each alive Val'kyr with appropriate icon + for (size_t i = 0; i < sortedValkyrs.size() && i < 3; ++i) + { + ObjectGuid currentIcon = group->GetTargetIcon(ICON_INDICES[i]); + Unit* currentIconUnit = botAI->GetUnit(currentIcon); + + if (!currentIconUnit || currentIconUnit != sortedValkyrs[i]) + { + group->SetTargetIcon(ICON_INDICES[i], bot->GetGUID(), sortedValkyrs[i]->GetGUID()); + } + } +} + +void IccLichKingAddsAction::HandleValkyrAssignment(const std::vector& grabbingValkyrs) +{ + Group* group = bot->GetGroup(); + if (!group) + return; + + // Double-check that all Val'kyrs in the list are actually alive and valid targets + std::vector validValkyrs; + for (Unit* valkyr : grabbingValkyrs) + { + if (valkyr && valkyr->IsAlive() && valkyr->HasAura(SPELL_HARVEST_SOUL_VALKYR)) + { + validValkyrs.push_back(valkyr); + } + } + + if (validValkyrs.empty()) + return; + + // Sort valid Val'kyrs for consistent assignment + std::sort(validValkyrs.begin(), validValkyrs.end(), [](Unit* a, Unit* b) { return a->GetGUID() < b->GetGUID(); }); + + // Get all non-main-tank members (DPS, healers, and off-tanks) + std::vector assistMembers; + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && !botAI->IsMainTank(member)) + assistMembers.push_back(member); + } + + if (assistMembers.empty()) + return; + + // Sort assist members by GUID for consistent assignment + std::sort(assistMembers.begin(), assistMembers.end(), + [](Player* a, Player* b) { return a->GetGUID() < b->GetGUID(); }); + + // Find our position among assist members + auto it = std::find(assistMembers.begin(), assistMembers.end(), bot); + if (it == assistMembers.end()) + return; // We're main tank, shouldn't handle Val'kyrs + + size_t myAssistIndex = std::distance(assistMembers.begin(), it); + size_t totalAssist = assistMembers.size(); + size_t aliveValkyrs = validValkyrs.size(); + + // Calculate balanced group sizes + std::vector groupSizes = CalculateBalancedGroupSizes(totalAssist, aliveValkyrs); + + // Determine which Val'kyr this bot should target + size_t assignedValkyrIndex = GetAssignedValkyrIndex(myAssistIndex, groupSizes); + + if (assignedValkyrIndex < validValkyrs.size()) + { + Unit* myValkyr = validValkyrs[assignedValkyrIndex]; + + // Set RTI context based on assignment + std::string rtiValue = GetRTIValueForValkyr(assignedValkyrIndex); + context->GetValue("rti")->Set(rtiValue); + + // Attack and apply CC + bot->SetTarget(myValkyr->GetGUID()); + bot->SetFacingToObject(myValkyr); + Difficulty diff = bot->GetRaidDifficulty(); + + if (sPlayerbotAIConfig->EnableICCBuffs && diff && (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) + { + //---------CHEAT--------- + if (!myValkyr->HasAura(SPELL_HAMMER_OF_JUSTICE)) + bot->AddAura(SPELL_HAMMER_OF_JUSTICE, myValkyr); + //---------CHEAT--------- + } + ApplyCCToValkyr(myValkyr); + } +} + +std::vector IccLichKingAddsAction::CalculateBalancedGroupSizes(size_t totalAssist, size_t numValkyrs) +{ + std::vector groupSizes(numValkyrs, 0); + + if (numValkyrs == 0) + return groupSizes; + + // Base size for each group + size_t baseSize = totalAssist / numValkyrs; + size_t remainder = totalAssist % numValkyrs; + + // Distribute assist members as evenly as possible + for (size_t i = 0; i < numValkyrs; ++i) + { + groupSizes[i] = baseSize; + if (i < remainder) + groupSizes[i]++; // Add extra member to first 'remainder' groups + } + + return groupSizes; +} + +size_t IccLichKingAddsAction::GetAssignedValkyrIndex(size_t assistIndex, const std::vector& groupSizes) +{ + size_t currentIndex = 0; + + for (size_t valkyrIndex = 0; valkyrIndex < groupSizes.size(); ++valkyrIndex) + { + if (assistIndex < currentIndex + groupSizes[valkyrIndex]) + return valkyrIndex; + + currentIndex += groupSizes[valkyrIndex]; + } + + // Fallback - should not happen with correct logic + return 0; +} + +std::string IccLichKingAddsAction::GetRTIValueForValkyr(size_t valkyrIndex) +{ + switch (valkyrIndex) + { + case 0: + return "skull"; + case 1: + return "cross"; + case 2: + return "star"; + default: + return "skull"; // Fallback + } +} + +void IccLichKingAddsAction::ApplyCCToValkyr(Unit* valkyr) +{ + switch (bot->getClass()) + { + case CLASS_MAGE: + if (!botAI->HasAura("Frost Nova", valkyr)) + botAI->CastSpell("Frost Nova", valkyr); + break; + case CLASS_DRUID: + if (!botAI->HasAura("Entangling Roots", valkyr)) + botAI->CastSpell("Entangling Roots", valkyr); + break; + case CLASS_PALADIN: + if (!botAI->HasAura("Hammer of Justice", valkyr)) + botAI->CastSpell("Hammer of Justice", valkyr); + break; + case CLASS_WARRIOR: + if (!botAI->HasAura("Hamstring", valkyr)) + botAI->CastSpell("Hamstring", valkyr); + break; + case CLASS_HUNTER: + if (!botAI->HasAura("Concussive Shot", valkyr)) + botAI->CastSpell("Concussive Shot", valkyr); + break; + case CLASS_ROGUE: + if (!botAI->HasAura("Kidney Shot", valkyr)) + botAI->CastSpell("Kidney Shot", valkyr); + break; + case CLASS_SHAMAN: + if (!botAI->HasAura("Frost Shock", valkyr)) + botAI->CastSpell("Frost Shock", valkyr); + break; + case CLASS_DEATH_KNIGHT: + if (!botAI->HasAura("Chains of Ice", valkyr)) + botAI->CastSpell("Chains of Ice", valkyr); + break; + case CLASS_PRIEST: + if (!botAI->HasAura("Psychic Scream", valkyr)) + botAI->CastSpell("Psychic Scream", valkyr); + break; + case CLASS_WARLOCK: + if (!botAI->HasAura("Fear", valkyr)) + botAI->CastSpell("Fear", valkyr); + break; + default: + break; + } +} + +bool IccLichKingAddsAction::IsValkyr(Unit* unit) +{ + return unit->GetEntry() == NPC_VALKYR_SHADOWGUARD1 || unit->GetEntry() == NPC_VALKYR_SHADOWGUARD2 || + unit->GetEntry() == NPC_VALKYR_SHADOWGUARD3 || unit->GetEntry() == NPC_VALKYR_SHADOWGUARD4; +} + +void IccLichKingAddsAction::HandleVileSpiritMechanics() +{ + const float radiusVile = 12.0f; - // Get the nearest hostile NPCs GuidVector npcs3 = AI_VALUE(GuidVector, "nearest hostile npcs"); for (auto& npc : npcs3) { Unit* unit = botAI->GetUnit(npc); - if (!unit || unit->GetEntry() != 37799 || unit->GetEntry() != 39284 || unit->GetEntry() != 39285 || - unit->GetEntry() != 39286) // vile spirit ID + if (!unit || (unit->GetEntry() != NPC_VILE_SPIRIT1 && unit->GetEntry() != NPC_VILE_SPIRIT2 && unit->GetEntry() != NPC_VILE_SPIRIT3 && + unit->GetEntry() != NPC_VILE_SPIRIT4)) continue; // Only run away if the spirit is targeting us - // Check by GUID comparison to ensure we're accurately identifying the specific spirit in 25HC multiple spirits spawn if (unit->GetVictim() && unit->GetVictim()->GetGUID() == bot->GetGUID()) { float currentDistance = bot->GetDistance2d(unit); - // Move away from the spirit if the bot is too close - if (currentDistance < radiusvile) + if (currentDistance < radiusVile) { - botAI->Reset(); // forces bot to stop channeling or getting locked by any other action - return MoveAway(unit, radiusvile - currentDistance); + botAI->Reset(); + MoveAway(unit, radiusVile - currentDistance); } } } - return false; } diff --git a/src/strategy/raids/icecrown/RaidIccActions.h b/src/strategy/raids/icecrown/RaidIccActions.h index 4922c1f0..d4a8791f 100644 --- a/src/strategy/raids/icecrown/RaidIccActions.h +++ b/src/strategy/raids/icecrown/RaidIccActions.h @@ -12,12 +12,17 @@ #include "RaidIccStrategy.h" #include "ScriptedCreature.h" #include "SharedDefines.h" - +#include "Trigger.h" +#include "CellImpl.h" +#include "GridNotifiers.h" +#include "GridNotifiersImpl.h" +#include "Vehicle.h" +#include "RaidIccTriggers.h" const Position ICC_LM_TANK_POSITION = Position(-391.0f, 2259.0f, 42.0f); const Position ICC_DARK_RECKONING_SAFE_POSITION = Position(-523.33386f, 2211.2031f, 62.823116f); const Position ICC_LDW_TANK_POSTION = Position(-570.1f, 2211.2456f, 49.476616f); //-590.0f -const Position ICC_ROTTING_FROST_GIANT_TANK_POSITION = Position(-265.90125f, 2209.0605f, 199.97006f); +const Position ICC_ROTTING_FROST_GIANT_TANK_POSITION = Position(-328.5085f, 2225.5142f, 199.97298f); const Position ICC_GUNSHIP_TELEPORT_ALLY = Position (-370.04645f, 1993.3536f, 466.65656f); const Position ICC_GUNSHIP_TELEPORT_ALLY2 = Position (-392.66208f, 2064.893f, 466.5672f, 5.058196f); const Position ICC_GUNSHIP_TELEPORT_HORDE = Position (-449.5343f, 2477.2024f, 470.17648f); @@ -29,33 +34,49 @@ const Position ICC_FESTERGUT_MELEE_SPORE = Position(4269.1772f, 3144.7673f, 360. const Position ICC_ROTFACE_TANK_POSITION = Position(4447.061f, 3150.9758f, 360.38568f); const Position ICC_ROTFACE_BIG_OOZE_POSITION = Position(4432.687f, 3142.3035f, 360.38623f); const Position ICC_ROTFACE_SAFE_POSITION = Position(4446.557f, 3065.6594f, 360.51843f); -const Position ICC_PUTRICIDE_TANK_POSITION = Position(4393.7676f, 3214.9375f, 389.3997f); -//const Position ICC_PUTRICIDE_GAS1_POSITION = Position(4350.772f, 3249.9773f, 389.39508f); -//const Position ICC_PUTRICIDE_GAS2_POSITION = Position(4390.002f, 3204.8855f, 389.39938f); +const Position ICC_ROTFACE_CENTER_POSITION = Position(4446.0547f, 3144.8677f, 360.38593f); //actual center 4.74089 4445.6616f, 3137.1526f, 360.38608 +const Position ICC_PUTRICIDE_TANK_POSITION = Position(4373.227f, 3222.058f, 389.4029f); +const Position ICC_PUTRICIDE_GREEN_POSITION = Position(4423.4126f, 3194.2715f, 389.37683f); +const Position ICC_PUTRICIDE_BAD_POSITION = Position(4356.1724f, 3261.5232f, 389.3985f); //const Position ICC_PUTRICIDE_GAS3_POSITION = Position(4367.753f, 3177.5894f, 389.39575f); //const Position ICC_PUTRICIDE_GAS4_POSITION = Position(4321.8486f, 3206.464f, 389.3982f); const Position ICC_BPC_OT_POSITION = Position(4649.2236f, 2796.0972f, 361.1815f); const Position ICC_BPC_MT_POSITION = Position(4648.5674f, 2744.847f, 361.18222f); const Position ICC_BQL_CENTER_POSITION = Position(4595.0f, 2769.0f, 400.0f); -const Position ICC_BQL_TANK_POSITION = Position(4616.102f, 2768.9167f, 400.13797f); -const Position ICC_VDW_GROUP_POSITION = Position(4204.839f, 2484.9338f, 364.87f); +const Position ICC_BQL_LWALL1_POSITION = Position(4624.685f, 2789.4895f, 400.13834f); +const Position ICC_BQL_LWALL2_POSITION = Position(4600.749f, 2805.7568f, 400.1374f); +const Position ICC_BQL_LWALL3_POSITION = Position(4572.857f, 2797.3872f, 400.1374f); +const Position ICC_BQL_RWALL1_POSITION = Position(4625.724f, 2748.9917f, 400.13693f); +const Position ICC_BQL_RWALL2_POSITION = Position(4608.3774f, 2735.7466f, 400.13693f); +const Position ICC_BQL_RWALL3_POSITION = Position(4576.813f, 2739.6067f, 400.13693f); +const Position ICC_BQL_LRWALL4_POSITION = Position(4539.345f, 2769.3853f, 403.7267f); +const Position ICC_BQL_TANK_POSITION = Position(4629.746f, 2769.6396f, 401.7479f); //old just in front of stairs 4616.102f, 2768.9167f, 400.13797f +const Position ICC_VDW_HEAL_POSITION = Position(4203.752f, 2483.4343f, 364.87274f); +const Position ICC_VDW_GROUP1_POSITION = Position(4203.585f, 2464.422f, 364.86887f); +const Position ICC_VDW_GROUP2_POSITION = Position(4203.5806f, 2505.2383f, 364.87677f); +const Position ICC_VDW_PORTALSTART_POSITION = Position(4202.637f, 2488.171f, 375.00256f); const Position ICC_SINDRAGOSA_TANK_POSITION = Position(4408.016f, 2508.0647f, 203.37955f); -const Position ICC_SINDRAGOSA_RANGED_POSITION = Position(4373.7686f, 2498.0042f, 203.38176f); -const Position ICC_SINDRAGOSA_MELEE_POSITION = Position(4389.22f, 2499.5237f, 203.38033f); -const Position ICC_SINDRAGOSA_BLISTERING_COLD_POSITION = Position(4345.922f, 2484.708f, 206.22516f); -const Position ICC_SINDRAGOSA_THOMB1_POSITION = Position(4381.819f, 2471.1448f, 203.37704f); // Westmost position -const Position ICC_SINDRAGOSA_THOMB2_POSITION = Position(4381.819f, 2483.1448f, 203.37704f); // 12y east from pos1 -const Position ICC_SINDRAGOSA_THOMB3_POSITION = Position(4381.819f, 2471.1448f, 203.37704f); // Same as pos1 -const Position ICC_SINDRAGOSA_THOMB4_POSITION = Position(4381.819f, 2483.1448f, 203.37704f); // Same as pos2 -const Position ICC_SINDRAGOSA_THOMB5_POSITION = Position(4381.819f, 2495.1448f, 203.37704f); // 12y east from pos2/4 +const Position ICC_SINDRAGOSA_FLYING_POSITION = Position(4525.6f, 2485.15f, 245.082f); +const Position ICC_SINDRAGOSA_RANGED_POSITION = Position(4441.572f, 2484.482f, 203.37836f); +const Position ICC_SINDRAGOSA_MELEE_POSITION = Position(4423.4546f, 2491.7175f, 203.37686f); +const Position ICC_SINDRAGOSA_BLISTERING_COLD_POSITION = Position(4473.6616f, 2484.8489f, 203.38258f); +const Position ICC_SINDRAGOSA_THOMB1_POSITION = Position(4433.6484f, 2469.4133f, 203.3806f); +const Position ICC_SINDRAGOSA_THOMB2_POSITION = Position(4434.143f, 2486.201f, 203.37473f); +const Position ICC_SINDRAGOSA_THOMB3_POSITION = Position(4436.1147f, 2501.464f, 203.38266f); +const Position ICC_SINDRAGOSA_UNCHAINEDMAGIC1_POSITION = Position(4444.9707f, 2455.7322f, 203.38701f); +const Position ICC_SINDRAGOSA_UNCHAINEDMAGIC2_POSITION = Position(4461.3945f, 2463.5513f, 203.38727f); +const Position ICC_SINDRAGOSA_UNCHAINEDMAGIC3_POSITION = Position(4473.6616f, 2484.8489f, 203.38258f); +const Position ICC_SINDRAGOSA_UNCHAINEDMAGIC4_POSITION = Position(4459.9336f, 2507.409f, 203.38606f); +const Position ICC_SINDRAGOSA_UNCHAINEDMAGIC5_POSITION = Position(4442.3096f, 2512.4688f, 203.38647f); const Position ICC_SINDRAGOSA_CENTER_POSITION = Position(4408.0464f, 2484.478f, 203.37529f); -const Position ICC_SINDRAGOSA_THOMBMB2_POSITION = Position(4382.6113f, 2505.4922f, 203.38197f); -const Position ICC_SINDRAGOSA_FBOMB_POSITION = Position(4400.031f, 2507.0295f, 203.37929f); //old 4400.031f, 2507.0295f, 203.37929f //alternate for 10 man 4366.0225f, 2501.569f, 203.38226f -const Position ICC_SINDRAGOSA_FBOMB10_POSITION = Position(4366.0225f, 2501.569f, 203.38226f); -const Position ICC_SINDRAGOSA_LOS2_POSITION = Position(4376.0938f, 2511.103f, 203.38303f); +const Position ICC_SINDRAGOSA_THOMBMB2_POSITION = Position(4436.895f, 2498.1401f, 203.38133f); +const Position ICC_SINDRAGOSA_FBOMB_POSITION = Position(4449.3647f, 2486.4524f, 203.379f); +const Position ICC_SINDRAGOSA_FBOMB10_POSITION = Position(4449.3647f, 2486.4524f, 203.379f); +const Position ICC_SINDRAGOSA_LOS2_POSITION = Position(4441.8286f, 2501.946f, 203.38435f); const Position ICC_LICH_KING_ADDS_POSITION = Position(476.7332f, -2095.3894f, 840.857f); // old 486.63647f, -2095.7915f, 840.857f const Position ICC_LICH_KING_MELEE_POSITION = Position(503.5546f, -2106.8213f, 840.857f); const Position ICC_LICH_KING_RANGED_POSITION = Position(501.3563f, -2085.1816f, 840.857f); +const Position ICC_LICH_KING_ASSISTHC_POSITION = Position(517.2145f, -2125.0674f, 840.857f); const Position ICC_LK_FROST1_POSITION = Position(503.96548f, -2183.216f, 840.857f); const Position ICC_LK_FROST2_POSITION = Position(563.07166f, -2125.7578f, 840.857f); const Position ICC_LK_FROST3_POSITION = Position(503.40182f, -2067.3435f, 840.857f); @@ -70,6 +91,8 @@ public: IccLmTankPositionAction(PlayerbotAI* botAI, std::string const name = "icc lm tank position") : AttackAction(botAI, name) {} bool Execute(Event event) override; + + bool MoveTowardPosition(const Position& position, float incrementSize); }; class IccSpikeAction : public AttackAction @@ -77,6 +100,10 @@ class IccSpikeAction : public AttackAction public: IccSpikeAction(PlayerbotAI* botAI) : AttackAction(botAI, "icc spike") {} bool Execute(Event event) override; + + bool HandleSpikeTargeting(Unit* boss); + bool MoveTowardPosition(const Position& position, float incrementSize); + void UpdateRaidTargetIcon(Unit* target); }; //Lady Deathwhisper @@ -94,6 +121,8 @@ public: IccRangedPositionLadyDeathwhisperAction(PlayerbotAI* botAI, std::string const name = "icc ranged position lady deathwhisper") : AttackAction(botAI, name) {} bool Execute(Event event) override; + + bool MaintainRangedSpacing(); }; class IccAddsLadyDeathwhisperAction : public AttackAction @@ -102,6 +131,12 @@ public: IccAddsLadyDeathwhisperAction(PlayerbotAI* botAI, std::string const name = "icc adds lady deathwhisper") : AttackAction(botAI, name) {} bool Execute(Event event) override; + + bool IsTargetedByShade(uint32 shadeEntry); + bool MoveTowardPosition(const Position& position, float incrementSize); + bool HandleAddTargeting(Unit* boss); + void UpdateRaidTargetIcon(Unit* target); + }; class IccShadeLadyDeathwhisperAction : public MovementAction @@ -127,6 +162,9 @@ public: IccCannonFireAction(PlayerbotAI* botAI, std::string const name = "icc cannon fire") : Action(botAI, name) {} bool Execute(Event event) override; + + Unit* FindValidCannonTarget(); + bool TryCastCannonSpell(uint32 spellId, Unit* target, Unit* vehicleBase); }; class IccGunshipEnterCannonAction : public MovementAction @@ -135,7 +173,10 @@ public: IccGunshipEnterCannonAction(PlayerbotAI* botAI, std::string const name = "icc gunship enter cannon") : MovementAction(botAI, name) {} bool Execute(Event event) override; + bool EnterVehicle(Unit* vehicleBase, bool moveIfFar); + Unit* FindBestAvailableCannon(); + bool IsValidCannon(Unit* vehicle, const uint32 validEntries[]); }; class IccGunshipTeleportAllyAction : public AttackAction @@ -144,6 +185,10 @@ public: IccGunshipTeleportAllyAction(PlayerbotAI* botAI, std::string const name = "icc gunship teleport ally") : AttackAction(botAI, name) {} bool Execute(Event event) override; + + bool TeleportTo(const Position& position); + void CleanupSkullIcon(uint8_t SKULL_ICON_INDEX); + void UpdateBossSkullIcon(Unit* boss, uint8_t SKULL_ICON_INDEX); }; class IccGunshipTeleportHordeAction : public AttackAction @@ -152,6 +197,10 @@ public: IccGunshipTeleportHordeAction(PlayerbotAI* botAI, std::string const name = "icc gunship teleport horde") : AttackAction(botAI, name) {} bool Execute(Event event) override; + + bool TeleportTo(const Position& position); + void CleanupSkullIcon(uint8_t SKULL_ICON_INDEX); + void UpdateBossSkullIcon(Unit* boss, uint8_t SKULL_ICON_INDEX); }; //DBS @@ -161,6 +210,10 @@ public: IccDbsTankPositionAction(PlayerbotAI* botAI, std::string const name = "icc dbs tank position") : AttackAction(botAI, name) {} bool Execute(Event event) override; + + bool CrowdControlBloodBeasts(); + bool EvadeBloodBeasts(); + bool PositionInRangedFormation(); }; class IccAddsDbsAction : public AttackAction @@ -169,15 +222,22 @@ public: IccAddsDbsAction(PlayerbotAI* botAI, std::string const name = "icc adds dbs") : AttackAction(botAI, name) {} bool Execute(Event event) override; + + Unit* FindPriorityTarget(Unit* boss); + void UpdateSkullMarker(Unit* priorityTarget); }; //FESTERGUT -class IccFestergutTankPositionAction : public AttackAction +class IccFestergutGroupPositionAction : public AttackAction { public: - IccFestergutTankPositionAction(PlayerbotAI* botAI, std::string const name = "icc festergut tank position") + IccFestergutGroupPositionAction(PlayerbotAI* botAI, std::string const name = "icc festergut group position") : AttackAction(botAI, name) {} bool Execute(Event event) override; + + bool HasSporesInGroup(); + bool PositionNonTankMembers(); + int CalculatePositionIndex(Group* group); }; class IccFestergutSporeAction : public AttackAction @@ -186,6 +246,17 @@ public: IccFestergutSporeAction(PlayerbotAI* botAI, std::string const name = "icc festergut spore") : AttackAction(botAI, name) {} bool Execute(Event event) override; + + Position CalculateSpreadPosition(); + struct SporeInfo + { + std::vector sporedPlayers; + ObjectGuid lowestGuid; + bool hasLowestGuid = false; + }; + SporeInfo FindSporedPlayers(); + Position DetermineTargetPosition(bool hasSpore, const SporeInfo& sporeInfo, const Position& spreadRangedPos); + bool CheckMainTankSpore(); }; //Rotface @@ -195,6 +266,11 @@ public: IccRotfaceTankPositionAction(PlayerbotAI* botAI, std::string const name = "icc rotface tank position") : AttackAction(botAI, name) {} bool Execute(Event event) override; + + void MarkBossWithSkull(Unit* boss); + bool PositionMainTankAndMelee(Unit *boss); + bool HandleAssistTankPositioning(Unit* boss); + bool HandleBigOozePositioning(Unit* boss); }; class IccRotfaceGroupPositionAction : public AttackAction @@ -203,25 +279,40 @@ public: IccRotfaceGroupPositionAction(PlayerbotAI* botAI, std::string const name = "icc rotface group position") : AttackAction(botAI, name) {} bool Execute(Event event) override; + + //bool MoveAwayFromBigOoze(Unit* bigOoze); + bool HandlePuddleAvoidance(Unit* boss); + bool MoveAwayFromPuddle(Unit* boss, Unit* puddle, float puddleDistance); + bool HandleOozeTargeting(); + bool HandleOozeMemberPositioning(); + bool PositionRangedAndHealers(Unit* boss,Unit* smallOoze); + bool FindAndMoveFromClosestMember(Unit* boss, Unit* smallOoze); }; class IccRotfaceMoveAwayFromExplosionAction : public MovementAction { - public: - IccRotfaceMoveAwayFromExplosionAction(PlayerbotAI* botAI, std::string const name = "icc rotface move away from explosion") - : MovementAction(botAI, name) {} - - bool Execute(Event event) override; +public: + IccRotfaceMoveAwayFromExplosionAction(PlayerbotAI* botAI, std::string const name = "icc rotface move away from explosion") + : MovementAction(botAI, name) {} + bool Execute(Event event) override; + + bool MoveToRandomSafeLocation(); + }; //PP - class IccPutricideGrowingOozePuddleAction : public AttackAction { public: IccPutricideGrowingOozePuddleAction(PlayerbotAI* botAI, std::string const name = "icc putricide growing ooze puddle") : AttackAction(botAI, name) {} bool Execute(Event event) override; + + Unit* FindClosestThreateningPuddle(); + Position CalculateSafeMovePosition(Unit* closestPuddle); + bool IsPositionTooCloseToOtherPuddles(float x, float y, Unit* ignorePuddle); + + }; class IccPutricideVolatileOozeAction : public AttackAction @@ -230,22 +321,41 @@ public: IccPutricideVolatileOozeAction(PlayerbotAI* botAI, std::string const name = "icc putricide volatile ooze") : AttackAction(botAI, name) {} bool Execute(Event event) override; + + void MarkOozeWithSkull(Unit* ooze); + Unit* FindAuraTarget(); }; class IccPutricideGasCloudAction : public AttackAction { - public: +public: IccPutricideGasCloudAction(PlayerbotAI* botAI, std::string const name = "icc putricide gas cloud") : AttackAction(botAI, name) {} - bool Execute(Event event) override; + bool Execute(Event event) override; + + bool HandleGaseousBloatMovement(Unit* gasCloud); + Position CalculateEmergencyPosition(const Position& botPos, float dx, float dy); + bool FindSafeMovementPosition(const Position& botPos, const Position& cloudPos, float dx, float dy, int numAngles, + Position& resultPos); + bool HandleGroupAuraSituation(Unit* gasCloud); + bool GroupHasGaseousBloat(Group* group); }; -class AvoidMalleableGooAction : public MovementAction +class IccPutricideAvoidMalleableGooAction : public MovementAction { - public: - AvoidMalleableGooAction(PlayerbotAI* botAI, std::string const name = "avoid malleable goo" ) +public: + IccPutricideAvoidMalleableGooAction(PlayerbotAI* botAI, std::string const name = "icc putricide avoid malleable goo") : MovementAction(botAI, name) {} - bool Execute(Event event) override; + bool Execute(Event event) override; + + bool HandleTankPositioning(Unit* boss); + bool HandleUnboundPlague(Unit* boss); + bool HandleBossPositioning(Unit* boss); + Position CalculateBossPosition(Unit* boss, float distance); + bool HasObstacleBetween(const Position& from, const Position& to); + bool IsOnPath(const Position& from, const Position& to, const Position& point, float threshold); + Position CalculateArcPoint(const Position& current, const Position& target, const Position& center); + Position CalculateIncrementalMove(const Position& current, const Position& target, float maxDistance); }; //BPC @@ -257,20 +367,14 @@ public: bool Execute(Event event) override; }; -class IccBpcNucleusAction : public AttackAction -{ -public: - IccBpcNucleusAction(PlayerbotAI* botAI) - : AttackAction(botAI, "icc bpc nucleus") {} - bool Execute(Event event) override; -}; - class IccBpcMainTankAction : public AttackAction { public: IccBpcMainTankAction(PlayerbotAI* botAI) : AttackAction(botAI, "icc bpc main tank") {} bool Execute(Event event) override; + + void MarkEmpoweredPrince(); }; class IccBpcEmpoweredVortexAction : public MovementAction @@ -279,6 +383,9 @@ public: IccBpcEmpoweredVortexAction(PlayerbotAI* botAI) : MovementAction(botAI, "icc bpc empowered vortex") {} bool Execute(Event event) override; + + bool MaintainRangedSpacing(); + bool HandleEmpoweredVortexSpread(); }; class IccBpcKineticBombAction : public AttackAction @@ -287,15 +394,45 @@ public: IccBpcKineticBombAction(PlayerbotAI* botAI) : AttackAction(botAI, "icc bpc kinetic bomb") {} bool Execute(Event event) override; + + Unit* FindOptimalKineticBomb(); + bool IsBombAlreadyHandled(Unit* bomb, Group* group); +}; + +class IccBpcBallOfFlameAction : public MovementAction +{ +public: + IccBpcBallOfFlameAction(PlayerbotAI* botAI) + : MovementAction(botAI, "icc bpc ball of flame") {} + bool Execute(Event event) override; }; //Blood Queen Lana'thel -class IccBqlTankPositionAction : public AttackAction +class IccBqlGroupPositionAction : public AttackAction { public: - IccBqlTankPositionAction(PlayerbotAI* botAI) - : AttackAction(botAI, "icc bql tank position") {} + IccBqlGroupPositionAction(PlayerbotAI* botAI) + : AttackAction(botAI, "icc group tank position") {} bool Execute(Event event) override; + + bool HandleTankPosition(Unit* boss, Aura* frenzyAura, Aura* shadowAura); + bool HandleShadowsMovement(); + Position AdjustControlPoint(const Position& wall, const Position& center, float factor); + Position CalculateBezierPoint(float t, const Position path[4]); + bool HandleGroupPosition(Unit* boss, Aura* frenzyAura, Aura* shadowAura); + +private: + // Evaluate curves + struct CurveInfo + { + Position moveTarget; + int curveIdx = 0; + bool foundSafe = false; + float minDist = 0.0f; + float score = 0.0f; + Position closestPoint; + float t_closest = 0.0f; + }; }; class IccBqlPactOfDarkfallenAction : public MovementAction @@ -304,6 +441,9 @@ public: IccBqlPactOfDarkfallenAction(PlayerbotAI* botAI) : MovementAction(botAI, "icc bql pact of darkfallen") {} bool Execute(Event event) override; + + void CalculateCenterPosition(Position& targetPos, Player* otherPlayer); + bool MoveToTargetPosition(const Position& targetPos, int auraCount); }; class IccBqlVampiricBiteAction : public AttackAction @@ -312,9 +452,14 @@ public: IccBqlVampiricBiteAction(PlayerbotAI* botAI) : AttackAction(botAI, "icc bql vampiric bite") {} bool Execute(Event event) override; + + Player* FindBestBiteTarget(Group* group); + bool IsInvalidTarget(Player* player); + bool MoveTowardsTarget(Player* target); + bool CastVampiricBite(Player* target); }; -//VDW +// Sister Svalna class IccValkyreSpearAction : public AttackAction { public: @@ -331,6 +476,21 @@ public: bool Execute(Event event) override; }; +// Valithria Dreamwalker + +class IccValithriaGroupAction : public AttackAction +{ +public: + IccValithriaGroupAction(PlayerbotAI* botAI) + : AttackAction(botAI, "icc valithria group") {} + bool Execute(Event event) override; + + bool MoveTowardsPosition(const Position& pos, float increment); + bool Handle25ManGroupLogic(); + bool HandleMarkingLogic(bool inGroup1, bool inGroup2, const Position& group1Pos, const Position& group2Pos); + bool Handle10ManGroupLogic(); +}; + class IccValithriaPortalAction : public MovementAction { public: @@ -356,12 +516,16 @@ public: }; //Sindragosa -class IccSindragosaTankPositionAction : public AttackAction +class IccSindragosaGroupPositionAction : public AttackAction { public: - IccSindragosaTankPositionAction(PlayerbotAI* botAI) - : AttackAction(botAI, "icc sindragosa tank position") {} + IccSindragosaGroupPositionAction(PlayerbotAI* botAI) + : AttackAction(botAI, "icc sindragosa group position") {} bool Execute(Event event) override; + + bool HandleTankPositioning(Unit* boss); + bool HandleNonTankPositioning(); + bool MoveIncrementallyToPosition(const Position& targetPos, float maxStep); }; class IccSindragosaFrostBeaconAction : public MovementAction @@ -370,6 +534,21 @@ public: IccSindragosaFrostBeaconAction(PlayerbotAI* botAI) : MovementAction(botAI, "icc sindragosa frost beacon") {} bool Execute(Event event) override; + + void HandleSupportActions(); + bool HandleBeaconedPlayer(const Unit* boss); + bool HandleNonBeaconedPlayer(const Unit* boss); + bool MoveToPositionIfNeeded(const Position& position, float tolerance); + bool MoveToPosition(const Position& position); + bool IsBossFlying(const Unit* boss); + + private: + static constexpr uint32 FROST_BEACON_AURA_ID = SPELL_FROST_BEACON; + static constexpr uint32 HAND_OF_FREEDOM_SPELL_ID = 1044; + static constexpr float POSITION_TOLERANCE = 1.0f; + static constexpr float TOMB_POSITION_TOLERANCE = 0.5f; + static constexpr float MIN_SAFE_DISTANCE = 13.0f; + static constexpr float MOVE_TOLERANCE = 2.0f; }; class IccSindragosaBlisteringColdAction : public MovementAction @@ -396,11 +575,11 @@ public: bool Execute(Event event) override; }; -class IccSindragosaMysticBuffetAction : public AttackAction +class IccSindragosaMysticBuffetAction : public MovementAction { public: IccSindragosaMysticBuffetAction(PlayerbotAI* botAI) - : AttackAction(botAI, "icc sindragosa mystic buffet") {} + : MovementAction(botAI, "icc sindragosa mystic buffet") {} bool Execute(Event event) override; }; @@ -444,6 +623,21 @@ class IccLichKingWinterAction : public AttackAction IccLichKingWinterAction(PlayerbotAI* botAI) : AttackAction(botAI, "icc lich king winter") {} bool Execute(Event event) override; + + void HandlePositionCorrection(); + bool IsValidCollectibleAdd(Unit* unit); + bool IsPositionSafeFromDefile(float x, float y, float z, float minSafeDistance); + void HandleTankPositioning(); + void HandleMeleePositioning(); + void HandleRangedPositioning(); + void HandleMainTankAddManagement(Unit* boss, const Position* tankPos); + void HandleAssistTankAddManagement(Unit* boss, const Position* tankPos); + + + private: + const Position* GetMainTankPosition(); + const Position* GetMainTankRangedPosition(); + bool TryMoveToPosition(float targetX, float targetY, float targetZ, bool isForced = true); }; class IccLichKingAddsAction : public AttackAction @@ -452,6 +646,28 @@ class IccLichKingAddsAction : public AttackAction IccLichKingAddsAction(PlayerbotAI* botAI) : AttackAction(botAI, "icc lich king adds") {} bool Execute(Event event) override; + + void HandleTeleportationFixes(Difficulty diff, Unit* terenasMenethilHC); + bool HandleSpiritBombAvoidance(Difficulty diff, Unit* terenasMenethilHC); + void HandleHeroicNonTankPositioning(Difficulty diff, Unit* terenasMenethilHC); + void HandleSpiritMarkingAndTargeting(Difficulty diff, Unit* terenasMenethilHC); + bool HandleQuakeMechanics(Unit* boss); + void HandleShamblingHorrors(Unit* boss, bool hasPlague); + bool HandleAssistTankAddManagement(Unit* boss, Difficulty diff); + void HandleMeleePositioning(Unit* boss, bool hasPlague, Difficulty diff); + void HandleMainTankTargeting(Unit* boss, Difficulty diff); + void HandleNonTankHeroicPositioning(Unit* boss, Difficulty diff, bool hasPlague); + void HandleRangedPositioning(Unit* boss, bool hasPlague, Difficulty diff); + void HandleDefileMechanics(Unit* boss, Difficulty diff); + void HandleValkyrMechanics(Difficulty diff); + std::vector CalculateBalancedGroupSizes(size_t totalAssist, size_t numValkyrs); + size_t GetAssignedValkyrIndex(size_t assistIndex, const std::vector& groupSizes); + std::string GetRTIValueForValkyr(size_t valkyrIndex); + void HandleValkyrMarking(const std::vector& grabbingValkyrs, Difficulty diff); + void HandleValkyrAssignment(const std::vector& grabbingValkyrs); + void ApplyCCToValkyr(Unit* valkyr); + bool IsValkyr(Unit* unit); + void HandleVileSpiritMechanics(); }; diff --git a/src/strategy/raids/icecrown/RaidIccMultipliers.cpp b/src/strategy/raids/icecrown/RaidIccMultipliers.cpp index b89000ca..8b3b3edf 100644 --- a/src/strategy/raids/icecrown/RaidIccMultipliers.cpp +++ b/src/strategy/raids/icecrown/RaidIccMultipliers.cpp @@ -20,6 +20,7 @@ #include "UseMeetingStoneAction.h" #include "WarriorActions.h" #include "PlayerbotAI.h" +#include "RaidIccTriggers.h" // LK global variables namespace @@ -31,35 +32,59 @@ std::map g_allowCure; std::mutex g_plagueMutex; // Lock before accessing shared variables } - +// Lady Deathwhisper float IccLadyDeathwhisperMultiplier::GetValue(Action* action) { Unit* boss = AI_VALUE2(Unit*, "find target", "lady deathwhisper"); if (!boss) - { return 1.0f; - } - if (dynamic_cast(action) || dynamic_cast(action)) - { + if (dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action)) return 0.0f; + + static constexpr uint32 VENGEFUL_SHADE_ID = NPC_SHADE; + + // Get the nearest hostile NPCs + const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + // Allow the IccShadeLadyDeathwhisperAction to run + if (dynamic_cast(action)) + return 1.0f; + + for (const auto& npcGuid : npcs) + { + Unit* shade = botAI->GetUnit(npcGuid); + + if (!shade || shade->GetEntry() != VENGEFUL_SHADE_ID) + continue; + + if (!shade->GetVictim() || shade->GetVictim()->GetGUID() != bot->GetGUID()) + continue; + + return 0.0f; // Cancel all other actions when we need to handle Vengeful Shade } return 1.0f; } +// dbs float IccAddsDbsMultiplier::GetValue(Action* action) { Unit* boss = AI_VALUE2(Unit*, "find target", "deathbringer saurfang"); if (!boss) - { return 1.0f; - } - if (dynamic_cast(action) || dynamic_cast(action)) - { + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action)) return 0.0f; - } + + if (botAI->IsRanged(bot)) + if (dynamic_cast(action)) + return 0.0f; if (botAI->IsMainTank(bot)) { @@ -67,7 +92,8 @@ float IccAddsDbsMultiplier::GetValue(Action* action) if (aura) { if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action)) + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) { return 0.0f; } @@ -77,6 +103,7 @@ float IccAddsDbsMultiplier::GetValue(Action* action) return 1.0f; } +// dogs float IccDogsMultiplier::GetValue(Action* action) { bool bossPresent = false; @@ -92,7 +119,8 @@ float IccDogsMultiplier::GetValue(Action* action) if (aura && aura->GetStackAmount() >= 8) { if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action)) + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) { return 0.0f; } @@ -101,18 +129,18 @@ float IccDogsMultiplier::GetValue(Action* action) return 1.0f; } +// Festergut float IccFestergutMultiplier::GetValue(Action* action) { Unit* boss = AI_VALUE2(Unit*, "find target", "festergut"); if (!boss) - { return 1.0f; - } if (dynamic_cast(action) || dynamic_cast(action)) - { return 0.0f; - } + + if (dynamic_cast(action)) + return 0.0f; if (botAI->IsMainTank(bot)) { @@ -120,43 +148,46 @@ float IccFestergutMultiplier::GetValue(Action* action) if (aura && aura->GetStackAmount() >= 6) { if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action)) + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) { return 0.0f; } } } - if (bot->HasAura(69279) && (bot->getClass() == CLASS_HUNTER)) - { - if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action)) - { - return 0.0f; - } - } + if (dynamic_cast(action)) + return 1.0f; + + if (bot->HasAura(SPELL_GAS_SPORE)) + return 0.0f; + + return 1.0f; } +// Rotface float IccRotfaceMultiplier::GetValue(Action* action) { - // If we're already executing the escape movement, don't interrupt it - if (dynamic_cast(action)) + Unit* boss1 = AI_VALUE2(Unit*, "find target", "rotface"); + if (!boss1) return 1.0f; - Unit* boss = AI_VALUE2(Unit*, "find target", "big ooze"); - if (!boss) { return 1.0f; } - - if (dynamic_cast(action) || dynamic_cast(action)) - { + if (dynamic_cast(action)) return 0.0f; - } - if (dynamic_cast(action)) - { + if (dynamic_cast(action) && !(bot->getClass() == CLASS_HUNTER)) + return 0.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (botAI->IsAssistTank(bot) && (dynamic_cast(action) || dynamic_cast(action))) return 0.0f; - } + + Unit* boss = AI_VALUE2(Unit*, "find target", "big ooze"); + if (!boss) + return 1.0f; static std::map lastExplosionTimes; static std::map hasMoved; @@ -164,7 +195,7 @@ float IccRotfaceMultiplier::GetValue(Action* action) ObjectGuid botGuid = bot->GetGUID(); // When cast starts, record the time - if (boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(69839)) + if (boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_UNSTABLE_OOZE_EXPLOSION)) { if (lastExplosionTimes[botGuid] == 0) // Only set if not already set { @@ -174,7 +205,7 @@ float IccRotfaceMultiplier::GetValue(Action* action) } // If explosion cast is no longer active, reset the timers - if (!boss->HasUnitState(UNIT_STATE_CASTING) || !boss->FindCurrentSpellBySpellId(69839)) + if (!boss->HasUnitState(UNIT_STATE_CASTING) || !boss->FindCurrentSpellBySpellId(SPELL_UNSTABLE_OOZE_EXPLOSION)) { if (lastExplosionTimes[botGuid] > 0 && time(nullptr) - lastExplosionTimes[botGuid] >= 16) { @@ -199,31 +230,32 @@ float IccRotfaceMultiplier::GetValue(Action* action) if (hasMoved[botGuid] && time(nullptr) - lastExplosionTimes[botGuid] < 16 // 9 seconds wait + 7 seconds stay && dynamic_cast(action) && !dynamic_cast(action)) - { return 0.0f; - } return 1.0f; } -/*float IccRotfaceGroupPositionMultiplier::GetValue(Action* action) -{ - if (dynamic_cast(action)) - return 1.0f; - - if (dynamic_cast(action)) - return 0.0f; - - return 1.0f; -}*/ - +// pp float IccAddsPutricideMultiplier::GetValue(Action* action) { Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); if (!boss) - { return 1.0f; - } + + bool hasGaseousBloat = botAI->HasAura("Gaseous Bloat", bot); + bool hasUnboundPlague = botAI->HasAura("Unbound Plague", bot); + + if (!(bot->getClass() == CLASS_HUNTER) && dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action)) + return 0.0f; if (botAI->IsMainTank(bot)) { @@ -231,54 +263,62 @@ float IccAddsPutricideMultiplier::GetValue(Action* action) if (aura && aura->GetStackAmount() >= 4) { if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action)) + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) { return 0.0f; } } } - if (dynamic_cast(action) || dynamic_cast(action)) + if (hasGaseousBloat) { - if (dynamic_cast(action) || dynamic_cast(action)) + if (dynamic_cast(action)) + return 1.0f; + + if (dynamic_cast(action)) + return 1.0f; + + if (botAI->IsHeal(bot)) + return 1.0f; + else + return 0.0f; // Cancel all other actions when we need to handle Gaseous Bloat + } + + if (hasUnboundPlague && boss && !boss->HealthBelowPct(35)) + { + if (dynamic_cast(action)) + return 1.0f; + else + return 0.0f; // Cancel all other actions when we need to handle Unbound Plague + } + + if (dynamic_cast(action)) + { + if (dynamic_cast(action)) return 0.0f; - } - - if (botAI->IsDps(bot)) - { - GuidVector targets = AI_VALUE(GuidVector, "possible targets"); - bool hasAdds = false; - for (auto& guid : targets) - { - Unit* unit = botAI->GetUnit(guid); - if (unit && (unit->GetEntry() == 37697 || unit->GetEntry() == 38604 || unit->GetEntry() == 38758 || unit->GetEntry() == 38759 ||//volatile ooze - unit->GetEntry() == 37562 || unit->GetEntry() == 38602 || unit->GetEntry() == 38760 || unit->GetEntry() == 38761)) //gas cloud - { - hasAdds = true; - break; - } - } - - if (hasAdds) - { - if (dynamic_cast(action) || dynamic_cast(action)) - return 2.0f; - else if (dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action)) - return 0.0f; - } + if (dynamic_cast(action) && !botAI->IsMainTank(bot)) + return 0.0f; + //if (dynamic_cast(action) && !hasGaseousBloat) + //return 0.0f; } + return 1.0f; } -//bpc +// bpc float IccBpcAssistMultiplier::GetValue(Action* action) { - if (!action) + Unit* keleseth = AI_VALUE2(Unit*, "find target", "prince keleseth"); + if (!keleseth) return 1.0f; - Unit* keleseth = AI_VALUE2(Unit*, "find target", "prince keleseth"); - if (!keleseth || !keleseth->IsAlive()) - return 1.0f; + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; Aura* aura = botAI->GetAura("Shadow Prison", bot, false, true); if (aura) @@ -296,305 +336,375 @@ float IccBpcAssistMultiplier::GetValue(Action* action) } } - Unit* Valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); - if (!Valanar || !Valanar->IsAlive()) + Unit* valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); + if (!valanar) return 1.0f; - Aura* auraValanar = botAI->GetAura("Invocation of Blood", Valanar); - - if (!botAI->IsTank(bot) && auraValanar && Valanar->HasUnitState(UNIT_STATE_CASTING)) + if (valanar && valanar->HasUnitState(UNIT_STATE_CASTING) && + (valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX1) || + valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX2) || + valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX3) || + valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX4))) { - if (dynamic_cast(action)) + if (dynamic_cast(action) || dynamic_cast(action)) return 1.0f; - - if (dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; + else + return 0.0f; // Cancel all other actions when we need to handle Empowered Vortex } - if (botAI->IsRangedDps(bot)) - { - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (unit) - { - if (unit->GetName() == "Kinetic Bomb" && ((unit->GetPositionZ() - bot->GetPositionZ()) < 25.0f)) - { - if (dynamic_cast(action)) - return 1.0f; + Unit* flame1 = bot->FindNearestCreature(NPC_BALL_OF_FLAME, 100.0f); + Unit* flame2 = bot->FindNearestCreature(NPC_BALL_OF_INFERNO_FLAME, 100.0f); + bool ballOfFlame = flame1 && flame1->GetVictim() == bot; + bool infernoFlame = flame2 && flame2->GetVictim() == bot; - if (dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; + if (flame2) + { + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action)) + return 1.0f; + } + + if (ballOfFlame || infernoFlame) + { + // If bot is tank, do nothing special + if (dynamic_cast(action)) + return 1.0f; + else + return 0.0f; // Cancel all other actions when we need to handle Ball of Flame + } + + static const std::array bombEntries = {NPC_KINETIC_BOMB1, NPC_KINETIC_BOMB2, NPC_KINETIC_BOMB3, + NPC_KINETIC_BOMB4}; + const GuidVector bombs = AI_VALUE(GuidVector, "possible targets no los"); + + bool bombFound = false; + + for (const auto entry : bombEntries) + { + for (const auto& guid : bombs) + { + if (Unit* unit = botAI->GetUnit(guid)) + { + if (unit->GetEntry() == entry) + { + // Check if bomb is within valid Z-axis range + if (unit->GetPositionZ() - bot->GetPositionZ() < 25.0f) + { + bombFound = true; + break; + } } } } + if (bombFound) + break; + } + + if (bombFound && !(aura && aura->GetStackAmount() > 12) && !botAI->IsTank(bot)) + { + // If kinetic bomb action is active, disable these actions + if (dynamic_cast(action)) + return 1.0f; + + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; } // For assist tank during BPC fight - if (botAI->IsAssistTank(bot)) + if (botAI->IsAssistTank(bot) && !(aura && aura->GetStackAmount() > 18)) { // Allow BPC-specific actions - if (dynamic_cast(action) || - dynamic_cast(action)) + if (dynamic_cast(action)) return 1.0f; // Disable normal assist behavior - if (dynamic_cast(action)) + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) return 0.0f; - // Disable following - if (dynamic_cast(action)) - return 0.0f; } return 1.0f; } //BQL -float IccBqlPactOfDarkfallenMultiplier::GetValue(Action* action) -{ - if (!action) - return 1.0f; - - if (action->getName() == "icc bql pact of darkfallen") - return 1.0f; - - // If bot has Pact of Darkfallen aura, return 0 for all other actions - if (bot->HasAura(71340)) - return 0.0f; - - return 1.0f; -} - -float IccBqlVampiricBiteMultiplier::GetValue(Action* action) +float IccBqlMultiplier::GetValue(Action* action) { Unit* boss = AI_VALUE2(Unit*, "find target", "blood-queen lana'thel"); if (!boss) return 1.0f; + Aura* aura2 = botAI->GetAura("Swarming Shadows", bot); Aura* aura = botAI->GetAura("Frenzied Bloodthirst", bot); - if (botAI->IsMelee(bot) && ((boss->GetPositionZ() - bot->GetPositionZ()) > 5.0f) && !aura) - { - if (dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + if (botAI->IsRanged(bot)) + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; - // If bot has frenzied bloodthirst, allow highest priority for bite action - if (aura) // If bot has frenzied bloodthirst + // If bot has Pact of Darkfallen aura, return 0 for all other actions + if (bot->HasAura(SPELL_PACT_OF_THE_DARKFALLEN)) { - if (dynamic_cast(action)) - return 5.0f; // Highest priority for bite action - - if (dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; // Disable all formation/movement actions + if (dynamic_cast(action)) + return 1.0f; // Allow Pact of Darkfallen action + else + return 0.0f; // Cancel all other actions when we need to handle Pact of Darkfallen } + if (botAI->IsMelee(bot) && ((boss->GetPositionZ() - ICC_BQL_CENTER_POSITION.GetPositionZ()) > 5.0f) && !aura) + { + if (dynamic_cast(action)) + return 1.0f; + else + return 0.0f; + } + + // If bot has frenzied bloodthirst, allow highest priority for bite action + if (aura) // If bot has frenzied bloodthirst + { + if (dynamic_cast(action)) + return 1.0f; + else + return 0.0f; + } + + if (aura2 && !aura) + { + if (dynamic_cast(action)) + return 1.0f; + else + return 0.0f; // Cancel all other actions when we need to handle Swarming Shadows + } return 1.0f; + + if ((boss->GetExactDist2d(ICC_BQL_TANK_POSITION.GetPositionX(), ICC_BQL_TANK_POSITION.GetPositionY()) > 10.0f) && + botAI->IsRanged(bot) && !((boss->GetPositionZ() - bot->GetPositionZ()) > 5.0f)) + { + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + } + } //VDW float IccValithriaDreamCloudMultiplier::GetValue(Action* action) { - if (!bot->HasAura(70766)) + Unit* boss = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); + + Aura* twistedNightmares = botAI->GetAura("Twisted Nightmares", bot); + Aura* emeraldVigor = botAI->GetAura("Emerald Vigor", bot); + + + if (!boss && !bot->HasAura(SPELL_DREAM_STATE)) return 1.0f; - // If bot is in dream state, prioritize cloud collection over other actions - if (bot->HasAura(70766) && dynamic_cast(action)) - return 2.0f; - else if (dynamic_cast(action)) + if (dynamic_cast(action) || dynamic_cast(action)) return 0.0f; - return 1.0f; -} - -//SINDRAGOSA -float IccSindragosaTankPositionMultiplier::GetValue(Action* action) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss) - return 1.0f; - - if (dynamic_cast(action)) - return 1.0f; - else if (dynamic_cast(action)) - return 0.0f; - return 1.0f; -} - -float IccSindragosaFrostBeaconMultiplier::GetValue(Action* action) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss) - return 1.0f; - - if (!dynamic_cast(action)) - return 1.0f; - - // Highest priority if we have beacon - if (bot->HasAura(70126)) - return 2.0f; - - // Lower priority for non-beaconed players - float const MIN_SAFE_DISTANCE = 11.0f; - GuidVector members = AI_VALUE(GuidVector, "group members"); - - for (auto& member : members) + if (botAI->IsTank(bot)) { - Unit* player = botAI->GetUnit(member); - if (!player || player->GetGUID() == bot->GetGUID()) - continue; - - if (player->HasAura(70126)) // Frost Beacon - { - float dist = bot->GetExactDist2d(player); - if (dist < MIN_SAFE_DISTANCE) - return 1.5f; // Medium priority if too close to beaconed player - } + if (dynamic_cast(action)) + return 0.0f; + } + + if (botAI->IsHeal(bot) && (twistedNightmares || emeraldVigor)) + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + if (bot->HasAura(SPELL_DREAM_STATE) && !bot->HealthBelowPct(50)) + { + if (dynamic_cast(action)) + return 1.0f; // Allow Dream Cloud action + else + return 0.0f; // Cancel all other actions when we need to handle Dream Cloud } return 1.0f; + } -float IccSindragosaBlisteringColdPriorityMultiplier::GetValue(Action* action) +//SINDRAGOSA + +float IccSindragosaMultiplier::GetValue(Action* action) { - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); + Unit* boss = bot->FindNearestCreature(NPC_SINDRAGOSA, 200.0f); if (!boss) return 1.0f; + Aura* aura = botAI->GetAura("Unchained Magic", bot, false, true); + + Difficulty diff = bot->GetRaidDifficulty(); + + if (boss->HealthBelowPct(95)) + { + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + } + + if (aura && (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC) && + !dynamic_cast(action)) + { + if (dynamic_cast(action) || dynamic_cast(action)) + return 1.0f; + else + return 0.0f; + } // Check if boss is casting blistering cold (using both normal and heroic spell IDs) - if (boss->HasUnitState(UNIT_STATE_CASTING) && - (boss->FindCurrentSpellBySpellId(70123) || boss->FindCurrentSpellBySpellId(71047) || + if (boss->HasUnitState(UNIT_STATE_CASTING) && + (boss->FindCurrentSpellBySpellId(70123) || boss->FindCurrentSpellBySpellId(71047) || boss->FindCurrentSpellBySpellId(71048) || boss->FindCurrentSpellBySpellId(71049))) { // If this is the blistering cold action, give it highest priority if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) - return 5.0f; + return 1.0f; // Disable all other actions while blistering cold is casting return 0.0f; } - return 1.0f; -} + // Highest priority if we have beacon + if (bot->HasAura(SPELL_FROST_BEACON)) + { + if (dynamic_cast(action)) + return 1.0f; + else + return 0.0f; + } -float IccSindragosaMysticBuffetMultiplier::GetValue(Action* action) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss) - return 1.0f; + Group* group = bot->GetGroup(); + // Check if anyone in group has Frost Beacon (SPELL_FROST_BEACON) + bool anyoneHasFrostBeacon = false; + + if (group) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && member->HasAura(SPELL_FROST_BEACON)) + { + anyoneHasFrostBeacon = true; + break; + } + } + } + + if (anyoneHasFrostBeacon && boss && + boss->GetExactDist2d(ICC_SINDRAGOSA_FLYING_POSITION.GetPositionX(), + ICC_SINDRAGOSA_FLYING_POSITION.GetPositionY()) < 30.0f && + !boss->HealthBelowPct(25) && !boss->HealthAbovePct(99)) + { + if (dynamic_cast(action)) + return 1.0f; + else + return 0.0f; + } + + if (anyoneHasFrostBeacon && !botAI->IsMainTank(bot)) + { + if (dynamic_cast(action)) + return 0.0f; + } if (botAI->IsMainTank(bot)) { Aura* aura = botAI->GetAura("mystic buffet", bot, false, true); - if (aura && aura->GetStackAmount() >= 8) + if (aura && aura->GetStackAmount() >= 6) { if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action)) - { + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) return 0.0f; - } } } - if (boss->GetVictim() == bot) - return 1.0f; - - // Only modify actions if we have buffet stacks - Aura* aura = bot->GetAura(70127); - Aura* aura2 = bot->GetAura(72528); - - // Return normal priority if no auras or not enough stacks - if (!aura && !aura2) - return 1.0f; - - bool hasEnoughStacks = (aura && aura->GetStackAmount() >= 3) || (aura2 && aura2->GetStackAmount() >= 3); - if (!hasEnoughStacks) - return 1.0f; - - if (dynamic_cast(action) || - dynamic_cast(action)) - return 5.0f; - else if (dynamic_cast(action) || - dynamic_cast(action) - || dynamic_cast(action) || dynamic_cast(action)) - return 0.0f; - return 1.0f; -} - -float IccSindragosaFrostBombMultiplier::GetValue(Action* action) -{ - - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss) - return 1.0f; - - float const MAX_REACTION_RANGE = 200.0f; - - // Check if there's an active frost bomb marker within range - bool hasMarkerInRange = false; - float closestDist = std::numeric_limits::max(); - - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto& npc : npcs) + if (!botAI->IsTank(bot) && boss && boss->HealthBelowPct(35)) { - Unit* unit = botAI->GetUnit(npc); - if (!unit || !unit->IsAlive()) - continue; + if (dynamic_cast(action)) + return 0.0f; + } - if (unit->HasAura(70022)) // Frost bomb visual + if (boss && botAI->IsTank(bot)) + { + if (boss->HealthBelowPct(35)) { - float dist = bot->GetDistance(unit); - if (dist <= MAX_REACTION_RANGE && dist < closestDist) - { - hasMarkerInRange = true; - closestDist = dist; - } + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + return 1.0f; + else + return 0.0f; } } - if (!hasMarkerInRange) - return 1.0f; + if (boss && boss->GetExactDist2d(ICC_SINDRAGOSA_FLYING_POSITION.GetPositionX(), ICC_SINDRAGOSA_FLYING_POSITION.GetPositionY()) < 30.0f && !boss->HealthBelowPct(25) && !boss->HealthAbovePct(99)) + { + if (dynamic_cast(action)) + return 1.0f; + + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } - if (dynamic_cast(action)) - return 5.0f; - else if (dynamic_cast(action) || - dynamic_cast(action) - || dynamic_cast(action) - || dynamic_cast(action)) - return 0.0f; return 1.0f; } -float IccLichKingNecroticPlagueMultiplier::GetValue(Action* action) +float IccLichKingAddsMultiplier::GetValue(Action* action) { + Unit* terenasMenethilHC = bot->FindNearestCreature(NPC_TERENAS_MENETHIL_HC, 55.0f); + + if (!terenasMenethilHC) + if (dynamic_cast(action)) + return 0.0f; + + if (terenasMenethilHC) + { + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + + if (!botAI->IsMainTank(bot) && mainTank && bot->GetExactDist2d(mainTank->GetPositionX(), mainTank->GetPositionY()) < 2.0f) + { + if (dynamic_cast(action)) + return 0.0f; + } + + if (botAI->IsMelee(bot) || (bot->getClass() == CLASS_WARLOCK)) + { + if (dynamic_cast(action) || dynamic_cast(action)) + return 1.0f; + else + return 0.0f; + } + + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + } + Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); if (!boss) return 1.0f; - /* - if (!botAI->IsHeal(bot) && (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action))) - return 0.0f; - */ - // Handle cure actions if (dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action) || @@ -659,35 +769,69 @@ float IccLichKingNecroticPlagueMultiplier::GetValue(Action* action) return 0.0f; } - // Block combat formation actions by default - if (dynamic_cast(action)) + if (dynamic_cast(action) && (bot->getClass() != CLASS_HUNTER)) return 0.0f; - return 1.0f; -} + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + if (boss && !boss->HealthBelowPct(71)) + { + if (!botAI->IsTank(bot)) + if (dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + } -float IccLichKingAddsMultiplier::GetValue(Action* action) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); - if (!boss) - return 1.0f; Unit* currentTarget = AI_VALUE(Unit*, "current target"); - if (dynamic_cast(action)) + bool hasWinterAura = boss && (boss->HasAura(SPELL_REMORSELESS_WINTER1) || boss->HasAura(SPELL_REMORSELESS_WINTER2) || boss->HasAura(SPELL_REMORSELESS_WINTER3) || boss->HasAura(SPELL_REMORSELESS_WINTER4)); + bool hasWinter2Aura = boss && (boss->HasAura(SPELL_REMORSELESS_WINTER5) || boss->HasAura(SPELL_REMORSELESS_WINTER6) || boss->HasAura(SPELL_REMORSELESS_WINTER7) || boss->HasAura(SPELL_REMORSELESS_WINTER8)); + bool isCasting = boss && boss->HasUnitState(UNIT_STATE_CASTING); + bool isWinter = boss && (boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER1) || boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER2) || + boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER5) || boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER6) || + boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER3) || boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER4) || + boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER7) || boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER8)); + + if (hasWinterAura || hasWinter2Aura || (isCasting && isWinter)) { - if (currentTarget && currentTarget->GetGUID() == boss->GetGUID()) - { - if (dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action)) + if (dynamic_cast(action) || dynamic_cast(action)) + return 1.0f; + + if (botAI->IsAssistTank(bot) && dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (currentTarget && boss && bot->GetDistance2d(boss->GetPositionX(), boss->GetPositionY()) > 50.0f && currentTarget == boss) + { + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action)) return 0.0f; } - if (dynamic_cast(action) || dynamic_cast(action)) - return 0.0f; + if (currentTarget && (currentTarget->GetEntry() == NPC_ICE_SPHERE1 || currentTarget->GetEntry() == NPC_ICE_SPHERE2 || + currentTarget->GetEntry() == NPC_ICE_SPHERE3 || currentTarget->GetEntry() == NPC_ICE_SPHERE4)) + { + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } - return 1.0f; } - if (botAI->IsRanged(bot)) + if (botAI->IsRanged(bot) && !botAI->GetAura("Harvest Soul", bot, false, false)) { // Check for defile presence GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); @@ -695,7 +839,7 @@ float IccLichKingAddsMultiplier::GetValue(Action* action) for (auto& npc : npcs) { Unit* unit = botAI->GetUnit(npc); - if (unit && unit->IsAlive() && unit->GetEntry() == 38757) // Defile entry + if (unit && unit->IsAlive() && unit->GetEntry() == DEFILE_NPC_ID) // Defile entry { defilePresent = true; break; @@ -714,25 +858,11 @@ float IccLichKingAddsMultiplier::GetValue(Action* action) } } - if (botAI->IsAssistTank(bot) && !boss->HealthBelowPct(71)) + if (botAI->IsAssistTank(bot) && boss && !boss->HealthBelowPct(71) && currentTarget == boss) { - // Allow BPC-specific actions - if (dynamic_cast(action)) - return 1.0f; - - // Disable normal assist behavior - if (dynamic_cast(action)) - return 0.0f; - - if (dynamic_cast(action)) - return 0.0f; - - // Disable following - if (dynamic_cast(action)) + if (dynamic_cast(action)) return 0.0f; } return 1.0f; } - -//raging spirit, Ice sphere, valkyere shadowguard, sphere 36633 39305 39306 39307 diff --git a/src/strategy/raids/icecrown/RaidIccMultipliers.h b/src/strategy/raids/icecrown/RaidIccMultipliers.h index beaead63..e734ed3e 100644 --- a/src/strategy/raids/icecrown/RaidIccMultipliers.h +++ b/src/strategy/raids/icecrown/RaidIccMultipliers.h @@ -68,20 +68,13 @@ public: }; //BQL -class IccBqlPactOfDarkfallenMultiplier : public Multiplier +class IccBqlMultiplier : public Multiplier { public: - IccBqlPactOfDarkfallenMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "icc bql pact of darkfallen multiplier") {} + IccBqlMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "icc bql multiplier") {} virtual float GetValue(Action* action) override; }; -class IccBqlVampiricBiteMultiplier : public Multiplier -{ -public: - IccBqlVampiricBiteMultiplier(PlayerbotAI* ai) : Multiplier(ai, "icc bql vampiric bite") {} - virtual float GetValue(Action* action); -}; - //VDW class IccValithriaDreamCloudMultiplier : public Multiplier { @@ -91,56 +84,14 @@ public: }; //SINDRAGOSA -class IccSindragosaTankPositionMultiplier : public Multiplier +class IccSindragosaMultiplier : public Multiplier { public: - IccSindragosaTankPositionMultiplier(PlayerbotAI* ai) : Multiplier(ai, "icc sindragosa tank position") {} - virtual float GetValue(Action* action); -}; - -class IccSindragosaFrostBeaconMultiplier : public Multiplier -{ -public: - IccSindragosaFrostBeaconMultiplier(PlayerbotAI* ai) : Multiplier(ai, "icc sindragosa frost beacon") {} - virtual float GetValue(Action* action); -}; - -/*class IccSindragosaFlyingMultiplier : public Multiplier -{ -public: - IccSindragosaFlyingMultiplier(PlayerbotAI* ai) : Multiplier(ai, "icc sindragosa flying") {} - virtual float GetValue(Action* action); -};*/ - -class IccSindragosaMysticBuffetMultiplier : public Multiplier -{ -public: - IccSindragosaMysticBuffetMultiplier(PlayerbotAI* ai) : Multiplier(ai, "icc sindragosa mystic buffet") {} - virtual float GetValue(Action* action); -}; - -class IccSindragosaBlisteringColdPriorityMultiplier : public Multiplier -{ -public: - IccSindragosaBlisteringColdPriorityMultiplier(PlayerbotAI* ai) : Multiplier(ai, "sindragosa blistering cold priority") {} - - virtual float GetValue(Action* action) override; -}; - -class IccSindragosaFrostBombMultiplier : public Multiplier -{ -public: - IccSindragosaFrostBombMultiplier(PlayerbotAI* ai) : Multiplier(ai, "icc sindragosa frost bomb") {} - virtual float GetValue(Action* action); -}; - -class IccLichKingNecroticPlagueMultiplier : public Multiplier -{ -public: - IccLichKingNecroticPlagueMultiplier(PlayerbotAI* ai) : Multiplier(ai, "icc lich king necrotic plague") {} + IccSindragosaMultiplier(PlayerbotAI* ai) : Multiplier(ai, "icc sindragosa") {} virtual float GetValue(Action* action); }; +//LK class IccLichKingAddsMultiplier : public Multiplier { public: @@ -149,4 +100,4 @@ public: }; -#endif \ No newline at end of file +#endif diff --git a/src/strategy/raids/icecrown/RaidIccStrategy.cpp b/src/strategy/raids/icecrown/RaidIccStrategy.cpp index 7b204661..53bbe601 100644 --- a/src/strategy/raids/icecrown/RaidIccStrategy.cpp +++ b/src/strategy/raids/icecrown/RaidIccStrategy.cpp @@ -5,25 +5,19 @@ void RaidIccStrategy::InitTriggers(std::vector& triggers) { //Lord Marrogwar - triggers.push_back(new TriggerNode("icc lm tank position", - NextAction::array(0, new NextAction("icc lm tank position", ACTION_RAID + 5), nullptr))); - - triggers.push_back(new TriggerNode("icc spike near", - NextAction::array(0, new NextAction("icc spike", ACTION_RAID + 3), nullptr))); + triggers.push_back(new TriggerNode("icc lm", + NextAction::array(0, new NextAction("icc lm tank position", ACTION_RAID + 5), + new NextAction("icc spike", ACTION_RAID + 3), nullptr))); //Lady Deathwhisper triggers.push_back(new TriggerNode("icc dark reckoning", NextAction::array(0, new NextAction("icc dark reckoning", ACTION_MOVE + 5), nullptr))); - triggers.push_back(new TriggerNode("icc ranged position lady deathwhisper", - NextAction::array(0, new NextAction("icc ranged position lady deathwhisper", ACTION_MOVE + 2), nullptr))); + triggers.push_back(new TriggerNode("icc lady deathwhisper", + NextAction::array(0, new NextAction("icc ranged position lady deathwhisper", ACTION_MOVE + 2), + new NextAction("icc adds lady deathwhisper", ACTION_RAID + 3), + new NextAction("icc shade lady deathwhisper", ACTION_RAID + 4), nullptr))); - triggers.push_back(new TriggerNode("icc adds lady deathwhisper", - NextAction::array(0, new NextAction("icc adds lady deathwhisper", ACTION_RAID + 3), nullptr))); - - triggers.push_back(new TriggerNode("icc shade lady deathwhisper", - NextAction::array(0, new NextAction("icc shade lady deathwhisper", ACTION_MOVE + 5), nullptr))); - //Gunship Battle triggers.push_back(new TriggerNode("icc rotting frost giant tank position", NextAction::array(0, new NextAction("icc rotting frost giant tank position", ACTION_RAID + 5), nullptr))); @@ -41,22 +35,20 @@ void RaidIccStrategy::InitTriggers(std::vector& triggers) NextAction::array(0, new NextAction("icc gunship teleport horde", ACTION_RAID + 4), nullptr))); //DBS - triggers.push_back(new TriggerNode("icc dbs tank position", - NextAction::array(0, new NextAction("icc dbs tank position", ACTION_RAID + 3), nullptr))); + triggers.push_back(new TriggerNode("icc dbs", + NextAction::array(0, new NextAction("icc dbs tank position", ACTION_RAID + 3), + new NextAction("icc adds dbs", ACTION_RAID + 5), nullptr))); triggers.push_back(new TriggerNode("icc dbs main tank rune of blood", NextAction::array(0, new NextAction("taunt spell", ACTION_EMERGENCY + 4), nullptr))); - - triggers.push_back(new TriggerNode("icc adds dbs", - NextAction::array(0, new NextAction("icc adds dbs", ACTION_RAID + 5), nullptr))); //DOGS triggers.push_back(new TriggerNode("icc stinky precious main tank mortal wound", NextAction::array(0, new NextAction("taunt spell", ACTION_EMERGENCY + 4), nullptr))); //FESTERGUT - triggers.push_back(new TriggerNode("icc festergut tank position", - NextAction::array(0, new NextAction("icc festergut tank position", ACTION_MOVE + 4), nullptr))); + triggers.push_back(new TriggerNode("icc festergut group position", + NextAction::array(0, new NextAction("icc festergut group position", ACTION_MOVE + 4), nullptr))); triggers.push_back(new TriggerNode("icc festergut main tank gastric bloat", NextAction::array(0, new NextAction("taunt spell", ACTION_EMERGENCY + 6), nullptr))); @@ -85,18 +77,15 @@ void RaidIccStrategy::InitTriggers(std::vector& triggers) NextAction::array(0, new NextAction("icc putricide growing ooze puddle", ACTION_RAID + 3), nullptr))); triggers.push_back(new TriggerNode("icc putricide main tank mutated plague", - NextAction::array(0, new NextAction("taunt spell", ACTION_RAID + 6), nullptr))); + NextAction::array(0, new NextAction("taunt spell", ACTION_RAID + 10), nullptr))); triggers.push_back(new TriggerNode("icc putricide malleable goo", - NextAction::array(0, new NextAction("avoid malleable goo", ACTION_RAID + 2), nullptr))); + NextAction::array(0, new NextAction("icc putricide avoid malleable goo", ACTION_RAID + 2), nullptr))); //BPC triggers.push_back(new TriggerNode("icc bpc keleseth tank", NextAction::array(0, new NextAction("icc bpc keleseth tank", ACTION_RAID + 1), nullptr))); - triggers.push_back(new TriggerNode("icc bpc nucleus", - NextAction::array(0, new NextAction("icc bpc nucleus", ACTION_RAID + 2), nullptr))); - triggers.push_back(new TriggerNode("icc bpc main tank", NextAction::array(0, new NextAction("icc bpc main tank", ACTION_RAID + 3), nullptr))); @@ -105,10 +94,13 @@ void RaidIccStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("icc bpc kinetic bomb", NextAction::array(0, new NextAction("icc bpc kinetic bomb", ACTION_RAID + 6), nullptr))); + + triggers.push_back(new TriggerNode("icc bpc ball of flame", + NextAction::array(0, new NextAction("icc bpc ball of flame", ACTION_RAID + 7), nullptr))); //BQL - triggers.push_back(new TriggerNode("icc bql tank position", - NextAction::array(0, new NextAction("icc bql tank position", ACTION_RAID), nullptr))); + triggers.push_back(new TriggerNode("icc bql group position", + NextAction::array(0, new NextAction("icc bql group position", ACTION_RAID), nullptr))); triggers.push_back(new TriggerNode("icc bql pact of darkfallen", NextAction::array(0, new NextAction("icc bql pact of darkfallen", ACTION_RAID +1), nullptr))); @@ -116,25 +108,30 @@ void RaidIccStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("icc bql vampiric bite", NextAction::array(0, new NextAction("icc bql vampiric bite", ACTION_EMERGENCY + 5), nullptr))); - //VDW + //Sister Svalna triggers.push_back(new TriggerNode("icc valkyre spear", NextAction::array(0, new NextAction("icc valkyre spear", ACTION_EMERGENCY + 5), nullptr))); triggers.push_back(new TriggerNode("icc sister svalna", NextAction::array(0, new NextAction("icc sister svalna", ACTION_RAID + 5), nullptr))); + + //VDW + triggers.push_back(new TriggerNode("icc valithria group", + NextAction::array(0, new NextAction("icc valithria group", ACTION_RAID + 1), nullptr))); + triggers.push_back(new TriggerNode("icc valithria portal", NextAction::array(0, new NextAction("icc valithria portal", ACTION_RAID + 5), nullptr))); triggers.push_back(new TriggerNode("icc valithria heal", - NextAction::array(0, new NextAction("icc valithria heal", ACTION_RAID+1), nullptr))); + NextAction::array(0, new NextAction("icc valithria heal", ACTION_RAID+2), nullptr))); triggers.push_back(new TriggerNode("icc valithria dream cloud", NextAction::array(0, new NextAction("icc valithria dream cloud", ACTION_RAID + 4), nullptr))); //SINDRAGOSA - triggers.push_back(new TriggerNode("icc sindragosa tank position", - NextAction::array(0, new NextAction("icc sindragosa tank position", ACTION_RAID + 1), nullptr))); + triggers.push_back(new TriggerNode("icc sindragosa group position", + NextAction::array(0, new NextAction("icc sindragosa group position", ACTION_RAID + 1), nullptr))); triggers.push_back(new TriggerNode("icc sindragosa frost beacon", NextAction::array(0, new NextAction("icc sindragosa frost beacon", ACTION_RAID + 5), nullptr))); @@ -181,18 +178,10 @@ void RaidIccStrategy::InitMultipliers(std::vector& multipliers) multipliers.push_back(new IccDogsMultiplier(botAI)); multipliers.push_back(new IccFestergutMultiplier(botAI)); multipliers.push_back(new IccRotfaceMultiplier(botAI)); - //multipliers.push_back(new IccRotfaceGroupPositionMultiplier(botAI)); multipliers.push_back(new IccAddsPutricideMultiplier(botAI)); multipliers.push_back(new IccBpcAssistMultiplier(botAI)); - multipliers.push_back(new IccBqlPactOfDarkfallenMultiplier(botAI)); - multipliers.push_back(new IccBqlVampiricBiteMultiplier(botAI)); + multipliers.push_back(new IccBqlMultiplier(botAI)); multipliers.push_back(new IccValithriaDreamCloudMultiplier(botAI)); - multipliers.push_back(new IccSindragosaTankPositionMultiplier(botAI)); - multipliers.push_back(new IccSindragosaFrostBeaconMultiplier(botAI)); - //multipliers.push_back(new IccSindragosaFlyingMultiplier(botAI)); - multipliers.push_back(new IccSindragosaMysticBuffetMultiplier(botAI)); - multipliers.push_back(new IccSindragosaBlisteringColdPriorityMultiplier(botAI)); - multipliers.push_back(new IccSindragosaFrostBombMultiplier(botAI)); - multipliers.push_back(new IccLichKingNecroticPlagueMultiplier(botAI)); + multipliers.push_back(new IccSindragosaMultiplier(botAI)); multipliers.push_back(new IccLichKingAddsMultiplier(botAI)); } diff --git a/src/strategy/raids/icecrown/RaidIccTriggerContext.h b/src/strategy/raids/icecrown/RaidIccTriggerContext.h index 2b0d800f..c2479baf 100644 --- a/src/strategy/raids/icecrown/RaidIccTriggerContext.h +++ b/src/strategy/raids/icecrown/RaidIccTriggerContext.h @@ -10,46 +10,55 @@ class RaidIccTriggerContext : public NamedObjectContext public: RaidIccTriggerContext() { - creators["icc lm tank position"] = &RaidIccTriggerContext::icc_lm_tank_position; - creators["icc spike near"] = &RaidIccTriggerContext::icc_spike_near; + creators["icc lm"] = &RaidIccTriggerContext::icc_lm; + creators["icc dark reckoning"] = &RaidIccTriggerContext::icc_dark_reckoning; - creators["icc ranged position lady deathwhisper"] = &RaidIccTriggerContext::icc_ranged_position_lady_deathwhisper; - creators["icc adds lady deathwhisper"] = &RaidIccTriggerContext::icc_adds_lady_deathwhisper; - creators["icc shade lady deathwhisper"] = &RaidIccTriggerContext::icc_shade_lady_deathwhisper; + creators["icc lady deathwhisper"] = &RaidIccTriggerContext::icc_lady_deathwhisper; + creators["icc rotting frost giant tank position"] = &RaidIccTriggerContext::icc_rotting_frost_giant_tank_position; creators["icc in cannon"] = &RaidIccTriggerContext::icc_in_cannon; creators["icc gunship cannon near"] = &RaidIccTriggerContext::icc_gunship_cannon_near; creators["icc gunship teleport ally"] = &RaidIccTriggerContext::icc_gunship_teleport_ally; creators["icc gunship teleport horde"] = &RaidIccTriggerContext::icc_gunship_teleport_horde; - creators["icc dbs tank position"] = &RaidIccTriggerContext::icc_dbs_tank_position; + + creators["icc dbs"] = &RaidIccTriggerContext::icc_dbs; creators["icc dbs main tank rune of blood"] = &RaidIccTriggerContext::icc_dbs_main_tank_rune_of_blood; - creators["icc adds dbs"] = &RaidIccTriggerContext::icc_adds_dbs; + creators["icc stinky precious main tank mortal wound"] = &RaidIccTriggerContext::icc_stinky_precious_main_tank_mortal_wound; - creators["icc festergut tank position"] = &RaidIccTriggerContext::icc_festergut_tank_position; + + creators["icc festergut group position"] = &RaidIccTriggerContext::icc_festergut_group_position; creators["icc festergut main tank gastric bloat"] = &RaidIccTriggerContext::icc_festergut_main_tank_gastric_bloat; creators["icc festergut spore"] = &RaidIccTriggerContext::icc_festergut_spore; + creators["icc rotface tank position"] = &RaidIccTriggerContext::icc_rotface_tank_position; creators["icc rotface group position"] = &RaidIccTriggerContext::icc_rotface_group_position; creators["icc rotface move away from explosion"] = &RaidIccTriggerContext::icc_rotface_move_away_from_explosion; + creators["icc putricide volatile ooze"] = &RaidIccTriggerContext::icc_putricide_volatile_ooze; creators["icc putricide gas cloud"] = &RaidIccTriggerContext::icc_putricide_gas_cloud; creators["icc putricide growing ooze puddle"] = &RaidIccTriggerContext::icc_putricide_growing_ooze_puddle; creators["icc putricide main tank mutated plague"] = &RaidIccTriggerContext::icc_putricide_main_tank_mutated_plague; creators["icc putricide malleable goo"] = &RaidIccTriggerContext::icc_putricide_malleable_goo; + creators["icc bpc keleseth tank"] = &RaidIccTriggerContext::icc_bpc_keleseth_tank; - creators["icc bpc nucleus"] = &RaidIccTriggerContext::icc_bpc_nucleus; creators["icc bpc main tank"] = &RaidIccTriggerContext::icc_bpc_main_tank; creators["icc bpc empowered vortex"] = &RaidIccTriggerContext::icc_bpc_empowered_vortex; creators["icc bpc kinetic bomb"] = &RaidIccTriggerContext::icc_bpc_kinetic_bomb; - creators["icc bql tank position"] = &RaidIccTriggerContext::icc_bql_tank_position; + creators["icc bpc ball of flame"] = &RaidIccTriggerContext::icc_bpc_ball_of_flame; + + creators["icc bql group position"] = &RaidIccTriggerContext::icc_bql_group_position; creators["icc bql pact of darkfallen"] = &RaidIccTriggerContext::icc_bql_pact_of_darkfallen; creators["icc bql vampiric bite"] = &RaidIccTriggerContext::icc_bql_vampiric_bite; + creators["icc valkyre spear"] = &RaidIccTriggerContext::icc_valkyre_spear; creators["icc sister svalna"] = &RaidIccTriggerContext::icc_sister_svalna; + + creators["icc valithria group"] = &RaidIccTriggerContext::icc_valithria_group; creators["icc valithria portal"] = &RaidIccTriggerContext::icc_valithria_portal; creators["icc valithria heal"] = &RaidIccTriggerContext::icc_valithria_heal; creators["icc valithria dream cloud"] = &RaidIccTriggerContext::icc_valithria_dream_cloud; - creators["icc sindragosa tank position"] = &RaidIccTriggerContext::icc_sindragosa_tank_position; + + creators["icc sindragosa group position"] = &RaidIccTriggerContext::icc_sindragosa_group_position; creators["icc sindragosa frost beacon"] = &RaidIccTriggerContext::icc_sindragosa_frost_beacon; creators["icc sindragosa blistering cold"] = &RaidIccTriggerContext::icc_sindragosa_blistering_cold; creators["icc sindragosa unchained magic"] = &RaidIccTriggerContext::icc_sindragosa_unchained_magic; @@ -58,6 +67,7 @@ public: creators["icc sindragosa main tank mystic buffet"] = &RaidIccTriggerContext::icc_sindragosa_main_tank_mystic_buffet; creators["icc sindragosa frost bomb"] = &RaidIccTriggerContext::icc_sindragosa_frost_bomb; creators["icc sindragosa tank swap position"] = &RaidIccTriggerContext::icc_sindragosa_tank_swap_position; + creators["icc lich king shadow trap"] = &RaidIccTriggerContext::icc_lich_king_shadow_trap; creators["icc lich king necrotic plague"] = &RaidIccTriggerContext::icc_lich_king_necrotic_plague; creators["icc lich king winter"] = &RaidIccTriggerContext::icc_lich_king_winter; @@ -65,46 +75,55 @@ public: } private: - static Trigger* icc_lm_tank_position(PlayerbotAI* ai) { return new IccLmTankPositionTrigger(ai); } - static Trigger* icc_spike_near(PlayerbotAI* ai) { return new IccSpikeNearTrigger(ai); } + static Trigger* icc_lm(PlayerbotAI* ai) { return new IccLmTrigger(ai); } + static Trigger* icc_dark_reckoning(PlayerbotAI* ai) { return new IccDarkReckoningTrigger(ai); } - static Trigger* icc_ranged_position_lady_deathwhisper(PlayerbotAI* ai) { return new IccRangedPositionLadyDeathwhisperTrigger(ai); } - static Trigger* icc_adds_lady_deathwhisper(PlayerbotAI* ai) { return new IccAddsLadyDeathwhisperTrigger(ai); } - static Trigger* icc_shade_lady_deathwhisper(PlayerbotAI* ai) { return new IccShadeLadyDeathwhisperTrigger(ai); } + static Trigger* icc_lady_deathwhisper(PlayerbotAI* ai) { return new IccLadyDeathwhisperTrigger(ai); } + static Trigger* icc_rotting_frost_giant_tank_position(PlayerbotAI* ai) { return new IccRottingFrostGiantTankPositionTrigger(ai); } static Trigger* icc_in_cannon(PlayerbotAI* ai) { return new IccInCannonTrigger(ai); } static Trigger* icc_gunship_cannon_near(PlayerbotAI* ai) { return new IccGunshipCannonNearTrigger(ai); } static Trigger* icc_gunship_teleport_ally(PlayerbotAI* ai) { return new IccGunshipTeleportAllyTrigger(ai); } static Trigger* icc_gunship_teleport_horde(PlayerbotAI* ai) { return new IccGunshipTeleportHordeTrigger(ai); } - static Trigger* icc_dbs_tank_position(PlayerbotAI* ai) { return new IccDbsTankPositionTrigger(ai); } + + static Trigger* icc_dbs(PlayerbotAI* ai) { return new IccDbsTrigger(ai); } static Trigger* icc_dbs_main_tank_rune_of_blood(PlayerbotAI* ai) { return new IccDbsMainTankRuneOfBloodTrigger(ai); } - static Trigger* icc_adds_dbs(PlayerbotAI* ai) { return new IccAddsDbsTrigger(ai); } + static Trigger* icc_stinky_precious_main_tank_mortal_wound(PlayerbotAI* ai) { return new IccStinkyPreciousMainTankMortalWoundTrigger(ai); } - static Trigger* icc_festergut_tank_position(PlayerbotAI* ai) { return new IccFestergutTankPositionTrigger(ai); } + + static Trigger* icc_festergut_group_position(PlayerbotAI* ai) { return new IccFestergutGroupPositionTrigger(ai); } static Trigger* icc_festergut_main_tank_gastric_bloat(PlayerbotAI* ai) { return new IccFestergutMainTankGastricBloatTrigger(ai); } static Trigger* icc_festergut_spore(PlayerbotAI* ai) { return new IccFestergutSporeTrigger(ai); } + static Trigger* icc_rotface_tank_position(PlayerbotAI* ai) { return new IccRotfaceTankPositionTrigger(ai); } static Trigger* icc_rotface_group_position(PlayerbotAI* ai) { return new IccRotfaceGroupPositionTrigger(ai); } static Trigger* icc_rotface_move_away_from_explosion(PlayerbotAI* ai) { return new IccRotfaceMoveAwayFromExplosionTrigger(ai); } + static Trigger* icc_putricide_volatile_ooze(PlayerbotAI* ai) { return new IccPutricideVolatileOozeTrigger(ai); } static Trigger* icc_putricide_gas_cloud(PlayerbotAI* ai) { return new IccPutricideGasCloudTrigger(ai); } static Trigger* icc_putricide_growing_ooze_puddle(PlayerbotAI* ai) { return new IccPutricideGrowingOozePuddleTrigger(ai); } static Trigger* icc_putricide_main_tank_mutated_plague(PlayerbotAI* ai) { return new IccPutricideMainTankMutatedPlagueTrigger(ai); } static Trigger* icc_putricide_malleable_goo(PlayerbotAI* ai) { return new IccPutricideMalleableGooTrigger(ai); } + static Trigger* icc_bpc_keleseth_tank(PlayerbotAI* ai) { return new IccBpcKelesethTankTrigger(ai); } - static Trigger* icc_bpc_nucleus(PlayerbotAI* ai) { return new IccBpcNucleusTrigger(ai); } static Trigger* icc_bpc_main_tank(PlayerbotAI* ai) { return new IccBpcMainTankTrigger(ai); } static Trigger* icc_bpc_empowered_vortex(PlayerbotAI* ai) { return new IccBpcEmpoweredVortexTrigger(ai); } static Trigger* icc_bpc_kinetic_bomb(PlayerbotAI* ai) { return new IccBpcKineticBombTrigger(ai); } - static Trigger* icc_bql_tank_position(PlayerbotAI* ai) { return new IccBqlTankPositionTrigger(ai); } + static Trigger* icc_bpc_ball_of_flame(PlayerbotAI* ai) { return new IccBpcBallOfFlameTrigger(ai); } + + static Trigger* icc_bql_group_position(PlayerbotAI* ai) { return new IccBqlGroupPositionTrigger(ai); } static Trigger* icc_bql_pact_of_darkfallen(PlayerbotAI* ai) { return new IccBqlPactOfDarkfallenTrigger(ai); } static Trigger* icc_bql_vampiric_bite(PlayerbotAI* ai) { return new IccBqlVampiricBiteTrigger(ai); } + static Trigger* icc_valkyre_spear(PlayerbotAI* ai) { return new IccValkyreSpearTrigger(ai); } static Trigger* icc_sister_svalna(PlayerbotAI* ai) { return new IccSisterSvalnaTrigger(ai); } + + static Trigger* icc_valithria_group(PlayerbotAI* ai) { return new IccValithriaGroupTrigger(ai); } static Trigger* icc_valithria_portal(PlayerbotAI* ai) { return new IccValithriaPortalTrigger(ai); } static Trigger* icc_valithria_heal(PlayerbotAI* ai) { return new IccValithriaHealTrigger(ai); } static Trigger* icc_valithria_dream_cloud(PlayerbotAI* ai) { return new IccValithriaDreamCloudTrigger(ai); } - static Trigger* icc_sindragosa_tank_position(PlayerbotAI* ai) { return new IccSindragosaTankPositionTrigger(ai); } + + static Trigger* icc_sindragosa_group_position(PlayerbotAI* ai) { return new IccSindragosaGroupPositionTrigger(ai); } static Trigger* icc_sindragosa_frost_beacon(PlayerbotAI* ai) { return new IccSindragosaFrostBeaconTrigger(ai); } static Trigger* icc_sindragosa_blistering_cold(PlayerbotAI* ai) { return new IccSindragosaBlisteringColdTrigger(ai); } static Trigger* icc_sindragosa_unchained_magic(PlayerbotAI* ai) { return new IccSindragosaUnchainedMagicTrigger(ai); } @@ -113,6 +132,7 @@ private: static Trigger* icc_sindragosa_main_tank_mystic_buffet(PlayerbotAI* ai) { return new IccSindragosaMainTankMysticBuffetTrigger(ai); } static Trigger* icc_sindragosa_frost_bomb(PlayerbotAI* ai) { return new IccSindragosaFrostBombTrigger(ai); } static Trigger* icc_sindragosa_tank_swap_position(PlayerbotAI* ai) { return new IccSindragosaTankSwapPositionTrigger(ai); } + static Trigger* icc_lich_king_shadow_trap(PlayerbotAI* ai) { return new IccLichKingShadowTrapTrigger(ai); } static Trigger* icc_lich_king_necrotic_plague(PlayerbotAI* ai) { return new IccLichKingNecroticPlagueTrigger(ai); } static Trigger* icc_lich_king_winter(PlayerbotAI* ai) { return new IccLichKingWinterTrigger(ai); } diff --git a/src/strategy/raids/icecrown/RaidIccTriggers.cpp b/src/strategy/raids/icecrown/RaidIccTriggers.cpp index 9af3cee3..d8b72cf5 100644 --- a/src/strategy/raids/icecrown/RaidIccTriggers.cpp +++ b/src/strategy/raids/icecrown/RaidIccTriggers.cpp @@ -9,80 +9,54 @@ #include "Playerbots.h" #include "ScriptedCreature.h" #include "Trigger.h" +#include "CellImpl.h" +#include "GridNotifiers.h" +#include "GridNotifiersImpl.h" +#include "Vehicle.h" //Lord Marrogwar -bool IccLmTankPositionTrigger::IsActive() +bool IccLmTrigger::IsActive() { Unit* boss = AI_VALUE2(Unit*, "find target", "lord marrowgar"); - if (!boss) { return false; } - - return botAI->IsTank(bot); -} - -bool IccSpikeNearTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "lord marrowgar"); - if (!boss) + if (!boss) return false; + if (bot->HasAura(SPELL_EXPERIENCED)) + bot->RemoveAura(SPELL_EXPERIENCED); + return true; } //Lady Deathwhisper bool IccDarkReckoningTrigger::IsActive() { - Unit* add = AI_VALUE2(Unit*, "find target", "deathspeaker high priest"); - if (add || bot->HasAura(69483)) + if (bot->HasAura(SPELL_DARK_RECKONING)) return true; return false; } -bool IccRangedPositionLadyDeathwhisperTrigger::IsActive() +bool IccLadyDeathwhisperTrigger::IsActive() { Unit* boss = AI_VALUE2(Unit*, "find target", "lady deathwhisper"); if (!boss) return false; - return (botAI->IsRangedDps(bot) || botAI->IsHeal(bot)); + if (bot->HasAura(SPELL_EXPERIENCED)) + bot->RemoveAura(SPELL_EXPERIENCED); + + return true; } -bool IccAddsLadyDeathwhisperTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "lady deathwhisper"); - if (!boss) - return false; - - return true; -} - -bool IccShadeLadyDeathwhisperTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "lady deathwhisper"); - if (!boss) { return false; } - - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (unit) - { - if (unit->GetEntry() == 38222 ) //vengeful sahde ID - { - return true; - } - } - } - - return false; -} - - //Gunship Battle bool IccRottingFrostGiantTankPositionTrigger::IsActive() { Unit* boss = AI_VALUE2(Unit*, "find target", "rotting frost giant"); - if (!boss) { return false; } + if (!boss) + return false; + + if (bot->HasAura(SPELL_EXPERIENCED)) + bot->RemoveAura(SPELL_EXPERIENCED); return true; } @@ -95,7 +69,7 @@ bool IccInCannonTrigger::IsActive() return false; uint32 entry = vehicleBase->GetEntry(); - return entry == 36838 || entry == 36839; + return entry == NPC_CANNONA || entry == NPC_CANNONH; } bool IccGunshipCannonNearTrigger::IsActive() @@ -103,9 +77,9 @@ bool IccGunshipCannonNearTrigger::IsActive() if (bot->GetVehicle()) return false; - Unit* mount1 = bot->FindNearestCreature(36838, 100.0f); + Unit* mount1 = bot->FindNearestCreature(NPC_CANNONA, 100.0f); - Unit* mount2 = bot->FindNearestCreature(36839, 100.0f); + Unit* mount2 = bot->FindNearestCreature(NPC_CANNONH, 100.0f); if (!mount1 && !mount2) return false; @@ -124,7 +98,7 @@ bool IccGunshipCannonNearTrigger::IsActive() bool IccGunshipTeleportAllyTrigger::IsActive() { - Unit* boss = bot->FindNearestCreature(36939, 100.0f); + Unit* boss = bot->FindNearestCreature(NPC_HIGH_OVERLORD_SAURFANG, 100.0f); if (!boss) return false; @@ -136,7 +110,7 @@ bool IccGunshipTeleportAllyTrigger::IsActive() bool IccGunshipTeleportHordeTrigger::IsActive() { - Unit* boss = bot->FindNearestCreature(36948, 100.0f); + Unit* boss = bot->FindNearestCreature(NPC_MURADIN_BRONZEBEARD, 100.0f); if (!boss) return false; @@ -147,12 +121,15 @@ bool IccGunshipTeleportHordeTrigger::IsActive() } //DBS -bool IccDbsTankPositionTrigger::IsActive() +bool IccDbsTrigger::IsActive() { Unit* boss = AI_VALUE2(Unit*, "find target", "deathbringer saurfang"); if (!boss) return false; + if (bot->HasAura(SPELL_EXPERIENCED)) + bot->RemoveAura(SPELL_EXPERIENCED); + return true; } @@ -177,15 +154,6 @@ bool IccDbsMainTankRuneOfBloodTrigger::IsActive() return true; } -bool IccAddsDbsTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "deathbringer saurfang"); - if (!boss) - return false; - - return true; -} - //DOGS bool IccStinkyPreciousMainTankMortalWoundTrigger::IsActive() { @@ -197,29 +165,29 @@ bool IccStinkyPreciousMainTankMortalWoundTrigger::IsActive() return false; if (!botAI->IsAssistTankOfIndex(bot, 0)) - { return false; - } + Unit* mt = AI_VALUE(Unit*, "main tank"); if (!mt) - { return false; - } + Aura* aura = botAI->GetAura("mortal wound", mt, false, true); if (!aura || aura->GetStackAmount() < 8) - { return false; - } + return true; } //FESTERGUT -bool IccFestergutTankPositionTrigger::IsActive() +bool IccFestergutGroupPositionTrigger::IsActive() { Unit* boss = AI_VALUE2(Unit*, "find target", "festergut"); - if (!boss || !(botAI->IsTank(bot) || botAI->IsRanged(bot) || botAI->IsHeal(bot))) + if (!boss) return false; + if (bot->HasAura(SPELL_EXPERIENCED)) + bot->RemoveAura(SPELL_EXPERIENCED); + return true; } @@ -264,7 +232,7 @@ bool IccFestergutSporeTrigger::IsActive() if (!member) continue; - if (member->HasAura(69279)) // Spore aura ID + if (member->HasAura(SPELL_GAS_SPORE)) return true; } @@ -275,16 +243,20 @@ bool IccFestergutSporeTrigger::IsActive() bool IccRotfaceTankPositionTrigger::IsActive() { Unit* boss = AI_VALUE2(Unit*, "find target", "rotface"); - if (!boss || !(botAI->IsTank(bot) || botAI->IsMainTank(bot) || botAI->IsAssistTank(bot))) + if (!boss || !(botAI->IsTank(bot) || botAI->IsMelee(bot))) return false; + if (bot->HasAura(SPELL_EXPERIENCED)) + bot->RemoveAura(SPELL_EXPERIENCED); + return true; } bool IccRotfaceGroupPositionTrigger::IsActive() { Unit* boss = AI_VALUE2(Unit*, "find target", "rotface"); - if (!boss) { return false; } + if (!boss) + return false; return true; } @@ -292,9 +264,10 @@ bool IccRotfaceGroupPositionTrigger::IsActive() bool IccRotfaceMoveAwayFromExplosionTrigger::IsActive() { Unit* boss = AI_VALUE2(Unit*, "find target", "big ooze"); - if (!boss) { return false; } + if (!boss) + return false; - return boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(69839); + return boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_UNSTABLE_OOZE_EXPLOSION); } //PP @@ -306,12 +279,24 @@ bool IccPutricideGrowingOozePuddleTrigger::IsActive() if (!boss) return false; - // Early return if bot has debuffs that would make movement dangerous - if (botAI->HasAura("Gaseous Bloat", bot) || botAI->HasAura("Volatile Ooze Adhesive", bot)) - return false; + Difficulty diff = bot->GetRaidDifficulty(); - // Check for nearby dangerous puddles - static const uint32 PUDDLE_IDS[] = {37690, 70341}; // Growing Ooze Puddle, Slime Puddle + if (sPlayerbotAIConfig->EnableICCBuffs && diff && (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) + { + //-------CHEAT------- + if (!bot->HasAura(SPELL_EXPERIENCED)) + bot->AddAura(SPELL_EXPERIENCED, bot); + + if (!bot->HasAura(SPELL_AGEIS_OF_DALARAN)) + bot->AddAura(SPELL_AGEIS_OF_DALARAN, bot); + + if (!bot->HasAura(SPELL_NO_THREAT) && botAI->HasAggro(boss) && !botAI->IsTank(bot)) + bot->AddAura(SPELL_NO_THREAT, bot); + + if (botAI->IsMainTank(bot) && !bot->HasAura(SPELL_SPITEFULL_FURY) && boss->GetVictim() != bot) + bot->AddAura(SPELL_SPITEFULL_FURY, bot); + //-------CHEAT------- + } const GuidVector& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); for (const auto& npc : npcs) @@ -319,7 +304,7 @@ bool IccPutricideGrowingOozePuddleTrigger::IsActive() if (Unit* unit = botAI->GetUnit(npc)) { const uint32 entry = unit->GetEntry(); - if (entry == PUDDLE_IDS[0] || entry == PUDDLE_IDS[1]) + if (entry == NPC_GROWING_OOZE_PUDDLE) return true; } } @@ -345,6 +330,16 @@ bool IccPutricideGasCloudTrigger::IsActive() if (!boss) return false; + Unit* boss1 = AI_VALUE2(Unit*, "find target", "volatile ooze"); + + bool hasGaseousBloat = botAI->HasAura("Gaseous Bloat", bot); + + if (hasGaseousBloat && boss1) + return true; + + if (boss1) + return false; + return true; } @@ -380,6 +375,9 @@ bool IccPutricideMalleableGooTrigger::IsActive() if (!boss) return false; + if (botAI->IsTank(bot)) + return true; + Unit* boss1 = AI_VALUE2(Unit*, "find target", "volatile ooze"); if (boss1) return false; @@ -398,6 +396,9 @@ bool IccBpcKelesethTankTrigger::IsActive() if (!boss) return false; + if (bot->HasAura(SPELL_EXPERIENCED)) + bot->RemoveAura(SPELL_EXPERIENCED); + if (!botAI->IsAssistTank(bot)) return false; @@ -406,57 +407,25 @@ bool IccBpcKelesethTankTrigger::IsActive() if (aura->GetStackAmount() > 18) return false; - // First priority is to check for nucleuses that need to be picked up - GuidVector targets = AI_VALUE(GuidVector, "possible targets"); - for (auto i = targets.begin(); i != targets.end(); ++i) - { - Unit* unit = botAI->GetUnit(*i); - if (unit && unit->IsAlive() && unit->GetEntry() == 38369) // Dark Nucleus entry - { - if (!unit->GetVictim() || unit->GetVictim() != bot) - return false; // Don't tank Keleseth if there's a nucleus to grab - } - } - return true; } -bool IccBpcNucleusTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "prince keleseth"); - if (!boss) - return false; - - if (!botAI->IsAssistTank(bot)) - return false; - - Aura* aura = botAI->GetAura("Shadow Prison", bot, false, true); - if (aura) - if (aura->GetStackAmount() > 18) - return false; - - // Actively look for any nucleus that isn't targeting us - GuidVector targets = AI_VALUE(GuidVector, "possible targets"); - for (auto i = targets.begin(); i != targets.end(); ++i) - { - Unit* unit = botAI->GetUnit(*i); - if (unit && unit->IsAlive() && unit->GetEntry() == 38369) // Dark Nucleus entry - { - if (!unit->GetVictim() || unit->GetVictim() != bot) - return true; // Found a nucleus that needs to be picked up - } - } - - return false; -} - bool IccBpcMainTankTrigger::IsActive() { + if (!botAI->IsTank(bot)) + return false; + Unit* valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); Unit* taldaram = AI_VALUE2(Unit*, "find target", "prince taldaram"); Unit* keleseth = AI_VALUE2(Unit*, "find target", "prince keleseth"); - return valanar != nullptr || taldaram != nullptr || keleseth != nullptr; + if (!(valanar || taldaram || keleseth)) + return false; + + if (bot->HasAura(SPELL_EXPERIENCED)) + bot->RemoveAura(SPELL_EXPERIENCED); + + return true; } bool IccBpcEmpoweredVortexTrigger::IsActive() @@ -478,15 +447,7 @@ bool IccBpcEmpoweredVortexTrigger::IsActive() if (!auraValanar) return false; - // For ranged, spread whenever Valanar is empowered - //if (botAI->IsRanged(bot) && auraValanar) - //return true; - - // For melee, only spread during vortex cast - if (auraValanar && valanar->HasUnitState(UNIT_STATE_CASTING)) - return true; - - return false; + return true; } bool IccBpcKineticBombTrigger::IsActive() @@ -495,7 +456,10 @@ bool IccBpcKineticBombTrigger::IsActive() Unit* taldaram = AI_VALUE2(Unit*, "find target", "prince taldaram"); Unit* keleseth = AI_VALUE2(Unit*, "find target", "prince keleseth"); - if (!(valanar || taldaram || keleseth)) + if (!(valanar || taldaram || keleseth)) + return false; + + if (!botAI->IsRanged(bot) || botAI->IsHeal(bot)) return false; // Early exit condition - if Shadow Prison has too many stacks @@ -505,55 +469,62 @@ bool IccBpcKineticBombTrigger::IsActive() return false; } - // Check for kinetic bombs - const GuidVector& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + static const std::array bombEntries = {NPC_KINETIC_BOMB1, NPC_KINETIC_BOMB2, NPC_KINETIC_BOMB3, + NPC_KINETIC_BOMB4}; + const GuidVector bombs = AI_VALUE(GuidVector, "possible targets no los"); + bool bombFound = false; - for (const auto& npc : npcs) + for (const auto entry : bombEntries) { - Unit* unit = botAI->GetUnit(npc); - if (!unit || unit->GetName() != "Kinetic Bomb") - continue; - - // Check if bomb is within valid Z-axis range - if (unit->GetPositionZ() - bot->GetPositionZ() < 25.0f) + for (const auto& guid : bombs) { - bombFound = true; + if (Unit* unit = botAI->GetUnit(guid)) + { + if (unit->GetEntry() == entry) + { + // Check if bomb is within valid Z-axis range + if (unit->GetPositionZ() - bot->GetPositionZ() < 25.0f) + { + bombFound = true; + break; + } + } + } + } + if (bombFound) break; - } } - // If no bomb is found, no need to handle it - if (!bombFound) - return false; - - // Check if the bot should handle the bomb - if (bot->getClass() == CLASS_HUNTER) - return true; // Hunters always handle bombs - - // If not a hunter, check if there are hunters in the group - Group* group = bot->GetGroup(); - if (group) - { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (member && member->getClass() == CLASS_HUNTER && member->IsAlive()) - return false; // Let the hunter handle it - } - } - - // If no hunters are available, any ranged DPS should handle bombs - return botAI->IsRangedDps(bot); + return botAI->IsRangedDps(bot) && bombFound; } - //BQL -bool IccBqlTankPositionTrigger::IsActive() +bool IccBpcBallOfFlameTrigger::IsActive() +{ + Unit* valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); + Unit* taldaram = AI_VALUE2(Unit*, "find target", "prince taldaram"); + Unit* keleseth = AI_VALUE2(Unit*, "find target", "prince keleseth"); + + if (!(valanar || taldaram || keleseth)) + return false; + + Aura* auraTaldaram = botAI->GetAura("Invocation of Blood", taldaram); + if (!auraTaldaram) + return false; + + return true; +} + +//BQL +bool IccBqlGroupPositionTrigger::IsActive() { Unit* boss = AI_VALUE2(Unit*, "find target", "blood-queen lana'thel"); if (!boss) return false; + if (bot->HasAura(SPELL_EXPERIENCED)) + bot->RemoveAura(SPELL_EXPERIENCED); + return true; } @@ -577,18 +548,17 @@ bool IccBqlVampiricBiteTrigger::IsActive() return false; Aura* aura = botAI->GetAura("Frenzied Bloodthirst", bot); - // Only trigger when bot has Frenzied Bloodthirst if (!aura) return false; return true; } -//VDW +// Sister Svalna bool IccValkyreSpearTrigger::IsActive() { // Check if there's a spear nearby - if (Creature* spear = bot->FindNearestCreature(38248, 100.0f)) + if (Creature* spear = bot->FindNearestCreature(NPC_SPEAR, 100.0f)) return true; return false; @@ -598,50 +568,75 @@ bool IccSisterSvalnaTrigger::IsActive() { Unit* boss = AI_VALUE2(Unit*, "find target", "sister svalna"); if (!boss) - { return false; - } + + if (bot->GetExactDist2d(boss) > 30.0f) + return false; + + return true; +} + +// VDW +bool IccValithriaGroupTrigger::IsActive() +{ + Unit* boss = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); + if (!boss) + return false; + + if (bot->HasAura(SPELL_EXPERIENCED)) + bot->RemoveAura(SPELL_EXPERIENCED); + return true; } bool IccValithriaPortalTrigger::IsActive() { - - Unit* boss = bot->FindNearestCreature(36789, 100.0f); - if (!boss) + Unit* boss = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); + if (!boss) + return false; + + Aura* aura = botAI->GetAura("Twisted Nightmares", bot, false, true); + if (aura && aura->GetStackAmount() >= 25) return false; - //for gruop position for non healers - if(!botAI->IsHeal(bot) && (bot->GetDistance(ICC_VDW_GROUP_POSITION) > 35.0f)) - return true; - // Only healers should use portals - if (!botAI->IsHeal(bot) || bot->HasAura(70766)) + if (!botAI->IsHeal(bot) || bot->HasAura(SPELL_DREAM_STATE)) + return false; + + Creature* worm = bot->FindNearestCreature(NPC_ROT_WORM, 100.0f); + Creature* zombie = bot->FindNearestCreature(NPC_BLISTERING_ZOMBIE, 100.0f); + + if ((worm && worm->GetVictim() == bot) || (zombie && zombie->GetVictim() == bot)) return false; Group* group = bot->GetGroup(); if (!group) return false; - // Count healers and collect their GUIDs + // Collect healer GUIDs and check for druids std::vector healerGuids; + std::vector druidGuids; int healerCount = 0; for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) { Player* member = itr->GetSource(); - if (!member || !member->IsAlive()) + if (!member || !member->IsAlive() || botAI->IsRealPlayer()) continue; - if (botAI->IsHeal(member)) + if (botAI->IsHeal(member) && !botAI->IsRealPlayer()) { healerCount++; healerGuids.push_back(member->GetGUID()); + // Check if druid (class 11) + if (member->getClass() == CLASS_DRUID) + druidGuids.push_back(member->GetGUID()); } } // Sort GUIDs to ensure consistent ordering std::sort(healerGuids.begin(), healerGuids.end()); + std::sort(druidGuids.begin(), druidGuids.end()); // Find position of current bot's GUID in the sorted list auto botGuidPos = std::find(healerGuids.begin(), healerGuids.end(), bot->GetGUID()); @@ -650,24 +645,41 @@ bool IccValithriaPortalTrigger::IsActive() int healerIndex = std::distance(healerGuids.begin(), botGuidPos); - // Determine if this healer should focus on raid + // Find if this bot is a druid + bool isDruid = (bot->getClass() == CLASS_DRUID); + + // Determine raid healer assignment bool shouldHealRaid = false; - - if (healerCount > 3) + int druidCount = druidGuids.size(); + + if (druidCount > 0) { - // If more than 3 healers, 2 should heal raid - if (bot->getClass() == CLASS_DRUID) - shouldHealRaid = true; // Druids prioritize raid healing - else - shouldHealRaid = (healerIndex >= (healerCount - 2)); // Last 2 healers (by GUID) heal raid if no druid + // If we have druids, they should heal raid + if (isDruid) + { + // If there are more druids than raid healers needed, extra druids can heal boss + int raidHealersNeeded = healerCount > 3 ? 2 : 1; + int druidIndex = + std::distance(druidGuids.begin(), std::find(druidGuids.begin(), druidGuids.end(), bot->GetGUID())); + if (druidIndex < raidHealersNeeded) + shouldHealRaid = true; + else + shouldHealRaid = false; + } + else if (healerCount > 3 && druidCount == 1) + { + // If only 1 druid and need 2 raid healers, pick the last non-druid healer as well + if (healerIndex == (healerCount - 1) && !isDruid) + shouldHealRaid = true; + } } else { - // If 3 or fewer healers, 1 should heal raid - if (bot->getClass() == CLASS_DRUID) - shouldHealRaid = true; // Druids prioritize raid healing + // No druids, assign raid healers as before + if (healerCount > 3) + shouldHealRaid = (healerIndex >= (healerCount - 2)); else - shouldHealRaid = (healerIndex == (healerCount - 1)); // Last healer (by GUID) heals raid if no druid + shouldHealRaid = (healerIndex == (healerCount - 1)); } // Raid healers should not use portals @@ -675,45 +687,61 @@ bool IccValithriaPortalTrigger::IsActive() return false; // Find the nearest portal creature - Creature* portal = bot->FindNearestCreature(37945, 100.0f); // Only check within 10 yards - Creature* portal2 = bot->FindNearestCreature(38430, 100.0f); // Only check within 10 yards + Creature* portal1 = bot->FindNearestCreature(NPC_DREAM_PORTAL, 100.0f); + if (!portal1) + portal1 = bot->FindNearestCreature(NPC_DREAM_PORTAL_PRE_EFFECT, 100.0f); - return portal || portal2; + Creature* portal2 = bot->FindNearestCreature(NPC_NIGHTMARE_PORTAL, 100.0f); + if (!portal2) + portal2 = bot->FindNearestCreature(NPC_NIGHTMARE_PORTAL_PRE_EFFECT, 100.0f); + + return portal1 || portal2; } bool IccValithriaHealTrigger::IsActive() { - Unit* boss = bot->FindNearestCreature(36789, 100.0f); - if (!boss) + Unit* boss = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); + if (!boss) return false; - + // Only healers should use healing - if (!botAI->IsHeal(bot) || bot->HasAura(70766)) + if (!botAI->IsHeal(bot) || bot->HasAura(SPELL_DREAM_STATE) || bot->HealthBelowPct(50)) + return false; + + Creature* worm = bot->FindNearestCreature(NPC_ROT_WORM, 100.0f); + Creature* zombie = bot->FindNearestCreature(NPC_BLISTERING_ZOMBIE, 100.0f); + + if ((worm && worm->GetVictim() == bot) || (zombie && zombie->GetVictim() == bot)) return false; Group* group = bot->GetGroup(); if (!group) return false; - // Count healers and collect their GUIDs + // Collect healer GUIDs and check for druids std::vector healerGuids; + std::vector druidGuids; int healerCount = 0; for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) { Player* member = itr->GetSource(); - if (!member || !member->IsAlive()) + if (!member || !member->IsAlive() || botAI->IsRealPlayer()) continue; - if (botAI->IsHeal(member)) + if (botAI->IsHeal(member) && !botAI->IsRealPlayer()) { healerCount++; healerGuids.push_back(member->GetGUID()); + // Check if druid (class 11) + if (member->getClass() == CLASS_DRUID) + druidGuids.push_back(member->GetGUID()); } } // Sort GUIDs to ensure consistent ordering std::sort(healerGuids.begin(), healerGuids.end()); + std::sort(druidGuids.begin(), druidGuids.end()); // Find position of current bot's GUID in the sorted list auto botGuidPos = std::find(healerGuids.begin(), healerGuids.end(), bot->GetGUID()); @@ -722,37 +750,56 @@ bool IccValithriaHealTrigger::IsActive() int healerIndex = std::distance(healerGuids.begin(), botGuidPos); - // Determine if this healer should focus on raid + // Find if this bot is a druid + bool isDruid = (bot->getClass() == CLASS_DRUID); + + // Determine raid healer assignment bool shouldHealRaid = false; - - if (healerCount > 3) + int druidCount = druidGuids.size(); + + if (druidCount > 0) { - // If more than 3 healers, 2 should heal raid - if (bot->getClass() == CLASS_DRUID) - shouldHealRaid = true; // Druids prioritize raid healing - else - shouldHealRaid = (healerIndex >= (healerCount - 2)); // Last 2 healers (by GUID) heal raid if no druid + // If we have druids, they should heal raid + if (isDruid) + { + // If there are more druids than raid healers needed, extra druids can heal boss + int raidHealersNeeded = healerCount > 3 ? 2 : 1; + int druidIndex = + std::distance(druidGuids.begin(), std::find(druidGuids.begin(), druidGuids.end(), bot->GetGUID())); + if (druidIndex < raidHealersNeeded) + shouldHealRaid = true; + else + shouldHealRaid = false; // extra druids can heal boss + } + else if (healerCount > 3 && druidCount == 1) + { + // If only 1 druid and need 2 raid healers, pick the last non-druid healer as well + if (healerIndex == (healerCount - 1) && !isDruid) + shouldHealRaid = true; + } } else { - // If 3 or fewer healers, 1 should heal raid - if (bot->getClass() == CLASS_DRUID) - shouldHealRaid = true; // Druids prioritize raid healing + // No druids, assign raid healers as before + if (healerCount > 3) + shouldHealRaid = (healerIndex >= (healerCount - 2)); else - shouldHealRaid = (healerIndex == (healerCount - 1)); // Last healer (by GUID) heals raid if no druid + shouldHealRaid = (healerIndex == (healerCount - 1)); } // If assigned to raid healing, return false to not heal Valithria if (shouldHealRaid) return false; + if (bot->GetHealthPct() < 50.0f) + return false; + // For Valithria healers, check portal logic // If no portal is found within 100 yards, we should heal - if (!bot->FindNearestCreature(37945, 100.0f) && !bot->FindNearestCreature(38430, 100.0f)) + if (!bot->FindNearestCreature(NPC_DREAM_PORTAL, 100.0f) && !bot->FindNearestCreature(NPC_NIGHTMARE_PORTAL, 100.0f)) return true; - // If portal is nearby (10 yards), don't heal - if (bot->FindNearestCreature(37945, 20.0f) || bot->FindNearestCreature(38430, 10.0f)) + if (bot->FindNearestCreature(NPC_DREAM_PORTAL, 10.0f) || bot->FindNearestCreature(NPC_NIGHTMARE_PORTAL, 10.0f)) return false; // If portal is far but within 100 yards, heal while moving to it @@ -762,21 +809,43 @@ bool IccValithriaHealTrigger::IsActive() bool IccValithriaDreamCloudTrigger::IsActive() { // Only active if we're in dream state - if (!bot->HasAura(70766)) + if (!bot->HasAura(SPELL_DREAM_STATE) || bot->HealthBelowPct(50)) return false; // Find nearest cloud of either type - Creature* dreamCloud = bot->FindNearestCreature(37985, 100.0f); - Creature* nightmareCloud = bot->FindNearestCreature(38421, 100.0f); + Creature* dreamCloud = bot->FindNearestCreature(NPC_DREAM_CLOUD, 100.0f); + Creature* nightmareCloud = bot->FindNearestCreature(NPC_NIGHTMARE_CLOUD, 100.0f); return (dreamCloud || nightmareCloud); } //SINDRAGOSA -bool IccSindragosaTankPositionTrigger::IsActive() +bool IccSindragosaGroupPositionTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss || bot->HasAura(70126) /*|| bot->HasAura(69762)*/ || boss->HasUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY)) + Unit* boss = bot->FindNearestCreature(NPC_SINDRAGOSA, 200.0f); // sindra + if (!boss) + return false; + + Difficulty diff = bot->GetRaidDifficulty(); + + if (sPlayerbotAIConfig->EnableICCBuffs && diff && (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) + { + //-------CHEAT------- + if (!bot->HasAura(SPELL_EXPERIENCED)) + bot->AddAura(SPELL_EXPERIENCED, bot); + + if (!bot->HasAura(SPELL_AGEIS_OF_DALARAN)) + bot->AddAura(SPELL_AGEIS_OF_DALARAN, bot); + + if (!bot->HasAura(SPELL_NO_THREAT) && botAI->HasAggro(boss) && !botAI->IsTank(bot)) + bot->AddAura(SPELL_NO_THREAT, bot); + + if (botAI->IsMainTank(bot) && !bot->HasAura(SPELL_SPITEFULL_FURY) && boss->GetVictim() != bot) + bot->AddAura(SPELL_SPITEFULL_FURY, bot); + //-------CHEAT------- + } + + if (!boss || bot->HasAura(SPELL_FROST_BEACON) /*|| bot->HasAura(69762)*/ || boss->GetExactDist2d(ICC_SINDRAGOSA_FLYING_POSITION.GetPositionX(), ICC_SINDRAGOSA_FLYING_POSITION.GetPositionY()) < 50.0f) return false; return true; @@ -792,7 +861,7 @@ bool IccSindragosaFrostBeaconTrigger::IsActive() if (!group) return false; - if (boss->HasUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY) && !boss->HealthBelowPct(35)) + if (bot->HasAura(SPELL_FROST_BEACON)) return true; Group::MemberSlotList const& groupSlot = group->GetMemberSlots(); @@ -802,9 +871,10 @@ bool IccSindragosaFrostBeaconTrigger::IsActive() if (!member || !member->IsAlive()) continue; - if (member->HasAura(70126)) // If any player has Frost Beacon, keep trigger active + if (member->HasAura(SPELL_FROST_BEACON)) // If any player has Frost Beacon, keep trigger active return true; } + return false; } @@ -822,14 +892,16 @@ bool IccSindragosaBlisteringColdTrigger::IsActive() if (!group) return false; - float dist = bot->GetDistance(boss); + float dist = bot->GetExactDist2d(boss->GetPositionX(), boss->GetPositionY()); - if (dist >= 30.0f) + if (dist >= 33.0f) return false; - bool isCasting = boss->HasUnitState(UNIT_STATE_CASTING); - bool isBlisteringCold = boss->FindCurrentSpellBySpellId(70123) || boss->FindCurrentSpellBySpellId(71047) || - boss->FindCurrentSpellBySpellId(71048) || boss->FindCurrentSpellBySpellId(71049); + bool isCasting = boss && boss->HasUnitState(UNIT_STATE_CASTING); + bool isBlisteringCold = boss && (boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD1) || + boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD2) || + boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD3) || + boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD4)); return isCasting && isBlisteringCold; } @@ -837,19 +909,43 @@ bool IccSindragosaBlisteringColdTrigger::IsActive() bool IccSindragosaUnchainedMagicTrigger::IsActive() { Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss || (boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(70123))) + if (!boss) return false; - return bot->HasAura(69762); + Aura* aura = botAI->GetAura("unchained magic", bot, false, false); + if (!aura) + return false; + + bool isBlisteringCold = boss && (boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD1) || + boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD2) || + boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD3) || + boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD4)); + + if (boss && boss->HasUnitState(UNIT_STATE_CASTING) && isBlisteringCold) + return false; + + return true; } bool IccSindragosaChilledToTheBoneTrigger::IsActive() { Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss || (boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(70123))) + if (!boss) return false; - return bot->HasAura(70106); + Aura* aura = botAI->GetAura("Chilled to the Bone", bot, false, false); + if (!aura) + return false; + + bool isBlisteringCold = boss && (boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD1) || + boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD2) || + boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD3) || + boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD4)); + + if (boss && boss->HasUnitState(UNIT_STATE_CASTING) && isBlisteringCold) + return false; + + return true; } bool IccSindragosaMysticBuffetTrigger::IsActive() @@ -858,20 +954,17 @@ bool IccSindragosaMysticBuffetTrigger::IsActive() if (!boss) return false; - if (botAI->IsTank(bot) && boss->GetVictim() == bot) + if (boss->GetVictim() == bot) return false; Aura* aura = botAI->GetAura("mystic buffet", bot, false, true); if (!aura) return false; - if (bot->HasAura(70126)) // Frost Beacon + if (bot->HasAura(SPELL_FROST_BEACON)) return false; - if (aura->GetStackAmount() >= 2 && botAI->IsAssistTank(bot)) - return true; - - if (aura->GetStackAmount() >= 3) + if (aura->GetStackAmount() >= 1) return true; return false; @@ -895,7 +988,7 @@ bool IccSindragosaMainTankMysticBuffetTrigger::IsActive() return false; // Don't swap if we have frost beacon - if (bot->HasAura(70126)) // Frost Beacon + if (bot->HasAura(SPELL_FROST_BEACON)) // Frost Beacon return false; Unit* mt = AI_VALUE(Unit*, "main tank"); @@ -904,7 +997,7 @@ bool IccSindragosaMainTankMysticBuffetTrigger::IsActive() // Check main tank stacks Aura* mtAura = botAI->GetAura("mystic buffet", mt, false, true); - if (!mtAura || mtAura->GetStackAmount() < 9) + if (!mtAura || mtAura->GetStackAmount() < 6) return false; // Check our own stacks - don't taunt if we have too many @@ -926,12 +1019,15 @@ bool IccSindragosaTankSwapPositionTrigger::IsActive() if (!boss) return false; + if (boss && boss->GetVictim() == bot) + return false; + // Only for assist tank if (!botAI->IsAssistTankOfIndex(bot, 0)) return false; // Don't move to position if we have frost beacon - if (bot->HasAura(70126)) // Frost Beacon + if (bot->HasAura(SPELL_FROST_BEACON)) return false; // Check our own stacks - don't try to tank if we have too many @@ -949,7 +1045,7 @@ bool IccSindragosaTankSwapPositionTrigger::IsActive() return false; uint32 mtStacks = mtAura->GetStackAmount(); - if (mtStacks < 9) // Only start moving when MT has 5+ stacks + if (mtStacks < 6) // Only start moving when MT has 5+ stacks return false; // Check if we're already in position @@ -962,24 +1058,15 @@ bool IccSindragosaTankSwapPositionTrigger::IsActive() bool IccSindragosaFrostBombTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); + Unit* boss = bot->FindNearestCreature(NPC_SINDRAGOSA, 200.0f); if (!boss) return false; - if (!bot->IsAlive() || bot->HasAura(70157)) // Skip if dead or in Ice Tomb + if (!bot->IsAlive() || bot->HasAura(SPELL_ICE_TOMB)) // Skip if dead or in Ice Tomb return false; - // Simply check if frost bomb marker exists - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (!unit || !unit->IsAlive()) - continue; - - if (unit->HasAura(70022)) // Frost bomb visual - return true; - } + if (boss->GetExactDist2d(ICC_SINDRAGOSA_FLYING_POSITION.GetPositionX(), ICC_SINDRAGOSA_FLYING_POSITION.GetPositionY()) < 50.0f && !boss->HealthBelowPct(25) && !boss->HealthAbovePct(99)) + return true; return false; } @@ -992,7 +1079,14 @@ bool IccLichKingShadowTrapTrigger::IsActive() if (!boss) return false; - if (boss->HealthBelowPct(70)) + bool hasPlague = botAI->HasAura("Necrotic Plague", bot); + if (hasPlague) + return false; + + if (!botAI->IsMainTank(bot)) + return false; + + if (boss->HealthBelowPct(65)) return false; // search for all nearby traps @@ -1006,7 +1100,7 @@ bool IccLichKingShadowTrapTrigger::IsActive() if (!unit || !unit->IsAlive()) continue; - if (unit->GetEntry() == 39137) // shadow trap + if (unit->GetEntry() == NPC_SHADOW_TRAP && bot->GetExactDist2d(unit) < 11.0f) // shadow trap return true; } @@ -1015,10 +1109,6 @@ bool IccLichKingShadowTrapTrigger::IsActive() bool IccLichKingNecroticPlagueTrigger::IsActive() { - if (!bot || !bot->IsAlive()) - return false; - - // Check for plague by name instead of ID bool hasPlague = botAI->HasAura("Necrotic Plague", bot); return hasPlague; @@ -1031,11 +1121,11 @@ bool IccLichKingWinterTrigger::IsActive() return false; // Check for either Remorseless Winter - bool hasWinterAura = boss->HasAura(72259) || boss->HasAura(74273) || boss->HasAura(74274) || boss->HasAura(74275); - bool hasWinter2Aura = boss->HasAura(68981) || boss->HasAura(74270) || boss->HasAura(74271) || boss->HasAura(74272); + bool hasWinterAura = boss->HasAura(SPELL_REMORSELESS_WINTER1) || boss->HasAura(SPELL_REMORSELESS_WINTER2) || boss->HasAura(SPELL_REMORSELESS_WINTER3) || boss->HasAura(SPELL_REMORSELESS_WINTER4); + bool hasWinter2Aura = boss->HasAura(SPELL_REMORSELESS_WINTER5) || boss->HasAura(SPELL_REMORSELESS_WINTER6) || boss->HasAura(SPELL_REMORSELESS_WINTER7) || boss->HasAura(SPELL_REMORSELESS_WINTER8); bool isCasting = boss->HasUnitState(UNIT_STATE_CASTING); - bool isWinter = boss->FindCurrentSpellBySpellId(77259) || boss->FindCurrentSpellBySpellId(74273) || boss->FindCurrentSpellBySpellId(68981) || boss->FindCurrentSpellBySpellId(74270) || - boss->FindCurrentSpellBySpellId(74274) || boss->FindCurrentSpellBySpellId(74275) || boss->FindCurrentSpellBySpellId(74271) || boss->FindCurrentSpellBySpellId(74272); + bool isWinter = boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER1) || boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER2) || boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER5) || boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER6) || + boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER3) || boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER4) || boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER7) || boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER8); if (hasWinterAura || hasWinter2Aura) return true; @@ -1050,6 +1140,19 @@ bool IccLichKingAddsTrigger::IsActive() { Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); + bool hasPlague = botAI->HasAura("Necrotic Plague", bot); + if (hasPlague) + return false; + + Unit* terenasMenethilHC = bot->FindNearestCreature(NPC_TERENAS_MENETHIL_HC, 55.0f); + Unit* terenasMenethil = bot->FindNearestCreature(NPC_TERENAS_MENETHIL, 55.0f); + + if (terenasMenethilHC) + return true; + + if (terenasMenethil) + return true; + if (!boss) return false; diff --git a/src/strategy/raids/icecrown/RaidIccTriggers.h b/src/strategy/raids/icecrown/RaidIccTriggers.h index 465ebc57..35761aa5 100644 --- a/src/strategy/raids/icecrown/RaidIccTriggers.h +++ b/src/strategy/raids/icecrown/RaidIccTriggers.h @@ -5,18 +5,223 @@ #include "Playerbots.h" #include "Trigger.h" -//Lord Marrowgar -class IccLmTankPositionTrigger : public Trigger +enum CreatureIdsICC { -public: - IccLmTankPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc lm tank position") {} - bool IsActive() override; + + // Lord Marrowgar + NPC_SPIKE1 = 36619, + NPC_SPIKE2 = 38711, + NPC_SPIKE3 = 38712, + + // Lady Deathwhisper + NPC_SHADE = 38222, + + // Gunship Battle + NPC_KOR_KRON_BATTLE_MAGE = 37117, + NPC_KOR_KRON_AXETHROWER = 36968, + NPC_KOR_KRON_ROCKETEER = 36982, + NPC_SKYBREAKER_SORCERER = 37116, + NPC_SKYBREAKER_RIFLEMAN = 36969, + NPC_SKYBREAKER_MORTAR_SOLDIER = 36978, + NPC_IGB_HIGH_OVERLORD_SAURFANG = 36939, + NPC_IGB_MURADIN_BRONZEBEARD = 36948, + NPC_CANNONA = 36838, + NPC_CANNONH = 36839, + NPC_MURADIN_BRONZEBEARD = 36948, + NPC_HIGH_OVERLORD_SAURFANG = 36939, + + // Deathbringer Saurfang + NPC_BLOOD_BEAST1 = 38508, + NPC_BLOOD_BEAST2 = 38596, + NPC_BLOOD_BEAST3 = 38597, + NPC_BLOOD_BEAST4 = 38598, + + // Rotface + NPC_PUDDLE = 37013, + NPC_BIG_OOZE = 36899, + + // Putricide + NPC_MALLEABLE_OOZE_STALKER = 38556, + NPC_GROWING_OOZE_PUDDLE = 37690, + NPC_CHOKING_GAS_BOMB = 38159, + + // Blood Prince Council + NPC_DARK_NUCLEUS = 38369, + NPC_PRINCE_KELESETH = 37972, + NPC_PRINCE_TALDARAM = 37973, + NPC_PRINCE_VALANAR = 37970, + NPC_KINETIC_BOMB1 = 38454, + NPC_KINETIC_BOMB2 = 38775, + NPC_KINETIC_BOMB3 = 38776, + NPC_KINETIC_BOMB4 = 38777, + NPC_BALL_OF_FLAME = 38332, + NPC_BALL_OF_INFERNO_FLAME = 38451, + + // Blood Queen Lana'thel + NPC_SWARMING_SHADOWS = 38163, + + // Sister Svalna + NPC_SPEAR = 38248, + ITEM_SPEAR = 50307, + + // Valithria Dreamwalker + NPC_VALITHRIA_DREAMWALKER = 36789, + NPC_DREAM_PORTAL = 37945, + NPC_NIGHTMARE_PORTAL = 38430, + NPC_DREAM_CLOUD = 37985, + NPC_NIGHTMARE_CLOUD = 38421, + NPC_RISEN_ARCHMAGE = 37868, + NPC_BLAZING_SKELETON = 36791, + NPC_SUPPRESSER = 37863, + NPC_BLISTERING_ZOMBIE = 37934, + NPC_GLUTTONOUS_ABOMINATION = 37886, + NPC_ROT_WORM = 37907, + NPC_COLUMN_OF_FROST = 37918, + NPC_MANA_VOID = 38068, + NPC_DREAM_PORTAL_PRE_EFFECT = 38186, + NPC_NIGHTMARE_PORTAL_PRE_EFFECT = 38429, + + // Sindragosa + NPC_SINDRAGOSA = 36853, + NPC_TOMB1 = 36980, + NPC_TOMB2 = 38320, + NPC_TOMB3 = 38321, + NPC_TOMB4 = 38322, + + // Lich King + NPC_THE_LICH_KING = 36597, + NPC_TERENAS_MENETHIL = 36823, + NPC_TERENAS_MENETHIL_HC = 39217, + NPC_SPIRIT_BOMB = 39189, + NPC_WICKED_SPIRIT1 = 39190, + NPC_WICKED_SPIRIT2 = 39287, + NPC_WICKED_SPIRIT3 = 39288, + NPC_WICKED_SPIRIT4 = 39289, + NPC_SHADOW_TRAP = 39137, + NPC_SHAMBLING_HORROR1 = 37698, + NPC_SHAMBLING_HORROR2 = 39299, + NPC_SHAMBLING_HORROR3 = 39300, + NPC_SHAMBLING_HORROR4 = 39301, + NPC_ICE_SPHERE1 = 36633, + NPC_ICE_SPHERE2 = 39305, + NPC_ICE_SPHERE3 = 39306, + NPC_ICE_SPHERE4 = 39307, + NPC_RAGING_SPIRIT1 = 36701, + NPC_RAGING_SPIRIT2 = 39302, + NPC_RAGING_SPIRIT3 = 39303, + NPC_RAGING_SPIRIT4 = 39304, + NPC_DRUDGE_GHOUL1 = 37695, + NPC_DRUDGE_GHOUL2 = 39309, + NPC_DRUDGE_GHOUL3 = 39310, + NPC_DRUDGE_GHOUL4 = 39311, + NPC_VALKYR_SHADOWGUARD1 = 36609, + NPC_VALKYR_SHADOWGUARD2 = 39120, + NPC_VALKYR_SHADOWGUARD3 = 39121, + NPC_VALKYR_SHADOWGUARD4 = 39122, + NPC_VILE_SPIRIT1 = 37799, + NPC_VILE_SPIRIT2 = 39284, + NPC_VILE_SPIRIT3 = 39285, + NPC_VILE_SPIRIT4 = 39286, + }; -class IccSpikeNearTrigger : public Trigger +enum SpellIdsICC +{ + // ICC cheat spells + SPELL_EMPOWERED_BLOOD = 70304, //70304 -->50%, 70227 /*100% more dmg, 100% more att speed*/ + SPELL_EXPERIENCED = 71188, //dmg 30% 20% att speed + SPELL_NO_THREAT = 70115, //reduce threat + SPELL_SPITEFULL_FURY = 36886, //500% more threat + SPELL_NITRO_BOOSTS = 54861, //Speed + SPELL_PAIN_SUPPRESION = 69910, //40% dmg reduction + SPELL_AGEIS_OF_DALARAN = 71638, //268 all ress + SPELL_CYCLONE = 33786, + SPELL_HAMMER_OF_JUSTICE = 10308, //stun + + // Lady Deathwhisper + SPELL_DARK_RECKONING = 69483, + + // Gunship Battle + SPELL_DEATH_PLAGUE = 72865, + SPELL_BELOW_ZERO = 69705, + + // Festergut + SPELL_GAS_SPORE = 69279, + + // Rotface + SPELL_SLIME_SPRAY = 69508, + SPELL_OOZE_FLOOD = 71215, + SPELL_UNSTABLE_OOZE_EXPLOSION = 69839, + SPELL_OOZE_FLOOD_VISUAL = 69785, + + // Putricide + SPELL_MALLEABLE_GOO = 70852, + SPELL_GROW_AURA = 70347, + + // Blood Prince Council + SPELL_EMPOWERED_SHOCK_VORTEX1 = 72039, + SPELL_EMPOWERED_SHOCK_VORTEX2 = 73037, + SPELL_EMPOWERED_SHOCK_VORTEX3 = 73038, + SPELL_EMPOWERED_SHOCK_VORTEX4 = 73039, + + // Blood Queen Lana'thel + SPELL_PACT_OF_THE_DARKFALLEN = 71340, + + // Sister Svalna + SPELL_AETHER_SHIELD = 71463, + + // Valithria Dreamwalker + SPELL_DREAM_STATE = 70766, + SPELL_EMERALD_VIGOR = 70873, + + // Sindragosa + SPELL_FROST_BEACON = 70126, + SPELL_ICE_TOMB = 70157, + SPELL_FROST_BOMB_VISUAL = 70022, + SPELL_BLISTERING_COLD1 = 70123, + SPELL_BLISTERING_COLD2 = 71047, + SPELL_BLISTERING_COLD3 = 71048, + SPELL_BLISTERING_COLD4 = 71049, + + // The Lich King + SPELL_HARVEST_SOUL_VALKYR = 68985, + SPELL_QUAKE = 72262, + SPELL_REMORSELESS_WINTER1 = 72259, + SPELL_REMORSELESS_WINTER2 = 74273, + SPELL_REMORSELESS_WINTER3 = 74274, + SPELL_REMORSELESS_WINTER4 = 74275, + SPELL_REMORSELESS_WINTER5 = 68981, + SPELL_REMORSELESS_WINTER6 = 74270, + SPELL_REMORSELESS_WINTER7 = 74271, + SPELL_REMORSELESS_WINTER8 = 74272, +}; + +const uint32 DEFILE_AURAS[] = {72756, 74162, 74163, 74164}; +const uint32 DEFILE_CAST_ID = 72762; +const uint32 DEFILE_NPC_ID = 38757; +const size_t DEFILE_AURA_COUNT = 4; + +// All fanatics and adherents entry ids Lady Deathwhisper +static const std::array addEntriesLady = { + 37949, 38394, 38625, 38626, 38010, 38397, 39000, 39001, + 38136, 38396, 38632, 38633, 37890, 38393, 38628, 38629, + 38135, 38395, 38634, 38009, 38398, 38630, 38631}; + +const std::vector spellEntriesFlood = { + 69782, 69783, 69796, 69797, 69798, + 69799, 69801, 69802, 69795}; + +const std::vector availableTargetsGS = { + NPC_KOR_KRON_AXETHROWER, NPC_KOR_KRON_ROCKETEER, NPC_KOR_KRON_BATTLE_MAGE, NPC_IGB_HIGH_OVERLORD_SAURFANG, + NPC_SKYBREAKER_RIFLEMAN, NPC_SKYBREAKER_MORTAR_SOLDIER, NPC_SKYBREAKER_SORCERER, NPC_IGB_MURADIN_BRONZEBEARD}; + +static std::vector sporeOrder; + +//Lord Marrowgar +class IccLmTrigger : public Trigger { public: - IccSpikeNearTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc spike near") {} + IccLmTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc lm") {} bool IsActive() override; }; @@ -28,24 +233,10 @@ public: bool IsActive() override; }; -class IccRangedPositionLadyDeathwhisperTrigger : public Trigger +class IccLadyDeathwhisperTrigger : public Trigger { public: - IccRangedPositionLadyDeathwhisperTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc ranged position lady deathwhisper") {} - bool IsActive() override; -}; - -class IccAddsLadyDeathwhisperTrigger : public Trigger -{ -public: - IccAddsLadyDeathwhisperTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc adds lady deathwhisper") {} - bool IsActive() override; -}; - -class IccShadeLadyDeathwhisperTrigger : public Trigger -{ -public: - IccShadeLadyDeathwhisperTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc shade lady deathwhisper") {} + IccLadyDeathwhisperTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc lady deathwhisper") {} bool IsActive() override; }; @@ -86,10 +277,10 @@ public: }; //DBS -class IccDbsTankPositionTrigger : public Trigger +class IccDbsTrigger : public Trigger { public: - IccDbsTankPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc dbs tank position") {} + IccDbsTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc dbs") {} bool IsActive() override; }; @@ -100,13 +291,6 @@ public: bool IsActive() override; }; -class IccAddsDbsTrigger : public Trigger -{ -public: - IccAddsDbsTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc adds dbs") {} - bool IsActive() override; -}; - //DOGS class IccStinkyPreciousMainTankMortalWoundTrigger : public Trigger { @@ -116,10 +300,10 @@ public: }; //FESTERGUT -class IccFestergutTankPositionTrigger : public Trigger +class IccFestergutGroupPositionTrigger : public Trigger { public: - IccFestergutTankPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc festergut tank position") {} + IccFestergutGroupPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc festergut group position") {} bool IsActive() override; }; @@ -205,13 +389,6 @@ public: bool IsActive() override; }; -class IccBpcNucleusTrigger : public Trigger -{ -public: - IccBpcNucleusTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc bpc nucleus") {} - bool IsActive() override; -}; - class IccBpcMainTankTrigger : public Trigger { public: @@ -233,11 +410,18 @@ public: bool IsActive() override; }; -//Bql -class IccBqlTankPositionTrigger : public Trigger +class IccBpcBallOfFlameTrigger : public Trigger { public: - IccBqlTankPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc bql tank position") {} + IccBpcBallOfFlameTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc bpc ball of flame") {} + bool IsActive() override; +}; + +//Bql +class IccBqlGroupPositionTrigger : public Trigger +{ +public: + IccBqlGroupPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc bql tank position") {} bool IsActive() override; }; @@ -255,7 +439,7 @@ public: bool IsActive() override; }; -//VDW +// Sister Svalna class IccValkyreSpearTrigger : public Trigger { public: @@ -270,6 +454,16 @@ public: bool IsActive() override; }; + +// Valithria Dreamwalker + +class IccValithriaGroupTrigger : public Trigger +{ +public: + IccValithriaGroupTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc valithria group") {} + bool IsActive() override; +}; + class IccValithriaPortalTrigger : public Trigger { public: @@ -293,10 +487,10 @@ public: //SINDRAGOSA -class IccSindragosaTankPositionTrigger : public Trigger +class IccSindragosaGroupPositionTrigger : public Trigger { public: - IccSindragosaTankPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc sindragosa tank position") {} + IccSindragosaGroupPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc sindragosa group position") {} bool IsActive() override; };