diff --git a/AGENT_GUIDE.md b/AGENT_GUIDE.md new file mode 100644 index 0000000..544aa8b --- /dev/null +++ b/AGENT_GUIDE.md @@ -0,0 +1,185 @@ +# CritTracker – Entwickler- & Agenten-Leitfaden (Developer & Agent Guide) + +Dieses Dokument dient als technische Referenz für Entwickler und KI-Agenten, die das Addon **CritTracker** nachvollziehen, erweitern oder in einer vergleichbaren Umgebung reproduzieren möchten. + +--- + +## 1. System- & Zielumgebung + +- **Spiel / Client:** World of Warcraft - Forever (Beta) +- **Engine-Basis:** WoW Retail 12.x Engine +- **TOC Interface-Version:** `16001` (`## Interface: 16001`) +- **Addon-Verzeichnis:** `Interface/AddOns/CritTracker/` +- **Slash-Befehle:** `/ctr`, `/crittracker` (Hinweis: `/ct` wurde bewusst vermieden, um Namenskonflikte mit anderen Addons wie CTMod auszuschließen). + +--- + +## 2. Projektstruktur + +``` +CritTracker/ +├── CritTracker.toc # Addon-Manifest für Interface 16001 +├── Localization.lua # Lokalisierungs-Strings (deDE und enUS) +├── CritTracker.lua # Core Engine: Event-Handling, Combat-Log, Cooldown, Reset +├── Config.lua # UI: 2-Spalten Einstellungsmenü (Modern Settings API + Standalone) +├── README.md # Benutzerdokumentation +├── AGENT_GUIDE.md # Diese technische Spezifikation für Agenten +└── sounds/ + ├── German/ # Deutsche Audio-Dateien + │ ├── crit1.mp3 .. crit4.mp3 + │ ├── heal1.mp3 .. heal4.mp3 + │ └── low.mp3 + └── English/ # Englische Audio-Dateien + ├── crit1.mp3 .. crit4.mp3 + ├── heal1.mp3 .. heal4.mp3 + └── low.mp3 +``` + +> [!IMPORTANT] +> **Audio-Asset-Korrektur:** Die ursprüngliche Datei `sounds/English/low.mp3` war eine fehlerhafte 1.024-Byte-Null-Datei (Stille). Sie wurde durch die funktionierende Audiodatei aus `sounds/German/low.mp3` ersetzt, damit englische Clients nicht stumm bleiben. + +--- + +## 3. Datenarchitektur (`SavedVariables: CritTrackerDB`) + +Die Daten werden in `CritTrackerDB` gespeichert. Die Konfiguration ist accountweit geteilt, während die Rekorde strikt pro Charakter und Realm isoliert sind. + +```lua +CritTrackerDB = { + settings = { + enabled = true, -- Addon an/aus + enableChat = true, -- Lokale Chat-Ausgabe an/aus + enableSound = true, -- Soundausgabe an/aus + enableLowCrit = true, -- Erkennung schwacher Crits an/aus + lowThreshold = 30, -- Grenzwert in Prozent (Standard: 30%) + cooldown = 5, -- Cooldown in Sekunden (1-30s, Schieberegler) + 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" + }, + chars = { + ["CharakterName - RealmName"] = { + level = 80, + maxDamage = 152000, + maxDamageSpell = "Feuerball", + maxHeal = 89000, + maxHealSpell = "Lichtblitz", + } + } +} +``` + +--- + +## 4. Logik- & Entscheidungsfluss (Core Engine) + +### 4.1. Combat Log Filterung (`COMBAT_LOG_EVENT_UNFILTERED`) + +Die Engine verwendet `CombatLogGetCurrentEventInfo()` mit folgenden Regeln: + +1. **Source-Check:** Nur Events mit `sourceGUID == UnitGUID("player")` werden verarbeitet (Spieler-Treffer). +2. **Sub-Events:** + - **Schaden:** `SWING_DAMAGE`, `SPELL_DAMAGE`, `RANGE_DAMAGE`, `SPELL_PERIODIC_DAMAGE`, `SPELL_BUILDING_DAMAGE`. + - Effektiver Schaden: `amount + absorbed` (Absorbe werden dem Treffer zugerechnet). + - Bedingung: `critical == true`. + - **Heilung:** `SPELL_HEAL`, `SPELL_PERIODIC_HEAL`, `SPELL_BUILDING_HEAL`. + - Effektive Heilung: `amount + overhealing` (Overheal wird berücksichtigt, damit große Heiler-Crits bei voller HP nicht fälschlich als "Low Crit" eingestuft werden). + - Bedingung: `critical == true`. + +### 4.2. Ablaufdiagramm der Crit-Verarbeitung + +```mermaid +flowchart TD + A["Combat Log Crit Event (Player)"] --> B{"Addon aktiviert?"} + B -- Nein --> End[Abbruch] + B -- Ja --> C["Hole bisherigen Rekord (Schaden/Heilung)"] + + C --> D{"amount > currentRecord?"} + D -- Ja --> E["Neuer Rekord! Aktualisiere charData (maxRecord = amount)"] + D -- Nein --> F{"enableLowCrit UND currentRecord > 0 UND amount <= (currentRecord * threshold)?"} + + F -- Ja --> G["Als Low-Crit markieren (Sound: low.mp3)"] + F -- Nein --> H["Als normaler Crit markieren (Sound: critX / healX)"] + + E --> I{"GetTime() - lastOutputTime < cooldown?"} + G --> I + H --> I + + I -- Ja (im Cooldown) --> J["Ausgabe unterdrückt (Anti-Spam)"] + I -- Nein (Cooldown frei) --> K["lastOutputTime = GetTime()"] + + K --> L["PlaySoundFile(soundPath, soundChannel)"] + K --> M["DEFAULT_CHAT_FRAME:AddMessage(formatierte Nachricht)"] +``` + +--- + +## 5. Kernfunktionen im Detail + +### 5.1. Level-Up Reset +- **Event:** `PLAYER_LEVEL_UP` +- **Sicherheits-Check:** Beim Login (`PLAYER_LOGIN`) wird geprüft, ob `UnitLevel("player") > charData.level`. Falls der Spieler offline oder bei deaktiviertem Addon aufgestiegen ist, wird der Reset nachträglich durchgeführt. +- **Wirkung:** `maxDamage = 0`, `maxHeal = 0`, `level = newLevel`. +- **Chat-Meldung:** Gibt lokalen Hinweis aus: `[CritTracker] Stufenaufstieg! Krit-Rekorde wurden auf 0 zurückgesetzt.` + +### 5.2. Cooldown für Audio- & Chat-Ausgabe +- Einstellbar von **1 bis 30 Sekunden** über den Schieberegler in den Optionen. +- **Wichtig für Agenten:** Der Cooldown blockiert **nicht** das Speichern neuer Rekorde! Trifft ein Spieler während des Cooldowns einen neuen Rekordwert, wird `charData.maxDamage` sofort aktualisiert. Lediglich die Soundwiedergabe und der Textchat werden gedrosselt, um Tonaussetzer und Chatüberflutung zu verhindern. + +### 5.3. Lokale Chat-Ausgabe +- Alle Chat-Meldungen verwenden `DEFAULT_CHAT_FRAME:AddMessage(msg)` oder `print(msg)`. +- Es werden **keine** öffentlichen Chatkanäle (`SAY`, `PARTY`, `RAID`) angesprochen. Die Ausgabe ist garantiert nur für den lokalen Client sichtbar. +- Abschaltbar über `CritTrackerDB.settings.enableChat`. + +### 5.4. Zweisprachigkeit (Localization Architecture) +- Lokalisierungs-Proxy via Lua-Metatable in `CritTracker.L`: + ```lua + CritTracker.L = setmetatable({}, { + __index = function(_, key) + local lang = CritTracker:GetActiveLanguage() + local locKey = (lang == "German") and "deDE" or "enUS" + return addonTable.Locales[locKey][key] or addonTable.Locales["enUS"][key] or key + end + }) + ``` +- **Sprachauflösung:** + - `auto`: Prüft `GetLocale()`. Bei `deDE` wird Deutsch gewählt, bei allen anderen Sprachen Englisch. + - `German`: Erzwingt Deutsch. + - `English`: Erzwingt Englisch. +- **Audio-Pfad:** + `Interface\AddOns\CritTracker\sounds\\.mp3` + +--- + +## 6. UI-Architektur (`Config.lua`) + +Das Einstellungsmenü ist für maximale Kompatibilität mit modernem Retail WoW (Dragonflight, War Within, Midnight / 12.x) ausgelegt: + +1. **Modern Settings API:** + - Registrierung über `Settings.RegisterCanvasLayoutCategory(panel, "CritTracker")` und `Settings.RegisterAddOnCategory(category)`. + - Öffnen über `Settings.OpenToCategory(category:GetID())`. +2. **Standalone-Fallback:** + - Sollte das Settings-Menü im Betastatus nicht ansprechbar sein oder während des Kampfes aufgerufen werden, öffnet sich das Panel automatisch als bewegliches Standalone-Fenster (`BackdropTemplateMixin`, Close-Button, Dragging). +3. **2-Spalten-Layout:** + - **Linke Spalte:** Checkboxen (Addon, Chat, Sound, Low-Crit), Cooldown-Slider (1–30s), Schwellenwert-Slider (5–100%), Charakter-Rekordanzeige und Reset-Button. + - **Rechte Spalte:** Sound-Auswahl (Schaden, Heilung, Low, Kanal), Sprachauswahl und Test-Buttons (`►`). + +--- + +## 7. Anleitung zur Nachbildung / Testen + +1. **Dateien anlegen:** Die Dateien `CritTracker.toc`, `Localization.lua`, `CritTracker.lua` und `Config.lua` wie spezifiziert erstellen. +2. **Sounds ablegen:** + - `sounds/German/`: `crit1.mp3` bis `crit4.mp3`, `heal1.mp3` bis `heal4.mp3`, `low.mp3`. + - `sounds/English/`: `crit1.mp3` bis `crit4.mp3`, `heal1.mp3` bis `heal4.mp3`, `low.mp3`. +3. **Im Spiel testen:** + - `/ctr` eingeben -> Einstellungsfenster muss sich öffnen. + - Auf die `►`-Buttons klicken -> Die jeweiligen MP3-Dateien müssen hörbar abgespielt werden. + - `/ctr test crit` -> Testet Schadensmeldung und Audio. + - `/ctr test heal` -> Testet Heilungsmeldung und Audio. + - `/ctr test low` -> Testet Low-Crit-Meldung und `low.mp3`. + - Schieberegler verstellen -> Werte in `CritTrackerDB.settings` müssen persistieren. + - `/ctr reset` -> Rekorde werden auf 0 gesetzt. diff --git a/Config.lua b/Config.lua new file mode 100644 index 0000000..5c9b61b --- /dev/null +++ b/Config.lua @@ -0,0 +1,488 @@ +--[[ + CritTracker - Configuration & Options UI + Supports Modern WoW 12.x / Retail Settings API and Standalone UI +]] + +local addonName, addonTable = ... +local CritTracker = addonTable.CritTracker + +local panel = CreateFrame("Frame", "CritTrackerOptionsPanel", UIParent) +CritTracker.optionsPanel = panel +panel.name = "CritTracker" + +-- Standalone Window Styling (Close Button & Dragging) +panel:SetSize(660, 560) +panel:SetMovable(true) +panel:EnableMouse(true) +panel:RegisterForDrag("LeftButton") +panel:SetScript("OnDragStart", panel.StartMoving) +panel:SetScript("OnDragStop", panel.StopMovingOrSizing) + +-- Close Button for Standalone Mode +local closeBtn = CreateFrame("Button", nil, panel, "UIPanelCloseButton") +closeBtn:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -6, -6) +closeBtn:SetScript("OnClick", function() + panel:Hide() +end) + +-- UI Element References for Language Updating +local uiLabels = {} + +local function RegisterLabel(fontString, locKey) + table.insert(uiLabels, { element = fontString, key = locKey, isText = true }) + fontString:SetText(CritTracker.L[locKey] or locKey) +end + +-- Controls State Update Helper +local function UpdateSegmentedButtons(buttons, selectedKey) + for key, btn in pairs(buttons) do + if key == selectedKey then + btn:LockHighlight() + btn:SetText("|cffffd100" .. (btn.displayLabel or key) .. "|r") + else + btn:UnlockHighlight() + btn:SetText(btn.displayLabel or key) + end + end +end + +-- Refresh Panel Displays +function CritTracker:UpdateOptionsDisplay() + if not self.db then return end + + -- Checkboxes + if self.ui_cbEnable then self.ui_cbEnable:SetChecked(self.db.enabled) end + if self.ui_cbChat then self.ui_cbChat:SetChecked(self.db.enableChat) end + if self.ui_cbSound then self.ui_cbSound:SetChecked(self.db.enableSound) end + if self.ui_cbLow then self.ui_cbLow:SetChecked(self.db.enableLowCrit) end + + -- Sliders + if self.ui_sliderCooldown then + self.ui_sliderCooldown:SetValue(self.db.cooldown or 5) + self.ui_lblCooldown:SetText(string.format(self.L["OPT_COOLDOWN_TITLE"], self.db.cooldown or 5)) + end + if self.ui_sliderThreshold then + self.ui_sliderThreshold:SetValue(self.db.lowThreshold or 30) + self.ui_lblThreshold:SetText(string.format(self.L["OPT_THRESHOLD_TITLE"], self.db.lowThreshold or 30)) + end + + -- Audio Buttons + if self.ui_damageButtons then + UpdateSegmentedButtons(self.ui_damageButtons, self.db.damageSound or "crit1") + end + if self.ui_healButtons then + UpdateSegmentedButtons(self.ui_healButtons, self.db.healSound or "heal1") + end + if self.ui_lowButtons then + UpdateSegmentedButtons(self.ui_lowButtons, self.db.lowSound or "low") + end + if self.ui_channelButtons then + UpdateSegmentedButtons(self.ui_channelButtons, self.db.soundChannel or "Master") + end + if self.ui_langButtons then + UpdateSegmentedButtons(self.ui_langButtons, self.db.language or "auto") + end + + -- Character Stats + local charData = self:GetCharData() + local dmgNum = self:FormatNumber(charData.maxDamage) + local dmgSpell = (charData.maxDamageSpell ~= "") and charData.maxDamageSpell or self.L["OPT_STATS_NONE"] + local healNum = self:FormatNumber(charData.maxHeal) + local healSpell = (charData.maxHealSpell ~= "") and charData.maxHealSpell or self.L["OPT_STATS_NONE"] + + if self.ui_lblDamageRecord then + self.ui_lblDamageRecord:SetText(string.format(self.L["OPT_STATS_DAMAGE"], "|cffff4444" .. dmgNum .. "|r", dmgSpell)) + end + if self.ui_lblHealRecord then + self.ui_lblHealRecord:SetText(string.format(self.L["OPT_STATS_HEAL"], "|cff44ff44" .. healNum .. "|r", healSpell)) + end + + -- Dynamic Language Labels + for _, item in ipairs(uiLabels) do + if item.isText and item.element and item.key then + item.element:SetText(self.L[item.key] or item.key) + end + end +end + +-- Initialize UI Components +function CritTracker:InitConfig() + -- Title & Header + local title = panel:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge") + title:SetPoint("TOPLEFT", panel, "TOPLEFT", 20, -18) + title:SetText("|cff00e5ffCritTracker|r") + + local subtitle = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") + subtitle:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -4) + subtitle:SetText(self.L["ADDON_SUBTITLE"] .. " |cff888888v1.0.0 (TOC 16001)|r") + + ------------------------------------------------------------------------- + -- LEFT COLUMN: General Options, Cooldown/Threshold Sliders, Character Stats + ------------------------------------------------------------------------- + local colLeftX = 20 + local curY = -70 + + -- Section: General + local secGeneral = panel:CreateFontString(nil, "ARTWORK", "GameFontNormal") + secGeneral:SetPoint("TOPLEFT", panel, "TOPLEFT", colLeftX, curY) + RegisterLabel(secGeneral, "OPT_SECTION_GENERAL") + curY = curY - 24 + + -- Checkbox: Enable Addon + local cbEnable = CreateFrame("CheckButton", "CritTrackerCB_Enable", panel, "UICheckButtonTemplate") + cbEnable:SetPoint("TOPLEFT", panel, "TOPLEFT", colLeftX, curY) + cbEnable.Text:SetText(self.L["OPT_ENABLE"]) + RegisterLabel(cbEnable.Text, "OPT_ENABLE") + cbEnable:SetScript("OnClick", function(selfBtn) + CritTracker.db.enabled = selfBtn:GetChecked() + end) + self.ui_cbEnable = cbEnable + curY = curY - 28 + + -- Checkbox: Enable Chat + local cbChat = CreateFrame("CheckButton", "CritTrackerCB_Chat", panel, "UICheckButtonTemplate") + cbChat:SetPoint("TOPLEFT", panel, "TOPLEFT", colLeftX, curY) + cbChat.Text:SetText(self.L["OPT_CHAT"]) + RegisterLabel(cbChat.Text, "OPT_CHAT") + cbChat:SetScript("OnClick", function(selfBtn) + CritTracker.db.enableChat = selfBtn:GetChecked() + end) + self.ui_cbChat = cbChat + curY = curY - 28 + + -- Checkbox: Enable Sound + local cbSound = CreateFrame("CheckButton", "CritTrackerCB_Sound", panel, "UICheckButtonTemplate") + cbSound:SetPoint("TOPLEFT", panel, "TOPLEFT", colLeftX, curY) + cbSound.Text:SetText(self.L["OPT_SOUND"]) + RegisterLabel(cbSound.Text, "OPT_SOUND") + cbSound:SetScript("OnClick", function(selfBtn) + CritTracker.db.enableSound = selfBtn:GetChecked() + end) + self.ui_cbSound = cbSound + curY = curY - 28 + + -- Checkbox: Enable Low Crit + local cbLow = CreateFrame("CheckButton", "CritTrackerCB_Low", panel, "UICheckButtonTemplate") + cbLow:SetPoint("TOPLEFT", panel, "TOPLEFT", colLeftX, curY) + cbLow.Text:SetText(self.L["OPT_LOW"]) + RegisterLabel(cbLow.Text, "OPT_LOW") + cbLow:SetScript("OnClick", function(selfBtn) + CritTracker.db.enableLowCrit = selfBtn:GetChecked() + end) + self.ui_cbLow = cbLow + curY = curY - 36 + + -- Section: Cooldown & Threshold + local secCooldown = panel:CreateFontString(nil, "ARTWORK", "GameFontNormal") + secCooldown:SetPoint("TOPLEFT", panel, "TOPLEFT", colLeftX, curY) + RegisterLabel(secCooldown, "OPT_SECTION_COOLDOWN_THRESHOLD") + curY = curY - 22 + + -- Slider: Cooldown (1 - 30 seconds) + local lblCooldown = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") + lblCooldown:SetPoint("TOPLEFT", panel, "TOPLEFT", colLeftX + 4, curY) + self.ui_lblCooldown = lblCooldown + curY = curY - 16 + + local sliderCooldown = CreateFrame("Slider", "CritTrackerSlider_Cooldown", panel, "OptionsSliderTemplate") + sliderCooldown:SetPoint("TOPLEFT", panel, "TOPLEFT", colLeftX + 6, curY) + sliderCooldown:SetWidth(270) + sliderCooldown:SetMinMaxValues(1, 30) + sliderCooldown:SetValueStep(1) + sliderCooldown:SetObeyStepOnDrag(true) + _G[sliderCooldown:GetName() .. "Low"]:SetText("1s") + _G[sliderCooldown:GetName() .. "High"]:SetText("30s") + _G[sliderCooldown:GetName() .. "Text"]:SetText("") + sliderCooldown:SetScript("OnValueChanged", function(selfSlider, val) + val = math.floor(val + 0.5) + CritTracker.db.cooldown = val + lblCooldown:SetText(string.format(CritTracker.L["OPT_COOLDOWN_TITLE"], val)) + end) + self.ui_sliderCooldown = sliderCooldown + curY = curY - 42 + + -- Slider: Low Threshold (5% - 100%, default 30%) + local lblThreshold = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") + lblThreshold:SetPoint("TOPLEFT", panel, "TOPLEFT", colLeftX + 4, curY) + self.ui_lblThreshold = lblThreshold + curY = curY - 16 + + local sliderThreshold = CreateFrame("Slider", "CritTrackerSlider_Threshold", panel, "OptionsSliderTemplate") + sliderThreshold:SetPoint("TOPLEFT", panel, "TOPLEFT", colLeftX + 6, curY) + sliderThreshold:SetWidth(270) + sliderThreshold:SetMinMaxValues(5, 100) + sliderThreshold:SetValueStep(5) + sliderThreshold:SetObeyStepOnDrag(true) + _G[sliderThreshold:GetName() .. "Low"]:SetText("5%") + _G[sliderThreshold:GetName() .. "High"]:SetText("100%") + _G[sliderThreshold:GetName() .. "Text"]:SetText("") + sliderThreshold:SetScript("OnValueChanged", function(selfSlider, val) + val = math.floor(val + 0.5) + CritTracker.db.lowThreshold = val + lblThreshold:SetText(string.format(CritTracker.L["OPT_THRESHOLD_TITLE"], val)) + end) + self.ui_sliderThreshold = sliderThreshold + curY = curY - 44 + + -- Section: Character Records + local secRecords = panel:CreateFontString(nil, "ARTWORK", "GameFontNormal") + secRecords:SetPoint("TOPLEFT", panel, "TOPLEFT", colLeftX, curY) + RegisterLabel(secRecords, "OPT_SECTION_RECORDS") + curY = curY - 20 + + -- Box for Records + local recordBox = CreateFrame("Frame", nil, panel) + recordBox:SetPoint("TOPLEFT", panel, "TOPLEFT", colLeftX, curY) + recordBox:SetSize(280, 80) + local bgTex = recordBox:CreateTexture(nil, "BACKGROUND") + bgTex:SetAllPoints() + bgTex:SetColorTexture(0, 0, 0, 0.35) + + local lblCharName = recordBox:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") + lblCharName:SetPoint("TOPLEFT", recordBox, "TOPLEFT", 8, -8) + lblCharName:SetText("|cff00e5ff" .. CritTracker:GetCharKey() .. "|r") + + local lblDmg = recordBox:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") + lblDmg:SetPoint("TOPLEFT", lblCharName, "BOTTOMLEFT", 0, -6) + self.ui_lblDamageRecord = lblDmg + + local lblHeal = recordBox:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") + lblHeal:SetPoint("TOPLEFT", lblDmg, "BOTTOMLEFT", 0, -4) + self.ui_lblHealRecord = lblHeal + + -- Reset Button + local resetBtn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") + resetBtn:SetPoint("TOPLEFT", recordBox, "BOTTOMLEFT", 0, -8) + resetBtn:SetSize(160, 24) + resetBtn:SetText(self.L["OPT_RESET_BUTTON"]) + RegisterLabel(resetBtn:GetFontString(), "OPT_RESET_BUTTON") + resetBtn:SetScript("OnClick", function() + CritTracker:ResetRecords() + end) + + ------------------------------------------------------------------------- + -- RIGHT COLUMN: Audio Selection, Test Buttons, Language + ------------------------------------------------------------------------- + local colRightX = 330 + local curRightY = -70 + + -- Section: Audio + local secAudio = panel:CreateFontString(nil, "ARTWORK", "GameFontNormal") + secAudio:SetPoint("TOPLEFT", panel, "TOPLEFT", colRightX, curRightY) + RegisterLabel(secAudio, "OPT_SECTION_AUDIO") + curRightY = curRightY - 24 + + -- Damage Sound Row + local lblDmgSound = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") + lblDmgSound:SetPoint("TOPLEFT", panel, "TOPLEFT", colRightX, curRightY) + RegisterLabel(lblDmgSound, "OPT_DAMAGE_SOUND") + curRightY = curRightY - 18 + + self.ui_damageButtons = {} + local dmgOptions = { "crit1", "crit2", "crit3", "crit4", "none" } + local btnX = colRightX + for _, opt in ipairs(dmgOptions) do + local btn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") + btn:SetPoint("TOPLEFT", panel, "TOPLEFT", btnX, curRightY) + btn:SetSize((opt == "none") and 42 or 46, 22) + btn.displayLabel = (opt == "none") and (self.L["OPT_SOUND_NONE"]) or opt + btn:SetText(btn.displayLabel) + btn:SetScript("OnClick", function() + CritTracker.db.damageSound = opt + UpdateSegmentedButtons(CritTracker.ui_damageButtons, opt) + end) + self.ui_damageButtons[opt] = btn + btnX = btnX + ((opt == "none") and 44 or 48) + end + + local testDmgBtn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") + testDmgBtn:SetPoint("TOPLEFT", panel, "TOPLEFT", btnX + 4, curRightY) + testDmgBtn:SetSize(52, 22) + testDmgBtn:SetText("►") + testDmgBtn:SetScript("OnClick", function() + CritTracker:PlaySound(CritTracker.db.damageSound or "crit1") + end) + curRightY = curRightY - 34 + + -- Heal Sound Row + local lblHealSound = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") + lblHealSound:SetPoint("TOPLEFT", panel, "TOPLEFT", colRightX, curRightY) + RegisterLabel(lblHealSound, "OPT_HEAL_SOUND") + curRightY = curRightY - 18 + + self.ui_healButtons = {} + local healOptions = { "heal1", "heal2", "heal3", "heal4", "none" } + btnX = colRightX + for _, opt in ipairs(healOptions) do + local btn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") + btn:SetPoint("TOPLEFT", panel, "TOPLEFT", btnX, curRightY) + btn:SetSize((opt == "none") and 42 or 46, 22) + btn.displayLabel = (opt == "none") and (self.L["OPT_SOUND_NONE"]) or opt + btn:SetText(btn.displayLabel) + btn:SetScript("OnClick", function() + CritTracker.db.healSound = opt + UpdateSegmentedButtons(CritTracker.ui_healButtons, opt) + end) + self.ui_healButtons[opt] = btn + btnX = btnX + ((opt == "none") and 44 or 48) + end + + local testHealBtn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") + testHealBtn:SetPoint("TOPLEFT", panel, "TOPLEFT", btnX + 4, curRightY) + testHealBtn:SetSize(52, 22) + testHealBtn:SetText("►") + testHealBtn:SetScript("OnClick", function() + CritTracker:PlaySound(CritTracker.db.healSound or "heal1") + end) + curRightY = curRightY - 34 + + -- Low Sound Row + local lblLowSound = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") + lblLowSound:SetPoint("TOPLEFT", panel, "TOPLEFT", colRightX, curRightY) + RegisterLabel(lblLowSound, "OPT_LOW_SOUND") + curRightY = curRightY - 18 + + self.ui_lowButtons = {} + local lowOptions = { "low", "none" } + btnX = colRightX + for _, opt in ipairs(lowOptions) do + local btn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") + btn:SetPoint("TOPLEFT", panel, "TOPLEFT", btnX, curRightY) + btn:SetSize((opt == "none") and 42 or 52, 22) + btn.displayLabel = (opt == "none") and (self.L["OPT_SOUND_NONE"]) or opt + btn:SetText(btn.displayLabel) + btn:SetScript("OnClick", function() + CritTracker.db.lowSound = opt + UpdateSegmentedButtons(CritTracker.ui_lowButtons, opt) + end) + self.ui_lowButtons[opt] = btn + btnX = btnX + ((opt == "none") and 44 or 54) + end + + local testLowBtn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") + testLowBtn:SetPoint("TOPLEFT", panel, "TOPLEFT", btnX + 4, curRightY) + testLowBtn:SetSize(52, 22) + testLowBtn:SetText("►") + testLowBtn:SetScript("OnClick", function() + CritTracker:PlaySound(CritTracker.db.lowSound or "low") + end) + curRightY = curRightY - 36 + + -- Sound Channel Row + local lblChannel = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") + lblChannel:SetPoint("TOPLEFT", panel, "TOPLEFT", colRightX, curRightY) + RegisterLabel(lblChannel, "OPT_SOUND_CHANNEL") + curRightY = curRightY - 18 + + self.ui_channelButtons = {} + local chanOptions = { "Master", "Dialog", "SFX" } + btnX = colRightX + for _, opt in ipairs(chanOptions) do + local btn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") + btn:SetPoint("TOPLEFT", panel, "TOPLEFT", btnX, curRightY) + btn:SetSize(62, 22) + btn.displayLabel = opt + btn:SetText(opt) + btn:SetScript("OnClick", function() + CritTracker.db.soundChannel = opt + UpdateSegmentedButtons(CritTracker.ui_channelButtons, opt) + end) + self.ui_channelButtons[opt] = btn + btnX = btnX + 66 + end + curRightY = curRightY - 38 + + -- Section: Language + local secLang = panel:CreateFontString(nil, "ARTWORK", "GameFontNormal") + secLang:SetPoint("TOPLEFT", panel, "TOPLEFT", colRightX, curRightY) + RegisterLabel(secLang, "OPT_SECTION_LANGUAGE") + curRightY = curRightY - 22 + + local lblLang = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") + lblLang:SetPoint("TOPLEFT", panel, "TOPLEFT", colRightX, curRightY) + RegisterLabel(lblLang, "OPT_LANGUAGE") + curRightY = curRightY - 18 + + self.ui_langButtons = {} + local langOptions = { + { id = "auto", label = "OPT_LANG_AUTO", width = 85 }, + { id = "German", label = "OPT_LANG_DE", width = 85 }, + { id = "English", label = "OPT_LANG_EN", width = 85 }, + } + btnX = colRightX + for _, item in ipairs(langOptions) do + local btn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") + btn:SetPoint("TOPLEFT", panel, "TOPLEFT", btnX, curRightY) + btn:SetSize(item.width, 22) + btn.displayLabel = self.L[item.label] or item.id + btn:SetText(btn.displayLabel) + btn:SetScript("OnClick", function() + CritTracker.db.language = item.id + CritTracker:UpdateOptionsDisplay() + end) + self.ui_langButtons[item.id] = btn + btnX = btnX + item.width + 4 + end + + -- Initial display refresh + panel:SetScript("OnShow", function() + CritTracker:UpdateOptionsDisplay() + end) + + -- Register with Modern Settings API if available + if Settings and Settings.RegisterCanvasLayoutCategory then + local category = Settings.RegisterCanvasLayoutCategory(panel, "CritTracker") + Settings.RegisterAddOnCategory(category) + CritTracker.settingsCategory = category + elseif InterfaceOptions_AddCategory then + InterfaceOptions_AddCategory(panel) + end +end + +-- Open Options Method +function CritTracker:OpenOptions() + if InCombatLockdown and InCombatLockdown() then + self:Print("|cffff4444[CritTracker]|r Einstellungen können während des Kampfes nicht geöffnet werden.") + return + end + + local opened = false + if Settings and Settings.OpenToCategory and self.settingsCategory then + local ok = pcall(function() + Settings.OpenToCategory(self.settingsCategory:GetID()) + end) + if ok then + opened = true + end + end + + if not opened and InterfaceOptionsFrame_OpenToCategory and panel then + local ok = pcall(function() + InterfaceOptionsFrame_OpenToCategory(panel) + end) + if ok then + opened = true + end + end + + -- Fallback to Standalone Movable Dialog Window + if not opened and panel then + if panel:GetParent() ~= UIParent then + panel:SetParent(UIParent) + end + if not panel.hasBackdrop and BackdropTemplateMixin then + Mixin(panel, BackdropTemplateMixin) + panel:SetBackdrop({ + bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark", + edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", + tile = true, tileSize = 32, edgeSize = 32, + insets = { left = 8, right = 8, top = 8, bottom = 8 }, + }) + panel.hasBackdrop = true + end + panel:ClearAllPoints() + panel:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + panel:Show() + self:UpdateOptionsDisplay() + end +end diff --git a/CritTracker.lua b/CritTracker.lua new file mode 100644 index 0000000..85d4788 --- /dev/null +++ b/CritTracker.lua @@ -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 diff --git a/CritTracker.toc b/CritTracker.toc new file mode 100644 index 0000000..c58be37 --- /dev/null +++ b/CritTracker.toc @@ -0,0 +1,13 @@ +## Interface: 16001 +## Title: CritTracker +## Title-deDE: CritTracker +## Notes: Tracks critical strikes and heals with sound alerts and chat announcements. +## Notes-deDE: Zeichnet kritische Treffer und Heilungen getrennt auf, mit Sound- und Chat-Benachrichtigungen. +## Author: Sarah +## Version: 1.0.0 +## SavedVariables: CritTrackerDB +## IconTexture: Interface\Icons\Spell_Nature_BloodLust + +Localization.lua +CritTracker.lua +Config.lua diff --git a/Localization.lua b/Localization.lua new file mode 100644 index 0000000..c3ff511 --- /dev/null +++ b/Localization.lua @@ -0,0 +1,157 @@ +--[[ + CritTracker Localization + Supports German (deDE) and English (default / enUS) +]] + +local _, addonTable = ... + +local L_enUS = { + -- General / Branding + ["ADDON_TITLE"] = "CritTracker", + ["ADDON_SUBTITLE"] = "World of Warcraft - Forever (12.x Beta)", + ["MELEE_ATTACK"] = "Melee Attack", + ["UNKNOWN"] = "Unknown", + ["TARGET_ON"] = "on %s", + + -- Chat Announcements (Player only) + ["NEW_RECORD_DAMAGE"] = "|cff00e5ff[CritTracker]|r |cffffd100★ NEW DAMAGE RECORD! ★|r %s with %s: |cffff4444%s|r!", + ["NEW_RECORD_HEAL"] = "|cff00e5ff[CritTracker]|r |cffffd100★ NEW HEAL RECORD! ★|r %s with %s: |cff44ff44%s|r!", + ["NORMAL_CRIT_DAMAGE"] = "|cff00e5ff[CritTracker]|r |cffff8800Critical Strike:|r %s with %s: |cffffffff%s|r", + ["NORMAL_CRIT_HEAL"] = "|cff00e5ff[CritTracker]|r |cff00ff88Critical Heal:|r %s with %s: |cffffffff%s|r", + ["LOW_CRIT_DAMAGE"] = "|cff00e5ff[CritTracker]|r |cff999999Low Crit (Damage):|r %s with %s: |cffff8888%s|r (|cffaaaaaa<= %d%% of record %s|r)", + ["LOW_CRIT_HEAL"] = "|cff00e5ff[CritTracker]|r |cff999999Low Crit (Heal):|r %s with %s: |cff88ff88%s|r (|cffaaaaaa<= %d%% of record %s|r)", + ["LEVEL_UP_RESET"] = "|cff00e5ff[CritTracker]|r Level Up (%d)! Crit records have been reset to 0.", + ["RESET_SUCCESS"] = "|cff00e5ff[CritTracker]|r Crit records for %s have been reset to 0.", + ["CURRENT_STATS"] = "|cff00e5ff[CritTracker]|r Current Records for %s (Level %d):\n - Damage: |cffff4444%s|r (%s)\n - Healing: |cff44ff44%s|r (%s)", + + -- Options UI + ["OPT_SECTION_GENERAL"] = "General Options", + ["OPT_ENABLE"] = "Enable Addon", + ["OPT_ENABLE_DESC"] = "Enable or disable CritTracker completely.", + ["OPT_CHAT"] = "Enable Chat Output", + ["OPT_CHAT_DESC"] = "Show critical strikes and heals in your chat window (visible to you only).", + ["OPT_SOUND"] = "Enable Audio Alerts", + ["OPT_SOUND_DESC"] = "Play audio cues for critical strikes, heals, and low crits.", + ["OPT_LOW"] = "Enable Low Crit Alerts", + ["OPT_LOW_DESC"] = "Trigger special alert and sound when a critical hit/heal falls below your threshold.", + + ["OPT_SECTION_COOLDOWN_THRESHOLD"] = "Cooldown & Threshold", + ["OPT_COOLDOWN_TITLE"] = "Alert Cooldown: %d sec.", + ["OPT_COOLDOWN_DESC"] = "Minimum time between chat and audio announcements (1-30 seconds). Prevents audio overlapping and chat spam.", + ["OPT_THRESHOLD_TITLE"] = "Low Crit Threshold: %d%%", + ["OPT_THRESHOLD_DESC"] = "Percentage of your current record. Crits at or below this value are considered low crits (Default: 30%%).", + + ["OPT_SECTION_AUDIO"] = "Audio Settings", + ["OPT_DAMAGE_SOUND"] = "Damage Crit Sound:", + ["OPT_HEAL_SOUND"] = "Heal Crit Sound:", + ["OPT_LOW_SOUND"] = "Low Crit Sound:", + ["OPT_SOUND_CHANNEL"] = "Sound Channel:", + ["OPT_CHANNEL_MASTER"] = "Master", + ["OPT_CHANNEL_DIALOG"] = "Dialog", + ["OPT_CHANNEL_SFX"] = "SFX", + ["OPT_SOUND_NONE"] = "None", + + ["OPT_SECTION_LANGUAGE"] = "Language / Sprache", + ["OPT_LANGUAGE"] = "Voice & Text Language:", + ["OPT_LANG_AUTO"] = "Auto (Client)", + ["OPT_LANG_DE"] = "German (Deutsch)", + ["OPT_LANG_EN"] = "English", + + ["OPT_TEST_BUTTON"] = "Play Test", + ["OPT_TEST_DAMAGE"] = "Test Damage", + ["OPT_TEST_HEAL"] = "Test Heal", + ["OPT_TEST_LOW"] = "Test Low", + + ["OPT_SECTION_RECORDS"] = "Character Records", + ["OPT_STATS_HEADER"] = "Current character records:", + ["OPT_STATS_DAMAGE"] = "Highest Damage: %s (%s)", + ["OPT_STATS_HEAL"] = "Highest Heal: %s (%s)", + ["OPT_STATS_NONE"] = "None recorded yet", + ["OPT_RESET_BUTTON"] = "Reset Records", + ["OPT_RESET_TOOLTIP"] = "Reset highest damage and healing records for this character to 0.", + + -- Slash Command Help + ["SLASH_HELP_HEADER"] = "|cff00e5ffCritTracker Commands:|r", + ["SLASH_HELP_CONFIG"] = " |cffffd100/ctr|r or |cffffd100/crittracker|r - Open settings panel", + ["SLASH_HELP_STATS"] = " |cffffd100/ctr stats|r - Show current records", + ["SLASH_HELP_RESET"] = " |cffffd100/ctr reset|r - Reset current character records", + ["SLASH_HELP_TEST"] = " |cffffd100/ctr test [crit|heal|low]|r - Test sound and chat alert", +} + +local L_deDE = { + -- General / Branding + ["ADDON_TITLE"] = "CritTracker", + ["ADDON_SUBTITLE"] = "World of Warcraft - Forever (12.x Beta)", + ["MELEE_ATTACK"] = "Nahkampfangriff", + ["UNKNOWN"] = "Unbekannt", + ["TARGET_ON"] = "an %s", + + -- Chat Announcements (Player only) + ["NEW_RECORD_DAMAGE"] = "|cff00e5ff[CritTracker]|r |cffffd100★ NEUER SCHADENS-REKORD! ★|r %s mit %s: |cffff4444%s|r!", + ["NEW_RECORD_HEAL"] = "|cff00e5ff[CritTracker]|r |cffffd100★ NEUER HEILUNGS-REKORD! ★|r %s mit %s: |cff44ff44%s|r!", + ["NORMAL_CRIT_DAMAGE"] = "|cff00e5ff[CritTracker]|r |cffff8800Kritischer Treffer:|r %s mit %s: |cffffffff%s|r", + ["NORMAL_CRIT_HEAL"] = "|cff00e5ff[CritTracker]|r |cff00ff88Kritische Heilung:|r %s mit %s: |cffffffff%s|r", + ["LOW_CRIT_DAMAGE"] = "|cff00e5ff[CritTracker]|r |cff999999Schwacher Krit (Schaden):|r %s mit %s: |cffff8888%s|r (|cffaaaaaa<= %d%% von Rekord %s|r)", + ["LOW_CRIT_HEAL"] = "|cff00e5ff[CritTracker]|r |cff999999Schwache kritische Heilung:|r %s mit %s: |cff88ff88%s|r (|cffaaaaaa<= %d%% von Rekord %s|r)", + ["LEVEL_UP_RESET"] = "|cff00e5ff[CritTracker]|r Stufenaufstieg (%d)! Krit-Rekorde wurden auf 0 zurückgesetzt.", + ["RESET_SUCCESS"] = "|cff00e5ff[CritTracker]|r Krit-Rekorde für %s wurden auf 0 zurückgesetzt.", + ["CURRENT_STATS"] = "|cff00e5ff[CritTracker]|r Aktuelle Rekorde für %s (Stufe %d):\n - Schaden: |cffff4444%s|r (%s)\n - Heilung: |cff44ff44%s|r (%s)", + + -- Options UI + ["OPT_SECTION_GENERAL"] = "Allgemeine Optionen", + ["OPT_ENABLE"] = "Addon aktivieren", + ["OPT_ENABLE_DESC"] = "Aktiviert oder deaktiviert CritTracker vollständig.", + ["OPT_CHAT"] = "Chat-Ausgabe aktivieren", + ["OPT_CHAT_DESC"] = "Zeigt kritische Treffer und Heilungen im Chat an (nur für dich sichtbar).", + ["OPT_SOUND"] = "Sound-Ausgabe aktivieren", + ["OPT_SOUND_DESC"] = "Spielt Sounds bei kritischen Treffern, Heilungen und schwachen Crits ab.", + ["OPT_LOW"] = "Schwache Crits erkennen & melden", + ["OPT_LOW_DESC"] = "Meldet kritische Treffer oder Heilungen, die unter dem Grenzwert deines Rekords liegen.", + + ["OPT_SECTION_COOLDOWN_THRESHOLD"] = "Cooldown & Grenzwert", + ["OPT_COOLDOWN_TITLE"] = "Ausgabe-Cooldown: %d Sek.", + ["OPT_COOLDOWN_DESC"] = "Mindestabstand zwischen Chat- und Sound-Ausgaben (1-30 Sekunden). Verhindert Audio-Überlagerungen und Chat-Spam bei schnellen Crits.", + ["OPT_THRESHOLD_TITLE"] = "Grenzwert für schwache Crits: %d%%", + ["OPT_THRESHOLD_DESC"] = "Prozentsatz deines aktuellen Rekords. Ein Krit bei oder unter diesem Wert gilt als schwacher Krit (Standard: 30%%).", + + ["OPT_SECTION_AUDIO"] = "Audio-Einstellungen", + ["OPT_DAMAGE_SOUND"] = "Sound für Schaden-Crits:", + ["OPT_HEAL_SOUND"] = "Sound für Heilung-Crits:", + ["OPT_LOW_SOUND"] = "Sound für schwache Crits:", + ["OPT_SOUND_CHANNEL"] = "Sound-Kanal:", + ["OPT_CHANNEL_MASTER"] = "Master", + ["OPT_CHANNEL_DIALOG"] = "Dialog", + ["OPT_CHANNEL_SFX"] = "SFX", + ["OPT_SOUND_NONE"] = "Aus", + + ["OPT_SECTION_LANGUAGE"] = "Sprache / Language", + ["OPT_LANGUAGE"] = "Audio- & Textsprache:", + ["OPT_LANG_AUTO"] = "Automatisch (Client)", + ["OPT_LANG_DE"] = "Deutsch (German)", + ["OPT_LANG_EN"] = "English", + + ["OPT_TEST_BUTTON"] = "Test abspielen", + ["OPT_TEST_DAMAGE"] = "Test Schaden", + ["OPT_TEST_HEAL"] = "Test Heilung", + ["OPT_TEST_LOW"] = "Test Low", + + ["OPT_SECTION_RECORDS"] = "Charakter-Rekorde", + ["OPT_STATS_HEADER"] = "Aktuelle Rekorde dieses Charakters:", + ["OPT_STATS_DAMAGE"] = "Höchster Schaden: %s (%s)", + ["OPT_STATS_HEAL"] = "Höchste Heilung: %s (%s)", + ["OPT_STATS_NONE"] = "Noch keine Rekorde erfasst", + ["OPT_RESET_BUTTON"] = "Rekorde zurücksetzen", + ["OPT_RESET_TOOLTIP"] = "Setzt den höchsten Schaden- und Heilungswert dieses Charakters auf 0 zurück.", + + -- Slash Command Help + ["SLASH_HELP_HEADER"] = "|cff00e5ffCritTracker Befehle:|r", + ["SLASH_HELP_CONFIG"] = " |cffffd100/ctr|r oder |cffffd100/crittracker|r - Einstellungsmenü öffnen", + ["SLASH_HELP_STATS"] = " |cffffd100/ctr stats|r - Aktuelle Rekorde anzeigen", + ["SLASH_HELP_RESET"] = " |cffffd100/ctr reset|r - Rekorde dieses Charakters auf 0 zurücksetzen", + ["SLASH_HELP_TEST"] = " |cffffd100/ctr test [crit|heal|low]|r - Test-Sound und Chat-Meldung auslösen", +} + +addonTable.Locales = { + ["enUS"] = L_enUS, + ["deDE"] = L_deDE, +} diff --git a/README.md b/README.md index a1d6630..2a6efec 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,46 @@ # CritTracker +CritTracker ist ein World of Warcraft Addon für **World of Warcraft - Forever (12.x Beta)** (TOC Interface `16001`), das kritische Treffer und Heilungen getrennt erfasst und mit anpassbarem Audio- und lokalem Chat-Feedback versieht. + +--- + +## Features + +- **Getrennte Erfassung für Schaden & Heilung:** + - Protokolliert höchste kritische Treffer (Rekorde) für Schaden und Heilung getrennt nach Zauber/Angriff. + - Nur für den Spieler sichtbare Chat-Ausgabe (`AddMessage`), die in den Einstellungen vollständig deaktiviert werden kann. +- **Erkennung schwacher kritischer Treffer ("Low Crits"):** + - Einstellbarer Schwellenwert (standardmäßig **30%** des aktuellen Rekords). + - Spielt bei schwachen Crits die Sound-Datei `low.mp3` ab und gibt eine dezent gefärbte Nachricht aus. +- **Sicherheits-Mechanismen:** + - **Level-Up Reset:** Bei jedem Stufenaufstieg (`PLAYER_LEVEL_UP`) sowie beim ersten Login nach einem Level-Up werden die Rekorde dieses Charakters automatisch auf 0 zurückgesetzt. + - **Ausgabe-Cooldown:** Ein Schieberegler (1–30 Sekunden, Standard 5s) verhindert Audio-Überlagerungen und Chat-Spam bei schnellen aufeinanderfolgenden Crits. + - **Charakterspezifische Datenspeicherung:** Rekorde werden getrennt nach Charakter & Realm gespeichert, sodass Twinks nicht gegen Endgame-Zahlen antreten. +- **Audio-Feedback:** + - Schadens-Crits: Auswahl zwischen `crit1` bis `crit4` (oder deaktiviert). + - Heilungs-Crits: Auswahl zwischen `heal1` bis `heal4` (oder deaktiviert). + - Schwache Crits: `low.mp3` (oder deaktiviert). + - Wählbarer Sound-Kanal (`Master`, `Dialog`, `SFX`). +- **Zweisprachigkeit:** + - Deutsche Clients (`deDE`): Deutsche Benutzeroberfläche und deutsche Sounds (`sounds/German/`). + - Alle anderen Clients: Englische Benutzeroberfläche und englische Sounds (`sounds/English/`). + - Optionale manuelle Sprachauswahl in den Einstellungen. + +--- + +## Befehle (Slash Commands) + +- `/ctr` oder `/crittracker` – Öffnet das Einstellungsmenü. +- `/ctr stats` – Zeigt die aktuellen Rekorde des Charakters im Chat an. +- `/ctr reset` – Setzt die Rekorde des aktuellen Charakters manuell auf 0 zurück. +- `/ctr test [crit|heal|low]` – Löst einen Test-Sound und eine Chat-Nachricht aus. + - `/ctr test crit` (Schadens-Crit Test) + - `/ctr test heal` (Heilungs-Crit Test) + - `/ctr test low` (Schwacher Treffer Test) + +--- + +## Installation + +Kopiere den Ordner `CritTracker` in dein Addon-Verzeichnis: +`World of Warcraft\_retail_\Interface\AddOns\CritTracker` diff --git a/sounds/English/low.mp3 b/sounds/English/low.mp3 index 06d7405..6f0e69f 100644 Binary files a/sounds/English/low.mp3 and b/sounds/English/low.mp3 differ