﻿
local mod = klhtm
local me = {}
mod.boss = me

--[[
KTM_Bosses.lua

This module contains all the code for special boss encounters, determining who has aggro, etc.

The old functions from the old KLHTMTargetting.lua are a bit scrappy and due for a major buff in R17, as well
as the entire rest of this module, with lots more boss encounters being added.
]]

me.mastertarget = nil
me.ismtworldboss = false

me.isspellreportingactive = false
me.istrackingspells = false
me.bosstarget = ""

-- me.onload() - called by Core.lua.
me.onload = function()
	
	-- Let's create our parser!
	me.createparser()
	
end

me.myevents = { "CHAT_MSG_MONSTER_EMOTE", "CHAT_MSG_MONSTER_YELL", "CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE", "CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS", "CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE", "CHAT_MSG_COMBAT_HOSTILE_DEATH", }

me.onevent = function()
	
	if event == "CHAT_MSG_MONSTER_EMOTE" then
		
		if string.find(arg1, mod.string.get("boss", "speech", "razorphase2")) then
			
			-- clear threat when phase 2 starts
			mod.table.resetraidthreat()
			
			-- set the master target to Razorgore, but only if a localised version of him exists.
			local bossname = mod.string.get("boss", "name", "razorgore")
			
			if mod.string.unlocalise("boss", "name", bossname) then
				me.automastertarget(bossname)
			end
			
			return
		end
	
	elseif event == "CHAT_MSG_MONSTER_YELL" then
	
		-- Nef Phase 2
		if string.find(arg1, mod.string.get("boss", "speech", "nefphase2")) then
			
			-- reset threat in phase 2
			mod.table.resetraidthreat()
			
			-- boss name is given by the arg2
			me.automastertarget(arg2)
			
			return
		end	
	
		-- ZG Tiger boss phase 2
		if string.find(arg1, mod.string.get("boss", "speech", "thekalphase2")) then
			
			-- reset threat in phase 2
			mod.table.resetraidthreat()
			
			-- boss name is given by the arg2
			me.automastertarget(arg2)
			
			return
		end
		
		-- Rajaxx attacks
		if string.find(arg1, mod.string.get("boss", "speech", "rajaxxfinal")) then
			
			-- reset threat when he finally attacks
			mod.table.resetraidthreat()
			
			-- boss name is given by arg2
			me.automastertarget(arg2)
			
			return
		end
		
		-- Azuregos Port
		if string.find(arg1, mod.string.get("boss", "speech", "azuregosport")) then
			
			-- 1) Find Azuregos
			local bossfound = false
			
			for x = 1, 40 do
				
				if UnitClassification("raid" .. x .. "target") == "worldboss" then
					if CheckInteractDistance("raid" .. x .. "target", 4) then
						mod.table.resetraidthreat()
					end
					
					bossfound = true
					break
				end	
			end
			
			-- couldn't find anyone targetting Azuregos. Better reset just to be sure.
			if bossfound == false then
				mod.table.resetraidthreat()
			end
			
			return
		end
	
	elseif event == "CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS" then
		
		-- 1) Scan for casting pattern
		local output = mod.regex.parse(me.parserset, arg1, event)
	
		if (output.hit == nil) or (output.parser.identifier ~= "mobbuffgain") then
			return
		end
		
		-- 2) Get Boss, Spell
		local boss, spell = output.final[1], output.final[2]
		
		-- noth blink
		if spell == mod.string.get("boss", "spell", "nothblink") then
			
			-- notify the raid, if this event isn't on cooldown 
			if GetTime() < me.bossevents.nothblink.lastoccurence + me.bossevents.nothblink.cooldown then
				-- on cooldown. don't send
			else
				mod.net.sendevent("nothblink")
			end
		end
	
	elseif event == "CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE" then
		
		-- 1) Scan for casting pattern
		local output = mod.regex.parse(me.parserset, arg1, event)
	
		if (output.hit == nil) or (output.parser.identifier ~= "mobspellcast") then
			return
		end
		
		-- 2) Get Boss, Spell
		local boss, spell = output.final[1], output.final[2]
		
		-- twin teleport
		if spell == mod.string.get("boss", "spell", "twinteleport") then
			
			-- notify the raid, if this event isn't on cooldown 
			if GetTime() < me.bossevents.twinteleport.lastoccurence + me.bossevents.twinteleport.cooldown then
				-- on cooldown. don't send
			else
				mod.net.sendevent("twinteleport")
			end
				
		-- gate of shazzrah
		elseif spell == mod.string.get("boss", "spell", "shazzrahgate") then

			-- notify the raid, if this event isn't on cooldown 
			if GetTime() < me.bossevents.shazzrahgate.lastoccurence + me.bossevents.shazzrahgate.cooldown then
				-- on cooldown. don't send
			else
				mod.net.sendevent("shazzrahgate")
			end
		end
	
	elseif event == "CHAT_MSG_COMBAT_HOSTILE_DEATH" then
		
		-- 1) Scan for mob death
		local output = mod.regex.parse(me.parserset, arg1, event)
	
		if (output.hit == nil) or (output.parser.identifier ~= "mobdeath") then
			return
		end
		
		local mobname = output.final[1]
		
		if (mobname == me.mastertarget) and (me.ismtworldboss == true) and (mod.net.lastmtsender == UnitName("player")) and mod.unit.isplayerofficer(UnitName("player")) then
			
			mod.net.clearmastertarget()
			me.ismtworldboss = false
		end
	
	else
		me.parsebossattack(arg1, event)
	end
	
