Quantcast
--[[--------------------------------------------------------------------
    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.
--]]--------------------------------------------------------------------

local _, Ovale = ...
local OvalePower = Ovale:NewModule("OvalePower", "AceEvent-3.0")
Ovale.OvalePower = OvalePower

--<private-static-properties>
-- Forward declarations for module dependencies.
local OvaleAura = nil
local OvaleFuture = nil
local OvaleData = nil
local OvaleState = nil

local ceil = math.ceil
local pairs = pairs
local type = type
local API_GetPowerRegen = GetPowerRegen
local API_GetSpellInfo = GetSpellInfo
local API_UnitPower = UnitPower
local API_UnitPowerMax = UnitPowerMax
local API_UnitPowerType = UnitPowerType
local SPELL_POWER_ALTERNATE_POWER = SPELL_POWER_ALTERNATE_POWER
local SPELL_POWER_BURNING_EMBERS = SPELL_POWER_BURNING_EMBERS
local SPELL_POWER_CHI = SPELL_POWER_CHI
local SPELL_POWER_DEMONIC_FURY = SPELL_POWER_DEMONIC_FURY
local SPELL_POWER_ECLIPSE = SPELL_POWER_ECLIPSE
local SPELL_POWER_ENERGY = SPELL_POWER_ENERGY
local SPELL_POWER_FOCUS = SPELL_POWER_FOCUS
local SPELL_POWER_HOLY_POWER = SPELL_POWER_HOLY_POWER
local SPELL_POWER_MANA = SPELL_POWER_MANA
local SPELL_POWER_RAGE = SPELL_POWER_RAGE
local SPELL_POWER_RUNIC_POWER = SPELL_POWER_RUNIC_POWER
local SPELL_POWER_SHADOW_ORBS = SPELL_POWER_SHADOW_ORBS
local SPELL_POWER_SOUL_SHARDS = SPELL_POWER_SOUL_SHARDS

-- Profiling set-up.
local Profiler = Ovale.Profiler
local profiler = nil
do
	local group = OvalePower:GetName()

	local function EnableProfiling()
		API_GetSpellInfo = Profiler:Wrap(group, "OvalePower_API_GetSpellInfo", GetSpellInfo)
	end

	local function DisableProfiling()
		API_GetSpellInfo = GetSpellInfo
	end

	Profiler:RegisterProfilingGroup(group, EnableProfiling, DisableProfiling)
	profiler = Profiler:GetProfilingGroup(group)
end

-- Table of functions to update spellcast information to register with OvaleFuture.
local self_updateSpellcastInfo = {}
--</private-static-properties>

--<public-static-properties>
-- Player's current power type (key for POWER table).
OvalePower.powerType = nil
-- Player's current power; power[powerType] = number.
OvalePower.power = {}
-- Player's current max power; maxPower[powerType] = number.
OvalePower.maxPower = {}
-- Player's current power regeneration rate for the active power type.
OvalePower.activeRegen = 0
OvalePower.inactiveRegen = 0

OvalePower.POWER_INFO =
{
	alternate = { id = SPELL_POWER_ALTERNATE_POWER, token = "ALTERNATE_RESOURCE_TEXT", mini = 0 },
	burningembers = { id = SPELL_POWER_BURNING_EMBERS, token = "BURNING_EMBERS", mini = 0, segments = true },
	chi = { id = SPELL_POWER_CHI, token = "CHI", mini = 0 },
	demonicfury = { id = SPELL_POWER_DEMONIC_FURY, token = "DEMONIC_FURY", mini = 0 },
	eclipse = { id = SPELL_POWER_ECLIPSE, token = "ECLIPSE", mini = -100, maxi = 100 },
	energy = { id = SPELL_POWER_ENERGY, token = "ENERGY", mini = 0 },
	focus = { id = SPELL_POWER_FOCUS, token = "FOCUS", mini = 0 },
	holy = { id = SPELL_POWER_HOLY_POWER, token = "HOLY_POWER", mini = 0 },
	mana = { id = SPELL_POWER_MANA, token = "MANA", mini = 0 },
	rage = { id = SPELL_POWER_RAGE, token = "RAGE", mini = 0 },
	runicpower = { id = SPELL_POWER_RUNIC_POWER, token = "RUNIC_POWER", mini = 0 },
	shadoworbs = { id = SPELL_POWER_SHADOW_ORBS, token = "SHADOW_ORBS", mini = 0 },
	shards = { id = SPELL_POWER_SOUL_SHARDS, token = "SOUL_SHARDS_POWER", mini = 0 },
}
OvalePower.SECONDARY_POWER = {
	alternate = true,
	burningembers = true,
	chi = true,
	demonicfury = true,
	focus = true,
	holy = true,
	rage = true,
	runicpower = true,
	shadoworbs = true,
	shards = true,
}
OvalePower.PRIMARY_POWER = {}
do
	for powerType in pairs(OvalePower.POWER_INFO) do
		if not OvalePower.SECONDARY_POWER[powerType] then
			-- Eclipse has special semantics; it's not a cost/generated resource.
			if powerType ~= "eclipse" then
				OvalePower.PRIMARY_POWER[powerType] = true
			end
		end
	end
