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