You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
466 lines
35 KiB
466 lines
35 KiB
--- ============================ HEADER ============================
|
|
--- ======= LOCALIZE =======
|
|
-- Addon
|
|
local addonName, addonTable = ...
|
|
-- HeroDBC
|
|
local DBC = HeroDBC.DBC
|
|
-- HeroLib
|
|
local HL = HeroLib
|
|
local Cache = HeroCache
|
|
local Unit = HL.Unit
|
|
local Player = Unit.Player
|
|
local Target = Unit.Target
|
|
local Pet = Unit.Pet
|
|
local Spell = HL.Spell
|
|
local Item = HL.Item
|
|
-- HeroRotation
|
|
local HR = HeroRotation
|
|
local AoEON = HR.AoEON
|
|
local CDsON = HR.CDsON
|
|
local Cast = HR.Cast
|
|
local CastSuggested = HR.CastSuggested
|
|
-- Num/Bool Helper Functions
|
|
local num = HR.Commons.Everyone.num
|
|
local bool = HR.Commons.Everyone.bool
|
|
-- lua
|
|
local mathmin = math.min
|
|
local mathmax = math.max
|
|
|
|
--- ============================ CONTENT ===========================
|
|
--- ======= APL LOCALS =======
|
|
-- luacheck: max_line_length 9999
|
|
|
|
-- Define S/I for spell and item arrays
|
|
local S = Spell.DemonHunter.Havoc
|
|
local I = Item.DemonHunter.Havoc
|
|
|
|
-- Create table to exclude above trinkets from On Use function
|
|
local OnUseExcludes = {
|
|
I.AlgetharPuzzleBox:ID(),
|
|
I.DragonfireBombDispenser:ID(),
|
|
I.ElementiumPocketAnvil:ID(),
|
|
I.ManicGrieftorch:ID(),
|
|
I.StormEatersBoon:ID(),
|
|
}
|
|
|
|
-- Trinket Item Objects
|
|
local equip = Player:GetEquipment()
|
|
local trinket1 = equip[13] and Item(equip[13]) or Item(0)
|
|
local trinket2 = equip[14] and Item(equip[14]) or Item(0)
|
|
|
|
-- Rotation Var
|
|
local Enemies8y, Enemies20y
|
|
local EnemiesCount8, EnemiesCount20
|
|
|
|
-- GUI Settings
|
|
local Everyone = HR.Commons.Everyone
|
|
local Settings = {
|
|
General = HR.GUISettings.General,
|
|
Commons = HR.GUISettings.APL.DemonHunter.Commons,
|
|
Havoc = HR.GUISettings.APL.DemonHunter.Havoc
|
|
}
|
|
|
|
-- Interrupts List
|
|
local StunInterrupts = {
|
|
{S.FelEruption, "Cast Fel Eruption (Interrupt)", function () return true; end},
|
|
{S.ChaosNova, "Cast Chaos Nova (Interrupt)", function () return true; end},
|
|
}
|
|
|
|
-- Variables
|
|
local Var3MinTrinket = false
|
|
local VarBladeDance = false
|
|
local VarPoolingForBladeDance = false
|
|
local VarPoolingForEyeBeam = false
|
|
local VarWaitingForEssenceBreak = false
|
|
local VarWaitingForMomentum = false
|
|
local VarHoldingMeta = false
|
|
local GCDMax = Player:GCD() + 0.25
|
|
local CombatTime = 0
|
|
local VarTrinketSyncSlot = 0
|
|
local VarUseEyeBeamFuryCondition = false
|
|
local BossFightRemains = 11111
|
|
local FightRemains = 11111
|
|
|
|
HL:RegisterForEvent(function()
|
|
VarBladeDance = false
|
|
VarPoolingForBladeDance = false
|
|
VarPoolingForEyeBeam = false
|
|
VarWaitingForEssenceBreak = false
|
|
VarWaitingForMomentum = false
|
|
BossFightRemains = 11111
|
|
FightRemains = 11111
|
|
end, "PLAYER_REGEN_ENABLED")
|
|
|
|
HL:RegisterForEvent(function()
|
|
equip = Player:GetEquipment()
|
|
trinket1 = equip[13] and Item(equip[13]) or Item(0)
|
|
trinket2 = equip[14] and Item(equip[14]) or Item(0)
|
|
end, "PLAYER_EQUIPMENT_CHANGED")
|
|
|
|
-- Functions
|
|
local function IsInMeleeRange(range)
|
|
if S.Felblade:TimeSinceLastCast() <= Player:GCD() then
|
|
return true
|
|
elseif S.VengefulRetreat:TimeSinceLastCast() < 1.0 then
|
|
return false
|
|
end
|
|
return range and Target:IsInMeleeRange(range) or Target:IsInMeleeRange(5)
|
|
end
|
|
|
|
local function UseFelRush()
|
|
return (Settings.Havoc.ConserveFelRush and S.FelRush:Charges() == 2) or not Settings.Havoc.ConserveFelRush
|
|
end
|
|
|
|
local function EvalutateTargetIfFilterDemonsBite(TargetUnit)
|
|
-- target_if=min:debuff.burning_wound.remains
|
|
return TargetUnit:DebuffRemains(S.BurningWoundDebuff) or TargetUnit:DebuffRemains(S.BurningWoundLegDebuff)
|
|
end
|
|
|
|
local function EvaluateTargetIfDemonsBite(TargetUnit)
|
|
-- if=talent.burning_wound&debuff.burning_wound.remains<4&active_dot.burning_wound<(spell_targets>?3)
|
|
return S.BurningWound:IsAvailable() and TargetUnit:DebuffRemains(S.BurningWoundDebuff) < 4 and S.BurningWoundDebuff:AuraActiveCount() < mathmin(EnemiesCount8, 3)
|
|
end
|
|
|
|
local function Precombat()
|
|
-- flask
|
|
-- augmentation
|
|
-- food
|
|
-- snapshot_stats
|
|
-- variable,name=3min_trinket,value=trinket.1.cooldown.duration=180|trinket.2.cooldown.duration=180
|
|
Var3MinTrinket = (trinket1:Cooldown() == 180 or trinket2:Cooldown() == 180)
|
|
-- variable,name=trinket_sync_slot,value=1,if=trinket.1.has_stat.any_dps&(!trinket.2.has_stat.any_dps|trinket.1.cooldown.duration>=trinket.2.cooldown.duration)
|
|
VarTrinketSyncSlot = 0
|
|
if (trinket1:HasStatAnyDps() and ((not trinket2:HasStatAnyDps()) or trinket1:Cooldown() >= trinket2:Cooldown())) then
|
|
VarTrinketSyncSlot = 1
|
|
end
|
|
-- variable,name=trinket_sync_slot,value=2,if=trinket.2.has_stat.any_dps&(!trinket.1.has_stat.any_dps|trinket.2.cooldown.duration>trinket.1.cooldown.duration)
|
|
if (trinket2:HasStatAnyDps() and ((not trinket1:HasStatAnyDps()) or trinket2:Cooldown() >= trinket1:Cooldown())) then
|
|
VarTrinketSyncSlot = 2
|
|
end
|
|
-- arcane_torrent
|
|
if S.ArcaneTorrent:IsCastable() and CDsON() then
|
|
if Cast(S.ArcaneTorrent, Settings.Commons.OffGCDasOffGCD.Racials, nil, not Target:IsInRange(8)) then return "arcane_torrent precombat 2"; end
|
|
end
|
|
-- use_item,name=algethar_puzzle_box
|
|
if Settings.Commons.Enabled.Trinkets and I.AlgetharPuzzleBox:IsEquippedAndReady() then
|
|
if Cast(I.AlgetharPuzzleBox, nil, Settings.Commons.DisplayStyle.Trinkets) then return "algethar_puzzle_box precombat 4"; end
|
|
end
|
|
-- sigil_of_flame,if=!equipped.algethar_puzzle_box
|
|
if S.SigilofFlame:IsCastable() and (not I.AlgetharPuzzleBox:IsEquipped()) then
|
|
if Cast(S.SigilofFlame, Settings.Havoc.GCDasOffGCD.SigilOfFlame, nil, not Target:IsInRange(30)) then return "sigil_of_flame precombat 6"; end
|
|
end
|
|
-- immolation_aura
|
|
if S.ImmolationAura:IsCastable() then
|
|
if Cast(S.ImmolationAura, Settings.Havoc.GCDasOffGCD.ImmolationAura, nil, not IsInMeleeRange(8)) then return "immolation_aura precombat 8"; end
|
|
end
|
|
-- Manually added: Fel Rush if out of range
|
|
if (not Target:IsInMeleeRange(5)) and S.FelRush:IsCastable() then
|
|
if Cast(S.FelRush, nil, Settings.Commons.DisplayStyle.FelRush, not Target:IsInRange(15)) then return "fel_rush precombat 10"; end
|
|
end
|
|
-- Manually added: Demon's Bite/Demon Blades if in melee range
|
|
if Target:IsInMeleeRange(5) and (S.DemonsBite:IsCastable() or S.DemonBlades:IsAvailable()) then
|
|
if Cast(S.DemonsBite, nil, nil, not Target:IsInMeleeRange(5)) then return "demons_bite or demon_blades precombat 12"; end
|
|
end
|
|
end
|
|
|
|
local function MetaEnd()
|
|
-- death_sweep
|
|
if S.DeathSweep:IsReady() then
|
|
if Cast(S.DeathSweep, nil, nil, not IsInMeleeRange(8)) then return "death_sweep meta_end 2"; end
|
|
end
|
|
-- annihilation
|
|
if S.Annihilation:IsReady() then
|
|
if Cast(S.Annihilation, nil, nil, not IsInMeleeRange(5)) then return "annihilation meta_end 4"; end
|
|
end
|
|
end
|
|
|
|
local function Cooldown()
|
|
if CDsON() then
|
|
-- metamorphosis,if=!talent.demonic&((!talent.chaotic_transformation|cooldown.eye_beam.remains>20)&active_enemies>desired_targets|raid_event.adds.in>60|fight_remains<25)
|
|
if S.Metamorphosis:IsCastable() and (not S.Demonic:IsAvailable()) then
|
|
if Cast(S.Metamorphosis, nil, Settings.Commons.DisplayStyle.Metamorphosis, not Target:IsInRange(40)) then return "metamorphosis cooldown 2"; end
|
|
end
|
|
-- metamorphosis,if=talent.demonic&(!talent.chaotic_transformation|cooldown.eye_beam.remains>20&(!variable.blade_dance|cooldown.blade_dance.remains>gcd.max)|fight_remains<25+talent.shattered_destiny*70&cooldown.eye_beam.remains&cooldown.blade_dance.remains)
|
|
if S.Metamorphosis:IsCastable() and (S.Demonic:IsAvailable() and ((not S.ChaoticTransformation:IsAvailable()) or S.EyeBeam:CooldownRemains() > 20 and ((not VarBladeDance) or S.BladeDance:CooldownRemains() > GCDMax) or FightRemains < 25 + num(S.ShatteredDestiny:IsAvailable()) * 70 and S.EyeBeam:CooldownDown() and S.BladeDance:CooldownDown())) then
|
|
if Cast(S.Metamorphosis, nil, Settings.Commons.DisplayStyle.Metamorphosis, not Target:IsInRange(40)) then return "metamorphosis cooldown 4"; end
|
|
end
|
|
end
|
|
-- potion,if=buff.metamorphosis.remains>25|buff.metamorphosis.up&cooldown.metamorphosis.ready|fight_remains<60|time>0.1&time<10
|
|
if Settings.Commons.Enabled.Potions and (Player:BuffRemains(S.MetamorphosisBuff) > 25 or Player:BuffUp(S.MetamorphosisBuff) and S.Metamorphosis:CooldownUp() or FightRemains < 60 or CombatTime > 0.1 and CombatTime < 10) then
|
|
local PotionSelected = Everyone.PotionSelected()
|
|
if PotionSelected and PotionSelected:IsReady() then
|
|
if Cast(PotionSelected, nil, Settings.Commons.DisplayStyle.Potions) then return "potion cooldown 6"; end
|
|
end
|
|
end
|
|
if Settings.Commons.Enabled.Trinkets then
|
|
-- use_item,name=manic_grieftorch,use_off_gcd=1,if=buff.vengeful_retreat_movement.down&((buff.initiative.remains>2&debuff.essence_break.down&cooldown.essence_break.remains>gcd.max&time>14|time_to_die<10|time<1&!equipped.algethar_puzzle_box|fight_remains%%120<5)&!prev_gcd.1.essence_break)
|
|
if I.ManicGrieftorch:IsEquippedAndReady() and ((Player:BuffRemains(S.InitiativeBuff) > 2 and Target:DebuffDown(S.EssenceBreakDebuff) and S.EssenceBreak:CooldownRemains() > GCDMax and CombatTime > 14 or Target:TimeToDie() < 10 or CombatTime < 1 and (not I.AlgetharPuzzleBox:IsEquipped()) or FightRemains % 120 < 5) and not Player:PrevGCDP(1, S.EssenceBreak)) then
|
|
if Cast(I.ManicGrieftorch, nil, Settings.Commons.DisplayStyle.Trinkets, not Target:IsInRange(40)) then return "manic_grieftorch cooldown 8"; end
|
|
end
|
|
-- use_item,name=algethar_puzzle_box,use_off_gcd=1,if=cooldown.metamorphosis.remains<=gcd.max*5|fight_remains%%180>10&fight_remains%%180<22|fight_remains<25
|
|
if I.AlgetharPuzzleBox:IsEquippedAndReady() and (S.Metamorphosis:CooldownRemains() <= GCDMax * 5 or FightRemains % 180 > 10 and FightRemains % 180 < 22 or FightRemains < 25) then
|
|
if Cast(I.AlgetharPuzzleBox, nil, Settings.Commons.DisplayStyle.Trinkets) then return "algethar_puzzle_box cooldown 10"; end
|
|
end
|
|
-- use_item,name=dragonfire_bomb_dispenser,use_off_gcd=1,if=fight_remains<20|charges=3|cooldown.vengeful_retreat.remains<5
|
|
local DBDSpell = I.DragonfireBombDispenser:OnUseSpell()
|
|
local DBDCharges = DBDSpell and DBDSpell:Charges() or 0
|
|
if I.DragonfireBombDispenser:IsEquippedAndReady() and (FightRemains < 20 or DBDCharges == 3 or S.VengefulRetreat:CooldownRemains() < 5) then
|
|
if Cast(I.DragonfireBombDispenser, nil, Settings.Commons.DisplayStyle.Trinkets, not Target:IsInRange(46)) then return "dragonfire_bomb_dispenser cooldown 12"; end
|
|
end
|
|
-- use_item,name=elementium_pocket_anvil,use_off_gcd=1
|
|
if I.ElementiumPocketAnvil:IsEquippedAndReady() then
|
|
if Cast(I.ElementiumPocketAnvil, nil, Settings.Commons.DisplayStyle.Trinkets, not Target:IsInRange(100)) then return "elementium_pocket_anvil cooldown 14"; end
|
|
end
|
|
end
|
|
-- elysian_decree,if=(active_enemies>desired_targets|raid_event.adds.in>30)
|
|
if CDsON() and S.ElysianDecree:IsCastable() then
|
|
if Cast(S.ElysianDecree, nil, Settings.Commons.DisplayStyle.Signature, not Target:IsInRange(30)) then return "elysian_decree cooldown 16"; end
|
|
end
|
|
if Settings.Commons.Enabled.Trinkets then
|
|
local Trinket1ToUse, _, Trinket1Range = Player:GetUseableItems(OnUseExcludes, 13)
|
|
local Trinket2ToUse, _, Trinket2Range = Player:GetUseableItems(OnUseExcludes, 14)
|
|
-- use_items,slots=trinket1,if=(variable.trinket_sync_slot=1&(buff.metamorphosis.up|(!talent.demonic.enabled&cooldown.metamorphosis.remains>(fight_remains>?trinket.1.cooldown.duration%2))|fight_remains<=20)|(variable.trinket_sync_slot=2&!trinket.2.cooldown.ready)|!variable.trinket_sync_slot)&(!talent.initiative|buff.initiative.up)
|
|
if Trinket1ToUse and ((VarTrinketSyncSlot == 1 and (Player:BuffUp(S.MetamorphosisBuff) or ((not S.Demonic:IsAvailable()) and S.Metamorphosis:CooldownRemains() > (mathmin(FightRemains, Trinket1ToUse:Cooldown()))) or FightRemains <= 20) or (VarTrinketSyncSlot == 2 and not Trinket2ToUse) or VarTrinketSyncSlot == 0) and ((not S.Initiative:IsAvailable()) or Player:BuffUp(S.InitiativeBuff))) then
|
|
if Cast(Trinket1ToUse, nil, Settings.Commons.DisplayStyle.Trinkets, not Target:IsInRange(Trinket1Range)) then return "trinket1 cooldown 18"; end
|
|
end
|
|
-- use_items,slots=trinket2,if=(variable.trinket_sync_slot=2&(buff.metamorphosis.up|(!talent.demonic.enabled&cooldown.metamorphosis.remains>(fight_remains>?trinket.2.cooldown.duration%2))|fight_remains<=20)|(variable.trinket_sync_slot=1&!trinket.1.cooldown.ready)|!variable.trinket_sync_slot)&(!talent.initiative|buff.initiative.up)
|
|
if Trinket2ToUse and ((VarTrinketSyncSlot == 2 and (Player:BuffUp(S.MetamorphosisBuff) or ((not S.Demonic:IsAvailable()) and S.Metamorphosis:CooldownRemains() > (mathmin(FightRemains, Trinket2ToUse:Cooldown()))) or FightRemains <= 20) or (VarTrinketSyncSlot == 1 and not Trinket1ToUse) or VarTrinketSyncSlot == 0) and ((not S.Initiative:IsAvailable()) or Player:BuffUp(S.InitiativeBuff))) then
|
|
if Cast(Trinket2ToUse, nil, Settings.Commons.DisplayStyle.Trinkets, not Target:IsInRange(Trinket2Range)) then return "trinket2 cooldown 20"; end
|
|
end
|
|
-- use_item,name=stormeaters_boon,use_off_gcd=1,if=cooldown.metamorphosis.remains&(!talent.momentum|buff.momentum.remains>5)&(active_enemies>1|raid_event.adds.in>140)
|
|
if I.StormEatersBoon:IsEquippedAndReady() and (S.Metamorphosis:CooldownDown() and ((not S.Momentum:IsAvailable()) or Player:BuffRemains(S.MomentumBuff) > 5)) then
|
|
if Cast(I.StormEatersBoon, nil, Settings.Commons.DisplayStyle.Trinkets) then return "stormeaters_boon cooldown 22"; end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- ======= ACTION LISTS =======
|
|
local function APL()
|
|
if AoEON() then
|
|
Enemies8y = Player:GetEnemiesInMeleeRange(8) -- Multiple Abilities
|
|
Enemies20y = Player:GetEnemiesInMeleeRange(20) -- Eye Beam
|
|
EnemiesCount8 = #Enemies8y
|
|
EnemiesCount20 = #Enemies20y
|
|
else
|
|
EnemiesCount8 = 1
|
|
EnemiesCount20 = 1
|
|
end
|
|
|
|
if Everyone.TargetIsValid() or Player:AffectingCombat() then
|
|
-- Calculate fight_remains
|
|
BossFightRemains = HL.BossFightRemains(nil, true)
|
|
FightRemains = BossFightRemains
|
|
if FightRemains == 11111 then
|
|
FightRemains = HL.FightRemains(Enemies8y, false)
|
|
end
|
|
|
|
-- Calculate gcd.max
|
|
GCDMax = Player:GCD() + 0.25
|
|
|
|
-- Calculate CombatTime
|
|
CombatTime = HL.CombatTime()
|
|
end
|
|
|
|
if Everyone.TargetIsValid() then
|
|
-- Precombat
|
|
if not Player:AffectingCombat() then
|
|
local ShouldReturn = Precombat(); if ShouldReturn then return ShouldReturn; end
|
|
end
|
|
-- Manually added: Defensive Blur
|
|
if S.Blur:IsCastable() and Player:HealthPercentage() <= Settings.Havoc.BlurHealthThreshold then
|
|
if Cast(S.Blur, Settings.Havoc.OffGCDasOffGCD.Blur) then return "blur defensive"; end
|
|
end
|
|
-- auto_attack
|
|
-- retarget_auto_attack,line_cd=1,target_if=min:debuff.burning_wound.remains,if=talent.burning_wound&talent.demon_blades&active_dot.burning_wound<(spell_targets>?3)
|
|
-- retarget_auto_attack,line_cd=1,target_if=min:!target.is_boss,if=talent.burning_wound&talent.demon_blades&active_dot.burning_wound=(spell_targets>?3)
|
|
-- variable,name=blade_dance,value=talent.first_blood|talent.trail_of_ruin|talent.chaos_theory&buff.chaos_theory.down|spell_targets.blade_dance1>1
|
|
VarBladeDance = (S.FirstBlood:IsAvailable() or S.TrailofRuin:IsAvailable() or S.ChaosTheory:IsAvailable() and Player:BuffDown(S.ChaosTheoryBuff) or EnemiesCount8 > 1)
|
|
-- variable,name=pooling_for_blade_dance,value=variable.blade_dance&fury<(75-talent.demon_blades*20)&cooldown.blade_dance.remains<gcd.max
|
|
VarPoolingForBladeDance = (VarBladeDance and Player:Fury() < (75 - num(S.DemonBlades:IsAvailable()) * 20) and S.BladeDance:CooldownRemains() < GCDMax)
|
|
-- variable,name=pooling_for_eye_beam,value=talent.demonic&!talent.blind_fury&cooldown.eye_beam.remains<(gcd.max*2)&fury.deficit>20
|
|
VarPoolingForEyeBeam = S.Demonic:IsAvailable() and (not S.BlindFury:IsAvailable()) and S.EyeBeam:CooldownRemains() < (GCDMax * 2) and Player:FuryDeficit() > 20
|
|
-- variable,name=waiting_for_momentum,value=talent.momentum&!buff.momentum.up
|
|
VarWaitingForMomentum = S.Momentum:IsAvailable() and Player:BuffDown(S.MomentumBuff)
|
|
-- variable,name=holding_meta,value=(talent.demonic&talent.essence_break)&variable.3min_trinket&fight_remains>cooldown.metamorphosis.remains+30+talent.shattered_destiny*60&cooldown.metamorphosis.remains<20&cooldown.metamorphosis.remains>action.eye_beam.execute_time+gcd.max*(talent.inner_demon+2)
|
|
local EyeBeamExecuteTime = mathmax(S.EyeBeam:BaseDuration(), Player:GCD())
|
|
VarHoldingMeta = (S.Demonic:IsAvailable() and S.EssenceBreak:IsAvailable()) and Var3MinTrinket and FightRemains > S.Metamorphosis:CooldownRemains() + 30 + num(S.ShatteredDestiny:IsAvailable()) * 60 and S.Metamorphosis:CooldownRemains() < 20 and S.Metamorphosis:CooldownRemains() > EyeBeamExecuteTime + GCDMax * (num(S.InnerDemon:IsAvailable()) + 2)
|
|
-- invoke_external_buff,name=power_infusion,if=time>170&!variable.holding_meta
|
|
-- Note: Not handling external buffs
|
|
-- immolation_aura,if=talent.ragefire&active_enemies>=3&(cooldown.blade_dance.remains|debuff.essence_break.down)
|
|
if S.ImmolationAura:IsCastable() and (S.Ragefire:IsAvailable() and EnemiesCount8 >= 3 and (S.BladeDance:CooldownDown() or Target:DebuffDown(S.EssenceBreakDebuff))) then
|
|
if Cast(S.ImmolationAura, Settings.Havoc.GCDasOffGCD.ImmolationAura, nil, not IsInMeleeRange(8)) then return "immolation_aura main 2"; end
|
|
end
|
|
-- throw_glaive,if=talent.soulrend&talent.furious_throws&active_enemies>=3&time<1
|
|
-- Note: Moved time check to the front
|
|
if CombatTime < 1 and S.ThrowGlaive:IsCastable() and (S.Soulrend:IsAvailable() and S.FuriousThrows:IsAvailable() and EnemiesCount8 >= 3) then
|
|
if Cast(S.ThrowGlaive, Settings.Havoc.GCDasOffGCD.ThrowGlaive, nil, not Target:IsSpellInRange(S.ThrowGlaive)) then return "throw_glaive main 4"; end
|
|
end
|
|
-- disrupt (and stun interrupts)
|
|
local ShouldReturn = Everyone.Interrupt(10, S.Disrupt, Settings.Commons.OffGCDasOffGCD.Disrupt, StunInterrupts); if ShouldReturn then return ShouldReturn; end
|
|
-- fel_rush,if=buff.unbound_chaos.up&(buff.unbound_chaos.remains<gcd.max*2|target.time_to_die<gcd.max*2)
|
|
if S.FelRush:IsCastable() and UseFelRush() and (Player:BuffUp(S.UnboundChaosBuff) and (Player:BuffRemains(S.UnboundChaosBuff) < GCDMax * 2 or Target:TimeToDie() < GCDMax * 2)) then
|
|
if Cast(S.FelRush, nil, Settings.Commons.DisplayStyle.FelRush) then return "fel_rush main 6"; end
|
|
end
|
|
-- call_action_list,name=cooldown
|
|
-- Note: CDsON check is within Cooldown(), as the function also includes trinkets and potions
|
|
if (true) then
|
|
local ShouldReturn = Cooldown(); if ShouldReturn then return ShouldReturn; end
|
|
end
|
|
-- call_action_list,name=meta_end,if=buff.metamorphosis.up&buff.metamorphosis.remains<gcd.max&active_enemies<3
|
|
if Player:BuffUp(S.MetamorphosisBuff) and Player:BuffRemains(S.MetamorphosisBuff) < GCDMax and EnemiesCount8 < 3 then
|
|
local ShouldReturn = MetaEnd(); if ShouldReturn then return ShouldReturn; end
|
|
end
|
|
-- pick_up_fragment,type=demon,if=demon_soul_fragments>0&(cooldown.eye_beam.remains<6|buff.metamorphosis.remains>5)&buff.empowered_demon_soul.remains<3|fight_remains<17
|
|
-- pick_up_fragment,mode=nearest,if=talent.demonic_appetite&fury.deficit>=35&(!cooldown.eye_beam.ready|fury<30)
|
|
-- TODO: Can't detect when orbs actually spawn, we could possibly show a suggested icon when we DON'T want to pick up souls so people can avoid moving?
|
|
-- annihilation,if=buff.inner_demon.up&cooldown.metamorphosis.remains<=gcd*3
|
|
if S.Annihilation:IsReady() and (Player:BuffUp(S.InnerDemonBuff) and S.Metamorphosis:CooldownRemains() <= Player:GCD() * 3) then
|
|
if Cast(S.Annihilation, nil, nil, not IsInMeleeRange(5)) then return "annihilation main 8"; end
|
|
end
|
|
-- vengeful_retreat,use_off_gcd=1,if=talent.initiative&talent.essence_break&time>1&(cooldown.essence_break.remains>15|cooldown.essence_break.remains<gcd.max&(!talent.demonic|buff.metamorphosis.up|cooldown.eye_beam.remains>15+(10*talent.cycle_of_hatred)))&(time<30|gcd.remains-1<0)&!talent.any_means_necessary
|
|
if S.VengefulRetreat:IsCastable() and (S.Initiative:IsAvailable() and S.EssenceBreak:IsAvailable() and CombatTime > 1 and (S.EssenceBreak:CooldownRemains() > 15 or S.EssenceBreak:CooldownRemains() < Player:GCD() + 0.5 and ((not S.Demonic:IsAvailable()) or Player:BuffUp(S.MetamorphosisBuff) or S.EyeBeam:CooldownRemains() > 15 + (10 * num(S.CycleofHatred:IsAvailable())))) and (CombatTime < 30 or Player:GCDRemains() - 1 < 0) and not S.AnyMeansNecessary:IsAvailable()) then
|
|
if Cast(S.VengefulRetreat, Settings.Havoc.OffGCDasOffGCD.VengefulRetreat) then return "vengeful_retreat main 10"; end
|
|
end
|
|
-- vengeful_retreat,use_off_gcd=1,if=talent.initiative&talent.essence_break&time>1&(cooldown.essence_break.remains>15|cooldown.essence_break.remains<gcd.max*2&(buff.initiative.remains<gcd.max&!variable.holding_meta&cooldown.eye_beam.remains=gcd.remains&(raid_event.adds.in>(40-talent.cycle_of_hatred*15))&fury>30|!talent.demonic|buff.metamorphosis.up|cooldown.eye_beam.remains>15+(10*talent.cycle_of_hatred)))&talent.any_means_necessary
|
|
if S.VengefulRetreat:IsCastable() and (S.Initiative:IsAvailable() and S.EssenceBreak:IsAvailable() and CombatTime > 1 and (S.EssenceBreak:CooldownRemains() > 15 or S.EssenceBreak:CooldownRemains() < GCDMax * 2 and (Player:BuffRemains(S.InitiativeBuff) < GCDMax and (not VarHoldingMeta) and S.EyeBeam:CooldownRemains() <= Player:GCDRemains() and Player:Fury() > 30 or (not S.Demonic:IsAvailable()) or Player:BuffUp(S.MetamorphosisBuff) or S.EyeBeam:CooldownRemains() > 15 + (10 * num(S.CycleofHatred:IsAvailable())))) and S.AnyMeansNecessary:IsAvailable()) then
|
|
if Cast(S.VengefulRetreat, Settings.Havoc.OffGCDasOffGCD.VengefulRetreat) then return "vengeful_retreat main 12"; end
|
|
end
|
|
-- vengeful_retreat,use_off_gcd=1,if=talent.initiative&!talent.essence_break&time>1&!buff.momentum.up
|
|
if S.VengefulRetreat:IsCastable() and (S.Initiative:IsAvailable() and (not S.EssenceBreak:IsAvailable()) and CombatTime > 1 and Player:BuffDown(S.MomentumBuff)) then
|
|
if Cast(S.VengefulRetreat, Settings.Havoc.OffGCDasOffGCD.VengefulRetreat) then return "vengeful_retreat main 14"; end
|
|
end
|
|
-- wait,sec=buff.out_of_range.remains,if=buff.out_of_range.up&buff.out_of_range.remains<gcd.max
|
|
-- fel_rush,if=talent.momentum.enabled&buff.momentum.remains<gcd.max*2&(charges_fractional>1.8|cooldown.eye_beam.remains<3)&debuff.essence_break.down
|
|
if S.FelRush:IsCastable() and (S.Momentum:IsAvailable() and Player:BuffRemains(S.MomentumBuff) < GCDMax * 2 and (S.FelRush:ChargesFractional() > 1.8 or S.EyeBeam:CooldownRemains() < 3) and Target:DebuffDown(S.EssenceBreakDebuff)) then
|
|
if Cast(S.FelRush, nil, Settings.Commons.DisplayStyle.FelRush) then return "fel_rush main 16"; end
|
|
end
|
|
-- essence_break,if=(active_enemies>desired_targets|raid_event.adds.in>40)&!variable.waiting_for_momentum&(buff.metamorphosis.up)&(!talent.tactical_retreat|buff.tactical_retreat.up)|fight_remains<6
|
|
if S.EssenceBreak:IsCastable() and ((not VarWaitingForMomentum) and Player:BuffUp(S.MetamorphosisBuff) and ((not S.TacticalRetreat:IsAvailable()) or Player:BuffUp(S.TacticalRetreatBuff)) or FightRemains < 6) then
|
|
if Cast(S.EssenceBreak, nil, nil, not IsInMeleeRange(10)) then return "essence_break main 18"; end
|
|
end
|
|
-- death_sweep,if=variable.blade_dance&(!talent.essence_break|cooldown.essence_break.remains>(cooldown.death_sweep.duration-4))
|
|
if S.DeathSweep:IsReady() and (VarBladeDance and ((not S.EssenceBreak:IsAvailable()) or S.EssenceBreak:CooldownRemains() > ((9 * Player:SpellHaste()) - 4))) then
|
|
if Cast(S.DeathSweep, nil, nil, not IsInMeleeRange(8)) then return "death_sweep main 20"; end
|
|
end
|
|
-- fel_barrage,if=active_enemies>desired_targets|raid_event.adds.in>30
|
|
if S.FelBarrage:IsCastable() then
|
|
if Cast(S.FelBarrage, nil, nil, not IsInMeleeRange(8)) then return "fel_barrage main 22"; end
|
|
end
|
|
-- glaive_tempest,if=(active_enemies>desired_targets|raid_event.adds.in>10)&(debuff.essence_break.down|active_enemies>1)
|
|
if S.GlaiveTempest:IsReady() and (Target:DebuffDown(S.EssenceBreakDebuff) or EnemiesCount8 > 1) then
|
|
if Cast(S.GlaiveTempest, Settings.Havoc.GCDasOffGCD.GlaiveTempest) then return "glaive_tempest main 24"; end
|
|
end
|
|
-- annihilation,if=buff.inner_demon.up&cooldown.eye_beam.remains<=gcd
|
|
if S.Annihilation:IsReady() and (Player:BuffUp(S.InnerDemonBuff) and S.EyeBeam:CooldownRemains() <= Player:GCD()) then
|
|
if Cast(S.Annihilation, nil, nil, not IsInMeleeRange(5)) then return "annihilation main 26"; end
|
|
end
|
|
-- fel_rush,if=talent.momentum.enabled&cooldown.eye_beam.remains<gcd.max*3&buff.momentum.remains<5&buff.metamorphosis.down
|
|
if S.FelRush:IsCastable() and UseFelRush() and (S.Momentum:IsAvailable() and S.EyeBeam:CooldownRemains() < GCDMax * 3 and Player:BuffRemains(S.MomentumBuff) < 5 and Player:BuffDown(S.MetamorphosisBuff)) then
|
|
if Cast(S.FelRush, nil, Settings.Commons.DisplayStyle.FelRush) then return "fel_rush main 28"; end
|
|
end
|
|
-- the_hunt,if=debuff.essence_break.down&(time<10|cooldown.metamorphosis.remains>10|!equipped.algethar_puzzle_box)&(raid_event.adds.in>90|active_enemies>3|time_to_die<10)&(time>8&debuff.essence_break.down|!set_bonus.tier30_2pc)
|
|
if S.TheHunt:IsCastable() and (Target:DebuffDown(S.EssenceBreakDebuff) and (CombatTime < 10 or S.Metamorphosis:CooldownRemains() > 10 or not I.AlgetharPuzzleBox:IsEquipped()) and (EnemiesCount8 == 1 or EnemiesCount8 > 3 or FightRemains < 10) and (CombatTime > 8 and Target:DebuffDown(S.EssenceBreakDebuff) or not Player:HasTier(30, 2))) then
|
|
if Cast(S.TheHunt, nil, Settings.Commons.DisplayStyle.Signature, not Target:IsSpellInRange(S.TheHunt)) then return "the_hunt main 30"; end
|
|
end
|
|
-- throw_glaive,if=talent.serrated_glaive&cooldown.eye_beam.remains<6&!debuff.serrated_glaive.up&!debuff.essence_break.up&cooldown.blade_dance.remains
|
|
if S.ThrowGlaive:IsCastable() and (S.SerratedGlaive:IsAvailable() and S.EyeBeam:CooldownRemains() < 6 and Target:DebuffDown(S.SerratedGlaiveDebuff) and Target:DebuffDown(S.EssenceBreakDebuff) and S.BladeDance:CooldownDown()) then
|
|
if Cast(S.ThrowGlaive, Settings.Havoc.GCDasOffGCD.ThrowGlaive, nil, not Target:IsSpellInRange(S.ThrowGlaive)) then return "throw_glaive main 32"; end
|
|
end
|
|
-- eye_beam,if=active_enemies>desired_targets|raid_event.adds.in>(40-talent.cycle_of_hatred*15)&!debuff.essence_break.up&(cooldown.metamorphosis.remains>40-talent.cycle_of_hatred*15|!variable.holding_meta)&(buff.metamorphosis.down|buff.metamorphosis.remains>gcd.max|!talent.restless_hunter)&(buff.metamorphosis.down|cooldown.blade_dance.remains>gcd.max)|fight_remains<15
|
|
if S.EyeBeam:IsReady() and (Target:DebuffDown(S.EssenceBreakDebuff) and (S.Metamorphosis:CooldownRemains() > 40 - num(S.CycleofHatred:IsAvailable()) * 15 or not VarHoldingMeta) and (Player:BuffDown(S.MetamorphosisBuff) or Player:BuffRemains(S.MetamorphosisBuff) > GCDMax or not S.RestlessHunter:IsAvailable()) and (Player:BuffDown(S.MetamorphosisBuff) or S.BladeDance:CooldownRemains() > GCDMax) or FightRemains < 15) then
|
|
if Cast(S.EyeBeam, Settings.Havoc.GCDasOffGCD.EyeBeam, nil, not IsInMeleeRange(20)) then return "eye_beam main 34"; end
|
|
end
|
|
-- blade_dance,if=variable.blade_dance&(cooldown.eye_beam.remains>5|equipped.algethar_puzzle_box&cooldown.metamorphosis.remains>(cooldown.blade_dance.duration)|!talent.demonic|(raid_event.adds.in>cooldown&raid_event.adds.in<25))
|
|
if S.BladeDance:IsReady() and (VarBladeDance and (S.EyeBeam:CooldownRemains() > 5 or I.AlgetharPuzzleBox:IsEquipped() and S.Metamorphosis:CooldownRemains() > 10 * Player:SpellHaste() or (not S.Demonic:IsAvailable()))) then
|
|
if Cast(S.BladeDance, nil, nil, not IsInMeleeRange(8)) then return "blade_dance main 36"; end
|
|
end
|
|
-- sigil_of_flame,if=talent.any_means_necessary&debuff.essence_break.down&active_enemies>=4
|
|
if S.SigilofFlame:IsCastable() and (S.AnyMeansNecessary:IsAvailable() and Target:DebuffDown(S.EssenceBreakDebuff) and EnemiesCount8 >= 4) then
|
|
if Cast(S.SigilofFlame, Settings.Havoc.GCDasOffGCD.SigilOfFlame, nil, not Target:IsInRange(30)) then return "sigil_of_flame main 38"; end
|
|
end
|
|
-- throw_glaive,if=talent.soulrend&(active_enemies>desired_targets|raid_event.adds.in>full_recharge_time+9)&spell_targets>=(2-talent.furious_throws)&!debuff.essence_break.up&(full_recharge_time<gcd.max*3|active_enemies>1)
|
|
if S.ThrowGlaive:IsCastable() and (S.Soulrend:IsAvailable() and EnemiesCount8 >= (2 - num(S.FuriousThrows:IsAvailable())) and Target:DebuffDown(S.EssenceBreakDebuff) and (S.ThrowGlaive:FullRechargeTime() < GCDMax * 3 or EnemiesCount8 > 1)) then
|
|
if Cast(S.ThrowGlaive, Settings.Havoc.GCDasOffGCD.ThrowGlaive, nil, not Target:IsSpellInRange(S.ThrowGlaive)) then return "throw_glaive main 40"; end
|
|
end
|
|
-- immolation_aura,if=fury<70&debuff.essence_break.down&time_to_die>3
|
|
if S.ImmolationAura:IsCastable() and (Player:Fury() < 70 and Target:DebuffDown(S.EssenceBreakDebuff) and FightRemains > 3) then
|
|
if Cast(S.ImmolationAura, Settings.Havoc.GCDasOffGCD.ImmolationAura, nil, not IsInMeleeRange(8)) then return "immolation_aura main 42"; end
|
|
end
|
|
-- sigil_of_flame,if=talent.any_means_necessary&debuff.essence_break.down
|
|
if S.SigilofFlame:IsCastable() and (S.AnyMeansNecessary:IsAvailable() and Target:DebuffDown(S.EssenceBreakDebuff)) then
|
|
if Cast(S.SigilofFlame, Settings.Havoc.GCDasOffGCD.SigilOfFlame, nil, not Target:IsInRange(30)) then return "sigil_of_flame main 44"; end
|
|
end
|
|
-- annihilation,if=!variable.pooling_for_blade_dance
|
|
if S.Annihilation:IsReady() and (not VarPoolingForBladeDance) then
|
|
if Cast(S.Annihilation, nil, nil, not IsInMeleeRange(5)) then return "annihilation main 46"; end
|
|
end
|
|
-- throw_glaive,if=talent.soulrend&(active_enemies>desired_targets|raid_event.adds.in>full_recharge_time+9)&spell_targets>=(2-talent.furious_throws)&!debuff.essence_break.up
|
|
if S.ThrowGlaive:IsReady() and (S.Soulrend:IsAvailable() and EnemiesCount8 >= (2 - num(S.FuriousThrows)) and Target:DebuffDown(S.EssenceBreakDebuff)) then
|
|
if Cast(S.ThrowGlaive, Settings.Havoc.GCDasOffGCD.ThrowGlaive, nil, not Target:IsSpellInRange(S.ThrowGlaive)) then return "throw_glaive main 48"; end
|
|
end
|
|
-- immolation_aura,if=!buff.immolation_aura.up&(!talent.ragefire|active_enemies>desired_targets|raid_event.adds.in>15)
|
|
if S.ImmolationAura:IsCastable() and (Player:BuffDown(S.ImmolationAuraBuff)) then
|
|
if Cast(S.ImmolationAura, Settings.Havoc.GCDasOffGCD.ImmolationAura, nil, not IsInMeleeRange(8)) then return "immolation_aura main 50"; end
|
|
end
|
|
-- fel_rush,if=talent.isolated_prey&active_enemies=1&fury.deficit>=35
|
|
if S.FelRush:IsCastable() and UseFelRush() and (S.IsolatedPrey:IsAvailable() and EnemiesCount8 == 1 and Player:FuryDeficit() >= 35) then
|
|
if Cast(S.FelRush, nil, Settings.Commons.DisplayStyle.FelRush) then return "fel_rush main 52"; end
|
|
end
|
|
-- chaos_strike,if=!variable.pooling_for_blade_dance&!variable.pooling_for_eye_beam
|
|
if S.ChaosStrike:IsReady() and ((not VarPoolingForBladeDance) and not VarPoolingForEyeBeam) then
|
|
if Cast(S.ChaosStrike, nil, nil, not Target:IsSpellInRange(S.ChaosStrike)) then return "chaos_strike main 54"; end
|
|
end
|
|
-- sigil_of_flame,if=raid_event.adds.in>15&fury.deficit>=30
|
|
if S.SigilofFlame:IsCastable() and (EnemiesCount8 == 1 and Player:FuryDeficit() >= 30) then
|
|
if Cast(S.SigilofFlame, Settings.Havoc.GCDasOffGCD.SigilOfFlame, nil, not Target:IsInRange(30)) then return "sigil_of_flame main 56"; end
|
|
end
|
|
-- felblade,if=fury.deficit>=40
|
|
if S.Felblade:IsCastable() and (Player:FuryDeficit() >= 40) then
|
|
if Cast(S.Felblade, nil, nil, not Target:IsSpellInRange(S.Felblade)) then return "felblade main 58"; end
|
|
end
|
|
-- fel_rush,if=!talent.momentum&talent.demon_blades&!cooldown.eye_beam.ready&(charges=2|(raid_event.movement.in>10&raid_event.adds.in>10))
|
|
if S.FelRush:IsCastable() and ((not S.Momentum:IsAvailable()) and S.DemonBlades:IsAvailable() and S.EyeBeam:CooldownDown() and UseFelRush()) then
|
|
if Cast(S.FelRush, nil, Settings.Commons.DisplayStyle.FelRush) then return "fel_rush main 60"; end
|
|
end
|
|
-- demons_bite,target_if=min:debuff.burning_wound.remains,if=talent.burning_wound&debuff.burning_wound.remains<4&active_dot.burning_wound<(spell_targets>?3)
|
|
if S.DemonsBite:IsCastable() then
|
|
if Everyone.CastTargetIf(S.DemonsBite, Enemies8y, "min", EvalutateTargetIfFilterDemonsBite, EvaluateTargetIfDemonsBite, not Target:IsSpellInRange(S.DemonsBite)) then return "demons_bite main 62"; end
|
|
end
|
|
-- fel_rush,if=!talent.momentum&!talent.demon_blades&spell_targets>1&(charges=2|(raid_event.movement.in>10&raid_event.adds.in>10))
|
|
if S.FelRush:IsCastable() and ((not S.Momentum:IsAvailable()) and (not S.DemonBlades:IsAvailable()) and EnemiesCount8 > 1 and UseFelRush()) then
|
|
if Cast(S.FelRush, nil, Settings.Commons.DisplayStyle.FelRush) then return "fel_rush main 64"; end
|
|
end
|
|
-- sigil_of_flame,if=raid_event.adds.in>15&fury.deficit>=30
|
|
if S.SigilofFlame:IsCastable() and (Player:FuryDeficit() >= 30) then
|
|
if Cast(S.SigilofFlame, Settings.Havoc.GCDasOffGCD.SigilOfFlame, nil, not Target:IsInRange(30)) then return "sigil_of_flame main 66"; end
|
|
end
|
|
-- demons_bite
|
|
if S.DemonsBite:IsCastable() then
|
|
if Cast(S.DemonsBite, nil, nil, not Target:IsSpellInRange(S.DemonsBite)) then return "demons_bite main 68"; end
|
|
end
|
|
-- fel_rush,if=movement.distance>15|(buff.out_of_range.up&!talent.momentum)
|
|
if S.FelRush:IsCastable() and ((not IsInMeleeRange()) and (not S.Momentum:IsAvailable()) and UseFelRush()) then
|
|
if Cast(S.FelRush, nil, Settings.Commons.DisplayStyle.FelRush) then return "fel_rush main 70"; end
|
|
end
|
|
-- vengeful_retreat,if=!talent.initiative&movement.distance>15
|
|
if S.VengefulRetreat:IsCastable() and ((not S.Initiative:IsAvailable()) and (not IsInMeleeRange())) then
|
|
if Cast(S.VengefulRetreat, Settings.Havoc.OffGCDasOffGCD.VengefulRetreat) then return "vengeful_retreat main 72"; end
|
|
end
|
|
-- throw_glaive,if=(talent.demon_blades.enabled|buff.out_of_range.up)&!debuff.essence_break.up
|
|
if S.ThrowGlaive:IsReady() and ((S.DemonBlades:IsAvailable() or not Target:IsInRange(12)) and Target:DebuffDown(S.EssenceBreakDebuff)) then
|
|
if Cast(S.ThrowGlaive, Settings.Havoc.GCDasOffGCD.ThrowGlaive, nil, not Target:IsSpellInRange(S.ThrowGlaive)) then return "throw_glaive main 74"; end
|
|
end
|
|
-- Show pool icon if nothing else to do (should only happen when Demon Blades is used)
|
|
if (S.DemonBlades:IsAvailable()) then
|
|
if Cast(S.Pool) then return "pool demon_blades"; end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function Init()
|
|
S.BurningWoundDebuff:RegisterAuraTracking()
|
|
|
|
HR.Print("Havoc DH rotation is currently a work in progress, but has been updated for patch 10.1.0.")
|
|
end
|
|
|
|
HR.SetAPL(577, APL, Init)
|
|
|