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