end

--[[
me.onupdate is called by Core.lua. It currently has 3 different parts that are unrelated.
]]
me.onupdate = function()

	local key, value, key2
	
	-- 1) if we are out of combat, reset the ticks on all boss abilities that have them
	if UnitAffectingCombat("player") == nil then
		
		for key, value in me.tickcounters do
			for key2 in value do
				value[key2] = 0
			end
		end
	end
	
	-- 2) clear out all old event reports - one report but no confirmations for 1.0 seconds.
	local timenow = GetTime()
	
	for key, value in me.bossevents do
		if (value.reporter ~= "") and (timenow > value.reporttime + 1.0) then
			
			-- debug
			if mod.out.checktrace("warning", me, "event") then
				mod.out.printtrace(string.format("The event %s has not been confirmed. It was reported by %s.", key, value.reporter))
			end
			
			-- remove the report
			value.reporter = ""
		end
	end

	-- 3) spellreporting: check for target changes
	if (me.isspellreportingactive == true) and (me.istrackingspells == true) and me.mastertarget then
		
		-- 1) find mt
		local x, newtarget
		
		for x = 1, 40 do
			name = UnitName("raid" .. x .. "target")
			if name == me.mastertarget then
				
				-- get target^2
				newtarget = UnitName("raid" .. x .. "targettarget")
				
				if newtarget == nil then
					newtarget = "<none>"
				end
				
				break
			end
		end
		
		-- couldn't find the boss?
		if newtarget == nil then
			newtarget = "<unknown>"
		end
		
		-- report!
		if newtarget ~= me.bosstarget then
			
			-- find the threat of the old target
			local oldthreat = mod.table.raiddata[me.bosstarget]
			if oldthreat == nil then
				oldthreat = "?"
			end
			
			-- threat of the boss' new target
			local newthreat = mod.table.raiddata[me.bosstarget] 
			if newthreat == nil then
				newthreat = "?"
			end
			
			-- print
			mod.out.print(string.format(mod.string.get("print", "boss", "bosstargetchange"), me.mastertarget, me.bosstarget, oldthreat, newtarget, newthreat))
			
			-- update bosstarget
			me.bosstarget = newtarget
		end
	end
	
	-- 4) Check triggers
	me.checktriggers()
	
end

--[[
------------------------------------------------------------------------------------------------
					Boss Events - Sending and Receiving
------------------------------------------------------------------------------------------------

Boss Events are when a mob changes his threat against everyone after taking some action. Some players may be out of (combat log) range of the boss action, so we have nearby players report these special events to the rest of the raid.
There is a potential for abuse if someone in the raid group sends false boss event reports, which could make the raid group incorrectly reset their threat. To counter this we require two people to report the same event within a small time interval for it to be activated.
For each event in <me.bossevents>, we keep track of the person who first reported it, and the time they reported it. If the trace key "boss.fireevent" is enabled, the mod will print out who first reported the event and who confirmed it.
Insertion: players in the raid report events in the network channel, and <me.reportevent(...)> is called from the <netin> module.
Maintenance: no OnUpdate maintenance necessary.
]]

me.bossevents = { }

--[[
me.addevent(eventid, cooldown)
Defines a new event in <me.bossevents>. This is just a helper method to create <me.bossevents>. Called at file load.
<eventid> is a localisation key in the "boss"-"spell" set.
<cooldown> is the minimum time between casts, extreme lower bound. Want it large enough to avoid spams. 1 sec would probably do.
]]
me.addevent = function(eventid, cooldown)
	
	me.bossevents[eventid] = 
	{
		["cooldown"] = cooldown,
		lastoccurence = 0, 	-- GetTime()
		reporter = "",
		reporttime = 0, 	-- GetTime()
		["eventid"] = eventid,
	}
	
end

-- define all possible events. These methods are called at file read time.
me.addevent("shazzrahgate", 5.0)
me.addevent("twinteleport", 5.0)
me.addevent("wrathofragnaros", 5.0)
me.addevent("nothblink", 5.0)

--[[
mod.boss.reportevent(eventid, player)
Called when someone in the raid reports a boss event.
<eventid> is the internal name of the event.
<player> is the name of the player who reported it.
]]
me.reportevent = function(eventid, player)

	local eventdata = me.bossevents[eventid]
	local timenow = GetTime()
	
	-- ignore if the event is cooling down
	if timenow < eventdata.lastoccurence + eventdata.cooldown then
		return
	end
	
	-- has this been reported recently? If so it is now confirmed and we can run it.
	if (eventdata.reporter ~= "") and (eventdata.reporter ~= player) then
		me.fireevent(eventdata, player)
	
	-- always trust reports from yourself
	elseif player == UnitName("player") then
		me.fireevent(eventdata, player)
	
	-- some player reports a new event. wait for confirmation
	else
		eventdata.reporter = player
		eventdata.reporttime = timenow
	end

end

