From 8554be381037ac7d8ad94b54ab204263d4620d89 Mon Sep 17 00:00:00 2001 From: Kneeull <156554816+Kneeull@users.noreply.github.com> Date: Sat, 11 Apr 2026 00:18:11 +0100 Subject: [PATCH 1/7] Add Mythic+ Timer module and UI hooks Introduce a new EllesmereUIMythicTimer module (standalone timer + options) and wire it into the main UI. - Add EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua: standalone Mythic+ timer overlay, run/objective tracking, HUD rendering, and Blizzard M+ suppression. - Add EllesmereUIMythicTimer/EUI_MythicTimer_Options.lua: options page registration and controls for the new module. - Update EllesmereUI.lua to register the new module so it appears in the sidebar. - Integrate Mythic+ rendering into the QuestTracker (EllesmereUIBasics_QuestTracker.lua) so the sidebar can show the M+ section. --- EllesmereUI.lua | 1 + .../EUI_MythicTimer_Options.lua | 240 ++++ .../EllesmereUIMythicTimer.lua | 1146 +++++++++++++++++ 3 files changed, 1387 insertions(+) create mode 100644 EllesmereUIMythicTimer/EUI_MythicTimer_Options.lua create mode 100644 EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua diff --git a/EllesmereUI.lua b/EllesmereUI.lua index 0551a6f..28a1140 100644 --- a/EllesmereUI.lua +++ b/EllesmereUI.lua @@ -5575,6 +5575,7 @@ function EllesmereUI:RegisterModule(folderName, config) EllesmereUIRaidFrames = true, EllesmereUIResourceBars = true, EllesmereUIUnitFrames = true, + EllesmereUIMythicTimer = true, } if not ALLOWED[callerFolder] then return end end diff --git a/EllesmereUIMythicTimer/EUI_MythicTimer_Options.lua b/EllesmereUIMythicTimer/EUI_MythicTimer_Options.lua new file mode 100644 index 0000000..36c7dae --- /dev/null +++ b/EllesmereUIMythicTimer/EUI_MythicTimer_Options.lua @@ -0,0 +1,240 @@ +------------------------------------------------------------------------------- +-- EUI_MythicTimer_Options.lua +-- Registers the Mythic+ Timer module with EllesmereUI sidebar options. +------------------------------------------------------------------------------- +local ADDON_NAME, ns = ... + +local PAGE_DISPLAY = "Mythic+ Timer" + +local initFrame = CreateFrame("Frame") +initFrame:RegisterEvent("PLAYER_LOGIN") +initFrame:SetScript("OnEvent", function(self) + self:UnregisterEvent("PLAYER_LOGIN") + + if not EllesmereUI or not EllesmereUI.RegisterModule then return end + + local db + C_Timer.After(0, function() db = _G._EMT_AceDB end) + + local function DB() + if not db then db = _G._EMT_AceDB end + return db and db.profile + end + + local function Cfg(key) + local p = DB() + return p and p[key] + end + + local function Set(key, val) + local p = DB() + if p then p[key] = val end + end + + local function Refresh() + if _G._EMT_Apply then _G._EMT_Apply() end + if EllesmereUI.RefreshPage then EllesmereUI:RefreshPage() end + end + + --------------------------------------------------------------------------- + -- Build Page + --------------------------------------------------------------------------- + local function BuildPage(_, parent, yOffset) + local W = EllesmereUI.Widgets + local y = yOffset + local row, h + + if EllesmereUI.ClearContentHeader then EllesmereUI:ClearContentHeader() end + parent._showRowDivider = true + + local alignValues = { LEFT = "Left", CENTER = "Center", RIGHT = "Right" } + local alignOrder = { "LEFT", "CENTER", "RIGHT" } + + -- ── DISPLAY ──────────────────────────────────────────────────────── + _, h = W:SectionHeader(parent, "DISPLAY", y); y = y - h + + row, h = W:DualRow(parent, y, + { type="toggle", text="Enable Module", + getValue=function() return Cfg("enabled") ~= false end, + setValue=function(v) Set("enabled", v); Refresh() end }, + { type="toggle", text="Show Preview", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("showPreview") == true end, + setValue=function(v) Set("showPreview", v); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="slider", text="Scale", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + min=0.5, max=2.0, step=0.05, isPercent=false, + getValue=function() return Cfg("scale") or 1.0 end, + setValue=function(v) Set("scale", v); Refresh() end }, + { type="slider", text="Opacity", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + min=0.1, max=1.0, step=0.05, isPercent=false, + getValue=function() return Cfg("standaloneAlpha") or 0.85 end, + setValue=function(v) Set("standaloneAlpha", v); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="toggle", text="Show Accent Stripe", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("showAccent") == true end, + setValue=function(v) Set("showAccent", v); Refresh() end }, + { type="dropdown", text="Title / Affix Align", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + values=alignValues, + order=alignOrder, + getValue=function() return Cfg("titleAlign") or "CENTER" end, + setValue=function(v) Set("titleAlign", v); Refresh() end }) + y = y - h + + -- ── TIMER ────────────────────────────────────────────────────────── + _, h = W:SectionHeader(parent, "TIMER", y); y = y - h + + row, h = W:DualRow(parent, y, + { type="toggle", text="+3 Threshold Text", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("showPlusThreeTimer") ~= false end, + setValue=function(v) Set("showPlusThreeTimer", v); Refresh() end }, + { type="toggle", text="+2 Threshold Text", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("showPlusTwoTimer") ~= false end, + setValue=function(v) Set("showPlusTwoTimer", v); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="toggle", text="+3 Bar Marker", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("showPlusThreeBar") ~= false end, + setValue=function(v) Set("showPlusThreeBar", v); Refresh() end }, + { type="toggle", text="+2 Bar Marker", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("showPlusTwoBar") ~= false end, + setValue=function(v) Set("showPlusTwoBar", v); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="dropdown", text="Timer Align", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + values=alignValues, + order=alignOrder, + getValue=function() return Cfg("timerAlign") or "CENTER" end, + setValue=function(v) Set("timerAlign", v); Refresh() end }, + { type="label", text="" }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="toggle", text="Timer Inside Bar", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("timerInBar") == true end, + setValue=function(v) Set("timerInBar", v); Refresh() end }, + { type="colorpicker", text="In-Bar Text Color", + disabled=function() return Cfg("enabled") == false or Cfg("timerInBar") ~= true end, + disabledTooltip="Requires Timer Inside Bar", + getValue=function() + local c = Cfg("timerBarTextColor") + if c then return c.r or 1, c.g or 1, c.b or 1 end + return 1, 1, 1 + end, + setValue=function(r, g, b) + Set("timerBarTextColor", { r = r, g = g, b = b }) + Refresh() + end }) + y = y - h + + -- ── OBJECTIVES ───────────────────────────────────────────────────── + _, h = W:SectionHeader(parent, "OBJECTIVES", y); y = y - h + + row, h = W:DualRow(parent, y, + { type="toggle", text="Show Affixes", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("showAffixes") ~= false end, + setValue=function(v) Set("showAffixes", v); Refresh() end }, + { type="toggle", text="Show Deaths", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("showDeaths") ~= false end, + setValue=function(v) Set("showDeaths", v); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="toggle", text="Show Boss Objectives", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("showObjectives") ~= false end, + setValue=function(v) Set("showObjectives", v); Refresh() end }, + { type="toggle", text="Show Enemy Forces", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("showEnemyBar") ~= false end, + setValue=function(v) Set("showEnemyBar", v); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="toggle", text="Deaths in Title", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("deathsInTitle") == true end, + setValue=function(v) Set("deathsInTitle", v); Refresh() end }, + { type="toggle", text="Time Lost in Title", + disabled=function() return Cfg("enabled") == false or Cfg("deathsInTitle") ~= true end, + disabledTooltip="Requires Deaths in Title", + getValue=function() return Cfg("deathTimeInTitle") == true end, + setValue=function(v) Set("deathTimeInTitle", v); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="dropdown", text="Enemy Forces Position", + disabled=function() return Cfg("enabled") == false or Cfg("showEnemyBar") == false end, + disabledTooltip="Requires Show Enemy Forces", + values={ BOTTOM = "Bottom (default)", UNDER_BAR = "Under Timer Bar" }, + order={ "BOTTOM", "UNDER_BAR" }, + getValue=function() return Cfg("enemyForcesPos") or "BOTTOM" end, + setValue=function(v) Set("enemyForcesPos", v); Refresh() end }, + { type="dropdown", text="Enemy Forces %", + disabled=function() return Cfg("enabled") == false or Cfg("showEnemyBar") == false end, + disabledTooltip="Requires Show Enemy Forces", + values={ LABEL = "In Label Text", BAR = "In Bar", BESIDE = "Beside Bar" }, + order={ "LABEL", "BAR", "BESIDE" }, + getValue=function() return Cfg("enemyForcesPctPos") or "LABEL" end, + setValue=function(v) Set("enemyForcesPctPos", v); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="dropdown", text="Objective Align", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + values=alignValues, + order=alignOrder, + getValue=function() return Cfg("objectiveAlign") or "LEFT" end, + setValue=function(v) Set("objectiveAlign", v); Refresh() end }, + { type="label", text="" }) + y = y - h + + parent:SetHeight(math.abs(y - yOffset)) + end + + --------------------------------------------------------------------------- + -- RegisterModule + --------------------------------------------------------------------------- + EllesmereUI:RegisterModule("EllesmereUIMythicTimer", { + title = "Mythic+ Timer", + icon_on = "Interface\\AddOns\\EllesmereUI\\media\\icons\\sidebar\\consumables-ig.tga", + icon_off = "Interface\\AddOns\\EllesmereUI\\media\\icons\\sidebar\\consumables-g.tga", + pages = { PAGE_DISPLAY }, + buildPage = BuildPage, + }) +end) diff --git a/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua b/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua new file mode 100644 index 0000000..0e15425 --- /dev/null +++ b/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua @@ -0,0 +1,1146 @@ +------------------------------------------------------------------------------- +-- EllesmereUIMythicTimer.lua +-- Mythic+ Dungeon Timer — standalone timer overlay for EllesmereUI. +-- Tracks M+ run state (timer, objectives, deaths, affixes) and renders +-- a movable standalone frame. Hides the default Blizzard M+ timer. +------------------------------------------------------------------------------- +local ADDON_NAME, ns = ... +local EMT = EllesmereUI.Lite.NewAddon(ADDON_NAME) +ns.EMT = EMT + +------------------------------------------------------------------------------- +-- Lua / WoW API upvalues +------------------------------------------------------------------------------- +local floor, min, max, abs = math.floor, math.min, math.max, math.abs +local format = string.format +local GetTime = GetTime +local GetWorldElapsedTime = GetWorldElapsedTime +local wipe = wipe + +------------------------------------------------------------------------------- +-- Constants +------------------------------------------------------------------------------- +local PLUS_TWO_RATIO = 0.8 +local PLUS_THREE_RATIO = 0.6 + +------------------------------------------------------------------------------- +-- Database defaults +------------------------------------------------------------------------------- +local DB_DEFAULTS = { + profile = { + enabled = true, + showAffixes = true, + showPlusTwoTimer = true, -- +2 time remaining text + showPlusThreeTimer = true, -- +3 time remaining text + showPlusTwoBar = true, -- +2 tick marker on progress bar + showPlusThreeBar = true, -- +3 tick marker on progress bar + showDeaths = true, + showObjectives = true, + showEnemyBar = true, + objectiveAlign = "LEFT", + timerAlign = "CENTER", + titleAlign = "CENTER", -- title / affixes justify + scale = 1.0, -- standalone frame scale + standaloneAlpha = 0.85, -- standalone background opacity + showAccent = false, -- right-edge accent stripe + showPreview = false, -- show preview frame outside a key + enemyForcesPos = "BOTTOM", -- "BOTTOM" (after objectives) or "UNDER_BAR" + enemyForcesPctPos = "LABEL", -- "LABEL", "BAR", "BESIDE" + deathsInTitle = false, -- show death count next to key name + deathTimeInTitle = false, -- show time lost beside death count + timerInBar = false, -- overlay timer text inside progress bar + timerBarTextColor = nil, -- {r,g,b} override for in-bar timer text + }, +} + +------------------------------------------------------------------------------- +-- State +------------------------------------------------------------------------------- +local db -- AceDB-like table (set on init) +local updateTicker -- C_Timer ticker (1 Hz) + +-- Current run data +local currentRun = { + active = false, + mapID = nil, + mapName = "", + level = 0, + affixes = {}, + maxTime = 0, + elapsed = 0, + completed = false, + deaths = 0, + deathTimeLost = 0, + objectives = {}, +} + +------------------------------------------------------------------------------- +-- Time formatting +------------------------------------------------------------------------------- +local function FormatTime(seconds) + if not seconds or seconds < 0 then seconds = 0 end + local m = floor(seconds / 60) + local s = floor(seconds % 60) + return format("%d:%02d", m, s) +end + +------------------------------------------------------------------------------- +-- Objective tracking +------------------------------------------------------------------------------- +local function UpdateObjectives() + local numCriteria = select(3, C_Scenario.GetStepInfo()) or 0 + local elapsed = currentRun.elapsed + + for i = 1, numCriteria do + local info = C_ScenarioInfo.GetCriteriaInfo(i) + if info then + local obj = currentRun.objectives[i] + if not obj then + obj = { + name = "", + completed = false, + elapsed = 0, + quantity = 0, + totalQuantity = 0, + isWeighted = false, + } + currentRun.objectives[i] = obj + end + + obj.name = info.description or ("Objective " .. i) + local wasCompleted = obj.completed + obj.completed = info.completed + + if obj.completed and not wasCompleted then + obj.elapsed = elapsed + end + + obj.quantity = info.quantity or 0 + obj.totalQuantity = info.totalQuantity or 0 + if info.isWeightedProgress then + obj.isWeighted = true + -- Match the reference addon logic: use the displayed weighted + -- progress value when available, then normalize it against the + -- criterion total. If totalQuantity is 100, this preserves a + -- percent value directly; if totalQuantity is a raw enemy-force + -- cap, this converts raw count -> percent with 2dp precision. + local rawQuantity = info.quantity or 0 + local quantityString = info.quantityString + if quantityString and quantityString ~= "" then + local normalized = quantityString:gsub("%%", "") + if normalized:find(",") and not normalized:find("%.") then + normalized = normalized:gsub(",", ".") + end + local parsed = tonumber(normalized) + if parsed then + rawQuantity = parsed + end + end + + if obj.totalQuantity and obj.totalQuantity > 0 then + local percent = (rawQuantity / obj.totalQuantity) * 100 + local mult = 10 ^ 2 + obj.quantity = math.floor(percent * mult + 0.5) / mult + else + obj.quantity = rawQuantity + end + + if obj.completed then + obj.quantity = 100 + obj.totalQuantity = 100 + end + else + obj.isWeighted = false + -- Ensure bosses (single-count) still report 0/1 or 1/1 + if obj.totalQuantity == 0 then + obj.quantity = obj.completed and 1 or 0 + obj.totalQuantity = 1 + end + end + end + end + + for i = numCriteria + 1, #currentRun.objectives do + currentRun.objectives[i] = nil + end +end + +------------------------------------------------------------------------------- +-- Notify standalone frame to refresh (coalesced) +------------------------------------------------------------------------------- +local _refreshTimer +local function NotifyRefresh() + if _refreshTimer then return end -- already pending + _refreshTimer = C_Timer.After(0.05, function() + _refreshTimer = nil + if _G._EMT_StandaloneRefresh then _G._EMT_StandaloneRefresh() end + end) +end + +------------------------------------------------------------------------------- +-- Timer tick (1 Hz while a key is active) +------------------------------------------------------------------------------- +local function OnTimerTick() + if not currentRun.active then return end + + local _, elapsedTime = GetWorldElapsedTime(1) + currentRun.elapsed = elapsedTime or 0 + + local deathCount, timeLost = C_ChallengeMode.GetDeathCount() + currentRun.deaths = deathCount or 0 + currentRun.deathTimeLost = timeLost or 0 + + UpdateObjectives() + NotifyRefresh() +end + +------------------------------------------------------------------------------- +-- Suppress / unsuppress Blizzard M+ scenario frame +------------------------------------------------------------------------------- +local _blizzHiddenParent +local _blizzOrigScenarioParent + +local function SuppressBlizzardMPlus() + if not db or not db.profile.enabled then return end + + if not _blizzHiddenParent then + _blizzHiddenParent = CreateFrame("Frame") + _blizzHiddenParent:Hide() + end + + -- ScenarioBlocksFrame is the container for Blizzard's M+ timer + local sbf = _G.ScenarioBlocksFrame + if sbf and sbf:GetParent() ~= _blizzHiddenParent then + _blizzOrigScenarioParent = sbf:GetParent() + sbf:SetParent(_blizzHiddenParent) + end +end + +local function UnsuppressBlizzardMPlus() + local sbf = _G.ScenarioBlocksFrame + if sbf and _blizzOrigScenarioParent and sbf:GetParent() == _blizzHiddenParent then + sbf:SetParent(_blizzOrigScenarioParent) + end +end + +------------------------------------------------------------------------------- +-- Run lifecycle +------------------------------------------------------------------------------- +local function StartRun() + local mapID = C_ChallengeMode.GetActiveChallengeMapID() + if not mapID then return end + + local mapName, _, timeLimit = C_ChallengeMode.GetMapUIInfo(mapID) + local level, affixes = C_ChallengeMode.GetActiveKeystoneInfo() + + currentRun.active = true + currentRun.completed = false + currentRun.mapID = mapID + currentRun.mapName = mapName or "Unknown" + currentRun.level = level or 0 + currentRun.maxTime = timeLimit or 0 + currentRun.elapsed = 0 + currentRun.deaths = 0 + currentRun.deathTimeLost = 0 + currentRun.affixes = affixes or {} + wipe(currentRun.objectives) + + if updateTicker then updateTicker:Cancel() end + updateTicker = C_Timer.NewTicker(1, OnTimerTick) + OnTimerTick() + + SuppressBlizzardMPlus() + NotifyRefresh() +end + +local function CompleteRun() + currentRun.completed = true + currentRun.active = false + + if updateTicker then updateTicker:Cancel(); updateTicker = nil end + + local _, elapsedTime = GetWorldElapsedTime(1) + currentRun.elapsed = elapsedTime or currentRun.elapsed + UpdateObjectives() + NotifyRefresh() +end + +local function ResetRun() + currentRun.active = false + currentRun.completed = false + currentRun.mapID = nil + currentRun.mapName = "" + currentRun.level = 0 + currentRun.maxTime = 0 + currentRun.elapsed = 0 + currentRun.deaths = 0 + currentRun.deathTimeLost = 0 + wipe(currentRun.affixes) + wipe(currentRun.objectives) + + if updateTicker then updateTicker:Cancel(); updateTicker = nil end + + UnsuppressBlizzardMPlus() + NotifyRefresh() +end + +local function CheckForActiveRun() + local mapID = C_ChallengeMode.GetActiveChallengeMapID() + if mapID then StartRun() end +end + +------------------------------------------------------------------------------- +-- Preview data for configuring outside a key (The Rookery) +------------------------------------------------------------------------------- +local PREVIEW_RUN = { + active = true, + completed = false, + mapID = 2648, + mapName = "The Rookery", + level = 12, + maxTime = 1920, + elapsed = 1380, + deaths = 2, + deathTimeLost = 10, + affixes = {}, + _previewAffixNames = { "Tyrannical", "Xal'atath's Bargain: Ascendant" }, + objectives = { + { name = "Kyrioss", completed = true, elapsed = 510, quantity = 1, totalQuantity = 1, isWeighted = false }, + { name = "Stormguard Gorren", completed = true, elapsed = 1005, quantity = 1, totalQuantity = 1, isWeighted = false }, + { name = "Code Taint Monstrosity", completed = false, elapsed = 0, quantity = 0, totalQuantity = 1, isWeighted = false }, + { name = "|cffff3333Ellesmere|r", completed = false, elapsed = 0, quantity = 0, totalQuantity = 1, isWeighted = false }, + { name = "Enemy Forces", completed = false, elapsed = 0, quantity = 78.42, totalQuantity = 100, isWeighted = true }, + }, +} + +-- Expose apply for options panel +_G._EMT_Apply = function() + if _G._EMT_StandaloneRefresh then _G._EMT_StandaloneRefresh() end +end + +------------------------------------------------------------------------------- +-- Standalone frame — the primary rendering surface. +------------------------------------------------------------------------------- +------------------------------------------------------------------------------- +local standaloneFrame -- main container +local standaloneCreated = false + +-- Font/color helpers (mirrors QT approach but self-contained) +local FALLBACK_FONT = "Fonts/FRIZQT__.TTF" +local function SFont() + if EllesmereUI and EllesmereUI.GetFontPath then + local p = EllesmereUI.GetFontPath("unitFrames") + if p and p ~= "" then return p end + end + return FALLBACK_FONT +end +local function SOutline() + if EllesmereUI.GetFontOutlineFlag then return EllesmereUI.GetFontOutlineFlag() end + return "" +end +local function SetFS(fs, size, flags) + if not fs then return end + local p = SFont() + flags = flags or SOutline() + fs:SetFont(p, size, flags) + if not fs:GetFont() then fs:SetFont(FALLBACK_FONT, size, flags) end +end +local function ApplyShadow(fs) + if not fs then return end + if EllesmereUI.GetFontUseShadow and EllesmereUI.GetFontUseShadow() then + fs:SetShadowColor(0, 0, 0, 0.8); fs:SetShadowOffset(1, -1) + else + fs:SetShadowOffset(0, 0) + end +end + +local function SetFittedText(fs, text, maxWidth, preferredSize, minSize) + if not fs then return end + text = text or "" + preferredSize = preferredSize or 10 + minSize = minSize or 8 + local outline = SOutline() + -- Ensure a valid font exists before first SetText; startup can + -- render this FontString before any prior SetFont call has happened. + SetFS(fs, preferredSize, outline) + ApplyShadow(fs) + fs:SetText(text) + + for size = preferredSize, minSize, -1 do + SetFS(fs, size, outline) + ApplyShadow(fs) + fs:SetText(text) + if not maxWidth or fs:GetStringWidth() <= maxWidth then + return + end + end +end + +local function GetAccentColor() + if EllesmereUI.ResolveThemeColor then + local theme = EllesmereUIDB and EllesmereUIDB.accentTheme or "Class Colored" + return EllesmereUI.ResolveThemeColor(theme) + end + return 0.05, 0.83, 0.62 +end + +-- Pool of objective row fontstrings +local objRows = {} +local function GetObjRow(parent, idx) + if objRows[idx] then return objRows[idx] end + local fs = parent:CreateFontString(nil, "OVERLAY") + fs:SetWordWrap(false) + objRows[idx] = fs + return fs +end + +local function CreateStandaloneFrame() + if standaloneCreated then return standaloneFrame end + standaloneCreated = true + + local FRAME_W = 260 + local PAD = 8 + + local f = CreateFrame("Frame", "EllesmereUIMythicTimerStandalone", UIParent, "BackdropTemplate") + f:SetSize(FRAME_W, 200) + f:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + f:SetFrameStrata("MEDIUM") + f:SetFrameLevel(10) + f:SetClampedToScreen(true) + + -- Background + f:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Buttons\\WHITE8x8", + edgeSize = 1, + }) + f:SetBackdropColor(0.05, 0.04, 0.08, 0.85) + f:SetBackdropBorderColor(0.15, 0.15, 0.15, 0.6) + + -- Accent stripe (right edge) + f._accent = f:CreateTexture(nil, "BORDER") + f._accent:SetWidth(2) + f._accent:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1) + f._accent:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1) + + -- Banner title + f._titleFS = f:CreateFontString(nil, "OVERLAY") + f._titleFS:SetWordWrap(false) + f._titleFS:SetJustifyV("MIDDLE") + + -- Affixes + f._affixFS = f:CreateFontString(nil, "OVERLAY") + f._affixFS:SetWordWrap(true) + + -- Timer + f._timerFS = f:CreateFontString(nil, "OVERLAY") + f._timerFS:SetJustifyH("CENTER") + + -- Timer bar bg + f._barBg = f:CreateTexture(nil, "BACKGROUND", nil, 1) + f._barFill = f:CreateTexture(nil, "ARTWORK") + f._seg3 = f:CreateTexture(nil, "OVERLAY") + f._seg2 = f:CreateTexture(nil, "OVERLAY") + + -- Threshold text + f._threshFS = f:CreateFontString(nil, "OVERLAY") + f._threshFS:SetWordWrap(false) + + -- Deaths + f._deathFS = f:CreateFontString(nil, "OVERLAY") + f._deathFS:SetWordWrap(false) + + -- Enemy forces label + f._enemyFS = f:CreateFontString(nil, "OVERLAY") + f._enemyFS:SetWordWrap(false) + + -- Enemy bar + f._enemyBarBg = f:CreateTexture(nil, "BACKGROUND", nil, 1) + f._enemyBarFill = f:CreateTexture(nil, "ARTWORK") + + -- Preview indicator + f._previewFS = f:CreateFontString(nil, "OVERLAY") + f._previewFS:SetWordWrap(false) + + -- The frame can be created by unlock-mode registration before it has any + -- content to render. Keep it hidden until RenderStandalone() explicitly + -- shows it. + f:Hide() + + standaloneFrame = f + return f +end + +local function RenderStandalone() + if not db or not db.profile.enabled then + if standaloneFrame then standaloneFrame:Hide() end + return + end + + local p = db.profile + local isPreview = false + local run = currentRun + if not run.active and not run.completed then + if p.showPreview then + run = PREVIEW_RUN + isPreview = true + else + if standaloneFrame then standaloneFrame:Hide() end + return + end + end + + local f = CreateStandaloneFrame() + local PAD = 10 + local ALIGN_PAD = 6 -- extra inset for L/R aligned content + local TBAR_PAD = 10 + local TBAR_H = p.timerInBar and 22 or 10 + local ROW_GAP = 6 + + -- Scale + local scale = p.scale or 1.0 + f:SetScale(scale) + + -- Opacity + local alpha = p.standaloneAlpha or 0.85 + f:SetBackdropColor(0.05, 0.04, 0.08, alpha) + f:SetBackdropBorderColor(0.15, 0.15, 0.15, min(alpha, 0.6)) + + -- Accent stripe (optional) + local aR, aG, aB = GetAccentColor() + if p.showAccent then + f._accent:SetColorTexture(aR, aG, aB, 0.9) + f._accent:Show() + else + f._accent:Hide() + end + + local frameW = f:GetWidth() + local innerW = frameW - PAD * 2 + local y = -PAD + + -- Helper: compute padding for content alignment + local function ContentPad(align) + if align == "LEFT" or align == "RIGHT" then return PAD + ALIGN_PAD end + return PAD + end + + --------------------------------------------------------------------------- + -- Title row (+deaths-in-title when enabled) + --------------------------------------------------------------------------- + local titleAlign = p.titleAlign or "CENTER" + local titleText = format("+%d %s", run.level, run.mapName or "Mythic+") + if p.showDeaths and p.deathsInTitle and run.deaths > 0 then + local deathPart = format("|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_8:0|t %d", run.deaths) + if p.deathTimeInTitle and run.deathTimeLost > 0 then + deathPart = deathPart .. format(" (-%s)", FormatTime(run.deathTimeLost)) + end + titleText = titleText .. format(" |cffee5555%s|r", deathPart) + end + f._titleFS:SetJustifyH(titleAlign) + f._titleFS:SetTextColor(1, 1, 1) + SetFittedText(f._titleFS, titleText, innerW, 13, 10) + f._titleFS:ClearAllPoints() + f._titleFS:SetPoint("TOPLEFT", f, "TOPLEFT", PAD, y) + f._titleFS:SetPoint("TOPRIGHT", f, "TOPRIGHT", -PAD, y) + f._titleFS:SetHeight(20) + f._titleFS:Show() + y = y - 22 - ROW_GAP + + --------------------------------------------------------------------------- + -- Affixes + --------------------------------------------------------------------------- + if p.showAffixes then + local names = {} + if run._previewAffixNames then + for _, name in ipairs(run._previewAffixNames) do + names[#names + 1] = name + end + else + for _, id in ipairs(run.affixes) do + local name = C_ChallengeMode.GetAffixInfo(id) + if name then names[#names + 1] = name end + end + end + if #names > 0 then + f._affixFS:SetTextColor(0.55, 0.55, 0.55) + f._affixFS:SetJustifyH(titleAlign) + SetFittedText(f._affixFS, table.concat(names, " \194\183 "), innerW, 10, 8) + f._affixFS:ClearAllPoints() + f._affixFS:SetPoint("TOPLEFT", f, "TOPLEFT", PAD, y) + f._affixFS:SetPoint("TOPRIGHT", f, "TOPRIGHT", -PAD, y) + f._affixFS:Show() + y = y - (f._affixFS:GetStringHeight() or 12) - ROW_GAP + else + f._affixFS:Hide() + end + else + f._affixFS:Hide() + end + + --------------------------------------------------------------------------- + -- Deaths row (right after affixes, if not shown in title) + --------------------------------------------------------------------------- + if p.showDeaths and run.deaths > 0 and not p.deathsInTitle then + local objAlign = p.objectiveAlign or "LEFT" + local dPad = ContentPad(objAlign) + SetFS(f._deathFS, 10) + ApplyShadow(f._deathFS) + f._deathFS:SetText(format("|cffee5555%d Death%s -%s|r", + run.deaths, run.deaths ~= 1 and "s" or "", FormatTime(run.deathTimeLost))) + f._deathFS:ClearAllPoints() + f._deathFS:SetPoint("TOPLEFT", f, "TOPLEFT", dPad, y) + f._deathFS:SetPoint("TOPRIGHT", f, "TOPRIGHT", -dPad, y) + f._deathFS:SetJustifyH(objAlign) + f._deathFS:Show() + y = y - (f._deathFS:GetStringHeight() or 12) - ROW_GAP + else + f._deathFS:Hide() + end + + --------------------------------------------------------------------------- + -- Compute timer colours + --------------------------------------------------------------------------- + local elapsed = run.elapsed or 0 + local maxTime = run.maxTime or 0 + local timeLeft = max(0, maxTime - elapsed) + local plusThreeT = maxTime * PLUS_THREE_RATIO + local plusTwoT = maxTime * PLUS_TWO_RATIO + + local timerText + if run.completed then + timerText = FormatTime(elapsed) + elseif elapsed > maxTime and maxTime > 0 then + timerText = "+" .. FormatTime(elapsed - maxTime) + else + timerText = FormatTime(timeLeft) + end + + local tR, tG, tB + if run.completed then + if elapsed <= plusThreeT then tR, tG, tB = 0.3, 0.8, 1 + elseif elapsed <= plusTwoT then tR, tG, tB = 0.4, 1, 0.4 + elseif elapsed <= maxTime then tR, tG, tB = 0.9, 0.7, 0.2 + else tR, tG, tB = 0.9, 0.2, 0.2 end + elseif timeLeft <= 0 then tR, tG, tB = 0.9, 0.2, 0.2 + elseif timeLeft < maxTime * 0.2 then tR, tG, tB = 0.9, 0.7, 0.2 + else tR, tG, tB = 1, 1, 1 end + + --------------------------------------------------------------------------- + -- Reusable sub-renderers (use upvalue y via closure) + --------------------------------------------------------------------------- + + local underBarMode = (p.enemyForcesPos == "UNDER_BAR") + + -- Threshold text (+3 / +2 remaining) + local function RenderThresholdText() + if (p.showPlusTwoTimer or p.showPlusThreeTimer) and maxTime > 0 then + local parts = {} + if p.showPlusThreeTimer then + local diff = plusThreeT - elapsed + if diff >= 0 then + parts[#parts + 1] = format("|cff4dccff+3 %s|r", FormatTime(diff)) + else + parts[#parts + 1] = format("|cff666666+3 -%s|r", FormatTime(abs(diff))) + end + end + if p.showPlusTwoTimer then + local diff = plusTwoT - elapsed + if diff >= 0 then + parts[#parts + 1] = format("|cff66ff66+2 %s|r", FormatTime(diff)) + else + parts[#parts + 1] = format("|cff666666+2 -%s|r", FormatTime(abs(diff))) + end + end + if #parts > 0 then + SetFS(f._threshFS, 10) + ApplyShadow(f._threshFS) + f._threshFS:SetTextColor(1, 1, 1) + f._threshFS:SetText(table.concat(parts, " ")) + f._threshFS:SetJustifyH("CENTER") + f._threshFS:ClearAllPoints() + f._threshFS:SetPoint("TOPLEFT", f, "TOPLEFT", PAD, y) + f._threshFS:SetPoint("TOPRIGHT", f, "TOPRIGHT", -PAD, y) + f._threshFS:Show() + y = y - (f._threshFS:GetStringHeight() or 12) - ROW_GAP + else + f._threshFS:Hide() + end + else + f._threshFS:Hide() + end + end + + -- Enemy forces label + bar + local function RenderEnemyForces() + if not p.showEnemyBar then + f._enemyFS:Hide(); f._enemyBarBg:Hide(); f._enemyBarFill:Hide() + if f._enemyBarText then f._enemyBarText:Hide() end + return + end + local enemyObj = nil + for _, obj in ipairs(run.objectives) do + if obj.isWeighted then enemyObj = obj; break end + end + if not enemyObj then + f._enemyFS:Hide(); f._enemyBarBg:Hide(); f._enemyBarFill:Hide() + if f._enemyBarText then f._enemyBarText:Hide() end + return + end + + local objAlign = p.objectiveAlign or "LEFT" + local ePad = ContentPad(objAlign) + local pctRaw = min(100, max(0, enemyObj.quantity)) + local pctPos = p.enemyForcesPctPos or "LABEL" + + -- Label text: include % only when pctPos is LABEL + local label + if pctPos == "LABEL" then + label = format("Enemy Forces %.2f%%", pctRaw) + else + label = "Enemy Forces" + end + + SetFS(f._enemyFS, 10) + ApplyShadow(f._enemyFS) + if enemyObj.completed then + f._enemyFS:SetTextColor(0.3, 0.8, 0.3) + else + f._enemyFS:SetTextColor(0.9, 0.9, 0.9) + end + f._enemyFS:SetText(label) + + -- Render bar then text (under-bar), or text then bar (default) + local function RenderEnemyBar() + if enemyObj.completed then + f._enemyBarBg:Hide(); f._enemyBarFill:Hide() + if f._enemyBarText then f._enemyBarText:Hide() end + return + end + -- Bar always uses PAD for consistent width; reserve space for beside text + local besideRoom = (pctPos == "BESIDE") and 46 or 0 + local barW = innerW - TBAR_PAD * 2 - besideRoom + f._enemyBarBg:ClearAllPoints() + f._enemyBarBg:SetPoint("TOPLEFT", f, "TOPLEFT", PAD + TBAR_PAD, y) + f._enemyBarBg:SetSize(barW, 6) + f._enemyBarBg:SetColorTexture(0.12, 0.12, 0.12, 0.9) + f._enemyBarBg:Show() + + local epct = min(1, max(0, pctRaw / 100)) + local eFillW = max(1, barW * epct) + f._enemyBarFill:ClearAllPoints() + f._enemyBarFill:SetPoint("TOPLEFT", f._enemyBarBg, "TOPLEFT", 0, 0) + f._enemyBarFill:SetSize(eFillW, 6) + f._enemyBarFill:SetColorTexture(aR, aG, aB, 0.8) + f._enemyBarFill:Show() + + -- % overlay / beside bar + if not f._enemyBarText then + f._enemyBarText = f:CreateFontString(nil, "OVERLAY") + f._enemyBarText:SetWordWrap(false) + end + if pctPos == "BAR" then + SetFS(f._enemyBarText, 8) + ApplyShadow(f._enemyBarText) + f._enemyBarText:SetTextColor(1, 1, 1) + f._enemyBarText:SetText(format("%.2f%%", pctRaw)) + f._enemyBarText:ClearAllPoints() + f._enemyBarText:SetPoint("CENTER", f._enemyBarBg, "CENTER", 0, 0) + f._enemyBarText:Show() + elseif pctPos == "BESIDE" then + SetFS(f._enemyBarText, 8) + ApplyShadow(f._enemyBarText) + f._enemyBarText:SetTextColor(0.9, 0.9, 0.9) + f._enemyBarText:SetText(format("%.2f%%", pctRaw)) + f._enemyBarText:ClearAllPoints() + f._enemyBarText:SetPoint("LEFT", f._enemyBarBg, "RIGHT", 4, 0) + f._enemyBarText:Show() + else + f._enemyBarText:Hide() + end + + y = y - 10 - ROW_GAP + end + + local function RenderEnemyLabel() + f._enemyFS:ClearAllPoints() + f._enemyFS:SetPoint("TOPLEFT", f, "TOPLEFT", ePad, y) + f._enemyFS:SetPoint("TOPRIGHT", f, "TOPRIGHT", -ePad, y) + f._enemyFS:SetJustifyH(objAlign) + f._enemyFS:Show() + y = y - (f._enemyFS:GetStringHeight() or 12) - 4 + end + + if underBarMode then + -- Under-bar: bar first, label below + RenderEnemyBar() + RenderEnemyLabel() + else + -- Default: label first, bar below + RenderEnemyLabel() + RenderEnemyBar() + end + end + + --------------------------------------------------------------------------- + -- Layout: under-bar mode renders timer then thresholds then bar then enemy + --------------------------------------------------------------------------- + + --------------------------------------------------------------------------- + -- Timer text (above bar, unless timerInBar) + --------------------------------------------------------------------------- + if not p.timerInBar then + local timerAlign = p.timerAlign or "CENTER" + SetFS(f._timerFS, 20) + ApplyShadow(f._timerFS) + f._timerFS:SetTextColor(tR, tG, tB) + f._timerFS:SetText(timerText) + f._timerFS:SetJustifyH(timerAlign) + f._timerFS:ClearAllPoints() + local timerBlockW = min(innerW, max(140, floor(innerW * 0.72))) + if timerAlign == "RIGHT" then + f._timerFS:SetPoint("TOPRIGHT", f, "TOPRIGHT", -(PAD + ALIGN_PAD), y) + elseif timerAlign == "LEFT" then + f._timerFS:SetPoint("TOPLEFT", f, "TOPLEFT", PAD + ALIGN_PAD, y) + else + f._timerFS:SetPoint("TOP", f, "TOP", 0, y) + end + f._timerFS:SetWidth(timerBlockW) + f._timerFS:Show() + local timerH = f._timerFS:GetStringHeight() or 20 + if timerH < 20 then timerH = 20 end + y = y - timerH - ROW_GAP + else + f._timerFS:Hide() + end + + --------------------------------------------------------------------------- + -- Under-bar mode: thresholds between timer and bar + --------------------------------------------------------------------------- + if underBarMode then + RenderThresholdText() + end + + --------------------------------------------------------------------------- + -- Timer progress bar + --------------------------------------------------------------------------- + if maxTime > 0 then + local barW = innerW - TBAR_PAD * 2 + + f._barBg:ClearAllPoints() + f._barBg:SetPoint("TOPLEFT", f, "TOPLEFT", PAD + TBAR_PAD, y) + f._barBg:SetSize(barW, TBAR_H) + f._barBg:SetColorTexture(0.12, 0.12, 0.12, 0.9) + f._barBg:Show() + + local fillPct = math.min(1, elapsed / maxTime) + local fillW = math.max(1, barW * fillPct) + f._barFill:ClearAllPoints() + f._barFill:SetPoint("TOPLEFT", f._barBg, "TOPLEFT", 0, 0) + f._barFill:SetSize(fillW, TBAR_H) + f._barFill:SetColorTexture(tR, tG, tB, 0.85) + f._barFill:Show() + + -- +3 marker (60%) + f._seg3:ClearAllPoints() + f._seg3:SetSize(1, TBAR_H + 4) + f._seg3:SetPoint("TOP", f._barBg, "TOPLEFT", floor(barW * 0.6), 2) + f._seg3:SetColorTexture(0.3, 0.8, 1, 0.9) + if p.showPlusThreeBar then f._seg3:Show() else f._seg3:Hide() end + + -- +2 marker (80%) + f._seg2:ClearAllPoints() + f._seg2:SetSize(1, TBAR_H + 4) + f._seg2:SetPoint("TOP", f._barBg, "TOPLEFT", floor(barW * 0.8), 2) + f._seg2:SetColorTexture(0.4, 1, 0.4, 0.9) + if p.showPlusTwoBar then f._seg2:Show() else f._seg2:Hide() end + + -- Timer text overlay inside bar + if p.timerInBar then + if not f._barTimerFS then + f._barTimerFS = f:CreateFontString(nil, "OVERLAY") + f._barTimerFS:SetWordWrap(false) + end + SetFS(f._barTimerFS, 12) + ApplyShadow(f._barTimerFS) + local btc = p.timerBarTextColor + if btc then + f._barTimerFS:SetTextColor(btc.r or 1, btc.g or 1, btc.b or 1) + else + f._barTimerFS:SetTextColor(tR, tG, tB) + end + f._barTimerFS:SetText(timerText) + f._barTimerFS:ClearAllPoints() + f._barTimerFS:SetPoint("CENTER", f._barBg, "CENTER", 0, 0) + f._barTimerFS:Show() + elseif f._barTimerFS then + f._barTimerFS:Hide() + end + + y = y - TBAR_H - ROW_GAP - 2 + else + f._barBg:Hide(); f._barFill:Hide() + f._seg3:Hide(); f._seg2:Hide() + if f._barTimerFS then f._barTimerFS:Hide() end + end + + --------------------------------------------------------------------------- + -- Under-bar mode: enemy forces immediately after bar + --------------------------------------------------------------------------- + if underBarMode then + RenderEnemyForces() + end + + --------------------------------------------------------------------------- + -- Default mode: thresholds after bar + --------------------------------------------------------------------------- + if not underBarMode then + RenderThresholdText() + end + + --------------------------------------------------------------------------- + -- Objectives + --------------------------------------------------------------------------- + local objIdx = 0 + if p.showObjectives then + local objAlign = p.objectiveAlign or "LEFT" + local oPad = ContentPad(objAlign) + for i, obj in ipairs(run.objectives) do + if not obj.isWeighted then + objIdx = objIdx + 1 + local row = GetObjRow(f, objIdx) + SetFS(row, 10) + ApplyShadow(row) + + local displayName = obj.name or ("Objective " .. i) + if obj.totalQuantity and obj.totalQuantity > 1 then + displayName = format("%d/%d %s", obj.quantity or 0, obj.totalQuantity, displayName) + end + if obj.completed then + displayName = "|TInterface\\RAIDFRAME\\ReadyCheck-Ready:0|t " .. displayName + row:SetTextColor(0.3, 0.8, 0.3) + else + row:SetTextColor(0.9, 0.9, 0.9) + end + local timeStr = "" + if obj.completed and obj.elapsed and obj.elapsed > 0 then + timeStr = " |cff888888" .. FormatTime(obj.elapsed) .. "|r" + end + row:SetText(displayName .. timeStr) + row:SetJustifyH(objAlign) + row:ClearAllPoints() + local oInnerW = frameW - oPad * 2 + local objBlockW = min(oInnerW, max(160, floor(oInnerW * 0.8))) + if objAlign == "RIGHT" then + row:SetPoint("TOPRIGHT", f, "TOPRIGHT", -oPad, y) + elseif objAlign == "CENTER" then + row:SetPoint("TOP", f, "TOP", 0, y) + else + row:SetPoint("TOPLEFT", f, "TOPLEFT", oPad, y) + end + row:SetWidth(objBlockW) + row:Show() + y = y - (row:GetStringHeight() or 12) - 3 + end + end + end + + -- Hide unused objective rows + for i = objIdx + 1, #objRows do + objRows[i]:Hide() + end + + --------------------------------------------------------------------------- + -- Default mode: enemy forces at bottom + --------------------------------------------------------------------------- + if not underBarMode then + RenderEnemyForces() + end + + --------------------------------------------------------------------------- + -- Resize frame to content + --------------------------------------------------------------------------- + local totalH = abs(y) + PAD + f:SetHeight(totalH) + + --------------------------------------------------------------------------- + -- Preview indicator + --------------------------------------------------------------------------- + if isPreview then + SetFS(f._previewFS, 8) + f._previewFS:SetTextColor(0.5, 0.5, 0.5, 0.6) + f._previewFS:SetText("PREVIEW") + f._previewFS:ClearAllPoints() + f._previewFS:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -PAD, 4) + f._previewFS:Show() + elseif f._previewFS then + f._previewFS:Hide() + end + + f:Show() +end + +-- Global refresh callback for standalone frame +_G._EMT_StandaloneRefresh = RenderStandalone + +-- Expose standalone frame getter for unlock mode +_G._EMT_GetStandaloneFrame = function() + return CreateStandaloneFrame() +end + +local function ApplyStandalonePosition() + if not db then return end + if not standaloneFrame then return end + local pos = db.profile.standalonePos + if pos then + standaloneFrame:ClearAllPoints() + standaloneFrame:SetPoint(pos.point, UIParent, pos.relPoint, pos.x, pos.y) + end +end + +local function ArePrimaryObjectivesComplete() + local numCriteria = select(3, C_Scenario.GetStepInfo()) or 0 + if numCriteria == 0 then return false end + + local seenPrimary = false + for i = 1, numCriteria do + local info = C_ScenarioInfo.GetCriteriaInfo(i) + if info and not info.isWeightedProgress then + seenPrimary = true + if not info.completed then + return false + end + end + end + + return seenPrimary +end + +local runtimeFrame = CreateFrame("Frame") +local runtimePollElapsed = 0 +local runtimeInitElapsed = 0 +local runtimeInitialized = false + +local function RuntimeOnUpdate(_, elapsed) + if not db then return end + + if not runtimeInitialized then + runtimeInitElapsed = runtimeInitElapsed + elapsed + if runtimeInitElapsed >= 1 then + runtimeInitialized = true + CheckForActiveRun() + ApplyStandalonePosition() + end + end + + runtimePollElapsed = runtimePollElapsed + elapsed + if runtimePollElapsed < 0.25 then return end + runtimePollElapsed = 0 + + if not db.profile.enabled then + if currentRun.active or currentRun.completed then + ResetRun() + end + return + end + + local activeMapID = C_ChallengeMode.GetActiveChallengeMapID() + if activeMapID then + if not currentRun.active and not currentRun.completed then + StartRun() + elseif currentRun.active and ArePrimaryObjectivesComplete() then + CompleteRun() + end + elseif currentRun.active or currentRun.completed then + ResetRun() + end +end + +function EMT:OnInitialize() + db = EllesmereUI.Lite.NewDB("EllesmereUIMythicTimerDB", DB_DEFAULTS) + _G._EMT_AceDB = db + + if db and db.profile and db.profile.objectiveAlign == nil then + local oldAlign = db.profile.thresholdAlign + if oldAlign == "RIGHT" then + db.profile.objectiveAlign = "RIGHT" + elseif oldAlign == "CENTER" then + db.profile.objectiveAlign = "CENTER" + else + db.profile.objectiveAlign = "LEFT" + end + end + + if db and db.profile and db.profile.timerAlign == nil then + db.profile.timerAlign = "CENTER" + end + + -- Migrate: detached is no longer a setting (always standalone) + if db and db.profile then + local pp = db.profile + pp.detached = nil + + if pp.showPlusTwo ~= nil and pp.showPlusTwoTimer == nil then + pp.showPlusTwoTimer = pp.showPlusTwo + pp.showPlusTwoBar = pp.showPlusTwo + pp.showPlusTwo = nil + end + if pp.showPlusThree ~= nil and pp.showPlusThreeTimer == nil then + pp.showPlusThreeTimer = pp.showPlusThree + pp.showPlusThreeBar = pp.showPlusThree + pp.showPlusThree = nil + end + end + + runtimeFrame:SetScript("OnUpdate", RuntimeOnUpdate) +end + +function EMT:OnEnable() + if not db or not db.profile.enabled then return end + + -- Register with unlock mode + if EllesmereUI and EllesmereUI.RegisterUnlockElements and EllesmereUI.MakeUnlockElement then + local MK = EllesmereUI.MakeUnlockElement + EllesmereUI:RegisterUnlockElements({ + MK({ + key = "EMT_MythicTimer", + label = "Mythic+ Timer", + group = "Mythic+", + order = 520, + noResize = true, + getFrame = function() + return _G._EMT_GetStandaloneFrame and _G._EMT_GetStandaloneFrame() + end, + getSize = function() + local f = standaloneFrame + if f then return f:GetWidth(), f:GetHeight() end + return 260, 200 + end, + isHidden = function() + return false + end, + savePos = function(_, point, relPoint, x, y) + db.profile.standalonePos = { point = point, relPoint = relPoint, x = x, y = y } + if standaloneFrame and not EllesmereUI._unlockActive then + standaloneFrame:ClearAllPoints() + standaloneFrame:SetPoint(point, UIParent, relPoint, x, y) + end + end, + loadPos = function() + return db.profile.standalonePos + end, + clearPos = function() + db.profile.standalonePos = nil + end, + applyPos = function() + local pos = db.profile.standalonePos + if pos and standaloneFrame then + standaloneFrame:ClearAllPoints() + standaloneFrame:SetPoint(pos.point, UIParent, pos.relPoint, pos.x, pos.y) + end + end, + }), + }) + end +end + From 3088bbd90119fcf80bc39f147abdfe26c797b495 Mon Sep 17 00:00:00 2001 From: Neil U <156554816+Kneeull@users.noreply.github.com> Date: Sat, 11 Apr 2026 00:21:17 +0100 Subject: [PATCH 2/7] Add Mythic+ Timer to EllesmereUI Adding dependency to EUI.lua --- EllesmereUI.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/EllesmereUI.lua b/EllesmereUI.lua index 28a1140..8e3cea1 100644 --- a/EllesmereUI.lua +++ b/EllesmereUI.lua @@ -260,6 +260,7 @@ local ADDON_ROSTER = { { folder = "EllesmereUICooldownManager", display = "Cooldown Manager", search_name = "EllesmereUI Cooldown Manager", icon_on = ICONS_PATH .. "sidebar\\cdmeffects-ig-on.png", icon_off = ICONS_PATH .. "sidebar\\cdmeffects-ig.png" }, { folder = "EllesmereUIResourceBars", display = "Resource Bars", search_name = "EllesmereUI Resource Bars", icon_on = ICONS_PATH .. "sidebar\\resourcebars-ig-on-2.png", icon_off = ICONS_PATH .. "sidebar\\resourcebars-ig-2.png" }, { folder = "EllesmereUIAuraBuffReminders", display = "AuraBuff Reminders", search_name = "EllesmereUI AuraBuff Reminders", icon_on = ICONS_PATH .. "sidebar\\beacons-ig-on.png", icon_off = ICONS_PATH .. "sidebar\\beacons-ig.png" }, + { folder = "EllesmereUIMythicTimer", display = "Mythic+ Timer", search_name = "EllesmereUI Mythic+ Timer", icon_on = ICONS_PATH .. "sidebar\\consumables-ig-on.png", icon_off = ICONS_PATH .. "sidebar\\consumables-ig.png" }, { folder = "EllesmereUIBasics", display = "Basics", search_name = "EllesmereUI Basics", icon_on = ICONS_PATH .. "sidebar\\basics-ig-on-2.png", icon_off = ICONS_PATH .. "sidebar\\basics-ig-2.png" }, { folder = "EllesmereUIPartyMode", display = "Party Mode", search_name = "EllesmereUI Party Mode", icon_on = ICONS_PATH .. "sidebar\\partymode-ig-on.png", icon_off = ICONS_PATH .. "sidebar\\partymode-ig.png", alwaysLoaded = true }, } From bbc63eb3dedc63ee5a5d2a5ad3e202661d726469 Mon Sep 17 00:00:00 2001 From: Neil U <156554816+Kneeull@users.noreply.github.com> Date: Sat, 11 Apr 2026 00:58:13 +0100 Subject: [PATCH 3/7] Add support for ObjectiveTrackerFrame suppression Added in suppression of QuestTracker for Blizzard Quest Tracker. --- EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua b/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua index 0e15425..ed88d0d 100644 --- a/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua +++ b/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua @@ -199,6 +199,7 @@ end ------------------------------------------------------------------------------- local _blizzHiddenParent local _blizzOrigScenarioParent +local _blizzOrigObjectiveTrackerParent local function SuppressBlizzardMPlus() if not db or not db.profile.enabled then return end @@ -214,6 +215,12 @@ local function SuppressBlizzardMPlus() _blizzOrigScenarioParent = sbf:GetParent() sbf:SetParent(_blizzHiddenParent) end + + local otf = _G.ObjectiveTrackerFrame + if otf and otf:GetParent() ~= _blizzHiddenParent then + _blizzOrigObjectiveTrackerParent = otf:GetParent() + otf:SetParent(_blizzHiddenParent) + end end local function UnsuppressBlizzardMPlus() @@ -221,6 +228,11 @@ local function UnsuppressBlizzardMPlus() if sbf and _blizzOrigScenarioParent and sbf:GetParent() == _blizzHiddenParent then sbf:SetParent(_blizzOrigScenarioParent) end + + local otf = _G.ObjectiveTrackerFrame + if otf and _blizzOrigObjectiveTrackerParent and otf:GetParent() == _blizzHiddenParent then + otf:SetParent(_blizzOrigObjectiveTrackerParent) + end end ------------------------------------------------------------------------------- @@ -324,7 +336,6 @@ end ------------------------------------------------------------------------------- local standaloneFrame -- main container local standaloneCreated = false - -- Font/color helpers (mirrors QT approach but self-contained) local FALLBACK_FONT = "Fonts/FRIZQT__.TTF" local function SFont() @@ -1143,4 +1154,3 @@ function EMT:OnEnable() }) end end - From e5db9dad883381b592e3ac1d9d758ed3527b35ef Mon Sep 17 00:00:00 2001 From: Neil U <156554816+Kneeull@users.noreply.github.com> Date: Sat, 11 Apr 2026 01:38:49 +0100 Subject: [PATCH 4/7] Add option to show enemy text in timer UI --- EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua b/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua index ed88d0d..f8a64d8 100644 --- a/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua +++ b/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua @@ -13,7 +13,6 @@ ns.EMT = EMT ------------------------------------------------------------------------------- local floor, min, max, abs = math.floor, math.min, math.max, math.abs local format = string.format -local GetTime = GetTime local GetWorldElapsedTime = GetWorldElapsedTime local wipe = wipe @@ -37,6 +36,7 @@ local DB_DEFAULTS = { showDeaths = true, showObjectives = true, showEnemyBar = true, + showEnemyText = true, objectiveAlign = "LEFT", timerAlign = "CENTER", titleAlign = "CENTER", -- title / affixes justify @@ -410,7 +410,6 @@ local function CreateStandaloneFrame() standaloneCreated = true local FRAME_W = 260 - local PAD = 8 local f = CreateFrame("Frame", "EllesmereUIMythicTimerStandalone", UIParent, "BackdropTemplate") f:SetSize(FRAME_W, 200) @@ -703,6 +702,7 @@ local function RenderStandalone() local ePad = ContentPad(objAlign) local pctRaw = min(100, max(0, enemyObj.quantity)) local pctPos = p.enemyForcesPctPos or "LABEL" + local showEnemyText = p.showEnemyText ~= false -- Label text: include % only when pctPos is LABEL local label @@ -774,6 +774,10 @@ local function RenderStandalone() end local function RenderEnemyLabel() + if not showEnemyText then + f._enemyFS:Hide() + return + end f._enemyFS:ClearAllPoints() f._enemyFS:SetPoint("TOPLEFT", f, "TOPLEFT", ePad, y) f._enemyFS:SetPoint("TOPRIGHT", f, "TOPRIGHT", -ePad, y) @@ -1154,3 +1158,4 @@ function EMT:OnEnable() }) end end + From 8592f09f4be802c772149623d36bf1b0be42fef2 Mon Sep 17 00:00:00 2001 From: Neil U <156554816+Kneeull@users.noreply.github.com> Date: Sat, 11 Apr 2026 01:39:13 +0100 Subject: [PATCH 5/7] Refactor timer and objective settings in options --- .../EUI_MythicTimer_Options.lua | 134 ++++++++++-------- 1 file changed, 73 insertions(+), 61 deletions(-) diff --git a/EllesmereUIMythicTimer/EUI_MythicTimer_Options.lua b/EllesmereUIMythicTimer/EUI_MythicTimer_Options.lua index 36c7dae..02013c3 100644 --- a/EllesmereUIMythicTimer/EUI_MythicTimer_Options.lua +++ b/EllesmereUIMythicTimer/EUI_MythicTimer_Options.lua @@ -94,46 +94,26 @@ initFrame:SetScript("OnEvent", function(self) setValue=function(v) Set("titleAlign", v); Refresh() end }) y = y - h - -- ── TIMER ────────────────────────────────────────────────────────── - _, h = W:SectionHeader(parent, "TIMER", y); y = y - h - row, h = W:DualRow(parent, y, - { type="toggle", text="+3 Threshold Text", - disabled=function() return Cfg("enabled") == false end, - disabledTooltip="Module is disabled", - getValue=function() return Cfg("showPlusThreeTimer") ~= false end, - setValue=function(v) Set("showPlusThreeTimer", v); Refresh() end }, - { type="toggle", text="+2 Threshold Text", - disabled=function() return Cfg("enabled") == false end, - disabledTooltip="Module is disabled", - getValue=function() return Cfg("showPlusTwoTimer") ~= false end, - setValue=function(v) Set("showPlusTwoTimer", v); Refresh() end }) - y = y - h - - row, h = W:DualRow(parent, y, - { type="toggle", text="+3 Bar Marker", - disabled=function() return Cfg("enabled") == false end, - disabledTooltip="Module is disabled", - getValue=function() return Cfg("showPlusThreeBar") ~= false end, - setValue=function(v) Set("showPlusThreeBar", v); Refresh() end }, - { type="toggle", text="+2 Bar Marker", + { type="dropdown", text="Objective Align", disabled=function() return Cfg("enabled") == false end, disabledTooltip="Module is disabled", - getValue=function() return Cfg("showPlusTwoBar") ~= false end, - setValue=function(v) Set("showPlusTwoBar", v); Refresh() end }) - y = y - h - - row, h = W:DualRow(parent, y, + values=alignValues, + order=alignOrder, + getValue=function() return Cfg("objectiveAlign") or "LEFT" end, + setValue=function(v) Set("objectiveAlign", v); Refresh() end }, { type="dropdown", text="Timer Align", disabled=function() return Cfg("enabled") == false end, disabledTooltip="Module is disabled", values=alignValues, order=alignOrder, getValue=function() return Cfg("timerAlign") or "CENTER" end, - setValue=function(v) Set("timerAlign", v); Refresh() end }, - { type="label", text="" }) + setValue=function(v) Set("timerAlign", v); Refresh() end }) y = y - h + -- ── TIMER ────────────────────────────────────────────────────────── + _, h = W:SectionHeader(parent, "TIMER", y); y = y - h + row, h = W:DualRow(parent, y, { type="toggle", text="Timer Inside Bar", disabled=function() return Cfg("enabled") == false end, @@ -151,76 +131,108 @@ initFrame:SetScript("OnEvent", function(self) setValue=function(r, g, b) Set("timerBarTextColor", { r = r, g = g, b = b }) Refresh() - end }) + end }, + { type="label", text="" }) y = y - h - -- ── OBJECTIVES ───────────────────────────────────────────────────── - _, h = W:SectionHeader(parent, "OBJECTIVES", y); y = y - h + row, h = W:DualRow(parent, y, + { type="toggle", text="+3 Threshold Text", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("showPlusThreeTimer") ~= false end, + setValue=function(v) Set("showPlusThreeTimer", v); Refresh() end }, + { type="toggle", text="+3 Bar Marker", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("showPlusThreeBar") ~= false end, + setValue=function(v) Set("showPlusThreeBar", v); Refresh() end }) + y = y - h row, h = W:DualRow(parent, y, - { type="toggle", text="Show Affixes", + { type="toggle", text="+2 Threshold Text", disabled=function() return Cfg("enabled") == false end, disabledTooltip="Module is disabled", - getValue=function() return Cfg("showAffixes") ~= false end, - setValue=function(v) Set("showAffixes", v); Refresh() end }, - { type="toggle", text="Show Deaths", + getValue=function() return Cfg("showPlusTwoTimer") ~= false end, + setValue=function(v) Set("showPlusTwoTimer", v); Refresh() end }, + { type="toggle", text="+2 Bar Marker", disabled=function() return Cfg("enabled") == false end, disabledTooltip="Module is disabled", - getValue=function() return Cfg("showDeaths") ~= false end, - setValue=function(v) Set("showDeaths", v); Refresh() end }) + getValue=function() return Cfg("showPlusTwoBar") ~= false end, + setValue=function(v) Set("showPlusTwoBar", v); Refresh() end }) y = y - h + -- ── OBJECTIVES ───────────────────────────────────────────────────── + _, h = W:SectionHeader(parent, "OBJECTIVES", y); y = y - h + row, h = W:DualRow(parent, y, - { type="toggle", text="Show Boss Objectives", + { type="toggle", text="Show Affixes", disabled=function() return Cfg("enabled") == false end, disabledTooltip="Module is disabled", - getValue=function() return Cfg("showObjectives") ~= false end, - setValue=function(v) Set("showObjectives", v); Refresh() end }, - { type="toggle", text="Show Enemy Forces", + getValue=function() return Cfg("showAffixes") ~= false end, + setValue=function(v) Set("showAffixes", v); Refresh() end }, + { type="toggle", text="Show Boss Objectives", disabled=function() return Cfg("enabled") == false end, disabledTooltip="Module is disabled", - getValue=function() return Cfg("showEnemyBar") ~= false end, - setValue=function(v) Set("showEnemyBar", v); Refresh() end }) + getValue=function() return Cfg("showObjectives") ~= false end, + setValue=function(v) Set("showObjectives", v); Refresh() end }) y = y - h row, h = W:DualRow(parent, y, - { type="toggle", text="Deaths in Title", + { type="toggle", text="Show Deaths", disabled=function() return Cfg("enabled") == false end, disabledTooltip="Module is disabled", + getValue=function() return Cfg("showDeaths") ~= false end, + setValue=function(v) Set("showDeaths", v); Refresh() end }, + { type="toggle", text="Deaths in Title", + disabled=function() return Cfg("enabled") == false or Cfg("showDeaths") == false end, + disabledTooltip=function() + if Cfg("enabled") == false then return "the module" end + return "Show Deaths" + end, getValue=function() return Cfg("deathsInTitle") == true end, - setValue=function(v) Set("deathsInTitle", v); Refresh() end }, + setValue=function(v) Set("deathsInTitle", v); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, { type="toggle", text="Time Lost in Title", - disabled=function() return Cfg("enabled") == false or Cfg("deathsInTitle") ~= true end, - disabledTooltip="Requires Deaths in Title", + disabled=function() return Cfg("enabled") == false or Cfg("showDeaths") == false or Cfg("deathsInTitle") ~= true end, + disabledTooltip=function() + if Cfg("enabled") == false then return "the module" end + if Cfg("showDeaths") == false then return "Show Deaths" end + return "Deaths in Title" + end, getValue=function() return Cfg("deathTimeInTitle") == true end, - setValue=function(v) Set("deathTimeInTitle", v); Refresh() end }) + setValue=function(v) Set("deathTimeInTitle", v); Refresh() end }, + { type="toggle", text="Show Enemy Forces", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("showEnemyBar") ~= false end, + setValue=function(v) Set("showEnemyBar", v); Refresh() end }) y = y - h row, h = W:DualRow(parent, y, + { type="toggle", text="Show Enemy Forces Text", + disabled=function() return Cfg("enabled") == false or Cfg("showEnemyBar") == false end, + disabledTooltip="Requires Show Enemy Forces", + getValue=function() return Cfg("showEnemyText") ~= false end, + setValue=function(v) Set("showEnemyText", v); Refresh() end }, { type="dropdown", text="Enemy Forces Position", disabled=function() return Cfg("enabled") == false or Cfg("showEnemyBar") == false end, disabledTooltip="Requires Show Enemy Forces", values={ BOTTOM = "Bottom (default)", UNDER_BAR = "Under Timer Bar" }, order={ "BOTTOM", "UNDER_BAR" }, getValue=function() return Cfg("enemyForcesPos") or "BOTTOM" end, - setValue=function(v) Set("enemyForcesPos", v); Refresh() end }, + setValue=function(v) Set("enemyForcesPos", v); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, { type="dropdown", text="Enemy Forces %", disabled=function() return Cfg("enabled") == false or Cfg("showEnemyBar") == false end, disabledTooltip="Requires Show Enemy Forces", values={ LABEL = "In Label Text", BAR = "In Bar", BESIDE = "Beside Bar" }, order={ "LABEL", "BAR", "BESIDE" }, getValue=function() return Cfg("enemyForcesPctPos") or "LABEL" end, - setValue=function(v) Set("enemyForcesPctPos", v); Refresh() end }) - y = y - h - - row, h = W:DualRow(parent, y, - { type="dropdown", text="Objective Align", - disabled=function() return Cfg("enabled") == false end, - disabledTooltip="Module is disabled", - values=alignValues, - order=alignOrder, - getValue=function() return Cfg("objectiveAlign") or "LEFT" end, - setValue=function(v) Set("objectiveAlign", v); Refresh() end }, + setValue=function(v) Set("enemyForcesPctPos", v); Refresh() end }, { type="label", text="" }) y = y - h From 74689189b31d7565f346635afece4606507b2d9d Mon Sep 17 00:00:00 2001 From: Kneeull <156554816+Kneeull@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:40:34 +0100 Subject: [PATCH 6/7] Add Best Runs viewer and expand options Add a new Best Runs viewer tab (EUI_MythicTimer_BestRuns.lua) that parses stored bestRuns, displays per-dungeon/level runs, shows objective splits, allows deleting runs (with confirmation) and includes font helpers and test data injection for development. Update the options UI (EUI_MythicTimer_Options.lua) to add presets, font selection, an Advanced Mode toggle and many new/advanced settings (affix display, compare modes, enemy forces format/position, objective time position, layout and color controls, clear-best-times button, etc.). Refactor config setters (Set/SetPreset), add RebuildPage/IsAdvanced helpers, and wire the Best Runs page into the settings builder so the new viewer is shown under the sidebar. (Also add .toc/main changes to register the new module.) --- .../EUI_MythicTimer_BestRuns.lua | 558 ++++++++ .../EUI_MythicTimer_Options.lua | 706 ++++++++-- .../EllesmereUIMythicTimer.lua | 1135 +++++++++++++---- .../EllesmereUIMythicTimer.toc | 19 + 4 files changed, 2027 insertions(+), 391 deletions(-) create mode 100644 EllesmereUIMythicTimer/EUI_MythicTimer_BestRuns.lua create mode 100644 EllesmereUIMythicTimer/EllesmereUIMythicTimer.toc diff --git a/EllesmereUIMythicTimer/EUI_MythicTimer_BestRuns.lua b/EllesmereUIMythicTimer/EUI_MythicTimer_BestRuns.lua new file mode 100644 index 0000000..b043e60 --- /dev/null +++ b/EllesmereUIMythicTimer/EUI_MythicTimer_BestRuns.lua @@ -0,0 +1,558 @@ +------------------------------------------------------------------------------- +-- EUI_MythicTimer_BestRuns.lua — Best Runs viewer tab +------------------------------------------------------------------------------- +local ADDON_NAME, ns = ... + +local floor = math.floor +local format = string.format +local abs = math.abs + +local initFrame = CreateFrame("Frame") +initFrame:RegisterEvent("PLAYER_LOGIN") +initFrame:SetScript("OnEvent", function(self) + self:UnregisterEvent("PLAYER_LOGIN") + + if not EllesmereUI then return end + + local db + C_Timer.After(0, function() db = _G._EMT_AceDB end) + + local function DB() + if not db then db = _G._EMT_AceDB end + return db and db.profile + end + + -- TEST DATA (remove before release) ────────────────────────────── + local function InjectTestData() + local p = DB() + if not p then return end + if not p.bestRuns then p.bestRuns = {} end + + -- Use current season map IDs from C_ChallengeMode.GetMapTable() + local currentMaps = C_ChallengeMode.GetMapTable() + if not currentMaps or #currentMaps == 0 then return end + + -- Build test runs dynamically from whatever dungeons are in the current season + local testTemplates = { + { level = 12, affixes = { 9, 148 }, deaths = 2, deathTimeLost = 10, date = time() - 86400, elapsed = 1785 }, + { level = 16, affixes = { 9, 148 }, deaths = 4, deathTimeLost = 20, date = time() - 172800, elapsed = 2040 }, + { level = 14, affixes = { 10, 148 }, deaths = 1, deathTimeLost = 5, date = time() - 3600, elapsed = 1620 }, + { level = 10, affixes = { 9, 148 }, deaths = 0, deathTimeLost = 0, date = time() - 7200, elapsed = 1440 }, + { level = 15, affixes = { 10, 148 }, deaths = 3, deathTimeLost = 15, date = time() - 259200, elapsed = 1980 }, + { level = 13, affixes = { 9, 148 }, deaths = 1, deathTimeLost = 5, date = time() - 43200, elapsed = 1710 }, + { level = 11, affixes = { 10, 148 }, deaths = 0, deathTimeLost = 0, date = time() - 600, elapsed = 1350 }, + { level = 18, affixes = { 9, 148 }, deaths = 5, deathTimeLost = 25, date = time() - 14400, elapsed = 2280 }, + } + + local function NormalizeAffixKey(affixes) + local ids = {} + for _, id in ipairs(affixes) do ids[#ids + 1] = id end + table.sort(ids) + return table.concat(ids, "-") + end + + for i, mapID in ipairs(currentMaps) do + local tmpl = testTemplates[((i - 1) % #testTemplates) + 1] + local mapName = C_ChallengeMode.GetMapUIInfo(mapID) + if mapName then + local _, _, timeLimit = C_ChallengeMode.GetMapUIInfo(mapID) + local numBosses = math.min(4, math.max(2, math.floor((timeLimit or 1800) / 500))) + local affixKey = NormalizeAffixKey(tmpl.affixes) + local scopeKey = format("%d:%d:%s", mapID, tmpl.level, affixKey) + + local objTimes = {} + local objNames = {} + local interval = math.floor(tmpl.elapsed / (numBosses + 1)) + for b = 1, numBosses do + objTimes[b] = interval * b + objNames[b] = format("Boss %d", b) + end + local enemyT = math.floor(tmpl.elapsed * 0.92) + + if not p.bestRuns[scopeKey] then + p.bestRuns[scopeKey] = { + elapsed = tmpl.elapsed, + mapID = mapID, + mapName = mapName, + level = tmpl.level, + affixes = tmpl.affixes, + deaths = tmpl.deaths, + deathTimeLost = tmpl.deathTimeLost, + date = tmpl.date, + objectiveTimes = objTimes, + objectiveNames = objNames, + enemyForcesTime = enemyT, + } + end + + -- Add a second level entry for the first 3 dungeons + if i <= 3 then + local tmpl2 = testTemplates[((i) % #testTemplates) + 1] + local affixKey2 = NormalizeAffixKey(tmpl2.affixes) + local scopeKey2 = format("%d:%d:%s", mapID, tmpl2.level, affixKey2) + if not p.bestRuns[scopeKey2] then + local objTimes2 = {} + local objNames2 = {} + local interval2 = math.floor(tmpl2.elapsed / (numBosses + 1)) + for b = 1, numBosses do + objTimes2[b] = interval2 * b + objNames2[b] = format("Boss %d", b) + end + p.bestRuns[scopeKey2] = { + elapsed = tmpl2.elapsed, + mapID = mapID, + mapName = mapName, + level = tmpl2.level, + affixes = tmpl2.affixes, + deaths = tmpl2.deaths, + deathTimeLost = tmpl2.deathTimeLost, + date = tmpl2.date, + objectiveTimes = objTimes2, + objectiveNames = objNames2, + enemyForcesTime = math.floor(tmpl2.elapsed * 0.92), + } + end + end + end + end + end + C_Timer.After(0.5, InjectTestData) + -- END TEST DATA ────────────────────────────────────────────────── + + -- Font helpers (mirrors main file, reads fontPath from same DB) + local FALLBACK_FONT = "Fonts/FRIZQT__.TTF" + local function SFont() + local p = DB() + if p and p.fontPath then return p.fontPath end + if EllesmereUI and EllesmereUI.GetFontPath then + local path = EllesmereUI.GetFontPath("unitFrames") + if path and path ~= "" then return path end + end + return FALLBACK_FONT + end + local function SOutline() + if EllesmereUI.GetFontOutlineFlag then return EllesmereUI.GetFontOutlineFlag() end + return "" + end + local function SetFS(fs, size, flags) + if not fs then return end + local p = SFont() + flags = flags or SOutline() + fs:SetFont(p, size, flags) + if not fs:GetFont() then fs:SetFont(FALLBACK_FONT, size, flags) end + end + local function ApplyShadow(fs) + if not fs then return end + if EllesmereUI.GetFontUseShadow and EllesmereUI.GetFontUseShadow() then + fs:SetShadowColor(0, 0, 0, 0.8); fs:SetShadowOffset(1, -1) + else + fs:SetShadowOffset(0, 0) + end + end + + local function FormatTime(seconds) + if not seconds or seconds < 0 then seconds = 0 end + local whole = floor(seconds) + local m = floor(whole / 60) + local s = floor(whole % 60) + return format("%d:%02d", m, s) + end + + -- State + local selectedMapID = nil + local selectedScopeKey = nil + local deleteConfirmKey = nil + + -- Frame pools + local dungeonBtns = {} + local levelBtns = {} + local detailLines = {} + local deleteBtn = nil + + local function GetButton(pool, parent, idx) + if pool[idx] then + pool[idx]:SetParent(parent) + return pool[idx] + end + local btn = CreateFrame("Button", nil, parent, "BackdropTemplate") + btn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Buttons\\WHITE8x8", + edgeSize = 1, + }) + btn.text = btn:CreateFontString(nil, "OVERLAY") + btn.text:SetPoint("CENTER") + btn.text:SetWordWrap(false) + pool[idx] = btn + return btn + end + + local function GetDetailLine(parent, idx) + if detailLines[idx] then + detailLines[idx]:SetParent(parent) + return detailLines[idx] + end + local fs = parent:CreateFontString(nil, "OVERLAY") + fs:SetWordWrap(false) + detailLines[idx] = fs + return fs + end + + local function StyleButton(btn, size, selected) + local bgR, bgG, bgB, bgA = 0.12, 0.12, 0.14, 0.9 + local borderR, borderG, borderB, borderA = 0.25, 0.25, 0.25, 0.6 + if selected then + bgR, bgG, bgB = 0.08, 0.30, 0.18 + borderR, borderG, borderB = 0.05, 0.83, 0.62 + end + btn:SetBackdropColor(bgR, bgG, bgB, bgA) + btn:SetBackdropBorderColor(borderR, borderG, borderB, borderA) + SetFS(btn.text, size) + ApplyShadow(btn.text) + btn.text:SetTextColor(selected and 1 or 0.75, selected and 1 or 0.75, selected and 1 or 0.75) + btn._selected = selected + btn:SetScript("OnEnter", function(self) + if not self._selected then + self:SetBackdropColor(0.18, 0.18, 0.20, 0.9) + end + end) + btn:SetScript("OnLeave", function(self) + if not self._selected then + self:SetBackdropColor(0.12, 0.12, 0.14, 0.9) + end + end) + end + + -- Parse bestRuns into grouped structure + local function GetDungeonData() + local p = DB() + if not p or not p.bestRuns then return {}, {} end + + local dungeons = {} + local dungeonOrder = {} + + for scopeKey, runData in pairs(p.bestRuns) do + local mapIDStr, levelStr = scopeKey:match("^(%d+):(%d+):") + local mapID = tonumber(mapIDStr) + local level = runData.level or tonumber(levelStr) or 0 + + local mapName = runData.mapName + if not mapName and mapID then + mapName = C_ChallengeMode.GetMapUIInfo(mapID) + end + mapName = mapName or ("Dungeon " .. (mapID or "?")) + + if mapID then + if not dungeons[mapID] then + dungeons[mapID] = { mapName = mapName, entries = {} } + dungeonOrder[#dungeonOrder + 1] = mapID + end + dungeons[mapID].entries[#dungeons[mapID].entries + 1] = { + scopeKey = scopeKey, + level = level, + data = runData, + } + end + end + + table.sort(dungeonOrder, function(a, b) + return (dungeons[a].mapName or "") < (dungeons[b].mapName or "") + end) + + for _, dung in pairs(dungeons) do + table.sort(dung.entries, function(a, b) return a.level > b.level end) + end + + return dungeons, dungeonOrder + end + + local function RebuildPage() + if EllesmereUI.RefreshPage then EllesmereUI:RefreshPage(true) end + end + + -- Build the Best Runs page + _G._EMT_BuildBestRunsPage = function(parent, yOffset) + local y = yOffset + if EllesmereUI.ClearContentHeader then EllesmereUI:ClearContentHeader() end + parent._showRowDivider = false + + local p = DB() + if not p then + parent:SetHeight(40) + return + end + + local dungeons, dungeonOrder = GetDungeonData() + + -- Hide all pooled frames + for i = 1, #dungeonBtns do dungeonBtns[i]:Hide() end + for i = 1, #levelBtns do levelBtns[i]:Hide() end + for i = 1, #detailLines do detailLines[i]:Hide() end + if deleteBtn then deleteBtn:Hide() end + + -- No data state + if #dungeonOrder == 0 then + local noData = GetDetailLine(parent, 1) + SetFS(noData, 14) + ApplyShadow(noData) + noData:SetTextColor(0.5, 0.5, 0.5) + noData:SetText("No best runs recorded yet. Complete a Mythic+ dungeon to see data here.") + noData:ClearAllPoints() + noData:SetPoint("TOPLEFT", parent, "TOPLEFT", 10, y - 20) + noData:SetWidth(500) + noData:SetWordWrap(true) + noData:Show() + parent:SetHeight(60) + return + end + + -- Auto-select first dungeon if none selected + if not selectedMapID or not dungeons[selectedMapID] then + selectedMapID = dungeonOrder[1] + selectedScopeKey = nil + end + + local selectedDungeon = dungeons[selectedMapID] + + -- Auto-select first level + if selectedDungeon and (not selectedScopeKey or not p.bestRuns[selectedScopeKey]) then + if selectedDungeon.entries[1] then + selectedScopeKey = selectedDungeon.entries[1].scopeKey + end + end + + -- Layout constants + local DUNGEON_W = 200 + local LEVEL_W = 70 + local PANEL_GAP = 12 + local BTN_H = 36 + local BTN_GAP = 5 + local DETAIL_LEFT = DUNGEON_W + LEVEL_W + PANEL_GAP * 3 + + -- Dungeon buttons (left column) + local dungY = y + for i, mapID in ipairs(dungeonOrder) do + local dung = dungeons[mapID] + local btn = GetButton(dungeonBtns, parent, i) + btn:SetSize(DUNGEON_W, BTN_H) + btn:ClearAllPoints() + btn:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, dungY) + + local isSelected = (mapID == selectedMapID) + StyleButton(btn, 14, isSelected) + + local displayName = dung.mapName or ("Map " .. mapID) + if #displayName > 22 then + displayName = displayName:sub(1, 21) .. "…" + end + btn.text:SetText(displayName) + + btn:SetScript("OnClick", function() + selectedMapID = mapID + selectedScopeKey = nil + deleteConfirmKey = nil + RebuildPage() + end) + btn:Show() + dungY = dungY - BTN_H - BTN_GAP + end + + -- Level buttons (middle column) + local levelY = y + if selectedDungeon then + for i, entry in ipairs(selectedDungeon.entries) do + local btn = GetButton(levelBtns, parent, i) + btn:SetSize(LEVEL_W, BTN_H) + btn:ClearAllPoints() + btn:SetPoint("TOPLEFT", parent, "TOPLEFT", DUNGEON_W + PANEL_GAP, levelY) + + local isSelected = (entry.scopeKey == selectedScopeKey) + StyleButton(btn, 16, isSelected) + btn.text:SetText("+" .. entry.level) + + btn:SetScript("OnClick", function() + selectedScopeKey = entry.scopeKey + deleteConfirmKey = nil + RebuildPage() + end) + btn:Show() + levelY = levelY - BTN_H - BTN_GAP + end + end + + -- Detail panel (right area) + local detailIdx = 0 + local detailY = y + + local function AddLine(text, r, g, b, size) + detailIdx = detailIdx + 1 + local fs = GetDetailLine(parent, detailIdx) + SetFS(fs, size or 14) + ApplyShadow(fs) + fs:SetTextColor(r or 0.9, g or 0.9, b or 0.9) + fs:SetText(text) + fs:SetWordWrap(false) + fs:ClearAllPoints() + fs:SetPoint("TOPLEFT", parent, "TOPLEFT", DETAIL_LEFT, detailY) + fs:SetWidth(500) + fs:Show() + detailY = detailY - (fs:GetStringHeight() or 18) - 7 + end + + local function AddSpacer(h) + detailY = detailY - (h or 6) + end + + if selectedScopeKey and p.bestRuns[selectedScopeKey] then + local run = p.bestRuns[selectedScopeKey] + local mapName = run.mapName or (selectedDungeon and selectedDungeon.mapName) or "Unknown" + local level = run.level or 0 + + -- Delete button (top-right of detail panel) + if not deleteBtn then + deleteBtn = CreateFrame("Button", nil, parent, "BackdropTemplate") + deleteBtn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Buttons\\WHITE8x8", + edgeSize = 1, + }) + deleteBtn.text = deleteBtn:CreateFontString(nil, "OVERLAY") + deleteBtn.text:SetPoint("CENTER") + deleteBtn.text:SetWordWrap(false) + end + deleteBtn:SetParent(parent) + deleteBtn:SetSize(140, 34) + deleteBtn:ClearAllPoints() + deleteBtn:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -10, y) + + local isConfirm = (deleteConfirmKey == selectedScopeKey) + SetFS(deleteBtn.text, 13) + ApplyShadow(deleteBtn.text) + if isConfirm then + deleteBtn:SetBackdropColor(0.5, 0.1, 0.1, 0.9) + deleteBtn:SetBackdropBorderColor(0.9, 0.2, 0.2, 0.8) + deleteBtn.text:SetTextColor(1, 0.4, 0.4) + deleteBtn.text:SetText("Confirm Delete") + else + deleteBtn:SetBackdropColor(0.15, 0.12, 0.12, 0.9) + deleteBtn:SetBackdropBorderColor(0.4, 0.2, 0.2, 0.6) + deleteBtn.text:SetTextColor(0.9, 0.4, 0.4) + deleteBtn.text:SetText("Delete Run") + end + + local capturedKey = selectedScopeKey + local capturedRun = run + deleteBtn:SetScript("OnClick", function() + if deleteConfirmKey == capturedKey then + -- Remove from bestRuns + if p.bestRuns then + p.bestRuns[capturedKey] = nil + end + -- Remove matching bestObjectiveSplits entries + if p.bestObjectiveSplits and capturedRun then + local mapID = capturedRun.mapID + local lv = capturedRun.level or 0 + if mapID then + local affixKey = capturedKey:match("^%d+:%d+:(.+)$") + -- Exact scope: mapID:level:affixKey + if affixKey then + p.bestObjectiveSplits[format("%s:%d:%s", mapID, lv, affixKey)] = nil + end + -- Level scope: mapID:level + p.bestObjectiveSplits[format("%s:%d", mapID, lv)] = nil + -- Dungeon scope: mapID (only if no other runs for this dungeon) + local hasOtherRuns = false + for key in pairs(p.bestRuns) do + if key:match("^" .. tostring(mapID) .. ":") then + hasOtherRuns = true + break + end + end + if not hasOtherRuns then + p.bestObjectiveSplits[tostring(mapID)] = nil + end + end + end + deleteConfirmKey = nil + if capturedKey == selectedScopeKey then + selectedScopeKey = nil + end + RebuildPage() + else + deleteConfirmKey = capturedKey + RebuildPage() + end + end) + deleteBtn:SetScript("OnLeave", function() + if deleteConfirmKey then + deleteConfirmKey = nil + RebuildPage() + end + end) + deleteBtn:Show() + + -- Header + AddLine(format("%s +%d", mapName, level), 1, 1, 1, 18) + AddSpacer(12) + + -- Total time + AddLine(format("Time: %s", FormatTime(run.elapsed or 0)), 0.05, 0.83, 0.62, 18) + AddSpacer(8) + + -- Objective splits + if run.objectiveTimes then + local maxIdx = 0 + for idx in pairs(run.objectiveTimes) do + if idx > maxIdx then maxIdx = idx end + end + for idx = 1, maxIdx do + local t = run.objectiveTimes[idx] + if t then + local name = (run.objectiveNames and run.objectiveNames[idx]) or ("Objective " .. idx) + AddLine(format("%s: %s", name, FormatTime(t)), 0.75, 0.75, 0.75) + end + end + end + + -- Enemy forces + if run.enemyForcesTime then + AddLine(format("Enemy Forces: %s", FormatTime(run.enemyForcesTime)), 0.75, 0.75, 0.75) + end + + AddSpacer(6) + + -- Deaths + if run.deaths and run.deaths > 0 then + AddLine(format("Deaths: %d (-%s)", run.deaths, FormatTime(run.deathTimeLost or 0)), 0.93, 0.33, 0.33) + else + AddLine("Deaths: 0", 0.5, 0.5, 0.5) + end + + -- Affixes + if run.affixes and #run.affixes > 0 then + local names = {} + for _, id in ipairs(run.affixes) do + local name = C_ChallengeMode.GetAffixInfo(id) + names[#names + 1] = name or ("Affix " .. id) + end + AddLine("Affixes: " .. table.concat(names, ", "), 0.55, 0.55, 0.55) + end + + -- Date + if run.date then + AddLine(format("Date: %s", date("%d/%m/%y %H:%M", run.date)), 0.55, 0.55, 0.55) + else + AddLine("Date: Unknown (pre-tracking)", 0.4, 0.4, 0.4) + end + end + + -- Calculate and set content height + local dungH = abs(dungY - y) + local levelH = abs(levelY - y) + local detailH = abs(detailY - y) + local maxH = dungH + if levelH > maxH then maxH = levelH end + if detailH > maxH then maxH = detailH end + parent:SetHeight(maxH + 20) + end +end) diff --git a/EllesmereUIMythicTimer/EUI_MythicTimer_Options.lua b/EllesmereUIMythicTimer/EUI_MythicTimer_Options.lua index 02013c3..b95a3de 100644 --- a/EllesmereUIMythicTimer/EUI_MythicTimer_Options.lua +++ b/EllesmereUIMythicTimer/EUI_MythicTimer_Options.lua @@ -1,10 +1,10 @@ ------------------------------------------------------------------------------- --- EUI_MythicTimer_Options.lua --- Registers the Mythic+ Timer module with EllesmereUI sidebar options. +-- EUI_MythicTimer_Options.lua — Settings page for M+ Timer ------------------------------------------------------------------------------- local ADDON_NAME, ns = ... local PAGE_DISPLAY = "Mythic+ Timer" +local PAGE_BEST_RUNS = "Best Runs" local initFrame = CreateFrame("Frame") initFrame:RegisterEvent("PLAYER_LOGIN") @@ -28,7 +28,32 @@ initFrame:SetScript("OnEvent", function(self) local function Set(key, val) local p = DB() - if p then p[key] = val end + if p then + p[key] = val + if key ~= "selectedPreset" and key ~= "advancedMode" and key ~= "fontPath" then + p.selectedPreset = "CUSTOM" + end + end + end + + local function SetPreset(presetID) + local p = DB() + if not p then return end + + if presetID == "CUSTOM" then + p.selectedPreset = "CUSTOM" + return + end + + if _G._EMT_ApplyPreset and _G._EMT_ApplyPreset(presetID) then + return + end + + p.selectedPreset = presetID + end + + local function IsAdvanced() + return Cfg("advancedMode") == true end local function Refresh() @@ -36,10 +61,20 @@ initFrame:SetScript("OnEvent", function(self) if EllesmereUI.RefreshPage then EllesmereUI:RefreshPage() end end - --------------------------------------------------------------------------- - -- Build Page - --------------------------------------------------------------------------- - local function BuildPage(_, parent, yOffset) + local function RebuildPage() + if _G._EMT_Apply then _G._EMT_Apply() end + if EllesmereUI.RefreshPage then EllesmereUI:RefreshPage(true) end + end + + -- Build Page + local function BuildPage(pageName, parent, yOffset) + if pageName == PAGE_BEST_RUNS then + if _G._EMT_BuildBestRunsPage then + _G._EMT_BuildBestRunsPage(parent, yOffset) + end + return + end + local W = EllesmereUI.Widgets local y = yOffset local row, h @@ -47,10 +82,42 @@ initFrame:SetScript("OnEvent", function(self) if EllesmereUI.ClearContentHeader then EllesmereUI:ClearContentHeader() end parent._showRowDivider = true + local presetValues = { + CUSTOM = "Custom", + ELLESMERE = "EllesmereUI", + WARP_DEPLETE = "Warp Deplete", + MYTHIC_PLUS_TIMER = "MythicPlusTimer", + } + local presetOrder = { "CUSTOM", "ELLESMERE", "WARP_DEPLETE", "MYTHIC_PLUS_TIMER" } + if _G._EMT_GetPresets then + local values, order = _G._EMT_GetPresets() + if values then presetValues = values end + if order then presetOrder = order end + end + local alignValues = { LEFT = "Left", CENTER = "Center", RIGHT = "Right" } local alignOrder = { "LEFT", "CENTER", "RIGHT" } + local affixDisplayValues = { TEXT = "Text", ICONS = "Icons", BOTH = "Text + Icons" } + local affixDisplayOrder = { "TEXT", "ICONS", "BOTH" } + local compareModeValues = { + NONE = "None", + DUNGEON = "Per Dungeon", + LEVEL = "Per Dungeon + Level", + LEVEL_AFFIX = "Per Dungeon + Level + Affixes", + RUN = "Best Full Run", + } + local compareModeOrder = { "NONE", "DUNGEON", "LEVEL", "LEVEL_AFFIX", "RUN" } + local forcesTextValues = { + PERCENT = "Percent", + COUNT = "Count / Total", + COUNT_PERCENT = "Count / Total + %", + REMAINING = "Remaining Count", + } + local forcesTextOrder = { "PERCENT", "COUNT", "COUNT_PERCENT", "REMAINING" } + local objectiveTimePositionValues = { END = "After Boss Name", START = "Before Boss Name" } + local objectiveTimePositionOrder = { "END", "START" } - -- ── DISPLAY ──────────────────────────────────────────────────────── + -- ── DISPLAY ────────────────────────────────────────────────────── _, h = W:SectionHeader(parent, "DISPLAY", y); y = y - h row, h = W:DualRow(parent, y, @@ -65,103 +132,175 @@ initFrame:SetScript("OnEvent", function(self) y = y - h row, h = W:DualRow(parent, y, - { type="slider", text="Scale", + { type="dropdown", text="Preset", disabled=function() return Cfg("enabled") == false end, disabledTooltip="Module is disabled", - min=0.5, max=2.0, step=0.05, isPercent=false, - getValue=function() return Cfg("scale") or 1.0 end, - setValue=function(v) Set("scale", v); Refresh() end }, - { type="slider", text="Opacity", + values=presetValues, + order=presetOrder, + getValue=function() return Cfg("selectedPreset") or "ELLESMERE" end, + setValue=function(v) SetPreset(v); Refresh() end }, + { type="toggle", text="Advanced Mode", disabled=function() return Cfg("enabled") == false end, disabledTooltip="Module is disabled", - min=0.1, max=1.0, step=0.05, isPercent=false, - getValue=function() return Cfg("standaloneAlpha") or 0.85 end, - setValue=function(v) Set("standaloneAlpha", v); Refresh() end }) + getValue=function() return Cfg("advancedMode") == true end, + setValue=function(v) + local p = DB() + if p then p.advancedMode = v end + RebuildPage() + end }) y = y - h - row, h = W:DualRow(parent, y, - { type="toggle", text="Show Accent Stripe", - disabled=function() return Cfg("enabled") == false end, - disabledTooltip="Module is disabled", - getValue=function() return Cfg("showAccent") == true end, - setValue=function(v) Set("showAccent", v); Refresh() end }, - { type="dropdown", text="Title / Affix Align", - disabled=function() return Cfg("enabled") == false end, - disabledTooltip="Module is disabled", - values=alignValues, - order=alignOrder, - getValue=function() return Cfg("titleAlign") or "CENTER" end, - setValue=function(v) Set("titleAlign", v); Refresh() end }) - y = y - h + local fontValues, fontOrder = {}, {} + if _G._EMT_GetFontOptions then + fontValues, fontOrder = _G._EMT_GetFontOptions() + end row, h = W:DualRow(parent, y, - { type="dropdown", text="Objective Align", + { type="slider", text="Scale", disabled=function() return Cfg("enabled") == false end, disabledTooltip="Module is disabled", - values=alignValues, - order=alignOrder, - getValue=function() return Cfg("objectiveAlign") or "LEFT" end, - setValue=function(v) Set("objectiveAlign", v); Refresh() end }, - { type="dropdown", text="Timer Align", + min=0.5, max=2.0, step=0.05, isPercent=false, + getValue=function() return Cfg("scale") or 1.0 end, + setValue=function(v) Set("scale", v); Refresh() end }, + { type="slider", text="Opacity", disabled=function() return Cfg("enabled") == false end, disabledTooltip="Module is disabled", - values=alignValues, - order=alignOrder, - getValue=function() return Cfg("timerAlign") or "CENTER" end, - setValue=function(v) Set("timerAlign", v); Refresh() end }) + min=0.1, max=1.0, step=0.05, isPercent=false, + getValue=function() return Cfg("standaloneAlpha") or 0.85 end, + setValue=function(v) Set("standaloneAlpha", v); Refresh() end }) y = y - h - -- ── TIMER ────────────────────────────────────────────────────────── - _, h = W:SectionHeader(parent, "TIMER", y); y = y - h - row, h = W:DualRow(parent, y, - { type="toggle", text="Timer Inside Bar", + { type="dropdown", text="Font", disabled=function() return Cfg("enabled") == false end, disabledTooltip="Module is disabled", - getValue=function() return Cfg("timerInBar") == true end, - setValue=function(v) Set("timerInBar", v); Refresh() end }, - { type="colorpicker", text="In-Bar Text Color", - disabled=function() return Cfg("enabled") == false or Cfg("timerInBar") ~= true end, - disabledTooltip="Requires Timer Inside Bar", - getValue=function() - local c = Cfg("timerBarTextColor") - if c then return c.r or 1, c.g or 1, c.b or 1 end - return 1, 1, 1 - end, - setValue=function(r, g, b) - Set("timerBarTextColor", { r = r, g = g, b = b }) + values=fontValues, + order=fontOrder, + getValue=function() return Cfg("fontPath") or "DEFAULT" end, + setValue=function(v) + Set("fontPath", v ~= "DEFAULT" and v or nil) Refresh() end }, { type="label", text="" }) y = y - h - row, h = W:DualRow(parent, y, - { type="toggle", text="+3 Threshold Text", - disabled=function() return Cfg("enabled") == false end, - disabledTooltip="Module is disabled", - getValue=function() return Cfg("showPlusThreeTimer") ~= false end, - setValue=function(v) Set("showPlusThreeTimer", v); Refresh() end }, - { type="toggle", text="+3 Bar Marker", - disabled=function() return Cfg("enabled") == false end, - disabledTooltip="Module is disabled", - getValue=function() return Cfg("showPlusThreeBar") ~= false end, - setValue=function(v) Set("showPlusThreeBar", v); Refresh() end }) - y = y - h + if IsAdvanced() then + row, h = W:DualRow(parent, y, + { type="toggle", text="Show Accent Stripe", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("showAccent") == true end, + setValue=function(v) Set("showAccent", v); Refresh() end }, + { type="toggle", text="Show MS On Completion", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("showCompletedMilliseconds") ~= false end, + setValue=function(v) Set("showCompletedMilliseconds", v); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="dropdown", text="Title / Affix Align", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + values=alignValues, + order=alignOrder, + getValue=function() return Cfg("titleAlign") or "CENTER" end, + setValue=function(v) Set("titleAlign", v); Refresh() end }, + { type="dropdown", text="Timer Align", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + values=alignValues, + order=alignOrder, + getValue=function() return Cfg("timerAlign") or "CENTER" end, + setValue=function(v) Set("timerAlign", v); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="dropdown", text="Objective Align", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + values=alignValues, + order=alignOrder, + getValue=function() return Cfg("objectiveAlign") or "LEFT" end, + setValue=function(v) Set("objectiveAlign", v); Refresh() end }, + { type="dropdown", text="Affix Display", + disabled=function() return Cfg("enabled") == false or Cfg("showAffixes") == false end, + disabledTooltip=function() + if Cfg("enabled") == false then return "the module" end + return "Show Affixes" + end, + values=affixDisplayValues, + order=affixDisplayOrder, + getValue=function() return Cfg("affixDisplayMode") or "TEXT" end, + setValue=function(v) Set("affixDisplayMode", v); Refresh() end }) + y = y - h + end + + -- ── TIMER ──────────────────────────────────────────────────────── + _, h = W:SectionHeader(parent, "TIMER", y); y = y - h row, h = W:DualRow(parent, y, - { type="toggle", text="+2 Threshold Text", + { type="toggle", text="Show Timer Bar", disabled=function() return Cfg("enabled") == false end, disabledTooltip="Module is disabled", - getValue=function() return Cfg("showPlusTwoTimer") ~= false end, - setValue=function(v) Set("showPlusTwoTimer", v); Refresh() end }, - { type="toggle", text="+2 Bar Marker", + getValue=function() return Cfg("showTimerBar") ~= false end, + setValue=function(v) Set("showTimerBar", v); Refresh() end }, + { type="toggle", text="Show Timer Details", disabled=function() return Cfg("enabled") == false end, disabledTooltip="Module is disabled", - getValue=function() return Cfg("showPlusTwoBar") ~= false end, - setValue=function(v) Set("showPlusTwoBar", v); Refresh() end }) + getValue=function() return Cfg("showTimerBreakdown") == true end, + setValue=function(v) Set("showTimerBreakdown", v); Refresh() end }) y = y - h - -- ── OBJECTIVES ───────────────────────────────────────────────────── + if IsAdvanced() then + row, h = W:DualRow(parent, y, + { type="toggle", text="Timer Inside Bar", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("timerInBar") == true end, + setValue=function(v) Set("timerInBar", v); Refresh() end }, + { type="colorpicker", text="In-Bar Text Color", + disabled=function() return Cfg("enabled") == false or Cfg("timerInBar") ~= true end, + disabledTooltip="Requires Timer Inside Bar", + getValue=function() + local c = Cfg("timerBarTextColor") + if c then return c.r or 1, c.g or 1, c.b or 1 end + return 1, 1, 1 + end, + setValue=function(r, g, b) + Set("timerBarTextColor", { r = r, g = g, b = b }) + Refresh() + end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="toggle", text="+3 Threshold Text", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("showPlusThreeTimer") ~= false end, + setValue=function(v) Set("showPlusThreeTimer", v); Refresh() end }, + { type="toggle", text="+3 Bar Marker", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("showPlusThreeBar") ~= false end, + setValue=function(v) Set("showPlusThreeBar", v); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="toggle", text="+2 Threshold Text", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("showPlusTwoTimer") ~= false end, + setValue=function(v) Set("showPlusTwoTimer", v); Refresh() end }, + { type="toggle", text="+2 Bar Marker", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("showPlusTwoBar") ~= false end, + setValue=function(v) Set("showPlusTwoBar", v); Refresh() end }) + y = y - h + end + + -- ── OBJECTIVES ─────────────────────────────────────────────────── _, h = W:SectionHeader(parent, "OBJECTIVES", y); y = y - h row, h = W:DualRow(parent, y, @@ -177,76 +316,399 @@ initFrame:SetScript("OnEvent", function(self) setValue=function(v) Set("showObjectives", v); Refresh() end }) y = y - h + row, h = W:DualRow(parent, y, + { type="toggle", text="Show Objective Times", + disabled=function() return Cfg("enabled") == false or Cfg("showObjectives") == false end, + disabledTooltip=function() + if Cfg("enabled") == false then return "the module" end + return "Show Boss Objectives" + end, + getValue=function() return Cfg("showObjectiveTimes") ~= false end, + setValue=function(v) Set("showObjectiveTimes", v); Refresh() end }, + { type="toggle", text="Show Enemy Forces", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() return Cfg("showEnemyBar") ~= false end, + setValue=function(v) Set("showEnemyBar", v); Refresh() end }) + y = y - h + row, h = W:DualRow(parent, y, { type="toggle", text="Show Deaths", disabled=function() return Cfg("enabled") == false end, disabledTooltip="Module is disabled", getValue=function() return Cfg("showDeaths") ~= false end, setValue=function(v) Set("showDeaths", v); Refresh() end }, - { type="toggle", text="Deaths in Title", + { type="dropdown", text="Death Align", disabled=function() return Cfg("enabled") == false or Cfg("showDeaths") == false end, disabledTooltip=function() if Cfg("enabled") == false then return "the module" end return "Show Deaths" end, - getValue=function() return Cfg("deathsInTitle") == true end, - setValue=function(v) Set("deathsInTitle", v); Refresh() end }) + values=alignValues, + order=alignOrder, + getValue=function() return Cfg("deathAlign") or "LEFT" end, + setValue=function(v) Set("deathAlign", v); Refresh() end }) y = y - h - row, h = W:DualRow(parent, y, - { type="toggle", text="Time Lost in Title", - disabled=function() return Cfg("enabled") == false or Cfg("showDeaths") == false or Cfg("deathsInTitle") ~= true end, - disabledTooltip=function() - if Cfg("enabled") == false then return "the module" end - if Cfg("showDeaths") == false then return "Show Deaths" end - return "Deaths in Title" - end, - getValue=function() return Cfg("deathTimeInTitle") == true end, - setValue=function(v) Set("deathTimeInTitle", v); Refresh() end }, - { type="toggle", text="Show Enemy Forces", - disabled=function() return Cfg("enabled") == false end, - disabledTooltip="Module is disabled", - getValue=function() return Cfg("showEnemyBar") ~= false end, - setValue=function(v) Set("showEnemyBar", v); Refresh() end }) - y = y - h + if IsAdvanced() then + row, h = W:DualRow(parent, y, + { type="dropdown", text="Boss Time Position", + disabled=function() return Cfg("enabled") == false or Cfg("showObjectives") == false or Cfg("showObjectiveTimes") == false end, + disabledTooltip=function() + if Cfg("enabled") == false then return "the module" end + if Cfg("showObjectives") == false then return "Show Boss Objectives" end + return "Show Objective Times" + end, + values=objectiveTimePositionValues, + order=objectiveTimePositionOrder, + getValue=function() return Cfg("objectiveTimePosition") or "END" end, + setValue=function(v) Set("objectiveTimePosition", v); Refresh() end }, + { type="dropdown", text="Enemy Text Format", + disabled=function() return Cfg("enabled") == false or Cfg("showEnemyBar") == false end, + disabledTooltip="Requires Show Enemy Forces", + values=forcesTextValues, + order=forcesTextOrder, + getValue=function() return Cfg("enemyForcesTextFormat") or "PERCENT" end, + setValue=function(v) Set("enemyForcesTextFormat", v); Refresh() end }) + y = y - h - row, h = W:DualRow(parent, y, - { type="toggle", text="Show Enemy Forces Text", - disabled=function() return Cfg("enabled") == false or Cfg("showEnemyBar") == false end, - disabledTooltip="Requires Show Enemy Forces", - getValue=function() return Cfg("showEnemyText") ~= false end, - setValue=function(v) Set("showEnemyText", v); Refresh() end }, - { type="dropdown", text="Enemy Forces Position", - disabled=function() return Cfg("enabled") == false or Cfg("showEnemyBar") == false end, - disabledTooltip="Requires Show Enemy Forces", - values={ BOTTOM = "Bottom (default)", UNDER_BAR = "Under Timer Bar" }, - order={ "BOTTOM", "UNDER_BAR" }, - getValue=function() return Cfg("enemyForcesPos") or "BOTTOM" end, - setValue=function(v) Set("enemyForcesPos", v); Refresh() end }) - y = y - h + row, h = W:DualRow(parent, y, + { type="dropdown", text="Split Compare", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + values=compareModeValues, + order=compareModeOrder, + getValue=function() return Cfg("objectiveCompareMode") or "NONE" end, + setValue=function(v) Set("objectiveCompareMode", v); Refresh() end }, + { type="toggle", text="Delta Only", + disabled=function() return Cfg("enabled") == false or (Cfg("objectiveCompareMode") or "NONE") == "NONE" end, + disabledTooltip="Requires Split Compare", + getValue=function() return Cfg("objectiveCompareDeltaOnly") == true end, + setValue=function(v) Set("objectiveCompareDeltaOnly", v); Refresh() end }) + y = y - h - row, h = W:DualRow(parent, y, - { type="dropdown", text="Enemy Forces %", - disabled=function() return Cfg("enabled") == false or Cfg("showEnemyBar") == false end, - disabledTooltip="Requires Show Enemy Forces", - values={ LABEL = "In Label Text", BAR = "In Bar", BESIDE = "Beside Bar" }, - order={ "LABEL", "BAR", "BESIDE" }, - getValue=function() return Cfg("enemyForcesPctPos") or "LABEL" end, - setValue=function(v) Set("enemyForcesPctPos", v); Refresh() end }, - { type="label", text="" }) - y = y - h + row, h = W:DualRow(parent, y, + { type="toggle", text="Show Upcoming Split Targets", + disabled=function() return Cfg("enabled") == false or (Cfg("objectiveCompareMode") or "NONE") == "NONE" end, + disabledTooltip="Requires Split Compare", + getValue=function() return Cfg("showUpcomingSplitTargets") == true end, + setValue=function(v) Set("showUpcomingSplitTargets", v); Refresh() end }, + { type="button", text="Clear Best Times", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + onClick=function() + local p = DB() + if p then + p.bestObjectiveSplits = {} + p.bestRuns = {} + end + Refresh() + end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="toggle", text="Deaths in Title", + disabled=function() return Cfg("enabled") == false or Cfg("showDeaths") == false end, + disabledTooltip=function() + if Cfg("enabled") == false then return "the module" end + return "Show Deaths" + end, + getValue=function() return Cfg("deathsInTitle") == true end, + setValue=function(v) Set("deathsInTitle", v); Refresh() end }, + { type="toggle", text="Time Lost in Title", + disabled=function() return Cfg("enabled") == false or Cfg("showDeaths") == false or Cfg("deathsInTitle") ~= true end, + disabledTooltip=function() + if Cfg("enabled") == false then return "the module" end + if Cfg("showDeaths") == false then return "Show Deaths" end + return "Deaths in Title" + end, + getValue=function() return Cfg("deathTimeInTitle") == true end, + setValue=function(v) Set("deathTimeInTitle", v); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="toggle", text="Show Enemy Forces Text", + disabled=function() return Cfg("enabled") == false or Cfg("showEnemyBar") == false end, + disabledTooltip="Requires Show Enemy Forces", + getValue=function() return Cfg("showEnemyText") ~= false end, + setValue=function(v) Set("showEnemyText", v); Refresh() end }, + { type="dropdown", text="Enemy Forces Position", + disabled=function() return Cfg("enabled") == false or Cfg("showEnemyBar") == false end, + disabledTooltip="Requires Show Enemy Forces", + values={ BOTTOM = "Bottom (default)", UNDER_BAR = "Under Timer Bar" }, + order={ "BOTTOM", "UNDER_BAR" }, + getValue=function() return Cfg("enemyForcesPos") or "BOTTOM" end, + setValue=function(v) Set("enemyForcesPos", v); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="dropdown", text="Enemy Bar Color", + disabled=function() return Cfg("enabled") == false or Cfg("showEnemyBar") == false end, + disabledTooltip="Requires Show Enemy Forces", + values={ PROGRESS = "Progress (% Breakpoints)", SOLID = "Solid" }, + order={ "PROGRESS", "SOLID" }, + getValue=function() return Cfg("enemyBarColorMode") or "PROGRESS" end, + setValue=function(v) Set("enemyBarColorMode", v); Refresh() end }, + { type="dropdown", text="Enemy Forces %", + disabled=function() return Cfg("enabled") == false or Cfg("showEnemyBar") == false end, + disabledTooltip="Requires Show Enemy Forces", + values={ LABEL = "In Label Text", BAR = "In Bar", BESIDE = "Beside Bar" }, + order={ "LABEL", "BAR", "BESIDE" }, + getValue=function() return Cfg("enemyForcesPctPos") or "LABEL" end, + setValue=function(v) Set("enemyForcesPctPos", v); Refresh() end }) + y = y - h + + end + + if IsAdvanced() then + -- ── LAYOUT ──────────────────────────────────────────────────── + _, h = W:SectionHeader(parent, "LAYOUT", y); y = y - h + + row, h = W:DualRow(parent, y, + { type="slider", text="Frame Width", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + min=220, max=420, step=10, isPercent=false, + getValue=function() return Cfg("frameWidth") or 260 end, + setValue=function(v) Set("frameWidth", v); Refresh() end }, + { type="slider", text="Bar Width", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + min=120, max=360, step=10, isPercent=false, + getValue=function() return Cfg("barWidth") or 220 end, + setValue=function(v) Set("barWidth", v); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="slider", text="Timer Bar Height", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + min=6, max=30, step=1, isPercent=false, + getValue=function() return Cfg("timerBarHeight") or 10 end, + setValue=function(v) Set("timerBarHeight", v); Refresh() end }, + { type="slider", text="Enemy Bar Height", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + min=4, max=20, step=1, isPercent=false, + getValue=function() return Cfg("enemyBarHeight") or 6 end, + setValue=function(v) Set("enemyBarHeight", v); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="slider", text="Element Spacing", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + min=0, max=16, step=1, isPercent=false, + getValue=function() return Cfg("rowGap") or 6 end, + setValue=function(v) Set("rowGap", v); Refresh() end }, + { type="slider", text="Objective Spacing", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + min=0, max=12, step=1, isPercent=false, + getValue=function() return Cfg("objectiveGap") or 3 end, + setValue=function(v) Set("objectiveGap", v); Refresh() end }) + y = y - h + + -- ── COLORS ──────────────────────────────────────────────────── + _, h = W:SectionHeader(parent, "COLORS", y); y = y - h + + row, h = W:DualRow(parent, y, + { type="colorpicker", text="Timer Running", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() + local c = Cfg("timerRunningColor") + if c then return c.r or 1, c.g or 1, c.b or 1 end + return 1, 1, 1 + end, + setValue=function(r, g, b) Set("timerRunningColor", { r = r, g = g, b = b }); Refresh() end }, + { type="colorpicker", text="Timer Warning", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() + local c = Cfg("timerWarningColor") + if c then return c.r or 0.9, c.g or 0.7, c.b or 0.2 end + return 0.9, 0.7, 0.2 + end, + setValue=function(r, g, b) Set("timerWarningColor", { r = r, g = g, b = b }); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="colorpicker", text="Timer Expired", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() + local c = Cfg("timerExpiredColor") + if c then return c.r or 0.9, c.g or 0.2, c.b or 0.2 end + return 0.9, 0.2, 0.2 + end, + setValue=function(r, g, b) Set("timerExpiredColor", { r = r, g = g, b = b }); Refresh() end }, + { type="colorpicker", text="+3 Text", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() + local c = Cfg("timerPlusThreeColor") + if c then return c.r or 0.3, c.g or 0.8, c.b or 1 end + return 0.3, 0.8, 1 + end, + setValue=function(r, g, b) Set("timerPlusThreeColor", { r = r, g = g, b = b }); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="colorpicker", text="+2 Text", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() + local c = Cfg("timerPlusTwoColor") + if c then return c.r or 0.4, c.g or 1, c.b or 0.4 end + return 0.4, 1, 0.4 + end, + setValue=function(r, g, b) Set("timerPlusTwoColor", { r = r, g = g, b = b }); Refresh() end }, + { type="colorpicker", text="Bar Past +3", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() + local c = Cfg("timerBarPastPlusThreeColor") + if c then return c.r or 0.3, c.g or 0.8, c.b or 1 end + return 0.3, 0.8, 1 + end, + setValue=function(r, g, b) Set("timerBarPastPlusThreeColor", { r = r, g = g, b = b }); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="colorpicker", text="Bar Past +2", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() + local c = Cfg("timerBarPastPlusTwoColor") + if c then return c.r or 0.4, c.g or 1, c.b or 0.4 end + return 0.4, 1, 0.4 + end, + setValue=function(r, g, b) Set("timerBarPastPlusTwoColor", { r = r, g = g, b = b }); Refresh() end }, + { type="colorpicker", text="Objective Active", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() + local c = Cfg("objectiveTextColor") + if c then return c.r or 0.9, c.g or 0.9, c.b or 0.9 end + return 0.9, 0.9, 0.9 + end, + setValue=function(r, g, b) Set("objectiveTextColor", { r = r, g = g, b = b }); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="colorpicker", text="Objective Complete", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() + local c = Cfg("objectiveCompletedColor") + if c then return c.r or 0.3, c.g or 0.8, c.b or 0.3 end + return 0.3, 0.8, 0.3 + end, + setValue=function(r, g, b) Set("objectiveCompletedColor", { r = r, g = g, b = b }); Refresh() end }, + { type="colorpicker", text="Deaths", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() + local c = Cfg("deathTextColor") + if c then return c.r or 0.93, c.g or 0.33, c.b or 0.33 end + return 0.93, 0.33, 0.33 + end, + setValue=function(r, g, b) Set("deathTextColor", { r = r, g = g, b = b }); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="colorpicker", text="Split Faster", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() + local c = Cfg("splitFasterColor") + if c then return c.r or 0.4, c.g or 1, c.b or 0.4 end + return 0.4, 1, 0.4 + end, + setValue=function(r, g, b) Set("splitFasterColor", { r = r, g = g, b = b }); Refresh() end }, + { type="colorpicker", text="Split Slower", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + getValue=function() + local c = Cfg("splitSlowerColor") + if c then return c.r or 1, c.g or 0.45, c.b or 0.45 end + return 1, 0.45, 0.45 + end, + setValue=function(r, g, b) Set("splitSlowerColor", { r = r, g = g, b = b }); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="colorpicker", text="Enemy Bar Solid", + disabled=function() return Cfg("enabled") == false or (Cfg("enemyBarColorMode") or "PROGRESS") ~= "SOLID" end, + disabledTooltip="Requires Enemy Bar Color: Solid", + getValue=function() + local c = Cfg("enemyBarSolidColor") + if c then return c.r or 0.35, c.g or 0.55, c.b or 0.8 end + return 0.35, 0.55, 0.8 + end, + setValue=function(r, g, b) Set("enemyBarSolidColor", { r = r, g = g, b = b }); Refresh() end }, + { type="colorpicker", text="Enemy 0-25%", + disabled=function() return Cfg("enabled") == false or (Cfg("enemyBarColorMode") or "PROGRESS") ~= "PROGRESS" end, + disabledTooltip="Requires Enemy Bar Color: Progress", + getValue=function() + local c = Cfg("enemy0to25Color") + if c then return c.r or 0.9, c.g or 0.25, c.b or 0.25 end + return 0.9, 0.25, 0.25 + end, + setValue=function(r, g, b) Set("enemy0to25Color", { r = r, g = g, b = b }); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="colorpicker", text="Enemy 25-50%", + disabled=function() return Cfg("enabled") == false or (Cfg("enemyBarColorMode") or "PROGRESS") ~= "PROGRESS" end, + disabledTooltip="Requires Enemy Bar Color: Progress", + getValue=function() + local c = Cfg("enemy25to50Color") + if c then return c.r or 0.95, c.g or 0.6, c.b or 0.2 end + return 0.95, 0.6, 0.2 + end, + setValue=function(r, g, b) Set("enemy25to50Color", { r = r, g = g, b = b }); Refresh() end }, + { type="colorpicker", text="Enemy 50-75%", + disabled=function() return Cfg("enabled") == false or (Cfg("enemyBarColorMode") or "PROGRESS") ~= "PROGRESS" end, + disabledTooltip="Requires Enemy Bar Color: Progress", + getValue=function() + local c = Cfg("enemy50to75Color") + if c then return c.r or 0.95, c.g or 0.85, c.b or 0.2 end + return 0.95, 0.85, 0.2 + end, + setValue=function(r, g, b) Set("enemy50to75Color", { r = r, g = g, b = b }); Refresh() end }) + y = y - h + + row, h = W:DualRow(parent, y, + { type="colorpicker", text="Enemy 75-100%", + disabled=function() return Cfg("enabled") == false or (Cfg("enemyBarColorMode") or "PROGRESS") ~= "PROGRESS" end, + disabledTooltip="Requires Enemy Bar Color: Progress", + getValue=function() + local c = Cfg("enemy75to100Color") + if c then return c.r or 0.3, c.g or 0.8, c.b or 0.3 end + return 0.3, 0.8, 0.3 + end, + setValue=function(r, g, b) Set("enemy75to100Color", { r = r, g = g, b = b }); Refresh() end }, + { type="label", text="" }) + y = y - h + end parent:SetHeight(math.abs(y - yOffset)) end - --------------------------------------------------------------------------- - -- RegisterModule - --------------------------------------------------------------------------- + -- RegisterModule EllesmereUI:RegisterModule("EllesmereUIMythicTimer", { title = "Mythic+ Timer", icon_on = "Interface\\AddOns\\EllesmereUI\\media\\icons\\sidebar\\consumables-ig.tga", icon_off = "Interface\\AddOns\\EllesmereUI\\media\\icons\\sidebar\\consumables-g.tga", - pages = { PAGE_DISPLAY }, + pages = { PAGE_DISPLAY, PAGE_BEST_RUNS }, buildPage = BuildPage, + onReset = function() + if EllesmereUIMythicTimerDB then + EllesmereUIMythicTimerDB.profiles = nil + EllesmereUIMythicTimerDB.profileKeys = nil + end + end, }) end) diff --git a/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua b/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua index f8a64d8..294aa69 100644 --- a/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua +++ b/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua @@ -1,65 +1,302 @@ ------------------------------------------------------------------------------- --- EllesmereUIMythicTimer.lua --- Mythic+ Dungeon Timer — standalone timer overlay for EllesmereUI. --- Tracks M+ run state (timer, objectives, deaths, affixes) and renders --- a movable standalone frame. Hides the default Blizzard M+ timer. +-- EllesmereUIMythicTimer.lua — M+ Timer overlay for EllesmereUI ------------------------------------------------------------------------------- local ADDON_NAME, ns = ... local EMT = EllesmereUI.Lite.NewAddon(ADDON_NAME) -ns.EMT = EMT -------------------------------------------------------------------------------- --- Lua / WoW API upvalues -------------------------------------------------------------------------------- +-- Upvalues local floor, min, max, abs = math.floor, math.min, math.max, math.abs local format = string.format local GetWorldElapsedTime = GetWorldElapsedTime +local GetTimePreciseSec = GetTimePreciseSec local wipe = wipe -------------------------------------------------------------------------------- --- Constants -------------------------------------------------------------------------------- +-- Constants local PLUS_TWO_RATIO = 0.8 local PLUS_THREE_RATIO = 0.6 +local CHALLENGERS_PERIL_AFFIX_ID = 152 + +local COMPARE_NONE = "NONE" +local COMPARE_DUNGEON = "DUNGEON" +local COMPARE_LEVEL = "LEVEL" +local COMPARE_LEVEL_AFFIX = "LEVEL_AFFIX" +local COMPARE_RUN = "RUN" + +local function CopyTable(src) + if type(src) ~= "table" then return src end + local out = {} + for key, value in pairs(src) do + out[key] = type(value) == "table" and CopyTable(value) or value + end + return out +end -------------------------------------------------------------------------------- --- Database defaults -------------------------------------------------------------------------------- +local PRESET_ORDER = { + "CUSTOM", + "ELLESMERE", + "WARP_DEPLETE", + "MYTHIC_PLUS_TIMER", +} + +local PRESET_LABELS = { + CUSTOM = "Custom", + ELLESMERE = "EllesmereUI", + WARP_DEPLETE = "Warp Deplete", + MYTHIC_PLUS_TIMER = "MythicPlusTimer", +} + +local PRESET_VALUES = { + ELLESMERE = { + showAffixes = true, + showPlusTwoTimer = true, + showPlusThreeTimer = true, + showPlusTwoBar = true, + showPlusThreeBar = true, + showDeaths = true, + showObjectives = true, + showObjectiveTimes = true, + showEnemyBar = true, + showEnemyText = true, + objectiveAlign = "LEFT", + timerAlign = "CENTER", + titleAlign = "CENTER", + standaloneAlpha = 0.85, + showAccent = false, + enemyForcesPos = "BOTTOM", + enemyForcesPctPos = "LABEL", + deathsInTitle = false, + deathTimeInTitle = false, + deathAlign = "LEFT", + timerInBar = false, + showTimerBar = true, + showTimerBreakdown = false, + affixDisplayMode = "TEXT", + enemyForcesTextFormat = "PERCENT", + objectiveTimePosition = "END", + showCompletedMilliseconds = true, + objectiveCompareMode = COMPARE_NONE, + objectiveCompareDeltaOnly = false, + showUpcomingSplitTargets = false, + enemyBarColorMode = "PROGRESS", + enemyBarSolidColor = { r = 0.35, g = 0.55, b = 0.8 }, + frameWidth = 260, + barWidth = 220, + timerBarHeight = 10, + enemyBarHeight = 6, + rowGap = 6, + objectiveGap = 3, + timerRunningColor = { r = 1, g = 1, b = 1 }, + timerWarningColor = { r = 0.9, g = 0.7, b = 0.2 }, + timerExpiredColor = { r = 0.9, g = 0.2, b = 0.2 }, + timerPlusTwoColor = { r = 0.4, g = 1, b = 0.4 }, + timerPlusThreeColor = { r = 0.3, g = 0.8, b = 1 }, + timerBarPastPlusThreeColor = { r = 0.3, g = 0.8, b = 1 }, + timerBarPastPlusTwoColor = { r = 0.4, g = 1, b = 0.4 }, + objectiveTextColor = { r = 0.9, g = 0.9, b = 0.9 }, + objectiveCompletedColor = { r = 0.3, g = 0.8, b = 0.3 }, + splitFasterColor = { r = 0.4, g = 1, b = 0.4 }, + splitSlowerColor = { r = 1, g = 0.45, b = 0.45 }, + deathTextColor = { r = 0.93, g = 0.33, b = 0.33 }, + enemy0to25Color = { r = 0.9, g = 0.25, b = 0.25 }, + enemy25to50Color = { r = 0.95, g = 0.6, b = 0.2 }, + enemy50to75Color = { r = 0.95, g = 0.85, b = 0.2 }, + enemy75to100Color = { r = 0.3, g = 0.8, b = 0.3 }, + }, + WARP_DEPLETE = { + showAffixes = true, + showPlusTwoTimer = true, + showPlusThreeTimer = true, + showPlusTwoBar = true, + showPlusThreeBar = true, + showDeaths = true, + showObjectives = true, + showObjectiveTimes = true, + showEnemyBar = true, + showEnemyText = true, + objectiveAlign = "RIGHT", + timerAlign = "RIGHT", + titleAlign = "RIGHT", + standaloneAlpha = 0.9, + showAccent = false, + enemyForcesPos = "UNDER_BAR", + enemyForcesPctPos = "BAR", + deathsInTitle = false, + deathTimeInTitle = false, + deathAlign = "RIGHT", + timerInBar = false, + showTimerBar = true, + showTimerBreakdown = false, + affixDisplayMode = "TEXT", + enemyForcesTextFormat = "PERCENT", + objectiveTimePosition = "START", + showCompletedMilliseconds = true, + objectiveCompareMode = COMPARE_DUNGEON, + objectiveCompareDeltaOnly = false, + showUpcomingSplitTargets = true, + enemyBarColorMode = "SOLID", + enemyBarSolidColor = { r = 0.73, g = 0.62, b = 0.13 }, + timerBarPastPlusThreeColor = { r = 0.3, g = 0.8, b = 1 }, + timerBarPastPlusTwoColor = { r = 0.4, g = 1, b = 0.4 }, + enemy0to25Color = { r = 0.9, g = 0.25, b = 0.25 }, + enemy25to50Color = { r = 0.95, g = 0.6, b = 0.2 }, + enemy50to75Color = { r = 0.95, g = 0.85, b = 0.2 }, + enemy75to100Color = { r = 0.3, g = 0.8, b = 0.3 }, + }, + MYTHIC_PLUS_TIMER = { + showAffixes = true, + showPlusTwoTimer = true, + showPlusThreeTimer = true, + showPlusTwoBar = false, + showPlusThreeBar = false, + showDeaths = true, + showObjectives = true, + showObjectiveTimes = true, + showEnemyBar = true, + showEnemyText = false, + objectiveAlign = "LEFT", + timerAlign = "LEFT", + titleAlign = "LEFT", + standaloneAlpha = 0.85, + showAccent = false, + enemyForcesPos = "BOTTOM", + enemyForcesPctPos = "BAR", + deathsInTitle = false, + deathTimeInTitle = false, + deathAlign = "LEFT", + timerInBar = false, + showTimerBar = false, + showTimerBreakdown = true, + affixDisplayMode = "TEXT", + enemyForcesTextFormat = "PERCENT", + objectiveTimePosition = "END", + showCompletedMilliseconds = false, + objectiveCompareMode = COMPARE_LEVEL_AFFIX, + objectiveCompareDeltaOnly = false, + showUpcomingSplitTargets = false, + enemyBarColorMode = "PROGRESS", + enemyBarSolidColor = { r = 0.35, g = 0.55, b = 0.8 }, + timerBarPastPlusThreeColor = { r = 0.3, g = 0.8, b = 1 }, + timerBarPastPlusTwoColor = { r = 0.4, g = 1, b = 0.4 }, + enemy0to25Color = { r = 0.8, g = 0.4, b = 0.4 }, + enemy25to50Color = { r = 0.8, g = 0.6, b = 0.3 }, + enemy50to75Color = { r = 0.7, g = 0.75, b = 0.3 }, + enemy75to100Color = { r = 0.4, g = 0.8, b = 0.4 }, + }, +} + +local function ApplyPresetToProfile(profile, presetID) + local preset = PRESET_VALUES[presetID] + if not profile or not preset then return false end + + for key, value in pairs(preset) do + profile[key] = type(value) == "table" and CopyTable(value) or value + end + + profile.selectedPreset = presetID + return true +end + +local function GetPresetValues() + local values = {} + for _, presetID in ipairs(PRESET_ORDER) do + values[presetID] = PRESET_LABELS[presetID] or presetID + end + return values, PRESET_ORDER +end + +local function CalculateBonusTimers(maxTime, affixes) + local plusTwoT = (maxTime or 0) * PLUS_TWO_RATIO + local plusThreeT = (maxTime or 0) * PLUS_THREE_RATIO + + if not maxTime or maxTime <= 0 then + return plusTwoT, plusThreeT + end + + if affixes then + for _, affixID in ipairs(affixes) do + if affixID == CHALLENGERS_PERIL_AFFIX_ID then + local oldTimer = maxTime - 90 + if oldTimer > 0 then + plusTwoT = oldTimer * PLUS_TWO_RATIO + 90 + plusThreeT = oldTimer * PLUS_THREE_RATIO + 90 + end + break + end + end + end + + return plusTwoT, plusThreeT +end + +-- Database defaults local DB_DEFAULTS = { profile = { enabled = true, showAffixes = true, - showPlusTwoTimer = true, -- +2 time remaining text - showPlusThreeTimer = true, -- +3 time remaining text - showPlusTwoBar = true, -- +2 tick marker on progress bar - showPlusThreeBar = true, -- +3 tick marker on progress bar + showPlusTwoTimer = true, + showPlusThreeTimer = true, + showPlusTwoBar = true, + showPlusThreeBar = true, showDeaths = true, showObjectives = true, + showObjectiveTimes = true, showEnemyBar = true, showEnemyText = true, objectiveAlign = "LEFT", timerAlign = "CENTER", - titleAlign = "CENTER", -- title / affixes justify - scale = 1.0, -- standalone frame scale - standaloneAlpha = 0.85, -- standalone background opacity - showAccent = false, -- right-edge accent stripe - showPreview = false, -- show preview frame outside a key - enemyForcesPos = "BOTTOM", -- "BOTTOM" (after objectives) or "UNDER_BAR" - enemyForcesPctPos = "LABEL", -- "LABEL", "BAR", "BESIDE" - deathsInTitle = false, -- show death count next to key name - deathTimeInTitle = false, -- show time lost beside death count - timerInBar = false, -- overlay timer text inside progress bar - timerBarTextColor = nil, -- {r,g,b} override for in-bar timer text + titleAlign = "CENTER", + scale = 1.0, + standaloneAlpha = 0.85, + showAccent = false, + showPreview = false, + enemyForcesPos = "BOTTOM", + enemyForcesPctPos = "LABEL", + deathsInTitle = false, + deathTimeInTitle = false, + deathAlign = "LEFT", + timerInBar = false, + showTimerBar = true, + showTimerBreakdown = false, + affixDisplayMode = "TEXT", + enemyForcesTextFormat = "PERCENT", + objectiveTimePosition = "END", + showCompletedMilliseconds = true, + objectiveCompareMode = "NONE", + objectiveCompareDeltaOnly = false, + showUpcomingSplitTargets = false, + frameWidth = 260, + barWidth = 220, + timerBarHeight = 10, + enemyBarHeight = 6, + rowGap = 6, + objectiveGap = 3, + timerRunningColor = { r = 1, g = 1, b = 1 }, + timerWarningColor = { r = 0.9, g = 0.7, b = 0.2 }, + timerExpiredColor = { r = 0.9, g = 0.2, b = 0.2 }, + timerPlusTwoColor = { r = 0.4, g = 1, b = 0.4 }, + timerPlusThreeColor = { r = 0.3, g = 0.8, b = 1 }, + timerBarPastPlusThreeColor = { r = 0.3, g = 0.8, b = 1 }, + timerBarPastPlusTwoColor = { r = 0.4, g = 1, b = 0.4 }, + objectiveTextColor = { r = 0.9, g = 0.9, b = 0.9 }, + objectiveCompletedColor = { r = 0.3, g = 0.8, b = 0.3 }, + splitFasterColor = { r = 0.4, g = 1, b = 0.4 }, + splitSlowerColor = { r = 1, g = 0.45, b = 0.45 }, + deathTextColor = { r = 0.93, g = 0.33, b = 0.33 }, + enemy0to25Color = { r = 0.9, g = 0.25, b = 0.25 }, + enemy25to50Color = { r = 0.95, g = 0.6, b = 0.2 }, + enemy50to75Color = { r = 0.95, g = 0.85, b = 0.2 }, + enemy75to100Color = { r = 0.3, g = 0.8, b = 0.3 }, + enemyBarColorMode = "PROGRESS", + enemyBarSolidColor = { r = 0.35, g = 0.55, b = 0.8 }, + fontPath = nil, + advancedMode = false, + selectedPreset = "ELLESMERE", }, } -------------------------------------------------------------------------------- --- State -------------------------------------------------------------------------------- -local db -- AceDB-like table (set on init) -local updateTicker -- C_Timer ticker (1 Hz) - --- Current run data +-- State +local db +local updateTicker local currentRun = { active = false, mapID = nil, @@ -74,19 +311,215 @@ local currentRun = { objectives = {}, } -------------------------------------------------------------------------------- --- Time formatting -------------------------------------------------------------------------------- -local function FormatTime(seconds) +-- Helpers +local function FormatTime(seconds, withMilliseconds) if not seconds or seconds < 0 then seconds = 0 end - local m = floor(seconds / 60) - local s = floor(seconds % 60) + local whole = floor(seconds) + local m = floor(whole / 60) + local s = floor(whole % 60) + if withMilliseconds then + local ms = floor(((seconds - whole) * 1000) + 0.5) + if ms >= 1000 then + whole = whole + 1 + m = floor(whole / 60) + s = floor(whole % 60) + ms = 0 + end + return format("%d:%02d.%03d", m, s, ms) + end return format("%d:%02d", m, s) end -------------------------------------------------------------------------------- --- Objective tracking -------------------------------------------------------------------------------- +local function RoundToInt(value) + if not value then return 0 end + return floor(value + 0.5) +end + +local function GetColor(tbl, fallbackR, fallbackG, fallbackB) + if tbl then + return tbl.r or fallbackR, tbl.g or fallbackG, tbl.b or fallbackB + end + return fallbackR, fallbackG, fallbackB +end + +local function GetEnemyForcesColor(profile, percent) + local pct = min(100, max(0, percent or 0)) + + if pct >= 75 then + return GetColor(profile and profile.enemy75to100Color, 0.3, 0.8, 0.3) + elseif pct >= 50 then + return GetColor(profile and profile.enemy50to75Color, 0.95, 0.85, 0.2) + elseif pct >= 25 then + return GetColor(profile and profile.enemy25to50Color, 0.95, 0.6, 0.2) + end + + return GetColor(profile and profile.enemy0to25Color, 0.9, 0.25, 0.25) +end + +local function GetTimerBarFillColor(profile, elapsed, plusThreeTime, plusTwoTime, maxTime) + if maxTime and maxTime > 0 then + if elapsed > maxTime then + return GetColor(profile and profile.timerExpiredColor, 0.9, 0.2, 0.2) + elseif elapsed > plusTwoTime then + return GetColor(profile and profile.timerBarPastPlusTwoColor, 0.4, 1, 0.4) + elseif elapsed > plusThreeTime then + return GetColor(profile and profile.timerBarPastPlusThreeColor, 0.3, 0.8, 1) + end + end + + return GetColor(profile and profile.timerRunningColor, 1, 1, 1) +end + +local function NormalizeAffixKey(affixes) + local ids = {} + for _, affixID in ipairs(affixes or {}) do + ids[#ids + 1] = affixID + end + table.sort(ids) + return table.concat(ids, "-") +end + +local function GetScopeKey(run, mode) + if not run or not run.mapID then return nil end + + if mode == COMPARE_DUNGEON then + return tostring(run.mapID) + elseif mode == COMPARE_LEVEL then + return format("%s:%d", run.mapID, run.level or 0) + elseif mode == COMPARE_LEVEL_AFFIX or mode == COMPARE_RUN then + return format("%s:%d:%s", run.mapID, run.level or 0, NormalizeAffixKey(run.affixes)) + end + + return nil +end + +local function EnsureProfileStore(key) + if not db or not db.profile then return nil end + if not db.profile[key] then db.profile[key] = {} end + return db.profile[key] +end + +local function GetReferenceObjectiveTime(run, objectiveIndex, mode) + if mode == COMPARE_NONE then return nil end + if mode == COMPARE_RUN then + local bestRuns = EnsureProfileStore("bestRuns") + local scopeKey = GetScopeKey(run, COMPARE_RUN) + local bestRun = bestRuns and bestRuns[scopeKey] + return bestRun and bestRun.objectiveTimes and bestRun.objectiveTimes[objectiveIndex] or nil + end + + local store = EnsureProfileStore("bestObjectiveSplits") + local scopeKey = GetScopeKey(run, mode) + local scope = store and scopeKey and store[scopeKey] + return scope and scope[objectiveIndex] or nil +end + +local function UpdateBestObjectiveSplits(run, objectiveIndex, elapsed) + local store = EnsureProfileStore("bestObjectiveSplits") + if not store then return end + + for _, mode in ipairs({ COMPARE_DUNGEON, COMPARE_LEVEL, COMPARE_LEVEL_AFFIX }) do + local scopeKey = GetScopeKey(run, mode) + if scopeKey then + if not store[scopeKey] then store[scopeKey] = {} end + local previous = store[scopeKey][objectiveIndex] + if not previous or elapsed < previous then + store[scopeKey][objectiveIndex] = elapsed + end + end + end +end + +local function UpdateObjectiveCompletion(obj, objectiveIndex) + if not db or not db.profile or not obj or not obj.elapsed or obj.elapsed <= 0 then return end + + local compareMode = db.profile.objectiveCompareMode or COMPARE_NONE + local reference = GetReferenceObjectiveTime(currentRun, objectiveIndex, compareMode) + obj.referenceElapsed = reference + obj.compareDelta = reference and (obj.elapsed - reference) or nil + obj.isNewBest = reference == nil or obj.elapsed < reference + + UpdateBestObjectiveSplits(currentRun, objectiveIndex, obj.elapsed) +end + +local function UpdateBestRun(run) + local bestRuns = EnsureProfileStore("bestRuns") + if not bestRuns then return end + + local scopeKey = GetScopeKey(run, COMPARE_RUN) + if not scopeKey then return end + + local existing = bestRuns[scopeKey] + local objectiveTimes = {} + local objectiveNames = {} + local enemyForcesTime = nil + for index, objective in ipairs(run.objectives) do + if objective.elapsed and objective.elapsed > 0 then + if objective.isWeighted then + enemyForcesTime = objective.elapsed + else + objectiveTimes[index] = objective.elapsed + end + objectiveNames[index] = objective.name + end + end + + if not existing or not existing.elapsed or run.elapsed < existing.elapsed then + bestRuns[scopeKey] = { + elapsed = run.elapsed, + objectiveTimes = objectiveTimes, + objectiveNames = objectiveNames, + enemyForcesTime = enemyForcesTime, + mapID = run.mapID, + mapName = run.mapName, + level = run.level, + affixes = run.affixes, + deaths = run.deaths, + deathTimeLost = run.deathTimeLost, + date = time(), + } + end +end + +local function BuildSplitCompareText(referenceTime, currentTime, deltaOnly, fasterColor, slowerColor) + if not referenceTime or not currentTime then return "" end + + local diff = currentTime - referenceTime + local color = diff <= 0 and fasterColor or slowerColor + local cR, cG, cB = GetColor(color, 0.4, 1, 0.4) + local diffPrefix = diff < 0 and "-" or "+" + local diffText = diff == 0 and "0:00" or FormatTime(abs(diff)) + local colorHex = format("|cff%02x%02x%02x", floor(cR * 255), floor(cG * 255), floor(cB * 255)) + + if deltaOnly then + return format(" %s(%s%s)|r", colorHex, diffPrefix, diffText) + end + + return format(" |cff888888(%s, %s%s%s)|r", FormatTime(referenceTime), colorHex, diffPrefix, diffText) +end + +local function FormatEnemyForcesText(enemyObj, formatId, compact) + local rawCurrent = enemyObj.rawQuantity or enemyObj.quantity or 0 + local rawTotal = enemyObj.rawTotalQuantity or enemyObj.totalQuantity or 100 + local percent = enemyObj.percent or enemyObj.quantity or 0 + local remaining = max(0, rawTotal - rawCurrent) + local prefix = compact and "" or "Enemy Forces " + + if formatId == "COUNT" then + return format("%s%d/%d", prefix, RoundToInt(rawCurrent), RoundToInt(rawTotal)) + elseif formatId == "COUNT_PERCENT" then + return format("%s%d/%d - %.2f%%", prefix, RoundToInt(rawCurrent), RoundToInt(rawTotal), percent) + elseif formatId == "REMAINING" then + if compact then + return format("%d left", RoundToInt(remaining)) + end + return format("%s%d remaining", prefix, RoundToInt(remaining)) + end + + return format("%s%.2f%%", prefix, percent) +end + +-- Objective tracking local function UpdateObjectives() local numCriteria = select(3, C_Scenario.GetStepInfo()) or 0 local elapsed = currentRun.elapsed @@ -102,6 +535,9 @@ local function UpdateObjectives() elapsed = 0, quantity = 0, totalQuantity = 0, + rawQuantity = 0, + rawTotalQuantity = 0, + percent = 0, isWeighted = false, } currentRun.objectives[i] = obj @@ -112,18 +548,29 @@ local function UpdateObjectives() obj.completed = info.completed if obj.completed and not wasCompleted then - obj.elapsed = elapsed + -- On reload, already-completed objectives would get current elapsed. + -- Use persisted split time if available (saved on first completion). + local saved = db and db.profile._activeRunSplits and db.profile._activeRunSplits[i] + if saved and saved > 0 then + obj.elapsed = saved + else + obj.elapsed = elapsed + -- Persist for reload survival + if db and db.profile then + if not db.profile._activeRunSplits then db.profile._activeRunSplits = {} end + db.profile._activeRunSplits[i] = elapsed + end + end + UpdateObjectiveCompletion(obj, i) end obj.quantity = info.quantity or 0 obj.totalQuantity = info.totalQuantity or 0 + obj.rawQuantity = info.quantity or 0 + obj.rawTotalQuantity = info.totalQuantity or 0 if info.isWeightedProgress then obj.isWeighted = true - -- Match the reference addon logic: use the displayed weighted - -- progress value when available, then normalize it against the - -- criterion total. If totalQuantity is 100, this preserves a - -- percent value directly; if totalQuantity is a raw enemy-force - -- cap, this converts raw count -> percent with 2dp precision. + -- Normalize weighted progress to a 0-100 percent value. local rawQuantity = info.quantity or 0 local quantityString = info.quantityString if quantityString and quantityString ~= "" then @@ -139,19 +586,22 @@ local function UpdateObjectives() if obj.totalQuantity and obj.totalQuantity > 0 then local percent = (rawQuantity / obj.totalQuantity) * 100 - local mult = 10 ^ 2 - obj.quantity = math.floor(percent * mult + 0.5) / mult + obj.quantity = floor(percent * 100 + 0.5) / 100 else obj.quantity = rawQuantity end + obj.percent = obj.quantity if obj.completed then obj.quantity = 100 - obj.totalQuantity = 100 + obj.percent = 100 + if obj.rawTotalQuantity and obj.rawTotalQuantity > 0 then + obj.rawQuantity = obj.rawTotalQuantity + end end else obj.isWeighted = false - -- Ensure bosses (single-count) still report 0/1 or 1/1 + obj.percent = 0 if obj.totalQuantity == 0 then obj.quantity = obj.completed and 1 or 0 obj.totalQuantity = 1 @@ -165,21 +615,17 @@ local function UpdateObjectives() end end -------------------------------------------------------------------------------- --- Notify standalone frame to refresh (coalesced) -------------------------------------------------------------------------------- +-- Coalesced refresh local _refreshTimer local function NotifyRefresh() - if _refreshTimer then return end -- already pending + if _refreshTimer then return end _refreshTimer = C_Timer.After(0.05, function() _refreshTimer = nil if _G._EMT_StandaloneRefresh then _G._EMT_StandaloneRefresh() end end) end -------------------------------------------------------------------------------- --- Timer tick (1 Hz while a key is active) -------------------------------------------------------------------------------- +-- Timer tick (1 Hz) local function OnTimerTick() if not currentRun.active then return end @@ -194,9 +640,7 @@ local function OnTimerTick() NotifyRefresh() end -------------------------------------------------------------------------------- --- Suppress / unsuppress Blizzard M+ scenario frame -------------------------------------------------------------------------------- +-- Suppress / restore Blizzard M+ frames local _blizzHiddenParent local _blizzOrigScenarioParent local _blizzOrigObjectiveTrackerParent @@ -209,7 +653,6 @@ local function SuppressBlizzardMPlus() _blizzHiddenParent:Hide() end - -- ScenarioBlocksFrame is the container for Blizzard's M+ timer local sbf = _G.ScenarioBlocksFrame if sbf and sbf:GetParent() ~= _blizzHiddenParent then _blizzOrigScenarioParent = sbf:GetParent() @@ -235,9 +678,7 @@ local function UnsuppressBlizzardMPlus() end end -------------------------------------------------------------------------------- --- Run lifecycle -------------------------------------------------------------------------------- +-- Run lifecycle local function StartRun() local mapID = C_ChallengeMode.GetActiveChallengeMapID() if not mapID then return end @@ -255,6 +696,8 @@ local function StartRun() currentRun.deaths = 0 currentRun.deathTimeLost = 0 currentRun.affixes = affixes or {} + currentRun.preciseStart = GetTimePreciseSec and GetTimePreciseSec() or nil + currentRun.preciseCompletedElapsed = nil wipe(currentRun.objectives) if updateTicker then updateTicker:Cancel() end @@ -273,7 +716,12 @@ local function CompleteRun() local _, elapsedTime = GetWorldElapsedTime(1) currentRun.elapsed = elapsedTime or currentRun.elapsed + if currentRun.preciseStart and GetTimePreciseSec then + currentRun.preciseCompletedElapsed = max(0, GetTimePreciseSec() - currentRun.preciseStart) + end + UpdateBestRun(currentRun) UpdateObjectives() + if db and db.profile then db.profile._activeRunSplits = nil end NotifyRefresh() end @@ -287,8 +735,11 @@ local function ResetRun() currentRun.elapsed = 0 currentRun.deaths = 0 currentRun.deathTimeLost = 0 + currentRun.preciseStart = nil + currentRun.preciseCompletedElapsed = nil wipe(currentRun.affixes) wipe(currentRun.objectives) + if db and db.profile then db.profile._activeRunSplits = nil end if updateTicker then updateTicker:Cancel(); updateTicker = nil end @@ -301,9 +752,7 @@ local function CheckForActiveRun() if mapID then StartRun() end end -------------------------------------------------------------------------------- --- Preview data for configuring outside a key (The Rookery) -------------------------------------------------------------------------------- +-- Preview data local PREVIEW_RUN = { active = true, completed = false, @@ -315,36 +764,66 @@ local PREVIEW_RUN = { deaths = 2, deathTimeLost = 10, affixes = {}, + preciseCompletedElapsed = nil, _previewAffixNames = { "Tyrannical", "Xal'atath's Bargain: Ascendant" }, + _previewAffixIDs = { 9, 152 }, objectives = { - { name = "Kyrioss", completed = true, elapsed = 510, quantity = 1, totalQuantity = 1, isWeighted = false }, - { name = "Stormguard Gorren", completed = true, elapsed = 1005, quantity = 1, totalQuantity = 1, isWeighted = false }, - { name = "Code Taint Monstrosity", completed = false, elapsed = 0, quantity = 0, totalQuantity = 1, isWeighted = false }, - { name = "|cffff3333Ellesmere|r", completed = false, elapsed = 0, quantity = 0, totalQuantity = 1, isWeighted = false }, - { name = "Enemy Forces", completed = false, elapsed = 0, quantity = 78.42, totalQuantity = 100, isWeighted = true }, + { name = "Kyrioss", completed = true, elapsed = 510, quantity = 1, totalQuantity = 1, rawQuantity = 1, rawTotalQuantity = 1, percent = 0, isWeighted = false }, + { name = "Stormguard Gorren", completed = true, elapsed = 1005, quantity = 1, totalQuantity = 1, rawQuantity = 1, rawTotalQuantity = 1, percent = 0, isWeighted = false }, + { name = "Code Taint Monstrosity", completed = false, elapsed = 0, quantity = 0, totalQuantity = 1, rawQuantity = 0, rawTotalQuantity = 1, percent = 0, isWeighted = false }, + { name = "|cffff3333Ellesmere|r", completed = false, elapsed = 0, quantity = 0, totalQuantity = 1, rawQuantity = 0, rawTotalQuantity = 1, percent = 0, isWeighted = false }, + { name = "Enemy Forces", completed = false, elapsed = 0, quantity = 78.42, totalQuantity = 100, rawQuantity = 188, rawTotalQuantity = 240, percent = 78.42, isWeighted = true }, }, } --- Expose apply for options panel _G._EMT_Apply = function() if _G._EMT_StandaloneRefresh then _G._EMT_StandaloneRefresh() end end -------------------------------------------------------------------------------- --- Standalone frame — the primary rendering surface. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -local standaloneFrame -- main container +_G._EMT_GetPresets = GetPresetValues +_G._EMT_ApplyPreset = function(presetID) + if not db or not db.profile then return false end + local applied = ApplyPresetToProfile(db.profile, presetID) + if applied and _G._EMT_StandaloneRefresh then + _G._EMT_StandaloneRefresh() + end + return applied +end + +-- Standalone frame +local standaloneFrame local standaloneCreated = false --- Font/color helpers (mirrors QT approach but self-contained) + +-- Font helpers local FALLBACK_FONT = "Fonts/FRIZQT__.TTF" +local FONT_OPTIONS = { + { key = nil, label = "EllesmereUI Default" }, + { key = "Fonts/FRIZQT__.TTF", label = "Fritz Quadrata" }, + { key = "Fonts/ARIALN.TTF", label = "Arial Narrow" }, + { key = "Fonts/MORPHEUS.TTF", label = "Morpheus" }, + { key = "Fonts/SKURRI.TTF", label = "Skurri" }, + { key = "Fonts/FRIZQT___CYR.TTF", label = "Fritz Quadrata (Cyrillic)" }, + { key = "Fonts/ARHei.TTF", label = "AR Hei (CJK)" }, +} local function SFont() + if db and db.profile and db.profile.fontPath then + return db.profile.fontPath + end if EllesmereUI and EllesmereUI.GetFontPath then local p = EllesmereUI.GetFontPath("unitFrames") if p and p ~= "" then return p end end return FALLBACK_FONT end +_G._EMT_GetFontOptions = function() + local values, order = {}, {} + for _, entry in ipairs(FONT_OPTIONS) do + local k = entry.key or "DEFAULT" + values[k] = entry.label + order[#order + 1] = k + end + return values, order +end local function SOutline() if EllesmereUI.GetFontOutlineFlag then return EllesmereUI.GetFontOutlineFlag() end return "" @@ -371,8 +850,6 @@ local function SetFittedText(fs, text, maxWidth, preferredSize, minSize) preferredSize = preferredSize or 10 minSize = minSize or 8 local outline = SOutline() - -- Ensure a valid font exists before first SetText; startup can - -- render this FontString before any prior SetFont call has happened. SetFS(fs, preferredSize, outline) ApplyShadow(fs) fs:SetText(text) @@ -395,7 +872,6 @@ local function GetAccentColor() return 0.05, 0.83, 0.62 end --- Pool of objective row fontstrings local objRows = {} local function GetObjRow(parent, idx) if objRows[idx] then return objRows[idx] end @@ -405,20 +881,42 @@ local function GetObjRow(parent, idx) return fs end +local affixIcons = {} +local function GetAffixIcon(parent, idx) + if affixIcons[idx] then return affixIcons[idx] end + + local frame = CreateFrame("Frame", nil, parent) + frame:SetSize(16, 16) + + local border = frame:CreateTexture(nil, "OVERLAY") + border:SetAllPoints() + border:SetAtlas("ChallengeMode-AffixRing-Sm") + frame.Border = border + + local portrait = frame:CreateTexture(nil, "ARTWORK") + portrait:SetSize(16, 16) + portrait:SetPoint("CENTER", border) + frame.Portrait = portrait + + frame.SetUp = ScenarioChallengeModeAffixMixin.SetUp + frame:SetScript("OnEnter", ScenarioChallengeModeAffixMixin.OnEnter) + frame:SetScript("OnLeave", GameTooltip_Hide) + + affixIcons[idx] = frame + return frame +end + local function CreateStandaloneFrame() if standaloneCreated then return standaloneFrame end standaloneCreated = true - local FRAME_W = 260 - local f = CreateFrame("Frame", "EllesmereUIMythicTimerStandalone", UIParent, "BackdropTemplate") - f:SetSize(FRAME_W, 200) - f:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + f:SetSize(260, 200) + f:SetPoint("TOPLEFT", UIParent, "CENTER", -130, 100) f:SetFrameStrata("MEDIUM") f:SetFrameLevel(10) f:SetClampedToScreen(true) - -- Background f:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8", edgeFile = "Interface\\Buttons\\WHITE8x8", @@ -427,56 +925,52 @@ local function CreateStandaloneFrame() f:SetBackdropColor(0.05, 0.04, 0.08, 0.85) f:SetBackdropBorderColor(0.15, 0.15, 0.15, 0.6) - -- Accent stripe (right edge) f._accent = f:CreateTexture(nil, "BORDER") f._accent:SetWidth(2) f._accent:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1) f._accent:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1) - -- Banner title f._titleFS = f:CreateFontString(nil, "OVERLAY") f._titleFS:SetWordWrap(false) f._titleFS:SetJustifyV("MIDDLE") - -- Affixes f._affixFS = f:CreateFontString(nil, "OVERLAY") f._affixFS:SetWordWrap(true) + f._affixIconsAnchor = CreateFrame("Frame", nil, f) + f._affixIconsAnchor:SetSize(1, 16) - -- Timer f._timerFS = f:CreateFontString(nil, "OVERLAY") f._timerFS:SetJustifyH("CENTER") - - -- Timer bar bg + f._timerDetailFS = f:CreateFontString(nil, "OVERLAY") + f._timerDetailFS:SetWordWrap(false) f._barBg = f:CreateTexture(nil, "BACKGROUND", nil, 1) f._barFill = f:CreateTexture(nil, "ARTWORK") f._seg3 = f:CreateTexture(nil, "OVERLAY") f._seg2 = f:CreateTexture(nil, "OVERLAY") - - -- Threshold text f._threshFS = f:CreateFontString(nil, "OVERLAY") f._threshFS:SetWordWrap(false) - - -- Deaths f._deathFS = f:CreateFontString(nil, "OVERLAY") f._deathFS:SetWordWrap(false) - - -- Enemy forces label f._enemyFS = f:CreateFontString(nil, "OVERLAY") f._enemyFS:SetWordWrap(false) - - -- Enemy bar f._enemyBarBg = f:CreateTexture(nil, "BACKGROUND", nil, 1) f._enemyBarFill = f:CreateTexture(nil, "ARTWORK") - - -- Preview indicator f._previewFS = f:CreateFontString(nil, "OVERLAY") f._previewFS:SetWordWrap(false) - -- The frame can be created by unlock-mode registration before it has any - -- content to render. Keep it hidden until RenderStandalone() explicitly - -- shows it. + -- Hidden until RenderStandalone() shows it f:Hide() + -- Apply saved scale and position immediately so the frame never flashes at default + if db and db.profile then + f:SetScale(db.profile.scale or 1.0) + if db.profile.standalonePos then + local pos = db.profile.standalonePos + f:ClearAllPoints() + f:SetPoint(pos.point, UIParent, pos.relPoint, pos.x, pos.y) + end + end + standaloneFrame = f return f end @@ -502,21 +996,22 @@ local function RenderStandalone() local f = CreateStandaloneFrame() local PAD = 10 - local ALIGN_PAD = 6 -- extra inset for L/R aligned content + local ALIGN_PAD = 6 local TBAR_PAD = 10 - local TBAR_H = p.timerInBar and 22 or 10 - local ROW_GAP = 6 + local configuredTimerBarH = p.timerBarHeight or 10 + local TBAR_H = p.timerInBar and max(configuredTimerBarH, 22) or configuredTimerBarH + local ENEMY_BAR_H = p.enemyBarHeight or 6 + local ROW_GAP = p.rowGap or 6 + local OBJ_GAP = p.objectiveGap or 3 + + f:SetWidth(p.frameWidth or 260) - -- Scale local scale = p.scale or 1.0 f:SetScale(scale) - - -- Opacity local alpha = p.standaloneAlpha or 0.85 f:SetBackdropColor(0.05, 0.04, 0.08, alpha) f:SetBackdropBorderColor(0.15, 0.15, 0.15, min(alpha, 0.6)) - -- Accent stripe (optional) local aR, aG, aB = GetAccentColor() if p.showAccent then f._accent:SetColorTexture(aR, aG, aB, 0.9) @@ -529,15 +1024,12 @@ local function RenderStandalone() local innerW = frameW - PAD * 2 local y = -PAD - -- Helper: compute padding for content alignment local function ContentPad(align) if align == "LEFT" or align == "RIGHT" then return PAD + ALIGN_PAD end return PAD end - --------------------------------------------------------------------------- - -- Title row (+deaths-in-title when enabled) - --------------------------------------------------------------------------- + -- Title local titleAlign = p.titleAlign or "CENTER" local titleText = format("+%d %s", run.level, run.mapName or "Mythic+") if p.showDeaths and p.deathsInTitle and run.deaths > 0 then @@ -557,22 +1049,33 @@ local function RenderStandalone() f._titleFS:Show() y = y - 22 - ROW_GAP - --------------------------------------------------------------------------- - -- Affixes - --------------------------------------------------------------------------- + -- Affixes if p.showAffixes then local names = {} + local affixIDs = {} if run._previewAffixNames then for _, name in ipairs(run._previewAffixNames) do names[#names + 1] = name end + if run._previewAffixIDs then + for _, affixID in ipairs(run._previewAffixIDs) do + affixIDs[#affixIDs + 1] = affixID + end + end else for _, id in ipairs(run.affixes) do local name = C_ChallengeMode.GetAffixInfo(id) - if name then names[#names + 1] = name end + if name then + names[#names + 1] = name + affixIDs[#affixIDs + 1] = id + end end end - if #names > 0 then + local affixMode = p.affixDisplayMode or "TEXT" + local showAffixText = (affixMode == "TEXT" or affixMode == "BOTH") and #names > 0 + local showAffixIcons = (affixMode == "ICONS" or affixMode == "BOTH") and #affixIDs > 0 + + if showAffixText then f._affixFS:SetTextColor(0.55, 0.55, 0.55) f._affixFS:SetJustifyH(titleAlign) SetFittedText(f._affixFS, table.concat(names, " \194\183 "), innerW, 10, 8) @@ -584,42 +1087,92 @@ local function RenderStandalone() else f._affixFS:Hide() end + + if showAffixIcons then + local iconSpacing = 4 + local iconSize = 16 + local totalIconW = (#affixIDs * iconSize) + ((#affixIDs - 1) * iconSpacing) + f._affixIconsAnchor:ClearAllPoints() + if titleAlign == "RIGHT" then + f._affixIconsAnchor:SetPoint("TOPRIGHT", f, "TOPRIGHT", -PAD, y) + elseif titleAlign == "LEFT" then + f._affixIconsAnchor:SetPoint("TOPLEFT", f, "TOPLEFT", PAD, y) + else + f._affixIconsAnchor:SetPoint("TOP", f, "TOP", 0, y) + end + f._affixIconsAnchor:SetSize(totalIconW, iconSize) + f._affixIconsAnchor:Show() + + for index, affixID in ipairs(affixIDs) do + local icon = GetAffixIcon(f._affixIconsAnchor, index) + icon:ClearAllPoints() + if titleAlign == "RIGHT" then + if index == 1 then + icon:SetPoint("TOPRIGHT", f._affixIconsAnchor, "TOPRIGHT", 0, 0) + else + icon:SetPoint("RIGHT", affixIcons[index - 1], "LEFT", -iconSpacing, 0) + end + else + if index == 1 then + icon:SetPoint("TOPLEFT", f._affixIconsAnchor, "TOPLEFT", 0, 0) + else + icon:SetPoint("LEFT", affixIcons[index - 1], "RIGHT", iconSpacing, 0) + end + end + icon:SetUp(affixID) + icon.affixID = affixID + icon:Show() + end + for index = #affixIDs + 1, #affixIcons do + affixIcons[index]:Hide() + end + + y = y - iconSize - ROW_GAP + else + f._affixIconsAnchor:Hide() + for index = 1, #affixIcons do + affixIcons[index]:Hide() + end + end else f._affixFS:Hide() + f._affixIconsAnchor:Hide() + for index = 1, #affixIcons do + affixIcons[index]:Hide() + end end - --------------------------------------------------------------------------- - -- Deaths row (right after affixes, if not shown in title) - --------------------------------------------------------------------------- + -- Deaths if p.showDeaths and run.deaths > 0 and not p.deathsInTitle then - local objAlign = p.objectiveAlign or "LEFT" - local dPad = ContentPad(objAlign) + local deathAlign = p.deathAlign or "LEFT" + local dPad = ContentPad(deathAlign) SetFS(f._deathFS, 10) ApplyShadow(f._deathFS) - f._deathFS:SetText(format("|cffee5555%d Death%s -%s|r", + local dR, dG, dB = GetColor(p.deathTextColor, 0.93, 0.33, 0.33) + f._deathFS:SetTextColor(dR, dG, dB) + f._deathFS:SetText(format("%d Death%s -%s", run.deaths, run.deaths ~= 1 and "s" or "", FormatTime(run.deathTimeLost))) f._deathFS:ClearAllPoints() f._deathFS:SetPoint("TOPLEFT", f, "TOPLEFT", dPad, y) f._deathFS:SetPoint("TOPRIGHT", f, "TOPRIGHT", -dPad, y) - f._deathFS:SetJustifyH(objAlign) + f._deathFS:SetJustifyH(deathAlign) f._deathFS:Show() y = y - (f._deathFS:GetStringHeight() or 12) - ROW_GAP else f._deathFS:Hide() end - --------------------------------------------------------------------------- - -- Compute timer colours - --------------------------------------------------------------------------- + -- Timer colours local elapsed = run.elapsed or 0 local maxTime = run.maxTime or 0 local timeLeft = max(0, maxTime - elapsed) - local plusThreeT = maxTime * PLUS_THREE_RATIO - local plusTwoT = maxTime * PLUS_TWO_RATIO + local plusTwoT, plusThreeT = CalculateBonusTimers(maxTime, run.affixes) + local completedElapsed = run.preciseCompletedElapsed or elapsed + local timerBarR, timerBarG, timerBarB = GetTimerBarFillColor(p, run.completed and completedElapsed or elapsed, plusThreeT, plusTwoT, maxTime) local timerText if run.completed then - timerText = FormatTime(elapsed) + timerText = FormatTime(completedElapsed, p.showCompletedMilliseconds ~= false) elseif elapsed > maxTime and maxTime > 0 then timerText = "+" .. FormatTime(elapsed - maxTime) else @@ -628,28 +1181,25 @@ local function RenderStandalone() local tR, tG, tB if run.completed then - if elapsed <= plusThreeT then tR, tG, tB = 0.3, 0.8, 1 - elseif elapsed <= plusTwoT then tR, tG, tB = 0.4, 1, 0.4 - elseif elapsed <= maxTime then tR, tG, tB = 0.9, 0.7, 0.2 - else tR, tG, tB = 0.9, 0.2, 0.2 end - elseif timeLeft <= 0 then tR, tG, tB = 0.9, 0.2, 0.2 - elseif timeLeft < maxTime * 0.2 then tR, tG, tB = 0.9, 0.7, 0.2 - else tR, tG, tB = 1, 1, 1 end - - --------------------------------------------------------------------------- - -- Reusable sub-renderers (use upvalue y via closure) - --------------------------------------------------------------------------- + if completedElapsed <= plusThreeT then tR, tG, tB = GetColor(p.timerPlusThreeColor, 0.3, 0.8, 1) + elseif completedElapsed <= plusTwoT then tR, tG, tB = GetColor(p.timerPlusTwoColor, 0.4, 1, 0.4) + elseif completedElapsed <= maxTime then tR, tG, tB = GetColor(p.timerWarningColor, 0.9, 0.7, 0.2) + else tR, tG, tB = GetColor(p.timerExpiredColor, 0.9, 0.2, 0.2) end + elseif timeLeft <= 0 then tR, tG, tB = GetColor(p.timerExpiredColor, 0.9, 0.2, 0.2) + elseif timeLeft < maxTime * 0.2 then tR, tG, tB = GetColor(p.timerWarningColor, 0.9, 0.7, 0.2) + else tR, tG, tB = GetColor(p.timerRunningColor, 1, 1, 1) end local underBarMode = (p.enemyForcesPos == "UNDER_BAR") - -- Threshold text (+3 / +2 remaining) + -- Threshold text local function RenderThresholdText() if (p.showPlusTwoTimer or p.showPlusThreeTimer) and maxTime > 0 then local parts = {} if p.showPlusThreeTimer then local diff = plusThreeT - elapsed if diff >= 0 then - parts[#parts + 1] = format("|cff4dccff+3 %s|r", FormatTime(diff)) + local cR, cG, cB = GetColor(p.timerPlusThreeColor, 0.3, 0.8, 1) + parts[#parts + 1] = format("|cff%02x%02x%02x+3 %s|r", floor(cR * 255), floor(cG * 255), floor(cB * 255), FormatTime(diff)) else parts[#parts + 1] = format("|cff666666+3 -%s|r", FormatTime(abs(diff))) end @@ -657,7 +1207,8 @@ local function RenderStandalone() if p.showPlusTwoTimer then local diff = plusTwoT - elapsed if diff >= 0 then - parts[#parts + 1] = format("|cff66ff66+2 %s|r", FormatTime(diff)) + local cR, cG, cB = GetColor(p.timerPlusTwoColor, 0.4, 1, 0.4) + parts[#parts + 1] = format("|cff%02x%02x%02x+2 %s|r", floor(cR * 255), floor(cG * 255), floor(cB * 255), FormatTime(diff)) else parts[#parts + 1] = format("|cff666666+2 -%s|r", FormatTime(abs(diff))) end @@ -667,7 +1218,7 @@ local function RenderStandalone() ApplyShadow(f._threshFS) f._threshFS:SetTextColor(1, 1, 1) f._threshFS:SetText(table.concat(parts, " ")) - f._threshFS:SetJustifyH("CENTER") + f._threshFS:SetJustifyH(p.timerAlign or "CENTER") f._threshFS:ClearAllPoints() f._threshFS:SetPoint("TOPLEFT", f, "TOPLEFT", PAD, y) f._threshFS:SetPoint("TOPRIGHT", f, "TOPRIGHT", -PAD, y) @@ -681,7 +1232,7 @@ local function RenderStandalone() end end - -- Enemy forces label + bar + -- Enemy forces local function RenderEnemyForces() if not p.showEnemyBar then f._enemyFS:Hide(); f._enemyBarBg:Hide(); f._enemyBarFill:Hide() @@ -704,48 +1255,53 @@ local function RenderStandalone() local pctPos = p.enemyForcesPctPos or "LABEL" local showEnemyText = p.showEnemyText ~= false - -- Label text: include % only when pctPos is LABEL - local label - if pctPos == "LABEL" then - label = format("Enemy Forces %.2f%%", pctRaw) - else - label = "Enemy Forces" - end + local enemyTextFormat = p.enemyForcesTextFormat or "PERCENT" + local label = pctPos == "LABEL" + and FormatEnemyForcesText(enemyObj, enemyTextFormat, false) + or "Enemy Forces" SetFS(f._enemyFS, 10) ApplyShadow(f._enemyFS) if enemyObj.completed then - f._enemyFS:SetTextColor(0.3, 0.8, 0.3) + f._enemyFS:SetTextColor(GetColor(p.objectiveCompletedColor, 0.3, 0.8, 0.3)) else - f._enemyFS:SetTextColor(0.9, 0.9, 0.9) + f._enemyFS:SetTextColor(GetColor(p.objectiveTextColor, 0.9, 0.9, 0.9)) end f._enemyFS:SetText(label) - -- Render bar then text (under-bar), or text then bar (default) local function RenderEnemyBar() - if enemyObj.completed then - f._enemyBarBg:Hide(); f._enemyBarFill:Hide() - if f._enemyBarText then f._enemyBarText:Hide() end - return - end - -- Bar always uses PAD for consistent width; reserve space for beside text - local besideRoom = (pctPos == "BESIDE") and 46 or 0 - local barW = innerW - TBAR_PAD * 2 - besideRoom + local besideRoom = (not enemyObj.completed and pctPos == "BESIDE") and 62 or 0 + local barW = min(p.barWidth or (innerW - TBAR_PAD * 2), innerW - TBAR_PAD * 2) - besideRoom + if barW < 60 then barW = 60 end f._enemyBarBg:ClearAllPoints() - f._enemyBarBg:SetPoint("TOPLEFT", f, "TOPLEFT", PAD + TBAR_PAD, y) - f._enemyBarBg:SetSize(barW, 6) + if objAlign == "RIGHT" then + f._enemyBarBg:SetPoint("TOPRIGHT", f, "TOPRIGHT", -(PAD + TBAR_PAD), y) + elseif objAlign == "CENTER" then + f._enemyBarBg:SetPoint("TOP", f, "TOP", 0, y) + else + f._enemyBarBg:SetPoint("TOPLEFT", f, "TOPLEFT", PAD + TBAR_PAD, y) + end + f._enemyBarBg:SetSize(barW, ENEMY_BAR_H) f._enemyBarBg:SetColorTexture(0.12, 0.12, 0.12, 0.9) f._enemyBarBg:Show() - local epct = min(1, max(0, pctRaw / 100)) + local eR, eG, eB + if enemyObj.completed then + eR, eG, eB = GetColor(p.objectiveCompletedColor, 0.3, 0.8, 0.3) + elseif (p.enemyBarColorMode or "PROGRESS") == "SOLID" then + eR, eG, eB = GetColor(p.enemyBarSolidColor, 0.35, 0.55, 0.8) + else + eR, eG, eB = GetEnemyForcesColor(p, pctRaw) + end + + local epct = enemyObj.completed and 1 or min(1, max(0, pctRaw / 100)) local eFillW = max(1, barW * epct) f._enemyBarFill:ClearAllPoints() f._enemyBarFill:SetPoint("TOPLEFT", f._enemyBarBg, "TOPLEFT", 0, 0) - f._enemyBarFill:SetSize(eFillW, 6) - f._enemyBarFill:SetColorTexture(aR, aG, aB, 0.8) + f._enemyBarFill:SetSize(eFillW, ENEMY_BAR_H) + f._enemyBarFill:SetColorTexture(eR, eG, eB, 0.8) f._enemyBarFill:Show() - -- % overlay / beside bar if not f._enemyBarText then f._enemyBarText = f:CreateFontString(nil, "OVERLAY") f._enemyBarText:SetWordWrap(false) @@ -753,24 +1309,36 @@ local function RenderStandalone() if pctPos == "BAR" then SetFS(f._enemyBarText, 8) ApplyShadow(f._enemyBarText) - f._enemyBarText:SetTextColor(1, 1, 1) - f._enemyBarText:SetText(format("%.2f%%", pctRaw)) + if enemyObj.completed then + f._enemyBarText:SetTextColor(GetColor(p.objectiveCompletedColor, 0.3, 0.8, 0.3)) + else + f._enemyBarText:SetTextColor(GetColor(p.objectiveTextColor, 0.9, 0.9, 0.9)) + end + f._enemyBarText:SetText(FormatEnemyForcesText(enemyObj, enemyTextFormat, true)) f._enemyBarText:ClearAllPoints() f._enemyBarText:SetPoint("CENTER", f._enemyBarBg, "CENTER", 0, 0) f._enemyBarText:Show() elseif pctPos == "BESIDE" then SetFS(f._enemyBarText, 8) ApplyShadow(f._enemyBarText) - f._enemyBarText:SetTextColor(0.9, 0.9, 0.9) - f._enemyBarText:SetText(format("%.2f%%", pctRaw)) + if enemyObj.completed then + f._enemyBarText:SetTextColor(GetColor(p.objectiveCompletedColor, 0.3, 0.8, 0.3)) + else + f._enemyBarText:SetTextColor(GetColor(p.objectiveTextColor, 0.9, 0.9, 0.9)) + end + f._enemyBarText:SetText(FormatEnemyForcesText(enemyObj, enemyTextFormat, true)) f._enemyBarText:ClearAllPoints() - f._enemyBarText:SetPoint("LEFT", f._enemyBarBg, "RIGHT", 4, 0) + if objAlign == "RIGHT" then + f._enemyBarText:SetPoint("RIGHT", f._enemyBarBg, "LEFT", -4, 0) + else + f._enemyBarText:SetPoint("LEFT", f._enemyBarBg, "RIGHT", 4, 0) + end f._enemyBarText:Show() else f._enemyBarText:Hide() end - y = y - 10 - ROW_GAP + y = y - ENEMY_BAR_H - ROW_GAP end local function RenderEnemyLabel() @@ -787,23 +1355,15 @@ local function RenderStandalone() end if underBarMode then - -- Under-bar: bar first, label below RenderEnemyBar() RenderEnemyLabel() else - -- Default: label first, bar below RenderEnemyLabel() RenderEnemyBar() end end - --------------------------------------------------------------------------- - -- Layout: under-bar mode renders timer then thresholds then bar then enemy - --------------------------------------------------------------------------- - - --------------------------------------------------------------------------- - -- Timer text (above bar, unless timerInBar) - --------------------------------------------------------------------------- + -- Timer text if not p.timerInBar then local timerAlign = p.timerAlign or "CENTER" SetFS(f._timerFS, 20) @@ -829,48 +1389,72 @@ local function RenderStandalone() f._timerFS:Hide() end - --------------------------------------------------------------------------- - -- Under-bar mode: thresholds between timer and bar - --------------------------------------------------------------------------- + if p.showTimerBreakdown and maxTime > 0 then + local timerAlign = p.timerAlign or "CENTER" + SetFS(f._timerDetailFS, 10) + ApplyShadow(f._timerDetailFS) + f._timerDetailFS:SetTextColor(0.65, 0.65, 0.65) + f._timerDetailFS:SetText(format("%s / %s", FormatTime(elapsed), FormatTime(maxTime))) + f._timerDetailFS:SetJustifyH(timerAlign) + f._timerDetailFS:ClearAllPoints() + local detailBlockW = min(innerW, max(140, floor(innerW * 0.72))) + if timerAlign == "RIGHT" then + f._timerDetailFS:SetPoint("TOPRIGHT", f, "TOPRIGHT", -(PAD + ALIGN_PAD), y) + elseif timerAlign == "LEFT" then + f._timerDetailFS:SetPoint("TOPLEFT", f, "TOPLEFT", PAD + ALIGN_PAD, y) + else + f._timerDetailFS:SetPoint("TOP", f, "TOP", 0, y) + end + f._timerDetailFS:SetWidth(detailBlockW) + f._timerDetailFS:Show() + y = y - (f._timerDetailFS:GetStringHeight() or 10) - ROW_GAP + else + f._timerDetailFS:Hide() + end + if underBarMode then RenderThresholdText() end - --------------------------------------------------------------------------- - -- Timer progress bar - --------------------------------------------------------------------------- - if maxTime > 0 then - local barW = innerW - TBAR_PAD * 2 + -- Timer bar + if maxTime > 0 and p.showTimerBar ~= false then + local barW = min(p.barWidth or (innerW - TBAR_PAD * 2), innerW - TBAR_PAD * 2) + if barW < 60 then barW = 60 end f._barBg:ClearAllPoints() - f._barBg:SetPoint("TOPLEFT", f, "TOPLEFT", PAD + TBAR_PAD, y) + if (p.timerAlign or "CENTER") == "RIGHT" then + f._barBg:SetPoint("TOPRIGHT", f, "TOPRIGHT", -(PAD + TBAR_PAD), y) + elseif (p.timerAlign or "CENTER") == "LEFT" then + f._barBg:SetPoint("TOPLEFT", f, "TOPLEFT", PAD + TBAR_PAD, y) + else + f._barBg:SetPoint("TOP", f, "TOP", 0, y) + end f._barBg:SetSize(barW, TBAR_H) f._barBg:SetColorTexture(0.12, 0.12, 0.12, 0.9) f._barBg:Show() - local fillPct = math.min(1, elapsed / maxTime) - local fillW = math.max(1, barW * fillPct) + local fillPct = min(1, elapsed / maxTime) + local fillW = max(1, barW * fillPct) f._barFill:ClearAllPoints() f._barFill:SetPoint("TOPLEFT", f._barBg, "TOPLEFT", 0, 0) f._barFill:SetSize(fillW, TBAR_H) - f._barFill:SetColorTexture(tR, tG, tB, 0.85) + f._barFill:SetColorTexture(timerBarR, timerBarG, timerBarB, 0.85) f._barFill:Show() - -- +3 marker (60%) + -- +3 marker f._seg3:ClearAllPoints() f._seg3:SetSize(1, TBAR_H + 4) - f._seg3:SetPoint("TOP", f._barBg, "TOPLEFT", floor(barW * 0.6), 2) + f._seg3:SetPoint("TOP", f._barBg, "TOPLEFT", floor(barW * (plusThreeT / maxTime)), 2) f._seg3:SetColorTexture(0.3, 0.8, 1, 0.9) if p.showPlusThreeBar then f._seg3:Show() else f._seg3:Hide() end - -- +2 marker (80%) + -- +2 marker f._seg2:ClearAllPoints() f._seg2:SetSize(1, TBAR_H + 4) - f._seg2:SetPoint("TOP", f._barBg, "TOPLEFT", floor(barW * 0.8), 2) + f._seg2:SetPoint("TOP", f._barBg, "TOPLEFT", floor(barW * (plusTwoT / maxTime)), 2) f._seg2:SetColorTexture(0.4, 1, 0.4, 0.9) if p.showPlusTwoBar then f._seg2:Show() else f._seg2:Hide() end - -- Timer text overlay inside bar if p.timerInBar then if not f._barTimerFS then f._barTimerFS = f:CreateFontString(nil, "OVERLAY") @@ -899,23 +1483,15 @@ local function RenderStandalone() if f._barTimerFS then f._barTimerFS:Hide() end end - --------------------------------------------------------------------------- - -- Under-bar mode: enemy forces immediately after bar - --------------------------------------------------------------------------- if underBarMode then RenderEnemyForces() end - --------------------------------------------------------------------------- - -- Default mode: thresholds after bar - --------------------------------------------------------------------------- if not underBarMode then RenderThresholdText() end - --------------------------------------------------------------------------- - -- Objectives - --------------------------------------------------------------------------- + -- Objectives local objIdx = 0 if p.showObjectives then local objAlign = p.objectiveAlign or "LEFT" @@ -933,15 +1509,28 @@ local function RenderStandalone() end if obj.completed then displayName = "|TInterface\\RAIDFRAME\\ReadyCheck-Ready:0|t " .. displayName - row:SetTextColor(0.3, 0.8, 0.3) + row:SetTextColor(GetColor(p.objectiveCompletedColor, 0.3, 0.8, 0.3)) else - row:SetTextColor(0.9, 0.9, 0.9) + row:SetTextColor(GetColor(p.objectiveTextColor, 0.9, 0.9, 0.9)) end local timeStr = "" - if obj.completed and obj.elapsed and obj.elapsed > 0 then - timeStr = " |cff888888" .. FormatTime(obj.elapsed) .. "|r" + if p.showObjectiveTimes ~= false and obj.completed and obj.elapsed and obj.elapsed > 0 then + timeStr = "|cff888888" .. FormatTime(obj.elapsed) .. "|r" + end + local compareSuffix = "" + if obj.completed and obj.referenceElapsed then + compareSuffix = BuildSplitCompareText(obj.referenceElapsed, obj.elapsed, p.objectiveCompareDeltaOnly, p.splitFasterColor, p.splitSlowerColor) + elseif (not obj.completed) and p.showUpcomingSplitTargets and (p.objectiveCompareMode or COMPARE_NONE) ~= COMPARE_NONE then + local target = GetReferenceObjectiveTime(run, i, p.objectiveCompareMode or COMPARE_NONE) + if target then + compareSuffix = " |cff888888PB " .. FormatTime(target) .. "|r" + end + end + if timeStr ~= "" and (p.objectiveTimePosition or "END") == "START" then + row:SetText(timeStr .. " " .. displayName .. compareSuffix) + else + row:SetText(displayName .. (timeStr ~= "" and (" " .. timeStr) or "") .. compareSuffix) end - row:SetText(displayName .. timeStr) row:SetJustifyH(objAlign) row:ClearAllPoints() local oInnerW = frameW - oPad * 2 @@ -955,32 +1544,22 @@ local function RenderStandalone() end row:SetWidth(objBlockW) row:Show() - y = y - (row:GetStringHeight() or 12) - 3 + y = y - (row:GetStringHeight() or 12) - OBJ_GAP end end end - -- Hide unused objective rows for i = objIdx + 1, #objRows do objRows[i]:Hide() end - --------------------------------------------------------------------------- - -- Default mode: enemy forces at bottom - --------------------------------------------------------------------------- if not underBarMode then RenderEnemyForces() end - --------------------------------------------------------------------------- - -- Resize frame to content - --------------------------------------------------------------------------- local totalH = abs(y) + PAD f:SetHeight(totalH) - --------------------------------------------------------------------------- - -- Preview indicator - --------------------------------------------------------------------------- if isPreview then SetFS(f._previewFS, 8) f._previewFS:SetTextColor(0.5, 0.5, 0.5, 0.6) @@ -995,10 +1574,7 @@ local function RenderStandalone() f:Show() end --- Global refresh callback for standalone frame _G._EMT_StandaloneRefresh = RenderStandalone - --- Expose standalone frame getter for unlock mode _G._EMT_GetStandaloneFrame = function() return CreateStandaloneFrame() end @@ -1006,6 +1582,7 @@ end local function ApplyStandalonePosition() if not db then return end if not standaloneFrame then return end + standaloneFrame:SetScale(db.profile.scale or 1.0) local pos = db.profile.standalonePos if pos then standaloneFrame:ClearAllPoints() @@ -1075,37 +1652,50 @@ function EMT:OnInitialize() db = EllesmereUI.Lite.NewDB("EllesmereUIMythicTimerDB", DB_DEFAULTS) _G._EMT_AceDB = db - if db and db.profile and db.profile.objectiveAlign == nil then - local oldAlign = db.profile.thresholdAlign - if oldAlign == "RIGHT" then - db.profile.objectiveAlign = "RIGHT" - elseif oldAlign == "CENTER" then - db.profile.objectiveAlign = "CENTER" - else - db.profile.objectiveAlign = "LEFT" + if db and db.profile then + local pp = db.profile + for key, value in pairs(DB_DEFAULTS.profile) do + if pp[key] == nil then + pp[key] = type(value) == "table" and CopyTable(value) or value + end end end - if db and db.profile and db.profile.timerAlign == nil then - db.profile.timerAlign = "CENTER" - end + -- Season-based data purge: clear best runs/splits from previous seasons + C_Timer.After(2, function() + if not db or not db.profile then return end + local currentMaps = C_ChallengeMode.GetMapTable() + if not currentMaps or #currentMaps == 0 then return end - -- Migrate: detached is no longer a setting (always standalone) - if db and db.profile then - local pp = db.profile - pp.detached = nil + local validMapIDs = {} + for _, mapID in ipairs(currentMaps) do + validMapIDs[mapID] = true + end + + local purged = false - if pp.showPlusTwo ~= nil and pp.showPlusTwoTimer == nil then - pp.showPlusTwoTimer = pp.showPlusTwo - pp.showPlusTwoBar = pp.showPlusTwo - pp.showPlusTwo = nil + if db.profile.bestRuns then + for scopeKey in pairs(db.profile.bestRuns) do + local mapIDStr = scopeKey:match("^(%d+):") + local mapID = tonumber(mapIDStr) + if mapID and not validMapIDs[mapID] then + db.profile.bestRuns[scopeKey] = nil + purged = true + end + end end - if pp.showPlusThree ~= nil and pp.showPlusThreeTimer == nil then - pp.showPlusThreeTimer = pp.showPlusThree - pp.showPlusThreeBar = pp.showPlusThree - pp.showPlusThree = nil + + if db.profile.bestObjectiveSplits then + for scopeKey in pairs(db.profile.bestObjectiveSplits) do + local mapIDStr = scopeKey:match("^(%d+)") + local mapID = tonumber(mapIDStr) + if mapID and not validMapIDs[mapID] then + db.profile.bestObjectiveSplits[scopeKey] = nil + purged = true + end + end end - end + end) runtimeFrame:SetScript("OnUpdate", RuntimeOnUpdate) end @@ -1113,7 +1703,6 @@ end function EMT:OnEnable() if not db or not db.profile.enabled then return end - -- Register with unlock mode if EllesmereUI and EllesmereUI.RegisterUnlockElements and EllesmereUI.MakeUnlockElement then local MK = EllesmereUI.MakeUnlockElement EllesmereUI:RegisterUnlockElements({ @@ -1135,10 +1724,17 @@ function EMT:OnEnable() return false end, savePos = function(_, point, relPoint, x, y) - db.profile.standalonePos = { point = point, relPoint = relPoint, x = x, y = y } - if standaloneFrame and not EllesmereUI._unlockActive then - standaloneFrame:ClearAllPoints() - standaloneFrame:SetPoint(point, UIParent, relPoint, x, y) + -- Save in frame's own coordinate space (TOPLEFT so height grows downward) + local f = standaloneFrame + if f and f:GetLeft() and f:GetTop() then + db.profile.standalonePos = { point = "TOPLEFT", relPoint = "BOTTOMLEFT", x = f:GetLeft(), y = f:GetTop() } + else + db.profile.standalonePos = { point = point, relPoint = relPoint, x = x, y = y } + end + if f and not EllesmereUI._unlockActive then + local pos = db.profile.standalonePos + f:ClearAllPoints() + f:SetPoint(pos.point, UIParent, pos.relPoint, pos.x, pos.y) end end, loadPos = function() @@ -1150,6 +1746,7 @@ function EMT:OnEnable() applyPos = function() local pos = db.profile.standalonePos if pos and standaloneFrame then + standaloneFrame:SetScale(db.profile.scale or 1.0) standaloneFrame:ClearAllPoints() standaloneFrame:SetPoint(pos.point, UIParent, pos.relPoint, pos.x, pos.y) end diff --git a/EllesmereUIMythicTimer/EllesmereUIMythicTimer.toc b/EllesmereUIMythicTimer/EllesmereUIMythicTimer.toc new file mode 100644 index 0000000..b1b93ab --- /dev/null +++ b/EllesmereUIMythicTimer/EllesmereUIMythicTimer.toc @@ -0,0 +1,19 @@ +## Interface: 120000, 120001 +## Title: |cff0cd29fEllesmereUI|r Mythic+ Timer +## Category: |cff0cd29fEllesmere|rUI +## Group: EllesmereUI +## Notes: Customizable Mythic+ dungeon timer with objective tracking +## Author: Ellesmere +## Version: 6.4.7 +## Dependencies: EllesmereUI +## SavedVariables: EllesmereUIMythicTimerDB +## IconTexture: Interface\AddOns\EllesmereUI\media\eg-logo.tga + +# Main Lua +EllesmereUIMythicTimer.lua + +# Best Runs Viewer +EUI_MythicTimer_BestRuns.lua + +# Options +EUI_MythicTimer_Options.lua From 61f54768c537acc1a60c71a7c485a12fdf115308 Mon Sep 17 00:00:00 2001 From: Kneeull <156554816+Kneeull@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:50:47 +0100 Subject: [PATCH 7/7] Update Reset function to do what it's meant to do Fixing reset function to fully reset the module to base settings. --- .../EUI_MythicTimer_BestRuns.lua | 97 ------------------- .../EllesmereUIMythicTimer.lua | 24 +++++ 2 files changed, 24 insertions(+), 97 deletions(-) diff --git a/EllesmereUIMythicTimer/EUI_MythicTimer_BestRuns.lua b/EllesmereUIMythicTimer/EUI_MythicTimer_BestRuns.lua index b043e60..8c46537 100644 --- a/EllesmereUIMythicTimer/EUI_MythicTimer_BestRuns.lua +++ b/EllesmereUIMythicTimer/EUI_MythicTimer_BestRuns.lua @@ -22,103 +22,6 @@ initFrame:SetScript("OnEvent", function(self) return db and db.profile end - -- TEST DATA (remove before release) ────────────────────────────── - local function InjectTestData() - local p = DB() - if not p then return end - if not p.bestRuns then p.bestRuns = {} end - - -- Use current season map IDs from C_ChallengeMode.GetMapTable() - local currentMaps = C_ChallengeMode.GetMapTable() - if not currentMaps or #currentMaps == 0 then return end - - -- Build test runs dynamically from whatever dungeons are in the current season - local testTemplates = { - { level = 12, affixes = { 9, 148 }, deaths = 2, deathTimeLost = 10, date = time() - 86400, elapsed = 1785 }, - { level = 16, affixes = { 9, 148 }, deaths = 4, deathTimeLost = 20, date = time() - 172800, elapsed = 2040 }, - { level = 14, affixes = { 10, 148 }, deaths = 1, deathTimeLost = 5, date = time() - 3600, elapsed = 1620 }, - { level = 10, affixes = { 9, 148 }, deaths = 0, deathTimeLost = 0, date = time() - 7200, elapsed = 1440 }, - { level = 15, affixes = { 10, 148 }, deaths = 3, deathTimeLost = 15, date = time() - 259200, elapsed = 1980 }, - { level = 13, affixes = { 9, 148 }, deaths = 1, deathTimeLost = 5, date = time() - 43200, elapsed = 1710 }, - { level = 11, affixes = { 10, 148 }, deaths = 0, deathTimeLost = 0, date = time() - 600, elapsed = 1350 }, - { level = 18, affixes = { 9, 148 }, deaths = 5, deathTimeLost = 25, date = time() - 14400, elapsed = 2280 }, - } - - local function NormalizeAffixKey(affixes) - local ids = {} - for _, id in ipairs(affixes) do ids[#ids + 1] = id end - table.sort(ids) - return table.concat(ids, "-") - end - - for i, mapID in ipairs(currentMaps) do - local tmpl = testTemplates[((i - 1) % #testTemplates) + 1] - local mapName = C_ChallengeMode.GetMapUIInfo(mapID) - if mapName then - local _, _, timeLimit = C_ChallengeMode.GetMapUIInfo(mapID) - local numBosses = math.min(4, math.max(2, math.floor((timeLimit or 1800) / 500))) - local affixKey = NormalizeAffixKey(tmpl.affixes) - local scopeKey = format("%d:%d:%s", mapID, tmpl.level, affixKey) - - local objTimes = {} - local objNames = {} - local interval = math.floor(tmpl.elapsed / (numBosses + 1)) - for b = 1, numBosses do - objTimes[b] = interval * b - objNames[b] = format("Boss %d", b) - end - local enemyT = math.floor(tmpl.elapsed * 0.92) - - if not p.bestRuns[scopeKey] then - p.bestRuns[scopeKey] = { - elapsed = tmpl.elapsed, - mapID = mapID, - mapName = mapName, - level = tmpl.level, - affixes = tmpl.affixes, - deaths = tmpl.deaths, - deathTimeLost = tmpl.deathTimeLost, - date = tmpl.date, - objectiveTimes = objTimes, - objectiveNames = objNames, - enemyForcesTime = enemyT, - } - end - - -- Add a second level entry for the first 3 dungeons - if i <= 3 then - local tmpl2 = testTemplates[((i) % #testTemplates) + 1] - local affixKey2 = NormalizeAffixKey(tmpl2.affixes) - local scopeKey2 = format("%d:%d:%s", mapID, tmpl2.level, affixKey2) - if not p.bestRuns[scopeKey2] then - local objTimes2 = {} - local objNames2 = {} - local interval2 = math.floor(tmpl2.elapsed / (numBosses + 1)) - for b = 1, numBosses do - objTimes2[b] = interval2 * b - objNames2[b] = format("Boss %d", b) - end - p.bestRuns[scopeKey2] = { - elapsed = tmpl2.elapsed, - mapID = mapID, - mapName = mapName, - level = tmpl2.level, - affixes = tmpl2.affixes, - deaths = tmpl2.deaths, - deathTimeLost = tmpl2.deathTimeLost, - date = tmpl2.date, - objectiveTimes = objTimes2, - objectiveNames = objNames2, - enemyForcesTime = math.floor(tmpl2.elapsed * 0.92), - } - end - end - end - end - end - C_Timer.After(0.5, InjectTestData) - -- END TEST DATA ────────────────────────────────────────────────── - -- Font helpers (mirrors main file, reads fontPath from same DB) local FALLBACK_FONT = "Fonts/FRIZQT__.TTF" local function SFont() diff --git a/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua b/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua index 294aa69..ffd0c4f 100644 --- a/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua +++ b/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua @@ -790,6 +790,30 @@ _G._EMT_ApplyPreset = function(presetID) return applied end +-- Reset the current profile back to defaults and apply the EllesmereUI preset. +-- Used by the module's "Reset" button in the EllesmereUI options panel. +_G._EMT_ResetProfile = function() + if not db or not db.profile then return false end + + -- Clear every key in the current profile + for key in pairs(db.profile) do + db.profile[key] = nil + end + + -- Repopulate with DB defaults + for key, value in pairs(DB_DEFAULTS.profile) do + db.profile[key] = type(value) == "table" and CopyTable(value) or value + end + + -- Apply the EllesmereUI preset on top (sets selectedPreset = "ELLESMERE") + ApplyPresetToProfile(db.profile, "ELLESMERE") + + if _G._EMT_StandaloneRefresh then + _G._EMT_StandaloneRefresh() + end + return true +end + -- Standalone frame local standaloneFrame local standaloneCreated = false