--- ============================ 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 -- Num/Bool Helper Functions local num = HR.Commons.Everyone.num local bool = HR.Commons.Everyone.bool --- ============================ CONTENT =========================== --- ======= APL LOCALS ======= -- luacheck: max_line_length 9999 -- Define S/I for spell and item arrays local S = Spell.Paladin.Protection local I = Item.Paladin.Protection -- Create table to exclude above trinkets from On Use function local OnUseExcludes = { } -- Interrupts List local StunInterrupts = { {S.HammerofJustice, "Cast Hammer of Justice (Interrupt)", function () return true; end}, } -- Rotation Var local ActiveMitigationNeeded local IsTanking local Enemies8y, Enemies30y local EnemiesCount8y, EnemiesCount30y -- GUI Settings local Everyone = HR.Commons.Everyone local Settings = { General = HR.GUISettings.General, Commons = HR.GUISettings.APL.Paladin.Commons, Protection = HR.GUISettings.APL.Paladin.Protection } local function EvaluateTargetIfFilterJudgment(TargetUnit) return TargetUnit:DebuffRemains(S.JudgmentDebuff) end local function MissingAura() return (Player:BuffDown(S.RetributionAura) and Player:BuffDown(S.DevotionAura) and Player:BuffDown(S.ConcentrationAura) and Player:BuffDown(S.CrusaderAura)) end local function Precombat() -- flask -- food -- augmentation -- snapshot_stats -- Manually added: devotion_aura if S.DevotionAura:IsCastable() and (MissingAura()) then if Cast(S.DevotionAura) then return "devotion_aura precombat 2"; end end -- lights_judgment if CDsON() and S.LightsJudgment:IsCastable() then if Cast(S.LightsJudgment, Settings.Commons.OffGCDasOffGCD.Racials, nil, not Target:IsSpellInRange(S.LightsJudgment)) then return "lights_judgment precombat 4"; end end -- arcane_torrent if CDsON() and S.ArcaneTorrent:IsCastable() then if Cast(S.ArcaneTorrent, Settings.Commons.OffGCDasOffGCD.Racials, nil, not Target:IsInRange(8)) then return "arcane_torrent precombat 6"; end end -- consecration if S.Consecration:IsCastable() and Target:IsInMeleeRange(8) then if Cast(S.Consecration) then return "consecration precombat 8"; end end -- variable,name=trinket_1_sync,op=setif,value=1,value_else=0.5,condition=trinket.1.has_use_buff&((talent.moment_of_glory.enabled&trinket.1.cooldown.duration%%cooldown.moment_of_glory.duration=0)|(!talent.moment_of_glory.enabled&trinket.1.cooldown.duration%%cooldown.avenging_wrath.duration=0)) -- variable,name=trinket_2_sync,op=setif,value=1,value_else=0.5,condition=trinket.2.has_use_buff&((talent.moment_of_glory.enabled&trinket.2.cooldown.duration%%cooldown.moment_of_glory.duration=0)|(!talent.moment_of_glory.enabled&trinket.2.cooldown.duration%%cooldown.avenging_wrath.duration=0)) -- variable,name=trinket_priority,op=setif,value=2,value_else=1,condition=!trinket.1.has_use_buff&trinket.2.has_use_buff|trinket.2.has_use_buff&((trinket.2.cooldown.duration%trinket.2.proc.any_dps.duration)*(1.5+trinket.2.has_buff.strength)*(variable.trinket_2_sync))>((trinket.1.cooldown.duration%trinket.1.proc.any_dps.duration)*(1.5+trinket.1.has_buff.strength)*(variable.trinket_1_sync)) -- variable,name=trinket_1_buffs,value=trinket.1.has_buff.strength|trinket.1.has_buff.mastery|trinket.1.has_buff.versatility|trinket.1.has_buff.haste|trinket.1.has_buff.crit -- variable,name=trinket_2_buffs,value=trinket.2.has_buff.strength|trinket.2.has_buff.mastery|trinket.2.has_buff.versatility|trinket.2.has_buff.haste|trinket.2.has_buff.crit -- Note: Unable to handle some trinket conditionals, such as cooldown.duration. -- Manually added: avengers_shield if S.AvengersShield:IsCastable() then if Cast(S.AvengersShield, nil, nil, not Target:IsSpellInRange(S.AvengersShield)) then return "avengers_shield precombat 10"; end end -- Manually added: judgment if S.Judgment:IsReady() then if Cast(S.Judgment, nil, nil, not Target:IsSpellInRange(S.Judgment)) then return "judgment precombat 12"; end end end local function Defensives() if Player:HealthPercentage() <= Settings.Protection.LoHHP and S.LayonHands:IsCastable() then if Cast(S.LayonHands, nil, Settings.Protection.DisplayStyle.Defensives) then return "lay_on_hands defensive 2"; end end if S.GuardianofAncientKings:IsCastable() and (Player:HealthPercentage() <= Settings.Protection.GoAKHP and Player:BuffDown(S.ArdentDefenderBuff)) then if Cast(S.GuardianofAncientKings, nil, Settings.Protection.DisplayStyle.Defensives) then return "guardian_of_ancient_kings defensive 4"; end end if S.ArdentDefender:IsCastable() and (Player:HealthPercentage() <= Settings.Protection.ArdentDefenderHP and Player:BuffDown(S.GuardianofAncientKingsBuff)) then if Cast(S.ArdentDefender, nil, Settings.Protection.DisplayStyle.Defensives) then return "ardent_defender defensive 6"; end end if S.WordofGlory:IsReady() and (Player:HealthPercentage() <= Settings.Protection.PrioSelfWordofGloryHP and not Player:HealingAbsorbed()) then -- cast word of glory on us if it's a) free or b) probably not going to drop sotr if (Player:BuffRemains(S.ShieldoftheRighteousBuff) >= 5 or Player:BuffUp(S.DivinePurposeBuff) or Player:BuffUp(S.ShiningLightFreeBuff)) then if Cast(S.WordofGlory) then return "word_of_glory defensive 8"; end else -- cast it anyway but run the fuck away if HR.CastAnnotated(S.WordofGlory, false, "KITE") then return "word_of_glory defensive 10"; end end end if S.ShieldoftheRighteous:IsReady() and (Player:BuffRefreshable(S.ShieldoftheRighteousBuff) and (ActiveMitigationNeeded or Player:HealthPercentage() <= Settings.Protection.SotRHP)) then if Cast(S.ShieldoftheRighteous, nil, Settings.Protection.DisplayStyle.ShieldOfTheRighteous) then return "shield_of_the_righteous defensive 14"; end end end local function Cooldowns() -- avenging_wrath if S.AvengingWrath:IsCastable() then if Cast(S.AvengingWrath, Settings.Protection.OffGCDasOffGCD.AvengingWrath) then return "avenging_wrath cooldowns 2"; end end -- sentinel -- Note: Protection Paladin APL has back-end code to replace AW with Sentinel when talented. if S.Sentinel:IsCastable() then if Cast(S.Sentinel, Settings.Protection.OffGCDasOffGCD.Sentinel) then return "sentinel cooldowns 3"; end end -- potion,if=buff.avenging_wrath.up if Settings.Commons.Enabled.Potions and (Player:BuffUp(S.AvengingWrathBuff)) then local PotionSelected = Everyone.PotionSelected() if PotionSelected and PotionSelected:IsReady() then if Cast(PotionSelected, nil, Settings.Commons.DisplayStyle.Potions) then return "potion cooldowns 4"; end end end -- moment_of_glory,if=(buff.avenging_wrath.remains<15|(time>10|(cooldown.avenging_wrath.remains>15))&(cooldown.avengers_shield.remains&cooldown.judgment.remains&cooldown.hammer_of_wrath.remains)) if S.MomentofGlory:IsCastable() and (Player:BuffRemains(S.AvengingWrathBuff) < 15 or (HL.CombatTime() > 10 or (S.AvengingWrath:CooldownRemains() > 15)) and (S.AvengersShield:CooldownDown() and S.Judgment:CooldownDown() and S.HammerofWrath:CooldownDown())) then if Cast(S.MomentofGlory, Settings.Protection.OffGCDasOffGCD.MomentOfGlory) then return "moment_of_glory cooldowns 6"; end end -- holy_avenger,if=buff.avenging_wrath.up|cooldown.avenging_wrath.remains>60 -- Note: Appears to have been removed in 10.0.7 -- bastion_of_light,if=buff.avenging_wrath.up if S.BastionofLight:IsCastable() and (Player:BuffUp(S.AvengingWrathBuff)) then if Cast(S.BastionofLight, Settings.Protection.OffGCDasOffGCD.BastionOfLight) then return "bastion_of_light cooldowns 10"; end end end local function Trinkets() -- use_item,slot=trinket1,if=(buff.moment_of_glory.up|!talent.moment_of_glory_enabled&buff.avenging_wrath.up)&(!trinket.2.has_cooldown|trinket.2.cooldown.remains|variable.trinket_priority=1)|trinket.1.proc.any_dps.duration>=fight_remains -- use_item,slot=trinket2,if=(buff.moment_of_glory.up|!talent.moment_of_glory_enabled&buff.avenging_wrath.up)&(!trinket.1.has_cooldown|trinket.1.cooldown.remains|variable.trinket_priority=2)|trinket.2.proc.any_dps.duration>=fight_remains -- use_item,slot=trinket1,if=!variable.trinket_1_buffs&(trinket.2.cooldown.remains|!variable.trinket_2_buffs|(cooldown.moment_of_glory.remains>20|(!talent.moment_of_glory.enabled&cooldown.avenging_wrath.remains>20))) -- use_item,slot=trinket2,if=!variable.trinket_2_buffs&(trinket.1.cooldown.remains|!variable.trinket_1_buffs|(cooldown.moment_of_glory.remains>20|(!talent.moment_of_glory.enabled&cooldown.avenging_wrath.remains>20))) -- Note: Unable to handle some trinket conditionals, such as cooldown.duration. Using a generic fallback instead. -- use_items,if=(buff.moment_of_glory.up|!talent.moment_of_glory.enabled&buff.avenging_wrath.up)|(cooldown.moment_of_glory.remains>20|!talent.moment_of_glory.enabled&cooldown.avenging_wrath.remains>20) if ((Player:BuffUp(S.MomentofGloryBuff) or (not S.MomentofGlory:IsAvailable()) and Player:BuffUp(S.AvengingWrathBuff)) or (S.MomentofGlory:CooldownRemains() > 20 or (not S.MomentofGlory:IsAvailable()) and S.AvengingWrath:CooldownRemains() > 20)) 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 end local function Standard() -- shield_of_the_righteous,if=(!talent.righteous_protector.enabled|cooldown.righteous_protector_icd.remains=0)&(buff.bastion_of_light.up|buff.divine_purpose.up|holy_power>2) -- TODO: Find a way to track RighteousProtector ICD. if S.ShieldoftheRighteous:IsReady() and (Player:BuffUp(S.BastionofLightBuff) or Player:BuffUp(S.DivinePurposeBuff) or Player:HolyPower() > 2) then if Cast(S.ShieldoftheRighteous, nil, Settings.Protection.DisplayStyle.ShieldOfTheRighteous) then return "shield_of_the_righteous standard 2"; end end -- avengers_shield,if=buff.moment_of_glory.up|!talent.moment_of_glory.enabled if S.AvengersShield:IsCastable() and (Player:BuffUp(S.MomentofGloryBuff) or not S.MomentofGlory:IsAvailable()) then if Cast(S.AvengersShield, nil, nil, not Target:IsSpellInRange(S.AvengersShield)) then return "avengers_shield standard 4"; end end -- hammer_of_wrath,if=buff.avenging_wrath.up if S.HammerofWrath:IsReady() and (Player:BuffUp(S.AvengingWrathBuff)) then if Cast(S.HammerofWrath, Settings.Commons.GCDasOffGCD.HammerOfWrath, nil, not Target:IsSpellInRange(S.HammerofWrath)) then return "hammer_of_wrath standard 6"; end end -- judgment,target_if=min:debuff.judgment.remains,if=charges=2|!talent.crusaders_judgment.enabled if S.Judgment:IsReady() and (S.Judgment:Charges() == 2 or not S.CrusadersJudgment:IsAvailable()) then if Everyone.CastTargetIf(S.Judgment, Enemies30y, "min", EvaluateTargetIfFilterJudgment, nil, not Target:IsSpellInRange(S.Judgment)) then return "judgment standard 8"; end end -- divine_toll,if=time>20|((buff.avenging_wrath.up|!talent.avenging_wrath.enabled)&(buff.moment_of_glory.up|!talent.moment_of_glory.enabled)) if CDsON() and S.DivineToll:IsReady() and (HL.CombatTime() > 20 or ((Player:BuffUp(S.AvengingWrathBuff) or not S.AvengingWrath:IsAvailable()) and (Player:BuffUp(S.MomentofGloryBuff) or not S.MomentofGlory:IsAvailable()))) then if Cast(S.DivineToll, nil, Settings.Commons.DisplayStyle.Signature, not Target:IsInRange(30)) then return "divine_toll standard 10"; end end -- avengers_shield if S.AvengersShield:IsCastable() then if Cast(S.AvengersShield, nil, nil, not Target:IsSpellInRange(S.AvengersShield)) then return "avengers_shield standard 12"; end end -- hammer_of_wrath if S.HammerofWrath:IsReady() then if Cast(S.HammerofWrath, Settings.Commons.GCDasOffGCD.HammerOfWrath, not Target:IsSpellInRange(S.HammerofWrath)) then return "hammer_of_wrath standard 14"; end end -- judgment,target_if=min:debuff.judgment.remains if S.Judgment:IsReady() then if Everyone.CastTargetIf(S.Judgment, Enemies30y, "min", EvaluateTargetIfFilterJudgment, nil, not Target:IsSpellInRange(S.Judgment)) then return "judgment standard 16"; end end -- consecration,if=!consecration.up if S.Consecration:IsCastable() and (Player:BuffDown(S.ConsecrationBuff)) then if Cast(S.Consecration) then return "consecration standard 18"; end end -- eye_of_tyr,if=talent.inmost_light.enabled&raid_event.adds.in>=45 if CDsON() and S.EyeofTyr:IsCastable() and (S.InmostLight:IsAvailable()) then if Cast(S.EyeofTyr, nil, nil, not Target:IsInMeleeRange(8)) then return "eye_of_tyr standard 20"; end end -- blessed_hammer if S.BlessedHammer:IsCastable() then if Cast(S.BlessedHammer, nil, nil, not Target:IsInMeleeRange(5)) then return "blessed_hammer standard 22"; end end -- hammer_of_the_righteous if S.HammeroftheRighteous:IsCastable() then if Cast(S.HammeroftheRighteous, nil, nil, not Target:IsInMeleeRange(5)) then return "hammer_of_the_righteous standard 24"; end end -- crusader_strike if S.CrusaderStrike:IsCastable() then if Cast(S.CrusaderStrike, nil, nil, not Target:IsInMeleeRange(5)) then return "crusader_strike standard 26"; end end -- eye_of_tyr,if=!talent.inmost_light.enabled&raid_event.adds.in>=60 if CDsON() and S.EyeofTyr:IsCastable() and (not S.InmostLight:IsAvailable()) then if Cast(S.EyeofTyr, nil, nil, not Target:IsInMeleeRange(8)) then return "eye_of_tyr standard 27"; end end -- word_of_glory,if=buff.shining_light_free.up if S.WordofGlory:IsReady() and (Player:BuffUp(S.ShiningLightFreeBuff)) then -- Is our health ok? Are we in a party with a wounded party member? Heal them instead. if Player:HealthPercentage() > 90 and Player:IsInParty() and not Player:IsInRaid() then for _, Char in pairs(Unit.Party) do if Char:Exists() and Char:IsInRange(40) and Char:HealthPercentage() <= 80 then if HR.CastAnnotated(S.WordofGlory, false, Char:Name()) then return "word_of_glory standard party 28"; end end end -- Nobody in the party needs it. We might as well heal ourselves for the extra block chance. if Cast(S.WordofGlory, Settings.Protection.GCDasOffGCD.WordOfGlory) then return "word_of_glory standard self 30"; end else -- We're either solo, in a raid, or injured. Heal ourselves. if Cast(S.WordofGlory, Settings.Protection.GCDasOffGCD.WordOfGlory) then return "word_of_glory standard self 32"; end end end -- consecration if S.Consecration:IsCastable() then if Cast(S.Consecration) then return "consecration standard 34"; end end end -- APL Main local function APL() Enemies8y = Player:GetEnemiesInMeleeRange(8) Enemies30y = Player:GetEnemiesInRange(30) if (AoEON()) then EnemiesCount8y = #Enemies8y EnemiesCount30y = #Enemies30y else EnemiesCount8y = 1 EnemiesCount30y = 1 end ActiveMitigationNeeded = Player:ActiveMitigationNeeded() IsTanking = Player:IsTankingAoE(8) or Player:IsTanking(Target) if Everyone.TargetIsValid() then -- Precombat if not Player:AffectingCombat() then local ShouldReturn = Precombat(); if ShouldReturn then return ShouldReturn; end end -- auto_attack -- Interrupts local ShouldReturn = Everyone.Interrupt(5, S.Rebuke, Settings.Commons.OffGCDasOffGCD.Rebuke, StunInterrupts); if ShouldReturn then return ShouldReturn; end -- Manually added: Defensives! if IsTanking then local ShouldReturn = Defensives(); if ShouldReturn then return ShouldReturn; end end -- call_action_list,name=cooldowns if CDsON() then local ShouldReturn = Cooldowns(); if ShouldReturn then return ShouldReturn; end end -- call_action_list,name=trinkets if Settings.Commons.Enabled.Trinkets or Settings.Commons.Enabled.Items then local ShouldReturn = Trinkets(); if ShouldReturn then return ShouldReturn; end end -- call_action_list,name=standard local ShouldReturn = Standard(); if ShouldReturn then return ShouldReturn; end -- Manually added: Pool, if nothing else to do if HR.CastAnnotated(S.Pool, false, "WAIT") then return "Wait/Pool Resources"; end end end local function Init() HR.Print("Protection Paladin rotation is currently a work in progress, but has been updated for patch 10.1.5.") end HR.SetAPL(66, APL, Init)