From 946350550bea41a4054b5730bd1ae4796dbd0d74 Mon Sep 17 00:00:00 2001 From: Daniel Vernon Date: Tue, 17 Mar 2026 01:10:32 +0000 Subject: [PATCH 01/20] Add party frames design spec Spec for configurable party frames using oUF SpawnHeader with existing component builders shared via ns namespace. Covers frame anatomy, header layout architecture, settings structure, file organization, CDM integration, and phased implementation plan. --- .../specs/2026-03-17-party-frames-design.md | 240 ++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-17-party-frames-design.md diff --git a/docs/superpowers/specs/2026-03-17-party-frames-design.md b/docs/superpowers/specs/2026-03-17-party-frames-design.md new file mode 100644 index 00000000..0fa5337f --- /dev/null +++ b/docs/superpowers/specs/2026-03-17-party-frames-design.md @@ -0,0 +1,240 @@ +# Party Frames Design Spec + +## Overview + +Add configurable party frames to EllesmereUIUnitFrames, built on oUF's `SpawnHeader` system. Party frames display party1-4 (optionally including the player) with the same component builder pattern used by existing unit frames. The architecture is designed to extend to raid frames in the future without structural changes. + +## Requirements + +- Configurable for healer or DPS use cases via settings +- Party only (4 members + optional player), raid-ready architecture +- Player inclusion in party group is toggleable +- Every visual component is independently toggleable +- Growth direction: vertical or horizontal +- Sort order: role, group index, or alphabetical +- Uses existing drag-to-position system for the header anchor +- Lives in a new file within the existing `EllesmereUIUnitFrames` addon +- Component builders shared via addon namespace (`ns`) + +## Architecture + +### Approach: Hybrid Header + +Use `oUF:SpawnHeader()` for group lifecycle management (create/destroy/sort/visibility) with a custom `StylePartyFrame` function that reuses existing component builders for appearance. + +- Blizzard's `SecureGroupHeaderTemplate` handles combat lockdown, group join/leave, and sorting +- `StylePartyFrame` calls the same `CreateHealthBar`, `CreatePowerBar`, etc. used by other frames +- Layout changes (sort, growth, spacing) require out-of-combat — standard for all group frame addons + +### Frame Anatomy + +Each party member frame contains (all toggleable): + +| Component | Builder | Default | +|---|---|---| +| Health Bar | `CreateHealthBar` (existing) | On | +| Absorb Bar | `CreateAbsorbBar` (existing) | On | +| Power Bar | `CreatePowerBar` (existing) | On, below | +| Portrait | `CreatePortrait` (existing) | Off | +| Castbar | `CreateCastBar` (existing) | Off | +| Name/Health Text | oUF Tag system (existing) | Name left, HP% right | +| Border | `CreateUnifiedBorder` (existing) | On | +| Auras/Debuffs | New party-specific filter (not `CreateTargetAuras` — party needs debuff-priority filtering with dispel highlights, which differs from the target's full aura display) | Debuffs on, buffs off | +| Role Icon | New | On | +| Range Fading | oUF Range element | On | +| Threat Indicator | oUF ThreatIndicator | On | +| Ready Check | oUF ReadyCheckIndicator | Always on | +| Leader/Assist | oUF LeaderIndicator | Always on | + +Layout: portrait is optional on the left side. Name and role icon sit on the health bar to the right of the portrait (or at the left edge when portrait is off). Health percentage on the far right of the health bar. + +### Header Spawn Pattern + +```lua +oUF:RegisterStyle("EllesmereParty", StylePartyFrame) +oUF:SetActiveStyle("EllesmereParty") + +partyHeader = oUF:SpawnHeader( + "EllesmereUIPartyHeader", + nil, + "custom [@party1,exists] show;hide", + "showPlayer", settings.showPlayer, + "showParty", true, + "showSolo", false, + "point", growthPoint, -- "TOP" or "LEFT" + "xOffset", xOff, + "yOffset", yOff, + "groupBy", "ASSIGNEDROLE", + "groupingOrder", "TANK,HEALER,DAMAGER" +) + +ApplyFramePosition(partyHeader, "party") +``` + +Layout settings map directly to header attributes: + +| Setting | Header Attribute | +|---|---| +| `growthDirection` | `point` + `xOffset`/`yOffset` | +| `sortOrder` | `groupBy` + `groupingOrder` | +| `spacing` | `xOffset`/`yOffset` value | +| `showPlayer` | `showPlayer` | +| Position | `ApplyFramePosition` (existing system) | + +### Settings Structure + +New `party` sub-table in `db.profile`: + +```lua +party = { + -- Frame dimensions + frameWidth = 160, + healthHeight = 36, + + -- Power bar + powerPosition = "below", + powerHeight = 4, + + -- Text (oUF tags) + leftTextContent = "name", + rightTextContent = "perhp", + centerTextContent = "none", + textSize = 11, + + -- Bar opacity + healthBarOpacity = 90, + powerBarOpacity = 100, + + -- Portrait + showPortrait = false, + + -- Party-specific components + showRoleIcon = true, + showCastbar = false, + showThreat = true, + enableRangeFade = true, + rangeFadeAlpha = 0.4, + + -- Auras + showDebuffs = true, + maxDebuffs = 3, + showBuffs = false, + maxBuffs = 0, + highlightDispellable = true, + + -- Layout (header attributes) + growthDirection = "vertical", + sortOrder = "role", + spacing = 1, + showPlayer = false, +}, +``` + +Shared settings (`borderSize`, `borderColor`, `healthBarTexture`, `darkTheme`, `portraitMode`) are read from top-level `db.profile`, same as boss frames. + +`enabledFrames.party` controls the master toggle. `positions.party` stores the header anchor. + +### File Structure + +``` +EllesmereUIUnitFrames/ +├── Libs/oUF/oUF.xml +├── EllesmereUIUnitFrames.lua ← modified (defaults, mappings, ns exports) +├── EllesmereUIPartyFrames.lua ← NEW (~300-400 lines) +├── EUI_UnitFrames_Options.lua ← modified (party options panel) +└── EllesmereUIUnitFrames.toc ← add EllesmereUIPartyFrames.lua +``` + +### Changes to Existing Files + +**`EllesmereUIUnitFrames.lua`:** +- Add `party` defaults to `defaults.profile` +- Add `party = true` to `enabledFrames` +- Add `party` position to `positions` +- Extend `UnitToSettingsKey`: add `if unit:match("^party%d$") then return "party" end` **before** the `db.profile[unit]` fallback probe (line 615), otherwise `party1`–`party4` will return nil since they are not keys in `db.profile` +- Extend `GetSettingsForUnit` map: add `party = db.profile.party` to `unitSettingsMap`. Note: this map is lazily cached — either nil out `unitSettingsMap` after extending it, or add the party entries at the same point where boss entries are built +- Expose component builders via `ns` (complete list): + - `ns.CreateHealthBar` + - `ns.CreateAbsorbBar` + - `ns.CreatePowerBar` + - `ns.CreatePortrait` + - `ns.CreateCastBar` + - `ns.CreateUnifiedBorder` + - `ns.ReparentBarsToClip` (required — every style function calls this for the overflow clip fix) + - `ns.UpdateBordersForScale` (called after border creation in every style function) + - `ns.ApplyFramePosition` + - `ns.SetFSFont` + - `ns.ContentToTag` + - `ns.EstimateUFTextWidth` + - `ns.GetSettingsForUnit` + - `ns.SetupUnitMenu` +- Call `SpawnPartyHeader()` from spawn section +- Register party header with `EllesmereUI.RegisterUnlockElements` for Unlock Mode drag support + +**`.toc` file:** +- Add `EllesmereUIPartyFrames.lua` after `EllesmereUIUnitFrames.lua` + +### New File: `EllesmereUIPartyFrames.lua` + +Contents: +- `StylePartyFrame(frame, unit)` — style function using `ns.*` builders +- `SpawnPartyHeader()` — creates oUF header with settings-driven attributes +- `UpdatePartyLayout()` — applies layout changes out of combat +- Role icon creation helper (small texture on health bar) +- Party aura filter with dispellable highlight logic +- Range fade setup via oUF Range element +- CDM registration: `EllesmereUI.PartyFrames[unit] = frame` + +### CDM Integration + +Party frames register in `EllesmereUI.PartyFrames` lookup table. `FindPlayerPartyFrame()` in `EllesmereUI.lua` must be modified to check this table first before iterating `PARTY_FRAME_SOURCES`. + +oUF names header child frames as `EllesmereUIPartyHeaderUnitButton1` through `EllesmereUIPartyHeaderUnitButton5`. Two integration options: + +1. **Preferred**: Add a first-check path in `FindPlayerPartyFrame()` that reads `EllesmereUI.PartyFrames` before the `PARTY_FRAME_SOURCES` loop +2. **Alternative**: Add `{ addon = "EllesmereUIUnitFrames", prefix = "EllesmereUIPartyHeaderUnitButton", count = 5 }` to `PARTY_FRAME_SOURCES` + +```lua +EllesmereUI.PartyFrames = EllesmereUI.PartyFrames or {} +-- Updated dynamically as header assigns units to child frames +``` + +**Note:** This requires a code change in `EllesmereUI.lua`, not just the unit frames addon. + +## Implementation Phases + +### Phase 1: Core Frame +- `StylePartyFrame` with health, power, absorb, name text, role icon, border +- `SpawnHeader` with growth direction, sort order, spacing, showPlayer +- DB defaults and `enabledFrames` toggle +- Position system integration + +### Phase 2: Auras & Indicators +- Party-specific debuff display with dispellable highlighting +- Buff tracking (optional) +- Threat indicator +- Range fading +- Ready check / resurrection / leader icons + +### Phase 3: CDM Integration +- Register party frames in `EllesmereUI.PartyFrames` +- Update `FindPlayerPartyFrame()` to check native frames first + +### Phase 4: Polish +- Castbar (toggleable per-member) +- Portrait support (2D/3D/class art modes) +- Options panel for party settings +- Live-update support (change settings without /reload) + +## Behavioral Notes + +- **Solo visibility**: Party frames are hidden when solo (`showSolo = false`). Even with `showPlayer = true`, the header only renders when in a group. This is intentional — the standalone player frame handles the solo case. +- **Combat lockdown**: Layout attribute changes (growth direction, sort order, spacing, showPlayer) require out-of-combat. The `UpdatePartyLayout()` function should queue changes and apply them on `PLAYER_REGEN_ENABLED` if called during combat. +- **Unlock Mode**: The party header anchor is registered with `EllesmereUI.RegisterUnlockElements` so it can be dragged in Unlock Mode like all other frames. Individual child frames are not independently draggable — only the header anchor moves. + +## Non-Goals + +- Raid frames (future, same architecture) +- Arena frames +- Per-member customization (all party members share settings) +- CDM bars on party members (separate feature) From 87343c2cfa85e778a9b63c01a729d033bfc9750e Mon Sep 17 00:00:00 2001 From: Daniel Vernon Date: Tue, 17 Mar 2026 01:19:03 +0000 Subject: [PATCH 02/20] Add party frames implementation plan 9-task plan covering defaults, settings mappings, ns exports, StylePartyFrame, SpawnPartyHeader, Unlock Mode, and CDM integration. Reviewed and fixed: WoW API correctness, ns.PP load-time availability, RegisterStyle guard, GetSettingsForUnit cache placement. --- .../plans/2026-03-17-party-frames.md | 954 ++++++++++++++++++ 1 file changed, 954 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-17-party-frames.md diff --git a/docs/superpowers/plans/2026-03-17-party-frames.md b/docs/superpowers/plans/2026-03-17-party-frames.md new file mode 100644 index 00000000..e71832de --- /dev/null +++ b/docs/superpowers/plans/2026-03-17-party-frames.md @@ -0,0 +1,954 @@ +# Party Frames Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add configurable party frames to EllesmereUIUnitFrames using oUF SpawnHeader, reusing existing component builders via the addon namespace. + +**Architecture:** Hybrid Header — oUF:SpawnHeader manages group lifecycle (create/destroy/sort/visibility), while a new StylePartyFrame function reuses existing component builders (CreateHealthBar, CreatePowerBar, etc.) exposed via the `ns` namespace table. New code lives in EllesmereUIPartyFrames.lua, loaded after the main file. + +**Tech Stack:** Lua (WoW addon), oUF unit frame framework, Blizzard SecureGroupHeaderTemplate + +**Spec:** `docs/superpowers/specs/2026-03-17-party-frames-design.md` + +--- + +## File Map + +| File | Action | Responsibility | +|---|---|---| +| `EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua` | Modify | Add party defaults, extend UnitToSettingsKey/GetSettingsForUnit, expose builders via ns, call SpawnPartyHeader, register Unlock Mode | +| `EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua` | Create | StylePartyFrame, SpawnPartyHeader, UpdatePartyLayout, role icon, aura filter, range fade, CDM registration | +| `EllesmereUIUnitFrames/EllesmereUIUnitFrames.toc` | Modify | Add EllesmereUIPartyFrames.lua to load order | +| `EllesmereUI.lua` | Modify | Add EllesmereUI party frames to PARTY_FRAME_SOURCES for CDM integration | + +--- + +## Chunk 1: Foundation — Defaults, Mappings, and ns Exports + +### Task 1: Add party defaults and enabledFrames entry + +**Files:** +- Modify: `EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua:476-498` + +- [ ] **Step 1: Add `party = true` to `enabledFrames`** + +In `defaults.profile.enabledFrames` (line 477), add `party = true` after `boss = true`: + +```lua +enabledFrames = { + player = true, + target = true, + focus = true, + pet = true, + targettarget = true, + focustarget = false, + boss = true, + party = true, +}, +``` + +- [ ] **Step 2: Add `party` position to `positions`** + +In `defaults.profile.positions` (line 486), add after the `classPower` entry: + +```lua +party = { point = "TOPLEFT", x = 20, y = -40 }, +``` + +- [ ] **Step 3: Add the full `party` defaults sub-table** + +After the `boss` defaults sub-table (around line 476, before `enabledFrames`) add: + +```lua +party = { + frameWidth = 160, + healthHeight = 36, + powerPosition = "below", + powerHeight = 4, + leftTextContent = "name", + rightTextContent = "perhp", + centerTextContent = "none", + textSize = 11, + healthBarOpacity = 90, + powerBarOpacity = 100, + showPortrait = false, + showRoleIcon = true, + showCastbar = false, + showThreat = true, + enableRangeFade = true, + rangeFadeAlpha = 0.4, + showDebuffs = true, + maxDebuffs = 3, + showBuffs = false, + maxBuffs = 0, + highlightDispellable = true, + growthDirection = "vertical", + sortOrder = "role", + spacing = 1, + showPlayer = false, +}, +``` + +- [ ] **Step 4: Add "party" to the opacity normalization list in ReloadFrames** + +In `ReloadFrames()` (line 3981), add `"party"` to the UNITS table: + +```lua +local UNITS = { "player", "target", "focus", "boss", "pet", "totPet", "party" } +``` + +- [ ] **Step 5: Commit** + +```bash +git add EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua +git commit -m "feat(party): add party frame defaults and enabledFrames entry" +``` + +--- + +### Task 2: Extend UnitToSettingsKey and GetSettingsForUnit + +**Files:** +- Modify: `EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua:610-868` + +- [ ] **Step 1: Add party pattern to UnitToSettingsKey** + +At line 612, **before** the `db.profile[unit]` fallback (line 615), add the party pattern match: + +```lua +local function UnitToSettingsKey(unit) + if not unit then return nil end + if unit:match("^boss%d$") then return "boss" end + if unit == "targettarget" or unit == "focustarget" then return "totPet" end + if unit == "pet" then return "pet" end + if unit:match("^party%d$") then return "party" end + if db.profile[unit] then return unit end + return nil +end +``` + +- [ ] **Step 2: Add party entries to GetSettingsForUnit** + +In `GetSettingsForUnit` (line 856), add party entries **inside** the `if not unitSettingsMap then` lazy-init block, right after the boss loop (line 866): + +```lua + for i = 1, 5 do + unitSettingsMap["boss" .. i] = db.profile.boss + end + for i = 1, 4 do + unitSettingsMap["party" .. i] = db.profile.party + end + unitSettingsMap["party"] = db.profile.party + end + return unitSettingsMap[unit] or db.profile.player +``` + +This ensures party entries are part of the initial cache build, not appended after. + +- [ ] **Step 3: Commit** + +```bash +git add EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua +git commit -m "feat(party): extend UnitToSettingsKey and GetSettingsForUnit for party units" +``` + +--- + +### Task 3: Expose component builders via ns namespace + +**Files:** +- Modify: `EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua` + +The component builders are all `local function` declarations. We need to expose them on the `ns` table so the party frames file can access them. Add these assignments **after** all the builder function definitions but **before** `RegisterStylesOnce()` (around line 3459). + +- [ ] **Step 1: Add ns exports block** + +Find the area just before `RegisterStylesOnce()` (line 3459) and add: + +```lua +-- Expose builders for party/raid frame files +ns.CreateHealthBar = CreateHealthBar +ns.CreateAbsorbBar = CreateAbsorbBar +ns.CreatePowerBar = CreatePowerBar +ns.CreatePortrait = CreatePortrait +ns.CreateCastBar = CreateCastBar +ns.CreateUnifiedBorder = CreateUnifiedBorder +ns.ReparentBarsToClip = ReparentBarsToClip +ns.UpdateBordersForScale = UpdateBordersForScale +ns.ApplyFramePosition = ApplyFramePosition +ns.SetFSFont = SetFSFont +ns.ContentToTag = ContentToTag +ns.EstimateUFTextWidth = EstimateUFTextWidth +ns.GetSettingsForUnit = GetSettingsForUnit +ns.SetupUnitMenu = nil -- will be set later inside spawn function +ns.GetCastbarColor = GetCastbarColor +ns.ApplyHealthBarTexture = ApplyHealthBarTexture +ns.ApplyDarkTheme = ApplyDarkTheme +ns.ApplyHealthBarAlpha = ApplyHealthBarAlpha +``` + +Note: `SetupUnitMenu` is defined as a local inside the spawn function scope (line 5403). It needs to be exported from there. Add this line inside the spawn function after `SetupUnitMenu` is defined (after line 5408): + +```lua +ns.SetupUnitMenu = SetupUnitMenu +``` + +Also expose the `db`, `frames`, and `oUF` references the party file will need: + +**Important:** `ns.PP` must be available at file-load time for the party file. Add this near the top of the file (after line 4, `local PP = EllesmereUI.PP`): + +```lua +ns.PP = PP +``` + +Then add the remaining shared state exports in the same block near `RegisterStylesOnce()`: + +```lua +ns.db = nil -- set after DB init +ns.frames = frames +``` + +Then after `db = EUILite.NewDB(...)` runs (find the DB init line), add: + +```lua +ns.db = db +``` + +- [ ] **Step 2: Commit** + +```bash +git add EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua +git commit -m "feat(party): expose component builders and shared state via ns namespace" +``` + +--- + +### Task 4: Update .toc file + +**Files:** +- Modify: `EllesmereUIUnitFrames/EllesmereUIUnitFrames.toc` + +- [ ] **Step 1: Add party frames file to load order** + +Add `EllesmereUIPartyFrames.lua` after the main file: + +``` +# oUF +Libs\oUF\oUF.xml + +# Main Luas +EllesmereUIUnitFrames.lua +EllesmereUIPartyFrames.lua + +# Options +EUI_UnitFrames_Options.lua +``` + +- [ ] **Step 2: Commit** + +```bash +git add EllesmereUIUnitFrames/EllesmereUIUnitFrames.toc +git commit -m "feat(party): add EllesmereUIPartyFrames.lua to toc load order" +``` + +--- + +## Chunk 2: Core Party Frames File — StylePartyFrame and SpawnPartyHeader + +### Task 5: Create EllesmereUIPartyFrames.lua with StylePartyFrame + +**Files:** +- Create: `EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua` + +This is the core new file. It accesses all builders through `ns.*`. + +- [ ] **Step 1: Write the complete party frames file** + +Create `EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua` with the following content: + +```lua +local addonName, ns = ... + +local oUF = ns.oUF +local PP = ns.PP + +---------------------------------------------------------------------- +-- Helpers +---------------------------------------------------------------------- + +-- Map sortOrder setting to SecureGroupHeader attributes +local SORT_CONFIGS = { + role = { groupBy = "ASSIGNEDROLE", groupingOrder = "TANK,HEALER,DAMAGER" }, + group = { groupBy = nil, groupingOrder = nil }, + alphabetical = { groupBy = nil, groupingOrder = nil, sortMethod = "NAME" }, +} + +-- Map growthDirection to header point/offset +local function GetGrowthAttributes(direction, spacing) + if direction == "horizontal" then + return "LEFT", spacing, 0 + else -- vertical (default) + return "TOP", 0, -spacing + end +end + +---------------------------------------------------------------------- +-- Role Icon +---------------------------------------------------------------------- + +local ROLE_TEXCOORDS = { + TANK = { 0, 19/64, 22/64, 41/64 }, + HEALER = { 20/64, 39/64, 1/64, 20/64 }, + DAMAGER = { 20/64, 39/64, 22/64, 41/64 }, +} + +local function CreateRoleIcon(frame, settings) + local icon = frame:CreateTexture(nil, "OVERLAY") + local sz = math.max(math.floor((settings.textSize or 11) + 2), 10) + PP.Size(icon, sz, sz) + icon:SetTexture("Interface\\LFGFrame\\UI-LFG-ICON-PORTRAITROLES") + icon:Hide() + + frame._roleIcon = icon + return icon +end + +local function UpdateRoleIcon(frame) + local icon = frame._roleIcon + if not icon then return end + + local db = ns.db + local settings = db and db.profile and db.profile.party + if not settings or settings.showRoleIcon == false then + icon:Hide() + return + end + + local unit = frame.unit or frame:GetAttribute("unit") + if not unit then icon:Hide(); return end + + local role = UnitGroupRolesAssigned(unit) + local coords = ROLE_TEXCOORDS[role] + if coords then + icon:SetTexCoord(unpack(coords)) + icon:Show() + else + icon:Hide() + end +end + +---------------------------------------------------------------------- +-- Party Aura Filter +---------------------------------------------------------------------- + +local function PartyAuraFilter(element, unit, data) + -- Prioritize debuffs the player can dispel + if data.isDebuff then + return true + end + return false +end + +local function CreatePartyAuras(frame, settings) + if not settings.showDebuffs and not settings.showBuffs then return end + + if settings.showDebuffs then + local debuffs = CreateFrame("Frame", nil, frame) + debuffs:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 1, 1) + debuffs.size = math.floor(settings.healthHeight * 0.5) + debuffs.num = settings.maxDebuffs or 3 + debuffs["growth-x"] = "RIGHT" + debuffs.FilterAura = PartyAuraFilter + + debuffs.PostCreateButton = function(self, button) + button.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + -- Dispellable glow + if not button._dispelGlow then + local glow = button:CreateTexture(nil, "OVERLAY") + glow:SetAllPoints() + glow:SetColorTexture(1, 1, 1, 0) + button._dispelGlow = glow + end + end + + debuffs.PostUpdateButton = function(self, button, unit, data) + if button._dispelGlow then + local db = ns.db + local s = db and db.profile and db.profile.party + if s and s.highlightDispellable and data.isDebuff and data.dispelName then + button._dispelGlow:SetColorTexture(0, 0.8, 1, 0.3) + else + button._dispelGlow:SetColorTexture(1, 1, 1, 0) + end + end + end + + frame.Debuffs = debuffs + end + + if settings.showBuffs and (settings.maxBuffs or 0) > 0 then + local buffs = CreateFrame("Frame", nil, frame) + buffs:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -1, 1) + buffs.size = math.floor(settings.healthHeight * 0.5) + buffs.num = settings.maxBuffs or 0 + buffs["growth-x"] = "LEFT" + buffs.PostCreateButton = function(self, button) + button.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + end + frame.Buffs = buffs + end +end + +---------------------------------------------------------------------- +-- StylePartyFrame +---------------------------------------------------------------------- + +local function StylePartyFrame(frame, unit) + local db = ns.db + if not db then return end + + local settings = db.profile.party + if not settings then return end + + local ppPos = settings.powerPosition or "below" + local ppIsAtt = (ppPos == "below" or ppPos == "above") + local powerHeight = ppIsAtt and (settings.powerHeight or 4) or 0 + local totalHeight = settings.healthHeight + powerHeight + local totalWidth = settings.frameWidth + + -- Portrait adds width when visible + local showPortrait = settings.showPortrait ~= false + and (db.profile.portraitStyle or "attached") ~= "none" + if showPortrait then + totalWidth = totalHeight + settings.frameWidth + end + + PP.Size(frame, totalWidth, totalHeight) + + -- Health bar + local healthRightInset = showPortrait and totalHeight or 0 + frame.Health = ns.CreateHealthBar(frame, unit, settings.healthHeight, 0, settings, healthRightInset) + + -- Absorb bar + ns.CreateAbsorbBar(frame, unit, settings) + + -- Power bar + if ppPos ~= "none" then + frame.Power = ns.CreatePowerBar(frame, unit, settings) + end + + -- Portrait (always create, hide backdrop when disabled — same pattern as boss) + frame.Portrait = ns.CreatePortrait(frame, "left", totalHeight, unit) + frame._portraitSide = "left" + if frame.Portrait and not showPortrait then + frame.Portrait.backdrop:Hide() + end + + -- Re-anchor health bar to portrait's snapped width (same fix as boss frames) + if frame.Portrait and frame.Portrait.backdrop and showPortrait and frame.Health then + local snappedPortW = frame.Portrait.backdrop:GetWidth() + local powerAboveOff = (ppPos == "above") and (settings.powerHeight or 4) or 0 + frame.Health:ClearAllPoints() + PP.Point(frame.Health, "TOPLEFT", frame, "TOPLEFT", snappedPortW, -powerAboveOff) + PP.Point(frame.Health, "RIGHT", frame, "RIGHT", 0, 0) + PP.Height(frame.Health, settings.healthHeight) + frame.Health._xOffset = snappedPortW + frame.Health._rightInset = 0 + frame.Health._topOffset = powerAboveOff + end + + -- Border + ns.CreateUnifiedBorder(frame, unit) + ns.UpdateBordersForScale(frame, unit) + + -- Clip bars to prevent overflow + ns.ReparentBarsToClip(frame) + + -- Text overlay + local textOverlay = CreateFrame("Frame", nil, frame.Health) + textOverlay:SetAllPoints(frame.Health) + textOverlay:SetFrameLevel(frame.Health:GetFrameLevel() + 12) + frame._textOverlay = textOverlay + + local ts = settings.textSize or 11 + local leftContent = settings.leftTextContent or "name" + local rightContent = settings.rightTextContent or "perhp" + local centerContent = settings.centerTextContent or "none" + + local leftText = textOverlay:CreateFontString(nil, "OVERLAY") + ns.SetFSFont(leftText, ts) + leftText:SetWordWrap(false) + leftText:SetTextColor(1, 1, 1) + frame.LeftText = leftText + + local rightText = textOverlay:CreateFontString(nil, "OVERLAY") + ns.SetFSFont(rightText, ts) + rightText:SetWordWrap(false) + rightText:SetTextColor(1, 1, 1) + frame.RightText = rightText + + local centerText = textOverlay:CreateFontString(nil, "OVERLAY") + ns.SetFSFont(centerText, ts) + centerText:SetWordWrap(false) + centerText:SetTextColor(1, 1, 1) + frame.CenterText = centerText + + frame.NameText = leftText + frame.HealthValue = rightText + + -- Tag system (same pattern as boss frames) + local function ApplyTextTags(lc, rc, cc) + local ltag = ns.ContentToTag(lc) + local rtag = ns.ContentToTag(rc) + local ctag = ns.ContentToTag(cc) + if leftText._curTag then frame:Untag(leftText); leftText._curTag = nil end + if rightText._curTag then frame:Untag(rightText); rightText._curTag = nil end + if centerText._curTag then frame:Untag(centerText); centerText._curTag = nil end + if ltag then frame:Tag(leftText, ltag); leftText._curTag = ltag end + if rtag then frame:Tag(rightText, rtag); rightText._curTag = rtag end + if ctag then frame:Tag(centerText, ctag); centerText._curTag = ctag end + if frame.UpdateTags then frame:UpdateTags() end + end + ApplyTextTags(leftContent, rightContent, centerContent) + frame._applyTextTags = ApplyTextTags + + -- Text positioning (same pattern as boss frames) + local function ApplyTextPositions(s) + local lc = s.leftTextContent or "name" + local rc = s.rightTextContent or "perhp" + local cc = s.centerTextContent or "none" + local barW = s.frameWidth or 160 + + -- Account for role icon width on the left + local roleOffset = (s.showRoleIcon ~= false) and (ts + 6) or 0 + + if cc ~= "none" then + centerText:ClearAllPoints() + centerText:SetPoint("CENTER", frame.Health, "CENTER", 0, 0) + centerText:SetWidth(0) + centerText:Show() + leftText:Hide(); rightText:Hide() + else + centerText:Hide() + if lc ~= "none" then + leftText:ClearAllPoints() + leftText:SetPoint("LEFT", frame.Health, "LEFT", 5 + roleOffset, 0) + leftText:SetJustifyH("LEFT") + if rc ~= "none" then + local rightUsed = ns.EstimateUFTextWidth(rc) + PP.Width(leftText, math.max(barW - rightUsed - 10 - roleOffset, 20)) + else + leftText:SetWidth(0) + end + leftText:Show() + else leftText:Hide() end + if rc ~= "none" then + rightText:ClearAllPoints() + rightText:SetPoint("RIGHT", frame.Health, "RIGHT", -5, 0) + rightText:SetJustifyH("RIGHT") + if lc ~= "none" then + local leftUsed = ns.EstimateUFTextWidth(lc) + PP.Width(rightText, math.max(barW - leftUsed - 10 - roleOffset, 20)) + else + rightText:SetWidth(0) + end + rightText:Show() + else rightText:Hide() end + end + end + ApplyTextPositions(settings) + frame._applyTextPositions = ApplyTextPositions + + -- Role icon (anchored to left of health bar, before name text) + local roleIcon = CreateRoleIcon(frame, settings) + roleIcon:SetPoint("LEFT", frame.Health, "LEFT", 4, 0) + + -- Auras + CreatePartyAuras(frame, settings) + + -- Range fading + if settings.enableRangeFade ~= false then + frame.Range = { + insideAlpha = 1, + outsideAlpha = settings.rangeFadeAlpha or 0.4, + } + end + + -- Threat indicator (border glow) + if settings.showThreat ~= false then + local threat = frame:CreateTexture(nil, "OVERLAY") + threat:SetAllPoints() + threat:Hide() + frame.ThreatIndicator = threat + end + + -- Ready check + local readyCheck = frame:CreateTexture(nil, "OVERLAY", nil, 7) + readyCheck:SetSize(16, 16) + readyCheck:SetPoint("CENTER", frame, "CENTER", 0, 0) + frame.ReadyCheckIndicator = readyCheck + + -- Leader indicator + local leader = frame:CreateTexture(nil, "OVERLAY", nil, 7) + leader:SetSize(12, 12) + leader:SetPoint("TOPLEFT", frame, "TOPLEFT", 2, -2) + frame.LeaderIndicator = leader + + -- Assistant indicator + local assist = frame:CreateTexture(nil, "OVERLAY", nil, 7) + assist:SetSize(12, 12) + assist:SetPoint("TOPLEFT", frame, "TOPLEFT", 2, -2) + frame.AssistantIndicator = assist + + -- Resurrection indicator + local resurrect = frame:CreateTexture(nil, "OVERLAY", nil, 7) + resurrect:SetSize(20, 20) + resurrect:SetPoint("CENTER", frame, "CENTER", 0, 0) + frame.ResurrectIndicator = resurrect + + -- Summon indicator + local summon = frame:CreateTexture(nil, "OVERLAY", nil, 7) + summon:SetSize(24, 24) + summon:SetPoint("CENTER", frame, "CENTER", 0, 0) + frame.SummonIndicator = summon + + -- Hook for role icon updates + -- Note: WoW Frame:RegisterEvent takes only event name (no callback arg). + -- Use OnEvent script + OnShow hook instead. + frame:RegisterEvent("GROUP_ROSTER_UPDATE") + frame:RegisterEvent("PLAYER_ROLES_ASSIGNED") + frame:HookScript("OnEvent", function(self, event) + if event == "GROUP_ROSTER_UPDATE" or event == "PLAYER_ROLES_ASSIGNED" then + UpdateRoleIcon(self) + end + end) + frame:HookScript("OnShow", function(self) + UpdateRoleIcon(self) + end) + + -- Right-click menu + if ns.SetupUnitMenu then + ns.SetupUnitMenu(frame, unit or "party") + end +end + +---------------------------------------------------------------------- +-- SpawnPartyHeader +---------------------------------------------------------------------- + +local partyHeader + +local function SpawnPartyHeader() + local db = ns.db + if not db then return end + + local settings = db.profile.party + if not settings then return end + + local point, xOff, yOff = GetGrowthAttributes( + settings.growthDirection or "vertical", + settings.spacing or 1 + ) + + -- Sort config + local sortCfg = SORT_CONFIGS[settings.sortOrder or "role"] or SORT_CONFIGS.role + + -- Guard: oUF errors on duplicate style registration (e.g. during ReloadFrames) + if not oUF.styles or not oUF.styles["EllesmereParty"] then + oUF:RegisterStyle("EllesmereParty", StylePartyFrame) + end + oUF:SetActiveStyle("EllesmereParty") + + local headerArgs = { + "showPlayer", settings.showPlayer or false, + "showParty", true, + "showSolo", false, + "point", point, + "xOffset", xOff, + "yOffset", yOff, + "oUF-initialConfigFunction", ([[ + self:SetWidth(%d) + self:SetHeight(%d) + ]]):format(settings.frameWidth or 160, (settings.healthHeight or 36) + ((settings.powerPosition ~= "none") and (settings.powerHeight or 4) or 0)), + } + + -- Add sort attributes + if sortCfg.groupBy then + headerArgs[#headerArgs + 1] = "groupBy" + headerArgs[#headerArgs + 1] = sortCfg.groupBy + end + if sortCfg.groupingOrder then + headerArgs[#headerArgs + 1] = "groupingOrder" + headerArgs[#headerArgs + 1] = sortCfg.groupingOrder + end + if sortCfg.sortMethod then + headerArgs[#headerArgs + 1] = "sortMethod" + headerArgs[#headerArgs + 1] = sortCfg.sortMethod + end + + partyHeader = oUF:SpawnHeader( + "EllesmereUIPartyHeader", + nil, + "custom [@party1,exists] show;hide", + unpack(headerArgs) + ) + + ns.ApplyFramePosition(partyHeader, "party") + + local enabled = db.profile.enabledFrames + if enabled.party == false then + RegisterAttributeDriver(partyHeader, "state-visibility", "hide") + end + + -- Store reference + ns.partyHeader = partyHeader + + return partyHeader +end + +---------------------------------------------------------------------- +-- UpdatePartyLayout (out of combat only) +---------------------------------------------------------------------- + +local function UpdatePartyLayout() + if InCombatLockdown() or not partyHeader then return end + + local db = ns.db + if not db then return end + + local settings = db.profile.party + if not settings then return end + + local point, xOff, yOff = GetGrowthAttributes( + settings.growthDirection or "vertical", + settings.spacing or 1 + ) + + partyHeader:SetAttribute("point", point) + partyHeader:SetAttribute("xOffset", xOff) + partyHeader:SetAttribute("yOffset", yOff) + partyHeader:SetAttribute("showPlayer", settings.showPlayer or false) + + local sortCfg = SORT_CONFIGS[settings.sortOrder or "role"] or SORT_CONFIGS.role + if sortCfg.groupBy then + partyHeader:SetAttribute("groupBy", sortCfg.groupBy) + partyHeader:SetAttribute("groupingOrder", sortCfg.groupingOrder) + end + if sortCfg.sortMethod then + partyHeader:SetAttribute("sortMethod", sortCfg.sortMethod) + end + + ns.ApplyFramePosition(partyHeader, "party") +end + +---------------------------------------------------------------------- +-- Public API +---------------------------------------------------------------------- + +ns.SpawnPartyHeader = SpawnPartyHeader +ns.UpdatePartyLayout = UpdatePartyLayout +ns.StylePartyFrame = StylePartyFrame +``` + +- [ ] **Step 2: Commit** + +```bash +git add EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua +git commit -m "feat(party): create EllesmereUIPartyFrames.lua with StylePartyFrame and SpawnPartyHeader" +``` + +--- + +### Task 6: Call SpawnPartyHeader from main spawn section + +**Files:** +- Modify: `EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua:5919-5920` + +- [ ] **Step 1: Add SpawnPartyHeader call after boss frame spawning** + +After the boss frame spawn loop (line 5919) and the blizzard boss hide loop (line 5927), add: + +```lua + -- Party frames (spawned via header in EllesmereUIPartyFrames.lua) + if ns.SpawnPartyHeader then + ns.SpawnPartyHeader() + end +``` + +- [ ] **Step 2: Commit** + +```bash +git add EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua +git commit -m "feat(party): call SpawnPartyHeader from main spawn section" +``` + +--- + +## Chunk 3: Unlock Mode and CDM Integration + +### Task 7: Register party header with Unlock Mode + +**Files:** +- Modify: `EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua:6228-6392` + +- [ ] **Step 1: Add "party" to UNIT_LABELS** + +In the `UNIT_LABELS` table (line 6230), add `party`: + +```lua +local UNIT_LABELS = { + player = "Player", target = "Target", focus = "Focus", + pet = "Pet", targettarget = "Target of Target", + focustarget = "Focus Target", boss = "Boss Frames", + party = "Party Frames", + classPower = "Class Resource", +} +``` + +- [ ] **Step 2: Add party to element list** + +After the boss element (line 6385), add: + +```lua +elements[#elements + 1] = MakeUFElement("party", 8) +``` + +- [ ] **Step 3: Handle party in getFrame callback** + +In the `getFrame` function (line 6247), add a party case before the default return: + +```lua +if k == "party" then return ns.partyHeader end +``` + +- [ ] **Step 4: Handle party in getSize callback** + +The party header's size for Unlock Mode should return the total group size. In `getSize` (line 6258), add: + +```lua +if k == "party" then + local s = db.profile.party + if not s then return 160, 36 end + local ppPos = s.powerPosition or "below" + local ppIsAtt = (ppPos == "below" or ppPos == "above") + local ph = ppIsAtt and (s.powerHeight or 4) or 0 + local frameH = s.healthHeight + ph + local frameW = s.frameWidth + -- Account for portrait + local showPortrait = s.showPortrait ~= false and (db.profile.portraitStyle or "attached") ~= "none" + if showPortrait then frameW = frameW + frameH end + return frameW, frameH +end +``` + +- [ ] **Step 5: Handle party in savePos callback** + +In the `savePos` function (line 6324), add a party case: + +```lua +elseif k == "party" then + if ns.partyHeader then + ns.partyHeader:ClearAllPoints() + ns.partyHeader:SetPoint(point, UIParent, relPoint, x, y) + end +``` + +- [ ] **Step 6: Handle party in applyPos callback** + +In the `applyPos` function (line 6351), add: + +```lua +elseif k == "party" then + if ns.partyHeader then + ns.partyHeader:ClearAllPoints() + ns.partyHeader:SetPoint(pos.point, UIParent, pos.relPoint or pos.point, pos.x, pos.y) + end +``` + +- [ ] **Step 7: Commit** + +```bash +git add EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua +git commit -m "feat(party): register party header with Unlock Mode" +``` + +--- + +### Task 8: CDM integration — add party frames to FindPlayerPartyFrame + +**Files:** +- Modify: `EllesmereUI.lua:1700-1703` + +- [ ] **Step 1: Add EllesmereUI party frames to PARTY_FRAME_SOURCES** + +At the top of the `PARTY_FRAME_SOURCES` table (line 1700), add our party frames as the first entry so they take priority: + +```lua +local PARTY_FRAME_SOURCES = { + { addon = "EllesmereUIUnitFrames", prefix = "EllesmereUIPartyHeaderUnitButton", count = 5 }, + { addon = "ElvUI", prefix = "ElvUF_PartyGroup1UnitButton", count = 5 }, + { addon = "Cell", prefix = "CellPartyFrameMember", count = 5 }, + { addon = nil, prefix = "CompactPartyFrameMember", count = 5 }, +``` + +- [ ] **Step 2: Commit** + +```bash +git add EllesmereUI.lua +git commit -m "feat(party): add native party frames to CDM FindPlayerPartyFrame sources" +``` + +--- + +## Out of Scope (Phase 4) + +- **Options panel** (`EUI_UnitFrames_Options.lua`) — party settings UI deferred to Phase 4. Settings can be changed via SavedVariables or `/run` commands for testing. +- **Combat-queued layout updates** — `UpdatePartyLayout()` currently returns early in combat. A future enhancement should queue changes and apply on `PLAYER_REGEN_ENABLED`. +- **Castbar** — deferred to Phase 4. +- **`EllesmereUI.PartyFrames` registration table** — deferred to Phase 3. CDM integration currently uses `PARTY_FRAME_SOURCES` prefix matching instead. + +--- + +## Chunk 4: Verification + +### Task 9: Verify addon loads without errors + +- [ ] **Step 1: Verify Lua syntax of the new file** + +```bash +luac -p EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua +``` + +If `luac` is not available, use: + +```bash +lua -e "loadfile('EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua')" 2>&1 +``` + +Expected: no output (no syntax errors). + +- [ ] **Step 2: Verify all modified files parse correctly** + +```bash +luac -p EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua +``` + +Expected: no output (no syntax errors). + +- [ ] **Step 3: Verify the .toc file lists all files in correct order** + +Read the `.toc` and confirm `EllesmereUIPartyFrames.lua` appears after `EllesmereUIUnitFrames.lua` and before `EUI_UnitFrames_Options.lua`. + +- [ ] **Step 4: Review: scan for common issues** + +Grep for potential problems: +- Any reference to `CreateHealthBar` (without `ns.`) in the party file → should all use `ns.CreateHealthBar` +- Any `local db` shadowing in party file that might conflict +- Verify `ns.db` is set before `SpawnPartyHeader` is called + +- [ ] **Step 5: Final commit if any fixes were needed** + +```bash +git add -A +git commit -m "fix(party): address any issues found during verification" +``` From 674b9c22308bae7194a7d4df03663c3baa1b69e2 Mon Sep 17 00:00:00 2001 From: Daniel Vernon Date: Tue, 17 Mar 2026 01:21:20 +0000 Subject: [PATCH 03/20] Add party frame defaults, enabledFrames entry, and position - Add full party defaults sub-table with layout, text, aura, threat, range-fade, growth direction, sort order, and spacing settings - Add party = true to enabledFrames - Add party position to positions table - Include "party" in opacity normalization UNITS list --- .../EllesmereUIUnitFrames.lua | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua index ff618472..7e89b474 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua @@ -501,6 +501,33 @@ local defaults = { borderColor = { r = 0, g = 0, b = 0 }, highlightColor = { r = 1, g = 1, b = 1 }, }, + party = { + frameWidth = 160, + healthHeight = 36, + powerPosition = "below", + powerHeight = 4, + leftTextContent = "name", + rightTextContent = "perhp", + centerTextContent = "none", + textSize = 11, + healthBarOpacity = 90, + powerBarOpacity = 100, + showPortrait = false, + showRoleIcon = true, + showCastbar = false, + showThreat = true, + enableRangeFade = true, + rangeFadeAlpha = 0.4, + showDebuffs = true, + maxDebuffs = 3, + showBuffs = false, + maxBuffs = 0, + highlightDispellable = true, + growthDirection = "vertical", + sortOrder = "role", + spacing = 1, + showPlayer = false, + }, enabledFrames = { player = true, target = true, @@ -509,6 +536,7 @@ local defaults = { targettarget = true, focustarget = false, boss = true, + party = true, }, positions = { player = { point = "CENTER", relPoint = "CENTER", x = -317, y = -193.5 }, @@ -519,6 +547,7 @@ local defaults = { focustarget = { point = "CENTER", relPoint = "CENTER", x = 50, y = -261 }, boss = { point = "CENTER", relPoint = "CENTER", x = 661, y = 251 }, classPower = { point = "CENTER", relPoint = "CENTER", x = 0, y = -220 }, + party = { point = "TOPLEFT", relPoint = "TOPLEFT", x = 20, y = -40 }, }, bossSpacing = 60, } @@ -4004,7 +4033,7 @@ local function ReloadFrames() -- Normalize opacity values: old profiles stored 0-1 floats, new format is 0-100 integers do local prof = db.profile - local UNITS = { "player", "target", "focus", "boss", "pet", "totPet" } + local UNITS = { "player", "target", "focus", "boss", "pet", "totPet", "party" } if prof.healthBarOpacity and prof.healthBarOpacity <= 1.0 then prof.healthBarOpacity = math.floor(prof.healthBarOpacity * 100 + 0.5) end From ba1feef734b92a2c9d248fe07a25d37e4a1b81de Mon Sep 17 00:00:00 2001 From: Daniel Vernon Date: Tue, 17 Mar 2026 01:21:40 +0000 Subject: [PATCH 04/20] Extend UnitToSettingsKey and GetSettingsForUnit for party units - Map "party1"-"party4" to the "party" settings key in UnitToSettingsKey - Add party1-party4 and "party" entries to the lazy-init unitSettingsMap --- EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua index 7e89b474..43a4327b 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua @@ -667,6 +667,7 @@ local function UnitToSettingsKey(unit) if unit:match("^boss%d$") then return "boss" end if unit == "targettarget" or unit == "focustarget" then return "totPet" end if unit == "pet" then return "pet" end + if unit:match("^party%d$") then return "party" end if db.profile[unit] then return unit end return nil end @@ -919,6 +920,10 @@ local function GetSettingsForUnit(unit) for i = 1, 5 do unitSettingsMap["boss" .. i] = db.profile.boss end + for i = 1, 4 do + unitSettingsMap["party" .. i] = db.profile.party + end + unitSettingsMap["party"] = db.profile.party end return unitSettingsMap[unit] or db.profile.player end From ad3856eb69ed2ef72f6f82024675037018303a77 Mon Sep 17 00:00:00 2001 From: Daniel Vernon Date: Tue, 17 Mar 2026 01:22:47 +0000 Subject: [PATCH 05/20] Expose component builders and shared state via ns namespace - Export ns.PP for PixelPerfect access from party/raid files - Export 17 builder/utility functions (CreateHealthBar, CreatePowerBar, CreateCastBar, CreateUnifiedBorder, ApplyFramePosition, etc.) - Set ns.db after DB initialization so party file can access profile - Export ns.frames for cross-file frame references - Export ns.SetupUnitMenu inside spawn function scope --- .../EllesmereUIUnitFrames.lua | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua index 43a4327b..c21281da 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua @@ -6,6 +6,7 @@ local string_format = string.format local oUF = ns.oUF or oUF local PP = EllesmereUI.PP +ns.PP = PP if not oUF then error("EllesmereUIUnitFrames: oUF library not found! Please install oUF to Libraries\\oUF\\ folder.") return @@ -3523,6 +3524,26 @@ local function StyleBossFrame(frame, unit) frame._applyTextPositions = ApplyTextPositions end +-- Expose builders for party/raid frame files +ns.CreateHealthBar = CreateHealthBar +ns.CreateAbsorbBar = CreateAbsorbBar +ns.CreatePowerBar = CreatePowerBar +ns.CreatePortrait = CreatePortrait +ns.CreateCastBar = CreateCastBar +ns.CreateUnifiedBorder = CreateUnifiedBorder +ns.ReparentBarsToClip = ReparentBarsToClip +ns.UpdateBordersForScale = UpdateBordersForScale +ns.ApplyFramePosition = ApplyFramePosition +ns.SetFSFont = SetFSFont +ns.ContentToTag = ContentToTag +ns.EstimateUFTextWidth = EstimateUFTextWidth +ns.GetSettingsForUnit = GetSettingsForUnit +ns.GetCastbarColor = GetCastbarColor +ns.ApplyHealthBarTexture = ApplyHealthBarTexture +ns.ApplyDarkTheme = ApplyDarkTheme +ns.ApplyHealthBarAlpha = ApplyHealthBarAlpha +ns.db = nil +ns.frames = frames local function RegisterStylesOnce() if _G.EllesmereUF_StylesRegistered then @@ -5709,6 +5730,7 @@ function InitializeFrames() frame:HookScript("OnEnter", UnitFrame_OnEnter) frame:HookScript("OnLeave", UnitFrame_OnLeave) end + ns.SetupUnitMenu = SetupUnitMenu -- Always spawn all frames; hide disabled ones for zero performance impact oUF:SetActiveStyle("EllesmerePlayer") @@ -6887,6 +6909,7 @@ local EllesmereUF = EllesmereUI.Lite.NewAddon("EllesmereUIUnitFrames") function EllesmereUF:OnInitialize() db = EllesmereUI.Lite.NewDB("EllesmereUIUnitFramesDB", defaults, true) + ns.db = db ResolveFontPath() From 2238f865126b9810949c70fd0360b6ede4e940b7 Mon Sep 17 00:00:00 2001 From: Daniel Vernon Date: Tue, 17 Mar 2026 01:23:37 +0000 Subject: [PATCH 06/20] feat(party): add EllesmereUIPartyFrames.lua to toc load order --- EllesmereUIUnitFrames/EllesmereUIUnitFrames.toc | 1 + 1 file changed, 1 insertion(+) diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.toc b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.toc index 9f7d40d0..b8f3474e 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.toc +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.toc @@ -15,6 +15,7 @@ Libs\oUF\oUF.xml # Main Luas EllesmereUIUnitFrames.lua +EllesmereUIPartyFrames.lua # Options EUI_UnitFrames_Options.lua From 073bc3c339e17436bfe21fef5ddb7c2b82142047 Mon Sep 17 00:00:00 2001 From: Daniel Vernon Date: Tue, 17 Mar 2026 01:25:04 +0000 Subject: [PATCH 07/20] feat(party): create EllesmereUIPartyFrames.lua with StylePartyFrame and SpawnPartyHeader Core party frames implementation: - StylePartyFrame: health, absorb, power, portrait, border, text, role icon, auras with dispel highlights, range fade, threat, ready check, leader/assist, resurrect, summon indicators - SpawnPartyHeader: oUF header with configurable growth direction, sort order, spacing, and showPlayer toggle - UpdatePartyLayout: out-of-combat layout attribute updates --- .../EllesmereUIPartyFrames.lua | 481 ++++++++++++++++++ 1 file changed, 481 insertions(+) create mode 100644 EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua diff --git a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua new file mode 100644 index 00000000..863fc23c --- /dev/null +++ b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua @@ -0,0 +1,481 @@ +local addonName, ns = ... + +local oUF = ns.oUF +local PP = ns.PP + +---------------------------------------------------------------------- +-- Helpers +---------------------------------------------------------------------- + +-- Map sortOrder setting to SecureGroupHeader attributes +local SORT_CONFIGS = { + role = { groupBy = "ASSIGNEDROLE", groupingOrder = "TANK,HEALER,DAMAGER" }, + group = { groupBy = nil, groupingOrder = nil }, + alphabetical = { groupBy = nil, groupingOrder = nil, sortMethod = "NAME" }, +} + +-- Map growthDirection to header point/offset +local function GetGrowthAttributes(direction, spacing) + if direction == "horizontal" then + return "LEFT", spacing, 0 + else -- vertical (default) + return "TOP", 0, -spacing + end +end + +---------------------------------------------------------------------- +-- Role Icon +---------------------------------------------------------------------- + +local ROLE_TEXCOORDS = { + TANK = { 0, 19/64, 22/64, 41/64 }, + HEALER = { 20/64, 39/64, 1/64, 20/64 }, + DAMAGER = { 20/64, 39/64, 22/64, 41/64 }, +} + +local function CreateRoleIcon(frame, settings) + local icon = frame:CreateTexture(nil, "OVERLAY") + local sz = math.max(math.floor((settings.textSize or 11) + 2), 10) + PP.Size(icon, sz, sz) + icon:SetTexture("Interface\\LFGFrame\\UI-LFG-ICON-PORTRAITROLES") + icon:Hide() + + frame._roleIcon = icon + return icon +end + +local function UpdateRoleIcon(frame) + local icon = frame._roleIcon + if not icon then return end + + local db = ns.db + local settings = db and db.profile and db.profile.party + if not settings or settings.showRoleIcon == false then + icon:Hide() + return + end + + local unit = frame.unit or frame:GetAttribute("unit") + if not unit then icon:Hide(); return end + + local role = UnitGroupRolesAssigned(unit) + local coords = ROLE_TEXCOORDS[role] + if coords then + icon:SetTexCoord(unpack(coords)) + icon:Show() + else + icon:Hide() + end +end + +---------------------------------------------------------------------- +-- Party Aura Filter +---------------------------------------------------------------------- + +local function PartyAuraFilter(element, unit, data) + -- Prioritize debuffs the player can dispel + if data.isDebuff then + return true + end + return false +end + +local function CreatePartyAuras(frame, settings) + if not settings.showDebuffs and not settings.showBuffs then return end + + if settings.showDebuffs then + local debuffs = CreateFrame("Frame", nil, frame) + debuffs:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 1, 1) + debuffs.size = math.floor(settings.healthHeight * 0.5) + debuffs.num = settings.maxDebuffs or 3 + debuffs["growth-x"] = "RIGHT" + debuffs.FilterAura = PartyAuraFilter + + debuffs.PostCreateButton = function(self, button) + button.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + -- Dispellable glow + if not button._dispelGlow then + local glow = button:CreateTexture(nil, "OVERLAY") + glow:SetAllPoints() + glow:SetColorTexture(1, 1, 1, 0) + button._dispelGlow = glow + end + end + + debuffs.PostUpdateButton = function(self, button, unit, data) + if button._dispelGlow then + local db = ns.db + local s = db and db.profile and db.profile.party + if s and s.highlightDispellable and data.isDebuff and data.dispelName then + button._dispelGlow:SetColorTexture(0, 0.8, 1, 0.3) + else + button._dispelGlow:SetColorTexture(1, 1, 1, 0) + end + end + end + + frame.Debuffs = debuffs + end + + if settings.showBuffs and (settings.maxBuffs or 0) > 0 then + local buffs = CreateFrame("Frame", nil, frame) + buffs:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -1, 1) + buffs.size = math.floor(settings.healthHeight * 0.5) + buffs.num = settings.maxBuffs or 0 + buffs["growth-x"] = "LEFT" + buffs.PostCreateButton = function(self, button) + button.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + end + frame.Buffs = buffs + end +end + +---------------------------------------------------------------------- +-- StylePartyFrame +---------------------------------------------------------------------- + +local function StylePartyFrame(frame, unit) + local db = ns.db + if not db then return end + + local settings = db.profile.party + if not settings then return end + + local ppPos = settings.powerPosition or "below" + local ppIsAtt = (ppPos == "below" or ppPos == "above") + local powerHeight = ppIsAtt and (settings.powerHeight or 4) or 0 + local totalHeight = settings.healthHeight + powerHeight + local totalWidth = settings.frameWidth + + -- Portrait adds width when visible + local showPortrait = settings.showPortrait ~= false + and (db.profile.portraitStyle or "attached") ~= "none" + if showPortrait then + totalWidth = totalHeight + settings.frameWidth + end + + PP.Size(frame, totalWidth, totalHeight) + + -- Health bar + local healthRightInset = showPortrait and totalHeight or 0 + frame.Health = ns.CreateHealthBar(frame, unit, settings.healthHeight, 0, settings, healthRightInset) + + -- Absorb bar + ns.CreateAbsorbBar(frame, unit, settings) + + -- Power bar + if ppPos ~= "none" then + frame.Power = ns.CreatePowerBar(frame, unit, settings) + end + + -- Portrait (always create, hide backdrop when disabled — same pattern as boss) + frame.Portrait = ns.CreatePortrait(frame, "left", totalHeight, unit) + frame._portraitSide = "left" + if frame.Portrait and not showPortrait then + frame.Portrait.backdrop:Hide() + end + + -- Re-anchor health bar to portrait's snapped width (same fix as boss frames) + if frame.Portrait and frame.Portrait.backdrop and showPortrait and frame.Health then + local snappedPortW = frame.Portrait.backdrop:GetWidth() + local powerAboveOff = (ppPos == "above") and (settings.powerHeight or 4) or 0 + frame.Health:ClearAllPoints() + PP.Point(frame.Health, "TOPLEFT", frame, "TOPLEFT", snappedPortW, -powerAboveOff) + PP.Point(frame.Health, "RIGHT", frame, "RIGHT", 0, 0) + PP.Height(frame.Health, settings.healthHeight) + frame.Health._xOffset = snappedPortW + frame.Health._rightInset = 0 + frame.Health._topOffset = powerAboveOff + end + + -- Border + ns.CreateUnifiedBorder(frame, unit) + ns.UpdateBordersForScale(frame, unit) + + -- Clip bars to prevent overflow + ns.ReparentBarsToClip(frame) + + -- Text overlay + local textOverlay = CreateFrame("Frame", nil, frame.Health) + textOverlay:SetAllPoints(frame.Health) + textOverlay:SetFrameLevel(frame.Health:GetFrameLevel() + 12) + frame._textOverlay = textOverlay + + local ts = settings.textSize or 11 + local leftContent = settings.leftTextContent or "name" + local rightContent = settings.rightTextContent or "perhp" + local centerContent = settings.centerTextContent or "none" + + local leftText = textOverlay:CreateFontString(nil, "OVERLAY") + ns.SetFSFont(leftText, ts) + leftText:SetWordWrap(false) + leftText:SetTextColor(1, 1, 1) + frame.LeftText = leftText + + local rightText = textOverlay:CreateFontString(nil, "OVERLAY") + ns.SetFSFont(rightText, ts) + rightText:SetWordWrap(false) + rightText:SetTextColor(1, 1, 1) + frame.RightText = rightText + + local centerText = textOverlay:CreateFontString(nil, "OVERLAY") + ns.SetFSFont(centerText, ts) + centerText:SetWordWrap(false) + centerText:SetTextColor(1, 1, 1) + frame.CenterText = centerText + + frame.NameText = leftText + frame.HealthValue = rightText + + -- Tag system (same pattern as boss frames) + local function ApplyTextTags(lc, rc, cc) + local ltag = ns.ContentToTag(lc) + local rtag = ns.ContentToTag(rc) + local ctag = ns.ContentToTag(cc) + if leftText._curTag then frame:Untag(leftText); leftText._curTag = nil end + if rightText._curTag then frame:Untag(rightText); rightText._curTag = nil end + if centerText._curTag then frame:Untag(centerText); centerText._curTag = nil end + if ltag then frame:Tag(leftText, ltag); leftText._curTag = ltag end + if rtag then frame:Tag(rightText, rtag); rightText._curTag = rtag end + if ctag then frame:Tag(centerText, ctag); centerText._curTag = ctag end + if frame.UpdateTags then frame:UpdateTags() end + end + ApplyTextTags(leftContent, rightContent, centerContent) + frame._applyTextTags = ApplyTextTags + + -- Text positioning (same pattern as boss frames) + local function ApplyTextPositions(s) + local lc = s.leftTextContent or "name" + local rc = s.rightTextContent or "perhp" + local cc = s.centerTextContent or "none" + local barW = s.frameWidth or 160 + + -- Account for role icon width on the left + local roleOffset = (s.showRoleIcon ~= false) and (ts + 6) or 0 + + if cc ~= "none" then + centerText:ClearAllPoints() + centerText:SetPoint("CENTER", frame.Health, "CENTER", 0, 0) + centerText:SetWidth(0) + centerText:Show() + leftText:Hide(); rightText:Hide() + else + centerText:Hide() + if lc ~= "none" then + leftText:ClearAllPoints() + leftText:SetPoint("LEFT", frame.Health, "LEFT", 5 + roleOffset, 0) + leftText:SetJustifyH("LEFT") + if rc ~= "none" then + local rightUsed = ns.EstimateUFTextWidth(rc) + PP.Width(leftText, math.max(barW - rightUsed - 10 - roleOffset, 20)) + else + leftText:SetWidth(0) + end + leftText:Show() + else leftText:Hide() end + if rc ~= "none" then + rightText:ClearAllPoints() + rightText:SetPoint("RIGHT", frame.Health, "RIGHT", -5, 0) + rightText:SetJustifyH("RIGHT") + if lc ~= "none" then + local leftUsed = ns.EstimateUFTextWidth(lc) + PP.Width(rightText, math.max(barW - leftUsed - 10 - roleOffset, 20)) + else + rightText:SetWidth(0) + end + rightText:Show() + else rightText:Hide() end + end + end + ApplyTextPositions(settings) + frame._applyTextPositions = ApplyTextPositions + + -- Role icon (anchored to left of health bar, before name text) + local roleIcon = CreateRoleIcon(frame, settings) + roleIcon:SetPoint("LEFT", frame.Health, "LEFT", 4, 0) + + -- Auras + CreatePartyAuras(frame, settings) + + -- Range fading + if settings.enableRangeFade ~= false then + frame.Range = { + insideAlpha = 1, + outsideAlpha = settings.rangeFadeAlpha or 0.4, + } + end + + -- Threat indicator (border glow) + if settings.showThreat ~= false then + local threat = frame:CreateTexture(nil, "OVERLAY") + threat:SetAllPoints() + threat:Hide() + frame.ThreatIndicator = threat + end + + -- Ready check + local readyCheck = frame:CreateTexture(nil, "OVERLAY", nil, 7) + readyCheck:SetSize(16, 16) + readyCheck:SetPoint("CENTER", frame, "CENTER", 0, 0) + frame.ReadyCheckIndicator = readyCheck + + -- Leader indicator + local leader = frame:CreateTexture(nil, "OVERLAY", nil, 7) + leader:SetSize(12, 12) + leader:SetPoint("TOPLEFT", frame, "TOPLEFT", 2, -2) + frame.LeaderIndicator = leader + + -- Assistant indicator + local assist = frame:CreateTexture(nil, "OVERLAY", nil, 7) + assist:SetSize(12, 12) + assist:SetPoint("TOPLEFT", frame, "TOPLEFT", 2, -2) + frame.AssistantIndicator = assist + + -- Resurrection indicator + local resurrect = frame:CreateTexture(nil, "OVERLAY", nil, 7) + resurrect:SetSize(20, 20) + resurrect:SetPoint("CENTER", frame, "CENTER", 0, 0) + frame.ResurrectIndicator = resurrect + + -- Summon indicator + local summon = frame:CreateTexture(nil, "OVERLAY", nil, 7) + summon:SetSize(24, 24) + summon:SetPoint("CENTER", frame, "CENTER", 0, 0) + frame.SummonIndicator = summon + + -- Hook for role icon updates + -- Note: WoW Frame:RegisterEvent takes only event name (no callback arg). + -- Use OnEvent script + OnShow hook instead. + frame:RegisterEvent("GROUP_ROSTER_UPDATE") + frame:RegisterEvent("PLAYER_ROLES_ASSIGNED") + frame:HookScript("OnEvent", function(self, event) + if event == "GROUP_ROSTER_UPDATE" or event == "PLAYER_ROLES_ASSIGNED" then + UpdateRoleIcon(self) + end + end) + frame:HookScript("OnShow", function(self) + UpdateRoleIcon(self) + end) + + -- Right-click menu + if ns.SetupUnitMenu then + ns.SetupUnitMenu(frame, unit or "party") + end +end + +---------------------------------------------------------------------- +-- SpawnPartyHeader +---------------------------------------------------------------------- + +local partyHeader + +local function SpawnPartyHeader() + local db = ns.db + if not db then return end + + local settings = db.profile.party + if not settings then return end + + local point, xOff, yOff = GetGrowthAttributes( + settings.growthDirection or "vertical", + settings.spacing or 1 + ) + + -- Sort config + local sortCfg = SORT_CONFIGS[settings.sortOrder or "role"] or SORT_CONFIGS.role + + -- Guard: oUF errors on duplicate style registration (e.g. during ReloadFrames) + if not oUF.styles or not oUF.styles["EllesmereParty"] then + oUF:RegisterStyle("EllesmereParty", StylePartyFrame) + end + oUF:SetActiveStyle("EllesmereParty") + + local headerArgs = { + "showPlayer", settings.showPlayer or false, + "showParty", true, + "showSolo", false, + "point", point, + "xOffset", xOff, + "yOffset", yOff, + "oUF-initialConfigFunction", ([[ + self:SetWidth(%d) + self:SetHeight(%d) + ]]):format(settings.frameWidth or 160, (settings.healthHeight or 36) + ((settings.powerPosition ~= "none") and (settings.powerHeight or 4) or 0)), + } + + -- Add sort attributes + if sortCfg.groupBy then + headerArgs[#headerArgs + 1] = "groupBy" + headerArgs[#headerArgs + 1] = sortCfg.groupBy + end + if sortCfg.groupingOrder then + headerArgs[#headerArgs + 1] = "groupingOrder" + headerArgs[#headerArgs + 1] = sortCfg.groupingOrder + end + if sortCfg.sortMethod then + headerArgs[#headerArgs + 1] = "sortMethod" + headerArgs[#headerArgs + 1] = sortCfg.sortMethod + end + + partyHeader = oUF:SpawnHeader( + "EllesmereUIPartyHeader", + nil, + "custom [@party1,exists] show;hide", + unpack(headerArgs) + ) + + ns.ApplyFramePosition(partyHeader, "party") + + local enabled = db.profile.enabledFrames + if enabled.party == false then + RegisterAttributeDriver(partyHeader, "state-visibility", "hide") + end + + -- Store reference + ns.partyHeader = partyHeader + + return partyHeader +end + +---------------------------------------------------------------------- +-- UpdatePartyLayout (out of combat only) +---------------------------------------------------------------------- + +local function UpdatePartyLayout() + if InCombatLockdown() or not partyHeader then return end + + local db = ns.db + if not db then return end + + local settings = db.profile.party + if not settings then return end + + local point, xOff, yOff = GetGrowthAttributes( + settings.growthDirection or "vertical", + settings.spacing or 1 + ) + + partyHeader:SetAttribute("point", point) + partyHeader:SetAttribute("xOffset", xOff) + partyHeader:SetAttribute("yOffset", yOff) + partyHeader:SetAttribute("showPlayer", settings.showPlayer or false) + + local sortCfg = SORT_CONFIGS[settings.sortOrder or "role"] or SORT_CONFIGS.role + if sortCfg.groupBy then + partyHeader:SetAttribute("groupBy", sortCfg.groupBy) + partyHeader:SetAttribute("groupingOrder", sortCfg.groupingOrder) + end + if sortCfg.sortMethod then + partyHeader:SetAttribute("sortMethod", sortCfg.sortMethod) + end + + ns.ApplyFramePosition(partyHeader, "party") +end + +---------------------------------------------------------------------- +-- Public API +---------------------------------------------------------------------- + +ns.SpawnPartyHeader = SpawnPartyHeader +ns.UpdatePartyLayout = UpdatePartyLayout +ns.StylePartyFrame = StylePartyFrame From e9a903738a4740138194bcac7ed94bdd8cebe516 Mon Sep 17 00:00:00 2001 From: Daniel Vernon Date: Tue, 17 Mar 2026 01:27:10 +0000 Subject: [PATCH 08/20] Add party frame spawn call and Unlock Mode registration Task 6: Call ns.SpawnPartyHeader() after boss frame hiding loop, guarded by an existence check so it's safe when the party module isn't loaded. Task 7: Register party frames with Unlock Mode system: - Add "party" to UNIT_LABELS - Route getFrame to ns.partyHeader - Compute getSize from party profile settings - Block setWidth/setHeight (not individually resizable) - Add savePos/applyPos branches for ns.partyHeader - Register MakeUFElement("party", 8) after boss --- .../EllesmereUIUnitFrames.lua | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua index c21281da..12da8360 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua @@ -6255,6 +6255,11 @@ function InitializeFrames() end end + -- Party frames (spawned via header in EllesmereUIPartyFrames.lua) + if ns.SpawnPartyHeader then + ns.SpawnPartyHeader() + end + -- Disable oUF elements for frames where features are initially off. -- Portrait backdrop is already hidden by style functions, but oUF -- auto-enables the element at spawn time since frame.Portrait is always set. @@ -6620,6 +6625,7 @@ function SetupOptionsPanel() playerCastbar = "Player Cast Bar", targetCastbar = "Target Cast Bar", focusCastbar = "Focus Cast Bar", + party = "Party Frames", } local elements = {} local orderBase = 100 @@ -6643,6 +6649,7 @@ function SetupOptionsPanel() return nil end if k == "classPower" then return frames._classPowerBar end + if k == "party" then return ns.partyHeader end return frames[k] end, getSize = function(k) @@ -6670,10 +6677,23 @@ function SetupOptionsPanel() end return 120, 14 end + if k == "party" then + local s = db.profile.party + if not s then return 160, 36 end + local ppPos = s.powerPosition or "below" + local ppIsAtt = (ppPos == "below" or ppPos == "above") + local ph = ppIsAtt and (s.powerHeight or 4) or 0 + local frameH = s.healthHeight + ph + local frameW = s.frameWidth + local showPortrait = s.showPortrait ~= false and (db.profile.portraitStyle or "attached") ~= "none" + if showPortrait then frameW = frameW + frameH end + return frameW, frameH + end if k == "boss" then return GetFrameDimensions("boss1") end return GetFrameDimensions(k) end, setWidth = function(k, w) + if k == "party" then return end if k == "playerCastbar" then db.profile.player.playerCastbarWidth = math.max(math.floor(w + 0.5), 30) local cbBg = frames.player and frames.player.Castbar and frames.player.Castbar:GetParent() @@ -6710,6 +6730,7 @@ function SetupOptionsPanel() Rebuild() end, setHeight = function(k, h) + if k == "party" then return end if k == "playerCastbar" then local newH = math.max(math.floor(h + 0.5), 5) db.profile.player.playerCastbarHeight = newH @@ -6775,6 +6796,11 @@ function SetupOptionsPanel() frames._classPowerBar:ClearAllPoints() frames._classPowerBar:SetPoint(point, UIParent, relPoint, x, y) end + elseif k == "party" then + if ns.partyHeader then + ns.partyHeader:ClearAllPoints() + ns.partyHeader:SetPoint(point, UIParent, relPoint, x, y) + end else local fr = frames[k] if fr then @@ -6812,6 +6838,11 @@ function SetupOptionsPanel() frames._classPowerBar:ClearAllPoints() frames._classPowerBar:SetPoint(pos.point, UIParent, pos.relPoint or pos.point, pos.x, pos.y) end + elseif k == "party" then + if ns.partyHeader then + ns.partyHeader:ClearAllPoints() + ns.partyHeader:SetPoint(pos.point, UIParent, pos.relPoint or pos.point, pos.x, pos.y) + end else local fr = frames[k] if fr then @@ -6831,6 +6862,7 @@ function SetupOptionsPanel() elements[#elements + 1] = MakeUFElement("targettarget", 5) elements[#elements + 1] = MakeUFElement("focustarget", 6) elements[#elements + 1] = MakeUFElement("boss", 7) + elements[#elements + 1] = MakeUFElement("party", 8) -- Conditional elements if db.profile.player.showClassPowerBar and not db.profile.player.lockClassPowerToFrame then From f77fb34f183996986fd1463997efa8eab5b99ab0 Mon Sep 17 00:00:00 2001 From: Daniel Vernon Date: Tue, 17 Mar 2026 01:27:42 +0000 Subject: [PATCH 09/20] feat(party): add native party frames to CDM FindPlayerPartyFrame sources EllesmereUI party frames checked first, before ElvUI/Cell/Blizzard fallbacks. --- EllesmereUI.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/EllesmereUI.lua b/EllesmereUI.lua index 1653b6ad..273fbe79 100644 --- a/EllesmereUI.lua +++ b/EllesmereUI.lua @@ -1697,6 +1697,7 @@ EllesmereUI.RESOURCE_BAR_ANCHOR_KEYS = { do local PARTY_FRAME_SOURCES = { + { addon = "EllesmereUIUnitFrames", prefix = "EllesmereUIPartyHeaderUnitButton", count = 5 }, { addon = "ElvUI", prefix = "ElvUF_PartyGroup1UnitButton", count = 5 }, { addon = "Cell", prefix = "CellPartyFrameMember", count = 5 }, { addon = nil, prefix = "CompactPartyFrameMember", count = 5 }, From a608682c4e2c2dfb4f442917bafc9b5deb146f17 Mon Sep 17 00:00:00 2001 From: Daniel Vernon Date: Tue, 17 Mar 2026 01:31:23 +0000 Subject: [PATCH 10/20] fix(party): clear stale sort attributes and suppress Blizzard party frames - UpdatePartyLayout now always sets all three sort attributes (groupBy, groupingOrder, sortMethod) with nil to clear stale values when switching between sort modes - Hide Blizzard CompactPartyFrame and PartyFrame when EUI party frames are enabled, preventing overlap --- EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua | 11 ++++------- EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua | 12 ++++++++++++ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua index 863fc23c..5574e965 100644 --- a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua @@ -460,14 +460,11 @@ local function UpdatePartyLayout() partyHeader:SetAttribute("yOffset", yOff) partyHeader:SetAttribute("showPlayer", settings.showPlayer or false) + -- Always set all sort attributes to clear stale values when switching modes local sortCfg = SORT_CONFIGS[settings.sortOrder or "role"] or SORT_CONFIGS.role - if sortCfg.groupBy then - partyHeader:SetAttribute("groupBy", sortCfg.groupBy) - partyHeader:SetAttribute("groupingOrder", sortCfg.groupingOrder) - end - if sortCfg.sortMethod then - partyHeader:SetAttribute("sortMethod", sortCfg.sortMethod) - end + partyHeader:SetAttribute("groupBy", sortCfg.groupBy) + partyHeader:SetAttribute("groupingOrder", sortCfg.groupingOrder) + partyHeader:SetAttribute("sortMethod", sortCfg.sortMethod) ns.ApplyFramePosition(partyHeader, "party") end diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua index 12da8360..564f4299 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua @@ -6260,6 +6260,18 @@ function InitializeFrames() ns.SpawnPartyHeader() end + -- Hide Blizzard party frames when our party frames are enabled + if enabled.party ~= false then + if CompactPartyFrame then + CompactPartyFrame:UnregisterAllEvents() + CompactPartyFrame:Hide() + end + if PartyFrame then + PartyFrame:UnregisterAllEvents() + PartyFrame:Hide() + end + end + -- Disable oUF elements for frames where features are initially off. -- Portrait backdrop is already hidden by style functions, but oUF -- auto-enables the element at spawn time since frame.Portrait is always set. From 2fd631ae1dcb37ba18cc53b8b72aab5838cedc3c Mon Sep 17 00:00:00 2001 From: Daniel Vernon Date: Tue, 17 Mar 2026 07:39:01 +0000 Subject: [PATCH 11/20] feat(party): add /partytest slash command for solo testing Toggles test mode that shows party frames when solo by overriding showSolo and visibility driver. Type /partytest again to restore normal behavior. --- .../EllesmereUIPartyFrames.lua | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua index 5574e965..519a2203 100644 --- a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua @@ -476,3 +476,43 @@ end ns.SpawnPartyHeader = SpawnPartyHeader ns.UpdatePartyLayout = UpdatePartyLayout ns.StylePartyFrame = StylePartyFrame + +---------------------------------------------------------------------- +-- Test Mode: /partytest +---------------------------------------------------------------------- + +local testMode = false + +SLASH_EUIPARTYTEST1 = "/partytest" +SlashCmdList.EUIPARTYTEST = function() + if InCombatLockdown() then + print("|cff0cd29f[EUI Party]|r Cannot toggle test mode during combat.") + return + end + if not partyHeader then + print("|cff0cd29f[EUI Party]|r Party header not initialized.") + return + end + + testMode = not testMode + if testMode then + partyHeader:SetAttribute("showSolo", true) + partyHeader:SetAttribute("showPlayer", true) + -- Force visibility when not in a party + RegisterAttributeDriver(partyHeader, "state-visibility", "show") + print("|cff0cd29f[EUI Party]|r Test mode |cff00ff00ON|r — showing party frames solo. Type /partytest again to disable.") + else + partyHeader:SetAttribute("showSolo", false) + local db = ns.db + local settings = db and db.profile and db.profile.party + partyHeader:SetAttribute("showPlayer", settings and settings.showPlayer or false) + -- Restore normal visibility driver + local enabled = db and db.profile and db.profile.enabledFrames + if enabled and enabled.party == false then + RegisterAttributeDriver(partyHeader, "state-visibility", "hide") + else + RegisterAttributeDriver(partyHeader, "state-visibility", "custom [@party1,exists] show;hide") + end + print("|cff0cd29f[EUI Party]|r Test mode |cffff6060OFF|r — normal party visibility restored.") + end +end From 75d37bdbb6d483d85e2c6ac6bd690c6c099e81b7 Mon Sep 17 00:00:00 2001 From: Daniel Vernon Date: Tue, 17 Mar 2026 08:12:01 +0000 Subject: [PATCH 12/20] fix(party): handle nil unit in StylePartyFrame and fix test mode visibility - StylePartyFrame now falls back to frame attribute or "player" when oUF calls it before a unit is assigned - /partytest unregisters the old driver before registering the new one and uses a macro conditional that lets the secure header properly assign units before styling frames --- EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua index 519a2203..b0932e44 100644 --- a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua @@ -138,6 +138,10 @@ local function StylePartyFrame(frame, unit) local db = ns.db if not db then return end + -- oUF may call style function before a unit is assigned (e.g. test mode) + -- Default to "player" so builders that require a unit don't error + unit = unit or frame:GetAttribute("unit") or "player" + local settings = db.profile.party if not settings then return end @@ -496,17 +500,24 @@ SlashCmdList.EUIPARTYTEST = function() testMode = not testMode if testMode then + -- Set showSolo and showPlayer first so the header knows to include + -- the player unit, then swap the visibility driver. Order matters: + -- the header must know about showSolo before it processes the show. partyHeader:SetAttribute("showSolo", true) partyHeader:SetAttribute("showPlayer", true) - -- Force visibility when not in a party - RegisterAttributeDriver(partyHeader, "state-visibility", "show") + -- Use the macro conditional that always evaluates to show, which + -- lets the secure header properly assign units before styling + UnregisterAttributeDriver(partyHeader, "state-visibility") + RegisterAttributeDriver(partyHeader, "state-visibility", "[exists] show; show") print("|cff0cd29f[EUI Party]|r Test mode |cff00ff00ON|r — showing party frames solo. Type /partytest again to disable.") else + -- Restore normal attributes partyHeader:SetAttribute("showSolo", false) local db = ns.db local settings = db and db.profile and db.profile.party partyHeader:SetAttribute("showPlayer", settings and settings.showPlayer or false) -- Restore normal visibility driver + UnregisterAttributeDriver(partyHeader, "state-visibility") local enabled = db and db.profile and db.profile.enabledFrames if enabled and enabled.party == false then RegisterAttributeDriver(partyHeader, "state-visibility", "hide") From 02586f311d4acbdd5fcb3b668200ce7c46af29f5 Mon Sep 17 00:00:00 2001 From: Daniel Vernon Date: Tue, 17 Mar 2026 08:28:49 +0000 Subject: [PATCH 13/20] fix(party): set oUF-guessUnit fallback to prevent nil index in oUF oUF ouf.lua:270 calls objectUnit:match() which crashes when both the unit parameter and oUF-guessUnit attribute are nil. This happens in solo test mode when the header creates frames with no real party member. Set a 'party' fallback in the initialConfigFunction. --- EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua index b0932e44..9174dfb5 100644 --- a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua @@ -404,6 +404,10 @@ local function SpawnPartyHeader() "oUF-initialConfigFunction", ([[ self:SetWidth(%d) self:SetHeight(%d) + -- Ensure oUF-guessUnit is never nil (oUF crashes at ouf.lua:270 otherwise) + if(not self:GetAttribute('oUF-guessUnit')) then + self:SetAttribute('oUF-guessUnit', 'party') + end ]]):format(settings.frameWidth or 160, (settings.healthHeight or 36) + ((settings.powerPosition ~= "none") and (settings.powerHeight or 4) or 0)), } From 0fc8c55a135eb3ae1deee4482f37bf6ef5c4e07d Mon Sep 17 00:00:00 2001 From: Daniel Vernon Date: Tue, 17 Mar 2026 08:34:50 +0000 Subject: [PATCH 14/20] fix(party): rewrite /partytest to use standalone oUF:Spawn instead of header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SecureGroupHeader's showSolo triggers oUF initObject with a nil unit which crashes at ouf.lua:270. Instead, spawn a standalone oUF frame with the party style using the player unit — bypasses the header entirely for preview purposes. --- .../EllesmereUIPartyFrames.lua | 61 +++++++++---------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua index 9174dfb5..888df8d0 100644 --- a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua @@ -404,10 +404,6 @@ local function SpawnPartyHeader() "oUF-initialConfigFunction", ([[ self:SetWidth(%d) self:SetHeight(%d) - -- Ensure oUF-guessUnit is never nil (oUF crashes at ouf.lua:270 otherwise) - if(not self:GetAttribute('oUF-guessUnit')) then - self:SetAttribute('oUF-guessUnit', 'party') - end ]]):format(settings.frameWidth or 160, (settings.healthHeight or 36) + ((settings.powerPosition ~= "none") and (settings.powerHeight or 4) or 0)), } @@ -488,8 +484,12 @@ ns.StylePartyFrame = StylePartyFrame ---------------------------------------------------------------------- -- Test Mode: /partytest ---------------------------------------------------------------------- +-- Spawns a standalone oUF frame styled as a party frame using the +-- player unit. This avoids SecureGroupHeader issues with showSolo +-- where oUF's initObject crashes on nil objectUnit. +---------------------------------------------------------------------- -local testMode = false +local testFrame SLASH_EUIPARTYTEST1 = "/partytest" SlashCmdList.EUIPARTYTEST = function() @@ -497,37 +497,32 @@ SlashCmdList.EUIPARTYTEST = function() print("|cff0cd29f[EUI Party]|r Cannot toggle test mode during combat.") return end - if not partyHeader then - print("|cff0cd29f[EUI Party]|r Party header not initialized.") + + if testFrame and testFrame:IsShown() then + testFrame:Hide() + testFrame:SetAttribute("unit", nil) + print("|cff0cd29f[EUI Party]|r Test mode |cffff6060OFF|r") return end - testMode = not testMode - if testMode then - -- Set showSolo and showPlayer first so the header knows to include - -- the player unit, then swap the visibility driver. Order matters: - -- the header must know about showSolo before it processes the show. - partyHeader:SetAttribute("showSolo", true) - partyHeader:SetAttribute("showPlayer", true) - -- Use the macro conditional that always evaluates to show, which - -- lets the secure header properly assign units before styling - UnregisterAttributeDriver(partyHeader, "state-visibility") - RegisterAttributeDriver(partyHeader, "state-visibility", "[exists] show; show") - print("|cff0cd29f[EUI Party]|r Test mode |cff00ff00ON|r — showing party frames solo. Type /partytest again to disable.") - else - -- Restore normal attributes - partyHeader:SetAttribute("showSolo", false) - local db = ns.db - local settings = db and db.profile and db.profile.party - partyHeader:SetAttribute("showPlayer", settings and settings.showPlayer or false) - -- Restore normal visibility driver - UnregisterAttributeDriver(partyHeader, "state-visibility") - local enabled = db and db.profile and db.profile.enabledFrames - if enabled and enabled.party == false then - RegisterAttributeDriver(partyHeader, "state-visibility", "hide") - else - RegisterAttributeDriver(partyHeader, "state-visibility", "custom [@party1,exists] show;hide") + if not testFrame then + -- Register guard (style may already be registered by SpawnPartyHeader) + if not oUF.styles or not oUF.styles["EllesmereParty"] then + oUF:RegisterStyle("EllesmereParty", StylePartyFrame) end - print("|cff0cd29f[EUI Party]|r Test mode |cffff6060OFF|r — normal party visibility restored.") + oUF:SetActiveStyle("EllesmereParty") + testFrame = oUF:Spawn("player", "EllesmereUIPartyTest") end + + -- Position near the party header anchor (or center-left if no header) + testFrame:ClearAllPoints() + if partyHeader then + testFrame:SetPoint("TOPLEFT", partyHeader, "TOPLEFT", 0, 0) + else + testFrame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 20, -40) + end + + testFrame:SetAttribute("unit", "player") + testFrame:Show() + print("|cff0cd29f[EUI Party]|r Test mode |cff00ff00ON|r — showing party frame preview (player unit). Type /partytest again to hide.") end From 5e9895a514e5e660fa0760f33d1b5a35b22aec58 Mon Sep 17 00:00:00 2001 From: Daniel Vernon Date: Tue, 17 Mar 2026 08:38:02 +0000 Subject: [PATCH 15/20] =?UTF-8?q?fix(party):=20fix=20style=20registration?= =?UTF-8?q?=20guard=20=E2=80=94=20oUF=20stores=20styles=20in=20local=20tab?= =?UTF-8?q?le?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit oUF.styles doesn't exist (styles is a local table), so the guard always passed and tried to re-register. Use ns._partyStyleRegistered flag instead. Also remove redundant registration attempt from /partytest. --- EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua index 888df8d0..012065b9 100644 --- a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua @@ -388,9 +388,11 @@ local function SpawnPartyHeader() -- Sort config local sortCfg = SORT_CONFIGS[settings.sortOrder or "role"] or SORT_CONFIGS.role - -- Guard: oUF errors on duplicate style registration (e.g. during ReloadFrames) - if not oUF.styles or not oUF.styles["EllesmereParty"] then + -- Register style once — oUF stores styles in a local table (not oUF.styles), + -- so we track registration ourselves to avoid "already registered" errors. + if not ns._partyStyleRegistered then oUF:RegisterStyle("EllesmereParty", StylePartyFrame) + ns._partyStyleRegistered = true end oUF:SetActiveStyle("EllesmereParty") @@ -506,10 +508,7 @@ SlashCmdList.EUIPARTYTEST = function() end if not testFrame then - -- Register guard (style may already be registered by SpawnPartyHeader) - if not oUF.styles or not oUF.styles["EllesmereParty"] then - oUF:RegisterStyle("EllesmereParty", StylePartyFrame) - end + -- Style is already registered by SpawnPartyHeader, just set it active oUF:SetActiveStyle("EllesmereParty") testFrame = oUF:Spawn("player", "EllesmereUIPartyTest") end From 1949a1a185be685552c745076c2438590d3216e2 Mon Sep 17 00:00:00 2001 From: Daniel Vernon Date: Tue, 17 Mar 2026 08:40:19 +0000 Subject: [PATCH 16/20] fix(party): use oUF RegisterEvent with callback for role icon updates oUF overrides frame:RegisterEvent to require (event, func) signature. Previous code used Blizzard-style single-arg registration which passed nil as the callback. --- EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua index 012065b9..1699c150 100644 --- a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua @@ -347,16 +347,9 @@ local function StylePartyFrame(frame, unit) summon:SetPoint("CENTER", frame, "CENTER", 0, 0) frame.SummonIndicator = summon - -- Hook for role icon updates - -- Note: WoW Frame:RegisterEvent takes only event name (no callback arg). - -- Use OnEvent script + OnShow hook instead. - frame:RegisterEvent("GROUP_ROSTER_UPDATE") - frame:RegisterEvent("PLAYER_ROLES_ASSIGNED") - frame:HookScript("OnEvent", function(self, event) - if event == "GROUP_ROSTER_UPDATE" or event == "PLAYER_ROLES_ASSIGNED" then - UpdateRoleIcon(self) - end - end) + -- Hook for role icon updates using oUF's RegisterEvent (requires callback) + frame:RegisterEvent("GROUP_ROSTER_UPDATE", UpdateRoleIcon) + frame:RegisterEvent("PLAYER_ROLES_ASSIGNED", UpdateRoleIcon) frame:HookScript("OnShow", function(self) UpdateRoleIcon(self) end) From 1866bc3df8ec21177e5bec217b8fc1d5b97eb99a Mon Sep 17 00:00:00 2001 From: Daniel Vernon Date: Tue, 17 Mar 2026 08:47:23 +0000 Subject: [PATCH 17/20] fix(party): mark role icon events as unitless for oUF RegisterEvent GROUP_ROSTER_UPDATE and PLAYER_ROLES_ASSIGNED are not unit events, oUF requires the third arg = true to register them. --- EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua index 1699c150..c957e130 100644 --- a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua @@ -347,9 +347,10 @@ local function StylePartyFrame(frame, unit) summon:SetPoint("CENTER", frame, "CENTER", 0, 0) frame.SummonIndicator = summon - -- Hook for role icon updates using oUF's RegisterEvent (requires callback) - frame:RegisterEvent("GROUP_ROSTER_UPDATE", UpdateRoleIcon) - frame:RegisterEvent("PLAYER_ROLES_ASSIGNED", UpdateRoleIcon) + -- Hook for role icon updates using oUF's RegisterEvent + -- Third arg = true marks these as unitless (they don't fire for specific units) + frame:RegisterEvent("GROUP_ROSTER_UPDATE", UpdateRoleIcon, true) + frame:RegisterEvent("PLAYER_ROLES_ASSIGNED", UpdateRoleIcon, true) frame:HookScript("OnShow", function(self) UpdateRoleIcon(self) end) From 6fd51f79961d02bbaf638e744babb3c8c008f504 Mon Sep 17 00:00:00 2001 From: Daniel Vernon Date: Tue, 17 Mar 2026 09:05:22 +0000 Subject: [PATCH 18/20] fix(party): adjust default party frame position to TOPLEFT 20, -200 --- EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua index 564f4299..ea17a195 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua @@ -548,7 +548,7 @@ local defaults = { focustarget = { point = "CENTER", relPoint = "CENTER", x = 50, y = -261 }, boss = { point = "CENTER", relPoint = "CENTER", x = 661, y = 251 }, classPower = { point = "CENTER", relPoint = "CENTER", x = 0, y = -220 }, - party = { point = "TOPLEFT", relPoint = "TOPLEFT", x = 20, y = -40 }, + party = { point = "TOPLEFT", relPoint = "TOPLEFT", x = 20, y = -200 }, }, bossSpacing = 60, } From cd45b8f6f398010b3e5a09e16164c2183df70f33 Mon Sep 17 00:00:00 2001 From: Daniel Vernon Date: Tue, 17 Mar 2026 09:25:03 +0000 Subject: [PATCH 19/20] fix(party): use persistent anchor frame for positioning The SecureGroupHeader hides when solo, which breaks Unlock Mode positioning (hidden frame reports wrong position). Added a persistent partyAnchor frame that holds the saved position; the header attaches to it. Unlock Mode now drags/saves via the anchor. --- .../EllesmereUIPartyFrames.lua | 21 ++++++++++++++++--- .../EllesmereUIUnitFrames.lua | 16 +++++++------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua index c957e130..6e8a0509 100644 --- a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua @@ -366,6 +366,7 @@ end ---------------------------------------------------------------------- local partyHeader +local partyAnchor -- persistent anchor frame for positioning (survives header hide) local function SpawnPartyHeader() local db = ns.db @@ -417,6 +418,16 @@ local function SpawnPartyHeader() headerArgs[#headerArgs + 1] = sortCfg.sortMethod end + -- Create a persistent anchor frame for positioning. + -- The header hides when solo (SecureGroupHeader visibility driver), + -- which breaks Unlock Mode positioning. The anchor stays visible and + -- holds the saved position; the header attaches to it. + if not partyAnchor then + partyAnchor = CreateFrame("Frame", "EllesmereUIPartyAnchor", UIParent) + partyAnchor:SetSize(settings.frameWidth or 160, (settings.healthHeight or 36) + ((settings.powerPosition ~= "none") and (settings.powerHeight or 4) or 0)) + end + ns.ApplyFramePosition(partyAnchor, "party") + partyHeader = oUF:SpawnHeader( "EllesmereUIPartyHeader", nil, @@ -424,15 +435,17 @@ local function SpawnPartyHeader() unpack(headerArgs) ) - ns.ApplyFramePosition(partyHeader, "party") + -- Attach header to the anchor + partyHeader:SetPoint("TOPLEFT", partyAnchor, "TOPLEFT", 0, 0) local enabled = db.profile.enabledFrames if enabled.party == false then RegisterAttributeDriver(partyHeader, "state-visibility", "hide") end - -- Store reference + -- Store references ns.partyHeader = partyHeader + ns.partyAnchor = partyAnchor return partyHeader end @@ -466,7 +479,9 @@ local function UpdatePartyLayout() partyHeader:SetAttribute("groupingOrder", sortCfg.groupingOrder) partyHeader:SetAttribute("sortMethod", sortCfg.sortMethod) - ns.ApplyFramePosition(partyHeader, "party") + if partyAnchor then + ns.ApplyFramePosition(partyAnchor, "party") + end end ---------------------------------------------------------------------- diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua index ea17a195..0cf29af1 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua @@ -6661,7 +6661,7 @@ function SetupOptionsPanel() return nil end if k == "classPower" then return frames._classPowerBar end - if k == "party" then return ns.partyHeader end + if k == "party" then return ns.partyAnchor or ns.partyHeader end return frames[k] end, getSize = function(k) @@ -6809,9 +6809,10 @@ function SetupOptionsPanel() frames._classPowerBar:SetPoint(point, UIParent, relPoint, x, y) end elseif k == "party" then - if ns.partyHeader then - ns.partyHeader:ClearAllPoints() - ns.partyHeader:SetPoint(point, UIParent, relPoint, x, y) + local anchor = ns.partyAnchor or ns.partyHeader + if anchor then + anchor:ClearAllPoints() + anchor:SetPoint(point, UIParent, relPoint, x, y) end else local fr = frames[k] @@ -6851,9 +6852,10 @@ function SetupOptionsPanel() frames._classPowerBar:SetPoint(pos.point, UIParent, pos.relPoint or pos.point, pos.x, pos.y) end elseif k == "party" then - if ns.partyHeader then - ns.partyHeader:ClearAllPoints() - ns.partyHeader:SetPoint(pos.point, UIParent, pos.relPoint or pos.point, pos.x, pos.y) + local anchor = ns.partyAnchor or ns.partyHeader + if anchor then + anchor:ClearAllPoints() + anchor:SetPoint(pos.point, UIParent, pos.relPoint or pos.point, pos.x, pos.y) end else local fr = frames[k] From e910cb544099163fc004816306632ae48eb402dd Mon Sep 17 00:00:00 2001 From: Daniel Vernon Date: Tue, 17 Mar 2026 09:26:59 +0000 Subject: [PATCH 20/20] fix(party): anchor test frame to partyAnchor instead of hidden header Test frame was anchoring to partyHeader which is hidden when solo, causing the test frame to not display. --- EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua index 6e8a0509..c1ade79d 100644 --- a/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIPartyFrames.lua @@ -522,12 +522,12 @@ SlashCmdList.EUIPARTYTEST = function() testFrame = oUF:Spawn("player", "EllesmereUIPartyTest") end - -- Position near the party header anchor (or center-left if no header) + -- Position at the party anchor (always visible, unlike the header) testFrame:ClearAllPoints() - if partyHeader then - testFrame:SetPoint("TOPLEFT", partyHeader, "TOPLEFT", 0, 0) + if partyAnchor then + testFrame:SetPoint("TOPLEFT", partyAnchor, "TOPLEFT", 0, 0) else - testFrame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 20, -40) + testFrame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 20, -200) end testFrame:SetAttribute("unit", "player")