Go to most recent revision | Compare with Previous | Blame | View Log
local addonName, Critline = ... _G.Critline = Critline -- local addon = { } -- local mt_func = { __index = function() return function() end end } -- local empty_tbl = { } -- local mt = { __index = function() return setmetatable(empty_tbl, mt_func) end } -- setmetatable(addon, mt) -- print(addon.module.method()) local L = LibStub("AceLocale-3.0"):GetLocale(addonName) local templates = Critline.templates local playerClass = select(2, UnitClass("player")) local debugging -- auto attack spell local AUTOATK_ID = 6603 local AUTOATK = GetSpellInfo(AUTOATK_ID) -- local references to commonly used functions and variables for faster access local HasPetUI = HasPetUI local tonumber = tonumber local CombatLog_Object_IsA = CombatLog_Object_IsA local bor = bit.bor local band = bit.band local COMBATLOG_FILTER_MINE = COMBATLOG_FILTER_MINE local COMBATLOG_FILTER_MY_PET = COMBATLOG_FILTER_MY_PET local COMBATLOG_OBJECT_REACTION_FRIENDLY = COMBATLOG_OBJECT_REACTION_FRIENDLY local COMBATLOG_OBJECT_REACTION_HOSTILE = COMBATLOG_OBJECT_REACTION_HOSTILE local COMBATLOG_OBJECT_CONTROL_PLAYER = COMBATLOG_OBJECT_CONTROL_PLAYER local COMBATLOG_OBJECT_TYPE_GUARDIAN = COMBATLOG_OBJECT_TYPE_GUARDIAN local treeNames = { dmg = L["damage"], heal = L["healing"], pet = L["pet"], } Critline.treeNames = treeNames Critline.icons = { dmg = [[Interface\Icons\Ability_SteelMelee]], heal = [[Interface\Icons\Spell_Holy_FlashHeal]], pet = [[Interface\Icons\Ability_Hunter_Pet_Bear]], } -- non hunter pets whose damage we may want to register local classPets = { [510] = true, -- Water Elemental [11859] = true, -- Doomguard [15438] = true, -- Greater Fire Elemental [27829] = true, -- Ebon Gargoyle [29264] = true, -- Spirit Wolf [37994] = true, -- Water Elemental (glyphed) } -- spells that are essentially the same, but has different IDs, we'll register them under the same ID local similarSpells = { [27285] = 27243, -- Seed of Corruption (direct) [33778] = 33763, -- Lifebloom (direct) [44461] = 44457, -- Living Bomb (direct) [47960] = 47897, -- Shadowflame (tick) [81170] = 6785, -- Ravage (Stampede) [83853] = 11129, -- Combustion (tick) [88148] = 2120, -- Flamestrike (Improved Flamestrike) [92315] = 11366, -- Pyroblast (Hot Streak) } -- spells whose actual effect is the result of a different spell, eg seals, damage shields, used for displaying relevant records in spell tooltips local indirectSpells = { [324] = 26364, -- Lightning Shield [724] = 7001, -- Lightwell [740] = 44203, -- Tranquility [772] = 94009, -- Rend [974] = 379, -- Earth Shield [1329] = 5374, -- Mutilate [1535] = 8349, -- Fire Nova [1949] = 5857, -- Hellfire [5938] = 5940, -- Shiv [8024] = 10444, -- Flametongue Weapon [8033] = 8034, -- Frostbrand Weapon [8232] = 25504, -- Windfury Weapon [16857] = 60089, -- Faerie Fire (Feral) [16914] = 42231, -- Hurricane [17364] = 32175, -- Stormstrike [20154] = 25742, -- Seal of Righteousness [20164] = 20170, -- Seal of Justice [20165] = 20167, -- Seal of Insight [20473] = 25912, -- Holy Shock [22842] = 22845, -- Frenzied Regeneration [26573] = 81297, -- Consecration [31801] = 42463, -- Seal of Truth [31850] = 66235, -- Ardent Defender [33076] = 33110, -- Prayer of Mending [43265] = 52212, -- Death and Decay [47540] = 47666, -- Penance [47541] = 47632, -- Death Coil (death knight) [47788] = 48153, -- Guardian Spirit [48045] = 49821, -- Mind Sear [51730] = 51945, -- Earthliving [61882] = 77478, -- Earthquake [64843] = 64844, -- Divine Hymn [73920] = 73921, -- Healing Rain [82327] = 86452, -- Holy Radiance [88685] = 88686, -- Holy Word: Sanctuary } -- those that has both a damage and a healing component has their healing spell listed here local indirectHeals = { [15237] = 23455, -- Holy Nova [20473] = 25914, -- Holy Shock [47540] = 47750, -- Penance [47541] = 47633, -- Death Coil (death knight) [49998] = 45470, -- Death Strike [53385] = 54172, -- Divine Storm } -- cache of spell ID -> spell name local spellNameCache = { -- add form name to hybrid druid abilities, so the user can tell which is cat and which is bear [33878] = format("%s (%s)", GetSpellInfo(33878)), -- Mangle (Bear Form) [33876] = format("%s (%s)", GetSpellInfo(33876)), -- Mangle (Cat Form) [779] = format("%s (%s)", GetSpellInfo(779)), -- Swipe (Bear Form) [62078] = format("%s (%s)", GetSpellInfo(62078)), -- Swipe (Cat Form) } -- cache of spell textures local spellTextureCache = { -- use a static icon for auto attack (otherwise uses your weapon's icon) [AUTOATK_ID] = [[Interface\Icons\INV_Sword_04]], -- "fix" some other misleading icons [20253] = GetSpellTexture(20252), -- Intercept [26364] = GetSpellTexture(324), -- use Lightning Shield icon for Lightning Shield damage [66235] = GetSpellTexture(31850), -- Ardent Defender icon for Ardent Defender heal } local swingDamage = function(amount, _, school, resisted, _, _, critical) return AUTOATK_ID, AUTOATK, amount, resisted, critical, school end local spellDamage = function(spellID, spellName, _, amount, _, school, resisted, _, _, critical) return spellID, spellName, amount, resisted, critical, school end local healing = function(spellID, spellName, _, amount, _, _, critical) return spellID, spellName, amount, 0, critical, 0 end local absorb = function(spellID, spellName, _, _, amount) return spellID, spellName, amount, 0, critical, 0 end local combatEvents = { SWING_DAMAGE = swingDamage, RANGE_DAMAGE = spellDamage, SPELL_DAMAGE = spellDamage, SPELL_PERIODIC_DAMAGE = spellDamage, SPELL_HEAL = healing, SPELL_PERIODIC_HEAL = healing, SPELL_AURA_APPLIED = absorb, SPELL_AURA_REFRESH = absorb, } -- alpha: sort by name local alpha = function(a, b) if a == b then return end if a.spellName == b.spellName then if a.spellID == b.spellID then -- sort DoT entries after non DoT return a.periodic < b.periodic else return a.spellID < b.spellID end else return a.spellName < b.spellName end end -- normal: sort by normal > crit > name local normal = function(a, b) if a == b then return end local normalA, normalB = (a.normal and a.normal.amount or 0), (b.normal and b.normal.amount or 0) if normalA == normalB then -- equal normal amounts, sort by crit amount instead local critA, critB = (a.crit and a.crit.amount or 0), (b.crit and b.crit.amount or 0) if critA == critB then -- equal crit amounts too, sort by name instead return alpha(a, b) else return critA > critB end else return normalA > normalB end end -- crit: sort by crit > normal > name local crit = function(a, b) if a == b then return end local critA, critB = (a.crit and a.crit.amount or 0), (b.crit and b.crit.amount or 0) if critA == critB then return normal(a, b) else return critA > critB end end local recordSorters = { alpha = alpha, normal = normal, crit = crit, } local callbacks = LibStub("CallbackHandler-1.0"):New(Critline) Critline.callbacks = callbacks -- this will hold the text for the summary tooltip local tooltips = {dmg = {}, heal = {}, pet = {}} -- indicates whether a given tree will need to have its tooltip updated before next use local doTooltipUpdate = {} -- overall record for each tree local topRecords = { dmg = {normal = 0, crit = 0}, heal = {normal = 0, crit = 0}, pet = {normal = 0, crit = 0}, } -- sortable spell tables local spellArrays = {dmg = {}, heal = {}, pet = {}} -- tooltip for level scanning local tooltip = CreateFrame("GameTooltip", "CritlineTooltip", nil, "GameTooltipTemplate") Critline.eventFrame = CreateFrame("Frame") function Critline:RegisterEvent(event) self.eventFrame:RegisterEvent(event) end function Critline:UnregisterEvent(event) self.eventFrame:UnregisterEvent(event) end Critline:RegisterEvent("ADDON_LOADED") Critline:RegisterEvent("PLAYER_TALENT_UPDATE") Critline:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") Critline.eventFrame:SetScript("OnEvent", function(self, event, ...) return Critline[event] and Critline[event](Critline, ...) end) local config = templates:CreateConfigFrame(addonName, nil, true) do local options = {} Critline.options = options local function toggleTree(self) callbacks:Fire("OnTreeStateChanged", self.setting, self:GetChecked()) local display = Critline.display if display then display:UpdateTree(self.setting) end end local checkButtons = { db = {}, percharDB = {}, { text = L["Record damage"], tooltipText = L["Check to enable damage events to be recorded."], setting = "dmg", perchar = true, func = toggleTree, }, { text = L["Record healing"], tooltipText = L["Check to enable healing events to be recorded."], setting = "heal", perchar = true, func = toggleTree, }, { text = L["Record pet damage"], tooltipText = L["Check to enable pet damage events to be recorded."], setting = "pet", perchar = true, func = toggleTree, }, { text = L["Record PvE"], tooltipText = L["Disable to ignore records where the target is an NPC."], setting = "PvE", gap = 16, }, { text = L["Record PvP"], tooltipText = L["Disable to ignore records where the target is a player."], setting = "PvP", }, { text = L["Ignore vulnerability"], tooltipText = L["Enable to ignore additional damage due to vulnerability."], setting = "ignoreVulnerability", }, { text = L["Chat output"], tooltipText = L["Prints new record notifications to the chat frame."], setting = "chatOutput", newColumn = true, }, { text = L["Play sound"], tooltipText = L["Plays a sound on a new record."], setting = "sound", }, { text = L["Screenshot"], tooltipText = L["Saves a screenshot on a new record."], setting = "screenshot", }, { text = L["Shorten records"], tooltipText = L["Use shorter format for records numbers."], setting = "shortFormat", func = function(self) callbacks:Fire("OnNewTopRecord") Critline:UpdateTooltips() end, gap = 16, }, { text = L["Records in spell tooltips"], tooltipText = L["Include (unfiltered) records in spell tooltips."], setting = "spellTooltips", }, { text = L["Detailed tooltip"], tooltipText = L["Use detailed format in the summary tooltip."], setting = "detailedTooltip", func = function(self) Critline:UpdateTooltips() end, }, } options.checkButtons = checkButtons for i, v in ipairs(checkButtons) do local btn = templates:CreateCheckButton(config, v) if i == 1 then btn:SetPoint("TOPLEFT", config.title, "BOTTOMLEFT", -2, -16) elseif v.newColumn then btn:SetPoint("TOPLEFT", config.title, "BOTTOM", 0, -16) else btn:SetPoint("TOP", checkButtons[i - 1], "BOTTOM", 0, -(v.gap or 8)) end btn.module = Critline local btns = checkButtons[btn.db] btns[#btns + 1] = btn checkButtons[i] = btn end -- summary sort dropdown local menu = { { text = L["Alphabetically"], value = "alpha", }, { text = L["By normal record"], value = "normal", }, { text = L["By crit record"], value = "crit", }, } local sorting = templates:CreateDropDownMenu("CritlineTooltipSorting", config, menu) sorting:SetFrameWidth(120) sorting:SetPoint("TOPLEFT", checkButtons[#checkButtons], "BOTTOMLEFT", -15, -24) sorting.label:SetText(L["Tooltip sorting:"]) sorting.onClick = function(self) self.owner:SetSelectedValue(self.value) Critline.db.profile.tooltipSort = self.value Critline:UpdateTooltips() end options.tooltipSort = sorting end Critline.SlashCmdHandlers = { debug = function() Critline:ToggleDebug() end, } SlashCmdList.CRITLINE = function(msg) msg = msg:trim():lower() local slashCmdHandler = Critline.SlashCmdHandlers[msg] if slashCmdHandler then slashCmdHandler() else Critline:OpenConfig() end end SLASH_CRITLINE1 = "/critline" SLASH_CRITLINE2 = "/cl" local defaults = { profile = { PvE = true, PvP = true, ignoreVulnerability = true, chatOutput = false, sound = false, screenshot = false, shortFormat = false, spellTooltips = false, detailedTooltip = false, tooltipSort = "normal", }, } -- which trees are enabled by default for a given class local treeDefaults = { DEATHKNIGHT = {dmg = true, heal = false, pet = false}, DRUID = {dmg = true, heal = true, pet = false}, HUNTER = {dmg = true, heal = false, pet = true}, MAGE = {dmg = true, heal = false, pet = false}, PALADIN = {dmg = true, heal = true, pet = false}, PRIEST = {dmg = true, heal = true, pet = false}, ROGUE = {dmg = true, heal = false, pet = false}, SHAMAN = {dmg = true, heal = true, pet = false}, WARLOCK = {dmg = true, heal = false, pet = true}, WARRIOR = {dmg = true, heal = false, pet = false}, } function Critline:ADDON_LOADED(addon) if addon == addonName then local AceDB = LibStub("AceDB-3.0") local db = AceDB:New("CritlineDB", defaults, nil) self.db = db local percharDefaults = { profile = treeDefaults[playerClass], } percharDefaults.profile.spells = { dmg = {}, heal = {}, pet = {}, } local percharDB = AceDB:New("CritlinePerCharDB", percharDefaults) self.percharDB = percharDB -- dual spec support local LibDualSpec = LibStub("LibDualSpec-1.0") LibDualSpec:EnhanceDatabase(self.db, addonName) LibDualSpec:EnhanceDatabase(self.percharDB, addonName) db.RegisterCallback(self, "OnProfileChanged", "LoadSettings") db.RegisterCallback(self, "OnProfileCopied", "LoadSettings") db.RegisterCallback(self, "OnProfileReset", "LoadSettings") percharDB.RegisterCallback(self, "OnProfileChanged", "LoadPerCharSettings") percharDB.RegisterCallback(self, "OnProfileCopied", "LoadPerCharSettings") percharDB.RegisterCallback(self, "OnProfileReset", "LoadPerCharSettings") self:UnregisterEvent("ADDON_LOADED") callbacks:Fire("AddonLoaded") self:LoadSettings() self:LoadPerCharSettings() self.ADDON_LOADED = nil end end -- import native spells to new database format (4.0) function Critline:PLAYER_TALENT_UPDATE() if GetMajorTalentTreeBonuses(1) then self:UnregisterEvent("PLAYER_TALENT_UPDATE") self.PLAYER_TALENT_UPDATE = nil else return end local tooltip = CreateFrame("GameTooltip", "CritlineImportScanTooltip", nil, "GameTooltipTemplate") local function getID(query) local link = GetSpellLink(query) if link then return tonumber(link:match("spell:(%d+)")) end for tab = 1, 3 do local id = GetMajorTalentTreeBonuses(tab) if GetSpellInfo(id) == query then return id end for i = 1, GetNumTalents(tab) do local name, _, _, _, _, _, _, _, _, isExceptional = GetTalentInfo(tab, i) if name == query and isExceptional then tooltip:SetOwner(UIParent) tooltip:SetTalent(tab, i) return select(3, tooltip:GetSpell()) end end end end for k, profile in pairs(self.percharDB.profiles) do if profile.spells then for k, tree in pairs(profile.spells) do local spells = {} for i, spell in pairs(tree) do if not spell.spellName then return end local id = getID(spell.spellName) id = (tree == heal and indirectHeals[id]) or indirectSpells[id] or id if id and (spell.normal or spell.crit) then spells[id] = spells[id] or {} spells[id][spell.isPeriodic and 2 or 1] = spell spell.spellName = nil spell.isPeriodic = nil end end profile.spells[k] = spells end end end tooltip:Hide() -- invert filter flag on all spells if inverted filter was enabled if self.filters then if self.filters.db.profile.invertFilter then for k, profile in pairs(self.percharDB.profiles) do if profile.spells then for k, tree in pairs(profile.spells) do for i, spell in pairs(tree) do for i, spell in pairs(spell) do spell.filtered = not spell.filtered end end end end end end for k, profile in pairs(self.filters.db.profiles) do profile.invertFilter = nil end end self:LoadPerCharSettings() end local TOC local dummyTable = {} do -- Because GetBuildInfo() still returns 40000 on the PTR local major, minor, rev = strsplit(".", (GetBuildInfo())) TOC = major * 10000 + minor * 100 end function Critline:COMBAT_LOG_EVENT_UNFILTERED(timestamp, eventType, hideCaster, sourceGUID, sourceName, sourceFlags, destGUID, destName, destFlags, ...) -- we seem to get events with standard arguments equal to nil, so they need to be ignored if not (timestamp and eventType) then self:Debug("nil errors on start") return end if TOC < 40100 and hideCaster ~= dummyTable then self:COMBAT_LOG_EVENT_UNFILTERED(timestamp, eventType, dummyTable, hideCaster, sourceGUID, sourceName, sourceFlags, destGUID, destName, destFlags, ...) return end -- 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 if not (destName or sourceName) then self:Debug("nil source/dest") return end local isPet -- if sourceGUID is not us or our pet, we leave if not CombatLog_Object_IsA(sourceFlags, COMBATLOG_FILTER_MINE) then local isMyPet = CombatLog_Object_IsA(sourceFlags, COMBATLOG_FILTER_MY_PET) local isGuardian = band(sourceFlags, COMBATLOG_OBJECT_TYPE_GUARDIAN) ~= 0 -- only register if it's a real pet, or a guardian tree pet that's included in the filter if isMyPet and ((not isGuardian and HasPetUI()) or classPets[tonumber(sourceGUID:sub(7, 10), 16)]) then isPet = true -- self:Debug(format("This is my pet (%s)", sourceName)) else -- self:Debug("This is not me, my trap or my pet; return.") return end else -- self:Debug(format("This is me or my trap (%s)", sourceName)) end if not combatEvents[eventType] then return end local isPeriodic local periodic = 1 local isHeal = eventType == "SPELL_HEAL" or eventType == "SPELL_PERIODIC_HEAL" or eventType == "SPELL_AURA_APPLIED" or eventType == "SPELL_AURA_REFRESH" -- we don't care about healing done by the pet if isHeal and isPet then self:Debug("Pet healing. Return.") return end if eventType == "SPELL_PERIODIC_DAMAGE" or eventType == "SPELL_PERIODIC_HEAL" then isPeriodic = true periodic = 2 end -- get the relevants arguments local spellID, spellName, amount, resisted, critical, school = combatEvents[eventType](...) local similarSpell = similarSpells[spellID] if similarSpell then spellID = similarSpell spellName = self:GetSpellName(similarSpell) end -- return if the event has no amount (non-absorbing aura applied) if not amount then return end if amount <= 0 then self:Debug(format("Amount <= 0. (%s) Return.", self:GetFullSpellName(spellID, periodic))) return end local tree = "dmg" if isPet then tree = "pet" elseif isHeal then tree = "heal" end local targetLevel = self:GetLevelFromGUID(destGUID) local passed, isFiltered if self.filters then passed, isFiltered = self.filters:SpellPassesFilters(tree, spellName, spellID, isPeriodic, destGUID, destName, school, targetLevel) if not passed then return end end local isPlayer = band(destFlags, COMBATLOG_OBJECT_CONTROL_PLAYER) ~= 0 local friendlyFire = band(destFlags, COMBATLOG_OBJECT_REACTION_FRIENDLY) ~= 0 local hostileTarget = band(destFlags, COMBATLOG_OBJECT_REACTION_HOSTILE) ~= 0 if not (isPlayer or self.db.profile.PvE or isHeal) then self:Debug(format("Target (%s) is an NPC and PvE damage is not registered.", destName)) return end if isPlayer and not (self.db.profile.PvP or isHeal or friendlyFire) then self:Debug(format("Target (%s) is a player and PvP damage is not registered.", destName)) return end -- ignore damage done to friendly targets if friendlyFire and not isHeal then self:Debug(format("Friendly fire (%s, %s).", spellName, destName)) return end -- ignore healing done to hostile targets if hostileTarget and isHeal then self:Debug(format("Healing hostile target (%s, %s).", spellName, destName)) return end -- exit if not recording tree dmg if not self.percharDB.profile[tree] then self:Debug(format("Not recording this tree (%s). Return.", tree)) return end -- ignore vulnerability damage if necessary if self.db.profile.ignoreVulnerability and resisted and resisted < 0 then amount = amount + resisted self:Debug(format("%d vulnerability damage ignored for a real value of %d.", abs(resisted), amount)) end local hitType = critical and "crit" or "normal" local data = self:GetSpellInfo(tree, spellID, periodic) local arrayData -- create spell database entries as required if not data then self:Debug(format("Creating data for %s (%s)", self:GetFullSpellName(spellID, periodic), tree)) data, arrayData = self:AddSpell(tree, spellID, periodic, spellName, isFiltered) self:UpdateSpells(tree) end if not data[hitType] then data[hitType] = {amount = 0} (arrayData or self:GetSpellArrayEntry(tree, spellID, periodic))[hitType] = data[hitType] end data = data[hitType] -- if new amount is larger than the stored amount we'll want to store it if amount > data.amount then self:NewRecord(tree, spellID, periodic, amount, critical, data, isFiltered) if not isFiltered then -- update the highest record if needed local topRecords = topRecords[tree] if amount > topRecords[hitType] then topRecords[hitType] = amount callbacks:Fire("OnNewTopRecord", tree) end end data.amount = amount data.target = destName data.targetLevel = targetLevel data.isPvPTarget = isPlayer self:UpdateRecords(tree, isFiltered) end end function Critline:GetLevelFromGUID(destGUID) tooltip:SetOwner(UIParent, "ANCHOR_NONE") tooltip:SetHyperlink("unit:"..destGUID) local level = -1 for i = 1, tooltip:NumLines() do local text = _G["CritlineTooltipTextLeft"..i]:GetText() if text then if text:match(LEVEL) then -- our destGUID has the word Level in it. level = text:match("(%d+)") -- find the level if level then -- if we found the level, break from the for loop level = tonumber(level) else -- well, the word Level is in this tooltip, but we could not find the level -- either the destGUID is at least 10 levels higher than us, or we just couldn't find it. level = -1 end end end end return level end function Critline:Message(msg) if msg then DEFAULT_CHAT_FRAME:AddMessage("|cffffff00Critline:|r "..msg) end end function Critline:Debug(msg) if debugging then DEFAULT_CHAT_FRAME:AddMessage("|cff56a3ffCritlineDebug:|r "..msg) end end function Critline:ToggleDebug() debugging = not debugging self:Message("Debugging "..(debugging and "enabled" or "disabled")) end function Critline:OpenConfig() InterfaceOptionsFrame_OpenToCategory(config) end function Critline:LoadSettings() callbacks:Fire("SettingsLoaded") local options = self.options for _, btn in ipairs(options.checkButtons.db) do btn:LoadSetting() end options.tooltipSort:SetSelectedValue(self.db.profile.tooltipSort) end function Critline:LoadPerCharSettings() for tree in pairs(treeNames) do wipe(spellArrays[tree]) for spellID, spell in pairs(self.percharDB.profile.spells[tree]) do for i, v in pairs(spell) do if type(v) ~= "table" or v.spellName then return end -- avoid error in pre 4.0 DB spellArrays[tree][#spellArrays[tree] + 1] = { spellID = spellID, spellName = self:GetSpellName(spellID), filtered = v.filtered, periodic = i, normal = v.normal, crit = v.crit, } end end end callbacks:Fire("PerCharSettingsLoaded") self:UpdateTopRecords() self:UpdateTooltips() for _, btn in ipairs(self.options.checkButtons.percharDB) do btn:LoadSetting() end end function Critline:NewRecord(tree, spellID, periodic, amount, critical, prevRecord, isFiltered) callbacks:Fire("NewRecord", tree, spellID, periodic, amount, critical, prevRecord, isFiltered) if isFiltered then return end amount = self:ShortenNumber(amount) if self.db.profile.oldRecord and prevRecord.amount > 0 then amount = format("%s (%s)", amount, self:ShortenNumber(prevRecord.amount)) end if self.db.profile.chatOutput then self:Message(format(L["New %s%s record - %s"], critical and L["critical "] or "", self:GetFullSpellName(spellID, periodic, true), amount)) end if self.db.profile.sound then PlaySound("LEVELUP", 1, 1, 0, 1, 3) end if self.db.profile.screenshot then TakeScreenshot() end end local FIRST_NUMBER_CAP = FIRST_NUMBER_CAP:lower() function Critline:ShortenNumber(amount) if tonumber(amount) and self.db.profile.shortFormat then if amount >= 1e7 then amount = (floor(amount / 1e5) / 10)..SECOND_NUMBER_CAP elseif amount >= 1e6 then amount = (floor(amount / 1e4) / 100)..SECOND_NUMBER_CAP elseif amount >= 1e4 then amount = (floor(amount / 100) / 10)..FIRST_NUMBER_CAP end end return amount end function Critline:GetSpellArrayEntry(tree, spellID, periodic) for i, v in ipairs(spellArrays[tree]) do if v.spellID == spellID and v.periodic == periodic then return v end end end -- local previousTree -- local previousSort function Critline:GetSpellArray(tree, useProfileSort) local array = spellArrays[tree] local sortMethod = useProfileSort and self.db.profile.tooltipSort or "alpha" -- no need to sort if it's already sorted the way we want it -- if sortMethod ~= previousSort or tree ~= previousTree then sort(array, recordSorters[sortMethod]) -- previousTree = tree -- previousSort = sortMethod -- end return array end -- return spell table from database, given tree, spell name and isPeriodic value function Critline:GetSpellInfo(tree, spellID, periodic) local spell = self.percharDB.profile.spells[tree][spellID] return spell and spell[periodic] end function Critline:GetSpellName(spellID) local spellName = spellNameCache[spellID] or GetSpellInfo(spellID) spellNameCache[spellID] = spellName return spellName end function Critline:GetSpellTexture(spellID) local spellTexture = spellTextureCache[spellID] or GetSpellTexture(spellID) spellTextureCache[spellID] = spellTexture return spellTexture end function Critline:GetFullSpellName(spellID, periodic, verbose) local spellName = self:GetSpellName(spellID) if periodic == 2 then spellName = format("%s (%s)", spellName, verbose and L["tick"] or "*") end return spellName end function Critline:GetFullTargetName(spell) local suffix = "" if spell.isPvPTarget then suffix = format(" (%s)", PVP) end return format("%s%s", spell.target, suffix) end -- retrieves the top, non filtered record amounts and spell names for a given tree function Critline:UpdateTopRecords(tree) if not tree then for tree in pairs(topRecords) do self:UpdateTopRecords(tree) end return end local normalRecord, critRecord = 0, 0 for spellID, spell in pairs(self.percharDB.profile.spells[tree]) do for i, v in pairs(spell) do if type(v) ~= "table" then return end -- avoid error in pre 4.0 DB if not (self.filters and v.filtered) then local normal = v.normal if normal then normalRecord = max(normal.amount, normalRecord) end local crit = v.crit if crit then critRecord = max(crit.amount, critRecord) end end end end local topRecords = topRecords[tree] topRecords.normal = normalRecord topRecords.crit = critRecord callbacks:Fire("OnNewTopRecord", tree) end -- retrieves the top, non filtered record amounts and spell names for a given tree function Critline:GetHighest(tree) local topRecords = topRecords[tree] return topRecords.normal, topRecords.crit end function Critline:AddSpell(tree, spellID, periodic, spellName, filtered) local spells = self.percharDB.profile.spells[tree] local spell = spells[spellID] or {} spells[spellID] = spell spell[periodic] = {filtered = filtered} local spellArray = spellArrays[tree] local arrayData = { spellID = spellID, spellName = spellName, filtered = filtered, periodic = periodic, } spellArray[#spellArray + 1] = arrayData return spell[periodic], arrayData end function Critline:DeleteSpell(tree, spellID, periodic) do local tree = self.percharDB.profile.spells[tree] local spell = tree[spellID] spell[periodic] = nil -- remove this entire spell entry if neither direct nor tick entries remain if not spell[3 - periodic] then tree[spellID] = nil end end for i, v in ipairs(spellArrays[tree]) do if v.spellID == spellID and v.periodic == periodic then tremove(spellArrays[tree], i) self:Message(format(L["Reset %s (%s) records."], self:GetFullSpellName(v.spellID, v.periodic), treeNames[tree])) break end end self:UpdateTopRecords(tree) end -- this "fires" when spells are added to/removed from the database function Critline:UpdateSpells(tree) if tree then doTooltipUpdate[tree] = true callbacks:Fire("SpellsChanged", tree) else for k in pairs(tooltips) do self:UpdateSpells(k) end end end -- this "fires" when a new record has been registered function Critline:UpdateRecords(tree, isFiltered) if tree then doTooltipUpdate[tree] = true callbacks:Fire("RecordsChanged", tree, isFiltered) else for k in pairs(tooltips) do self:UpdateRecords(k, isFiltered) end end end function Critline:UpdateTooltips() for k in pairs(tooltips) do doTooltipUpdate[k] = true end end local LETHAL_LEVEL = "??" local leftFormat = "|cffc0c0c0%s:|r %s" local leftFormatIndent = leftFormat local rightFormat = format("%s%%s|r (%%s)", HIGHLIGHT_FONT_COLOR_CODE) local recordFormat = format("%s%%s|r", GREEN_FONT_COLOR_CODE) local r, g, b = HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b function Critline:ShowTooltip(tree) if doTooltipUpdate[tree] then self:UpdateTooltip(tree) end local r, g, b = r, g, b local rR, gR, bR GameTooltip:AddLine("Critline "..treeNames[tree], r, g, b) if not self.db.profile.detailedTooltip then -- advanced tooltip uses different text color rR, gR, bR = r, g, b r, g, b = nil end local tooltip = tooltips[tree] for i = 1, #tooltips[tree] do local v = tooltip[i] -- v is either an array containing the left and right tooltip strings, or a single string if type(v) == "table" then local left, right = unpack(v) GameTooltip:AddDoubleLine(left, right, r, g, b, rR, gR, bR) else GameTooltip:AddLine(v) end end GameTooltip:Show() end function Critline:UpdateTooltip(tree) local tooltip = tooltips[tree] wipe(tooltip) local normalRecord, critRecord = self:GetHighest(tree) local n = 1 for _, v in ipairs(self:GetSpellArray(tree, true)) do if not (self.filters and self:GetSpellInfo(tree, v.spellID, v.periodic).filtered) then local spellName = self:GetFullSpellName(v.spellID, v.periodic) -- if this is a DoT/HoT, and a direct entry exists, add the proper suffix -- if v.periodic == 2 and not (self.filters and self.filters:IsFilteredSpell(tree, v.spellID, 1)) then -- spellName = self:GetFullSpellName(v.spellID, 2) -- end if self.db.profile.detailedTooltip then tooltip[n] = spellName n = n + 1 tooltip[n] = {self:GetTooltipLine(v, "normal", tree)} n = n + 1 tooltip[n] = {self:GetTooltipLine(v, "crit", tree)} else local normalAmount, critAmount = 0, 0 -- color the top score amount green local normal = v.normal if normal then normalAmount = self:ShortenNumber(normal.amount) normalAmount = normal.amount == normalRecord and GREEN_FONT_COLOR_CODE..normalAmount..FONT_COLOR_CODE_CLOSE or normalAmount end local crit = v.crit if crit then critAmount = self:ShortenNumber(crit.amount) critAmount = crit.amount == critRecord and GREEN_FONT_COLOR_CODE..critAmount..FONT_COLOR_CODE_CLOSE or critAmount end tooltip[n] = {spellName, crit and format("%s / %s", normalAmount, critAmount) or normalAmount} end n = n + 1 end end if #tooltip == 0 then tooltip[1] = L["No records"] end doTooltipUpdate[tree] = nil end local hitTypes = { normal = L["Normal"], crit = L["Crit"], } function Critline:GetTooltipLine(data, hitType, tree) local leftFormat = tree and " "..leftFormat or leftFormat data = data and data[hitType] if data then local amount = self:ShortenNumber(data.amount) if tree and data.amount == topRecords[tree][hitType] then amount = format(recordFormat, amount) end local level = data.targetLevel level = level > 0 and level or LETHAL_LEVEL return format(leftFormat, hitTypes[hitType], amount), format(rightFormat, self:GetFullTargetName(data), level), r, g, b end end function Critline:AddTooltipLine(data, tree) GameTooltip:AddDoubleLine(self:GetTooltipLine(data, "normal", tree)) GameTooltip:AddDoubleLine(self:GetTooltipLine(data, "crit", tree)) end local funcset = {} for k in pairs(treeNames)do funcset[k] = function(spellID, num) local data = Critline:GetSpellInfo(k, spellID, num) return not (Critline.filters and data and data.filtered) and data end end local function addLine(header, nonTick, tick) if header then GameTooltip:AddLine(L[header]) end Critline:AddTooltipLine(nonTick) if tick and nonTick then GameTooltip:AddLine(" ") GameTooltip:AddLine(L["Tick"]) end Critline:AddTooltipLine(tick) end GameTooltip:HookScript("OnTooltipSetSpell", function(self) if self.Critline or not Critline.db.profile.spellTooltips then return end local spellName, rank, spellID = self:GetSpell() local indirectSpell = indirectSpells[spellID] local indirectHeal = indirectHeals[spellID] local dmg1 = funcset.dmg(indirectSpell or spellID, 1) local dmg2 = funcset.dmg(indirectSpell or spellID, 2) local dmg = dmg1 or dmg2 local heal1 = funcset.heal(indirectHeal or indirectSpell or spellID, 1) local heal2 = funcset.heal(indirectHeal or indirectSpell or spellID, 2) local heal = heal1 or heal2 local pet1 = funcset.pet(indirectSpell or spellID, 1) local pet2 = funcset.pet(indirectSpell or spellID, 2) local pet = pet1 or pet2 if dmg or heal or pet then self:AddLine(" ") end if dmg then addLine((heal or pet) and "Damage", dmg1, dmg2) end if heal then if dmg then GameTooltip:AddLine(" ") end addLine((dmg or pet) and "Healing", heal1, heal2) end if pet then if dmg or heal then GameTooltip:AddLine(" ") end addLine((dmg or heal) and "Pet", pet1, pet2) end end) GameTooltip:HookScript("OnTooltipCleared", function(self) self.Critline = nil end)