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

--- ============================ 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)