Quantcast

Add OvaleTotem module to keep state for player's totems.

Johnny C. Lam [12-15-14 - 15:46]
Add OvaleTotem module to keep state for player's totems.

OvaleTotem tracks everything that is considered to be a totem in-game.
This includes shaman totems, mage's Rune of Power and Prismatic Crystal,
druid Wild Mushroom, and monk statues.  Casting a spell that summons a
totem in the simulator will now actually summon the totem in the
simulator.

Modify script conditions to use the new state methods provided by
OvaleTotem.  RuneOfPowerRemaining() is deprecated and the Totem*()
conditions now directly take the ID of the spell that summons the totem.

Decorate totem spell descriptions in the default scripts with information
used by OvaleTotem.
Filename
Ovale.toc
SimulationCraft.lua
Totem.lua
conditions.lua
scripts/ovale_druid_spells.lua
scripts/ovale_mage_spells.lua
scripts/ovale_monk_spells.lua
scripts/ovale_shaman_spells.lua
diff --git a/Ovale.toc b/Ovale.toc
index 106f8a1..7432344 100644
--- a/Ovale.toc
+++ b/Ovale.toc
@@ -72,6 +72,7 @@ ShadowWordDeath.lua
 Skada.lua
 SpellDamage.lua
 SteadyFocus.lua
+Totem.lua

 conditions.lua
 scripts\files.xml
diff --git a/SimulationCraft.lua b/SimulationCraft.lua
index 6b09dd4..e4e78f3 100644
--- a/SimulationCraft.lua
+++ b/SimulationCraft.lua
@@ -186,27 +186,6 @@ local EMIT_DISAMBIGUATION = {}
 local EMIT_EXTRA_PARAMETERS = {}
 local OPERAND_TOKEN_PATTERN = "[^.]+"

