Quantcast
--[[--------------------------------------------------------------------
    Copyright (C) 2012 Sidoine De Wispelaere.
    Copyright (C) 2012, 2013, 2014 Johnny C. Lam.
    See the file LICENSE.txt for copying permission.
--]]--------------------------------------------------------------------

-- This addon tracks the player's active spells, talents, and glyphs.

local OVALE, Ovale = ...
local OvaleSpellBook = Ovale:NewModule("OvaleSpellBook", "AceEvent-3.0")
Ovale.OvaleSpellBook = OvaleSpellBook

--<private-static-properties>
local L = Ovale.L
local OvaleDebug = Ovale.OvaleDebug
local OvaleProfiler = Ovale.OvaleProfiler

-- Forward declarations for module dependencies.
local OvaleCooldown = nil
local OvaleData = nil
local OvalePower = nil
local OvaleRunes = nil
local OvaleState = nil

local ipairs = ipairs
local pairs = pairs
local strmatch = string.match
local tconcat = table.concat
local tinsert = table.insert
local tonumber = tonumber
local tostring = tostring
local tsort = table.sort
local wipe = wipe
local API_GetActiveSpecGroup = GetActiveSpecGroup
local API_GetFlyoutInfo = GetFlyoutInfo
local API_GetFlyoutSlotInfo = GetFlyoutSlotInfo
local API_GetGlyphSocketInfo = GetGlyphSocketInfo
local API_GetNumGlyphSockets = GetNumGlyphSockets
local API_GetSpellBookItemInfo = GetSpellBookItemInfo
local API_GetSpellInfo = GetSpellInfo
local API_GetSpellLink = GetSpellLink
local API_GetSpellTabInfo = GetSpellTabInfo
local API_GetSpellTexture = GetSpellTexture
local API_GetTalentInfo = GetTalentInfo
local API_HasPetSpells = HasPetSpells
local API_IsHarmfulSpell = IsHarmfulSpell
local API_IsHelpfulSpell = IsHelpfulSpell
local API_IsSpellInRange = IsSpellInRange
local API_IsUsableSpell = IsUsableSpell
local BOOKTYPE_PET = BOOKTYPE_PET
local BOOKTYPE_SPELL = BOOKTYPE_SPELL
local MAX_TALENT_TIERS = MAX_TALENT_TIERS
local NUM_TALENT_COLUMNS = NUM_TALENT_COLUMNS
local MAX_NUM_TALENTS = NUM_TALENT_COLUMNS * MAX_TALENT_TIERS

-- Register for debugging messages.
OvaleDebug:RegisterDebugging(OvaleSpellBook)
-- Register for profiling.
OvaleProfiler:RegisterProfiling(OvaleSpellBook)

do
	local debugOptions = {
		glyph = {
			name = L["Glyphs"],
			type = "group",
			args = {
				glyph = {
					name = L["Glyphs"],
					type = "input",
					multiline = 25,
					width = "full",
					get = function(info) return OvaleSpellBook:DebugGlyphs() end,
				},
			},
		},
		spellbook = {
			name = L["Spellbook"],
			type = "group",
			args = {
				spellbook = {
					name = L["Spellbook"],
					type = "input",
					multiline = 25,
					width = "full",
					get = function(info) return OvaleSpellBook:DebugSpells() end,
				},
			},
		},
		talent = {
			name = L["Talents"],
			type = "group",
			args = {
				talent = {
					name = L["Talents"],
					type = "input",
					multiline = 25,
					width = "full",
					get = function(info) return OvaleSpellBook:DebugTalents() end,
				},
			},
		},
	}
	-- Insert debug options into OvaleDebug.
	for k, v in pairs(debugOptions) do
		OvaleDebug.options.args[k] = v
	end
end
--</private-static-properties>

