You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
6694 lines
204 KiB
6694 lines
204 KiB
|
5 years ago
|
-- State.lua
|
||
|
|
-- June 2014
|
||
|
|
|
||
|
|
local addon, ns = ...
|
||
|
|
local Hekili = _G[ addon ]
|
||
|
|
|
||
|
|
local auras = ns.auras
|
||
|
|
|
||
|
|
local formatKey = ns.formatKey
|
||
|
|
local getSpecializationID = ns.getSpecializationID
|
||
|
|
local ResourceRegenerates = ns.ResourceRegenerates
|
||
|
|
|
||
|
|
local Error = ns.Error
|
||
|
|
local IsActiveSpell = ns.IsActiveSpell
|
||
|
|
|
||
|
|
local orderedPairs = ns.orderedPairs
|
||
|
|
local round, roundUp, roundDown = ns.round, ns.roundUp, ns.roundDown
|
||
|
|
local safeMin, safeMax = ns.safeMin, ns.safeMax
|
||
|
|
|
||
|
|
local tcopy = ns.tableCopy
|
||
|
|
|
||
|
|
-- Clean up table_x later.
|
||
|
|
local insert, remove, sort, unpack, wipe = table.insert, table.remove, table.sort, table.unpack, table.wipe
|
||
|
|
local RC = LibStub( "LibRangeCheck-2.0" )
|
||
|
|
local LSR = LibStub( "SpellRange-1.0" )
|
||
|
|
|
||
|
|
|
||
|
|
local class = Hekili.Class
|
||
|
|
local scripts = Hekili.Scripts
|
||
|
|
|
||
|
|
|
||
|
|
-- This will be our environment table for local functions.
|
||
|
|
local state = Hekili.State
|
||
|
|
|
||
|
|
|
||
|
|
state.iteration = 0
|
||
|
|
|
||
|
|
local PTR = ns.PTR
|
||
|
|
state.PTR = PTR
|
||
|
|
state.ptr = PTR and 1 or 0
|
||
|
|
|
||
|
|
state.now = 0
|
||
|
|
state.offset = 0
|
||
|
|
|
||
|
|
state.encounterID = 0
|
||
|
|
state.encounterName = "None"
|
||
|
|
state.encounterDifficulty = 0
|
||
|
|
|
||
|
|
state.aggro = false
|
||
|
|
state.tanking = false
|
||
|
|
|
||
|
|
state.delay = 0
|
||
|
|
state.delayMin = 0
|
||
|
|
state.delayMax = 60
|
||
|
|
|
||
|
|
state.false_start = 0
|
||
|
|
state.latency = 0
|
||
|
|
|
||
|
|
state.filter = "none"
|
||
|
|
state.cast_target = "nobody"
|
||
|
|
|
||
|
|
state.arena = false
|
||
|
|
state.bg = false
|
||
|
|
|
||
|
|
state.mainhand_speed = 0
|
||
|
|
state.offhand_speed = 0
|
||
|
|
|
||
|
|
state.min_targets = 0
|
||
|
|
state.max_targets = 0
|
||
|
|
|
||
|
|
state.action = {}
|
||
|
|
state.active_dot = {}
|
||
|
|
state.args = {}
|
||
|
|
state.azerite = {}
|
||
|
|
state.essence = {}
|
||
|
|
state.aura = {}
|
||
|
|
state.buff = {}
|
||
|
|
state.auras = auras
|
||
|
|
state.consumable = {}
|
||
|
|
state.cooldown = {}
|
||
|
|
state.corruptions = {} -- TODO: REMOVE
|
||
|
|
state.legendary = {}
|
||
|
|
state.runeforge = state.legendary -- Different APLs use runeforge.X.equipped vs. legendary.X.enabled.
|
||
|
|
--[[ state.health = {
|
||
|
|
resource = "health",
|
||
|
|
actual = 10000,
|
||
|
|
max = 10000,
|
||
|
|
regen = 0
|
||
|
|
} ]]
|
||
|
|
state.debuff = {}
|
||
|
|
state.dot = {}
|
||
|
|
state.equipped = {}
|
||
|
|
state.gcd = {}
|
||
|
|
|
||
|
|
state.history = {
|
||
|
|
casts = {},
|
||
|
|
units = {}
|
||
|
|
}
|
||
|
|
|
||
|
|
state.holds = {}
|
||
|
|
|
||
|
|
state.items = {}
|
||
|
|
state.pet = {
|
||
|
|
fake_pet = {
|
||
|
|
name = "Mary-Kate Olsen",
|
||
|
|
expires = 0,
|
||
|
|
permanent = false,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
state.player = {
|
||
|
|
lastcast = "none",
|
||
|
|
lastgcd = "none",
|
||
|
|
lastoffgcd = "none",
|
||
|
|
casttime = 0,
|
||
|
|
updated = true,
|
||
|
|
channeling = false,
|
||
|
|
channel_start = 0,
|
||
|
|
channel_end = 0,
|
||
|
|
channel_spell = nil
|
||
|
|
}
|
||
|
|
state.prev = {
|
||
|
|
meta = 'castsAll',
|
||
|
|
history = { "no_action", "no_action", "no_action", "no_action", "no_action" }
|
||
|
|
}
|
||
|
|
state.prev_gcd = {
|
||
|
|
meta = 'castsOn',
|
||
|
|
history = { "no_action", "no_action", "no_action", "no_action", "no_action" }
|
||
|
|
}
|
||
|
|
state.prev_off_gcd = {
|
||
|
|
meta = 'castsOff',
|
||
|
|
history = { "no_action", "no_action", "no_action", "no_action", "no_action" }
|
||
|
|
}
|
||
|
|
state.predictions = {}
|
||
|
|
state.predictionsOff = {}
|
||
|
|
state.predictionsOn = {}
|
||
|
|
state.purge = {}
|
||
|
|
state.pvptalent = {}
|
||
|
|
state.race = {}
|
||
|
|
state.script = {}
|
||
|
|
state.set_bonus = {}
|
||
|
|
state.settings = {}
|
||
|
|
state.sim = {}
|
||
|
|
state.spec = {}
|
||
|
|
state.stance = {}
|
||
|
|
state.stat = {}
|
||
|
|
state.swings = {
|
||
|
|
mh_actual = 0,
|
||
|
|
mh_speed = UnitAttackSpeed( "player" ) > 0 and UnitAttackSpeed( "player" ) or 2.6,
|
||
|
|
mh_projected = 2.6,
|
||
|
|
oh_actual = 0,
|
||
|
|
oh_speed = select( 2, UnitAttackSpeed( "player" ) ) or 2.6,
|
||
|
|
oh_projected = 3.9
|
||
|
|
}
|
||
|
|
state.system = {}
|
||
|
|
state.table = table
|
||
|
|
state.talent = {}
|
||
|
|
state.target = {
|
||
|
|
debuff = state.debuff,
|
||
|
|
dot = state.dot,
|
||
|
|
health = {},
|
||
|
|
updated = true
|
||
|
|
}
|
||
|
|
|
||
|
|
state.movement = {}
|
||
|
|
|
||
|
|
setmetatable( state.movement, {
|
||
|
|
__index = function( t, k )
|
||
|
|
if k == "distance" then
|
||
|
|
if state.buff.movement.up then return state.target.distance end
|
||
|
|
return 0
|
||
|
|
end
|
||
|
|
|
||
|
|
return state.target[ k ]
|
||
|
|
end
|
||
|
|
} )
|
||
|
|
|
||
|
|
state.sim.target = state.target
|
||
|
|
state.toggle = {}
|
||
|
|
state.totem = {}
|
||
|
|
|
||
|
|
|
||
|
|
state.trinket = {
|
||
|
|
t1 = {
|
||
|
|
slot = "t1",
|
||
|
|
|
||
|
|
--[[ has_cooldown = {
|
||
|
|
slot = "t1"
|
||
|
|
}, ]]
|
||
|
|
|
||
|
|
stacking_stat = {
|
||
|
|
slot = "t1"
|
||
|
|
},
|
||
|
|
has_stacking_stat = {
|
||
|
|
slot = "t1"
|
||
|
|
},
|
||
|
|
|
||
|
|
stat = {
|
||
|
|
slot = "t1"
|
||
|
|
},
|
||
|
|
has_stat = {
|
||
|
|
slot = "t1"
|
||
|
|
},
|
||
|
|
|
||
|
|
is = {
|
||
|
|
slot = "t1"
|
||
|
|
},
|
||
|
|
},
|
||
|
|
|
||
|
|
t2 = {
|
||
|
|
slot = "t2",
|
||
|
|
|
||
|
|
--[[ has_cooldown = {
|
||
|
|
slot = "t2",
|
||
|
|
}, ]]
|
||
|
|
|
||
|
|
stacking_stat = {
|
||
|
|
slot = "t2"
|
||
|
|
},
|
||
|
|
has_stacking_stat = {
|
||
|
|
slot = "t2"
|
||
|
|
},
|
||
|
|
|
||
|
|
stat = {
|
||
|
|
slot = "t2"
|
||
|
|
},
|
||
|
|
has_stat = {
|
||
|
|
slot = "t2",
|
||
|
|
},
|
||
|
|
|
||
|
|
is = {
|
||
|
|
slot = "t2",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
|
||
|
|
any = {},
|
||
|
|
|
||
|
|
cooldown = {
|
||
|
|
},
|
||
|
|
has_cooldown = {
|
||
|
|
},
|
||
|
|
|
||
|
|
stacking_stat = {
|
||
|
|
},
|
||
|
|
has_stacking_stat = {
|
||
|
|
},
|
||
|
|
|
||
|
|
stacking_proc = {
|
||
|
|
},
|
||
|
|
has_stacking_proc = {
|
||
|
|
},
|
||
|
|
|
||
|
|
stat = {
|
||
|
|
},
|
||
|
|
has_stat = {
|
||
|
|
},
|
||
|
|
}
|
||
|
|
state.trinket.proc = state.trinket.stat
|
||
|
|
state.trinket[1] = state.trinket.t1
|
||
|
|
state.trinket[2] = state.trinket.t2
|
||
|
|
|
||
|
|
state.using_apl = setmetatable( {}, {
|
||
|
|
__index = function( t, k )
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
} )
|
||
|
|
|
||
|
|
|
||
|
|
state.role = setmetatable( {}, {
|
||
|
|
__index = function( t, k )
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
} )
|
||
|
|
|
||
|
|
local mt_no_trinket_cooldown = {
|
||
|
|
}
|
||
|
|
|
||
|
|
local mt_no_trinket_stacking_stat = {
|
||
|
|
}
|
||
|
|
|
||
|
|
local mt_no_trinket_stat = {
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
local mt_no_trinket = {
|
||
|
|
__index = function( t, k )
|
||
|
|
if k:sub(1,4) == "has_" then
|
||
|
|
return false
|
||
|
|
elseif k == "down" then
|
||
|
|
return true
|
||
|
|
end
|
||
|
|
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
}
|
||
|
|
|
||
|
|
local no_trinket = setmetatable( {
|
||
|
|
slot = "none",
|
||
|
|
cooldown = setmetatable( {}, mt_no_trinket_cooldown ),
|
||
|
|
stacking_stat = setmetatable( {}, mt_no_trinket_stacking_stat ),
|
||
|
|
stat = setmetatable( {}, mt_no_trinket_stat ),
|
||
|
|
is = setmetatable( {}, {
|
||
|
|
__index = function( t, k )
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
} )
|
||
|
|
}, mt_no_trinket )
|
||
|
|
|
||
|
|
|
||
|
|
state.trinket.stat.any = state.trinket.any
|
||
|
|
|
||
|
|
|
||
|
|
local mt_trinket_any = {
|
||
|
|
__index = function( t, k )
|
||
|
|
return state.trinket.t1[ k ] or state.trinket.t2[ k ]
|
||
|
|
end
|
||
|
|
}
|
||
|
|
|
||
|
|
setmetatable( state.trinket.any, mt_trinket_any )
|
||
|
|
|
||
|
|
local mt_trinket_any_stacking_stat = {
|
||
|
|
__index = function( t, k )
|
||
|
|
if state.trinket.t1.has_stacking_stat[k] then return state.trinket.t1
|
||
|
|
elseif state.trinket.t2.has_stacking_stat[k] then return state.trinket.t2 end
|
||
|
|
return no_trinket
|
||
|
|
end
|
||
|
|
}
|
||
|
|
|
||
|
|
setmetatable( state.trinket.stacking_stat, mt_trinket_any_stacking_stat )
|
||
|
|
setmetatable( state.trinket.stacking_proc, mt_trinket_any_stacking_stat )
|
||
|
|
|
||
|
|
local mt_trinket_any_stat = {
|
||
|
|
__index = function( t, k )
|
||
|
|
--[[ if k == "any" then
|
||
|
|
return ( state.trinket.has_stat[
|
||
|
|
end ]]
|
||
|
|
|
||
|
|
if state.trinket.t1.has_stat[k] then return state.trinket.t1
|
||
|
|
elseif state.trinket.t2.has_stat[k] then return state.trinket.t2 end
|
||
|
|
return no_trinket
|
||
|
|
end
|
||
|
|
}
|
||
|
|
|
||
|
|
setmetatable( state.trinket.stat, mt_trinket_any_stat )
|
||
|
|
|
||
|
|
|
||
|
|
local mt_trinket = {
|
||
|
|
__index = function( t, k )
|
||
|
|
local isEnabled = ( not rawget( t, "__usable" ) ) or ( rawget( t, "__ability" ) and not state:IsDisabled( t.__ability ) or false )
|
||
|
|
|
||
|
|
if k == "id" then
|
||
|
|
return isEnabled and t.__id or 0
|
||
|
|
elseif k == "ability" then
|
||
|
|
return rawget( t, "__ability" ) or "null_cooldown"
|
||
|
|
elseif k == "usable" then
|
||
|
|
return rawget( t, "__usable" ) or false
|
||
|
|
elseif k == "has_use_buff" or k == "use_buff" then
|
||
|
|
return isEnabled and t.__has_use_buff or false
|
||
|
|
elseif k == "use_buff_duration" or k == "buff_duration" then
|
||
|
|
return isEnabled and t.__has_use_buff and t.__use_buff.duration or 0
|
||
|
|
elseif k == "has_proc" or k == "proc" then
|
||
|
|
return isEnabled and t.__proc or false
|
||
|
|
end
|
||
|
|
|
||
|
|
if k == "up" or k == "ticking" or k == "active" then
|
||
|
|
return isEnabled and class.trinkets[ t.id ].buff and state.buff[ class.trinkets[ t.id ].buff ].up or false
|
||
|
|
elseif k == "react" or k == "stack" or k == "stacks" then
|
||
|
|
return isEnabled and class.trinkets[ t.id ].buff and state.buff[ class.trinkets[ t.id ].buff ][k] or 0
|
||
|
|
elseif k == "remains" then
|
||
|
|
return isEnabled and class.trinkets[ t.id ].buff and state.buff[ class.trinkets[ t.id ].buff ].remains or 0
|
||
|
|
elseif k == "has_cooldown" then
|
||
|
|
return isEnabled and ( GetItemSpell( t.id ) ~= nil ) or false
|
||
|
|
elseif k == "ready_cooldown" then
|
||
|
|
if isEnabled and t.usable and t.ability then
|
||
|
|
return t.cooldown.ready
|
||
|
|
end
|
||
|
|
return true
|
||
|
|
elseif k == "cooldown" then
|
||
|
|
if t.usable and t.ability and state.cooldown[ t.ability ] then
|
||
|
|
return state.cooldown[ t.ability ]
|
||
|
|
end
|
||
|
|
return state.cooldown.null_cooldown
|
||
|
|
end
|
||
|
|
|
||
|
|
return k
|
||
|
|
end
|
||
|
|
}
|
||
|
|
|
||
|
|
setmetatable( state.trinket.t1, mt_trinket )
|
||
|
|
setmetatable( state.trinket.t2, mt_trinket )
|
||
|
|
|
||
|
|
|
||
|
|
local mt_trinket_is = {
|
||
|
|
__index = function( t, k )
|
||
|
|
local item = state.trinket[ t.slot ]
|
||
|
|
|
||
|
|
if item.usable and item.ability == k then return true end
|
||
|
|
|
||
|
|
return false
|
||
|
|
end,
|
||
|
|
}
|
||
|
|
|
||
|
|
setmetatable( state.trinket.t1.is, mt_trinket_is )
|
||
|
|
setmetatable( state.trinket.t2.is, mt_trinket_is )
|
||
|
|
|
||
|
|
|
||
|
|
--[[ local mt_trinket_cooldown = {
|
||
|
|
__index = function(t, k)
|
||
|
|
if k == "duration" or k == "expires" then
|
||
|
|
-- Refresh the ID in case we changed specs and ability is spec dependent.
|
||
|
|
local start, duration = GetItemCooldown( state.trinket[ t.slot ].id )
|
||
|
|
|
||
|
|
t.duration = duration or 0
|
||
|
|
t.expires = start and ( start + duration ) or 0
|
||
|
|
|
||
|
|
return t[k]
|
||
|
|
|
||
|
|
elseif k == "remains" then
|
||
|
|
return max( 0, t.expires - ( state.query_time ) )
|
||
|
|
|
||
|
|
elseif k == "up" then
|
||
|
|
return t.remains == 0
|
||
|
|
|
||
|
|
elseif k == "down" then
|
||
|
|
return t.remains > 0
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
-- return Error( "UNK: " .. k )
|
||
|
|
|
||
|
|
end
|
||
|
|
}
|
||
|
|
|
||
|
|
setmetatable( state.trinket.t1.cooldown, mt_trinket_cooldown )
|
||
|
|
setmetatable( state.trinket.t2.cooldown, mt_trinket_cooldown ) ]]
|
||
|
|
|
||
|
|
|
||
|
|
local mt_trinket_has_stacking_stat = {
|
||
|
|
__index = function( t, k )
|
||
|
|
local trinket = state.trinket[ t.slot ].id
|
||
|
|
|
||
|
|
if trinket == 0 then return false end
|
||
|
|
|
||
|
|
if k == "any" or k == "any_dps" then return class.trinkets[ trinket ].stacking_stat ~= nil end
|
||
|
|
if k == "ms" then k = "multistrike" end
|
||
|
|
|
||
|
|
return class.trinkets[ trinket ].stacking_stat == k
|
||
|
|
end
|
||
|
|
}
|
||
|
|
|
||
|
|
setmetatable( state.trinket.t1.has_stacking_stat, mt_trinket_has_stacking_stat )
|
||
|
|
setmetatable( state.trinket.t2.has_stacking_stat, mt_trinket_has_stacking_stat )
|
||
|
|
|
||
|
|
|
||
|
|
local mt_trinket_has_stat = {
|
||
|
|
__index = function( t, k )
|
||
|
|
local trinket = state.trinket[ t.slot ].id
|
||
|
|
|
||
|
|
if trinket == 0 then return false end
|
||
|
|
|
||
|
|
if k == "any" or k == "any_dps" then return class.trinkets[ trinket ].stat ~= nil end
|
||
|
|
if k == "ms" then k = "multistrike" end
|
||
|
|
|
||
|
|
return class.trinkets[ trinket ].stat == k
|
||
|
|
end
|
||
|
|
}
|
||
|
|
|
||
|
|
setmetatable( state.trinket.t1.has_stat, mt_trinket_has_stat )
|
||
|
|
setmetatable( state.trinket.t2.has_stat, mt_trinket_has_stat )
|
||
|
|
|
||
|
|
|
||
|
|
local mt_trinkets_has_stat = {
|
||
|
|
__index = function( t, k )
|
||
|
|
if k == "ms" then k = "multistrike" end
|
||
|
|
|
||
|
|
if k == "any" then
|
||
|
|
return class.trinkets[ state.trinket.t1.id ].stat ~= nil or class.trinkets[ state.trinket.t2.id ].stat ~= nil
|
||
|
|
end
|
||
|
|
|
||
|
|
return class.trinkets[ state.trinket.t1.id ].stat == k or class.trinkets[ state.trinket.t2.id ].stat == k
|
||
|
|
end
|
||
|
|
}
|
||
|
|
|
||
|
|
setmetatable( state.trinket.has_stat, mt_trinkets_has_stat )
|
||
|
|
|
||
|
|
|
||
|
|
local mt_trinkets_has_stacking_stat = {
|
||
|
|
__index = function( t, k )
|
||
|
|
if k == "ms" then k = "multistrike" end
|
||
|
|
|
||
|
|
if k == "any" then
|
||
|
|
return class.trinkets[ state.trinket.t1.id ].stacking_stat ~= nil or class.trinkets[ state.trinket.t2.id ].stacking_stat ~= nil
|
||
|
|
end
|
||
|
|
|
||
|
|
return class.trinkets[ state.trinket.t1.id ].stacking_stat == k or class.trinkets[ state.trinket.t2.id ].stacking_stat == k
|
||
|
|
end
|
||
|
|
}
|
||
|
|
|
||
|
|
setmetatable( state.trinket.has_stacking_stat, mt_trinkets_has_stacking_stat )
|
||
|
|
|
||
|
|
|
||
|
|
state.max = safeMax
|
||
|
|
state.min = safeMin
|
||
|
|
state.print = print
|
||
|
|
|
||
|
|
state.Enum = Enum
|
||
|
|
state.FindUnitBuffByID = ns.FindUnitBuffByID
|
||
|
|
state.FindUnitDebuffByID = ns.FindUnitDebuffByID
|
||
|
|
state.GetActiveLossOfControlData = C_LossOfControl.GetActiveLossOfControlData
|
||
|
|
state.GetActiveLossOfControlDataCount = C_LossOfControl.GetActiveLossOfControlDataCount
|
||
|
|
state.GetItemCooldown = GetItemCooldown
|
||
|
|
state.GetItemCount = GetItemCount
|
||
|
|
state.GetItemGem = GetItemGem
|
||
|
|
state.GetPlayerAuraBySpellID = GetPlayerAuraBySpellID
|
||
|
|
state.GetShapeshiftForm = GetShapeshiftForm
|
||
|
|
state.GetShapeshiftFormInfo = GetShapeshiftFormInfo
|
||
|
|
state.GetSpellCount = GetSpellCount
|
||
|
|
state.GetSpellInfo = GetSpellInfo
|
||
|
|
state.GetSpellTexture = GetSpellTexture
|
||
|
|
state.GetStablePetInfo = GetStablePetInfo
|
||
|
|
state.GetTime = GetTime
|
||
|
|
state.GetTotemInfo = GetTotemInfo
|
||
|
|
state.IsActiveSpell = ns.IsActiveSpell
|
||
|
|
state.IsPlayerSpell = IsPlayerSpell
|
||
|
|
state.IsSpellKnown = IsSpellKnown
|
||
|
|
state.IsSpellKnownOrOverridesKnown = IsSpellKnownOrOverridesKnown
|
||
|
|
state.IsUsableItem = IsUsableItem
|
||
|
|
state.IsUsableSpell = IsUsableSpell
|
||
|
|
state.UnitBuff = UnitBuff
|
||
|
|
state.UnitCanAttack = UnitCanAttack
|
||
|
|
state.UnitCastingInfo = UnitCastingInfo
|
||
|
|
state.UnitChannelInfo = UnitChannelInfo
|
||
|
|
state.UnitClassification = UnitClassification
|
||
|
|
state.UnitDebuff = UnitDebuff
|
||
|
|
state.UnitExists = UnitExists
|
||
|
|
state.UnitGetTotalAbsorbs = UnitGetTotalAbsorbs
|
||
|
|
state.UnitGUID = UnitGUID
|
||
|
|
state.UnitHealth = UnitHealth
|
||
|
|
state.UnitHealthMax = UnitHealthMax
|
||
|
|
state.UnitName = UnitName
|
||
|
|
state.UnitIsFriend = UnitIsFriend
|
||
|
|
state.UnitIsUnit = UnitIsUnit
|
||
|
|
state.UnitIsPlayer = UnitIsPlayer
|
||
|
|
state.UnitLevel = UnitLevel
|
||
|
|
state.UnitPower = UnitPower
|
||
|
|
state.UnitPowerMax = UnitPowerMax
|
||
|
|
state.abs = math.abs
|
||
|
|
state.ceil = math.ceil
|
||
|
|
state.floor = math.floor
|
||
|
|
state.format = string.format
|
||
|
|
state.ipairs = ipairs
|
||
|
|
state.pairs = pairs
|
||
|
|
state.rawget = rawget
|
||
|
|
state.rawset = rawset
|
||
|
|
state.select = select
|
||
|
|
state.tinsert = table.insert
|
||
|
|
state.insert = table.insert
|
||
|
|
state.remove = table.remove
|
||
|
|
state.tonumber = tonumber
|
||
|
|
state.tostring = tostring
|
||
|
|
state.type = type
|
||
|
|
|
||
|
|
state.safenum = function( val )
|
||
|
|
if type( val ) == "number" then return val end
|
||
|
|
return val == true and 1 or 0
|
||
|
|
end
|
||
|
|
|
||
|
|
state.safebool = function( val )
|
||
|
|
if type( val ) == "boolean" then return val end
|
||
|
|
return val ~= 0 and true or false
|
||
|
|
end
|
||
|
|
|
||
|
|
state.combat = 0
|
||
|
|
state.faction = UnitFactionGroup( "player" )
|
||
|
|
state.race[ formatKey( UnitRace("player") ) ] = true
|
||
|
|
|
||
|
|
state.class = Hekili.Class
|
||
|
|
state.targets = ns.targets
|
||
|
|
|
||
|
|
state._G = 0
|
||
|
|
|
||
|
|
|
||
|
|
-- Place an ability on cooldown in the simulated game state.
|
||
|
|
local function setCooldown( action, duration )
|
||
|
|
local cd = state.cooldown[ action ] or {}
|
||
|
|
cd.duration = duration
|
||
|
|
cd.expires = state.query_time + duration
|
||
|
|
|
||
|
|
cd.charge = 0
|
||
|
|
cd.recharge_began = state.query_time
|
||
|
|
cd.next_charge = cd.expires
|
||
|
|
cd.recharge = duration
|
||
|
|
|
||
|
|
state.cooldown[ action ] = cd
|
||
|
|
end
|
||
|
|
state.setCooldown = setCooldown
|
||
|
|
|
||
|
|
|
||
|
|
local function spendCharges( action, charges )
|
||
|
|
local ability = class.abilities[ action ]
|
||
|
|
|
||
|
|
if not ability.charges or ability.charges == 1 then
|
||
|
|
setCooldown( action, ability.cooldown )
|
||
|
|
return
|
||
|
|
end
|
||
|
|
|
||
|
|
if not state.cooldown[ action ] then state.cooldown[ action ] = {} end
|
||
|
|
local cd = state.cooldown[ action ]
|
||
|
|
|
||
|
|
if cd.next_charge <= state.query_time then
|
||
|
|
cd.recharge_began = state.query_time
|
||
|
|
cd.next_charge = state.query_time + ( ability.recharge or ability.cooldown )
|
||
|
|
cd.recharge = ability.recharge
|
||
|
|
end
|
||
|
|
|
||
|
|
cd.charge = max( 0, cd.charge - charges )
|
||
|
|
|
||
|
|
if cd.charge == 0 then
|
||
|
|
cd.duration = ability.recharge or ability.cooldown
|
||
|
|
cd.expires = cd.next_charge
|
||
|
|
else
|
||
|
|
cd.duration = ability.recharge or ability.cooldown
|
||
|
|
cd.expires = 0
|
||
|
|
end
|
||
|
|
end
|
||
|
|
state.spendCharges = spendCharges
|
||
|
|
|
||
|
|
|
||
|
|
local function gainCharges( action, charges )
|
||
|
|
|
||
|
|
if class.abilities[ action ].charges then
|
||
|
|
state.cooldown[ action ].charge = min( class.abilities[ action ].charges, state.cooldown[ action ].charge + charges )
|
||
|
|
|
||
|
|
-- resolve cooldown state.
|
||
|
|
if state.cooldown[ action ].charge > 0 then
|
||
|
|
state.cooldown[ action ].duration = 0
|
||
|
|
state.cooldown[ action ].expires = 0
|
||
|
|
end
|
||
|
|
|
||
|
|
if state.cooldown[ action ].charge == class.abilities[ action ].charges then
|
||
|
|
state.cooldown[ action ].next_charge = 0
|
||
|
|
state.cooldown[ action ].recharge = 0
|
||
|
|
state.cooldown[ action ].recharge_began = 0
|
||
|
|
end
|
||
|
|
|
||
|
|
else
|
||
|
|
-- Error-proof gaining charges for abilities without charges.
|
||
|
|
if charges >= 1 then
|
||
|
|
setCooldown( action, 0 )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
end
|
||
|
|
state.gainCharges = gainCharges
|
||
|
|
|
||
|
|
|
||
|
|
function state.gainChargeTime( action, time, debug )
|
||
|
|
local ability = class.abilities[ action ]
|
||
|
|
if not ability then return end
|
||
|
|
|
||
|
|
local cooldown = state.cooldown[ action ]
|
||
|
|
|
||
|
|
if not ability.charges then
|
||
|
|
-- Error-proof gaining charge time on chargeless abilities.
|
||
|
|
cooldown.expires = cooldown.expires - time
|
||
|
|
return
|
||
|
|
end
|
||
|
|
|
||
|
|
if cooldown.charge == ability.charges then return end
|
||
|
|
|
||
|
|
cooldown.next_charge = cooldown.next_charge - time
|
||
|
|
cooldown.recharge_began = cooldown.recharge_began - time
|
||
|
|
|
||
|
|
if cooldown.expires > 0 then cooldown.expires = max( 0, cooldown.expires - time ) end
|
||
|
|
|
||
|
|
if cooldown.next_charge <= state.query_time then
|
||
|
|
cooldown.charge = min( ability.charges, cooldown.charge + 1 )
|
||
|
|
|
||
|
|
-- We have a charge, reset cooldown.
|
||
|
|
-- cooldown.duration = 0
|
||
|
|
cooldown.expires = 0
|
||
|
|
|
||
|
|
if cooldown.charge == ability.charges then
|
||
|
|
cooldown.next_charge = 0
|
||
|
|
cooldown.recharge = 0
|
||
|
|
cooldown.recharge_began = 0
|
||
|
|
else
|
||
|
|
cooldown.recharge_began = cooldown.next_charge
|
||
|
|
cooldown.next_charge = cooldown.next_charge + ability.recharge
|
||
|
|
cooldown.recharge = ability.recharge
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state.reduceCooldown( action, time )
|
||
|
|
local ability = class.abilities[ action ]
|
||
|
|
if not ability then return end
|
||
|
|
|
||
|
|
if ability.charges then
|
||
|
|
state.gainChargeTime( action, time )
|
||
|
|
return
|
||
|
|
end
|
||
|
|
|
||
|
|
state.cooldown[ action ].expires = max( 0, state.cooldown[ action ].expires - time )
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
-- Cycling System...
|
||
|
|
do
|
||
|
|
local cycle = {}
|
||
|
|
local debug = function( ... ) if Hekili.ActiveDebug then Hekili:Debug( ... ) end end
|
||
|
|
local format = string.format
|
||
|
|
|
||
|
|
function state.SetupCycle( ability, quiet )
|
||
|
|
wipe( cycle )
|
||
|
|
|
||
|
|
if not ability and not quiet then
|
||
|
|
debug( " - no ability provided to SetupCycle." )
|
||
|
|
return
|
||
|
|
end
|
||
|
|
|
||
|
|
local aura = ability.cycle
|
||
|
|
|
||
|
|
if not aura then
|
||
|
|
-- Fallback check, is there an aura with the same name as the ability?
|
||
|
|
aura = class.auras[ ability.key ] and ability.key
|
||
|
|
end
|
||
|
|
|
||
|
|
if not aura and not quiet then
|
||
|
|
debug( " - no aura identified for target-cycling and no aura matching " .. ability.key .. " found in ability / spec module; target cycling disabled." )
|
||
|
|
return
|
||
|
|
end
|
||
|
|
|
||
|
|
local cDebuff = state.debuff[ aura ]
|
||
|
|
|
||
|
|
if not cDebuff then
|
||
|
|
debug( " - the debuff '%s' was not found in our database.", aura )
|
||
|
|
return
|
||
|
|
end
|
||
|
|
|
||
|
|
if cDebuff.up then
|
||
|
|
cycle.expires = cDebuff.expires
|
||
|
|
cycle.minTTD = max( state.settings.cycle_min, ability.min_ttd or 0, cDebuff.duration / 2 )
|
||
|
|
cycle.maxTTD = ability.max_ttd
|
||
|
|
|
||
|
|
cycle.aura = aura
|
||
|
|
|
||
|
|
if not quiet then
|
||
|
|
debug( " - we will use the ability on a different target, if available, until %s expires at %.2f [+%.2f].", cycle.aura, cycle.expires, cycle.expires - state.query_time )
|
||
|
|
end
|
||
|
|
else
|
||
|
|
if not quiet then debug( " - cycle aura appears to be down, so we're sticking with our current target." ) end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
function state.GetCycleInfo()
|
||
|
|
return cycle.expires, cycle.minTTD, cycle.maxTTD, cycle.aura
|
||
|
|
end
|
||
|
|
|
||
|
|
function state.SetCycleInfo( expires, minTTD, maxTTD, aura )
|
||
|
|
cycle.expires = expires
|
||
|
|
cycle.minTTD = minTTD
|
||
|
|
cycle.maxTTD = maxTTD
|
||
|
|
cycle.aura = aura
|
||
|
|
end
|
||
|
|
|
||
|
|
function state.HasCyclingDebuff( aura )
|
||
|
|
if not cycle.aura then return false end
|
||
|
|
if aura and aura ~= cycle.aura then return false end
|
||
|
|
return true
|
||
|
|
end
|
||
|
|
|
||
|
|
function state.IsCycling( aura, quiet )
|
||
|
|
if not cycle.aura then
|
||
|
|
return false, "cycle.aura is nil"
|
||
|
|
end
|
||
|
|
if aura and cycle.aura ~= aura then
|
||
|
|
if not quiet then debug( "cycle.aura ~= '%s'", aura ) end
|
||
|
|
return false, format( "cycle aura (%s) is not '%s'", cycle.aura or "none", aura )
|
||
|
|
end
|
||
|
|
if state.cycle_enemies == 1 then
|
||
|
|
return false, "cycle_enemies == 1"
|
||
|
|
end
|
||
|
|
if cycle.expires < state.query_time then
|
||
|
|
return false, format( "cycle aura (%s) expires before current time", cycle.aura )
|
||
|
|
end
|
||
|
|
if state.active_dot[ cycle.aura ] >= state.cycle_enemies then
|
||
|
|
return false, format( "active_dot[%d] >= cycle_enemies[%d]", state.active_dot[ cycle.aura ], state.cycle_enemies )
|
||
|
|
end
|
||
|
|
|
||
|
|
return true
|
||
|
|
end
|
||
|
|
|
||
|
|
function state.ClearCycle()
|
||
|
|
if cycle.aura then wipe( cycle ) end
|
||
|
|
end
|
||
|
|
|
||
|
|
state.cycleInfo = cycle
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
-- Apply a buff to the current game state.
|
||
|
|
local function applyBuff( aura, duration, stacks, value, v2, v3, applied )
|
||
|
|
if not aura then
|
||
|
|
Error( "Attempted to apply/remove a nameless aura '%s'.", aura or "nil" )
|
||
|
|
return
|
||
|
|
end
|
||
|
|
|
||
|
|
local auraInfo = class.auras[ aura ]
|
||
|
|
|
||
|
|
if not auraInfo then
|
||
|
|
local spec = class.specs[ state.spec.id ]
|
||
|
|
if spec then
|
||
|
|
spec:RegisterAura( aura, { ["duration"] = duration } )
|
||
|
|
class.auras[ aura ] = spec.auras[ aura ]
|
||
|
|
end
|
||
|
|
|
||
|
|
auraInfo = class.auras[ aura ]
|
||
|
|
if not auraInfo then return end
|
||
|
|
end
|
||
|
|
|
||
|
|
if auraInfo.alias then
|
||
|
|
aura = auraInfo.alias[1]
|
||
|
|
end
|
||
|
|
|
||
|
|
if state.cycle then
|
||
|
|
if duration == 0 then state.active_dot[ aura ] = state.active_dot[ aura ] - 1
|
||
|
|
else state.active_dot[ aura ] = state.active_dot[ aura ] + 1 end
|
||
|
|
return
|
||
|
|
end
|
||
|
|
|
||
|
|
local b = state.buff[ aura ]
|
||
|
|
if not b then return end
|
||
|
|
if not duration then duration = class.auras[ aura ].duration or 15 end
|
||
|
|
|
||
|
|
if duration == 0 then
|
||
|
|
b.last_expiry = b.expires or 0
|
||
|
|
b.expires = 0
|
||
|
|
|
||
|
|
b.lastCount = b.count
|
||
|
|
b.count = 0
|
||
|
|
|
||
|
|
b.lastApplied = b.applied
|
||
|
|
b.last_application = b.applied or 0
|
||
|
|
|
||
|
|
b.v1 = value or 0
|
||
|
|
b.v2 = nil
|
||
|
|
b.v3 = nil
|
||
|
|
b.applied = 0
|
||
|
|
b.caster = "unknown"
|
||
|
|
|
||
|
|
state.active_dot[ aura ] = max( 0, state.active_dot[ aura ] - 1 )
|
||
|
|
|
||
|
|
else
|
||
|
|
if not b.up then state.active_dot[ aura ] = state.active_dot[ aura ] + 1 end
|
||
|
|
|
||
|
|
b.lastCount = b.count
|
||
|
|
b.lastApplied = b.applied
|
||
|
|
|
||
|
|
b.applied = applied or state.query_time
|
||
|
|
b.last_application = b.applied or 0
|
||
|
|
|
||
|
|
b.duration = duration
|
||
|
|
|
||
|
|
b.expires = b.applied + duration
|
||
|
|
b.last_expiry = b.expires
|
||
|
|
|
||
|
|
b.count = min( class.auras[ aura ].max_stack or 1, stacks or 1 )
|
||
|
|
b.v1 = value or 0
|
||
|
|
b.v2 = v2
|
||
|
|
b.v3 = v3
|
||
|
|
b.caster = "player"
|
||
|
|
end
|
||
|
|
|
||
|
|
for resource, auras in pairs( class.resourceAuras ) do
|
||
|
|
if auras[ aura ] then
|
||
|
|
state.forecastResources( resource )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if aura == "heroism" or aura == "time_warp" or aura == "ancient_hysteria" then
|
||
|
|
applyBuff( "bloodlust", duration, stacks, value )
|
||
|
|
elseif aura ~= "potion" and class.auras.potion and class.auras[ aura ].id == class.auras.potion.id then
|
||
|
|
applyBuff( "potion", duration, stacks, value )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
state.applyBuff = applyBuff
|
||
|
|
|
||
|
|
|
||
|
|
local function removeBuff( aura )
|
||
|
|
applyBuff( aura, 0 )
|
||
|
|
|
||
|
|
local auraInfo = class.auras[ aura ]
|
||
|
|
if auraInfo and auraInfo.alias then
|
||
|
|
for _, child in ipairs( auraInfo.alias ) do
|
||
|
|
applyBuff( child, 0 )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
state.removeBuff = removeBuff
|
||
|
|
|
||
|
|
|
||
|
|
-- Apply stacks of a buff to the current game state.
|
||
|
|
-- Wraps around Buff() to check for an existing buff.
|
||
|
|
local function addStack( aura, duration, stacks, value )
|
||
|
|
|
||
|
|
local a = class.auras[ aura ]
|
||
|
|
|
||
|
|
duration = duration or ( a and a.duration or 15 )
|
||
|
|
stacks = stacks or 1
|
||
|
|
|
||
|
|
local max_stack = a and a.max_stack or 1
|
||
|
|
|
||
|
|
local b = state.buff[ aura ]
|
||
|
|
|
||
|
|
if b.remains > 0 then
|
||
|
|
applyBuff( aura, duration, min( max_stack, b.count + stacks ), value )
|
||
|
|
else
|
||
|
|
applyBuff( aura, duration, min( max_stack, stacks ), value )
|
||
|
|
end
|
||
|
|
|
||
|
|
end
|
||
|
|
state.addStack = addStack
|
||
|
|
|
||
|
|
|
||
|
|
local function removeStack( aura, stacks )
|
||
|
|
stacks = stacks or 1
|
||
|
|
|
||
|
|
local b = state.buff[ aura ]
|
||
|
|
|
||
|
|
if b.count > stacks then
|
||
|
|
b.lastCount = b.count
|
||
|
|
b.count = max( 1, b.count - stacks )
|
||
|
|
else
|
||
|
|
removeBuff( aura )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
state.removeStack = removeStack
|
||
|
|
|
||
|
|
|
||
|
|
local function removeDebuffStack( unit, aura, stacks )
|
||
|
|
stacks = stacks or 1
|
||
|
|
|
||
|
|
local d = state.debuff[ aura ]
|
||
|
|
|
||
|
|
if not d then return end
|
||
|
|
|
||
|
|
if d.count > stacks then
|
||
|
|
d.lastCount = d.count
|
||
|
|
d.count = max( 1, d.count - stacks )
|
||
|
|
else
|
||
|
|
removeDebuff( unit, aura )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
state.removeDebuffStack = removeDebuffStack
|
||
|
|
|
||
|
|
|
||
|
|
-- Add a debuff to the simulated game state.
|
||
|
|
-- Needs to actually use "unit" !
|
||
|
|
local function applyDebuff( unit, aura, duration, stacks, value, noPandemic )
|
||
|
|
if not aura then aura = unit; unit = "target" end
|
||
|
|
|
||
|
|
if not class.auras[ aura ] then
|
||
|
|
Error( "Attempted to apply unknown aura '%s'.", aura )
|
||
|
|
local spec = class.specs[ state.spec.id ]
|
||
|
|
if spec then
|
||
|
|
spec:RegisterAura( aura, { ["duration"] = duration } )
|
||
|
|
class.auras[ aura ] = spec.auras[ aura ]
|
||
|
|
end
|
||
|
|
|
||
|
|
if not class.auras[ aura ] then return end
|
||
|
|
end
|
||
|
|
|
||
|
|
if state.cycle then
|
||
|
|
if duration == 0 then
|
||
|
|
if Hekili.ActiveDebug then Hekili:Debug( "Removed an application of '%s' while target-cycling.", aura ) end
|
||
|
|
state.active_dot[ aura ] = state.active_dot[ aura ] - 1
|
||
|
|
else
|
||
|
|
if Hekili.ActiveDebug then Hekili:Debug( "Added an application of '%s' while target-cycling.", aura ) end
|
||
|
|
state.active_dot[ aura ] = state.active_dot[ aura ] + 1
|
||
|
|
end
|
||
|
|
return
|
||
|
|
end
|
||
|
|
|
||
|
|
local d = state.debuff[ aura ]
|
||
|
|
duration = duration or class.auras[ aura ].duration or 15
|
||
|
|
|
||
|
|
if duration == 0 then
|
||
|
|
d.expires = 0
|
||
|
|
|
||
|
|
d.lastCount = d.count
|
||
|
|
d.lastApplied = d.lastApplied
|
||
|
|
|
||
|
|
d.count = 0
|
||
|
|
d.value = 0
|
||
|
|
d.applied = 0
|
||
|
|
d.unit = unit
|
||
|
|
|
||
|
|
state.active_dot[ aura ] = max( 0, state.active_dot[ aura ] - 1 )
|
||
|
|
else
|
||
|
|
if d.down or state.active_dot[ aura ] == 0 then
|
||
|
|
state.active_dot[ aura ] = state.active_dot[ aura ] + 1
|
||
|
|
-- TODO: Aura scraping utility may want to populate active_dot table when it sees an aura that wasn't tracked.
|
||
|
|
end
|
||
|
|
|
||
|
|
-- state.debuff[ aura ] = state.debuff[ aura ] or {}
|
||
|
|
d.expires = state.query_time + ( noPandemic and 0 or min( d.remains, 0.3 * ( class.auras[ aura ].duration or 15 ) ) ) + duration
|
||
|
|
|
||
|
|
d.lastCount = d.count or 0
|
||
|
|
d.lastApplied = d.applied or 0
|
||
|
|
|
||
|
|
d.count = min( class.auras[ aura ].max_stack or 1, stacks or 1 )
|
||
|
|
d.value = value or 0
|
||
|
|
d.applied = state.now
|
||
|
|
d.unit = unit or "target"
|
||
|
|
end
|
||
|
|
|
||
|
|
end
|
||
|
|
state.applyDebuff = applyDebuff
|
||
|
|
|
||
|
|
|
||
|
|
local function removeDebuff( unit, aura )
|
||
|
|
applyDebuff( unit, aura, 0 )
|
||
|
|
end
|
||
|
|
state.removeDebuff = removeDebuff
|
||
|
|
|
||
|
|
|
||
|
|
local function setStance( stance )
|
||
|
|
for k in pairs( state.stance ) do
|
||
|
|
state.stance[ k ] = false
|
||
|
|
end
|
||
|
|
state.stance[ stance ] = true
|
||
|
|
end
|
||
|
|
state.setStance = setStance
|
||
|
|
|
||
|
|
|
||
|
|
local function interrupt()
|
||
|
|
removeDebuff( "target", "casting" )
|
||
|
|
end
|
||
|
|
state.interrupt = interrupt
|
||
|
|
|
||
|
|
|
||
|
|
-- Use this for readyTime in an interrupt action; will interrupt casts at end of cast and channels ASAP.
|
||
|
|
local function timeToInterrupt()
|
||
|
|
if debuff.casting.down or debuff.casting.v2 then return 3600 end
|
||
|
|
if debuff.casting.v3 then return 0 end
|
||
|
|
return debuff.casting.remains - 0.25
|
||
|
|
end
|
||
|
|
state.timeToInterrupt = timeToInterrupt
|
||
|
|
|
||
|
|
|
||
|
|
-- Pet stuff.
|
||
|
|
local function summonPet( name, duration, spec )
|
||
|
|
state.pet[ name ] = rawget( state.pet, name ) or {}
|
||
|
|
state.pet[ name ].name = name
|
||
|
|
state.pet[ name ].expires = state.query_time + ( duration or 3600 )
|
||
|
|
|
||
|
|
if class.pets[ name ] then
|
||
|
|
state.pet[ name ].id = id
|
||
|
|
end
|
||
|
|
|
||
|
|
if spec then
|
||
|
|
state.pet[ name ].spec = spec
|
||
|
|
|
||
|
|
for k, v in pairs( state.pet ) do
|
||
|
|
if type(v) == "boolean" then state.pet[k] = false end
|
||
|
|
end
|
||
|
|
|
||
|
|
state.pet[ spec ] = state.pet[ name ]
|
||
|
|
end
|
||
|
|
end
|
||
|
|
state.summonPet = summonPet
|
||
|
|
|
||
|
|
|
||
|
|
local function dismissPet( name )
|
||
|
|
|
||
|
|
state.pet[ name ] = rawget( state.pet, name ) or {}
|
||
|
|
state.pet[ name ].name = name
|
||
|
|
state.pet[ name ].expires = 0
|
||
|
|
|
||
|
|
|
||
|
|
end
|
||
|
|
state.dismissPet = dismissPet
|
||
|
|
|
||
|
|
|
||
|
|
local function summonTotem( name, elem, duration )
|
||
|
|
|
||
|
|
if elem then
|
||
|
|
state.totem[ elem ] = rawget( state.totem, elem ) or {}
|
||
|
|
state.totem[ elem ].name = name
|
||
|
|
state.totem[ elem ].expires = state.query_time + duration
|
||
|
|
summonPet( elem, duration )
|
||
|
|
end
|
||
|
|
|
||
|
|
summonPet( name, duration )
|
||
|
|
end
|
||
|
|
state.summonTotem = summonTotem
|
||
|
|
|
||
|
|
|
||
|
|
-- Useful for things like leap/charge/etc.
|
||
|
|
local function setDistance( minimum, maximum )
|
||
|
|
state.target.minR = minimum or 5
|
||
|
|
state.target.maxR = maximum or minimum or 5
|
||
|
|
state.target.distance = ( state.target.minR + state.target.maxR ) / 2
|
||
|
|
end
|
||
|
|
state.setDistance = setDistance
|
||
|
|
|
||
|
|
|
||
|
|
-- For tracking if we are currently channeling.
|
||
|
|
function state.channelSpell( name, start, duration, id )
|
||
|
|
if name then
|
||
|
|
local ability = class.abilities[ name ]
|
||
|
|
|
||
|
|
start = start or state.query_time
|
||
|
|
|
||
|
|
if ability then
|
||
|
|
duration = duration or ability.cast
|
||
|
|
end
|
||
|
|
|
||
|
|
if not duration then return end
|
||
|
|
|
||
|
|
applyBuff( "casting", duration, nil, id or ( ability and ability.id ) or 0, nil, true, start )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
function state.stopChanneling( reset, action )
|
||
|
|
if not reset then
|
||
|
|
local spell = state.channel
|
||
|
|
local ability = spell and class.abilities[ spell ]
|
||
|
|
|
||
|
|
if spell then
|
||
|
|
if Hekili.ActiveDebug then Hekili:Debug( "Breaking channel of %s.", spell ) end
|
||
|
|
if ability and ability.breakchannel then ability.breakchannel() end
|
||
|
|
state:RemoveSpellEvents( spell )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
-- This will lock in gains from channeling before the channel ends.
|
||
|
|
for resource, auras in pairs( class.resourceAuras ) do
|
||
|
|
if auras.casting then state[ resource ].actual = state[ resource ].current end
|
||
|
|
end
|
||
|
|
|
||
|
|
removeBuff( "casting" )
|
||
|
|
end
|
||
|
|
-- See mt_state for 'isChanneling'.
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
-- Spell Targets, so I don't have to convert it in APLs any more.
|
||
|
|
-- This will also factor in target caps and TTD restrictions.
|
||
|
|
state.spell_targets = setmetatable( {}, {
|
||
|
|
__index = function( t, k )
|
||
|
|
local ability = class.abilities[ k ]
|
||
|
|
|
||
|
|
if not ability or state.active_enemies == 1 then return state.active_enemies end
|
||
|
|
|
||
|
|
local n = state.active_enemies
|
||
|
|
|
||
|
|
if ability.max_ttd then n = min( n, Hekili:GetNumTTDsBefore( ability.max_ttd + state.offset + state.delay ) ) end
|
||
|
|
if ability.min_ttd then n = min( n, Hekili:GetNumTTDsAfter( ability.min_ttd + state.offset + state.delay ) ) end
|
||
|
|
|
||
|
|
if ability.max_targets then n = min( n, ability.max_targets ) end
|
||
|
|
|
||
|
|
return n
|
||
|
|
end
|
||
|
|
} )
|
||
|
|
|
||
|
|
|
||
|
|
local raid_event_filter = {
|
||
|
|
["in"] = 3600,
|
||
|
|
amount = 0,
|
||
|
|
duration = 0,
|
||
|
|
remains = 0,
|
||
|
|
cooldown = 0,
|
||
|
|
exists = false,
|
||
|
|
distance = 0,
|
||
|
|
max_distance = 0,
|
||
|
|
min_distance = 0,
|
||
|
|
to_pct = 0,
|
||
|
|
up = false,
|
||
|
|
down = true
|
||
|
|
}
|
||
|
|
|
||
|
|
state.raid_event = setmetatable( {}, {
|
||
|
|
__index = function( t, k )
|
||
|
|
return raid_event_filter[ k ] or raid_event_filter
|
||
|
|
end
|
||
|
|
} )
|
||
|
|
|
||
|
|
|
||
|
|
-- We'll pretend we're in an active raid_event.adds when there are multiple targets.
|
||
|
|
state.raid_event.adds = setmetatable( {
|
||
|
|
["in"] = 3600, -- raid_event.adds.in appears to return time to the next add event, so we can just always say it's waaaay in the future.
|
||
|
|
}, {
|
||
|
|
__index = function( t, k )
|
||
|
|
if k == "up" or k == "exists" then
|
||
|
|
return state.active_enemies > 1
|
||
|
|
elseif k == "down" then
|
||
|
|
return state.active_enemies <= 1
|
||
|
|
elseif k == "in" then
|
||
|
|
return state.active_enemies > 1 and 0 or 3600
|
||
|
|
elseif k == "duration" or k == "remains" then
|
||
|
|
return state.active_enemies > 1 and state.fight_remains or 0
|
||
|
|
elseif raid_event_filter[k] ~= nil then return raid_event_filter[k] end
|
||
|
|
|
||
|
|
return 0
|
||
|
|
end
|
||
|
|
} )
|
||
|
|
|
||
|
|
|
||
|
|
-- Resource Modeling!
|
||
|
|
local events = {}
|
||
|
|
local remains = {}
|
||
|
|
|
||
|
|
local function resourceModelSort( a, b )
|
||
|
|
return b == nil or ( a.next < b.next )
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
local FORECAST_DURATION = 10.01
|
||
|
|
|
||
|
|
local function forecastResources( resource )
|
||
|
|
if not resource then return end
|
||
|
|
|
||
|
|
wipe( events )
|
||
|
|
wipe( remains )
|
||
|
|
|
||
|
|
local now = state.now + state.offset -- roundDown( state.now + state.offset, 2 )
|
||
|
|
|
||
|
|
local timeout = FORECAST_DURATION * state.haste -- roundDown( FORECAST_DURATION * state.haste, 2 )
|
||
|
|
if state.class.file == "DEATHKNIGHT" and state.runes then
|
||
|
|
timeout = max( timeout, 0.01 + 2 * state.runes.cooldown )
|
||
|
|
end
|
||
|
|
|
||
|
|
local r = state[ resource ]
|
||
|
|
|
||
|
|
-- We account for haste here so that we don't compute lots of extraneous future resource gains in Bloodlust/high haste situations.
|
||
|
|
remains[ resource ] = timeout
|
||
|
|
|
||
|
|
wipe( r.times )
|
||
|
|
wipe( r.values )
|
||
|
|
r.forecast[1] = r.forecast[1] or {}
|
||
|
|
r.forecast[1].t = now
|
||
|
|
r.forecast[1].v = r.actual
|
||
|
|
r.forecast[1].e = "actual"
|
||
|
|
r.fcount = 1
|
||
|
|
|
||
|
|
local models = r.regenModel
|
||
|
|
|
||
|
|
if models then
|
||
|
|
for k, v in pairs( models ) do
|
||
|
|
if ( not v.resource or v.resource == resource ) and
|
||
|
|
( not v.spec or state.spec[ v.spec ] ) and
|
||
|
|
( not v.equip or state.equipped[ v.equip ] ) and
|
||
|
|
( not v.talent or state.talent[ v.talent ].enabled ) and
|
||
|
|
( not v.pvptalent or state.pvptalent[ v.pvptalent ].enabled ) and
|
||
|
|
( not v.aura or state[ v.debuff and "debuff" or "buff" ][ v.aura ].remains > 0 ) and
|
||
|
|
( not v.set_bonus or state.set_bonus[ v.set_bonus ] > 0 ) and
|
||
|
|
( not v.setting or state.settings[ v.setting ] ) and
|
||
|
|
( not v.swing or state.swings[ v.swing .. "_speed" ] and state.swings[ v.swing .. "_speed" ] > 0 ) and
|
||
|
|
( not v.channel or state.buff.casting.up and state.buff.casting.v3 and state.buff.casting.v1 == class.abilities[ v.channel ].id ) then
|
||
|
|
|
||
|
|
local r = state[ v.resource ]
|
||
|
|
|
||
|
|
local l = v.last()
|
||
|
|
local i = type( v.interval ) == "number" and v.interval or ( type( v.interval ) == "function" and v.interval( now, r.actual ) or ( type( v.interval ) == "string" and state[ v.interval ] or 0 ) )
|
||
|
|
-- local i = roundDown( type( v.interval ) == "number" and v.interval or ( type( v.interval ) == "function" and v.interval( now, r.actual ) or ( type( v.interval ) == "string" and state[ v.interval ] or 0 ) ), 2 )
|
||
|
|
|
||
|
|
v.next = l + i
|
||
|
|
v.name = k
|
||
|
|
|
||
|
|
if i > 0 and v.next >= 0 then
|
||
|
|
table.insert( events, v )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
sort( events, resourceModelSort )
|
||
|
|
|
||
|
|
local finish = now + timeout
|
||
|
|
|
||
|
|
local prev = now
|
||
|
|
local iter = 0
|
||
|
|
|
||
|
|
while( #events > 0 and now <= finish and iter < 20 ) do
|
||
|
|
local e = events[1]
|
||
|
|
local r = state[ e.resource ]
|
||
|
|
iter = iter + 1
|
||
|
|
|
||
|
|
if e.next > finish or not r or not r.actual then
|
||
|
|
table.remove( events, 1 )
|
||
|
|
|
||
|
|
else
|
||
|
|
now = e.next
|
||
|
|
|
||
|
|
local bonus = r.regen * ( now - prev )
|
||
|
|
|
||
|
|
if ( e.stop and e.stop( r.forecast[ r.fcount ].v ) ) or ( e.aura and state[ e.debuff and "debuff" or "buff" ][ e.aura ].expires < now ) or ( e.channel and state.buff.casting.expires < now ) then
|
||
|
|
table.remove( events, 1 )
|
||
|
|
|
||
|
|
local v = max( 0, min( r.max, r.forecast[ r.fcount ].v + bonus ) )
|
||
|
|
local idx
|
||
|
|
|
||
|
|
if r.forecast[ r.fcount ].t == now then
|
||
|
|
-- Reuse the last one.
|
||
|
|
idx = r.fcount
|
||
|
|
else
|
||
|
|
idx = r.fcount + 1
|
||
|
|
end
|
||
|
|
|
||
|
|
r.forecast[ idx ] = r.forecast[ idx ] or {}
|
||
|
|
r.forecast[ idx ].t = now
|
||
|
|
r.forecast[ idx ].v = v
|
||
|
|
r.forecast[ idx ].e = e.name or "none"
|
||
|
|
r.fcount = idx
|
||
|
|
else
|
||
|
|
prev = now
|
||
|
|
|
||
|
|
local val = r.fcount > 0 and r.forecast[ r.fcount ].v or r.actual
|
||
|
|
|
||
|
|
local v = max( 0, min( r.max, val + bonus ) )
|
||
|
|
v = max( 0, min( r.max, v + ( type( e.value ) == "number" and e.value or e.value( now ) ) ) )
|
||
|
|
|
||
|
|
local idx
|
||
|
|
|
||
|
|
if r.forecast[ r.fcount ].t == now then
|
||
|
|
-- Reuse the last one.
|
||
|
|
idx = r.fcount
|
||
|
|
else
|
||
|
|
idx = r.fcount + 1
|
||
|
|
end
|
||
|
|
|
||
|
|
r.forecast[ idx ] = r.forecast[ idx ] or {}
|
||
|
|
r.forecast[ idx ].t = now
|
||
|
|
r.forecast[ idx ].v = v
|
||
|
|
r.forecast[ idx ].e = e.name or "none"
|
||
|
|
r.fcount = idx
|
||
|
|
|
||
|
|
-- interval() takes the last tick and the current value to remember the next step.
|
||
|
|
local step = roundDown( type( e.interval ) == "number" and e.interval or ( type( e.interval ) == "function" and e.interval( now, v ) or ( type( e.interval ) == "string" and state[ e.interval ] or 0 ) ), 3 )
|
||
|
|
|
||
|
|
remains[ e.resource ] = finish - e.next
|
||
|
|
e.next = e.next + step
|
||
|
|
|
||
|
|
if e.next > finish or step < 0 or ( e.aura and state[ e.debuff and "debuff" or "buff" ][ e.aura ].expires < e.next ) or ( e.channel and state.buff.casting.expires < e.next ) then
|
||
|
|
table.remove( events, 1 )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if #events > 1 then sort( events, resourceModelSort ) end
|
||
|
|
end
|
||
|
|
|
||
|
|
if r.regen > 0 and r.forecast[ r.fcount ].v < r.max then
|
||
|
|
for k, v in pairs( remains ) do
|
||
|
|
local r = state[ k ]
|
||
|
|
local val = r.fcount > 0 and r.forecast[ r.fcount ].v or r.actual
|
||
|
|
local idx = r.fcount + 1
|
||
|
|
|
||
|
|
r.forecast[ idx ] = r.forecast[ idx ] or {}
|
||
|
|
r.forecast[ idx ].t = finish
|
||
|
|
r.forecast[ idx ].v = min( r.max, val + ( v * r.regen ) )
|
||
|
|
r.fcount = idx
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
ns.forecastResources = forecastResources
|
||
|
|
state.forecastResources = forecastResources
|
||
|
|
|
||
|
|
Hekili:ProfileCPU( "forecastResources", forecastResources )
|
||
|
|
|
||
|
|
|
||
|
|
local resourceChange = function( amount, resource, overcap )
|
||
|
|
if amount == 0 then return false end
|
||
|
|
|
||
|
|
local r = state[ resource ]
|
||
|
|
local pre = r.current
|
||
|
|
|
||
|
|
if amount < 0 and r.spend then r.spend( -amount, resource, overcap )
|
||
|
|
elseif amount > 0 and r.gain then r.gain( amount, resource, overcap )
|
||
|
|
else
|
||
|
|
r.actual = max( 0, r.current + amount )
|
||
|
|
if not overcap then r.actual = min( r.max, r.actual ) end
|
||
|
|
end
|
||
|
|
|
||
|
|
return true
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
-- Noteworthy hooks for gain/spend:
|
||
|
|
-- pregain - the hook is expected to return modified values for the resource (i.e., special cost reduction or refunds).
|
||
|
|
-- gain - the hook can do whatever it wants, but if it changes the same resource again it will cause another forecast.
|
||
|
|
|
||
|
|
local gain = function( amount, resource, overcap )
|
||
|
|
amount, resource, overcap = ns.callHook( "pregain", amount, resource, overcap )
|
||
|
|
resourceChange( amount, resource, overcap )
|
||
|
|
if resource ~= "health" then forecastResources( resource ) end
|
||
|
|
ns.callHook( "gain", amount, resource, overcap )
|
||
|
|
end
|
||
|
|
|
||
|
|
local rawGain = function( amount, resource, overcap )
|
||
|
|
resourceChange( amount, resource, overcap )
|
||
|
|
forecastResources( resource )
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
local spend = function( amount, resource, clean )
|
||
|
|
amount, resource = ns.callHook( "prespend", amount, resource )
|
||
|
|
resourceChange( -amount, resource, overcap )
|
||
|
|
if resource ~= "health" then forecastResources( resource ) end
|
||
|
|
ns.callHook( "spend", amount, resource, overcap, true )
|
||
|
|
end
|
||
|
|
|
||
|
|
local rawSpend = function( amount, resource )
|
||
|
|
resourceChange( -amount, resource, overcap )
|
||
|
|
forecastResources( resource )
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
state.gain = gain
|
||
|
|
state.rawGain = rawGain
|
||
|
|
|
||
|
|
state.spend = spend
|
||
|
|
state.rawSpend = rawSpend
|
||
|
|
|
||
|
|
|
||
|
|
do
|
||
|
|
-- Rechecking System
|
||
|
|
-- Setup on a per-ability basis, this gives the prediction engine a head's up that the ability may become ready in a short time.
|
||
|
|
|
||
|
|
local workTable = {}
|
||
|
|
state.recheckTimes = {}
|
||
|
|
|
||
|
|
local function recheckHelper( t, ... )
|
||
|
|
local n = select( "#", ... )
|
||
|
|
|
||
|
|
for i = 1, n do
|
||
|
|
local x = select( i, ... )
|
||
|
|
if type( x ) == "number" then
|
||
|
|
if x > 0 and x >= state.delayMin and x <= state.delayMax then
|
||
|
|
t[ x ] = true
|
||
|
|
elseif x < 60 then
|
||
|
|
Hekili:Debug( "Excluded %.2f recheck time as it is outside our constraints ( %.2f - %.2f ).", x, state.delayMin or -1, state.delayMax or -1 )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
local function channelInfo( ability )
|
||
|
|
if state.system.packName and scripts.Channels[ state.system.packName ] then
|
||
|
|
return scripts.Channels[ state.system.packName ][ state.channel ], class.auras[ state.channel ]
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state.recheck( ability, script, stack, block )
|
||
|
|
local times = state.recheckTimes
|
||
|
|
wipe( workTable )
|
||
|
|
|
||
|
|
-- local debug = Hekili.ActiveDebug
|
||
|
|
-- local steps = {}
|
||
|
|
|
||
|
|
if script then
|
||
|
|
if script.Recheck then
|
||
|
|
recheckHelper( workTable, script.Recheck() )
|
||
|
|
end
|
||
|
|
|
||
|
|
-- This can be CPU intensive but is needed for some APLs (i.e., Unholy).
|
||
|
|
if script.Variables and state.settings.enhancedRecheck then
|
||
|
|
-- if Hekili.ActiveDebug then table.insert( steps, debugprofilestop() ) end
|
||
|
|
for i, var in ipairs( script.Variables ) do
|
||
|
|
local varIDs = state:GetVariableIDs( var )
|
||
|
|
|
||
|
|
if varIDs then
|
||
|
|
for _, entry in ipairs( varIDs ) do
|
||
|
|
local vr = scripts.DB[ entry.id ].VarRecheck
|
||
|
|
if vr then
|
||
|
|
recheckHelper( workTable, vr() )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
-- if Hekili.ActiveDebug then table.insert( steps, debugprofilestop() ) end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
-- if Hekili.ActiveDebug then table.insert( steps, debugprofilestop() ) end
|
||
|
|
|
||
|
|
local data = class.abilities[ ability ]
|
||
|
|
if data and data.aura then
|
||
|
|
local a = state.buff[ data.aura ]
|
||
|
|
if a and a.up then
|
||
|
|
recheckHelper( workTable, a.remains )
|
||
|
|
end
|
||
|
|
|
||
|
|
a = state.debuff[ data.aura ]
|
||
|
|
if a and a.up then
|
||
|
|
recheckHelper( workTable, a.remains )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
-- if Hekili.ActiveDebug then table.insert( steps, debugprofilestop() ) end
|
||
|
|
|
||
|
|
if stack and #stack > 0 then
|
||
|
|
for i, caller in ipairs( stack ) do
|
||
|
|
local callScript = caller.script
|
||
|
|
callScript = callScript and scripts:GetScript( callScript )
|
||
|
|
|
||
|
|
if callScript and callScript.Recheck then
|
||
|
|
recheckHelper( workTable, callScript.Recheck() )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if block and #block > 0 then
|
||
|
|
for i, caller in ipairs( block ) do
|
||
|
|
local callScript = caller.script
|
||
|
|
callScript = callScript and scripts:GetScript( callScript )
|
||
|
|
|
||
|
|
if callScript and callScript.Recheck then
|
||
|
|
recheckHelper( workTable, callScript.Recheck() )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
-- if Hekili.ActiveDebug then table.insert( steps, debugprofilestop() ) end
|
||
|
|
|
||
|
|
--[[ if state.channeling then
|
||
|
|
local aura = class.auras[ state.channel ]
|
||
|
|
local remains = state.channel_remains
|
||
|
|
|
||
|
|
if aura and aura.tick_time then
|
||
|
|
-- Put tick times into recheck.
|
||
|
|
local i = 1
|
||
|
|
while ( true ) do
|
||
|
|
if remains - ( i * aura.tick_time ) > 0 then
|
||
|
|
workTable[ roundUp( remains - ( i * aura.tick_time ), 2 ) ] = true
|
||
|
|
else break end
|
||
|
|
i = i + 1
|
||
|
|
end
|
||
|
|
|
||
|
|
for time in pairs( workTable ) do
|
||
|
|
if ( ( remains - time ) / aura.tick_time ) % 1 <= 0.5 then
|
||
|
|
workTable[ time ] = nil
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
workTable[ remains ] = true
|
||
|
|
end ]]
|
||
|
|
|
||
|
|
--[[ if Hekili.ActiveDebug and #steps > 0 then
|
||
|
|
-- table.insert( steps, debugprofilestop() )
|
||
|
|
local str = string.format( "RECHECK: %.2f", steps[#steps] - steps[1] )
|
||
|
|
|
||
|
|
for i = 2, #steps do
|
||
|
|
str = string.format( "%s, %.2f ", str, steps[i] - steps[i-1] )
|
||
|
|
end
|
||
|
|
|
||
|
|
Hekili:Debug( str )
|
||
|
|
end ]]
|
||
|
|
|
||
|
|
wipe( times )
|
||
|
|
|
||
|
|
for k, v in pairs( workTable ) do
|
||
|
|
if Hekili.ActiveDebug then Hekili:Debug( "%s - %s", tostring( k ), tostring( v ) ) end
|
||
|
|
times[ #times + 1 ] = k
|
||
|
|
end
|
||
|
|
|
||
|
|
sort( times )
|
||
|
|
|
||
|
|
if Hekili.ActiveDebug then
|
||
|
|
if #times > 0 then
|
||
|
|
local o
|
||
|
|
for i, time in ipairs( times ) do
|
||
|
|
o = string.format( "%s - %.2f", o or "", time )
|
||
|
|
end
|
||
|
|
-- Hekili:Debug( "Recheck times for this entry are: %s\n%s [ %.2f - %.2f ]", scripts:GetConditionsAndValues( script.ID, nil, nil, true ), o, state.delayMin, state.delayMax )
|
||
|
|
-- else
|
||
|
|
-- Hekili:Debug( "Recheck times for this entry are: %s", scripts:GetConditionsAndValues( script.ID, nil, nil, true ) )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
--------------------------------------
|
||
|
|
-- UGLY METATABLES BELOW THIS POINT --
|
||
|
|
--------------------------------------
|
||
|
|
ns.metatables = {}
|
||
|
|
local metafunctions = {
|
||
|
|
action = {},
|
||
|
|
active_dot = {},
|
||
|
|
buff = {},
|
||
|
|
cooldown = {},
|
||
|
|
debuff = {},
|
||
|
|
default_action = {},
|
||
|
|
default_aura = {},
|
||
|
|
default_cooldown = {},
|
||
|
|
default_debuff = {},
|
||
|
|
default_pet = {},
|
||
|
|
default_totem = {},
|
||
|
|
perk = {},
|
||
|
|
pet = {},
|
||
|
|
resource = {},
|
||
|
|
set_bonus = {},
|
||
|
|
settings = {},
|
||
|
|
spec = {},
|
||
|
|
stance = {},
|
||
|
|
stat = {},
|
||
|
|
state = {},
|
||
|
|
talent = {},
|
||
|
|
target = {},
|
||
|
|
target_health = {},
|
||
|
|
toggle = {},
|
||
|
|
totem = {},
|
||
|
|
}
|
||
|
|
|
||
|
|
ns.addMetaFunction = function( t, k, func )
|
||
|
|
|
||
|
|
if metafunctions[ t ] then
|
||
|
|
metafunctions[ t ][ k ] = setfenv( func, state )
|
||
|
|
return
|
||
|
|
end
|
||
|
|
|
||
|
|
Error( "addMetaFunction() - no such table '" .. t .. "' for key '" .. k .. "'." )
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
-- Returns false instead of nil when a key is not found.
|
||
|
|
local mt_false = {
|
||
|
|
__index = function(t, k)
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_false = mt_false
|
||
|
|
|
||
|
|
|
||
|
|
do
|
||
|
|
local a = class.knownAuraAttributes
|
||
|
|
|
||
|
|
-- Populate table of known aura attributes so we know if we should bother looking in buffs/debuffs for this information.
|
||
|
|
|
||
|
|
a.applied = true
|
||
|
|
a.caster = true
|
||
|
|
a.cooldown_remains = true
|
||
|
|
a.count = true
|
||
|
|
a.down = true
|
||
|
|
a.duration = true
|
||
|
|
a.expires = true
|
||
|
|
a.i_up = true
|
||
|
|
a.id = true
|
||
|
|
a.key = true
|
||
|
|
a.lastApplied = true
|
||
|
|
a.lastCount = true
|
||
|
|
a.last_application = true
|
||
|
|
a.last_expiry = true
|
||
|
|
a.max_stack = true
|
||
|
|
a.max_stacks = true
|
||
|
|
a.mine = true
|
||
|
|
a.name = true
|
||
|
|
a.rank = true
|
||
|
|
a.react = true
|
||
|
|
a.refreshable = true
|
||
|
|
a.remains = true
|
||
|
|
a.stack = true
|
||
|
|
a.stack_pct = true
|
||
|
|
a.stacks = true
|
||
|
|
a.tick_time_remains = true
|
||
|
|
a.ticking = true
|
||
|
|
a.ticks = true
|
||
|
|
a.ticks_remain = true
|
||
|
|
a.time_to_refresh = true
|
||
|
|
a.timeMod = true
|
||
|
|
a.unit = true
|
||
|
|
a.up = true
|
||
|
|
a.v1 = true
|
||
|
|
a.v2 = true
|
||
|
|
a.v3 = true
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
-- Gives calculated values for some state options in order to emulate SimC syntax.
|
||
|
|
local mt_state = {
|
||
|
|
__index = function( t, k )
|
||
|
|
|
||
|
|
if metafunctions.state[ k ] then
|
||
|
|
return metafunctions.state[ k ]()
|
||
|
|
|
||
|
|
elseif class.stateExprs[ k ] then
|
||
|
|
return class.stateExprs[ k ]()
|
||
|
|
|
||
|
|
elseif k == "display" then
|
||
|
|
return "Primary"
|
||
|
|
|
||
|
|
elseif k == "scriptID" then
|
||
|
|
return "NilScriptID"
|
||
|
|
|
||
|
|
elseif k == "resetting" then
|
||
|
|
return false
|
||
|
|
|
||
|
|
-- First, any values that don't reference an ability or aura.
|
||
|
|
elseif k == "this_action" then
|
||
|
|
return "wait"
|
||
|
|
|
||
|
|
elseif k == "current_action" then
|
||
|
|
return t.this_action
|
||
|
|
|
||
|
|
elseif k == "cast_target" then
|
||
|
|
return "nobody"
|
||
|
|
|
||
|
|
elseif k == 'canBreakChannel' then
|
||
|
|
return false
|
||
|
|
|
||
|
|
elseif k == "delay" then
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "whitelist" then
|
||
|
|
return nil
|
||
|
|
|
||
|
|
elseif k == "selection" then
|
||
|
|
return t.selection_time < 60
|
||
|
|
|
||
|
|
elseif k == "selection_time" then
|
||
|
|
return 60
|
||
|
|
|
||
|
|
elseif k == "desired_targets" then
|
||
|
|
return 1
|
||
|
|
|
||
|
|
elseif k == 'inEncounter' or k == "encounter" then
|
||
|
|
return t.encounterID > 0
|
||
|
|
|
||
|
|
elseif k == "torghast" then
|
||
|
|
return false
|
||
|
|
|
||
|
|
elseif k == "mounted" or k == "is_mounted" then
|
||
|
|
return IsMounted()
|
||
|
|
|
||
|
|
elseif k == "boss" then
|
||
|
|
return ( t.encounterID > 0 or ( UnitCanAttack( "player", "target" ) and ( UnitClassification( "target" ) == "worldboss" or UnitLevel( "target" ) == -1 ) ) ) == true
|
||
|
|
|
||
|
|
elseif k == "cycle" then
|
||
|
|
return false
|
||
|
|
|
||
|
|
elseif k == "hardcast" then
|
||
|
|
return false -- will set to true if/when a spell is hardcast.
|
||
|
|
|
||
|
|
elseif k == "channeling" then
|
||
|
|
return t.buff.casting.up and t.buff.casting.v3
|
||
|
|
|
||
|
|
elseif k == "channel" then
|
||
|
|
if t.buff.casting.down or not t.buff.casting.v3 then return nil end
|
||
|
|
local chan = class.abilities[ t.buff.casting.v1 ]
|
||
|
|
|
||
|
|
if chan then return chan.key end
|
||
|
|
return tostring( t.buff.casting.v1 )
|
||
|
|
|
||
|
|
elseif k == "channel_remains" then
|
||
|
|
return t.buff.casting.up and t.buff.casting.v3 and t.buff.casting.remains or 0
|
||
|
|
|
||
|
|
elseif k == "ranged" then
|
||
|
|
return false
|
||
|
|
|
||
|
|
elseif k == "wait_for_gcd" then
|
||
|
|
-- For specs that have to weave a lot of off GCD stuff.
|
||
|
|
-- i.e., Frost DK.
|
||
|
|
return false
|
||
|
|
|
||
|
|
elseif k == "query_time" then
|
||
|
|
return t.now + t.offset + t.delay
|
||
|
|
|
||
|
|
elseif k == "time_to_die" or k == "fight_remains" or k == "interpolated_fight_remains" then
|
||
|
|
-- if not t.boss then return 3600 end
|
||
|
|
return max( 1, Hekili:GetGreatestTTD() - ( t.offset + t.delay ) )
|
||
|
|
|
||
|
|
elseif k:sub(1, 12) == "time_to_pct_" then
|
||
|
|
local percent = tonumber( k:sub( 13 ) ) or 0
|
||
|
|
return Hekili:GetGreatestTimeToPct( percent ) - ( t.offset + t.delay )
|
||
|
|
|
||
|
|
elseif k == "shortest_ttd" then
|
||
|
|
return Hekili:GetLowestTTD()
|
||
|
|
|
||
|
|
elseif k == "longest_ttd" then
|
||
|
|
return Hekili:GetGreatestTTD()
|
||
|
|
|
||
|
|
elseif k == "expected_combat_length" then
|
||
|
|
if not t.boss then return 3600 end
|
||
|
|
return Hekili:GetGreatestTTD() + t.time -- + t.offset + t.delay
|
||
|
|
|
||
|
|
elseif k == "moving" then
|
||
|
|
return ( GetUnitSpeed("player") > 0 )
|
||
|
|
|
||
|
|
elseif k == "solo" then
|
||
|
|
return GetNumGroupMembers() == 0
|
||
|
|
|
||
|
|
elseif k == "group" then
|
||
|
|
return GetNumGroupMembers() > 1
|
||
|
|
|
||
|
|
elseif k == "group_members" then
|
||
|
|
return max( 1, GetNumGroupMembers() )
|
||
|
|
|
||
|
|
elseif k == "raid" then
|
||
|
|
return IsInRaid() and t.group_members > 5
|
||
|
|
|
||
|
|
elseif k == "level" then
|
||
|
|
return UnitEffectiveLevel("player") or MAX_PLAYER_LEVEL
|
||
|
|
|
||
|
|
elseif k == "active" then
|
||
|
|
return false
|
||
|
|
|
||
|
|
elseif k == "active_enemies" then
|
||
|
|
t[k] = ns.getNumberTargets()
|
||
|
|
|
||
|
|
if t.min_targets > 0 then t[k] = max( t.min_targets, t[k] ) end
|
||
|
|
if t.max_targets > 0 then t[k] = min( t.max_targets, t[k] ) end
|
||
|
|
|
||
|
|
t[k] = max( 1, t[k] )
|
||
|
|
|
||
|
|
return t[k]
|
||
|
|
|
||
|
|
elseif k == "my_enemies" then
|
||
|
|
-- The above is not needed as the nameplate target system will add missing enemies.
|
||
|
|
t[k] = ns.numTargets()
|
||
|
|
|
||
|
|
if t.min_targets > 0 then t[k] = max( t.min_targets, t[k] ) end
|
||
|
|
if t.max_targets > 0 then t[k] = min( t.max_targets, t[k] ) end
|
||
|
|
|
||
|
|
t[k] = max( 1, t[k] )
|
||
|
|
|
||
|
|
return t[k]
|
||
|
|
|
||
|
|
elseif k == "cycle_enemies" then
|
||
|
|
if not t.settings.cycle then return 1 end
|
||
|
|
|
||
|
|
local targets = t.active_enemies
|
||
|
|
local timeframe = t.delay + t.offset
|
||
|
|
|
||
|
|
local minTTD = timeframe + min( t.cycleInfo.minTTD or 10, t.settings.cycle_min )
|
||
|
|
local maxTTD = t.cycleInfo.maxTTD
|
||
|
|
|
||
|
|
if not t.HasCyclingDebuff() and t.settings.cycleDebuff then
|
||
|
|
-- See if the specialization has a default aura to use for cycling (i.e., Unholy using Festering Wound).
|
||
|
|
minTTD = max( minTTD, t.debuff[ t.settings.cycleDebuff ].duration / 2 )
|
||
|
|
end
|
||
|
|
|
||
|
|
targets = targets - Hekili:GetNumTTDsBefore( minTTD )
|
||
|
|
|
||
|
|
if maxTTD then
|
||
|
|
targets = targets - Hekili:GetNumTTDsAfter( maxTTD )
|
||
|
|
end
|
||
|
|
|
||
|
|
-- So the reason we're stuck here is that we may need "cycle_enemies" when we *aren't* cycling targets.
|
||
|
|
-- I.e., we would cycle Festering Strike (festering_wound) but if we've already dotted our valid adds, we'd hit Death and Decay.
|
||
|
|
|
||
|
|
if t.min_targets > 0 then targets = max( t.min_targets, targets ) end
|
||
|
|
if t.max_targets > 0 then targets = min( t.max_targets, targets ) end
|
||
|
|
|
||
|
|
if Hekili.ActiveDebug then Hekili:Debug( "cycle min:%.2f, max:%.2f, ae:%d, before:%d, after:%d, cycle_enemies:%d", minTTD or 0, maxTTD or 0, t.active_enemies, minTTD and Hekili:GetNumTTDsBefore( minTTD ) or 0, maxTTD and Hekili:GetNumTTDsAfter( maxTTD ) or 0, max( 1, targets ) ) end
|
||
|
|
|
||
|
|
return max( 1, targets )
|
||
|
|
|
||
|
|
elseif k == "true_active_enemies" then
|
||
|
|
t[k] = max( 1, ns.getNumberTargets() )
|
||
|
|
return t[k]
|
||
|
|
|
||
|
|
elseif k == "true_my_enemies" then
|
||
|
|
t[k] = max( 1, ns.numTargets() )
|
||
|
|
return t[k]
|
||
|
|
|
||
|
|
elseif k == "crit" or k == "spell_crit" or k == "attack_crit" then
|
||
|
|
return ( t.stat.crit / 100 )
|
||
|
|
|
||
|
|
elseif k == "haste" or k == "spell_haste" then
|
||
|
|
return ( 1 / ( 1 + t.stat.spell_haste ) )
|
||
|
|
|
||
|
|
elseif k == "melee_haste" then
|
||
|
|
return ( 1 / ( 1 + t.stat.melee_haste ) )
|
||
|
|
|
||
|
|
elseif k == "mastery_value" then
|
||
|
|
return ( GetMasteryEffect() / 100 )
|
||
|
|
|
||
|
|
elseif k == "miss_react" then
|
||
|
|
return false
|
||
|
|
|
||
|
|
elseif k == "cooldown_react" or k == "cooldown_up" then
|
||
|
|
return t.cooldown[ t.this_action ].remains == 0
|
||
|
|
|
||
|
|
elseif k == "cast_delay" then return 0
|
||
|
|
|
||
|
|
elseif k == "in_flight" then
|
||
|
|
local data = t.action[ t.this_action ]
|
||
|
|
if data then return data.in_flight end
|
||
|
|
return false
|
||
|
|
|
||
|
|
elseif k == "in_flight_remains" then
|
||
|
|
local data = t.action[ t.this_action ]
|
||
|
|
if data then return data.in_flight.remains end
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "executing" then
|
||
|
|
return state:IsCasting( t.this_action ) or ( state.prev[1][ t.this_action ] and state.gcd.remains > 0 )
|
||
|
|
|
||
|
|
elseif k == "execute_remains" then
|
||
|
|
return ( state:IsCasting( t.this_action ) and max( state:QueuedCastRemains( t.this_action ), state.gcd.remains ) ) or ( state.prev[1][ t.this_action ] and state.gcd.remains ) or 0
|
||
|
|
|
||
|
|
elseif k == "prowling" then
|
||
|
|
return t.buff.prowl.up or ( t.buff.cat_form.up and t.buff.shadowform.up )
|
||
|
|
|
||
|
|
elseif type(k) == "string" and k:sub(1, 16) == "incoming_damage_" then
|
||
|
|
local remains = k:sub(17)
|
||
|
|
local time = remains:match("^(%d+)[m]?s")
|
||
|
|
|
||
|
|
if not time then
|
||
|
|
return 0
|
||
|
|
-- Error("ERR: " .. remains )
|
||
|
|
end
|
||
|
|
|
||
|
|
time = tonumber( time )
|
||
|
|
|
||
|
|
if time > 100 then
|
||
|
|
t[k] = ns.damageInLast( time / 1000 )
|
||
|
|
else
|
||
|
|
t[k] = ns.damageInLast( min( 15, time ) )
|
||
|
|
end
|
||
|
|
|
||
|
|
table.insert( t.purge, k )
|
||
|
|
return t[ k ]
|
||
|
|
|
||
|
|
elseif type(k) == "string" and k:sub(1, 18) == "incoming_physical_" then
|
||
|
|
local remains = k:sub(19)
|
||
|
|
local time = remains:match("^(%d+)[m]?s")
|
||
|
|
|
||
|
|
if not time then
|
||
|
|
return 0
|
||
|
|
-- Error("ERR: " .. remains )
|
||
|
|
end
|
||
|
|
|
||
|
|
time = tonumber( time )
|
||
|
|
|
||
|
|
if time > 100 then
|
||
|
|
t[k] = ns.damageInLast( time / 1000, true )
|
||
|
|
else
|
||
|
|
t[k] = ns.damageInLast( min( 15, time ), true )
|
||
|
|
end
|
||
|
|
|
||
|
|
table.insert( t.purge, k )
|
||
|
|
return t[ k ]
|
||
|
|
|
||
|
|
elseif type(k) == "string" and k:sub(1, 15) == "incoming_magic_" then
|
||
|
|
local remains = k:sub(16)
|
||
|
|
local time = remains:match("^(%d+)[m]?s")
|
||
|
|
|
||
|
|
if not time then
|
||
|
|
return 0
|
||
|
|
-- Error("ERR: " .. remains )
|
||
|
|
end
|
||
|
|
|
||
|
|
time = tonumber( time )
|
||
|
|
|
||
|
|
if time > 100 then
|
||
|
|
t[k] = ns.damageInLast( time / 1000, false )
|
||
|
|
else
|
||
|
|
t[k] = ns.damageInLast( min( 15, time ), false )
|
||
|
|
end
|
||
|
|
|
||
|
|
table.insert( t.purge, k )
|
||
|
|
return t[ k ]
|
||
|
|
|
||
|
|
elseif type(k) == "string" and k:sub(1, 14) == "incoming_heal_" then
|
||
|
|
local remains = k:sub(15)
|
||
|
|
local time = remains:match("^(%d+)[m]?s")
|
||
|
|
|
||
|
|
if not time then
|
||
|
|
return 0
|
||
|
|
-- Error("ERR: " .. remains)
|
||
|
|
end
|
||
|
|
|
||
|
|
time = tonumber( time )
|
||
|
|
|
||
|
|
if time > 100 then
|
||
|
|
t[ k ] = ns.healingInLast( time / 1000 )
|
||
|
|
else
|
||
|
|
t[ k ] = ns.healingInLast( min( 15, time ) )
|
||
|
|
end
|
||
|
|
|
||
|
|
table.insert( t.purge, k )
|
||
|
|
return t[ k ]
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
-- The next block are values that reference an ability.
|
||
|
|
local action = t.this_action
|
||
|
|
local ability = class.abilities[ action ]
|
||
|
|
|
||
|
|
if k == "time" then
|
||
|
|
-- Calculate time in combat.
|
||
|
|
if t.combat == 0 and t.false_start == 0 then return 0 end
|
||
|
|
|
||
|
|
local start = t.combat > 0 and t.combat or ( t.false_start > 0 and t.false_start or t.query_time )
|
||
|
|
return t.query_time - start
|
||
|
|
|
||
|
|
elseif k == "cast_time" then
|
||
|
|
return ability and ability.cast or 0
|
||
|
|
|
||
|
|
elseif k == "execute_time" then
|
||
|
|
return max( state.gcd.execute, ability and ability.cast or 0 )
|
||
|
|
|
||
|
|
elseif k == "travel_time" then
|
||
|
|
local v = ability.velocity or 0
|
||
|
|
if v > 0 then
|
||
|
|
return t.target.maxR / v
|
||
|
|
end
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "action_cooldown" then
|
||
|
|
return ability and ability.cooldown or 0
|
||
|
|
|
||
|
|
elseif k == "charges" then
|
||
|
|
return t.cooldown[ action ].charges
|
||
|
|
|
||
|
|
elseif k == "charges_fractional" then
|
||
|
|
return t.cooldown[ action ].charges_fractional
|
||
|
|
|
||
|
|
elseif k == "time_to_max_charges" or k == "full_recharge_time" then
|
||
|
|
return ( ( ability.charges or 1 ) - t.charges_fractional ) * ( ability.recharge or ability.cooldown )
|
||
|
|
|
||
|
|
elseif k == "max_charges" or k == "charges_max" then
|
||
|
|
return ability and ability.charges or 1
|
||
|
|
|
||
|
|
elseif k == "recharge" then
|
||
|
|
-- TODO: Recheck what value SimC would use for recharge if an ability doesn't have charges.
|
||
|
|
return t.cooldown[ action ].recharge
|
||
|
|
|
||
|
|
elseif k == "recharge_time" then
|
||
|
|
-- TODO: Recheck what value SimC would use for recharge if an ability doesn't have charges.
|
||
|
|
return t.cooldown[ action ].recharge_time
|
||
|
|
|
||
|
|
elseif k == "cost" then
|
||
|
|
if ability then
|
||
|
|
local c = ability.cost
|
||
|
|
|
||
|
|
if c then return c end
|
||
|
|
|
||
|
|
c = ability.spend
|
||
|
|
|
||
|
|
if c and c > 0 and c < 1 then
|
||
|
|
c = c * state[ ability.spendType or class.primaryResource ].modmax
|
||
|
|
end
|
||
|
|
|
||
|
|
return c or 0
|
||
|
|
end
|
||
|
|
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "cast_regen" then
|
||
|
|
return ( max( state.gcd.execute, ability.cast or 0 ) * state[ ability.spendType or class.primaryResource ].regen ) - ( ability and ability.spend or 0 )
|
||
|
|
|
||
|
|
elseif k == "crit_pct_current" or k == "crit_percent_current" then
|
||
|
|
-- This is the crit % of the current ability.
|
||
|
|
-- Pulse from the ability's "critical" value or uses current character sheet crit.
|
||
|
|
return ability and ability.critical or t.stat.crit
|
||
|
|
|
||
|
|
elseif k == "in_range" then
|
||
|
|
return t.action[ action ].in_range
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
if class.knownAuraAttributes[ k ] then
|
||
|
|
-- Buffs, debuffs...
|
||
|
|
local aura_name = ability and ability.aura or t.this_action
|
||
|
|
local aura = class.auras[ aura_name ]
|
||
|
|
|
||
|
|
local app = aura and ( t.buff[ aura_name ].up and t.buff[ aura_name ] ) or ( t.debuff[ aura_name ].up and t.debuff[ aura_name ] ) or nil
|
||
|
|
|
||
|
|
-- This uses the default aura duration (if available) to keep pandemic windows accurate.
|
||
|
|
local duration = aura and aura.duration or 15
|
||
|
|
|
||
|
|
-- This allows for overridden tick times on a particular application of an aura (i.e., Exsanguinate).
|
||
|
|
local tick_time = app and app.tick_time or ( aura and aura.tick_time ) or ( 3 * t.haste )
|
||
|
|
|
||
|
|
if k == "duration" then
|
||
|
|
return duration
|
||
|
|
|
||
|
|
elseif k == "refreshable" then
|
||
|
|
-- When cycling targets, we want to consider that there may be a valid other target.
|
||
|
|
-- if t.isCyclingTargets( action, aura_name ) then return true end
|
||
|
|
if app then return app.remains < 0.3 * duration end
|
||
|
|
return true
|
||
|
|
|
||
|
|
elseif k == "time_to_refresh" then
|
||
|
|
-- if t.isCyclingTargets( action, aura_name ) then return 0 end
|
||
|
|
if app then return max( 0, 0.01 + app.remains - ( 0.3 * app.duration ) ) end
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "ticking" or k == "up" then
|
||
|
|
if app then return app.up end
|
||
|
|
return false
|
||
|
|
|
||
|
|
elseif k == "down" then
|
||
|
|
if app then return app.down end
|
||
|
|
return true
|
||
|
|
|
||
|
|
elseif k == "ticks" then
|
||
|
|
if app then return app.ticks end
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "ticks_remain" then
|
||
|
|
if app then return app.ticks_remain end
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "tick_time_remains" then
|
||
|
|
if app then return ( app.remains % tick_time ) end
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "remains" then
|
||
|
|
if app then return app.remains end
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "tick_time" then
|
||
|
|
if app then return tick_time end
|
||
|
|
return 0
|
||
|
|
|
||
|
|
else
|
||
|
|
if app and app[ k ] ~= nil then return app[ k ] end
|
||
|
|
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
-- Check if this is a resource table pre-init.
|
||
|
|
for i, key in pairs( class.resources ) do
|
||
|
|
if k == key then
|
||
|
|
return nil
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
if state:GetVariableIDs( k ) then return t.variable[ k ] end
|
||
|
|
|
||
|
|
if t.settings[ k ] ~= nil then return t.settings[ k ] end
|
||
|
|
if t.toggle[ k ] ~= nil then return t.toggle[ k ] end
|
||
|
|
|
||
|
|
Hekili:Error( "Returned unknown string '" .. k .. "' in state metatable [" .. state.scriptID .. "]." )
|
||
|
|
return nil
|
||
|
|
end,
|
||
|
|
__newindex = function(t, k, v)
|
||
|
|
rawset(t, k, v)
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_state = mt_state
|
||
|
|
|
||
|
|
|
||
|
|
local mt_spec = {
|
||
|
|
__index = function(t, k)
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_spec = mt_spec
|
||
|
|
|
||
|
|
|
||
|
|
local mt_stat = {
|
||
|
|
__index = function(t, k)
|
||
|
|
if k == "strength" then
|
||
|
|
return UnitStat("player", 1)
|
||
|
|
|
||
|
|
elseif k == "agility" then
|
||
|
|
return UnitStat("player", 2)
|
||
|
|
|
||
|
|
elseif k == "stamina" then
|
||
|
|
return UnitStat("player", 3)
|
||
|
|
|
||
|
|
elseif k == "intellect" then
|
||
|
|
return UnitStat("player", 4)
|
||
|
|
|
||
|
|
elseif k == "spirit" then
|
||
|
|
return UnitStat("player", 5)
|
||
|
|
|
||
|
|
elseif k == "health" then
|
||
|
|
return UnitHealth("player")
|
||
|
|
|
||
|
|
elseif k == "maximum_health" then
|
||
|
|
return UnitHealthMax("player")
|
||
|
|
|
||
|
|
elseif k == "health_pct" then
|
||
|
|
return UnitHealth( "player" ) / UnitHealthMax( "player" ) * 100
|
||
|
|
|
||
|
|
elseif k == "mana" then
|
||
|
|
return Hekili.State.mana and Hekili.State.mana.current or 0
|
||
|
|
|
||
|
|
elseif k == "maximum_mana" then
|
||
|
|
return Hekili.State.mana and Hekili.State.mana.max or 0
|
||
|
|
|
||
|
|
elseif k == "rage" then
|
||
|
|
return Hekili.State.rage and Hekili.State.rage.current or 0
|
||
|
|
|
||
|
|
elseif k == "maximum_rage" then
|
||
|
|
return Hekili.State.rage and Hekili.State.rage.max or 0
|
||
|
|
|
||
|
|
elseif k == "energy" then
|
||
|
|
return Hekili.State.energy and Hekili.State.energy.current or 0
|
||
|
|
|
||
|
|
elseif k == "maximum_energy" then
|
||
|
|
return Hekili.State.energy and Hekili.State.energy.max or 0
|
||
|
|
|
||
|
|
elseif k == "focus" then
|
||
|
|
return Hekili.State.focus and Hekili.State.focus.current or 0
|
||
|
|
|
||
|
|
elseif k == "maximum_focus" then
|
||
|
|
return Hekili.State.focus and Hekili.State.focus.max or 0
|
||
|
|
|
||
|
|
elseif k == "runic" or k == "runic_power" then
|
||
|
|
return Hekili.State.runic_power and Hekili.State.runic_power.current or 0
|
||
|
|
|
||
|
|
elseif k == "maximum_runic" or k == "maximum_runic_power" then
|
||
|
|
return Hekili.State.runic_power and Hekili.State.runic_power.max or 0
|
||
|
|
|
||
|
|
elseif k == "spell_power" then
|
||
|
|
return GetSpellBonusDamage(7)
|
||
|
|
|
||
|
|
elseif k == "mp5" then
|
||
|
|
return t.mana and Hekili.State.mana.regen or 0
|
||
|
|
|
||
|
|
elseif k == "attack_power" then
|
||
|
|
return UnitAttackPower("player") + UnitWeaponAttackPower("player")
|
||
|
|
|
||
|
|
elseif k == "crit_rating" then
|
||
|
|
return GetCombatRating(CR_CRIT_MELEE)
|
||
|
|
|
||
|
|
elseif k == "haste_rating" then
|
||
|
|
return GetCombatRating(CR_HASTE_MELEE)
|
||
|
|
|
||
|
|
elseif k == "weapon_dps" then
|
||
|
|
return -- Error("NYI")
|
||
|
|
|
||
|
|
elseif k == "weapon_speed" then
|
||
|
|
return -- Error("NYI")
|
||
|
|
|
||
|
|
elseif k == "weapon_offhand_dps" then
|
||
|
|
return -- Error("NYI")
|
||
|
|
-- return OffhandHasWeapon()
|
||
|
|
|
||
|
|
elseif k == "weapon_offhand_speed" then
|
||
|
|
return -- Error("NYI")
|
||
|
|
|
||
|
|
elseif k == "armor" then
|
||
|
|
return -- Error("NYI")
|
||
|
|
|
||
|
|
elseif k == "bonus_armor" then
|
||
|
|
return UnitArmor("player")
|
||
|
|
|
||
|
|
elseif k == "resilience_rating" then
|
||
|
|
return GetCombatRating(CR_CRIT_TAKEN_SPELL)
|
||
|
|
|
||
|
|
elseif k == "mastery_rating" then
|
||
|
|
return GetCombatRating(CR_MASTERY)
|
||
|
|
|
||
|
|
elseif k == "mastery_value" then
|
||
|
|
return GetMasteryEffect()
|
||
|
|
|
||
|
|
elseif k == "versatility_atk_rating" then
|
||
|
|
return GetCombatRating(CR_VERSATILITY_DAMAGE_DONE)
|
||
|
|
|
||
|
|
elseif k == "versatility_atk_mod" then
|
||
|
|
return GetCombatRatingBonus(CR_VERSATILITY_DAMAGE_DONE) / 100
|
||
|
|
|
||
|
|
elseif k == "versatility_def_rating" then
|
||
|
|
return GetCombatRating(CR_VERSATILITY_DAMAGE_TAKEN)
|
||
|
|
|
||
|
|
elseif k == "versatility_def_mod" then
|
||
|
|
return GetCombatRatingBonus(CR_VERSATILITY_DAMAGE_TAKEN) / 100
|
||
|
|
|
||
|
|
elseif k == "mod_haste_pct" then
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "spell_haste" then
|
||
|
|
return ( UnitSpellHaste( "player" ) + ( t.mod_haste_pct or 0 ) ) / 100
|
||
|
|
|
||
|
|
elseif k == "melee_haste" then
|
||
|
|
return ( GetMeleeHaste("player") + ( t.mod_haste_pct or 0 ) ) / 100
|
||
|
|
|
||
|
|
elseif k == "haste" then
|
||
|
|
return t.spell_haste or t.melee_haste
|
||
|
|
|
||
|
|
elseif k == "mod_crit_pct" then
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "crit" then
|
||
|
|
return ( max( GetCritChance( "player" ), GetSpellCritChance( "player" ), GetRangedCritChance( "player" ) ) + ( t.mod_crit_pct or 0 ) )
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Hekili:Error( "Unknown state.stat key: '" .. k .. "'." )
|
||
|
|
return
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_stat = mt_stat
|
||
|
|
|
||
|
|
|
||
|
|
-- Table of default handlers for specific pets/totems.
|
||
|
|
local mt_default_pet = {
|
||
|
|
__index = function( t, k )
|
||
|
|
--[[ if rawget( t, "permanent" ) then
|
||
|
|
if k == "up" or k == "exists" then
|
||
|
|
return UnitExists( "pet" ) and ( not UnitIsDead( "pet" ) )
|
||
|
|
|
||
|
|
elseif k == "alive" then
|
||
|
|
return not UnitIsDead( "pet" )
|
||
|
|
|
||
|
|
elseif k == "dead" then
|
||
|
|
return UnitIsDead( "pet" )
|
||
|
|
|
||
|
|
elseif k == "remains" then
|
||
|
|
return 3600
|
||
|
|
|
||
|
|
elseif k == "down" then
|
||
|
|
return not UnitExists( "pet" ) or UnitIsDead( "pet" )
|
||
|
|
|
||
|
|
end
|
||
|
|
end ]]
|
||
|
|
|
||
|
|
if k == "expires" then
|
||
|
|
local present, name, start, duration
|
||
|
|
|
||
|
|
for i = 1, 5 do
|
||
|
|
present, name, start, duration = GetTotemInfo( i )
|
||
|
|
if duration == 0 then duration = 3600 end
|
||
|
|
|
||
|
|
if present and class.abilities[ t.key ] and name == class.abilities[ t.key ].name then
|
||
|
|
t.expires = start + duration
|
||
|
|
return t.expires
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
t.expires = 0
|
||
|
|
return t[ k ]
|
||
|
|
|
||
|
|
elseif k == "remains" then
|
||
|
|
return max( 0, t.expires - ( state.query_time ) )
|
||
|
|
|
||
|
|
elseif k == "up" or k == "active" or k == "alive" or k == "exists" then
|
||
|
|
-- TODO: Need to make pet.alive work here.
|
||
|
|
return ( t.expires >= ( state.query_time ) )
|
||
|
|
|
||
|
|
elseif k == "down" then
|
||
|
|
return ( t.expires < ( state.query_time ) )
|
||
|
|
|
||
|
|
elseif k == "id" then
|
||
|
|
return t.exists and UnitGUID( "pet" ) and tonumber( UnitGUID( "pet" ):match("(%d+)-%x-$" ) ) or nil
|
||
|
|
|
||
|
|
elseif k == "spec" then
|
||
|
|
return t.exists and GetSpecialization( false, true )
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
return -- Error("UNK: " .. k)
|
||
|
|
end,
|
||
|
|
}
|
||
|
|
ns.metatables.mt_default_pet = mt_default_pet
|
||
|
|
|
||
|
|
|
||
|
|
-- Table of pet data.
|
||
|
|
local mt_pets = {
|
||
|
|
__index = function(t, k)
|
||
|
|
-- Should probably add all totems, but holding off for now.
|
||
|
|
for id, pet in pairs( t ) do
|
||
|
|
if type( pet ) == "table" and pet.up and pet[ k ] ~= nil then
|
||
|
|
return pet[ k ]
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if k == "up" or k == "exists" or k == "active" then
|
||
|
|
for k, v in pairs( t ) do
|
||
|
|
if type(v) == "table" then
|
||
|
|
if v.expires > state.query_time then return true end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
return UnitExists( "pet" ) and ( not UnitIsDead( "pet" ) )
|
||
|
|
|
||
|
|
elseif k == "alive" then
|
||
|
|
return UnitExists( "pet" ) and not UnitIsDead( "pet" ) and UnitHealth( "pet" ) > 0
|
||
|
|
|
||
|
|
elseif k == "dead" then
|
||
|
|
return UnitExists( "pet" ) and UnitIsDead( "pet" )
|
||
|
|
|
||
|
|
elseif k == "health_pct" or k == "health_percent" then
|
||
|
|
if t.alive then return 100 * UnitHealth( "pet" ) / UnitHealthMax( "pet" ) end
|
||
|
|
return 100
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
local model = class.pets[ k ]
|
||
|
|
|
||
|
|
if model then
|
||
|
|
t[ k ] = {
|
||
|
|
id = model.id,
|
||
|
|
name = k,
|
||
|
|
duration = model.duration,
|
||
|
|
expires = nil,
|
||
|
|
spec = model.spec,
|
||
|
|
}
|
||
|
|
|
||
|
|
if model.spec then
|
||
|
|
t[ model.spec ] = t[ k ]
|
||
|
|
end
|
||
|
|
|
||
|
|
return t[ k ]
|
||
|
|
end
|
||
|
|
|
||
|
|
return t.fake_pet
|
||
|
|
end,
|
||
|
|
|
||
|
|
__newindex = function(t, k, v)
|
||
|
|
if type(v) == "table" then
|
||
|
|
if not v.key then v.key = k end
|
||
|
|
rawset( t, k, setmetatable( v, mt_default_pet ) )
|
||
|
|
else
|
||
|
|
rawset( t, k, v )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
}
|
||
|
|
ns.metatables.mt_pets = mt_pets
|
||
|
|
|
||
|
|
|
||
|
|
local mt_stances = {
|
||
|
|
__index = function( t, k )
|
||
|
|
if not class.stances[ k ] or not GetShapeshiftForm() then return false
|
||
|
|
elseif GetShapeshiftForm() < 1 then return false
|
||
|
|
elseif not select( 5, GetShapeshiftFormInfo( GetShapeshiftForm() ) ) == class.stances[k] then return false end
|
||
|
|
rawset(t, k, select( 5, GetShapeshiftFormInfo( GetShapeshiftForm() ) ) == class.stances[k] )
|
||
|
|
return t[k]
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_stances = mt_stances
|
||
|
|
|
||
|
|
-- Table of supported toggles (via keybinding).
|
||
|
|
-- Need to add a commandline interface for these, but for some reason, I keep neglecting that.
|
||
|
|
local mt_toggle = {
|
||
|
|
__index = function(t, k)
|
||
|
|
if not k then return end
|
||
|
|
|
||
|
|
if metafunctions.toggle[ k ] then
|
||
|
|
return metafunctions.toggle[ k ]()
|
||
|
|
end
|
||
|
|
|
||
|
|
local db = Hekili.DB
|
||
|
|
if not db then return end
|
||
|
|
|
||
|
|
local toggle = db.profile.toggles[ k ]
|
||
|
|
|
||
|
|
if k == "cooldowns" and toggle.override and state.buff.bloodlust.up then return true end
|
||
|
|
if k == "essences" and toggle.override and state.toggle.cooldowns then return true end
|
||
|
|
|
||
|
|
if toggle then return toggle.value end
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_toggle = mt_toggle
|
||
|
|
|
||
|
|
|
||
|
|
local mt_settings = {
|
||
|
|
__index = function( t, k )
|
||
|
|
if metafunctions.settings[ k ] then
|
||
|
|
return metafunctions.settings[ k ]()
|
||
|
|
end
|
||
|
|
|
||
|
|
local ability = state.this_action and class.abilities[ state.this_action ]
|
||
|
|
|
||
|
|
if rawget( t, "spec" ) then
|
||
|
|
if t.spec.settings[ k ] ~= nil then return t.spec.settings[ k ] end
|
||
|
|
if t.spec[ k ] ~= nil then return t.spec[ k ] end
|
||
|
|
|
||
|
|
if ability then
|
||
|
|
if ability.item and t.spec.items[ state.this_action ] ~= nil then return t.spec.items[ state.this_action ][ k ]
|
||
|
|
elseif not ability.item and t.spec.abilities[ state.this_action ] ~= nil then return t.spec.abilities[ state.this_action ][ k ] end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
return
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_settings = mt_settings
|
||
|
|
|
||
|
|
|
||
|
|
-- Table of target attributes. Needs to be expanded.
|
||
|
|
-- Needs review.
|
||
|
|
local mt_target = {
|
||
|
|
__index = function(t, k)
|
||
|
|
if k == "level" then
|
||
|
|
return UnitLevel("target") or UnitLevel("player")
|
||
|
|
|
||
|
|
elseif k == "unit" then
|
||
|
|
if state.args.cycle_target == 1 then return UnitGUID( "target" ) .. "c" or "cycle"
|
||
|
|
elseif state.args.target then return UnitGUID( "target" ) .. '+' .. state.args.target or "unknown" end
|
||
|
|
return UnitGUID( "target" ) or "unknown"
|
||
|
|
|
||
|
|
elseif k == "class" then
|
||
|
|
if not UnitExists( "target" ) then return "virtual"
|
||
|
|
elseif not UnitIsPlayer( "target" ) then return "npc" end
|
||
|
|
|
||
|
|
local c = UnitClassBase( "target" )
|
||
|
|
if c then return strlower( c ) end
|
||
|
|
|
||
|
|
return "unknown"
|
||
|
|
|
||
|
|
elseif k == "time_to_die" then
|
||
|
|
local ttd = Hekili:GetTTD( "target" )
|
||
|
|
if ttd == 3600 then return ttd end
|
||
|
|
return max( 1, Hekili:GetTTD( "target" ) - ( state.offset + state.delay ) )
|
||
|
|
|
||
|
|
elseif k:sub(1, 12) == "time_to_pct_" then
|
||
|
|
local percent = tonumber( k:sub( 13 ) ) or 0
|
||
|
|
return Hekili:GetTimeToPct( "target", percent ) - ( state.offset + state.delay )
|
||
|
|
|
||
|
|
elseif k == "health_current" then
|
||
|
|
return ( UnitHealth("target") > 0 and UnitHealth("target") or 50000 )
|
||
|
|
|
||
|
|
elseif k == "health_max" then
|
||
|
|
return ( UnitHealthMax("target") > 0 and UnitHealthMax("target") or 50000 )
|
||
|
|
|
||
|
|
elseif k == "health_pct" or k == "health_percent" then
|
||
|
|
-- TBD: should health_pct use our time offset and TTD calculation to predict health?
|
||
|
|
-- Currently deciding not to, as predicting that you can use something that you can't is
|
||
|
|
-- probably worse than saying you can't use something that you can. Right?
|
||
|
|
return t.health_max ~= 0 and ( 100 * ( t.health_current / t.health_max ) ) or 0
|
||
|
|
|
||
|
|
elseif k == "adds" then
|
||
|
|
-- Need to return # of active targets minus 1.
|
||
|
|
return max(0, ns.numTargets() - 1)
|
||
|
|
|
||
|
|
elseif k == "distance" then
|
||
|
|
-- Need to identify a couple of spells to roughly get the distance to an enemy.
|
||
|
|
-- We'd probably use IsSpellInRange() on an individual action instead, so maybe not.
|
||
|
|
t.distance = UnitCanAttack( "player", "target" ) and ( ( t.minR + t.maxR ) / 2 ) or 7.5
|
||
|
|
return t.distance
|
||
|
|
|
||
|
|
elseif k == "moving" then
|
||
|
|
return GetUnitSpeed( "target" ) > 0
|
||
|
|
|
||
|
|
elseif k == "exists" then
|
||
|
|
return UnitExists( "target" )
|
||
|
|
|
||
|
|
elseif k == "casting" then
|
||
|
|
return state.debuff.casting.up and not state.debuff.casting.v2
|
||
|
|
|
||
|
|
elseif k == "in_range" then
|
||
|
|
return t.distance <= 8
|
||
|
|
|
||
|
|
--[[ local ability = state.this_action and class.abilities[ state.this_action ]
|
||
|
|
|
||
|
|
if ability then
|
||
|
|
return ( not state.target.exists or ( LibStub( "SpellRange-1.0" ).IsSpellInRange( ability.id, "target" ) == true ) )
|
||
|
|
end
|
||
|
|
|
||
|
|
return true ]]
|
||
|
|
|
||
|
|
elseif k == "is_demon" then
|
||
|
|
return UnitCreatureType( "target" ) == PET_TYPE_DEMON
|
||
|
|
|
||
|
|
elseif k == "is_undead" then
|
||
|
|
return UnitCreatureType( "target" ) == BATTLE_PET_NAME_4
|
||
|
|
|
||
|
|
elseif k == "is_player" then
|
||
|
|
return UnitIsPlayer( "target" )
|
||
|
|
|
||
|
|
elseif k == "is_boss" then
|
||
|
|
if UnitExists( "boss1" ) and UnitIsUnit( "target", "boss1" ) or
|
||
|
|
UnitExists( "boss2" ) and UnitIsUnit( "target", "boss2" ) or
|
||
|
|
UnitExists( "boss3" ) and UnitIsUnit( "target", "boss3" ) or
|
||
|
|
UnitExists( "boss4" ) and UnitIsUnit( "target", "boss4" ) or
|
||
|
|
UnitExists( "boss5" ) and UnitIsUnit( "target", "boss5" ) then return true
|
||
|
|
end
|
||
|
|
return ( UnitCanAttack( "player", "target" ) and ( UnitClassification( "target" ) == "worldboss" or UnitLevel( "target" ) == -1 ) )
|
||
|
|
|
||
|
|
elseif k == "is_add" then
|
||
|
|
return not t.is_boss
|
||
|
|
|
||
|
|
elseif k == "is_friendly" then
|
||
|
|
return UnitExists( "target" ) and UnitIsFriend( "target", "player" )
|
||
|
|
|
||
|
|
elseif k:sub(1, 6) == "within" then
|
||
|
|
local maxR = k:match( "^within(%d+)$" )
|
||
|
|
|
||
|
|
if not maxR then
|
||
|
|
-- Error("UNK: " .. k)
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
|
||
|
|
return ( t.maxR <= tonumber( maxR ) )
|
||
|
|
|
||
|
|
elseif k:sub(1, 7) == "outside" then
|
||
|
|
local minR = k:match( "^outside(%d+)$" )
|
||
|
|
|
||
|
|
if not minR then
|
||
|
|
-- Error("UNK: " .. k)
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
|
||
|
|
return ( t.minR > tonumber( minR ) )
|
||
|
|
|
||
|
|
elseif k:sub(1, 5) == "range" then
|
||
|
|
local minR, maxR = k:match( "^range(%d+)to(%d+)$" )
|
||
|
|
|
||
|
|
if not minR or not maxR then
|
||
|
|
return false
|
||
|
|
-- Error("UNK: " .. k)
|
||
|
|
end
|
||
|
|
|
||
|
|
return ( t.minR >= tonumber( minR ) and t.maxR <= tonumber( maxR ) )
|
||
|
|
|
||
|
|
elseif k == 'minR' then
|
||
|
|
local minR = LibStub( "LibRangeCheck-2.0" ):GetRange( "target" )
|
||
|
|
if minR then
|
||
|
|
t.minR = minR
|
||
|
|
return t.minR
|
||
|
|
end
|
||
|
|
return 5
|
||
|
|
|
||
|
|
elseif k == 'maxR' then
|
||
|
|
local maxR = select( 2, LibStub( "LibRangeCheck-2.0" ):GetRange( "target" ) )
|
||
|
|
if maxR then
|
||
|
|
t.maxR = maxR
|
||
|
|
return t.maxR
|
||
|
|
end
|
||
|
|
return 10
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
return
|
||
|
|
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_target = mt_target
|
||
|
|
|
||
|
|
|
||
|
|
local mt_target_health = {
|
||
|
|
__index = function(t, k)
|
||
|
|
if k == "current" or k == "actual" then
|
||
|
|
return UnitCanAttack("player", "target") and not UnitIsDead( "target" ) and UnitHealth("target") or 10000
|
||
|
|
|
||
|
|
elseif k == "max" then
|
||
|
|
return UnitCanAttack("player", "target") and not UnitIsDead( "target" ) and UnitHealthMax("target") or 10000
|
||
|
|
|
||
|
|
elseif k == "pct" or k == "percent" then
|
||
|
|
return t.max ~= 0 and ( 100 * t.current / t.max ) or 100
|
||
|
|
end
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_target_health = mt_target_health
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
local mt_consumable = {
|
||
|
|
__index = function( t, k )
|
||
|
|
return class.potion == k
|
||
|
|
end
|
||
|
|
}
|
||
|
|
setmetatable( state.consumable, mt_consumable )
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
local cd_meta_functions = {}
|
||
|
|
|
||
|
|
function ns.addCooldownMetaFunction( ability, key, func )
|
||
|
|
if not state.cooldown[ ability ] then state.cooldown[ ability ] = { key = ability } end
|
||
|
|
if not rawget( state.cooldown[ ability ], "meta" ) then state.cooldown[ ability ].meta = {} end
|
||
|
|
state.cooldown[ ability ].meta[ key ] = setfenv( func, state )
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
-- Table of default handlers for specific ability cooldowns.
|
||
|
|
local mt_default_cooldown = {
|
||
|
|
__index = function( t, k )
|
||
|
|
local ability = t.key and class.abilities[ t.key ]
|
||
|
|
|
||
|
|
if rawget( ability, "meta" ) and ability.meta[ k ] then
|
||
|
|
return ability.meta[ k ]( t )
|
||
|
|
end
|
||
|
|
|
||
|
|
local GetCooldown = _G.GetSpellCooldown
|
||
|
|
local profile = Hekili.DB.profile
|
||
|
|
local id = ability.id
|
||
|
|
|
||
|
|
if ability then
|
||
|
|
if ability.item then
|
||
|
|
GetCooldown = _G.GetItemCooldown
|
||
|
|
id = ability.itemCd or ability.item
|
||
|
|
elseif ability.funcs.cooldown_special then
|
||
|
|
GetCooldown = ability.funcs.cooldown_special
|
||
|
|
id = 999999
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
local raw = state.display ~= "Primary" and state.display ~= "AOE"
|
||
|
|
|
||
|
|
if k:sub(1, 5) == "true_" then
|
||
|
|
k = k:sub(6)
|
||
|
|
raw = true
|
||
|
|
end
|
||
|
|
|
||
|
|
if k == "duration" or k == "expires" or k == "next_charge" or k == "charge" or k == "recharge_began" then
|
||
|
|
-- Refresh the ID in case we changed specs and ability is spec dependent.
|
||
|
|
t.id = ability.id
|
||
|
|
|
||
|
|
local start, duration = 0, 0
|
||
|
|
|
||
|
|
if id > 0 then start, duration = GetCooldown( id ) end
|
||
|
|
|
||
|
|
if t.key ~= "global_cooldown" then
|
||
|
|
local gcdStart, gcdDuration = GetSpellCooldown( 61304 )
|
||
|
|
if gcdStart == start and gcdDuration == duration then start, duration = 0, 0 end
|
||
|
|
end
|
||
|
|
|
||
|
|
local true_duration = duration
|
||
|
|
|
||
|
|
if t.key == "ascendance" and state.buff.ascendance.up then
|
||
|
|
start = state.buff.ascendance.expires - class.auras.ascendance.duration
|
||
|
|
duration = class.abilities[ "ascendance" ].cooldown
|
||
|
|
|
||
|
|
elseif t.key == "potion" then
|
||
|
|
local itemName = state.args.ModName or state.args.name or class.potion
|
||
|
|
local potion = class.potions[ itemName ]
|
||
|
|
|
||
|
|
if state.toggle.potions and potion and GetItemCount( potion.item ) > 0 then
|
||
|
|
start, duration = GetItemCooldown( potion.item )
|
||
|
|
|
||
|
|
else
|
||
|
|
start = state.now
|
||
|
|
duration = 0
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
elseif not state:IsKnown( t.id ) then
|
||
|
|
start = state.now
|
||
|
|
duration = 0
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
t.duration = max( duration or 0, ability.cooldown or 0, ability.recharge or 0 ) or 0
|
||
|
|
t.expires = start and ( start + duration ) or 0
|
||
|
|
t.true_duration = true_duration
|
||
|
|
t.true_expires = start and ( start + true_duration ) or 0
|
||
|
|
|
||
|
|
if ability.charges and ability.charges > 1 then
|
||
|
|
local charges, maxCharges, start, duration = GetSpellCharges( t.id )
|
||
|
|
|
||
|
|
--[[ if class.abilities[ t.key ].toggle and not state.toggle[ class.abilities[ t.key ].toggle ] then
|
||
|
|
charges = 1
|
||
|
|
maxCharges = 1
|
||
|
|
start = state.now
|
||
|
|
duration = 0
|
||
|
|
end ]]
|
||
|
|
|
||
|
|
t.charge = charges or 1
|
||
|
|
t.recharge = duration or ability.recharge
|
||
|
|
|
||
|
|
if charges and charges < maxCharges then
|
||
|
|
t.next_charge = start + duration
|
||
|
|
else
|
||
|
|
t.next_charge = 0
|
||
|
|
end
|
||
|
|
t.recharge_began = start or t.expires - t.duration
|
||
|
|
|
||
|
|
else
|
||
|
|
t.charge = t.expires < state.query_time and 1 or 0
|
||
|
|
t.next_charge = t.expires > state.query_time and t.expires or 0
|
||
|
|
t.recharge_began = t.expires - t.duration
|
||
|
|
end
|
||
|
|
|
||
|
|
return t[k]
|
||
|
|
|
||
|
|
elseif k == "charges" then
|
||
|
|
if not raw and ( state:IsDisabled( t.key ) or ability.disabled ) then
|
||
|
|
return 0
|
||
|
|
end
|
||
|
|
|
||
|
|
return floor( t.charges_fractional )
|
||
|
|
|
||
|
|
elseif k == "charges_max" or k == "max_charges" then
|
||
|
|
return ability.charges or 1
|
||
|
|
|
||
|
|
elseif k == "recharge" then
|
||
|
|
return ability.recharge or ability.cooldown or 0
|
||
|
|
|
||
|
|
elseif k == "time_to_max_charges" or k == "full_recharge_time" then
|
||
|
|
if raw then return ( ( ability.charges or 1 ) - t.true_charges_fractional ) * ( ability.recharge or ability.cooldown ) end
|
||
|
|
return ( ( ability.charges or 1 ) - t.charges_fractional ) * ( ability.recharge or ability.cooldown )
|
||
|
|
|
||
|
|
elseif k == "remains" then
|
||
|
|
if t.key == "global_cooldown" then
|
||
|
|
return max( 0, t.expires - state.query_time )
|
||
|
|
end
|
||
|
|
|
||
|
|
-- If the ability is toggled off in the profile, we may want to fake its CD.
|
||
|
|
-- Revisit this if I add base_cooldown to the ability tables.
|
||
|
|
if not raw and ( state:IsDisabled( t.key ) or ability.disabled ) then
|
||
|
|
return ability.cooldown
|
||
|
|
end
|
||
|
|
|
||
|
|
local bonus_cdr = 0
|
||
|
|
bonus_cdr = ns.callHook( "cooldown_recovery", bonus_cdr ) or bonus_cdr
|
||
|
|
|
||
|
|
return max( 0, t.expires - state.query_time - bonus_cdr )
|
||
|
|
|
||
|
|
elseif k == "charges_fractional" then
|
||
|
|
if not state:IsKnown( t.key ) then return 1 end
|
||
|
|
if not raw and ( state:IsDisabled( t.key ) or ability.disabled ) then return 0 end
|
||
|
|
|
||
|
|
if ability.charges and ability.charges > 1 then
|
||
|
|
-- run this ad-hoc rather than with every advance.
|
||
|
|
while t.next_charge > 0 and t.next_charge < state.now + state.offset do
|
||
|
|
-- if class.abilities[ k ].charges and cd.next_charge > 0 and cd.next_charge < state.now + state.offset then
|
||
|
|
t.charge = t.charge + 1
|
||
|
|
if t.charge < ability.charges then
|
||
|
|
t.recharge_began = t.next_charge
|
||
|
|
t.next_charge = t.next_charge + ability.recharge
|
||
|
|
else
|
||
|
|
t.recharge_began = 0
|
||
|
|
t.next_charge = 0
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if t.charge < ability.charges then
|
||
|
|
return min( ability.charges, t.charge + ( max( 0, state.query_time - t.recharge_began ) / t.recharge ) )
|
||
|
|
-- return t.charges + ( 1 - ( class.abilities[ t.key ].recharge - t.recharge_time ) / class.abilities[ t.key ].recharge )
|
||
|
|
end
|
||
|
|
return t.charge
|
||
|
|
end
|
||
|
|
|
||
|
|
return t.remains > 0 and ( 1 - ( t.remains / ability.cooldown ) ) or 1
|
||
|
|
|
||
|
|
--
|
||
|
|
elseif k == "recharge_time" then
|
||
|
|
if not ability.charges then return t.duration or 0 end
|
||
|
|
return t.recharge
|
||
|
|
|
||
|
|
elseif k == "up" or k == "ready" then
|
||
|
|
return ( ability.cooldown_ready == nil or ability.cooldown_ready ) and t.remains == 0
|
||
|
|
|
||
|
|
-- Hunters
|
||
|
|
elseif k == "remains_guess" or k == "remains_expected" then
|
||
|
|
if t.remains == t.duration then return t.remains end
|
||
|
|
|
||
|
|
local lastCast = state.action[ t.key ].lastCast or 0
|
||
|
|
if lastCast == 0 then return t.remains end
|
||
|
|
|
||
|
|
local reduction = ( state.query_time - lastCast ) / ( t.duration - t.remains )
|
||
|
|
return t.remains * reduction
|
||
|
|
|
||
|
|
elseif k == "duration_guess" then
|
||
|
|
if t.remains == t.duration then return t.duration end
|
||
|
|
|
||
|
|
-- not actually the same as simc here, which tracks when CDs charge.
|
||
|
|
local lastCast = state.action[ t.key ].lastCast or 0
|
||
|
|
if lastCast == 0 then return t.duration end
|
||
|
|
|
||
|
|
local reduction = ( state.query_time - lastCast ) / ( t.duration - t.remains )
|
||
|
|
return t.duration * reduction
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
Error( "UNK: cooldown." .. t.key .. "." .. k )
|
||
|
|
return
|
||
|
|
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_default_cooldown = mt_default_cooldown
|
||
|
|
|
||
|
|
|
||
|
|
-- Table for gathering cooldown information. Some abilities with odd behavior are getting embedded here.
|
||
|
|
-- Probably need a better system that I can keep in the class modules.
|
||
|
|
-- Needs review.
|
||
|
|
local mt_cooldowns = {
|
||
|
|
-- The action doesn't exist in our table so check the real game state, -- and copy it so we don't have to use the API next time.
|
||
|
|
__index = function( t, k )
|
||
|
|
local entry = class.abilities[ k ]
|
||
|
|
|
||
|
|
if not entry then
|
||
|
|
-- Check if this is one of SimC's lovely itemname_spellid type tokens.
|
||
|
|
local shortkey = k:match( "^([a-z0-9_])_%d+$" )
|
||
|
|
|
||
|
|
if shortkey and class.abilities[ shortkey ] then
|
||
|
|
class.abilities[ k ] = class.abilities[ shortkey ]
|
||
|
|
entry = class.abilities[ k ]
|
||
|
|
else
|
||
|
|
return t.null_cooldown
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if k ~= entry.key then
|
||
|
|
t[ k ] = t[ entry.key ]
|
||
|
|
return t[ k ]
|
||
|
|
end
|
||
|
|
|
||
|
|
t[ k ] = { key = k }
|
||
|
|
return t[ k ]
|
||
|
|
end,
|
||
|
|
__newindex = function(t, k, v)
|
||
|
|
rawset( t, k, setmetatable( v, mt_default_cooldown ) )
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_cooldowns = mt_cooldowns
|
||
|
|
|
||
|
|
|
||
|
|
local mt_dot = {
|
||
|
|
__index = function(t, k)
|
||
|
|
local a = class.auras[ k ]
|
||
|
|
|
||
|
|
if a and a.dot == "buff" then
|
||
|
|
return state.buff[ k ]
|
||
|
|
end
|
||
|
|
|
||
|
|
return state.debuff[ k ]
|
||
|
|
end,
|
||
|
|
}
|
||
|
|
ns.metatables.mt_dot = mt_dot
|
||
|
|
|
||
|
|
|
||
|
|
local mt_gcd = {
|
||
|
|
__index = function( t, k )
|
||
|
|
if k == "execute" then
|
||
|
|
local ability = state.this_action and class.abilities[ state.this_action ]
|
||
|
|
|
||
|
|
-- We can specify this for any ability, if we want.
|
||
|
|
if ability and ability.gcdTime then return ability.gcdTime end
|
||
|
|
|
||
|
|
local gcd = ( state.this_action == "wait" and "spell" ) or ( ability and ability.gcd or "spell" )
|
||
|
|
|
||
|
|
if gcd == "off" then return 0 end
|
||
|
|
if gcd == "totem" then return 1 end
|
||
|
|
|
||
|
|
if UnitPowerType( "player" ) == Enum.PowerType.Energy then
|
||
|
|
return state.buff.adrenaline_rush.up and 0.8 or 1
|
||
|
|
end
|
||
|
|
|
||
|
|
return max( 1.5 * state.haste, state.buff.voidform.up and 0.67 or 0.75 )
|
||
|
|
|
||
|
|
elseif k == "remains" then
|
||
|
|
return state.cooldown.global_cooldown.remains
|
||
|
|
|
||
|
|
elseif k == "max" or k == "duration" then
|
||
|
|
if UnitPowerType( "player" ) == Enum.PowerType.Energy then
|
||
|
|
return state.buff.adrenaline_rush.up and 0.8 or 1
|
||
|
|
end
|
||
|
|
|
||
|
|
return max( 1.5 * state.haste, state.buff.voidform.up and 0.67 or 0.75 )
|
||
|
|
|
||
|
|
elseif k == "lastStart" then
|
||
|
|
return 0
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
return
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_gcd = mt_gcd
|
||
|
|
setmetatable( state.gcd, mt_gcd )
|
||
|
|
|
||
|
|
|
||
|
|
local mt_prev_lookup = {
|
||
|
|
__index = function( t, k )
|
||
|
|
local idx = t.index
|
||
|
|
local preds, prev
|
||
|
|
local action
|
||
|
|
|
||
|
|
if t.meta == 'castsAll' then preds, prev = state.predictions, state.prev
|
||
|
|
elseif t.meta == 'castsOn' then preds, prev = state.predictionsOn, state.prev_gcd
|
||
|
|
elseif t.meta == 'castsOff' then preds, prev = state.predictionsOff, state.prev_off_gcd end
|
||
|
|
|
||
|
|
if k == "spell" then
|
||
|
|
-- Return the actual spell for the slot, for lookups.
|
||
|
|
if preds[ idx ] then return preds[ idx ] end
|
||
|
|
|
||
|
|
if state.player.queued_ability then
|
||
|
|
if idx == #preds + 1 then return state.player.queued_ability end
|
||
|
|
return prev.history[ idx - #preds + 1 ]
|
||
|
|
end
|
||
|
|
|
||
|
|
if idx == 1 and prev.override then
|
||
|
|
return prev.override
|
||
|
|
end
|
||
|
|
|
||
|
|
return prev.history[ idx - #preds ]
|
||
|
|
end
|
||
|
|
|
||
|
|
if preds[ idx ] then return preds[ idx ] == k end
|
||
|
|
|
||
|
|
if state.player.queued_ability then
|
||
|
|
if idx == #preds + 1 then return state.player.queued_ability == k end
|
||
|
|
return prev.history[ idx - #preds + 1 ] == k
|
||
|
|
end
|
||
|
|
|
||
|
|
if idx == 1 and prev.override then
|
||
|
|
return prev.override == k
|
||
|
|
end
|
||
|
|
|
||
|
|
if state.time == 0 then return false end
|
||
|
|
|
||
|
|
return prev.history[ idx - #preds ] == k
|
||
|
|
end,
|
||
|
|
}
|
||
|
|
|
||
|
|
local prev_lookup = setmetatable( {
|
||
|
|
index = 1,
|
||
|
|
meta = 'castsAll'
|
||
|
|
}, mt_prev_lookup )
|
||
|
|
|
||
|
|
|
||
|
|
local mt_prev = {
|
||
|
|
__index = function( t, k )
|
||
|
|
if type( k ) == "number" then
|
||
|
|
-- This is a SimulationCraft 7.1.5 or later indexed lookup, we support up to #5.
|
||
|
|
if k < 1 or k > 5 then return false end
|
||
|
|
prev_lookup.meta = t.meta -- Which data to use? castsAll, castsOn (GCD), castsOff (offGCD)?
|
||
|
|
prev_lookup.index = k
|
||
|
|
return prev_lookup
|
||
|
|
end
|
||
|
|
|
||
|
|
if k == t.last then
|
||
|
|
return true
|
||
|
|
end
|
||
|
|
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_prev = mt_prev
|
||
|
|
|
||
|
|
|
||
|
|
local resource_meta_functions = {}
|
||
|
|
|
||
|
|
function state:AddResourceMetaFunction( name, f )
|
||
|
|
resource_meta_functions[ name ] = f
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state:TimeToResource( t, amount )
|
||
|
|
if not amount or amount > t.max then return 3600
|
||
|
|
elseif t.current >= amount then return 0 end
|
||
|
|
|
||
|
|
local pad, lastTick = 0
|
||
|
|
if t.resource == "energy" or t.resource == "focus" then
|
||
|
|
-- Round any result requiring ticks to the next tick.
|
||
|
|
lastTick = t.last_tick
|
||
|
|
end
|
||
|
|
|
||
|
|
if t.forecast and t.fcount > 0 then
|
||
|
|
local q = state.query_time
|
||
|
|
local index, slice
|
||
|
|
|
||
|
|
if t.times[ amount ] then return t.times[ amount ] - q end
|
||
|
|
|
||
|
|
if t.regen == 0 then
|
||
|
|
for i = 1, t.fcount do
|
||
|
|
local v = t.forecast[ i ]
|
||
|
|
if v.v >= amount then
|
||
|
|
t.times[ amount ] = v.t
|
||
|
|
return max( 0, t.times[ amount ] - q )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
t.times[ amount ] = q + 3600
|
||
|
|
return max( 0, t.times[ amount ] - q )
|
||
|
|
end
|
||
|
|
|
||
|
|
for i = 1, t.fcount do
|
||
|
|
local slice = t.forecast[ i ]
|
||
|
|
local after = t.forecast[ i + 1 ]
|
||
|
|
|
||
|
|
if slice.v >= amount then
|
||
|
|
t.times[ amount ] = slice.t
|
||
|
|
|
||
|
|
if lastTick then
|
||
|
|
pad = ( slice.t - lastTick ) % 0.1
|
||
|
|
pad = 0.1 - pad
|
||
|
|
end
|
||
|
|
|
||
|
|
return max( 0, pad + t.times[ amount ] - q )
|
||
|
|
|
||
|
|
elseif after and after.v >= amount then
|
||
|
|
-- Our next slice will have enough resources. Check to see if we'd regen enough in-between.
|
||
|
|
local time_diff = after.t - slice.t
|
||
|
|
local deficit = amount - slice.v
|
||
|
|
local regen_time = deficit / t.regen
|
||
|
|
|
||
|
|
if lastTick then
|
||
|
|
pad = ( slice.t - lastTick ) % 0.1
|
||
|
|
pad = 0.1 - pad
|
||
|
|
end
|
||
|
|
|
||
|
|
if regen_time < time_diff then
|
||
|
|
t.times[ amount ] = ( pad + slice.t + regen_time )
|
||
|
|
else
|
||
|
|
t.times[ amount ] = after.t
|
||
|
|
end
|
||
|
|
return max( 0, t.times[ amount ] - q )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
t.times[ amount ] = q + 3600
|
||
|
|
return max( 0, t.times[ amount ] - q )
|
||
|
|
end
|
||
|
|
|
||
|
|
-- This wasn't a modeled resource,, just look at regen time.
|
||
|
|
if lastTick then
|
||
|
|
pad = ( slice.t - lastTick ) % 0.1
|
||
|
|
pad = 0.1 - pad
|
||
|
|
end
|
||
|
|
|
||
|
|
if t.regen <= 0 then return 3600 end
|
||
|
|
return max( 0, pad + ( ( amount - t.current ) / t.regen ) )
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
local mt_resource = {
|
||
|
|
__index = function(t, k)
|
||
|
|
|
||
|
|
if resource_meta_functions[ k ] then
|
||
|
|
local result = resource_meta_functions[ k ]( t )
|
||
|
|
|
||
|
|
if result then
|
||
|
|
return result
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if k == "pct" or k == "percent" then
|
||
|
|
return 100 * ( t.current / t.max )
|
||
|
|
|
||
|
|
elseif k == "deficit_pct" or k == "deficit_percent" then
|
||
|
|
return 100 - t.pct
|
||
|
|
|
||
|
|
elseif k == "current" then
|
||
|
|
-- If this is a modeled resource, use our lookup system.
|
||
|
|
if t.forecast and t.fcount > 0 then
|
||
|
|
local q = state.query_time
|
||
|
|
local index, slice
|
||
|
|
|
||
|
|
if t.values[ q ] then return t.values[ q ] end
|
||
|
|
|
||
|
|
for i = 1, t.fcount do
|
||
|
|
local v = t.forecast[ i ]
|
||
|
|
if v.t <= q then
|
||
|
|
index = i
|
||
|
|
slice = v
|
||
|
|
else
|
||
|
|
break
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
-- We have a slice.
|
||
|
|
if index and slice then
|
||
|
|
t.values[ q ] = max( 0, min( t.max, slice.v + ( ( state.query_time - slice.t ) * t.regen ) ) )
|
||
|
|
return t.values[ q ]
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
-- No forecast.
|
||
|
|
if t.regen ~= 0 then
|
||
|
|
return max( 0, min( t.max, t.actual + ( t.regen * state.delay ) ) )
|
||
|
|
end
|
||
|
|
|
||
|
|
return t.actual
|
||
|
|
|
||
|
|
elseif k == "deficit" then
|
||
|
|
return t.max - t.current
|
||
|
|
|
||
|
|
elseif k == "max_nonproc" then
|
||
|
|
return t.max -- need to accommodate buffs that increase mana, etc.
|
||
|
|
|
||
|
|
elseif k == "time_to_max" then
|
||
|
|
return state:TimeToResource( t, t.max )
|
||
|
|
|
||
|
|
elseif k == "time_to_max_combined" then
|
||
|
|
if not state.spec.assassination then return t.time_to_max end
|
||
|
|
|
||
|
|
-- Assassination, April 2021
|
||
|
|
-- Using the same as time_to_max because our time_to_max uses modeled regen events...
|
||
|
|
return state:TimeToResource( t, t.max )
|
||
|
|
|
||
|
|
elseif k:sub(1, 8) == "time_to_" then
|
||
|
|
local amount = k:sub(9)
|
||
|
|
amount = tonumber(amount)
|
||
|
|
|
||
|
|
if not amount then return 3600 end
|
||
|
|
|
||
|
|
return state:TimeToResource( t, amount )
|
||
|
|
|
||
|
|
elseif k == "regen" then
|
||
|
|
return ( state.time > 0 and t.active_regen or t.inactive_regen ) or 0
|
||
|
|
|
||
|
|
elseif k == "regen_combined" then
|
||
|
|
-- Assassination, April 2021
|
||
|
|
return max( t.regen, state:TimeToResource( t, t.max ) / t.deficit )
|
||
|
|
|
||
|
|
elseif k == "modmax" then
|
||
|
|
return t.max
|
||
|
|
|
||
|
|
elseif k == "model" then
|
||
|
|
return
|
||
|
|
|
||
|
|
elseif k == 'onAdvance' then
|
||
|
|
return
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_resource = mt_resource
|
||
|
|
|
||
|
|
|
||
|
|
local default_buff_values = {
|
||
|
|
name = "no_name",
|
||
|
|
count = 0,
|
||
|
|
lastCount = 0,
|
||
|
|
lastApplied = 0,
|
||
|
|
expires = 0,
|
||
|
|
applied = 0,
|
||
|
|
duration = 15,
|
||
|
|
caster = "nobody",
|
||
|
|
timeMod = 1,
|
||
|
|
v1 = 0,
|
||
|
|
v2 = 0,
|
||
|
|
v3 = 0,
|
||
|
|
|
||
|
|
last_application = 0,
|
||
|
|
last_expiry = 0,
|
||
|
|
|
||
|
|
unit = "player"
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
function state:AddBuffMetaFunction( aura, key, func )
|
||
|
|
local a = class.auras[ aura ]
|
||
|
|
if not a then return end
|
||
|
|
|
||
|
|
if not a.meta then a.meta = {} end
|
||
|
|
a.meta[ key ] = setfenv( func, self )
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
-- Aliases let a single buff name refer to any of multiple buffs.
|
||
|
|
-- Developed mainly for RtB; it will also report "stack" or "count" as the sum of stacks of multiple buffs.
|
||
|
|
local mt_alias_buff = {
|
||
|
|
__index = function( t, k )
|
||
|
|
local aura = class.auras[ t.key ]
|
||
|
|
local type = aura.aliasType or "buff"
|
||
|
|
|
||
|
|
if aura.meta and aura.meta[ k ] then return aura.meta[ k ]() end
|
||
|
|
|
||
|
|
if k == "count" or k == "stack" or k == "stacks" then
|
||
|
|
local n = 0
|
||
|
|
|
||
|
|
for i, child in ipairs( aura.alias ) do
|
||
|
|
if state[ type ][ child ].up then n = n + max( 1, state[ type ][ child ].stack ) end
|
||
|
|
end
|
||
|
|
|
||
|
|
return n
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
local alias
|
||
|
|
local mode = aura.aliasMode or "first"
|
||
|
|
|
||
|
|
for i, v in ipairs( aura.alias ) do
|
||
|
|
local child = state[ type ][ v ]
|
||
|
|
if not alias and mode == "first" and child.up then return child[ k ] end
|
||
|
|
|
||
|
|
if child.up then
|
||
|
|
if mode == "shortest" and ( not alias or child.remains < alias.remains ) then alias = child
|
||
|
|
elseif mode == "longest" and ( not alias or child.remains > alias.remains ) then alias = child end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if alias then return alias[ k ]
|
||
|
|
else return state[ type ][ aura.alias[1] ][ k ] end
|
||
|
|
end
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
local requiresLookup = {
|
||
|
|
name = true,
|
||
|
|
count = true,
|
||
|
|
lastCount = true,
|
||
|
|
lastApplied = true,
|
||
|
|
expires = true,
|
||
|
|
applied = true,
|
||
|
|
caster = true,
|
||
|
|
id = true,
|
||
|
|
timeMod = true,
|
||
|
|
v1 = true,
|
||
|
|
v2 = true,
|
||
|
|
v3 = true,
|
||
|
|
|
||
|
|
last_application = true,
|
||
|
|
last_expiry = true,
|
||
|
|
|
||
|
|
unit = true
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
-- Table of default handlers for auras (buffs, debuffs).
|
||
|
|
local mt_default_buff = {
|
||
|
|
mtID = "default_buff",
|
||
|
|
|
||
|
|
__index = function( t, k )
|
||
|
|
local aura = class.auras[ t.key ]
|
||
|
|
|
||
|
|
if aura and rawget( aura, "meta" ) and aura.meta[ k ] then
|
||
|
|
return aura.meta[ k ]( t, "buff" )
|
||
|
|
|
||
|
|
elseif requiresLookup[ k ] then
|
||
|
|
if aura and aura.generate then
|
||
|
|
for attr, a_val in pairs( default_buff_values ) do
|
||
|
|
t[ attr ] = rawget( t, attr ) or rawget( aura, attr ) or a_val
|
||
|
|
end
|
||
|
|
|
||
|
|
aura.generate( t, "buff" )
|
||
|
|
t.id = aura and aura.id or t.key
|
||
|
|
|
||
|
|
return rawget( t, k )
|
||
|
|
end
|
||
|
|
|
||
|
|
local real = auras.player.buff[ t.key ] or auras.target.buff[ t.key ]
|
||
|
|
|
||
|
|
if real then
|
||
|
|
t.name = real.name
|
||
|
|
t.count = real.count
|
||
|
|
t.lastCount = real.lastCount or 0
|
||
|
|
t.lastApplied = real.lastApplied or 0
|
||
|
|
t.duration = real.duration
|
||
|
|
t.expires = real.expires
|
||
|
|
t.applied = max( 0, real.expires - real.duration )
|
||
|
|
t.caster = real.caster
|
||
|
|
t.id = real.id or class.auras[ t.key ].id
|
||
|
|
t.timeMod = real.timeMod
|
||
|
|
t.v1 = real.v1
|
||
|
|
t.v2 = real.v2
|
||
|
|
t.v3 = real.v3
|
||
|
|
|
||
|
|
t.last_application = real.last_application or 0
|
||
|
|
t.last_expiry = real.last_expiry or 0
|
||
|
|
|
||
|
|
t.unit = real.unit
|
||
|
|
else
|
||
|
|
local meta = aura and rawget( aura, "meta" )
|
||
|
|
|
||
|
|
for attr, a_val in pairs( default_buff_values ) do
|
||
|
|
if not meta or not meta[ attr ] then
|
||
|
|
t[ attr ] = aura and aura[ attr ] or a_val
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
t.id = rawget( t, id ) or ( aura and aura.id ) or t.key
|
||
|
|
end
|
||
|
|
|
||
|
|
return rawget( t, k )
|
||
|
|
|
||
|
|
elseif k == "up" or k == "ticking" then
|
||
|
|
return t.remains > 0
|
||
|
|
|
||
|
|
elseif k == "react" then
|
||
|
|
if t.expires > state.query_time then
|
||
|
|
return t.count
|
||
|
|
end
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "down" then
|
||
|
|
return t.remains == 0
|
||
|
|
|
||
|
|
elseif k == "remains" then
|
||
|
|
return max( 0, t.expires - state.query_time )
|
||
|
|
|
||
|
|
elseif k == "refreshable" then
|
||
|
|
return t.remains < 0.3 * ( aura.duration or 30 )
|
||
|
|
|
||
|
|
elseif k == "time_to_refresh" then
|
||
|
|
return t.up and max( 0, 0.01 + t.remains - ( 0.3 * ( aura.duration or 30 ) ) ) or 0
|
||
|
|
|
||
|
|
elseif k == "cooldown_remains" then
|
||
|
|
return state.cooldown[ t.key ] and state.cooldown[ t.key ].remains or 0
|
||
|
|
|
||
|
|
elseif k == "max_stack" or k == "max_stacks" then
|
||
|
|
return class.auras[ t.key ].max_stack or 1
|
||
|
|
|
||
|
|
elseif k == "mine" then
|
||
|
|
return t.caster == "player"
|
||
|
|
|
||
|
|
elseif k == "value" then
|
||
|
|
return t.v1
|
||
|
|
|
||
|
|
elseif k == "stack_value" then
|
||
|
|
return t.v1 * t.stack
|
||
|
|
|
||
|
|
elseif k == "stack" or k == "stacks" then
|
||
|
|
if t.up then return ( t.count ) else return 0 end
|
||
|
|
|
||
|
|
elseif k == "stack_pct" then
|
||
|
|
if t.up then return ( 100 * t.stack / t.max_stack ) else return 0 end
|
||
|
|
|
||
|
|
elseif k == "ticks" then
|
||
|
|
if t.up then return t.duration / t.tick_time - t.ticks_remain end
|
||
|
|
-- if t.up then return 1 + ( ( class.auras[ t.key ].duration or ( 30 * state.haste ) ) / ( class.auras[ t.key ].tick_time or ( 3 * t.haste ) ) ) - t.ticks_remain end
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "tick_time" then
|
||
|
|
return aura and aura.tick_time or ( 3 * state.haste ) -- Default tick time will be 3 because why not?
|
||
|
|
|
||
|
|
elseif k == "ticks_remain" then
|
||
|
|
if t.up then return t.remains / t.tick_time end
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "last_trigger" then
|
||
|
|
if state.combat > 0 then return max( 0, t.last_application - state.combat ) end
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "last_expire" then
|
||
|
|
if state.combat > 0 then return max( 0, t.last_expiry - state.combat ) end
|
||
|
|
return 0
|
||
|
|
|
||
|
|
else
|
||
|
|
if class.auras[ t.key ] and class.auras[ t.key ][ k ] ~= nil then
|
||
|
|
return class.auras[ t.key ][ k ]
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
Error( "UNK: buff." .. t.key .. "." .. k )
|
||
|
|
|
||
|
|
end,
|
||
|
|
|
||
|
|
newindex = function( t, k, v )
|
||
|
|
-- Prevent a fixed value from being entered if it is calculated by a meta function.
|
||
|
|
if t.meta and t.meta[ k ] then
|
||
|
|
return
|
||
|
|
end
|
||
|
|
class.knownAuraAttributes[ k ] = true
|
||
|
|
t[ k ] = v
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_default_buff = mt_default_buff
|
||
|
|
|
||
|
|
|
||
|
|
local unknown_buff = setmetatable( {
|
||
|
|
key = "unknown_buff",
|
||
|
|
name = "No Name",
|
||
|
|
count = 0,
|
||
|
|
lastCount = 0,
|
||
|
|
lastApplied = 0,
|
||
|
|
duration = 30,
|
||
|
|
expires = 0,
|
||
|
|
applied = 0,
|
||
|
|
caster = "nobody",
|
||
|
|
timeMod = 1,
|
||
|
|
v1 = 0,
|
||
|
|
v2 = 0,
|
||
|
|
v3 = 0,
|
||
|
|
unit = "player"
|
||
|
|
}, mt_default_buff )
|
||
|
|
|
||
|
|
|
||
|
|
-- This will currently accept any key and make an honest effort to find the buff on the player.
|
||
|
|
-- Unfortunately, that means a buff.dog_farts.up check will actually get a return value.
|
||
|
|
|
||
|
|
-- Fullscan definitely needs revamping, but it works for now.
|
||
|
|
local mt_buffs = {
|
||
|
|
-- The aura doesn't exist in our table so check the real game state, -- and copy it so we don't have to use the API next time.
|
||
|
|
__index = function( t, k )
|
||
|
|
if k == "__scanned" then
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
|
||
|
|
local aura = class.auras[ k ]
|
||
|
|
|
||
|
|
if not aura then
|
||
|
|
return unknown_buff
|
||
|
|
end
|
||
|
|
|
||
|
|
if k ~= aura.key then
|
||
|
|
t[ aura.key ] = rawget( t, aura.key ) or {
|
||
|
|
key = aura.key,
|
||
|
|
name = aura.name
|
||
|
|
}
|
||
|
|
t[ k ] = t[ aura.key ]
|
||
|
|
else
|
||
|
|
t[k] = {
|
||
|
|
key = aura.key,
|
||
|
|
name = aura.name
|
||
|
|
}
|
||
|
|
end
|
||
|
|
|
||
|
|
if aura.generate then
|
||
|
|
for attr, a_val in pairs( default_buff_values ) do
|
||
|
|
t[ k ][ attr ] = rawget( t[ k ], attr ) or a_val
|
||
|
|
end
|
||
|
|
aura.generate( t[ k ], "buff" )
|
||
|
|
return t[ k ]
|
||
|
|
end
|
||
|
|
|
||
|
|
local real = auras.player.buff[ k ] or auras.target.buff[ k ]
|
||
|
|
|
||
|
|
local buff = t[k]
|
||
|
|
|
||
|
|
if real then
|
||
|
|
buff.name = real.name
|
||
|
|
buff.count = real.count
|
||
|
|
buff.lastCount = real.lastCount or 0
|
||
|
|
buff.lastApplied = real.lastApplied or 0
|
||
|
|
buff.duration = real.duration
|
||
|
|
buff.expires = real.expires
|
||
|
|
buff.applied = max( 0, real.expires - real.duration )
|
||
|
|
buff.caster = real.caster
|
||
|
|
buff.id = real.id
|
||
|
|
buff.timeMod = real.timeMod
|
||
|
|
buff.v1 = real.v1
|
||
|
|
buff.v2 = real.v2
|
||
|
|
buff.v3 = real.v3
|
||
|
|
|
||
|
|
buff.unit = real.unit
|
||
|
|
end
|
||
|
|
|
||
|
|
return t[ k ]
|
||
|
|
|
||
|
|
end,
|
||
|
|
|
||
|
|
__newindex = function( t, k, v )
|
||
|
|
local aura = class.auras[ k ]
|
||
|
|
|
||
|
|
if aura and aura.alias then
|
||
|
|
rawset( t, k, setmetatable( v, mt_alias_buff ) )
|
||
|
|
return
|
||
|
|
end
|
||
|
|
|
||
|
|
rawset( t, k, setmetatable( v, mt_default_buff ) )
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_buffs = mt_buffs
|
||
|
|
|
||
|
|
|
||
|
|
local mt_default_talent = {
|
||
|
|
__index = function( t, k )
|
||
|
|
if k == "i_enabled" or k == "rank" then return t.enabled and 1 or 0 end
|
||
|
|
return k
|
||
|
|
end,
|
||
|
|
}
|
||
|
|
ns.metatables.mt_default_talent = mt_default_talent
|
||
|
|
|
||
|
|
|
||
|
|
local null_talent = setmetatable( {
|
||
|
|
enabled = false,
|
||
|
|
}, mt_default_talent )
|
||
|
|
ns.metatables.null_talent = null_talent
|
||
|
|
|
||
|
|
|
||
|
|
local mt_talents = {
|
||
|
|
__index = function( t, k )
|
||
|
|
return ( null_talent )
|
||
|
|
end,
|
||
|
|
|
||
|
|
__newindex = function( t, k, v )
|
||
|
|
if type( v ) == "table" then
|
||
|
|
rawset( t, k, setmetatable( v, mt_default_talent ) )
|
||
|
|
return
|
||
|
|
end
|
||
|
|
rawset( t, k, v )
|
||
|
|
end,
|
||
|
|
}
|
||
|
|
ns.metatables.mt_talents = mt_talents
|
||
|
|
|
||
|
|
|
||
|
|
local function IslandPvP()
|
||
|
|
local _, instanceType, difficulty = GetInstanceInfo()
|
||
|
|
return instanceType == "scenario" and difficulty == 45
|
||
|
|
end
|
||
|
|
|
||
|
|
local mt_default_pvptalent = {
|
||
|
|
__index = function( t, k )
|
||
|
|
local enlisted = state.bg or state.arena or state.buff.enlisted.up or IslandPvP()
|
||
|
|
|
||
|
|
if k == "enabled" then return enlisted and t._enabled or false
|
||
|
|
elseif k == "_enabled" then return false
|
||
|
|
elseif k == "i_enabled" or k == "rank" then return ( enlisted and t._enabled ) and 1 or 0 end
|
||
|
|
|
||
|
|
return k
|
||
|
|
end,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
local null_pvptalent = setmetatable( {
|
||
|
|
_enabled = false
|
||
|
|
}, mt_default_pvptalent )
|
||
|
|
|
||
|
|
|
||
|
|
local mt_pvptalents = {
|
||
|
|
__index = function( t, k )
|
||
|
|
return null_pvptalent
|
||
|
|
end,
|
||
|
|
|
||
|
|
__newindex = function( t, k, v )
|
||
|
|
rawset( t, k, setmetatable( v, mt_default_pvptalent ) )
|
||
|
|
end,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
do
|
||
|
|
local mt_default_gen_trait = {
|
||
|
|
__index = function( t, k )
|
||
|
|
if k == "enabled" or k == "minor" or k == "equipped" then
|
||
|
|
return t.rank and t.rank > 0
|
||
|
|
elseif k == "time_value" then
|
||
|
|
local mod = t.mod
|
||
|
|
|
||
|
|
if mod >= 1000 or mod <= -1000 then
|
||
|
|
return mod / 1000
|
||
|
|
end
|
||
|
|
|
||
|
|
return mod
|
||
|
|
elseif k == "disabled" then
|
||
|
|
return not t.rank or t.rank == 0
|
||
|
|
end
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_default_gen_trait = mt_default_gen_trait
|
||
|
|
|
||
|
|
local mt_generic_traits = {
|
||
|
|
__index = function( t, k )
|
||
|
|
return t.no_trait
|
||
|
|
end,
|
||
|
|
|
||
|
|
__newindex = function( t, k, v )
|
||
|
|
rawset( t, k, setmetatable( v, mt_default_gen_trait ) )
|
||
|
|
return t[ k ]
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_generic_traits = mt_generic_traits
|
||
|
|
|
||
|
|
setmetatable( state.corruptions, mt_generic_traits )
|
||
|
|
state.corruptions.no_trait = { rank = 0 }
|
||
|
|
|
||
|
|
setmetatable( state.legendary, mt_generic_traits )
|
||
|
|
state.legendary.no_trait = { rank = 0 }
|
||
|
|
|
||
|
|
-- Azerite and Essences.
|
||
|
|
local mt_default_trait = {
|
||
|
|
__index = function( t, k )
|
||
|
|
local heart = C_AzeriteItem.FindActiveAzeriteItem()
|
||
|
|
|
||
|
|
heart = heart and heart:IsValid() and C_AzeriteItem.IsAzeriteItemEnabled( heart ) or false
|
||
|
|
|
||
|
|
if k == "enabled" or k == "minor" or k == "equipped" then
|
||
|
|
return heart and t.__rank and t.__rank > 0
|
||
|
|
elseif k == "disabled" then
|
||
|
|
return not heart or not t.__rank or t.__rank == 0
|
||
|
|
elseif k == "rank" then
|
||
|
|
return heart and t.__rank or 0
|
||
|
|
elseif k == "major" then
|
||
|
|
return heart and t.__major or false
|
||
|
|
elseif k == "minor" then
|
||
|
|
return heart and t.__minor or false
|
||
|
|
end
|
||
|
|
end
|
||
|
|
}
|
||
|
|
|
||
|
|
local mt_artifact_traits = {
|
||
|
|
__index = function( t, k )
|
||
|
|
return t.no_trait
|
||
|
|
end,
|
||
|
|
|
||
|
|
__newindex = function( t, k, v )
|
||
|
|
rawset( t, k, setmetatable( v, mt_default_trait ) )
|
||
|
|
return t[ k ]
|
||
|
|
end
|
||
|
|
}
|
||
|
|
|
||
|
|
ns.metatables.mt_artifact_traits = mt_artifact_traits
|
||
|
|
|
||
|
|
setmetatable( state.azerite, mt_artifact_traits )
|
||
|
|
state.azerite.no_trait = { rank = 0 }
|
||
|
|
state.artifact = state.azerite
|
||
|
|
|
||
|
|
setmetatable( state.essence, ns.metatables.mt_artifact_traits )
|
||
|
|
state.essence.no_trait = { rank = 0, major = false, minor = false }
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
do
|
||
|
|
local db = scripts.DB
|
||
|
|
|
||
|
|
-- Args table, make it nicer.
|
||
|
|
setmetatable( state.args, {
|
||
|
|
__index = function( t, k )
|
||
|
|
-- No script selected.
|
||
|
|
if not state.scriptID then return end
|
||
|
|
|
||
|
|
local script = db[ state.scriptID ]
|
||
|
|
|
||
|
|
-- No script by that name.
|
||
|
|
if not script then return end
|
||
|
|
|
||
|
|
-- Script has no modifiers.
|
||
|
|
if not script.Modifiers then return end
|
||
|
|
|
||
|
|
local mod = script.Modifiers[ k ]
|
||
|
|
|
||
|
|
if mod then
|
||
|
|
local s, val = pcall( mod )
|
||
|
|
if s then return val end
|
||
|
|
end
|
||
|
|
end,
|
||
|
|
} )
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
-- Table for counting active dots.
|
||
|
|
local mt_active_dot = {
|
||
|
|
__index = function(t, k)
|
||
|
|
local aura = class.auras[ k ]
|
||
|
|
|
||
|
|
if aura then
|
||
|
|
if rawget( t, aura.key ) then return t[ aura.key ] end
|
||
|
|
t[ k ] = ns.numDebuffs( aura.id )
|
||
|
|
return t[ k ]
|
||
|
|
|
||
|
|
else
|
||
|
|
return 0
|
||
|
|
|
||
|
|
end
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_active_dot = mt_active_dot
|
||
|
|
|
||
|
|
|
||
|
|
-- Table of default handlers for a totem. Under-implemented at the moment.
|
||
|
|
-- Needs review.
|
||
|
|
local mt_default_totem = {
|
||
|
|
__index = function(t, k)
|
||
|
|
if k == "expires" then
|
||
|
|
local _, name, start, duration = GetTotemInfo( t.totem )
|
||
|
|
|
||
|
|
t.name = name
|
||
|
|
t.expires = ( start or 0 ) + ( duration or 0 )
|
||
|
|
|
||
|
|
return t[ k ]
|
||
|
|
|
||
|
|
elseif k == "up" or k == "active" then
|
||
|
|
return ( t.expires > ( state.query_time ) )
|
||
|
|
|
||
|
|
elseif k == "remains" then
|
||
|
|
if t.expires > ( state.query_time ) then
|
||
|
|
return ( t.expires - ( state.query_time ) )
|
||
|
|
else
|
||
|
|
return 0
|
||
|
|
end
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
Error( "UNK: totem." .. name or "no_name" .. "." .. k )
|
||
|
|
end
|
||
|
|
}
|
||
|
|
Hekili.mt_default_totem = mt_default_totem
|
||
|
|
|
||
|
|
|
||
|
|
-- Table of totems. Currently Shaman-centric.
|
||
|
|
-- Needs review.
|
||
|
|
local mt_totem = {
|
||
|
|
__index = function(t, k)
|
||
|
|
if k == "fire" then
|
||
|
|
local _, name, start, duration = GetTotemInfo(1)
|
||
|
|
|
||
|
|
t[k] = {
|
||
|
|
key = k, totem = 1, name = name, expires = (start + duration) or 0, }
|
||
|
|
return t[k]
|
||
|
|
|
||
|
|
elseif k == "earth" then
|
||
|
|
local _, name, start, duration = GetTotemInfo(2)
|
||
|
|
|
||
|
|
t[k] = {
|
||
|
|
key = k, totem = 2, name = name, expires = (start + duration) or 0, }
|
||
|
|
return t[k]
|
||
|
|
|
||
|
|
elseif k == "water" then
|
||
|
|
local _, name, start, duration = GetTotemInfo(3)
|
||
|
|
|
||
|
|
t[k] = {
|
||
|
|
key = k, totem = 3, name = name, expires = (start + duration) or 0, }
|
||
|
|
return t[k]
|
||
|
|
|
||
|
|
elseif k == "air" then
|
||
|
|
local _, name, start, duration = GetTotemInfo(4)
|
||
|
|
|
||
|
|
t[k] = {
|
||
|
|
key = k, totem = 4, name = name, expires = (start + duration) or 0, }
|
||
|
|
return t[k]
|
||
|
|
end
|
||
|
|
|
||
|
|
if pet[ k ] ~= nil then return pet[ k ] end
|
||
|
|
|
||
|
|
Error( "UNK: totem." .. k )
|
||
|
|
end,
|
||
|
|
__newindex = function( t, k, v )
|
||
|
|
rawset( t, k, setmetatable( v, mt_default_totem ) )
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_totem = mt_totem
|
||
|
|
|
||
|
|
|
||
|
|
do
|
||
|
|
local db = {}
|
||
|
|
local cache = {}
|
||
|
|
local pathState = {}
|
||
|
|
|
||
|
|
state.varDB = db
|
||
|
|
-- state.varCache = cache
|
||
|
|
state.varPaths = pathState
|
||
|
|
|
||
|
|
|
||
|
|
local entryPool = {}
|
||
|
|
|
||
|
|
function state:RegisterVariable( key, scriptID, list, preconditions )
|
||
|
|
db[ key ] = db[ key ] or {}
|
||
|
|
local data = db[ key ]
|
||
|
|
|
||
|
|
cache[ key ] = cache[ key ] or {}
|
||
|
|
|
||
|
|
local fullPath = scriptID
|
||
|
|
|
||
|
|
local entry = remove( entryPool ) or {
|
||
|
|
mustPass = {},
|
||
|
|
-- mustFail = {}
|
||
|
|
}
|
||
|
|
|
||
|
|
entry.id = scriptID
|
||
|
|
entry.list = list
|
||
|
|
|
||
|
|
if preconditions then
|
||
|
|
for i, prereq in ipairs( preconditions ) do
|
||
|
|
local script = prereq.script
|
||
|
|
if script ~= 0 then
|
||
|
|
insert( entry.mustPass, script )
|
||
|
|
fullPath = fullPath .. "+" .. script
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
--[[ if preclusions then
|
||
|
|
for i, block in ipairs( preclusions ) do
|
||
|
|
local script = block.script
|
||
|
|
if script ~= 0 then
|
||
|
|
insert( entry.mustFail, script )
|
||
|
|
fullPath = fullPath .. "-" .. script
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end ]]
|
||
|
|
|
||
|
|
entry.fullPath = fullPath
|
||
|
|
insert( data, entry )
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state:PurgeListVariables( list )
|
||
|
|
for variable, data in pairs( db ) do
|
||
|
|
for i = #data, 1, -1 do
|
||
|
|
if data[ i ].list == list then
|
||
|
|
local item = remove( data, i )
|
||
|
|
wipe( item.mustPass )
|
||
|
|
insert( entryPool, item )
|
||
|
|
wipe( cache[ variable ] )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state:ResetVariables()
|
||
|
|
for k, v in pairs( db ) do
|
||
|
|
for i = #v, 1, -1 do
|
||
|
|
local x = remove( v, i )
|
||
|
|
wipe( x.mustPass )
|
||
|
|
-- wipe( x.mustFail )
|
||
|
|
insert( entryPool, x )
|
||
|
|
end
|
||
|
|
wipe( cache[ k ] )
|
||
|
|
wipe( self.variable )
|
||
|
|
end
|
||
|
|
|
||
|
|
wipe( pathState )
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state:GetVariableIDs( key )
|
||
|
|
return db[ key ]
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
state.variable = setmetatable( {}, {
|
||
|
|
__index = function( t, var )
|
||
|
|
local debug = Hekili.ActiveDebug
|
||
|
|
|
||
|
|
if class.variables[ var ] then
|
||
|
|
-- We have a hardcoded shortcut.
|
||
|
|
if debug then Hekili:Debug( "Using class var '%s'.", var ) end
|
||
|
|
return class.variables[ var ]()
|
||
|
|
end
|
||
|
|
|
||
|
|
local now = state.query_time
|
||
|
|
|
||
|
|
if Hekili.LoadingScripts then
|
||
|
|
return 0
|
||
|
|
end
|
||
|
|
|
||
|
|
if not db[ var ] then
|
||
|
|
if debug then Hekili:Debug( "No such variable '%s'.", var ) end
|
||
|
|
Hekili:Error( "Variable '%s' referenced in %s but is undefined.", var, state.scriptID )
|
||
|
|
return 0
|
||
|
|
end
|
||
|
|
|
||
|
|
state.variable[ var ] = 0
|
||
|
|
|
||
|
|
local data = db[ var ]
|
||
|
|
local parent = state.scriptID
|
||
|
|
|
||
|
|
-- If we're checking variable with no script loaded, don't bother.
|
||
|
|
if not parent or parent == "NilScriptID" then return 0 end
|
||
|
|
|
||
|
|
local default = 0
|
||
|
|
local value = 0
|
||
|
|
|
||
|
|
local which_mod = "value"
|
||
|
|
|
||
|
|
for i, entry in ipairs( data ) do
|
||
|
|
local scriptID = entry.id
|
||
|
|
local currPath = entry.fullPath .. ":" .. now
|
||
|
|
|
||
|
|
-- Check the requirements/exclusions in the APL stack.
|
||
|
|
if pathState[ currPath ] == nil then
|
||
|
|
pathState[ currPath ] = true
|
||
|
|
|
||
|
|
for r, prereq in ipairs( entry.mustPass ) do
|
||
|
|
state.scriptID = prereq
|
||
|
|
if not scripts:CheckScript( prereq ) then
|
||
|
|
pathState[ currPath ] = false
|
||
|
|
break
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
--[[ if pathState[ currPath ] then
|
||
|
|
for e, excl in ipairs( entry.mustFail ) do
|
||
|
|
state.scriptID = excl
|
||
|
|
if scripts:CheckScript( excl ) then
|
||
|
|
pathState[ currPath ] = false
|
||
|
|
break
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end ]]
|
||
|
|
end
|
||
|
|
|
||
|
|
if pathState[ currPath ] then
|
||
|
|
local pathKey = currPath .. "-" .. i
|
||
|
|
|
||
|
|
if cache[ var ][ pathKey ] ~= nil then
|
||
|
|
value = cache[ var ][ pathKey ]
|
||
|
|
|
||
|
|
else
|
||
|
|
state.scriptID = scriptID
|
||
|
|
local op = state.args.op or "set"
|
||
|
|
|
||
|
|
local passed = scripts:CheckScript( scriptID )
|
||
|
|
|
||
|
|
--[[ add = "Add Value",
|
||
|
|
ceil
|
||
|
|
x default = "Set Default Value",
|
||
|
|
div = "Divide Value",
|
||
|
|
floor
|
||
|
|
max = "Maximum Value",
|
||
|
|
min = "Minimum Value",
|
||
|
|
mod = "Modulo Value",
|
||
|
|
mul = "Multiply Value",
|
||
|
|
pow = "Raise Value to X Power",
|
||
|
|
x reset = "Reset to Default",
|
||
|
|
x set = "Set Value",
|
||
|
|
x setif = "Set Value If...",
|
||
|
|
sub = "Subtract Value" ]]
|
||
|
|
|
||
|
|
if op == "set" or op == "setif" then
|
||
|
|
if passed then
|
||
|
|
local v1 = state.args.value
|
||
|
|
if v1 ~= nil then value = v1
|
||
|
|
else value = state.args.default end
|
||
|
|
else
|
||
|
|
local v2 = state.args.value_else
|
||
|
|
if v2 ~= nil then
|
||
|
|
value = v2
|
||
|
|
which_mod = "value_else"
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
elseif op == "reset" then
|
||
|
|
if passed then
|
||
|
|
local v = state.args.value
|
||
|
|
if v == nil then v = state.args.default end
|
||
|
|
if v == nil then v = 0 end
|
||
|
|
value = v
|
||
|
|
end
|
||
|
|
|
||
|
|
elseif passed then
|
||
|
|
-- Math Ops.
|
||
|
|
local currType = type( value )
|
||
|
|
|
||
|
|
if currType == "number" then
|
||
|
|
-- Operations on existing value.
|
||
|
|
if op == "floor" then
|
||
|
|
value = floor( value )
|
||
|
|
elseif op == "ceil" then
|
||
|
|
value = ceil( value )
|
||
|
|
else
|
||
|
|
-- Operations with two values.
|
||
|
|
local newVal = state.args.value
|
||
|
|
local valType = type( newVal )
|
||
|
|
|
||
|
|
if valType == "number" then
|
||
|
|
if op == "add" then
|
||
|
|
value = value + newVal
|
||
|
|
elseif op == "div" then
|
||
|
|
if newVal == 0 then value = 0
|
||
|
|
else value = value / newVal end
|
||
|
|
elseif op == "max" then
|
||
|
|
value = max( value, newVal )
|
||
|
|
elseif op == "min" then
|
||
|
|
value = min( value, newVal )
|
||
|
|
elseif op == "mod" then
|
||
|
|
if newVal == 0 then value = 0
|
||
|
|
else value = value % newVal end
|
||
|
|
elseif op == "mul" then
|
||
|
|
value = value * newVal
|
||
|
|
elseif op == "pow" then
|
||
|
|
value = value ^ newVal
|
||
|
|
elseif op == "sub" then
|
||
|
|
value = value - newVal
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Cache the value in case it is an intermediate value (i.e., multiple calculation steps).
|
||
|
|
state.variable[ var ] = value
|
||
|
|
cache[ var ][ pathKey ] = value
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Clear cache and clear the flag that we are checking this variable already.
|
||
|
|
state.variable[ var ] = nil
|
||
|
|
|
||
|
|
--[[ if debug then
|
||
|
|
Hekili:Debug( "Spent %.2fms calculating value of %s -- %s [%s].", debugprofilestop() - varStart, var, tostring( value ), parent )
|
||
|
|
end ]]
|
||
|
|
|
||
|
|
state.scriptID = parent
|
||
|
|
|
||
|
|
return value
|
||
|
|
end
|
||
|
|
} )
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
-- Table of set bonuses. Some string manipulation to honor the SimC syntax.
|
||
|
|
-- Currently returns 1 for true, 0 for false to be consistent with SimC conditionals.
|
||
|
|
-- Won't catch fake set names. Should revise.
|
||
|
|
local mt_set_bonuses = {
|
||
|
|
__index = function(t, k)
|
||
|
|
if type(k) == "number" then return 0 end
|
||
|
|
|
||
|
|
-- if ( not class.artifacts[ k ] ) and ( state.bg or state.arena ) then return 0 end
|
||
|
|
|
||
|
|
local set, pieces, class = k:match("^(.-)_"), tonumber( k:match("_(%d+)pc") ), k:match("pc(.-)$")
|
||
|
|
|
||
|
|
if not pieces or not set then
|
||
|
|
-- This wasn't a tier set bonus.
|
||
|
|
return 0
|
||
|
|
|
||
|
|
else
|
||
|
|
if class then set = set .. class end
|
||
|
|
|
||
|
|
if not t[set] then
|
||
|
|
return 0
|
||
|
|
end
|
||
|
|
|
||
|
|
return t[set] >= pieces and 1 or 0
|
||
|
|
end
|
||
|
|
|
||
|
|
return 0
|
||
|
|
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_set_bonuses = mt_set_bonuses
|
||
|
|
|
||
|
|
|
||
|
|
local mt_equipped = {
|
||
|
|
__index = function(t, k)
|
||
|
|
-- if not class.artifacts[ k ] and ( state.bg or state.arena ) then return false end
|
||
|
|
return state.set_bonus[k] > 0 or state.legendary[k].rank > 0 or state.corruptions[k].rank > 0
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_equipped = mt_equipped
|
||
|
|
|
||
|
|
|
||
|
|
-- Aliases let a single buff name refer to any of multiple buffs.
|
||
|
|
-- Developed mainly for RtB; it will also report "stack" or "count" as the sum of stacks of multiple buffs.
|
||
|
|
local mt_alias_debuff = {
|
||
|
|
__index = function( t, k )
|
||
|
|
local aura = class.auras[ t.key ]
|
||
|
|
local type = aura.aliasType or "debuff"
|
||
|
|
|
||
|
|
if aura.meta and aura.meta[ k ] then return aura.meta[ k ]() end
|
||
|
|
|
||
|
|
if k == "count" or k == "stack" or k == "stacks" then
|
||
|
|
local n = 0
|
||
|
|
|
||
|
|
for i, child in ipairs( aura.alias ) do
|
||
|
|
if state[ type ][ child ].up then n = n + max( 1, state[ type ][ child ].stack ) end
|
||
|
|
end
|
||
|
|
|
||
|
|
return n
|
||
|
|
end
|
||
|
|
|
||
|
|
local alias
|
||
|
|
local mode = aura.aliasMode or "first"
|
||
|
|
|
||
|
|
for i, v in ipairs( aura.alias ) do
|
||
|
|
local child = state[ type ][ v ]
|
||
|
|
if not alias and mode == "first" and child.up then return child[ k ] end
|
||
|
|
|
||
|
|
if child.up then
|
||
|
|
if mode == "shortest" and ( not alias or child.remains < alias.remains ) then alias = child
|
||
|
|
elseif mode == "longest" and ( not alias or child.remains > alias.remains ) then alias = child end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if alias then return alias[ k ]
|
||
|
|
else return state[ type ][ aura.alias[1] ][ k ] end
|
||
|
|
end
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
local default_debuff_values = {
|
||
|
|
name = "no_name",
|
||
|
|
count = 0,
|
||
|
|
lastCount = 0,
|
||
|
|
lastApplied = 0,
|
||
|
|
expires = 0,
|
||
|
|
applied = 0,
|
||
|
|
duration = 15,
|
||
|
|
caster = "nobody",
|
||
|
|
timeMod = 1,
|
||
|
|
v1 = 0,
|
||
|
|
v2 = 0,
|
||
|
|
v3 = 0,
|
||
|
|
unit = "target"
|
||
|
|
}
|
||
|
|
|
||
|
|
local cycle_debuff = {
|
||
|
|
name = "cycle",
|
||
|
|
count = 0,
|
||
|
|
lastCount = 0,
|
||
|
|
lastApplied = 0,
|
||
|
|
expires = 0,
|
||
|
|
applied = 0,
|
||
|
|
duration = 0,
|
||
|
|
caster = "nobody",
|
||
|
|
timeMod = 1,
|
||
|
|
v1 = 0,
|
||
|
|
v2 = 0,
|
||
|
|
v3 = 0,
|
||
|
|
unit = "target",
|
||
|
|
|
||
|
|
down = true,
|
||
|
|
i_up = 0,
|
||
|
|
rank = 0,
|
||
|
|
react = 0,
|
||
|
|
refreshable = true,
|
||
|
|
remains = 0,
|
||
|
|
stack = 0,
|
||
|
|
stack_pct = 0,
|
||
|
|
tick_time_remains = 0,
|
||
|
|
ticking = false,
|
||
|
|
ticks = 0,
|
||
|
|
ticks_remain = 0,
|
||
|
|
time_to_refresh = 0,
|
||
|
|
up = false,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
-- Table of default handlers for debuffs.
|
||
|
|
-- Needs review.
|
||
|
|
local mt_default_debuff = {
|
||
|
|
mtID = "default_debuff",
|
||
|
|
|
||
|
|
__index = function( t, k )
|
||
|
|
local aura = class.auras[ t.key ]
|
||
|
|
|
||
|
|
if state.IsCycling( t.key, true ) and cycle_debuff[ k ] ~= nil then
|
||
|
|
return cycle_debuff[ k ]
|
||
|
|
end
|
||
|
|
|
||
|
|
if aura and rawget( aura, "meta" ) and aura.meta[ k ] then
|
||
|
|
return aura.meta[ k ]( t, "debuff" )
|
||
|
|
|
||
|
|
elseif requiresLookup[ k ] then
|
||
|
|
if aura and aura.generate then
|
||
|
|
for attr, a_val in pairs( default_debuff_values ) do
|
||
|
|
t[ attr ] = rawget( t, attr ) or rawget( aura, attr ) or a_val
|
||
|
|
end
|
||
|
|
|
||
|
|
aura.generate( t, "debuff" )
|
||
|
|
t.id = aura and aura.id or t.key
|
||
|
|
|
||
|
|
return rawget( t, k )
|
||
|
|
end
|
||
|
|
|
||
|
|
local real = auras.target.debuff[ t.key ] or auras.player.debuff[ t.key ]
|
||
|
|
|
||
|
|
if real then
|
||
|
|
t.name = real.name or t.key
|
||
|
|
t.count = real.count
|
||
|
|
t.lastCount = real.lastCount or 0
|
||
|
|
t.lastApplied = real.lastApplied or 0
|
||
|
|
t.duration = real.duration
|
||
|
|
t.expires = real.expires or 0
|
||
|
|
t.applied = max( 0, real.expires - real.duration )
|
||
|
|
t.caster = real.caster
|
||
|
|
t.id = real.id
|
||
|
|
t.timeMod = real.timeMod
|
||
|
|
t.v1 = real.v1
|
||
|
|
t.v2 = real.v2
|
||
|
|
t.v3 = real.v3
|
||
|
|
|
||
|
|
t.unit = real.unit
|
||
|
|
else
|
||
|
|
for attr, a_val in pairs( default_debuff_values ) do
|
||
|
|
t[ attr ] = aura and aura[ attr ] or a_val
|
||
|
|
end
|
||
|
|
|
||
|
|
t.id = aura and aura.id or t.id
|
||
|
|
end
|
||
|
|
|
||
|
|
return rawget( t, k )
|
||
|
|
|
||
|
|
elseif k == "up" or k == "ticking" then
|
||
|
|
return t.remains > 0
|
||
|
|
|
||
|
|
elseif k == "i_up" or k == "rank" then
|
||
|
|
return t.up and 1 or 0
|
||
|
|
|
||
|
|
elseif k == "down" then
|
||
|
|
return not t.up
|
||
|
|
|
||
|
|
elseif k == "remains" then
|
||
|
|
return max( 0, t.expires - state.query_time )
|
||
|
|
|
||
|
|
elseif k == "refreshable" then
|
||
|
|
-- if state.isCyclingTargets( nil, t.key ) then return true end
|
||
|
|
return t.remains < 0.3 * ( aura and aura.duration or t.duration or 30 )
|
||
|
|
|
||
|
|
elseif k == "time_to_refresh" then
|
||
|
|
-- if state.isCyclingTargets( nil, t.key ) then return 0 end
|
||
|
|
return t.up and ( max( 0, 0.01 + state.query_time - ( 0.3 * ( aura and aura.duration or t.duration or 30 ) ) ) ) or 0
|
||
|
|
|
||
|
|
elseif k == "stack" then
|
||
|
|
-- if state.isCyclingTargets( nil, t.key ) then return 0 end
|
||
|
|
if t.up then return ( t.count ) else return 0 end
|
||
|
|
|
||
|
|
elseif k == "react" then
|
||
|
|
if t.expires > state.query_time then
|
||
|
|
return t.count
|
||
|
|
end
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "max_stack" or k == "max_stacks" then
|
||
|
|
return aura and aura.max_stack or 1
|
||
|
|
|
||
|
|
elseif k == "stack_pct" then
|
||
|
|
if t.up then
|
||
|
|
if aura then aura.max_stack = max( aura.max_stack or 1, t.count ) end
|
||
|
|
return ( 100 * t.count / aura and aura.max_stack or t.count )
|
||
|
|
end
|
||
|
|
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "value" then
|
||
|
|
return t.v1
|
||
|
|
|
||
|
|
elseif k == "stack_value" then
|
||
|
|
return t.v1 * t.stack
|
||
|
|
|
||
|
|
elseif k == "pmultiplier" then
|
||
|
|
-- Persistent modifier, used by Druids.
|
||
|
|
return ns.getModifier( aura.id, state.target.unit )
|
||
|
|
|
||
|
|
elseif k == "ticks" then
|
||
|
|
if t.up then return t.duration / t.tick_time - t.ticks_remain end
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "tick_time" then
|
||
|
|
return aura.tick_time or ( 3 * state.haste )
|
||
|
|
|
||
|
|
elseif k == "ticks_remain" then
|
||
|
|
return t.remains / t.tick_time
|
||
|
|
|
||
|
|
elseif k == "tick_time_remains" then
|
||
|
|
if not aura.tick_time then return t.remains end
|
||
|
|
return t.remains % aura.tick_time
|
||
|
|
|
||
|
|
else
|
||
|
|
if aura and aura[ k ] ~= nil then
|
||
|
|
return aura[ k ]
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
Error ( "UNK: debuff." .. t.key .. "." .. k )
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_default_debuff = mt_default_debuff
|
||
|
|
|
||
|
|
|
||
|
|
local unknown_debuff = setmetatable( {
|
||
|
|
count = 0,
|
||
|
|
expires = 0,
|
||
|
|
timeMod = 1,
|
||
|
|
v1 = 0,
|
||
|
|
v2 = 0,
|
||
|
|
v3 = 0
|
||
|
|
}, mt_default_debuff )
|
||
|
|
|
||
|
|
|
||
|
|
-- Table of debuffs applied to the target by the player.
|
||
|
|
-- Needs review.
|
||
|
|
local mt_debuffs = {
|
||
|
|
-- The debuff/ doesn't exist in our table so check the real game state, -- and copy it so we don't have to use the API next time.
|
||
|
|
__index = function( t, k )
|
||
|
|
local aura = class.auras[ k ]
|
||
|
|
|
||
|
|
if aura then
|
||
|
|
if k ~= aura.key then
|
||
|
|
t[ aura.key ] = rawget( t, aura.key ) or {
|
||
|
|
key = aura.key,
|
||
|
|
name = aura.name
|
||
|
|
}
|
||
|
|
t[ k ] = t[ aura.key ]
|
||
|
|
else
|
||
|
|
t[ k ] = {
|
||
|
|
key = aura.key,
|
||
|
|
name = aura.name
|
||
|
|
}
|
||
|
|
end
|
||
|
|
|
||
|
|
if aura.generate then
|
||
|
|
for attr, a_val in pairs( default_debuff_values ) do
|
||
|
|
t[ k ][ attr ] = rawget( t[ k ], attr ) or a_val
|
||
|
|
end
|
||
|
|
aura.generate( t[ k ], "debuff" )
|
||
|
|
return t[ k ]
|
||
|
|
end
|
||
|
|
|
||
|
|
else
|
||
|
|
t[ k ] = {
|
||
|
|
key = k,
|
||
|
|
name = k,
|
||
|
|
id = k
|
||
|
|
}
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
local real = auras.player.debuff[ k ] or auras.target.debuff[ k ]
|
||
|
|
local debuff = t[k]
|
||
|
|
|
||
|
|
if real then
|
||
|
|
debuff.name = real.name
|
||
|
|
debuff.count = real.count
|
||
|
|
debuff.lastCount = real.lastCount or 0
|
||
|
|
debuff.lastApplied = real.lastApplied or 0
|
||
|
|
debuff.duration = real.duration
|
||
|
|
debuff.expires = real.expires
|
||
|
|
debuff.applied = max( 0, real.expires - real.duration )
|
||
|
|
debuff.caster = real.caster
|
||
|
|
debuff.id = real.id
|
||
|
|
debuff.timeMod = real.timeMod
|
||
|
|
debuff.v1 = real.v1
|
||
|
|
debuff.v2 = real.v2
|
||
|
|
debuff.v3 = real.v3
|
||
|
|
|
||
|
|
debuff.unit = real.unit
|
||
|
|
|
||
|
|
else
|
||
|
|
debuff.name = aura and aura.name or "No Name"
|
||
|
|
debuff.count = 0
|
||
|
|
debuff.lastCount = 0
|
||
|
|
debuff.lastApplied = 0
|
||
|
|
debuff.duration = aura and aura.duration or 30
|
||
|
|
debuff.expires = 0
|
||
|
|
debuff.applied = 0
|
||
|
|
debuff.caster = "nobody"
|
||
|
|
-- debuff.id = nil
|
||
|
|
debuff.timeMod = 1
|
||
|
|
debuff.v1 = 0
|
||
|
|
debuff.v2 = 0
|
||
|
|
debuff.v3 = 0
|
||
|
|
|
||
|
|
debuff.unit = aura and aura.unit or "player"
|
||
|
|
end
|
||
|
|
|
||
|
|
t[k] = debuff
|
||
|
|
return t[ k ]
|
||
|
|
end,
|
||
|
|
|
||
|
|
__newindex = function( t, k, v )
|
||
|
|
local aura = class.auras[ k ]
|
||
|
|
|
||
|
|
if aura and aura.alias then
|
||
|
|
rawset( t, k, setmetatable( v, mt_alias_debuff ) )
|
||
|
|
return
|
||
|
|
end
|
||
|
|
|
||
|
|
rawset( t, k, setmetatable( v, mt_default_debuff ) )
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_debuffs = mt_debuffs
|
||
|
|
|
||
|
|
|
||
|
|
-- Table of default handlers for actions.
|
||
|
|
-- Needs review.
|
||
|
|
local mt_default_action = {
|
||
|
|
__index = function( t, k )
|
||
|
|
local ability = t.action and class.abilities[ t.action ]
|
||
|
|
local aura = ability and ability.aura or t.action
|
||
|
|
|
||
|
|
if k == "enabled" or k == "known" then
|
||
|
|
return state:IsKnown( t.action )
|
||
|
|
|
||
|
|
elseif k == "disabled" then
|
||
|
|
return state:IsDisabled( t.action )
|
||
|
|
|
||
|
|
elseif k == "gcd" then
|
||
|
|
local queued_action = state.this_action
|
||
|
|
state.this_action = t.action
|
||
|
|
|
||
|
|
local value = state.gcd.execute
|
||
|
|
state.this_action = queued_action
|
||
|
|
|
||
|
|
return value
|
||
|
|
|
||
|
|
elseif k == "execute_time" then
|
||
|
|
local queued_action = state.this_action
|
||
|
|
state.this_action = t.action
|
||
|
|
|
||
|
|
local value = state.gcd.execute
|
||
|
|
state.this_action = queued_action
|
||
|
|
|
||
|
|
return max( value, t.cast_time )
|
||
|
|
|
||
|
|
elseif k == "charges" then
|
||
|
|
return ability.charges and state.cooldown[ t.action ].charges or 0
|
||
|
|
|
||
|
|
elseif k == "charges_fractional" then
|
||
|
|
return state.cooldown[ t.action ].charges_fractional
|
||
|
|
|
||
|
|
elseif k == "recharge_time" then
|
||
|
|
return state.cooldown[ t.action ].recharge_time
|
||
|
|
|
||
|
|
elseif k == "max_charges" then
|
||
|
|
return ability.charges or 0
|
||
|
|
|
||
|
|
elseif k == "time_to_max_charges" or k == "full_recharge_time" then
|
||
|
|
return ( ( ability.charges or 1 ) - state.cooldown[ t.action ].charges_fractional ) * ( ability.recharge or ability.cooldown )
|
||
|
|
|
||
|
|
elseif k == "ready_time" then
|
||
|
|
return state:IsUsable( t.action ) and state:TimeToReady( t.action ) or 999
|
||
|
|
|
||
|
|
elseif k == "ready" then
|
||
|
|
return state:IsUsable( t.action ) and state:IsReady( t.action )
|
||
|
|
|
||
|
|
elseif k == "cast_time" then
|
||
|
|
return ability.cast
|
||
|
|
|
||
|
|
elseif k == "cooldown" then
|
||
|
|
return ability.cooldown
|
||
|
|
|
||
|
|
elseif k == "damage" then
|
||
|
|
return ability.damage or 1
|
||
|
|
|
||
|
|
elseif k == "crit_pct_current" then
|
||
|
|
return ability.critical or state.stat.crit
|
||
|
|
|
||
|
|
elseif k == "ticking" then
|
||
|
|
return ( state.dot[ aura ].ticking )
|
||
|
|
|
||
|
|
elseif k == "ticks" then
|
||
|
|
return 1 + ( state.dot[ aura ].duration or ( 30 * state.haste ) / class.auras[ aura ].tick_time or ( 3 * state.haste ) ) - t.ticks_remain
|
||
|
|
|
||
|
|
elseif k == "ticks_remain" then
|
||
|
|
return state.dot[ aura ].remains / ( class.auras[ aura ].tick_time or ( 3 * state.haste ) )
|
||
|
|
|
||
|
|
elseif k == "remains" then
|
||
|
|
return ( state.dot[ aura ].remains )
|
||
|
|
|
||
|
|
elseif k == "tick_time" then
|
||
|
|
return class.auras[ aura ].tick_time or ( 3 * state.haste )
|
||
|
|
|
||
|
|
elseif k == "travel_time" then
|
||
|
|
-- NYI: maybe capture the last travel time for the spell and use that?
|
||
|
|
local v = ability.velocity
|
||
|
|
|
||
|
|
if v and v > 0 then return state.target.maxR / v end
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "miss_react" then
|
||
|
|
return false
|
||
|
|
|
||
|
|
elseif k == "cooldown_react" then
|
||
|
|
return false
|
||
|
|
|
||
|
|
elseif k == "cast_delay" then
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "cast_regen" then
|
||
|
|
return floor( max( state.gcd.execute, t.cast_time ) * state[ class.primaryResource ].regen ) - t.cost
|
||
|
|
|
||
|
|
elseif k == "cost" then
|
||
|
|
if ability then
|
||
|
|
local c = ability.cost
|
||
|
|
|
||
|
|
if c then return c end
|
||
|
|
|
||
|
|
c = ability.spend
|
||
|
|
|
||
|
|
if c and c > 0 and c < 1 then
|
||
|
|
c = c * state[ ability.spendType or class.primaryResource ].modmax
|
||
|
|
end
|
||
|
|
|
||
|
|
return c or 0
|
||
|
|
end
|
||
|
|
|
||
|
|
return 0
|
||
|
|
|
||
|
|
elseif k == "cost_type" then
|
||
|
|
local a, _ = ability.spendType
|
||
|
|
if type( a ) == "string" then return a end
|
||
|
|
|
||
|
|
a = ability.spend
|
||
|
|
if type( a ) == "function" then _, a = a() end
|
||
|
|
if type( a ) == "string" then return a end
|
||
|
|
return class.primaryResource
|
||
|
|
|
||
|
|
elseif k == "in_flight" then
|
||
|
|
if ability and ability.flightTime then
|
||
|
|
return ability.lastCast + ability.flightTime > state.query_time
|
||
|
|
end
|
||
|
|
return state:IsInFlight( t.action )
|
||
|
|
|
||
|
|
elseif k == "in_flight_remains" then
|
||
|
|
if ability and ability.flightTime then
|
||
|
|
return max( 0, ability.lastCast + ability.flightTime - state.query_time )
|
||
|
|
end
|
||
|
|
return state:InFlightRemains( t.action )
|
||
|
|
|
||
|
|
elseif k == "channeling" then
|
||
|
|
return state:IsChanneling( t.action )
|
||
|
|
|
||
|
|
elseif k == "channel_remains" then
|
||
|
|
return state:IsChanneling( t.action ) and state:QueuedCastRemains( t.action ) or 0
|
||
|
|
|
||
|
|
elseif k == "executing" then
|
||
|
|
return state:IsCasting( t.action ) or ( state.prev[ 1 ][ t.action ] and state.gcd.remains > 0 )
|
||
|
|
|
||
|
|
elseif k == "execute_remains" then
|
||
|
|
return ( state:IsCasting( t.action ) and max( state:QueuedCastRemains( t.action ), state.gcd.remains ) ) or ( state.prev[1][ t.action ] and state.gcd.remains ) or 0
|
||
|
|
|
||
|
|
elseif k == "last_used" then
|
||
|
|
return state.combat > 0 and max( 0, ability.lastCast - state.combat ) or 0
|
||
|
|
|
||
|
|
elseif k == "in_range" then
|
||
|
|
if UnitExists( "target" ) and UnitCanAttack( "player", "target" ) and LSR.IsSpellInRange( ability.rangeSpell or ability.id, "target" ) == 0 then
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
|
||
|
|
return true
|
||
|
|
|
||
|
|
else
|
||
|
|
local val = ability[ k ]
|
||
|
|
|
||
|
|
if val ~= nil then
|
||
|
|
if type( val ) == "function" then return val() end
|
||
|
|
return val
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
return 0
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_default_action = mt_default_action
|
||
|
|
|
||
|
|
|
||
|
|
-- mt_actions: provides action information for display/priority queue/action criteria.
|
||
|
|
-- NYI.
|
||
|
|
local mt_actions = {
|
||
|
|
__index = function(t, k)
|
||
|
|
local action = class.abilities[ k ]
|
||
|
|
|
||
|
|
-- Need a null_action table.
|
||
|
|
if not action then return nil end
|
||
|
|
|
||
|
|
t[k] = {
|
||
|
|
action = k,
|
||
|
|
name = action.name,
|
||
|
|
gcdType = action.gcd
|
||
|
|
}
|
||
|
|
|
||
|
|
local h = state.haste
|
||
|
|
state.haste = 0
|
||
|
|
t[k].base_cast = action.cast
|
||
|
|
state.haste = h
|
||
|
|
|
||
|
|
return ( t[k] )
|
||
|
|
end, __newindex = function(t, k, v)
|
||
|
|
rawset( t, k, setmetatable( v, mt_default_action ) )
|
||
|
|
end
|
||
|
|
}
|
||
|
|
ns.metatables.mt_actions = mt_actions
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
-- mt_swings: used for projecting weapon swing-based resource gains.
|
||
|
|
local mt_swings = {
|
||
|
|
__index = function( t, k )
|
||
|
|
if k == "mainhand" then
|
||
|
|
return t.mh_pseudo and t.mh_pseudo or t.mh_actual
|
||
|
|
|
||
|
|
elseif k == "offhand" then
|
||
|
|
return t.oh_pseudo and t.oh_pseudo or t.oh_actual
|
||
|
|
|
||
|
|
elseif k == "mainhand_speed" then
|
||
|
|
return t.mh_pseudo_speed and t.mh_pseudo_speed or t.mh_speed
|
||
|
|
|
||
|
|
elseif k == "offhand_speed" then
|
||
|
|
return t.oh_pseudo_speed and t.oh_pseudo_speed or t.oh_speed
|
||
|
|
|
||
|
|
end
|
||
|
|
end
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
local mt_aura = {
|
||
|
|
__index = function( t, k )
|
||
|
|
return rawget( state.buff, k ) or rawget( state.debuff, k )
|
||
|
|
end
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
setmetatable( state, mt_state )
|
||
|
|
setmetatable( state.action, mt_actions )
|
||
|
|
setmetatable( state.active_dot, mt_active_dot )
|
||
|
|
setmetatable( state.aura, mt_aura )
|
||
|
|
setmetatable( state.buff, mt_buffs )
|
||
|
|
setmetatable( state.cooldown, mt_cooldowns )
|
||
|
|
setmetatable( state.debuff, mt_debuffs )
|
||
|
|
setmetatable( state.dot, mt_dot )
|
||
|
|
setmetatable( state.equipped, mt_equipped )
|
||
|
|
-- setmetatable( state.health, mt_resource )
|
||
|
|
setmetatable( state.pet, mt_pets )
|
||
|
|
setmetatable( state.pet.fake_pet, mt_default_pet )
|
||
|
|
setmetatable( state.prev, mt_prev )
|
||
|
|
setmetatable( state.prev_gcd, mt_prev )
|
||
|
|
setmetatable( state.prev_off_gcd, mt_prev )
|
||
|
|
setmetatable( state.pvptalent, mt_pvptalents )
|
||
|
|
setmetatable( state.race, mt_false )
|
||
|
|
setmetatable( state.set_bonus, mt_set_bonuses )
|
||
|
|
setmetatable( state.settings, mt_settings )
|
||
|
|
setmetatable( state.spec, mt_spec )
|
||
|
|
setmetatable( state.stance, mt_stances )
|
||
|
|
setmetatable( state.stat, mt_stat )
|
||
|
|
setmetatable( state.swings, mt_swings )
|
||
|
|
setmetatable( state.talent, mt_talents )
|
||
|
|
setmetatable( state.target, mt_target )
|
||
|
|
setmetatable( state.target.health, mt_target_health )
|
||
|
|
setmetatable( state.toggle, mt_toggle )
|
||
|
|
setmetatable( state.totem, mt_totem )
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
local all = class.specs[ 0 ]
|
||
|
|
|
||
|
|
-- 04072017: Let's go ahead and cache aura information to reduce overhead.
|
||
|
|
local autoAuraKey = setmetatable( {}, {
|
||
|
|
__index = function( t, k )
|
||
|
|
local aura_name = GetSpellInfo( k )
|
||
|
|
|
||
|
|
if not aura_name then return end
|
||
|
|
|
||
|
|
local name
|
||
|
|
|
||
|
|
if class.auras[ aura_name ] then
|
||
|
|
local i = 1
|
||
|
|
|
||
|
|
while( true ) do
|
||
|
|
local new = aura_name .. ' ' .. i
|
||
|
|
|
||
|
|
if not class.auras[ new ] then
|
||
|
|
name = new
|
||
|
|
break
|
||
|
|
end
|
||
|
|
|
||
|
|
i = i + 1
|
||
|
|
end
|
||
|
|
end
|
||
|
|
name = name or aura_name
|
||
|
|
|
||
|
|
local key = formatKey( aura_name )
|
||
|
|
|
||
|
|
if class.auras[ key ] then
|
||
|
|
local i = 1
|
||
|
|
|
||
|
|
while ( true ) do
|
||
|
|
local new = key .. "_" .. i
|
||
|
|
|
||
|
|
if not class.auras[ new ] then
|
||
|
|
key = new
|
||
|
|
break
|
||
|
|
end
|
||
|
|
|
||
|
|
i = i + 1
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Store the aura and save the key if we can.
|
||
|
|
if not all then all = class.specs[ 0 ] end
|
||
|
|
if all then
|
||
|
|
all:RegisterAura( key, {
|
||
|
|
id = k,
|
||
|
|
name = name
|
||
|
|
} )
|
||
|
|
end
|
||
|
|
t[k] = key
|
||
|
|
|
||
|
|
return t[k]
|
||
|
|
end
|
||
|
|
} )
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
do
|
||
|
|
local scraped = {}
|
||
|
|
|
||
|
|
function state.ScrapeUnitAuras( unit, newTarget )
|
||
|
|
local db = ns.auras[ unit ]
|
||
|
|
|
||
|
|
if scraped[ unit ] then
|
||
|
|
for k,v in pairs( db.buff ) do
|
||
|
|
v.name = nil
|
||
|
|
v.lastCount = newTarget and 0 or v.count
|
||
|
|
v.lastApplied = newTarget and 0 or v.applied
|
||
|
|
|
||
|
|
v.last_application = max( 0, v.applied, v.last_application )
|
||
|
|
v.last_expiry = max( 0, v.expires, v.last_expiry )
|
||
|
|
|
||
|
|
v.count = 0
|
||
|
|
v.expires = 0
|
||
|
|
v.applied = 0
|
||
|
|
v.duration = class.auras[ k ] and class.auras[ k ].duration or v.duration
|
||
|
|
v.caster = "nobody"
|
||
|
|
v.timeMod = 1
|
||
|
|
v.v1 = 0
|
||
|
|
v.v2 = 0
|
||
|
|
v.v3 = 0
|
||
|
|
v.unit = unit
|
||
|
|
end
|
||
|
|
|
||
|
|
for k,v in pairs( db.debuff ) do
|
||
|
|
v.name = nil
|
||
|
|
v.lastCount = newTarget and 0 or v.count
|
||
|
|
v.lastApplied = newTarget and 0 or v.applied
|
||
|
|
v.count = 0
|
||
|
|
v.expires = 0
|
||
|
|
v.applied = 0
|
||
|
|
v.duration = class.auras[ k ] and class.auras[ k ].duration or v.duration
|
||
|
|
v.caster = "nobody"
|
||
|
|
v.timeMod = 1
|
||
|
|
v.v1 = 0
|
||
|
|
v.v2 = 0
|
||
|
|
v.v3 = 0
|
||
|
|
v.unit = unit
|
||
|
|
end
|
||
|
|
|
||
|
|
scraped[ unit ] = false
|
||
|
|
end
|
||
|
|
|
||
|
|
if not UnitExists( unit ) then return end
|
||
|
|
|
||
|
|
scraped[ unit ] = true
|
||
|
|
|
||
|
|
local i = 1
|
||
|
|
while ( true ) do
|
||
|
|
local name, _, count, _, duration, expires, caster, _, _, spellID, _, _, _, _, timeMod, v1, v2, v3 = UnitBuff( unit, i, "PLAYER" )
|
||
|
|
if not name then break end
|
||
|
|
|
||
|
|
local key = class.auras[ spellID ] and class.auras[ spellID ].key
|
||
|
|
-- if not key then key = class.auras[ name ] and class.auras[ name ].key end
|
||
|
|
if not key then key = autoAuraKey[ spellID ] end
|
||
|
|
|
||
|
|
if key then
|
||
|
|
db.buff[ key ] = db.buff[ key ] or {}
|
||
|
|
local buff = db.buff[ key ]
|
||
|
|
|
||
|
|
if expires == 0 then
|
||
|
|
expires = GetTime() + 3600
|
||
|
|
duration = 7200
|
||
|
|
end
|
||
|
|
|
||
|
|
buff.key = key
|
||
|
|
buff.id = spellID
|
||
|
|
buff.name = name
|
||
|
|
buff.count = count > 0 and count or 1
|
||
|
|
buff.expires = expires
|
||
|
|
buff.duration = duration
|
||
|
|
buff.applied = expires - duration
|
||
|
|
buff.caster = caster
|
||
|
|
buff.timeMod = timeMod
|
||
|
|
buff.v1 = v1
|
||
|
|
buff.v2 = v2
|
||
|
|
buff.v3 = v3
|
||
|
|
|
||
|
|
buff.last_application = buff.last_application or 0
|
||
|
|
buff.last_expiry = buff.last_expiry or 0
|
||
|
|
|
||
|
|
buff.unit = unit
|
||
|
|
end
|
||
|
|
|
||
|
|
i = i + 1
|
||
|
|
end
|
||
|
|
|
||
|
|
i = 1
|
||
|
|
while ( true ) do
|
||
|
|
local name, _, count, _, duration, expires, caster, _, _, spellID, _, _, _, _, timeMod, v1, v2, v3 = UnitDebuff( unit, i, unit ~= "player" and "PLAYER" or nil )
|
||
|
|
if not name then break end
|
||
|
|
|
||
|
|
|
||
|
|
local key = class.auras[ spellID ] and class.auras[ spellID ].key
|
||
|
|
-- if not key then key = class.auras[ name ] and class.auras[ name ].key end
|
||
|
|
if not key then key = autoAuraKey[ spellID ] end
|
||
|
|
|
||
|
|
if key then
|
||
|
|
db.debuff[ key ] = db.debuff[ key ] or {}
|
||
|
|
local debuff = db.debuff[ key ]
|
||
|
|
|
||
|
|
if expires == 0 then
|
||
|
|
expires = GetTime() + 3600
|
||
|
|
duration = 7200
|
||
|
|
end
|
||
|
|
|
||
|
|
debuff.key = key
|
||
|
|
debuff.id = spellID
|
||
|
|
debuff.name = name
|
||
|
|
debuff.count = count > 0 and count or 1
|
||
|
|
debuff.expires = expires
|
||
|
|
debuff.duration = duration
|
||
|
|
debuff.applied = expires - duration
|
||
|
|
debuff.caster = caster
|
||
|
|
debuff.timeMod = timeMod
|
||
|
|
debuff.v1 = v1
|
||
|
|
debuff.v2 = v2
|
||
|
|
debuff.v3 = v3
|
||
|
|
|
||
|
|
debuff.unit = unit
|
||
|
|
end
|
||
|
|
|
||
|
|
i = i + 1
|
||
|
|
end
|
||
|
|
|
||
|
|
if UnitIsUnit( unit, "player" ) and IsInJailersTower() then
|
||
|
|
i = 1
|
||
|
|
while ( true ) do
|
||
|
|
local name, _, count, _, duration, expires, caster, _, _, spellID, _, _, _, _, timeMod, v1, v2, v3 = UnitBuff( unit, i, "MAW" )
|
||
|
|
if not name then break end
|
||
|
|
|
||
|
|
local aura = class.auras[ spellID ]
|
||
|
|
local key = aura and aura.key
|
||
|
|
|
||
|
|
if not key then key = autoAuraKey[ spellID ] end
|
||
|
|
|
||
|
|
if key then
|
||
|
|
db.buff[ key ] = db.buff[ key ] or {}
|
||
|
|
local buff = db.buff[ key ]
|
||
|
|
|
||
|
|
if expires == 0 then
|
||
|
|
expires = GetTime() + 3600
|
||
|
|
duration = 7200
|
||
|
|
end
|
||
|
|
|
||
|
|
buff.key = key
|
||
|
|
buff.id = spellID
|
||
|
|
buff.name = name
|
||
|
|
buff.count = count > 0 and count or 1
|
||
|
|
buff.expires = expires
|
||
|
|
buff.duration = duration
|
||
|
|
buff.applied = expires - duration
|
||
|
|
buff.caster = caster
|
||
|
|
buff.timeMod = timeMod
|
||
|
|
buff.v1 = v1
|
||
|
|
buff.v2 = v2
|
||
|
|
buff.v3 = v3
|
||
|
|
|
||
|
|
if aura and buff.count > aura.max_stack then aura.max_stack = buff.count end
|
||
|
|
|
||
|
|
buff.last_application = buff.last_application or 0
|
||
|
|
buff.last_expiry = buff.last_expiry or 0
|
||
|
|
|
||
|
|
buff.unit = unit
|
||
|
|
end
|
||
|
|
|
||
|
|
i = i + 1
|
||
|
|
end
|
||
|
|
|
||
|
|
i = 1
|
||
|
|
while ( true ) do
|
||
|
|
local name, _, count, _, duration, expires, caster, _, _, spellID, _, _, _, _, timeMod, v1, v2, v3 = UnitDebuff( unit, i, "MAW" )
|
||
|
|
if not name then break end
|
||
|
|
|
||
|
|
local aura = class.auras[ spellID ]
|
||
|
|
local key = aura and aura.key
|
||
|
|
|
||
|
|
if not key then key = autoAuraKey[ spellID ] end
|
||
|
|
|
||
|
|
if key then
|
||
|
|
db.debuff[ key ] = db.debuff[ key ] or {}
|
||
|
|
local debuff = db.debuff[ key ]
|
||
|
|
|
||
|
|
if expires == 0 then
|
||
|
|
expires = GetTime() + 3600
|
||
|
|
duration = 7200
|
||
|
|
end
|
||
|
|
|
||
|
|
debuff.key = key
|
||
|
|
debuff.id = spellID
|
||
|
|
debuff.name = name
|
||
|
|
debuff.count = count > 0 and count or 1
|
||
|
|
debuff.expires = expires
|
||
|
|
debuff.duration = duration
|
||
|
|
debuff.applied = expires - duration
|
||
|
|
debuff.caster = caster
|
||
|
|
debuff.timeMod = timeMod
|
||
|
|
debuff.v1 = v1
|
||
|
|
debuff.v2 = v2
|
||
|
|
debuff.v3 = v3
|
||
|
|
|
||
|
|
if aura and debuff.count > aura.max_stack then aura.max_stack = debuff.count end
|
||
|
|
|
||
|
|
debuff.unit = unit
|
||
|
|
end
|
||
|
|
|
||
|
|
i = i + 1
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
Hekili.ScrapeUnitAuras = state.ScrapeUnitAuras
|
||
|
|
Hekili:ProfileCPU( "ScrapeUnitAuras", state.ScrapeUnitAuras )
|
||
|
|
|
||
|
|
Hekili.AuraDB = ns.auras
|
||
|
|
end
|
||
|
|
local ScrapeUnitAuras = state.ScrapeUnitAuras
|
||
|
|
|
||
|
|
|
||
|
|
-- Helper functions to query the real aura data that has been scraped.
|
||
|
|
-- Used for snapshotting projectile data to be handled when a spell impacts.
|
||
|
|
function state.PlayerBuffUp( buff )
|
||
|
|
local aura = state.auras.player.buff[ buff ]
|
||
|
|
return aura and aura.expires > GetTime()
|
||
|
|
end
|
||
|
|
|
||
|
|
function state.PlayerDebuffUp( debuff )
|
||
|
|
local aura = state.auras.player.debuff[ debuff ]
|
||
|
|
return aura and aura.expires > GetTime()
|
||
|
|
end
|
||
|
|
|
||
|
|
function state.TargetBuffUp( buff )
|
||
|
|
local aura = state.auras.target.buff[ buff ]
|
||
|
|
return aura and aura.expires > GetTime()
|
||
|
|
end
|
||
|
|
|
||
|
|
function state.TargetDebuffUp( debuff )
|
||
|
|
local aura = state.auras.target.debuff[ debuff ]
|
||
|
|
return aura and aura.expires > GetTime()
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state.putTrinketsOnCD( val )
|
||
|
|
val = val or 10
|
||
|
|
|
||
|
|
for i, item in ipairs( state.items ) do
|
||
|
|
if ( not class.abilities[ item ].essence and not class.abilities[ item ].no_icd ) and state.cooldown[ item ].remains < val then setCooldown( item, val ) end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
do
|
||
|
|
-- Simpler Queue System
|
||
|
|
local realQueue = {}
|
||
|
|
state.realQueue = realQueue
|
||
|
|
|
||
|
|
local virtualQueue = {}
|
||
|
|
state.queue = virtualQueue
|
||
|
|
|
||
|
|
local byTime = function( a, b )
|
||
|
|
return a.time < b.time
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
local eventPool = {}
|
||
|
|
|
||
|
|
local function NewEvent()
|
||
|
|
if #eventPool > 0 then
|
||
|
|
return remove( eventPool, 1 )
|
||
|
|
end
|
||
|
|
|
||
|
|
return {}
|
||
|
|
end
|
||
|
|
|
||
|
|
local function RecycleEvent( queue, i )
|
||
|
|
local e = queue[ i ]
|
||
|
|
|
||
|
|
if e then
|
||
|
|
e.action = nil
|
||
|
|
e.start = nil
|
||
|
|
e.time = nil
|
||
|
|
e.type = nil
|
||
|
|
e.target = nil
|
||
|
|
e.func = nil
|
||
|
|
|
||
|
|
insert( eventPool, e )
|
||
|
|
remove( queue, i )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
function state:QueueEvent( action, start, time, type, target, real )
|
||
|
|
local queue = real and realQueue or virtualQueue
|
||
|
|
local e = NewEvent()
|
||
|
|
|
||
|
|
if not time then
|
||
|
|
local ability = class.abilities[ action ]
|
||
|
|
|
||
|
|
if ability then
|
||
|
|
if type == "PROJECTILE_IMPACT" then
|
||
|
|
if ability.flightTime then time = start + 0.05 + ability.flightTime
|
||
|
|
else time = start + 0.05 + ( state.target.distance / ability.velocity ) end
|
||
|
|
|
||
|
|
elseif type == "CHANNEL_START" then
|
||
|
|
time = start
|
||
|
|
|
||
|
|
elseif type == "CHANNEL_FINISH" or type == "CAST_FINISH" then
|
||
|
|
time = start + ability.cast
|
||
|
|
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if action and start and time and type then
|
||
|
|
if time < start then time = start + time end
|
||
|
|
|
||
|
|
e.action = action
|
||
|
|
e.start = start
|
||
|
|
e.time = time
|
||
|
|
e.type = type
|
||
|
|
e.target = target
|
||
|
|
e.func = nil
|
||
|
|
|
||
|
|
insert( queue, e )
|
||
|
|
sort( queue, byTime )
|
||
|
|
|
||
|
|
if Hekili.ActiveDebug and not real then Hekili:Debug( "Queued %s from %.2f to %.2f (%s).", action, start, time, type ) end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
function state:QueueAuraExpiration( action, func, time )
|
||
|
|
local queue = virtualQueue
|
||
|
|
local e = NewEvent()
|
||
|
|
|
||
|
|
if not time then return end
|
||
|
|
|
||
|
|
e.action = action
|
||
|
|
e.func = func
|
||
|
|
e.start = self.query_time
|
||
|
|
e.time = time
|
||
|
|
e.type = "AURA_EXPIRATION"
|
||
|
|
e.target = "nobody"
|
||
|
|
|
||
|
|
insert( queue, e )
|
||
|
|
sort( queue, byTime )
|
||
|
|
|
||
|
|
if Hekili.ActiveDebug then Hekili:Debug( "Queued %s AURA_EXPIRATION at +%.2f.", action, time - state.query_time ) end
|
||
|
|
end
|
||
|
|
|
||
|
|
function state:RemoveAuraExpiration( action )
|
||
|
|
local queue = virtualQueue
|
||
|
|
|
||
|
|
Hekili:Debug( "Trying to remove %s AURA_EXPIRATION from queue.", action )
|
||
|
|
|
||
|
|
for i = 1, #queue do
|
||
|
|
local e = queue[ i ]
|
||
|
|
|
||
|
|
if e.action == action and e.type == "AURA_EXPIRATION" then
|
||
|
|
RecycleEvent( queue, i )
|
||
|
|
Hekili:Debug( "Removed #%d from queue.", i )
|
||
|
|
break
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
function state:RemoveEvent( e, real )
|
||
|
|
local queue = real and realQueue or virtualQueue
|
||
|
|
|
||
|
|
Hekili:Debug( "Trying to remove %s %s from queue.", ( e.action or "NO_ACTION" ), ( e.type or "NO_TYPE" ) )
|
||
|
|
|
||
|
|
for i = #queue, 1, -1 do
|
||
|
|
if queue[ i ] == e then
|
||
|
|
Hekili:Debug( "Removing %d from queue.", i )
|
||
|
|
RecycleEvent( queue, i )
|
||
|
|
break
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
function state:GetEventInfo( action, start, time, type, target, real )
|
||
|
|
local queue = real and realQueue or virtualQueue
|
||
|
|
|
||
|
|
-- Find the first event that matches the provided criteria and return all the data.
|
||
|
|
for i, event in ipairs( queue ) do
|
||
|
|
if ( not action or event.action == action ) and
|
||
|
|
( not start or event.start == start ) and
|
||
|
|
( not time or event.time == time ) and
|
||
|
|
( not type or event.type == type ) and
|
||
|
|
( not target or event.target == target ) then
|
||
|
|
|
||
|
|
return event.action, event.start, event.time, event.type, event.target
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
function state:RemoveSpellEvent( action, real, eType, reverse )
|
||
|
|
local queue = real and realQueue or virtualQueue
|
||
|
|
|
||
|
|
local success = false
|
||
|
|
|
||
|
|
if reverse then
|
||
|
|
for i = #queue, 1, -1 do
|
||
|
|
local e = queue[ i ]
|
||
|
|
|
||
|
|
if e.action == action and ( eType == nil or e.type == eType ) then
|
||
|
|
RecycleEvent( queue, i )
|
||
|
|
return true
|
||
|
|
end
|
||
|
|
end
|
||
|
|
else
|
||
|
|
for i = 1, #queue do
|
||
|
|
local e = queue[ i ]
|
||
|
|
|
||
|
|
if e.action == action and ( eType == nil or e.type == eType ) then
|
||
|
|
RecycleEvent( queue, i )
|
||
|
|
return true
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
|
||
|
|
function state:RemoveSpellEvents( action, real, eType )
|
||
|
|
local queue = real and realQueue or virtualQueue
|
||
|
|
|
||
|
|
local success = false
|
||
|
|
|
||
|
|
for i = #queue, 1, -1 do
|
||
|
|
local e = queue[ i ]
|
||
|
|
|
||
|
|
if e.action == action and ( eType == nil or e.type == eType ) then
|
||
|
|
RecycleEvent( queue, i )
|
||
|
|
success = true
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if success then
|
||
|
|
for k in pairs( class.resources ) do
|
||
|
|
forecastResources( k )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
return success
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
local EVENT_EXPIRE_MARGIN = 0.2
|
||
|
|
|
||
|
|
function state:ResetQueues()
|
||
|
|
for i = #virtualQueue, 1, -1 do
|
||
|
|
RecycleEvent( virtualQueue, i )
|
||
|
|
end
|
||
|
|
|
||
|
|
local now = GetTime()
|
||
|
|
|
||
|
|
for i = #realQueue, 1, -1 do
|
||
|
|
local e = realQueue[ i ]
|
||
|
|
|
||
|
|
if e.time + EVENT_EXPIRE_MARGIN < now then
|
||
|
|
RecycleEvent( realQueue, i )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
for i, r in ipairs( realQueue ) do
|
||
|
|
local e = NewEvent()
|
||
|
|
|
||
|
|
e.action = r.action
|
||
|
|
e.start = r.start
|
||
|
|
e.time = r.time
|
||
|
|
e.type = r.type
|
||
|
|
e.target = r.target
|
||
|
|
|
||
|
|
virtualQueue[ i ] = e
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
local times = {}
|
||
|
|
|
||
|
|
function state:GetQueueTimes( queue )
|
||
|
|
wipe( times )
|
||
|
|
|
||
|
|
for i, v in ipairs( queue ) do
|
||
|
|
times[ i ] = v.time
|
||
|
|
end
|
||
|
|
|
||
|
|
return unpack( times )
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state:GetQueue( real )
|
||
|
|
if real then return realQueue end
|
||
|
|
return virtualQueue
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state:HandleEvent( e )
|
||
|
|
if not e then return end
|
||
|
|
|
||
|
|
local action = e.action
|
||
|
|
local ability
|
||
|
|
local curr_action = self.this_action
|
||
|
|
|
||
|
|
if e.type ~= "AURA_EXPIRATION" then
|
||
|
|
ability = class.abilities[ e.action ]
|
||
|
|
|
||
|
|
if not ability then
|
||
|
|
state:RemoveEvent( e )
|
||
|
|
return
|
||
|
|
end
|
||
|
|
|
||
|
|
self.this_action = action
|
||
|
|
end
|
||
|
|
|
||
|
|
if Hekili.ActiveDebug then Hekili:Debug( "\nHandling %s at %.2f (%s).", action, e.time, e.type ) end
|
||
|
|
|
||
|
|
if e.type == "CAST_FINISH" then
|
||
|
|
self.hardcast = true
|
||
|
|
local cooldown = ability.cooldown
|
||
|
|
|
||
|
|
-- Put the action on cooldown. (It's slightly premature, but addresses CD resets like Echo of the Elements.)
|
||
|
|
-- if ability.charges and ability.charges > 1 and ability.recharge > 0 then
|
||
|
|
if ability.charges and ability.recharge > 0 then
|
||
|
|
self.spendCharges( action, 1 )
|
||
|
|
|
||
|
|
elseif action ~= "global_cooldown" then
|
||
|
|
self.setCooldown( action, cooldown )
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Spend resources.
|
||
|
|
ns.spendResources( action )
|
||
|
|
|
||
|
|
local wasCycling = self.IsCycling( nil, true )
|
||
|
|
local expires, minTTD, maxTTD, aura
|
||
|
|
|
||
|
|
if wasCycling then
|
||
|
|
expires, minTTD, maxTTD, aura = self.GetCycleInfo()
|
||
|
|
end
|
||
|
|
|
||
|
|
if Hekili.ActiveDebug then Hekili:Debug( "%s %s %s %s", tostring( self.cycle ), tostring( e.target ) or "notarget", tostring( self.target.unit ) or "notarget", tostring( e.target == self.target.unit ) ) end
|
||
|
|
|
||
|
|
if e.target and self.target.unit ~= "unknown" and e.target ~= self.target.unit then
|
||
|
|
if Hekili.ActiveDebug then Hekili:Debug( "Using ability on a different target." ) end
|
||
|
|
self.SetupCycle( ability )
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Perform the action.
|
||
|
|
self:RunHandler( action )
|
||
|
|
self.hardcast = nil
|
||
|
|
|
||
|
|
if wasCycling then
|
||
|
|
self.SetCycleInfo( expires, minTTD, maxTTD, aura )
|
||
|
|
else
|
||
|
|
self.ClearCycle()
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.item and not ability.essence then
|
||
|
|
self.putTrinketsOnCD( cooldown / 6 )
|
||
|
|
end
|
||
|
|
|
||
|
|
elseif e.type == "CHANNEL_TICK" then
|
||
|
|
if ability.tick then ability.tick() end
|
||
|
|
|
||
|
|
elseif e.type == "CHANNEL_FINISH" then
|
||
|
|
if ability.finish then ability.finish() end
|
||
|
|
self.stopChanneling( false, ability.key )
|
||
|
|
|
||
|
|
elseif e.type == "PROJECTILE_IMPACT" then
|
||
|
|
if ability.impact then ability.impact() end
|
||
|
|
self:StartCombat()
|
||
|
|
|
||
|
|
elseif e.type == "AURA_EXPIRATION" then
|
||
|
|
if e.func then e.func() end
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
state.this_action = curr_action
|
||
|
|
state:RemoveEvent( e )
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state:IsQueued( action, real )
|
||
|
|
local queue = real and realQueue or virtualQueue
|
||
|
|
|
||
|
|
for i, entry in ipairs( queue ) do
|
||
|
|
if entry.action == action then return true end
|
||
|
|
end
|
||
|
|
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state:IsInFlight( action, real )
|
||
|
|
local queue = real and realQueue or virtualQueue
|
||
|
|
|
||
|
|
for i, entry in ipairs( queue ) do
|
||
|
|
if entry.action == action and entry.type == "PROJECTILE_IMPACT" and entry.start <= self.query_time then return true end
|
||
|
|
end
|
||
|
|
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state:InFlightRemains( action, real )
|
||
|
|
local queue = real and realQueue or virtualQueue
|
||
|
|
|
||
|
|
for i, entry in ipairs( queue ) do
|
||
|
|
if entry.action == action and entry.type == "PROJECTILE_IMPACT" and entry.start <= self.query_time then return max( 0, entry.time - self.query_time ) end
|
||
|
|
end
|
||
|
|
|
||
|
|
return 0
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
local cast_events = {
|
||
|
|
CAST_FINISH = true,
|
||
|
|
CHANNEL_FINISH = true
|
||
|
|
}
|
||
|
|
|
||
|
|
function state:IsCasting( action, real )
|
||
|
|
local queue = real and realQueue or virtualQueue
|
||
|
|
|
||
|
|
for i, entry in ipairs( queue ) do
|
||
|
|
if entry.type == "CAST_FINISH" and ( action == nil or entry.action == action ) and entry.start <= self.query_time then return true end
|
||
|
|
end
|
||
|
|
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state:QueuedCastRemains( action, real )
|
||
|
|
local queue = real and realQueue or virtualQueue
|
||
|
|
|
||
|
|
for i, entry in ipairs( queue ) do
|
||
|
|
if cast_events[ entry.type ] and ( action == nil or entry.action == action ) and entry.start <= self.query_time then return max( 0, entry.time - self.query_time ) end
|
||
|
|
end
|
||
|
|
|
||
|
|
return 0
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state:IsChanneling( action, real )
|
||
|
|
local queue = real and realQueue or virtualQueue
|
||
|
|
|
||
|
|
for i, entry in ipairs( queue ) do
|
||
|
|
if entry.type == "CHANNEL_FINISH" and ( action == nil or entry.action == action ) and entry.start <= self.query_time then return true end
|
||
|
|
end
|
||
|
|
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state:ApplyCastingAuraFromQueue( action, real )
|
||
|
|
local queue = real and realQueue or virtualQueue
|
||
|
|
|
||
|
|
for i, entry in ipairs( queue ) do
|
||
|
|
if cast_events[ entry.type ] and ( action == nil or entry.action == action ) and entry.start <= self.query_time then
|
||
|
|
local casting = self.buff.casting
|
||
|
|
|
||
|
|
casting.applied = entry.start
|
||
|
|
|
||
|
|
if entry.time > entry.start then
|
||
|
|
casting.expires = entry.time
|
||
|
|
else
|
||
|
|
casting.expires = entry.start + entry.time
|
||
|
|
end
|
||
|
|
|
||
|
|
casting.duration = casting.expires - casting.applied
|
||
|
|
|
||
|
|
casting.v3 = entry.type == "CHANNEL_FINISH"
|
||
|
|
|
||
|
|
if entry.action then
|
||
|
|
local spell = class.abilities[ entry.action ]
|
||
|
|
if spell and spell.id then
|
||
|
|
casting.v1 = spell.id
|
||
|
|
else
|
||
|
|
casting.v1 = 0
|
||
|
|
end
|
||
|
|
else
|
||
|
|
casting.v1 = 0
|
||
|
|
end
|
||
|
|
|
||
|
|
return
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state:RunHandler( key, noStart )
|
||
|
|
local ability = class.abilities[ key ]
|
||
|
|
|
||
|
|
if not ability then
|
||
|
|
-- ns.Error( "runHandler() attempting to run handler for non-existant ability '" .. key .. "'." )
|
||
|
|
return
|
||
|
|
end
|
||
|
|
|
||
|
|
if self.channeling and not ability.castableWhileCasting then
|
||
|
|
self.stopChanneling( false, ability.key )
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.channeled then
|
||
|
|
if ability.start then ability.start() end
|
||
|
|
self.channelSpell( key, self.query_time, ability.cast, ability.id )
|
||
|
|
elseif ability.handler then ability.handler() end
|
||
|
|
|
||
|
|
self.prev.last = key
|
||
|
|
self[ ability.gcd == "off" and "prev_off_gcd" or "prev_gcd" ].last = key
|
||
|
|
|
||
|
|
table.insert( self.predictions, 1, key )
|
||
|
|
table.insert( self[ ability.gcd == "off" and 'predictionsOff' or 'predictionsOn' ], 1, key )
|
||
|
|
|
||
|
|
self.history.casts[ key ] = self.query_time
|
||
|
|
|
||
|
|
self.predictions[6] = nil
|
||
|
|
self.predictionsOn[6] = nil
|
||
|
|
self.predictionsOff[6] = nil
|
||
|
|
|
||
|
|
self.prev.override = nil
|
||
|
|
self.prev_gcd.override = nil
|
||
|
|
self.prev_off_gcd.override = nil
|
||
|
|
|
||
|
|
if self.time == 0 and ability.startsCombat and not ability.isProjectile and not noStart then
|
||
|
|
self.false_start = self.query_time - 0.01
|
||
|
|
|
||
|
|
-- Assume MH swing at combat start and OH swing half a swing later?
|
||
|
|
if self.target.distance < 8 then
|
||
|
|
if self.swings.mainhand_speed > 0 and self.nextMH == 0 then self.swings.mh_pseudo = 0.01 + self.query_time - self.swings.mainhand_speed end
|
||
|
|
if self.swings.offhand_speed > 0 and self.nextOH == 0 then self.swings.oh_pseudo = 0.01 + self.query_time - ( self.offhand_speed / 2 ) end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
-- state.cast_start = 0
|
||
|
|
ns.callHook( 'runHandler', key )
|
||
|
|
end
|
||
|
|
|
||
|
|
function state.runHandler( key, noStart )
|
||
|
|
state:RunHandler( key, noStart )
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state.reset( dispName )
|
||
|
|
state.now = GetTime()
|
||
|
|
state.index = 0
|
||
|
|
state.scriptID = nil
|
||
|
|
state.offset = 0
|
||
|
|
state.delay = 0
|
||
|
|
state.false_start = 0
|
||
|
|
|
||
|
|
state.resetting = true
|
||
|
|
|
||
|
|
state.cycle = nil
|
||
|
|
state.ClearCycle()
|
||
|
|
state:ResetVariables()
|
||
|
|
|
||
|
|
state.selection_time = 60
|
||
|
|
state.selected_action = nil
|
||
|
|
|
||
|
|
local _, zone = GetInstanceInfo()
|
||
|
|
|
||
|
|
state.bg = zone == "pvp"
|
||
|
|
state.arena = zone == "arena"
|
||
|
|
|
||
|
|
state.torghast = IsInJailersTower()
|
||
|
|
|
||
|
|
state.min_targets = 0
|
||
|
|
state.max_targets = 0
|
||
|
|
|
||
|
|
state.active_enemies = nil
|
||
|
|
state.my_enemies = nil
|
||
|
|
state.true_active_enemies = nil
|
||
|
|
state.true_my_enemies = nil
|
||
|
|
|
||
|
|
state.latency = select( 4, GetNetStats() ) / 1000
|
||
|
|
|
||
|
|
-- Projectiles
|
||
|
|
state:ResetQueues()
|
||
|
|
|
||
|
|
local p = Hekili.DB.profile
|
||
|
|
|
||
|
|
local display = dispName and p.displays[ dispName ]
|
||
|
|
local spec = state.spec.id and p.specs[ state.spec.id ]
|
||
|
|
local mode = p.toggles.mode.value
|
||
|
|
|
||
|
|
state.display = dispName
|
||
|
|
state.filter = "none"
|
||
|
|
state.rangefilter = false
|
||
|
|
|
||
|
|
if display then
|
||
|
|
if dispName == 'Primary' then
|
||
|
|
if mode == "single" or mode == "dual" or mode == "reactive" then state.max_targets = 1
|
||
|
|
elseif mode == "aoe" then state.min_targets = spec and spec.aoe or 3 end
|
||
|
|
elseif dispName == 'AOE' then state.min_targets = spec and spec.aoe or 3
|
||
|
|
elseif dispName == 'Cooldowns' then state.filter = "cooldowns"
|
||
|
|
elseif dispName == 'Interrupts' then state.filter = "interrupts"
|
||
|
|
elseif dispName == 'Defensives' then state.filter = "defensives"
|
||
|
|
end
|
||
|
|
|
||
|
|
state.rangefilter = display.range.enabled and display.range.type == "xclude"
|
||
|
|
end
|
||
|
|
|
||
|
|
for i = #state.purge, 1, -1 do
|
||
|
|
state[ state.purge[ i ] ] = nil
|
||
|
|
table.remove( state.purge, i )
|
||
|
|
end
|
||
|
|
|
||
|
|
for k in pairs( state.active_dot ) do
|
||
|
|
state.active_dot[ k ] = nil
|
||
|
|
end
|
||
|
|
|
||
|
|
for k in pairs( state.stat ) do
|
||
|
|
state.stat[ k ] = nil
|
||
|
|
end
|
||
|
|
|
||
|
|
state.haste = nil
|
||
|
|
|
||
|
|
ns.callHook( "reset_preauras" )
|
||
|
|
|
||
|
|
if state.target.updated then
|
||
|
|
ScrapeUnitAuras( "target" )
|
||
|
|
state.target.updated = false
|
||
|
|
end
|
||
|
|
|
||
|
|
if state.player.updated then
|
||
|
|
ScrapeUnitAuras( "player" )
|
||
|
|
state.player.updated = false
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
for k, v in pairs( state.buff ) do
|
||
|
|
for attr in pairs( default_buff_values ) do
|
||
|
|
v[ attr ] = nil
|
||
|
|
end
|
||
|
|
v.lastCount = nil
|
||
|
|
v.lastApplied = nil
|
||
|
|
end
|
||
|
|
|
||
|
|
for k, v in pairs( state.cooldown ) do
|
||
|
|
v.duration = nil
|
||
|
|
v.expires = nil
|
||
|
|
v.charge = nil
|
||
|
|
v.next_charge = nil
|
||
|
|
v.recharge_began = nil
|
||
|
|
v.recharge_duration = nil
|
||
|
|
v.true_expires = nil
|
||
|
|
v.true_remains = nil
|
||
|
|
end
|
||
|
|
|
||
|
|
--[[ state.trinket.t1.cooldown.duration = nil
|
||
|
|
state.trinket.t1.cooldown.expires = nil
|
||
|
|
state.trinket.t2.cooldown.duration = nil
|
||
|
|
state.trinket.t2.cooldown.expires = nil ]]
|
||
|
|
|
||
|
|
for k, v in pairs( state.debuff ) do
|
||
|
|
for attr in pairs( default_debuff_values ) do
|
||
|
|
v[ attr ] = nil
|
||
|
|
end
|
||
|
|
v.lastCount = nil
|
||
|
|
v.lastApplied = nil
|
||
|
|
end
|
||
|
|
|
||
|
|
state.pet.exists = nil
|
||
|
|
for k, v in pairs( state.pet ) do
|
||
|
|
if type(v) == "table" and k ~= "fake_pet" then
|
||
|
|
v.expires = nil
|
||
|
|
end
|
||
|
|
end
|
||
|
|
-- rawset( state.pet, "exists", UnitExists( "pet" ) )
|
||
|
|
|
||
|
|
for k in pairs( state.stance ) do
|
||
|
|
state.stance[ k ] = nil
|
||
|
|
end
|
||
|
|
|
||
|
|
for k in pairs( class.stateTables ) do
|
||
|
|
if rawget( state[ k ], "onReset" ) then state[ k ].onReset( state[ k ] ) end
|
||
|
|
end
|
||
|
|
|
||
|
|
for k in pairs( state.totem ) do
|
||
|
|
state.totem[ k ].expires = nil
|
||
|
|
end
|
||
|
|
|
||
|
|
for k, v in pairs( state.pet ) do
|
||
|
|
if type(v) == "table" then
|
||
|
|
v.expires = 0
|
||
|
|
|
||
|
|
if not rawget( v, "key" ) then
|
||
|
|
v.key = k
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
local petID = UnitGUID( "pet" )
|
||
|
|
if petID then
|
||
|
|
petID = tonumber( petID:match( "%-(%d+)%-[0-9A-F]+$" ) )
|
||
|
|
|
||
|
|
for k, v in pairs( class.pets ) do
|
||
|
|
local id = v.id and ( type( v.id ) == "function" and v.id() ) or v.id
|
||
|
|
|
||
|
|
if id == petID then
|
||
|
|
local lastCast = v.spell and class.abilities[ v.spell ] and class.abilities[ v.spell ].lastCast or 0
|
||
|
|
local duration = v.duration and ( ( type( v.duration ) == "function" and v.duration() ) or v.duration ) or 3600
|
||
|
|
|
||
|
|
if lastCast > 0 and duration < 3600 then
|
||
|
|
summonPet( k, lastCast + duration - state.now )
|
||
|
|
else
|
||
|
|
summonPet( k )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
for i = 1, 5 do
|
||
|
|
local _, _, start, duration, icon = GetTotemInfo(i)
|
||
|
|
|
||
|
|
if icon and class.totems[ icon ] then
|
||
|
|
summonPet( class.totems[ icon ], start + duration - state.now )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
for k, v in pairs( state.pet ) do
|
||
|
|
if type(v) == "table" and k ~= "fake_pet" and v.summonTime and v.summonTime > 0 and v.duration then
|
||
|
|
local remains = ( v.summonTime + v.duration ) - state.now
|
||
|
|
if remains > 0 then
|
||
|
|
summonPet( k, remains )
|
||
|
|
else
|
||
|
|
v.summonTime = 0
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
state.target.health.actual = nil
|
||
|
|
state.target.health.current = nil
|
||
|
|
state.target.health.max = nil
|
||
|
|
|
||
|
|
state.aggro = ( UnitThreatSituation( "player" ) or 0 ) > 1
|
||
|
|
state.tanking = state.role.tank and state.aggro
|
||
|
|
|
||
|
|
-- range checks
|
||
|
|
state.target.minR = nil
|
||
|
|
state.target.maxR = nil
|
||
|
|
state.target.distance = nil
|
||
|
|
|
||
|
|
state.prev.last = state.player.lastcast
|
||
|
|
state.prev.override = nil
|
||
|
|
|
||
|
|
state.prev_gcd.last = state.player.lastgcd
|
||
|
|
state.prev_gcd.override = nil
|
||
|
|
|
||
|
|
state.prev_off_gcd.last = state.player.lastoffgcd
|
||
|
|
state.prev_off_gcd.override = nil
|
||
|
|
|
||
|
|
for i = 1, 5 do
|
||
|
|
state.predictions[i] = nil
|
||
|
|
state.predictionsOn[i] = nil
|
||
|
|
state.predictionsOff[i] = nil
|
||
|
|
end
|
||
|
|
|
||
|
|
wipe( state.history.casts )
|
||
|
|
wipe( state.history.units )
|
||
|
|
|
||
|
|
local last_act = state.player.lastcast and class.abilities[ state.player.lastcast ]
|
||
|
|
if last_act and last_act.startsCombat and state.combat == 0 and state.now - last_act.lastCast < 1 then
|
||
|
|
state.false_start = last_act.lastCast - 0.01
|
||
|
|
end
|
||
|
|
|
||
|
|
-- interrupts
|
||
|
|
state.target.casting = nil
|
||
|
|
|
||
|
|
local foundResource = false
|
||
|
|
|
||
|
|
for k, power in pairs( class.resources ) do
|
||
|
|
local res = rawget( state, k )
|
||
|
|
|
||
|
|
if res then
|
||
|
|
res.actual = UnitPower( "player", power.type )
|
||
|
|
res.max = UnitPowerMax( "player", power.type )
|
||
|
|
|
||
|
|
if res.max > 0 then foundResource = true end
|
||
|
|
|
||
|
|
if k == "mana" and state.spec.arcane then
|
||
|
|
res.modmax = res.max / ( 1 + state.mastery_value )
|
||
|
|
end
|
||
|
|
|
||
|
|
res.last_tick = rawget( res, "last_tick" ) or 0
|
||
|
|
res.tick_rate = rawget( res, "tick_rate" ) or 0.1
|
||
|
|
|
||
|
|
if power.type == Enum.PowerType.Mana then
|
||
|
|
local inactive, active = GetManaRegen()
|
||
|
|
|
||
|
|
res.active_regen = active or 0
|
||
|
|
res.inactive_regen = inactive or 0
|
||
|
|
res.regen = nil
|
||
|
|
else
|
||
|
|
if ResourceRegenerates( k ) then
|
||
|
|
local inactive, active = GetPowerRegenForPowerType( power.type )
|
||
|
|
res.active_regen = active or 0
|
||
|
|
res.inactive_regen = inactive or 0
|
||
|
|
res.regen = nil
|
||
|
|
else
|
||
|
|
res.regen = 0
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if res.reset then res.reset() end
|
||
|
|
forecastResources( k )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if not foundResource then
|
||
|
|
state.resetting = false
|
||
|
|
return false, "no available resources"
|
||
|
|
end
|
||
|
|
|
||
|
|
state.health = rawget( state, "health" ) or setmetatable( { resource = "health" }, mt_resource )
|
||
|
|
state.health.current = nil
|
||
|
|
state.health.actual = UnitHealth( "player" ) or 10000
|
||
|
|
state.health.max = max( 1, UnitHealthMax( "player" ) or 10000 )
|
||
|
|
state.health.regen = 0
|
||
|
|
|
||
|
|
state.swings.mh_speed, state.swings.oh_speed = UnitAttackSpeed( "player" )
|
||
|
|
state.swings.mh_speed = state.swings.mh_speed or 0
|
||
|
|
state.swings.oh_speed = state.swings.oh_speed or 0
|
||
|
|
|
||
|
|
state.mainhand_speed = state.swings.mh_speed
|
||
|
|
state.offhand_speed = state.swings.oh_speed
|
||
|
|
|
||
|
|
state.nextMH = ( state.combat > 0 and state.swings.mh_actual > state.combat and state.swings.mh_actual + state.mainhand_speed ) or 0
|
||
|
|
state.nextOH = ( state.combat > 0 and state.swings.oh_actual > state.combat and state.swings.oh_actual + state.offhand_speed ) or 0
|
||
|
|
|
||
|
|
state.swings.mh_pseudo = nil
|
||
|
|
state.swings.oh_pseudo = nil
|
||
|
|
|
||
|
|
-- Special case spells that suck.
|
||
|
|
if class.abilities[ "ascendance" ] and state.buff.ascendance.up then
|
||
|
|
setCooldown( "ascendance", state.buff.ascendance.remains + 165 )
|
||
|
|
end
|
||
|
|
|
||
|
|
local cast_time, casting, ability = 0, nil, nil
|
||
|
|
|
||
|
|
if state.buff.casting.up then
|
||
|
|
cast_time = state.buff.casting.remains
|
||
|
|
|
||
|
|
local castID = state.buff.casting.v1
|
||
|
|
ability = class.abilities[ castID ]
|
||
|
|
|
||
|
|
casting = ability and ability.key or formatKey( state.buff.casting.name )
|
||
|
|
|
||
|
|
if castID == class.abilities.cyclotronic_blast.id then
|
||
|
|
-- Set up Pocket-Sized Computation Device.
|
||
|
|
if state.buff.casting.v3 then
|
||
|
|
-- We are in the channeled part of the cast.
|
||
|
|
setCooldown( "pocketsized_computation_device", state.buff.casting.applied + 120 - state.now )
|
||
|
|
setCooldown( "global_cooldown", cast_time )
|
||
|
|
else
|
||
|
|
-- This is the casting portion.
|
||
|
|
casting = class.abilities.pocketsized_computation_device.key
|
||
|
|
state.buff.casting.v1 = class.abilities.pocketsized_computation_device.id
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
ns.callHook( "reset_precast" )
|
||
|
|
|
||
|
|
-- Okay, two paths here.
|
||
|
|
-- 1. We can cast while casting (i.e., Fire Blast for Fire Mage), so we want to hand off the current cast to the event system, and then let the recommendation engine sort it out.
|
||
|
|
-- 2. We cannot cast anything while casting (typical), so we want to advance the clock, complete the cast, and then generate recommendations.
|
||
|
|
|
||
|
|
if casting and cast_time > 0 then
|
||
|
|
local channeled, destGUID = state.buff.casting.v3
|
||
|
|
|
||
|
|
if ability then
|
||
|
|
channeled = channeled or ability.channeled
|
||
|
|
destGUID = Hekili:GetMacroCastTarget( ability.key, state.buff.casting.applied, "RESET" ) or state.target.unit
|
||
|
|
end
|
||
|
|
|
||
|
|
if not state:IsCasting() and not channeled then
|
||
|
|
state:QueueEvent( casting, state.buff.casting.applied, state.buff.casting.expires, "CAST_FINISH", destGUID )
|
||
|
|
|
||
|
|
-- Projectile spells have two handlers, effectively. An onCast handler, and then an onImpact handler.
|
||
|
|
if ability and ability.isProjectile then
|
||
|
|
state:QueueEvent( ability.key, state.buff.casting.expires, nil, "PROJECTILE_IMPACT", destGUID )
|
||
|
|
-- state:QueueEvent( action, "projectile", true )
|
||
|
|
end
|
||
|
|
|
||
|
|
elseif not state:IsChanneling() and channeled then
|
||
|
|
state:QueueEvent( casting, state.buff.casting.applied, state.buff.casting.expires, "CHANNEL_FINISH", destGUID )
|
||
|
|
|
||
|
|
if channeled and ability then
|
||
|
|
local tick_time = ability.tick_time or ( ability.aura and class.auras[ ability.aura ].tick_time )
|
||
|
|
|
||
|
|
if tick_time and tick_time > 0 then
|
||
|
|
local eoc = state.buff.casting.expires - tick_time
|
||
|
|
|
||
|
|
while ( eoc > state.now ) do
|
||
|
|
state:QueueEvent( casting, state.buff.casting.applied, eoc, "CHANNEL_TICK", destGUID )
|
||
|
|
eoc = eoc - tick_time
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Projectile spells have two handlers, effectively. An onCast handler, and then an onImpact handler.
|
||
|
|
if ability and ability.isProjectile then
|
||
|
|
state:QueueEvent( ability.key, state.buff.casting.expires, nil, "PROJECTILE_IMPACT", destGUID )
|
||
|
|
-- state:QueueEvent( action, "projectile", true )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Delay to end of GCD.
|
||
|
|
if dispName == "Primary" or dispName == "AOE" then
|
||
|
|
local delay = 0
|
||
|
|
|
||
|
|
if state.settings.spec and state.settings.spec.gcdSync then
|
||
|
|
delay = state.cooldown.global_cooldown and state.cooldown.global_cooldown.remains or 0
|
||
|
|
end
|
||
|
|
|
||
|
|
if not state.spec.canCastWhileCasting and state.buff.casting.up and not state.buff.casting.v3 then -- v3 means it's channeled.
|
||
|
|
delay = max( delay, state.buff.casting.remains )
|
||
|
|
end
|
||
|
|
|
||
|
|
delay = ns.callHook( "reset_postcast", delay )
|
||
|
|
|
||
|
|
if delay > 0 then
|
||
|
|
if Hekili.ActiveDebug then Hekili:Debug( "Advancing by %.2f per GCD or cast or channel or reset_postcast value.", delay ) end
|
||
|
|
state.advance( delay )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
state.resetting = false
|
||
|
|
return true
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
Hekili:ProfileCPU( "state.reset", state.reset )
|
||
|
|
|
||
|
|
|
||
|
|
function state:SetConstraint( min, max )
|
||
|
|
state.delayMin = min or 0
|
||
|
|
state.delayMax = max or 3600
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state:SetWhitelist( t )
|
||
|
|
state.whitelist = t
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state:StartCombat()
|
||
|
|
self.false_start = self.query_time - 0.01
|
||
|
|
if self.swings.mainhand_speed > 0 and self.nextMH == 0 then self.swings.mh_pseudo = self.false_start end
|
||
|
|
if self.swings.offhand_speed > 0 and self.nextOH == 0 then self.swings.oh_pseudo = self.false_start + ( self.offhand_speed / 2 ) end
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state.advance( time )
|
||
|
|
|
||
|
|
if time <= 0 then
|
||
|
|
return
|
||
|
|
end
|
||
|
|
|
||
|
|
time = ns.callHook( "advance", time ) or time
|
||
|
|
if not state.resetting then time = roundUp( time, 2 ) end
|
||
|
|
|
||
|
|
state.delay = 0
|
||
|
|
|
||
|
|
local realOffset = state.offset
|
||
|
|
|
||
|
|
if state.player.queued_ability then
|
||
|
|
local lands = max( state.now + 0.01, state.player.queued_lands )
|
||
|
|
|
||
|
|
if lands > state.query_time and lands <= state.query_time + time then
|
||
|
|
state.offset = lands - state.query_time
|
||
|
|
if Hekili.ActiveDebug then Hekili:Debug( "Using queued ability '" .. state.player.queued_ability .. "' at " .. state.query_time .. "." ) end
|
||
|
|
state:RunHandler( state.player.queued_ability, true )
|
||
|
|
|
||
|
|
state.offset = realOffset
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
local events = state:GetQueue()
|
||
|
|
local event = events[ 1 ]
|
||
|
|
|
||
|
|
local eCount = 0
|
||
|
|
|
||
|
|
while( event ) do
|
||
|
|
if event.time <= state.query_time + time then
|
||
|
|
state.offset = event.time - state.now
|
||
|
|
|
||
|
|
if Hekili.ActiveDebug then Hekili:Debug( "While advancing by %.2f to %.2f, %s %s occurred at %.2f.", time, realOffset + time, event.action, event.type, state.offset ) end
|
||
|
|
|
||
|
|
state:HandleEvent( event )
|
||
|
|
|
||
|
|
event = events[ 1 ]
|
||
|
|
state.offset = realOffset
|
||
|
|
else
|
||
|
|
break
|
||
|
|
end
|
||
|
|
|
||
|
|
eCount = eCount + 1
|
||
|
|
if eCount == 10 then break end
|
||
|
|
end
|
||
|
|
|
||
|
|
for k in pairs( class.resources ) do
|
||
|
|
local resource = state[ k ]
|
||
|
|
|
||
|
|
if not resource.regenModel then
|
||
|
|
local override = ns.callHook( "advance_resource_regen", false, k, time )
|
||
|
|
|
||
|
|
if not override and resource.regen and resource.regen ~= 0 then
|
||
|
|
resource.actual = min( resource.max, max( 0, resource.actual + ( resource.regen * time ) ) )
|
||
|
|
end
|
||
|
|
else
|
||
|
|
-- revisit this, may want to forecastResources( k ) instead.
|
||
|
|
state.delay = time
|
||
|
|
resource.actual = resource.current
|
||
|
|
state.delay = 0
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
state.offset = state.offset + time
|
||
|
|
|
||
|
|
local bonus_cdr = 0 -- ns.callHook( "advance_bonus_cdr", 0 )
|
||
|
|
|
||
|
|
--[[ for k, cd in pairs( state.cooldown ) do
|
||
|
|
if state:IsKnown( k ) then
|
||
|
|
if bonus_cdr > 0 then
|
||
|
|
if cd.next_charge > 0 then
|
||
|
|
cd.next_charge = cd.next_charge - bonus_cdr
|
||
|
|
end
|
||
|
|
cd.expires = max( 0, cd.expires - bonus_cdr )
|
||
|
|
cd.true_expires = max( 0, cd.expires - bonus_cdr )
|
||
|
|
end
|
||
|
|
|
||
|
|
local ability = class.abilities[ k ]
|
||
|
|
|
||
|
|
while ability.charges and ability.charges > 1 and cd.next_charge > 0 and cd.next_charge < state.now + state.offset do
|
||
|
|
-- if class.abilities[ k ].charges and cd.next_charge > 0 and cd.next_charge < state.now + state.offset then
|
||
|
|
cd.charge = cd.charge + 1
|
||
|
|
if cd.charge < class.abilities[ k ].charges then
|
||
|
|
cd.recharge_began = cd.next_charge
|
||
|
|
cd.next_charge = cd.next_charge + class.abilities[ k ].recharge
|
||
|
|
else
|
||
|
|
cd.recharge_began = 0
|
||
|
|
cd.next_charge = 0
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end ]]
|
||
|
|
|
||
|
|
ns.callHook( "advance_end", time )
|
||
|
|
|
||
|
|
return time
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state.GetResourceType( ability )
|
||
|
|
local action = class.abilities[ ability ]
|
||
|
|
|
||
|
|
if not action then return end
|
||
|
|
|
||
|
|
if action.spend ~= nil then
|
||
|
|
if type( action.spend ) == "number" then
|
||
|
|
return action.spendType or class.primaryResource
|
||
|
|
|
||
|
|
elseif type( action.spend ) == "function" then
|
||
|
|
return select( 2, action.spend() ) or action.spendType or class.primaryResource
|
||
|
|
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
return nil
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
local hysteria_resources = {
|
||
|
|
mana = true,
|
||
|
|
rage = true,
|
||
|
|
focus = true,
|
||
|
|
energy = true,
|
||
|
|
runic_power = true,
|
||
|
|
astral_power = true,
|
||
|
|
maelstrom = true,
|
||
|
|
insanity = true,
|
||
|
|
fury = true,
|
||
|
|
pain = true
|
||
|
|
}
|
||
|
|
|
||
|
|
ns.spendResources = function( ability )
|
||
|
|
local action = class.abilities[ ability ]
|
||
|
|
|
||
|
|
if not action then return end
|
||
|
|
|
||
|
|
-- First, spend resources.
|
||
|
|
if action.spend ~= nil then
|
||
|
|
local cost, resource
|
||
|
|
|
||
|
|
if type( action.spend ) == "number" then
|
||
|
|
cost = action.spend
|
||
|
|
resource = action.spendType or class.primaryResource
|
||
|
|
elseif type( action.spend ) == "function" then
|
||
|
|
cost, resource = action.spend()
|
||
|
|
resource = resource or action.spendType or class.primaryResource
|
||
|
|
else
|
||
|
|
cost = cost or 0
|
||
|
|
resource = resource or "health"
|
||
|
|
end
|
||
|
|
|
||
|
|
if cost > 0 and cost < 1 then
|
||
|
|
cost = ( cost * state[ resource ].modmax )
|
||
|
|
end
|
||
|
|
|
||
|
|
if state.debuff.hysteria.up and hysteria_resources[ resource ] then
|
||
|
|
cost = cost + ( .03 * state.debuff.hysteria.stack * cost )
|
||
|
|
end
|
||
|
|
|
||
|
|
if cost ~= 0 then
|
||
|
|
state.spend( cost, resource )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
state.SpendResources = ns.spendResources
|
||
|
|
|
||
|
|
|
||
|
|
do
|
||
|
|
local HOLD_PERMANENT = 1
|
||
|
|
local HOLD_COMBAT = 2
|
||
|
|
|
||
|
|
function Hekili:PlaceHold( action, combat, verbose )
|
||
|
|
if not action then return end
|
||
|
|
|
||
|
|
action = action:trim()
|
||
|
|
local ability = class.abilities[ action ]
|
||
|
|
|
||
|
|
if not ability then
|
||
|
|
action = action:lower()
|
||
|
|
-- Try to auto-complete.
|
||
|
|
for k, v in orderedPairs( class.abilities ) do
|
||
|
|
if type(k) == "string" and k:sub( 1, action:len() ):lower() == action then
|
||
|
|
action = v.key
|
||
|
|
ability = class.abilities[ action ]
|
||
|
|
break
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability then
|
||
|
|
state.holds[ ability.key ] = combat and HOLD_COMBAT or HOLD_PERMANENT
|
||
|
|
if verbose then Hekili:Print( class.abilities[ ability.key ].name .. " placed on hold" .. ( combat and " until end of combat." or "." ) ) end
|
||
|
|
Hekili:ForceUpdate( "HEKILI_HOLD_APPLIED" )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
function Hekili:RemoveHold( action, verbose )
|
||
|
|
if not action then return end
|
||
|
|
|
||
|
|
action = action:trim()
|
||
|
|
local ability = class.abilities[ action ]
|
||
|
|
|
||
|
|
if not ability then
|
||
|
|
action = action:lower()
|
||
|
|
-- Try to auto-complete.
|
||
|
|
for k, v in orderedPairs( class.abilities ) do
|
||
|
|
if type(k) == "string" and k:sub( 1, action:len() ):lower() == action then
|
||
|
|
action = v.key
|
||
|
|
ability = class.abilities[ action ]
|
||
|
|
break
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability and state.holds[ ability.key ] then
|
||
|
|
state.holds[ ability.key ] = nil
|
||
|
|
if verbose then Hekili:Print( class.abilities[ ability.key ].name .. " hold removed." ) end
|
||
|
|
Hekili:ForceUpdate( "HEKILI_HOLD_REMOVED" )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
function Hekili:ToggleHold( action, combat, verbose )
|
||
|
|
if self:IsHeld( action ) then
|
||
|
|
self:RemoveHold( action, verbose )
|
||
|
|
return
|
||
|
|
end
|
||
|
|
|
||
|
|
self:PlaceHold( action, combat, verbose )
|
||
|
|
end
|
||
|
|
|
||
|
|
function Hekili:IsHeld( action )
|
||
|
|
action = action and action:trim()
|
||
|
|
local ability = class.abilities[ action ]
|
||
|
|
|
||
|
|
if not ability then
|
||
|
|
action = action:lower()
|
||
|
|
-- Try to auto-complete.
|
||
|
|
for k, v in orderedPairs( class.abilities ) do
|
||
|
|
if type(k) == "string" and k:sub( 1, action:len() ):lower() == action then
|
||
|
|
action = v.key
|
||
|
|
ability = class.abilities[ action ]
|
||
|
|
break
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability and state.holds[ ability.key ] then
|
||
|
|
return true, state.holds[ ability.key ]
|
||
|
|
end
|
||
|
|
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
|
||
|
|
function Hekili:ReleaseHolds( combat )
|
||
|
|
local holdRemoved = false
|
||
|
|
|
||
|
|
for k, v in pairs( state.holds ) do
|
||
|
|
if not combat or v == HOLD_COMBAT then
|
||
|
|
state.holds[ k ] = nil
|
||
|
|
holdRemoved = true
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if holdRemoved then Hekili:ForceUpdate( "HEKILI_COMBAT_HOLD_REMOVED" ) end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state:IsKnown( sID, notoggle )
|
||
|
|
|
||
|
|
if type(sID) ~= "number" then sID = class.abilities[ sID ] and class.abilities[ sID ].id or nil end
|
||
|
|
|
||
|
|
if not sID then
|
||
|
|
return false, "could not find valid ID" -- no ability
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
local ability = class.abilities[ sID ]
|
||
|
|
|
||
|
|
if not ability then
|
||
|
|
Error( "IsKnown() - " .. sID .. " not found in abilities table." )
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
|
||
|
|
if sID < 0 then
|
||
|
|
if ability.known ~= nil then
|
||
|
|
if type( ability.known ) == "number" then
|
||
|
|
return IsUsableItem( ability.known ), "IsUsableItem"
|
||
|
|
end
|
||
|
|
return ability.known
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.item then
|
||
|
|
return IsUsableItem( ability.item ), "IsUsableItem"
|
||
|
|
end
|
||
|
|
|
||
|
|
return true
|
||
|
|
end
|
||
|
|
|
||
|
|
local profile = Hekili.DB.profile
|
||
|
|
|
||
|
|
if ability.spec and not state.spec[ ability.spec ] then
|
||
|
|
return false, "wrong specialization"
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.nospec and state.spec[ ability.nospec ] then
|
||
|
|
return false, "spec [ " .. ability.nospec .. " ] disallowed"
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.talent and not state.talent[ ability.talent ].enabled then
|
||
|
|
return false, "talent [ " .. ability.talent .. " ] missing"
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.notalent and state.talent[ ability.notalent ].enabled then
|
||
|
|
return false, "talent [ " .. ability.notalent .. " ] disallowed"
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.pvptalent and not state.pvptalent[ ability.pvptalent ].enabled then
|
||
|
|
return false, "PvP talent [ " .. ability.pvptalent .. " ] missing"
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.nopvptalent and state.pvptalent[ ability.nopvptalent ].enabled then
|
||
|
|
return false, "PvP talent [ " ..ability.nopvptalent .. " ] disallowed"
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.trait and not state.artifact[ ability.trait ].enabled then
|
||
|
|
return false, "trait [ " .. ability.trait .. " ] missing"
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.equipped and not state.equipped[ ability.equipped ] then
|
||
|
|
return false, "equipment [ " .. ability.equipped .. " ] missing"
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.item and not ability.bagItem and not state.equipped[ ability.item ] then
|
||
|
|
return false, "item [ " .. ability.item .. " ] missing"
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.noOverride and IsSpellKnownOrOverridesKnown( ability.noOverride ) then
|
||
|
|
return false, "override [ " .. ability.noOverride .. " ] disallowed"
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.known ~= nil then
|
||
|
|
if type( ability.known ) == "number" then
|
||
|
|
return IsPlayerSpell( ability.known ) or IsSpellKnownOrOverridesKnown( ability.known ) or IsSpellKnown( ability.known, true )
|
||
|
|
end
|
||
|
|
return ability.known
|
||
|
|
end
|
||
|
|
|
||
|
|
return IsPlayerSpell( sID ) or IsSpellKnownOrOverridesKnown( sID ) or IsSpellKnown( sID, true )
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
do
|
||
|
|
local toggleSpells = {
|
||
|
|
potion = true,
|
||
|
|
cancel_buff = true,
|
||
|
|
phial_of_serenity = true,
|
||
|
|
}
|
||
|
|
|
||
|
|
-- If an ability has been manually disabled, don't consider it.
|
||
|
|
function state:IsDisabled( spell, strict )
|
||
|
|
spell = spell or self.this_action
|
||
|
|
|
||
|
|
local ability = class.abilities[ spell ]
|
||
|
|
if not ability then return false end
|
||
|
|
|
||
|
|
spell = ability.key
|
||
|
|
|
||
|
|
if self.holds[ spell ] then return true, "on hold" end
|
||
|
|
|
||
|
|
local profile = Hekili.DB.profile
|
||
|
|
local spec = profile.specs[ state.spec.id ]
|
||
|
|
|
||
|
|
local option = ability.item and spec.items[ spell ] or spec.abilities[ spell ]
|
||
|
|
|
||
|
|
if option.disabled then return true, "preference" end
|
||
|
|
if option.boss and not state.boss then return true, "boss-only" end
|
||
|
|
|
||
|
|
if not strict then
|
||
|
|
local toggle = option.toggle
|
||
|
|
if not toggle or toggle == "default" then toggle = ability.toggle end
|
||
|
|
|
||
|
|
if ability.id < -100 or ability.id > 0 or toggleSpells[ spell ] then
|
||
|
|
if state.filter ~= "none" and state.filter ~= toggle and not ability[ state.filter ] then return true, "display"
|
||
|
|
elseif ability.item and not ability.bagItem and not state.equipped[ ability.item ] then return false
|
||
|
|
elseif toggle and toggle ~= "none" then
|
||
|
|
if not self.toggle[ toggle ] or ( profile.toggles[ toggle ].separate and state.filter ~= toggle ) then return true, "toggle" end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
-- TODO: Finish this, need to support toggles that knock spells to their own display vs. toggles that disable an ability entirely.
|
||
|
|
function state:IsFiltered( spell )
|
||
|
|
if state.filter == "none" then return false end
|
||
|
|
|
||
|
|
spell = spell or self.this_action
|
||
|
|
|
||
|
|
local ability = class.abilities[ spell ]
|
||
|
|
if not ability then return false end
|
||
|
|
|
||
|
|
spell = ability.key
|
||
|
|
|
||
|
|
local profile = Hekili.DB.profile
|
||
|
|
local spec = profile.specs[ state.spec.id ]
|
||
|
|
|
||
|
|
local option = ability.item and spec.items[ spell ] or spec.abilities[ spell ]
|
||
|
|
|
||
|
|
local toggle = option.toggle
|
||
|
|
if not toggle or toggle == "default" then toggle = ability.toggle end
|
||
|
|
|
||
|
|
if ability.id < -100 or ability.id > 0 or toggleSpells[ spell ] then
|
||
|
|
if state.filter ~= "none" and state.filter ~= toggle and not ability[ state.filter ] then return true, "display"
|
||
|
|
elseif ability.item and not ability.bagItem and not state.equipped[ ability.item ] then return false, "not equipped"
|
||
|
|
elseif toggle and toggle ~= "none" then
|
||
|
|
if not self.toggle[ toggle ] or ( profile.toggles[ toggle ].separate and state.filter ~= toggle ) then return true, "toggle" end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
local LRC = LibStub( "LibRangeCheck-2.0" )
|
||
|
|
|
||
|
|
-- Filter out non-resource driven issues with abilities.
|
||
|
|
-- Unusable abilities are treated as on CD unless overridden.
|
||
|
|
function state:IsUsable( spell )
|
||
|
|
spell = spell or self.this_action
|
||
|
|
|
||
|
|
local ability = class.abilities[ spell ]
|
||
|
|
if not ability then return true end
|
||
|
|
|
||
|
|
local profile = Hekili.DB.profile
|
||
|
|
|
||
|
|
if self.rangefilter and UnitExists( "target" ) then
|
||
|
|
if LSR.IsSpellInRange( ability.rangeSpell or ability.id, "target" ) == 0 then
|
||
|
|
return false, "filtered out of range"
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.range then
|
||
|
|
local _, dist = LRC:GetRange( "target", true )
|
||
|
|
|
||
|
|
if dist and dist > ability.range then
|
||
|
|
return false, "not within ability-specified range (" .. ability.range .. ")"
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.item then
|
||
|
|
if not ability.bagItem and not self.equipped[ ability.item ] then
|
||
|
|
return false, "item not equipped"
|
||
|
|
end
|
||
|
|
else
|
||
|
|
local cfg = self.settings.spec and self.settings.spec.abilities[ ability.key ]
|
||
|
|
|
||
|
|
if cfg then
|
||
|
|
if cfg.targetMin > 0 and self.active_enemies < cfg.targetMin then
|
||
|
|
return false, "active_enemies[" .. self.active_enemies .. "] is less than ability's minimum targets [" .. cfg.targetMin .. "]"
|
||
|
|
elseif cfg.targetMax > 0 and self.active_enemies > cfg.targetMax then
|
||
|
|
return false, "active_enemies[" .. self.active_enemies .. "] is more than ability's maximum targets [" .. cfg.targetMax .. "]"
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.disabled then
|
||
|
|
return false, "ability.disabled returned true"
|
||
|
|
end
|
||
|
|
|
||
|
|
if self.args.only_cwc and ( not self.buff.casting.up or not self.buff.casting.v3 or not ability.castableWhileCasting ) then
|
||
|
|
return false, "only castable while channeling"
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.nomounted and IsMounted() then
|
||
|
|
return false, "not recommended while mounted"
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.form and not state.buff[ ability.form ].up then
|
||
|
|
return false, "required form (" .. ability.form .. ") not active"
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.noform and state.buff[ ability.noform ].up then
|
||
|
|
return false, "not usable in current form (" .. ability.noform .. ")"
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.buff and not state.buff[ ability.buff ].up then
|
||
|
|
return false, "required buff (" .. ability.buff .. ") not active"
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.debuff and not state.debuff[ ability.debuff ].up then
|
||
|
|
return false, "required debuff (" ..ability.debuff .. ") not active"
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.channeling then
|
||
|
|
local c = class.abilities[ ability.channeling ] and class.abilities[ ability.channeling ].id
|
||
|
|
|
||
|
|
if not c or state.buff.casting.remains < 0.1 or not state.buff.casting.v3 or state.buff.casting.v1 ~= c then
|
||
|
|
return false, "required channel (" .. c .. " / " .. ability.channeling .. ") not active or too short [ " .. state.buff.casting.remains .. " / " .. state.buff.casting.applied .. " / " .. state.buff.casting.expires .. " / " .. state.query_time .. " / " .. tostring( state.buff.casting.v3 ) .. " / " .. state.buff.casting.v1 .. " ]"
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if self.args.moving == 1 and state.buff.movement.down then
|
||
|
|
return false, "entry requires movement and player is not moving"
|
||
|
|
end
|
||
|
|
|
||
|
|
if self.args.moving == 0 and state.buff.movement.up then
|
||
|
|
return false, "entry requires no movement and player is moving"
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Moved this into TimeToReady; we can see when the buff falls off.
|
||
|
|
--[[ if ability.nobuff and state.buff[ ability.nobuff ].up then
|
||
|
|
return false
|
||
|
|
end ]]
|
||
|
|
|
||
|
|
local hook, reason = ns.callHook( "IsUsable", spell )
|
||
|
|
if hook == false then return false, reason end
|
||
|
|
|
||
|
|
if ability.usable ~= nil then
|
||
|
|
if type( ability.usable ) == "number" then
|
||
|
|
if IsUsableSpell( ability.usable ) then
|
||
|
|
return true
|
||
|
|
end
|
||
|
|
return false, "IsSpellUsable(" .. ability.usable .. ") was false"
|
||
|
|
elseif type( rawget( ability, "usable" ) ) == "boolean" then
|
||
|
|
return ability.usable
|
||
|
|
end
|
||
|
|
local usable, reason = ability.funcs.usable()
|
||
|
|
if usable then return true end
|
||
|
|
return false, reason or "ability 'usable' function returned false without explanation"
|
||
|
|
end
|
||
|
|
|
||
|
|
return true
|
||
|
|
end
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
ns.hasRequiredResources = function( ability )
|
||
|
|
local action = class.abilities[ ability ]
|
||
|
|
|
||
|
|
if not action then return end
|
||
|
|
|
||
|
|
-- First, spend resources.
|
||
|
|
if action.spend and action.spend ~= 0 then
|
||
|
|
local spend, resource
|
||
|
|
|
||
|
|
if type( action.spend ) == "number" then
|
||
|
|
spend = action.spend
|
||
|
|
resource = action.spendType or class.primaryResource
|
||
|
|
elseif type( action.spend ) == "function" then
|
||
|
|
spend, resource = action.spend()
|
||
|
|
end
|
||
|
|
|
||
|
|
if resource == "focus" or resource == "energy" then
|
||
|
|
-- Thought: We'll already delay CD based on time to get energy/focus.
|
||
|
|
-- So let's leave it alone.
|
||
|
|
return true
|
||
|
|
end
|
||
|
|
|
||
|
|
if spend > 0 and spend < 1 then
|
||
|
|
spend = ( spend * state[ resource ].modmax )
|
||
|
|
end
|
||
|
|
|
||
|
|
if spend > 0 then
|
||
|
|
return ( state[ resource ].current >= spend )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
return true
|
||
|
|
end
|
||
|
|
function state:HasRequiredResources( action )
|
||
|
|
return ns.hasRequiredResources( action )
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
local power_tick_rate = 0.115
|
||
|
|
|
||
|
|
|
||
|
|
-- Needs to be expanded to handle energy regen before Rogue, Monk, Druid will work.
|
||
|
|
function state:TimeToReady( action, pool )
|
||
|
|
local now = self.now + self.offset
|
||
|
|
local action = action or self.this_action
|
||
|
|
|
||
|
|
-- Need to ignore the wait for this part.
|
||
|
|
local wait = self.cooldown[ action ].remains
|
||
|
|
local ability = class.abilities[ action ]
|
||
|
|
|
||
|
|
if ability.id < -99 or ability.id > 0 then
|
||
|
|
-- if not ability.castableWhileCasting and ( ability.gcd ~= "off" or ( ability.item and not ability.essence ) or not ability.interrupt ) then
|
||
|
|
if ( ability.gcd ~= "off" or ability.castableWhileCasting ) or ( ability.item and not ability.essence ) then
|
||
|
|
wait = max( wait, self.cooldown.global_cooldown.remains )
|
||
|
|
end
|
||
|
|
|
||
|
|
if not state.canBreakChannel and not ability.castableWhileCasting and self.buff.casting.remains > 0 then
|
||
|
|
wait = max( wait, self.buff.casting.remains )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
local line_cd = state.args.line_cd
|
||
|
|
if ( line_cd and type( line_cd ) == "number" ) and ability.lastCast > self.combat then
|
||
|
|
if Hekili.Debug then Hekili:Debug( "Line CD is " .. line_cd .. ", last cast was " .. ability.lastCast .. ", remaining CD: " .. max( 0, ability.lastCast + line_cd - self.query_time ) ) end
|
||
|
|
wait = max( wait, ability.lastCast + self.args.line_cd - self.query_time )
|
||
|
|
end
|
||
|
|
|
||
|
|
local synced = state.args.sync and class.abilities[ state.args.sync ]
|
||
|
|
if synced then wait = max( wait, state.cooldown[ state.args.sync ].remains ) end
|
||
|
|
|
||
|
|
wait = ns.callHook( "TimeToReady", wait, action )
|
||
|
|
|
||
|
|
local spend, resource
|
||
|
|
|
||
|
|
if ability.spend then
|
||
|
|
if type( ability.spend ) == "number" then
|
||
|
|
spend = ability.spend
|
||
|
|
resource = ability.spendType or class.primaryResource
|
||
|
|
elseif type( ability.spend ) == "function" then
|
||
|
|
spend, resource = ability.spend()
|
||
|
|
resource = resource or ability.spendType or class.primaryResource
|
||
|
|
end
|
||
|
|
|
||
|
|
spend = ns.callHook( 'TimeToReady_spend', spend )
|
||
|
|
spend = spend or 0
|
||
|
|
|
||
|
|
if state.debuff.hysteria.up and resource and hysteria_resources[ resource ] then
|
||
|
|
spend = spend * ( 1 + 0.03 * state.debuff.hysteria.stack )
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
-- For special cases where we want to pool more of a resource than is required for usage.
|
||
|
|
if not pool and ability.readySpend then
|
||
|
|
spend = ability.readySpend
|
||
|
|
end
|
||
|
|
|
||
|
|
if spend and resource and spend > 0 and spend < 1 then
|
||
|
|
spend = spend * self[ resource ].modmax
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Okay, so we don't have enough of the resource.
|
||
|
|
if spend and resource and spend > self[ resource ].current then
|
||
|
|
wait = max( wait, self[ resource ][ "time_to_" .. spend ] or 0 )
|
||
|
|
wait = ceil( wait * 100 ) / 100 -- round to the hundredth.
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.nobuff and self.buff[ ability.nobuff ].up then
|
||
|
|
wait = max( wait, self.buff[ ability.nobuff ].remains )
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.nodebuff and self.debuff[ ability.nodebuff ].up then
|
||
|
|
wait = max( wait, self.debuff[ ability.nodebuff ].remains )
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Need to house this in an encounter module, really.
|
||
|
|
if self.debuff.repeat_performance.up and self.prev[1][ action ] then
|
||
|
|
wait = max( wait, self.debuff.repeat_performance.remains )
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.icd and self.query_time - ability.lastCast < ability.icd then
|
||
|
|
wait = max( wait, ability.lastCast + ability.icd - self.query_time )
|
||
|
|
end
|
||
|
|
|
||
|
|
-- If ready is a function, it returns time.
|
||
|
|
-- Ignore this if we are just checking pool_resources.
|
||
|
|
if not pool and ability.readyTime then
|
||
|
|
wait = max( wait, ability.readyTime )
|
||
|
|
end
|
||
|
|
|
||
|
|
if state.spec.fire and state.buff.casting.up and ( ability.id > 0 or ability.id < -99 ) and ability.gcd ~= "off" and not ability.castableWhileCasting then
|
||
|
|
wait = max( wait, state.buff.casting.remains )
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.timeToReady then
|
||
|
|
wait = max( wait, ability.timeToReady )
|
||
|
|
end
|
||
|
|
|
||
|
|
wait = max( wait, self.delayMin )
|
||
|
|
return max( wait, self.delayMin )
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state:IsReady( action )
|
||
|
|
action = action or self.this_action
|
||
|
|
local ability = action and class.abilities[ action ]
|
||
|
|
|
||
|
|
if not ability then
|
||
|
|
Hekili:Error( "Failed state:IsReady( " .. ( action or "BLANK" ) .. " )." )
|
||
|
|
return false
|
||
|
|
end
|
||
|
|
|
||
|
|
if ability.spend then
|
||
|
|
local spend, resource
|
||
|
|
|
||
|
|
if type( ability.spend ) == "number" then
|
||
|
|
spend = ability.spend
|
||
|
|
resource = ability.spendType or class.primaryResource
|
||
|
|
elseif type( ability.spend ) == "function" then
|
||
|
|
spend, resource = ability.spend()
|
||
|
|
end
|
||
|
|
|
||
|
|
if resource == "focus" or resource == "energy" or state.script.entry then
|
||
|
|
local ttr = self:TimeToReady( action )
|
||
|
|
|
||
|
|
if ttr <= self.delayMin or ttr >= self.delayMax then return false, "not ready within our time contraint"
|
||
|
|
elseif ttr >= state.delay then return false, "not ready before selected action" end
|
||
|
|
|
||
|
|
return true
|
||
|
|
end
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
return self:HasRequiredResources( action ) and self.cooldown[ action ].remains <= self.delay
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function state:IsReadyNow( action )
|
||
|
|
action = action or self.this_action
|
||
|
|
local a = class.abilities[ action ]
|
||
|
|
|
||
|
|
if not a then return false end
|
||
|
|
|
||
|
|
action = a.key
|
||
|
|
local profile = Hekili.DB.profile
|
||
|
|
local spec = profile.specs[ state.spec.id ]
|
||
|
|
local option = spec.abilities[ action ]
|
||
|
|
local clash = option.clash or 0
|
||
|
|
|
||
|
|
if self.cooldown[ action ].remains - clash > 0 then return false end
|
||
|
|
local wait = ns.callHook( "TimeToReady", 0, action )
|
||
|
|
if wait and wait > 0 then return false end
|
||
|
|
|
||
|
|
if a.ready and type( a.ready ) == "function" and a.ready() > 0 then return false end
|
||
|
|
|
||
|
|
if a.spend and a.spend ~= 0 then
|
||
|
|
local spend, resource
|
||
|
|
|
||
|
|
if type( a.spend ) == "number" then
|
||
|
|
spend = a.spend
|
||
|
|
resource = a.spendType or class.primaryResource
|
||
|
|
|
||
|
|
elseif type( a.spend ) == "function" then
|
||
|
|
spend, resource = a.spend()
|
||
|
|
|
||
|
|
end
|
||
|
|
|
||
|
|
if a.ready and type( a.ready ) == "number" then
|
||
|
|
spend = a.ready
|
||
|
|
end
|
||
|
|
|
||
|
|
if spend > 0 and spend < 1 then
|
||
|
|
spend = ( spend * state[ resource ].modmax )
|
||
|
|
end
|
||
|
|
|
||
|
|
if spend > 0 then
|
||
|
|
return state[ resource ].current >= spend
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
return true
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
function state:ClashOffset( action )
|
||
|
|
local a = class.abilities[ action ]
|
||
|
|
if not a then return 0 end
|
||
|
|
action = a.key
|
||
|
|
|
||
|
|
local profile = Hekili.DB.profile
|
||
|
|
local spec = profile.specs[ state.spec.id ]
|
||
|
|
local option = spec.abilities[ action ]
|
||
|
|
|
||
|
|
return ns.callHook( "clash", option.clash, action )
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
for k, v in pairs( state ) do
|
||
|
|
ns.commitKey( k )
|
||
|
|
end
|
||
|
|
|
||
|
|
ns.attr = { "serenity", "active", "active_enemies", "my_enemies", "active_flame_shock", "adds", "agility", "air", "armor", "attack_power", "bonus_armor", "cast_delay", "cast_time", "casting", "cooldown_react", "cooldown_remains", "cooldown_up", "crit_rating", "deficit", "distance", "down", "duration", "earth", "enabled", "energy", "execute_time", "fire", "five", "focus", "four", "gcd", "hardcasts", "haste", "haste_rating", "health", "health_max", "health_pct", "intellect", "level", "mana", "mastery_rating", "mastery_value", "max_nonproc", "max_stack", "maximum_energy", "maximum_focus", "maximum_health", "maximum_mana", "maximum_rage", "maximum_runic", "melee_haste", "miss_react", "moving", "mp5", "multistrike_pct", "multistrike_rating", "one", "pct", "rage", "react", "regen", "remains", "resilience_rating", "runic", "seal", "spell_haste", "spell_power", "spirit", "stack", "stack_pct", "stacks", "stamina", "strength", "this_action", "three", "tick_damage", "tick_dmg", "tick_time", "ticking", "ticks", "ticks_remain", "time", "time_to_die", "time_to_max", "travel_time", "two", "up", "water", "weapon_dps", "weapon_offhand_dps", "weapon_offhand_speed", "weapon_speed", "single", "aoe", "cleave", "percent", "last_judgment_target", "unit", "ready", "refreshable", "pvptalent", "conduit", "legendary", "runeforge", "covenant", "soulbind", "enabled" }
|