end
OvalePower.POWER_TYPE = {}
do
	for powerType, v in pairs(OvalePower.POWER_INFO) do
		OvalePower.POWER_TYPE[v.id] = powerType
		OvalePower.POWER_TYPE[v.token] = powerType
	end
end
--</public-static-properties>

--<private-static-methods>
-- Manage spellcast.holy information.
local function SaveToSpellcast(spellcast)
	if spellcast.spellId then
		local si = OvaleData.spellInfo[spellcast.spellId]
		-- Save the number of holy power used if this spell is a finisher.
		if si.holy == "finisher" then
			local max_holy = si.max_holy or 3
			-- If a buff is present that removes the holy power cost of the spell,
			-- then treat it as using the maximum amount of holy power.
			if si.buff_holy_none then
				if OvaleAura:GetAura("player", si.buff_holy_none) then
					spellcast.holy = max_holy
				end
			end
			local holy = OvalePower.power.holy
			if holy > 0 then
				if holy > max_holy then
					spellcast.holy = max_holy
				else
					spellcast.holy = holy
				end
			end
		end
	end
end

local function UpdateFromSpellcast(dest, spellcast)
	if spellcast.holy then
		dest.holy = spellcast.holy
	end
end

do
	self_updateSpellcastInfo.SaveToSpellcast = SaveToSpellcast
	self_updateSpellcastInfo.UpdateFromSpellcast = UpdateFromSpellcast
end
--</private-static-methods>

--<public-static-methods>
function OvalePower:OnInitialize()
	-- Resolve module dependencies.
	OvaleAura = Ovale.OvaleAura
	OvaleData = Ovale.OvaleData
	OvaleFuture = Ovale.OvaleFuture
	OvaleState = Ovale.OvaleState
end

function OvalePower:OnEnable()
	self:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED", "EventHandler")
	self:RegisterEvent("PLAYER_ALIVE", "EventHandler")
	self:RegisterEvent("PLAYER_ENTERING_WORLD", "EventHandler")
	self:RegisterEvent("PLAYER_LEVEL_UP", "EventHandler")
	self:RegisterEvent("PLAYER_TALENT_UPDATE", "EventHandler")
	self:RegisterEvent("UNIT_DISPLAYPOWER")
	self:RegisterEvent("UNIT_LEVEL")
	self:RegisterEvent("UNIT_MAXPOWER")
	self:RegisterEvent("UNIT_POWER")
	self:RegisterEvent("UNIT_POWER_FREQUENT", "UNIT_POWER")
	self:RegisterEvent("UNIT_RANGEDDAMAGE")
	self:RegisterEvent("UNIT_SPELL_HASTE", "UNIT_RANGEDDAMAGE")
	self:RegisterMessage("Ovale_StanceChanged", "EventHandler")
	OvaleState:RegisterState(self, self.statePrototype)
	OvaleFuture:RegisterSpellcastInfo(self_updateSpellcastInfo)
end

