--[[ CritTracker - Core Engine World of Warcraft - Forever (12.x Beta / Interface 16001) ]] local addonName, addonTable = ... local CritTracker = CreateFrame("Frame", "CritTrackerFrame") addonTable.CritTracker = CritTracker _G["CritTracker"] = CritTracker -- Timing & State CritTracker.lastOutputTime = 0 -- Defaults local defaultSettings = { enabled = true, enableChat = true, enableSound = true, enableLowCrit = true, lowThreshold = 30, -- 30% of highest recorded crit cooldown = 5, -- 5 seconds output cooldown (1-30s) damageSound = "crit1", -- crit1, crit2, crit3, crit4, none healSound = "heal1", -- heal1, heal2, heal3, heal4, none lowSound = "low", -- low, none soundChannel = "Master", -- Master, Dialog, SFX language = "auto", -- "auto", "German", "English" announceMode = "all", -- "all" (subject to cooldown) or "records_only" } -- Dynamic Localization Proxy CritTracker.L = setmetatable({}, { __index = function(_, key) local lang = CritTracker:GetActiveLanguage() local locKey = (lang == "German") and "deDE" or "enUS" local locTable = (addonTable.Locales and addonTable.Locales[locKey]) or (addonTable.Locales and addonTable.Locales["enUS"]) if locTable and locTable[key] then return locTable[key] end if addonTable.Locales and addonTable.Locales["enUS"] and addonTable.Locales["enUS"][key] then return addonTable.Locales["enUS"][key] end return key end }) -- Language Resolution function CritTracker:GetActiveLanguage() local setting = (self.db and self.db.language) or "auto" if setting == "German" then return "German" elseif setting == "English" then return "English" else -- Auto: German client -> German, all others -> English local locale = GetLocale() if locale == "deDE" then return "German" else return "English" end end end -- Sound File Path function CritTracker:GetSoundFilePath(soundKey) local lang = self:GetActiveLanguage() return "Interface\\AddOns\\CritTracker\\sounds\\" .. lang .. "\\" .. soundKey .. ".mp3" end -- Play Sound function CritTracker:PlaySound(soundKey) if not self.db.enableSound or not soundKey or soundKey == "none" then return end local path = self:GetSoundFilePath(soundKey) local channel = self.db.soundChannel or "Master" PlaySoundFile(path, channel) end -- Print Message (Visible strictly to the local player) function CritTracker:Print(msg) if not msg then return end if DEFAULT_CHAT_FRAME then DEFAULT_CHAT_FRAME:AddMessage(msg) else print(msg) end end -- Format Numbers with commas / dots function CritTracker:FormatNumber(n) if not n then return "0" end if BreakUpLargeNumbers then return BreakUpLargeNumbers(n) end local left, num, right = string.match(tostring(n), '^([^%d]*%d)(%d*)(.-)$') if not left then return tostring(n) end return left .. (num:reverse():gsub('(%d%d%d)', '%1.'):reverse()) .. right end -- Character Data Accessor (strictly per character & realm) function CritTracker:GetCharKey() local name = UnitName("player") or "Unknown" local realm = GetRealmName() or "UnknownRealm" return name .. " - " .. realm end function CritTracker:GetCharData() if not CritTrackerDB then CritTrackerDB = {} end if not CritTrackerDB.chars then CritTrackerDB.chars = {} end local key = self:GetCharKey() if not CritTrackerDB.chars[key] then CritTrackerDB.chars[key] = { level = UnitLevel("player") or 1, maxDamage = 0, maxDamageSpell = "", maxHeal = 0, maxHealSpell = "", } end return CritTrackerDB.chars[key] end -- Reset Records for Current Character function CritTracker:ResetRecords() local charData = self:GetCharData() charData.maxDamage = 0 charData.maxDamageSpell = "" charData.maxHeal = 0 charData.maxHealSpell = "" charData.level = UnitLevel("player") or 1 local charName = UnitName("player") or "Player" self:Print(string.format(self.L["RESET_SUCCESS"], charName)) if self.UpdateOptionsDisplay then self:UpdateOptionsDisplay() end end -- Level Up Handler function CritTracker:OnLevelUp(newLevel) local charData = self:GetCharData() charData.maxDamage = 0 charData.maxDamageSpell = "" charData.maxHeal = 0 charData.maxHealSpell = "" charData.level = newLevel or UnitLevel("player") self:Print(string.format(self.L["LEVEL_UP_RESET"], charData.level)) if self.UpdateOptionsDisplay then self:UpdateOptionsDisplay() end end -- Initialization function CritTracker:OnInitialize() if not CritTrackerDB then CritTrackerDB = {} end if not CritTrackerDB.settings then CritTrackerDB.settings = {} end for k, v in pairs(defaultSettings) do if CritTrackerDB.settings[k] == nil then CritTrackerDB.settings[k] = v end end self.db = CritTrackerDB.settings -- Initialize Character local charData = self:GetCharData() local curLevel = UnitLevel("player") if charData.level and charData.level > 0 and curLevel > charData.level then -- Leveled up while offline / addon disabled self:OnLevelUp(curLevel) else charData.level = curLevel end if self.InitConfig then self:InitConfig() end end -- Main Crit Processing function CritTracker:ProcessCrit(isHeal, spellName, amount, destName) if not self.db or not self.db.enabled or not amount or amount <= 0 then return end local charData = self:GetCharData() local currentRecord = isHeal and charData.maxHeal or charData.maxDamage local isNewRecord = (amount > currentRecord) local isLowCrit = false local thresholdPercent = self.db.lowThreshold or 30 local thresholdValue = math.floor(currentRecord * (thresholdPercent / 100)) -- Check for Low Crit if self.db.enableLowCrit and currentRecord > 0 and not isNewRecord and amount <= thresholdValue then isLowCrit = true end -- Update Record if higher if isNewRecord then if isHeal then charData.maxHeal = amount charData.maxHealSpell = spellName or "" else charData.maxDamage = amount charData.maxDamageSpell = spellName or "" end if self.UpdateOptionsDisplay then self:UpdateOptionsDisplay() end end -- Cooldown Check for Audio and Text Output local now = GetTime() local cooldown = self.db.cooldown or 5 if (now - self.lastOutputTime) < cooldown then -- Suppressed by Cooldown (Records are already preserved) return end -- Determine Sound and Text local soundKey = nil local chatMsg = nil local destStr = (destName and destName ~= "") and string.format(self.L["TARGET_ON"], destName) or "" local spellStr = (spellName and spellName ~= "") and spellName or self.L["UNKNOWN"] local amountStr = self:FormatNumber(amount) local recordStr = self:FormatNumber(currentRecord) if isLowCrit then soundKey = self.db.lowSound or "low" local formatStr = isHeal and self.L["LOW_CRIT_HEAL"] or self.L["LOW_CRIT_DAMAGE"] chatMsg = string.format(formatStr, destStr, spellStr, amountStr, thresholdPercent, recordStr) elseif isNewRecord then soundKey = isHeal and (self.db.healSound or "heal1") or (self.db.damageSound or "crit1") local formatStr = isHeal and self.L["NEW_RECORD_HEAL"] or self.L["NEW_RECORD_DAMAGE"] chatMsg = string.format(formatStr, destStr, spellStr, amountStr) else -- Normal Crit (between threshold and current record) if self.db.announceMode == "all" then soundKey = isHeal and (self.db.healSound or "heal1") or (self.db.damageSound or "crit1") local formatStr = isHeal and self.L["NORMAL_CRIT_HEAL"] or self.L["NORMAL_CRIT_DAMAGE"] chatMsg = string.format(formatStr, destStr, spellStr, amountStr) end end -- If something triggered, apply cooldown and output if soundKey or chatMsg then self.lastOutputTime = now if self.db.enableSound and soundKey and soundKey ~= "none" then self:PlaySound(soundKey) end if self.db.enableChat and chatMsg then self:Print(chatMsg) end end end -- Combat Log Parser function CritTracker:OnCombatLogEvent() local timestamp, subevent, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20, arg21, arg22, arg23, arg24 = CombatLogGetCurrentEventInfo() if not sourceGUID or sourceGUID ~= UnitGUID("player") then return end if subevent == "SWING_DAMAGE" then local amount = arg12 local overkill = arg13 local school = arg14 local resisted = arg15 local blocked = arg16 local absorbed = arg17 local critical = arg18 if critical and critical ~= 0 then local totalDamage = (amount or 0) + (absorbed or 0) self:ProcessCrit(false, self.L["MELEE_ATTACK"], totalDamage, destName) end elseif subevent == "SPELL_DAMAGE" or subevent == "RANGE_DAMAGE" or subevent == "SPELL_PERIODIC_DAMAGE" or subevent == "SPELL_BUILDING_DAMAGE" then local spellId = arg12 local spellName = arg13 local spellSchool = arg14 local amount = arg15 local overkill = arg16 local school = arg17 local resisted = arg18 local blocked = arg19 local absorbed = arg20 local critical = arg21 if critical and critical ~= 0 then local totalDamage = (amount or 0) + (absorbed or 0) self:ProcessCrit(false, spellName, totalDamage, destName) end elseif subevent == "SPELL_HEAL" or subevent == "SPELL_PERIODIC_HEAL" or subevent == "SPELL_BUILDING_HEAL" then local spellId = arg12 local spellName = arg13 local spellSchool = arg14 local amount = arg15 local overhealing = arg16 local absorbed = arg17 local critical = arg18 if critical and critical ~= 0 then local totalHeal = (amount or 0) + (overhealing or 0) self:ProcessCrit(true, spellName, totalHeal, destName) end end end -- Event Registration CritTracker:RegisterEvent("ADDON_LOADED") CritTracker:RegisterEvent("PLAYER_LOGIN") CritTracker:RegisterEvent("PLAYER_LEVEL_UP") CritTracker:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") CritTracker:SetScript("OnEvent", function(self, event, ...) if event == "COMBAT_LOG_EVENT_UNFILTERED" then self:OnCombatLogEvent() elseif event == "PLAYER_LEVEL_UP" then local newLevel = ... self:OnLevelUp(newLevel) elseif event == "ADDON_LOADED" then local name = ... if name == addonName then self:OnInitialize() end elseif event == "PLAYER_LOGIN" then local charData = self:GetCharData() local curLevel = UnitLevel("player") if charData.level and charData.level > 0 and curLevel > charData.level then self:OnLevelUp(curLevel) else charData.level = curLevel end end end) -- Slash Commands SLASH_CRITTRACKER1 = "/crittracker" SLASH_CRITTRACKER2 = "/ctr" SlashCmdList["CRITTRACKER"] = function(msg) local cmd, arg = string.match((msg or ""):lower():trim(), "^(%S*)%s*(.-)$") if cmd == "" or cmd == "config" or cmd == "options" or cmd == "menu" then if CritTracker.OpenOptions then CritTracker:OpenOptions() end elseif cmd == "reset" then CritTracker:ResetRecords() elseif cmd == "stats" then local charData = CritTracker:GetCharData() local dmgStr = CritTracker:FormatNumber(charData.maxDamage) local healStr = CritTracker:FormatNumber(charData.maxHeal) local dmgSpell = (charData.maxDamageSpell ~= "") and charData.maxDamageSpell or CritTracker.L["OPT_STATS_NONE"] local healSpell = (charData.maxHealSpell ~= "") and charData.maxHealSpell or CritTracker.L["OPT_STATS_NONE"] local name = UnitName("player") or "Player" CritTracker:Print(string.format(CritTracker.L["CURRENT_STATS"], name, charData.level or 1, dmgStr, dmgSpell, healStr, healSpell)) elseif cmd == "test" then if arg == "heal" then CritTracker:PlaySound(CritTracker.db.healSound or "heal1") CritTracker:Print(string.format(CritTracker.L["NORMAL_CRIT_HEAL"], "", "Test Heal", CritTracker:FormatNumber(50000))) elseif arg == "low" then CritTracker:PlaySound("low") CritTracker:Print(string.format(CritTracker.L["LOW_CRIT_DAMAGE"], "", "Test Low Hit", CritTracker:FormatNumber(150), CritTracker.db.lowThreshold, CritTracker:FormatNumber(1000))) else CritTracker:PlaySound(CritTracker.db.damageSound or "crit1") CritTracker:Print(string.format(CritTracker.L["NORMAL_CRIT_DAMAGE"], "", "Test Strike", CritTracker:FormatNumber(125000))) end else CritTracker:Print(CritTracker.L["SLASH_HELP_HEADER"]) CritTracker:Print(CritTracker.L["SLASH_HELP_CONFIG"]) CritTracker:Print(CritTracker.L["SLASH_HELP_STATS"]) CritTracker:Print(CritTracker.L["SLASH_HELP_RESET"]) CritTracker:Print(CritTracker.L["SLASH_HELP_TEST"]) end end