Quantcast

Remove rune handling in OvaleState in favor of an OvaleRunes module.

Johnny C. Lam [11-15-13 - 19:54]
Remove rune handling in OvaleState in favor of an OvaleRunes module.

OvaleRunes tracks the current rune information via events instead of
polling through the Blizzard API, and also keeps rune state for the
simulator.

git-svn-id: svn://svn.curseforge.net/wow/ovale/mainline/trunk@1159 d5049fe3-3747-40f7-a4b5-f36d6801af5f
Filename
Ovale.toc
OvaleBestAction.lua
OvaleRunes.lua
OvaleState.lua
conditions/RuneCount.lua
conditions/Runes.lua
diff --git a/Ovale.toc b/Ovale.toc
index b34e42c..b5ffc7f 100644
--- a/Ovale.toc
+++ b/Ovale.toc
@@ -47,6 +47,7 @@ OvaleCooldown.lua
 OvaleOptions.lua
 OvalePower.lua
 OvaleRecount.lua
+OvaleRunes.lua
 OvaleSkada.lua
 OvaleSpellDamage.lua
 OvaleSwing.lua
diff --git a/OvaleBestAction.lua b/OvaleBestAction.lua
index 755901c..fa10f92 100644
--- a/OvaleBestAction.lua
+++ b/OvaleBestAction.lua
@@ -752,7 +752,8 @@ function OvaleBestAction:GetActionInfo(element)
 			end
 			if actionCooldownStart and actionCooldownDuration then
 				if si.blood or si.frost or si.unholy or si.death then
-					local runecd = OvaleState:GetRunesCooldown(si.blood, si.frost, si.unholy, si.death, false)
+					-- Spell requires runes.
+					local runecd = state:GetRunesCooldown(si.blood, si.unholy, si.frost, si.death, false)
 					if runecd > actionCooldownStart + actionCooldownDuration then
 						actionCooldownDuration = runecd - actionCooldownStart
 					end
