Quantcast
--[[--------------------------------------------------------------------
    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, 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>