function OvalePower:OnDisable()
	OvaleState:UnregisterState(self)
	OvaleFuture:UnregisterSpellcastInfo(self_updateSpellcastInfo)
	self:UnregisterEvent("ACTIVE_TALENT_GROUP_CHANGED")
	self:UnregisterEvent("PLAYER_ALIVE")
	self:UnregisterEvent("PLAYER_ENTERING_WORLD")
	self:UnregisterEvent("PLAYER_LEVEL_UP")
	self:UnregisterEvent("PLAYER_TALENT_UPDATE")
	self:UnregisterEvent("UNIT_DISPLAYPOWER")
	self:UnregisterEvent("UNIT_LEVEL")
	self:UnregisterEvent("UNIT_MAXPOWER")
	self:UnregisterEvent("UNIT_POWER")
	self:UnregisterEvent("UNIT_POWER_FREQUENT")
	self:UnregisterEvent("UNIT_RANGEDDAMAGE")
	self:UnregisterEvent("UNIT_SPELL_HASTE")
	self:UnregisterMessage("Ovale_StanceChanged")
end

function OvalePower:EventHandler(event)
	self:UpdatePowerType()
	self:UpdateMaxPower()
	self:UpdatePower()
	self:UpdatePowerRegen()
end

function OvalePower:UNIT_DISPLAYPOWER(event, unitId)
	if unitId == "player" then
		self:UpdatePowerType()
		self:UpdatePowerRegen()
	end
end

function OvalePower:UNIT_LEVEL(event, unitId)
	if unitId == "player" then
		self:EventHandler(event)
	end
end

function OvalePower:UNIT_MAXPOWER(event, unitId, powerToken)
	if unitId == "player" then
		self:UpdateMaxPower(self.POWER_TYPE[powerToken])
	end
end

function OvalePower:UNIT_POWER(event, unitId, powerToken)
	if unitId == "player" then
		self:UpdatePower(self.POWER_TYPE[powerToken])
	end
end

function OvalePower:UNIT_RANGEDDAMAGE(event, unitId)
	if unitId == "player" then
		self:UpdatePowerRegen()
	end
end

function OvalePower:UpdateMaxPower(powerType)
	profiler.Start("OvalePower_UpdateMaxPower")
	if powerType then
		local powerInfo = self.POWER_INFO[powerType]
		self.maxPower[powerType] = API_UnitPowerMax("player", powerInfo.id, powerInfo.segments)
	else
		for powerType, powerInfo in pairs(self.POWER_INFO) do
			self.maxPower[powerType] = API_UnitPowerMax("player", powerInfo.id, powerInfo.segments)
		end
	end
	profiler.Stop("OvalePower_UpdateMaxPower")
end

function OvalePower:UpdatePower(powerType)
	profiler.Start("OvalePower_UpdatePower")
	if powerType then
		local powerInfo = self.POWER_INFO[powerType]
		self.power[powerType] = API_UnitPower("player", powerInfo.id, powerInfo.segments)
	else
		for powerType, powerInfo in pairs(self.POWER_INFO) do
			self.power[powerType] = API_UnitPower("player", powerInfo.id, powerInfo.segments)
		end
	end
	profiler.Stop("OvalePower_UpdatePower")
end

function OvalePower:UpdatePowerRegen()
	profiler.Start("OvalePower_UpdatePowerRegen")
	self.inactiveRegen, self.activeRegen = API_GetPowerRegen()
	profiler.Stop("OvalePower_UpdatePowerRegen")
end

function OvalePower:UpdatePowerType()
	profiler.Start("OvalePower_UpdatePowerType")
	local currentType, currentToken = API_UnitPowerType("player")
	self.powerType = self.POWER_TYPE[currentType]
	profiler.Stop("OvalePower_UpdatePowerType")
end

function OvalePower:Debug()
	Ovale:FormatPrint("Power type: %s", self.powerType)
	for powerType, v in pairs(self.power) do
		Ovale:FormatPrint("Power (%s): %d / %d", powerType, v, self.maxPower[powerType])
	end
	Ovale:FormatPrint("Active regen: %f", self.activeRegen)
	Ovale:FormatPrint("Inactive regen: %f", self.inactiveRegen)
end
--</public-static-methods>

--[[----------------------------------------------------------------------------
	State machine for simulator.
--]]----------------------------------------------------------------------------

--<public-static-properties>
OvalePower.statePrototype = {}
--</public-static-properties>

--<private-static-properties>
local statePrototype = OvalePower.statePrototype
--</private-static-properties>