--[[
me.fireevent(eventdata, player)
Run when an event is confirmed. Does whatever the event does.
<eventdata> is an structure in <me.bossevents>
<player> is the name of the player who confirmed the event
]]
me.fireevent = function(eventdata, player)
	
	-- debug
	if mod.out.checktrace("info", me, "event") then
		mod.out.printtrace(string.format("The event |cffffff00%s|r has occured. It was reported by %s and confirmed by %s.", eventdata.eventid, eventdata.reporter, player))
	end
	
	-- first reset the event's timers
	eventdata.lastoccurence = GetTime()
	eventdata.reporter = ""
	
	-- now actually do the event
	if eventdata.eventid == "shazzrahgate" then
		mod.table.resetraidthreat()
		
	elseif eventdata.eventid == "twinteleport" then
		mod.table.resetraidthreat()
		
		-- activate the proximity aggro detection trigger
		me.starttrigger("twinemps")
		
	elseif eventdata.eventid == "wrathofragnaros" then
		mod.table.resetraidthreat()
	
	elseif eventdata.eventid == "nothblink" then
		mod.table.resetraidthreat()
	end
	
end

--[[
------------------------------------------------------------------------------------------------
				Triggers - Hard To Detect Events That Require Polling
------------------------------------------------------------------------------------------------

Some events have no easily defined actions such as a combat log event, and must be checked for periodically instead.
For example in the Twin Emperors encounter, after a teleport the closest person to each emperor is given a moderate amount of threat. The only way to see who received the threat is to see who the emperors target. However, after the teleport they become stunned and have no target, so we have to wait until the stun period has ended. So we make a trigger to periodically check them for new targets.
Each trigger has these properties:
<isactive>		boolean, whether the mod is checking the trigger
<startdelay>	time in seconds after the trigger is activated that the mod will start checking it.
<timeout>		time in seconds after the trigger has started that the mod should give up on it
<mystarttime>	when the most recent activation of the trigger occured.

The names and basic properties of triggers are defined in the variable <me.triggers>. This is a key-value list where the key is the internal name of the trigger, and the value is a structure with the variables described above.
To activate a trigger, call the <me.starttrigger(trigger)> function with the internal name of the trigger.
The code that will run each time a trigger is polled is contained in the variable <me.triggerfunctions>. This is a key-value list, where the key is the internal name and the value is the function that is run.
]]

me.triggers = 
{
	twinemps = 
	{
		isactive = false,
		startdelay = 0.5,
		timeout = 5.0,
		mystarttime = 0,
		data = 0,
	},
	autotarget = 
	{
		isactive = false,
		startdelay = 1.0,
		timeout = 300.0,
		mystarttime = 0,
		data = 0,
	}
}

--[[
me.starttrigger(trigger)
Activates a trigger. The mod will start periodically checking for it.
<trigger> is the mod's internal identifier of the trigger, and matches a key to me.triggers.
]]
me.starttrigger = function(trigger)
	
	-- debug check for trigger being defined. We should generalise this, since is happens in a flew different places in the mod. i.e. "badidentifierargument"
	-- maybe also some kind of flood control to stop error messages spamming onupdate.
	
	if (trigger == nil) or (me.triggers[trigger] == nil) then
		
		-- report error
		if mod.out.checktrace("error", me, "trigger") then
			mod.out.printtrace(string.format("There is no trigger |cffffff00%s|r.", trigger or "<nil>"))
		end
		
		return
	end
	
	local triggerdata = me.triggers[trigger]
	triggerdata.isactive = true
	triggerdata.mystarttime = GetTime() + triggerdata.startdelay
	triggerdata.data = 0
	
	-- debug
	if mod.out.checktrace("info", me, "trigger") then
		mod.out.printtrace(string.format("The |cffffff00%s|r trigger has been activated.", trigger))
	end
		
end

