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

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