Files
CritTracker/CritTracker.lua
Sarah Kuhn bf81a9f75a Wechsel auf UNIT_COMBAT Engine (WoW Forever Beta Interface 16001)
- Entferne deaktiviertes COMBAT_LOG_EVENT_UNFILTERED
- Implementiere UNIT_COMBAT mit Anti-Triplet Filter (unit == 'target')
- Doppel-Auslösungs-Schutz bei Selbstheilung
- Mapping von school auf lokalisierte Schadensschulen (SCHOOL_NAMES)
- Parameter-Validierung mit issecretvalue
- AGENT_GUIDE.md aktualisiert
2026-09-30 10:09:23 +02:00

371 lines
13 KiB
Lua

--[[
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