--[[
This variable gives the code that runs when an active trigger is checked. The keys are the internal names of triggers, that match keys in <me.triggers>.
The values are functions. The functions should return non-nil if the trigger is to be deactivated.
]]
me.triggerfunctions = 
{
	--[[ 
	We want to find out if we are being targetting by one of the emps. To do this we find the emperors by scanning the targets of everyone in the raid group. Then once we have an emps's target, we check whether that is us.
	It might occur that one emps' target is known but the other is not (not sure if this could happen). In this case the trigger should not end; we should keep checking until we know both targets.
	However, if one of the emps is targetting us, we can instantly give ourself threat and exit.
	The threat gained is set at 2000. This isn't confirmed, and is instead a bit of a guess.
	]]
	twinemps = function(triggerdata)
		
		local x, name, firstbossname, unitid
		local bosshits = 0
		local bosstargets = 0
		
		-- loop through everyone in the raid
		for x = 1, 40 do
			
			unitid = "raid" .. x .. "target"
			if UnitExists(unitid) and (UnitClassification(unitid) == "worldboss") then
				
				-- we've found an emperor. check if we've seen him before
				name = UnitName(unitid)
				
				if name ~= firstbossname then
					bosshits = bosshits + 1
					
					-- if this is the first boss we've seen, put his name up
					if bosshits == 1 then
						firstbossname = name
					end
					
					-- now find the player the boss is targetting
					unitid = unitid .. "target"
					
					if UnitExists(unitid) then
						bosstargets = bosstargets + 1
						
						if UnitIsUnit("player", unitid) then
							-- an emp is targetting us. give us a bit of threat.
							
							mod.combat.event.hits = 1
							mod.combat.event.threat = 2000
							mod.combat.event.damage = 0
							mod.combat.event.rage = 0
							
							mod.combat.addattacktodata(mod.string.get("threatsource", "threatwipe"), mod.combat.event)
							
							-- clear hits for total column
							mod.combat.event.hits = 0
							mod.combat.addattacktodata(mod.string.get("threatsource", "total"), mod.combat.event)
							
							-- if an emperor is targetting us, he will be the only one, and we have all the information we need, so we want the trigger to deactivate
							bosstargets = 2
							bosshits = 2
						end
					end
					
					-- if we have found 2 bosses now, there's no need to do more searching
					if bosshits == 2 then 
						break
					end
				end
			end
		end
		
		-- don't give up on the trigger until we have found both boss targets on one loop
		if bosstargets == 2 then
			return true
		end
	end,
	
	--[[
	Autotarget trigger runs when you run the command "/ktm boss autoatarget". When you next target a world boss, you will set the target and clear the meter.
	]]
	autotarget = function(triggerdata)
		
		if UnitExists("target") and (UnitClassification("target") == "worldboss") then
			
			-- found a target. now only activate if we've been targetting him for a while
			if triggerdata.data == 0 then
				triggerdata.data = GetTime()
				return
				
			else
				-- 500 ms minimum.
				if GetTime() < triggerdata.data + 0.5 then
					return
				end
			end
			
			-- found a target. Activate
			if me.mastertarget == UnitName("target") then
				
				-- someone has already set the master target to this mob. In this case don't do anything.
				mod.out.print(string.format(mod.string.get("print", "boss", "autotargetabort"), UnitName("target")))
				
			else
				mod.net.clearraidthreat()
				mod.net.sendmastertarget()
			end
			
			return true
		end	
		
	end
}

--[[
me.checktriggers()
Loops through all possible triggers, checking for active ones and running them if need be. This is called in the OnUpdate() method.
]]
me.checktriggers = function()

	local key, data
	local timenow = GetTime()
	
	for key, data in me.triggers do
		
		-- ignore inactive triggers
		if data.isactive == true then
			
			-- stop the trigger if it has timed out
			if timenow > data.mystarttime + data.timeout then
				
				data.isactive = false
				
				-- debug
				if mod.out.checktrace("warning", me, "trigger") then
					mod.out.printtrace(string.format("The trigger |cffffff00%s|r timed out.", key))
				end
				
			-- don't process the trigger if the start delay is not over
			elseif timenow < data.mystarttime then
				-- (do nothing)
				
			else
				-- ok, run a trigger check
				if me.triggerfunctions[key](data) then
					data.isactive = false
				end
			end
		end
	end
end

--[[
------------------------------------------------------------------------------------------------
			Parsing the Combat Log to Detect Boss Special Attacks and Spells
------------------------------------------------------------------------------------------------
]]

-- me.parserset = { }  -- defined in me.createparser

--[[
me.createparser()
Called from me.onload() on startup. Creates the parser engine from the constructor.
]]
me.createparser = function()
	
	me.parserset = { }
	
	local parserdata
	
	for _, parserdata in me.parserconstructor do
		mod.regex.addparsestring(me.parserset, parserdata[1], parserdata[2], parserdata[3])
	end
	
end

-- This describes all the combat log lines we are checking for
me.parserconstructor = 
{
	-- this is for school spells or debuffs
	{"magicresist", "SPELLRESISTOTHERSELF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"}, -- "%s's %s was resisted."

	-- these two are for school spells only
	{"spellhit", "SPELLLOGSCHOOLOTHERSELF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"}, -- "%s's %s hits you for %d %s damage."
	{"spellhit", "SPELLLOGCRITSCHOOLOTHERSELF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"}, -- "%s's %s crits you for %d %s damage."

	-- spellboth is for abilities or school spells
	{"attackabsorb", "SPELLLOGABSORBOTHERSELF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"}, -- "You absorb %s's %s."
		
	-- ability hit / miss only works for physical spells.
	{"abilityhit", "SPELLLOGOTHERSELF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"}, 		-- "%s's %s hits you for %d."
	{"abilityhit", "SPELLLOGCRITOTHERSELF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"},	-- "%s's %s hits you for %d."
	{"abilityhit", "SPELLBLOCKEDOTHERSELF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"}, 	-- "%s's %s was blocked."
	{"abilitymiss", "SPELLDODGEDOTHERSELF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"},	-- "%s's %s was dodged."
	{"abilitymiss", "SPELLPARRIEDOTHERSELF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"},	-- "%s's %s was parried."
	{"abilitymiss", "SPELLMISSOTHERSELF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"},		-- "%s's %s misses you."
	
	{"debuffstart", "AURAADDEDSELFHARMFUL", "CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE"}, -- "You are afflicated by %s."
	
	{"mobspellcast", "SPELLCASTGOOTHER", "CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE"},		-- "%s casts %s."
	{"mobbuffgain", "AURAADDEDOTHERHELPFUL", "CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS"}, 		-- "%s gains %s."
	
	{"mobdeath", "UNITDIESOTHER", "CHAT_MSG_COMBAT_HOSTILE_DEATH"}, -- "%s dies."
}

