From 4a34e90037a851612519bcbe44b25dc9e77b6a29 Mon Sep 17 00:00:00 2001 From: Daniel <14241290+dnlxh@users.noreply.github.com> Date: Sat, 11 Apr 2026 01:08:44 +0200 Subject: [PATCH 01/16] Add themed character sheet with custom layout, stats panel, titles, and equipment manager --- EUI__General_Options.lua | 510 +++++++- EllesmereUI.toc | 3 +- charactersheet.lua | 2605 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 3059 insertions(+), 59 deletions(-) create mode 100644 charactersheet.lua diff --git a/EUI__General_Options.lua b/EUI__General_Options.lua index 7ecab160..24e56715 100644 --- a/EUI__General_Options.lua +++ b/EUI__General_Options.lua @@ -1237,6 +1237,7 @@ initFrame:SetScript("OnEvent", function(self) "autoRepairGuild", "hideScreenshotStatus", "autoUnwrapCollections", "trainAllButton", "ahCurrentExpansion", "quickLoot", "autoFillDelete", "skipCinematics", "skipCinematicsAuto", + "autoAcceptRoleCheck", "autoAcceptRoleCheckShift", "sortByMythicScore", "autoInsertKeystone", "quickSignup", "persistSignupNote", "hideBlizzardPartyFrame", "instanceResetAnnounce", "instanceResetAnnounceMsg", @@ -3313,7 +3314,18 @@ initFrame:SetScript("OnEvent", function(self) --------------------------------------------------------------------------- _, h = W:SectionHeader(parent, "GROUP FINDER", y); y = y - h - _, h = W:DualRow(parent, y, + local roleCheckRow + roleCheckRow, h = W:DualRow(parent, y, + { type="toggle", text="Auto Accept Role Check", + tooltip="Automatically accepts the role check popup when queuing via Premade Groups, using your already selected roles.", + getValue=function() + return EllesmereUIDB and EllesmereUIDB.autoAcceptRoleCheck or false + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.autoAcceptRoleCheck = v + EllesmereUI:RefreshPage() + end }, { type="toggle", text="Sort by Mythic+ Rating", -- Feature temporarily disabled: the previous implementation -- hooksecurefunc'd LFGListUtil_SortApplicants and mutated the @@ -3325,7 +3337,61 @@ initFrame:SetScript("OnEvent", function(self) disabled = function() return true end, disabledTooltip = "This option is temporarily disabled", getValue=function() return false end, - setValue=function() end }, + setValue=function() end } + ); y = y - h + + -- Cog on Auto Accept Role Check (left region) + do + local leftRgn = roleCheckRow._leftRegion + local function roleCheckOff() + return not (EllesmereUIDB and EllesmereUIDB.autoAcceptRoleCheck) + end + + local _, rcCogShow = EllesmereUI.BuildCogPopup({ + title = "Role Check Settings", + rows = { + { type="toggle", label="Hold Shift to Skip Auto-Accept", + get=function() + return EllesmereUIDB and EllesmereUIDB.autoAcceptRoleCheckShift or false + end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.autoAcceptRoleCheckShift = v + end }, + }, + }) + + local rcCogBtn = CreateFrame("Button", nil, leftRgn) + rcCogBtn:SetSize(26, 26) + rcCogBtn:SetPoint("RIGHT", leftRgn._lastInline or leftRgn._control, "LEFT", -9, 0) + leftRgn._lastInline = rcCogBtn + rcCogBtn:SetFrameLevel(leftRgn:GetFrameLevel() + 5) + rcCogBtn:SetAlpha(roleCheckOff() and 0.15 or 0.4) + local rcCogTex = rcCogBtn:CreateTexture(nil, "OVERLAY") + rcCogTex:SetAllPoints() + rcCogTex:SetTexture(EllesmereUI.COGS_ICON) + rcCogBtn:SetScript("OnEnter", function(self) self:SetAlpha(0.7) end) + rcCogBtn:SetScript("OnLeave", function(self) self:SetAlpha(roleCheckOff() and 0.15 or 0.4) end) + rcCogBtn:SetScript("OnClick", function(self) rcCogShow(self) end) + + local rcCogBlock = CreateFrame("Frame", nil, rcCogBtn) + rcCogBlock:SetAllPoints() + rcCogBlock:SetFrameLevel(rcCogBtn:GetFrameLevel() + 10) + rcCogBlock:EnableMouse(true) + rcCogBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(rcCogBtn, EllesmereUI.DisabledTooltip("Auto Accept Role Check")) + end) + rcCogBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + EllesmereUI.RegisterWidgetRefresh(function() + local off = roleCheckOff() + rcCogBtn:SetAlpha(off and 0.15 or 0.4) + if off then rcCogBlock:Show() else rcCogBlock:Hide() end + end) + if roleCheckOff() then rcCogBlock:Show() else rcCogBlock:Hide() end + end + + _, h = W:DualRow(parent, y, { type="toggle", text="Auto Insert Keystone", tooltip="Automatically inserts your key into the Font of Power.", getValue=function() @@ -3335,10 +3401,7 @@ initFrame:SetScript("OnEvent", function(self) setValue=function(v) if not EllesmereUIDB then EllesmereUIDB = {} end EllesmereUIDB.autoInsertKeystone = v - end } - ); y = y - h - - _, h = W:DualRow(parent, y, + end }, { type="toggle", text="Announce Instance Reset", tooltip="After a successful instance reset, automatically announces it in party or raid chat so your group knows they can re-enter.", getValue=function() @@ -3353,7 +3416,7 @@ initFrame:SetScript("OnEvent", function(self) local quickSignupRow quickSignupRow, h = W:DualRow(parent, y, { type="toggle", text="Quick Signup", - tooltip="Double-click a group listing to instantly sign up. Automatically accepts role check.", + tooltip="Double-click a group listing to instantly sign up without pressing the Sign Up button.", getValue=function() return EllesmereUIDB and EllesmereUIDB.quickSignup or false end, @@ -3375,57 +3438,6 @@ initFrame:SetScript("OnEvent", function(self) end } ); y = y - h - -- Cog on Quick Signup (left region) - do - local leftRgn = quickSignupRow._leftRegion - local function quickSignupOff() - return not (EllesmereUIDB and EllesmereUIDB.quickSignup) - end - - local _, qsCogShow = EllesmereUI.BuildCogPopup({ - title = "Quick Signup Settings", - rows = { - { type="toggle", label="Hold Shift to stop automatic Role Check", - get=function() - return EllesmereUIDB and EllesmereUIDB.quickSignupAutoRoleShift or false - end, - set=function(v) - if not EllesmereUIDB then EllesmereUIDB = {} end - EllesmereUIDB.quickSignupAutoRoleShift = v - end }, - }, - }) - - local qsCogBtn = CreateFrame("Button", nil, leftRgn) - qsCogBtn:SetSize(26, 26) - qsCogBtn:SetPoint("RIGHT", leftRgn._lastInline or leftRgn._control, "LEFT", -9, 0) - leftRgn._lastInline = qsCogBtn - qsCogBtn:SetFrameLevel(leftRgn:GetFrameLevel() + 5) - qsCogBtn:SetAlpha(quickSignupOff() and 0.15 or 0.4) - local qsCogTex = qsCogBtn:CreateTexture(nil, "OVERLAY") - qsCogTex:SetAllPoints() - qsCogTex:SetTexture(EllesmereUI.COGS_ICON) - qsCogBtn:SetScript("OnEnter", function(self) self:SetAlpha(0.7) end) - qsCogBtn:SetScript("OnLeave", function(self) self:SetAlpha(quickSignupOff() and 0.15 or 0.4) end) - qsCogBtn:SetScript("OnClick", function(self) qsCogShow(self) end) - - local qsCogBlock = CreateFrame("Frame", nil, qsCogBtn) - qsCogBlock:SetAllPoints() - qsCogBlock:SetFrameLevel(qsCogBtn:GetFrameLevel() + 10) - qsCogBlock:EnableMouse(true) - qsCogBlock:SetScript("OnEnter", function() - EllesmereUI.ShowWidgetTooltip(qsCogBtn, EllesmereUI.DisabledTooltip("Quick Signup")) - end) - qsCogBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) - if quickSignupOff() then - qsCogBtn:Disable() - qsCogBlock:Show() - else - qsCogBtn:Enable() - qsCogBlock:Hide() - end - end - _, h = W:Spacer(parent, y, 20); y = y - h --------------------------------------------------------------------------- @@ -3486,6 +3498,386 @@ initFrame:SetScript("OnEvent", function(self) end } ); y = y - h + --------------------------------------------------------------------------- + -- CHARACTER PANEL CUSTOMIZATIONS + --------------------------------------------------------------------------- + _, h = W:SectionHeader(parent, "CHARACTER PANEL CUSTOMIZATIONS", y); y = y - h + + _, h = W:DualRow(parent, y, + { type="toggle", text="Themed Character Sheet", + tooltip="Applies EllesmereUI theme styling to the character sheet window.", + getValue=function() + return EllesmereUIDB and EllesmereUIDB.themedCharacterSheet or false + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.themedCharacterSheet = v + if EllesmereUI.ShowConfirmPopup then + EllesmereUI:ShowConfirmPopup({ + title = "Reload Required", + message = "Character Sheet theme setting requires a UI reload to fully apply.", + confirmText = "Reload Now", + cancelText = "Later", + onConfirm = function() ReloadUI() end, + }) + end + EllesmereUI:RefreshPage() + end }, + { type="slider", text="Character Sheet Scale", + min=0.5, max=1.5, step=0.05, + tooltip="Adjusts the scale of the themed character sheet window.", + getValue=function() + return EllesmereUIDB and EllesmereUIDB.themedCharacterSheetScale or 1 + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.themedCharacterSheetScale = v + if CharacterFrame then + CharacterFrame:SetScale(v) + end + end } + ); y = y - h + + -- Disabled overlay for Scale slider when themed is off + do + local function themedOff() + return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) + end + + local scaleBlock = CreateFrame("Frame", nil, parent) + scaleBlock:SetSize(400, 30) + scaleBlock:SetPoint("TOPLEFT", parent, "TOPLEFT", 420, -y + 30) + scaleBlock:SetFrameLevel(parent:GetFrameLevel() + 20) + scaleBlock:EnableMouse(true) + local scaleBg = EllesmereUI.SolidTex(scaleBlock, "BACKGROUND", 0, 0, 0, 0) + scaleBg:SetAllPoints() + scaleBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(scaleBlock, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + end) + scaleBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + EllesmereUI.RegisterWidgetRefresh(function() + if themedOff() then + scaleBlock:Show() + else + scaleBlock:Hide() + end + end) + if themedOff() then scaleBlock:Show() else scaleBlock:Hide() end + end + + local colorItemLevelRow + colorItemLevelRow, h = W:DualRow(parent, y, + { type="toggle", text="Color Item Level by Rarity", + tooltip="Colors the item level text based on the item's rarity (Common, Uncommon, Rare, Epic, etc.).", + getValue=function() + return EllesmereUIDB and EllesmereUIDB.charSheetColorItemLevel or false + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetColorItemLevel = v + if EllesmereUI._applyCharSheetItemColors then + EllesmereUI._applyCharSheetItemColors() + end + end }, + { type="label", text="" } + ); y = y - h + + -- Disabled overlay for Color Item Level by Rarity when themed is off + do + local function themedOff() + return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) + end + + local colorItemBlock = CreateFrame("Frame", nil, colorItemLevelRow) + colorItemBlock:SetAllPoints(colorItemLevelRow) + colorItemBlock:SetFrameLevel(colorItemLevelRow:GetFrameLevel() + 10) + colorItemBlock:EnableMouse(true) + local colorItemBg = EllesmereUI.SolidTex(colorItemBlock, "BACKGROUND", 0, 0, 0, 0) + colorItemBg:SetAllPoints() + colorItemBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(colorItemBlock, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + end) + colorItemBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + EllesmereUI.RegisterWidgetRefresh(function() + if themedOff() then + colorItemBlock:Show() + colorItemLevelRow:SetAlpha(0.3) + else + colorItemBlock:Hide() + colorItemLevelRow:SetAlpha(1) + end + end) + if themedOff() then colorItemBlock:Show() colorItemLevelRow:SetAlpha(0.3) else colorItemBlock:Hide() colorItemLevelRow:SetAlpha(1) end + end + + local itemLevelRow + itemLevelRow, h = W:DualRow(parent, y, + { type="slider", text="Item Level Font Size", + min=8, max=16, step=1, + tooltip="Adjusts the font size for item level text on the character sheet.", + getValue=function() + return EllesmereUIDB and EllesmereUIDB.charSheetItemLevelSize or 11 + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetItemLevelSize = v + if EllesmereUI._applyCharSheetTextSizes then + EllesmereUI._applyCharSheetTextSizes() + end + end }, + { type="slider", text="Upgrade Track Font Size", + min=8, max=16, step=1, + tooltip="Adjusts the font size for upgrade track text on the character sheet.", + getValue=function() + return EllesmereUIDB and EllesmereUIDB.charSheetUpgradeTrackSize or 11 + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetUpgradeTrackSize = v + if EllesmereUI._applyCharSheetTextSizes then + EllesmereUI._applyCharSheetTextSizes() + end + end } + ); y = y - h + + -- Disabled overlay for font size row when themed is off + do + local function themedOff() + return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) + end + + local fontBlock = CreateFrame("Frame", nil, itemLevelRow) + fontBlock:SetAllPoints(itemLevelRow) + fontBlock:SetFrameLevel(itemLevelRow:GetFrameLevel() + 10) + fontBlock:EnableMouse(true) + local fontBg = EllesmereUI.SolidTex(fontBlock, "BACKGROUND", 0, 0, 0, 0) + fontBg:SetAllPoints() + fontBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(fontBlock, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + end) + fontBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + EllesmereUI.RegisterWidgetRefresh(function() + if themedOff() then + fontBlock:Show() + itemLevelRow:SetAlpha(0.3) + else + fontBlock:Hide() + itemLevelRow:SetAlpha(1) + end + end) + if themedOff() then fontBlock:Show() itemLevelRow:SetAlpha(0.3) else fontBlock:Hide() itemLevelRow:SetAlpha(1) end + end + + local enchantRow + enchantRow, h = W:DualRow(parent, y, + { type="slider", text="Enchant Font Size", + min=8, max=12, step=1, + tooltip="Adjusts the font size for enchant text on the character sheet.", + getValue=function() + return EllesmereUIDB and EllesmereUIDB.charSheetEnchantSize or 9 + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetEnchantSize = v + if EllesmereUI._applyCharSheetTextSizes then + EllesmereUI._applyCharSheetTextSizes() + end + end }, + { type="label", text="" } + ); y = y - h + + -- Disabled overlay for enchant row when themed is off + do + local function themedOff() + return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) + end + + local enchantBlock = CreateFrame("Frame", nil, enchantRow) + enchantBlock:SetAllPoints(enchantRow) + enchantBlock:SetFrameLevel(enchantRow:GetFrameLevel() + 10) + enchantBlock:EnableMouse(true) + local enchantBg = EllesmereUI.SolidTex(enchantBlock, "BACKGROUND", 0, 0, 0, 0) + enchantBg:SetAllPoints() + enchantBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(enchantBlock, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + end) + enchantBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + EllesmereUI.RegisterWidgetRefresh(function() + if themedOff() then + enchantBlock:Show() + enchantRow:SetAlpha(0.3) + else + enchantBlock:Hide() + enchantRow:SetAlpha(1) + end + end) + if themedOff() then enchantBlock:Show() enchantRow:SetAlpha(0.3) else enchantBlock:Hide() enchantRow:SetAlpha(1) end + end + + -- Stat Category Toggles + _, h = W:Spacer(parent, y, 10); y = y - h + + local categoryRow1, h1 = W:DualRow(parent, y, + { type="toggle", text="Show Attributes", + tooltip="Toggle visibility of the Attributes stat category.", + getValue=function() + return EllesmereUIDB and EllesmereUIDB.showStatCategory_Attributes ~= false + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.showStatCategory_Attributes = v + if EllesmereUI._updateStatCategoryVisibility then + EllesmereUI._updateStatCategoryVisibility() + end + end }, + { type="toggle", text="Show Secondary Stats", + tooltip="Toggle visibility of the Secondary Stats category.", + getValue=function() + return EllesmereUIDB and EllesmereUIDB.showStatCategory_SecondaryStats ~= false + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.showStatCategory_SecondaryStats = v + if EllesmereUI._updateStatCategoryVisibility then + EllesmereUI._updateStatCategoryVisibility() + end + end } + ); y = y - h1 + + -- Disabled overlay for categoryRow1 when themed is off + do + local function themedOff() + return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) + end + + local categoryBlock1 = CreateFrame("Frame", nil, categoryRow1) + categoryBlock1:SetAllPoints(categoryRow1) + categoryBlock1:SetFrameLevel(categoryRow1:GetFrameLevel() + 10) + categoryBlock1:EnableMouse(true) + local categoryBg1 = EllesmereUI.SolidTex(categoryBlock1, "BACKGROUND", 0, 0, 0, 0) + categoryBg1:SetAllPoints() + categoryBlock1:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(categoryBlock1, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + end) + categoryBlock1:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + EllesmereUI.RegisterWidgetRefresh(function() + if themedOff() then + categoryBlock1:Show() + categoryRow1:SetAlpha(0.3) + else + categoryBlock1:Hide() + categoryRow1:SetAlpha(1) + end + end) + if themedOff() then categoryBlock1:Show() categoryRow1:SetAlpha(0.3) else categoryBlock1:Hide() categoryRow1:SetAlpha(1) end + end + + local categoryRow2, h2 = W:DualRow(parent, y, + { type="toggle", text="Show Attack", + tooltip="Toggle visibility of the Attack stat category.", + getValue=function() + return EllesmereUIDB and EllesmereUIDB.showStatCategory_Attack ~= false + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.showStatCategory_Attack = v + if EllesmereUI._updateStatCategoryVisibility then + EllesmereUI._updateStatCategoryVisibility() + end + end }, + { type="toggle", text="Show Defense", + tooltip="Toggle visibility of the Defense stat category.", + getValue=function() + return EllesmereUIDB and EllesmereUIDB.showStatCategory_Defense ~= false + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.showStatCategory_Defense = v + if EllesmereUI._updateStatCategoryVisibility then + EllesmereUI._updateStatCategoryVisibility() + end + end } + ); y = y - h2 + + -- Disabled overlay for categoryRow2 when themed is off + do + local function themedOff() + return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) + end + + local categoryBlock2 = CreateFrame("Frame", nil, categoryRow2) + categoryBlock2:SetAllPoints(categoryRow2) + categoryBlock2:SetFrameLevel(categoryRow2:GetFrameLevel() + 10) + categoryBlock2:EnableMouse(true) + local categoryBg2 = EllesmereUI.SolidTex(categoryBlock2, "BACKGROUND", 0, 0, 0, 0) + categoryBg2:SetAllPoints() + categoryBlock2:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(categoryBlock2, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + end) + categoryBlock2:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + EllesmereUI.RegisterWidgetRefresh(function() + if themedOff() then + categoryBlock2:Show() + categoryRow2:SetAlpha(0.3) + else + categoryBlock2:Hide() + categoryRow2:SetAlpha(1) + end + end) + if themedOff() then categoryBlock2:Show() categoryRow2:SetAlpha(0.3) else categoryBlock2:Hide() categoryRow2:SetAlpha(1) end + end + + local categoryRow3, h3 = W:DualRow(parent, y, + { type="toggle", text="Show Crests", + tooltip="Toggle visibility of the Crests stat category.", + getValue=function() + return EllesmereUIDB and EllesmereUIDB.showStatCategory_Crests ~= false + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.showStatCategory_Crests = v + if EllesmereUI._updateStatCategoryVisibility then + EllesmereUI._updateStatCategoryVisibility() + end + end }, + { type="label", text="" } + ); y = y - h3 + + -- Disabled overlay for categoryRow3 when themed is off + do + local function themedOff() + return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) + end + + local categoryBlock3 = CreateFrame("Frame", nil, categoryRow3) + categoryBlock3:SetAllPoints(categoryRow3) + categoryBlock3:SetFrameLevel(categoryRow3:GetFrameLevel() + 10) + categoryBlock3:EnableMouse(true) + local categoryBg3 = EllesmereUI.SolidTex(categoryBlock3, "BACKGROUND", 0, 0, 0, 0) + categoryBg3:SetAllPoints() + categoryBlock3:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(categoryBlock3, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + end) + categoryBlock3:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + EllesmereUI.RegisterWidgetRefresh(function() + if themedOff() then + categoryBlock3:Show() + categoryRow3:SetAlpha(0.3) + else + categoryBlock3:Hide() + categoryRow3:SetAlpha(1) + end + end) + if themedOff() then categoryBlock3:Show() categoryRow3:SetAlpha(0.3) else categoryBlock3:Hide() categoryRow3:SetAlpha(1) end + end + _, h = W:Spacer(parent, y, 20); y = y - h return math.abs(y) end @@ -4957,6 +5349,8 @@ initFrame:SetScript("OnEvent", function(self) EllesmereUIDB.unlockHeightMatch = nil -- QoL Features defaults EllesmereUIDB.hideBlizzardPartyFrame = false + EllesmereUIDB.autoAcceptRoleCheck = false + EllesmereUIDB.autoAcceptRoleCheckShift = false EllesmereUIDB.quickLoot = false EllesmereUIDB.quickLootShiftSkip = false EllesmereUIDB.skipCinematics = false diff --git a/EllesmereUI.toc b/EllesmereUI.toc index ee096aa3..b1ecf7fd 100644 --- a/EllesmereUI.toc +++ b/EllesmereUI.toc @@ -2,7 +2,7 @@ ## Title: |cff0cd29fEllesmereUI|r ## Notes: Shared framework for the EllesmereUI addon suite ## Author: Ellesmere -## Version: 6.4.7 +## Version: 6.4.3 ## SavedVariables: EllesmereUIDB ## IconTexture: Interface\AddOns\EllesmereUI\media\eg-logo.tga ## X-Curse-Project-ID: 1477613 @@ -35,3 +35,4 @@ EUI__General_Options.lua EUI_UnlockMode.lua EUI_PartyMode_Options.lua EllesmereUI_Glows.lua +charactersheet.lua diff --git a/charactersheet.lua b/charactersheet.lua new file mode 100644 index 00000000..8a30e9db --- /dev/null +++ b/charactersheet.lua @@ -0,0 +1,2605 @@ +-------------------------------------------------------------------------------- +-- Themed Character Sheet +-------------------------------------------------------------------------------- +local ADDON_NAME = ... +local skinned = false +local activeEquipmentSetID = nil -- Track currently equipped set + +-- Apply EUI theme to character sheet frame +local function SkinCharacterSheet() + if skinned then return end + skinned = true + + local frame = CharacterFrame + if not frame then return end + + -- Hide Blizzard decorations + if CharacterFrame.NineSlice then CharacterFrame.NineSlice:Hide() end + -- NOTE: Don't hide frame.Bg - we need it as anchor for slots! + if frame.Background then frame.Background:Hide() end + if frame.TitleBg then frame.TitleBg:Hide() end + if frame.TopTileStreaks then frame.TopTileStreaks:Hide() end + if frame.Portrait then frame.Portrait:Hide() end + if CharacterFramePortrait then CharacterFramePortrait:Hide() end + -- NOTE: Don't hide CharacterFrameBg - we use it as anchor point for item slots! + if CharacterModelFrameBackgroundOverlay then CharacterModelFrameBackgroundOverlay:Hide() end + if CharacterModelFrameBackgroundTopLeft then CharacterModelFrameBackgroundTopLeft:Hide() end + if CharacterModelFrameBackgroundBotLeft then CharacterModelFrameBackgroundBotLeft:Hide() end + if CharacterModelFrameBackgroundTopRight then CharacterModelFrameBackgroundTopRight:Hide() end + if CharacterModelFrameBackgroundBotRight then CharacterModelFrameBackgroundBotRight:Hide() end + -- NOTE: Don't hide CharacterFrameBg - we need it as anchor point for item slots! + if CharacterFrameInsetRight then + if CharacterFrameInsetRight.NineSlice then CharacterFrameInsetRight.NineSlice:Hide() end + CharacterFrameInsetRight:ClearAllPoints() + CharacterFrameInsetRight:SetPoint("TOPLEFT", frame, "TOPLEFT", 500, -500) + end + if CharacterFrameInsetBG then CharacterFrameInsetBG:Hide() end + if CharacterFrameInset and CharacterFrameInset.NineSlice then + for _, edge in ipairs({"TopEdge", "BottomEdge", "LeftEdge", "RightEdge", "TopLeftCorner", "TopRightCorner", "BottomLeftCorner", "BottomRightCorner"}) do + if CharacterFrameInset.NineSlice[edge] then + CharacterFrameInset.NineSlice[edge]:Hide() + end + end + end + -- Add colored backgrounds to CharacterFrameInset (EUI FriendsList style) + local FRAME_BG_R, FRAME_BG_G, FRAME_BG_B = 0.03, 0.045, 0.05 + if CharacterFrameInset then + if CharacterFrameInset.AbsBg then + CharacterFrameInset.AbsBg:SetColorTexture(FRAME_BG_R, FRAME_BG_G, FRAME_BG_B, 1) + end + if CharacterFrameInset.Bg then + CharacterFrameInset.Bg:SetColorTexture(0.02, 0.02, 0.025, 1) -- Darker for ScrollBox + end + end + + if CharacterModelScene then + CharacterModelScene:Show() + CharacterModelScene:ClearAllPoints() + CharacterModelScene:SetPoint("TOPLEFT", frame, "TOPLEFT", 110, -100) + CharacterModelScene:SetFrameLevel(1) -- Keep model behind text + + -- Hide control frame (zoom, rotation buttons) + if CharacterModelScene.ControlFrame then + CharacterModelScene.ControlFrame:Hide() + end + end + + -- Center the level text under character name + if CharacterLevelText and CharacterFrameTitleText then + CharacterLevelText:ClearAllPoints() + CharacterLevelText:SetPoint("TOP", CharacterFrameTitleText, "BOTTOM", 0, -5) + CharacterLevelText:SetJustifyH("CENTER") + end + + -- Hide model control help text + if CharacterModelFrameHelpText then CharacterModelFrameHelpText:Hide() end + + if CharacterFrameInsetBG then CharacterFrameInsetBG:Hide() end + if CharacterFrameInset and CharacterFrameInset.NineSlice then + for _, edge in ipairs({"TopEdge", "BottomEdge", "LeftEdge", "RightEdge", "TopLeftCorner", "TopRightCorner", "BottomLeftCorner", "BottomRightCorner"}) do + if CharacterFrameInset.NineSlice[edge] then + CharacterFrameInset.NineSlice[edge]:Hide() + end + end + end + + + -- Hide PaperDoll borders (Blizzard's outer window frame) + if frame.PaperDollFrame then + if frame.PaperDollFrame.InnerBorder then + for _, name in ipairs({"Top", "Bottom", "Left", "Right", "TopLeft", "TopRight", "BottomLeft", "BottomRight"}) do + if frame.PaperDollFrame.InnerBorder[name] then + frame.PaperDollFrame.InnerBorder[name]:Hide() + end + end + end + end + + -- Hide all PaperDollInnerBorder textures + for _, name in ipairs({"TopLeft", "TopRight", "BottomLeft", "BottomRight", "Top", "Bottom", "Left", "Right", "Bottom2"}) do + if _G["PaperDollInnerBorder" .. name] then + _G["PaperDollInnerBorder" .. name]:Hide() + end + end + + if PaperDollItemsFrame then PaperDollItemsFrame:Hide() end + if CharacterStatPane then + -- Hide ClassBackground + if CharacterStatPane.ClassBackground then + CharacterStatPane.ClassBackground:Hide() + end + -- Move CharacterStatPane off-screen + CharacterStatPane:ClearAllPoints() + CharacterStatPane:SetPoint("TOPLEFT", frame, "BOTTOMLEFT", 0, -500) + end + + -- Hide secondary hand slot extra element for testing + if _G["CharacterSecondaryHandSlot.26129b81ae0"] then + _G["CharacterSecondaryHandSlot.26129b81ae0"]:Hide() + end + + + -- Hide all SlotFrame wrapper containers + _G.CharacterBackSlotFrame:Hide() + _G.CharacterChestSlotFrame:Hide() + _G.CharacterFeetSlotFrame:Hide() + _G.CharacterFinger0SlotFrame:Hide() + _G.CharacterFinger1SlotFrame:Hide() + _G.CharacterHandsSlotFrame:Hide() + _G.CharacterHeadSlotFrame:Hide() + _G.CharacterLegsSlotFrame:Hide() + _G.CharacterMainHandSlotFrame:Hide() + _G.CharacterNeckSlotFrame:Hide() + _G.CharacterSecondaryHandSlotFrame:Hide() + _G.CharacterShirtSlotFrame:Hide() + _G.CharacterShoulderSlotFrame:Hide() + _G.CharacterTabardSlotFrame:Hide() + _G.CharacterTrinket0SlotFrame:Hide() + _G.CharacterTrinket1SlotFrame:Hide() + _G.CharacterWaistSlotFrame:Hide() + _G.CharacterWristSlotFrame:Hide() + + -- Custom flexible grid layout (NO REPARENTING!) + -- Slots stay in original parents, positioned via grid system + if CharacterFrameBg then CharacterFrameBg:Show() end + + local slotNames = { + "CharacterHeadSlot", "CharacterNeckSlot", "CharacterShoulderSlot", "CharacterBackSlot", + "CharacterChestSlot", "CharacterShirtSlot", "CharacterTabardSlot", "CharacterWristSlot", + "CharacterHandsSlot", "CharacterWaistSlot", "CharacterLegsSlot", "CharacterFeetSlot", + "CharacterTrinket0Slot", "CharacterTrinket1Slot", "CharacterFinger0Slot", "CharacterFinger1Slot", + "CharacterMainHandSlot", "CharacterSecondaryHandSlot" + } + + -- Show all slots AND their parents + for _, slotName in ipairs(slotNames) do + local slot = _G[slotName] + if slot then + slot:Show() + local parent = slot:GetParent() + if parent then + parent:Show() + end + end + end + + -- Grid-based layout system (2 columns) + local gridCols = 2 + local cellWidth = 360 + local cellHeight = 45 + local gridStartX = 30 + local gridStartY = -60 + + -- Equipment slot grid positions (2 columns: left & right) + local slotGridMap = { + -- Left column + CharacterHeadSlot = {col = 0, row = 0}, + CharacterNeckSlot = {col = 0, row = 1}, + CharacterShoulderSlot = {col = 0, row = 2}, + CharacterBackSlot = {col = 0, row = 3}, + CharacterChestSlot = {col = 0, row = 4}, + CharacterShirtSlot = {col = 0, row = 5}, + CharacterTabardSlot = {col = 0, row = 6}, + CharacterWristSlot = {col = 0, row = 7}, + + -- Right column + CharacterHandsSlot = {col = 1, row = 0}, + CharacterWaistSlot = {col = 1, row = 1}, + CharacterLegsSlot = {col = 1, row = 2}, + CharacterFeetSlot = {col = 1, row = 3}, + CharacterFinger0Slot = {col = 1, row = 4}, + CharacterFinger1Slot = {col = 1, row = 5}, + CharacterTrinket0Slot = {col = 1, row = 6}, + CharacterTrinket1Slot = {col = 1, row = 7}, + } + + -- Position main grid slots using anchor calculations + for slotName, gridPos in pairs(slotGridMap) do + local slot = _G[slotName] + if slot then + slot:ClearAllPoints() + local xOffset = gridStartX + (gridPos.col * cellWidth) + local yOffset = gridStartY - (gridPos.row * cellHeight) + slot:SetPoint("TOPLEFT", CharacterFrame, "TOPLEFT", xOffset, yOffset) + end + end + + -- Weapons positioned in bottom-right area (separate from grid) + _G.CharacterMainHandSlot:ClearAllPoints() + _G.CharacterMainHandSlot:SetPoint("BOTTOMLEFT", CharacterFrame, "BOTTOMLEFT", 175, 25) + _G.CharacterSecondaryHandSlot:ClearAllPoints() + _G.CharacterSecondaryHandSlot:SetPoint("TOPLEFT", _G.CharacterMainHandSlot, "TOPRIGHT", 12, 0) + + + + -- Hook slot enter to show flyout when equipment mode is active + if not frame._slotHookDone then + local origOnEnter = PaperDollItemSlotButton_OnEnter + PaperDollItemSlotButton_OnEnter = function(button) + origOnEnter(button) + -- If flyout mode is active, also show flyout + if frame._flyoutModeActive and button:GetID() then + if EquipmentFlyout_Show then + pcall(EquipmentFlyout_Show, button) + end + end + end + frame._slotHookDone = true + end + + -- Hide slot textures and borders (Chonky style) + select(16, _G.CharacterMainHandSlot:GetRegions()):SetTexCoord(.8,.8,.8,.8,.8,.8,.8,.8) + select(17, _G.CharacterMainHandSlot:GetRegions()):SetTexCoord(.8,.8,.8,.8,.8,.8,.8,.8) + select(16, _G.CharacterSecondaryHandSlot:GetRegions()):SetTexCoord(.8,.8,.8,.8,.8,.8,.8,.8) + select(17, _G.CharacterSecondaryHandSlot:GetRegions()):SetTexCoord(.8,.8,.8,.8,.8,.8,.8,.8) + + -- Hide icon borders and adjust texcoords + local slotsToHide = { + "CharacterBackSlot", "CharacterChestSlot", "CharacterFeetSlot", + "CharacterFinger0Slot", "CharacterFinger1Slot", "CharacterHandsSlot", + "CharacterHeadSlot", "CharacterLegsSlot", "CharacterMainHandSlot", + "CharacterNeckSlot", "CharacterSecondaryHandSlot", "CharacterShirtSlot", + "CharacterShoulderSlot", "CharacterTabardSlot", "CharacterTrinket0Slot", + "CharacterTrinket1Slot", "CharacterWaistSlot", "CharacterWristSlot" + } + + for _, slotName in ipairs(slotsToHide) do + local slot = _G[slotName] + if slot then + slot:Show() + if slot.IconBorder then + slot.IconBorder:SetTexCoord(.8,.8,.8,.8,.8,.8,.8,.8) + end + local iconTexture = _G[slotName .. "IconTexture"] + if iconTexture then + iconTexture:SetTexCoord(.07,.07,.07,.93,.93,.07,.93,.93) + end + local normalTexture = _G[slotName .. "NormalTexture"] + if normalTexture then + normalTexture:Hide() + end + end + end + + -- Hide special regions on weapon slots + select(16, _G.CharacterMainHandSlot:GetRegions()):SetTexCoord(0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8) + select(17, _G.CharacterMainHandSlot:GetRegions()):SetTexCoord(0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8) + select(16, _G.CharacterSecondaryHandSlot:GetRegions()):SetTexCoord(0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8) + select(17, _G.CharacterSecondaryHandSlot:GetRegions()):SetTexCoord(0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8) + + -- Show all slots with blending + local slotNames = { + "CharacterHeadSlot", "CharacterNeckSlot", "CharacterShoulderSlot", "CharacterBackSlot", + "CharacterChestSlot", "CharacterShirtSlot", "CharacterTabardSlot", "CharacterWristSlot", + "CharacterHandsSlot", "CharacterWaistSlot", "CharacterLegsSlot", "CharacterFeetSlot", + "CharacterTrinket0Slot", "CharacterTrinket1Slot", "CharacterFinger0Slot", "CharacterFinger1Slot", + "CharacterMainHandSlot", "CharacterSecondaryHandSlot" + } + for _, slotName in ipairs(slotNames) do + local slot = _G[slotName] + if slot then + slot:Show() + if slot._slotBg then + slot._slotBg:SetBlendMode("BLEND") + end + end + end + + -- Apply scale if saved + local scale = EllesmereUIDB and EllesmereUIDB.themedCharacterSheetScale or 1 + frame:SetScale(scale) + + -- Raise frame strata for visibility + frame:SetFrameStrata("HIGH") + + -- Resize frame to be wider + -- Resize frame and hook to keep the size + -- Set fixed frame size directly (not expanding from original) + local newWidth = 698 -- Fixed width + local newHeight = 480 -- Fixed height + frame:SetWidth(newWidth) + frame:SetHeight(newHeight) + + -- Also expand CharacterFrameInset to match + if CharacterFrameInset then + CharacterFrameInset:SetWidth(newWidth - 20) + CharacterFrameInset:SetHeight(newHeight - 90) + CharacterFrameInset:SetClipsChildren(false) -- Prevent clipping + end + + -- Hook SetWidth to prevent Blizzard from changing it back + hooksecurefunc(frame, "SetWidth", function(self, w) + if w ~= newWidth then + self:SetWidth(newWidth) + end + end) + + -- Hook SetHeight to prevent Blizzard from changing it back + hooksecurefunc(frame, "SetHeight", function(self, h) + if h ~= newHeight then + self:SetHeight(newHeight) + end + end) + + -- Add SetPoint hook too - Blizzard might resize via SetPoint + local hookLock = false + hooksecurefunc(frame, "SetPoint", function(self, ...) + if not hookLock and frame._sizeCheckDone then + hookLock = true + self:SetSize(newWidth, newHeight) + if self._ebsBg then + self._ebsBg:SetSize(newWidth, newHeight) + end + hookLock = false + end + end) + + -- Aggressive size enforcement with immediate re-setup + if not frame._sizeCheckDone then + local function EnforceSize() + if frame:IsShown() then + frame:SetSize(newWidth, newHeight) + -- Regenerate background immediately + if frame._ebsBg then + frame._ebsBg:SetSize(newWidth, newHeight) + frame._ebsBg:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0) + end + if CharacterFrameInset then + CharacterFrameInset:SetClipsChildren(false) + CharacterFrameInset:SetSize(newWidth - 20, newHeight - 90) + end + end + end + + -- Continuous check with OnUpdate (no event registration needed) + local updateFrame = CreateFrame("Frame") + updateFrame:SetScript("OnUpdate", EnforceSize) + + frame._sizeCheckDone = true + end + + -- Strip textures from frame regions + for i = 1, select("#", frame:GetRegions()) do + local region = select(i, frame:GetRegions()) + if region and region:IsObjectType("Texture") then + region:SetAlpha(0) + end + end + + -- Add custom background with EUI colors (same as FriendsFrame) + local FRAME_BG_R, FRAME_BG_G, FRAME_BG_B = 0.03, 0.045, 0.05 + + -- Main frame background at BACKGROUND layer -8 (fixed size, not scaled) + frame._ebsBg = frame:CreateTexture(nil, "BACKGROUND", nil, -8) + frame._ebsBg:SetColorTexture(FRAME_BG_R, FRAME_BG_G, FRAME_BG_B) + frame._ebsBg:SetSize(newWidth, newHeight) + frame._ebsBg:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0) + frame._ebsBg:SetAlpha(1) + + -- Create dark gray border using PP.CreateBorder + if EllesmereUI and EllesmereUI.PanelPP then + EllesmereUI.PanelPP.CreateBorder(frame, 0.2, 0.2, 0.2, 1, 1, "OVERLAY", 7) + end + + -- Skin close button + local closeBtn = frame.CloseButton or _G.CharacterFrameCloseButton + if closeBtn then + -- Strip button textures + if closeBtn.SetNormalTexture then closeBtn:SetNormalTexture("") end + if closeBtn.SetPushedTexture then closeBtn:SetPushedTexture("") end + if closeBtn.SetHighlightTexture then closeBtn:SetHighlightTexture("") end + if closeBtn.SetDisabledTexture then closeBtn:SetDisabledTexture("") end + + -- Strip texture regions + for i = 1, select("#", closeBtn:GetRegions()) do + local region = select(i, closeBtn:GetRegions()) + if region and region:IsObjectType("Texture") then + region:SetAlpha(0) + end + end + + -- Get font path for close button + local fontPath = EllesmereUI.GetFontPath and EllesmereUI.GetFontPath() or STANDARD_TEXT_FONT + + -- Create X text + closeBtn._ebsX = closeBtn:CreateFontString(nil, "OVERLAY") + closeBtn._ebsX:SetFont(fontPath, 14, "") + closeBtn._ebsX:SetText("x") + closeBtn._ebsX:SetTextColor(1, 1, 1, 0.5) + closeBtn._ebsX:SetPoint("CENTER", -2, -3) + + -- Hover effect + closeBtn:HookScript("OnEnter", function() + if closeBtn._ebsX then closeBtn._ebsX:SetTextColor(1, 1, 1, 0.9) end + end) + closeBtn:HookScript("OnLeave", function() + if closeBtn._ebsX then closeBtn._ebsX:SetTextColor(1, 1, 1, 0.5) end + end) + end + + -- Restyle character frame tabs (matching FriendsFrame pattern) + local fontPath = EllesmereUI.GetFontPath and EllesmereUI.GetFontPath() or STANDARD_TEXT_FONT + local EG = EllesmereUI.ELLESMERE_GREEN or { r = 0.51, g = 0.784, b = 1 } + + for i = 1, 3 do + local tab = _G["CharacterFrameTab" .. i] + if tab then + -- Strip Blizzard's tab textures + for j = 1, select("#", tab:GetRegions()) do + local region = select(j, tab:GetRegions()) + if region and region:IsObjectType("Texture") then + region:SetTexture("") + if region.SetAtlas then region:SetAtlas("") end + end + end + if tab.Left then tab.Left:SetTexture("") end + if tab.Middle then tab.Middle:SetTexture("") end + if tab.Right then tab.Right:SetTexture("") end + if tab.LeftDisabled then tab.LeftDisabled:SetTexture("") end + if tab.MiddleDisabled then tab.MiddleDisabled:SetTexture("") end + if tab.RightDisabled then tab.RightDisabled:SetTexture("") end + local hl = tab:GetHighlightTexture() + if hl then hl:SetTexture("") end + + -- Dark background + if not tab._ebsBg then + tab._ebsBg = tab:CreateTexture(nil, "BACKGROUND") + tab._ebsBg:SetAllPoints() + tab._ebsBg:SetColorTexture(FRAME_BG_R, FRAME_BG_G, FRAME_BG_B, 1) + end + + -- Active highlight + if not tab._activeHL then + local activeHL = tab:CreateTexture(nil, "ARTWORK", nil, -6) + activeHL:SetAllPoints() + activeHL:SetColorTexture(1, 1, 1, 0.05) + activeHL:SetBlendMode("ADD") + activeHL:Hide() + tab._activeHL = activeHL + end + + -- Hide Blizzard's label and use our own + local blizLabel = tab:GetFontString() + local labelText = blizLabel and blizLabel:GetText() or ("Tab " .. i) + if blizLabel then blizLabel:SetTextColor(0, 0, 0, 0) end + tab:SetPushedTextOffset(0, 0) + + if not tab._label then + local label = tab:CreateFontString(nil, "OVERLAY") + label:SetFont(fontPath, 9, "") + label:SetPoint("CENTER", tab, "CENTER", 0, 0) + label:SetJustifyH("CENTER") + label:SetText(labelText) + tab._label = label + -- Sync our label when Blizzard updates the text + hooksecurefunc(tab, "SetText", function(_, newText) + if newText and label then label:SetText(newText) end + end) + end + + -- Accent underline (pixel-perfect) + if not tab._underline then + local underline = tab:CreateTexture(nil, "OVERLAY", nil, 6) + if EllesmereUI and EllesmereUI.PanelPP and EllesmereUI.PanelPP.DisablePixelSnap then + EllesmereUI.PanelPP.DisablePixelSnap(underline) + underline:SetHeight(EllesmereUI.PanelPP.mult or 1) + else + underline:SetHeight(1) + end + underline:SetPoint("BOTTOMLEFT", tab, "BOTTOMLEFT", 0, 0) + underline:SetPoint("BOTTOMRIGHT", tab, "BOTTOMRIGHT", 0, 0) + underline:SetColorTexture(EG.r or 0.51, EG.g or 0.784, EG.b or 1, 1) + if EllesmereUI and EllesmereUI.RegAccent then + EllesmereUI.RegAccent({ type = "solid", obj = underline, a = 1 }) + end + underline:Hide() + tab._underline = underline + end + end + end + + -- Hook to update tab visuals when selection changes + local function UpdateTabVisuals() + for i = 1, 3 do + local tab = _G["CharacterFrameTab" .. i] + if tab then + -- PanelTemplates_GetSelectedTab doesn't work reliably, use frame's attribute + local isActive = (frame.selectedTab or 1) == i + if tab._label then + tab._label:SetTextColor(1, 1, 1, isActive and 1 or 0.5) + end + if tab._underline then + tab._underline:SetShown(isActive) + end + if tab._activeHL then + tab._activeHL:SetShown(isActive) + end + end + end + end + + -- Hook Blizzard's tab selection to update our visuals and show/hide slots + hooksecurefunc("PanelTemplates_SetTab", function(panel) + if panel == frame then + UpdateTabVisuals() + + -- Show slots only on tab 1 (Character tab) + local isCharacterTab = (frame.selectedTab or 1) == 1 + if frame._themedSlots then + for _, slotName in ipairs(frame._themedSlots) do + local slot = _G[slotName] + if slot then + if isCharacterTab then + slot:Show() + if slot._itemLevelLabel then slot._itemLevelLabel:Show() end + if slot._enchantLabel then slot._enchantLabel:Show() end + if slot._upgradeTrackLabel then slot._upgradeTrackLabel:Show() end + else + slot:Hide() + if slot._itemLevelLabel then slot._itemLevelLabel:Hide() end + if slot._enchantLabel then slot._enchantLabel:Hide() end + if slot._upgradeTrackLabel then slot._upgradeTrackLabel:Hide() end + end + end + end + end + + -- Show/hide custom buttons based on tab + for _, btnName in ipairs({"EUI_CharSheet_Stats", "EUI_CharSheet_Titles", "EUI_CharSheet_Equipment"}) do + local btn = _G[btnName] + if btn then + if isCharacterTab then + btn:Show() + else + btn:Hide() + end + end + end + + -- Show/hide stats panel and titles panel based on tab + if frame._statsPanel then + if isCharacterTab then + frame._statsPanel:Show() + else + frame._statsPanel:Hide() + end + end + + if frame._titlesPanel then + if not isCharacterTab then + frame._titlesPanel:Hide() + end + end + + if frame._equipPanel then + if not isCharacterTab then + frame._equipPanel:Hide() + end + end + + if frame._socketContainer then + if not isCharacterTab then + frame._socketContainer:Hide() + else + frame._socketContainer:Show() + end + end + end + end) + UpdateTabVisuals() + + -- Create custom stats panel with scroll + local statsPanel = CreateFrame("Frame", "EUI_CharSheet_StatsPanel", frame) + statsPanel:SetSize(220 + 360, 340) -- Also expand with frame + statsPanel:SetPoint("TOPLEFT", frame, "TOPLEFT", 452, -90) + statsPanel:SetFrameLevel(50) + + -- Hook to prevent size reset + hooksecurefunc(statsPanel, "SetSize", function(self, w, h) + if w ~= (220 + 360) then + self:SetSize(220 + 360, 340) + end + end) + + hooksecurefunc(statsPanel, "SetWidth", function(self, w) + if w ~= (220 + 360) then + self:SetWidth(220 + 360) + end + end) + + -- Stats panel background (fixed width, not scaled) + local statsBg = statsPanel:CreateTexture(nil, "BACKGROUND") + statsBg:SetColorTexture(0.03, 0.045, 0.05, 0.95) + statsBg:SetSize(220, 340) -- Fixed size, doesn't expand + statsBg:SetPoint("TOPLEFT", statsPanel, "TOPLEFT", 0, 0) + + -- Itemlevel display (anchor to center of statsBg background) + local iLvlText = statsPanel:CreateFontString(nil, "OVERLAY") + iLvlText:SetFont(fontPath, 20, "") + iLvlText:SetPoint("TOP", statsBg, "TOP", 0,60) + iLvlText:SetTextColor(0.6, 0.2, 1, 1) + + -- Function to update itemlevel + local function UpdateItemLevelDisplay() + local avgItemLevel, avgItemLevelEquipped = GetAverageItemLevel() + + -- Format with two decimals + local avgFormatted = format("%.2f", avgItemLevel) + local avgEquippedFormatted = format("%.2f", avgItemLevelEquipped) + + -- Display format: equipped / average + iLvlText:SetText(format("%s / %s", avgEquippedFormatted, avgFormatted)) + end + + -- Create update frame for itemlevel and spec changes + local iLvlUpdateFrame = CreateFrame("Frame") + iLvlUpdateFrame:SetScript("OnUpdate", function() + UpdateItemLevelDisplay() + -- RefreshAttributeStats will be called later after it's defined + end) + + UpdateItemLevelDisplay() + + --[[ Stats panel border + if EllesmereUI and EllesmereUI.PanelPP then + EllesmereUI.PanelPP.CreateBorder(statsPanel, 0.15, 0.15, 0.15, 1, 1, "OVERLAY", 1) + end + ]]-- + + -- Create scroll frame + local scrollFrame = CreateFrame("ScrollFrame", "EUI_CharSheet_ScrollFrame", statsPanel) + scrollFrame:SetSize(260, 320) + scrollFrame:SetPoint("TOPLEFT", statsPanel, "TOPLEFT", 5, -10) + scrollFrame:SetFrameLevel(51) + + -- Create scroll child + local scrollChild = CreateFrame("Frame", "EUI_CharSheet_ScrollChild", scrollFrame) + scrollChild:SetWidth(200) + scrollFrame:SetScrollChild(scrollChild) + + -- Create scrollbar (without template to avoid unwanted textures) + local scrollBar = CreateFrame("Slider", "EUI_CharSheet_ScrollBar", statsPanel) + scrollBar:SetSize(8, 320) + scrollBar:SetPoint("TOPRIGHT", statsPanel, "TOPRIGHT", -5, -10) + scrollBar:SetMinMaxValues(0, 0) + scrollBar:SetValue(0) + scrollBar:SetOrientation("VERTICAL") + + -- Scrollbar background (disabled - causes visual glitches) + -- local scrollBarBg = scrollBar:CreateTexture(nil, "BACKGROUND") + -- scrollBarBg:SetAllPoints() + -- scrollBarBg:SetTexture("Interface/Tooltips/UI-Tooltip-Background") + -- scrollBarBg:SetVertexColor(0.1, 0.1, 0.1, 0.5) + + -- Scrollbar thumb + local scrollBarThumb = scrollBar:GetThumbTexture() + if scrollBarThumb then + scrollBarThumb:SetTexture("Interface/Buttons/UI-SliderBar-Button-Horizontal") + scrollBarThumb:SetSize(8, 20) + end + + -- Scroll handler + scrollBar:SetScript("OnValueChanged", function(self, value) + scrollFrame:SetVerticalScroll(value) + end) + + -- Update scrollbar visibility + scrollChild:SetScript("OnSizeChanged", function() + local scrollHeight = scrollChild:GetHeight() + local viewHeight = scrollFrame:GetHeight() + if scrollHeight > viewHeight then + scrollBar:SetMinMaxValues(0, scrollHeight - viewHeight) + scrollBar:Show() + else + scrollBar:SetValue(0) + scrollBar:Hide() + end + end) + + -- Enable mouse wheel scrolling + scrollFrame:SetScript("OnMouseWheel", function(self, delta) + local minVal, maxVal = scrollBar:GetMinMaxValues() + local newVal = scrollBar:GetValue() - (delta * 20) + newVal = math.max(minVal, math.min(maxVal, newVal)) + scrollBar:SetValue(newVal) + end) + + -- Helper function to get crest values + local function GetCrestValue(currencyID) + if C_CurrencyInfo and C_CurrencyInfo.GetCurrencyInfo then + local info = C_CurrencyInfo.GetCurrencyInfo(currencyID) + if info then + return info.quantity or 0 + end + end + return 0 + end + + -- Crest maximum values (per season) + local crestMaxValues = { + [3347] = 400, -- Myth + [3345] = 400, -- Hero + [3344] = 700, -- Champion + [3341] = 700, -- Veteran + [3391] = 700, -- Adventurer + } + + -- Helper function to get crest maximum value + local function GetCrestMaxValue(currencyID) + return crestMaxValues[currencyID] or 3000 + end + + -- Check if a stat should be shown based on class/spec conditions + local function ShouldShowStat(statShowWhen) + if not statShowWhen then return true end -- Show by default if no condition + + if statShowWhen == "brewmaster" then + local specIndex = GetSpecialization() + if specIndex then + local specId = (GetSpecializationInfo(specIndex)) + return specId == 268 -- Brewmaster Monk + end + return false + end + + return true + end + + -- Determine which stats to show based on class/spec + local function GetFilteredAttributeStats() + local spec = GetSpecialization() + local primaryStatIndex = 4 -- default Intellect + + if spec then + -- Get primary stat directly from spec info (6th return value) + local _, _, _, _, _, primaryStat = GetSpecializationInfo(spec) + primaryStatIndex = primaryStat or 4 + end + + local primaryStatNames = { "Strength", "Agility", "Stamina", "Intellect" } + local primaryStat = primaryStatNames[primaryStatIndex] + + -- Return fixed order: Primary Stat, Stamina, Health + return { + { name = primaryStat, func = function() return UnitStat("player", primaryStatIndex) end }, + { name = "Stamina", func = function() return UnitStat("player", 3) end }, + { name = "Health", func = function() return UnitHealthMax("player") end }, + } + end + + -- Load stat sections order from saved data or use defaults + local function GetStatSectionsOrder() + local defaultOrder = { + { + title = "Attributes", + color = { r = 0.047, g = 0.824, b = 0.616 }, + stats = GetFilteredAttributeStats() + }, + { + title = "Secondary Stats", + color = { r = 0.471, g = 0.255, b = 0.784 }, + stats = { + { name = "Crit", func = function() return GetCritChance("player") or 0 end, format = "%.2f%%" }, + { name = "Haste", func = function() return UnitSpellHaste("player") or 0 end, format = "%.2f%%" }, + { name = "Mastery", func = function() return GetMasteryEffect() or 0 end, format = "%.2f%%" }, + { name = "Versatility", func = function() return GetCombatRatingBonus(CR_VERSATILITY_DAMAGE_DONE) or 0 end, format = "%.2f%%" }, + } + }, + { + title = "Attack", + color = { r = 1, g = 0.353, b = 0.122 }, + stats = { + { name = "Spell Power", func = function() return GetSpellBonusDamage(7) end }, + { name = "Attack Speed", func = function() return UnitAttackSpeed("player") or 0 end, format = "%.2f" }, + } + }, + { + title = "Defense", + color = { r = 0.247, g = 0.655, b = 1 }, + stats = { + { name = "Armor", func = function() local base, effectiveArmor = UnitArmor("player") return effectiveArmor end }, + { name = "Dodge", func = function() return GetDodgeChance() or 0 end, format = "%.2f%%" }, + { name = "Parry", func = function() return GetParryChance() or 0 end, format = "%.2f%%" }, + { name = "Stagger Effect", func = function() return C_PaperDollInfo.GetStaggerPercentage("player") or 0 end, format = "%.2f%%", showWhen = "brewmaster" }, + } + }, + { + title = "Crests", + color = { r = 1, g = 0.784, b = 0.341 }, + stats = { + { name = "Myth", func = function() return GetCrestValue(3347) end, format = "%d", currencyID = 3347 }, + { name = "Hero", func = function() return GetCrestValue(3345) end, format = "%d", currencyID = 3345 }, + { name = "Champion", func = function() return GetCrestValue(3344) end, format = "%d", currencyID = 3344 }, + { name = "Veteran", func = function() return GetCrestValue(3341) end, format = "%d", currencyID = 3341 }, + { name = "Adventurer", func = function() return GetCrestValue(3391) end, format = "%d", currencyID = 3391 }, + } + } + } + + -- Apply saved order if exists + if EllesmereUIDB and EllesmereUIDB.statSectionsOrder then + local orderedSections = {} + for _, title in ipairs(EllesmereUIDB.statSectionsOrder) do + for _, section in ipairs(defaultOrder) do + if section.title == title then + table.insert(orderedSections, section) + break + end + end + end + return #orderedSections == #defaultOrder and orderedSections or defaultOrder + end + return defaultOrder + end + + local statSections = GetStatSectionsOrder() + + frame._statsPanel = statsPanel + frame._statsValues = {} -- Will be filled as sections are created + frame._statsSections = {} -- Store sections for collapse/expand + frame._lastSpec = GetSpecialization() -- Track current spec + + -- Function to refresh attributes stats if spec changed + local function RefreshAttributeStats() + local currentSpec = GetSpecialization() + if currentSpec == frame._lastSpec then return end + + frame._lastSpec = currentSpec + + -- Find and update Attributes section + for sectionIdx, sectionData in ipairs(frame._statsSections) do + if sectionData.sectionTitle == "Attributes" then + -- Get new stats for current spec + local newStats = GetFilteredAttributeStats() + + -- Update existing stat elements with new names and functions + local labelIndex = 0 + for _, stat in ipairs(sectionData.stats) do + if stat.label then + labelIndex = labelIndex + 1 + + if newStats[labelIndex] then + -- Update label text + stat.label:SetText(newStats[labelIndex].name) + stat.label:Show() + + if stat.value then + -- Find and update the corresponding entry in frame._statsValues + for _, statsValueEntry in ipairs(frame._statsValues) do + if statsValueEntry.value == stat.value then + -- Update the function + statsValueEntry.func = newStats[labelIndex].func + statsValueEntry.format = newStats[labelIndex].format or "%d" + -- Update display immediately + local newValue = newStats[labelIndex].func() + if newValue ~= nil then + local fmt = statsValueEntry.format + if fmt:find("%%") then + stat.value:SetText(format(fmt, newValue)) + else + stat.value:SetText(format(fmt, newValue)) + end + end + break + end + end + stat.value:Show() + end + else + -- Hide stats that aren't in newStats + stat.label:Hide() + if stat.value then stat.value:Hide() end + end + elseif stat.divider then + -- Show dividers only between visible stats + stat.divider:SetShown(labelIndex < #newStats) + end + end + + frame._recalculateSections() + break + end + end + end + + -- Function to refresh visibility based on showWhen conditions + local function RefreshStatsVisibility() + local currentSpec = GetSpecialization() + + for _, sectionData in ipairs(frame._statsSections) do + for _, stat in ipairs(sectionData.stats) do + if stat.label and stat.showWhen then + local shouldShow = ShouldShowStat(stat.showWhen) + if stat.label then stat.label:SetShown(shouldShow) end + if stat.value then stat.value:SetShown(shouldShow) end + end + end + end + end + + -- Create update frame to monitor spec changes + local specUpdateFrame = CreateFrame("Frame") + specUpdateFrame:SetScript("OnUpdate", function() + RefreshAttributeStats() -- Update Primary Stat + RefreshStatsVisibility() -- Update showWhen visibility + end) + + -- Function to update visibility of stat categories + local function UpdateStatCategoryVisibility() + if not frame._statsSections or #frame._statsSections == 0 then return end + + for _, sectionData in ipairs(frame._statsSections) do + local categoryTitle = sectionData.sectionTitle + local settingKey = "showStatCategory_" .. categoryTitle:gsub(" ", "") + local shouldShow = not (EllesmereUIDB and EllesmereUIDB[settingKey] == false) + + if shouldShow then + sectionData.container:Show() + else + sectionData.container:Hide() + end + end + frame._recalculateSections() + end + EllesmereUI._updateStatCategoryVisibility = UpdateStatCategoryVisibility + + -- Function to recalculate all section positions + local function RecalculateSectionPositions() + local yOffset = 0 + for _, sectionData in ipairs(frame._statsSections) do + -- Skip hidden categories + if sectionData.container:IsShown() then + local sectionHeight = sectionData.isCollapsed and 16 or sectionData.height + sectionData.container:ClearAllPoints() + sectionData.container:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, yOffset) + sectionData.container:SetPoint("TOPRIGHT", scrollChild, "TOPRIGHT", 0, yOffset) + sectionData.container:SetHeight(sectionHeight) + yOffset = yOffset - sectionHeight - 16 + end + end + scrollChild:SetHeight(-yOffset) + end + frame._recalculateSections = RecalculateSectionPositions + + -- Create sections in scroll child + local yOffset = 0 + for sectionIdx, section in ipairs(statSections) do + -- Section container + local sectionContainer = CreateFrame("Frame", nil, scrollChild) + sectionContainer:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, yOffset) + sectionContainer:SetPoint("TOPRIGHT", scrollChild, "TOPRIGHT", 0, yOffset) + sectionContainer:SetWidth(260) + + -- Container for title and bars (clickable) + local titleContainer = CreateFrame("Button", nil, sectionContainer) + titleContainer:SetPoint("TOP", sectionContainer, "TOPLEFT", 100, 0) + titleContainer:SetSize(200, 16) + titleContainer:RegisterForClicks("LeftButtonUp") + + -- Section title (centered in container) + local sectionTitle = titleContainer:CreateFontString(nil, "OVERLAY") + sectionTitle:SetFont(fontPath, 13, "") + sectionTitle:SetTextColor(section.color.r, section.color.g, section.color.b, 1) + sectionTitle:SetPoint("CENTER", titleContainer, "CENTER", 0, 0) + sectionTitle:SetText(section.title) + + -- Left bar (from left edge of container to text) + local leftBar = titleContainer:CreateTexture(nil, "ARTWORK") + leftBar:SetColorTexture(section.color.r, section.color.g, section.color.b, 0.8) + leftBar:SetPoint("LEFT", titleContainer, "LEFT", 0, 0) + leftBar:SetPoint("RIGHT", sectionTitle, "LEFT", -8, 0) + leftBar:SetHeight(2) + + -- Right bar (from text to right edge of container) + local rightBar = titleContainer:CreateTexture(nil, "ARTWORK") + rightBar:SetColorTexture(section.color.r, section.color.g, section.color.b, 0.8) + rightBar:SetPoint("LEFT", sectionTitle, "RIGHT", 8, 0) + rightBar:SetPoint("RIGHT", titleContainer, "RIGHT", 0, 0) + rightBar:SetHeight(2) + + local statYOffset = -22 + + -- Store section data for collapse/expand + local sectionData = { + title = titleContainer, + container = sectionContainer, + stats = {}, + isCollapsed = false, + height = 0, + sectionTitle = section.title -- Store title for reordering + } + table.insert(frame._statsSections, sectionData) + + -- Stats in section + for statIdx, stat in ipairs(section.stats) do + -- Skip stats that don't meet the show conditions + if ShouldShowStat(stat.showWhen) then + -- Stat label + local label = sectionContainer:CreateFontString(nil, "OVERLAY") + label:SetFont(fontPath, 12, "") + label:SetTextColor(0.7, 0.7, 0.7, 0.8) + label:SetPoint("TOPLEFT", sectionContainer, "TOPLEFT", 15, statYOffset) + label:SetText(stat.name) + + -- Stat value + local value = sectionContainer:CreateFontString(nil, "OVERLAY") + value:SetFont(fontPath, 12, "") + value:SetTextColor(section.color.r, section.color.g, section.color.b, 1) + value:SetPoint("TOPRIGHT", sectionContainer, "TOPRIGHT", -2, statYOffset) + value:SetJustifyH("RIGHT") + value:SetText("0") + + -- Create button overlay for crest values to show tooltips + local valueButton = nil + if stat.currencyID then + valueButton = CreateFrame("Button", nil, sectionContainer) + valueButton:SetPoint("TOPRIGHT", sectionContainer, "TOPRIGHT", -2, statYOffset) + valueButton:SetSize(50, 16) + valueButton:EnableMouse(true) + valueButton:SetScript("OnEnter", function() + local current = GetCrestValue(stat.currencyID) + local maximum = GetCrestMaxValue(stat.currencyID) + GameTooltip:SetOwner(valueButton, "ANCHOR_RIGHT") + GameTooltip:AddLine(stat.name .. " Crests", 1, 1, 1) + GameTooltip:AddLine(string.format("%d / %d", current, maximum), 0.7, 0.7, 0.7) + GameTooltip:Show() + end) + valueButton:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + end + + -- Store for updates + table.insert(frame._statsValues, { + value = value, + func = stat.func, + format = stat.format or "%d" + }) + + -- Store stat elements for collapse/expand (include showWhen for visibility checks) + table.insert(sectionData.stats, {label = label, value = value, button = valueButton, showWhen = stat.showWhen}) + + -- Divider line between stats + if statIdx < #section.stats then + local divider = sectionContainer:CreateTexture(nil, "OVERLAY") + divider:SetColorTexture(0.1, 0.1, 0.1, 0.4) + divider:SetPoint("TOPLEFT", sectionContainer, "TOPLEFT", 10, statYOffset - 8) + divider:SetPoint("TOPRIGHT", sectionContainer, "TOPRIGHT", -10, statYOffset - 8) + divider:SetHeight(1) + table.insert(sectionData.stats, {divider = divider}) + end + + statYOffset = statYOffset - 16 + end + end + + sectionData.height = -statYOffset + + -- Click handler for collapse/expand + titleContainer:SetScript("OnClick", function() + sectionData.isCollapsed = not sectionData.isCollapsed + for _, stat in ipairs(sectionData.stats) do + if sectionData.isCollapsed then + if stat.label then stat.label:Hide() end + if stat.value then stat.value:Hide() end + if stat.button then stat.button:Hide() end + if stat.divider then stat.divider:Hide() end + else + if stat.label then stat.label:Show() end + if stat.value then stat.value:Show() end + if stat.button then stat.button:Show() end + if stat.divider then stat.divider:Show() end + end + end + + frame._recalculateSections() + end) + + -- Up/Down arrow buttons (shown on hover) + do + local arrowSize = 10 + local MEDIA = "Interface\\AddOns\\EllesmereUI\\media\\" + + -- Up arrow button + local upBtn = CreateFrame("Button", nil, titleContainer) + upBtn:SetSize(arrowSize, arrowSize) + upBtn:SetPoint("RIGHT", titleContainer, "RIGHT", 32, 0) + upBtn:SetAlpha(0) -- Hidden by default + local upIcon = upBtn:CreateTexture(nil, "OVERLAY") + upIcon:SetAllPoints() + upIcon:SetTexture(MEDIA .. "icons\\eui-arrow-up3.png") + upBtn:SetScript("OnClick", function() + -- Find current index in _statsSections + local currentIdx = nil + for i, sec in ipairs(frame._statsSections) do + if sec == sectionData then + currentIdx = i + break + end + end + + if currentIdx and currentIdx > 1 then + -- Swap with previous section + frame._statsSections[currentIdx], frame._statsSections[currentIdx - 1] = frame._statsSections[currentIdx - 1], frame._statsSections[currentIdx] + + -- Save new order + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.statSectionsOrder = {} + for _, sec in ipairs(frame._statsSections) do + table.insert(EllesmereUIDB.statSectionsOrder, sec.sectionTitle) + end + + frame._recalculateSections() + end + end) + + -- Down arrow button + local downBtn = CreateFrame("Button", nil, titleContainer) + downBtn:SetSize(arrowSize, arrowSize) + downBtn:SetPoint("RIGHT", upBtn, "LEFT", -4, 0) + downBtn:SetAlpha(0) -- Hidden by default + local downIcon = downBtn:CreateTexture(nil, "OVERLAY") + downIcon:SetAllPoints() + downIcon:SetTexture(MEDIA .. "icons\\eui-arrow-down3.png") + downBtn:SetScript("OnClick", function() + -- Find current index in _statsSections + local currentIdx = nil + for i, sec in ipairs(frame._statsSections) do + if sec == sectionData then + currentIdx = i + break + end + end + + if currentIdx and currentIdx < #frame._statsSections then + -- Swap with next section + frame._statsSections[currentIdx], frame._statsSections[currentIdx + 1] = frame._statsSections[currentIdx + 1], frame._statsSections[currentIdx] + + -- Save new order + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.statSectionsOrder = {} + for _, sec in ipairs(frame._statsSections) do + table.insert(EllesmereUIDB.statSectionsOrder, sec.sectionTitle) + end + + frame._recalculateSections() + end + end) + + -- Show arrows on hover (both on titleContainer and arrow buttons) + local function ShowArrows() + upBtn:SetAlpha(0.8) + downBtn:SetAlpha(0.8) + end + local function HideArrows() + upBtn:SetAlpha(0) + downBtn:SetAlpha(0) + end + + titleContainer:SetScript("OnEnter", ShowArrows) + titleContainer:SetScript("OnLeave", HideArrows) + upBtn:SetScript("OnEnter", ShowArrows) + upBtn:SetScript("OnLeave", HideArrows) + downBtn:SetScript("OnEnter", ShowArrows) + downBtn:SetScript("OnLeave", HideArrows) + end + + sectionContainer:SetHeight(sectionData.height) + yOffset = yOffset - sectionData.height - 16 + end + + -- Set scroll child height + scrollChild:SetHeight(-yOffset) + + -- Save initial order if not already saved + if not (EllesmereUIDB and EllesmereUIDB.statSectionsOrder) then + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.statSectionsOrder = {} + for _, sec in ipairs(frame._statsSections) do + table.insert(EllesmereUIDB.statSectionsOrder, sec.sectionTitle) + end + end + + -- Apply initial visibility settings + UpdateStatCategoryVisibility() + + -- Function to update all stats + local function UpdateAllStats() + for _, statEntry in ipairs(frame._statsValues) do + local result = statEntry.func() + if result ~= nil then + if statEntry.format:find("%%") then + statEntry.value:SetText(format(statEntry.format, result)) + else + statEntry.value:SetText(format(statEntry.format, result)) + end + else + statEntry.value:SetText("0") + end + end + end + + -- Update stats immediately once + UpdateAllStats() + + -- Monitor to update stats + local statsMonitor = CreateFrame("Frame") + statsMonitor:SetScript("OnUpdate", function() + if not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) then + return + end + if frame and frame:IsShown() and (frame.selectedTab or 1) == 1 then + UpdateAllStats() + end + end) + + -- Apply custom rarity borders to slots (like CharacterSheetINSPO style) + local function ApplyCustomSlotBorder(slotName) + local slot = _G[slotName] + if not slot then return end + + -- Hide Blizzard IconBorder + if slot.IconBorder then + slot.IconBorder:Hide() + end + + -- Hide overlay textures + if slot.IconOverlay then + slot.IconOverlay:Hide() + end + if slot.IconOverlay2 then + slot.IconOverlay2:Hide() + end + + -- Crop icon inward + if slot.icon then + slot.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) + end + + -- Hide NormalTexture + local normalTexture = _G[slotName .. "NormalTexture"] + if normalTexture then + normalTexture:Hide() + end + + -- Get item rarity color for border + local itemLink = GetInventoryItemLink("player", slot:GetID()) + local borderR, borderG, borderB = 1, 1, 0 -- Default yellow + if itemLink then + local _, _, rarity = GetItemInfo(itemLink) + if rarity then + borderR, borderG, borderB = C_Item.GetItemQualityColor(rarity) + end + end + + -- Add border directly on the slot with item color (2px thickness) + if EllesmereUI and EllesmereUI.PanelPP then + EllesmereUI.PanelPP.CreateBorder(slot, borderR, borderG, borderB, 1, 2, "OVERLAY", 7) + end + end + + -- Apply custom rarity borders to all item slots + local itemSlots = { + "CharacterHeadSlot", "CharacterNeckSlot", "CharacterShoulderSlot", + "CharacterChestSlot", "CharacterWaistSlot", "CharacterLegsSlot", + "CharacterFeetSlot", "CharacterWristSlot", "CharacterHandsSlot", + "CharacterFinger0Slot", "CharacterFinger1Slot", + "CharacterTrinket0Slot", "CharacterTrinket1Slot", + "CharacterBackSlot", "CharacterMainHandSlot", "CharacterSecondaryHandSlot", + "CharacterShirtSlot", "CharacterTabardSlot" + } + + -- Store on frame for use in tab hooks + frame._themedSlots = itemSlots + + -- Create custom buttons for right side (Character, Titles, Equipment Manager) + local buttonWidth = 70 + local buttonHeight = 25 + local buttonSpacing = 5 + -- Center buttons in right column (right column is ~268px wide starting at x=420) + local totalButtonWidth = (buttonWidth * 3) + (buttonSpacing * 2) + local rightColumnWidth = 268 + local startX = 425 + (rightColumnWidth - totalButtonWidth) / 2 + local startY = -60 -- Position near bottom of frame, but within bounds + + local function CreateEUIButton(name, label, onClick) + local btn = CreateFrame("Button", "EUI_CharSheet_" .. name, frame, "SecureActionButtonTemplate") + btn:SetSize(buttonWidth, buttonHeight) + btn:SetPoint("TOPLEFT", frame, "TOPLEFT", startX, startY) + + -- Background + local bg = btn:CreateTexture(nil, "BACKGROUND") + bg:SetColorTexture(0.03, 0.045, 0.05, 1) + bg:SetAllPoints() + + -- Border + if EllesmereUI and EllesmereUI.PanelPP then + EllesmereUI.PanelPP.CreateBorder(btn, 0.2, 0.2, 0.2, 1, 1, "OVERLAY", 2) + end + + -- Text + local text = btn:CreateFontString(nil, "OVERLAY") + text:SetFont(fontPath, 11, "") + text:SetTextColor(1, 1, 1, 0.8) + text:SetPoint("CENTER", btn, "CENTER", 0, 0) + text:SetText(label) + + -- Hover effect + btn:SetScript("OnEnter", function() + text:SetTextColor(1, 1, 1, 1) + bg:SetColorTexture(0.05, 0.07, 0.08, 1) + end) + btn:SetScript("OnLeave", function() + text:SetTextColor(1, 1, 1, 0.8) + bg:SetColorTexture(0.03, 0.045, 0.05, 1) + end) + + -- Click handler + btn:SetScript("OnClick", onClick) + + return btn + end + + -- Character button (will be updated after stats panel is created) + local characterBtn = CreateEUIButton("Stats", "Character", function() end) + + -- Create Titles Panel (same position and size as stats panel) + local titlesPanel = CreateFrame("Frame", "EUI_CharSheet_TitlesPanel", frame) + titlesPanel:SetSize(220, 340) + titlesPanel:SetPoint("TOPLEFT", frame, "TOPLEFT", 452, -90) + titlesPanel:SetFrameLevel(50) + titlesPanel:Hide() + frame._titlesPanel = titlesPanel -- Store reference on frame + + -- Titles panel background + local titlesBg = titlesPanel:CreateTexture(nil, "BACKGROUND") + titlesBg:SetColorTexture(0.03, 0.045, 0.05, 0.95) + titlesBg:SetAllPoints() + + -- Search box for titles + local titlesSearchBox = CreateFrame("EditBox", "EUI_CharSheet_TitlesSearchBox", titlesPanel) + titlesSearchBox:SetSize(200, 24) + titlesSearchBox:SetPoint("TOPLEFT", titlesPanel, "TOPLEFT", 10, -10) + titlesSearchBox:SetAutoFocus(false) + titlesSearchBox:SetMaxLetters(20) + + local searchBg = titlesSearchBox:CreateTexture(nil, "BACKGROUND") + searchBg:SetColorTexture(0.1, 0.12, 0.14, 0.9) + searchBg:SetAllPoints() + + titlesSearchBox:SetTextColor(1, 1, 1, 1) + titlesSearchBox:SetFont(fontPath, 10, "") + + -- Hint text + local hintText = titlesSearchBox:CreateFontString(nil, "OVERLAY") + hintText:SetFont(fontPath, 10, "") + hintText:SetText("search for title") + hintText:SetTextColor(0.6, 0.6, 0.6, 0.7) + hintText:SetPoint("LEFT", titlesSearchBox, "LEFT", 5, 0) + + -- Create scroll frame for titles + local titlesScrollFrame = CreateFrame("ScrollFrame", "EUI_CharSheet_TitlesScrollFrame", titlesPanel) + titlesScrollFrame:SetSize(200, 300) + titlesScrollFrame:SetPoint("TOPLEFT", titlesPanel, "TOPLEFT", 5, -40) + titlesScrollFrame:EnableMouseWheel(true) + + -- Create scroll child + local titlesScrollChild = CreateFrame("Frame", "EUI_CharSheet_TitlesScrollChild", titlesScrollFrame) + titlesScrollChild:SetWidth(200) + titlesScrollFrame:SetScrollChild(titlesScrollChild) + + -- Mousewheel support + titlesScrollFrame:SetScript("OnMouseWheel", function(self, delta) + local currentScroll = titlesScrollFrame:GetVerticalScroll() + local maxScroll = math.max(0, titlesScrollChild:GetHeight() - titlesScrollFrame:GetHeight()) + local newScroll = currentScroll - delta * 20 + newScroll = math.max(0, math.min(newScroll, maxScroll)) + titlesScrollFrame:SetVerticalScroll(newScroll) + end) + + -- Populate titles + local function RefreshTitlesList() + -- Clear old buttons + for _, child in ipairs({titlesScrollChild:GetChildren()}) do + child:Hide() + end + + local currentTitle = GetCurrentTitle() + local yOffset = 0 + local searchText = titlesSearchBox:GetText():lower() + local titleButtons = {} -- Store button references + + -- Add "No Title" button + local noTitleBtn = CreateFrame("Button", nil, titlesScrollChild) + noTitleBtn:SetWidth(240) + noTitleBtn:SetHeight(24) + noTitleBtn:SetPoint("TOPLEFT", titlesScrollChild, "TOPLEFT", 10, yOffset) + + local noTitleBg = noTitleBtn:CreateTexture(nil, "BACKGROUND") + noTitleBg:SetColorTexture(0.05, 0.07, 0.08, 0.8) + noTitleBg:SetAllPoints() + titleButtons[0] = { btn = noTitleBtn, bg = noTitleBg } + + local noTitleText = noTitleBtn:CreateFontString(nil, "OVERLAY") + noTitleText:SetFont(fontPath, 11, "") + noTitleText:SetText("No Title") + noTitleText:SetTextColor(1, 1, 1, 1) + noTitleText:SetPoint("LEFT", noTitleBtn, "LEFT", 10, 0) + + noTitleBtn:SetScript("OnClick", function() + SetCurrentTitle(0) + titlesSearchBox:SetText("") + hintText:Show() + RefreshTitlesList() + end) + + noTitleBtn:SetScript("OnEnter", function() + noTitleBg:SetColorTexture(0.047, 0.824, 0.616, 0.2) + end) + + noTitleBtn:SetScript("OnLeave", function() + if GetCurrentTitle() == 0 then + noTitleBg:SetColorTexture(0.1, 0.12, 0.14, 0.9) -- Lighter gray for active title + else + noTitleBg:SetColorTexture(0.05, 0.07, 0.08, 0.8) + end + end) + + yOffset = yOffset - 28 + + -- Add all available titles + for titleIndex = 1, GetNumTitles() do + if IsTitleKnown(titleIndex) then + local titleName = GetTitleName(titleIndex) + if titleName and (searchText == "" or titleName:lower():find(searchText, 1, true)) then + local titleBtn = CreateFrame("Button", nil, titlesScrollChild) + titleBtn:SetWidth(240) + titleBtn:SetHeight(24) + titleBtn:SetPoint("TOPLEFT", titlesScrollChild, "TOPLEFT", 10, yOffset) + titleBtn._titleIndex = titleIndex + + local btnBg = titleBtn:CreateTexture(nil, "BACKGROUND") + btnBg:SetColorTexture(0.05, 0.07, 0.08, 0.8) + btnBg:SetAllPoints() + titleButtons[titleIndex] = { btn = titleBtn, bg = btnBg } + + local titleText = titleBtn:CreateFontString(nil, "OVERLAY") + titleText:SetFont(fontPath, 11, "") + titleText:SetText(titleName) + titleText:SetTextColor(1, 1, 1, 1) + titleText:SetPoint("LEFT", titleBtn, "LEFT", 10, 0) + + titleBtn:SetScript("OnClick", function() + SetCurrentTitle(titleBtn._titleIndex) + titlesSearchBox:SetText("") + hintText:Show() + -- Schedule refresh after a frame to ensure the title is updated + C_Timer.After(0, RefreshTitlesList) + end) + + titleBtn:SetScript("OnEnter", function() + btnBg:SetColorTexture(0.047, 0.824, 0.616, 0.2) + end) + + titleBtn:SetScript("OnLeave", function() + if GetCurrentTitle() == titleIndex then + btnBg:SetColorTexture(0.1, 0.12, 0.14, 0.9) -- Lighter gray for active title + else + btnBg:SetColorTexture(0.05, 0.07, 0.08, 0.8) + end + end) + + yOffset = yOffset - 28 + end + end + end + + -- Re-read current title to ensure it's updated + currentTitle = GetCurrentTitle() + + -- Update colors based on current title + for titleIndex, btnData in pairs(titleButtons) do + if currentTitle == titleIndex then + btnData.bg:SetColorTexture(0.1, 0.12, 0.14, 0.9) + else + btnData.bg:SetColorTexture(0.05, 0.07, 0.08, 0.8) + end + end + + titlesScrollChild:SetHeight(-yOffset) + end + + -- Search input handler + titlesSearchBox:SetScript("OnTextChanged", function() + RefreshTitlesList() + end) + + -- Focus gained handler + titlesSearchBox:SetScript("OnEditFocusGained", function() + if titlesSearchBox:GetText() == "" then + hintText:Hide() + end + end) + + -- Focus lost handler + titlesSearchBox:SetScript("OnEditFocusLost", function() + if titlesSearchBox:GetText() == "" then + hintText:Show() + end + end) + + -- Populate initially + RefreshTitlesList() + + -- Hook to refresh titles when shown + frame._titlesPanel:HookScript("OnShow", function() + titlesSearchBox:SetText("") + RefreshTitlesList() + end) + + -- Update the Character button to show stats + characterBtn:SetScript("OnClick", function() + if not statsPanel:IsShown() then + statsPanel:Show() + CharacterFrame._titlesPanel:Hide() + CharacterFrame._equipPanel:Hide() + end + end) + + -- Titles button to show titles + CreateEUIButton("Titles", "Titles", function() + if not CharacterFrame._titlesPanel:IsShown() then + CharacterFrame._titlesPanel:Show() + statsPanel:Hide() + CharacterFrame._equipPanel:Hide() + end + end) + + -- Create Equipment Panel (same position and size as stats panel) + local equipPanel = CreateFrame("Frame", "EUI_CharSheet_EquipPanel", frame) + equipPanel:SetSize(220, 340) + equipPanel:SetPoint("TOPLEFT", frame, "TOPLEFT", 452, -90) + equipPanel:SetFrameLevel(50) + equipPanel:Hide() + frame._equipPanel = equipPanel + + -- Equipment panel background + local equipBg = equipPanel:CreateTexture(nil, "BACKGROUND") + equipBg:SetColorTexture(0.03, 0.045, 0.05, 0.95) + equipBg:SetAllPoints() + + -- Create scroll frame for equipment + local equipScrollFrame = CreateFrame("ScrollFrame", "EUI_CharSheet_EquipScrollFrame", equipPanel) + equipScrollFrame:SetSize(200, 320) + equipScrollFrame:SetPoint("TOPLEFT", equipPanel, "TOPLEFT", 5, -10) + equipScrollFrame:EnableMouseWheel(true) + + -- Create scroll child + local equipScrollChild = CreateFrame("Frame", "EUI_CharSheet_EquipScrollChild", equipScrollFrame) + equipScrollChild:SetWidth(200) + equipScrollFrame:SetScrollChild(equipScrollChild) + + -- Mousewheel support + equipScrollFrame:SetScript("OnMouseWheel", function(self, delta) + local currentScroll = equipScrollFrame:GetVerticalScroll() + local maxScroll = math.max(0, equipScrollChild:GetHeight() - equipScrollFrame:GetHeight()) + local newScroll = currentScroll - delta * 20 + newScroll = math.max(0, math.min(newScroll, maxScroll)) + equipScrollFrame:SetVerticalScroll(newScroll) + end) + + -- Track selected equipment set + local selectedSetID = nil + + -- Function to reload equipment sets + local function RefreshEquipmentSets() + -- Clear old buttons + for _, child in ipairs({equipScrollChild:GetChildren()}) do + child:Hide() + end + + local equipmentSets = {} + if C_EquipmentSet then + local setIDs = C_EquipmentSet.GetEquipmentSetIDs() + if setIDs then + for _, setID in ipairs(setIDs) do + local setName = C_EquipmentSet.GetEquipmentSetInfo(setID) + if setName and setName ~= "" then + table.insert(equipmentSets, {id = setID, name = setName}) + end + end + end + end + + -- "New Set" button at top + local newSetBtn = CreateFrame("Button", nil, equipScrollChild) + newSetBtn:SetWidth(200) + newSetBtn:SetHeight(24) + newSetBtn:SetPoint("TOPLEFT", equipScrollChild, "TOPLEFT", 0, 0) + + local newSetBg = newSetBtn:CreateTexture(nil, "BACKGROUND") + newSetBg:SetColorTexture(0.047, 0.824, 0.616, 0.3) + newSetBg:SetAllPoints() + + local newSetText = newSetBtn:CreateFontString(nil, "OVERLAY") + newSetText:SetFont(fontPath, 11, "") + newSetText:SetText("+ New Set") + newSetText:SetTextColor(1, 1, 1, 1) + newSetText:SetPoint("CENTER", newSetBtn, "CENTER", 0, 0) + + newSetBtn:SetScript("OnClick", function() + StaticPopupDialogs["EUI_NEW_EQUIPMENT_SET"] = { + text = "New equipment set name:", + button1 = "Create", + button2 = "Cancel", + OnAccept = function(dialog) + local newName = dialog.EditBox:GetText() + if newName ~= "" then + C_EquipmentSet.CreateEquipmentSet(newName) + RefreshEquipmentSets() + end + end, + hasEditBox = true, + editBoxWidth = 350, + timeout = 0, + whileDead = false, + hideOnEscape = true, + } + StaticPopup_Show("EUI_NEW_EQUIPMENT_SET") + end) + + newSetBtn:SetScript("OnEnter", function() + newSetBg:SetColorTexture(0.047, 0.824, 0.616, 0.5) + end) + + newSetBtn:SetScript("OnLeave", function() + newSetBg:SetColorTexture(0.047, 0.824, 0.616, 0.3) + end) + + -- Equip and Save buttons at top + local equipTopBtn = CreateFrame("Button", nil, equipScrollChild) + equipTopBtn:SetWidth(95) + equipTopBtn:SetHeight(24) + equipTopBtn:SetPoint("TOPLEFT", equipScrollChild, "TOPLEFT", 0, -44) + + local equipTopBg = equipTopBtn:CreateTexture(nil, "BACKGROUND") + equipTopBg:SetColorTexture(0.05, 0.07, 0.08, 1) + equipTopBg:SetAllPoints() + + if EllesmereUI and EllesmereUI.PanelPP then + EllesmereUI.PanelPP.CreateBorder(equipTopBtn, 0.8, 0.8, 0.8, 1, 1, "OVERLAY", 1) + end + + local equipTopText = equipTopBtn:CreateFontString(nil, "OVERLAY") + equipTopText:SetFont(fontPath, 10, "") + equipTopText:SetText("Equip") + equipTopText:SetTextColor(1, 1, 1, 1) + equipTopText:SetPoint("CENTER", equipTopBtn, "CENTER", 0, 0) + + equipTopBtn:SetScript("OnClick", function() + if selectedSetID then + C_EquipmentSet.UseEquipmentSet(selectedSetID) + activeEquipmentSetID = selectedSetID + -- Save to DB for persistence + if EllesmereUIDB then + EllesmereUIDB.lastEquippedSet = selectedSetID + end + RefreshEquipmentSets() + end + end) + + -- Save button at top + local saveTopBtn = CreateFrame("Button", nil, equipScrollChild) + saveTopBtn:SetWidth(95) + saveTopBtn:SetHeight(24) + saveTopBtn:SetPoint("TOPLEFT", equipScrollChild, "TOPLEFT", 105, -44) + + local saveTopBg = saveTopBtn:CreateTexture(nil, "BACKGROUND") + saveTopBg:SetColorTexture(0.05, 0.07, 0.08, 1) + saveTopBg:SetAllPoints() + + if EllesmereUI and EllesmereUI.PanelPP then + EllesmereUI.PanelPP.CreateBorder(saveTopBtn, 0.8, 0.8, 0.8, 1, 1, "OVERLAY", 1) + end + + local saveTopText = saveTopBtn:CreateFontString(nil, "OVERLAY") + saveTopText:SetFont(fontPath, 10, "") + saveTopText:SetText("Save") + saveTopText:SetTextColor(1, 1, 1, 1) + saveTopText:SetPoint("CENTER", saveTopBtn, "CENTER", 0, 0) + + saveTopBtn:SetScript("OnClick", function() + -- Visual feedback: change text to "Saved!" and color it green + saveTopText:SetText("Saved!") + saveTopText:SetTextColor(0.047, 0.824, 0.616, 1) -- Green + + if selectedSetID then + C_EquipmentSet.SaveEquipmentSet(selectedSetID) + end + + -- Change back to "Save" after 1 second + C_Timer.After(1, function() + if saveTopText then + saveTopText:SetText("Save") + saveTopText:SetTextColor(1, 1, 1, 1) -- White + end + end) + end) + + local yOffset = -88 -- After buttons + for _, setData in ipairs(equipmentSets) do + local setBtn = CreateFrame("Button", nil, equipScrollChild) + setBtn:SetWidth(200) + setBtn:SetHeight(24) + setBtn:SetPoint("TOPLEFT", equipScrollChild, "TOPLEFT", 0, yOffset) + + -- Background + local btnBg = setBtn:CreateTexture(nil, "BACKGROUND") + if activeEquipmentSetID == setData.id then + btnBg:SetColorTexture(0.1, 0.12, 0.14, 0.9) -- Lighter gray for active set + else + btnBg:SetColorTexture(0.05, 0.07, 0.08, 0.8) + end + btnBg:SetAllPoints() + + -- Text + local setText = setBtn:CreateFontString(nil, "OVERLAY") + setText:SetFont(fontPath, 10, "") + setText:SetText(setData.name) + setText:SetTextColor(1, 1, 1, 1) + setText:SetPoint("LEFT", setBtn, "LEFT", 10, 0) + + -- Spec icon + local assignedSpec = C_EquipmentSet.GetEquipmentSetAssignedSpec(setData.id) + if assignedSpec then + local _, specName, _, specIcon = GetSpecializationInfo(assignedSpec) + if specIcon then + local specIconTexture = setBtn:CreateTexture(nil, "OVERLAY") + specIconTexture:SetTexture(specIcon) + specIconTexture:SetSize(16, 16) + specIconTexture:SetPoint("RIGHT", setBtn, "RIGHT", -25, 0) + end + end + + + -- Click handler (select set) + setBtn:SetScript("OnClick", function() + selectedSetID = setData.id + RefreshEquipmentSets() + end) + + -- Hover effect + setBtn:SetScript("OnEnter", function() + btnBg:SetColorTexture(0.047, 0.824, 0.616, 0.2) + end) + + setBtn:SetScript("OnLeave", function() + if selectedSetID == setData.id then + btnBg:SetColorTexture(0.047, 0.824, 0.616, 0.5) + elseif activeEquipmentSetID == setData.id then + btnBg:SetColorTexture(0.1, 0.12, 0.14, 0.9) -- Lighter gray for active set + else + btnBg:SetColorTexture(0.05, 0.07, 0.08, 0.8) + end + end) + + -- Highlight selected set + if selectedSetID == setData.id then + btnBg:SetColorTexture(0.047, 0.824, 0.616, 0.5) + end + + -- Border for active set + if activeEquipmentSetID == setData.id then + if EllesmereUI and EllesmereUI.PanelPP then + EllesmereUI.PanelPP.CreateBorder(setBtn, 0.15, 0.17, 0.19, 1, 1, "OVERLAY", 1) + end + end + + -- Cogwheel button for spec assignment + local cogBtn = CreateFrame("Button", nil, setBtn) + cogBtn:SetWidth(14) + cogBtn:SetHeight(14) + cogBtn:SetPoint("RIGHT", setBtn, "RIGHT", -5, 0) + + local cogIcon = cogBtn:CreateTexture(nil, "OVERLAY") + cogIcon:SetTexture("Interface/Buttons/UI-OptionsButton") + cogIcon:SetAllPoints() + + -- Hover highlight + local cogHL = cogBtn:CreateTexture(nil, "HIGHLIGHT") + cogHL:SetColorTexture(0.047, 0.824, 0.616, 0.3) + cogHL:SetAllPoints() + cogBtn:SetHighlightTexture(cogHL) + + cogBtn:SetScript("OnClick", function(self, button) + -- Create a simple spec selection menu + local specs = {} + local numSpecs = GetNumSpecializations() + for i = 1, numSpecs do + local id, name = GetSpecializationInfo(i) + if id then + table.insert(specs, {index = i, name = name}) + end + end + + -- Create or reuse menu frame + if not cogBtn.menuFrame then + -- Create invisible backdrop to catch clicks outside menu + local backdrop = CreateFrame("Button", nil, UIParent) + backdrop:SetFrameStrata("DIALOG") + backdrop:SetFrameLevel(99) + backdrop:SetSize(2560, 1440) + backdrop:SetPoint("CENTER", UIParent, "CENTER") + backdrop:SetScript("OnClick", function() + cogBtn.menuFrame:Hide() + backdrop:Hide() + end) + cogBtn.menuBackdrop = backdrop + + cogBtn.menuFrame = CreateFrame("Frame", nil, UIParent) + cogBtn.menuFrame:SetFrameStrata("DIALOG") + cogBtn.menuFrame:SetFrameLevel(100) + cogBtn.menuFrame:SetSize(120, #specs * 24 + 10) + + -- Add background texture + local bg = cogBtn.menuFrame:CreateTexture(nil, "BACKGROUND") + bg:SetColorTexture(0.05, 0.07, 0.08, 0.9) + bg:SetAllPoints() + + -- Add border + local border = cogBtn.menuFrame:CreateTexture(nil, "BORDER") + border:SetColorTexture(0.047, 0.824, 0.616, 1) + border:SetPoint("TOPLEFT", cogBtn.menuFrame, "TOPLEFT", 0, 0) + border:SetPoint("TOPRIGHT", cogBtn.menuFrame, "TOPRIGHT", 0, 0) + border:SetHeight(1) + + border = cogBtn.menuFrame:CreateTexture(nil, "BORDER") + border:SetColorTexture(0.047, 0.824, 0.616, 1) + border:SetPoint("BOTTOMLEFT", cogBtn.menuFrame, "BOTTOMLEFT", 0, 0) + border:SetPoint("BOTTOMRIGHT", cogBtn.menuFrame, "BOTTOMRIGHT", 0, 0) + border:SetHeight(1) + + border = cogBtn.menuFrame:CreateTexture(nil, "BORDER") + border:SetColorTexture(0.047, 0.824, 0.616, 1) + border:SetPoint("TOPLEFT", cogBtn.menuFrame, "TOPLEFT", 0, 0) + border:SetPoint("BOTTOMLEFT", cogBtn.menuFrame, "BOTTOMLEFT", 0, 0) + border:SetWidth(1) + + border = cogBtn.menuFrame:CreateTexture(nil, "BORDER") + border:SetColorTexture(0.047, 0.824, 0.616, 1) + border:SetPoint("TOPRIGHT", cogBtn.menuFrame, "TOPRIGHT", 0, 0) + border:SetPoint("BOTTOMRIGHT", cogBtn.menuFrame, "BOTTOMRIGHT", 0, 0) + border:SetWidth(1) + end + + -- Clear previous buttons + for _, btn in ipairs(cogBtn.menuFrame.specButtons or {}) do + btn:Hide() + end + cogBtn.menuFrame.specButtons = {} + + -- Create spec buttons + local yOffset = 0 + for _, spec in ipairs(specs) do + local btn = CreateFrame("Button", nil, cogBtn.menuFrame) + btn:SetSize(110, 24) + btn:SetPoint("TOP", cogBtn.menuFrame, "TOP", 0, -5 - (yOffset * 24)) + btn:SetNormalFontObject(GameFontNormal) + btn:SetText(spec.name) + + local texture = btn:CreateTexture(nil, "BACKGROUND") + texture:SetColorTexture(0.05, 0.07, 0.08, 0.5) + texture:SetAllPoints() + btn:SetNormalTexture(texture) + + local hlTexture = btn:CreateTexture(nil, "HIGHLIGHT") + hlTexture:SetColorTexture(0.047, 0.824, 0.616, 0.3) + hlTexture:SetAllPoints() + btn:SetHighlightTexture(hlTexture) + + btn:SetScript("OnClick", function() + C_EquipmentSet.AssignSpecToEquipmentSet(setData.id, spec.index) + RefreshEquipmentSets() + cogBtn.menuFrame:Hide() + cogBtn.menuBackdrop:Hide() + end) + + table.insert(cogBtn.menuFrame.specButtons, btn) + yOffset = yOffset + 1 + end + + -- Position and show menu + cogBtn.menuFrame:SetPoint("TOPLEFT", self, "BOTTOMLEFT", 0, -5) + cogBtn.menuFrame:Show() + cogBtn.menuBackdrop:Show() + end) + + yOffset = yOffset - 30 + end + + equipScrollChild:SetHeight(-yOffset) + end + + -- Event handler for equipment set changes + local equipSetChangeFrame = CreateFrame("Frame") + equipSetChangeFrame:RegisterEvent("EQUIPMENT_SETS_CHANGED") + equipSetChangeFrame:SetScript("OnEvent", function() + -- activeEquipmentSetID is set by the Equip button and auto-equip logic + if CharacterFrame and CharacterFrame:IsShown() and CharacterFrame._equipPanel and CharacterFrame._equipPanel:IsShown() then + RefreshEquipmentSets() + end + end) + + -- Hook to refresh equipment sets when shown + equipPanel:HookScript("OnShow", function() + RefreshEquipmentSets() + end) + + -- Equipment Manager button + CreateEUIButton("Equipment", "Equipment", function() + if not CharacterFrame._equipPanel:IsShown() then + CharacterFrame._equipPanel:Show() + statsPanel:Hide() + CharacterFrame._titlesPanel:Hide() + + -- Activate Flyout-Style mode: show flyout menu on hover for all slots + frame._flyoutModeActive = true + else + frame._flyoutModeActive = false + end + end) + + -- Update button positions to stack horizontally + local buttons = { + "EUI_CharSheet_Stats", + "EUI_CharSheet_Titles", + "EUI_CharSheet_Equipment" + } + for i, btnName in ipairs(buttons) do + local btn = _G[btnName] + if btn then + btn:ClearAllPoints() + btn:SetPoint("TOPLEFT", frame, "TOPLEFT", startX + (i - 1) * (buttonWidth + buttonSpacing), startY) + end + end + + -- Left column slots (show itemlevel on right) + local leftColumnSlots = { + "CharacterHeadSlot", "CharacterNeckSlot", "CharacterShoulderSlot", + "CharacterBackSlot", "CharacterChestSlot", "CharacterShirtSlot", + "CharacterTabardSlot", "CharacterWristSlot" + } + + -- Right column slots (show itemlevel on left) + local rightColumnSlots = { + "CharacterHandsSlot", "CharacterWaistSlot", "CharacterLegsSlot", + "CharacterFeetSlot", "CharacterFinger0Slot", "CharacterFinger1Slot", + "CharacterTrinket0Slot", "CharacterTrinket1Slot" + } + + local fontPath = EllesmereUI.GetFontPath and EllesmereUI.GetFontPath() or STANDARD_TEXT_FONT + + -- Create global socket container for all slot icons + local globalSocketContainer = CreateFrame("Frame", "EUI_CharSheet_SocketContainer", frame) + globalSocketContainer:SetFrameLevel(100) + globalSocketContainer:Show() + frame._socketContainer = globalSocketContainer -- Store reference on frame + + for _, slotName in ipairs(itemSlots) do + ApplyCustomSlotBorder(slotName) + + -- Create itemlevel labels + local slot = _G[slotName] + if slot and not slot._itemLevelLabel then + local itemLevelSize = EllesmereUIDB and EllesmereUIDB.charSheetItemLevelSize or 11 + local label = frame:CreateFontString(nil, "OVERLAY") + label:SetFont(fontPath, itemLevelSize, "") + label:SetTextColor(1, 1, 1, 0.8) + label:SetJustifyH("CENTER") + + -- Position based on column + if tContains(leftColumnSlots, slotName) then + -- Left column: show on right side + label:SetPoint("CENTER", slot, "RIGHT", 15, 10) + elseif tContains(rightColumnSlots, slotName) then + -- Right column: show on left side + label:SetPoint("CENTER", slot, "LEFT", -15, 10) + elseif slotName == "CharacterMainHandSlot" then + -- MainHand: show on left side + label:SetPoint("CENTER", slot, "LEFT", -15, 10) + elseif slotName == "CharacterSecondaryHandSlot" then + -- OffHand: show on right side + label:SetPoint("CENTER", slot, "RIGHT", 15, 10) + end + + slot._itemLevelLabel = label + end + + -- Create enchant labels + if slot and not slot._enchantLabel then + local enchantSize = EllesmereUIDB and EllesmereUIDB.charSheetEnchantSize or 9 + local enchantLabel = frame:CreateFontString(nil, "OVERLAY") + enchantLabel:SetFont(fontPath, enchantSize, "") + enchantLabel:SetTextColor(1, 1, 1, 0.8) + enchantLabel:SetJustifyH("CENTER") + + -- Position based on column (below itemlevel) + if tContains(leftColumnSlots, slotName) then + enchantLabel:SetPoint("LEFT", slot, "RIGHT", 5, -5) + elseif tContains(rightColumnSlots, slotName) then + enchantLabel:SetPoint("Right", slot, "LEFT", -5, -5) + elseif slotName == "CharacterMainHandSlot" then + enchantLabel:SetPoint("RIGHT", slot, "LEFT", -5, -5) + elseif slotName == "CharacterSecondaryHandSlot" then + enchantLabel:SetPoint("LEFT", slot, "RIGHT", 15, -5) + end + + slot._enchantLabel = enchantLabel + end + + -- Create upgrade track labels (positioned relative to itemlevel) + if slot and not slot._upgradeTrackLabel and slot._itemLevelLabel then + local upgradeTrackSize = EllesmereUIDB and EllesmereUIDB.charSheetUpgradeTrackSize or 11 + local upgradeTrackLabel = frame:CreateFontString(nil, "OVERLAY") + upgradeTrackLabel:SetFont(fontPath, upgradeTrackSize, "") + upgradeTrackLabel:SetTextColor(1, 1, 1, 0.6) + upgradeTrackLabel:SetJustifyH("CENTER") + + -- Position beside itemlevel label based on column + if tContains(leftColumnSlots, slotName) then + -- Left column: upgradeTrack RIGHT of itemLevel + upgradeTrackLabel:SetPoint("LEFT", slot._itemLevelLabel, "RIGHT", 3, 0) + elseif tContains(rightColumnSlots, slotName) then + -- Right column: upgradeTrack LEFT of itemLevel + upgradeTrackLabel:SetPoint("RIGHT", slot._itemLevelLabel, "LEFT", -3, 0) + elseif slotName == "CharacterMainHandSlot" then + -- MainHand: upgradeTrack LEFT of itemLevel + upgradeTrackLabel:SetPoint("RIGHT", slot._itemLevelLabel, "LEFT", -3, 0) + elseif slotName == "CharacterSecondaryHandSlot" then + -- OffHand: upgradeTrack RIGHT of itemLevel + upgradeTrackLabel:SetPoint("LEFT", slot._itemLevelLabel, "RIGHT", 3, 0) + end + + slot._upgradeTrackLabel = upgradeTrackLabel + end + end + + -- Socket icon creation and display logic + local function GetOrCreateSocketIcons(slot, side, slotIndex) + if slot._euiCharSocketsIcons then return slot._euiCharSocketsIcons end + + slot._euiCharSocketsIcons = {} + slot._euiCharSocketsBtns = {} + slot._gemLinks = {} + + for i = 1, 4 do -- Max 4 sockets per item + local icon = globalSocketContainer:CreateTexture(nil, "OVERLAY") + icon:SetSize(16, 16) + icon:Hide() + slot._euiCharSocketsIcons[i] = icon + + -- Create invisible button for gem tooltip + local socketBtn = CreateFrame("Button", nil, globalSocketContainer) + socketBtn:SetSize(16, 16) + socketBtn:EnableMouse(true) + socketBtn:Hide() + slot._euiCharSocketsBtns[i] = socketBtn + end + + slot._euiCharSocketsSide = side + slot._euiCharSocketsSlotIndex = slotIndex + + return slot._euiCharSocketsIcons + end + + -- Update socket icons for all slots + local function UpdateSocketIcons(slotName) + local slot = _G[slotName] + if not slot then return end + + local slotIndex = slot:GetID() + local side = tContains(leftColumnSlots, slotName) and "RIGHT" or "LEFT" + + local socketIcons = GetOrCreateSocketIcons(slot, side, slotIndex) + + local link = GetInventoryItemLink("player", slotIndex) + if not link then + for _, icon in ipairs(socketIcons) do icon:Hide() end + return + end + + -- Create tooltip to extract socket textures + local tooltip = CreateFrame("GameTooltip", "EUI_CharSheet_SocketTooltip_" .. slotName, nil, "GameTooltipTemplate") + tooltip:SetOwner(UIParent, "ANCHOR_NONE") + tooltip:SetInventoryItem("player", slotIndex) + + -- Extract socket textures from tooltip + local socketTextures = {} + for i = 1, 10 do + local texture = _G["EUI_CharSheet_SocketTooltip_" .. slotName .. "Texture" .. i] + if texture and texture:IsShown() then + local tex = texture:GetTexture() or texture:GetTextureFileID() + if tex then + table.insert(socketTextures, tex) + end + end + end + + tooltip:Hide() + + -- Extract gem links directly from item link + slot._gemLinks = {} + local itemLink = GetInventoryItemLink("player", slotIndex) + if itemLink then + -- Item link format: |cff...|Hitem:itemID:enchant:gem1:gem2:gem3:gem4:...|h[Name]|h|r + -- Extract the item data part + local itemData = string.match(itemLink, "|H(item:[^|]+)|h") + if itemData then + local parts = {} + for part in string.gmatch(itemData, "([^:]+)") do + table.insert(parts, part) + end + + -- parts[1] = "item", parts[2] = itemID, parts[3] = enchantID, parts[4-7] = gem IDs + if #parts >= 4 then + for i = 4, 7 do + local gemID = tonumber(parts[i]) + if gemID and gemID > 0 then + -- Create a gem link from the ID + local gemName = GetItemInfo(gemID) + if gemName then + -- Create a valid link: |cff...|Hitem:gemID|h[Name]|h|r + local gemLink = "|cff9d9d9d|Hitem:" .. gemID .. "|h[" .. gemName .. "]|h|r" + table.insert(slot._gemLinks, gemLink) + end + end + end + end + end + end + + -- Position and show socket icons + if #socketTextures > 0 then + for i, icon in ipairs(socketIcons) do + if socketTextures[i] then + icon:SetTexture(socketTextures[i]) + icon:SetScale(1) + + -- Position icons based on column + if side == "LEFT" then + icon:SetPoint("RIGHT", slot, "RIGHT", 20, 0 - (i-1)*18) + else + icon:SetPoint("LEFT", slot, "LEFT", -20, 0 - (i-1)*18) + end + icon:Show() + + -- Position button wrapper + local btn = slot._euiCharSocketsBtns[i] + btn:SetPoint("CENTER", icon, "CENTER") + btn:Show() + else + icon:Hide() + local btn = slot._euiCharSocketsBtns[i] + if btn then btn:Hide() end + end + end + + -- Setup tooltip scripts for all gem buttons + for i, btn in ipairs(slot._euiCharSocketsBtns) do + btn:SetScript("OnEnter", function(self) + if slot._gemLinks[i] then + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") + GameTooltip:SetHyperlink(slot._gemLinks[i]) + GameTooltip:Show() + end + end) + btn:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + end + else + for _, icon in ipairs(socketIcons) do + icon:Hide() + end + for _, btn in ipairs(slot._euiCharSocketsBtns or {}) do + btn:Hide() + end + end + end + + -- Refresh socket icons for all slots + local function RefreshAllSocketIcons() + for _, slotName in ipairs(itemSlots) do + UpdateSocketIcons(slotName) + end + end + + -- Hook into equipment changes + local socketWatcher = CreateFrame("Frame") + socketWatcher:RegisterEvent("PLAYER_EQUIPMENT_CHANGED") + socketWatcher:RegisterEvent("PLAYER_ENTERING_WORLD") + socketWatcher:SetScript("OnEvent", function() + if EllesmereUIDB and EllesmereUIDB.themedCharacterSheet then + C_Timer.After(0.1, RefreshAllSocketIcons) + end + end) + + -- Hook frame show/hide + frame:HookScript("OnShow", function() + RefreshAllSocketIcons() + globalSocketContainer:Show() + -- Reset to Stats panel on open + if statsPanel and CharacterFrame._titlesPanel and CharacterFrame._equipPanel then + statsPanel:Show() + CharacterFrame._titlesPanel:Hide() + CharacterFrame._equipPanel:Hide() + end + end) + + frame:HookScript("OnHide", function() + globalSocketContainer:Hide() + end) + + + -- Create reusable tooltip for enchant scanning + local enchantTooltip = CreateFrame("GameTooltip", "EUICharacterSheetEnchantTooltip", nil, "GameTooltipTemplate") + enchantTooltip:SetOwner(UIParent, "ANCHOR_NONE") + + -- Cache item IDs to only update when items change + local itemIdCache = {} + + -- Function to update enchant text and upgrade track for a slot + local function UpdateSlotInfo(slotName) + local slot = _G[slotName] + if not slot then return end + + local itemLink = GetInventoryItemLink("player", slot:GetID()) + local itemLevel = "" + local enchantText = "" + local upgradeTrackText = "" + local upgradeTrackColor = { r = 1, g = 1, b = 1 } + local itemQuality = nil + + if itemLink then + local _, _, quality, ilvl = GetItemInfo(itemLink) + itemLevel = ilvl or "" + itemQuality = quality + + -- Get enchant and upgrade track from tooltip + enchantTooltip:SetInventoryItem("player", slot:GetID()) + for i = 1, enchantTooltip:NumLines() do + local textLeft = _G["EUICharacterSheetEnchantTooltipTextLeft" .. i]:GetText() or "" + + -- Get enchant text + if textLeft:match("Enchanted:") then + enchantText = textLeft:gsub("Enchanted:%s*", "") + enchantText = enchantText:gsub("^Enchant%s+[^-]+%s*-%s*", "") + end + + -- Get upgrade track + if textLeft:match("Upgrade Level:") then + local trackInfo = textLeft:gsub("Upgrade Level:%s*", "") + local trk, nums = trackInfo:match("^(%w+)%s+(.+)$") + + if trk and nums then + -- Map track types to short names and colors + if trk == "Champion" then + upgradeTrackText = "(Champion " .. nums .. ")" + upgradeTrackColor = { r = 0.00, g = 0.44, b = 0.87 } -- blue + elseif trk:match("Myth") then + upgradeTrackText = "(Myth " .. nums .. ")" + upgradeTrackColor = { r = 1.00, g = 0.50, b = 0.00 } -- orange + elseif trk:match("Hero") then + upgradeTrackText = "(Hero " .. nums .. ")" + upgradeTrackColor = { r = 1.00, g = 0.30, b = 1.00 } -- purple + elseif trk:match("Veteran") then + upgradeTrackText = "(Veteran " .. nums .. ")" + upgradeTrackColor = { r = 0.12, g = 1.00, b = 0.00 } -- green + elseif trk:match("Adventurer") then + upgradeTrackText = "(Adventurer " .. nums .. ")" + upgradeTrackColor = { r = 1.00, g = 1.00, b = 1.00 } -- white + elseif trk:match("Delve") or trk:match("Explorer") then + upgradeTrackText = "(" .. trk .. " " .. nums .. ")" + upgradeTrackColor = { r = 0.62, g = 0.62, b = 0.62 } -- gray + end + end + end + end + end + + -- Update itemlevel label with optional rarity color + if slot._itemLevelLabel then + slot._itemLevelLabel:SetText(tostring(itemLevel) or "") + + -- Apply rarity color if enabled + if EllesmereUIDB and EllesmereUIDB.charSheetColorItemLevel and itemQuality then + local r, g, b = GetItemQualityColor(itemQuality) + slot._itemLevelLabel:SetTextColor(r, g, b, 0.9) + else + slot._itemLevelLabel:SetTextColor(1, 1, 1, 0.9) + end + end + + -- Update enchant label + if slot._enchantLabel then + slot._enchantLabel:SetText(enchantText or "") + end + + -- Update upgrade track label + if slot._upgradeTrackLabel then + slot._upgradeTrackLabel:SetText(upgradeTrackText or "") + slot._upgradeTrackLabel:SetTextColor(upgradeTrackColor.r, upgradeTrackColor.g, upgradeTrackColor.b, 0.8) + end + end + + -- Monitor and update only when items change + if not frame._itemLevelMonitor then + frame._itemLevelMonitor = CreateFrame("Frame") + frame._itemLevelMonitor:SetScript("OnUpdate", function() + if not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) then + return + end + if frame and frame:IsShown() then + for _, slotName in ipairs(itemSlots) do + -- Use GetItemInfoInstant to check if item changed without tooltip overhead + local itemLink = GetInventoryItemLink("player", _G[slotName]:GetID()) + local itemId = itemLink and GetItemInfoInstant(itemLink) or nil + + -- Only update if item ID changed + if itemIdCache[slotName] ~= itemId then + itemIdCache[slotName] = itemId + UpdateSlotInfo(slotName) + end + end + end + end) + end +end + +-- Get item rarity color from link +local function GetRarityColorFromLink(itemLink) + if not itemLink then + return 0.9, 0.9, 0.9, 1 -- Default gray + end + + local itemRarity = select(3, GetItemInfo(itemLink)) + if not itemRarity then + return 0.9, 0.9, 0.9, 1 + end + + -- WoW standard rarity colors + local rarityColors = { + [0] = { 0.62, 0.62, 0.62 }, -- Poor + [1] = { 1, 1, 1 }, -- Common + [2] = { 0.12, 1, 0 }, -- Uncommon + [3] = { 0, 0.44, 0.87 }, -- Rare + [4] = { 0.64, 0.21, 0.93 }, -- Epic + [5] = { 1, 0.5, 0 }, -- Legendary + [6] = { 0.9, 0.8, 0.5 }, -- Artifact + [7] = { 0.9, 0.8, 0.5 }, -- Heirloom + } + + local color = rarityColors[itemRarity] or rarityColors[1] + return color[1], color[2], color[3], 1 +end + +-- Style a character slot with rarity-based border +local function SkinCharacterSlot(slotName, slotID) + local slot = _G[slotName] + if not slot or slot._ebsSkinned then return end + slot._ebsSkinned = true + + -- Hide Blizzard IconBorder + if slot.IconBorder then + slot.IconBorder:Hide() + end + + -- Adjust IconTexture + local iconTexture = _G[slotName .. "IconTexture"] + if iconTexture then + iconTexture:SetTexCoord(0.07, 0.07, 0.07, 0.93, 0.93, 0.07, 0.93, 0.93) + end + + -- Test: Hide CharacterHandsSlot completely + if slotName == "CharacterHandsSlot" then + slot:Hide() + end + + -- Hide NormalTexture + local normalTexture = _G[slotName .. "NormalTexture"] + if normalTexture then + normalTexture:Hide() + end + + -- EUI-style background for the slot + local slotBg = slot:CreateTexture(nil, "BACKGROUND", nil, -5) + slotBg:SetAllPoints(slot) + slotBg:SetColorTexture(0.03, 0.045, 0.05, 0.7) -- EUI frame BG with transparency + slot._slotBg = slotBg + + -- Create custom border on the slot using PP.CreateBorder + if EllesmereUI and EllesmereUI.PanelPP then + EllesmereUI.PanelPP.CreateBorder(slot, 1, 1, 1, 0.4, 2, "OVERLAY", 7) + end +end + +-- Main function to apply themed character sheet +local function ApplyThemedCharacterSheet() + if not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) then + return + end + + if CharacterFrame then + SkinCharacterSheet() + end +end + +-- Register the feature +if EllesmereUI then + EllesmereUI.ApplyThemedCharacterSheet = ApplyThemedCharacterSheet + + -- Setup at PLAYER_LOGIN to register drag hooks early + local initFrame = CreateFrame("Frame") + initFrame:RegisterEvent("PLAYER_LOGIN") + initFrame:SetScript("OnEvent", function(self) + self:UnregisterEvent("PLAYER_LOGIN") + if CharacterFrame then + -- Setup drag functionality at login (before first open) + CharacterFrame:SetMovable(true) + CharacterFrame:SetClampedToScreen(true) + local _ebsDragging = false + + CharacterFrame:SetScript("OnMouseDown", function(btn, button) + if button ~= "LeftButton" then return end + if not IsShiftKeyDown() and not IsControlKeyDown() then return end + _ebsDragging = IsShiftKeyDown() and "save" or "temp" + btn:StartMoving() + end) + + CharacterFrame:SetScript("OnMouseUp", function(btn, button) + if button ~= "LeftButton" or not _ebsDragging then return end + btn:StopMovingOrSizing() + _ebsDragging = false + end) + + -- Hook styling on OnShow + CharacterFrame:HookScript("OnShow", ApplyThemedCharacterSheet) + ApplyThemedCharacterSheet() + + -- Function to detect and set active equipment set + local function UpdateActiveEquipmentSet() + local setIDs = C_EquipmentSet.GetEquipmentSetIDs() + if setIDs then + for _, setID in ipairs(setIDs) do + local setItems = GetEquipmentSetItemIDs(setID) + if setItems then + local allMatch = true + for slotIndex, itemID in pairs(setItems) do + if itemID ~= 0 then + local currentItemID = GetInventoryItemID("player", slotIndex) + if currentItemID ~= itemID then + allMatch = false + break + end + end + end + if allMatch then + activeEquipmentSetID = setID + return + end + end + end + end + activeEquipmentSetID = nil + end + + -- Auto-equip equipment set when spec changes + local specChangeFrame = CreateFrame("Frame") + specChangeFrame:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED") + specChangeFrame:RegisterEvent("EQUIPMENT_SETS_CHANGED") + specChangeFrame:SetScript("OnEvent", function(self, event) + if event == "EQUIPMENT_SETS_CHANGED" then + -- Update active set when equipment changes + -- UpdateActiveEquipmentSet() -- API no longer available in current WoW version + -- RefreshEquipmentSets() -- Function not in scope here + if CharacterFrame and CharacterFrame:IsShown() and CharacterFrame._equipPanel and CharacterFrame._equipPanel:IsShown() then + -- Equipment panel will be refreshed by the equipSetChangeFrame handler + end + else + -- Auto-equip when spec changes + local setIDs = C_EquipmentSet.GetEquipmentSetIDs() + if setIDs then + for _, setID in ipairs(setIDs) do + local assignedSpec = C_EquipmentSet.GetEquipmentSetAssignedSpec(setID) + if assignedSpec then + local currentSpecIndex = GetSpecialization() + if assignedSpec == currentSpecIndex then + C_EquipmentSet.UseEquipmentSet(setID) + activeEquipmentSetID = setID + if EllesmereUIDB then + EllesmereUIDB.lastEquippedSet = setID + end + break + end + end + end + end + end + end) + + -- Initialize active set on login + local loginFrame = CreateFrame("Frame") + loginFrame:RegisterEvent("PLAYER_LOGIN") + loginFrame:SetScript("OnEvent", function() + loginFrame:UnregisterEvent("PLAYER_LOGIN") + -- Restore last equipped set if available + if EllesmereUIDB and EllesmereUIDB.lastEquippedSet then + activeEquipmentSetID = EllesmereUIDB.lastEquippedSet + end + end) + end + end) +end + +-- Function to apply character sheet text size settings +function EllesmereUI._applyCharSheetTextSizes() + if not CharacterFrame then return end + + local itemLevelSize = EllesmereUIDB and EllesmereUIDB.charSheetItemLevelSize or 11 + local upgradeTrackSize = EllesmereUIDB and EllesmereUIDB.charSheetUpgradeTrackSize or 11 + local enchantSize = EllesmereUIDB and EllesmereUIDB.charSheetEnchantSize or 9 + local fontPath = EllesmereUI.GetFontPath and EllesmereUI.GetFontPath() or STANDARD_TEXT_FONT + + -- Update all slot labels + local itemSlots = { + "CharacterHeadSlot", "CharacterNeckSlot", "CharacterShoulderSlot", "CharacterBackSlot", + "CharacterChestSlot", "CharacterWaistSlot", "CharacterLegsSlot", "CharacterFeetSlot", + "CharacterWristSlot", "CharacterHandsSlot", "CharacterFinger0Slot", "CharacterFinger1Slot", + "CharacterTrinket0Slot", "CharacterTrinket1Slot", "CharacterMainHandSlot", "CharacterSecondaryHandSlot" + } + + for _, slotName in ipairs(itemSlots) do + local slot = _G[slotName] + if slot then + if slot._itemLevelLabel then + slot._itemLevelLabel:SetFont(fontPath, itemLevelSize, "") + end + if slot._upgradeTrackLabel then + slot._upgradeTrackLabel:SetFont(fontPath, upgradeTrackSize, "") + end + if slot._enchantLabel then + slot._enchantLabel:SetFont(fontPath, enchantSize, "") + end + end + end +end + +-- Function to recolor item level labels based on rarity setting +function EllesmereUI._applyCharSheetItemColors() + if not CharacterFrame then return end + + local itemSlots = { + "CharacterHeadSlot", "CharacterNeckSlot", "CharacterShoulderSlot", "CharacterBackSlot", + "CharacterChestSlot", "CharacterWaistSlot", "CharacterLegsSlot", "CharacterFeetSlot", + "CharacterWristSlot", "CharacterHandsSlot", "CharacterFinger0Slot", "CharacterFinger1Slot", + "CharacterTrinket0Slot", "CharacterTrinket1Slot", "CharacterMainHandSlot", "CharacterSecondaryHandSlot" + } + + for _, slotName in ipairs(itemSlots) do + local slot = _G[slotName] + if slot and slot._itemLevelLabel then + local itemLink = GetInventoryItemLink("player", slot:GetID()) + if itemLink then + local _, _, quality = GetItemInfo(itemLink) + if EllesmereUIDB and EllesmereUIDB.charSheetColorItemLevel and quality then + local r, g, b = GetItemQualityColor(quality) + slot._itemLevelLabel:SetTextColor(r, g, b, 0.9) + else + slot._itemLevelLabel:SetTextColor(1, 1, 1, 0.9) + end + else + slot._itemLevelLabel:SetTextColor(1, 1, 1, 0.9) + end + end + end +end 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 02/16] 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 0551a6fd..28a11402 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 00000000..36c7dae0 --- /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 00000000..0e154253 --- /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 03/16] 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 28a11402..8e3cea14 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 04/16] 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 0e154253..ed88d0d6 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 05/16] 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 ed88d0d6..f8a64d86 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 06/16] 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 36c7dae0..02013c3d 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 87fb6b8a73d6478f8ce7d01d7757c169b6ee0020 Mon Sep 17 00:00:00 2001 From: Daniel <14241290+dnlxh@users.noreply.github.com> Date: Sat, 11 Apr 2026 03:31:25 +0200 Subject: [PATCH 07/16] Add themed character sheet with custom layout, stats panel, titles, and equipment manager Equipment panel improvements: - Add New Set, Equip, and Save buttons with pixel-perfect borders - Implement pixel-perfect borders using EllesmereUI.PanelPP system - Add hover effects with turquoise color transitions on all buttons - Display "Equipped" message on Equip button click - Add delete button (X) to remove equipment sets - Auto-equip sets when spec changes - Track and restore last equipped set on login Character sheet UI refinements: - Implement pixel-perfect borders for equipment buttons - Add font customization options (shadow and outline) for ItemLevel, UpgradeTrack, and Enchant text - Make character model visible with proper FrameLevel layering Visual consistency: - Change item slot borders from yellow/default to dark gray (0.4, 0.4, 0.4) - Update empty slot backgrounds to gray - Match button text colors to UI theme (white with turquoise hover effect) - Implement darker turquoise hover border colors Dynamic updates: - Add UNIT_INVENTORY_CHANGED event listener for real-time border color updates - Borders now reflect item rarity colors when equipped UI cleanup: - Remove spacing gaps in Character Panel Customizations menu - Ensure text labels appear above character model --- EUI__General_Options.lua | 226 ++++++++++++++++--- charactersheet.lua | 470 +++++++++++++++++++++++++++++---------- 2 files changed, 539 insertions(+), 157 deletions(-) diff --git a/EUI__General_Options.lua b/EUI__General_Options.lua index 24e56715..079de423 100644 --- a/EUI__General_Options.lua +++ b/EUI__General_Options.lua @@ -3588,7 +3588,6 @@ initFrame:SetScript("OnEvent", function(self) local function themedOff() return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) end - local colorItemBlock = CreateFrame("Frame", nil, colorItemLevelRow) colorItemBlock:SetAllPoints(colorItemLevelRow) colorItemBlock:SetFrameLevel(colorItemLevelRow:GetFrameLevel() + 10) @@ -3599,15 +3598,8 @@ initFrame:SetScript("OnEvent", function(self) EllesmereUI.ShowWidgetTooltip(colorItemBlock, EllesmereUI.DisabledTooltip("Themed Character Sheet")) end) colorItemBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) - EllesmereUI.RegisterWidgetRefresh(function() - if themedOff() then - colorItemBlock:Show() - colorItemLevelRow:SetAlpha(0.3) - else - colorItemBlock:Hide() - colorItemLevelRow:SetAlpha(1) - end + if themedOff() then colorItemBlock:Show() colorItemLevelRow:SetAlpha(0.3) else colorItemBlock:Hide() colorItemLevelRow:SetAlpha(1) end end) if themedOff() then colorItemBlock:Show() colorItemLevelRow:SetAlpha(0.3) else colorItemBlock:Hide() colorItemLevelRow:SetAlpha(1) end end @@ -3627,6 +3619,76 @@ initFrame:SetScript("OnEvent", function(self) EllesmereUI._applyCharSheetTextSizes() end end }, + { type="label", text="" } + ); y = y - h + + -- Cogwheel for item level text effects (shadow and outline) + do + local function themedOff() + return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) + end + local leftRgn = itemLevelRow._leftRegion + + local _, itemLevelCogShow = EllesmereUI.BuildCogPopup({ + title = "Item Level Text Effects", + rows = { + { type="toggle", label="Font Shadow", + get=function() return EllesmereUIDB and EllesmereUIDB.charSheetItemLevelShadow or false end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetItemLevelShadow = v + if EllesmereUI._applyCharSheetTextSizes then + EllesmereUI._applyCharSheetTextSizes() + end + end }, + { type="toggle", label="Font Outline", + get=function() return EllesmereUIDB and EllesmereUIDB.charSheetItemLevelOutline or false end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetItemLevelOutline = v + if EllesmereUI._applyCharSheetTextSizes then + EllesmereUI._applyCharSheetTextSizes() + end + end }, + }, + }) + + local itemLevelCogBtn = CreateFrame("Button", nil, leftRgn) + itemLevelCogBtn:SetSize(26, 26) + itemLevelCogBtn:SetPoint("RIGHT", leftRgn._lastInline or leftRgn._control, "LEFT", -9, 0) + leftRgn._lastInline = itemLevelCogBtn + itemLevelCogBtn:SetFrameLevel(leftRgn:GetFrameLevel() + 5) + itemLevelCogBtn:SetAlpha(themedOff() and 0.15 or 0.4) + local itemLevelCogTex = itemLevelCogBtn:CreateTexture(nil, "OVERLAY") + itemLevelCogTex:SetAllPoints() + itemLevelCogTex:SetTexture(EllesmereUI.COGS_ICON) + itemLevelCogBtn:SetScript("OnEnter", function(self) self:SetAlpha(0.7) end) + itemLevelCogBtn:SetScript("OnLeave", function(self) self:SetAlpha(themedOff() and 0.15 or 0.4) end) + itemLevelCogBtn:SetScript("OnClick", function(self) itemLevelCogShow(self) end) + + local itemLevelCogBlock = CreateFrame("Frame", nil, itemLevelCogBtn) + itemLevelCogBlock:SetAllPoints() + itemLevelCogBlock:SetFrameLevel(itemLevelCogBtn:GetFrameLevel() + 10) + itemLevelCogBlock:EnableMouse(true) + itemLevelCogBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(itemLevelCogBtn, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + end) + itemLevelCogBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + EllesmereUI.RegisterWidgetRefresh(function() + if themedOff() then + itemLevelCogBtn:SetAlpha(0.15) + itemLevelCogBlock:Show() + else + itemLevelCogBtn:SetAlpha(0.4) + itemLevelCogBlock:Hide() + end + end) + if themedOff() then itemLevelCogBtn:SetAlpha(0.15) itemLevelCogBlock:Show() else itemLevelCogBtn:SetAlpha(0.4) itemLevelCogBlock:Hide() end + end + + local upgradeTrackRow + upgradeTrackRow, h = W:DualRow(parent, y, { type="slider", text="Upgrade Track Font Size", min=8, max=16, step=1, tooltip="Adjusts the font size for upgrade track text on the character sheet.", @@ -3639,36 +3701,73 @@ initFrame:SetScript("OnEvent", function(self) if EllesmereUI._applyCharSheetTextSizes then EllesmereUI._applyCharSheetTextSizes() end - end } + end }, + { type="label", text="" } ); y = y - h - -- Disabled overlay for font size row when themed is off + -- Cogwheel for upgrade track text effects (shadow and outline) do local function themedOff() return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) end + local leftRgn = upgradeTrackRow._leftRegion - local fontBlock = CreateFrame("Frame", nil, itemLevelRow) - fontBlock:SetAllPoints(itemLevelRow) - fontBlock:SetFrameLevel(itemLevelRow:GetFrameLevel() + 10) - fontBlock:EnableMouse(true) - local fontBg = EllesmereUI.SolidTex(fontBlock, "BACKGROUND", 0, 0, 0, 0) - fontBg:SetAllPoints() - fontBlock:SetScript("OnEnter", function() - EllesmereUI.ShowWidgetTooltip(fontBlock, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + local _, upgradeTrackCogShow = EllesmereUI.BuildCogPopup({ + title = "Upgrade Track Text Effects", + rows = { + { type="toggle", label="Font Shadow", + get=function() return EllesmereUIDB and EllesmereUIDB.charSheetUpgradeTrackShadow or false end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetUpgradeTrackShadow = v + if EllesmereUI._applyCharSheetTextSizes then + EllesmereUI._applyCharSheetTextSizes() + end + end }, + { type="toggle", label="Font Outline", + get=function() return EllesmereUIDB and EllesmereUIDB.charSheetUpgradeTrackOutline or false end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetUpgradeTrackOutline = v + if EllesmereUI._applyCharSheetTextSizes then + EllesmereUI._applyCharSheetTextSizes() + end + end }, + }, + }) + + local upgradeTrackCogBtn = CreateFrame("Button", nil, leftRgn) + upgradeTrackCogBtn:SetSize(26, 26) + upgradeTrackCogBtn:SetPoint("RIGHT", leftRgn._lastInline or leftRgn._control, "LEFT", -9, 0) + leftRgn._lastInline = upgradeTrackCogBtn + upgradeTrackCogBtn:SetFrameLevel(leftRgn:GetFrameLevel() + 5) + upgradeTrackCogBtn:SetAlpha(themedOff() and 0.15 or 0.4) + local upgradeTrackCogTex = upgradeTrackCogBtn:CreateTexture(nil, "OVERLAY") + upgradeTrackCogTex:SetAllPoints() + upgradeTrackCogTex:SetTexture(EllesmereUI.COGS_ICON) + upgradeTrackCogBtn:SetScript("OnEnter", function(self) self:SetAlpha(0.7) end) + upgradeTrackCogBtn:SetScript("OnLeave", function(self) self:SetAlpha(themedOff() and 0.15 or 0.4) end) + upgradeTrackCogBtn:SetScript("OnClick", function(self) upgradeTrackCogShow(self) end) + + local upgradeTrackCogBlock = CreateFrame("Frame", nil, upgradeTrackCogBtn) + upgradeTrackCogBlock:SetAllPoints() + upgradeTrackCogBlock:SetFrameLevel(upgradeTrackCogBtn:GetFrameLevel() + 10) + upgradeTrackCogBlock:EnableMouse(true) + upgradeTrackCogBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(upgradeTrackCogBtn, EllesmereUI.DisabledTooltip("Themed Character Sheet")) end) - fontBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + upgradeTrackCogBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) EllesmereUI.RegisterWidgetRefresh(function() if themedOff() then - fontBlock:Show() - itemLevelRow:SetAlpha(0.3) + upgradeTrackCogBtn:SetAlpha(0.15) + upgradeTrackCogBlock:Show() else - fontBlock:Hide() - itemLevelRow:SetAlpha(1) + upgradeTrackCogBtn:SetAlpha(0.4) + upgradeTrackCogBlock:Hide() end end) - if themedOff() then fontBlock:Show() itemLevelRow:SetAlpha(0.3) else fontBlock:Hide() itemLevelRow:SetAlpha(1) end + if themedOff() then upgradeTrackCogBtn:SetAlpha(0.15) upgradeTrackCogBlock:Show() else upgradeTrackCogBtn:SetAlpha(0.4) upgradeTrackCogBlock:Hide() end end local enchantRow @@ -3689,12 +3788,76 @@ initFrame:SetScript("OnEvent", function(self) { type="label", text="" } ); y = y - h - -- Disabled overlay for enchant row when themed is off + -- Cogwheel for enchant text effects (shadow and outline) do local function themedOff() return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) end + local leftRgn = enchantRow._leftRegion + local _, enchantCogShow = EllesmereUI.BuildCogPopup({ + title = "Enchant Text Effects", + rows = { + { type="toggle", label="Font Shadow", + get=function() return EllesmereUIDB and EllesmereUIDB.charSheetEnchantShadow or false end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetEnchantShadow = v + if EllesmereUI._applyCharSheetTextSizes then + EllesmereUI._applyCharSheetTextSizes() + end + end }, + { type="toggle", label="Font Outline", + get=function() return EllesmereUIDB and EllesmereUIDB.charSheetEnchantOutline or false end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetEnchantOutline = v + if EllesmereUI._applyCharSheetTextSizes then + EllesmereUI._applyCharSheetTextSizes() + end + end }, + }, + }) + + local enchantCogBtn = CreateFrame("Button", nil, leftRgn) + enchantCogBtn:SetSize(26, 26) + enchantCogBtn:SetPoint("RIGHT", leftRgn._lastInline or leftRgn._control, "LEFT", -9, 0) + leftRgn._lastInline = enchantCogBtn + enchantCogBtn:SetFrameLevel(leftRgn:GetFrameLevel() + 5) + enchantCogBtn:SetAlpha(themedOff() and 0.15 or 0.4) + local enchantCogTex = enchantCogBtn:CreateTexture(nil, "OVERLAY") + enchantCogTex:SetAllPoints() + enchantCogTex:SetTexture(EllesmereUI.COGS_ICON) + enchantCogBtn:SetScript("OnEnter", function(self) self:SetAlpha(0.7) end) + enchantCogBtn:SetScript("OnLeave", function(self) self:SetAlpha(themedOff() and 0.15 or 0.4) end) + enchantCogBtn:SetScript("OnClick", function(self) enchantCogShow(self) end) + + local enchantCogBlock = CreateFrame("Frame", nil, enchantCogBtn) + enchantCogBlock:SetAllPoints() + enchantCogBlock:SetFrameLevel(enchantCogBtn:GetFrameLevel() + 10) + enchantCogBlock:EnableMouse(true) + enchantCogBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(enchantCogBtn, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + end) + enchantCogBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + EllesmereUI.RegisterWidgetRefresh(function() + if themedOff() then + enchantCogBtn:SetAlpha(0.15) + enchantCogBlock:Show() + else + enchantCogBtn:SetAlpha(0.4) + enchantCogBlock:Hide() + end + end) + if themedOff() then enchantCogBtn:SetAlpha(0.15) enchantCogBlock:Show() else enchantCogBtn:SetAlpha(0.4) enchantCogBlock:Hide() end + end + + -- Disabled overlay for enchant row when themed is off + do + local function themedOff() + return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) + end local enchantBlock = CreateFrame("Frame", nil, enchantRow) enchantBlock:SetAllPoints(enchantRow) enchantBlock:SetFrameLevel(enchantRow:GetFrameLevel() + 10) @@ -3705,21 +3868,14 @@ initFrame:SetScript("OnEvent", function(self) EllesmereUI.ShowWidgetTooltip(enchantBlock, EllesmereUI.DisabledTooltip("Themed Character Sheet")) end) enchantBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) - EllesmereUI.RegisterWidgetRefresh(function() - if themedOff() then - enchantBlock:Show() - enchantRow:SetAlpha(0.3) - else - enchantBlock:Hide() - enchantRow:SetAlpha(1) - end + if themedOff() then enchantBlock:Show() enchantRow:SetAlpha(0.3) else enchantBlock:Hide() enchantRow:SetAlpha(1) end end) if themedOff() then enchantBlock:Show() enchantRow:SetAlpha(0.3) else enchantBlock:Hide() enchantRow:SetAlpha(1) end end -- Stat Category Toggles - _, h = W:Spacer(parent, y, 10); y = y - h + _, h = W:Spacer(parent, y, 5); y = y - h local categoryRow1, h1 = W:DualRow(parent, y, { type="toggle", text="Show Attributes", diff --git a/charactersheet.lua b/charactersheet.lua index 8a30e9db..efb4ea42 100644 --- a/charactersheet.lua +++ b/charactersheet.lua @@ -56,12 +56,33 @@ local function SkinCharacterSheet() CharacterModelScene:Show() CharacterModelScene:ClearAllPoints() CharacterModelScene:SetPoint("TOPLEFT", frame, "TOPLEFT", 110, -100) - CharacterModelScene:SetFrameLevel(1) -- Keep model behind text + CharacterModelScene:SetFrameLevel(2) -- Keep model behind text -- Hide control frame (zoom, rotation buttons) if CharacterModelScene.ControlFrame then CharacterModelScene.ControlFrame:Hide() end + + -- Create update loop to keep all control buttons hidden + local hideControlButtons = CreateFrame("Frame") + hideControlButtons:SetScript("OnUpdate", function() + if CharacterModelScene.ControlFrame then + -- Hide zoom buttons + if CharacterModelScene.ControlFrame.zoomInButton and CharacterModelScene.ControlFrame.zoomInButton:IsShown() then + CharacterModelScene.ControlFrame.zoomInButton:Hide() + end + if CharacterModelScene.ControlFrame.zoomOutButton and CharacterModelScene.ControlFrame.zoomOutButton:IsShown() then + CharacterModelScene.ControlFrame.zoomOutButton:Hide() + end + -- Hide all other buttons in ControlFrame (rotation buttons, etc) + for i = 1, CharacterModelScene.ControlFrame:GetNumChildren() do + local child = select(i, CharacterModelScene.ControlFrame:GetChildren()) + if child and child:IsShown() then + child:Hide() + end + end + end + end) end -- Center the level text under character name @@ -1264,7 +1285,7 @@ local function SkinCharacterSheet() -- Get item rarity color for border local itemLink = GetInventoryItemLink("player", slot:GetID()) - local borderR, borderG, borderB = 1, 1, 0 -- Default yellow + local borderR, borderG, borderB = 0.4, 0.4, 0.4 -- Default dark gray if itemLink then local _, _, rarity = GetItemInfo(itemLink) if rarity then @@ -1591,142 +1612,233 @@ local function SkinCharacterSheet() -- Track selected equipment set local selectedSetID = nil - -- Function to reload equipment sets - local function RefreshEquipmentSets() - -- Clear old buttons - for _, child in ipairs({equipScrollChild:GetChildren()}) do - child:Hide() - end + -- Forward declare the refresh function (will be defined after buttons) + local RefreshEquipmentSets - local equipmentSets = {} - if C_EquipmentSet then - local setIDs = C_EquipmentSet.GetEquipmentSetIDs() - if setIDs then - for _, setID in ipairs(setIDs) do - local setName = C_EquipmentSet.GetEquipmentSetInfo(setID) - if setName and setName ~= "" then - table.insert(equipmentSets, {id = setID, name = setName}) - end + -- Create "New Set" button once (outside refresh to avoid recreation) + local newSetBtn = CreateFrame("Button", nil, equipScrollChild) + newSetBtn:SetWidth(65) + newSetBtn:SetHeight(24) + newSetBtn:SetPoint("TOPLEFT", equipScrollChild, "TOPLEFT", 0, -5) + + local newSetBg = newSetBtn:CreateTexture(nil, "BACKGROUND") + newSetBg:SetColorTexture(0.05, 0.07, 0.08, 1) + newSetBg:SetAllPoints() + + -- Create border using pixelperfect + if EllesmereUI and EllesmereUI.PanelPP then + EllesmereUI.PanelPP.CreateBorder(newSetBtn, 0.8, 0.8, 0.8, 1, 1, "OVERLAY", 1) + end + + local newSetText = newSetBtn:CreateFontString(nil, "OVERLAY") + newSetText:SetFont(fontPath, 10, "") + newSetText:SetText("New Set") + newSetText:SetTextColor(1, 1, 1, 0.7) + newSetText:SetPoint("CENTER", newSetBtn, "CENTER", 0, 0) + + newSetBtn:SetScript("OnClick", function() + StaticPopupDialogs["EUI_NEW_EQUIPMENT_SET"] = { + text = "New equipment set name:", + button1 = "Create", + button2 = "Cancel", + OnAccept = function(dialog) + local newName = dialog.EditBox:GetText() + if newName ~= "" then + C_EquipmentSet.CreateEquipmentSet(newName) + RefreshEquipmentSets() end - end + end, + hasEditBox = true, + editBoxWidth = 350, + timeout = 0, + whileDead = false, + hideOnEscape = true, + } + StaticPopup_Show("EUI_NEW_EQUIPMENT_SET") + end) + + newSetBtn:SetScript("OnEnter", function() + newSetBg:SetColorTexture(0.047, 0.824, 0.616, 0.2) + newSetText:SetTextColor(0.15, 1, 0.8, 1) + if EllesmereUI and EllesmereUI.PanelPP then + EllesmereUI.PanelPP.SetBorderColor(newSetBtn, 0.03, 0.6, 0.45, 1) + end + end) + + newSetBtn:SetScript("OnLeave", function() + newSetBg:SetColorTexture(0.05, 0.07, 0.08, 1) + newSetText:SetTextColor(1, 1, 1, 0.7) + if EllesmereUI and EllesmereUI.PanelPP then + EllesmereUI.PanelPP.SetBorderColor(newSetBtn, 0.8, 0.8, 0.8, 1) end + end) - -- "New Set" button at top - local newSetBtn = CreateFrame("Button", nil, equipScrollChild) - newSetBtn:SetWidth(200) - newSetBtn:SetHeight(24) - newSetBtn:SetPoint("TOPLEFT", equipScrollChild, "TOPLEFT", 0, 0) - - local newSetBg = newSetBtn:CreateTexture(nil, "BACKGROUND") - newSetBg:SetColorTexture(0.047, 0.824, 0.616, 0.3) - newSetBg:SetAllPoints() - - local newSetText = newSetBtn:CreateFontString(nil, "OVERLAY") - newSetText:SetFont(fontPath, 11, "") - newSetText:SetText("+ New Set") - newSetText:SetTextColor(1, 1, 1, 1) - newSetText:SetPoint("CENTER", newSetBtn, "CENTER", 0, 0) - - newSetBtn:SetScript("OnClick", function() - StaticPopupDialogs["EUI_NEW_EQUIPMENT_SET"] = { - text = "New equipment set name:", - button1 = "Create", - button2 = "Cancel", - OnAccept = function(dialog) - local newName = dialog.EditBox:GetText() - if newName ~= "" then - C_EquipmentSet.CreateEquipmentSet(newName) - RefreshEquipmentSets() - end - end, - hasEditBox = true, - editBoxWidth = 350, - timeout = 0, - whileDead = false, - hideOnEscape = true, - } - StaticPopup_Show("EUI_NEW_EQUIPMENT_SET") - end) + -- Create Equip button once (outside refresh to preserve animation closure) + local equipTopBtn = CreateFrame("Button", nil, equipScrollChild) + equipTopBtn:SetWidth(60) + equipTopBtn:SetHeight(24) + equipTopBtn:SetPoint("TOPLEFT", equipScrollChild, "TOPLEFT", 67, -5) - newSetBtn:SetScript("OnEnter", function() - newSetBg:SetColorTexture(0.047, 0.824, 0.616, 0.5) - end) + local equipTopBg = equipTopBtn:CreateTexture(nil, "BACKGROUND") + equipTopBg:SetColorTexture(0.05, 0.07, 0.08, 1) + equipTopBg:SetAllPoints() - newSetBtn:SetScript("OnLeave", function() - newSetBg:SetColorTexture(0.047, 0.824, 0.616, 0.3) - end) + -- Create border using pixelperfect + if EllesmereUI and EllesmereUI.PanelPP then + EllesmereUI.PanelPP.CreateBorder(equipTopBtn, 0.8, 0.8, 0.8, 1, 1, "OVERLAY", 1) + end - -- Equip and Save buttons at top - local equipTopBtn = CreateFrame("Button", nil, equipScrollChild) - equipTopBtn:SetWidth(95) - equipTopBtn:SetHeight(24) - equipTopBtn:SetPoint("TOPLEFT", equipScrollChild, "TOPLEFT", 0, -44) + local equipTopText = equipTopBtn:CreateFontString(nil, "OVERLAY") + equipTopText:SetFont(fontPath, 10, "") + equipTopText:SetText("Equip") + equipTopText:SetTextColor(1, 1, 1, 0.7) + equipTopText:SetPoint("CENTER", equipTopBtn, "CENTER", 0, 0) - local equipTopBg = equipTopBtn:CreateTexture(nil, "BACKGROUND") - equipTopBg:SetColorTexture(0.05, 0.07, 0.08, 1) - equipTopBg:SetAllPoints() + equipTopBtn:SetScript("OnEnter", function() + equipTopBg:SetColorTexture(0.047, 0.824, 0.616, 0.2) + equipTopText:SetTextColor(0.15, 1, 0.8, 1) + if EllesmereUI and EllesmereUI.PanelPP then + EllesmereUI.PanelPP.SetBorderColor(equipTopBtn, 0.03, 0.6, 0.45, 1) + end + end) + equipTopBtn:SetScript("OnLeave", function() + equipTopBg:SetColorTexture(0.05, 0.07, 0.08, 1) + equipTopText:SetTextColor(1, 1, 1, 0.7) if EllesmereUI and EllesmereUI.PanelPP then - EllesmereUI.PanelPP.CreateBorder(equipTopBtn, 0.8, 0.8, 0.8, 1, 1, "OVERLAY", 1) - end - - local equipTopText = equipTopBtn:CreateFontString(nil, "OVERLAY") - equipTopText:SetFont(fontPath, 10, "") - equipTopText:SetText("Equip") - equipTopText:SetTextColor(1, 1, 1, 1) - equipTopText:SetPoint("CENTER", equipTopBtn, "CENTER", 0, 0) - - equipTopBtn:SetScript("OnClick", function() - if selectedSetID then - C_EquipmentSet.UseEquipmentSet(selectedSetID) - activeEquipmentSetID = selectedSetID - -- Save to DB for persistence - if EllesmereUIDB then - EllesmereUIDB.lastEquippedSet = selectedSetID - end - RefreshEquipmentSets() + EllesmereUI.PanelPP.SetBorderColor(equipTopBtn, 0.8, 0.8, 0.8, 1) + end + end) + + equipTopBtn:SetScript("OnClick", function() + -- Visual feedback: change text to "Equipped!" and color it green + equipTopText:SetText("Equipped!") + equipTopText:SetTextColor(0.047, 0.824, 0.616, 1) -- Green + + if selectedSetID then + C_EquipmentSet.UseEquipmentSet(selectedSetID) + activeEquipmentSetID = selectedSetID + -- Save to DB for persistence + if EllesmereUIDB then + EllesmereUIDB.lastEquippedSet = selectedSetID + end + RefreshEquipmentSets() + end + + -- Change back to "Equip" after 1 second + C_Timer.After(1, function() + if equipTopText then + equipTopText:SetText("Equip") + equipTopText:SetTextColor(1, 1, 1, 0.7) -- Zurück zu Standard end end) + end) - -- Save button at top - local saveTopBtn = CreateFrame("Button", nil, equipScrollChild) - saveTopBtn:SetWidth(95) - saveTopBtn:SetHeight(24) - saveTopBtn:SetPoint("TOPLEFT", equipScrollChild, "TOPLEFT", 105, -44) + -- Create Save button once (outside refresh to preserve animation closure) + local saveTopBtn = CreateFrame("Button", nil, equipScrollChild) + saveTopBtn:SetWidth(71) + saveTopBtn:SetHeight(24) + saveTopBtn:SetPoint("TOPLEFT", equipScrollChild, "TOPLEFT", 129, -5) - local saveTopBg = saveTopBtn:CreateTexture(nil, "BACKGROUND") - saveTopBg:SetColorTexture(0.05, 0.07, 0.08, 1) - saveTopBg:SetAllPoints() + local saveTopBg = saveTopBtn:CreateTexture(nil, "BACKGROUND") + saveTopBg:SetColorTexture(0.05, 0.07, 0.08, 1) + saveTopBg:SetAllPoints() + + -- Create border using pixelperfect + if EllesmereUI and EllesmereUI.PanelPP then + EllesmereUI.PanelPP.CreateBorder(saveTopBtn, 0.8, 0.8, 0.8, 1, 1, "OVERLAY", 1) + end + local saveTopText = saveTopBtn:CreateFontString(nil, "OVERLAY") + saveTopText:SetFont(fontPath, 10, "") + saveTopText:SetText("Save") + saveTopText:SetTextColor(1, 1, 1, 0.7) + saveTopText:SetPoint("CENTER", saveTopBtn, "CENTER", 0, 0) + + saveTopBtn:SetScript("OnEnter", function() + saveTopBg:SetColorTexture(0.047, 0.824, 0.616, 0.2) + saveTopText:SetTextColor(0.15, 1, 0.8, 1) + if EllesmereUI and EllesmereUI.PanelPP then + EllesmereUI.PanelPP.SetBorderColor(saveTopBtn, 0.03, 0.6, 0.45, 1) + end + end) + + saveTopBtn:SetScript("OnLeave", function() + saveTopBg:SetColorTexture(0.05, 0.07, 0.08, 1) + saveTopText:SetTextColor(1, 1, 1, 0.7) if EllesmereUI and EllesmereUI.PanelPP then - EllesmereUI.PanelPP.CreateBorder(saveTopBtn, 0.8, 0.8, 0.8, 1, 1, "OVERLAY", 1) + EllesmereUI.PanelPP.SetBorderColor(saveTopBtn, 0.8, 0.8, 0.8, 1) + end + end) + + saveTopBtn:SetScript("OnClick", function() + -- Visual feedback: change text to "Saved!" and color it green + saveTopText:SetText("Saved!") + saveTopText:SetTextColor(0.047, 0.824, 0.616, 1) -- Green + + if selectedSetID then + C_EquipmentSet.SaveEquipmentSet(selectedSetID) end - local saveTopText = saveTopBtn:CreateFontString(nil, "OVERLAY") - saveTopText:SetFont(fontPath, 10, "") - saveTopText:SetText("Save") - saveTopText:SetTextColor(1, 1, 1, 1) - saveTopText:SetPoint("CENTER", saveTopBtn, "CENTER", 0, 0) + -- Change back to "Save" after 1 second + C_Timer.After(1, function() + if saveTopText then + saveTopText:SetText("Save") + saveTopText:SetTextColor(1, 1, 1, 0.7) -- Zurück zu Standard + end + end) + end) - saveTopBtn:SetScript("OnClick", function() - -- Visual feedback: change text to "Saved!" and color it green - saveTopText:SetText("Saved!") - saveTopText:SetTextColor(0.047, 0.824, 0.616, 1) -- Green + -- Create "Sets" section header + local setsHeaderFrame = CreateFrame("Frame", nil, equipScrollChild) + setsHeaderFrame:SetWidth(200) + setsHeaderFrame:SetHeight(15) + setsHeaderFrame:SetPoint("TOPLEFT", equipScrollChild, "TOPLEFT", 0, -27) + + -- Left line + local leftLine = setsHeaderFrame:CreateTexture(nil, "BACKGROUND") + leftLine:SetColorTexture(0.047, 0.824, 0.616, 1) + leftLine:SetPoint("LEFT", setsHeaderFrame, "LEFT", 0, -14) + leftLine:SetPoint("RIGHT", setsHeaderFrame, "CENTER", -25, -14) + leftLine:SetHeight(2) + + -- Text + local setsHeaderText = setsHeaderFrame:CreateFontString(nil, "OVERLAY") + setsHeaderText:SetFont(fontPath, 13, "") + setsHeaderText:SetText("Sets") + setsHeaderText:SetTextColor(0.047, 0.824, 0.616, 1) + setsHeaderText:SetPoint("CENTER", setsHeaderFrame, "CENTER", 0, -14) + + -- Right line + local rightLine = setsHeaderFrame:CreateTexture(nil, "BACKGROUND") + rightLine:SetColorTexture(0.047, 0.824, 0.616, 1) + rightLine:SetPoint("LEFT", setsHeaderFrame, "CENTER", 25, -14) + rightLine:SetPoint("RIGHT", setsHeaderFrame, "RIGHT", 0, -14) + rightLine:SetHeight(2) - if selectedSetID then - C_EquipmentSet.SaveEquipmentSet(selectedSetID) + -- Function to reload equipment sets + RefreshEquipmentSets = function() + -- Clear old set buttons (but keep the new set, equip, save buttons, and header) + for _, child in ipairs({equipScrollChild:GetChildren()}) do + if child ~= newSetBtn and child ~= equipTopBtn and child ~= saveTopBtn and child ~= setsHeaderFrame then + child:Hide() end + end - -- Change back to "Save" after 1 second - C_Timer.After(1, function() - if saveTopText then - saveTopText:SetText("Save") - saveTopText:SetTextColor(1, 1, 1, 1) -- White + local equipmentSets = {} + if C_EquipmentSet then + local setIDs = C_EquipmentSet.GetEquipmentSetIDs() + if setIDs then + for _, setID in ipairs(setIDs) do + local setName = C_EquipmentSet.GetEquipmentSetInfo(setID) + if setName and setName ~= "" then + table.insert(equipmentSets, {id = setID, name = setName}) + end end - end) - end) + end + end - local yOffset = -88 -- After buttons + local yOffset = -59 -- After buttons and header for _, setData in ipairs(equipmentSets) do local setBtn = CreateFrame("Button", nil, equipScrollChild) setBtn:SetWidth(200) @@ -1757,7 +1869,7 @@ local function SkinCharacterSheet() local specIconTexture = setBtn:CreateTexture(nil, "OVERLAY") specIconTexture:SetTexture(specIcon) specIconTexture:SetSize(16, 16) - specIconTexture:SetPoint("RIGHT", setBtn, "RIGHT", -25, 0) + specIconTexture:SetPoint("RIGHT", setBtn, "RIGHT", -45, 0) end end @@ -1914,6 +2026,43 @@ local function SkinCharacterSheet() cogBtn.menuBackdrop:Show() end) + -- Delete button (X) for removing equipment set + local deleteBtn = CreateFrame("Button", nil, setBtn) + deleteBtn:SetWidth(14) + deleteBtn:SetHeight(14) + deleteBtn:SetPoint("RIGHT", cogBtn, "LEFT", -5, 0) + + local deleteText = deleteBtn:CreateFontString(nil, "OVERLAY") + deleteText:SetFont(fontPath, 18, "") + deleteText:SetText("×") + deleteText:SetTextColor(1, 1, 1, 0.8) + deleteText:SetPoint("CENTER", deleteBtn, "CENTER", 0, 0) + + deleteBtn:SetScript("OnEnter", function() + deleteText:SetTextColor(1, 0.2, 0.2, 1) -- Red + end) + + deleteBtn:SetScript("OnLeave", function() + deleteText:SetTextColor(1, 1, 1, 0.8) + end) + + deleteBtn:SetScript("OnClick", function() + -- Show confirmation dialog + StaticPopupDialogs["EUI_DELETE_EQUIPMENT_SET"] = { + text = "Delete equipment set '" .. setData.name .. "'?", + button1 = "Delete", + button2 = "Cancel", + OnAccept = function() + C_EquipmentSet.DeleteEquipmentSet(setData.id) + RefreshEquipmentSets() + end, + timeout = 0, + whileDead = false, + hideOnEscape = true, + } + StaticPopup_Show("EUI_DELETE_EQUIPMENT_SET") + end) + yOffset = yOffset - 30 end @@ -1985,6 +2134,13 @@ local function SkinCharacterSheet() globalSocketContainer:Show() frame._socketContainer = globalSocketContainer -- Store reference on frame + -- Create overlay frame for text labels (above model, transparent, no mouse input) + local textOverlayFrame = CreateFrame("Frame", "EUI_CharSheet_TextOverlay", frame) + textOverlayFrame:SetFrameLevel(5) -- Higher than model (FrameLevel 2) + textOverlayFrame:EnableMouse(false) + textOverlayFrame:Show() + frame._textOverlayFrame = textOverlayFrame + for _, slotName in ipairs(itemSlots) do ApplyCustomSlotBorder(slotName) @@ -1992,7 +2148,7 @@ local function SkinCharacterSheet() local slot = _G[slotName] if slot and not slot._itemLevelLabel then local itemLevelSize = EllesmereUIDB and EllesmereUIDB.charSheetItemLevelSize or 11 - local label = frame:CreateFontString(nil, "OVERLAY") + local label = textOverlayFrame:CreateFontString(nil, "OVERLAY") label:SetFont(fontPath, itemLevelSize, "") label:SetTextColor(1, 1, 1, 0.8) label:SetJustifyH("CENTER") @@ -2018,7 +2174,7 @@ local function SkinCharacterSheet() -- Create enchant labels if slot and not slot._enchantLabel then local enchantSize = EllesmereUIDB and EllesmereUIDB.charSheetEnchantSize or 9 - local enchantLabel = frame:CreateFontString(nil, "OVERLAY") + local enchantLabel = textOverlayFrame:CreateFontString(nil, "OVERLAY") enchantLabel:SetFont(fontPath, enchantSize, "") enchantLabel:SetTextColor(1, 1, 1, 0.8) enchantLabel:SetJustifyH("CENTER") @@ -2040,7 +2196,7 @@ local function SkinCharacterSheet() -- Create upgrade track labels (positioned relative to itemlevel) if slot and not slot._upgradeTrackLabel and slot._itemLevelLabel then local upgradeTrackSize = EllesmereUIDB and EllesmereUIDB.charSheetUpgradeTrackSize or 11 - local upgradeTrackLabel = frame:CreateFontString(nil, "OVERLAY") + local upgradeTrackLabel = textOverlayFrame:CreateFontString(nil, "OVERLAY") upgradeTrackLabel:SetFont(fontPath, upgradeTrackSize, "") upgradeTrackLabel:SetTextColor(1, 1, 1, 0.6) upgradeTrackLabel:SetJustifyH("CENTER") @@ -2064,6 +2220,35 @@ local function SkinCharacterSheet() end end + -- Update slot borders on inventory changes + local function UpdateSlotBorders() + for _, slotName in ipairs(itemSlots) do + local slot = _G[slotName] + if slot then + local itemLink = GetInventoryItemLink("player", slot:GetID()) + local borderR, borderG, borderB = 0.4, 0.4, 0.4 -- Default dark gray + if itemLink then + local _, _, rarity = GetItemInfo(itemLink) + if rarity then + borderR, borderG, borderB = C_Item.GetItemQualityColor(rarity) + end + end + if EllesmereUI and EllesmereUI.PanelPP then + EllesmereUI.PanelPP.SetBorderColor(slot, borderR, borderG, borderB, 1) + end + end + end + end + + -- Listen for inventory changes and update borders + local inventoryFrame = CreateFrame("Frame") + inventoryFrame:RegisterEvent("UNIT_INVENTORY_CHANGED") + inventoryFrame:SetScript("OnEvent", function(self, event, unit) + if event == "UNIT_INVENTORY_CHANGED" and unit == "player" then + UpdateSlotBorders() + end + end) + -- Socket icon creation and display logic local function GetOrCreateSocketIcons(slot, side, slotIndex) if slot._euiCharSocketsIcons then return slot._euiCharSocketsIcons end @@ -2414,7 +2599,7 @@ local function SkinCharacterSlot(slotName, slotID) -- EUI-style background for the slot local slotBg = slot:CreateTexture(nil, "BACKGROUND", nil, -5) slotBg:SetAllPoints(slot) - slotBg:SetColorTexture(0.03, 0.045, 0.05, 0.7) -- EUI frame BG with transparency + slotBg:SetColorTexture(0.5, 0.5, 0.5, 0.7) -- Gray background with transparency slot._slotBg = slotBg -- Create custom border on the slot using PP.CreateBorder @@ -2548,6 +2733,14 @@ function EllesmereUI._applyCharSheetTextSizes() local itemLevelSize = EllesmereUIDB and EllesmereUIDB.charSheetItemLevelSize or 11 local upgradeTrackSize = EllesmereUIDB and EllesmereUIDB.charSheetUpgradeTrackSize or 11 local enchantSize = EllesmereUIDB and EllesmereUIDB.charSheetEnchantSize or 9 + + local itemLevelShadow = EllesmereUIDB and EllesmereUIDB.charSheetItemLevelShadow or false + local itemLevelOutline = EllesmereUIDB and EllesmereUIDB.charSheetItemLevelOutline or false + local upgradeTrackShadow = EllesmereUIDB and EllesmereUIDB.charSheetUpgradeTrackShadow or false + local upgradeTrackOutline = EllesmereUIDB and EllesmereUIDB.charSheetUpgradeTrackOutline or false + local enchantShadow = EllesmereUIDB and EllesmereUIDB.charSheetEnchantShadow or false + local enchantOutline = EllesmereUIDB and EllesmereUIDB.charSheetEnchantOutline or false + local fontPath = EllesmereUI.GetFontPath and EllesmereUI.GetFontPath() or STANDARD_TEXT_FONT -- Update all slot labels @@ -2562,13 +2755,46 @@ function EllesmereUI._applyCharSheetTextSizes() local slot = _G[slotName] if slot then if slot._itemLevelLabel then - slot._itemLevelLabel:SetFont(fontPath, itemLevelSize, "") + local flags = "" + if itemLevelOutline then + flags = "OUTLINE" + end + slot._itemLevelLabel:SetFont(fontPath, itemLevelSize, flags) + -- Apply shadow effect if enabled + if itemLevelShadow then + slot._itemLevelLabel:SetShadowColor(0, 0, 0, 1) + slot._itemLevelLabel:SetShadowOffset(1, -1) + else + slot._itemLevelLabel:SetShadowColor(0, 0, 0, 0) + end end if slot._upgradeTrackLabel then - slot._upgradeTrackLabel:SetFont(fontPath, upgradeTrackSize, "") + local flags = "" + if upgradeTrackOutline then + flags = "OUTLINE" + end + slot._upgradeTrackLabel:SetFont(fontPath, upgradeTrackSize, flags) + -- Apply shadow effect if enabled + if upgradeTrackShadow then + slot._upgradeTrackLabel:SetShadowColor(0, 0, 0, 1) + slot._upgradeTrackLabel:SetShadowOffset(1, -1) + else + slot._upgradeTrackLabel:SetShadowColor(0, 0, 0, 0) + end end if slot._enchantLabel then - slot._enchantLabel:SetFont(fontPath, enchantSize, "") + local flags = "" + if enchantOutline then + flags = "OUTLINE" + end + slot._enchantLabel:SetFont(fontPath, enchantSize, flags) + -- Apply shadow effect if enabled + if enchantShadow then + slot._enchantLabel:SetShadowColor(0, 0, 0, 1) + slot._enchantLabel:SetShadowOffset(1, -1) + else + slot._enchantLabel:SetShadowColor(0, 0, 0, 0) + end end end end From 799b1d79cb1353466c48a5e0655e52a01afe99e1 Mon Sep 17 00:00:00 2001 From: Daniel <14241290+dnlxh@users.noreply.github.com> Date: Sat, 11 Apr 2026 03:39:02 +0200 Subject: [PATCH 08/16] Hide custom character stats and socket icons when switching tabs - Only show average itemlevel, stats panel background, and scrollbars on Character tab - Hide all stat section containers when switching away from Character tab - Only refresh socket icons and show socket container if on Character tab - Fix socketWatcher to only update sockets when on Character tab and frame is shown - Ensure clean tab switching without leftover UI elements --- charactersheet.lua | 75 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/charactersheet.lua b/charactersheet.lua index efb4ea42..87c1afcc 100644 --- a/charactersheet.lua +++ b/charactersheet.lua @@ -586,6 +586,53 @@ local function SkinCharacterSheet() end end + -- Hide average itemlevel text when not on character tab + if frame._iLvlText then + if isCharacterTab then + frame._iLvlText:Show() + else + frame._iLvlText:Hide() + end + end + + -- Hide/show all stat sections based on tab + if frame._statsSections then + for _, sectionData in ipairs(frame._statsSections) do + if sectionData.container then + if isCharacterTab then + sectionData.container:Show() + else + sectionData.container:Hide() + end + end + end + end + + -- Hide/show stat panel background, scrollFrame and scrollBar + if frame._statsBg then + if isCharacterTab then + frame._statsBg:Show() + else + frame._statsBg:Hide() + end + end + + if frame._scrollFrame then + if isCharacterTab then + frame._scrollFrame:Show() + else + frame._scrollFrame:Hide() + end + end + + if frame._scrollBar then + if isCharacterTab then + frame._scrollBar:Show() + else + frame._scrollBar:Hide() + end + end + if frame._titlesPanel then if not isCharacterTab then frame._titlesPanel:Hide() @@ -633,12 +680,14 @@ local function SkinCharacterSheet() statsBg:SetColorTexture(0.03, 0.045, 0.05, 0.95) statsBg:SetSize(220, 340) -- Fixed size, doesn't expand statsBg:SetPoint("TOPLEFT", statsPanel, "TOPLEFT", 0, 0) + frame._statsBg = statsBg -- Store on frame for tab visibility control -- Itemlevel display (anchor to center of statsBg background) local iLvlText = statsPanel:CreateFontString(nil, "OVERLAY") iLvlText:SetFont(fontPath, 20, "") iLvlText:SetPoint("TOP", statsBg, "TOP", 0,60) iLvlText:SetTextColor(0.6, 0.2, 1, 1) + frame._iLvlText = iLvlText -- Store on frame for tab visibility control -- Function to update itemlevel local function UpdateItemLevelDisplay() @@ -672,6 +721,7 @@ local function SkinCharacterSheet() scrollFrame:SetSize(260, 320) scrollFrame:SetPoint("TOPLEFT", statsPanel, "TOPLEFT", 5, -10) scrollFrame:SetFrameLevel(51) + frame._scrollFrame = scrollFrame -- Store on frame for tab visibility control -- Create scroll child local scrollChild = CreateFrame("Frame", "EUI_CharSheet_ScrollChild", scrollFrame) @@ -685,6 +735,7 @@ local function SkinCharacterSheet() scrollBar:SetMinMaxValues(0, 0) scrollBar:SetValue(0) scrollBar:SetOrientation("VERTICAL") + frame._scrollBar = scrollBar -- Store on frame for tab visibility control -- Scrollbar background (disabled - causes visual glitches) -- local scrollBarBg = scrollBar:CreateTexture(nil, "BACKGROUND") @@ -2131,7 +2182,13 @@ local function SkinCharacterSheet() -- Create global socket container for all slot icons local globalSocketContainer = CreateFrame("Frame", "EUI_CharSheet_SocketContainer", frame) globalSocketContainer:SetFrameLevel(100) - globalSocketContainer:Show() + -- Only show if on character tab + local isCharacterTab = (frame.selectedTab or 1) == 1 + if isCharacterTab then + globalSocketContainer:Show() + else + globalSocketContainer:Hide() + end frame._socketContainer = globalSocketContainer -- Store reference on frame -- Create overlay frame for text labels (above model, transparent, no mouse input) @@ -2405,14 +2462,24 @@ local function SkinCharacterSheet() socketWatcher:RegisterEvent("PLAYER_ENTERING_WORLD") socketWatcher:SetScript("OnEvent", function() if EllesmereUIDB and EllesmereUIDB.themedCharacterSheet then - C_Timer.After(0.1, RefreshAllSocketIcons) + -- Only refresh if on character tab and frame is shown + local isCharacterTab = (frame.selectedTab or 1) == 1 + if frame:IsShown() and isCharacterTab then + C_Timer.After(0.1, RefreshAllSocketIcons) + end end end) -- Hook frame show/hide frame:HookScript("OnShow", function() - RefreshAllSocketIcons() - globalSocketContainer:Show() + -- Only refresh sockets and show container if on character tab + local isCharacterTab = (frame.selectedTab or 1) == 1 + if isCharacterTab then + RefreshAllSocketIcons() + globalSocketContainer:Show() + else + globalSocketContainer:Hide() + end -- Reset to Stats panel on open if statsPanel and CharacterFrame._titlesPanel and CharacterFrame._equipPanel then statsPanel:Show() From 97b3dd4268d512a3cafe3221c4aa3ac163f6ba34 Mon Sep 17 00:00:00 2001 From: Daniel <14241290+dnlxh@users.noreply.github.com> Date: Sat, 11 Apr 2026 16:45:31 +0200 Subject: [PATCH 09/16] fixing and features added **Fixed:** - Combat lockdown errors in Equipment Manager - Item caching with different upgrade levels - Secondary stat tooltip colors **Added:** - Raw stat values and combat ratings for secondary stats - Mythic+ Rating display - Equipment set completion indicator (red = incomplete) - Missing items tooltip - Real-time equipment monitoring --- EUI__General_Options.lua | 17 ++ charactersheet.lua | 532 ++++++++++++++++++++++++++++++++++----- 2 files changed, 491 insertions(+), 58 deletions(-) diff --git a/EUI__General_Options.lua b/EUI__General_Options.lua index 079de423..c575e1a9 100644 --- a/EUI__General_Options.lua +++ b/EUI__General_Options.lua @@ -3583,6 +3583,23 @@ initFrame:SetScript("OnEvent", function(self) { type="label", text="" } ); y = y - h + local mythicRatingRow + mythicRatingRow, h = W:DualRow(parent, y, + { type="toggle", text="Show Mythic+ Rating", + tooltip="Display your Mythic+ rating above the item level on the character sheet.", + getValue=function() + return EllesmereUIDB and EllesmereUIDB.showMythicRating or false + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.showMythicRating = v + if EllesmereUI._updateMythicRatingDisplay then + EllesmereUI._updateMythicRatingDisplay() + end + end }, + { type="label", text="" } + ); y = y - h + -- Disabled overlay for Color Item Level by Rarity when themed is off do local function themedOff() diff --git a/charactersheet.lua b/charactersheet.lua index 87c1afcc..e73249ac 100644 --- a/charactersheet.lua +++ b/charactersheet.lua @@ -328,16 +328,16 @@ local function SkinCharacterSheet() CharacterFrameInset:SetClipsChildren(false) -- Prevent clipping end - -- Hook SetWidth to prevent Blizzard from changing it back + -- Hook SetWidth to prevent Blizzard from changing it back (skip in combat) hooksecurefunc(frame, "SetWidth", function(self, w) - if w ~= newWidth then + if w ~= newWidth and not InCombatLockdown() then self:SetWidth(newWidth) end end) - -- Hook SetHeight to prevent Blizzard from changing it back + -- Hook SetHeight to prevent Blizzard from changing it back (skip in combat) hooksecurefunc(frame, "SetHeight", function(self, h) - if h ~= newHeight then + if h ~= newHeight and not InCombatLockdown() then self:SetHeight(newHeight) end end) @@ -347,9 +347,11 @@ local function SkinCharacterSheet() hooksecurefunc(frame, "SetPoint", function(self, ...) if not hookLock and frame._sizeCheckDone then hookLock = true - self:SetSize(newWidth, newHeight) - if self._ebsBg then - self._ebsBg:SetSize(newWidth, newHeight) + if not InCombatLockdown() then + self:SetSize(newWidth, newHeight) + if self._ebsBg then + self._ebsBg:SetSize(newWidth, newHeight) + end end hookLock = false end @@ -358,7 +360,7 @@ local function SkinCharacterSheet() -- Aggressive size enforcement with immediate re-setup if not frame._sizeCheckDone then local function EnforceSize() - if frame:IsShown() then + if frame:IsShown() and not InCombatLockdown() then frame:SetSize(newWidth, newHeight) -- Regenerate background immediately if frame._ebsBg then @@ -539,6 +541,22 @@ local function SkinCharacterSheet() end end + -- Helper function to safely show frames (deferred in combat) + local function SafeShow(element) + if InCombatLockdown() then + -- Defer until out of combat + local deferredShow = CreateFrame("Frame") + deferredShow:RegisterEvent("PLAYER_REGEN_ENABLED") + deferredShow:SetScript("OnEvent", function(self) + self:UnregisterEvent("PLAYER_REGEN_ENABLED") + if element then element:Show() end + self:Hide() + end) + else + if element then element:Show() end + end + end + -- Hook Blizzard's tab selection to update our visuals and show/hide slots hooksecurefunc("PanelTemplates_SetTab", function(panel) if panel == frame then @@ -551,7 +569,11 @@ local function SkinCharacterSheet() local slot = _G[slotName] if slot then if isCharacterTab then - slot:Show() + if InCombatLockdown() then + SafeShow(slot) + else + slot:Show() + end if slot._itemLevelLabel then slot._itemLevelLabel:Show() end if slot._enchantLabel then slot._enchantLabel:Show() end if slot._upgradeTrackLabel then slot._upgradeTrackLabel:Show() end @@ -565,22 +587,22 @@ local function SkinCharacterSheet() end end - -- Show/hide custom buttons based on tab + -- Show/hide custom buttons based on tab (deferred in combat) for _, btnName in ipairs({"EUI_CharSheet_Stats", "EUI_CharSheet_Titles", "EUI_CharSheet_Equipment"}) do local btn = _G[btnName] if btn then if isCharacterTab then - btn:Show() + SafeShow(btn) else btn:Hide() end end end - -- Show/hide stats panel and titles panel based on tab + -- Show/hide stats panel and titles panel based on tab (deferred in combat) if frame._statsPanel then if isCharacterTab then - frame._statsPanel:Show() + SafeShow(frame._statsPanel) else frame._statsPanel:Hide() end @@ -682,14 +704,169 @@ local function SkinCharacterSheet() statsBg:SetPoint("TOPLEFT", statsPanel, "TOPLEFT", 0, 0) frame._statsBg = statsBg -- Store on frame for tab visibility control + -- Map INVTYPE to inventory slot numbers and display names + local INVTYPE_TO_SLOT = { + INVTYPE_HEAD = {slot = 1, name = "Head"}, + INVTYPE_NECK = {slot = 2, name = "Neck"}, + INVTYPE_SHOULDER = {slot = 3, name = "Shoulder"}, + INVTYPE_CHEST = {slot = 5, name = "Chest"}, + INVTYPE_WAIST = {slot = 6, name = "Waist"}, + INVTYPE_LEGS = {slot = 7, name = "Legs"}, + INVTYPE_FEET = {slot = 8, name = "Feet"}, + INVTYPE_WRIST = {slot = 9, name = "Wrist"}, + INVTYPE_HAND = {slot = 10, name = "Hands"}, + INVTYPE_FINGER = {slots = {11, 12}, name = "Ring"}, + INVTYPE_TRINKET = {slots = {13, 14}, name = "Trinket"}, + INVTYPE_BACK = {slot = 15, name = "Back"}, + INVTYPE_MAINHAND = {slot = 16, name = "Main Hand"}, + INVTYPE_OFFHAND = {slot = 17, name = "Off Hand"}, + INVTYPE_RELIC = {slot = 18, name = "Relic"}, + INVTYPE_BODY = {slot = 4, name = "Body"}, + INVTYPE_SHIELD = {slot = 17, name = "Shield"}, + INVTYPE_2HWEAPON = {slot = 16, name = "Two-Hand"}, + } + + -- Function to get itemlevel of equipped item in a specific slot + local function GetEquippedItemLevel(slot) + local itemLink = GetInventoryItemLink("player", slot) + if itemLink then + local _, _, _, itemLevel = GetItemInfo(itemLink) + return tonumber(itemLevel) or 0 + end + return 0 + end + + -- Function to get better items from inventory (equipment only) + local function GetBetterInventoryItems() + local betterItems = {} + + -- Check all bag slots (0 = backpack, 1-4 = bag slots) + for bagSlot = 0, 4 do + local bagSize = C_Container.GetContainerNumSlots(bagSlot) + for slotIndex = 1, bagSize do + local itemLink = C_Container.GetContainerItemLink(bagSlot, slotIndex) + if itemLink then + local itemName, _, itemRarity, itemLevel, _, itemType, _, _, equipSlot, itemIcon = GetItemInfo(itemLink) + itemLevel = tonumber(itemLevel) + + -- Only show Weapon and Armor items + if itemLevel and itemName and (itemType == "Weapon" or itemType == "Armor") and equipSlot then + -- Get the slot(s) this item can equip to + local slotInfo = INVTYPE_TO_SLOT[equipSlot] + if slotInfo then + local isBetter = false + local compareSlots = slotInfo.slots or {slotInfo.slot} + + -- Check if item is better than ANY of its possible slots + for _, slot in ipairs(compareSlots) do + local equippedLevel = GetEquippedItemLevel(slot) + if itemLevel > equippedLevel then + isBetter = true + break + end + end + + if isBetter then + table.insert(betterItems, { + name = itemName, + level = itemLevel, + rarity = itemRarity or 1, + icon = itemIcon, + slot = slotInfo.name + }) + end + end + end + end + end + end + + -- Sort by level descending + table.sort(betterItems, function(a, b) return a.level > b.level end) + + return betterItems + end + + -- Mythic+ Rating display (anchor above itemlevel) + local mythicRatingLabel = statsPanel:CreateFontString(nil, "OVERLAY") + mythicRatingLabel:SetFont(fontPath, 11, "") + mythicRatingLabel:SetPoint("TOP", statsBg, "TOP", 0, 80) + mythicRatingLabel:SetTextColor(0.8, 0.8, 0.8, 1) + mythicRatingLabel:SetText("Mythic+ Rating:") + frame._mythicRatingLabel = mythicRatingLabel + + local mythicRatingValue = statsPanel:CreateFontString(nil, "OVERLAY") + mythicRatingValue:SetFont(fontPath, 13, "") + mythicRatingValue:SetPoint("TOP", mythicRatingLabel, "BOTTOM", 0, -2) + frame._mythicRatingValue = mythicRatingValue + -- Itemlevel display (anchor to center of statsBg background) local iLvlText = statsPanel:CreateFontString(nil, "OVERLAY") - iLvlText:SetFont(fontPath, 20, "") - iLvlText:SetPoint("TOP", statsBg, "TOP", 0,60) + iLvlText:SetFont(fontPath, 18, "") + iLvlText:SetPoint("TOP", statsBg, "TOP", 0, 50) iLvlText:SetTextColor(0.6, 0.2, 1, 1) frame._iLvlText = iLvlText -- Store on frame for tab visibility control - -- Function to update itemlevel + -- Button overlay for itemlevel tooltip + local iLvlButton = CreateFrame("Button", nil, statsPanel) + iLvlButton:SetPoint("TOP", statsBg, "TOP", 0, 60) + iLvlButton:SetSize(100, 30) + iLvlButton:EnableMouse(true) + iLvlButton:SetScript("OnEnter", function(self) + local betterItems = GetBetterInventoryItems() + + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") + GameTooltip:AddLine("Equipped Item Level", 0.6, 0.2, 1, 1) + + if #betterItems > 0 then + GameTooltip:AddLine(" ") + GameTooltip:AddLine( + string.format("You have %d better item%s in inventory", #betterItems, #betterItems == 1 and "" or "s"), + 0.2, 1, 0.2 + ) + GameTooltip:AddLine(" ") + + -- Show up to 10 items with icons and slots (slot on right side) + local maxShow = math.min(#betterItems, 10) + for i = 1, maxShow do + local item = betterItems[i] + local leftText = string.format("|T%s:16|t %s (iLvl %d)", item.icon, item.name, item.level) + GameTooltip:AddDoubleLine(leftText, item.slot, 1, 1, 1, 0.7, 0.7, 0.7) + end + + if #betterItems > 10 then + GameTooltip:AddLine( + string.format(" ... and %d more", #betterItems - 10), + 0.7, 0.7, 0.7 + ) + end + else + GameTooltip:AddLine(" ") + GameTooltip:AddLine("No better items in inventory", 0.7, 0.7, 0.7, true) + end + + -- Calculate minimum width based on longest item text + local maxWidth = 250 + if #betterItems > 0 then + local maxShow = math.min(#betterItems, 10) + for i = 1, maxShow do + local item = betterItems[i] + local text = string.format("%s (iLvl %d) - %s", item.name, item.level, item.slot) + -- Rough estimate: ~6 pixels per character + icon space + local estimatedWidth = #text * 6 + 30 + if estimatedWidth > maxWidth then + maxWidth = estimatedWidth + end + end + end + GameTooltip:SetMinimumWidth(maxWidth) + GameTooltip:Show() + end) + iLvlButton:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + + -- Function to update itemlevel and mythic+ rating local function UpdateItemLevelDisplay() local avgItemLevel, avgItemLevelEquipped = GetAverageItemLevel() @@ -699,6 +876,48 @@ local function SkinCharacterSheet() -- Display format: equipped / average iLvlText:SetText(format("%s / %s", avgEquippedFormatted, avgFormatted)) + + -- Update Mythic+ Rating if option is enabled + if EllesmereUIDB and EllesmereUIDB.showMythicRating and frame._mythicRatingValue then + local mythicRating = C_ChallengeMode.GetOverallDungeonScore() + if mythicRating and mythicRating > 0 then + -- Color brackets based on rating + local r, g, b = 0.7, 0.7, 0.7 -- Gray default + + if mythicRating >= 2500 then + r, g, b = 1.0, 0.64, 0.0 -- Orange/Gold + elseif mythicRating >= 2000 then + r, g, b = 0.64, 0.21, 1.0 -- Purple + elseif mythicRating >= 1500 then + r, g, b = 0.0, 0.44, 0.87 -- Blue + elseif mythicRating >= 1000 then + r, g, b = 0.12, 1.0, 0.0 -- Green + end + + frame._mythicRatingValue:SetText(tostring(math.floor(mythicRating))) + frame._mythicRatingValue:SetTextColor(r, g, b, 1) + frame._mythicRatingLabel:Show() + frame._mythicRatingValue:Show() + + -- Adjust itemlevel display when mythic+ rating is shown + iLvlText:SetFont(fontPath, 18, "") + iLvlText:SetPoint("TOP", statsBg, "TOP", 0, 50) + else + frame._mythicRatingLabel:Hide() + frame._mythicRatingValue:Hide() + + -- Restore itemlevel display when mythic+ rating is not available + iLvlText:SetFont(fontPath, 20, "") + iLvlText:SetPoint("TOP", statsBg, "TOP", 0, 60) + end + elseif frame._mythicRatingValue then + frame._mythicRatingLabel:Hide() + frame._mythicRatingValue:Hide() + + -- Restore itemlevel display when mythic+ rating is disabled + iLvlText:SetFont(fontPath, 20, "") + iLvlText:SetPoint("TOP", statsBg, "TOP", 0, 60) + end end -- Create update frame for itemlevel and spec changes @@ -710,6 +929,11 @@ local function SkinCharacterSheet() UpdateItemLevelDisplay() + -- Store callback for option changes + EllesmereUI._updateMythicRatingDisplay = function() + UpdateItemLevelDisplay() + end + --[[ Stats panel border if EllesmereUI and EllesmereUI.PanelPP then EllesmereUI.PanelPP.CreateBorder(statsPanel, 0.15, 0.15, 0.15, 1, 1, "OVERLAY", 1) @@ -833,9 +1057,9 @@ local function SkinCharacterSheet() -- Return fixed order: Primary Stat, Stamina, Health return { - { name = primaryStat, func = function() return UnitStat("player", primaryStatIndex) end }, - { name = "Stamina", func = function() return UnitStat("player", 3) end }, - { name = "Health", func = function() return UnitHealthMax("player") end }, + { name = primaryStat, func = function() return UnitStat("player", primaryStatIndex) end, statIndex = primaryStatIndex, tooltip = (primaryStatIndex == 1 and "Increases melee attack power") or (primaryStatIndex == 2 and "Increases dodge chance and melee attack power") or (primaryStatIndex == 4 and "Increase the magnitude of your attacks and Abilities") or "Primary stat" }, + { name = "Stamina", func = function() return UnitStat("player", 3) end, statIndex = 3, tooltip = "Increases health" }, + { name = "Health", func = function() return UnitHealthMax("player") end, tooltip = "The amount of damage you can take" }, } end @@ -851,28 +1075,28 @@ local function SkinCharacterSheet() title = "Secondary Stats", color = { r = 0.471, g = 0.255, b = 0.784 }, stats = { - { name = "Crit", func = function() return GetCritChance("player") or 0 end, format = "%.2f%%" }, - { name = "Haste", func = function() return UnitSpellHaste("player") or 0 end, format = "%.2f%%" }, - { name = "Mastery", func = function() return GetMasteryEffect() or 0 end, format = "%.2f%%" }, - { name = "Versatility", func = function() return GetCombatRatingBonus(CR_VERSATILITY_DAMAGE_DONE) or 0 end, format = "%.2f%%" }, + { name = "Crit", func = function() return GetCritChance("player") or 0 end, format = "%.2f%%", rawFunc = function() return GetCombatRating(CR_CRIT_MELEE) or 0 end }, + { name = "Haste", func = function() return UnitSpellHaste("player") or 0 end, format = "%.2f%%", rawFunc = function() return GetCombatRating(CR_HASTE_MELEE) or 0 end }, + { name = "Mastery", func = function() return GetMasteryEffect() or 0 end, format = "%.2f%%", rawFunc = function() return GetCombatRating(CR_MASTERY) or 0 end }, + { name = "Versatility", func = function() return GetCombatRatingBonus(CR_VERSATILITY_DAMAGE_DONE) or 0 end, format = "%.2f%%", rawFunc = function() return GetCombatRating(CR_VERSATILITY_DAMAGE_DONE) or 0 end }, } }, { title = "Attack", color = { r = 1, g = 0.353, b = 0.122 }, stats = { - { name = "Spell Power", func = function() return GetSpellBonusDamage(7) end }, - { name = "Attack Speed", func = function() return UnitAttackSpeed("player") or 0 end, format = "%.2f" }, + { name = "Spell Power", func = function() return GetSpellBonusDamage(7) end, tooltip = "Increases the power of your spells and abilities" }, + { name = "Attack Speed", func = function() return UnitAttackSpeed("player") or 0 end, format = "%.2f", tooltip = "Attacks per second" }, } }, { title = "Defense", color = { r = 0.247, g = 0.655, b = 1 }, stats = { - { name = "Armor", func = function() local base, effectiveArmor = UnitArmor("player") return effectiveArmor end }, - { name = "Dodge", func = function() return GetDodgeChance() or 0 end, format = "%.2f%%" }, - { name = "Parry", func = function() return GetParryChance() or 0 end, format = "%.2f%%" }, - { name = "Stagger Effect", func = function() return C_PaperDollInfo.GetStaggerPercentage("player") or 0 end, format = "%.2f%%", showWhen = "brewmaster" }, + { name = "Armor", func = function() local base, effectiveArmor = UnitArmor("player") return effectiveArmor end, tooltip = "Reduces physical damage taken" }, + { name = "Dodge", func = function() return GetDodgeChance() or 0 end, format = "%.2f%%", tooltip = "Chance to avoid melee attacks" }, + { name = "Parry", func = function() return GetParryChance() or 0 end, format = "%.2f%%", tooltip = "Chance to block melee attacks" }, + { name = "Stagger Effect", func = function() return C_PaperDollInfo.GetStaggerPercentage("player") or 0 end, format = "%.2f%%", showWhen = "brewmaster", tooltip = "Converts damage into a delayed effect" }, } }, { @@ -1101,30 +1325,96 @@ local function SkinCharacterSheet() value:SetJustifyH("RIGHT") value:SetText("0") - -- Create button overlay for crest values to show tooltips - local valueButton = nil - if stat.currencyID then - valueButton = CreateFrame("Button", nil, sectionContainer) - valueButton:SetPoint("TOPRIGHT", sectionContainer, "TOPRIGHT", -2, statYOffset) - valueButton:SetSize(50, 16) - valueButton:EnableMouse(true) - valueButton:SetScript("OnEnter", function() + -- Create button overlay for all stats with tooltips + local valueButton = CreateFrame("Button", nil, sectionContainer) + valueButton:SetPoint("TOPRIGHT", sectionContainer, "TOPRIGHT", -2, statYOffset) + valueButton:SetSize(90, 16) + valueButton:EnableMouse(true) + + valueButton:SetScript("OnEnter", function() + local statValue = stat.func() + GameTooltip:SetOwner(valueButton, "ANCHOR_RIGHT") + + -- Format value according to stat's format string + local displayValue = statValue + if stat.format then + displayValue = string.format(stat.format, statValue) + else + displayValue = tostring(statValue) + end + + -- Build title line based on stat type + local titleLine = stat.name .. " " .. displayValue + + -- Currency (Crests) + if stat.currencyID then local current = GetCrestValue(stat.currencyID) local maximum = GetCrestMaxValue(stat.currencyID) - GameTooltip:SetOwner(valueButton, "ANCHOR_RIGHT") - GameTooltip:AddLine(stat.name .. " Crests", 1, 1, 1) - GameTooltip:AddLine(string.format("%d / %d", current, maximum), 0.7, 0.7, 0.7) - GameTooltip:Show() - end) - valueButton:SetScript("OnLeave", function() - GameTooltip:Hide() - end) - end + GameTooltip:AddLine(stat.name .. " Crests", section.color.r, section.color.g, section.color.b, 1) + GameTooltip:AddLine(string.format("%d / %d", current, maximum), 1, 1, 1, true) + -- Secondary stats with raw rating + elseif stat.rawFunc then + local percentValue = stat.func() + local rawValue = stat.rawFunc() + GameTooltip:AddLine( + string.format("%s %.2f%% (%d rating)", stat.name, percentValue, rawValue), + section.color.r, section.color.g, section.color.b, 1 -- Use category color + ) + -- Description for secondary stats + local description = "" + if stat.name == "Crit" then + description = string.format("Increases your chance to critically hit by %.2f%%.", percentValue) + elseif stat.name == "Haste" then + description = string.format("Increases attack and casting speed by %.2f%%.", percentValue) + elseif stat.name == "Mastery" then + description = string.format("Increases the effectiveness of your Mastery by %.2f%%.", percentValue) + elseif stat.name == "Versatility" then + description = string.format("Increases damage and healing done by %.2f%% and reduces damage taken by %.2f%%.", percentValue, percentValue / 2) + end + GameTooltip:AddLine(description, 1, 1, 1, true) + -- Attributes + elseif stat.statIndex then + local base, _, posBuff, negBuff = UnitStat("player", stat.statIndex) + local statLabel = stat.name + + -- Map to Blizzard global names + if stat.name == "Strength" then + statLabel = STAT_STRENGTH or "Strength" + elseif stat.name == "Agility" then + statLabel = STAT_AGILITY or "Agility" + elseif stat.name == "Intellect" then + statLabel = STAT_INTELLECT or "Intellect" + elseif stat.name == "Stamina" then + statLabel = STAT_STAMINA or "Stamina" + end + + local bonus = (posBuff or 0) + (negBuff or 0) + local statLine = statLabel .. " " .. statValue + if bonus ~= 0 then + statLine = statLine .. " (" .. base .. (bonus > 0 and "+" or "") .. bonus .. ")" + end + GameTooltip:AddLine(statLine, section.color.r, section.color.g, section.color.b, 1) + GameTooltip:AddLine(stat.tooltip, 1, 1, 1, true) + -- Generic stats (Attack, Defense, etc.) + else + GameTooltip:AddLine(titleLine, section.color.r, section.color.g, section.color.b, 1) + if stat.tooltip then + GameTooltip:AddLine(stat.tooltip, 1, 1, 1, true) + end + end + + GameTooltip:Show() + end) + + valueButton:SetScript("OnLeave", function() + GameTooltip:Hide() + end) -- Store for updates table.insert(frame._statsValues, { value = value, func = stat.func, + rawFunc = stat.rawFunc, format = stat.format or "%d" }) @@ -1688,6 +1978,8 @@ local function SkinCharacterSheet() newSetText:SetPoint("CENTER", newSetBtn, "CENTER", 0, 0) newSetBtn:SetScript("OnClick", function() + if InCombatLockdown() then return end + StaticPopupDialogs["EUI_NEW_EQUIPMENT_SET"] = { text = "New equipment set name:", button1 = "Create", @@ -1762,6 +2054,8 @@ local function SkinCharacterSheet() end) equipTopBtn:SetScript("OnClick", function() + if InCombatLockdown() then return end + -- Visual feedback: change text to "Equipped!" and color it green equipTopText:SetText("Equipped!") equipTopText:SetTextColor(0.047, 0.824, 0.616, 1) -- Green @@ -1823,6 +2117,8 @@ local function SkinCharacterSheet() end) saveTopBtn:SetScript("OnClick", function() + if InCombatLockdown() then return end + -- Visual feedback: change text to "Saved!" and color it green saveTopText:SetText("Saved!") saveTopText:SetTextColor(0.047, 0.824, 0.616, 1) -- Green @@ -1867,6 +2163,75 @@ local function SkinCharacterSheet() rightLine:SetPoint("RIGHT", setsHeaderFrame, "RIGHT", 0, -14) rightLine:SetHeight(2) + -- Function to check if all items of a set are equipped + local function IsEquipmentSetComplete(setName) + if not C_EquipmentSet or not C_EquipmentSet.GetEquipmentSetID then + return true -- API not available + end + + -- Get the set ID from the name + local setID = C_EquipmentSet.GetEquipmentSetID(setName) + if not setID then + return true -- Set not found + end + + -- Get the items in this set + local setItems = C_EquipmentSet.GetItemIDs(setID) + if not setItems then + return true -- No items in set + end + + -- Compare each slot + for slot, setItemID in pairs(setItems) do + if setItemID and setItemID ~= 0 then + local equippedID = GetInventoryItemID("player", slot) + if equippedID ~= setItemID then + return false -- Mismatch = incomplete set + end + end + end + + return true -- All items match + end + + -- Function to get missing items from a set + local function GetMissingSetItems(setName) + if not C_EquipmentSet or not C_EquipmentSet.GetEquipmentSetID then + return {} + end + + local setID = C_EquipmentSet.GetEquipmentSetID(setName) + if not setID then return {} end + + local setItems = C_EquipmentSet.GetItemIDs(setID) + if not setItems then return {} end + + local missing = {} + local slotNames = { + "Head", "Neck", "Shoulder", "Back", + "Chest", "Waist", "Legs", "Feet", + "Wrist", "Hands", "Finger 1", "Finger 2", + "Trinket 1", "Trinket 2", "Main Hand", "Off Hand", + "Tabard", "Chest (Relic)", "Back (Relic)" + } + + for slot, setItemID in pairs(setItems) do + if setItemID and setItemID ~= 0 then + local equippedID = GetInventoryItemID("player", slot) + if equippedID ~= setItemID then + local itemName = GetItemInfo(setItemID) + table.insert(missing, { + slot = slotNames[slot] or "Unknown", + itemID = setItemID, + itemName = itemName or "Unknown Item" + }) + end + end + end + + return missing + end + -- Function to reload equipment sets RefreshEquipmentSets = function() -- Clear old set buttons (but keep the new set, equip, save buttons, and header) @@ -1909,9 +2274,20 @@ local function SkinCharacterSheet() local setText = setBtn:CreateFontString(nil, "OVERLAY") setText:SetFont(fontPath, 10, "") setText:SetText(setData.name) - setText:SetTextColor(1, 1, 1, 1) + + -- Check if all items are equipped, if not, color red + if IsEquipmentSetComplete(setData.name) then + setText:SetTextColor(1, 1, 1, 1) -- White + else + setText:SetTextColor(1, 0.3, 0.3, 1) -- Red + end + setText:SetPoint("LEFT", setBtn, "LEFT", 10, 0) + -- Store references for the color monitor + setBtn._setText = setText + setBtn._setName = setData.name + -- Spec icon local assignedSpec = C_EquipmentSet.GetEquipmentSetAssignedSpec(setData.id) if assignedSpec then @@ -1931,12 +2307,34 @@ local function SkinCharacterSheet() RefreshEquipmentSets() end) - -- Hover effect + -- Hover effect and tooltip for missing items setBtn:SetScript("OnEnter", function() btnBg:SetColorTexture(0.047, 0.824, 0.616, 0.2) + + -- Show tooltip if set is incomplete + if not IsEquipmentSetComplete(setData.name) then + local missing = GetMissingSetItems(setData.name) + if #missing > 0 then + GameTooltip:SetOwner(setBtn, "ANCHOR_RIGHT") + GameTooltip:AddLine("Missing Items:", 1, 0.3, 0.3, 1) + + for _, item in ipairs(missing) do + local icon = GetItemIcon(item.itemID) + local iconText = icon and string.format("|T%s:16|t", icon) or "" + GameTooltip:AddLine( + string.format("%s %s: %s", iconText, item.slot, item.itemName), + 1, 1, 1, true + ) + end + + GameTooltip:Show() + end + end end) setBtn:SetScript("OnLeave", function() + GameTooltip:Hide() + if selectedSetID == setData.id then btnBg:SetColorTexture(0.047, 0.824, 0.616, 0.5) elseif activeEquipmentSetID == setData.id then @@ -2120,11 +2518,30 @@ local function SkinCharacterSheet() equipScrollChild:SetHeight(-yOffset) end + -- Continuous monitor to update set colors when equipment changes + local equipmentColorMonitor = CreateFrame("Frame") + equipmentColorMonitor:SetScript("OnUpdate", function() + if not (CharacterFrame and CharacterFrame:IsShown() and CharacterFrame._equipPanel and CharacterFrame._equipPanel:IsShown()) then + return + end + + -- Update colors of all visible set buttons + for _, child in ipairs({equipScrollChild:GetChildren()}) do + if child._setText and child._setName then + if IsEquipmentSetComplete(child._setName) then + child._setText:SetTextColor(1, 1, 1, 1) -- White + else + child._setText:SetTextColor(1, 0.3, 0.3, 1) -- Red + end + end + end + end) + -- Event handler for equipment set changes local equipSetChangeFrame = CreateFrame("Frame") equipSetChangeFrame:RegisterEvent("EQUIPMENT_SETS_CHANGED") - equipSetChangeFrame:SetScript("OnEvent", function() - -- activeEquipmentSetID is set by the Equip button and auto-equip logic + equipSetChangeFrame:SetScript("OnEvent", function(self, event) + -- Full refresh when sets change if CharacterFrame and CharacterFrame:IsShown() and CharacterFrame._equipPanel and CharacterFrame._equipPanel:IsShown() then RefreshEquipmentSets() end @@ -2497,8 +2914,8 @@ local function SkinCharacterSheet() local enchantTooltip = CreateFrame("GameTooltip", "EUICharacterSheetEnchantTooltip", nil, "GameTooltipTemplate") enchantTooltip:SetOwner(UIParent, "ANCHOR_NONE") - -- Cache item IDs to only update when items change - local itemIdCache = {} + -- Cache item info (ID, level, upgrade track) to update when items change + local itemCache = {} -- Function to update enchant text and upgrade track for a slot local function UpdateSlotInfo(slotName) @@ -2584,7 +3001,7 @@ local function SkinCharacterSheet() end end - -- Monitor and update only when items change + -- Monitor and update only when items change (including upgrade level) if not frame._itemLevelMonitor then frame._itemLevelMonitor = CreateFrame("Frame") frame._itemLevelMonitor:SetScript("OnUpdate", function() @@ -2593,13 +3010,12 @@ local function SkinCharacterSheet() end if frame and frame:IsShown() then for _, slotName in ipairs(itemSlots) do - -- Use GetItemInfoInstant to check if item changed without tooltip overhead + -- Get full item link (includes itemlevel and upgrade info) local itemLink = GetInventoryItemLink("player", _G[slotName]:GetID()) - local itemId = itemLink and GetItemInfoInstant(itemLink) or nil - -- Only update if item ID changed - if itemIdCache[slotName] ~= itemId then - itemIdCache[slotName] = itemId + -- Compare full link to detect changes in itemlevel or upgrade track + if itemCache[slotName] ~= itemLink then + itemCache[slotName] = itemLink UpdateSlotInfo(slotName) end end From 17fb2a1fbf1beee3d88f86eed5e0750b691675d4 Mon Sep 17 00:00:00 2001 From: Daniel <14241290+dnlxh@users.noreply.github.com> Date: Sat, 11 Apr 2026 20:58:00 +0200 Subject: [PATCH 10/16] Character Panel Redesign and cleanup Character Panel Redesign - Reorganized UI layout - Themed Character Sheet cogwheel with scale slider - Custom color pickers for all stat categories - Auto-disable related options when theme is off Upgradetrack, Itemlevel & Enchants - Cogwheel menus with Font Size, Shadow & Outline options - Custom color picker for each - Color picker auto-disables when toggled off Crests System - Seasonal max now dynamically fetched from WoW API - Fixed currency IDs (Champion: 3343, Adventurer: 3383) Enchant Display - Shows "" in red for enchantable slots - Only appears on relevant slots (Head, Shoulders, Back, Chest, etc.) --- EUI__General_Options.lua | 920 ++++++++++++++++++++++++++------------- charactersheet.lua | 403 +++++++++++++++-- 2 files changed, 1001 insertions(+), 322 deletions(-) diff --git a/EUI__General_Options.lua b/EUI__General_Options.lua index c575e1a9..ff4810fa 100644 --- a/EUI__General_Options.lua +++ b/EUI__General_Options.lua @@ -3503,7 +3503,8 @@ initFrame:SetScript("OnEvent", function(self) --------------------------------------------------------------------------- _, h = W:SectionHeader(parent, "CHARACTER PANEL CUSTOMIZATIONS", y); y = y - h - _, h = W:DualRow(parent, y, + local themedCharacterSheetRow + themedCharacterSheetRow, h = W:DualRow(parent, y, { type="toggle", text="Themed Character Sheet", tooltip="Applies EllesmereUI theme styling to the character sheet window.", getValue=function() @@ -3512,6 +3513,18 @@ initFrame:SetScript("OnEvent", function(self) setValue=function(v) if not EllesmereUIDB then EllesmereUIDB = {} end EllesmereUIDB.themedCharacterSheet = v + -- If turning off themed, disable all related options + if not v then + EllesmereUIDB.showMythicRating = false + EllesmereUIDB.showItemLevel = false + EllesmereUIDB.showUpgradeTrack = false + EllesmereUIDB.showEnchants = false + EllesmereUIDB.showStatCategory_Attributes = false + EllesmereUIDB.showStatCategory_Attack = false + EllesmereUIDB.showStatCategory_Crests = false + EllesmereUIDB.showStatCategory_SecondaryStats = false + EllesmereUIDB.showStatCategory_Defense = false + end if EllesmereUI.ShowConfirmPopup then EllesmereUI:ShowConfirmPopup({ title = "Reload Required", @@ -3523,65 +3536,137 @@ initFrame:SetScript("OnEvent", function(self) end EllesmereUI:RefreshPage() end }, - { type="slider", text="Character Sheet Scale", - min=0.5, max=1.5, step=0.05, - tooltip="Adjusts the scale of the themed character sheet window.", + { type="toggle", text="Show Attributes", + tooltip="Toggle visibility of the Attributes stat category.", getValue=function() - return EllesmereUIDB and EllesmereUIDB.themedCharacterSheetScale or 1 + return EllesmereUIDB and EllesmereUIDB.showStatCategory_Attributes ~= false end, setValue=function(v) if not EllesmereUIDB then EllesmereUIDB = {} end - EllesmereUIDB.themedCharacterSheetScale = v - if CharacterFrame then - CharacterFrame:SetScale(v) + EllesmereUIDB.showStatCategory_Attributes = v + if EllesmereUI._updateStatCategoryVisibility then + EllesmereUI._updateStatCategoryVisibility() end end } ); y = y - h - -- Disabled overlay for Scale slider when themed is off + -- Disabled overlay for themedCharacterSheetRow when themed is off (only for Show Attributes on right) do local function themedOff() return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) end - local scaleBlock = CreateFrame("Frame", nil, parent) - scaleBlock:SetSize(400, 30) - scaleBlock:SetPoint("TOPLEFT", parent, "TOPLEFT", 420, -y + 30) - scaleBlock:SetFrameLevel(parent:GetFrameLevel() + 20) - scaleBlock:EnableMouse(true) - local scaleBg = EllesmereUI.SolidTex(scaleBlock, "BACKGROUND", 0, 0, 0, 0) - scaleBg:SetAllPoints() - scaleBlock:SetScript("OnEnter", function() - EllesmereUI.ShowWidgetTooltip(scaleBlock, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + local attrBlock = CreateFrame("Frame", nil, themedCharacterSheetRow._rightRegion) + attrBlock:SetAllPoints(themedCharacterSheetRow._rightRegion) + attrBlock:SetFrameLevel(themedCharacterSheetRow._rightRegion:GetFrameLevel() + 10) + attrBlock:EnableMouse(true) + local attrBg = EllesmereUI.SolidTex(attrBlock, "BACKGROUND", 0, 0, 0, 0) + attrBg:SetAllPoints() + attrBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(attrBlock, EllesmereUI.DisabledTooltip("Themed Character Sheet")) end) - scaleBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + attrBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) EllesmereUI.RegisterWidgetRefresh(function() if themedOff() then - scaleBlock:Show() + attrBlock:Show() + themedCharacterSheetRow._rightRegion:SetAlpha(0.3) else - scaleBlock:Hide() + attrBlock:Hide() + themedCharacterSheetRow._rightRegion:SetAlpha(1) end end) - if themedOff() then scaleBlock:Show() else scaleBlock:Hide() end + if themedOff() then attrBlock:Show() themedCharacterSheetRow._rightRegion:SetAlpha(0.3) else attrBlock:Hide() themedCharacterSheetRow._rightRegion:SetAlpha(1) end end - local colorItemLevelRow - colorItemLevelRow, h = W:DualRow(parent, y, - { type="toggle", text="Color Item Level by Rarity", - tooltip="Colors the item level text based on the item's rarity (Common, Uncommon, Rare, Epic, etc.).", - getValue=function() - return EllesmereUIDB and EllesmereUIDB.charSheetColorItemLevel or false - end, - setValue=function(v) - if not EllesmereUIDB then EllesmereUIDB = {} end - EllesmereUIDB.charSheetColorItemLevel = v - if EllesmereUI._applyCharSheetItemColors then - EllesmereUI._applyCharSheetItemColors() - end - end }, - { type="label", text="" } - ); y = y - h + -- Cogwheel for Character Sheet settings (left) + Color picker for Attributes (right) + do + local function themedOff() + return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) + end + + -- THEMED COGWHEEL (LEFT) + local leftRgn = themedCharacterSheetRow._leftRegion + local _, themedCogShow = EllesmereUI.BuildCogPopup({ + title = "Character Sheet Settings", + rows = { + { type="slider", label="Scale", + min=0.5, max=1.5, step=0.05, + get=function() + return EllesmereUIDB and EllesmereUIDB.themedCharacterSheetScale or 1 + end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.themedCharacterSheetScale = v + if CharacterFrame then + CharacterFrame:SetScale(v) + end + end }, + }, + }) + + local themedCogBtn = CreateFrame("Button", nil, leftRgn) + themedCogBtn:SetSize(26, 26) + themedCogBtn:SetPoint("RIGHT", leftRgn._lastInline or leftRgn._control, "LEFT", -9, 0) + leftRgn._lastInline = themedCogBtn + themedCogBtn:SetFrameLevel(leftRgn:GetFrameLevel() + 5) + themedCogBtn:SetAlpha(themedOff() and 0.15 or 0.4) + local themedCogTex = themedCogBtn:CreateTexture(nil, "OVERLAY") + themedCogTex:SetAllPoints() + themedCogTex:SetTexture(EllesmereUI.COGS_ICON) + themedCogBtn:SetScript("OnEnter", function(self) self:SetAlpha(0.7) end) + themedCogBtn:SetScript("OnLeave", function(self) self:SetAlpha(themedOff() and 0.15 or 0.4) end) + themedCogBtn:SetScript("OnClick", function(self) themedCogShow(self) end) + + local themedCogBlock = CreateFrame("Frame", nil, themedCogBtn) + themedCogBlock:SetAllPoints() + themedCogBlock:SetFrameLevel(themedCogBtn:GetFrameLevel() + 10) + themedCogBlock:EnableMouse(true) + themedCogBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(themedCogBtn, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + end) + themedCogBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + EllesmereUI.RegisterWidgetRefresh(function() + if themedOff() then + themedCogBtn:SetAlpha(0.15) + themedCogBlock:Show() + else + themedCogBtn:SetAlpha(0.4) + themedCogBlock:Hide() + end + end) + if themedOff() then themedCogBtn:SetAlpha(0.15) themedCogBlock:Show() else themedCogBtn:SetAlpha(0.4) themedCogBlock:Hide() end + + -- COLOR PICKER FOR ATTRIBUTES (RIGHT) + local rightRgn = themedCharacterSheetRow._rightRegion + local attrSwGet = function() + local c = EllesmereUIDB and EllesmereUIDB.statCategoryColors and EllesmereUIDB.statCategoryColors.Attributes + if c then return c.r, c.g, c.b, 1 end + return 0.047, 0.824, 0.616, 1 + end + local attrSwSet = function(r, g, b) + if not EllesmereUIDB then EllesmereUIDB = {} end + if not EllesmereUIDB.statCategoryColors then EllesmereUIDB.statCategoryColors = {} end + EllesmereUIDB.statCategoryColors.Attributes = { r = r, g = g, b = b } + if EllesmereUI._refreshCharacterSheetColors then EllesmereUI._refreshCharacterSheetColors() end + end + local attrSwatch, attrUpdateSwatch = EllesmereUI.BuildColorSwatch(rightRgn, rightRgn:GetFrameLevel() + 5, attrSwGet, attrSwSet, false, 20) + PP.Point(attrSwatch, "RIGHT", rightRgn._control, "LEFT", -12, 0) + rightRgn._lastInline = attrSwatch + + EllesmereUI.RegisterWidgetRefresh(function() + if themedOff() then + attrSwatch:SetAlpha(0.3) + attrSwatch:EnableMouse(false) + else + attrSwatch:SetAlpha(1) + attrSwatch:EnableMouse(true) + end + attrUpdateSwatch() + end) + if themedOff() then attrSwatch:SetAlpha(0.3) attrSwatch:EnableMouse(false) else attrSwatch:SetAlpha(1) attrSwatch:EnableMouse(true) end + end local mythicRatingRow mythicRatingRow, h = W:DualRow(parent, y, @@ -3597,141 +3682,223 @@ initFrame:SetScript("OnEvent", function(self) EllesmereUI._updateMythicRatingDisplay() end end }, - { type="label", text="" } + { type="toggle", text="Show Attack", + tooltip="Toggle visibility of the Attack stat category.", + getValue=function() + return EllesmereUIDB and EllesmereUIDB.showStatCategory_Attack ~= false + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.showStatCategory_Attack = v + if EllesmereUI._updateStatCategoryVisibility then + EllesmereUI._updateStatCategoryVisibility() + end + end } ); y = y - h - -- Disabled overlay for Color Item Level by Rarity when themed is off + -- Disabled overlay for mythicRatingRow when themed is off do local function themedOff() return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) end - local colorItemBlock = CreateFrame("Frame", nil, colorItemLevelRow) - colorItemBlock:SetAllPoints(colorItemLevelRow) - colorItemBlock:SetFrameLevel(colorItemLevelRow:GetFrameLevel() + 10) - colorItemBlock:EnableMouse(true) - local colorItemBg = EllesmereUI.SolidTex(colorItemBlock, "BACKGROUND", 0, 0, 0, 0) - colorItemBg:SetAllPoints() - colorItemBlock:SetScript("OnEnter", function() - EllesmereUI.ShowWidgetTooltip(colorItemBlock, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + + local mythicBlock = CreateFrame("Frame", nil, mythicRatingRow) + mythicBlock:SetAllPoints(mythicRatingRow) + mythicBlock:SetFrameLevel(mythicRatingRow:GetFrameLevel() + 10) + mythicBlock:EnableMouse(true) + local mythicBg = EllesmereUI.SolidTex(mythicBlock, "BACKGROUND", 0, 0, 0, 0) + mythicBg:SetAllPoints() + mythicBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(mythicBlock, EllesmereUI.DisabledTooltip("Themed Character Sheet")) end) - colorItemBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + mythicBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + EllesmereUI.RegisterWidgetRefresh(function() - if themedOff() then colorItemBlock:Show() colorItemLevelRow:SetAlpha(0.3) else colorItemBlock:Hide() colorItemLevelRow:SetAlpha(1) end + if themedOff() then + mythicBlock:Show() + mythicRatingRow:SetAlpha(0.3) + else + mythicBlock:Hide() + mythicRatingRow:SetAlpha(1) + end end) - if themedOff() then colorItemBlock:Show() colorItemLevelRow:SetAlpha(0.3) else colorItemBlock:Hide() colorItemLevelRow:SetAlpha(1) end + if themedOff() then mythicBlock:Show() mythicRatingRow:SetAlpha(0.3) else mythicBlock:Hide() mythicRatingRow:SetAlpha(1) end end local itemLevelRow itemLevelRow, h = W:DualRow(parent, y, - { type="slider", text="Item Level Font Size", - min=8, max=16, step=1, - tooltip="Adjusts the font size for item level text on the character sheet.", + { type="toggle", text="Itemlevel", + tooltip="Toggle visibility of item level text on the character sheet.", getValue=function() - return EllesmereUIDB and EllesmereUIDB.charSheetItemLevelSize or 11 + return EllesmereUIDB and EllesmereUIDB.showItemLevel ~= false end, setValue=function(v) if not EllesmereUIDB then EllesmereUIDB = {} end - EllesmereUIDB.charSheetItemLevelSize = v - if EllesmereUI._applyCharSheetTextSizes then - EllesmereUI._applyCharSheetTextSizes() + EllesmereUIDB.showItemLevel = v + if EllesmereUI._refreshItemLevelVisibility then + EllesmereUI._refreshItemLevelVisibility() end end }, - { type="label", text="" } + { type="toggle", text="Show Crests", + tooltip="Toggle visibility of the Crests stat category.", + getValue=function() + return EllesmereUIDB and EllesmereUIDB.showStatCategory_Crests ~= false + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.showStatCategory_Crests = v + if EllesmereUI._updateStatCategoryVisibility then + EllesmereUI._updateStatCategoryVisibility() + end + end } ); y = y - h - -- Cogwheel for item level text effects (shadow and outline) + -- Disabled overlay for itemLevelRow when themed is off do local function themedOff() return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) end - local leftRgn = itemLevelRow._leftRegion - - local _, itemLevelCogShow = EllesmereUI.BuildCogPopup({ - title = "Item Level Text Effects", - rows = { - { type="toggle", label="Font Shadow", - get=function() return EllesmereUIDB and EllesmereUIDB.charSheetItemLevelShadow or false end, - set=function(v) - if not EllesmereUIDB then EllesmereUIDB = {} end - EllesmereUIDB.charSheetItemLevelShadow = v - if EllesmereUI._applyCharSheetTextSizes then - EllesmereUI._applyCharSheetTextSizes() - end - end }, - { type="toggle", label="Font Outline", - get=function() return EllesmereUIDB and EllesmereUIDB.charSheetItemLevelOutline or false end, - set=function(v) - if not EllesmereUIDB then EllesmereUIDB = {} end - EllesmereUIDB.charSheetItemLevelOutline = v - if EllesmereUI._applyCharSheetTextSizes then - EllesmereUI._applyCharSheetTextSizes() - end - end }, - }, - }) - local itemLevelCogBtn = CreateFrame("Button", nil, leftRgn) - itemLevelCogBtn:SetSize(26, 26) - itemLevelCogBtn:SetPoint("RIGHT", leftRgn._lastInline or leftRgn._control, "LEFT", -9, 0) - leftRgn._lastInline = itemLevelCogBtn - itemLevelCogBtn:SetFrameLevel(leftRgn:GetFrameLevel() + 5) - itemLevelCogBtn:SetAlpha(themedOff() and 0.15 or 0.4) - local itemLevelCogTex = itemLevelCogBtn:CreateTexture(nil, "OVERLAY") - itemLevelCogTex:SetAllPoints() - itemLevelCogTex:SetTexture(EllesmereUI.COGS_ICON) - itemLevelCogBtn:SetScript("OnEnter", function(self) self:SetAlpha(0.7) end) - itemLevelCogBtn:SetScript("OnLeave", function(self) self:SetAlpha(themedOff() and 0.15 or 0.4) end) - itemLevelCogBtn:SetScript("OnClick", function(self) itemLevelCogShow(self) end) + local itemLevelBlock = CreateFrame("Frame", nil, itemLevelRow) + itemLevelBlock:SetAllPoints(itemLevelRow) + itemLevelBlock:SetFrameLevel(itemLevelRow:GetFrameLevel() + 10) + itemLevelBlock:EnableMouse(true) + local itemLevelBg = EllesmereUI.SolidTex(itemLevelBlock, "BACKGROUND", 0, 0, 0, 0) + itemLevelBg:SetAllPoints() + itemLevelBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(itemLevelBlock, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + end) + itemLevelBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) - local itemLevelCogBlock = CreateFrame("Frame", nil, itemLevelCogBtn) - itemLevelCogBlock:SetAllPoints() - itemLevelCogBlock:SetFrameLevel(itemLevelCogBtn:GetFrameLevel() + 10) - itemLevelCogBlock:EnableMouse(true) - itemLevelCogBlock:SetScript("OnEnter", function() - EllesmereUI.ShowWidgetTooltip(itemLevelCogBtn, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + EllesmereUI.RegisterWidgetRefresh(function() + if themedOff() then + itemLevelBlock:Show() + itemLevelRow:SetAlpha(0.3) + else + itemLevelBlock:Hide() + itemLevelRow:SetAlpha(1) + end end) - itemLevelCogBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + if themedOff() then itemLevelBlock:Show() itemLevelRow:SetAlpha(0.3) else itemLevelBlock:Hide() itemLevelRow:SetAlpha(1) end + end + + -- Cogwheel for item level (left) + Color picker for Crests (right) + do + local function themedOff() + return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) + end + + -- ITEMLEVEL COGWHEEL (LEFT) - already in itemLevelRow above + -- Color picker for Crests (right side) + local rightRgn = itemLevelRow._rightRegion + local crestsSwGet = function() + local c = EllesmereUIDB and EllesmereUIDB.statCategoryColors and EllesmereUIDB.statCategoryColors.Crests + if c then return c.r, c.g, c.b, 1 end + return 1, 0.784, 0.341, 1 + end + local crestsSwSet = function(r, g, b) + if not EllesmereUIDB then EllesmereUIDB = {} end + if not EllesmereUIDB.statCategoryColors then EllesmereUIDB.statCategoryColors = {} end + EllesmereUIDB.statCategoryColors.Crests = { r = r, g = g, b = b } + if EllesmereUI._refreshCharacterSheetColors then EllesmereUI._refreshCharacterSheetColors() end + end + local crestsSwatch, crestsUpdateSwatch = EllesmereUI.BuildColorSwatch(rightRgn, rightRgn:GetFrameLevel() + 5, crestsSwGet, crestsSwSet, false, 20) + PP.Point(crestsSwatch, "RIGHT", rightRgn._control, "LEFT", -12, 0) + rightRgn._lastInline = crestsSwatch EllesmereUI.RegisterWidgetRefresh(function() if themedOff() then - itemLevelCogBtn:SetAlpha(0.15) - itemLevelCogBlock:Show() + crestsSwatch:SetAlpha(0.3) + crestsSwatch:EnableMouse(false) else - itemLevelCogBtn:SetAlpha(0.4) - itemLevelCogBlock:Hide() + crestsSwatch:SetAlpha(1) + crestsSwatch:EnableMouse(true) end + crestsUpdateSwatch() end) - if themedOff() then itemLevelCogBtn:SetAlpha(0.15) itemLevelCogBlock:Show() else itemLevelCogBtn:SetAlpha(0.4) itemLevelCogBlock:Hide() end + if themedOff() then crestsSwatch:SetAlpha(0.3) crestsSwatch:EnableMouse(false) else crestsSwatch:SetAlpha(1) crestsSwatch:EnableMouse(true) end end local upgradeTrackRow upgradeTrackRow, h = W:DualRow(parent, y, - { type="slider", text="Upgrade Track Font Size", - min=8, max=16, step=1, - tooltip="Adjusts the font size for upgrade track text on the character sheet.", + { type="toggle", text="Upgradetrack", + tooltip="Toggle visibility of upgrade track text on the character sheet.", getValue=function() - return EllesmereUIDB and EllesmereUIDB.charSheetUpgradeTrackSize or 11 + return EllesmereUIDB and EllesmereUIDB.showUpgradeTrack ~= false end, setValue=function(v) if not EllesmereUIDB then EllesmereUIDB = {} end - EllesmereUIDB.charSheetUpgradeTrackSize = v - if EllesmereUI._applyCharSheetTextSizes then - EllesmereUI._applyCharSheetTextSizes() + EllesmereUIDB.showUpgradeTrack = v + if EllesmereUI._refreshUpgradeTrackVisibility then + EllesmereUI._refreshUpgradeTrackVisibility() end end }, - { type="label", text="" } + { type="toggle", text="Show Secondary Stats", + tooltip="Toggle visibility of the Secondary Stats category.", + getValue=function() + return EllesmereUIDB and EllesmereUIDB.showStatCategory_SecondaryStats ~= false + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.showStatCategory_SecondaryStats = v + if EllesmereUI._updateStatCategoryVisibility then + EllesmereUI._updateStatCategoryVisibility() + end + end } ); y = y - h - -- Cogwheel for upgrade track text effects (shadow and outline) + -- Disabled overlay for upgradeTrackRow when themed is off do local function themedOff() return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) end - local leftRgn = upgradeTrackRow._leftRegion + local upgradeTrackBlock = CreateFrame("Frame", nil, upgradeTrackRow) + upgradeTrackBlock:SetAllPoints(upgradeTrackRow) + upgradeTrackBlock:SetFrameLevel(upgradeTrackRow:GetFrameLevel() + 10) + upgradeTrackBlock:EnableMouse(true) + local upgradeTrackBg = EllesmereUI.SolidTex(upgradeTrackBlock, "BACKGROUND", 0, 0, 0, 0) + upgradeTrackBg:SetAllPoints() + upgradeTrackBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(upgradeTrackBlock, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + end) + upgradeTrackBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + EllesmereUI.RegisterWidgetRefresh(function() + if themedOff() then + upgradeTrackBlock:Show() + upgradeTrackRow:SetAlpha(0.3) + else + upgradeTrackBlock:Hide() + upgradeTrackRow:SetAlpha(1) + end + end) + if themedOff() then upgradeTrackBlock:Show() upgradeTrackRow:SetAlpha(0.3) else upgradeTrackBlock:Hide() upgradeTrackRow:SetAlpha(1) end + end + + -- Cogwheel for upgrade track (left) + Color picker for Secondary Stats (right) + do + local function themedOff() + return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) + end + + -- UPGRADETRACK COGWHEEL (LEFT) - moved here from itemLevelRow + local leftRgn = upgradeTrackRow._leftRegion local _, upgradeTrackCogShow = EllesmereUI.BuildCogPopup({ - title = "Upgrade Track Text Effects", + title = "Upgradetrack Settings", rows = { + { type="slider", label="Font Size", + min=8, max=16, step=1, + get=function() + return EllesmereUIDB and EllesmereUIDB.charSheetUpgradeTrackSize or 11 + end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetUpgradeTrackSize = v + if EllesmereUI._applyCharSheetTextSizes then + EllesmereUI._applyCharSheetTextSizes() + end + end }, { type="toggle", label="Font Shadow", get=function() return EllesmereUIDB and EllesmereUIDB.charSheetUpgradeTrackShadow or false end, set=function(v) @@ -3750,6 +3917,16 @@ initFrame:SetScript("OnEvent", function(self) EllesmereUI._applyCharSheetTextSizes() end end }, + { type="toggle", label="Use Custom Color", + get=function() return EllesmereUIDB and EllesmereUIDB.charSheetUpgradeTrackUseColor or false end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetUpgradeTrackUseColor = v + if EllesmereUI._refreshUpgradeTrackColors then + EllesmereUI._refreshUpgradeTrackColors() + end + EllesmereUI:RefreshPage() + end }, }, }) @@ -3775,282 +3952,433 @@ initFrame:SetScript("OnEvent", function(self) end) upgradeTrackCogBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + local upgradeTrackSwGet = function() + local c = EllesmereUIDB and EllesmereUIDB.charSheetUpgradeTrackColor + if c then return c.r, c.g, c.b, 1 end + return 1, 1, 1, 1 + end + local upgradeTrackSwSet = function(r, g, b) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetUpgradeTrackColor = { r = r, g = g, b = b } + if EllesmereUI._refreshUpgradeTrackColors then + EllesmereUI._refreshUpgradeTrackColors() + end + end + local upgradeTrackSwatch, upgradeTrackUpdateSwatch = EllesmereUI.BuildColorSwatch(leftRgn, leftRgn:GetFrameLevel() + 5, upgradeTrackSwGet, upgradeTrackSwSet, false, 20) + PP.Point(upgradeTrackSwatch, "RIGHT", upgradeTrackCogBtn, "LEFT", -9, 0) + leftRgn._lastInline = upgradeTrackSwatch + EllesmereUI.RegisterWidgetRefresh(function() + local colorEnabled = EllesmereUIDB and EllesmereUIDB.charSheetUpgradeTrackUseColor if themedOff() then upgradeTrackCogBtn:SetAlpha(0.15) upgradeTrackCogBlock:Show() + upgradeTrackSwatch:SetAlpha(0.15) + upgradeTrackSwatch:EnableMouse(false) else upgradeTrackCogBtn:SetAlpha(0.4) upgradeTrackCogBlock:Hide() + upgradeTrackSwatch:SetAlpha(colorEnabled and 1 or 0.3) + upgradeTrackSwatch:EnableMouse(colorEnabled) end + upgradeTrackUpdateSwatch() end) - if themedOff() then upgradeTrackCogBtn:SetAlpha(0.15) upgradeTrackCogBlock:Show() else upgradeTrackCogBtn:SetAlpha(0.4) upgradeTrackCogBlock:Hide() end - end + local colorEnabled = EllesmereUIDB and EllesmereUIDB.charSheetUpgradeTrackUseColor + if themedOff() then + upgradeTrackCogBtn:SetAlpha(0.15) + upgradeTrackCogBlock:Show() + upgradeTrackSwatch:SetAlpha(0.15) + upgradeTrackSwatch:EnableMouse(false) + else + upgradeTrackCogBtn:SetAlpha(0.4) + upgradeTrackCogBlock:Hide() + upgradeTrackSwatch:SetAlpha(colorEnabled and 1 or 0.3) + upgradeTrackSwatch:EnableMouse(colorEnabled) + end - local enchantRow - enchantRow, h = W:DualRow(parent, y, - { type="slider", text="Enchant Font Size", - min=8, max=12, step=1, - tooltip="Adjusts the font size for enchant text on the character sheet.", - getValue=function() - return EllesmereUIDB and EllesmereUIDB.charSheetEnchantSize or 9 - end, - setValue=function(v) - if not EllesmereUIDB then EllesmereUIDB = {} end - EllesmereUIDB.charSheetEnchantSize = v - if EllesmereUI._applyCharSheetTextSizes then - EllesmereUI._applyCharSheetTextSizes() - end - end }, - { type="label", text="" } - ); y = y - h + -- COLOR PICKER FOR SECONDARY STATS (RIGHT) + local rightRgn = upgradeTrackRow._rightRegion + local secondarySwGet = function() + local c = EllesmereUIDB and EllesmereUIDB.statCategoryColors and EllesmereUIDB.statCategoryColors.SecondaryStats + if c then return c.r, c.g, c.b, 1 end + return 0.157, 1, 0.949, 1 + end + local secondarySwSet = function(r, g, b) + if not EllesmereUIDB then EllesmereUIDB = {} end + if not EllesmereUIDB.statCategoryColors then EllesmereUIDB.statCategoryColors = {} end + EllesmereUIDB.statCategoryColors.SecondaryStats = { r = r, g = g, b = b } + if EllesmereUI._refreshCharacterSheetColors then EllesmereUI._refreshCharacterSheetColors() end + end + local secondarySwatch, secondaryUpdateSwatch = EllesmereUI.BuildColorSwatch(rightRgn, rightRgn:GetFrameLevel() + 5, secondarySwGet, secondarySwSet, false, 20) + PP.Point(secondarySwatch, "RIGHT", rightRgn._control, "LEFT", -12, 0) + rightRgn._lastInline = secondarySwatch + + EllesmereUI.RegisterWidgetRefresh(function() + if themedOff() then + secondarySwatch:SetAlpha(0.3) + secondarySwatch:EnableMouse(false) + else + secondarySwatch:SetAlpha(1) + secondarySwatch:EnableMouse(true) + end + secondaryUpdateSwatch() + end) + if themedOff() then secondarySwatch:SetAlpha(0.3) secondarySwatch:EnableMouse(false) else secondarySwatch:SetAlpha(1) secondarySwatch:EnableMouse(true) end + end - -- Cogwheel for enchant text effects (shadow and outline) + -- Cogwheel for item level settings (left) and upgrade track settings (right) do local function themedOff() return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) end - local leftRgn = enchantRow._leftRegion - local _, enchantCogShow = EllesmereUI.BuildCogPopup({ - title = "Enchant Text Effects", + -- ITEMLEVEL COGWHEEL (LEFT) + local leftRgn = itemLevelRow._leftRegion + local _, itemLevelCogShow = EllesmereUI.BuildCogPopup({ + title = "Itemlevel Settings", rows = { + { type="slider", label="Font Size", + min=8, max=16, step=1, + get=function() + return EllesmereUIDB and EllesmereUIDB.charSheetItemLevelSize or 11 + end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetItemLevelSize = v + if EllesmereUI._applyCharSheetTextSizes then + EllesmereUI._applyCharSheetTextSizes() + end + end }, { type="toggle", label="Font Shadow", - get=function() return EllesmereUIDB and EllesmereUIDB.charSheetEnchantShadow or false end, + get=function() return EllesmereUIDB and EllesmereUIDB.charSheetItemLevelShadow or false end, set=function(v) if not EllesmereUIDB then EllesmereUIDB = {} end - EllesmereUIDB.charSheetEnchantShadow = v + EllesmereUIDB.charSheetItemLevelShadow = v if EllesmereUI._applyCharSheetTextSizes then EllesmereUI._applyCharSheetTextSizes() end end }, { type="toggle", label="Font Outline", - get=function() return EllesmereUIDB and EllesmereUIDB.charSheetEnchantOutline or false end, + get=function() return EllesmereUIDB and EllesmereUIDB.charSheetItemLevelOutline or false end, set=function(v) if not EllesmereUIDB then EllesmereUIDB = {} end - EllesmereUIDB.charSheetEnchantOutline = v + EllesmereUIDB.charSheetItemLevelOutline = v if EllesmereUI._applyCharSheetTextSizes then EllesmereUI._applyCharSheetTextSizes() end end }, + { type="toggle", label="Use Custom Color", + get=function() return EllesmereUIDB and EllesmereUIDB.charSheetItemLevelUseColor or false end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetItemLevelUseColor = v + if EllesmereUI._refreshItemLevelColors then + EllesmereUI._refreshItemLevelColors() + end + EllesmereUI:RefreshPage() + end }, }, }) - local enchantCogBtn = CreateFrame("Button", nil, leftRgn) - enchantCogBtn:SetSize(26, 26) - enchantCogBtn:SetPoint("RIGHT", leftRgn._lastInline or leftRgn._control, "LEFT", -9, 0) - leftRgn._lastInline = enchantCogBtn - enchantCogBtn:SetFrameLevel(leftRgn:GetFrameLevel() + 5) - enchantCogBtn:SetAlpha(themedOff() and 0.15 or 0.4) - local enchantCogTex = enchantCogBtn:CreateTexture(nil, "OVERLAY") - enchantCogTex:SetAllPoints() - enchantCogTex:SetTexture(EllesmereUI.COGS_ICON) - enchantCogBtn:SetScript("OnEnter", function(self) self:SetAlpha(0.7) end) - enchantCogBtn:SetScript("OnLeave", function(self) self:SetAlpha(themedOff() and 0.15 or 0.4) end) - enchantCogBtn:SetScript("OnClick", function(self) enchantCogShow(self) end) + local itemLevelCogBtn = CreateFrame("Button", nil, leftRgn) + itemLevelCogBtn:SetSize(26, 26) + itemLevelCogBtn:SetPoint("RIGHT", leftRgn._lastInline or leftRgn._control, "LEFT", -9, 0) + leftRgn._lastInline = itemLevelCogBtn + itemLevelCogBtn:SetFrameLevel(leftRgn:GetFrameLevel() + 5) + itemLevelCogBtn:SetAlpha(themedOff() and 0.15 or 0.4) + local itemLevelCogTex = itemLevelCogBtn:CreateTexture(nil, "OVERLAY") + itemLevelCogTex:SetAllPoints() + itemLevelCogTex:SetTexture(EllesmereUI.COGS_ICON) + itemLevelCogBtn:SetScript("OnEnter", function(self) self:SetAlpha(0.7) end) + itemLevelCogBtn:SetScript("OnLeave", function(self) self:SetAlpha(themedOff() and 0.15 or 0.4) end) + itemLevelCogBtn:SetScript("OnClick", function(self) itemLevelCogShow(self) end) - local enchantCogBlock = CreateFrame("Frame", nil, enchantCogBtn) - enchantCogBlock:SetAllPoints() - enchantCogBlock:SetFrameLevel(enchantCogBtn:GetFrameLevel() + 10) - enchantCogBlock:EnableMouse(true) - enchantCogBlock:SetScript("OnEnter", function() - EllesmereUI.ShowWidgetTooltip(enchantCogBtn, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + local itemLevelCogBlock = CreateFrame("Frame", nil, itemLevelCogBtn) + itemLevelCogBlock:SetAllPoints() + itemLevelCogBlock:SetFrameLevel(itemLevelCogBtn:GetFrameLevel() + 10) + itemLevelCogBlock:EnableMouse(true) + itemLevelCogBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(itemLevelCogBtn, EllesmereUI.DisabledTooltip("Themed Character Sheet")) end) - enchantCogBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + itemLevelCogBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + local itemLevelSwGet = function() + local c = EllesmereUIDB and EllesmereUIDB.charSheetItemLevelColor + if c then return c.r, c.g, c.b, 1 end + return 1, 1, 1, 1 + end + local itemLevelSwSet = function(r, g, b) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetItemLevelColor = { r = r, g = g, b = b } + if EllesmereUI._refreshItemLevelColors then + EllesmereUI._refreshItemLevelColors() + end + end + local itemLevelSwatch, itemLevelUpdateSwatch + itemLevelSwatch, itemLevelUpdateSwatch = EllesmereUI.BuildColorSwatch(leftRgn, leftRgn:GetFrameLevel() + 5, itemLevelSwGet, itemLevelSwSet, false, 20) + PP.Point(itemLevelSwatch, "RIGHT", itemLevelCogBtn, "LEFT", -9, 0) + leftRgn._lastInline = itemLevelSwatch EllesmereUI.RegisterWidgetRefresh(function() + local colorEnabled = EllesmereUIDB and EllesmereUIDB.charSheetItemLevelUseColor if themedOff() then - enchantCogBtn:SetAlpha(0.15) - enchantCogBlock:Show() + itemLevelCogBtn:SetAlpha(0.15) + itemLevelCogBlock:Show() + itemLevelSwatch:SetAlpha(0.15) + itemLevelSwatch:EnableMouse(false) else - enchantCogBtn:SetAlpha(0.4) - enchantCogBlock:Hide() + itemLevelCogBtn:SetAlpha(0.4) + itemLevelCogBlock:Hide() + itemLevelSwatch:SetAlpha(colorEnabled and 1 or 0.3) + itemLevelSwatch:EnableMouse(colorEnabled) end + itemLevelUpdateSwatch() end) - if themedOff() then enchantCogBtn:SetAlpha(0.15) enchantCogBlock:Show() else enchantCogBtn:SetAlpha(0.4) enchantCogBlock:Hide() end - end - - -- Disabled overlay for enchant row when themed is off - do - local function themedOff() - return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) + local colorEnabled = EllesmereUIDB and EllesmereUIDB.charSheetItemLevelUseColor + if themedOff() then + itemLevelCogBtn:SetAlpha(0.15) + itemLevelCogBlock:Show() + itemLevelSwatch:SetAlpha(0.15) + itemLevelSwatch:EnableMouse(false) + else + itemLevelCogBtn:SetAlpha(0.4) + itemLevelCogBlock:Hide() + itemLevelSwatch:SetAlpha(colorEnabled and 1 or 0.3) + itemLevelSwatch:EnableMouse(colorEnabled) end - local enchantBlock = CreateFrame("Frame", nil, enchantRow) - enchantBlock:SetAllPoints(enchantRow) - enchantBlock:SetFrameLevel(enchantRow:GetFrameLevel() + 10) - enchantBlock:EnableMouse(true) - local enchantBg = EllesmereUI.SolidTex(enchantBlock, "BACKGROUND", 0, 0, 0, 0) - enchantBg:SetAllPoints() - enchantBlock:SetScript("OnEnter", function() - EllesmereUI.ShowWidgetTooltip(enchantBlock, EllesmereUI.DisabledTooltip("Themed Character Sheet")) - end) - enchantBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) - EllesmereUI.RegisterWidgetRefresh(function() - if themedOff() then enchantBlock:Show() enchantRow:SetAlpha(0.3) else enchantBlock:Hide() enchantRow:SetAlpha(1) end - end) - if themedOff() then enchantBlock:Show() enchantRow:SetAlpha(0.3) else enchantBlock:Hide() enchantRow:SetAlpha(1) end end - -- Stat Category Toggles - _, h = W:Spacer(parent, y, 5); y = y - h - - local categoryRow1, h1 = W:DualRow(parent, y, - { type="toggle", text="Show Attributes", - tooltip="Toggle visibility of the Attributes stat category.", + local enchantRow + enchantRow, h = W:DualRow(parent, y, + { type="toggle", text="Enchants", + tooltip="Toggle visibility of enchant text on the character sheet.", getValue=function() - return EllesmereUIDB and EllesmereUIDB.showStatCategory_Attributes ~= false + return EllesmereUIDB and EllesmereUIDB.showEnchants ~= false end, setValue=function(v) if not EllesmereUIDB then EllesmereUIDB = {} end - EllesmereUIDB.showStatCategory_Attributes = v - if EllesmereUI._updateStatCategoryVisibility then - EllesmereUI._updateStatCategoryVisibility() + EllesmereUIDB.showEnchants = v + if EllesmereUI._refreshEnchantsVisibility then + EllesmereUI._refreshEnchantsVisibility() end end }, - { type="toggle", text="Show Secondary Stats", - tooltip="Toggle visibility of the Secondary Stats category.", + { type="toggle", text="Show Defense", + tooltip="Toggle visibility of the Defense stat category.", getValue=function() - return EllesmereUIDB and EllesmereUIDB.showStatCategory_SecondaryStats ~= false + return EllesmereUIDB and EllesmereUIDB.showStatCategory_Defense ~= false end, setValue=function(v) if not EllesmereUIDB then EllesmereUIDB = {} end - EllesmereUIDB.showStatCategory_SecondaryStats = v + EllesmereUIDB.showStatCategory_Defense = v if EllesmereUI._updateStatCategoryVisibility then EllesmereUI._updateStatCategoryVisibility() end end } - ); y = y - h1 + ); y = y - h - -- Disabled overlay for categoryRow1 when themed is off + -- Disabled overlay for enchantRow when themed is off do local function themedOff() return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) end - local categoryBlock1 = CreateFrame("Frame", nil, categoryRow1) - categoryBlock1:SetAllPoints(categoryRow1) - categoryBlock1:SetFrameLevel(categoryRow1:GetFrameLevel() + 10) - categoryBlock1:EnableMouse(true) - local categoryBg1 = EllesmereUI.SolidTex(categoryBlock1, "BACKGROUND", 0, 0, 0, 0) - categoryBg1:SetAllPoints() - categoryBlock1:SetScript("OnEnter", function() - EllesmereUI.ShowWidgetTooltip(categoryBlock1, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + local enchantBlock = CreateFrame("Frame", nil, enchantRow) + enchantBlock:SetAllPoints(enchantRow) + enchantBlock:SetFrameLevel(enchantRow:GetFrameLevel() + 10) + enchantBlock:EnableMouse(true) + local enchantBg = EllesmereUI.SolidTex(enchantBlock, "BACKGROUND", 0, 0, 0, 0) + enchantBg:SetAllPoints() + enchantBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(enchantBlock, EllesmereUI.DisabledTooltip("Themed Character Sheet")) end) - categoryBlock1:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + enchantBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) EllesmereUI.RegisterWidgetRefresh(function() if themedOff() then - categoryBlock1:Show() - categoryRow1:SetAlpha(0.3) + enchantBlock:Show() + enchantRow:SetAlpha(0.3) else - categoryBlock1:Hide() - categoryRow1:SetAlpha(1) + enchantBlock:Hide() + enchantRow:SetAlpha(1) end end) - if themedOff() then categoryBlock1:Show() categoryRow1:SetAlpha(0.3) else categoryBlock1:Hide() categoryRow1:SetAlpha(1) end + if themedOff() then enchantBlock:Show() enchantRow:SetAlpha(0.3) else enchantBlock:Hide() enchantRow:SetAlpha(1) end end - local categoryRow2, h2 = W:DualRow(parent, y, - { type="toggle", text="Show Attack", - tooltip="Toggle visibility of the Attack stat category.", - getValue=function() - return EllesmereUIDB and EllesmereUIDB.showStatCategory_Attack ~= false - end, - setValue=function(v) - if not EllesmereUIDB then EllesmereUIDB = {} end - EllesmereUIDB.showStatCategory_Attack = v - if EllesmereUI._updateStatCategoryVisibility then - EllesmereUI._updateStatCategoryVisibility() - end - end }, - { type="toggle", text="Show Defense", - tooltip="Toggle visibility of the Defense stat category.", - getValue=function() - return EllesmereUIDB and EllesmereUIDB.showStatCategory_Defense ~= false - end, - setValue=function(v) - if not EllesmereUIDB then EllesmereUIDB = {} end - EllesmereUIDB.showStatCategory_Defense = v - if EllesmereUI._updateStatCategoryVisibility then - EllesmereUI._updateStatCategoryVisibility() - end - end } - ); y = y - h2 - - -- Disabled overlay for categoryRow2 when themed is off + -- Color picker for Defense (right side) do local function themedOff() return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) end - - local categoryBlock2 = CreateFrame("Frame", nil, categoryRow2) - categoryBlock2:SetAllPoints(categoryRow2) - categoryBlock2:SetFrameLevel(categoryRow2:GetFrameLevel() + 10) - categoryBlock2:EnableMouse(true) - local categoryBg2 = EllesmereUI.SolidTex(categoryBlock2, "BACKGROUND", 0, 0, 0, 0) - categoryBg2:SetAllPoints() - categoryBlock2:SetScript("OnEnter", function() - EllesmereUI.ShowWidgetTooltip(categoryBlock2, EllesmereUI.DisabledTooltip("Themed Character Sheet")) - end) - categoryBlock2:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + local rightRgn = enchantRow._rightRegion + local defenseSwGet = function() + local c = EllesmereUIDB and EllesmereUIDB.statCategoryColors and EllesmereUIDB.statCategoryColors.Defense + if c then return c.r, c.g, c.b, 1 end + return 0.247, 0.655, 1, 1 + end + local defenseSwSet = function(r, g, b) + if not EllesmereUIDB then EllesmereUIDB = {} end + if not EllesmereUIDB.statCategoryColors then EllesmereUIDB.statCategoryColors = {} end + EllesmereUIDB.statCategoryColors.Defense = { r = r, g = g, b = b } + if EllesmereUI._refreshCharacterSheetColors then EllesmereUI._refreshCharacterSheetColors() end + end + local defenseSwatch, defenseUpdateSwatch = EllesmereUI.BuildColorSwatch(rightRgn, rightRgn:GetFrameLevel() + 5, defenseSwGet, defenseSwSet, false, 20) + PP.Point(defenseSwatch, "RIGHT", rightRgn._control, "LEFT", -12, 0) + rightRgn._lastInline = defenseSwatch EllesmereUI.RegisterWidgetRefresh(function() if themedOff() then - categoryBlock2:Show() - categoryRow2:SetAlpha(0.3) + defenseSwatch:SetAlpha(0.3) + defenseSwatch:EnableMouse(false) else - categoryBlock2:Hide() - categoryRow2:SetAlpha(1) + defenseSwatch:SetAlpha(1) + defenseSwatch:EnableMouse(true) end + defenseUpdateSwatch() end) - if themedOff() then categoryBlock2:Show() categoryRow2:SetAlpha(0.3) else categoryBlock2:Hide() categoryRow2:SetAlpha(1) end + if themedOff() then defenseSwatch:SetAlpha(0.3) defenseSwatch:EnableMouse(false) else defenseSwatch:SetAlpha(1) defenseSwatch:EnableMouse(true) end end - local categoryRow3, h3 = W:DualRow(parent, y, - { type="toggle", text="Show Crests", - tooltip="Toggle visibility of the Crests stat category.", - getValue=function() - return EllesmereUIDB and EllesmereUIDB.showStatCategory_Crests ~= false - end, - setValue=function(v) - if not EllesmereUIDB then EllesmereUIDB = {} end - EllesmereUIDB.showStatCategory_Crests = v - if EllesmereUI._updateStatCategoryVisibility then - EllesmereUI._updateStatCategoryVisibility() - end - end }, - { type="label", text="" } - ); y = y - h3 - - -- Disabled overlay for categoryRow3 when themed is off + -- Cogwheel for enchant settings do local function themedOff() return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) end + local leftRgn = enchantRow._leftRegion + + local _, enchantCogShow = EllesmereUI.BuildCogPopup({ + title = "Enchants Settings", + rows = { + { type="slider", label="Font Size", + min=8, max=12, step=1, + get=function() + return EllesmereUIDB and EllesmereUIDB.charSheetEnchantSize or 9 + end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetEnchantSize = v + if EllesmereUI._applyCharSheetTextSizes then + EllesmereUI._applyCharSheetTextSizes() + end + end }, + { type="toggle", label="Font Shadow", + get=function() return EllesmereUIDB and EllesmereUIDB.charSheetEnchantShadow or false end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetEnchantShadow = v + if EllesmereUI._applyCharSheetTextSizes then + EllesmereUI._applyCharSheetTextSizes() + end + end }, + { type="toggle", label="Font Outline", + get=function() return EllesmereUIDB and EllesmereUIDB.charSheetEnchantOutline or false end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetEnchantOutline = v + if EllesmereUI._applyCharSheetTextSizes then + EllesmereUI._applyCharSheetTextSizes() + end + end }, + { type="toggle", label="Use Custom Color", + get=function() return EllesmereUIDB and EllesmereUIDB.charSheetEnchantUseColor or false end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetEnchantUseColor = v + if EllesmereUI._refreshEnchantsColors then + EllesmereUI._refreshEnchantsColors() + end + EllesmereUI:RefreshPage() + end }, + }, + }) + + -- Cogwheel button + local enchantCogBtn = CreateFrame("Button", nil, leftRgn) + enchantCogBtn:SetSize(26, 26) + enchantCogBtn:SetPoint("RIGHT", leftRgn._lastInline or leftRgn._control, "LEFT", -9, 0) + leftRgn._lastInline = enchantCogBtn + enchantCogBtn:SetFrameLevel(leftRgn:GetFrameLevel() + 5) + enchantCogBtn:SetAlpha(themedOff() and 0.15 or 0.4) + local enchantCogTex = enchantCogBtn:CreateTexture(nil, "OVERLAY") + enchantCogTex:SetAllPoints() + enchantCogTex:SetTexture(EllesmereUI.COGS_ICON) + enchantCogBtn:SetScript("OnEnter", function(self) self:SetAlpha(0.7) end) + enchantCogBtn:SetScript("OnLeave", function(self) self:SetAlpha(themedOff() and 0.15 or 0.4) end) + enchantCogBtn:SetScript("OnClick", function(self) enchantCogShow(self) end) - local categoryBlock3 = CreateFrame("Frame", nil, categoryRow3) - categoryBlock3:SetAllPoints(categoryRow3) - categoryBlock3:SetFrameLevel(categoryRow3:GetFrameLevel() + 10) - categoryBlock3:EnableMouse(true) - local categoryBg3 = EllesmereUI.SolidTex(categoryBlock3, "BACKGROUND", 0, 0, 0, 0) - categoryBg3:SetAllPoints() - categoryBlock3:SetScript("OnEnter", function() - EllesmereUI.ShowWidgetTooltip(categoryBlock3, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + local enchantCogBlock = CreateFrame("Frame", nil, enchantCogBtn) + enchantCogBlock:SetAllPoints() + enchantCogBlock:SetFrameLevel(enchantCogBtn:GetFrameLevel() + 10) + enchantCogBlock:EnableMouse(true) + enchantCogBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(enchantCogBtn, EllesmereUI.DisabledTooltip("Themed Character Sheet")) end) - categoryBlock3:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + enchantCogBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + -- Color swatch (shown when custom color is enabled) + local enchantSwGet = function() + local c = EllesmereUIDB and EllesmereUIDB.charSheetEnchantColor + if c then return c.r, c.g, c.b, 1 end + return 1, 1, 1, 1 + end + local enchantSwSet = function(r, g, b) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.charSheetEnchantColor = { r = r, g = g, b = b } + -- Refresh all item slots to update enchant colors + local itemSlots = { + "CharacterHeadSlot", "CharacterNeckSlot", "CharacterShoulderSlot", "CharacterBackSlot", + "CharacterChestSlot", "CharacterWaistSlot", "CharacterLegsSlot", "CharacterFeetSlot", + "CharacterWristSlot", "CharacterHandsSlot", "CharacterFinger0Slot", "CharacterFinger1Slot", + "CharacterTrinket0Slot", "CharacterTrinket1Slot", "CharacterMainHandSlot", "CharacterSecondaryHandSlot" + } + for _, slotName in ipairs(itemSlots) do + local slot = _G[slotName] + if slot and slot._enchantLabel then + local displayColor + if EllesmereUIDB and EllesmereUIDB.charSheetEnchantUseColor and EllesmereUIDB.charSheetEnchantColor then + displayColor = EllesmereUIDB.charSheetEnchantColor + else + displayColor = { r = 1, g = 1, b = 1 } + end + slot._enchantLabel:SetTextColor(displayColor.r, displayColor.g, displayColor.b, 1) + end + end + end + local enchantSwatch, enchantUpdateSwatch + enchantSwatch, enchantUpdateSwatch = EllesmereUI.BuildColorSwatch(leftRgn, leftRgn:GetFrameLevel() + 5, enchantSwGet, enchantSwSet, false, 20) + PP.Point(enchantSwatch, "RIGHT", enchantCogBtn, "LEFT", -9, 0) + leftRgn._lastInline = enchantSwatch EllesmereUI.RegisterWidgetRefresh(function() + local colorEnabled = EllesmereUIDB and EllesmereUIDB.charSheetEnchantUseColor if themedOff() then - categoryBlock3:Show() - categoryRow3:SetAlpha(0.3) + enchantCogBtn:SetAlpha(0.15) + enchantCogBlock:Show() + enchantSwatch:SetAlpha(0.15) + enchantSwatch:EnableMouse(false) else - categoryBlock3:Hide() - categoryRow3:SetAlpha(1) + enchantCogBtn:SetAlpha(0.4) + enchantCogBlock:Hide() + enchantSwatch:SetAlpha(colorEnabled and 1 or 0.3) + enchantSwatch:EnableMouse(colorEnabled) end + enchantUpdateSwatch() end) - if themedOff() then categoryBlock3:Show() categoryRow3:SetAlpha(0.3) else categoryBlock3:Hide() categoryRow3:SetAlpha(1) end + local colorEnabled = EllesmereUIDB and EllesmereUIDB.charSheetEnchantUseColor + if themedOff() then + enchantCogBtn:SetAlpha(0.15) + enchantCogBlock:Show() + enchantSwatch:SetAlpha(0.15) + enchantSwatch:EnableMouse(false) + else + enchantCogBtn:SetAlpha(0.4) + enchantCogBlock:Hide() + enchantSwatch:SetAlpha(colorEnabled and 1 or 0.3) + enchantSwatch:EnableMouse(colorEnabled) + end end + _, h = W:Spacer(parent, y, 20); y = y - h return math.abs(y) end diff --git a/charactersheet.lua b/charactersheet.lua index e73249ac..7f320d64 100644 --- a/charactersheet.lua +++ b/charactersheet.lua @@ -1015,14 +1015,18 @@ local function SkinCharacterSheet() local crestMaxValues = { [3347] = 400, -- Myth [3345] = 400, -- Hero - [3344] = 700, -- Champion + [3343] = 700, -- Champion [3341] = 700, -- Veteran - [3391] = 700, -- Adventurer + [3383] = 700, -- Adventurer } - -- Helper function to get crest maximum value + -- Helper function to get crest maximum value (now using API to get seasonal max) local function GetCrestMaxValue(currencyID) - return crestMaxValues[currencyID] or 3000 + local currencyInfo = C_CurrencyInfo.GetCurrencyInfo(currencyID) + if currencyInfo and currencyInfo.maxQuantity then + return currencyInfo.maxQuantity + end + return crestMaxValues[currencyID] or 3000 -- Fallback to hardcoded values if API fails end -- Check if a stat should be shown based on class/spec conditions @@ -1063,17 +1067,33 @@ local function SkinCharacterSheet() } end + -- Default category colors + local DEFAULT_CATEGORY_COLORS = { + Attributes = { r = 0.047, g = 0.824, b = 0.616 }, + ["Secondary Stats"] = { r = 0.471, g = 0.255, b = 0.784 }, + Attack = { r = 1, g = 0.353, b = 0.122 }, + Defense = { r = 0.247, g = 0.655, b = 1 }, + Crests = { r = 1, g = 0.784, b = 0.341 }, + } + + -- Get category color, applying customization if available + local function GetCategoryColor(title) + local custom = EllesmereUIDB and EllesmereUIDB.statCategoryColors and EllesmereUIDB.statCategoryColors[title] + if custom then return custom end + return DEFAULT_CATEGORY_COLORS[title] or { r = 1, g = 1, b = 1 } + end + -- Load stat sections order from saved data or use defaults local function GetStatSectionsOrder() local defaultOrder = { { title = "Attributes", - color = { r = 0.047, g = 0.824, b = 0.616 }, + color = GetCategoryColor("Attributes"), stats = GetFilteredAttributeStats() }, { title = "Secondary Stats", - color = { r = 0.471, g = 0.255, b = 0.784 }, + color = GetCategoryColor("Secondary Stats"), stats = { { name = "Crit", func = function() return GetCritChance("player") or 0 end, format = "%.2f%%", rawFunc = function() return GetCombatRating(CR_CRIT_MELEE) or 0 end }, { name = "Haste", func = function() return UnitSpellHaste("player") or 0 end, format = "%.2f%%", rawFunc = function() return GetCombatRating(CR_HASTE_MELEE) or 0 end }, @@ -1083,7 +1103,7 @@ local function SkinCharacterSheet() }, { title = "Attack", - color = { r = 1, g = 0.353, b = 0.122 }, + color = GetCategoryColor("Attack"), stats = { { name = "Spell Power", func = function() return GetSpellBonusDamage(7) end, tooltip = "Increases the power of your spells and abilities" }, { name = "Attack Speed", func = function() return UnitAttackSpeed("player") or 0 end, format = "%.2f", tooltip = "Attacks per second" }, @@ -1091,7 +1111,7 @@ local function SkinCharacterSheet() }, { title = "Defense", - color = { r = 0.247, g = 0.655, b = 1 }, + color = GetCategoryColor("Defense"), stats = { { name = "Armor", func = function() local base, effectiveArmor = UnitArmor("player") return effectiveArmor end, tooltip = "Reduces physical damage taken" }, { name = "Dodge", func = function() return GetDodgeChance() or 0 end, format = "%.2f%%", tooltip = "Chance to avoid melee attacks" }, @@ -1101,13 +1121,13 @@ local function SkinCharacterSheet() }, { title = "Crests", - color = { r = 1, g = 0.784, b = 0.341 }, + color = GetCategoryColor("Crests"), stats = { { name = "Myth", func = function() return GetCrestValue(3347) end, format = "%d", currencyID = 3347 }, { name = "Hero", func = function() return GetCrestValue(3345) end, format = "%d", currencyID = 3345 }, - { name = "Champion", func = function() return GetCrestValue(3344) end, format = "%d", currencyID = 3344 }, + { name = "Champion", func = function() return GetCrestValue(3343) end, format = "%d", currencyID = 3343 }, { name = "Veteran", func = function() return GetCrestValue(3341) end, format = "%d", currencyID = 3341 }, - { name = "Adventurer", func = function() return GetCrestValue(3391) end, format = "%d", currencyID = 3391 }, + { name = "Adventurer", func = function() return GetCrestValue(3383) end, format = "%d", currencyID = 3383 }, } } } @@ -1302,7 +1322,10 @@ local function SkinCharacterSheet() stats = {}, isCollapsed = false, height = 0, - sectionTitle = section.title -- Store title for reordering + sectionTitle = section.title, -- Store title for reordering + titleFS = sectionTitle, -- Store title fontstring for color updates + leftBar = leftBar, -- Store left bar for color updates + rightBar = rightBar -- Store right bar for color updates } table.insert(frame._statsSections, sectionData) @@ -1348,10 +1371,13 @@ local function SkinCharacterSheet() -- Currency (Crests) if stat.currencyID then - local current = GetCrestValue(stat.currencyID) - local maximum = GetCrestMaxValue(stat.currencyID) - GameTooltip:AddLine(stat.name .. " Crests", section.color.r, section.color.g, section.color.b, 1) - GameTooltip:AddLine(string.format("%d / %d", current, maximum), 1, 1, 1, true) + local currencyInfo = C_CurrencyInfo.GetCurrencyInfo(stat.currencyID) + if currencyInfo then + local earned = currencyInfo.totalEarned or 0 + local maximum = currencyInfo.maxQuantity or 0 + GameTooltip:AddLine(stat.name .. " Crests", section.color.r, section.color.g, section.color.b, 1) + GameTooltip:AddLine(string.format("%d / %d", earned, maximum), 1, 1, 1, true) + end -- Secondary stats with raw rating elseif stat.rawFunc then local percentValue = stat.func() @@ -2917,6 +2943,20 @@ local function SkinCharacterSheet() -- Cache item info (ID, level, upgrade track) to update when items change local itemCache = {} + -- Slots that can have enchants in current expansion + local ENCHANT_SLOTS = { + [INVSLOT_HEAD] = true, + [INVSLOT_SHOULDER] = true, + [INVSLOT_BACK] = false, + [INVSLOT_CHEST] = true, + [INVSLOT_WRIST] = false, + [INVSLOT_LEGS] = true, + [INVSLOT_FEET] = true, + [INVSLOT_FINGER1] = true, + [INVSLOT_FINGER2] = true, + [INVSLOT_MAINHAND] = true, + } + -- Function to update enchant text and upgrade track for a slot local function UpdateSlotInfo(slotName) local slot = _G[slotName] @@ -2928,6 +2968,8 @@ local function SkinCharacterSheet() local upgradeTrackText = "" local upgradeTrackColor = { r = 1, g = 1, b = 1 } local itemQuality = nil + local slotID = slot:GetID() + local canHaveEnchant = ENCHANT_SLOTS[slotID] if itemLink then local _, _, quality, ilvl = GetItemInfo(itemLink) @@ -2978,26 +3020,93 @@ local function SkinCharacterSheet() -- Update itemlevel label with optional rarity color if slot._itemLevelLabel then - slot._itemLevelLabel:SetText(tostring(itemLevel) or "") + -- Check if itemlevel is enabled (default: true) + local showItemLevel = (not EllesmereUIDB) or (EllesmereUIDB.showItemLevel ~= false) + + if showItemLevel then + slot._itemLevelLabel:SetText(tostring(itemLevel) or "") + slot._itemLevelLabel:Show() + + -- Determine color to use + local displayColor + if EllesmereUIDB and EllesmereUIDB.charSheetItemLevelUseColor and EllesmereUIDB.charSheetItemLevelColor then + -- Use custom color if enabled + displayColor = EllesmereUIDB.charSheetItemLevelColor + else + -- Use rarity color by default, unless explicitly disabled + if (not EllesmereUIDB or EllesmereUIDB.charSheetColorItemLevel ~= false) and itemQuality then + local r, g, b = GetItemQualityColor(itemQuality) + displayColor = { r = r, g = g, b = b } + else + displayColor = { r = 1, g = 1, b = 1 } + end + end - -- Apply rarity color if enabled - if EllesmereUIDB and EllesmereUIDB.charSheetColorItemLevel and itemQuality then - local r, g, b = GetItemQualityColor(itemQuality) - slot._itemLevelLabel:SetTextColor(r, g, b, 0.9) + slot._itemLevelLabel:SetTextColor(displayColor.r, displayColor.g, displayColor.b, 0.9) else - slot._itemLevelLabel:SetTextColor(1, 1, 1, 0.9) + slot._itemLevelLabel:Hide() end end -- Update enchant label if slot._enchantLabel then - slot._enchantLabel:SetText(enchantText or "") + -- Check if enchants are enabled (default: true) + local showEnchants = (not EllesmereUIDB) or (EllesmereUIDB.showEnchants ~= false) + + if showEnchants then + -- Check if enchant is missing (only for slots that can have enchants) + local isMissing = canHaveEnchant and itemLink and (enchantText == "" or not enchantText) + + if isMissing then + slot._enchantLabel:SetText("") + slot._enchantLabel:Show() + -- Red for missing enchant + slot._enchantLabel:SetTextColor(1, 0, 0, 1) + elseif enchantText and enchantText ~= "" then + slot._enchantLabel:SetText(enchantText) + slot._enchantLabel:Show() + + -- Determine color to use + local displayColor + if EllesmereUIDB and EllesmereUIDB.charSheetEnchantUseColor and EllesmereUIDB.charSheetEnchantColor then + -- Use custom color if enabled + displayColor = EllesmereUIDB.charSheetEnchantColor + else + -- Use default white color + displayColor = { r = 1, g = 1, b = 1 } + end + slot._enchantLabel:SetTextColor(displayColor.r, displayColor.g, displayColor.b, 1) + else + slot._enchantLabel:Hide() + end + else + slot._enchantLabel:Hide() + end end -- Update upgrade track label if slot._upgradeTrackLabel then - slot._upgradeTrackLabel:SetText(upgradeTrackText or "") - slot._upgradeTrackLabel:SetTextColor(upgradeTrackColor.r, upgradeTrackColor.g, upgradeTrackColor.b, 0.8) + -- Check if upgradetrack is enabled (default: true) + local showUpgradeTrack = (not EllesmereUIDB) or (EllesmereUIDB.showUpgradeTrack ~= false) + + if showUpgradeTrack then + slot._upgradeTrackLabel:SetText(upgradeTrackText or "") + slot._upgradeTrackLabel:Show() + + -- Determine color to use + local displayColor + if EllesmereUIDB and EllesmereUIDB.charSheetUpgradeTrackUseColor and EllesmereUIDB.charSheetUpgradeTrackColor then + -- Use custom color if enabled + displayColor = EllesmereUIDB.charSheetUpgradeTrackColor + else + -- Use original rarity color by default + displayColor = upgradeTrackColor + end + + slot._upgradeTrackLabel:SetTextColor(displayColor.r, displayColor.g, displayColor.b, 0.8) + else + slot._upgradeTrackLabel:Hide() + end end end @@ -3300,7 +3409,8 @@ function EllesmereUI._applyCharSheetItemColors() local itemLink = GetInventoryItemLink("player", slot:GetID()) if itemLink then local _, _, quality = GetItemInfo(itemLink) - if EllesmereUIDB and EllesmereUIDB.charSheetColorItemLevel and quality then + -- Use rarity color by default, unless explicitly disabled + if (not EllesmereUIDB or EllesmereUIDB.charSheetColorItemLevel ~= false) and quality then local r, g, b = GetItemQualityColor(quality) slot._itemLevelLabel:SetTextColor(r, g, b, 0.9) else @@ -3312,3 +3422,244 @@ function EllesmereUI._applyCharSheetItemColors() end end end + +-- Function to refresh category colors when changed in options +function EllesmereUI._refreshCharacterSheetColors() + local charFrame = CharacterFrame + if not charFrame or not charFrame._statsSections then return end + + -- Default category colors + local DEFAULT_CATEGORY_COLORS = { + Attributes = { r = 0.047, g = 0.824, b = 0.616 }, + ["Secondary Stats"] = { r = 0.471, g = 0.255, b = 0.784 }, + Attack = { r = 1, g = 0.353, b = 0.122 }, + Defense = { r = 0.247, g = 0.655, b = 1 }, + Crests = { r = 1, g = 0.784, b = 0.341 }, + } + + -- Helper to get category color + local function GetCategoryColor(title) + local custom = EllesmereUIDB and EllesmereUIDB.statCategoryColors and EllesmereUIDB.statCategoryColors[title] + if custom then return custom end + return DEFAULT_CATEGORY_COLORS[title] or { r = 1, g = 1, b = 1 } + end + + -- Update each section's colors + for _, sectionData in ipairs(charFrame._statsSections) do + local categoryName = sectionData.sectionTitle + local newColor = GetCategoryColor(categoryName) + + -- Update title color + if sectionData.titleFS then + sectionData.titleFS:SetTextColor(newColor.r, newColor.g, newColor.b, 1) + end + + -- Update bars + if sectionData.leftBar then + sectionData.leftBar:SetColorTexture(newColor.r, newColor.g, newColor.b, 0.8) + end + if sectionData.rightBar then + sectionData.rightBar:SetColorTexture(newColor.r, newColor.g, newColor.b, 0.8) + end + + -- Update stat values + for _, stat in ipairs(sectionData.stats) do + if stat.value then + stat.value:SetTextColor(newColor.r, newColor.g, newColor.b, 1) + end + end + end +end + +-- Function to refresh upgrade track visibility when toggle changes +function EllesmereUI._refreshUpgradeTrackVisibility() + local itemSlots = { + "CharacterHeadSlot", "CharacterNeckSlot", "CharacterShoulderSlot", "CharacterBackSlot", + "CharacterChestSlot", "CharacterWaistSlot", "CharacterLegsSlot", "CharacterFeetSlot", + "CharacterWristSlot", "CharacterHandsSlot", "CharacterFinger0Slot", "CharacterFinger1Slot", + "CharacterTrinket0Slot", "CharacterTrinket1Slot", "CharacterMainHandSlot", "CharacterSecondaryHandSlot" + } + + local showUpgradeTrack = (not EllesmereUIDB) or (EllesmereUIDB.showUpgradeTrack ~= false) + + for _, slotName in ipairs(itemSlots) do + local slot = _G[slotName] + if slot and slot._upgradeTrackLabel then + if showUpgradeTrack then + slot._upgradeTrackLabel:Show() + else + slot._upgradeTrackLabel:Hide() + end + end + end +end + +-- Function to refresh enchants visibility when toggle changes +function EllesmereUI._refreshEnchantsVisibility() + local itemSlots = { + "CharacterHeadSlot", "CharacterNeckSlot", "CharacterShoulderSlot", "CharacterBackSlot", + "CharacterChestSlot", "CharacterWaistSlot", "CharacterLegsSlot", "CharacterFeetSlot", + "CharacterWristSlot", "CharacterHandsSlot", "CharacterFinger0Slot", "CharacterFinger1Slot", + "CharacterTrinket0Slot", "CharacterTrinket1Slot", "CharacterMainHandSlot", "CharacterSecondaryHandSlot" + } + + local showEnchants = (not EllesmereUIDB) or (EllesmereUIDB.showEnchants ~= false) + + for _, slotName in ipairs(itemSlots) do + local slot = _G[slotName] + if slot and slot._enchantLabel then + if showEnchants then + slot._enchantLabel:Show() + else + slot._enchantLabel:Hide() + end + end + end +end + +-- Function to refresh enchants colors +function EllesmereUI._refreshEnchantsColors() + local itemSlots = { + "CharacterHeadSlot", "CharacterNeckSlot", "CharacterShoulderSlot", "CharacterBackSlot", + "CharacterChestSlot", "CharacterWaistSlot", "CharacterLegsSlot", "CharacterFeetSlot", + "CharacterWristSlot", "CharacterHandsSlot", "CharacterFinger0Slot", "CharacterFinger1Slot", + "CharacterTrinket0Slot", "CharacterTrinket1Slot", "CharacterMainHandSlot", "CharacterSecondaryHandSlot" + } + + for _, slotName in ipairs(itemSlots) do + local slot = _G[slotName] + if slot and slot._enchantLabel then + -- Determine color to use + local displayColor + if EllesmereUIDB and EllesmereUIDB.charSheetEnchantUseColor and EllesmereUIDB.charSheetEnchantColor then + -- Use custom color if enabled + displayColor = EllesmereUIDB.charSheetEnchantColor + else + -- Use default color + displayColor = { r = 1, g = 1, b = 1 } + end + + slot._enchantLabel:SetTextColor(displayColor.r, displayColor.g, displayColor.b, 1) + end + end +end + +-- Function to refresh item level visibility when toggle changes +function EllesmereUI._refreshItemLevelVisibility() + local itemSlots = { + "CharacterHeadSlot", "CharacterNeckSlot", "CharacterShoulderSlot", "CharacterBackSlot", + "CharacterChestSlot", "CharacterWaistSlot", "CharacterLegsSlot", "CharacterFeetSlot", + "CharacterWristSlot", "CharacterHandsSlot", "CharacterFinger0Slot", "CharacterFinger1Slot", + "CharacterTrinket0Slot", "CharacterTrinket1Slot", "CharacterMainHandSlot", "CharacterSecondaryHandSlot" + } + + local showItemLevel = (not EllesmereUIDB) or (EllesmereUIDB.showItemLevel ~= false) + + for _, slotName in ipairs(itemSlots) do + local slot = _G[slotName] + if slot and slot._itemLevelLabel then + if showItemLevel then + slot._itemLevelLabel:Show() + else + slot._itemLevelLabel:Hide() + end + end + end +end + +-- Function to refresh item level colors +function EllesmereUI._refreshItemLevelColors() + local itemSlots = { + "CharacterHeadSlot", "CharacterNeckSlot", "CharacterShoulderSlot", "CharacterBackSlot", + "CharacterChestSlot", "CharacterWaistSlot", "CharacterLegsSlot", "CharacterFeetSlot", + "CharacterWristSlot", "CharacterHandsSlot", "CharacterFinger0Slot", "CharacterFinger1Slot", + "CharacterTrinket0Slot", "CharacterTrinket1Slot", "CharacterMainHandSlot", "CharacterSecondaryHandSlot" + } + + for _, slotName in ipairs(itemSlots) do + local slot = _G[slotName] + if slot and slot._itemLevelLabel then + -- Determine color to use + local displayColor + if EllesmereUIDB and EllesmereUIDB.charSheetItemLevelUseColor and EllesmereUIDB.charSheetItemLevelColor then + -- Use custom color if enabled + displayColor = EllesmereUIDB.charSheetItemLevelColor + else + -- Use rarity color by default, unless explicitly disabled + local itemLink = GetInventoryItemLink("player", slot:GetID()) + if itemLink and (not EllesmereUIDB or EllesmereUIDB.charSheetColorItemLevel ~= false) then + local _, _, quality = GetItemInfo(itemLink) + if quality then + local r, g, b = GetItemQualityColor(quality) + displayColor = { r = r, g = g, b = b } + else + displayColor = { r = 1, g = 1, b = 1 } + end + else + displayColor = { r = 1, g = 1, b = 1 } + end + end + + slot._itemLevelLabel:SetTextColor(displayColor.r, displayColor.g, displayColor.b, 0.9) + end + end +end + +-- Function to refresh upgrade track colors +function EllesmereUI._refreshUpgradeTrackColors() + local itemSlots = { + "CharacterHeadSlot", "CharacterNeckSlot", "CharacterShoulderSlot", "CharacterBackSlot", + "CharacterChestSlot", "CharacterWaistSlot", "CharacterLegsSlot", "CharacterFeetSlot", + "CharacterWristSlot", "CharacterHandsSlot", "CharacterFinger0Slot", "CharacterFinger1Slot", + "CharacterTrinket0Slot", "CharacterTrinket1Slot", "CharacterMainHandSlot", "CharacterSecondaryHandSlot" + } + + for _, slotName in ipairs(itemSlots) do + local slot = _G[slotName] + if slot and slot._upgradeTrackLabel then + local itemLink = GetInventoryItemLink("player", slot:GetID()) + if itemLink then + -- Get the upgrade track text to determine the color + local enchantTooltip = CreateFrame("GameTooltip", "EUICharacterSheetEnchantTooltip", nil, "GameTooltipTemplate") + enchantTooltip:SetOwner(UIParent, "ANCHOR_NONE") + enchantTooltip:SetInventoryItem("player", slot:GetID()) + + local upgradeTrackColor = { r = 1, g = 1, b = 1 } + for i = 1, enchantTooltip:NumLines() do + local textLeft = _G["EUICharacterSheetEnchantTooltipTextLeft" .. i]:GetText() or "" + if textLeft:match("Upgrade Level:") then + local trackInfo = textLeft:gsub("Upgrade Level:%s*", "") + local trk, nums = trackInfo:match("^(%w+)%s+(.+)$") + + if trk and nums then + if trk == "Champion" then + upgradeTrackColor = { r = 0.00, g = 0.44, b = 0.87 } + elseif trk:match("Myth") then + upgradeTrackColor = { r = 1.00, g = 0.50, b = 0.00 } + elseif trk:match("Hero") then + upgradeTrackColor = { r = 1.00, g = 0.30, b = 1.00 } + elseif trk:match("Veteran") then + upgradeTrackColor = { r = 0.12, g = 1.00, b = 0.00 } + elseif trk:match("Adventurer") then + upgradeTrackColor = { r = 1.00, g = 1.00, b = 1.00 } + elseif trk:match("Delve") or trk:match("Explorer") then + upgradeTrackColor = { r = 0.62, g = 0.62, b = 0.62 } + end + end + break + end + end + + -- Apply color + local displayColor + if EllesmereUIDB and EllesmereUIDB.charSheetUpgradeTrackUseColor and EllesmereUIDB.charSheetUpgradeTrackColor then + displayColor = EllesmereUIDB.charSheetUpgradeTrackColor + else + displayColor = upgradeTrackColor + end + + slot._upgradeTrackLabel:SetTextColor(displayColor.r, displayColor.g, displayColor.b, 0.8) + end + end + end +end From 97ffc9527ba2144e92a1c8719f7608eb7ea79979 Mon Sep 17 00:00:00 2001 From: Daniel <14241290+dnlxh@users.noreply.github.com> Date: Sat, 11 Apr 2026 22:54:21 +0200 Subject: [PATCH 11/16] Option changes & fixes --- EUI__General_Options.lua | 366 ++++++++++++++++++++++++++++++++++++--- charactersheet.lua | 8 +- 2 files changed, 344 insertions(+), 30 deletions(-) diff --git a/EUI__General_Options.lua b/EUI__General_Options.lua index ff4810fa..693fde8a 100644 --- a/EUI__General_Options.lua +++ b/EUI__General_Options.lua @@ -3638,8 +3638,47 @@ initFrame:SetScript("OnEvent", function(self) end) if themedOff() then themedCogBtn:SetAlpha(0.15) themedCogBlock:Show() else themedCogBtn:SetAlpha(0.4) themedCogBlock:Hide() end - -- COLOR PICKER FOR ATTRIBUTES (RIGHT) + -- COGWHEEL + COLOR PICKER FOR ATTRIBUTES (RIGHT) local rightRgn = themedCharacterSheetRow._rightRegion + + local _, attrCogShow = EllesmereUI.BuildCogPopup({ + title = "Attributes Color", + rows = { + { type="toggle", label="Use Custom Color", + get=function() return EllesmereUIDB and EllesmereUIDB.statCategoryUseColor and EllesmereUIDB.statCategoryUseColor.Attributes or false end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + if not EllesmereUIDB.statCategoryUseColor then EllesmereUIDB.statCategoryUseColor = {} end + EllesmereUIDB.statCategoryUseColor.Attributes = v + if EllesmereUI._refreshCharacterSheetColors then EllesmereUI._refreshCharacterSheetColors() end + EllesmereUI:RefreshPage() + end }, + }, + }) + + local attrCogBtn = CreateFrame("Button", nil, rightRgn) + attrCogBtn:SetSize(26, 26) + attrCogBtn:SetPoint("RIGHT", rightRgn._lastInline or rightRgn._control, "LEFT", -9, 0) + rightRgn._lastInline = attrCogBtn + attrCogBtn:SetFrameLevel(rightRgn:GetFrameLevel() + 5) + attrCogBtn:SetAlpha(themedOff() and 0.15 or 0.4) + local attrCogTex = attrCogBtn:CreateTexture(nil, "OVERLAY") + attrCogTex:SetAllPoints() + attrCogTex:SetTexture(EllesmereUI.COGS_ICON) + attrCogBtn:SetScript("OnEnter", function(self) self:SetAlpha(0.7) end) + attrCogBtn:SetScript("OnLeave", function(self) self:SetAlpha(themedOff() and 0.15 or 0.4) end) + attrCogBtn:SetScript("OnClick", function(self) attrCogShow(self) end) + + local attrCogBlock = CreateFrame("Frame", nil, attrCogBtn) + attrCogBlock:SetAllPoints() + attrCogBlock:SetFrameLevel(attrCogBtn:GetFrameLevel() + 10) + attrCogBlock:EnableMouse(true) + attrCogBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(attrCogBtn, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + end) + attrCogBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + -- Color swatch (shown when custom color is enabled) local attrSwGet = function() local c = EllesmereUIDB and EllesmereUIDB.statCategoryColors and EllesmereUIDB.statCategoryColors.Attributes if c then return c.r, c.g, c.b, 1 end @@ -3652,20 +3691,36 @@ initFrame:SetScript("OnEvent", function(self) if EllesmereUI._refreshCharacterSheetColors then EllesmereUI._refreshCharacterSheetColors() end end local attrSwatch, attrUpdateSwatch = EllesmereUI.BuildColorSwatch(rightRgn, rightRgn:GetFrameLevel() + 5, attrSwGet, attrSwSet, false, 20) - PP.Point(attrSwatch, "RIGHT", rightRgn._control, "LEFT", -12, 0) + PP.Point(attrSwatch, "RIGHT", attrCogBtn, "LEFT", -9, 0) rightRgn._lastInline = attrSwatch EllesmereUI.RegisterWidgetRefresh(function() + local customEnabled = EllesmereUIDB and EllesmereUIDB.statCategoryUseColor and EllesmereUIDB.statCategoryUseColor.Attributes if themedOff() then - attrSwatch:SetAlpha(0.3) + attrCogBtn:SetAlpha(0.15) + attrCogBlock:Show() + attrSwatch:SetAlpha(0.15) attrSwatch:EnableMouse(false) else - attrSwatch:SetAlpha(1) - attrSwatch:EnableMouse(true) + attrCogBtn:SetAlpha(0.4) + attrCogBlock:Hide() + attrSwatch:SetAlpha(customEnabled and 1 or 0.3) + attrSwatch:EnableMouse(customEnabled) end attrUpdateSwatch() end) - if themedOff() then attrSwatch:SetAlpha(0.3) attrSwatch:EnableMouse(false) else attrSwatch:SetAlpha(1) attrSwatch:EnableMouse(true) end + if themedOff() then + attrCogBtn:SetAlpha(0.15) + attrCogBlock:Show() + attrSwatch:SetAlpha(0.15) + attrSwatch:EnableMouse(false) + else + attrCogBtn:SetAlpha(0.4) + attrCogBlock:Hide() + local customEnabled = EllesmereUIDB and EllesmereUIDB.statCategoryUseColor and EllesmereUIDB.statCategoryUseColor.Attributes + attrSwatch:SetAlpha(customEnabled and 1 or 0.3) + attrSwatch:EnableMouse(customEnabled) + end end local mythicRatingRow @@ -3725,6 +3780,96 @@ initFrame:SetScript("OnEvent", function(self) if themedOff() then mythicBlock:Show() mythicRatingRow:SetAlpha(0.3) else mythicBlock:Hide() mythicRatingRow:SetAlpha(1) end end + -- COGWHEEL + COLOR PICKER FOR ATTACK (RIGHT SIDE OF MYTHICRATINGROW) + do + local function themedOff() + return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) + end + + local rightRgn = mythicRatingRow._rightRegion + + local _, attackCogShow = EllesmereUI.BuildCogPopup({ + title = "Attack Color", + rows = { + { type="toggle", label="Use Custom Color", + get=function() return EllesmereUIDB and EllesmereUIDB.statCategoryUseColor and EllesmereUIDB.statCategoryUseColor.Attack or false end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + if not EllesmereUIDB.statCategoryUseColor then EllesmereUIDB.statCategoryUseColor = {} end + EllesmereUIDB.statCategoryUseColor.Attack = v + if EllesmereUI._refreshCharacterSheetColors then EllesmereUI._refreshCharacterSheetColors() end + EllesmereUI:RefreshPage() + end }, + }, + }) + + local attackCogBtn = CreateFrame("Button", nil, rightRgn) + attackCogBtn:SetSize(26, 26) + attackCogBtn:SetPoint("RIGHT", rightRgn._lastInline or rightRgn._control, "LEFT", -9, 0) + rightRgn._lastInline = attackCogBtn + attackCogBtn:SetFrameLevel(rightRgn:GetFrameLevel() + 5) + attackCogBtn:SetAlpha(themedOff() and 0.15 or 0.4) + local attackCogTex = attackCogBtn:CreateTexture(nil, "OVERLAY") + attackCogTex:SetAllPoints() + attackCogTex:SetTexture(EllesmereUI.COGS_ICON) + attackCogBtn:SetScript("OnEnter", function(self) self:SetAlpha(0.7) end) + attackCogBtn:SetScript("OnLeave", function(self) self:SetAlpha(themedOff() and 0.15 or 0.4) end) + attackCogBtn:SetScript("OnClick", function(self) attackCogShow(self) end) + + local attackCogBlock = CreateFrame("Frame", nil, attackCogBtn) + attackCogBlock:SetAllPoints() + attackCogBlock:SetFrameLevel(attackCogBtn:GetFrameLevel() + 10) + attackCogBlock:EnableMouse(true) + attackCogBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(attackCogBtn, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + end) + attackCogBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + -- Color swatch (shown when custom color is enabled) + local attackSwGet = function() + local c = EllesmereUIDB and EllesmereUIDB.statCategoryColors and EllesmereUIDB.statCategoryColors.Attack + if c then return c.r, c.g, c.b, 1 end + return 1, 0.353, 0.122, 1 + end + local attackSwSet = function(r, g, b) + if not EllesmereUIDB then EllesmereUIDB = {} end + if not EllesmereUIDB.statCategoryColors then EllesmereUIDB.statCategoryColors = {} end + EllesmereUIDB.statCategoryColors.Attack = { r = r, g = g, b = b } + if EllesmereUI._refreshCharacterSheetColors then EllesmereUI._refreshCharacterSheetColors() end + end + local attackSwatch, attackUpdateSwatch = EllesmereUI.BuildColorSwatch(rightRgn, rightRgn:GetFrameLevel() + 5, attackSwGet, attackSwSet, false, 20) + PP.Point(attackSwatch, "RIGHT", attackCogBtn, "LEFT", -9, 0) + rightRgn._lastInline = attackSwatch + + EllesmereUI.RegisterWidgetRefresh(function() + local customEnabled = EllesmereUIDB and EllesmereUIDB.statCategoryUseColor and EllesmereUIDB.statCategoryUseColor.Attack + if themedOff() then + attackCogBtn:SetAlpha(0.15) + attackCogBlock:Show() + attackSwatch:SetAlpha(0.15) + attackSwatch:EnableMouse(false) + else + attackCogBtn:SetAlpha(0.4) + attackCogBlock:Hide() + attackSwatch:SetAlpha(customEnabled and 1 or 0.3) + attackSwatch:EnableMouse(customEnabled) + end + attackUpdateSwatch() + end) + if themedOff() then + attackCogBtn:SetAlpha(0.15) + attackCogBlock:Show() + attackSwatch:SetAlpha(0.15) + attackSwatch:EnableMouse(false) + else + attackCogBtn:SetAlpha(0.4) + attackCogBlock:Hide() + local customEnabled = EllesmereUIDB and EllesmereUIDB.statCategoryUseColor and EllesmereUIDB.statCategoryUseColor.Attack + attackSwatch:SetAlpha(customEnabled and 1 or 0.3) + attackSwatch:EnableMouse(customEnabled) + end + end + local itemLevelRow itemLevelRow, h = W:DualRow(parent, y, { type="toggle", text="Itemlevel", @@ -3782,15 +3927,54 @@ initFrame:SetScript("OnEvent", function(self) if themedOff() then itemLevelBlock:Show() itemLevelRow:SetAlpha(0.3) else itemLevelBlock:Hide() itemLevelRow:SetAlpha(1) end end - -- Cogwheel for item level (left) + Color picker for Crests (right) + -- Cogwheel for item level (left) + COGWHEEL + COLOR PICKER FOR CRESTS (right) do local function themedOff() return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) end -- ITEMLEVEL COGWHEEL (LEFT) - already in itemLevelRow above - -- Color picker for Crests (right side) + -- COGWHEEL + Color picker for Crests (right side) local rightRgn = itemLevelRow._rightRegion + + local _, crestsCogShow = EllesmereUI.BuildCogPopup({ + title = "Crests Color", + rows = { + { type="toggle", label="Use Custom Color", + get=function() return EllesmereUIDB and EllesmereUIDB.statCategoryUseColor and EllesmereUIDB.statCategoryUseColor.Crests or false end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + if not EllesmereUIDB.statCategoryUseColor then EllesmereUIDB.statCategoryUseColor = {} end + EllesmereUIDB.statCategoryUseColor.Crests = v + if EllesmereUI._refreshCharacterSheetColors then EllesmereUI._refreshCharacterSheetColors() end + EllesmereUI:RefreshPage() + end }, + }, + }) + + local crestsCogBtn = CreateFrame("Button", nil, rightRgn) + crestsCogBtn:SetSize(26, 26) + crestsCogBtn:SetPoint("RIGHT", rightRgn._lastInline or rightRgn._control, "LEFT", -9, 0) + rightRgn._lastInline = crestsCogBtn + crestsCogBtn:SetFrameLevel(rightRgn:GetFrameLevel() + 5) + crestsCogBtn:SetAlpha(themedOff() and 0.15 or 0.4) + local crestsCogTex = crestsCogBtn:CreateTexture(nil, "OVERLAY") + crestsCogTex:SetAllPoints() + crestsCogTex:SetTexture(EllesmereUI.COGS_ICON) + crestsCogBtn:SetScript("OnEnter", function(self) self:SetAlpha(0.7) end) + crestsCogBtn:SetScript("OnLeave", function(self) self:SetAlpha(themedOff() and 0.15 or 0.4) end) + crestsCogBtn:SetScript("OnClick", function(self) crestsCogShow(self) end) + + local crestsCogBlock = CreateFrame("Frame", nil, crestsCogBtn) + crestsCogBlock:SetAllPoints() + crestsCogBlock:SetFrameLevel(crestsCogBtn:GetFrameLevel() + 10) + crestsCogBlock:EnableMouse(true) + crestsCogBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(crestsCogBtn, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + end) + crestsCogBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + -- Color swatch (shown when custom color is enabled) local crestsSwGet = function() local c = EllesmereUIDB and EllesmereUIDB.statCategoryColors and EllesmereUIDB.statCategoryColors.Crests if c then return c.r, c.g, c.b, 1 end @@ -3803,20 +3987,36 @@ initFrame:SetScript("OnEvent", function(self) if EllesmereUI._refreshCharacterSheetColors then EllesmereUI._refreshCharacterSheetColors() end end local crestsSwatch, crestsUpdateSwatch = EllesmereUI.BuildColorSwatch(rightRgn, rightRgn:GetFrameLevel() + 5, crestsSwGet, crestsSwSet, false, 20) - PP.Point(crestsSwatch, "RIGHT", rightRgn._control, "LEFT", -12, 0) + PP.Point(crestsSwatch, "RIGHT", crestsCogBtn, "LEFT", -9, 0) rightRgn._lastInline = crestsSwatch EllesmereUI.RegisterWidgetRefresh(function() + local customEnabled = EllesmereUIDB and EllesmereUIDB.statCategoryUseColor and EllesmereUIDB.statCategoryUseColor.Crests if themedOff() then - crestsSwatch:SetAlpha(0.3) + crestsCogBtn:SetAlpha(0.15) + crestsCogBlock:Show() + crestsSwatch:SetAlpha(0.15) crestsSwatch:EnableMouse(false) else - crestsSwatch:SetAlpha(1) - crestsSwatch:EnableMouse(true) + crestsCogBtn:SetAlpha(0.4) + crestsCogBlock:Hide() + crestsSwatch:SetAlpha(customEnabled and 1 or 0.3) + crestsSwatch:EnableMouse(customEnabled) end crestsUpdateSwatch() end) - if themedOff() then crestsSwatch:SetAlpha(0.3) crestsSwatch:EnableMouse(false) else crestsSwatch:SetAlpha(1) crestsSwatch:EnableMouse(true) end + if themedOff() then + crestsCogBtn:SetAlpha(0.15) + crestsCogBlock:Show() + crestsSwatch:SetAlpha(0.15) + crestsSwatch:EnableMouse(false) + else + crestsCogBtn:SetAlpha(0.4) + crestsCogBlock:Hide() + local customEnabled = EllesmereUIDB and EllesmereUIDB.statCategoryUseColor and EllesmereUIDB.statCategoryUseColor.Crests + crestsSwatch:SetAlpha(customEnabled and 1 or 0.3) + crestsSwatch:EnableMouse(customEnabled) + end end local upgradeTrackRow @@ -3996,34 +4196,89 @@ initFrame:SetScript("OnEvent", function(self) upgradeTrackSwatch:EnableMouse(colorEnabled) end - -- COLOR PICKER FOR SECONDARY STATS (RIGHT) + -- COGWHEEL + COLOR PICKER FOR SECONDARY STATS (RIGHT) local rightRgn = upgradeTrackRow._rightRegion + + local _, secondaryCogShow = EllesmereUI.BuildCogPopup({ + title = "Secondary Stats Color", + rows = { + { type="toggle", label="Use Custom Color", + get=function() return EllesmereUIDB and EllesmereUIDB.statCategoryUseColor and EllesmereUIDB.statCategoryUseColor["Secondary Stats"] or false end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + if not EllesmereUIDB.statCategoryUseColor then EllesmereUIDB.statCategoryUseColor = {} end + EllesmereUIDB.statCategoryUseColor["Secondary Stats"] = v + if EllesmereUI._refreshCharacterSheetColors then EllesmereUI._refreshCharacterSheetColors() end + EllesmereUI:RefreshPage() + end }, + }, + }) + + local secondaryCogBtn = CreateFrame("Button", nil, rightRgn) + secondaryCogBtn:SetSize(26, 26) + secondaryCogBtn:SetPoint("RIGHT", rightRgn._lastInline or rightRgn._control, "LEFT", -9, 0) + rightRgn._lastInline = secondaryCogBtn + secondaryCogBtn:SetFrameLevel(rightRgn:GetFrameLevel() + 5) + secondaryCogBtn:SetAlpha(themedOff() and 0.15 or 0.4) + local secondaryCogTex = secondaryCogBtn:CreateTexture(nil, "OVERLAY") + secondaryCogTex:SetAllPoints() + secondaryCogTex:SetTexture(EllesmereUI.COGS_ICON) + secondaryCogBtn:SetScript("OnEnter", function(self) self:SetAlpha(0.7) end) + secondaryCogBtn:SetScript("OnLeave", function(self) self:SetAlpha(themedOff() and 0.15 or 0.4) end) + secondaryCogBtn:SetScript("OnClick", function(self) secondaryCogShow(self) end) + + local secondaryCogBlock = CreateFrame("Frame", nil, secondaryCogBtn) + secondaryCogBlock:SetAllPoints() + secondaryCogBlock:SetFrameLevel(secondaryCogBtn:GetFrameLevel() + 10) + secondaryCogBlock:EnableMouse(true) + secondaryCogBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(secondaryCogBtn, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + end) + secondaryCogBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + -- Color swatch (shown when custom color is enabled) local secondarySwGet = function() - local c = EllesmereUIDB and EllesmereUIDB.statCategoryColors and EllesmereUIDB.statCategoryColors.SecondaryStats + local c = EllesmereUIDB and EllesmereUIDB.statCategoryColors and EllesmereUIDB.statCategoryColors["Secondary Stats"] if c then return c.r, c.g, c.b, 1 end - return 0.157, 1, 0.949, 1 + return 0.471, 0.255, 0.784, 1 end local secondarySwSet = function(r, g, b) if not EllesmereUIDB then EllesmereUIDB = {} end if not EllesmereUIDB.statCategoryColors then EllesmereUIDB.statCategoryColors = {} end - EllesmereUIDB.statCategoryColors.SecondaryStats = { r = r, g = g, b = b } + EllesmereUIDB.statCategoryColors["Secondary Stats"] = { r = r, g = g, b = b } if EllesmereUI._refreshCharacterSheetColors then EllesmereUI._refreshCharacterSheetColors() end end local secondarySwatch, secondaryUpdateSwatch = EllesmereUI.BuildColorSwatch(rightRgn, rightRgn:GetFrameLevel() + 5, secondarySwGet, secondarySwSet, false, 20) - PP.Point(secondarySwatch, "RIGHT", rightRgn._control, "LEFT", -12, 0) + PP.Point(secondarySwatch, "RIGHT", secondaryCogBtn, "LEFT", -9, 0) rightRgn._lastInline = secondarySwatch EllesmereUI.RegisterWidgetRefresh(function() + local customEnabled = EllesmereUIDB and EllesmereUIDB.statCategoryUseColor and EllesmereUIDB.statCategoryUseColor["Secondary Stats"] if themedOff() then - secondarySwatch:SetAlpha(0.3) + secondaryCogBtn:SetAlpha(0.15) + secondaryCogBlock:Show() + secondarySwatch:SetAlpha(0.15) secondarySwatch:EnableMouse(false) else - secondarySwatch:SetAlpha(1) - secondarySwatch:EnableMouse(true) + secondaryCogBtn:SetAlpha(0.4) + secondaryCogBlock:Hide() + secondarySwatch:SetAlpha(customEnabled and 1 or 0.3) + secondarySwatch:EnableMouse(customEnabled) end secondaryUpdateSwatch() end) - if themedOff() then secondarySwatch:SetAlpha(0.3) secondarySwatch:EnableMouse(false) else secondarySwatch:SetAlpha(1) secondarySwatch:EnableMouse(true) end + if themedOff() then + secondaryCogBtn:SetAlpha(0.15) + secondaryCogBlock:Show() + secondarySwatch:SetAlpha(0.15) + secondarySwatch:EnableMouse(false) + else + secondaryCogBtn:SetAlpha(0.4) + secondaryCogBlock:Hide() + local customEnabled = EllesmereUIDB and EllesmereUIDB.statCategoryUseColor and EllesmereUIDB.statCategoryUseColor["Secondary Stats"] + secondarySwatch:SetAlpha(customEnabled and 1 or 0.3) + secondarySwatch:EnableMouse(customEnabled) + end end -- Cogwheel for item level settings (left) and upgrade track settings (right) @@ -4205,12 +4460,51 @@ initFrame:SetScript("OnEvent", function(self) if themedOff() then enchantBlock:Show() enchantRow:SetAlpha(0.3) else enchantBlock:Hide() enchantRow:SetAlpha(1) end end - -- Color picker for Defense (right side) + -- COGWHEEL + COLOR PICKER FOR DEFENSE (RIGHT SIDE) do local function themedOff() return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) end local rightRgn = enchantRow._rightRegion + + local _, defenseCogShow = EllesmereUI.BuildCogPopup({ + title = "Defense Color", + rows = { + { type="toggle", label="Use Custom Color", + get=function() return EllesmereUIDB and EllesmereUIDB.statCategoryUseColor and EllesmereUIDB.statCategoryUseColor.Defense or false end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + if not EllesmereUIDB.statCategoryUseColor then EllesmereUIDB.statCategoryUseColor = {} end + EllesmereUIDB.statCategoryUseColor.Defense = v + if EllesmereUI._refreshCharacterSheetColors then EllesmereUI._refreshCharacterSheetColors() end + EllesmereUI:RefreshPage() + end }, + }, + }) + + local defenseCogBtn = CreateFrame("Button", nil, rightRgn) + defenseCogBtn:SetSize(26, 26) + defenseCogBtn:SetPoint("RIGHT", rightRgn._lastInline or rightRgn._control, "LEFT", -9, 0) + rightRgn._lastInline = defenseCogBtn + defenseCogBtn:SetFrameLevel(rightRgn:GetFrameLevel() + 5) + defenseCogBtn:SetAlpha(themedOff() and 0.15 or 0.4) + local defenseCogTex = defenseCogBtn:CreateTexture(nil, "OVERLAY") + defenseCogTex:SetAllPoints() + defenseCogTex:SetTexture(EllesmereUI.COGS_ICON) + defenseCogBtn:SetScript("OnEnter", function(self) self:SetAlpha(0.7) end) + defenseCogBtn:SetScript("OnLeave", function(self) self:SetAlpha(themedOff() and 0.15 or 0.4) end) + defenseCogBtn:SetScript("OnClick", function(self) defenseCogShow(self) end) + + local defenseCogBlock = CreateFrame("Frame", nil, defenseCogBtn) + defenseCogBlock:SetAllPoints() + defenseCogBlock:SetFrameLevel(defenseCogBtn:GetFrameLevel() + 10) + defenseCogBlock:EnableMouse(true) + defenseCogBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(defenseCogBtn, EllesmereUI.DisabledTooltip("Themed Character Sheet")) + end) + defenseCogBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + -- Color swatch (shown when custom color is enabled) local defenseSwGet = function() local c = EllesmereUIDB and EllesmereUIDB.statCategoryColors and EllesmereUIDB.statCategoryColors.Defense if c then return c.r, c.g, c.b, 1 end @@ -4223,20 +4517,36 @@ initFrame:SetScript("OnEvent", function(self) if EllesmereUI._refreshCharacterSheetColors then EllesmereUI._refreshCharacterSheetColors() end end local defenseSwatch, defenseUpdateSwatch = EllesmereUI.BuildColorSwatch(rightRgn, rightRgn:GetFrameLevel() + 5, defenseSwGet, defenseSwSet, false, 20) - PP.Point(defenseSwatch, "RIGHT", rightRgn._control, "LEFT", -12, 0) + PP.Point(defenseSwatch, "RIGHT", defenseCogBtn, "LEFT", -9, 0) rightRgn._lastInline = defenseSwatch EllesmereUI.RegisterWidgetRefresh(function() + local customEnabled = EllesmereUIDB and EllesmereUIDB.statCategoryUseColor and EllesmereUIDB.statCategoryUseColor.Defense if themedOff() then - defenseSwatch:SetAlpha(0.3) + defenseCogBtn:SetAlpha(0.15) + defenseCogBlock:Show() + defenseSwatch:SetAlpha(0.15) defenseSwatch:EnableMouse(false) else - defenseSwatch:SetAlpha(1) - defenseSwatch:EnableMouse(true) + defenseCogBtn:SetAlpha(0.4) + defenseCogBlock:Hide() + defenseSwatch:SetAlpha(customEnabled and 1 or 0.3) + defenseSwatch:EnableMouse(customEnabled) end defenseUpdateSwatch() end) - if themedOff() then defenseSwatch:SetAlpha(0.3) defenseSwatch:EnableMouse(false) else defenseSwatch:SetAlpha(1) defenseSwatch:EnableMouse(true) end + if themedOff() then + defenseCogBtn:SetAlpha(0.15) + defenseCogBlock:Show() + defenseSwatch:SetAlpha(0.15) + defenseSwatch:EnableMouse(false) + else + defenseCogBtn:SetAlpha(0.4) + defenseCogBlock:Hide() + local customEnabled = EllesmereUIDB and EllesmereUIDB.statCategoryUseColor and EllesmereUIDB.statCategoryUseColor.Defense + defenseSwatch:SetAlpha(customEnabled and 1 or 0.3) + defenseSwatch:EnableMouse(customEnabled) + end end -- Cogwheel for enchant settings diff --git a/charactersheet.lua b/charactersheet.lua index 7f320d64..2874d165 100644 --- a/charactersheet.lua +++ b/charactersheet.lua @@ -3439,8 +3439,12 @@ function EllesmereUI._refreshCharacterSheetColors() -- Helper to get category color local function GetCategoryColor(title) - local custom = EllesmereUIDB and EllesmereUIDB.statCategoryColors and EllesmereUIDB.statCategoryColors[title] - if custom then return custom end + -- Check if custom color is enabled for this category + local useCustom = EllesmereUIDB and EllesmereUIDB.statCategoryUseColor and EllesmereUIDB.statCategoryUseColor[title] + if useCustom then + local custom = EllesmereUIDB and EllesmereUIDB.statCategoryColors and EllesmereUIDB.statCategoryColors[title] + if custom then return custom end + end return DEFAULT_CATEGORY_COLORS[title] or { r = 1, g = 1, b = 1 } end From 71199fda11d7bbd3e38862d4b76a99f246c31706 Mon Sep 17 00:00:00 2001 From: Daniel <14241290+dnlxh@users.noreply.github.com> Date: Sun, 12 Apr 2026 13:06:31 +0200 Subject: [PATCH 12/16] Fix unintended equipment set auto-equipping on group events The PLAYER_SPECIALIZATION_CHANGED event was being fired by WoW even when the specialization didn't actually change, particularly when joining groups or when guild members come online. This caused equipment sets to be auto-equipped unnecessarily. Track the previous specialization index and only trigger auto-equipping when it actually changes. This prevents spurious equipment changes from false events while preserving the intended auto-equip functionality. --- charactersheet.lua | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/charactersheet.lua b/charactersheet.lua index 2874d165..fa487302 100644 --- a/charactersheet.lua +++ b/charactersheet.lua @@ -3272,6 +3272,7 @@ if EllesmereUI then -- Auto-equip equipment set when spec changes local specChangeFrame = CreateFrame("Frame") + local lastSpecIndex = GetSpecialization() specChangeFrame:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED") specChangeFrame:RegisterEvent("EQUIPMENT_SETS_CHANGED") specChangeFrame:SetScript("OnEvent", function(self, event) @@ -3283,20 +3284,23 @@ if EllesmereUI then -- Equipment panel will be refreshed by the equipSetChangeFrame handler end else - -- Auto-equip when spec changes - local setIDs = C_EquipmentSet.GetEquipmentSetIDs() - if setIDs then - for _, setID in ipairs(setIDs) do - local assignedSpec = C_EquipmentSet.GetEquipmentSetAssignedSpec(setID) - if assignedSpec then - local currentSpecIndex = GetSpecialization() - if assignedSpec == currentSpecIndex then - C_EquipmentSet.UseEquipmentSet(setID) - activeEquipmentSetID = setID - if EllesmereUIDB then - EllesmereUIDB.lastEquippedSet = setID + -- Auto-equip when spec actually changes (not just event noise) + local currentSpecIndex = GetSpecialization() + if currentSpecIndex ~= lastSpecIndex then + lastSpecIndex = currentSpecIndex + local setIDs = C_EquipmentSet.GetEquipmentSetIDs() + if setIDs then + for _, setID in ipairs(setIDs) do + local assignedSpec = C_EquipmentSet.GetEquipmentSetAssignedSpec(setID) + if assignedSpec then + if assignedSpec == currentSpecIndex then + C_EquipmentSet.UseEquipmentSet(setID) + activeEquipmentSetID = setID + if EllesmereUIDB then + EllesmereUIDB.lastEquippedSet = setID + end + break end - break end end end From 2823767255b5202f1a5b12b3f75434cae5747955 Mon Sep 17 00:00:00 2001 From: EllesmereGaming Date: Sun, 12 Apr 2026 17:10:36 -0400 Subject: [PATCH 13/16] Release v6.5 --- .gitignore | 1 + EUI_QoL.lua | 32 +- EUI_UnlockMode.lua | 123 ++-- EllesmereUI.lua | 92 +-- EllesmereUI.toc | 2 +- .../EllesmereUIActionBars.lua | 81 +-- .../EllesmereUIActionBars.toc | 2 +- .../EllesmereUIAuraBuffReminders.lua | 10 +- .../EllesmereUIAuraBuffReminders.toc | 2 +- .../EUI_Basics_QuestTracker_Options.lua | 21 +- EllesmereUIBasics/EllesmereUIBasics.lua | 57 +- EllesmereUIBasics/EllesmereUIBasics.toc | 2 +- .../EllesmereUIBasics_QuestTracker.lua | 184 +++++- .../EUI_CooldownManager_Options.lua | 330 +++-------- .../EllesmereUICdmHooks.lua | 148 ++++- .../EllesmereUICdmSpellPicker.lua | 100 ++-- .../EllesmereUICooldownManager.lua | 545 ++++++------------ .../EllesmereUICooldownManager.toc | 2 +- .../EllesmereUINameplates.toc | 2 +- .../EllesmereUIResourceBars.lua | 84 ++- .../EllesmereUIResourceBars.toc | 2 +- .../EllesmereUIUnitFrames.toc | 2 +- EllesmereUI_Migration.lua | 18 + EllesmereUI_Profiles.lua | 2 +- EllesmereUI_Widgets.lua | 16 + 25 files changed, 928 insertions(+), 932 deletions(-) diff --git a/.gitignore b/.gitignore index a829a949..c9cf77a8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ EllesmereUIUnitFrames/Libs/ # Claude Code workspace files .claude/ CLAUDE.md +CLAUDE_PATTERNS.md # Developer scripts *.ps1 diff --git a/EUI_QoL.lua b/EUI_QoL.lua index 8e4f75fe..276117df 100644 --- a/EUI_QoL.lua +++ b/EUI_QoL.lua @@ -375,28 +375,32 @@ qolFrame:SetScript("OnEvent", function(self) -- Hide Screenshot Status --------------------------------------------------------------------------- do - local function ApplyScreenshotStatus() + local hooked = false + + local function HideActionStatus() local actionStatus = _G.ActionStatus - if not actionStatus then return end - if not EllesmereUIDB or EllesmereUIDB.hideScreenshotStatus ~= false then - actionStatus:UnregisterEvent("SCREENSHOT_STARTED") - actionStatus:UnregisterEvent("SCREENSHOT_SUCCEEDED") - actionStatus:UnregisterEvent("SCREENSHOT_FAILED") + if actionStatus then actionStatus:Hide() - else - actionStatus:RegisterEvent("SCREENSHOT_STARTED") - actionStatus:RegisterEvent("SCREENSHOT_SUCCEEDED") - actionStatus:RegisterEvent("SCREENSHOT_FAILED") end end + local function ApplyScreenshotStatus() + -- ActionStatus is lazy-created by Blizzard on the first screenshot + -- event, so it may not exist yet. The ssFrame below catches the + -- events and hides it immediately after Blizzard shows it. + end + EllesmereUI._applyScreenshotStatus = ApplyScreenshotStatus local ssFrame = CreateFrame("Frame") - ssFrame:RegisterEvent("PLAYER_LOGIN") - ssFrame:SetScript("OnEvent", function(self) - self:UnregisterEvent("PLAYER_LOGIN") - ApplyScreenshotStatus() + ssFrame:RegisterEvent("SCREENSHOT_STARTED") + ssFrame:RegisterEvent("SCREENSHOT_SUCCEEDED") + ssFrame:RegisterEvent("SCREENSHOT_FAILED") + ssFrame:SetScript("OnEvent", function() + if not EllesmereUIDB or EllesmereUIDB.hideScreenshotStatus ~= false then + -- Hide on next frame so Blizzard's handler runs first + C_Timer.After(0, HideActionStatus) + end end) end diff --git a/EUI_UnlockMode.lua b/EUI_UnlockMode.lua index 9c48a6de..b3a85ff2 100644 --- a/EUI_UnlockMode.lua +++ b/EUI_UnlockMode.lua @@ -125,21 +125,9 @@ if not EllesmereUI.ReapplyOwnAnchor then local centerX = cx - uiW / 2 local centerY = cy - uiH / 2 - -- Snap CENTER offset to a grid that keeps the child's edges on - -- whole pixels. SnapCenterForDim picks the integer or integer+0.5 - -- grid based on the child's pixel-dimension parity. Plain SnapForES - -- would round 540.5 to 541 and force half-pixel edges (1px drift). - local PPa = EllesmereUI.PP - if PPa then - if PPa.SnapCenterForDim then - centerX = PPa.SnapCenterForDim(centerX, cW, cS) - centerY = PPa.SnapCenterForDim(centerY, cH, cS) - elseif PPa.SnapForES then - centerX = PPa.SnapForES(centerX, cS) - centerY = PPa.SnapForES(centerY, cS) - end - end - + -- No explicit snap: the center was computed from pixel-aligned target + -- edges and pixel-aligned child dimensions. Snapping here introduces + -- 1px drift from floating-point dust in the conversions. pcall(function() childBar:ClearAllPoints() childBar:SetPoint("CENTER", UIParent, "CENTER", centerX, centerY) @@ -221,16 +209,11 @@ if not EllesmereUI.NotifyElementResized then return end - -- Snap to physical pixel grid. Edge anchor case (LEFT/RIGHT/TOP/BOTTOM - -- with raw fw/2 or fh/2): adjX/Y represents an EDGE offset that lands - -- on a whole pixel, so SnapForES (round-to-nearest) is correct. - local PPa = EllesmereUI.PP - if PPa and PPa.SnapForES then - local es = frame:GetEffectiveScale() - adjX = PPa.SnapForES(adjX, es) - adjY = PPa.SnapForES(adjY, es) - end - + -- No explicit snap: the edge offset is derived from a stored CENTER + -- value that was computed from pixel-aligned edges. The derivation + -- cx +/- dim/2 reproduces the original edge within floating-point + -- epsilon. Snapping here can round that epsilon the wrong way, + -- causing 1px drift on every reload. pcall(function() frame:ClearAllPoints() frame:SetPoint(anchor, UIParent, "CENTER", adjX, adjY) @@ -1605,30 +1588,16 @@ ApplyAnchorPosition = function(childKey, targetKey, side, noMark, noMove) local centerY = cy - uiH / 2 -- Only move the actual bar frame when noMove is not set - -- Convert UIParent-space offsets to child bar's coordinate space - -- and snap to physical pixel grid. + -- Convert UIParent-space offsets to child bar's coordinate space. + -- No explicit snap: the center was computed from pixel-aligned target + -- edges and pixel-aligned child dimensions. Applying SnapCenterForDim + -- here can shift the computed center by 1 physical pixel due to + -- floating-point dust in the coordinate-space conversions, breaking + -- the exact edge alignment that the anchor system guarantees. if not noMove then local acRatio = uiS / cS local bCenterX = centerX * acRatio local bCenterY = centerY * acRatio - -- Snap with SnapCenterForDim, NOT SnapForES. The two behave - -- differently for odd-pixel-dimension frames: - -- SnapForES rounds the center to the nearest WHOLE pixel, - -- which forces an odd-dim frame to have half-pixel edges - -- (1px drift on every save & exit). - -- SnapCenterForDim picks the right grid based on parity: - -- even dim => center on whole pixel; odd dim => center on - -- integer + 0.5 so both edges land on whole pixels. - -- Without snapping, any tiny sub-pixel error in the stored - -- offset propagates straight to the rendered position. - local PPa = EllesmereUI and EllesmereUI.PP - if PPa and PPa.SnapCenterForDim then - local es = childBar:GetEffectiveScale() - local cwS = childBar:GetWidth() or cW - local chS = childBar:GetHeight() or cH - bCenterX = PPa.SnapCenterForDim(bCenterX, cwS, es) - bCenterY = PPa.SnapCenterForDim(bCenterY, chS, es) - end pcall(function() childBar:ClearAllPoints() childBar:SetPoint("CENTER", UIParent, "CENTER", bCenterX, bCenterY) @@ -1991,27 +1960,14 @@ ApplyCenterPosition = function(barKey, pos) end end - -- Snap to physical pixel grid. Two cases: - -- 1. Edge anchor (LEFT/RIGHT/TOP/BOTTOM with raw fw/2 or fh/2): adjX/Y - -- represents an EDGE offset that should land on a whole pixel. - -- SnapForES (round-to-nearest) is correct here. - -- 2. CENTER anchor (no growDir / growDir=CENTER): adjX/Y is the - -- frame's center offset. For odd-pixel-dim frames, the center - -- must land on a half pixel (integer + 0.5) so that center +/- - -- dim/2 are both whole pixels. SnapCenterForDim handles this. - local PPa = EllesmereUI and EllesmereUI.PP - if PPa and adjX and adjY then - local es = frame:GetEffectiveScale() - if anchor == "CENTER" and PPa.SnapCenterForDim then - local fw = frame:GetWidth() or 0 - local fh = frame:GetHeight() or 0 - adjX = PPa.SnapCenterForDim(adjX, fw, es) - adjY = PPa.SnapCenterForDim(adjY, fh, es) - elseif PPa.SnapForES then - adjX = PPa.SnapForES(adjX, es) - adjY = PPa.SnapForES(adjY, es) - end - end + -- No explicit snap here. The stored CENTER values were computed from + -- pixel-aligned edges (via ConvertToCenterPos reading live frame bounds). + -- Deriving the edge back with cx +/- dim/2 reproduces the original edge + -- exactly (within floating-point epsilon, far below 0.5 px). Applying + -- SnapForES or SnapCenterForDim here would round-to-nearest, which can + -- shift a value like N - 0.0001 to N-1 instead of N, causing 1px drift + -- on every save/load cycle. The renderer handles sub-pixel epsilon + -- correctly without our help. pcall(function() if InCombatLockdown() and frame:IsProtected() then @@ -2441,6 +2397,26 @@ if not EAB then EllesmereUI.ReapplyAllUnlockAnchors() end end) + -- Force a fresh width/height match propagation ~3s after login. + -- By this point CDM has finished its initial rebuild, all unlock + -- elements have registered, and frames are at their final sizes. + -- This guarantees that any element with a stale stored width + -- (e.g. from a previous ui scale, profile switch, or an earlier + -- propagation that was gated off by _cdmRebuilding / zone-transition + -- guards) is corrected to its target's current width without the + -- user having to manually un-match / re-match. + C_Timer.After(3, function() + if EllesmereUI._cdmRebuilding then + -- CDM still rebuilding -- retry a bit later + C_Timer.After(1.5, function() + if EllesmereUI.ApplyAllWidthHeightMatches then + EllesmereUI.ApplyAllWidthHeightMatches() + end + end) + elseif EllesmereUI.ApplyAllWidthHeightMatches then + EllesmereUI.ApplyAllWidthHeightMatches() + end + end) end) end @@ -4720,18 +4696,23 @@ local function CreateMover(barKey) function mover:SyncSize() local bk = self._barKey local elem = registeredElements[bk] - local floor = math.floor if elem and elem.getSize then local gw, gh = elem.getSize(bk) - if gw and gw > 0 then baseW = floor(gw + 0.5) end - if gh and gh > 0 then baseH = floor(gh + 0.5) end + -- Use the raw value from getSize without rounding. The layout + -- function (LayoutCDMBar, LayoutBar, etc.) already snapped + -- dimensions to the physical pixel grid. Re-rounding to the + -- nearest integer shifts values off the pixel grid when + -- PP.mult != 1.0 (e.g. 214.4 -> 214 instead of staying 214.4 + -- which is exactly 402 physical pixels at mult=0.533). + if gw and gw > 0 then baseW = gw end + if gh and gh > 0 then baseH = gh end else local b = GetBarFrame(bk) if b then local s = b:GetEffectiveScale() local uiS = UIParent:GetEffectiveScale() - baseW = floor(((b:GetWidth() or baseW) * s / uiS) + 0.5) - baseH = floor(((b:GetHeight() or baseH) * s / uiS) + 0.5) + baseW = (b:GetWidth() or baseW) * s / uiS + baseH = (b:GetHeight() or baseH) * s / uiS end end -- moverCX/moverCY are already updated by RecenterBarAnchor (called before diff --git a/EllesmereUI.lua b/EllesmereUI.lua index 0551a6fd..b5faeb74 100644 --- a/EllesmereUI.lua +++ b/EllesmereUI.lua @@ -887,14 +887,11 @@ do --------------------------------------------------------------------------- local function SnapBorderTextures(container, frame, borderSize) - -- Calculate thickness that maps to exactly N physical pixels in this - -- frame's coordinate space, regardless of parent scale chains. - -- physPixels = borderSize (always an integer: 1, 2, 3 …) - -- 1 physical pixel in screen coords = 1 / physicalHeight - -- 1 physical pixel in frame coords = (1 / physicalHeight) / effectiveScale - -- = perfect / (768 * effectiveScale) - -- Simplified: perfect / es gives 1 physical pixel in frame coords. - local es = container:GetEffectiveScale() + -- Guard: container may have been recycled by Blizzard (e.g. tooltip + -- frames in the renown panel). Bail if it's no longer valid. + if not container.GetEffectiveScale then return end + local ok, es = pcall(container.GetEffectiveScale, container) + if not ok or not es then return end local onePixel = es > 0 and (PP.perfect / es) or PP.mult local bs = borderSize or 1 local t = bs > 0 and math.max(onePixel, math.floor(bs + 0.5) * onePixel) or 0 @@ -6199,7 +6196,7 @@ end ------------------------------------------------------------------------------- -- Slash commands ------------------------------------------------------------------------------- -EllesmereUI.VERSION = "6.4.7" +EllesmereUI.VERSION = "6.5" -- Register this addon's version into a shared global table (taint-free at load time) if not _G._EUI_AddonVersions then _G._EUI_AddonVersions = {} end @@ -6371,6 +6368,8 @@ if not _G._EUI_ConflictChecked then { addon = "Aloft", label = "Aloft", targets = { "EllesmereUINameplates" } }, { addon = "SenseiClassResourceBar", label = "Sensei Class Resource Bar", targets = { "EllesmereUIResourceBars" } }, { addon = "FriendGroups", label = "FriendGroups", targets = { "EllesmereUIBasics" }, moduleCheck = function() return BasicsModuleEnabled("friends") end }, + { addon = "AccWideUILayoutSelection", label = "Account Wide Interface Settings", targets = { "EllesmereUIBasics" }, moduleCheck = function() return BasicsModuleEnabled("questTracker") end, + message = "Account Wide Interface Settings interferes with the EllesmereUI Quest Tracker. Disable either Account Wide Interface Settings or the EUI Quest Tracker module in Basics settings." }, { addon = "SexyMap", label = "SexyMap", targets = { "EllesmereUIBasics" }, moduleCheck = function() return BasicsModuleEnabled("minimap") end }, { addon = "MinimapButtonButton", label = "MinimapButtonButton", targets = { "EllesmereUIBasics" }, moduleCheck = function() return BasicsModuleEnabled("minimap") end }, -- { addon = "Prat-3.0", label = "Prat", targets = { "EllesmereUIBasics" } }, @@ -7368,27 +7367,23 @@ EllesmereUI.VIS_OPT_ITEMS = { tooltip = "This bar will only show if you have an enemy targeted" }, } --- Runtime check: returns true if the element should be HIDDEN by visibility options. --- `opts` is the settings table containing the vis option booleans. -local DRUID_MOUNT_FORM_IDS = { - [3] = true, -- travel form - [4] = true, -- aquatic form - [27] = true, -- flight form - [29] = true, -- flight form variant -} +-- Cache player class once at load time (never changes). +local _, _playerClass = UnitClass("player") +-- Druid mount-like form spell IDs. Travel Form applies a player aura with +-- spell ID 783 regardless of the active ground/swim/fly subform, so an +-- aura lookup is the most reliable cross-patch detection. local DRUID_MOUNT_FORM_SPELLS = { - [783] = true, -- Travel Form - [1066] = true, -- Aquatic Form - [33943] = true, -- Flight Form - [40120] = true, -- Swift Flight Form - [165962] = true, -- Mount Form - [210053] = true, -- Mount Form (variant) + 783, -- Travel Form + 1066, -- Aquatic Form + 33943, -- Flight Form + 40120, -- Swift Flight Form + 165962, -- Flight Form (variant) + 210053, -- Mount Form (variant) } --- Cache player class once at load time (never changes). -local _, _playerClass = UnitClass("player") - +-- Runtime check: returns true if the element should be HIDDEN by visibility options. +-- `opts` is the settings table containing the vis option booleans. function EllesmereUI.IsPlayerMountedLike() -- Fast path for regular mounts. if IsMounted and IsMounted() then return true end @@ -7396,28 +7391,25 @@ function EllesmereUI.IsPlayerMountedLike() -- Only druids have mount-like shapeshift forms. if _playerClass ~= "DRUID" then return false end - -- Primary check: form ID lookup (covers the common cases). - if GetShapeshiftFormID then - local formID = GetShapeshiftFormID() - if formID and DRUID_MOUNT_FORM_IDS[formID] then - return true - end - end - - -- Spell fallback for mount-form variants whose form IDs may differ. - -- GetShapeshiftFormInfo returns: icon, active, castable, spellID - local form = GetShapeshiftForm and GetShapeshiftForm() - if form and form > 0 and GetShapeshiftFormInfo then - local _, active, _, spellID = GetShapeshiftFormInfo(form) - if active and spellID and DRUID_MOUNT_FORM_SPELLS[spellID] then - return true + -- Aura check: the Travel Form buff is present on the player whenever + -- the druid is shifted, regardless of ground/swim/fly subform. + if C_UnitAuras and C_UnitAuras.GetPlayerAuraBySpellID then + for i = 1, #DRUID_MOUNT_FORM_SPELLS do + if C_UnitAuras.GetPlayerAuraBySpellID(DRUID_MOUNT_FORM_SPELLS[i]) then + return true + end end end return false end -function EllesmereUI.CheckVisibilityOptions(opts) +-- Non-macro visibility subset: the options that CAN'T be expressed in a +-- secure [macro] condition and must be evaluated in Lua. Used by secure +-- action bar frames that delegate the macro-expressible options +-- (target/combat/group) to their state-visibility driver and only need +-- Lua handling for these three. +function EllesmereUI.CheckVisibilityOptionsNonMacro(opts) if not opts then return false end -- Only Show in Instances @@ -7437,17 +7429,25 @@ function EllesmereUI.CheckVisibilityOptions(opts) -- Hide in Housing if opts.visHideHousing then - if C_Map and C_Map.GetBestMapForUnit then - local mapID = C_Map.GetBestMapForUnit("player") - if mapID and mapID > 2600 then return true end + if C_Housing and C_Housing.IsInsideHouseOrPlot and C_Housing.IsInsideHouseOrPlot() then + return true end end - -- Hide when Mounted + -- Hide when Mounted (includes druid travel/flight/aquatic forms) if opts.visHideMounted then if EllesmereUI.IsPlayerMountedLike and EllesmereUI.IsPlayerMountedLike() then return true end end + return false +end + +function EllesmereUI.CheckVisibilityOptions(opts) + if not opts then return false end + + -- Instances / housing / mounted (shared with secure-frame fast path). + if EllesmereUI.CheckVisibilityOptionsNonMacro(opts) then return true end + -- Hide without Target if opts.visHideNoTarget then if not UnitExists("target") then return true end diff --git a/EllesmereUI.toc b/EllesmereUI.toc index ee096aa3..c1567f93 100644 --- a/EllesmereUI.toc +++ b/EllesmereUI.toc @@ -2,7 +2,7 @@ ## Title: |cff0cd29fEllesmereUI|r ## Notes: Shared framework for the EllesmereUI addon suite ## Author: Ellesmere -## Version: 6.4.7 +## Version: 6.5 ## SavedVariables: EllesmereUIDB ## IconTexture: Interface\AddOns\EllesmereUI\media\eg-logo.tga ## X-Curse-Project-ID: 1477613 diff --git a/EllesmereUIActionBars/EllesmereUIActionBars.lua b/EllesmereUIActionBars/EllesmereUIActionBars.lua index ad63846b..b3705ecb 100644 --- a/EllesmereUIActionBars/EllesmereUIActionBars.lua +++ b/EllesmereUIActionBars/EllesmereUIActionBars.lua @@ -2243,7 +2243,10 @@ local function ComputeBarLayout(key) if numRows < 1 then numRows = 1 end local stride = ceil(numIcons / numRows) numRows = ceil(numIcons / stride) - local padding = SnapForScale(s.buttonPadding or 2, 1) + -- Raw coord values -- do NOT pre-snap with SnapForScale (PP.Scale + -- truncates, which loses a pixel at UI scales where PP.mult > 1). + -- Pixel-lock happens below after shape adjustments. + local padding = s.buttonPadding or 2 local isVertical = (s.orientation == "vertical") local growDir = (s.growDirection or "up"):upper() local shape = s.buttonShape or "none" @@ -2258,12 +2261,21 @@ local function ComputeBarLayout(key) btnH = btnH + SHAPE_BTN_EXPAND end if shape == "cropped" then btnH = btnH * 0.80 end - btnW = SnapForScale(btnW, 1) - btnH = SnapForScale(btnH, 1) - local stepW = btnW + padding - local stepH = btnH + padding local PPc = EllesmereUI and EllesmereUI.PP local onePxC = PPc and PPc.mult or 1 + -- Lock btnW / btnH / padding to exact physical pixel multiples so + -- positioning (stepW, stepH) and the frame-size math below use the + -- same pixel grid as the width-match extras (onePxC). Without this, + -- raw coord values drift sub-pixel as col index grows, shrinking + -- spacing and making the last button undershoot the match target. + local btnWPxC = math.floor(btnW / onePxC + 0.5) + local btnHPxC = math.floor(btnH / onePxC + 0.5) + local paddingPxC = math.floor(padding / onePxC + 0.5) + btnW = btnWPxC * onePxC + btnH = btnHPxC * onePxC + padding = paddingPxC * onePxC + local stepW = btnW + padding + local stepH = btnH + padding local extraWC = s._matchExtraPixels or 0 local extraHC = s._matchExtraPixelsH or 0 @@ -2323,23 +2335,16 @@ local function ComputeBarLayout(key) end end - -- Compute frame size in integer physical pixels first, then convert - -- back to coord. Multiplying snapped coord values (e.g. 21.6666... * 3) - -- compounds floating-point dust; PP.Scale's floor then loses 1 phys px - -- when the result lands just below an integer pixel. The button row - -- ends up rendering one pixel wider/taller than the frame, leaving - -- the last button protruding past the mover overlay. + -- Frame size in integer physical pixels, then back to coord. btnW / + -- btnH / padding are already locked to exact pixel multiples above, + -- so these multiplies produce exact pixel counts without floating- + -- point dust or 1px truncation loss. local totalCols = isVertical and numRows or stride local totalRows = isVertical and stride or numRows - local PPlc = EllesmereUI and EllesmereUI.PP - local onePxLc = PPlc and PPlc.mult or 1 - local btnWPx = math.floor(btnW / onePxLc + 0.5) - local btnHPx = math.floor(btnH / onePxLc + 0.5) - local paddingPx = math.floor(padding / onePxLc + 0.5) - local frameWPx = totalCols * btnWPx + (totalCols - 1) * paddingPx + extraWC - local frameHPx = totalRows * btnHPx + (totalRows - 1) * paddingPx + extraHC - local frameW = frameWPx * onePxLc - local frameH = frameHPx * onePxLc + local frameWPx = totalCols * btnWPxC + (totalCols - 1) * paddingPxC + extraWC + local frameHPx = totalRows * btnHPxC + (totalRows - 1) * paddingPxC + extraHC + local frameW = frameWPx * onePxC + local frameH = frameHPx * onePxC return result, max(frameW, 1), max(frameH, 1) end @@ -2365,7 +2370,10 @@ local function LayoutBar(key) if stride < 1 then stride = 1 end -- Recalculate actual rows needed (avoids empty trailing rows) numRows = ceil(numIcons / stride) - local padding = SnapForScale(s.buttonPadding or 2, 1) + -- Raw coord values -- do NOT pre-snap with SnapForScale (PP.Scale + -- truncates, which loses a pixel at UI scales where PP.mult > 1). + -- Pixel-lock happens below after shape adjustments. + local padding = s.buttonPadding or 2 local isVertical = (s.orientation == "vertical") local growDir = (s.growDirection or "up"):upper() local shape = s.buttonShape or "none" @@ -2386,15 +2394,22 @@ local function LayoutBar(key) btnH = btnH * 0.80 end - -- Snap button dimensions - btnW = SnapForScale(btnW, 1) - btnH = SnapForScale(btnH, 1) - local stepW = btnW + padding - local stepH = btnH + padding - -- Width/height match: distribute extra physical pixels across buttons local PP = EllesmereUI and EllesmereUI.PP local onePx = PP and PP.mult or 1 + -- Lock btnW / btnH / padding to exact physical pixel multiples so + -- positioning (stepW) and width-match +1px extras share the same + -- pixel grid. Prevents sub-pixel drift that shrinks visible spacing + -- at UI scales with PP.mult > 1. + local btnWPx = math.floor(btnW / onePx + 0.5) + local btnHPx = math.floor(btnH / onePx + 0.5) + local paddingPx = math.floor(padding / onePx + 0.5) + btnW = btnWPx * onePx + btnH = btnHPx * onePx + padding = paddingPx * onePx + local stepW = btnW + padding + local stepH = btnH + padding + local extraW = s._matchExtraPixels or 0 local extraH = s._matchExtraPixelsH or 0 @@ -4801,14 +4816,10 @@ function EAB:UpdateHousingVisibility() if not inInstance then return true end end if s.visHideHousing then - if C_Map and C_Map.GetBestMapForUnit then - local mapID = C_Map.GetBestMapForUnit("player") - if mapID and mapID > 2600 then return true end + if C_Housing and C_Housing.IsInsideHouseOrPlot and C_Housing.IsInsideHouseOrPlot() then + return true end end - -- Mounted is normally handled by secure [mounted] state conditions. - -- Also check the shared runtime mounted-like helper here so druid - -- travel/flight/aquatic forms hide correctly on non-macro refreshes. if s.visHideMounted then if EllesmereUI and EllesmereUI.IsPlayerMountedLike and EllesmereUI.IsPlayerMountedLike() then return true @@ -6141,7 +6152,7 @@ local function RegisterWithUnlockMode() local PP = EllesmereUI and EllesmereUI.PP local onePx = PP and PP.mult or 1 local physTarget = math.floor(w / onePx + 0.5) - local physPad = math.floor(SnapForScale(pad, 1) / onePx + 0.5) + local physPad = math.floor(pad / onePx + 0.5) local rawPhysBtn = (physTarget - (cols - 1) * physPad) / cols if shape ~= "none" and shape ~= "cropped" then rawPhysBtn = rawPhysBtn - math.floor((SHAPE_BTN_EXPAND or 10) / onePx + 0.5) @@ -6181,7 +6192,7 @@ local function RegisterWithUnlockMode() local PP = EllesmereUI and EllesmereUI.PP local onePx = PP and PP.mult or 1 local physTarget = math.floor(h / onePx + 0.5) - local physPad = math.floor(SnapForScale(pad, 1) / onePx + 0.5) + local physPad = math.floor(pad / onePx + 0.5) local rawPhysBtn = (physTarget - (rows - 1) * physPad) / rows if shape ~= "none" and shape ~= "cropped" then rawPhysBtn = rawPhysBtn - math.floor((SHAPE_BTN_EXPAND or 10) / onePx + 0.5) diff --git a/EllesmereUIActionBars/EllesmereUIActionBars.toc b/EllesmereUIActionBars/EllesmereUIActionBars.toc index f94edc58..497ab474 100644 --- a/EllesmereUIActionBars/EllesmereUIActionBars.toc +++ b/EllesmereUIActionBars/EllesmereUIActionBars.toc @@ -4,7 +4,7 @@ ## Group: EllesmereUI ## Notes: Custom Action Bars ## Author: Ellesmere -## Version: 6.4.7 +## Version: 6.5 ## Dependencies: EllesmereUI ## SavedVariables: EllesmereUIActionBarsDB ## IconTexture: Interface\AddOns\EllesmereUI\media\eg-logo.tga diff --git a/EllesmereUIAuraBuffReminders/EllesmereUIAuraBuffReminders.lua b/EllesmereUIAuraBuffReminders/EllesmereUIAuraBuffReminders.lua index 5c6fb427..5906203c 100644 --- a/EllesmereUIAuraBuffReminders/EllesmereUIAuraBuffReminders.lua +++ b/EllesmereUIAuraBuffReminders/EllesmereUIAuraBuffReminders.lua @@ -2382,11 +2382,6 @@ local function Refresh() CacheInstanceInfo() - -- Suppress all non-beacon reminders in PvP instances (arenas/battlegrounds). - if InPvPInstance() then - HideCombatIcons(); HideCursorIcons(); HideAllIcons(); return - end - -- MEMORY PROBES (temporary -- remove after diagnosis) local _memProbe = _G._EABR_MemProbe local _m0, _m1, _m2, _m3, _m4, _m5, _m6, _m7 @@ -2400,6 +2395,7 @@ local function Refresh() local inInstance = InRealInstancedContent() local inKeystone = InMythicPlusKey() local inCombat = InCombat() + local inPvP = InPvPInstance() -- Collect missing reminders (reuse pooled entry tables) ResetEntryPool() @@ -2421,9 +2417,9 @@ local function Refresh() if _memProbe then _m3 = collectgarbage("count") end --------------------------------------------------------------------------- - -- 3) Consumables (suppressed in M+ keystones and always in combat) + -- 3) Consumables (suppressed in M+ keystones, in combat, and in PvP) --------------------------------------------------------------------------- - if not inKeystone and not inCombat then + if not inKeystone and not inCombat and not inPvP then CollectConsumables(missing, playerClass, specID, inInstance, inKeystone, inCombat) end if _memProbe then _m4 = collectgarbage("count") end diff --git a/EllesmereUIAuraBuffReminders/EllesmereUIAuraBuffReminders.toc b/EllesmereUIAuraBuffReminders/EllesmereUIAuraBuffReminders.toc index 511b847f..c8734699 100644 --- a/EllesmereUIAuraBuffReminders/EllesmereUIAuraBuffReminders.toc +++ b/EllesmereUIAuraBuffReminders/EllesmereUIAuraBuffReminders.toc @@ -4,7 +4,7 @@ ## Group: EllesmereUI ## Notes: Reminders for upkeeping Auras, Buffs and Consumables ## Author: Ellesmere -## Version: 6.4.7 +## Version: 6.5 ## Dependencies: EllesmereUI ## SavedVariables: EllesmereUIAuraBuffRemindersDB ## IconTexture: Interface\AddOns\EllesmereUI\media\eg-logo.tga diff --git a/EllesmereUIBasics/EUI_Basics_QuestTracker_Options.lua b/EllesmereUIBasics/EUI_Basics_QuestTracker_Options.lua index 6151ebbe..f0bb20ae 100644 --- a/EllesmereUIBasics/EUI_Basics_QuestTracker_Options.lua +++ b/EllesmereUIBasics/EUI_Basics_QuestTracker_Options.lua @@ -149,13 +149,13 @@ initFrame:SetScript("OnEvent", function(self) local f = EQT.frame if not f then return end f:SetHeight(v) + local pv = EQT.PAD_V or 6 + local totalH = (f.content and f.content:GetHeight() or 0) + pv * 2 + 7 if f.inner then - local pv = EQT.PAD_V or 6 - local totalH = (f.content and f.content:GetHeight() or 0) + pv * 2 + 7 f.inner:SetHeight(math.min(totalH, v)) if EQT.UpdateInnerAlignment then EQT.UpdateInnerAlignment(f) end end - if f._updateScrollThumb then f._updateScrollThumb() end + if f._updateScrollThumb then f._updateScrollThumb(totalH > v) end end }) y = y - h @@ -170,7 +170,14 @@ initFrame:SetScript("OnEvent", function(self) local br, bg, bb = Cfg("bgR") or 0, Cfg("bgG") or 0, Cfg("bgB") or 0 if EQT.frame and EQT.frame.bg then EQT.frame.bg:SetColorTexture(br, bg, bb, v/100) end end }, - { type="label", text="" }) + { type="toggle", text="Hide Top Line", + getValue=function() return Cfg("showTopLine") == false end, + setValue=function(v) + Set("showTopLine", not v) + if EQT.frame and EQT.frame.topLine then + if v then EQT.frame.topLine:Hide() else EQT.frame.topLine:Show() end + end + end }) do local rgn = bgRow._leftRegion local ctrl = rgn._control @@ -412,7 +419,7 @@ initFrame:SetScript("OnEvent", function(self) end, false, 20) local ctrl = rgn._control - sw:SetPoint("RIGHT", ctrl, "LEFT", -8, 0) + PP.Point(sw, "RIGHT", ctrl, "LEFT", -8, 0) sw:SetScript("OnEnter", function(s) EllesmereUI.ShowWidgetTooltip(s, label .. " Color") end) sw:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) end @@ -532,7 +539,7 @@ initFrame:SetScript("OnEvent", function(self) end, false, 20) local ctrl = rgn._control - sw:SetPoint("RIGHT", ctrl, "LEFT", -8, 0) + PP.Point(sw, "RIGHT", ctrl, "LEFT", -8, 0) sw:SetScript("OnEnter", function(s) EllesmereUI.ShowWidgetTooltip(s, label .. " Color") end) sw:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) end @@ -561,7 +568,7 @@ initFrame:SetScript("OnEvent", function(self) end, false, 20) local ctrl = rgn._control - sw:SetPoint("RIGHT", ctrl, "LEFT", -8, 0) + PP.Point(sw, "RIGHT", ctrl, "LEFT", -8, 0) sw:SetScript("OnEnter", function(s) EllesmereUI.ShowWidgetTooltip(s, "Focused Color") end) sw:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) end diff --git a/EllesmereUIBasics/EllesmereUIBasics.lua b/EllesmereUIBasics/EllesmereUIBasics.lua index e0c0adc9..10d80b95 100644 --- a/EllesmereUIBasics/EllesmereUIBasics.lua +++ b/EllesmereUIBasics/EllesmereUIBasics.lua @@ -277,6 +277,7 @@ local defaults = { secColorUseAccent = true, delveCollapsed = false, questsCollapsed = false, + achievementsCollapsed = false, showPreyQuests = true, preyCollapsed = false, questItemHotkey = nil, @@ -1734,6 +1735,24 @@ local function ApplyMinimap() minimap._texCircBorder:Show() end + -- Live-update border when accent color changes (only when using accent) + if p.useClassColor then + if not minimap._accentBorderCB then + minimap._accentBorderCB = function(ar, ag, ab) + if minimap._ppBorders then + PP.SetBorderColor(minimap, ar, ag, ab, 1) + end + if minimap._circBorder and minimap._circBorder:IsShown() then + minimap._circBorder._tex:SetVertexColor(ar, ag, ab, 1) + end + if minimap._texCircBorder and minimap._texCircBorder:IsShown() then + minimap._texCircBorder:SetVertexColor(ar, ag, ab, 1) + end + end + end + EllesmereUI.RegAccent({ type = "callback", fn = minimap._accentBorderCB }) + end + -- Size minimap:SetScale(1.0) local mapSize = p.mapSize or 140 @@ -7051,7 +7070,7 @@ function EBS:OnEnable() if EllesmereUI and EllesmereUI.RegisterUnlockElements then local MK = EllesmereUI.MakeUnlockElement local function MDB() return EBS.db and EBS.db.profile.minimap end - local function CDB() return EBS.db and EBS.db.profile.chat end + -- CDB removed: chat unlock element stripped (see comment below) EllesmereUI:RegisterUnlockElements({ MK({ key = "EBS_Minimap", @@ -7090,38 +7109,10 @@ function EBS:OnEnable() ApplyMinimap() end, }), - MK({ - key = "EBS_Chat", - label = "Chat", - group = "Basics", - order = 510, - getFrame = function() return ChatFrame1 end, - getSize = function() - return ChatFrame1:GetWidth(), ChatFrame1:GetHeight() - end, - isHidden = function() - local c = CDB() - return not c or not c.enabled - end, - savePos = function(_, point, relPoint, x, y) - local c = CDB(); if not c then return end - c.position = { point = point, relPoint = relPoint, x = x, y = y } - if not EllesmereUI._unlockActive then - ApplyChat() - end - end, - loadPos = function() - local c = CDB() - return c and c.position - end, - clearPos = function() - local c = CDB(); if not c then return end - c.position = nil - end, - applyPos = function() - ApplyChat() - end, - }), + -- Chat unlock element removed: chat module is stripped for rebuild. + -- Registering ChatFrame1 as a moveable caused SetPoint from + -- EllesmereUIBasics context, tainting the frame. Blizzard's + -- ChatHistory_GetToken then failed on the tainted strings. }) end end diff --git a/EllesmereUIBasics/EllesmereUIBasics.toc b/EllesmereUIBasics/EllesmereUIBasics.toc index 7074e8a8..aab5ffc2 100644 --- a/EllesmereUIBasics/EllesmereUIBasics.toc +++ b/EllesmereUIBasics/EllesmereUIBasics.toc @@ -4,7 +4,7 @@ ## Group: EllesmereUI ## Notes: Chat, Minimap, Friends List, Quest Tracker, and Cursor skinning ## Author: Ellesmere -## Version: 6.4.7 +## Version: 6.5 ## Dependencies: EllesmereUI ## SavedVariables: EllesmereUIBasicsDB ## IconTexture: Interface\AddOns\EllesmereUI\media\eg-logo.tga diff --git a/EllesmereUIBasics/EllesmereUIBasics_QuestTracker.lua b/EllesmereUIBasics/EllesmereUIBasics_QuestTracker.lua index b411403c..cb89d2ee 100644 --- a/EllesmereUIBasics/EllesmereUIBasics_QuestTracker.lua +++ b/EllesmereUIBasics/EllesmereUIBasics_QuestTracker.lua @@ -420,6 +420,40 @@ local function TitleRowOnClick(self, btn) end return end + local achievementID = self._achievementID + if achievementID then + local function UntrackAchievement() + if C_ContentTracking and C_ContentTracking.StopTracking + and Enum and Enum.ContentTrackingType and Enum.ContentTrackingType.Achievement then + C_ContentTracking.StopTracking(Enum.ContentTrackingType.Achievement, achievementID, Enum.ContentTrackingStopType.Manual) + EQT:SetDirty(true) + elseif RemoveTrackedAchievement then + RemoveTrackedAchievement(achievementID) + EQT:SetDirty(true) + end + end + if btn == "RightButton" then + ShowContextMenu(self, { + { text = "Untrack Achievement", onClick = UntrackAchievement }, + }) + elseif IsShiftKeyDown() then + UntrackAchievement() + else + -- Open the Achievement UI focused on this achievement + if not AchievementFrame and UIParentLoadAddOn then + UIParentLoadAddOn("Blizzard_AchievementUI") + end + if OpenAchievementFrameToAchievement then + OpenAchievementFrameToAchievement(achievementID) + elseif AchievementFrame_SelectAchievement then + if AchievementFrame and not AchievementFrame:IsShown() and AchievementFrame_ToggleAchievementFrame then + AchievementFrame_ToggleAchievementFrame() + end + AchievementFrame_SelectAchievement(achievementID) + end + end + return + end local qID = self._questID if not qID then return end if btn == "RightButton" then @@ -535,6 +569,7 @@ local function ReleaseRow(r) r.frame._isAutoComplete = nil; r.frame._isComplete = nil r.frame._recipeID = nil; r.frame._isRecraft = nil r.frame._collectableType = nil; r.frame._collectableID = nil + r.frame._achievementID = nil r._baseR, r._baseG, r._baseB = nil, nil, nil r._rowType = nil; r._objIndex = nil; r._objCount = nil if r.numFS then r.numFS:Hide() end @@ -1569,6 +1604,79 @@ local function GetTrackedCollections() return _collections end +------------------------------------------------------------------------------- +-- Tracked Achievements +------------------------------------------------------------------------------- +local _achievements = {} +local _achievement_entries = {} +local _criteria_pool = {} +local _criteria_pool_n = 0 + +local function GetTrackedAchievements() + -- Recycle criteria tables from previous call + for i = 1, #_achievements do + local e = _achievements[i] + if e and e.criteria then + for j = 1, #e.criteria do + _criteria_pool_n = _criteria_pool_n + 1 + _criteria_pool[_criteria_pool_n] = e.criteria[j] + e.criteria[j] = nil + end + end + _achievements[i] = nil + end + + if not (C_ContentTracking and C_ContentTracking.GetTrackedIDs + and Enum and Enum.ContentTrackingType and Enum.ContentTrackingType.Achievement) then + return _achievements + end + + local ids = C_ContentTracking.GetTrackedIDs(Enum.ContentTrackingType.Achievement) + if not ids or #ids == 0 then return _achievements end + + local listN = 0 + for _, achievementID in ipairs(ids) do + local _, name = GetAchievementInfo(achievementID) + if name then + listN = listN + 1 + local entry = _achievement_entries[listN] + if not entry then + entry = { criteria = {} } + _achievement_entries[listN] = entry + end + entry.achievementID = achievementID + entry.name = name + + local critN = 0 + local numCriteria = (GetAchievementNumCriteria and GetAchievementNumCriteria(achievementID)) or 0 + for ci = 1, numCriteria do + local critName, _, completed, quantity, reqQuantity = GetAchievementCriteriaInfo(achievementID, ci) + if critName and critName ~= "" then + local c + if _criteria_pool_n > 0 then + c = _criteria_pool[_criteria_pool_n] + _criteria_pool[_criteria_pool_n] = nil + _criteria_pool_n = _criteria_pool_n - 1 + else + c = {} + end + -- Numeric-progress criteria get a "x/y" prefix; plain criteria are just the text. + if reqQuantity and reqQuantity > 1 and quantity then + c.text = quantity .. "/" .. reqQuantity .. " " .. critName + else + c.text = critName + end + c.finished = completed == true + critN = critN + 1 + entry.criteria[critN] = c + end + end + _achievements[listN] = entry + end + end + return _achievements +end + ------------------------------------------------------------------------------- -- Refresh ------------------------------------------------------------------------------- @@ -1842,7 +1950,7 @@ function EQT:Refresh(skipAlphaFlash) self.rows[#self.rows + 1] = r end - local function AddTitleRow(text, cr, cg, cb, qID, isAutoComplete, isComplete, recipeID, isRecraft, collectableType, collectableID) + local function AddTitleRow(text, cr, cg, cb, qID, isAutoComplete, isComplete, recipeID, isRecraft, collectableType, collectableID, achievementID) local r = AcquireRow(content) if r.numFS then r.numFS:Hide() end SetFontSafe(r.text, tfp, tfs, tff) @@ -1984,6 +2092,9 @@ function EQT:Refresh(skipAlphaFlash) r.frame._collectableType = collectableType; r.frame:EnableMouse(true) r.frame._collectableID = collectableID r.frame:SetScript("OnMouseUp", TitleRowOnClick) + elseif achievementID then + r.frame._achievementID = achievementID; r.frame:EnableMouse(true) + r.frame:SetScript("OnMouseUp", TitleRowOnClick) end r._rowType = "title" yOff = yOff + rh + ROW_GAP @@ -2091,10 +2202,32 @@ function EQT:Refresh(skipAlphaFlash) end end + -- Achievements Tracking section (between recipes and collections) + local achievements = GetTrackedAchievements() + if #achievements > 0 then + if #recipes > 0 then yOff = yOff + 4 end + local ac = db.achievementsCollapsed or false + AddCollapsibleSection("ACHIEVEMENTS", ac, function() + DB().achievementsCollapsed = not DB().achievementsCollapsed; EQT:Refresh() + end) + if not ac then + for _, ach in ipairs(achievements) do + AddTitleRow(ach.name, tc.r, tc.g, tc.b, nil, nil, nil, nil, nil, nil, nil, ach.achievementID) + for _, crit in ipairs(ach.criteria) do + local cr = crit.finished and cc.r or oc.r + local cg = crit.finished and cc.g or oc.g + local cb = crit.finished and cc.b or oc.b + AddObjRow(crit.text, cr, cg, cb, crit.finished) + end + yOff = yOff + 3 + end + end + end + -- Collections Tracking section (after recipes, before delves) local collections = GetTrackedCollections() if #collections > 0 then - if #recipes > 0 then yOff = yOff + 4 end + if #recipes > 0 or #achievements > 0 then yOff = yOff + 4 end local clc = db.collectionsCollapsed or false AddCollapsibleSection("COLLECTIONS", clc, function() DB().collectionsCollapsed = not DB().collectionsCollapsed; EQT:Refresh() @@ -2111,7 +2244,7 @@ function EQT:Refresh(skipAlphaFlash) end -- Scenario / Delve section - local anyAboveScenario = #recipes > 0 or #collections > 0 + local anyAboveScenario = #recipes > 0 or #achievements > 0 or #collections > 0 if scenario then if anyAboveScenario or #watched > 0 or #zone > 0 or #world > 0 then yOff = yOff + 4 end @@ -2256,8 +2389,8 @@ function EQT:Refresh(skipAlphaFlash) yOff = yOff + 10 end - -- Order: Recipes, Collections, Delves, Active WQ, Prey, Zone, World, Quests (bottom) - local anyAbove = #recipes > 0 or #collections > 0 or scenario ~= nil + -- Order: Recipes, Achievements, Collections, Delves, Active WQ, Prey, Zone, World, Quests (bottom) + local anyAbove = #recipes > 0 or #achievements > 0 or #collections > 0 or scenario ~= nil if #active > 0 then if anyAbove then yOff = yOff + 4 end; anyAbove = true @@ -2297,7 +2430,7 @@ function EQT:Refresh(skipAlphaFlash) end) if not qc then RenderList(watched, 0) end end - local hasContent = scenario or #active > 0 or #watched > 0 or #zone > 0 or #world > 0 or #prey > 0 or #recipes > 0 or #collections > 0 + local hasContent = scenario or #active > 0 or #watched > 0 or #zone > 0 or #world > 0 or #prey > 0 or #recipes > 0 or #achievements > 0 or #collections > 0 if not hasContent then if f.inner then f.inner:Hide() end if f.bg then f.bg:Hide() end @@ -2325,7 +2458,10 @@ function EQT:Refresh(skipAlphaFlash) if cur > maxScroll then f.sf:SetVerticalScroll(maxScroll) end - if f._updateScrollThumb then f._updateScrollThumb() end + -- Pass the deterministic overflow answer computed from totalH/maxH + -- so the scrollbar hides immediately when content fits, regardless + -- of whether the ScrollFrame's lazy scroll range has updated yet. + if f._updateScrollThumb then f._updateScrollThumb(totalH > maxH) end end -- Restore visibility after rebuild is complete (prevents teardown flicker) @@ -2525,12 +2661,14 @@ local function BuildFrame() content:SetHeight(1) sf:SetScrollChild(content); f.content = content - -- Thin scrollbar (parented to inner so it isn't clipped by ScrollFrame) + -- Thin scrollbar (parented to inner so it isn't clipped by ScrollFrame). + -- Hidden by default; UpdateScrollThumb shows it only when content overflows. local scrollTrack = CreateFrame("Frame", nil, inner) scrollTrack:SetWidth(4) scrollTrack:SetPoint("TOPRIGHT", inner, "TOPRIGHT", -4, -(PAD_V + 2 + 4)) scrollTrack:SetPoint("BOTTOMRIGHT", inner, "BOTTOMRIGHT", -4, PAD_V + 5 + 4) scrollTrack:SetFrameLevel(sf:GetFrameLevel() + 3) + scrollTrack:Hide() local trackBg = scrollTrack:CreateTexture(nil, "BACKGROUND") trackBg:SetAllPoints() @@ -2552,6 +2690,7 @@ local function BuildFrame() scrollHitArea:SetPoint("BOTTOMRIGHT", inner, "BOTTOMRIGHT", 0, PAD_V + 5 + 4) scrollHitArea:SetFrameLevel(scrollTrack:GetFrameLevel() + 2) scrollHitArea:EnableMouse(true) + scrollHitArea:Hide() scrollHitArea:RegisterForDrag("LeftButton") scrollHitArea:SetScript("OnDragStart", function() end) scrollHitArea:SetScript("OnDragStop", function() end) @@ -2577,11 +2716,22 @@ local function BuildFrame() end local SCROLLBAR_ALPHA = 0.35 - - local function UpdateScrollThumb() + scrollTrack:SetAlpha(SCROLLBAR_ALPHA) + + -- overflowHint: optional authoritative signal from callers that computed + -- overflow from known content/viewport heights (refresh path). Needed + -- because ScrollFrame:GetVerticalScrollRange() updates lazily on the + -- next layout pass, so calling it right after SetHeight can return a + -- stale non-zero value and leave the scrollbar visible when content fits. + local function UpdateScrollThumb(overflowHint) local maxScroll = EllesmereUI.SafeScrollRange(sf) - if maxScroll <= 0 then scrollTrack:SetAlpha(0); return end - scrollTrack:SetAlpha(SCROLLBAR_ALPHA) + if overflowHint == false or maxScroll <= 0 then + scrollTrack:Hide() + scrollHitArea:Hide() + return + end + scrollTrack:Show() + scrollHitArea:Show() local trackH = scrollTrack:GetHeight() local visH = sf:GetHeight() local visibleRatio = visH / (visH + maxScroll) @@ -2990,6 +3140,10 @@ function EQT:Init() "CONTENT_TRACKING_LIST_UPDATE", "CONTENT_TRACKING_UPDATE", "TRACKING_TARGET_INFO_UPDATE", + "TRACKED_ACHIEVEMENT_UPDATE", + "TRACKED_ACHIEVEMENT_LIST_CHANGED", + "ACHIEVEMENT_EARNED", + "CRITERIA_UPDATE", } local ZONE_EVENTS = {"ZONE_CHANGED_NEW_AREA","ZONE_CHANGED"} @@ -3032,6 +3186,8 @@ function EQT:Init() TRACKED_RECIPE_UPDATE = true, CONTENT_TRACKING_LIST_UPDATE = true, CONTENT_TRACKING_UPDATE = true, + TRACKED_ACHIEVEMENT_LIST_CHANGED = true, + ACHIEVEMENT_EARNED = true, } local SCENARIO_EVENTS = { SCENARIO_CRITERIA_UPDATE = true, @@ -3469,12 +3625,12 @@ function EQT:Init() h = math.max(60, math.floor(h + 0.5)) DB().height = h f:SetHeight(h) + local totalH = (f.content and f.content:GetHeight() or 0) + PAD_V * 2 + 7 if f.inner then - local totalH = (f.content and f.content:GetHeight() or 0) + PAD_V * 2 + 7 f.inner:SetHeight(math.min(totalH, h)) UpdateInnerAlignment(f) end - if f._updateScrollThumb then f._updateScrollThumb() end + if f._updateScrollThumb then f._updateScrollThumb(totalH > h) end end, savePos = function(_, point, relPoint, x, y) DB().pos = { point = point, relPoint = relPoint, x = x, y = y } diff --git a/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua b/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua index 6fa37db7..c9cc2868 100644 --- a/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua +++ b/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua @@ -115,55 +115,6 @@ initFrame:SetScript("OnEvent", function(self) --------------------------------------------------------------------------- -- Buff spell list from viewer pool - -- Enumerates active frames in BuffIconCooldownViewer, resolves spell IDs - -- using frame:GetSpellID() first, then cooldownInfo fallback. - --------------------------------------------------------------------------- - local function IsUntaintedPositive(v) - if type(v) ~= "number" then return false end - if issecretvalue(v) then return false end - return v > 0 - end - - local function ResolveBuffFrameSpellID(frame) - -- Try the frame's own spell ID first, then cooldown info as fallback. - if frame.GetSpellID then - local sid = frame:GetSpellID() - if IsUntaintedPositive(sid) then return sid end - end - local cdID = frame.cooldownID - if not cdID then return nil end - local gci = C_CooldownViewer and C_CooldownViewer.GetCooldownViewerCooldownInfo - if not gci then return nil end - local info = gci(cdID) - if not info then return nil end - local sid = info.overrideSpellID or info.spellID - if IsUntaintedPositive(sid) then return sid end - return nil - end - - local function GetTrackedBuffSpellList() - local seen, list = {}, {} - local buffViewer = _G["BuffIconCooldownViewer"] - if not buffViewer or not buffViewer.itemFramePool then return list end - local temp = {} - for frame in buffViewer.itemFramePool:EnumerateActive() do - local sid = ResolveBuffFrameSpellID(frame) - if sid and sid > 0 and not seen[sid] then - seen[sid] = true - local li = frame.layoutIndex - temp[#temp + 1] = { sid = sid, li = (type(li) == "number") and li or 0 } - end - end - table.sort(temp, function(a, b) - if a.li ~= b.li then return a.li < b.li end - return a.sid < b.sid - end) - for _, e in ipairs(temp) do - list[#list + 1] = e.sid - end - return list - end - --------------------------------------------------------------------------- -- Bar Glows page buff action button glow assignments) --------------------------------------------------------------------------- @@ -3790,32 +3741,15 @@ initFrame:SetScript("OnEvent", function(self) -- Use the same data source as CD/utility: GetCDMSpellsForBar local allSpells = ns.GetCDMSpellsForBar and ns.GetCDMSpellsForBar(targetBarKey) or {} - local ghostKey = ns.GHOST_BUFF_BAR_KEY - - -- Categorize spells: every buff spell is shown. Spells assigned to - -- any buff bar are grayed out with a tooltip. Ghost bar spells are - -- clickable (can be restored to the current bar). - local knownSpells = {} -- buff spells from picker (always learned) + -- Every buff spell is shown and every row is clickable. Clicking a + -- spell routes AddTrackedSpell, whose family sweep removes the + -- spell from every other buff-family bar (including the ghost + -- hidden bar) before claiming it for the target. Same model as + -- CD/utility bars: one click, one move. + local knownSpells = {} for _, sp in ipairs(allSpells) do - if sp.cdmCatGroup ~= "buff" then - -- skip non-buff category spells - else - local sid = sp.spellID - local routedBar = ns.ResolveVariantValue - and ns._divertedSpells - and ns.ResolveVariantValue(ns._divertedSpells, sid) - local isHidden = ns.IsBuffSpellHidden and ns.IsBuffSpellHidden(sid) - - -- Determine which buff bar this spell is currently routed to - local assignedBarKey = nil - if not isHidden and routedBar and routedBar ~= ghostKey then - if ns.IsBarBuffFamily and ns.IsBarBuffFamily(routedBar) then - assignedBarKey = routedBar - end - end - - sp._assignedBarKey = assignedBarKey + if sp.cdmCatGroup == "buff" then knownSpells[#knownSpells + 1] = sp end end @@ -3840,7 +3774,7 @@ initFrame:SetScript("OnEvent", function(self) local mH = 4 -- Helper: create a spell row - local function MakeSpellRow(sp, disabled, disabledMsg) + local function MakeSpellRow(sp) local item = CreateFrame("Button", nil, inner) item:SetHeight(ITEM_H) item:SetPoint("TOPLEFT", inner, "TOPLEFT", 1, -mH) @@ -3852,7 +3786,6 @@ initFrame:SetScript("OnEvent", function(self) iconTex:SetPoint("LEFT", 4, 0) iconTex:SetTexture(sp.icon) iconTex:SetTexCoord(0.08, 0.92, 0.08, 0.92) - if disabled then iconTex:SetDesaturated(true) end local lbl = item:CreateFontString(nil, "OVERLAY") lbl:SetFont(FONT_PATH, 11, GetCDMOptOutline()) @@ -3860,81 +3793,32 @@ initFrame:SetScript("OnEvent", function(self) lbl:SetPoint("RIGHT", -4, 0) lbl:SetJustifyH("LEFT") lbl:SetText(sp.name or "") - if disabled then - lbl:SetTextColor(tDimR * 0.5, tDimG * 0.5, tDimB * 0.5, tDimA * 0.5) - else - lbl:SetTextColor(tDimR, tDimG, tDimB, tDimA) - end + lbl:SetTextColor(tDimR, tDimG, tDimB, tDimA) local hl = item:CreateTexture(nil, "ARTWORK") hl:SetAllPoints(); hl:SetColorTexture(1, 1, 1, 0) - if disabled then - item:SetScript("OnEnter", function() - hl:SetColorTexture(1, 1, 1, hlA * 0.3) - if disabledMsg then - EllesmereUI.ShowWidgetTooltip(item, disabledMsg) - end - end) - item:SetScript("OnLeave", function() - hl:SetColorTexture(1, 1, 1, 0) - EllesmereUI.HideWidgetTooltip() - end) - else - item:SetScript("OnEnter", function() - lbl:SetTextColor(1, 1, 1, 1) - hl:SetColorTexture(1, 1, 1, hlA) - end) - item:SetScript("OnLeave", function() - lbl:SetTextColor(tDimR, tDimG, tDimB, tDimA) - hl:SetColorTexture(1, 1, 1, 0) - end) - end + item:SetScript("OnEnter", function() + lbl:SetTextColor(1, 1, 1, 1) + hl:SetColorTexture(1, 1, 1, hlA) + end) + item:SetScript("OnLeave", function() + lbl:SetTextColor(tDimR, tDimG, tDimB, tDimA) + hl:SetColorTexture(1, 1, 1, 0) + end) mH = mH + ITEM_H return item end - -- Helper: add a divider - local function AddDivider() - local div = inner:CreateTexture(nil, "ARTWORK") - div:SetHeight(1) - div:SetColorTexture(1, 1, 1, 0.10) - div:SetPoint("TOPLEFT", inner, "TOPLEFT", 1, -mH - 4) - div:SetPoint("TOPRIGHT", inner, "TOPRIGHT", -1, -mH - 4) - mH = mH + 9 - end - - local function RenderSpell(sp) - local assigned = sp._assignedBarKey - if assigned then - local rbd = ns.barDataByKey and ns.barDataByKey[assigned] - local barName = rbd and (rbd.name or assigned) or assigned - MakeSpellRow(sp, true, "Already assigned to " .. barName) - else - -- Available: add to this bar - local item = MakeSpellRow(sp, false) - item:SetScript("OnClick", function() - menu:Hide() - if onChanged then onChanged(sp.spellID) end - end) - end - end - - -- Available spells first, then assigned ones at the bottom - local availableSpells, assignedSpells = {}, {} for _, sp in ipairs(knownSpells) do - if sp._assignedBarKey then - assignedSpells[#assignedSpells + 1] = sp - else - availableSpells[#availableSpells + 1] = sp - end + local item = MakeSpellRow(sp) + item:SetScript("OnClick", function() + menu:Hide() + if onChanged then onChanged(sp.spellID) end + end) end - for _, sp in ipairs(availableSpells) do RenderSpell(sp) end - if #availableSpells > 0 and #assignedSpells > 0 then AddDivider() end - for _, sp in ipairs(assignedSpells) do RenderSpell(sp) end - inner:SetHeight(mH + 4) local totalH = math.min(mH + 4, MAX_H) menu:SetSize(menuW, totalH) @@ -4118,23 +4002,8 @@ initFrame:SetScript("OnEvent", function(self) end) rmItem:SetScript("OnClick", function() menu:Hide() - -- Main buff bar: hide via ghost bar (no assignedSpells to remove from) - -- Extra buff bars: use RemoveTrackedSpell like CD/utility bars - local bd = ns.barDataByKey and ns.barDataByKey[barKey] - local isMainBuff = bd and bd.key == "buffs" - if isMainBuff and anchorFrame and anchorFrame._previewSpellID then - ns.HideBuffSpell(anchorFrame._previewSpellID) - if ns.FullCDMRebuild then ns.FullCDMRebuild("spell_remove") end - Refresh() - C_Timer.After(0.05, function() - if ns.CDMApplyVisibility then ns.CDMApplyVisibility() end - if _cdmPreview and _cdmPreview.Update then _cdmPreview:Update() end - UpdateCDMPreviewAndResize() - end) - else - ns.RemoveTrackedSpell(barKey, slotIndex) - RefreshCDPreview() - end + ns.RemoveTrackedSpell(barKey, slotIndex) + RefreshCDPreview() end) allItems[#allItems + 1] = rmItem @@ -5269,9 +5138,14 @@ initFrame:SetScript("OnEvent", function(self) for _, rEntry in ipairs(racialList) do local rSid = type(rEntry) == "table" and rEntry[1] or rEntry local reqClass = type(rEntry) == "table" and rEntry.class or nil - if not reqClass or reqClass == _pClass then + local excludeClass = type(rEntry) == "table" and rEntry.notClass or nil + local classOk = (not reqClass or reqClass == _pClass) + and (not excludeClass or excludeClass ~= _pClass) + if classOk then local inBook = C_SpellBook and C_SpellBook.IsSpellInSpellBook and C_SpellBook.IsSpellInSpellBook(rSid) if not inBook then rSid = nil end + else + rSid = nil end if rSid then local rName = C_Spell.GetSpellName(rSid) @@ -6414,25 +6288,6 @@ initFrame:SetScript("OnEvent", function(self) end local bd = SelectedCDMBar() if not bd then return end - -- Main buff bar: hide via ghost bar (no assignedSpells) - -- Extra buff bars fall through to RemoveTrackedSpell below - if bd.key == "buffs" then - local sid = self._previewSpellID - if not sid then return end - if button == "MiddleButton" then - ns.HideBuffSpell(sid) - if ns.FullCDMRebuild then ns.FullCDMRebuild("spell_remove") end - Refresh() - C_Timer.After(0.05, function() - if ns.CDMApplyVisibility then ns.CDMApplyVisibility() end - if _cdmPreview and _cdmPreview.Update then _cdmPreview:Update() end - UpdateCDMPreviewAndResize() - end) - elseif button == "RightButton" or button == "LeftButton" then - ShowSpellPicker(self, bd.key, self._slotIdx, {}, function() end, true) - end - return - end if button == "MiddleButton" then local si = self._slotIdx @@ -6772,19 +6627,14 @@ initFrame:SetScript("OnEvent", function(self) end if ns.IsBarBuffFamily(bd) then - -- Buff bars use ShowBuffBarPicker (its own UI for popular - -- buffs + tracked buff spells + manual ID entry). - local pickerBarKey = bd.key - ShowBuffBarPicker(self, pickerBarKey, function(restoredSID) - if restoredSID then - -- Unhide from ghost bar if hidden - if ns.IsBuffSpellHidden and ns.IsBuffSpellHidden(restoredSID) then - ns.UnhideBuffSpell(restoredSID) - end - -- For extra buff bars, add the spell to this bar - if pickerBarKey ~= "buffs" then - ns.AddTrackedSpell(pickerBarKey, restoredSID) - end + -- Buff bars use ShowBuffBarPicker (walks the BuffIcon + -- viewer pool). Click routes AddTrackedSpell -- the + -- family sweep removes the spell from every other + -- buff-family bar (including the ghost hidden bar, which + -- is the "unhide" step) before claiming it for bd.key. + ShowBuffBarPicker(self, bd.key, function(newSpellID) + if newSpellID then + ns.AddTrackedSpell(bd.key, newSpellID) end FinalizeAdd() end) @@ -6834,57 +6684,14 @@ initFrame:SetScript("OnEvent", function(self) local isBuffBar = ns.IsBarBuffFamily(bd) local isCustomBuffBar = (bd.barType == "custom_buff") local isFocusKick = (bd.key == "focuskick") - local tracked - local count - local isMainBuffBar = isBuffBar and (bd.key == "buffs") - - -- (debug removed) - - if isMainBuffBar then - -- Main buff bar preview: use category API (all tracked buff spells). - tracked = {} - local allBuffSpells = GetTrackedBuffSpellList() - for _, sid in ipairs(allBuffSpells) do - -- Skip spells hidden via the ghost buff bar - if ns.IsBuffSpellHidden and ns.IsBuffSpellHidden(sid) then - -- excluded - else - -- Only consider routed if target is a BUFF bar (not CD/utility) - local routedBar = ns.ResolveVariantValue - and ns._divertedSpells - and ns.ResolveVariantValue(ns._divertedSpells, sid) - local routedIsBuff = false - if routedBar then - local rbd = ns.barDataByKey and ns.barDataByKey[routedBar] - routedIsBuff = ns.IsBarBuffFamily and rbd and ns.IsBarBuffFamily(rbd) or false - end - local effectiveRoute = routedIsBuff and routedBar or nil - if not effectiveRoute or effectiveRoute == bd.key then - tracked[#tracked + 1] = sid - end - end - end - count = #tracked - else - -- CD/utility/custom: read from assignedSpells (existing behavior) - local sdUpd = EnsureAssignedSpells(bd.key) - local rawTracked = sdUpd and sdUpd.assignedSpells or {} - tracked = rawTracked - if not isCustomBuffBar and #rawTracked > 0 then - tracked = {} - for _, sid in ipairs(rawTracked) do - if not sid or sid <= 0 then - tracked[#tracked + 1] = sid - elseif ns.IsSpellKnownInCDM(sid) then - tracked[#tracked + 1] = sid - elseif not ns.IsSpellInAnyCDMCategory(sid) then - tracked[#tracked + 1] = sid - end - end - end - count = #tracked - end + -- All bar types (CD/utility/buff/custom) read from assignedSpells. + -- assignedSpells is pure user intent -- every entry is something + -- the user wants on this bar, and the route map + live CDM viewer + -- decide at runtime whether the frame is currently available. + local sdUpd = EnsureAssignedSpells(bd.key) + local tracked = sdUpd and sdUpd.assignedSpells or {} + local count = #tracked -- Use the same stride logic as the runtime (ComputeTopRowStride) local stride, topRowCount @@ -8234,9 +8041,15 @@ initFrame:SetScript("OnEvent", function(self) iconTooltip = function() return "Preview Sound" end, } + -- Spell dropdown values/order -- rebuilt live on every dropdown + -- click (see OnClick hook below) so the list always reflects what + -- is currently on the focuskick bar, even if the user added or + -- removed spells via the spell picker without closing options. local spellValues = {} local spellOrder = {} - do + local function RebuildSpellOptions() + wipe(spellValues) + for i = #spellOrder, 1, -1 do spellOrder[i] = nil end local sd = ns.GetBarSpellData and ns.GetBarSpellData("focuskick") local list = sd and sd.assignedSpells if list then @@ -8259,8 +8072,10 @@ initFrame:SetScript("OnEvent", function(self) spellOrder[#spellOrder + 1] = "__none" end end + RebuildSpellOptions() - _, h = W:DualRow(parent, y, + local focusKickRow + focusKickRow, h = W:DualRow(parent, y, { type = "dropdown", text = "Focus Cast Sound", values = soundValues, order = soundOrder, getValue = function() return BD().focusCastSoundKey or "none" end, @@ -8280,6 +8095,22 @@ initFrame:SetScript("OnEvent", function(self) end end }); y = y - h + -- Live refresh: every click on the Interrupt Spell dropdown + -- rebuilds the option list from the bar's current spells and + -- invalidates the cached menu so the new options appear. + do + local rightRgn = focusKickRow and focusKickRow._rightRegion + local ddBtn = rightRgn and rightRgn._control + if ddBtn then + local origOnClick = ddBtn:GetScript("OnClick") + ddBtn:SetScript("OnClick", function(self, ...) + RebuildSpellOptions() + if ddBtn._invalidateMenu then ddBtn._invalidateMenu() end + if origOnClick then origOnClick(self, ...) end + end) + end + end + _, h = W:Spacer(parent, y, 8); y = y - h else _, h = W:Spacer(parent, y, 8); y = y - h @@ -8824,13 +8655,32 @@ initFrame:SetScript("OnEvent", function(self) false, 20) PP.Point(scSwatch, "RIGHT", ctrl, "LEFT", -12, 0) rightRgn._lastInline = scSwatch + + local scBlock = CreateFrame("Frame", nil, scSwatch) + scBlock:SetAllPoints(); scBlock:SetFrameLevel(scSwatch:GetFrameLevel() + 10); scBlock:EnableMouse(true) + scBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(scSwatch, EllesmereUI.DisabledTooltip("Enable Item Count")) + end) + scBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) EllesmereUI.RegisterWidgetRefresh(function() if updateScSwatch then updateScSwatch() end + local on = BD().showItemCount ~= false + scSwatch:SetAlpha(on and 1 or 0.3) + if on then scBlock:Hide() else scBlock:Show() end end) + local on = BD().showItemCount ~= false + scSwatch:SetAlpha(on and 1 or 0.3) + if on then scBlock:Hide() else scBlock:Show() end local _, scCogShow = EllesmereUI.BuildCogPopup({ title = "Stack Count", rows = { + { type="toggle", label="Show Item Count", + get=function() return BD().showItemCount ~= false end, + set=function(v) + BD().showItemCount = v + ns.RefreshCDMIconAppearance(BD().key); ns.BuildAllCDMBars(); Refresh(); UpdateCDMPreview(); EllesmereUI:RefreshPage() + end }, { type="slider", label="X Offset", min=-50, max=50, step=1, get=function() return BD().stackCountX or 0 end, set=function(v) diff --git a/EllesmereUICooldownManager/EllesmereUICdmHooks.lua b/EllesmereUICooldownManager/EllesmereUICdmHooks.lua index 7d8857cf..0ed3a507 100644 --- a/EllesmereUICooldownManager/EllesmereUICdmHooks.lua +++ b/EllesmereUICooldownManager/EllesmereUICdmHooks.lua @@ -155,26 +155,36 @@ ns.ResolveFrameSpellID = ResolveFrameSpellID ------------------------------------------------------------------------------- -- Spell Routing State -- --- _divertedSpells: variant-keyed map of every spell ID that's been claimed --- by a specific bar (default OR custom OR ghost). Built --- once by RebuildSpellRouteMap from the bar list. Queried --- per-frame at reanchor time by ResolveCDIDToBar. A spell --- with no entry here falls through to the bar that owns --- the viewer pool the frame was enumerated from. +-- _divertedSpellsBuff / _divertedSpellsCD: +-- variant-keyed maps of every spell ID claimed by a bar. +-- Split by viewer family so the same spellID (e.g. Divine +-- Shield 642, which exists as both a cooldown in the +-- essential viewer AND a buff in the buff viewer) can +-- route independently per family. Without the split, a +-- later pass writing the same spellID for a different +-- family would clobber the earlier pass and the frame +-- would fall through to its viewer default. +-- Built once by RebuildSpellRouteMap from the bar list. +-- Queried per-frame at reanchor time by ResolveCDIDToBar +-- using the viewer's family. -- -- _cdidRouteMap: memoization cache, cooldownID -> barKey. Lazily -- populated by ResolveCDIDToBar on first lookup. Wiped --- by RebuildSpellRouteMap. +-- by RebuildSpellRouteMap. Safe as a single map because +-- a given cooldownID only exists in ONE viewer, so the +-- buff-vs-CD family is already implicit in the key. ------------------------------------------------------------------------------- local _cdidRouteMap = {} -local _divertedSpells = {} -ns._divertedSpells = _divertedSpells +local _divertedSpellsBuff = {} +local _divertedSpellsCD = {} +ns._divertedSpellsBuff = _divertedSpellsBuff +ns._divertedSpellsCD = _divertedSpellsCD -- Sentinel: set true at the end of RebuildSpellRouteMap on a successful -- build. CollectAndReanchor's safety net tests this (NOT _cdidRouteMap, --- which is a lazy cache and intentionally empty post-build, NOT --- _divertedSpells, which can legitimately be empty for users with no +-- which is a lazy cache and intentionally empty post-build, NOT the +-- diversion maps, which can legitimately be empty for users with no -- diversions). local _routeMapBuilt = false @@ -199,9 +209,17 @@ local _routeMapBuilt = false --- Priority for collisions (rare under the 1-spell-per-bar invariant): --- ghost bars (lowest) -> custom buff -> custom CD/util -> default bars --- (highest). Later passes overwrite earlier via preserveExisting=false. +--- +--- Family split: each bar writes to either _divertedSpellsBuff (buff +--- family) or _divertedSpellsCD (non-buff family). This prevents a +--- buff-family bar and a CD-family bar from clobbering each other's +--- diversion entries when they both claim the same spellID (e.g. Divine +--- Shield, which has a cooldown frame AND a buff frame under the same +--- spellID 642). function ns.RebuildSpellRouteMap() wipe(_cdidRouteMap) - wipe(_divertedSpells) + wipe(_divertedSpellsBuff) + wipe(_divertedSpellsCD) _routeMapBuilt = false local p = ECME.db and ECME.db.profile @@ -210,12 +228,15 @@ function ns.RebuildSpellRouteMap() local SVV = ns.StoreVariantValue if not SVV then return end + local IsBuffFamily = ns.IsBarBuffFamily + local function CollectDiversionsFor(bd) local sd = ns.GetBarSpellData(bd.key) if not sd or not sd.assignedSpells then return end + local targetMap = IsBuffFamily and IsBuffFamily(bd) and _divertedSpellsBuff or _divertedSpellsCD for _, sid in ipairs(sd.assignedSpells) do if type(sid) == "number" and sid > 0 then - SVV(_divertedSpells, sid, bd.key, false) + SVV(targetMap, sid, bd.key, false) end end end @@ -226,10 +247,22 @@ function ns.RebuildSpellRouteMap() CollectDiversionsFor(bd) end end - -- Pass 2: custom buff bars + -- Pass 2: custom buff bars (extra buff bars) + custom_buff (TBB) bars. + -- TBB bars compete for the same buff icon spells, so their diversions + -- must land in _divertedSpellsBuff even though IsBarBuffFamily returns + -- false for custom_buff. We write directly to _divertedSpellsBuff here. for _, bd in ipairs(p.cdmBars.bars) do - if bd.enabled and bd.barType == "buffs" and bd.key ~= "buffs" and not bd.isGhostBar then - CollectDiversionsFor(bd) + if bd.enabled and not bd.isGhostBar + and ((bd.barType == "buffs" and bd.key ~= "buffs") + or bd.barType == "custom_buff") then + local sd = ns.GetBarSpellData(bd.key) + if sd and sd.assignedSpells then + for _, sid in ipairs(sd.assignedSpells) do + if type(sid) == "number" and sid > 0 then + SVV(_divertedSpellsBuff, sid, bd.key, false) + end + end + end end end -- Pass 3: custom CD/utility bars @@ -254,12 +287,14 @@ end --- Lazily resolve a cooldownID to a bar key. Called per-frame at reanchor --- time. Uses _cdidRouteMap as a memoization cache; on cache miss, computes ---- the route from the diversion set or falls back to viewerDefaultBar (the ---- bar that owns the viewer the frame came from). Caches the result. +--- the route from the per-family diversion map or falls back to +--- viewerDefaultBar (the bar that owns the viewer the frame came from). +--- Caches the result. --- --- viewerDefaultBar is "cooldowns" / "utility" / "buffs" depending on which --- viewer pool the frame was enumerated from -- this is the user-visible ---- ground truth, not the static category API. +--- ground truth, not the static category API. It also tells us which +--- family to consult (buffs -> _divertedSpellsBuff, otherwise CD). local function ResolveCDIDToBar(cdID, viewerDefaultBar) if not cdID then return viewerDefaultBar end local cached = _cdidRouteMap[cdID] @@ -272,20 +307,22 @@ local function ResolveCDIDToBar(cdID, viewerDefaultBar) return viewerDefaultBar end + local divertMap = (viewerDefaultBar == "buffs") and _divertedSpellsBuff or _divertedSpellsCD + local info = gci(cdID) local routedBar = nil if info then if info.spellID and info.spellID > 0 then - routedBar = RVV(_divertedSpells, info.spellID) + routedBar = RVV(divertMap, info.spellID) end if not routedBar and info.overrideSpellID and info.overrideSpellID > 0 and info.overrideSpellID ~= info.spellID then - routedBar = RVV(_divertedSpells, info.overrideSpellID) + routedBar = RVV(divertMap, info.overrideSpellID) end if not routedBar and info.linkedSpellIDs then for _, lid in ipairs(info.linkedSpellIDs) do if type(lid) == "number" and lid > 0 then - routedBar = RVV(_divertedSpells, lid) + routedBar = RVV(divertMap, lid) if routedBar then break end end end @@ -629,14 +666,33 @@ local function DecorateFrame(frame, barData) cdw:SetUseAuraDisplayTime(false) end if cdw.SetCooldownFromDurationObject then + -- Resolve effective spell ID: when a spell is + -- transformed (e.g. Judgment -> Hammer of Wrath + -- under Wings), Blizzard's charge/cooldown APIs + -- report against the override ID, not the base. + -- Query the override first and fall back to the + -- base ID so non-transformed spells still work. + local effID = sid2 + if C_SpellBook and C_SpellBook.FindSpellOverrideByID then + local ovr = C_SpellBook.FindSpellOverrideByID(sid2) + if ovr and ovr > 0 and ovr ~= sid2 then + effID = ovr + end + end local hasCharges = type(frame.HasVisualDataSource_Charges) == "function" and frame:HasVisualDataSource_Charges() local durObj if hasCharges and C_Spell.GetSpellChargeDuration then - durObj = C_Spell.GetSpellChargeDuration(sid2) + durObj = C_Spell.GetSpellChargeDuration(effID) + if not durObj and effID ~= sid2 then + durObj = C_Spell.GetSpellChargeDuration(sid2) + end end if not durObj and C_Spell.GetSpellCooldownDuration then - durObj = C_Spell.GetSpellCooldownDuration(sid2) + durObj = C_Spell.GetSpellCooldownDuration(effID) + if not durObj and effID ~= sid2 then + durObj = C_Spell.GetSpellCooldownDuration(sid2) + end end if durObj then cdw:SetCooldownFromDurationObject(durObj) @@ -682,7 +738,7 @@ local function CategorizeFrame(frame, viewerBarKey) local claimBD = barDataByKey[claimBarKey] local claimType = claimBD and claimBD.barType or claimBarKey local viewerIsBuff = (viewerBarKey == "buffs") - local claimIsBuff = (claimType == "buffs") + local claimIsBuff = (claimType == "buffs" or claimType == "custom_buff") if viewerIsBuff == claimIsBuff then return claimBarKey, displaySID, baseSID end @@ -1051,9 +1107,9 @@ local function CollectAndReanchor() -- Safety: if RebuildSpellRouteMap has never run successfully (API was -- unavailable during zone-in rebuild, e.g. fast arena transitions), - -- attempt a fresh rebuild now. Test the build sentinel, NOT _divertedSpells - -- (which can legitimately be empty for users with no diversions) and NOT - -- _cdidRouteMap (which is a lazy cache, intentionally empty post-build). + -- attempt a fresh rebuild now. Test the build sentinel, NOT the + -- diversion maps (which can legitimately be empty for users with no + -- diversions) and NOT _cdidRouteMap (lazy cache, empty post-build). if not _routeMapBuilt and ns.RebuildSpellRouteMap then ns.RebuildSpellRouteMap() end @@ -1391,7 +1447,8 @@ local function CollectAndReanchor() end end if f._itemCountText then - if total > 1 then + local showItemCount = barData.showItemCount ~= false + if total > 1 and showItemCount then f._itemCountText:SetText(total) f._itemCountText:Show() else @@ -1430,7 +1487,21 @@ local function CollectAndReanchor() if not hasClaim then local isRacial = ns._myRacialsSet and ns._myRacialsSet[sid] local isCustomSpell = sd and sd.customSpellIDs and sd.customSpellIDs[sid] - if not isRacial and not isCustomSpell and ns.IsSpellKnownInCDM and not ns.IsSpellKnownInCDM(sid) then + -- Phase 3 injects custom frames for spells that + -- are NOT in Blizzard's CDM category (user-added + -- racials, user-added customs). If a spell IS in + -- CDM, Blizzard's native frame is authoritative + -- and renders on its own bar; injecting here + -- would produce a ghost duplicate that the user + -- cannot remove from the live bar (the picker + -- only touches assignedSpells). Example: a + -- Dracthyr Evoker utility preset with Wing + -- Buffet in assignedSpells -- Blizzard already + -- tracks it in CDM, so we must not inject. + local isKnownInCDM = ns.IsSpellKnownInCDM and ns.IsSpellKnownInCDM(sid) + if isKnownInCDM then + -- Leave it to Blizzard's native frame. Skip. + elseif not isRacial and not isCustomSpell then -- Unknown spell, skip else local fkey = barKey .. ":" .. (isRacial and "racial" or "custom") .. ":" .. sid @@ -1709,6 +1780,23 @@ local function CollectAndReanchor() end end + -- Per-spec one-shot: fold legacy dormantSpells back into assignedSpells + -- at their saved slot index. Under the new "assignedSpells is pure user + -- intent" model, dormant entries are restored so spells the old + -- reconcile system evicted (pet abilities, choice-node talents) return + -- to the user's chosen position. Rebuild the route map afterward so + -- the revived entries become diversions. + if ns.MergeDormantSpellsIntoAssigned then + local specKey2 = ns.GetActiveSpecKey and ns.GetActiveSpecKey() + local sa2 = EllesmereUIDB and EllesmereUIDB.spellAssignments + local prof2 = sa2 and sa2.specProfiles and specKey2 and sa2.specProfiles[specKey2] + if prof2 and not prof2._dormantMerged then + ns.MergeDormantSpellsIntoAssigned() + if ns.RebuildSpellRouteMap then ns.RebuildSpellRouteMap() end + if ns.QueueReanchor then ns.QueueReanchor() end + end + end + if ns.RequestBarGlowUpdate then ns.RequestBarGlowUpdate() end -- Authoritative final layout pass. Set by CDMFinishSetup (login) and diff --git a/EllesmereUICooldownManager/EllesmereUICdmSpellPicker.lua b/EllesmereUICooldownManager/EllesmereUICdmSpellPicker.lua index fead3b49..52abc419 100644 --- a/EllesmereUICooldownManager/EllesmereUICdmSpellPicker.lua +++ b/EllesmereUICooldownManager/EllesmereUICdmSpellPicker.lua @@ -245,8 +245,8 @@ ns.EnumerateCDMViewerSpells = EnumerateCDMViewerSpells -- (default bars, custom bars, ghost bars). Variant-aware via IsVariantOf so -- adding the same spell under a different variant ID collapses to a no-op. -- --- These are the canonical functions; AddTrackedSpell / RemoveTrackedSpell / --- HideBuffSpell / UnhideBuffSpell now delegate to them. +-- These are the canonical functions; AddTrackedSpell / RemoveTrackedSpell +-- delegate to them. ------------------------------------------------------------------------------- --- Find the index of an entry in a spell list. @@ -571,6 +571,61 @@ function ns.MigrateSpecToBarFilterModelV6() return addedCount end +--- One-shot per-spec migration: merge any pre-existing dormantSpells back +--- into assignedSpells at their stored slot index. The old reconcile model +--- evicted "currently-unknown" spells (pet abilities, choice-node talents, +--- etc.) into dormantSpells to preserve their position. Under the new model +--- assignedSpells is pure user intent and is never mutated based on +--- "is this spell currently known", so dormant entries must be folded back +--- in at their saved positions. After this runs, sd.dormantSpells is wiped. +--- Flagged per-spec via prof._dormantMerged so it only runs once. +function ns.MergeDormantSpellsIntoAssigned() + local sa = EllesmereUIDB and EllesmereUIDB.spellAssignments + local sp = sa and sa.specProfiles + if not sp then return end + + local specKey = ns.GetActiveSpecKey() + if not specKey or specKey == "0" then return end + + local prof = sp[specKey] + if not prof or prof._dormantMerged then return end + if not prof.barSpells then prof._dormantMerged = true; return end + + for _barKey, bs in pairs(prof.barSpells) do + if type(bs) == "table" and type(bs.dormantSpells) == "table" then + if not bs.assignedSpells then bs.assignedSpells = {} end + + -- Collect dormant entries sorted by saved slot (lowest first) + -- so earlier inserts don't shift later slots. + local returning = {} + for sid, slot in pairs(bs.dormantSpells) do + if type(sid) == "number" and sid ~= 0 and type(slot) == "number" then + returning[#returning + 1] = { sid = sid, slot = slot } + end + end + table.sort(returning, function(a, b) return a.slot < b.slot end) + + -- Build dedup set for the active list so we don't double-insert + local activeSet = {} + for _, sid in ipairs(bs.assignedSpells) do activeSet[sid] = true end + + for _, entry in ipairs(returning) do + if not activeSet[entry.sid] then + local insertAt = entry.slot + if insertAt > #bs.assignedSpells + 1 then insertAt = #bs.assignedSpells + 1 end + if insertAt < 1 then insertAt = 1 end + table.insert(bs.assignedSpells, insertAt, entry.sid) + activeSet[entry.sid] = true + end + end + + bs.dormantSpells = nil + end + end + + prof._dormantMerged = true +end + --- Lazy-seed assignedSpells from the bar's currently rendered icons. --- Called by reorder helpers (Swap/Move) when the user reorders a bar --- whose assignedSpells is empty -- captures the current visible order so @@ -939,18 +994,18 @@ function ns.RemoveTrackedSpell(barKey, idx) end end - -- Route the removed spell to the ghost CD bar so frames stay in the - -- routing system but are hidden. Buff-type bars skip ghost routing - -- (the spell returns to the main buff bar naturally). Negative IDs - -- (presets/trinkets) and non-viewer spells (customs, racials) skip - -- ghost routing too. + -- Route the removed spell to the appropriate ghost bar so frames stay + -- in the routing system but are hidden. Buff-family bars route to the + -- ghost buff bar; CD/utility bars route to the ghost CD bar. Negative + -- IDs (presets/trinkets) and non-viewer spells (customs, racials) skip + -- ghost routing entirely. local bd = barDataByKey[barKey] - local isBuff = bd and (bd.barType == "buffs") local isNonViewer = removedID and removedID > 0 and ((sd.customSpellIDs and sd.customSpellIDs[removedID]) or (ns._myRacialsSet and ns._myRacialsSet[removedID])) - if removedID and removedID > 0 and not isBuff and not isNonViewer then - ns.AddSpellToBar(ns.GHOST_CD_BAR_KEY, removedID) + if removedID and removedID > 0 and not isNonViewer then + local ghostKey = IsBarBuffFamily(barKey) and ns.GHOST_BUFF_BAR_KEY or ns.GHOST_CD_BAR_KEY + ns.AddSpellToBar(ghostKey, removedID) end local frame = cdmBarFrames[barKey] @@ -1026,7 +1081,7 @@ function ns.AddCDMBar(barType, name, numRows) bgR = 0.08, bgG = 0.08, bgB = 0.08, bgA = 0.6, iconZoom = 0.08, iconShape = "none", verticalOrientation = false, barBgEnabled = false, barBgR = 0, barBgG = 0, barBgB = 0, - showCooldownText = true, cooldownFontSize = 12, + showCooldownText = true, showItemCount = true, cooldownFontSize = 12, showCharges = true, chargeFontSize = 11, desaturateOnCD = true, swipeAlpha = 0.7, activeStateAnim = "blizzard", @@ -1131,26 +1186,3 @@ function ns.RemoveCDMBar(key) return false end -------------------------------------------------------------------------------- --- Ghost Buff Bar helpers: route/unroute spells to hide them from buff bars -------------------------------------------------------------------------------- -function ns.HideBuffSpell(spellID) - if ns.AddSpellToBar(ns.GHOST_BUFF_BAR_KEY, spellID) then - if ns.RebuildSpellRouteMap then ns.RebuildSpellRouteMap() end - if ns.QueueReanchor then ns.QueueReanchor() end - end -end - -function ns.UnhideBuffSpell(spellID) - if ns.RemoveSpellFromBar(ns.GHOST_BUFF_BAR_KEY, spellID) then - if ns.RebuildSpellRouteMap then ns.RebuildSpellRouteMap() end - if ns.QueueReanchor then ns.QueueReanchor() end - end -end - -function ns.IsBuffSpellHidden(spellID) - if not _IsUsableSID(spellID) then return false end - local sd = ns.GetBarSpellData(ns.GHOST_BUFF_BAR_KEY) - if not sd or not sd.assignedSpells then return false end - return FindVariantIndex(sd.assignedSpells, spellID) ~= nil -end diff --git a/EllesmereUICooldownManager/EllesmereUICooldownManager.lua b/EllesmereUICooldownManager/EllesmereUICooldownManager.lua index 5db817b5..6d577052 100644 --- a/EllesmereUICooldownManager/EllesmereUICooldownManager.lua +++ b/EllesmereUICooldownManager/EllesmereUICooldownManager.lua @@ -24,18 +24,6 @@ local GetTime = GetTime ns.DEFAULT_MAPPING_NAME = "Buff Name (eg: Divine Purpose)" -local RECONCILE = { - readyDelay = 0.5, - retryDelay = 1, - retryMax = 5, - lastSpecChangeAt = 0, - lastZoneInAt = 0, - pending = false, - retries = 0, - retryToken = 0, -} - - ------------------------------------------------------------------------------- -- Shape Constants (shared with action bars) ------------------------------------------------------------------------------- @@ -142,7 +130,12 @@ local RACE_RACIALS = { ZandalariTroll = { 291944 }, Vulpera = { 312411 }, Mechagnome = { 312924 }, - Dracthyr = { 357214, { 368970, class = "EVOKER" } }, + -- Wing Buffet (357214) is available to every Dracthyr class, but + -- Evokers already have it tracked by Blizzard's CDM category, so + -- gate the custom-racial entry off for Evokers to avoid duplicate + -- injection. Tail Swipe (368970) is Evoker-only and also in CDM, + -- so it is omitted from this list entirely. + Dracthyr = { { 357214, notClass = "EVOKER" } }, EarthenDwarf = { 436344 }, Haranir = { 1287685 }, } @@ -284,7 +277,7 @@ local DEFAULTS = { barVisibility = "always", housingHideEnabled = true, visHideHousing = true, visOnlyInstances = false, visHideMounted = false, visHideNoTarget = false, visHideNoEnemy = false, - showCooldownText = true, showTooltip = false, showKeybind = false, + showCooldownText = true, showItemCount = true, showTooltip = false, showKeybind = false, keybindSize = 10, keybindOffsetX = 2, keybindOffsetY = -2, keybindR = 1, keybindG = 1, keybindB = 1, keybindA = 0.9, }, @@ -303,7 +296,7 @@ local DEFAULTS = { barVisibility = "always", housingHideEnabled = true, visHideHousing = true, visOnlyInstances = false, visHideMounted = false, visHideNoTarget = false, visHideNoEnemy = false, - showCooldownText = true, showTooltip = false, showKeybind = false, + showCooldownText = true, showItemCount = true, showTooltip = false, showKeybind = false, keybindSize = 10, keybindOffsetX = 2, keybindOffsetY = -2, keybindR = 1, keybindG = 1, keybindB = 1, keybindA = 0.9, }, @@ -322,7 +315,7 @@ local DEFAULTS = { barVisibility = "always", housingHideEnabled = true, visHideHousing = true, visOnlyInstances = false, visHideMounted = false, visHideNoTarget = false, visHideNoEnemy = false, - showCooldownText = true, showTooltip = false, showKeybind = false, + showCooldownText = true, showItemCount = true, showTooltip = false, showKeybind = false, keybindSize = 10, keybindOffsetX = 2, keybindOffsetY = -2, keybindR = 1, keybindG = 1, keybindB = 1, keybindA = 0.9, }, @@ -425,23 +418,6 @@ function ns.GetCharKey() return name .. "-" .. realm end -function ns.IsReconcileReady() - local p = ECME.db and ECME.db.profile - if not p then return false end - if not ns.GetActiveSpecKey() then return false end - local now = GetTime() - if RECONCILE.lastSpecChangeAt > 0 and (now - RECONCILE.lastSpecChangeAt) < RECONCILE.readyDelay then return false end - if RECONCILE.lastZoneInAt > 0 and (now - RECONCILE.lastZoneInAt) < RECONCILE.readyDelay then return false end - if not (C_CooldownViewer and C_CooldownViewer.GetCooldownViewerCategorySet) then return false end - for cat = 0, 3 do - local knownIDs = C_CooldownViewer.GetCooldownViewerCategorySet(cat, false) - if knownIDs and next(knownIDs) then - return true - end - end - return true -end - local function EnsureSpec(profile, key) profile.spec[key] = profile.spec[key] or { mappings = {}, selectedMapping = 1 } return profile.spec[key] @@ -504,10 +480,6 @@ MAIN_BAR_KEYS[GHOST_BUFF_BAR_KEY] = true local GHOST_CD_BAR_KEY = "__ghost_cd" MAIN_BAR_KEYS[GHOST_CD_BAR_KEY] = true --- Bar types that support talent-aware dormant slot persistence. --- Trinket/racial/potion and buff bars are excluded. -local TALENT_AWARE_BAR_TYPES = { cooldowns = true, utility = true } - ------------------------------------------------------------------------------- -- Resolve the best spellID from a CooldownViewerCooldownInfo struct. -- Priority: overrideSpellID > first linkedSpellID > spellID. @@ -2172,26 +2144,41 @@ BuildCDMBar = function(barIndex) end end else - local pos = p.cdmBarPositions[key] - if pos and pos.point then - -- Skip for unlock-anchored bars (anchor system is authority) - local unlockKey = "CDM_" .. key - local anchored = EllesmereUI.IsUnlockAnchored and EllesmereUI.IsUnlockAnchored(unlockKey) - if not anchored or not frame:GetLeft() then - ApplyBarPositionCentered(frame, pos, key) - end + -- If the bar is unlock-anchored and already positioned, DO NOT touch + -- its position. The anchor system (ApplyAnchorPosition / PropagateAnchorChain) + -- is authoritative for unlock-anchored bars. Previously, a rebuild + -- during combat (or any transient state) could fall into the "no + -- legacy pos saved" branch below and teleport the bar to a hardcoded + -- default (e.g. CENTER 0,-275 for cooldowns, CENTER 0,0 for custom + -- bars). The anchor system would later re-propagate, but if the + -- anchor target was temporarily unavailable (hidden frame, pre-layout + -- race, etc.) the re-anchor would bail and the bar would stay stuck + -- at the hardcoded fallback. + local unlockKey = "CDM_" .. key + local anchored = EllesmereUI.IsUnlockAnchored and EllesmereUI.IsUnlockAnchored(unlockKey) + if anchored and frame:GetLeft() then + -- Unlock-anchored and already has bounds: leave position alone. else - -- Default fallback positions - frame:ClearAllPoints() - if key == "cooldowns" then - frame:SetPoint("CENTER", UIParent, "CENTER", 0, -275) - elseif key == "utility" then - frame:SetPoint("CENTER", UIParent, "CENTER", 0, -320) - elseif key == "buffs" then - frame:SetPoint("CENTER", UIParent, "CENTER", 0, -365) - else - frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + local pos = p.cdmBarPositions[key] + if pos and pos.point then + ApplyBarPositionCentered(frame, pos, key) + elseif not anchored then + -- Default fallback positions (only for truly un-anchored bars + -- with no saved position). + frame:ClearAllPoints() + if key == "cooldowns" then + frame:SetPoint("CENTER", UIParent, "CENTER", 0, -275) + elseif key == "utility" then + frame:SetPoint("CENTER", UIParent, "CENTER", 0, -320) + elseif key == "buffs" then + frame:SetPoint("CENTER", UIParent, "CENTER", 0, -365) + else + frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + end end + -- If anchored but frame has no bounds yet, do not set a fallback + -- position. ReapplyOwnAnchor runs after BuildAllCDMBars and will + -- place the frame correctly once the target is available. end end @@ -2234,12 +2221,14 @@ local function CountCDMBarSpells(barKey) end local function ComputeCDMBarSize(barData, count) - local iW = SnapForScale(barData.iconSize or 36, 1) + -- Raw coord values -- see LayoutCDMBar for why we don't pre-snap + -- with SnapForScale. + local iW = barData.iconSize or 36 local iH = iW if (barData.iconShape or "none") == "cropped" then - iH = SnapForScale(math.floor((barData.iconSize or 36) * 0.80 + 0.5), 1) + iH = math.floor((barData.iconSize or 36) * 0.80 + 0.5) end - local sp = SnapForScale(barData.spacing or 2, 1) + local sp = barData.spacing or 2 local rows = barData.numRows or 1 if rows < 1 then rows = 1 end local stride = ComputeTopRowStride(barData, count) @@ -2287,7 +2276,12 @@ LayoutCDMBar = function(barKey) local numRows = barData.numRows or 1 if numRows < 1 then numRows = 1 end local isHoriz = (grow == "RIGHT" or grow == "LEFT" or grow == "CENTER") - local spacing = SnapForScale(barData.spacing or 2, 1) + -- spacing is a raw coord value; the per-frame pixel conversion below + -- (spacingPx = floor(spacing / onePx + 0.5)) rounds to nearest whole + -- physical pixel. Do NOT pre-snap with SnapForScale: PP.Scale truncates, + -- which can lose a pixel at UI scales where PP.mult > 1 (e.g. spacing=2 + -- gets truncated from 2 coord to 1.0667 coord = 1 px instead of 2 px). + local spacing = barData.spacing or 2 -- Width/height match: derive iconSize live from the SOURCE bar's -- current width on every layout pass. The source bar IS the truth, so @@ -2358,14 +2352,16 @@ LayoutCDMBar = function(barKey) if not iconW then -- Not matched, OR target frame couldn't be read (early in build, -- before source bar exists, etc.). Use the user's stored iconSize. - -- A subsequent LayoutCDMBar pass will re-read live and correct it. - iconW = SnapForScale(barData.iconSize or 36, 1) + -- Raw coord value: the per-frame pixel conversion below rounds to + -- nearest whole physical pixel. Do NOT pre-snap with SnapForScale + -- (see spacing note above). + iconW = barData.iconSize or 36 end local iconH = iconW local shape = barData.iconShape or "none" if shape == "cropped" then - iconH = SnapForScale(math.floor((barData.iconSize or 36) * 0.80 + 0.5), 1) + iconH = math.floor((barData.iconSize or 36) * 0.80 + 0.5) end -- Use ALL icons in the array (not just IsShown). CollectAndReanchor @@ -2382,14 +2378,36 @@ LayoutCDMBar = function(barKey) local sd = ns.GetBarSpellData(barKey) if sd and sd.assignedSpells then local visibleAssigned = 0 + local isRacialSet = ns._myRacialsSet + local customSet = sd.customSpellIDs + local IsKnown = ns.IsSpellKnownInCDM for _, sid in ipairs(sd.assignedSpells) do if sid == -13 or sid == -14 then local slot = -sid local tf = ns._trinketFrames and ns._trinketFrames[slot] local hasItem = GetInventoryItemID("player", slot) ~= nil if hasItem and tf and tf._trinketIsOnUse then visibleAssigned = visibleAssigned + 1 end - elseif sid and sid ~= 0 then + elseif sid and sid <= -100 then + -- Item preset: always counted (frame exists if icon resolved) visibleAssigned = visibleAssigned + 1 + elseif sid and sid > 0 then + -- Positive spell: this counter reserves a minimum slot + -- count so the bar doesn't shrink while Phase 3 custom + -- injection runs. Only count spells that Phase 3 will + -- actually render as a custom frame: racials and user + -- customs that are NOT already in Blizzard's CDM + -- category. CDM-known spells are handled by Blizzard's + -- native frame (counted via `count` if on this bar, or + -- not counted at all if on another bar). Unknown spells + -- are skipped entirely. + local isKnownInCDM = IsKnown and IsKnown(sid) + if not isKnownInCDM then + local isRacial = isRacialSet and isRacialSet[sid] + local isCustom = customSet and customSet[sid] + if isRacial or isCustom then + visibleAssigned = visibleAssigned + 1 + end + end end end sizeCount = math.max(count, visibleAssigned) @@ -2431,6 +2449,15 @@ LayoutCDMBar = function(barKey) local iconWPx = math.floor(iconW / onePx + 0.5) local iconHPx = math.floor(iconH / onePx + 0.5) local spacingPx = math.floor(spacing / onePx + 0.5) + -- Lock iconW / iconH / spacing to exact physical pixel multiples. + -- Positioning (stepW, stepH) uses these coord values, and the width- + -- match math uses the iconWPx / spacingPx integers. If coord and + -- pixel values aren't in lockstep, icons drift sub-pixel as col index + -- grows -- making spacing "shrink" and the final icon undershoot the + -- width-match target by 1 px. + iconW = iconWPx * onePx + iconH = iconHPx * onePx + spacing = spacingPx * onePx local totalWPx, totalHPx if isHoriz then totalWPx = stride * iconWPx + (stride - 1) * spacingPx + extraPixels @@ -2440,12 +2467,14 @@ LayoutCDMBar = function(barKey) totalHPx = stride * iconHPx + (stride - 1) * spacingPx + extraPixelsH end - -- CENTER grow anchors icons to the container's center point. Center must - -- land on a physical pixel boundary, which requires an even pixel total. - if grow == "CENTER" then - if totalWPx % 2 == 1 then totalWPx = totalWPx + 1 end - if totalHPx % 2 == 1 then totalHPx = totalHPx + 1 end - end + -- NOTE: a previous "force even totalWPx for CENTER grow" adjustment + -- used to live here. It is no longer needed: SnapCenterForDim (used by + -- ApplyBarPositionCentered for CENTER-anchored frames) places the + -- frame's center on a half-pixel grid when the dimension is odd, so + -- both edges still land on whole physical pixels. The old +1 padded + -- the frame 1 px wider than the actual icon layout (icons+spacing), + -- leaving an empty pixel strip at the right/bottom that showed up as + -- the unlock-mode overlay being 1 px bigger than the rightmost icon. local totalW = totalWPx * onePx local totalH = totalHPx * onePx @@ -3002,6 +3031,7 @@ local function RefreshCDMIconAppearance(barKey) local scSize = (barData.stackCountSize or 11) * fontScale local scR, scG, scB = barData.stackCountR or 1, barData.stackCountG or 1, barData.stackCountB or 1 local scX, scY = barData.stackCountX or 0, (barData.stackCountY or 0) + 2 + local showItemCount = barData.showItemCount ~= false local borderLvl = icon:GetFrameLevel() + 5 local textLvl = 25 -- Applications (buff stacks / aura applications) @@ -3012,6 +3042,7 @@ local function RefreshCDMIconAppearance(barKey) SetBlizzCDMFont(appsFS, scFont, scSize, scR, scG, scB) appsFS:ClearAllPoints() appsFS:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", scX, scY) + if showItemCount then appsFS:Show() else appsFS:Hide() end end end -- ChargeCount (spell charges like Holy Power spenders) @@ -3022,6 +3053,7 @@ local function RefreshCDMIconAppearance(barKey) SetBlizzCDMFont(chargeFS, scFont, scSize, scR, scG, scB) chargeFS:ClearAllPoints() chargeFS:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", scX, scY) + if showItemCount then chargeFS:Show() else chargeFS:Hide() end end end -- Item count text (potions/healthstones) -- our own frame, safe to reparent @@ -3030,6 +3062,7 @@ local function RefreshCDMIconAppearance(barKey) SetBlizzCDMFont(icon._itemCountText, scFont, scSize, scR, scG, scB) icon._itemCountText:ClearAllPoints() icon._itemCountText:SetPoint("BOTTOMRIGHT", txOverlay or icon, "BOTTOMRIGHT", scX, scY) + if showItemCount then icon._itemCountText:Show() else icon._itemCountText:Hide() end end -- Update keybind text style @@ -3117,7 +3150,7 @@ local function EnsureFocusKickBar() iconZoom = 0.08, iconShape = "none", verticalOrientation = false, barBgEnabled = false, barBgR = 0, barBgG = 0, barBgB = 0, - showCooldownText = true, cooldownFontSize = 12, + showCooldownText = true, showItemCount = true, cooldownFontSize = 12, showCharges = true, chargeFontSize = 11, desaturateOnCD = true, swipeAlpha = 0.7, activeStateAnim = "blizzard", @@ -4348,8 +4381,8 @@ end --- Repopulate all main bars from Blizzard CDM for the current spec. --- Wipes ONLY Blizzard-sourced entries (positive spell IDs that the CDM ---- viewer owns) from assignedSpells/removedSpells/dormantSpells, then ---- rebuilds route maps and reanchors. Preserves user-added entries: +--- viewer owns) from assignedSpells/removedSpells, then rebuilds route +--- maps and reanchors. Preserves user-added entries: --- * Negative IDs (trinket slots -13/-14, item presets <= -100) --- * Custom spell IDs (entries in sd.customSpellIDs) --- * Racial spells (entries in _myRacialsSet) @@ -4392,39 +4425,43 @@ function ns.RepopulateFromBlizzard() end end - -- Filter Blizzard entries off all CD/utility bars (main + custom). - -- Skip ghost, buffs, and custom_buff bars -- they're handled separately + -- Filter Blizzard entries off all CD/utility/buff bars (main + custom). + -- Skip ghost and custom_buff bars -- they're handled separately -- (or not at all, in custom_buff's case, since they're a separate system). for _, barData in ipairs(p.cdmBars.bars) do - if not barData.isGhostBar and barData.key ~= "buffs" + if not barData.isGhostBar and (barData.barType == "cooldowns" or barData.barType == "utility" + or barData.barType == "buffs" or MAIN_BAR_KEYS[barData.key]) then local sd = ns.GetBarSpellData(barData.key) if sd then FilterListPreservingUserAdded(sd, sd.assignedSpells) FilterSetPreservingUserAdded(sd, sd.removedSpells) - FilterSetPreservingUserAdded(sd, sd.dormantSpells) -- spellSettings is per-spell config (font color, etc.) -- preserve -- entirely so user-added customs keep their styling. end end end - -- Ghost CD bar holds Blizzard-owned spells the user explicitly hid. + -- Ghost bars hold Blizzard-owned spells the user explicitly hid. -- Filter the same way so user-added presets that may have been routed -- here (rare edge case) are preserved. local ghostSD = ns.GetBarSpellData(GHOST_CD_BAR_KEY) if ghostSD then FilterListPreservingUserAdded(ghostSD, ghostSD.assignedSpells) FilterSetPreservingUserAdded(ghostSD, ghostSD.removedSpells) - FilterSetPreservingUserAdded(ghostSD, ghostSD.dormantSpells) + end + local ghostBuffSD = ns.GetBarSpellData(GHOST_BUFF_BAR_KEY) + if ghostBuffSD then + FilterListPreservingUserAdded(ghostBuffSD, ghostBuffSD.assignedSpells) + FilterSetPreservingUserAdded(ghostBuffSD, ghostBuffSD.removedSpells) end -- (Site #10 re-snapshot deleted: under the new model, "repopulate from -- Blizzard" is just "wipe diversions and let the route map's spillover -- show everything from the viewer." The wipes above already cleared - -- assignedSpells / removedSpells / dormantSpells / spellSettings and the - -- ghost CD bar -- nothing else needed.) + -- assignedSpells / removedSpells / spellSettings and the ghost CD bar + -- -- nothing else needed.) ns.FullCDMRebuild("repopulate") if ns.CollectAndReanchor then ns.CollectAndReanchor() end @@ -4437,8 +4474,9 @@ function ns.RepopulateFromBlizzard() -- bar's icons and append any positive spell IDs (Blizzard frames) -- not already present, preserving the existing user-entry order. for _, barData in ipairs(p.cdmBars.bars) do - if not barData.isGhostBar and barData.key ~= "buffs" + if not barData.isGhostBar and (barData.barType == "cooldowns" or barData.barType == "utility" + or barData.barType == "buffs" or MAIN_BAR_KEYS[barData.key]) then local sd = ns.GetBarSpellData(barData.key) local icons = ns.cdmBarIcons and ns.cdmBarIcons[barData.key] @@ -4684,7 +4722,10 @@ function ECME:OnEnable() for _, entry in ipairs(racialList) do local sid = type(entry) == "table" and entry[1] or entry local reqClass = type(entry) == "table" and entry.class or nil - if not reqClass or reqClass == _playerClass then + local excludeClass = type(entry) == "table" and entry.notClass or nil + local classOk = (not reqClass or reqClass == _playerClass) + and (not excludeClass or excludeClass ~= _playerClass) + if classOk then _myRacials[#_myRacials + 1] = sid _myRacialsSet[sid] = true end @@ -4756,263 +4797,15 @@ end -- (ForcePopulateBlizzardViewers removed -- replaced by viewer hooks) -------------------------------------------------------------------------------- --- Talent-Aware Reconcile --- When talents change, instead of wiping assignedSpells and losing ordering, --- this function: --- 1) Moves unavailable spells from the active list to dormantSpells with --- their original slot index preserved --- 2) Re-inserts any dormant spells that became available again at their --- saved slot position (pushing existing spells forward) --- 3) Appends genuinely new spells (not previously tracked) at the end --- Applies to: cooldown bar, utility bar, custom cooldown/utility bars -------------------------------------------------------------------------------- -local function TalentAwareReconcile() - local p = ECME.db and ECME.db.profile - if not p or not p.cdmBars then return end - - local knownSet = BuildAvailableSpellPool() - - -- Build a reverse override map from CDM cooldownInfo: - -- overrideSpellID -> base spellID. Handles conditional overrides - -- (e.g. Glacial Spike -> Frostbolt) where C_Spell.GetBaseSpell - -- may not work because the relationship is state-dependent. - local overrideToBase = {} - if C_CooldownViewer and C_CooldownViewer.GetCooldownViewerCategorySet - and C_CooldownViewer.GetCooldownViewerCooldownInfo then - for cat = 0, 3 do - local allIDs = C_CooldownViewer.GetCooldownViewerCategorySet(cat, true) - if allIDs then - for _, cdID in ipairs(allIDs) do - local info = C_CooldownViewer.GetCooldownViewerCooldownInfo(cdID) - if info and info.overrideSpellID and info.overrideSpellID > 0 - and info.spellID and info.spellID > 0 - and info.overrideSpellID ~= info.spellID then - overrideToBase[info.overrideSpellID] = info.spellID - end - end - end - end - end - - -- Helper: reconcile a single spell list (assignedSpells) - -- Returns the new active list with dormant spells removed and returning - -- spells re-inserted at their saved positions. - -- classSpellSet: optional set of ALL class spellIDs (from the full CDM - -- category set). When provided, spells in this set are never moved to - -- dormant -- they are permanent class abilities that may appear missing - -- from the "currently known" set during API timing gaps. - local function ReconcileSpellList(spellList, dormant, removed, classSpellSet) - if not spellList then return nil, dormant end - if not dormant then dormant = {} end - - -- Phase 1: separate active list into still-known and newly-dormant - -- Also check IsPlayerSpell as a fallback for spells the CDM viewer - -- hasn't updated yet (e.g. choice-node talent swaps). - -- For conditional overrides (e.g. Glacial Spike from Frostbolt), - -- also check the base spell -- the override may not be "known" when - -- its condition isn't met (e.g. icicles = 0 on reload/zone change). - local _IPS = IsPlayerSpell - local _GBS = C_Spell and C_Spell.GetBaseSpell - local active = {} - local seenInActive = {} - for i, sid in ipairs(spellList) do - if sid and sid ~= 0 then - if sid < 0 then - -- Negative IDs are items/trinkets -- always keep - active[#active + 1] = sid - seenInActive[sid] = true - elseif seenInActive[sid] then - -- Duplicate already in active list -- skip silently - elseif knownSet[sid] or (_IPS and _IPS(sid)) - or (classSpellSet and classSpellSet[sid]) then - active[#active + 1] = sid - seenInActive[sid] = true - else - -- Check if this is an override whose base spell is known - -- (e.g. Glacial Spike stored while Frostbolt is the base). - -- Try C_Spell.GetBaseSpell first, fall back to CDM - -- cooldownInfo reverse map for conditional overrides. - local base = _GBS and _GBS(sid) - if base == sid then base = nil end - if not base then base = overrideToBase[sid] end - if base and base > 0 and base ~= sid - and (_IPS and _IPS(sid)) - and (knownSet[base] or (_IPS and _IPS(base)) - or (classSpellSet and classSpellSet[base])) then - active[#active + 1] = sid - seenInActive[sid] = true - else - -- Spell is no longer known -- save its slot index and move to dormant - dormant[sid] = i - end - end - end - end - - -- Build a set of spells already in the active list for dedup - local activeSet = seenInActive - - -- Phase 2: check dormant spells -- any that are now known get re-inserted - -- Collect returning spells sorted by their saved slot index (lowest first) - -- so insertions don't shift each other's target positions. - -- Also check IsPlayerSpell directly on dormant spells as a fallback -- - -- the CDM viewer may not have updated its entries yet after a talent - -- swap (e.g. choice-node spells like Bladestorm/Avatar share a viewer - -- slot and the viewer may still report the old spell's ID). - local returning = {} - for sid, savedSlot in pairs(dormant) do - local isKnown = knownSet[sid] - or (_IPS and _IPS(sid)) - or (classSpellSet and classSpellSet[sid]) - -- Also check base spell for conditional overrides - if not isKnown then - local base = _GBS and _GBS(sid) - if base == sid then base = nil end - if not base then base = overrideToBase[sid] end - if base and base > 0 and base ~= sid - and (_IPS and _IPS(sid)) then - isKnown = knownSet[base] or (_IPS and _IPS(base)) - or (classSpellSet and classSpellSet[base]) - end - end - if isKnown and not (removed and removed[sid]) then - -- Only return spells that aren't already in the active list - if not activeSet[sid] then - returning[#returning + 1] = { sid = sid, slot = savedSlot } - else - -- Already active -- just clean it from dormant - dormant[sid] = nil - end - end - end - table.sort(returning, function(a, b) return a.slot < b.slot end) - - -- Insert each returning spell at its saved slot (clamped to list bounds) - for _, entry in ipairs(returning) do - dormant[entry.sid] = nil -- no longer dormant - -- Clamp insertion index: if the list is shorter now, insert at end - local insertAt = entry.slot - if insertAt > #active + 1 then insertAt = #active + 1 end - if insertAt < 1 then insertAt = 1 end - table.insert(active, insertAt, entry.sid) - end - - -- Phase 3: clean up dormant entries for spells that are no longer in - -- any CDM category at all (removed from game / different class) - -- Keep dormant entries for spells that exist but are just unlearned. - -- Store ALL related IDs (base, override, linked) so a spell stored - -- by its base ID is still recognized even if the viewer resolves it - -- to an override ID. - local allSpellIDs = {} - if C_CooldownViewer and C_CooldownViewer.GetCooldownViewerCategorySet then - for cat = 0, 3 do - local allIDs = C_CooldownViewer.GetCooldownViewerCategorySet(cat, true) - if allIDs then - for _, cdID in ipairs(allIDs) do - local info = C_CooldownViewer.GetCooldownViewerCooldownInfo(cdID) - if info then - if info.spellID and info.spellID > 0 then - allSpellIDs[info.spellID] = true - end - if info.overrideSpellID and info.overrideSpellID > 0 then - allSpellIDs[info.overrideSpellID] = true - end - if info.linkedSpellIDs then - for _, lsid in ipairs(info.linkedSpellIDs) do - if lsid and lsid > 0 then - allSpellIDs[lsid] = true - end - end - end - end - end - end - end - end - for sid in pairs(dormant) do - if not allSpellIDs[sid] then - dormant[sid] = nil - end - end - - return active, (next(dormant) and dormant or nil) - end - - -- Build a set of ALL class spellIDs (regardless of current talents). - -- Used for custom bars so permanent class abilities (e.g. Stampeding Roar) - -- are never moved to dormant due to API timing gaps during talent swaps. - local classSpellSet = {} - if C_CooldownViewer and C_CooldownViewer.GetCooldownViewerCategorySet then - for cat = 0, 3 do - local allIDs = C_CooldownViewer.GetCooldownViewerCategorySet(cat, true) - if allIDs then - for _, cdID in ipairs(allIDs) do - local info = C_CooldownViewer.GetCooldownViewerCooldownInfo(cdID) - if info then - if info.spellID and info.spellID > 0 then - classSpellSet[info.spellID] = true - end - if info.overrideSpellID and info.overrideSpellID > 0 then - classSpellSet[info.overrideSpellID] = true - end - if info.linkedSpellIDs then - for _, lsid in ipairs(info.linkedSpellIDs) do - if lsid and lsid > 0 then - classSpellSet[lsid] = true - end - end - end - end - end - end - end - end - - -- Process each bar - for _, barData in ipairs(p.cdmBars.bars) do - local sd = ns.GetBarSpellData(barData.key) - if not sd then - -- no spell data for this bar/spec yet, skip - elseif MAIN_BAR_KEYS[barData.key] and TALENT_AWARE_BAR_TYPES[barData.key] then - if sd.assignedSpells and #sd.assignedSpells > 0 then - sd.assignedSpells, sd.dormantSpells = - ReconcileSpellList(sd.assignedSpells, sd.dormantSpells, sd.removedSpells, nil) - end - elseif TALENT_AWARE_BAR_TYPES[barData.barType] then - if sd.assignedSpells and #sd.assignedSpells > 0 then - sd.assignedSpells, sd.dormantSpells = - ReconcileSpellList(sd.assignedSpells, sd.dormantSpells, sd.removedSpells, nil) - end - end - end - - ns._lastReconciledSpec = ns.GetActiveSpecKey() - ns.FullCDMRebuild("talent_reconcile") -end - -function ns.RequestTalentReconcile(reason) - if reason ~= "retry" then - RECONCILE.retries = 0 - RECONCILE.retryToken = RECONCILE.retryToken + 1 - end - if ns.IsReconcileReady() then - RECONCILE.pending = false - RECONCILE.retries = 0 - TalentAwareReconcile() - return - end - RECONCILE.pending = true - if RECONCILE.retries >= RECONCILE.retryMax then return end - RECONCILE.retries = RECONCILE.retries + 1 - RECONCILE.retryToken = RECONCILE.retryToken + 1 - local token = RECONCILE.retryToken - C_Timer.After(RECONCILE.retryDelay, function() - if token ~= RECONCILE.retryToken then return end - if not RECONCILE.pending then return end - ns.RequestTalentReconcile("retry") - end) -end +-- (TalentAwareReconcile / ReconcileSpellList / ns.RequestTalentReconcile / +-- ns.IsReconcileReady / RECONCILE state removed. Under the new model +-- assignedSpells is pure user intent and is never mutated based on "is +-- this spell currently known." Talent/spec/reload events rebuild the +-- cdID route map and reanchor instead -- the route map is the source of +-- truth for which Blizzard frame renders on which bar. Spells whose +-- backing frame is temporarily absent (pet dismissed, choice-node talent +-- swapped away) simply don't render until the frame returns; their +-- assigned slot is preserved for that return.) -- (ReconcileMainBarSpells / ForceResnapshotMainBars / StartResnapshotRetry -- removed -- CollectAndReanchor auto-snapshots and hooks handle everything) @@ -5062,12 +4855,16 @@ function ECME:CDMFinishSetup() cdmBarFrames[key] = frame cdmBarIcons[key] = {} end - local iconW = SnapForScale(barData.iconSize or 36, 1) + -- Raw coord values -- see LayoutCDMBar for why + -- we don't pre-snap with SnapForScale (PP.Scale + -- truncation loses a pixel at UI scales with + -- PP.mult > 1). + local iconW = barData.iconSize or 36 local iconH = iconW if (barData.iconShape or "none") == "cropped" then - iconH = SnapForScale(math.floor((barData.iconSize or 36) * 0.80 + 0.5), 1) + iconH = math.floor((barData.iconSize or 36) * 0.80 + 0.5) end - local spacing = SnapForScale(barData.spacing or 2, 1) + local spacing = barData.spacing or 2 local grow = barData.growDirection or "CENTER" local numRows = barData.numRows or 1 if numRows < 1 then numRows = 1 end @@ -5308,7 +5105,16 @@ eventFrame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED") -- Visibility option events: mounted, target, instance zone changes eventFrame:RegisterEvent("PLAYER_MOUNT_DISPLAY_CHANGED") eventFrame:RegisterEvent("PLAYER_TARGET_CHANGED") --- UPDATE_SHAPESHIFT_FORM not needed: OnCooldownIDSet hooks handle form shifts +-- Druid travel/flight/aquatic form needs an explicit re-check for the +-- visHideMounted option. PLAYER_MOUNT_DISPLAY_CHANGED only fires for real +-- mounts, and the viewer hooks rebuild icon content on shapeshift but +-- don't re-run bar-level visibility. Only register for druids -- non-druid +-- classes have no mount-like shapeshift forms, and druid combat shifts +-- (Bear/Cat) would otherwise trigger unnecessary visibility recomputes. +local _, _playerClassCDM = UnitClass("player") +if _playerClassCDM == "DRUID" then + eventFrame:RegisterEvent("UPDATE_SHAPESHIFT_FORM") +end -- Debounce token for talent-change rebuilds: rapid talent clicks collapse -- into a single deferred rebuild rather than firing once per click. @@ -5335,10 +5141,12 @@ local function ScheduleTalentRebuild() wipe(db.sv.multiChargeSpells) end end - -- Reconcile bar spellIDs against the new talent set. - -- Unavailable spells are moved to dormant slots (preserving position); - -- returning spells are re-inserted at their saved slot index. - ns.RequestTalentReconcile("talent") + -- Rebuild the cdID route map against the new talent set. The + -- stored assignedSpells is left untouched (it's pure user intent); + -- the route map is the live source of truth for which frame + -- renders on which bar. A full CDM rebuild + reanchor below picks + -- up the new routing. + if ns.RebuildSpellRouteMap then ns.RebuildSpellRouteMap() end -- Clear cached viewer child info so the next tick re-reads from API -- (overrideSpellID may have changed with the new talent set) for _, vname in ipairs(_cdmViewerNames) do @@ -5431,6 +5239,24 @@ eventFrame:SetScript("OnEvent", function(_, event, unit, updateInfo, arg3) _CDMApplyVisibility() return end + if event == "UPDATE_SHAPESHIFT_FORM" then + -- Bail fast if no bar actually uses visHideMounted: druids shift + -- constantly in combat (Bear/Cat) and we don't want to re-run the + -- visibility pipeline for nothing. + local p = ECME.db and ECME.db.profile + local bars = p and p.cdmBars and p.cdmBars.bars + if not bars then return end + local anyMountedOpt = false + for _, bd in ipairs(bars) do + if bd.visHideMounted then anyMountedOpt = true; break end + end + if not anyMountedOpt then return end + -- Defer one frame: the Travel Form aura is applied slightly after + -- UPDATE_SHAPESHIFT_FORM fires, so IsPlayerMountedLike's aura check + -- would miss it on the immediate pass. + C_Timer.After(0, _CDMApplyVisibility) + return + end if event == "PLAYER_REGEN_DISABLED" or event == "PLAYER_REGEN_ENABLED" or event == "ZONE_CHANGED_NEW_AREA" then if event == "PLAYER_REGEN_DISABLED" then _inCombat = true @@ -5466,7 +5292,6 @@ eventFrame:SetScript("OnEvent", function(_, event, unit, updateInfo, arg3) end if event == "PLAYER_ENTERING_WORLD" then _inCombat = InCombatLockdown and InCombatLockdown() or false - RECONCILE.lastZoneInAt = GetTime() -- Zone-in: invalidate the spec cache (in case the player auto-swapped -- spec via LFG / dungeon role) and rebuild if the spec is known. local gen = ns.GetRebuildGen() @@ -5477,13 +5302,6 @@ eventFrame:SetScript("OnEvent", function(_, event, unit, updateInfo, arg3) local p = ECME.db and ECME.db.profile if p then ns.FullCDMRebuild("zone_in") - if RECONCILE.pending then - C_Timer.After(0.5, function() - if RECONCILE.pending then - ns.RequestTalentReconcile("PEW") - end - end) - end end end) -- Install rotation helper hook after CDM frames have been built @@ -5503,29 +5321,12 @@ eventFrame:SetScript("OnEvent", function(_, event, unit, updateInfo, arg3) if ns._pendingSpecChange and ns.ProcessSpecChange then ns.ProcessSpecChange() end - -- Existing reconcile path: SPELLS_CHANGED also serves as a "talents - -- changed" signal that should trigger reconcile when pending. - if RECONCILE.pending then - local currentSpec = ns.GetActiveSpecKey() - local lastSpec = ns._lastReconciledSpec - -- Only reconcile if the spec actually changed since the last - -- reconcile -- equipment swaps that temporarily remove spells - -- shouldn't trigger this path. - if currentSpec and currentSpec ~= lastSpec then - C_Timer.After(0.2, function() - if RECONCILE.pending then - ns.RequestTalentReconcile("SPELLS_CHANGED") - end - end) - end - end return end if event == "PLAYER_SPECIALIZATION_CHANGED" and unit == "player" then if EllesmereUI and EllesmereUI.InvalidateFrameCache then EllesmereUI.InvalidateFrameCache() end - RECONCILE.lastSpecChangeAt = GetTime() ns.OnSpecChanged() end if event == "UNIT_AURA" then return end diff --git a/EllesmereUICooldownManager/EllesmereUICooldownManager.toc b/EllesmereUICooldownManager/EllesmereUICooldownManager.toc index 6818b991..39b29bcc 100644 --- a/EllesmereUICooldownManager/EllesmereUICooldownManager.toc +++ b/EllesmereUICooldownManager/EllesmereUICooldownManager.toc @@ -4,7 +4,7 @@ ## Group: EllesmereUI ## Notes: CDM look customization, action bar glows, and buff bars ## Author: Ellesmere -## Version: 6.4.7 +## Version: 6.5 ## Dependencies: EllesmereUI ## SavedVariables: EllesmereUICooldownManagerDB ## IconTexture: Interface\AddOns\EllesmereUI\media\eg-logo.tga diff --git a/EllesmereUINameplates/EllesmereUINameplates.toc b/EllesmereUINameplates/EllesmereUINameplates.toc index 2796ced7..f251d23e 100644 --- a/EllesmereUINameplates/EllesmereUINameplates.toc +++ b/EllesmereUINameplates/EllesmereUINameplates.toc @@ -4,7 +4,7 @@ ## Group: EllesmereUI ## Notes: Custom Nameplate Design ## Author: Ellesmere -## Version: 6.4.7 +## Version: 6.5 ## Dependencies: EllesmereUI ## SavedVariables: EllesmereUINameplatesDB ## IconTexture: Interface\AddOns\EllesmereUI\media\eg-logo.tga diff --git a/EllesmereUIResourceBars/EllesmereUIResourceBars.lua b/EllesmereUIResourceBars/EllesmereUIResourceBars.lua index d9e078b1..344aeff6 100644 --- a/EllesmereUIResourceBars/EllesmereUIResourceBars.lua +++ b/EllesmereUIResourceBars/EllesmereUIResourceBars.lua @@ -979,7 +979,17 @@ local function RegisterUnlockElements() elements[#elements + 1] = MK({ key = "ERB_Health", label = "Health Bar", group = "Resource Bars", order = 500, getFrame = function() return healthBar end, - getSize = function() local s = S(); return s.width, s.height end, + getSize = function() + -- Return actual frame dimensions (post-PP.Scale snap in Rebuild) + -- so width-matching reads the real rendered size, not the raw + -- setting which may differ from the pixel-snapped value. + if healthBar then + local w = healthBar:GetWidth() + local h = healthBar:GetHeight() + if w and w > 1 and h and h > 1 then return w, h end + end + local s = S(); return s.width, s.height + end, setWidth = function(_, w) S().width = w; Rebuild() end, setHeight = function(_, h) S().height = h; Rebuild() end, isAnchored = function() local s = S(); return s.anchorTo and s.anchorTo ~= "none" end, @@ -995,7 +1005,17 @@ local function RegisterUnlockElements() elements[#elements + 1] = MK({ key = "ERB_Power", label = "Power Bar", group = "Resource Bars", order = 501, getFrame = function() return primaryBar end, - getSize = function() local s = S(); return s.width, s.height end, + getSize = function() + -- Return actual frame dimensions (post-PP.Scale snap in Rebuild) + -- so width-matching reads the real rendered size, not the raw + -- setting which may differ from the pixel-snapped value. + if primaryBar then + local w = primaryBar:GetWidth() + local h = primaryBar:GetHeight() + if w and w > 1 and h and h > 1 then return w, h end + end + local s = S(); return s.width, s.height + end, setWidth = function(_, w) S().width = w; Rebuild() end, setHeight = function(_, h) S().height = h; Rebuild() end, isAnchored = function() local s = S(); return s.anchorTo and s.anchorTo ~= "none" end, @@ -1435,17 +1455,27 @@ local function BuildBars() -- Health bar local hp = p.health or FALLBACK.health + -- Snap stored width/height to the physical pixel grid so the frame is + -- always a whole number of physical pixels. Use SnapForES (round to + -- nearest) rather than PP.Scale (truncate) -- see Power bar note below. + local hpWidth = hp.width or 214 + local hpHeight = hp.height or 16 + local _hpEs = (healthBar and healthBar:GetEffectiveScale()) or (UIParent and UIParent:GetEffectiveScale()) or 1 + if PP and PP.SnapForES then + hpWidth = PP.SnapForES(hpWidth, _hpEs) + hpHeight = PP.SnapForES(hpHeight, _hpEs) + end if hp.enabled then local hpOri = hp.orientation or g.orientation or "HORIZONTAL" if not healthBar then - healthBar = CreateStatusBar(mainFrame, "ERB_HealthBar", hp.width, hp.height, + healthBar = CreateStatusBar(mainFrame, "ERB_HealthBar", hpWidth, hpHeight, hp.borderSize, hp.borderR, hp.borderG, hp.borderB, hp.borderA) healthBar:SetFrameStrata("MEDIUM") healthBar:SetFrameLevel(10) end local healthAnchorKey = NormalizeAnchorKey(hp.anchorTo) if healthAnchorKey ~= "none" then - local ow, oh = OrientedSize(hp.width, hp.height, hpOri) + local ow, oh = OrientedSize(hpWidth, hpHeight, hpOri) local offsetX, offsetY = GetAnchorOffsets(hp) healthBar:SetSize(ow, oh) if not ApplyBarAnchor(healthBar, healthAnchorKey, hp.anchorPosition, offsetX, offsetY, hp.growthDirection, hp.growCentered) then @@ -1453,7 +1483,7 @@ local function BuildBars() end elseif hp.unlockPos and hp.unlockPos.point then local rp = hp.unlockPos.relPoint or hp.unlockPos.point - local ow, oh = OrientedSize(hp.width, hp.height, hpOri) + local ow, oh = OrientedSize(hpWidth, hpHeight, hpOri) ApplyBarAnchor(healthBar, "none") healthBar:SetSize(ow, oh) if not EllesmereUI._unlockActive then @@ -1468,14 +1498,14 @@ local function BuildBars() ApplyBarAnchor(healthBar, "none") if EllesmereUI._unlockActive then -- During unlock mode, only update size -- position is managed by the mover - local ow, oh = OrientedSize(hp.width or 214, hp.height or 16, hpOri) + local ow, oh = OrientedSize(hpWidth, hpHeight, hpOri) healthBar:SetSize(ow, oh) else local function ApplyHealthBarTransform() local ox = healthBar["_barAnim_ox"] or hp.offsetX or 0 local oy = healthBar["_barAnim_oy"] or hp.offsetY or -64 - local w = healthBar["_barAnim_w"] or hp.width or 214 - local h2 = healthBar["_barAnim_h"] or hp.height or 16 + local w = healthBar["_barAnim_w"] or hpWidth + local h2 = healthBar["_barAnim_h"] or hpHeight local ow, oh = OrientedSize(w, h2, hpOri) healthBar:ClearAllPoints() healthBar:SetPoint("CENTER", mainFrame, "CENTER", ox, oy) @@ -1483,8 +1513,8 @@ local function BuildBars() end SmoothBarAnimate(healthBar, "ox", hp.offsetX or 0, function() ApplyHealthBarTransform() end) SmoothBarAnimate(healthBar, "oy", hp.offsetY or -64, function() ApplyHealthBarTransform() end) - SmoothBarAnimate(healthBar, "w", hp.width or 214, function() ApplyHealthBarTransform() end) - SmoothBarAnimate(healthBar, "h", hp.height or 16, function() ApplyHealthBarTransform() end) + SmoothBarAnimate(healthBar, "w", hpWidth, function() ApplyHealthBarTransform() end) + SmoothBarAnimate(healthBar, "h", hpHeight, function() ApplyHealthBarTransform() end) end end healthBar:ApplyBorder(hp.borderSize, hp.borderR, hp.borderG, hp.borderB, hp.borderA) @@ -1535,11 +1565,25 @@ local function BuildBars() ppHeight = ppHeight + ppExpandDelta end end + -- Snap stored width/height to the physical pixel grid so the frame is + -- always a whole number of physical pixels. Use SnapForES (round to + -- nearest) rather than PP.Scale (truncate toward zero) so a stored + -- value like 214.6 rounds to 215 instead of losing 1px to 214. Without + -- this, a stale stored value (e.g. from a previous ui scale) can land + -- 1px short of the width-match target, and the user would have to + -- un-match/re-match to correct it. + local ppWidthRaw = pp.width or 214 + local _ppEs = (primaryBar and primaryBar:GetEffectiveScale()) or (UIParent and UIParent:GetEffectiveScale()) or 1 + if PP and PP.SnapForES then + ppWidthRaw = PP.SnapForES(ppWidthRaw, _ppEs) + ppHeight = PP.SnapForES(ppHeight, _ppEs) + end + local ppWidth = ppWidthRaw -- Always create the frame when enabled so anchored elements (CDM bars, -- cast bar, etc.) have a valid target. If the spec has no primary power -- the frame stays at zero alpha but retains its position. if pp.enabled ~= false and not primaryBar then - primaryBar = CreateStatusBar(mainFrame, "ERB_PrimaryBar", pp.width, ppHeight, + primaryBar = CreateStatusBar(mainFrame, "ERB_PrimaryBar", ppWidth, ppHeight, pp.borderSize, pp.borderR, pp.borderG, pp.borderB, pp.borderA) primaryBar:SetFrameStrata("MEDIUM") primaryBar:SetFrameLevel(10) @@ -1550,10 +1594,10 @@ local function BuildBars() local primaryUnlockAnchored = EllesmereUI.IsUnlockAnchored("ERB_Power") if primaryUnlockAnchored then -- Unlock anchor system owns positioning; only update size - local ow, oh = OrientedSize(pp.width, ppHeight, ppOri) + local ow, oh = OrientedSize(ppWidth, ppHeight, ppOri) primaryBar:SetSize(ow, oh) elseif primaryAnchorKey ~= "none" then - local ow, oh = OrientedSize(pp.width, ppHeight, ppOri) + local ow, oh = OrientedSize(ppWidth, ppHeight, ppOri) local offsetX, offsetY = GetAnchorOffsets(pp) primaryBar:SetSize(ow, oh) if not ApplyBarAnchor(primaryBar, primaryAnchorKey, pp.anchorPosition, offsetX, offsetY, pp.growthDirection, pp.growCentered) then @@ -1561,7 +1605,7 @@ local function BuildBars() end elseif pp.unlockPos and pp.unlockPos.point then local rp = pp.unlockPos.relPoint or pp.unlockPos.point - local ow, oh = OrientedSize(pp.width, ppHeight, ppOri) + local ow, oh = OrientedSize(ppWidth, ppHeight, ppOri) ApplyBarAnchor(primaryBar, "none") primaryBar:SetSize(ow, oh) if not EllesmereUI._unlockActive then @@ -1576,14 +1620,14 @@ local function BuildBars() ApplyBarAnchor(primaryBar, "none") if EllesmereUI._unlockActive then -- During unlock mode, only update size -- position is managed by the mover - local ow, oh = OrientedSize(pp.width or 214, ppHeight or 4, ppOri) + local ow, oh = OrientedSize(ppWidth, ppHeight, ppOri) primaryBar:SetSize(ow, oh) else local function ApplyPowerBarTransform() local ox = primaryBar["_barAnim_ox"] or pp.offsetX or 0 local oy = primaryBar["_barAnim_oy"] or pp.offsetY or -54 - local w = primaryBar["_barAnim_w"] or pp.width or 214 - local h2 = primaryBar["_barAnim_h"] or ppHeight or 4 + local w = primaryBar["_barAnim_w"] or ppWidth + local h2 = primaryBar["_barAnim_h"] or ppHeight local ow, oh = OrientedSize(w, h2, ppOri) primaryBar:ClearAllPoints() -- Internal layout: shift up by half expand delta within mainFrame @@ -1592,8 +1636,8 @@ local function BuildBars() end SmoothBarAnimate(primaryBar, "ox", pp.offsetX or 0, function() ApplyPowerBarTransform() end) SmoothBarAnimate(primaryBar, "oy", pp.offsetY or -54, function() ApplyPowerBarTransform() end) - SmoothBarAnimate(primaryBar, "w", pp.width or 214, function() ApplyPowerBarTransform() end) - SmoothBarAnimate(primaryBar, "h", ppHeight or 4, function() ApplyPowerBarTransform() end) + SmoothBarAnimate(primaryBar, "w", ppWidth, function() ApplyPowerBarTransform() end) + SmoothBarAnimate(primaryBar, "h", ppHeight, function() ApplyPowerBarTransform() end) end end primaryBar:ApplyBorder(pp.borderSize, pp.borderR, pp.borderG, pp.borderB, pp.borderA) @@ -1634,7 +1678,7 @@ local function BuildBars() -- Enabled but no resource for this spec: keep the frame positioned -- at zero alpha so anchored elements (CDM bars, etc.) have a target. local ppOri = pp.orientation or g.orientation or "HORIZONTAL" - local ow, oh = OrientedSize(pp.width or 214, ppHeight or 4, ppOri) + local ow, oh = OrientedSize(ppWidth, ppHeight, ppOri) primaryBar:SetSize(ow, oh) primaryBar:Show() if not EllesmereUI.IsUnlockAnchored("ERB_Power") then diff --git a/EllesmereUIResourceBars/EllesmereUIResourceBars.toc b/EllesmereUIResourceBars/EllesmereUIResourceBars.toc index 1b202af0..81a0c1ba 100644 --- a/EllesmereUIResourceBars/EllesmereUIResourceBars.toc +++ b/EllesmereUIResourceBars/EllesmereUIResourceBars.toc @@ -4,7 +4,7 @@ ## Group: EllesmereUI ## Notes: Create custom resource bars ## Author: Ellesmere -## Version: 6.4.7 +## Version: 6.5 ## Dependencies: EllesmereUI ## SavedVariables: EllesmereUIResourceBarsDB ## IconTexture: Interface\AddOns\EllesmereUI\media\eg-logo.tga diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.toc b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.toc index eea3b83a..ebfee4ab 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.toc +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.toc @@ -4,7 +4,7 @@ ## Group: EllesmereUI ## Notes: Custom Unit Frames ## Author: Ellesmere -## Version: 6.4.7 +## Version: 6.5 ## Dependencies: EllesmereUI ## SavedVariables: EllesmereUIUnitFramesDB ## IconTexture: Interface\AddOns\EllesmereUI\media\eg-logo.tga diff --git a/EllesmereUI_Migration.lua b/EllesmereUI_Migration.lua index 36895852..6e64cc6a 100644 --- a/EllesmereUI_Migration.lua +++ b/EllesmereUI_Migration.lua @@ -1226,6 +1226,24 @@ EllesmereUI.RegisterMigration({ end, }) +EllesmereUI.RegisterMigration({ + id = "cdm_buff_assignedspells_reseed_v1", + scope = "specProfile", + description = "Clear buffs.assignedSpells so the unified model re-seeds from live icons (buff/CD unification).", + body = function(ctx) + -- The buff bar previously never wrote to assignedSpells. With the + -- unified model, EnsureAssignedSpells lazily seeds from live icons. + -- Any stale data from the first options-panel open (before the route + -- map fix for TBB diversions) must be cleared so it re-seeds cleanly. + local bs = ctx.specProfile.barSpells + if not bs then return end + local buffData = bs["buffs"] + if buffData and buffData.assignedSpells then + buffData.assignedSpells = nil + end + end, +}) + local migrationFrame = CreateFrame("Frame") migrationFrame:RegisterEvent("ADDON_LOADED") migrationFrame:SetScript("OnEvent", function(self, event, addonName) diff --git a/EllesmereUI_Profiles.lua b/EllesmereUI_Profiles.lua index 73303292..3054680c 100644 --- a/EllesmereUI_Profiles.lua +++ b/EllesmereUI_Profiles.lua @@ -1669,7 +1669,7 @@ end -- To update the weekly spotlight: change WEEKLY_SPOTLIGHT. ------------------------------------------------------------------------------- EllesmereUI.POPULAR_PRESETS = { - { name = "EllesmereUI (2k)", description = "The default EllesmereUI look", exportString = "!EUI_T33wZTTTwJ(xPpEopepKG3PFYYXonJZTTJABYE2ZOHwI2IFrMuFKujXnJ)VFWATaabib1fhNSBp1zMwBlrcSUFfx(2fn(P3M3MX)L408nfVFE2Q8sNJc9sG)fheM4hNC8fnrPnZRZZl)1sxFFhTp4pkzbHohFpmsT3ToN)JR3SAf8aFoVUPOQS07y4lxKHtJllDt5QQ5F6vz3vTP9BWdMvoFzvDd9TxxnFtZPznTxLvZ)KW02S6BYBBcOVbE(QRVUjV9JC40ZZliiYLfN4eYcW5PPyro)TM82PtF7R7E6pu(mVJCcCJcIIDzb5pZjaaAx30FpFzX8v5N91Iw95lk91zfLtYQnNrMFscquy(m344Uz0lD6BFN(05gfEuyyyGNVpZl2tmzN9VMoBAD28pLBGC(P8jk0CMCddJc98Cccd9Dc7Mj)0xD25tnWmCMss4queHvmXat)F9zs(l6ZL7rCKk2pY3L56K4VfKkm4Oypp)KqNepXu5LE6ZF9S5vvRwu9LYg95IJVW3TPTyvr7Dgy3wrhxa31rhxI3peBSiu4c0IWGq)eFxd62aKjWrGa0GzrMZgX6zOuNhZZnj2j0HTDHoCkKdZofVEwWr(UHboE9eNdsV8LV4xnisrh5e7XcCtc84IZWS4LUUpRE9QS7YR7b9a1zWmyvHrWTd5QyHUrmcvOX8bJkwy3Xhff44e664f4geItts6zxoz27Q(I5m5gHF(PRYAAUmVPAt98CtLgWQLxCsOFSpiGUD8Z5ihhh3ypxNa)quwdKhCs)sXI2LVoRD(YHMK6e50GXramqFh(u(BYPoJ9q9K)uylj4VmV4MLTkGXWcIYSrFLWEAEKu(6LzLTv3oPAt5IMVDpYmZwSOc1zzHPNTAvEZT515)2lpvmqVoRm7gKfeLoFXTasapRBADvBwl32(VMVADE9zLzxTkFX54xTKtUNSHtI)JL5LVSmBEBXNZ5mC3K0nn5twv8N)zw9c4jGHdEhVEd3lwv9L33Eh3reXabIY3aK3p9Zfn)kF8)1QnnfL3mf)mY7X7QAkGHG)8RYVUfqo(7n5MjOTgoPN7S5TxFzw5n5VL7yIleFoGvLBU9YQV0u6EmY5B(sX68twbKkNJIq3C5eYnf8q(P87UQOCXjGG2XOObohNPEeo40SeGEo)5uoHU9CKN1O(7xqZelDDw5I8BlMdi75642RHhdjMrPxvvViVwGcbYN4nvtrbeTh5s8r8sVcWxULHJbOTyEv57l(Z8sVeWGk3Dsa3Pfo)UiyE6syCAMI0hDa69RZZxu6t0eefjAc(U(sYWBLQ6SJhmcNwTQcKBCtVIG(0BO3oTM)ts0wmmii66mCmEvrzEtzmH7eEoL7Q(t8pTbI2yzrjcoZr048QYwAOKyOIOpbNAjT6e8V8GPNRdR0zqDesw6TAEP4FiNa87fnfxH6sGsZQVKDh88HQ3UBYzejQBYnGjsB4KYIBFbNpfh0)tVeHn)0YSBZ5a3PAaNocrpMsEuiuzc(FGa)qbwJwDqUcOY5OnNgFdNstFdxeUf)A(0CLqPfMsAkMwbazvjy)koDjPocYMDQcsY9lkTWAj1BaQtiX0LzyOJIH0r8UeTthvjmNJvCDH3wU6UxwYPkLZZB2HXhE8N5nzTBQ5y1Blp95t1v5Et1zL53Ehne6GPsCRKjuVavFiMcU1mUvK2cE8YVTUiVKSFDocAOQLGZnn)RTtPx9sPMPbRuiL1tP6d4ekutNYhR2I1N3twHmlOOmKqUWQ3LIVdOT)7QQBLtnahVq(hINvWG0bQpO(i(0Fbn(O1MM1zZ5CAo0D)pydYi(FOgK7jZTFgJn5npm7W9ufhZqmtJS(KX3OuvMb)an9k04oilVrP)McY2ID3jhQD3ofixIWUnBTBXi9iMC7fpY5dfT(hMzxBUREmn(omoUFW2F7I27WT)2xSDxwGFyHepMO5(zl(huGXM6D6gKd6OZ)ZYGSq4d4BbhB3(mFAaD6l(rAD(bfyCq6ebG9yguCpN2dSCye)YGingAN7X107qt7paJX)0m8(JlE3D408hQ93X8iF42ITXl2FDxtB02uD5SlOEPv3oZFglYlooEMFa7cR2ZTQDRBKFCZHw59BXWqe8ntHUeWXkbac2yEqMm6R5Btjufp37Qq6VL0sT5eCF9wnMkUf3yJepyphzBt02QFoRgtSAzWW8MP(5aJpJ59KDSzDLS790Ih7VF7m2cLBl(bTyY5qDjBZZr)CyT7G(r1i0qd)wdX6(V1tPTVwyqAz(NPMbyeHNx6n1vF55f15ZrvevVge6oCaGpplYl)fHt3oXbhtXbHKJqdxf9GWIKx6Sz3SSQPDMmQcN0IMxaFYe(R8qHFtOrmxoQ5A(ITHIJc8g1fusiCKeItF(qW)(EGpJ8dl0ENEpkUs1qx5n4BxOpr8)kiDDvrj05Mtp7ntp7sacRZx9oJp8(lK0wu89UYNf6a9Pb6utINtO)XwFnBJUB6xlPMGz1FHy8DdPEQf5WIDypGrxLOVeGtcGoV44WCzE(UHh2qgYokaEzxVKWKyFQPnS02RUQJWsK7wSnjQEnq9pPjFfxmG)Pz1IykL9xiMlkLVA1lFEZ3kzX8udk9yUXSJlJD8c8oUKf6Zjc8pnXXloK)Zyxph3Jl999Dcz4pzbrht9qc1JB6CyfGcrKontOMpDzDo325QfAk3bCz1SfGLpJp085bt3XQbLSVX879qVo7RAdHdNwUEZQS6lWYEF1QQQfR4SCyqUUy1kyitccJd584iqeiSZsfyMO)OxsznOjX3hibtCu8Usmc(egztRV7XrZ1jAiwjspQ)8nrmFe(a)velX31nk2j2ZNHFdOUtKReYd)6S6pPhNWVMxxv0CBhc3OeQ4rOWXbWcuOORCLmFYce(GspeTf3Mx)bLNtuIIKNu(DvZ)u4zNQElc0iW8dYSSWVrbfbP1WudJDfy4R9ozehagmiqtHx)JrvCt61uUtZMgN0buFTa86tJprgPfaHt6cAONNagzdcGOonajDsgGzlpCXn1DHTiddWNaw7HbKqKJUrTt66sH01wJ1y0OaQfE)LrbCfz6QB4NO05W2dxYIe6fKHZNdHDcR2fFIwaC33WX)TLjijPkOPUQzI7OcD282lF5)(TVz6jVshmupnqSFErd0oEfjKgqqnlWpXXnj0ZpI5fgYTrkIOHIVCD(CWmGBQd(PCMWNkeSZguBGl2o)BalHHEbItVnB9AUtw0oPsR)CuUldDQoztBlh7jgn3p37xv1ks3v8ac7TCuPF4xhjj9CkmYKKXGjul9CWe()fGE(l)FYVj9xEEXN5Mi(L3TPEDvt()xCzmvHT5)KtN(YF)m50OfsoLy851v3E6ZF953tEiKodEnHE0ScyD4)aXAxh))XH1S0WGFWiDNLffkt0H)RH0aR(hnw)xpwnOwh)poSMlG)pnBz43ePVEMMK1umxS(s)F3K302TemTxSeR1NZjnBtB1jZNNVgliKx6668740HvzRBiAzc(it3ux(YY3VS4623)PI1t7fOSSkNRHWCPcM8LQ6vlmgjV0)K7s34J4CLQBxVkhkPIzjc97(gt2cvHgTsBWdrsUKkBYN3nmWA0vfwRFawxWaIw1yaeoaHzyL940Ikz(OUU(h5ff4eZIy(rWiFaz3XJw)iwOJxOxCadYUtpO5rAg0ngLZ0wzYabfon)paY8)crQUEsH)9lBZVTzQE8YXPvx9)ysnJykP8iMIGslmsoR3iHFsaJkKXS5zRHqDx8wU4fjbWh7E8qpIAdacfdTS6NCq8DC5S(G9)MlEO(mzqPEoMKjrTOjj6f5R(SHefxADvXnL3YdRKJ4TvR1Z4lgeqmX)qmUEPAUVIc44lZ6Tt6NKvAlAxLBoi(rQXiXv3ubMQd)X1R)lw8GBlkl4wV(2Oi2qv1W0nL3uxTznK9pywdx3I(62eEnnQAw94SWxvC1ZN8sEK1UoZMS5M3ZzILe7nczVNwNDDlFEFleQSwbCvrLphwA7Clj8NDQmjVtHpRVrGGu(NIzmOACiz5LdtI1kcMvVdoVNaRtZt564z1Ta7IWr13iWXZ7aHZRQVnd4QUSLiMvaPUYTJdaWKwrAA0mQxqCoeN958fu5kpgeSAWAlhM28)UjRMwVQsiN2TaUh5gmUUPE9TDG9max(dgDIgyrtLriGr)6cqufEnbMo9IEvTwuwb6X4wv55wsudqIeEX3UHwqaRfzilSvrM6oa7tjbHgRG677WYlLzSpqFFeJMca(fqAI8SAphae4dEDwXQbKIpsIy0tGMx4YHe5y6fJ4gZpfLfSiU9bjhbNbtgjJwwWkPerNVb)rkjBGxsT(PEbk3rW1LzflEEX1xxmFZQ27i)yBAYnx9kG3k(RXnQnVa3GiIIeEnKP)IMdZPSVqfJpbGElzmSTyv(lKCd0oqZWfM4jIGzWQwarzW5mqii1LMQQj4WjDXqEuj3jxMFdh8LtRMI0robhVv2E2COwsifr615s1RfWzks6feWudvpzoD8e81MMD1ut)EuDjAiU4X91n01YvZIQDqbcqfqiQeaks4evtj4FhxUzLiEfB6T0BCPuScI6drayoccH5G5Ph)eYRFz5NlAjER2R8cvuKE4m)Bst5tVyhEa6S8O6XI(IByIgNrnByTud9oEmz1RLb)wwMx3C(9qCsZ3u3GU084Hm9v(h4C6Zzjp3oOytS1Gzz1m4yYowWFd)kCn6SM2tlQNVkxda9o)KOZphGDOosBA4rkOlZ4qE8ZABZMVehOa(uccxXoKjHgOsP6kr1qpUXXUe81ScFQcH6kO6Ar7x3jiqjXs0J264QPZxObVNJ)tdErFv7l82bDC7Eu5s3nWDEpGBeJq7CCqKuwCtUYdakZiAeMq08LzTJBJZMY)a5xDD)WTicnVA9DKlIZ7B3BGbbxytwv2Y9tcQ5uDHj)tC7nNZHPLBrSmbNkQS9uGlgPykxukQOuGDDytt2n5VxR5DCFiIp98m91owmcyKPm0xKnLmhSWWCc5TRB07eowL)8S65l78v6XPXfZ)em()2LVYSN1AUwqJ81T5LNUeu(x1WLoRGfRGnl9iiUmBbpXo7AP2SUeOJWqia8yEDWgAXC1JvfeewVkJBQKcJL8uU6U3ZrUNNFDg3zlkRqtdi7bjVFji5JmX8SvTlFZMBVc6EbNBWd1hEaY9okafj2KtaK1r5zS0ffqlmG6w)Bn5qVrvaprBpxakV5DNIR)igA)zsw9VvIHCwVzDBXvOTir8)Yusv)An9Ru(8qBlay7duCkY0(wKdD88LQgmes2RfZ1Vs5T6grYXscapfBUKzoWpbYdTaE2uNjRvpeLyxgyCqV421CgEwjUxS6AfIhrH4unj09I6QV0Uu1LfFK93eeK(saN5UmY)p8aZFBzZ)rJl(FUnFrr2)bF0)ZzFDn38qdx95OPtph9ok5QaSc2f(OGe8m5YgN2xyVxZeiKeaYCHEq3RFkCQg346IxZTBlds2NghSPH603ytzg9e2Kf2XmvnEwgZH5B3pzSo3qNPYCjlD4gO7uShk9QDHsoj23yizGw40SYpDYn8adupFSRw65HDf8a2MU3JKdHyVbQpARVyPMsCclpkH6lZZwCNAY7MAVoH6ezLb0HxnTlhkL)xxDLUE354UXBGOyxFS8igVgDD3SbpQTHDA9(AA9If(K0JIa9xw0Ev1xrPTpif3inUPDTGtKJAhN8SBx3Exh8iwLS9OrDfpbPqXyk(xv10OONjXDLAG57PE(a3yr64a3zQrRrvAOOyhOOJlGR9tonOBxEkTM4ieqUQI7442bgL80uV4Wahl4tVO5S8Ven78siP9Ri)9uDs96k)H83RXi4jGOZClv4trplnqgJc)jKPnGSqfK0JFRp(7MKaBI8Yp9RznM6zHD8X4ogjTD6zDgFpfBwUUUgxoXWmJHEyGU5vnqMPENxX)YoJBKFM3O6WSWprhVAGFcuvUlQ7KEq6UPhEKm3Ejo5OrPfH1nFuPvoHrA7)DGx(pAkMjr0DpRrAkIFWK6(jU1dOJ)AljF5gn(KAU3SoktK0RANYOIH3vZViXeQ8U2tY0vUEhnSP7e2j(l)9AXV3hbMCJDZioXDcGo6sGXK52qfXKJd9Q9iTEhjzx1l656l0(a59xvbbSzkY72rb0k8BSlz8cKka5iv4aqKJqP54kTsUGyHjO5Xtfiig)PKCrfIGh0ImSHlqjvn9WZ07Ct)LrdtjIkRGksqu2c0fkIslZ3WZ)A1EHMC5qPi0ULdd0qunvoE0KRBEdpy89N6YO8zPKAmYXstbgSmORZiSLVtlwQiu0SwkIStTmB0cGHStOzUjeJKA9av2TSstpYJLmqeuAYy(8EHBeAGIgZCtb0WhTLGdyPRVf2EEf6hGjLlKrmiDQkrDXPhh0ftJBGut1yM0IgWsy0uIHsB0Qiw)dSNfOj6yDZ)9ct0K(3lXaihKtwT65y(bycqrDCfkWM1)EPRipTyTSRLzUqv0stkBNI3mQMJcFktrGSFEf7Eu6Y1PNWtNUJ8lKoS0KtnidMQcFWC46Cjosm)8ai7)nD8IpAYlSVYQeA3Arm(rTigvOZlYwtb0eOp3AI1rg4XUjHez)9ARoDRjX11czb)V46crhmaZ26XnAgr8UHGqrMPOV9D)4(9eMvXZydU7gomzIKe3WqEYmbUjA5qhhZIcyUEEXrSykozYthZn0pWZNL4lo8HI4XRwNF2QI28xAUHKdsxM1SeusFxEnuHzQ6GWNlvvnYWU751szPl)1F16ln6k80fDV9MQEof6cuGzSIcq7K98WLOZfSfHvcQFBK5SUGje(EOqr5czzNpvMPZuSKV8)kxlKOWowqGKhZbvpjFwLN0(iu0z8rtzWf8w2p67UjvFiIsH1HEHwvwC68Y42bEEjcx5s(3oboUC5a2kz22nqy6rxHrwdcCbqxxSoFXZ(mSTJ0cb5JM(KWLcrxyOcl4dZ(fz8exgvAf9R2vScv56)NP3CvkYwL2pqnvHuTPGosHwZbpfblyaLf(T7vsdyvbFxEBdwu65ZrGOlkbtJZ9WrWifqzSMPkl1swpJUXI8e0GpQfxPBIrL4EhEanj2hRiXryGKe)deafziCUQUeuzzvntd1lGYGZI1RejetlSsEUKdBLWYf9BOKk0cSn3c0F4du1(9BW6ps2)QSn3aTUEw9gSlsCAaiI8j63PAQdTOLtpeROHQnLIwAuu(P7MD1QmSn6S0V0mLQ(FD1AUpiCak3C7vf0UUJZQZQBxUcwHr3Gd11Cm6tOA8I8mYLFC6x4iW1BQH)aQFxLyUY4XvvC9D0RgZzWT5ZYw8)qdmFGUnVTQ8Mnu79XVTjRCov03VKNTUQCwE58LeYWT7W1c4diF0ivDavMayc0MAA5xZe72S15ZlYw18MQszhbe1GDDD(15115l(dC8pJgE(GZrR2zBAYx0RJsEDVY5CeZ8j5AhalqS2HFnTORU9UwoLeHWUxfOA6VRmjPvWQTwjais734tvLUmGY0ybnzFdOOGhqynEae7UQGIIiuF1ob7Qds9Hvagu0zeT0rXER(DoNPgkfapDM2mo686ZE(l)n8W(6oPIcA132sxZlkuP7hj(9UamWUdl3840YAhnzLt71rk60o0fbTVQR(tpHEmzIiVuUVOO32HP47HNJo5eFNALwXcOXBVx9cwpdetWcznrUJIiBY6sHN3xLoRyk0QUMpDhQuDf)fWLxbpWGBLBOIBRA)c9T1vL)zUqlRUf0dmewJO2l3UKlm96IMgHUsaA2K2vuAp9qOBQj0XblC1LaXe85QzOTxCwUINMt1xM9L87Q5VAfAeYJ70)AXpxr6)nytC4qkUevyyBLwbb5Y)psl)koGMx)Pz08lg8vfq8bqR(qGTbHGM74gLQArtuO7RRLV09dBYZVvw0EoifJCbkOGRYQFRMCEIA5RGpZWtUW9DdK52)eve3wwSax8eXlGfehftrq81s)e9tvp)JpGzzWXqjon8XN2bxE(oXH(IPXl2dpyf9DtCW1HY(olqr)AuAeX0zDwKNRNdLZhSLXCODWwSJRRFs4bm46LsaOq4jtWbOW1)8FKgfxhA)VbByV4ijbW1xa6HXXXheDwVa8QjHDG2geGQ68DClG6Z8Cf7GVihQMA7lOkpKkPbp8abXNbRVrXwzr2EzPgdjgmTdfMQ4Et7ybt7l7F(aX0PcOCkPNaJQ0baU4c6K3I06aGw0NCFtLYWWe1LykMGKqJg6lS(zZW5QIiIbaIH0bBxpoZKc1wKVc(jIm0(O05ILQSk9MjCE4isuv)1b3x6DjhJCLkBbpTuOvMCkq9Y1xiR)8vDzZjAIiaWKtTpilcH4b7ktNS18WZQAzkSiGUqw(d95KwR4d9poC1mbvsejSNqNCW4tajVPYIhafz5y2zsoUHgpVwvSDsVn7RcEOABhIeYjAwMLjusWerueBkZHKDQ6VybO0qK(uhcdEpS7b76XHWb4H0XxXzA2soRB1(2A3UXqTZ5PD(ypMfnNs5YUnO4v58ySUqlFnLynLOYezL6c0MQtGvICxf0DPrviZf2xKtaydkeNyTQjCicFqSGCPluo9I(6diiM0nKVJhOch2KlZFTNuSdEzgFyhpLC0RjAQLQNykv7RFcWeOON)qzi1PvJIJlf5sKjXri65fRwzguBONwzM1kQEONOM3gIx7wQqa7WcorklJGlbzY9HyNvdHwcUoCPLIm(DRxtBfLVIYaLctjDsbY9ql(YFS8zILEkgCVOSdHYQ3Glza9MeTpnyc4BeRft8HLYwCVGhtFmvUGULwMMlRdsnAR6m9490PqvFfjTNEamFHvJ2AhmgFh2whvC7qC2mUnQHkyM8xh3hm)LyWsVqgVxGB36jiuVDguV)0JqZ(6YZItDQIjdw3y8O6xKtcpsSKoFwIv8rksE63)GQo9TxHv92yfI1X7rg0JrKawwBAjk5f1UoVNtFXYXLdJQLA9UC7hapTy9e1t(0IpMrCDz4aMjMWobFdA1H61NPWAJMLosOakblsoL5jwYWuGhWOWL2b2F0a2)K9KZC4bdmUI(wd4ZvZ40hK0)bRPsQshVNd2tVyB(5TT4tH(22z)R3IESVx)UYOAXT)w8K0Z)p5S2Idugijo5g9mP9HpQt1P9kTJ5bvFa2IzShuaaXWePfsFp3a(ylCdhi(y22BzangfuwrbtukDcC)q90BgKsFh84cSzjHICtb0s8bF0EE1zjJOf0lOzFLzVH(i7S8HxbcszeRo(eGLPfkv4Rdst78lKHk(YY3Lv3E32cjmUZqMHG8Ook7EbJG9hKKxyhrCa6NO9D6ZASKGPlBR1cEH3ZXob26878rzWL6umAmel1Xofyn7Jdt8Bq0A9ZCnUZiLoM0jQ(rzm12fzO6WT1e(ShIKRKwzygxhF1dz6asRtiNL3TEqWXoX0zG03z4XMkmQ9ELEypXYrq(uM5jQBX(JY33MVIdki1HHEV9K2ioH1dAgMJbH9vIDBMEgDdOIdJ9CVtFtfUu)0P1dUu3TLE2Bdcc2RZKI2NU1q4D6Eff2O(ersIJwXhYAJ1K3GGsdtID8syU4XXux8UueSy3NHZ2fNOyFpxVimJolUn0wBqrKMUTu4elvRPlZH2NyUl4CDeMiHT13uPqC)qYhDz9URIFOmLjVpG4)zx8NhE4)6XX(XYbLZ0sCVBLdTxXX33nLsC3wG7(cOriFlpiQge7TLOSnwrDiBsR6DZX1y8llxaRQLQAL8OjZ7GkPe1D150rG(iXMVFrrUVrcmsuKB1OYU2dbkzfZdLcrtKOK5o5QMQ6RoVVWZfwZsXweB7iImUnDdUU4v)(cVARbbzpENV7vPVfp77BqkJglrViW2YrkDVaa0k)Of3Z2CTA3DzS5QaI4DY1wYE5YASOab6TPEjP3k0K7hP9E4pbs1U3iQhl5dSwG)y8Ce1BLKq5g33c4hgy4F8IgmOIj2kdYws78ryPLZn3a9FVdRMwHTFIAlSMbAGYbMib)lMCmLmwIqS5k4At7GlDXWYomAXdEGLiy809TKJ(axqgUY6hMSL0XniFtKUghnZ8hu237rM1wtB7GtHE8O4hl5JdlUQXsiLnGnm0LOvaUxGy9ZtTFnfgAzdNHMqrqB9RCKTuu3ZqWhl7udNzMCrBvaYmf1VJk09yKkRLEkQBBsF1mBnd2HzsoAUR90QuLmWsDphTQQJwq0TLTBSPDETqgSKi7Ovq2wUBhCoRKEN1Cw3ATfT3gGXQrKT9p(JqURxzRAwxzRrbXAH0A2q09TVaXKZnrU3kpB9lA52kA12kT7Huyc9QZg1dXUktyRBR5woOox7tHg7v7O(Lr(bMu6wQJGOJK0Yvr1ak7TAAq1dTMn5Wo3mYY1Oxv727WO3wmdJR1VZQkzPpp73saWAb)hVP)wY7ZQzMTgHHTSa7NGM1M0TLubTha0(12DTm)2)OUhX5GD1TDyP0QnXTRIEVXARBV7560hJI2mivIr71W(2(1XZ7y3Twv2hXdO1QJfI72656iDb5rRBQ9l3K1JKMrdMWscndJZ5rT)OFxf16bM6u)EilpI)2ArUoWgM2p27HDiDpZjBVAr6EhEXwnOoSsBd6JPTLl1doJo7rRDGz5zREY7A5(TZ66nSgBJg4Iz9J26A40ATV6hy3ifPZAqOJ1s0XQs3iUi(51r0r7v7HL35G0w7xcZbndDqUEpgPqo2kcB8vD1pUwHEy1UOxPgCTT2(2wILhswewY18HN6)GekEa5HUVTcXsUh7vPO)5090TvL(TfM43B7uhlZ3)Iv57Tu8plLDz8vqRTeB(X0I02Q2390kCvOLTFwd2QT2FSRWvUneyl2bYENVtaX2ID2CxNDI8O9xSJwwVPHRp0VWX6njj0PBP0gluWWdOaALiaj5IJXV170oKfOURZoBXn59)AqxdFpnDqXja1vM3zA(PCW1dHly3K5l22uUrbSKd80M31lKUAZeV89Irpum6r(hfb7iogZjYXZ9ahChF3JscddcyrE((cxs)Rn5BWB4XUDvwqmTjRybmxNGdefEwONZrrjjjHjbbboHkCG9OqHEMfsehl(98LfZxLF2xl62wFHX4g4ZlYjjio(WMg3OiXEcmjmYVJrelhDVKGJIbwrSNlDMfFaJ(ZCtoYN)U(Woy0lkqn8bkwq4bYCDe2ap7RCfSt0UanuaCeCP8XcCtc8CzhiaZccG32pj0jraVrPVoRauuLtaZH2dE021m5WMah6gdKZS8CctIqPgUM2zLZXCCR7MhFx2rW9EhxWKXyh4PLUJIshPOlbUhX4IRbrSOaVdeSFMFa7ihoz1bPPO7Rjz3Orvcc9pYh2VQXojSWdKShgWfHddt898cs8IPawFDX86k9zGlQctqsOB8bRSg7WqXygF8ts8jpYVhl5L2u4gj3HSXbbHhOQexvrOlbBJuQZ1HPCp86YoXEitiokkHLCGm1N5h5Igu406eFAduG8yFfjIHg0EyMnf7WZqdVfui1hjA8KkkL((kGd(Gk6mZqTLXfxj2qyXWb7gCRpGEsMId1Mg1D2XZZAZahHNBmqMEN3(HfaDhjipq4X7UbbGk8nJNLbmYRMCcuNVbWXRK4Zg4CmYGA8WCmlDU3hw0UHAdosCuqRDlz2z5XeL(oIrqCNBQ7k32jM8wVN7hwbh5bKRXkS)k0OSmmuqsV39FlCBea3WZvL7rmVm6HL5H9mzm8qXbZ4MjWeFWbT31BUOgn2pXV7UcQnf2Cv0qxffu7GcfQHR4Usx0LtkpSFfVLAPAA1BOTCGX3Dd0IjfOF0vSLe9CfewrHmfjljq69JwgzmX7(5nbuJd5mgDGEpDzD1MBwsevlvF22fNSGd8586A4P1Lmi7gDx)78Ntdb1kKsKGU8EmxF1HCUxNKf(WI9IRTliGB5Wvoae0zWlmQ88i1V0NJeR5IEGol9wqURxFkXpR39OUXts5ydhJ866VYZ(OXs6XwMu41dt)AOOzkW6a1)czMLUH2opAAxFOK8BWvwrdQDLJeNSZ0VpTizXE3c6X6WMXXEQIABqFxNDd4FapfQWcXjynsA5ULq7P1lkKrGoCOxAbmlrTjLscstuKWBjAJ447YwSqT6qKYeGCNgQy9QM3sUT7Il5Mw1D3RcC)L8)(p5uc8YEqtOLo1SDuxFmKzy9UBA)293iBREMwfKUhzha(0b0ZJKdGUAZDGuw7UhmTPkavJRmQVhJ(JiwS1LCRng5JLlSFOM)Tyrwup0dXbG9ysS7dyGZI(w)Dzgd5tM)hy(F8irEYlG6WhXsynhIVblXQAAMpwDXBFXwSXB3E(fhwmGKpME1dBO9SX4uwXVr0tSQInQ5Grr7TAS0WgKr9R(7HRnlUFoKuB2F((fpexM2C(z4D8WC7zZpEFUBqAz(NXBmNN869KxVN8693xVE2IK9bNDJU)jIMosUnbPF4DIAU(dXRM8s)1xUy)T5JBVDznv7cGMo8ThLgmG09RV9Yx(VF7BMEYRUyeZnuUt9BVW3jzzmdB7oiG(80(wsTqlV3Sk6)u8P7rgSES8P7jSu(Kp9lEYN(t(0FYN(t(0puF6DRSIFkoaECByLsTApDa4APjppedKFFfU0WJWy3jVdLfouRYhO3UHwQ3IzD7MF)RxhQEYY(tw2)oSS)3TovTLCJ2UdGGFAoa8cp(7ZbGF6SMSpJlKdTSc7LxG4wYCpDlmuSC6dIR995wyeHNd0mEFrGKoC7c7(vgT032mYVfFchQ)PhNebSTsj(XNgaGW)d0zHLGV(b4LyllGN)66QGq(r2JO2cX7qCR8aDh8acTTl2077TcD)P4DW394VpVdw4mItb)Hg(2x)dgXR)t3RWHBwDK(KUNLnLKyFCCjCaQdhOzF76Lwm9JG9JPPFHxNHw13hhaFCBoag3U3F18dm0g)a3ahMn)E(sTLK)pDt)JeFIzKM(cQRT8ASzHF4stESug(zzX)GcW8E9fg)pfpcpLVWt5l8u(cpLVWfpLVWF5ZxOBFX9tX1WtlqSdXVWtntEuFcp1mzdbVF0EfEQLdBXVG4kc6VL9s2DKDp(FZwvuM7q6VtGF3a5pT1VDV9L9(fMHPbYdY)mEGb945FMjSiSN(NTRc8y4e(asrB02eEqXoSfU8H6D9H4bFlbUDGlrGhh31pWu4MEXyoRThW0tUR)VG76)su5V92zThniwZHZMpcBPfzlsXhMR6ERROrCsRF(I8))4Blm9Y81)iXQ)wT20VhpUfvhSfdo8ncuNlhQxbpgI1ogTUmVPAtngSaSn2IWZKoIaNKYhS2PCS5fLYJHo6ic7vIpNMJBQZwaqUH69nZxmHlOViR(UlrIwKWMXjDg3noBGnKQ9sBGZZuWquNz(e1mbWtyKRJNtyONJpDOD0rOJpwh4pH)WjbgV)jcikdpTpjZpbPxxSA1e(d7Xc87UOYoUp6OigjPBkHtE(3vPoqSCJeNprrUbUr0PA7HC(XibJlH5qCnQ5h4KeRraFHmUhnysHJCjZBepGtAlh7HZ5XMlnW3pw(SaFXZEP0PfE27kP5t7MTj4d0r6icKFsKxSNRRFeESCrNwKtlUvErrOM5jcVP0WdN)BQzBQ8zqLKw4L)OfuBI0zjcoe8kECvfAWtpsUYzz(kyuB6beVGe5g4ancpX90p55ascqlfRyuyCFXPpxckNJdIsY8d9incMgN)hfL4g6fg2Jnih5ozZlfKheD6K0LgHyUYdan)aA4WRWeuL5SBPt4X2SBOi61GmIociZenzBHMNRcG5Ha03qdDimloYL7Gt9HbK)9nVO)WceEtEzED2QVTtBzoG1ZP5dVV2mvi1exHJxuCowxxClNr8TlSh35i5V0VAXYtWqDJp6gNIsR0JlHoDs7ognnpid1p2gjWwZoMd(LItuuTZm02LCuFj3V9Lsrt1NqGG0AKWLA(xxNvU4Lx)MkP96ZnFlqTKDCFJektIXAyLu9pgydDo1ewdoYju)FcFwdcDX08G9yJSOVPRelepTmHkTqYQTUv2HwoTyG9EtAZle0g(N95rotG2I4QWIQfWCCTuUjj1SBwTg5N(U5InjL90Z1vyvEeowjnRNagtdtHdZ5ISvWODUU6AG4eNJoXn)PQ9mUYIRUYY5pgklCjgVJpi9c3qtfJWF(kgd9UTNkgwfXhreZI0OM07XpCDGdq89OKq7sWJPx2x(Lhfro3X0cHdGHy109wioeyXtqAzWX7W8VzKQB1(VHi90dv4n0ISBmp8pgCD5646L0Dlj0linJi((9SvBYBACshijBlGvF6er01jYnk6appfrNYK6H7EPoOPP5ftq86I1YlomxC2NVe0twacm((DSQxOunEHCY4V6B3Eyg9QxspfMqJBJyLINB3KoPCCt02mHMaWKSQOoAkSUBrmxHYNysbMy(NaelyqvvZ7yQJB)31V7Bof4iyDm6hjnIOxoU1Qr0Lf5S2xxogOa)HMHLoL4KJVhp5uy6(pA(29ibgdD9KMMIBkVfouSP875F683vxXhHC4tyPHbyEPCygZvbFSeX4nlCMNFKpZBMxOp5JldhW8fIhMZtcGJUuykJ7El)WqNWGzjSVDV(WfW)cFxw0SKOEFtm)BI5V0mNOKVrvuB2SBww10odVWeSp5bEb8mclzmVy3OJlrHo6KXUhkKWrHqNKTHcL8aW79Q(ZybXXEXZcJDT(QdECEkCXCujGzFMCzEHWj9k(lHCpLL8SAfhET8hh5yYlehokl)wm9BT3Xr8x4qX)lliSVN3SGi7qTopZ4TIDyoZcIhb4h7TI8cCNL4hDyVfNzhXz2b2PROaqD(TvF2Kwh3jjXscCJ8Mnk0sYeMtCeFIddcdNfhmYe3do5pTFShh7c)gMS0Mw0uJ9PeobG5oScIcC4)mbScY)PdJb)FUH3YWeF5EukhA2Nb2Im)3N3chmTaVhFfyIcsPB0Q3)LI154H3U8cJJg8D9meGSJNcG9DnqeMTJNcXXT(mwKGCy(Zcd2IowSohmWn0fmTyLjWnjeYzdSOGOi4No(It8wRYtA2FcD5bfaMHs(2qamGB)WDeT6Dp4cCmzgl(a1sI8JcNLe6T)VfyZkXj2HBI1UfB4Xds3IrvxMVRxq8X9TUg6ZSbHUjUZ4gP2FimezH(EZ89gHAawILV6ye3ydGioGRthVFM3DuEwMVyS3aeAeIs28M46d)4WKgmbyU1BUfSDr066FeYR6BKGMRfq2DLTsV3C9uF3qWPHWRs)H3lka5REUXGbk(pyUJAvQhICbuJ)zn3vohVlKeaYulw3Jd5sfHhaj6ErS1qP1PYMBS1r0oeQLvWr(rDX4iRgxMrupEPUZc)23qw)8Eh4Lzy)6qG4LpNBReSWbZDJ4tiDay95bxQl8y7o50PV83pdRrPXbcUaQWUalUEzOwdFV8zXBhMssGcaj39dK4YGIvdGeMekKpUa1dewKglEebMqffYFVOqct9)K4AS9cMIdy)myAKV0GuUw78pX1n4g1fnDYIgJ4GwNaFEsxZ)uJE5XHBmhk9CMl91tTuVnRTMkO3ZJvT)ylFXerEB9BBuxBt6shkkek7ppfD4AZGDSrFy87nULd6HB)z(fdMzSiiXqi1qhxymFgo9fQRCt63f9pjQ3496SVs1MMiCFud4X6J5WhYOeF3iy0HbGLcLBo)2cS3HN3Vbo0nweii98IpxuM)lVFzr(QfD4AJ2nUiDxlPvZnMpE7FK4sxVmKGh(wIcUK0d8Xw6G1ZOtcn0xSqeUwE3z9AsqLSR3VLugnmYSzwSOihVeUszGtSy7HGpR2TGP4oRmkTs)M)FS7M7idI37XM8sfYqVhxdQb4aPGtK15a4StmFyA7fH)jJkzzPX9hQKIkxPjJ1NlFIE)zXjNQw5uo3sBPmB1vc2QlpFEiCHXjhR3VPE0aJfG0XxmCzBaxGeApVAbivsI5uHk9IdWRVe3OqEudXdNMxXffBkPlDeG5)g6EaVpdbTh1vnJ9PRyS(6WCnkn7mUi3O7snMee6Q8bZL56YtrkGtQcuvbP9QR0VCOCtXuksVRK5kAICm)vIp0lQfFg92(CLRKWemyPu5Lovq4rQRIiEYdh2qtPdMkVEMySiO0HrHSqiZ)dESeXNHr3obUQGZZ4HD5n9EkO)RROaIcbVixLTc4jN911CgqZxYUdP6vBAxX55cn)MLz8Gp5V9)Vd" }, + { name = "EllesmereUI (2k)", description = "The default EllesmereUI look", exportString = "!EUI_S3xwZXTnwJ(xjpEVpivCFZpPnhRA82iRKzYutvDr1nLeVQfz)rY2YkPY)97zbaeGeSxKLDs(IYlrMnjWbaN9n8B)J2GS7l6YH)ijRyD5NMNVSOY5WOGx9pAJZAN3uuu9Mk3GahTh8VQ8cJCE1VJFD3JRkG)31RxUe)Ipx00wwxv5dVEq2ICAOD9YwxTSE(DVn)X61DWtIZYRMFBDtl(R(zD5n3u0DsEB3v5nWJIepr9h4xuF91TfD)s1ba4b)xyuqAGBGtKhnvTLlkGx)4pC5LF4D9V()gwmbUrHo(PWBgIWSxq2jN(UzZx32vF)mxNzEHPjobbZsD1NAaSOxRUE5I6hQA1bHOdDD8dDdHrS4aNWEaWp7Yp8r9z)GOGddt8dCccdCDCJqaW1n7NlUTC(YIZ(sjUBOwUXzVlVS648g9jB6L7OzZnoc3B0xTWKD2)8Yzx2Kp)UcJ92GmyIImMP(fsq2Bp71xAUsWHooa2QIsIPDs)SvfgWFu2QL5pwya(WXvIVVFGFOtysyQFYMpUoikeFFJnS0SZU44zFS(bZfGBm98twM32ErrB96M5fJ34sIDCDJDrqEJ4jaCg4g7K4hh44bGBcDu5jWezm0THA6AIzUb8cEv6M6K6gNgLsZ1aeodKXTSu3WM8yusIIii23pjjc2y4fQlHTVURCzz3Jp)0bW6fM0uhNi)G4iImqISSvcGdOVnY1j2noo2hznjO3TGLMCyCOJBIVRtiGNsRnj51xXefMDX5)4BmOhIpeWvcsJCs9zQbyl86AKRYiuLq(h0XnjmLW0WGWK0KaTL0OJRGuKbINNlqbfgelWkPb0cdtlt1EYUCmlYeGdzKBQF4SuhdwK2i39p0j0nomoXDR8gJ5TqbJHGx974MOt2dLl6U9D5DZVLfoyWfU1orpYLdzfasqaMAt9sE85GyBRvTxnucKMyhnwpwhv(C)2IYBUTtbZgSCbCvKpltGV628kqQZX1RRw0(B4coklFXI6kuqOxu2zlxw0EFrtXpD(jcbpVlVk)gIXxC28f3Jlp8DDZAQ7Y7azTVPy5QIMZQYVAzXIxt)0TGWWJxdYl)x3wuDEv(8UYpxayVUPzRBloEz5V(R5nlW3aho8B8hmC)4Y6h(u3JGYamBtqUC7VHNhbzFUS9nW4)M61TLv3GJAGqA(hRBlricE)Lfx3Hlo47o(MJRqThGtoq4)hU(I8QBk(aOOaiQaM64SQ13Fr9dTvU4l5K1(q5QIJwIBvohgtQDuWloyUsYUR4XRkRwCe8JP4hKKDldjiqj2fyGQ9wCnaY9ob2U7GPcoPaQtX)(hP5Zp7QBUagkhuSu)I7D4xq7MXzxv3SOOrSgcLl)3xFjrNdVIcM6hs4T5HmjRCED1Nk)1Ik)0dbUyGi0WapxATcGdaINCloqTimhNTkVArX9LZP9)vfflQqEdWUcTrY7k03gi3i(GuTiupObJWj1lRrmhazH)QSBe))RGJeg3vSFsGOR4CshkEBzvrBfT9ek2lUeuE5o4PTO(F3wwrGZCAz866QoEOKRq1g(X0ul3ppsS9dtFBAMMwwavcRBOCHrWeCoL38ZLTLxrcOqYMLpK)iWt1ns919toTxayaQj3aMy6HJQkV)hHZPeedhEzq)X8U1n5DfFO6KtHJJGSQ87laOtskcZM6SMxngyuxWpYNovpVR4EjENNtw)usQRqhmigP5If0wfhbGSHW5mF1qXGaO0DeWdaZvcszeW4DTlRr4UUI0ec2ZUe1p3ydwEcaR9XymmnpUoszm3BZj97vJibyafJaRK(N8MlHTlWMeBfEg4Z46ncPF(q1YhpVcoCQMxqCF2ahlxTToEuH3wh)uHmwHh6i9mYAicHVu0CKUsWMMp0uwuXSlrOWJjN65zcWfrkk45EzXx6q6rD(f)cVMhq39VPzvqjFj85DLRyon9N4cwhkoNVV(SQI7rEFc(J82jZQ4)uxFVK5bUyqmuItI4DfNA6qMaRH5M8py2JeNR2v5ZbgJvh4(QF)fE3a7lHOR9N3TN2P8l8RJZe2jmIbMGi5zHBnjYrNoAxywhN9tSjmpfw1aj)F4SQ12BFHr9lmQ)2OKTvHy)Ve1S9ir))Du1AHgcO6pOkT200g0Ghnl85HVnz(1iLS3aB7X6YjuXomJSw9psE2w0FguyGvIZUg3aZjD9qzBffkC3VppWKUnQETnJqqt)2Ss2w0RuE0l0tFFv6(pwnST4bGDr)AtPLpxAwRP33M0SEdwgh370PpHU9bggL9uty28axIer2HUAjAzQWyF0EbF0ylWmnYNw3bwKt(jyK3tSApToVExbeErX9GHdcR5yUOb8Gl)fHnVplA3r(hyGgoBYg8xtEqLxLJnFBspqnHnW7IJPaZhv77hr2w)jmgtsFoB3kDt3fzHPIPdN8ZUPP(HtlBkagn1vTk)mBG)IFJYJsGgQghkAgXoIvHjFGEdcrSAnpt5j4zpWBut4BclgkVtwDpeWzCzHXRdW0ypPne)t4vblE2Z(yZwJ4jC7Rel(NAloA(8IkYc)r0wm8yZ5h7L95Bw0HPO5HcsiB1TYZEApT276YEVlmHdxn8QH0PC6(G1MIdaXGiGPbZ8I9tssMfe69u54mH3chtxVbwn28V7E6kWHo)DkpacSq)ynsF(dhr6dal7P868geeO7YgbM8y)rpgjBSAg7cNNTP7InFHyWZXMJ(n5dTj3WnwRLPyfDa73WEVKtEJD3vF3QJ72j(r282VnTaT4eqtL62Qx2nL2yLTQ5i27ZDBmIggMaLoL7L)c7Lmc72cAB04atPYiVidLfgsrgMvv8zoI)68q0orXdyHE(aU8BkxSOO6he673lss4q5Y2F82AkiEuWwhPpnR0dqqoB2n4loR3EgRssFMGFX06OM25liBOMq6nG(pXcXWt8YDfh5UYjNsbjw63wNVrUT9jjbaeXkWsmIh8ZSiGnW)CAzbtOK3EezisbOXrRytkK(XIM26Q8LOX0thdsLhNLU8FhJlPf5aw5BOm(BQiwA3U2Xoc3kJgRkH89qkGhg33XsbShLMPIA6Ujf4PeSnBbgApLjyjYHMQJmLebtP0t5EHDwuaPN9ifV0nm7pJQJ2ZmYi)9EMzgzpxhgNwbBql1bbdMn5qgT7DlE1wiX3q0Qb8Q3wEDXp8P8pxwDZitSNcFXwGShRDQjU3Uh76TAvS1yZTjhSPfMedYMn66QNUwP7B8)E6ALAQR4oAoHiSB7jlidg5BZGH9rb19YszlrYEkXPdytHPpLHUPECIpjYjIlX8BjuKZuQutcZDk7AYq5jZJvh4g7EOxqAAuAqIxa7QKMILFSUSQRnk7KZE)LNDX)OnmB1GN4M9LQd88dW08jXpnegcxmjBu4QYziMs(tpy0JDIDWuLEpMHi)d999tCC9Isd9PjqtPsykgdywgD4Re(Owcubj44676LMK4LWwLTZlBNdDCC8c48XMZgsRomqmzUrCMYg74L4SN7WuElnb))NLnymLTHtqFpNehNyyNGYlrVSURUQhlIXT6OSQwLiDu60lZuUeqn(ILlp)02FRcoQdFvLVNBI3RQsC8d9FvLxuaSbapn1Xpjc()jU(oUVQkiGYPD8)7fYP7PdNitT9jXuizWSW)wckOlVTPaKnTCHMhbdbluYxG5(IXdPHt9(y(VjYVlZFa95iIia24LFFHIjJ5l9U8ViYBmx1KDAzdc1V5dxC()5dV)YJElogxxUCjN1xYGgPGakjdniLd5LS6nWy)ZzLKCbHprMXpI4CeiSq5umlUWQqGQAH(pHNDpriRuJnSc63Dy4ex5XEPbUUXjyoQIteVlW(hK3)5)wyfxC2BkAQlBVVF91QWxaz(WzaAMCKizrR8eLybDYYJuuwx59fn8)andKWFyShB2NKsz8ZL43qwl3v8LU1asGmDT6x546HoI95PqbyHznyQRsPMmMTqDpYNqE0IDucDkezGVbm5gNqxwcRJwNSTKGwavJ53PqjWD0J7d1vFkrrO9021Kz0fSzr7Jwfe2NhLslcWXkHwH9KuP8oJ(dKOAibcIbioF2I0wm1krrjYuTet9sIjnP44Q8M7GdRqonNR8I3ykxcmrGp69aGAj9qh63hcVeXEr4nnmkjc4UgJERLcMJdiWA16L5nyeOsZUAzD9ILRBHZEq5pCZ)0YwS8nuOp8GH0zHbPoUPyQ775JjVpN5OTfllM3bS)YflrmutCatxvmhNf3mhcNdWjVRuC4Ij6kqbcVbkkolkKQ9O7ZxTc0xLYXyfJaov9WHeWmw31bu0CuJMV4(pTSUtKNIIxGGcICFjMjFYmFfooioh4bcqeqb0JshwQ8LOm803HI)8pG7Z)W)NIBY(HtlbTNl(HpUUzvDBX)xQmPQPcm6OtU88F(m500NUKywGdkF86M67p5039AAlcq1fBrVJxE8S6N5fg9nEv3NVVQ1mVr8h4Q21j4VHRAVqV)wUQt(B4Q215Bn3S)8rxdSW)7gZmI5ESEX6CCEB5CQyw9Y(Fwx021xVLJcKgkvZso7aYNZx3vJHMEfvSkCao)p1vf)tCeffisBXCs2MruStPp9Y1nvNx9PBlVU7t3vUIujSVYzu5qZkm6s8e8qDZYfWWTmFvlvTlGS6FfMqJhbIBRVF1YcSEymRUdqFB5VykV1N0xvtdiq)jYQqqFgb8pOQuafwCsblJs9CrfxqBIDZqjwGjYHGbP(HO24EC58XLBvvqenn8(gV4rTKCW08ECDI4dQ)qNqGn(UUbh6hh6K4fd2GVNgGdweCOxKJFKFsOxsalrNQIi20gHV1nCiVOYDCoK3zSf9vuBb48(FHhj9h4msa9VXkkbrc61bpjR(Q)FM78XS(nOQoI)SH)t2KCd)JWLemULfYiTyzPOJkqpeNwwdCrUqbZ5a8a)SzZZxHMES4dvZjvvNkLZYNFBzXNlUhSgYy2K6d77ik3NvnfpAao8oXhHhR2Da0(LL3urdgyyt9kEZruIeyWBw(ztCzuP159qpHM5ak(3TeFprTsXODQ9Xuw7XEDCPxx33TKpEUVSQe0IDYC3q5YIJ7O6btZGCBCd8YUQRY0FdrzRRUPPE9k0JdO2WuHagOZh6DmqOPSmCO)2YRo94Zbf8DDMD86B(eCSxXieXecXjn5x3bAN(bSwE0k5nLLzaKCm8r4CxTG4d5MnhRN)ZRWYUhEaBCZj4ZgY2jmdEkzoUQs1yL4b4KXROJCy7bHLJWIH8eGRsEthEUY1mK6xeRBUcBiq411n3NJh)UE3sO4LOD2Gjbyfdk2Qzu3eKbavFIcUabH4H8E4LlWMQHLxpSWZ)CXcUQDidWAPkMkkR9)zDEdMQi(QnaU7k4EOROIZSXPqVydDWgWaqsIJotMooUDGahAFWiBQcPDm8ZeBymFrK1U6iByczWzzQG2XcfTlXMfhZpSMsDkJSnJOKq5bWOa0OZboWG1AMCefvmkE4nbpAyXGa(pI27cgJt1wj(G3LxUKnguBT(lSNk43GK3c4W86fbpFHZGu4aKdegInks)mVSRBkkEx9NrugSK0aiKMkZJm4aHRsSMf4lfGNS3jSsfrCnNlgYUiVCXPLxFD581l7WI(cGS1TfM12hVUrz9yPN9AIHY1ynRTGeznvYGnv0LPbhP4rI1qG)2Yc1blXbPTVqznZ2lryGrxla4WOnVnvM6rG(jzPQ(CLYZW9LlkUbo0Lt7GC7C6t8Ckz8i2VsXBuf5kKuoxvVKO55e3hpCduKdF5xXjkIwTXc0GmHgZDFq6tBI3RIbsOqxcCHWEHsT1jQFpoNKa6SLG(ttroYB2Q6JeDVaTYqhWfs6R45RRNhDgFE1Nl7OkHeax1NqL7ejZLdI4pjz(Zv3SzzyBOuzVQhQWaRxqaI1J2urEaMv6YcYzq21sxSuvv00IONra1vtljW0h0h7lWdCo5uV0tTRcKnfEnoMSYABkCgllEdnDbA382UtkBMVSqda9F9rXVMQ6s0N4RBR8fCSzSfsmuswExhOKcvS5HG(fybxNq8X8W8YP5oDkNgSa4PfFfkgAj9wLIkBfRYvlK76YhXDskkce7n)SBMVqdEFn9FAWRh7mEb29wG34mj0b88yF8UDGJK(lSzabUj48SBlsP7Obr1iOmJ3Jihqn)2CSzQmXWhc8ELfjTKOxleYmRHEAEr1(obk086vpYsfgPAJgRab7gSfEu1bc4qACYz2crsxMF1Rby62ni8cJc3Qh5u3J1PXWrMYACMuCJjxb2zT53u8jrz6syzjYN(6C92zqcbySZPjbz2Ykautwym7YVFfwA(CnBlkT5pvK3m)wdXJllNFhQL0pDXBj2Q2udLmkPPRO6eGoOQyzlGLwpijbf7DyEPdG4T5lQFykwuw4UeQVGrP(vUEoKlM9C1vTfreOAGqO1llEC5JFcynEAX15G0vcxHj5qCp0fXxGy(0Hyr(YUBF)67VcJcdGscMlGVaRck5GCzLAG8R7PY98YwuIHIbBjdGv3yb1RkFFoo)VwakV)JNqLZoOMc3is(PksB0M1R6kHnAsVtY6cP9XQ)eTsdEk71yS5zGW2)MvQtQg1Icmzhr5RAanOMdpxVHnk2vK5uYnGlRxbANxGfCoU9Wv466MCzmYq9n6J5ba6L3VcoWZbjYW423gkiHdX4UMe6(XM6h6Uv1RpWm4hSdlmm7CCndImk(VG28FOQ9)Q1vt(V3xSOm))sV6)9SVaw512(q(JhE5L4gNVqPNLeSImq)fXwWbaobFSrDDdHAzSXIOdbWdxmPrhebmaNgyUU4DaFBzHTqupUCen13FtmXz0nh0S1zineemazooFB)nbjhOAJW(jE4HvzmA8cWHJAPvNqnISboqrz9zcbVkRp9qQVlZRU7OBaf5uiujUA29hjny15qz3DkqIUBSKNSDJagFAGPj44OqMVOiFb2KOyS5(Pw43hezovAHPbLyFxgbqdj3l8U6ROLUw)RjAmoyFtEGWdJuNva69w3)THuZSPL97eQGAeTKgzNNHwZOE5Dxv)fcF7Fl0ranTaolP4UASG6ptp7(vDO7leEuq0ZwgSBjdffUds(gdDHWv1TSxQWD2uXPp6ohVa2Py4MBOl30UabyW5ebikxfOOrjHjiPo1dC2nmvrlrcrvL8tyAmuuji5Ic7MbBjFncmagGvbWIqetzyJIy8CoAr)vSeFAD5637Ef5FJRlEvfQXWLdWMO12ySy06OlkSBdidqseqspFEHnG9m03kYd1b4QU7n5TMuCuBhCqmfjJwybxsS(tOW(Rt1bi(gKeguKGYG9my1I7naecbAVfKn0lBNL0Gml16FpbANvJKuqIp6npcSMtmWmKU99dWKceNBNy85ODsIYLbf7Mpj2kwdxcbPFeLZBSXWKC4cD7qySgZvdmIWmSmjXevqRprj7oCh1aYZ63zGbHLR2tmQoWLSKX2tftgQKVAkofKZi6Kpgox0jQh9x(3i6p(3841Zj54BSZgXrejBK1G8VPXaEoR4UCZewdd8Tj3SR44HR4P4d6vkYQjaF)T1yl1YeL3Lk47H(wgtqkK5fIvGeAkfcqhAJE0d4ajpfqAgtzFk98fUvuySo5)bqTfz7qdMxatvJouZ7PJtBFw8v)rbrQcw9HkizHbw)M9pMVI514RWu2o6MWAfswUgLfO24Q23dADV7BIOlIrblC3qvT1Gkt3tNIma0jneSSvOPtXyIoMajc6mffQWPsci8NnyhOXvP)NazUg0MaupVxRuI4yQmrfz(771Jhjr)yGRxSoUi1MBqnBqT0LfAPheQX(qwPdy)ljaK6sY7J6kSd8zESG86eCIBOosVQ6uFdvqSWojIeb7kYnpAd1Qc7PgQQz0z6u6R(VOKaIS(WtP2VgFFoOhhTC5PKU)cJBuiYAVzSwrIt9wVv)CLRWYSen7PL2QWoWsdDtJdLv9Db62H2nSDAd2ZvuTV8XIoc87nVzGUO9KHYFGGdtmFJJydusJFrxDfdHVtv0IcxjRDwoa6gzwe7isn1d)fH6HwTHsLRwywJkiXa6iJUUNUi(TV1YhhsFhqCTe9caHISsBHO0hTPCvXId(mwXU(z3M3ElQa82NeHjXIrC7VFugBKkjKF7VEmO8ytXzll7k6zGicAGjJOEvEcvG)hlAqphZUXd2xLllJuDvrQzyFmOoHY(luRvSiGq1BnSb9nsRkn(0u9LOvTUrJuRU791dy83lZNJKQYMoK)5aHvtNT9if7aRFvDPGbAmjn6uMGOy8zK2TS9ZgXlZMB33CiDrXAd1gwXHuYFukSbNpbYt)PsCgw5MLA(dXPxIa33ijwS(uBr2g50q9z8CJcc9d8sdsyJKivBs8Id9C99tI9epMmmnnfBMWjjHUPShxWgeGqXt1roZy2nu4tocBP86soqMKAo62zPPBHXza7EiwEoPFjXbe4OPzyIbFlqHzI(HMbriTLzmlOpwVqeuR1vajTsovVuQEzu2ngzGkXSdvu97v03ik1UwxsE6Fobo9AjyUagG1Xla5dfQEHSQquCR0ltvtsGiTI1Dn5l7TBCdAGcWjGSrnkxHXZC6Hq4cH9yvECntaQOqPmSGbktsXh3Wirh3Zv(zPpDCiTYiv9q3J7LO7HsutxmpsLnAcYoE3mmmCRVhD0k(aLpHPSFvgpR813Gr7EwZAk0saUfYY(o(V5akGXCf0rGJO3dyK45GWwwD3JZUAjAIp(cpGVagfJM6v3clbrHzFLOv9chU5nD3UeZVvS39cApcRi8trw35lw(i95puwT461n4)a9RxTyUYbLWkV(r(ttafQ7kMLV4)hciU4aDFrxD1nR5mcG(128kegWCnQiFvD1SIQ5ypqMEJ5GujyaHrdpqcOLYX4kbdeQixYfT5HvfZlZx2((6kzJdfbjGYDvtX1fnnfl(x04Fgp8WGdlRUzRBlwOfYew1E1N8AyHz(MaTgEeiYe63XP879p2b7Kee2)P4UM(3knDcWI66raeodaXTupv5stcBlMmrg0dLsxsuOhMzj4bBV3rjnn44TDefDkA3htZz0z0uCo1dM2Gu5hozAqheausD5WY5DND65)eDfj8OW)NC0kSL40(XKUPSnFI)U3)NutBqe8ckQeC6Q339uXLt)YLwTFr3NRSFs0zijuPJK3G8oyT)2I0kyxovCdtamyez3HhT9Uhz3XbJAD504I(pM6wbC8QO8XxdlupIzejDEjC6a69Ehs8a6vaFaLVeG(j3Z5(pMl5DyisGFTPU6xrYfKkRPdPdmclkWKcJ5C3TGvkVRe4jX0kHKUFiez82JHoI8xeppkYITuMKG6W856zexzIv4vfTD1pm7HIhBaMnyAiqPsv91I))sMxtlfChasP0rXJc3eWwRfPwzQ8RaaTO5UzCZ9vm4llrgZyiaPqxIVkyh2J3FvzDhXIcg6ffxl)OFFCWF(PQYUxJyX0PaRAYv5nFqRKvsvP3ZexQi7ADXHSYm6J)uvb6fcwt544g6fMeNWYH(svqkQprQFsAua3g)35zz0nlbx8HHEhgcZJJFGtcDlSGtJFcv0F(bUPaeShZI2DSaVkCPk1ZXjicaAwJ0VuDGVBi)4yhyA3JXx35d4Wt9s79GMJkMpT7NHnaK(Uuz5ff7hLKq(fFN3QXC5rk2wWD4apkGs7lOIUwv4F(hR8s4o6ESVRVdNUkWMzKdxBJjoUUbPi7ZDfoLxNk8Mq0EcFhGPojLzYOx35ldajfdd2ehh(cAHL8Jy50dzDafIX7X95mBI)r1RqqjjWgDUzBRuaaLKe97pOPDYaVP57dq2u1Yhz)Ui8PbkhNDVlqrJnXF9lTakfWinI7tbvSqMafWyv0LPlk(eHbHI6ZYQVxLsZKgzi87enGIphfFPh9CYBlS)UR6WunfoplAUDfTtlbBrWxfbxebyJeYd01HxF9E1JZ3AhAXPcLkMIDWtzFKOpNCLkjvrGThJ0TEu(nbNySk4CpRJFd02w(kzaDUmakYW)UD7az9QLVVwuxCYUp)lIZqrK0qmDyJ8ynoZY85LHj9(xOhGmoyBNJhQS34lxid3D4vWNWQJSpYhKc(7xKGfx5d3cNVucYS99cWUufsTQka5k7CWHfN6fyoNJiwY7fJqq(iO1MqGNjAn7zsstd2nz9t1rywo37xDxEufELq0UBhU7IPRKzqNLTtfgrej2teboxhPeFU5bdDKtc84H8JGIkaSrPMRCbYEIHtMsuPCTZ1(ZuwqVgQPMj0IPuLoMSBBelr6k)bMlDCi1vLGYbpsLbsLgXXl0xxUCPP)cI81CaDFqyXeGJDrUb612XkeWoMikswie4YqMSQk75AiOsOm9KZEz63wbCuWcH8leoaFVFOJWjRryAZ9xQoqCXGqk3lCxbjZOpZx1cDe649Tf2j8CJpAjdF8Y8wWItKpMcgRwkY1FdGbQKThKrab800mYu1rC2ZL(8qcjvYnzbMje4rmTvFXxfVvngmMOB7JWMPZwLXeyymr7pFDexWiCJ4sp0GB98fpj5OfJsHmchziNrzKrEr6H4GJiOU6z2tezlc15S5zcH6OeFxGAMt0sj)e(QgbZsFgjensq(3cQDWqLURWydnm7PvyOiVQjvjaJVwpINX9WYu8LsvsIfGWir(I4fcaMkRRfV6Kc9bXaDxrzMMuLcj25w4DQjWLZhQjfoJUFrQ2IXcDFf)ZHLAuSuNwHjBzMmAkjQccomGYbS8nR1YaZIF3L4pn1SLu)s0MB7XbunF2rvIbzSB9dNxHPVpk4AAr5kQkMjSNp6cAV4r43CWlni9SQn6yj)9EEDSOF2LlFciOztY1f6ZsOTi1elUORo(gDZNbn37UQxZ5U(EDsFSrSLHO9eiQpEKosBqGNkZmfS7dO46I(I0K5Gz0WLkUy4sz1UuQI8sC(UVs0nvgzOGCk9AWAnc4zae9Cc(iUbFmKE7HUzXCzW4aQ9Dw5yARN4QjmWWI0nQ1t3JhyvaNaSaInnErk1uhzogIDW10W5vFmVPd9x1KQ(bgHjzzzWozsbI9FGHs9JmMdWUKBIJw(0bPiVK0NvASXnmL6IsGtZ8KPBkB9IveBommiRzgJKSUNxHMnxGW(bg4jWy61kBOfQj9w0PVs6rvv9QA7OmmX9gnSZUQqaSY7vABne5FFncPPA0Ey(gWzrD6yyvO6PIth7M4cRCDrcYqSk6uj6k9q9Ikmokkr3IJnn0dtdh3lLrhRIn5nYnAC2eDhhq)c9n23kQ7rDl3gzM2y73f9(4TBMMsBOHMnRBLMUSlDR0g6ODSlTi1yqdvFJQQ70)jIJeTNOUvYSKbCt4fgH6IJTyJ6(moXjb(U(u5AZkR2NhErA1oUhLeDwKHOL9qKNpTB3MiDUU82cmMjM1bhZYFOE39zc6Gm61M)i4qZHnAjcg3Ohqu854DxsorVAO7VnaKV06Us64ms3E2FGSx1hvszF9QWJ9WrH(8IBpbHBO0FUuN9ab0iUGw5vTfnWTORTrs4rNAA1)YCk7JpVAbg)(AHW6rA4TxUvs)y8RttZDvlbBkmVngoBRoduusMTfjryUyV0E0vT1nxbklSlA6nW4DsBoLBMSRTMlioMMiXPUqsGWiotFOSZQEz3dacfKS4AA0Cv9Oe8uYFFQbBz6x6DvbManHBg6zmq7Sn0OzhOCGc73u)FfB6XcyTlMwK6GKegLBmLjMIUEqQrEq52QZd0WLj4(TjDjZTvqjt8MWzvPfHs80KYAWkWZGsxlObSLYpb)b(Trqchy3(Sjryv8aoGYgmypF)PDDWiNLy6BwH(Lt7bKNHKohy3GfeF)Q6YAkeuCOH7JbhDSGgJJYxmpXu4yPc3NcNyWxVVETWIdhMkAcBWdcJfp3lvzA)byX98ylF2aXujf3Mk0wSo3y7dWv2IH6pjxWVdwDlnZYuHZ918AGJVAzpu1v7gMSNQDnHXQWcCWXW4sVZAQppqpSH2Wo0FdJ5SXn3aSM5r)2kZtMEN2m281Du98PSC1qyMPVtS5qitZx)k8D1ZHzUwIROoVjJmE2M1TAULrqMnzyj1zkbuvk3jyXLNt6q1n4c2PTWnXuNhTqzB1o3jcESTOVzjKrpz7zD3KJ7ShkGPcDOTAlFQWpUh21A1txwJra7NuwLwTkX1EfAz1(ua3gfUjSlxjzZWKkYQYPDO1MCT7(40cDN1YAs0VWqvC2UPLJ8b2U4esvybj)XmkjhuDCN9mUJt7JboOvIuwHsdf0qv7HBAKNfTAn5oNYgd8O3oRg9M0zyAQ(n6XjoU5JI9ZULgaw9))0b(Vhxsz33(hKFBwbomY220oDtMcoGvTYZTKncBj07kZg3hTUhQYLkeK2i32cNsDV8OsNIn79hklpPm0A6(KI9KPYwuAEAHBDGVB6d)GnN3SFMBS94Qk6gr7tCvTKEotBV0uET5zlYPdJGO1iEUJrlDsLm2)CK6jAVJUEjBmSPdD))Uh4uoAyBqg52C3fiuAyMrT9WJUJwGTtXhDNZcKNKbAyEABp9hgNTupzJ5MqEXENsutQe02C69wdRQf)SnPYlM(qYsut7ZkRPmkDO(D29tM1GUovqt3GB8Mq)5TzYPUj1g5gZofQ0jdIR9ycnLrNJSzDRXj1EilFomICk3HAX4YVDriD)CBXaVmaO5JtTVnf109XaclHt9PB1)iBjEcHuDxJcYKEk)pnbwDCovSjRO)AJV6gJqNi9W(tJtVNoLyTMLz2JtZ2cQ6ZqusnJ0fNcK1DFKT(CViZEjtxNOVS9TntxtZwGLAhgcFmlN2uzbypPNnB9G4T(DD1X5nKTyXzRw3EBXIHopwVjFf50NsTjI0nNk(8yohZbwM0y8tdAeWEHQlhYZwCtXWFgtzb670sLbrFkdiY1VJ7cYaW1xwStUbIANYno0lDp739U(uxVokw8Xa8tJoERpGLQuCWHXyLX55HxMD75fhNRtG7HPrrHHEX(bChiYn7FUUyDXN6Y7yRFX6HkmHRCnVqpxNW9CjCqKVZHXP4D6xyyOteFl4aRb8QzHR7UVMDOdSSfbRIFU42Y5llo7lLCNpdNMi(w2Zp2jnmbdb2EubAUXXIAdmnkoGqKOfbEtRqJUFA4Hj4rrIpShTNLI3bUPhgaFBawjJ(XuVdGgE8knHpcOgl4EaWu19HxugFPRjxqerDIvfahtxeIHU4nziLP)7XO7fsxJIbProPc4no7D5LiHQQk)C4cBKlBtSSp2JjWbhE6WY3jknMWAC9YoRAE9AmPp7NNaxVdXBZpaX0ZJU7I3NPrTthR2xcDp0dqxdJ9Idj2h7X4DqqO3HoW2kqmY36grWX4nA7kHrbhgG1TAItQh1Le2JHpkeqHJIsd89dt9Po2xs27kN3uRpdaQkobPrUj7nXAIJhHg7bJFAkXtind4gunVqBkCJ9pK6nt7bSduieJWuFxaJWxuEdGGDDuMeFAVpjoo1lDpplpii2LQjuylonGRFcIicV5RyIipIp2tJBPOap7VbHrHeCsoW3Xz4n8HOA2gkIa77b1ZP(8HQ9)bI5OlaI8w8(F4i8UJaL0r1YDk2K1L3LQNM3LJY)Wil4QgOrINCf36bYEapDPoiGhr7iI6ybuEYH9UbgGuDXamo)11lxu)qfUYS3572MSvJlMoDmhnz3dbgTBS2WdLvCKa6g0zwNUJiOVA)kuoqCfIkqCiz428s4gUAGTfSnzNZvluJyBYd5glR0kexpoRA99xu)qR8I6K41HTKITxhCEWHh8YQR5dXP8CSptF5Tn1RVHAF2bz3v84vLvlmQqw0xbiWikIckbnJZYx(q(JTO791U5pO(dkwGZxc4kDLRO6L0t8UVPCXIc6cGijREnamxKxjWKauCUIi4RucGJLStvypwMYsFwpJ0IKa)UTLm5D99ypfQP(O1Wp2Fj)IqTrt8X8N07hpPzxjBAq4s32fRmxmo1FUOPb2wEV(PFp14U1Upd13C0YjCMlZ)Gp15JafALkxGS2rZLyfCTHjtQTeejL4Xikc6jkMO7XZydSm)m6zdU4194NQxCYy3LxN9JdZva7ETwpDgNg3yZ4aa6Hv9Z2oM9ZUPP(HtlBkiJoA9YwJvAUbT1)wEZNOndg1ypNMlyT0B0l0gqp03wLu5bAFBsIBfVyEuHvSIr)s9(AqYaIWOn4ypC6gugc1LROABt0J7L73BN)XagdIq8jUrDgM82yturagChBwua4AiYmgIOdurrTtdez0qMNZhZxSaVDofTzyE3dz6OTaXQGIQFlJYFXnRg7CyGLk4zLliUd(3)kWTKUli0Gr(oOfBBKetwTRLTHodomRQa2BnSYYo)PNDjauBP)Rtca3Mi05yI3zp7IaGby4Y8)DiR6DG5pZPZoZF7hJdKCjMBWK6F22njhyfUqc2xPqGiD2Uc(z0CVbE97lpzUNaneZFpfeyx)dTnH9Hzp33kSD)vyLMEeZE6(iCm7ndM4Q8R8VOS7NwIoGzBNRpid3uDNNg7D7sCbu()mW1FG6mM6mYxQgwvfWIIKCDrT78)n50NKbSO7kbdP2aBEl5w2wK(3lyWQFWm1XL9SP9JfRIWMGWXUK0P4qmLmnCZCk(Ldeby43Q)QiAZgU2UkABcr805NnTk2bzCBYah7if2KX9xglCErQ3FhK6ztfW)ye3Tb5VFVT0zGmVj1z87JGVD1ehlEzXq4wy2)(JcNU(nrQM8(eoq2IfSzT4olYIAsEIBDA(c2AsXGJSi8nF4IZ)pF49xE0BNYbm2dRWx52YuYV3UsagMQsx5QdnFB0EjfJHE3O)DsMo133)Amx1uMo3g5FrMo1vHeEHFGJYEw9A5lY0FrMUPRf)2Ac7lY05MclWK7P52YTjtVpJk(ojaGI0(ZNaaA42zba2SN67yaR2lpqoTbOBGOysPa2SACdES0IZd)lsyPM2tLVexk7XLsCDP8sCPWWq9x74sTN66zKTy24n(ShTkFS5Y81W9piBwB(Nlwa2HU5JkMpWt1YG0EF0(14XVnfvlPIKdIzPLmD4BPh)2GSK9t8GS)dniCT7zqR2x7lEYzVWEeqlUN(pi7fEXmGVNMbm2748TuY338xyJrYI4STxrYYUc3wJV1tu3)9uCGEsoaIgmsm3jQ5vJuXGYMJNwESfGF7xJCHbH6HafB8n3DbcJs4S9E3KVuS3CsVT58EyVmyWw053yvl(5Yw866xE5CrPK3KgqSVCM3G0d7P3Wg01ypfIyp4OwevqQiS1eDJfvmvUeXrJ2mx3amVXPX22Z3nrYtzpF3Mwq9F2s7Tjen(TnOqJeqBlUu)blWWsyGOl1IjY0blkKRLYZrCYnoHwKprbgwJcTrmGIYUOy1lbbI9kN0(jQgq(o5)SV6897LKIWA)I7LaOW3IV7O4WxsfqrUwVlz(9g9W2lbq59YcdyVnH6PNmGpx5eXuvk5FXYeaZQb8Re43E6kO56UVT5S4GAqC3m5(RWow6wI)RXow7(3CqHw9m6AtlUB8R2Q1HP3kFhLSx2YoHP76LQM8Uu67qwpmTPP7yqX2xJNFY5UpNH0MfQ1lreZnRPIRR9jQuRNqQ7pbtUNjP5)zW6v98LJzyli5gvUwuB)ZUxDMW22jmB95SGTMOajhynBFr2))Ee61x86VetpTIb7Ly6TXIuZMmNNLcrBFf(9sm9wu0WfNNKV63SAs(pqltFjMEpLYq(RQ(0WRylV(U7XOwbsOQ7EilIA8l8ZoB5YI27lAk(PZVOOTEDd1uxW28eFhFW(donBj83xwo)UFSY5q(2jIBzyVv8CUTfCtt(cS(R1QpDVSBMV4yq7(f5npEbH3hlQYQJyQamUD5TTubPt5d5G6XUfVzX7VXtXVjvnti8ef7647ef57eWnJK(IdaBPt9a)ryl)c9TE)3ZWaOkdD7SZ1PAy21LlxEm8Y(EHb9x2zmWQTCuBgPzRRWUr)hRvnil3yr)kk2n0LAsj7rJXH0OLbJlWnC3WOKiV4GqNuCfj3aH5FemPwJ(zxDJ4fCY6GJU3L3CxR8eGxV)s1bH4gg(UWeHdMpFbMk3ZbRoKZgSHyS1XBqbPX(j(UUbXuJmJV20VSeqP4(KUAMpM2P5F)tRW(bNA2qelc6Oc7Od)yraZm2Uzaqcom8kED11poDn(a6ZwvSehvQ15Obe)iJYnQ9qeN1v8LU1n9n1cClb3lPKqfB6b1p8JNCQergr3DYuyM803Jvjo0s89JJtDJ8Je3JIQnd5i3JBYhmP8QVV(TLfoJNRSLXfeYdhDTMqKmNDp3117YVPGI0RgKX7J4IH39yKkbLNRIm60Yg8Z0logUvnJLbpFzJji01hge)py0v9xC2nfvfn5lXqNS56VbohYPRQFCNx)oCZKGu(Va0vxhI2igyNvEpWrzQ2XZe5Qamv69Jf1DZUoZhUeqLmgQV(62c5nBkFxq2FRYB2yd1B4UmMQwFLaqc7VG5vxhyWtVfw63wVCbJaGiRYNWGGKBe9RjzfFzvE1IZV(91s(18jU6RqYs2vv8cH5bOyjY8p4vLK8pbpg6DCNGBWHalvT)tuNvJUAMnzpypnzSqVXahtelqpTmHkQqMRToxwSnUww1bRSto79xE2fwzWIogqBVbryjHxo4LRy5vLll7EegbUVgTn0vbhvlGzpvQp3pbtCDCJf64Poz0AFjE9NxFCExfxRtwDHPobRsIa(2UecLEJhc4iOMl8geUmFjMeia(b)H4Qpu0b64wu(3vQNPjwyrWcIfb68xhXcW3M8CXUtx4YPVQIWicLl(9LWyS0TDKWWkk(eOywWg1KXWiwd0EvxQW00a7b67HPK8WXyWtrxoe)f0IOaemHQuYXN3gn2Ujcic59DmjQNB3oBG9VPMQGQf7ikTqjKDLtpOr7iUDC7k1pf7MHXoEEXoH7z3)m6qql5qqtsVWIdCi2aSiErxxBGsLSst)C(Y1fTTozJOfgsALaAN6H3iWoauIWplfXmGlVJ6uNSo(8(SuNofDNpkGcVtbkxjVAX4f68BXEzeiMe05GvYNo4yXBmAPyYGp9dMTbkJI(DCVbBGeIiJ73yL8jC0ftkd1tKxiwlWgaMK91pj8IOCcr(wfgPwYSEaQ)jRiT6FIC1J5(IzD98EP4tlnWnONE5e8ePI4vAll2eRxwmTPM3wfmXMxzHYobpqfntqHQSYEYPU4P0x97)ox)18DZhFt99B4JIy1TpQTT8MQ7bJmjtTaRqxvm)Jn1GaUc8jEzruhY1nB29LG9OuNadEGVF28f3pd7l9ZaRnHXOybnGTZafWkkwm7ZUaDQFa9Anf3x)z4zI3ORE2n3w32nB(IzOgRW75Q9EZwu2cSJ6kRwdFZkC86qlp8aBOHbRTRPC1SURUA2YYQ7G3GUibg9cllUjF(J8uo7UIhXxqao8iSQUTexoai0cJYnlRFq(EIzshiNnFzrE16vZ(S)LCF)LxdGf9NG)sXcFycijCyt)Kn5xtHvJ(2PQd3vSSyoqb3FkW)IBgVNkpx8ZCN5(B45G4A8Loi5M8wo1j9ilmp)0kGHKVi0Q0khEKNhO)eIrJz75IcGO7OtU88F(mYclau7BDDn8St3(4I7m5RiQkQh1IVlDj2c43)(tewC9cC9drEspBateGJJCSCNfJ7qdxt8cyWQcxOCqUgS7Lghkm9uBXAbyhEoeZutWUDyGBas2XGuWoDO5fhgtkyPnm(HKxoE22MGZmbm5TtWusO33d8iI)uks1qyWelhahN4wnlEMRtONVhSp6LqCTCYMPyDWABlz(i(8kWa6xvf6dKbVQsSVw55eGDME0fDQXozMxyKBQF4mq8K9HYZjmn4vvhG4So4Feg66rdtkF3LDhis36NstfWsJxgUoZCHvHFemNEX06iv(BbZ8I9tssMbI7TdfWNgH9j6k8pIa9QR8JDfT(Ay9t4DNUwXzw(RvUavM23aO10)IgkWajsUGcms4D6y)zayySpbaVxyAItqWS0j2NI8H96O0ahagbDPEfEgeZtGV68cLuqIygFKrmOsPtnaNVcu6XhKal5BX73Yw2ane(8Q(tfDyJDMLNnEy9JdPXZ1nWnkew0Ujj4A3nXZfomDbGgpA9s4MbQBgyJutUapIbEHClTNbS9BFSAo6hh50JIdcP7ls7RpCbeNTUJSo2(oa2fXb4me5(a7HGczvUPoXa4ofKzUbahT4hHdEiXs7ZfF6HYvf0vAj7DKFhFh4WzBVdpTB5Tq4DBdeVA2YBrynB8DeYATTP3tmNIOVHrorZCCc(ncZlmdomNdQhCmCWiVClglew0oQzXzTDWx0Q7TWkqZc2BKa5m9Zxkn7WO3ZAXt9HdEFYjM4ab0Sgd07Y)IXyz(RO)Nq5x9(uU3r2XrHj(a1skEhdGoIrZj1dNLQrTM1HaiQ574ObaZaW1bDhTNxaohHzLaYV01(4FZofDyqacZWnp(36dbGNdmqXPbUX4yIYL9jNPqkHqOR93J0Mo3gqJr18G3)0Ypxwv8dF62YILlisF(GtDdHadbOq7))k7kyNgggg6xeslPPPR9mWjaXHjUIkSkXK6ArRdTT)EE(5M2KoHe78wswStT97Lv)AoeXtN1rLsO0OsXHMRLJAKRBSyjEfs3nrQnNu2tdG0A3R722SFhBV7Ko3ew6D0cf1sFVMOFBrXQSsK0pF1AcEptzQEAlmPgLfv9Fx)jICeUDhXkm91CvIyoZYrJ)nPn5ifwxm))jy1iMRLhcuWrZeCRVhfAt4xPWrw)5hrcwr1p)38(tDCPCQVsgbOK2Vf00Ns9pCAIuv4qEi)A5mt6TuKU1t6N9J19P1)nw0N0H)JCFB(co6oakN9h5a3nzR988s2QCtbRxDHf(jCcCGQ8VI6)fyvcVPLZNn0YLNPD()CrbyHUk(acDRWceNX97gendgX1vF)mBowJ1yqMGCyPYNG(bKsXANJPIPXRU0znJ3PgYUdlm3lhAAFDbpWxZmSP6C3DoRoAhEEQ0JsELjnOjp508Pk1IdF4nn16FaTQG61GAMinnERxk85MNlT6G332FyFD3XNBeApuxLa(8XDTisHe5P9n)gwoQOmTkEyVa65J6wj5WdNfKOdNQVqpr)phb8tndlBg6B7pHr)7" }, } EllesmereUI.WEEKLY_SPOTLIGHT = nil -- { name = "...", description = "...", exportString = "!EUI_..." } diff --git a/EllesmereUI_Widgets.lua b/EllesmereUI_Widgets.lua index 4ecc74d0..0a637e31 100644 --- a/EllesmereUI_Widgets.lua +++ b/EllesmereUI_Widgets.lua @@ -1731,6 +1731,22 @@ local function BuildDropdownControl(parent, ddW, fLevel, values, order, getValue ddBtn._ddRefresh = refresh WireDropdownScripts(ddBtn, ddLbl, ddBg, ddBrd, menu, refresh, RD_DD_COLOURS) end + -- Public hook: invalidate the cached menu so the next click rebuilds + -- from the current contents of `order` / `values`. Use this when the + -- dropdown's options can change at runtime (e.g. a dropdown listing + -- the spells currently on a CDM bar). + ddBtn._invalidateMenu = function() + if menu then + menu:Hide() + if menu.SetParent then pcall(menu.SetParent, menu, nil) end + menu = nil + refresh = nil + ddBtn._ddMenu = nil + ddBtn._ddRefresh = nil + end + -- Also refresh the label text in case the current selection's label changed + ddLbl:SetText(DDResolveLabel(values, order, getValue())) + end -- Lightweight hover scripts (before menu is created). -- Once EnsureMenu() runs, WireDropdownScripts replaces these with 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 14/16] 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 00000000..b043e60b --- /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 02013c3d..b95a3de8 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 f8a64d86..294aa699 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 00000000..b1b93ab9 --- /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 15/16] 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 b043e60b..8c465378 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 294aa699..ffd0c4fe 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 From 69bfb9dff12e2141346fa16d918dbf94959f4626 Mon Sep 17 00:00:00 2001 From: Neil U <156554816+Kneeull@users.noreply.github.com> Date: Mon, 13 Apr 2026 00:25:48 +0100 Subject: [PATCH 16/16] Enhance dungeon completion tracking Update dungeon completion logic to track if the last dungeon was completed. Prior if a last boss was defeated and completion happened at the same time, we'd miss the run complete. Added logic to handle this and to handle if trash is done after final boss to avoid false positives on best runs. --- .../EllesmereUIMythicTimer.lua | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua b/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua index ffd0c4fe..41819f40 100644 --- a/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua +++ b/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua @@ -698,6 +698,7 @@ local function StartRun() currentRun.affixes = affixes or {} currentRun.preciseStart = GetTimePreciseSec and GetTimePreciseSec() or nil currentRun.preciseCompletedElapsed = nil + currentRun._lastDungeonComplete = false wipe(currentRun.objectives) if updateTicker then updateTicker:Cancel() end @@ -737,6 +738,7 @@ local function ResetRun() currentRun.deathTimeLost = 0 currentRun.preciseStart = nil currentRun.preciseCompletedElapsed = nil + currentRun._lastDungeonComplete = false wipe(currentRun.affixes) wipe(currentRun.objectives) if db and db.profile then db.profile._activeRunSplits = nil end @@ -1614,22 +1616,27 @@ local function ApplyStandalonePosition() end end -local function ArePrimaryObjectivesComplete() +-- True only when every scenario objective is complete: all bosses AND the +-- weighted Enemy Forces bar. This is the real "dungeon finished" condition. +-- Using just primary objectives is wrong — killing the last boss before +-- trash reaches 100% does not end the run in WoW, and saving at that moment +-- records an artificially short time. +local function IsDungeonComplete() local numCriteria = select(3, C_Scenario.GetStepInfo()) or 0 if numCriteria == 0 then return false end - local seenPrimary = false + local seenAny = false for i = 1, numCriteria do local info = C_ScenarioInfo.GetCriteriaInfo(i) - if info and not info.isWeightedProgress then - seenPrimary = true + if info then + seenAny = true if not info.completed then return false end end end - return seenPrimary + return seenAny end local runtimeFrame = CreateFrame("Frame") @@ -1664,11 +1671,27 @@ local function RuntimeOnUpdate(_, elapsed) if activeMapID then if not currentRun.active and not currentRun.completed then StartRun() - elseif currentRun.active and ArePrimaryObjectivesComplete() then + elseif currentRun.active and IsDungeonComplete() then + -- Every objective done (bosses + Enemy Forces). Blizzard is about + -- to clear the challenge map, so save now with the accurate time. CompleteRun() end + -- Cache completion state while scenario APIs still answer. Used below + -- to salvage a run if the challenge map clears between polls. + if currentRun.active then + currentRun._lastDungeonComplete = IsDungeonComplete() + end elseif currentRun.active or currentRun.completed then - ResetRun() + -- Challenge map is gone. If we saw the dungeon complete on the last + -- poll, or Blizzard ended the map while the run was active (which + -- itself implies completion — you can't abandon an M+ without the + -- map clearing via completion or timer-out), salvage it as a + -- completion rather than discarding. + if currentRun.active and currentRun._lastDungeonComplete then + CompleteRun() + else + ResetRun() + end end end