--<public-static-properties>
-- Whether the spellbook information is ready for use by other modules.
OvaleSpellBook.ready = false
-- self.spell[spellId] = spellName
OvaleSpellBook.spell = {}
-- self.spellbookId[bookType][spellId] = index of spell in the spellbook
OvaleSpellBook.spellbookId = {
	[BOOKTYPE_PET] = {},
	[BOOKTYPE_SPELL] = {},
}
-- self.isHarmful[spellId] = true/false
OvaleSpellBook.isHarmful = {}
-- self.isHelpful[spellId] = true/false
OvaleSpellBook.isHelpful = {}
-- self.texture[spellId] = path to texture
OvaleSpellBook.texture = {}
-- self.talent[talentId] = talentName
OvaleSpellBook.talent = {}
-- self.talentPoints[talentId] = 0 or 1
OvaleSpellBook.talentPoints = {}
-- self.glyph[glyphSpellId] = glyphName
OvaleSpellBook.glyph = {}
--</public-static-properties>

--<private-static-methods>
local function ParseHyperlink(hyperlink)
	local color, linkType, linkData, text = strmatch(hyperlink, "|?c?f?f?(%x*)|?H?([^:]*):?(%d+)|?h?%[?([^%[%]]*)%]?|?h?|?r?")
	return color, linkType, linkData, text
end

