--[[ CritTracker - Core Engine (WoW Forever Edition) Optimiert für Interface 16001 / UNIT_COMBAT Engine ]] local addonName, addonTable = ... local CritTracker = CreateFrame("Frame", "CritTrackerFrame") addonTable.CritTracker = CritTracker _G["CritTracker"] = CritTracker -- Timing & State CritTracker.lastOutputTime = 0 -- Standardeinstellungen local defaultSettings = { enabled = true, enableChat = true, enableSound = true, enableLowCrit = true, lowThreshold = 30, -- 30% des Rekords cooldown = 5, -- Cooldown in Sekunden 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" oder "records_only" } -- Schadensschulen-Übersetzung für UNIT_COMBAT local SCHOOL_NAMES = { [0] = { deDE = "Physisch", enUS = "Physical" }, [1] = { deDE = "Physisch", enUS = "Physical" }, [2] = { deDE = "Heilig", enUS = "Holy" }, [4] = { deDE = "Feuer", enUS = "Fire" }, [8] = { deDE = "Natur", enUS = "Nature" }, [16] = { deDE = "Frost", enUS = "Frost" }, [32] = { deDE = "Schatten", enUS = "Shadow" }, [64] = { deDE = "Arkan", enUS = "Arcane" }, } -- Schutz vor Forever Secret Values local function IsPublic(val) if type(val) == "nil" then return false end if type(issecretvalue) == "function" then local ok, secret = pcall(issecretvalue, val) if ok and secret == true then return false end end return true end -- Lokalisierungs-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 }) 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 local locale = GetLocale() return (locale == "deDE") and "German" or "English" end end function CritTracker:GetSoundFilePath(soundKey) local lang = self:GetActiveLanguage() return "Interface\\AddOns\\CritTracker\\sounds\\" .. lang .. "\\" .. soundKey .. ".mp3" end function CritTracker:PlaySound(soundKey) if not self.db or 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 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 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 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 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 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 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 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 if self.InitConfig then self:InitConfig() end end function CritTracker:ProcessCrit(isHeal, spellOrSchool, 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)) if self.db.enableLowCrit and currentRecord > 0 and not isNewRecord and amount <= thresholdValue then isLowCrit = true end if isNewRecord then if isHeal then charData.maxHeal = amount charData.maxHealSpell = spellOrSchool or "" else charData.maxDamage = amount charData.maxDamageSpell = spellOrSchool or "" end if self.UpdateOptionsDisplay then self:UpdateOptionsDisplay() end end local now = GetTime() local cooldown = self.db.cooldown or 5 if (now - self.lastOutputTime) < cooldown then return end local soundKey = nil local chatMsg = nil local destStr = (destName and destName ~= "") and string.format(self.L["TARGET_ON"], destName) or "" local spellStr = (spellOrSchool and spellOrSchool ~= "") and spellOrSchool or (isHeal and "Heilung" or "Treffer") 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 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 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 -- UNIT_COMBAT Parser function CritTracker:OnUnitCombat(unit, action, modifier, amount, school) if not IsPublic(unit) or not IsPublic(action) or not IsPublic(modifier) or not IsPublic(amount) then return end if modifier ~= "CRITICAL" or type(amount) ~= "number" or amount <= 0 then return end local isDamage = false local isHeal = false -- Schaden: Nur 'target' werten (verhindert Alias-Triplets durch nameplate1/softenemy) if action == "WOUND" and unit == "target" then isDamage = true -- Heilung: 'target' oder 'player' elseif action == "HEAL" then if unit == "target" then isHeal = true elseif unit == "player" then -- Verhindert doppelte Auslösung bei Ziel auf sich selbst if not UnitIsUnit("player", "target") then isHeal = true end end end if not isDamage and not isHeal then return end local targetName = UnitName(unit) or (isDamage and "Ziel" or "Spieler") -- Schadensart / Schule auflösen local langKey = (self:GetActiveLanguage() == "German") and "deDE" or "enUS" local schoolName = (school and SCHOOL_NAMES[school] and SCHOOL_NAMES[school][langKey]) or (isHeal and (langKey == "deDE" and "Heilung" or "Heal") or (langKey == "deDE" and "Angriff" or "Attack")) self:ProcessCrit(isHeal, schoolName, amount, targetName) end -- Event Registrierung CritTracker:RegisterEvent("ADDON_LOADED") CritTracker:RegisterEvent("PLAYER_LOGIN") CritTracker:RegisterEvent("PLAYER_LEVEL_UP") CritTracker:RegisterEvent("UNIT_COMBAT") CritTracker:SetScript("OnEvent", function(self, event, ...) if event == "UNIT_COMBAT" then self:OnUnitCombat(...) 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 (/ctr & /crittracker) 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"], "", "Heilig", CritTracker:FormatNumber(50000))) elseif arg == "low" then CritTracker:PlaySound("low") CritTracker:Print(string.format(CritTracker.L["LOW_CRIT_DAMAGE"], "", "Physisch", 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"], "", "Feuer", 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