From 4b62c3a087b6647357f155e608290ae96f34de0d Mon Sep 17 00:00:00 2001 From: Marco Date: Mon, 23 Mar 2026 14:54:34 +0100 Subject: [PATCH] Move aura designer to group options --- .../Auras2/MSUF_A2_Units.lua | 54 +- .../Core/MSUF_GroupAuras.lua | 258 +++++++++ .../Core/MSUF_GroupFrames.lua | 519 ++++++++++++++++++ .../Core/MSUF_GroupHide.lua | 106 ++++ .../Core/MSUF_GroupIndicators.lua | 122 ++++ .../Core/MSUF_GroupPrivateAuras.lua | 65 +++ .../Core/MSUF_GroupRange.lua | 61 ++ .../Core/MSUF_GroupRoster.lua | 216 ++++++++ .../Core/MSUF_GroupUpdate.lua | 256 +++++++++ .../EditMode2/MSUF_EM2_Group.lua | 247 +++++++++ .../EditMode2/MSUF_EM2_HUD.lua | 10 +- .../EditMode2/MSUF_EM2_Movers.lua | 13 +- .../EditMode2/MSUF_EM2_Nudge.lua | 51 +- .../EditMode2/MSUF_EM2_Popups.lua | 5 + .../EditMode2/MSUF_EM2_Ticker.lua | 103 ++-- .../MidnightSimpleUnitFrames_SlashMenu.lua | 5 +- .../Foundation/MSUF_Defaults.lua | 58 +- .../MidnightSimpleUnitFrames.toc | 10 + .../Options/MSUF_Options_Auras.lua | 166 +++++- .../Options/MSUF_Options_Group.lua | 466 ++++++++++++++++ 20 files changed, 2730 insertions(+), 61 deletions(-) create mode 100644 MidnightSimpleUnitFrames/Core/MSUF_GroupAuras.lua create mode 100644 MidnightSimpleUnitFrames/Core/MSUF_GroupFrames.lua create mode 100644 MidnightSimpleUnitFrames/Core/MSUF_GroupHide.lua create mode 100644 MidnightSimpleUnitFrames/Core/MSUF_GroupIndicators.lua create mode 100644 MidnightSimpleUnitFrames/Core/MSUF_GroupPrivateAuras.lua create mode 100644 MidnightSimpleUnitFrames/Core/MSUF_GroupRange.lua create mode 100644 MidnightSimpleUnitFrames/Core/MSUF_GroupRoster.lua create mode 100644 MidnightSimpleUnitFrames/Core/MSUF_GroupUpdate.lua create mode 100644 MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_Group.lua create mode 100644 MidnightSimpleUnitFrames/Options/MSUF_Options_Group.lua diff --git a/MidnightSimpleUnitFrames/Auras2/MSUF_A2_Units.lua b/MidnightSimpleUnitFrames/Auras2/MSUF_A2_Units.lua index af01c06..061fc48 100644 --- a/MidnightSimpleUnitFrames/Auras2/MSUF_A2_Units.lua +++ b/MidnightSimpleUnitFrames/Auras2/MSUF_A2_Units.lua @@ -43,17 +43,18 @@ if type(Units.BOSS) ~= "table" then Units.BOSS = t end +if type(Units.GROUP_PARTY) ~= "table" then Units.GROUP_PARTY = { "party1", "party2", "party3", "party4" } end +if type(Units.GROUP_RAID) ~= "table" then + local t = {} + for i = 1, 40 do t[i] = "raid" .. i end + Units.GROUP_RAID = t +end + if type(Units.ALL) ~= "table" then local t = {} local n = 0 - for i = 1, #Units.BASE do - n = n + 1 - t[n] = Units.BASE[i] - end - for i = 1, #Units.BOSS do - n = n + 1 - t[n] = Units.BOSS[i] - end + for i = 1, #Units.BASE do n = n + 1; t[n] = Units.BASE[i] end + for i = 1, #Units.BOSS do n = n + 1; t[n] = Units.BOSS[i] end Units.ALL = t end @@ -65,7 +66,7 @@ end function Units.ForEachAll(fn) if type(fn) ~= "function" then return end - local t = Units.ALL + local t = Units.GetAll() for i = 1, #t do fn(t[i]) end @@ -79,8 +80,39 @@ function Units.ForEachBoss(fn) end end +local function AppendActiveGroupUnits(out) + local groupNS = ns and ns.Group + local roster = groupNS and groupNS.roster + if _G.MSUF_GroupPreviewActive and type(roster) == "table" and type(roster.units) == "table" then + for i = 1, #roster.units do + local unit = roster.units[i] + if unit then + out[#out + 1] = unit + end + end + return + end + + local rosterUnits = groupNS and groupNS.rosterUnits + if type(rosterUnits) == "table" then + for i = 1, #Units.GROUP_PARTY do + local unit = Units.GROUP_PARTY[i] + if rosterUnits[unit] then out[#out + 1] = unit end + end + for i = 1, #Units.GROUP_RAID do + local unit = Units.GROUP_RAID[i] + if rosterUnits[unit] then out[#out + 1] = unit end + end + end +end + -- Optionally expose simple getter. function Units.GetAll() - return Units.ALL + local out = {} + for i = 1, #Units.ALL do out[#out + 1] = Units.ALL[i] end + local db = _G.MSUF_DB and _G.MSUF_DB.group + if db and db.enabled ~= false then + AppendActiveGroupUnits(out) + end + return out end - diff --git a/MidnightSimpleUnitFrames/Core/MSUF_GroupAuras.lua b/MidnightSimpleUnitFrames/Core/MSUF_GroupAuras.lua new file mode 100644 index 0000000..8e57178 --- /dev/null +++ b/MidnightSimpleUnitFrames/Core/MSUF_GroupAuras.lua @@ -0,0 +1,258 @@ +local addonName, ns = ... +ns = ns or {} +ns.Group = ns.Group or {} + +local Group = ns.Group +local pairs = pairs +local wipe = wipe +local C_UnitAuras = C_UnitAuras +local CreateFrame = CreateFrame +local canaccessvalue = _G.canaccessvalue +local issecretvalue = _G.issecretvalue + +local SATED = { + [57723] = true, [57724] = true, [80354] = true, [95809] = true, + [160455] = true, [264689] = true, [390435] = true, [26013] = true, [71041] = true, +} + +local owner = {} +local compactHooked = false +local blizzCache = {} +local designerLookupCache = { + party = { stamp = false, lookup = {} }, + raid = { stamp = false, lookup = {} }, +} + +local function IsAccessible(v) + if canaccessvalue then + return canaccessvalue(v) == true + end + return not (issecretvalue and issecretvalue(v)) +end + +local function Shared() + local db = _G.MSUF_DB and _G.MSUF_DB.group and _G.MSUF_DB.group.shared + return db or {} +end + +local function ScopeForFrame(frame) + return (frame and frame._groupScope) or "party" +end + +local function GetAuraSetting(scope, key, fallback) + if type(_G.MSUF_Group_GetSetting) == "function" then + return _G.MSUF_Group_GetSetting(scope, "aura", key, fallback) + end + local shared = Shared() + return shared[key] ~= nil and shared[key] or fallback +end + +local function BuildDesignerLookup(scope) + local designer = GetAuraSetting(scope, "designer", nil) + local slot = designerLookupCache[scope] + if not slot then + slot = { stamp = false, lookup = {} } + designerLookupCache[scope] = slot + end + local stamp = false + if type(designer) == "table" then + stamp = tostring(designer.text or "") .. "#" .. tostring(type(designer.groups) == "table" and #designer.groups or 0) + end + if slot.stamp == stamp then + return slot.lookup + end + + local lookup = slot.lookup + wipe(lookup) + slot.stamp = stamp + + if type(designer) ~= "table" or type(designer.groups) ~= "table" then + return lookup + end + for i = 1, #designer.groups do + local entry = designer.groups[i] + if type(entry) == "table" and type(entry.spells) == "table" then + local key = entry.name or ("group" .. i) + for spellID in pairs(entry.spells) do + lookup[spellID] = key + end + end + end + return lookup +end + +local function EnsureIcon(container, list, index) + local icon = list[index] + if icon then return icon end + icon = CreateFrame("Frame", nil, container, "BackdropTemplate") + icon:SetSize(16, 16) + icon.icon = icon:CreateTexture(nil, "ARTWORK") + icon.icon:SetAllPoints() + icon.count = icon:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + icon.count:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 1, -1) + list[index] = icon + return icon +end + +local function LayoutIcons(container, list, maxCount, iconSize) + if container._msufLayoutIconSize == iconSize and container._msufLayoutMaxCount == maxCount then + return + end + container._msufLayoutIconSize = iconSize + container._msufLayoutMaxCount = maxCount + container:SetSize((iconSize + 2) * maxCount, iconSize) + for i = 1, maxCount do + local icon = EnsureIcon(container, list, i) + icon:ClearAllPoints() + icon:SetPoint("LEFT", container, "LEFT", (i - 1) * (iconSize + 2), 0) + icon:SetSize(iconSize, iconSize) + end +end + +local function ShouldSkipAura(shared, aura, excludeSated) + if not aura then return true end + if excludeSated == false then return false end + local sid = aura.spellId + if sid == nil or not IsAccessible(sid) then return false end + return SATED[sid] == true +end + +local function ApplyAura(icon, aura) + if not icon or not aura then return false end + icon.icon:SetTexture(aura.icon) + + local count = aura.applications + if count ~= nil and IsAccessible(count) and count > 1 then + icon.count:SetText(count) + else + icon.count:SetText("") + end + icon:Show() + return true +end + +local function AddAuraToList(aura, out, seen, groupSeen, designerLookup, maxCount, excludeSated, shared) + if not aura or ShouldSkipAura(shared, aura, excludeSated) then return false end + local groupKey + local sid = aura.spellId + if sid ~= nil and IsAccessible(sid) and designerLookup then + groupKey = designerLookup[sid] + end + if groupKey then + if groupSeen[groupKey] then return false end + groupSeen[groupKey] = true + end + local auraInstanceID = aura.auraInstanceID + if auraInstanceID then + if seen[auraInstanceID] then return false end + seen[auraInstanceID] = true + end + out[#out + 1] = aura + return #out >= maxCount +end + +local function AddCompactAuras(unit, cacheList, out, seen, groupSeen, designerLookup, maxCount, excludeSated, shared) + if not (C_UnitAuras and C_UnitAuras.GetAuraDataByAuraInstanceID) or not cacheList then return end + for i = 1, #cacheList do + local auraInstanceID = cacheList[i] + if auraInstanceID and not seen[auraInstanceID] then + local aura = C_UnitAuras.GetAuraDataByAuraInstanceID(unit, auraInstanceID) + if aura and AddAuraToList(aura, out, seen, groupSeen, designerLookup, maxCount, excludeSated, shared) then + return + end + end + end +end + +local function AddFilteredAuras(unit, filter, out, seen, groupSeen, designerLookup, maxCount, excludeSated, shared) + if not (C_UnitAuras and C_UnitAuras.GetAuraDataByIndex) then return end + for idx = 1, 40 do + local aura = C_UnitAuras.GetAuraDataByIndex(unit, idx, filter) + if not aura then break end + if AddAuraToList(aura, out, seen, groupSeen, designerLookup, maxCount, excludeSated, shared) then + return + end + end +end + +local function DisplayAuraList(frame, key, filter, maxCount) + local container = frame[key] + local list = (key == "buffContainer") and frame.buffIcons or frame.debuffIcons + if not container or not list or not C_UnitAuras then return end + + local sh = Shared() + local scope = ScopeForFrame(frame) + local iconSize = GetAuraSetting(scope, "iconSize", sh.auraIconSize or 16) + LayoutIcons(container, list, maxCount, iconSize) + + local unit = frame._assignedUnit + local shown = 0 + for i = 1, maxCount do + list[i]:Hide() + end + if not unit then return end + + local cache = blizzCache[unit] + local compactList = cache and ((key == "buffContainer") and cache.buffs or cache.debuffs) or nil + local picked, seen, groupSeen = {}, {}, {} + local designerLookup = BuildDesignerLookup(scope) + local excludeSated = GetAuraSetting(scope, "excludeSated", sh.excludeSated ~= false) + AddCompactAuras(unit, compactList, picked, seen, groupSeen, designerLookup, maxCount, excludeSated, sh) + AddFilteredAuras(unit, filter, picked, seen, groupSeen, designerLookup, maxCount, excludeSated, sh) + + for i = 1, #picked do + shown = shown + 1 + if not ApplyAura(list[shown], picked[i]) or shown >= maxCount then + break + end + end +end + +local function RefreshUnit(unit) + local frame = Group.activeFrames and Group.activeFrames[unit] + if not frame then return end + local sh = Shared() + local scope = ScopeForFrame(frame) + DisplayAuraList(frame, "buffContainer", "HELPFUL|RAID", GetAuraSetting(scope, "maxBuffs", sh.maxBuffs or 3)) + DisplayAuraList(frame, "debuffContainer", "HARMFUL|RAID", GetAuraSetting(scope, "maxDebuffs", sh.maxDebuffs or 3)) +end + +local function UnitHandler(_, event, unit) + RefreshUnit(unit) +end + +function _G.MSUF_GroupAuras_RefreshAll() + if not Group.activeFrames then return end + for unit in pairs(Group.activeFrames) do + RefreshUnit(unit) + end +end + +local function CacheCompactList(frame, src, list) + if not src then return end + for i = 1, #src do + local auraFrame = src[i] + if auraFrame and auraFrame.auraInstanceID then + list[#list + 1] = auraFrame.auraInstanceID + end + end +end + +local function HookCompactAuras() + if compactHooked or type(hooksecurefunc) ~= "function" or type(_G.CompactUnitFrame_UpdateAuras) ~= "function" then return end + compactHooked = true + hooksecurefunc("CompactUnitFrame_UpdateAuras", function(frame) + local unit = frame and frame.unit + if not unit or not Group.rosterUnits or not Group.rosterUnits[unit] then return end + local cache = blizzCache[unit] or { buffs = {}, debuffs = {} } + blizzCache[unit] = cache + wipe(cache.buffs) + wipe(cache.debuffs) + CacheCompactList(frame, frame.buffFrames, cache.buffs) + CacheCompactList(frame, frame.debuffFrames, cache.debuffs) + RefreshUnit(unit) + end) +end + +HookCompactAuras() +Group.AddUnitEvent(owner, "UNIT_AURA", UnitHandler) diff --git a/MidnightSimpleUnitFrames/Core/MSUF_GroupFrames.lua b/MidnightSimpleUnitFrames/Core/MSUF_GroupFrames.lua new file mode 100644 index 0000000..6ec5f11 --- /dev/null +++ b/MidnightSimpleUnitFrames/Core/MSUF_GroupFrames.lua @@ -0,0 +1,519 @@ +local addonName, ns = ... +ns = ns or {} +ns.Group = ns.Group or {} + +local Group = ns.Group +local CreateFrame = CreateFrame +local UnitExists = UnitExists +local UnitIsUnit = UnitIsUnit +local RegisterUnitWatch = RegisterUnitWatch +local UnregisterUnitWatch = UnregisterUnitWatch +local math_floor = math.floor + +local framesCreated = false +local partyFrames, raidFrames, activeFrames = {}, {}, {} +local partyContainer, raidContainer + +Group.partyFrames = partyFrames +Group.raidFrames = raidFrames +Group.activeFrames = activeFrames + +local function EnsureOverrideTables(scopeDB) + scopeDB = scopeDB or {} + scopeDB.overrides = scopeDB.overrides or {} + scopeDB.overrides.bars = scopeDB.overrides.bars or {} + scopeDB.overrides.font = scopeDB.overrides.font or {} + scopeDB.overrides.aura = scopeDB.overrides.aura or {} + return scopeDB +end + +local function NormalizeGroupConf(conf, defaultY) + conf = conf or {} + conf.anchor = conf.anchor or { "TOPLEFT", nil, "TOPLEFT", 20, defaultY or -200 } + conf.anchor[1] = conf.anchor[1] or "TOPLEFT" + conf.anchor[3] = conf.anchor[3] or conf.anchor[1] + + local anchorX = tonumber(conf.anchor[4]) + local anchorY = tonumber(conf.anchor[5]) + if anchorX == nil then anchorX = 20 end + if anchorY == nil then anchorY = defaultY or -200 end + + if conf.offsetX == nil then conf.offsetX = anchorX end + if conf.offsetY == nil then conf.offsetY = anchorY end + + conf.offsetX = tonumber(conf.offsetX) or anchorX + conf.offsetY = tonumber(conf.offsetY) or anchorY + conf.anchor[4] = conf.offsetX + conf.anchor[5] = conf.offsetY + return conf +end + +local function GetGroupDB() + if not _G.MSUF_DB and type(_G.EnsureDB) == "function" then + _G.EnsureDB() + end + local db = _G.MSUF_DB or {} + db.group = db.group or {} + db.group.shared = db.group.shared or {} + db.group.shared.bars = db.group.shared.bars or {} + db.group.shared.font = db.group.shared.font or {} + db.group.shared.aura = db.group.shared.aura or {} + db.group.shared.aura.designer = db.group.shared.aura.designer or { text = "", groups = {} } + db.group.party = EnsureOverrideTables(NormalizeGroupConf(db.group.party, -200)) + db.group.raid = EnsureOverrideTables(NormalizeGroupConf(db.group.raid, -400)) + return db.group +end + +local function GetScopeForUnit(unit) + if type(unit) ~= "string" then return "party" end + if string.sub(unit, 1, 4) == "raid" then return "raid" end + return "party" +end + +_G.MSUF_Group_NormalizeConf = NormalizeGroupConf +_G.MSUF_Group_GetOffsets = function(conf, defaultY) + conf = NormalizeGroupConf(conf, defaultY) + return tonumber(conf.offsetX) or 0, tonumber(conf.offsetY) or 0 +end +_G.MSUF_Group_SetOffsets = function(conf, x, y, defaultY) + conf = NormalizeGroupConf(conf, defaultY) + conf.offsetX = tonumber(x) or 0 + conf.offsetY = tonumber(y) or 0 + conf.anchor[4] = conf.offsetX + conf.anchor[5] = conf.offsetY + return conf +end +_G.MSUF_Group_GetScopeForUnit = GetScopeForUnit +_G.MSUF_Group_GetSetting = function(scope, category, key, fallback) + local groupDB = GetGroupDB() + local shared = groupDB.shared or {} + local scopeDB = groupDB[scope] or {} + local overrides = scopeDB.overrides and scopeDB.overrides[category] + if overrides and overrides[key] ~= nil then + return overrides[key] + end + local sharedCategory = shared[category] + if sharedCategory and sharedCategory[key] ~= nil then + return sharedCategory[key] + end + if shared[key] ~= nil then + return shared[key] + end + return fallback +end + +local function UpdateTargetHighlight(frame) + local unit = frame and frame._assignedUnit + if not frame or not frame.highlightBorder then return end + frame.highlightBorder:SetShown(unit and UnitExists(unit) and UnitIsUnit(unit, "target") or false) +end + +local function CreateIndicatorBorder(frame, key, inset, r, g, b) + local border = ns.UF.MakeFrame(frame, key, "Frame", "self", (BackdropTemplateMixin and "BackdropTemplate") or nil) + border:SetPoint("TOPLEFT", frame, "TOPLEFT", -inset, inset) + border:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", inset, -inset) + border:SetBackdrop({ edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1 }) + border:SetBackdropBorderColor(r or 1, g or 1, b or 1, 0.95) + border:Hide() + return border +end + +local function CreateOverlayBar(parent, baseBar, frameLevel, r, g, b, a, reverseFill) + local bar = CreateFrame("StatusBar", nil, parent) + bar:SetStatusBarTexture(_G.MSUF_GetBarTexture()) + bar:SetMinMaxValues(0, 1) + _G.MSUF_SetBarValue(bar, 0, false) + bar:SetFrameLevel(frameLevel or (baseBar:GetFrameLevel() + 1)) + bar:SetStatusBarColor(r or 1, g or 1, b or 1, a or 0.6) + bar:SetAllPoints(baseBar) + if bar.SetReverseFill then + bar:SetReverseFill(reverseFill and true or false) + end + bar:Hide() + return bar +end + +local function CreateGroupFrame(name) + local f = CreateFrame("Button", name, UIParent, "BackdropTemplate,SecureUnitButtonTemplate,PingableUnitFrameTemplate") + f:SetClampedToScreen(true) + f:RegisterForClicks("AnyUp") + f:SetAttribute("*type1", "target") + f:SetAttribute("*type2", "togglemenu") + f:EnableMouse(true) + f:SetScript("OnEnter", ns.UF.Unitframe_OnEnter) + f:SetScript("OnLeave", ns.UF.Unitframe_OnLeave) + + local bg = ns.UF.MakeTex(f, "bg", "self", "BACKGROUND") + bg:SetAllPoints() + bg:SetTexture("Interface\\Buttons\\WHITE8x8") + bg:SetVertexColor(0.08, 0.08, 0.08, 0.85) + + local hpBar = ns.UF.MakeBar(f, "hpBar", "self") + hpBar:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1) + hpBar:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1) + hpBar:SetStatusBarTexture(_G.MSUF_GetBarTexture()) + hpBar:SetMinMaxValues(0, 1) + _G.MSUF_SetBarValue(hpBar, 1, false) + hpBar:SetFrameLevel(f:GetFrameLevel() + 1) + + local hpBG = ns.UF.MakeTex(f, "hpBarBG", "hpBar", "BACKGROUND") + hpBG:SetAllPoints(hpBar) + hpBG:SetTexture("Interface\\Buttons\\WHITE8x8") + hpBG:SetVertexColor(0, 0, 0, 0.35) + + local powerBar = ns.UF.MakeBar(f, "powerBar", "self") + powerBar:SetStatusBarTexture(_G.MSUF_GetBarTexture()) + powerBar:SetMinMaxValues(0, 1) + _G.MSUF_SetBarValue(powerBar, 0, false) + powerBar:SetFrameLevel(hpBar:GetFrameLevel() + 1) + powerBar:Hide() + + f.absorbBar = CreateOverlayBar(f, hpBar, hpBar:GetFrameLevel() + 2, 0.3, 0.8, 1.0, 0.55, true) + f.healAbsorbBar = CreateOverlayBar(f, hpBar, hpBar:GetFrameLevel() + 3, 0.95, 0.2, 0.6, 0.55, false) + f.healPredictionBar = CreateOverlayBar(f, hpBar, hpBar:GetFrameLevel() + 4, 0.1, 1.0, 0.35, 0.35, true) + + local textFrame = ns.UF.MakeFrame(f, "textFrame", "Frame", "self") + textFrame:SetAllPoints() + textFrame:SetFrameLevel(hpBar:GetFrameLevel() + 3) + + local fontPath = ns.Castbars and ns.Castbars._GetFontPath and ns.Castbars._GetFontPath() or STANDARD_TEXT_FONT + local flags = ns.Castbars and ns.Castbars._GetFontFlags and ns.Castbars._GetFontFlags() or "" + local fr, fg, fb = 1, 1, 1 + if type(ns.MSUF_GetConfiguredFontColor) == "function" then + local r, g, b = ns.MSUF_GetConfiguredFontColor() + fr, fg, fb = r or 1, g or 1, b or 1 + end + + local nameText = ns.UF.MakeFont(f, "nameText", "textFrame", "GameFontHighlight", "OVERLAY") + nameText:SetPoint("LEFT", f, "LEFT", 4, 0) + nameText:SetPoint("RIGHT", f, "RIGHT", -42, 0) + nameText:SetJustifyH("LEFT") + if nameText.SetFont then nameText:SetFont(fontPath, 11, flags) end + nameText:SetTextColor(fr, fg, fb, 1) + + local stateText = ns.UF.MakeFont(f, "stateText", "textFrame", "GameFontHighlightSmall", "OVERLAY") + stateText:SetPoint("RIGHT", f, "RIGHT", -4, 0) + stateText:SetJustifyH("RIGHT") + if stateText.SetFont then stateText:SetFont(fontPath, 10, flags) end + stateText:SetTextColor(1, 0.2, 0.2, 1) + + local hpText = ns.UF.MakeFont(f, "hpText", "textFrame", "GameFontHighlightSmall", "OVERLAY") + hpText:SetPoint("RIGHT", f, "RIGHT", -4, 0) + hpText:SetJustifyH("RIGHT") + if hpText.SetFont then hpText:SetFont(fontPath, 10, flags) end + hpText:SetTextColor(fr, fg, fb, 1) + hpText:Hide() + + local afkText = ns.UF.MakeFont(f, "afkText", "textFrame", "GameFontHighlightSmall", "OVERLAY") + afkText:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -2, 2) + if afkText.SetFont then afkText:SetFont(fontPath, 9, flags) end + afkText:SetTextColor(1, 0.82, 0, 1) + afkText:Hide() + + local roleIcon = ns.UF.MakeTex(f, "roleIcon", "textFrame", "OVERLAY", 6) + roleIcon:SetSize(12, 12) + roleIcon:SetPoint("LEFT", f, "LEFT", 2, 0) + roleIcon:Hide() + + local readyCheckIcon = ns.UF.MakeTex(f, "readyCheckIcon", "textFrame", "OVERLAY", 6) + readyCheckIcon:SetSize(14, 14) + readyCheckIcon:SetPoint("RIGHT", f, "RIGHT", -2, 0) + readyCheckIcon:Hide() + + local resIcon = ns.UF.MakeTex(f, "resIcon", "textFrame", "OVERLAY", 6) + resIcon:SetSize(12, 12) + resIcon:SetPoint("RIGHT", readyCheckIcon, "LEFT", -2, 0) + resIcon:Hide() + + local summonIcon = ns.UF.MakeTex(f, "summonIcon", "textFrame", "OVERLAY", 6) + summonIcon:SetSize(12, 12) + summonIcon:SetPoint("RIGHT", resIcon, "LEFT", -2, 0) + summonIcon:Hide() + + local raidMarkerIcon = ns.UF.MakeTex(f, "raidMarkerIcon", "textFrame", "OVERLAY", 6) + raidMarkerIcon:SetSize(14, 14) + raidMarkerIcon:SetPoint("TOP", f, "TOP", 0, 6) + raidMarkerIcon:Hide() + + local phasedIcon = ns.UF.MakeTex(f, "phasedIcon", "textFrame", "OVERLAY", 6) + phasedIcon:SetSize(12, 12) + phasedIcon:SetPoint("LEFT", roleIcon, "RIGHT", 2, 0) + phasedIcon:SetTexture("Interface\\TargetingFrame\\UI-PhasingIcon") + phasedIcon:Hide() + + local buffContainer = ns.UF.MakeFrame(f, "buffContainer", "Frame", "self") + buffContainer:SetPoint("BOTTOMLEFT", f, "TOPLEFT", 0, 2) + buffContainer:SetSize(64, 18) + f.buffIcons = {} + + local debuffContainer = ns.UF.MakeFrame(f, "debuffContainer", "Frame", "self") + debuffContainer:SetPoint("TOPLEFT", f, "BOTTOMLEFT", 0, -2) + debuffContainer:SetSize(64, 18) + f.debuffIcons = {} + + local privateAuraContainer = ns.UF.MakeFrame(f, "privateAuraContainer", "Frame", "self") + privateAuraContainer:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", 0, 2) + privateAuraContainer:SetSize(64, 18) + + CreateIndicatorBorder(f, "highlightBorder", 1, 1, 1, 1) + CreateIndicatorBorder(f, "threatBorder", 2, 1, 0.65, 0) + CreateIndicatorBorder(f, "selfBorder", 3, 0.2, 1, 0.2) + + if ClickCastFrames then + ClickCastFrames[f] = true + end + + f.UpdateTargetHighlight = UpdateTargetHighlight + return f +end + +local function EnsureContainers() + if partyContainer and raidContainer then return end + partyContainer = CreateFrame("Frame", "MSUF_GroupPartyContainer", UIParent) + raidContainer = CreateFrame("Frame", "MSUF_GroupRaidContainer", UIParent) + Group.partyContainer = partyContainer + Group.raidContainer = raidContainer +end + +local function ApplyContainerAnchor(container, conf) + conf = NormalizeGroupConf(conf) + local anchor = conf.anchor or { "TOPLEFT", nil, "TOPLEFT", conf.offsetX or 20, conf.offsetY or -200 } + local point, rel, relPoint = anchor[1], anchor[2], anchor[3] + local x = tonumber(conf.offsetX) + local y = tonumber(conf.offsetY) + if x == nil then x = tonumber(anchor[4]) or 0 end + if y == nil then y = tonumber(anchor[5]) or 0 end + anchor[4], anchor[5] = x, y + container:ClearAllPoints() + container:SetPoint(point or "TOPLEFT", rel or UIParent, relPoint or point or "TOPLEFT", x or 0, y or 0) +end + +local function ApplyFrameGeometry(frame, conf, shared) + local scope = frame._groupScope or "party" + local width = conf.width or 90 + local height = conf.height or 36 + local powerMode = _G.MSUF_Group_GetSetting(scope, "bars", "showPowerBar", shared.showPowerBar or "HEALER") + local powerHeight = _G.MSUF_Group_GetSetting(scope, "bars", "powerBarHeight", shared.powerBarHeight or 3) + local nameSize = _G.MSUF_Group_GetSetting(scope, "font", "nameSize", 11) + local hpSize = _G.MSUF_Group_GetSetting(scope, "font", "hpSize", 10) + + frame:SetSize(width, height) + if frame.nameText and frame.nameText.SetFont then + local fontPath = ns.Castbars and ns.Castbars._GetFontPath and ns.Castbars._GetFontPath() or STANDARD_TEXT_FONT + local flags = ns.Castbars and ns.Castbars._GetFontFlags and ns.Castbars._GetFontFlags() or "" + frame.nameText:SetFont(fontPath, nameSize, flags) + end + if frame.hpText and frame.hpText.SetFont then + local fontPath = ns.Castbars and ns.Castbars._GetFontPath and ns.Castbars._GetFontPath() or STANDARD_TEXT_FONT + local flags = ns.Castbars and ns.Castbars._GetFontFlags and ns.Castbars._GetFontFlags() or "" + frame.hpText:SetFont(fontPath, hpSize, flags) + end + frame.hpBar:ClearAllPoints() + frame.hpBar:SetPoint("TOPLEFT", frame, "TOPLEFT", 1, -1) + frame.hpBar:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -1, 1) + + if powerMode ~= "NONE" then + frame.hpBar:ClearAllPoints() + frame.hpBar:SetPoint("TOPLEFT", frame, "TOPLEFT", 1, -1) + frame.hpBar:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -1, -1) + frame.hpBar:SetPoint("BOTTOM", frame, "BOTTOM", 0, powerHeight + 1) + frame.powerBar:ClearAllPoints() + frame.powerBar:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 1, 1) + frame.powerBar:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -1, 1) + frame.powerBar:SetHeight(powerHeight) + else + frame.powerBar:Hide() + end + + if frame.absorbBar then + frame.absorbBar:ClearAllPoints() + frame.absorbBar:SetAllPoints(frame.hpBar) + end + if frame.healAbsorbBar then + frame.healAbsorbBar:ClearAllPoints() + frame.healAbsorbBar:SetAllPoints(frame.hpBar) + end + if frame.healPredictionBar then + frame.healPredictionBar:ClearAllPoints() + frame.healPredictionBar:SetAllPoints(frame.hpBar) + end +end + +local function SetContainerBounds(container, used, conf) + if not container then return end + local width = conf.width or 90 + local height = conf.height or 36 + local spacing = conf.spacing or 2 + local grow = conf.growthDirection or "DOWN" + local wrap = math.max(1, tonumber(conf.wrapAfter) or 5) + + if used == nil or used < 1 then + container:SetSize(width, height) + return + end + + local cols, rows + if grow == "LEFT" or grow == "RIGHT" then + cols = math.min(used, wrap) + rows = math.ceil(used / wrap) + else + rows = math.min(used, wrap) + cols = math.ceil(used / wrap) + end + + local totalWidth = (cols * width) + (math.max(0, cols - 1) * spacing) + local totalHeight = (rows * height) + (math.max(0, rows - 1) * spacing) + container:SetSize(totalWidth, totalHeight) +end + +local function LayoutPool(container, pool, used, conf) + local width = conf.width or 90 + local height = conf.height or 36 + local spacing = conf.spacing or 2 + local grow = conf.growthDirection or "DOWN" + local wrap = conf.wrapAfter or 5 + + SetContainerBounds(container, used, conf) + + local stepX, stepY, wrapX, wrapY = 0, 0, 0, 0 + if grow == "DOWN" then stepY = -(height + spacing); wrapX = width + spacing end + if grow == "UP" then stepY = height + spacing; wrapX = width + spacing end + if grow == "RIGHT" then stepX = width + spacing; wrapY = -(height + spacing) end + if grow == "LEFT" then stepX = -(width + spacing); wrapY = -(height + spacing) end + + for i = 1, #pool do + local frame = pool[i] + if i <= used then + local col = (i - 1) % wrap + local row = math_floor((i - 1) / wrap) + frame:ClearAllPoints() + frame:SetPoint("TOPLEFT", container, "TOPLEFT", col * stepX + row * wrapX, col * stepY + row * wrapY) + frame:Show() + else + frame:Hide() + end + end +end + +local function AssignUnit(frame, unit) + if frame._assignedUnit == unit then return end + Group.DeferIfCombat(function() + if frame._assignedUnit then + UnregisterUnitWatch(frame) + activeFrames[frame._assignedUnit] = nil + end + frame._groupScope = unit and GetScopeForUnit(unit) or frame._groupScope + frame._assignedUnit = unit + frame:SetAttribute("unit", unit) + if unit then + RegisterUnitWatch(frame) + activeFrames[unit] = frame + else + frame:Hide() + end + if type(_G.MSUF_Group_OnAssignedUnit) == "function" then + _G.MSUF_Group_OnAssignedUnit(frame, unit) + end + end) +end + +local function ReleaseUnit(frame) + AssignUnit(frame, nil) +end + +local function EnsurePools() + if framesCreated then return end + EnsureContainers() + for i = 1, 4 do + partyFrames[i] = CreateGroupFrame("MSUF_GroupParty" .. i) + partyFrames[i]:SetParent(partyContainer) + end + for i = 1, 40 do + raidFrames[i] = CreateGroupFrame("MSUF_GroupRaid" .. i) + raidFrames[i]:SetParent(raidContainer) + end + framesCreated = true +end + +function _G.MSUF_EnsureGroupFrames() + local groupDB = GetGroupDB() + if groupDB.enabled == false then return end + EnsurePools() +end + +function _G.MSUF_HideAllGroupFrames() + for i = 1, #partyFrames do ReleaseUnit(partyFrames[i]) end + for i = 1, #raidFrames do ReleaseUnit(raidFrames[i]) end + if partyContainer then partyContainer:Hide() end + if raidContainer then raidContainer:Hide() end +end + +function _G.MSUF_LayoutGroupFrames() + if not framesCreated then return end + local groupDB = GetGroupDB() + if groupDB.enabled == false then + _G.MSUF_HideAllGroupFrames() + return + end + + local shared = groupDB.shared or {} + local partyConf = groupDB.party or {} + local raidConf = groupDB.raid or {} + + ApplyContainerAnchor(partyContainer, partyConf) + ApplyContainerAnchor(raidContainer, raidConf) + + for i = 1, #partyFrames do + ApplyFrameGeometry(partyFrames[i], partyConf, shared) + end + for i = 1, #raidFrames do + ApplyFrameGeometry(raidFrames[i], raidConf, shared) + end + + local roster = Group.roster or {} + local count = roster.count or 0 + if roster.type == "raid" then + partyContainer:Hide() + raidContainer:Show() + LayoutPool(raidContainer, raidFrames, count, raidConf) + elseif roster.type == "party" then + raidContainer:Hide() + partyContainer:Show() + LayoutPool(partyContainer, partyFrames, count, partyConf) + else + partyContainer:Hide() + raidContainer:Hide() + end +end + +local function OnRosterChanged(roster) + if not framesCreated then return end + local groupDB = GetGroupDB() + if groupDB.enabled == false then + _G.MSUF_HideAllGroupFrames() + return + end + + if roster.type == "raid" then + for i = 1, 40 do + local unit = roster.units[i] + if unit then AssignUnit(raidFrames[i], unit) else ReleaseUnit(raidFrames[i]) end + end + for i = 1, 4 do ReleaseUnit(partyFrames[i]) end + elseif roster.type == "party" then + for i = 1, 4 do + local unit = roster.units[i] + if unit then AssignUnit(partyFrames[i], unit) else ReleaseUnit(partyFrames[i]) end + end + for i = 1, 40 do ReleaseUnit(raidFrames[i]) end + else + _G.MSUF_HideAllGroupFrames() + end + + _G.MSUF_LayoutGroupFrames() + if type(_G.MSUF_Group_RefreshAll) == "function" then + _G.MSUF_Group_RefreshAll() + end +end + +Group.OnRosterChanged = OnRosterChanged diff --git a/MidnightSimpleUnitFrames/Core/MSUF_GroupHide.lua b/MidnightSimpleUnitFrames/Core/MSUF_GroupHide.lua new file mode 100644 index 0000000..bdd3ab7 --- /dev/null +++ b/MidnightSimpleUnitFrames/Core/MSUF_GroupHide.lua @@ -0,0 +1,106 @@ +local addonName, ns = ... +ns = ns or {} +ns.Group = ns.Group or {} + +local hiddenFrames = { + "PartyFrame", + "CompactPartyFrame", + "CompactRaidFrameContainer", + "CompactRaidFrameManager", +} + +local hiddenMembers = { + "CompactPartyFrameMember", + "CompactRaidFrame", +} + +local blizzardHidden = false +local pendingSync = false + +local function GetGroupDB() + local db = _G.MSUF_DB + return db and db.group +end + +local function ShouldHideBlizzard() + local groupDB = GetGroupDB() + if not groupDB then return false end + if groupDB.enabled == false then return false end + return groupDB.hideBlizzard ~= false +end + +local function HideFrame(frame) + if not frame then return end + frame:Hide() + if not frame._msufGroupHideHooked then + frame._msufGroupHideHooked = true + hooksecurefunc(frame, "Show", function(self) + if ShouldHideBlizzard() then + self:Hide() + end + end) + if frame.SetShown then + hooksecurefunc(frame, "SetShown", function(self, shown) + if shown and ShouldHideBlizzard() then + self:Hide() + end + end) + end + end +end + +local function RestoreFrame(frame) + if not frame then return end + frame:Show() +end + +local function ForEachBlizzardGroupFrame(fn) + for i = 1, #hiddenFrames do + fn(_G[hiddenFrames[i]]) + end + for i = 1, 4 do + fn(_G[hiddenMembers[1] .. i]) + end + for i = 1, 40 do + fn(_G[hiddenMembers[2] .. i]) + end +end + +local function ApplySync() + pendingSync = false + local shouldHide = ShouldHideBlizzard() + if shouldHide then + ForEachBlizzardGroupFrame(HideFrame) + blizzardHidden = true + elseif blizzardHidden then + ForEachBlizzardGroupFrame(RestoreFrame) + if type(_G.CompactRaidFrameManager_UpdateShown) == "function" then + pcall(_G.CompactRaidFrameManager_UpdateShown, _G.CompactRaidFrameManager) + end + blizzardHidden = false + end +end + +function _G.MSUF_SyncBlizzardGroupFrames() + if InCombatLockdown and InCombatLockdown() then + pendingSync = true + return + end + ApplySync() +end + +local function SyncLater() + C_Timer.After(0.5, function() + _G.MSUF_SyncBlizzardGroupFrames() + end) +end + +if type(_G.MSUF_EventBus_Register) == "function" then + _G.MSUF_EventBus_Register("PLAYER_LOGIN", "MSUF_GROUP_HIDE_LOGIN", SyncLater) + _G.MSUF_EventBus_Register("PLAYER_ENTERING_WORLD", "MSUF_GROUP_HIDE_WORLD", SyncLater) + _G.MSUF_EventBus_Register("PLAYER_REGEN_ENABLED", "MSUF_GROUP_HIDE_REGEN", function() + if pendingSync then + ApplySync() + end + end) +end diff --git a/MidnightSimpleUnitFrames/Core/MSUF_GroupIndicators.lua b/MidnightSimpleUnitFrames/Core/MSUF_GroupIndicators.lua new file mode 100644 index 0000000..6433fed --- /dev/null +++ b/MidnightSimpleUnitFrames/Core/MSUF_GroupIndicators.lua @@ -0,0 +1,122 @@ +local addonName, ns = ... +ns = ns or {} +ns.Group = ns.Group or {} + +local Group = ns.Group +local pairs = pairs +local UnitHasIncomingResurrection = UnitHasIncomingResurrection +local UnitThreatSituation = UnitThreatSituation +local UnitIsAFK = UnitIsAFK +local UnitPhaseReason = UnitPhaseReason +local UnitIsUnit = UnitIsUnit +local GetRaidTargetIndex = GetRaidTargetIndex +local SetRaidTargetIconTexture = SetRaidTargetIconTexture +local GetReadyCheckStatus = GetReadyCheckStatus +local C_IncomingSummon = C_IncomingSummon +local issecretvalue = _G.issecretvalue + +local owner = {} + +local function IsSecret(v) + return issecretvalue and issecretvalue(v) or false +end + +local function SafeBool(v) + if IsSecret(v) then return false end + return v == true +end + +local function RefreshUnit(unit) + local frame = Group.activeFrames and Group.activeFrames[unit] + if not frame then return end + + if frame.resIcon then + frame.resIcon:SetTexture("Interface\\RaidFrame\\Raid-Icon-Rez") + frame.resIcon:SetShown(SafeBool(UnitHasIncomingResurrection and UnitHasIncomingResurrection(unit))) + end + + if frame.summonIcon then + frame.summonIcon:SetTexture("Interface\\RaidFrame\\Raid-Icon-Summon") + local shown = false + if C_IncomingSummon and C_IncomingSummon.HasIncomingSummon then + shown = SafeBool(C_IncomingSummon.HasIncomingSummon(unit)) + end + frame.summonIcon:SetShown(shown) + end + + if frame.readyCheckIcon then + local status = GetReadyCheckStatus and GetReadyCheckStatus(unit) + if status == "ready" then + frame.readyCheckIcon:SetAtlas("ReadyCheck-Ready", true) + frame.readyCheckIcon:Show() + elseif status == "notready" then + frame.readyCheckIcon:SetAtlas("ReadyCheck-NotReady", true) + frame.readyCheckIcon:Show() + elseif status == "waiting" then + frame.readyCheckIcon:SetAtlas("ReadyCheck-Waiting", true) + frame.readyCheckIcon:Show() + else + frame.readyCheckIcon:Hide() + end + end + + if frame.threatBorder then + local threat = UnitThreatSituation and UnitThreatSituation(unit) + if threat ~= nil and not IsSecret(threat) and threat > 0 then + local r, g, b = 1, 1, 0 + if threat >= 3 then r, g, b = 1, 0.1, 0.1 elseif threat == 2 then r, g, b = 1, 0.45, 0 end end + frame.threatBorder:SetBackdropBorderColor(r, g, b, 0.95) + frame.threatBorder:Show() + else + frame.threatBorder:Hide() + end + end + + if frame.selfBorder then + frame.selfBorder:SetShown(UnitIsUnit and UnitIsUnit(unit, "player") or false) + end + + if frame.afkText then + local afk = UnitIsAFK and UnitIsAFK(unit) + local showAFK = SafeBool(afk) and not (_G.MSUF_InCombat == true) + frame.afkText:SetShown(showAFK) + if showAFK then frame.afkText:SetText("AFK") end + end + + if frame.phasedIcon then + local phaseReason = UnitPhaseReason and UnitPhaseReason(unit) + frame.phasedIcon:SetShown(phaseReason ~= nil and not IsSecret(phaseReason)) + end + + if frame.raidMarkerIcon then + local idx = GetRaidTargetIndex and GetRaidTargetIndex(unit) + if idx then + SetRaidTargetIconTexture(frame.raidMarkerIcon, idx) + frame.raidMarkerIcon:Show() + else + frame.raidMarkerIcon:Hide() + end + end +end + +local function UnitHandler(_, event, unit) + RefreshUnit(unit) +end + +function _G.MSUF_GroupIndicators_RefreshAll() + if not Group.activeFrames then return end + for unit in pairs(Group.activeFrames) do + RefreshUnit(unit) + end +end + +Group.AddUnitEvent(owner, "UNIT_THREAT_SITUATION_UPDATE", UnitHandler) +Group.AddUnitEvent(owner, "UNIT_FLAGS", UnitHandler) + +if type(_G.MSUF_EventBus_Register) == "function" then + for _, ev in ipairs({ "READY_CHECK", "READY_CHECK_CONFIRM", "READY_CHECK_FINISHED", "INCOMING_RESURRECT_CHANGED", "INCOMING_SUMMON_CHANGED", "RAID_TARGET_UPDATE", "PLAYER_FLAGS_CHANGED", "PLAYER_TARGET_CHANGED" }) do + _G.MSUF_EventBus_Register(ev, "MSUF_GROUP_INDICATORS_" .. ev, function() + _G.MSUF_GroupIndicators_RefreshAll() + end) + end +end diff --git a/MidnightSimpleUnitFrames/Core/MSUF_GroupPrivateAuras.lua b/MidnightSimpleUnitFrames/Core/MSUF_GroupPrivateAuras.lua new file mode 100644 index 0000000..c51115a --- /dev/null +++ b/MidnightSimpleUnitFrames/Core/MSUF_GroupPrivateAuras.lua @@ -0,0 +1,65 @@ +local addonName, ns = ... +ns = ns or {} + +local function Shared() + local db = _G.MSUF_DB and _G.MSUF_DB.group and _G.MSUF_DB.group.shared + return db or {} +end + +local function ClearAnchors(frame) + if not frame or not frame._privateAnchors or not C_UnitAuras or not C_UnitAuras.RemovePrivateAuraAnchor then return end + for i = 1, #frame._privateAnchors do + C_UnitAuras.RemovePrivateAuraAnchor(frame._privateAnchors[i]) + end + wipe(frame._privateAnchors) +end + +function _G.MSUF_Group_OnAssignedUnit(frame, unit) + if not C_UnitAuras or not C_UnitAuras.AddPrivateAuraAnchor then return end + if not frame then return end + ClearAnchors(frame) + if not unit or not frame.privateAuraContainer then return end + + local sh = Shared() + local maxSlots = sh.maxPrivateAuras or 3 + if maxSlots <= 0 then + frame.privateAuraContainer:Hide() + return + end + frame._privateAnchors = frame._privateAnchors or {} + local container = frame.privateAuraContainer + local slots = container._slots + if type(slots) ~= "table" then + slots = {} + container._slots = slots + end + container:SetSize(maxSlots * 20, 18) + container:Show() + + for i = 1, maxSlots do + local slot = slots[i] + if not (slot and slot.SetPoint and slot.SetSize) then + slot = CreateFrame("Frame", nil, container) + slot:SetSize(18, 18) + slots[i] = slot + end + slot:ClearAllPoints() + slot:SetPoint("LEFT", container, "LEFT", (i - 1) * 20, 0) + if slot.Show then slot:Show() end + frame._privateAnchors[i] = C_UnitAuras.AddPrivateAuraAnchor({ + unitToken = unit, + auraIndex = i, + parent = slot, + showCountdownFrame = true, + showCountdownNumbers = false, + iconInfo = { iconWidth = 18, iconHeight = 18 }, + }) + end + + for i = maxSlots + 1, #slots do + local slot = slots[i] + if slot and slot.Hide then + slot:Hide() + end + end +end diff --git a/MidnightSimpleUnitFrames/Core/MSUF_GroupRange.lua b/MidnightSimpleUnitFrames/Core/MSUF_GroupRange.lua new file mode 100644 index 0000000..9ffb0ee --- /dev/null +++ b/MidnightSimpleUnitFrames/Core/MSUF_GroupRange.lua @@ -0,0 +1,61 @@ +local addonName, ns = ... +ns = ns or {} +ns.Group = ns.Group or {} + +local Group = ns.Group +local pairs = pairs +local UnitInRange = UnitInRange +local issecretvalue = _G.issecretvalue +local ticker +local owner = {} + +local function Shared() + local db = _G.MSUF_DB and _G.MSUF_DB.group and _G.MSUF_DB.group.shared + return db or {} +end + +local function ApplyRange(unit) + local frame = Group.activeFrames and Group.activeFrames[unit] + if not frame then return end + local sh = Shared() + if sh.rangeFade == false then + frame:SetAlpha(1) + return + end + local inRange, checked + if UnitInRange then + inRange, checked = UnitInRange(unit) + end + if issecretvalue and ((inRange ~= nil and issecretvalue(inRange)) or (checked ~= nil and issecretvalue(checked))) then + inRange = nil + end + local alpha = (inRange == false) and (sh.rangeFadeAlpha or 0.4) or 1 + if frame._lastRangeAlpha ~= alpha then + frame._lastRangeAlpha = alpha + frame:SetAlpha(alpha) + end +end + +local function UnitHandler(_, event, unit) + ApplyRange(unit) +end + +function _G.MSUF_GroupRange_RefreshAll() + if not Group.activeFrames then return end + for unit in pairs(Group.activeFrames) do + ApplyRange(unit) + end +end + +local function EnsureTicker() + if ticker or not (C_Timer and C_Timer.NewTicker) then return end + ticker = C_Timer.NewTicker(0.5, function() + local roster = Group.roster or {} + if roster.type == "raid" then + _G.MSUF_GroupRange_RefreshAll() + end + end) +end + +EnsureTicker() +Group.AddUnitEvent(owner, "UNIT_IN_RANGE_UPDATE", UnitHandler) diff --git a/MidnightSimpleUnitFrames/Core/MSUF_GroupRoster.lua b/MidnightSimpleUnitFrames/Core/MSUF_GroupRoster.lua new file mode 100644 index 0000000..976b837 --- /dev/null +++ b/MidnightSimpleUnitFrames/Core/MSUF_GroupRoster.lua @@ -0,0 +1,216 @@ +local addonName, ns = ... +ns = ns or {} +ns.Group = ns.Group or {} + +local Group = ns.Group +local pairs, next, wipe, type = pairs, next, wipe, type +local CreateFrame = CreateFrame +local UnitExists = UnitExists +local UnitGroupRolesAssigned = UnitGroupRolesAssigned +local GetNumGroupMembers = GetNumGroupMembers +local IsInRaid = IsInRaid +local InCombatLockdown = InCombatLockdown +local unpack = unpack or table.unpack +local math_min = math.min + +local unitEventFrames = {} +local activeEvents = {} +local rosterUnits = {} +local rebuildScheduled = false +local combatQueue = {} + +Group.unitEventFrames = unitEventFrames +Group.activeEvents = activeEvents +Group.rosterUnits = rosterUnits +Group.roster = Group.roster or { type = "solo", count = 0, units = {}, roles = {} } + +local function FlushCombatQueue() + if InCombatLockdown and InCombatLockdown() then return end + for i = 1, #combatQueue do + local entry = combatQueue[i] + if entry and entry.fn then + entry.fn(unpack(entry.args or {})) + end + end + wipe(combatQueue) +end + +function Group.DeferIfCombat(fn, ...) + if InCombatLockdown and InCombatLockdown() then + combatQueue[#combatQueue + 1] = { fn = fn, args = { ... } } + return true + end + fn(...) + return false +end + +local function DispatchUnitEvent(event, unit, ...) + local handlers = activeEvents[event] + if not handlers then return end + for owner, fn in next, handlers do + fn(owner, event, unit, ...) + end +end + +local function RegisterRosterUnit(unit) + if not unit then return end + local f = unitEventFrames[unit] + if not f then + f = CreateFrame("Frame") + f:Hide() + f:SetScript("OnEvent", function(_, event, u, ...) + DispatchUnitEvent(event, u, ...) + end) + unitEventFrames[unit] = f + end + f:UnregisterAllEvents() + for event in pairs(activeEvents) do + f:RegisterUnitEvent(event, unit) + end + rosterUnits[unit] = true +end + +local function UnregisterRosterUnit(unit) + local f = unitEventFrames[unit] + if f then + f:UnregisterAllEvents() + end + rosterUnits[unit] = nil +end + +function Group.AddUnitEvent(owner, event, fn) + if not owner or type(event) ~= "string" or type(fn) ~= "function" then return end + local bucket = activeEvents[event] + if not bucket then + bucket = {} + activeEvents[event] = bucket + end + bucket[owner] = fn + for unit in pairs(rosterUnits) do + local f = unitEventFrames[unit] + if f then + f:RegisterUnitEvent(event, unit) + end + end +end + +function Group.RemoveUnitEvent(owner, event) + local bucket = activeEvents[event] + if not bucket then return end + bucket[owner] = nil + if next(bucket) then return end + activeEvents[event] = nil + for unit in pairs(rosterUnits) do + local f = unitEventFrames[unit] + if f then + f:UnregisterEvent(event) + end + end +end + +local ROLE_ORDER = { TANK = 1, HEALER = 2, DAMAGER = 3, NONE = 4 } + +local function SortRosterUnits(a, b) + local roles = Group.roster.roles + local ra = roles[a] or "NONE" + local rb = roles[b] or "NONE" + local oa = ROLE_ORDER[ra] or 99 + local ob = ROLE_ORDER[rb] or 99 + if oa ~= ob then + return oa < ob + end + return a < b +end + +function Group.RebuildRoster() + local roster = Group.roster + local oldUnits = {} + for unit in pairs(rosterUnits) do + oldUnits[unit] = true + end + + wipe(roster.units) + wipe(roster.roles) + + local isRaid = IsInRaid and IsInRaid() + local count = (GetNumGroupMembers and GetNumGroupMembers()) or 0 + roster.type = isRaid and "raid" or ((count > 0) and "party" or "solo") + roster.count = 0 + + if isRaid then + for i = 1, count do + local unit = "raid" .. i + if UnitExists(unit) then + roster.count = roster.count + 1 + roster.units[roster.count] = unit + roster.roles[unit] = UnitGroupRolesAssigned(unit) or "NONE" + RegisterRosterUnit(unit) + oldUnits[unit] = nil + end + end + elseif count > 0 then + for i = 1, math_min(4, count - 1) do + local unit = "party" .. i + if UnitExists(unit) then + roster.count = roster.count + 1 + roster.units[roster.count] = unit + roster.roles[unit] = UnitGroupRolesAssigned(unit) or "NONE" + RegisterRosterUnit(unit) + oldUnits[unit] = nil + end + end + end + + table.sort(roster.units, SortRosterUnits) + + for unit in pairs(oldUnits) do + UnregisterRosterUnit(unit) + end + + if type(Group.OnRosterChanged) == "function" then + Group.OnRosterChanged(roster) + end +end + +local function ScheduleRosterRebuild() + if rebuildScheduled then return end + rebuildScheduled = true + C_Timer.After(0.1, function() + rebuildScheduled = false + local groupDB = _G.MSUF_DB and _G.MSUF_DB.group + if groupDB and groupDB.enabled == false then + if type(_G.MSUF_HideAllGroupFrames) == "function" then + _G.MSUF_HideAllGroupFrames() + end + if type(_G.MSUF_SyncBlizzardGroupFrames) == "function" then + _G.MSUF_SyncBlizzardGroupFrames() + end + return + end + if type(_G.MSUF_EnsureGroupFrames) == "function" then + _G.MSUF_EnsureGroupFrames() + end + Group.RebuildRoster() + if type(_G.MSUF_SyncBlizzardGroupFrames) == "function" then + _G.MSUF_SyncBlizzardGroupFrames() + end + end) +end + +local driver = CreateFrame("Frame") +driver:RegisterEvent("GROUP_ROSTER_UPDATE") +driver:RegisterEvent("PLAYER_ROLES_ASSIGNED") +driver:RegisterEvent("PLAYER_REGEN_ENABLED") +driver:RegisterEvent("PLAYER_ENTERING_WORLD") +driver:SetScript("OnEvent", function(_, event) + if event == "PLAYER_REGEN_ENABLED" then + FlushCombatQueue() + ScheduleRosterRebuild() + else + ScheduleRosterRebuild() + end +end) + +Group.ScheduleRosterRebuild = ScheduleRosterRebuild +Group.RegisterRosterUnit = RegisterRosterUnit +Group.UnregisterRosterUnit = UnregisterRosterUnit diff --git a/MidnightSimpleUnitFrames/Core/MSUF_GroupUpdate.lua b/MidnightSimpleUnitFrames/Core/MSUF_GroupUpdate.lua new file mode 100644 index 0000000..046f342 --- /dev/null +++ b/MidnightSimpleUnitFrames/Core/MSUF_GroupUpdate.lua @@ -0,0 +1,256 @@ +local addonName, ns = ... +ns = ns or {} +ns.Group = ns.Group or {} + +local Group = ns.Group +local UnitHealthPercent = UnitHealthPercent +local UnitHealthMax = UnitHealthMax +local UnitGetIncomingHeals = UnitGetIncomingHeals +local UnitName = UnitName +local UnitClass = UnitClass +local UnitGroupRolesAssigned = UnitGroupRolesAssigned +local UnitIsConnected = UnitIsConnected +local UnitIsDeadOrGhost = UnitIsDeadOrGhost +local UnitPowerPercent = UnitPowerPercent +local UnitPowerType = UnitPowerType +local UnitIsUnit = UnitIsUnit +local RAID_CLASS_COLORS = RAID_CLASS_COLORS +local PowerBarColor = PowerBarColor +local issecretvalue = _G.issecretvalue + +local owner = {} +local hpCurve + +local function IsSecret(v) + return issecretvalue and issecretvalue(v) or false +end + +local function Shared() + local db = _G.MSUF_DB and _G.MSUF_DB.group and _G.MSUF_DB.group.shared + return db or {} +end + +local function ScopeForFrame(frame, unit) + if frame and frame._groupScope then return frame._groupScope end + local fn = _G.MSUF_Group_GetScopeForUnit + if type(fn) == "function" then + return fn(unit) + end + return "party" +end + +local function EnsureCurve() + if hpCurve or not C_CurveUtil or not Enum or not Enum.LuaCurveType then return end + hpCurve = C_CurveUtil.CreateColorCurve() + hpCurve:SetType(Enum.LuaCurveType.Linear) + hpCurve:AddPoint(0.0, CreateColor(1, 0, 0)) + hpCurve:AddPoint(0.5, CreateColor(1, 1, 0)) + hpCurve:AddPoint(1.0, CreateColor(0, 1, 0)) +end + +local function GetFrame(unit) + return Group.activeFrames and Group.activeFrames[unit] +end + +local function SafeSetFontStringText(frame, widget, cacheKey, text) + if not widget then return end + if IsSecret(text) then + frame[cacheKey] = nil + widget:SetText(text) + return + end + if frame[cacheKey] ~= text then + frame[cacheKey] = text + widget:SetText(text or "") + end +end + +local function SetRoleIcon(frame, role) + if not frame or not frame.roleIcon then return end + local atlas = (role == "TANK" and "roleicon-tank") or (role == "HEALER" and "roleicon-healer") or ((role == "DAMAGER" or role == "DPS") and "roleicon-dps") or nil + if atlas then + frame.roleIcon:SetAtlas(atlas, true) + frame.roleIcon:Show() + else + frame.roleIcon:Hide() + end +end + +local function UpdateName(frame, unit) + if not frame or not frame.nameText then return end + local scope = ScopeForFrame(frame, unit) + local showName = _G.MSUF_Group_GetSetting and _G.MSUF_Group_GetSetting(scope, "font", "showName", true) or true + if showName ~= true then + frame._lastName = nil + frame.nameText:SetText("") + frame.nameText:Hide() + return + end + local name = UnitName(unit) or unit + SafeSetFontStringText(frame, frame.nameText, "_lastName", name) + local _, class = UnitClass(unit) + local cc = class and RAID_CLASS_COLORS and RAID_CLASS_COLORS[class] + if cc then + frame.nameText:SetTextColor(cc.r, cc.g, cc.b, 1) + end + frame.nameText:Show() +end + +local function UpdateHealth(frame, unit) + if not frame or not frame.hpBar then return end + EnsureCurve() + local pct = UnitHealthPercent and UnitHealthPercent(unit, true) or 0 + if IsSecret(pct) then + frame._lastHealthPct = nil + _G.MSUF_SetBarValue(frame.hpBar, pct, false) + elseif frame._lastHealthPct ~= pct then + frame._lastHealthPct = pct + _G.MSUF_SetBarValue(frame.hpBar, pct, false) + local colorObj = hpCurve and UnitHealthPercent(unit, true, hpCurve) + if colorObj and colorObj.GetRGB then + local r, g, b = colorObj:GetRGB() + frame.hpBar:SetStatusBarColor(r, g, b) + end + end + + if frame.hpText then + local shared = Shared() + local scope = ScopeForFrame(frame, unit) + local showHPText = _G.MSUF_Group_GetSetting and _G.MSUF_Group_GetSetting(scope, "font", "showHPText", shared.showHPText == true) or (shared.showHPText == true) + if showHPText == true and not IsSecret(pct) then + local txt = string.format("%d%%", math.floor((pct * 100) + 0.5)) + if frame._lastHPText ~= txt then + frame._lastHPText = txt + frame.hpText:SetText(txt) + end + frame.hpText:Show() + else + frame._lastHPText = nil + frame.hpText:SetText("") + frame.hpText:Hide() + end + end + + local maxHP = UnitHealthMax and UnitHealthMax(unit) + if maxHP and not (issecretvalue and issecretvalue(maxHP)) then + if ns.Bars and ns.Bars._UpdateAbsorbBar then + ns.Bars._UpdateAbsorbBar(frame, unit, maxHP) + end + if ns.Bars and ns.Bars._UpdateHealAbsorbBar then + ns.Bars._UpdateHealAbsorbBar(frame, unit, maxHP) + end + if frame.healPredictionBar and UnitGetIncomingHeals then + local incoming = UnitGetIncomingHeals(unit) + if incoming ~= nil and not (issecretvalue and issecretvalue(incoming)) and incoming > 0 then + frame.healPredictionBar:SetMinMaxValues(0, maxHP) + _G.MSUF_SetBarValue(frame.healPredictionBar, incoming, false) + frame.healPredictionBar:Show() + else + frame.healPredictionBar:Hide() + end + end + elseif frame.absorbBar then + frame.absorbBar:Hide() + if frame.healAbsorbBar then frame.healAbsorbBar:Hide() end + if frame.healPredictionBar then frame.healPredictionBar:Hide() end + end +end + +local function ShouldShowPower(frame, role) + local db = Shared() + local scope = frame and frame._groupScope or "party" + local mode = _G.MSUF_Group_GetSetting and _G.MSUF_Group_GetSetting(scope, "bars", "showPowerBar", db.showPowerBar or "HEALER") or (db.showPowerBar or "HEALER") + if mode == "NONE" then return false end + if mode == "ALL" then return true end + return role == "HEALER" +end + +local function UpdatePower(frame, unit, role) + if not frame or not frame.powerBar then return end + if not ShouldShowPower(frame, role) then + frame.powerBar:Hide() + return + end + local pct = UnitPowerPercent(unit, nil, false) or 0 + local pType = UnitPowerType(unit) + if not IsSecret(pct) and frame._lastPowerPct == pct and frame._lastPowerType == pType and frame.powerBar:IsShown() then return end + frame._lastPowerPct = IsSecret(pct) and nil or pct + frame._lastPowerType = pType + local color = PowerBarColor and PowerBarColor[pType or 0] + frame.powerBar:SetStatusBarColor((color and color.r) or 0, (color and color.g) or 0.55, (color and color.b) or 1) + _G.MSUF_SetBarValue(frame.powerBar, pct, false) + frame.powerBar:Show() +end + +local function UpdateState(frame, unit) + if not frame or not frame.stateText then return end + local txt + local connected = UnitIsConnected and UnitIsConnected(unit) + local dead = UnitIsDeadOrGhost and UnitIsDeadOrGhost(unit) + if connected ~= nil and not IsSecret(connected) and connected ~= true then + txt = "DC" + elseif dead ~= nil and not IsSecret(dead) and dead == true then + txt = "DEAD" + end + if frame._lastStateText ~= txt then + frame._lastStateText = txt + frame.stateText:SetText(txt or "") + end + frame.stateText:SetShown(txt ~= nil) + if frame.hpText then + frame.hpText:SetShown((txt == nil) and frame.hpText:GetText() ~= "") + end +end + +local function UpdateOne(unit) + local frame = GetFrame(unit) + if not frame then return end + local role = UnitGroupRolesAssigned(unit) or "NONE" + UpdateHealth(frame, unit) + UpdateName(frame, unit) + SetRoleIcon(frame, role) + UpdateState(frame, unit) + UpdatePower(frame, unit, role) + if frame.UpdateTargetHighlight then + frame:UpdateTargetHighlight() + elseif frame.highlightBorder then + frame.highlightBorder:SetShown(UnitIsUnit(unit, "target")) + end +end + +local function UnitHandler(_, event, unit) + UpdateOne(unit) +end + +function _G.MSUF_Group_RefreshAll() + if not Group.activeFrames then return end + for unit in pairs(Group.activeFrames) do + UpdateOne(unit) + end + if type(_G.MSUF_GroupIndicators_RefreshAll) == "function" then _G.MSUF_GroupIndicators_RefreshAll() end + if type(_G.MSUF_GroupAuras_RefreshAll) == "function" then _G.MSUF_GroupAuras_RefreshAll() end + if type(_G.MSUF_GroupRange_RefreshAll) == "function" then _G.MSUF_GroupRange_RefreshAll() end +end + +Group.AddUnitEvent(owner, "UNIT_HEALTH", UnitHandler) +Group.AddUnitEvent(owner, "UNIT_MAXHEALTH", UnitHandler) +Group.AddUnitEvent(owner, "UNIT_POWER_UPDATE", UnitHandler) +Group.AddUnitEvent(owner, "UNIT_MAXPOWER", UnitHandler) +Group.AddUnitEvent(owner, "UNIT_DISPLAYPOWER", UnitHandler) +Group.AddUnitEvent(owner, "UNIT_ABSORB_AMOUNT_CHANGED", UnitHandler) +Group.AddUnitEvent(owner, "UNIT_HEAL_ABSORB_AMOUNT_CHANGED", UnitHandler) +Group.AddUnitEvent(owner, "UNIT_HEAL_PREDICTION", UnitHandler) +Group.AddUnitEvent(owner, "UNIT_NAME_UPDATE", UnitHandler) +Group.AddUnitEvent(owner, "UNIT_CONNECTION", UnitHandler) + +if type(_G.MSUF_EventBus_Register) == "function" then + _G.MSUF_EventBus_Register("PLAYER_TARGET_CHANGED", "MSUF_GROUP_TARGET", function() + _G.MSUF_Group_RefreshAll() + end) + _G.MSUF_EventBus_Register("READY_CHECK", "MSUF_GROUP_READY_REFRESH", function() + _G.MSUF_Group_RefreshAll() + end) + _G.MSUF_EventBus_Register("PLAYER_ROLES_ASSIGNED", "MSUF_GROUP_ROLE_REFRESH", function() + _G.MSUF_Group_RefreshAll() + end) +end diff --git a/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_Group.lua b/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_Group.lua new file mode 100644 index 0000000..aeec638 --- /dev/null +++ b/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_Group.lua @@ -0,0 +1,247 @@ +local addonName, ns = ... +local EM2 = _G.MSUF_EM2 +if not EM2 or not EM2.Registry or not EM2.PopupFactory then return end + +local Reg = EM2.Registry +local F = EM2.PopupFactory +local floor, max, min = math.floor, math.max, math.min +local pf + +local function GroupDB() + local db = _G.MSUF_DB + return db and db.group +end + +local function ScopeConf(scope) + local g = GroupDB() + return g and g[scope] +end + +local function GetAnchorXY(conf, defaultY) + local fn = _G.MSUF_Group_GetOffsets + if type(fn) == "function" then + return fn(conf, defaultY) + end + local anchor = conf and conf.anchor or { "TOPLEFT", nil, "TOPLEFT", 20, defaultY or -200 } + return tonumber(anchor[4]) or 0, tonumber(anchor[5]) or 0 +end + +local function SetAnchorXY(conf, x, y, defaultY) + local fn = _G.MSUF_Group_SetOffsets + if type(fn) == "function" then + fn(conf, x, y, defaultY) + return + end + conf.anchor = conf.anchor or { "TOPLEFT", nil, "TOPLEFT", 0, 0 } + conf.anchor[4], conf.anchor[5] = x, y +end + +local function Apply() + if not pf or not pf.scope then return end + local conf = ScopeConf(pf.scope) + if not conf then return end + if type(_G.MSUF_EM_UndoBeforeChange) == "function" then _G.MSUF_EM_UndoBeforeChange("group", pf.scope) end + local function num(box, d, lo, hi) + local v = tonumber(box and box.GetText and box:GetText()) or d + if lo and v < lo then v = lo end + if hi and v > hi then v = hi end + return floor(v + 0.5) + end + local defaultY = (pf.scope == "raid") and -400 or -200 + local x = num(pf.xBox, 0) + local y = num(pf.yBox, 0) + SetAnchorXY(conf, x, y, defaultY) + conf.width = num(pf.wBox, conf.width or 90, 20, 400) + conf.height = num(pf.hBox, conf.height or 36, 8, 200) + conf.spacing = num(pf.spacingBox, conf.spacing or 2, 0, 20) + if pf.wrapBox then conf.wrapAfter = num(pf.wrapBox, conf.wrapAfter or 5, 1, 10) end + if pf.growthDrop and pf._growthValue then conf.growthDirection = pf._growthValue end + if type(_G.MSUF_Group_SyncPreview) == "function" then + _G.MSUF_Group_SyncPreview() + else + if type(_G.MSUF_LayoutGroupFrames) == "function" then _G.MSUF_LayoutGroupFrames() end + if type(_G.MSUF_Group_RefreshAll) == "function" then _G.MSUF_Group_RefreshAll() end + end + if EM2.Movers and EM2.Movers.SyncAll then EM2.Movers.SyncAll() end +end + +local function Sync() + if not pf or not pf.scope then return end + local conf = ScopeConf(pf.scope) + if not conf then return end + local x, y = GetAnchorXY(conf, (pf.scope == "raid") and -400 or -200) + pf._titleFS:SetText(pf.scope == "raid" and "Raid" or "Party") + pf.xBox:SetText(x) + pf.yBox:SetText(y) + pf.wBox:SetText(conf.width or 90) + pf.hBox:SetText(conf.height or 36) + pf.spacingBox:SetText(conf.spacing or 2) + if pf.wrapBox then pf.wrapBox:SetText(conf.wrapAfter or 5) end + pf._growthValue = conf.growthDirection or "DOWN" + if pf.growthDrop then pf.growthDrop:SetValue(pf._growthValue) end + pf.wrapRow:SetShown(pf.scope == "raid") + pf._recalcScroll() +end + +local function Build() + if pf then return pf end + pf = F.Panel("MSUF_EM2_GroupPopup", 380, 360, "Party") + local top = pf._contentTop + local GROWTH = { { "DOWN", "Down" }, { "UP", "Up" }, { "RIGHT", "Right" }, { "LEFT", "Left" } } + + local c1, b1 = F.Card(pf, top, "Position & Size", -2, true) + local xy = F.PairRow(pf, b1, c1, { label1 = "X:", label2 = "Y:", key1 = "xBox", key2 = "yBox", onChanged = Apply }) + local wh = F.PairRow(pf, b1, c1, { label1 = "W:", label2 = "H:", key1 = "wBox", key2 = "hBox", anchorTo = xy, onChanged = Apply }) + c1:RecalcHeight() + + local c2, b2 = F.Card(pf, c1, "Layout", -6, true) + local sp = F.PairRow(pf, b2, c2, { label1 = "Space:", label2 = "Wrap:", key1 = "spacingBox", key2 = "wrapBox", onChanged = Apply }) + pf.wrapRow = sp + local dd = F.SizeAnchorRow(pf, b2, c2, { sizeKey = nil, anchorKey = "growthDrop", stateKey = "_growthValue", options = GROWTH, anchorTo = sp, onChanged = Apply, sizeLabel = "", anchorLabel = "Growth" }) + c2:RecalcHeight() + + local ok, cancel = F.FooterButtons(pf) + ok:SetScript("OnClick", function() Apply(); pf:Hide() end) + cancel:SetScript("OnClick", function() pf:Hide() end) + pf._recalcScroll = function() pf:UpdateScrollHeight(280) end + return pf +end + +local GroupPopup = {} +EM2.GroupPopup = GroupPopup +function GroupPopup.Open(scope) if InCombatLockdown and InCombatLockdown() then return end Build(); pf.scope = scope; Sync(); pf:Show() end +function GroupPopup.Close() if pf then pf:Hide() end end +function GroupPopup.IsOpen() return pf and pf:IsShown() or false end +function GroupPopup.Sync() if pf and pf:IsShown() then Sync() end end + +local PARTY_PREVIEW = { + { name = "Thrall", class = "SHAMAN", role = "HEALER", hp = 0.86, power = 0.74 }, + { name = "Jaina", class = "MAGE", role = "DAMAGER", hp = 0.48, power = 0.92 }, + { name = "Anduin", class = "PRIEST", role = "HEALER", hp = 0.95, power = 0.53 }, + { name = "Garrosh", class = "WARRIOR", role = "TANK", hp = 0.71, power = 0.25 }, +} + +local RAID_PREVIEW = { + { name = "Garrosh", class = "WARRIOR", role = "TANK", hp = 1.00, power = 0.35 }, + { name = "Tyrande", class = "DRUID", role = "TANK", hp = 0.82, power = 0.58 }, + { name = "Anduin", class = "PRIEST", role = "HEALER", hp = 0.95, power = 0.45 }, + { name = "Thrall", class = "SHAMAN", role = "HEALER", hp = 0.88, power = 0.70 }, + { name = "Velen", class = "PRIEST", role = "HEALER", hp = 0.90, power = 0.62 }, + { name = "Jaina", class = "MAGE", role = "DAMAGER", hp = 0.67, power = 0.91 }, + { name = "Valeera", class = "ROGUE", role = "DAMAGER", hp = 0.54, power = 0.84 }, + { name = "Rexxar", class = "HUNTER", role = "DAMAGER", hp = 0.79, power = 0.73 }, + { name = "Kael", class = "MAGE", role = "DAMAGER", hp = 0.42, power = 0.95 }, + { name = "Illidan", class = "DEMONHUNTER", role = "DAMAGER", hp = 0.61, power = 0.48 }, + { name = "Muradin", class = "WARRIOR", role = "DAMAGER", hp = 0.76, power = 0.31 }, + { name = "Maiev", class = "DEMONHUNTER", role = "DAMAGER", hp = 0.69, power = 0.67 }, + { name = "Malfurion", class = "DRUID", role = "HEALER", hp = 0.93, power = 0.57 }, + { name = "Uther", class = "PALADIN", role = "HEALER", hp = 0.87, power = 0.64 }, + { name = "Baine", class = "WARRIOR", role = "TANK", hp = 0.84, power = 0.28 }, + { name = "Lor'themar", class = "HUNTER", role = "DAMAGER", hp = 0.58, power = 0.79 }, + { name = "Talanji", class = "PRIEST", role = "HEALER", hp = 0.91, power = 0.60 }, + { name = "Genn", class = "ROGUE", role = "DAMAGER", hp = 0.65, power = 0.52 }, + { name = "Alleria", class = "HUNTER", role = "DAMAGER", hp = 0.72, power = 0.86 }, + { name = "Khadgar", class = "MAGE", role = "DAMAGER", hp = 0.83, power = 0.94 }, +} + +local function ApplyPreviewFrame(frame, data) + if not (frame and data) then return end + frame:Show() + if frame.nameText then frame.nameText:SetText(data.name or "") end + if frame.hpText then + frame.hpText:SetText(string.format("%d%%", floor(((data.hp or 0) * 100) + 0.5))) + frame.hpText:Show() + end + _G.MSUF_SetBarValue(frame.hpBar, data.hp or 1, false) + if frame.powerBar then + frame.powerBar:Show() + _G.MSUF_SetBarValue(frame.powerBar, data.power or 0, false) + end + if frame.roleIcon then + local atlas = (data.role == "TANK" and "roleicon-tank") or (data.role == "HEALER" and "roleicon-healer") or "roleicon-dps" + frame.roleIcon:SetAtlas(atlas, true) + frame.roleIcon:Show() + end + if frame.stateText then + frame.stateText:SetText("") + frame.stateText:Hide() + end + frame:SetAlpha(1) +end + +local function ShowPreviewPool(group, scope, frames, container, data) + if not (group and frames and container) then return end + container:Show() + for i = 1, #frames do + local frame = frames[i] + if data[i] then + ApplyPreviewFrame(frame, data[i]) + elseif frame then + frame:Hide() + end + end + if group.roster then + group.roster.type = scope + group.roster.count = #data + group.roster.units = group.roster.units or {} + wipe(group.roster.units) + for i = 1, #data do + group.roster.units[i] = scope .. i + end + end +end + +function _G.MSUF_Group_SyncPreview() + if not _G.MSUF_GroupPreviewActive then + if ns.Group and ns.Group.ScheduleRosterRebuild then ns.Group.ScheduleRosterRebuild() end + if type(_G.MSUF_LayoutGroupFrames) == "function" then _G.MSUF_LayoutGroupFrames() end + if type(_G.MSUF_Group_RefreshAll) == "function" then _G.MSUF_Group_RefreshAll() end + if EM2.Movers and EM2.Movers.SyncAll then EM2.Movers.SyncAll() end + return + end + if type(_G.MSUF_EnsureGroupFrames) == "function" then _G.MSUF_EnsureGroupFrames() end + local group = ns.Group + if not group then return end + + local activeKey = EM2.State and EM2.State.GetUnitKey and EM2.State.GetUnitKey() + local wantRaid = (pf and pf.scope == "raid") or (activeKey == "group_raid") + if wantRaid then + if group.partyContainer then group.partyContainer:Hide() end + ShowPreviewPool(group, "raid", group.raidFrames, group.raidContainer, RAID_PREVIEW) + else + if group.raidContainer then group.raidContainer:Hide() end + ShowPreviewPool(group, "party", group.partyFrames, group.partyContainer, PARTY_PREVIEW) + end + + if type(_G.MSUF_LayoutGroupFrames) == "function" then + _G.MSUF_LayoutGroupFrames() + if wantRaid and group.raidContainer then + group.raidContainer:Show() + elseif group.partyContainer then + group.partyContainer:Show() + end + end + if EM2.Movers and EM2.Movers.SyncAll then EM2.Movers.SyncAll() end +end + +local function RegisterAll() + Reg.Register({ + key = "group_party", label = "Party", order = 70, popupType = "group", canResize = true, canNudge = true, + getFrame = function() return (ns.Group and ns.Group.partyContainer) end, + getConf = function() local g = GroupDB(); return g and g.party end, + isEnabled = function() local g = GroupDB(); return g and g.enabled ~= false end, + }) + Reg.Register({ + key = "group_raid", label = "Raid", order = 80, popupType = "group", canResize = true, canNudge = true, + getFrame = function() return (ns.Group and ns.Group.raidContainer) end, + getConf = function() local g = GroupDB(); return g and g.raid end, + isEnabled = function() local g = GroupDB(); return g and g.enabled ~= false end, + }) +end + +local f = CreateFrame("Frame") +f:RegisterEvent("PLAYER_LOGIN") +f:SetScript("OnEvent", function(self) + self:UnregisterAllEvents() + C_Timer.After(0, RegisterAll) +end) diff --git a/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_HUD.lua b/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_HUD.lua index 553685f..3d81261 100644 --- a/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_HUD.lua +++ b/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_HUD.lua @@ -13,7 +13,7 @@ local W8 = "Interface/Buttons/WHITE8X8" local floor, max, min = math.floor, math.max, math.min local hudFrame, row2Frame -local previewBtn, auraBtn, snapToggle, cdmBtn, anchorBtn +local previewBtn, auraBtn, groupBtn, snapToggle, cdmBtn, anchorBtn local undoBtn, redoBtn, cancelAllBtn, exitBtn local alphaFS, stepFS local helpBtn, tutorialPanel, tourState @@ -677,6 +677,14 @@ local function EnsureHUD() SetTip(auraBtn, "Toggle aura preview icons\nand aura mover boxes.") r1[#r1+1] = auraBtn + groupBtn = MakeBtn(c1, "Group", 54, BTN_H, 12, function() + _G.MSUF_GroupPreviewActive = not (_G.MSUF_GroupPreviewActive and true or false) + if type(_G.MSUF_Group_SyncPreview) == "function" then _G.MSUF_Group_SyncPreview() end + SetActive(groupBtn, _G.MSUF_GroupPreviewActive) + end) + SetTip(groupBtn, "Show party/raid preview frames\nwith placeholder data.") + r1[#r1+1] = groupBtn + snapToggle = MakeBtn(c1, "Snap", 48, BTN_H, 12, function() if EM2.Snap then local on = not EM2.Snap.IsEnabled() diff --git a/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_Movers.lua b/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_Movers.lua index e841734..e9c480a 100644 --- a/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_Movers.lua +++ b/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_Movers.lua @@ -26,6 +26,11 @@ end local movers = {} local moverParent +local function IsGroupPreviewMover(key) + if key ~= "group_party" and key ~= "group_raid" then return false end + return _G.MSUF_GroupPreviewActive and true or false +end + local function SyncMoverToFrame(mover, frame) if not frame then return end local l, r, t, b = frame:GetLeft(), frame:GetRight(), frame:GetTop(), frame:GetBottom() @@ -89,7 +94,7 @@ local function CreateMover(key, cfg) -- Hide label when preview is active (preview frame already shows unit name) function mover:UpdateLabelVisibility() - if _G.MSUF_PreviewTestMode and not self._dragging then + if (_G.MSUF_PreviewTestMode or IsGroupPreviewMover(self._barKey)) and not self._dragging then self._label:Hide() self._bg:SetColorTexture(0, 0, 0, 0) self._brd:SetBackdropBorderColor(th.edgeR, th.edgeG, th.edgeB, 0.25) @@ -106,6 +111,12 @@ local function CreateMover(key, cfg) self._dragging = true self._coordFS:Show() + if EM2.State then EM2.State.SetUnitKey(key) end + if EM2.HUD and EM2.HUD.RefreshUnitSelector then EM2.HUD.RefreshUnitSelector() end + if cfg and cfg.popupType == "group" and type(_G.MSUF_Group_SyncPreview) == "function" then + _G.MSUF_Group_SyncPreview() + end + if type(_G.MSUF_EM_UndoBeforeChange) == "function" then _G.MSUF_EM_UndoBeforeChange("unit", key) end diff --git a/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_Nudge.lua b/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_Nudge.lua index 1d1e371..028c67a 100644 --- a/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_Nudge.lua +++ b/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_Nudge.lua @@ -35,6 +35,44 @@ local function GetCastbarOffsetKeys(unit) return prefix .. "OffsetX", prefix .. "OffsetY" end +local function NudgeGroup(scope, ndx, ndy) + local db = _G.MSUF_DB + local g = db and db.group + local conf = g and g[scope] + if not conf then return false end + if type(_G.MSUF_EM_UndoBeforeChange) == "function" then + _G.MSUF_EM_UndoBeforeChange("group", scope, true) + end + local getFn = _G.MSUF_Group_GetOffsets + local setFn = _G.MSUF_Group_SetOffsets + local defaultY = (scope == "raid") and -400 or -200 + local x, y + if type(getFn) == "function" then + x, y = getFn(conf, defaultY) + else + local anchor = conf.anchor or { "TOPLEFT", nil, "TOPLEFT", 20, defaultY } + x = tonumber(conf.offsetX) or tonumber(anchor[4]) or 0 + y = tonumber(conf.offsetY) or tonumber(anchor[5]) or 0 + end + x = floor((x + ndx) + 0.5) + y = floor((y + ndy) + 0.5) + if type(setFn) == "function" then setFn(conf, x, y, defaultY) + else + conf.offsetX, conf.offsetY = x, y + conf.anchor = conf.anchor or { "TOPLEFT", nil, "TOPLEFT", x, y } + conf.anchor[4], conf.anchor[5] = x, y + end + if type(_G.MSUF_Group_SyncPreview) == "function" then + _G.MSUF_Group_SyncPreview() + else + if type(_G.MSUF_LayoutGroupFrames) == "function" then _G.MSUF_LayoutGroupFrames() end + if type(_G.MSUF_Group_RefreshAll) == "function" then _G.MSUF_Group_RefreshAll() end + end + if EM2.GroupPopup and EM2.GroupPopup.IsOpen() then EM2.GroupPopup.Sync() end + if EM2.Movers and EM2.Movers.SyncAll then EM2.Movers.SyncAll() end + return true +end + local function NudgeTarget(dx, dy) if not EM2.State or not EM2.State.IsActive() then return end if InCombatLockdown and InCombatLockdown() then return end @@ -125,8 +163,19 @@ local function NudgeTarget(dx, dy) return end - -- Priority 3: current unit frame + -- Priority 3: group popup / group mover selection + if EM2.GroupPopup and EM2.GroupPopup.IsOpen() then + local groupPF = _G.MSUF_EM2_GroupPopup + local scope = groupPF and groupPF.scope + if scope and NudgeGroup(scope, ndx, ndy) then return end + end + local key = EM2.State.GetUnitKey() or "player" + if key == "group_party" or key == "group_raid" then + if NudgeGroup(key == "group_raid" and "raid" or "party", ndx, ndy) then return end + end + + -- Priority 4: current unit frame local conf = db[key] if not conf then return end if type(_G.MSUF_EM_UndoBeforeChange) == "function" then diff --git a/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_Popups.lua b/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_Popups.lua index b1f23a0..34cea97 100644 --- a/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_Popups.lua +++ b/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_Popups.lua @@ -13,6 +13,7 @@ function Popups.CloseAll() if EM2.UnitPopup then EM2.UnitPopup.Close() end if EM2.CastPopup then EM2.CastPopup.Close() end if EM2.AuraPopup then EM2.AuraPopup.Close() end + if EM2.GroupPopup then EM2.GroupPopup.Close() end if EM2.State then EM2.State.SetPopupOpen(false) end end @@ -51,6 +52,9 @@ function Popups.Open(key, anchorFrame) if key:sub(1, 5) == "aura_" then unit = key:sub(6) end local frame = cfg and cfg.getFrame and cfg.getFrame() if EM2.AuraPopup then EM2.AuraPopup.Open(unit, frame or anchorFrame) end + elseif pType == "group" then + local scope = (key == "group_raid") and "raid" or "party" + if EM2.GroupPopup then EM2.GroupPopup.Open(scope, anchorFrame) end end end @@ -58,5 +62,6 @@ function Popups.IsAnyOpen() return (EM2.UnitPopup and EM2.UnitPopup.IsOpen()) or (EM2.CastPopup and EM2.CastPopup.IsOpen()) or (EM2.AuraPopup and EM2.AuraPopup.IsOpen()) + or (EM2.GroupPopup and EM2.GroupPopup.IsOpen()) or false end diff --git a/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_Ticker.lua b/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_Ticker.lua index 7f4a3bd..27146af 100644 --- a/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_Ticker.lua +++ b/MidnightSimpleUnitFrames/EditMode2/MSUF_EM2_Ticker.lua @@ -59,6 +59,47 @@ local function ResolveAnchor(key, conf) return anchor end +local function RectPointXY(l, r, t, b, p) + if p == "CENTER" then return (l + r) * 0.5, (t + b) * 0.5 end + local cx, cy = (l + r) * 0.5, (t + b) * 0.5 + if p == "TOPLEFT" then return l, t end + if p == "TOP" then return cx, t end + if p == "TOPRIGHT" then return r, t end + if p == "LEFT" then return l, cy end + if p == "RIGHT" then return r, cy end + if p == "BOTTOMLEFT" then return l, b end + if p == "BOTTOM" then return cx, b end + if p == "BOTTOMRIGHT" then return r, b end + return (l + r) * 0.5, (t + b) * 0.5 +end + +local function ApplyGroupDragOffsets(d, moverLeft, moverRight, moverTop, moverBottom, uiScale) + local conf = d.conf + local anchor = conf and conf.anchor or nil + local point = anchor and anchor[1] or "TOPLEFT" + local relPoint = anchor and anchor[3] or point + local ax, ay = PointXY(d.anchor or UIParent, relPoint) + if not (ax and ay) then return end + local anchorScale = (d.anchor and d.anchor.GetEffectiveScale and d.anchor:GetEffectiveScale()) or 1 + if anchorScale == 0 then anchorScale = 1 end + local fx, fy = RectPointXY(moverLeft, moverRight, moverTop, moverBottom, point) + if not (fx and fy) then return end + local offX = ((fx * uiScale) - (ax * anchorScale)) / anchorScale + local offY = ((fy * uiScale) - (ay * anchorScale)) / anchorScale + local setFn = _G.MSUF_Group_SetOffsets + if type(setFn) == "function" then + setFn(conf, round(offX), round(offY), d.groupDefaultY) + else + conf.anchor = conf.anchor or { point, nil, relPoint, 0, 0 } + conf.anchor[4], conf.anchor[5] = round(offX), round(offY) + conf.offsetX, conf.offsetY = conf.anchor[4], conf.anchor[5] + end + if d.bar and not InCombatLockdown() then + d.bar:ClearAllPoints() + d.bar:SetPoint(point, d.anchor or UIParent, relPoint, conf.anchor[4], conf.anchor[5]) + end +end + local tickerFrame local activeDrag local idleSyncAcc = 0 @@ -97,49 +138,34 @@ local function OnUpdate(self, elapsed) round(snapCY - d.screenH * 0.5))) end - -- ═══════════════════════════════════════════════════════════════ - -- Position bar with CENTER — same code path as PositionUnitFrame - -- ═══════════════════════════════════════════════════════════════ - -- snapCX/snapCY = desired bar center in UIParent screen coords. - -- Convert to anchor-relative offset (same math as _UpdateDBFromFrame). - -- Then SetPoint("CENTER", anchor, "CENTER", ...) — identical to pipeline. - local bar = d.bar - if bar and not InCombatLockdown() then + if d.isGroup then + local moverLeft = snapCX - d.halfW + local moverRight = snapCX + d.halfW + local moverTop = snapCY + d.halfH + local moverBottom = snapCY - d.halfH + ApplyGroupDragOffsets(d, moverLeft, moverRight, moverTop, moverBottom, sc) + elseif bar and not InCombatLockdown() then local anchor = d.anchor local conf = d.conf - - -- Compute where bar center IS in screen pixels after hypothetical move - -- snapCX/snapCY are in UIParent coords. - -- Bar center in screen pixels = snapCX * uiScale - -- We need: offset = (barCenter * barScale - anchorCenter * anchorScale) / anchorScale - -- Since we WANT barCenter at snapCX (UIParent coords), and UIParent coords = screen / uiScale: - -- barCenter_local = snapCX * (uiScale / barScale) - -- But simpler: just write offset then SetPoint with CENTER. - local ax, ay = anchor:GetCenter() if ax and ay then local as = anchor:GetEffectiveScale() or 1 local fs = bar:GetEffectiveScale() or 1 if as == 0 then as = 1 end; if fs == 0 then fs = 1 end - -- Desired bar center in absolute screen pixels - local barScreenCX = snapCX * sc -- sc = UIParent:GetEffectiveScale() + local barScreenCX = snapCX * sc local barScreenCY = snapCY * sc - -- Anchor center in absolute screen pixels local ancScreenCX = ax * as local ancScreenCY = ay * as - -- Offset in anchor's coord space local offX = (barScreenCX - ancScreenCX) / as local offY = (barScreenCY - ancScreenCY) / as - -- Boss spacing adjustment if d.bossAdj then offY = offY - d.bossAdj end conf.offsetX = round(offX) conf.offsetY = round(offY) - -- Check ECV path local db = _G.MSUF_DB local _g = db and db.general local ecvFn = _G.MSUF_GetEffectiveCooldownFrame @@ -148,24 +174,15 @@ local function OnUpdate(self, elapsed) local ecvRule = d.ecvRule if _g and _g.anchorToCooldown and ecv and anchor == ecv and ecvRule then - -- ECV path: PositionUnitFrame uses point-to-point - -- We wrote center-to-center offset above, need to convert for ECV local point, relPoint, baseX, extraY = ecvRule[1], ecvRule[2], ecvRule[3] or 0, ecvRule[4] or 0 - -- For ECV: gapY = offsetY, x = baseX + offsetX - -- PositionUnitFrame: MSUF_ApplyPoint(f, point, ecv, relPoint, baseX + offsetX, offsetY + extraY) - -- So we need to recompute offsetX/offsetY for ECV path local ax2, ay2 = PointXY(ecv, relPoint) - -- Desired bar point position local fx2, fy2 = PointXY(bar, point) - -- But bar hasn't moved yet... use target position instead - -- Actually, just temporarily position with CENTER, read PointXY, then fix pcall(function() bar._msufDragActive = false bar:ClearAllPoints() bar:SetPoint("CENTER", anchor, "CENTER", conf.offsetX, conf.offsetY) end) bar._msufDragActive = true - -- Now read the ECV offsets fx2, fy2 = PointXY(bar, point) if ax2 and ay2 and fx2 and fy2 then conf.offsetX = round((fx2 * fs - ax2 * as) / as - baseX) @@ -176,7 +193,6 @@ local function OnUpdate(self, elapsed) bar:SetPoint(point, ecv, relPoint, baseX + conf.offsetX, conf.offsetY + extraY) end) else - -- Normal path: CENTER-to-CENTER (same as PositionUnitFrame line 2429) pcall(function() bar._msufDragActive = false bar:ClearAllPoints() @@ -241,6 +257,8 @@ function Ticker.BeginDrag(mover, key, cfg) screenW = UIParent:GetWidth(), screenH = UIParent:GetHeight(), bossAdj = bossAdj, + isGroup = cfg and cfg.popupType == "group", + groupDefaultY = (key == "group_raid") and -400 or -200, } end @@ -259,15 +277,24 @@ function Ticker.EndDrag() local moved = abs(cx - d.startCX) > 0.5 or abs(cy - d.startCY) > 0.5 if moved then - -- Offsets already written by OnUpdate. Just finalize pipeline. - if type(ApplySettingsForKey) == "function" then - ApplySettingsForKey(d.key) + if d.isGroup then + if type(_G.MSUF_Group_SyncPreview) == "function" then + _G.MSUF_Group_SyncPreview() + else + if type(_G.MSUF_LayoutGroupFrames) == "function" then _G.MSUF_LayoutGroupFrames() end + if type(_G.MSUF_Group_RefreshAll) == "function" then _G.MSUF_Group_RefreshAll() end + end + if EM2.GroupPopup and EM2.GroupPopup.IsOpen() then EM2.GroupPopup.Sync() end + else + if type(ApplySettingsForKey) == "function" then + ApplySettingsForKey(d.key) + end + if _G.MSUF_SyncUnitPositionPopup then _G.MSUF_SyncUnitPositionPopup() end + if EM2.UnitPopup and EM2.UnitPopup.IsOpen() then EM2.UnitPopup.Sync() end end C_Timer.After(0.06, function() if EM2.Movers and EM2.Movers.SyncAll then EM2.Movers.SyncAll() end end) - if _G.MSUF_SyncUnitPositionPopup then _G.MSUF_SyncUnitPositionPopup() end - if EM2.UnitPopup and EM2.UnitPopup.IsOpen() then EM2.UnitPopup.Sync() end end return moved diff --git a/MidnightSimpleUnitFrames/Features/MidnightSimpleUnitFrames_SlashMenu.lua b/MidnightSimpleUnitFrames/Features/MidnightSimpleUnitFrames_SlashMenu.lua index 6595591..6c061ab 100644 --- a/MidnightSimpleUnitFrames/Features/MidnightSimpleUnitFrames_SlashMenu.lua +++ b/MidnightSimpleUnitFrames/Features/MidnightSimpleUnitFrames_SlashMenu.lua @@ -1961,6 +1961,7 @@ local function MSUF_EnsureColorsPanelBuilt() return MSUF_EnsureSubOptionsPanelBu local function MSUF_EnsureAuras2PanelBuilt() return MSUF_EnsureSubOptionsPanelBuilt("auras2") end local function MSUF_EnsureGameplayPanelBuilt() return MSUF_EnsureSubOptionsPanelBuilt("gameplay") end local function MSUF_EnsurePortraitsPanelBuilt() return MSUF_EnsureSubOptionsPanelBuilt("portraits") end +local function MSUF_EnsureGroupOptionsPanelBuilt() if type(_G.MSUF_EnsureGroupOptionsPanelBuilt)=="function" then return _G.MSUF_EnsureGroupOptionsPanelBuilt() end return nil end local function MSUF_EnsureModulesPanelBuilt() if _G.MSUF_ModulesMirrorPanel and _G.MSUF_ModulesMirrorPanel.__MSUF_ModulesBuilt then return _G.MSUF_ModulesMirrorPanel end local p=CreateFrame("Frame","MSUF_ModulesMirrorPanel",UIParent) _G.MSUF_ModulesMirrorPanel=p p.__MSUF_ModulesBuilt=true p.__MSUF_MirrorNoRestoreShow=true p:SetPoint("TOPLEFT",0,0) @@ -2071,6 +2072,8 @@ local MIRROR_PAGES={home={title="Midnight Simple Unitframes (Version 1.9b3)",nav },uf_focus={title="MSUF Focus",build=MSUF_EnsureMainOptionsPanelBuilt,select=function() MSUF_SelectMainOptionsKey("focus") end },uf_boss={title="MSUF Boss Frames",build=MSUF_EnsureMainOptionsPanelBuilt,select=function() MSUF_SelectMainOptionsKey("boss") end },uf_pet={title="MSUF Pet",build=MSUF_EnsureMainOptionsPanelBuilt,select=function() MSUF_SelectMainOptionsKey("pet") end +},uf_group_party={title="MSUF Party Frames",build=MSUF_EnsureGroupOptionsPanelBuilt,select=function() if type(_G.MSUF_SelectGroupOptionsScope)=="function" then _G.MSUF_SelectGroupOptionsScope("party") end end +},uf_group_raid={title="MSUF Raid Frames",build=MSUF_EnsureGroupOptionsPanelBuilt,select=function() if type(_G.MSUF_SelectGroupOptionsScope)=="function" then _G.MSUF_SelectGroupOptionsScope("raid") end end },opt_bars={title="MSUF Bars",build=MSUF_EnsureMainOptionsPanelBuilt,select=function() MSUF_SelectMainOptionsKey("bars") end },opt_fonts={title="MSUF Fonts",build=MSUF_EnsureMainOptionsPanelBuilt,select=function() MSUF_SelectMainOptionsKey("fonts") end },opt_auras={title="MSUF Auras",build=MSUF_EnsureMainOptionsPanelBuilt,select=function() MSUF_SelectMainOptionsKey("auras") end @@ -2475,7 +2478,7 @@ navParent._msufNavStripe=stripe end local function MakeButton(label,w,onClick,isHeader,isChild) local b=UI_Button(navParent,tostring(label or""),w,btnH,"TOPLEFT",navParent,"TOPLEFT",0,0,onClick) MSUF_LeftJustifyButtonText(b,isHeader and 18 or(isChild and 22 or 24)) MSUF_SkinNavButton(b,isHeader,isChild) return b end -local NAV={{type="leaf",key="home",label="Dashboard"},{type="header",id="unitframes",label="Unit Frames",defaultOpen=true,children={{key="uf_player",label="Player"},{key="uf_target",label="Target"},{key="uf_targettarget",label="Target of Target"},{key="uf_focus",label="Focus"},{key="uf_boss",label="Boss Frames"},{key="uf_pet",label="Pet"},}},{type="header",id="options",label="Options",defaultOpen=true,children={{key="opt_bars",label="Bars"},{key="opt_fonts",label="Fonts"},{key="auras2",label="Auras 2.0"},{key="opt_castbar",label="Castbar"},{key="opt_portraits",label="Portraits"},{key="opt_colors",label="Colors"},{key="opt_misc",label="Miscellaneous"},}},{type="leaf",key="classpower",label="Class Resources"},{type="leaf",key="gameplay",label="Gameplay"},{type="header",id="modules",label="Modules",defaultOpen=false,children={{key="modules",label="Style"},}},{type="leaf",key="profiles",label="Profiles"},} +local NAV={{type="leaf",key="home",label="Dashboard"},{type="header",id="unitframes",label="Unit Frames",defaultOpen=true,children={{key="uf_player",label="Player"},{key="uf_target",label="Target"},{key="uf_targettarget",label="Target of Target"},{key="uf_focus",label="Focus"},{key="uf_boss",label="Boss Frames"},{key="uf_pet",label="Pet"},{key="uf_group_party",label="Party Frames"},{key="uf_group_raid",label="Raid Frames"},}},{type="header",id="options",label="Options",defaultOpen=true,children={{key="opt_bars",label="Bars"},{key="opt_fonts",label="Fonts"},{key="auras2",label="Auras 2.0"},{key="opt_castbar",label="Castbar"},{key="opt_portraits",label="Portraits"},{key="opt_colors",label="Colors"},{key="opt_misc",label="Miscellaneous"},}},{type="leaf",key="classpower",label="Class Resources"},{type="leaf",key="gameplay",label="Gameplay"},{type="header",id="modules",label="Modules",defaultOpen=false,children={{key="modules",label="Style"},}},{type="leaf",key="profiles",label="Profiles"},} local headerLabels={} for _,node in ipairs(NAV) do if node.type=="header"then headerLabels[node.id]=node.label end diff --git a/MidnightSimpleUnitFrames/Foundation/MSUF_Defaults.lua b/MidnightSimpleUnitFrames/Foundation/MSUF_Defaults.lua index 28e126a..b876719 100644 --- a/MidnightSimpleUnitFrames/Foundation/MSUF_Defaults.lua +++ b/MidnightSimpleUnitFrames/Foundation/MSUF_Defaults.lua @@ -1470,6 +1470,62 @@ local function fill(key, defaults) for k, v in pairs(textDefaults) do if MSUF_DB.boss[k] == nil then MSUF_DB.boss[k] = v end end + MSUF_DB.group = MSUF_DB.group or {} + local group = MSUF_DB.group + if group.enabled == nil then group.enabled = true end + if group.hideBlizzard == nil then group.hideBlizzard = true end + group.shared = group.shared or {} + group.party = group.party or {} + group.raid = group.raid or {} + + local shared = group.shared + if shared.showPowerBar == nil then shared.showPowerBar = "HEALER" end + if shared.powerBarHeight == nil then shared.powerBarHeight = 3 end + if shared.showName == nil then shared.showName = true end + if shared.showHPText == nil then shared.showHPText = false end + if shared.rangeFade == nil then shared.rangeFade = true end + if shared.rangeFadeAlpha == nil then shared.rangeFadeAlpha = 0.4 end + if shared.maxBuffs == nil then shared.maxBuffs = 3 end + if shared.maxDebuffs == nil then shared.maxDebuffs = 3 end + if shared.auraIconSize == nil then shared.auraIconSize = 16 end + if shared.excludeSated == nil then shared.excludeSated = true end + shared.bars = shared.bars or {} + shared.font = shared.font or {} + shared.aura = shared.aura or {} + if shared.bars.showPowerBar == nil then shared.bars.showPowerBar = shared.showPowerBar end + if shared.bars.powerBarHeight == nil then shared.bars.powerBarHeight = shared.powerBarHeight end + if shared.font.showName == nil then shared.font.showName = shared.showName end + if shared.font.showHPText == nil then shared.font.showHPText = shared.showHPText end + if shared.font.nameSize == nil then shared.font.nameSize = 11 end + if shared.font.hpSize == nil then shared.font.hpSize = 10 end + if shared.aura.maxBuffs == nil then shared.aura.maxBuffs = 3 end + if shared.aura.maxDebuffs == nil then shared.aura.maxDebuffs = 3 end + if shared.aura.iconSize == nil then shared.aura.iconSize = 16 end + if shared.aura.excludeSated == nil then shared.aura.excludeSated = true end + if shared.aura.mode == nil then shared.aura.mode = "BLIZZARD" end + shared.aura.designer = shared.aura.designer or { text = "", groups = {} } + + local party = group.party + if party.width == nil then party.width = 90 end + if party.height == nil then party.height = 36 end + if party.anchor == nil then party.anchor = { "TOPLEFT", nil, "TOPLEFT", 20, -200 } end + if party.offsetX == nil then party.offsetX = tonumber(party.anchor[4]) or 20 end + if party.offsetY == nil then party.offsetY = tonumber(party.anchor[5]) or -200 end + if party.growthDirection == nil then party.growthDirection = "DOWN" end + if party.spacing == nil then party.spacing = 2 end + party.overrides = party.overrides or { bars = {}, font = {}, aura = {} } + + local raid = group.raid + if raid.width == nil then raid.width = 72 end + if raid.height == nil then raid.height = 32 end + if raid.anchor == nil then raid.anchor = { "TOPLEFT", nil, "TOPLEFT", 20, -400 } end + if raid.offsetX == nil then raid.offsetX = tonumber(raid.anchor[4]) or 20 end + if raid.offsetY == nil then raid.offsetY = tonumber(raid.anchor[5]) or -400 end + if raid.growthDirection == nil then raid.growthDirection = "DOWN" end + if raid.spacing == nil then raid.spacing = 1 end + if raid.wrapAfter == nil then raid.wrapAfter = 5 end + raid.overrides = raid.overrides or { bars = {}, font = {}, aura = {} } + for _, unitKey in ipairs({"player", "target", "targettarget", "focus", "pet", "boss"}) do MSUF_DB[unitKey] = MSUF_DB[unitKey] or {} local u = MSUF_DB[unitKey] @@ -1518,4 +1574,4 @@ function EnsureDB() end -- Optional exports for other modules ns.MSUF_EnsureDB_Heavy = MSUF_EnsureDB_Heavy -ns.EnsureDB = EnsureDB \ No newline at end of file +ns.EnsureDB = EnsureDB diff --git a/MidnightSimpleUnitFrames/MidnightSimpleUnitFrames.toc b/MidnightSimpleUnitFrames/MidnightSimpleUnitFrames.toc index 6e1f748..a6584da 100644 --- a/MidnightSimpleUnitFrames/MidnightSimpleUnitFrames.toc +++ b/MidnightSimpleUnitFrames/MidnightSimpleUnitFrames.toc @@ -71,6 +71,7 @@ EditMode2\MSUF_EM2_Popup_Cast.lua EditMode2\MSUF_EM2_Popup_Aura.lua EditMode2\MSUF_EM2_Nudge.lua EditMode2\MSUF_EM2_HUD.lua +EditMode2\MSUF_EM2_Group.lua EditMode2\MSUF_EM2_Elements.lua EditMode2\MSUF_EM2_Compat.lua EditMode2\MSUF_EM2_Init.lua @@ -132,6 +133,14 @@ Core\MSUF_Portraits.lua Core\MSUF_Alpha.lua Core\MSUF_LoadConditions.lua Core\MSUF_Borders.lua +Core\MSUF_GroupRoster.lua +Core\MSUF_GroupFrames.lua +Core\MSUF_GroupUpdate.lua +Core\MSUF_GroupAuras.lua +Core\MSUF_GroupIndicators.lua +Core\MSUF_GroupRange.lua +Core\MSUF_GroupHide.lua +Core\MSUF_GroupPrivateAuras.lua ClassPower\MSUF_CP_Constants.lua ClassPower\MSUF_CP_Profiles.lua ClassPower\Features\MSUF_CP_Balance.lua @@ -168,6 +177,7 @@ Options\MSUF_Options_Fonts.lua Options\MSUF_Options_Misc.lua Options\MSUF_Options_Portraits.lua Options\MSUF_Options_Gameplay.lua +Options\MSUF_Options_Group.lua # ═══════════════════════════════════════════════════════════════ # MODULES diff --git a/MidnightSimpleUnitFrames/Options/MSUF_Options_Auras.lua b/MidnightSimpleUnitFrames/Options/MSUF_Options_Auras.lua index 523c592..4e11f84 100644 --- a/MidnightSimpleUnitFrames/Options/MSUF_Options_Auras.lua +++ b/MidnightSimpleUnitFrames/Options/MSUF_Options_Auras.lua @@ -853,7 +853,8 @@ function ns.MSUF_RegisterAurasOptions_Full(parentCategory) return box, bodyHost end -- Display + Layout are collapsible for a cleaner menu, but stay open by default. - local displayOuter, displayBody = MakeCollapsibleBox(content, leftTop, 720, 244, "Display", true) + local designerOuter, designerBody = MakeCollapsibleBox(content, leftTop, 720, 110, "Group Aura Designer", false) + local displayOuter, displayBody = MakeCollapsibleBox(content, designerOuter, 720, 244, "Display", true) local capsOuter, capsBody = MakeCollapsibleBox(content, displayOuter, 720, 266, "Layout & Caps", true) -- Timer / cooldown text color controls local timerBox, timerBody = MakeCollapsibleBox(content, capsOuter, 720, 248, "Timer Colors", false) @@ -871,6 +872,7 @@ function ns.MSUF_RegisterAurasOptions_Full(parentCategory) local _displayBoxOuter = displayOuter local _capsBoxOuter = capsOuter local _reminderBoxOuter = reminderBox + designerBox = designerBody or designerOuter displayBox = displayBody or displayOuter capsBox = capsBody or capsOuter timerBox = timerBody or timerBox @@ -943,6 +945,7 @@ local function A2_Settings() return s end local A2_REMINDER_GROWTH_OK = { RIGHT = true, LEFT = true, UP = true, DOWN = true } +local GetOverrideLayoutForEditing, SetOverrideLayoutForEditing local function A2_NormalizeReminderGrowth(v) if type(v) ~= "string" or not A2_REMINDER_GROWTH_OK[v] then return "RIGHT" @@ -962,6 +965,115 @@ local function A2_GetReminderGrowthValue() end return "RIGHT" end +local A2_LAYOUT_SHARED_FIELDS = { + "offsetX", "offsetY", + "spacing", + "buffGroupOffsetX", "buffGroupOffsetY", + "debuffGroupOffsetX", "debuffGroupOffsetY", + "privateOffsetX", "privateOffsetY", + "reminderOffsetX", "reminderOffsetY", + "buffGroupIconSize", "debuffGroupIconSize", "privateSize", "reminderIconSize", + "stackTextSize", "stackTextOffsetX", "stackTextOffsetY", + "cooldownTextSize", "cooldownTextOffsetX", "cooldownTextOffsetY", + "reminderSpacing", "reminderGrowth", +} +local A2_DESIGNER_RESET_FIELDS = { + "offsetX", "offsetY", "spacing", + "buffGroupOffsetX", "buffGroupOffsetY", + "debuffGroupOffsetX", "debuffGroupOffsetY", + "privateOffsetX", "privateOffsetY", + "reminderOffsetX", "reminderOffsetY", + "buffGroupIconSize", "debuffGroupIconSize", "privateSize", + "reminderIconSize", + "stackTextSize", "stackTextOffsetX", "stackTextOffsetY", + "cooldownTextSize", "cooldownTextOffsetX", "cooldownTextOffsetY", + "reminderSpacing", "reminderGrowth", +} +local A2_DESIGNER_RESET_DEFAULTS = { + offsetX = 0, offsetY = 6, + spacing = 2, + reminderOffsetX = 0, reminderOffsetY = 0, + reminderIconSize = 22, + stackTextSize = 14, + cooldownTextSize = 14, + reminderSpacing = 2, reminderGrowth = "RIGHT", +} +GetOverrideLayoutForEditing = function() + local key = GetEditingKey() + if key == "shared" then return false end + local a2 = select(1, GetAuras2DB()) + if not a2 or not a2.perUnit or not a2.perUnit[key] then return false end + return (a2.perUnit[key].overrideLayout == true) +end +SetOverrideLayoutForEditing = function(v) + local key = GetEditingKey() + if key == "shared" then return end + local a2, shared = GetAuras2DB() + if not a2 or not shared then return end + a2.perUnit = (type(a2.perUnit) == "table") and a2.perUnit or {} + if type(a2.perUnit[key]) ~= "table" then a2.perUnit[key] = {} end + local u = a2.perUnit[key] + if v == true then + u.overrideLayout = true + if type(u.layout) ~= "table" then u.layout = {} end + for i = 1, #A2_LAYOUT_SHARED_FIELDS do + local field = A2_LAYOUT_SHARED_FIELDS[i] + if u.layout[field] == nil and shared[field] ~= nil then + u.layout[field] = shared[field] + end + end + else + u.overrideLayout = false + end + A2_RequestApply() + C_Timer.After(0, function() + if panel and panel.OnRefresh then panel.OnRefresh() end + end) +end +local function A2_GetLayoutValue(key, fallback) + local a2, shared = GetAuras2DB() + if not shared then return fallback end + local editKey = GetEditingKey() + if editKey ~= "shared" and a2 and a2.perUnit then + local u = a2.perUnit[editKey] + if u and u.overrideLayout == true and type(u.layout) == "table" then + local v = u.layout[key] + if v ~= nil then return v end + end + end + local v = shared[key] + if v ~= nil then return v end + return fallback +end +local function A2_SetLayoutValue(key, value) + local a2, shared = GetAuras2DB() + if not a2 or not shared then return end + local editKey = GetEditingKey() + if editKey ~= "shared" then + if not GetOverrideLayoutForEditing() then + SetOverrideLayoutForEditing(true) + end + a2.perUnit = (type(a2.perUnit) == "table") and a2.perUnit or {} + if type(a2.perUnit[editKey]) ~= "table" then a2.perUnit[editKey] = {} end + local u = a2.perUnit[editKey] + u.overrideLayout = true + u.layout = (type(u.layout) == "table") and u.layout or {} + u.layout[key] = value + else + shared[key] = value + end + local api = ns and ns.MSUF_Auras2 + local rm = api and api.Reminder + if rm and (key == "reminderOffsetX" or key == "reminderOffsetY" or key == "reminderIconSize" or key == "reminderSpacing" or key == "reminderGrowth") then + if rm.MarkDirty then rm.MarkDirty() end + end + A2_RequestApply() + C_Timer.After(0, function() + if panel and panel.__msufA2_RefreshDesignerPreview then + panel.__msufA2_RefreshDesignerPreview() + end + end) +end local function A2_SetReminderGrowthValue(v) local a2, shared = GetAuras2DB() if not a2 or not shared then return end @@ -1163,7 +1275,7 @@ local function BuildBoolPathCheckboxes(parent, entries, out) local function A2_EnsureTrackTables() if not panel then return nil end if not panel.__msufA2_tracked then - panel.__msufA2_tracked = { global = {}, filters = {}, caps = {} } + panel.__msufA2_tracked = { global = {}, filters = {}, caps = {}, layout = {} } end return panel.__msufA2_tracked end @@ -1221,6 +1333,7 @@ local function A2_RestoreAllScopes() A2_ApplyScopeState("global", true) A2_ApplyScopeState("filters", true) A2_ApplyScopeState("caps", true) + A2_ApplyScopeState("layout", true) end local function A2_ShowOverrideWarn(msg, holdSeconds) if not panel then return end @@ -1260,6 +1373,16 @@ local function A2_AutoOverrideCapsIfNeeded() A2_ShowOverrideWarn("Caps override enabled for this unit.") return true end +local function A2_AutoOverrideLayoutIfNeeded() + if GetEditingKey() == "shared" then return false end + if GetOverrideLayoutForEditing and GetOverrideLayoutForEditing() then return false end + if SetOverrideLayoutForEditing then + SetOverrideLayoutForEditing(true) + A2_ShowOverrideWarn("Frame designer override enabled for this unit.") + return true + end + return false +end local function A2_WrapCheckboxAutoOverride(cb, scope) if not cb or type(cb.GetScript) ~= "function" then return end local old = cb:GetScript("OnClick") @@ -1268,6 +1391,8 @@ local function A2_WrapCheckboxAutoOverride(cb, scope) A2_AutoOverrideFiltersIfNeeded() elseif scope == "caps" then A2_AutoOverrideCapsIfNeeded() + elseif scope == "layout" then + A2_AutoOverrideLayoutIfNeeded() end if old then return old(self, ...) end end) @@ -1627,7 +1752,7 @@ do boss1 = "Boss 1", boss2 = "Boss 2", boss3 = "Boss 3", boss4 = "Boss 4", boss5 = "Boss 5", } local function GetUnitOverrideState(key) - if key == "shared" then return false, false end + if key == "shared" then return false, false, false end local a2 = select(1, GetAuras2DB()) local u = a2 and a2.perUnit and a2.perUnit[key] local overrideFilters = (type(u) == "table" and u.overrideFilters == true) and true or false @@ -1640,10 +1765,12 @@ do end local function GetUnitOverrideTooltip(key) local overrideFilters, overrideCaps = GetUnitOverrideState(key) - if overrideFilters and overrideCaps then return "Override active: this unit uses its own Filters and Caps." end - if overrideFilters then return "Override active: this unit uses its own Filters." end - if overrideCaps then return "Override active: this unit uses its own Caps." end - return "Uses Shared filters and caps." + local parts = {} + if overrideFilters then parts[#parts + 1] = "Filters" end + if overrideCaps then parts[#parts + 1] = "Caps" end + if #parts == 0 then return "Uses Shared filters and caps." end + if #parts == 1 then return "Override active: this unit uses its own " .. parts[1] .. "." end + return "Override active: this unit uses its own " .. table.concat(parts, ", ") .. "." end local scopeBtns = {} local function RefreshScopeButtons() @@ -1839,6 +1966,8 @@ do u.filters = nil -- revert to Shared u.overrideSharedLayout = false u.layoutShared = nil -- revert to Shared + u.overrideLayout = false + u.layout = nil -- revert to Shared designer end end A2_RequestApply() @@ -1896,6 +2025,26 @@ end CreateBoolToggleButtonPath(leftTop, "Target", 108, -120, 90, 22, A2_DB, "showTarget", nil, nil, A2_RequestApply) CreateBoolToggleButtonPath(leftTop, "Focus", 204, -120, 90, 22, A2_DB, "showFocus", nil, nil, A2_RequestApply) CreateBoolToggleButtonPath(leftTop, "Boss 1-5", 300, -120, 96, 22, A2_DB, "showBoss", nil, nil, A2_RequestApply) + + -- ================================================================ + -- GROUP AURA DESIGNER NOTE + -- ================================================================ + do + local noteTitle = designerBox:CreateFontString(nil, "ARTWORK", "GameFontNormal") + noteTitle:SetPoint("TOPLEFT", designerBox, "TOPLEFT", 12, -10) + noteTitle:SetText(TR("Group aura groups moved")) + + local noteText = designerBox:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall") + noteText:SetPoint("TOPLEFT", noteTitle, "BOTTOMLEFT", 0, -6) + noteText:SetWidth(680) + noteText:SetJustifyH("LEFT") + noteText:SetText(TR("The aura designer is group-frame only. Configure party/raid aura groups in the Party Frames / Raid Frames menus, where spell groups can be edited specifically for group frame behavior (similar to VuhDo/Grid2 style grouping).")) + + panel.__msufA2_RefreshDesignerPreview = function() end + panel.__msufA2_UpdateDesignerUnitState = function() end + if _G then _G.MSUF_A2_UpdateDesignerUnitState = panel.__msufA2_UpdateDesignerUnitState end + end + -- ================================================================ -- DISPLAY (grouped: Buffs/Debuffs columns + Icons/Cooldown/Borders columns) -- ================================================================ @@ -2984,6 +3133,9 @@ end if panel and panel.__msufA2_UpdateOverrideSummary then panel.__msufA2_UpdateOverrideSummary() end + -- Sync aura designer state (editing key + player-only widgets). + local fnDesigner = rawget(_G, "MSUF_A2_UpdateDesignerUnitState") + if type(fnDesigner) == "function" then pcall(fnDesigner) end -- Sync ignore list box state (editing key + override gating) local fn = rawget(_G, "MSUF_A2_UpdateIgnoreBoxState") if type(fn) == "function" then pcall(fn) end diff --git a/MidnightSimpleUnitFrames/Options/MSUF_Options_Group.lua b/MidnightSimpleUnitFrames/Options/MSUF_Options_Group.lua new file mode 100644 index 0000000..e228c46 --- /dev/null +++ b/MidnightSimpleUnitFrames/Options/MSUF_Options_Group.lua @@ -0,0 +1,466 @@ +local addonName, ns = ... +ns = ns or {} + +local panel, scopeButtons = nil, {} +local currentScope = "party" + +local LEGACY_MAP = { + bars = { showPowerBar = "showPowerBar", powerBarHeight = "powerBarHeight" }, + font = { showName = "showName", showHPText = "showHPText" }, + aura = { maxBuffs = "maxBuffs", maxDebuffs = "maxDebuffs", iconSize = "auraIconSize", excludeSated = "excludeSated" }, +} + +local function GroupDB() + if type(_G.EnsureDB) == "function" then _G.EnsureDB() end + local db = _G.MSUF_DB + db.group = db.group or {} + db.group.shared = db.group.shared or {} + db.group.shared.bars = db.group.shared.bars or {} + db.group.shared.font = db.group.shared.font or {} + db.group.shared.aura = db.group.shared.aura or {} + db.group.shared.aura.designer = db.group.shared.aura.designer or { text = "", groups = {} } + db.group.party = db.group.party or {} + db.group.raid = db.group.raid or {} + db.group.party.overrides = db.group.party.overrides or { bars = {}, font = {}, aura = {} } + db.group.raid.overrides = db.group.raid.overrides or { bars = {}, font = {}, aura = {} } + return db.group +end + +local function GetScopeDefaultY(scope) + return (scope == "raid") and -400 or -200 +end + +local function GetScopeOffsets(conf, scope) + local fn = _G.MSUF_Group_GetOffsets + if type(fn) == "function" then + return fn(conf, GetScopeDefaultY(scope)) + end + local anchor = conf.anchor or { "TOPLEFT", nil, "TOPLEFT", 20, GetScopeDefaultY(scope) } + return tonumber(conf.offsetX) or tonumber(anchor[4]) or 20, tonumber(conf.offsetY) or tonumber(anchor[5]) or GetScopeDefaultY(scope) +end + +local function CategoryOverride(scope, category) + local g = GroupDB() + local scopeDB = g[scope] + return scopeDB and scopeDB.overrides and scopeDB.overrides[category] or nil +end + +local function IsCategoryOverrideEnabled(scope, category) + local override = CategoryOverride(scope, category) + return override and next(override) ~= nil or false +end + +local function GetGroupValue(scope, category, key, fallback) + if type(_G.MSUF_Group_GetSetting) == "function" then + return _G.MSUF_Group_GetSetting(scope, category, key, fallback) + end + local g = GroupDB() + local override = CategoryOverride(scope, category) + if override and override[key] ~= nil then + return override[key] + end + local sharedCategory = g.shared[category] + if sharedCategory and sharedCategory[key] ~= nil then + return sharedCategory[key] + end + local legacy = LEGACY_MAP[category] and LEGACY_MAP[category][key] + if legacy and g.shared[legacy] ~= nil then + return g.shared[legacy] + end + return fallback +end + +local function WriteGroupValue(scope, category, key, value) + local g = GroupDB() + local sharedCategory = g.shared[category] + local overrideEnabled = IsCategoryOverrideEnabled(scope, category) + if overrideEnabled then + g[scope].overrides[category][key] = value + else + sharedCategory[key] = value + end + local legacy = LEGACY_MAP[category] and LEGACY_MAP[category][key] + if legacy then + if overrideEnabled then + g[scope].overrides[category][legacy] = nil + else + g.shared[legacy] = value + end + end +end + +local function SetCategoryOverride(scope, category, enabled) + local g = GroupDB() + local override = g[scope].overrides[category] + if enabled then + if next(override) ~= nil then return end + local sharedCategory = g.shared[category] or {} + for key, value in pairs(sharedCategory) do + if key ~= "designer" then + override[key] = value + elseif type(value) == "table" then + override[key] = { text = value.text or "", groups = value.groups or {} } + end + end + else + g[scope].overrides[category] = {} + end +end + +local function ParseDesignerText(text) + local groups = {} + if type(text) ~= "string" then return groups end + for line in string.gmatch(text, "[^\r\n]+") do + local name, ids = string.match(line, "^%s*([^=]+)%s*=%s*(.+)%s*$") + if name and ids then + local entry = { name = name, spells = {} } + for rawID in string.gmatch(ids, "[^,%s]+") do + local spellID = tonumber(rawID) + if spellID and spellID > 0 then + entry.spells[spellID] = true + end + end + if next(entry.spells) then + groups[#groups + 1] = entry + end + end + end + return groups +end + +local function CountDesignerSpells(groups) + local total = 0 + if type(groups) ~= "table" then return 0 end + for i = 1, #groups do + local entry = groups[i] + if type(entry) == "table" and type(entry.spells) == "table" then + for _ in pairs(entry.spells) do + total = total + 1 + end + end + end + return total +end + +local function DeepCopy(v) + if type(v) ~= "table" then return v end + local out = {} + for k, vv in pairs(v) do + out[k] = DeepCopy(vv) + end + return out +end + +local function DesignerTextForScope(scope) + local designer = GetGroupValue(scope, "aura", "designer", nil) + if type(designer) == "table" and type(designer.text) == "string" then + return designer.text + end + return "" +end + +local function SaveDesigner(scope, text) + local value = { text = text or "", groups = ParseDesignerText(text or "") } + WriteGroupValue(scope, "aura", "designer", value) +end + +local function UpdateDesignerStatus(scope) + if not panel or not panel.designerStatus then return end + local designer = GetGroupValue(scope, "aura", "designer", nil) + local groups = type(designer) == "table" and designer.groups or nil + local groupCount = type(groups) == "table" and #groups or 0 + local spellCount = CountDesignerSpells(groups) + if groupCount > 0 then + panel.designerStatus:SetText(("Configured groups: %d • Spell IDs: %d"):format(groupCount, spellCount)) + else + panel.designerStatus:SetText("No aura groups configured yet. Add one group per line below.") + end +end + +local function RefreshRuntime() + if type(_G.MSUF_SyncBlizzardGroupFrames) == "function" then _G.MSUF_SyncBlizzardGroupFrames() end + if type(_G.MSUF_EnsureGroupFrames) == "function" then _G.MSUF_EnsureGroupFrames() end + if type(_G.MSUF_LayoutGroupFrames) == "function" then _G.MSUF_LayoutGroupFrames() end + if type(_G.MSUF_Group_RefreshAll) == "function" then _G.MSUF_Group_RefreshAll() end +end + +function _G.MSUF_SelectGroupOptionsScope(scope) + currentScope = (scope == "raid") and "raid" or "party" + if not panel then return end + local g = GroupDB() + local conf = g[currentScope] + panel.title:SetText(currentScope == "raid" and "Raid Frames" or "Party Frames") + panel.enableCB:SetChecked(g.enabled ~= false) + panel.hideCB:SetChecked(g.hideBlizzard ~= false) + local x, y = GetScopeOffsets(conf, currentScope) + panel.xBox:SetText(x) + panel.yBox:SetText(y) + panel.wBox:SetText(conf.width or 90) + panel.hBox:SetText(conf.height or 36) + panel.spacingBox:SetText(conf.spacing or 2) + panel.wrapBox:SetText(conf.wrapAfter or 5) + panel.wrapLabel:SetShown(currentScope == "raid") + panel.wrapBox:SetShown(currentScope == "raid") + panel.growthValue = conf.growthDirection or "DOWN" + panel.growthBtn:SetText("Growth: " .. panel.growthValue) + + panel.barsOverrideCB:SetChecked(IsCategoryOverrideEnabled(currentScope, "bars")) + panel.fontOverrideCB:SetChecked(IsCategoryOverrideEnabled(currentScope, "font")) + panel.auraOverrideCB:SetChecked(IsCategoryOverrideEnabled(currentScope, "aura")) + + panel.powerModeBtn.value = GetGroupValue(currentScope, "bars", "showPowerBar", "HEALER") + panel.powerModeBtn:SetText("Power: " .. panel.powerModeBtn.value) + panel.powerHeightBox:SetText(GetGroupValue(currentScope, "bars", "powerBarHeight", 3)) + + panel.showNameCB:SetChecked(GetGroupValue(currentScope, "font", "showName", true) == true) + panel.showHPTextCB:SetChecked(GetGroupValue(currentScope, "font", "showHPText", false) == true) + panel.nameSizeBox:SetText(GetGroupValue(currentScope, "font", "nameSize", 11)) + panel.hpSizeBox:SetText(GetGroupValue(currentScope, "font", "hpSize", 10)) + + panel.maxBuffsBox:SetText(GetGroupValue(currentScope, "aura", "maxBuffs", 3)) + panel.maxDebuffsBox:SetText(GetGroupValue(currentScope, "aura", "maxDebuffs", 3)) + panel.iconSizeBox:SetText(GetGroupValue(currentScope, "aura", "iconSize", 16)) + panel.excludeSatedCB:SetChecked(GetGroupValue(currentScope, "aura", "excludeSated", true) == true) + panel.designerEdit:SetText(DesignerTextForScope(currentScope)) + UpdateDesignerStatus(currentScope) + + for key, btn in pairs(scopeButtons) do + if btn._label then btn._label:SetTextColor(key == currentScope and 0.38 or 0.72, key == currentScope and 0.65 or 0.74, 1, 1) end + end +end + +local function MakeLabel(parent, text, x, y) + local fs = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + fs:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + fs:SetText(text) + return fs +end + +local function MakeBox(parent, x, y, w) + local eb = CreateFrame("EditBox", nil, parent, "InputBoxTemplate") + eb:SetSize(w or 60, 20) + eb:SetAutoFocus(false) + eb:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + return eb +end + +local function MakeCheck(parent, text, x, y, onClick) + local cb = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate") + cb:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + local fs = cb:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + fs:SetPoint("LEFT", cb, "RIGHT", 2, 1) + fs:SetText(text) + cb.label = fs + if onClick then cb:SetScript("OnClick", onClick) end + return cb +end + +local function Apply() + if not panel then return end + local g = GroupDB() + local conf = g[currentScope] + g.enabled = panel.enableCB:GetChecked() and true or false + g.hideBlizzard = panel.hideCB:GetChecked() and true or false + + local setFn = _G.MSUF_Group_SetOffsets + local x = tonumber(panel.xBox:GetText()) or 20 + local y = tonumber(panel.yBox:GetText()) or GetScopeDefaultY(currentScope) + if type(setFn) == "function" then + setFn(conf, x, y, GetScopeDefaultY(currentScope)) + else + conf.offsetX, conf.offsetY = x, y + conf.anchor = conf.anchor or { "TOPLEFT", nil, "TOPLEFT", x, y } + conf.anchor[4], conf.anchor[5] = x, y + end + conf.width = tonumber(panel.wBox:GetText()) or conf.width or 90 + conf.height = tonumber(panel.hBox:GetText()) or conf.height or 36 + conf.spacing = tonumber(panel.spacingBox:GetText()) or conf.spacing or 2 + conf.wrapAfter = tonumber(panel.wrapBox:GetText()) or conf.wrapAfter or 5 + conf.growthDirection = panel.growthValue or conf.growthDirection or "DOWN" + + SetCategoryOverride(currentScope, "bars", panel.barsOverrideCB:GetChecked() == true) + SetCategoryOverride(currentScope, "font", panel.fontOverrideCB:GetChecked() == true) + SetCategoryOverride(currentScope, "aura", panel.auraOverrideCB:GetChecked() == true) + + WriteGroupValue(currentScope, "bars", "showPowerBar", panel.powerModeBtn.value or "HEALER") + WriteGroupValue(currentScope, "bars", "powerBarHeight", tonumber(panel.powerHeightBox:GetText()) or 3) + WriteGroupValue(currentScope, "font", "showName", panel.showNameCB:GetChecked() == true) + WriteGroupValue(currentScope, "font", "showHPText", panel.showHPTextCB:GetChecked() == true) + WriteGroupValue(currentScope, "font", "nameSize", tonumber(panel.nameSizeBox:GetText()) or 11) + WriteGroupValue(currentScope, "font", "hpSize", tonumber(panel.hpSizeBox:GetText()) or 10) + WriteGroupValue(currentScope, "aura", "maxBuffs", tonumber(panel.maxBuffsBox:GetText()) or 3) + WriteGroupValue(currentScope, "aura", "maxDebuffs", tonumber(panel.maxDebuffsBox:GetText()) or 3) + WriteGroupValue(currentScope, "aura", "iconSize", tonumber(panel.iconSizeBox:GetText()) or 16) + WriteGroupValue(currentScope, "aura", "excludeSated", panel.excludeSatedCB:GetChecked() == true) + SaveDesigner(currentScope, panel.designerEdit:GetText() or "") + + RefreshRuntime() + _G.MSUF_SelectGroupOptionsScope(currentScope) +end + +function _G.MSUF_EnsureGroupOptionsPanelBuilt() + if panel then return panel end + panel = CreateFrame("Frame", "MSUF_GroupOptionsPanel", UIParent) + panel:SetSize(760, 840) + panel:Hide() + panel.title = MakeLabel(panel, "Party Frames", 16, -16) + + local function makeScope(label, scope, x) + local b = CreateFrame("Button", nil, panel) + b:SetSize(80, 22) + b:SetPoint("TOPLEFT", panel, "TOPLEFT", x, -12) + b._label = b:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + b._label:SetPoint("CENTER") + b._label:SetText(label) + b:SetScript("OnClick", function() _G.MSUF_SelectGroupOptionsScope(scope) end) + scopeButtons[scope] = b + return b + end + makeScope("Party", "party", 520) + makeScope("Raid", "raid", 608) + + panel.enableCB = MakeCheck(panel, "Enable Party/Raid Frames", 20, -50, Apply) + panel.hideCB = MakeCheck(panel, "Hide Blizzard Party/Raid Frames", 20, -78, Apply) + + MakeLabel(panel, "X", 20, -126) + panel.xBox = MakeBox(panel, 80, -120) + MakeLabel(panel, "Y", 160, -126) + panel.yBox = MakeBox(panel, 220, -120) + MakeLabel(panel, "Width", 20, -160) + panel.wBox = MakeBox(panel, 80, -154) + MakeLabel(panel, "Height", 160, -160) + panel.hBox = MakeBox(panel, 220, -154) + MakeLabel(panel, "Spacing", 20, -194) + panel.spacingBox = MakeBox(panel, 80, -188) + panel.wrapLabel = MakeLabel(panel, "Wrap", 160, -194) + panel.wrapBox = MakeBox(panel, 220, -188) + + panel.growthValue = "DOWN" + panel.growthBtn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") + panel.growthBtn:SetSize(120, 24) + panel.growthBtn:SetPoint("TOPLEFT", panel, "TOPLEFT", 20, -228) + panel.growthBtn:SetText("Growth: DOWN") + panel.growthBtn:SetScript("OnClick", function(self) + local order = { "DOWN", "UP", "RIGHT", "LEFT" } + local current = panel.growthValue or "DOWN" + local nextIndex = 1 + for i, v in ipairs(order) do + if v == current then nextIndex = (i % #order) + 1 break end + end + panel.growthValue = order[nextIndex] + self:SetText("Growth: " .. panel.growthValue) + Apply() + end) + + MakeLabel(panel, "Bars Override", 20, -278) + panel.barsOverrideCB = MakeCheck(panel, "Override shared bar settings", 20, -298, Apply) + panel.powerModeBtn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") + panel.powerModeBtn:SetSize(140, 24) + panel.powerModeBtn:SetPoint("TOPLEFT", panel, "TOPLEFT", 20, -328) + panel.powerModeBtn.value = "HEALER" + panel.powerModeBtn:SetScript("OnClick", function(self) + local order = { "HEALER", "ALL", "NONE" } + local current = self.value or "HEALER" + local nextIndex = 1 + for i, v in ipairs(order) do + if v == current then nextIndex = (i % #order) + 1 break end + end + self.value = order[nextIndex] + self:SetText("Power: " .. self.value) + Apply() + end) + MakeLabel(panel, "Power Height", 180, -334) + panel.powerHeightBox = MakeBox(panel, 270, -328) + + MakeLabel(panel, "Font Override", 20, -378) + panel.fontOverrideCB = MakeCheck(panel, "Override shared font settings", 20, -398, Apply) + panel.showNameCB = MakeCheck(panel, "Show Name", 20, -428, Apply) + panel.showHPTextCB = MakeCheck(panel, "Show HP Text", 140, -428, Apply) + MakeLabel(panel, "Name Size", 20, -462) + panel.nameSizeBox = MakeBox(panel, 100, -456) + MakeLabel(panel, "HP Size", 180, -462) + panel.hpSizeBox = MakeBox(panel, 250, -456) + + MakeLabel(panel, "Aura Override", 20, -506) + panel.auraOverrideCB = MakeCheck(panel, "Override shared aura settings", 20, -526, Apply) + MakeLabel(panel, "Max Buffs", 20, -560) + panel.maxBuffsBox = MakeBox(panel, 100, -554) + MakeLabel(panel, "Max Debuffs", 180, -560) + panel.maxDebuffsBox = MakeBox(panel, 280, -554) + MakeLabel(panel, "Icon Size", 360, -560) + panel.iconSizeBox = MakeBox(panel, 430, -554) + panel.excludeSatedCB = MakeCheck(panel, "Exclude Sated / Exhaustion", 20, -590, Apply) + + MakeLabel(panel, "Aura Groups / Whitelist (group frames only)", 20, -630) + local designerDesc = panel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + designerDesc:SetPoint("TOPLEFT", panel, "TOPLEFT", 20, -648) + designerDesc:SetWidth(700) + designerDesc:SetJustifyH("LEFT") + designerDesc:SetText("Create spell groups like VuhDo/Grid2-style bouquet lists. One line = one group. Only the first matching aura from each group is shown once on the frame.") + local designerBG = CreateFrame("Frame", nil, panel, "BackdropTemplate") + designerBG:SetPoint("TOPLEFT", panel, "TOPLEFT", 20, -690) + designerBG:SetSize(500, 128) + designerBG:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8", edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1 }) + designerBG:SetBackdropColor(0.05, 0.05, 0.05, 0.85) + designerBG:SetBackdropBorderColor(0.2, 0.2, 0.2, 1) + panel.designerEdit = CreateFrame("EditBox", nil, designerBG) + panel.designerEdit:SetMultiLine(true) + panel.designerEdit:SetFontObject("GameFontHighlightSmall") + panel.designerEdit:SetPoint("TOPLEFT", designerBG, "TOPLEFT", 6, -6) + panel.designerEdit:SetPoint("BOTTOMRIGHT", designerBG, "BOTTOMRIGHT", -6, 6) + panel.designerEdit:SetAutoFocus(false) + panel.designerEdit:SetScript("OnEscapePressed", function(self) self:ClearFocus() end) + + local applyBtn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") + applyBtn:SetSize(120, 24) + applyBtn:SetPoint("TOPLEFT", panel, "TOPLEFT", 540, -690) + applyBtn:SetText("Apply") + applyBtn:SetScript("OnClick", Apply) + + local resetDesignerBtn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") + resetDesignerBtn:SetSize(120, 24) + resetDesignerBtn:SetPoint("TOPLEFT", panel, "TOPLEFT", 540, -722) + resetDesignerBtn:SetText("Reset groups") + resetDesignerBtn:SetScript("OnClick", function() + panel.designerEdit:SetText("") + Apply() + end) + + local copyBtn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") + copyBtn:SetSize(160, 24) + copyBtn:SetPoint("TOPLEFT", panel, "TOPLEFT", 540, -754) + copyBtn:SetText("Copy overrides to other") + copyBtn:SetScript("OnClick", function() + local g = GroupDB() + local srcKey = currentScope + local dstKey = (currentScope == "party") and "raid" or "party" + g[dstKey].overrides.bars = DeepCopy(g[srcKey].overrides.bars or {}) + g[dstKey].overrides.font = DeepCopy(g[srcKey].overrides.font or {}) + g[dstKey].overrides.aura = DeepCopy(g[srcKey].overrides.aura or {}) + Apply() + end) + + local designerHint = panel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + designerHint:SetPoint("TOPLEFT", panel, "TOPLEFT", 540, -790) + designerHint:SetWidth(190) + designerHint:SetJustifyH("LEFT") + designerHint:SetJustifyV("TOP") + designerHint:SetText("Format:\nExternal=102342,6940,33206\nHaste=2825,32182,80353\nDefensive=97462,6940") + + panel.designerStatus = panel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + panel.designerStatus:SetPoint("TOPLEFT", designerBG, "BOTTOMLEFT", 0, -8) + panel.designerStatus:SetWidth(500) + panel.designerStatus:SetJustifyH("LEFT") + panel.designerStatus:SetText("") + + local function bindApply(editBox) + editBox:SetScript("OnEnterPressed", function(self) self:ClearFocus(); Apply() end) + end + for _, box in ipairs({ panel.xBox, panel.yBox, panel.wBox, panel.hBox, panel.spacingBox, panel.wrapBox, panel.powerHeightBox, panel.nameSizeBox, panel.hpSizeBox, panel.maxBuffsBox, panel.maxDebuffsBox, panel.iconSizeBox }) do + bindApply(box) + end + + panel:SetScript("OnShow", function() _G.MSUF_SelectGroupOptionsScope(currentScope) end) + panel.__MSUF_MirrorHeaderTargets = { panel.title } + return panel +end