me.tickcounters = { }

--[[
me.parsebossattack(message, event)

Handles a combat log line that describes a boss's attack or spell against the player.
--> Stage one is to parse the message to find which formatting pattern the message matches, e.g. "magicresist" or
"spellhit" etc, or none (then just exit).
--> Stage two is to fill in <me.action>, whch descibes the important parts of the attack, using the formatting patter and the arguments captured by the pattern.
--> Then we check whether this attack is actually a threat modifying attack. For this to be the case, there would be a localisation string whose value is the attack name, and there would be an entry in <me.bossattacks> with the same key as the localisation key.
--> Next we identify the ability w.r.t. the mob. Does the ability only come from one mob, and if so is the mob who just attacked us the right one? This involves a check in the next level of <me.bossattacks>.
--> Now we know the specific attack performed against us, and we have to work out whether it triggered. If the ability does not trigger on a miss (e.g. Knock Away), we won't do anything. If the ability only triggers after a number of ticks (Time Lapse), it will only trigger if the correct number of ticks has passed.
--> If it triggers, we just change our threat by the right amount, then report the threat change in the <combat> and <table> modules.

<message> is the combat log line.
<event> is the chat message event <message> was received on.
Returns: nothing.
]]
me.parsebossattack = function(message, event)
	
	-- stage 1: regex
	local output = mod.regex.parse(me.parserset, message, event)
	
	if output.hit == nil then
		return
	end

	-- interrupt: wrath of ragnaros
	if output.final[2] == mod.string.get("boss", "spell", "wrathofragnaros") then

		-- notify the raid, if this event isn't on cooldown 
		if GetTime() < me.bossevents.wrathofragnaros.lastoccurence + me.bossevents.wrathofragnaros.cooldown then
			-- on cooldown. don't send
		else
			mod.net.sendevent("wrathofragnaros")
		end
	end
 
	-- Set the mob and ability (always arg1 and arg2, except for debuffgain)
	if output.parser.identifier == "debuffstart" then
		me.resetaction("", output.final[1])
	else	
		me.resetaction(output.final[1], output.final[2])
	end
	
	-- set the spell and hit types
	local description = me.attackdescription[output.parser.identifier]

	if description.ishit then me.action.ishit = true end
	if description.isspell then me.action.isspell = true end
	if description.isdebuff then me.action.isdebuff = true end
	if description.isphysical then me.action.isphysical = true end
	
	-- Now, is the attack known?
	local spellid = mod.string.unlocalise("boss", "spell", me.action.ability)
	if (spellid == nil) or (me.bossattacks[spellid] == nil) then
		return
	end
	
	-- Check for a mob match
	local spelldata
	
	local mobid = mod.string.unlocalise("boss", "name", me.action.mobname)
	
	if mobid and me.bossattacks[spellid][mobid] then
		-- there is a specific version of this spell for this particular mob
		spelldata = me.bossattacks[spellid][mobid]
	
	elseif me.bossattacks[spellid].default == nil then
		-- this mob does not match any of the mobs that have the ability
		return
		
	else
		spelldata = me.bossattacks[spellid].default
	end
	
	-- Now process the spell
	
	-- 1) Does the ability activate on a miss?
	if (me.action.ishit == false) and (spelldata.effectonmiss == false) then
		
		-- ability will not activate
		if mod.out.checktrace("info", me, "attack") then
			mod.out.printtrace(string.format("%s's attack %s did not activate because it missed.", me.action.mobname, me.action.ability))
		end
		
		-- spell reporting
		if me.isspellreportingactive == true then
			mod.net.reportspelleffect(me.action.ability, me.action.mobname, "miss")
		end
		
		return
	end
	
	-- 2) Check number of ticks
	local mytickdata
	
	if spelldata.ticks ~= 1 then
		
		-- create a list if none exists yet
		if me.tickcounters[me.action.ability] == nil then
			me.tickcounters[me.action.ability] = { }
		end
		
		if me.tickcounters[me.action.ability][me.action.mobname] == nil then
			me.tickcounters[me.action.ability][me.action.mobname] = 0
		end
		
		-- create an entry if none exists so far
		me.tickcounters[me.action.ability][me.action.mobname] = me.tickcounters[me.action.ability][me.action.mobname] + 1
		
		-- now, have we gone enough ticks?	
		if me.tickcounters[me.action.ability][me.action.mobname] < spelldata.ticks then
			
			-- not enough ticks
			if mod.out.checktrace("info", me, "attack") then
				mod.out.printtrace(string.format("This is tick number %d of %s; it will activate in another %d ticks.", me.tickcounters[me.action.ability][me.action.mobname], me.action.ability, spelldata.ticks - me.tickcounters[me.action.ability][me.action.mobname]))
			end
			
			-- spell reporting
			local value1 = me.tickcounters[me.action.ability][me.action.mobname]
			local value2 = spelldata.ticks - value1
			
			if me.isspellreportingactive then
				mod.net.reportspelleffect(me.action.ability, me.action.mobname, "tick", value1, value2)
			end
			
			return
			
		else
			-- we just got enough ticks, so now reset to 0
			me.tickcounters[me.action.ability][me.action.mobname] = 0
			
		end
	end
	
	-- 3) To get here, the ability is definitely activating
	if mod.out.checktrace("info", me, "attack") then
		mod.out.printtrace(string.format("%s's %s activates, multiplying your threat by %s then adding %s.", me.action.mobname, me.action.ability, spelldata.multiplier, spelldata.addition))
	end
	
	-- compute new threat
	local newthreat = mod.table.getraidthreat() * spelldata.multiplier + spelldata.addition
	
	-- remember threat can't go below 0
	newthreat = math.max(0, newthreat)
	
	-- threat change is the (possibly negative) amount of threat that was added
	local threatchange = newthreat - mod.table.getraidthreat()
	
	-- spellreporting
	if me.isspellreportingactive then
		mod.net.reportspelleffect(me.action.ability, me.action.mobname, "proc", math.floor(0.5 + mod.table.getraidthreat()), math.floor(0.5 + newthreat))
	end
	
	-- add to threat wipes section, but not to totals
	mod.combat.event.hits = 1
	mod.combat.event.damage = 0
	mod.combat.event.rage = 0
	mod.combat.event.threat = threatchange
	
	mod.combat.addattacktodata(mod.string.get("threatsource", "threatwipe"), mod.combat.event)
	
	-- now add it to your raid threat total (but not your personal threat total)
	mod.table.raidthreatoffset = mod.table.raidthreatoffset + threatchange
	
	-- ask for a redraw of the personal window
	KLHTM_RequestRedraw("self")
	
