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.
387 lines
18 KiB
387 lines
18 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 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)
|
|
|