From ad2a536efedd1afaf789115e85a1aa954b4e085c Mon Sep 17 00:00:00 2001 From: Florian Berthold Date: Sun, 31 May 2026 14:45:28 +0200 Subject: [PATCH 1/6] fix(dispels): add Curse to Witch Doctor dispel set (Hexbreak 806240) WITCHDOCTOR could only flag Disease/Poison (Cleansing Idol). Hexbreak (806240) is a single-target Curse remover, so add Curse=true. highlight.lua reads this via GetCoaDispels, so the dispellable-debuff highlight picks it up. --- ShadowedUnitFrames/modules/auras.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ShadowedUnitFrames/modules/auras.lua b/ShadowedUnitFrames/modules/auras.lua index db32685..a7e6e65 100644 --- a/ShadowedUnitFrames/modules/auras.lua +++ b/ShadowedUnitFrames/modules/auras.lua @@ -21,7 +21,7 @@ local COA_CLASS_DISPELS = { ["WITCHHUNTER"] = { Curse = true }, ["SUNCLERIC"] = { Magic = true, Disease = true, Poison = true }, -- Sanctify ["WILDWALKER"] = { Disease = true, Poison = true }, -- Primalist (Soothing Touch — DBC says Magic, gameplay is Poison/Disease) - ["WITCHDOCTOR"] = { Disease = true, Poison = true }, -- Cleansing Idol (AoE) + ["WITCHDOCTOR"] = { Curse = true, Disease = true, Poison = true }, -- Hexbreak (806240, single-target Curse) + Cleansing Idol (504840, AoE Disease/Poison) ["TINKER"] = { Disease = true, Poison = true }, -- Nanobot Cleanser } local function getCoaDispels() From d241fd1b5e2e33e8a371bab8e1c4c73c09a5c5c9 Mon Sep 17 00:00:00 2001 From: Florian Berthold Date: Sun, 31 May 2026 15:11:21 +0200 Subject: [PATCH 2/6] feat(filters): match aura whitelist/blacklist by spell ID, not just name CoA's 3.3.5 client returns spellId as the 11th UnitAura value (stock 3.3.5a stops at 10). The scan loop now captures it and the whitelist/blacklist match on name OR spellId. The filter 'add' box accepts a numeric spell ID (stored as a numeric key so it matches UnitAura's spellId) and the list view resolves IDs to 'Name (ID)' via GetSpellInfo. Fixes ambiguity from name-only matching where CoA custom spells share display names across ranks/variants. --- ShadowedUF_Options/config.lua | 19 +++++++++++++++---- ShadowedUnitFrames/localization/enUS.lua | 2 ++ ShadowedUnitFrames/modules/auras.lua | 8 +++++--- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/ShadowedUF_Options/config.lua b/ShadowedUF_Options/config.lua index f895b76..91f346a 100644 --- a/ShadowedUF_Options/config.lua +++ b/ShadowedUF_Options/config.lua @@ -3760,14 +3760,17 @@ local function loadFilterOptions() add = { order = 0, type = "input", - name = L["Aura name"], + name = L["Aura name or spell ID"], + desc = L["Enter an exact aura name, or a numeric spell ID for precise matching (CoA's client exposes aura spell IDs)."], --dialogControl = "Aura_EditBox", hidden = false, set = function(info, value) local filterType = info[#(info) - 3] local filter = filterMap[info[#(info) - 2]] - ShadowUF.db.profile.filters[filterType][filter][value] = true + -- A purely-numeric entry is stored as a number so it matches the + -- spellId returned by UnitAura; anything else stays a name string. + ShadowUF.db.profile.filters[filterType][filter][tonumber(value) or value] = true reloadUnitAuras() rebuildFilters() @@ -3844,9 +3847,17 @@ local function loadFilterOptions() type = "description", -- Odd I know, AceConfigDialog-3.0 expands descriptions to full width if width is nil -- on the other hand we can't set width to "normal" so tricking it - width = "", + width = "", fontSize = "medium", - name = function(info) return spellMap[info[#(info)]] end, + name = function(info) + local entry = spellMap[info[#(info)]] + -- Numeric entries are spell IDs; show "Name (ID)" when the client can resolve it. + if( type(entry) == "number" ) then + local spellName = GetSpellInfo(entry) + return spellName and string.format("%s (%d)", spellName, entry) or tostring(entry) + end + return entry + end, } local spellRow = { diff --git a/ShadowedUnitFrames/localization/enUS.lua b/ShadowedUnitFrames/localization/enUS.lua index dc6c48f..e895bce 100644 --- a/ShadowedUnitFrames/localization/enUS.lua +++ b/ShadowedUnitFrames/localization/enUS.lua @@ -48,6 +48,8 @@ L["Ascending"] = "Ascending" L["Aura border style"] = "Aura border style" L["Aura filters"] = "Aura filters" L["Aura name"] = "Aura name" +L["Aura name or spell ID"] = "Aura name or spell ID" +L["Enter an exact aura name, or a numeric spell ID for precise matching (CoA's client exposes aura spell IDs)."] = "Enter an exact aura name, or a numeric spell ID for precise matching (CoA's client exposes aura spell IDs)." L["Auras"] = "Auras" L["Aura types to filter"] = "Aura types to filter" L["B"] = "B" diff --git a/ShadowedUnitFrames/modules/auras.lua b/ShadowedUnitFrames/modules/auras.lua index a7e6e65..f8ae826 100644 --- a/ShadowedUnitFrames/modules/auras.lua +++ b/ShadowedUnitFrames/modules/auras.lua @@ -531,10 +531,12 @@ local function scan(parent, frame, type, config, filter) local index = 0 while( true ) do index = index + 1 - local name, rank, texture, count, auraType, duration, endTime, caster, isStealable = UnitAura(frame.parent.unit, index, filter) + -- CoA's 3.3.5 client returns spellId as the 11th value (stock 3.3.5a stops at 10), + -- which lets the whitelist/blacklist match by ID as well as by name. + local name, rank, texture, count, auraType, duration, endTime, caster, isStealable, _, spellId = UnitAura(frame.parent.unit, index, filter) if( not name ) then break end - - if( ( not coaFilter or (auraType and coaFilter[auraType]) ) and ( not config.player or playerUnits[caster] ) and ( not parent.whitelist[type] and not parent.blacklist[type] or parent.whitelist[type] and parent.whitelist[name] or parent.blacklist[type] and not parent.blacklist[name] ) ) then + + if( ( not coaFilter or (auraType and coaFilter[auraType]) ) and ( not config.player or playerUnits[caster] ) and ( not parent.whitelist[type] and not parent.blacklist[type] or parent.whitelist[type] and ( parent.whitelist[name] or parent.whitelist[spellId] ) or parent.blacklist[type] and not ( parent.blacklist[name] or parent.blacklist[spellId] ) ) ) then -- Create any buttons we need frame.totalAuras = frame.totalAuras + 1 if( #(frame.buttons) < frame.totalAuras ) then From 2d9e9329db0d5fa76e4b6ddb9dc62f3b20177722 Mon Sep 17 00:00:00 2001 From: Florian Berthold Date: Mon, 8 Jun 2026 18:10:33 +0200 Subject: [PATCH 3/6] feat(necromancer): add Necromancer mana bar (mana + runic power at once) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the Druid mana bar for the CoA Necromancer (class token NECROMANCER): the main power bar shows the active power type while a second bar shows the other one, so mana and runic power are both visible at the same time. Adaptive — recolors to whichever resource it is currently showing, so it works regardless of which power CoA treats as primary. Off by default; toggle under Player bars (auto-hidden for non-Necromancers). --- ShadowedUF_Options/config.lua | 12 +++- ShadowedUnitFrames/ShadowedUnitFrames.lua | 1 + ShadowedUnitFrames/ShadowedUnitFrames.toc | 1 + ShadowedUnitFrames/localization/enUS.lua | 2 + ShadowedUnitFrames/modules/defaultlayout.lua | 1 + ShadowedUnitFrames/modules/necromancer.lua | 70 ++++++++++++++++++++ 6 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 ShadowedUnitFrames/modules/necromancer.lua diff --git a/ShadowedUF_Options/config.lua b/ShadowedUF_Options/config.lua index 91f346a..b66b6d3 100644 --- a/ShadowedUF_Options/config.lua +++ b/ShadowedUF_Options/config.lua @@ -2029,14 +2029,14 @@ local function loadUnitOptions() }, sep2 = {order = 1.75, type = "description", name = "", hidden = function(info) local moduleKey = info[#(info) - 1] - return ( moduleKey ~= "healthBar" and moduleKey ~= "powerBar" and moduleKey ~= "druidBar" ) or not ShadowUF.db.profile.advanced + return ( moduleKey ~= "healthBar" and moduleKey ~= "powerBar" and moduleKey ~= "druidBar" and moduleKey ~= "necromancerBar" ) or not ShadowUF.db.profile.advanced end}, invert = { order = 2, type = "toggle", name = L["Invert colors"], desc = L["Flips coloring so the bar color is shown as the background color and the background as the bar"], - hidden = function(info) return ( info[#(info) - 1] ~= "healthBar" and info[#(info) - 1] ~= "powerBar" and info[#(info) - 1] ~= "druidBar" ) or not ShadowUF.db.profile.advanced end, + hidden = function(info) return ( info[#(info) - 1] ~= "healthBar" and info[#(info) - 1] ~= "powerBar" and info[#(info) - 1] ~= "druidBar" and info[#(info) - 1] ~= "necromancerBar" ) or not ShadowUF.db.profile.advanced end, arg = "$parent.invert", }, order = { @@ -2997,6 +2997,14 @@ local function loadUnitOptions() hidden = hideRestrictedOption, arg = "druidBar.enabled", }, + necromancerBar = { + order = 1.5, + type = "toggle", + name = string.format(L["Enable %s"], L["Necromancer mana bar"]), + desc = L["Adds a second power bar to the player frame showing your mana and runic power at the same time."], + hidden = hideRestrictedOption, + arg = "necromancerBar.enabled", + }, xpBar = { order = 2, type = "toggle", diff --git a/ShadowedUnitFrames/ShadowedUnitFrames.lua b/ShadowedUnitFrames/ShadowedUnitFrames.lua index d0db7dc..c7878a0 100644 --- a/ShadowedUnitFrames/ShadowedUnitFrames.lua +++ b/ShadowedUnitFrames/ShadowedUnitFrames.lua @@ -243,6 +243,7 @@ function ShadowUF:LoadUnitDefaults() self.defaults.profile.units.player.runeBar = {enabled = false} self.defaults.profile.units.player.totemBar = {enabled = false} self.defaults.profile.units.player.druidBar = {enabled = false} + self.defaults.profile.units.player.necromancerBar = {enabled = false} self.defaults.profile.units.player.xpBar = {enabled = false} self.defaults.profile.units.player.fader = {enabled = false, combatAlpha = 1.0, inactiveAlpha = 0.60} self.defaults.profile.units.player.indicators.lfdRole = {enabled = true, size = 0, x = 0, y = 0} diff --git a/ShadowedUnitFrames/ShadowedUnitFrames.toc b/ShadowedUnitFrames/ShadowedUnitFrames.toc index 61eaa58..f7ccf8c 100644 --- a/ShadowedUnitFrames/ShadowedUnitFrames.toc +++ b/ShadowedUnitFrames/ShadowedUnitFrames.toc @@ -51,5 +51,6 @@ modules\incheal.lua modules\range.lua modules\empty.lua modules\druid.lua +modules\necromancer.lua CoAClassColors.lua diff --git a/ShadowedUnitFrames/localization/enUS.lua b/ShadowedUnitFrames/localization/enUS.lua index e895bce..1638551 100644 --- a/ShadowedUnitFrames/localization/enUS.lua +++ b/ShadowedUnitFrames/localization/enUS.lua @@ -192,6 +192,8 @@ L["Down"] = "Down" L["Druid form"] = "Druid form" L["Druid form (Short)"] = "Druid form (Short)" L["Druid mana bar"] = "Druid mana bar" +L["Necromancer mana bar"] = "Necromancer mana bar" +L["Adds a second power bar to the player frame showing your mana and runic power at the same time."] = "Adds a second power bar to the player frame showing your mana and runic power at the same time." L["Due to the nature of fake units, cast bars for %s are not super efficient and can take at most 0.10 seconds to notice a change in cast."] = "Due to the nature of fake units, cast bars for %s are not super efficient and can take at most 0.10 seconds to notice a change in cast." L["Dungeon role"] = "Dungeon role" L["Edge size"] = "Edge size" diff --git a/ShadowedUnitFrames/modules/defaultlayout.lua b/ShadowedUnitFrames/modules/defaultlayout.lua index 64a8985..1f4d573 100644 --- a/ShadowedUnitFrames/modules/defaultlayout.lua +++ b/ShadowedUnitFrames/modules/defaultlayout.lua @@ -214,6 +214,7 @@ function ShadowUF:LoadDefaultLayout(useMerge) healthBar = {background = true, colorType = "class", reactionType = "npc", height = 1.20, order = 10}, powerBar = {background = true, height = 1.0, order = 20}, druidBar = {background = true, height = 0.40, order = 25}, + necromancerBar = {background = true, height = 0.40, order = 26}, xpBar = {background = true, height = 0.25, order = 55}, castBar = {background = true, height = 0.60, order = 40, icon = "HIDE", name = {enabled = true, size = 0, anchorTo = "$parent", rank = true, anchorPoint = "CLI", x = 1, y = 0}, time = {enabled = true, size = 0, anchorTo = "$parent", anchorPoint = "CRI", x = -1, y = 0}}, runeBar = {background = false, height = 0.40, order = 70}, diff --git a/ShadowedUnitFrames/modules/necromancer.lua b/ShadowedUnitFrames/modules/necromancer.lua new file mode 100644 index 0000000..d9b6d78 --- /dev/null +++ b/ShadowedUnitFrames/modules/necromancer.lua @@ -0,0 +1,70 @@ +local Necromancer = {} +ShadowUF:RegisterModule(Necromancer, "necromancerBar", ShadowUF.L["Necromancer mana bar"], true, "NECROMANCER") + +-- Power type enums (3.3.5): 0 = mana, 6 = runic power. +local MANA, RUNIC_POWER = 0, 6 + +-- The Necromancer juggles mana and runic power. The main power bar shows +-- whichever is the active power type; this secondary bar shows the other one +-- so both are visible at once (same idea as the Druid mana bar in forms). +local function secondaryPower(unit) + local active = UnitPowerType(unit) + if( active == RUNIC_POWER ) then return MANA end + if( active == MANA ) then return RUNIC_POWER end + return nil +end + +function Necromancer:OnEnable(frame) + frame.necromancerBar = frame.necromancerBar or ShadowUF.Units:CreateBar(frame) + + frame:RegisterUnitEvent("UNIT_MAXMANA", self, "Update") + frame:RegisterUnitEvent("UNIT_MANA", self, "Update") + frame:RegisterUnitEvent("UNIT_RUNIC_POWER", self, "Update") + frame:RegisterUnitEvent("UNIT_MAXRUNIC_POWER", self, "Update") + frame:RegisterUnitEvent("UNIT_DISPLAYPOWER", self, "PowerChanged") + + frame:RegisterUpdateFunc(self, "PowerChanged") + frame:RegisterUpdateFunc(self, "Update") +end + +function Necromancer:OnDisable(frame) + frame:UnregisterAll(self) +end + +function Necromancer:UpdateColor(frame, power) + local color = ShadowUF.db.profile.powerColors[power == RUNIC_POWER and "RUNIC_POWER" or "MANA"] + + if( not ShadowUF.db.profile.units[frame.unitType].necromancerBar.invert ) then + frame.necromancerBar:SetStatusBarColor(color.r, color.g, color.b, ShadowUF.db.profile.bars.alpha) + if( not frame.necromancerBar.background.overrideColor ) then + frame.necromancerBar.background:SetVertexColor(color.r, color.g, color.b, ShadowUF.db.profile.bars.backgroundAlpha) + end + else + frame.necromancerBar.background:SetVertexColor(color.r, color.g, color.b, ShadowUF.db.profile.bars.alpha) + + color = frame.necromancerBar.background.overrideColor or color + frame.necromancerBar:SetStatusBarColor(color.r, color.g, color.b, ShadowUF.db.profile.bars.backgroundAlpha) + end +end + +function Necromancer:OnLayoutApplied(frame) + if( frame.visibility.necromancerBar ) then + self:UpdateColor(frame, secondaryPower(frame.unit) or MANA) + end +end + +-- Show the bar whenever there is a second power type to display, and recolor it +-- to match whichever resource that currently is. +function Necromancer:PowerChanged(frame) + local power = secondaryPower(frame.unit) + ShadowUF.Layout:SetBarVisibility(frame, "necromancerBar", power ~= nil) + if( power ) then + self:UpdateColor(frame, power) + end +end + +function Necromancer:Update(frame) + local power = secondaryPower(frame.unit) or MANA + frame.necromancerBar:SetMinMaxValues(0, UnitPowerMax(frame.unit, power)) + frame.necromancerBar:SetValue(UnitIsDeadOrGhost(frame.unit) and 0 or not UnitIsConnected(frame.unit) and 0 or UnitPower(frame.unit, power)) +end From 319bd0593099ac41dd2f9e840e7f9cbf61b9e464 Mon Sep 17 00:00:00 2001 From: Florian Berthold Date: Mon, 8 Jun 2026 20:42:10 +0200 Subject: [PATCH 4/6] fix(necromancer): ship bar layout in defaults so existing profiles don't crash The necromancerBar height/order/background only reached a profile via the defaultlayout merge, which runs on profile reset. Enabling the bar on a pre-existing profile left height nil, crashing Layout:PositionWidgets ("attempt to compare number with nil" at layout.lua:441). Carry the layout in the AceDB defaults so it resolves via the defaults metatable for old and new profiles alike. --- ShadowedUnitFrames/ShadowedUnitFrames.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ShadowedUnitFrames/ShadowedUnitFrames.lua b/ShadowedUnitFrames/ShadowedUnitFrames.lua index c7878a0..7fb1109 100644 --- a/ShadowedUnitFrames/ShadowedUnitFrames.lua +++ b/ShadowedUnitFrames/ShadowedUnitFrames.lua @@ -243,7 +243,11 @@ function ShadowUF:LoadUnitDefaults() self.defaults.profile.units.player.runeBar = {enabled = false} self.defaults.profile.units.player.totemBar = {enabled = false} self.defaults.profile.units.player.druidBar = {enabled = false} - self.defaults.profile.units.player.necromancerBar = {enabled = false} + -- Carry the layout (height/order/background) in the defaults, not only in the + -- defaultlayout merge: that merge only runs on profile reset, so a bar added to + -- an existing profile would otherwise have a nil height and crash + -- Layout:PositionWidgets ("attempt to compare number with nil" in layout.lua). + self.defaults.profile.units.player.necromancerBar = {enabled = false, height = 0.40, order = 26, background = true} self.defaults.profile.units.player.xpBar = {enabled = false} self.defaults.profile.units.player.fader = {enabled = false, combatAlpha = 1.0, inactiveAlpha = 0.60} self.defaults.profile.units.player.indicators.lfdRole = {enabled = true, size = 0, x = 0, y = 0} From 4d2ef28ad05d60f6a6747c01ff3d355851b3ba2d Mon Sep 17 00:00:00 2001 From: Florian Berthold Date: Wed, 10 Jun 2026 02:11:47 +0200 Subject: [PATCH 5/6] ci(release): sync release.yml from coa-template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hide_archive_links is only honored by Gitea on release edit, not create — add the PATCH step after create/lookup so auto-generated source archive links actually stay hidden (coa-template 90874c5). --- .gitea/workflows/release.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 709a7cd..2f93975 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -37,10 +37,14 @@ jobs: RID=$(curl -sf -X POST -H "Authorization: token $GITEA_TOKEN" \ -H "Content-Type: application/json" \ "$API/repos/$REPO/releases" \ - -d "$(jq -nc --arg t "$TAG" '{tag_name:$t,name:$t,draft:false,prerelease:false}')" \ + -d "$(jq -nc --arg t "$TAG" '{tag_name:$t,name:$t,draft:false,prerelease:false,hide_archive_links:true}')" \ | jq -r '.id') fi echo "release id: $RID" + # Gitea honors hide_archive_links only on edit, not create — PATCH it + # so the auto-generated Source Code (zip/tar.gz) links stay hidden. + curl -sf -X PATCH -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \ + "$API/repos/$REPO/releases/$RID" -d '{"hide_archive_links":true}' >/dev/null || true # Upload every dist/*.zip. Per-asset failures don't fail the job — # we want partial releases to still publish rather than block the # whole pipeline on one big file. From 2aad7af12d302a6fb483261cfc2cbcb070849da2 Mon Sep 17 00:00:00 2001 From: Florian Berthold Date: Wed, 10 Jun 2026 02:16:14 +0200 Subject: [PATCH 6/6] chore: bump toc Version to v3.3.0-coa.7 to match latest release tag --- ShadowedUnitFrames/ShadowedUnitFrames.toc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ShadowedUnitFrames/ShadowedUnitFrames.toc b/ShadowedUnitFrames/ShadowedUnitFrames.toc index f7ccf8c..7d3a63a 100644 --- a/ShadowedUnitFrames/ShadowedUnitFrames.toc +++ b/ShadowedUnitFrames/ShadowedUnitFrames.toc @@ -2,7 +2,7 @@ ## Title: Shadowed Unit Frames ## Notes: An apple a day keeps the raptor away, or so they say ## Author: Shadowed -## Version: v3.3.0-coa.2 +## Version: v3.3.0-coa.7 ## SavedVariables: ShadowedUFDB ## OptionalDeps: Ace3, LibSharedMedia-3.0, LibHealComm-4.0, AceGUI-3.0-SharedMediaWidgets ## X-Curse-Packaged-Version: v3.2.12