- 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
371 lines
13 KiB
Lua
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
|