end

--[[
me.resetaction()
Sets the values of me.action to their defaults.
]]
me.resetaction = function(mobname, ability)

	me.action.mobname = mobname
	me.action.ability = ability
	me.action.ishit = false
	me.action.isphysical = false
	me.action.isdebuff = false
	me.action.isspell = false
	
end

me.action = 
{
	mobname = "",
	ability = "",
	ishit = false,
	isphysical = false,
	isdebuff = false,
	isspell = false,
}

-- Note that <ishit> defaults to false, so we only set it when it is true
me.attackdescription = 
{
	["magicresist"] = 
	{
		isspell = true,
		isdebuff = true,
	},
	["spellhit"] =
	{
		isspell = true,
		ishit = true,
	},
	["attackabsorb"] = 
	{
		isspell = true,
		isphysical = true,
		ishit = true,
	},
	["abilityhit"] = 
	{
		isphysical = true,
		ishit = true,
	},
	["abilitymiss"] = 
	{	
		isphysical = true,
	},
	["debuffstart"] = 
	{
		ishit = true,
		isdebuff = true,
	},
}

--[[
Here is where you define all the boss' attacks that affect threat.
	The first key in me.bossattacks is the identifier of the spell. That is, mod.string.get("boss", "spell", <first key>) 
is the localised version.
	The second key deep specifies which mob the attack comes from. You can choose "default" to make it apply to all mobs,
or you can specify a mob id, which will override the "default" value. Mob id's recognised are all the keys in the 
"boss" -> "name" section of the localisation tree.
	So if you want to define a new attack name or boss name, you'll have to add a new key to the localisation tree in the
"boss" -> "spell" and "boss" -> "name" sections respectively.
	Inside each block, the follow parameters are defined:
	<multiplier> - a value that your threat is multiplier by. e.g. the standard Knock Away is -50% threat, so this would be a 
multiplier of 0.5. A complete threat wipe would be a multiplier of 0.
	<addition> - a flat value that is added to your threat. Can be positive or negative or 0.
	<effectonmiss> - a boolean value specifying whether the event triggers even when it is resisted or misses you.
	<ticks> - the number of times you must suffer the attack before your threat is changed. e.g. most knockbacks happen every
time so <ticks> = 1, but Time Lapse reduces your threat only after a certain number of applications.
	<type> - describes the attack. Can be "physical" or "spell" or "debuff". Not used by the mod at the moment: it will 
assume that if the name matches, it has found the right ability.
]]
me.bossattacks = 
{
	knockaway = 
	{
		default = 
		{
			multiplier = 0.5,
			addition = 0,
			effectonmiss = false,
			ticks = 1,
			type = "physical",
		},
		onyxia = 
		{
			multiplier = 0.67,
			addition = 0,
			effectonmiss = false,
			ticks = 1,
			type = "physical",
		},
	},
	wingbuffet = 
	{
		default = 
		{
			multiplier = 0.5,
			addition = 0,
			effectonmiss = false,
			ticks = 1,
			type = "physical",
		},
		onyxia = 
		{
			multiplier = 1.0,
			addition = 0,
			effectonmiss = false,
			ticks = 1,
			type = "physical",
		},
	},
	timelapse = 
	{
		default = 
		{
			multiplier = 0,
			addition = 0,
			effectonmiss = false,
			ticks = 5,
			type = "debuff"
		}
	},
	sandblast = 
	{
		default = 
		{
			multiplier = 0,
			addition = 0,
			effectonmiss = false,
			ticks = 1,
			type = "spell"
		}
	},
}

