--[[-------------------------------------------------------------------- Copyright (C) 2012 Sidoine De Wispelaere. Copyright (C) 2012, 2013, 2014 Johnny C. Lam. See the file LICENSE.txt for copying permission. --]]-------------------------------------------------------------------- local OVALE, Ovale = ... local OvaleCooldown = Ovale:NewModule("OvaleCooldown", "AceEvent-3.0") Ovale.OvaleCooldown = OvaleCooldown --<private-static-properties> local OvaleProfiler = Ovale.OvaleProfiler -- Forward declarations for module dependencies. local OvaleData = nil local OvaleGUID = nil local OvalePaperDoll = nil local OvaleSpellBook = nil local OvaleStance = nil local OvaleState = nil local next = next local pairs = pairs local API_GetSpellCharges = GetSpellCharges local API_GetSpellCooldown = GetSpellCooldown local API_UnitClass = UnitClass -- Spell ID for the dummy Global Cooldown spell. local GLOBAL_COOLDOWN = 61304 -- Register for profiling. OvaleProfiler:RegisterProfiling(OvaleCooldown) -- 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 = {} -- BASE_GCD[class] = { gcd, isCaster } local BASE_GCD = { ["DEATHKNIGHT"] = { 1.0, false }, ["DRUID"] = { 1.5, true }, ["HUNTER"] = { 1.0, false }, ["MAGE"] = { 1.5, true }, ["MONK"] = { 1.5, false }, ["PALADIN"] = { 1.5, false }, ["PRIEST"] = { 1.5, true }, ["ROGUE"] = { 1.0, false }, ["SHAMAN"] = { 1.5, true }, ["WARLOCK"] = { 1.5, true }, ["WARRIOR"] = { 1.5, false }, } -- Spells that cause haste to affect the global cooldown. local FOCUS_AND_HARMONY = 154555 local HEADLONG_RUSH = 158836 --</private-static-properties> --<public-static-methods> function OvaleCooldown:OnInitialize() -- Resolve module dependencies. OvaleData = Ovale.OvaleData OvaleGUID = Ovale.OvaleGUID OvalePaperDoll = Ovale.OvalePaperDoll OvaleSpellBook = Ovale.OvaleSpellBook 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 cdStart, cdDuration, cdEnable = 0, 0, 1 local gcdStart, gcdDuration = API_GetSpellCooldown(GLOBAL_COOLDOWN) if self_sharedCooldownSpells[spellId] then for id in pairs(self_sharedCooldownSpells[spellId]) do local start, duration, enable = self:GetSpellCooldown(id) if start then break end end else local start, duration, enable local index, bookType = OvaleSpellBook:GetSpellBookIndex(spellId) if index and bookType then start, duration, enable = API_GetSpellCooldown(index, bookType) else start, duration, enable = API_GetSpellCooldown(spellId) end if start and start > 0 then if duration > gcdDuration then -- Spell is on cooldown. cdStart, cdDuration, cdEnable = start, duration, enable else -- GCD is active, so set the start to when the spell can next be cast. cdStart = start + duration cdDuration = 0 cdEnable = enable end else -- Spell is ready now. cdStart, cdDuration, cdEnable = start, duration, enable end end return cdStart, cdDuration, cdEnable end -- Return the base GCD and caster status. function OvaleCooldown:GetBaseGCD() local gcd, isCaster local baseGCD = BASE_GCD[self_class] if baseGCD then gcd, isCaster = baseGCD[1], baseGCD[2] else gcd, isCaster = 1.5, true end if self_class == "DRUID" then if OvaleStance:IsStance("druid_cat_form") then gcd = 1.0 isCaster = false elseif OvaleStance:IsStance("druid_bear_form") then isCaster = false end elseif self_class == "MONK" then if OvaleStance:IsStance("monk_stance_of_the_fierce_tiger") then gcd = 1.0 elseif OvaleStance:IsStance("monk_stance_of_the_sturdy_ox") then gcd = 1.0 else isCaster = true end end return gcd, isCaster end --</public-static-methods> --[[---------------------------------------------------------------------------- State machine for simulator. --]]---------------------------------------------------------------------------- --<public-static-properties> OvaleCooldown.statePrototype = {} --</public-static-properties> --<private-static-properties> local statePrototype = OvaleCooldown.statePrototype --</private-static-properties> --<state-properties> statePrototype.cd = nil --</state-properties> --<public-static-methods> -- Initialize the state. function OvaleCooldown:InitializeState(state) state.cd = {} end -- Reset the state to the current conditions. function OvaleCooldown:ResetState(state) self:StartProfiling("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 self:StopProfiling("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, spellcast) self:StartProfiling("OvaleCooldown_ApplySpellAfterCast") local cd = state:GetCD(spellId) local target = OvaleGUID:GetUnitId(targetGUID) or state.defaultTarget local start = isChanneled and startCast or endCast local duration = state:GetSpellCooldownDuration(spellId, start, target) local si = OvaleData.spellInfo[spellId] if duration == 0 then cd.start = 0 cd.duration = 0 cd.enable = 1 else cd.start = start cd.duration = duration cd.enable = 1 end -- 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 state:Log("Spell %d cooldown info: start=%f, duration=%f", spellId, cd.start, cd.duration) self:StopProfiling("OvaleCooldown_ApplySpellAfterCast") end --</public-static-methods> --<state-methods> statePrototype.DebugCooldown = function(state) for spellId, cd in pairs(state.cd) do if cd.start then if cd.charges then OvaleCooldown:Print("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 OvaleCooldown:Print("Spell %s cooldown: start=%f, duration=%f", spellId, cd.start, cd.duration) end end end end -- Return the GCD after the given spell is cast. -- If no spell is given, then returns the GCD after the current spell has been cast. statePrototype.GetGCD = function(state, spellId, target) spellId = spellId or state.currentSpellId local gcd = spellId and state:GetSpellInfoProperty(spellId, "gcd", target) if not gcd then local isCaster, haste gcd, isCaster = OvaleCooldown:GetBaseGCD() if self_class == "MONK" and OvaleSpellBook:IsKnownSpell(FOCUS_AND_HARMONY) then haste = "melee" elseif self_class == "WARRIOR" and OvaleSpellBook:IsKnownSpell(HEADLONG_RUSH) then haste = "melee" end local gcd_haste = spellId and state:GetSpellInfoProperty(spellId, "gcd_haste", target) if gcd_haste then haste = gcd_haste else local si_haste = spellId and state:GetSpellInfoProperty(spellId, "haste", target) if si_haste then haste = si_haste end end if not haste and isCaster then haste = "spell" end if haste == "melee" then gcd = gcd / state:GetMeleeHasteMultiplier() elseif haste == "ranged" then gcd = gcd / state:GetRangedHasteMultiplier() elseif haste == "spell" then gcd = gcd / state:GetSpellHasteMultiplier() end -- Clamp GCD at 1s. gcd = (gcd > 1) and gcd or 1 end return gcd end -- Return the table holding the simulator's cooldown information for the given spell. statePrototype.GetCD = function(state, spellId) OvaleCooldown:StartProfiling("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 si and si.forcecd then start, duration = OvaleCooldown:GetSpellCooldown(si.forcecd) 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 OvaleCooldown:StopProfiling("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 -- Get the duration of a spell's cooldown. Returns either the current duration if -- already on cooldown or the duration if cast at the specified time. statePrototype.GetSpellCooldownDuration = function(state, spellId, atTime, target) local start, duration = state:GetSpellCooldown(spellId) if duration > 0 and start + duration > atTime then state:Log("Spell %d is on cooldown for %fs starting at %s.", spellId, duration, start) else local si = OvaleData.spellInfo[spellId] if si and si.cd then duration = state:GetSpellInfoProperty(spellId, "cd", target) if si.addcd then duration = duration + si.addcd end if duration < 0 then duration = 0 end else duration = 0 end state:Log("Spell %d has a base cooldown of %fs.", spellId, duration) if duration > 0 then -- Adjust cooldown duration if it is affected by haste: "cd_haste=melee" or "cd_haste=spell". if si.cd_haste then local cd_haste = state:GetSpellInfoProperty(spellId, "cd_haste", target) if cd_haste == "melee" then duration = duration / state:GetMeleeHasteMultiplier() elseif cd_haste == "ranged" then duration = duration / OvalePaperDoll:GetSpellHasteMultiplier() elseif cd_haste == "spell" then duration = duration / state:GetSpellHasteMultiplier() 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, atTime) then duration = duration * aura.value1 end end end end return duration 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 --</state-methods>