Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --------------------------------------------------------------------------------
- -- CONFIGURATION
- --
- -- The configuration options have moved to the "Author Options" tab as of
- -- WeakAuras Version 2.10.
- --
- -- DO NOT EDIT THIS CODE!
- --------------------------------------------------------------------------------
- local ZT = aura_env
- -- Local versions of commonly used functions
- local ipairs = ipairs
- local pairs = pairs
- local print = print
- local select = select
- local tonumber = tonumber
- local tinsert = tinsert
- local IsInGroup = IsInGroup
- local IsInRaid = IsInRaid
- local UnitGUID = UnitGUID
- -- Turns on/off debugging messages
- local DEBUG_EVENT = { isEnabled = false, color = "FF2281F4" }
- local DEBUG_MESSAGE = { isEnabled = false, color = "FF11D825" }
- local DEBUG_TIMER = { isEnabled = false, color = "FFF96D27" }
- local DEBUG_TRACKING = { isEnabled = false, color = "FFA53BF7" }
- -- Turns on/off testing of combatlog-based tracking for the player
- -- (Note: This will disable sharing of player CD updates over addon messages)
- local TEST_CLEU = false
- local function prdebug(type, ...)
- if type.isEnabled then
- print("|c"..type.color.."[ZT-Debug]", ...)
- end
- end
- local function prerror(...)
- print("|cFFFF0000[ZT-Error]", ...)
- end
- -- Utility functions for creating tables/maps
- local function DefaultTable_Create(genDefaultFunc)
- local metatable = {}
- metatable.__index = function(table, key)
- local value = genDefaultFunc()
- rawset(table, key, value)
- return value
- end
- return setmetatable({}, metatable)
- end
- local function Map_FromTable(table)
- local map = {}
- for _,value in ipairs(table) do
- map[value] = true
- end
- return map
- end
- --##############################################################################
- -- Class and Spec Information
- local DH = {ID=12, name="DEMONHUNTER", Havoc=577, Veng=581}
- local DK = {ID=6, name="DEATHKNIGHT", Blood=250, Frost=251, Unholy=252}
- local Druid = {ID=11, name="DRUID", Balance=102, Feral=103, Guardian=104, Resto=105}
- local Hunter = {ID=3, name="HUNTER", BM=253, MM=254, SV=255}
- local Mage = {ID=8, name="MAGE", Arcane=62, Fire=63, Frost=64}
- local Monk = {ID=10, name="MONK", BRM=268, WW=269, MW=270}
- local Paladin = {ID=2, name="PALADIN", Holy=65, Prot=66, Ret=70}
- local Priest = {ID=5, name="PRIEST", Disc=256, Holy=257, Shadow=258}
- local Rogue = {ID=4, name="ROGUE", Sin=259, Outlaw=260, Sub=261}
- local Shaman = {ID=7, name="SHAMAN", Ele=262, Enh=263, Resto=264}
- local Warlock = {ID=9, name="WARLOCK", Affl=265, Demo=266, Destro=267}
- local Warrior = {ID=1, name="WARRIOR", Arms=71, Fury=72, Prot=73}
- local AllClasses = {
- [DH.name] = DH, [DK.name] = DK, [Druid.name] = Druid, [Hunter.name] = Hunter,
- [Mage.name] = Mage, [Monk.name] = Monk, [Paladin.name] = Paladin,
- [Priest.name] = Priest, [Rogue.name] = Rogue, [Shaman.name] = Shaman,
- [Warlock.name] = Warlock, [Warrior.name] = Warrior
- }
- --##############################################################################
- -- Spell Requirements
- local function Requirement(type, check, indices)
- return { type = type, check = check, indices = indices }
- end
- local function LevelReq(minLevel)
- return Requirement("level", function(member) return member.level >= minLevel end, {minLevel})
- end
- local function RaceReq(race)
- return Requirement("race", function(member) return member.race == race end, {race})
- end
- local function ClassReq(class)
- return Requirement("class", function(member) return member.classID == class.ID end, {class.ID})
- end
- local function SpecReq(ids)
- local idsMap = Map_FromTable(ids)
- return Requirement("spec", function(member) return idsMap[member.specID] ~= nil end, ids)
- end
- local function TalentReq(id)
- return Requirement("talent", function(member) return member.talents[id] ~= nil end, {id})
- end
- local function NoTalentReq(id)
- return Requirement("notalent", function(member) return member.talents[id] == nil end, {id})
- end
- -- local function ItemReq(id)
- -- return Requirement("items", function(member) return false end)
- -- end
- local function CovenantReq(name)
- return Requirement("covenant", function(member) return false end)
- end
- --##############################################################################
- -- Spell Modifiers (Static and Dynamic)
- local function StaticMod(func)
- return { type = "Static", func = func }
- end
- local function SubtractMod(amount)
- return StaticMod(function(watchInfo) watchInfo.duration = watchInfo.duration - amount end)
- end
- local function MultiplyMod(coeff)
- return StaticMod(function(watchInfo) watchInfo.duration = watchInfo.duration * coeff end)
- end
- local function ChargesMod(amount)
- return StaticMod(function(watchInfo)
- watchInfo.charges = amount
- watchInfo.maxCharges = amount
- end)
- end
- local function DynamicMod(handlers)
- if handlers.type then
- handlers = { handlers }
- end
- return { type = "Dynamic", handlers = handlers }
- end
- local function EventDeltaMod(type, spellID, delta)
- return DynamicMod({
- type = type,
- spellID = spellID,
- handler = function(watchInfo)
- watchInfo:updateCDDelta(delta)
- end
- })
- end
- local function CastDeltaMod(spellID, delta)
- return EventDeltaMod("SPELL_CAST_SUCCESS", spellID, delta)
- end
- local function EventRemainingMod(type, spellID, remaining)
- return DynamicMod({
- type = type,
- spellID = spellID,
- handler = function(watchInfo)
- watchInfo:updateCDRemaining(remaining)
- end
- })
- end
- local function CastRemainingMod(spellID, remaining)
- return EventRemainingMod("SPELL_CAST_SUCCESS", spellID, remaining)
- end
- -- If Shockwave 3+ targets hit then reduces cooldown by 15 seconds
- local RumblingEarthMod = DynamicMod({
- {
- type = "SPELL_CAST_SUCCESS", spellID = 46968,
- handler = function(watchInfo)
- watchInfo.numHits = 0
- end
- },
- {
- type = "SPELL_AURA_APPLIED", spellID = 132168,
- handler = function(watchInfo)
- watchInfo.numHits = watchInfo.numHits + 1
- if watchInfo.numHits == 3 then
- watchInfo:updateCDDelta(-15)
- end
- end
- }
- })
- -- Each target hit by Capacitor Totem reduces cooldown by 5 seconds (up to 4 targets hit)
- local function StaticChargeAuraHandler(watchInfo)
- watchInfo.numHits = watchInfo.numHits + 1
- if watchInfo.numHits <= 4 then
- watchInfo:updateCDDelta(-5)
- end
- end
- local StaticChargeMod = DynamicMod({
- type = "SPELL_SUMMON", spellID = 192058,
- handler = function(watchInfo)
- watchInfo.numHits = 0
- if watchInfo.totemGUID then
- ZT.eventHandlers:remove("SPELL_AURA_APPLIED", 118905, watchInfo.totemGUID, StaticChargeAuraHandler)
- end
- watchInfo.totemGUID = select(8, CombatLogGetCurrentEventInfo())
- ZT.eventHandlers:add("SPELL_AURA_APPLIED", 118905, watchInfo.totemGUID, StaticChargeAuraHandler, watchInfo)
- end
- })
- -- Guardian Spirit: If expires watchInfothout healing then reset to 60 seconds
- local GuardianAngelMod = DynamicMod({
- {
- type = "SPELL_HEAL", spellID = 48153,
- handler = function(watchInfo)
- watchInfo.spiritHeal = true
- end
- },
- {
- type = "SPELL_AURA_REMOVED", spellID = 47788,
- handler = function(watchInfo)
- if not watchInfo.spiritHeal then
- watchInfo:updateCDRemaining(60)
- end
- watchInfo.spiritHeal = false
- end
- }
- })
- -- Dispels: Go on cooldown only if a debuff is dispelled
- local function DispelMod(spellID)
- return DynamicMod({
- type = "SPELL_DISPEL",
- spellID = spellID,
- handler = function(watchInfo)
- watchInfo:updateCDRemaining(8)
- end
- })
- end
- -- Resource Spending: For every spender, reduce cooldown by (coefficient * cost) seconds
- -- Note: By default, I try to use minimum cost values as to not over-estimate the cooldown reduction
- local specIDToSpenderInfo = {
- [DK.Blood] = {
- [47541] = 40, -- Death Coil
- [49998] = 40, -- Death Strike (Assumes -5 due to Ossuary)
- [61999] = 30, -- Raise Ally
- [327574] = 20, -- Sacrificial Pact
- },
- [Warrior.Arms] = {
- [845] = 20, -- Cleave
- [163201] = 20, -- Execute (Ignores Sudden Death)
- [1715] = 10, -- Hamstring
- [202168] = 10, -- Impending Victory
- [12294] = 30, -- Moral Strike
- [772] = 30, -- Rend
- [1464] = 20, -- Slam
- [1680] = 30, -- Whirlwind
- [190456] = 40, -- Ignore Pain
- },
- [Warrior.Fury] = {
- [202168] = 10, -- Impending Victory
- [184367] = 75, -- Rampage (Assumes -10 from Carnage)
- [12323] = 10, -- Piercing Howl
- [190456] = 40, -- Ignore Pain
- },
- [Warrior.Prot] = {
- [190456] = 40, -- Ignore Pain (Ignores Vengeance)
- [202168] = 10, -- Impending Victory
- [6572] = 30, -- Revenge (Ignores Vengeance)
- [2565] = 30, -- Shield Block
- },
- [Hunter.BM] = { -- TODO
- },
- [Hunter.MM] = { -- TODO
- },
- [Hunter.SV] = { -- TODO
- },
- [Paladin] = {
- [85673] = 3, -- Word of Glory
- [85222] = 3, -- Light of Dawn
- [152262] = 3, -- Seraphim
- [53600] = 3, -- Shield of the Righteous
- [85256] = 3, -- Templar's Verdict
- [53385] = 3, -- Divine Storm
- [343527] = 3, -- Execution Sentence
- },
- [Paladin.Holy] = {
- [85673] = 3, -- Word of Glory
- [85222] = 3, -- Light of Dawn
- [152262] = 3, -- Seraphim
- },
- [Paladin.Prot] = {
- [85673] = 3, -- Word of Glory
- [53600] = 3, -- Shield of the Righteous
- [152262] = 3, -- Seraphim
- },
- [Paladin.Ret] = {
- [85673] = 3, -- Word of Glory
- [85256] = 3, -- Templar's Verdict
- [53385] = 3, -- Divine Storm
- [343527] = 3, -- Execution Sentence
- [152262] = 3, -- Seraphim
- },
- }
- local function ResourceSpendingMods(specID, coefficient)
- local handlers = {}
- local spenderInfo = specIDToSpenderInfo[specID]
- for spellID,cost in pairs(spenderInfo) do
- local delta = -(coefficient * cost)
- handlers[#handlers+1] = {
- type = "SPELL_CAST_SUCCESS",
- spellID = spellID,
- handler = function(watchInfo)
- watchInfo:updateCDDelta(delta)
- end
- }
- end
- return DynamicMod(handlers)
- end
- --##############################################################################
- -- List of Tracked Spells
- -- TODO: Denote which spells should be modified by UnitSpellHaste(...)
- ZT.spellListVersion = 102
- ZT.spellList = {
- -- Racials
- {type="HARDCC", id=255654, cd=120, reqs={RaceReq("HighmountainTauren")}}, -- Bull Rush
- {type="HARDCC", id=20549, cd=90, reqs={RaceReq("Tauren")}}, -- War Stomp
- {type="STHARDCC", id=287712, cd=150, reqs={RaceReq("KulTiran")}}, -- Haymaker
- {type="STSOFTCC", id=107079, cd=120, reqs={RaceReq("Pandaren")}}, -- Quaking Palm
- {type="DISPEL", id=202719, cd=120, reqs={RaceReq("BloodElf"), ClassReq(DH)}}, -- Arcane Torrent
- {type="DISPEL", id=50613, cd=120, reqs={RaceReq("BloodElf"), ClassReq(DK)}}, -- Arcane Torrent
- {type="DISPEL", id=80483, cd=120, reqs={RaceReq("BloodElf"), ClassReq(Hunter)}}, -- Arcane Torrent
- {type="DISPEL", id=28730, cd=120, reqs={RaceReq("BloodElf"), ClassReq(Mage)}}, -- Arcane Torrent
- {type="DISPEL", id=129597, cd=120, reqs={RaceReq("BloodElf"), ClassReq(Monk)}}, -- Arcane Torrent
- {type="DISPEL", id=155145, cd=120, reqs={RaceReq("BloodElf"), ClassReq(Paladin)}}, -- Arcane Torrent
- {type="DISPEL", id=232633, cd=120, reqs={RaceReq("BloodElf"), ClassReq(Priest)}}, -- Arcane Torrent
- {type="DISPEL", id=25046, cd=120, reqs={RaceReq("BloodElf"), ClassReq(Rogue)}}, -- Arcane Torrent
- {type="DISPEL", id=28730, cd=120, reqs={RaceReq("BloodElf"), ClassReq(Warlock)}}, -- Arcane Torrent
- {type="DISPEL", id=69179, cd=120, reqs={RaceReq("BloodElf"), ClassReq(Warrior)}}, -- Arcane Torrent
- {type="DISPEL", id=20594, cd=120, reqs={RaceReq("Dwarf")}, mods={{mod=EventRemainingMod("SPELL_AURA_APPLIED",65116,120)}}}, -- Stoneform
- {type="DISPEL", id=265221, cd=120, reqs={RaceReq("DarkIronDwarf")}, mods={{mod=EventRemainingMod("SPELL_AURA_APPLIED",265226,120)}}}, -- Fireblood
- {type="UTILITY", id=58984, cd=120, reqs={RaceReq("NightElf")}}, -- Shadowmeld
- -- Covenants
- {type="UTILITY", id=324739, cd=300, reqs={CovenantReq("Kyrian")}, version=101},-- Summon Steward
- {type="UTILITY", id=300728, cd=60, reqs={CovenantReq("Venthyr")}, version=101},-- Door of Shadows
- {type="UTILITY", id=310143, cd=90, reqs={CovenantReq("NightFae")}, version=101},-- Soulshape
- {type="PERSONAL", id=324631, cd=90, reqs={CovenantReq("Necrolord")}, version=101},-- Fleshcraft
- -- DH
- ---- Base
- {type="INTERRUPT", id=183752, cd=15, reqs={ClassReq(DH)}}, -- Disrupt
- {type="UTILITY", id=188501, cd=60, reqs={ClassReq(DH)}, mods={{reqs={ClassReq(DH), LevelReq(42)}, mod=SubtractMod(30)}}}, -- Spectral Sight
- {type="TANK", id=185245, cd=8, reqs={ClassReq(DH), LevelReq(9)}}, -- Torment
- {type="DISPEL", id=278326, cd=10, reqs={ClassReq(DH), LevelReq(17)}}, -- Consume Magic
- {type="STSOFTCC", id=217832, cd=45, reqs={ClassReq(DH), LevelReq(34)}}, -- Imprison
- ---- DH.Havoc
- {type="HARDCC", id=179057, cd=60, reqs={SpecReq({DH.Havoc})}, mods={{reqs={TalentReq(206477)}, mod=SubtractMod(20)}}}, -- Chaos Nova
- {type="PERSONAL", id=198589, cd=60, reqs={SpecReq({DH.Havoc}), LevelReq(21)}}, -- Blur
- {type="RAIDCD", id=196718, cd=300, reqs={SpecReq({DH.Havoc}), LevelReq(39)}, mods={{reqs={LevelReq(47)}, mod=SubtractMod(120)}}}, -- Darkness
- {type="DAMAGE", id=191427, cd=300, reqs={SpecReq({DH.Havoc})}, mods={{reqs={LevelReq(48)}, mod=SubtractMod(60)}}}, -- Metamorphosis
- ---- DH.Veng
- {type="TANK", id=204021, cd=60, reqs={SpecReq({DH.Veng})}}, -- Fiery Brand
- {type="TANK", id=212084, cd=45, reqs={SpecReq({DH.Veng}), LevelReq(11)}}, -- Fel Devastation
- {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
- {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
- {type="TANK", id=187827, cd=300, reqs={SpecReq({DH.Veng})}, mods={{reqs={LevelReq(20)}, mod=SubtractMod(60)}, {reqs={LevelReq(48)}, mod=SubtractMod(60)}}}, -- Metamorphosis
- ---- Talents
- {type="IMMUNITY", id=196555, cd=180, reqs={TalentReq(196555)}}, -- Netherwalk
- {type="SOFTCC", id=202138, cd=90, reqs={TalentReq(202138)}}, -- Sigil of Chains
- {type="STHARDCC", id=211881, cd=30, reqs={TalentReq(211881)}}, -- Fel Eruption
- {type="TANK", id=263648, cd=30, reqs={TalentReq(263648)}}, -- Soul Barrier
- {type="DAMAGE", id=258925, cd=60, reqs={TalentReq(258925)}}, -- Fel Barrage
- {type="TANK", id=320341, cd=90, reqs={TalentReq(320341)}}, -- Bulk Extraction
- -- DK
- -- TODO: Raise Ally (Brez support)
- ---- Base
- {type="INTERRUPT", id=47528, cd=15, reqs={ClassReq(DK), LevelReq(7)}}, -- Mind Freeze
- {type="PERSONAL", id=48707, cd=60, reqs={ClassReq(DK), LevelReq(9)}, mods={{reqs={TalentReq(205727)}, mod=SubtractMod(20)}}}, -- Anti-Magic Shell
- {type="TANK", id=56222, cd=8, reqs={ClassReq(DK), LevelReq(14)}}, -- Dark Command
- {type="PERSONAL", id=49039, cd=120, reqs={ClassReq(DK), LevelReq(33)}}, -- Lichborne
- {type="PERSONAL", id=48792, cd=180, reqs={ClassReq(DK), LevelReq(38)}}, -- Icebound Fortitude
- {type="BREZ", id=61999, cd=600, reqs={ClassReq(DK), LevelReq(39)}}, -- Raise Ally
- {type="RAIDCD", id=51052, cd=120, reqs={ClassReq(DK), LevelReq(47)}}, -- Anti-Magic Zone
- {type="PERSONAL", id=327574, cd=120, reqs={ClassReq(DK), LevelReq(54)}}, -- Sacrificial Pact
- ---- DK.Blood
- {type="STHARDCC", id=221562, cd=45, reqs={SpecReq({DK.Blood}), LevelReq(13)}}, -- Asphyxiate
- {type="TANK", id=55233, cd=90, reqs={SpecReq({DK.Blood}), LevelReq(29)}, mods={{reqs={TalentReq(205723)}, mod=ResourceSpendingMods(DK.Blood, 0.15)}}}, -- Vampiric Blood
- {type="SOFTCC", id=108199, cd=120, reqs={SpecReq({DK.Blood}), LevelReq(44)}, mods={{reqs={TalentReq(206970)}, mod=SubtractMod(30)}}}, -- Gorefiend's Grasp
- {type="TANK", id=49028, cd=120, reqs={SpecReq({DK.Blood}), LevelReq(34)}}, -- Dancing Rune Weapon
- ---- DK.Frost
- {type="DAMAGE", id=51271, cd=45, reqs={SpecReq({DK.Frost}), LevelReq(29)}}, -- Pillar of Frost
- {type="DAMAGE", id=279302, cd=180, reqs={SpecReq({DK.Frost}), LevelReq(44)}}, -- Frostwyrm's Fury
- ---- DK.Unholy
- {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
- {type="DAMAGE", id=63560, cd=60, reqs={SpecReq({DK.Unholy}), LevelReq(32)}, mods={{reqs={LevelReq(41)}, mod=CastDeltaMod(47541,-1)}}}, -- Dark Transformation
- {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
- ---- Talents
- {type="TANK", id=219809, cd=60, reqs={TalentReq(219809)}}, -- Tombstone
- {type="DAMAGE", id=115989, cd=45, reqs={TalentReq(115989)}}, -- Unholy Blight
- {type="STHARDCC", id=108194, cd=45, reqs={TalentReq(108194)}}, -- Asphyxiate
- {type="SOFTCC", id=207167, cd=60, reqs={TalentReq(207167)}}, -- Blinding Sleet
- {type="PERSONAL", id=48743, cd=120, reqs={TalentReq(48743)}}, -- Death Pact
- {type="TANK", id=194844, cd=60, reqs={TalentReq(194844)}}, -- Bonestorm
- {type="DAMAGE", id=152279, cd=120, reqs={TalentReq(152279)}}, -- Breath of Sindragosa
- {type="DAMAGE", id=49206, cd=180, reqs={TalentReq(49206)}}, -- Summon Gargoyle
- {type="DAMAGE", id=207289, cd=75, reqs={TalentReq(207289)}}, -- Unholy Assault
- -- Druid
- -- TODO: Rebirth (Brez support)
- ---- Base
- {type="TANK", id=6795, cd=8, reqs={ClassReq(Druid), LevelReq(14)}}, -- Growl
- {type="PERSONAL", id=22812, cd=60, reqs={ClassReq(Druid), LevelReq(24)}, mods={{reqs={TalentReq(203965)}, mod=MultiplyMod(0.67)}}}, -- Barkskin
- {type="BREZ", id=20484, cd=600, reqs={ClassReq(Druid), LevelReq(29)}}, -- Rebirth
- {type="DISPEL", id=2908, cd=10, reqs={ClassReq(Druid), LevelReq(41)}}, -- Soothe
- {type="UTILITY", id=106898, cd=120, reqs={ClassReq(Druid), LevelReq(43)}, mods={{reqs={SpecReq({Druid.Guardian}), LevelReq(49)}, mod=SubtractMod(60)}}}, -- Stampeding Roar
- ---- Shared
- {type="DISPEL", id=2782, cd=8, reqs={SpecReq({Druid.Balance, Druid.Feral, Druid.Guardian}), LevelReq(19)}, mods={{mod=DispelMod(2782)}}, ignoreCast=true}, -- Remove Corruption
- {type="INTERRUPT", id=106839, cd=15, reqs={SpecReq({Druid.Feral, Druid.Guardian}), LevelReq(26)}}, -- Skull Bash
- {type="PERSONAL", id=61336, cd=180, reqs={SpecReq({Druid.Feral, Druid.Guardian}), LevelReq(32)}, mods={{reqs={SpecReq({Druid.Guardian}), LevelReq(47)}, mod=ChargesMod(2)}}}, -- Survival Instincts
- {type="UTILITY", id=29166, cd=180, reqs={SpecReq({Druid.Balance, Druid.Resto}), LevelReq(42)}}, -- Innervate
- ---- Druid.Balance
- {type="INTERRUPT", id=78675, cd=60, reqs={SpecReq({Druid.Balance}), LevelReq(26)}}, -- Solar Beam
- {type="SOFTCC", id=132469, cd=30, reqs={SpecReq({Druid.Balance}), LevelReq(28)}}, -- Typhoon
- {type="DAMAGE", id=194223, cd=180, reqs={SpecReq({Druid.Balance}, NoTalentReq(102560)), LevelReq(39)}}, -- Celestial Alignment
- ---- Druid.Feral
- {type="STHARDCC", id=22570, cd=20, reqs={SpecReq({Druid.Feral}), LevelReq(28)}}, -- Maim
- {type="DAMAGE", id=106951, cd=180, reqs={SpecReq({Druid.Feral}), LevelReq(34)}}, -- Berserk
- ---- Druid.Guardian
- {type="SOFTCC", id=99, cd=30, reqs={SpecReq({Druid.Guardian}), LevelReq(28)}}, -- Incapacitating Roar
- {type="TANK", id=50334, cd=180, reqs={SpecReq({Druid.Guardian}), LevelReq(34)}}, -- Berserk
- ---- Druid.Resto
- {type="EXTERNAL", id=102342, cd=90, reqs={SpecReq({Druid.Resto}), LevelReq(12)}}, -- Ironbark
- {type="DISPEL", id=88423, cd=8, reqs={SpecReq({Druid.Resto}), LevelReq(19)}, mods={{mod=DispelMod(88423)}}, ignoreCast=true}, -- Remove Corruption
- {type="SOFTCC", id=102793, cd=60, reqs={SpecReq({Druid.Resto}), LevelReq(28)}}, -- Ursol's Vortex
- {type="HEALING", id=740, cd=180, reqs={SpecReq({Druid.Resto}), LevelReq(37)}, mods={{reqs={SpecReq({Druid.Resto}), TalentReq(197073)}, mod=SubtractMod(60)}}}, -- Tranquility
- {type="UTILITY", id=132158, cd=60, reqs={SpecReq({Druid.Resto}), LevelReq(58)}}, -- Nature's Swiftness
- ---- Talents
- {type="HEALING", id=102351, cd=30, reqs={TalentReq(102351)}}, -- Cenarion Ward
- {type="UTILITY", id=205636, cd=60, reqs={TalentReq(205636)}}, -- Force of Nature
- {type="PERSONAL", id=108238, cd=90, reqs={TalentReq(108238)}}, -- Renewal
- {type="STHARDCC", id=5211, cd=60, reqs={TalentReq(5211)}}, -- Mighty Bash
- {type="SOFTCC", id=102359, cd=30, reqs={TalentReq(102359)}}, -- Mass Entanglement
- {type="SOFTCC", id=132469, cd=30, reqs={TalentReq(197632)}}, -- Typhoon
- {type="SOFTCC", id=132469, cd=30, reqs={TalentReq(197488)}}, -- Typhoon
- {type="SOFTCC", id=102793, cd=60, reqs={TalentReq(197492)}}, -- Ursol's Vortex
- {type="SOFTCC", id=99, cd=30, reqs={TalentReq(197491)}}, -- Incapacitating Roar
- {type="SOFTCC", id=99, cd=30, reqs={TalentReq(217615)}}, -- Incapacitating Roar
- {type="DAMAGE", id=319454, cd=300, reqs={TalentReq(319454), TalentReq(202157)}}, -- Heart of the Wild
- {type="PERSONAL", id=319454, cd=300, reqs={TalentReq(319454), TalentReq(197491)}}, -- Heart of the Wild
- {type="HEALING", id=319454, cd=300, reqs={TalentReq(319454), TalentReq(197492)}}, -- Heart of the Wild
- {type="DAMAGE", id=319454, cd=300, reqs={TalentReq(319454), TalentReq(197488)}}, -- Heart of the Wild
- {type="PERSONAL", id=319454, cd=300, reqs={TalentReq(319454), TalentReq(217615)}}, -- Heart of the Wild
- {type="DAMAGE", id=319454, cd=300, reqs={TalentReq(319454), TalentReq(202155)}}, -- Heart of the Wild
- {type="DAMAGE", id=319454, cd=300, reqs={TalentReq(319454), TalentReq(197632)}}, -- Heart of the Wild
- {type="DAMAGE", id=319454, cd=300, reqs={TalentReq(319454), TalentReq(197490)}}, -- Heart of the Wild
- {type="DAMAGE", id=102543, cd=180, reqs={TalentReq(102543)}}, -- Incarnation: King of the Jungle
- {type="DAMAGE", id=102560, cd=180, reqs={TalentReq(102560)}}, -- Incarnation: Chosen of Elune
- {type="TANK", id=102558, cd=180, reqs={TalentReq(102558)}}, -- Incarnation: Guardian of Ursoc
- {type="HEALING", id=33891, cd=180, reqs={TalentReq(33891)}, mods={{mod=EventRemainingMod("SPELL_AURA_APPLIED",117679,180)}}, ignoreCast=true}, -- Incarnation: Tree of Life
- {type="HEALING", id=203651, cd=60, reqs={TalentReq(203651)}}, -- Overgrowth
- {type="DAMAGE", id=202770, cd=60, reqs={TalentReq(202770)}}, -- Fury of Elune
- {type="TANK", id=204066, cd=75, reqs={TalentReq(204066)}}, -- Lunar Beam
- {type="HEALING", id=197721, cd=90, reqs={TalentReq(197721)}}, -- Flourish
- {type="TANK", id=80313, cd=30, reqs={TalentReq(80313)}}, -- Pulverize
- -- Hunter
- -- TODO: Update ResourceSpendingMod to include spender costs
- ---- Base
- {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
- {type="UTILITY", id=5384, cd=30, reqs={ClassReq(Hunter), LevelReq(6)}}, -- Feign Death
- {type="IMMUNITY", id=186265, cd=180, reqs={ClassReq(Hunter), LevelReq(8)}, mods={{reqs={ClassReq(Hunter), TalentReq(266921)}, mod=MultiplyMod(0.8)}}}, -- Aspect of the Turtle
- {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
- {type="STSOFTCC", id=187650, cd=30, reqs={ClassReq(Hunter), LevelReq(10)}, mods={{reqs={ClassReq(Hunter), LevelReq(56)}, mod=SubtractMod(5)}}}, -- Freezing Trap
- {type="UTILITY", id=34477, cd=30, reqs={ClassReq(Hunter), LevelReq(27)}}, -- Misdirection
- {type="DISPEL", id=19801, cd=10, reqs={ClassReq(Hunter), LevelReq(37)}}, -- Tranquilizing Shot
- ---- Shared
- {type="INTERRUPT", id=147362, cd=24, reqs={SpecReq({Hunter.BM, Hunter.MM}), LevelReq(18)}}, -- Counter Shot
- {type="STHARDCC", id=19577, cd=60, reqs={SpecReq({Hunter.BM, Hunter.SV}), LevelReq(33)}}, -- Intimidation
- ---- Hunter.BM
- {type="DAMAGE", id=19574, cd=90, reqs={SpecReq({Hunter.BM}), LevelReq(20)}}, -- Bestial Wrath
- {type="DAMAGE", id=193530, cd=120, reqs={SpecReq({Hunter.BM}), LevelReq(38)}}, -- Aspect of the Wild
- ---- Hunter.MM
- {type="STSOFTCC", id=186387, cd=30, reqs={SpecReq({Hunter.MM}), LevelReq(12)}}, -- Bursting Shot
- {type="HARDCC", id=109248, cd=45, reqs={SpecReq({Hunter.MM}), LevelReq(33)}}, -- Binding Shot
- {type="DAMAGE", id=288613, cd=120, reqs={SpecReq({Hunter.MM}), LevelReq(34)}}, -- Trueshot
- ---- Hunter.SV
- {type="INTERRUPT", id=187707, cd=15, reqs={SpecReq({Hunter.SV}), LevelReq(18)}}, -- Muzzle
- {type="DAMAGE", id=266779, cd=120, reqs={SpecReq({Hunter.SV}), LevelReq(34)}}, -- Coordinated Assault
- ---- Talents
- {type="UTILITY", id=199483, cd=60, reqs={TalentReq(199483)}}, -- Camouflage
- {type="SOFTCC", id=162488, cd=30, reqs={TalentReq(162488)}}, -- Steel Trap
- {type="HARDCC", id=109248, cd=45, reqs={SpecReq({Hunter.BM, Hunter.SV}), TalentReq(109248)}}, -- Binding Shot
- {type="DAMAGE", id=201430, cd=120, reqs={TalentReq(201430)}}, -- Stampede
- {type="DAMAGE", id=260402, cd=60, reqs={TalentReq(260402)}}, -- Double Tap
- {type="DAMAGE", id=321530, cd=60, reqs={TalentReq(321530)}}, -- Bloodshed
- -- Mage
- -- TODO: Invisibility seemed to be bugged (even w/o combat log tracking)
- -- TODO: Arcane should have Invisibility from 34 to 46, then Greater Invisibility from 47 onward
- ---- Base
- {type="INTERRUPT", id=2139, cd=24, reqs={ClassReq(Mage), LevelReq(7)}}, -- Counterspell
- {type="DISPEL", id=475, cd=8, reqs={ClassReq(Mage), LevelReq(21)}, mods={{mod=DispelMod(475)}}, ignoreCast=true}, -- Remove Curse
- {type="IMMUNITY", id=45438, cd=240, reqs={ClassReq(Mage), LevelReq(22)}, mods={{mod=CastRemainingMod(235219, 0)}}}, -- Ice Block
- {type="DAMAGE", id=55342, cd=120, reqs={ClassReq(Mage), LevelReq(44)}}, -- Mirror Image
- ---- Shared
- {type="UTILITY", id=66, cd=300, reqs={SpecReq({Mage.Fire, Mage.Frost}), LevelReq(34)}}, -- Invisibility
- {type="PERSONAL", id=108978, cd=60, reqs={SpecReq({Mage.Fire, Mage.Frost}), LevelReq(58)}}, -- Alter Time
- ---- Mage.Arcane
- {type="PERSONAL", id=342245, cd=60, reqs={SpecReq({Mage.Arcane}), LevelReq(19)}, mods={{reqs={TalentReq(342249)}, mod=SubtractMod(30)}}}, -- Alter Time
- {type="PERSONAL", id=235450, cd=25, reqs={SpecReq({Mage.Arcane}), LevelReq(28)}}, -- Prismatic Barrier
- {type="DAMAGE", id=12042, cd=120, reqs={SpecReq({Mage.Arcane}), LevelReq(29)}}, -- Arcane Power
- {type="DAMAGE", id=321507, cd=45, reqs={SpecReq({Mage.Arcane}), LevelReq(33)}}, -- Touch of the Magi
- {type="UTILITY", id=205025, cd=60, reqs={SpecReq({Mage.Arcane}), LevelReq(42)}}, -- Presence of Mind
- {type="UTILITY", id=110959, cd=120, reqs={SpecReq({Mage.Arcane}), LevelReq(47)}}, -- Greater Invisibility
- ---- Mage.Fire
- {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
- {type="PERSONAL", id=235313, cd=25, reqs={SpecReq({Mage.Fire}), LevelReq(28)}}, -- Blazing Barrier
- {type="DAMAGE", id=190319, cd=120, reqs={SpecReq({Mage.Fire}), LevelReq(29)}}, -- Combustion
- ---- Mage.Frost
- {type="PERSONAL", id=11426, cd=25, reqs={SpecReq({Mage.Frost}), LevelReq(28)}}, -- Ice Barrier
- {type="DAMAGE", id=12472, cd=180, reqs={SpecReq({Mage.Frost}), LevelReq(29)}}, -- Icy Veins
- {type="DAMAGE", id=84714, cd=60, reqs={SpecReq({Mage.Frost}), LevelReq(38)}}, -- Frozen Orb
- {type="UTILITY", id=235219, cd=300, reqs={SpecReq({Mage.Frost}), LevelReq(42)}, mods={{reqs={SpecReq({Mage.Frost}), LevelReq(54)}, mod=SubtractMod(30)}}}, -- Cold Snap
- ---- Talents
- {type="SOFTCC", id=113724, cd=45, reqs={TalentReq(113724)}}, -- Ring of Frost
- -- Monk
- -- TODO: Spiritual Focus (280197) as a ResourceSpendingMod
- -- TODO: Black Ox Brew modifiers
- -- TODO: Blackout Combo modifiers
- ---- Base
- {type="DAMAGE", id=322109, cd=180, reqs={ClassReq(Monk)}}, -- Touch of Death
- {type="TANK", id=115546, cd=8, reqs={ClassReq(Monk), LevelReq(14)}}, -- Provoke
- {type="STSOFTCC", id=115078, cd=45, reqs={ClassReq(Monk), LevelReq(22)}, mods={{reqs={ClassReq(Monk), LevelReq(56)}, mod=SubtractMod(15)}}}, -- Paralysis
- {type="HARDCC", id=119381, cd=60, reqs={ClassReq(Monk), LevelReq(6)}, mods={{reqs={ClassReq(Monk), TalentReq(264348)}, mod=SubtractMod(10)}}}, -- Leg Sweep
- ---- Shared
- {type="INTERRUPT", id=116705, cd=15, reqs={SpecReq({Monk.BRM, Monk.WW}), LevelReq(18)}}, -- Spear Hand Strike
- {type="PERSONAL", id=243435, cd=420, reqs={SpecReq({Monk.MW, Monk.WW}), LevelReq(28)}, mods={{reqs={LevelReq(48)}, mod=SubtractMod(240)}}}, -- Fortifying Brew
- ---- Monk.BRM
- {type="TANK", id=322507, cd=60, reqs={SpecReq({Monk.BRM}), LevelReq(27)}, mods={{reqs={SpecReq({Monk.BRM}), TalentReq(325093)}, mod=MultiplyMod(0.8)}, {mod=CastDeltaMod(121253, -3)}, {mod=CastDeltaMod(100780, -1)}}}, -- Celestial Brew
- {type="PERSONAL", id=115203, cd=360, reqs={SpecReq({Monk.BRM}), LevelReq(28)}}, -- Fortifying Brew
- {type="TANK", id=115176, cd=300, reqs={SpecReq({Monk.BRM}), LevelReq(34)}}, -- Zen Meditation
- {type="SOFTCC", id=324312, cd=30, reqs={SpecReq({Monk.BRM}), LevelReq(54)}}, -- Clash
- {type="TANK", id=132578, cd=180, reqs={SpecReq({Monk.BRM}), LevelReq(42)}}, -- Invoke Niuzao, the Black Ox
- ---- Monk.MW
- {type="HEALING", id=322118, cd=180, reqs={SpecReq({Monk.MW}), LevelReq(42)}}, -- Invoke Yu'lon, the Jade Serpent
- {type="HEALING", id=115310, cd=180, reqs={SpecReq({Monk.MW}), LevelReq(46)}}, -- Revival
- {type="EXTERNAL", id=116849, cd=120, reqs={SpecReq({Monk.MW}), LevelReq(27)}}, -- Life Cocoon
- ---- Monk.WW
- {type="PERSONAL", id=122470, cd=90, reqs={SpecReq({Monk.WW}), LevelReq(29)}}, -- Touch of Karma
- {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
- {type="DAMAGE", id=123904, cd=120, reqs={SpecReq({Monk.WW}), LevelReq(42)}}, -- Invoke Xuen, the White Tiger
- {type="DAMAGE", id=113656, cd=24, reqs={SpecReq({Monk.WW}), LevelReq(12)}}, -- Fists of Fury
- ---- Talents
- {type="UTILITY", id=116841, cd=30, reqs={TalentReq(116841)}}, -- Tiger's Lust
- {type="TANK", id=115399, cd=120, reqs={TalentReq(115399)}}, -- Black Ox Brew
- {type="SOFTCC", id=198898, cd=30, reqs={TalentReq(198898)}}, -- Song of Chi-Ji
- {type="SOFTCC", id=116844, cd=45, reqs={TalentReq(116844)}}, -- Ring of Peace
- {type="PERSONAL", id=122783, cd=90, reqs={TalentReq(122783)}}, -- Diffuse Magic
- {type="PERSONAL", id=122278, cd=120, reqs={TalentReq(122278)}}, -- Dampen Harm
- {type="TANK", id=325153, cd=60, reqs={TalentReq(325153)}}, -- Exploding Keg
- {type="HEALING", id=325197, cd=120, reqs={TalentReq(325197)}}, -- Invoke Chi-Ji, the Red Crane
- {type="DAMAGE", id=152173, cd=90, reqs={TalentReq(152173)}}, -- Serenity
- -- Paladin
- -- TODO: Prot should have Divine Protection from 28 to 41, then Ardent Defender from 42 onward
- ---- Base
- {type="IMMUNITY", id=642, cd=300, reqs={ClassReq(Paladin)}, mods={{reqs={TalentReq(114154)}, mod=MultiplyMod(0.7)}}}, -- Divine Shield
- {type="STHARDCC", id=853, cd=60, reqs={ClassReq(Paladin), LevelReq(5)}, mods={{reqs={TalentReq(234299)}, mod=ResourceSpendingMods(Paladin, 2)}}}, -- Hammer of Justice
- {type="EXTERNAL", id=633, cd=600, reqs={ClassReq(Paladin), LevelReq(9)}, mods={{reqs={TalentReq(114154)}, mod=MultiplyMod(0.3)}}}, -- Lay on Hands
- {type="UTILITY", id=1044, cd=25, reqs={ClassReq(Paladin), LevelReq(22)}, version=101}, -- Blessing of Freedom
- {type="EXTERNAL", id=6940, cd=120, reqs={ClassReq(Paladin), LevelReq(32)}}, -- Blessing of Sacrifice
- {type="EXTERNAL", id=1022, cd=300, reqs={ClassReq(Paladin), LevelReq(41), NoTalentReq(204018)}}, -- Blessing of Protection
- ---- Shared
- {type="DISPEL", id=213644, cd=8, reqs={SpecReq({Paladin.Prot, Paladin.Ret}), LevelReq(12)}}, -- Cleanse Toxins
- {type="INTERRUPT", id=96231, cd=15, reqs={SpecReq({Paladin.Prot, Paladin.Ret}), LevelReq(23)}}, -- Rebuke
- {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
- ---- Paladin.Holy
- {type="DISPEL", id=4987, cd=8, reqs={SpecReq({Paladin.Holy}), LevelReq(12)}, mods={{mod=DispelMod(4987)}}, ignoreCast=true}, -- Cleanse
- {type="PERSONAL", id=498, cd=60, reqs={SpecReq({Paladin.Holy}), LevelReq(26)}, mods={{reqs={TalentReq(114154)}, mod=MultiplyMod(0.7)}}}, -- Divine Protection
- {type="HEALING", id=31884, cd=180, reqs={SpecReq({Paladin.Holy}), LevelReq(37), NoTalentReq(216331)}, mods={{reqs={LevelReq(49)}, mod=SubtractMod(60)}}}, -- Avenging Wrath
- {type="RAIDCD", id=31821, cd=180, reqs={SpecReq({Paladin.Holy}), LevelReq(39)}}, -- Aura Mastery
- ---- Paladin.Prot
- {type="INTERRUPT", id=31935, cd=15, reqs={SpecReq({Paladin.Prot}), LevelReq(10)}}, -- Avenger's Shield
- {type="TANK", id=62124, cd=8, reqs={SpecReq({Paladin.Prot}), LevelReq(14)}, version=102}, -- Hand of Reckoning
- {type="TANK", id=86659, cd=300, reqs={SpecReq({Paladin.Prot}), LevelReq(39)}}, -- Guardian of Ancient Kings
- {type="TANK", id=31850, cd=120, reqs={SpecReq({Paladin.Prot}), LevelReq(42)}, mods={{reqs={TalentReq(114154)}, mod=MultiplyMod(0.7)}}}, -- Ardent Defender
- ---- Paladin.Ret
- {type="PERSONAL", id=184662, cd=120, reqs={SpecReq({Paladin.Ret}), LevelReq(26)}, mods={{reqs={TalentReq(114154)}, mod=MultiplyMod(0.7)}}}, -- Shield of Vengeance
- ---- Talents
- {type="STSOFTCC", id=20066, cd=15, reqs={TalentReq(20066)}}, -- Repentance
- {type="SOFTCC", id=115750, cd=90, reqs={TalentReq(115750)}}, -- Blinding Light
- {type="PERSONAL", id=205191, cd=60, reqs={TalentReq(205191)}}, -- Eye for an Eye
- {type="EXTERNAL", id=204018, cd=180, reqs={TalentReq(204018)}}, -- Blessing of Spellwarding
- {type="HEALING", id=105809, cd=180, reqs={TalentReq(105809), SpecReq({Paladin.Holy})}}, -- Holy Avenger
- {type="TANK", id=105809, cd=180, reqs={TalentReq(105809), SpecReq({Paladin.Prot})}}, -- Holy Avenger
- {type="DAMAGE", id=105809, cd=180, reqs={TalentReq(105809), SpecReq({Paladin.Ret})}}, -- Holy Avenger
- {type="HEALING", id=216331, cd=120, reqs={TalentReq(216331)}}, -- Avenging Crusader
- {type="DAMAGE", id=231895, cd=20, reqs={TalentReq(231895)}}, -- Crusade
- {type="DAMAGE", id=343721, cd=60, reqs={TalentReq(343721)}}, -- Final Reckoning
- {type="HEALING", id=200025, cd=15, reqs={TalentReq(200025)}}, -- Beacon of Virtue
- -- Priest
- ---- Base
- {type="SOFTCC", id=8122, cd=60, reqs={ClassReq(Priest), LevelReq(7)}, mods={{reqs={TalentReq(196704)}, mod=SubtractMod(30)}}}, -- Psychic Scream
- {type="PERSONAL", id=19236, cd=90, reqs={ClassReq(Priest), LevelReq(8)}}, -- Desperate Prayer
- {type="DISPEL", id=32375, cd=45, reqs={ClassReq(Priest), LevelReq(42)}}, -- Mass Dispel
- {type="UTILITY", id=73325, cd=90, reqs={ClassReq(Priest), LevelReq(49)}}, -- Leap of Faith
- ---- Shared
- {type="DISPEL", id=527, cd=8, reqs={SpecReq({Priest.Disc, Priest.Holy}), LevelReq(18)}, mods={{mod=DispelMod(4987)}}, ignoreCast=true}, -- Purify
- {type="HEALING", id=10060, cd=120, reqs={SpecReq({Priest.Disc, Priest.Holy}), LevelReq(58)}}, -- Power Infusion
- ---- Priest.Disc
- {type="EXTERNAL", id=33206, cd=180, reqs={SpecReq({Priest.Disc}), LevelReq(38)}}, -- Pain Suppression
- {type="HEALING", id=47536, cd=90, reqs={SpecReq({Priest.Disc}), LevelReq(41), NoTalentReq(109964)}}, -- Rapture
- {type="RAIDCD", id=62618, cd=180, reqs={SpecReq({Priest.Disc}), LevelReq(44)}}, -- Power Word: Barrier
- ---- Priest.Holy
- {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
- {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
- {type="EXTERNAL", id=47788, cd=180, reqs={SpecReq({Priest.Holy}), LevelReq(38)}, mods={{reqs={TalentReq(200209)}, mod=GuardianAngelMod}}}, -- Guardian Spirit
- {type="HEALING", id=64843, cd=180, reqs={SpecReq({Priest.Holy}), LevelReq(44)}}, -- Divine Hymn
- {type="UTILITY", id=64901, cd=300, reqs={SpecReq({Priest.Holy}), LevelReq(47)}}, -- Symbol of Hope
- ---- Priest.Shadow
- {type="PERSONAL", id=47585, cd=120, reqs={SpecReq({Priest.Shadow}), LevelReq(16)}, mods={{reqs={TalentReq(288733)}, mod=SubtractMod(30)}}}, -- Dispersion
- {type="DISPEL", id=213634, cd=8, reqs={SpecReq({Priest.Shadow}), LevelReq(18)}}, -- Purify Disease
- {type="DAMAGE", id=228260, cd=90, reqs={SpecReq({Priest.Shadow}), LevelReq(23)}}, -- Void Eruption
- {type="HEALING", id=15286, cd=120, reqs={SpecReq({Priest.Shadow}), LevelReq(38)}, mods={{reqs={TalentReq(199855)}, mod=SubtractMod(45)}}}, -- Vampiric Embrace
- {type="INTERRUPT", id=15487, cd=45, reqs={SpecReq({Priest.Shadow}), LevelReq(41)}, mods={{reqs={TalentReq(263716)}, mod=SubtractMod(15)}}}, -- Silence
- {type="DAMAGE", id=10060, cd=120, reqs={SpecReq({Priest.Shadow}), LevelReq(58)}}, -- Power Infusion
- ---- Talents
- {type="HARDCC", id=205369, cd=30, reqs={TalentReq(205369)}}, -- Mind Bomb
- {type="SOFTCC", id=204263, cd=45, reqs={TalentReq(204263)}}, -- Shining Force
- {type="STHARDCC", id=64044, cd=45, reqs={TalentReq(64044)}}, -- Psychic Horror
- {type="HEALING", id=109964, cd=60, reqs={TalentReq(109964)}}, -- Spirit Shell
- {type="HEALING", id=200183, cd=120, reqs={TalentReq(200183)}}, -- Apotheosis
- {type="HEALING", id=246287, cd=90, reqs={TalentReq(246287)}}, -- Evangelism
- {type="HEALING", id=265202, cd=720, reqs={TalentReq(265202)}, mods={{mod=CastDeltaMod(34861,-30)}, {mod=CastDeltaMod(2050,-30)}}}, -- Holy Word: Salvation
- {type="DAMAGE", id=319952, cd=90, reqs={TalentReq(319952)}}, -- Surrender to Madness
- -- Rogue
- ---- Base
- {type="UTILITY", id=57934, cd=30, reqs={ClassReq(Rogue), LevelReq(44)}}, -- Tricks of the Trade
- {type="UTILITY", id=114018, cd=360, reqs={ClassReq(Rogue), LevelReq(47)}}, -- Shroud of Concealment
- {type="UTILITY", id=1856, cd=120, reqs={ClassReq(Rogue), LevelReq(31)}}, -- Vanish
- {type="IMMUNITY", id=31224, cd=120, reqs={ClassReq(Rogue), LevelReq(49)}}, -- Cloak of Shadows
- {type="STHARDCC", id=408, cd=20, reqs={ClassReq(Rogue), LevelReq(20)}}, -- Kidney Shot
- {type="UTILITY", id=1725, cd=30, reqs={ClassReq(Rogue), LevelReq(36)}}, -- Distract
- {type="STSOFTCC", id=2094, cd=120, reqs={ClassReq(Rogue), LevelReq(41)}, mods={{reqs={TalentReq(256165)}, mod=SubtractMod(30)}}}, -- Blind
- {type="PERSONAL", id=5277, cd=120, reqs={ClassReq(Rogue), LevelReq(23)}}, -- Evasion
- {type="INTERRUPT", id=1766, cd=15, reqs={ClassReq(Rogue), LevelReq(6)}}, -- Kick
- {type="PERSONAL", id=185311, cd=30, reqs={ClassReq(Rogue), LevelReq(8)}}, -- Crimson Vial
- ---- Rogue.Sin
- {type="DAMAGE", id=79140, cd=120, reqs={SpecReq({Rogue.Sin}), LevelReq(34)}}, -- Vendetta
- ---- Rogue.Outlaw
- {type="DAMAGE", id=13877, cd=30, reqs={SpecReq({Rogue.Outlaw}), LevelReq(33)}, mods={{reqs={SpecReq({Rogue.Outlaw}), TalentReq(272026)}, mod=SubtractMod(-3)}}}, -- Blade Flurry
- {type="DAMAGE", id=13750, cd=180, reqs={SpecReq({Rogue.Outlaw}), LevelReq(34)}}, -- Adrenaline Rush
- {type="STSOFTCC", id=1776, cd=15, reqs={SpecReq({Rogue.Outlaw}), LevelReq(46)}, version=101}, -- Gouge
- ---- Rogue.Sub
- {type="DAMAGE", id=121471, cd=180, reqs={SpecReq({Rogue.Sub}), LevelReq(34)}}, -- Shadow Blades
- ---- Talents
- {type="DAMAGE", id=343142, cd=90, reqs={SpecReq({Rogue.Outlaw}), TalentReq(343142)}}, -- Dreadblades
- {type="DAMAGE", id=271877, cd=45, reqs={SpecReq({Rogue.Outlaw}), TalentReq(271877)}}, -- Blade Rush
- {type="DAMAGE", id=51690, cd=120, reqs={SpecReq({Rogue.Outlaw}), TalentReq(51690)}}, -- Killing Spree
- {type="DAMAGE", id=277925, cd=60, reqs={SpecReq({Rogue.Sub}), TalentReq(277925)}}, -- Shuriken Tornado
- -- Shaman
- -- TODO: Add support for Reincarnation
- ---- Base
- {type="UTILITY", id=20608, cd=1800, reqs={ClassReq(Shaman), LevelReq(8)}}, -- Reincarnation
- {type="INTERRUPT", id=57994, cd=12, reqs={ClassReq(Shaman), LevelReq(12)}}, -- Wind Shear
- {type="HARDCC", id=192058, cd=60, reqs={ClassReq(Shaman), LevelReq(23)}, mods={{reqs={TalentReq(265046)}, mod=StaticChargeMod}}}, -- Capacitor Totem
- {type="UTILITY", id=198103, cd=300, reqs={ClassReq(Shaman), LevelReq(37)}}, -- Earth Elemental
- {type="STSOFTCC", id=51514, cd=30, reqs={ClassReq(Shaman), LevelReq(41)}, mods={{reqs={LevelReq(56)}, mod=SubtractMod(10)}}}, -- Hex
- {type="PERSONAL", id=108271, cd=90, reqs={ClassReq(Shaman), LevelReq(42)}}, -- Astral Shift
- {type="DISPEL", id=8143, cd=60, reqs={ClassReq(Shaman), LevelReq(47)}}, -- Tremor Totem
- ---- Shared
- {type="DISPEL", id=51886, cd=8, reqs={SpecReq({Shaman.Ele, Shaman.Enh}), LevelReq(18)}, mods={{mod=DispelMod(51886)}}, ignoreCast=true}, -- Cleanse Spirit
- {type="UTILITY", id=79206, cd=120, reqs={SpecReq({Shaman.Ele, Shaman.Resto}), LevelReq(44)}, mods={{reqs={TalentReq(192088)}, mod=SubtractMod(60)}}}, -- Spiritwalker's Grace
- ---- Shaman.Ele
- {type="DAMAGE", id=198067, cd=150, reqs={SpecReq({Shaman.Ele}), LevelReq(34), NoTalentReq(192249)}}, -- Fire Elemental
- ---- Shaman.Enh
- {type="DAMAGE", id=51533, cd=120, reqs={SpecReq({Shaman.Enh}), LevelReq(34)}, mods={{reqs={SpecReq({Shaman.Enh}), TalentReq(262624)}, mod=SubtractMod(30)}}}, -- Feral Spirit
- ---- Shaman.Resto
- {type="DISPEL", id=77130, cd=8, reqs={SpecReq({Shaman.Resto}), LevelReq(18)}, mods={{mod=DispelMod(77130)}}, ignoreCast=true}, -- Purify Spirit
- {type="UTILITY", id=16191, cd=180, reqs={SpecReq({Shaman.Resto}), LevelReq(38)}}, -- Mana Tide Totem
- {type="RAIDCD", id=98008, cd=180, reqs={SpecReq({Shaman.Resto}), LevelReq(43)}, version=101}, -- Spirit Link Totem
- {type="UTILITY", id=79206, cd=120, reqs={SpecReq({Shaman.Resto}), LevelReq(44)}, mods={{reqs={SpecReq({Shaman.Resto}), TalentReq(192088)}, mod=SubtractMod(60)}}}, -- Spiritwalker's Grace
- {type="HEALING", id=108280, cd=180, reqs={SpecReq({Shaman.Resto}), LevelReq(49)}}, -- Healing Tide Totem
- ---- Talents
- {type="SOFTCC", id=51485, cd=30, reqs={SpecReq({Shaman.Resto}), TalentReq(51485)}}, -- Earthgrab Totem
- {type="HEALING", id=198838, cd=60, reqs={SpecReq({Shaman.Resto}), TalentReq(198838)}}, -- Earthen Wall Totem
- {type="DAMAGE", id=192249, cd=150, reqs={SpecReq({Shaman.Ele}), TalentReq(192249)}}, -- Fire Elemental
- {type="EXTERNAL", id=207399, cd=300, reqs={SpecReq({Shaman.Resto}), TalentReq(207399)}}, -- Ancestral Protection Totem
- {type="HEALING", id=108281, cd=120, reqs={SpecReq({Shaman.Ele}), TalentReq(108281)}}, -- Ancestral Guidance
- {type="UTILITY", id=192077, cd=120, reqs={ClassReq(Shaman), TalentReq(192077)}}, -- Wind Rush Totem
- {type="DAMAGE", id=191634, cd=60, reqs={SpecReq({Shaman.Ele}), TalentReq(191634)}}, -- Stormkeeper
- {type="HEALING", id=114052, cd=180, reqs={SpecReq({Shaman.Resto}), TalentReq(114052)}}, -- Ascendance
- {type="DAMAGE", id=114050, cd=180, reqs={SpecReq({Shaman.Ele}), TalentReq(114050)}}, -- Ascendance
- {type="DAMAGE", id=114051, cd=180, reqs={SpecReq({Shaman.Enh}), TalentReq(114051)}}, -- Ascendance
- -- Warlock
- -- TODO: Soulstone (Brez Support)
- -- TODO: PetReq for Spell Lock and Axe Toss
- ---- Base
- {type="PERSONAL", id=104773, cd=180, reqs={ClassReq(Warlock), LevelReq(4)}}, -- Unending Resolve
- {type="UTILITY", id=333889, cd=180, reqs={ClassReq(Warlock), LevelReq(22)}}, -- Fel Domination
- {type="BREZ", id=20707, cd=600, reqs={ClassReq(Warlock), LevelReq(48)}}, -- Soulstone
- {type="HARDCC", id=30283, cd=60, reqs={ClassReq(Warlock), LevelReq(38)}, mods={{reqs={TalentReq(264874)}, mod=SubtractMod(15)}}}, -- Shadowfury
- ---- Shared
- {type="INTERRUPT", id=19647, cd=24, reqs={SpecReq({Warlock.Affl, Warlock.Destro}), LevelReq(29)}}, -- Spell Lock
- ---- Warlock.Affl
- {type="DAMAGE", id=205180, cd=180, reqs={SpecReq({Warlock.Affl}), LevelReq(42)}, mods={{reqs={TalentReq(334183)}, mod=SubtractMod(60)}}}, -- Summon Darkglare
- ---- Warlock.Demo
- {type="INTERRUPT", id=89766, cd=30, reqs={SpecReq({Warlock.Demo}), LevelReq(29)}}, -- Axe Toss
- {type="DAMAGE", id=265187, cd=90, reqs={SpecReq({Warlock.Demo}), LevelReq(42)}}, -- Summon Demonic Tyrant
- ---- Warlock.Destro
- {type="DAMAGE", id=1122, cd=180, reqs={SpecReq({Warlock.Destro}), LevelReq(42)}}, -- Summon Infernal
- ---- Talents
- {type="PERSONAL", id=108416, cd=60, reqs={ClassReq(Warlock), TalentReq(108416)}}, -- Dark Pact
- {type="DAMAGE", id=152108, cd=30, reqs={SpecReq({Warlock.Destro}), TalentReq(152108)}}, -- Cataclysm
- {type="STHARDCC", id=6789, cd=45, reqs={ClassReq(Warlock), TalentReq(6789)}}, -- Mortal Coil
- {type="SOFTCC", id=5484, cd=40, reqs={ClassReq(Warlock), TalentReq(5484)}}, -- Howl of Terror
- {type="DAMAGE", id=111898, cd=120, reqs={SpecReq({Warlock.Demo}), TalentReq(111898)}}, -- Grimoire: Felguard
- {type="DAMAGE", id=113858, cd=120, reqs={SpecReq({Warlock.Destro}), TalentReq(113858)}}, -- Dark Soul: Instability
- {type="DAMAGE", id=267217, cd=180, reqs={SpecReq({Warlock.Demo}), TalentReq(267217)}}, -- Nether Portal
- {type="DAMAGE", id=113860, cd=120, reqs={SpecReq({Warlock.Affl}), TalentReq(113860)}}, -- Dark Soul: Misery
- -- Warrior
- -- Note: Spell Reflection is separated to generate REMOVE/ADD events (Request from Nnoggie)
- ---- Base
- {type="INTERRUPT", id=6552, cd=15, reqs={ClassReq(Warrior), LevelReq(7)}}, -- Pummel
- {type="TANK", id=355, cd=8, reqs={ClassReq(Warrior), LevelReq(14)}}, -- Taunt
- {type="SOFTCC", id=5246, cd=90, reqs={ClassReq(Warrior), LevelReq(34)}}, -- Intimidating Shout
- {type="UTILITY", id=64382, cd=180, reqs={ClassReq(Warrior), LevelReq(41)}}, -- Shattering Throw
- {type="EXTERNAL", id=3411, cd=30, reqs={ClassReq(Warrior), LevelReq(43)}}, -- Intervene
- {type="RAIDCD", id=97462, cd=180, reqs={ClassReq(Warrior), LevelReq(46)}}, -- Rallying Cry
- {type="TANK", id=1161, cd=240, reqs={ClassReq(Warrior), LevelReq(54)}}, -- Challenging Shout
- ---- Shared
- {type="PERSONAL", id=23920, cd=25, reqs={SpecReq({Warrior.Arms, Warrior.Fury}), LevelReq(47)}}, -- Spell Reflection
- ---- Warrior.Arms
- {type="PERSONAL", id=118038, cd=180, reqs={SpecReq({Warrior.Arms}), LevelReq(23)}, mods={{reqs={LevelReq(52)}, mod=SubtractMod(60)}}}, -- Die by the Sword
- {type="DAMAGE", id=227847, cd=90, reqs={SpecReq({Warrior.Arms}), LevelReq(38)}, mods={{reqs={TalentReq(152278)}, mod=ResourceSpendingMods(Warrior.Arms, 0.05)}}}, -- Bladestorm
- ---- Warrior.Fury
- {type="PERSONAL", id=184364, cd=180, reqs={SpecReq({Warrior.Fury}), LevelReq(23)}, mods={{reqs={LevelReq(32)}, mod=SubtractMod(60)}}}, -- Enraged Regeneration
- {type="DAMAGE", id=1719, cd=90, reqs={SpecReq({Warrior.Fury}), LevelReq(38)}, mods={{reqs={TalentReq(152278)}, mod=ResourceSpendingMods(Warrior.Arms, 0.05)}}}, -- Recklessness
- ---- Warrior.Prot
- {type="HARDCC", id=46968, cd=40, reqs={SpecReq({Warrior.Prot}), LevelReq(21)}, mods={{reqs={TalentReq(275339)}, mod=RumblingEarthMod}}}, -- Shockwave
- {type="TANK", id=871, cd=240, reqs={SpecReq({Warrior.Prot}), LevelReq(23)}, mods={{reqs={TalentReq(152278)}, mod=ResourceSpendingMods(Warrior.Arms, 0.1)}}}, -- Shield Wall
- {type="TANK", id=1160, cd=45, reqs={SpecReq({Warrior.Prot}), LevelReq(27)}}, -- Demoralizing Shout
- {type="DAMAGE", id=107574, cd=90, reqs={SpecReq({Warrior.Prot}), LevelReq(32)}, mods={{reqs={TalentReq(152278)}, mod=ResourceSpendingMods(Warrior.Arms, 0.1)}}}, -- Avatar
- {type="TANK", id=12975, cd=180, reqs={SpecReq({Warrior.Prot}), LevelReq(38)}, mods={{reqs={TalentReq(280001)}, mod=SubtractMod(60)}}}, -- Last Stand
- {type="PERSONAL", id=23920, cd=25, reqs={SpecReq({Warrior.Prot}), LevelReq(47)}}, -- Spell Reflection
- ---- Talents
- {type="STHARDCC", id=107570, cd=30, reqs={ClassReq(Warrior), TalentReq(107570)}}, -- Storm Bolt
- {type="DAMAGE", id=107574, cd=90, reqs={SpecReq({Warrior.Arms}), TalentReq(107574)}}, -- Avatar
- {type="DAMAGE", id=262228, cd=60, reqs={SpecReq({Warrior.Arms}), TalentReq(262228)}}, -- Deadly Calm
- {type="DAMAGE", id=228920, cd=45, reqs={SpecReq({Warrior.Prot}), TalentReq(228920)}}, -- Ravager
- {type="DAMAGE", id=46924, cd=60, reqs={SpecReq({Warrior.Fury}), TalentReq(46924)}}, -- Bladestorm
- {type="DAMAGE", id=152277, cd=45, reqs={SpecReq({Warrior.Arms}), TalentReq(152277)}}, -- Ravager
- {type="DAMAGE", id=280772, cd=30, reqs={SpecReq({Warrior.Fury}), TalentReq(280772)}}, -- Siegebreaker
- }
- ZT.linkedSpellIDs = {
- [19647] = {119910, 132409, 115781}, -- Spell Lock
- [89766] = {119914, 347008}, -- Axe Toss
- [51514] = {211004, 211015, 277778, 309328, 210873, 211010, 269352, 277784}, -- Hex
- [132469] = {61391}, -- Typhoon
- [191427] = {200166}, -- Metamorphosis
- [106898] = {77761, 77764}, -- Stampeding Roar
- [86659] = {212641}, -- Guardian of the Ancient Kings (+Glyph)
- }
- ZT.separateLinkedSpellIDs = {
- [86659] = {212641}, -- Guardian of the Ancient Kings (+Glyph)
- }
- --##############################################################################
- -- Handling custom spells specified by the user in the configuration
- local spellConfigPrefix = "return function(DH,DK,Druid,Hunter,Mage,Monk,Paladin,Priest,Rogue,Shaman,Warlock,Warrior,LevelReq,RaceReq,ClassReq,SpecReq,TalentReq,NoTalentReq,SubtractMod,MultiplyMod,ChargesMod,DynamicMod,EventDeltaMod,CastDeltaMod,EventRemainingMod,CastRemainingMod,DispelMod) return "
- local spellConfigSuffix = "end"
- local function trim(s) -- From PiL2 20.4
- if s ~= nil then
- return s:gsub("^%s*(.-)%s*$", "%1")
- end
- return ""
- end
- local function addCustomSpell(spellConfig, i)
- if not spellConfig or type(spellConfig) ~= "table" then
- prerror("Custom Spell", i, "is not represented as a valid table")
- return
- end
- if type(spellConfig.type) ~= "string" then
- prerror("Custom Spell", i, "does not have a valid 'type' entry")
- return
- end
- if type(spellConfig.id) ~= "number" then
- prerror("Custom Spell", i, "does not have a valid 'id' entry")
- return
- end
- if type(spellConfig.cd) ~= "number" then
- prerror("Custom Spell", i, "does not have a valid 'cd' entry")
- return
- end
- spellConfig.version = 10000
- spellConfig.isCustom = true
- ZT.spellList[#ZT.spellList + 1] = spellConfig
- end
- for i = 1,16 do
- local spellConfig = trim(ZT.config["custom"..i])
- if spellConfig ~= "" then
- local spellConfigFunc = WeakAuras.LoadFunction(spellConfigPrefix..spellConfig..spellConfigSuffix, "ZenTracker Custom Spell "..i)
- if spellConfigFunc then
- local spell = spellConfigFunc(DH,DK,Druid,Hunter,Mage,Monk,Paladin,Priest,Rogue,Shaman,Warlock,Warrior,LevelReq,RaceReq,ClassReq,SpecReq,TalentReq,NoTalentReq,SubtractMod,MultiplyMod,ChargesMod,DynamicMod,EventDeltaMod,CastDeltaMod,EventRemainingMod,CastRemainingMod,DispelMod)
- addCustomSpell(spell, i)
- end
- end
- end
- --##############################################################################
- -- Compiling the complete indexed tables of spells
- ZT.spells = DefaultTable_Create(function() return DefaultTable_Create(function() return {} end) end)
- for _,spellInfo in ipairs(ZT.spellList) do
- spellInfo.version = spellInfo.version or 100
- spellInfo.isRegistered = false
- spellInfo.frontends = {}
- -- Indexing for faster lookups based on the info/requirements
- if spellInfo.reqs and (#spellInfo.reqs > 0) then
- for _,req in ipairs(spellInfo.reqs) do
- if req.indices then
- for _,index in ipairs(req.indices) do
- tinsert(ZT.spells[req.type][index], spellInfo)
- end
- end
- end
- else
- tinsert(ZT.spells["generic"], spellInfo)
- end
- if spellInfo.mods then
- for _,mod in ipairs(spellInfo.mods) do
- if mod.reqs then
- for _,req in ipairs(mod.reqs) do
- for _,index in ipairs(req.indices) do
- tinsert(ZT.spells[req.type][index], spellInfo)
- end
- end
- end
- end
- end
- tinsert(ZT.spells["type"][spellInfo.type], spellInfo)
- tinsert(ZT.spells["id"][spellInfo.id], spellInfo)
- end
- --##############################################################################
- -- Handling combatlog and WeakAura events by invoking specified callbacks
- ZT.eventHandlers = { handlers = {} }
- function ZT.eventHandlers:add(type, spellID, sourceGUID, func, data)
- local types = self.handlers[spellID]
- if not types then
- types = {}
- self.handlers[spellID] = types
- end
- local sources = types[type]
- if not sources then
- sources = {}
- types[type] = sources
- end
- local handlers = sources[sourceGUID]
- if not handlers then
- handlers = {}
- sources[sourceGUID] = handlers
- end
- handlers[func] = data
- end
- function ZT.eventHandlers:remove(type, spellID, sourceGUID, func)
- local types = self.handlers[spellID]
- if types then
- local sources = types[type]
- if sources then
- local handlers = sources[sourceGUID]
- if handlers then
- handlers[func] = nil
- end
- end
- end
- end
- function ZT.eventHandlers:removeAll(sourceGUID)
- for _,spells in pairs(self.eventHandlers) do
- for _,sources in pairs(spells) do
- for GUID,handlers in pairs(sources) do
- if GUID == sourceGUID then
- wipe(handlers)
- end
- end
- end
- end
- end
- local function fixSourceGUID(sourceGUID) -- Based on https://wago.io/p/Nnogga
- local type = strsplit("-", sourceGUID)
- if type == "Pet" then
- for unit in WA_IterateGroupMembers() do
- if UnitGUID(unit.."pet") == sourceGUID then
- sourceGUID = UnitGUID(unit)
- break
- end
- end
- end
- return sourceGUID
- end
- function ZT.eventHandlers:handle(type, spellID, sourceGUID)
- local types = self.handlers[spellID]
- if not types then
- return
- end
- local sources = types[type]
- if not sources then
- return
- end
- local handlers = sources[sourceGUID]
- if not handlers then
- sourceGUID = fixSourceGUID(sourceGUID)
- handlers = sources[sourceGUID]
- if not handlers then
- return
- end
- end
- for func,data in pairs(handlers) do
- func(data, spellID)
- end
- end
- --##############################################################################
- -- Managing timer callbacks in a way that allows for updates/removals
- ZT.timers = { heap={}, callbackTimes={} }
- function ZT.timers:fixHeapUpwards(index)
- local heap = self.heap
- local timer = heap[index]
- local parentIndex, parentTimer
- while index > 1 do
- parentIndex = floor(index / 2)
- parentTimer = heap[parentIndex]
- if timer.time >= parentTimer.time then
- break
- end
- parentTimer.index = index
- heap[index] = parentTimer
- index = parentIndex
- end
- if timer.index ~= index then
- timer.index = index
- heap[index] = timer
- end
- end
- function ZT.timers:fixHeapDownwards(index)
- local heap = self.heap
- local timer = heap[index]
- local childIndex, minChildTimer, leftChildTimer, rightChildTimer
- while true do
- childIndex = 2 * index
- leftChildTimer = heap[childIndex]
- if leftChildTimer then
- rightChildTimer = heap[childIndex + 1]
- if rightChildTimer and (rightChildTimer.time < leftChildTimer.time) then
- minChildTimer = rightChildTimer
- else
- minChildTimer = leftChildTimer
- end
- else
- break
- end
- if timer.time <= minChildTimer.time then
- break
- end
- childIndex = minChildTimer.index
- minChildTimer.index = index
- heap[index] = minChildTimer
- index = childIndex
- end
- if timer.index ~= index then
- timer.index = index
- heap[index] = timer
- end
- end
- function ZT.timers:setupCallback()
- local minTimer = self.heap[1]
- if minTimer then
- local timeNow = GetTime()
- local remaining = minTimer.time - timeNow
- if remaining <= 0 then
- self:handle()
- elseif not self.callbackTimes[minTimer.time] then
- for time,_ in pairs(self.callbackTimes) do
- if time < timeNow then
- self.callbackTimes[time] = nil
- end
- end
- self.callbackTimes[minTimer.time] = true
- -- Note: This 0.001 avoids early callbacks that I ran into
- remaining = remaining + 0.001
- prdebug(DEBUG_TIMER, "Setting callback for handling timers after", remaining, "seconds")
- C_Timer.After(remaining, function() self:handle() end)
- end
- end
- end
- function ZT.timers:handle()
- local timeNow = GetTime()
- local heap = self.heap
- local minTimer = heap[1]
- prdebug(DEBUG_TIMER, "Handling timers at time", timeNow, "( Min @", minTimer and minTimer.time or "NONE", ")")
- while minTimer and minTimer.time <= timeNow do
- local heapSize = #heap
- if heapSize > 1 then
- heap[1] = heap[heapSize]
- heap[1].index = 1
- heap[heapSize] = nil
- self:fixHeapDownwards(1)
- else
- heap[1] = nil
- end
- minTimer.index = -1
- minTimer.callback()
- minTimer = heap[1]
- end
- self:setupCallback()
- end
- function ZT.timers:add(time, callback)
- local heap = self.heap
- local index = #heap + 1
- local timer = {time=time, callback=callback, index=index}
- heap[index] = timer
- self:fixHeapUpwards(index)
- self:setupCallback()
- return timer
- end
- function ZT.timers:cancel(timer)
- local index = timer.index
- if index == -1 then
- return
- end
- timer.index = -1
- local heap = self.heap
- local heapSize = #heap
- if heapSize ~= index then
- heap[index] = heap[heapSize]
- heap[index].index = index
- heap[heapSize] = nil
- self:fixHeapDownwards(index)
- self:setupCallback()
- else
- heap[index] = nil
- end
- end
- function ZT.timers:update(timer, time)
- local fixHeapFunc = (time <= timer.time) and self.fixHeapUpwards or self.fixHeapDownwards
- timer.time = time
- fixHeapFunc(self, timer.index)
- self:setupCallback()
- end
- --##############################################################################
- -- Managing the set of spells that are being watched
- local WatchInfo = { nextID = 1 }
- local WatchInfoMT = { __index = WatchInfo }
- ZT.watching = {}
- function WatchInfo:create(member, spellInfo, isHidden)
- local watchInfo = {
- id = self.nextID,
- member = member,
- spellInfo = spellInfo,
- duration = spellInfo.cd,
- expiration = GetTime(),
- charges = spellInfo.charges,
- maxCharges = spellInfo.charges,
- isHidden = isHidden,
- isLazy = spellInfo.isLazy,
- ignoreSharing = false,
- }
- self.nextID = self.nextID + 1
- watchInfo = setmetatable(watchInfo, WatchInfoMT)
- watchInfo:updateModifiers()
- return watchInfo
- end
- function WatchInfo:updateModifiers()
- if not self.spellInfo.mods then
- return
- end
- self.duration = self.spellInfo.cd
- self.charges = self.spellInfo.charges
- self.maxCharges = self.spellInfo.charges
- for _,modifier in ipairs(self.spellInfo.mods) do
- if modifier.mod.type == "Static" then
- if self.member:checkRequirements(modifier.reqs) then
- modifier.mod.func(self)
- end
- end
- end
- end
- function WatchInfo:sendAddEvent()
- if not self.isLazy and not self.isHidden then
- local spellInfo = self.spellInfo
- prdebug(DEBUG_EVENT, "Sending ZT_ADD", spellInfo.type, self.id, self.member.name, spellInfo.id, self.duration, self.charges)
- WeakAuras.ScanEvents("ZT_ADD", spellInfo.type, self.id, self.member, spellInfo.id, self.duration, self.charges)
- if self.expiration > GetTime() then
- self:sendTriggerEvent()
- end
- end
- end
- function WatchInfo:sendTriggerEvent()
- if self.isLazy then
- self.isLazy = false
- self:sendAddEvent()
- end
- if not self.isHidden then
- prdebug(DEBUG_EVENT, "Sending ZT_TRIGGER", self.spellInfo.type, self.id, self.duration, self.expiration, self.charges)
- WeakAuras.ScanEvents("ZT_TRIGGER", self.spellInfo.type, self.id, self.duration, self.expiration, self.charges)
- end
- end
- function WatchInfo:sendRemoveEvent()
- if not self.isLazy and not self.isHidden then
- prdebug(DEBUG_EVENT, "Sending ZT_REMOVE", self.spellInfo.type, self.id)
- WeakAuras.ScanEvents("ZT_REMOVE", self.spellInfo.type, self.id)
- end
- end
- function WatchInfo:hide()
- if not self.isHidden then
- self:sendRemoveEvent()
- self.isHidden = true
- end
- end
- function WatchInfo:unhide(suppressAddEvent)
- if self.isHidden then
- self.isHidden = false
- if not suppressAddEvent then
- self:sendAddEvent()
- end
- end
- end
- function WatchInfo:toggleHidden(toggle, suppressAddEvent)
- if toggle then
- self:hide()
- else
- self:unhide(suppressAddEvent)
- end
- end
- function WatchInfo:handleReadyTimer()
- if self.charges then
- self.charges = self.charges + 1
- -- If we are not at max charges, update expiration and start a ready timer
- if self.charges < self.maxCharges then
- self.expiration = self.expiration + self.duration
- prdebug(DEBUG_TIMER, "Adding ready timer of", self.expiration, "for spellID", self.spellInfo.id)
- self.readyTimer = ZT.timers:add(self.expiration, function() self:handleReadyTimer() end)
- else
- self.readyTimer = nil
- end
- else
- self.readyTimer = nil
- end
- self:sendTriggerEvent()
- end
- function WatchInfo:updateReadyTimer() -- Returns true if a timer was set, false if handled immediately
- if self.expiration > GetTime() then
- if self.readyTimer then
- prdebug(DEBUG_TIMER, "Updating ready timer from", self.readyTimer.time, "to", self.expiration, "for spellID", self.spellInfo.id)
- ZT.timers:update(self.readyTimer, self.expiration)
- else
- prdebug(DEBUG_TIMER, "Adding ready timer of", self.expiration, "for spellID", self.spellInfo.id)
- self.readyTimer = ZT.timers:add(self.expiration, function() self:handleReadyTimer() end)
- end
- return true
- else
- if self.readyTimer then
- prdebug(DEBUG_TIMER, "Canceling ready timer for spellID", self.spellInfo.id)
- ZT.timers:cancel(self.readyTimer)
- self.readyTimer = nil
- end
- self:handleReadyTimer(self.expiration)
- return false
- end
- end
- function WatchInfo:startCD()
- if self.charges then
- if self.charges == 0 or self.charges == self.maxCharges then
- self.expiration = GetTime() + self.duration
- self:updateReadyTimer()
- end
- if self.charges > 0 then
- self.charges = self.charges - 1
- end
- else
- self.expiration = GetTime() + self.duration
- self:updateReadyTimer()
- end
- self:sendTriggerEvent()
- end
- function WatchInfo:updateCDDelta(delta)
- self.expiration = self.expiration + delta
- local time = GetTime()
- local remaining = self.expiration - time
- if self.charges and remaining <= 0 then
- local chargesGained = 1 - floor(remaining / self.duration)
- self.charges = max(self.charges + chargesGained, self.maxCharges)
- if self.charges == self.maxCharges then
- self.expiration = time
- else
- self.expiration = self.expiration + (chargesGained * self.duration)
- end
- end
- if self:updateReadyTimer() then
- self:sendTriggerEvent()
- end
- end
- function WatchInfo:updateCDRemaining(remaining)
- -- Note: This assumes that when remaining is 0 and the spell uses charges then it gains a charge
- if self.charges and remaining == 0 then
- if self.charges < self.maxCharges then
- self.charges = self.charges + 1
- end
- -- Below maximum charges the expiration time doesn't change
- if self.charges < self.maxCharges then
- self:sendTriggerEvent()
- else
- self.expiration = GetTime()
- self:updateReadyTimer()
- end
- else
- self.expiration = GetTime() + remaining
- if self:updateReadyTimer() then
- self:sendTriggerEvent()
- end
- end
- end
- function WatchInfo:updatePlayerCharges()
- local charges = GetSpellCharges(self.spellInfo.id)
- if charges then
- self.charges = charges
- end
- end
- function WatchInfo:updatePlayerCD(spellID, ignoreIfReady)
- local startTime, duration, enabled
- if self.charges then
- self.charges, self.maxCharges, startTime, duration = GetSpellCharges(spellID)
- if self.charges == self.maxCharges then
- startTime = 0
- end
- enabled = 1
- else
- startTime, duration, enabled = GetSpellCooldown(spellID)
- end
- if enabled ~= 0 then
- local ignoreRateLimit
- if startTime ~= 0 then
- ignoreRateLimit = (self.expiration < GetTime())
- self.duration = duration
- self.expiration = startTime + duration
- else
- ignoreRateLimit = true
- self.expiration = GetTime()
- end
- if (not ignoreIfReady) or (startTime ~= 0) then
- ZT:sendCDUpdate(self, ignoreRateLimit)
- self:sendTriggerEvent()
- end
- end
- end
- function ZT:togglePlayerHandlers(watchInfo, enable)
- local spellID = watchInfo.spellInfo.id
- local toggleHandlerFunc = enable and self.eventHandlers.add or self.eventHandlers.remove
- if enable then
- WeakAuras.WatchSpellCooldown(spellID)
- end
- toggleHandlerFunc(self.eventHandlers, "SPELL_COOLDOWN_CHANGED", spellID, 0, watchInfo.updatePlayerCD, watchInfo)
- local links = self.separateLinkedSpellIDs[spellID]
- if links then
- for _,linkedSpellID in ipairs(links) do
- if enable then
- WeakAuras.WatchSpellCooldown(linkedSpellID)
- end
- toggleHandlerFunc(self.eventHandlers, "SPELL_COOLDOWN_CHANGED", linkedSpellID, 0, watchInfo.updatePlayerCD, watchInfo)
- end
- end
- end
- function ZT:toggleCombatLogHandlers(watchInfo, enable)
- local spellInfo = watchInfo.spellInfo
- local spellID = spellInfo.id
- local member = watchInfo.member
- local toggleHandlerFunc = enable and self.eventHandlers.add or self.eventHandlers.remove
- if not spellInfo.ignoreCast then
- toggleHandlerFunc(self.eventHandlers, "SPELL_CAST_SUCCESS", spellID, member.GUID, watchInfo.startCD, watchInfo)
- local links = self.linkedSpellIDs[spellID]
- if links then
- for _,linkedSpellID in ipairs(links) do
- toggleHandlerFunc(self.eventHandlers, "SPELL_CAST_SUCCESS", linkedSpellID, member.GUID, watchInfo.startCD, watchInfo)
- end
- end
- end
- if spellInfo.mods then
- for _,modifier in ipairs(spellInfo.mods) do
- if modifier.mod.type == "Dynamic" then
- if not enable or member:checkRequirements(modifier.reqs) then
- for _,handlerInfo in ipairs(modifier.mod.handlers) do
- toggleHandlerFunc(self.eventHandlers, handlerInfo.type, handlerInfo.spellID, member.GUID, handlerInfo.handler, watchInfo)
- end
- end
- end
- end
- end
- end
- function ZT:watch(spellInfo, member)
- -- Only handle registered spells (or those for the player)
- if not spellInfo.isRegistered and not member.isPlayer then
- return
- end
- -- Only handle spells that meet all the requirements for the member
- if not member:checkRequirements(spellInfo.reqs) then
- return
- end
- local spellID = spellInfo.id
- local spells = self.watching[spellID]
- if not spells then
- spells = {}
- self.watching[spellID] = spells
- end
- local isHidden = (member.isPlayer and not spellInfo.isRegistered) or member.isHidden
- local watchInfo = spells[member.GUID]
- local isNew = (watchInfo == nil)
- if not watchInfo then
- watchInfo = WatchInfo:create(member, spellInfo, isHidden)
- spells[member.GUID] = watchInfo
- member.watching[spellID] = watchInfo
- else
- -- If the type changed, send a remove event
- if not isHidden and spellInfo.type ~= watchInfo.spellInfo.type then
- watchInfo:sendRemoveEvent()
- end
- watchInfo.spellInfo = spellInfo
- watchInfo:updateModifiers()
- watchInfo:toggleHidden(isHidden, true) -- We will send the ZT_ADD event later
- end
- if member.isPlayer then
- watchInfo:updatePlayerCharges()
- watchInfo:sendAddEvent()
- watchInfo:updatePlayerCD(spellID, true)
- local links = self.separateLinkedSpellIDs[spellID]
- if links then
- for _,linkedSpellID in ipairs(links) do
- watchInfo:updatePlayerCD(linkedSpellID, true)
- end
- end
- else
- watchInfo:sendAddEvent()
- end
- if member.isPlayer and not TEST_CLEU then
- if isNew then
- self:togglePlayerHandlers(watchInfo, true)
- end
- elseif member.tracking == "CombatLog" or (member.tracking == "Sharing" and member.spellsVersion < spellInfo.version) then
- watchInfo.ignoreSharing = true
- if not isNew then
- self:toggleCombatLogHandlers(watchInfo, false)
- end
- self:toggleCombatLogHandlers(watchInfo, true)
- else
- watchInfo.ignoreSharing = false
- end
- end
- function ZT:unwatch(spellInfo, member)
- -- Only handle registered spells (or those for the player)
- if not spellInfo.isRegistered and not member.isPlayer then
- return
- end
- local spellID = spellInfo.id
- local sources = self.watching[spellID]
- if not sources then
- return
- end
- local watchInfo = sources[member.GUID]
- if not watchInfo then
- return
- end
- -- Ignoring unwatch requests if the spellInfo doesn't match (yet spellID does)
- -- (Note: This serves to ease updating after spec/talent changes)
- if watchInfo.spellInfo ~= spellInfo then
- return
- end
- if member.isPlayer and not TEST_CLEU then
- -- If called due to front-end unregistration, only hide it to allow continued sharing of updates
- -- Otherwise, called due to a spec/talent change, so actually unwatch it
- if not spellInfo.isRegistered then
- watchInfo:hide()
- return
- end
- self:togglePlayerHandlers(watchInfo, false)
- elseif member.tracking == "CombatLog" or (member.tracking == "Sharing" and member.spellsVersion < spellInfo.version) then
- self:toggleCombatLogHandlers(watchInfo, false)
- end
- if watchInfo.readyTimer then
- self.timers:cancel(watchInfo.readyTimer)
- end
- sources[member.GUID] = nil
- member.watching[spellID] = nil
- watchInfo:sendRemoveEvent()
- end
- --##############################################################################
- -- Tracking types registered by front-end WAs
- function ZT:registerSpells(frontendID, spells)
- for _,spellInfo in ipairs(spells) do
- local frontends = spellInfo.frontends
- if next(frontends, nil) ~= nil then
- -- Some front-end already registered for this spell, so just send ADD events
- local watched = self.watching[spellInfo.id]
- if watched then
- for _,watchInfo in pairs(watched) do
- if watchInfo.spellInfo == spellInfo then
- watchInfo:sendAddEvent()
- end
- end
- end
- else
- -- No front-end was registered for this spell, so watch as needed
- spellInfo.isRegistered = true
- for _,member in pairs(self.members) do
- if not member.isIgnored then
- self:watch(spellInfo, member)
- end
- end
- end
- frontends[frontendID] = true
- end
- end
- function ZT:unregisterSpells(frontendID, spells)
- for _,spellInfo in ipairs(spells) do
- local frontends = spellInfo.frontends
- frontends[frontendID] = nil
- if next(frontends, nil) == nil then
- local watched = self.watching[spellInfo.id]
- if watched then
- for _,watchInfo in pairs(watched) do
- if watchInfo.spellInfo == spellInfo then
- self:unwatch(spellInfo, watchInfo.member)
- end
- end
- end
- spellInfo.isRegistered = false
- end
- end
- end
- function ZT:toggleFrontEndRegistration(frontendID, info, toggle)
- local infoType = type(info)
- local registerFunc = toggle and self.registerSpells or self.unregisterSpells
- if infoType == "string" then -- Registration info is a type
- prdebug(DEBUG_EVENT, "Received", toggle and "ZT_REGISTER" or "ZT_UNREGISTER", "from", frontendID, "for type", info)
- registerFunc(self, frontendID, self.spells["type"][info])
- elseif infoType == "number" then -- Registration info is a spellID
- prdebug(DEBUG_EVENT, "Received", toggle and "ZT_REGISTER" or "ZT_UNREGISTER", "from", frontendID, "for spellID", info)
- registerFunc(self, frontendID, self.spells["id"][info])
- elseif infoType == "table" then -- Registration info is a table of types or spellIDs
- infoType = type(info[1])
- if infoType == "string" then
- prdebug(DEBUG_EVENT, "Received", toggle and "ZT_REGISTER" or "ZT_UNREGISTER", "from", frontendID, "for multiple types")
- for _,type in ipairs(info) do
- registerFunc(self, frontendID, self.spells["type"][type])
- end
- elseif infoType == "number" then
- prdebug(DEBUG_EVENT, "Received", toggle and "ZT_REGISTER" or "ZT_UNREGISTER", "from", frontendID, "for multiple spells")
- for _,spellID in ipairs(info) do
- registerFunc(self, frontendID, self.spells["id"][spellID])
- end
- end
- end
- end
- function ZT:registerFrontEnd(frontendID, info)
- self:toggleFrontEndRegistration(frontendID, info, true)
- end
- function ZT:unregisterFrontEnd(frontendID, info)
- self:toggleFrontEndRegistration(frontendID, info, false)
- end
- --##############################################################################
- -- Managing member information (e.g., spec, talents) for all group members
- local Member = { }
- local MemberMT = { __index = Member }
- ZT.members = {}
- ZT.inEncounter = false
- local membersToIgnore = {}
- if ZT.config["ignoreList"] then
- local ignoreListStr = trim(ZT.config["ignoreList"])
- if ignoreListStr ~= "" then
- ignoreListStr = "return "..ignoreListStr
- local ignoreList = WeakAuras.LoadFunction(ignoreListStr, "ZenTracker Ignore List")
- if ignoreList and (type(ignoreList) == "table") then
- for i,name in ipairs(ignoreList) do
- if type(name) == "string" then
- membersToIgnore[strlower(name)] = true
- else
- prerror("Ignore list entry", i, "is not a string. Skipping...")
- end
- end
- else
- prerror("Ignore list is not in the form of a table. Usage: {\"Zenlia\", \"Cistara\"}")
- end
- end
- end
- function Member:create(memberInfo)
- local member = memberInfo
- member.watching = {}
- member.tracking = member.tracking and member.tracking or "CombatLog"
- member.isPlayer = (member.GUID == UnitGUID("player"))
- member.isHidden = false
- member.isReady = false
- return setmetatable(member, MemberMT)
- end
- function Member:update(memberInfo)
- if memberInfo.level then
- self.level = memberInfo.level
- end
- self.specID = memberInfo.specID
- self.talents = memberInfo.talents
- self.talentsStr = memberInfo.talentsStr
- if memberInfo.unit then
- self.unit = memberInfo.unit
- end
- end
- function Member:gatherInfo()
- local _,className,_,race,_,name = GetPlayerInfoByGUID(self.GUID)
- self.name = name and gsub(name, "%-[^|]+", "") or nil
- self.class = className and AllClasses[className] or nil
- self.classID = className and AllClasses[className].ID or nil
- self.classColor = className and RAID_CLASS_COLORS[className] or nil
- self.race = race
- self.level = self.unit and UnitLevel(self.unit) or -1
- if (self.tracking == "Sharing") and self.name then
- prdebug(DEBUG_TRACKING, self.name, "is using ZenTracker with spellsVersion", self.spellsVersion)
- end
- if self.name and membersToIgnore[strlower(self.name)] then
- self.isIgnored = true
- return false
- end
- self.isReady = (self.name ~= nil) and (self.classID ~= nil) and (self.race ~= nil) and (self.level >= 1)
- return self.isReady
- end
- function Member:checkRequirements(reqs)
- if not reqs then
- return true
- end
- for _,req in ipairs(reqs) do
- if not req.check(self) then
- return false
- end
- end
- return true
- end
- function Member:hide()
- if not self.isHidden and not self.isPlayer then
- self.isHidden = true
- for _,watchInfo in pairs(self.watching) do
- watchInfo:hide()
- end
- end
- end
- function Member:unhide()
- if self.isHidden and not self.isPlayer then
- self.isHidden = false
- for _,watchInfo in pairs(self.watching) do
- watchInfo:unhide()
- end
- end
- end
- -- TODO: Fix rare issue where somehow only talented spells are being shown?
- function ZT:addOrUpdateMember(memberInfo)
- local member = self.members[memberInfo.GUID]
- if not member then
- member = Member:create(memberInfo)
- self.members[member.GUID] = member
- end
- if member.isIgnored then
- return
- end
- -- Determining which properties of the member have updated
- local isInitialUpdate = not member.isReady and member:gatherInfo()
- local isLevelUpdate = memberInfo.level and (memberInfo.level ~= member.level)
- local isSpecUpdate = memberInfo.specID and (memberInfo.specID ~= member.specID)
- local isTalentUpdate = isSpecUpdate
- for talent,_ in pairs(memberInfo.talents) do
- if member.talents[talent] == nil then
- isTalentUpdate = true
- break
- end
- end
- if member.isReady and (isInitialUpdate or isLevelUpdate or isSpecUpdate or isTalentUpdate) then
- local prevSpecID = member.specID
- local prevTalents = member.talents
- member:update(memberInfo)
- -- This handshake should come before any cooldown updates for newly watched spells
- if member.isPlayer then
- self:sendHandshake()
- end
- -- If we are in an encounter, hide the member if they are outside the player's instance
- -- (Note: Previously did this on member creation, which seemed to introduce false positives)
- if isInitialUpdate and self.inEncounter and (not member.isPlayer) then
- local _,_,_,instanceID = UnitPosition("player")
- local _,_,_,mInstanceID = UnitPosition(member.unit)
- if instanceID ~= mInstanceID then
- member:hide()
- end
- end
- -- Generic Spells + Class Spells + Race Spells
- -- Note: These are set once and never change
- if isInitialUpdate then
- for _,spellInfo in ipairs(self.spells["generic"]) do
- self:watch(spellInfo, member)
- end
- for _,spellInfo in ipairs(self.spells["race"][member.race]) do
- self:watch(spellInfo, member)
- end
- for _,spellInfo in ipairs(self.spells["class"][member.classID]) do
- self:watch(spellInfo, member)
- end
- end
- -- Specialization Spells
- if (isInitialUpdate or isSpecUpdate) and member.specID then
- for _,spellInfo in ipairs(self.spells["spec"][member.specID]) do
- self:watch(spellInfo, member)
- end
- if isSpecUpdate and prevSpecID then
- for _,spellInfo in ipairs(self.spells["spec"][prevSpecID]) do
- if not member:checkRequirements(spellInfo.reqs) then
- self:unwatch(spellInfo, member)
- end
- end
- end
- end
- -- Talented Spells
- if isInitialUpdate or isTalentUpdate then
- for talent,_ in pairs(member.talents) do
- for _,spellInfo in ipairs(self.spells["talent"][talent]) do
- self:watch(spellInfo, member)
- end
- for _,spellInfo in ipairs(self.spells["notalent"][talent]) do
- if not member:checkRequirements(spellInfo.reqs) then
- self:unwatch(spellInfo, member)
- end
- end
- end
- if isTalentUpdate then
- for talent,_ in pairs(prevTalents) do
- if not member.talents[talent] then
- for _,spellInfo in ipairs(self.spells["talent"][talent]) do
- if not member:checkRequirements(spellInfo.reqs) then
- self:unwatch(spellInfo, member) -- Talent was required
- else
- self:watch(spellInfo, member) -- Talent was a modifier
- end
- end
- for _,spellInfo in ipairs(self.spells["notalent"][talent]) do
- self:watch(spellInfo, member)
- end
- end
- end
- end
- end
- end
- -- If tracking changed from "CombatLog" to "Sharing", remove unnecessary event handlers and send a handshake/updates
- if (member.tracking == "CombatLog") and (memberInfo.tracking == "Sharing") then
- member.tracking = "Sharing"
- member.spellsVersion = memberInfo.spellsVersion
- if member.name then
- prdebug(DEBUG_TRACKING, member.name, "is using ZenTracker with spell list version", member.spellsVersion)
- end
- for _,watchInfo in pairs(member.watching) do
- if watchInfo.spellInfo.version <= member.spellsVersion then
- watchInfo.ignoreSharing = false
- self:toggleCombatLogHandlers(watchInfo, false)
- end
- end
- self:sendHandshake()
- local time = GetTime()
- for _,watchInfo in pairs(self.members[UnitGUID("player")].watching) do
- if watchInfo.expiration > time then
- self:sendCDUpdate(watchInfo)
- end
- end
- end
- end
- --##############################################################################
- -- Handling raid and M+ encounters
- function ZT:resetEncounterCDs()
- for _,member in pairs(self.members) do
- local resetMemberCDs = not member.isPlayer and member.tracking ~= "Sharing"
- for _,watchInfo in pairs(member.watching) do
- if resetMemberCDs and watchInfo.duration >= 180 then
- watchInfo.charges = watchInfo.maxCharges
- watchInfo:updateCDRemaining(0)
- end
- -- If spell uses lazy tracking and it was triggered, reset lazy tracking at this point
- if watchInfo.spellInfo.isLazy and not watchInfo.isLazy then
- watchInfo:sendRemoveEvent()
- watchInfo.isLazy = true
- end
- end
- end
- end
- function ZT:startEncounter(event)
- self.inEncounter = true
- local _,_,_,instanceID = UnitPosition("player")
- for _,member in pairs(self.members) do
- local _,_,_,mInstanceID = UnitPosition(self.inspectLib:GuidToUnit(member.GUID))
- if mInstanceID ~= instanceID then
- member:hide()
- else
- member:unhide() -- Note: Shouldn't be hidden, but just in case...
- end
- end
- if event == "CHALLENGE_MODE_START" then
- self:resetEncounterCDs()
- end
- end
- function ZT:endEncounter(event)
- if self.inEncounter then
- self.inEncounter = false
- for _,member in pairs(self.members) do
- member:unhide()
- end
- end
- if event == "ENCOUNTER_END" then
- self:resetEncounterCDs()
- end
- end
- --##############################################################################
- -- Public functions for other addons/auras to query ZenTracker information
- -- TODO: Under heavy construction
- local function Public_Query(typeOrID)
- local results = {}
- if type(typeOrID) == "string" then
- local type = tostring(typeOrID)
- for _,member in pairs(ZT.members) do
- for _,watchInfo in pairs(member.watching) do
- if watchInfo.spellInfo.type == type then
- tinsert(results, {spellID = watchInfo.spellInfo.id, member = member, expiration = watchInfo.expiration, charges = watchInfo.charges})
- end
- end
- end
- elseif type(typeOrID) == "number" then
- local id = tonumber(typeOrID)
- for _,member in pairs(ZT.members) do
- local watchInfo = member.watching[id]
- if watchInfo then
- tinsert(results, {spellID = watchInfo.spellInfo.id, member = member, expiration = watchInfo.expiration, charges = watchInfo.charges})
- end
- end
- end
- return results
- end
- setglobal("ZenTracker_PublicFunctions", { query = Public_Query })
- --##############################################################################
- -- Handling the exchange of addon messages with other ZT clients
- --
- -- Message Format = <Protocol Version (%d)>:<Message Type (%s)>:<Member GUID (%s)>...
- -- Type = "H" (Handshake)
- -- ...:<Spec ID (%d)>:<Talents (%s)>:<IsInitial? (%d) [2]>:<Spells Version (%d) [2]>
- -- Type = "U" (CD Update)
- -- ...:<Spell ID (%d)>:<Duration (%f)>:<Remaining (%f)>:<#Charges (%d) [3]>
- ZT.protocolVersion = 4
- ZT.timeBetweenHandshakes = 5 --seconds
- ZT.timeOfNextHandshake = 0
- ZT.handshakeTimer = nil
- ZT.timeBetweenCDUpdates = 5 --seconds (per spellID)
- ZT.timeOfNextCDUpdate = {}
- ZT.updateTimers = {}
- local function sendMessage(message)
- prdebug(DEBUG_MESSAGE, "Sending message '"..message.."'")
- if not IsInGroup() and not IsInRaid() then
- return
- end
- local channel = IsInGroup(2) and "INSTANCE_CHAT" or "RAID"
- C_ChatInfo.SendAddonMessage("ZenTracker", message, channel)
- end
- ZT.hasSentHandshake = false
- function ZT:sendHandshake()
- local time = GetTime()
- if time < self.timeOfNextHandshake then
- if not self.handshakeTimer then
- self.handshakeTimer = self.timers:add(self.timeOfNextHandshake, function() self:sendHandshake() end)
- end
- return
- end
- local GUID = UnitGUID("player")
- if not self.members[GUID] then
- return -- This may happen when rejoining a group after login, so ignore this attempt to send a handshake
- end
- local member = self.members[GUID]
- local specID = member.specID or 0
- local talents = member.talentsStr or ""
- local isInitial = self.hasSentHandshake and 0 or 1
- local message = string.format("%d:H:%s:%d:%s:%d:%d", self.protocolVersion, GUID, specID, talents, isInitial, self.spellListVersion)
- sendMessage(message)
- self.hasSentHandshake = true
- self.timeOfNextHandshake = time + self.timeBetweenHandshakes
- if self.handshakeTimer then
- self.timers:cancel(self.handshakeTimer)
- self.handshakeTimer = nil
- end
- end
- function ZT:sendCDUpdate(watchInfo, ignoreRateLimit)
- local spellID = watchInfo.spellInfo.id
- local time = GetTime()
- local timer = self.updateTimers[spellID]
- if ignoreRateLimit then
- if timer then
- self.timers:cancel(timer)
- self.updateTimers[spellID] = nil
- end
- elseif timer then
- return
- else
- local timeOfNextCDUpdate = self.timeOfNextCDUpdate[spellID]
- if timeOfNextCDUpdate and (time < timeOfNextCDUpdate) then
- self.updateTimers[spellID] = self.timers:add(timeOfNextCDUpdate, function() self:sendCDUpdate(watchInfo, true) end)
- return
- end
- end
- local GUID = watchInfo.member.GUID
- local duration = watchInfo.duration
- local remaining = watchInfo.expiration - time
- if remaining < 0 then
- remaining = 0
- end
- local charges = watchInfo.charges and tostring(watchInfo.charges) or "-"
- local message = string.format("%d:U:%s:%d:%0.2f:%0.2f:%s", self.protocolVersion, GUID, spellID, duration, remaining, charges)
- sendMessage(message)
- self.timeOfNextCDUpdate[spellID] = time + self.timeBetweenCDUpdates
- end
- function ZT:handleHandshake(version, mGUID, specID, talentsStr, isInitial, spellsVersion)
- -- Protocol V4: Ignore any earlier versions due to substantial changes (talents)
- if version < 4 then
- return
- end
- specID = tonumber(specID)
- if specID == 0 then
- specID = nil
- end
- local talents = {}
- if talents ~= "" then
- for index in talentsStr:gmatch("%d+") do
- index = tonumber(index)
- talents[index] = true
- end
- end
- -- Protocol V2: Assume false if not present
- if isInitial == "1" then
- isInitial = true
- else
- isInitial = false
- end
- -- Protocol V2: Assume spellsVersion is 1 if not present
- if spellsVersion then
- spellsVersion = tonumber(spellsVersion)
- if not spellsVersion then
- spellsVersion = 1
- end
- else
- spellsVersion = 1
- end
- local memberInfo = {
- GUID = mGUID,
- specID = specID,
- talents = talents,
- talentsStr = talentsStr,
- tracking = "Sharing",
- spellsVersion = spellsVersion,
- }
- self:addOrUpdateMember(memberInfo)
- if isInitial then
- self:sendHandshake()
- end
- end
- function ZT:handleCDUpdate(version, mGUID, spellID, duration, remaining, charges)
- local member = self.members[mGUID]
- if not member or not member.isReady then
- return
- end
- spellID = tonumber(spellID)
- duration = tonumber(duration)
- remaining = tonumber(remaining)
- if not spellID or not duration or not remaining then
- return
- end
- local sources = self.watching[spellID]
- if sources then
- local watchInfo = sources[member.GUID]
- if not watchInfo or watchInfo.ignoreSharing then
- return
- end
- -- Protocol V3: Ignore charges if not present
- -- (Note that this shouldn't happen because of spell list version handling)
- if charges then
- charges = tonumber(charges)
- if charges then
- watchInfo.charges = charges
- end
- end
- watchInfo.duration = duration
- watchInfo.expiration = GetTime() + remaining
- watchInfo:sendTriggerEvent()
- end
- end
- function ZT:handleMessage(message)
- local version, type, mGUID, arg1, arg2, arg3, arg4, arg5 = strsplit(":", message)
- version = tonumber(version)
- -- Ignore any messages sent by the player
- if mGUID == UnitGUID("player") then
- return
- end
- prdebug(DEBUG_MESSAGE, "Received message '"..message.."'")
- if type == "H" then -- Handshake
- self:handleHandshake(version, mGUID, arg1, arg2, arg3, arg4, arg5)
- elseif type == "U" then -- CD Update
- self:handleCDUpdate(version, mGUID, arg1, arg2, arg3, arg4, arg5)
- else
- return
- end
- end
- if not C_ChatInfo.RegisterAddonMessagePrefix("ZenTracker") then
- prerror("Could not register addon message prefix. Defaulting to local-only cooldown tracking.")
- end
- --##############################################################################
- -- Callback functions for libGroupInspecT for updating/removing members
- ZT.delayedUpdates = {}
- function ZT:libInspectUpdate(_, GUID, _, info)
- local specID = info.global_spec_id
- if specID == 0 then
- specID = nil
- end
- local talents = {}
- local talentsStr = ""
- if info.talents then
- for _,talent in pairs(info.talents) do
- if talent.spell_id then -- This is rarely nil, not sure why...
- talents[talent.spell_id] = true
- talentsStr = talentsStr..talent.spell_id..","
- end
- end
- end
- local memberInfo = {
- GUID = GUID,
- unit = info.lku,
- specID = specID,
- talents = talents,
- talentsStr = strsub(talentsStr, 0, -2),
- }
- if not self.delayedUpdates then
- self:addOrUpdateMember(memberInfo)
- else
- self.delayedUpdates[GUID] = memberInfo
- end
- end
- function ZT:libInspectRemove(_, GUID)
- local member = self.members[GUID]
- if not member then
- return
- end
- for _,watchInfo in pairs(member.watching) do
- self:unwatch(watchInfo.spellInfo, member)
- end
- self.members[GUID] = nil
- end
- function ZT:handleDelayedUpdates()
- if self.delayedUpdates then
- for _,memberInfo in pairs(self.delayedUpdates) do
- self:addOrUpdateMember(memberInfo)
- end
- self.delayedUpdates = nil
- end
- end
- ZT.inspectLib = LibStub:GetLibrary("LibGroupInSpecT-1.1", true)
- if ZT.inspectLib then
- local prevZT = _G["ZenTracker_AuraEnv"]
- if prevZT then
- ZT.inspectLib.UnregisterAllCallbacks(prevZT)
- if prevZT.timers then
- prevZT.timers.heap = {}
- end
- end
- _G["ZenTracker_AuraEnv"] = ZT
- -- If prevZT exists, we know it wasn't a login or reload. If it doesn't exist,
- -- it still might not be a login or reload if the user is installing ZenTracker
- -- for the first time. IsLoginFinished() takes care of the second case.
- if prevZT or WeakAuras.IsLoginFinished() then
- ZT.delayedUpdates = nil
- end
- ZT.inspectLib.RegisterCallback(ZT, "GroupInSpecT_Update", "libInspectUpdate")
- ZT.inspectLib.RegisterCallback(ZT, "GroupInSpecT_Remove", "libInspectRemove")
- for unit in WA_IterateGroupMembers() do
- local GUID = UnitGUID(unit)
- if GUID then
- local info = ZT.inspectLib:GetCachedInfo(GUID)
- if info then
- ZT:libInspectUpdate("Init", GUID, unit, info)
- else
- ZT.inspectLib:Rescan(GUID)
- end
- end
- end
- else
- prerror("LibGroupInSpecT-1.1 not found")
- end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement