Addon CritTracker implementiert (WoW Forever 12.x Beta, Interface 16001)

- Getrennte Erfassung für Schaden und Heilung
- Erkennung schwacher Crits mit einstellbarem Schwellenwert (Standard 30%)
- Audio-Feedback (crit1-4, heal1-4, low.mp3) für Deutsch und Englisch
- Einstellungsmenü mit Cooldown-Slider (1-30s), Sound-Auswahl und Test-Buttons
- Automatischer Reset der Rekorde bei Stufenaufstieg (PLAYER_LEVEL_UP)
- Lokale Chat-Benachrichtigungen (nur für Spieler sichtbar)
- Slash-Befehle /ctr und /crittracker
- Entwickler- & Agenten-Leitfaden (AGENT_GUIDE.md) hinzugefügt
- sounds/English/low.mp3 repariert
This commit is contained in:
2026-09-30 09:41:35 +02:00
parent 370ae9b0be
commit edc52fdd86
7 changed files with 1285 additions and 0 deletions

398
CritTracker.lua Normal file
View File

@@ -0,0 +1,398 @@
--[[
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