-local TOTEM_TYPE = {
-	["prismatic_crystal"] = "crystal",	-- XXX
-	["capacitor_totem"] = "air",
-	["cloudburst_totem"] = "water",
-	["earth_elemental_totem"] = "earth",
-	["earthbind_totem"] = "earth",
-	["earthgrab_totem"] = "earth",
-	["fire_elemental_totem"] = "fire",
-	["grounding_totem"] = "air",
-	["healing_stream_totem"] = "water",
-	["healing_tide_totem"] = "water",
-	["magma_totem"] = "fire",
-	["mana_tide_totem"] = "water",
-	["searing_totem"] = "fire",
-	["spirit_link_totem"] = "air",
-	["storm_elemental_totem"] = "air",
-	["stone_bulwark_totem"] = "earth",
-	["tremor_totem"] = "earth",
-	["windwalk_totem"] = "air",
-}
-
 local POTION_STAT = {
 	["draenic_agility"]		= "agility",
 	["draenic_armor"]		= "armor",
@@ -1065,6 +1044,23 @@ local function InitializeDisambiguation()
 	AddDisambiguation("shield_barrier",			"shield_barrier_tank",			"WARRIOR",		"protection")
 end

+local function IsTotem(name)
+	if strsub(name, 1, 13) == "wild_mushroom" then
+		-- Druids.
+		return true
+	elseif name == "prismatic_crystal" or name == "rune_of_power" then
+		-- Mages.
+		return true
+	elseif strsub(name, -7, -1) == "_statue" then
+		-- Monks.
+		return true
+	elseif strsub(name, -6, -1) == "_totem" then
+		-- Shamans.
+		return true
+	end
+	return false
+end
+
 local EMIT_VISITOR = nil
 -- Forward declarations of code generation functions.
 local Emit = nil
@@ -2003,14 +1999,8 @@ EmitOperandAction = function(operand, parseNode, nodeList, annotation, action, t

 	local code
 	if property == "active" then
-		if strsub(name, -6) == "_totem" then
-			local totemType = TOTEM_TYPE[name]
-			if totemType then
-				code = format("TotemPresent(%s totem=%s)", totemType, name)
-			else
-				code = format("TotemPresent(%s)", name)
-				symbol = false
-			end
+		if IsTotem(name) then
+			code = format("TotemPresent(%s)", name)
 		else
 			code = format("%s%sPresent(%s)", target, prefix, buffName)
 			symbol = buffName
@@ -2047,14 +2037,8 @@ EmitOperandAction = function(operand, parseNode, nodeList, annotation, action, t
 	elseif property == "recharge_time" then
 		code = format("SpellChargeCooldown(%s)", name)
 	elseif property == "remains" then
-		if strsub(name, -6) == "_totem" then
-			local totemType = TOTEM_TYPE[name]
-			if totemType then
-				code = format("TotemRemaining(%s totem=%s)", totemType, name)
-			else
-				code = format("TotemRemaining(%s)", name)
-				symbol = false
-			end
+		if IsTotem(name) then
+			code = format("TotemRemaining(%s)", name)
 		else
 			code = format("%s%sRemaining(%s)", buffTarget, prefix, buffName)
 			symbol = buffName
@@ -2478,21 +2462,13 @@ EmitOperandPet = function(operand, parseNode, nodeList, annotation, action)
 		local name = tokenIterator()
 		local property = tokenIterator()
 		name = Disambiguate(name, annotation.class, annotation.specialization)
-		local totemType = TOTEM_TYPE[name]
+		local isTotem = IsTotem(name)

 		local code
-		if property == "active" then
-			if totemType then
-				code = format("TotemPresent(%s totem=%s)", totemType, name)
-			else
-				code = format("TotemPresent(%s)", name)
-			end
-		elseif property == "remains" then
-			if totemType then
-				code = format("TotemRemaining(%s totem=%s)", totemType, name)
-			else
-				code = format("TotemRemaining(%s)", name)
-			end
+		if isTotem and property == "active" then
+			code = format("TotemPresent(%s)", name)
+		elseif isTotem and property == "remains" then
+			code = format("TotemRemaining(%s)", name)
 		else
 			-- Strip the "pet.<name>." from the operand and re-evaluate.
 			local pattern = format("^pet%%.%s%%.([%%w_.]+)", name)
@@ -2522,9 +2498,7 @@ EmitOperandPet = function(operand, parseNode, nodeList, annotation, action)
 		if ok and code then
 			annotation.astAnnotation = annotation.astAnnotation or {}
 			node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation)
-			if totemType then
-				AddSymbol(annotation, name)
-			end
+			AddSymbol(annotation, name)
 		end
 	else
 		ok = false
@@ -2732,7 +2706,7 @@ EmitOperandSpecial = function(operand, parseNode, nodeList, annotation, action,
 		AddSymbol(annotation, fbName)
 		AddSymbol(annotation, ffbName)
 	elseif class == "MAGE" and operand == "buff.rune_of_power.remains" then
-		code = "RuneOfPowerRemaining()"
+		code = "TotemRemaining(rune_of_power)"
 	elseif class == "MAGE" and operand == "dot.frozen_orb.ticking" then
 		-- The Frozen Orb is ticking if fewer than 10s have elapsed since it was cast.
 		local name = "frozen_orb"
diff --git a/Totem.lua b/Totem.lua
new file mode 100644
index 0000000..b620d22
--- /dev/null
+++ b/Totem.lua
@@ -0,0 +1,325 @@
+--[[--------------------------------------------------------------------
+    Copyright (C) 2014 Johnny C. Lam.
+    See the file LICENSE.txt for copying permission.
+--]]--------------------------------------------------------------------
+
+local OVALE, Ovale = ...
+local OvaleTotem = Ovale:NewModule("OvaleTotem", "AceEvent-3.0")
+Ovale.OvaleTotem = OvaleTotem
+
+--<private-static-properties>
+local OvaleProfiler = Ovale.OvaleProfiler
+
+-- Forward declarations for module dependencies.
+local OvaleData = nil
+local OvaleSpellBook = nil
+local OvaleState = nil
+
+local ipairs = ipairs
+local pairs = pairs
+local API_GetTotemInfo = GetTotemInfo
+local API_UnitClass = UnitClass
+local AIR_TOTEM_SLOT = AIR_TOTEM_SLOT		-- FrameXML\Constants
+local EARTH_TOTEM_SLOT = EARTH_TOTEM_SLOT	-- FrameXML\Constants
+local FIRE_TOTEM_SLOT = FIRE_TOTEM_SLOT		-- FrameXML\Constants
+local INFINITY = math.huge
+local MAX_TOTEMS = MAX_TOTEMS				-- FrameXML\Constants
+local WATER_TOTEM_SLOT = WATER_TOTEM_SLOT	-- FrameXML\Constants
+
+-- Register for profiling.
+OvaleProfiler:RegisterProfiling(OvaleTotem)
+
+-- Player's class.
+local _, self_class = API_UnitClass("player")
+-- Current age of totem state.
+local self_serial = 0
+
+-- Classes that can have totems.
+local TOTEM_CLASS = {
+	DRUID = true,			-- Wild Mushroom
+	MAGE = true,			-- Rune of Power, Prismatic Crystal
+	MONK = true,			-- Summon Black Ox Statue, Summon Jade Serpent Statue
+	SHAMAN = true,			-- Totems
+}
+
+-- Maps totem type to the totem slot.
+local TOTEM_SLOT = {
+	air = AIR_TOTEM_SLOT,
+	earth = EARTH_TOTEM_SLOT,
+	fire = FIRE_TOTEM_SLOT,
+	water = WATER_TOTEM_SLOT,
+}
+
+-- Shaman's Totemic Recall destroys all totems.
+local TOTEMIC_RECALL = 36936
+--</private-static-properties>
+
+--<public-static-properties>
+-- Current totem information, indexed by slot.
+OvaleTotem.totem = {}
+--</public-static-properties>
+
+--<public-static-methods>
+function OvaleTotem:OnInitialize()
+	-- Resolve module dependencies.
+	OvaleData = Ovale.OvaleData
+	OvaleSpellBook = Ovale.OvaleSpellBook
+	OvaleState = Ovale.OvaleState
+end
+
+function OvaleTotem:OnEnable()
+	if TOTEM_CLASS[self_class] then
+		self:RegisterEvent("PLAYER_ENTERING_WORLD", "Update")
+		self:RegisterEvent("PLAYER_TALENT_UPDATE", "Update")
+		self:RegisterEvent("PLAYER_TOTEM_UPDATE", "Update")
+		self:RegisterEvent("UPDATE_SHAPESHIFT_FORM", "Update")
+		OvaleState:RegisterState(self, self.statePrototype)
+	end
+end
+
+function OvaleTotem:OnDisable()
+	if TOTEM_CLASS[self_class] then
+		OvaleState:UnregisterState(self)
+		self:UnregisterEvent("PLAYER_ENTERING_WORLD")
+		self:UnregisterEvent("PLAYER_TALENT_UPDATE")
+		self:UnregisterEvent("PLAYER_TOTEM_UPDATE")
+		self:UnregisterEvent("UPDATE_SHAPESHIFT_FORM")
+	end
+end
+
+function OvaleTotem:Update()
+	-- Advance age of current totem state.
+	self_serial = self_serial + 1
+end
+--</public-static-methods>
+
+--[[----------------------------------------------------------------------------
+	State machine for simulator.
+--]]----------------------------------------------------------------------------
+
+--<public-static-properties>
+OvaleTotem.statePrototype = {}
+--</public-static-properties>
+
+--<private-static-properties>
+local statePrototype = OvaleTotem.statePrototype
+--</private-static-properties>
+
+--<state-properties>
+-- Totem state, indexed by slot (1 through 4).
+statePrototype.totem = nil
+--</state-properties>
+
+--<public-static-methods>
+-- Initialize the state.
+function OvaleTotem:InitializeState(state)
+	state.totem = {}
+	for slot = 1, MAX_TOTEMS do
+		state.totem[slot] = {}
+	end
+end
+
+-- Reset the state to the current conditions.
+function OvaleTotem:ResetState(state)
+	self:StartProfiling("OvaleTotem_ResetState")
+	for _, totem in pairs(state.totem) do
+		-- Remove outdated totems.
+		if totem.serial and totem.serial < self_serial then
+			for k in pairs(totem) do
+				totem[k] = nil
+			end
+		end
+	end
+	self:StopProfiling("OvaleTotem_ResetState")
+end
+
+-- Release state resources prior to removing from the simulator.
+function OvaleTotem:CleanState(state)
+	for slot, totem in pairs(state.totem) do
+		for k in pairs(totem) do
+			totem[k] = nil
+		end
+		state.totem[slot] = nil
+	end
+end
+
+-- Apply the effects of the spell on the player's state, assuming the spellcast completes.
+function OvaleTotem:ApplySpellAfterCast(state, spellId, targetGUID, startCast, endCast, nextCast, isChanneled, spellcast)
+	self:StartProfiling("OvaleTotem_ApplySpellAfterCast")
+	if self_class == "SHAMAN" and spellId == TOTEMIC_RECALL then
+		-- Shaman's Totemic Recall destroys all totems.
+		for slot in ipairs(state.totem) do
+			state:DestroyTotem(slot)
+		end
+	else
+		local slot = state:GetTotemSlot(spellId)
+		if slot then
+			state:SummonTotem(spellId, slot)
+		end
+	end
+	self:StopProfiling("OvaleTotem_ApplySpellAfterCast")
+end
+--</public-static-methods>
+
+--<state-methods>
+-- Return the table holding the simulator's totem information for the given slot.
+statePrototype.GetTotem = function(state, slot)
+	OvaleTotem:StartProfiling("OvaleTotem_state_GetTotem")
+	slot = TOTEM_SLOT[slot] or slot
+	-- Populate the totem information from the current game state if it is outdated.
+	local totem = state.totem[slot]
+	if totem then
+		if not totem.isActive or not totem.serial or totem.serial < self_serial then
+			local haveTotem, name, startTime, duration, icon = API_GetTotemInfo(slot)
+			totem.isActive = haveTotem
+			totem.name = name
+			totem.start = startTime
+			totem.duration = duration
+			totem.icon = icon
+			totem.serial = self_serial
+		end
+		-- Advance the totem state to the current time.
+		if totem.isActive and totem.start + totem.duration <= state.currentTime then
+			state:DestroyTotem(slot)
+		end
+	end
+	OvaleTotem:StopProfiling("OvaleTotem_state_GetTotem")
+	return totem
+end
+
+-- Return the totem information in the given slot in the simulator.
+statePrototype.GetTotemInfo = function(state, slot)
+	local haveTotem, name, startTime, duration, icon
+	slot = TOTEM_SLOT[slot] or slot
+	local totem = state:GetTotem(slot)
+	if totem then
+		haveTotem = totem.isActive
+		name = totem.name
+		startTime = totem.start
+		duration = totem.duration
+		icon = totem.icon
+	end
+	return haveTotem, name, startTime, duration, icon
+end
+
+-- Return the number of totems previously summoned by the spell and the interval of time that at least one totem is active.
+statePrototype.GetTotemCount = function(state, spellId)
+	local start, ending
+	local count = 0
+	local si = OvaleData.spellInfo[spellId]
+	if si and si.totem then
+		local buffPresent = true
+		-- "buff_totem" is the ID of the aura applied by the totem summoned by the spell.
+		-- If the aura is absent, then the totem is considered to be expired.
+		if si.buff_totem then
+			local aura = state:GetAura("player", si.buff_totem)
+			buffPresent = state:IsActiveAura(aura)
+		end
+		if buffPresent then
+			local texture = OvaleSpellBook:GetSpellTexture(spellId)
+			-- "max_totems" is the maximum number of the totem that can be summoned concurrently.
+			-- Default to allowing only one such totem.
+			local maxTotems = si.max_totems or 1
+			for slot in ipairs(state.totem) do
+				local totem = state:GetTotem(slot)
+				if totem.isActive and totem.icon == texture then
+					count = count + 1
+					-- Save earliest start time.
+					if not start or start > totem.start then
+						start = totem.start
+					end
+					-- Save latest ending time.
+					if not ending or ending < totem.start + totem.duration then
+						ending = totem.start + totem.duration
+					end
+				end
+				if count >= maxTotems then
+					break
+				end
+			end
+		end
+	end
+	return count, start, ending
+end
+
+-- Return the totem slot that will contain the totem summoned by the spell.
+statePrototype.GetTotemSlot = function(state, spellId)
+	OvaleTotem:StartProfiling("OvaleTotem_state_GetTotemSlot")
+	local totemSlot
+	local si = OvaleData.spellInfo[spellId]
+	if si and si.totem then
+		-- Check if the totem summoned by the spell maps to a known totem slot.
+		totemSlot = TOTEM_SLOT[si.totem]
+		if not totemSlot then
+			-- Find the first available totem slot.
+			local availableSlot
+			for slot in ipairs(state.totem) do
+				local totem = state:GetTotem(slot)
+				if not totem.isActive then
+					availableSlot = slot
+					break
+				end
+			end
+
+			local texture = OvaleSpellBook:GetSpellTexture(spellId)
+			-- "max_totems" is the maximum number of the totem that can be summoned concurrently.
+			-- Default to allowing only one such totem.
+			local maxTotems = si.max_totems or 1
+			local count = 0
+			-- Find the totem slot with the oldest such totem.
+			local start = INFINITY
+			for slot in ipairs(state.totem) do
+				local totem = state:GetTotem(slot)
+				if totem.isActive and totem.icon == texture then
+					count = count + 1
+					if start > totem.start then
+						start = totem.start
+						totemSlot = slot
+					end
+				end
+			end
+			-- If there are fewer than the maximum number of totems, then summon into the first available slot.
+			if count < maxTotems then
+				totemSlot = availableSlot
+			end
+		end
+		-- Catch-all: if there are no totem slots for the spell, then summon the totem into the first totem slot.
+		totemSlot = totemSlot or 1
+	end
+	OvaleTotem:StopProfiling("OvaleTotem_state_GetTotemSlot")
+	return totemSlot
+end
+
+-- Summon a totem into the slot in the simulator at the current time.
+statePrototype.SummonTotem = function(state, spellId, slot)
+	OvaleTotem:StartProfiling("OvaleTotem_state_SummonTotem")
+	slot = TOTEM_SLOT[slot] or slot
+	state:Log("Spell %d summons totem into slot %d.", spellId, slot)
+	local name, _, icon = OvaleSpellBook:GetSpellInfo(spellId)
+	local duration = state:GetSpellInfoProperty(spellId, "duration")
+	local totem = state.totem[slot]
+	totem.isActive = true
+	-- The name is not always the same as the name of the summoning spell, but totems
+	-- are compared based on their icon/texture, so this inaccuracy doesn't break anything.
+	totem.name = name
+	totem.start = state.currentTime
+	-- Default to 15 seconds if no duration is found.
+	totem.duration = duration or 15
+	totem.icon = icon
+	OvaleTotem:StopProfiling("OvaleTotem_state_SummonTotem")
+end
+
+-- Destroy the totem in the slot.
+statePrototype.DestroyTotem = function(state, slot)
+	OvaleTotem:StartProfiling("OvaleTotem_state_DestroyTotem")
+	slot = TOTEM_SLOT[slot] or slot
+	state:Log("Destroying totem in slot %d.", slot)
+	local totem = state.totem[slot]
+	totem.isActive = false
+	totem.name = ""
+	totem.start = 0
+	totem.duration = 0
+	totem.icon = ""
+	OvaleTotem:StopProfiling("OvaleTotem_state_DestroyTotem")
+end
+--</state-methods>
diff --git a/conditions.lua b/conditions.lua
index 37a3eff..d9b67fc 100644
--- a/conditions.lua
+++ b/conditions.lua
@@ -35,7 +35,6 @@ local API_GetItemCooldown = GetItemCooldown
 local API_GetItemCount = GetItemCount
 local API_GetNumTrackingTypes = GetNumTrackingTypes
 local API_GetTime = GetTime
-local API_GetTotemInfo = GetTotemInfo
 local API_GetTrackingInfo = GetTrackingInfo
 local API_GetUnitSpeed = GetUnitSpeed
 local API_GetWeaponEnchantInfo = GetWeaponEnchantInfo
@@ -133,7 +132,7 @@ do
 	local function AfterWhiteHit(condition, state)
 		local seconds, comparator, limit = condition[1], condition[2], condition[3]
 		local value = 0
-		Ovale:OneTimeMessage("Warning: 'AfterWhiteHit() is not implemented.")
+		Ovale:OneTimeMessage("Warning: 'AfterWhiteHit()' is not implemented.")
 		return TestValue(0, INFINITY, value, state.currentTime, -1, comparator, limit)
 	end

@@ -4257,6 +4256,7 @@ do
 end

 do
+	local RUNE_OF_POWER = 116011
 	local RUNE_OF_POWER_BUFF = 116014

 	--- Get the remaining time in seconds before the latest Rune of Power expires.
@@ -4271,20 +4271,11 @@ do
 	-- if RuneOfPowerRemaining() < CastTime(rune_of_power) Spell(rune_of_power)

 	local function RuneOfPowerRemaining(condition, state)
+		Ovale:OneTimeMessage("Warning: 'RuneOfPowerRemaining()' is deprecated; use 'TotemRemaining(rune_of_power)' instead.")
 		local comparator, limit = condition[1], condition[2]
-		local aura = state:GetAura("player", RUNE_OF_POWER_BUFF, "HELPFUL")
-		if state:IsActiveAura(aura) then
-			local start, ending
-			for totemSlot = 1, 2 do
-				local haveTotem, name, startTime, duration = API_GetTotemInfo(totemSlot)
-				if haveTotem and startTime and (not start or startTime > start) then
-					start = startTime
-					ending = startTime + duration
-				end
-			end
-			if start then
-				return TestValue(0, INFINITY, ending - start, start, -1, comparator, limit)
-			end
+		local count, start, ending = state:GetTotemCount(RUNE_OF_POWER)
+		if count > 0 then
+			return TestValue(start, ending, 0, ending, -1, comparator, limit)
 		end
 		return Compare(0, comparator, limit)
 	end
@@ -4971,7 +4962,7 @@ do
 			comparator, limit = condition[1], condition[2]
 			start = 0
 		end
-		Ovale:OneTimeMessage("Warning: 'LastSwing() is not implemented.")
+		Ovale:OneTimeMessage("Warning: 'LastSwing()' is not implemented.")
 		return TestValue(start, INFINITY, 0, start, 1, comparator, limit)
 	end

@@ -4998,7 +4989,7 @@ do
 			comparator, limit = condition[1], condition[2]
 			ending = 0
 		end
-		Ovale:OneTimeMessage("Warning: 'NextSwing() is not implemented.")
+		Ovale:OneTimeMessage("Warning: 'NextSwing()' is not implemented.")
 		return TestValue(0, ending, 0, ending, -1, comparator, limit)
 	end

@@ -5443,72 +5434,99 @@ do
 end

 do
-	local OVALE_TOTEMTYPE =
-	{
-		-- Death Knights
-		ghoul = 1,
-		-- Druid
-		mushroom = 1,
-		-- XXX Mage
-		crystal = 4,
-		-- Monks
-		statue = 1,
-		-- Shamans
-		fire = 1,
-		earth = 2,
-		water = 3,
-		air = 4
-	}
+	-- Deprecated: totem types
+	local function CheckDeprecatedTotem(id, state)
+		local warning = false
+		local specialization = state.specialization
+		if id == "mushroom" then
+			warning = id
+			if specialization == 1 then
+				-- Balance.
+				id = 88747
+			elseif specialization == 4 then
+				-- Restoration.
+				id = 145205
+			end
+		elseif id == "statue" then
+			warning = id
+			if specialization == 1 then
+				-- Brewmaster.
+				id = 115315
+			elseif specialization == 2 then
+				-- Mistweaver.
+				id = 115313
+			end
+		elseif id == "ghoul" then
+			-- Ghouls are no longer totems, but pets summoned by Unholy Death Knights.
+			warning = id
+		end
+		if warning then
+			Ovale:OneTimeMessage("Warning: '%s' is deprecated; using '%s' instead.", warning, tostring(id))
+		end
+		return id
+	end

-	--- Test if the totem for shamans, the mushroom for druids, the ghoul for death knights, or the statue for monks has expired.
+	--- Test if the totem has expired.
 	-- @name TotemExpires
 	-- @paramsig boolean
-	-- @param id The totem ID of the totem, ghoul or statue, or the type of totem.
-	--     Valid types: fire, water, air, earth, ghoul, mushroom, statue.
+	-- @param id The ID of the spell used to summon the totem or one of the four shaman totem categories (air, earth, fire, water).
 	-- @param seconds Optional. The maximum number of seconds before the totem should expire.
 	--     Defaults to 0 (zero).
-	-- @param totem Optional. Sets the specific totem to check of given totem ID type.
-	--     Valid values: any totem spell ID
 	-- @return A boolean value.
 	-- @see TotemPresent, TotemRemaining
 	-- @usage
 	-- if TotemExpires(fire) Spell(searing_totem)
-	-- if TotemPresent(water totem=healing_stream_totem) and TotemExpires(water 3) Spell(totemic_recall)
+	-- if TotemPresent(healing_stream_totem) and TotemExpires(water 3) Spell(totemic_recall)

 	local function TotemExpires(condition, state)
-		local totemId, seconds = condition[1], condition[2]
+		local id, seconds = condition[1], condition[2]
 		seconds = seconds or 0
-		if type(totemId) ~= "number" then
-			totemId = OVALE_TOTEMTYPE[totemId]
+		if condition.totem then
+			id = condition.totem
+			Ovale:OneTimeMessage("Warning: using 'totem' parameter in 'TotemExpires()' is deprecated.")
 		end
-		local haveTotem, name, startTime, duration = API_GetTotemInfo(totemId)
-		if haveTotem and startTime and (not condition.totem or OvaleSpellBook:GetSpellName(condition.totem) == name) then
-			return startTime + duration - seconds, INFINITY
+		id = CheckDeprecatedTotem(id, state)
+		if type(id) == "string" then
+			local haveTotem, name, startTime, duration = state:GetTotemInfo(id)
+			if haveTotem and startTime then
+				return startTime + duration - seconds, INFINITY
+			end
+		else -- if type(id) == "number" then
+			local count, start, ending = state:GetTotemCount(id)
+			if count > 0 then
+				return ending - seconds, INFINITY
+			end
 		end
 		return 0, INFINITY
 	end

-	--- Test if the totem for shamans, the ghoul for death knights, or the statue for monks is present.
+	--- Test if the totem is present.
 	-- @name TotemPresent
 	-- @paramsig boolean
-	-- @param id The totem ID of the totem, ghoul or statue, or the type of totem.
-	--     Valid types: fire, water, air, earth, ghoul, statue.
-	-- @param totem Optional. Sets the specific totem to check of given totem ID type.
-	--     Valid values: any totem spell ID
+	-- @param id The ID of the spell used to summon the totem or one of the four shaman totem categories (air, earth, fire, water).
 	-- @return A boolean value.
 	-- @see TotemExpires, TotemRemaining
 	-- @usage
 	-- if not TotemPresent(fire) Spell(searing_totem)
-	-- if TotemPresent(water totem=healing_stream_totem) and TotemExpires(water 3) Spell(totemic_recall)
+	-- if TotemPresent(healing_stream_totem) and TotemExpires(water 3) Spell(totemic_recall)

 	local function TotemPresent(condition, state)
-		local totemId = condition[1]
-		if type(totemId) ~= "number" then
-			totemId = OVALE_TOTEMTYPE[totemId]
+		local id = condition[1]
+		if condition.totem then
+			id = condition.totem
+			Ovale:OneTimeMessage("Warning: using 'totem' parameter in 'TotemPresent()' is deprecated.")
 		end
-		local haveTotem, name, startTime, duration = API_GetTotemInfo(totemId)
-		if haveTotem and startTime and (not condition.totem or OvaleSpellBook:GetSpellName(condition.totem) == name) then
-			return startTime, startTime + duration
+		id = CheckDeprecatedTotem(id, state)
+		if type(id) == "string" then
+			local haveTotem, name, startTime, duration = state:GetTotemInfo(id)
+			if haveTotem and startTime then
+				return startTime, startTime + duration
+			end
+		else -- if type(id) == "number" then
+			local count, start, ending = state:GetTotemCount(id)
+			if count > 0 then
+				return start, ending
+			end
 		end
 		return nil
 	end
@@ -5519,27 +5537,33 @@ do
 	--- Get the remaining time in seconds before a totem expires.
 	-- @name TotemRemaining
 	-- @paramsig number or boolean
-	-- @param id The totem ID of the totem, ghoul or statue, or the type of totem.
-	--     Valid types: fire, water, air, earth, ghoul, statue.
+	-- @param id The ID of the spell used to summon the totem or one of the four shaman totem categories (air, earth, fire, water).
 	-- @param operator Optional. Comparison operator: less, atMost, equal, atLeast, more.
 	-- @param number Optional. The number to compare against.
-	-- @param totem Optional. Sets the specific totem to check of given totem ID type.
-	--     Valid values: any totem spell ID
 	-- @return The number of seconds.
 	-- @return A boolean value for the result of the comparison.
 	-- @see TotemExpires, TotemPresent
 	-- @usage
-	-- if TotemRemaining(water totem=healing_stream_totem) <2 Spell(totemic_recall)
+	-- if TotemRemaining(healing_stream_totem) <2 Spell(totemic_recall)

 	local function TotemRemaining(condition, state)
-		local totemId, comparator, limit = condition[1], condition[2], condition[3]
-		if type(totemId) ~= "number" then
-			totemId = OVALE_TOTEMTYPE[totemId]
+		local id, comparator, limit = condition[1], condition[2], condition[3]
+		if condition.totem then
+			id = condition.totem
+			Ovale:OneTimeMessage("Warning: using 'totem' parameter in 'TotemRemaining()' is deprecated.")
 		end
-		local haveTotem, name, startTime, duration = API_GetTotemInfo(totemId)
-		if haveTotem and startTime and (not condition.totem or OvaleSpellBook:GetSpellName(condition.totem) == name) then
-			local start, ending = startTime, startTime + duration
-			return TestValue(start, ending, duration, start, -1, comparator, limit)
+		id = CheckDeprecatedTotem(id, state)
+		if type(id) == "string" then
+			local haveTotem, name, startTime, duration = state:GetTotemInfo(id)
+			if haveTotem and startTime then
+				local start, ending = startTime, startTime + duration
+				return TestValue(start, ending, 0, ending, -1, comparator, limit)
+			end
+		else -- if type(id) == "number" then
+			local count, start, ending = state:GetTotemCount(id)
+			if count > 0 then
+				return TestValue(start, ending, 0, ending, -1, comparator, limit)
+			end
 		end
 		return Compare(0, comparator, limit)
 	end
diff --git a/scripts/ovale_druid_spells.lua b/scripts/ovale_druid_spells.lua
index 436856a..f081ece 100644
--- a/scripts/ovale_druid_spells.lua
+++ b/scripts/ovale_druid_spells.lua
@@ -400,6 +400,7 @@ Define(wild_growth 48438)
 Define(wild_growth_buff 48438)
 	SpellInfo(wild_growth_buff duration=7 haste=spell tick=1)
 Define(wild_mushroom_heal 145205)
+	SpellInfo(wild_mushroom_heal duration=30 totem=1)
 Define(wrath 5176)
 	SpellAddBuff(wrath solar_empowerment_buff=-1)
 	SpellAddTargetBuff(wrath sunfire_debuff=extend,4 if_spell=balance_of_power)
diff --git a/scripts/ovale_mage_spells.lua b/scripts/ovale_mage_spells.lua
index 553def6..eaf2373 100644
--- a/scripts/ovale_mage_spells.lua
+++ b/scripts/ovale_mage_spells.lua
@@ -182,7 +182,7 @@ Define(presence_of_mind_buff 12043)
 Define(profound_magic_buff 145252)
 	SpellInfo(profound_magic_buff duration=10 max_stacks=4)
 Define(prismatic_crystal 152087)
-	SpellInfo(prismatic_crystal cd=90)
+	SpellInfo(prismatic_crystal cd=90 duration=12 totem=1)
 Define(prismatic_crystal_talent 20)
 Define(pyroblast 11366)
 	SpellAddBuff(pyroblast ice_floes_buff=0 if_spell=ice_floes)
@@ -195,11 +195,10 @@ Define(pyroblast_debuff 11366)
 Define(pyromaniac_buff 166868)
 	SpellInfo(pyromaniac_buff duration=4)
 Define(rune_of_power 116011)
+	SpellInfo(rune_of_power buff_totem=rune_of_power_buff duration=180 max_totems=2 totem=1)
 	SpellAddBuff(rune_of_power ice_floes_buff=0 if_spell=ice_floes)
-	SpellAddBuff(rune_of_power rune_of_power_buff=1)
 	SpellAddBuff(rune_of_power presence_of_mind_buff=0 if_spell=presence_of_mind)
 Define(rune_of_power_buff 116014)
-	SpellInfo(rune_of_power_buff duration=180)
 Define(scorch 2948)
 Define(spellsteal 30449)
 Define(supernova 157980)
diff --git a/scripts/ovale_monk_spells.lua b/scripts/ovale_monk_spells.lua
index 55a4225..cf9c07a 100644
--- a/scripts/ovale_monk_spells.lua
+++ b/scripts/ovale_monk_spells.lua
@@ -244,9 +244,9 @@ Define(stance_of_the_wise_serpent 115070)
 	SpellInfo(stance_of_the_wise_serpent to_stance=monk_stance_of_the_wise_serpent)
 	SpellInfo(stance_of_the_wise_serpent unusable=1 if_stance=monk_stance_of_the_wise_serpent)
 Define(summon_black_ox_statue 115315)
-	SpellInfo(summon_black_ox_statue cd=10)
+	SpellInfo(summon_black_ox_statue cd=10 duration=900 totem=1)
 Define(summon_jade_serpent_statue 115313)
-	SpellInfo(summon_jade_serpent_statue cd=10)
+	SpellInfo(summon_jade_serpent_statue cd=10 duration=900 totem=1)
 Define(surging_mist 116694)
 	SpellInfo(surging_mist chi=-1 if_stance=monk_stance_of_the_wise_serpent)
 	SpellInfo(surging_mist replace=surging_mist_glyphed unusable=1 glyph=glyph_of_surging_mist)
diff --git a/scripts/ovale_shaman_spells.lua b/scripts/ovale_shaman_spells.lua
index 15c933d..f71ce45 100644
--- a/scripts/ovale_shaman_spells.lua
+++ b/scripts/ovale_shaman_spells.lua
@@ -59,10 +59,10 @@ Define(chain_lightning 421)
 	SpellAddBuff(chain_lightning maelstrom_weapon_buff=0 if_spell=maelstrom_weapon)
 	SpellAddBuff(chain_lightning enhanced_chain_lightning_buff=1 if_spell=enhanced_chain_lightning)
 Define(cloudburst_totem 157153)
-	SpellInfo(cloudburst_totem cd=30)
+	SpellInfo(cloudburst_totem cd=30 duration=15 totem=water)
 Define(cloudburst_totem_talent 19)
 Define(earth_elemental_totem 2062)
-	SpellInfo(earth_elemental_totem cd=300)
+	SpellInfo(earth_elemental_totem cd=300 duration=60 totem=earth)
 	SpellInfo(earth_elemental_totem buff_cdr=cooldown_reduction_agility_buff specialization=enhancement)
 Define(earth_shield 974)
 	SpellAddTargetBuff(earth_shield earth_shield_buff=1)
@@ -109,7 +109,7 @@ Define(feral_spirit 51533)
 	SpellInfo(feral_spirit addcd=60 glyph=glyph_of_ephemeral_spirits)
 	SpellInfo(feral_spirit buff_cdr=cooldown_reduction_agility_buff)
 Define(fire_elemental_totem 2894)
-	SpellInfo(fire_elemental_totem cd=300)
+	SpellInfo(fire_elemental_totem cd=300 duration=60 totem=fire)
 	SpellInfo(fire_elemental_totem cd=150 glyph=glyph_of_fire_elemental_totem)
 	SpellInfo(fire_elemental_totem buff_cdr=cooldown_reduction_agility_buff specialization=enhancement)
 Define(fire_nova 1535)
@@ -150,13 +150,13 @@ Define(healing_rain 73920)
 	SpellAddBuff(healing_rain ancestral_swiftness_buff=0 if_spell=ancestral_swiftness)
 	SpellAddBuff(healing_rain maelstrom_weapon_buff=0 if_spell=maelstrom_weapon)
 Define(healing_stream_totem 5394)
-	SpellInfo(healing_stream_totem cd=30)
+	SpellInfo(healing_stream_totem cd=30 duration=15 totem=water)
 Define(healing_surge 8004)
 	SpellAddBuff(healing_surge ancestral_swiftness_buff=0 if_spell=ancestral_swiftness)
 	SpellAddBuff(healing_surge tidal_waves_buff=-1 if_spell=tidal_waves)
 	SpellAddBuff(healing_surge unleash_life_buff=0 if_spell=unleash_life)
 Define(healing_tide_totem 108280)
-	SpellInfo(healing_tide_totem cd=180)
+	SpellInfo(healing_tide_totem cd=180 duration=10 totem=water)
 Define(healing_wave 77472)
 	SpellAddBuff(healing_wave ancestral_swiftness_buff=0 if_spell=ancestral_swiftness)
 	SpellAddBuff(healing_wave tidal_waves_buff=-1 if_spell=tidal_waves)
@@ -203,6 +203,7 @@ Define(liquid_magma_talent 21)
 Define(maelstrom_weapon_buff 53817)
 	SpellInfo(maelstrom_weapon_buff duration=30 max_stacks=5)
 Define(magma_totem 8190)
+	SpellInfo(magma_totem duration=60 totem=fire)
 Define(primal_elementalist_talent 17)
 Define(primal_strike 73899)
 	SpellInfo(primal_strike cd=8)
@@ -215,8 +216,9 @@ Define(riptide 61295)
 Define(riptide_buff 61295)
 	SpellInfo(riptide_buff duration=18 haste=spell tick=3)
 Define(searing_totem 3599)
+	SpellInfo(searing_totem duration=60 totem=fire)
 Define(spirit_link_totem 98008)
-	SpellInfo(spirit_link_totem cd=180)
+	SpellInfo(spirit_link_totem cd=180 duration=6 totem=air)
 Define(spirit_walk 58875)
 	SpellInfo(spirit_walk cd=60)
 	SpellInfo(spirit_walk addcd=-15 glyph=glyph_of_spirit_walk)
@@ -225,7 +227,7 @@ Define(spiritwalkers_grace 79206)
 	SpellInfo(spiritwalkers_grace addcd=-60 glyph=glyph_of_spiritual_focus)
 	SpellInfo(spiritwalkers_grace buff_cdr=cooldown_reduction_agility_buff specialization=enhancement)
 Define(storm_elemental_totem 152256)
-	SpellInfo(storm_elemental_totem cd=300)
+	SpellInfo(storm_elemental_totem cd=300 duration=60 totem=air)
 Define(storm_elemental_totem_talent 20)
 Define(stormstrike 17364)
 	SpellInfo(stormstrike cd=7.5)
@@ -240,7 +242,7 @@ Define(tidal_waves_buff 53390)
 Define(totemic_persistence_talent 8)
 Define(totemic_recall 36936)
 Define(tremor_totem 8143)
-	SpellInfo(tremor_totem cd=60)
+	SpellInfo(tremor_totem cd=60 duration=10 totem=earth)
 Define(unleash_elements 73680)
 	SpellInfo(unleash_elements cd=15)
 	SpellInfo(unleash_elements cd_haste=melee gcd_haste=melee if_spell=flurry)
@@ -269,7 +271,7 @@ Define(windstrike 115356)
 	SpellRequire(windstrike cd 0=buff,echo_of_the_elements_buff if_spell=echo_of_the_elements)
 	SpellAddBuff(windstrike echo_of_the_elements_buff=0 if_spell=echo_of_the_elements)
 Define(windwalk_totem 108273)
-	SpellInfo(windwalk_totem cd=60)
+	SpellInfo(windwalk_totem cd=60 duration=6 totem=air)

 # Pet spells (Primal Elementalist Talent)
 Define(pet_empower 118350)