From b65ac17a792ba43125705b610535f1d6fa12990d Mon Sep 17 00:00:00 2001 From: Denis Davydov Date: Tue, 27 Jan 2026 23:05:32 +0000 Subject: [PATCH] Add Use Blizzard Bar feature for secondary resource bar Adds option to replace custom secondary resource bar with Blizzard's default bar --- Bars/SecondaryResourceBar.lua | 312 ++++++++++++++++++++++++++++++++-- Constants.lua | 12 ++ Locales/enUS.lua | 2 + Locales/koKR.lua | 2 + Locales/ruRU.lua | 2 + Locales/zhCN.lua | 2 + 6 files changed, 321 insertions(+), 11 deletions(-) diff --git a/Bars/SecondaryResourceBar.lua b/Bars/SecondaryResourceBar.lua index c340185..63355a2 100644 --- a/Bars/SecondaryResourceBar.lua +++ b/Bars/SecondaryResourceBar.lua @@ -230,11 +230,34 @@ function SecondaryResourceBarMixin:ApplyVisibilitySettings(layoutName, inCombat) local data = self:GetData(layoutName) if not data then return end + -- If using Blizzard bar, delegate to UseBlizzardBarMode which handles + -- both compatible and incompatible specs (falling back to custom bar) + if data.useBlizzardBar then + self:UseBlizzardBarMode(layoutName, inCombat) + return + end + self:HideBlizzardSecondaryResource(layoutName, data) addonTable.PowerBarMixin.ApplyVisibilitySettings(self, layoutName, inCombat) end +function SecondaryResourceBarMixin:OnShow() + local data = self:GetData() + + if data and data.positionMode ~= nil and data.positionMode ~= "Self" then + self:ApplyLayout() + end +end + +function SecondaryResourceBarMixin:OnHide() + local data = self:GetData() + + if data and data.positionMode ~= nil and data.positionMode ~= "Self" then + self:ApplyLayout() + end +end + function SecondaryResourceBarMixin:HideBlizzardSecondaryResource(layoutName, data) data = data or self:GetData(layoutName) if not data then return end @@ -243,18 +266,9 @@ function SecondaryResourceBarMixin:HideBlizzardSecondaryResource(layoutName, dat if data.hideBlizzardSecondaryResourceUi == nil or InCombatLockdown() then return end local playerClass = select(2, UnitClass("player")) - local blizzardResourceFrames = { - ["DEATHKNIGHT"] = RuneFrame, - ["DRUID"] = DruidComboPointBarFrame, - ["EVOKER"] = EssencePlayerFrame, - ["MAGE"] = MageArcaneChargesFrame, - ["MONK"] = MonkHarmonyBarFrame, - ["PALADIN"] = PaladinPowerBarFrame, - ["ROGUE"] = RogueComboPointBarFrame, - ["WARLOCK"] = WarlockPowerFrame, - } - for class, f in pairs(blizzardResourceFrames) do + for class, frameData in pairs(addonTable.blizzardResourceFrames) do + local f = frameData.frame if f and playerClass == class then if data.hideBlizzardSecondaryResourceUi == true then if LEM:IsInEditMode() then @@ -271,6 +285,256 @@ function SecondaryResourceBarMixin:HideBlizzardSecondaryResource(layoutName, dat end end +-- Returns the Blizzard frame for the class (regardless of spec compatibility) +function SecondaryResourceBarMixin:GetBlizzardFrameForClass() + local playerClass = select(2, UnitClass("player")) + local frameData = addonTable.blizzardResourceFrames[playerClass] + return frameData and frameData.frame +end + +-- Returns the Blizzard frame only if it matches the current spec's resource +function SecondaryResourceBarMixin:GetBlizzardResourceFrame() + local playerClass = select(2, UnitClass("player")) + local frameData = addonTable.blizzardResourceFrames[playerClass] + if not frameData then + return nil + end + + -- Check if the current spec's resource matches what the Blizzard bar displays + -- If not, return nil to fall back to the custom bar + local currentResource = self:GetResource() + if currentResource ~= frameData.resource then + return nil + end + + return frameData.frame +end + +-- Returns the screen-space offset between the frame's center and the +-- average center of its visible children, plus the ratio of visual content +-- size to frame size. For frames like RogueComboPointBarFrame where the content +-- extends beyond the frame bounds (e.g. 7 combo points in a 5-CP-wide frame), +-- this lets us position and size the placeholder accurately. +-- Returns: offsetX, offsetY, widthRatio, heightRatio +function SecondaryResourceBarMixin:GetBlizzardFrameContentOffset(blizzardFrame) + local frameCX, frameCY = blizzardFrame:GetCenter() + if not frameCX or not frameCY then return 0, 0, 1, 1 end + + local children = {blizzardFrame:GetChildren()} + if #children == 0 then return 0, 0, 1, 1 end + + local sumCX, sumCY, count = 0, 0, 0 + local minLeft, maxRight = math.huge, -math.huge + local minBottom, maxTop = math.huge, -math.huge + + for _, child in ipairs(children) do + if child:IsShown() then + local cx, cy = child:GetCenter() + local left, right = child:GetLeft(), child:GetRight() + local bottom, top = child:GetBottom(), child:GetTop() + if cx and cy and left and right and bottom and top then + sumCX = sumCX + cx + sumCY = sumCY + cy + count = count + 1 + minLeft = math.min(minLeft, left) + maxRight = math.max(maxRight, right) + minBottom = math.min(minBottom, bottom) + maxTop = math.max(maxTop, top) + end + end + end + + if count == 0 then return 0, 0, 1, 1 end + + local offsetX = (sumCX / count) - frameCX + local offsetY = (sumCY / count) - frameCY + + -- Only return offset if significant (avoids floating point noise on centered frames) + if math.abs(offsetX) < 0.5 then offsetX = 0 end + if math.abs(offsetY) < 0.5 then offsetY = 0 end + + -- Compute ratio of visual content size to frame size + local frameLeft, frameRight = blizzardFrame:GetLeft(), blizzardFrame:GetRight() + local frameBottom, frameTop = blizzardFrame:GetBottom(), blizzardFrame:GetTop() + local widthRatio, heightRatio = 1, 1 + + if frameLeft and frameRight and frameBottom and frameTop then + local frameScreenW = frameRight - frameLeft + local frameScreenH = frameTop - frameBottom + if frameScreenW > 0 then + local visualScreenW = maxRight - minLeft + local r = visualScreenW / frameScreenW + if math.abs(r - 1) > 0.05 then widthRatio = r end + end + if frameScreenH > 0 then + local visualScreenH = maxTop - minBottom + local r = visualScreenH / frameScreenH + if math.abs(r - 1) > 0.05 then heightRatio = r end + end + end + + return offsetX, offsetY, widthRatio, heightRatio +end + +-- Determine if bar should be visible based on visibility settings. +-- Uses the parent's ApplyVisibilitySettings to decide, then reads the result +-- from self.Frame:IsShown(). This avoids duplicating the parent's visibility logic. +function SecondaryResourceBarMixin:ShouldBeVisible(layoutName, inCombat) + if LEM:IsInEditMode() then + return true + end + + addonTable.PowerBarMixin.ApplyVisibilitySettings(self, layoutName, inCombat) + local visible = self.Frame:IsShown() + self.Frame:Hide() + return visible +end + +function SecondaryResourceBarMixin:UseBlizzardBarMode(layoutName, inCombat) + local data = self:GetData(layoutName) + if not data then return end + + local blizzardFrame = self:GetBlizzardResourceFrame() + if not blizzardFrame then + -- No compatible Blizzard bar for current spec, fall back to custom bar + -- Hide the class's Blizzard bar if it exists (e.g., Chi bar for Brewmaster) + local classBlizzardFrame = self:GetBlizzardFrameForClass() + if classBlizzardFrame and data.useBlizzardBar and not InCombatLockdown() then + classBlizzardFrame:Hide() + end + -- Use the custom bar as if useBlizzardBar wasn't enabled + self.Frame:Show() + self.Frame:SetAlpha(1.0) + addonTable.PowerBarMixin.ApplyLayout(self, layoutName) + addonTable.PowerBarMixin.ApplyVisibilitySettings(self, layoutName, inCombat) + self:UpdateDisplay(true) + return + end + + if data.useBlizzardBar then + -- One-time setup: intercept SetPoint and ClearAllPoints so that Blizzard's + -- layout system and other addons cannot move the frame while active. + -- Intercepts are installed once and never removed; behavior is controlled + -- by _useBlizzardBarActive (block/passthrough) and _allowBlizzardFramePositioning. + if not self._blizzardFrameIntercepted then + self._blizzardFrameIntercepted = true + + local originalSetPoint = blizzardFrame.SetPoint + local originalClearAllPoints = blizzardFrame.ClearAllPoints + blizzardFrame.SetPoint = function(frame, ...) + if not self._useBlizzardBarActive or self._allowBlizzardFramePositioning then + originalSetPoint(frame, ...) + end + end + blizzardFrame.ClearAllPoints = function(frame, ...) + if not self._useBlizzardBarActive or self._allowBlizzardFramePositioning then + originalClearAllPoints(frame, ...) + end + end + + -- Reparent to UIParent so the frame renders independently of + -- PlayerFrame (which addons like ElvUI may hide or reparent) + self._originalParent = blizzardFrame:GetParent() + + -- OnShow hook: re-apply positioning when the frame is re-shown + -- (e.g. after Toggle User Interface with Alt+Z) + blizzardFrame:HookScript("OnShow", function() + if self._useBlizzardBarActive then + self:UseBlizzardBarMode() + end + end) + end + + self._useBlizzardBarActive = true + blizzardFrame:SetParent(UIParent) + + local shouldBeVisible = self:ShouldBeVisible(layoutName, inCombat) + + -- Show the Blizzard frame and use SetAlpha for visibility control + blizzardFrame:Show() + blizzardFrame:SetAlpha(shouldBeVisible and 1 or 0) + + -- Get the saved position + local point, relativeFrame, relativePoint, x, y = self:GetPoint(layoutName, true) + + -- Apply scale + local scale = data.scale or self.defaults.scale or 1 + blizzardFrame:SetScale(scale) + + -- Compute content offset and size ratio for frames where visual content + -- isn't centered or extends beyond the frame (e.g. Rogue combo points with talents) + local contentOffsetX, contentOffsetY, widthRatio, heightRatio = self:GetBlizzardFrameContentOffset(blizzardFrame) + + -- Position the Blizzard frame using the original methods via our flag + self._allowBlizzardFramePositioning = true + + -- Size placeholder to match Blizzard bar's visual content + local blizzardWidth, blizzardHeight = blizzardFrame:GetSize() + if blizzardWidth and blizzardHeight and blizzardWidth > 0 and blizzardHeight > 0 then + self.Frame:SetSize(blizzardWidth * scale * widthRatio, blizzardHeight * scale * heightRatio) + end + + -- Position placeholder at the saved location using the user's anchor point + self.Frame:ClearAllPoints() + self.Frame:SetPoint(point, relativeFrame, relativePoint, x, y) + + -- Anchor Blizzard bar CENTER-to-CENTER with the placeholder, compensating + -- for content offset so the visual content aligns with the placeholder. + -- Using the placeholder as an anchor in both modes ensures Edit Mode and + -- normal mode always produce the same position regardless of anchor point. + blizzardFrame:ClearAllPoints() + blizzardFrame:SetPoint("CENTER", self.Frame, "CENTER", -contentOffsetX, -contentOffsetY) + + if LEM:IsInEditMode() then + -- In Edit Mode, show the placeholder for drag-and-drop positioning + self.Frame:Show() + self.Frame:SetAlpha(0.5) + self.TextFrame:Hide() + else + -- In normal mode, hide the placeholder (it still serves as a position anchor) + self.Frame:Hide() + end + + self._allowBlizzardFramePositioning = false + + if blizzardFrame.SetIgnoreParentScale then + blizzardFrame:SetIgnoreParentScale(false) + end + else + -- Restore Blizzard bar to default behavior + self._useBlizzardBarActive = false + + if self._blizzardFrameIntercepted and self._originalParent then + blizzardFrame:SetParent(self._originalParent) + end + + blizzardFrame:ClearAllPoints() + blizzardFrame:SetPoint("TOP", PlayerFrame, "BOTTOM", 0, 16) + blizzardFrame:SetScale(1) + + self:HideBlizzardSecondaryResource(layoutName) + + -- Show our custom bar + self.Frame:Show() + self.Frame:SetAlpha(1.0) + self:ApplyVisibilitySettings(layoutName) + self:ApplyLayout(layoutName) + end +end + +function SecondaryResourceBarMixin:ApplyLayout(layoutName, force) + local data = self:GetData(layoutName) + + -- If using Blizzard bar, delegate to UseBlizzardBarMode which handles + -- both compatible and incompatible specs (falling back to custom bar) + if data and data.useBlizzardBar then + self:UseBlizzardBarMode(layoutName) + return + end + + addonTable.PowerBarMixin.ApplyLayout(self, layoutName, force) +end + addonTable.SecondaryResourceBarMixin = SecondaryResourceBarMixin addonTable.RegisteredBar = addonTable.RegisteredBar or {} @@ -285,6 +549,7 @@ addonTable.RegisteredBar.SecondaryResourceBar = { x = 0, y = -40, hideBlizzardSecondaryResourceUi = false, + useBlizzardBar = false, hideManaOnRole = {}, showManaAsPercent = false, showTicks = true, @@ -334,6 +599,31 @@ addonTable.RegisteredBar.SecondaryResourceBar = { bar:HideBlizzardSecondaryResource(layoutName) end, tooltip = L["HIDE_BLIZZARD_UI_SECONDARY_POWER_BAR_TOOLTIP"], + isEnabled = function(layoutName) + local data = SenseiClassResourceBarDB[dbName][layoutName] + return not (data and data.useBlizzardBar) + end, + }, + { + parentId = L["CATEGORY_BAR_VISIBILITY"], + order = 106, + name = L["USE_BLIZZARD_BAR"], + kind = LEM.SettingType.Checkbox, + default = defaults.useBlizzardBar, + get = function(layoutName) + local data = SenseiClassResourceBarDB[dbName][layoutName] + if data and data.useBlizzardBar ~= nil then + return data.useBlizzardBar + else + return defaults.useBlizzardBar + end + end, + set = function(layoutName, value) + SenseiClassResourceBarDB[dbName][layoutName] = SenseiClassResourceBarDB[dbName][layoutName] or CopyTable(defaults) + SenseiClassResourceBarDB[dbName][layoutName].useBlizzardBar = value + bar:UseBlizzardBarMode(layoutName) + end, + tooltip = L["USE_BLIZZARD_BAR_TOOLTIP"], }, { parentId = L["CATEGORY_BAR_SETTINGS"], diff --git a/Constants.lua b/Constants.lua index db0001a..fede57b 100644 --- a/Constants.lua +++ b/Constants.lua @@ -351,6 +351,18 @@ addonTable.tickedPowerTypes = { ["WHIRLWIND"] = true, } +-- Map of Blizzard frames and the resource types they display +addonTable.blizzardResourceFrames = { + ["DEATHKNIGHT"] = { frame = RuneFrame, resource = Enum.PowerType.Runes }, + ["DRUID"] = { frame = DruidComboPointBarFrame, resource = Enum.PowerType.ComboPoints }, + ["EVOKER"] = { frame = EssencePlayerFrame, resource = Enum.PowerType.Essence }, + ["MAGE"] = { frame = MageArcaneChargesFrame, resource = Enum.PowerType.ArcaneCharges }, + ["MONK"] = { frame = MonkHarmonyBarFrame, resource = Enum.PowerType.Chi }, + ["PALADIN"] = { frame = PaladinPowerBarFrame, resource = Enum.PowerType.HolyPower }, + ["ROGUE"] = { frame = RogueComboPointBarFrame, resource = Enum.PowerType.ComboPoints }, + ["WARLOCK"] = { frame = WarlockPowerFrame, resource = Enum.PowerType.SoulShards }, +} + -- Power types that are fragmented (multiple independent segments) addonTable.fragmentedPowerTypes = { [Enum.PowerType.ComboPoints] = true, diff --git a/Locales/enUS.lua b/Locales/enUS.lua index 5b48acf..b279040 100644 --- a/Locales/enUS.lua +++ b/Locales/enUS.lua @@ -87,6 +87,8 @@ local baseLocale = { ["HIDE_BLIZZARD_UI_SECONDARY_POWER_BAR_TOOLTIP"] = "Hides the default Blizzard secondary resource UI (e.g. Rune Frame for Death Knights)", ["ENABLE_HP_BAR_MOUSE_INTERACTION"] = "Clickable Health Bar", ["ENABLE_HP_BAR_MOUSE_INTERACTION_TOOLTIP"] = "Enable default Player Frame click behavior on the Health Bar.", + ["USE_BLIZZARD_BAR"] = "Use Blizzard Bar", + ["USE_BLIZZARD_BAR_TOOLTIP"] = "Replace the custom bar with Blizzard's default secondary resource bar, preserving all animations and styling. The bar will be positioned according to your settings.", -- Position & Size category - Edit Mode ["CATEGORY_POSITION_AND_SIZE"] = "Position & Size", diff --git a/Locales/koKR.lua b/Locales/koKR.lua index b6c34bd..95f62d2 100644 --- a/Locales/koKR.lua +++ b/Locales/koKR.lua @@ -84,6 +84,8 @@ local baseLocale = { ["HIDE_BLIZZARD_UI_SECONDARY_POWER_BAR_TOOLTIP"] = "블리자드 기본 보조 자원 UI를 숨깁니다 (예: 죽음의 기사 룬 프레임)", ["ENABLE_HP_BAR_MOUSE_INTERACTION"] = "생명력 바 마우스 상호작용", ["ENABLE_HP_BAR_MOUSE_INTERACTION_TOOLTIP"] = "생명력 바에서 기본 플레이어 프레임 클릭 동작을 활성화합니다.", + ["USE_BLIZZARD_BAR"] = "블리자드 바 사용", + ["USE_BLIZZARD_BAR_TOOLTIP"] = "사용자 정의 바를 블리자드 기본 보조 자원 바로 대체합니다. 모든 애니메이션과 스타일이 유지됩니다. 바는 설정에 따라 배치됩니다.", -- Position & Size category - Edit Mode ["CATEGORY_POSITION_AND_SIZE"] = "위치 및 크기", diff --git a/Locales/ruRU.lua b/Locales/ruRU.lua index 1a6c4ba..d4e0a77 100644 --- a/Locales/ruRU.lua +++ b/Locales/ruRU.lua @@ -87,6 +87,8 @@ local baseLocale = { ["HIDE_BLIZZARD_UI_SECONDARY_POWER_BAR_TOOLTIP"] = "Скрывает стандартный интерфейс вторичных ресурсов (напр. руны ДК)", ["ENABLE_HP_BAR_MOUSE_INTERACTION"] = "Кликабельная панель здоровья", ["ENABLE_HP_BAR_MOUSE_INTERACTION_TOOLTIP"] = "Включает стандартное взаимодействие с рамкой персонажа при нажатии на панель здоровья.", + ["USE_BLIZZARD_BAR"] = "Использовать панель Blizzard", + ["USE_BLIZZARD_BAR_TOOLTIP"] = "Заменяет пользовательскую панель на стандартную панель вторичного ресурса Blizzard, сохраняя все анимации и стили. Панель будет расположена в соответствии с вашими настройками.", -- Position & Size category - Edit Mode ["CATEGORY_POSITION_AND_SIZE"] = "Положение и размер", diff --git a/Locales/zhCN.lua b/Locales/zhCN.lua index 167e54d..0877044 100644 --- a/Locales/zhCN.lua +++ b/Locales/zhCN.lua @@ -87,6 +87,8 @@ local baseLocale = { ["HIDE_BLIZZARD_UI_SECONDARY_POWER_BAR_TOOLTIP"] = "隐藏暴雪自带的次要资源界面(例如死亡骑士的符文条)", ["ENABLE_HP_BAR_MOUSE_INTERACTION"] = "可点击生命条", ["ENABLE_HP_BAR_MOUSE_INTERACTION_TOOLTIP"] = "在生命条上启用玩家框体的默认点击行为。", + ["USE_BLIZZARD_BAR"] = "使用暴雪资源条", + ["USE_BLIZZARD_BAR_TOOLTIP"] = "用暴雪默认的次要资源条替换自定义资源条,保留所有动画和样式。资源条将根据您的设置进行定位。", -- Position & Size category - Edit Mode ["CATEGORY_POSITION_AND_SIZE"] = "位置与大小",