--<state-properties>
--[[
	This block is here for compiler.pl to know that these properties are added to the state machine.

	statePrototype.alternate = nil
	statePrototype.burningembers = nil
	statePrototype.chi = nil
	statePrototype.demonicfury = nil
	statePrototype.eclipse = nil
	statePrototype.energy = nil
	statePrototype.focus = nil
	statePrototype.holy = nil
	statePrototype.mana = nil
	statePrototype.rage = nil
	statePrototype.runicpower = nil
	statePrototype.shadoworbs = nil
	statePrototype.shards = nil
--]]

-- powerRate[powerType] = regen rate
statePrototype.powerRate = nil
--</state-properties>

--<public-static-methods>
-- Initialize the state.
function OvalePower:InitializeState(state)
	for powerType in pairs(self.POWER_INFO) do
		state[powerType] = 0
	end
	state.powerRate = {}
end

-- Reset the state to the current conditions.
function OvalePower:ResetState(state)
	profiler.Start("OvalePower_ResetState")
	-- Power levels for each resource.
	for powerType in pairs(self.POWER_INFO) do
		state[powerType] = self.power[powerType] or 0
	end
	-- Clear power regeneration rates for each resource.
	for powerType in pairs(self.POWER_INFO) do
		state.powerRate[powerType] = 0
	end
	-- Set power regeneration for current resource.
	if Ovale.enCombat then
		state.powerRate[self.powerType] = self.activeRegen
	else
		state.powerRate[self.powerType] = self.inactiveRegen
	end
	profiler.Stop("OvalePower_ResetState")
end

-- Release state resources prior to removing from the simulator.
function OvalePower:CleanState(state)
	for powerType in pairs(self.POWER_INFO) do
		state[powerType] = nil
	end
	for k in pairs(state.powerRate) do
		state.powerRate[k] = nil
	end
end

-- Apply the effects of the spell at the start of the spellcast.
function OvalePower:ApplySpellStartCast(state, spellId, targetGUID, startCast, endCast, nextCast, isChanneled, nocd, spellcast)
	profiler.Start("OvalePower_ApplySpellStartCast")
	-- Channeled spells cost resources at the start of the channel.
	if isChanneled then
		state:ApplyPowerCost(spellId)
	end
	profiler.Stop("OvalePower_ApplySpellStartCast")
end

-- Apply the effects of the spell on the player's state, assuming the spellcast completes.
function OvalePower:ApplySpellAfterCast(state, spellId, targetGUID, startCast, endCast, nextCast, isChanneled, nocd, spellcast)
	profiler.Start("OvalePower_ApplySpellAfterCast")
	-- Instant or cast-time spells cost resources at the end of the spellcast.
	if not isChanneled then
		state:ApplyPowerCost(spellId)
	end
	profiler.Stop("OvalePower_ApplySpellAfterCast")
end
--</public-static-methods>

--<state-methods>
-- Update the state of the simulator for the power cost of the given spell.
statePrototype.ApplyPowerCost = function(state, spellId)
	profiler.Start("OvalePower_state_ApplyPowerCost")
	local si = OvaleData.spellInfo[spellId]

	-- Update power using information from GetSpellInfo() if there is no SpellInfo() for the spell's cost.
	do
		local _, _, _, cost, _, powerTypeId = API_GetSpellInfo(spellId)
		if cost and powerTypeId then
			powerType = OvalePower.POWER_TYPE[powerTypeId]
			if state[powerType] and not (si and si[powerType]) then
				state[powerType] = state[powerType] - cost
			end
		end
	end

	if si then
		-- Update power state except for eclipse energy (handled by OvaleEclipse).
		for powerType, powerInfo in pairs(OvalePower.POWER_INFO) do
			if powerType ~= "eclipse" then
				local cost = state:PowerCost(spellId, powerType)
				local power = state[powerType] or 0
				if cost then
					power = power - cost
					-- Clamp power to lower and upper limits.
					local mini = powerInfo.mini or 0
					local maxi = powerInfo.maxi or OvalePower.maxPower[powerType]
					if mini and power < mini then
						power = mini
					end
					if maxi and power > maxi then
						power = maxi
					end
					state[powerType] = power
				end
			end
		end
	end
	profiler.Stop("OvalePower_state_ApplyPowerCost")
