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.
4413 lines
133 KiB
4413 lines
133 KiB
-- Classes.lua
|
|
-- January 2025
|
|
|
|
local addon, ns = ...
|
|
local Hekili = _G[ addon ]
|
|
|
|
local class = Hekili.Class
|
|
local state = Hekili.State
|
|
|
|
local CommitKey = ns.commitKey
|
|
local FindUnitBuffByID, FindUnitDebuffByID = ns.FindUnitBuffByID, ns.FindUnitDebuffByID
|
|
local GetItemInfo = ns.CachedGetItemInfo
|
|
local GetResourceInfo, GetResourceKey = ns.GetResourceInfo, ns.GetResourceKey
|
|
local ResetDisabledGearAndSpells = ns.ResetDisabledGearAndSpells
|
|
local RegisterEvent = ns.RegisterEvent
|
|
local RegisterUnitEvent = ns.RegisterUnitEvent
|
|
|
|
local getSpecializationKey = ns.getSpecializationKey
|
|
|
|
local LSR = LibStub( "SpellRange-1.0" )
|
|
|
|
local insert, wipe = table.insert, table.wipe
|
|
|
|
local mt_resource = ns.metatables.mt_resource
|
|
|
|
local GetActiveLossOfControlData, GetActiveLossOfControlDataCount = C_LossOfControl.GetActiveLossOfControlData, C_LossOfControl.GetActiveLossOfControlDataCount
|
|
local GetItemCooldown = C_Item.GetItemCooldown
|
|
local GetSpellDescription, GetSpellTexture = C_Spell.GetSpellDescription, C_Spell.GetSpellTexture
|
|
local GetSpecialization, GetSpecializationInfo = C_SpecializationInfo.GetSpecialization, C_SpecializationInfo.GetSpecializationInfo
|
|
local GetItemSpell, GetItemCount, IsUsableItem = C_Item.GetItemSpell, C_Item.GetItemCount, C_Item.IsUsableItem
|
|
local GetSpellInfo = C_Spell.GetSpellInfo
|
|
local GetSpellLink = C_Spell.GetSpellLink
|
|
|
|
local UnitBuff, UnitDebuff = ns.UnitBuff, ns.UnitDebuff
|
|
|
|
local specTemplate = {
|
|
enabled = true,
|
|
|
|
aoe = 2,
|
|
cycle = false,
|
|
cycle_min = 6,
|
|
gcdSync = true,
|
|
|
|
nameplates = true,
|
|
petbased = false,
|
|
|
|
damage = true,
|
|
damageExpiration = 8,
|
|
damageDots = false,
|
|
damageOnScreen = true,
|
|
damageRange = 0,
|
|
damagePets = false,
|
|
|
|
-- Toggles
|
|
custom1Name = "Custom 1",
|
|
custom2Name = "Custom 2",
|
|
noFeignedCooldown = false,
|
|
|
|
abilities = {
|
|
['**'] = {
|
|
disabled = false,
|
|
toggle = "default",
|
|
clash = 0,
|
|
targetMin = 0,
|
|
targetMax = 0,
|
|
dotCap = 0,
|
|
boss = false
|
|
}
|
|
},
|
|
items = {
|
|
['**'] = {
|
|
disabled = false,
|
|
toggle = "default",
|
|
clash = 0,
|
|
targetMin = 0,
|
|
targetMax = 0,
|
|
boss = false,
|
|
criteria = nil
|
|
}
|
|
},
|
|
|
|
placeboBar = 3,
|
|
|
|
ranges = {},
|
|
settings = {},
|
|
phases = {},
|
|
cooldowns = {},
|
|
utility = {},
|
|
defensives = {},
|
|
custom1 = {},
|
|
custom2 = {},
|
|
}
|
|
ns.specTemplate = specTemplate -- for options.
|
|
|
|
|
|
local function Aura_DetectSharedAura( t, type )
|
|
if not t then return end
|
|
local finder = type == "debuff" and FindUnitDebuffByID or FindUnitBuffByID
|
|
local aura = class.auras[ t.key ]
|
|
|
|
local name, _, count, _, duration, expirationTime, caster = finder( aura.shared, aura.id )
|
|
|
|
if name then
|
|
t.count = count > 0 and count or 1
|
|
|
|
if expirationTime > 0 then
|
|
t.applied = expirationTime - duration
|
|
t.expires = expirationTime
|
|
else
|
|
t.applied = state.query_time
|
|
t.expires = state.query_time + t.duration
|
|
end
|
|
t.caster = caster
|
|
return
|
|
end
|
|
|
|
t.count = 0
|
|
t.applied = 0
|
|
t.expires = 0
|
|
t.caster = "nobody"
|
|
end
|
|
|
|
|
|
local protectedFunctions = {
|
|
-- Channels.
|
|
start = true,
|
|
tick = true,
|
|
finish = true,
|
|
|
|
-- Casts
|
|
handler = true, -- Cast finish.
|
|
impact = true, -- Projectile impact.
|
|
}
|
|
|
|
|
|
local HekiliSpecMixin = {
|
|
RegisterResource = function( self, resourceID, regen, model, meta )
|
|
local resource = GetResourceKey( resourceID )
|
|
|
|
if not resource then
|
|
Hekili:Error( "Unable to identify resource with PowerType " .. resourceID .. "." )
|
|
return
|
|
end
|
|
|
|
local r = self.resources[ resource ] or {}
|
|
|
|
r.resource = resource
|
|
r.type = resourceID
|
|
r.state = model or setmetatable( {
|
|
resource = resource,
|
|
type = resourceID,
|
|
|
|
forecast = {},
|
|
fcount = 0,
|
|
times = {},
|
|
values = {},
|
|
|
|
actual = 0,
|
|
max = 1,
|
|
|
|
active_regen = 0.001,
|
|
inactive_regen = 0.001,
|
|
last_tick = 0,
|
|
|
|
swingGen = false,
|
|
|
|
add = function( amt, overcap )
|
|
-- Bypasses forecast, useful in hooks.
|
|
if overcap then r.state.amount = r.state.amount + amt
|
|
else r.state.amount = max( 0, min( r.state.amount + amt, r.state.max ) ) end
|
|
end,
|
|
|
|
timeTo = function( x )
|
|
return state:TimeToResource( r.state, x )
|
|
end,
|
|
}, mt_resource )
|
|
r.state.regenModel = regen
|
|
r.state.meta = meta or {}
|
|
|
|
for _, func in pairs( r.state.meta ) do
|
|
setfenv( func, state )
|
|
end
|
|
|
|
if r.state.regenModel then
|
|
for _, v in pairs( r.state.regenModel ) do
|
|
v.resource = v.resource or resource
|
|
self.resourceAuras[ v.resource ] = self.resourceAuras[ v.resource ] or {}
|
|
|
|
if v.aura then
|
|
self.resourceAuras[ v.resource ][ v.aura ] = true
|
|
end
|
|
|
|
if v.channel then
|
|
self.resourceAuras[ v.resource ].casting = true
|
|
end
|
|
|
|
if v.swing then
|
|
r.state.swingGen = true
|
|
end
|
|
end
|
|
end
|
|
|
|
self.primaryResource = self.primaryResource or resource
|
|
self.resources[ resource ] = r
|
|
|
|
CommitKey( resource )
|
|
end,
|
|
|
|
RegisterTalents = function( self, talents )
|
|
for talent, id in pairs( talents ) do
|
|
self.talents[ talent ] = id
|
|
CommitKey( talent )
|
|
|
|
local hero = id[ 4 ]
|
|
|
|
if hero then
|
|
self.talents[ hero ] = id
|
|
CommitKey( hero )
|
|
id[ 4 ] = nil
|
|
end
|
|
end
|
|
end,
|
|
|
|
RegisterPvpTalents = function( self, pvp )
|
|
for talent, spell in pairs( pvp ) do
|
|
self.pvptalents[ talent ] = spell
|
|
CommitKey( talent )
|
|
end
|
|
end,
|
|
|
|
RegisterAura = function( self, aura, data )
|
|
CommitKey( aura )
|
|
|
|
local a = setmetatable( {
|
|
funcs = {}
|
|
}, {
|
|
__index = function( t, k )
|
|
if t.funcs[ k ] then return t.funcs[ k ]() end
|
|
|
|
local setup = rawget( t, "onLoad" )
|
|
if setup then
|
|
t.onLoad = nil
|
|
setup( t )
|
|
|
|
return t[ k ]
|
|
end
|
|
end
|
|
} )
|
|
|
|
a.key = aura
|
|
|
|
if not data.id then
|
|
self.pseudoAuras = self.pseudoAuras + 1
|
|
data.id = ( -1000 * self.id ) - self.pseudoAuras
|
|
end
|
|
|
|
-- default values.
|
|
data.duration = data.duration or 30
|
|
data.max_stack = data.max_stack or 1
|
|
|
|
-- This is a shared buff that can come from anyone, give it a special generator.
|
|
--[[ if data.shared then
|
|
a.generate = Aura_DetectSharedAura
|
|
end ]]
|
|
|
|
for element, value in pairs( data ) do
|
|
if type( value ) == "function" then
|
|
setfenv( value, state )
|
|
if element ~= "generate" then a.funcs[ element ] = value
|
|
else a[ element ] = value end
|
|
else
|
|
a[ element ] = value
|
|
end
|
|
|
|
class.knownAuraAttributes[ element ] = true
|
|
end
|
|
|
|
if data.tick_time and not data.tick_fixed then
|
|
if a.funcs.tick_time then
|
|
local original = a.funcs.tick_time
|
|
a.funcs.tick_time = setfenv( function( ... )
|
|
local val = original( ... )
|
|
return ( val or 3 ) * haste
|
|
end, state )
|
|
a.funcs.base_tick_time = original
|
|
else
|
|
local original = a.tick_time
|
|
a.funcs.tick_time = setfenv( function( ... )
|
|
return ( original or 3 ) * haste
|
|
end, state )
|
|
a.base_tick_time = original
|
|
a.tick_time = nil
|
|
end
|
|
end
|
|
|
|
self.auras[ aura ] = a
|
|
|
|
if a.id then
|
|
if a.id > 0 then
|
|
-- Hekili:ContinueOnSpellLoad( a.id, function( success )
|
|
a.onLoad = function( a )
|
|
local d = GetSpellInfo( a.id )
|
|
a.name = d and d.name
|
|
|
|
if not a.name then
|
|
for k, v in pairs( class.auraList ) do
|
|
if v == a then class.auraList[ k ] = nil end
|
|
end
|
|
|
|
Hekili.InvalidSpellIDs = Hekili.InvalidSpellIDs or {}
|
|
Hekili.InvalidSpellIDs[ a.id ] = a.name or a.key
|
|
|
|
a.id = a.key
|
|
a.name = a.name or a.key
|
|
|
|
return
|
|
end
|
|
|
|
a.desc = GetSpellDescription( a.id )
|
|
|
|
local texture = a.texture or GetSpellTexture( a.id )
|
|
|
|
if self.id > 0 then
|
|
class.auraList[ a.key ] = "|T" .. texture .. ":0|t " .. a.name
|
|
end
|
|
|
|
self.auras[ a.name ] = a
|
|
if GetSpecializationInfo( GetSpecialization() or 0 ) == self.id then
|
|
-- Copy to class table as well.
|
|
class.auras[ a.name ] = a
|
|
end
|
|
|
|
if self.pendingItemSpells[ a.name ] then
|
|
local items = self.pendingItemSpells[ a.name ]
|
|
|
|
if type( items ) == "table" then
|
|
for i, item in ipairs( items ) do
|
|
local ability = self.abilities[ item ]
|
|
ability.itemSpellKey = a.key .. "_" .. ability.itemSpellID
|
|
|
|
self.abilities[ ability.itemSpellKey ] = a
|
|
class.abilities[ ability.itemSpellKey ] = a
|
|
end
|
|
else
|
|
local ability = self.abilities[ items ]
|
|
ability.itemSpellKey = a.key .. "_" .. ability.itemSpellID
|
|
|
|
self.abilities[ ability.itemSpellKey ] = a
|
|
class.abilities[ ability.itemSpellKey ] = a
|
|
end
|
|
|
|
self.pendingItemSpells[ a.name ] = nil
|
|
self.itemPended = nil
|
|
end
|
|
end
|
|
end
|
|
self.auras[ a.id ] = a
|
|
end
|
|
|
|
if data.meta then
|
|
for k, v in pairs( data.meta ) do
|
|
if type( v ) == "function" then data.meta[ k ] = setfenv( v, state ) end
|
|
class.knownAuraAttributes[ k ] = true
|
|
end
|
|
end
|
|
|
|
if data.copy then
|
|
if type( data.copy ) ~= "table" then
|
|
self.auras[ data.copy ] = a
|
|
else
|
|
for _, key in ipairs( data.copy ) do
|
|
self.auras[ key ] = a
|
|
end
|
|
end
|
|
end
|
|
end,
|
|
|
|
RegisterAuras = function( self, auras )
|
|
for aura, data in pairs( auras ) do
|
|
self:RegisterAura( aura, data )
|
|
end
|
|
end,
|
|
|
|
RegisterPower = function( self, power, id, aura )
|
|
self.powers[ power ] = id
|
|
CommitKey( power )
|
|
|
|
if aura and type( aura ) == "table" then
|
|
self:RegisterAura( power, aura )
|
|
end
|
|
end,
|
|
|
|
RegisterPowers = function( self, powers )
|
|
for k, v in pairs( powers ) do
|
|
self.powers[ k ] = v.id
|
|
self.powers[ v.id ] = k
|
|
|
|
for token, ids in pairs( v.triggers ) do
|
|
if not self.auras[ token ] then
|
|
self:RegisterAura( token, {
|
|
id = v.id,
|
|
copy = ids
|
|
} )
|
|
end
|
|
end
|
|
end
|
|
end,
|
|
|
|
RegisterStateExpr = function( self, key, func )
|
|
setfenv( func, state )
|
|
self.stateExprs[ key ] = func
|
|
class.stateExprs[ key ] = func
|
|
CommitKey( key )
|
|
end,
|
|
|
|
RegisterStateFunction = function( self, key, func )
|
|
setfenv( func, state )
|
|
self.stateFuncs[ key ] = func
|
|
class.stateFuncs[ key ] = func
|
|
CommitKey( key )
|
|
end,
|
|
|
|
RegisterStateTable = function( self, key, data )
|
|
for _, f in pairs( data ) do
|
|
if type( f ) == "function" then
|
|
setfenv( f, state )
|
|
end
|
|
end
|
|
|
|
local meta = getmetatable( data )
|
|
|
|
if meta and meta.__index then
|
|
setfenv( meta.__index, state )
|
|
end
|
|
|
|
self.stateTables[ key ] = data
|
|
class.stateTables[ key ] = data
|
|
CommitKey( key )
|
|
end,
|
|
|
|
-- Phases are for more durable variables that should be recalculated over the course of recommendations.
|
|
-- The start/finish conditions are calculated on reset and that state is persistent between sets of recommendations.
|
|
-- Within a set of recommendations, the phase conditions are recalculated when the clock advances and/or when ability handlers are fired.
|
|
-- Notably, finish is only fired if we are currently in the phase.
|
|
RegisterPhase = function( self, key, start, finish, ... )
|
|
if start then start = setfenv( start, state ) end
|
|
if finish then finish = setfenv( finish, state ) end
|
|
|
|
self.phases[ key ] = {
|
|
activate = start,
|
|
deactivate = finish,
|
|
virtual = {},
|
|
real = {}
|
|
}
|
|
|
|
local phase = self.phases[ key ]
|
|
local n = select( "#", ... )
|
|
|
|
for i = 1, n do
|
|
local hook = select( i, ... )
|
|
|
|
if hook == "reset_precast" then
|
|
self:RegisterHook( hook, function()
|
|
local d = display or "Primary"
|
|
|
|
if phase.real[ d ] == nil then
|
|
phase.real[ d ] = false
|
|
end
|
|
|
|
local original = phase.real[ d ]
|
|
|
|
if state.time == 0 and not InCombatLockdown() then
|
|
phase.real[ d ] = false
|
|
-- Hekili:Print( format( "[ %s ] Phase '%s' set to '%s' (%s) - out of combat.", self.name or "Unspecified", key, tostring( phase.real[ d ] ), hook ) )
|
|
-- if Hekili.ActiveDebug then Hekili:Debug( "[ %s ] Phase '%s' set to '%s' (%s) - out of combat.", self.name or "Unspecified", key, tostring( phase.virtual[ display or "Primary" ] ), hook ) end
|
|
end
|
|
|
|
if not phase.real[ d ] and phase.activate() then
|
|
phase.real[ d ] = true
|
|
end
|
|
|
|
if phase.real[ d ] and phase.deactivate() then
|
|
phase.real[ d ] = false
|
|
end
|
|
|
|
--[[ if phase.real[ d ] ~= original then
|
|
if d == "Primary" then Hekili:Print( format( "Phase change for %s [ %s ] (from %s to %s).", key, d, tostring( original ), tostring( phase.real[ d ] ) ) ) end
|
|
end ]]
|
|
|
|
phase.virtual[ d ] = phase.real[ d ]
|
|
|
|
if Hekili.ActiveDebug then Hekili:Debug( "[ %s ] Phase '%s' set to '%s' (%s).", self.name or "Unspecified", key, tostring( phase.virtual[ d ] ), hook ) end
|
|
end )
|
|
else
|
|
self:RegisterHook( hook, function()
|
|
local d = display or "Primary"
|
|
local previous = phase.virtual[ d ]
|
|
|
|
if phase.virtual[ d ] ~= true and phase.activate() then
|
|
phase.virtual[ d ] = true
|
|
end
|
|
|
|
if phase.virtual[ d ] == true and phase.deactivate() then
|
|
phase.virtual[ d ] = false
|
|
end
|
|
|
|
if Hekili.ActiveDebug and phase.virtual[ d ] ~= previous then Hekili:Debug( "[ %s ] Phase '%s' set to '%s' (%s) - virtual.", self.name or "Unspecified", key, tostring( phase.virtual[ d ] ), hook ) end
|
|
end )
|
|
end
|
|
end
|
|
|
|
self:RegisterVariable( key, function()
|
|
return self.phases[ key ].virtual[ display or "Primary" ]
|
|
end )
|
|
end,
|
|
|
|
RegisterPhasedVariable = function( self, key, default, value, ... )
|
|
value = setfenv( value, state )
|
|
|
|
self.phases[ key ] = {
|
|
update = value,
|
|
virtual = {},
|
|
real = {}
|
|
}
|
|
|
|
local phase = self.phases[ key ]
|
|
local n = select( "#", ... )
|
|
|
|
if type( default ) == "function" then
|
|
phase.default = setfenv( default, state )
|
|
else
|
|
phase.default = setfenv( function() return default end, state )
|
|
end
|
|
|
|
for i = 1, n do
|
|
local hook = select( i, ... )
|
|
|
|
if hook == "reset_precast" then
|
|
self:RegisterHook( hook, function()
|
|
local d = display or "Primary"
|
|
|
|
if phase.real[ d ] == nil or ( state.time == 0 and not InCombatLockdown() ) then
|
|
phase.real[ d ] = phase.default()
|
|
end
|
|
|
|
local original = phase.real[ d ] or "nil"
|
|
|
|
phase.real[ d ] = phase.update( phase.real[ d ], phase.default() )
|
|
phase.virtual[ d ] = phase.real[ d ]
|
|
|
|
if Hekili.ActiveDebug then
|
|
Hekili:Debug( "[ %s ] Phased variable '%s' set to '%s' (%s) - was '%s'.", self.name or "Unspecified", key, tostring( phase.virtual[ display or "Primary" ] ), hook, tostring( original ) )
|
|
end
|
|
end )
|
|
else
|
|
self:RegisterHook( hook, function()
|
|
local d = display or "Primary"
|
|
local previous = phase.virtual[ d ]
|
|
|
|
phase.virtual[ d ] = phase.update( phase.virtual[ d ], phase.default() )
|
|
|
|
if Hekili.ActiveDebug and phase.virtual[ d ] ~= previous then Hekili:Debug( "[ %s ] Phased variable '%s' set to '%s' (%s) - virtual.", self.name or "Unspecified", key, tostring( phase.virtual[ display or "Primary" ] ), hook ) end
|
|
end )
|
|
end
|
|
end
|
|
|
|
self:RegisterVariable( key, function()
|
|
return self.phases[ key ].virtual[ display or "Primary" ]
|
|
end )
|
|
end,
|
|
|
|
RegisterGear = function( self, ... )
|
|
local arg1 = select( 1, ... )
|
|
if not arg1 then return end
|
|
|
|
-- If the first arg is a table, it's registering multiple items/sets
|
|
if type( arg1 ) == "table" then
|
|
for set, data in pairs( arg1 ) do
|
|
self:RegisterGear( set, data )
|
|
end
|
|
return
|
|
end
|
|
|
|
local arg2 = select( 2, ... )
|
|
if not arg2 then return end
|
|
|
|
-- If the first arg is a string, register it
|
|
if type( arg1 ) == "string" then
|
|
local gear = self.gear[ arg1 ] or {}
|
|
local found = false
|
|
|
|
-- If the second arg is a table, it's a tier set with auras
|
|
if type( arg2 ) == "table" then
|
|
if arg2.items then
|
|
for _, item in ipairs( arg2.items ) do
|
|
if not gear[ item ] then
|
|
table.insert( gear, item )
|
|
gear[ item ] = true
|
|
found = true
|
|
end
|
|
end
|
|
end
|
|
|
|
if arg2.auras then
|
|
-- Register auras (even if no items are found, can be useful for early patch testing).
|
|
self:RegisterAuras( arg2.auras )
|
|
end
|
|
end
|
|
|
|
-- If the second arg is a number, this is a legacy registration with a single set/item
|
|
if type( arg2 ) == "number" then
|
|
local n = select( "#", ... )
|
|
|
|
for i = 2, n do
|
|
local item = select( i, ... )
|
|
|
|
if not gear[ item ] then
|
|
table.insert( gear, item )
|
|
gear[ item ] = true
|
|
found = true
|
|
end
|
|
end
|
|
end
|
|
|
|
if found then
|
|
self.gear[ arg1 ] = gear
|
|
CommitKey( arg1 )
|
|
end
|
|
|
|
return
|
|
end
|
|
|
|
-- Debug print if needed
|
|
-- Hekili:Print( "|cFFFF0000[Hekili]|r Invalid input passed to RegisterGear." )
|
|
end,
|
|
|
|
|
|
-- Check for the set bonus based on hidden aura instead of counting the number of equipped items.
|
|
-- This may be useful for tier set items that are crafted so their item ID doesn't match.
|
|
-- The alternative is *probably* to treat sets based on bonusIDs.
|
|
RegisterSetBonus = function( self, key, spellID )
|
|
self.setBonuses[ key ] = spellID
|
|
CommitKey( key )
|
|
end,
|
|
|
|
RegisterSetBonuses = function( self, ... )
|
|
local n = select( "#", ... )
|
|
|
|
for i = 1, n, 2 do
|
|
self:RegisterSetBonus( select( i, ... ) )
|
|
end
|
|
end,
|
|
|
|
RegisterPotion = function( self, potion, data )
|
|
self.potions[ potion ] = data
|
|
|
|
data.key = potion
|
|
|
|
if data.items then
|
|
if type( data.items ) == "table" then
|
|
for _, key in ipairs( data.items ) do
|
|
self.potions[ key ] = data
|
|
CommitKey( key )
|
|
end
|
|
else
|
|
self.potions[ data.items ] = data
|
|
CommitKey( data.items )
|
|
end
|
|
end
|
|
|
|
local potionItem = Item:CreateFromItemID( data.item )
|
|
|
|
if not potionItem:IsItemEmpty() then
|
|
potionItem:ContinueOnItemLoad( function()
|
|
local name = potionItem:GetItemName() or data.name
|
|
local link = potionItem:GetItemLink() or data.link
|
|
|
|
data.name = name
|
|
data.link = link
|
|
|
|
class.potionList[ potion ] = link
|
|
return true
|
|
end )
|
|
end
|
|
|
|
CommitKey( potion )
|
|
end,
|
|
|
|
RegisterPotions = function( self, potions )
|
|
for k, v in pairs( potions ) do
|
|
self:RegisterPotion( k, v )
|
|
end
|
|
end,
|
|
|
|
RegisterRecheck = function( self, func )
|
|
self.recheck = func
|
|
end,
|
|
|
|
RegisterHook = function( self, hook, func, noState )
|
|
if not ( noState == true or hook == "COMBAT_LOG_EVENT_UNFILTERED" and noState == nil ) then
|
|
func = setfenv( func, state )
|
|
end
|
|
self.hooks[ hook ] = self.hooks[ hook ] or {}
|
|
insert( self.hooks[ hook ], func )
|
|
end,
|
|
|
|
RegisterAbility = function( self, ability, data )
|
|
CommitKey( ability )
|
|
|
|
local a = setmetatable( {
|
|
funcs = {},
|
|
}, {
|
|
__index = function( t, k )
|
|
local setup = rawget( t, "onLoad" )
|
|
if setup then
|
|
t.onLoad = nil
|
|
setup( t )
|
|
return t[ k ]
|
|
end
|
|
|
|
if t.funcs[ k ] then return t.funcs[ k ]() end
|
|
if k == "lastCast" then return state.history.casts[ t.key ] or t.realCast end
|
|
if k == "lastUnit" then return state.history.units[ t.key ] or t.realUnit end
|
|
end,
|
|
} )
|
|
|
|
a.key = ability
|
|
a.from = self.id
|
|
|
|
if not data.id then
|
|
if data.item then
|
|
class.specs[ 0 ].itemAbilities = class.specs[ 0 ].itemAbilities + 1
|
|
data.id = -100 - class.specs[ 0 ].itemAbilities
|
|
else
|
|
self.pseudoAbilities = self.pseudoAbilities + 1
|
|
data.id = -1000 * self.id - self.pseudoAbilities
|
|
end
|
|
a.id = data.id
|
|
end
|
|
|
|
if data.id and type( data.id ) == "function" then
|
|
if not data.copy or type( data.copy ) == "table" and #data.copy == 0 then
|
|
Hekili:Error( "RegisterAbility for %s (Specialization %d) will fail; ability has an ID function but needs to have 'copy' entries for the abilities table.", ability, self.id )
|
|
end
|
|
end
|
|
|
|
|
|
local item = data.item
|
|
if item and type( item ) == "function" then
|
|
setfenv( item, state )
|
|
item = item()
|
|
end
|
|
|
|
if data.meta then
|
|
for k, v in pairs( data.meta ) do
|
|
if type( v ) == "function" then data.meta[ k ] = setfenv( v, state ) end
|
|
end
|
|
end
|
|
|
|
-- default values.
|
|
if not data.cast then data.cast = 0 end
|
|
if not data.cooldown then data.cooldown = 0 end
|
|
if not data.recharge then data.recharge = data.cooldown end
|
|
if not data.charges then data.charges = 1 end
|
|
|
|
if data.hasteCD then
|
|
if type( data.cooldown ) == "number" and data.cooldown > 0 then data.cooldown = Hekili:Loadstring( "return " .. data.cooldown .. " * haste" ) end
|
|
if type( data.recharge ) == "number" and data.recharge > 0 then data.recharge = Hekili:Loadstring( "return " .. data.recharge .. " * haste" ) end
|
|
end
|
|
|
|
if not data.fixedCast and type( data.cast ) == "number" then
|
|
data.cast = Hekili:Loadstring( "return " .. data.cast .. " * haste" )
|
|
end
|
|
|
|
if data.toggle == "interrupts" and data.gcd == "off" and data.readyTime == state.timeToInterrupt and data.interrupt == nil then
|
|
data.interrupt = true
|
|
end
|
|
|
|
for key, value in pairs( data ) do
|
|
if type( value ) == "function" then
|
|
setfenv( value, state )
|
|
|
|
if not protectedFunctions[ key ] then a.funcs[ key ] = value
|
|
else a[ key ] = value end
|
|
data[ key ] = nil
|
|
else
|
|
a[ key ] = value
|
|
end
|
|
end
|
|
|
|
if ( a.velocity or a.flightTime ) and a.impact and a.isProjectile == nil then
|
|
a.isProjectile = true
|
|
end
|
|
|
|
a.realCast = 0
|
|
|
|
if item then
|
|
--[[ local name, link, _, _, _, _, _, _, _, texture = GetItemInfo( item )
|
|
|
|
a.name = name or ability
|
|
a.link = link or ability ]]
|
|
|
|
class.itemMap[ item ] = ability
|
|
|
|
-- Register the item if it doesn't already exist.
|
|
class.specs[0]:RegisterGear( ability, item )
|
|
if data.copy then
|
|
if type( data.copy ) == "table" then
|
|
for _, iID in ipairs( data.copy ) do
|
|
if type( iID ) == "number" and iID < 0 then class.specs[0]:RegisterGear( ability, -iID ) end
|
|
end
|
|
else
|
|
if type( data.copy ) == "number" and data.copy < 0 then class.specs[0]:RegisterGear( ability, -data.copy ) end
|
|
end
|
|
end
|
|
|
|
local actionItem = Item:CreateFromItemID( item )
|
|
if not actionItem:IsItemEmpty() then
|
|
actionItem:ContinueOnItemLoad( function( success )
|
|
local name = actionItem:GetItemName()
|
|
local link = actionItem:GetItemLink()
|
|
local texture = actionItem:GetItemIcon()
|
|
|
|
|
|
if name then
|
|
if not a.name or a.name == a.key then a.name = name end
|
|
if not a.link or a.link == a.key then a.link = link end
|
|
if not a.funcs.texture then a.texture = a.texture or texture end
|
|
|
|
if a.suffix then
|
|
a.actualName = name
|
|
a.name = a.name .. " " .. a.suffix
|
|
end
|
|
|
|
self.abilities[ ability ] = self.abilities[ ability ] or a
|
|
self.abilities[ a.name ] = self.abilities[ a.name ] or a
|
|
self.abilities[ a.link ] = self.abilities[ a.link ] or a
|
|
self.abilities[ data.id ] = self.abilities[ a.link ] or a
|
|
|
|
a.itemLoaded = GetTime()
|
|
|
|
if a.item and a.item ~= 158075 then
|
|
a.itemSpellName, a.itemSpellID = GetItemSpell( a.item )
|
|
|
|
if a.itemSpellID then
|
|
a.itemSpellKey = a.key .. "_" .. a.itemSpellID
|
|
self.abilities[ a.itemSpellKey ] = a
|
|
class.abilities[ a.itemSpellKey ] = a
|
|
end
|
|
|
|
if a.itemSpellName then
|
|
local itemAura = self.auras[ a.itemSpellName ]
|
|
|
|
if itemAura then
|
|
a.itemSpellKey = itemAura.key .. "_" .. a.itemSpellID
|
|
self.abilities[ a.itemSpellKey ] = a
|
|
class.abilities[ a.itemSpellKey ] = a
|
|
|
|
else
|
|
if self.pendingItemSpells[ a.itemSpellName ] then
|
|
if type( self.pendingItemSpells[ a.itemSpellName ] ) == "table" then
|
|
table.insert( self.pendingItemSpells[ a.itemSpellName ], ability )
|
|
else
|
|
local first = self.pendingItemSpells[ a.itemSpellName ]
|
|
self.pendingItemSpells[ a.itemSpellName ] = {
|
|
first,
|
|
ability
|
|
}
|
|
end
|
|
else
|
|
self.pendingItemSpells[ a.itemSpellName ] = ability
|
|
a.itemPended = GetTime()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if not a.unlisted then
|
|
class.abilityList[ ability ] = a.listName or ( "|T" .. ( a.texture or texture ) .. ":0|t " .. link )
|
|
class.itemList[ item ] = a.listName or ( "|T" .. a.texture .. ":0|t " .. link )
|
|
class.abilityByName[ a.name ] = a
|
|
end
|
|
|
|
if data.copy then
|
|
if type( data.copy ) == "string" or type( data.copy ) == "number" then
|
|
self.abilities[ data.copy ] = a
|
|
elseif type( data.copy ) == "table" then
|
|
for _, key in ipairs( data.copy ) do
|
|
self.abilities[ key ] = a
|
|
end
|
|
end
|
|
end
|
|
|
|
if data.items then
|
|
local addedToItemList = false
|
|
|
|
for _, id in ipairs( data.items ) do
|
|
local copyItem = Item:CreateFromItemID( id )
|
|
|
|
if not copyItem:IsItemEmpty() then
|
|
self:RegisterGear( a.key, id )
|
|
copyItem:ContinueOnItemLoad( function()
|
|
local name = copyItem:GetItemName()
|
|
local link = copyItem:GetItemLink()
|
|
local texture = copyItem:GetItemIcon()
|
|
|
|
if name then
|
|
class.abilities[ name ] = a
|
|
self.abilities[ name ] = a
|
|
|
|
if not class.itemList[ id ] then
|
|
class.itemList[ id ] = a.listName or ( "|T" .. ( a.texture or texture ) .. ":0|t " .. link )
|
|
addedToItemList = true
|
|
end
|
|
end
|
|
end )
|
|
end
|
|
end
|
|
|
|
if addedToItemList then
|
|
if ns.ReadKeybindings then ns.ReadKeybindings() end
|
|
end
|
|
end
|
|
|
|
if ability then class.abilities[ ability ] = a end
|
|
if a.name then class.abilities[ a.name ] = a end
|
|
if a.link then class.abilities[ a.link ] = a end
|
|
if a.id then class.abilities[ a.id ] = a end
|
|
|
|
Hekili.OptionsReady = false
|
|
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end )
|
|
end
|
|
end
|
|
|
|
if a.id and a.id > 0 then
|
|
-- Hekili:ContinueOnSpellLoad( a.id, function( success )
|
|
a.onLoad = function()
|
|
local spellInfo = GetSpellInfo( a.id )
|
|
|
|
if spellInfo == nil then
|
|
spellInfo = GetItemInfo( a.id )
|
|
end
|
|
if spellInfo then
|
|
a.name = spellInfo.name
|
|
else
|
|
a.name = nil
|
|
end
|
|
|
|
if not a.name then
|
|
for k, v in pairs( class.abilityList ) do
|
|
if v == a then class.abilityList[ k ] = nil end
|
|
end
|
|
Hekili.InvalidSpellIDs = Hekili.InvalidSpellIDs or {}
|
|
table.insert( Hekili.InvalidSpellIDs, a.id )
|
|
Hekili:Error( "Name info not available for " .. a.id .. "." )
|
|
return
|
|
end
|
|
|
|
if not a.name then Hekili:Error( "Name info not available for " .. a.id .. "." ); return false end
|
|
|
|
a.desc = GetSpellDescription( a.id ) -- was returning raw tooltip data.
|
|
|
|
if a.suffix then
|
|
a.actualName = a.name
|
|
a.name = a.name .. " " .. a.suffix
|
|
end
|
|
|
|
local texture = a.texture or GetSpellTexture( a.id )
|
|
|
|
self.abilities[ a.name ] = self.abilities[ a.name ] or a
|
|
class.abilities[ a.name ] = class.abilities[ a.name ] or a
|
|
|
|
if not a.unlisted then
|
|
class.abilityList[ ability ] = a.listName or ( "|T" .. texture .. ":0|t " .. a.name )
|
|
class.abilityByName[ a.name ] = class.abilities[ a.name ] or a
|
|
end
|
|
|
|
if a.rangeSpell and type( a.rangeSpell ) == "number" then
|
|
Hekili:ContinueOnSpellLoad( a.rangeSpell, function( success )
|
|
if success then
|
|
local info = GetSpellInfo( a.rangeSpell )
|
|
if info then
|
|
a.rangeSpell = info.name
|
|
else
|
|
a.rangeSpell = nil
|
|
end
|
|
else
|
|
a.rangeSpell = nil
|
|
end
|
|
end )
|
|
end
|
|
|
|
Hekili.OptionsReady = false
|
|
end
|
|
end
|
|
|
|
self.abilities[ ability ] = a
|
|
self.abilities[ a.id ] = a
|
|
|
|
if not a.unlisted then class.abilityList[ ability ] = class.abilityList[ ability ] or a.listName or a.name end
|
|
|
|
if data.copy then
|
|
if type( data.copy ) == "string" or type( data.copy ) == "number" then
|
|
self.abilities[ data.copy ] = a
|
|
elseif type( data.copy ) == "table" then
|
|
for _, key in ipairs( data.copy ) do
|
|
self.abilities[ key ] = a
|
|
end
|
|
end
|
|
end
|
|
|
|
if data.items then
|
|
for _, itemID in ipairs( data.items ) do
|
|
class.itemMap[ itemID ] = ability
|
|
end
|
|
end
|
|
|
|
if a.dual_cast or a.funcs.dual_cast then
|
|
self.can_dual_cast = true
|
|
self.dual_cast[ a.key ] = true
|
|
end
|
|
|
|
if a.empowered or a.funcs.empowered then
|
|
self.can_empower = true
|
|
end
|
|
|
|
if a.auras then
|
|
self:RegisterAuras( a.auras )
|
|
end
|
|
end,
|
|
|
|
RegisterAbilities = function( self, abilities )
|
|
for ability, data in pairs( abilities ) do
|
|
self:RegisterAbility( ability, data )
|
|
end
|
|
end,
|
|
|
|
RegisterPack = function( self, name, version, import )
|
|
self.packs[ name ] = {
|
|
version = tonumber( version ),
|
|
import = import:gsub("([^|])|([^|])", "%1||%2")
|
|
}
|
|
end,
|
|
|
|
RegisterPriority = function( self, name, version, notes, priority )
|
|
end,
|
|
|
|
RegisterRanges = function( self, ... )
|
|
if type( ... ) == "table" then
|
|
self.ranges = ...
|
|
return
|
|
end
|
|
|
|
for i = 1, select( "#", ... ) do
|
|
insert( self.ranges, ( select( i, ... ) ) )
|
|
end
|
|
end,
|
|
|
|
RegisterRangeFilter = function( self, name, func )
|
|
self.filterName = name
|
|
self.filter = func
|
|
end,
|
|
|
|
RegisterOptions = function( self, options )
|
|
self.options = options
|
|
end,
|
|
|
|
RegisterEvent = function( self, event, func )
|
|
RegisterEvent( event, function( ... )
|
|
if state.spec.id == self.id then func( ... ) end
|
|
end )
|
|
end,
|
|
|
|
RegisterUnitEvent = function( self, event, unit1, unit2, func )
|
|
RegisterUnitEvent( event, unit1, unit2, function( ... )
|
|
if state.spec.id == self.id then func( ... ) end
|
|
end )
|
|
end,
|
|
|
|
RegisterCombatLogEvent = function( self, func )
|
|
self:RegisterHook( "COMBAT_LOG_EVENT_UNFILTERED", func )
|
|
end,
|
|
|
|
RegisterCycle = function( self, func )
|
|
self.cycle = setfenv( func, state )
|
|
end,
|
|
|
|
RegisterPet = function( self, token, id, spell, duration, ... )
|
|
CommitKey( token )
|
|
|
|
-- Prepare the main model
|
|
local model = {
|
|
id = type( id ) == "function" and setfenv( id, state ) or id,
|
|
token = token,
|
|
spell = spell,
|
|
duration = type( duration ) == "function" and setfenv( duration, state ) or duration
|
|
}
|
|
|
|
-- Register the main pet token
|
|
self.pets[ token ] = model
|
|
|
|
-- Register copies, but avoid overwriting unrelated registrations
|
|
local n = select( "#", ... )
|
|
if n and n > 0 then
|
|
for i = 1, n do
|
|
local alias = select( i, ... )
|
|
|
|
if self.pets[ alias ] and self.pets[ alias ] ~= model then
|
|
if Hekili.ActiveDebug then
|
|
Hekili:Debug( "RegisterPet: Alias '%s' already assigned to a different pet. Skipping for token '%s'.", tostring( alias ), tostring( token ) )
|
|
end
|
|
else
|
|
self.pets[ alias ] = model
|
|
end
|
|
end
|
|
end
|
|
end,
|
|
|
|
|
|
RegisterPets = function( self, pets )
|
|
for token, data in pairs( pets ) do
|
|
-- Extract fields from the pet definition.
|
|
local id = data.id
|
|
local spell = data.spell
|
|
local duration = data.duration
|
|
local copy = data.copy
|
|
|
|
-- Register the pet and handle the copy field if it exists.
|
|
if copy then
|
|
self:RegisterPet( token, id, spell, duration, type( copy ) == "string" and copy or unpack( copy ) )
|
|
else
|
|
self:RegisterPet( token, id, spell, duration )
|
|
end
|
|
end
|
|
end,
|
|
|
|
RegisterTotem = function( self, token, id, ... )
|
|
-- Register the primary totem.
|
|
self.totems[ token ] = id
|
|
self.totems[ id ] = token
|
|
|
|
-- Handle copies if provided.
|
|
local n = select( "#", ... )
|
|
if n and n > 0 then
|
|
for i = 1, n do
|
|
local copy = select( i, ... )
|
|
self.totems[ copy ] = id
|
|
self.totems[ id ] = copy
|
|
end
|
|
end
|
|
|
|
-- Commit the primary token.
|
|
CommitKey( token )
|
|
end,
|
|
|
|
RegisterTotems = function( self, totems )
|
|
for token, data in pairs( totems ) do
|
|
local id = data.id
|
|
local copy = data.copy
|
|
|
|
-- Register the primary totem.
|
|
self.totems[ token ] = id
|
|
self.totems[ id ] = token
|
|
|
|
-- Register any copies (aliases).
|
|
if copy then
|
|
if type( copy ) == "string" then
|
|
self.totems[ copy ] = id
|
|
self.totems[ id ] = copy
|
|
elseif type( copy ) == "table" then
|
|
for _, alias in ipairs( copy ) do
|
|
self.totems[ alias ] = id
|
|
self.totems[ id ] = alias
|
|
end
|
|
end
|
|
end
|
|
|
|
CommitKey( token )
|
|
end
|
|
end,
|
|
|
|
GetSetting = function( self, info )
|
|
local setting = info[ #info ]
|
|
return Hekili.DB.profile.specs[ self.id ].settings[ setting ]
|
|
end,
|
|
|
|
SetSetting = function( self, info, val )
|
|
local setting = info[ #info ]
|
|
Hekili.DB.profile.specs[ self.id ].settings[ setting ] = val
|
|
end,
|
|
|
|
-- option should be an AceOption table.
|
|
RegisterSetting = function( self, key, value, option )
|
|
CommitKey( key )
|
|
|
|
table.insert( self.settings, {
|
|
name = key,
|
|
default = value,
|
|
info = option
|
|
} )
|
|
|
|
option.order = 100 + #self.settings
|
|
|
|
option.get = option.get or function( info )
|
|
local setting = info[ #info ]
|
|
local val = Hekili.DB.profile.specs[ self.id ].settings[ setting ]
|
|
|
|
if val ~= nil then return val end
|
|
return value
|
|
end
|
|
|
|
option.set = option.set or function( info, val )
|
|
local setting = info[ #info ]
|
|
Hekili.DB.profile.specs[ self.id ].settings[ setting ] = val
|
|
end
|
|
end,
|
|
|
|
-- For faster variables.
|
|
RegisterVariable = function( self, key, func )
|
|
CommitKey( key )
|
|
self.variables[ key ] = setfenv( func, state )
|
|
end,
|
|
}
|
|
|
|
|
|
function Hekili:RestoreDefaults()
|
|
local p = self.DB.profile
|
|
local reverted = {}
|
|
local changed = {}
|
|
|
|
for k, v in pairs( class.packs ) do
|
|
local existing = rawget( p.packs, k )
|
|
|
|
if not existing or not existing.version or existing.version ~= v.version then
|
|
local data = self.DeserializeActionPack( v.import )
|
|
|
|
if data and type( data ) == "table" then
|
|
p.packs[ k ] = data.payload
|
|
data.payload.version = v.version
|
|
data.payload.date = v.version
|
|
data.payload.builtIn = true
|
|
|
|
if not existing or not existing.version or existing.version < v.version then
|
|
insert( changed, k )
|
|
else
|
|
insert( reverted, k )
|
|
end
|
|
|
|
local specID = data.payload.spec
|
|
|
|
if specID then
|
|
local spec = rawget( p.specs, specID )
|
|
if spec then
|
|
if spec.package then
|
|
local currPack = p.packs[ spec.package ]
|
|
if not currPack or currPack.spec ~= specID then
|
|
spec.package = k
|
|
end
|
|
else
|
|
spec.package = k
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if #changed > 0 or #reverted > 0 then
|
|
self:LoadScripts()
|
|
end
|
|
|
|
if #changed > 0 then
|
|
local msg
|
|
|
|
if #changed == 1 then
|
|
msg = "The |cFFFFD100" .. changed[1] .. "|r priority was updated."
|
|
elseif #changed == 2 then
|
|
msg = "The |cFFFFD100" .. changed[1] .. "|r and |cFFFFD100" .. changed[2] .. "|r priorities were updated."
|
|
else
|
|
msg = "|cFFFFD100" .. changed[1] .. "|r"
|
|
|
|
for i = 2, #changed - 1 do
|
|
msg = msg .. ", |cFFFFD100" .. changed[i] .. "|r"
|
|
end
|
|
|
|
msg = "The " .. msg .. ", and |cFFFFD100" .. changed[ #changed ] .. "|r priorities were updated."
|
|
end
|
|
|
|
if msg then
|
|
C_Timer.After( 5, function()
|
|
if Hekili.DB.profile.notifications.enabled then Hekili:Notify( msg, 6 ) end
|
|
Hekili:Print( msg )
|
|
end )
|
|
end
|
|
end
|
|
|
|
if #reverted > 0 then
|
|
local msg
|
|
|
|
if #reverted == 1 then
|
|
msg = "The |cFFFFD100" .. reverted[1] .. "|r priority was reverted."
|
|
elseif #reverted == 2 then
|
|
msg = "The |cFFFFD100" .. reverted[1] .. "|r and |cFFFFD100" .. reverted[2] .. "|r priorities were reverted."
|
|
else
|
|
msg = "|cFFFFD100" .. reverted[1] .. "|r"
|
|
|
|
for i = 2, #reverted - 1 do
|
|
msg = msg .. ", |cFFFFD100" .. reverted[i] .. "|r"
|
|
end
|
|
|
|
msg = "The " .. msg .. ", and |cFFFFD100" .. reverted[ #reverted ] .. "|r priorities were reverted."
|
|
end
|
|
|
|
if msg then
|
|
C_Timer.After( 6, function()
|
|
if Hekili.DB.profile.notifications.enabled then Hekili:Notify( msg, 6 ) end
|
|
Hekili:Print( msg )
|
|
end )
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function Hekili:RestoreDefault( name )
|
|
local p = self.DB.profile
|
|
|
|
local default = class.packs[ name ]
|
|
|
|
if default then
|
|
local data = self.DeserializeActionPack( default.import )
|
|
|
|
if data and type( data ) == "table" then
|
|
p.packs[ name ] = data.payload
|
|
data.payload.version = default.version
|
|
data.payload.date = default.version
|
|
data.payload.builtIn = true
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
ns.restoreDefaults = function( category, purge )
|
|
end
|
|
|
|
|
|
ns.isDefault = function( name, category )
|
|
if not name or not category then
|
|
return false
|
|
end
|
|
|
|
for i, default in ipairs( class.defaults ) do
|
|
if default.type == category and default.name == name then
|
|
return true, i
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function Hekili:NewSpecialization( specID, isRanged, icon )
|
|
|
|
if not specID or specID < 0 then return end
|
|
|
|
isRanged = isRanged or ns.Specializations[ specID ].ranged
|
|
|
|
local id, name, _, texture, role, pClass
|
|
|
|
if Hekili.IsRetail() and specID > 0 then id, name, _, texture, role, pClass = GetSpecializationInfoByID( specID )
|
|
else
|
|
id = specID
|
|
texture = icon
|
|
end
|
|
|
|
if not id then
|
|
Hekili:Error( "Unable to generate specialization DB for spec ID #" .. specID .. "." )
|
|
return nil
|
|
end
|
|
|
|
if specID ~= 0 then
|
|
class.initialized = true
|
|
end
|
|
|
|
local token = getSpecializationKey( id )
|
|
|
|
local spec = class.specs[ id ] or {
|
|
id = id,
|
|
key = token,
|
|
name = name,
|
|
texture = texture,
|
|
role = role,
|
|
class = pClass,
|
|
melee = not isRanged,
|
|
|
|
resources = {},
|
|
resourceAuras = {},
|
|
primaryResource = nil,
|
|
primaryStat = nil,
|
|
|
|
talents = {},
|
|
pvptalents = {},
|
|
powers = {},
|
|
|
|
auras = {},
|
|
pseudoAuras = 0,
|
|
|
|
abilities = {},
|
|
pseudoAbilities = 0,
|
|
itemAbilities = 0,
|
|
pendingItemSpells = {},
|
|
|
|
pets = {},
|
|
totems = {},
|
|
|
|
potions = {},
|
|
|
|
ranges = {},
|
|
settings = {},
|
|
|
|
stateExprs = {}, -- expressions are returned as values and take no args.
|
|
stateFuncs = {}, -- functions can take arguments and can be used as helper functions in handlers.
|
|
stateTables = {}, -- tables are... tables.
|
|
|
|
gear = {},
|
|
setBonuses = {},
|
|
|
|
hooks = {},
|
|
funcHooks = {},
|
|
phases = {},
|
|
interrupts = {},
|
|
|
|
dual_cast = {},
|
|
|
|
packs = {},
|
|
options = {},
|
|
|
|
variables = {}
|
|
}
|
|
|
|
class.num = class.num + 1
|
|
|
|
for key, func in pairs( HekiliSpecMixin ) do
|
|
spec[ key ] = func
|
|
end
|
|
|
|
class.specs[ id ] = spec
|
|
return spec
|
|
end
|
|
|
|
function Hekili:GetSpecialization( specID )
|
|
if not specID then return class.specs[ 0 ] end
|
|
return class.specs[ specID ]
|
|
end
|
|
|
|
|
|
class.file = UnitClassBase( "player" )
|
|
local all = Hekili:NewSpecialization( 0, "All", "Interface\\Addons\\Hekili\\Textures\\LOGO-WHITE.blp" )
|
|
|
|
------------------------------
|
|
-- SHARED SPELLS/BUFFS/ETC. --
|
|
------------------------------
|
|
|
|
---@diagnostic disable-next-line: need-check-nil
|
|
all:RegisterAuras( {
|
|
|
|
enlisted_a = {
|
|
id = 282559,
|
|
duration = 3600,
|
|
},
|
|
|
|
enlisted_b = {
|
|
id = 289954,
|
|
duration = 3600,
|
|
},
|
|
|
|
enlisted_c = {
|
|
id = 269083,
|
|
duration = 3600,
|
|
},
|
|
|
|
enlisted = {
|
|
alias = { "enlisted_c", "enlisted_b", "enlisted_a" },
|
|
aliasMode = "first",
|
|
aliasType = "buff",
|
|
duration = 3600,
|
|
},
|
|
|
|
-- The War Within M+ Affix auras
|
|
-- Haste
|
|
cosmic_ascension = {
|
|
id = 461910,
|
|
duration = 30,
|
|
max_stack = 1
|
|
},
|
|
-- Crit
|
|
rift_essence = {
|
|
id = 465136,
|
|
duration = 30,
|
|
max_stack = 1
|
|
},
|
|
-- Mastery
|
|
void_essence = {
|
|
id = 463767,
|
|
duration = 30,
|
|
max_stack = 1
|
|
},
|
|
-- CDR & Vers
|
|
voidbinding = {
|
|
id = 462661,
|
|
duration = 30,
|
|
max_stack = 1
|
|
},
|
|
-- Priory of the Sacred Flame
|
|
blessing_of_the_sacred_flame = {
|
|
id = 435088,
|
|
duration = 1800,
|
|
max_stack = 1
|
|
},
|
|
|
|
-- Can be used in GCD calculation.
|
|
shadowform = {
|
|
id = 232698,
|
|
duration = 3600,
|
|
max_stack = 1,
|
|
},
|
|
|
|
voidform = {
|
|
id = 194249,
|
|
duration = 15,
|
|
max_stack = 1,
|
|
},
|
|
|
|
adrenaline_rush = {
|
|
id = 13750,
|
|
duration = 20,
|
|
max_stack = 1,
|
|
},
|
|
|
|
-- Bloodlusts
|
|
ancient_hysteria = {
|
|
id = 90355,
|
|
shared = "player", -- use anyone's buff on the player, not just player's.
|
|
duration = 40,
|
|
max_stack = 1,
|
|
},
|
|
|
|
heroism = {
|
|
id = 32182,
|
|
shared = "player", -- use anyone's buff on the player, not just player's.
|
|
duration = 40,
|
|
max_stack = 1,
|
|
},
|
|
|
|
time_warp = {
|
|
id = 80353,
|
|
shared = "player", -- use anyone's buff on the player, not just player's.
|
|
duration = 40,
|
|
max_stack = 1,
|
|
},
|
|
|
|
netherwinds = {
|
|
id = 160452,
|
|
shared = "player", -- use anyone's buff on the player, not just player's.
|
|
duration = 40,
|
|
max_stack = 1,
|
|
},
|
|
|
|
primal_rage = {
|
|
id = 264667,
|
|
shared = "player", -- use anyone's buff on the player, not just player's.
|
|
duration = 40,
|
|
max_stack = 1,
|
|
},
|
|
|
|
drums_of_deathly_ferocity = {
|
|
id = 309658,
|
|
shared = "player", -- use anyone's buff on the player, not just player's.
|
|
duration = 40,
|
|
max_stack = 1,
|
|
},
|
|
|
|
bloodlust = {
|
|
alias = { "ancient_hysteria", "bloodlust_actual", "drums_of_deathly_ferocity", "fury_of_the_aspects", "heroism", "netherwinds", "primal_rage", "time_warp", "harriers_cry" },
|
|
aliasMode = "first",
|
|
aliasType = "buff",
|
|
duration = 3600,
|
|
},
|
|
|
|
bloodlust_actual = {
|
|
id = 2825,
|
|
duration = 40,
|
|
shared = "player",
|
|
max_stack = 1,
|
|
},
|
|
|
|
exhaustion = {
|
|
id = 57723,
|
|
duration = 600,
|
|
shared = "player",
|
|
max_stack = 1,
|
|
copy = 390435
|
|
},
|
|
|
|
insanity = {
|
|
id = 95809,
|
|
duration = 600,
|
|
shared = "player",
|
|
max_stack = 1
|
|
},
|
|
|
|
temporal_displacement = {
|
|
id = 80354,
|
|
duration = 600,
|
|
shared = "player",
|
|
max_stack = 1
|
|
},
|
|
|
|
fury_of_the_aspects = {
|
|
id = 390386,
|
|
duration = 40,
|
|
max_stack = 1,
|
|
shared = "player",
|
|
},
|
|
|
|
harriers_cry = {
|
|
id = 466904,
|
|
duration = 40,
|
|
max_stack = 1,
|
|
shared = "player"
|
|
},
|
|
|
|
mark_of_the_wild = {
|
|
id = 1126,
|
|
duration = 3600,
|
|
max_stack = 1,
|
|
shared = "player",
|
|
},
|
|
|
|
fatigued = {
|
|
id = 264689,
|
|
duration = 600,
|
|
shared = "player",
|
|
max_stack = 1
|
|
},
|
|
|
|
sated = {
|
|
alias = { "exhaustion", "fatigued", "insanity", "sated_actual", "temporal_displacement" },
|
|
aliasMode = "first",
|
|
aliasType = "debuff",
|
|
duration = 3600,
|
|
},
|
|
|
|
sated_actual = {
|
|
id = 57724,
|
|
duration = 600,
|
|
shared = "player",
|
|
max_stack = 1,
|
|
},
|
|
|
|
blessing_of_the_bronze = {
|
|
alias = {
|
|
"blessing_of_the_bronze_evoker",
|
|
"blessing_of_the_bronze_deathknight",
|
|
"blessing_of_the_bronze_demonhunter",
|
|
"blessing_of_the_bronze_druid",
|
|
"blessing_of_the_bronze_hunter",
|
|
"blessing_of_the_bronze_mage",
|
|
"blessing_of_the_bronze_monk",
|
|
"blessing_of_the_bronze_paladin",
|
|
"blessing_of_the_bronze_priest",
|
|
"blessing_of_the_bronze_rogue",
|
|
"blessing_of_the_bronze_shaman",
|
|
"blessing_of_the_bronze_warlock",
|
|
"blessing_of_the_bronze_warrior",
|
|
},
|
|
aliasType = "buff",
|
|
aliasMode = "longest"
|
|
},
|
|
-- Can always be seen and tracked by the Hunter.; Damage taken increased by $428402s4% while above $s3% health.
|
|
-- https://wowhead.com/beta/spell=257284
|
|
hunters_mark = {
|
|
id = 257284,
|
|
duration = 3600,
|
|
tick_time = 0.5,
|
|
type = "Magic",
|
|
max_stack = 1,
|
|
shared = "target"
|
|
},
|
|
chaos_brand = {
|
|
id = 1490,
|
|
duration = 3600,
|
|
type = "Magic",
|
|
max_stack = 1,
|
|
shared = "target"
|
|
},
|
|
blessing_of_the_bronze_deathknight = {
|
|
id = 381732,
|
|
duration = 3600,
|
|
max_stack = 1,
|
|
shared = "player"
|
|
},
|
|
blessing_of_the_bronze_demonhunter = {
|
|
id = 381741,
|
|
duration = 3600,
|
|
max_stack = 1,
|
|
shared = "player"
|
|
},
|
|
blessing_of_the_bronze_druid = {
|
|
id = 381746,
|
|
duration = 3600,
|
|
max_stack = 1,
|
|
shared = "player"
|
|
},
|
|
blessing_of_the_bronze_evoker = {
|
|
id = 381748,
|
|
duration = 3600,
|
|
max_stack = 1,
|
|
shared = "player"
|
|
},
|
|
blessing_of_the_bronze_hunter = {
|
|
id = 364342,
|
|
duration = 3600,
|
|
max_stack = 1,
|
|
shared = "player"
|
|
},
|
|
blessing_of_the_bronze_mage = {
|
|
id = 381750,
|
|
duration = 3600,
|
|
max_stack = 1,
|
|
shared = "player"
|
|
},
|
|
blessing_of_the_bronze_monk = {
|
|
id = 381751,
|
|
duration = 3600,
|
|
max_stack = 1,
|
|
shared = "player"
|
|
},
|
|
blessing_of_the_bronze_paladin = {
|
|
id = 381752,
|
|
duration = 3600,
|
|
max_stack = 1,
|
|
shared = "player"
|
|
},
|
|
blessing_of_the_bronze_priest = {
|
|
id = 381753,
|
|
duration = 3600,
|
|
max_stack = 1,
|
|
shared = "player"
|
|
},
|
|
blessing_of_the_bronze_rogue = {
|
|
id = 381754,
|
|
duration = 3600,
|
|
max_stack = 1,
|
|
shared = "player"
|
|
},
|
|
blessing_of_the_bronze_shaman = {
|
|
id = 381756,
|
|
duration = 3600,
|
|
max_stack = 1,
|
|
shared = "player"
|
|
},
|
|
blessing_of_the_bronze_warlock = {
|
|
id = 381757,
|
|
duration = 3600,
|
|
max_stack = 1,
|
|
shared = "player"
|
|
},
|
|
blessing_of_the_bronze_warrior = {
|
|
id = 381758,
|
|
duration = 3600,
|
|
max_stack = 1,
|
|
shared = "player"
|
|
},
|
|
|
|
power_infusion = {
|
|
id = 10060,
|
|
duration = 20,
|
|
max_stack = 1,
|
|
shared = "player",
|
|
dot = "buff"
|
|
},
|
|
|
|
battle_shout = {
|
|
id = 6673,
|
|
duration = 3600,
|
|
max_stack = 1,
|
|
shared = "player",
|
|
dot = "buff"
|
|
},
|
|
|
|
-- Mastery increased by $w1% and auto attacks have a $h% chance to instantly strike again.
|
|
skyfury = {
|
|
id = 462854,
|
|
duration = 3600,
|
|
max_stack = 1,
|
|
shared = "player",
|
|
dot = "buff"
|
|
},
|
|
|
|
-- SL Season 3
|
|
decrypted_urh_cypher = {
|
|
id = 368239,
|
|
duration = 10,
|
|
max_stack = 1,
|
|
},
|
|
|
|
old_war = {
|
|
id = 188028,
|
|
duration = 25,
|
|
},
|
|
|
|
deadly_grace = {
|
|
id = 188027,
|
|
duration = 25,
|
|
},
|
|
|
|
prolonged_power = {
|
|
id = 229206,
|
|
duration = 60,
|
|
},
|
|
|
|
dextrous = {
|
|
id = 146308,
|
|
duration = 20,
|
|
},
|
|
|
|
vicious = {
|
|
id = 148903,
|
|
duration = 10,
|
|
},
|
|
|
|
-- WoD Legendaries
|
|
archmages_incandescence_agi = {
|
|
id = 177161,
|
|
duration = 10,
|
|
},
|
|
|
|
archmages_incandescence_int = {
|
|
id = 177159,
|
|
duration = 10,
|
|
},
|
|
|
|
archmages_incandescence_str = {
|
|
id = 177160,
|
|
duration = 10,
|
|
},
|
|
|
|
archmages_greater_incandescence_agi = {
|
|
id = 177172,
|
|
duration = 10,
|
|
},
|
|
|
|
archmages_greater_incandescence_int = {
|
|
id = 177176,
|
|
duration = 10,
|
|
},
|
|
|
|
archmages_greater_incandescence_str = {
|
|
id = 177175,
|
|
duration = 10,
|
|
},
|
|
|
|
maalus = {
|
|
id = 187620,
|
|
duration = 15,
|
|
},
|
|
|
|
thorasus = {
|
|
id = 187619,
|
|
duration = 15,
|
|
},
|
|
|
|
sephuzs_secret = {
|
|
id = 208052,
|
|
duration = 10,
|
|
max_stack = 1,
|
|
},
|
|
|
|
str_agi_int = {
|
|
duration = 3600,
|
|
},
|
|
|
|
stamina = {
|
|
duration = 3600,
|
|
},
|
|
|
|
attack_power_multiplier = {
|
|
duration = 3600,
|
|
},
|
|
|
|
haste = {
|
|
duration = 3600,
|
|
},
|
|
|
|
spell_power_multiplier = {
|
|
duration = 3600,
|
|
},
|
|
|
|
critical_strike = {
|
|
duration = 3600,
|
|
},
|
|
|
|
mastery = {
|
|
duration = 3600,
|
|
},
|
|
|
|
versatility = {
|
|
duration = 3600,
|
|
},
|
|
|
|
empowering = {
|
|
name = "Empowering",
|
|
duration = 3600,
|
|
generate = function( t )
|
|
local e = state.empowerment
|
|
local ability = class.abilities[ e.spell ]
|
|
local spell = ability and ability.key or e.spell
|
|
|
|
t.name = ability and ability.name or "Empowering"
|
|
t.count = e.start > 0 and 1 or 0
|
|
t.expires = e.hold
|
|
t.applied = e.start - 0.1
|
|
t.duration = e.hold - t.applied
|
|
t.v1 = ability and ability.id or 0
|
|
t.v2 = 0
|
|
t.v3 = 0
|
|
t.spell = spell
|
|
t.caster = "player"
|
|
|
|
if t.remains > 0 then
|
|
local timeDiff = state.now - e.start - 0.1
|
|
if Hekili.ActiveDebug then
|
|
Hekili:Debug( "Empowerment spell: %s[%.2f], unit: %s; rewinding %.2f...", t.name, t.remains, t.caster, timeDiff )
|
|
end
|
|
state.now = state.now - timeDiff
|
|
end
|
|
end,
|
|
},
|
|
|
|
casting = {
|
|
name = "Casting",
|
|
generate = function( t, auraType )
|
|
local unit = auraType == "debuff" and "target" or "player"
|
|
|
|
if unit == "player" and state.buff.empowering.up then
|
|
removeBuff( "casting" )
|
|
return
|
|
end
|
|
|
|
if unit == "player" or UnitCanAttack( "player", unit ) then
|
|
local spell, _, _, startCast, endCast, _, _, notInterruptible, spellID = UnitCastingInfo( unit )
|
|
|
|
if spell then
|
|
startCast = startCast / 1000
|
|
endCast = endCast / 1000
|
|
|
|
t.name = spell
|
|
t.count = 1
|
|
t.expires = endCast
|
|
t.applied = startCast
|
|
t.duration = endCast - startCast
|
|
t.v1 = spellID
|
|
t.v2 = notInterruptible and 1 or 0
|
|
t.v3 = 0
|
|
t.caster = unit
|
|
|
|
if unit ~= "target" then return end
|
|
|
|
if state.target.is_dummy then
|
|
-- Pretend that all casts by target dummies are interruptible.
|
|
if Hekili.ActiveDebug then Hekili:Debug( "Cast '%s' is fake-interruptible", spell ) end
|
|
t.v2 = 0
|
|
|
|
elseif Hekili.DB.profile.toggles.interrupts.filterCasts and class.spellFilters[ state.instance_id ] and class.interruptibleFilters and not class.interruptibleFilters[ spellID ] then
|
|
if Hekili.ActiveDebug then Hekili:Debug( "Cast '%s' not interruptible per user preference.", spell ) end
|
|
t.v2 = 1
|
|
end
|
|
|
|
return
|
|
end
|
|
|
|
spell, _, _, startCast, endCast, _, notInterruptible, spellID = UnitChannelInfo( unit )
|
|
startCast = ( startCast or 0 ) / 1000
|
|
endCast = ( endCast or 0 ) / 1000
|
|
local duration = endCast - startCast
|
|
|
|
-- Channels greater than 10 seconds are nonsense. Probably.
|
|
if spell and duration <= 10 then
|
|
t.name = spell
|
|
t.count = 1
|
|
t.expires = endCast
|
|
t.applied = startCast
|
|
t.duration = duration
|
|
t.v1 = spellID
|
|
t.v2 = notInterruptible and 1 or 0
|
|
t.v3 = 1 -- channeled.
|
|
t.caster = unit
|
|
|
|
if class.abilities[ spellID ] and class.abilities[ spellID ].dontChannel then
|
|
removeBuff( "casting" )
|
|
return
|
|
end
|
|
|
|
if unit ~= "target" then return end
|
|
|
|
if state.target.is_dummy then
|
|
-- Pretend that all casts by target dummies are interruptible.
|
|
if Hekili.ActiveDebug then Hekili:Debug( "Channel '%s' is fake-interruptible", spell ) end
|
|
t.v2 = 0
|
|
|
|
elseif Hekili.DB.profile.toggles.interrupts.filterCasts and class.spellFilters[ state.instance_id ] and class.interruptibleFilters and not class.interruptibleFilters[ spellID ] then
|
|
if Hekili.ActiveDebug then Hekili:Debug( "Channel '%s' not interruptible per user preference.", spell ) end
|
|
t.v2 = 1
|
|
end
|
|
|
|
return
|
|
end
|
|
end
|
|
|
|
t.name = "Casting"
|
|
t.count = 0
|
|
t.expires = 0
|
|
t.applied = 0
|
|
t.v1 = 0
|
|
t.v2 = 0
|
|
t.v3 = 0
|
|
t.caster = unit
|
|
end,
|
|
},
|
|
|
|
movement = {
|
|
duration = 5,
|
|
max_stack = 1,
|
|
generate = function ()
|
|
local m = buff.movement
|
|
|
|
if moving then
|
|
m.count = 1
|
|
m.expires = query_time + 5
|
|
m.applied = query_time
|
|
m.caster = "player"
|
|
return
|
|
end
|
|
|
|
m.count = 0
|
|
m.expires = 0
|
|
m.applied = 0
|
|
m.caster = "nobody"
|
|
end,
|
|
},
|
|
|
|
repeat_performance = {
|
|
id = 304409,
|
|
duration = 30,
|
|
max_stack = 1,
|
|
},
|
|
|
|
-- Why do we have this, again?
|
|
unknown_buff = {},
|
|
|
|
berserking = {
|
|
id = 26297,
|
|
duration = 10,
|
|
},
|
|
|
|
hyper_organic_light_originator = {
|
|
id = 312924,
|
|
duration = 6,
|
|
},
|
|
|
|
blood_fury = {
|
|
id = 20572,
|
|
duration = 15,
|
|
},
|
|
|
|
shadowmeld = {
|
|
id = 58984,
|
|
duration = 3600,
|
|
},
|
|
|
|
ferocity_of_the_frostwolf = {
|
|
id = 274741,
|
|
duration = 15,
|
|
},
|
|
|
|
might_of_the_blackrock = {
|
|
id = 274742,
|
|
duration = 15,
|
|
},
|
|
|
|
zeal_of_the_burning_blade = {
|
|
id = 274740,
|
|
duration = 15,
|
|
},
|
|
|
|
rictus_of_the_laughing_skull = {
|
|
id = 274739,
|
|
duration = 15,
|
|
},
|
|
|
|
ancestral_call = {
|
|
duration = 15,
|
|
alias = { "ferocity_of_the_frostwolf", "might_of_the_blackrock", "zeal_of_the_burning_blade", "rictus_of_the_laughing_skull" },
|
|
aliasMode = "first",
|
|
},
|
|
|
|
arcane_pulse = {
|
|
id = 260369,
|
|
duration = 12,
|
|
},
|
|
|
|
fireblood = {
|
|
id = 273104,
|
|
duration = 8,
|
|
},
|
|
|
|
out_of_range = {
|
|
generate = function ( oor )
|
|
oor.rangeSpell = rawget( oor, "rangeSpell" ) or settings.spec.rangeChecker or class.specs[ state.spec.id ].ranges[ 1 ]
|
|
|
|
if LSR.IsSpellInRange( class.abilities[ oor.rangeSpell ].name, "target" ) ~= 1 then
|
|
oor.count = 1
|
|
oor.applied = query_time
|
|
oor.expires = query_time + 3600
|
|
oor.caster = "player"
|
|
oor.v1 = oor.rangeSpell
|
|
return
|
|
end
|
|
|
|
oor.count = 0
|
|
oor.applied = 0
|
|
oor.expires = 0
|
|
oor.caster = "nobody"
|
|
end,
|
|
},
|
|
|
|
loss_of_control = {
|
|
duration = 10,
|
|
generate = function( t )
|
|
local max_events = GetActiveLossOfControlDataCount()
|
|
|
|
if max_events > 0 then
|
|
local spell, start, duration, remains = 0, 0, 0, 0
|
|
|
|
for i = 1, max_events do
|
|
local event = GetActiveLossOfControlData( i )
|
|
|
|
if event and event.lockoutSchool == 0 and event.startTime and event.startTime > 0 and event.timeRemaining and event.timeRemaining > 0 and event.timeRemaining > remains then
|
|
spell = event.spellID
|
|
start = event.startTime
|
|
duration = event.duration
|
|
remains = event.timeRemaining
|
|
end
|
|
end
|
|
|
|
if start + duration > query_time then
|
|
t.count = 1
|
|
t.expires = start + duration
|
|
t.applied = start
|
|
t.duration = duration
|
|
t.caster = "anybody"
|
|
t.v1 = spell
|
|
return
|
|
end
|
|
end
|
|
|
|
t.count = 0
|
|
t.expires = 0
|
|
t.applied = 0
|
|
t.duration = 10
|
|
t.caster = "nobody"
|
|
t.v1 = 0
|
|
end,
|
|
},
|
|
|
|
disoriented = { -- Disorients (e.g., Polymorph, Dragon’s Breath, Blind)
|
|
duration = 10,
|
|
generate = function( t )
|
|
local max_events = GetActiveLossOfControlDataCount()
|
|
|
|
if max_events > 0 then
|
|
local spell, start, duration, remains = 0, 0, 0, 0
|
|
|
|
for i = 1, max_events do
|
|
local event = GetActiveLossOfControlData( i )
|
|
|
|
if event and event.locType == "CONFUSE"
|
|
and event.startTime and event.startTime > 0
|
|
and event.timeRemaining and event.timeRemaining > 0
|
|
and event.timeRemaining > remains then
|
|
|
|
spell = event.spellID
|
|
start = event.startTime
|
|
duration = event.duration
|
|
remains = event.timeRemaining
|
|
end
|
|
end
|
|
|
|
if start + duration > query_time then
|
|
t.count = 1
|
|
t.expires = start + duration
|
|
t.applied = start
|
|
t.duration = duration
|
|
t.caster = "anybody"
|
|
t.v1 = spell
|
|
return
|
|
end
|
|
end
|
|
|
|
t.count = 0
|
|
t.expires = 0
|
|
t.applied = 0
|
|
t.duration = 10
|
|
t.caster = "nobody"
|
|
t.v1 = 0
|
|
end,
|
|
},
|
|
|
|
feared = {
|
|
duration = 10,
|
|
generate = function( t )
|
|
local max_events = GetActiveLossOfControlDataCount()
|
|
|
|
if max_events > 0 then
|
|
local spell, start, duration, remains = 0, 0, 0, 0
|
|
|
|
for i = 1, max_events do
|
|
local event = GetActiveLossOfControlData( i )
|
|
|
|
if event and ( event.locType == "FEAR" or event.locType == "FEAR_MECHANIC" or event.locType == "HORROR" )
|
|
and event.startTime and event.startTime > 0
|
|
and event.timeRemaining and event.timeRemaining > 0
|
|
and event.timeRemaining > remains then
|
|
|
|
spell = event.spellID
|
|
start = event.startTime
|
|
duration = event.duration
|
|
remains = event.timeRemaining
|
|
end
|
|
end
|
|
|
|
if start + duration > query_time then
|
|
t.count = 1
|
|
t.expires = start + duration
|
|
t.applied = start
|
|
t.duration = duration
|
|
t.caster = "anybody"
|
|
t.v1 = spell
|
|
return
|
|
end
|
|
end
|
|
|
|
t.count = 0
|
|
t.expires = 0
|
|
t.applied = 0
|
|
t.duration = 10
|
|
t.caster = "nobody"
|
|
t.v1 = 0
|
|
end,
|
|
},
|
|
|
|
incapacitated = { -- Effects like Sap, Freezing Trap, Gouge
|
|
duration = 10,
|
|
generate = function( t )
|
|
local max_events = GetActiveLossOfControlDataCount()
|
|
|
|
if max_events > 0 then
|
|
local spell, start, duration, remains = 0, 0, 0, 0
|
|
|
|
for i = 1, max_events do
|
|
local event = GetActiveLossOfControlData( i )
|
|
|
|
if event and event.locType == "STUN"
|
|
and event.startTime and event.startTime > 0
|
|
and event.timeRemaining and event.timeRemaining > 0
|
|
and event.timeRemaining > remains then
|
|
|
|
spell = event.spellID
|
|
start = event.startTime
|
|
duration = event.duration
|
|
remains = event.timeRemaining
|
|
end
|
|
end
|
|
|
|
if start + duration > query_time then
|
|
t.count = 1
|
|
t.expires = start + duration
|
|
t.applied = start
|
|
t.duration = duration
|
|
t.caster = "anybody"
|
|
t.v1 = spell
|
|
return
|
|
end
|
|
end
|
|
|
|
t.count = 0
|
|
t.expires = 0
|
|
t.applied = 0
|
|
t.duration = 10
|
|
t.caster = "nobody"
|
|
t.v1 = 0
|
|
end,
|
|
copy = "sapped"
|
|
},
|
|
|
|
rooted = {
|
|
duration = 10,
|
|
generate = function( t )
|
|
local max_events = GetActiveLossOfControlDataCount()
|
|
|
|
if max_events > 0 then
|
|
local spell, start, duration, remains = 0, 0, 0, 0
|
|
|
|
for i = 1, max_events do
|
|
local event = GetActiveLossOfControlData( i )
|
|
|
|
if event and event.locType == "ROOT" and event.startTime and event.startTime > 0 and event.timeRemaining and event.timeRemaining > 0 and event.timeRemaining > remains then
|
|
spell = event.spellID
|
|
start = event.startTime
|
|
duration = event.duration
|
|
remains = event.timeRemaining
|
|
end
|
|
end
|
|
|
|
if start + duration > query_time then
|
|
t.count = 1
|
|
t.expires = start + duration
|
|
t.applied = start
|
|
t.duration = duration
|
|
t.caster = "anybody"
|
|
t.v1 = spell
|
|
return
|
|
end
|
|
end
|
|
|
|
t.count = 0
|
|
t.expires = 0
|
|
t.applied = 0
|
|
t.duration = 10
|
|
t.caster = "nobody"
|
|
t.v1 = 0
|
|
end,
|
|
},
|
|
|
|
snared = {
|
|
duration = 10,
|
|
generate = function( t )
|
|
local max_events = GetActiveLossOfControlDataCount()
|
|
|
|
if max_events > 0 then
|
|
local spell, start, duration, remains = 0, 0, 0, 0
|
|
|
|
for i = 1, max_events do
|
|
local event = GetActiveLossOfControlData( i )
|
|
|
|
if event and event.locType == "SNARE" and event.startTime and event.startTime > 0 and event.timeRemaining and event.timeRemaining > 0 and event.timeRemaining > remains then
|
|
spell = event.spellID
|
|
start = event.startTime
|
|
duration = event.duration
|
|
remains = event.timeRemaining
|
|
end
|
|
end
|
|
|
|
if start + duration > query_time then
|
|
t.count = 1
|
|
t.expires = start + duration
|
|
t.applied = start
|
|
t.duration = duration
|
|
t.caster = "anybody"
|
|
t.v1 = spell
|
|
return
|
|
end
|
|
end
|
|
|
|
t.count = 0
|
|
t.expires = 0
|
|
t.applied = 0
|
|
t.duration = 10
|
|
t.caster = "nobody"
|
|
t.v1 = 0
|
|
end,
|
|
copy = "slowed"
|
|
},
|
|
|
|
stunned = { -- Shorter stuns (e.g., Kidney Shot, Cheap Shot, Bash)
|
|
duration = 10,
|
|
generate = function( t )
|
|
local max_events = GetActiveLossOfControlDataCount()
|
|
|
|
if max_events > 0 then
|
|
local spell, start, duration, remains = 0, 0, 0, 0
|
|
|
|
for i = 1, max_events do
|
|
local event = GetActiveLossOfControlData( i )
|
|
|
|
if event and event.locType == "STUN_MECHANIC"
|
|
and event.startTime and event.startTime > 0
|
|
and event.timeRemaining and event.timeRemaining > 0
|
|
and event.timeRemaining > remains then
|
|
|
|
spell = event.spellID
|
|
start = event.startTime
|
|
duration = event.duration
|
|
remains = event.timeRemaining
|
|
end
|
|
end
|
|
|
|
if start + duration > query_time then
|
|
t.count = 1
|
|
t.expires = start + duration
|
|
t.applied = start
|
|
t.duration = duration
|
|
t.caster = "anybody"
|
|
t.v1 = spell
|
|
return
|
|
end
|
|
end
|
|
|
|
t.count = 0
|
|
t.expires = 0
|
|
t.applied = 0
|
|
t.duration = 10
|
|
t.caster = "nobody"
|
|
t.v1 = 0
|
|
end,
|
|
},
|
|
|
|
dispellable_curse = {
|
|
generate = function( t )
|
|
local i = 1
|
|
local name, _, count, debuffType, duration, expirationTime = UnitDebuff( "player", i, "RAID" )
|
|
|
|
while( name ) do
|
|
if debuffType == "Curse" then break end
|
|
|
|
i = i + 1
|
|
name, _, count, debuffType, duration, expirationTime = UnitDebuff( "player", i, "RAID" )
|
|
end
|
|
|
|
if name then
|
|
t.count = count > 0 and count or 1
|
|
t.expires = expirationTime > 0 and expirationTime or query_time + 5
|
|
t.applied = expirationTime > 0 and ( expirationTime - duration ) or query_time
|
|
t.caster = "nobody"
|
|
return
|
|
end
|
|
|
|
t.count = 0
|
|
t.expires = 0
|
|
t.applied = 0
|
|
t.caster = "nobody"
|
|
end,
|
|
},
|
|
|
|
dispellable_poison = {
|
|
generate = function( t )
|
|
local i = 1
|
|
local name, _, count, debuffType, duration, expirationTime = UnitDebuff( "player", i, "RAID" )
|
|
|
|
while( name ) do
|
|
if debuffType == "Poison" then break end
|
|
|
|
i = i + 1
|
|
name, _, count, debuffType, duration, expirationTime = UnitDebuff( "player", i, "RAID" )
|
|
end
|
|
|
|
if name then
|
|
t.count = count > 0 and count or 1
|
|
t.expires = expirationTime > 0 and expirationTime or query_time + 5
|
|
t.applied = expirationTime > 0 and ( expirationTime - duration ) or query_time
|
|
t.caster = "nobody"
|
|
return
|
|
end
|
|
|
|
t.count = 0
|
|
t.expires = 0
|
|
t.applied = 0
|
|
t.caster = "nobody"
|
|
end,
|
|
},
|
|
|
|
dispellable_disease = {
|
|
generate = function( t )
|
|
local i = 1
|
|
local name, _, count, debuffType, duration, expirationTime = UnitDebuff( "player", i, "RAID" )
|
|
|
|
while( name ) do
|
|
if debuffType == "Disease" then break end
|
|
|
|
i = i + 1
|
|
name, _, count, debuffType, duration, expirationTime = UnitDebuff( "player", i, "RAID" )
|
|
end
|
|
|
|
if name then
|
|
t.count = count > 0 and count or 1
|
|
t.expires = expirationTime > 0 and expirationTime or query_time + 5
|
|
t.applied = expirationTime > 0 and ( expirationTime - duration ) or query_time
|
|
t.caster = "nobody"
|
|
return
|
|
end
|
|
|
|
t.count = 0
|
|
t.expires = 0
|
|
t.applied = 0
|
|
t.caster = "nobody"
|
|
end,
|
|
},
|
|
|
|
dispellable_magic = {
|
|
generate = function( t, auraType )
|
|
if auraType == "buff" then
|
|
local i = 1
|
|
local name, _, count, debuffType, duration, expirationTime, _, canDispel = UnitBuff( "target", i )
|
|
|
|
while( name ) do
|
|
if debuffType == "Magic" and canDispel then break end
|
|
|
|
i = i + 1
|
|
name, _, count, debuffType, duration, expirationTime, _, canDispel = UnitBuff( "target", i )
|
|
end
|
|
|
|
if canDispel then
|
|
t.count = count > 0 and count or 1
|
|
t.expires = expirationTime > 0 and expirationTime or query_time + 5
|
|
t.applied = expirationTime > 0 and ( expirationTime - duration ) or query_time
|
|
t.caster = "nobody"
|
|
return
|
|
end
|
|
|
|
else
|
|
local i = 1
|
|
local name, _, count, debuffType, duration, expirationTime = UnitDebuff( "player", i, "RAID" )
|
|
|
|
while( name ) do
|
|
if debuffType == "Magic" then break end
|
|
|
|
i = i + 1
|
|
name, _, count, debuffType, duration, expirationTime = UnitDebuff( "player", i, "RAID" )
|
|
end
|
|
|
|
if name then
|
|
t.count = count > 0 and count or 1
|
|
t.expires = expirationTime > 0 and expirationTime or query_time + 5
|
|
t.applied = expirationTime > 0 and ( expirationTime - duration ) or query_time
|
|
t.caster = "nobody"
|
|
return
|
|
end
|
|
|
|
end
|
|
|
|
t.count = 0
|
|
t.expires = 0
|
|
t.applied = 0
|
|
t.caster = "nobody"
|
|
end,
|
|
},
|
|
|
|
stealable_magic = {
|
|
generate = function( t )
|
|
if UnitCanAttack( "player", "target" ) then
|
|
local i = 1
|
|
local name, _, count, debuffType, duration, expirationTime, _, canDispel = UnitBuff( "target", i )
|
|
|
|
while( name ) do
|
|
if debuffType == "Magic" and canDispel then break end
|
|
|
|
i = i + 1
|
|
name, _, count, debuffType, duration, expirationTime, _, canDispel = UnitBuff( "target", i )
|
|
end
|
|
|
|
if canDispel then
|
|
t.count = count > 0 and count or 1
|
|
t.expires = expirationTime > 0 and expirationTime or query_time + 5
|
|
t.applied = expirationTime > 0 and ( expirationTime - duration ) or query_time
|
|
t.caster = "nobody"
|
|
return
|
|
end
|
|
end
|
|
|
|
t.count = 0
|
|
t.expires = 0
|
|
t.applied = 0
|
|
t.caster = "nobody"
|
|
end,
|
|
},
|
|
|
|
reversible_magic = {
|
|
generate = function( t )
|
|
local i = 1
|
|
local name, _, count, debuffType, duration, expirationTime = UnitDebuff( "player", i, "RAID" )
|
|
|
|
while( name ) do
|
|
if debuffType == "Magic" then break end
|
|
|
|
i = i + 1
|
|
name, _, count, debuffType, duration, expirationTime = UnitDebuff( "player", i, "RAID" )
|
|
end
|
|
|
|
if name then
|
|
t.count = count > 0 and count or 1
|
|
t.expires = expirationTime > 0 and expirationTime or query_time + 5
|
|
t.applied = expirationTime > 0 and ( expirationTime - duration ) or query_time
|
|
t.caster = "nobody"
|
|
return
|
|
end
|
|
|
|
t.count = 0
|
|
t.expires = 0
|
|
t.applied = 0
|
|
t.caster = "nobody"
|
|
end,
|
|
},
|
|
|
|
dispellable_enrage = {
|
|
generate = function( t )
|
|
if UnitCanAttack( "player", "target" ) then
|
|
local i = 1
|
|
local name, _, count, debuffType, duration, expirationTime, _, canDispel = UnitBuff( "target", i )
|
|
|
|
while( name ) do
|
|
if debuffType == "" and canDispel then break end
|
|
|
|
i = i + 1
|
|
name, _, count, debuffType, duration, expirationTime, _, canDispel = UnitBuff( "target", i )
|
|
end
|
|
|
|
if canDispel then
|
|
t.count = count > 0 and count or 1
|
|
t.expires = expirationTime > 0 and expirationTime or query_time + 5
|
|
t.applied = expirationTime > 0 and ( expirationTime - duration ) or query_time
|
|
t.caster = "nobody"
|
|
return
|
|
end
|
|
end
|
|
|
|
t.count = 0
|
|
t.expires = 0
|
|
t.applied = 0
|
|
t.caster = "nobody"
|
|
end,
|
|
},
|
|
|
|
all_absorbs = {
|
|
duration = 15,
|
|
max_stack = 1,
|
|
-- TODO: Check if function works.
|
|
generate = function( t, auraType )
|
|
local unit = auraType == "debuff" and "target" or "player"
|
|
local amount = UnitGetTotalAbsorbs( unit )
|
|
|
|
if amount > 0 then
|
|
-- t.name = ABSORB
|
|
t.count = 1
|
|
t.expires = now + 10
|
|
t.applied = now - 5
|
|
t.caster = unit
|
|
return
|
|
end
|
|
|
|
t.count = 0
|
|
t.expires = 0
|
|
t.applied = 0
|
|
t.caster = "nobody"
|
|
end,
|
|
copy = "unravel_absorb"
|
|
},
|
|
|
|
devouring_rift = {
|
|
id = 440313,
|
|
duration = 15,
|
|
shared = "player",
|
|
max_stack = 1
|
|
}
|
|
} )
|
|
|
|
do
|
|
-- Dragonflight Potions
|
|
-- There are multiple items for each potion, and there are also Toxic potions that people may not want to use.
|
|
local exp_potions = {
|
|
{
|
|
name = "tempered_potion",
|
|
items = { 212971, 212970, 212969, 212265, 212264, 212263 }
|
|
},
|
|
{
|
|
name = "potion_of_unwavering_focus",
|
|
items = { 212965, 212964, 212963, 212259, 212258, 212257 }
|
|
},
|
|
{
|
|
name = "frontline_potion",
|
|
items = { 212968, 212967, 212966, 212262, 212261, 212260 }
|
|
},
|
|
{
|
|
name = "elemental_potion_of_ultimate_power",
|
|
items = { 191914, 191913, 191912, 191383, 191382, 191381 }
|
|
},
|
|
{
|
|
name = "elemental_potion_of_power",
|
|
items = { 191907, 191906, 191905, 191389, 191388, 191387 }
|
|
},
|
|
{
|
|
name = "algari_healing_potion",
|
|
items = { 211878, 211879, 211880 }
|
|
},
|
|
{
|
|
name = "cavedwellers_delight",
|
|
items = { 212242, 212243, 212244 }
|
|
}
|
|
}
|
|
|
|
---@diagnostic disable-next-line: need-check-nil
|
|
all:RegisterAura( "fake_potion", {
|
|
duration = 30,
|
|
max_stack = 1,
|
|
} )
|
|
|
|
local first_potion, first_potion_key
|
|
local potion_items = {}
|
|
|
|
all:RegisterHook( "reset_precast", function ()
|
|
wipe( potion_items )
|
|
for _, potion in ipairs( exp_potions ) do
|
|
for _, item in ipairs( potion.items ) do
|
|
if GetItemCount( item, false ) > 0 then
|
|
potion_items[ potion.name ] = item
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end )
|
|
|
|
all:RegisterAura( "potion", {
|
|
alias = { "fake_potion" },
|
|
aliasMode = "first",
|
|
aliasType = "buff",
|
|
duration = 30
|
|
} )
|
|
|
|
local GetItemInfo = C_Item.GetItemInfo
|
|
|
|
for _, potion in ipairs( exp_potions ) do
|
|
local potionItem = Item:CreateFromItemID( potion.items[ #potion.items ] )
|
|
|
|
-- all:RegisterAbility( potion.name, {} ) -- Create stub.
|
|
|
|
potionItem:ContinueOnItemLoad( function()
|
|
all:RegisterAbility( potion.name, {
|
|
name = potionItem:GetItemName(),
|
|
listName = potionItem:GetItemLink(),
|
|
cast = 0,
|
|
cooldown = 300,
|
|
gcd = "off",
|
|
|
|
startsCombat = false,
|
|
toggle = "potions",
|
|
|
|
item = function ()
|
|
return potion_items[ potion.name ] or potion.items[ #potion.items ]
|
|
end,
|
|
items = potion.items,
|
|
bagItem = true,
|
|
texture = potionItem:GetItemIcon(),
|
|
|
|
handler = function ()
|
|
applyBuff( potion.name )
|
|
end,
|
|
} )
|
|
|
|
class.abilities[ potion.name ] = all.abilities[ potion.name ]
|
|
|
|
class.potions[ potion.name ] = {
|
|
name = potionItem:GetItemName(),
|
|
link = potionItem:GetItemLink(),
|
|
item = potion.items[ #potion.items ]
|
|
}
|
|
|
|
class.potionList[ potion.name ] = "|T" .. potionItem:GetItemIcon() .. ":0|t |cff00ccff[" .. potionItem:GetItemName() .. "]|r"
|
|
|
|
for i, item in ipairs( potion.items ) do
|
|
if not first_potion then
|
|
first_potion_key = potion.name
|
|
first_potion = item
|
|
end
|
|
|
|
local each_potion = Item:CreateFromItemID( item )
|
|
|
|
if not each_potion:IsItemEmpty() then
|
|
each_potion:ContinueOnItemLoad( function()
|
|
local _, spell = GetItemSpell( item )
|
|
|
|
if not spell then Hekili:Error( "No spell found for item %d.", item ) return false end
|
|
|
|
if not all.auras[ potion.name ] then
|
|
all:RegisterAura( potion.name, {
|
|
id = spell,
|
|
duration = 30,
|
|
max_stack = 1,
|
|
copy = { spell }
|
|
} )
|
|
|
|
class.auras[ spell ] = all.auras[ potion.name ]
|
|
else
|
|
local existing = all.auras[ potion.name ]
|
|
if not existing.copy then existing.copy = {} end
|
|
insert( existing.copy, spell )
|
|
all.auras[ spell ] = all.auras[ potion.name ]
|
|
class.auras[ spell ] = all.auras[ potion.name ]
|
|
end
|
|
|
|
return true
|
|
end )
|
|
else
|
|
Hekili:Error( "Item %d is empty.", item )
|
|
end
|
|
end
|
|
end )
|
|
end
|
|
|
|
all:RegisterAbility( "potion", {
|
|
name = "Potion",
|
|
listName = '|T136243:0|t |cff00ccff[Potion]|r',
|
|
cast = 0,
|
|
cooldown = 300,
|
|
gcd = "off",
|
|
|
|
startsCombat = false,
|
|
toggle = "potions",
|
|
|
|
consumable = function() return state.args.potion or settings.potion or first_potion_key or "tempered_potion" end,
|
|
item = function()
|
|
if state.args.potion and class.abilities[ state.args.potion ] then return class.abilities[ state.args.potion ].item end
|
|
if spec.potion and class.abilities[ spec.potion ] then return class.abilities[ spec.potion ].item end
|
|
if first_potion and class.abilities[ first_potion ] then return class.abilities[ first_potion ].item end
|
|
return 191387
|
|
end,
|
|
bagItem = true,
|
|
|
|
handler = function ()
|
|
local use = all.abilities.potion
|
|
use = use and use.consumable
|
|
|
|
if use and use ~= "global_cooldown" then
|
|
class.abilities[ use ].handler()
|
|
setCooldown( use, action[ use ].cooldown )
|
|
end
|
|
end,
|
|
|
|
usable = function ()
|
|
return potion_items[ all.abilities.potion.item ], "no valid potions found in inventory"
|
|
end,
|
|
|
|
copy = "potion_default"
|
|
} )
|
|
end
|
|
|
|
|
|
|
|
local gotn_classes = {
|
|
WARRIOR = 28880,
|
|
MONK = 121093,
|
|
DEATHKNIGHT = 59545,
|
|
SHAMAN = 59547,
|
|
HUNTER = 59543,
|
|
PRIEST = 59544,
|
|
MAGE = 59548,
|
|
PALADIN = 59542,
|
|
ROGUE = 370626
|
|
}
|
|
|
|
local baseClass = UnitClassBase( "player" ) or "WARRIOR"
|
|
|
|
all:RegisterAura( "gift_of_the_naaru", {
|
|
id = gotn_classes[ baseClass ],
|
|
duration = 5,
|
|
max_stack = 1,
|
|
copy = { 28800, 121093, 59545, 59547, 59543, 59544, 59548, 59542, 370626 }
|
|
} )
|
|
|
|
all:RegisterAbility( "gift_of_the_naaru", {
|
|
id = gotn_classes[ baseClass ],
|
|
cast = 0,
|
|
cooldown = 180,
|
|
gcd = "off",
|
|
|
|
handler = function ()
|
|
applyBuff( "gift_of_the_naaru" )
|
|
end,
|
|
} )
|
|
|
|
|
|
all:RegisterAbilities( {
|
|
global_cooldown = {
|
|
id = 61304,
|
|
cast = 0,
|
|
cooldown = 0,
|
|
gcd = "spell",
|
|
|
|
unlisted = true,
|
|
known = function () return true end,
|
|
},
|
|
|
|
ancestral_call = {
|
|
id = 274738,
|
|
cast = 0,
|
|
cooldown = 120,
|
|
gcd = "off",
|
|
|
|
toggle = "cooldowns",
|
|
|
|
-- usable = function () return race.maghar_orc end,
|
|
handler = function ()
|
|
applyBuff( "ancestral_call" )
|
|
end,
|
|
},
|
|
|
|
arcane_pulse = {
|
|
id = 260364,
|
|
cast = 0,
|
|
cooldown = 180,
|
|
gcd = "spell",
|
|
|
|
toggle = "cooldowns",
|
|
|
|
-- usable = function () return race.nightborne end,
|
|
handler = function ()
|
|
applyDebuff( "target", "arcane_pulse" )
|
|
end,
|
|
},
|
|
|
|
berserking = {
|
|
id = 26297,
|
|
cast = 0,
|
|
cooldown = 180,
|
|
gcd = "off",
|
|
|
|
toggle = "cooldowns",
|
|
|
|
-- usable = function () return race.troll end,
|
|
handler = function ()
|
|
applyBuff( "berserking" )
|
|
end,
|
|
},
|
|
|
|
hyper_organic_light_originator = {
|
|
id = 312924,
|
|
cast = 0,
|
|
cooldown = 180,
|
|
gcd = "off",
|
|
|
|
toggle = "defensives",
|
|
|
|
handler = function ()
|
|
applyBuff( "hyper_organic_light_originator" )
|
|
end
|
|
},
|
|
|
|
bag_of_tricks = {
|
|
id = 312411,
|
|
cast = 0,
|
|
cooldown = 90,
|
|
gcd = "spell",
|
|
|
|
toggle = "cooldowns",
|
|
},
|
|
|
|
haymaker = {
|
|
id = 287712,
|
|
cast = 1,
|
|
cooldown = 150,
|
|
gcd = "spell",
|
|
|
|
handler = function ()
|
|
if not target.is_boss then applyDebuff( "target", "haymaker" ) end
|
|
end,
|
|
|
|
auras = {
|
|
haymaker = {
|
|
id = 287712,
|
|
duration = 3,
|
|
max_stack = 1,
|
|
},
|
|
}
|
|
}
|
|
} )
|
|
|
|
|
|
-- Blood Fury spell IDs vary by class (whether you need AP/Int/both).
|
|
local bf_classes = {
|
|
DEATHKNIGHT = 20572,
|
|
HUNTER = 20572,
|
|
MAGE = 33702,
|
|
MONK = 33697,
|
|
ROGUE = 20572,
|
|
SHAMAN = 33697,
|
|
WARLOCK = 33702,
|
|
WARRIOR = 20572,
|
|
PRIEST = 33702
|
|
}
|
|
|
|
all:RegisterAbilities( {
|
|
blood_fury = {
|
|
id = function () return bf_classes[ class.file ] or 20572 end,
|
|
cast = 0,
|
|
cooldown = 120,
|
|
gcd = "off",
|
|
|
|
toggle = "cooldowns",
|
|
|
|
-- usable = function () return race.orc end,
|
|
handler = function ()
|
|
applyBuff( "blood_fury", 15 )
|
|
end,
|
|
|
|
copy = { 33702, 20572, 33697 },
|
|
},
|
|
|
|
arcane_torrent = {
|
|
id = function ()
|
|
if class.file == "PALADIN" then return 155145 end
|
|
if class.file == "MONK" then return 129597 end
|
|
if class.file == "DEATHKNIGHT" then return 50613 end
|
|
if class.file == "WARRIOR" then return 69179 end
|
|
if class.file == "ROGUE" then return 25046 end
|
|
if class.file == "HUNTER" then return 80483 end
|
|
if class.file == "DEMONHUNTER" then return 202719 end
|
|
if class.file == "PRIEST" then return 232633 end
|
|
return 28730
|
|
end,
|
|
cast = 0,
|
|
cooldown = 120,
|
|
gcd = "spell",
|
|
|
|
-- It does start combat if there are enemies in range, but we often use it precombat for resources.
|
|
startsCombat = false,
|
|
|
|
-- usable = function () return race.blood_elf end,
|
|
toggle = "cooldowns",
|
|
|
|
handler = function ()
|
|
if class.file == "DEATHKNIGHT" then gain( 20, "runic_power" )
|
|
elseif class.file == "HUNTER" then gain( 15, "focus" )
|
|
elseif class.file == "MONK" then gain( 1, "chi" )
|
|
elseif class.file == "PALADIN" then gain( 1, "holy_power" )
|
|
elseif class.file == "ROGUE" then gain( 15, "energy" )
|
|
elseif class.file == "WARRIOR" then gain( 15, "rage" )
|
|
elseif class.file == "DEMONHUNTER" then gain( 15, "fury" )
|
|
elseif class.file == "PRIEST" and state.spec.shadow then gain( 15, "insanity" ) end
|
|
|
|
removeBuff( "dispellable_magic" )
|
|
end,
|
|
|
|
copy = { 155145, 129597, 50613, 69179, 25046, 80483, 202719, 232633 }
|
|
},
|
|
|
|
will_to_survive = {
|
|
id = 59752,
|
|
cast = 0,
|
|
cooldown = 180,
|
|
gcd = "off",
|
|
|
|
toggle = "defensives",
|
|
},
|
|
|
|
shadowmeld = {
|
|
id = 58984,
|
|
cast = 0,
|
|
cooldown = 120,
|
|
gcd = "off",
|
|
|
|
usable = function ()
|
|
if not boss or solo then return false, "requires boss fight or group (to avoid resetting)" end
|
|
if moving then return false, "can't shadowmeld while moving" end
|
|
return true
|
|
end,
|
|
|
|
handler = function ()
|
|
applyBuff( "shadowmeld" )
|
|
end,
|
|
},
|
|
|
|
|
|
lights_judgment = {
|
|
id = 255647,
|
|
cast = 0,
|
|
cooldown = 150,
|
|
gcd = "spell",
|
|
|
|
-- usable = function () return race.lightforged_draenei end,
|
|
|
|
toggle = "cooldowns",
|
|
},
|
|
|
|
|
|
stoneform = {
|
|
id = 20594,
|
|
cast = 0,
|
|
cooldown = 120,
|
|
gcd = "off",
|
|
|
|
toggle = "defensives",
|
|
|
|
buff = function()
|
|
local aura, remains = "dispellable_poison", buff.dispellable_poison.remains
|
|
|
|
for _, effect in pairs( { "dispellable_disease", "dispellable_curse", "dispellable_magic", "dispellable_bleed" } ) do
|
|
local rem = buff[ effect ].remains
|
|
if rem > remains then
|
|
aura = effect
|
|
remains = rem
|
|
end
|
|
end
|
|
|
|
return aura
|
|
end,
|
|
|
|
handler = function ()
|
|
removeBuff( "dispellable_poison" )
|
|
removeBuff( "dispellable_disease" )
|
|
removeBuff( "dispellable_curse" )
|
|
removeBuff( "dispellable_magic" )
|
|
removeBuff( "dispellable_bleed" )
|
|
|
|
applyBuff( "stoneform" )
|
|
end,
|
|
|
|
auras = {
|
|
stoneform = {
|
|
id = 65116,
|
|
duration = 8,
|
|
max_stack = 1
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
fireblood = {
|
|
id = 265221,
|
|
cast = 0,
|
|
cooldown = 120,
|
|
gcd = "off",
|
|
|
|
toggle = "cooldowns",
|
|
|
|
-- usable = function () return race.dark_iron_dwarf end,
|
|
handler = function () applyBuff( "fireblood" ) end,
|
|
},
|
|
|
|
|
|
-- INTERNAL HANDLERS
|
|
call_action_list = {
|
|
name = "|cff00ccff[Call Action List]|r",
|
|
listName = '|T136243:0|t |cff00ccff[Call Action List]|r',
|
|
cast = 0,
|
|
cooldown = 0,
|
|
gcd = "off",
|
|
essential = true,
|
|
},
|
|
|
|
run_action_list = {
|
|
name = "|cff00ccff[Run Action List]|r",
|
|
listName = '|T136243:0|t |cff00ccff[Run Action List]|r',
|
|
cast = 0,
|
|
cooldown = 0,
|
|
gcd = "off",
|
|
essential = true,
|
|
},
|
|
|
|
wait = {
|
|
name = "|cff00ccff[Wait]|r",
|
|
listName = '|T136243:0|t |cff00ccff[Wait]|r',
|
|
cast = 0,
|
|
cooldown = 0,
|
|
gcd = "off",
|
|
essential = true,
|
|
},
|
|
|
|
pool_resource = {
|
|
name = "|cff00ccff[Pool Resource]|r",
|
|
listName = "|T136243:0|t |cff00ccff[Pool Resource]|r",
|
|
cast = 0,
|
|
cooldown = 0,
|
|
gcd = "off",
|
|
},
|
|
|
|
cancel_action = {
|
|
name = "|cff00ccff[Cancel Action]|r",
|
|
listName = "|T136243:0|t |cff00ccff[Cancel Action]|r",
|
|
cast = 0,
|
|
cooldown = 0,
|
|
gcd = "off",
|
|
|
|
usable = function ()
|
|
local a = args.action_name
|
|
local ability = class.abilities[ a ]
|
|
if not a or not ability then return false, "no action identified" end
|
|
if buff.casting.down or buff.casting.v3 ~= 1 then return false, "not channeling" end
|
|
if buff.casting.v1 ~= ability.id then return false, "not channeling " .. a end
|
|
return true
|
|
end,
|
|
timeToReady = function () return gcd.remains end,
|
|
},
|
|
|
|
variable = {
|
|
name = "|cff00ccff[Variable]|r",
|
|
listName = '|T136243:0|t |cff00ccff[Variable]|r',
|
|
cast = 0,
|
|
cooldown = 0,
|
|
gcd = "off",
|
|
essential = true,
|
|
},
|
|
|
|
healthstone = {
|
|
name = "Healthstone",
|
|
listName = "|T538745:0|t |cff00ccff[Healthstone]|r",
|
|
cast = 0,
|
|
cooldown = function () return time > 0 and 3600 or 60 end,
|
|
gcd = "off",
|
|
|
|
item = function() return talent.pact_of_gluttony.enabled and 224464 or 5512 end,
|
|
items = { 224464, 5512 },
|
|
bagItem = true,
|
|
|
|
startsCombat = false,
|
|
texture = function() return talent.pact_of_gluttony.enabled and 538744 or 538745 end,
|
|
|
|
usable = function ()
|
|
local item = talent.pact_of_gluttony.enabled and 224464 or 5512
|
|
if GetItemCount( item ) == 0 then return false, "requires healthstone in bags"
|
|
elseif not IsUsableItem( item ) then return false, "healthstone on CD"
|
|
elseif health.current >= health.max then return false, "must be damaged" end
|
|
return true
|
|
end,
|
|
|
|
readyTime = function ()
|
|
local start, duration = GetItemCooldown( talent.pact_of_gluttony.enabled and 224464 or 5512 )
|
|
return max( 0, start + duration - query_time )
|
|
end,
|
|
|
|
handler = function ()
|
|
gain( 0.25 * health.max, "health" )
|
|
end,
|
|
},
|
|
|
|
weyrnstone = {
|
|
name = function () return ( GetItemInfo( 205146 ) ) or "Weyrnstone" end,
|
|
listName = function ()
|
|
local _, link, _, _, _, _, _, _, _, tex = GetItemInfo( 205146 )
|
|
if link and tex then return "|T" .. tex .. ":0|t " .. link end
|
|
return "|cff00ccff[Weyrnstone]|r"
|
|
end,
|
|
cast = 1.5,
|
|
cooldown = 120,
|
|
gcd = "spell",
|
|
|
|
item = 205146,
|
|
bagItem = true,
|
|
|
|
startsCombat = false,
|
|
texture = 5199618,
|
|
|
|
usable = function ()
|
|
if GetItemCount( 205146 ) == 0 then return false, "requires weyrnstone in bags" end
|
|
if solo then return false, "must have an ally to teleport" end
|
|
return true
|
|
end,
|
|
|
|
readyTime = function ()
|
|
local start, duration = GetItemCooldown( 205146 )
|
|
return max( 0, start + duration - query_time )
|
|
end,
|
|
|
|
handler = function ()
|
|
end,
|
|
|
|
copy = { "use_weyrnstone", "active_weyrnstone" }
|
|
},
|
|
|
|
cancel_buff = {
|
|
name = "|cff00ccff[Cancel Buff]|r",
|
|
listName = '|T136243:0|t |cff00ccff[Cancel Buff]|r',
|
|
cast = 0,
|
|
gcd = "off",
|
|
|
|
startsCombat = false,
|
|
|
|
buff = function () return args.buff_name or nil end,
|
|
|
|
indicator = "cancel",
|
|
texture = function ()
|
|
if not args.buff_name then return 134400 end
|
|
|
|
local a = class.auras[ args.buff_name ]
|
|
-- if not a then return 134400 end
|
|
if a.texture then return a.texture end
|
|
|
|
a = a and a.id
|
|
a = a and GetSpellTexture( a )
|
|
|
|
return a or 134400
|
|
end,
|
|
|
|
usable = function () return args.buff_name ~= nil, "no buff name detected" end,
|
|
timeToReady = function () return gcd.remains end,
|
|
handler = function ()
|
|
if not args.buff_name then return end
|
|
|
|
local cancel = args.buff_name and buff[ args.buff_name ]
|
|
cancel = cancel and rawget( cancel, "onCancel" )
|
|
|
|
if cancel then
|
|
cancel()
|
|
return
|
|
end
|
|
|
|
removeBuff( args.buff_name )
|
|
end,
|
|
},
|
|
|
|
null_cooldown = {
|
|
name = "|cff00ccff[Null Cooldown]|r",
|
|
listName = "|T136243:0|t |cff00ccff[Null Cooldown]|r",
|
|
cast = 0,
|
|
cooldown = 0.001,
|
|
gcd = "off",
|
|
|
|
startsCombat = false,
|
|
|
|
unlisted = true
|
|
},
|
|
|
|
trinket1 = {
|
|
name = "|cff00ccff[Trinket #1]|r",
|
|
listName = "|T136243:0|t |cff00ccff[Trinket #1]|r",
|
|
cast = 0,
|
|
cooldown = 600,
|
|
gcd = "off",
|
|
|
|
usable = false,
|
|
|
|
copy = "actual_trinket1",
|
|
},
|
|
|
|
trinket2 = {
|
|
name = "|cff00ccff[Trinket #2]|r",
|
|
listName = "|T136243:0|t |cff00ccff[Trinket #2]|r",
|
|
cast = 0,
|
|
cooldown = 600,
|
|
gcd = "off",
|
|
|
|
usable = false,
|
|
|
|
copy = "actual_trinket2",
|
|
},
|
|
|
|
main_hand = {
|
|
name = "|cff00ccff[" .. INVTYPE_WEAPONMAINHAND .. "]|r",
|
|
listName = "|T136243:0|t |cff00ccff[" .. INVTYPE_WEAPONMAINHAND .. "]|r",
|
|
cast = 0,
|
|
cooldown = 600,
|
|
gcd = "off",
|
|
|
|
usable = false,
|
|
|
|
copy = "actual_main_hand",
|
|
}
|
|
} )
|
|
|
|
|
|
-- Use Items
|
|
do
|
|
-- Should handle trinkets/items internally.
|
|
-- 1. Check APLs and don't try to recommend items that have their own APL entries.
|
|
-- 2. Respect item preferences registered in spec options.
|
|
|
|
all:RegisterAbility( "use_items", {
|
|
name = "Use Items",
|
|
listName = "|T136243:0|t |cff00ccff[Use Items]|r",
|
|
cast = 0,
|
|
cooldown = 120,
|
|
gcd = "off",
|
|
} )
|
|
|
|
all:RegisterAbility( "unusable_trinket", {
|
|
name = "Unusable Trinket",
|
|
listName = "|T136240:0|t |cff00ccff[Unusable Trinket]|r",
|
|
cast = 0,
|
|
cooldown = 180,
|
|
gcd = "off",
|
|
|
|
usable = false,
|
|
unlisted = true
|
|
} )
|
|
|
|
all:RegisterAbility( "heart_essence", {
|
|
name = function () return ( GetItemInfo( 158075 ) ) or "Heart Essence" end,
|
|
listName = function ()
|
|
local _, link, _, _, _, _, _, _, _, tex = GetItemInfo( 158075 )
|
|
if link and tex then return "|T" .. tex .. ":0|t " .. link end
|
|
return "|cff00ccff[Heart Essence]|r"
|
|
end,
|
|
cast = 0,
|
|
cooldown = 0,
|
|
gcd = "off",
|
|
|
|
item = 158075,
|
|
essence = true,
|
|
|
|
toggle = "essences",
|
|
|
|
usable = function () return false, "your equipped major essence is supported elsewhere in the priority or is not an active ability" end
|
|
} )
|
|
end
|
|
|
|
|
|
-- x.x - Heirloom Trinket(s)
|
|
all:RegisterAbility( "touch_of_the_void", {
|
|
cast = 0,
|
|
cooldown = 120,
|
|
gcd = "off",
|
|
|
|
item = 128318,
|
|
toggle = "cooldowns",
|
|
} )
|
|
|
|
|
|
-- PvP Trinkets
|
|
-- Medallions
|
|
do
|
|
local pvp_medallions = {
|
|
{ "dread_aspirants_medallion", 162897 },
|
|
{ "dread_gladiators_medallion", 161674 },
|
|
{ "sinister_aspirants_medallion", 165220 },
|
|
{ "sinister_gladiators_medallion", 165055 },
|
|
{ "notorious_aspirants_medallion", 167525 },
|
|
{ "notorious_gladiators_medallion", 167377 },
|
|
{ "old_corrupted_gladiators_medallion", 172666 },
|
|
{ "corrupted_aspirants_medallion", 184058 },
|
|
{ "corrupted_gladiators_medallion", 184055 },
|
|
{ "sinful_aspirants_medallion", 184052 },
|
|
{ "sinful_gladiators_medallion", 181333 },
|
|
{ "unchained_aspirants_medallion", 185309 },
|
|
{ "unchained_gladiators_medallion", 185304 },
|
|
{ "cosmic_aspirants_medallion", 186966 },
|
|
{ "cosmic_gladiators_medallion", 186869 },
|
|
{ "eternal_aspirants_medallion", 192412 },
|
|
{ "eternal_gladiators_medallion", 192298 },
|
|
{ "obsidian_combatants_medallion", 204164 },
|
|
{ "obsidian_aspirants_medallion", 205779 },
|
|
{ "obsidian_gladiators_medallion", 205711 },
|
|
{ "forged_aspirants_medallion", 218422 },
|
|
{ "forged_gladiators_medallion", 218716 }
|
|
}
|
|
|
|
local pvp_medallions_copy = {}
|
|
|
|
for _, v in ipairs( pvp_medallions ) do
|
|
insert( pvp_medallions_copy, v[1] )
|
|
all:RegisterGear( v[1], v[2] )
|
|
all:RegisterGear( "gladiators_medallion", v[2] )
|
|
end
|
|
|
|
all:RegisterAbility( "gladiators_medallion", {
|
|
name = function ()
|
|
local data = GetSpellInfo( 277179 )
|
|
return data and data.name or "Gladiator's Medallion"
|
|
end,
|
|
listName = function ()
|
|
local data = GetSpellInfo( 277179 )
|
|
if data and data.iconID then return "|T" .. data.iconID .. ":0|t " .. ( GetSpellLink( 277179 ) ) end
|
|
end,
|
|
link = function () return ( GetSpellLink( 277179 ) ) end,
|
|
cast = 0,
|
|
cooldown = 120,
|
|
gcd = "off",
|
|
|
|
item = function ()
|
|
local m
|
|
for _, medallion in ipairs( pvp_medallions ) do
|
|
m = medallion[ 2 ]
|
|
if equipped[ m ] then return m end
|
|
end
|
|
return m
|
|
end,
|
|
items = { 161674, 162897, 165055, 165220, 167377, 167525, 181333, 184052, 184055, 172666, 184058, 185309, 185304, 186966, 186869, 192412, 192298, 204164, 205779, 205711, 205779, 205711, 218422, 218716 },
|
|
toggle = "defensives",
|
|
|
|
usable = function () return debuff.loss_of_control.up, "requires loss of control effect" end,
|
|
|
|
handler = function ()
|
|
applyBuff( "gladiators_medallion" )
|
|
end,
|
|
|
|
copy = pvp_medallions_copy
|
|
} )
|
|
|
|
all:RegisterAura( "gladiators_medallion", {
|
|
id = 277179,
|
|
duration = 20,
|
|
max_stack = 1
|
|
} )
|
|
end
|
|
|
|
-- Badges
|
|
do
|
|
local pvp_badges = {
|
|
{ "dread_aspirants_badge", 162966 },
|
|
{ "dread_gladiators_badge", 161902 },
|
|
{ "sinister_aspirants_badge", 165223 },
|
|
{ "sinister_gladiators_badge", 165058 },
|
|
{ "notorious_aspirants_badge", 167528 },
|
|
{ "notorious_gladiators_badge", 167380 },
|
|
{ "corrupted_aspirants_badge", 172849 },
|
|
{ "corrupted_gladiators_badge", 172669 },
|
|
{ "sinful_aspirants_badge_of_ferocity", 175884 },
|
|
{ "sinful_gladiators_badge_of_ferocity", 175921 },
|
|
{ "unchained_aspirants_badge_of_ferocity", 185161 },
|
|
{ "unchained_gladiators_badge_of_ferocity", 185197 },
|
|
{ "cosmic_aspirants_badge_of_ferocity", 186906 },
|
|
{ "cosmic_gladiators_badge_of_ferocity", 186866 },
|
|
{ "eternal_aspirants_badge_of_ferocity", 192352 },
|
|
{ "eternal_gladiators_badge_of_ferocity", 192295 },
|
|
{ "crimson_aspirants_badge_of_ferocity", 201449 },
|
|
{ "crimson_gladiators_badge_of_ferocity", 201807 },
|
|
{ "obsidian_aspirants_badge_of_ferocity", 205778 },
|
|
{ "obsidian_gladiator_badge_of_ferocity", 205708 },
|
|
{ "verdant_aspirants_badge_of_ferocity", 209763 },
|
|
{ "verdant_gladiators_badge_of_ferocity", 209343 },
|
|
{ "forged_aspirants_badge_of_ferocity", 218421 },
|
|
{ "forged_gladiators_badge_of_ferocity", 218713 },
|
|
{ "prized_aspirants_badge_of_ferocity", 229491 },
|
|
{ "prized_gladiators_badge_of_ferocity", 229780 }
|
|
}
|
|
|
|
local pvp_badges_copy = {}
|
|
|
|
for _, v in ipairs( pvp_badges ) do
|
|
insert( pvp_badges_copy, v[1] )
|
|
all:RegisterGear( v[1], v[2] )
|
|
all:RegisterGear( "gladiators_badge", v[2] )
|
|
end
|
|
|
|
all:RegisterAbility( "gladiators_badge", {
|
|
name = function ()
|
|
local data = GetSpellInfo( 277185 )
|
|
return data and data.name or "Gladiator's Badge"
|
|
end,
|
|
listName = function ()
|
|
local data = GetSpellInfo( 277185 )
|
|
if data and data.iconID then return "|T" .. data.iconID .. ":0|t " .. ( GetSpellLink( 277185 ) ) end
|
|
end,
|
|
link = function () return ( GetSpellLink( 277185 ) ) end,
|
|
cast = 0,
|
|
cooldown = 120,
|
|
gcd = "off",
|
|
|
|
items = { 162966, 161902, 165223, 165058, 167528, 167380, 172849, 172669, 175884, 175921, 185161, 185197, 186906, 186866, 192352, 192295, 201449, 201807, 205778, 205708, 209763, 209343, 218421, 218713, 229491, 229780 },
|
|
texture = 135884,
|
|
|
|
toggle = "cooldowns",
|
|
item = function ()
|
|
local b
|
|
|
|
for i = #pvp_badges, 1, -1 do
|
|
b = pvp_badges[ i ][ 2 ]
|
|
if equipped[ b ] then
|
|
break
|
|
end
|
|
end
|
|
return b
|
|
end,
|
|
|
|
usable = function () return set_bonus.gladiators_badge > 0, "requires Gladiator's Badge" end,
|
|
handler = function ()
|
|
applyBuff( "gladiators_badge" )
|
|
end,
|
|
|
|
copy = pvp_badges_copy
|
|
} )
|
|
|
|
all:RegisterAura( "gladiators_badge", {
|
|
id = 277185,
|
|
duration = 15,
|
|
max_stack = 1
|
|
} )
|
|
end
|
|
|
|
|
|
-- Insignias -- N/A, not on-use.
|
|
all:RegisterAura( "gladiators_insignia", {
|
|
id = 277181,
|
|
duration = 20,
|
|
max_stack = 1,
|
|
copy = 345230
|
|
} )
|
|
|
|
|
|
-- Safeguard (equipped, not on-use)
|
|
all:RegisterAura( "gladiators_safeguard", {
|
|
id = 286342,
|
|
duration = 10,
|
|
max_stack = 1
|
|
} )
|
|
|
|
|
|
-- Emblems
|
|
do
|
|
local pvp_emblems = {
|
|
-- dread_combatants_emblem = 161812,
|
|
dread_aspirants_emblem = 162898,
|
|
dread_gladiators_emblem = 161675,
|
|
sinister_aspirants_emblem = 165221,
|
|
sinister_gladiators_emblem = 165056,
|
|
notorious_gladiators_emblem = 167378,
|
|
notorious_aspirants_emblem = 167526,
|
|
corrupted_gladiators_emblem = 172667,
|
|
corrupted_aspirants_emblem = 172847,
|
|
sinful_aspirants_emblem = 178334,
|
|
sinful_gladiators_emblem = 178447,
|
|
unchained_aspirants_emblem = 185242,
|
|
unchained_gladiators_emblem = 185282,
|
|
cosmic_aspirants_emblem = 186946,
|
|
cosmic_gladiators_emblem = 186868,
|
|
eternal_aspirants_emblem = 192392,
|
|
eternal_gladiators_emblem = 192297,
|
|
crimson_aspirants_emblem = 201452,
|
|
crimson_gladiators_emblem = 201809,
|
|
obsidian_combatants_emblem = 204166,
|
|
obsidian_aspirants_emblem = 205781,
|
|
obsidian_gladiators_emblem = 205710,
|
|
verdant_aspirants_emblem = 209766,
|
|
verdant_combatants_emblem = 208309,
|
|
verdant_gladiators_emblem = 209345,
|
|
algari_competitors_emblem = 219933,
|
|
forged_gladiators_emblem = 218715,
|
|
prized_aspirants_emblem = 229494,
|
|
prized_gladiators_emblem = 229782
|
|
}
|
|
|
|
local pvp_emblems_copy = {}
|
|
|
|
for k, v in pairs( pvp_emblems ) do
|
|
insert( pvp_emblems_copy, k )
|
|
all:RegisterGear( k, v )
|
|
all:RegisterGear( "gladiators_emblem", v )
|
|
end
|
|
|
|
|
|
all:RegisterAbility( "gladiators_emblem", {
|
|
name = function ()
|
|
local data = GetSpellInfo( 277187 )
|
|
return data and data.name or "Gladiator's Emblem"
|
|
end,
|
|
listName = function ()
|
|
local data = GetSpellInfo( 277187 )
|
|
if data and data.iconID then return "|T" .. data.iconID .. ":0|t " .. ( GetSpellLink( 277187 ) ) end
|
|
end,
|
|
link = function () return ( GetSpellLink( 277187 ) ) end,
|
|
cast = 0,
|
|
cooldown = 90,
|
|
gcd = "off",
|
|
|
|
item = function ()
|
|
local e
|
|
for _, emblem in pairs( pvp_emblems ) do
|
|
e = emblem
|
|
if equipped[ e ] then return e end
|
|
end
|
|
return e
|
|
end,
|
|
items = { 162898, 161675, 165221, 165056, 167378, 167526, 172667, 172847, 178334, 178447, 185242, 185282, 186946, 186868, 192392, 192297, 201452, 201809, 204166, 205781, 205710, 209766, 208309, 209345, 219933, 218715, 229494, 229782 },
|
|
toggle = "cooldowns",
|
|
|
|
handler = function ()
|
|
applyBuff( "gladiators_emblem" )
|
|
end,
|
|
|
|
copy = pvp_emblems_copy
|
|
} )
|
|
|
|
all:RegisterAura( "gladiators_emblem", {
|
|
id = 277187,
|
|
duration = 15,
|
|
max_stack = 1,
|
|
} )
|
|
end
|
|
|
|
|
|
-- 8.3 Corrupted On-Use
|
|
|
|
-- DNI, because potentially you have no enemies w/ Corruption w/in range.
|
|
--[[
|
|
all:RegisterAbility( "corrupted_gladiators_breach", {
|
|
cast = 0,
|
|
cooldown = 120,
|
|
gcd = "off",
|
|
|
|
item = 174276,
|
|
toggle = "defensives",
|
|
|
|
handler = function ()
|
|
applyBuff( "void_jaunt" )
|
|
-- +Debuff?
|
|
end,
|
|
|
|
auras = {
|
|
void_jaunt = {
|
|
id = 314517,
|
|
duration = 6,
|
|
max_stack = 1,
|
|
}
|
|
}
|
|
} )
|
|
]]
|
|
|
|
|
|
all:RegisterAbility( "corrupted_gladiators_spite", {
|
|
cast = 0,
|
|
cooldown = 60,
|
|
gcd = "off",
|
|
|
|
item = 174472,
|
|
toggle = "cooldowns",
|
|
|
|
handler = function ()
|
|
applyDebuff( "target", "gladiators_spite" )
|
|
applyDebuff( "target", "lingering_spite" )
|
|
end,
|
|
|
|
auras = {
|
|
gladiators_spite = {
|
|
id = 315391,
|
|
duration = 15,
|
|
max_stack = 1,
|
|
},
|
|
|
|
lingering_spite = {
|
|
id = 320297,
|
|
duration = 3600,
|
|
max_stack = 1,
|
|
}
|
|
}
|
|
} )
|
|
|
|
|
|
all:RegisterAbility( "corrupted_gladiators_maledict", {
|
|
cast = 0,
|
|
cooldown = 120,
|
|
gcd = "off", -- ???
|
|
|
|
item = 172672,
|
|
toggle = "cooldowns",
|
|
|
|
handler = function ()
|
|
applyDebuff( "target", "gladiators_maledict" )
|
|
end,
|
|
|
|
auras = {
|
|
gladiators_maledict = {
|
|
id = 305252,
|
|
duration = 6,
|
|
max_stack = 1
|
|
}
|
|
}
|
|
} )
|
|
|
|
|
|
-- BREWFEST
|
|
all:RegisterAbility( "brawlers_statue", {
|
|
cast = 0,
|
|
cooldown = 120,
|
|
gcd = "off",
|
|
|
|
item = 117357,
|
|
toggle = "defensives",
|
|
|
|
handler = function ()
|
|
applyBuff( "drunken_evasiveness" )
|
|
end
|
|
} )
|
|
|
|
all:RegisterAura( "drunken_evasiveness", {
|
|
id = 127967,
|
|
duration = 20,
|
|
max_stack = 1
|
|
} )
|
|
|
|
|
|
-- HALLOW'S END
|
|
all:RegisterAbility( "the_horsemans_sinister_slicer", {
|
|
cast = 0,
|
|
cooldown = 600,
|
|
gcd = "off",
|
|
|
|
item = 117356,
|
|
toggle = "cooldowns",
|
|
} )
|
|
|
|
|
|
ns.addToggle = function( name, default, optionName, optionDesc )
|
|
|
|
table.insert( class.toggles, {
|
|
name = name,
|
|
state = default,
|
|
option = optionName,
|
|
oDesc = optionDesc
|
|
} )
|
|
|
|
if Hekili.DB.profile[ 'Toggle State: ' .. name ] == nil then
|
|
Hekili.DB.profile[ 'Toggle State: ' .. name ] = default
|
|
end
|
|
|
|
end
|
|
|
|
|
|
ns.addSetting = function( name, default, options )
|
|
|
|
table.insert( class.settings, {
|
|
name = name,
|
|
state = default,
|
|
option = options
|
|
} )
|
|
|
|
if Hekili.DB.profile[ 'Class Option: ' .. name ] == nil then
|
|
Hekili.DB.profile[ 'Class Option: ' ..name ] = default
|
|
end
|
|
|
|
end
|
|
|
|
|
|
ns.addWhitespace = function( name, size )
|
|
|
|
table.insert( class.settings, {
|
|
name = name,
|
|
option = {
|
|
name = " ",
|
|
type = "description",
|
|
desc = " ",
|
|
width = size
|
|
}
|
|
} )
|
|
|
|
end
|
|
|
|
|
|
ns.addHook = function( hook, func )
|
|
insert( class.hooks[ hook ], func )
|
|
end
|
|
|
|
|
|
do
|
|
local inProgress = {}
|
|
local vars = {}
|
|
|
|
local function load_args( ... )
|
|
local count = select( "#", ... )
|
|
if count == 0 then return end
|
|
|
|
for i = 1, count do
|
|
vars[ i ] = select( i, ... )
|
|
end
|
|
end
|
|
|
|
ns.callHook = function( event, ... )
|
|
if not class.hooks[ event ] or inProgress[ event ] then return ... end
|
|
wipe( vars )
|
|
load_args( ... )
|
|
|
|
inProgress[ event ] = true
|
|
for i, hook in ipairs( class.hooks[ event ] ) do
|
|
load_args( hook( unpack( vars ) ) )
|
|
end
|
|
inProgress[ event ] = nil
|
|
|
|
return unpack( vars )
|
|
end
|
|
end
|
|
|
|
|
|
ns.registerCustomVariable = function( var, default )
|
|
state[ var ] = default
|
|
end
|
|
|
|
|
|
|
|
|
|
ns.setClass = function( name )
|
|
-- deprecated.
|
|
--class.file = name
|
|
end
|
|
|
|
|
|
function ns.setRange( value )
|
|
class.range = value
|
|
end
|
|
|
|
|
|
local function storeAbilityElements( key, values )
|
|
|
|
local ability = class.abilities[ key ]
|
|
|
|
if not ability then
|
|
ns.Error( "storeAbilityElements( " .. key .. " ) - no such ability in abilities table." )
|
|
return
|
|
end
|
|
|
|
for k, v in pairs( values ) do
|
|
ability.elem[ k ] = type( v ) == "function" and setfenv( v, state ) or v
|
|
end
|
|
|
|
end
|
|
ns.storeAbilityElements = storeAbilityElements
|
|
|
|
|
|
local function modifyElement( t, k, elem, value )
|
|
|
|
local entry = class[ t ][ k ]
|
|
|
|
if not entry then
|
|
ns.Error( "modifyElement() - no such key '" .. k .. "' in '" .. t .. "' table." )
|
|
return
|
|
end
|
|
|
|
if type( value ) == "function" then
|
|
entry.mods[ elem ] = setfenv( value, Hekili.State )
|
|
else
|
|
entry.elem[ elem ] = value
|
|
end
|
|
|
|
end
|
|
ns.modifyElement = modifyElement
|
|
|
|
|
|
|
|
local function setUsableItemCooldown( cd )
|
|
state.setCooldown( "usable_items", cd or 10 )
|
|
end
|
|
|
|
|
|
-- For Trinket Settings.
|
|
class.itemSettings = {}
|
|
|
|
local function addItemSettings( key, itemID, options )
|
|
|
|
options = options or {}
|
|
|
|
--[[ options.icon = {
|
|
type = "description",
|
|
name = function () return select( 2, GetItemInfo( itemID ) ) or format( "[%d]", itemID ) end,
|
|
order = 1,
|
|
image = function ()
|
|
local tex = select( 10, GetItemInfo( itemID ) )
|
|
if tex then
|
|
return tex, 50, 50
|
|
end
|
|
return nil
|
|
end,
|
|
imageCoords = { 0.1, 0.9, 0.1, 0.9 },
|
|
width = "full",
|
|
fontSize = "large"
|
|
} ]]
|
|
|
|
options.disabled = {
|
|
type = "toggle",
|
|
name = function () return format( "Disable %s via |cff00ccff[Use Items]|r", select( 2, GetItemInfo( itemID ) ) or ( "[" .. itemID .. "]" ) ) end,
|
|
desc = function( info )
|
|
local output = "If disabled, the addon will not recommend this item via the |cff00ccff[Use Items]|r action. " ..
|
|
"You can still manually include the item in your action lists with your own tailored criteria."
|
|
return output
|
|
end,
|
|
order = 25,
|
|
width = "full"
|
|
}
|
|
|
|
options.minimum = {
|
|
type = "range",
|
|
name = "Minimum Targets",
|
|
desc = "The addon will only recommend this trinket (via |cff00ccff[Use Items]|r) when there are at least this many targets available to hit.",
|
|
order = 26,
|
|
width = "full",
|
|
min = 1,
|
|
max = 10,
|
|
step = 1
|
|
}
|
|
|
|
options.maximum = {
|
|
type = "range",
|
|
name = "Maximum Targets",
|
|
desc = "The addon will only recommend this trinket (via |cff00ccff[Use Items]|r) when there are no more than this many targets detected.\n\n" ..
|
|
"This setting is ignored if set to 0.",
|
|
order = 27,
|
|
width = "full",
|
|
min = 0,
|
|
max = 10,
|
|
step = 1
|
|
}
|
|
|
|
class.itemSettings[ itemID ] = {
|
|
key = key,
|
|
name = function () return select( 2, GetItemInfo( itemID ) ) or ( "[" .. itemID .. "]" ) end,
|
|
item = itemID,
|
|
options = options,
|
|
}
|
|
|
|
end
|
|
|
|
|
|
--[[ local function addUsableItem( key, id )
|
|
class.items = class.items or {}
|
|
class.items[ key ] = id
|
|
|
|
addGearSet( key, id )
|
|
addItemSettings( key, id )
|
|
end
|
|
ns.addUsableItem = addUsableItem ]]
|
|
|
|
|
|
function Hekili:GetAbilityInfo( index )
|
|
|
|
local ability = class.abilities[ index ]
|
|
|
|
if not ability then return end
|
|
|
|
-- Decide if more details are needed later.
|
|
return ability.id, ability.name, ability.key, ability.item
|
|
end
|
|
|
|
class.interrupts = {}
|
|
|
|
|
|
local function addPet( key, permanent )
|
|
state.pet[ key ] = rawget( state.pet, key ) or {}
|
|
state.pet[ key ].name = key
|
|
state.pet[ key ].expires = 0
|
|
|
|
ns.commitKey( key )
|
|
end
|
|
ns.addPet = addPet
|
|
|
|
|
|
local function addStance( key, spellID )
|
|
class.stances[ key ] = spellID
|
|
ns.commitKey( key )
|
|
end
|
|
ns.addStance = addStance
|
|
|
|
|
|
local function setRole( key )
|
|
|
|
for k,v in pairs( state.role ) do
|
|
state.role[ k ] = nil
|
|
end
|
|
|
|
state.role[ key ] = true
|
|
|
|
end
|
|
ns.setRole = setRole
|
|
|
|
|
|
function Hekili:GetActiveSpecOption( opt )
|
|
if not self.currentSpecOpts then return end
|
|
return self.currentSpecOpts[ opt ]
|
|
end
|
|
|
|
|
|
function Hekili:GetActivePack()
|
|
return self:GetActiveSpecOption( "package" )
|
|
end
|
|
|
|
|
|
Hekili.SpecChangeHistory = {}
|
|
|
|
function Hekili:SpecializationChanged()
|
|
local currentSpec = GetSpecialization()
|
|
local currentID = GetSpecializationInfo( currentSpec )
|
|
|
|
if currentID == nil then
|
|
self.PendingSpecializationChange = true
|
|
return
|
|
end
|
|
|
|
self.PendingSpecializationChange = false
|
|
self:ForceUpdate( "ACTIVE_PLAYER_SPECIALIZATION_CHANGED" )
|
|
|
|
insert( self.SpecChangeHistory, {
|
|
spec = currentID,
|
|
time = GetTime(),
|
|
bt = debugstack()
|
|
} )
|
|
|
|
for k, _ in pairs( state.spec ) do
|
|
state.spec[ k ] = nil
|
|
end
|
|
|
|
for key in pairs( GetResourceInfo() ) do
|
|
state[ key ] = nil
|
|
class[ key ] = nil
|
|
end
|
|
|
|
class.primaryResource = nil
|
|
|
|
wipe( state.buff )
|
|
wipe( state.debuff )
|
|
|
|
wipe( class.auras )
|
|
wipe( class.abilities )
|
|
wipe( class.hooks )
|
|
wipe( class.talents )
|
|
wipe( class.pvptalents )
|
|
wipe( class.powers )
|
|
wipe( class.gear )
|
|
wipe( class.setBonuses )
|
|
wipe( class.packs )
|
|
wipe( class.resources )
|
|
wipe( class.resourceAuras )
|
|
|
|
wipe( class.pets )
|
|
|
|
local specs = {}
|
|
|
|
-- If the player does not have a specialization, use their first spec instead.
|
|
if currentSpec == 5 then
|
|
currentSpec = 1
|
|
currentID = GetSpecializationInfo( 1 )
|
|
end
|
|
|
|
for i = 1, 4 do
|
|
local id, name, _, _, role, primaryStat = GetSpecializationInfo( i )
|
|
|
|
if not id then break end
|
|
|
|
if i == currentSpec then
|
|
insert( specs, 1, id )
|
|
|
|
state.spec.id = id
|
|
state.spec.name = name
|
|
state.spec.key = getSpecializationKey( id )
|
|
|
|
for k in pairs( state.role ) do
|
|
state.role[ k ] = false
|
|
end
|
|
|
|
if role == "DAMAGER" then
|
|
state.role.attack = true
|
|
elseif role == "TANK" then
|
|
state.role.tank = true
|
|
else
|
|
state.role.healer = true
|
|
end
|
|
|
|
if primaryStat == 1 then
|
|
state.spec.primaryStat = "strength"
|
|
elseif primaryStat == 2 then
|
|
state.spec.primaryStat = "agility"
|
|
else
|
|
state.spec.primaryStat = "intellect"
|
|
end
|
|
|
|
state.spec[ state.spec.key ] = true
|
|
else
|
|
insert( specs, id )
|
|
end
|
|
end
|
|
|
|
insert( specs, 0 )
|
|
|
|
|
|
for key in pairs( GetResourceInfo() ) do
|
|
state[ key ] = nil
|
|
class[ key ] = nil
|
|
end
|
|
if rawget( state, "rune" ) then state.rune = nil; class.rune = nil; end
|
|
|
|
for k in pairs( class.resourceAuras ) do
|
|
class.resourceAuras[ k ] = nil
|
|
end
|
|
|
|
class.primaryResource = nil
|
|
|
|
for k in pairs( class.stateTables ) do
|
|
rawset( state, k, nil )
|
|
class.stateTables[ k ] = nil
|
|
end
|
|
|
|
for k in pairs( class.stateFuncs ) do
|
|
rawset( state, k, nil )
|
|
class.stateFuncs[ k ] = nil
|
|
end
|
|
|
|
for k in pairs( class.stateExprs ) do
|
|
class.stateExprs[ k ] = nil
|
|
end
|
|
|
|
self.currentSpec = nil
|
|
self.currentSpecOpts = nil
|
|
|
|
for i, specID in ipairs( specs ) do
|
|
local spec = class.specs[ specID ]
|
|
|
|
if spec then
|
|
if specID == currentID then
|
|
self.currentSpec = spec
|
|
self.currentSpecOpts = rawget( self.DB.profile.specs, specID )
|
|
state.settings.spec = self.currentSpecOpts
|
|
|
|
state.spec.can_dual_cast = spec.can_dual_cast
|
|
state.spec.dual_cast = spec.dual_cast
|
|
|
|
for res, model in pairs( spec.resources ) do
|
|
class.resources[ res ] = model
|
|
state[ res ] = model.state
|
|
end
|
|
if rawget( state, "runes" ) then state.rune = state.runes end
|
|
|
|
for k,v in pairs( spec.resourceAuras ) do
|
|
class.resourceAuras[ k ] = v
|
|
end
|
|
|
|
class.primaryResource = spec.primaryResource
|
|
|
|
for talent, id in pairs( spec.talents ) do
|
|
class.talents[ talent ] = id
|
|
end
|
|
|
|
for talent, id in pairs( spec.pvptalents ) do
|
|
class.pvptalents[ talent ] = id
|
|
end
|
|
|
|
class.variables = spec.variables
|
|
|
|
class.potionList.default = "|T967533:0|t |cFFFFD100Default|r"
|
|
end
|
|
|
|
if specID == currentID or specID == 0 then
|
|
for event, hooks in pairs( spec.hooks ) do
|
|
for _, hook in ipairs( hooks ) do
|
|
class.hooks[ event ] = class.hooks[ event ] or {}
|
|
insert( class.hooks[ event ], hook )
|
|
end
|
|
end
|
|
end
|
|
|
|
for res, model in pairs( spec.resources ) do
|
|
if not class.resources[ res ] then
|
|
class.resources[ res ] = model
|
|
state[ res ] = model.state
|
|
end
|
|
end
|
|
|
|
if rawget( state, "runes" ) then state.rune = state.runes end
|
|
|
|
for k, v in pairs( spec.auras ) do
|
|
if not class.auras[ k ] then class.auras[ k ] = v end
|
|
end
|
|
|
|
for k, v in pairs( spec.powers ) do
|
|
if not class.powers[ k ] then class.powers[ k ] = v end
|
|
end
|
|
|
|
for k, v in pairs( spec.abilities ) do
|
|
if not class.abilities[ k ] then class.abilities[ k ] = v end
|
|
end
|
|
|
|
for k, v in pairs( spec.gear ) do
|
|
if not class.gear[ k ] then class.gear[ k ] = v end
|
|
end
|
|
|
|
for k, v in pairs( spec.setBonuses ) do
|
|
if not class.setBonuses[ k ] then class.setBonuses[ k ] = v end
|
|
end
|
|
|
|
for k, v in pairs( spec.pets ) do
|
|
if not class.pets[ k ] then class.pets[ k ] = v end
|
|
end
|
|
|
|
for k, v in pairs( spec.totems ) do
|
|
if not class.totems[ k ] then class.totems[ k ] = v end
|
|
end
|
|
|
|
for k, v in pairs( spec.packs ) do
|
|
if not class.packs[ k ] then class.packs[ k ] = v end
|
|
end
|
|
|
|
for name, func in pairs( spec.stateExprs ) do
|
|
if not class.stateExprs[ name ] then
|
|
if rawget( state, name ) then state[ name ] = nil end
|
|
class.stateExprs[ name ] = func
|
|
end
|
|
end
|
|
|
|
for name, func in pairs( spec.stateFuncs ) do
|
|
if not class.stateFuncs[ name ] then
|
|
if rawget( state, name ) then
|
|
Hekili:Error( "Cannot RegisterStateFunc for an existing expression ( " .. spec.name .. " - " .. name .. " )." )
|
|
else
|
|
class.stateFuncs[ name ] = func
|
|
rawset( state, name, func )
|
|
-- Hekili:Error( "Not real error, registered " .. name .. " for " .. spec.name .. " (RSF)." )
|
|
end
|
|
end
|
|
end
|
|
|
|
for name, t in pairs( spec.stateTables ) do
|
|
if not class.stateTables[ name ] then
|
|
if rawget( state, name ) then
|
|
Hekili:Error( "Cannot RegisterStateTable for an existing expression ( " .. spec.name .. " - " .. name .. " )." )
|
|
else
|
|
class.stateTables[ name ] = t
|
|
rawset( state, name, t )
|
|
-- Hekili:Error( "Not real error, registered " .. name .. " for " .. spec.name .. " (RST)." )
|
|
end
|
|
end
|
|
end
|
|
|
|
if spec.id > 0 then
|
|
local s = rawget( Hekili.DB.profile.specs, spec.id )
|
|
|
|
if s then
|
|
for k, v in pairs( spec.settings ) do
|
|
if s.settings[ v.name ] == nil then s.settings[ v.name ] = v.default end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
for k in pairs( class.abilityList ) do
|
|
local ability = class.abilities[ k ]
|
|
|
|
if ability and ability.id > 0 then
|
|
if not ability.texture or not ability.name then
|
|
local data = GetSpellInfo( ability.id )
|
|
|
|
if data and data.name and data.iconID then
|
|
ability.name = ability.name or data.name
|
|
class.abilityList[ k ] = "|T" .. data.iconID .. ":0|t " .. ability.name
|
|
end
|
|
else
|
|
class.abilityList[ k ] = "|T" .. ability.texture .. ":0|t " .. ability.name
|
|
end
|
|
end
|
|
end
|
|
|
|
state.GUID = UnitGUID( "player" )
|
|
state.player.unit = UnitGUID( "player" )
|
|
|
|
ns.callHook( "specializationChanged" )
|
|
|
|
ns.updateTalents()
|
|
ResetDisabledGearAndSpells()
|
|
|
|
state.swings.mh_speed, state.swings.oh_speed = UnitAttackSpeed( "player" )
|
|
|
|
HekiliEngine.activeThread = nil
|
|
self:UpdateDisplayVisibility()
|
|
self:UpdateDamageDetectionForCLEU()
|
|
end
|
|
|
|
|
|
do
|
|
RegisterEvent( "PLAYER_ENTERING_WORLD", function( event, login, reload )
|
|
if login or reload then
|
|
local currentSpec = GetSpecialization()
|
|
local currentID = GetSpecializationInfo( currentSpec )
|
|
|
|
if currentID ~= state.spec.id then
|
|
Hekili:SpecializationChanged()
|
|
end
|
|
end
|
|
end )
|
|
|
|
local SpellDisableEvents = {
|
|
CHALLENGE_MODE_START = 1,
|
|
CHALLENGE_MODE_RESET = 1,
|
|
CHALLENGE_MODE_COMPLETED = 1,
|
|
PLAYER_ALIVE = 1,
|
|
ZONE_CHANGED_NEW_AREA = 1,
|
|
QUEST_SESSION_CREATED = 1,
|
|
QUEST_SESSION_DESTROYED = 1,
|
|
QUEST_SESSION_ENABLED_STATE_CHANGED = 1,
|
|
QUEST_SESSION_JOINED = 1,
|
|
QUEST_SESSION_LEFT = 1
|
|
}
|
|
|
|
local WipeCovenantCache = ns.WipeCovenantCache
|
|
|
|
local function CheckSpellsAndGear()
|
|
WipeCovenantCache()
|
|
ResetDisabledGearAndSpells()
|
|
ns.updateGear()
|
|
end
|
|
|
|
for k in pairs( SpellDisableEvents ) do
|
|
RegisterEvent( k, function( event )
|
|
C_Timer.After( 1, CheckSpellsAndGear )
|
|
end )
|
|
end
|
|
end
|
|
|
|
|
|
class.trinkets = {
|
|
[0] = { -- for when nothing is equipped.
|
|
},
|
|
}
|
|
|
|
|
|
setmetatable( class.trinkets, {
|
|
__index = function( t, k )
|
|
return t[0]
|
|
end
|
|
} )
|