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.
327 lines
12 KiB
327 lines
12 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 Pet = Unit.Pet
|
|
local Target = Unit.Target
|
|
local Spell = HL.Spell
|
|
local MultiSpell = HL.MultiSpell
|
|
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 mathmin = math.min
|
|
|
|
--- ============================ CONTENT ============================
|
|
--- ======= APL LOCALS =======
|
|
|
|
-- Define S/I for spell and item arrays
|
|
local S = Spell.Shaman.Restoration
|
|
local I = Item.Shaman.Restoration
|
|
|
|
-- Create table to exclude above trinkets from On Use function
|
|
local OnUseExcludes = {
|
|
}
|
|
|
|
|
|
-- GUI Settings
|
|
local Everyone = HR.Commons.Everyone
|
|
local Shaman = HR.Commons.Shaman
|
|
local Settings = {
|
|
General = HR.GUISettings.General,
|
|
Commons = HR.GUISettings.APL.Shaman.Commons,
|
|
Elemental = HR.GUISettings.APL.Shaman.Restoration
|
|
}
|
|
|
|
|
|
HL:RegisterForEvent(function()
|
|
S.LavaBurst:RegisterInFlight()
|
|
end, "LEARNED_SPELL_IN_TAB")
|
|
S.LavaBurst:RegisterInFlight()
|
|
|
|
-- These variables are rotational modifiers parameters.
|
|
local NumEnemiesInCombat
|
|
local NumEnemiesInLargestCluster
|
|
local ActiveFlameshocks
|
|
local RefreshableFlameshocks
|
|
local FightTimeRemaining
|
|
local CoreUnitInLargestCluster
|
|
local BestFlameshockUnit
|
|
local SplashedEnemiesTable
|
|
|
|
-- We keep track of total enemies in combat, as well as a bunch of parameters around the encounter.
|
|
-- We also care about the state of our friendly units in the raid!
|
|
local function BattlefieldSnapshot()
|
|
-- Parameters for damage automation
|
|
NumEnemiesInCombat = 0
|
|
NumEnemiesInLargestCluster = 0
|
|
ActiveFlameshocks = 0
|
|
RefreshableFlameshocks = 0
|
|
FightTimeRemaining = 0
|
|
SplashedEnemiesTable = {}
|
|
CoreUnitInLargestCluster = nil
|
|
BestFlameshockUnit = nil
|
|
-- Parameters for healing automation
|
|
InjuredFriends = 0
|
|
LowestHealthFriendPercentage = 1.0
|
|
|
|
|
|
local min_flameshock_duration = 999
|
|
local max_hp = 0
|
|
if AoEON() then
|
|
for _, Enemy in pairs(Player:GetEnemiesInRange(40)) do
|
|
-- NOTE: the IsDummy() check will assume that you ARE IN COMBAT with all dummies on screen, so zoom in camera to "work around" for testing.
|
|
if Enemy:AffectingCombat() or Enemy:IsDummy() then
|
|
-- Update enemies-in-combat count.
|
|
NumEnemiesInCombat = NumEnemiesInCombat + 1
|
|
|
|
-- Update flameshock data on your targets.
|
|
-- Select as "best flameshock unit" the enemy with minimum fs duration remaining, breaking ties by highest remaining health.
|
|
local fs_duration = Enemy:DebuffRemains(S.FlameShockDebuff)
|
|
if fs_duration > 0 then
|
|
ActiveFlameshocks = ActiveFlameshocks + 1
|
|
end
|
|
if fs_duration < 5 then
|
|
RefreshableFlameshocks = RefreshableFlameshocks + 1
|
|
end
|
|
if fs_duration < min_flameshock_duration then
|
|
min_flameshock_duration = fs_duration
|
|
BestFlameshockUnit = Enemy
|
|
end
|
|
if fs_duration == 0 and Enemy:Health() > max_hp then
|
|
max_hp = Enemy:Health()
|
|
BestFlameshockUnit = Enemy
|
|
end
|
|
|
|
-- Update splashed enemy data. This actually assigns to each unit a GROUP of splashed units, called a splash_cluster.
|
|
-- We can use this to choose when to chain lightning; specifically, we want to CL when any one of these
|
|
-- groups has two or more units in it.
|
|
-- TODO: sometimes we don't want to CL because the second or third targets are immune or irrelevant, for example third boss halls adds
|
|
-- double TODO: figure out the spell value of CL's maelstrom gen versus CL's maelstrom gen + damage (squad leader pulls in spires?)
|
|
-- We can't currently figure out which target is the "center" of the group.
|
|
-- BUG: If you just call Enemy:GetEnemiesInSplashRange(), chain lightning and earthquake seem to double count?!
|
|
-- We do a stupid O(N^2) deduplication. This is probably dumb but works okay for small N.
|
|
local potentially_duplicated_splashes = Enemy:GetEnemiesInSplashRange(10)
|
|
local splash_cluster = {}
|
|
for _, potential_dupe in pairs(potentially_duplicated_splashes) do
|
|
local dupe_found = false
|
|
for _, unique_guy in pairs(splash_cluster) do
|
|
if potential_dupe:GUID() == unique_guy:GUID() then
|
|
dupe_found = true
|
|
break
|
|
end
|
|
end
|
|
if not dupe_found then table.insert(splash_cluster, potential_dupe) end
|
|
end
|
|
SplashedEnemiesTable[Enemy] = splash_cluster
|
|
if #splash_cluster > NumEnemiesInLargestCluster then
|
|
NumEnemiesInLargestCluster = #splash_cluster
|
|
CoreUnitInLargestCluster = Enemy
|
|
end
|
|
|
|
-- Update FightTimeRemaining
|
|
if not Enemy:TimeToDieIsNotValid() and not Enemy:IsUserCycleBlacklisted() then
|
|
FightTimeRemaining = math.max(FightTimeRemaining, Enemy:TimeToDie())
|
|
end
|
|
end
|
|
end
|
|
else
|
|
-- AoEON is disabled, so only care about the primary target
|
|
NumEnemiesInCombat = 1
|
|
|
|
-- Update flameshock data
|
|
local fs_duration = Target:DebuffRemains(S.FlameShockDebuff)
|
|
if fs_duration > 0 then
|
|
ActiveFlameshocks = 1
|
|
end
|
|
if fs_duration < 5 then
|
|
RefreshableFlameshocks = 1
|
|
end
|
|
BestFlameshockUnit = Target
|
|
|
|
-- Update "splash data"
|
|
NumEnemiesInLargestCluster = 1
|
|
CoreUnitInLargestCluster = Target
|
|
|
|
-- Update FightTimeRemaining
|
|
if not Target:TimeToDieIsNotValid() and not Target:IsUserCycleBlacklisted() then
|
|
FightTimeRemaining = Target:TimeToDie()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Some spells aren't castable while moving or if you're currently casting them, so we handle that behavior here.
|
|
-- Additionally, lavaburst isn't castable without a charge or a proc.
|
|
local function IsViable(spell)
|
|
if spell == nil then
|
|
return nil
|
|
end
|
|
local BaseCheck = spell:IsCastable() and spell:IsReady()
|
|
local MovementPredicate = (Player:BuffUp(S.SpiritwalkersGraceBuff) or not Player:IsMoving())
|
|
if spell == S.LightningBolt or
|
|
spell == S.ChainLightning or
|
|
spell == S.HealingRain or
|
|
spell == S.HealingWave or
|
|
spell == S.HealingSurge or
|
|
spell == S.Wellspring then
|
|
return BaseCheck and MovementPredicate
|
|
elseif spell == S.LavaBurst then
|
|
local a = Player:BuffUp(S.LavaSurgeBuff)
|
|
local b = (not Player:IsCasting(S.LavaBurst) and S.LavaBurst:Charges() >= 1)
|
|
local c = (Player:IsCasting(S.LavaBurst) and S.LavaBurst:Charges() == 2)
|
|
return BaseCheck and (MovementPredicate or Player:BuffUp(S.LavaSurgeBuff)) and (a or b or c)
|
|
else
|
|
return BaseCheck
|
|
end
|
|
end
|
|
|
|
|
|
local function Precombat()
|
|
if IsViable(S.Fleshcraft) then
|
|
if Cast(S.Fleshcraft, nil, Settings.Commons.DisplayStyle.Covenant) then return "Precombat Fleshcraft" end
|
|
end
|
|
if NumEnemiesInLargestCluster >= 3 and IsViable(S.ChainLightning) and not Player:IsCasting(S.ChainLightning) then
|
|
if Cast(S.ChainLightning, nil, nil, not Target:IsSpellInRange(S.ChainLightning)) then return "Precombat Chain Lightning" end
|
|
end
|
|
if IsViable(S.LavaBurst) and not Player:IsCasting(S.LavaBurst) then
|
|
if Cast(S.LavaBurst, nil, nil, not Target:IsSpellInRange(S.LavaBurst)) then return "Precombat Lavaburst" end
|
|
end
|
|
if Player:IsCasting(S.LavaBurst) and S.FlameShock:CooldownRemains() == 0 then
|
|
if Cast(S.FlameShock, nil, nil, not Target:IsSpellInRange(S.FlameShock)) then return "Precombat Flameshock" end
|
|
end
|
|
end
|
|
|
|
local function Cooldowns()
|
|
local TrinketToUse = Player:GetUseableTrinkets(OnUseExcludes)
|
|
if TrinketToUse then
|
|
if Cast(TrinketToUse, nil, Settings.Commons.DisplayStyle.Trinkets) then return "Trinket CD" end
|
|
end
|
|
if Player:IsMoving() and S.SpiritwalkersGrace:IsCastable() then
|
|
if Cast(S.SpiritwalkersGrace, nil, Settings.Commons.DisplayStyle.SpiritwalkersGrace) then return "Suggest SWG" end
|
|
end
|
|
if IsViable(S.ChainHarvest) then
|
|
if Cast(S.ChainHarvest, nil, Settings.Commons.DisplayStyle.Covenant) then return "Chain Harvest CD" end
|
|
end
|
|
if IsViable(S.FaeTransfusion) then
|
|
if Cast(S.FaeTransfusion, nil, Settings.Commons.DisplayStyle.Covenant) then return "Fae Transfusion CD" end
|
|
end
|
|
if IsViable(S.VesperTotem) then
|
|
if Cast(S.VesperTotem, nil, Settings.Commons.DisplayStyle.Covenant) then return "Vesper Totem CD" end
|
|
end
|
|
end
|
|
|
|
local function NumFlameShocksToMaintain()
|
|
-- On AOE, don't maintain flame shock.
|
|
if NumEnemiesInLargestCluster >= 3 then return 0 end
|
|
-- On ST or 2T, return 1 or 2.
|
|
return NumEnemiesInLargestCluster
|
|
end
|
|
|
|
local function ApplyFlameShock()
|
|
if S.FlameShock:CooldownRemains() > 0 or BestFlameshockUnit == nil then return nil end
|
|
if BestFlameshockUnit:GUID() == Target:GUID() then
|
|
if Cast(S.FlameShock, nil, nil, not Target:IsInRange(40)) then return "main-target flameshock"; end
|
|
else
|
|
if HR.CastLeftNameplate(BestFlameshockUnit, S.FlameShock) then return "off-target flameshock"; end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function SingleTargetAndSpreadCleaveBuilder()
|
|
if IsViable(S.LavaBurst) then
|
|
return S.LavaBurst, false
|
|
elseif IsViable(S.LightningBolt) then
|
|
return S.LightningBolt, true
|
|
end
|
|
-- End up here when there are no castable builders for a st/spread cleave situation (on the move, no LB charges)
|
|
return nil, false
|
|
end
|
|
|
|
local function AOEBuilder()
|
|
if IsViable(S.ChainLightning) then
|
|
return S.ChainLightning, true
|
|
elseif IsViable(S.LavaBurst) then
|
|
return S.LavaBurst, true
|
|
end
|
|
-- End up here when there are no castable builders for a stacked cleave situation (on the move, no LB charges)
|
|
return nil, false
|
|
end
|
|
|
|
local function CoreRotation()
|
|
local DebugMessage
|
|
|
|
-- Keep minimum number of flameshocks up
|
|
if ActiveFlameshocks < NumFlameShocksToMaintain() then
|
|
DebugMessage = ApplyFlameShock()
|
|
if DebugMessage then return DebugMessage end;
|
|
end
|
|
|
|
local builder, prefer_fs_refresh = nil, false
|
|
if NumEnemiesInLargestCluster < 3 then
|
|
builder, prefer_fs_refresh = SingleTargetAndSpreadCleaveBuilder()
|
|
else
|
|
builder, prefer_fs_refresh = AOEBuilder()
|
|
end
|
|
|
|
-- Refresh flameshocks when the builder is low priority.
|
|
if prefer_fs_refresh and RefreshableFlameshocks > 0 and ActiveFlameshocks <= NumFlameShocksToMaintain() then
|
|
DebugMessage = ApplyFlameShock()
|
|
if DebugMessage then return DebugMessage end;
|
|
end
|
|
|
|
-- If you have a non-nil + viable builder, then you should cast it!
|
|
if builder ~= nil and IsViable(builder) then
|
|
if Cast(builder) then return "Building Maelstrom with optimal Builder (AOE)" end
|
|
end
|
|
if builder == nil then
|
|
-- Try to refresh flameshocks
|
|
DebugMessage = ApplyFlameShock()
|
|
if DebugMessage then return "Refreshing Flame Shock because we cannot build or spend" end
|
|
if Cast(S.FrostShock) then return "Casting Frost Shock because we cannot build or spend or refresh flame shock" end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
--- ======= MAIN =======
|
|
local function APL()
|
|
-- Generalized Data Updates (per frame)
|
|
BattlefieldSnapshot()
|
|
|
|
local DebugMessage
|
|
if Everyone.TargetIsValid() then
|
|
if not Player:AffectingCombat() then
|
|
DebugMessage = Precombat();
|
|
if DebugMessage then return DebugMessage end;
|
|
end
|
|
Everyone.Interrupt(30, S.WindShear, Settings.Commons.OffGCDasOffGCD.WindShear, false);
|
|
|
|
DebugMessage = Cooldowns()
|
|
if DebugMessage then return DebugMessage end;
|
|
|
|
DebugMessage = CoreRotation()
|
|
if DebugMessage then return DebugMessage end;
|
|
|
|
-- This is actually an "error" state, we should always be able to frost shock.
|
|
HR.CastAnnotated(S.FrostShock, false, "ERR");
|
|
end
|
|
end
|
|
|
|
local function Init()
|
|
HR.Print("Restoration Shaman rotation has not been updated for pre-patch 10.0. It may not function properly or may cause errors in-game.")
|
|
end
|
|
|
|
HR.SetAPL(264, APL, Init)
|
|
|