--- ============================ 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 Cast = HR.Cast
local CastMainNameplate = HR.CastMainNameplate
local CastMainNameplateSuggested = HR.CastMainNameplateSuggested
-- Num/Bool Helper Functions
local num = HR.Commons . Everyone.num
local bool = HR.Commons . Everyone.bool
-- WoW API
local mathfloor = math.floor
local GetTotemInfo = GetTotemInfo
local GetTime = GetTime
--- ============================ 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 } ,
}
local GCDMax
local Enemies8y , Enemies30y
local EnemiesCount8y , EnemiesCount30y
local InterruptibleEnemyUnits
local WrathableEnemyUnits
local HighestHPEnemyUnit
local PartyHealCandidates
local PartyDispelCandidates
-- 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 ConsecrationTimeRemaining ( )
for index = 1 , 4 do
local _ , totemName , startTime , duration = GetTotemInfo ( index )
if totemName == S.Consecration : Name ( ) then
return ( mathfloor ( startTime + duration - GetTime ( ) + 0.5 ) ) or 0
end
end
return 0
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 ScanBattlefield ( )
InterruptibleEnemyUnits = { }
WrathableEnemyUnits = { }
HighestHPEnemyUnit = nil
local highest_health = 0
for _ , CycleUnit in pairs ( Enemies30y ) do
if not CycleUnit : IsFacingBlacklisted ( ) and not CycleUnit : IsUserCycleBlacklisted ( ) then
if CycleUnit : IsInterruptible ( ) then
table.insert ( InterruptibleEnemyUnits , { CycleUnit , CycleUnit : Health ( ) } )
end
if CycleUnit : HealthPercentage ( ) <= 20 or Player : BuffUp ( S.AvengingWrathBuff ) then
table.insert ( WrathableEnemyUnits , { CycleUnit , CycleUnit : Health ( ) } )
end
-- TODO: cycle judgment debuffs around rather than hit highest HP?
if CycleUnit : Health ( ) >= highest_health then
highest_health = CycleUnit : Health ( )
HighestHPEnemyUnit = CycleUnit
end
end
end
PartyHealCandidates = { }
PartyDispelCandidates = { } -- TODO: add some whitelist code for dispellable debuffs on players (consider cleanse, freedom, bop, spellwarding)
if Player : IsInParty ( ) and not Player : IsInRaid ( ) then
for _ , Char in pairs ( Unit.Party ) do
if Char ~= nil and Char : Exists ( ) and Char : IsInRange ( 40 ) and Char : HealthPercentage ( ) < Settings.Protection . FriendlyWordofGloryHP then
table.insert ( PartyHealCandidates , { Char , Char : HealthPercentage ( ) } )
end
end
end
table.sort ( InterruptibleEnemyUnits , function ( a , b ) return a [ 2 ] > b [ 2 ] end )
table.sort ( WrathableEnemyUnits , function ( a , b ) return a [ 2 ] > b [ 2 ] end )
table.sort ( PartyHealCandidates , function ( a , b ) return a [ 2 ] < b [ 2 ] end )
end
-- Returns `true` if it's safe to dump SOTR or healing without incurring the ICD bug, `false` if you should wait a bit
local function RPSafe ( )
return not S.RighteousProtector : IsAvailable ( ) or ( S.ShieldoftheRighteous : TimeSinceLastCast ( ) > 1 and S.WordofGlory : TimeSinceLastCast ( ) > 1 )
end
local function Precombat ( )
if S.DevotionAura : IsCastable ( ) and ( MissingAura ( ) ) then
if Cast ( S.DevotionAura ) then return " devotion_aura precombat " ; end
end
if S.HammerofWrath : IsReady ( ) then
if CastMainNameplate ( Target , S.HammerofWrath ) then return " hammer of wrath precombat " ; end
end
if S.Judgment : FullRechargeTime ( ) < GCDMax and S.Judgment : IsReady ( ) then
if CastMainNameplate ( Target , S.Judgment ) then return " max charges judgment precombat " ; end
end
if S.AvengersShield : IsCastable ( ) then
if CastMainNameplate ( Target , S.AvengersShield ) then return " avengers_shield precombat " ; end
end
if S.Judgment : IsReady ( ) then
if CastMainNameplate ( Target , S.Judgment ) then return " judgment precombat " ; end
end
if S.Consecration : IsCastable ( ) and Target : IsInMeleeRange ( 8 ) then
if CastMainNameplate ( Target , S.Consecration ) then return " consecration precombat 8 " ; end
end
end
local function Defensives ( )
if Player : HealthPercentage ( ) <= Settings.Protection . BubbleHP and S.DivineShield : IsCastable ( ) then
if Cast ( S.DivineShield , nil , Settings.Protection . DisplayStyle.Defensives ) then return " bubble defensive " ; end
end
if Player : HealthPercentage ( ) <= Settings.Protection . LoHHP and S.LayonHands : IsCastable ( ) then
if HR.CastAnnotated ( S.LayonHands , nil , " SELF " ) then return " lay_on_hands self defensive " ; end
end
if Player : HealthPercentage ( ) < Settings.Protection . PrioSelfWordofGloryHP and Player : BuffUp ( S.ShiningLightFreeBuff ) then
if HR.CastAnnotated ( S.WordofGlory , nil , " SELF " ) then return " free WOG self defensive " ; 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 " ; 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 " ; end
end
if S.ShieldoftheRighteous : IsReady ( ) and Player : BuffRefreshable ( S.ShieldoftheRighteousBuff ) and RPSafe ( ) then
-- TODO: figure out how to do this on nameplates too?
if CastMainNameplateSuggested ( Target , S.ShieldoftheRighteous ) then return " shield_of_the_righteous refresh defensive " ; end
end
end
local function Cooldowns ( )
if S.LightsJudgment : IsCastable ( ) then
if Cast ( S.LightsJudgment , Settings.Commons . OffGCDasOffGCD.Racials , nil , not Target : IsSpellInRange ( S.LightsJudgment ) ) then return " lights_judgment cooldowns " ; end
end
if S.AvengingWrath : IsCastable ( ) then
if Cast ( S.AvengingWrath , Settings.Protection . OffGCDasOffGCD.AvengingWrath ) then return " avenging_wrath cooldowns " ; end
end
if S.Sentinel : IsCastable ( ) then
if Cast ( S.Sentinel , Settings.Protection . OffGCDasOffGCD.Sentinel ) then return " sentinel cooldowns " ; end
end
if S.DivineToll : IsCastable ( ) and ( 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 " ; end
end
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 " ; end
end
end
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 " ; end
end
if S.DivineToll : IsReady ( ) and ( EnemiesCount8y >= 3 ) then
if Cast ( S.DivineToll , nil , Settings.Commons . DisplayStyle.Signature , not Target : IsInRange ( 30 ) ) then return " divine_toll cooldowns " ; end
end
if S.BastionofLight : IsCastable ( ) and ( Player : BuffUp ( S.AvengingWrathBuff ) ) then
if Cast ( S.BastionofLight , Settings.Protection . OffGCDasOffGCD.BastionOfLight ) then return " bastion_of_light cooldowns " ; end
end
end
local function Trinkets ( )
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
-- Return a (unit, spell) pair that generates at least one holy power; or (nil, nil) if no holy power generating spell is ready.
-- uses Rebuke (the kick) as a last ditch priority, assuming you have punishment
local function ForceGenerateHolyPowerGlobal ( )
if S.AvengersShield : IsReady ( ) and # InterruptibleEnemyUnits > 0 then
return InterruptibleEnemyUnits [ 1 ] [ 1 ] , S.AvengersShield
end
if S.HammerofWrath : IsReady ( ) and # WrathableEnemyUnits > 0 then
return WrathableEnemyUnits [ 1 ] [ 1 ] , S.HammerofWrath
end
if S.Judgment : IsReady ( ) then
return Target , S.Judgment
end
if S.BlessedHammer : IsReady ( ) then
return Target , S.BlessedHammer
end
return nil , nil
end
-- Returns the appropriate {target, global, generated_hpower} based on our priorities.
-- if we have avengers_shield + hammer of wrath on CD and judgment + blessed_hammer recharging appropriately, we return {nil, nil} to indicate that we can do a low priority global here.
local function PrioGlobal ( )
if Player : HealthPercentage ( ) < Settings.Protection . PrioSelfWordofGloryHP and ( Player : BuffUp ( S.BastionofLightBuff ) or Player : BuffUp ( S.DivinePurposeBuff ) or Player : BuffUp ( S.ShiningLightFreeBuff ) or not Player : BuffRefreshable ( S.ShieldoftheRighteousBuff ) ) then
return Player , S.WordofGlory , 0
end
if S.AvengersShield : IsReady ( ) and # InterruptibleEnemyUnits > 0 then
return InterruptibleEnemyUnits [ 1 ] [ 1 ] , S.AvengersShield , 1
end
if S.AvengersShield : IsReady ( ) and ( Player : BuffUp ( S.MomentofGloryBuff ) or ( Player : HasTier ( 29 , 2 ) and ( Player : BuffDown ( S.AllyoftheLightBuff ) or Player : BuffRemains ( S.AllyoftheLightBuff ) < Player : GCD ( ) ) ) ) then
return Target , S.AvengersShield , 0
end
if S.AvengersShield : IsReady ( ) and EnemiesCount8y >= 4 then
return Target , S.AvengersShield , 0
end
if S.HammerofWrath : IsReady ( ) and # WrathableEnemyUnits > 0 then
return WrathableEnemyUnits [ 1 ] [ 1 ] , S.HammerofWrath , 1
end
-- not sure which order on these two is better - do we prefer AS or keeping judge on CD?
if S.AvengersShield : IsReady ( ) and EnemiesCount8y >= 2 then
return Target , S.AvengersShield , 0
end
if S.Judgment : IsReady ( ) and S.Judgment : FullRechargeTime ( ) < GCDMax then
return HighestHPEnemyUnit , S.Judgment , 1 + num ( Player : BuffUp ( S.AvengingWrathBuff ) )
end
if S.AvengersShield : IsReady ( ) then
return Target , S.AvengersShield , 0 -- single target
end
-- use OPPORTUNISTIC threshold for healing here - cast the heal on us or a party member if it (probably) won't overheal
if Player : BuffUp ( S.BastionofLightBuff ) or Player : BuffUp ( S.DivinePurposeBuff ) or Player : BuffUp ( S.ShiningLightFreeBuff ) then
if Player : HealthPercentage ( ) < Settings.Protection . OpportunisticSelfWordofGloryHP then
return Player , S.WordofGlory , 0
end
if # PartyHealCandidates > 0 then
return PartyHealCandidates [ 1 ] [ 1 ] , S.WordofGlory , 0
end
end
if S.EyeofTyr : IsReady ( ) then
return Target , S.EyeofTyr , 0
end
if S.Judgment : IsReady ( ) then
return Target , S.Judgment , 1 + num ( Player : BuffUp ( S.AvengingWrathBuff ) )
end
if S.BlessedHammer : FullRechargeTime ( ) < GCDMax then
return Target , S.BlessedHammer , 1
end
return nil , nil , nil
end
local function LowPrioGlobal ( )
if S.Consecration : IsCastable ( ) and not Player : IsMoving ( ) and ConsecrationTimeRemaining ( ) <= 3 then
return Target , S.Consecration
end
if S.BlessedHammer : IsReady ( ) then
return Target , S.BlessedHammer
end
if S.Consecration : IsCastable ( ) and not Player : IsMoving ( ) then
return Target , S.Consecration
end
if Player : BuffUp ( S.ShiningLightFreeBuff ) then
return Player , S.WordofGlory
end
return nil , nil
end
local function Core ( )
-- Save allies from death with Lay on Hands where possible, when we're probably not going to die.
if Player : HealthPercentage ( ) > 40 and # PartyHealCandidates > 0 then
local BestCandidate = PartyHealCandidates [ 1 ]
local Friend = BestCandidate [ 1 ]
local FriendHP = BestCandidate [ 2 ]
if FriendHP <= 15 and S.LayonHands : IsCastable ( ) then -- TODO: handle forbearance here
if HR.CastAnnotated ( S.LayonHands , false , Friend : Name ( ) ) then return " lay_on_hands party_member core " ; end
end
end
----------------------------------------------------------------------
-- Guarantee defensive SOTR uptime > Consecration uptime > then other stuff
-- TODO: consider adding a condition for being "non-scared" if Player:CooldownRemains(S.DivineToll) < Player:BuffRemains(S.ShieldoftheRighteousBuff)
if not ( Player : BuffUp ( S.DivinePurposeBuff ) or Player : BuffUp ( S.BastionofLightBuff ) or Player : BuffUp ( S.AvengingWrathBuff ) ) then
local target = nil
local spell = nil
-- You drop SOTR in (one/two/three) globals, but you're short holy power, so you *MUST* generate holy power this turn.
if ( 1.0 * Player : HolyPower ( ) + Player : BuffRemains ( S.ShieldoftheRighteousBuff ) <= 3 ) then
target , spell = ForceGenerateHolyPowerGlobal ( )
end
if target ~= nil and spell ~= nil then
if CastMainNameplate ( target , spell ) then return " force_generated_holy_power_global standard " ; end
end
-- This is a bad case, it means there was no holy power generator available when we really needed one.
end
-- Dump HOLY POWER into SOTR. We want to do this if our next global is a builder and we're capped on holy power already.
local prio_target , prio_global , prio_hpower = PrioGlobal ( )
if prio_target ~= nil and prio_global ~= nil and S.ShieldoftheRighteous : IsReady ( ) and ( ( prio_hpower + Player : HolyPower ( ) > 4 ) or Player : BuffUp ( S.BastionofLightBuff ) or Player : BuffUp ( S.DivinePurposeBuff ) ) then
if CastMainNameplateSuggested ( prio_target , S.ShieldoftheRighteous ) then return " shield_of_the_righteous holy power dump standard " ; end
end
if S.Consecration : IsCastable ( ) and ConsecrationTimeRemaining ( ) < 2 and not Player : IsMoving ( ) then
if CastMainNameplate ( Target , S.Consecration ) then return " defensive_consecration standard " ; end
end
-------------------------------------------------------------------
if prio_global ~= nil and prio_target ~= nil then
if prio_global == S.WordofGlory then
if HR.CastAnnotated ( S.WordofGlory , false , prio_target : Name ( ) ) then return " prio_global heal standard " ; end
else
if CastMainNameplate ( prio_target , prio_global ) then return " prio_global standard " ; end
end
end
-------------------------------------------------------------------
local low_prio_target , low_prio_global = LowPrioGlobal ( )
if low_prio_global ~= nil and low_prio_target ~= nil then
if low_prio_global == S.WordofGlory then
if HR.CastAnnotated ( S.WordofGlory , false , low_prio_target : Name ( ) ) then return " low_prio heal standard " ; end
else
if CastMainNameplate ( low_prio_target , low_prio_global ) then return " low_prio standard " ; end
end
end
end
-- APL Main
local function APL ( )
Enemies8y = Player : GetEnemiesInMeleeRange ( 8 )
Enemies30y = Player : GetEnemiesInRange ( 30 )
EnemiesCount8y = # Enemies8y
EnemiesCount30y = # Enemies30y
-- constant term to account for human reaction time; tunable a bit.
GCDMax = Player : GCD ( ) + 0.050
-- Get information on targets
ScanBattlefield ( )
-- Even if you're not in combat and don't have a target, press blessed hammer if you're capped on charges and at less than max holy power
if not Player : AffectingCombat ( ) and S.BlessedHammer : FullRechargeTime ( ) < GCDMax and Player : HolyPower ( ) < 5 then
if Cast ( S.BlessedHammer ) then return " out of combat blessed hammer " ; end
end
if Everyone.TargetIsValid ( ) then
if not Player : AffectingCombat ( ) then
local ShouldReturn = Precombat ( ) ; if ShouldReturn then return ShouldReturn ; end
end
local ShouldReturn = Everyone.Interrupt ( 5 , S.Rebuke , Settings.Commons . OffGCDasOffGCD.Rebuke , StunInterrupts ) ; if ShouldReturn then return ShouldReturn ; end
local ShouldReturn = Defensives ( ) ; if ShouldReturn then return ShouldReturn ; end
local ShouldReturn = Cooldowns ( ) ; if ShouldReturn then return ShouldReturn ; end
local ShouldReturn = Trinkets ( ) ; if ShouldReturn then return ShouldReturn ; end
local ShouldReturn = Core ( ) ; if ShouldReturn then return ShouldReturn ; end
if HR.CastAnnotated ( S.Pool , false , " WAIT " ) then return " Wait/Pool Resources " ; end
end
end
local function Init ( )
HR.Print ( " This is a Work In Progress APL optimized for M+ tanking, by Synecd0che " )
end
HR.SetAPL ( 66 , APL , Init )