· 5 years ago · Nov 25, 2020, 05:26 PM
1--------------------------------------------------------------------------------
2-- CONFIGURATION
3--
4-- The configuration options have moved to the "Author Options" tab as of
5-- WeakAuras Version 2.10.
6--
7-- DO NOT EDIT THIS CODE!
8--------------------------------------------------------------------------------
9local ZT = aura_env
10
11-- Local versions of commonly used functions
12local ipairs = ipairs
13local pairs = pairs
14local print = print
15local select = select
16local tonumber = tonumber
17local tinsert = tinsert
18
19local IsInGroup = IsInGroup
20local IsInRaid = IsInRaid
21local UnitGUID = UnitGUID
22
23-- Turns on/off debugging messages
24local DEBUG_EVENT = { isEnabled = false, color = "FF2281F4" }
25local DEBUG_MESSAGE = { isEnabled = false, color = "FF11D825" }
26local DEBUG_TIMER = { isEnabled = false, color = "FFF96D27" }
27local DEBUG_TRACKING = { isEnabled = false, color = "FFA53BF7" }
28
29-- Turns on/off testing of combatlog-based tracking for the player
30-- (Note: This will disable sharing of player CD updates over addon messages)
31local TEST_CLEU = false
32
33local function prdebug(type, ...)
34 if type.isEnabled then
35 print("|c"..type.color.."[ZT-Debug]", ...)
36 end
37end
38
39local function prerror(...)
40 print("|cFFFF0000[ZT-Error]", ...)
41end
42
43-- Utility functions for creating tables/maps
44local function DefaultTable_Create(genDefaultFunc)
45 local metatable = {}
46 metatable.__index = function(table, key)
47 local value = genDefaultFunc()
48 rawset(table, key, value)
49 return value
50 end
51
52 return setmetatable({}, metatable)
53end
54
55local function Map_FromTable(table)
56 local map = {}
57 for _,value in ipairs(table) do
58 map[value] = true
59 end
60 return map
61end
62
63--##############################################################################
64-- Class and Spec Information
65
66local DH = {ID=12, name="DEMONHUNTER", Havoc=577, Veng=581}
67local DK = {ID=6, name="DEATHKNIGHT", Blood=250, Frost=251, Unholy=252}
68local Druid = {ID=11, name="DRUID", Balance=102, Feral=103, Guardian=104, Resto=105}
69local Hunter = {ID=3, name="HUNTER", BM=253, MM=254, SV=255}
70local Mage = {ID=8, name="MAGE", Arcane=62, Fire=63, Frost=64}
71local Monk = {ID=10, name="MONK", BRM=268, WW=269, MW=270}
72local Paladin = {ID=2, name="PALADIN", Holy=65, Prot=66, Ret=70}
73local Priest = {ID=5, name="PRIEST", Disc=256, Holy=257, Shadow=258}
74local Rogue = {ID=4, name="ROGUE", Sin=259, Outlaw=260, Sub=261}
75local Shaman = {ID=7, name="SHAMAN", Ele=262, Enh=263, Resto=264}
76local Warlock = {ID=9, name="WARLOCK", Affl=265, Demo=266, Destro=267}
77local Warrior = {ID=1, name="WARRIOR", Arms=71, Fury=72, Prot=73}
78
79local AllClasses = {
80 [DH.name] = DH, [DK.name] = DK, [Druid.name] = Druid, [Hunter.name] = Hunter,
81 [Mage.name] = Mage, [Monk.name] = Monk, [Paladin.name] = Paladin,
82 [Priest.name] = Priest, [Rogue.name] = Rogue, [Shaman.name] = Shaman,
83 [Warlock.name] = Warlock, [Warrior.name] = Warrior
84}
85
86local AllCovenants = {
87 ["Kyrian"] = 1,
88 ["Venthyr"] = 2,
89 ["NightFae"] = 3,
90 ["Necrolord"] = 4,
91}
92
93--##############################################################################
94-- Spell Requirements
95
96local function Requirement(type, check, indices)
97 return { type = type, check = check, indices = indices }
98end
99
100local function LevelReq(minLevel)
101 return Requirement("level", function(member)
102 if type(member.level) == "string" then
103 prerror("!!!", member.level)
104 end
105 return member.level >= minLevel end, {minLevel})
106end
107
108local function RaceReq(race)
109 return Requirement("race", function(member) return member.race == race end, {race})
110end
111
112local function ClassReq(class)
113 return Requirement("class", function(member) return member.classID == class.ID end, {class.ID})
114end
115
116local function SpecReq(ids)
117 local idsMap = Map_FromTable(ids)
118 return Requirement("spec", function(member) return idsMap[member.specID] ~= nil end, ids)
119end
120
121local function TalentReq(id)
122 return Requirement("talent", function(member) return member.talents[id] ~= nil end, {id})
123end
124
125local function NoTalentReq(id)
126 return Requirement("notalent", function(member) return member.talents[id] == nil end, {id})
127end
128
129-- local function ItemReq(id)
130-- return Requirement("items", function(member) return false end)
131-- end
132
133local function CovenantReq(name)
134 local covenantID = AllCovenants[name]
135 return Requirement("covenant", function(member) return covenantID == member.covenantID end, {covenantID})
136end
137
138--##############################################################################
139-- Spell Modifiers (Static and Dynamic)
140
141local function StaticMod(func)
142 return { type = "Static", func = func }
143end
144
145local function SubtractMod(amount)
146 return StaticMod(function(watchInfo) watchInfo.duration = watchInfo.duration - amount end)
147end
148
149local function MultiplyMod(coeff)
150 return StaticMod(function(watchInfo) watchInfo.duration = watchInfo.duration * coeff end)
151end
152
153local function ChargesMod(amount)
154 return StaticMod(function(watchInfo)
155 watchInfo.charges = amount
156 watchInfo.maxCharges = amount
157 end)
158end
159
160
161local function DynamicMod(handlers)
162 if handlers.type then
163 handlers = { handlers }
164 end
165
166 return { type = "Dynamic", handlers = handlers }
167end
168
169local function EventDeltaMod(type, spellID, delta)
170 return DynamicMod({
171 type = type,
172 spellID = spellID,
173 handler = function(watchInfo)
174 watchInfo:updateCDDelta(delta)
175 end
176 })
177end
178
179local function CastDeltaMod(spellID, delta)
180 return EventDeltaMod("SPELL_CAST_SUCCESS", spellID, delta)
181end
182
183local function EventRemainingMod(type, spellID, remaining)
184 return DynamicMod({
185 type = type,
186 spellID = spellID,
187 handler = function(watchInfo)
188 watchInfo:updateCDRemaining(remaining)
189 end
190 })
191end
192
193local function CastRemainingMod(spellID, remaining)
194 return EventRemainingMod("SPELL_CAST_SUCCESS", spellID, remaining)
195end
196
197-- If Shockwave 3+ targets hit then reduces cooldown by 15 seconds
198local RumblingEarthMod = DynamicMod({
199 {
200 type = "SPELL_CAST_SUCCESS", spellID = 46968,
201 handler = function(watchInfo)
202 watchInfo.numHits = 0
203 end
204 },
205 {
206 type = "SPELL_AURA_APPLIED", spellID = 132168,
207 handler = function(watchInfo)
208 watchInfo.numHits = watchInfo.numHits + 1
209 if watchInfo.numHits == 3 then
210 watchInfo:updateCDDelta(-15)
211 end
212 end
213 }
214})
215
216-- Each target hit by Capacitor Totem reduces cooldown by 5 seconds (up to 4 targets hit)
217local function StaticChargeAuraHandler(watchInfo)
218 watchInfo.numHits = watchInfo.numHits + 1
219 if watchInfo.numHits <= 4 then
220 watchInfo:updateCDDelta(-5)
221 end
222end
223
224local StaticChargeMod = DynamicMod({
225 type = "SPELL_SUMMON", spellID = 192058,
226 handler = function(watchInfo)
227 watchInfo.numHits = 0
228
229 if watchInfo.totemGUID then
230 ZT.eventHandlers:remove("SPELL_AURA_APPLIED", 118905, watchInfo.totemGUID, StaticChargeAuraHandler)
231 end
232
233 watchInfo.totemGUID = select(8, CombatLogGetCurrentEventInfo())
234 ZT.eventHandlers:add("SPELL_AURA_APPLIED", 118905, watchInfo.totemGUID, StaticChargeAuraHandler, watchInfo)
235 end
236})
237
238-- Guardian Spirit: If expires watchInfothout healing then reset to 60 seconds
239local GuardianAngelMod = DynamicMod({
240 {
241 type = "SPELL_HEAL", spellID = 48153,
242 handler = function(watchInfo)
243 watchInfo.spiritHeal = true
244 end
245 },
246 {
247 type = "SPELL_AURA_REMOVED", spellID = 47788,
248 handler = function(watchInfo)
249 if not watchInfo.spiritHeal then
250 watchInfo:updateCDRemaining(60)
251 end
252 watchInfo.spiritHeal = false
253 end
254 }
255})
256
257-- Dispels: Go on cooldown only if a debuff is dispelled
258local function DispelMod(spellID)
259 return DynamicMod({
260 type = "SPELL_DISPEL",
261 spellID = spellID,
262 handler = function(watchInfo)
263 watchInfo:updateCDRemaining(8)
264 end
265 })
266end
267
268-- Resource Spending: For every spender, reduce cooldown by (coefficient * cost) seconds
269-- Note: By default, I try to use minimum cost values as to not over-estimate the cooldown reduction
270local specIDToSpenderInfo = {
271 [DK.Blood] = {
272 [47541] = 40, -- Death Coil
273 [49998] = 40, -- Death Strike (Assumes -5 due to Ossuary)
274 [61999] = 30, -- Raise Ally
275 [327574] = 20, -- Sacrificial Pact
276 },
277 [Warrior.Arms] = {
278 [845] = 20, -- Cleave
279 [163201] = 20, -- Execute (Ignores Sudden Death)
280 [1715] = 10, -- Hamstring
281 [202168] = 10, -- Impending Victory
282 [12294] = 30, -- Moral Strike
283 [772] = 30, -- Rend
284 [1464] = 20, -- Slam
285 [1680] = 30, -- Whirlwind
286 [190456] = 40, -- Ignore Pain
287 },
288 [Warrior.Fury] = {
289 [202168] = 10, -- Impending Victory
290 [184367] = 75, -- Rampage (Assumes -10 from Carnage)
291 [12323] = 10, -- Piercing Howl
292 [190456] = 40, -- Ignore Pain
293 },
294 [Warrior.Prot] = {
295 [190456] = 40, -- Ignore Pain (Ignores Vengeance)
296 [202168] = 10, -- Impending Victory
297 [6572] = 30, -- Revenge (Ignores Vengeance)
298 [2565] = 30, -- Shield Block
299 },
300 [Hunter.BM] = {
301 [185358] = 40, -- Arcane Shot
302 [195645] = 30, -- Wing Clip
303 [982] = 35, -- Revive Pet
304 [34026] = 30, -- Kill Command
305 [193455] = 35, -- Cobra Shot
306 [2643] = 40, -- Multi-Shot
307 [1513] = 25, -- Scare Beast
308 [53351] = 10, -- Kill Shot
309 [131894] = 30, -- A Murder of Crows
310 [120360] = 60, -- Barrage
311 },
312 [Hunter.MM] = {
313 [185358] = 20, -- Arcane Shot
314 [195645] = 30, -- Wing Clip
315 [982] = 35, -- Revive Pet
316 [19434] = 35, -- Aimed Shot
317 [186387] = 10, -- Bursting Shot
318 [257620] = 20, -- Multi-Shot
319 [53351] = 10, -- Kill Shot
320 [271788] = 60, -- Serpent Sting
321 [131894] = 30, -- A Murder of Crows
322 [120360] = 60, -- Barrage
323 [212431] = 20, -- Explosive Shot
324 [342049] = 20, -- Chimaera Shot
325 },
326 [Hunter.SV] = {
327 [185358] = 40, -- Arcane Shot
328 [195645] = 30, -- Wing Clip
329 [982] = 35, -- Revive Pet
330 [186270] = 30, -- Raptor Strike
331 [259491] = 20, -- Serpent Sting
332 [187708] = 35, -- Carve
333 [320976] = 10, -- Kill Shot
334 [212436] = 30, -- Butchery
335 [259387] = 30, -- Mongoose Bite
336 [259391] = 15, -- Chakrams
337 },
338 [Paladin] = {
339 [85673] = 3, -- Word of Glory
340 [85222] = 3, -- Light of Dawn
341 [152262] = 3, -- Seraphim
342 [53600] = 3, -- Shield of the Righteous
343 [85256] = 3, -- Templar's Verdict
344 [53385] = 3, -- Divine Storm
345 [343527] = 3, -- Execution Sentence
346 },
347 [Paladin.Holy] = {
348 [85673] = 3, -- Word of Glory
349 [85222] = 3, -- Light of Dawn
350 [152262] = 3, -- Seraphim
351 },
352 [Paladin.Prot] = {
353 [85673] = 3, -- Word of Glory
354 [53600] = 3, -- Shield of the Righteous
355 [152262] = 3, -- Seraphim
356 },
357 [Paladin.Ret] = {
358 [85673] = 3, -- Word of Glory
359 [85256] = 3, -- Templar's Verdict
360 [53385] = 3, -- Divine Storm
361 [343527] = 3, -- Execution Sentence
362 [152262] = 3, -- Seraphim
363 },
364}
365
366local function ResourceSpendingMods(specID, coefficient)
367 local handlers = {}
368 local spenderInfo = specIDToSpenderInfo[specID]
369
370 for spellID,cost in pairs(spenderInfo) do
371 local delta = -(coefficient * cost)
372
373 handlers[#handlers+1] = {
374 type = "SPELL_CAST_SUCCESS",
375 spellID = spellID,
376 handler = function(watchInfo)
377 watchInfo:updateCDDelta(delta)
378 end
379 }
380 end
381
382 return DynamicMod(handlers)
383end
384
385-- Duration Modifier (For active buff durations)
386local function DurationMod(spellID, refreshes)
387 local handlers = {}
388 handlers[1] = {
389 type = "SPELL_AURA_REMOVED",
390 force = true,
391 spellID = spellID,
392 handler = function(watchInfo)
393 watchInfo.activeExpiration = GetTime()
394 ZT:sendCDUpdate(watchInfo, true)
395 watchInfo:sendTriggerEvent()
396 end
397 }
398
399 if refreshes then
400 for r in pairs(refreshes) do
401 handlers[#handlers+1] = {
402 type = "SPELL_CAST_SUCCESS",
403 spellID = r,
404 handler = function(watchInfo)
405 end
406 }
407 end
408 end
409
410 return DynamicMod(handlers)
411end
412
413local function ActiveMod(spellID, duration, refreshes)
414 return { spellID = spellID, duration = duration , refreshes = refreshes}
415end
416
417--##############################################################################
418-- List of Tracked Spells
419-- TODO: Denote which spells should be modified by UnitSpellHaste(...)
420
421ZT.spellListVersion = 103
422ZT.spellList = {
423 -- Racials
424 {type="HARDCC", id=255654, cd=120, reqs={RaceReq("HighmountainTauren")}}, -- Bull Rush
425 {type="HARDCC", id=20549, cd=90, reqs={RaceReq("Tauren")}}, -- War Stomp
426 {type="STHARDCC", id=287712, cd=150, reqs={RaceReq("KulTiran")}}, -- Haymaker
427 {type="STSOFTCC", id=107079, cd=120, reqs={RaceReq("Pandaren")}}, -- Quaking Palm
428 {type="DISPEL", id=202719, cd=120, reqs={RaceReq("BloodElf"), ClassReq(DH)}}, -- Arcane Torrent
429 {type="DISPEL", id=50613, cd=120, reqs={RaceReq("BloodElf"), ClassReq(DK)}}, -- Arcane Torrent
430 {type="DISPEL", id=80483, cd=120, reqs={RaceReq("BloodElf"), ClassReq(Hunter)}}, -- Arcane Torrent
431 {type="DISPEL", id=28730, cd=120, reqs={RaceReq("BloodElf"), ClassReq(Mage)}}, -- Arcane Torrent
432 {type="DISPEL", id=129597, cd=120, reqs={RaceReq("BloodElf"), ClassReq(Monk)}}, -- Arcane Torrent
433 {type="DISPEL", id=155145, cd=120, reqs={RaceReq("BloodElf"), ClassReq(Paladin)}}, -- Arcane Torrent
434 {type="DISPEL", id=232633, cd=120, reqs={RaceReq("BloodElf"), ClassReq(Priest)}}, -- Arcane Torrent
435 {type="DISPEL", id=25046, cd=120, reqs={RaceReq("BloodElf"), ClassReq(Rogue)}}, -- Arcane Torrent
436 {type="DISPEL", id=28730, cd=120, reqs={RaceReq("BloodElf"), ClassReq(Warlock)}}, -- Arcane Torrent
437 {type="DISPEL", id=69179, cd=120, reqs={RaceReq("BloodElf"), ClassReq(Warrior)}}, -- Arcane Torrent
438 {type="DISPEL", id=20594, cd=120, reqs={RaceReq("Dwarf")}, mods={{mod=EventRemainingMod("SPELL_AURA_APPLIED",65116,120)}}}, -- Stoneform
439 {type="DISPEL", id=265221, cd=120, reqs={RaceReq("DarkIronDwarf")}, mods={{mod=EventRemainingMod("SPELL_AURA_APPLIED",265226,120)}}}, -- Fireblood
440 {type="UTILITY", id=58984, cd=120, reqs={RaceReq("NightElf")}}, -- Shadowmeld
441
442 -- Covenants
443 {type="COVENANT", id=324739, cd=300, reqs={CovenantReq("Kyrian")}, version=101},-- Summon Steward
444 {type="COVENANT", id=323436, cd=180, reqs={CovenantReq("Kyrian")}, version=103},-- Purify Soul
445 {type="COVENANT", id=300728, cd=60, reqs={CovenantReq("Venthyr")}, version=101},-- Door of Shadows
446 {type="COVENANT", id=310143, cd=90, reqs={CovenantReq("NightFae")}, version=101},-- Soulshape
447 {type="COVENANT", id=324631, cd=90, reqs={CovenantReq("Necrolord")}, version=101},-- Fleshcraft
448
449 -- DH
450 ---- Base
451 {type="INTERRUPT", id=183752, cd=15, reqs={ClassReq(DH)}}, -- Disrupt
452 {type="UTILITY", id=188501, cd=60, reqs={ClassReq(DH)}, mods={{reqs={ClassReq(DH), LevelReq(42)}, mod=SubtractMod(30)}}}, -- Spectral Sight
453 {type="TANK", id=185245, cd=8, reqs={ClassReq(DH), LevelReq(9)}}, -- Torment
454 {type="DISPEL", id=278326, cd=10, reqs={ClassReq(DH), LevelReq(17)}}, -- Consume Magic
455 {type="STSOFTCC", id=217832, cd=45, reqs={ClassReq(DH), LevelReq(34)}}, -- Imprison
456 ---- DH.Havoc
457 {type="HARDCC", id=179057, cd=60, reqs={SpecReq({DH.Havoc})}, mods={{reqs={TalentReq(206477)}, mod=SubtractMod(20)}}}, -- Chaos Nova
458 {type="PERSONAL", id=198589, cd=60, reqs={SpecReq({DH.Havoc}), LevelReq(21)}, active=ActiveMod(212800, 10)}, -- Blur
459 {type="RAIDCD", id=196718, cd=300, reqs={SpecReq({DH.Havoc}), LevelReq(39)}, mods={{reqs={LevelReq(47)}, mod=SubtractMod(120)}}, active=ActiveMod(nil, 8)}, -- Darkness
460 {type="DAMAGE", id=191427, cd=300, reqs={SpecReq({DH.Havoc})}, mods={{reqs={LevelReq(48)}, mod=SubtractMod(60)}}}, -- Metamorphosis
461 ---- DH.Veng
462 {type="TANK", id=204021, cd=60, reqs={SpecReq({DH.Veng})}}, -- Fiery Brand
463 {type="TANK", id=212084, cd=45, reqs={SpecReq({DH.Veng}), LevelReq(11)}}, -- Fel Devastation
464 {type="SOFTCC", id=207684, cd=180, reqs={SpecReq({DH.Veng}), LevelReq(21)}, mods={{reqs={LevelReq(33)}, mod=SubtractMod(90)}, {reqs={TalentReq(209281)}, mod=MultiplyMod(0.8)}}}, -- Sigil of Misery
465 {type="SOFTCC", id=202137, cd=120, reqs={SpecReq({DH.Veng}), LevelReq(39)}, mods={{reqs={LevelReq(48)}, mod=SubtractMod(60)}, {reqs={TalentReq(209281)}, mod=MultiplyMod(0.8)}}}, -- Sigil of Silence
466 {type="TANK", id=187827, cd=300, reqs={SpecReq({DH.Veng})}, mods={{reqs={LevelReq(20)}, mod=SubtractMod(60)}, {reqs={LevelReq(48)}, mod=SubtractMod(60)}}}, -- Metamorphosis
467 ---- Talents
468 {type="IMMUNITY", id=196555, cd=180, reqs={TalentReq(196555)}, active=ActiveMod(196555, 5)}, -- Netherwalk
469 {type="SOFTCC", id=202138, cd=90, reqs={TalentReq(202138)}}, -- Sigil of Chains
470 {type="STHARDCC", id=211881, cd=30, reqs={TalentReq(211881)}}, -- Fel Eruption
471 {type="TANK", id=263648, cd=30, reqs={TalentReq(263648)}}, -- Soul Barrier
472 {type="DAMAGE", id=258925, cd=60, reqs={TalentReq(258925)}}, -- Fel Barrage
473 {type="TANK", id=320341, cd=90, reqs={TalentReq(320341)}}, -- Bulk Extraction
474 ---- Covenants
475 {type="COVENANT", id=312202, cd=60, reqs={ClassReq(DK), CovenantReq("Kyrian")}, version=103}, -- Shackle the Unworthy
476 {type="COVENANT", id=311648, cd=60, reqs={ClassReq(DK), CovenantReq("Venthyr")}, version=103}, -- Swarming Mist
477 {type="COVENANT", id=324128, cd=30, reqs={ClassReq(DK), CovenantReq("NightFae")}, version=103}, -- Death's Due
478 {type="COVENANT", id=315443, cd=120, reqs={ClassReq(DK), CovenantReq("Necrolord")}, version=103}, -- Abomination Limb
479
480 -- DK
481 -- TODO: Raise Ally (Brez support)
482 ---- Base
483 {type="UTILITY", id=49576, cd=25, reqs={ClassReq(DK), LevelReq(5)}, version=103}, -- Death Grip
484 {type="INTERRUPT", id=47528, cd=15, reqs={ClassReq(DK), LevelReq(7)}}, -- Mind Freeze
485 {type="PERSONAL", id=48707, cd=60, reqs={ClassReq(DK), LevelReq(9)}, mods={{reqs={TalentReq(205727)}, mod=SubtractMod(20)}}}, -- Anti-Magic Shell
486 {type="TANK", id=56222, cd=8, reqs={ClassReq(DK), LevelReq(14)}}, -- Dark Command
487 {type="PERSONAL", id=49039, cd=120, reqs={ClassReq(DK), LevelReq(33)}, active=ActiveMod(49039, 10)}, -- Lichborne
488 {type="PERSONAL", id=48792, cd=180, reqs={ClassReq(DK), LevelReq(38)}, active=ActiveMod(48792, 8)}, -- Icebound Fortitude
489 {type="BREZ", id=61999, cd=600, reqs={ClassReq(DK), LevelReq(39)}}, -- Raise Ally
490 {type="RAIDCD", id=51052, cd=120, reqs={ClassReq(DK), LevelReq(47)}, active=ActiveMod(nil, 10)}, -- Anti-Magic Zone
491 {type="PERSONAL", id=327574, cd=120, reqs={ClassReq(DK), LevelReq(54)}}, -- Sacrificial Pact
492 ---- DK.Blood
493 {type="STHARDCC", id=221562, cd=45, reqs={SpecReq({DK.Blood}), LevelReq(13)}}, -- Asphyxiate
494 {type="TANK", id=55233, cd=90, reqs={SpecReq({DK.Blood}), LevelReq(29)}, mods={{reqs={TalentReq(205723)}, mod=ResourceSpendingMods(DK.Blood, 0.15)}}, active=ActiveMod(55233, 10)}, -- Vampiric Blood
495 {type="SOFTCC", id=108199, cd=120, reqs={SpecReq({DK.Blood}), LevelReq(44)}, mods={{reqs={TalentReq(206970)}, mod=SubtractMod(30)}}}, -- Gorefiend's Grasp
496 {type="TANK", id=49028, cd=120, reqs={SpecReq({DK.Blood}), LevelReq(34)}, active=ActiveMod(81256, 8)}, -- Dancing Rune Weapon
497 ---- DK.Frost
498 {type="DAMAGE", id=51271, cd=45, reqs={SpecReq({DK.Frost}), LevelReq(29)}}, -- Pillar of Frost
499 {type="DAMAGE", id=279302, cd=180, reqs={SpecReq({DK.Frost}), LevelReq(44)}}, -- Frostwyrm's Fury
500 ---- DK.Unholy
501 {type="DAMAGE", id=275699, cd=90, reqs={SpecReq({DK.Unholy}), LevelReq(19)}, mods={{reqs={LevelReq(49)}, mod=SubtractMod(15)}, {reqs={TalentReq(276837)}, mod=CastDeltaMod(47541,-1)}, {reqs={TalentReq(276837)}, mod=CastDeltaMod(207317,-1)}}}, -- Apocalypse
502 {type="DAMAGE", id=63560, cd=60, reqs={SpecReq({DK.Unholy}), LevelReq(32)}, mods={{reqs={LevelReq(41)}, mod=CastDeltaMod(47541,-1)}}}, -- Dark Transformation
503 {type="DAMAGE", id=42650, cd=480, reqs={SpecReq({DK.Unholy}), LevelReq(44)}, mods={{reqs={TalentReq(276837)}, mod=CastDeltaMod(47541,-5)}, {reqs={TalentReq(276837)}, mod=CastDeltaMod(207317,-5)}}}, -- Army of the Dead
504 ---- Talents
505 {type="TANK", id=219809, cd=60, reqs={TalentReq(219809)}}, -- Tombstone
506 {type="DAMAGE", id=115989, cd=45, reqs={TalentReq(115989)}}, -- Unholy Blight
507 {type="STHARDCC", id=108194, cd=45, reqs={TalentReq(108194)}}, -- Asphyxiate
508 {type="SOFTCC", id=207167, cd=60, reqs={TalentReq(207167)}}, -- Blinding Sleet
509 {type="PERSONAL", id=48743, cd=120, reqs={TalentReq(48743)}}, -- Death Pact
510 {type="TANK", id=194844, cd=60, reqs={TalentReq(194844)}}, -- Bonestorm
511 {type="DAMAGE", id=152279, cd=120, reqs={TalentReq(152279)}}, -- Breath of Sindragosa
512 {type="DAMAGE", id=49206, cd=180, reqs={TalentReq(49206)}}, -- Summon Gargoyle
513 {type="DAMAGE", id=207289, cd=75, reqs={TalentReq(207289)}}, -- Unholy Assault
514 ---- Covenants
515 {type="COVENANT", id=306830, cd=60, reqs={ClassReq(DH), CovenantReq("Kyrian")}, version=103}, -- Elysian Decree
516 {type="COVENANT", id=317009, cd=60, reqs={ClassReq(DH), CovenantReq("Venthyr")}, version=103}, -- Sinful Brand
517 {type="COVENANT", id=323639, cd=90, reqs={ClassReq(DH), CovenantReq("NightFae")}, version=103}, -- The Hunt
518 {type="COVENANT", id=329554, cd=120, reqs={ClassReq(DH), CovenantReq("Necrolord")}, version=103}, -- Fodder to the Flame
519
520 -- Druid
521 -- TODO: Rebirth (Brez support)
522 ---- Base
523 {type="TANK", id=6795, cd=8, reqs={ClassReq(Druid), LevelReq(14)}}, -- Growl
524 {type="PERSONAL", id=22812, cd=60, reqs={ClassReq(Druid), LevelReq(24)}, mods={{reqs={TalentReq(203965)}, mod=MultiplyMod(0.67)}}, active=ActiveMod(22812, 12)}, -- Barkskin
525 {type="BREZ", id=20484, cd=600, reqs={ClassReq(Druid), LevelReq(29)}}, -- Rebirth
526 {type="DISPEL", id=2908, cd=10, reqs={ClassReq(Druid), LevelReq(41)}}, -- Soothe
527 {type="UTILITY", id=106898, cd=120, reqs={ClassReq(Druid), LevelReq(43)}, mods={{reqs={SpecReq({Druid.Guardian}), LevelReq(49)}, mod=SubtractMod(60)}}}, -- Stampeding Roar
528 ---- Shared
529 {type="DISPEL", id=2782, cd=8, reqs={SpecReq({Druid.Balance, Druid.Feral, Druid.Guardian}), LevelReq(19)}, mods={{mod=DispelMod(2782)}}, ignoreCast=true}, -- Remove Corruption
530 {type="INTERRUPT", id=106839, cd=15, reqs={SpecReq({Druid.Feral, Druid.Guardian}), LevelReq(26)}}, -- Skull Bash
531 {type="PERSONAL", id=61336, cd=180, reqs={SpecReq({Druid.Feral, Druid.Guardian}), LevelReq(32)}, mods={{reqs={SpecReq({Druid.Guardian}), LevelReq(47)}, mod=ChargesMod(2)}}, active=ActiveMod(61336, 6)}, -- Survival Instincts
532 {type="UTILITY", id=29166, cd=180, reqs={SpecReq({Druid.Balance, Druid.Resto}), LevelReq(42)}}, -- Innervate
533 ---- Druid.Balance
534 {type="INTERRUPT", id=78675, cd=60, reqs={SpecReq({Druid.Balance}), LevelReq(26)}, active=ActiveMod(nil, 8)}, -- Solar Beam
535 {type="SOFTCC", id=132469, cd=30, reqs={SpecReq({Druid.Balance}), LevelReq(28)}}, -- Typhoon
536 {type="DAMAGE", id=194223, cd=180, reqs={SpecReq({Druid.Balance}), NoTalentReq(102560), LevelReq(39)}}, -- Celestial Alignment
537 ---- Druid.Feral
538 {type="STHARDCC", id=22570, cd=20, reqs={SpecReq({Druid.Feral}), LevelReq(28)}}, -- Maim
539 {type="DAMAGE", id=106951, cd=180, reqs={SpecReq({Druid.Feral}), NoTalentReq(102543), LevelReq(34)}}, -- Berserk
540 ---- Druid.Guardian
541 {type="SOFTCC", id=99, cd=30, reqs={SpecReq({Druid.Guardian}), LevelReq(28)}}, -- Incapacitating Roar
542 {type="TANK", id=50334, cd=180, reqs={SpecReq({Druid.Guardian}), NoTalentReq(102558), LevelReq(34)}}, -- Berserk
543 ---- Druid.Resto
544 {type="EXTERNAL", id=102342, cd=90, reqs={SpecReq({Druid.Resto}), LevelReq(12)}}, -- Ironbark
545 {type="DISPEL", id=88423, cd=8, reqs={SpecReq({Druid.Resto}), LevelReq(19)}, mods={{mod=DispelMod(88423)}}, ignoreCast=true}, -- Remove Corruption
546 {type="SOFTCC", id=102793, cd=60, reqs={SpecReq({Druid.Resto}), LevelReq(28)}}, -- Ursol's Vortex
547 {type="HEALING", id=740, cd=180, reqs={SpecReq({Druid.Resto}), LevelReq(37)}, mods={{reqs={SpecReq({Druid.Resto}), TalentReq(197073)}, mod=SubtractMod(60)}}}, -- Tranquility
548 {type="UTILITY", id=132158, cd=60, reqs={SpecReq({Druid.Resto}), LevelReq(58)}}, -- Nature's Swiftness
549 ---- Talents
550 {type="HEALING", id=102351, cd=30, reqs={TalentReq(102351)}}, -- Cenarion Ward
551 {type="UTILITY", id=205636, cd=60, reqs={TalentReq(205636)}}, -- Force of Nature
552 {type="PERSONAL", id=108238, cd=90, reqs={TalentReq(108238)}}, -- Renewal
553 {type="STHARDCC", id=5211, cd=60, reqs={TalentReq(5211)}}, -- Mighty Bash
554 {type="SOFTCC", id=102359, cd=30, reqs={TalentReq(102359)}}, -- Mass Entanglement
555 {type="SOFTCC", id=132469, cd=30, reqs={TalentReq(197632)}}, -- Typhoon
556 {type="SOFTCC", id=132469, cd=30, reqs={TalentReq(197488)}}, -- Typhoon
557 {type="SOFTCC", id=102793, cd=60, reqs={TalentReq(197492)}}, -- Ursol's Vortex
558 {type="SOFTCC", id=99, cd=30, reqs={TalentReq(197491)}}, -- Incapacitating Roar
559 {type="SOFTCC", id=99, cd=30, reqs={TalentReq(217615)}}, -- Incapacitating Roar
560 {type="DAMAGE", id=319454, cd=300, reqs={TalentReq(319454), TalentReq(202157)}}, -- Heart of the Wild
561 {type="PERSONAL", id=319454, cd=300, reqs={TalentReq(319454), TalentReq(197491)}}, -- Heart of the Wild
562 {type="HEALING", id=319454, cd=300, reqs={TalentReq(319454), TalentReq(197492)}}, -- Heart of the Wild
563 {type="DAMAGE", id=319454, cd=300, reqs={TalentReq(319454), TalentReq(197488)}}, -- Heart of the Wild
564 {type="PERSONAL", id=319454, cd=300, reqs={TalentReq(319454), TalentReq(217615)}}, -- Heart of the Wild
565 {type="DAMAGE", id=319454, cd=300, reqs={TalentReq(319454), TalentReq(202155)}}, -- Heart of the Wild
566 {type="DAMAGE", id=319454, cd=300, reqs={TalentReq(319454), TalentReq(197632)}}, -- Heart of the Wild
567 {type="DAMAGE", id=319454, cd=300, reqs={TalentReq(319454), TalentReq(197490)}}, -- Heart of the Wild
568 {type="DAMAGE", id=102543, cd=180, reqs={TalentReq(102543)}}, -- Incarnation: King of the Jungle
569 {type="DAMAGE", id=102560, cd=180, reqs={TalentReq(102560)}}, -- Incarnation: Chosen of Elune
570 {type="TANK", id=102558, cd=180, reqs={TalentReq(102558)}}, -- Incarnation: Guardian of Ursoc
571 {type="HEALING", id=33891, cd=180, reqs={TalentReq(33891)}, mods={{mod=EventRemainingMod("SPELL_AURA_APPLIED",117679,180)}}, ignoreCast=true, active=ActiveMod(117679, 30)}, -- Incarnation: Tree of Life
572 {type="HEALING", id=203651, cd=60, reqs={TalentReq(203651)}}, -- Overgrowth
573 {type="DAMAGE", id=202770, cd=60, reqs={TalentReq(202770)}}, -- Fury of Elune
574 {type="TANK", id=204066, cd=75, reqs={TalentReq(204066)}}, -- Lunar Beam
575 {type="HEALING", id=197721, cd=90, reqs={TalentReq(197721)}}, -- Flourish
576 {type="TANK", id=80313, cd=30, reqs={TalentReq(80313)}}, -- Pulverize
577 ---- Covenants
578 ---- TODO: Kindered Spirits
579 {type="COVENANT", id=323546, cd=180, reqs={ClassReq(Druid), CovenantReq("Venthyr")}, version=103}, -- Ravenous Frenzy
580 {type="COVENANT", id=323764, cd=120, reqs={ClassReq(Druid), CovenantReq("NightFae")}, version=103}, -- Channel the Spirits
581 {type="COVENANT", id=325727, cd=25, reqs={ClassReq(Druid), CovenantReq("Necrolord")}, version=103}, -- Adaptive Swarm
582
583 -- Hunter
584 ---- Base
585 {type="UTILITY", id=186257, cd=180, reqs={ClassReq(Hunter), LevelReq(5)}, mods={{reqs={ClassReq(Hunter), TalentReq(266921)}, mod=MultiplyMod(0.8)}}}, -- Aspect of the Cheetah
586 {type="UTILITY", id=5384, cd=30, reqs={ClassReq(Hunter), LevelReq(6)}}, -- Feign Death
587 {type="IMMUNITY", id=186265, cd=180, reqs={ClassReq(Hunter), LevelReq(8)}, mods={{reqs={ClassReq(Hunter), TalentReq(266921)}, mod=MultiplyMod(0.8)}}, active=ActiveMod(186265, 8)}, -- Aspect of the Turtle
588 {type="PERSONAL", id=109304, cd=120, reqs={ClassReq(Hunter), LevelReq(9)}, mods={{reqs={SpecReq({Hunter.BM}), TalentReq(270581)}, mod=ResourceSpendingMods(Hunter.BM, 0.033)}, {reqs={SpecReq({Hunter.MM}), TalentReq(270581)}, mod=ResourceSpendingMods(Hunter.MM, 0.05)}, {reqs={SpecReq({Hunter.SV}), TalentReq(270581)}, mod=ResourceSpendingMods(Hunter.SV, 0.05)}}}, -- Exhilaration
589 {type="STSOFTCC", id=187650, cd=30, reqs={ClassReq(Hunter), LevelReq(10)}, mods={{reqs={ClassReq(Hunter), LevelReq(56)}, mod=SubtractMod(5)}}}, -- Freezing Trap
590 {type="UTILITY", id=34477, cd=30, reqs={ClassReq(Hunter), LevelReq(27)}}, -- Misdirection
591 {type="DISPEL", id=19801, cd=10, reqs={ClassReq(Hunter), LevelReq(37)}}, -- Tranquilizing Shot
592 {type="PERSONAL", id=264735, cd=180, reqs={ClassReq(Hunter)}, active=ActiveMod(264735, 10), version=103}, -- Survival of the Fittest
593 ---- Shared
594 {type="INTERRUPT", id=147362, cd=24, reqs={SpecReq({Hunter.BM, Hunter.MM}), LevelReq(18)}}, -- Counter Shot
595 {type="STHARDCC", id=19577, cd=60, reqs={SpecReq({Hunter.BM, Hunter.SV}), LevelReq(33)}}, -- Intimidation
596 ---- Hunter.BM
597 {type="DAMAGE", id=19574, cd=90, reqs={SpecReq({Hunter.BM}), LevelReq(20)}}, -- Bestial Wrath
598 {type="DAMAGE", id=193530, cd=120, reqs={SpecReq({Hunter.BM}), LevelReq(38)}}, -- Aspect of the Wild
599 ---- Hunter.MM
600 {type="STSOFTCC", id=186387, cd=30, reqs={SpecReq({Hunter.MM}), LevelReq(12)}}, -- Bursting Shot
601 {type="HARDCC", id=109248, cd=45, reqs={SpecReq({Hunter.MM}), LevelReq(33)}}, -- Binding Shot
602 {type="DAMAGE", id=288613, cd=120, reqs={SpecReq({Hunter.MM}), LevelReq(34)}}, -- Trueshot
603 ---- Hunter.SV
604 {type="INTERRUPT", id=187707, cd=15, reqs={SpecReq({Hunter.SV}), LevelReq(18)}}, -- Muzzle
605 {type="DAMAGE", id=266779, cd=120, reqs={SpecReq({Hunter.SV}), LevelReq(34)}}, -- Coordinated Assault
606 ---- Talents
607 {type="UTILITY", id=199483, cd=60, reqs={TalentReq(199483)}}, -- Camouflage
608 {type="SOFTCC", id=162488, cd=30, reqs={TalentReq(162488)}}, -- Steel Trap
609 {type="HARDCC", id=109248, cd=45, reqs={SpecReq({Hunter.BM, Hunter.SV}), TalentReq(109248)}}, -- Binding Shot
610 {type="DAMAGE", id=201430, cd=120, reqs={TalentReq(201430)}}, -- Stampede
611 {type="DAMAGE", id=260402, cd=60, reqs={TalentReq(260402)}}, -- Double Tap
612 {type="DAMAGE", id=321530, cd=60, reqs={TalentReq(321530)}}, -- Bloodshed
613 ---- Covenants
614 {type="COVENANT", id=308491, cd=60, reqs={ClassReq(Hunter), CovenantReq("Kyrian")}, version=103}, -- Resonating Arrow
615 {type="COVENANT", id=324149, cd=30, reqs={ClassReq(Hunter), CovenantReq("Venthyr")}, version=103}, -- Flayed Shot
616 {type="COVENANT", id=328231, cd=120, reqs={ClassReq(Hunter), CovenantReq("NightFae")}, version=103}, -- Wild Spirits
617 {type="COVENANT", id=325028, cd=45, reqs={ClassReq(Hunter), CovenantReq("Necrolord")}, version=103}, -- Death Chakram
618
619 -- Mage
620 -- TODO: Arcane should have Invisibility from 34 to 46, then Greater Invisibility from 47 onward
621 ---- Base
622 {type="INTERRUPT", id=2139, cd=24, reqs={ClassReq(Mage), LevelReq(7)}}, -- Counterspell
623 {type="DISPEL", id=475, cd=8, reqs={ClassReq(Mage), LevelReq(21)}, mods={{mod=DispelMod(475)}}, ignoreCast=true}, -- Remove Curse
624 {type="IMMUNITY", id=45438, cd=240, reqs={ClassReq(Mage), LevelReq(22)}, mods={{mod=CastRemainingMod(235219, 0)}}, active=ActiveMod(45438, 10)}, -- Ice Block
625 {type="PERSONAL", id=55342, cd=120, reqs={ClassReq(Mage), LevelReq(44)}}, -- Mirror Image
626 ---- Shared
627 {type="UTILITY", id=66, cd=300, reqs={SpecReq({Mage.Fire, Mage.Frost}), LevelReq(34)}}, -- Invisibility
628 {type="PERSONAL", id=108978, cd=60, reqs={SpecReq({Mage.Fire, Mage.Frost}), LevelReq(58)}}, -- Alter Time
629 ---- Mage.Arcane
630 {type="PERSONAL", id=342245, cd=60, reqs={SpecReq({Mage.Arcane}), LevelReq(19)}, mods={{reqs={TalentReq(342249)}, mod=SubtractMod(30)}}}, -- Alter Time
631 {type="PERSONAL", id=235450, cd=25, reqs={SpecReq({Mage.Arcane}), LevelReq(28)}}, -- Prismatic Barrier
632 {type="DAMAGE", id=12042, cd=120, reqs={SpecReq({Mage.Arcane}), LevelReq(29)}}, -- Arcane Power
633 {type="DAMAGE", id=321507, cd=45, reqs={SpecReq({Mage.Arcane}), LevelReq(33)}}, -- Touch of the Magi
634 {type="UTILITY", id=205025, cd=60, reqs={SpecReq({Mage.Arcane}), LevelReq(42)}}, -- Presence of Mind
635 {type="UTILITY", id=110959, cd=120, reqs={SpecReq({Mage.Arcane}), LevelReq(47)}}, -- Greater Invisibility
636 ---- Mage.Fire
637 {type="SOFTCC", id=31661, cd=20, reqs={SpecReq({Mage.Fire}), LevelReq(27)}, mods={{reqs={SpecReq({Mage.Fire}), LevelReq(38)}, mod=SubtractMod(2)}}}, -- Dragon's Breath
638 {type="PERSONAL", id=235313, cd=25, reqs={SpecReq({Mage.Fire}), LevelReq(28)}}, -- Blazing Barrier
639 {type="DAMAGE", id=190319, cd=120, reqs={SpecReq({Mage.Fire}), LevelReq(29)}}, -- Combustion
640 ---- Mage.Frost
641 {type="PERSONAL", id=11426, cd=25, reqs={SpecReq({Mage.Frost}), LevelReq(28)}}, -- Ice Barrier
642 {type="DAMAGE", id=12472, cd=180, reqs={SpecReq({Mage.Frost}), LevelReq(29)}}, -- Icy Veins
643 {type="DAMAGE", id=84714, cd=60, reqs={SpecReq({Mage.Frost}), LevelReq(38)}}, -- Frozen Orb
644 {type="UTILITY", id=235219, cd=300, reqs={SpecReq({Mage.Frost}), LevelReq(42)}, mods={{reqs={SpecReq({Mage.Frost}), LevelReq(54)}, mod=SubtractMod(30)}}}, -- Cold Snap
645 ---- Talents
646 {type="SOFTCC", id=113724, cd=45, reqs={TalentReq(113724)}}, -- Ring of Frost
647 ---- Covenants
648 {type="COVENANT", id=307443, cd=30, reqs={ClassReq(Mage), CovenantReq("Kyrian")}, version=103}, -- Radiant Spark
649 {type="COVENANT", id=314793, cd=90, reqs={ClassReq(Mage), CovenantReq("Venthyr")}, version=103}, -- Mirrors of Torment
650 {type="COVENANT", id=314791, cd=45, reqs={ClassReq(Mage), CovenantReq("NightFae")}, version=103}, -- Shifting Power
651 {type="COVENANT", id=324220, cd=180, reqs={ClassReq(Mage), CovenantReq("Necrolord")}, version=103}, -- Deathborne
652
653 -- Monk
654 -- TODO: Spiritual Focus (280197) as a ResourceSpendingMod
655 -- TODO: Blackout Combo modifiers
656 ---- Base
657 {type="DAMAGE", id=322109, cd=180, reqs={ClassReq(Monk)}}, -- Touch of Death
658 {type="TANK", id=115546, cd=8, reqs={ClassReq(Monk), LevelReq(14)}}, -- Provoke
659 {type="STSOFTCC", id=115078, cd=45, reqs={ClassReq(Monk), LevelReq(22)}, mods={{reqs={ClassReq(Monk), LevelReq(56)}, mod=SubtractMod(15)}}}, -- Paralysis
660 {type="HARDCC", id=119381, cd=60, reqs={ClassReq(Monk), LevelReq(6)}, mods={{reqs={ClassReq(Monk), TalentReq(264348)}, mod=SubtractMod(10)}}}, -- Leg Sweep
661 ---- Shared
662 {type="INTERRUPT", id=116705, cd=15, reqs={SpecReq({Monk.BRM, Monk.WW}), LevelReq(18)}}, -- Spear Hand Strike
663 {type="DISPEL", id=218164, cd=8, reqs={SpecReq({Monk.BRM, Monk.WW}), LevelReq(24)}, mods={{mod=DispelMod(218164)}}, ignoreCast=true, version=103}, -- Detox
664 {type="PERSONAL", id=243435, cd=420, reqs={SpecReq({Monk.MW, Monk.WW}), LevelReq(28)}, mods={{reqs={LevelReq(48)}, mod=SubtractMod(240)}}, active=ActiveMod(243435, 15)}, -- Fortifying Brew
665 ---- Monk.BRM
666 {type="TANK", id=322507, cd=60, reqs={SpecReq({Monk.BRM}), LevelReq(27)}, mods={{reqs={SpecReq({Monk.BRM}), TalentReq(325093)}, mod=MultiplyMod(0.8)}, {reqs={TalentReq(115399)}, mod=CastRemainingMod(115399, 0)}}}, -- Celestial Brew
667 {type="PERSONAL", id=115203, cd=360, reqs={SpecReq({Monk.BRM}), LevelReq(28)}, active=ActiveMod(115203, 15)}, -- Fortifying Brew
668 {type="TANK", id=115176, cd=300, reqs={SpecReq({Monk.BRM}), LevelReq(34)}}, -- Zen Meditation
669 {type="SOFTCC", id=324312, cd=30, reqs={SpecReq({Monk.BRM}), LevelReq(54)}}, -- Clash
670 {type="TANK", id=132578, cd=180, reqs={SpecReq({Monk.BRM}), LevelReq(42)}, active=ActiveMod(nil, 25)}, -- Invoke Niuzao, the Black Ox
671 ---- Monk.MW
672 {type="DISPEL", id=115450, cd=8, reqs={SpecReq({Monk.MW}), LevelReq(24)}, mods={{mod=DispelMod(115450)}}, ignoreCast=true, version=103}, -- Detox
673 {type="HEALING", id=322118, cd=180, reqs={SpecReq({Monk.MW}), NoTalentReq(325197), LevelReq(42)}, active=ActiveMod(nil, 25)}, -- Invoke Yu'lon, the Jade Serpent
674 {type="HEALING", id=115310, cd=180, reqs={SpecReq({Monk.MW}), LevelReq(46)}}, -- Revival
675 {type="EXTERNAL", id=116849, cd=120, reqs={SpecReq({Monk.MW}), LevelReq(27)}}, -- Life Cocoon
676 ---- Monk.WW
677 {type="PERSONAL", id=122470, cd=90, reqs={SpecReq({Monk.WW}), LevelReq(29)}}, -- Touch of Karma
678 {type="DAMAGE", id=137639, cd=90, reqs={SpecReq({Monk.WW}), LevelReq(27), NoTalentReq(152173)}, mods={{reqs={LevelReq(47)}, mod=ChargesMod(2)}}}, -- Storm, Earth, and Fire
679 {type="DAMAGE", id=123904, cd=120, reqs={SpecReq({Monk.WW}), LevelReq(42)}}, -- Invoke Xuen, the White Tiger
680 {type="DAMAGE", id=113656, cd=24, reqs={SpecReq({Monk.WW}), LevelReq(12)}}, -- Fists of Fury
681 ---- Talents
682 {type="UTILITY", id=116841, cd=30, reqs={TalentReq(116841)}}, -- Tiger's Lust
683 {type="TANK", id=115399, cd=120, reqs={TalentReq(115399)}}, -- Black Ox Brew
684 {type="SOFTCC", id=198898, cd=30, reqs={TalentReq(198898)}}, -- Song of Chi-Ji
685 {type="SOFTCC", id=116844, cd=45, reqs={TalentReq(116844)}, active=ActiveMod(nil, 5)}, -- Ring of Peace
686 {type="PERSONAL", id=122783, cd=90, reqs={TalentReq(122783)}}, -- Diffuse Magic
687 {type="PERSONAL", id=122278, cd=120, reqs={TalentReq(122278)}, active=ActiveMod(122278, 10)}, -- Dampen Harm
688 {type="TANK", id=325153, cd=60, reqs={TalentReq(325153)}}, -- Exploding Keg
689 {type="HEALING", id=325197, cd=120, reqs={TalentReq(325197)}, active=ActiveMod(nil, 25)}, -- Invoke Chi-Ji, the Red Crane
690 {type="DAMAGE", id=152173, cd=90, reqs={TalentReq(152173)}}, -- Serenity
691 ---- Covenants
692 {type="COVENANT", id=310454, cd=120, reqs={ClassReq(Monk), CovenantReq("Kyrian")}, version=103}, -- Weapons of Order
693 {type="COVENANT", id=326860, cd=180, reqs={ClassReq(Monk), CovenantReq("Venthyr")}, version=103}, -- Fallen Order
694 {type="COVENANT", id=327104, cd=30, reqs={ClassReq(Monk), CovenantReq("NightFae")}, version=103}, -- Faeline Stomp
695 {type="COVENANT", id=325216, cd=60, reqs={ClassReq(Monk), CovenantReq("Necrolord")}, version=103}, -- Bonedust Brew
696
697 -- Paladin
698 -- TODO: Prot should have Divine Protection from 28 to 41, then Ardent Defender from 42 onward
699 ---- Base
700 {type="IMMUNITY", id=642, cd=300, reqs={ClassReq(Paladin)}, mods={{reqs={TalentReq(114154)}, mod=MultiplyMod(0.7)}}, active=ActiveMod(642, 8)}, -- Divine Shield
701 {type="STHARDCC", id=853, cd=60, reqs={ClassReq(Paladin), LevelReq(5)}, mods={{reqs={TalentReq(234299)}, mod=ResourceSpendingMods(Paladin, 2)}}}, -- Hammer of Justice
702 {type="EXTERNAL", id=633, cd=600, reqs={ClassReq(Paladin), LevelReq(9)}, mods={{reqs={TalentReq(114154)}, mod=MultiplyMod(0.3)}}}, -- Lay on Hands
703 {type="UTILITY", id=1044, cd=25, reqs={ClassReq(Paladin), LevelReq(22)}, version=101}, -- Blessing of Freedom
704 {type="EXTERNAL", id=6940, cd=120, reqs={ClassReq(Paladin), LevelReq(32)}}, -- Blessing of Sacrifice
705 {type="EXTERNAL", id=1022, cd=300, reqs={ClassReq(Paladin), LevelReq(41), NoTalentReq(204018)}}, -- Blessing of Protection
706 ---- Shared
707 {type="DISPEL", id=213644, cd=8, reqs={SpecReq({Paladin.Prot, Paladin.Ret}), LevelReq(12)}}, -- Cleanse Toxins
708 {type="INTERRUPT", id=96231, cd=15, reqs={SpecReq({Paladin.Prot, Paladin.Ret}), LevelReq(23)}}, -- Rebuke
709 {type="DAMAGE", id=31884, cd=180, reqs={SpecReq({Paladin.Prot, Paladin.Ret}), LevelReq(37), NoTalentReq(231895)}, mods={{reqs={LevelReq(49)}, mod=SubtractMod(60)}}}, -- Avenging Wrath
710 ---- Paladin.Holy
711 {type="DISPEL", id=4987, cd=8, reqs={SpecReq({Paladin.Holy}), LevelReq(12)}, mods={{mod=DispelMod(4987)}}, ignoreCast=true}, -- Cleanse
712 {type="PERSONAL", id=498, cd=60, reqs={SpecReq({Paladin.Holy}), LevelReq(26)}, mods={{reqs={TalentReq(114154)}, mod=MultiplyMod(0.7)}}, active=ActiveMod(498, 8)}, -- Divine Protection
713 {type="HEALING", id=31884, cd=180, reqs={SpecReq({Paladin.Holy}), LevelReq(37), NoTalentReq(216331)}, mods={{reqs={LevelReq(49)}, mod=SubtractMod(60)}}, active=ActiveMod(31884, 20)}, -- Avenging Wrath
714 {type="RAIDCD", id=31821, cd=180, reqs={SpecReq({Paladin.Holy}), LevelReq(39)}, active=ActiveMod(31821, 6)}, -- Aura Mastery
715 ---- Paladin.Prot
716 {type="INTERRUPT", id=31935, cd=15, reqs={SpecReq({Paladin.Prot}), LevelReq(10)}}, -- Avenger's Shield
717 {type="TANK", id=62124, cd=8, reqs={SpecReq({Paladin.Prot}), LevelReq(14)}, version=102}, -- Hand of Reckoning
718 {type="TANK", id=86659, cd=300, reqs={SpecReq({Paladin.Prot}), LevelReq(39)}, active=ActiveMod(86659, 8)}, -- Guardian of Ancient Kings
719 {type="TANK", id=31850, cd=120, reqs={SpecReq({Paladin.Prot}), LevelReq(42)}, mods={{reqs={TalentReq(114154)}, mod=MultiplyMod(0.7)}}, active=ActiveMod(31850, 8)}, -- Ardent Defender
720 ---- Paladin.Ret
721 {type="PERSONAL", id=184662, cd=120, reqs={SpecReq({Paladin.Ret}), LevelReq(26)}, mods={{reqs={TalentReq(114154)}, mod=MultiplyMod(0.7)}}}, -- Shield of Vengeance
722 ---- Talents
723 {type="STSOFTCC", id=20066, cd=15, reqs={TalentReq(20066)}}, -- Repentance
724 {type="SOFTCC", id=115750, cd=90, reqs={TalentReq(115750)}}, -- Blinding Light
725 {type="PERSONAL", id=205191, cd=60, reqs={TalentReq(205191)}, active=ActiveMod(205191, 10)}, -- Eye for an Eye
726 {type="EXTERNAL", id=204018, cd=180, reqs={TalentReq(204018)}}, -- Blessing of Spellwarding
727 {type="HEALING", id=105809, cd=180, reqs={TalentReq(105809), SpecReq({Paladin.Holy})}, active=ActiveMod(105809, 20)}, -- Holy Avenger
728 {type="TANK", id=105809, cd=180, reqs={TalentReq(105809), SpecReq({Paladin.Prot})}}, -- Holy Avenger
729 {type="DAMAGE", id=105809, cd=180, reqs={TalentReq(105809), SpecReq({Paladin.Ret})}}, -- Holy Avenger
730 {type="HEALING", id=216331, cd=120, reqs={TalentReq(216331)}, active=ActiveMod(216331, 20)}, -- Avenging Crusader
731 {type="DAMAGE", id=231895, cd=20, reqs={TalentReq(231895)}}, -- Crusade
732 {type="DAMAGE", id=343721, cd=60, reqs={TalentReq(343721)}}, -- Final Reckoning
733 {type="HEALING", id=200025, cd=15, reqs={TalentReq(200025)}}, -- Beacon of Virtue
734 ---- Covenants
735 {type="COVENANT", id=304971, cd=60, reqs={ClassReq(Paladin), CovenantReq("Kyrian")}, version=103}, -- Divine Toll
736 {type="COVENANT", id=316958, cd=240, reqs={ClassReq(Paladin), CovenantReq("Venthyr")}, version=103}, -- Ashen Hallow
737 ---- TODO: Blessing of Summer
738 {type="COVENANT", id=328204, cd=30, reqs={ClassReq(Paladin), CovenantReq("Necrolord")}, version=103}, -- Vanquisher's Hammer
739
740 -- Priest
741 ---- Base
742 {type="SOFTCC", id=8122, cd=60, reqs={ClassReq(Priest), LevelReq(7)}, mods={{reqs={TalentReq(196704)}, mod=SubtractMod(30)}}}, -- Psychic Scream
743 {type="PERSONAL", id=19236, cd=90, reqs={ClassReq(Priest), LevelReq(8)}, active=ActiveMod(19236, 10)}, -- Desperate Prayer
744 {type="DISPEL", id=32375, cd=45, reqs={ClassReq(Priest), LevelReq(42)}}, -- Mass Dispel
745 {type="UTILITY", id=73325, cd=90, reqs={ClassReq(Priest), LevelReq(49)}}, -- Leap of Faith
746 ---- Shared
747 {type="DISPEL", id=527, cd=8, reqs={SpecReq({Priest.Disc, Priest.Holy}), LevelReq(18)}, mods={{mod=DispelMod(4987)}}, ignoreCast=true}, -- Purify
748 {type="HEALING", id=10060, cd=120, reqs={SpecReq({Priest.Disc, Priest.Holy}), LevelReq(58)}}, -- Power Infusion
749 ---- Priest.Disc
750 {type="EXTERNAL", id=33206, cd=180, reqs={SpecReq({Priest.Disc}), LevelReq(38)}}, -- Pain Suppression
751 {type="HEALING", id=47536, cd=90, reqs={SpecReq({Priest.Disc}), LevelReq(41), NoTalentReq(109964)}, active=ActiveMod(47536, 8)}, -- Rapture
752 {type="RAIDCD", id=62618, cd=180, reqs={SpecReq({Priest.Disc}), LevelReq(44)}, active=ActiveMod(nil, 10)}, -- Power Word: Barrier
753 ---- Priest.Holy
754 {type="STSOFTCC", id=88625, cd=60, reqs={SpecReq({Priest.Holy}), LevelReq(23), NoTalentReq(200199)}, mods={{mod=CastDeltaMod(585, -4)}, {reqs={TalentReq(196985)}, mod=CastDeltaMod(585, -1.3333)}}}, -- Holy Word: Chastise
755 {type="STHARDCC", id=88625, cd=60, reqs={SpecReq({Priest.Holy}), LevelReq(23), TalentReq(200199)}, mods={{mod=CastDeltaMod(585, -4)}, {reqs={TalentReq(196985)}, mod=CastDeltaMod(585, -1.3333)}}}, -- Holy Word: Chastise
756 {type="EXTERNAL", id=47788, cd=180, reqs={SpecReq({Priest.Holy}), LevelReq(38)}, mods={{reqs={TalentReq(200209)}, mod=GuardianAngelMod}}}, -- Guardian Spirit
757 {type="HEALING", id=64843, cd=180, reqs={SpecReq({Priest.Holy}), LevelReq(44)}}, -- Divine Hymn
758 {type="UTILITY", id=64901, cd=300, reqs={SpecReq({Priest.Holy}), LevelReq(47)}}, -- Symbol of Hope
759 ---- Priest.Shadow
760 {type="PERSONAL", id=47585, cd=120, reqs={SpecReq({Priest.Shadow}), LevelReq(16)}, mods={{reqs={TalentReq(288733)}, mod=SubtractMod(30)}}, active=ActiveMod(47585, 6)}, -- Dispersion
761 {type="DISPEL", id=213634, cd=8, reqs={SpecReq({Priest.Shadow}), LevelReq(18)}}, -- Purify Disease
762 {type="DAMAGE", id=228260, cd=90, reqs={SpecReq({Priest.Shadow}), LevelReq(23)}}, -- Void Eruption
763 {type="HEALING", id=15286, cd=120, reqs={SpecReq({Priest.Shadow}), LevelReq(38)}, mods={{reqs={TalentReq(199855)}, mod=SubtractMod(45)}}, active=ActiveMod(15286, 15)}, -- Vampiric Embrace
764 {type="INTERRUPT", id=15487, cd=45, reqs={SpecReq({Priest.Shadow}), LevelReq(41)}, mods={{reqs={TalentReq(263716)}, mod=SubtractMod(15)}}}, -- Silence
765 {type="DAMAGE", id=10060, cd=120, reqs={SpecReq({Priest.Shadow}), LevelReq(58)}}, -- Power Infusion
766 ---- Talents
767 {type="HARDCC", id=205369, cd=30, reqs={TalentReq(205369)}}, -- Mind Bomb
768 {type="SOFTCC", id=204263, cd=45, reqs={TalentReq(204263)}}, -- Shining Force
769 {type="STHARDCC", id=64044, cd=45, reqs={TalentReq(64044)}}, -- Psychic Horror
770 {type="HEALING", id=109964, cd=60, reqs={TalentReq(109964)}, active=ActiveMod(109964, 10)}, -- Spirit Shell
771 {type="HEALING", id=200183, cd=120, reqs={TalentReq(200183)}, active=ActiveMod(200183, 20)}, -- Apotheosis
772 {type="HEALING", id=246287, cd=90, reqs={TalentReq(246287)}}, -- Evangelism
773 {type="HEALING", id=265202, cd=720, reqs={TalentReq(265202)}, mods={{mod=CastDeltaMod(34861,-30)}, {mod=CastDeltaMod(2050,-30)}}}, -- Holy Word: Salvation
774 {type="DAMAGE", id=319952, cd=90, reqs={TalentReq(319952)}}, -- Surrender to Madness
775 ---- Covenants
776 {type="COVENANT", id=325013, cd=180, reqs={ClassReq(Priest), CovenantReq("Kyrian")}, version=103}, -- Boon of the Ascended
777 {type="COVENANT", id=323673, cd=45, reqs={ClassReq(Priest), CovenantReq("Venthyr")}, version=103}, -- Mindgames
778 {type="COVENANT", id=327661, cd=90, reqs={ClassReq(Priest), CovenantReq("NightFae")}, version=103}, -- Fae Guardians
779 {type="COVENANT", id=324724, cd=60, reqs={ClassReq(Priest), CovenantReq("Necrolord")}, version=103}, -- Unholy Nova
780
781 -- Rogue
782 ---- Base
783 {type="UTILITY", id=57934, cd=30, reqs={ClassReq(Rogue), LevelReq(44)}}, -- Tricks of the Trade
784 {type="UTILITY", id=114018, cd=360, reqs={ClassReq(Rogue), LevelReq(47)}, active=ActiveMod(114018, 15)}, -- Shroud of Concealment
785 {type="UTILITY", id=1856, cd=120, reqs={ClassReq(Rogue), LevelReq(31)}}, -- Vanish
786 {type="IMMUNITY", id=31224, cd=120, reqs={ClassReq(Rogue), LevelReq(49)}, active=ActiveMod(31224, 5)}, -- Cloak of Shadows
787 {type="STHARDCC", id=408, cd=20, reqs={ClassReq(Rogue), LevelReq(20)}}, -- Kidney Shot
788 {type="UTILITY", id=1725, cd=30, reqs={ClassReq(Rogue), LevelReq(36)}}, -- Distract
789 {type="STSOFTCC", id=2094, cd=120, reqs={ClassReq(Rogue), LevelReq(41)}, mods={{reqs={TalentReq(256165)}, mod=SubtractMod(30)}}}, -- Blind
790 {type="PERSONAL", id=5277, cd=120, reqs={ClassReq(Rogue), LevelReq(23)}, active=ActiveMod(5277, 10)}, -- Evasion
791 {type="INTERRUPT", id=1766, cd=15, reqs={ClassReq(Rogue), LevelReq(6)}}, -- Kick
792 {type="PERSONAL", id=185311, cd=30, reqs={ClassReq(Rogue), LevelReq(8)}}, -- Crimson Vial
793 ---- Rogue.Sin
794 {type="DAMAGE", id=79140, cd=120, reqs={SpecReq({Rogue.Sin}), LevelReq(34)}}, -- Vendetta
795 ---- Rogue.Outlaw
796 {type="DAMAGE", id=13877, cd=30, reqs={SpecReq({Rogue.Outlaw}), LevelReq(33)}, mods={{reqs={SpecReq({Rogue.Outlaw}), TalentReq(272026)}, mod=SubtractMod(-3)}}}, -- Blade Flurry
797 {type="DAMAGE", id=13750, cd=180, reqs={SpecReq({Rogue.Outlaw}), LevelReq(34)}}, -- Adrenaline Rush
798 {type="STSOFTCC", id=1776, cd=15, reqs={SpecReq({Rogue.Outlaw}), LevelReq(46)}, version=101}, -- Gouge
799 ---- Rogue.Sub
800 {type="DAMAGE", id=121471, cd=180, reqs={SpecReq({Rogue.Sub}), LevelReq(34)}}, -- Shadow Blades
801 ---- Talents
802 {type="DAMAGE", id=343142, cd=90, reqs={TalentReq(343142)}}, -- Dreadblades
803 {type="DAMAGE", id=271877, cd=45, reqs={TalentReq(271877)}}, -- Blade Rush
804 {type="DAMAGE", id=51690, cd=120, reqs={TalentReq(51690)}}, -- Killing Spree
805 {type="DAMAGE", id=277925, cd=60, reqs={TalentReq(277925)}}, -- Shuriken Tornado
806 ---- Covenants
807 {type="COVENANT", id=323547, cd=45, reqs={ClassReq(Rogue), CovenantReq("Kyrian")}, version=103}, -- Echoing Reprimand
808 {type="COVENANT", id=323654, cd=90, reqs={ClassReq(Rogue), CovenantReq("Venthyr")}, version=103}, -- Flagellation
809 {type="COVENANT", id=328305, cd=90, reqs={ClassReq(Rogue), CovenantReq("NightFae")}, version=103}, -- Sepsis
810 {type="COVENANT", id=328547, cd=30, reqs={ClassReq(Rogue), CovenantReq("Necrolord")}, charges=3, version=103}, -- Serrated Bone Spike
811
812 -- Shaman
813 -- TODO: Add support for Reincarnation
814 ---- Base
815 {type="INTERRUPT", id=57994, cd=12, reqs={ClassReq(Shaman), LevelReq(12)}}, -- Wind Shear
816 {type="HARDCC", id=192058, cd=60, reqs={ClassReq(Shaman), LevelReq(23)}, mods={{reqs={TalentReq(265046)}, mod=StaticChargeMod}}}, -- Capacitor Totem
817 {type="UTILITY", id=198103, cd=300, reqs={ClassReq(Shaman), LevelReq(37)}}, -- Earth Elemental
818 {type="STSOFTCC", id=51514, cd=30, reqs={ClassReq(Shaman), LevelReq(41)}, mods={{reqs={LevelReq(56)}, mod=SubtractMod(10)}}}, -- Hex
819 {type="PERSONAL", id=108271, cd=90, reqs={ClassReq(Shaman), LevelReq(42)}, active=ActiveMod(108271, 8)}, -- Astral Shift
820 {type="DISPEL", id=8143, cd=60, reqs={ClassReq(Shaman), LevelReq(47)}, active=ActiveMod(nil, 10)}, -- Tremor Totem
821 ---- Shared
822 {type="DISPEL", id=51886, cd=8, reqs={SpecReq({Shaman.Ele, Shaman.Enh}), LevelReq(18)}, mods={{mod=DispelMod(51886)}}, ignoreCast=true}, -- Cleanse Spirit
823 {type="UTILITY", id=79206, cd=120, reqs={SpecReq({Shaman.Ele, Shaman.Resto}), LevelReq(44)}, mods={{reqs={TalentReq(192088)}, mod=SubtractMod(60)}}}, -- Spiritwalker's Grace
824 ---- Shaman.Ele
825 {type="DAMAGE", id=198067, cd=150, reqs={SpecReq({Shaman.Ele}), LevelReq(34), NoTalentReq(192249)}}, -- Fire Elemental
826 ---- Shaman.Enh
827 {type="DAMAGE", id=51533, cd=120, reqs={SpecReq({Shaman.Enh}), LevelReq(34)}, mods={{reqs={SpecReq({Shaman.Enh}), TalentReq(262624)}, mod=SubtractMod(30)}}}, -- Feral Spirit
828 ---- Shaman.Resto
829 {type="DISPEL", id=77130, cd=8, reqs={SpecReq({Shaman.Resto}), LevelReq(18)}, mods={{mod=DispelMod(77130)}}, ignoreCast=true}, -- Purify Spirit
830 {type="UTILITY", id=16191, cd=180, reqs={SpecReq({Shaman.Resto}), LevelReq(38)}}, -- Mana Tide Totem
831 {type="RAIDCD", id=98008, cd=180, reqs={SpecReq({Shaman.Resto}), LevelReq(43)}, active=ActiveMod(nil, 6), version=101}, -- Spirit Link Totem
832 {type="HEALING", id=108280, cd=180, reqs={SpecReq({Shaman.Resto}), LevelReq(49)}}, -- Healing Tide Totem
833 ---- Talents
834 {type="SOFTCC", id=51485, cd=30, reqs={TalentReq(51485)}}, -- Earthgrab Totem
835 {type="HEALING", id=198838, cd=60, reqs={TalentReq(198838)}}, -- Earthen Wall Totem
836 {type="DAMAGE", id=192249, cd=150, reqs={TalentReq(192249)}}, -- Fire Elemental
837 {type="EXTERNAL", id=207399, cd=300, reqs={TalentReq(207399)}}, -- Ancestral Protection Totem
838 {type="HEALING", id=108281, cd=120, reqs={TalentReq(108281)}, active=ActiveMod(108281, 10)}, -- Ancestral Guidance
839 {type="UTILITY", id=192077, cd=120, reqs={TalentReq(192077)}}, -- Wind Rush Totem
840 {type="DAMAGE", id=191634, cd=60, reqs={TalentReq(191634)}}, -- Stormkeeper
841 {type="HEALING", id=114052, cd=180, reqs={TalentReq(114052)}, active=ActiveMod(264735, 10)}, -- Ascendance
842 {type="DAMAGE", id=114050, cd=180, reqs={TalentReq(114050)}}, -- Ascendance
843 {type="DAMAGE", id=114051, cd=180, reqs={TalentReq(114051)}}, -- Ascendance
844 ---- Covenants
845 {type="COVENANT", id=324386, cd=60, reqs={ClassReq(Shaman), CovenantReq("Kyrian")}, version=103}, -- Vesper Totem
846 {type="COVENANT", id=320674, cd=90, reqs={ClassReq(Shaman), CovenantReq("Venthyr")}, version=103}, -- Chain Harvest
847 {type="COVENANT", id=328923, cd=120, reqs={ClassReq(Shaman), CovenantReq("NightFae")}, version=103}, -- Fae Transfusion
848 {type="COVENANT", id=326059, cd=45, reqs={ClassReq(Shaman), CovenantReq("Necrolord")}, version=103}, -- Primordial Wave
849
850 -- Warlock
851 -- TODO: Soulstone (Brez Support)
852 -- TODO: PetReq for Spell Lock and Axe Toss
853 ---- Base
854 {type="PERSONAL", id=104773, cd=180, reqs={ClassReq(Warlock), LevelReq(4)}, active=ActiveMod(104773, 8)}, -- Unending Resolve
855 {type="UTILITY", id=333889, cd=180, reqs={ClassReq(Warlock), LevelReq(22)}}, -- Fel Domination
856 {type="BREZ", id=20707, cd=600, reqs={ClassReq(Warlock), LevelReq(48)}}, -- Soulstone
857 {type="HARDCC", id=30283, cd=60, reqs={ClassReq(Warlock), LevelReq(38)}, mods={{reqs={TalentReq(264874)}, mod=SubtractMod(15)}}}, -- Shadowfury
858 ---- Shared
859 {type="INTERRUPT", id=19647, cd=24, reqs={SpecReq({Warlock.Affl, Warlock.Destro}), LevelReq(29)}}, -- Spell Lock
860 ---- Warlock.Affl
861 {type="DAMAGE", id=205180, cd=180, reqs={SpecReq({Warlock.Affl}), LevelReq(42)}, mods={{reqs={TalentReq(334183)}, mod=SubtractMod(60)}}}, -- Summon Darkglare
862 ---- Warlock.Demo
863 {type="INTERRUPT", id=89766, cd=30, reqs={SpecReq({Warlock.Demo}), LevelReq(29)}}, -- Axe Toss
864 {type="DAMAGE", id=265187, cd=90, reqs={SpecReq({Warlock.Demo}), LevelReq(42)}}, -- Summon Demonic Tyrant
865 ---- Warlock.Destro
866 {type="DAMAGE", id=1122, cd=180, reqs={SpecReq({Warlock.Destro}), LevelReq(42)}}, -- Summon Infernal
867 ---- Talents
868 {type="PERSONAL", id=108416, cd=60, reqs={TalentReq(108416)}}, -- Dark Pact
869 {type="DAMAGE", id=152108, cd=30, reqs={TalentReq(152108)}}, -- Cataclysm
870 {type="STHARDCC", id=6789, cd=45, reqs={TalentReq(6789)}}, -- Mortal Coil
871 {type="SOFTCC", id=5484, cd=40, reqs={TalentReq(5484)}}, -- Howl of Terror
872 {type="DAMAGE", id=111898, cd=120, reqs={TalentReq(111898)}}, -- Grimoire: Felguard
873 {type="DAMAGE", id=113858, cd=120, reqs={TalentReq(113858)}}, -- Dark Soul: Instability
874 {type="DAMAGE", id=267217, cd=180, reqs={TalentReq(267217)}}, -- Nether Portal
875 {type="DAMAGE", id=113860, cd=120, reqs={TalentReq(113860)}}, -- Dark Soul: Misery
876 ---- Covenants
877 {type="COVENANT", id=312321, cd=40, reqs={ClassReq(Warlock), CovenantReq("Kyrian")}, version=103}, -- Scouring Tithe
878 {type="COVENANT", id=321792, cd=60, reqs={ClassReq(Warlock), CovenantReq("Venthyr")}, version=103}, -- Impending Catastrophe
879 {type="COVENANT", id=325640, cd=60, reqs={ClassReq(Warlock), CovenantReq("NightFae")}, version=103}, -- Soul Rot
880 {type="COVENANT", id=325289, cd=45, reqs={ClassReq(Warlock), CovenantReq("Necrolord")}, version=103}, -- Decimating Bolt
881
882 -- Warrior
883 ---- Base
884 {type="INTERRUPT", id=6552, cd=15, reqs={ClassReq(Warrior), LevelReq(7)}}, -- Pummel
885 {type="TANK", id=355, cd=8, reqs={ClassReq(Warrior), LevelReq(14)}}, -- Taunt
886 {type="SOFTCC", id=5246, cd=90, reqs={ClassReq(Warrior), LevelReq(34)}}, -- Intimidating Shout
887 {type="UTILITY", id=64382, cd=180, reqs={ClassReq(Warrior), LevelReq(41)}}, -- Shattering Throw
888 {type="EXTERNAL", id=3411, cd=30, reqs={ClassReq(Warrior), LevelReq(43)}}, -- Intervene
889 {type="RAIDCD", id=97462, cd=180, reqs={ClassReq(Warrior), LevelReq(46)}, active=ActiveMod(97462, 10)}, -- Rallying Cry
890 {type="TANK", id=1161, cd=240, reqs={ClassReq(Warrior), LevelReq(54)}}, -- Challenging Shout
891 ---- Shared
892 {type="PERSONAL", id=23920, cd=25, reqs={SpecReq({Warrior.Arms, Warrior.Fury}), LevelReq(47)}, active=ActiveMod(23920, 5)}, -- Spell Reflection
893 ---- Warrior.Arms
894 {type="PERSONAL", id=118038, cd=180, reqs={SpecReq({Warrior.Arms}), LevelReq(23)}, mods={{reqs={LevelReq(52)}, mod=SubtractMod(60)}}, active=ActiveMod(118038, 8)}, -- Die by the Sword
895 {type="DAMAGE", id=227847, cd=90, reqs={SpecReq({Warrior.Arms}), LevelReq(38)}, mods={{reqs={TalentReq(152278)}, mod=ResourceSpendingMods(Warrior.Arms, 0.05)}}}, -- Bladestorm
896 ---- Warrior.Fury
897 {type="PERSONAL", id=184364, cd=180, reqs={SpecReq({Warrior.Fury}), LevelReq(23)}, mods={{reqs={LevelReq(32)}, mod=SubtractMod(60)}}, active=ActiveMod(184364, 8)}, -- Enraged Regeneration
898 {type="DAMAGE", id=1719, cd=90, reqs={SpecReq({Warrior.Fury}), LevelReq(38)}, mods={{reqs={TalentReq(152278)}, mod=ResourceSpendingMods(Warrior.Fury, 0.05)}}}, -- Recklessness
899 ---- Warrior.Prot
900 {type="HARDCC", id=46968, cd=40, reqs={SpecReq({Warrior.Prot}), LevelReq(21)}, mods={{reqs={TalentReq(275339)}, mod=RumblingEarthMod}}}, -- Shockwave
901 {type="TANK", id=871, cd=240, reqs={SpecReq({Warrior.Prot}), LevelReq(23)}, mods={{reqs={TalentReq(152278)}, mod=ResourceSpendingMods(Warrior.Arms, 0.1)}}, active=ActiveMod(871, 8)}, -- Shield Wall
902 {type="TANK", id=1160, cd=45, reqs={SpecReq({Warrior.Prot}), LevelReq(27)}}, -- Demoralizing Shout
903 {type="DAMAGE", id=107574, cd=90, reqs={SpecReq({Warrior.Prot}), LevelReq(32)}, mods={{reqs={TalentReq(152278)}, mod=ResourceSpendingMods(Warrior.Prot, 0.1)}}}, -- Avatar
904 {type="TANK", id=12975, cd=180, reqs={SpecReq({Warrior.Prot}), LevelReq(38)}, mods={{reqs={TalentReq(280001)}, mod=SubtractMod(60)}}, active=ActiveMod(12975, 15)}, -- Last Stand
905 {type="PERSONAL", id=23920, cd=25, reqs={SpecReq({Warrior.Prot}), LevelReq(47)}, active=ActiveMod(23920, 5)}, -- Spell Reflection
906 ---- Talents
907 {type="STHARDCC", id=107570, cd=30, reqs={TalentReq(107570)}}, -- Storm Bolt
908 {type="DAMAGE", id=107574, cd=90, reqs={TalentReq(107574)}}, -- Avatar
909 {type="DAMAGE", id=262228, cd=60, reqs={TalentReq(262228)}}, -- Deadly Calm
910 {type="DAMAGE", id=228920, cd=45, reqs={TalentReq(228920)}}, -- Ravager
911 {type="DAMAGE", id=46924, cd=60, reqs={TalentReq(46924)}}, -- Bladestorm
912 {type="DAMAGE", id=152277, cd=45, reqs={TalentReq(152277)}}, -- Ravager
913 {type="DAMAGE", id=280772, cd=30, reqs={TalentReq(280772)}}, -- Siegebreaker
914 ---- Covenants
915 {type="COVENANT", id=307865, cd=60, reqs={ClassReq(Warrior), CovenantReq("Kyrian")}, version=103}, -- Spear of Bastion
916 {type="COVENANT", id=325886, cd=90, reqs={ClassReq(Warrior), CovenantReq("NightFae")}, version=103}, -- Ancient Aftershock
917 {type="COVENANT", id=324143, cd=180, reqs={ClassReq(Warrior), CovenantReq("Necrolord")}, version=103}, -- Conqueror's Banner
918}
919
920ZT.linkedSpellIDs = {
921 [19647] = {119910, 132409, 115781}, -- Spell Lock
922 [89766] = {119914, 347008}, -- Axe Toss
923 [51514] = {211004, 211015, 277778, 309328, 210873, 211010, 269352, 277784}, -- Hex
924 [132469] = {61391}, -- Typhoon
925 [191427] = {200166}, -- Metamorphosis
926 [106898] = {77761, 77764}, -- Stampeding Roar
927 [86659] = {212641}, -- Guardian of the Ancient Kings (+Glyph)
928 [281195] = {264735}, -- Survival of the Fittest (+Lone Wolf)
929}
930
931ZT.separateLinkedSpellIDs = {
932 [86659] = {212641}, -- Guardian of the Ancient Kings (+Glyph)
933}
934
935--##############################################################################
936-- Handling custom spells specified by the user in the configuration
937
938local spellConfigPrefix = "return function(DH,DK,Druid,Hunter,Mage,Monk,Paladin,Priest,Rogue,Shaman,Warlock,Warrior,LevelReq,RaceReq,ClassReq,SpecReq,TalentReq,NoTalentReq,SubtractMod,MultiplyMod,ChargesMod,DynamicMod,EventDeltaMod,CastDeltaMod,EventRemainingMod,CastRemainingMod,DispelMod) return "
939local spellConfigSuffix = "end"
940
941local function trim(s) -- From PiL2 20.4
942 if s ~= nil then
943 return s:gsub("^%s*(.-)%s*$", "%1")
944 end
945 return ""
946end
947
948local function addCustomSpell(spellConfig, i)
949 if not spellConfig or type(spellConfig) ~= "table" then
950 prerror("Custom Spell", i, "is not represented as a valid table")
951 return
952 end
953
954 if type(spellConfig.type) ~= "string" then
955 prerror("Custom Spell", i, "does not have a valid 'type' entry")
956 return
957 end
958
959 if type(spellConfig.id) ~= "number" then
960 prerror("Custom Spell", i, "does not have a valid 'id' entry")
961 return
962 end
963
964 if type(spellConfig.cd) ~= "number" then
965 prerror("Custom Spell", i, "does not have a valid 'cd' entry")
966 return
967 end
968
969 spellConfig.version = 10000
970 spellConfig.isCustom = true
971
972 ZT.spellList[#ZT.spellList + 1] = spellConfig
973end
974
975for i = 1,16 do
976 local spellConfig = trim(ZT.config["custom"..i])
977 if spellConfig ~= "" then
978 local spellConfigFunc = WeakAuras.LoadFunction(spellConfigPrefix..spellConfig..spellConfigSuffix, "ZenTracker Custom Spell "..i)
979 if spellConfigFunc then
980 local spell = spellConfigFunc(DH,DK,Druid,Hunter,Mage,Monk,Paladin,Priest,Rogue,Shaman,Warlock,Warrior,LevelReq,RaceReq,ClassReq,SpecReq,TalentReq,NoTalentReq,SubtractMod,MultiplyMod,ChargesMod,DynamicMod,EventDeltaMod,CastDeltaMod,EventRemainingMod,CastRemainingMod,DispelMod)
981 addCustomSpell(spell, i)
982 end
983 end
984end
985
986--##############################################################################
987-- Compiling the complete indexed tables of spells
988
989ZT.spells = DefaultTable_Create(function() return DefaultTable_Create(function() return {} end) end)
990
991for _,spellInfo in ipairs(ZT.spellList) do
992 spellInfo.version = spellInfo.version or 100
993 spellInfo.isRegistered = false
994 spellInfo.frontends = {}
995
996 -- Indexing for faster lookups based on the info/requirements
997 if spellInfo.reqs and (#spellInfo.reqs > 0) then
998 for _,req in ipairs(spellInfo.reqs) do
999 if req.indices then
1000 for _,index in ipairs(req.indices) do
1001 tinsert(ZT.spells[req.type][index], spellInfo)
1002 end
1003 end
1004 end
1005 else
1006 tinsert(ZT.spells["generic"], spellInfo)
1007 end
1008
1009 if spellInfo.mods then
1010 for _,mod in ipairs(spellInfo.mods) do
1011 if mod.reqs then
1012 for _,req in ipairs(mod.reqs) do
1013 for _,index in ipairs(req.indices) do
1014 tinsert(ZT.spells[req.type][index], spellInfo)
1015 end
1016 end
1017 end
1018 end
1019 end
1020
1021 tinsert(ZT.spells["type"][spellInfo.type], spellInfo)
1022 tinsert(ZT.spells["id"][spellInfo.id], spellInfo)
1023
1024 -- Handling more convenient way of specifying active durations
1025 if spellInfo.active then
1026 local spellID = spellInfo.active.spellID
1027 local duration = spellInfo.active.duration
1028
1029 spellInfo.duration = duration
1030 if spellID then
1031 if not spellInfo.mods then
1032 spellInfo.mods = {}
1033 end
1034 tinsert(spellInfo.mods, {mod=DurationMod(spellID)})
1035 end
1036 end
1037end
1038
1039--##############################################################################
1040-- Handling combatlog and WeakAura events by invoking specified callbacks
1041
1042ZT.eventHandlers = { handlers = {} }
1043
1044function ZT.eventHandlers:add(type, spellID, sourceGUID, func, data)
1045 local types = self.handlers[spellID]
1046 if not types then
1047 types = {}
1048 self.handlers[spellID] = types
1049 end
1050
1051 local sources = types[type]
1052 if not sources then
1053 sources = {}
1054 types[type] = sources
1055 end
1056
1057 local handlers = sources[sourceGUID]
1058 if not handlers then
1059 handlers = {}
1060 sources[sourceGUID] = handlers
1061 end
1062
1063 handlers[func] = data
1064end
1065
1066function ZT.eventHandlers:remove(type, spellID, sourceGUID, func)
1067 local types = self.handlers[spellID]
1068 if types then
1069 local sources = types[type]
1070 if sources then
1071 local handlers = sources[sourceGUID]
1072 if handlers then
1073 handlers[func] = nil
1074 end
1075 end
1076 end
1077end
1078
1079function ZT.eventHandlers:removeAll(sourceGUID)
1080 for _,spells in pairs(self.eventHandlers) do
1081 for _,sources in pairs(spells) do
1082 for GUID,handlers in pairs(sources) do
1083 if GUID == sourceGUID then
1084 wipe(handlers)
1085 end
1086 end
1087 end
1088 end
1089end
1090
1091local function fixSourceGUID(sourceGUID) -- Based on https://wago.io/p/Nnogga
1092 local type = strsplit("-", sourceGUID)
1093 if type == "Pet" then
1094 for unit in WA_IterateGroupMembers() do
1095 if UnitGUID(unit.."pet") == sourceGUID then
1096 sourceGUID = UnitGUID(unit)
1097 break
1098 end
1099 end
1100 end
1101
1102 return sourceGUID
1103end
1104
1105function ZT.eventHandlers:handle(type, spellID, sourceGUID)
1106 local types = self.handlers[spellID]
1107 if not types then
1108 return
1109 end
1110
1111 local sources = types[type]
1112 if not sources then
1113 return
1114 end
1115
1116 local handlers = sources[sourceGUID]
1117 if not handlers then
1118 sourceGUID = fixSourceGUID(sourceGUID)
1119 handlers = sources[sourceGUID]
1120 if not handlers then
1121 return
1122 end
1123 end
1124
1125 for func,data in pairs(handlers) do
1126 func(data, spellID)
1127 end
1128end
1129
1130--##############################################################################
1131-- Managing timer callbacks in a way that allows for updates/removals
1132
1133ZT.timers = { heap={}, callbackTimes={} }
1134
1135function ZT.timers:fixHeapUpwards(index)
1136 local heap = self.heap
1137 local timer = heap[index]
1138
1139 local parentIndex, parentTimer
1140 while index > 1 do
1141 parentIndex = floor(index / 2)
1142 parentTimer = heap[parentIndex]
1143 if timer.time >= parentTimer.time then
1144 break
1145 end
1146
1147 parentTimer.index = index
1148 heap[index] = parentTimer
1149 index = parentIndex
1150 end
1151
1152 if timer.index ~= index then
1153 timer.index = index
1154 heap[index] = timer
1155 end
1156end
1157
1158function ZT.timers:fixHeapDownwards(index)
1159 local heap = self.heap
1160 local timer = heap[index]
1161
1162 local childIndex, minChildTimer, leftChildTimer, rightChildTimer
1163 while true do
1164 childIndex = 2 * index
1165
1166 leftChildTimer = heap[childIndex]
1167 if leftChildTimer then
1168 rightChildTimer = heap[childIndex + 1]
1169 if rightChildTimer and (rightChildTimer.time < leftChildTimer.time) then
1170 minChildTimer = rightChildTimer
1171 else
1172 minChildTimer = leftChildTimer
1173 end
1174 else
1175 break
1176 end
1177
1178 if timer.time <= minChildTimer.time then
1179 break
1180 end
1181
1182 childIndex = minChildTimer.index
1183 minChildTimer.index = index
1184 heap[index] = minChildTimer
1185 index = childIndex
1186 end
1187
1188 if timer.index ~= index then
1189 timer.index = index
1190 heap[index] = timer
1191 end
1192end
1193
1194function ZT.timers:setupCallback()
1195 local minTimer = self.heap[1]
1196 if minTimer then
1197 local timeNow = GetTime()
1198 local remaining = minTimer.time - timeNow
1199 if remaining <= 0 then
1200 self:handle()
1201 elseif not self.callbackTimes[minTimer.time] then
1202 for time,_ in pairs(self.callbackTimes) do
1203 if time < timeNow then
1204 self.callbackTimes[time] = nil
1205 end
1206 end
1207 self.callbackTimes[minTimer.time] = true
1208
1209 -- Note: This 0.001 avoids early callbacks that I ran into
1210 remaining = remaining + 0.001
1211 prdebug(DEBUG_TIMER, "Setting callback for handling timers after", remaining, "seconds")
1212 C_Timer.After(remaining, function() self:handle() end)
1213 end
1214 end
1215end
1216
1217function ZT.timers:handle()
1218 local timeNow = GetTime()
1219 local heap = self.heap
1220 local minTimer = heap[1]
1221
1222 prdebug(DEBUG_TIMER, "Handling timers at time", timeNow, "( Min @", minTimer and minTimer.time or "NONE", ")")
1223 while minTimer and minTimer.time <= timeNow do
1224 local heapSize = #heap
1225 if heapSize > 1 then
1226 heap[1] = heap[heapSize]
1227 heap[1].index = 1
1228 heap[heapSize] = nil
1229 self:fixHeapDownwards(1)
1230 else
1231 heap[1] = nil
1232 end
1233
1234 minTimer.index = -1
1235 minTimer.callback()
1236
1237 minTimer = heap[1]
1238 end
1239
1240 self:setupCallback()
1241end
1242
1243function ZT.timers:add(time, callback)
1244 local heap = self.heap
1245
1246 local index = #heap + 1
1247 local timer = {time=time, callback=callback, index=index}
1248 heap[index] = timer
1249
1250 self:fixHeapUpwards(index)
1251 self:setupCallback()
1252
1253 return timer
1254end
1255
1256function ZT.timers:cancel(timer)
1257 local index = timer.index
1258 if index == -1 then
1259 return
1260 end
1261
1262 timer.index = -1
1263
1264 local heap = self.heap
1265 local heapSize = #heap
1266 if heapSize ~= index then
1267 heap[index] = heap[heapSize]
1268 heap[index].index = index
1269 heap[heapSize] = nil
1270 self:fixHeapDownwards(index)
1271 self:setupCallback()
1272 else
1273 heap[index] = nil
1274 end
1275end
1276
1277function ZT.timers:update(timer, time)
1278 local fixHeapFunc = (time <= timer.time) and self.fixHeapUpwards or self.fixHeapDownwards
1279 timer.time = time
1280
1281 fixHeapFunc(self, timer.index)
1282 self:setupCallback()
1283end
1284
1285--##############################################################################
1286-- Managing the set of spells that are being watched
1287
1288local WatchInfo = { nextID = 1 }
1289local WatchInfoMT = { __index = WatchInfo }
1290
1291ZT.watching = {}
1292
1293function WatchInfo:create(member, spellInfo, isHidden)
1294 local time = GetTime()
1295 local watchInfo = {
1296 id = self.nextID,
1297 member = member,
1298 spellInfo = spellInfo,
1299 duration = spellInfo.cd,
1300 expiration = time,
1301 activeDuration = spellInfo.active and spellInfo.active.duration or nil,
1302 activeExpiration = time,
1303 charges = spellInfo.charges,
1304 maxCharges = spellInfo.charges,
1305 isHidden = isHidden,
1306 isLazy = spellInfo.isLazy,
1307 ignoreSharing = false,
1308 }
1309 self.nextID = self.nextID + 1
1310
1311 watchInfo = setmetatable(watchInfo, WatchInfoMT)
1312 watchInfo:updateModifiers()
1313
1314 return watchInfo
1315end
1316
1317function WatchInfo:updateModifiers()
1318 if not self.spellInfo.mods then
1319 return
1320 end
1321
1322 self.duration = self.spellInfo.cd
1323 self.charges = self.spellInfo.charges
1324 self.maxCharges = self.spellInfo.charges
1325
1326 for _,modifier in ipairs(self.spellInfo.mods) do
1327 if modifier.mod.type == "Static" then
1328 if self.member:checkRequirements(modifier.reqs) then
1329 modifier.mod.func(self)
1330 end
1331 end
1332 end
1333end
1334
1335function WatchInfo:sendAddEvent()
1336 if not self.isLazy and not self.isHidden then
1337 local spellInfo = self.spellInfo
1338 prdebug(DEBUG_EVENT, "Sending ZT_ADD", spellInfo.type, self.id, self.member.name, spellInfo.id, self.duration, self.charges)
1339 WeakAuras.ScanEvents("ZT_ADD", spellInfo.type, self.id, self.member, spellInfo.id, self.duration, self.charges)
1340
1341 if self.expiration > GetTime() then
1342 self:sendTriggerEvent()
1343 end
1344 end
1345end
1346
1347function WatchInfo:sendTriggerEvent()
1348 if self.isLazy then
1349 self.isLazy = false
1350 self:sendAddEvent()
1351 end
1352
1353 if not self.isHidden then
1354 prdebug(DEBUG_EVENT, "Sending ZT_TRIGGER", self.spellInfo.type, self.id, self.duration, self.expiration, self.charges, self.activeDuration, self.activeExpiration)
1355 WeakAuras.ScanEvents("ZT_TRIGGER", self.spellInfo.type, self.id, self.duration, self.expiration, self.charges, self.activeDuration, self.activeExpiration)
1356 end
1357end
1358
1359function WatchInfo:sendRemoveEvent()
1360 if not self.isLazy and not self.isHidden then
1361 prdebug(DEBUG_EVENT, "Sending ZT_REMOVE", self.spellInfo.type, self.id)
1362 WeakAuras.ScanEvents("ZT_REMOVE", self.spellInfo.type, self.id)
1363 end
1364end
1365
1366function WatchInfo:hide()
1367 if not self.isHidden then
1368 self:sendRemoveEvent()
1369 self.isHidden = true
1370 end
1371end
1372
1373function WatchInfo:unhide(suppressAddEvent)
1374 if self.isHidden then
1375 self.isHidden = false
1376 if not suppressAddEvent then
1377 self:sendAddEvent()
1378 end
1379 end
1380end
1381
1382function WatchInfo:toggleHidden(toggle, suppressAddEvent)
1383 if toggle then
1384 self:hide()
1385 else
1386 self:unhide(suppressAddEvent)
1387 end
1388end
1389
1390function WatchInfo:handleReadyTimer()
1391 if self.charges then
1392 self.charges = self.charges + 1
1393
1394 -- If we are not at max charges, update expiration and start a ready timer
1395 if self.charges < self.maxCharges then
1396 self.expiration = self.expiration + self.duration
1397 prdebug(DEBUG_TIMER, "Adding ready timer of", self.expiration, "for spellID", self.spellInfo.id)
1398 self.readyTimer = ZT.timers:add(self.expiration, function() self:handleReadyTimer() end)
1399 else
1400 self.readyTimer = nil
1401 end
1402 else
1403 self.readyTimer = nil
1404 end
1405
1406 self:sendTriggerEvent()
1407end
1408
1409function WatchInfo:updateReadyTimer() -- Returns true if a timer was set, false if handled immediately
1410 if self.expiration > GetTime() then
1411 if self.readyTimer then
1412 prdebug(DEBUG_TIMER, "Updating ready timer from", self.readyTimer.time, "to", self.expiration, "for spellID", self.spellInfo.id)
1413 ZT.timers:update(self.readyTimer, self.expiration)
1414 else
1415 prdebug(DEBUG_TIMER, "Adding ready timer of", self.expiration, "for spellID", self.spellInfo.id)
1416 self.readyTimer = ZT.timers:add(self.expiration, function() self:handleReadyTimer() end)
1417 end
1418
1419 return true
1420 else
1421 if self.readyTimer then
1422 prdebug(DEBUG_TIMER, "Canceling ready timer for spellID", self.spellInfo.id)
1423 ZT.timers:cancel(self.readyTimer)
1424 self.readyTimer = nil
1425 end
1426
1427 self:handleReadyTimer(self.expiration)
1428 return false
1429 end
1430end
1431
1432function WatchInfo:handleActiveTimer()
1433 self.activeTimer = nil
1434 self:sendTriggerEvent()
1435 if self.member.isPlayer then
1436 ZT:sendCDUpdate(self, true)
1437 end
1438end
1439
1440function WatchInfo:updateActiveTimer() -- Returns true if a timer was set, false if handled immediately
1441 if self.activeExpiration > GetTime() then
1442 if self.activeTimer then
1443 prdebug(DEBUG_TIMER, "Updating active timer from", self.activeTimer.time, "to", self.activeExpiration, "for spellID", self.spellInfo.id)
1444 ZT.timers:update(self.activeTimer, self.activeExpiration)
1445 else
1446 prdebug(DEBUG_TIMER, "Adding active timer of", self.expiration, "for spellID", self.spellInfo.id)
1447 self.activeTimer = ZT.timers:add(self.activeExpiration, function() self:handleActiveTimer() end)
1448 end
1449
1450 return true
1451 else
1452 if self.activeTimer then
1453 prdebug(DEBUG_TIMER, "Canceling active timer for spellID", self.spellInfo.id)
1454 ZT.timers:cancel(self.activeTimer)
1455 self.activeTimer = nil
1456 end
1457
1458 self:handleActiveTimer()
1459 return false
1460 end
1461end
1462
1463local function GetActiveInfo(member, activeSpellID)
1464 for a=1,40 do
1465 local name,_,_,_,duration,expirationTime,_,_,_,spellID = UnitAura(member.unit, a)
1466 if spellID == activeSpellID then
1467 return duration, expirationTime
1468 elseif not name then
1469 return
1470 end
1471 end
1472end
1473
1474function WatchInfo:updateActive(time)
1475 local active = self.spellInfo.active
1476 if not active then
1477 return
1478 end
1479
1480 if not time then
1481 time = GetTime()
1482 end
1483
1484 local activeSpellID = active.spellID
1485 local activeDefaultDuration = active.duration
1486
1487 if activeSpellID then
1488 self.activeDuration, self.activeExpiration = GetActiveInfo(self.member, activeSpellID)
1489 else
1490 self.activeDuration = activeDefaultDuration
1491 self.activeExpiration = time + activeDefaultDuration
1492 self:updateActiveTimer()
1493 end
1494end
1495
1496function WatchInfo:startCD()
1497 local time = GetTime()
1498
1499 if self.charges then
1500 if self.charges == 0 or self.charges == self.maxCharges then
1501 self.expiration = time + self.duration
1502 self:updateReadyTimer()
1503 end
1504
1505 if self.charges > 0 then
1506 self.charges = self.charges - 1
1507 end
1508 else
1509 self.expiration = time + self.duration
1510 self:updateReadyTimer()
1511 end
1512
1513 self:updateActive(time)
1514 self:sendTriggerEvent()
1515end
1516
1517function WatchInfo:updateCDDelta(delta)
1518 self.expiration = self.expiration + delta
1519
1520 local time = GetTime()
1521 local remaining = self.expiration - time
1522
1523 if self.charges and remaining <= 0 then
1524 local chargesGained = 1 - floor(remaining / self.duration)
1525 self.charges = max(self.charges + chargesGained, self.maxCharges)
1526 if self.charges == self.maxCharges then
1527 self.expiration = time
1528 else
1529 self.expiration = self.expiration + (chargesGained * self.duration)
1530 end
1531 end
1532
1533 if self:updateReadyTimer() then
1534 self:sendTriggerEvent()
1535 end
1536end
1537
1538function WatchInfo:updateCDRemaining(remaining)
1539 -- Note: This assumes that when remaining is 0 and the spell uses charges then it gains a charge
1540 if self.charges and remaining == 0 then
1541 if self.charges < self.maxCharges then
1542 self.charges = self.charges + 1
1543 end
1544
1545 -- Below maximum charges the expiration time doesn't change
1546 if self.charges < self.maxCharges then
1547 self:sendTriggerEvent()
1548 else
1549 self.expiration = GetTime()
1550 self:updateReadyTimer()
1551 end
1552 else
1553 self.expiration = GetTime() + remaining
1554 if self:updateReadyTimer() then
1555 self:sendTriggerEvent()
1556 end
1557 end
1558end
1559
1560function WatchInfo:updatePlayerCharges()
1561 local charges, maxCharges = GetSpellCharges(self.spellInfo.id)
1562 if charges then
1563 self.charges = charges
1564 self.maxCharges = maxCharges
1565 end
1566end
1567
1568function WatchInfo:updatePlayerCD(spellID, ignoreIfReady)
1569 local startTime, duration, enabled, charges, chargesUsed
1570 if self.charges then
1571 charges, self.maxCharges, startTime, duration = GetSpellCharges(spellID)
1572 if charges == self.maxCharges then
1573 startTime = 0
1574 end
1575 chargesUsed = self.charges > charges
1576 self.charges = charges
1577 enabled = 1
1578 else
1579 startTime, duration, enabled = GetSpellCooldown(spellID)
1580 chargesUsed = false
1581 end
1582
1583 if enabled ~= 0 then
1584 local time = GetTime()
1585 local ignoreRateLimit
1586 if startTime ~= 0 then
1587 if (self.expiration <= time) or chargesUsed then
1588 ignoreRateLimit = true
1589 self:updateActive(time)
1590 end
1591
1592 self.duration = duration
1593 self.expiration = startTime + duration
1594 else
1595 ignoreRateLimit = true
1596 self.expiration = time
1597 end
1598
1599 if (not ignoreIfReady) or (startTime ~= 0) then
1600 ZT:sendCDUpdate(self, ignoreRateLimit)
1601 self:sendTriggerEvent()
1602 end
1603 end
1604end
1605
1606function ZT:togglePlayerHandlers(watchInfo, enable)
1607 local spellInfo = watchInfo.spellInfo
1608 local spellID = spellInfo.id
1609 local member = watchInfo.member
1610 local toggleHandlerFunc = enable and self.eventHandlers.add or self.eventHandlers.remove
1611
1612 if enable then
1613 WeakAuras.WatchSpellCooldown(spellID)
1614 end
1615 toggleHandlerFunc(self.eventHandlers, "SPELL_COOLDOWN_CHANGED", spellID, 0, watchInfo.updatePlayerCD, watchInfo)
1616
1617 local links = self.separateLinkedSpellIDs[spellID]
1618 if links then
1619 for _,linkedSpellID in ipairs(links) do
1620 if enable then
1621 WeakAuras.WatchSpellCooldown(linkedSpellID)
1622 end
1623 toggleHandlerFunc(self.eventHandlers, "SPELL_COOLDOWN_CHANGED", linkedSpellID, 0, watchInfo.updatePlayerCD, watchInfo)
1624 end
1625 end
1626
1627 -- Handling any dynamic modifiers that are always required (with the 'force' tag)
1628 if spellInfo.mods then
1629 for _,modifier in ipairs(spellInfo.mods) do
1630 if modifier.mod.type == "Dynamic" then
1631 if not enable or member:checkRequirements(modifier.reqs) then
1632 for _,handlerInfo in ipairs(modifier.mod.handlers) do
1633 if handlerInfo.force then
1634 toggleHandlerFunc(self.eventHandlers, handlerInfo.type, handlerInfo.spellID, member.GUID, handlerInfo.handler, watchInfo)
1635 end
1636 end
1637 end
1638 end
1639 end
1640 end
1641end
1642
1643function ZT:toggleCombatLogHandlers(watchInfo, enable)
1644 local spellInfo = watchInfo.spellInfo
1645 local spellID = spellInfo.id
1646 local member = watchInfo.member
1647 local toggleHandlerFunc = enable and self.eventHandlers.add or self.eventHandlers.remove
1648
1649 if not spellInfo.ignoreCast then
1650 toggleHandlerFunc(self.eventHandlers, "SPELL_CAST_SUCCESS", spellID, member.GUID, watchInfo.startCD, watchInfo)
1651
1652 local links = self.linkedSpellIDs[spellID]
1653 if links then
1654 for _,linkedSpellID in ipairs(links) do
1655 toggleHandlerFunc(self.eventHandlers, "SPELL_CAST_SUCCESS", linkedSpellID, member.GUID, watchInfo.startCD, watchInfo)
1656 end
1657 end
1658 end
1659
1660 if spellInfo.mods then
1661 for _,modifier in ipairs(spellInfo.mods) do
1662 if modifier.mod.type == "Dynamic" then
1663 if not enable or member:checkRequirements(modifier.reqs) then
1664 for _,handlerInfo in ipairs(modifier.mod.handlers) do
1665 toggleHandlerFunc(self.eventHandlers, handlerInfo.type, handlerInfo.spellID, member.GUID, handlerInfo.handler, watchInfo)
1666 end
1667 end
1668 end
1669 end
1670 end
1671end
1672
1673function ZT:watch(spellInfo, member)
1674 -- Only handle registered spells (or those for the player)
1675 if not spellInfo.isRegistered and not member.isPlayer then
1676 return
1677 end
1678
1679 -- Only handle spells that meet all the requirements for the member
1680 if not member:checkRequirements(spellInfo.reqs) then
1681 return
1682 end
1683
1684 local spellID = spellInfo.id
1685 local spells = self.watching[spellID]
1686 if not spells then
1687 spells = {}
1688 self.watching[spellID] = spells
1689 end
1690
1691 local isHidden = (member.isPlayer and not spellInfo.isRegistered) or member.isHidden
1692
1693 local watchInfo = spells[member.GUID]
1694 local isNew = (watchInfo == nil)
1695 if not watchInfo then
1696 watchInfo = WatchInfo:create(member, spellInfo, isHidden)
1697 spells[member.GUID] = watchInfo
1698 member.watching[spellID] = watchInfo
1699 else
1700 -- If the type changed, send a remove event
1701 if not isHidden and spellInfo.type ~= watchInfo.spellInfo.type then
1702 watchInfo:sendRemoveEvent()
1703 end
1704 watchInfo.spellInfo = spellInfo
1705 watchInfo:updateModifiers()
1706 watchInfo:toggleHidden(isHidden, true) -- We will send the ZT_ADD event later
1707 end
1708
1709 if member.isPlayer then
1710 watchInfo:updatePlayerCharges()
1711 watchInfo:sendAddEvent()
1712
1713 watchInfo:updatePlayerCD(spellID, true)
1714
1715 local links = self.separateLinkedSpellIDs[spellID]
1716 if links then
1717 for _,linkedSpellID in ipairs(links) do
1718 watchInfo:updatePlayerCD(linkedSpellID, true)
1719 end
1720 end
1721 else
1722 watchInfo:sendAddEvent()
1723 end
1724
1725 if member.isPlayer and not TEST_CLEU then
1726 if isNew then
1727 self:togglePlayerHandlers(watchInfo, true)
1728 end
1729 elseif member.tracking == "CombatLog" or (member.tracking == "Sharing" and member.spellsVersion < spellInfo.version) then
1730 watchInfo.ignoreSharing = true
1731 if not isNew then
1732 self:toggleCombatLogHandlers(watchInfo, false)
1733 end
1734 self:toggleCombatLogHandlers(watchInfo, true)
1735 else
1736 watchInfo.ignoreSharing = false
1737 end
1738end
1739
1740function ZT:unwatch(spellInfo, member)
1741 -- Only handle registered spells (or those for the player)
1742 if not spellInfo.isRegistered and not member.isPlayer then
1743 return
1744 end
1745
1746 local spellID = spellInfo.id
1747 local sources = self.watching[spellID]
1748 if not sources then
1749 return
1750 end
1751
1752 local watchInfo = sources[member.GUID]
1753 if not watchInfo then
1754 return
1755 end
1756
1757 -- Ignoring unwatch requests if the spellInfo doesn't match (yet spellID does)
1758 -- (Note: This serves to ease updating after spec/talent changes)
1759 if watchInfo.spellInfo ~= spellInfo then
1760 return
1761 end
1762
1763 if member.isPlayer and not TEST_CLEU then
1764 -- If called due to front-end unregistration, only hide it to allow continued sharing of updates
1765 -- Otherwise, called due to a spec/talent change, so actually unwatch it
1766 if not spellInfo.isRegistered then
1767 watchInfo:hide()
1768 return
1769 end
1770
1771 self:togglePlayerHandlers(watchInfo, false)
1772 elseif member.tracking == "CombatLog" or (member.tracking == "Sharing" and member.spellsVersion < spellInfo.version) then
1773 self:toggleCombatLogHandlers(watchInfo, false)
1774 end
1775
1776 if watchInfo.readyTimer then
1777 self.timers:cancel(watchInfo.readyTimer)
1778 end
1779
1780 sources[member.GUID] = nil
1781 member.watching[spellID] = nil
1782
1783 watchInfo:sendRemoveEvent()
1784end
1785
1786--##############################################################################
1787-- Tracking types registered by front-end WAs
1788
1789function ZT:registerSpells(frontendID, spells)
1790 for _,spellInfo in ipairs(spells) do
1791 local frontends = spellInfo.frontends
1792 if next(frontends, nil) ~= nil then
1793 -- Some front-end already registered for this spell, so just send ADD events
1794 local watched = self.watching[spellInfo.id]
1795 if watched then
1796 for _,watchInfo in pairs(watched) do
1797 if watchInfo.spellInfo == spellInfo then
1798 watchInfo:sendAddEvent()
1799 end
1800 end
1801 end
1802 else
1803 -- No front-end was registered for this spell, so watch as needed
1804 spellInfo.isRegistered = true
1805 for _,member in pairs(self.members) do
1806 if not member.isIgnored then
1807 self:watch(spellInfo, member)
1808 end
1809 end
1810 end
1811
1812 frontends[frontendID] = true
1813 end
1814end
1815
1816function ZT:unregisterSpells(frontendID, spells)
1817 for _,spellInfo in ipairs(spells) do
1818 local frontends = spellInfo.frontends
1819 frontends[frontendID] = nil
1820
1821 if next(frontends, nil) == nil then
1822 local watched = self.watching[spellInfo.id]
1823 if watched then
1824 for _,watchInfo in pairs(watched) do
1825 if watchInfo.spellInfo == spellInfo then
1826 self:unwatch(spellInfo, watchInfo.member)
1827 end
1828 end
1829 end
1830 spellInfo.isRegistered = false
1831 end
1832 end
1833end
1834
1835function ZT:toggleFrontEndRegistration(frontendID, info, toggle)
1836 local infoType = type(info)
1837 local registerFunc = toggle and self.registerSpells or self.unregisterSpells
1838
1839 if infoType == "string" then -- Registration info is a type
1840 prdebug(DEBUG_EVENT, "Received", toggle and "ZT_REGISTER" or "ZT_UNREGISTER", "from", frontendID, "for type", info)
1841 registerFunc(self, frontendID, self.spells["type"][info])
1842 elseif infoType == "number" then -- Registration info is a spellID
1843 prdebug(DEBUG_EVENT, "Received", toggle and "ZT_REGISTER" or "ZT_UNREGISTER", "from", frontendID, "for spellID", info)
1844 registerFunc(self, frontendID, self.spells["id"][info])
1845 elseif infoType == "table" then -- Registration info is a table of types or spellIDs
1846 infoType = type(info[1])
1847
1848 if infoType == "string" then
1849 prdebug(DEBUG_EVENT, "Received", toggle and "ZT_REGISTER" or "ZT_UNREGISTER", "from", frontendID, "for multiple types")
1850 for _,type in ipairs(info) do
1851 registerFunc(self, frontendID, self.spells["type"][type])
1852 end
1853 elseif infoType == "number" then
1854 prdebug(DEBUG_EVENT, "Received", toggle and "ZT_REGISTER" or "ZT_UNREGISTER", "from", frontendID, "for multiple spells")
1855 for _,spellID in ipairs(info) do
1856 registerFunc(self, frontendID, self.spells["id"][spellID])
1857 end
1858 end
1859 end
1860end
1861
1862function ZT:registerFrontEnd(frontendID, info)
1863 self:toggleFrontEndRegistration(frontendID, info, true)
1864end
1865
1866function ZT:unregisterFrontEnd(frontendID, info)
1867 self:toggleFrontEndRegistration(frontendID, info, false)
1868end
1869
1870--##############################################################################
1871-- Managing member information (e.g., spec, talents) for all group members
1872
1873local Member = { }
1874local MemberMT = { __index = Member }
1875
1876ZT.members = {}
1877ZT.inEncounter = false
1878
1879local membersToIgnore = {}
1880if ZT.config["ignoreList"] then
1881 local ignoreListStr = trim(ZT.config["ignoreList"])
1882 if ignoreListStr ~= "" then
1883 ignoreListStr = "return "..ignoreListStr
1884 local ignoreList = WeakAuras.LoadFunction(ignoreListStr, "ZenTracker Ignore List")
1885 if ignoreList and (type(ignoreList) == "table") then
1886 for i,name in ipairs(ignoreList) do
1887 if type(name) == "string" then
1888 membersToIgnore[strlower(name)] = true
1889 else
1890 prerror("Ignore list entry", i, "is not a string. Skipping...")
1891 end
1892 end
1893 else
1894 prerror("Ignore list is not in the form of a table. Usage: {\"Zenlia\", \"Cistara\"}")
1895 end
1896 end
1897end
1898
1899function Member:create(memberInfo)
1900 local member = memberInfo
1901 member.watching = {}
1902 member.tracking = member.tracking and member.tracking or "CombatLog"
1903 member.isPlayer = (member.GUID == UnitGUID("player"))
1904 member.isHidden = false
1905 member.isReady = false
1906
1907 return setmetatable(member, MemberMT)
1908end
1909
1910function Member:update(memberInfo)
1911 self.level = memberInfo.level or self.level
1912 self.specID = memberInfo.specID or self.specID
1913 self.talents = memberInfo.talents or self.talents
1914 self.talentsStr = memberInfo.talentsStr or self.talentsStr
1915 self.covenantID = memberInfo.covenantID or self.covenantID
1916 self.unit = memberInfo.unit or self.unit
1917 if memberInfo.tracking then
1918 self.tracking = memberInfo.tracking
1919 self.spellsVersion = memberInfo.spellsVersion
1920 self.protocolVersion = memberInfo.protocolVersion
1921 end
1922end
1923
1924function Member:gatherInfo()
1925 local _,className,_,race,_,name = GetPlayerInfoByGUID(self.GUID)
1926 self.name = name and gsub(name, "%-[^|]+", "") or nil
1927 self.class = className and AllClasses[className] or nil
1928 self.classID = className and AllClasses[className].ID or nil
1929 self.classColor = className and RAID_CLASS_COLORS[className] or nil
1930 self.race = race
1931 self.level = self.unit and UnitLevel(self.unit) or -1
1932
1933 if (self.tracking == "Sharing") and self.name then
1934 prdebug(DEBUG_TRACKING, self.name, "is using ZenTracker with spell list version", self.spellsVersion)
1935 end
1936
1937 if self.name and membersToIgnore[strlower(self.name)] then
1938 self.isIgnored = true
1939 return false
1940 end
1941
1942 if self.isPlayer then
1943 self.covenantID = ZT:updateCovenantInfo()
1944 end
1945
1946 self.isReady = (self.name ~= nil) and (self.classID ~= nil) and (self.race ~= nil) and (self.level >= 1)
1947 return self.isReady
1948end
1949
1950function Member:checkRequirements(reqs)
1951 if not reqs then
1952 return true
1953 end
1954
1955 for _,req in ipairs(reqs) do
1956 if not req.check(self) then
1957 return false
1958 end
1959 end
1960 return true
1961end
1962
1963function Member:hide()
1964 if not self.isHidden and not self.isPlayer then
1965 self.isHidden = true
1966 for _,watchInfo in pairs(self.watching) do
1967 watchInfo:hide()
1968 end
1969 end
1970end
1971
1972function Member:unhide()
1973 if self.isHidden and not self.isPlayer then
1974 self.isHidden = false
1975 for _,watchInfo in pairs(self.watching) do
1976 watchInfo:unhide()
1977 end
1978 end
1979end
1980
1981-- TODO: Fix rare issue where somehow only talented spells are being shown?
1982function ZT:addOrUpdateMember(memberInfo)
1983 local member = self.members[memberInfo.GUID]
1984 if not member then
1985 member = Member:create(memberInfo)
1986 self.members[member.GUID] = member
1987 end
1988
1989 if member.isIgnored then
1990 return
1991 end
1992
1993 -- Determining which properties of the member have updated
1994 local isInitialUpdate = not member.isReady and member:gatherInfo()
1995 local isLevelUpdate = memberInfo.level and (memberInfo.level ~= member.level)
1996 local isSpecUpdate = memberInfo.specID and (memberInfo.specID ~= member.specID)
1997 local isTalentUpdate = false
1998 if memberInfo.talents then
1999 for talent,_ in pairs(memberInfo.talents) do
2000 if member.talents[talent] == nil then
2001 isTalentUpdate = true
2002 break
2003 end
2004 end
2005 end
2006 local isCovenantUpdate = memberInfo.covenantID and (memberInfo.covenantID ~= member.covenantID)
2007
2008 if member.isReady and (isInitialUpdate or isLevelUpdate or isSpecUpdate or isTalentUpdate or isCovenantUpdate) then
2009 local prevSpecID = member.specID
2010 local prevTalents = member.talents or {}
2011 local prevCovenantID = member.covenantID
2012 member:update(memberInfo)
2013
2014 -- This handshake should come before any cooldown updates for newly watched spells
2015 if member.isPlayer then
2016 self:sendHandshake()
2017 end
2018
2019 -- If we are in an encounter, hide the member if they are outside the player's instance
2020 -- (Note: Previously did this on member creation, which seemed to introduce false positives)
2021 if isInitialUpdate and self.inEncounter and (not member.isPlayer) then
2022 local _,_,_,instanceID = UnitPosition("player")
2023 local _,_,_,mInstanceID = UnitPosition(member.unit)
2024 if instanceID ~= mInstanceID then
2025 member:hide()
2026 end
2027 end
2028
2029 -- Generic Spells + Class Spells + Race Spells
2030 -- Note: These are set once and never change
2031 if isInitialUpdate then
2032 for _,spellInfo in ipairs(self.spells["generic"]) do
2033 self:watch(spellInfo, member)
2034 end
2035 for _,spellInfo in ipairs(self.spells["race"][member.race]) do
2036 self:watch(spellInfo, member)
2037 end
2038 for _,spellInfo in ipairs(self.spells["class"][member.classID]) do
2039 self:watch(spellInfo, member)
2040 end
2041 end
2042
2043 -- Leveling (No need to handle on initial update)
2044 if isLevelUpdate then
2045 for _,spellInfo in ipairs(self.spells["level"][member.level]) do
2046 self:watch(spellInfo, member)
2047 end
2048 end
2049
2050 -- Specialization Spells
2051 if (isInitialUpdate or isSpecUpdate) and member.specID then
2052 for _,spellInfo in ipairs(self.spells["spec"][member.specID]) do
2053 self:watch(spellInfo, member)
2054 end
2055
2056 if isSpecUpdate and prevSpecID then
2057 for _,spellInfo in ipairs(self.spells["spec"][prevSpecID]) do
2058 if not member:checkRequirements(spellInfo.reqs) then
2059 self:unwatch(spellInfo, member)
2060 end
2061 end
2062 end
2063 end
2064
2065 -- Talented Spells
2066 if (isInitialUpdate or isTalentUpdate) and member.talents then
2067 -- Handling talents that were just selected
2068 for talent,_ in pairs(member.talents) do
2069 if isInitialUpdate or not prevTalents[talent] then
2070 for _,spellInfo in ipairs(self.spells["talent"][talent]) do
2071 self:watch(spellInfo, member)
2072 end
2073 for _,spellInfo in ipairs(self.spells["notalent"][talent]) do
2074 if not member:checkRequirements(spellInfo.reqs) then
2075 self:unwatch(spellInfo, member)
2076 end
2077 end
2078 end
2079 end
2080
2081 -- Handling talents that were just unselected
2082 if not isInitialUpdate then
2083 for talent,_ in pairs(prevTalents) do
2084 if not member.talents[talent] then
2085 for _,spellInfo in ipairs(self.spells["talent"][talent]) do
2086 if not member:checkRequirements(spellInfo.reqs) then
2087 self:unwatch(spellInfo, member) -- Talent was required
2088 else
2089 self:watch(spellInfo, member) -- Talent was a modifier
2090 end
2091 end
2092 for _,spellInfo in ipairs(self.spells["notalent"][talent]) do
2093 self:watch(spellInfo, member)
2094 end
2095 end
2096 end
2097 end
2098 end
2099
2100 -- Covenant Spells
2101 if (isInitialUpdate or isCovenantUpdate) and member.covenantID then
2102 for _,spellInfo in ipairs(self.spells["covenant"][member.covenantID]) do
2103 self:watch(spellInfo, member)
2104 end
2105
2106 if isCovenantUpdate and prevCovenantID then
2107 for _,spellInfo in ipairs(self.spells["covenant"][prevCovenantID]) do
2108 if not member:checkRequirements(spellInfo.reqs) then
2109 self:unwatch(spellInfo, member)
2110 end
2111 end
2112 end
2113 end
2114 end
2115
2116 -- If tracking changed from "CombatLog" to "Sharing", remove unnecessary event handlers and send a handshake/updates
2117 if (member.tracking == "CombatLog") and (memberInfo.tracking == "Sharing") then
2118 member.tracking = "Sharing"
2119 member.spellsVersion = memberInfo.spellsVersion
2120
2121 if member.name then
2122 prdebug(DEBUG_TRACKING, member.name, "is using ZenTracker with spell list version", member.spellsVersion)
2123 end
2124
2125 for _,watchInfo in pairs(member.watching) do
2126 if watchInfo.spellInfo.version <= member.spellsVersion then
2127 watchInfo.ignoreSharing = false
2128 self:toggleCombatLogHandlers(watchInfo, false)
2129 end
2130 end
2131
2132 self:sendHandshake()
2133 local time = GetTime()
2134 for _,watchInfo in pairs(self.members[UnitGUID("player")].watching) do
2135 if watchInfo.expiration > time then
2136 self:sendCDUpdate(watchInfo)
2137 end
2138 end
2139 end
2140end
2141
2142--##############################################################################
2143-- Handling raid and M+ encounters
2144
2145function ZT:resetEncounterCDs()
2146 for _,member in pairs(self.members) do
2147 local resetMemberCDs = not member.isPlayer and member.tracking ~= "Sharing"
2148
2149 for _,watchInfo in pairs(member.watching) do
2150 if resetMemberCDs and watchInfo.duration >= 180 then
2151 watchInfo.charges = watchInfo.maxCharges
2152 watchInfo:updateCDRemaining(0)
2153 end
2154
2155 -- If spell uses lazy tracking and it was triggered, reset lazy tracking at this point
2156 if watchInfo.spellInfo.isLazy and not watchInfo.isLazy then
2157 watchInfo:sendRemoveEvent()
2158 watchInfo.isLazy = true
2159 end
2160 end
2161 end
2162end
2163
2164function ZT:startEncounter(event)
2165 self.inEncounter = true
2166
2167 local _,_,_,instanceID = UnitPosition("player")
2168 for _,member in pairs(self.members) do
2169 local _,_,_,mInstanceID = UnitPosition(self.inspectLib:GuidToUnit(member.GUID))
2170 if mInstanceID ~= instanceID then
2171 member:hide()
2172 else
2173 member:unhide() -- Note: Shouldn't be hidden, but just in case...
2174 end
2175 end
2176
2177 if event == "CHALLENGE_MODE_START" then
2178 self:resetEncounterCDs()
2179 end
2180end
2181
2182function ZT:endEncounter(event)
2183 if self.inEncounter then
2184 self.inEncounter = false
2185 for _,member in pairs(self.members) do
2186 member:unhide()
2187 end
2188 end
2189
2190 if event == "ENCOUNTER_END" then
2191 self:resetEncounterCDs()
2192 end
2193end
2194
2195--##############################################################################
2196-- Public functions for other addons/auras to query ZenTracker information
2197-- Note: This API is subject to change at any time (for now)
2198
2199-- Parameters:
2200-- type (string) -> Filter by a specific spell type (e.g., "IMMUNITY")
2201-- spellIDs (map<number, bool>) -> Filter by a specific set of spell IDs (e.g., {[642]=true, [1022]=true})
2202-- unitOrGUID (string) -> Filter by a specific member, as specified by a GUID or current unit (e.g., "player")
2203-- available (bool) -> Filters by whether a spell is available for use or not (e.g., true)
2204-- (Note: Set parameters to nil if they should be ignored)
2205-- Return Value:
2206-- Array containing tables with the following keys: spellID, member, expiration, charges, activeExpiration
2207local function Public_Query(type, spellIDs, unitOrGUID, available)
2208 local results = {}
2209
2210 local members
2211 if unitOrGUID then
2212 local GUID = UnitGUID(unitOrGUID) or unitOrGUID
2213 if GUID and ZT.members[GUID] then
2214 members = {[GUID]=ZT.members[GUID]}
2215 else
2216 return results
2217 end
2218 else
2219 members = ZT.members
2220 end
2221
2222 local time = GetTime()
2223 for _,member in pairs(members) do
2224 for _,watchInfo in pairs(member.watching) do
2225 local spellInfo = watchInfo.spellInfo
2226 if (not type or spellInfo.type == type) and (not spellIDs or spellIDs[spellInfo.id]) and (available == nil or (watchInfo.expiration <= time or (watchInfo.charges and watchInfo.charges > 0)) == available) then
2227 tinsert(results, {spellID = spellInfo.id, member = member, expiration = watchInfo.expiration, charges = watchInfo.charges, activeExpiration = watchInfo.activeExpiration})
2228 end
2229 end
2230 end
2231
2232 return results
2233end
2234
2235setglobal("ZenTracker_PublicFunctions", { query = Public_Query })
2236
2237--##############################################################################
2238-- Handling the exchange of addon messages with other ZT clients
2239--
2240-- Message Format = <Protocol Version (%d)>:<Message Type (%s)>:<Member GUID (%s)>...
2241-- Type = "H" (Handshake)
2242-- ...:<Spec ID (%d)>:<Talents (%s)>:<IsInitial? (%d)>:<Spells Version (%d)>:<Covenant ID (%d)>
2243-- Type = "U" (CD Update)
2244-- ...:<Spell ID (%d)>:<Duration (%f)>:<Remaining (%f)>:<#Charges (%d)>:<Active Duration (%f)>:<Active Remaining (%f)>
2245
2246ZT.protocolVersion = 4
2247
2248ZT.timeBetweenHandshakes = 5 --seconds
2249ZT.timeOfNextHandshake = 0
2250ZT.handshakeTimer = nil
2251
2252ZT.timeBetweenCDUpdates = 5 --seconds (per spellID)
2253ZT.timeOfNextCDUpdate = {}
2254ZT.updateTimers = {}
2255
2256local function sendMessage(message)
2257 prdebug(DEBUG_MESSAGE, "Sending message '"..message.."'")
2258
2259 if not IsInGroup() and not IsInRaid() then
2260 return
2261 end
2262
2263 local channel = IsInGroup(2) and "INSTANCE_CHAT" or "RAID"
2264 C_ChatInfo.SendAddonMessage("ZenTracker", message, channel)
2265end
2266
2267ZT.hasSentHandshake = false
2268function ZT:sendHandshake()
2269 local time = GetTime()
2270 if time < self.timeOfNextHandshake then
2271 if not self.handshakeTimer then
2272 self.handshakeTimer = self.timers:add(self.timeOfNextHandshake, function() self:sendHandshake() end)
2273 end
2274 return
2275 end
2276
2277 local GUID = UnitGUID("player")
2278 if not self.members[GUID] then
2279 return -- This may happen when rejoining a group after login, so ignore this attempt to send a handshake
2280 end
2281
2282 local member = self.members[GUID]
2283 local specID = member.specID or 0
2284 local talents = member.talentsStr or ""
2285 local isInitial = self.hasSentHandshake and 0 or 1
2286 local covenantID = member.covenantID or 0
2287 local message = string.format("%d:H:%s:%d:%s:%d:%d:%d", self.protocolVersion, GUID, specID, talents, isInitial, self.spellListVersion, covenantID)
2288 sendMessage(message)
2289
2290 self.hasSentHandshake = true
2291 self.timeOfNextHandshake = time + self.timeBetweenHandshakes
2292 if self.handshakeTimer then
2293 self.timers:cancel(self.handshakeTimer)
2294 self.handshakeTimer = nil
2295 end
2296end
2297
2298function ZT:sendCDUpdate(watchInfo, ignoreRateLimit)
2299 local spellID = watchInfo.spellInfo.id
2300 local time = GetTime()
2301
2302 local timer = self.updateTimers[spellID]
2303 if ignoreRateLimit then
2304 if timer then
2305 self.timers:cancel(timer)
2306 self.updateTimers[spellID] = nil
2307 end
2308 elseif timer then
2309 return
2310 else
2311 local timeOfNextCDUpdate = self.timeOfNextCDUpdate[spellID]
2312 if timeOfNextCDUpdate and (time < timeOfNextCDUpdate) then
2313 self.updateTimers[spellID] = self.timers:add(timeOfNextCDUpdate, function() self:sendCDUpdate(watchInfo, true) end)
2314 return
2315 end
2316 end
2317
2318 local message
2319 local GUID = watchInfo.member.GUID
2320 local duration = watchInfo.duration
2321 local remaining = watchInfo.expiration - time
2322 if remaining < 0 then
2323 remaining = 0
2324 end
2325 local charges = watchInfo.charges and tostring(watchInfo.charges) or "-"
2326 local activeDuration = watchInfo.activeDuration
2327 if activeDuration then
2328 local activeRemaining = watchInfo.activeExpiration - time
2329 if activeRemaining < 0 then
2330 activeRemaining = 0
2331 end
2332 message = string.format("%d:U:%s:%d:%0.2f:%0.2f:%s:%0.2f:%0.2f", self.protocolVersion, GUID, spellID, duration, remaining, charges, activeDuration, activeRemaining)
2333 else
2334 message = string.format("%d:U:%s:%d:%0.2f:%0.2f:%s", self.protocolVersion, GUID, spellID, duration, remaining, charges)
2335 end
2336 sendMessage(message)
2337
2338 self.timeOfNextCDUpdate[spellID] = time + self.timeBetweenCDUpdates
2339end
2340
2341function ZT:handleHandshake(version, mGUID, specID, talentsStr, isInitial, spellsVersion, covenantID)
2342 -- Protocol V4: Ignore any earlier versions due to substantial changes (talents)
2343 if version < 4 then
2344 return
2345 end
2346
2347 specID = tonumber(specID)
2348 if specID == 0 then
2349 specID = nil
2350 end
2351
2352 local talents = {}
2353 if talents ~= "" then
2354 for index in talentsStr:gmatch("%d+") do
2355 index = tonumber(index)
2356 talents[index] = true
2357 end
2358 end
2359
2360 -- Protocol V2: Assume false if not present
2361 if isInitial == "1" then
2362 isInitial = true
2363 else
2364 isInitial = false
2365 end
2366
2367 -- Protocol V2: Assume spellsVersion is 1 if not present
2368 if spellsVersion then
2369 spellsVersion = tonumber(spellsVersion)
2370 if not spellsVersion then
2371 spellsVersion = 1
2372 end
2373 else
2374 spellsVersion = 1
2375 end
2376
2377 -- Protocol V4: Assume covenantID is nil if not present
2378 covenantID = tonumber(covenantID)
2379 if covenantID == 0 then
2380 covenantID = nil
2381 end
2382
2383 local memberInfo = {
2384 GUID = mGUID,
2385 specID = specID,
2386 talents = talents,
2387 talentsStr = talentsStr,
2388 covenantID = covenantID,
2389 tracking = "Sharing",
2390 protocolVersion = version,
2391 spellsVersion = spellsVersion,
2392 }
2393
2394 self:addOrUpdateMember(memberInfo)
2395 if isInitial then
2396 self:sendHandshake()
2397 end
2398end
2399
2400function ZT:handleCDUpdate(version, mGUID, spellID, duration, remaining, charges, activeDuration, activeRemaining)
2401 local member = self.members[mGUID]
2402 if not member or not member.isReady then
2403 return
2404 end
2405
2406 spellID = tonumber(spellID)
2407 duration = tonumber(duration)
2408 remaining = tonumber(remaining)
2409 if not spellID or not duration or not remaining then
2410 return
2411 end
2412
2413 local sources = self.watching[spellID]
2414 if sources then
2415 local watchInfo = sources[member.GUID]
2416 if not watchInfo or watchInfo.ignoreSharing then
2417 return
2418 end
2419
2420 local time = GetTime()
2421
2422 -- Protocol V3: Charges (Ignore if not present)
2423 charges = tonumber(charges)
2424 if charges then
2425 watchInfo.charges = charges
2426 end
2427
2428 -- Protocol V4: Active Duration/ Expiration (Assume default or inspect buff if not present)
2429 activeDuration = tonumber(activeDuration)
2430 activeRemaining = tonumber(activeRemaining)
2431 if activeDuration and activeRemaining then
2432 watchInfo.activeDuration = activeDuration
2433 watchInfo.activeExpiration = time + activeRemaining
2434 elseif watchInfo.spellInfo.active then
2435 watchInfo:updateActive(time)
2436 end
2437
2438 watchInfo.duration = duration
2439 watchInfo.expiration = time + remaining
2440 watchInfo:sendTriggerEvent()
2441 end
2442end
2443
2444function ZT:handleMessage(message)
2445 local version, type, mGUID, arg1, arg2, arg3, arg4, arg5, arg6 = strsplit(":", message)
2446 version = tonumber(version)
2447
2448 -- Ignore any messages sent by the player
2449 if mGUID == UnitGUID("player") then
2450 return
2451 end
2452
2453 prdebug(DEBUG_MESSAGE, "Received message '"..message.."'")
2454
2455 if type == "H" then -- Handshake
2456 self:handleHandshake(version, mGUID, arg1, arg2, arg3, arg4, arg5, arg6)
2457 elseif type == "U" then -- CD Update
2458 self:handleCDUpdate(version, mGUID, arg1, arg2, arg3, arg4, arg5, arg6)
2459 else
2460 return
2461 end
2462end
2463
2464if not C_ChatInfo.RegisterAddonMessagePrefix("ZenTracker") then
2465 prerror("Could not register addon message prefix. Defaulting to local-only cooldown tracking.")
2466end
2467
2468--##############################################################################
2469-- Callback functions for libGroupInspecT for updating/removing members
2470
2471ZT.delayedUpdates = {}
2472
2473function ZT:updateCovenantInfo()
2474 local covenantID = C_Covenants.GetActiveCovenantID()
2475 if covenantID == 0 then
2476 return
2477 end
2478
2479 -- local soulbindID = C_Soulbinds.GetActiveSoulbindID()
2480 -- local soulbindData = C_Soulbinds.GetSoulbindData(soulbindID)
2481 -- if soulbindData and soulbindData.tree and soulbindData.tree.nodes then
2482 -- for _,node in pairs(soulbindData.tree.nodes) do
2483 -- if node.state == 3 then
2484 -- if node.conduitID ~= 0 then
2485 -- -- Process node.conduitID, node.conduitRank
2486 -- else
2487 -- -- Process node.spellID
2488 -- end
2489 -- end
2490 -- end
2491 -- end
2492
2493 return covenantID
2494end
2495
2496function ZT:libInspectUpdate(_, GUID, _, info)
2497 local specID = info.global_spec_id
2498 if specID == 0 then
2499 specID = nil
2500 end
2501
2502 local talents = {}
2503 local talentsStr = ""
2504 if info.talents then
2505 for _,talent in pairs(info.talents) do
2506 if talent.spell_id then -- This is rarely nil, not sure why...
2507 talents[talent.spell_id] = true
2508 talentsStr = talentsStr..talent.spell_id..","
2509 end
2510 end
2511 end
2512
2513 local memberInfo = {
2514 GUID = GUID,
2515 unit = info.lku,
2516 specID = specID,
2517 talents = talents,
2518 talentsStr = strsub(talentsStr, 0, -2),
2519 }
2520
2521 if not self.delayedUpdates then
2522 self:addOrUpdateMember(memberInfo)
2523 else
2524 self.delayedUpdates[GUID] = memberInfo
2525 end
2526end
2527
2528function ZT:libInspectRemove(_, GUID)
2529 local member = self.members[GUID]
2530 if not member then
2531 return
2532 end
2533
2534 for _,watchInfo in pairs(member.watching) do
2535 self:unwatch(watchInfo.spellInfo, member)
2536 end
2537 self.members[GUID] = nil
2538end
2539
2540function ZT:handleDelayedUpdates()
2541 if self.delayedUpdates then
2542 for _,memberInfo in pairs(self.delayedUpdates) do
2543 self:addOrUpdateMember(memberInfo)
2544 end
2545 self.delayedUpdates = nil
2546 end
2547end
2548
2549ZT.inspectLib = LibStub:GetLibrary("LibGroupInSpecT-1.1", true)
2550
2551if ZT.inspectLib then
2552 local prevZT = _G["ZenTracker_AuraEnv"]
2553 if prevZT then
2554 ZT.inspectLib.UnregisterAllCallbacks(prevZT)
2555 if prevZT.timers then
2556 prevZT.timers.heap = {}
2557 end
2558 end
2559 _G["ZenTracker_AuraEnv"] = ZT
2560
2561 -- If prevZT exists, we know it wasn't a login or reload. If it doesn't exist,
2562 -- it still might not be a login or reload if the user is installing ZenTracker
2563 -- for the first time. IsLoginFinished() takes care of the second case.
2564 if prevZT or WeakAuras.IsLoginFinished() then
2565 ZT.delayedUpdates = nil
2566 end
2567
2568 ZT.inspectLib.RegisterCallback(ZT, "GroupInSpecT_Update", "libInspectUpdate")
2569 ZT.inspectLib.RegisterCallback(ZT, "GroupInSpecT_Remove", "libInspectRemove")
2570
2571 for unit in WA_IterateGroupMembers() do
2572 local GUID = UnitGUID(unit)
2573 if GUID then
2574 local info = ZT.inspectLib:GetCachedInfo(GUID)
2575 if info then
2576 ZT:libInspectUpdate("Init", GUID, unit, info)
2577 else
2578 ZT.inspectLib:Rescan(GUID)
2579 end
2580 end
2581 end
2582else
2583 prerror("LibGroupInSpecT-1.1 not found")
2584end
2585
2586