--- ============================ HEADER ============================ --- ======= LOCALIZE ======= -- Addon local addonName, addonTable = ... -- HeroDBC local DBC = HeroDBC.DBC -- HeroLib local HL = HeroLib local Unit = HL.Unit local Player = Unit.Player local Target = Unit.Target local Spell = HL.Spell local Item = HL.Item -- HeroRotation local HR = HeroRotation local Cast = HR.Cast local AoEON = HR.AoEON local CDsON = HR.CDsON -- Num/Bool Helper Functions local num = HR.Commons.Everyone.num local bool = HR.Commons.Everyone.bool -- lua local mathfloor = math.floor --- ============================ CONTENT =========================== --- ======= APL LOCALS ======= -- luacheck: max_line_length 9999 -- Spells local S = Spell.Warrior.Protection local I = Item.Warrior.Protection -- Create table to exclude above trinkets from On Use function local OnUseExcludes = { } -- Variables local TargetInMeleeRange -- Enemies Variables local Enemies8y local EnemiesCount8 -- GUI Settings local Everyone = HR.Commons.Everyone local Settings = { General = HR.GUISettings.General, Commons = HR.GUISettings.APL.Warrior.Commons, Protection = HR.GUISettings.APL.Warrior.Protection } -- Stuns local StunInterrupts = { {S.IntimidatingShout, "Cast Intimidating Shout (Interrupt)", function () return true; end}, } local function IsCurrentlyTanking() return Player:IsTankingAoE(16) or Player:IsTanking(Target) or Target:IsDummy() end local function IgnorePainWillNotCap() if Player:BuffUp(S.IgnorePain) then local absorb = Player:AttackPowerDamageMod() * 3.5 * (1 + Player:VersatilityDmgPct() / 100) local spellTable = Player:AuraInfo(S.IgnorePain, nil, true) local IPAmount = spellTable.points[1] --return IPAmount < (0.5 * mathfloor(absorb * 1.3)) -- Ignore Pain appears to cap at 2 times its absorb value now return IPAmount < absorb else return true end end local function IgnorePainValue() if Player:BuffUp(S.IgnorePain) then local IPBuffInfo = Player:BuffInfo(S.IgnorePain, nil, true) return IPBuffInfo.points[1] else return 0 end end local function ShouldPressShieldBlock() -- shield_block,if=buff.shield_block.duration<=18&talent.enduring_defenses.enabled|buff.shield_block.duration<=12 return IsCurrentlyTanking() and S.ShieldBlock:IsReady() and (Player:BuffRemains(S.ShieldBlockBuff) <= 18 and S.EnduringDefenses:IsAvailable() or Player:BuffRemains(S.ShieldBlockBuff) <= 12) end -- A bit of logic to decide whether to pre-cast-rage-dump on ignore pain. local function SuggestRageDump(RageFromSpell) -- Get RageMax from setting (default 80) local RageMax = Settings.Protection.RageCapValue -- If the setting value is lower than 35, it's not possible to cast Ignore Pain, so just return false if (RageMax < 35 or Player:Rage() < 35) then return false end local shouldPreRageDump = false -- Make sure we have enough Rage to cast IP, that it's not on CD, and that we shouldn't use Shield Block local AbleToCastIP = (Player:Rage() >= 35 and not ShouldPressShieldBlock()) if AbleToCastIP and (Player:Rage() + RageFromSpell >= RageMax or S.DemoralizingShout:IsReady()) then -- should pre-dump rage into IP if rage + RageFromSpell >= RageMax or Demo Shout is ready shouldPreRageDump = true end if shouldPreRageDump then if IsCurrentlyTanking() and IgnorePainWillNotCap() then if Cast(S.IgnorePain, nil, Settings.Protection.DisplayStyle.Defensive) then return "ignore_pain rage capped"; end else if Cast(S.Revenge, nil, nil, not TargetInMeleeRange) then return "revenge rage capped"; end end end end local function Precombat() -- flask -- food -- augmentation -- snapshot_stats -- Manually added: Group buff check if S.BattleShout:IsCastable() and (Player:BuffDown(S.BattleShoutBuff, true) or Everyone.GroupBuffMissing(S.BattleShoutBuff)) then if Cast(S.BattleShout, Settings.Commons.GCDasOffGCD.BattleShout) then return "battle_shout precombat"; 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"; end end -- Manually added opener if Target:IsInMeleeRange(12) then if S.ThunderClap:IsCastable() then if Cast(S.ThunderClap) then return "thunder_clap precombat"; end end else if S.Charge:IsCastable() and not Target:IsInRange(8) then if Cast(S.Charge, nil, nil, not Target:IsSpellInRange(S.Charge)) then return "charge precombat"; end end end end local function Aoe() --thunder_clap,if=dot.rend.remains<=1 if S.ThunderClap:IsCastable() and Target:DebuffRemains(S.RendDebuff) <= 1 then SuggestRageDump(5) if Cast(S.ThunderClap, nil, nil, not Target:IsInMeleeRange(8)) then return "thunder_clap aoe 2"; end end --thunder_clap,if=buff.violent_outburst.up&spell_targets.thunderclap>5&buff.avatar.up&talent.unstoppable_force.enabled if S.ThunderClap:IsCastable() and Player:BuffUp(S.ViolentOutburstBuff) and EnemiesCount8 > 5 and Player:BuffUp(S.AvatarBuff) and S.UnstoppableForce:IsAvailable() then SuggestRageDump(5) if Cast(S.ThunderClap, nil, nil, not Target:IsInMeleeRange(8)) then return "thunder_clap aoe 4"; end end --revenge,if=rage>=70&talent.seismic_reverberation.enabled&spell_targets.revenge>=3 if S.Revenge:IsReady() and Player:Rage() >= 70 and S.SeismicReverberation:IsAvailable() and EnemiesCount8 >= 3 then if Cast(S.Revenge, nil, nil, not TargetInMeleeRange) then return "revenge aoe 6"; end end --shield_slam,if=rage<=60|buff.violent_outburst.up&spell_targets.thunderclap<=4 if S.ShieldSlam:IsCastable() and (Player:Rage() <= 60 or Player:BuffUp(S.ViolentOutburstBuff) and EnemiesCount8 <= 4) then SuggestRageDump(20) if Cast(S.ShieldSlam, nil, nil, not TargetInMeleeRange) then return "shield_slam aoe 8"; end end --thunder_clap if S.ThunderClap:IsCastable() then SuggestRageDump(5) if Cast(S.ThunderClap, nil, nil, not Target:IsInMeleeRange(8)) then return "thunder_clap aoe 10"; end end --revenge,if=rage>=30|rage>=40&talent.barbaric_training.enabled if S.Revenge:IsReady() and (Player:Rage() >= 30 or Player:Rage() >= 40 and S.BarbaricTraining:IsAvailable()) then if Cast(S.Revenge, nil, nil, not TargetInMeleeRange) then return "revenge aoe 12"; end end end local function Generic() -- shield_slam if S.ShieldSlam:IsCastable() then SuggestRageDump(20) if Cast(S.ShieldSlam, nil, nil, not TargetInMeleeRange) then return "shield_slam generic 2"; end end -- thunder_clap,if=dot.rend.remains<=1&buff.violent_outburst.down if S.ThunderClap:IsCastable() and Target:DebuffRemains(S.RendDebuff) <= 1 and Player:BuffDown(S.ViolentOutburstBuff) then SuggestRageDump(5) if Cast(S.ThunderClap, nil, nil, not Target:IsInMeleeRange(8)) then return "thunder_clap generic 4"; end end -- execute,if=buff.sudden_death.up&talent.sudden_death.enabled if S.Execute:IsReady() and Player:BuffUp(S.SuddenDeathBuff) and S.SuddenDeath:IsAvailable() then if Cast(S.Execute, nil, nil, not TargetInMeleeRange) then return "execute generic 6"; end end -- execute,if=spell_targets.revenge=1&(talent.massacre.enabled|talent.juggernaut.enabled)&rage>=50 if S.Execute:IsReady() and EnemiesCount8 == 1 and (S.Massacre:IsAvailable() or S.Juggernaut:IsAvailable()) and Player:Rage() >= 50 then if Cast(S.Execute, nil, nil, not TargetInMeleeRange) then return "execute generic 6"; end end -- execute,if=spell_targets.revenge=1&rage>=50 if S.Execute:IsReady() and EnemiesCount8 == 1 and Player:Rage() >= 50 then if Cast(S.Execute, nil, nil, not TargetInMeleeRange) then return "execute generic 10"; end end -- thunder_clap,if=(spell_targets.thunder_clap>1|cooldown.shield_slam.remains&!buff.violent_outburst.up) if S.ThunderClap:IsCastable() and (EnemiesCount8 > 1 or S.ShieldSlam:CooldownDown() and not Player:BuffUp(S.ViolentOutburstBuff)) then SuggestRageDump(5) if Cast(S.ThunderClap, nil, nil, not Target:IsInMeleeRange(8)) then return "thunder_clap generic 12"; end end --revenge,if= --(rage>=60&target.health.pct>20|buff.revenge.up&target.health.pct<=20&rage<=18&cooldown.shield_slam.remains|buff.revenge.up&target.health.pct>20) --|(rage>=60&target.health.pct>35|buff.revenge.up&target.health.pct<=35&rage<=18&cooldown.shield_slam.remains|buff.revenge.up&target.health.pct>35) --&talent.massacre.enabled if S.Revenge:IsReady() and ((Player:Rage() >= 60 and Target:HealthPercentage() > 20 or Player:BuffUp(S.RevengeBuff) and Target:HealthPercentage() <= 20 and Player:Rage() <= 18 and S.ShieldSlam:CooldownDown() or Player:BuffUp(S.RevengeBuff) and Target:HealthPercentage() > 20) or (Player:Rage() >= 60 and Target:HealthPercentage() > 35 or Player:BuffUp(S.RevengeBuff) and Target:HealthPercentage() <= 35 and Player:Rage() <= 18 and S.ShieldSlam:CooldownDown() or Player:BuffUp(S.RevengeBuff) and Target:HealthPercentage() > 35) and S.Massacre:IsAvailable()) then if Cast(S.Revenge, nil, nil, not TargetInMeleeRange) then return "revenge generic 14"; end end -- execute,if=spell_targets.revenge=1 if S.Execute:IsReady() and EnemiesCount8 == 1 then if Cast(S.Execute, nil, nil, not TargetInMeleeRange) then return "execute generic 16"; end end -- revenge if S.Revenge:IsReady() then if Cast(S.Revenge, nil, nil, not TargetInMeleeRange) then return "revenge generic 18"; end end -- thunder_clap,if=(spell_targets.thunder_clap>=1|cooldown.shield_slam.remains&buff.violent_outburst.up) if S.ThunderClap:IsCastable() and (EnemiesCount8 >= 1 or S.ShieldSlam:CooldownDown() and Player:BuffUp(S.ViolentOutburstBuff)) then SuggestRageDump(5) if Cast(S.ThunderClap, nil, nil, not Target:IsInMeleeRange(8)) then return "thunder_clap generic 20"; end end -- devastate if S.Devastate:IsCastable() then if Cast(S.Devastate, nil, nil, not TargetInMeleeRange) then return "devastate generic 22"; end end end --- ======= ACTION LISTS ======= local function APL() if AoEON() then Enemies8y = Player:GetEnemiesInMeleeRange(8) -- Multiple Abilities EnemiesCount8 = #Enemies8y else EnemiesCount8 = 1 end -- Range check TargetInMeleeRange = Target:IsInMeleeRange(5) if Everyone.TargetIsValid() then -- call precombat if not Player:AffectingCombat() then local ShouldReturn = Precombat(); if ShouldReturn then return ShouldReturn; end end -- Manually added: battle_shout during combat if S.BattleShout:IsCastable() and Settings.Commons.ShoutDuringCombat and (Player:BuffDown(S.BattleShoutBuff, true) or Everyone.GroupBuffMissing(S.BattleShoutBuff)) then if Cast(S.BattleShout, Settings.Commons.GCDasOffGCD.BattleShout) then return "battle_shout precombat"; end end -- Manually added: VR/IV if Player:HealthPercentage() < Settings.Commons.VictoryRushHP then if S.VictoryRush:IsReady() then if Cast(S.VictoryRush) then return "victory_rush defensive"; end end if S.ImpendingVictory:IsReady() then if Cast(S.ImpendingVictory) then return "impending_victory defensive"; end end end -- Interrupt local ShouldReturn = Everyone.Interrupt(5, S.Pummel, Settings.Commons.OffGCDasOffGCD.Pummel, StunInterrupts); if ShouldReturn then return ShouldReturn; end -- auto_attack -- charge,if=time=0 -- Note: Handled in Precombat -- use_items if CDsON() and (Settings.Commons.Enabled.Trinkets or Settings.Commons.Enabled.Items) 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 -- avatar if CDsON() and S.Avatar:IsCastable() then if Cast(S.Avatar, Settings.Protection.GCDasOffGCD.Avatar) then return "avatar main 2"; end end -- shield_wall,if=talent.immovable_object.enabled&buff.avatar.down if IsCurrentlyTanking() and S.ShieldWall:IsCastable() and (S.ImmovableObject:IsAvailable() and Player:BuffDown(S.AvatarBuff)) then if Cast(S.ShieldWall, nil, Settings.Protection.DisplayStyle.Defensive) then return "shield_wall main 4"; end end if CDsON() then -- blood_fury if S.BloodFury:IsCastable() then if Cast(S.BloodFury, Settings.Commons.OffGCDasOffGCD.Racials) then return "blood_fury main 6"; end end -- berserking if S.Berserking:IsCastable() then if Cast(S.Berserking, Settings.Commons.OffGCDasOffGCD.Racials) then return "berserking main 8"; end end -- arcane_torrent if S.ArcaneTorrent:IsCastable() then if Cast(S.ArcaneTorrent, Settings.Commons.OffGCDasOffGCD.Racials) then return "arcane_torrent main 10"; end end -- lights_judgment if S.LightsJudgment:IsCastable() then if Cast(S.LightsJudgment, Settings.Commons.OffGCDasOffGCD.Racials) then return "lights_judgment main 12"; end end -- fireblood if S.Fireblood:IsCastable() then if Cast(S.Fireblood, Settings.Commons.OffGCDasOffGCD.Racials) then return "fireblood main 14"; end end -- ancestral_call if S.AncestralCall:IsCastable() then if Cast(S.AncestralCall, Settings.Commons.OffGCDasOffGCD.Racials) then return "ancestral_call main 16"; end end -- bag_of_tricks if S.BagofTricks:IsCastable() then if Cast(S.BagofTricks, Settings.Commons.OffGCDasOffGCD.Racials) then return "ancestral_call main 18"; end end end -- potion,if=buff.avatar.up|buff.avatar.up&target.health.pct<=20 if Settings.Commons.Enabled.Potions and (Player:BuffUp(S.AvatarBuff) or Player:BuffDown(S.AvatarBuff) and Target:HealthPercentage() <= 20) then local PotionSelected = Everyone.PotionSelected() if PotionSelected and PotionSelected:IsReady() then if Cast(PotionSelected, nil, Settings.Commons.DisplayStyle.Potions) then return "potion main 20"; end end end -- ignore_pain,if=target.health.pct>=20& --(rage.deficit<=15&cooldown.shield_slam.ready --|rage.deficit<=40&cooldown.shield_charge.ready&talent.champions_bulwark.enabled --|rage.deficit<=20&cooldown.shield_charge.ready --|rage.deficit<=30&cooldown.demoralizing_shout.ready&talent.booming_voice.enabled --|rage.deficit<=20&cooldown.avatar.ready --|rage.deficit<=45&cooldown.demoralizing_shout.ready&talent.booming_voice.enabled&buff.last_stand.up&talent.unnerving_focus.enabled --|rage.deficit<=30&cooldown.avatar.ready&buff.last_stand.up&talent.unnerving_focus.enabled --|rage.deficit<=20 --|rage.deficit<=40&cooldown.shield_slam.ready&buff.violent_outburst.up&talent.heavy_repercussions.enabled&talent.impenetrable_wall.enabled --|rage.deficit<=55&cooldown.shield_slam.ready&buff.violent_outburst.up&buff.last_stand.up&talent.unnerving_focus.enabled&talent.heavy_repercussions.enabled&talent.impenetrable_wall.enabled --|rage.deficit<=17&cooldown.shield_slam.ready&talent.heavy_repercussions.enabled --|rage.deficit<=18&cooldown.shield_slam.ready&talent.impenetrable_wall.enabled),use_off_gcd=1 if S.IgnorePain:IsReady() and IgnorePainWillNotCap() and (Target:HealthPercentage() >= 20 and (Player:RageDeficit() <= 15 and S.ShieldSlam:CooldownUp() or Player:RageDeficit() <= 40 and S.ShieldCharge:CooldownUp() and S.ChampionsBulwark:IsAvailable() or Player:RageDeficit() <= 20 and S.ShieldCharge:CooldownUp() or Player:RageDeficit() <= 30 and S.DemoralizingShout:CooldownUp() and S.BoomingVoice:IsAvailable() or Player:RageDeficit() <= 20 and S.Avatar:CooldownUp() or Player:RageDeficit() <= 45 and S.DemoralizingShout:CooldownUp() and S.BoomingVoice:IsAvailable() and Player:BuffUp(S.LastStandBuff) and S.UnnervingFocus:IsAvailable() or Player:RageDeficit() <= 30 and S.Avatar:CooldownUp() and Player:BuffUp(S.LastStandBuff) and S.UnnervingFocus:IsAvailable() or Player:RageDeficit() <= 20 or Player:RageDeficit() <= 40 and S.ShieldSlam:CooldownUp() and Player:BuffUp(S.ViolentOutburstBuff) and S.HeavyRepercussions:IsAvailable() and S.ImpenetrableWall:IsAvailable() or Player:RageDeficit() <= 55 and S.ShieldSlam:CooldownUp() and Player:BuffUp(S.ViolentOutburstBuff) and Player:BuffUp(S.LastStandBuff) and S.UnnervingFocus:IsAvailable() and S.HeavyRepercussions:IsAvailable() and S.ImpenetrableWall:IsAvailable() or Player:RageDeficit() <= 17 and S.ShieldSlam:CooldownUp() and S.HeavyRepercussions:IsAvailable() or Player:RageDeficit() <= 18 and S.ShieldSlam:CooldownUp() and S.ImpenetrableWall:IsAvailable())) then if Cast(S.IgnorePain, nil, Settings.Protection.DisplayStyle.Defensive) then return "ignore_pain main 22"; end end -- last_stand,if=(target.health.pct>=90&talent.unnerving_focus.enabled|target.health.pct<=20&talent.unnerving_focus.enabled)|talent.bolster.enabled|set_bonus.tier30_2pc|set_bonus.tier30_4pc -- Note: If set_bonus.tier30_4pc is true, then tier30_2pc would be true as well, so just check for 2pc if IsCurrentlyTanking() and S.LastStand:IsCastable() and Player:BuffDown(S.ShieldWallBuff) and ((Target:HealthPercentage() >= 90 and S.UnnervingFocus:IsAvailable() or Target:HealthPercentage() <= 20 and S.UnnervingFocus:IsAvailable()) or S.Bolster:IsAvailable() or Player:HasTier(30, 2)) then if Cast(S.LastStand, nil, Settings.Protection.DisplayStyle.Defensive) then return "last_stand main 24"; end end -- ravager if CDsON() and S.Ravager:IsCastable() then SuggestRageDump(10) if Cast(S.Ravager, Settings.Protection.GCDasOffGCD.Ravager, nil, not Target:IsInRange(40)) then return "ravager main 26"; end end --demoralizing_shout,if=talent.booming_voice.enabled if S.DemoralizingShout:IsCastable() and S.BoomingVoice:IsAvailable() then SuggestRageDump(30) if Cast(S.DemoralizingShout, Settings.Protection.GCDasOffGCD.DemoralizingShout) then return "demoralizing_shout main 28"; end end -- spear_of_bastion if CDsON() and S.SpearofBastion:IsCastable() then SuggestRageDump(20) if Cast(S.SpearofBastion, nil, Settings.Commons.DisplayStyle.Signature, not Target:IsInRange(25)) then return "spear_of_bastion main 30"; end end -- thunderous_roar if CDsON() and S.ThunderousRoar:IsCastable() then if Cast(S.ThunderousRoar, Settings.Protection.GCDasOffGCD.ThunderousRoar, nil, not Target:IsInMeleeRange(12)) then return "thunderous_roar main 32"; end end -- shockwave,if=talent.sonic_boom.enabled&buff.avatar.up&talent.unstoppable_force.enabled&!talent.rumbling_earth.enabled if S.Shockwave:IsCastable() and S.SonicBoom:IsAvailable() and Player:BuffUp(S.AvatarBuff) and S.UnstoppableForce:IsAvailable() and not S.RumblingEarth:IsAvailable() then SuggestRageDump(10) if Cast(S.Shockwave, Settings.Protection.GCDasOffGCD.Shockwave, nil, not Target:IsInMeleeRange(10)) then return "shockwave main 34"; end end -- shield_charge if S.ShieldCharge:IsCastable() then SuggestRageDump(40) if Cast(S.ShieldCharge, nil, nil, not Target:IsSpellInRange(S.ShieldCharge)) then return "shield_charge main 36"; end end -- shield_block,if=buff.shield_block.duration<=18&talent.enduring_defenses.enabled|buff.shield_block.duration<=12 if ShouldPressShieldBlock() then if Cast(S.ShieldBlock, nil, Settings.Protection.DisplayStyle.Defensive) then return "shield_block main 38"; end end -- run_action_list,name=aoe,if=spell_targets.thunder_clap>3 if EnemiesCount8 > 3 then local ShouldReturn = Aoe(); if ShouldReturn then return ShouldReturn; end if HR.CastAnnotated(S.Pool, false, "WAIT") then return "Pool for Aoe()"; end end -- call_action_list,name=generic local ShouldReturn = Generic(); if ShouldReturn then return ShouldReturn; end -- If nothing else to do, show the Pool icon if HR.CastAnnotated(S.Pool, false, "WAIT") then return "Wait/Pool Resources"; end end end local function Init() HR.Print("Protection Warrior rotation is currently a work in progress, but has been updated for patch 10.1.0.") end HR.SetAPL(73, APL, Init)