diff --git a/OvaleRunes.lua b/OvaleRunes.lua
new file mode 100644
index 0000000..64c2f10
--- /dev/null
+++ b/OvaleRunes.lua
@@ -0,0 +1,429 @@
+--[[--------------------------------------------------------------------
+    Ovale Spell Priority
+    Copyright (C) 2012 Sidoine
+    Copyright (C) 2012, 2013 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.
+--]]
+
+local _, Ovale = ...
+local OvaleRunes = Ovale:NewModule("OvaleRunes", "AceEvent-3.0")
+Ovale.OvaleRunes = OvaleRunes
+
+--<private-static-properties>
+local OvaleData = Ovale.OvaleData
+local OvalePaperDoll = Ovale.OvalePaperDoll
+local OvaleSpellBook = Ovale.OvaleSpellBook
+local OvaleStance = Ovale.OvaleStance
+local OvaleState = Ovale.OvaleState
+
+local ipairs = ipairs
+local pairs = pairs
+local select = select
+local API_GetRuneCooldown = GetRuneCooldown
+local API_GetRuneType = GetRuneType
+local API_GetTime = GetTime
+local API_UnitClass = UnitClass
+
+-- Player's class.
+local self_class = select(2, 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 },
+}
+
+--[[
+	In-game testing shows that death runes are preferred in the order:
+		Frost death runes > Blood death runes > Unholy death runes
+--]]
+local DEATH_RUNE_PRIORITY = { 5, 6, 1, 2, 4, 5 }
+
+-- Improved Blood Presence increases rune regenerate rate by 20%.
+local IMPROVED_BLOOD_PRESENCE = 50371
+--</private-static-properties>
+
+--<public-static-properties>
+-- Current rune information, indexed by slot.
+OvaleRunes.rune = {}
+OvaleRunes.RUNE_TYPE = RUNE_TYPE
+--</public-static-properties>
+
+--<public-static-methods>
+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] = { slotType = runeType }
+			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
+	rune.active = runeReady
+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.active 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.
+--]]----------------------------------------------------------------------------
+
+--<public-static-properties>
+OvaleRunes.statePrototype = {
+	rune = nil,				-- indexed by slot (1 through 6)
+}
+--</public-static-properties>
+
+--<public-static-methods>
+-- Initialize the state.
+function OvaleRunes:InitializeState(state)
+	state.rune = {}
+	for slot = 1, 6 do
+		state.rune[slot] = {}
+	end
+end
+
+-- Reset the state to the current conditions.
+function OvaleRunes:ResetState(state)
+	for slot = 1, 6 do
+		local rune = state.rune[slot]
+		for k, v in pairs(self.rune[slot]) do
+			rune[k] = v
+		end
+	end
+end
+
+-- Apply the effects of the spell on the player's state, assuming the spellcast completes.
+function OvaleRunes:ApplySpellOnPlayer(state, spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
+	-- If the spellcast has already ended, then the effects on the player have already occurred.
+	if endCast <= OvaleState.now then
+		return
+	end
+
+	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
+				state:ConsumeRune(endCast, name)
+				count = count - 1
+			end
+		end
+	end
+end
+--</public-static-methods>
+
+-- Mix-in methods for simulator state.
+do
+	local statePrototype = OvaleRunes.statePrototype
+
+	function statePrototype:DebugRunes()
+		local now = OvaleState.currentTime
+		for slot = 1, 6 do
+			local rune = self.rune[slot]
+			if rune.active 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
+
+	-- Consume a rune of the given type.  Assume that the required runes are available.
+	function statePrototype:ConsumeRune(atTime, name)
+		local state = self
+		--[[
+			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.active 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
+					if rune.type == DEATH_RUNE and rune.active 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
+			for _, slot in ipairs(DEATH_RUNE_PRIORITY) do
+				local rune = state.rune[slot]
+				if rune.type == DEATH_RUNE and rune.active 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 k = consumedRune.slotType
+			local start = atTime
+			for _, slot in ipairs(RUNE_SLOTS[consumedRune.slotType]) do
+				local rune = state.rune[slot]
+				if rune.endCooldown > start then
+					start = rune.endCooldown
+				end
+			end
+			local duration = 10 / OvalePaperDoll:GetSpellHasteMultiplier()
+			if OvaleStance:IsStance("death_knight_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
+			consumedRune.active = false
+		else
+			Ovale:Errorf("No %s rune available to consume!", RUNE_NAME[runeType])
+		end
+	end
+
+	function statePrototype:RuneCount(name, death)
+		local state = self
+		local count = 0
+		local startCooldown, endCooldown = math.huge, math.huge
+		local runeType = RUNE_TYPE[name]
+		if runeType ~= DEATH_RUNE then
+			if deathCondition == "any" then
+				-- Match runes of the given type or any death runes.
+				for slot, rune in ipairs(state.rune) do
+					if rune.type == runeType or rune.type == DEATH_RUNE then
+						if rune.active then
+							count = count + 1
+						elseif rune.endCooldown < endCooldown then
+							startCooldown, endCooldown = rune.startCooldown, rune.endCooldown
+						end
+					end
+				end
+			else
+				-- 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" and rune.type ~= DEATH_RUNE) then
+						if rune.active then
+							count = count + 1
+						elseif rune.endCooldown < endCooldown then
+							startCooldown, endCooldown = rune.startCooldown, rune.endCooldown
+						end
+					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.active 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
+		-- If the rune is active, then return the remaining active runes count requirement.
+		-- Also return the time of the next rune becoming active.
+		local function MatchRune(rune, count, endCooldown)
+			if count > 0 then
+				count = count - 1
+				if rune.endCooldown > endCooldown then
+					endCooldown = rune.endCooldown
+				end
+			else
+				if rune.endCooldown < endCooldown then
+					endCooldown = rune.endCooldown
+				end
+			end
+			return count, endCooldown
+		end
+
+		-- The remaining count requirements, indexed by rune type.
+		local runeCount = {}
+		-- The latest time till a rune of that type is off cooldown, indexed by rune type.
+		local runeEndCooldown = {}
+
+		function statePrototype:GetRunesCooldown(blood, unholy, frost, death, deathCondition)
+			local state = self
+
+			-- Initialize static variables.
+			runeCount[BLOOD_RUNE] = blood or 0
+			runeCount[UNHOLY_RUNE] = unholy or 0
+			runeCount[FROST_RUNE] = frost or 0
+			runeCount[DEATH_RUNE] = death or 0
+			runeEndCooldown[BLOOD_RUNE] = 0
+			runeEndCooldown[UNHOLY_RUNE] = 0
+			runeEndCooldown[FROST_RUNE] = 0
+			runeEndCooldown[DEATH_RUNE] = 0
+
+			-- Use regular runes to meet the count requirements.
+			for slot, rune in ipairs(state.rune) do
+				if rune.type ~= DEATH_RUNE then
+					local runeType = rune.type
+					local count, endCooldown = MatchRune(rune, runeCount[runeType], runeEndCooldown[runeType])
+					runeCount[runeType] = count
+					runeEndCooldown[runeType] = endCooldown
+				end
+			end
+			-- Use death runes of the matching rune type to meet the count requirements.
+			if deathCondition ~= "none" then
+				for slot, rune in ipairs(state.rune) do
+					if rune.type == DEATH_RUNE then
+						local runeType = rune.slotType
+						local count, endCooldown = MatchRune(rune, runeCount[runeType], runeEndCooldown[runeType])
+						runeCount[runeType] = count
+						runeEndCooldown[runeType] = endCooldown
+					end
+				end
+			end
+
+			-- Remaining rune requirements that have not yet been met.
+			local remainingCount = 0
+			for runeType = 1, 4 do
+				remainingCount = remainingCount + runeCount[runeType]
+			end
+
+			-- Use death runes of any type to meet any remaining count requirements.
+			if deathCondition == "any" then
+				for _, slot in ipairs(DEATH_RUNE_PRIORITY) do
+					local rune = state.rune[slot]
+					local runeType = DEATH_RUNE
+					local count, endCooldown = MatchRune(rune, remainingCount, runeEndCooldown[runeType])
+					remainingCount = count
+					runeEndCooldown[runeType] = endCooldown
+				end
+			end
+
+			-- This shouldn't happen because it means the rune requirements will never be met.
+			if remainingCount > 0 then
+				Ovale:Logf("Impossible rune count requirements: blood=%d, unholy=%d, frost=%d, death=%d", blood, unholy, frost, death)
+				return math.huge
+			end
+
+			local maxEndCooldown = 0
+			for runeType = 1, 4 do
+				if runeEndCooldown[runeType] > maxEndCooldown then
+					maxEndCooldown = runeEndCooldown[runeType]
+				end
+			end
+			if maxEndCooldown > 0 then
+				return maxEndCooldown - OvaleState.currentTime
+			end
+			return 0
+		end
+	end
+end
diff --git a/OvaleState.lua b/OvaleState.lua
index 9eda4dd..a098fbc 100644
--- a/OvaleState.lua
+++ b/OvaleState.lua
@@ -8,8 +8,9 @@
     file accompanying this program.
 --]]--------------------------------------------------------------------

--- Keep the current state in the simulation
--- XXX Split out Runes module.
+--[[
+	This addon is the core of the state machine for the simulator.
+--]]

 local _, Ovale = ...
 local OvaleState = Ovale:NewModule("OvaleState")
@@ -20,28 +21,19 @@ local OvaleData = Ovale.OvaleData
 local OvaleQueue = Ovale.OvaleQueue

 local pairs = pairs
-local select = select
-local API_GetRuneCooldown = GetRuneCooldown
-local API_GetRuneType = GetRuneType
 local API_GetTime = GetTime
-local API_UnitClass = UnitClass

 local self_statePrototype = {}
 local self_stateModules = OvaleQueue:NewQueue("OvaleState_stateModules")

-local self_runes = {}
-local self_runesCD = {}
-
--- Player's class.
-local self_class = select(2, API_UnitClass("player"))
 -- Whether the state of the simulator has been initialized.
 local self_stateIsInitialized = false
 --</private-static-properties>

 --<public-static-properties>
--- The state in the current frame
+-- The state for the simulator.
 OvaleState.state = {}
--- The spell being cast
+-- The spell being cast.
 OvaleState.currentSpellId = nil
 OvaleState.now = nil
 OvaleState.currentTime = nil
@@ -52,16 +44,6 @@ OvaleState.lastSpellId = nil
 --</public-static-properties>

 --<private-static-methods>
--- XXX The way this function updates the rune state looks completely wrong.
-local function AddRune(atTime, runeType, value)
-	local self = OvaleState
-	for i = 1, 6 do
-		local rune = self.state.rune[i]
-		if (rune.type == runeType or rune.type == 4) and rune.cd <= atTime then
-			rune.cd = atTIme + 10
-		end
-	end
-end
 --</private-static-methods>

 --<public-static-methods>
@@ -110,39 +92,18 @@ end

 function OvaleState:InitializeState()
 	self:InvokeMethod("InitializeState")
-
-	self.state.rune = {}
-	for i = 1, 6 do
-		self.state.rune[i] = {}
-	end
-
 	self_stateIsInitialized = true
 end

 function OvaleState:Reset()
-	self.lastSpellId = Ovale.lastSpellcast and Ovale.lastSpellcast.spellId
 	self.currentTime = self.now
 	Ovale:Logf("Reset state with current time = %f", self.currentTime)
+
+	self.lastSpellId = Ovale.lastSpellcast and Ovale.lastSpellcast.spellId
 	self.currentSpellId = nil
 	self.nextCast = self.now

 	self:InvokeMethod("ResetState")
-
-	if self_class == "DEATHKNIGHT" then
-		for i=1,6 do
-			self.state.rune[i].type = API_GetRuneType(i)
-			local start, duration, runeReady = API_GetRuneCooldown(i)
-			self.state.rune[i].duration = duration
-			if runeReady then
-				self.state.rune[i].cd = start
-			else
-				self.state.rune[i].cd = duration + start
-				if self.state.rune[i].cd<0 then
-					self.state.rune[i].cd = 0
-				end
-			end
-		end
-	end
 end

 --[[
@@ -186,105 +147,8 @@ function OvaleState:ApplySpell(spellId, startCast, endCast, nextCast, nocd, targ
 			2. Spell effects on player assuming the cast completes.
 			3. Spell effects on target when it lands.
 	--]]
-	self:ApplySpellStart(spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
-	self:ApplySpellOnPlayer(spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
-	self:ApplySpellOnTarget(spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
-end
-
--- Apply the effects of the spell at the start of the spellcast.
-function OvaleState:ApplySpellStart(spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
 	self:InvokeMethod("ApplySpellStart", spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
-end
-
--- Apply the effects of the spell on the player's state, assuming the spellcast completes.
-function OvaleState:ApplySpellOnPlayer(spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
 	self:InvokeMethod("ApplySpellOnPlayer", spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
-	--[[
-		If the spellcast has already ended, then the effects have already occurred,
-		so only consider spells that have not yet finished casting in the simulator.
-	--]]
-	if endCast > self.now then
-		local si = OvaleData.spellInfo[spellId]
-		if si then
-			-- Runes
-			if si.blood and si.blood < 0 then
-				AddRune(startCast, 1, si.blood)
-			end
-			if si.unholy and si.unholy < 0 then
-				AddRune(startCast, 2, si.unholy)
-			end
-			if si.frost and si.frost < 0 then
-				AddRune(startCast, 3, si.frost)
-			end
-			if si.death and si.death < 0 then
-				AddRune(startCast, 4, si.death)
-			end
-		end
-	end
-end
-
--- Apply the effects of the spell on the target's state when it lands on the target.
-function OvaleState:ApplySpellOnTarget(spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
 	self:InvokeMethod("ApplySpellOnTarget", spellId, startCast, endCast, nextCast, nocd, targetGUID, spellcast)
 end
-
--- Returns the cooldown time before all of the required runes are available.
-function OvaleState:GetRunesCooldown(blood, frost, unholy, death, nodeath)
-	local nombre = 0
-	local nombreCD = 0
-	local maxCD = nil
-
-	for i=1,4 do
-		self_runesCD[i] = 0
-	end
-
-	self_runes[1] = blood or 0
-	self_runes[2] = frost or 0
-	self_runes[3] = unholy or 0
-	self_runes[4] = death or 0
-
-	for i=1,6 do
-		local rune = self.state.rune[i]
-		if rune then
-			if self_runes[rune.type] > 0 then
-				self_runes[rune.type] = self_runes[rune.type] - 1
-				if rune.cd > self_runesCD[rune.type] then
-					self_runesCD[rune.type] = rune.cd
-				end
-			elseif rune.cd < self_runesCD[rune.type] then
-				self_runesCD[rune.type] = rune.cd
-			end
-		end
-	end
-
-	if not nodeath then
-		for i=1,6 do
-			local rune = self.state.rune[i]
-			if rune and rune.type == 4 then
-				for j=1,3 do
-					if self_runes[j]>0 then
-						self_runes[j] = self_runes[j] - 1
-						if rune.cd > self_runesCD[j] then
-							self_runesCD[j] = rune.cd
-						end
-						break
-					elseif rune.cd < self_runesCD[j] then
-						self_runesCD[j] = rune.cd
-						break
-					end
-				end
-			end
-		end
-	end
-
-	for i=1,4 do
-		if self_runes[i]> 0 then
-			return nil
-		end
-		if not maxCD or self_runesCD[i]>maxCD then
-			maxCD = self_runesCD[i]
-		end
-	end
-	return maxCD
-end
 --</public-static-methods>
diff --git a/conditions/RuneCount.lua b/conditions/RuneCount.lua
index 6f53e4e..a1709ec 100644
--- a/conditions/RuneCount.lua
+++ b/conditions/RuneCount.lua
@@ -12,27 +12,25 @@ local _, Ovale = ...

 do
 	local OvaleCondition = Ovale.OvaleCondition
+	local OvaleRunes = Ovale.OvaleRunes
 	local OvaleState = Ovale.OvaleState

+	local Compare = OvaleCondition.Compare
 	local TestValue = OvaleCondition.TestValue

-	local RUNE_TYPE = {
-		blood = 1,
-		unholy = 2,
-		frost = 3,
-		death = 4,
-	}
-
-	--- Get the current number of runes of the given type for death knights.
+	--- Get the current number of active runes of the given type for death knights.
 	-- @name RuneCount
 	-- @paramsig number or boolean
 	-- @param type The type of rune.
 	--     Valid values: blood, frost, unholy, death
 	-- @param operator Optional. Comparison operator: less, atMost, equal, atLeast, more.
 	-- @param number Optional. The number to compare against.
-	-- @param death Sets whether death runes can fulfill the rune count requirements. If set to 1, then death runes are allowed.
-	--     Defaults to death=0 (zero).
-	--     Valid values: 0, 1.
+	-- @param death Sets how death runes are used to fulfill the rune count requirements.
+	--     If not set, then only death runes of the proper rune type are used.
+	--     If set with "death=0", then no death runes are used.
+	--     If set with "death=1", then death runes of any rune type are used.
+	--     Default is unset.
+	--     Valid values: unset, 0, 1
 	-- @return The number of runes.
 	-- @return A boolean value for the result of the comparison.
 	-- @usage
@@ -40,33 +38,18 @@ do
 	--     Spell(obliterate)

 	local function RuneCount(condition)
-		local runeType, comparator, limit = condition[1], condition[2], condition[3]
-		local death = condition.death
-		local state = OvaleState.state
-		runeType = RUNE_TYPE[runeType]
+		local name, comparator, limit = condition[1], condition[2], condition[3]
+		local deathCondition = condition.death

-		-- Loop through the rune state and count the number of runes that match the given rune type.
-		local value, origin, rate = 0, nil, nil
-		for i = 1, 6 do
-			local rune = state.rune[i]
-			if rune and (rune.type == runeType or (rune.type == 4 and death == 1)) then
-				if rune.cd > OvaleState.currentTime then
-					-- Rune matches but is on cooldown.
-					if not origin or rune.cd < origin then
-						origin = rune.cd
-						rate = 1 / rune.duration
-					end
-				else
-					-- Rune matches and is available, so increment the counter.
-					value = value + 1
-				end
-			end
-		end
-		if not origin then
-			origin, rate = 0, 0
+		local state = OvaleState.state
+		local count, startCooldown, endCooldown = state:RuneCount(name, deathCondition)
+		if startCooldown < math.huge then
+			local origin = startCooldown
+			local rate = 1 / (endCooldown - startCooldown)
+			local start, ending = startCooldown, math.huge
+			return TestValue(start, ending, count, origin, rate, comparator, limit)
 		end
-		local start, ending = OvaleState.currentTime, math.huge
-		return TestValue(start, ending, value, origin, rate, comparator, limit)
+		return Compare(count, comparator, limit)
 	end

 	OvaleCondition:RegisterCondition("runecount", false, RuneCount)
diff --git a/conditions/Runes.lua b/conditions/Runes.lua
index 7f57218..f7e9063 100644
--- a/conditions/Runes.lua
+++ b/conditions/Runes.lua
@@ -12,56 +12,66 @@ local _, Ovale = ...

 do
 	local OvaleCondition = Ovale.OvaleCondition
+	local OvaleRunes = Ovale.OvaleRunes
 	local OvaleState = Ovale.OvaleState

-	local wipe = table.wipe
+	local RUNE_TYPE = OvaleRunes.RUNE_TYPE

-	local RUNE_TYPE = {
-		blood = 1,
-		unholy = 2,
-		frost = 3,
-		death = 4
-	}
+	local ParseRuneCondition = nil
+	do
+		local runes = {}

-	local runes = {}
-
-	local function ParseRuneCondition(condition)
-		wipe(runes)
-
-		local k = 1
-		while true do
-			local runeType, count = condition[2*k - 1], condition[2*k]
-			if not RUNE_TYPE[runeType] then break end
-			runes[runeType] = runes[runeType] + count
-			k = k + 1
+		function ParseRuneCondition(condition)
+			for name in pairs(RUNE_TYPE) do
+				runes[name] = 0
+			end
+			local k = 1
+			while true do
+				local name, count = condition[2*k - 1], condition[2*k]
+				if not RUNE_TYPE[name] then break end
+				runes[name] = runes[name] + count
+				k = k + 1
+			end
+			local deathCondition
+			if condition.death == 0 then
+				deathCondition = "none"
+			elseif condition.death == 1 then
+				deathCondition = "any"
+			end
+			-- Legacy parameter "nodeath"; no longer documented.
+			if not condition.death and condition.nodeath == 1 then
+				deathCondition = "none"
+			elseif condition.nodeath == 0 then
+				deathCondition = "any"
+			end
+			return runes.blood, runes.unholy, runes.frost, runes.death, deathCondition
 		end
-		return runes.blood, runes.frost, runes.unholy, runes.death, condition.nodeath
 	end

-	--- Test if the current rune count meets the minimum rune requirements set out in the parameters.
+	--- Test if the current active rune counst meets the minimum rune requirements set out in the parameters.
 	-- This condition takes pairs of "type number" to mean that there must be a minimum of number runes of the named type.
-	-- E.g., Runes(blood 1 frost 1 unholy 1) means at least one blood, one frost, and one unholy rune is available, death runes included.
+	-- E.g., Runes(blood 1 frost 1 unholy 1) means at least one blood, one frost, and one unholy rune is available.
 	-- @name Runes
 	-- @paramsig boolean
 	-- @param type The type of rune.
 	--     Valid values: blood, frost, unholy, death
 	-- @param number The number of runes
 	-- @param ... Optional. Additional "type number" pairs for minimum rune requirements.
-	-- @param nodeath Sets whether death runes can fulfill the rune count requirements. If set to 0, then death runes are allowed.
-	--     Defaults to nodeath=0 (zero).
-	--     Valid values: 0, 1.
+	-- @param death Sets how death runes are used to fulfill the rune count requirements.
+	--     If not set, then only death runes of the proper rune type are used.
+	--     If set with "death=0", then no death runes are used.
+	--     If set with "death=1", then death runes of any rune type are used.
+	--     Default is unset.
+	--     Valid values: unset, 0, 1
 	-- @return A boolean value.
 	-- @usage
 	-- if Runes(frost 1) Spell(howling_blast)

 	local function Runes(condition)
-		local blood, frost, unholy, death, nodeath = ParseRuneCondition(condition)
-		local seconds = OvaleState:GetRunesCooldown(blood, frost, unholy, death, nodeath)
-		local boolean = (seconds == 0)
-		if boolean then
-			return 0, math.huge
-		end
-		return nil
+		local blood, unholy, frost, death, deathCondition = ParseRuneCondition(condition)
+		local state = OvaleState.state
+		local seconds = state:GetRunesCooldown(blood, unholy, frost, death, deathCondition)
+		return OvaleState.currentTime + seconds, math.huge
 	end

 	--- Get the number of seconds before the rune conditions are met.
@@ -74,21 +84,19 @@ do
 	--     Valid values: blood, frost, unholy, death
 	-- @param number The number of runes
 	-- @param ... Optional. Additional "type number" pairs for minimum rune requirements.
-	-- @param nodeath Sets whether death runes can fulfill the rune count requirements. If set to 0, then death runes are allowed.
-	--     Defaults to nodeath=0 (zero).
-	--     Valid values: 0, 1.
+	-- @param death Sets how death runes are used to fulfill the rune count requirements.
+	--     If not set, then only death runes of the proper rune type are used.
+	--     If set with "death=0", then no death runes are used.
+	--     If set with "death=1", then death runes of any rune type are used.
+	--     Default is unset.
+	--     Valid values: unset, 0, 1
 	-- @return The number of seconds.

 	local function RunesCooldown(condition)
-		local blood, frost, unholy, death, nodeath = ParseRuneCondition(condition)
-		local seconds = OvaleState:GetRunesCooldown(blood, frost, unholy, death, nodeath)
-		if seconds then
-			if seconds < OvaleState.now then
-				seconds = OvaleState.now
-			end
-			return 0, OvaleState.currentTime + seconds, seconds, OvaleState.currentTime, -1
-		end
-		return nil
+		local blood, unholy, frost, death, deathCondition = ParseRuneCondition(condition)
+		local state = OvaleState.state
+		local seconds = state:GetRunesCooldown(blood, unholy, frost, death, deathCondition)
+		return 0, OvaleState.currentTime + seconds, seconds, OvaleState.currentTime, -1
 	end

 	OvaleCondition:RegisterCondition("runes", false, Runes)