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:
398
CritTracker.lua
Normal file
398
CritTracker.lua
Normal 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
|
||||
Reference in New Issue
Block a user