--[[
------------------------------------------------------------------------------------------------
	+ B +			Normal Shit
------------------------------------------------------------------------------------------------
]]

--[[
me.automastertarget(target)
Called when the mod itself sets the mastertarget.
<target> is the localised name of the mob.
]]
me.automastertarget = function(target)

	me.mastertarget = target
	
	-- stop network module autoupdating the master target
	mod.net.lastmtsender = ""

	-- explain to user
	mod.out.print(string.format(mod.string.get("print", "boss", "automt"), target))
	
end

--[[
mod.boss.newmastertarget(author, target)
Handles a request to change the master target.
<author> is the name of the officer in the group who changed the master target.
<target> is the name of his current mob, localised to him.
The problem is that if you have a different localisation to <author>, you will think his target is spelt differently to <target>! So we have to check for this, and override if necessary.
]]
me.newmastertarget = function(author, target)
	
	-- 1) Find the author's UnitID
	local officerunit = mod.unit.findunitidfromname(author)
	local officertarget = UnitName(tostring(officerunit) .. "target")
		
	-- 2) Check for differences
	if officertarget == nil then
		mod.out.print(string.format(mod.string.get("print", "network", "newmttargetnil"), target, author))
	
	elseif officertarget ~= target then
		mod.out.print(string.format(mod.string.get("print", "network", "newmttargetmismatch"), author, target, officertarget))
		target = officertarget
	end
	
	-- 3) Check for worldboss target
	if author == UnitName("player") and UnitClassification("target") == "worldboss" then
		me.ismtworldboss = true
	else
		me.ismtworldboss = false
	end
	
	-- 3) OK
	me.mastertarget = target
	mod.out.print(string.format(mod.string.get("print", "network", "newmt"), target, author))
	
end

-- todo: stuff, maybe?
me.clearmastertarget = function()
	
	me.mastertarget = nil
	
end

--[[
mod.boss.targetismaster(target)
Checks whether <target> is the master target. The master target is usually just a name / string, but it may be something
more general in the future (e.g. tracking both bosses in the Twin Emps fight).
<target> is the name of the mob being queried.
Return: non-nil if <target> is a mastertarget.
]]
me.targetismaster = function(target)
	
	if me.mastertarget == nil then
		return true
	end
	
	if target == me.mastertarget then
		return true
	end
	
	-- insert other checks here, later.
	
	return -- (nil)
	
end

-----------------------------------
--		Targeting Behaviour      --
-----------------------------------
--[[

True = True target. Who the mob would have aggro on, if we discount secondary targetting and taunts, etc.
Curr = Current target. Who the mob's target unitid is
New  = New target. If the mob's current target has changed

x, y = players with known threat values
nil  = no target
?    = player with unknown threat value


True	Curr	New		Result
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
nil		nil		x		true and curr become x

x		x		y		curr -> y. In 2 seconds with no change, true -> y. also, if their threat goes above 110 of yours in that time, put them up.
x		x		?		curr -> ?. In 2 seconds with no change, true -> ?
x		x		nil		curr -> nil.

x		y		z		curr -> z. In 2 secs, true -> z
x		y		nil		curr -> nil.
x		y		?		curr -> ?. In 2 secs, true -> ?
x		y		x		curr -> x

x		nil		y		If it's been nil for more than 1 second, true = y. Otherwise true -> y after 2 - (secs at nil) secs.
x		nil		x		curr -> x
x		nil		?		same as x - nil - y

?		?		x		true -> x. Easy enough.

]]

-- Master Target Variables
me.mttruetarget = nil	-- The Name of the player who this mod thinks is the true target
me.mtcurrenttarget = nil	-- The Name of the player the mob is currently targetting
me.mttargetswaptime = 0	-- The time when the mob last changed its target
me.unknowntarget = "#unknown" 
me.mastertargettarget = nil

-- lm2: aggro gain is now calculated just before redrawing the raid frame
me.updateaggrogain = function()
	
	if mod.boss.mastertarget == nil then
		me.recalculateaggrogain()
	else
		me.updatetrueaggrotarget()
	end
	
end
	

