--[[-------------------------------------------------------------------- Ovale Spell Priority Copyright (C) 2012 Sidoine Copyright (C) 2012, 2013, 2014 Johnny C. Lam This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License in the LICENSE file accompanying this program. --]]-------------------------------------------------------------------- local _, Ovale = ... local OvaleCooldown = Ovale:NewModule("OvaleCooldown", "AceEvent-3.0") Ovale.OvaleCooldown = OvaleCooldown -- -- Forward declarations for module dependencies. local OvaleData = nil local OvaleGUID = nil local OvalePaperDoll = nil local OvaleStance = nil local OvaleState = nil local next = next local pairs = pairs local API_GetSpellCharges = GetSpellCharges local API_GetSpellCooldown = GetSpellCooldown local API_UnitHealth = UnitHealth local API_UnitHealthMax = UnitHealthMax local API_UnitClass = UnitClass -- Profiling set-up. local Profiler = Ovale.Profiler local profiler = nil do local group = OvaleCooldown:GetName() local function EnableProfiling() API_GetSpellCharges = Profiler:Wrap(group, "OvaleCooldown_API_GetSpellCharges", GetSpellCharges) API_GetSpellCooldown = Profiler:Wrap(group, "OvaleCooldown_API_GetSpellCooldown", GetSpellCooldown) API_UnitHealth = Profiler:Wrap(group, "OvaleCooldown_API_UnitHealth", UnitHealth) API_UnitHealthMax = Profiler:Wrap(group, "OvaleCooldown_API_UnitHealthMax", UnitHealthMax) end local function DisableProfiling() API_GetSpellCharges = GetSpellCharges API_GetSpellCooldown = GetSpellCooldown API_UnitHealth = UnitHealth API_UnitHealthMax = UnitHealthMax end Profiler:RegisterProfilingGroup(group, EnableProfiling, DisableProfiling) profiler = Profiler:GetProfilingGroup(group) end -- Player's class. local _, self_class = API_UnitClass("player") -- Current age of cooldown state. local self_serial = 0 -- Shared cooldown name (sharedcd) to spell table mapping. local self_sharedCooldownSpells = {} -- -- function OvaleCooldown:OnInitialize() -- Resolve module dependencies. OvaleData = Ovale.OvaleData OvaleGUID = Ovale.OvaleGUID OvalePaperDoll = Ovale.OvalePaperDoll OvaleStance = Ovale.OvaleStance OvaleState = Ovale.OvaleState end function OvaleCooldown:OnEnable() self:RegisterEvent("SPELL_UPDATE_CHARGES", "Update") self:RegisterEvent("SPELL_UPDATE_USABLE", "Update") self:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START", "Update") self:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP", "Update") self:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED", "Update") self:RegisterEvent("UNIT_SPELLCAST_START", "Update") self:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED", "Update") OvaleState:RegisterState(self, self.statePrototype) end function OvaleCooldown:OnDisable() OvaleState:UnregisterState(self) self:UnregisterEvent("SPELL_UPDATE_CHARGES") self:UnregisterEvent("SPELL_UPDATE_USABLE") self:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_START") self:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_STOP") self:UnregisterEvent("UNIT_SPELLCAST_INTERRUPTED") self:UnregisterEvent("UNIT_SPELLCAST_START") self:UnregisterEvent("UNIT_SPELLCAST_SUCCEEDED") end function OvaleCooldown:Update() -- Advance age of current cooldown state. self_serial = self_serial + 1 end -- Empty out the sharedcd table. function OvaleCooldown:ResetSharedCooldowns() for name, spellTable in pairs(self_sharedCooldownSpells) do for spellId in pairs(spellTable) do spellTable[spellId] = nil end end end function OvaleCooldown:IsSharedCooldown(name) local spellTable = self_sharedCooldownSpells[name] return (spellTable and next(spellTable) ~= nil) end function OvaleCooldown:AddSharedCooldown(name, spellId) self_sharedCooldownSpells[name] = self_sharedCooldownSpells[name] or {} self_sharedCooldownSpells[name][spellId] = true end -- Get the cooldown information for the given spell ID. If given a shared cooldown name, -- then cycle through all spells associated with that spell ID to find the cooldown -- information. function OvaleCooldown:GetSpellCooldown(spellId) local start, duration, enable if self_sharedCooldownSpells[spellId] then for id in pairs(self_sharedCooldownSpells[spellId]) do start, duration, enable = API_GetSpellCooldown(id) if start then break end end else start, duration, enable = API_GetSpellCooldown(spellId) end return start, duration, enable end -- Return the GCD after the given spellId is cast. -- If no spellId is given, then returns the GCD after a "yellow-hit" ability has been cast. function OvaleCooldown:GetGCD(spellId) -- Base global cooldown. local cd local isCaster = false if self_class == "DEATHKNIGHT" then cd = 1.0 elseif self_class == "DRUID" and OvaleStance:IsStance("druid_cat_form") then cd = 1.0 elseif self_class == "HUNTER" then cd = 1.0 elseif self_class == "MONK" then cd = 1.0 elseif self_class == "ROGUE" then cd = 1.0 else isCaster = true cd = 1.5 end -- Use SpellInfo() information if available. if spellId and OvaleData.spellInfo[spellId] then local si = OvaleData.spellInfo[spellId] if si.gcd then return si.gcd end if si.haste then if si.haste == "melee" then cd = cd / OvalePaperDoll:GetMeleeHasteMultiplier() elseif si.haste == "ranged" then cd = cd / OvalePaperDoll:GetRangedHasteMultiplier() elseif si.haste == "spell" then cd = cd / OvalePaperDoll:GetSpellHasteMultiplier() end end elseif isCaster then cd = cd / OvalePaperDoll:GetSpellHasteMultiplier() end -- Clamp GCD at 1s. cd = (cd > 1) and cd or 1 return cd end -- --[[---------------------------------------------------------------------------- State machine for simulator. --]]---------------------------------------------------------------------------- -- OvaleCooldown.statePrototype = {} -- -- local statePrototype = OvaleCooldown.statePrototype -- -- statePrototype.cd = nil -- -- -- Initialize the state. function OvaleCooldown:InitializeState(state) state.cd = {} end -- Reset the state to the current conditions. function OvaleCooldown:ResetState(state) profiler.Start("OvaleCooldown_ResetState") for _, cd in pairs(state.cd) do -- Remove outdated cooldown state. if cd.serial and cd.serial < self_serial then for k in pairs(cd) do cd[k] = nil end end end profiler.Stop("OvaleCooldown_ResetState") end -- Release state resources prior to removing from the simulator. function OvaleCooldown:CleanState(state) for spellId, cd in pairs(state.cd) do for k in pairs(cd) do cd[k] = nil end state.cd[spellId] = nil end end -- Apply the effects of the spell on the player's state, assuming the spellcast completes. function OvaleCooldown:ApplySpellAfterCast(state, spellId, targetGUID, startCast, endCast, nextCast, isChanneled, nocd, spellcast) profiler.Start("OvaleCooldown_ApplySpellAfterCast") local si = OvaleData.spellInfo[spellId] if si then local cd = state:GetCD(spellId) cd.start = isChanneled and startCast or endCast cd.duration = si.cd or 0 cd.enable = 1 -- If the spell has charges, then remove a charge. if cd.charges and cd.charges > 0 then cd.chargeStart = cd.start cd.charges = cd.charges - 1 if cd.charges == 0 then cd.duration = cd.chargeDuration end end -- Test for no cooldown. if nocd then cd.duration = 0 else -- There is no cooldown if the buff named by "buff_no_cd" parameter is present. local buffNoCooldown = si.buff_no_cd or si.buffnocd if buffNoCooldown then local aura = state:GetAura("player", buffNoCooldown) if state:IsActiveAura(aura, cd.start) then Ovale:Logf("buff_no_cd stacks = %s, start = %s, ending = %s, cd.start = %f", aura.stacks, aura.start, aura.ending, cd.start) cd.duration = 0 end end -- There is no cooldown if the target's health percent is below what's specified -- with the "target_health_pct_no_cd" parameter. local target = OvaleGUID:GetUnitId(targetGUID) local targetHealthPctNoCooldown = si.target_health_pct_no_cd or si.targetlifenocd if target and targetHealthPctNoCooldown then local healthPercent = API_UnitHealth(target) / API_UnitHealthMax(target) * 100 if healthPercent < targetHealthPctNoCooldown then cd.duration = 0 end end end if cd.duration > 0 then -- Adjust cooldown duration if it is affected by haste: "cd_haste=melee" or "cd_haste=spell". if si.cd_haste then if si.cd_haste == "melee" then cd.duration = cd.duration / state:GetMeleeHasteMultiplier(spellcast.snapshot) elseif si.haste == "ranged" then cd.duration = cd.duration / OvalePaperDoll:GetSpellHasteMultiplier() elseif si.cd_haste == "spell" then cd.duration = cd.duration / state:GetSpellHasteMultiplier(spellcast.snapshot) end end -- Adjust cooldown duration if it is affected by a cooldown reduction trinket: "buff_cdr=auraId". if si.buff_cdr then local aura = state:GetAura("player", si.buff_cdr) if state:IsActiveAura(aura, cd.start) then cd.duration = cd.duration / aura.value1 end end end Ovale:Logf("Spell %d cooldown info: start=%f, duration=%f", spellId, cd.start, cd.duration) end profiler.Stop("OvaleCooldown_ApplySpellAfterCast") end -- -- statePrototype.DebugCooldown = function(state) for spellId, cd in pairs(state.cd) do if cd.start then if cd.charges then Ovale:FormatPrint("Spell %s cooldown: start=%f, duration=%f, charges=%d, maxCharges=%d, chargeStart=%f, chargeDuration=%f", spellId, cd.start, cd.duration, cd.charges, cd.start, cd.duration) else Ovale:FormatPrint("Spell %s cooldown: start=%f, duration=%f", spellId, cd.start, cd.duration) end end end end -- Return the table holding the simulator's cooldown information for the given spell. statePrototype.GetCD = function(state, spellId) profiler.Start("OvaleCooldown_state_GetCD") local cdName = spellId local si = OvaleData.spellInfo[spellId] if si and si.sharedcd then cdName = si.sharedcd end if not state.cd[cdName] then state.cd[cdName] = {} end -- Populate the cooldown information from the current game state if it is outdated. local cd = state.cd[cdName] if not cd.start or not cd.serial or cd.serial < self_serial then local start, duration, enable = OvaleCooldown:GetSpellCooldown(spellId) if start and start > 0 then charges = 0 end if si and si.forcecd then if si.forcecd then start, duration = OvaleCooldown:GetSpellCooldown(si.forcecd) end end cd.serial = self_serial cd.start = start cd.duration = duration cd.enable = enable local charges, maxCharges, chargeStart, chargeDuration = API_GetSpellCharges(spellId) if charges then cd.charges = charges cd.maxCharges = maxCharges cd.chargeStart = chargeStart cd.chargeDuration = chargeDuration end end -- Advance the cooldown state to the current time. local now = state.currentTime if cd.start then if cd.start + cd.duration <= now then cd.start = 0 cd.duration = 0 end end if cd.charges then local charges, maxCharges, chargeStart, chargeDuration = cd.charges, cd.maxCharges, cd.chargeStart, cd.chargeDuration while chargeStart + chargeDuration <= now and charges < maxCharges do chargeStart = chargeStart + chargeDuration charges = charges + 1 end cd.charges = charges cd.chargeStart = chargeStart end profiler.Stop("OvaleCooldown_state_GetCD") return cd end -- Return the cooldown for the spell in the simulator. statePrototype.GetSpellCooldown = function(state, spellId) local cd = state:GetCD(spellId) return cd.start, cd.duration, cd.enable end -- Return the information on the number of charges for the spell in the simulator. statePrototype.GetSpellCharges = function(state, spellId) local cd = state:GetCD(spellId) return cd.charges, cd.maxCharges, cd.chargeStart, cd.chargeDuration end -- Force the cooldown of a spell to reset at the specified time. statePrototype.ResetSpellCooldown = function(state, spellId, atTime) local now = state.currentTime if atTime >= now then local cd = state:GetCD(spellId) if cd.start + cd.duration > now then cd.start = now cd.duration = atTime - now end end end --