vanilla-wow-addons – Blame information for rev 1

Subversion Repositories:
Rev:
Rev Author Line No. Line
1 office 1 
2 local mod = klhtm
3 local me = {}
4 mod.boss = me
5  
6 --[[
7 KTM_Bosses.lua
8  
9 This module contains all the code for special boss encounters, determining who has aggro, etc.
10  
11 The old functions from the old KLHTMTargetting.lua are a bit scrappy and due for a major buff in R17, as well
12 as the entire rest of this module, with lots more boss encounters being added.
13 ]]
14  
15 me.mastertarget = nil
16 me.ismtworldboss = false
17  
18 me.isspellreportingactive = false
19 me.istrackingspells = false
20 me.bosstarget = ""
21  
22 -- me.onload() - called by Core.lua.
23 me.onload = function()
24  
25 -- Let's create our parser!
26 me.createparser()
27  
28 end
29  
30 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", }
31  
32 me.onevent = function()
33  
34 if event == "CHAT_MSG_MONSTER_EMOTE" then
35  
36 if string.find(arg1, mod.string.get("boss", "speech", "razorphase2")) then
37  
38 -- clear threat when phase 2 starts
39 mod.table.resetraidthreat()
40  
41 -- set the master target to Razorgore, but only if a localised version of him exists.
42 local bossname = mod.string.get("boss", "name", "razorgore")
43  
44 if mod.string.unlocalise("boss", "name", bossname) then
45 me.automastertarget(bossname)
46 end
47  
48 return
49 end
50  
51 elseif event == "CHAT_MSG_MONSTER_YELL" then
52  
53 -- Nef Phase 2
54 if string.find(arg1, mod.string.get("boss", "speech", "nefphase2")) then
55  
56 -- reset threat in phase 2
57 mod.table.resetraidthreat()
58  
59 -- boss name is given by the arg2
60 me.automastertarget(arg2)
61  
62 return
63 end
64  
65 -- ZG Tiger boss phase 2
66 if string.find(arg1, mod.string.get("boss", "speech", "thekalphase2")) then
67  
68 -- reset threat in phase 2
69 mod.table.resetraidthreat()
70  
71 -- boss name is given by the arg2
72 me.automastertarget(arg2)
73  
74 return
75 end
76  
77 -- Rajaxx attacks
78 if string.find(arg1, mod.string.get("boss", "speech", "rajaxxfinal")) then
79  
80 -- reset threat when he finally attacks
81 mod.table.resetraidthreat()
82  
83 -- boss name is given by arg2
84 me.automastertarget(arg2)
85  
86 return
87 end
88  
89 -- Azuregos Port
90 if string.find(arg1, mod.string.get("boss", "speech", "azuregosport")) then
91  
92 -- 1) Find Azuregos
93 local bossfound = false
94  
95 for x = 1, 40 do
96  
97 if UnitClassification("raid" .. x .. "target") == "worldboss" then
98 if CheckInteractDistance("raid" .. x .. "target", 4) then
99 mod.table.resetraidthreat()
100 end
101  
102 bossfound = true
103 break
104 end
105 end
106  
107 -- couldn't find anyone targetting Azuregos. Better reset just to be sure.
108 if bossfound == false then
109 mod.table.resetraidthreat()
110 end
111  
112 return
113 end
114  
115 elseif event == "CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS" then
116  
117 -- 1) Scan for casting pattern
118 local output = mod.regex.parse(me.parserset, arg1, event)
119  
120 if (output.hit == nil) or (output.parser.identifier ~= "mobbuffgain") then
121 return
122 end
123  
124 -- 2) Get Boss, Spell
125 local boss, spell = output.final[1], output.final[2]
126  
127 -- noth blink
128 if spell == mod.string.get("boss", "spell", "nothblink") then
129  
130 -- notify the raid, if this event isn't on cooldown
131 if GetTime() < me.bossevents.nothblink.lastoccurence + me.bossevents.nothblink.cooldown then
132 -- on cooldown. don't send
133 else
134 mod.net.sendevent("nothblink")
135 end
136 end
137  
138 elseif event == "CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE" then
139  
140 -- 1) Scan for casting pattern
141 local output = mod.regex.parse(me.parserset, arg1, event)
142  
143 if (output.hit == nil) or (output.parser.identifier ~= "mobspellcast") then
144 return
145 end
146  
147 -- 2) Get Boss, Spell
148 local boss, spell = output.final[1], output.final[2]
149  
150 -- twin teleport
151 if spell == mod.string.get("boss", "spell", "twinteleport") then
152  
153 -- notify the raid, if this event isn't on cooldown
154 if GetTime() < me.bossevents.twinteleport.lastoccurence + me.bossevents.twinteleport.cooldown then
155 -- on cooldown. don't send
156 else
157 mod.net.sendevent("twinteleport")
158 end
159  
160 -- gate of shazzrah
161 elseif spell == mod.string.get("boss", "spell", "shazzrahgate") then
162  
163 -- notify the raid, if this event isn't on cooldown
164 if GetTime() < me.bossevents.shazzrahgate.lastoccurence + me.bossevents.shazzrahgate.cooldown then
165 -- on cooldown. don't send
166 else
167 mod.net.sendevent("shazzrahgate")
168 end
169 end
170  
171 elseif event == "CHAT_MSG_COMBAT_HOSTILE_DEATH" then
172  
173 -- 1) Scan for mob death
174 local output = mod.regex.parse(me.parserset, arg1, event)
175  
176 if (output.hit == nil) or (output.parser.identifier ~= "mobdeath") then
177 return
178 end
179  
180 local mobname = output.final[1]
181  
182 if (mobname == me.mastertarget) and (me.ismtworldboss == true) and (mod.net.lastmtsender == UnitName("player")) and mod.unit.isplayerofficer(UnitName("player")) then
183  
184 mod.net.clearmastertarget()
185 me.ismtworldboss = false
186 end
187  
188 else
189 me.parsebossattack(arg1, event)
190 end
191  
192 end
193  
194 --[[
195 me.onupdate is called by Core.lua. It currently has 3 different parts that are unrelated.
196 ]]
197 me.onupdate = function()
198  
199 local key, value, key2
200  
201 -- 1) if we are out of combat, reset the ticks on all boss abilities that have them
202 if UnitAffectingCombat("player") == nil then
203  
204 for key, value in me.tickcounters do
205 for key2 in value do
206 value[key2] = 0
207 end
208 end
209 end
210  
211 -- 2) clear out all old event reports - one report but no confirmations for 1.0 seconds.
212 local timenow = GetTime()
213  
214 for key, value in me.bossevents do
215 if (value.reporter ~= "") and (timenow > value.reporttime + 1.0) then
216  
217 -- debug
218 if mod.out.checktrace("warning", me, "event") then
219 mod.out.printtrace(string.format("The event %s has not been confirmed. It was reported by %s.", key, value.reporter))
220 end
221  
222 -- remove the report
223 value.reporter = ""
224 end
225 end
226  
227 -- 3) spellreporting: check for target changes
228 if (me.isspellreportingactive == true) and (me.istrackingspells == true) and me.mastertarget then
229  
230 -- 1) find mt
231 local x, newtarget
232  
233 for x = 1, 40 do
234 name = UnitName("raid" .. x .. "target")
235 if name == me.mastertarget then
236  
237 -- get target^2
238 newtarget = UnitName("raid" .. x .. "targettarget")
239  
240 if newtarget == nil then
241 newtarget = "<none>"
242 end
243  
244 break
245 end
246 end
247  
248 -- couldn't find the boss?
249 if newtarget == nil then
250 newtarget = "<unknown>"
251 end
252  
253 -- report!
254 if newtarget ~= me.bosstarget then
255  
256 -- find the threat of the old target
257 local oldthreat = mod.table.raiddata[me.bosstarget]
258 if oldthreat == nil then
259 oldthreat = "?"
260 end
261  
262 -- threat of the boss' new target
263 local newthreat = mod.table.raiddata[me.bosstarget]
264 if newthreat == nil then
265 newthreat = "?"
266 end
267  
268 -- print
269 mod.out.print(string.format(mod.string.get("print", "boss", "bosstargetchange"), me.mastertarget, me.bosstarget, oldthreat, newtarget, newthreat))
270  
271 -- update bosstarget
272 me.bosstarget = newtarget
273 end
274 end
275  
276 -- 4) Check triggers
277 me.checktriggers()
278  
279 end
280  
281 --[[
282 ------------------------------------------------------------------------------------------------
283 Boss Events - Sending and Receiving
284 ------------------------------------------------------------------------------------------------
285  
286 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.
287 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.
288 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.
289 Insertion: players in the raid report events in the network channel, and <me.reportevent(...)> is called from the <netin> module.
290 Maintenance: no OnUpdate maintenance necessary.
291 ]]
292  
293 me.bossevents = { }
294  
295 --[[
296 me.addevent(eventid, cooldown)
297 Defines a new event in <me.bossevents>. This is just a helper method to create <me.bossevents>. Called at file load.
298 <eventid> is a localisation key in the "boss"-"spell" set.
299 <cooldown> is the minimum time between casts, extreme lower bound. Want it large enough to avoid spams. 1 sec would probably do.
300 ]]
301 me.addevent = function(eventid, cooldown)
302  
303 me.bossevents[eventid] =
304 {
305 ["cooldown"] = cooldown,
306 lastoccurence = 0, -- GetTime()
307 reporter = "",
308 reporttime = 0, -- GetTime()
309 ["eventid"] = eventid,
310 }
311  
312 end
313  
314 -- define all possible events. These methods are called at file read time.
315 me.addevent("shazzrahgate", 5.0)
316 me.addevent("twinteleport", 5.0)
317 me.addevent("wrathofragnaros", 5.0)
318 me.addevent("nothblink", 5.0)
319  
320 --[[
321 mod.boss.reportevent(eventid, player)
322 Called when someone in the raid reports a boss event.
323 <eventid> is the internal name of the event.
324 <player> is the name of the player who reported it.
325 ]]
326 me.reportevent = function(eventid, player)
327  
328 local eventdata = me.bossevents[eventid]
329 local timenow = GetTime()
330  
331 -- ignore if the event is cooling down
332 if timenow < eventdata.lastoccurence + eventdata.cooldown then
333 return
334 end
335  
336 -- has this been reported recently? If so it is now confirmed and we can run it.
337 if (eventdata.reporter ~= "") and (eventdata.reporter ~= player) then
338 me.fireevent(eventdata, player)
339  
340 -- always trust reports from yourself
341 elseif player == UnitName("player") then
342 me.fireevent(eventdata, player)
343  
344 -- some player reports a new event. wait for confirmation
345 else
346 eventdata.reporter = player
347 eventdata.reporttime = timenow
348 end
349  
350 end
351  
352 --[[
353 me.fireevent(eventdata, player)
354 Run when an event is confirmed. Does whatever the event does.
355 <eventdata> is an structure in <me.bossevents>
356 <player> is the name of the player who confirmed the event
357 ]]
358 me.fireevent = function(eventdata, player)
359  
360 -- debug
361 if mod.out.checktrace("info", me, "event") then
362 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))
363 end
364  
365 -- first reset the event's timers
366 eventdata.lastoccurence = GetTime()
367 eventdata.reporter = ""
368  
369 -- now actually do the event
370 if eventdata.eventid == "shazzrahgate" then
371 mod.table.resetraidthreat()
372  
373 elseif eventdata.eventid == "twinteleport" then
374 mod.table.resetraidthreat()
375  
376 -- activate the proximity aggro detection trigger
377 me.starttrigger("twinemps")
378  
379 elseif eventdata.eventid == "wrathofragnaros" then
380 mod.table.resetraidthreat()
381  
382 elseif eventdata.eventid == "nothblink" then
383 mod.table.resetraidthreat()
384 end
385  
386 end
387  
388 --[[
389 ------------------------------------------------------------------------------------------------
390 Triggers - Hard To Detect Events That Require Polling
391 ------------------------------------------------------------------------------------------------
392  
393 Some events have no easily defined actions such as a combat log event, and must be checked for periodically instead.
394 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.
395 Each trigger has these properties:
396 <isactive> boolean, whether the mod is checking the trigger
397 <startdelay> time in seconds after the trigger is activated that the mod will start checking it.
398 <timeout> time in seconds after the trigger has started that the mod should give up on it
399 <mystarttime> when the most recent activation of the trigger occured.
400  
401 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.
402 To activate a trigger, call the <me.starttrigger(trigger)> function with the internal name of the trigger.
403 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.
404 ]]
405  
406 me.triggers =
407 {
408 twinemps =
409 {
410 isactive = false,
411 startdelay = 0.5,
412 timeout = 5.0,
413 mystarttime = 0,
414 data = 0,
415 },
416 autotarget =
417 {
418 isactive = false,
419 startdelay = 1.0,
420 timeout = 300.0,
421 mystarttime = 0,
422 data = 0,
423 }
424 }
425  
426 --[[
427 me.starttrigger(trigger)
428 Activates a trigger. The mod will start periodically checking for it.
429 <trigger> is the mod's internal identifier of the trigger, and matches a key to me.triggers.
430 ]]
431 me.starttrigger = function(trigger)
432  
433 -- debug check for trigger being defined. We should generalise this, since is happens in a flew different places in the mod. i.e. "badidentifierargument"
434 -- maybe also some kind of flood control to stop error messages spamming onupdate.
435  
436 if (trigger == nil) or (me.triggers[trigger] == nil) then
437  
438 -- report error
439 if mod.out.checktrace("error", me, "trigger") then
440 mod.out.printtrace(string.format("There is no trigger |cffffff00%s|r.", trigger or "<nil>"))
441 end
442  
443 return
444 end
445  
446 local triggerdata = me.triggers[trigger]
447 triggerdata.isactive = true
448 triggerdata.mystarttime = GetTime() + triggerdata.startdelay
449 triggerdata.data = 0
450  
451 -- debug
452 if mod.out.checktrace("info", me, "trigger") then
453 mod.out.printtrace(string.format("The |cffffff00%s|r trigger has been activated.", trigger))
454 end
455  
456 end
457  
458 --[[
459 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>.
460 The values are functions. The functions should return non-nil if the trigger is to be deactivated.
461 ]]
462 me.triggerfunctions =
463 {
464 --[[
465 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.
466 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.
467 However, if one of the emps is targetting us, we can instantly give ourself threat and exit.
468 The threat gained is set at 2000. This isn't confirmed, and is instead a bit of a guess.
469 ]]
470 twinemps = function(triggerdata)
471  
472 local x, name, firstbossname, unitid
473 local bosshits = 0
474 local bosstargets = 0
475  
476 -- loop through everyone in the raid
477 for x = 1, 40 do
478  
479 unitid = "raid" .. x .. "target"
480 if UnitExists(unitid) and (UnitClassification(unitid) == "worldboss") then
481  
482 -- we've found an emperor. check if we've seen him before
483 name = UnitName(unitid)
484  
485 if name ~= firstbossname then
486 bosshits = bosshits + 1
487  
488 -- if this is the first boss we've seen, put his name up
489 if bosshits == 1 then
490 firstbossname = name
491 end
492  
493 -- now find the player the boss is targetting
494 unitid = unitid .. "target"
495  
496 if UnitExists(unitid) then
497 bosstargets = bosstargets + 1
498  
499 if UnitIsUnit("player", unitid) then
500 -- an emp is targetting us. give us a bit of threat.
501  
502 mod.combat.event.hits = 1
503 mod.combat.event.threat = 2000
504 mod.combat.event.damage = 0
505 mod.combat.event.rage = 0
506  
507 mod.combat.addattacktodata(mod.string.get("threatsource", "threatwipe"), mod.combat.event)
508  
509 -- clear hits for total column
510 mod.combat.event.hits = 0
511 mod.combat.addattacktodata(mod.string.get("threatsource", "total"), mod.combat.event)
512  
513 -- 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
514 bosstargets = 2
515 bosshits = 2
516 end
517 end
518  
519 -- if we have found 2 bosses now, there's no need to do more searching
520 if bosshits == 2 then
521 break
522 end
523 end
524 end
525 end
526  
527 -- don't give up on the trigger until we have found both boss targets on one loop
528 if bosstargets == 2 then
529 return true
530 end
531 end,
532  
533 --[[
534 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.
535 ]]
536 autotarget = function(triggerdata)
537  
538 if UnitExists("target") and (UnitClassification("target") == "worldboss") then
539  
540 -- found a target. now only activate if we've been targetting him for a while
541 if triggerdata.data == 0 then
542 triggerdata.data = GetTime()
543 return
544  
545 else
546 -- 500 ms minimum.
547 if GetTime() < triggerdata.data + 0.5 then
548 return
549 end
550 end
551  
552 -- found a target. Activate
553 if me.mastertarget == UnitName("target") then
554  
555 -- someone has already set the master target to this mob. In this case don't do anything.
556 mod.out.print(string.format(mod.string.get("print", "boss", "autotargetabort"), UnitName("target")))
557  
558 else
559 mod.net.clearraidthreat()
560 mod.net.sendmastertarget()
561 end
562  
563 return true
564 end
565  
566 end
567 }
568  
569 --[[
570 me.checktriggers()
571 Loops through all possible triggers, checking for active ones and running them if need be. This is called in the OnUpdate() method.
572 ]]
573 me.checktriggers = function()
574  
575 local key, data
576 local timenow = GetTime()
577  
578 for key, data in me.triggers do
579  
580 -- ignore inactive triggers
581 if data.isactive == true then
582  
583 -- stop the trigger if it has timed out
584 if timenow > data.mystarttime + data.timeout then
585  
586 data.isactive = false
587  
588 -- debug
589 if mod.out.checktrace("warning", me, "trigger") then
590 mod.out.printtrace(string.format("The trigger |cffffff00%s|r timed out.", key))
591 end
592  
593 -- don't process the trigger if the start delay is not over
594 elseif timenow < data.mystarttime then
595 -- (do nothing)
596  
597 else
598 -- ok, run a trigger check
599 if me.triggerfunctions[key](data) then
600 data.isactive = false
601 end
602 end
603 end
604 end
605 end
606  
607 --[[
608 ------------------------------------------------------------------------------------------------
609 Parsing the Combat Log to Detect Boss Special Attacks and Spells
610 ------------------------------------------------------------------------------------------------
611 ]]
612  
613 -- me.parserset = { } -- defined in me.createparser
614  
615 --[[
616 me.createparser()
617 Called from me.onload() on startup. Creates the parser engine from the constructor.
618 ]]
619 me.createparser = function()
620  
621 me.parserset = { }
622  
623 local parserdata
624  
625 for _, parserdata in me.parserconstructor do
626 mod.regex.addparsestring(me.parserset, parserdata[1], parserdata[2], parserdata[3])
627 end
628  
629 end
630  
631 -- This describes all the combat log lines we are checking for
632 me.parserconstructor =
633 {
634 -- this is for school spells or debuffs
635 {"magicresist", "SPELLRESISTOTHERSELF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"}, -- "%s's %s was resisted."
636  
637 -- these two are for school spells only
638 {"spellhit", "SPELLLOGSCHOOLOTHERSELF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"}, -- "%s's %s hits you for %d %s damage."
639 {"spellhit", "SPELLLOGCRITSCHOOLOTHERSELF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"}, -- "%s's %s crits you for %d %s damage."
640  
641 -- spellboth is for abilities or school spells
642 {"attackabsorb", "SPELLLOGABSORBOTHERSELF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"}, -- "You absorb %s's %s."
643  
644 -- ability hit / miss only works for physical spells.
645 {"abilityhit", "SPELLLOGOTHERSELF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"}, -- "%s's %s hits you for %d."
646 {"abilityhit", "SPELLLOGCRITOTHERSELF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"}, -- "%s's %s hits you for %d."
647 {"abilityhit", "SPELLBLOCKEDOTHERSELF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"}, -- "%s's %s was blocked."
648 {"abilitymiss", "SPELLDODGEDOTHERSELF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"}, -- "%s's %s was dodged."
649 {"abilitymiss", "SPELLPARRIEDOTHERSELF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"}, -- "%s's %s was parried."
650 {"abilitymiss", "SPELLMISSOTHERSELF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"}, -- "%s's %s misses you."
651  
652 {"debuffstart", "AURAADDEDSELFHARMFUL", "CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE"}, -- "You are afflicated by %s."
653  
654 {"mobspellcast", "SPELLCASTGOOTHER", "CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE"}, -- "%s casts %s."
655 {"mobbuffgain", "AURAADDEDOTHERHELPFUL", "CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS"}, -- "%s gains %s."
656  
657 {"mobdeath", "UNITDIESOTHER", "CHAT_MSG_COMBAT_HOSTILE_DEATH"}, -- "%s dies."
658 }
659  
660 me.tickcounters = { }
661  
662 --[[
663 me.parsebossattack(message, event)
664  
665 Handles a combat log line that describes a boss's attack or spell against the player.
666 --> Stage one is to parse the message to find which formatting pattern the message matches, e.g. "magicresist" or
667 "spellhit" etc, or none (then just exit).
668 --> 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.
669 --> 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.
670 --> 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>.
671 --> 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.
672 --> If it triggers, we just change our threat by the right amount, then report the threat change in the <combat> and <table> modules.
673  
674 <message> is the combat log line.
675 <event> is the chat message event <message> was received on.
676 Returns: nothing.
677 ]]
678 me.parsebossattack = function(message, event)
679  
680 -- stage 1: regex
681 local output = mod.regex.parse(me.parserset, message, event)
682  
683 if output.hit == nil then
684 return
685 end
686  
687 -- interrupt: wrath of ragnaros
688 if output.final[2] == mod.string.get("boss", "spell", "wrathofragnaros") then
689  
690 -- notify the raid, if this event isn't on cooldown
691 if GetTime() < me.bossevents.wrathofragnaros.lastoccurence + me.bossevents.wrathofragnaros.cooldown then
692 -- on cooldown. don't send
693 else
694 mod.net.sendevent("wrathofragnaros")
695 end
696 end
697  
698 -- Set the mob and ability (always arg1 and arg2, except for debuffgain)
699 if output.parser.identifier == "debuffstart" then
700 me.resetaction("", output.final[1])
701 else
702 me.resetaction(output.final[1], output.final[2])
703 end
704  
705 -- set the spell and hit types
706 local description = me.attackdescription[output.parser.identifier]
707  
708 if description.ishit then me.action.ishit = true end
709 if description.isspell then me.action.isspell = true end
710 if description.isdebuff then me.action.isdebuff = true end
711 if description.isphysical then me.action.isphysical = true end
712  
713 -- Now, is the attack known?
714 local spellid = mod.string.unlocalise("boss", "spell", me.action.ability)
715 if (spellid == nil) or (me.bossattacks[spellid] == nil) then
716 return
717 end
718  
719 -- Check for a mob match
720 local spelldata
721  
722 local mobid = mod.string.unlocalise("boss", "name", me.action.mobname)
723  
724 if mobid and me.bossattacks[spellid][mobid] then
725 -- there is a specific version of this spell for this particular mob
726 spelldata = me.bossattacks[spellid][mobid]
727  
728 elseif me.bossattacks[spellid].default == nil then
729 -- this mob does not match any of the mobs that have the ability
730 return
731  
732 else
733 spelldata = me.bossattacks[spellid].default
734 end
735  
736 -- Now process the spell
737  
738 -- 1) Does the ability activate on a miss?
739 if (me.action.ishit == false) and (spelldata.effectonmiss == false) then
740  
741 -- ability will not activate
742 if mod.out.checktrace("info", me, "attack") then
743 mod.out.printtrace(string.format("%s's attack %s did not activate because it missed.", me.action.mobname, me.action.ability))
744 end
745  
746 -- spell reporting
747 if me.isspellreportingactive == true then
748 mod.net.reportspelleffect(me.action.ability, me.action.mobname, "miss")
749 end
750  
751 return
752 end
753  
754 -- 2) Check number of ticks
755 local mytickdata
756  
757 if spelldata.ticks ~= 1 then
758  
759 -- create a list if none exists yet
760 if me.tickcounters[me.action.ability] == nil then
761 me.tickcounters[me.action.ability] = { }
762 end
763  
764 if me.tickcounters[me.action.ability][me.action.mobname] == nil then
765 me.tickcounters[me.action.ability][me.action.mobname] = 0
766 end
767  
768 -- create an entry if none exists so far
769 me.tickcounters[me.action.ability][me.action.mobname] = me.tickcounters[me.action.ability][me.action.mobname] + 1
770  
771 -- now, have we gone enough ticks?
772 if me.tickcounters[me.action.ability][me.action.mobname] < spelldata.ticks then
773  
774 -- not enough ticks
775 if mod.out.checktrace("info", me, "attack") then
776 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]))
777 end
778  
779 -- spell reporting
780 local value1 = me.tickcounters[me.action.ability][me.action.mobname]
781 local value2 = spelldata.ticks - value1
782  
783 if me.isspellreportingactive then
784 mod.net.reportspelleffect(me.action.ability, me.action.mobname, "tick", value1, value2)
785 end
786  
787 return
788  
789 else
790 -- we just got enough ticks, so now reset to 0
791 me.tickcounters[me.action.ability][me.action.mobname] = 0
792  
793 end
794 end
795  
796 -- 3) To get here, the ability is definitely activating
797 if mod.out.checktrace("info", me, "attack") then
798 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))
799 end
800  
801 -- compute new threat
802 local newthreat = mod.table.getraidthreat() * spelldata.multiplier + spelldata.addition
803  
804 -- remember threat can't go below 0
805 newthreat = math.max(0, newthreat)
806  
807 -- threat change is the (possibly negative) amount of threat that was added
808 local threatchange = newthreat - mod.table.getraidthreat()
809  
810 -- spellreporting
811 if me.isspellreportingactive then
812 mod.net.reportspelleffect(me.action.ability, me.action.mobname, "proc", math.floor(0.5 + mod.table.getraidthreat()), math.floor(0.5 + newthreat))
813 end
814  
815 -- add to threat wipes section, but not to totals
816 mod.combat.event.hits = 1
817 mod.combat.event.damage = 0
818 mod.combat.event.rage = 0
819 mod.combat.event.threat = threatchange
820  
821 mod.combat.addattacktodata(mod.string.get("threatsource", "threatwipe"), mod.combat.event)
822  
823 -- now add it to your raid threat total (but not your personal threat total)
824 mod.table.raidthreatoffset = mod.table.raidthreatoffset + threatchange
825  
826 -- ask for a redraw of the personal window
827 KLHTM_RequestRedraw("self")
828  
829 end
830  
831 --[[
832 me.resetaction()
833 Sets the values of me.action to their defaults.
834 ]]
835 me.resetaction = function(mobname, ability)
836  
837 me.action.mobname = mobname
838 me.action.ability = ability
839 me.action.ishit = false
840 me.action.isphysical = false
841 me.action.isdebuff = false
842 me.action.isspell = false
843  
844 end
845  
846 me.action =
847 {
848 mobname = "",
849 ability = "",
850 ishit = false,
851 isphysical = false,
852 isdebuff = false,
853 isspell = false,
854 }
855  
856 -- Note that <ishit> defaults to false, so we only set it when it is true
857 me.attackdescription =
858 {
859 ["magicresist"] =
860 {
861 isspell = true,
862 isdebuff = true,
863 },
864 ["spellhit"] =
865 {
866 isspell = true,
867 ishit = true,
868 },
869 ["attackabsorb"] =
870 {
871 isspell = true,
872 isphysical = true,
873 ishit = true,
874 },
875 ["abilityhit"] =
876 {
877 isphysical = true,
878 ishit = true,
879 },
880 ["abilitymiss"] =
881 {
882 isphysical = true,
883 },
884 ["debuffstart"] =
885 {
886 ishit = true,
887 isdebuff = true,
888 },
889 }
890  
891 --[[
892 Here is where you define all the boss' attacks that affect threat.
893 The first key in me.bossattacks is the identifier of the spell. That is, mod.string.get("boss", "spell", <first key>)
894 is the localised version.
895 The second key deep specifies which mob the attack comes from. You can choose "default" to make it apply to all mobs,
896 or you can specify a mob id, which will override the "default" value. Mob id's recognised are all the keys in the
897 "boss" -> "name" section of the localisation tree.
898 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
899 "boss" -> "spell" and "boss" -> "name" sections respectively.
900 Inside each block, the follow parameters are defined:
901 <multiplier> - a value that your threat is multiplier by. e.g. the standard Knock Away is -50% threat, so this would be a
902 multiplier of 0.5. A complete threat wipe would be a multiplier of 0.
903 <addition> - a flat value that is added to your threat. Can be positive or negative or 0.
904 <effectonmiss> - a boolean value specifying whether the event triggers even when it is resisted or misses you.
905 <ticks> - the number of times you must suffer the attack before your threat is changed. e.g. most knockbacks happen every
906 time so <ticks> = 1, but Time Lapse reduces your threat only after a certain number of applications.
907 <type> - describes the attack. Can be "physical" or "spell" or "debuff". Not used by the mod at the moment: it will
908 assume that if the name matches, it has found the right ability.
909 ]]
910 me.bossattacks =
911 {
912 knockaway =
913 {
914 default =
915 {
916 multiplier = 0.5,
917 addition = 0,
918 effectonmiss = false,
919 ticks = 1,
920 type = "physical",
921 },
922 onyxia =
923 {
924 multiplier = 0.67,
925 addition = 0,
926 effectonmiss = false,
927 ticks = 1,
928 type = "physical",
929 },
930 },
931 wingbuffet =
932 {
933 default =
934 {
935 multiplier = 0.5,
936 addition = 0,
937 effectonmiss = false,
938 ticks = 1,
939 type = "physical",
940 },
941 onyxia =
942 {
943 multiplier = 1.0,
944 addition = 0,
945 effectonmiss = false,
946 ticks = 1,
947 type = "physical",
948 },
949 },
950 timelapse =
951 {
952 default =
953 {
954 multiplier = 0,
955 addition = 0,
956 effectonmiss = false,
957 ticks = 5,
958 type = "debuff"
959 }
960 },
961 sandblast =
962 {
963 default =
964 {
965 multiplier = 0,
966 addition = 0,
967 effectonmiss = false,
968 ticks = 1,
969 type = "spell"
970 }
971 },
972 }
973  
974 --[[
975 ------------------------------------------------------------------------------------------------
976 + B + Normal Shit
977 ------------------------------------------------------------------------------------------------
978 ]]
979  
980 --[[
981 me.automastertarget(target)
982 Called when the mod itself sets the mastertarget.
983 <target> is the localised name of the mob.
984 ]]
985 me.automastertarget = function(target)
986  
987 me.mastertarget = target
988  
989 -- stop network module autoupdating the master target
990 mod.net.lastmtsender = ""
991  
992 -- explain to user
993 mod.out.print(string.format(mod.string.get("print", "boss", "automt"), target))
994  
995 end
996  
997 --[[
998 mod.boss.newmastertarget(author, target)
999 Handles a request to change the master target.
1000 <author> is the name of the officer in the group who changed the master target.
1001 <target> is the name of his current mob, localised to him.
1002 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.
1003 ]]
1004 me.newmastertarget = function(author, target)
1005  
1006 -- 1) Find the author's UnitID
1007 local officerunit = mod.unit.findunitidfromname(author)
1008 local officertarget = UnitName(tostring(officerunit) .. "target")
1009  
1010 -- 2) Check for differences
1011 if officertarget == nil then
1012 mod.out.print(string.format(mod.string.get("print", "network", "newmttargetnil"), target, author))
1013  
1014 elseif officertarget ~= target then
1015 mod.out.print(string.format(mod.string.get("print", "network", "newmttargetmismatch"), author, target, officertarget))
1016 target = officertarget
1017 end
1018  
1019 -- 3) Check for worldboss target
1020 if author == UnitName("player") and UnitClassification("target") == "worldboss" then
1021 me.ismtworldboss = true
1022 else
1023 me.ismtworldboss = false
1024 end
1025  
1026 -- 3) OK
1027 me.mastertarget = target
1028 mod.out.print(string.format(mod.string.get("print", "network", "newmt"), target, author))
1029  
1030 end
1031  
1032 -- todo: stuff, maybe?
1033 me.clearmastertarget = function()
1034  
1035 me.mastertarget = nil
1036  
1037 end
1038  
1039 --[[
1040 mod.boss.targetismaster(target)
1041 Checks whether <target> is the master target. The master target is usually just a name / string, but it may be something
1042 more general in the future (e.g. tracking both bosses in the Twin Emps fight).
1043 <target> is the name of the mob being queried.
1044 Return: non-nil if <target> is a mastertarget.
1045 ]]
1046 me.targetismaster = function(target)
1047  
1048 if me.mastertarget == nil then
1049 return true
1050 end
1051  
1052 if target == me.mastertarget then
1053 return true
1054 end
1055  
1056 -- insert other checks here, later.
1057  
1058 return -- (nil)
1059  
1060 end
1061  
1062 -----------------------------------
1063 -- Targeting Behaviour --
1064 -----------------------------------
1065 --[[
1066  
1067 True = True target. Who the mob would have aggro on, if we discount secondary targetting and taunts, etc.
1068 Curr = Current target. Who the mob's target unitid is
1069 New = New target. If the mob's current target has changed
1070  
1071 x, y = players with known threat values
1072 nil = no target
1073 ? = player with unknown threat value
1074  
1075  
1076 True Curr New Result
1077 ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
1078 nil nil x true and curr become x
1079  
1080 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.
1081 x x ? curr -> ?. In 2 seconds with no change, true -> ?
1082 x x nil curr -> nil.
1083  
1084 x y z curr -> z. In 2 secs, true -> z
1085 x y nil curr -> nil.
1086 x y ? curr -> ?. In 2 secs, true -> ?
1087 x y x curr -> x
1088  
1089 x nil y If it's been nil for more than 1 second, true = y. Otherwise true -> y after 2 - (secs at nil) secs.
1090 x nil x curr -> x
1091 x nil ? same as x - nil - y
1092  
1093 ? ? x true -> x. Easy enough.
1094  
1095 ]]
1096  
1097 -- Master Target Variables
1098 me.mttruetarget = nil -- The Name of the player who this mod thinks is the true target
1099 me.mtcurrenttarget = nil -- The Name of the player the mob is currently targetting
1100 me.mttargetswaptime = 0 -- The time when the mob last changed its target
1101 me.unknowntarget = "#unknown"
1102 me.mastertargettarget = nil
1103  
1104 -- lm2: aggro gain is now calculated just before redrawing the raid frame
1105 me.updateaggrogain = function()
1106  
1107 if mod.boss.mastertarget == nil then
1108 me.recalculateaggrogain()
1109 else
1110 me.updatetrueaggrotarget()
1111 end
1112  
1113 end
1114  
1115  
1116 me.updatetrueaggrotarget = function()
1117  
1118 -- 1) find a UnitID for the master target
1119 local mastertargetid = nil
1120  
1121 if UnitName("target") == me.mastertarget then
1122 mastertargetid = "target"
1123  
1124 else
1125 -- check everyone in the raid
1126 local x
1127  
1128 for x = 1, 40 do
1129 if UnitName("raid" .. x .. "target") == me.mastertarget then
1130 mastertargetid = "raid" .. x .. "target"
1131 break
1132 end
1133 end
1134 end
1135  
1136 -- 2) If noone can see the mob, give up.
1137 if mastertargetid == nil then
1138  
1139 me.mttruetarget = me.unknowntarget
1140 me.mtcurrenttarget = me.unknownuarget
1141 mod.table.raiddata[mod.string.get("misc", "aggrogain")] = nil
1142  
1143 return
1144 end
1145  
1146 -- 3) Get the boss' current target
1147 local targetnow = UnitName(mastertargetid .. "target")
1148  
1149 -- 4) Reevaluate True, Current, Time
1150  
1151 -- a) Transitions from true=unknown
1152 if (me.mttruetarget == nil) or (me.mttruetarget == me.unknowntarget) or (mod.table.raiddata[me.mttruetarget] == nil) then
1153  
1154 -- debug print
1155 if targetnow ~= me.mttruetarget then
1156 if targetnow == nil then
1157 if mod.out.checktrace("info", me, "target") then
1158 mod.out.printtrace("Target changed from bad to nil.")
1159 end
1160 else
1161 if mod.out.checktrace("info", me, "target") then
1162 mod.out.printtrace(string.format("Target changed from bad to %s.", targetnow))
1163 end
1164  
1165 if (me.isknockbackdiscoveryactive == true) and (targetnow == UnitName("player")) then
1166 mod.net.sendmessage("aggrogain " .. mod.table.getraidthreat())
1167 end
1168 end
1169 end
1170  
1171 me.mttruetarget = targetnow
1172 me.mtcurrenttarget = targetnow
1173  
1174 -- b) Transitions from true = known
1175 elseif targetnow ~= me.mtcurrenttarget then
1176  
1177 if me.mtcurrenttarget ~= nil then
1178 me.mttargetswaptime = GetTime()
1179 end
1180  
1181 me.mtcurrenttarget = targetnow
1182  
1183 if targetnow == nil then
1184 if mod.out.checktrace("info", me, "target") then
1185 mod.out.printtrace("CurrentTarget changed to nil.")
1186 end
1187 else
1188 if mod.out.checktrace("info", me, "target") then
1189 mod.out.printtrace(string.format("CurrentTarget changed from bad to %s.", targetnow))
1190 end
1191  
1192 if (me.isknockbackdiscoveryactive == true) and (targetnow == UnitName("player")) then
1193 mod.net.sendmessage("aggrogain " .. mod.table.getraidthreat())
1194 end
1195 end
1196 end
1197  
1198 -- 5) Check if CurrentTarget should become Truetarget
1199 if me.mttruetarget ~= me.mtcurrenttarget then
1200 -- to get here, true target is known.
1201  
1202 if me.mtcurrenttarget == nil then
1203 -- do nothing
1204  
1205 elseif mod.table.raiddata[me.mtcurrenttarget] == nil then
1206 -- switch to unknown if it's been more than 2 seconds
1207  
1208 if GetTime() > me.mttargetswaptime + 2 then
1209 me.mttruetarget = me.mtcurrenttarget
1210  
1211 if mod.out.checktrace("info", me, "target") then
1212 mod.out.printtrace(string.format("TrueTarget switches to the unknown %s after 2 seconds.", me.mttruetarget))
1213 end
1214 end
1215  
1216 else -- current target is a known user
1217 if GetTime() - me.mttargetswaptime > 2 then
1218 me.mttruetarget = me.mtcurrenttarget
1219  
1220 if mod.out.checktrace("info", me, "target") then
1221 mod.out.printtrace(string.format("TrueTarget switches to the known player %s after 2 seconds.", me.mttruetarget))
1222 end
1223  
1224 elseif mod.table.raiddata[me.mtcurrenttarget] > mod.data.threatconstants.meleeaggrogain * mod.table.raiddata[me.mttruetarget] then
1225 me.mttruetarget = me.mtcurrenttarget
1226  
1227 if mod.out.checktrace("info", me, "target") then
1228 mod.out.printtrace(string.format("TrueTarget switches to the known player %s due to high threat.", me.mttruetarget))
1229 end
1230 end
1231 end
1232 end
1233  
1234 -- update the AggroGain virtual player
1235 if ((me.mttruetarget ~= nil) and (me.truetarget ~= me.unknowntarget) and
1236 (mod.table.raiddata[me.mttruetarget] ~= nil)) then
1237  
1238 local aggro = mod.table.raiddata[me.mttruetarget];
1239  
1240 if (UnitName("player") ~= me.mttruetarget) then
1241 if CheckInteractDistance(mastertargetid, 1) then
1242 aggro = math.ceil(aggro * mod.data.threatconstants.meleeaggrogain)
1243 else
1244 aggro = math.ceil(aggro * mod.data.threatconstants.rangeaggrogain)
1245 end
1246 end
1247  
1248 mod.table.raiddata[mod.string.get("misc", "aggrogain")] = aggro;
1249 else
1250 mod.table.raiddata[mod.string.get("misc", "aggrogain")] = nil
1251 end
1252 end
1253  
1254 me.recalculateaggrogain = function()
1255  
1256 -- update aggro, and such
1257 local newaggrogain
1258 local targetname = ""
1259 local maxdepth = 5
1260 local i
1261 local targetacquired = false
1262  
1263 for i = 1, maxdepth do
1264 targetname = targetname .. "target"
1265  
1266 if UnitName(targetname) == nil then
1267 break
1268  
1269 elseif UnitIsFriend("player", targetname) == nil then
1270 targetacquired = true
1271 break
1272 end
1273 end
1274  
1275 if targetacquired == false then
1276 -- remove aggro gain
1277 newaggrogain = nil
1278  
1279 else
1280 local mobtarget = UnitName(targetname .. "target")
1281 if mobtarget == nil then
1282 mobtarget = "<nil>"
1283 end
1284  
1285 if mod.table.raiddata[mobtarget] then
1286 -- aggro target has a known threat value
1287  
1288 if UnitName("player") == mobtarget then
1289 newaggrogain = mod.table.raiddata[mobtarget]
1290  
1291 else
1292 -- now check our range to the mob
1293 if CheckInteractDistance(targetname, 1) then
1294 -- we're in melee range
1295 newaggrogain = math.ceil(mod.table.raiddata[mobtarget] * mod.data.threatconstants.meleeaggrogain)
1296  
1297 else
1298 -- there's a small region where we might be in melee range. for now, assume not
1299 newaggrogain = math.ceil(mod.table.raiddata[mobtarget] * mod.data.threatconstants.rangeaggrogain)
1300 end
1301 end
1302 else
1303 newaggrogain = nil
1304 end
1305 end
1306  
1307 local currentaggrogain = mod.table.raiddata[mod.string.get("misc", "aggrogain")]
1308 if newaggrogain ~= currentaggrogain then
1309 mod.table.raiddata[mod.string.get("misc", "aggrogain")] = newaggrogain
1310 end
1311  
1312 end