--[[-------------------------------------------------------------------- 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. --]]-------------------------------------------------------------------- --[[ This addon tracks rune information on death knights. TODO: Handle spells in the simulator that reactivate runes, e.g., Empower Rune Weapon, Blood Tap, etc. --]] local _, Ovale = ... local OvaleRunes = Ovale:NewModule("OvaleRunes", "AceEvent-3.0") Ovale.OvaleRunes = OvaleRunes --<private-static-properties> -- Forward declarations for module dependencies. local OvaleData = nil local OvalePower = nil local OvaleSpellBook = nil local OvaleStance = nil local OvaleState = nil --local debugprint = print local ipairs = ipairs local pairs = pairs local API_GetRuneCooldown = GetRuneCooldown local API_GetRuneType = GetRuneType local API_GetSpellInfo = GetSpellInfo local API_GetTime = GetTime local API_UnitClass = UnitClass -- Player's class. local _, self_class = API_UnitClass("player") local BLOOD_RUNE = 1 local UNHOLY_RUNE = 2 local FROST_RUNE = 3 local DEATH_RUNE = 4 local RUNE_TYPE = { blood = BLOOD_RUNE, unholy = UNHOLY_RUNE, frost = FROST_RUNE, death = DEATH_RUNE, } local RUNE_NAME = {} do for k, v in pairs(RUNE_TYPE) do RUNE_NAME[v] = k end end --[[ Rune slots are numbered as follows in the default UI: blood frost unholy [1][2] [5][6] [3][4] --]] local RUNE_SLOTS = { [BLOOD_RUNE] = { 1, 2 }, [UNHOLY_RUNE] = { 3, 4 }, [FROST_RUNE] = { 5, 6 }, } --[[ From SimulationCraft's sc_death_knight.cpp: If explicitly consuming a death rune, any death runes available are preferred in the order: frost > blood > unholy. If consuming a non-death rune of a given type and that no rune of that type is available, any death runes available are preferred in the order: blood > unholy > frost --]] local DEATH_RUNE_PRIORITY = { 3, 4, 5, 6, 1, 2 } local ANY_RUNE_PRIORITY = { 1, 2, 3, 4, 5, 6 } -- Improved Blood Presence increases rune regenerate rate by 20%. local IMPROVED_BLOOD_PRESENCE = 50371 -- Blood of the North (frost) permanently transforms Blood Runes into Death Runes. local BLOOD_OF_THE_NORTH = 54637 -- Blood Rites (blood) causes the Frost and Unholy runes consumed by Death Strike to reactivate as Death runes. local BLOOD_RITES = 50034 local BLOOD_RITES_ATTACK = { [49998] = API_GetSpellInfo(49998), -- Death Strike } -- Reaping (unholy) causes the runes consumed by Blood Strike, Pestilence, Festering Strike, Icy Touch or Blood Boil to reactivate as Death Runes. local REAPING = 56835 local REAPING_ATTACK = { [45477] = API_GetSpellInfo(45477), -- Icy Touch [45902] = API_GetSpellInfo(45902), -- Blood Strike [48721] = API_GetSpellInfo(48721), -- Blood Boil [50842] = API_GetSpellInfo(50842), -- Pestilence [85948] = API_GetSpellInfo(85948), -- Festering Strike } -- Empower Rune Weapon immediately reactivates all runes. local EMPOWER_RUNE_WEAPON = 47568 -- 4pT16 tanking bonus causes Dancing Rune Weapon to reactivate immediately all Frost and Unholy runes as Death runes. local DANCING_RUNE_WEAPON = 49028 --</private-static-properties> --<public-static-properties> -- Current rune information, indexed by slot. OvaleRunes.rune = {} OvaleRunes.RUNE_TYPE = RUNE_TYPE --</public-static-properties> --<private-static-methods> local function IsActiveRune(rune, atTime) return (rune.startCooldown == 0 or rune.endCooldown <= atTime) end --</private-static-methods> --<public-static-methods> function OvaleRunes:OnInitialize() -- Resolve module dependencies. OvaleData = Ovale.OvaleData OvalePower = Ovale.OvalePower OvaleSpellBook = Ovale.OvaleSpellBook OvaleStance = Ovale.OvaleStance OvaleState = Ovale.OvaleState end function OvaleRunes:OnEnable() if self_class == "DEATHKNIGHT" then -- Initialize rune database. for runeType, slots in ipairs(RUNE_SLOTS) do for _, slot in pairs(slots) do self.rune[slot] = { slot = slot, slotType = runeType, IsActiveRune = IsActiveRune } end end self:RegisterEvent("PLAYER_ENTERING_WORLD", "UpdateAllRunes") self:RegisterEvent("PLAYER_LOGIN", "UpdateAllRunes") self:RegisterEvent("RUNE_POWER_UPDATE") self:RegisterEvent("RUNE_TYPE_UPDATE") self:RegisterEvent("UNIT_RANGEDDAMAGE") self:RegisterEvent("UNIT_SPELL_HASTE", "UNIT_RANGEDDAMAGE") OvaleState:RegisterState(self, self.statePrototype) end end function OvaleRunes:OnDisable() if self_class == "DEATHKNIGHT" then self:UnregisterEvent("PLAYER_ENTERING_WORLD") self:UnregisterEvent("PLAYER_LOGIN") self:UnregisterEvent("RUNE_POWER_UPDATE") self:UnregisterEvent("RUNE_TYPE_UPDATE") self:UnregisterEvent("UNIT_RANGEDDAMAGE") self:UnregisterEvent("UNIT_SPELL_HASTE") OvaleState:UnregisterState(self) self.rune = {} end end function OvaleRunes:RUNE_POWER_UPDATE(event, slot, usable) self:UpdateRune(slot) end function OvaleRunes:RUNE_TYPE_UPDATE(event, slot) self:UpdateRune(slot) end function OvaleRunes:UNIT_RANGEDDAMAGE(event, unitId) if unitId == "player" then self:UpdateAllRunes() end end function OvaleRunes:UpdateRune(slot) local rune = self.rune[slot] local runeType = API_GetRuneType(slot) local start, duration, runeReady = API_GetRuneCooldown(slot) rune.type = runeType if start > 0 then -- Rune is on cooldown. rune.startCooldown = start rune.endCooldown = start + duration else -- Rune is active. rune.startCooldown = 0 rune.endCooldown = 0 end end function OvaleRunes:UpdateAllRunes() for slot = 1, 6 do self:UpdateRune(slot) end end function OvaleRunes:Debug() local now = API_GetTime() for slot = 1, 6 do local rune = self.rune[slot] if rune:IsActiveRune(now) then Ovale:FormatPrint("rune[%d] (%s) is active.", slot, RUNE_NAME[rune.type]) else Ovale:FormatPrint("rune[%d] (%s) comes off cooldown in %f seconds.", slot, RUNE_NAME[rune.type], rune.endCooldown - now) end end end --</public-static-methods> --[[---------------------------------------------------------------------------- State machine for simulator. AFTER: OvalePower --]]---------------------------------------------------------------------------- --<public-static-properties> OvaleRunes.statePrototype = {} --</public-static-properties> --<private-static-properties> local statePrototype = OvaleRunes.statePrototype --</private-static-properties> --<state-properties> -- indexed by slot (1 through 6) statePrototype.rune = nil --</state-properties> --<public-static-methods> -- Initialize the state. function OvaleRunes:InitializeState(state) state.rune = {} for slot in ipairs(self.rune) do state.rune[slot] = {} end end -- Reset the state to the current conditions. function OvaleRunes:ResetState(state) for slot, rune in ipairs(self.rune) do local stateRune = state.rune[slot] for k, v in pairs(rune) do stateRune[k] = v end end end -- Release state resources prior to removing from the simulator. function OvaleRunes:CleanState(state) for slot, rune in ipairs(state.rune) do for k in pairs(rune) do rune[k] = nil end state.rune[slot] = nil end end -- Apply the effects of the spell at the start of the spellcast. function OvaleRunes:ApplySpellStartCast(state, spellId, targetGUID, startCast, endCast, nextCast, isChanneled, nocd, spellcast) -- Channeled spells cost resources at the start of the channel. if isChanneled then state:ApplyRuneCost(spellId, startCast, spellcast) end end -- Apply the effects of the spell on the player's state, assuming the spellcast completes. function OvaleRunes:ApplySpellAfterCast(state, spellId, targetGUID, startCast, endCast, nextCast, isChanneled, nocd, spellcast) -- Instant or cast-time spells cost resources at the end of the spellcast. if not isChanneled then state:ApplyRuneCost(spellId, endCast, spellcast) if spellId == EMPOWER_RUNE_WEAPON then -- Empower Rune Weapon immediately reactivates all runes. for slot in ipairs(state.rune) do state:ReactivateRune(slot, endCast) end elseif spellId == DANCING_RUNE_WEAPON and OvaleEquipement:GetArmorSetCount("T16_tank") >= 4 then -- 4pT16 tanking bonus causes Dancing Rune Weapon to reactivate immediately all Frost and Unholy runes as Death runes. for slot in ipairs(RUNE_SLOTS[FROST_RUNE]) do state:ReactivateRune(slot, endCast, DEATH_RUNE) end for slot in ipairs(RUNE_SLOTS[UNHOLY_RUNE]) do state:ReactivateRune(slot, endCast, DEATH_RUNE) end end end end --</public-static-methods> --<state-methods> statePrototype.DebugRunes = function(state) Ovale:FormatPrint("Current rune state:") local now = state.currentTime for slot, rune in ipairs(state.rune) do if rune:IsActiveRune(now) then Ovale:FormatPrint(" rune[%d] (%s) is active.", slot, RUNE_NAME[rune.type]) else Ovale:FormatPrint(" rune[%d] (%s) comes off cooldown in %f seconds.", slot, RUNE_NAME[rune.type], rune.endCooldown - now) end end end -- Update the rune state with the rune cost of the give spell. statePrototype.ApplyRuneCost = function(state, spellId, atTime, spellcast) local si = OvaleData.spellInfo[spellId] if si then for i, name in ipairs(RUNE_NAME) do local count = si[name] or 0 while count > 0 do local snapshot = spellcast and spellcast.snapshot or nil state:ConsumeRune(spellId, atTime, name, snapshot) count = count - 1 end end end end -- Reactivate the rune in the given slot. If runeType is given, then reactivate as that type of rune. statePrototype.ReactivateRune = function(state, slot, atTime, runeType) local rune = state.rune[slot] if atTime < state.currentTime then atTime = state.currentTime end if rune.startCooldown > atTime then rune.startCooldown = atTime end rune.endCooldown = atTime if runeType then rune.type = runeType end end -- Consume a rune of the given type. Assume that the required runes are available. statePrototype.ConsumeRune = function(state, spellId, atTime, name, snapshot) --[[ Find a usable rune, preferring a regular rune of that rune type over death runes of that rune type over death runes of any rune type. --]] local consumedRune local runeType = RUNE_TYPE[name] if runeType ~= DEATH_RUNE then -- Search for an active regular rune of the given rune type. for _, slot in ipairs(RUNE_SLOTS[runeType]) do local rune = state.rune[slot] if rune.type == runeType and rune:IsActiveRune(atTime) then consumedRune = rune break end end if not consumedRune then -- Search for an active death rune of the given rune type. for _, slot in ipairs(RUNE_SLOTS[runeType]) do local rune = state.rune[slot] if rune.type == DEATH_RUNE and rune:IsActiveRune(atTime) then consumedRune = rune break end end end end -- No runes of the right type are active, so look for any active death rune. if not consumedRune then local deathRunePriority = (runeType == DEATH_RUNE) and DEATH_RUNE_PRIORITY or ANY_RUNE_PRIORITY for _, slot in ipairs(deathRunePriority) do local rune = state.rune[slot] if rune.type == DEATH_RUNE and rune:IsActiveRune(atTime) then consumedRune = rune break end end end if consumedRune then -- Put that rune on cooldown, starting when the other rune of that slot type comes off cooldown. local slotType = consumedRune.slotType local start = atTime for _, slot in ipairs(RUNE_SLOTS[slotType]) do local rune = state.rune[slot] if rune.endCooldown > start then start = rune.endCooldown end end local duration = 10 / state:GetSpellHasteMultiplier(snapshot) if OvaleStance:IsStance("deathknight_blood_presence") and OvaleSpellBook:IsKnownSpell(IMPROVED_BLOOD_PRESENCE) then -- Improved Blood Presence increases rune regeneration rate by 20%. duration = duration / 1.2 end consumedRune.startCooldown = start consumedRune.endCooldown = start + duration -- Set the type of rune that this consumed rune will reactivate as. if slotType == BLOOD_RUNE and OvaleSpellBook:IsKnownSpell(BLOOD_OF_THE_NORTH) then -- Blood of the North (frost) permanently transforms Blood Runes into Death Runes. consumedRune.type = DEATH_RUNE elseif (slotType == FROST_RUNE or slotType == UNHOLY_RUNE) and BLOOD_RITES_ATTACK[spellId] and OvaleSpellBook:IsKnownSpell(BLOOD_RITES) then -- Blood Rites (blood) causes the Frost and Unholy runes consumed by Death Strike to reactivate as Death runes. consumedRune.type = DEATH_RUNE elseif REAPING_ATTACK[spellId] and OvaleSpellBook:IsKnownSpell(REAPING) then -- Reaping (unholy) causes the runes consumed by Blood Strike, Pestilence, Festering Strike, Icy Touch or -- Blood Boil to reactivate as Death Runes. consumedRune.type = DEATH_RUNE else -- In all other cases, runes reactivate according to their slot type. consumedRune.type = slotType end -- Each rune consumed generates 10 (12, if in Frost Presence) runic power. local runicpower = state.runicpower if OvaleStance:IsStance("deathknight_frost_presence") then runicpower = runicpower + 12 else runicpower = runicpower + 10 end local maxi = OvalePower.maxPower.runicpower state.runicpower = (runicpower < maxi) and runicpower or maxi else Ovale:FormatPrint("No %s rune available at %f to consume for spell %d!", RUNE_NAME[runeType], atTime, spellId) end end -- Returns a triplet of count, startCooldown, endCooldown: -- count The number of currently active runes of the given type. -- startCooldown The time at which the next rune of the given type went on cooldown. -- endCooldown The time at which the next rune of the given type will be active. statePrototype.RuneCount = function(state, name, atTime) -- Default to checking the rune count at the end of the current spellcast in the -- simulator, or at the current time if no spell is being cast. if not atTime then if state.endCast and state.endCast > state.currentTime then atTime = state.endCast else atTime = state.currentTime end end local count = 0 local startCooldown, endCooldown = math.huge, math.huge local runeType = RUNE_TYPE[name] if runeType ~= DEATH_RUNE then -- Match only the runes of the given type. for _, slot in ipairs(RUNE_SLOTS[runeType]) do local rune = state.rune[slot] if not deathCondition or ((deathCondition == "none" or deathCondition == 0) and rune.type ~= DEATH_RUNE) then if rune:IsActiveRune(atTime) then count = count + 1 elseif rune.endCooldown < endCooldown then startCooldown, endCooldown = rune.startCooldown, rune.endCooldown end end end else -- Match any requested death runes. for slot, rune in ipairs(state.rune) do if rune.type == DEATH_RUNE then if rune:IsActiveRune(atTime) then count = count + 1 elseif rune.endCooldown < endCooldown then startCooldown, endCooldown = rune.startCooldown, rune.endCooldown end end end end return count, startCooldown, endCooldown end -- Returns the number of seconds before all of the required runes are available. statePrototype.GetRunesCooldown = nil do -- The remaining count requirements, indexed by rune type. local count = {} local usedRune = {} statePrototype.GetRunesCooldown = function(state, blood, unholy, frost, death, atTime) -- Default to checking runes at the end of the current spellcast in the -- simulator, or at the current time if no spell is being cast. if not atTime then if state.endCast and state.endCast > state.currentTime then atTime = state.endCast else atTime = state.currentTime end end -- Initialize static variables. count[BLOOD_RUNE] = blood or 0 count[UNHOLY_RUNE] = unholy or 0 count[FROST_RUNE] = frost or 0 count[DEATH_RUNE] = death or 0 wipe(usedRune) for runeType in pairs(RUNE_SLOTS) do -- Match active, regular runes. for _, slot in pairs(RUNE_SLOTS[runeType]) do if count[runeType] == 0 then break end local rune = state.rune[slot] if not usedRune[rune] and rune.type ~= DEATH_RUNE and IsActiveRune(rune, atTime) then --debugprint(string.format(" [1] Match active regular rune in slot %d to %s", slot, RUNE_NAME[runeType])) usedRune[rune] = true count[runeType] = count[runeType] - 1 end end -- Match active death runes of the same socket type. for _, slot in pairs(RUNE_SLOTS[runeType]) do if count[runeType] == 0 then break end local rune = state.rune[slot] if not usedRune[rune] and rune.type == DEATH_RUNE and IsActiveRune(rune, atTime) then --debugprint(string.format(" [2] Match active death rune in slot %d to %s, type = %s", slot, RUNE_NAME[slotType])) usedRune[rune] = true count[runeType] = count[runeType] - 1 end end end -- Match active death runes in DEATH_RUNE_PRIORITY order to meet death count requirements. for _, slot in ipairs(DEATH_RUNE_PRIORITY) do if count[DEATH_RUNE] == 0 then break end local rune = state.rune[slot] if not usedRune[rune] and rune.type == DEATH_RUNE and IsActiveRune(rune, atTime) then --debugprint(string.format(" [3] Match active death rune in slot %d", slot)) usedRune[rune] = true count[DEATH_RUNE] = count[DEATH_RUNE] - 1 end end -- At this point, if count[runeType] > 0 then there are no active runes of the appropriate type that match that requirement. -- Match active death runes in ANY_RUNE_PRIORITY order to meet remaining count requirements. for _, runeType in pairs(RUNE_TYPE) do for _, slot in ipairs(ANY_RUNE_PRIORITY) do if count[runeType] == 0 then break end local rune = state.rune[slot] if not usedRune[rune] and rune.type == DEATH_RUNE and IsActiveRune(rune, atTime) then --debugprint(string.format(" [4] Match active death rune in slot %d to %s", slot, RUNE_NAME[runeType])) usedRune[rune] = true count[runeType] = count[runeType] - 1 end end end -- At this point, there are no more active runes, death or otherwise, that can satisfy count requirements. for runeType, slotList in pairs(RUNE_SLOTS) do -- Match regenerating runes of the appropriate socket type. if count[runeType] > 0 then local slot1, slot2 = slotList[1], slotList[2] local rune1, rune2 = state.rune[slot1], state.rune[slot2] if count[runeType] == 1 then local rune, slot if not usedRune[rune1] and not usedRune[rune2] then rune = (rune1.endCooldown < rune2.endCooldown) and rune1 or rune2 slot = (rune1.endCooldown < rune2.endCooldown) and slot1 or slot2 elseif not usedRune[rune1] then rune = rune1 slot = slot1 elseif not usedRune[rune2] then rune = rune2 slot = slot2 end if rune then --debugprint(string.format(" [5] Match regenerating rune in slot %d to %s", slot, RUNE_NAME[runeType])) usedRune[rune] = true count[runeType] = 0 end else -- if count[runeType] == 2 then if not usedRune[rune1] and not usedRune[rune2] then --debugprint(string.format(" [5] Match regenerating rune in slot %d to %s", slot1, RUNE_NAME[runeType])) --debugprint(string.format(" [5] Match regenerating rune in slot %d to %s", slot2, RUNE_NAME[runeType])) usedRune[rune1] = true usedRune[rune2] = true count[runeType] = 0 elseif not usedRune[rune1] then --debugprint(string.format(" [5] Match regenerating rune in slot %d to %s", slot1, RUNE_NAME[runeType])) usedRune[rune1] = true count[runeType] = 1 elseif not usedRune[rune2] then --debugprint(string.format(" [5] Match regenerating rune in slot %d to %s", slot2, RUNE_NAME[runeType])) usedRune[rune2] = true count[runeType] = 1 end end end -- Match any unused, regenerating death runes. for slot, rune in pairs(state.rune) do if count[runeType] == 0 then break end if not usedRune[rune] and rune.type == DEATH_RUNE then --debugprint(string.format(" [6] Match regenerating rune in slot %d to %s", slot, RUNE_NAME[runeType])) usedRune[rune] = true count[runeType] = count[runeType] - 1 end end end -- Replace any used runes with a regenerating death rune with a shorter cooldown. for slot, rune in pairs(state.rune) do if not usedRune[rune] and rune.type == DEATH_RUNE then for used in pairs(usedRune) do if rune.endCooldown < used.endCooldown then --debugprint(string.format(" [7] Replacing matched rune in slot %d with regenerating rune in slot %d", used.slot, slot)) usedRune[used] = nil usedRune[rune] = true break end end end end -- This shouldn't happen because it means the rune requirements will never be met. for _, runeType in pairs(RUNE_TYPE) do if count[runeType] > 0 then Ovale:Logf("Impossible rune count requirements: blood=%d, unholy=%d, frost=%d, death=%d", blood, unholy, frost, death) return math.huge end end local maxEndCooldown = 0 for rune in pairs(usedRune) do if maxEndCooldown < rune.endCooldown then maxEndCooldown = rune.endCooldown end end if maxEndCooldown > atTime then return maxEndCooldown - atTime end return 0 end end --</state-methods>