vanilla-wow-addons – Rev 1

Subversion Repositories:
Rev:

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