· 6 years ago · Sep 14, 2019, 12:22 PM
1local Libra = LibStub("Libra")
2
3local Critline, addonName = Libra:NewAddon(...)
4_G.Critline = Critline
5Libra:EmbedWidgets(Critline)
6
7Critline.L = LibStub("AceLocale-3.0"):GetLocale(addonName)
8local L = Critline.L
9local LSM = LibStub("LibSharedMedia-3.0")
10
11local _, playerClass = UnitClass("player")
12local spellMappings, tooltipMappings, spellNameOverrides, spellIconOverrides
13local debugging
14
15local OutputMessageDmg = "BAM!"
16-- adjust chatchannel here (possible: YELL, SAY, PARTY, GUILD, RAID, RAID_WARNING, EMOTE)
17local OutputChannel = "PARTY"
18
19-- debugging = true
20
21-- auto attack spell
22local AUTO_ATTACK_ID = 6603
23local AUTO_ATTACK = GetSpellInfo(AUTO_ATTACK_ID)
24
25-- local references to commonly used functions and variables for faster access
26local floor, band, tonumber, format = floor, bit.band, tonumber, format
27local CombatLogGetCurrentEventInfo = CombatLogGetCurrentEventInfo
28local CombatLog_Object_IsA = CombatLog_Object_IsA
29local HasPetUI = HasPetUI
30local GetSpellInfo = GetSpellInfo
31local GetSpellLink = GetSpellLink
32local IsPlayerSpell = IsPlayerSpell
33
34local COMBATLOG_FILTER_MINE = COMBATLOG_FILTER_MINE
35local COMBATLOG_FILTER_MY_PET = COMBATLOG_FILTER_MY_PET
36local COMBATLOG_OBJECT_REACTION_FRIENDLY = COMBATLOG_OBJECT_REACTION_FRIENDLY
37local COMBATLOG_OBJECT_REACTION_HOSTILE = COMBATLOG_OBJECT_REACTION_HOSTILE
38local COMBATLOG_OBJECT_CONTROL_PLAYER = COMBATLOG_OBJECT_CONTROL_PLAYER
39local COMBATLOG_OBJECT_TYPE_GUARDIAN = COMBATLOG_OBJECT_TYPE_GUARDIAN
40
41local trees = {
42 dmg = {
43 title = L["Damage"],
44 label = L["damage"],
45 icon = [[Interface\Icons\Ability_SteelMelee]],
46 },
47 heal = {
48 title = L["Healing"],
49 label = L["healing"],
50 icon = [[Interface\Icons\Spell_Holy_FlashHeal]],
51 },
52 pet = {
53 title = L["Pet"],
54 label = L["pet"],
55 icon = [[Interface\Icons\Ability_Hunter_Pet_Bear]],
56 },
57}
58Critline.trees = trees
59Critline.treeIndex = {
60 "dmg",
61 "heal",
62 "pet",
63}
64
65local RAID_BOSS_LEVELS = {
66 [469] = 63, -- Blackwing Lair
67 [409] = 63, -- Molten Core
68 [509] = 63, -- Ruins of Ahn'Qiraj
69 [531] = 63, -- Temple of Ahn'Qiraj
70
71 [564] = 73, -- Black Temple
72 [565] = 73, -- Gruul's Lair
73 [534] = 73, -- Hyjal Summit
74 [532] = 73, -- Karazhan
75 [544] = 73, -- Magtheridon's Lair
76 [548] = 73, -- Serpentshrine Cavern
77 [580] = 73, -- Sunwell Plateau
78 [550] = 73, -- The Eye
79
80 [631] = 83, -- Icecrown Citadel
81 [533] = 83, -- Naxxramas
82 [249] = 83, -- Onyxia's Lair
83 [616] = 83, -- The Eye of Eternity
84 [615] = 83, -- The Obsidian Sanctum
85 [724] = 83, -- The Ruby Sanctum
86 [649] = 83, -- Trial of the Crusader
87 [603] = 83, -- Ulduar
88 [624] = 83, -- Vault of Archavon
89
90 [757] = 88, -- Baradin Hold
91 [669] = 88, -- Blackwing Descent
92 [967] = 88, -- Dragon Soul
93 [720] = 88, -- Firelands
94 [671] = 88, -- The Bastion of Twilight
95 [754] = 88, -- Throne of the Four Winds
96
97 [1009] = 93, -- Heart of Fear
98 [1008] = 93, -- Mogu'shan Vaults
99 [1136] = 93, -- Siege of Orgrimmar
100 [996] = 93, -- Terrace of Endless Spring
101 [1098] = 93, -- Throne of Thunder
102
103 [1228] = 103, -- Highmaul
104 [1205] = 103, -- Blackrock Foundry
105 [1448] = 103, -- Hellfire Citadel
106
107 [1520] = 113, -- The Emerald Nightmare
108 [1648] = 113, -- Trial of Valor
109 [1530] = 113, -- The Nighthold
110 [1676] = 113, -- Tomb of Sargeras
111 [1712] = 113, -- Antorus, the Burning Throne
112}
113
114local bossLevel
115
116-- guardian type pets whose damage we may want to register
117local classPets = {
118 [89] = true, -- Infernal
119 [11859] = true, -- Doomguard
120 [15438] = true, -- Greater Fire Elemental
121 [27829] = true, -- Ebon Gargoyle
122 [29264] = true, -- Spirit Wolf
123}
124
125local spellIDCache = {}
126
127-- cache of spell ID -> spell name
128local spellNameCache = {}
129
130-- cache of spell textures
131local spellTextureCache = {
132 -- use a static icon for auto attack (otherwise uses your weapon's icon)
133 [AUTO_ATTACK_ID] = [[Interface\Icons\INV_Sword_04]],
134 [5019] = [[Interface\Icons\Ability_ShootWand]], -- Shoot (wand)
135}
136
137local swingDamage = function(amount, _, school, resisted, _, _, critical)
138 return AUTO_ATTACK_ID, AUTO_ATTACK, amount, resisted, critical
139end
140
141local spellDamage = function(spellID, spellName, _, amount, _, school, resisted, _, _, critical)
142 return spellID, spellName, amount, resisted, critical
143end
144
145local healing = function(spellID, spellName, _, amount, _, _, critical)
146 return spellID, spellName, amount, 0, critical
147end
148
149local absorb = function(spellID, spellName, _, _, amount)
150 return spellID, spellName, amount, 0, critical
151end
152
153local combatEvents = {
154 SWING_DAMAGE = swingDamage,
155 RANGE_DAMAGE = spellDamage,
156 SPELL_DAMAGE = spellDamage,
157 SPELL_PERIODIC_DAMAGE = spellDamage,
158 SPELL_HEAL = healing,
159 SPELL_PERIODIC_HEAL = healing,
160 SPELL_AURA_APPLIED = absorb,
161 SPELL_AURA_REFRESH = absorb,
162}
163
164
165-- alpha: sort by name
166local alpha = function(a, b)
167 if a == b then return end
168 if a.name == b.name then
169 if a.id == b.id then
170 -- sort DoT entries after non DoT
171 return a.periodic < b.periodic
172 else
173 return a.id < b.id
174 end
175 else
176 return a.name < b.name
177 end
178end
179
180-- normal: sort by normal > crit > name
181local normal = function(a, b)
182 if a == b then return end
183 local normalA, normalB = (a.normal and a.normal.amount or 0), (b.normal and b.normal.amount or 0)
184 if normalA == normalB then
185 -- equal normal amounts, sort by crit amount instead
186 local critA, critB = (a.crit and a.crit.amount or 0), (b.crit and b.crit.amount or 0)
187 if critA == critB then
188 -- equal crit amounts too, sort by name instead
189 return alpha(a, b)
190 else
191 return critA > critB
192 end
193 else
194 return normalA > normalB
195 end
196end
197
198-- crit: sort by crit > normal > name
199local crit = function(a, b)
200 if a == b then return end
201 local critA, critB = (a.crit and a.crit.amount or 0), (b.crit and b.crit.amount or 0)
202 if critA == critB then
203 return normal(a, b)
204 else
205 return critA > critB
206 end
207end
208
209local recordSorters = {
210 alpha = alpha,
211 normal = normal,
212 crit = crit,
213}
214
215
216local callbacks = LibStub("CallbackHandler-1.0"):New(Critline)
217Critline.callbacks = callbacks
218
219
220-- this will hold the text for the summary tooltip
221local tooltips = {dmg = {}, heal = {}, pet = {}}
222
223-- indicates whether a given tree will need to have its tooltip updated before next use
224local doTooltipUpdate = {}
225
226-- overall record for each tree
227local topRecords = {
228 dmg = {normal = 0, crit = 0},
229 heal = {normal = 0, crit = 0},
230 pet = {normal = 0, crit = 0},
231}
232
233-- sortable spell tables
234local spellArrays = {dmg = {}, heal = {}, pet = {}}
235
236
237LSM:Register("sound", "Level up", 567431) -- Sound\Interface\LevelUp.ogg
238
239
240Critline.SlashCmdHandlers = {
241 debug = function() Critline:ToggleDebug() end,
242}
243
244SlashCmdList.CRITLINE = function(msg)
245 msg = msg:trim():lower()
246 local slashCmdHandler = Critline.SlashCmdHandlers[msg]
247 if slashCmdHandler then
248 slashCmdHandler()
249 else
250 Critline:OpenConfig()
251 end
252end
253
254SLASH_CRITLINE1 = "/critline"
255SLASH_CRITLINE2 = "/cl"
256
257-- tooltip for level scanning
258local tooltip = CreateFrame("GameTooltip", "CritlineTooltip", nil, "GameTooltipTemplate")
259
260
261local config = Critline:CreateOptionsFrame(addonName)
262Critline.config = config
263
264do
265 local function set(self, value)
266 Critline.percharDB.profile[self.key] = value
267 end
268
269 local function get(self)
270 return Critline.percharDB.profile[self.key]
271 end
272
273 local function toggleTree(self, checked)
274 callbacks:Fire("OnTreeStateChanged", self.key, checked)
275 end
276
277 -- summary sort dropdown
278 local menu = {
279 {
280 text = L["Spell name"],
281 value = "alpha",
282 },
283 {
284 text = L["Normal record"],
285 value = "normal",
286 },
287 {
288 text = L["Crit record"],
289 value = "crit",
290 },
291 }
292
293 local options = {
294 {
295 type = "CheckButton",
296 text = L["Record damage"],
297 tooltip = L["Check to enable damage events to be recorded."],
298 key = "dmg",
299 set = set,
300 get = get,
301 func = toggleTree,
302 },
303 {
304 type = "CheckButton",
305 text = L["Record healing"],
306 tooltip = L["Check to enable healing events to be recorded."],
307 key = "heal",
308 func = toggleTree,
309 set = set,
310 get = get,
311 },
312 {
313 type = "CheckButton",
314 text = L["Record pet damage"],
315 tooltip = L["Check to enable pet damage events to be recorded."],
316 key = "pet",
317 func = toggleTree,
318 set = set,
319 get = get,
320 },
321 {
322 type = "CheckButton",
323 text = L["Record PvE"],
324 tooltip = L["Disable to ignore records where the target is an NPC."],
325 key = "PvE",
326 padding = 8,
327 },
328 {
329 type = "CheckButton",
330 text = L["Record PvP"],
331 tooltip = L["Disable to ignore records where the target is a player."],
332 key = "PvP",
333 },
334 {
335 type = "CheckButton",
336 text = L["Ignore vulnerability"],
337 tooltip = L["Enable to ignore additional damage due to vulnerability."],
338 key = "ignoreVulnerability",
339 },
340 {
341 newColumn = true,
342 type = "CheckButton",
343 text = L["Shorten records"],
344 tooltip = L["Use shorter format for record numbers."],
345 key = "shortFormat",
346 func = function(self, checked)
347 callbacks:Fire("FormatChanged")
348 Critline:UpdateTooltips()
349 end,
350 },
351 {
352 type = "CheckButton",
353 text = L["Records in spell tooltips"],
354 tooltip = L["Include (unfiltered) records in spell tooltips."],
355 key = "spellTooltips",
356 },
357 {
358 type = "CheckButton",
359 text = L["Detailed tooltip"],
360 tooltip = L["Use detailed format in the summary tooltip."],
361 key = "detailedTooltip",
362 func = "UpdateTooltips",
363 },
364 {
365 type = "Dropdown",
366 text = L["Sort tooltips by:"],
367 key = "tooltipSort",
368 width = 160,
369 func = "UpdateTooltips",
370 menuList = {
371 "alpha",
372 "normal",
373 "crit",
374 },
375 properties = {
376 text = {
377 alpha = L["Spell name"],
378 normal = L["Normal record"],
379 crit = L["Crit record"],
380 },
381 },
382 },
383 {
384 type = "CheckButton",
385 text = L["Include old record"],
386 tooltip = L["Includes previous record along with \"New record\" messages."],
387 key = "oldRecord",
388 },
389 {
390 type = "CheckButton",
391 text = L["Chat output"],
392 tooltip = L["Prints new record notifications to the chat frame."],
393 key = "chatOutput",
394 },
395 {
396 type = "CheckButton",
397 text = L["Screenshot"],
398 tooltip = L["Saves a screenshot on a new record."],
399 key = "screenshot",
400 },
401 {
402 type = "Dropdown",
403 text = L["Sound effect"],
404 key = "sound",
405 func = function(self, value)
406 -- hack not to play the sound when settings are loaded from a triggered event
407 if not GetMouseButtonClicked() then return end
408 PlaySoundFile(LSM:Fetch("sound", value))
409 end,
410 width = 160,
411 menuList = function() return LSM:List("sound") end,
412 },
413 }
414
415 config:CreateOptions(options)
416end
417
418
419local defaults = {
420 profile = {
421 PvE = true,
422 PvP = true,
423 ignoreVulnerability = true,
424 shortFormat = false,
425 spellTooltips = true,
426 detailedTooltip = false,
427 tooltipSort = "normal",
428 oldRecord = false,
429 chatOutput = false,
430 screenshot = false,
431 sound = "None",
432 },
433 global = {
434 spellMappings = {},
435 tooltipMappings = {},
436 spellNameOverrides = {
437 -- pre-add form name to hybrid druid abilities, so the user can tell which is cat and which is bear
438 -- [33878] = format("%s (%s)", GetSpellInfo(33878), GetSpellInfo(5487)), -- Mangle (Bear Form)
439 -- [33876] = format("%s (%s)", GetSpellInfo(33876), GetSpellInfo(768)), -- Mangle (Cat Form)
440 -- [779] = format("%s (%s)", GetSpellInfo(779), GetSpellInfo(5487)), -- Swipe (Bear Form)
441 -- [62078] = format("%s (%s)", GetSpellInfo(62078), GetSpellInfo(768)), -- Swipe (Cat Form)
442 },
443 spellIconOverrides = {},
444 },
445}
446
447-- which trees are enabled by default for a given class
448-- if not specified; defaults to only damage enabled
449local treeDefaults = {
450 DRUID = {heal = true},
451 HUNTER = {pet = true},
452 MONK = {heal = true},
453 PALADIN = {heal = true},
454 PRIEST = {heal = true},
455 SHAMAN = {heal = true},
456 WARLOCK = {pet = true},
457}
458
459function Critline:OnInitialize()
460 local AceDB = LibStub("AceDB-3.0")
461 local db = AceDB:New("CritlineDB", defaults, nil)
462 self.db = db
463
464 config:SetDatabase(self.db, true)
465 config:SetHandler(self)
466
467 local percharDefaults = {
468 profile = treeDefaults[playerClass] or {},
469 }
470 -- everyone wants damage!
471 percharDefaults.profile.dmg = true
472 -- set these to false rather than nil if disabled, for consistency
473 percharDefaults.profile.heal = percharDefaults.profile.heal or false
474 percharDefaults.profile.pet = percharDefaults.profile.pet or false
475 percharDefaults.profile.spells = {
476 dmg = {},
477 heal = {},
478 pet = {},
479 }
480
481 local percharDB = AceDB:New("CritlinePerCharDB", percharDefaults)
482 self.percharDB = percharDB
483
484 -- dual spec support
485 local LibDualSpec = LibStub("LibDualSpec-1.0", true)
486 if LibDualSpec then
487 LibDualSpec:EnhanceDatabase(self.db, addonName)
488 LibDualSpec:EnhanceDatabase(self.percharDB, addonName)
489 end
490
491 db.RegisterCallback(self, "OnProfileChanged", "LoadSettings")
492 db.RegisterCallback(self, "OnProfileCopied", "LoadSettings")
493 db.RegisterCallback(self, "OnProfileReset", "LoadSettings")
494
495 percharDB.RegisterCallback(self, "OnProfileChanged", "LoadPerCharSettings")
496 percharDB.RegisterCallback(self, "OnProfileCopied", "LoadPerCharSettings")
497 percharDB.RegisterCallback(self, "OnProfileReset", "LoadPerCharSettings")
498
499 self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
500 self:RegisterEvent("PLAYER_ENTERING_WORLD")
501
502 spellMappings = db.global.spellMappings
503 tooltipMappings = db.global.tooltipMappings
504 spellNameOverrides = db.global.spellNameOverrides
505 spellIconOverrides = db.global.spellIconOverrides
506
507 -- purge invalid spell mappings
508 for k, v in pairs(spellMappings) do
509 if not GetSpellLink(v) then
510 spellMappings[k] = nil
511 end
512 end
513
514 self:LoadSettings()
515 self:LoadPerCharSettings()
516end
517
518function Critline:LoadSettings()
519 callbacks:Fire("SettingsLoaded")
520 config:SetupControls()
521end
522
523function Critline:LoadPerCharSettings()
524 self:FixSpells()
525 self:BuildSpellArray()
526
527 callbacks:Fire("PerCharSettingsLoaded")
528 self:UpdateTopRecords()
529 self:UpdateTooltips()
530
531 config:SetupControls()
532end
533
534function Critline:FixSpells()
535 for k, tree in pairs(self.percharDB.profile.spells) do
536 for spellID, spell in pairs(tree) do
537 -- merge any spell remnants that has gotten new mappings, into their new spell ID
538 local spellMapping = spellMappings[spellID]
539 if spellMapping and spellMapping ~= spellID then
540 local map = tree[spellMapping] or spell
541 for i = 1, 2 do
542 map[i] = map[i] or spell[i]
543 end
544 tree[spellID] = nil
545 end
546
547 -- remove spells that have been taken out of the game
548 if not GetSpellLink(spellID) then
549 tree[spellID] = nil
550 end
551 end
552 end
553end
554
555local healEvents = {
556 SPELL_HEAL = true,
557 SPELL_PERIODIC_HEAL = true,
558 SPELL_AURA_APPLIED = true,
559 SPELL_AURA_REFRESH = true,
560}
561
562function Critline:COMBAT_LOG_EVENT_UNFILTERED()
563 local timestamp, eventType, hideCaster, sourceGUID, sourceName, sourceFlags, sourceFlags2, destGUID, destName, destFlags, destFlags2,
564 arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20, arg21 = CombatLogGetCurrentEventInfo()
565 local isPet
566
567 -- if sourceGUID is not us or our pet, we leave
568 if not CombatLog_Object_IsA(sourceFlags, COMBATLOG_FILTER_MINE) then
569 -- only register if it's a real pet, or a guardian tree pet that's included in the filter
570 if self:IsMyPet(sourceFlags, sourceGUID) then
571 isPet = true
572 -- self:Debug(format("This is my pet (%s)", sourceName))
573 else
574 -- self:Debug("This is not me, my trap or my pet; return.")
575 return
576 end
577 else
578 -- self:Debug(format("This is me or my trap (%s)", sourceName))
579 end
580
581 local isPeriodic
582 local periodic = 1
583 local isHeal = healEvents[eventType]
584 -- we don't care about healing done by the pet
585 if isHeal and isPet then
586 self:Debug("Pet healing. Return.")
587 return
588 end
589 if eventType == "SPELL_PERIODIC_DAMAGE" or eventType == "SPELL_PERIODIC_HEAL" then
590 isPeriodic = true
591 periodic = 2
592 end
593
594 local combatEvent = combatEvents[eventType]
595 if not combatEvent then
596 return
597 end
598
599 -- get the relevants arguments
600 local spellID, spellName, amount, resisted, critical = combatEvent(arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20, arg21)
601
602 if WOW_PROJECT_ID == WOW_PROJECT_CLASSIC then
603 spellID = select(7, GetSpellInfo(spellName))
604 if not spellID then
605 return
606 end
607 end
608
609 local rawID = spellID
610 local cachedID = spellIDCache[spellID]
611 if cachedID then
612 spellID = cachedID
613 elseif not IsPlayerSpell(spellID) then
614 local spellLink = GetSpellLink(spellName)
615 if spellLink then
616 local id = tonumber(spellLink:match("spell:(%d+)"))
617 if id and IsPlayerSpell(id) then
618 spellIDCache[spellID] = id
619 spellID = id
620 end
621 end
622 else
623 -- cache either way so we don't have to check with IsPlayerSpell when we know that it is
624 spellIDCache[spellID] = spellID
625 end
626
627 -- if we don't have a destName (who we hit or healed) and we don't have a sourceName (us or our pets) then we leave
628 if not destName then
629 self:Debug(format("No target info for %s (%d).", spellName, spellID))
630 return
631 end
632
633 -- return if the event has no amount (non-absorbing aura applied)
634 if not amount then
635 return
636 end
637
638 local spellMapping = spellMappings[spellID]
639 if spellMapping then
640 spellID = spellMapping
641 end
642
643 -- some absorb effects seem to have a floating point amount
644 amount = floor(amount)
645
646 if amount <= 0 then
647 self:Debug(format("Amount <= 0. (%s) Return.", self:GetFullSpellName(spellName, periodic)))
648 return
649 end
650
651 local tree = "dmg"
652
653 if isPet then
654 tree = "pet"
655 elseif isHeal then
656 tree = "heal"
657 end
658
659 -- exit if not recording tree dmg
660 if not self.percharDB.profile[tree] then
661 self:Debug(format("Not recording %s spells. Return.", tree))
662 return
663 end
664
665 local targetLevel = self:GetLevelFromGUID(destGUID)
666 local passed, isFiltered
667 if self.filters then
668 passed, isFiltered = self.filters:SpellPassesFilters(tree, spellName, spellID, isPeriodic, destGUID, destName, targetLevel, rawID)
669 if not passed then
670 return
671 end
672 end
673
674 local isPvPTarget = band(destFlags, COMBATLOG_OBJECT_CONTROL_PLAYER) ~= 0
675 local friendlyFire = band(destFlags, COMBATLOG_OBJECT_REACTION_FRIENDLY) ~= 0
676 local hostileTarget = band(destFlags, COMBATLOG_OBJECT_REACTION_HOSTILE) ~= 0
677
678 if not (isPvPTarget or self.db.profile.PvE or isHeal) then
679 self:Debug(format("%s is an NPC and PvE damage is not registered.", destName))
680 return
681 end
682
683 if isPvPTarget and not (self.db.profile.PvP or isHeal or friendlyFire) then
684 self:Debug(format("%s is a player and PvP damage is not registered.", destName))
685 return
686 end
687
688 -- ignore damage done to friendly targets
689 if friendlyFire and not isHeal then
690 self:Debug(format("Skipped %s @ %s. Friendly fire.", GetSpellLink(rawID), destName))
691 return
692 end
693
694 -- ignore healing done to hostile targets
695 if hostileTarget and isHeal then
696 self:Debug(format("Skipped %s @ %s. Healing hostile target", GetSpellLink(rawID), destName))
697 return
698 end
699
700 -- ignore vulnerability damage if necessary
701 if self.db.profile.ignoreVulnerability and resisted and resisted < 0 then
702 amount = amount + resisted
703 self:Debug(format("%d vulnerability damage ignored for a real value of %d.", abs(resisted), amount))
704 end
705
706 local hitType = critical and "crit" or "normal"
707 local data = self:GetSpellInfo(tree, spellID, periodic)
708 local arrayData
709
710 spellName = self:GetSpellName(spellID)
711
712 -- create spell database entries as required
713 if not data then
714 self:Debug(format("Creating data for %s (%s)", self:GetFullSpellName(spellName, periodic), tree))
715 data, arrayData = self:AddSpell(tree, spellID, periodic, spellName, isFiltered)
716 self:UpdateSpells(tree)
717 end
718
719 if not data[hitType] then
720 data[hitType] = {amount = 0}
721 (arrayData or self:GetSpellArrayEntry(tree, spellID, periodic))[hitType] = data[hitType]
722 end
723
724 data = data[hitType]
725
726 -- if new amount is larger than the stored amount we'll want to store it
727 if amount > data.amount then
728 self:NewRecord(tree, spellID, spellName, periodic, amount, critical, data, isFiltered)
729
730 if not isFiltered then
731 -- update the highest record if needed
732 local topRecords = topRecords[tree]
733 if amount > topRecords[hitType] then
734 topRecords[hitType] = amount
735 callbacks:Fire("OnNewTopRecord", tree)
736 end
737 end
738
739 data.amount = amount
740 data.target = destName
741 data.targetLevel = targetLevel
742 data.isPvPTarget = isPvPTarget
743
744 self:UpdateRecords(tree, isFiltered)
745 end
746end
747
748function Critline:PLAYER_ENTERING_WORLD()
749 local name, instanceType, difficultyID, difficultyName, maxPlayers, dynamicDifficulty, isDynamic, instanceMapID = GetInstanceInfo()
750 bossLevel = RAID_BOSS_LEVELS[instanceMapID] or -1
751end
752
753function Critline:IsMyPet(flags, guid)
754 local _, _, _, _, _, npcID = strsplit("-", guid)
755 local isMyPet = CombatLog_Object_IsA(flags, COMBATLOG_FILTER_MY_PET)
756 local isGuardian = band(flags, COMBATLOG_OBJECT_TYPE_GUARDIAN) ~= 0
757 return isMyPet and ((not isGuardian and HasPetUI()) or classPets[npcID])
758end
759
760local levelCache = {}
761
762local levelStrings = {
763 TOOLTIP_UNIT_LEVEL:format("(%d+)"),
764 TOOLTIP_UNIT_LEVEL_CLASS:format("(%d+)", ".+"),
765 TOOLTIP_UNIT_LEVEL_CLASS_TYPE:format("(%d+)", ".+", ".+"),
766 TOOLTIP_UNIT_LEVEL_RACE_CLASS:format("(%d+)", ".+", ".+"),
767 TOOLTIP_UNIT_LEVEL_RACE_CLASS_TYPE:format("(%d+)", ".+", ".+", ".+"),
768 TOOLTIP_UNIT_LEVEL_TYPE:format("(%d+)", ".+"),
769}
770
771function Critline:GetLevelFromGUID(destGUID)
772 if levelCache[destGUID] then
773 return levelCache[destGUID]
774 end
775
776 tooltip:SetOwner(UIParent, "ANCHOR_NONE")
777 tooltip:SetHyperlink("unit:"..destGUID)
778
779 local level = bossLevel
780
781 for i = 1, tooltip:NumLines() do
782 local text = _G["CritlineTooltipTextLeft"..i]:GetText()
783 for i, v in ipairs(levelStrings) do
784 local level = text and text:match(v)
785 if level then
786 level = tonumber(level) or bossLevel
787 levelCache[destGUID] = level
788 return level
789 end
790 end
791 end
792 return level
793end
794
795function Critline:Message(...)
796 print("|cffffff00Critline:|r", ...)
797end
798
799function Critline:Debug(...)
800 if debugging then
801 print("|cff56a3ffCritlineDebug:|r", ...)
802 end
803end
804
805function Critline:ToggleDebug()
806 debugging = not debugging
807 self:Message("Debugging "..(debugging and "enabled" or "disabled"))
808end
809
810function Critline:OpenConfig()
811 InterfaceOptionsFrame_OpenToCategory(config)
812end
813
814function Critline:NewRecord(tree, spellID, spellName, periodic, amount, critical, prevRecord, isFiltered)
815 callbacks:Fire("NewRecord", tree, spellID, spellName, periodic, amount, critical, prevRecord, isFiltered)
816
817 if isFiltered then
818 return
819 end
820
821 amount = self:ShortenNumber(amount)
822
823 if self.db.profile.oldRecord and prevRecord.amount > 0 then
824 amount = format("%s (%s)", amount, self:ShortenNumber(prevRecord.amount))
825 end
826
827 if self.db.profile.chatOutput then
828 self:Message(format(L["New %s%s record - %s"], critical and "|cffff0000"..L["critical "].."|r" or "", self:GetFullSpellName(spellName, periodic, true), amount))
829 end
830
831 if self.db.profile.screenshot then
832 Screenshot()
833 end
834
835 SendChatMessage(OutputMessageDmg.." "..spellName.." - "..format_thousand(amount), OutputChannel, nil);
836 PlaySoundFile("Interface\\Addons\\Critline\\bam.ogg", "Dialog")
837end
838
839
840local FIRST_NUMBER_CAP = FIRST_NUMBER_CAP:lower()
841
842function Critline:ShortenNumber(amount)
843 if tonumber(amount) and self.db.profile.shortFormat then
844 if amount >= 1e7 then
845 amount = (floor(amount / 1e5) / 10)..SECOND_NUMBER_CAP
846 elseif amount >= 1e6 then
847 amount = (floor(amount / 1e4) / 100)..SECOND_NUMBER_CAP
848 elseif amount >= 1e4 then
849 amount = (floor(amount / 100) / 10)..FIRST_NUMBER_CAP
850 end
851 end
852 return amount
853end
854
855function Critline:BuildSpellArray(tree)
856 if not tree then
857 for tree in pairs(trees) do
858 self:BuildSpellArray(tree)
859 end
860 return
861 end
862
863 local array = spellArrays[tree]
864 wipe(array)
865 for spellID, spell in pairs(self.percharDB.profile.spells[tree]) do
866 for i, v in pairs(spell) do
867 array[#array + 1] = {
868 id = spellID,
869 name = self:GetSpellName(spellID) or tostring(spellID),
870 filtered = v.filtered,
871 periodic = i,
872 normal = v.normal,
873 crit = v.crit,
874 }
875 end
876 end
877end
878
879function Critline:GetSpellArrayEntry(tree, spellID, periodic)
880 for i, spell in ipairs(spellArrays[tree]) do
881 if spell.id == spellID and spell.periodic == periodic then
882 return spell
883 end
884 end
885end
886
887-- local previousTree
888-- local previousSort
889
890function Critline:GetSpellArray(tree, useProfileSort)
891 local array = spellArrays[tree]
892 if useProfileSort ~= false then
893 local sortMethod = useProfileSort and self.db.profile.tooltipSort or "alpha"
894 -- no need to sort if it's already sorted the way we want it
895 -- if sortMethod ~= previousSort or tree ~= previousTree then
896 sort(array, recordSorters[sortMethod])
897 -- previousTree = tree
898 -- previousSort = sortMethod
899 -- end
900 end
901 return array
902end
903
904-- return spell table from database, given tree, spell name and isPeriodic value
905function Critline:GetSpellInfo(tree, spellID, periodic)
906 local spell = self.percharDB.profile.spells[tree][spellID]
907 return spell and spell[periodic]
908end
909
910function Critline:GetSpellName(spellID, raw)
911 local spellName = spellNameCache[spellID] or GetSpellInfo(spellID)
912 spellNameCache[spellID] = spellName
913 return (not raw and spellNameOverrides[spellID]) or spellName
914end
915
916function Critline:GetSpellTexture(spellID)
917 local spellTexture = spellIconOverrides[spellID] or spellTextureCache[spellID] or GetSpellTexture(spellID)
918 spellTextureCache[spellID] = spellTexture
919 return spellTexture
920end
921
922function Critline:GetFullSpellName(spellName, periodic, verbose)
923 if periodic == 2 then
924 spellName = format("%s (%s)", spellName, verbose and L["tick"] or "*")
925 end
926 return spellName
927end
928
929function Critline:GetFullTargetName(spell)
930 local suffix = ""
931 if spell.isPvPTarget then
932 suffix = format(" (%s)", PVP)
933 end
934 return format("%s%s", spell.target or "??", suffix)
935end
936
937-- retrieves the top, non filtered record amounts and spell names for a given tree
938function Critline:UpdateTopRecords(tree)
939 if not tree then
940 for tree in pairs(topRecords) do
941 self:UpdateTopRecords(tree)
942 end
943 return
944 end
945
946 local normalRecord, critRecord = 0, 0
947
948 for spellID, spell in pairs(self.percharDB.profile.spells[tree]) do
949 for i, v in pairs(spell) do
950 if not (self.filters and v.filtered) then
951 local normal = v.normal
952 if normal then
953 normalRecord = max(normal.amount, normalRecord)
954 end
955 local crit = v.crit
956 if crit then
957 critRecord = max(crit.amount, critRecord)
958 end
959 end
960 end
961 end
962 local topRecords = topRecords[tree]
963 topRecords.normal = normalRecord
964 topRecords.crit = critRecord
965
966 callbacks:Fire("OnNewTopRecord", tree)
967end
968
969-- retrieves the top, non filtered record amounts and spell names for a given tree
970function Critline:GetHighest(tree)
971 local topRecords = topRecords[tree]
972 return topRecords.normal, topRecords.crit
973end
974
975function Critline:AddSpell(tree, spellID, periodic, spellName, filtered)
976 local spells = self.percharDB.profile.spells[tree]
977
978 local spell = spells[spellID] or {}
979 spells[spellID] = spell
980 spell[periodic] = {filtered = filtered}
981
982 local spellArray = spellArrays[tree]
983 local arrayData = {
984 id = spellID,
985 name = spellName,
986 filtered = filtered,
987 periodic = periodic,
988 }
989 spellArray[#spellArray + 1] = arrayData
990
991 return spell[periodic], arrayData
992end
993
994function Critline:DeleteSpell(tree, spellID, periodic)
995 do
996 local tree = self.percharDB.profile.spells[tree]
997 local spell = tree[spellID]
998 spell[periodic] = nil
999
1000 -- remove this entire spell entry if neither direct nor tick entries remain
1001 if not spell[3 - periodic] then
1002 tree[spellID] = nil
1003 end
1004 end
1005
1006 for i, v in ipairs(spellArrays[tree]) do
1007 if v.id == spellID and v.periodic == periodic then
1008 tremove(spellArrays[tree], i)
1009 self:Message(format(L["Reset %s (%s) records."], self:GetFullSpellName(v.name, v.periodic), trees[tree].label))
1010 break
1011 end
1012 end
1013
1014 self:UpdateTopRecords(tree)
1015end
1016
1017-- this "fires" when spells are added to/removed from the database
1018function Critline:UpdateSpells(tree)
1019 if tree then
1020 doTooltipUpdate[tree] = true
1021 callbacks:Fire("SpellsChanged", tree)
1022 else
1023 for k in pairs(tooltips) do
1024 self:UpdateSpells(k)
1025 end
1026 end
1027end
1028
1029-- this "fires" when a new record has been registered
1030function Critline:UpdateRecords(tree, isFiltered)
1031 if tree then
1032 doTooltipUpdate[tree] = true
1033 callbacks:Fire("RecordsChanged", tree, isFiltered)
1034 else
1035 for k in pairs(tooltips) do
1036 self:UpdateRecords(k, isFiltered)
1037 end
1038 end
1039end
1040
1041function Critline:UpdateTooltips()
1042 for k in pairs(tooltips) do
1043 doTooltipUpdate[k] = true
1044 end
1045end
1046
1047local LETHAL_LEVEL = "??"
1048local leftFormat = "|cffc0c0c0%s:|r %s"
1049local leftFormatIndent = leftFormat
1050local rightFormat = format("%s%%s|r (%%s)", HIGHLIGHT_FONT_COLOR_CODE)
1051local recordFormat = format("%s%%s|r", GREEN_FONT_COLOR_CODE)
1052local r, g, b = HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b
1053
1054function Critline:ShowTooltip(tree)
1055 if doTooltipUpdate[tree] then
1056 self:UpdateTooltip(tree)
1057 end
1058 local r, g, b = r, g, b
1059 local rR, gR, bR
1060 GameTooltip:AddLine("Critline "..trees[tree].label, r, g, b)
1061 if not self.db.profile.detailedTooltip then
1062 -- advanced tooltip uses different text color
1063 rR, gR, bR = r, g, b
1064 r, g, b = nil
1065 end
1066 local tooltip = tooltips[tree]
1067 for i = 1, #tooltips[tree] do
1068 local v = tooltip[i]
1069 -- v is either an array containing the left and right tooltip strings, or a single string
1070 if type(v) == "table" then
1071 local left, right = unpack(v)
1072 GameTooltip:AddDoubleLine(left, right, r, g, b, rR, gR, bR)
1073 else
1074 GameTooltip:AddLine(v)
1075 end
1076 end
1077 GameTooltip:Show()
1078end
1079
1080function Critline:UpdateTooltip(tree)
1081 local tooltip = tooltips[tree]
1082 wipe(tooltip)
1083
1084 local normalRecord, critRecord = self:GetHighest(tree)
1085 local n = 1
1086
1087 for _, v in ipairs(self:GetSpellArray(tree, true)) do
1088 if not (self.filters and self:GetSpellInfo(tree, v.id, v.periodic).filtered) then
1089 local spellName = self:GetFullSpellName(v.name, v.periodic)
1090
1091 -- if this is a DoT/HoT, and a direct entry exists, add the proper suffix
1092 -- if v.periodic == 2 and not (self.filters and self.filters:IsFilteredSpell(tree, v.spellID, 1)) then
1093 -- spellName = self:GetFullSpellName(v.spellID, 2)
1094 -- end
1095
1096 if self.db.profile.detailedTooltip then
1097 tooltip[n] = spellName
1098 n = n + 1
1099 tooltip[n] = {self:GetTooltipLine(v, "normal", tree)}
1100 n = n + 1
1101 tooltip[n] = {self:GetTooltipLine(v, "crit", tree)}
1102 else
1103 local normalAmount, critAmount = 0, 0
1104
1105 -- color the top score amount green
1106 local normal = v.normal
1107 if normal then
1108 normalAmount = self:ShortenNumber(normal.amount)
1109 normalAmount = normal.amount == normalRecord and GREEN_FONT_COLOR_CODE..normalAmount..FONT_COLOR_CODE_CLOSE or normalAmount
1110 end
1111
1112 local crit = v.crit
1113 if crit then
1114 critAmount = self:ShortenNumber(crit.amount)
1115 critAmount = crit.amount == critRecord and GREEN_FONT_COLOR_CODE..critAmount..FONT_COLOR_CODE_CLOSE or critAmount
1116 end
1117
1118 tooltip[n] = {spellName, crit and format("%s / %s", normalAmount, critAmount) or normalAmount}
1119 end
1120
1121 n = n + 1
1122 end
1123 end
1124
1125 if #tooltip == 0 then
1126 tooltip[1] = L["No records"]
1127 end
1128
1129 doTooltipUpdate[tree] = nil
1130end
1131
1132local hitTypes = {
1133 normal = L["Normal"],
1134 crit = L["Crit"],
1135}
1136
1137function Critline:GetTooltipLine(data, hitType, tree)
1138 local leftFormat = tree and " "..leftFormat or leftFormat
1139 data = data and data[hitType]
1140 if data then
1141 local amount = self:ShortenNumber(data.amount)
1142 if tree and data.amount == topRecords[tree][hitType] then
1143 amount = format(recordFormat, amount)
1144 end
1145 local level = data.targetLevel or 0
1146 level = level > 0 and level or LETHAL_LEVEL
1147 return format(leftFormat, hitTypes[hitType], amount), format(rightFormat, self:GetFullTargetName(data), level), r, g, b
1148 end
1149end
1150
1151function Critline:AddTooltipLine(data, tree)
1152 GameTooltip:AddDoubleLine(self:GetTooltipLine(data, "normal", tree))
1153 GameTooltip:AddDoubleLine(self:GetTooltipLine(data, "crit", tree))
1154end
1155
1156local funcset = {}
1157
1158for k in pairs(trees) do
1159 funcset[k] = function(spellID)
1160 local spell = Critline.percharDB.profile.spells[k][spellID]
1161 if not spell then
1162 return
1163 end
1164 local direct = spell[1]
1165 local tick = spell[2]
1166 if Critline.filters then
1167 direct = direct and not direct.filtered and direct
1168 tick = tick and not tick.filtered and tick
1169 end
1170 return direct, tick
1171 end
1172end
1173
1174local function addLine(header, nonTick, tick)
1175 if header then
1176 GameTooltip:AddLine(header)
1177 end
1178 Critline:AddTooltipLine(nonTick)
1179 if tick and nonTick then
1180 GameTooltip:AddLine(" ")
1181 GameTooltip:AddLine(L["Tick"])
1182 end
1183 Critline:AddTooltipLine(tick)
1184end
1185
1186GameTooltip:HookScript("OnTooltipSetSpell", function(self)
1187 if self.Critline then
1188 return
1189 end
1190
1191 self.Critline = true
1192
1193 local spellName, spellID = self:GetSpell()
1194
1195 if debugging then
1196 self:AddLine(format("Spell ID: |cffffffff%d|r", spellID))
1197 end
1198
1199 if not Critline.db.profile.spellTooltips then
1200 return
1201 end
1202
1203 spellID = tooltipMappings[spellID] or spellID
1204
1205 local dmg1, dmg2 = funcset.dmg(spellID)
1206 local dmg = dmg1 or dmg2
1207
1208 local heal1, heal2 = funcset.heal(spellID)
1209 local heal = heal1 or heal2
1210
1211 -- ignore pet auto attack records here, since that's handled by another function
1212 local pet1, pet2 = spellID ~= AUTO_ATTACK_ID and funcset.pet(spellID)
1213 local pet = pet1 or pet2
1214
1215 if dmg or heal or pet then
1216 self:AddLine(" ")
1217 end
1218
1219 if dmg then
1220 addLine((heal or pet) and L["Damage"], dmg1, dmg2)
1221 end
1222
1223 if heal then
1224 if dmg then
1225 GameTooltip:AddLine(" ")
1226 end
1227 addLine((dmg or pet) and L["Healing"], heal1, heal2)
1228 end
1229
1230 if pet then
1231 if dmg or heal then
1232 GameTooltip:AddLine(" ")
1233 end
1234 addLine((dmg or heal) and L["Pet"], pet1, pet2)
1235 end
1236end)
1237
1238GameTooltip:HookScript("OnTooltipCleared", function(self)
1239 self.Critline = nil
1240end)
1241
1242hooksecurefunc(GameTooltip, "SetPetAction", function(self, action)
1243 if not Critline.db.profile.spellTooltips then
1244 return
1245 end
1246
1247 if GetPetActionInfo(action) == "PET_ACTION_ATTACK" then
1248 addLine(" ", (funcset.pet(AUTO_ATTACK_ID)))
1249 self:Show()
1250 end
1251end)