me.updatetrueaggrotarget = function()
		
	-- 1) find a UnitID for the master target
	local mastertargetid = nil
	
	if UnitName("target") == me.mastertarget then
		mastertargetid = "target"
		
	else
		-- check everyone in the raid
		local x
		
		for x = 1, 40 do
			if UnitName("raid" .. x .. "target") == me.mastertarget then
				mastertargetid = "raid" .. x .. "target"
				break
			end
		end
	end
	
	-- 2) If noone can see the mob, give up.
	if mastertargetid == nil then
		
		me.mttruetarget = me.unknowntarget
		me.mtcurrenttarget = me.unknownuarget
		mod.table.raiddata[mod.string.get("misc", "aggrogain")] = nil
		
		return
	end
	
	-- 3) Get the boss' current target
	local targetnow = UnitName(mastertargetid .. "target")
	 
	-- 4) Reevaluate True, Current, Time
	
	-- a) Transitions from true=unknown	
	if (me.mttruetarget == nil) or (me.mttruetarget == me.unknowntarget) or (mod.table.raiddata[me.mttruetarget] == nil) then
		
		-- debug print
		if targetnow ~= me.mttruetarget then
			if targetnow == nil then
				if mod.out.checktrace("info", me, "target") then
					mod.out.printtrace("Target changed from bad to nil.")
				end
			else
				if mod.out.checktrace("info", me, "target") then
					mod.out.printtrace(string.format("Target changed from bad to %s.", targetnow))
				end
				
				if (me.isknockbackdiscoveryactive == true) and (targetnow == UnitName("player")) then
					mod.net.sendmessage("aggrogain " .. mod.table.getraidthreat())
				end
			end
		end
		
		me.mttruetarget = targetnow
		me.mtcurrenttarget = targetnow
	
	-- b) Transitions from true = known
	elseif targetnow ~= me.mtcurrenttarget then
	
		if me.mtcurrenttarget ~= nil then
			me.mttargetswaptime = GetTime()
		end
	
		me.mtcurrenttarget = targetnow
		
		if targetnow == nil then
			if mod.out.checktrace("info", me, "target") then
				mod.out.printtrace("CurrentTarget changed to nil.")
			end
		else
			if mod.out.checktrace("info", me, "target") then
				mod.out.printtrace(string.format("CurrentTarget changed from bad to %s.", targetnow))
			end
			
			if (me.isknockbackdiscoveryactive == true) and (targetnow == UnitName("player")) then
				mod.net.sendmessage("aggrogain " .. mod.table.getraidthreat())
			end
		end
	end
	
	-- 5) Check if CurrentTarget should become Truetarget
	if me.mttruetarget ~= me.mtcurrenttarget then
		-- to get here, true target is known.
		
		if me.mtcurrenttarget == nil then
			-- do nothing
			
		elseif mod.table.raiddata[me.mtcurrenttarget] == nil then
			-- switch to unknown if it's been more than 2 seconds
			
			if GetTime() > me.mttargetswaptime + 2 then
				me.mttruetarget = me.mtcurrenttarget
				
				if mod.out.checktrace("info", me, "target") then
					mod.out.printtrace(string.format("TrueTarget switches to the unknown %s after 2 seconds.", me.mttruetarget))
				end
			end
			
		else -- current target is a known user
			if GetTime() - me.mttargetswaptime > 2 then
				me.mttruetarget = me.mtcurrenttarget
				
				if mod.out.checktrace("info", me, "target") then
					mod.out.printtrace(string.format("TrueTarget switches to the known player %s after 2 seconds.", me.mttruetarget))
				end
				
			elseif mod.table.raiddata[me.mtcurrenttarget] > mod.data.threatconstants.meleeaggrogain * mod.table.raiddata[me.mttruetarget] then
				me.mttruetarget = me.mtcurrenttarget

				if mod.out.checktrace("info", me, "target") then
					mod.out.printtrace(string.format("TrueTarget switches to the known player %s due to high threat.", me.mttruetarget))
				end
			end
		end
	end
	
	-- update the AggroGain virtual player
	if ((me.mttruetarget ~= nil) and (me.truetarget ~= me.unknowntarget) and
		(mod.table.raiddata[me.mttruetarget] ~= nil)) then
		
		local aggro = mod.table.raiddata[me.mttruetarget];

		if (UnitName("player") ~= me.mttruetarget) then
			if CheckInteractDistance(mastertargetid, 1) then
				aggro = math.ceil(aggro * mod.data.threatconstants.meleeaggrogain)
			else
				aggro = math.ceil(aggro * mod.data.threatconstants.rangeaggrogain)
			end
		end
		
		mod.table.raiddata[mod.string.get("misc", "aggrogain")] = aggro;
	else
		mod.table.raiddata[mod.string.get("misc", "aggrogain")] = nil
	end
end

me.recalculateaggrogain = function()

	-- update aggro, and such
	local newaggrogain
	local targetname = ""
	local maxdepth = 5
	local i
	local targetacquired = false
	
	for i = 1, maxdepth do
		targetname = targetname .. "target"
		
		if UnitName(targetname) == nil then
			break
		
		elseif UnitIsFriend("player", targetname) == nil then
			targetacquired = true
			break
		end
	end
	
	if targetacquired == false then
		-- remove aggro gain
		newaggrogain = nil
		
	else
		local mobtarget = UnitName(targetname .. "target")
		if mobtarget == nil then
			mobtarget = "<nil>"
		end
		
		if mod.table.raiddata[mobtarget] then
			-- aggro target has a known threat value
			
			if UnitName("player") == mobtarget then
				newaggrogain = mod.table.raiddata[mobtarget]
			
			else
				-- now check our range to the mob
				if CheckInteractDistance(targetname, 1) then
					-- we're in melee range 
					newaggrogain = math.ceil(mod.table.raiddata[mobtarget] * mod.data.threatconstants.meleeaggrogain)
					
				else
					-- there's a small region where we might be in melee range. for now, assume not
					newaggrogain = math.ceil(mod.table.raiddata[mobtarget] * mod.data.threatconstants.rangeaggrogain)
				end
			end
		else
			newaggrogain = nil
		end
	end
	
	local currentaggrogain = mod.table.raiddata[mod.string.get("misc", "aggrogain")]
	if newaggrogain ~= currentaggrogain then
		mod.table.raiddata[mod.string.get("misc", "aggrogain")] = newaggrogain
	end

end
