- 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
399 lines
14 KiB
Lua
399 lines
14 KiB
Lua
--[[
|
|
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
|