local function OutputTableValues(output, tbl)
	local array = {}
	for k, v in pairs(tbl) do
		tinsert(array, tostring(v) .. ": " .. tostring(k))
	end
	tsort(array)
	for _, v in ipairs(array) do
		output[#output + 1] = v
	end
end
--</private-static-methods>

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

function OvaleSpellBook:OnEnable()
	self:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED", "Update")
	self:RegisterEvent("CHARACTER_POINTS_CHANGED", "UpdateTalents")
	self:RegisterEvent("GLYPH_ADDED", "UpdateGlyphs")
	self:RegisterEvent("GLYPH_DISABLED", "UpdateGlyphs")
	self:RegisterEvent("GLYPH_ENABLED", "UpdateGlyphs")
	self:RegisterEvent("GLYPH_REMOVED", "UpdateGlyphs")
	self:RegisterEvent("GLYPH_UPDATED", "UpdateGlyphs")
	self:RegisterEvent("PLAYER_ENTERING_WORLD", "Update")
	self:RegisterEvent("PLAYER_TALENT_UPDATE", "UpdateTalents")
	self:RegisterEvent("SPELLS_CHANGED", "UpdateSpells")
	self:RegisterEvent("UNIT_PET")
	OvaleState:RegisterState(self, self.statePrototype)
end

function OvaleSpellBook:OnDisable()
	OvaleState:UnregisterState(self)
	self:UnregisterEvent("ACTIVE_TALENT_GROUP_CHANGED")
	self:UnregisterEvent("CHARACTER_POINTS_CHANGED")
	self:UnregisterEvent("GLYPH_ADDED")
	self:UnregisterEvent("GLYPH_DISABLED")
	self:UnregisterEvent("GLYPH_ENABLED")
	self:UnregisterEvent("GLYPH_REMOVED")
	self:UnregisterEvent("GLYPH_UPDATED")
	self:UnregisterEvent("PLAYER_ENTERING_WORLD")
	self:UnregisterEvent("PLAYER_TALENT_UPDATE")
	self:UnregisterEvent("SPELLS_CHANGED")
	self:UnregisterEvent("UNIT_PET")
end

-- Update spells if the player's pet is summoned or dismissed.
function OvaleSpellBook:UNIT_PET(unitId)
	if unitId == "player" then
		self:UpdateSpells()
	end
end

function OvaleSpellBook:Update()
	self:UpdateTalents()
	self:UpdateGlyphs()
	self:UpdateSpells()
	self.ready = true
end

-- Update the player's talents by scanning the talent tab for the active specialization.
-- Store the number of points assigned to each talent.
function OvaleSpellBook:UpdateTalents()
	self:Debug("Updating talents.")
	wipe(self.talent)
	wipe(self.talentPoints)

	local activeTalentGroup = API_GetActiveSpecGroup()
	for i = 1, MAX_TALENT_TIERS do
		for j = 1, NUM_TALENT_COLUMNS do
			local talentId, name, _, selected, _ = API_GetTalentInfo(i, j, activeTalentGroup)
			if talentId then
				local index = 3 * (i - 1) + j
				if index <= MAX_NUM_TALENTS then
					self.talent[index] = name
					if selected then
						self.talentPoints[index] = 1
					else
						self.talentPoints[index] = 0
					end
					self:Debug("    Talent %s (%d) is %s.", name, index, selected and "enabled" or "disabled")
				end
			end
		end
	end
	self:SendMessage("Ovale_TalentsChanged")
end

-- Update the player's glyphs by scanning the glyph socket tab for the active specialization.
function OvaleSpellBook:UpdateGlyphs()
	self:Debug("Updating glyphs.")
	wipe(self.glyph)

	for i = 1, API_GetNumGlyphSockets() do
		local enabled, _, _, glyphSpell, _ = API_GetGlyphSocketInfo(i)
		if enabled and glyphSpell then
			local name = self:GetSpellName(glyphSpell)
			self.glyph[glyphSpell] = name
			self:Debug("    Glyph socket %d has %s (%d).", i, name, glyphSpell)
		else
			self:Debug("    Glyph socket %d is empty.", i)
		end
	end
	self:SendMessage("Ovale_GlyphsChanged")
end

function OvaleSpellBook:UpdateSpells()
	wipe(self.spell)
	wipe(self.spellbookId[BOOKTYPE_PET])
	wipe(self.spellbookId[BOOKTYPE_SPELL])
	wipe(self.isHarmful)
	wipe(self.isHelpful)
	wipe(self.texture)

	-- Scan the first two tabs of the player's spellbook.
	for tab = 1, 2 do
		local name, _, offset, numSpells = API_GetSpellTabInfo(tab)
		if name then
			self:ScanSpellBook(BOOKTYPE_SPELL, numSpells, offset)
		end
	end

	-- Scan the pet's spellbook.
	local numPetSpells, petToken = API_HasPetSpells()
	if numPetSpells then
		self:ScanSpellBook(BOOKTYPE_PET, numPetSpells)
	end

	self:SendMessage("Ovale_SpellsChanged")
end

-- Scan a spellbook and populate self.spell table.
function OvaleSpellBook:ScanSpellBook(bookType, numSpells, offset)
	offset = offset or 0
	self:Debug("Updating '%s' spellbook starting at offset %d.", bookType, offset)
	for index = offset + 1, offset + numSpells do
		local skillType, spellId = API_GetSpellBookItemInfo(index, bookType)
		if skillType == "SPELL" or skillType == "PETACTION" then
			-- Use GetSpellLink() in case this spellbook item was replaced by another spell,
			-- i.e., through talents or Symbiosis.
			local spellLink = API_GetSpellLink(index, bookType)
			if spellLink then
				local _, _, linkData, spellName = ParseHyperlink(spellLink)
				local id = tonumber(linkData)
				self:Debug("    %s (%d) is at offset %d.", spellName, id, index)
				self.spell[id] = spellName
				self.isHarmful[id] = API_IsHarmfulSpell(index, bookType)
				self.isHelpful[id] = API_IsHelpfulSpell(index, bookType)
				self.texture[id] = API_GetSpellTexture(index, bookType)
				self.spellbookId[bookType][id] = index
				if spellId and id ~= spellId then
					self:Debug("    %s (%d) is at offset %d.", spellName, spellId, index)
					self.spell[spellId] = spellName
					self.isHarmful[spellId] = self.isHarmful[id]
					self.isHelpful[spellId] = self.isHelpful[id]
					self.texture[spellId] = self.texture[id]
					self.spellbookId[bookType][spellId] = index
				end
			end
		elseif skillType == "FLYOUT" then
			local flyoutId = spellId
			local _, _, numSlots, isKnown = API_GetFlyoutInfo(flyoutId)
			if numSlots > 0 and isKnown then
				for flyoutIndex = 1, numSlots do
					local id, overrideId, isKnown, spellName = API_GetFlyoutSlotInfo(flyoutId, flyoutIndex)
					if isKnown then
						self:Debug("    %s (%d) is at offset %d.", spellName, id, index)
						self.spell[id] = spellName
						self.isHarmful[id] = API_IsHarmfulSpell(spellName)
						self.isHelpful[id] = API_IsHelpfulSpell(spellName)
						self.texture[id] = API_GetSpellTexture(index, bookType)
						-- Flyout spells have no spellbook index.
						self.spellbookId[bookType][id] = nil
						if id ~= overrideId then
							self:Debug("    %s (%d) is at offset %d.", spellName, overrideId, index)
							self.spell[overrideId] = spellName
							self.isHarmful[overrideId] = self.isHarmful[id]
							self.isHelpful[overrideId] = self.isHelpful[id]
							self.texture[overrideId] = self.texture[id]
							-- Flyout spells have no spellbook index.
							self.spellbookId[bookType][overrideId] = nil
						end
					end
				end
			end
		elseif skillType == "FUTURESPELL" then
			--	no-op
		elseif not skillType then
			break
		end
	end
end

-- Returns the cast time of a spell in seconds.
function OvaleSpellBook:GetCastTime(spellId)
	if spellId then
		local name, _, _, castTime = self:GetSpellInfo(spellId)
		if name then
			if castTime then
				castTime = castTime / 1000
			else
				castTime = 0
			end
		else
			castTime = nil
		end
		return castTime
	end
end

function OvaleSpellBook:GetSpellInfo(spellId)
	local index, bookType = self:GetSpellBookIndex(spellId)
	if index and bookType then
		return API_GetSpellInfo(index, bookType)
	else
		return API_GetSpellInfo(spellId)
	end
end

function OvaleSpellBook:GetSpellName(spellId)
	if spellId then
		local spellName = self.spell[spellId]
		if not spellName then
			spellName = self:GetSpellInfo(spellId)
		end
		return spellName
	end
end

function OvaleSpellBook:GetSpellTexture(spellId)
	return self.texture[spellId]
end

function OvaleSpellBook:GetTalentPoints(talentId)
	local points = 0
	if talentId and self.talentPoints[talentId] then
		points = self.talentPoints[talentId]
	end
	return points
end

function OvaleSpellBook:AddSpell(spellId, name)
	if spellId and name then
		self.spell[spellId] = name
	end
end

-- Returns true if the given glyph spell Id is an active glyph in the player's glyph tab.
function OvaleSpellBook:IsActiveGlyph(glyphId)
	return (glyphId and self.glyph[glyphId]) and true or false
end

-- Returns whether a spell can be used against hostile units.
function OvaleSpellBook:IsHarmfulSpell(spellId)
	return (spellId and self.isHarmful[spellId]) and true or false
end

-- Returns whether a spell can be used on the player or friendly units.
function OvaleSpellBook:IsHelpfulSpell(spellId)
	return (spellId and self.isHelpful[spellId]) and true or false
end

-- Returns true if the given spellId is found in the player's list of known spells.
function OvaleSpellBook:IsKnownSpell(spellId)
	return (spellId and self.spell[spellId]) and true or false
end

-- Returns true if the given talentId is found in the player's talent tree.
function OvaleSpellBook:IsKnownTalent(talentId)
	return (talentId and self.talentPoints[talentId]) and true or false
end

-- Returns the index in the spellbook of the given spell.
function OvaleSpellBook:GetSpellBookIndex(spellId)
	local bookType = BOOKTYPE_SPELL
	while true do
		local index = self.spellbookId[bookType][spellId]
		if index then
			return index, bookType
		elseif bookType == BOOKTYPE_SPELL then
			bookType = BOOKTYPE_PET
		else
			break
		end
	end
end

-- Returns whether a spell is a pet spell.
function OvaleSpellBook:IsPetSpell(spellId)
	local index, bookType = self:GetSpellBookIndex(spellId)
	return bookType == BOOKTYPE_PET
end

-- Returns whether the unit is within range of the spell.
function OvaleSpellBook:IsSpellInRange(spellId, unitId)
	local index, bookType = self:GetSpellBookIndex(spellId)
	if index and bookType then
		return API_IsSpellInRange(index, bookType, unitId)
	elseif self:IsKnownSpell(spellId) then
		local name = self:GetSpellName(spellId)
		return API_IsSpellInRange(name, unitId)
	end
end

-- Returns true if the given spell ID is usable.  A spell is *not* usable if:
--     The player lacks required mana or reagents.
--     Reactive conditions haven't been met.
function OvaleSpellBook:IsUsableSpell(spellId)
	local index, bookType = self:GetSpellBookIndex(spellId)
	if index and bookType then
		return API_IsUsableSpell(index, bookType)
	elseif self:IsKnownSpell(spellId) then
		local name = self:GetSpellName(spellId)
		return API_IsUsableSpell(name)
	end
end

-- Print out the list of active glyphs in alphabetical order.
do
	local output = {}

	function OvaleSpellBook:DebugGlyphs()
		wipe(output)
		OutputTableValues(output, self.glyph)
		return tconcat(output, "\n")
	end

	-- Print out the list of known spells in alphabetical order.
	function OvaleSpellBook:DebugSpells()
		wipe(output)
		OutputTableValues(output, self.spell)
		local total = 0
		for _ in pairs(self.spell) do
			total = total + 1
		end
		output[#output + 1] = "Total spells: " .. total
		return tconcat(output, "\n")
	end

	-- Print out the list of talents in alphabetical order.
	function OvaleSpellBook:DebugTalents()
		wipe(output)
		OutputTableValues(output, self.talent)
		return tconcat(output, "\n")
	end
end
--</public-static-methods>

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

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

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

--<state-methods>
statePrototype.IsUsableSpell = function(state, spellId, target)
	OvaleSpellBook:StartProfiling("OvaleSpellBook_state_IsUsableSpell")
	local isUsable = OvaleSpellBook:IsKnownSpell(spellId)
	local noMana = false
	-- Verify that the spell may be cast given restrictions specified in SpellInfo().
	local si = OvaleData.spellInfo[spellId]
	if si then
		-- Flagged as not usable in the spell information.
		if isUsable and si.unusable then
			local unusable = state:GetSpellInfoProperty(spellId, "unusable", target)
			if unusable == 1 then
				state:Log("Spell ID '%s' is flagged as unusable.", spellId)
				isUsable = false
			end
		end
		-- Verify all requirements with registered handlers.
		if isUsable then
			local requirement
			isUsable, requirement = state:CheckSpellInfo(spellId, target)
			if not isUsable then
				-- Set noMana if the failed requirement is for a primary (poolable) power type.
				if OvalePower.PRIMARY_POWER[requirement] then
					noMana = true
				end
				if noMana then
					state:Log("Spell ID '%s' does not have enough %s.", spellId, requirement)
				else
					state:Log("Spell ID '%s' failed '%s' requirements.", spellId, requirement)
				end
			end
		end
	else
		isUsable, noMana = OvaleSpellBook:IsUsableSpell(spellId, target)
	end
	OvaleSpellBook:StopProfiling("OvaleSpellBook_state_IsUsableSpell")
	return isUsable, noMana
end

-- Get the number of seconds before the spell is ready to be cast, either due to cooldown or resources.
statePrototype.GetTimeToSpell = function(state, spellId, target)
	local timeToSpell = 0
	-- Cooldown.
	do
		local start, duration = state:GetSpellCooldown(spellId)
		local seconds = (duration > 0) and (start + duration - state.currentTime) or 0
		if timeToSpell < seconds then
			timeToSpell = seconds
		end
	end
	-- Pooled resource.
	do
		local seconds = state:TimeToPower(spellId, target)
		if timeToSpell < seconds then
			timeToSpell = seconds
		end
	end
	-- Death knight runes.
	do
		local blood = state:GetSpellInfoProperty(spellId, "blood", target)
		local unholy = state:GetSpellInfoProperty(spellId, "unholy", target)
		local frost = state:GetSpellInfoProperty(spellId, "frost", target)
		local death = state:GetSpellInfoProperty(spellId, "death", target)
		if blood or unholy or frost or death then
			local seconds = state:GetRunesCooldown(blood, unholy, frost, death)
			if timeToSpell < seconds then
				timeToSpell = seconds
			end
		end
	end
	return timeToSpell
end
--</state-methods>