From 17797efcd7b4ee04345ff282a604318e3e1ee698 Mon Sep 17 00:00:00 2001 From: Mateo Date: Mon, 2 Feb 2026 11:24:25 -0300 Subject: [PATCH 1/3] ArenaStats TBC + Improvements **New Features/Improvements:** - **Map IDs:** Added correct Zone IDs for TBC Arenas (Nagrand, Blade's Edge and Ruins of Lordaeron). - **Icons:** Fixed a bug where icons where showing a "green square" instead of the class/spec icon. - **Minimap Button:** Fixed a bug where right clicking minimap panel (options menu) didnt worked and resulted in a error. - **Match Details:** New feature, now when you click on a match a pop-up window opens showing in-depth stats for that match (damage done, healing done, team name, etc). - **Delete Match:** New feature, now you can delete a specific match from the log with a button next to it. - **Test Data:** New feature, added a "Generate Test Data" button inside the options window, so you can see how it looks without the need to play a arena match. - **Live Updating:** Now every feature (Generate Test Data, Purge Database, Delete Match Button) will do what it should without the need to close and re-open the ArenaStats window to see the changes. - **Visual Change:** Added a more opaque window (85% alpha). --- ArenaStats.lua | 1580 ++++++++++++++++++++++++-------------------- ArenaStats.toc | 33 +- ArenaStats_TBC.toc | 33 +- GUI.lua | 1149 ++++++++++++++++---------------- MatchDetails.lua | 223 +++++++ enUS.lua | 49 ++ options.lua | 264 ++++---- 7 files changed, 1917 insertions(+), 1414 deletions(-) create mode 100644 MatchDetails.lua create mode 100644 enUS.lua diff --git a/ArenaStats.lua b/ArenaStats.lua index 674cd3b..7df2ff2 100644 --- a/ArenaStats.lua +++ b/ArenaStats.lua @@ -1,703 +1,877 @@ -local addonName = "ArenaStats" -local addonTitle = select(2, (C_AddOns and C_AddOns.GetAddOnInfo or GetAddOnInfo)(addonName)) -local ArenaStats = _G.LibStub("AceAddon-3.0"):NewAddon(addonName, - "AceConsole-3.0", - "AceEvent-3.0") -local L = _G.LibStub("AceLocale-3.0"):GetLocale(addonName, true) -local libDBIcon = _G.LibStub("LibDBIcon-1.0") -local LibRaces = _G.LibStub("LibRaces-1.0") -local IsActiveBattlefieldArena = IsActiveBattlefieldArena -local GetBattlefieldStatus, GetBattlefieldTeamInfo, GetNumBattlefieldScores, - GetBattlefieldScore, GetBattlefieldWinner, IsArenaSkirmish, IsInInstance, - GetInstanceInfo = GetBattlefieldStatus, GetBattlefieldTeamInfo, - GetNumBattlefieldScores, GetBattlefieldScore, - GetBattlefieldWinner, IsArenaSkirmish, IsInInstance, - GetInstanceInfo -local UnitName, UnitRace, UnitClass, UnitGUID, UnitFactionGroup, UnitIsPlayer = - UnitName, UnitRace, UnitClass, UnitGUID, UnitFactionGroup, UnitIsPlayer - -function ArenaStats:OnInitialize() - self.db = _G.LibStub("AceDB-3.0"):New(addonName, { - profile = { - minimapButton = {hide = false}, -- Note: LibDBIcon requires this format - maxHistory = 0, - showCharacterNamesOnHover = true, - showSpec = true - }, - char = {history = {}} - }) - - self:Print("Tracking ready, have a nice session!") - self.specSpells = self:GetSpecSpells(); - - self:RegisterEvent("UPDATE_BATTLEFIELD_STATUS") - self:RegisterEvent("UPDATE_BATTLEFIELD_SCORE") - self:RegisterEvent("ZONE_CHANGED_NEW_AREA") - - self:RegisterEvent("ARENA_OPPONENT_UPDATE") - self:RegisterEvent("UNIT_AURA") - self:RegisterEvent("UNIT_SPELLCAST_START") - self:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START") - self:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED") - - self:DrawMinimapIcon() - self:RegisterOptionsTable() - - self.specTable = {} - self.arenaEnded = false - self.current = { status = "none", stats = {}, units = {} } - - -- Cache for BuildTable to avoid rebuilding on every GUI refresh - self.tableCache = nil - self.tableCacheSize = 0 - - self:Reset() -end - -function ArenaStats:OnDisable() - self:UnregisterAllEvents() - if _G.AsFrame then - _G.AsFrame:Hide() - end -end - -function ArenaStats:OnSpecDetected(unitName, spec) - local existingPlayer = self.specTable[unitName] - - if existingPlayer then - return - end - - self.specTable[unitName] = spec -end - -function ArenaStats:ScanUnitBuffs(unit) - if not unit then return end - - for n = 1, 30 do - local name, spellID, unitCaster = self:GetUnitBuff(unit, n) - - if not name then - break - end - - if self.specSpells[spellID] then - -- For TBC, we can't reliably get unitCaster, so we assume self-buffs - if self.isTBC then - local casterName = GetUnitName(unit, true) - if casterName then - self:OnSpecDetected(casterName, self.specSpells[spellID]) - end - elseif unitCaster then - local unitPet = string.gsub(unit, "%d$", "pet%1") - if UnitIsUnit(unit, unitCaster) or UnitIsUnit(unitPet, unitCaster) then - local casterName = GetUnitName(unitCaster, true) - if casterName then - self:OnSpecDetected(casterName, self.specSpells[spellID]) - end - end - end - end - end -end - -function ArenaStats:ARENA_OPPONENT_UPDATE(_, unit, updateReason) - self:ScanUnitBuffs(unit) -end - -function ArenaStats:UNIT_AURA(_, unit, isFullUpdate, updatedAuras) - self:ScanUnitBuffs(unit) -end - -function ArenaStats:UNIT_SPELLCAST_START(_, unit, castGUID, spellID) - local spellName = spellID and GetSpellInfo(spellID) or nil - - if unit then - self:ScanUnitBuffs(unit) - end - - if spellID and self.specSpells[spellID] and unit then - local name = GetUnitName(unit, true) - if name then - self:OnSpecDetected(name, self.specSpells[spellID]) - end - end -end - -function ArenaStats:UNIT_SPELLCAST_CHANNEL_START(_, unit, castGuid, spellId) - if unit then - self:ScanUnitBuffs(unit) - end - - if spellId and self.specSpells[spellId] and unit then - local name = GetUnitName(unit, true) - if name then - self:OnSpecDetected(name, self.specSpells[spellId]) - end - end -end - -function ArenaStats:UNIT_SPELLCAST_SUCCEEDED(_, unit, castGuid, spellId) - if unit then - self:ScanUnitBuffs(unit) - end - - if spellId and self.specSpells[spellId] and unit then - local name = GetUnitName(unit, true) - if name then - self:OnSpecDetected(name, self.specSpells[spellId]) - end - end -end - -function ArenaStats:ZONE_CHANGED_NEW_AREA() - local _, instanceType = IsInInstance() - if (instanceType == "arena") then - self.arenaEnded = false - self.specTable = {} - end -end - -function ArenaStats:UPDATE_BATTLEFIELD_STATUS(_, index) - local status, mapName, instanceID, levelRangeMin, levelRangeMax, teamSize, - isRankedArena, suspendedQueue, bool, queueType = - GetBattlefieldStatus(index) - if (status == "active" and teamSize > 0 and IsActiveBattlefieldArena()) then - self.current["status"] = status - self.current["stats"]["teamSize"] = teamSize - self.current["stats"]["isRanked"] = not IsArenaSkirmish() - if (self.current["stats"]["startTime"] == nil or - self.current["stats"]["startTime"] == '') then - self.current["stats"]["startTime"] = _G.time() - end - end -end - -function ArenaStats:GetSpecOrDefault(unitName) - if not unitName then - return "Unknown" - end - - local detectedSpec = self.specTable[unitName] - - if detectedSpec then - return detectedSpec - end - - return "Unknown" -end - ---- Collects and stores all arena ranking data at the end of a match. ---- This function is called when the battlefield score is updated and a winner is determined. ---- ---- The function performs three main tasks: ---- 1. Identifies which team (GREEN=0 or GOLD=1) the player belongs to by scanning scores ---- 2. Retrieves team ratings and MMR for both teams from GetBattlefieldTeamInfo ---- 3. Collects individual player data (class, name, race, spec) for both teams ---- ---- Data is stored in self.current["stats"] with 0-based indexing for player arrays ---- to maintain compatibility with the existing data format. -function ArenaStats:SetLastArenaRankingData() - local playerTeam = '' - local greenTeam = {} - local goldTeam = {} - local myName = UnitName("player") - local numScores = GetNumBattlefieldScores() - - -- Step 1: Scan all players to determine which team we're on and group players by team - -- GREEN team has teamIndex=0, GOLD team has teamIndex=1 - for i = 1, numScores do - local data = { GetBattlefieldScore(i) } - local teamIndex = data[6] - - -- Check if this player is us to determine our team color - if data[1] == myName then - playerTeam = (teamIndex == 0) and 'GREEN' or 'GOLD' - end - - -- Group players into their respective teams - if teamIndex == 0 then - table.insert(greenTeam, data) - else - table.insert(goldTeam, data) - end - end - - self.current["stats"]["teamColor"] = playerTeam - - -- Step 2: Get team ratings and MMR from both teams - -- Team index 0 = GREEN, Team index 1 = GOLD - for i = 0, 1 do - local teamName, oldTeamRating, newTeamRating, teamMMR = - GetBattlefieldTeamInfo(i) - if teamMMR > 0 then - local isPlayerTeam = (i == 0 and playerTeam == 'GREEN') or - (i == 1 and playerTeam == 'GOLD') - if isPlayerTeam then - self.current["stats"]["teamName"] = teamName - self.current["stats"]["oldTeamRating"] = oldTeamRating - self.current["stats"]["newTeamRating"] = newTeamRating - self.current["stats"]["diffRating"] = newTeamRating - oldTeamRating - self.current["stats"]["mmr"] = teamMMR - else - self.current["stats"]["enemyTeamName"] = teamName - self.current["stats"]["enemyOldTeamRating"] = oldTeamRating - self.current["stats"]["enemyNewTeamRating"] = newTeamRating - self.current["stats"]["enemyDiffRating"] = newTeamRating - oldTeamRating - self.current["stats"]["enemyMmr"] = teamMMR - end - end - end - - -- Step 3: Initialize player data arrays and collect individual player info - self.current["stats"]["teamClass"] = {} - self.current["stats"]["teamCharName"] = {} - self.current["stats"]["teamRace"] = {} - self.current["stats"]["teamSpec"] = {} - - self.current["stats"]["enemyClass"] = {} - self.current["stats"]["enemyName"] = {} - self.current["stats"]["enemyRace"] = {} - self.current["stats"]["enemySpec"] = {} - - -- Determine which raw data table corresponds to player's team vs enemy team - local playerTeamTable = (playerTeam == 'GREEN') and greenTeam or goldTeam - local enemyTeamTable = (playerTeam == 'GREEN') and goldTeam or greenTeam - - -- Helper to convert localized race name to uppercase race token - local function convertRace(raceInput) - if not raceInput then return '' end - local race = LibRaces:GetRaceToken(raceInput) - return race and race:upper() or '' - end - - -- GetBattlefieldScore returns: - -- [1]=playerName, [2]=killingBlows, [3]=honorKills, [4]=deaths, [5]=honorGained, - -- [6]=faction, [7]=rank, [8]=race, [9]=class, [10]=classToken, [11]=damageDone, [12]=healingDone - - -- Collect player's team data (0-based indexing for storage) - for i = 1, #playerTeamTable do - local row = playerTeamTable[i] - local raceUpper = convertRace(row[8]) - local idx = i - 1 -- Convert to 0-based index - - self.current["stats"]["teamClass"][idx] = row[10] and row[10]:upper() or '' - self.current["stats"]["teamCharName"][idx] = row[1] - self.current["stats"]["teamRace"][idx] = raceUpper - self.current["stats"]["teamSpec"][idx] = self:GetSpecOrDefault(row[1]) - end - - -- Collect enemy team data (0-based indexing for storage) - for i = 1, #enemyTeamTable do - local row = enemyTeamTable[i] - local raceUpper = convertRace(row[8]) - local idx = i - 1 -- Convert to 0-based index - - self.current["stats"]["enemyClass"][idx] = row[10] and row[10]:upper() or '' - self.current["stats"]["enemyName"][idx] = row[1] - self.current["stats"]["enemyRace"][idx] = raceUpper - self.current["stats"]["enemyFaction"] = self:RaceToFaction(raceUpper) - self.current["stats"]["enemySpec"][idx] = self:GetSpecOrDefault(row[1]) - end -end - --- Alliance races lookup table -local ALLIANCE_RACES = { - HUMAN = true, GNOME = true, NIGHTELF = true, - DRAENEI = true, DWARF = true, WORGEN = true -} - -function ArenaStats:RaceToFaction(race) - return ALLIANCE_RACES[race] and 1 or 0 -end - -function ArenaStats:UPDATE_BATTLEFIELD_SCORE() - local battlefieldWinner = GetBattlefieldWinner() - if battlefieldWinner == nil or self.arenaEnded then return end - - if self.current.status ~= 'none' then - self.current["stats"]["zoneId"] = select(8, GetInstanceInfo()) - self.current["stats"]["endTime"] = _G.time() - self.arenaEnded = true - if (battlefieldWinner == 0) then - self.current["stats"]["winnerColor"] = "GREEN"; - elseif (battlefieldWinner == 1) then - self.current["stats"]["winnerColor"] = "GOLD"; - end - self:SetLastArenaRankingData() - if GetNumBattlefieldScores() ~= 0 then self:RecordArena() end - end -end - -function ArenaStats:Reset() - self.current["status"] = "none" - - self.current["stats"] = {} - self.current["units"] = {} - self.specTable = {} -end - -function ArenaStats:RecordArena() - self:AddEntryToHistory(self.current["stats"]) - self:Print("Arena recorded") - self:Reset() -end - -function ArenaStats:AddEntryToHistory(stats) - table.insert(self.db.char.history, stats) - if self.db.profile.maxHistory > 0 then - while (#self.db.char.history > self.db.profile.maxHistory) do - table.remove(self.db.char.history, 1) - end - end - -- Invalidate BuildTable cache since history changed - self.tableCache = nil -end - -function ArenaStats:DrawMinimapIcon() - libDBIcon:Register(addonName, - _G.LibStub("LibDataBroker-1.1"):NewDataObject(addonName, - { - type = "data source", - text = addonName, - icon = "interface/icons/achievement_arena_2v2_7", - OnClick = function(self, button) - if button == "RightButton" then - _G.InterfaceOptionsFrame_OpenToCategory(addonName) - _G.InterfaceOptionsFrame_OpenToCategory(addonName) - else - ArenaStats:Toggle() - end - end, - OnTooltipShow = function(tooltip) - tooltip:AddLine(string.format("%s |cff777777v%s|r", addonTitle, - "0.3.0")) - tooltip:AddLine(string.format("|cFFCFCFCF%s|r %s", L["Left Click"], - L["to open the main window"])) - tooltip:AddLine(string.format("|cFFCFCFCF%s|r %s", L["Right Click"], - L["to open options"])) - tooltip:AddLine(string.format("|cFFCFCFCF%s|r %s", L["Drag"], - L["to move this button"])) - end - }), self.db.profile.minimapButton) -end - -function ArenaStats:ToggleMinimapButton() - self.db.profile.minimapButton.hide = not self.db.profile.minimapButton.hide - if self.db.profile.minimapButton.hide then - libDBIcon:Hide(addonName) - else - libDBIcon:Show(addonName) - end -end - -function ArenaStats:CalculateTeamSize(row) - if (row["teamSize"] ~= nil or row["teamClass"] == nil) then - return row["teamSize"] - end - - local teamSize = 0 - for i = 0, 5 do - if row["teamClass"][i] ~= nil then teamSize = teamSize + 1 end - end - return teamSize -end - ---- Builds a display-ready table from the history database. ---- Transforms the raw storage format (0-based indexed arrays) into a flat structure ---- with named fields (e.g., teamPlayerClass1, teamPlayerClass2, etc.) for easier GUI rendering. ---- Uses caching to avoid rebuilding when the history hasn't changed. ---- @return table[] Array of arena match records, sorted from newest to oldest -function ArenaStats:BuildTable() - local tableLength = #self.db.char.history - - -- Return cached table if history size hasn't changed - if self.tableCache and self.tableCacheSize == tableLength then - return self.tableCache - end - - local tbl = {} - - for i = 1, tableLength do - local row = self.db.char.history[tableLength + 1 - i] - table.insert(tbl, { - - -- Common stats - - ["startTime"] = row["startTime"], - ["endTime"] = row["endTime"], - ["zoneId"] = self:RemapZoneId(row["zoneId"]), - ["isRanked"] = row["isRanked"], - ["teamSize"] = self:CalculateTeamSize(row), - ["duration"] = (row["endTime"] and row["startTime"] and - (row["endTime"] - row["startTime"]) or 0), - - -- Player's team - - ["teamName"] = row["teamName"], - - ["teamPlayerClass1"] = row["teamClass"] and row["teamClass"][0] or - nil, - ["teamPlayerClass2"] = row["teamClass"] and row["teamClass"][1] or - nil, - ["teamPlayerClass3"] = row["teamClass"] and row["teamClass"][2] or - nil, - ["teamPlayerClass4"] = row["teamClass"] and row["teamClass"][3] or - nil, - ["teamPlayerClass5"] = row["teamClass"] and row["teamClass"][4] or - nil, - ["teamPlayerName1"] = row["teamCharName"] and row["teamCharName"][0] or - nil, - ["teamPlayerName2"] = row["teamCharName"] and row["teamCharName"][1] or - nil, - ["teamPlayerName3"] = row["teamCharName"] and row["teamCharName"][2] or - nil, - ["teamPlayerName4"] = row["teamCharName"] and row["teamCharName"][3] or - nil, - ["teamPlayerName5"] = row["teamCharName"] and row["teamCharName"][4] or - nil, - ["teamPlayerRace1"] = row["teamRace"] and row["teamRace"][0] or nil, - ["teamPlayerRace2"] = row["teamRace"] and row["teamRace"][1] or nil, - ["teamPlayerRace3"] = row["teamRace"] and row["teamRace"][2] or nil, - ["teamPlayerRace4"] = row["teamRace"] and row["teamRace"][3] or nil, - ["teamPlayerRace5"] = row["teamRace"] and row["teamRace"][4] or nil, - ["teamPlayerSpec1"] = row["teamSpec"] and row["teamSpec"][0] or nil, - ["teamPlayerSpec2"] = row["teamSpec"] and row["teamSpec"][1] or nil, - ["teamPlayerSpec3"] = row["teamSpec"] and row["teamSpec"][2] or nil, - ["teamPlayerSpec4"] = row["teamSpec"] and row["teamSpec"][3] or nil, - ["teamPlayerSpec5"] = row["teamSpec"] and row["teamSpec"][4] or nil, - - ["oldTeamRating"] = row["oldTeamRating"], - ["newTeamRating"] = row["newTeamRating"], - ["diffRating"] = row["diffRating"], - ["mmr"] = row["mmr"], - ["teamColor"] = row["teamColor"], - ["winnerColor"] = row["winnerColor"], - - -- Enemy team - - ["enemyTeamName"] = row["enemyTeamName"], - - ["enemyPlayerClass1"] = row["enemyClass"] and row["enemyClass"][0] or - nil, - ["enemyPlayerClass2"] = row["enemyClass"] and row["enemyClass"][1] or - nil, - ["enemyPlayerClass3"] = row["enemyClass"] and row["enemyClass"][2] or - nil, - ["enemyPlayerClass4"] = row["enemyClass"] and row["enemyClass"][3] or - nil, - ["enemyPlayerClass5"] = row["enemyClass"] and row["enemyClass"][4] or - nil, - ["enemyPlayerName1"] = row["enemyName"] and row["enemyName"][0] or - nil, - ["enemyPlayerName2"] = row["enemyName"] and row["enemyName"][1] or - nil, - ["enemyPlayerName3"] = row["enemyName"] and row["enemyName"][2] or - nil, - ["enemyPlayerName4"] = row["enemyName"] and row["enemyName"][3] or - nil, - ["enemyPlayerName5"] = row["enemyName"] and row["enemyName"][4] or - nil, - ["enemyPlayerRace1"] = row["enemyRace"] and row["enemyRace"][0] or - nil, - ["enemyPlayerRace2"] = row["enemyRace"] and row["enemyRace"][1] or - nil, - ["enemyPlayerRace3"] = row["enemyRace"] and row["enemyRace"][2] or - nil, - ["enemyPlayerRace4"] = row["enemyRace"] and row["enemyRace"][3] or - nil, - ["enemyPlayerRace5"] = row["enemyRace"] and row["enemyRace"][4] or - nil, - ["enemyPlayerSpec1"] = row["enemySpec"] and row["enemySpec"][0] or nil, - ["enemyPlayerSpec2"] = row["enemySpec"] and row["enemySpec"][1] or nil, - ["enemyPlayerSpec3"] = row["enemySpec"] and row["enemySpec"][2] or nil, - ["enemyPlayerSpec4"] = row["enemySpec"] and row["enemySpec"][3] or nil, - ["enemyPlayerSpec5"] = row["enemySpec"] and row["enemySpec"][4] or nil, - ["enemyFaction"] = row["enemyFaction"], - - ["enemyOldTeamRating"] = row["enemyOldTeamRating"], - ["enemyNewTeamRating"] = row["enemyNewTeamRating"], - ["enemyDiffRating"] = row["enemyDiffRating"], - ["enemyMmr"] = row["enemyMmr"] - - }) - end - - -- Store in cache for subsequent calls - self.tableCache = tbl - self.tableCacheSize = tableLength - - return tbl -end - -function ArenaStats:RemapZoneId(mapAreaId) - -- remap old mapAreaId to instanceids (for backward compatibility) - if mapAreaId == 3698 then - return 559 - elseif mapAreaId == 3702 then - return 562 - elseif mapAreaId == 3968 then - return 572 - end - return mapAreaId -end - -function ArenaStats:ResetDatabase() - self.db:ResetDB() - -- Invalidate BuildTable cache since database was reset - self.tableCache = nil - self:Print(L["Database reset"]) -end - --- Helper to safely get value or empty string -local function safeVal(val) - return val ~= nil and val or "" -end - --- Helper to safely get indexed value from table -local function safeIndexedVal(tbl, index) - return tbl and tbl[index] ~= nil and tbl[index] or "" -end - -function ArenaStats:ExportCSV() - local csvParts = { - "isRanked,startTime,endTime,zoneId,duration,teamName,teamColor,winnerColor," .. - "teamPlayerName1,teamPlayerName2,teamPlayerName3,teamPlayerName4,teamPlayerName5," .. - "teamPlayerClass1,teamPlayerClass2,teamPlayerClass3,teamPlayerClass4,teamPlayerClass5," .. - "teamPlayerRace1,teamPlayerRace2,teamPlayerRace3,teamPlayerRace4,teamPlayerRace5," .. - "oldTeamRating,newTeamRating,diffRating,mmr," .. - "enemyOldTeamRating,enemyNewTeamRating,enemyDiffRating,enemyMmr,enemyTeamName," .. - "enemyPlayerName1,enemyPlayerName2,enemyPlayerName3,enemyPlayerName4,enemyPlayerName5," .. - "enemyPlayerClass1,enemyPlayerClass2,enemyPlayerClass3,enemyPlayerClass4,enemyPlayerClass5," .. - "enemyPlayerRace1,enemyPlayerRace2,enemyPlayerRace3,enemyPlayerRace4,enemyPlayerRace5,enemyFaction," .. - "enemySpec1,enemySpec2,enemySpec3,enemySpec4,enemySpec5," .. - "teamSpec1,teamSpec2,teamSpec3,teamSpec4,teamSpec5,\n" - } - - for _, row in ipairs(self.db.char.history) do - local duration = (row["startTime"] and row["endTime"]) and (row["endTime"] - row["startTime"]) or "" - local rowParts = { - self:YesOrNo(row["isRanked"]), - safeVal(row["startTime"]), - safeVal(row["endTime"]), - safeVal(row["zoneId"]), - duration, - safeVal(row["teamName"]), - safeVal(row["teamColor"]), - safeVal(row["winnerColor"]), - -- Team player names - safeIndexedVal(row["teamCharName"], 0), - safeIndexedVal(row["teamCharName"], 1), - safeIndexedVal(row["teamCharName"], 2), - safeIndexedVal(row["teamCharName"], 3), - safeIndexedVal(row["teamCharName"], 4), - -- Team player classes - safeIndexedVal(row["teamClass"], 0), - safeIndexedVal(row["teamClass"], 1), - safeIndexedVal(row["teamClass"], 2), - safeIndexedVal(row["teamClass"], 3), - safeIndexedVal(row["teamClass"], 4), - -- Team player races - safeIndexedVal(row["teamRace"], 0), - safeIndexedVal(row["teamRace"], 1), - safeIndexedVal(row["teamRace"], 2), - safeIndexedVal(row["teamRace"], 3), - safeIndexedVal(row["teamRace"], 4), - -- Team ratings - self:ComputeSafeNumber(row["oldTeamRating"]), - self:ComputeSafeNumber(row["newTeamRating"]), - self:ComputeSafeNumber(row["diffRating"]), - self:ComputeSafeNumber(row["mmr"]), - -- Enemy ratings - self:ComputeSafeNumber(row["enemyOldTeamRating"]), - self:ComputeSafeNumber(row["enemyNewTeamRating"]), - self:ComputeSafeNumber(row["enemyDiffRating"]), - self:ComputeSafeNumber(row["enemyMmr"]), - safeVal(row["enemyTeamName"]), - -- Enemy player names - safeIndexedVal(row["enemyName"], 0), - safeIndexedVal(row["enemyName"], 1), - safeIndexedVal(row["enemyName"], 2), - safeIndexedVal(row["enemyName"], 3), - safeIndexedVal(row["enemyName"], 4), - -- Enemy player classes - safeIndexedVal(row["enemyClass"], 0), - safeIndexedVal(row["enemyClass"], 1), - safeIndexedVal(row["enemyClass"], 2), - safeIndexedVal(row["enemyClass"], 3), - safeIndexedVal(row["enemyClass"], 4), - -- Enemy player races - safeIndexedVal(row["enemyRace"], 0), - safeIndexedVal(row["enemyRace"], 1), - safeIndexedVal(row["enemyRace"], 2), - safeIndexedVal(row["enemyRace"], 3), - safeIndexedVal(row["enemyRace"], 4), - self:ComputeFaction(row["enemyFaction"]), - -- Team specs - safeIndexedVal(row["teamSpec"], 0), - safeIndexedVal(row["teamSpec"], 1), - safeIndexedVal(row["teamSpec"], 2), - safeIndexedVal(row["teamSpec"], 3), - safeIndexedVal(row["teamSpec"], 4), - -- Enemy specs - safeIndexedVal(row["enemySpec"], 0), - safeIndexedVal(row["enemySpec"], 1), - safeIndexedVal(row["enemySpec"], 2), - safeIndexedVal(row["enemySpec"], 3), - safeIndexedVal(row["enemySpec"], 4), - } - csvParts[#csvParts + 1] = table.concat(rowParts, ",") .. ",\n" - end - - local csv = table.concat(csvParts) - ArenaStats:ExportFrame().eb:SetText(csv) - ArenaStats:ExportFrame():SetTitle(L["Export"]) - ArenaStats:ExportFrame().eb:SetNumLines(29) - ArenaStats:ExportFrame().eb:SetLabel( - "Export String " .. " (" .. string.len(csv) .. ") ") - ArenaStats:ExportFrame():Show() - ArenaStats:ExportFrame().eb:SetFocus() - ArenaStats:ExportFrame().eb:HighlightText() -end - -function ArenaStats:WebsiteURL() - ArenaStats:ExportFrame():SetTitle(L["Tool"]) - ArenaStats:ExportFrame().eb:SetLabel("Tool Website URL") - ArenaStats:ExportFrame().eb:SetNumLines(1) - ArenaStats:ExportFrame().eb:SetText( - "https://denishamann.github.io/arena-stats-visualizer/") - ArenaStats:ExportFrame():Show() - ArenaStats:ExportFrame().eb:SetFocus() - ArenaStats:ExportFrame().eb:HighlightText() -end - -function ArenaStats:ComputeFaction(factionId) - if factionId == 1 then - return "ALLIANCE" - elseif factionId == 0 then - return "HORDE" - end - return "" -end - -function ArenaStats:YesOrNo(bool) - if bool then - return "YES" - elseif not bool then - return "NO" - end - return "" -end - -function ArenaStats:ComputeSafeNumber(number) - if number == nil then - return "" - elseif number == 0 then - return "0" - end - return number -end - -function ArenaStats:ShouldShowCharacterNamesTooltips() - return self.db.profile.showCharacterNamesOnHover -end +local addonName = "ArenaStats" +local addonTitle = select(2, (C_AddOns and C_AddOns.GetAddOnInfo or GetAddOnInfo)(addonName)) +local ArenaStats = _G.LibStub("AceAddon-3.0"):NewAddon(addonName, + "AceConsole-3.0", + "AceEvent-3.0") +local L = _G.LibStub("AceLocale-3.0"):GetLocale(addonName, true) +local libDBIcon = _G.LibStub("LibDBIcon-1.0") +local LibRaces = _G.LibStub("LibRaces-1.0") +local IsActiveBattlefieldArena = IsActiveBattlefieldArena +local GetBattlefieldStatus, GetBattlefieldTeamInfo, GetNumBattlefieldScores, + GetBattlefieldScore, GetBattlefieldWinner, IsArenaSkirmish, IsInInstance, + GetInstanceInfo = GetBattlefieldStatus, GetBattlefieldTeamInfo, + GetNumBattlefieldScores, GetBattlefieldScore, + GetBattlefieldWinner, IsArenaSkirmish, IsInInstance, + GetInstanceInfo +local UnitName, UnitRace, UnitClass, UnitGUID, UnitFactionGroup, UnitIsPlayer = + UnitName, UnitRace, UnitClass, UnitGUID, UnitFactionGroup, UnitIsPlayer + +function ArenaStats:OnInitialize() + self.db = _G.LibStub("AceDB-3.0"):New(addonName, { + profile = { + minimapButton = {hide = false}, -- Note: LibDBIcon requires this format + maxHistory = 0, + showCharacterNamesOnHover = true, + showSpec = true + }, + char = {history = {}} + }) + + self:Print("Tracking ready, have a nice session!") + self.specSpells = self:GetSpecSpells(); + + self:RegisterEvent("UPDATE_BATTLEFIELD_STATUS") + self:RegisterEvent("UPDATE_BATTLEFIELD_SCORE") + self:RegisterEvent("ZONE_CHANGED_NEW_AREA") + + self:RegisterEvent("ARENA_OPPONENT_UPDATE") + self:RegisterEvent("UNIT_AURA") + self:RegisterEvent("UNIT_SPELLCAST_START") + self:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START") + self:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED") + + self:DrawMinimapIcon() + self:RegisterOptionsTable() + + self.specTable = {} + self.arenaEnded = false + self.current = { status = "none", stats = {}, units = {} } + + -- Cache for BuildTable to avoid rebuilding on every GUI refresh + self.tableCache = nil + self.tableCacheSize = 0 + + self:Reset() +end + +function ArenaStats:OnDisable() + self:UnregisterAllEvents() + if _G.AsFrame then + _G.AsFrame:Hide() + end +end + +function ArenaStats:OnSpecDetected(unitName, spec) + local existingPlayer = self.specTable[unitName] + + if existingPlayer then + return + end + + self.specTable[unitName] = spec +end + +function ArenaStats:ScanUnitBuffs(unit) + if not unit then return end + + for n = 1, 30 do + local name, spellID, unitCaster = self:GetUnitBuff(unit, n) + + if not name then + break + end + + if self.specSpells[spellID] then + -- For TBC, we can't reliably get unitCaster, so we assume self-buffs + if self.isTBC then + local casterName = GetUnitName(unit, true) + if casterName then + self:OnSpecDetected(casterName, self.specSpells[spellID]) + end + elseif unitCaster then + local unitPet = string.gsub(unit, "%d$", "pet%1") + if UnitIsUnit(unit, unitCaster) or UnitIsUnit(unitPet, unitCaster) then + local casterName = GetUnitName(unitCaster, true) + if casterName then + self:OnSpecDetected(casterName, self.specSpells[spellID]) + end + end + end + end + end +end + +function ArenaStats:ARENA_OPPONENT_UPDATE(_, unit, updateReason) + self:ScanUnitBuffs(unit) +end + +function ArenaStats:UNIT_AURA(_, unit, isFullUpdate, updatedAuras) + self:ScanUnitBuffs(unit) +end + +function ArenaStats:UNIT_SPELLCAST_START(_, unit, castGUID, spellID) + local spellName = spellID and GetSpellInfo(spellID) or nil + + if unit then + self:ScanUnitBuffs(unit) + end + + if spellID and self.specSpells[spellID] and unit then + local name = GetUnitName(unit, true) + if name then + self:OnSpecDetected(name, self.specSpells[spellID]) + end + end +end + +function ArenaStats:UNIT_SPELLCAST_CHANNEL_START(_, unit, castGuid, spellId) + if unit then + self:ScanUnitBuffs(unit) + end + + if spellId and self.specSpells[spellId] and unit then + local name = GetUnitName(unit, true) + if name then + self:OnSpecDetected(name, self.specSpells[spellId]) + end + end +end + +function ArenaStats:UNIT_SPELLCAST_SUCCEEDED(_, unit, castGuid, spellId) + if unit then + self:ScanUnitBuffs(unit) + end + + if spellId and self.specSpells[spellId] and unit then + local name = GetUnitName(unit, true) + if name then + self:OnSpecDetected(name, self.specSpells[spellId]) + end + end +end + +function ArenaStats:ZONE_CHANGED_NEW_AREA() + local _, instanceType = IsInInstance() + if (instanceType == "arena") then + self.arenaEnded = false + self.specTable = {} + end +end + +function ArenaStats:UPDATE_BATTLEFIELD_STATUS(_, index) + local status, mapName, instanceID, levelRangeMin, levelRangeMax, teamSize, + isRankedArena, suspendedQueue, bool, queueType = + GetBattlefieldStatus(index) + if (status == "active" and teamSize > 0 and IsActiveBattlefieldArena()) then + self.current["status"] = status + self.current["stats"]["teamSize"] = teamSize + self.current["stats"]["isRanked"] = not IsArenaSkirmish() + if (self.current["stats"]["startTime"] == nil or + self.current["stats"]["startTime"] == '') then + self.current["stats"]["startTime"] = _G.time() + end + end +end + +function ArenaStats:GetSpecOrDefault(unitName) + if not unitName then + return "Unknown" + end + + local detectedSpec = self.specTable[unitName] + + if detectedSpec then + return detectedSpec + end + + return "Unknown" +end + +--- Collects and stores all arena ranking data at the end of a match. +--- This function is called when the battlefield score is updated and a winner is determined. +--- +--- The function performs three main tasks: +--- 1. Identifies which team (GREEN=0 or GOLD=1) the player belongs to by scanning scores +--- 2. Retrieves team ratings and MMR for both teams from GetBattlefieldTeamInfo +--- 3. Collects individual player data (class, name, race, spec) for both teams +--- +--- Data is stored in self.current["stats"] with 0-based indexing for player arrays +--- to maintain compatibility with the existing data format. +function ArenaStats:SetLastArenaRankingData() + local playerTeam = '' + local greenTeam = {} + local goldTeam = {} + local myName = UnitName("player") + local numScores = GetNumBattlefieldScores() + + -- Step 1: Scan all players to determine which team we're on and group players by team + -- GREEN team has teamIndex=0, GOLD team has teamIndex=1 + for i = 1, numScores do + local data = { GetBattlefieldScore(i) } + local teamIndex = data[6] + + -- Check if this player is us to determine our team color + if data[1] == myName then + playerTeam = (teamIndex == 0) and 'GREEN' or 'GOLD' + end + + -- Group players into their respective teams + if teamIndex == 0 then + table.insert(greenTeam, data) + else + table.insert(goldTeam, data) + end + end + + self.current["stats"]["teamColor"] = playerTeam + + -- Step 2: Get team ratings and MMR from both teams + -- Team index 0 = GREEN, Team index 1 = GOLD + for i = 0, 1 do + local teamName, oldTeamRating, newTeamRating, teamMMR = + GetBattlefieldTeamInfo(i) + if teamMMR > 0 then + local isPlayerTeam = (i == 0 and playerTeam == 'GREEN') or + (i == 1 and playerTeam == 'GOLD') + if isPlayerTeam then + self.current["stats"]["teamName"] = teamName + self.current["stats"]["oldTeamRating"] = oldTeamRating + self.current["stats"]["newTeamRating"] = newTeamRating + self.current["stats"]["diffRating"] = newTeamRating - oldTeamRating + self.current["stats"]["mmr"] = teamMMR + else + self.current["stats"]["enemyTeamName"] = teamName + self.current["stats"]["enemyOldTeamRating"] = oldTeamRating + self.current["stats"]["enemyNewTeamRating"] = newTeamRating + self.current["stats"]["enemyDiffRating"] = newTeamRating - oldTeamRating + self.current["stats"]["enemyMmr"] = teamMMR + end + end + end + + -- Step 3: Initialize player data arrays and collect individual player info + self.current["stats"]["teamClass"] = {} + self.current["stats"]["teamCharName"] = {} + self.current["stats"]["teamRace"] = {} + self.current["stats"]["teamSpec"] = {} + self.current["stats"]["teamDamage"] = {} + self.current["stats"]["teamHealing"] = {} + + self.current["stats"]["enemyClass"] = {} + self.current["stats"]["enemyName"] = {} + self.current["stats"]["enemyRace"] = {} + self.current["stats"]["enemySpec"] = {} + self.current["stats"]["enemyDamage"] = {} + self.current["stats"]["enemyHealing"] = {} + + -- Determine which raw data table corresponds to player's team vs enemy team + local playerTeamTable = (playerTeam == 'GREEN') and greenTeam or goldTeam + local enemyTeamTable = (playerTeam == 'GREEN') and goldTeam or greenTeam + + -- Helper to convert localized race name to uppercase race token + local function convertRace(raceInput) + if not raceInput then return '' end + local race = LibRaces:GetRaceToken(raceInput) + return race and race:upper() or '' + end + + -- GetBattlefieldScore returns: + -- [1]=playerName, [2]=killingBlows, [3]=honorKills, [4]=deaths, [5]=honorGained, + -- [6]=faction, [7]=rank, [8]=race, [9]=class, [10]=classToken, [11]=damageDone, [12]=healingDone + + -- Collect player's team data (0-based indexing for storage) + for i = 1, #playerTeamTable do + local row = playerTeamTable[i] + local raceUpper = convertRace(row[8]) + local idx = i - 1 -- Convert to 0-based index + + self.current["stats"]["teamClass"][idx] = row[10] and row[10]:upper() or '' + self.current["stats"]["teamCharName"][idx] = row[1] + self.current["stats"]["teamRace"][idx] = raceUpper + self.current["stats"]["teamSpec"][idx] = self:GetSpecOrDefault(row[1]) + self.current["stats"]["teamDamage"][idx] = row[11] + self.current["stats"]["teamHealing"][idx] = row[12] + end + + -- Collect enemy team data (0-based indexing for storage) + for i = 1, #enemyTeamTable do + local row = enemyTeamTable[i] + local raceUpper = convertRace(row[8]) + local idx = i - 1 -- Convert to 0-based index + + self.current["stats"]["enemyClass"][idx] = row[10] and row[10]:upper() or '' + self.current["stats"]["enemyName"][idx] = row[1] + self.current["stats"]["enemyRace"][idx] = raceUpper + self.current["stats"]["enemyFaction"] = self:RaceToFaction(raceUpper) + self.current["stats"]["enemyFaction"] = self:RaceToFaction(raceUpper) + self.current["stats"]["enemySpec"][idx] = self:GetSpecOrDefault(row[1]) + self.current["stats"]["enemyDamage"][idx] = row[11] + self.current["stats"]["enemyHealing"][idx] = row[12] + end +end + +-- Alliance races lookup table +local ALLIANCE_RACES = { + HUMAN = true, GNOME = true, NIGHTELF = true, + DRAENEI = true, DWARF = true, WORGEN = true +} + +function ArenaStats:RaceToFaction(race) + return ALLIANCE_RACES[race] and 1 or 0 +end + +function ArenaStats:UPDATE_BATTLEFIELD_SCORE() + local battlefieldWinner = GetBattlefieldWinner() + if battlefieldWinner == nil or self.arenaEnded then return end + + if self.current.status ~= 'none' then + self.current["stats"]["zoneId"] = select(8, GetInstanceInfo()) + self.current["stats"]["endTime"] = _G.time() + self.arenaEnded = true + if (battlefieldWinner == 0) then + self.current["stats"]["winnerColor"] = "GREEN"; + elseif (battlefieldWinner == 1) then + self.current["stats"]["winnerColor"] = "GOLD"; + end + self:SetLastArenaRankingData() + if GetNumBattlefieldScores() ~= 0 then self:RecordArena() end + end +end + +function ArenaStats:Reset() + self.current["status"] = "none" + + self.current["stats"] = {} + self.current["units"] = {} + self.specTable = {} +end + +function ArenaStats:RecordArena() + self:AddEntryToHistory(self.current["stats"]) + self:Print("Arena recorded") + self:Reset() +end + +function ArenaStats:AddEntryToHistory(stats) + table.insert(self.db.char.history, stats) + if self.db.profile.maxHistory > 0 then + while (#self.db.char.history > self.db.profile.maxHistory) do + table.remove(self.db.char.history, 1) + end + end + -- Invalidate BuildTable cache since history changed + self.tableCache = nil +end + +function ArenaStats:DrawMinimapIcon() + libDBIcon:Register(addonName, + _G.LibStub("LibDataBroker-1.1"):NewDataObject(addonName, + { + type = "data source", + text = addonName, + icon = "interface/icons/achievement_arena_2v2_7", + OnClick = function(self, button) + if button == "RightButton" then + _G.LibStub("AceConfigDialog-3.0"):Open(addonName) + else + ArenaStats:Toggle() + end + end, + OnTooltipShow = function(tooltip) + tooltip:AddLine(string.format("%s |cff777777v%s|r", addonTitle, + "0.2.5")) + tooltip:AddLine(string.format("|cFFCFCFCF%s|r %s", L["Left Click"], + L["to open the main window"])) + tooltip:AddLine(string.format("|cFFCFCFCF%s|r %s", L["Right Click"], + L["to open options"])) + tooltip:AddLine(string.format("|cFFCFCFCF%s|r %s", L["Drag"], + L["to move this button"])) + end + }), self.db.profile.minimapButton) +end + +function ArenaStats:ToggleMinimapButton() + self.db.profile.minimapButton.hide = not self.db.profile.minimapButton.hide + if self.db.profile.minimapButton.hide then + libDBIcon:Hide(addonName) + else + libDBIcon:Show(addonName) + end +end + +function ArenaStats:CalculateTeamSize(row) + if (row["teamSize"] ~= nil or row["teamClass"] == nil) then + return row["teamSize"] + end + + local teamSize = 0 + for i = 0, 5 do + if row["teamClass"][i] ~= nil then teamSize = teamSize + 1 end + end + return teamSize +end + +--- Builds a display-ready table from the history database. +--- Transforms the raw storage format (0-based indexed arrays) into a flat structure +--- with named fields (e.g., teamPlayerClass1, teamPlayerClass2, etc.) for easier GUI rendering. +--- Uses caching to avoid rebuilding when the history hasn't changed. +--- @return table[] Array of arena match records, sorted from newest to oldest +function ArenaStats:BuildTable() + local tableLength = #self.db.char.history + + -- Return cached table if history size hasn't changed + if self.tableCache and self.tableCacheSize == tableLength then + return self.tableCache + end + + local tbl = {} + + for i = 1, tableLength do + local row = self.db.char.history[tableLength + 1 - i] + table.insert(tbl, { + + ["_original"] = row, + -- Common stats + + ["startTime"] = row["startTime"], + ["endTime"] = row["endTime"], + ["zoneId"] = self:RemapZoneId(row["zoneId"]), + ["isRanked"] = row["isRanked"], + ["teamSize"] = self:CalculateTeamSize(row), + ["duration"] = (row["endTime"] and row["startTime"] and + (row["endTime"] - row["startTime"]) or 0), + + -- Player's team + + ["teamName"] = row["teamName"], + + ["teamPlayerClass1"] = row["teamClass"] and row["teamClass"][0] or + nil, + ["teamPlayerClass2"] = row["teamClass"] and row["teamClass"][1] or + nil, + ["teamPlayerClass3"] = row["teamClass"] and row["teamClass"][2] or + nil, + ["teamPlayerClass4"] = row["teamClass"] and row["teamClass"][3] or + nil, + ["teamPlayerClass5"] = row["teamClass"] and row["teamClass"][4] or + nil, + ["teamPlayerName1"] = row["teamCharName"] and row["teamCharName"][0] or + nil, + ["teamPlayerName2"] = row["teamCharName"] and row["teamCharName"][1] or + nil, + ["teamPlayerName3"] = row["teamCharName"] and row["teamCharName"][2] or + nil, + ["teamPlayerName4"] = row["teamCharName"] and row["teamCharName"][3] or + nil, + ["teamPlayerName5"] = row["teamCharName"] and row["teamCharName"][4] or + nil, + ["teamPlayerRace1"] = row["teamRace"] and row["teamRace"][0] or nil, + ["teamPlayerRace2"] = row["teamRace"] and row["teamRace"][1] or nil, + ["teamPlayerRace3"] = row["teamRace"] and row["teamRace"][2] or nil, + ["teamPlayerRace4"] = row["teamRace"] and row["teamRace"][3] or nil, + ["teamPlayerRace5"] = row["teamRace"] and row["teamRace"][4] or nil, + ["teamPlayerSpec1"] = row["teamSpec"] and row["teamSpec"][0] or nil, + ["teamPlayerSpec2"] = row["teamSpec"] and row["teamSpec"][1] or nil, + ["teamPlayerSpec3"] = row["teamSpec"] and row["teamSpec"][2] or nil, + ["teamPlayerSpec4"] = row["teamSpec"] and row["teamSpec"][3] or nil, + ["teamPlayerSpec5"] = row["teamSpec"] and row["teamSpec"][4] or nil, + + ["teamPlayerDamage1"] = row["teamDamage"] and row["teamDamage"][0] or 0, + ["teamPlayerDamage2"] = row["teamDamage"] and row["teamDamage"][1] or 0, + ["teamPlayerDamage3"] = row["teamDamage"] and row["teamDamage"][2] or 0, + ["teamPlayerDamage4"] = row["teamDamage"] and row["teamDamage"][3] or 0, + ["teamPlayerDamage5"] = row["teamDamage"] and row["teamDamage"][4] or 0, + + ["teamPlayerHealing1"] = row["teamHealing"] and row["teamHealing"][0] or 0, + ["teamPlayerHealing2"] = row["teamHealing"] and row["teamHealing"][1] or 0, + ["teamPlayerHealing3"] = row["teamHealing"] and row["teamHealing"][2] or 0, + ["teamPlayerHealing4"] = row["teamHealing"] and row["teamHealing"][3] or 0, + ["teamPlayerHealing5"] = row["teamHealing"] and row["teamHealing"][4] or 0, + + ["oldTeamRating"] = row["oldTeamRating"], + ["newTeamRating"] = row["newTeamRating"], + ["diffRating"] = row["diffRating"], + ["mmr"] = row["mmr"], + ["teamColor"] = row["teamColor"], + ["winnerColor"] = row["winnerColor"], + + -- Enemy team + + ["enemyTeamName"] = row["enemyTeamName"], + + ["enemyPlayerClass1"] = row["enemyClass"] and row["enemyClass"][0] or + nil, + ["enemyPlayerClass2"] = row["enemyClass"] and row["enemyClass"][1] or + nil, + ["enemyPlayerClass3"] = row["enemyClass"] and row["enemyClass"][2] or + nil, + ["enemyPlayerClass4"] = row["enemyClass"] and row["enemyClass"][3] or + nil, + ["enemyPlayerClass5"] = row["enemyClass"] and row["enemyClass"][4] or + nil, + ["enemyPlayerName1"] = row["enemyName"] and row["enemyName"][0] or + nil, + ["enemyPlayerName2"] = row["enemyName"] and row["enemyName"][1] or + nil, + ["enemyPlayerName3"] = row["enemyName"] and row["enemyName"][2] or + nil, + ["enemyPlayerName4"] = row["enemyName"] and row["enemyName"][3] or + nil, + ["enemyPlayerName5"] = row["enemyName"] and row["enemyName"][4] or + nil, + ["enemyPlayerRace1"] = row["enemyRace"] and row["enemyRace"][0] or + nil, + ["enemyPlayerRace2"] = row["enemyRace"] and row["enemyRace"][1] or + nil, + ["enemyPlayerRace3"] = row["enemyRace"] and row["enemyRace"][2] or + nil, + ["enemyPlayerRace4"] = row["enemyRace"] and row["enemyRace"][3] or + nil, + ["enemyPlayerRace5"] = row["enemyRace"] and row["enemyRace"][4] or + nil, + ["enemyPlayerSpec1"] = row["enemySpec"] and row["enemySpec"][0] or nil, + ["enemyPlayerSpec2"] = row["enemySpec"] and row["enemySpec"][1] or nil, + ["enemyPlayerSpec3"] = row["enemySpec"] and row["enemySpec"][2] or nil, + ["enemyPlayerSpec4"] = row["enemySpec"] and row["enemySpec"][3] or nil, + ["enemyPlayerSpec5"] = row["enemySpec"] and row["enemySpec"][4] or nil, + + ["enemyPlayerDamage1"] = row["enemyDamage"] and row["enemyDamage"][0] or 0, + ["enemyPlayerDamage2"] = row["enemyDamage"] and row["enemyDamage"][1] or 0, + ["enemyPlayerDamage3"] = row["enemyDamage"] and row["enemyDamage"][2] or 0, + ["enemyPlayerDamage4"] = row["enemyDamage"] and row["enemyDamage"][3] or 0, + ["enemyPlayerDamage5"] = row["enemyDamage"] and row["enemyDamage"][4] or 0, + + ["enemyPlayerHealing1"] = row["enemyHealing"] and row["enemyHealing"][0] or 0, + ["enemyPlayerHealing2"] = row["enemyHealing"] and row["enemyHealing"][1] or 0, + ["enemyPlayerHealing3"] = row["enemyHealing"] and row["enemyHealing"][2] or 0, + ["enemyPlayerHealing4"] = row["enemyHealing"] and row["enemyHealing"][3] or 0, + ["enemyPlayerHealing5"] = row["enemyHealing"] and row["enemyHealing"][4] or 0, + ["enemyFaction"] = row["enemyFaction"], + + ["enemyOldTeamRating"] = row["enemyOldTeamRating"], + ["enemyNewTeamRating"] = row["enemyNewTeamRating"], + ["enemyDiffRating"] = row["enemyDiffRating"], + ["enemyMmr"] = row["enemyMmr"] + + }) + end + + -- Store in cache for subsequent calls + self.tableCache = tbl + self.tableCacheSize = tableLength + + return tbl +end + +function ArenaStats:RemapZoneId(mapAreaId) + -- remap old mapAreaId to instanceids (for backward compatibility) + if mapAreaId == 3698 then + return 559 + elseif mapAreaId == 3702 then + return 562 + elseif mapAreaId == 3968 then + return 572 + end + return mapAreaId +end + +function ArenaStats:ResetDatabase() + self.db:ResetDB() + -- Invalidate BuildTable cache since database was reset + self.tableCache = nil + self:Print(L["Database reset"]) +end + +-- Helper to safely get value or empty string +local function safeVal(val) + return val ~= nil and val or "" +end + +-- Helper to safely get indexed value from table +local function safeIndexedVal(tbl, index) + return tbl and tbl[index] ~= nil and tbl[index] or "" +end + +function ArenaStats:ExportCSV() + local csvParts = { + "isRanked,startTime,endTime,zoneId,duration,teamName,teamColor,winnerColor," .. + "teamPlayerName1,teamPlayerName2,teamPlayerName3,teamPlayerName4,teamPlayerName5," .. + "teamPlayerClass1,teamPlayerClass2,teamPlayerClass3,teamPlayerClass4,teamPlayerClass5," .. + "teamPlayerRace1,teamPlayerRace2,teamPlayerRace3,teamPlayerRace4,teamPlayerRace5," .. + "oldTeamRating,newTeamRating,diffRating,mmr," .. + "enemyOldTeamRating,enemyNewTeamRating,enemyDiffRating,enemyMmr,enemyTeamName," .. + "enemyPlayerName1,enemyPlayerName2,enemyPlayerName3,enemyPlayerName4,enemyPlayerName5," .. + "enemyPlayerClass1,enemyPlayerClass2,enemyPlayerClass3,enemyPlayerClass4,enemyPlayerClass5," .. + "enemyPlayerRace1,enemyPlayerRace2,enemyPlayerRace3,enemyPlayerRace4,enemyPlayerRace5,enemyFaction," .. + "enemySpec1,enemySpec2,enemySpec3,enemySpec4,enemySpec5," .. + "teamSpec1,teamSpec2,teamSpec3,teamSpec4,teamSpec5,\n" + } + + for _, row in ipairs(self.db.char.history) do + local duration = (row["startTime"] and row["endTime"]) and (row["endTime"] - row["startTime"]) or "" + local rowParts = { + self:YesOrNo(row["isRanked"]), + safeVal(row["startTime"]), + safeVal(row["endTime"]), + safeVal(row["zoneId"]), + duration, + safeVal(row["teamName"]), + safeVal(row["teamColor"]), + safeVal(row["winnerColor"]), + -- Team player names + safeIndexedVal(row["teamCharName"], 0), + safeIndexedVal(row["teamCharName"], 1), + safeIndexedVal(row["teamCharName"], 2), + safeIndexedVal(row["teamCharName"], 3), + safeIndexedVal(row["teamCharName"], 4), + -- Team player classes + safeIndexedVal(row["teamClass"], 0), + safeIndexedVal(row["teamClass"], 1), + safeIndexedVal(row["teamClass"], 2), + safeIndexedVal(row["teamClass"], 3), + safeIndexedVal(row["teamClass"], 4), + -- Team player races + safeIndexedVal(row["teamRace"], 0), + safeIndexedVal(row["teamRace"], 1), + safeIndexedVal(row["teamRace"], 2), + safeIndexedVal(row["teamRace"], 3), + safeIndexedVal(row["teamRace"], 4), + -- Team ratings + self:ComputeSafeNumber(row["oldTeamRating"]), + self:ComputeSafeNumber(row["newTeamRating"]), + self:ComputeSafeNumber(row["diffRating"]), + self:ComputeSafeNumber(row["mmr"]), + -- Enemy ratings + self:ComputeSafeNumber(row["enemyOldTeamRating"]), + self:ComputeSafeNumber(row["enemyNewTeamRating"]), + self:ComputeSafeNumber(row["enemyDiffRating"]), + self:ComputeSafeNumber(row["enemyMmr"]), + safeVal(row["enemyTeamName"]), + -- Enemy player names + safeIndexedVal(row["enemyName"], 0), + safeIndexedVal(row["enemyName"], 1), + safeIndexedVal(row["enemyName"], 2), + safeIndexedVal(row["enemyName"], 3), + safeIndexedVal(row["enemyName"], 4), + -- Enemy player classes + safeIndexedVal(row["enemyClass"], 0), + safeIndexedVal(row["enemyClass"], 1), + safeIndexedVal(row["enemyClass"], 2), + safeIndexedVal(row["enemyClass"], 3), + safeIndexedVal(row["enemyClass"], 4), + -- Enemy player races + safeIndexedVal(row["enemyRace"], 0), + safeIndexedVal(row["enemyRace"], 1), + safeIndexedVal(row["enemyRace"], 2), + safeIndexedVal(row["enemyRace"], 3), + safeIndexedVal(row["enemyRace"], 4), + self:ComputeFaction(row["enemyFaction"]), + -- Team specs + safeIndexedVal(row["teamSpec"], 0), + safeIndexedVal(row["teamSpec"], 1), + safeIndexedVal(row["teamSpec"], 2), + safeIndexedVal(row["teamSpec"], 3), + safeIndexedVal(row["teamSpec"], 4), + -- Enemy specs + safeIndexedVal(row["enemySpec"], 0), + safeIndexedVal(row["enemySpec"], 1), + safeIndexedVal(row["enemySpec"], 2), + safeIndexedVal(row["enemySpec"], 3), + safeIndexedVal(row["enemySpec"], 4), + } + csvParts[#csvParts + 1] = table.concat(rowParts, ",") .. ",\n" + end + + local csv = table.concat(csvParts) + ArenaStats:ExportFrame().eb:SetText(csv) + ArenaStats:ExportFrame():SetTitle(L["Export"]) + ArenaStats:ExportFrame().eb:SetNumLines(29) + ArenaStats:ExportFrame().eb:SetLabel( + "Export String " .. " (" .. string.len(csv) .. ") ") + ArenaStats:ExportFrame():Show() + ArenaStats:ExportFrame().eb:SetFocus() + ArenaStats:ExportFrame().eb:HighlightText() +end + +function ArenaStats:WebsiteURL() + ArenaStats:ExportFrame():SetTitle(L["Tool"]) + ArenaStats:ExportFrame().eb:SetLabel("Tool Website URL") + ArenaStats:ExportFrame().eb:SetNumLines(1) + ArenaStats:ExportFrame().eb:SetText( + "https://denishamann.github.io/arena-stats-visualizer/") + ArenaStats:ExportFrame():Show() + ArenaStats:ExportFrame().eb:SetFocus() + ArenaStats:ExportFrame().eb:HighlightText() +end + +function ArenaStats:ComputeFaction(factionId) + if factionId == 1 then + return "ALLIANCE" + elseif factionId == 0 then + return "HORDE" + end + return "" +end + +function ArenaStats:YesOrNo(bool) + if bool then + return "YES" + elseif not bool then + return "NO" + end + return "" +end + +function ArenaStats:ComputeSafeNumber(number) + if number == nil then + return "" + elseif number == 0 then + return "0" + end + return number +end + +function ArenaStats:ShouldShowCharacterNamesTooltips() + return self.db.profile.showCharacterNamesOnHover +end + +function ArenaStats:TestData() + local brackets = {2, 3, 5} + local teamSize = brackets[math.random(#brackets)] + local maps = {559, 562, 572} + local zoneId = maps[math.random(#maps)] + local duration = math.random(120, 900) -- 2 to 15 mins + local startTime = _G.time() - duration + local endTime = _G.time() + + local classes = { + ["WARRIOR"] = {"Arms", "Fury", "Protection"}, + ["PALADIN"] = {"Holy", "Protection", "Retribution"}, + ["HUNTER"] = {"BeastMastery", "Marksmanship", "Survival"}, + ["ROGUE"] = {"Assassination", "Combat", "Subtlety"}, + ["PRIEST"] = {"Discipline", "Holy", "Shadow"}, + ["SHAMAN"] = {"Elemental", "Enhancement", "Restoration"}, + ["MAGE"] = {"Arcane", "Fire", "Frost"}, + ["WARLOCK"] = {"Affliction", "Demonology", "Destruction"}, + ["DRUID"] = {"Balance", "Feral", "Restoration"} + } + + local classKeys = {} + for k in pairs(classes) do table.insert(classKeys, k) end + + local function getRandomPlayer() + local class = classKeys[math.random(#classKeys)] + local specs = classes[class] + local spec = specs[math.random(#specs)] + local races = {"Human", "Orc", "Undead", "Night Elf", "Gnome", "Troll", "Dwarf", "Blood Elf", "Draenei"} + return { + name = "Player" .. math.random(1000), + class = class, + spec = spec, + race = races[math.random(#races)] + } + end + + + + local teamColors = {"GOLD", "GREEN"} + local myTeamColor = teamColors[math.random(2)] + local winnerColor = teamColors[math.random(2)] + + local oldRating = math.random(1500, 2200) + local enemyOldRating = math.random(1500, 2200) + + local change = math.random(10, 25) + local newRating, enemyNewRating + + if myTeamColor == winnerColor then + -- We won + newRating = oldRating + change + enemyNewRating = enemyOldRating - change + else + -- We lost + newRating = oldRating - change + enemyNewRating = enemyOldRating + change + end + + local stats = { + startTime = startTime, + endTime = endTime, + zoneId = zoneId, + isRanked = true, + teamSize = teamSize, + teamName = "Test Team", + teamColor = myTeamColor, + winnerColor = winnerColor, + oldTeamRating = oldRating, + newTeamRating = newRating, + mmr = math.random(1500, 2500), + enemyTeamName = "Enemy Team", + enemyOldTeamRating = enemyOldRating, + enemyNewTeamRating = enemyNewRating, + enemyMmr = math.random(1500, 2500), + teamClass = {}, teamCharName = {}, teamRace = {}, teamSpec = {}, teamDamage = {}, teamHealing = {}, + enemyClass = {}, enemyName = {}, enemyRace = {}, enemySpec = {}, enemyDamage = {}, enemyHealing = {}, enemyFaction = math.random(0, 1) + } + stats.diffRating = stats.newTeamRating - stats.oldTeamRating + stats.enemyDiffRating = stats.enemyNewTeamRating - stats.enemyOldTeamRating + + -- Generate players + for i = 0, teamSize - 1 do + local p = getRandomPlayer() + stats.teamClass[i] = p.class + stats.teamCharName[i] = p.name + stats.teamRace[i] = string.upper(p.race) + stats.teamSpec[i] = p.spec + stats.teamDamage[i] = math.random(0, 100000) + stats.teamHealing[i] = math.random(0, 100000) + + local e = getRandomPlayer() + stats.enemyClass[i] = e.class + stats.enemyName[i] = e.name + stats.enemyRace[i] = string.upper(e.race) + stats.enemySpec[i] = e.spec + stats.enemyDamage[i] = math.random(0, 100000) + stats.enemyHealing[i] = math.random(0, 100000) + end + + self:AddEntryToHistory(stats) + self:ReloadData() + self:Print("Random test data generated") +end + +function ArenaStats:DeleteEntry(entry) + local target = entry._original or entry + for i, v in ipairs(self.db.char.history) do + if v == target then + table.remove(self.db.char.history, i) + self:ReloadData() + self:Print("Match deleted.") + return + end + end +end + +function ArenaStats:Toggle() + if asGui and asGui.f:IsShown() then + asGui.f:Hide() + else + if not asGui then + self:CreateGUI() + if self.SetData then + self:SetData(self:BuildTable()) + end + else + if self.SetData then + self:SetData(self:BuildTable()) + end + end + asGui.f:Show() + end +end + +function ArenaStats:ReloadData() + if self.SetData then + self:SetData(self:BuildTable()) + end +end diff --git a/ArenaStats.toc b/ArenaStats.toc index b5fe01a..7f429fc 100644 --- a/ArenaStats.toc +++ b/ArenaStats.toc @@ -1,16 +1,17 @@ -## Interface: 40402 -## Title: ArenaStats -## Notes: Arena history and statistics with csv export -## Author: Kallias-Sulfuron -## Version: 0.3.0 -## SavedVariables: ArenaStats - -embeds.xml -locales.xml - -ArenaStats.lua -Compat.lua -GUI.lua -HybridScrollFrame.xml -options.lua -Constants_Cata.lua \ No newline at end of file +## Interface: 20505 +## Title: ArenaStats-TBC +## Notes: Arena history and statistics with csv export +## Author: Kallias-Sulfuron +## Version: 0.3.0 +## SavedVariables: ArenaStats + +embeds.xml +locales.xml + +ArenaStats.lua +Compat.lua +GUI.lua +MatchDetails.lua +HybridScrollFrame.xml +options.lua +Constants_TBC.lua \ No newline at end of file diff --git a/ArenaStats_TBC.toc b/ArenaStats_TBC.toc index 7741b6a..3997056 100644 --- a/ArenaStats_TBC.toc +++ b/ArenaStats_TBC.toc @@ -1,16 +1,17 @@ -## Interface: 20505 -## Title: ArenaStats -## Notes: Arena history and statistics with csv export -## Author: Kallias-Sulfuron -## Version: 0.3.0 -## SavedVariables: ArenaStats - -embeds.xml -locales.xml - -ArenaStats.lua -Compat.lua -GUI.lua -HybridScrollFrame.xml -options.lua -Constants_TBC.lua +## Interface: 20505 +## Title: ArenaStats-TBC +## Notes: Arena history and statistics with csv export +## Author: Kallias-Sulfuron +## Version: 0.3.0 +## SavedVariables: ArenaStats + +embeds.xml +locales.xml + +ArenaStats.lua +Compat.lua +GUI.lua +MatchDetails.lua +HybridScrollFrame.xml +options.lua +Constants_TBC.lua diff --git a/GUI.lua b/GUI.lua index e40fb60..482080c 100644 --- a/GUI.lua +++ b/GUI.lua @@ -1,552 +1,597 @@ -local _G = _G -local addonName = "ArenaStats" -local addonTitle = select(2, (C_AddOns and C_AddOns.GetAddOnInfo or GetAddOnInfo)(addonName)) -local ArenaStats = _G.LibStub("AceAddon-3.0"):GetAddon(addonName) -local L = _G.LibStub("AceLocale-3.0"):GetLocale(addonName, true) -local AceGUI = _G.LibStub("AceGUI-3.0") -local sbyte = _G.string.byte - -local filters, asGui -local rows, filtered - --- Reusable tables to avoid garbage collection pressure during scroll refresh -local reusableTeamClassSpec = {{}, {}, {}, {}, {}} -local reusableEnemyClassSpec = {{}, {}, {}, {}, {}} -local reusableTeamPlayerNames = {} -local reusableEnemyPlayerNames = {} - -function ArenaStats:CSize(char) - if not char then - return 0 - elseif char > 240 then - return 4 - elseif char > 225 then - return 3 - elseif char > 192 then - return 2 - else - return 1 - end -end - -function ArenaStats:StrSub(str, startChar, numChars) - local startIndex = 1 - while startChar > 1 do - local char = sbyte(str, startIndex) - startIndex = startIndex + self:CSize(char) - startChar = startChar - 1 - end - local currentIndex = startIndex - while numChars > 0 and currentIndex <= #str do - local char = sbyte(str, currentIndex) - currentIndex = currentIndex + self:CSize(char) - numChars = numChars - 1 - end - return str:sub(startIndex, currentIndex - 1) -end - -function ArenaStats:CreateShortMapName(mapName) - local mapNameTemp = {strsplit(" ", mapName)} - local mapShortName = "" - for i = 1, #mapNameTemp do - mapShortName = mapShortName .. self:StrSub(mapNameTemp[i], 0, 1) - end - return mapShortName -end - -ArenaStats.mapListShortName = { - [559] = ArenaStats:CreateShortMapName(GetRealZoneText(559)), - [562] = ArenaStats:CreateShortMapName(GetRealZoneText(562)), - [572] = ArenaStats:CreateShortMapName(GetRealZoneText(572)), - [617] = ArenaStats:CreateShortMapName(GetRealZoneText(617)), - [618] = ArenaStats:CreateShortMapName(GetRealZoneText(618)) -} - -function ArenaStats:CreateGUI() - asGui = {} - filters = {} - filtered = {} - rows = {} - - filters.bracket = 0 - filters.arenaType = 0 - filters.name = "" - - asGui.f = AceGUI:Create("Frame") - asGui.f:Hide() - asGui.f:SetWidth(859) - asGui.f:EnableResize(false) - - asGui.f:SetTitle(addonTitle) - asGui.f:SetStatusText("Status Bar") - asGui.f:SetLayout("Flow") - - table.insert(_G.UISpecialFrames, "AsFrame") - _G.AsFrame = asGui.f - - local exportButton = AceGUI:Create("Button") - exportButton:SetWidth(100) - exportButton:SetText(string.format(" %s ", L["Export"])) - exportButton:SetCallback("OnClick", function() ArenaStats:ExportCSV() end) - asGui.f:AddChild(exportButton) - - local exportTool = AceGUI:Create("Button") - exportTool:SetWidth(120) - exportTool:SetText(string.format(" %s ", L["Tool Website"])) - exportTool:SetCallback("OnClick", function() ArenaStats:WebsiteURL() end) - asGui.f:AddChild(exportTool) - - local bracketSizeDropdown = AceGUI:Create("Dropdown") - bracketSizeDropdown:SetWidth(80) - bracketSizeDropdown:SetCallback("OnValueChanged", function(_, _, val) - ArenaStats:OnBracketChange(val) - end) - bracketSizeDropdown:SetList({ - [0] = _G.ALL, - [2] = "2v2", - [3] = "3v3", - [5] = "5v5" - }) - bracketSizeDropdown:SetValue(filters.bracket) - asGui.f:AddChild(bracketSizeDropdown) - - local arenaTypeDropdown = AceGUI:Create("Dropdown") - arenaTypeDropdown:SetWidth(100) - arenaTypeDropdown:SetCallback("OnValueChanged", function(_, _, val) - ArenaStats:OnArenaTypeChange(val) - end) - arenaTypeDropdown:SetList({ - [0] = _G.ALL, - [true] = _G.ARENA_RATED, - [false] = _G.ARENA_CASUAL - }) - arenaTypeDropdown:SetValue(filters.arenaType) - asGui.f:AddChild(arenaTypeDropdown) - - local nameFilter = AceGUI:Create("EditBox") - nameFilter:SetLabel(L["Filter By Name"]) - nameFilter:SetWidth(150) - nameFilter:SetCallback("OnEnterPressed", function(widget, event, text) - self:OnFilterNameChange(text) - end) - asGui.f:AddChild(nameFilter) - - -- TABLE HEADER - local tableHeader = AceGUI:Create("SimpleGroup") - tableHeader:SetFullWidth(true) - tableHeader:SetLayout("Flow") - asGui.f:AddChild(tableHeader) - - local margin = AceGUI:Create("Label") - margin:SetWidth(4) - tableHeader:AddChild(margin) - - self:CreateScoreButton(tableHeader, 145, "Date") - self:CreateScoreButton(tableHeader, 40, "Map") - self:CreateScoreButton(tableHeader, 94, "Duration") - self:CreateScoreButton(tableHeader, 100, "Team") - self:CreateScoreButton(tableHeader, 64, "Rating") - self:CreateScoreButton(tableHeader, 40, "MMR") - self:CreateScoreButton(tableHeader, 100, "Enemy Team") - self:CreateScoreButton(tableHeader, 75, "Enemy MMR") - self:CreateScoreButton(tableHeader, 80, "Enemy Faction") - - -- TABLE - local scrollContainer = AceGUI:Create("SimpleGroup") - scrollContainer:SetFullWidth(true) - scrollContainer:SetFullHeight(true) - scrollContainer:SetLayout("Fill") - asGui.f:AddChild(scrollContainer) - - asGui.scrollFrame = _G.CreateFrame("ScrollFrame", nil, - scrollContainer.frame, - "ArenaStatsHybridScrollFrame") - _G.HybridScrollFrame_CreateButtons(asGui.scrollFrame, - "ArenaStatsHybridScrollListItemTemplate") - asGui.scrollFrame.update = function() ArenaStats:UpdateTableView() end - - -- Export frame - - asGui.exportFrame = AceGUI:Create("Frame") - asGui.exportFrame:SetWidth(550) - asGui.exportFrame.sizer_se:Hide() - asGui.exportFrame:SetStatusText("") - asGui.exportFrame:SetLayout("Flow") - asGui.exportFrame:SetTitle(L["Export"]) - asGui.exportFrame:Hide() - - asGui.exportEditBox = AceGUI:Create("MultiLineEditBox") - asGui.exportEditBox:SetLabel("Export String") - asGui.exportEditBox:SetNumLines(29) - asGui.exportEditBox:SetText("") - asGui.exportEditBox:SetWidth(500) - asGui.exportEditBox.button:Hide() - asGui.exportEditBox.frame:SetClipsChildren(true) - asGui.exportFrame:AddChild(asGui.exportEditBox) - asGui.exportFrame.eb = asGui.exportEditBox -end - -function ArenaStats:UpdateTableView() self:RefreshLayout() end - -function ArenaStats:OnBracketChange(key) - filters.bracket = key - self:SortTable() - self:UpdateTableView() -end - -function ArenaStats:OnArenaTypeChange(key) - filters.arenaType = key - self:SortTable() - self:UpdateTableView() -end - -function ArenaStats:OnFilterNameChange(text) - filters.name = text - self:SortTable() - self:UpdateTableView() -end - -function ArenaStats:CreateScoreButton(tableHeader, width, localeStr) - local btn = AceGUI:Create("Label") - btn:SetWidth(width) - btn:SetText(string.format(" %s ", L[localeStr])) - btn:SetJustifyH("LEFT") - tableHeader:AddChild(btn) - local margin = AceGUI:Create("Label") - margin:SetWidth(4) - tableHeader:AddChild(margin) -end - -function ArenaStats:EnemyNameFilterRow(row) - if filters.name == "" then - return false - end - local lowerFilter = filters.name:lower() - for i = 1, 5 do - local name = row["enemyPlayerName" .. i] - if name and name:lower():find(lowerFilter, 1, true) then - return false - end - end - return true -end - -function ArenaStats:FilterRow(row) - if (filters.bracket ~= 0 and row["teamSize"] ~= filters.bracket) then - return true - end - if (filters.arenaType ~= 0 and row["isRanked"] ~= filters.arenaType) then - return true - end - if (self:EnemyNameFilterRow(row)) then - return true - end - return false -end - -function ArenaStats:SortTable() - filtered = {} - for i = 1, #rows do - local row = rows[i] - if (not self:FilterRow(row)) then table.insert(filtered, row) end - end -end - - - - -function ArenaStats:SortClassSpecTable(a, b) - -- Sort nils to the end of the list - -- Healer specs pushed to the end (before nils) - -- If no spec then sort by class as before - if not a or not b then - return not b - end - if not a.class or not b.class then - return not b.class - end - if not a.spec or not b.spec then - return a.class < b.class - end - if self:IsHealerSpec(a.spec) and not self:IsHealerSpec(b.spec) then - return false - end - if not self:IsHealerSpec(a.spec) and self:IsHealerSpec(b.spec) then - return true - end - - return a.class < b.class -end - -function ArenaStats:IsHealerSpec(spec) - return spec == "Restoration" or spec == "Discipline" or spec == "Holy" -end - --- Helper to populate reusable class/spec table -local function populateClassSpecTable(tbl, row, prefix) - for i = 1, 5 do - tbl[i].class = row[prefix .. "Class" .. i] - tbl[i].spec = row[prefix .. "Spec" .. i] - end -end - --- Helper to check if any enemies exist in the class/spec table -local function hasAnyClassOrSpec(tbl) - for i = 1, 5 do - if tbl[i].class or tbl[i].spec then - return true - end - end - return false -end - -function ArenaStats:RefreshLayout() - local buttons = _G.HybridScrollFrame_GetButtons(asGui.scrollFrame) - local offset = _G.HybridScrollFrame_GetOffset(asGui.scrollFrame) - - asGui.f:SetStatusText(string.format(L["Recorded %i arenas"], #rows)) - - for buttonIndex = 1, #buttons do - local button = buttons[buttonIndex] - local itemIndex = buttonIndex + offset - local row = filtered[itemIndex] - - if (itemIndex <= #filtered) then - button:SetID(itemIndex) - - -- Nil protection for date display - if row["endTime"] then - button.Date:SetText(_G.date(L["%F %T"], row["endTime"])) - else - button.Date:SetText("-") - end - - button.Map:SetText(self:GetShortMapName(row["zoneId"])) - button.Duration:SetText(self:HumanDuration(row["duration"])) - - -- Reuse team class/spec table instead of creating new ones - populateClassSpecTable(reusableTeamClassSpec, row, "teamPlayer") - - table.sort(reusableTeamClassSpec, function(a, b) - return self:SortClassSpecTable(a, b) - end) - - -- Populate reusable name tables for tooltip - for i = 1, 5 do - reusableTeamPlayerNames[i] = row["teamPlayerName" .. i] - reusableEnemyPlayerNames[i] = row["enemyPlayerName" .. i] - end - - -- Capture current names for this button's tooltip (needed for closure) - local tooltipTeamNames = {unpack(reusableTeamPlayerNames)} - local tooltipEnemyNames = {unpack(reusableEnemyPlayerNames)} - - button:SetScript("OnEnter", function(self) - ArenaStats:ShowTooltip(self, tooltipTeamNames, tooltipEnemyNames) - end) - button:SetScript("OnLeave", function() - ArenaStats:HideTooltip() - end) - - button.IconTeamPlayerClass1:SetTexture(self:ClassIconId(reusableTeamClassSpec[1])) - button.IconTeamPlayerClass2:SetTexture(self:ClassIconId(reusableTeamClassSpec[2])) - button.IconTeamPlayerClass3:SetTexture(self:ClassIconId(reusableTeamClassSpec[3])) - button.IconTeamPlayerClass4:SetTexture(self:ClassIconId(reusableTeamClassSpec[4])) - button.IconTeamPlayerClass5:SetTexture(self:ClassIconId(reusableTeamClassSpec[5])) - - button.Rating:SetText((row["newTeamRating"] or "-") .. " (" .. - ((row["diffRating"] and row["diffRating"] > - 0 and "+" .. row["diffRating"] or - row["diffRating"]) or "0") .. ")") - - button.Rating:SetTextColor(self:ColorForRating(row["diffRating"])) - - if (row["teamColor"] ~= nil and row["winnerColor"] ~= nil) then - if (row["teamColor"] ~= row["winnerColor"]) then - button.Rating:SetTextColor(255, 0, 0, 1) - else - button.Rating:SetTextColor(0, 255, 0, 1) - end - end - button.MMR:SetText(row["mmr"] or "-") - - -- Reuse enemy class/spec table instead of creating new ones - populateClassSpecTable(reusableEnemyClassSpec, row, "enemyPlayer") - - -- Don't sort if match ends immediately due to no enemies (otherwise gui crashes) - if hasAnyClassOrSpec(reusableEnemyClassSpec) then - table.sort(reusableEnemyClassSpec, function(a, b) - return self:SortClassSpecTable(a, b) - end) - end - - button.IconEnemyPlayer1:SetTexture(self:ClassIconId(reusableEnemyClassSpec[1])) - button.IconEnemyPlayer2:SetTexture(self:ClassIconId(reusableEnemyClassSpec[2])) - button.IconEnemyPlayer3:SetTexture(self:ClassIconId(reusableEnemyClassSpec[3])) - button.IconEnemyPlayer4:SetTexture(self:ClassIconId(reusableEnemyClassSpec[4])) - button.IconEnemyPlayer5:SetTexture(self:ClassIconId(reusableEnemyClassSpec[5])) - - button.EnemyMMR:SetText(row["enemyMmr"] or "-") - - button.EnemyFaction:SetTexture(self:FactionIconId( - row["enemyFaction"])) - - button:SetWidth(asGui.scrollFrame.scrollChild:GetWidth()) - button:Show() - else - button:Hide() - end - end - - local buttonHeight = asGui.scrollFrame.buttonHeight - local totalHeight = #filtered * buttonHeight - local shownHeight = #buttons * buttonHeight - - _G.HybridScrollFrame_Update(asGui.scrollFrame, totalHeight, shownHeight) -end - -function ArenaStats:Show() - if not _G.AsFrame then self:CreateGUI() end - - rows = self:BuildTable() - - self:SortTable() - self:RefreshLayout() - _G.AsFrame:Show() -end - -function ArenaStats:Hide() _G.AsFrame:Hide() end - -function ArenaStats:Toggle() - if _G.AsFrame and _G.AsFrame:IsShown() then - self:Hide() - else - self:Show() - end -end - -function ArenaStats:HumanDuration(seconds) - if seconds < 60 then return string.format(L["%is"], seconds) end - local minutes = math.floor(seconds / 60) - if minutes < 60 then - return string.format(L["%im %is"], minutes, (seconds - minutes * 60)) - end - local hours = math.floor(minutes / 60) - return string.format(L["%ih %im"], hours, (minutes - hours * 60)) -end - --- Spec icon lookup table: [class][spec] = iconId, with "default" for class icon -local SPEC_ICONS = { - MAGE = { - Frost = 135846, Fire = 135809, Arcane = 135932, - default = 626001 - }, - PRIEST = { - Shadow = 136207, Holy = 237542, Discipline = 135940, - default = 626004 - }, - DRUID = { - Restoration = 136041, Feral = 136112, Balance = 136096, - default = 625999 - }, - SHAMAN = { - Restoration = 136052, Elemental = 136048, Enhancement = 136051, - default = 626006 - }, - PALADIN = { - Retribution = 135873, Holy = 135920, Protection = 236264, - default = 626003 - }, - WARLOCK = { - Affliction = 136145, Demonology = 136172, Destruction = 136186, - default = 626007 - }, - WARRIOR = { - Arms = 132355, Fury = 132347, Protection = 132341, - default = 626008 - }, - HUNTER = { - BeastMastery = 461112, Marksmanship = 236179, Survival = 461113, - default = 626000 - }, - ROGUE = { - Assassination = 132292, Combat = 132090, Subtlety = 132320, - default = 626005 - }, - DEATHKNIGHT = { - Frost = 135773, Unholy = 135775, Blood = 135770, - default = 135771 - }, -} - -function ArenaStats:ClassIconId(classSpec) - if not classSpec then - return 0 - end - - local className = classSpec.class - local classIcons = SPEC_ICONS[className] - if not classIcons then - return 0 - end - - local spec = classSpec.spec - if not self.db.profile.showSpec or not spec then - return classIcons.default - end - - return classIcons[spec] or classIcons.default -end - -function ArenaStats:FactionIconId(factionId) - if not factionId then return 0 end - - if factionId == 0 then - return 132485 - else - return 132486 - end -end - -function ArenaStats:ColorForRating(rating) - if not rating or rating == 0 then return 255, 255, 255, 1 end - - if rating < 0 then - return 255, 0, 0, 1 - else - return 0, 255, 0, 1 - end -end - -function ArenaStats:GetShortMapName(id) - local name = ArenaStats.mapListShortName[id] - if name then - return name - elseif id then - return "E" .. id - else - return "E" - end -end - -function ArenaStats:ShowTooltip(owner, teamPlayerNames, enemyPlayerNames) - AceGUI.tooltip:SetOwner(owner, "ANCHOR_TOP") - AceGUI.tooltip:ClearLines() - AceGUI.tooltip:AddLine(L["Names"]) - for i, name in ipairs(teamPlayerNames) do - AceGUI.tooltip:AddLine(name, 0, 1, 0) - end - AceGUI.tooltip:AddLine('---------------') - for i, name in ipairs(enemyPlayerNames) do - AceGUI.tooltip:AddLine(name, 1, 0, 0) - end - if self:ShouldShowCharacterNamesTooltips() then - AceGUI.tooltip:Show() - end -end - -function ArenaStats:HideTooltip() AceGUI.tooltip:Hide() end - -function ArenaStats:ExportFrame() return asGui.exportFrame end - -function ArenaStats:ExportEditBox() return asGui.exportEditBox end +local _G = _G +local addonName = "ArenaStats" +local addonTitle = select(2, (C_AddOns and C_AddOns.GetAddOnInfo or GetAddOnInfo)(addonName)) +local ArenaStats = _G.LibStub("AceAddon-3.0"):GetAddon(addonName) +local L = _G.LibStub("AceLocale-3.0"):GetLocale(addonName, true) +local AceGUI = _G.LibStub("AceGUI-3.0") +local sbyte = _G.string.byte + +local filters, asGui +local rows, filtered + +-- Reusable tables to avoid garbage collection pressure during scroll refresh +local reusableTeamClassSpec = {{}, {}, {}, {}, {}} +local reusableEnemyClassSpec = {{}, {}, {}, {}, {}} +local reusableTeamPlayerNames = {} +local reusableEnemyPlayerNames = {} + +function ArenaStats:CSize(char) + if not char then + return 0 + elseif char > 240 then + return 4 + elseif char > 225 then + return 3 + elseif char > 192 then + return 2 + else + return 1 + end +end + +function ArenaStats:StrSub(str, startChar, numChars) + local startIndex = 1 + while startChar > 1 do + local char = sbyte(str, startIndex) + startIndex = startIndex + self:CSize(char) + startChar = startChar - 1 + end + local currentIndex = startIndex + while numChars > 0 and currentIndex <= #str do + local char = sbyte(str, currentIndex) + currentIndex = currentIndex + self:CSize(char) + numChars = numChars - 1 + end + return str:sub(startIndex, currentIndex - 1) +end + +function ArenaStats:CreateShortMapName(mapName) + local mapNameTemp = {strsplit(" ", mapName)} + local mapShortName = "" + for i = 1, #mapNameTemp do + mapShortName = mapShortName .. self:StrSub(mapNameTemp[i], 0, 1) + end + return mapShortName +end + +ArenaStats.mapListShortName = { + -- Hardcoded TBC Arena Maps + [559] = "NAG", + [562] = "BEA", + [572] = "ROL", +} + +function ArenaStats:CreateGUI() + asGui = {} + filters = {} + filtered = {} + rows = {} + + filters.bracket = 0 + filters.arenaType = 0 + filters.name = "" + + function ArenaStats:SetData(newRows) + rows = newRows or {} + self:SortTable() + self:UpdateTableView() + end + + asGui.f = AceGUI:Create("Frame") + asGui.f:Hide() + asGui.f:SetWidth(859) + asGui.f:EnableResize(false) + + asGui.f:SetTitle(addonTitle) + asGui.f:SetStatusText("Status Bar") + asGui.f:SetLayout("Flow") + + -- Make window opaque solid black + if asGui.f.frame.SetBackdrop then + local backdrop = asGui.f.frame:GetBackdrop() or {} + backdrop.bgFile = "Interface\\Buttons\\WHITE8X8" + asGui.f.frame:SetBackdrop(backdrop) + asGui.f.frame:SetBackdropColor(0, 0, 0, 0.85) + end + + table.insert(_G.UISpecialFrames, "AsFrame") + _G.AsFrame = asGui.f + + local exportButton = AceGUI:Create("Button") + exportButton:SetWidth(100) + exportButton:SetText(string.format(" %s ", L["Export"])) + exportButton:SetCallback("OnClick", function() ArenaStats:ExportCSV() end) + asGui.f:AddChild(exportButton) + + local exportTool = AceGUI:Create("Button") + exportTool:SetWidth(120) + exportTool:SetText(string.format(" %s ", L["Tool Website"])) + exportTool:SetCallback("OnClick", function() ArenaStats:WebsiteURL() end) + asGui.f:AddChild(exportTool) + + local bracketSizeDropdown = AceGUI:Create("Dropdown") + bracketSizeDropdown:SetWidth(80) + bracketSizeDropdown:SetCallback("OnValueChanged", function(_, _, val) + ArenaStats:OnBracketChange(val) + end) + bracketSizeDropdown:SetList({ + [0] = _G.ALL, + [2] = "2v2", + [3] = "3v3", + [5] = "5v5" + }) + bracketSizeDropdown:SetValue(filters.bracket) + asGui.f:AddChild(bracketSizeDropdown) + + local arenaTypeDropdown = AceGUI:Create("Dropdown") + arenaTypeDropdown:SetWidth(100) + arenaTypeDropdown:SetCallback("OnValueChanged", function(_, _, val) + ArenaStats:OnArenaTypeChange(val) + end) + arenaTypeDropdown:SetList({ + [0] = _G.ALL, + [true] = _G.ARENA_RATED, + [false] = _G.ARENA_CASUAL + }) + arenaTypeDropdown:SetValue(filters.arenaType) + asGui.f:AddChild(arenaTypeDropdown) + + local nameFilter = AceGUI:Create("EditBox") + nameFilter:SetLabel(L["Filter By Name"]) + nameFilter:SetWidth(150) + nameFilter:SetCallback("OnEnterPressed", function(widget, event, text) + self:OnFilterNameChange(text) + end) + asGui.f:AddChild(nameFilter) + + -- TABLE HEADER + local tableHeader = AceGUI:Create("SimpleGroup") + tableHeader:SetFullWidth(true) + tableHeader:SetLayout("Flow") + asGui.f:AddChild(tableHeader) + + local margin = AceGUI:Create("Label") + margin:SetWidth(4) + tableHeader:AddChild(margin) + + self:CreateScoreButton(tableHeader, 145, "Date") + self:CreateScoreButton(tableHeader, 40, "Map") + self:CreateScoreButton(tableHeader, 94, "Duration") + self:CreateScoreButton(tableHeader, 100, "Team") + self:CreateScoreButton(tableHeader, 64, "Rating") + self:CreateScoreButton(tableHeader, 40, "MMR") + self:CreateScoreButton(tableHeader, 100, "Enemy Team") + self:CreateScoreButton(tableHeader, 75, "Enemy MMR") + self:CreateScoreButton(tableHeader, 80, "Enemy Faction") + self:CreateScoreButton(tableHeader, 25, "") + + -- TABLE + local scrollContainer = AceGUI:Create("SimpleGroup") + scrollContainer:SetFullWidth(true) + scrollContainer:SetFullHeight(true) + scrollContainer:SetLayout("Fill") + asGui.f:AddChild(scrollContainer) + + asGui.scrollFrame = _G.CreateFrame("ScrollFrame", nil, + scrollContainer.frame, + "ArenaStatsHybridScrollFrame") + _G.HybridScrollFrame_CreateButtons(asGui.scrollFrame, + "ArenaStatsHybridScrollListItemTemplate") + asGui.scrollFrame.update = function() ArenaStats:UpdateTableView() end + + -- Export frame + + asGui.exportFrame = AceGUI:Create("Frame") + asGui.exportFrame:SetWidth(550) + asGui.exportFrame.sizer_se:Hide() + asGui.exportFrame:SetStatusText("") + asGui.exportFrame:SetLayout("Flow") + asGui.exportFrame:SetTitle(L["Export"]) + asGui.exportFrame:Hide() + + asGui.exportEditBox = AceGUI:Create("MultiLineEditBox") + asGui.exportEditBox:SetLabel("Export String") + asGui.exportEditBox:SetNumLines(29) + asGui.exportEditBox:SetText("") + asGui.exportEditBox:SetWidth(500) + asGui.exportEditBox.button:Hide() + asGui.exportEditBox.frame:SetClipsChildren(true) + asGui.exportFrame:AddChild(asGui.exportEditBox) + asGui.exportFrame.eb = asGui.exportEditBox +end + +function ArenaStats:UpdateTableView() self:RefreshLayout() end + +function ArenaStats:OnBracketChange(key) + filters.bracket = key + self:SortTable() + self:UpdateTableView() +end + +function ArenaStats:OnArenaTypeChange(key) + filters.arenaType = key + self:SortTable() + self:UpdateTableView() +end + +function ArenaStats:OnFilterNameChange(text) + filters.name = text + self:SortTable() + self:UpdateTableView() +end + +function ArenaStats:CreateScoreButton(tableHeader, width, localeStr) + local btn = AceGUI:Create("Label") + btn:SetWidth(width) + local text = "" + if localeStr and localeStr ~= "" then + text = L[localeStr] + end + btn:SetText(string.format(" %s ", text)) + btn:SetJustifyH("LEFT") + tableHeader:AddChild(btn) + local margin = AceGUI:Create("Label") + margin:SetWidth(4) + tableHeader:AddChild(margin) +end + +function ArenaStats:EnemyNameFilterRow(row) + if filters.name == "" then + return false + end + local lowerFilter = filters.name:lower() + for i = 1, 5 do + local name = row["enemyPlayerName" .. i] + if name and name:lower():find(lowerFilter, 1, true) then + return false + end + end + return true +end + +function ArenaStats:FilterRow(row) + if (filters.bracket ~= 0 and row["teamSize"] ~= filters.bracket) then + return true + end + if (filters.arenaType ~= 0 and row["isRanked"] ~= filters.arenaType) then + return true + end + if (self:EnemyNameFilterRow(row)) then + return true + end + return false +end + +function ArenaStats:SortTable() + filtered = {} + for i = 1, #rows do + local row = rows[i] + if (not self:FilterRow(row)) then table.insert(filtered, row) end + end +end + + + + +function ArenaStats:SortClassSpecTable(a, b) + -- Sort nils to the end of the list + -- Healer specs pushed to the end (before nils) + -- If no spec then sort by class as before +function ArenaStats:SortClassSpecTable(a, b) + -- Sort nils to the end of the list + if not a or not a.class then return false end + if not b or not b.class then return true end + + -- Safe spec sort + local specA = a.spec + local specB = b.spec + + -- Healer check + local isHealerA = self:IsHealerSpec(specA) + local isHealerB = self:IsHealerSpec(specB) + + if isHealerA and not isHealerB then return false end + if not isHealerA and isHealerB then return true end + + -- Default to class comparison + return a.class < b.class +end +end + +function ArenaStats:IsHealerSpec(spec) + return spec == "Restoration" or spec == "Discipline" or spec == "Holy" +end + +-- Helper to populate reusable class/spec table +local function populateClassSpecTable(tbl, row, prefix) + for i = 1, 5 do + if not tbl[i] then tbl[i] = {} end + tbl[i].class = row[prefix .. "Class" .. i] + tbl[i].spec = row[prefix .. "Spec" .. i] + end +end + +-- Helper to check if any enemies exist in the class/spec table +local function hasAnyClassOrSpec(tbl) + for i = 1, 5 do + if tbl[i].class or tbl[i].spec then + return true + end + end + return false +end + +function ArenaStats:RefreshLayout() + local buttons = _G.HybridScrollFrame_GetButtons(asGui.scrollFrame) + local offset = _G.HybridScrollFrame_GetOffset(asGui.scrollFrame) + + asGui.f:SetStatusText(string.format(L["Recorded %i arenas"], #rows)) + + for buttonIndex = 1, #buttons do + local button = buttons[buttonIndex] + local itemIndex = buttonIndex + offset + local row = filtered[itemIndex] + + if (itemIndex <= #filtered) then + button:SetID(itemIndex) + + -- Nil protection for date display + if row["endTime"] then + button.Date:SetText(_G.date(L["%F %T"], row["endTime"])) + else + button.Date:SetText("-") + end + + button.Map:SetText(self:GetShortMapName(row["zoneId"])) + button.Duration:SetText(self:HumanDuration(row["duration"])) + + -- Reuse team class/spec table instead of creating new ones + populateClassSpecTable(reusableTeamClassSpec, row, "teamPlayer") + + table.sort(reusableTeamClassSpec, function(a, b) + return self:SortClassSpecTable(a, b) + end) + + -- Populate reusable name tables for tooltip + for i = 1, 5 do + reusableTeamPlayerNames[i] = row["teamPlayerName" .. i] + reusableEnemyPlayerNames[i] = row["enemyPlayerName" .. i] + end + + -- Capture current names for this button's tooltip (needed for closure) + local tooltipTeamNames = {unpack(reusableTeamPlayerNames)} + local tooltipEnemyNames = {unpack(reusableEnemyPlayerNames)} + + button:SetScript("OnEnter", function(self) + ArenaStats:ShowTooltip(self, tooltipTeamNames, tooltipEnemyNames) + end) + button:SetScript("OnLeave", function() + ArenaStats:HideTooltip() + end) + + button.IconTeamPlayerClass1:SetTexture(self:ClassIconId(reusableTeamClassSpec[1])) + button.IconTeamPlayerClass2:SetTexture(self:ClassIconId(reusableTeamClassSpec[2])) + button.IconTeamPlayerClass3:SetTexture(self:ClassIconId(reusableTeamClassSpec[3])) + button.IconTeamPlayerClass4:SetTexture(self:ClassIconId(reusableTeamClassSpec[4])) + button.IconTeamPlayerClass5:SetTexture(self:ClassIconId(reusableTeamClassSpec[5])) + + button.Rating:SetText((row["newTeamRating"] or "-") .. " (" .. + ((row["diffRating"] and row["diffRating"] > + 0 and "+" .. row["diffRating"] or + row["diffRating"]) or "0") .. ")") + + button.Rating:SetTextColor(self:ColorForRating(row["diffRating"])) + + if (row["teamColor"] ~= nil and row["winnerColor"] ~= nil) then + if (row["teamColor"] ~= row["winnerColor"]) then + button.Rating:SetTextColor(255, 0, 0, 1) + else + button.Rating:SetTextColor(0, 255, 0, 1) + end + end + button.MMR:SetText(row["mmr"] or "-") + + -- Reuse enemy class/spec table instead of creating new ones + populateClassSpecTable(reusableEnemyClassSpec, row, "enemyPlayer") + + -- Don't sort if match ends immediately due to no enemies (otherwise gui crashes) + if hasAnyClassOrSpec(reusableEnemyClassSpec) then + table.sort(reusableEnemyClassSpec, function(a, b) + return self:SortClassSpecTable(a, b) + end) + end + + button.IconEnemyPlayer1:SetTexture(self:ClassIconId(reusableEnemyClassSpec[1])) + button.IconEnemyPlayer2:SetTexture(self:ClassIconId(reusableEnemyClassSpec[2])) + button.IconEnemyPlayer3:SetTexture(self:ClassIconId(reusableEnemyClassSpec[3])) + button.IconEnemyPlayer4:SetTexture(self:ClassIconId(reusableEnemyClassSpec[4])) + button.IconEnemyPlayer5:SetTexture(self:ClassIconId(reusableEnemyClassSpec[5])) + + button.EnemyMMR:SetText(row["enemyMmr"] or "-") + + button.EnemyFaction:SetTexture(self:FactionIconId( + row["enemyFaction"])) + + -- Create or Update Delete Button + -- Create or Update Delete Button + if not button.Delete then + button.Delete = CreateFrame("Button", nil, button) + button.Delete:SetSize(16, 16) + -- Moved 40px to the right as requested + button.Delete:SetPoint("LEFT", button.EnemyFaction, "RIGHT", 40, 0) + button.Delete:SetNormalTexture("Interface\\Buttons\\UI-GroupLoot-Pass-Up") + button.Delete:SetHighlightTexture("Interface\\Buttons\\UI-GroupLoot-Pass-Highlight") + button.Delete:SetPushedTexture("Interface\\Buttons\\UI-GroupLoot-Pass-Down") + -- Ensure button intercepts clicks + button.Delete:EnableMouse(true) + button.Delete:SetFrameLevel(button:GetFrameLevel() + 5) + end + button.Delete:SetScript("OnClick", function() + ArenaStats:DeleteEntry(row) + end) + button.Delete:Show() + + button:EnableMouse(true) + button:SetScript("OnClick", function() + ArenaStats:ShowMatchDetails(row) + end) + + button:SetWidth(asGui.scrollFrame.scrollChild:GetWidth()) + button:Show() + else + button:Hide() + end + end + + local buttonHeight = asGui.scrollFrame.buttonHeight + local totalHeight = #filtered * buttonHeight + local shownHeight = #buttons * buttonHeight + + _G.HybridScrollFrame_Update(asGui.scrollFrame, totalHeight, shownHeight) +end + +function ArenaStats:Show() + if not _G.AsFrame then self:CreateGUI() end + + rows = self:BuildTable() + + self:SortTable() + self:RefreshLayout() + _G.AsFrame:Show() +end + +function ArenaStats:Hide() _G.AsFrame:Hide() end + +function ArenaStats:Toggle() + if _G.AsFrame and _G.AsFrame:IsShown() then + self:Hide() + else + self:Show() + end +end + +function ArenaStats:HumanDuration(seconds) + if seconds < 60 then return string.format(L["%is"], seconds) end + local minutes = math.floor(seconds / 60) + if minutes < 60 then + return string.format(L["%im %is"], minutes, (seconds - minutes * 60)) + end + local hours = math.floor(minutes / 60) + return string.format(L["%ih %im"], hours, (minutes - hours * 60)) +end + +function ArenaStats:ClassIconId(classSpec) + if not classSpec then + return nil + end + + local spec = classSpec.spec + local className = classSpec.class + + if className == "MAGE" then + if spec == "Frost" then return "Interface\\Icons\\Spell_Frost_FrostBolt02" end + if spec == "Fire" then return "Interface\\Icons\\Spell_Fire_FireBolt02" end + if spec == "Arcane" then return "Interface\\Icons\\Spell_Holy_MagicalSentry" end + return "Interface\\Icons\\ClassIcon_Mage" + elseif className == "PRIEST" then + if spec == "Shadow" then return "Interface\\Icons\\Spell_Shadow_ShadowWordPain" end + if spec == "Holy" then return "Interface\\Icons\\Spell_Holy_HolyBolt" end + if spec == "Discipline" then return "Interface\\Icons\\Spell_Holy_WordFortitude" end + return "Interface\\Icons\\ClassIcon_Priest" + elseif className == "DRUID" then + if spec == "Restoration" then return "Interface\\Icons\\Spell_Nature_HealingTouch" end + if spec == "Feral" then return "Interface\\Icons\\Ability_Racial_BearForm" end + if spec == "Balance" then return "Interface\\Icons\\Spell_Nature_StarFall" end + return "Interface\\Icons\\ClassIcon_Druid" + elseif className == "SHAMAN" then + if spec == "Restoration" then return "Interface\\Icons\\Spell_Nature_HealingWaveGreater" end + if spec == "Elemental" then return "Interface\\Icons\\Spell_Nature_Lightning" end + if spec == "Enhancement" then return "Interface\\Icons\\Spell_Nature_LightningShield" end + return "Interface\\Icons\\ClassIcon_Shaman" + elseif className == "PALADIN" then + if spec == "Retribution" then return "Interface\\Icons\\Spell_Holy_AuraOfLight" end + if spec == "Holy" then return "Interface\\Icons\\Spell_Holy_HolyBolt" end + if spec == "Protection" then return "Interface\\Icons\\Spell_Holy_DevotionAura" end + return "Interface\\Icons\\ClassIcon_Paladin" + elseif className == "WARLOCK" then + if spec == "Affliction" then return "Interface\\Icons\\Spell_Shadow_DeathCoil" end + if spec == "Demonology" then return "Interface\\Icons\\Spell_Shadow_SummonFelGuard" end + if spec == "Destruction" then return "Interface\\Icons\\Spell_Shadow_RainOfFire" end + return "Interface\\Icons\\ClassIcon_Warlock" + elseif className == "WARRIOR" then + if spec == "Arms" then return "Interface\\Icons\\Ability_Warrior_SavageBlow" end + if spec == "Fury" then return "Interface\\Icons\\Spell_Nature_BloodLust" end + if spec == "Protection" then return "Interface\\Icons\\Ability_Warrior_DefensiveStance" end + return "Interface\\Icons\\ClassIcon_Warrior" + elseif className == "HUNTER" then + if spec == "BeastMastery" then return "Interface\\Icons\\Ability_Hunter_BeastTaming" end + if spec == "Marksmanship" then return "Interface\\Icons\\Ability_Marksmanship" end + if spec == "Survival" then return "Interface\\Icons\\Ability_Hunter_SwiftStrike" end + return "Interface\\Icons\\ClassIcon_Hunter" + elseif className == "ROGUE" then + if spec == "Assassination" then return "Interface\\Icons\\Ability_Rogue_Eviscerate" end + if spec == "Combat" then return "Interface\\Icons\\Ability_BackStab" end + if spec == "Subtlety" then return "Interface\\Icons\\Ability_Stealth" end + return "Interface\\Icons\\ClassIcon_Rogue" + elseif className == "DEATHKNIGHT" then + return "Interface\\Icons\\Spell_DeathKnight_ClassIcon" + end + return nil +end + +function ArenaStats:FactionIconId(factionId) + if not factionId then return nil end + + if factionId == 0 then + return "Interface\\Icons\\Inv_BannerPVP_01" + else + return "Interface\\Icons\\Inv_BannerPVP_02" + end +end + +function ArenaStats:ColorForRating(rating) + if not rating or rating == 0 then return 255, 255, 255, 1 end + + if rating < 0 then + return 255, 0, 0, 1 + else + return 0, 255, 0, 1 + end +end + +function ArenaStats:GetShortMapName(id) + if id == 559 then return "NAG" end + if id == 562 then return "BEA" end + if id == 572 then return "ROL" end + + local name = ArenaStats.mapListShortName[id] + if name then + return name + elseif id then + return "E" .. id + else + return "E" + end +end + +function ArenaStats:ShowTooltip(owner, teamPlayerNames, enemyPlayerNames) + AceGUI.tooltip:SetOwner(owner, "ANCHOR_TOP") + AceGUI.tooltip:ClearLines() + AceGUI.tooltip:AddLine(L["Names"]) + for i, name in ipairs(teamPlayerNames) do + AceGUI.tooltip:AddLine(name, 0, 1, 0) + end + AceGUI.tooltip:AddLine('---------------') + for i, name in ipairs(enemyPlayerNames) do + AceGUI.tooltip:AddLine(name, 1, 0, 0) + end + if self:ShouldShowCharacterNamesTooltips() then + AceGUI.tooltip:Show() + end +end + +function ArenaStats:HideTooltip() AceGUI.tooltip:Hide() end + +function ArenaStats:ExportFrame() return asGui.exportFrame end + +function ArenaStats:ExportEditBox() return asGui.exportEditBox end diff --git a/MatchDetails.lua b/MatchDetails.lua new file mode 100644 index 0000000..ea943a6 --- /dev/null +++ b/MatchDetails.lua @@ -0,0 +1,223 @@ + +local addonName = "ArenaStats" +local ArenaStats = LibStub("AceAddon-3.0"):GetAddon(addonName) +local L = LibStub("AceLocale-3.0"):GetLocale(addonName, true) +local AceGUI = LibStub("AceGUI-3.0") + +local asGui = ArenaStats.asGui or {} -- Ensure asGui reference exists or retrieve it correctly later (it's actually local in GUI.lua, we need to handle that) +-- Wait, 'asGui' is local in GUI.lua! I cannot access it directly here unless I expose it or attach it to ArenaStats. +-- In GUI.lua: 'local filters, asGui' +-- In GUI.lua: 'function ArenaStats:CreateGUI() asGui = {} ...' +-- It seems I need to access the main frame or storage. +-- Let's check GUI.lua again. 'asGui' is file-local. But ArenaStats:CreateGUI saves 'asGui.f' to _G.AsFrame. +-- I should attach 'detailsFrame' to the ArenaStats object or use a property. +-- Better approach: ArenaStats.detailsFrame to store the frame reference. + + + +function ArenaStats:ShowMatchDetails(row) + if not self.detailsFrame then + local frame = AceGUI:Create("Frame") + frame:SetTitle(L["Match Details"]) + frame:SetLayout("Flow") + frame:EnableResize(false) + + -- Make window opaque solid black + if frame.frame.SetBackdrop then + local backdrop = frame.frame:GetBackdrop() or {} + backdrop.bgFile = "Interface\\Buttons\\WHITE8X8" + frame.frame:SetBackdrop(backdrop) + frame.frame:SetBackdropColor(0, 0, 0, 0.85) + end + + self.detailsFrame = frame + end + + local f = self.detailsFrame + f:ReleaseChildren() + + -- Calculate dynamic height: Header(70) + TeamHeaders(40*2) + Rows(25 * numPlayers * 2) + Padding(100) + local teamSize = row["teamSize"] or 2 + local rowHeight = 25 + local baseHeight = 270 -- Increased from 220 + local totalHeight = baseHeight + (teamSize * 2 * rowHeight) + + f:SetWidth(700) + f:SetHeight(totalHeight) + + -- Anchor to the right of the main window if possible + f:ClearAllPoints() + if _G.AsFrame and _G.AsFrame.frame and _G.AsFrame.frame:IsShown() then + f:SetPoint("TOPLEFT", _G.AsFrame.frame, "TOPRIGHT", 5, 0) -- 5px padding + else + f:SetPoint("CENTER") + end + + -- Hide the bottom status bar as requested + if f.statusbg then f.statusbg:Hide() end + if f.statustext then f.statustext:Hide() end + + f:Show() + + -- [HEADER] Map Info + local headerGroup = AceGUI:Create("SimpleGroup") + headerGroup:SetFullWidth(true) + headerGroup:SetLayout("Flow") + f:AddChild(headerGroup) + + local function GetFullMapName(id) + if id == 559 then return "Nagrand Arena" end + if id == 562 then return "Blade's Edge Arena" end + if id == 572 then return "Ruins of Lordaeron" end + return "Unknown Arena (" .. (id or "?") .. ")" + end + + local mapName = GetFullMapName(row["zoneId"]) + local duration = self:HumanDuration(row["duration"]) + local dateStr = row["endTime"] and _G.date(L["%F %T"], row["endTime"]) or "-" + + local infoText = AceGUI:Create("Label") + infoText:SetFullWidth(true) + infoText:SetText(string.format("Map: %s Duration: %s Date: %s", mapName, duration, dateStr)) + infoText:SetFont("Fonts\\FRIZQT__.TTF", 14, "OUTLINE") + infoText:SetColor(1, 0.82, 0) -- Gold color + headerGroup:AddChild(infoText) + + -- Shared function for table headers + local function AddTableHeader(container) + local grp = AceGUI:Create("SimpleGroup") + grp:SetFullWidth(true) + grp:SetLayout("Flow") + + local function AddCol(text, width) + local lbl = AceGUI:Create("Label") + lbl:SetText(text) + lbl:SetWidth(width) + lbl:SetColor(1, 0.82, 0) -- Gold + lbl:SetFont("Fonts\\FRIZQT__.TTF", 12, "OUTLINE") + grp:AddChild(lbl) + end + + AddCol("", 25) -- Icon + AddCol(L["Name"], 130) + AddCol(L["Damage"], 80) + AddCol(L["Healing"], 80) + AddCol(L["Rating"], 100) + AddCol(L["MMR"], 50) + + container:AddChild(grp) + end + + -- Shared function for player row + local function AddPlayerRow(container, name, class, spec, race, dmg, heal, ratingStr, mmr, diffRating) + local grp = AceGUI:Create("SimpleGroup") + grp:SetFullWidth(true) + grp:SetLayout("Flow") + + -- Icon + local icon = AceGUI:Create("Icon") + icon:SetImage(self:ClassIconId({class=class, spec=spec}) or "Interface\\Icons\\Inv_Misc_QuestionMark") + icon:SetImageSize(18, 18) + icon:SetWidth(25) + grp:AddChild(icon) + + -- Name (%s %s) -> Name (ClassColor) + local label = AceGUI:Create("Label") + local classColor = _G.RAID_CLASS_COLORS[class] or {r=1, g=1, b=1} + label:SetText(name or "Unknown") + label:SetColor(classColor.r, classColor.g, classColor.b) + label:SetWidth(130) + grp:AddChild(label) + + -- Damage + local dmgLabel = AceGUI:Create("Label") + dmgLabel:SetText(dmg or "-") + dmgLabel:SetWidth(80) + grp:AddChild(dmgLabel) + + -- Healing + local healLabel = AceGUI:Create("Label") + healLabel:SetText(heal or "-") + healLabel:SetWidth(80) + grp:AddChild(healLabel) + + -- Rating + local ratingLabel = AceGUI:Create("Label") + ratingLabel:SetText(ratingStr or "-") + ratingLabel:SetWidth(100) + + -- Color logic based on diffRating + local dr = diffRating or 0 + if dr > 0 then + ratingLabel:SetColor(0, 1, 0) -- Green + elseif dr < 0 then + ratingLabel:SetColor(1, 0, 0) -- Red + else + ratingLabel:SetColor(1, 1, 1) -- White + end + + grp:AddChild(ratingLabel) + + -- MMR + local mmrLabel = AceGUI:Create("Label") + mmrLabel:SetText(mmr or "-") + mmrLabel:SetWidth(50) + grp:AddChild(mmrLabel) + + container:AddChild(grp) + end + + -- Format Rating String: "1500 (+12)" + local function GetRatingStr(new, diff) + local d = diff or 0 + local sign = (d > 0) and "+" or "" + return string.format("%s (%s%s)", new or "-", sign, d) + end + + -- [MY TEAM] + local teamHeader = AceGUI:Create("Heading") + teamHeader:SetText(L["Team"] .. " (" .. (row["teamName"] or "Unknown") .. ")") + teamHeader:SetFullWidth(true) + f:AddChild(teamHeader) + + AddTableHeader(f) + + local myRatingStr = GetRatingStr(row["newTeamRating"], row["diffRating"]) + local myMMR = row["mmr"] + local myDiff = row["diffRating"] + + for i = 1, 5 do + if row["teamPlayerClass" .. i] then + AddPlayerRow(f, row["teamPlayerName" .. i], row["teamPlayerClass" .. i], + row["teamPlayerSpec" .. i], row["teamPlayerRace" .. i], + row["teamPlayerDamage" .. i], row["teamPlayerHealing" .. i], + myRatingStr, myMMR, myDiff) + end + end + + -- [ENEMY TEAM] + local space = AceGUI:Create("Label") + space:SetText(" ") + space:SetFullWidth(true) + f:AddChild(space) + + local enemyHeader = AceGUI:Create("Heading") + enemyHeader:SetText(L["Enemy Team"] .. " (" .. (row["enemyTeamName"] or "Unknown") .. ")") + enemyHeader:SetFullWidth(true) + f:AddChild(enemyHeader) + + AddTableHeader(f) + + local enemyRatingStr = GetRatingStr(row["enemyNewTeamRating"], row["enemyDiffRating"]) + local enemyMMR = row["enemyMmr"] + local enemyDiff = row["enemyDiffRating"] + + for i = 1, 5 do + if row["enemyPlayerClass" .. i] then + AddPlayerRow(f, row["enemyPlayerName" .. i], row["enemyPlayerClass" .. i], + row["enemyPlayerSpec" .. i], row["enemyPlayerRace" .. i], + row["enemyPlayerDamage" .. i], row["enemyPlayerHealing" .. i], + enemyRatingStr, enemyMMR, enemyDiff) + end + end +end diff --git a/enUS.lua b/enUS.lua new file mode 100644 index 0000000..15111e2 --- /dev/null +++ b/enUS.lua @@ -0,0 +1,49 @@ +local L = LibStub("AceLocale-3.0"):NewLocale("ArenaStats", "enUS", true) +if not L then return end + +L["%F %T"] = true +L["%ih %im"] = true +L["%im %is"] = true +L["%is"] = true +L["Battlegrounds records can impact memory usage (0 means unlimited)"] = true +L["Database reset"] = true +L["Database Settings"] = true +L["Date"] = true +L["Delete all collected data"] = true +L["Drag"] = true +L["Duration"] = true +L["Enemy Faction"] = true +L["Enemy MMR"] = true +L["Enemy Team"] = true +L["Export"] = true +L["Interface Settings"] = true +L["Left Click"] = true +L["Map"] = true +L["Maximum history records"] = true +L["Minimap Button Settings"] = true +L["MMR"] = true +L["Names"] = true +L["Opens or closes the main window"] = true +L["Options"] = true +L["Purge database"] = true +L["Rating"] = true +L["Recorded %i arenas"] = true +L["Right Click"] = true +L["Show character names on hover"] = true +L["Show minimap button"] = true +L["Team"] = true +L["Tool"] = true +L["Tool Website"] = true +L["to open the main window"] = true +L["to open options"] = true +L["to move this button"] = true +L["Toggle"] = true +L["Filter By Name"] = true +L["Show specialization"] = true +L["Spec Detection"] = true +L["Generate Test Data"] = true +L["Generates a dummy arena record for testing purposes"] = true +L["Match Details"] = true +L["Name"] = true +L["Damage"] = true +L["Healing"] = true \ No newline at end of file diff --git a/options.lua b/options.lua index edc9ce4..fb8b0d6 100644 --- a/options.lua +++ b/options.lua @@ -1,127 +1,137 @@ -local addonName = "ArenaStats" -local _, addonTitle, addonNotes = (C_AddOns and C_AddOns.GetAddOnInfo or GetAddOnInfo)(addonName) -local ArenaStats = LibStub("AceAddon-3.0"):GetAddon(addonName) -local L = LibStub("AceLocale-3.0"):GetLocale(addonName, true) -local AceConfig = LibStub("AceConfig-3.0") -local AceDBOptions = LibStub("AceDBOptions-3.0") -local AceConfigDialog = LibStub("AceConfigDialog-3.0") - -function ArenaStats:RegisterOptionsTable() - AceConfig:RegisterOptionsTable(addonName, { - name = addonName, - descStyle = "inline", - handler = ArenaStats, - type = "group", - args = { - Toggle = { - order = 0, - type = "execute", - name = L["Toggle"], - desc = L["Opens or closes the main window"], - func = function() self:Toggle() end - }, - General = { - order = 1, - type = "group", - name = L["Options"], - args = { - intro = { order = 0, type = "description", name = addonNotes }, - group1 = { - order = 10, - type = "group", - name = L["Database Settings"], - inline = true, - args = { - maxHistory = { - order = 11, - type = "range", - name = L["Maximum history records"], - desc = L["Battlegrounds records can impact memory usage (0 means unlimited)"], - min = 0, - max = 1000, - step = 10, - get = function() - return self.db.profile.maxHistory - end, - set = function(_, val) - self.db.profile.maxHistory = val - end - }, - purge = { - order = 19, - type = "execute", - name = L["Purge database"], - desc = L["Delete all collected data"], - confirm = true, - func = function() - self:ResetDatabase() - end - } - } - }, - group2 = { - order = 20, - type = "group", - name = L["Minimap Button Settings"], - inline = true, - args = { - minimapButton = { - order = 21, - type = "toggle", - name = L["Show minimap button"], - get = function() - return - not self.db.profile.minimapButton.hide - end, - set = 'ToggleMinimapButton' - } - } - }, - group3 = { - order = 30, - type = "group", - name = L["Interface Settings"], - inline = true, - args = { - showCharacterNamesOnHover = { - order = 31, - type = "toggle", - name = L["Show character names on hover"], - get = function() - return self.db.profile.showCharacterNamesOnHover - end, - set = function(_, val) - self.db.profile.showCharacterNamesOnHover = val - end - } - } - }, - group4 = { - order = 40, - type = "group", - name = L["Spec Detection"], - inline = true, - args = { - showSpec = { - order = 41, - type = "toggle", - name = L["Show specialization"], - get = function() - return self.db.profile.showSpec - end, - set = function(_, val) - self.db.profile.showSpec = val - end - } - } - } - } - }, - Profiles = AceDBOptions:GetOptionsTable(ArenaStats.db) - } - }, { "arenastats", "as" }) - AceConfigDialog:AddToBlizOptions(addonName, nil, nil, "General") - - AceConfigDialog:AddToBlizOptions(addonName, "Profiles", addonName, - "Profiles") -end +local addonName = "ArenaStats" +local _, addonTitle, addonNotes = (C_AddOns and C_AddOns.GetAddOnInfo or GetAddOnInfo)(addonName) +local ArenaStats = LibStub("AceAddon-3.0"):GetAddon(addonName) +local L = LibStub("AceLocale-3.0"):GetLocale(addonName, true) +local AceConfig = LibStub("AceConfig-3.0") +local AceDBOptions = LibStub("AceDBOptions-3.0") +local AceConfigDialog = LibStub("AceConfigDialog-3.0") + +function ArenaStats:RegisterOptionsTable() + AceConfig:RegisterOptionsTable(addonName, { + name = "ArenaStats-TBC", + descStyle = "inline", + handler = ArenaStats, + type = "group", + args = { + Toggle = { + order = 0, + type = "execute", + name = L["Toggle"], + desc = L["Opens or closes the main window"], + func = function() self:Toggle() end + }, + General = { + order = 1, + type = "group", + name = L["Options"], + args = { + intro = { order = 0, type = "description", name = addonNotes }, + group1 = { + order = 10, + type = "group", + name = L["Database Settings"], + inline = true, + args = { + maxHistory = { + order = 11, + type = "range", + name = L["Maximum history records"], + desc = L["Battlegrounds records can impact memory usage (0 means unlimited)"], + min = 0, + max = 1000, + step = 10, + get = function() + return self.db.profile.maxHistory + end, + set = function(_, val) + self.db.profile.maxHistory = val + end + }, + purge = { + order = 19, + type = "execute", + name = L["Purge database"], + desc = L["Delete all collected data"], + confirm = true, + func = function() + self:ResetDatabase() + self:ReloadData() + end + }, + testData = { + order = 20, + type = "execute", + name = L["Generate Test Data"], + desc = L["Generates a dummy arena record for testing purposes"], + func = function() + self:TestData() + end + } + } + }, + group2 = { + order = 20, + type = "group", + name = L["Minimap Button Settings"], + inline = true, + args = { + minimapButton = { + order = 21, + type = "toggle", + name = L["Show minimap button"], + get = function() + return + not self.db.profile.minimapButton.hide + end, + set = 'ToggleMinimapButton' + } + } + }, + group3 = { + order = 30, + type = "group", + name = L["Interface Settings"], + inline = true, + args = { + showCharacterNamesOnHover = { + order = 31, + type = "toggle", + name = L["Show character names on hover"], + get = function() + return self.db.profile.showCharacterNamesOnHover + end, + set = function(_, val) + self.db.profile.showCharacterNamesOnHover = val + end + } + } + }, + group4 = { + order = 40, + type = "group", + name = L["Spec Detection"], + inline = true, + args = { + showSpec = { + order = 41, + type = "toggle", + name = L["Show specialization"], + get = function() + return self.db.profile.showSpec + end, + set = function(_, val) + self.db.profile.showSpec = val + end + } + } + } + } + }, + Profiles = AceDBOptions:GetOptionsTable(ArenaStats.db) + } + }, { "arenastats", "as" }) + AceConfigDialog:AddToBlizOptions(addonName, nil, nil, "General") + + AceConfigDialog:AddToBlizOptions(addonName, "Profiles", addonName, + "Profiles") +end From 3d7728d55dfcb2c27ee6062b104343ce386a5097 Mon Sep 17 00:00:00 2001 From: Mateo Date: Mon, 2 Feb 2026 11:27:46 -0300 Subject: [PATCH 2/3] ArenaStats TBC + Improvements --- Locales/enUS.lua | 92 ++++++++++++++++++++++++++---------------------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/Locales/enUS.lua b/Locales/enUS.lua index 871413b..15111e2 100644 --- a/Locales/enUS.lua +++ b/Locales/enUS.lua @@ -1,43 +1,49 @@ -local L = LibStub("AceLocale-3.0"):NewLocale("ArenaStats", "enUS", true) -if not L then return end - -L["%F %T"] = true -L["%ih %im"] = true -L["%im %is"] = true -L["%is"] = true -L["Battlegrounds records can impact memory usage (0 means unlimited)"] = true -L["Database reset"] = true -L["Database Settings"] = true -L["Date"] = true -L["Delete all collected data"] = true -L["Drag"] = true -L["Duration"] = true -L["Enemy Faction"] = true -L["Enemy MMR"] = true -L["Enemy Team"] = true -L["Export"] = true -L["Interface Settings"] = true -L["Left Click"] = true -L["Map"] = true -L["Maximum history records"] = true -L["Minimap Button Settings"] = true -L["MMR"] = true -L["Names"] = true -L["Opens or closes the main window"] = true -L["Options"] = true -L["Purge database"] = true -L["Rating"] = true -L["Recorded %i arenas"] = true -L["Right Click"] = true -L["Show character names on hover"] = true -L["Show minimap button"] = true -L["Team"] = true -L["Tool"] = true -L["Tool Website"] = true -L["to open the main window"] = true -L["to open options"] = true -L["to move this button"] = true -L["Toggle"] = true -L["Filter By Name"] = true -L["Show specialization"] = true -L["Spec Detection"] = true \ No newline at end of file +local L = LibStub("AceLocale-3.0"):NewLocale("ArenaStats", "enUS", true) +if not L then return end + +L["%F %T"] = true +L["%ih %im"] = true +L["%im %is"] = true +L["%is"] = true +L["Battlegrounds records can impact memory usage (0 means unlimited)"] = true +L["Database reset"] = true +L["Database Settings"] = true +L["Date"] = true +L["Delete all collected data"] = true +L["Drag"] = true +L["Duration"] = true +L["Enemy Faction"] = true +L["Enemy MMR"] = true +L["Enemy Team"] = true +L["Export"] = true +L["Interface Settings"] = true +L["Left Click"] = true +L["Map"] = true +L["Maximum history records"] = true +L["Minimap Button Settings"] = true +L["MMR"] = true +L["Names"] = true +L["Opens or closes the main window"] = true +L["Options"] = true +L["Purge database"] = true +L["Rating"] = true +L["Recorded %i arenas"] = true +L["Right Click"] = true +L["Show character names on hover"] = true +L["Show minimap button"] = true +L["Team"] = true +L["Tool"] = true +L["Tool Website"] = true +L["to open the main window"] = true +L["to open options"] = true +L["to move this button"] = true +L["Toggle"] = true +L["Filter By Name"] = true +L["Show specialization"] = true +L["Spec Detection"] = true +L["Generate Test Data"] = true +L["Generates a dummy arena record for testing purposes"] = true +L["Match Details"] = true +L["Name"] = true +L["Damage"] = true +L["Healing"] = true \ No newline at end of file From a2b21e1d6e37a05a74aab3b6cd1ee7b354b95d3a Mon Sep 17 00:00:00 2001 From: Mateo Date: Mon, 2 Feb 2026 11:28:45 -0300 Subject: [PATCH 3/3] Delete enUS.lua --- enUS.lua | 49 ------------------------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 enUS.lua diff --git a/enUS.lua b/enUS.lua deleted file mode 100644 index 15111e2..0000000 --- a/enUS.lua +++ /dev/null @@ -1,49 +0,0 @@ -local L = LibStub("AceLocale-3.0"):NewLocale("ArenaStats", "enUS", true) -if not L then return end - -L["%F %T"] = true -L["%ih %im"] = true -L["%im %is"] = true -L["%is"] = true -L["Battlegrounds records can impact memory usage (0 means unlimited)"] = true -L["Database reset"] = true -L["Database Settings"] = true -L["Date"] = true -L["Delete all collected data"] = true -L["Drag"] = true -L["Duration"] = true -L["Enemy Faction"] = true -L["Enemy MMR"] = true -L["Enemy Team"] = true -L["Export"] = true -L["Interface Settings"] = true -L["Left Click"] = true -L["Map"] = true -L["Maximum history records"] = true -L["Minimap Button Settings"] = true -L["MMR"] = true -L["Names"] = true -L["Opens or closes the main window"] = true -L["Options"] = true -L["Purge database"] = true -L["Rating"] = true -L["Recorded %i arenas"] = true -L["Right Click"] = true -L["Show character names on hover"] = true -L["Show minimap button"] = true -L["Team"] = true -L["Tool"] = true -L["Tool Website"] = true -L["to open the main window"] = true -L["to open options"] = true -L["to move this button"] = true -L["Toggle"] = true -L["Filter By Name"] = true -L["Show specialization"] = true -L["Spec Detection"] = true -L["Generate Test Data"] = true -L["Generates a dummy arena record for testing purposes"] = true -L["Match Details"] = true -L["Name"] = true -L["Damage"] = true -L["Healing"] = true \ No newline at end of file