diff --git a/.gitignore b/.gitignore index a829a94..c9cf77a 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 8e4f75f..276117d 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 9c48a6d..b3a85ff 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/EUI__General_Options.lua b/EUI__General_Options.lua index 7ecab16..693fde8 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,1197 @@ initFrame:SetScript("OnEvent", function(self) end } ); y = y - h + --------------------------------------------------------------------------- + -- CHARACTER PANEL CUSTOMIZATIONS + --------------------------------------------------------------------------- + _, h = W:SectionHeader(parent, "CHARACTER PANEL CUSTOMIZATIONS", y); y = y - h + + 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() + return EllesmereUIDB and EllesmereUIDB.themedCharacterSheet or false + end, + 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", + 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="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 } + ); y = y - h + + -- 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 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) + attrBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + EllesmereUI.RegisterWidgetRefresh(function() + if themedOff() then + attrBlock:Show() + themedCharacterSheetRow._rightRegion:SetAlpha(0.3) + else + attrBlock:Hide() + themedCharacterSheetRow._rightRegion:SetAlpha(1) + end + end) + if themedOff() then attrBlock:Show() themedCharacterSheetRow._rightRegion:SetAlpha(0.3) else attrBlock:Hide() themedCharacterSheetRow._rightRegion:SetAlpha(1) end + end + + -- 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 + + -- 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 + 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", attrCogBtn, "LEFT", -9, 0) + rightRgn._lastInline = attrSwatch + + EllesmereUI.RegisterWidgetRefresh(function() + local customEnabled = EllesmereUIDB and EllesmereUIDB.statCategoryUseColor and EllesmereUIDB.statCategoryUseColor.Attributes + if themedOff() then + attrCogBtn:SetAlpha(0.15) + attrCogBlock:Show() + attrSwatch:SetAlpha(0.15) + attrSwatch:EnableMouse(false) + else + attrCogBtn:SetAlpha(0.4) + attrCogBlock:Hide() + attrSwatch:SetAlpha(customEnabled and 1 or 0.3) + attrSwatch:EnableMouse(customEnabled) + end + attrUpdateSwatch() + 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 + 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="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 mythicRatingRow when themed is off + do + local function themedOff() + return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) + end + + 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) + mythicBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + EllesmereUI.RegisterWidgetRefresh(function() + if themedOff() then + mythicBlock:Show() + mythicRatingRow:SetAlpha(0.3) + else + mythicBlock:Hide() + mythicRatingRow:SetAlpha(1) + end + end) + 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", + tooltip="Toggle visibility of item level text on the character sheet.", + getValue=function() + return EllesmereUIDB and EllesmereUIDB.showItemLevel ~= false + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.showItemLevel = v + if EllesmereUI._refreshItemLevelVisibility then + EllesmereUI._refreshItemLevelVisibility() + end + end }, + { 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 + + -- Disabled overlay for itemLevelRow when themed is off + do + local function themedOff() + return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) + 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) + + EllesmereUI.RegisterWidgetRefresh(function() + if themedOff() then + itemLevelBlock:Show() + itemLevelRow:SetAlpha(0.3) + else + itemLevelBlock:Hide() + itemLevelRow:SetAlpha(1) + end + end) + if themedOff() then itemLevelBlock:Show() itemLevelRow:SetAlpha(0.3) else itemLevelBlock:Hide() itemLevelRow:SetAlpha(1) end + end + + -- 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 + -- 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 + 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", crestsCogBtn, "LEFT", -9, 0) + rightRgn._lastInline = crestsSwatch + + EllesmereUI.RegisterWidgetRefresh(function() + local customEnabled = EllesmereUIDB and EllesmereUIDB.statCategoryUseColor and EllesmereUIDB.statCategoryUseColor.Crests + if themedOff() then + crestsCogBtn:SetAlpha(0.15) + crestsCogBlock:Show() + crestsSwatch:SetAlpha(0.15) + crestsSwatch:EnableMouse(false) + else + crestsCogBtn:SetAlpha(0.4) + crestsCogBlock:Hide() + crestsSwatch:SetAlpha(customEnabled and 1 or 0.3) + crestsSwatch:EnableMouse(customEnabled) + end + crestsUpdateSwatch() + 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 + upgradeTrackRow, h = W:DualRow(parent, y, + { type="toggle", text="Upgradetrack", + tooltip="Toggle visibility of upgrade track text on the character sheet.", + getValue=function() + return EllesmereUIDB and EllesmereUIDB.showUpgradeTrack ~= false + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.showUpgradeTrack = v + if EllesmereUI._refreshUpgradeTrackVisibility then + EllesmereUI._refreshUpgradeTrackVisibility() + 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 - h + + -- Disabled overlay for upgradeTrackRow when themed is off + do + local function themedOff() + return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) + end + + 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 = "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) + 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 }, + { 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 }, + }, + }) + + 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) + 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) + 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 + + -- 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["Secondary Stats"] + if c then return c.r, c.g, c.b, 1 end + 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["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", secondaryCogBtn, "LEFT", -9, 0) + rightRgn._lastInline = secondarySwatch + + EllesmereUI.RegisterWidgetRefresh(function() + local customEnabled = EllesmereUIDB and EllesmereUIDB.statCategoryUseColor and EllesmereUIDB.statCategoryUseColor["Secondary Stats"] + if themedOff() then + secondaryCogBtn:SetAlpha(0.15) + secondaryCogBlock:Show() + secondarySwatch:SetAlpha(0.15) + secondarySwatch:EnableMouse(false) + else + secondaryCogBtn:SetAlpha(0.4) + secondaryCogBlock:Hide() + secondarySwatch:SetAlpha(customEnabled and 1 or 0.3) + secondarySwatch:EnableMouse(customEnabled) + end + secondaryUpdateSwatch() + 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) + do + local function themedOff() + return not (EllesmereUIDB and EllesmereUIDB.themedCharacterSheet) + end + + -- 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.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 }, + { 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 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) + + 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 + 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 + itemLevelUpdateSwatch() + end) + 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 + end + + 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.showEnchants ~= false + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.showEnchants = v + if EllesmereUI._refreshEnchantsVisibility then + EllesmereUI._refreshEnchantsVisibility() + 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 - h + + -- Disabled overlay for enchantRow 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 + + -- 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 + 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", defenseCogBtn, "LEFT", -9, 0) + rightRgn._lastInline = defenseSwatch + + EllesmereUI.RegisterWidgetRefresh(function() + local customEnabled = EllesmereUIDB and EllesmereUIDB.statCategoryUseColor and EllesmereUIDB.statCategoryUseColor.Defense + if themedOff() then + defenseCogBtn:SetAlpha(0.15) + defenseCogBlock:Show() + defenseSwatch:SetAlpha(0.15) + defenseSwatch:EnableMouse(false) + else + defenseCogBtn:SetAlpha(0.4) + defenseCogBlock:Hide() + defenseSwatch:SetAlpha(customEnabled and 1 or 0.3) + defenseSwatch:EnableMouse(customEnabled) + end + defenseUpdateSwatch() + 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 + 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 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) + + -- 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 + 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 + enchantUpdateSwatch() + 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 @@ -4957,6 +6160,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.lua b/EllesmereUI.lua index 0551a6f..1ce40c5 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 }, } @@ -887,14 +888,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 @@ -5575,6 +5573,7 @@ function EllesmereUI:RegisterModule(folderName, config) EllesmereUIRaidFrames = true, EllesmereUIResourceBars = true, EllesmereUIUnitFrames = true, + EllesmereUIMythicTimer = true, } if not ALLOWED[callerFolder] then return end end @@ -6199,7 +6198,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 +6370,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 +7369,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 +7393,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 +7431,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 ee096aa..3f5be13 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 @@ -35,3 +35,4 @@ EUI__General_Options.lua EUI_UnlockMode.lua EUI_PartyMode_Options.lua EllesmereUI_Glows.lua +charactersheet.lua diff --git a/EllesmereUIActionBars/EllesmereUIActionBars.lua b/EllesmereUIActionBars/EllesmereUIActionBars.lua index ad63846..b3705ec 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 f94edc5..497ab47 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 5c6fb42..5906203 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 511b847..c873469 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 6151ebb..f0bb20a 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 e0c0adc..10d80b9 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 7074e8a..aab5ffc 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 b411403..cb89d2e 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 6fa37db..c9cc286 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 7d8857c..0ed3a50 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 fead3b4..52abc41 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 5db817b..6d57705 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 6818b99..39b29bc 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/EllesmereUIMythicTimer/EUI_MythicTimer_BestRuns.lua b/EllesmereUIMythicTimer/EUI_MythicTimer_BestRuns.lua new file mode 100644 index 0000000..8c46537 --- /dev/null +++ b/EllesmereUIMythicTimer/EUI_MythicTimer_BestRuns.lua @@ -0,0 +1,461 @@ +------------------------------------------------------------------------------- +-- 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 + + -- 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 new file mode 100644 index 0000000..b95a3de --- /dev/null +++ b/EllesmereUIMythicTimer/EUI_MythicTimer_Options.lua @@ -0,0 +1,714 @@ +------------------------------------------------------------------------------- +-- 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") +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 + 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() + if _G._EMT_Apply then _G._EMT_Apply() end + if EllesmereUI.RefreshPage then EllesmereUI:RefreshPage() end + end + + 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 + + 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 ────────────────────────────────────────────────────── + _, 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="dropdown", text="Preset", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + 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", + 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 + + local fontValues, fontOrder = {}, {} + if _G._EMT_GetFontOptions then + fontValues, fontOrder = _G._EMT_GetFontOptions() + end + + 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="dropdown", text="Font", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + 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 + + 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="Show Timer Bar", + disabled=function() return Cfg("enabled") == false end, + disabledTooltip="Module is disabled", + 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("showTimerBreakdown") == true end, + setValue=function(v) Set("showTimerBreakdown", v); Refresh() end }) + y = y - h + + 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, + { 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 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 }) + 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="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, + values=alignValues, + order=alignOrder, + getValue=function() return Cfg("deathAlign") or "LEFT" end, + setValue=function(v) Set("deathAlign", 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="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="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 + 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, 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 new file mode 100644 index 0000000..41819f4 --- /dev/null +++ b/EllesmereUIMythicTimer/EllesmereUIMythicTimer.lua @@ -0,0 +1,1805 @@ +------------------------------------------------------------------------------- +-- EllesmereUIMythicTimer.lua — M+ Timer overlay for EllesmereUI +------------------------------------------------------------------------------- +local ADDON_NAME, ns = ... +local EMT = EllesmereUI.Lite.NewAddon(ADDON_NAME) + +-- 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 +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 + +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, + showPlusThreeTimer = true, + showPlusTwoBar = true, + showPlusThreeBar = true, + showDeaths = true, + showObjectives = true, + showObjectiveTimes = true, + showEnemyBar = true, + showEnemyText = true, + objectiveAlign = "LEFT", + timerAlign = "CENTER", + 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 +local updateTicker +local currentRun = { + active = false, + mapID = nil, + mapName = "", + level = 0, + affixes = {}, + maxTime = 0, + elapsed = 0, + completed = false, + deaths = 0, + deathTimeLost = 0, + objectives = {}, +} + +-- Helpers +local function FormatTime(seconds, withMilliseconds) + if not seconds or seconds < 0 then seconds = 0 end + 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 + +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 + + 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, + rawQuantity = 0, + rawTotalQuantity = 0, + percent = 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 + -- 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 + -- Normalize weighted progress to a 0-100 percent value. + 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 + 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.percent = 100 + if obj.rawTotalQuantity and obj.rawTotalQuantity > 0 then + obj.rawQuantity = obj.rawTotalQuantity + end + end + else + obj.isWeighted = false + obj.percent = 0 + 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 + +-- Coalesced refresh +local _refreshTimer +local function NotifyRefresh() + 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) +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 / restore Blizzard M+ frames +local _blizzHiddenParent +local _blizzOrigScenarioParent +local _blizzOrigObjectiveTrackerParent + +local function SuppressBlizzardMPlus() + if not db or not db.profile.enabled then return end + + if not _blizzHiddenParent then + _blizzHiddenParent = CreateFrame("Frame") + _blizzHiddenParent:Hide() + end + + local sbf = _G.ScenarioBlocksFrame + if sbf and sbf:GetParent() ~= _blizzHiddenParent then + _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() + local sbf = _G.ScenarioBlocksFrame + 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 + +-- 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 {} + currentRun.preciseStart = GetTimePreciseSec and GetTimePreciseSec() or nil + currentRun.preciseCompletedElapsed = nil + currentRun._lastDungeonComplete = false + 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 + 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 + +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 + 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 + + 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 +local PREVIEW_RUN = { + active = true, + completed = false, + mapID = 2648, + mapName = "The Rookery", + level = 12, + maxTime = 1920, + elapsed = 1380, + 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, 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 }, + }, +} + +_G._EMT_Apply = function() + if _G._EMT_StandaloneRefresh then _G._EMT_StandaloneRefresh() end +end + +_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 + +-- 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 + +-- 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 "" +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() + 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 + +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 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 f = CreateFrame("Frame", "EllesmereUIMythicTimerStandalone", UIParent, "BackdropTemplate") + f:SetSize(260, 200) + f:SetPoint("TOPLEFT", UIParent, "CENTER", -130, 100) + f:SetFrameStrata("MEDIUM") + f:SetFrameLevel(10) + f:SetClampedToScreen(true) + + 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) + + 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) + + f._titleFS = f:CreateFontString(nil, "OVERLAY") + f._titleFS:SetWordWrap(false) + f._titleFS:SetJustifyV("MIDDLE") + + f._affixFS = f:CreateFontString(nil, "OVERLAY") + f._affixFS:SetWordWrap(true) + f._affixIconsAnchor = CreateFrame("Frame", nil, f) + f._affixIconsAnchor:SetSize(1, 16) + + f._timerFS = f:CreateFontString(nil, "OVERLAY") + f._timerFS:SetJustifyH("CENTER") + 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") + f._threshFS = f:CreateFontString(nil, "OVERLAY") + f._threshFS:SetWordWrap(false) + f._deathFS = f:CreateFontString(nil, "OVERLAY") + f._deathFS:SetWordWrap(false) + f._enemyFS = f:CreateFontString(nil, "OVERLAY") + f._enemyFS:SetWordWrap(false) + f._enemyBarBg = f:CreateTexture(nil, "BACKGROUND", nil, 1) + f._enemyBarFill = f:CreateTexture(nil, "ARTWORK") + f._previewFS = f:CreateFontString(nil, "OVERLAY") + f._previewFS:SetWordWrap(false) + + -- 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 + +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 + local TBAR_PAD = 10 + 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) + + local scale = p.scale or 1.0 + f:SetScale(scale) + 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)) + + 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 + + local function ContentPad(align) + if align == "LEFT" or align == "RIGHT" then return PAD + ALIGN_PAD end + return PAD + end + + -- 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 + 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 = {} + 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 + affixIDs[#affixIDs + 1] = id + end + end + end + 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) + 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 + + 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 + if p.showDeaths and run.deaths > 0 and not p.deathsInTitle then + local deathAlign = p.deathAlign or "LEFT" + local dPad = ContentPad(deathAlign) + SetFS(f._deathFS, 10) + ApplyShadow(f._deathFS) + 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(deathAlign) + f._deathFS:Show() + y = y - (f._deathFS:GetStringHeight() or 12) - ROW_GAP + else + f._deathFS:Hide() + end + + -- Timer colours + local elapsed = run.elapsed or 0 + local maxTime = run.maxTime or 0 + local timeLeft = max(0, maxTime - elapsed) + 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(completedElapsed, p.showCompletedMilliseconds ~= false) + 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 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 + 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 + 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 + end + if p.showPlusTwoTimer then + local diff = plusTwoT - elapsed + if diff >= 0 then + 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 + 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(p.timerAlign or "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 + 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" + local showEnemyText = p.showEnemyText ~= false + + 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(GetColor(p.objectiveCompletedColor, 0.3, 0.8, 0.3)) + else + f._enemyFS:SetTextColor(GetColor(p.objectiveTextColor, 0.9, 0.9, 0.9)) + end + f._enemyFS:SetText(label) + + local function RenderEnemyBar() + 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() + 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 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, ENEMY_BAR_H) + f._enemyBarFill:SetColorTexture(eR, eG, eB, 0.8) + f._enemyBarFill:Show() + + 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) + 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) + 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() + 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 - ENEMY_BAR_H - ROW_GAP + 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) + f._enemyFS:SetJustifyH(objAlign) + f._enemyFS:Show() + y = y - (f._enemyFS:GetStringHeight() or 12) - 4 + end + + if underBarMode then + RenderEnemyBar() + RenderEnemyLabel() + else + RenderEnemyLabel() + RenderEnemyBar() + end + end + + -- Timer text + 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 + + 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 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() + 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 = 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(timerBarR, timerBarG, timerBarB, 0.85) + f._barFill:Show() + + -- +3 marker + f._seg3:ClearAllPoints() + f._seg3:SetSize(1, TBAR_H + 4) + 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 + f._seg2:ClearAllPoints() + f._seg2:SetSize(1, TBAR_H + 4) + 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 + + 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 + + if underBarMode then + RenderEnemyForces() + end + + 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(GetColor(p.objectiveCompletedColor, 0.3, 0.8, 0.3)) + else + row:SetTextColor(GetColor(p.objectiveTextColor, 0.9, 0.9, 0.9)) + end + local timeStr = "" + 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: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) - OBJ_GAP + end + end + end + + for i = objIdx + 1, #objRows do + objRows[i]:Hide() + end + + if not underBarMode then + RenderEnemyForces() + end + + local totalH = abs(y) + PAD + f:SetHeight(totalH) + + 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 + +_G._EMT_StandaloneRefresh = RenderStandalone +_G._EMT_GetStandaloneFrame = function() + return CreateStandaloneFrame() +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() + standaloneFrame:SetPoint(pos.point, UIParent, pos.relPoint, pos.x, pos.y) + end +end + +-- 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 seenAny = false + for i = 1, numCriteria do + local info = C_ScenarioInfo.GetCriteriaInfo(i) + if info then + seenAny = true + if not info.completed then + return false + end + end + end + + return seenAny +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 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 + -- 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 + +function EMT:OnInitialize() + db = EllesmereUI.Lite.NewDB("EllesmereUIMythicTimerDB", DB_DEFAULTS) + _G._EMT_AceDB = db + + 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 + + -- 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 + + local validMapIDs = {} + for _, mapID in ipairs(currentMaps) do + validMapIDs[mapID] = true + end + + local purged = false + + 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 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) + + runtimeFrame:SetScript("OnUpdate", RuntimeOnUpdate) +end + +function EMT:OnEnable() + if not db or not db.profile.enabled then return end + + 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) + -- 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() + 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:SetScale(db.profile.scale or 1.0) + standaloneFrame:ClearAllPoints() + standaloneFrame:SetPoint(pos.point, UIParent, pos.relPoint, pos.x, pos.y) + end + end, + }), + }) + end +end + diff --git a/EllesmereUIMythicTimer/EllesmereUIMythicTimer.toc b/EllesmereUIMythicTimer/EllesmereUIMythicTimer.toc new file mode 100644 index 0000000..b1b93ab --- /dev/null +++ b/EllesmereUIMythicTimer/EllesmereUIMythicTimer.toc @@ -0,0 +1,19 @@ +## Interface: 120000, 120001 +## Title: |cff0cd29fEllesmereUI|r Mythic+ Timer +## Category: |cff0cd29fEllesmere|rUI +## Group: EllesmereUI +## Notes: Customizable Mythic+ dungeon timer with objective tracking +## Author: Ellesmere +## Version: 6.4.7 +## Dependencies: EllesmereUI +## SavedVariables: EllesmereUIMythicTimerDB +## IconTexture: Interface\AddOns\EllesmereUI\media\eg-logo.tga + +# Main Lua +EllesmereUIMythicTimer.lua + +# Best Runs Viewer +EUI_MythicTimer_BestRuns.lua + +# Options +EUI_MythicTimer_Options.lua diff --git a/EllesmereUINameplates/EllesmereUINameplates.toc b/EllesmereUINameplates/EllesmereUINameplates.toc index 2796ced..f251d23 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 d9e078b..344aeff 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 1b202af..81a0c1b 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 eea3b83..ebfee4a 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 3689585..6e64cc6 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 7330329..3054680 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 4ecc74d..0a637e3 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 diff --git a/charactersheet.lua b/charactersheet.lua new file mode 100644 index 0000000..fa48730 --- /dev/null +++ b/charactersheet.lua @@ -0,0 +1,3673 @@ +-------------------------------------------------------------------------------- +-- 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(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 + 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 (skip in combat) + hooksecurefunc(frame, "SetWidth", function(self, w) + if w ~= newWidth and not InCombatLockdown() then + self:SetWidth(newWidth) + end + end) + + -- Hook SetHeight to prevent Blizzard from changing it back (skip in combat) + hooksecurefunc(frame, "SetHeight", function(self, h) + if h ~= newHeight and not InCombatLockdown() 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 + if not InCombatLockdown() then + self:SetSize(newWidth, newHeight) + if self._ebsBg then + self._ebsBg:SetSize(newWidth, newHeight) + end + end + hookLock = false + end + end) + + -- Aggressive size enforcement with immediate re-setup + if not frame._sizeCheckDone then + local function EnforceSize() + if frame:IsShown() and not InCombatLockdown() 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 + + -- 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 + 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 + 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 + 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 (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 + SafeShow(btn) + else + btn:Hide() + end + end + end + + -- Show/hide stats panel and titles panel based on tab (deferred in combat) + if frame._statsPanel then + if isCharacterTab then + SafeShow(frame._statsPanel) + else + frame._statsPanel:Hide() + 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() + 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) + 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, 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 + + -- 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() + + -- 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)) + + -- 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 + local iLvlUpdateFrame = CreateFrame("Frame") + iLvlUpdateFrame:SetScript("OnUpdate", function() + UpdateItemLevelDisplay() + -- RefreshAttributeStats will be called later after it's defined + end) + + 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) + 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) + frame._scrollFrame = scrollFrame -- Store on frame for tab visibility control + + -- 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") + frame._scrollBar = scrollBar -- Store on frame for tab visibility control + + -- 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 + [3343] = 700, -- Champion + [3341] = 700, -- Veteran + [3383] = 700, -- Adventurer + } + + -- Helper function to get crest maximum value (now using API to get seasonal max) + local function GetCrestMaxValue(currencyID) + 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 + 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, 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 + + -- 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 = GetCategoryColor("Attributes"), + stats = GetFilteredAttributeStats() + }, + { + title = "Secondary Stats", + 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 }, + { 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 = 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" }, + } + }, + { + title = "Defense", + 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" }, + { 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" }, + } + }, + { + title = "Crests", + 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(3343) end, format = "%d", currencyID = 3343 }, + { name = "Veteran", func = function() return GetCrestValue(3341) end, format = "%d", currencyID = 3341 }, + { name = "Adventurer", func = function() return GetCrestValue(3383) end, format = "%d", currencyID = 3383 }, + } + } + } + + -- 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 + 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) + + -- 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 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 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() + 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" + }) + + -- 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 = 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 + + -- 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 + + -- Forward declare the refresh function (will be defined after buttons) + local RefreshEquipmentSets + + -- 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() + if InCombatLockdown() then return end + + 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.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) + + -- 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) + + local equipTopBg = equipTopBtn:CreateTexture(nil, "BACKGROUND") + equipTopBg:SetColorTexture(0.05, 0.07, 0.08, 1) + equipTopBg:SetAllPoints() + + -- 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 + + 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) + + 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.SetBorderColor(equipTopBtn, 0.8, 0.8, 0.8, 1) + end + 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 + + 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) + + -- 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() + + -- 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.SetBorderColor(saveTopBtn, 0.8, 0.8, 0.8, 1) + end + 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 + + 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, 0.7) -- Zurück zu Standard + end + end) + end) + + -- 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) + + -- 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) + for _, child in ipairs({equipScrollChild:GetChildren()}) do + if child ~= newSetBtn and child ~= equipTopBtn and child ~= saveTopBtn and child ~= setsHeaderFrame then + child:Hide() + end + 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 + + local yOffset = -59 -- After buttons and header + 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) + + -- 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 + 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", -45, 0) + end + end + + + -- Click handler (select set) + setBtn:SetScript("OnClick", function() + selectedSetID = setData.id + RefreshEquipmentSets() + end) + + -- 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 + 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) + + -- 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 + + 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(self, event) + -- Full refresh when sets change + 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) + -- 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) + 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) + + -- Create itemlevel labels + local slot = _G[slotName] + if slot and not slot._itemLevelLabel then + local itemLevelSize = EllesmereUIDB and EllesmereUIDB.charSheetItemLevelSize or 11 + local label = textOverlayFrame: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 = textOverlayFrame: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 = textOverlayFrame: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 + + -- 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 + + 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 + -- 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() + -- 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() + 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 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] + 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 + local slotID = slot:GetID() + local canHaveEnchant = ENCHANT_SLOTS[slotID] + + 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 + -- 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 + + slot._itemLevelLabel:SetTextColor(displayColor.r, displayColor.g, displayColor.b, 0.9) + else + slot._itemLevelLabel:Hide() + end + end + + -- Update enchant label + if slot._enchantLabel then + -- 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 + -- 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 + + -- Monitor and update only when items change (including upgrade level) + 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 + -- Get full item link (includes itemlevel and upgrade info) + local itemLink = GetInventoryItemLink("player", _G[slotName]:GetID()) + + -- Compare full link to detect changes in itemlevel or upgrade track + if itemCache[slotName] ~= itemLink then + itemCache[slotName] = itemLink + 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.5, 0.5, 0.5, 0.7) -- Gray background 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") + local lastSpecIndex = GetSpecialization() + 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 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 + 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 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 + 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 + 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 + 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 + 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 +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) + -- 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 + slot._itemLevelLabel:SetTextColor(1, 1, 1, 0.9) + end + else + slot._itemLevelLabel:SetTextColor(1, 1, 1, 0.9) + end + 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) + -- 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 + + -- 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