--- ============================ HEADER ============================ --- ======= LOCALIZE ======= -- Addon local addonName, addonTable = ... -- HeroDBC local DBC = HeroDBC.DBC -- HeroLib local HL = HeroLib local Utils = HL.Utils local Cache = HeroCache local Unit = HL.Unit local Player = Unit.Player local Pet = Unit.Pet local Target = Unit.Target local Spell = HL.Spell local Item = HL.Item -- HeroRotation local HR = HeroRotation local AoEON = HR.AoEON local CDsON = HR.CDsON local Cast = HR.Cast -- Num/Bool Helper Functions local num = HR.Commons.Everyone.num local bool = HR.Commons.Everyone.bool -- Lua local max = math.max --- ============================ CONTENT ============================ --- ======= APL LOCALS ======= -- Commons local Everyone = HR.Commons.Everyone -- GUI Settings local Settings = { General = HR.GUISettings.General, Commons = HR.GUISettings.APL.Warlock.Commons, Destruction = HR.GUISettings.APL.Warlock.Destruction } -- Spells local S = Spell.Warlock.Destruction -- Items local I = Item.Warlock.Destruction local OnUseExcludes = { I.ConjuredChillglobe:ID(), I.DesperateInvokersCodex:ID(), I.EruptingSpearFragment:ID(), I.IcebloodDeathsnare:ID(), } -- Enemies local Enemies40y, EnemiesCount8ySplash -- Rotation Variables local GCDMax = Player:GCD() local VarPoolSoulShards = false local VarCleaveAPL = false local VarHavocActive = false local VarHavocRemains = 0 local VarHavocImmoTime = 0 local BossFightRemains = 11111 local FightRemains = 11111 HL:RegisterForEvent(function() GCDMax = Player:GCD() VarPoolSoulShards = false VarCleaveAPL = false VarHavocActive = false VarHavocRemains = 0 VarHavocImmoTime = 0 BossFightRemains = 11111 FightRemains = 11111 end, "PLAYER_REGEN_ENABLED") S.SummonInfernal:RegisterInFlight() S.ChaosBolt:RegisterInFlight() S.Incinerate:RegisterInFlight() local function UnitWithHavoc(enemies) for k in pairs(enemies) do local CycleUnit = enemies[k] if CycleUnit:DebuffUp(S.Havoc) then return true, CycleUnit:DebuffRemains(S.HavocDebuff), CycleUnit:DebuffRemains(S.ImmolateDebuff) end end return false, 0, 0 end local function InfernalTime() return HL.GuardiansTable.InfernalDuration or (S.SummonInfernal:InFlight() and 30) or 0 end local function BlasphemyTime() return HL.GuardiansTable.BlasphemyDuration or 0 end local function ChannelDemonfireExecuteTime() return 3 * Player:SpellHaste() end -- CastTargetIf/CastCycle functions local function EvaluateTargetIfFilterHavoc(TargetUnit) -- target_if=min:((-target.time_to_die)8 -- if=(!cooldown.summon_infernal.up|!talent.summon_infernal|(talent.inferno&active_enemies>4))&target.time_to_die>8 -- Note: For both lines, all but time_to_die is handled before CastTargetIf return (TargetUnit:TimeToDie() > 8) end local function EvaluateTargetIfFilterImmolate(TargetUnit) -- target_if=min:dot.immolate.remains+99*debuff.havoc.remains return (TargetUnit:DebuffRemains(S.ImmolateDebuff) + 99 * TargetUnit:DebuffRemains(S.HavocDebuff)) end local function EvaluateTargetIfImmolateAoE(TargetUnit) -- if=dot.immolate.refreshable&(!talent.cataclysm.enabled|cooldown.cataclysm.remains>dot.immolate.remains)&(!talent.raging_demonfire|cooldown.channel_demonfire.remains>remains|time<5)&active_dot.immolate<=4&target.time_to_die>18 -- Note: active_dot.immolate handled before CastCycle return (TargetUnit:DebuffRefreshable(S.ImmolateDebuff) and ((not S.Cataclysm:IsAvailable()) or S.Cataclysm:CooldownRemains() > TargetUnit:DebuffRemains(S.ImmolateDebuff)) and (not S.RagingDemonfire:IsAvailable() or S.ChannelDemonfire:CooldownRemains() > TargetUnit:DebuffRemains(S.ImmolateDebuff) or HL.CombatTime() < 5) and TargetUnit:TimeToDie() > 18) end local function EvaluateTargetIfImmolateAoE2(TargetUnit) -- if=((dot.immolate.refreshable&(!talent.cataclysm.enabled|cooldown.cataclysm.remains>dot.immolate.remains))|active_enemies>active_dot.immolate)&target.time_to_die>10&!havoc_active return (((TargetUnit:DebuffRefreshable(S.ImmolateDebuff) and ((not S.Cataclysm:IsAvailable()) or S.Cataclysm:CooldownRemains() > TargetUnit:DebuffRemains(S.ImmolateDebuff))) or EnemiesCount8ySplash > S.ImmolateDebuff:AuraActiveCount()) and TargetUnit:TimeToDie() > 10 and not VarHavocActive) end local function EvaluateTargetIfImmolateAoE3(TargetUnit) -- if=((dot.immolate.refreshable&variable.havoc_immo_time<5.4)|(dot.immolate.remains<2&dot.immolate.remainsdot.immolate.remains)&target.time_to_die>11 return (((TargetUnit:DebuffRefreshable(S.ImmolateDebuff) and VarHavocImmoTime < 5.4) or (TargetUnit:DebuffRemains(S.ImmolateDebuff) < 2 and TargetUnit:DebuffRemains(S.ImmolateDebuff) < VarHavocRemains) or TargetUnit:DebuffDown(S.ImmolateDebuff) or bool(num(VarHavocImmoTime < 2) * num(VarHavocActive))) and ((not S.Cataclysm:IsAvailable()) or S.Cataclysm:CooldownRemains() > TargetUnit:DebuffRemains(S.ImmolateDebuff)) and TargetUnit:TimeToDie() > 11) end local function EvaluateTargetIfImmolateCleave(TargetUnit) -- if=(dot.immolate.refreshable&(dot.immolate.remainsremains)&(!talent.soul_fire|cooldown.soul_fire.remains+(!talent.mayhem*action.soul_fire.cast_time)>dot.immolate.remains)&target.time_to_die>15 return ((TargetUnit:DebuffRefreshable(S.ImmolateDebuff) and (TargetUnit:DebuffRemains(S.ImmolateDebuff) < S.Havoc:CooldownRemains() or TargetUnit:DebuffDown(S.ImmolateDebuff))) and ((not S.Cataclysm:IsAvailable()) or S.Cataclysm:CooldownRemains() > TargetUnit:DebuffRemains(S.ImmolateDebuff)) and ((not S.SoulFire:IsAvailable()) or S.SoulFire:CooldownRemains() + (num(not S.Mayhem:IsAvailable()) * S.SoulFire:CastTime()) > TargetUnit:DebuffRemains(S.ImmolateDebuff)) and TargetUnit:TimeToDie() > 15) end local function EvaluateTargetIfImmolateHavoc(TargetUnit) -- if=(((dot.immolate.refreshable&variable.havoc_immo_time<5.4)&target.time_to_die>5)|((dot.immolate.remains<2&dot.immolate.remains11)&soul_shard<4.5 -- Note: Soul Shard check handled before CastTargetIf call. return (((TargetUnit:DebuffRefreshable(S.ImmolateDebuff) and VarHavocImmoTime < 5.4) and TargetUnit:TimeToDie() > 5) or ((TargetUnit:DebuffRemains(S.ImmolateDebuff) < 2 and TargetUnit:DebuffRemains(S.ImmolateDebuff) < VarHavocRemains) or TargetUnit:DebuffDown(S.ImmolateDebuff) or VarHavocImmoTime < 2) and TargetUnit:TimeToDie() > 11) end local function Precombat() -- flask -- food -- augmentation -- summon_pet -- Moved to APL() -- variable,name=cleave_apl,default=0,op=reset VarCleaveAPL = false -- grimoire_of_sacrifice,if=talent.grimoire_of_sacrifice.enabled if S.GrimoireofSacrifice:IsReady() then if Cast(S.GrimoireofSacrifice, Settings.Destruction.GCDasOffGCD.GrimoireOfSacrifice) then return "grimoire_of_sacrifice precombat 2"; end end -- snapshot_stats -- soul_fire if S.SoulFire:IsReady() and (not Player:IsCasting(S.SoulFire)) then if Cast(S.SoulFire, nil, nil, not Target:IsSpellInRange(S.SoulFire)) then return "soul_fire precombat 4"; end end -- cataclysm,if=raid_event.adds.in>15 if S.Cataclysm:IsCastable() then if Cast(S.Cataclysm, Settings.Destruction.GCDasOffGCD.Cataclysm, nil, not Target:IsInRange(40)) then return "cataclysm precombat 6"; end end -- incinerate if S.Incinerate:IsCastable() and (not Player:IsCasting(S.Incinerate)) then if Cast(S.Incinerate, nil, nil, not Target:IsSpellInRange(S.Incinerate)) then return "incinerate precombat 8"; end end end local function Items() -- use_items,if=pet.infernal.active|!talent.summon_infernal|time_to_die<21 if (InfernalTime() > 0 or (not S.SummonInfernal:IsAvailable()) or FightRemains < 21) then local ItemToUse, ItemSlot, ItemRange = Player:GetUseableItems(OnUseExcludes) if ItemToUse then local DisplayStyle = Settings.Commons.DisplayStyle.Trinkets if ItemSlot ~= 13 and ItemSlot ~= 14 then DisplayStyle = Settings.Commons.DisplayStyle.Items end if ((ItemSlot == 13 or ItemSlot == 14) and Settings.Commons.Enabled.Trinkets) or (ItemSlot ~= 13 and ItemSlot ~= 14 and Settings.Commons.Enabled.Items) then if Cast(ItemToUse, nil, DisplayStyle, not Target:IsInRange(ItemRange)) then return "Generic use_items for " .. ItemToUse:Name(); end end end end if Settings.Commons.Enabled.Trinkets then --TODO manage trinkets --use_item,slot=trinket1,if=pet.infernal.active|!talent.summon_infernal|fight_remains<21|trinket.1.cooldown.durationtrinket.1.cooldown.duration)|fight_remains0&trinket.2.cooldown.remainstrinket.2.cooldown.duration)|fight_remains0&trinket.1.cooldown.remainstrinket.erupting_spear_fragment.cooldown.duration)|fight_remains 90) or FightRemains < S.SummonInfernal:CooldownRemains() or 90 < S.SummonInfernal:CooldownRemains() + 5) then if Cast(I.EruptingSpearFragment, nil, Settings.Commons.DisplayStyle.Trinkets, not Target:IsInRange(40)) then return "erupting_spear_fragment items 2"; end end --use_item,name=desperate_invokers_codex if I.DesperateInvokersCodex:IsEquippedAndReady() then if Cast(I.DesperateInvokersCodex, nil, Settings.Commons.DisplayStyle.Trinkets, not Target:IsInRange(45)) then return "desperate_invokers_codex items 4"; end end --use_item,name=iceblood_deathsnare if I.IcebloodDeathsnare:IsEquippedAndReady() then if Cast(I.IcebloodDeathsnare, nil, Settings.Commons.DisplayStyle.Trinkets, not Target:IsInRange(45)) then return "iceblood_deathsnare items 6"; end end --use_item,name=conjured_chillglobe if I.ConjuredChillglobe:IsEquippedAndReady() then if Cast(I.ConjuredChillglobe, nil, Settings.Commons.DisplayStyle.Trinkets) then return "conjured_chillglobe items 8"; end end end end local function oGCD() -- potion,if=pet.infernal.active|!talent.summon_infernal if Settings.Commons.Enabled.Potions and (InfernalTime() > 0 or not S.SummonInfernal:IsAvailable()) then local PotionSelected = Everyone.PotionSelected() if PotionSelected and PotionSelected:IsReady() then if Cast(PotionSelected, nil, Settings.Commons.DisplayStyle.Potions) then return "potion cds 2"; end end end -- invoke_external_buff,name=power_infusion,if=pet.infernal.active|!talent.summon_infernal|(fight_remainscooldown.invoke_power_infusion_0.duration)|fight_remainscooldown.berserking.duration))|fight_remains 0 or (not S.SummonInfernal:IsAvailable()) or (FightRemains < (S.SummonInfernal:CooldownRemains() + 12) and (FightRemains > 12)) or FightRemains < S.SummonInfernal:CooldownRemains()) then if Cast(S.Berserking, Settings.Commons.OffGCDasOffGCD.Racials) then return "berserking cds 4"; end end -- blood_fury,if=pet.infernal.active|!talent.summon_infernal|(fight_remainscooldown.blood_fury.duration)|fight_remains 0 or (not S.SummonInfernal:IsAvailable()) or (FightRemains < (S.SummonInfernal:CooldownRemains() + 10 + 15) and (FightRemains > 15)) or FightRemains < S.SummonInfernal:CooldownRemains()) then if Cast(S.BloodFury, Settings.Commons.OffGCDasOffGCD.Racials) then return "blood_fury cds 6"; end end -- fireblood,if=pet.infernal.active|!talent.summon_infernal|(fight_remainscooldown.fireblood.duration)|fight_remains 0 or (not S.SummonInfernal:IsAvailable()) or (FightRemains < (S.SummonInfernal:CooldownRemains() + 10 + 8) and (FightRemains > 8)) or FightRemains < S.SummonInfernal:CooldownRemains()) then if Cast(S.Fireblood, Settings.Commons.OffGCDasOffGCD.Racials) then return "fireblood cds 8"; end end -- ancestral_call,if=variable.infernal_active|!talent.summon_infernal|(fight_remains<(cooldown.summon_infernal.remains_expected+cooldown.berserking.duration)&(fight_remains>cooldown.berserking.duration))|fight_remains 0 or (not S.SummonInfernal:IsAvailable()) or (FightRemains < (S.SummonInfernal:CooldownRemains() + 15) and (FightRemains > 15)) or FightRemains < S.SummonInfernal:CooldownRemains()) then if Cast(S.AncestralCall, Settings.Commons.OffGCDasOffGCD.Racials) then return "ancestral_call ogcd 10"; end end end local function Havoc() -- conflagrate,if=talent.backdraft&buff.backdraft.down&soul_shard>=1&soul_shard<=4 if S.Conflagrate:IsCastable() and (S.Backdraft:IsAvailable() and Player:BuffDown(S.BackdraftBuff) and Player:SoulShardsP() >= 1 and Player:SoulShardsP() <= 4) then if Cast(S.Conflagrate, nil, nil, not Target:IsSpellInRange(S.Conflagrate)) then return "conflagrate havoc 2"; end end -- soul_fire,if=cast_time5)|((dot.immolate.remains<2&dot.immolate.remains11)&soul_shard<4.5 if S.Immolate:IsCastable() and (Player:SoulShardsP() < 4.5) then if Everyone.CastTargetIf(S.Immolate, Enemies40y, "min", EvaluateTargetIfFilterImmolate, EvaluateTargetIfImmolateHavoc, not Target:IsSpellInRange(S.Immolate)) then return "immolate havoc 8"; end end -- chaos_bolt,if=((talent.cry_havoc&!talent.inferno)|!talent.rain_of_fire)&cast_time=3&talent.inferno if S.RainofFire:IsReady() and (EnemiesCount8ySplash >= 3 and S.Inferno:IsAvailable()) then if Cast(S.RainofFire, nil, nil, not Target:IsSpellInRange(S.Conflagrate)) then return "rain_of_fire havoc 14"; end end -- rain_of_fire,if=(active_enemies>=4-talent.inferno+talent.madness_of_the_azjaqir) if S.RainofFire:IsReady() and (EnemiesCount8ySplash >= 4 - num(S.Inferno:IsAvailable()) + num(S.MadnessoftheAzjAqir:IsAvailable())) then if Cast(S.RainofFire, nil, nil, not Target:IsSpellInRange(S.Conflagrate)) then return "rain_of_fire havoc 16"; end end -- rain_of_fire,if=active_enemies>2&(talent.avatar_of_destruction|(talent.rain_of_chaos&buff.rain_of_chaos.up))&talent.inferno.enabled if S.RainofFire:IsReady() and (EnemiesCount8ySplash > 2 and (S.AvatarofDestruction:IsAvailable() or (S.RainofChaos:IsAvailable() and Player:BuffUp(S.RainofChaosBuff))) and S.Inferno:IsAvailable()) then if Cast(S.RainofFire, nil, nil, not Target:IsSpellInRange(S.Conflagrate)) then return "rain_of_fire havoc 18"; end end -- channel_demonfire,if=soul_shard<4.5 if S.ChannelDemonfire:IsReady() and (Player:SoulShardsP() < 4.5) then if Cast(S.ChannelDemonfire, nil, nil, not Target:IsInRange(40)) then return "channel_demonfire havoc 19"; end end -- conflagrate,if=!talent.backdraft if S.Conflagrate:IsCastable() and not S.Backdraft:IsAvailable() then if Cast(S.Conflagrate, nil, nil, not Target:IsSpellInRange(S.Conflagrate)) then return "conflagrate havoc 20"; end end -- incinerate,if=cast_timegcd.max if (VarHavocActive and VarHavocRemains > GCDMax) then local ShouldReturn = Havoc(); if ShouldReturn then return ShouldReturn; end end -- variable,name=pool_soul_shards,value=cooldown.havoc.remains<=10|talent.mayhem VarPoolSoulShards = (S.Havoc:CooldownRemains() <= 10 or S.Mayhem:IsAvailable()) -- conflagrate,if=(talent.roaring_blaze.enabled&debuff.conflagrate.remains<1.5)|charges=max_charges if S.Conflagrate:IsCastable() and ((S.RoaringBlaze:IsAvailable() and Target:DebuffRemains(S.RoaringBlazeDebuff) < 1.5) or S.Conflagrate:Charges() == S.Conflagrate:MaxCharges()) then if Cast(S.Conflagrate, nil, nil, not Target:IsSpellInRange(S.Conflagrate)) then return "conflagrate cleave 2"; end end -- dimensional_rift,if=soul_shard<4.7&(charges>2|fight_remains 2 or FightRemains < S.DimensionalRift:Cooldown())) then if Cast(S.DimensionalRift, Settings.Destruction.GCDasOffGCD.DimensionalRift, nil, not Target:IsSpellInRange(S.DimensionalRift)) then return "dimensional_rift cleave 4"; end end -- cataclysm,if=raid_event.adds.in>15 if CDsON() and S.Cataclysm:IsCastable() then if Cast(S.Cataclysm, Settings.Destruction.GCDasOffGCD.Cataclysm, nil, not Target:IsSpellInRange(S.Cataclysm)) then return "cataclysm cleave 6"; end end -- channel_demonfire,if=talent.raging_demonfire&active_dot.immolate=2 if S.ChannelDemonfire:IsReady() and (S.RagingDemonfire:IsAvailable() and S.ImmolateDebuff:AuraActiveCount() == 2) then if Cast(S.ChannelDemonfire, nil, nil, not Target:IsInRange(40)) then return "channel_demonfire cleave 8"; end end -- soul_fire,if=soul_shard<=3.5&(debuff.conflagrate.remains>cast_time+travel_time|!talent.roaring_blaze&buff.backdraft.up)&!variable.pool_soul_shards if S.SoulFire:IsCastable() and (Player:SoulShardsP() <= 3.5 and (Target:DebuffRemains(S.RoaringBlazeDebuff) > S.SoulFire:CastTime() + S.SoulFire:TravelTime() or (not S.RoaringBlaze:IsAvailable()) and Player:BuffUp(S.BackdraftBuff)) and not VarPoolSoulShards) then if Cast(S.SoulFire, nil, nil, not Target:IsSpellInRange(S.SoulFire)) then return "soul_fire cleave 10"; end end -- immolate,target_if=min:dot.immolate.remains+99*debuff.havoc.remains,if=(dot.immolate.refreshable&(dot.immolate.remainsremains)&(!talent.soul_fire|cooldown.soul_fire.remains+(!talent.mayhem*action.soul_fire.cast_time)>dot.immolate.remains)&target.time_to_die>15 if S.Immolate:IsCastable() then if Everyone.CastTargetIf(S.Immolate, Enemies40y, "min", EvaluateTargetIfFilterImmolate, EvaluateTargetIfImmolateCleave, not Target:IsSpellInRange(S.Immolate)) then return "immolate cleave 12"; end end -- havoc,target_if=min:((-target.time_to_die)8 if S.Havoc:IsCastable() and (S.SummonInfernal:CooldownDown() or not S.SummonInfernal:IsAvailable()) then --if Everyone.CastTargetIf(S.Havoc, Enemies40y, "min", EvaluateTargetIfFilterHavoc, EvaluateTargetIfHavoc, not Target:IsSpellInRange(S.Havoc)) then return "havoc cleave 14"; end local BestUnit, BestConditionValue, CUCV = nil, nil, nil for _, CycleUnit in pairs(Enemies40y) do if CycleUnit:GUID() ~= Target:GUID() then if BestConditionValue then CUCV = EvaluateTargetIfFilterHavoc(CycleUnit) end if not CycleUnit:IsFacingBlacklisted() and not CycleUnit:IsUserCycleBlacklisted() and (CycleUnit:AffectingCombat() or CycleUnit:IsDummy()) and ((not BestConditionValue) or Utils.CompareThis("min", CUCV, BestConditionValue)) then BestUnit, BestConditionValue = CycleUnit, CUCV end end end if BestUnit and EvaluateTargetIfHavoc(BestUnit) then HR.CastLeftNameplate(BestUnit, S.Havoc) end end -- chaos_bolt,if=pet.infernal.active|pet.blasphemy.active|soul_shard>=4 if S.ChaosBolt:IsReady() and (InfernalTime() > 0 or BlasphemyTime() > 0 or Player:SoulShardsP() >= 4) then if Cast(S.ChaosBolt, nil, nil, not Target:IsSpellInRange(S.ChaosBolt)) then return "chaos_bolt cleave 16"; end end -- summon_infernal if CDsON() and S.SummonInfernal:IsCastable() then if Cast(S.SummonInfernal, Settings.Destruction.GCDasOffGCD.SummonInfernal) then return "summon_infernal cleave 18"; end end -- channel_demonfire,if=talent.ruin.rank>1&!(talent.diabolic_embers&talent.avatar_of_destruction&(talent.burn_to_ashes|talent.chaos_incarnate)) if S.ChannelDemonfire:IsReady() and (S.Ruin:TalentRank() > 1 and (not (S.DiabolicEmbers:IsAvailable() and S.AvatarofDestruction:IsAvailable() and (S.BurntoAshes:IsAvailable() or S.ChaosIncarnate:IsAvailable())))) then if Cast(S.ChannelDemonfire, nil, nil, not Target:IsInRange(40)) then return "channel_demonfire cleave 20"; end end -- conflagrate,if=buff.backdraft.down&soul_shard>=1.5&!variable.pool_soul_shards if S.Conflagrate:IsCastable() and (Player:BuffDown(S.BackdraftBuff) and Player:SoulShardsP() >= 1.5 and not VarPoolSoulShards) then if Cast(S.Conflagrate, nil, nil, not Target:IsSpellInRange(S.Conflagrate)) then return "conflagrate cleave 22"; end end -- incinerate,if=cast_time+action.chaos_bolt.cast_timecast_time if S.ChaosBolt:IsReady() and (Player:BuffRemains(S.RainofChaosBuff) > S.ChaosBolt:CastTime()) then if Cast(S.ChaosBolt, nil, nil, not Target:IsSpellInRange(S.ChaosBolt)) then return "chaos_bolt cleave 26"; end end -- chaos_bolt,if=buff.backdraft.up&!variable.pool_soul_shards if S.ChaosBolt:IsReady() and (Player:BuffUp(S.BackdraftBuff) and not VarPoolSoulShards) then if Cast(S.ChaosBolt, nil, nil, not Target:IsSpellInRange(S.ChaosBolt)) then return "chaos_bolt cleave 28"; end end -- chaos_bolt,if=talent.eradication&!variable.pool_soul_shards&debuff.eradication.remains3.5&!variable.pool_soul_shards if S.ChaosBolt:IsReady() and (Player:SoulShardsP() > 3.5 and not VarPoolSoulShards) then if Cast(S.ChaosBolt, nil, nil, not Target:IsSpellInRange(S.ChaosBolt)) then return "chaos_bolt cleave 40"; end end -- chaos_bolt,if=!variable.pool_soul_shards&(talent.soul_conduit&!talent.madness_of_the_azjaqir|!talent.backdraft) if S.ChaosBolt:IsReady() and ((not VarPoolSoulShards) and (S.SoulConduit:IsAvailable() and (not S.MadnessoftheAzjAqir:IsAvailable()) or not S.Backdraft:IsAvailable())) then if Cast(S.ChaosBolt, nil, nil, not Target:IsSpellInRange(S.ChaosBolt)) then return "chaos_bolt cleave 42"; end end -- chaos_bolt,if=fight_remains<5&fight_remains>cast_time+travel_time -- Note: Added a buffer of 0.25s if S.ChaosBolt:IsReady() and (FightRemains < 5.25 and Target:TimeToDie() > S.ChaosBolt:CastTime() + S.ChaosBolt:TravelTime() + 0.25) then if Cast(S.ChaosBolt, nil, nil, not Target:IsSpellInRange(S.ChaosBolt)) then return "chaos_bolt cleave 44"; end end -- summon_soulkeeper,if=buff.tormented_soul.stack=10|buff.tormented_soul.stack>3&fight_remains<10 if S.SummonSoulkeeper:IsCastable() and (S.SummonSoulkeeper:Count() == 10 or S.SummonSoulkeeper:Count() > 3 and FightRemains < 10) then if Cast(S.SummonSoulkeeper, Settings.Destruction.GCDasOffGCD.SummonSoulkeeper) then return "summon_soulkeeper cleave 46"; end end -- conflagrate,if=charges>(max_charges-1)|fight_remains (S.Conflagrate:MaxCharges() - 1) or FightRemains < GCDMax * S.Conflagrate:Charges()) then if Cast(S.Conflagrate, nil, nil, not Target:IsSpellInRange(S.Conflagrate)) then return "conflagrate cleave 48"; end end -- incinerate if S.Incinerate:IsCastable() then if Cast(S.Incinerate, nil, nil, not Target:IsSpellInRange(S.Incinerate)) then return "incinerate cleave 50"; end end end local function Aoe() -- call_action_list,name=ogcd if CDsON() then local ShouldReturn = oGCD(); if ShouldReturn then return ShouldReturn; end end -- call_action_list,name=items if CDsON() and (Settings.Commons.Enabled.Trinkets or Settings.Commons.Enabled.Trinkets) then local ShouldReturn = Items(); if ShouldReturn then return ShouldReturn; end end -- call_action_list,name=havoc,if=havoc_active&havoc_remains>gcd.max&active_enemies<5+(talent.cry_havoc&!talent.inferno)&(!cooldown.summon_infernal.up|!talent.summon_infernal) if (VarHavocActive and VarHavocRemains > GCDMax and EnemiesCount8ySplash < 5 + num(S.CryHavoc:IsAvailable() and not S.Inferno:IsAvailable()) and (S.SummonInfernal:CooldownDown() or not S.SummonInfernal:IsAvailable())) then local ShouldReturn = Havoc(); if ShouldReturn then return ShouldReturn; end end -- rain_of_fire,if=pet.infernal.active|pet.blasphemy.active if S.RainofFire:IsReady() and (InfernalTime() > 0 or BlasphemyTime() > 0) then if Cast(S.RainofFire, nil, nil, not Target:IsSpellInRange(S.Conflagrate)) then return "rain_of_fire aoe 2"; end end -- rain_of_fire,if=fight_remains<12 if S.RainofFire:IsReady() and (FightRemains < 12) then if Cast(S.RainofFire, nil, nil, not Target:IsSpellInRange(S.Conflagrate)) then return "rain_of_fire aoe 4"; end end -- rain_of_fire,if=gcd.max>buff.madness_rof.remains&buff.madness_rof.up if S.RainofFire:IsReady() and (GCDMax > Player:BuffRemains(S.MadnessRoFBuff) and Player:BuffUp(S.MadnessRoFBuff)) then if Cast(S.RainofFire, nil, nil, not Target:IsSpellInRange(S.Conflagrate)) then return "rain_of_fire aoe 6"; end end -- rain_of_fire,if=soul_shard>=(4.5-0.1*active_dot.immolate)&time>5 if S.RainofFire:IsReady() and (Player:SoulShardsP() >= (4.5 - 0.1 * S.ImmolateDebuff:AuraActiveCount()) and HL.CombatTime() > 5) then if Cast(S.RainofFire, nil, nil, not Target:IsSpellInRange(S.Conflagrate)) then return "rain_of_fire aoe 8"; end end -- chaos_bolt,if=soul_shard>3.5-(0.1*active_enemies)&!talent.rain_of_fire if S.ChaosBolt:IsReady() and (Player:SoulShardsP() > 3.5 - (0.1 * EnemiesCount8ySplash) and not S.RainofFire:IsAvailable()) then if Cast(S.ChaosBolt, nil, nil, not Target:IsSpellInRange(S.ChaosBolt)) then return "chaos_bolt aoe 10"; end end -- cataclysm,if=raid_event.adds.in>15 if CDsON() and S.Cataclysm:IsCastable() then if Cast(S.Cataclysm, Settings.Destruction.GCDasOffGCD.Cataclysm, nil, not Target:IsSpellInRange(S.Cataclysm)) then return "cataclysm aoe 12"; end end -- havoc,target_if=min:((-target.time_to_die)4))&target.time_to_die>8 if S.Havoc:IsReady() and (S.SummonInfernal:CooldownDown() or (not S.SummonInfernal:IsAvailable()) or (S.Inferno:IsAvailable() and EnemiesCount8ySplash > 4)) then if Everyone.CastTargetIf(S.Havoc, Enemies40y, "min", EvaluateTargetIfFilterHavoc, EvaluateTargetIfHavoc, not Target:IsSpellInRange(S.Havoc)) then return "havoc aoe 14"; end end -- immolate,target_if=min:dot.immolate.remains+99*debuff.havoc.remains,if=dot.immolate.refreshable&(!talent.cataclysm.enabled|cooldown.cataclysm.remains>dot.immolate.remains)&(!talent.raging_demonfire|cooldown.channel_demonfire.remains>remains|time<5)&active_dot.immolate<=4&target.time_to_die>18 if S.Immolate:IsCastable() and (S.ImmolateDebuff:AuraActiveCount() <= 4) then if Everyone.CastTargetIf(S.Immolate, Enemies40y, "min", EvaluateTargetIfFilterImmolate, EvaluateTargetIfImmolateAoE, not Target:IsSpellInRange(S.Immolate)) then return "immolate aoe 16"; end end -- channel_demonfire,if=dot.immolate.remains>cast_time&talent.raging_demonfire if S.ChannelDemonfire:IsReady() and (Target:DebuffRemains(S.ImmolateDebuff) > ChannelDemonfireExecuteTime() and S.RagingDemonfire:IsAvailable()) then if Cast(S.ChannelDemonfire, nil, nil, not Target:IsInRange(40)) then return "channel_demonfire aoe 17"; end end -- summon_soulkeeper,if=buff.tormented_soul.stack=10|buff.tormented_soul.stack>3&fight_remains<10 if S.SummonSoulkeeper:IsCastable() and (S.SummonSoulkeeper:Count() == 10 or S.SummonSoulkeeper:Count() > 3 and FightRemains < 10) then if Cast(S.SummonSoulkeeper, Settings.Destruction.GCDasOffGCD.SummonSoulkeeper) then return "summon_soulkeeper aoe 18"; end end -- call_action_list,name=ogcd if CDsON() then local ShouldReturn = oGCD(); if ShouldReturn then return ShouldReturn; end end -- summon_infernal if CDsON() and S.SummonInfernal:IsCastable() then if Cast(S.SummonInfernal, Settings.Destruction.GCDasOffGCD.SummonInfernal) then return "summon_infernal aoe 20"; end end -- rain_of_fire,if=debuff.pyrogenics.down&active_enemies<=4 if S.RainofFire:IsReady() and (Target:DebuffDown(S.PyrogenicsDebuff) and EnemiesCount8ySplash <= 4) then if Cast(S.RainofFire, nil, nil, not Target:IsSpellInRange(S.Conflagrate)) then return "rain_of_fire aoe 22"; end end -- channel_demonfire,if=dot.immolate.remains>cast_time if S.ChannelDemonfire:IsReady() and (Target:DebuffRemains(S.ImmolateDebuff) > ChannelDemonfireExecuteTime()) then if Cast(S.ChannelDemonfire, nil, nil, not Target:IsSpellInRange(S.ChannelDemonfire)) then return "channel_demonfire aoe 24"; end end -- immolate,target_if=min:dot.immolate.remains+99*debuff.havoc.remains,if=((dot.immolate.refreshable&(!talent.cataclysm.enabled|cooldown.cataclysm.remains>dot.immolate.remains))|active_enemies>active_dot.immolate)&target.time_to_die>10&!havoc_active if S.Immolate:IsCastable() then if Everyone.CastTargetIf(S.Immolate, Enemies40y, "min", EvaluateTargetIfFilterImmolate, EvaluateTargetIfImmolateAoE2, not Target:IsSpellInRange(S.Immolate)) then return "immolate aoe 26"; end end -- immolate,target_if=min:dot.immolate.remains+99*debuff.havoc.remains,if=((dot.immolate.refreshable&variable.havoc_immo_time<5.4)|(dot.immolate.remains<2&dot.immolate.remainsdot.immolate.remains)&target.time_to_die>11 if S.Immolate:IsCastable() then if Everyone.CastTargetIf(S.Immolate, Enemies40y, "min", EvaluateTargetIfFilterImmolate, EvaluateTargetIfImmolateAoE3, not Target:IsSpellInRange(S.Immolate)) then return "immolate aoe 28"; end end -- soul_fire,if=buff.backdraft.up if S.SoulFire:IsCastable() and (Player:BuffUp(S.BackdraftBuff)) then if Cast(S.SoulFire, nil, nil, not Target:IsSpellInRange(S.SoulFire)) then return "soul_fire aoe 30"; end end -- incinerate,if=talent.fire_and_brimstone.enabled&buff.backdraft.up if S.Incinerate:IsCastable() and (S.FireandBrimstone:IsAvailable() and Player:BuffUp(S.BackdraftBuff)) then if Cast(S.Incinerate, nil, nil, not Target:IsSpellInRange(S.Incinerate)) then return "incinerate aoe 32"; end end -- conflagrate,if=buff.backdraft.stack<2|!talent.backdraft if S.Conflagrate:IsCastable() and (Player:BuffStack(S.BackdraftBuff) < 2 or not S.Backdraft:IsAvailable()) then if Cast(S.Conflagrate, nil, nil, not Target:IsSpellInRange(S.Conflagrate)) then return "conflagrate aoe 34"; end end -- dimensional_rift if CDsON() and S.DimensionalRift:IsCastable() then if Cast(S.DimensionalRift, Settings.Destruction.GCDasOffGCD.DimensionalRift, nil, not Target:IsSpellInRange(S.DimensionalRift)) then return "dimensional_rift aoe 36"; end end -- incinerate if S.Incinerate:IsCastable() then if Cast(S.Incinerate, nil, nil, not Target:IsSpellInRange(S.Incinerate)) then return "incinerate aoe 38"; end end end --- ======= MAIN ======= local function APL() -- Unit Update Enemies40y = Player:GetEnemiesInRange(40) Enemies8ySplash = Target:GetEnemiesInSplashRange(12) if AoEON() then EnemiesCount8ySplash = Target:GetEnemiesInSplashRangeCount(12) else EnemiesCount8ySplash = 1 end if Everyone.TargetIsValid() or Player:AffectingCombat() then -- Define gcd.max (0.25 seconds to allow for latency and player reaction time) GCDMax = Player:GCD() + 0.25 -- Check Havoc Status VarHavocActive, VarHavocRemains, VarHavocImmoTime = UnitWithHavoc(Enemies40y) -- Calculate fight_remains BossFightRemains = HL.BossFightRemains(nil, true) FightRemains = BossFightRemains if FightRemains == 11111 then FightRemains = HL.FightRemains(Enemies8ySplash, false) end end -- Summon Pet if S.SummonPet:IsCastable() then if Cast(S.SummonPet, Settings.Destruction.GCDasOffGCD.SummonPet) then return "summon_pet ooc"; end end if Everyone.TargetIsValid() then -- Precombat if (not Player:AffectingCombat()) then local ShouldReturn = Precombat(); if ShouldReturn then return ShouldReturn; end end -- variable,name=havoc_immo_time,op=reset -- cycling_variable,name=havoc_immo_time,op=add,value=dot.immolate.remains*debuff.havoc.up -- Note: Above lines are to check how long Immolate remains on our Havoc target. This is included in UnitWithHavoc() now. -- call_action_list,name=aoe,if=(active_enemies>=3-(talent.inferno&!talent.madness_of_the_azjaqir))&!(!talent.inferno&talent.madness_of_the_azjaqir&talent.chaos_incarnate&active_enemies<4)&!variable.cleave_apl if ((EnemiesCount8ySplash >= 3 - (num(S.Inferno:IsAvailable() and not S.MadnessoftheAzjAqir:IsAvailable()))) and (not ((not S.Inferno:IsAvailable()) and S.MadnessoftheAzjAqir:IsAvailable() and S.ChaosIncarnate:IsAvailable() and EnemiesCount8ySplash < 4)) and not VarCleaveAPL) then local ShouldReturn = Aoe(); if ShouldReturn then return ShouldReturn; end end -- call_action_list,name=cleave,if=active_enemies!=1|variable.cleave_apl if (EnemiesCount8ySplash > 1 or VarCleaveAPL) then local ShouldReturn = Cleave(); if ShouldReturn then return ShouldReturn; end end -- call_action_list,name=items if CDsON() and (Settings.Commons.Enabled.Trinkets or Settings.Commons.Enabled.Trinkets) then local ShouldReturn = Items(); if ShouldReturn then return ShouldReturn; end end -- call_action_list,name=ogcd if CDsON() then local ShouldReturn = oGCD(); if ShouldReturn then return ShouldReturn; end end -- conflagrate,if=(talent.roaring_blaze&debuff.conflagrate.remains<1.5)|charges=max_charges if S.Conflagrate:IsReady() and ((S.RoaringBlaze:IsAvailable() and Target:DebuffRemains(S.RoaringBlazeDebuff) < 1.5) or S.Conflagrate:Charges() == S.Conflagrate:MaxCharges()) then if Cast(S.Conflagrate, nil, nil, not Target:IsSpellInRange(S.Conflagrate)) then return "conflagrate main 2"; end end -- dimensional_rift,if=soul_shard<4.7&(charges>2|fight_remains 2 or FightRemains < S.DimensionalRift:Cooldown())) then if Cast(S.DimensionalRift, Settings.Destruction.GCDasOffGCD.DimensionalRift, nil, not Target:IsSpellInRange(S.DimensionalRift)) then return "dimensional_rift main 4"; end end -- cataclysm,if=raid_event.adds.in>15 if CDsON() and S.Cataclysm:IsReady() then if Cast(S.Cataclysm, Settings.Destruction.GCDasOffGCD.Cataclysm, nil, not Target:IsInRange(40)) then return "cataclysm main 6"; end end -- channel_demonfire,if=talent.raging_demonfire&(dot.immolate.remains-5*(action.chaos_bolt.in_flight&talent.internal_combustion))>cast_time&(debuff.conflagrate.remains>execute_time|!talent.roaring_blaze) if S.ChannelDemonfire:IsReady() and (S.RagingDemonfire:IsAvailable() and (Target:DebuffRemains(S.ImmolateDebuff) - 5 * num(S.ChaosBolt:InFlight() and S.InternalCombustion:IsAvailable())) > ChannelDemonfireExecuteTime() and (Target:DebuffRemains(S.ConflagrateDebuff) > ChannelDemonfireExecuteTime() or not S.RoaringBlaze:IsAvailable())) then if Cast(S.ChannelDemonfire, nil, nil, not Target:IsInRange(40)) then return "channel_demonfire main 8"; end end -- soul_fire,if=soul_shard<=3.5&(debuff.conflagrate.remains>cast_time+travel_time|!talent.roaring_blaze&buff.backdraft.up) if S.SoulFire:IsCastable() and (Player:SoulShardsP() <= 3.5 and (Target:DebuffRemains(S.RoaringBlazeDebuff) > S.SoulFire:CastTime() + S.SoulFire:TravelTime() or (not S.RoaringBlaze:IsAvailable()) and Player:BuffUp(S.BackdraftBuff))) then if Cast(S.SoulFire, nil, nil, not Target:IsSpellInRange(S.SoulFire)) then return "soul_fire main 10"; end end -- immolate,if=(((dot.immolate.remains-5*(action.chaos_bolt.in_flight&talent.internal_combustion))dot.immolate.remains)&(!talent.soul_fire|cooldown.soul_fire.remains+action.soul_fire.cast_time>(dot.immolate.remains-5*talent.internal_combustion))&target.time_to_die>8 if S.Immolate:IsCastable() and ((((Target:DebuffRemains(S.ImmolateDebuff) - 5 * num(S.ChaosBolt:InFlight() and S.InternalCombustion:IsAvailable())) < S.ImmolateDebuff:PandemicThreshold()) or Target:DebuffRemains(S.ImmolateDebuff) < 3 or (Target:DebuffRemains(S.ImmolateDebuff) - S.ChaosBolt:ExecuteTime()) < 5 and S.InternalCombustion:IsAvailable() and S.ChaosBolt:IsReady()) and ((not S.Cataclysm:IsAvailable()) or S.Cataclysm:CooldownRemains() > Target:DebuffRemains(S.ImmolateDebuff)) and ((not S.SoulFire:IsAvailable()) or S.SoulFire:CooldownRemains() + S.SoulFire:CastTime() > (Target:DebuffRemains(S.ImmolateDebuff) - 5 * num(S.InternalCombustion:IsAvailable()))) and Target:TimeToDie() > 8) then if Cast(S.Immolate, nil, nil, not Target:IsSpellInRange(S.Immolate)) then return "immolate main 12"; end end -- channel_demonfire,if=dot.immolate.remains>cast_time&set_bonus.tier30_4pc if S.ChannelDemonfire:IsReady() and (Target:DebuffRemains(S.ImmolateDebuff) > ChannelDemonfireExecuteTime() and Player:HasTier(30, 4)) then if Cast(S.ChannelDemonfire, nil, nil, not Target:IsInRange(40)) then return "channel_demonfire main 16"; end end -- chaos_bolt,if=pet.infernal.active|pet.blasphemy.active|soul_shard>=4 if S.ChaosBolt:IsReady() and (InfernalTime() > 0 or BlasphemyTime() > 0 or Player:SoulShardsP() >= 4) then if Cast(S.ChaosBolt, nil, nil, not Target:IsSpellInRange(S.ChaosBolt)) then return "chaos_bolt main 18"; end end -- summon_infernal if CDsON() and S.SummonInfernal:IsCastable() then if Cast(S.SummonInfernal, Settings.Destruction.GCDasOffGCD.SummonInfernal) then return "summon_infernal main 20"; end end -- channel_demonfire,if=talent.ruin.rank>1&!(talent.diabolic_embers&talent.avatar_of_destruction&(talent.burn_to_ashes|talent.chaos_incarnate))&dot.immolate.remains>cast_time if S.ChannelDemonfire:IsReady() and (S.Ruin:TalentRank() > 1 and not (S.DiabolicEmbers:IsAvailable() and S.AvatarofDestruction:IsAvailable() and (S.BurntoAshes:IsAvailable() or S.ChaosIncarnate:IsAvailable())) and Target:DebuffRemains(S.ImmolateDebuff) > ChannelDemonfireExecuteTime()) then if Cast(S.ChannelDemonfire, nil, nil, not Target:IsInRange(40)) then return "channel_demonfire main 22"; end end -- conflagrate,if=buff.backdraft.down&soul_shard>=1.5&!talent.roaring_blaze if S.Conflagrate:IsCastable() and (Player:BuffDown(S.BackdraftBuff) and Player:SoulShardsP() >= 1.5 and not S.RoaringBlaze:IsAvailable()) then if Cast(S.Conflagrate, nil, nil, not Target:IsSpellInRange(S.Conflagrate)) then return "conflagrate main 24"; end end -- incinerate,if=cast_time+action.chaos_bolt.cast_timecast_time if S.ChaosBolt:IsReady() and (Player:BuffRemains(S.RainofChaosBuff) > S.ChaosBolt:CastTime()) then if Cast(S.ChaosBolt, nil, nil, not Target:IsSpellInRange(S.ChaosBolt)) then return "chaos_bolt main 28"; end end -- chaos_bolt,if=buff.backdraft.up&!talent.eradication&!talent.madness_of_the_azjaqir if S.ChaosBolt:IsReady() and (Player:BuffUp(S.BackdraftBuff) and (not S.Eradication:IsAvailable()) and not S.MadnessoftheAzjAqir:IsAvailable()) then if Cast(S.ChaosBolt, nil, nil, not Target:IsSpellInRange(S.ChaosBolt)) then return "chaos_bolt main 30"; end end -- chaos_bolt,if=buff.madness_cb.up if S.ChaosBolt:IsReady() and (Player:BuffUp(S.MadnessCBBuff)) then if Cast(S.ChaosBolt, nil, nil, not Target:IsSpellInRange(S.ChaosBolt)) then return "chaos_bolt main 32"; end end -- channel_demonfire,if=!(talent.diabolic_embers&talent.avatar_of_destruction&(talent.burn_to_ashes|talent.chaos_incarnate))&dot.immolate.remains>cast_time if S.ChannelDemonfire:IsReady() and (not (S.DiabolicEmbers:IsAvailable() and S.AvatarofDestruction:IsAvailable() and (S.BurntoAshes:IsAvailable() or S.ChaosIncarnate:IsAvailable())) and Target:DebuffRemains(S.ImmolateDebuff) > ChannelDemonfireExecuteTime()) then if Cast(S.ChannelDemonfire, nil, nil, not Target:IsInRange(40)) then return "channel_demonfire main 34"; end end -- dimensional_rift if CDsON() and S.DimensionalRift:IsCastable() then if Cast(S.DimensionalRift, Settings.Destruction.GCDasOffGCD.DimensionalRift, nil, not Target:IsSpellInRange(S.DimensionalRift)) then return "dimensional_rift main 36"; end end -- chaos_bolt,if=soul_shard>3.5 if S.ChaosBolt:IsReady() and (Player:SoulShardsP() >= 3.5) then if Cast(S.ChaosBolt, nil, nil, not Target:IsSpellInRange(S.ChaosBolt)) then return "chaos_bolt main 38"; end end -- chaos_bolt,if=talent.soul_conduit&!talent.madness_of_the_azjaqir|!talent.backdraft if S.ChaosBolt:IsReady() and (S.SoulConduit:IsAvailable() and (not S.MadnessoftheAzjAqir:IsAvailable()) or not S.Backdraft:IsAvailable()) then if Cast(S.ChaosBolt, nil, nil, not Target:IsSpellInRange(S.ChaosBolt)) then return "chaos_bolt main 40"; end end -- chaos_bolt,if=fight_remains<5&fight_remains>cast_time+travel_time -- Note: Added a buffer of 0.5s if S.ChaosBolt:IsReady() and (FightRemains < 5.5 and FightRemains > S.ChaosBolt:CastTime() + S.ChaosBolt:TravelTime() + 0.5) then if Cast(S.ChaosBolt, nil, nil, not Target:IsSpellInRange(S.ChaosBolt)) then return "chaos_bolt main 42"; end end -- conflagrate,if=charges>(max_charges-1)|time_to_die (S.Conflagrate:MaxCharges() - 1) or FightRemains < GCDMax * S.Conflagrate:Charges()) then if Cast(S.Conflagrate, nil, nil, not Target:IsSpellInRange(S.Conflagrate)) then return "conflagrate main 44"; end end -- incinerate if S.Incinerate:IsCastable() then if Cast(S.Incinerate, nil, nil, not Target:IsSpellInRange(S.Incinerate)) then return "incinerate main 46"; end end end end local function OnInit() S.ImmolateDebuff:RegisterAuraTracking() HR.Print("Destruction Warlock rotation is currently a work in progress, but has been updated for patch 10.1.5.") end HR.SetAPL(267, APL, OnInit)