end

-- Return the number of seconds before all of the primary resources needed by a spell are available.
statePrototype.TimeToPower = function(state, spellId, powerType)
	local power = state[powerType]
	local powerRate = state.powerRate[powerType]
	local cost = state:PowerCost(spellId, powerType)

	local seconds = 0
	if power < cost then
		if powerRate > 0 then
			seconds = (cost - power) / powerRate
		else
			seconds = math.huge
		end
	end
	return seconds
end

-- Return the amount of the given resource needed to cast the given spell.
do
	local BUFF_PERCENT_REDUCTION = {
		["_less15"] = 0.85,
		["_less50"] = 0.50,
		["_less75"] = 0.25,
		["_half"] = 0.5,
	}

	statePrototype.PowerCost = function(state, spellId, powerType)
		profiler.Start("OvalePower_state_PowerCost")
		local buffParam = "buff_" .. powerType
		local spellCost = 0
		local si = OvaleData.spellInfo[spellId]
		if si and si[powerType] then
			--[[
				cost == 0 means the that spell uses no resources.
				cost > 0 means that the spell costs resources.
				cost < 0 means that the spell generates resources.
				cost == "finisher" means that the spell uses all of the resources (zeroes it out).
			--]]
			local cost = si[powerType]
			if cost == "finisher" then
				-- This spell is a finisher so compute the cost based on the amount of resources consumed.
				cost = state[powerType]
				-- Clamp cost between values defined by min_<powerType> and max_<powerType>.
				local minCostParam = "min_" .. powerType
				local maxCostParam = "max_" .. powerType
				local minCost = si[minCostParam] or 1
				local maxCost = si[maxCostParam]
				if cost < minCost then
					cost = minCost
				end
				if maxCost and cost > maxCost then
					cost = maxCost
				end
			else
				--[[
					Add extra resource generated by presence of a buff.
					"buff_<powerType>" is the spell ID of the buff that causes extra resources to be generated or used.
					"buff_<powerType>_amount" is the amount of extra resources generated or used, defaulting to -1
						(one extra resource generated).
				--]]
				local buffExtraParam = buffParam
				local buffAmountParam = buffParam .. "_amount"
				local buffExtra = si[buffExtraParam]
				if buffExtra then
					local aura = state:GetAura("player", buffExtra, nil, true)
					if state:IsActiveAura(aura) then
						local buffAmount = si[buffAmountParam] or -1
						cost = cost + buffAmount
					end
				end
				if cost > 0 then
					--[[
						Apply any percent reductions to cost after fixed reductions are applied.
						This seems to be a consistent Blizzard rule for spell costs so that you
						never end up with a negative spell cost.
					--]]
					for suffix, multiplier in pairs(BUFF_PERCENT_REDUCTION) do
						local buffPercentReduction = si[buffParam .. suffix]
						if buffPercentReduction then
							local aura = state:GetAura("player", buffPercentReduction)
							if state:IsActiveAura(aura) then
								cost = cost * multiplier
							end
						end
					end
					cost = ceil(cost)
				end
			end

			local buffNoCostParam = buffParam .. "_none"
			local buffNoCost = si[buffNoCostParam]
			if buffNoCost then
				-- "buff_<powerType>_none" is the spell ID of the buff that makes casting the spell resource-free.
				local aura = state:GetAura("player", buffNoCost)
				if state:IsActiveAura(aura) then
					cost = 0
				end
			end
			spellCost = cost
		else
			-- Determine cost using information from GetSpellInfo() if there is no SpellInfo() for the spell's cost.
			local _, _, _, cost, _, powerTypeId = API_GetSpellInfo(spellId)
			if cost and powerTypeId and powerType == OvalePower.POWER_TYPE[powerTypeId] then
				spellCost = cost
			end
		end
		profiler.Stop("OvalePower_state_PowerCost")
		return spellCost
	end
end

-- Print out the levels of each power type in the current state.
statePrototype.DebugPower = function(state)
	for powerType in pairs(OvalePower.POWER_INFO) do
		Ovale:FormatPrint("%s = %d", powerType, state[powerType])
	end
end
--</state-methods>