From 9bc27aef43254d739716fe7d1fae7cf0ef60a85f Mon Sep 17 00:00:00 2001 From: jdtoppin <6392799+jdtoppin@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:18:37 -0600 Subject: [PATCH 01/61] =?UTF-8?q?PR=201:=20Core=20aura=20pipeline=20?= =?UTF-8?q?=E2=80=94=20AnnotateAura,=20utility=20layer,=20SetCooldownFromA?= =?UTF-8?q?ura?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace SanitizeAura (sanitize-then-recover) with AnnotateAura (read-only _hasSecrets tag). Secret values flow directly to C-level APIs which accept them natively. Core changes: - Utils.lua: Add F.HasAnySecretValues(), remove F.IsSecretValue(), remove unjustified pcalls from range checks and FindAuraById - UnitButton.lua: AnnotateAura replaces SanitizeAura, remove all _raw* shadow fields, merge caches, secret cooldown infrastructure. New Midnight display path via SetCooldownFromAura. Dispel detection via bracket curves with pass-through color rendering. Health calculator path. CLEU removal. - Base.lua: Add SetCooldownFromAura to BorderIcon (DurationObject) and BarIcon (EvaluateElapsedPercent with linear curve) Mechanical cleanup across 12 files: - Migrate F.IsSecretValue callers to not F.IsValueNonSecret() - Replace bare issecretvalue with F.IsValueNonSecret - Replace rawequal(x, nil) with x == nil - Remove local issecretvalue/hasanysecretvalues polyfills - Replace hasanysecretvalues with F.HasAnySecretValues Addresses all feedback from Krealle and ljosberinn on PRs 462/463. Co-Authored-By: Claude Opus 4.6 (1M context) --- Defaults/Indicator_Bleeds.lua | 3 +- Defaults/Indicator_DefaultSpells.lua | 14 +- Defaults/Indicator_Defaults.lua | 4 +- Indicators/Actions.lua | 7 +- Indicators/Base.lua | 170 ++- Indicators/Custom.lua | 10 +- Indicators/TargetCounter.lua | 2 +- Libs/LibGroupInfo.lua | 11 +- Media/gradient.tga | Bin 0 -> 34 bytes RaidFrames/UnitButton.lua | 1649 ++++++++++++++++++++------ Utilities/QuickAssist.lua | 2 +- Utilities/QuickCast.lua | 4 +- Utils.lua | 62 +- 13 files changed, 1515 insertions(+), 423 deletions(-) create mode 100644 Media/gradient.tga diff --git a/Defaults/Indicator_Bleeds.lua b/Defaults/Indicator_Bleeds.lua index 46c556cc..3a308e13 100644 --- a/Defaults/Indicator_Bleeds.lua +++ b/Defaults/Indicator_Bleeds.lua @@ -1,11 +1,12 @@ local _, Cell = ... local I = Cell.iFuncs +local F = Cell.funcs local bleedList function I.CheckDebuffType(debuffType, spellId) -- Midnight 12.0.0+: debuffType and spellId may be secret — can't compare or use as table key - if issecretvalue and (issecretvalue(spellId) or issecretvalue(debuffType)) then + if not F.IsValueNonSecret(spellId) or not F.IsValueNonSecret(debuffType) then return debuffType end if (not debuffType or debuffType == "") and bleedList[spellId] then diff --git a/Defaults/Indicator_DefaultSpells.lua b/Defaults/Indicator_DefaultSpells.lua index 29988548..a86e3717 100644 --- a/Defaults/Indicator_DefaultSpells.lua +++ b/Defaults/Indicator_DefaultSpells.lua @@ -184,7 +184,7 @@ function I.UpdateAoEHealings(t) end function I.IsAoEHealing(name, id) - if issecretvalue and (issecretvalue(name) or issecretvalue(id)) then return end + if not F.IsValueNonSecret(name) or not F.IsValueNonSecret(id) then return end return builtInAoEHealings[name] or builtInAoEHealings[id] or customAoEHealings[id] end @@ -339,7 +339,7 @@ end local UnitIsUnit = UnitIsUnit local bos = F.GetSpellInfo(6940) -- 牺牲祝福 function I.IsExternalCooldown(name, id, source, target) - if issecretvalue and (issecretvalue(name) or issecretvalue(id)) then return end + if not F.IsValueNonSecret(name) or not F.IsValueNonSecret(id) then return end if name == bos then if source and target then -- NOTE: hide bos on caster @@ -490,7 +490,7 @@ function I.UpdateDefensives(t) end function I.IsDefensiveCooldown(name, id) - if issecretvalue and (issecretvalue(name) or issecretvalue(id)) then return end + if not F.IsValueNonSecret(name) or not F.IsValueNonSecret(id) then return end return builtInDefensives[name] or builtInDefensives[id] or customDefensives[id] end @@ -549,7 +549,7 @@ do end function I.IsTankActiveMitigation(spellId) - if issecretvalue and issecretvalue(spellId) then return end + if not F.IsValueNonSecret(spellId) then return end return tankActiveMitigations[spellId] end @@ -737,7 +737,7 @@ do end function I.IsDrinking(name) - if issecretvalue and issecretvalue(name) then return end + if not F.IsValueNonSecret(name) then return end return drinks[name] end @@ -811,7 +811,7 @@ local spells = { 1244893, -- 救世主道标 - Beacon of the Savior -- priest - 139, -- 恢复 - Renew + -- 139, -- 恢复 - Renew (removed in 12.0) 200829, -- 恳求 - Plea (added in 12.0, Disc) 41635, -- 愈合祷言 - Prayer of Mending 17, -- 真言术:盾 - Power Word: Shield @@ -1317,6 +1317,6 @@ function I.UpdateCrowdControls(t) end function I.IsCrowdControls(name, id) - if issecretvalue and (issecretvalue(name) or issecretvalue(id)) then return end + if not F.IsValueNonSecret(name) or not F.IsValueNonSecret(id) then return end return builtInCrowdControls[name] or builtInCrowdControls[id] or customCrowdControls[name] end diff --git a/Defaults/Indicator_Defaults.lua b/Defaults/Indicator_Defaults.lua index 2890810c..abe96b13 100644 --- a/Defaults/Indicator_Defaults.lua +++ b/Defaults/Indicator_Defaults.lua @@ -268,7 +268,7 @@ end ------------------------------------------------- function I.GetDebuffTypeColor(debuffType) -- Midnight 12.0.0+: debuffType may be secret; cannot use as table key - if issecretvalue and issecretvalue(debuffType) then return 0, 0, 0 end + if not F.IsValueNonSecret(debuffType) then return 0, 0, 0 end if debuffType and CellDB["debuffTypeColor"][debuffType] then return CellDB["debuffTypeColor"][debuffType]["r"], CellDB["debuffTypeColor"][debuffType]["g"], CellDB["debuffTypeColor"][debuffType]["b"] @@ -304,4 +304,4 @@ function I.ResetDebuffTypeColor() CellDB["debuffTypeColor"]["Bleed"] = {r = 1, g = 0.2, b = 0.6} -- add cleu -- CellDB["debuffTypeColor"].cleu = {r=0, g=1, b=1} -end \ No newline at end of file +end diff --git a/Indicators/Actions.lua b/Indicators/Actions.lua index ea50e3b0..0f97b076 100644 --- a/Indicators/Actions.lua +++ b/Indicators/Actions.lua @@ -35,14 +35,15 @@ eventFrame:SetScript("OnEvent", function(self, event, unit, castGUID, spellID) -- filter out players not in your group if not (UnitInRaid(unit) or UnitInParty(unit) or unit == "player" or unit == "pet") then return end + -- Midnight 12.0.0+: spellID from UNIT_SPELLCAST_SUCCEEDED may be secret + -- during restricted contexts; skip if so since we can't use it as a table key + if not F.IsValueNonSecret(spellID) then return end + if Cell.vars.actionsDebugModeEnabled then local name = F.GetSpellInfo(spellID) print("|cFFFF3030[Cell]|r |cFFB2B2B2" .. event .. ":|r", unit, "|cFF00FF00" .. (spellID or "nil") .. "|r", name) end - -- Midnight 12.0.0+: spellID from UNIT_SPELLCAST_SUCCEEDED is secret during restricted contexts - if Cell.isMidnight and issecretvalue and issecretvalue(spellID) then return end - if Cell.vars.actions[spellID] then F.HandleUnitButton("unit", unit, Display, unpack(Cell.vars.actions[spellID])) end diff --git a/Indicators/Base.lua b/Indicators/Base.lua index b1c89250..1ba073bc 100644 --- a/Indicators/Base.lua +++ b/Indicators/Base.lua @@ -13,15 +13,13 @@ local P = Cell.pixelPerfectFuncs local LCG = LibStub("LibCustomGlow-1.0") --- Midnight 12.0.0+: aura count (applications) may be secret; sanitize for safe comparisons/table-key use -local function _SanitizeCount(count) - if issecretvalue and issecretvalue(count) then return 0 end - return count or 0 -end - -- Midnight 12.0.0+: helper for stack text display (most common pattern) +-- Secret stack counts are passed through directly; SetText is C-level and +-- renders secret values safely. We cannot test == 0/1, so secret stacks +-- always display (may show "1" for single-stack auras, which is acceptable). local function _StackText(count) - count = _SanitizeCount(count) + if not F.IsValueNonSecret(count) then return count end + count = count or 0 return (count == 0 or count == 1) and "" or count end @@ -117,7 +115,11 @@ end local function VerticalCooldown_OnUpdate(self, elapsed) self.elapsed = (self.elapsed or 0) + elapsed if self.elapsed >= 0.1 then - self:SetValue(self:GetValue() + self.elapsed) + -- Track value in Lua variable instead of GetValue() — avoids permanent + -- taint from secret SetValue() calls (12.0+). GetValue() returns tainted + -- forever once any secret value is passed to SetValue/SetMinMaxValues. + self._currentValue = (self._currentValue or 0) + self.elapsed + self:SetValue(self._currentValue) self.elapsed = 0 end end @@ -140,7 +142,8 @@ local function VerticalCooldown_ShowCooldown(self, start, duration, _, icon, deb self.elapsed = 0.1 -- update immediately self:SetMinMaxValues(0, duration) - self:SetValue(GetTime() - start) + self._currentValue = GetTime() - start + self:SetValue(self._currentValue) self:Show() end @@ -432,6 +435,99 @@ local function Icon_OnUpdate_ElapsedTime(frame, elapsed) end end +------------------------------------------------- +-- Midnight 12.0.0+: C-level cooldown display via DurationObject +-- These methods use GetAuraDuration → DurationObject → SetCooldownFromDurationObject +-- (BorderIcon) or EvaluateElapsedPercent (BarIcon). No Lua arithmetic on secret values. +-- APIs return nil for expired/invalid auras (no pcall needed). +------------------------------------------------- +local _GetAuraDuration = C_UnitAuras and C_UnitAuras.GetAuraDuration +local _GetAuraAppDisplayCount = C_UnitAuras and C_UnitAuras.GetAuraApplicationDisplayCount + +-- BorderIcon: SetCooldownFromAura — drives CooldownFrame with DurationObject +local function BorderIcon_SetCooldownFromAura(frame, unit, auraInstanceID, texture, refreshing) + -- Icon and stack + frame.icon:SetTexture(texture) + if _GetAuraAppDisplayCount then + local displayCount = _GetAuraAppDisplayCount(unit, auraInstanceID) + frame.stack:SetText(displayCount and _StackText(displayCount) or "") + else + frame.stack:SetText("") + end + + -- Cooldown swipe via DurationObject + -- Swipe color defaults to black; UnitButton.lua overrides border/swipe color + -- for dispel types via bracket curves after this call. + local durObj = _GetAuraDuration and _GetAuraDuration(unit, auraInstanceID) + if durObj and frame.cooldown and frame.cooldown._SetCooldown + and frame.cooldown.SetCooldownFromDurationObject then + frame.cooldown:SetReverse(true) + frame.cooldown:SetCooldownFromDurationObject(durObj, true) + -- Keep border visible as base color (caller sets color); black swipe fills over it + frame.cooldown:Show() + else + -- No cooldown animation — show static border + frame.border:Show() + frame.border:SetColorTexture(0, 0, 0) + frame.cooldown:Hide() + end + + -- Duration text hidden on Midnight (SetFormattedText produces invisible output with secrets) + frame.duration:Hide() + frame:SetScript("OnUpdate", nil) + frame:Show() + + if refreshing then + frame.ag:Play() + end +end + +-- BarIcon: SetCooldownFromAura — CooldownFrame overlay for clock-swipe animation. +-- DurationObject:EvaluateElapsedPercent/EvaluateRemainingPercent both error in tainted +-- combat contexts, so StatusBar can't be driven by DurationObject directly. +-- NOTE: Built-in indicators (debuffs, defensives, externals) use BorderIcon on Midnight. +-- This BarIcon path remains for user-created custom indicators that use BarIcon. +local function BarIcon_SetCooldownFromAura(frame, unit, auraInstanceID, texture, refreshing) + frame.icon:SetTexture(texture) + if _GetAuraAppDisplayCount then + local displayCount = _GetAuraAppDisplayCount(unit, auraInstanceID) + frame.stack:SetText(displayCount and _StackText(displayCount) or "") + else + frame.stack:SetText("") + end + + local durObj = _GetAuraDuration and _GetAuraDuration(unit, auraInstanceID) + if durObj then + if not frame._midnightCooldown then + local cd = CreateFrame("Cooldown", nil, frame) + cd:SetAllPoints(frame.icon) + cd:SetFrameLevel(frame:GetFrameLevel() + 5) + cd:SetSwipeTexture(Cell.vars.whiteTexture) + cd:SetSwipeColor(0, 0, 0) + cd:SetDrawEdge(false) + cd:SetReverse(true) + cd:SetHideCountdownNumbers(true) + cd.noCooldownCount = true + frame._midnightCooldown = cd + end + if frame._midnightCooldown.SetCooldownFromDurationObject then + frame._midnightCooldown:SetCooldownFromDurationObject(durObj, false) + frame._midnightCooldown:Show() + end + else + if frame._midnightCooldown then frame._midnightCooldown:Hide() end + end + + frame.cooldown:Hide() + frame.duration:Hide() + frame:SetBackdropColor(0, 0, 0) + frame:Show() + + if refreshing then + frame.ag:Play() + end +end + ------------------------------------------------- -- CreateAura_BorderIcon ------------------------------------------------- @@ -568,7 +664,13 @@ function I.CreateAura_BorderIcon(name, parent, borderSize) frame.SetFont = Shared_SetFont frame.SetBorder = BorderIcon_SetBorder frame.SetCooldown = BorderIcon_SetCooldown + frame.SetCooldownFromAura = BorderIcon_SetCooldownFromAura frame.ShowDuration = BorderIcon_ShowDuration + -- BarIcon-compatible methods (no-ops for BorderIcon, needed when used as + -- cooldown indicator child frames which call these on all children) + frame.ShowAnimation = function() end + frame.ShowStack = function() end + frame.SetupGlow = function() end frame.UpdatePixelPerfect = BorderIcon_UpdatePixelPerfect return frame @@ -688,6 +790,7 @@ function I.CreateAura_BarIcon(name, parent) frame.SetFont = Shared_SetFont frame.SetCooldown = BarIcon_SetCooldown + frame.SetCooldownFromAura = BarIcon_SetCooldownFromAura frame.ShowDuration = Shared_ShowDuration frame.ShowStack = Shared_ShowStack frame.ShowAnimation = BarIcon_ShowAnimation @@ -1050,12 +1153,20 @@ end local circled = {"①","②","③","④","⑤","⑥","⑦","⑧","⑨","⑩","⑪","⑫","⑬","⑭","⑮","⑯","⑰","⑱","⑲","⑳","㉑","㉒","㉓","㉔","㉕","㉖","㉗","㉘","㉙","㉚","㉛","㉜","㉝","㉞","㉟","㊱","㊲","㊳","㊴","㊵","㊶","㊷","㊸","㊹","㊺","㊻","㊼","㊽","㊾","㊿"} local function Text_SetCooldown(frame, start, duration, debuffType, texture, count) + -- Secret stack count: can't compare or index circled[], pass directly to + -- SetText (C-level, renders secret values safely) + local isSecretCount = not F.IsValueNonSecret(count) + if duration == 0 then -- always show stack - count = _SanitizeCount(count) - count = count == 0 and 1 or count - count = frame.circledStackNums and circled[count] or count - frame.text:SetText(count) + if isSecretCount then + frame.text:SetText(count) + else + count = count or 0 + count = count == 0 and 1 or count + count = frame.circledStackNums and circled[count] or count + frame.text:SetText(count) + end frame.text:SetTextColor(frame.colors[1][1], frame.colors[1][2], frame.colors[1][3], frame.colors[1][4]) frame:SetScript("OnUpdate", nil) frame._count = nil @@ -1068,27 +1179,36 @@ local function Text_SetCooldown(frame, start, duration, debuffType, texture, cou frame._duration = duration if frame.durationTbl[1] then - count = _SanitizeCount(count) - if frame.showStack and count ~= 0 then - if frame.circledStackNums then - frame._count = circled[count].." " + if isSecretCount then + -- Can't concatenate secret with " "; skip stack prefix for duration text + frame._count = "" + else + count = count or 0 + if frame.showStack and count ~= 0 then + if frame.circledStackNums then + frame._count = circled[count].." " + else + frame._count = count.." " + end else - frame._count = count.." " + frame._count = "" end - else - frame._count = "" end frame._elapsed = 0.1 -- update immediately frame:SetScript("OnUpdate", Text_OnUpdateDuration) else -- always show stack - count = _SanitizeCount(count) - count = count == 0 and 1 or count - if frame.circledStackNums then - frame.text:SetText(circled[count]) - else + if isSecretCount then frame.text:SetText(count) + else + count = count or 0 + count = count == 0 and 1 or count + if frame.circledStackNums then + frame.text:SetText(circled[count]) + else + frame.text:SetText(count) + end end frame._elapsed = 0.1 -- update immediately diff --git a/Indicators/Custom.lua b/Indicators/Custom.lua index c7cce5eb..ccc04617 100644 --- a/Indicators/Custom.lua +++ b/Indicators/Custom.lua @@ -11,7 +11,7 @@ local I = Cell.iFuncs -- - DO NOT compare secret values with == or use arithmetic on them -- - DO NOT use secret values as table keys -- - FontString:SetText() and SetTexture() ACCEPT secrets safely --- - Use issecretvalue(val) to check if a value is secret +-- - Use F.IsValueNonSecret(val) to check if a value is non-secret -- - Use GetRestrictedActionStatus(0) to check if aura access is restricted ------------------------------------------------- @@ -257,13 +257,13 @@ function I.UpdateCustomIndicators(unitButton, auraInfo) -- Midnight 12.0.0+: bail only if aura type (isHelpful) is secret — we need it to classify buff/debuff. -- spellId may still be secret for some auras even with hotfix; don't bail on spellId. - if issecretvalue and issecretvalue(auraInfo.isHelpful) then return end + if not F.IsValueNonSecret(auraInfo.isHelpful) then return end local auraType = auraInfo.isHelpful and "buff" or "debuff" local icon = auraInfo.icon -- Midnight 12.0.0+: dispelName may be secret; sanitize to avoid table-key/comparison crashes downstream local rawDispelName = auraInfo.dispelName - local debuffType = auraInfo.isHarmful and ((rawDispelName and (not issecretvalue or not issecretvalue(rawDispelName))) and rawDispelName or "") or nil + local debuffType = auraInfo.isHarmful and ((rawDispelName and F.IsValueNonSecret(rawDispelName)) and rawDispelName or "") or nil local count = auraInfo.applications local duration = auraInfo.duration -- Use per-aura check for duration: non-secret auras get real timers, secret ones get zeroed. @@ -276,7 +276,7 @@ function I.UpdateCustomIndicators(unitButton, auraInfo) end local castByMe = auraInfo.sourceUnit == "player" or auraInfo.sourceUnit == "pet" - -- check Bleed + -- check Bleed (isHarmful is safe: guarded by isHelpful non-secret check above) if auraInfo.isHarmful then debuffType = I.CheckDebuffType(debuffType, auraInfo.spellId) end @@ -291,7 +291,7 @@ function I.UpdateCustomIndicators(unitButton, auraInfo) end -- Midnight 12.0.0+: spell (name or spellId) may be secret; cannot use as table key - if spell and (not issecretvalue or not issecretvalue(spell)) and indicatorTable["auras"][spell] or (indicatorTable["auras"][0] and duration ~= 0) then -- is in indicator spell list + if spell and F.IsValueNonSecret(spell) and indicatorTable["auras"][spell] or (indicatorTable["auras"][0] and duration ~= 0) then -- is in indicator spell list -- check caster if (indicatorTable["castBy"] == "me" and castByMe) or (indicatorTable["castBy"] == "others" and not castByMe) or (indicatorTable["castBy"] == "anyone") then if auraType == "buff" then diff --git a/Indicators/TargetCounter.lua b/Indicators/TargetCounter.lua index 3bd87630..21d3d32c 100644 --- a/Indicators/TargetCounter.lua +++ b/Indicators/TargetCounter.lua @@ -64,7 +64,7 @@ local function StartTicker() local target = UnitGUID(unit.."target") -- Midnight 12.0.0+: UnitGUID for nameplate targets may return secret strings - if Cell.isMidnight and issecretvalue and issecretvalue(target) then + if not F.IsValueNonSecret(target) then nameplateTargets[unit] = nil elseif not target then -- no target nameplateTargets[unit] = nil diff --git a/Libs/LibGroupInfo.lua b/Libs/LibGroupInfo.lua index a9644ee4..d6df8085 100644 --- a/Libs/LibGroupInfo.lua +++ b/Libs/LibGroupInfo.lua @@ -23,6 +23,15 @@ local IS_RETAIL = WOW_PROJECT_ID == WOW_PROJECT_MAINLINE local IS_WRATH = WOW_PROJECT_ID == WOW_PROJECT_WRATH_CLASSIC local IS_MISTS = WOW_PROJECT_ID == WOW_PROJECT_MISTS_CLASSIC +-- Cell addon integration: check for secret values on Midnight 12.0.0+ +-- issecretvalue is cached at file scope for library independence; +-- Cell files should use F.IsValueNonSecret() instead. +local _issecretvalue = rawget(_G, "issecretvalue") +local function IsValueSecret(val) + if _issecretvalue and _issecretvalue(val) then return true end + return false +end + local debugMode = false local function Print(...) if debugMode then @@ -599,7 +608,7 @@ end function frame:UNIT_LEVEL(unit) local guid = UnitGUID(unit) -- Midnight 12.0.0+: nameplate GUIDs may be secret; cannot use as table key - if not guid or (issecretvalue and issecretvalue(guid)) then return end + if not guid or IsValueSecret(guid) then return end if cache[guid] then cache[guid].level = UnitLevel(unit) end diff --git a/Media/gradient.tga b/Media/gradient.tga new file mode 100644 index 0000000000000000000000000000000000000000..d1f7d9c73fd62284322cc50743206829901c9531 GIT binary patch literal 34 bcmZQzU}As)Mg|rJ1&;p^unI_p0x<&sgSrkP literal 0 HcmV?d00001 diff --git a/RaidFrames/UnitButton.lua b/RaidFrames/UnitButton.lua index d687cf01..78c9a584 100644 --- a/RaidFrames/UnitButton.lua +++ b/RaidFrames/UnitButton.lua @@ -25,6 +25,8 @@ local UnitHealthMax = UnitHealthMax local UnitGetIncomingHeals = UnitGetIncomingHeals local UnitGetTotalAbsorbs = UnitGetTotalAbsorbs local UnitGetTotalHealAbsorbs = UnitGetTotalHealAbsorbs +-- 12.0+ APIs: secret value globals are only referenced in Utils.lua wrappers. +-- All callsites use F.IsValueNonSecret() / F.HasAnySecretValues() instead. local UnitIsFriend = UnitIsFriend local UnitIsUnit = UnitIsUnit local UnitIsPlayer = UnitIsPlayer @@ -41,11 +43,10 @@ local SetRaidTargetIconTexture = SetRaidTargetIconTexture local GetTime = GetTime local GetRaidTargetIndex = GetRaidTargetIndex local GetReadyCheckStatus = GetReadyCheckStatus +local GetSpecialization = GetSpecialization +local GetSpecializationRole = GetSpecializationRole local UnitHasVehicleUI = UnitHasVehicleUI --- local UnitInVehicle = UnitInVehicle --- local UnitUsingVehicle = UnitUsingVehicle local UnitIsCharmed = UnitIsCharmed -local UnitIsPlayer = UnitIsPlayer local UnitInPartyIsAI = UnitInPartyIsAI local UnitGroupRolesAssigned = UnitGroupRolesAssigned local UnitThreatSituation = UnitThreatSituation @@ -56,16 +57,25 @@ local UnitIsGroupAssistant = UnitIsGroupAssistant local InCombatLockdown = InCombatLockdown local UnitAffectingCombat = UnitAffectingCombat local UnitPhaseReason = UnitPhaseReason --- local UnitBuff = UnitBuff --- local UnitDebuff = UnitDebuff local IsInRaid = IsInRaid local UnitDetailedThreatSituation = UnitDetailedThreatSituation -local GetAuraDataByAuraInstanceID = C_UnitAuras.GetAuraDataByAuraInstanceID +local CombatLogGetCurrentEventInfo = CombatLogGetCurrentEventInfo -- nil in 12.0+ +local _GetAuraDataByAuraInstanceID = C_UnitAuras.GetAuraDataByAuraInstanceID local GetAuraSlots = C_UnitAuras.GetAuraSlots -local GetAuraDataBySlot = C_UnitAuras.GetAuraDataBySlot +local _GetAuraDataBySlot = C_UnitAuras.GetAuraDataBySlot +local _GetAuraDispelTypeColor = C_UnitAuras.GetAuraDispelTypeColor +local _IsAuraFilteredOut = C_UnitAuras.IsAuraFilteredOutByInstanceID +local _GetAuraDuration = C_UnitAuras.GetAuraDuration -- 12.0+: NOT restricted, returns LuaDurationObject +-- wrapped versions applied after AnnotateAura is defined (see below) +local GetAuraDataByAuraInstanceID, GetAuraDataBySlot local IsDelveInProgress = C_PartyInfo.IsDelveInProgress -local UnitGetDetailedHealPrediction = UnitGetDetailedHealPrediction -- nil pre-12.0 -local CreateUnitHealPredictionCalculator = CreateUnitHealPredictionCalculator -- nil pre-12.0 +-- 12.0+ heal prediction and interpolation APIs (nil pre-12.0) +local UnitGetDetailedHealPrediction = UnitGetDetailedHealPrediction +local CreateUnitHealPredictionCalculator = CreateUnitHealPredictionCalculator +local UnitHealthPercent = UnitHealthPercent +local AbbreviateNumbers = AbbreviateNumbers +local SBI_ExponentialEaseOut = Enum and Enum.StatusBarInterpolation and Enum.StatusBarInterpolation.ExponentialEaseOut +local SBI_Immediate = Enum and Enum.StatusBarInterpolation and Enum.StatusBarInterpolation.Immediate --! for AI followers, UnitClassBase is buggy local UnitClassBase = function(unit) @@ -77,14 +87,14 @@ local shieldEnabled, overshieldEnabled, overshieldReverseFillEnabled local absorbEnabled, absorbInvertColor -- Midnight: Curve for CELL_FADE_OUT_HEALTH_PERCENT feature --- Maps health percent → alpha so we can evaluate secret health% without comparisons +-- Maps health percent -> alpha so we can evaluate secret health% without comparisons local fadeOutHealthCurve local fadeOutHealthCurve_threshold -- track last threshold to know when to rebuild local fadeOutHealthCurve_alpha -- track last outOfRangeAlpha to know when to rebuild -- Builds/rebuilds the fade-out health curve when threshold or alpha changes. --- health% < threshold → alpha 1.0 (fully visible, needs healing) --- health% >= threshold → outOfRangeAlpha (faded out, healthy enough) +-- health% < threshold -> alpha 1.0 (fully visible, needs healing) +-- health% >= threshold -> outOfRangeAlpha (faded out, healthy enough) local function RebuildFadeOutHealthCurve() if not Cell.isMidnight or not C_CurveUtil then return end local threshold = CELL_FADE_OUT_HEALTH_PERCENT @@ -109,6 +119,147 @@ local function RebuildFadeOutHealthCurve() fadeOutHealthCurve_alpha = alpha end +local CheckCLEURequired + +------------------------------------------------- +-- 12.0+ aura annotation (read-only tagging) +------------------------------------------------- +-- AnnotateAura sets a single _hasSecrets flag on the aura table without +-- mutating any of Blizzard's aura fields. Secret values flow through +-- to C-level APIs (SetTexture, SetText, SetCooldownFromDurationObject, etc.) +-- which accept them natively. +local function AnnotateAura(aura) + if not aura then return nil end + + -- auraInstanceID is the cache key — if secret, drop the aura + if not F.IsValueNonSecret(aura.auraInstanceID) then return nil end + + -- Fast path: spellId readable and whitelisted → all fields are non-secret + if F.IsValueNonSecret(aura.spellId) and F.IsSpellAuraNonSecret(aura.spellId) then + aura._hasSecrets = false + return aura + end + + -- Slow path: some or all fields are secret (12.0+ in restricted context) + aura._hasSecrets = true + return aura +end + +-- Wrap aura data retrieval to annotate secret state (read-only tag only) +GetAuraDataByAuraInstanceID = function(unit, id) + return AnnotateAura(_GetAuraDataByAuraInstanceID(unit, id)) +end +GetAuraDataBySlot = function(unit, slot) + return AnnotateAura(_GetAuraDataBySlot(unit, slot)) +end + +------------------------------------------------- +-- 12.0+ dispel display via bracket curves +------------------------------------------------- +-- WoW step curves CLAMP below the first point (never return nil). +-- So we can't use nil/non-nil for type detection. Instead: +-- +-- 1. Use F.IsValueNonSecret(aura.dispelName) to detect dispellable vs non-dispellable +-- (non-dispellable = nil, dispellable = SECRET in combat) +-- 2. Use "bracket curves" with 3 points to isolate each type: +-- e.g. Magic: {0:transparent, 1:visible, 2:transparent} +-- The step curve returns visible only for index 1, transparent for all others. +-- 3. Pass raw (secret) colors to C-level SetVertexColor for rendering. +-- +-- Dispel type indices: None=0, Magic=1, Curse=2, Disease=3, Poison=4, Enrage=9, Bleed=11 + +local _dispelCurvesReady = false + +-- Highlight curve: maps each type -> its correct display color +local _dispelHighlightCurve + +-- Bracket curves: isolate each type (visible alpha for match, 0 alpha for non-match) +local _bracketCurves = {} -- [typeName] = curve + +-- Type definitions for curve building (order matches Built-in.lua dispelOrder) +local _dispelTypes = { + {name = "Magic", idx = 1, nextIdx = 2, r = 0.20, g = 0.60, b = 1.00}, + {name = "Curse", idx = 2, nextIdx = 3, r = 0.60, g = 0.00, b = 1.00}, + {name = "Disease", idx = 3, nextIdx = 4, r = 0.60, g = 0.40, b = 0.00}, + {name = "Poison", idx = 4, nextIdx = 5, r = 0.00, g = 0.60, b = 0.00}, + {name = "Bleed", idx = 11, nextIdx = nil, r = 1.00, g = 0.20, b = 0.60}, +} + +-- Feature check via API existence (no pcall — check before calling) +if C_CurveUtil and C_CurveUtil.CreateColorCurve and _GetAuraDispelTypeColor + and Enum and Enum.LuaCurveType and Enum.LuaCurveType.Step then + local stepType = Enum.LuaCurveType.Step + local transparent = CreateColor(0, 0, 0, 0) + + -- highlight curve: all types -> correct colors, non-dispellable -> transparent + _dispelHighlightCurve = C_CurveUtil.CreateColorCurve() + _dispelHighlightCurve:SetType(stepType) + _dispelHighlightCurve:AddPoint(0, transparent) -- None + for _, t in ipairs(_dispelTypes) do + _dispelHighlightCurve:AddPoint(t.idx, CreateColor(t.r, t.g, t.b, 1)) + end + _dispelHighlightCurve:AddPoint(9, transparent) -- Enrage + + -- bracket curves: isolate each type + -- e.g. Magic: {0:transparent, 1:typeColor, 2:transparent} + for _, t in ipairs(_dispelTypes) do + local curve = C_CurveUtil.CreateColorCurve() + curve:SetType(stepType) + curve:AddPoint(0, transparent) -- below target: invisible + curve:AddPoint(t.idx, CreateColor(t.r, t.g, t.b, 1)) -- target: visible + if t.nextIdx then + curve:AddPoint(t.nextIdx, transparent) -- above target: invisible + end + _bracketCurves[t.name] = curve + end + + _dispelCurvesReady = true +end + +-- Get a ColorMixin from a curve for a specific aura. +-- Returns nil if curve is nil or aura has expired (API returns nil for invalid auras). +local function _getCurveColor(unit, auraInstanceID, curve) + if not curve then return nil end + return _GetAuraDispelTypeColor(unit, auraInstanceID, curve) +end + +-- Gradient texture path: 1x4 white texture with baked-in vertical alpha gradient +-- (opaque at bottom, transparent at top). Used with SetVertexColor for secret +-- dispel display — the alpha gradient comes from the texture file, and the color +-- is applied via C-level SetVertexColor which handles secret values. +local GRADIENT_TEXTURE = "Interface\\AddOns\\Cell\\Media\\gradient" + +-- Lazily create a single gradient overlay texture for secret dispel display. +local function _ensureGradientOverlay(dispels) + if dispels._secretGradientOverlay then return dispels._secretGradientOverlay end + + local hlParent = dispels.highlight:GetParent() + local tex = hlParent:CreateTexture(nil, "ARTWORK", nil, 0) + tex:SetTexture(GRADIENT_TEXTURE) + tex:SetBlendMode("BLEND") + tex:Hide() + + dispels._secretGradientOverlay = tex + return tex +end + +-- Debug dispel trace (gated behind Cell.debug) +local _dispelTraceEnabled = false +if Cell.debug then + function F.ToggleDispelTrace() + _dispelTraceEnabled = not _dispelTraceEnabled + print("|cff00ff00[Cell]|r Dispel trace:", _dispelTraceEnabled and "ON" or "OFF") + end + function F.PrintDispelDiag() + print("|cff00ff00[Cell Dispel Diag]|r") + print(" GetAuraDispelTypeColor:", _GetAuraDispelTypeColor and "exists" or "MISSING") + print(" IsAuraFilteredOut:", _IsAuraFilteredOut and "exists" or "MISSING") + print(" bracketCurves:", _dispelCurvesReady and "initialized" or "NOT READY") + print(" highlightCurve:", _dispelHighlightCurve and "yes" or "NO") + print(" InCombatLockdown:", InCombatLockdown() and "YES" or "NO") + end +end + ------------------------------------------------- -- unit button func declarations ------------------------------------------------- @@ -175,6 +326,7 @@ local function ResetIndicators() elseif t["indicatorName"] == "targetedSpells" then I.UpdateTargetedSpellsNum(t["num"]) I.ShowAllTargetedSpells(t["showAllSpells"]) + I.UpdateTargetedSpellsDisplayMode(t["displayMode"] or "Both") I.EnableTargetedSpells(t["enabled"]) -- update actions @@ -631,15 +783,50 @@ local function UpdateIndicators(layout, indicatorName, setting, value, value2) end end, true) elseif indicatorName == "powerText" then - F.IterateAllUnitButtons(function(b) + -- Ensure SetFormat has been called (SetValue is noop until then). + local fmt + for _, t in next, Cell.vars.currentLayoutTable["indicators"] do + if t["indicatorName"] == "powerText" then + fmt = t["format"] + break + end + end + + -- IterateAllUnitButtons doesn't reach active party header + -- children. Use the .units sub-table which maps unit tokens + -- to the actual visible buttons assigned by the secure header. + local function UpdatePowerForButton(b) + local indicator = b.indicators[indicatorName] + if indicator and fmt then + indicator:SetFormat(fmt) + end b._shouldShowPowerText = ShouldShowPowerText(b) CheckPowerEventRegistration(b) if b._shouldShowPowerText then B.UpdatePowerText(b) else - b.indicators[indicatorName]:Hide() + if indicator then indicator:Hide() end end - end, true) + end + + -- Standard iterator (covers solo, raid, pet, npc, spotlight) + F.IterateAllUnitButtons(UpdatePowerForButton, true) + + -- Also reach active party/raid buttons via .units tables + if Cell.unitButtons.party and Cell.unitButtons.party.units then + for _, b in pairs(Cell.unitButtons.party.units) do + UpdatePowerForButton(b) + end + end + if Cell.unitButtons.raid then + for header, buttons in pairs(Cell.unitButtons.raid) do + if type(buttons) == "table" and buttons.units then + for _, b in pairs(buttons.units) do + UpdatePowerForButton(b) + end + end + end + end elseif indicatorName == "shieldBar" then F.IterateAllUnitButtons(function(b) B.UpdateShield(b) @@ -1161,56 +1348,89 @@ end local function ResetDebuffVars(self) self._debuffs.resurrectionFound = false self._debuffs.crowdControlsFound = 0 + self._dispelAuraID = nil + self._dispelUnit = nil self.states.BGOrb = nil -- TODO: move to _debuffs end + local function HandleDebuff(self, auraInfo) local auraInstanceID = auraInfo.auraInstanceID + local unit = self.states.displayedUnit + local name = auraInfo.name - -- auraInfo.icon may be a secret fileID on Midnight 12.0.0+ - -- SetTexture() accepts secret numbers, so this works as-is local icon = auraInfo.icon local count = auraInfo.applications - -- Midnight 12.0.0+: dispelName may be secret (truthy, so `or ""` won't help); sanitize it - local debuffType = (auraInfo.dispelName and (not issecretvalue or not issecretvalue(auraInfo.dispelName))) and auraInfo.dispelName or "" - local expirationTime = auraInfo.expirationTime or 0 - local duration = auraInfo.duration - -- Midnight 12.0.0+: expirationTime and duration may be secret even when spellId is not. - -- Guard per-field: non-secret temporal fields get proper duration/cooldown display. - local start - if F.IsValueNonSecret(expirationTime) and F.IsValueNonSecret(duration) then - start = expirationTime - duration + local spellId = auraInfo.spellId + + -- Dispel detection (pass-through): + -- aura.dispelName == nil → not dispellable (safe: secrets never equal nil) + -- not (aura.dispelName == nil) → dispellable (covers secret string and plain string) + local isDispellable = not (auraInfo.dispelName == nil) + local debuffType + if auraInfo._hasSecrets then + -- Secret aura: can't read dispelName as a Lua string for type matching. + -- Track auraInstanceID for curve-based dispel display in UpdateDebuffs. + debuffType = "" + if isDispellable and unit then + self._dispelAuraID = auraInstanceID + self._dispelUnit = unit + end else + debuffType = auraInfo.dispelName or "" + end + + -- check Bleed (guards internally against secret values) + debuffType = I.CheckDebuffType(debuffType, spellId) + + -- Duration: secret auras use 0 placeholders for Lua logic + -- (display uses SetCooldownFromAura with C-level DurationObject APIs). + -- Non-secret auras still get real values for Lua arithmetic paths. + local start, duration + if auraInfo._hasSecrets then start = 0 duration = 0 + else + local expirationTime = auraInfo.expirationTime or 0 + duration = auraInfo.duration + start = expirationTime - duration end local source = auraInfo.sourceUnit - local spellId = auraInfo.spellId - -- local attribute = auraInfo.points[1] -- UnitAura:arg16 auraInfo.refreshing = false - -- check Bleed - -- On Midnight in restricted context, spellId may be secret; I.CheckDebuffType guards internally - debuffType = I.CheckDebuffType(debuffType, spellId) + if _dispelTraceEnabled then + -- print() is C-level and accepts secret values; avoid tostring() which crashes on secrets + print("|cff00ff00[Dispel]|r", "id=", auraInstanceID, "dispel=", debuffType, + "rawDispel=", auraInfo.dispelName, "secrets=", auraInfo._hasSecrets) + end - if duration then + -- duration ~= nil is safe on secrets (secrets never equal nil) + if Cell.isMidnight or (duration ~= nil) then UpdateAuraRefreshState(auraInfo) self._debuffs_cache[auraInstanceID] = auraInfo + -- Classification: use _hasSecrets to choose between Lua lookup vs server filter local isBig = false local isBlacklisted = false local isDispelBlacklisted = false - if F.IsAuraNonSecret(auraInfo) then - isBig = spellId and Cell.vars.bigDebuffs[spellId] or false - isBlacklisted = spellId and Cell.vars.debuffBlacklist[spellId] or false - isDispelBlacklisted = spellId and Cell.vars.dispelBlacklist[spellId] or false + if not auraInfo._hasSecrets and spellId then + isBig = Cell.vars.bigDebuffs[spellId] or false + isBlacklisted = Cell.vars.debuffBlacklist[spellId] or false + isDispelBlacklisted = Cell.vars.dispelBlacklist[spellId] or false end if enabledIndicators["debuffs"] and not isBlacklisted then -- all debuffs / only dispellableByMe - if not indicatorBooleans["debuffs"] or I.CanDispel(debuffType) then + local canDispel = not indicatorBooleans["debuffs"] or I.CanDispel(debuffType) + -- 12.0+: when dispelName is secret, use server-side filter + if not canDispel and isDispellable and auraInfo._hasSecrets + and _IsAuraFilteredOut and unit then + canDispel = not _IsAuraFilteredOut(unit, + auraInstanceID, "HARMFUL|RAID_PLAYER_DISPELLABLE") + end + if canDispel then if isBig then self._debuffs_big[auraInstanceID] = true else @@ -1224,6 +1444,21 @@ local function HandleDebuff(self, auraInfo) -- prepare raidDebuffs local order = I.GetDebuffOrder(name, spellId, count) + -- Tier 2: secret aura → server RAID filter + if not order and auraInfo._hasSecrets and enabledIndicators["raidDebuffs"] + and _IsAuraFilteredOut and unit then + local isFiltered = _IsAuraFilteredOut(unit, + auraInstanceID, "HARMFUL|RAID") + -- Not filtered (false) or secret result → show as raid debuff + if not F.IsValueNonSecret(isFiltered) or isFiltered == false then + order = 100 + end + end + -- Tier 3: encounter fallback for unidentified secret debuffs + if not order and auraInfo._hasSecrets and enabledIndicators["raidDebuffs"] + and IsEncounterInProgress and IsEncounterInProgress() then + order = 100 + end if enabledIndicators["raidDebuffs"] and order then auraInfo.raidDebuffOrder = order tinsert(self._debuffs_raid, auraInstanceID) @@ -1240,10 +1475,9 @@ local function HandleDebuff(self, auraInfo) if enabledIndicators["dispels"] and debuffType and debuffType ~= "" then -- all dispels / only dispellableByMe - if not indicatorBooleans ["dispels"]["dispellableByMe"] or I.CanDispel(debuffType) then + if not indicatorBooleans["dispels"]["dispellableByMe"] or I.CanDispel(debuffType) then if indicatorBooleans["dispels"][debuffType] then if isDispelBlacklisted then - -- no highlight self._debuffs_dispel[debuffType] = false else self._debuffs_dispel[debuffType] = true @@ -1255,15 +1489,19 @@ local function HandleDebuff(self, auraInfo) -- crowdControls if enabledIndicators["crowdControls"] and I.IsCrowdControls(name, spellId) and self._debuffs.crowdControlsFound < indicatorNums["crowdControls"] then self._debuffs.crowdControlsFound = self._debuffs.crowdControlsFound + 1 - self.indicators.crowdControls[self._debuffs.crowdControlsFound]:SetCooldown(start, duration, debuffType, icon, count, auraInfo.refreshing) + if Cell.isMidnight then + self.indicators.crowdControls[self._debuffs.crowdControlsFound]:SetCooldownFromAura(unit, auraInstanceID, icon, auraInfo.refreshing) + else + self.indicators.crowdControls[self._debuffs.crowdControlsFound]:SetCooldown(start, duration, debuffType, icon, count, auraInfo.refreshing) + end -- remove from debuffs self._debuffs_big[auraInstanceID] = nil self._debuffs_normal[auraInstanceID] = nil end -- Per-aura check: only compare spellId if non-secret - if F.IsAuraNonSecret(auraInfo) then - -- resurrections: 图腾复生/复生 + if not auraInfo._hasSecrets and spellId then + -- resurrections: 图腾复ç"Ÿ/复ç"Ÿ if spellId == 255234 or spellId == 225080 then -- NOTE: this rez lasts longer than the debuff self._debuffs.resurrectionFound = true @@ -1293,7 +1531,7 @@ local function UnitButton_UpdateDebuffs(self, isFullUpdate) I.ResetCustomIndicators(self, "debuff") if isFullUpdate then - wipe(self._debuffs_cache) + self._debuffs_cache = {} ForEachAura(self, "HARMFUL", HandleDebuff) else ForEachAuraCache(self, "HARMFUL", HandleDebuff) @@ -1319,53 +1557,61 @@ local function UnitButton_UpdateDebuffs(self, isFullUpdate) -- sort indices sort(self._debuffs_raid, function(a, b) - return self._debuffs_cache[a]["raidDebuffOrder"] < self._debuffs_cache[b]["raidDebuffOrder"] + local ca, cb = self._debuffs_cache[a], self._debuffs_cache[b] + if not ca or not cb then return ca ~= nil end + return ca["raidDebuffOrder"] < cb["raidDebuffOrder"] end) -- show local topAuraInstanceID - -- for i = 1+offset, indicatorNums["raidDebuffs"] do for i = 1, indicatorNums["raidDebuffs"] do local auraInstanceID = self._debuffs_raid[i] if auraInstanceID then local auraInfo = self._debuffs_cache[auraInstanceID] if auraInfo then - local rdStart, rdDur - if F.IsValueNonSecret(auraInfo.expirationTime) and F.IsValueNonSecret(auraInfo.duration) then - rdStart = (auraInfo.expirationTime or 0) - auraInfo.duration - rdDur = auraInfo.duration + if Cell.isMidnight then + -- Pass-through: C-level DurationObject APIs handle secret values + self.indicators.raidDebuffs[i]:SetCooldownFromAura( + unit, auraInstanceID, auraInfo.icon, auraInfo.refreshing) + -- Dispel color: border = dispel type color (base), swipe = black + local frame = self.indicators.raidDebuffs[i] + if frame.cooldown and frame.cooldown.SetSwipeColor then + frame.cooldown:SetSwipeColor(0, 0, 0) + end + if auraInfo._hasSecrets and (auraInfo.dispelName == nil) then + if frame.border then frame.border:SetColorTexture(1, 0, 0); frame.border:Show() end + elseif auraInfo._hasSecrets and _dispelCurvesReady then + local hlColor = _getCurveColor(unit, auraInstanceID, _dispelHighlightCurve) + if hlColor then + local r, g, b = hlColor:GetRGBA() + if frame.border then frame.border:SetColorTexture(r, g, b); frame.border:Show() end + end + elseif not auraInfo._hasSecrets and auraInfo.dispelName then + local r, g, b = I.GetDebuffTypeColor(auraInfo.dispelName) + if frame.border then frame.border:SetColorTexture(r, g, b); frame.border:Show() end + else + if frame.border then frame.border:SetColorTexture(1, 0, 0); frame.border:Show() end + end else - rdStart = 0 - rdDur = 0 + -- Pre-Midnight: standard Lua arithmetic (identical to upstream) + local rdStart = (auraInfo.expirationTime or 0) - auraInfo.duration + self.indicators.raidDebuffs[i]:SetCooldown( + rdStart, auraInfo.duration, + auraInfo.dispelName or "", + auraInfo.icon, auraInfo.applications, + auraInfo.refreshing, + I.IsDebuffUseElapsedTime(auraInfo.name, auraInfo.spellId)) end - self.indicators.raidDebuffs[i]:SetCooldown( - rdStart, - rdDur, - (auraInfo.dispelName and (not issecretvalue or not issecretvalue(auraInfo.dispelName))) and auraInfo.dispelName or "", - auraInfo.icon, - auraInfo.applications, - auraInfo.refreshing, - I.IsDebuffUseElapsedTime(auraInfo.name, auraInfo.spellId) - ) self.indicators.raidDebuffs[i].auraInstanceID = auraInstanceID -- NOTE: for tooltip startIndex = startIndex + 1 - -- remove from debuffs self._debuffs_big[auraInstanceID] = nil self._debuffs_normal[auraInstanceID] = nil - if i == 1 then -- top - topAuraInstanceID = auraInstanceID - end + if i == 1 then topAuraInstanceID = auraInstanceID end end end end - -- if cleuUnits[unit] then - -- self.indicators.raidDebuffs[1]:SetCooldown(cleuUnits[unit][1], cleuUnits[unit][2], "cleu", cleuUnits[unit][3], 1) - -- topGlowType, topGlowOptions = unpack(CellDB["cleuGlow"]) - -- end - - -- update raidDebuffs self.indicators.raidDebuffs:UpdateSize(startIndex - 1) for i = startIndex, 3 do self.indicators.raidDebuffs[i].auraInstanceID = nil @@ -1374,7 +1620,9 @@ local function UnitButton_UpdateDebuffs(self, isFullUpdate) -- update glow if not indicatorBooleans["raidDebuffs"] then -- to make sure top glow has highest priority - local topGlowType, topGlowOptions = self._debuffs_cache[topAuraInstanceID]["raidDebuffGlowType"], self._debuffs_cache[topAuraInstanceID]["raidDebuffGlowOptions"] + local topAura = topAuraInstanceID and self._debuffs_cache[topAuraInstanceID] + local topGlowType = topAura and topAura["raidDebuffGlowType"] + local topGlowOptions = topAura and topAura["raidDebuffGlowOptions"] if topGlowType and topGlowType ~= "None" then self._debuffs_glow_current[topGlowType] = topGlowOptions end @@ -1388,13 +1636,16 @@ local function UnitButton_UpdateDebuffs(self, isFullUpdate) end wipe(self._debuffs_glow_current) else - self.indicators.raidDebuffs:ShowGlow( - I.GetDebuffGlow( - self._debuffs_cache[topAuraInstanceID]["name"], - self._debuffs_cache[topAuraInstanceID]["spellId"], - self._debuffs_cache[topAuraInstanceID]["applications"] + local topAura = topAuraInstanceID and self._debuffs_cache[topAuraInstanceID] + if topAura then + self.indicators.raidDebuffs:ShowGlow( + I.GetDebuffGlow( + topAura["name"], + topAura["spellId"], + topAura["applications"] + ) ) - ) + end end else self.indicators.raidDebuffs:Hide() @@ -1403,23 +1654,59 @@ local function UnitButton_UpdateDebuffs(self, isFullUpdate) -- update debuffs startIndex = 1 if enabledIndicators["debuffs"] then + -- helper to display a debuff indicator + local function showDebuff(auraInstanceID, auraInfo, isBig) + if Cell.isMidnight then + local frame = self.indicators.debuffs[startIndex] + frame:SetCooldownFromAura( + unit, auraInstanceID, auraInfo.icon, auraInfo.refreshing) + -- Border = dispel type color (base), swipe = black (fills over as time expires). + -- SetReverse(true) in SetCooldownFromAura makes the swipe fill IN. + if frame.cooldown and frame.cooldown.SetSwipeColor then + frame.cooldown:SetSwipeColor(0, 0, 0) + end + local br, bg, bb = 1, 0, 0 + if auraInfo._hasSecrets and (auraInfo.dispelName == nil) then + -- Non-dispellable secret: red (check before curves since curve returns transparent for Physical) + br, bg, bb = 1, 0, 0 + elseif auraInfo._hasSecrets and _dispelCurvesReady then + local hlColor = _getCurveColor(unit, auraInstanceID, _dispelHighlightCurve) + if hlColor then + br, bg, bb = hlColor:GetRGBA() + end + elseif not auraInfo._hasSecrets and auraInfo.dispelName then + br, bg, bb = I.GetDebuffTypeColor(auraInfo.dispelName) + else + br, bg, bb = 1, 0, 0 + end + if frame.border then + frame.border:SetColorTexture(br, bg, bb) + frame.border:Show() + end + -- Big debuff sizing (matches pre-Midnight wrapper in Built-in.lua) + local debuffs = self.indicators.debuffs + if isBig then + P.Size(frame, debuffs.bigSize[1], debuffs.bigSize[2]) + else + P.Size(frame, debuffs.normalSize[1], debuffs.normalSize[2]) + end + else + local dStart = (auraInfo.expirationTime or 0) - auraInfo.duration + self.indicators.debuffs[startIndex]:SetCooldown( + dStart, auraInfo.duration, + auraInfo.dispelName or "", auraInfo.icon, + auraInfo.applications, auraInfo.refreshing, isBig) + end + self.indicators.debuffs[startIndex].auraInstanceID = auraInstanceID + self.indicators.debuffs[startIndex].spellId = auraInfo.spellId + startIndex = startIndex + 1 + end + -- bigDebuffs first for auraInstanceID in next, self._debuffs_big do local auraInfo = self._debuffs_cache[auraInstanceID] if auraInfo and startIndex <= indicatorNums["debuffs"] then - -- start, duration, debuffType, texture, count - local bStart, bDur - if F.IsValueNonSecret(auraInfo.expirationTime) and F.IsValueNonSecret(auraInfo.duration) then - bStart = (auraInfo.expirationTime or 0) - auraInfo.duration - bDur = auraInfo.duration - else - bStart = 0 - bDur = 0 - end - self.indicators.debuffs[startIndex]:SetCooldown(bStart, bDur, (auraInfo.dispelName and (not issecretvalue or not issecretvalue(auraInfo.dispelName))) and auraInfo.dispelName or "", auraInfo.icon, auraInfo.applications, auraInfo.refreshing, true) - self.indicators.debuffs[startIndex].auraInstanceID = auraInstanceID -- NOTE: for tooltip - self.indicators.debuffs[startIndex].spellId = auraInfo.spellId -- NOTE: for blacklist - startIndex = startIndex + 1 + showDebuff(auraInstanceID, auraInfo, true) elseif startIndex > indicatorNums["debuffs"] then break end @@ -1428,26 +1715,13 @@ local function UnitButton_UpdateDebuffs(self, isFullUpdate) for auraInstanceID in next, self._debuffs_normal do local auraInfo = self._debuffs_cache[auraInstanceID] if auraInfo and startIndex <= indicatorNums["debuffs"] then - -- start, duration, debuffType, texture, count - local nStart, nDur - if F.IsValueNonSecret(auraInfo.expirationTime) and F.IsValueNonSecret(auraInfo.duration) then - nStart = (auraInfo.expirationTime or 0) - auraInfo.duration - nDur = auraInfo.duration - else - nStart = 0 - nDur = 0 - end - self.indicators.debuffs[startIndex]:SetCooldown(nStart, nDur, (auraInfo.dispelName and (not issecretvalue or not issecretvalue(auraInfo.dispelName))) and auraInfo.dispelName or "", auraInfo.icon, auraInfo.applications, auraInfo.refreshing) - self.indicators.debuffs[startIndex].auraInstanceID = auraInstanceID -- NOTE: for tooltip - self.indicators.debuffs[startIndex].spellId = auraInfo.spellId -- NOTE: for blacklist - startIndex = startIndex + 1 + showDebuff(auraInstanceID, auraInfo) elseif startIndex > indicatorNums["debuffs"] then break end end end - -- update debuffs self.indicators.debuffs:UpdateSize(startIndex - 1) for i = startIndex, 10 do self.indicators.debuffs[i].auraInstanceID = nil @@ -1456,7 +1730,81 @@ local function UnitButton_UpdateDebuffs(self, isFullUpdate) -- update dispels if F.UnitInGroup(unit) or UnitIsFriend("player", unit) then + -- Restore icon positions from previous secret dispel mode + if self.indicators.dispels._secretIconsStacked then + self.indicators.dispels:SetOrientation(self.indicators.dispels._orientation) + self.indicators.dispels._secretIconsStacked = nil + for i = 1, 5 do + self.indicators.dispels[i]:SetAlpha(1) + self.indicators.dispels[i]:SetVertexColor(1, 1, 1, 1) + end + end + if self.indicators.dispels._secretGradientShown then + self.indicators.dispels._secretGradientShown = nil + if self.indicators.dispels._secretGradientOverlay then + self.indicators.dispels._secretGradientOverlay:Hide() + end + end self.indicators.dispels:SetDispels(self._debuffs_dispel) + + -- Midnight: if SetDispels found nothing but we detected a dispellable aura, + -- use color curves to render via pass-through (no pcall needed — colors + -- flow directly to C-level SetVertexColor/SetAlpha). + if self._dispelAuraID and _dispelCurvesReady + and not self.indicators.dispels.highlight:IsShown() + and enabledIndicators["dispels"] then + + local dispels = self.indicators.dispels + local sUnit = self._dispelUnit + local sAuraID = self._dispelAuraID + + local hlColor = _getCurveColor(sUnit, sAuraID, _dispelHighlightCurve) + if hlColor then + local cr, cg, cb, ca = hlColor:GetRGBA() + -- highlight: match the user's highlight type setting + local ht = dispels.highlightType + if ht and ht ~= "none" then + if ht == "entire" then + dispels.highlight:SetTexture(Cell.vars.whiteTexture) + dispels.highlight:SetVertexColor(cr, cg, cb, 0.5) + dispels.highlight:Show() + elseif ht == "current" or ht == "current+" then + dispels.highlight:SetTexture(Cell.vars.texture) + dispels.highlight:SetVertexColor(cr, cg, cb, 1) + dispels.highlight:Show() + elseif ht == "gradient" or ht == "gradient-half" then + local overlay = _ensureGradientOverlay(dispels) + overlay:ClearAllPoints() + overlay:SetAllPoints(dispels.highlight) + overlay:SetVertexColor(cr, cg, cb, 1) + overlay:Show() + dispels._secretGradientShown = true + end + end + + -- icons: bracket curve alpha controls visibility (pass-through to SetAlpha) + if dispels.showIcons then + for i, t in ipairs(_dispelTypes) do + local dIcon = dispels[i] + if dIcon then + if dIcon.SetDispel then dIcon:SetDispel(t.name) end + local bColor = _getCurveColor(sUnit, sAuraID, _bracketCurves[t.name]) + if bColor then + local _, _, _, ba = bColor:GetRGBA() + dIcon:SetAlpha(ba) -- C-level, accepts secret numbers + end + if i > 1 then + dIcon:ClearAllPoints() + dIcon:SetAllPoints(dispels[1]) + end + dIcon:Show() + end + end + dispels:UpdateSize(1) + dispels._secretIconsStacked = true + end + end + end end -- update crowdControls @@ -1486,54 +1834,89 @@ end local function HandleBuff(self, auraInfo) local unit = self.states.displayedUnit - local auraInstanceID = auraInfo.auraInstanceID + local name = auraInfo.name - -- auraInfo.icon may be a secret fileID on Midnight 12.0.0+ - -- SetTexture() accepts secret numbers, so this works as-is local icon = auraInfo.icon local count = auraInfo.applications - -- local debuffType = auraInfo.isHarmful and auraInfo.dispelName - local expirationTime = auraInfo.expirationTime or 0 - local duration = auraInfo.duration - -- Midnight 12.0.0+: expirationTime and duration may be secret even when spellId is not. - -- Guard per-field: non-secret temporal fields get proper duration/cooldown display. - local start - if F.IsValueNonSecret(expirationTime) and F.IsValueNonSecret(duration) then - start = expirationTime - duration - else + local spellId = auraInfo.spellId + local source = auraInfo.sourceUnit + + -- Duration: secret auras on Midnight use 0 placeholders for Lua logic + -- (display uses SetCooldownFromAura with C-level DurationObject APIs). + -- Non-secret auras on Midnight still get real values for indicators that + -- need Lua arithmetic (e.g. tankActiveMitigation StatusBar). + local start, duration + if auraInfo._hasSecrets then start = 0 duration = 0 + else + local expirationTime = auraInfo.expirationTime or 0 + duration = auraInfo.duration + start = expirationTime - duration end - local source = auraInfo.sourceUnit - local spellId = auraInfo.spellId - -- local attribute = auraInfo.points[1] -- UnitAura:arg16 auraInfo.refreshing = false - if duration then + -- duration ~= nil is safe on secrets (secrets never equal nil) + if Cell.isMidnight or (duration ~= nil) then UpdateAuraRefreshState(auraInfo) self._buffs_cache[auraInstanceID] = auraInfo - -- defensiveCooldowns - if enabledIndicators["defensiveCooldowns"] and I.IsDefensiveCooldown(name, spellId) and self._buffs.defensiveFound < indicatorNums["defensiveCooldowns"] then + -- defensiveCooldowns / externalCooldowns / allCooldowns + local isDefensive = I.IsDefensiveCooldown(name, spellId) + local isExternal = I.IsExternalCooldown(name, spellId, source, unit) + + -- Secret auras: fall back to server-side aura filters + if not isDefensive and not isExternal and auraInfo._hasSecrets and _IsAuraFilteredOut then + isExternal = not _IsAuraFilteredOut(unit, auraInstanceID, "HELPFUL|EXTERNAL_DEFENSIVE") + if not isExternal then + isDefensive = not _IsAuraFilteredOut(unit, auraInstanceID, "HELPFUL|BIG_DEFENSIVE") + end + -- Catch remaining raid-important secret buffs (e.g. Power Infusion) + if not isDefensive and not isExternal then + isExternal = not _IsAuraFilteredOut(unit, auraInstanceID, "HELPFUL|RAID") + end + end + + if enabledIndicators["defensiveCooldowns"] and isDefensive and self._buffs.defensiveFound < indicatorNums["defensiveCooldowns"] then self._buffs.defensiveFound = self._buffs.defensiveFound + 1 - -- start, duration, debuffType, texture, count, refreshing - self.indicators.defensiveCooldowns[self._buffs.defensiveFound]:SetCooldown(start, duration, nil, icon, count, auraInfo.refreshing) + local frame = self.indicators.defensiveCooldowns[self._buffs.defensiveFound] + if Cell.isMidnight then + frame:SetCooldownFromAura(unit, auraInstanceID, icon, auraInfo.refreshing) + -- Yellow base, black swipe fills in + if frame.border then frame.border:SetColorTexture(1, 0.85, 0); frame.border:Show() end + if frame.cooldown and frame.cooldown.SetSwipeColor then frame.cooldown:SetSwipeColor(0, 0, 0) end + else + frame:SetCooldown(start, duration, nil, icon, count, auraInfo.refreshing) + end + frame.auraInstanceID = auraInstanceID end - -- externalCooldowns - if enabledIndicators["externalCooldowns"] and I.IsExternalCooldown(name, spellId, source, unit) and self._buffs.externalFound < indicatorNums["externalCooldowns"] then + if enabledIndicators["externalCooldowns"] and isExternal and self._buffs.externalFound < indicatorNums["externalCooldowns"] then self._buffs.externalFound = self._buffs.externalFound + 1 - -- start, duration, debuffType, texture, count, refreshing - self.indicators.externalCooldowns[self._buffs.externalFound]:SetCooldown(start, duration, nil, icon, count, auraInfo.refreshing) + local frame = self.indicators.externalCooldowns[self._buffs.externalFound] + if Cell.isMidnight then + frame:SetCooldownFromAura(unit, auraInstanceID, icon, auraInfo.refreshing) + if frame.border then frame.border:SetColorTexture(1, 0.85, 0); frame.border:Show() end + if frame.cooldown and frame.cooldown.SetSwipeColor then frame.cooldown:SetSwipeColor(0, 0, 0) end + else + frame:SetCooldown(start, duration, nil, icon, count, auraInfo.refreshing) + end + frame.auraInstanceID = auraInstanceID end - -- allCooldowns - if enabledIndicators["allCooldowns"] and (I.IsExternalCooldown(name, spellId, source, unit) or I.IsDefensiveCooldown(name, spellId)) and self._buffs.allFound < indicatorNums["allCooldowns"] then + if enabledIndicators["allCooldowns"] and (isDefensive or isExternal) and self._buffs.allFound < indicatorNums["allCooldowns"] then self._buffs.allFound = self._buffs.allFound + 1 - -- start, duration, debuffType, texture, count, refreshing - self.indicators.allCooldowns[self._buffs.allFound]:SetCooldown(start, duration, nil, icon, count, auraInfo.refreshing) + local frame = self.indicators.allCooldowns[self._buffs.allFound] + if Cell.isMidnight then + frame:SetCooldownFromAura(unit, auraInstanceID, icon, auraInfo.refreshing) + if frame.border then frame.border:SetColorTexture(1, 0.85, 0); frame.border:Show() end + if frame.cooldown and frame.cooldown.SetSwipeColor then frame.cooldown:SetSwipeColor(0, 0, 0) end + else + frame:SetCooldown(start, duration, nil, icon, count, auraInfo.refreshing) + end + frame.auraInstanceID = auraInstanceID end -- tankActiveMitigation @@ -1555,8 +1938,7 @@ local function HandleBuff(self, auraInfo) I.UpdateCustomIndicators(self, auraInfo) -- Per-aura check: only compare spellId if non-secret - if F.IsAuraNonSecret(auraInfo) then - -- check BG flags for statusIcon + if not auraInfo._hasSecrets and spellId then if spellId == 156621 then self.states.BGFlag = "alliance" elseif spellId == 156618 then @@ -1573,7 +1955,7 @@ local function UnitButton_UpdateBuffs(self, isFullUpdate) I.ResetCustomIndicators(self, "buff") if isFullUpdate then - wipe(self._buffs_cache) + self._buffs_cache = {} ForEachAura(self, "HELPFUL", HandleBuff) else ForEachAuraCache(self, "HELPFUL", HandleBuff) @@ -1673,10 +2055,24 @@ end ------------------------------------------------- -- check auras using CLEU -- NOTE: COMBAT_LOG_EVENT_UNFILTERED is unavailable on Midnight (12.0.0+). --- CheckCLEURequired has been removed; the cleu frame is guarded below. +-- CheckCLEURequired guards registration; CLEU handler is wrapped with Cell.isMidnight check. ------------------------------------------------- local cleu = CreateFrame("Frame") +function CheckCLEURequired() + -- CLEU (CombatLogGetCurrentEventInfo) removed in 12.0+ + if not CombatLogGetCurrentEventInfo then return end + + if (Cell.vars.currentLayoutTable.indicators[Cell.defaults.indicatorIndices.externalCooldowns].enabled + or Cell.vars.currentLayoutTable.indicators[Cell.defaults.indicatorIndices.defensiveCooldowns].enabled + or Cell.vars.currentLayoutTable.indicators[Cell.defaults.indicatorIndices.allCooldowns].enabled) + and (I.IsDefensiveCooldown(55342) or I.IsExternalCooldown(414660)) then + cleu:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") + else + cleu:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED") + end +end + local function UpdateMirrorImage(b, event) if event == "SPELL_AURA_APPLIED" then b._mirror_image = GetTime() @@ -1689,9 +2085,9 @@ local function UpdateMirrorImage(b, event) end local SelfBarriers = { - [11426] = true, -- 寒冰护体 (self) - [235313] = true, -- 烈焰护体 (self) - [235450] = true, -- 棱光护体 (self) + [11426] = true, -- å¯'冰护ä½" (self) + [235313] = true, -- 烈焰护ä½" (self) + [235450] = true, -- 棱光护ä½" (self) } local function UpdateMassBarrier(b, event) @@ -1757,28 +2153,38 @@ UnitButton_UpdateAuras = function(self, updateInfo) else -- Midnight 12.0.0+: some aura fields may still be secret. Per-aura checks in -- HandleBuff/HandleDebuff handle this. We no longer force full update for ALL - -- Midnight aura events — only fall back to full update if we encounter secret + -- Midnight aura events -- only fall back to full update if we encounter secret -- isHelpful/isHarmful fields in addedAuras that prevent classification. local buffsChanged, debuffsChanged wipe(self._missing_auras) if updateInfo.addedAuras then - for _, aura in next, updateInfo.addedAuras do - if F.IsAuraNonSecret(aura) then - if aura.isHelpful then + for _, rawAura in next, updateInfo.addedAuras do + local aura = AnnotateAura(rawAura) + if aura then + local isHelpful, isHarmful + if aura._hasSecrets then + -- Secret aura: can't boolean-test isHelpful/isHarmful. Use server filter. + if _IsAuraFilteredOut then + isHarmful = not _IsAuraFilteredOut(unit, aura.auraInstanceID, "HARMFUL") + isHelpful = not isHarmful and not _IsAuraFilteredOut(unit, aura.auraInstanceID, "HELPFUL") + end + else + -- Non-secret: use fields directly (safe boolean test) + isHelpful, isHarmful = aura.isHelpful, aura.isHarmful + if not isHelpful and not isHarmful and _IsAuraFilteredOut then + isHarmful = not _IsAuraFilteredOut(unit, aura.auraInstanceID, "HARMFUL") + isHelpful = not isHarmful and not _IsAuraFilteredOut(unit, aura.auraInstanceID, "HELPFUL") + end + end + if isHelpful then buffsChanged = true self._buffs_cache[aura.auraInstanceID] = aura end - if aura.isHarmful then + if isHarmful then debuffsChanged = true self._debuffs_cache[aura.auraInstanceID] = aura end - else - -- Secret aura: can't classify as buff/debuff; force full update - UnitButton_UpdateBuffs(self, true) - UnitButton_UpdateDebuffs(self, true) - I.UpdateStatusIcon(self) - return end end end @@ -1791,8 +2197,8 @@ UnitButton_UpdateAuras = function(self, updateInfo) buffsChanged = true aura = GetAuraDataByAuraInstanceID(unit, auraInstanceID) if aura then - if F.IsAuraNonSecret(aura) then - -- Sanitize cached values: they may be secret even if new aura's spellId is not + if not aura._hasSecrets then + -- Non-secret: safe to read cached values for refresh animation local cachedExp = self._buffs_cache[auraInstanceID].expirationTime local cachedApp = self._buffs_cache[auraInstanceID].applications aura.oldExpirationTime = (cachedExp and F.IsValueNonSecret(cachedExp)) and cachedExp or 0 @@ -1804,8 +2210,7 @@ UnitButton_UpdateAuras = function(self, updateInfo) debuffsChanged = true aura = GetAuraDataByAuraInstanceID(unit, auraInstanceID) if aura then - if F.IsAuraNonSecret(aura) then - -- Sanitize cached values: they may be secret even if new aura's spellId is not + if not aura._hasSecrets then local cachedExp = self._debuffs_cache[auraInstanceID].expirationTime local cachedApp = self._debuffs_cache[auraInstanceID].applications aura.oldExpirationTime = (cachedExp and F.IsValueNonSecret(cachedExp)) and cachedExp or 0 @@ -1838,17 +2243,28 @@ UnitButton_UpdateAuras = function(self, updateInfo) if next(self._missing_auras) then for _, aura in next, self._missing_auras do - if F.IsAuraNonSecret(aura) then - if aura.isHelpful then + if aura then + local isHelpful, isHarmful + if aura._hasSecrets then + if _IsAuraFilteredOut then + isHarmful = not _IsAuraFilteredOut(unit, aura.auraInstanceID, "HARMFUL") + isHelpful = not isHarmful and not _IsAuraFilteredOut(unit, aura.auraInstanceID, "HELPFUL") + end + else + isHelpful, isHarmful = aura.isHelpful, aura.isHarmful + if not isHelpful and not isHarmful and _IsAuraFilteredOut then + isHarmful = not _IsAuraFilteredOut(unit, aura.auraInstanceID, "HARMFUL") + isHelpful = not isHarmful and not _IsAuraFilteredOut(unit, aura.auraInstanceID, "HELPFUL") + end + end + if isHelpful then buffsChanged = true self._buffs_cache[aura.auraInstanceID] = aura - elseif aura.isHarmful then + elseif isHarmful then debuffsChanged = true self._debuffs_cache[aura.auraInstanceID] = aura end end - -- Secret missing auras are silently dropped — they'll be - -- picked up on the next full update if needed end end @@ -1865,23 +2281,28 @@ local function UnitButton_UpdateCalculator(self) if not unit then return end local calc = self.widgets.healthCalculator if not calc then return end - UnitGetDetailedHealPrediction(unit, "player", calc) + -- UnitGetDetailedHealPrediction is C-level; guard with UnitExists for AI followers/unavailable units + if UnitExists(unit) then + UnitGetDetailedHealPrediction(unit, "player", calc) + end end local function UnitButton_UpdateHealthStates(self, diff) local unit = self.states.displayedUnit if Cell.isMidnight and self.widgets.healthCalculator then - -- MIDNIGHT PATH: use calculator â€" no arithmetic on secrets + -- MIDNIGHT PATH: use calculator -- no arithmetic on secrets UnitButton_UpdateCalculator(self) -- Store healthPercent for color logic. - -- GetCurrentHealthPercent() returns a secret value inside PvP instances — - -- Lua comparisons on secrets throw errors. Use it only when non-secret. - local hpPct = self.widgets.healthCalculator:GetCurrentHealthPercent() - if F.IsValueNonSecret(hpPct) then - self.states.healthPercent = hpPct + -- Calculator's GetCurrentHealthPercent() always returns secret (even out of combat). + -- Fall back to UnitHealth/UnitHealthMax which are non-secret outside PvP instances. + local health = UnitHealth(unit) + local healthMax = UnitHealthMax(unit) + if not F.HasAnySecretValues(health, healthMax) and healthMax > 0 then + self.states.healthPercent = health / healthMax + self.states.healthMax = healthMax else - -- Secret: default to 0 so F.GetHealthBarColor won't trigger fullColor (which checks == 1). + -- In-combat secret: default to 0 so F.GetHealthBarColor won't trigger fullColor (which checks == 1). -- class_color / class_color_dark modes don't use percent, so they still work. self.states.healthPercent = 0 end @@ -1892,15 +2313,22 @@ local function UnitButton_UpdateHealthStates(self, diff) self.states.wasDeadOrGhost = self.states.isDeadOrGhost self.states.isDeadOrGhost = UnitIsDeadOrGhost(unit) or false - -- Health text: use calculator secret values + -- Health text: calculator values flow to C-level SetFormattedText/SetText if enabledIndicators["healthText"] then local calc = self.widgets.healthCalculator local health = calc:GetCurrentHealth() local maxHealth = calc:GetMaximumHealth() local totalAbsorbs = calc:GetTotalDamageAbsorbs() local healAbsorbs = calc:GetTotalHealAbsorbs() - -- SetValue accepts secret values - self.indicators.healthText:SetValue(health, maxHealth, totalAbsorbs, healAbsorbs) + -- Fallback: if calculator wasn't populated (e.g. solo, no group), + -- use direct UnitHealth/UnitHealthMax for health text display. + if F.IsValueNonSecret(maxHealth) and maxHealth == 0 and unit then + health = UnitHealth(unit) + maxHealth = UnitHealthMax(unit) + totalAbsorbs = UnitGetTotalAbsorbs(unit) or 0 + healAbsorbs = UnitGetTotalHealAbsorbs(unit) or 0 + end + self.indicators.healthText:SetValue(health, maxHealth, totalAbsorbs, healAbsorbs, unit) self.indicators.healthText:Show() else self.indicators.healthText:Hide() @@ -1955,7 +2383,7 @@ local function UnitButton_UpdateHealthStates(self, diff) end if enabledIndicators["healthText"] then -- and not self.states.isDeadOrGhost then - self.indicators.healthText:SetValue(health, healthMax, self.states.totalAbsorbs, self.states.healAbsorbs) + self.indicators.healthText:SetValue(health, healthMax, self.states.totalAbsorbs, self.states.healAbsorbs, unit) self.indicators.healthText:Show() else self.indicators.healthText:Hide() @@ -1967,10 +2395,11 @@ local function UnitButton_UpdatePowerStates(self) local unit = self.states.displayedUnit if not unit then return end + -- 12.0+: UnitPower may return secret values; store raw for SetValue self.states.power = UnitPower(unit) self.states.powerMax = UnitPowerMax(unit) - -- Midnight 12.0.0+: UnitPowerMax may return a secret number during restricted contexts - if not (Cell.isMidnight and F.IsAuraRestricted()) then + -- Midnight 12.0.0+: UnitPowerMax may be secret — only clamp when non-secret + if F.IsValueNonSecret(self.states.powerMax) then if self.states.powerMax <= 0 then self.states.powerMax = 1 end end end @@ -1983,46 +2412,107 @@ local function GetRole(b) return b.states.role end + -- For the player's own unit, get role from current spec directly + -- (UnitGroupRolesAssigned returns "NONE" when solo or in non-LFG groups) + -- UnitIsUnit may return a secret boolean; check before boolean test. + -- For player identity check, treat secret as false (safe fallback). + local isPlayer = b.states.unit and UnitIsUnit(b.states.unit, "player") + if GetSpecialization and GetSpecializationRole + and b.states.unit and F.IsValueNonSecret(isPlayer) and isPlayer then + local spec = GetSpecialization() + if spec then + local specRole = GetSpecializationRole(spec) + if specRole and specRole ~= "NONE" then + return specRole + end + end + end + + -- Fresh UnitGroupRolesAssigned check (role may have been assigned after init) + if b.states.unit then + local freshRole = UnitGroupRolesAssigned(b.states.unit) + if freshRole and freshRole ~= "NONE" then + b.states.role = freshRole + return freshRole + end + end + local info = LGI:GetCachedInfo(b.states.guid) if not info then return end return info.role end -ShouldShowPowerText = function(b) - if not enabledIndicators["powerText"] then return end - if not (b:IsVisible() or b.isPreview) then return end - - if not b.states.guid then - return true +-- Evaluate a role filter table when the specific role is unknown. +-- Returns false if ALL roles in the table are disabled, true otherwise. +local function EvaluateFilterWithoutRole(filterTable) + if type(filterTable) == "boolean" then + return filterTable end + -- If any role is enabled, show (safe default when role unknown) + for _, enabled in pairs(filterTable) do + if enabled then + return true + end + end + -- All roles disabled for this class → hide + return false +end +-- Determine class and role for a unit button (used by power filter functions) +local function GetClassAndRole(b) local class, role + local guid = b.states.guid + -- 12.0+: guid may be secret for NPC units — can't use string.find on secrets + if guid and not F.IsValueNonSecret(guid) then + -- Fallback: use UnitInPartyIsAI to detect AI followers without needing guid + if b.states.unit and UnitInPartyIsAI(b.states.unit) then + class = b.states.class + role = GetRole(b) + end + return class, role + end if b.states.inVehicle then class = "VEHICLE" - elseif F.IsPlayer(b.states.guid) then + elseif F.IsPlayer(guid) then class = b.states.class role = GetRole(b) - elseif F.IsPet(b.states.guid) then + elseif F.IsPet(guid) then class = "PET" - elseif F.IsNPC(b.states.guid) then + elseif F.IsNPC(guid) then if UnitInPartyIsAI(b.states.unit) then class = b.states.class role = GetRole(b) else class = "NPC" end - elseif F.IsVehicle(b.states.guid) then + elseif F.IsVehicle(guid) then class = "VEHICLE" end + return class, role +end + +ShouldShowPowerText = function(b) + if not enabledIndicators["powerText"] then return end + if not (b:IsVisible() or b.isPreview) then return end + + -- guid may be secret for NPC/follower units; `== nil` is safe on secrets. + if b.states.guid == nil then + return true + end + + local class, role = GetClassAndRole(b) if class then - if type(indicatorCustoms["powerText"][class]) == "boolean" then - return indicatorCustoms["powerText"][class] + local filter = indicatorCustoms["powerText"] and indicatorCustoms["powerText"][class] + if filter == nil then + return true + elseif type(filter) == "boolean" then + return filter else if role then - return indicatorCustoms["powerText"][class][role] + return filter[role] else - return true -- show power if role not found + return EvaluateFilterWithoutRole(filter) end end end @@ -2034,37 +2524,24 @@ ShouldShowPowerBar = function(b) if not (b:IsVisible() or b.isPreview) then return end if not b.powerSize or b.powerSize == 0 then return end - if not b.states.guid then + -- guid may be secret for NPC/follower units; == nil is safe on secrets. + if b.states.guid == nil then return true end - local class, role - if b.states.inVehicle then - class = "VEHICLE" - elseif F.IsPlayer(b.states.guid) then - class = b.states.class - role = GetRole(b) - elseif F.IsPet(b.states.guid) then - class = "PET" - elseif F.IsNPC(b.states.guid) then - if UnitInPartyIsAI(b.states.unit) then - class = b.states.class - role = GetRole(b) - else - class = "NPC" - end - elseif F.IsVehicle(b.states.guid) then - class = "VEHICLE" - end + local class, role = GetClassAndRole(b) if class and Cell.vars.currentLayoutTable then - if type(Cell.vars.currentLayoutTable["powerFilters"][class]) == "boolean" then - return Cell.vars.currentLayoutTable["powerFilters"][class] + local filter = Cell.vars.currentLayoutTable["powerFilters"] and Cell.vars.currentLayoutTable["powerFilters"][class] + if filter == nil then + return true + elseif type(filter) == "boolean" then + return filter else if role then - return Cell.vars.currentLayoutTable["powerFilters"][class][role] + return filter[role] else - return true -- show power if role not found + return EvaluateFilterWithoutRole(filter) end end end @@ -2133,7 +2610,10 @@ local function UnitButton_UpdateTarget(self) local unit = self.states.displayedUnit if not unit then return end - if UnitIsUnit(unit, "target") then + -- UnitIsUnit may return a secret boolean in combat; check before boolean test. + -- Treat secret as true (better to show highlight than miss the player's target). + local isTarget = UnitIsUnit(unit, "target") + if not F.IsValueNonSecret(isTarget) or isTarget then if highlightEnabled then self.widgets.targetHighlight:Show() end else self.widgets.targetHighlight:Hide() @@ -2152,7 +2632,7 @@ local function CheckVehicleRoot(self, petUnit) local pName = UnitName(playerUnit) -- On Midnight 12.0.0+, UnitName() may return a secret string in instances -- Comparing a secret string with == will error, so guard before comparing - if not (Cell.isMidnight and F.IsSecretValue and F.IsSecretValue(pName)) and pName == occupantName then + if F.IsValueNonSecret(pName) and pName == occupantName then isRoot = controlType == "Root" break end @@ -2175,7 +2655,7 @@ UnitButton_UpdateRole = function(self) --! check vehicle root -- Midnight 12.0.0+: guid may be secret for NPC/boss units - if self.states.guid and not (issecretvalue and issecretvalue(self.states.guid)) and strfind(self.states.guid, "^Vehicle") and not UnitInPartyIsAI(unit) then + if self.states.guid and F.IsValueNonSecret(self.states.guid) and strfind(self.states.guid, "^Vehicle") and not UnitInPartyIsAI(unit) then CheckVehicleRoot(self, unit) end else @@ -2279,13 +2759,43 @@ end UnitButton_UpdatePowerText = function(self) if not self._shouldShowPowerText then return end - if self.states.powerMax and self.states.power and not self.states.isDeadOrGhost then - -- On Midnight, power and powerMax may be secret values (Midnight 12.0.0+) - -- SetValue uses string.format/AbbreviateNumbers which accept secrets → FontString:SetText accepts secrets - -- Secret handling is in the powerText indicator's SetValue method (Indicators/Base.lua) — Phase 8 follow-up - self.indicators.powerText:SetValue(self.states.power, self.states.powerMax) - else + local power = self.states.power + local powerMax = self.states.powerMax + -- 12.0+: power may be secret; == nil is safe on secrets + if power == nil or self.states.isDeadOrGhost then self.indicators.powerText:Hide() + return + end + + if not F.HasAnySecretValues(power, powerMax) then + self.indicators.powerText:SetValue(power, powerMax) + else + -- Pass secret values to C-level SetFormattedText directly. + local unit = self.states.displayedUnit + local fmt = self.indicators.powerText._format + if fmt == "percentage" then + -- UnitPowerPercent returns 0-1 by default; use ScaleTo100 curve for 0-100 + local pct + if unit and UnitPowerPercent then + if CurveConstants and CurveConstants.ScaleTo100 then + pct = UnitPowerPercent(unit, nil, true, CurveConstants.ScaleTo100) + else + pct = UnitPowerPercent(unit) + end + end + if pct then + self.indicators.powerText.text:SetFormattedText("%d%%", pct) + else + self.indicators.powerText.text:SetFormattedText("%d", power) + end + elseif fmt == "number-short" and AbbreviateNumbers then + self.indicators.powerText.text:SetFormattedText("%s", AbbreviateNumbers(power)) + else + -- "number" or "number-short" without AbbreviateNumbers: raw number + self.indicators.powerText.text:SetFormattedText("%d", power) + end + -- GetStringWidth returns secret when text is tainted; skip SetWidth + self.indicators.powerText:Show() end end @@ -2305,10 +2815,11 @@ UnitButton_UpdatePowerTextColor = function(self) end UnitButton_UpdatePowerMax = function(self) - if not (self._shouldShowPowerBar and self.states.powerMax) then return end + if not self._shouldShowPowerBar then return end + if self.states.powerMax == nil then return end -- powerMax may be secret on Midnight 12.0.0+ for some units. - -- SetMinMaxSmoothedValue is a Lua mixin that does arithmetic (Clamp) â€" fails on secrets. + -- SetMinMaxSmoothedValue is a Lua mixin that does arithmetic (Clamp) -- fails on secrets. -- SetMinMaxValues is native C API that accepts secrets. Use it as fallback on Midnight. if barAnimationType == "Smooth" and F.IsValueNonSecret(self.states.powerMax) then self.widgets.powerBar:SetMinMaxSmoothedValue(0, self.states.powerMax) @@ -2318,13 +2829,14 @@ UnitButton_UpdatePowerMax = function(self) end UnitButton_UpdatePower = function(self) - if not (self._shouldShowPowerBar and self.states.power) then return end + if not self._shouldShowPowerBar then return end + if self.states.power == nil then return end - -- self.states.power may be a secret value on Midnight 12.0.0+ - -- SetBarValue maps to SetSmoothedValue in Smooth mode, which does Lua Clamp and fails on secrets. - -- Use native SetValue on Midnight when power is secret. - if Cell.isMidnight and not F.IsValueNonSecret(self.states.power) then - self.widgets.powerBar:SetValue(self.states.power) + -- On Midnight, use native StatusBarInterpolation for smooth animation (secret-safe). + -- Pre-Midnight uses SetBarValue which maps to SetSmoothedValue in Smooth mode. + if Cell.isMidnight and SBI_ExponentialEaseOut then + local smoothEnum = (barAnimationType == "Smooth" and SBI_ExponentialEaseOut) or SBI_Immediate + self.widgets.powerBar:SetValue(self.states.power, smoothEnum) else self.widgets.powerBar:SetBarValue(self.states.power) end @@ -2358,7 +2870,7 @@ local function UnitButton_UpdateHealthMax(self) if Cell.isMidnight and self.widgets.healthCalculator then -- MIDNIGHT PATH: pass secret maxHealth directly - -- SetMinMaxSmoothedValue is a Lua mixin that does arithmetic (Clamp) — fails on secrets. + -- SetMinMaxSmoothedValue is a Lua mixin that does arithmetic (Clamp) -- fails on secrets. -- Always use native SetMinMaxValues on Midnight since maxHealth may be secret. local maxHealth = self.widgets.healthCalculator:GetMaximumHealth() self.widgets.healthBar:SetMinMaxValues(0, maxHealth) @@ -2398,16 +2910,13 @@ local function UnitButton_UpdateHealth(self, diff, skipStateUpdates) end if Cell.isMidnight and self.widgets.healthCalculator then - -- MIDNIGHT PATH: pass secret values directly to status bar + -- MIDNIGHT PATH: pass health to status bar + -- Use native StatusBarInterpolation enum for smooth animation — C-level, + -- handles secret values without Lua arithmetic (like ElvUI's approach). local calc = self.widgets.healthCalculator local health = calc:GetCurrentHealth() - -- Always use native SetValue on Midnight — SetSmoothedValue (SetBarValue in Smooth mode) - -- is a Lua mixin that does Clamp() arithmetic, which fails on secret values. - self.widgets.healthBar:SetValue(health) - if barAnimationType == "Flash" then - -- Flash: we can't compute exact diff without arithmetic on secrets, so skip precise flash - B.HideFlash(self) - end + local smoothEnum = (barAnimationType == "Smooth" and SBI_ExponentialEaseOut) or SBI_Immediate + self.widgets.healthBar:SetValue(health, smoothEnum) if Cell.vars.useThresholdColor or Cell.vars.useFullColor then UnitButton_UpdateHealthColor(self) @@ -2428,7 +2937,7 @@ local function UnitButton_UpdateHealth(self, diff, skipStateUpdates) -- EvaluateCurrentHealthPercent feeds secret health% into the curve -- Curve output: 1.0 if below threshold (needs healing), outOfRangeAlpha if above local targetAlpha = self.widgets.healthCalculator:EvaluateCurrentHealthPercent(fadeOutHealthCurve) - -- targetAlpha is a secret value — SetAlpha accepts secrets on Midnight + -- targetAlpha is a secret value -- SetAlpha accepts secrets on Midnight self:SetAlpha(targetAlpha) end end @@ -2436,17 +2945,7 @@ local function UnitButton_UpdateHealth(self, diff, skipStateUpdates) -- CLASSIC/PRE-MIDNIGHT PATH: original logic local healthPercent = self.states.healthPercent - if barAnimationType == "Flash" then - self.widgets.healthBar:SetValue(self.states.health) - local diff = healthPercent - (self.states.healthPercentOld or healthPercent) - if diff >= 0 or self.states.healthMax == 0 then - B.HideFlash(self) - elseif diff <= -0.05 and diff >= -1 then --! player (just joined) UnitHealthMax(unit) may be 1 ====> diff == -maxHealth - B.ShowFlash(self, abs(diff)) - end - else - self.widgets.healthBar:SetBarValue(self.states.health) - end + self.widgets.healthBar:SetBarValue(self.states.health) if Cell.vars.useThresholdColor or Cell.vars.useFullColor then UnitButton_UpdateHealthColor(self) @@ -2513,17 +3012,39 @@ local function UnitButton_UpdateHealPrediction(self, skipStateUpdates) local unit = self.states.displayedUnit if not unit then return end - local value = UnitGetIncomingHeals(unit) or 0 - if value == 0 then - self.widgets.incomingHeal:Hide() - return - end - if not skipStateUpdates then UnitButton_UpdateHealthStates(self) end - self.widgets.incomingHeal:SetValue(value / self.states.healthMax, self.states.healthPercent) + local incomingHeal = self.widgets.incomingHeal + -- Set size to match health bar for correct proportions + if self.orientation == "horizontal" then + incomingHeal:SetWidth(self.widgets.healthBar:GetWidth()) + else + incomingHeal:SetHeight(self.widgets.healthBar:GetHeight()) + end + + -- 12.0+: UnitGetDetailedHealPrediction populates a calculator object whose + -- getter methods return potentially secret values. These are passed directly + -- to SetMinMaxValues/SetValue (C-level APIs that accept secrets). + local calc = self.widgets.healPredictionCalculator + if calc and UnitGetDetailedHealPrediction then + if UnitExists(unit) then + UnitGetDetailedHealPrediction(unit, nil, calc) + end + local allHeal + if calc.GetIncomingHeals then + allHeal = select(1, calc:GetIncomingHeals()) + end + incomingHeal:SetMinMaxValues(0, self.states.healthMax) + incomingHeal:SetValue((allHeal == nil) and 0 or allHeal) + else + -- Fallback for pre-12.0 + local value = UnitGetIncomingHeals(unit) or 0 + incomingHeal:SetMinMaxValues(0, self.states.healthMax) + incomingHeal:SetValue(value) + end + incomingHeal:Show() end UnitButton_UpdateShieldAbsorbs = function(self, skipStateUpdates) @@ -2541,27 +3062,56 @@ UnitButton_UpdateShieldAbsorbs = function(self, skipStateUpdates) if not unit then return end -- Refresh calculator so we have current data (critical for standalone UNIT_ABSORB_AMOUNT_CHANGED events) UnitButton_UpdateCalculator(self) - local absorbs = self.widgets.healthCalculator:GetDamageAbsorbs() - -- Update the shield widget bars - self.widgets.shieldBar:SetValue(absorbs) - self.widgets.shieldBar:Show() - - -- Overshield glow and reverse-fill bar - -- NOTE: absorbs is a secret value on Midnight — we can't compare it to health to detect overshield. - -- Show the glow whenever shields are present and overshieldEnabled is on. - -- TODO: Use a Curve to map (absorbs + health - maxHealth) to glow visibility for precise overshield detection. + local absorbs = self.widgets.healthCalculator:GetTotalDamageAbsorbs() + local healthMax = self.widgets.healthCalculator:GetMaximumHealth() + + -- Overshield detection: GetDamageAbsorbs returns (amount, isClamped) + -- where isClamped is true when absorbs exceed max health (overshield). + -- isClamped may be a secret boolean; SetAlphaFromBoolean handles that. + local _, isClamped + local calc = self.widgets.healPredictionCalculator + if calc and UnitGetDetailedHealPrediction then + if UnitExists(unit) then + UnitGetDetailedHealPrediction(unit, nil, calc) + end + if calc.GetDamageAbsorbs then + _, isClamped = calc:GetDamageAbsorbs() + end + end + if overshieldReverseFillEnabled then + self.widgets.shieldBar:Hide() + self.widgets.shieldBarR:SetMinMaxValues(0, healthMax) self.widgets.shieldBarR:SetValue(absorbs) self.widgets.shieldBarR:Show() - if overshieldEnabled then - self.widgets.overShieldGlowR:Show() + if overshieldEnabled and isClamped ~= nil then + local glow = self.widgets.overShieldGlowR + if glow.SetAlphaFromBoolean then + glow:Show() + glow:SetAlphaFromBoolean(isClamped, 1, 0) + elseif F.IsValueNonSecret(isClamped) and isClamped then + glow:Show() + else + glow:Hide() + end else self.widgets.overShieldGlowR:Hide() end self.widgets.overShieldGlow:Hide() else - if overshieldEnabled then - self.widgets.overShieldGlow:Show() + self.widgets.shieldBar:SetMinMaxValues(0, healthMax) + self.widgets.shieldBar:SetValue(absorbs) + self.widgets.shieldBar:Show() + if overshieldEnabled and isClamped ~= nil then + local glow = self.widgets.overShieldGlow + if glow.SetAlphaFromBoolean then + glow:Show() + glow:SetAlphaFromBoolean(isClamped, 1, 0) + elseif F.IsValueNonSecret(isClamped) and isClamped then + glow:Show() + else + glow:Hide() + end else self.widgets.overShieldGlow:Hide() end @@ -2571,12 +3121,16 @@ UnitButton_UpdateShieldAbsorbs = function(self, skipStateUpdates) -- Update shield indicator (user-configurable indicator on top of health bar) if enabledIndicators["shieldBar"] then - -- On Midnight, we pass the secret absorb value directly; the indicator's SetValue - -- accepts secrets since it's backed by a StatusBar on Midnight. - -- NOTE: indicatorBooleans["shieldBar"] (onlyShowOvershields) can't be honored with - -- secrets since we can't compute overshieldPercent. Show full absorbs instead. - self.indicators.shieldBar:Show() - self.indicators.shieldBar:SetValue(absorbs) + local indBar = self.indicators.shieldBar + if indicatorBooleans["shieldBar"] then + -- onlyShowOvershields: can't compute overshield from secrets, hide indicator + -- TODO: Use a Curve to detect overshield (absorbs + health > maxHealth) + indBar:Hide() + else + -- SetAbsorbs anchors to health bar and uses StatusBar fill for proportioning + indBar:Show() + indBar:SetAbsorbs(absorbs, healthMax) + end else self.indicators.shieldBar:Hide() end @@ -2591,34 +3145,160 @@ UnitButton_UpdateShieldAbsorbs = function(self, skipStateUpdates) UnitButton_UpdateHealthStates(self) end - if self.states.totalAbsorbs > 0 then - local shieldPercent = self.states.totalAbsorbs / self.states.healthMax + local shieldBar = self.widgets.shieldBar + local _ta = self.states.totalAbsorbs + local totalAbsorbs = (_ta == nil) and 0 or _ta + local healthMax = self.states.healthMax + local health = self.states.health + + -- Check if values are secret (12.0+ combat) + local isSecret = F.HasAnySecretValues(totalAbsorbs, healthMax, health) + + if isSecret then + -- Secret path: use StatusBar min/max approach (C-level handles secrets) + -- Set size to match health bar for correct proportions + if self.orientation == "horizontal" then + shieldBar:SetWidth(self.widgets.healthBar:GetWidth()) + else + shieldBar:SetHeight(self.widgets.healthBar:GetHeight()) + end + + -- 12.0+: calculator returns potentially secret values, passed to C-level StatusBar APIs + local calc = self.widgets.healPredictionCalculator + local absorbAmt, isClamped + if calc and UnitGetDetailedHealPrediction then + if UnitExists(unit) then + UnitGetDetailedHealPrediction(unit, nil, calc) + end + if calc.GetDamageAbsorbs then + absorbAmt, isClamped = calc:GetDamageAbsorbs() + end + end + local displayAbsorbs = (absorbAmt == nil) and totalAbsorbs or absorbAmt + + if shieldEnabled then + shieldBar:SetMinMaxValues(0, healthMax) + shieldBar:SetValue(displayAbsorbs) + shieldBar:Show() + else + shieldBar:Hide() + end + -- Overshield glow: use SetAlphaFromBoolean for secret bool support + if overshieldEnabled and isClamped ~= nil then + local glow = self.widgets.overShieldGlow + if glow.SetAlphaFromBoolean then + glow:Show() + glow:SetAlphaFromBoolean(isClamped, 1, 0) + else + if F.IsValueNonSecret(isClamped) and isClamped then + glow:Show() + else + glow:Hide() + end + end + else + self.widgets.overShieldGlow:Hide() + end + self.widgets.shieldBarR:Hide() + self.widgets.overShieldGlowR:Hide() + + -- Indicator: StatusBar-based, C-level handles secret values if enabledIndicators["shieldBar"] then - if indicatorBooleans["shieldBar"] then - -- onlyShowOvershields - local overshieldPercent = (self.states.totalAbsorbs + self.states.health - self.states.healthMax) / self.states.healthMax - if overshieldPercent > 0 then + -- Size the indicator to match health bar + local indBar = self.indicators.shieldBar + if self.orientation == "horizontal" then + indBar:SetWidth(self.widgets.healthBar:GetWidth()) + else + indBar:SetHeight(self.widgets.healthBar:GetHeight()) + end + indBar:SetAbsorbs(displayAbsorbs, healthMax) + indBar:Show() + else + self.indicators.shieldBar:Hide() + end + else + -- Normal path: Lua arithmetic is safe (non-secret values) + if totalAbsorbs > 0 then + local shieldPercent = totalAbsorbs / healthMax + + -- Indicator (percentage-based overlay) + if enabledIndicators["shieldBar"] then + if indicatorBooleans["shieldBar"] then + -- onlyShowOvershields + local overshieldPercent = (totalAbsorbs + health - healthMax) / healthMax + if overshieldPercent > 0 then + self.indicators.shieldBar:Show() + self.indicators.shieldBar:SetPercent(overshieldPercent) + else + self.indicators.shieldBar:Hide() + end + else self.indicators.shieldBar:Show() - self.indicators.shieldBar:SetValue(overshieldPercent) + self.indicators.shieldBar:SetPercent(shieldPercent) + end + else + self.indicators.shieldBar:Hide() + end + + -- Widget shield bar (StatusBar) + if shieldEnabled then + -- Set size to match health bar + if self.orientation == "horizontal" then + shieldBar:SetWidth(self.widgets.healthBar:GetWidth()) + else + shieldBar:SetHeight(self.widgets.healthBar:GetHeight()) + end + shieldBar:SetMinMaxValues(0, healthMax) + shieldBar:SetValue(totalAbsorbs) + shieldBar:Show() + else + shieldBar:Hide() + end + + -- Overshield glow + local healthPercent = self.states.healthPercent + if shieldPercent + healthPercent > 1 then + if overshieldReverseFillEnabled then + local p = shieldPercent + healthPercent - 1 + if p > healthPercent then p = healthPercent end + local barSize = (self.orientation == "horizontal") + and self.widgets.healthBar:GetWidth() + or self.widgets.healthBar:GetHeight() + local shieldBarR = self.widgets.shieldBarR + if self.orientation == "horizontal" then + shieldBarR:SetWidth(p * barSize) + else + shieldBarR:SetHeight(p * barSize) + end + shieldBarR:Show() + if overshieldEnabled then + self.widgets.overShieldGlowR:Show() + else + self.widgets.overShieldGlowR:Hide() + end + self.widgets.overShieldGlow:Hide() else - self.indicators.shieldBar:Hide() + if overshieldEnabled then + self.widgets.overShieldGlow:Show() + else + self.widgets.overShieldGlow:Hide() + end + self.widgets.shieldBarR:Hide() + self.widgets.overShieldGlowR:Hide() end else - self.indicators.shieldBar:Show() - self.indicators.shieldBar:SetValue(shieldPercent) + self.widgets.overShieldGlow:Hide() + self.widgets.shieldBarR:Hide() + self.widgets.overShieldGlowR:Hide() end else self.indicators.shieldBar:Hide() + shieldBar:Hide() + self.widgets.overShieldGlow:Hide() + self.widgets.shieldBarR:Hide() + self.widgets.overShieldGlowR:Hide() end - - self.widgets.shieldBar:SetValue(shieldPercent, self.states.healthPercent) - else - self.indicators.shieldBar:Hide() - self.widgets.shieldBar:Hide() - self.widgets.overShieldGlow:Hide() - self.widgets.shieldBarR:Hide() - self.widgets.overShieldGlowR:Hide() end end @@ -2654,12 +3334,52 @@ local function UnitButton_UpdateHealAbsorbs(self, skipStateUpdates) UnitButton_UpdateHealthStates(self) end - if self.states.healAbsorbs > 0 then - local absorbsPercent = self.states.healAbsorbs / self.states.healthMax - self.widgets.absorbsBar:SetValue(absorbsPercent, self.states.healthPercent) + local absorbsBar = self.widgets.absorbsBar + if absorbInvertColor then + local r, g, b = F.InvertColor(self.widgets.healthBar:GetStatusBarColor()) + absorbsBar:SetStatusBarColor(r, g, b) + absorbsBar.overAbsorbGlow:SetVertexColor(r, g, b) + end + + -- 12.0+: calculator returns potentially secret values, passed to C-level StatusBar APIs + local calc = self.widgets.healPredictionCalculator + local healAbsorbAmt, isClamped + if calc and UnitGetDetailedHealPrediction then + if UnitExists(unit) then + UnitGetDetailedHealPrediction(unit, nil, calc) + end + if calc.GetHealAbsorbs then + healAbsorbAmt, isClamped = calc:GetHealAbsorbs() + end + end + + local _healAbs = (healAbsorbAmt == nil) and self.states.healAbsorbs or healAbsorbAmt + local displayAbsorbs = (_healAbs == nil) and 0 or _healAbs + absorbsBar:SetMinMaxValues(0, self.states.health) + absorbsBar:SetValue(displayAbsorbs) + absorbsBar:Show() + + -- Over-absorb glow using SetAlphaFromBoolean for secret bool support + local glow = self.widgets.overAbsorbGlow + if isClamped ~= nil then + if SetAlphaFromBoolean then + glow:Show() + SetAlphaFromBoolean(glow, isClamped, 1, 0) + else + if F.IsValueNonSecret(isClamped) and isClamped then + glow:Show() + else + glow:Hide() + end + end else - self.widgets.absorbsBar:Hide() - self.widgets.overAbsorbGlow:Hide() + -- No isClamped available: compare displayAbsorbs to health when non-secret + local showGlow = F.IsValueNonSecret(displayAbsorbs) and displayAbsorbs and displayAbsorbs > self.states.health + if showGlow then + glow:Show() + else + glow:Hide() + end end end @@ -2795,7 +3515,7 @@ UnitButton_UpdateStatusText = function(self) statusText:Show() statusText:SetStatus("OFFLINE") statusText:ShowTimer() - -- Midnight 12.0.0+: UnitIsAFK may return a secret boolean — skip on Midnight + -- Midnight 12.0.0+: UnitIsAFK may return a secret boolean -- skip on Midnight elseif not Cell.isMidnight and UnitIsAFK(unit) then statusText:Show() statusText:SetStatus("AFK") @@ -2880,11 +3600,6 @@ UnitButton_UpdateHealthColor = function(self) local unit = self.states.unit if not unit then return end - -- NOTE: Health bar coloring uses non-secret data (class, settings, UnitIsPlayer, etc.) - -- so the classic color logic below works on both Midnight and pre-Midnight. - -- TODO: implement proper ColorCurve coloring for threshold/gradient modes once - -- SetStatusBarColor secret color API is verified on PTR. - self.states.class = UnitClassBase(unit) --! update class local barR, barG, barB @@ -2896,6 +3611,74 @@ UnitButton_UpdateHealthColor = function(self) lossA = CellDB["appearance"]["lossAlpha"] end + -- MIDNIGHT PATH: use UnitHealthPercent + color curves for secret-safe gradient evaluation + if Cell.isMidnight and self.widgets.healthBarColorCurve and UnitHealthPercent then + local useCurve = false + + if UnitIsPlayer(unit) or UnitInPartyIsAI(unit) then + if not UnitIsConnected(unit) then + barR, barG, barB = 0.4, 0.4, 0.4 + lossR, lossG, lossB = 0.4, 0.4, 0.4 + elseif UnitIsCharmed(unit) then + barR, barG, barB, barA = 0.5, 0, 1, 1 + lossR, lossG, lossB, lossA = barR*0.2, barG*0.2, barB*0.2, 1 + else + useCurve = true + end + elseif F.IsPet(self.states.guid, self.states.unit) then + useCurve = true + else + useCurve = true + end + + if useCurve then + -- Rebuild curves (handles class color per unit + current settings) + B.UpdateHealthColorCurve(self) + -- UnitHealthPercent(unit, true, curve) evaluates health% against the curve + -- entirely at the C level — secret-safe, returns a ColorMixin + local barColor = UnitHealthPercent(unit, true, self.widgets.healthBarColorCurve) + local lossColor = UnitHealthPercent(unit, true, self.widgets.healthLossColorCurve) + if barColor then + barR, barG, barB = barColor:GetRGB() + end + if lossColor then + lossR, lossG, lossB = lossColor:GetRGB() + end + -- fullColor override: check if at full health (non-secret path) + if Cell.vars.useFullColor then + local health = UnitHealth(unit) + local healthMax = UnitHealthMax(unit) + if not F.HasAnySecretValues(health, healthMax) and healthMax > 0 and health == healthMax then + barR = CellDB["appearance"]["fullColor"][2][1] + barG = CellDB["appearance"]["fullColor"][2][2] + barB = CellDB["appearance"]["fullColor"][2][3] + end + end + -- deathColor override + if (self.states.isDeadOrGhost or self.states.isDead) and Cell.vars.useDeathColor then + lossR = CellDB["appearance"]["deathColor"][2][1] + lossG = CellDB["appearance"]["deathColor"][2][2] + lossB = CellDB["appearance"]["deathColor"][2][3] + end + end + + -- Apply colors — SetStatusBarColor and SetVertexColor accept secret ColorMixin results + if barR then + self.widgets.healthBar:SetStatusBarColor(barR, barG, barB, barA) + end + if lossR then + self.widgets.healthBarLoss:SetVertexColor(lossR, lossG, lossB, lossA) + end + -- Incoming heal color + if Cell.loaded and CellDB["appearance"]["healPrediction"][2] then + self.widgets.incomingHeal:SetStatusBarColor(CellDB["appearance"]["healPrediction"][3][1], CellDB["appearance"]["healPrediction"][3][2], CellDB["appearance"]["healPrediction"][3][3], CellDB["appearance"]["healPrediction"][3][4]) + elseif barR then + self.widgets.incomingHeal:SetStatusBarColor(barR, barG, barB, 0.4) + end + return + end + + -- PRE-MIDNIGHT PATH: original Lua-based color logic if UnitIsPlayer(unit) or UnitInPartyIsAI(unit) then -- player if not UnitIsConnected(unit) then barR, barG, barB = 0.4, 0.4, 0.4 @@ -2917,34 +3700,115 @@ UnitButton_UpdateHealthColor = function(self) self.widgets.healthBar:SetStatusBarColor(barR, barG, barB, barA) self.widgets.healthBarLoss:SetVertexColor(lossR, lossG, lossB, lossA) - if Cell.isMidnight then - -- StatusBar on Midnight: use SetStatusBarColor - if Cell.loaded and CellDB["appearance"]["healPrediction"][2] then - self.widgets.incomingHeal:SetStatusBarColor(CellDB["appearance"]["healPrediction"][3][1], CellDB["appearance"]["healPrediction"][3][2], CellDB["appearance"]["healPrediction"][3][3], CellDB["appearance"]["healPrediction"][3][4]) - else - self.widgets.incomingHeal:SetStatusBarColor(barR, barG, barB, 0.4) - end + -- Texture on pre-Midnight: use SetVertexColor + if Cell.loaded and CellDB["appearance"]["healPrediction"][2] then + self.widgets.incomingHeal:SetVertexColor(CellDB["appearance"]["healPrediction"][3][1], CellDB["appearance"]["healPrediction"][3][2], CellDB["appearance"]["healPrediction"][3][3], CellDB["appearance"]["healPrediction"][3][4]) else - -- Texture on pre-Midnight: use SetVertexColor - if Cell.loaded and CellDB["appearance"]["healPrediction"][2] then - self.widgets.incomingHeal:SetVertexColor(CellDB["appearance"]["healPrediction"][3][1], CellDB["appearance"]["healPrediction"][3][2], CellDB["appearance"]["healPrediction"][3][3], CellDB["appearance"]["healPrediction"][3][4]) - else - self.widgets.incomingHeal:SetVertexColor(barR, barG, barB, 0.4) - end + self.widgets.incomingHeal:SetVertexColor(barR, barG, barB, 0.4) end end --- Configures the health color curve for a button (Midnight 12.0.0+) --- Called when color settings change (e.g., class color, custom color toggled) -function B.UpdateHealthColorCurve(button) - if not (Cell.isMidnight and button.widgets.healthColorCurve) then return end - local curve = button.widgets.healthColorCurve - curve:ClearPoints() - -- Default green gradient; overridden by class color / custom color settings - -- TODO: read from CellDB["appearance"] color settings and build proper curve - curve:AddPoint(0.0, {r=1, g=0, b=0, a=1}) -- red at 0% - curve:AddPoint(0.5, {r=1, g=1, b=0, a=1}) -- yellow at 50% - curve:AddPoint(1.0, {r=0, g=0.9, b=0, a=1}) -- green at 100% +-- Curve-based health color system (Midnight 12.0.0+) +-- do...end scopes the helper functions (BuildThresholdCurve, BuildFlatCurve) +-- so they don't leak into the file's top-level scope. +do + -- Builds a color curve from 3 color points + boundary settings. + local function BuildThresholdCurve(curve, c1, c2, c3, lowBound, highBound, useGradient) + curve:ClearPoints() + lowBound = lowBound or 0.05 + highBound = highBound or 0.95 + + local col1 = CreateColor(c1[1], c1[2], c1[3], 1) + local col2 = CreateColor(c2[1], c2[2], c2[3], 1) + local col3 = CreateColor(c3[1], c3[2], c3[3], 1) + + if useGradient then + curve:SetType(Enum.LuaCurveType.Linear) + curve:AddPoint(0.0, col1) + curve:AddPoint(lowBound, col1) + local mid = (lowBound + highBound) / 2 + curve:AddPoint(mid, col2) + curve:AddPoint(highBound, col3) + curve:AddPoint(1.0, col3) + else + curve:SetType(Enum.LuaCurveType.Linear) + local eps = 0.001 + curve:AddPoint(0.0, col1) + if lowBound > eps then + curve:AddPoint(lowBound - eps, col1) + end + curve:AddPoint(lowBound + eps, col2) + if highBound - lowBound > 2 * eps then + curve:AddPoint(highBound - eps, col2) + end + curve:AddPoint(highBound + eps, col3) + curve:AddPoint(1.0, col3) + end + end + + local function BuildFlatCurve(curve, r, g, b) + curve:ClearPoints() + curve:SetType(Enum.LuaCurveType.Linear) + local col = CreateColor(r, g, b, 1) + curve:AddPoint(0.0, col) + curve:AddPoint(1.0, col) + end + + -- Configures the health color curves for a button (Midnight 12.0.0+) + -- Builds curves from user settings so UnitHealthPercent(unit, true, curve) + -- can evaluate gradient colors at the C level. + function B.UpdateHealthColorCurve(button) + if not Cell.isMidnight then return end + if not button.widgets.healthBarColorCurve then return end + if not Cell.loaded then return end + + local unit = button.states.displayedUnit or button.states.unit + local barCurve = button.widgets.healthBarColorCurve + local lossCurve = button.widgets.healthLossColorCurve + + local class = button.states.class or (unit and UnitClassBase(unit)) or Cell.vars.playerClass + local cr, cg, cb = F.GetClassColor(class) + + -- Build bar color curve + local barMode = CellDB["appearance"]["barColor"][1] + if barMode == "threshold1" then + local c = CellDB["appearance"]["colorThresholds"] + BuildThresholdCurve(barCurve, c[1], c[2], c[3], c[4], c[5], c[6]) + elseif barMode == "threshold2" then + local c = CellDB["appearance"]["colorThresholds"] + BuildThresholdCurve(barCurve, c[1], c[2], {cr, cg, cb}, c[4], c[5], c[6]) + elseif barMode == "threshold3" then + local c = CellDB["appearance"]["colorThresholds"] + BuildThresholdCurve(barCurve, c[1], c[2], {cr*0.2, cg*0.2, cb*0.2}, c[4], c[5], c[6]) + elseif barMode == "class_color" then + BuildFlatCurve(barCurve, cr, cg, cb) + elseif barMode == "class_color_dark" then + BuildFlatCurve(barCurve, cr*0.2, cg*0.2, cb*0.2) + else + local cc = CellDB["appearance"]["barColor"][2] + BuildFlatCurve(barCurve, cc[1], cc[2], cc[3]) + end + + -- Build loss color curve + local lossMode = CellDB["appearance"]["lossColor"][1] + if lossMode == "threshold1" then + local c = CellDB["appearance"]["colorThresholdsLoss"] + BuildThresholdCurve(lossCurve, c[1], c[2], c[3], c[4], c[5], c[6]) + elseif lossMode == "threshold2" then + local c = CellDB["appearance"]["colorThresholdsLoss"] + BuildThresholdCurve(lossCurve, {cr, cg, cb}, c[2], c[3], c[4], c[5], c[6]) + elseif lossMode == "threshold3" then + local c = CellDB["appearance"]["colorThresholdsLoss"] + BuildThresholdCurve(lossCurve, {cr*0.2, cg*0.2, cb*0.2}, c[2], c[3], c[4], c[5], c[6]) + elseif lossMode == "class_color" then + BuildFlatCurve(lossCurve, cr, cg, cb) + elseif lossMode == "class_color_dark" then + BuildFlatCurve(lossCurve, cr*0.2, cg*0.2, cb*0.2) + else + local cc = CellDB["appearance"]["lossColor"][2] + BuildFlatCurve(lossCurve, cc[1], cc[2], cc[3]) + end + end end ------------------------------------------------- @@ -3087,7 +3951,10 @@ local function UnitButton_RegisterEvents(self) -- self:RegisterEvent("UNIT_PET") self:RegisterEvent("UNIT_PORTRAIT_UPDATE") -- pet summoned far away - --! OnShow时立即执行,但UpdateIndicators可能并未执行完毕,导致在ResetCustomIndicators过程中指示器发生变化,进而报错 + --! OnShow时立即执行,但UpdateIndicators可能并未执行完毕,导致在ResetCustomIndicators过程中指示器发生变化,进而报错 + -- OnShow fires immediately but UpdateIndicators may not have completed yet, + -- so indicators can change during ResetCustomIndicators and cause errors. + -- pcall prevents one frame's error from breaking all other frames. local success, result = pcall(UnitButton_UpdateAll, self) if not success then F.Debug("UnitButton_UpdateAll |cffff0000FAILED:|r", self:GetName(), result) @@ -3130,9 +3997,17 @@ local function UnitButton_OnEvent(self, event, unit, arg) elseif event == "UNIT_ABSORB_AMOUNT_CHANGED" then UnitButton_UpdateShieldAbsorbs(self) + -- Refresh health text so shield component updates immediately + if enabledIndicators["healthText"] then + UnitButton_UpdateHealthStates(self) + end elseif event == "UNIT_HEAL_ABSORB_AMOUNT_CHANGED" then UnitButton_UpdateHealAbsorbs(self) + -- Refresh health text so healAbsorb component updates immediately + if enabledIndicators["healthText"] then + UnitButton_UpdateHealthStates(self) + end elseif event == "UNIT_MAXPOWER" then UnitButton_UpdatePowerStates(self) @@ -3199,6 +4074,28 @@ local function UnitButton_OnEvent(self, event, unit, arg) elseif event == "PLAYER_REGEN_ENABLED" or event == "PLAYER_REGEN_DISABLED" then UnitButton_UpdateLeader(self, event) + if event == "PLAYER_REGEN_ENABLED" then + -- 12.0+: secret values may linger briefly after combat ends. + -- Immediate refresh + delayed retry to catch stale secrets. + UnitButton_UpdateHealth(self) + UnitButton_UpdateShieldAbsorbs(self) + UnitButton_UpdateHealAbsorbs(self) + UnitButton_UpdatePowerStates(self) + UnitButton_UpdatePowerText(self) + UnitButton_UpdateAuras(self) + -- Delayed retry: values at full health/power won't get events + local btn = self + C_Timer.After(0.5, function() + if btn.states.displayedUnit then + UnitButton_UpdateHealth(btn) + UnitButton_UpdateShieldAbsorbs(btn) + UnitButton_UpdateHealAbsorbs(btn) + UnitButton_UpdatePowerStates(btn) + UnitButton_UpdatePowerText(btn) + UnitButton_UpdateAuras(btn) + end + end) + end elseif event == "PLAYER_TARGET_CHANGED" then UnitButton_UpdateTarget(self) @@ -3367,8 +4264,7 @@ local function UnitButton_OnLeave(self) GameTooltip:Hide() end -local UNKNOWN = _G.UNKNOWN -local UNKNOWNOBJECT = _G.UNKNOWNOBJECT +local UNKNOWN, UNKNOWNOBJECT = _G.UNKNOWN, _G.UNKNOWNOBJECT local function UnitButton_OnTick(self) -- print(GetTime(), "OnTick", self._updateRequired, self:GetAttribute("refreshOnUpdate"), self:GetName()) local e = (self.__tickCount or 0) + 1 @@ -3377,26 +4273,43 @@ local function UnitButton_OnTick(self) if self.states.unit and self.states.displayedUnit then local displayedGuid = UnitGUID(self.states.displayedUnit) - if displayedGuid ~= self.__displayedGuid then + -- UnitGUID and __displayedGuid may be secret strings; == comparison on secrets + -- is safe (secrets never equal non-secrets), but ~= crashes. Use == nil check + -- and F.IsValueNonSecret to guard the comparison. + local guidChanged = false + if not F.IsValueNonSecret(displayedGuid) or not F.IsValueNonSecret(self.__displayedGuid) then + -- Secret GUID: assume changed (forces update, safe fallback) + guidChanged = true + else + guidChanged = displayedGuid ~= self.__displayedGuid + end + if guidChanged then -- NOTE: displayed unit entity changed F.RemoveElementsExceptKeys(self.states, "unit", "displayedUnit") self.__displayedGuid = displayedGuid - if displayedGuid then --? clearing unit may come before hiding + if displayedGuid ~= nil then --? clearing unit may come before hiding self._updateRequired = 1 self._powerUpdateRequired = 1 end end local guid = UnitGUID(self.states.unit) - if guid and guid ~= self.__unitGuid then + -- Same secret guard for unit GUID comparison + local unitGuidChanged = false + if not F.IsValueNonSecret(guid) or not F.IsValueNonSecret(self.__unitGuid) then + unitGuidChanged = guid ~= nil + else + unitGuidChanged = guid and guid ~= self.__unitGuid + end + if unitGuidChanged then -- print("guidChanged:", self:GetName(), self.states.unit, guid) -- NOTE: unit entity changed -- update Cell.vars.guids self.__unitGuid = guid -- On Midnight 12.0.0+, GUIDs for non-player units in instances are secret - -- Can't use a secret as a table key — only store non-secret GUIDs + -- Can't use a secret as a table key -- only store non-secret GUIDs if not self.isSpotlight then - if not (Cell.isMidnight and F.IsSecretValue and F.IsSecretValue(guid)) then + if F.IsValueNonSecret(guid) then Cell.vars.guids[guid] = self.states.unit end end @@ -3411,7 +4324,7 @@ local function UnitButton_OnTick(self) self.__nameRetries = nil else -- NOTE: update on next tick - -- 国服可以起名为“未知目标”,干!就只多重试4次好了 + -- 国服可以起名为"未知目标",干!就只多重试4次好了 self.__nameRetries = (self.__nameRetries or 0) + 1 self.__unitGuid = nil end @@ -3734,6 +4647,15 @@ function B.SetOrientation(button, orientation, rotateTexture) healthBar:SetRotatesTexture(rotateTexture) powerBar:SetRotatesTexture(rotateTexture) + -- StatusBar orientation for shield/absorb/heal bars (12.0 secret value support) + local barOrientation = (orientation == "vertical_health") and "vertical" or orientation + incomingHeal:SetOrientation(barOrientation) + incomingHeal:SetRotatesTexture(rotateTexture) + shieldBar:SetOrientation(barOrientation) + shieldBar:SetRotatesTexture(rotateTexture) + absorbsBar:SetOrientation(barOrientation) + absorbsBar:SetRotatesTexture(rotateTexture) + button.indicators.healthThresholds:SetOrientation(orientation) if rotateTexture then @@ -3741,16 +4663,11 @@ function B.SetOrientation(button, orientation, rotateTexture) F.RotateTexture(powerBarLoss, 90) if not Cell.isMidnight then F.RotateTexture(incomingHeal, 90) end F.RotateTexture(damageFlashTex, 90) - -- F.RotateTexture(shieldBar, 90) - -- F.RotateTexture(absorbsBar, 90) else F.RotateTexture(healthBarLoss, 0) F.RotateTexture(powerBarLoss, 0) if not Cell.isMidnight then F.RotateTexture(incomingHeal, 0) end F.RotateTexture(damageFlashTex, 0) - -- F.RotateTexture(overShieldGlow, 0) - -- F.RotateTexture(shieldBar, 0) - -- F.RotateTexture(absorbsBar, 0) end if orientation == "horizontal" then @@ -4020,6 +4937,19 @@ end -- powerText function B.UpdatePowerText(button) + -- displayedUnit is set by UnitButton_UpdateAll (OnShow/vehicle check). + -- When enabling the indicator at runtime, buttons may not have gone + -- through UpdateAll yet. Fall back to states.unit so power APIs work. + if not button.states.displayedUnit and button.states.unit then + button.states.displayedUnit = button.states.unit + end + -- If still no unit, try GetAttribute (secure header always sets this) + if not button.states.displayedUnit then + local attrUnit = button:GetAttribute("unit") + if attrUnit then + button.states.displayedUnit = attrUnit + end + end if button.states.displayedUnit then UnitButton_UpdatePowerStates(button) UnitButton_UpdatePowerText(button) @@ -4041,7 +4971,14 @@ end function B.UpdateAnimation(button) barAnimationType = CellDB["appearance"]["barAnimation"] - if barAnimationType == "Smooth" then + if Cell.isMidnight then + -- Midnight: smooth animation handled via StatusBarInterpolation enum in SetValue(). + -- Never use SetSmoothedValue mixin (does Lua Clamp arithmetic, crashes on secrets). + button.widgets.healthBar:ResetSmoothedValue() + button.widgets.healthBar.SetBarValue = button.widgets.healthBar.SetValue + button.widgets.powerBar:ResetSmoothedValue() + button.widgets.powerBar.SetBarValue = button.widgets.powerBar.SetValue + elseif barAnimationType == "Smooth" then button.widgets.healthBar.SetBarValue = button.widgets.healthBar.SetSmoothedValue button.widgets.powerBar.SetBarValue = button.widgets.powerBar.SetSmoothedValue else @@ -4051,18 +4988,6 @@ function B.UpdateAnimation(button) button.widgets.powerBar.SetBarValue = button.widgets.powerBar.SetValue end - if barAnimationType ~= "Flash" then - button.widgets.damageFlashAG:Finish() - end -end - --- damageFlash -function B.ShowFlash(button, lostPercent) - button.widgets.damageFlashTex:SetValue(lostPercent) - button.widgets.damageFlashAG:Play() -end - -function B.HideFlash(button) button.widgets.damageFlashAG:Finish() end @@ -4130,11 +5055,12 @@ local startTimeCache = {} -- Layers --------------------------------------- -- OVERLAY -- ARTWORK --- -2 overAbsorbGlow --- -3 absorbsBar --- -4 overShieldGlow, overShieldGlowR --- -5 shieldBar, shieldBarR --- -6 incomingHeal, damageFlashTex +-- -2 overAbsorbGlow (texture) +-- absorbsBar (StatusBar, frame level midLevel+2) +-- -4 overShieldGlow, overShieldGlowR (texture) +-- shieldBar (StatusBar, frame level midLevel+1), shieldBarR (texture) +-- incomingHeal (StatusBar, frame level healthBar+1) +-- -6 damageFlashTex -- -7 healthBar, healthBarLoss -- BORDER -- 0 gapTexture @@ -4158,9 +5084,10 @@ function CellUnitButton_OnLoad(button) -- corrupt the shared healthCalculator used by health/absorb reads. button.widgets.healPredictionCalculator = CreateUnitHealPredictionCalculator() end - -- Color curve for health bar coloring (Patch 12.0.0+) + -- Color curves for health bar coloring (Patch 12.0.0+) if Cell.isMidnight and C_CurveUtil then - button.widgets.healthColorCurve = C_CurveUtil.CreateColorCurve() + button.widgets.healthBarColorCurve = C_CurveUtil.CreateColorCurve() + button.widgets.healthLossColorCurve = C_CurveUtil.CreateColorCurve() end InitAuraTables(button) diff --git a/Utilities/QuickAssist.lua b/Utilities/QuickAssist.lua index 864cdfe2..deffc4cf 100644 --- a/Utilities/QuickAssist.lua +++ b/Utilities/QuickAssist.lua @@ -298,7 +298,7 @@ end local function QuickAssist_UpdateCasts(self, spellId) if not self.unit then return end -- Midnight 12.0.0+: spellId from UNIT_SPELLCAST_SUCCEEDED is secret during restricted contexts - if Cell.isMidnight and issecretvalue and issecretvalue(spellId) then return end + if not F.IsValueNonSecret(spellId) then return end if not offensiveCasts[spellId] then return end self._casts[spellId] = GetTime() diff --git a/Utilities/QuickCast.lua b/Utilities/QuickCast.lua index 2a539129..555a6336 100644 --- a/Utilities/QuickCast.lua +++ b/Utilities/QuickCast.lua @@ -898,7 +898,7 @@ local function QuickCast_UpdateAuras(self) AuraUtil.ForEachAura(self.unit, "HELPFUL", nil, function(name, icon, count, debuffType, duration, expirationTime, source, isStealable, nameplateShowPersonal, spellId) -- Midnight 12.0.0+: skip auras whose fields are secret; non-secret auras (e.g. raid buffs) are safe to read - if Cell.isMidnight and issecretvalue and issecretvalue(spellId) then return end + if not F.IsValueNonSecret(spellId) then return end if glowBuffs[name] then glowBuffFound = true @@ -925,7 +925,7 @@ end local function QuickCast_UpdateCasts(self, spellId) -- Midnight 12.0.0+: spellId from UNIT_SPELLCAST_SUCCEEDED is secret during restricted contexts - if Cell.isMidnight and issecretvalue and issecretvalue(spellId) then return end + if not F.IsValueNonSecret(spellId) then return end if glowCasts[spellId] then self:SetGlowCastCooldown(GetTime(), glowCasts[spellId]) end diff --git a/Utils.lua b/Utils.lua index 018550d2..c7999edd 100644 --- a/Utils.lua +++ b/Utils.lua @@ -22,6 +22,14 @@ Cell.isCata = WOW_PROJECT_ID == WOW_PROJECT_CATACLYSM_CLASSIC Cell.isMists = WOW_PROJECT_ID == WOW_PROJECT_MISTS_CLASSIC Cell.isTWW = LE_EXPANSION_LEVEL_CURRENT == LE_EXPANSION_WAR_WITHIN +------------------------------------------------- +-- 12.0+ API compatibility shims +------------------------------------------------- +-- IsEncounterInProgress moved to C_InstanceEncounter namespace in 12.0 +if not IsEncounterInProgress and C_InstanceEncounter and C_InstanceEncounter.IsEncounterInProgress then + IsEncounterInProgress = C_InstanceEncounter.IsEncounterInProgress +end + if Cell.isRetail then Cell.flavor = "retail" elseif Cell.isMists then @@ -1046,9 +1054,13 @@ function F.HandleUnitButton(type, unit, func, ...) end for _, b in pairs(Cell.unitButtons.spotlight) do - if b.states.unit and UnitIsUnit(b.states.unit, unit) then - func(b, ...) - handled = true + if b.states.unit then + local isMatch = UnitIsUnit(b.states.unit, unit) + -- UnitIsUnit may return a secret boolean on Midnight; treat secret as true + if not F.IsValueNonSecret(isMatch) or isMatch then + func(b, ...) + handled = true + end end end @@ -1058,6 +1070,13 @@ end function F.UpdateTextWidth(fs, text, width, relativeTo) if not text or not width then return end + -- Midnight: text may be a secret string (e.g. NPC names); cannot do Lua string + -- operations on secrets. SetText is C-level and handles secrets directly. + if not F.IsValueNonSecret(text) then + fs:SetText(text) + return + end + if width == "unlimited" then fs:SetText(text) elseif width[1] == "percentage" then @@ -2004,6 +2023,8 @@ local function predicate(...) end function F.FindAuraById(unit, type, spellId) + -- 12.0+: skip when aura data is restricted (secret values) + if Cell.isMidnight and F.IsAuraRestricted() then return nil end if type == "BUFF" then return AuraUtil.FindAura(predicate, unit, "HELPFUL", spellId) else @@ -2017,6 +2038,8 @@ if Cell.isRetail then if Cell.isMidnight and F.IsAuraRestricted() then return {} end local debuffs = {} AuraUtil.ForEachAura(unit, "HARMFUL", nil, function(name, icon, count, debuffType, duration, expirationTime, source, isStealable, nameplateShowPersonal, spellId) + -- Guard: spellId may be secret even when IsAuraRestricted is false + if not F.IsValueNonSecret(spellId) then return end if spellIds[spellId] then debuffs[spellId] = I.CheckDebuffType(debuffType, spellId) end @@ -2029,6 +2052,8 @@ if Cell.isRetail then if Cell.isMidnight and F.IsAuraRestricted() then return {} end local debuffs = {} AuraUtil.ForEachAura(unit, "HARMFUL", nil, function(name, icon, count, debuffType, duration, expirationTime, source, isStealable, nameplateShowPersonal, spellId) + -- Guard: spellId/debuffType may be secret even when IsAuraRestricted is false + if not F.IsValueNonSecret(spellId) or not F.IsValueNonSecret(debuffType) then return end if types == "all" or types[debuffType] then debuffs[spellId] = I.CheckDebuffType(debuffType, spellId) end @@ -2291,11 +2316,15 @@ local harmItems = { local UnitInSpellRange if C_Spell and C_Spell.IsSpellInRange then UnitInSpellRange = function(spellName, unit) - return IsSpellInRange(spellName, unit) + local r = IsSpellInRange(spellName, unit) + if not F.IsValueNonSecret(r) then return nil end + return r and true or false end else UnitInSpellRange = function(spellName, unit) - return IsSpellInRange(spellName, unit) == 1 + local result = IsSpellInRange(spellName, unit) + if not F.IsValueNonSecret(result) then return nil end + return result == 1 end end @@ -2349,7 +2378,8 @@ end rc:SetScript("OnEvent", DELAYED_SPELLS_CHANGED) function F.IsInRange(unit, check) - if not UnitIsVisible(unit) then + local visible = UnitIsVisible(unit) + if not F.IsValueNonSecret(visible) or not visible then return false end @@ -2361,7 +2391,7 @@ function F.IsInRange(unit, check) --! but not available for PLAYER PET when SOLO local inRange, checked = UnitInRange(unit) -- Midnight 12.0.0+: UnitInRange returns secret booleans during restricted contexts - if Cell.isMidnight and issecretvalue and issecretvalue(checked) then + if not F.IsValueNonSecret(checked) then return F.IsInRange(unit, true) end if not checked then @@ -2385,7 +2415,7 @@ function F.IsInRange(unit, check) local inRange, checked = UnitInRange(unit) -- Midnight 12.0.0+: UnitInRange returns secret booleans during restricted contexts - if Cell.isMidnight and issecretvalue and issecretvalue(checked) then + if not F.IsValueNonSecret(checked) then -- Skip, fall through to pet/interact checks below elseif checked then return inRange @@ -2509,12 +2539,16 @@ end ------------------------------------------------- -- Secret value utilities (Patch 12.0.0+) ------------------------------------------------- --- issecretvalue() is a native WoW API available in 12.0.0+ -function F.IsSecretValue(val) - if issecretvalue then - return issecretvalue(val) - end - return false +-- issecretvalue() and hasanysecretvalues() are native WoW APIs available in 12.0.0+. +-- Convention: these globals are ONLY referenced inside Utils.lua wrapper implementations. +-- All other files use F.IsValueNonSecret(), F.HasAnySecretValues(), etc. + +-- Varargs check: returns true if ANY argument is a secret value. +-- Wraps the global hasanysecretvalues() with a Cell.isMidnight guard. +function F.HasAnySecretValues(...) + if not Cell.isMidnight then return false end + if not hasanysecretvalues then return false end + return hasanysecretvalues(...) end -- GetRestrictedActionStatus() returns non-secret boolean From 6aea840f39b87cac0cbe9e36605c01eb78ca9736 Mon Sep 17 00:00:00 2001 From: jdtoppin <6392799+jdtoppin@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:19:12 -0600 Subject: [PATCH 02/61] PR 2: Health/power text + color gradients Health text: BuildSecretSegment pre-builds C-level format strings for secret values. UnitHealthPercent (C-level) for percentage display. AbbreviateNumbers for numeric display. Remove unjustified pcall around UnitHealthPercent. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Power text: F.HasAnySecretValues guard, UnitPowerPercent with ScaleTo100 curve, AbbreviateNumbers for short format. Shield/absorb display: rawequal → == nil cleanup. hideIfEmptyOrFull: stays removed on Midnight (no C-level path for health == maxHealth check). Co-Authored-By: Claude Opus 4.6 (1M context) --- Indicators/Built-in.lua | 370 +++++++++++++++++++++++++++++++--------- 1 file changed, 288 insertions(+), 82 deletions(-) diff --git a/Indicators/Built-in.lua b/Indicators/Built-in.lua index 35f36133..d095f1d6 100644 --- a/Indicators/Built-in.lua +++ b/Indicators/Built-in.lua @@ -11,6 +11,9 @@ local P = Cell.pixelPerfectFuncs local LCG = LibStub("LibCustomGlow-1.0") local LibTranslit = LibStub("LibTranslit-1.0") +local AbbreviateNumbers = AbbreviateNumbers +local UnitHealthPercent = UnitHealthPercent +local CurveConstants = CurveConstants local function noop() end @@ -216,7 +219,9 @@ function I.CreateDefensiveCooldowns(parent) for i = 1, 5 do local name = parent:GetName().."DefensiveCooldown"..i - local frame = I.CreateAura_BarIcon(name, defensiveCooldowns) + local frame = Cell.isMidnight + and I.CreateAura_BorderIcon(name, defensiveCooldowns, 1.5) + or I.CreateAura_BarIcon(name, defensiveCooldowns) tinsert(defensiveCooldowns, frame) end end @@ -241,7 +246,9 @@ function I.CreateExternalCooldowns(parent) for i = 1, 5 do local name = parent:GetName().."ExternalCooldown"..i - local frame = I.CreateAura_BarIcon(name, externalCooldowns) + local frame = Cell.isMidnight + and I.CreateAura_BorderIcon(name, externalCooldowns, 1.5) + or I.CreateAura_BarIcon(name, externalCooldowns) tinsert(externalCooldowns, frame) end end @@ -265,8 +272,10 @@ function I.CreateAllCooldowns(parent) allCooldowns.UpdatePixelPerfect = I.Cooldowns_UpdatePixelPerfect for i = 1, 5 do - local name = parent:GetName().."ExternalCooldown"..i - local frame = I.CreateAura_BarIcon(name, allCooldowns) + local name = parent:GetName().."AllCooldown"..i + local frame = Cell.isMidnight + and I.CreateAura_BorderIcon(name, allCooldowns, 1.5) + or I.CreateAura_BarIcon(name, allCooldowns) tinsert(allCooldowns, frame) end end @@ -513,7 +522,14 @@ function I.CreateDebuffs(parent) for i = 1, 10 do local name = parent:GetName().."Debuff"..i - local frame = I.CreateAura_BarIcon(name, debuffs) + local frame + if Cell.isMidnight then + -- Midnight: use BorderIcon for consistent clock-swipe cooldown display + -- (DurationObject can't drive StatusBar in tainted combat contexts) + frame = I.CreateAura_BorderIcon(name, debuffs, 1.5) + else + frame = I.CreateAura_BarIcon(name, debuffs) + end tinsert(debuffs, frame) frame._SetCooldown = frame.SetCooldown @@ -779,7 +795,7 @@ end) local function CheckCondition(operator, checkedValue, currentValue) -- Midnight 12.0.0+: applications (count) may be secret even when spellId is not; -- comparisons on secret values throw errors - if issecretvalue and (issecretvalue(currentValue) or issecretvalue(checkedValue)) then return end + if not F.IsValueNonSecret(currentValue) or not F.IsValueNonSecret(checkedValue) then return end if operator == "=" then if currentValue == checkedValue then return true end elseif operator == ">" then @@ -797,7 +813,7 @@ end function I.GetDebuffOrder(spellName, spellId, count) -- Midnight 12.0.0+: spellId/spellName may be secret; cannot use as table key - if issecretvalue and (issecretvalue(spellId) or issecretvalue(spellName)) then return end + if not F.IsValueNonSecret(spellId) or not F.IsValueNonSecret(spellName) then return end local t = currentAreaDebuffs[spellId] or currentAreaDebuffs[spellName] if not t then return end @@ -814,7 +830,7 @@ end function I.GetDebuffGlow(spellName, spellId, count) -- Midnight 12.0.0+: spellId/spellName may be secret; cannot use as table key - if issecretvalue and (issecretvalue(spellId) or issecretvalue(spellName)) then return end + if not F.IsValueNonSecret(spellId) or not F.IsValueNonSecret(spellName) then return end local t = currentAreaDebuffs[spellId] or currentAreaDebuffs[spellName] if not t then return end @@ -834,9 +850,14 @@ function I.GetDebuffGlow(spellName, spellId, count) end end +-- 12.0+: return the currentAreaDebuffs table for pre-scanning secret auras +function I.GetCurrentAreaDebuffs() + return currentAreaDebuffs +end + function I.IsDebuffUseElapsedTime(spellName, spellId) -- Midnight 12.0.0+: spellId/spellName may be secret; cannot use as table key - if issecretvalue and (issecretvalue(spellId) or issecretvalue(spellName)) then return end + if not F.IsValueNonSecret(spellId) or not F.IsValueNonSecret(spellName) then return end local t = currentAreaDebuffs[spellId] or currentAreaDebuffs[spellName] if not t then return end @@ -955,6 +976,24 @@ end -- private auras ------------------------------------------------- local function PrivateAuras_UpdatePrivateAuraAnchor(self, unit) + -- 12.0.1+: AddPrivateAuraAnchor/RemovePrivateAuraAnchor cannot be called in combat. + -- Defer until combat ends if needed. + if InCombatLockdown() then + self._pendingUnit = unit + if not self._combatDeferred then + self._combatDeferred = true + local f = CreateFrame("Frame") + f:RegisterEvent("PLAYER_REGEN_ENABLED") + f:SetScript("OnEvent", function() + f:UnregisterAllEvents() + self._combatDeferred = nil + PrivateAuras_UpdatePrivateAuraAnchor(self, self._pendingUnit) + self._pendingUnit = nil + end) + end + return + end + -- remove old if self.auraAnchorID then C_UnitAuras.RemovePrivateAuraAnchor(self.auraAnchorID) @@ -978,6 +1017,7 @@ local function PrivateAuras_UpdatePrivateAuraAnchor(self, unit) iconInfo = { iconWidth = self:GetWidth(), iconHeight = self:GetHeight(), + borderScale = self:GetWidth() / 16, iconAnchor = { point = "CENTER", relativeTo = self, @@ -1378,7 +1418,7 @@ local function StatusText_ShowTimer(self) -- Midnight 12.0.0+: guid may be secret for NPC/boss units local showGuid = self.parent.states.guid - if not (issecretvalue and issecretvalue(showGuid)) then + if F.IsValueNonSecret(showGuid) then if showGuid and not startTimeCache[showGuid] then startTimeCache[showGuid] = GetTime() end end @@ -1387,7 +1427,7 @@ local function StatusText_ShowTimer(self) self.parent.states.guid = UnitGUID(self.parent.states.unit) end local tickGuid = self.parent.states.guid - if tickGuid and not (issecretvalue and issecretvalue(tickGuid)) and startTimeCache[tickGuid] then + if tickGuid and F.IsValueNonSecret(tickGuid) and startTimeCache[tickGuid] then self.timer:SetFormattedText(F.FormatTime(GetTime() - startTimeCache[tickGuid])) else self.timer:SetText("") @@ -1402,7 +1442,7 @@ local function StatusText_HideTimer(self, reset) if self.ticker then self.ticker:Cancel() end -- Midnight 12.0.0+: guid may be secret for NPC/boss units local guid = self.parent.states.guid - if guid and not (issecretvalue and issecretvalue(guid)) then + if guid and F.IsValueNonSecret(guid) then startTimeCache[guid] = nil end end @@ -1453,42 +1493,33 @@ local formatter = { end, -- health - ["health"] = function(pattern, hideIfEmptyOrFull, health, maxHealth, absorbs, healAbsorbs) - if hideIfEmptyOrFull and (health == 0 or health == maxHealth) then return "" end + ["health"] = function(pattern, _, health, maxHealth, absorbs, healAbsorbs) return pattern:format(health) end, - ["health_short"] = function(pattern, hideIfEmptyOrFull, health, maxHealth, absorbs, healAbsorbs) - if hideIfEmptyOrFull and (health == 0 or health == maxHealth) then return "" end + ["health_short"] = function(pattern, _, health, maxHealth, absorbs, healAbsorbs) return pattern:format(F.FormatNumber(health)) end, - ["health_percent"] = function(pattern, hideIfEmptyOrFull, health, maxHealth, absorbs, healAbsorbs) - if hideIfEmptyOrFull and (health == 0 or health == maxHealth) then return "" end + ["health_percent"] = function(pattern, _, health, maxHealth, absorbs, healAbsorbs) return pattern:format(F.Round(health / maxHealth * 100)) end, - ["deficit"] = function(pattern, hideIfEmptyOrFull, health, maxHealth, absorbs, healAbsorbs) - if hideIfEmptyOrFull and (health == 0 or health == maxHealth) then return "" end + ["deficit"] = function(pattern, _, health, maxHealth, absorbs, healAbsorbs) return pattern:format(health - maxHealth) end, - ["deficit_short"] = function(pattern, hideIfEmptyOrFull, health, maxHealth, absorbs, healAbsorbs) - if hideIfEmptyOrFull and (health == 0 or health == maxHealth) then return "" end + ["deficit_short"] = function(pattern, _, health, maxHealth, absorbs, healAbsorbs) return pattern:format(F.FormatNumber(health - maxHealth)) end, - ["deficit_percent"] = function(pattern, hideIfEmptyOrFull, health, maxHealth, absorbs, healAbsorbs) - if hideIfEmptyOrFull and (health == 0 or health == maxHealth) then return "" end + ["deficit_percent"] = function(pattern, _, health, maxHealth, absorbs, healAbsorbs) return pattern:format(F.Round((health - maxHealth) / maxHealth * 100)) end, -- effective health - ["effective"] = function(pattern, hideIfEmptyOrFull, health, maxHealth, absorbs, healAbsorbs) - if hideIfEmptyOrFull and (health == 0 or health == maxHealth) and absorbs == 0 and healAbsorbs == 0 then return "" end + ["effective"] = function(pattern, _, health, maxHealth, absorbs, healAbsorbs) return pattern:format(health + absorbs - healAbsorbs) end, - ["effective_short"] = function(pattern, hideIfEmptyOrFull, health, maxHealth, absorbs, healAbsorbs) - if hideIfEmptyOrFull and (health == 0 or health == maxHealth) and absorbs == 0 and healAbsorbs == 0 then return "" end + ["effective_short"] = function(pattern, _, health, maxHealth, absorbs, healAbsorbs) return pattern:format(F.FormatNumber(health + absorbs - healAbsorbs)) end, - ["effective_percent"] = function(pattern, hideIfEmptyOrFull, health, maxHealth, absorbs, healAbsorbs) - if hideIfEmptyOrFull and (health == 0 or health == maxHealth) and absorbs == 0 and healAbsorbs == 0 then return "" end + ["effective_percent"] = function(pattern, _, health, maxHealth, absorbs, healAbsorbs) return pattern:format(F.Round((health + absorbs - healAbsorbs) / maxHealth * 100)) end, @@ -1542,6 +1573,67 @@ local function BuildPattern(config) end end +-- Build a C-level format segment for secret values. Returns the format string +-- segment (with %d placeholder) and a key identifying which argument to use: +-- "health" = raw health, "pct" = health percent (0-100), "shields" = absorbs, +-- "healAbsorbs" = heal absorbs, nil = skip (format is "none") +local function BuildSecretSegment(config) + if config.format == "none" then + return nil, nil + end + + local prefix + if config.delimiter == nil then + prefix = "" + else + prefix = "|cffababab" .. config.delimiter .. "|r" + end + + local isPercent = config.format:find("percent$") and true or false + + local colorStart, colorEnd + if config.color[1] == "class_color" then + colorStart, colorEnd = "", "" + else + colorStart = "|cff" .. F.ConvertRGBToHEX(F.ConvertRGB_256(unpack(config.color[2]))) + colorEnd = "|r" + end + + -- Determine which argument to pass for this segment + local fmt = config.format:gsub("_no_sign$", "") + local argKey + local isAbsorb = false + if fmt == "shields" or fmt == "shields_short" or fmt == "shields_percent" then + argKey = "shields" + isAbsorb = true + elseif fmt == "healabsorbs" or fmt == "healabsorbs_short" or fmt == "healabsorbs_percent" then + argKey = "healAbsorbs" + isAbsorb = true + elseif isPercent then + argKey = "pct" + else + argKey = "health" + end + + -- For absorb percentages, we can't compute absorbs/maxHealth on secrets, + -- so fall back to abbreviated raw number (no %% suffix). + -- Health percent uses UnitHealthPercent (C-level) so it gets a real 0-100 value. + local isAbsorbPercent = isAbsorb and isPercent + local isNoSign = config.format:find("_no_sign$") and true or false + local suffix = (isPercent and not isAbsorbPercent and not isNoSign) and "%%" or "" + + -- Use abbreviated display for _short formats and absorb percent fallback + local useAbbrev = (config.format:find("short") or isAbsorbPercent) and not (isPercent and not isAbsorbPercent) + local specifier = (useAbbrev and AbbreviateNumbers) and "%s" or "%d" + local segment = prefix .. colorStart .. specifier .. suffix .. colorEnd + + if useAbbrev and AbbreviateNumbers then + argKey = argKey .. "_abbr" + end + + return segment, argKey +end + local function HealthText_SetFormat(self, format) self.GetHealth1 = formatter[format.health1.format:gsub("_no_sign$", "")] self.GetHealth2 = formatter[format.health2.format:gsub("_no_sign$", "")] @@ -1549,30 +1641,106 @@ local function HealthText_SetFormat(self, format) self.GetHealAbsorbs = formatter[format.healAbsorbs.format:gsub("_no_sign$", "")] self.health1 = BuildPattern(format.health1) - self.health1_hideIfEmptyOrFull = format.health1.hideIfEmptyOrFull self.health2 = BuildPattern(format.health2) - self.health2_hideIfEmptyOrFull = format.health2.hideIfEmptyOrFull self.shields = BuildPattern(format.shields) self.healAbsorbs = BuildPattern(format.healAbsorbs) -end -local function HealthText_SetValue(self, health, maxHealth, shields, healAbsorbs) - -- On Midnight 12.0.0+, health/maxHealth may be secret values in restricted contexts. - -- Arithmetic (/, *, -, comparison) on secret values causes errors. - -- AbbreviateNumbers() and FontString:SetText() accept secret values safely. - -- DO NOT divide, multiply, or compare secret values. - if Cell.isMidnight and F.IsAuraRestricted and F.IsAuraRestricted() then - local healthStr = AbbreviateNumbers and AbbreviateNumbers(health) or tostring(health) - self.text:SetText(healthStr) - self:SetWidth(self.text:GetStringWidth()) + -- Pre-build C-level format segments for use when values are secret. + -- Each active component gets a segment with a %d/%s placeholder. + -- Stored individually so the render path can skip zero-value absorb components. + local segments = {} + local argKeys = {} + for _, cfg in ipairs({format.health1, format.health2, format.shields, format.healAbsorbs}) do + local seg, key = BuildSecretSegment(cfg) + if seg then + segments[#segments + 1] = seg + argKeys[#argKeys + 1] = key + end + end + if #segments > 0 then + self._secretSegments = segments + self._secretArgKeys = argKeys + else + self._secretSegments = nil + self._secretArgKeys = nil + end + +end + +local function HealthText_SetValue(self, health, maxHealth, shields, healAbsorbs, unit) + -- 12.0+: UnitHealth/UnitHealthMax/absorbs may return secret values in combat. + -- C-level SetFormattedText/SetText (AllowedWhenTainted) handles secrets for display. + -- The caller in UnitButton passes secrets to C-level directly; this guard is a + -- safety net for any remaining Lua arithmetic in the formatters below. + if not F.IsValueNonSecret(health) or not F.IsValueNonSecret(maxHealth) + or not F.IsValueNonSecret(shields) or not F.IsValueNonSecret(healAbsorbs) then + -- Use pre-built secret segments (individual per component). + -- Absorb components are skipped when their value is known to be 0. + local segments = self._secretSegments + local argKeys = self._secretArgKeys + if segments and argKeys then + local fmtParts = {} + local argValues = {} + -- Get health percent via C-level API for pct segments + local healthPct + for i, key in ipairs(argKeys) do + local raw + local isAbsorbKey = (key == "shields" or key == "shields_abbr" + or key == "healAbsorbs" or key == "healAbsorbs_abbr") + if key == "pct" then + -- UnitHealthPercent is C-level and accepts secret values + if not healthPct and unit and UnitHealthPercent then + if CurveConstants and CurveConstants.ScaleTo100 then + healthPct = UnitHealthPercent(unit, true, CurveConstants.ScaleTo100) + else + healthPct = UnitHealthPercent(unit) + end + end + raw = healthPct or health + elseif key == "health" or key == "health_abbr" then + raw = health + elseif key == "shields" or key == "shields_abbr" then + raw = (shields == nil) and 0 or shields + elseif key == "healAbsorbs" or key == "healAbsorbs_abbr" then + raw = (healAbsorbs == nil) and 0 or healAbsorbs + else + raw = 0 + end + -- Skip absorb components that are known to be 0 (non-secret) + if isAbsorbKey and F.IsValueNonSecret(raw) and raw == 0 then + -- skip this component + else + fmtParts[#fmtParts + 1] = segments[i] + argValues[#argValues + 1] = key:sub(-5) == "_abbr" and AbbreviateNumbers(raw) or raw + end + end + local fmt = table.concat(fmtParts) + local n = #argValues + if n == 0 then + self.text:SetText("") + elseif n == 1 then + self.text:SetFormattedText(fmt, argValues[1]) + elseif n == 2 then + self.text:SetFormattedText(fmt, argValues[1], argValues[2]) + elseif n == 3 then + self.text:SetFormattedText(fmt, argValues[1], argValues[2], argValues[3]) + elseif n == 4 then + self.text:SetFormattedText(fmt, argValues[1], argValues[2], argValues[3], argValues[4]) + end + else + self.text:SetFormattedText("%d", health) + end + -- GetStringWidth returns secret when text is tainted; use fallback width + -- so the frame isn't zero-width (which would clip the text entirely). + local sw = self.text:GetStringWidth() + self:SetWidth(F.IsValueNonSecret(sw) and sw or 50) return end - maxHealth = maxHealth == 0 and 1 or maxHealth self.text:SetFormattedText("%s%s%s%s", - self.GetHealth1(self.health1, self.health1_hideIfEmptyOrFull, health, maxHealth, shields, healAbsorbs), - self.GetHealth2(self.health2, self.health2_hideIfEmptyOrFull, health, maxHealth, shields, healAbsorbs), + self.GetHealth1(self.health1, false, health, maxHealth, shields, healAbsorbs), + self.GetHealth2(self.health2, false, health, maxHealth, shields, healAbsorbs), self.GetShields(self.shields, health, maxHealth, shields, healAbsorbs), self.GetHealAbsorbs(self.healAbsorbs, health, maxHealth, shields, healAbsorbs)) self:SetWidth(self.text:GetStringWidth()) @@ -1600,7 +1768,9 @@ local function HealthText_SetFont(self, font, size, outline, shadow) self.text:SetShadowColor(0, 0, 0, 0) end - self:SetSize(self.text:GetStringWidth(), size) + -- 12.0+: GetStringWidth returns secret when text is tainted; use fallback + local w = self.text:GetStringWidth() + self:SetSize((F.IsValueNonSecret(w) and w > 0) and w or 50, size) end local function HealthText_SetPoint(self, point, relativeTo, relativePoint, x, y) @@ -1654,33 +1824,51 @@ end -- power text ------------------------------------------------- local function SetPower_Percentage(self, current, max) - if self.hideIfEmptyOrFull and (current == 0 or current == max) then - self:Hide() - else - self.text:SetFormattedText("%d%%", current/max*100) - self:SetWidth(self.text:GetStringWidth()) + -- 12.0+: UnitPower() may return secret values in combat. + -- SetFormattedText is C-level (AllowedWhenTainted) and handles secrets. + if not F.IsValueNonSecret(current) or not F.IsValueNonSecret(max) then + self.text:SetFormattedText("%d%%", current) + local w = self.text:GetStringWidth() + self:SetWidth((F.IsValueNonSecret(w) and w > 0) and w or 50) self:Show() + return end + self.text:SetFormattedText("%d%%", current/max*100) + local w = self.text:GetStringWidth() + self:SetWidth(F.IsValueNonSecret(w) and w or 50) + self:Show() end local function SetPower_Number(self, current, max) - if self.hideIfEmptyOrFull and (current == 0 or current == max) then - self:Hide() - else + if not F.IsValueNonSecret(current) or not F.IsValueNonSecret(max) then self.text:SetText(current) - self:SetWidth(self.text:GetStringWidth()) + local w = self.text:GetStringWidth() + self:SetWidth((F.IsValueNonSecret(w) and w > 0) and w or 50) self:Show() + return end + self.text:SetText(current) + local w = self.text:GetStringWidth() + self:SetWidth(F.IsValueNonSecret(w) and w or 50) + self:Show() end local function SetPower_Number_Short(self, current, max) - if self.hideIfEmptyOrFull and (current == 0 or current == max) then - self:Hide() - else - self.text:SetText(F.FormatNumber(current)) - self:SetWidth(self.text:GetStringWidth()) + if not F.IsValueNonSecret(current) or not F.IsValueNonSecret(max) then + if AbbreviateNumbers then + self.text:SetFormattedText("%s", AbbreviateNumbers(current)) + else + self.text:SetText(current) + end + local w = self.text:GetStringWidth() + self:SetWidth((F.IsValueNonSecret(w) and w > 0) and w or 50) self:Show() + return end + self.text:SetText(F.FormatNumber(current)) + local w = self.text:GetStringWidth() + self:SetWidth(F.IsValueNonSecret(w) and w or 50) + self:Show() end local function PowerText_SetFont(self, font, size, outline, shadow) @@ -1705,7 +1893,9 @@ local function PowerText_SetFont(self, font, size, outline, shadow) self.text:SetShadowColor(0, 0, 0, 0) end - self:SetSize(self.text:GetStringWidth(), size) + -- 12.0+: GetStringWidth returns secret when text is tainted; use fallback + local w = self.text:GetStringWidth() + self:SetSize((F.IsValueNonSecret(w) and w > 0) and w or 50, size) end local function PowerText_SetPoint(self, point, relativeTo, relativePoint, x, y) @@ -1721,6 +1911,7 @@ local function PowerText_SetPoint(self, point, relativeTo, relativePoint, x, y) end local function PowerText_SetFormat(self, format) + self._format = format -- store for secret value fallback path if format == "percentage" then self.SetValue = SetPower_Percentage elseif format == "number" then @@ -1734,9 +1925,7 @@ local function PowerText_SetColor(self, r, g, b) self.text:SetTextColor(r, g, b) end -local function PowerText_SetHideIfEmptyOrFull(self, hideIfEmptyOrFull) - self.hideIfEmptyOrFull = hideIfEmptyOrFull -end +-- hideIfEmptyOrFull removed: caused regressions with secret values in 12.0 local function PowerText_UpdatePreviewColor(self, color) local r, g, b @@ -1763,7 +1952,7 @@ function I.CreatePowerText(parent) powerText.SetPoint = PowerText_SetPoint powerText.SetFormat = PowerText_SetFormat powerText.SetColor = PowerText_SetColor - powerText.SetHideIfEmptyOrFull = PowerText_SetHideIfEmptyOrFull + powerText.SetHideIfEmptyOrFull = function() end -- no-op, feature removed powerText.UpdatePreviewColor = PowerText_UpdatePreviewColor powerText.SetValue = noop end @@ -2075,6 +2264,12 @@ local function ShieldBar_SetHorizontalValue(bar, percent) barWidth = maxWidth * percent end bar:SetWidth(max(barWidth, 3)) + -- Restore border (may have been hidden by SetAbsorbs secret path) + bar:SetBackdropBorderColor(0, 0, 0, 1) + -- StatusBar must be "full" so the texture fills the frame; + -- the frame width controls the visible portion. + bar:SetMinMaxValues(0, 1) + bar:SetValue(1) end local function ShieldBar_SetVerticalValue(bar, percent) @@ -2086,46 +2281,57 @@ local function ShieldBar_SetVerticalValue(bar, percent) barHeight = maxHeight * percent end bar:SetHeight(max(barHeight, 3)) + -- Restore border (may have been hidden by SetAbsorbs secret path) + bar:SetBackdropBorderColor(0, 0, 0, 1) + bar:SetMinMaxValues(0, 1) + bar:SetValue(1) +end + +-- Secret-safe: set width to full health bar, use StatusBar proportional +-- fill to show shield amount. Keeps original height/position (thin bar at +-- bottom like power bar). Border hidden so only the filled portion is visible. +local function ShieldBar_SetAbsorbs(bar, absorbs, healthMax) + bar:SetBackdropBorderColor(0, 0, 0, 0) + local parent = bar.parentHealthBar + if parent then + bar:SetWidth(parent:GetWidth()) + end + bar:SetMinMaxValues(0, healthMax) + bar:SetValue(absorbs) end local function ShieldBar_SetPoint(bar, point, anchorTo, anchorPoint, x, y) - -- if point == "HEALTH_BAR_HORIZONTAL" then - -- bar:_SetPoint("TOPLEFT", b.widgets.healthBar) - -- bar:_SetPoint("BOTTOMLEFT", b.widgets.healthBar) - -- bar.SetValue = ShieldBar_SetHorizontalValue - -- elseif point == "HEALTH_BAR_VERTICAL" then - -- bar:_SetPoint("TOPLEFT", b.widgets.healthBar) - -- bar:_SetPoint("BOTTOMLEFT", b.widgets.healthBar) - -- bar.SetValue = ShieldBar_SetVerticalValue if point == "HEALTH_BAR" then bar:_SetPoint("TOPLEFT", bar.parentHealthBar, P.Scale(-1), P.Scale(1)) bar:_SetPoint("BOTTOMLEFT", bar.parentHealthBar, P.Scale(-1), P.Scale(-1)) - bar.SetValue = ShieldBar_SetHorizontalValue + bar.SetPercent = ShieldBar_SetHorizontalValue else bar:_SetPoint(point, anchorTo, anchorPoint, x, y) - bar.SetValue = ShieldBar_SetHorizontalValue + bar.SetPercent = ShieldBar_SetHorizontalValue end end function I.CreateShieldBar(parent) - local shieldBar = CreateFrame("Frame", parent:GetName().."ShieldBar", parent.widgets.indicatorFrame, "BackdropTemplate") + local shieldBar = CreateFrame("StatusBar", parent:GetName().."ShieldBar", parent.widgets.indicatorFrame, "BackdropTemplate") parent.indicators.shieldBar = shieldBar - -- shieldBar:SetSize(4, 4) shieldBar:Hide() shieldBar:SetBackdrop({edgeFile=Cell.vars.whiteTexture, edgeSize=P.Scale(1)}) shieldBar:SetBackdropBorderColor(0, 0, 0, 1) - - local tex = shieldBar:CreateTexture(nil, "BORDER", nil, -7) - tex:SetAllPoints() + shieldBar:SetStatusBarTexture(Cell.vars.whiteTexture) + shieldBar:SetMinMaxValues(0, 1) + shieldBar:SetValue(0) shieldBar._SetPoint = shieldBar.SetPoint shieldBar.SetPoint = ShieldBar_SetPoint - shieldBar.SetValue = ShieldBar_SetHorizontalValue + -- Percentage-based SetValue for normal (non-secret) path + shieldBar.SetPercent = ShieldBar_SetHorizontalValue + -- Secret-safe SetAbsorbs for combat path + shieldBar.SetAbsorbs = ShieldBar_SetAbsorbs shieldBar.parentHealthBar = parent.widgets.healthBar function shieldBar:SetColor(r, g, b, a) - tex:SetColorTexture(r, g, b, a) + shieldBar:SetStatusBarColor(r, g, b, a) end function shieldBar:UpdatePixelPerfect() From 577bac7e9a25fd5d9a0e9c12a0da0ced973cf8de Mon Sep 17 00:00:00 2001 From: jdtoppin <6392799+jdtoppin@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:19:30 -0600 Subject: [PATCH 03/61] =?UTF-8?q?PR=203:=20Raid=20debuff=20detection=20?= =?UTF-8?q?=E2=80=94=20tiered=20fallback,=20Midnight=20dungeon=20data=20Ti?= =?UTF-8?q?ered=20fallback=20for=20secret=20auras:=20-=20Tier=201:=20Non-s?= =?UTF-8?q?ecret=20(whitelisted)=20auras=20use=20standard=20spellId/name?= =?UTF-8?q?=20lookup=20-=20Tier=202:=20Secret=20auras=20use=20IsAuraFilter?= =?UTF-8?q?edOutByInstanceID=20with=20HARMFUL|RAID=20-=20Tier=203:=20Encou?= =?UTF-8?q?nter=20fallback=20for=20unidentified=20secret=20debuffs=20durin?= =?UTF-8?q?g=20bosses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RaidDebuffs module: FillMissingInstances for EJ gaps, dual name+id indexing. RaidDebuffs_Midnight.lua: Complete Midnight expansion dungeon/raid debuff data (The Voidspire, March on Quel'Danas, 4 dungeons). Co-Authored-By: Claude Opus 4.6 (1M context) --- Modules/RaidDebuffs/RaidDebuffs.lua | 42 +- RaidDebuffs/RaidDebuffs_Midnight.lua | 608 ++++++++++++++++----------- 2 files changed, 412 insertions(+), 238 deletions(-) diff --git a/Modules/RaidDebuffs/RaidDebuffs.lua b/Modules/RaidDebuffs/RaidDebuffs.lua index b3c8dc27..bc7ee61d 100644 --- a/Modules/RaidDebuffs/RaidDebuffs.lua +++ b/Modules/RaidDebuffs/RaidDebuffs.lua @@ -316,8 +316,33 @@ local function LoadDebuffs() -- texplore(loadedDebuffs[477]) -- 悬槌堡 end +-- Fill in instances from unsortedDebuffs that weren't found by the EJ API. +-- This can happen when new raids/dungeons aren't yet classified in the EJ. +local function FillMissingInstances() + if not unsortedDebuffs then return end + + local latestTierName = tierNames[#tierNames] + if not latestTierName or not encounterJournalList[latestTierName] then return end + + for instanceId in pairs(unsortedDebuffs) do + if not instanceIdToName[instanceId] then + -- This instance wasn't loaded by EJ_GetInstanceByIndex; try direct lookup + local name, _, _, image = EJ_GetInstanceInfo(instanceId) + if name then + local instanceTable = {["name"]=name, ["id"]=instanceId, ["image"]=image, ["bosses"]={}} + LoadBossList(instanceId, instanceTable["bosses"]) + tinsert(encounterJournalList[latestTierName], instanceTable) + local iIndex = #encounterJournalList[latestTierName] + instanceNameMapping[name] = latestTierName..":"..iIndex..":"..instanceId + instanceIdToName[instanceId] = name + end + end + end +end + local function UpdateRaidDebuffs() LoadList() + FillMissingInstances() -- LoadDungeonsForCurrentSeason() LoadDebuffs() end @@ -2239,7 +2264,7 @@ function F.GetDebuffList(instanceName) local spellName = F.GetSpellInfo(t["id"]) if spellName then -- list[spellName/spellId] = {order, glowType, glowOptions} - list[t["trackByID"] and t["id"] or spellName] = { + local entry = { ["order"] = t["order"], ["condition"] = t["condition"], ["glowType"] = t["glowType"], @@ -2247,6 +2272,12 @@ function F.GetDebuffList(instanceName) ["glowCondition"] = t["glowCondition"], ["useElapsedTime"] = t["useElapsedTime"], } + list[t["trackByID"] and t["id"] or spellName] = entry + -- 12.0+: also index by name for trackByID entries so + -- GetDebuffOrder can match by resolved name when spellId is secret + if t["trackByID"] and spellName and not list[spellName] then + list[spellName] = entry + end end end end @@ -2256,7 +2287,7 @@ function F.GetDebuffList(instanceName) for _, t in pairs(bTable["enabled"]) do local spellName = F.GetSpellInfo(t["id"]) if spellName then -- check again - list[t["trackByID"] and t["id"] or spellName] = { + local entry = { ["order"] = t["order"]+n, ["condition"] = t["condition"], ["glowType"] = t["glowType"], @@ -2264,6 +2295,11 @@ function F.GetDebuffList(instanceName) ["glowCondition"] = t["glowCondition"], ["useElapsedTime"] = t["useElapsedTime"], } + list[t["trackByID"] and t["id"] or spellName] = entry + -- 12.0+: also index by name for trackByID entries + if t["trackByID"] and spellName and not list[spellName] then + list[spellName] = entry + end end end end @@ -2476,4 +2512,4 @@ local function UpdateIndicators(layout, indicatorName, setting, value) end end end -Cell.RegisterCallback("UpdateIndicators", "RaidDebuffsTab_UpdateIndicators", UpdateIndicators) \ No newline at end of file +Cell.RegisterCallback("UpdateIndicators", "RaidDebuffsTab_UpdateIndicators", UpdateIndicators) diff --git a/RaidDebuffs/RaidDebuffs_Midnight.lua b/RaidDebuffs/RaidDebuffs_Midnight.lua index ae33bde5..1863ecb4 100644 --- a/RaidDebuffs/RaidDebuffs_Midnight.lua +++ b/RaidDebuffs/RaidDebuffs_Midnight.lua @@ -12,146 +12,10 @@ local Cell = select(2, ...) local F = Cell.funcs local debuffs = { - [1299] = { -- Windrunner Spire - ["general"] = { - }, - [2655] = { -- Emberdawn - 465904, -- Burning Gale - 466556, -- Flaming Updraft - 466064, -- Searing Beak - 469633, -- Flaming Twisters - 467120, -- Ignited Embers - 1217762, -- Fire Breath - }, - [2656] = { -- Derelict Duo - 472736, -- Debilitating Shriek - 474105, -- Curse of Darkness - 472724, -- Shadow Bolt - 472795, -- Heaving Yank - 474075, -- Heaving Chop - 472745, -- Splattering Spew - 472777, -- Gunk Splatter - 472888, -- Bone Hack - 1219551, -- Broken Bond - 1282272, -- Splattered - 1215813, -- Shadowy - }, - [2657] = { -- Commander Kroluk - 470963, -- Bladestorm - 468070, -- Rallying Bellow - 467620, -- Rampage - 1217094, -- Throw Axe - 472043, -- Rallying Bellow - 472081, -- Reckless Leap - 1250851, -- Shield Wall - 1253026, -- Intimidating Shout - 1251981, -- Chain Lightning - 467815, -- Intercepting Charge - 1270620, -- Flame Nova - 1283357, -- Falling Rubble - }, - [2658] = { -- The Restless Heart - 1253986, -- Gust Shot - 468429, -- Bullseye Windblast - 468442, -- Billowing Wind - 472556, -- Arrow Rain - 1253977, -- Turbulent Arrows - 474528, -- Bolt Gale - 472662, -- Tempest Slash - 1216042, -- Squall Leap - 1282932, -- Storming Soulfont - }, - }, - - [1300] = { -- Magisters' Terrace - ["general"] = { - }, - [2659] = { -- Arcanotron Custos - 474345, -- Refueling Protocol - 474308, -- Energy Orb - 474496, -- Repulsing Slam - 1214038, -- Ethereal Shackles - 1243905, -- Unstable Energy - 1214081, -- Arcane Expulsion - 474407, -- Arcane Empowerment - 1214089, -- Arcane Residue - }, - [2661] = { -- Seranel Sunlash - 1224903, -- Suppression Zone - 1225135, -- Feedback - 1225193, -- Wave of Silence - 1225792, -- Runic Mark - 1246446, -- Null Reaction - 1248689, -- Hastening Ward - }, - [2660] = { -- Gemellus - 1223847, -- Triplicate - 1223936, -- Synaptic Nexus - 1224299, -- Astral Grasp - 1224401, -- Cosmic Radiation - 1224100, -- Void Secretions - 1284958, -- Cosmic Sting - 1253707, -- Neural Link - }, - [2662] = { -- Degentrius - 1215087, -- Unstable Void Essence - 1215161, -- Void Destruction - 1214714, -- Void Torrent - 1280113, -- Hulking Fragment - 1215897, -- Devouring Entropy - 1271066, -- Entropy Blast - 1269631, -- Entropy Orb - 1284627, -- Umbral Splinters - 1284628, -- Stygian Ichor - }, - }, - - [1304] = { -- Murder Row - ["general"] = { - }, - [2679] = { -- Kystia Manaheart - 1230289, -- Illicit Infusion - 1217989, -- Felshield - 1223906, -- Fel Nova - 1230298, -- Chaos Barrage - 1253811, -- Fel Spray - 1228198, -- Corroding Spittle - 1264095, -- Mirror Images - 1264106, -- Felstorm - 1230304, -- Light Infusion - 1265412, -- Destabilized - }, - [2680] = { -- Zaen Bladesorrow - 474478, -- Killing Spree - 1218347, -- Murder in a Row - 474765, -- Same-Day Delivery - 1201553, -- Fel-Infused Freight - 1214357, -- Fire Bomb - 1222795, -- Envenom - 474515, -- Heartstop Poison - 1266241, -- Freight Explosion - }, - [2681] = { -- Xathuux the Annihilator - 1214663, -- Axe Toss - 474197, -- Demonic Rage - 474234, -- Burning Steps - 473898, -- Legion Strike - 1214650, -- Fel Lightning - }, - [2682] = { -- Lithiel Cinderfury - 1223204, -- Felfire Burst - 474375, -- Chaos Bolt - 1214675, -- Demonic Gateway - 474457, -- Fingers of Gul'dan - 1217384, -- Malefic Wave - 1217415, -- Felshield - 1226469, -- Malefic Empowerment - 1231262, -- Felfire Core - 1216945, -- Searing Fel Flame - }, - }, - - [1307] = { -- The Voidspire + -- ==================================================================== + -- The Voidspire (Raid - 6 bosses) + -- ==================================================================== + [1307] = { ["general"] = { }, [2733] = { -- Imperator Averzian @@ -192,24 +56,6 @@ local debuffs = { 1272527, -- Creep Spit 1280101, -- Dark Energy }, - [2736] = { -- Fallen-King Salhadaar - 1246175, -- Entropic Unraveling - 1250686, -- Twisting Obscurity - 1254081, -- Fractured Projection - 1247738, -- Void Convergence - 1254088, -- Shadow Fracture - 1271577, -- Destabilizing Strikes - 1260015, -- Umbral Beams - 1245960, -- Void Infusion - 1250991, -- Dark Radiation - 1253032, -- Shattering Twilight - 1251213, -- Twilight Spikes - 1245592, -- Torturous Extract - 1248697, -- Despotic Command - 1248709, -- Oppressive Darkness - 1275056, -- Nexus Shield - 1250828, -- Void Exposure - }, [2735] = { -- Vaelgor & Ezzorak 1244221, -- Dread Breath 1262623, -- Nullbeam @@ -238,6 +84,24 @@ local debuffs = { 1270852, -- Diminish 1270513, -- Shadowmark }, + [2736] = { -- Fallen-King Salhadaar + 1246175, -- Entropic Unraveling + 1250686, -- Twisting Obscurity + 1254081, -- Fractured Projection + 1247738, -- Void Convergence + 1254088, -- Shadow Fracture + 1271577, -- Destabilizing Strikes + 1260015, -- Umbral Beams + 1245960, -- Void Infusion + 1250991, -- Dark Radiation + 1253032, -- Shattering Twilight + 1251213, -- Twilight Spikes + 1245592, -- Torturous Extract + 1248697, -- Despotic Command + 1248709, -- Oppressive Darkness + 1275056, -- Nexus Shield + 1250828, -- Void Exposure + }, [2737] = { -- Lightblinded Vanguard 1246162, -- Aura of Devotion 1251857, -- Judgment @@ -272,7 +136,7 @@ local debuffs = { 1280159, -- Execution Sentence 1249130, -- Elekk Charge }, - [2738] = { -- Crown of the Cosmos + [2738] = { -- Crown of the Cosmos (Xal'atath) 1239080, -- Aspect of the End 1232470, -- Grasp of Emptiness 1233865, -- Null Corona @@ -315,7 +179,10 @@ local debuffs = { }, }, - [1308] = { -- March on Quel'Danas + -- ==================================================================== + -- March on Quel'Danas (Raid - 2 bosses) + -- ==================================================================== + [1308] = { ["general"] = { }, [2739] = { -- Belo'ren, Child of Al'ar @@ -414,40 +281,260 @@ local debuffs = { }, }, - [1314] = { -- The Dreamrift + -- ==================================================================== + -- Windrunner Spire (Dungeon) + -- ==================================================================== + [1299] = { ["general"] = { }, - [2795] = { -- Chimaerus the Undreamt God - 1262289, -- Alndust Upheaval - 1245486, -- Corrupted Devastation - 1245698, -- Alnsight - 1245406, -- Ravenous Dive - 1245844, -- Cannibalized Essence - 1245919, -- Alndust Essence - 1246132, -- Rift Shroud - 1272726, -- Rending Tear - 1249017, -- Fearsome Cry - 1249207, -- Discordant Roar - 1250953, -- Rift Sickness - 1252863, -- Insatiable - 1246653, -- Caustic Phlegm - 1257087, -- Consuming Miasma - 1253744, -- Rift Vulnerability - 1257093, -- Lingering Miasma - 1258610, -- Rift Emergence - 1261997, -- Essence Bolt - 1262020, -- Colossal Strikes - 1245727, -- Alnshroud - 1246621, -- Caustic Phlegm - 1257085, -- Consuming Miasma - 1267201, -- Dissonance - 1264756, -- Rift Madness - 1245396, -- Consume - 1282001, -- Alndust Upheaval + [2655] = { -- Emberdawn + 465904, -- Burning Gale + 466556, -- Flaming Updraft + 466064, -- Searing Beak + 469633, -- Flaming Twisters + 467120, -- Ignited Embers + 1217762, -- Fire Breath + }, + [2656] = { -- Derelict Duo + 472736, -- Debilitating Shriek + 474105, -- Curse of Darkness + 472724, -- Shadow Bolt + 472795, -- Heaving Yank + 474075, -- Heaving Chop + 472745, -- Splattering Spew + 472777, -- Gunk Splatter + 472888, -- Bone Hack + 1219551, -- Broken Bond + 1282272, -- Splattered + 1215813, -- Shadowy + }, + [2657] = { -- Commander Kroluk + 470963, -- Bladestorm + 468070, -- Rallying Bellow + 467620, -- Rampage + 1217094, -- Throw Axe + 472043, -- Rallying Bellow + 472081, -- Reckless Leap + 1250851, -- Shield Wall + 1253026, -- Intimidating Shout + 1251981, -- Chain Lightning + 467815, -- Intercepting Charge + 1270620, -- Flame Nova + 1283357, -- Falling Rubble + }, + [2658] = { -- The Restless Heart + 1253986, -- Gust Shot + 468429, -- Bullseye Windblast + 468442, -- Billowing Wind + 472556, -- Arrow Rain + 1253977, -- Turbulent Arrows + 474528, -- Bolt Gale + 472662, -- Tempest Slash + 1216042, -- Squall Leap + 1282932, -- Storming Soulfont + }, + }, + + -- ==================================================================== + -- Magisters' Terrace (Dungeon) + -- ==================================================================== + [1300] = { + ["general"] = { + }, + [2659] = { -- Arcanotron Custos + 474345, -- Refueling Protocol + 474308, -- Energy Orb + 474496, -- Repulsing Slam + 1214038, -- Ethereal Shackles + 1243905, -- Unstable Energy + 1214081, -- Arcane Expulsion + 474407, -- Arcane Empowerment + 1214089, -- Arcane Residue + }, + [2661] = { -- Seranel Sunlash + 1224903, -- Suppression Zone + 1225135, -- Feedback + 1225193, -- Wave of Silence + 1225792, -- Runic Mark + 1246446, -- Null Reaction + 1248689, -- Hastening Ward + }, + [2660] = { -- Gemellus + 1223847, -- Triplicate + 1223936, -- Synaptic Nexus + 1224299, -- Astral Grasp + 1224401, -- Cosmic Radiation + 1224100, -- Void Secretions + 1284958, -- Cosmic Sting + 1253707, -- Neural Link + }, + [2662] = { -- Degentrius + 1215087, -- Unstable Void Essence + 1215161, -- Void Destruction + 1214714, -- Void Torrent + 1280113, -- Hulking Fragment + 1215897, -- Devouring Entropy + 1271066, -- Entropy Blast + 1269631, -- Entropy Orb + 1284627, -- Umbral Splinters + 1284628, -- Stygian Ichor + }, + }, + + -- ==================================================================== + -- Blackrock Depths (Dungeon) + -- ==================================================================== + [1301] = { + ["general"] = { + }, + [2663] = { -- Lord Roccor + 462346, -- Living Magma + 463674, -- Crystallize + 462322, -- Eruption + 462320, -- Igneous Crystallization + 462351, -- Roiling Magma + }, + [2664] = { -- Bael'Gar + 462974, + 463890, + 463143, + 462972, + 462968, + }, + [2665] = { -- Lord Incendius + 463487, + 463503, + 463486, + 463471, + 463472, + 463495, + 463499, + }, + [2666] = { -- Golem Lord Argelmach + 463821, + 463829, + 464485, + 463852, + 463847, + 463823, + 463837, + 464489, + }, + [2667] = { -- The Seven + 464347, + 464348, + 464358, + 464359, + 464361, + 464331, + 464371, + 464333, + 464334, + 464353, + 464363, + 464362, + 464366, + 464367, + 464337, + 464340, + 464344, + }, + [2668] = { -- General Angerforge + 464425, + 466265, + 466273, + 467424, + 467423, + 466259, + 466107, + 464417, + 467464, + 466096, + 466086, + }, + [2669] = { -- Ambassador Flamelash + 464372, -- Burning Spirit + 464998, + 470244, + 464769, + 470203, + 470207, + 464379, + 464981, + 464382, + 464983, + 464377, + }, + [2670] = { -- Emperor Dagran Thaurissan + 465069, + 465077, + 465079, + 465268, + 465093, + 465060, + 465070, + 465225, + 465065, + 465086, + 466371, + 465091, + 466504, + 465099, }, }, - [1309] = { -- The Blinding Vale + -- ==================================================================== + -- Murder Row (Dungeon) + -- ==================================================================== + [1304] = { + ["general"] = { + }, + [2679] = { -- Kystia Manaheart + 1230289, -- Illicit Infusion + 1217989, -- Felshield + 1223906, -- Fel Nova + 1230298, -- Chaos Barrage + 1253811, -- Fel Spray + 1228198, -- Corroding Spittle + 1264095, -- Mirror Images + 1264106, -- Felstorm + 1230304, -- Light Infusion + 1265412, -- Destabilized + }, + [2680] = { -- Zaen Bladesorrow + 474478, -- Killing Spree + 1218347, -- Murder in a Row + 474765, -- Same-Day Delivery + 1201553, -- Fel-Infused Freight + 1214357, -- Fire Bomb + 1222795, -- Envenom + 474515, -- Heartstop Poison + 1266241, -- Freight Explosion + }, + [2681] = { -- Xathuux the Annihilator + 1214663, -- Axe Toss + 474197, -- Demonic Rage + 474234, -- Burning Steps + 473898, -- Legion Strike + 1214650, -- Fel Lightning + }, + [2682] = { -- Lithiel Cinderfury + 1223204, -- Felfire Burst + 474375, -- Chaos Bolt + 1214675, -- Demonic Gateway + 474457, -- Fingers of Gul'dan + 1217384, -- Malefic Wave + 1217415, -- Felshield + 1226469, -- Malefic Empowerment + 1231262, -- Felfire Core + 1216945, -- Searing Fel Flame + }, + }, + + -- ==================================================================== + -- The Blinding Vale (Dungeon) + -- ==================================================================== + [1309] = { ["general"] = { }, [2769] = { -- Lightblossom Trinity @@ -501,7 +588,10 @@ local debuffs = { }, }, - [1311] = { -- Den of Nalorakk + -- ==================================================================== + -- Den of Nalorakk (Dungeon) + -- ==================================================================== + [1311] = { ["general"] = { }, [2776] = { -- The Hoardmonger @@ -532,51 +622,17 @@ local debuffs = { 1242860, -- Echoing Maul 1255385, -- Forceful Roar 1243585, -- Overwhelming Onslaught - 1262253, -- Demoralizing Scream - 1243063, -- Concussive Shock - 1255577, -- Spectral Slash - 1261776, -- Defensive Stance + 1262253, -- Brutal Slam + 1243063, -- Tempest of Fury + 1255577, -- Raging Tempest + 1261776, -- Echoing Tempest }, }, - [1312] = { -- Midnight - ["general"] = { - }, - [2827] = { -- Lu'ashal - 1276436, -- Dawncrazed Halo - 1276247, -- Dawnfire Breath - 1243963, -- Radiant Sunder - 1243988, -- Blinding Fissure - 1258427, -- Radiant Flare - 1258426, -- Radiant Ember - }, - [2829] = { -- Thorm'belan - 1257825, -- Scintillating Shard - 1257320, -- Radiant Mote - 1257737, -- Shard Eruption - 1258136, -- Rending Claw - 1257618, -- Dazzling Radiance - 1258639, -- Shredding Tendrils - }, - [2828] = { -- Predaxas - 1276193, -- Regurgitation - 1276320, -- Seismic Slam - 1276884, -- Voidscatter - 1277043, -- Bilepool - 1276988, -- Toxin Splatter - 1277711, -- Bestial Rage - 1277694, -- Blood Nova - 1277829, -- Devour - }, - [2782] = { -- Cragpine - 1235144, -- War Club - 1257906, -- Ancient Seeds - 1235131, -- Rootquake - 1235134, -- Erupting Roots - }, - }, - - [1313] = { -- Voidscar Arena + -- ==================================================================== + -- Voidscar Arena (Dungeon) + -- ==================================================================== + [1313] = { ["general"] = { }, [2791] = { -- Taz'Rah @@ -607,7 +663,46 @@ local debuffs = { }, }, - [1315] = { -- Maisara Caverns + -- ==================================================================== + -- The Dreamrift (Dungeon) + -- ==================================================================== + [1314] = { + ["general"] = { + }, + [2795] = { -- Chimaerus the Undreamt God + 1262289, -- Alndust Upheaval + 1245486, -- Corrupted Devastation + 1245698, -- Alnsight + 1245406, -- Ravenous Dive + 1245844, -- Cannibalized Essence + 1245919, -- Alndust Essence + 1246132, -- Rift Shroud + 1272726, -- Rending Tear + 1249017, -- Fearsome Cry + 1249207, -- Discordant Roar + 1250953, -- Rift Sickness + 1252863, -- Insatiable + 1246653, -- Caustic Phlegm + 1257087, -- Consuming Miasma + 1253744, -- Rift Vulnerability + 1257093, -- Lingering Miasma + 1258610, -- Rift Emergence + 1261997, -- Essence Bolt + 1262020, -- Colossal Strikes + 1245727, -- Alnshroud + 1246621, -- Caustic Phlegm + 1257085, -- Consuming Miasma + 1267201, -- Dissonance + 1264756, -- Rift Madness + 1245396, -- Consume + 1282001, -- Alndust Upheaval + }, + }, + + -- ==================================================================== + -- Maisara Caverns (Dungeon) + -- ==================================================================== + [1315] = { ["general"] = { }, [2810] = { -- Muro'jin and Nekraxx @@ -656,7 +751,10 @@ local debuffs = { }, }, - [1316] = { -- Nexus-Point Xenas + -- ==================================================================== + -- Nexus-Point Xenas (Dungeon) + -- ==================================================================== + [1316] = { ["general"] = { }, [2813] = { -- Chief Corewright Kasreth @@ -692,6 +790,46 @@ local debuffs = { }, }, + -- ==================================================================== + -- Midnight (Dungeon) + -- ==================================================================== + [1312] = { + ["general"] = { + }, + [2827] = { -- Lu'ashal + 1276436, -- Dawncrazed Halo + 1276247, -- Dawnfire Breath + 1243963, -- Radiant Sunder + 1243988, -- Blinding Fissure + 1258427, -- Radiant Flare + 1258426, -- Radiant Ember + }, + [2829] = { -- Thorm'belan + 1257825, -- Scintillating Shard + 1257320, -- Radiant Mote + 1257737, -- Shard Eruption + 1258136, -- Rending Claw + 1257618, -- Dazzling Radiance + 1258639, -- Shredding Tendrils + }, + [2828] = { -- Predaxas + 1276193, -- Regurgitation + 1276320, -- Seismic Slam + 1276884, -- Voidscatter + 1277043, -- Bilepool + 1276988, -- Toxin Splatter + 1277711, -- Bestial Rage + 1277694, -- Blood Nova + 1277829, -- Devour + }, + [2782] = { -- Cragpine + 1235144, -- War Club + 1257906, -- Ancient Seeds + 1235131, -- Rootquake + 1235134, -- Erupting Roots + }, + }, + } -F.LoadBuiltInDebuffs(debuffs) \ No newline at end of file +F.LoadBuiltInDebuffs(debuffs) From 35aaa05c7dad24ced991a11ea9234e3129e02dc5 Mon Sep 17 00:00:00 2001 From: jdtoppin <6392799+jdtoppin@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:19:43 -0600 Subject: [PATCH 04/61] =?UTF-8?q?PR=204:=20Targeted=20spells=20=E2=80=94?= =?UTF-8?q?=20secret=20value=20compatibility=20rewrite=20Complete=20rewrit?= =?UTF-8?q?e=20for=20Midnight=20secret=20value=20compatibility:=20-=20Remo?= =?UTF-8?q?ve=20pcall=20wrappers=20around=20UnitIsUnit,=20C=5FSpell.GetSpe?= =?UTF-8?q?llTexture,=20=20=20C=5FSpell.IsSpellImportant=20(C-level=20APIs?= =?UTF-8?q?=20accept=20secrets=20natively)=20-=20Secret-safe=20target=20re?= =?UTF-8?q?solution=20via=20SafeUnitIsUnit=20+=20server=20filter=20fallbac?= =?UTF-8?q?k=20-=20Display=20mode=20system=20(Icons/Border/Both)=20with=20?= =?UTF-8?q?settings=20UI=20-=20sourceUnit=20string=20as=20tracking=20key?= =?UTF-8?q?=20instead=20of=20UnitGUID=20(GUIDs=20can=20be=20secret)=20-=20?= =?UTF-8?q?Spell=20list=20and=20showAllSpells=20hidden=20on=20Midnight=20c?= =?UTF-8?q?lients?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- Defaults/Layout_Defaults.lua | 1 + Indicators/TargetedSpells.lua | 503 ++++++++++++++++++++++++---------- 2 files changed, 364 insertions(+), 140 deletions(-) diff --git a/Defaults/Layout_Defaults.lua b/Defaults/Layout_Defaults.lua index 1232e3da..df0f0825 100644 --- a/Defaults/Layout_Defaults.lua +++ b/Defaults/Layout_Defaults.lua @@ -479,6 +479,7 @@ Cell.defaults.layout = { ["type"] = "built-in", ["enabled"] = true, ["showAllSpells"] = false, + ["displayMode"] = "Both", ["position"] = {"TOPLEFT", "button", "TOPLEFT", -4, 4}, ["frameLevel"] = 50, ["size"] = {20, 20}, diff --git a/Indicators/TargetedSpells.lua b/Indicators/TargetedSpells.lua index e7cc75ae..6e4ebedd 100644 --- a/Indicators/TargetedSpells.lua +++ b/Indicators/TargetedSpells.lua @@ -13,82 +13,220 @@ local UnitIsUnit = UnitIsUnit local UnitIsEnemy = UnitIsEnemy local UnitCastingInfo = UnitCastingInfo local UnitChannelInfo = UnitChannelInfo +-- issecretvalue polyfill removed; use F.IsValueNonSecret() instead +local C_Spell = C_Spell local casts = {} local castsOnUnit, sortedCastsOnUnit = {}, {} local recheck = {} local maxIcons, showAllSpells +local displayMode = "Both" -- "Icons", "Border", "Both" +local useSecretPath = false -- set true when UnitIsUnit returns secrets local eventFrame = CreateFrame("Frame") +-- Secret-safe UnitIsUnit — returns false when result is secret +-- UnitIsUnit is C-level and accepts secrets; no pcall needed. +local function SafeUnitIsUnit(a, b) + local result = UnitIsUnit(a, b) + if not F.IsValueNonSecret(result) then return false end + return result +end + +-- Try to resolve target unit ID (non-secret path) +local function GetTargetUnitID_Safe(target, sourceUnit) + local resolved + + if SafeUnitIsUnit(target, "player") then return "player", false end + if SafeUnitIsUnit(target, "pet") then return "pet", false end + + for unit in F.IterateGroupMembers() do + if SafeUnitIsUnit(target, unit) then return unit, false end + end + + for unit in F.IterateGroupPets() do + if SafeUnitIsUnit(target, unit) then return unit, false end + end + + -- Check if UnitIsUnit is returning secrets (not just nil/no target) + if Cell.isMidnight and UnitExists(target) then + local result = UnitIsUnit(target, "player") + if not F.IsValueNonSecret(result) then + return nil, true -- target exists but results are secret + end + end + + return nil, false +end + +local allActiveCasts -- forward declaration for Reset() + local function Reset() wipe(recheck) wipe(casts) wipe(castsOnUnit) wipe(sortedCastsOnUnit) + if allActiveCasts then wipe(allActiveCasts) end end ------------------------------------------------- -- show / hide ------------------------------------------------- local function HideCasts(b) - b.indicators.targetedSpells:UpdateSize(0) - b.indicators.targetedSpells:HideGlow() + local ts = b.indicators.targetedSpells + if displayMode ~= "Border" then + ts:UpdateSize(0) + end + ts:HideGlow() + -- Reset glow frame alpha in case SetShown was used + if ts.tsGlowFrame then + ts.tsGlowFrame:SetAlpha(1) + ts.tsGlowFrame:Show() + end end local function ShowCasts(b, showGlow, sortedCasts, num) - num = min(maxIcons, num) - for i = 1, num do - local cast = sortedCasts[i] - b.indicators.targetedSpells[i].cooldown:SetReverse(not cast.isChanneling) - b.indicators.targetedSpells[i]:SetCooldown(cast.startTime, cast.endTime-cast.startTime, cast.icon, cast.count) + local ts = b.indicators.targetedSpells + + -- Show icons in Icons and Both modes + if displayMode ~= "Border" then + num = min(maxIcons, num) + for i = 1, num do + local cast = sortedCasts[i] + ts[i].cooldown:SetReverse(not cast.isChanneling) + ts[i]:SetCooldown(cast.startTime, cast.endTime-cast.startTime, cast.icon, cast.count) + end + ts:UpdateSize(num) end - b.indicators.targetedSpells:UpdateSize(num) - if showGlow then - b.indicators.targetedSpells:ShowGlow(unpack(Cell.vars.targetedSpellsGlow)) + -- Show glow in Border and Both modes only + if displayMode ~= "Icons" then + ts:ShowGlow(unpack(Cell.vars.targetedSpellsGlow)) else - b.indicators.targetedSpells:HideGlow() + ts:HideGlow() end end ------------------------------------------------- --- update casts for guid +-- Midnight secret-value display path +-- Uses SetShown() with secret booleans from UnitIsUnit +-- so the C-level API handles visibility without Lua boolean tests ------------------------------------------------- -local function GetCastsOnUnit(guid) - if castsOnUnit[guid] then - wipe(castsOnUnit[guid]) - wipe(sortedCastsOnUnit[guid]) +allActiveCasts = {} + +local function GetAllActiveCasts() + wipe(allActiveCasts) + local now = GetTime() + for sourceKey, castInfo in pairs(casts) do + if castInfo["endTime"] > now then + tinsert(allActiveCasts, castInfo) + else + casts[sourceKey] = nil + end + end + return allActiveCasts +end + +local function ShowCastsSecret(b, activeCasts, numCasts) + local ts = b.indicators.targetedSpells + local unit = b.states.displayedUnit or b.states.unit + if not unit then return end + + -- Icons: set up each icon slot, use SetAlphaFromBoolean for secret-safe visibility + -- SetAlphaFromBoolean is AllowedWhenTainted (works from addon code) + if displayMode ~= "Border" then + local num = min(maxIcons, numCasts) + for i = 1, num do + local cast = activeCasts[i] + ts[i].cooldown:SetReverse(not cast.isChanneling) + ts[i].duration:Hide() + if cast.count and cast.count ~= 1 then + ts[i].stack:Show() + ts[i].stack:SetText(cast.count) + else + ts[i].stack:Hide() + end + ts[i].border:Show() + ts[i].cooldown:Show() + ts[i].cooldown:SetSwipeColor(unpack(Cell.vars.targetedSpellsGlow[2])) + ts[i].cooldown:SetCooldown(cast.startTime, cast.endTime - cast.startTime) + ts[i].icon:SetTexture(cast.icon) + ts[i]:Show() + -- SetAlphaFromBoolean: alpha 1 if targeted, alpha 0 if not (C-level, accepts secrets) + ts[i]:SetAlphaFromBoolean(UnitIsUnit(cast.sourceUnit .. "target", unit)) + end + -- Hide unused slots + for i = numCasts + 1, #ts do + ts[i]:Hide() + end + ts:UpdateSize(num) + end + + -- Glow: start the glow effect, use SetAlphaFromBoolean on tsGlowFrame + if displayMode ~= "Icons" and numCasts > 0 then + ts:ShowGlow(unpack(Cell.vars.targetedSpellsGlow)) + ts.tsGlowFrame:SetAlphaFromBoolean(UnitIsUnit(activeCasts[1].sourceUnit .. "target", unit)) else - castsOnUnit[guid] = {} - sortedCastsOnUnit[guid] = {} + ts:HideGlow() + end +end + +local function UpdateAllButtonsCasts() + local activeCasts = GetAllActiveCasts() + local numCasts = #activeCasts + + if numCasts == 0 then + F.IterateAllUnitButtons(HideCasts, true) + return + end + + F.IterateAllUnitButtons(function(b) + ShowCastsSecret(b, activeCasts, numCasts) + end, true) +end + +------------------------------------------------- +-- update casts for unit (non-secret path) +------------------------------------------------- +local function GetCastsOnUnit(targetUnit) + if castsOnUnit[targetUnit] then + wipe(castsOnUnit[targetUnit]) + wipe(sortedCastsOnUnit[targetUnit]) + else + castsOnUnit[targetUnit] = {} + sortedCastsOnUnit[targetUnit] = {} end local inListFound - for sourceGUID, castInfo in pairs(casts) do - if guid == castInfo["targetGUID"] then + local castIndex = 0 + for sourceKey, castInfo in pairs(casts) do + if targetUnit == castInfo["targetUnit"] then if castInfo["endTime"] > GetTime() then -- not expired - local spellId = castInfo["spellId"] - if not castsOnUnit[guid][spellId] then - castsOnUnit[guid][spellId] = {["count"] = 0} + -- On Midnight, spellId may be secret — can't use as table key. + -- Use a numeric index instead to group casts. + castIndex = castIndex + 1 + local key = castInfo["nonSecretSpellId"] or castIndex + if not castsOnUnit[targetUnit][key] then + castsOnUnit[targetUnit][key] = {["count"] = 0} end - if not castsOnUnit[guid][spellId]["endTime"] or castsOnUnit[guid][spellId]["endTime"] > castInfo["endTime"] then --! shorter duration - castsOnUnit[guid][spellId]["startTime"] = castInfo["startTime"] - castsOnUnit[guid][spellId]["endTime"] = castInfo["endTime"] - castsOnUnit[guid][spellId]["icon"] = castInfo["icon"] + if not castsOnUnit[targetUnit][key]["endTime"] or castsOnUnit[targetUnit][key]["endTime"] > castInfo["endTime"] then --! shorter duration + castsOnUnit[targetUnit][key]["startTime"] = castInfo["startTime"] + castsOnUnit[targetUnit][key]["endTime"] = castInfo["endTime"] + castsOnUnit[targetUnit][key]["icon"] = castInfo["icon"] + castsOnUnit[targetUnit][key]["isChanneling"] = castInfo["isChanneling"] end - castsOnUnit[guid][spellId]["count"] = castsOnUnit[guid][spellId]["count"] + 1 + castsOnUnit[targetUnit][key]["count"] = castsOnUnit[targetUnit][key]["count"] + 1 - if Cell.vars.targetedSpellsList[spellId] then - castsOnUnit[guid][spellId]["inList"] = true + if castInfo["inList"] then + castsOnUnit[targetUnit][key]["inList"] = true inListFound = true end else - casts[sourceGUID] = nil + casts[sourceKey] = nil end end end - return castsOnUnit[guid], inListFound + return castsOnUnit[targetUnit], inListFound end local function Comparator(a, b) @@ -98,40 +236,22 @@ local function Comparator(a, b) return a.startTime < b.startTime end -local function UpdateCastsOnUnit(guid) - if not guid then return end - - -- local startTime, endTime, spellId, icon, isChanneling - local t, showGlow = GetCastsOnUnit(guid) - - for spellId, castInfo in pairs(t) do - tinsert(sortedCastsOnUnit[guid], castInfo) +local function UpdateCastsOnUnit(targetUnit) + if not targetUnit then return end - -- if not endTime then --! init - -- startTime, endTime, spellId, icon, isChanneling = castInfo["startTime"], castInfo["endTime"], castInfo["spellId"], castInfo["icon"], castInfo["isChanneling"] - -- else - -- spellId = castInfo["spellId"] - -- if Cell.vars.targetedSpellsList[spellId] then --! [IN LIST] - -- if not inListFound or endTime > castInfo["endTime"] then --! NOT FOUND BEFORE or SHORTER DURATION - -- startTime, endTime, icon, isChanneling = castInfo["startTime"], castInfo["endTime"], castInfo["icon"], castInfo["isChanneling"] - -- end - -- elseif not inListFound and endTime > castInfo["endTime"] then --! [NOT IN LIST] NOT FOUND BEFORE and SHORTER DURATION - -- startTime, endTime, icon, isChanneling = castInfo["startTime"], castInfo["endTime"], castInfo["icon"], castInfo["isChanneling"] - -- end - -- end + local t, showGlow = GetCastsOnUnit(targetUnit) - -- if Cell.vars.targetedSpellsList[spellId] then - -- inListFound = true - -- end + for key, castInfo in pairs(t) do + tinsert(sortedCastsOnUnit[targetUnit], castInfo) end - local n = #sortedCastsOnUnit[guid] + local n = #sortedCastsOnUnit[targetUnit] if n == 0 then - F.HandleUnitButton("guid", guid, HideCasts) + F.HandleUnitButton("unit", targetUnit, HideCasts) else - table.sort(sortedCastsOnUnit[guid], Comparator) - F.HandleUnitButton("guid", guid, ShowCasts, showGlow, sortedCastsOnUnit[guid], n) + table.sort(sortedCastsOnUnit[targetUnit], Comparator) + F.HandleUnitButton("unit", targetUnit, ShowCasts, showGlow, sortedCastsOnUnit[targetUnit], n) end end @@ -141,27 +261,21 @@ end local function CheckUnitCast(sourceUnit, isRecheck) if not UnitIsEnemy("player", sourceUnit) then return end - -- On Midnight 12.0.0+, enemy spellcast info is secret in instances - -- Player's own casts (and pets) are always non-secret - if Cell.isMidnight then - local isPlayerCast = (sourceUnit == "player" or sourceUnit == "pet" or sourceUnit == "vehicle") - if not isPlayerCast and F.IsAuraRestricted and F.IsAuraRestricted() then - return -- skip enemy spell tracking during restricted periods - end - end - - local sourceGUID = UnitGUID(sourceUnit) - -- Midnight 12.0.0+: UnitGUID for nameplates may return secret strings - if Cell.isMidnight and issecretvalue and issecretvalue(sourceGUID) then return end - local targetGUID + -- Use sourceUnit as tracking key (e.g., "nameplate1", "target"). + -- UnitGUID can be secret on Midnight — sourceUnit strings are always safe. + local sourceKey = sourceUnit local previousTarget, isChanneling - if casts[sourceGUID] then - previousTarget = casts[sourceGUID]["targetGUID"] - if casts[sourceGUID]["endTime"] <= GetTime() then + if casts[sourceKey] then + previousTarget = casts[sourceKey]["targetUnit"] + if casts[sourceKey]["endTime"] <= GetTime() then --! expired - casts[sourceGUID] = nil - UpdateCastsOnUnit(previousTarget) + casts[sourceKey] = nil + if useSecretPath then + UpdateAllButtonsCasts() + else + UpdateCastsOnUnit(previousTarget) + end previousTarget = nil end end @@ -174,48 +288,119 @@ local function CheckUnitCast(sourceUnit, isRecheck) isChanneling = true end - -- print(sourceUnit, name, spellId) + if not spellId then return end - if spellId and (Cell.vars.targetedSpellsList[spellId] or showAllSpells) then - if casts[sourceGUID] then - casts[sourceGUID]["startTime"] = startTimeMS/1000 - casts[sourceGUID]["endTime"] = endTimeMS/1000 - casts[sourceGUID]["spellId"] = spellId - casts[sourceGUID]["icon"] = texture - else - casts[sourceGUID] = { - ["startTime"] = startTimeMS/1000, - ["endTime"] = endTimeMS/1000, - ["spellId"] = spellId, - ["icon"] = texture, - ["isChanneling"] = isChanneling, - -- ["targetGUID"] = targetGUID, - -- ["sourceUnit"] = sourceUnit, - -- ["targetUnit"] = targetUnit, - ["recheck"] = 0, - } + -- Determine if spellId is secret + local spellIdIsSecret = not F.IsValueNonSecret(spellId) + local nonSecretSpellId -- used for grouping and list lookup when available + + if not spellIdIsSecret then + nonSecretSpellId = spellId + end + + -- Get icon: C_Spell.GetSpellTexture is C-level and accepts secret spellId + if Cell.isMidnight and C_Spell and C_Spell.GetSpellTexture then + local tex = C_Spell.GetSpellTexture(spellId) + if tex then texture = tex end + end + + -- Determine if this spell should be tracked + local inList = false + local shouldTrack = false + + if nonSecretSpellId then + -- Non-secret: use normal list lookup + if Cell.vars.targetedSpellsList[nonSecretSpellId] then + inList = true + shouldTrack = true + elseif showAllSpells then + shouldTrack = true end + else + -- Secret spellId: can't look up in list. + -- Use C_Spell.IsSpellImportant as a proxy for "dangerous/boss spell" + if C_Spell and C_Spell.IsSpellImportant then + local important = C_Spell.IsSpellImportant(spellId) + if not F.IsValueNonSecret(important) then + -- Secret boolean — treat as important (safe assumption for enemy casts) + inList = true + shouldTrack = true + elseif important then + inList = true + shouldTrack = true + end + end + -- In showAllSpells mode, show all enemy casts even if secret + if showAllSpells then + shouldTrack = true + end + end - local targetUnit = sourceUnit.."target" - targetUnit = F.GetTargetUnitID(targetUnit) -- units in group (players/pets), no npcs - if targetUnit then targetGUID = UnitGUID(targetUnit) end + -- In Border or Both mode, track all enemy casts targeting group members + -- (glow always shows regardless of spell list) + if not shouldTrack and displayMode ~= "Icons" then + shouldTrack = true + end - -- update spell target - casts[sourceGUID]["targetUnit"] = targetUnit - casts[sourceGUID]["targetGUID"] = targetGUID - casts[sourceGUID]["nonNameplate"] = not strfind(sourceUnit, "^nameplate") + if not shouldTrack then return end - UpdateCastsOnUnit(targetGUID) + -- Time values may be secret on Midnight — guard with F.IsValueNonSecret + local startTime, endTime + if F.IsValueNonSecret(startTimeMS) and F.IsValueNonSecret(endTimeMS) then + startTime = startTimeMS / 1000 + endTime = endTimeMS / 1000 + else + -- Fallback: use current time + reasonable estimate + startTime = GetTime() + endTime = GetTime() + 3 + end - if not isRecheck then - if not recheck[sourceGUID] or not (strfind(sourceUnit, "target$") or strfind(sourceUnit, "^nameplate")) then - recheck[sourceGUID] = sourceUnit - end - eventFrame:Show() + if casts[sourceKey] then + casts[sourceKey]["startTime"] = startTime + casts[sourceKey]["endTime"] = endTime + casts[sourceKey]["spellId"] = spellId + casts[sourceKey]["nonSecretSpellId"] = nonSecretSpellId + casts[sourceKey]["icon"] = texture + casts[sourceKey]["inList"] = inList + casts[sourceKey]["sourceUnit"] = sourceUnit + else + casts[sourceKey] = { + ["startTime"] = startTime, + ["endTime"] = endTime, + ["spellId"] = spellId, + ["nonSecretSpellId"] = nonSecretSpellId, + ["icon"] = texture, + ["isChanneling"] = isChanneling, + ["inList"] = inList, + ["sourceUnit"] = sourceUnit, + ["recheck"] = 0, + } + end + + -- Resolve target + local targetUnit, isSecret = GetTargetUnitID_Safe(sourceUnit.."target", sourceUnit) + + if isSecret then + -- UnitIsUnit returns secrets — use broadcast path with SetShown + useSecretPath = true + casts[sourceKey]["targetUnit"] = nil + casts[sourceKey]["nonNameplate"] = not strfind(sourceUnit, "^nameplate") + UpdateAllButtonsCasts() + else + -- Normal path — resolved target + casts[sourceKey]["targetUnit"] = targetUnit + casts[sourceKey]["nonNameplate"] = not strfind(sourceUnit, "^nameplate") + UpdateCastsOnUnit(targetUnit) + end + + if not isRecheck then + if not recheck[sourceKey] or not (strfind(sourceUnit, "target$") or strfind(sourceUnit, "^nameplate")) then + recheck[sourceKey] = sourceUnit end + eventFrame:Show() end - if previousTarget and previousTarget ~= targetGUID then + if not useSecretPath and previousTarget and previousTarget ~= targetUnit then UpdateCastsOnUnit(previousTarget) end end @@ -231,21 +416,30 @@ eventFrame:SetScript("OnUpdate", function(self, elapsed) local empty = true - for guid, unit in pairs(recheck) do - if casts[guid] then - casts[guid]["recheck"] = casts[guid]["recheck"] + 1 - if casts[guid]["recheck"] >= 6 then - recheck[guid] = nil + for sourceKey, unit in pairs(recheck) do + if casts[sourceKey] then + casts[sourceKey]["recheck"] = casts[sourceKey]["recheck"] + 1 + if casts[sourceKey]["recheck"] >= 6 then + recheck[sourceKey] = nil else empty = false - local recheckRequired = (not casts[guid]["targetUnit"] and UnitExists(unit.."target")) or (casts[guid]["targetUnit"] and not UnitIsUnit(unit.."target", casts[guid]["targetUnit"])) - if recheckRequired then - -- print(unit, casts[guid]["recheck"], recheckRequired) + if useSecretPath then + -- On secret path, just recheck cast and broadcast CheckUnitCast(unit, true) + else + local recheckRequired + if not casts[sourceKey]["targetUnit"] then + recheckRequired = UnitExists(unit.."target") + else + recheckRequired = not SafeUnitIsUnit(unit.."target", casts[sourceKey]["targetUnit"]) + end + if recheckRequired then + CheckUnitCast(unit, true) + end end end else - recheck[guid] = nil + recheck[sourceKey] = nil end end @@ -259,7 +453,7 @@ end) -- events ------------------------------------------------- eventFrame:SetScript("OnEvent", function(_, event, sourceUnit) - if event == "ENCOUNTER_END" then + if event == "ENCOUNTER_END" or event == "PLAYER_REGEN_ENABLED" then Reset() F.IterateAllUnitButtons(HideCasts, true) return @@ -274,23 +468,28 @@ eventFrame:SetScript("OnEvent", function(_, event, sourceUnit) CheckUnitCast(sourceUnit) elseif event == "UNIT_SPELLCAST_STOP" or event == "UNIT_SPELLCAST_INTERRUPTED" or event == "UNIT_SPELLCAST_FAILED" or event == "UNIT_SPELLCAST_CHANNEL_STOP" then - local sourceGUID = UnitGUID(sourceUnit) - -- Midnight 12.0.0+: UnitGUID may return secret strings — can't use as table key - if issecretvalue and issecretvalue(sourceGUID) then return end - if casts[sourceGUID] then - previousTarget = casts[sourceGUID]["targetGUID"] - casts[sourceGUID] = nil - UpdateCastsOnUnit(previousTarget) + -- Use sourceUnit as key (secret-safe, unlike UnitGUID) + local sourceKey = sourceUnit + if casts[sourceKey] then + local previousTarget = casts[sourceKey]["targetUnit"] + casts[sourceKey] = nil + if useSecretPath then + UpdateAllButtonsCasts() + else + UpdateCastsOnUnit(previousTarget) + end end elseif event == "NAME_PLATE_UNIT_REMOVED" then - local sourceGUID = UnitGUID(sourceUnit) - -- Midnight 12.0.0+: UnitGUID may return secret strings — can't use as table key - if issecretvalue and issecretvalue(sourceGUID) then return end - if casts[sourceGUID] and not casts[sourceGUID]["nonNameplate"] then - previousTarget = casts[sourceGUID]["targetGUID"] - casts[sourceGUID] = nil - UpdateCastsOnUnit(previousTarget) + local sourceKey = sourceUnit + if casts[sourceKey] and not casts[sourceKey]["nonNameplate"] then + local previousTarget = casts[sourceKey]["targetUnit"] + casts[sourceKey] = nil + if useSecretPath then + UpdateAllButtonsCasts() + else + UpdateCastsOnUnit(previousTarget) + end end end end) @@ -323,7 +522,28 @@ local function SetFont(frame, ...) end local function ShowGlowPreview(frame) - frame:ShowGlow(unpack(Cell.vars.targetedSpellsGlow)) + -- Show/hide icon previews based on display mode + if displayMode == "Border" then + -- Border only: hide icons, show glow + for i = 1, #frame do + frame[i]:Hide() + end + frame:UpdateSize(0) + else + -- Icons or Both: show preview icons (OnShow hooks handle icon/cooldown) + local num = min(maxIcons or 1, #frame) + for i = 1, num do + frame[i]:Show() + end + frame:UpdateSize(num) + end + + -- Show glow in Border and Both modes; hide in Icons mode + if displayMode == "Icons" then + frame:HideGlow() + else + frame:ShowGlow(unpack(Cell.vars.targetedSpellsGlow)) + end end local function ShowGlow(frame, glowType, color, arg1, arg2, arg3, arg4) @@ -400,6 +620,7 @@ end -- NOTE: in case there's a casting spell, hide! local function EnterLeaveInstance() Reset() + useSecretPath = false F.IterateAllUnitButtons(HideCasts, true) end @@ -409,10 +630,6 @@ function I.EnableTargetedSpells(enabled) b.indicators.targetedSpells:Show() end, true) - -- UNIT_SPELLCAST_DELAYED UNIT_SPELLCAST_FAILED UNIT_SPELLCAST_INTERRUPTED UNIT_SPELLCAST_START UNIT_SPELLCAST_STOP - -- UNIT_SPELLCAST_CHANNEL_START UNIT_SPELLCAST_CHANNEL_STOP - -- PLAYER_TARGET_CHANGED ENCOUNTER_END - eventFrame:RegisterEvent("UNIT_SPELLCAST_START") eventFrame:RegisterEvent("UNIT_SPELLCAST_STOP") eventFrame:RegisterEvent("UNIT_SPELLCAST_DELAYED") @@ -427,11 +644,13 @@ function I.EnableTargetedSpells(enabled) eventFrame:RegisterEvent("NAME_PLATE_UNIT_REMOVED") eventFrame:RegisterEvent("ENCOUNTER_END") + eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED") Cell.RegisterCallback("EnterInstance", "TargetedSpells_EnterInstance", EnterLeaveInstance) Cell.RegisterCallback("LeaveInstance", "TargetedSpells_LeaveInstance", EnterLeaveInstance) else Reset() + useSecretPath = false eventFrame:Hide() eventFrame:UnregisterAllEvents() @@ -451,4 +670,8 @@ end function I.UpdateTargetedSpellsNum(num) maxIcons = num -end \ No newline at end of file +end + +function I.UpdateTargetedSpellsDisplayMode(mode) + displayMode = mode or "Both" +end From 8b332ac70317640f48f17ed14aed7dca09a72480 Mon Sep 17 00:00:00 2001 From: jdtoppin <6392799+jdtoppin@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:19:58 -0600 Subject: [PATCH 05/61] =?UTF-8?q?PR=205:=20Misc=20fixes=20=E2=80=94=20Deat?= =?UTF-8?q?hReport,=20range=20checks,=20CLEU=20removal,=20comm=20guards=20?= =?UTF-8?q?DeathReport:=20Fix=20shared=20handlers=20(PLAYER=5FENTERING=5FW?= =?UTF-8?q?ORLD,=20GROUP=5FROSTER=5FUPDATE)=20that=20short-circuited=20on?= =?UTF-8?q?=20Midnight,=20breaking=20instance=20tracking=20and=20priority.?= =?UTF-8?q?=20Simplified=20death=20detection=20via=20UNIT=5FHEALTH=20+=20U?= =?UTF-8?q?nitIsDeadOrGhost=20on=20Midnight.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comm: Remove unused QueueComm/FlushPendingComms dead code. Add IsCommRestricted() guards for encounter/M+/PvP contexts. Simplify Nicknames.lua redundant F.IsCommRestricted existence checks. StatusIcon: F.IsValueNonSecret for GUID checks, CLEU conditional unregister. Request_Show: CombatLogGetCurrentEventInfo nil guards. BuffTracker: F.IsAuraNonSecret guard for sourceUnit comparison. Co-Authored-By: Claude Opus 4.6 (1M context) --- Comm/Comm.lua | 24 ---------- Comm/Nicknames.lua | 8 ++-- Indicators/StatusIcon.lua | 8 ++-- Utilities/BuffTracker.lua | 2 +- Utilities/DeathReport.lua | 15 ++++--- Utilities/Marks.lua | 91 +++++++++++++++++++------------------- Utilities/Request_Show.lua | 13 ++++-- 7 files changed, 75 insertions(+), 86 deletions(-) diff --git a/Comm/Comm.lua b/Comm/Comm.lua index 65ff64af..17a495c0 100644 --- a/Comm/Comm.lua +++ b/Comm/Comm.lua @@ -48,30 +48,6 @@ function F.IsCommRestricted() return IsCommRestricted() end --- Simple queue for deferred sends (used when comms are restricted) -local pendingComms = {} - -local function QueueComm(prefix, message, channel, target, priority) - tinsert(pendingComms, {prefix=prefix, message=message, channel=channel, target=target, priority=priority}) -end - -local function FlushPendingComms() - if IsCommRestricted() then return end - if #pendingComms == 0 then return end - local toSend = pendingComms - pendingComms = {} - for _, msg in ipairs(toSend) do - Comm:SendCommMessage(msg.prefix, msg.message, msg.channel, msg.target, msg.priority or "NORMAL") - end -end - -local commFrame = CreateFrame("Frame") -commFrame:RegisterEvent("ENCOUNTER_END") -commFrame:RegisterEvent("PLAYER_LEAVING_WORLD") -commFrame:SetScript("OnEvent", function() - C_Timer.After(1, FlushPendingComms) -end) - ----------------------------------------- -- for WA ----------------------------------------- diff --git a/Comm/Nicknames.lua b/Comm/Nicknames.lua index 1ce50419..b377dd67 100644 --- a/Comm/Nicknames.lua +++ b/Comm/Nicknames.lua @@ -74,7 +74,7 @@ local function CheckNicknames() nic_check = C_Timer.NewTimer(random(3), function() UpdateSendChannel() -- Addon comms blocked during encounters/M+/PvP on Midnight 12.0.0+ - if Cell.isMidnight and F.IsCommRestricted and F.IsCommRestricted() then + if Cell.isMidnight and F.IsCommRestricted() then F.Debug("Cell: Comm suppressed - restricted context (CELL_CNIC)") return end @@ -166,7 +166,7 @@ local function UpdateNicknames(which, value1, value2) -- disabled, notify others UpdateSendChannel() -- Addon comms blocked during encounters/M+/PvP on Midnight 12.0.0+ - if Cell.isMidnight and F.IsCommRestricted and F.IsCommRestricted() then + if Cell.isMidnight and F.IsCommRestricted() then F.Debug("Cell: Comm suppressed - restricted context (CELL_NIC sync-off)") else Comm:SendCommMessage("CELL_NIC", "CELL_NONE", sendChannel) @@ -189,7 +189,7 @@ local function UpdateNicknames(which, value1, value2) if IsInGroup() and CellDB["nicknames"]["sync"] then UpdateSendChannel() -- Addon comms blocked during encounters/M+/PvP on Midnight 12.0.0+ - if Cell.isMidnight and F.IsCommRestricted and F.IsCommRestricted() then + if Cell.isMidnight and F.IsCommRestricted() then F.Debug("Cell: Comm suppressed - restricted context (CELL_NIC mine)") else Comm:SendCommMessage("CELL_NIC", Cell.vars.playerNickname or "CELL_NONE", sendChannel) @@ -234,7 +234,7 @@ Comm:RegisterComm("CELL_CNIC", function(prefix, message, channel, sender) nic_send = C_Timer.NewTimer(3, function() UpdateSendChannel() -- Addon comms blocked during encounters/M+/PvP on Midnight 12.0.0+ - if Cell.isMidnight and F.IsCommRestricted and F.IsCommRestricted() then + if Cell.isMidnight and F.IsCommRestricted() then F.Debug("Cell: Comm suppressed - restricted context (CELL_NIC nic_send)") return end diff --git a/Indicators/StatusIcon.lua b/Indicators/StatusIcon.lua index f63d09e3..8b141320 100644 --- a/Indicators/StatusIcon.lua +++ b/Indicators/StatusIcon.lua @@ -77,7 +77,7 @@ else local guid = UnitGUID(unit) if not guid then return end -- Midnight 12.0.0+: UnitGUID may return secret strings for non-group units - if issecretvalue and issecretvalue(guid) then return end + if not F.IsValueNonSecret(guid) then return end -- Check if soulstone buff is now absent but was present -- (simple: after UNIT_AURA fires, see if unit still has it) local hasSoulstone = F.FindAuraByName and F.FindAuraByName(unit, "BUFF", SOULSTONE) @@ -95,7 +95,7 @@ else local guid = UnitGUID(unit) if not guid then return end -- Midnight 12.0.0+: UnitGUID may return secret strings for non-group units - if issecretvalue and issecretvalue(guid) then return end + if not F.IsValueNonSecret(guid) then return end if UnitIsDeadOrGhost(unit) then if soulstones[guid] then F.HandleUnitButton("unit", unit, DiedWithSoulstone) @@ -396,7 +396,9 @@ function I.EnableStatusIcon(enabled) end else eventFrame:UnregisterAllEvents() - cleuFrame:UnregisterAllEvents() + if CombatLogGetCurrentEventInfo then + cleuFrame:UnregisterAllEvents() + end F.IterateAllUnitButtons(function(b) b.indicators.statusIcon:Hide() b.indicators.resurrectionIcon:Hide() diff --git a/Utilities/BuffTracker.lua b/Utilities/BuffTracker.lua index 01283bb4..d3bb29f8 100644 --- a/Utilities/BuffTracker.lua +++ b/Utilities/BuffTracker.lua @@ -349,7 +349,7 @@ local enabled local myUnit = "" local hasBuffProvider -local fl function Reset(which) +local function Reset(which) if not which or which == "available" then for k, v in pairs(available) do available[k] = false diff --git a/Utilities/DeathReport.lua b/Utilities/DeathReport.lua index 1bb9be40..f7c87754 100644 --- a/Utilities/DeathReport.lua +++ b/Utilities/DeathReport.lua @@ -178,9 +178,14 @@ else local function OnUnitHealth(unit) if not unit then return end + -- UnitGUID can return a secret value in 12.0+; secret values can't be + -- used as table keys, so skip units whose GUID is secret. + local guid = UnitGUID(unit) + if not F.IsValueNonSecret(guid) then return end + if not guid then return end + if UnitIsDeadOrGhost(unit) and not UnitIsFeignDeath(unit) then - local guid = UnitGUID(unit) - if guid and not reportedDead[guid] then + if not reportedDead[guid] then reportedDead[guid] = true if not CheckSendLimit() then return end local name = UnitName(unit) or unit @@ -188,10 +193,7 @@ else end else -- unit is alive again; allow future death reports - local guid = UnitGUID(unit) - if guid then - reportedDead[guid] = nil - end + reportedDead[guid] = nil end end @@ -271,6 +273,7 @@ end -- priority ---------------------------------------------------- local function UpdatePriority(hasHighestPriority) + if not CombatLogGetCurrentEventInfo then return end if Cell.isMidnight then -- Midnight: CLEU unavailable; UNIT_HEALTH registration is handled in UpdateTools return diff --git a/Utilities/Marks.lua b/Utilities/Marks.lua index 1d6de0ad..d9d9888d 100644 --- a/Utilities/Marks.lua +++ b/Utilities/Marks.lua @@ -87,7 +87,9 @@ marks:Hide() local ticker local markButtons = {} for i = 1, 9 do - markButtons[i] = Cell.CreateButton(marks, "", "accent-hover", {20, 20}) + -- Midnight 12.0+: SetRaidTarget is protected. Use SecureActionButtonTemplate + -- with type="raidtarget" so marking works in and out of combat. + markButtons[i] = Cell.CreateButton(marks, "", "accent-hover", {20, 20}, false, false, nil, nil, "SecureActionButtonTemplate") markButtons[i].texture = markButtons[i]:CreateTexture(nil, "ARTWORK") P.Point(markButtons[i].texture, "TOPLEFT", markButtons[i], "TOPLEFT", 2, -2) P.Point(markButtons[i].texture, "BOTTOMRIGHT", markButtons[i], "BOTTOMRIGHT", -2, 2) @@ -95,59 +97,58 @@ for i = 1, 9 do if i == 9 then -- clear all marks markButtons[i].texture:SetTexture("Interface\\Buttons\\UI-GroupLoot-Pass-Up") - markButtons[i]:SetScript("OnClick", function() - RemoveRaidTargets() - -- markButtons[i]:SetEnabled(false) - -- markButtons[i].texture:SetDesaturated(true) - -- for j = 1, 8 do - -- SetRaidTarget("player", j) - -- end - -- C_Timer.After(0.5, function() - -- SetRaidTarget("player", 0) - -- markButtons[i]:SetEnabled(true) - -- markButtons[i].texture:SetDesaturated(false) - -- end) - end) + markButtons[i]:RegisterForClicks("AnyDown", "AnyUp") + markButtons[i]:SetAttribute("type", "raidtarget") + markButtons[i]:SetAttribute("action", "clear-all") else markButtons[i].texture:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons") SetRaidTargetIconTexture(markButtons[i].texture, i) - markButtons[i]:RegisterForClicks("LeftButtonDown", "RightButtonDown") - markButtons[i]:SetScript("OnClick", function(self, button) - if button == "LeftButton" then - -- set raid target icon - if GetRaidTargetIndex("target") == i then - SetRaidTarget("target", 0) - else - SetRaidTarget("target", i) - end - elseif button == "RightButton" then - -- lock raid target icon + markButtons[i]:RegisterForClicks("AnyDown", "AnyUp") + + -- Left click: toggle raid target icon (secure action) + markButtons[i]:SetAttribute("type1", "raidtarget") + markButtons[i]:SetAttribute("marker", i) + markButtons[i]:SetAttribute("action1", "toggle") + + -- Right click: lock/unlock raid target icon (post-click script) + -- Lock uses SetRaidTarget in a timer which can't be secured; + -- this is a best-effort feature that may not work during combat. + local idx = i + markButtons[i]:SetScript("PostClick", function(self, button) + if button == "RightButton" then local unit, name, class = F.GetTargetUnitInfo() if unit and name then - if markButtons[i].locked then - F.NotifyMarkUnlock(i, name, class) - SetRaidTarget(markButtons[i].locked, 0) - markButtons[i]:SetBackdropBorderColor(0, 0, 0, 1) - markButtons[i].locked = nil - if markButtons[i].ticker then - markButtons[i].ticker:Cancel() - markButtons[i].ticker = nil + if markButtons[idx].locked then + F.NotifyMarkUnlock(idx, name, class) + -- Clear the mark from the locked unit (skip in combat — protected) + if not InCombatLockdown() then + SetRaidTarget(markButtons[idx].locked, 0) + end + markButtons[idx]:SetBackdropBorderColor(0, 0, 0, 1) + markButtons[idx].locked = nil + if markButtons[idx].ticker then + markButtons[idx].ticker:Cancel() + markButtons[idx].ticker = nil end else - F.NotifyMarkLock(i, name, class) - SetRaidTarget(unit, i) - markButtons[i]:SetBackdropBorderColor(markColors[i][1], markColors[i][2], markColors[i][3], 1) - markButtons[i].locked = unit - markButtons[i].ticker = C_Timer.NewTicker(1.5, function() + F.NotifyMarkLock(idx, name, class) + -- Apply mark immediately (skip in combat — protected) + if not InCombatLockdown() then + SetRaidTarget(unit, idx) + end + markButtons[idx]:SetBackdropBorderColor(markColors[idx][1], markColors[idx][2], markColors[idx][3], 1) + markButtons[idx].locked = unit + markButtons[idx].ticker = C_Timer.NewTicker(1.5, function() + -- SetRaidTarget is protected on Midnight; skip in combat + if InCombatLockdown() then return end if UnitName(unit) == name then - if GetRaidTargetIndex(unit) ~= i then - SetRaidTarget(unit, i) - end + -- Re-apply mark (SetRaidTarget is a no-op if already correct) + SetRaidTarget(unit, idx) else - markButtons[i].locked = nil - markButtons[i].ticker:Cancel() - markButtons[i].ticker = nil - markButtons[i]:SetBackdropBorderColor(0, 0, 0, 1) + markButtons[idx].locked = nil + markButtons[idx].ticker:Cancel() + markButtons[idx].ticker = nil + markButtons[idx]:SetBackdropBorderColor(0, 0, 0, 1) end end) end diff --git a/Utilities/Request_Show.lua b/Utilities/Request_Show.lua index 21824c2e..9c274d9c 100644 --- a/Utilities/Request_Show.lua +++ b/Utilities/Request_Show.lua @@ -264,7 +264,9 @@ end SR:SetScript("OnEvent", function(self, event, ...) if event == "COMBAT_LOG_EVENT_UNFILTERED" then - self:COMBAT_LOG_EVENT_UNFILTERED(CombatLogGetCurrentEventInfo()) + if CombatLogGetCurrentEventInfo then + self:COMBAT_LOG_EVENT_UNFILTERED(CombatLogGetCurrentEventInfo()) + end else self[event](self, ...) end @@ -298,7 +300,9 @@ local function SR_UpdateRequests(which) SR:UnregisterEvent("CHAT_MSG_WHISPER") end - SR:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") + if CombatLogGetCurrentEventInfo then + SR:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") + end else SR:UnregisterAllEvents() end @@ -351,6 +355,7 @@ end -- hide glow if removed DR:SetScript("OnEvent", function(self, event) if event == "COMBAT_LOG_EVENT_UNFILTERED" then + if not CombatLogGetCurrentEventInfo then return end local timestamp, subEvent, _, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, spellID = CombatLogGetCurrentEventInfo() if subEvent == "SPELL_AURA_REMOVED" then local unit = Cell.vars.guids[destGUID] @@ -422,7 +427,9 @@ local function DR_UpdateRequests(which) drDebuffs = F.ConvertTable(CellDB["dispelRequest"]["debuffs"]) drDisplayType = CellDB["dispelRequest"]["type"] - DR:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") + if CombatLogGetCurrentEventInfo then + DR:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") + end DR:RegisterEvent("ENCOUNTER_START") DR:RegisterEvent("ENCOUNTER_END") else From df234757b01bb337ecb1838d38a977f2432d1a2d Mon Sep 17 00:00:00 2001 From: jdtoppin <6392799+jdtoppin@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:20:16 -0600 Subject: [PATCH 06/61] =?UTF-8?q?PR=206:=20Settings,=20appearance,=20clean?= =?UTF-8?q?up=20=E2=80=94=20dead=20code=20removal,=20UI=20fixes=20Appearan?= =?UTF-8?q?ce:=20Preview=20button=20Midnight=20StatusBar=20API=20compatibi?= =?UTF-8?q?lity,=20Flash=20animation=20removal=20(Smooth=20only=20on=20Mid?= =?UTF-8?q?night),=20shield=20preview=20rework.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings: Remove unused durationVisibilitySimple widget. Add TargetedSpells display mode dropdown. Update indicator setting lists. Cleanup: - Delete RaidDebuffs_Midnight_skeleton.lua (development artifact) - Fix stale F.IsSecretValue() references in changelog - Revise.lua: Flash → Smooth migration - Cell.toc: version bump - ClickCasting: updated Midnight spell list Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 3 +- CLAUDE.md | 64 ++ Cell.toc | 2 +- Defaults/Appearance_Defaults.lua | 2 +- Defaults/ClickCasting_DefaultSpells.lua | 16 +- HideBlizzard.lua | 2 +- Locales/enUS.lua | 4 +- Modules/Appearance/Appearance.lua | 78 +- Modules/General/General.lua | 2 +- Modules/Indicators/Indicators.lua | 67 +- RaidDebuffs/RaidDebuffs_Midnight_skeleton.lua | 697 ------------------ Revise.lua | 6 +- Widgets/Widgets_IndicatorSettings.lua | 73 +- 13 files changed, 238 insertions(+), 778 deletions(-) create mode 100644 CLAUDE.md delete mode 100644 RaidDebuffs/RaidDebuffs_Midnight_skeleton.lua diff --git a/.gitignore b/.gitignore index 5c499c57..4c19d4e8 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ Libs/* !Libs/LibBadWords.lua !Libs/LoadLibs.xml !Libs/LoadLibs_Classic.xml -!Libs/LibTranslit-1.0 \ No newline at end of file +!Libs/LibTranslit-1.0 +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..c918071f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,64 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Cell is a World of Warcraft raid frame addon by enderneko. This fork adds fixes and enhancements for the WoW 12.0 (Midnight) expansion, particularly around secret value handling and new raid/dungeon debuff data. + +## Architecture + +- **Core.lua** — Addon initialization, event handling, main namespace (`Cell`, `F`, `I`, `P`) +- **Utils.lua** — Utility functions used throughout +- **RaidFrames/UnitButton.lua** — Main unit button logic including aura processing (`HandleBuff`, `HandleDebuff`, `UnitButton_UpdateAuras`) +- **Indicators/Base.lua** — Indicator rendering system (icons, bars, text overlays on unit frames) +- **Defaults/Indicator_DefaultSpells.lua** — Spell definitions for built-in indicators +- **RaidDebuffs/** — Per-expansion raid debuff definitions (e.g., `RaidDebuffs_Midnight.lua`) +- **Widgets/** — UI widget library +- **Comm/** — Addon communication +- **Modules/** — Feature modules (click casting, raid tools, etc.) + +## Key Patterns + +- Aura iteration: `ForEachAura` (full update via `GetAuraSlots` + `GetAuraDataBySlot`) and `ForEachAuraCache` (partial update from cached auras) +- `UnitButton_UpdateAuras` handles both full and partial updates (UNIT_AURA updateInfo) +- External cooldowns: matched via `I.IsExternalCooldown(name, spellId, source, unit)` +- Dispels: `dispelName` field on debuff auras, rendered via `self.indicators.dispels:SetDispels()` + +## WoW 12.0 Secret Values (Critical) + +WoW 12.0 introduced "secret values" that crash on boolean tests: +- `secretVal or 0`, `if secretVal then`, `secretVal and x` all crash +- Safe: `issecretvalue()`, C-level APIs (`SetText`, `SetValue`, `SetVertexColor`, `SetMinMaxValues`) +- `rawequal(x, nil)` is safe for nil checks on potentially-secret values +- Non-dispellable debuffs: `dispelName = nil`; dispellable: `dispelName = SECRET` +- Use `issecretvalue(aura.dispelName)` to detect dispellable vs non-dispellable +- `CooldownFrame:SetCooldownFromDurationObject(durObj, clearIfZero)` for secret-safe cooldown display +- `AbbreviateNumbers(value)` is C-level and accepts secrets +- `SetFormattedText` with secrets produces invisible output — don't use for duration text + +## Packaging + +```bash +# External libs cached at /Users/josiahtoppin/Documents/Projects/Cell-external-libs/ +# Required: LibStub, CallbackHandler-1.0, AceComm-3.0, LibSerialize, LibCustomGlow-1.0, LibSharedMedia-3.0, LibDeflate +mkdir -p /private/tmp/Cell-release-build +git archive HEAD | tar -x -C /private/tmp/Cell-release-build/Cell +cp -R /Users/josiahtoppin/Documents/Projects/Cell-external-libs/* /private/tmp/Cell-release-build/Cell/Libs/ +cd /private/tmp/Cell-release-build && zip -r .zip Cell/ +``` + +- Libs go into `Libs/` subdirectory (NOT addon root) +- When merging to master, use `git merge --ff-only` to avoid duplicate merge commits + +## Branches + +- `master` — main branch +- `jdtoppin-patch-1` — WoW 12.0 secret value fixes + +## Midnight Expansion Data + +- Released: March 2, 2026 +- Raids: The Voidspire (1307, 6 bosses), March on Quel'Danas (1308, 2 bosses) +- Dungeon debuffs: `RaidDebuffs/RaidDebuffs_Midnight.lua` + entry in `LoadRaidDebuffs.xml` +- Detailed encounter/spell data in memory file `midnight-expansion-data.md` diff --git a/Cell.toc b/Cell.toc index 9fe590cd..1f0ac3cf 100644 --- a/Cell.toc +++ b/Cell.toc @@ -1,6 +1,6 @@ ## Interface: 120001 ## Title: Cell -## Version: r275-beta +## Version: r275-release ## Author: enderneko ## X-Flavor: Mainline ## SavedVariables: CellDB, CellDBBackup diff --git a/Defaults/Appearance_Defaults.lua b/Defaults/Appearance_Defaults.lua index f0220988..5be6ffe5 100644 --- a/Defaults/Appearance_Defaults.lua +++ b/Defaults/Appearance_Defaults.lua @@ -16,7 +16,7 @@ Cell.defaults.appearance = { ["barAlpha"] = 1, ["lossAlpha"] = 1, ["bgAlpha"] = 1, - ["barAnimation"] = "Flash", + ["barAnimation"] = "Smooth", ["colorThresholds"] = {{1,0,0}, {1,0.7,0}, {0.7,1,0}, 0.05, 0.95, true}, ["colorThresholdsLoss"] = {{1,0,0}, {1,0.7,0}, {0.7,1,0}, 0.05, 0.95, true}, ["auraIconOptions"] = { diff --git a/Defaults/ClickCasting_DefaultSpells.lua b/Defaults/ClickCasting_DefaultSpells.lua index 2538b68c..6f37f750 100644 --- a/Defaults/ClickCasting_DefaultSpells.lua +++ b/Defaults/ClickCasting_DefaultSpells.lua @@ -53,11 +53,7 @@ local defaultSpells = { 212040, -- Revitalize - 新生 88423, -- Nature's Cure - 自然之愈 "33763S", -- Lifebloom - 生命绽放 - "102351S", -- Cenarion Ward - 塞纳里奥结界 - "50464S", -- Nourish - 滋养 "102342S", -- Ironbark - 铁木树皮 - "203651S", -- Overgrowth - 过度生长 - "392160S", -- Invigorate - 鼓舞 "18562S", -- Swiftmend - 迅捷治愈 "102693H", -- Grove Guardians - 林莽卫士 "305497P", -- pvp - Thorns - 荆棘术 @@ -91,7 +87,6 @@ local defaultSpells = { 360823, -- Naturalize - 自然平衡 "364343S", -- Echo - 回响 "366155S", -- Reversion - 逆转 - "367226S", -- Spiritbloom - 精神之花 "357170S", -- Time Dilation - 时间膨胀 }, -- 1473 - Augmentation @@ -184,12 +179,11 @@ local defaultSpells = { "223306S", -- Bestow Faith -- 赋予信仰 "114165S", -- Holy Prism - 神圣棱镜 "183998S", -- Light of the Martyr -- 殉道者之光 - "148039S", -- Barrier of Faith - 信仰屏障 "156910S", -- Beacon of Faith - 信仰道标 - "388007S", -- Blessing of Summer - 仲夏祝福 "200025S", -- Beacon of Virtue -- 美德道标 "432459H", -- Holy Bulwark - 神圣壁垒 "156322H", -- Eternal Flame - 永恒之火 + "148039P", -- pvp - Barrier of Faith - 信仰屏障 }, -- 66 - Protection [66] = { @@ -229,6 +223,7 @@ local defaultSpells = { "194509S", -- Power Word: Radiance - 真言术:耀 "33206S", -- Pain Suppression - 痛苦压制 "47536S", -- Rapture - 全神贯注 + "62618S", -- Power Word: Barrier - 真言术:障 -- "314867S", -- Shadow Covenant - 暗影盟约 (removed in 12.0) -- "421453S", -- Ultimate Penitence - 终极苦修 }, @@ -239,16 +234,19 @@ local defaultSpells = { 2060, -- Heal - 治疗术 "33076S", -- Prayer of Mending - 愈合祷言 (moved from class to Holy in 12.0) "2050S", -- Holy Word: Serenity - 圣言术:静 + "34861S", -- Holy Word: Sanctify - 圣言术:灵 "596S", -- Prayer of Healing - 治疗祷言 "47788S", -- Guardian Spirit - 守护之魂 "204883S", -- Circle of Healing - 治疗之环 + "64843S", -- Divine Hymn - 神圣赞美诗 + "200183S", -- Apotheosis - 神化 "289666P", -- pvp - Greater Heal - 强效治疗术 "213610P", -- pvp - Holy Ward - 神圣守卫 "197268P", -- pvp - Ray of Hope - 希望之光 }, -- 258 - Shadow [258] = { - "213634C", -- Purify Disease - 净化疾病 + "213634C", -- Purify Disease }, }, @@ -266,7 +264,6 @@ local defaultSpells = { ["common"] = { 462854, -- Skyfury - 天怒 2008, -- Ancestral Spirit - 先祖之魂 - 8004, -- Healing Surge - 治疗之涌 546, -- Water Walking - 水上行走 "1064C", -- Chain Heal - 治疗链 "974C", -- Earth Shield - 大地之盾 @@ -287,7 +284,6 @@ local defaultSpells = { "61295S", -- Riptide - 激流 "77472S", -- Healing Wave - 治疗波 "73685S", -- Unleash Life - 生命释放 - "428332S", -- Primordial Wave - 始源之潮 }, }, diff --git a/HideBlizzard.lua b/HideBlizzard.lua index 76234c35..8ab6f6c7 100644 --- a/HideBlizzard.lua +++ b/HideBlizzard.lua @@ -92,4 +92,4 @@ function F.HideBlizzardRaidManager() _G.CompactRaidFrameManager:UnregisterAllEvents() _G.CompactRaidFrameManager:SetParent(hiddenParent) end -end \ No newline at end of file +end diff --git a/Locales/enUS.lua b/Locales/enUS.lua index d1183692..539b867f 100644 --- a/Locales/enUS.lua +++ b/Locales/enUS.lua @@ -99,7 +99,7 @@ select(2, ...).L = setmetatable({

r275-release — WoW 12.0.0 (Midnight) Compatibility

Secret Values (12.0.0+)

-

+ Added Cell.isMidnight detection flag and F.IsSecretValue(), F.IsAuraRestricted(), F.IsCooldownRestricted() utility functions.

+

+ Added Cell.isMidnight detection flag and F.IsValueNonSecret(), F.IsAuraRestricted(), F.IsCooldownRestricted(), F.HasAnySecretValues() utility functions.

+ Added per-aura F.IsAuraNonSecret(), F.IsSpellAuraNonSecret(), F.IsValueNonSecret() helpers — non-secret (whitelisted) auras now get real countdown timers, source detection, and duration display; secret auras gracefully degrade.

* UnitButton: major dual-path refactor — Midnight uses UnitHealPredictionCalculator, C_CurveUtil.CreateCurve(), and StatusBar overlays for health/prediction/shields; pre-Midnight retains arithmetic-based paths.

* Appearance: IncomingHeal widget uses SetStatusBarTexture on Midnight (StatusBar) vs SetTexture pre-Midnight (Texture).

@@ -114,7 +114,7 @@ select(2, ...).L = setmetatable({

- General: removed useCleuHealthUpdater checkbox (CLEU health updater obsolete).

* Revise: r275 migration removes useCleuHealthUpdater from saved variables.

Comm Restrictions

-

+ Comm: IsCommRestricted() detects encounters/M+/PvP; all SendCommMessage calls guarded; pending queue with flush on ENCOUNTER_END.

+

+ Comm: IsCommRestricted() detects encounters/M+/PvP; all SendCommMessage calls guarded.

+ Nicknames: all nickname sync sends guarded with F.IsCommRestricted().

Heal Prediction & Health Bar Fixes

* Created a dedicated healPredictionCalculator separate from the shared healthCalculator — fixes corrupted health/absorb reads.

diff --git a/Modules/Appearance/Appearance.lua b/Modules/Appearance/Appearance.lua index 028f7fe3..1020fedd 100644 --- a/Modules/Appearance/Appearance.lua +++ b/Modules/Appearance/Appearance.lua @@ -385,18 +385,7 @@ local function CreatePreviewButtons() healthPercent = health / 100 previewButton.perc = healthPercent - if CellDB["appearance"]["barAnimation"] == "Flash" then - previewButton.widgets.healthBar:SetValue(health) - - local diff = healthPercent - (healthPercentOld or healthPercent) - if diff >= 0 then - B.HideFlash(previewButton) - -- previewButton.widgets.damageFlashTex:Hide() - elseif diff <= -0.05 and diff >= -1 then - B.ShowFlash(previewButton, abs(diff)) - -- print(abs(diff)) - end - elseif CellDB["appearance"]["barAnimation"] == "Smooth" then + if CellDB["appearance"]["barAnimation"] == "Smooth" then previewButton.widgets.healthBar:SetSmoothedValue(health) else previewButton.widgets.healthBar:SetValue(health) @@ -431,8 +420,19 @@ local function CreatePreviewButtons() end local function UpdatePreviewShields(r, g, b) + -- Preview 3 shows heal prediction, heal absorb, shield, and overshield. + -- On Midnight, these widgets are StatusBars (not Textures), so use StatusBar API. + + -- Heal prediction if CellDB["appearance"]["healPrediction"][1] then - previewButton2.widgets.incomingHeal:SetValue(0.2, 0.6) + if Cell.isMidnight then + -- StatusBar: set range to match health bar, show 20% incoming heal + previewButton2.widgets.incomingHeal:SetMinMaxValues(0, 100) + previewButton2.widgets.incomingHeal:SetValue(20) + previewButton2.widgets.incomingHeal:Show() + else + previewButton2.widgets.incomingHeal:SetValue(0.2, 0.6) + end if CellDB["appearance"]["healPrediction"][2] then previewButton2.widgets.incomingHeal:SetVertexColor(CellDB["appearance"]["healPrediction"][3][1], CellDB["appearance"]["healPrediction"][3][2], CellDB["appearance"]["healPrediction"][3][3], CellDB["appearance"]["healPrediction"][3][4]) else @@ -442,9 +442,16 @@ local function UpdatePreviewShields(r, g, b) previewButton2.widgets.incomingHeal:Hide() end + -- Heal absorb if Cell.isRetail or Cell.isMists then if CellDB["appearance"]["healAbsorb"][1] then - previewButton2.widgets.absorbsBar:SetValue(0.8, 0.6) + if Cell.isMidnight then + previewButton2.widgets.absorbsBar:SetMinMaxValues(0, 100) + previewButton2.widgets.absorbsBar:SetValue(20) + previewButton2.widgets.absorbsBar:Show() + else + previewButton2.widgets.absorbsBar:SetValue(0.8, 0.6) + end if CellDB["appearance"]["healAbsorbInvertColor"] then previewButton2.widgets.absorbsBar:SetVertexColor(F.InvertColor(previewButton2.widgets.healthBar:GetStatusBarColor())) previewButton2.widgets.overAbsorbGlow:SetVertexColor(F.InvertColor(previewButton2.widgets.healthBar:GetStatusBarColor())) @@ -458,16 +465,38 @@ local function UpdatePreviewShields(r, g, b) end end + -- Shield texture if Cell.isRetail or Cell.isMists or Cell.isWrath or Cell.isCata then + local reverseFilling = CellDB["appearance"]["shield"][1] and CellDB["appearance"]["overshieldReverseFill"] + if CellDB["appearance"]["shield"][1] then - previewButton2.widgets.shieldBar:SetValue(0.6, 0.6) - previewButton2.widgets.shieldBar:SetVertexColor(unpack(CellDB["appearance"]["shield"][2])) + if reverseFilling then + -- Reverse fill: only show shieldBarR, hide shieldBar + previewButton2.widgets.shieldBar:Hide() + if Cell.isMidnight then + previewButton2.widgets.shieldBarR:SetMinMaxValues(0, 100) + previewButton2.widgets.shieldBarR:SetValue(30) + end + previewButton2.widgets.shieldBarR:SetVertexColor(unpack(CellDB["appearance"]["shield"][2])) + previewButton2.widgets.shieldBarR:Show() + else + -- Normal fill: show shieldBar, hide shieldBarR + if Cell.isMidnight then + previewButton2.widgets.shieldBar:SetMinMaxValues(0, 100) + previewButton2.widgets.shieldBar:SetValue(30) + else + previewButton2.widgets.shieldBar:SetValue(0.6, 0.6) + end + previewButton2.widgets.shieldBar:SetVertexColor(unpack(CellDB["appearance"]["shield"][2])) + previewButton2.widgets.shieldBar:Show() + previewButton2.widgets.shieldBarR:Hide() + end else previewButton2.widgets.shieldBar:Hide() + previewButton2.widgets.shieldBarR:Hide() end - local reverseFilling = CellDB["appearance"]["shield"][1] and CellDB["appearance"]["overshieldReverseFill"] - + -- Overshield glow if CellDB["appearance"]["overshield"][1] and not reverseFilling then previewButton2.widgets.overShieldGlow:SetVertexColor(unpack(CellDB["appearance"]["overshield"][2])) previewButton2.widgets.overShieldGlow:Show() @@ -476,9 +505,6 @@ local function UpdatePreviewShields(r, g, b) end if reverseFilling then - previewButton2.widgets.shieldBarR:SetVertexColor(unpack(CellDB["appearance"]["shield"][2])) - previewButton2.widgets.shieldBarR:Show() - if CellDB["appearance"]["overshield"][1] then previewButton2.widgets.overShieldGlowR:SetVertexColor(unpack(CellDB["appearance"]["overshield"][2])) previewButton2.widgets.overShieldGlowR:Show() @@ -486,7 +512,6 @@ local function UpdatePreviewShields(r, g, b) previewButton2.widgets.overShieldGlowR:Hide() end else - previewButton2.widgets.shieldBarR:Hide() previewButton2.widgets.overShieldGlowR:Hide() end end @@ -1338,13 +1363,6 @@ local function CreateUnitButtonStylePane() barAnimationDropdown = Cell.CreateDropdown(unitButtonPane, 141) barAnimationDropdown:SetPoint("TOPLEFT", powerColorDropdown, "BOTTOMLEFT", 0, -30) barAnimationDropdown:SetItems({ - { - ["text"] = L["Flash"], - ["onClick"] = function() - CellDB["appearance"]["barAnimation"] = "Flash" - Cell.Fire("UpdateAppearance", "animation") - end, - }, { ["text"] = L["Smooth"], ["onClick"] = function() @@ -1863,4 +1881,4 @@ local function UpdateAppearance(which) UpdatePreviewButton(which) end end -Cell.RegisterCallback("UpdateAppearance", "UpdateAppearance", UpdateAppearance) \ No newline at end of file +Cell.RegisterCallback("UpdateAppearance", "UpdateAppearance", UpdateAppearance) diff --git a/Modules/General/General.lua b/Modules/General/General.lua index 875a7725..d717aedd 100644 --- a/Modules/General/General.lua +++ b/Modules/General/General.lua @@ -525,4 +525,4 @@ local function ShowTab(tab) generalTab:Hide() end end -Cell.RegisterCallback("ShowOptionsTab", "GeneralTab_ShowTab", ShowTab) \ No newline at end of file +Cell.RegisterCallback("ShowOptionsTab", "GeneralTab_ShowTab", ShowTab) diff --git a/Modules/Indicators/Indicators.lua b/Modules/Indicators/Indicators.lua index 96c9bd99..87b8ace6 100644 --- a/Modules/Indicators/Indicators.lua +++ b/Modules/Indicators/Indicators.lua @@ -128,16 +128,42 @@ end -- indicator preview onupdate local function SetOnUpdate(indicator, type, icon, stack, extra) indicator.preview = indicator.preview or CreateFrame("Frame", nil, indicator) + -- Midnight BorderIcon preview: use reversed swipe so the colored border + -- is visible as base and black fills in (matches in-game SetCooldownFromAura). + local isMidnightBorderIcon = Cell.isMidnight and indicator.cooldown + and indicator.cooldown._SetCooldown and not indicator.cooldown.SetMinMaxValues + local function doPreview() + if isMidnightBorderIcon and not type then + -- Buff cooldowns (no debuff type): yellow border base, black swipe fills in + indicator.icon:SetTexture(icon) + indicator.stack:SetText(stack and stack > 1 and stack or "") + -- Yellow border as base color + if indicator.border then + indicator.border:SetColorTexture(1, 0.85, 0) + indicator.border:Show() + end + -- Black swipe fills IN (reverse) over the yellow border + if indicator.cooldown then + indicator.cooldown:SetReverse(true) + indicator.cooldown:SetSwipeColor(0, 0, 0) + indicator.cooldown:_SetCooldown(GetTime(), 13) + indicator.cooldown:Show() + end + indicator:Show() + else + indicator:SetCooldown(GetTime(), 13, type, icon, stack or 0, false, extra) + end + end indicator.preview:SetScript("OnUpdate", function(self, elapsed) self.elapsedTime = (self.elapsedTime or 0) + elapsed if self.elapsedTime >= 13 then self.elapsedTime = 0 - indicator:SetCooldown(GetTime(), 13, type, icon, stack, false, extra) + doPreview() end end) indicator:SetScript("OnShow", function() indicator.preview.elapsedTime = 0 - indicator:SetCooldown(GetTime(), 13, type, icon, stack, false, extra) + doPreview() end) end @@ -302,7 +328,7 @@ local function InitIndicator(indicatorName) end) elseif indicatorName == "shieldBar" then - indicator:SetValue(0.5) + indicator:SetPercent(0.5) elseif indicatorName == "powerWordShield" then indicator:SetScript("OnShow", function() @@ -1545,7 +1571,7 @@ if Cell.isRetail or Cell.isMists then ["nameText"] = {"enabled", "color-class", "textWidth", "checkbutton:showGroupNumber", "vehicleNamePosition", "position", "frameLevel", "font-noOffset"}, ["statusText"] = {"enabled", "checkbutton:showTimer", "checkbutton2:showBackground", "statusColors", "statusPosition", "frameLevel", "font-noOffset"}, ["healthText"] = {"|cffff7727"..L["MODERATE CPU USAGE"], "enabled", "healthFormat", "position", "frameLevel", "font-noOffset"}, - ["powerText"] = {"enabled", "color-power", "powerFormat", "powerTextFilters", "checkbutton:hideIfEmptyOrFull", "position", "frameLevel", "font-noOffset"}, + ["powerText"] = {"enabled", "color-power", "powerFormat", "powerTextFilters", "position", "frameLevel", "font-noOffset"}, ["statusIcon"] = { -- "|A:dungeonskull:18:18|a ".. "|TInterface\\LFGFrame\\LFG-Eye:18:18:0:0:512:256:72:120:72:120|t ".. @@ -1573,10 +1599,12 @@ if Cell.isRetail or Cell.isMists then ["allCooldowns"] = {"enabled", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, ["tankActiveMitigation"] = {"|cffb7b7b7"..I.GetTankActiveMitigationString(), "enabled", "color-class", "size", "position", "frameLevel"}, ["dispels"] = {"enabled", "dispelFilters", "highlightType", "dispelBlacklist", "iconStyle", "orientation", "size-square", "position", "frameLevel"}, - ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "durationVisibility", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, - ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "durationVisibility", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, + ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont"}, + ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont"}, ["privateAuras"] = {"|cffb7b7b7"..L["Due to restrictions of the private aura system, this indicator can only use Blizzard style."], "enabled", "privateAuraOptions", "size-square", "position", "frameLevel"}, - ["targetedSpells"] = {"enabled", "checkbutton:showAllSpells:"..L["Glow is only available to the spells in the list below"], "targetedSpellsList", "targetedSpellsGlow", "size-border", "num:3", "orientation", "position", "frameLevel", "font"}, + ["targetedSpells"] = Cell.isMidnight + and {"enabled", "targetedSpellsDisplayMode", "targetedSpellsGlow", "size-border", "num:3", "orientation", "position", "frameLevel", "font"} + or {"enabled", "checkbutton:showAllSpells:"..L["Glow is only available to the spells in the list below"], "targetedSpellsDisplayMode", "targetedSpellsList", "targetedSpellsGlow", "size-border", "num:3", "orientation", "position", "frameLevel", "font"}, ["targetCounter"] = {"|cffff2727"..L["HIGH CPU USAGE"].."!|r |cffb7b7b7"..L["Check all visible enemy nameplates."], "enabled", "targetCounterFilters", "color", "position", "frameLevel", "font-noOffset"}, ["crowdControls"] = {"enabled", "builtInCrowdControls", "customCrowdControls", "durationVisibility", "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, ["actions"] = {"|cffb7b7b7"..L["Play animation when the unit uses a specific spell/item. The list is global shared, not layout-specific."], "enabled", "actionsPreview", "actionsList"}, @@ -1593,7 +1621,7 @@ elseif Cell.isCata or Cell.isWrath then ["nameText"] = {"enabled", "color-class", "textWidth", "checkbutton:showGroupNumber", "vehicleNamePosition", "position", "frameLevel", "font-noOffset"}, ["statusText"] = {"enabled", "checkbutton:showTimer", "checkbutton2:showBackground", "statusColors", "statusPosition", "frameLevel", "font-noOffset"}, ["healthText"] = {"|cffff7727"..L["MODERATE CPU USAGE"], "enabled", "healthFormat", "position", "frameLevel", "font-noOffset"}, - ["powerText"] = {"enabled", "color-power", "powerFormat", "powerTextFilters", "checkbutton:hideIfEmptyOrFull", "position", "frameLevel", "font-noOffset"}, + ["powerText"] = {"enabled", "color-power", "powerFormat", "powerTextFilters", "position", "frameLevel", "font-noOffset"}, ["statusIcon"] = { -- "|A:dungeonskull:18:18|a ".. "|TInterface\\LFGFrame\\LFG-Eye:18:18:0:0:512:256:72:120:72:120|t ".. @@ -1617,9 +1645,9 @@ elseif Cell.isCata or Cell.isWrath then ["defensiveCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInDefensives", "customDefensives", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, ["allCooldowns"] = {"enabled", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, ["dispels"] = {"enabled", "dispelFilters", "highlightType", "dispelBlacklist", "iconStyle", "orientation", "size-square", "position", "frameLevel"}, - ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "durationVisibility", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, - ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "durationVisibility", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, - ["targetedSpells"] = {"enabled", "checkbutton:showAllSpells:"..L["Glow is only available to the spells in the list below"], "targetedSpellsList", "targetedSpellsGlow", "size-border", "num:3", "orientation", "position", "frameLevel", "font"}, + ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont"}, + ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont"}, + ["targetedSpells"] = {"enabled", "checkbutton:showAllSpells:"..L["Glow is only available to the spells in the list below"], "targetedSpellsDisplayMode", "targetedSpellsList", "targetedSpellsGlow", "size-border", "num:3", "orientation", "position", "frameLevel", "font"}, ["targetCounter"] = {"|cffff2727"..L["HIGH CPU USAGE"].."!|r |cffb7b7b7"..L["Check all visible enemy nameplates."], "enabled", "targetCounterFilters", "color", "position", "frameLevel", "font-noOffset"}, ["actions"] = {"|cffb7b7b7"..L["Play animation when the unit uses a specific spell/item. The list is global shared, not layout-specific."], "enabled", "actionsPreview", "actionsList"}, ["healthThresholds"] = {"enabled", "thresholds", "thickness"}, @@ -1630,7 +1658,7 @@ elseif Cell.isVanilla or Cell.isTBC then ["nameText"] = {"enabled", "color-class", "textWidth", "checkbutton:showGroupNumber", "vehicleNamePosition", "position", "frameLevel", "font-noOffset"}, ["statusText"] = {"enabled", "checkbutton:showTimer", "checkbutton2:showBackground", "statusColors", "statusPosition", "frameLevel", "font-noOffset"}, ["healthText"] = {"|cffff7727"..L["MODERATE CPU USAGE"], "enabled", "healthFormat", "position", "frameLevel", "font-noOffset"}, - ["powerText"] = {"enabled", "color-power", "powerFormat", "powerTextFilters", "checkbutton:hideIfEmptyOrFull", "position", "frameLevel", "font-noOffset"}, + ["powerText"] = {"enabled", "color-power", "powerFormat", "powerTextFilters", "position", "frameLevel", "font-noOffset"}, ["statusIcon"] = { -- "|A:dungeonskull:18:18|a ".. "|TInterface\\LFGFrame\\LFG-Eye:18:18:0:0:512:256:72:120:72:120|t ".. @@ -1653,9 +1681,9 @@ elseif Cell.isVanilla or Cell.isTBC then ["defensiveCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInDefensives", "customDefensives", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, ["allCooldowns"] = {"enabled", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, ["dispels"] = {"enabled", "dispelFilters", "highlightType", "dispelBlacklist", "iconStyle", "orientation", "size-square", "position", "frameLevel"}, - ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "durationVisibility", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, - ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "durationVisibility", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, - ["targetedSpells"] = {"enabled", "checkbutton:showAllSpells:"..L["Glow is only available to the spells in the list below"], "targetedSpellsList", "targetedSpellsGlow", "size-border", "num:3", "orientation", "position", "frameLevel", "font"}, + ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont"}, + ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont"}, + ["targetedSpells"] = {"enabled", "checkbutton:showAllSpells:"..L["Glow is only available to the spells in the list below"], "targetedSpellsDisplayMode", "targetedSpellsList", "targetedSpellsGlow", "size-border", "num:3", "orientation", "position", "frameLevel", "font"}, ["targetCounter"] = {"|cffff2727"..L["HIGH CPU USAGE"].."!|r |cffb7b7b7"..L["Check all visible enemy nameplates."], "enabled", "targetCounterFilters", "color", "position", "frameLevel", "font-noOffset"}, ["actions"] = {"|cffb7b7b7"..L["Play animation when the unit uses a specific spell/item. The list is global shared, not layout-specific."], "enabled", "actionsPreview", "actionsList"}, ["healthThresholds"] = {"enabled", "thresholds", "thickness"}, @@ -1936,6 +1964,15 @@ local function ShowIndicatorSettings(id) Cell.vars.targetedSpellsList = F.ConvertTable(CellDB["targetedSpellsList"]) end) + -- targetedSpellsDisplayMode + elseif currentSetting == "targetedSpellsDisplayMode" then + w:SetDBValue(indicatorTable["displayMode"] or "Both") + w:SetFunc(function(value) + indicatorTable["displayMode"] = value + I.UpdateTargetedSpellsDisplayMode(value) + CellIndicatorsPreviewButton.indicators.targetedSpells:ShowGlowPreview() + end) + -- targetedSpellsGlow elseif currentSetting == "targetedSpellsGlow" then w:SetDBValue(CellDB["targetedSpellsGlow"]) diff --git a/RaidDebuffs/RaidDebuffs_Midnight_skeleton.lua b/RaidDebuffs/RaidDebuffs_Midnight_skeleton.lua deleted file mode 100644 index baee865f..00000000 --- a/RaidDebuffs/RaidDebuffs_Midnight_skeleton.lua +++ /dev/null @@ -1,697 +0,0 @@ ---------------------------------------------------------------------- --- File: RaidDebuffs_Midnight.lua --- Author: enderneko (enderneko-dev@outlook.com) --- Created : 2026-03-09 --- Note: Spell IDs extracted from wago.tools DB2 JournalEncounterSection. --- These are ALL encounter abilities, not just debuffs. --- Spells need in-game verification to confirm which are player debuffs. ---------------------------------------------------------------------- - ----@class Cell -local Cell = select(2, ...) -local F = Cell.funcs - -local debuffs = { - [1299] = { -- Windrunner Spire - ["general"] = { - }, - [2655] = { -- Emberdawn - -- 465904, -- Burning Gale - -- 466556, -- Flaming Updraft - -- 466064, -- Searing Beak - -- 469633, -- Flaming Twisters - -- 467120, -- Ignited Embers - -- 1217762, -- Fire Breath - }, - [2656] = { -- Derelict Duo - -- 472736, -- Debilitating Shriek - -- 474105, -- Curse of Darkness - -- 472724, -- Shadow Bolt - -- 472795, -- Heaving Yank - -- 474075, -- Heaving Chop - -- 472745, -- Splattering Spew - -- 472777, -- Gunk Splatter - -- 472888, -- Bone Hack - -- 1219551, -- Broken Bond - -- 1282272, -- Splattered - -- 1215813, -- Shadowy - }, - [2657] = { -- Commander Kroluk - -- 470963, -- Bladestorm - -- 468070, -- Rallying Bellow - -- 467620, -- Rampage - -- 1217094, -- Throw Axe - -- 472043, -- Rallying Bellow - -- 472081, -- Reckless Leap - -- 1250851, -- Shield Wall - -- 1253026, -- Intimidating Shout - -- 1251981, -- Chain Lightning - -- 467815, -- Intercepting Charge - -- 1270620, -- Flame Nova - -- 1283357, -- Falling Rubble - }, - [2658] = { -- The Restless Heart - -- 1253986, -- Gust Shot - -- 468429, -- Bullseye Windblast - -- 468442, -- Billowing Wind - -- 472556, -- Arrow Rain - -- 1253977, -- Turbulent Arrows - -- 474528, -- Bolt Gale - -- 472662, -- Tempest Slash - -- 1216042, -- Squall Leap - -- 1282932, -- Storming Soulfont - }, - }, - - [1300] = { -- Magisters' Terrace - ["general"] = { - }, - [2659] = { -- Arcanotron Custos - -- 474345, -- Refueling Protocol - -- 474308, -- Energy Orb - -- 474496, -- Repulsing Slam - -- 1214038, -- Ethereal Shackles - -- 1243905, -- Unstable Energy - -- 1214081, -- Arcane Expulsion - -- 474407, -- Arcane Empowerment - -- 1214089, -- Arcane Residue - }, - [2661] = { -- Seranel Sunlash - -- 1224903, -- Suppression Zone - -- 1225135, -- Feedback - -- 1225193, -- Wave of Silence - -- 1225792, -- Runic Mark - -- 1246446, -- Null Reaction - -- 1248689, -- Hastening Ward - }, - [2660] = { -- Gemellus - -- 1223847, -- Triplicate - -- 1223936, -- Synaptic Nexus - -- 1224299, -- Astral Grasp - -- 1224401, -- Cosmic Radiation - -- 1224100, -- Void Secretions - -- 1284958, -- Cosmic Sting - -- 1253707, -- Neural Link - }, - [2662] = { -- Degentrius - -- 1215087, -- Unstable Void Essence - -- 1215161, -- Void Destruction - -- 1214714, -- Void Torrent - -- 1280113, -- Hulking Fragment - -- 1215897, -- Devouring Entropy - -- 1271066, -- Entropy Blast - -- 1269631, -- Entropy Orb - -- 1284627, -- Umbral Splinters - -- 1284628, -- Stygian Ichor - }, - }, - - [1304] = { -- Murder Row - ["general"] = { - }, - [2679] = { -- Kystia Manaheart - -- 1230289, -- Illicit Infusion - -- 1217989, -- Felshield - -- 1223906, -- Fel Nova - -- 1230298, -- Chaos Barrage - -- 1253811, -- Fel Spray - -- 1228198, -- Corroding Spittle - -- 1264095, -- Mirror Images - -- 1264106, -- Felstorm - -- 1230304, -- Light Infusion - -- 1265412, -- Destabilized - }, - [2680] = { -- Zaen Bladesorrow - -- 474478, -- Killing Spree - -- 1218347, -- Murder in a Row - -- 474765, -- Same-Day Delivery - -- 1201553, -- Fel-Infused Freight - -- 1214357, -- Fire Bomb - -- 1222795, -- Envenom - -- 474515, -- Heartstop Poison - -- 1266241, -- Freight Explosion - }, - [2681] = { -- Xathuux the Annihilator - -- 1214663, -- Axe Toss - -- 474197, -- Demonic Rage - -- 474234, -- Burning Steps - -- 473898, -- Legion Strike - -- 1214650, -- Fel Lightning - }, - [2682] = { -- Lithiel Cinderfury - -- 1223204, -- Felfire Burst - -- 474375, -- Chaos Bolt - -- 1214675, -- Demonic Gateway - -- 474457, -- Fingers of Gul'dan - -- 1217384, -- Malefic Wave - -- 1217415, -- Felshield - -- 1226469, -- Malefic Empowerment - -- 1231262, -- Felfire Core - -- 1216945, -- Searing Fel Flame - }, - }, - - [1307] = { -- The Voidspire - ["general"] = { - }, - [2733] = { -- Imperator Averzian - -- 1251361, -- Shadow's Advance - -- 1251583, -- March of the Endless - -- 1249262, -- Umbral Collapse - -- 1260712, -- Oblivion's Wrath - -- 1253918, -- Imperator's Glory - -- 1249251, -- Dark Upheaval - -- 1249714, -- Umbral Barrier - -- 1262036, -- Void Rupture - -- 1265540, -- Blackening Wounds - -- 1255683, -- Gnashing Void - -- 1264164, -- Dark Resilience - -- 1267205, -- Hobbled - -- 1255702, -- Pitch Bulwark - -- 1255749, -- Gathering Darkness - -- 1258883, -- Void Fall - -- 1274846, -- Dark Barrage - -- 1275059, -- Black Miasma - -- 1280035, -- Cosmic Shell - -- 1280015, -- Void Marked - -- 1280075, -- Lingering Darkness - -- 1283069, -- Weakened - -- 1284786, -- Shadow Phalanx - }, - [2734] = { -- Vorasius - -- 1254199, -- Parasite Expulsion - -- 1259186, -- Blisterburst - -- 1241692, -- Shadowclaw Slam - -- 1256855, -- Void Breath - -- 1243270, -- Dark Goo - -- 1241844, -- Smashed - -- 1260052, -- Primordial Roar - -- 1244419, -- Overpowering Pulse - -- 1273067, -- Aftershock - -- 1272937, -- Primordial Power - -- 1272527, -- Creep Spit - -- 1280101, -- Dark Energy - }, - [2736] = { -- Fallen-King Salhadaar - -- 1246175, -- Entropic Unraveling - -- 1250686, -- Twisting Obscurity - -- 1254081, -- Fractured Projection - -- 1247738, -- Void Convergence - -- 1254088, -- Shadow Fracture - -- 1271577, -- Destabilizing Strikes - -- 1260015, -- Umbral Beams - -- 1245960, -- Void Infusion - -- 1250991, -- Dark Radiation - -- 1253032, -- Shattering Twilight - -- 1251213, -- Twilight Spikes - -- 1245592, -- Torturous Extract - -- 1248697, -- Despotic Command - -- 1248709, -- Oppressive Darkness - -- 1275056, -- Nexus Shield - -- 1250828, -- Void Exposure - }, - [2735] = { -- Vaelgor & Ezzorak - -- 1244221, -- Dread Breath - -- 1262623, -- Nullbeam - -- 1244672, -- Nullzone - -- 1244917, -- Void Howl - -- 1245175, -- Voidbolt - -- 1245391, -- Gloom - -- 1245420, -- Gloomfield - -- 1245554, -- Gloomtouched - -- 1249748, -- Midnight Flames - -- 1245645, -- Rakfang - -- 1248847, -- Radiant Barrier - -- 1272867, -- Aura of Light - -- 1244413, -- Nullsnap - -- 1252157, -- Nullzone Implosion - -- 1255763, -- Midnight Manifestation - -- 1251686, -- Unbound Shadow - -- 1265152, -- Impale - -- 1280458, -- Grappling Maw - -- 1265131, -- Vaelwing - -- 1264467, -- Tail Lash - -- 1266570, -- Nullscatter - -- 1263623, -- Cosmosis - -- 1270189, -- Twilight Bond - -- 1270250, -- Twilight Fury - -- 1270852, -- Diminish - -- 1270513, -- Shadowmark - }, - [2737] = { -- Lightblinded Vanguard - -- 1246162, -- Aura of Devotion - -- 1251857, -- Judgment - -- 1246485, -- Avenger's Shield - -- 1248644, -- Divine Toll - -- 1248449, -- Aura of Wrath - -- 1246736, -- Judgment - -- 1246765, -- Divine Storm - -- 1246749, -- Sacred Toll - -- 1248983, -- Execution Sentence - -- 1248451, -- Aura of Peace - -- 1246745, -- Exorcism - -- 1248674, -- Sacred Shield - -- 1248710, -- Tyr's Wrath - -- 1251859, -- Shield of the Righteous - -- 1251812, -- Final Verdict - -- 1246155, -- Consecration - -- 1256133, -- Retribution - -- 1255738, -- Searing Radiance - -- 1258659, -- Light Infused - -- 1246391, -- Forbearance - -- 1276243, -- Zealous Spirit - -- 1272324, -- Divine Tempest - -- 1272471, -- Spirit of the Mender - -- 1272700, -- Spirit of the Defender - -- 1272699, -- Spirit of the Vindictive - -- 1276982, -- Divine Consecration - -- 1246385, -- Avenging Wrath - -- 1246384, -- Divine Shield - -- 1258514, -- Blinding Light - -- 1249047, -- Divine Hammer - -- 1280159, -- Execution Sentence - -- 1249130, -- Elekk Charge - }, - [2738] = { -- Crown of the Cosmos - -- 1239080, -- Aspect of the End - -- 1232470, -- Grasp of Emptiness - -- 1233865, -- Null Corona - -- 1237251, -- Empowering Darkness - -- 1233602, -- Silverstrike Arrow - -- 1243982, -- Silverstrike Barrage - -- 1234569, -- Stellar Emission - -- 1237614, -- Ranger Captain's Mark - -- 1237729, -- Silverstrike Ricochet - -- 1237038, -- Voidstalker Sting - -- 1256787, -- Call of the Void - -- 1233470, -- Umbral Tether - -- 1232784, -- Bursting Emptiness - -- 1261531, -- Corrupting Essence - -- 1233689, -- Silver Residue - -- 1233778, -- Echoing Darkness - -- 1233787, -- Dark Hand - -- 1238843, -- Devouring Cosmos - -- 1260000, -- Void Barrage - -- 1238206, -- Volatile Fissure - -- 1238708, -- Dark Rush - -- 1239089, -- Gravity Collapse - -- 1239279, -- Echoing Darkness - -- 1243743, -- Interrupting Tremor - -- 1243753, -- Ravenous Abyss - -- 1234564, -- Silverstrike Barrage - -- 1235622, -- Singularity Eruption - -- 1245874, -- Orbiting Matter - -- 1246461, -- Rift Slash - -- 1246918, -- Cosmic Barrier - -- 1255368, -- Void Expulsion - -- 1242553, -- Void Remnants - -- 1232467, -- Grasp of Emptiness - -- 1237837, -- Call of the Void - -- 1237844, -- Umbral Tether - -- 1238672, -- Coalesced Form - -- 1233526, -- Corrupting Essence - -- 1255378, -- Bursting Emptiness - -- 1261165, -- Empowering Darkness - }, - }, - - [1308] = { -- March on Quel'Danas - ["general"] = { - }, - [2739] = { -- Belo'ren, Child of Al'ar - -- 1242792, -- Incubation of Flames - -- 1241313, -- Rebirth - -- 1241282, -- Embers of Belo'ren - -- 1242260, -- Infused Quills - -- 1242981, -- Radiant Echoes - -- 1242515, -- Voidlight Convergence - -- 1241162, -- Light Feather - -- 1241163, -- Void Feather - -- 1241292, -- Light Dive - -- 1243852, -- Light Eruption - -- 1241339, -- Void Dive - -- 1243854, -- Void Eruption - -- 1242093, -- Light Quill - -- 1242094, -- Void Quill - -- 1243021, -- Light Echo - -- 1243026, -- Void Echo - -- 1243866, -- Voidlight Rupture - -- 1246709, -- Death Drop - -- 1260763, -- Guardian's Edict - -- 1261217, -- Light Edict - -- 1261218, -- Void Edict - -- 1283067, -- Burning Heart - -- 1244344, -- Eternal Burns - -- 1241640, -- Voidlight Edict - -- 1241838, -- Light Patch - -- 1241845, -- Void Patch - -- 1263412, -- Rebirth - -- 1264696, -- Light Blast - -- 1264698, -- Void Blast - -- 1244348, -- Light Burn - -- 1266404, -- Void Burn - -- 1262573, -- Ashen Benediction - -- 1243320, -- Immortal Flame - -- 1242803, -- Light Flames - -- 1242815, -- Void Flames - }, - [2740] = { -- Midnight Falls - -- 1273158, -- Death's Requiem - -- 1249609, -- Dark Rune - -- 1249584, -- Dissonance - -- 1249796, -- Shattered Sky - -- 1284931, -- Termination Prism - -- 1284934, -- Terminate - -- 1250898, -- The Dark Archangel - -- 1251649, -- Disintegration - -- 1251789, -- Cosmic Fracture - -- 1266388, -- Dark Constellation - -- 1252974, -- Dimming - -- 1251807, -- Cosmic Fracture - -- 1253915, -- Heaven's Glaives - -- 1263970, -- Heaven's Lance - -- 1282027, -- The Darkwell - -- 1254642, -- Thunderous Well - -- 1279463, -- Iris of Oblivion - -- 1281194, -- Dark Meltdown - -- 1284699, -- Light's End - -- 1254398, -- Glimmering - -- 1254262, -- Tears of L'ura - -- 1254256, -- Naaru's Lament - -- 1265842, -- Impaled - -- 1263253, -- Black Tide - -- 1266897, -- Light Siphon - -- 1266898, -- Stellar Implosion - -- 1249582, -- Resonance - -- 1244412, -- Death's Dirge - -- 1274455, -- Severance - -- 1276529, -- Dimension Breach - -- 1276062, -- Dimension Link - -- 1260261, -- Total Eclipse - -- 1282441, -- Starsplinter - -- 1285561, -- Dark Quasar - -- 1282034, -- Into the Darkwell - -- 1282008, -- Abyssal Pool - -- 1282412, -- Core Harvest - -- 1266622, -- Midnight - -- 1266113, -- Torchbearer - -- 1284525, -- Galvanize - -- 1282246, -- Void Cores - -- 1282249, -- Cosmic Fission - -- 1284638, -- Decay - -- 1282373, -- Charged Core - -- 1282458, -- Radiance - -- 1285827, -- Overkill Current - -- 1281184, -- Criticality - -- 1251386, -- Safeguard Prism - -- 1251392, -- Safeguard - -- 1284980, -- Grim Symphony - -- 1279420, -- Dark Quasar - -- 1262055, -- Eclipsed - -- 1285685, -- Black Shroud - -- 1253104, -- Dawnlight Barrier - -- 1287702, -- Severed Surge - }, - }, - - [1314] = { -- The Dreamrift - ["general"] = { - }, - [2795] = { -- Chimaerus the Undreamt God - -- 1262289, -- Alndust Upheaval - -- 1245486, -- Corrupted Devastation - -- 1245698, -- Alnsight - -- 1245406, -- Ravenous Dive - -- 1245844, -- Cannibalized Essence - -- 1245919, -- Alndust Essence - -- 1246132, -- Rift Shroud - -- 1272726, -- Rending Tear - -- 1249017, -- Fearsome Cry - -- 1249207, -- Discordant Roar - -- 1250953, -- Rift Sickness - -- 1252863, -- Insatiable - -- 1246653, -- Caustic Phlegm - -- 1257087, -- Consuming Miasma - -- 1253744, -- Rift Vulnerability - -- 1257093, -- Lingering Miasma - -- 1258610, -- Rift Emergence - -- 1261997, -- Essence Bolt - -- 1262020, -- Colossal Strikes - -- 1245727, -- Alnshroud - -- 1246621, -- Caustic Phlegm - -- 1257085, -- Consuming Miasma - -- 1267201, -- Dissonance - -- 1264756, -- Rift Madness - -- 1245396, -- Consume - -- 1282001, -- Alndust Upheaval - }, - }, - - [1309] = { -- The Blinding Vale - ["general"] = { - }, - [2769] = { -- Lightblossom Trinity - -- 1234753, -- Bedrock Slam - -- 1234782, -- Fertile Loam - -- 1234850, -- Lightsower Dash - -- 1235640, -- Thornblade - -- 1235564, -- Lightblossom Beam - -- 1235751, -- Lightbloom Overgrowth - -- 1235814, -- Light-Scorched Earth - -- 1235616, -- Light Bolt - -- 1235729, -- Light-Gorged - -- 1253028, -- Thicket's Trinity - -- 1261011, -- Fan Of Thorns - -- 1276586, -- Bedrock Surge - }, - [2770] = { -- Ikuzz the Light Hunter - -- 1236658, -- Bloodthorn Roots - -- 1236746, -- Verdant Stomp - -- 1236709, -- Thorncaller Roar - -- 1237090, -- Bloodthirsty Gaze - -- 1237093, -- Crushing Footfalls - -- 1237166, -- Incise - -- 1237073, -- Lightcrazed Frenzy - -- 1272290, -- Crunched - }, - [2771] = { -- Lightwarden Ruia - -- 1239824, -- Lightfire - -- 1239830, -- Lightfire Beams - -- 1240098, -- Lightfall - -- 1239821, -- Warden's Wrath - -- 1240210, -- Pulverizing Strikes - -- 1241058, -- Grievous Thrash - -- 1241067, -- Spirits of the Vale - -- 1257094, -- Pulverized - -- 1272265, -- Mangling Claws - }, - [2772] = { -- Ziekket - -- 1246372, -- Awaken the Lightbloom - -- 1247669, -- Lightspore Shot - -- 1246379, -- Dormant - -- 1246607, -- Concentrated Lightbeam - -- 1246660, -- Lightsap - -- 1246858, -- Lightbloom's Essence - -- 1247039, -- Fluorescent Outburst - -- 1247052, -- Lightbloom's Might - -- 1247685, -- Thornspike - -- 1247377, -- Oozing Xylem - -- 1247050, -- Fluorescent Shield - -- 1253320, -- Vicious Regrowth - }, - }, - - [1311] = { -- Den of Nalorakk - ["general"] = { - }, - [2776] = { -- The Hoardmonger - -- 1235072, -- Resourceful Measures - -- 1235125, -- Hearty Bellow - -- 1235129, -- Bonespike Slam - -- 1235405, -- Bonespiked - -- 1235105, -- Overflowing Supplies - -- 1234233, -- Spoiled Supplies - -- 1234846, -- Toxic Spores - -- 1245593, -- Putrid Burst - -- 1234021, -- Earthshatter Slam - -- 1234681, -- Ravenous Bellow - }, - [2777] = { -- Sentinel of Winter - -- 1235783, -- Shattering Frostspike - -- 1235829, -- Winter's Shroud - -- 1234314, -- Snowdrift - -- 1235656, -- Eternal Winter - -- 1235623, -- Raging Squall - -- 1235548, -- Glacial Torment - -- 1263590, -- Rimeshatter - -- 1263597, -- Rime Detonation - }, - [2778] = { -- Nalorakk - -- 1243002, -- Fury of the War God - -- 1243408, -- Echoing Fury - -- 1242860, -- Echoing Maul - -- 1255385, -- Forceful Roar - -- 1243585, -- Overwhelming Onslaught - -- 1262253, -- Demoralizing Scream - -- 1243063, -- Concussive Shock - -- 1255577, -- Spectral Slash - -- 1261776, -- Defensive Stance - }, - }, - - [1312] = { -- Midnight - ["general"] = { - }, - [2827] = { -- Lu'ashal - -- 1276436, -- Dawncrazed Halo - -- 1276247, -- Dawnfire Breath - -- 1243963, -- Radiant Sunder - -- 1243988, -- Blinding Fissure - -- 1258427, -- Radiant Flare - -- 1258426, -- Radiant Ember - }, - [2829] = { -- Thorm'belan - -- 1257825, -- Scintillating Shard - -- 1257320, -- Radiant Mote - -- 1257737, -- Shard Eruption - -- 1258136, -- Rending Claw - -- 1257618, -- Dazzling Radiance - -- 1258639, -- Shredding Tendrils - }, - [2828] = { -- Predaxas - -- 1276193, -- Regurgitation - -- 1276320, -- Seismic Slam - -- 1276884, -- Voidscatter - -- 1277043, -- Bilepool - -- 1276988, -- Toxin Splatter - -- 1277711, -- Bestial Rage - -- 1277694, -- Blood Nova - -- 1277829, -- Devour - }, - [2782] = { -- Cragpine - -- 1235144, -- War Club - -- 1257906, -- Ancient Seeds - -- 1235131, -- Rootquake - -- 1235134, -- Erupting Roots - }, - }, - - [1313] = { -- Voidscar Arena - ["general"] = { - }, - [2791] = { -- Taz'Rah - -- 1222199, -- Dark Rift - -- 1222098, -- Nether Dash - -- 1222085, -- Cosmic Spike - -- 1225107, -- Ethereal Shards - -- 1263593, -- Gather Shadows - }, - [2792] = { -- Atroxus - -- 1222724, -- Noxious Breath - -- 1222642, -- Hulking Claw - -- 1226031, -- Poison Splash - -- 1222692, -- Toxic Aura - -- 1262497, -- Monstrous Stomp - -- 1222484, -- Poison Pool - -- 1263971, -- Lingering Poison - -- 1222371, -- Provoke Creeper - -- 1282892, -- Sickening Bite - -- 1283506, -- Fixate - }, - [2793] = { -- Charonus - -- 1248130, -- Unstable Singularity - -- 1227197, -- Cosmic Blast - -- 1222755, -- Void Cascade - -- 1223298, -- Gravitic Orbs - -- 1263983, -- Condensed Mass - }, - }, - - [1315] = { -- Maisara Caverns - ["general"] = { - }, - [2810] = { -- Muro'jin and Nekraxx - -- 1246666, -- Infected Pinions - -- 1249789, -- Revive Pet - -- 1243900, -- Fetid Quillstorm - -- 1260731, -- Freezing Trap - -- 1249479, -- Carrion Swoop - -- 1249769, -- Coordinated Assault - -- 1249948, -- Bestial Wrath - -- 1266480, -- Flanking Spear - -- 1260643, -- Barrage - -- 1260709, -- Vilebranch Sting - -- 1243751, -- Icy Slick - -- 1266488, -- Open Wound - }, - [2811] = { -- Vordaza - -- 1251554, -- Drain Soul - -- 1250708, -- Necrotic Convergence - -- 1251204, -- Wrest Phantoms - -- 1251775, -- Final Pursuit - -- 1251833, -- Soulrot - -- 1251813, -- Lingering Dread - -- 1252054, -- Unmake - -- 1251598, -- Deathshroud - -- 1252611, -- Coalesced Death - -- 1264974, -- Veiled Presence - -- 1264987, -- Withering Miasma - -- 1266706, -- Haunting Remains - }, - [2812] = { -- Rak'tul, Vessel of Souls - -- 1252676, -- Crush Souls - -- 1252777, -- Soulbind - -- 1252816, -- Chill of Death - -- 1251023, -- Spiritbreaker - -- 1248863, -- Deathgorged Vessel - -- 1248980, -- Volatile Essence - -- 1253788, -- Soulrending Roar - -- 1253844, -- Withering Soul - -- 1254175, -- Cries of the Fallen - -- 1254010, -- Eternal Suffering - -- 1255629, -- Spectral Residue - -- 1259810, -- Shattered Totem - -- 1253909, -- Soul Expulsion - -- 1266723, -- Spectral Decay - }, - }, - - [1316] = { -- Nexus-Point Xenas - ["general"] = { - }, - [2813] = { -- Chief Corewright Kasreth - -- 1250553, -- Arcane Zap - -- 1251767, -- Reflux Charge - -- 1257509, -- Corespark Detonation - -- 1264040, -- Flux Collapse - -- 1264042, -- Arcane Spill - -- 1251579, -- Leyline Array - -- 1276485, -- Sparkburn - }, - [2814] = { -- Corewarden Nysarra - -- 1247976, -- Lightscar Flare - -- 1247937, -- Umbral Lash - -- 1249014, -- Eclipsing Step - -- 1282723, -- Dusk Frights - -- 1282665, -- Void Lash - -- 1252828, -- Void Gash - -- 1252883, -- Devour the Unworthy - -- 1253965, -- Lightscarred - -- 1282679, -- Flailstorm - -- 1282722, -- Nullify - -- 1252703, -- Null Vanguard - }, - [2815] = { -- Lothraxion - -- 1253848, -- Brilliant Dispersion - -- 1253950, -- Searing Rend - -- 1255531, -- Flicker - -- 1255389, -- Radiant Scar - -- 1266713, -- Mirrored Rend - -- 1257613, -- Divine Guile - -- 1271511, -- Core Exposure - }, - }, - -} - -F.LoadBuiltInDebuffs(debuffs) \ No newline at end of file diff --git a/Revise.lua b/Revise.lua index 9f4c1f31..292f0433 100644 --- a/Revise.lua +++ b/Revise.lua @@ -583,7 +583,7 @@ function F.Revise() -- r49-release if CellDB["revise"] and dbRevision < 49 then if type(CellDB["appearance"]["barAnimation"]) ~= "string" then - CellDB["appearance"]["barAnimation"] = "Flash" + CellDB["appearance"]["barAnimation"] = "Smooth" end end @@ -3419,6 +3419,10 @@ function F.Revise() if CellDB["general"] then CellDB["general"]["useCleuHealthUpdater"] = nil end + -- Migrate "Flash" bar animation to "Smooth" (Flash removed in 12.0.0) + if CellDB["appearance"] and CellDB["appearance"]["barAnimation"] == "Flash" then + CellDB["appearance"]["barAnimation"] = "Smooth" + end -- Note: profile import compatibility warning added elsewhere. -- Saved variable secrets: any secrets stored before this version will be nil'd by WoW. end diff --git a/Widgets/Widgets_IndicatorSettings.lua b/Widgets/Widgets_IndicatorSettings.lua index f3f20472..649320f1 100644 --- a/Widgets/Widgets_IndicatorSettings.lua +++ b/Widgets/Widgets_IndicatorSettings.lua @@ -1043,14 +1043,12 @@ local function CreateSetting_HealthFormat(parent) local function UpdateWidgets() local health1Enabled = widget.format.health1.format ~= "none" - widget.health1HideIfEmptyOrFullCB:SetEnabled(health1Enabled) widget.health1ColorDropdown:SetEnabled(health1Enabled) widget.health1ColorPicker:SetEnabled(health1Enabled) local health2Enabled = widget.format.health2.format ~= "none" widget.health2DelimiterEB:SetEnabled(health2Enabled) widget.health2DelimiterEB.confirmBtn:Hide() - widget.health2HideIfEmptyOrFullCB:SetEnabled(health2Enabled) widget.health2ColorDropdown:SetEnabled(health2Enabled) widget.health2ColorPicker:SetEnabled(health2Enabled) if health2Enabled then @@ -1124,14 +1122,8 @@ local function CreateSetting_HealthFormat(parent) health1Text:SetPoint("BOTTOMLEFT", widget.health1FormatDropdown, "TOPLEFT", 0, 1) health1Text:SetText(L["Health"] .. " 1") - widget.health1HideIfEmptyOrFullCB = Cell.CreateCheckButton(widget, L["hideIfEmptyOrFull"], function(checked) - widget.format.health1.hideIfEmptyOrFull = checked - widget.func() - end) - widget.health1HideIfEmptyOrFullCB:SetPoint("TOPLEFT", widget.health1FormatDropdown, "BOTTOMLEFT", 0, -10) - widget.health1ColorDropdown = Cell.CreateDropdown(widget, 127) - widget.health1ColorDropdown:SetPoint("TOPLEFT", widget.health1HideIfEmptyOrFullCB, "BOTTOMLEFT", 0, -10) + widget.health1ColorDropdown:SetPoint("TOPLEFT", widget.health1FormatDropdown, "BOTTOMLEFT", 0, -10) widget.health1ColorDropdown:SetItems({ { ["text"] = L["Class Color"], @@ -1182,14 +1174,8 @@ local function CreateSetting_HealthFormat(parent) widget.health2DelimiterText:SetPoint("BOTTOMLEFT", widget.health2DelimiterEB, "TOPLEFT", 0, 1) widget.health2DelimiterText:SetText(L["Delimiter"]) - widget.health2HideIfEmptyOrFullCB = Cell.CreateCheckButton(widget, L["hideIfEmptyOrFull"], function(checked) - widget.format.health2.hideIfEmptyOrFull = checked - widget.func() - end) - widget.health2HideIfEmptyOrFullCB:SetPoint("TOPLEFT", widget.health2FormatDropdown, "BOTTOMLEFT", 0, -10) - widget.health2ColorDropdown = Cell.CreateDropdown(widget, 127) - widget.health2ColorDropdown:SetPoint("TOPLEFT", widget.health2HideIfEmptyOrFullCB, "BOTTOMLEFT", 0, -10) + widget.health2ColorDropdown:SetPoint("TOPLEFT", widget.health2FormatDropdown, "BOTTOMLEFT", 0, -10) widget.health2ColorDropdown:SetItems({ { ["text"] = L["Class Color"], @@ -1355,14 +1341,12 @@ local function CreateSetting_HealthFormat(parent) -- health1 widget.health1FormatDropdown:SetSelectedValue(format.health1.format) - widget.health1HideIfEmptyOrFullCB:SetChecked(format.health1.hideIfEmptyOrFull) widget.health1ColorDropdown:SetSelectedValue(format.health1.color[1]) widget.health1ColorPicker:SetColor(unpack(format.health1.color[2])) -- health2 widget.health2FormatDropdown:SetSelectedValue(format.health2.format) widget.health2DelimiterEB:SetText(format.health2.delimiter) - widget.health2HideIfEmptyOrFullCB:SetChecked(format.health2.hideIfEmptyOrFull) widget.health2ColorDropdown:SetSelectedValue(format.health2.color[1]) widget.health2ColorPicker:SetColor(unpack(format.health2.color[2])) @@ -6487,6 +6471,58 @@ local function CreateSetting_IconStyle(parent) return widget end +local function CreateSetting_TargetedSpellsDisplayMode(parent) + local widget + + if not settingWidgets["targetedSpellsDisplayMode"] then + widget = Cell.CreateFrame("CellIndicatorSettings_TargetedSpellsDisplayMode", parent, 240, 50) + settingWidgets["targetedSpellsDisplayMode"] = widget + + widget.dropdown = Cell.CreateDropdown(widget, 245) + widget.dropdown:SetPoint("TOPLEFT", 5, -20) + widget.dropdown:SetItems({ + { + ["text"] = L["Icons"], + ["value"] = "Icons", + ["onClick"] = function() + widget.func("Icons") + end, + }, + { + ["text"] = L["Border"], + ["value"] = "Border", + ["onClick"] = function() + widget.func("Border") + end, + }, + { + ["text"] = L["Both"], + ["value"] = "Both", + ["onClick"] = function() + widget.func("Both") + end, + }, + }) + + widget.label = widget:CreateFontString(nil, "OVERLAY", font_name) + widget.label:SetText(L["Display Mode"]) + widget.label:SetPoint("BOTTOMLEFT", widget.dropdown, "TOPLEFT", 0, 1) + + function widget:SetFunc(func) + widget.func = func + end + + function widget:SetDBValue(value) + widget.dropdown:SetSelectedValue(value or "Both") + end + else + widget = settingWidgets["targetedSpellsDisplayMode"] + end + + widget:Show() + return widget +end + local CLASS_ROLES = { ["DEATHKNIGHT"] = {"TANK", "DAMAGER"}, ["DEMONHUNTER"] = {"TANK", "DAMAGER"}, @@ -6796,6 +6832,7 @@ local builders = { -- ["showOn"] = CreateSetting_ShowOn, ["maxValue"] = CreateSetting_MaxValue, ["iconStyle"] = CreateSetting_IconStyle, + ["targetedSpellsDisplayMode"] = CreateSetting_TargetedSpellsDisplayMode, ["powerTextFilters"] = CreateSetting_RoleFilters, } From 731cbccfb0ddcf328bd40e4c122795dd3e76bb87 Mon Sep 17 00:00:00 2001 From: jdtoppin <6392799+jdtoppin@users.noreply.github.com> Date: Sun, 22 Mar 2026 17:18:43 -0600 Subject: [PATCH 07/61] feat: enable Blizzard countdown text on Midnight BorderIcon indicators SetCooldownFromDurationObject drives a CooldownFrame that supports built-in countdown numbers. Enable them with SetHideCountdownNumbers(false) and size the FontString to match Cell's duration font settings. Limitations: anchor point, offset, and color are controlled by Blizzard's C-level rendering and cannot be customized via Cell settings. TODO: Fix preview pane to show countdown text. Co-Authored-By: Claude Opus 4.6 (1M context) --- Indicators/Base.lua | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Indicators/Base.lua b/Indicators/Base.lua index 1ba073bc..394f7993 100644 --- a/Indicators/Base.lua +++ b/Indicators/Base.lua @@ -90,6 +90,8 @@ end local function Shared_SetFont(frame, font1, font2) I.SetFont(frame.stack, frame, unpack(font1)) I.SetFont(frame.duration, frame, unpack(font2)) + -- Store duration font config for Midnight countdown text on CooldownFrame + frame._durationFont = font2 end local function Shared_ShowStack(frame, show) @@ -461,8 +463,29 @@ local function BorderIcon_SetCooldownFromAura(frame, unit, auraInstanceID, textu local durObj = _GetAuraDuration and _GetAuraDuration(unit, auraInstanceID) if durObj and frame.cooldown and frame.cooldown._SetCooldown and frame.cooldown.SetCooldownFromDurationObject then + -- Enable Blizzard's countdown text before setting cooldown so the FontString is created + frame.cooldown:SetHideCountdownNumbers(false) frame.cooldown:SetReverse(true) frame.cooldown:SetCooldownFromDurationObject(durObj, true) + -- Size the countdown text to fit Cell's small frames + local cdText = frame.cooldown:GetCountdownFontString() + if cdText then + -- Re-parent to iconFrame so text renders above the icon + cdText:SetParent(frame.iconFrame) + local df = frame._durationFont + if df then + local fontFace = F.GetFont(df[1]) or cdText:GetFont() + local fontSize = df[2] or 11 + local outline = df[3] + local flags = outline == "Outline" and "OUTLINE" + or outline == "Monochrome" and "OUTLINE,MONOCHROME" + or "" + cdText:SetFont(fontFace, fontSize, flags) + else + local fontFace = cdText:GetFont() + cdText:SetFont(fontFace, 11, "OUTLINE") + end + end -- Keep border visible as base color (caller sets color); black swipe fills over it frame.cooldown:Show() else From 5b0755400383ac61cc965ed219286a05824cf621 Mon Sep 17 00:00:00 2001 From: jdtoppin <6392799+jdtoppin@users.noreply.github.com> Date: Sun, 22 Mar 2026 17:25:13 -0600 Subject: [PATCH 08/61] feat: enable countdown text on Midnight BorderIcon previews Preview pane now shows Blizzard countdown numbers matching in-game display. Uses same font sizing as the frame countdown text. TODO: Live-update preview countdown font when settings change. Co-Authored-By: Claude Opus 4.6 (1M context) --- Modules/Indicators/Indicators.lua | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/Modules/Indicators/Indicators.lua b/Modules/Indicators/Indicators.lua index 87b8ace6..68268acf 100644 --- a/Modules/Indicators/Indicators.lua +++ b/Modules/Indicators/Indicators.lua @@ -132,26 +132,52 @@ local function SetOnUpdate(indicator, type, icon, stack, extra) -- is visible as base and black fills in (matches in-game SetCooldownFromAura). local isMidnightBorderIcon = Cell.isMidnight and indicator.cooldown and indicator.cooldown._SetCooldown and not indicator.cooldown.SetMinMaxValues + -- Enable countdown text on Midnight BorderIcon previews + local function enableCountdownText(cd) + if not cd then return end + cd:SetHideCountdownNumbers(false) + local cdText = cd:GetCountdownFontString() + if cdText then + local df = indicator._durationFont + if df then + local fontFace = F.GetFont(df[1]) or cdText:GetFont() + local fontSize = df[2] or 11 + local outline = df[3] + local flags = outline == "Outline" and "OUTLINE" + or outline == "Monochrome" and "OUTLINE,MONOCHROME" + or "" + cdText:SetFont(fontFace, fontSize, flags) + cdText:SetParent(indicator.iconFrame) + else + local fontFace = cdText:GetFont() + cdText:SetFont(fontFace, 11, "OUTLINE") + cdText:SetParent(indicator.iconFrame) + end + end + end + local function doPreview() if isMidnightBorderIcon and not type then -- Buff cooldowns (no debuff type): yellow border base, black swipe fills in indicator.icon:SetTexture(icon) indicator.stack:SetText(stack and stack > 1 and stack or "") - -- Yellow border as base color if indicator.border then indicator.border:SetColorTexture(1, 0.85, 0) indicator.border:Show() end - -- Black swipe fills IN (reverse) over the yellow border if indicator.cooldown then indicator.cooldown:SetReverse(true) indicator.cooldown:SetSwipeColor(0, 0, 0) indicator.cooldown:_SetCooldown(GetTime(), 13) indicator.cooldown:Show() + enableCountdownText(indicator.cooldown) end indicator:Show() else indicator:SetCooldown(GetTime(), 13, type, icon, stack or 0, false, extra) + if isMidnightBorderIcon and indicator.cooldown then + enableCountdownText(indicator.cooldown) + end end end indicator.preview:SetScript("OnUpdate", function(self, elapsed) From 3760ebba09c50148540cec620a47823a0915a3e2 Mon Sep 17 00:00:00 2001 From: jdtoppin <6392799+jdtoppin@users.noreply.github.com> Date: Sun, 22 Mar 2026 18:00:40 -0600 Subject: [PATCH 09/61] feat: countdown text settings, preview fixes, raid debuff colors - Re-add durationVisibility and durationFont settings to debuffs and raidDebuffs - Shared_SetFont live-updates countdown FontString when font settings change - Shared_ShowDuration toggles SetHideCountdownNumbers for Midnight - ApplyCountdownFont guards for CooldownFrame vs StatusBar (BarIcon) - SetCountdownAbbrevThreshold(60) abbreviates above 60s (shows "1m", "2m") - Preview: hide Cell duration text, show Blizzard countdown, no swipe override - Raid debuffs: colored border base + black swipe (matches regular debuffs) Co-Authored-By: Claude Opus 4.6 (1M context) --- Indicators/Base.lua | 61 +++++++++++++++++++---------- Modules/Indicators/Indicators.lua | 64 +++++++++++++++---------------- RaidFrames/UnitButton.lua | 2 + 3 files changed, 73 insertions(+), 54 deletions(-) diff --git a/Indicators/Base.lua b/Indicators/Base.lua index 394f7993..8d2dcd6a 100644 --- a/Indicators/Base.lua +++ b/Indicators/Base.lua @@ -87,11 +87,40 @@ end ------------------------------------------------- -- Shared ------------------------------------------------- +-- Apply font settings to Blizzard's CooldownFrame countdown text (Midnight) +-- Only applies to CooldownFrame cooldowns (BorderIcon), not StatusBar (BarIcon) +local function ApplyCountdownFont(frame, font2) + if not frame.cooldown then return end + if not frame.cooldown.GetCountdownFontString then return end + local cdText = frame.cooldown:GetCountdownFontString() + if not cdText then return end + -- Re-parent once so text renders above icon (iconFrame is above cooldown) + if frame.iconFrame and cdText:GetParent() ~= frame.iconFrame then + cdText:SetParent(frame.iconFrame) + end + if font2 then + local fontFace = F.GetFont(font2[1]) or cdText:GetFont() + local fontSize = font2[2] or 11 + local outline = font2[3] + local flags = outline == "Outline" and "OUTLINE" + or outline == "Monochrome" and "OUTLINE,MONOCHROME" + or "" + cdText:SetFont(fontFace, fontSize, flags) + else + local fontFace = cdText:GetFont() + cdText:SetFont(fontFace, 11, "OUTLINE") + end +end + local function Shared_SetFont(frame, font1, font2) I.SetFont(frame.stack, frame, unpack(font1)) I.SetFont(frame.duration, frame, unpack(font2)) -- Store duration font config for Midnight countdown text on CooldownFrame frame._durationFont = font2 + -- Live-update countdown FontString if it exists + if Cell.isMidnight then + ApplyCountdownFont(frame, font2) + end end local function Shared_ShowStack(frame, show) @@ -101,6 +130,10 @@ end local function Shared_ShowDuration(frame, show) frame.showDuration = show frame.duration:SetShown(show) + -- Toggle Blizzard's countdown text on Midnight BorderIcon + if Cell.isMidnight and frame.cooldown and frame.cooldown.SetHideCountdownNumbers then + frame.cooldown:SetHideCountdownNumbers(not show) + end end ------------------------------------------------- @@ -463,29 +496,15 @@ local function BorderIcon_SetCooldownFromAura(frame, unit, auraInstanceID, textu local durObj = _GetAuraDuration and _GetAuraDuration(unit, auraInstanceID) if durObj and frame.cooldown and frame.cooldown._SetCooldown and frame.cooldown.SetCooldownFromDurationObject then - -- Enable Blizzard's countdown text before setting cooldown so the FontString is created - frame.cooldown:SetHideCountdownNumbers(false) + -- Enable Blizzard's countdown text (respects showDuration setting) + if frame.showDuration then + frame.cooldown:SetHideCountdownNumbers(false) + frame.cooldown:SetCountdownAbbrevThreshold(60) + end frame.cooldown:SetReverse(true) frame.cooldown:SetCooldownFromDurationObject(durObj, true) - -- Size the countdown text to fit Cell's small frames - local cdText = frame.cooldown:GetCountdownFontString() - if cdText then - -- Re-parent to iconFrame so text renders above the icon - cdText:SetParent(frame.iconFrame) - local df = frame._durationFont - if df then - local fontFace = F.GetFont(df[1]) or cdText:GetFont() - local fontSize = df[2] or 11 - local outline = df[3] - local flags = outline == "Outline" and "OUTLINE" - or outline == "Monochrome" and "OUTLINE,MONOCHROME" - or "" - cdText:SetFont(fontFace, fontSize, flags) - else - local fontFace = cdText:GetFont() - cdText:SetFont(fontFace, 11, "OUTLINE") - end - end + -- Apply font settings to countdown text + ApplyCountdownFont(frame, frame._durationFont) -- Keep border visible as base color (caller sets color); black swipe fills over it frame.cooldown:Show() else diff --git a/Modules/Indicators/Indicators.lua b/Modules/Indicators/Indicators.lua index 68268acf..f5ff5124 100644 --- a/Modules/Indicators/Indicators.lua +++ b/Modules/Indicators/Indicators.lua @@ -132,30 +132,6 @@ local function SetOnUpdate(indicator, type, icon, stack, extra) -- is visible as base and black fills in (matches in-game SetCooldownFromAura). local isMidnightBorderIcon = Cell.isMidnight and indicator.cooldown and indicator.cooldown._SetCooldown and not indicator.cooldown.SetMinMaxValues - -- Enable countdown text on Midnight BorderIcon previews - local function enableCountdownText(cd) - if not cd then return end - cd:SetHideCountdownNumbers(false) - local cdText = cd:GetCountdownFontString() - if cdText then - local df = indicator._durationFont - if df then - local fontFace = F.GetFont(df[1]) or cdText:GetFont() - local fontSize = df[2] or 11 - local outline = df[3] - local flags = outline == "Outline" and "OUTLINE" - or outline == "Monochrome" and "OUTLINE,MONOCHROME" - or "" - cdText:SetFont(fontFace, fontSize, flags) - cdText:SetParent(indicator.iconFrame) - else - local fontFace = cdText:GetFont() - cdText:SetFont(fontFace, 11, "OUTLINE") - cdText:SetParent(indicator.iconFrame) - end - end - end - local function doPreview() if isMidnightBorderIcon and not type then -- Buff cooldowns (no debuff type): yellow border base, black swipe fills in @@ -168,15 +144,23 @@ local function SetOnUpdate(indicator, type, icon, stack, extra) if indicator.cooldown then indicator.cooldown:SetReverse(true) indicator.cooldown:SetSwipeColor(0, 0, 0) + if indicator.showDuration then + indicator.cooldown:SetHideCountdownNumbers(false) + end indicator.cooldown:_SetCooldown(GetTime(), 13) indicator.cooldown:Show() - enableCountdownText(indicator.cooldown) end indicator:Show() else indicator:SetCooldown(GetTime(), 13, type, icon, stack or 0, false, extra) - if isMidnightBorderIcon and indicator.cooldown then - enableCountdownText(indicator.cooldown) + if isMidnightBorderIcon then + -- Hide Cell's duration text; Blizzard's centered countdown replaces it + if indicator.duration then + indicator.duration:Hide() + end + if indicator.cooldown and indicator.showDuration then + indicator.cooldown:SetHideCountdownNumbers(false) + end end end end @@ -474,11 +458,25 @@ local function InitIndicator(indicatorName) elseif indicatorName == "raidDebuffs" then indicator.isRaidDebuffs = true local types = {"", "Curse", "Magic"} + local isMidnightBorderIcon = Cell.isMidnight and indicator[1] and indicator[1].cooldown + and indicator[1].cooldown._SetCooldown and not indicator[1].cooldown.SetMinMaxValues for i = 1, 3 do indicator[i]:HookScript("OnShow", function() indicator[i]:SetCooldown(GetTime(), 13, types[i], "Interface\\Icons\\INV_Misc_QuestionMark", 7) + if isMidnightBorderIcon then + if indicator[i].duration then indicator[i].duration:Hide() end + if indicator[i].cooldown and indicator[i].showDuration then + indicator[i].cooldown:SetHideCountdownNumbers(false) + end + end indicator[i].cooldown:SetScript("OnCooldownDone", function() indicator[i]:SetCooldown(GetTime(), 13, types[i], "Interface\\Icons\\INV_Misc_QuestionMark", 7) + if isMidnightBorderIcon then + if indicator[i].duration then indicator[i].duration:Hide() end + if indicator[i].cooldown and indicator[i].showDuration then + indicator[i].cooldown:SetHideCountdownNumbers(false) + end + end end) end) indicator[i]:HookScript("OnHide", function() @@ -1625,8 +1623,8 @@ if Cell.isRetail or Cell.isMists then ["allCooldowns"] = {"enabled", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, ["tankActiveMitigation"] = {"|cffb7b7b7"..I.GetTankActiveMitigationString(), "enabled", "color-class", "size", "position", "frameLevel"}, ["dispels"] = {"enabled", "dispelFilters", "highlightType", "dispelBlacklist", "iconStyle", "orientation", "size-square", "position", "frameLevel"}, - ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont"}, - ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont"}, + ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "durationVisibility", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, + ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "durationVisibility", "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, ["privateAuras"] = {"|cffb7b7b7"..L["Due to restrictions of the private aura system, this indicator can only use Blizzard style."], "enabled", "privateAuraOptions", "size-square", "position", "frameLevel"}, ["targetedSpells"] = Cell.isMidnight and {"enabled", "targetedSpellsDisplayMode", "targetedSpellsGlow", "size-border", "num:3", "orientation", "position", "frameLevel", "font"} @@ -1671,8 +1669,8 @@ elseif Cell.isCata or Cell.isWrath then ["defensiveCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInDefensives", "customDefensives", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, ["allCooldowns"] = {"enabled", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, ["dispels"] = {"enabled", "dispelFilters", "highlightType", "dispelBlacklist", "iconStyle", "orientation", "size-square", "position", "frameLevel"}, - ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont"}, - ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont"}, + ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "durationVisibility", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, + ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "durationVisibility", "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, ["targetedSpells"] = {"enabled", "checkbutton:showAllSpells:"..L["Glow is only available to the spells in the list below"], "targetedSpellsDisplayMode", "targetedSpellsList", "targetedSpellsGlow", "size-border", "num:3", "orientation", "position", "frameLevel", "font"}, ["targetCounter"] = {"|cffff2727"..L["HIGH CPU USAGE"].."!|r |cffb7b7b7"..L["Check all visible enemy nameplates."], "enabled", "targetCounterFilters", "color", "position", "frameLevel", "font-noOffset"}, ["actions"] = {"|cffb7b7b7"..L["Play animation when the unit uses a specific spell/item. The list is global shared, not layout-specific."], "enabled", "actionsPreview", "actionsList"}, @@ -1707,8 +1705,8 @@ elseif Cell.isVanilla or Cell.isTBC then ["defensiveCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInDefensives", "customDefensives", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, ["allCooldowns"] = {"enabled", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, ["dispels"] = {"enabled", "dispelFilters", "highlightType", "dispelBlacklist", "iconStyle", "orientation", "size-square", "position", "frameLevel"}, - ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont"}, - ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont"}, + ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "durationVisibility", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, + ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "durationVisibility", "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, ["targetedSpells"] = {"enabled", "checkbutton:showAllSpells:"..L["Glow is only available to the spells in the list below"], "targetedSpellsDisplayMode", "targetedSpellsList", "targetedSpellsGlow", "size-border", "num:3", "orientation", "position", "frameLevel", "font"}, ["targetCounter"] = {"|cffff2727"..L["HIGH CPU USAGE"].."!|r |cffb7b7b7"..L["Check all visible enemy nameplates."], "enabled", "targetCounterFilters", "color", "position", "frameLevel", "font-noOffset"}, ["actions"] = {"|cffb7b7b7"..L["Play animation when the unit uses a specific spell/item. The list is global shared, not layout-specific."], "enabled", "actionsPreview", "actionsList"}, diff --git a/RaidFrames/UnitButton.lua b/RaidFrames/UnitButton.lua index 78c9a584..c227407c 100644 --- a/RaidFrames/UnitButton.lua +++ b/RaidFrames/UnitButton.lua @@ -1574,11 +1574,13 @@ local function UnitButton_UpdateDebuffs(self, isFullUpdate) self.indicators.raidDebuffs[i]:SetCooldownFromAura( unit, auraInstanceID, auraInfo.icon, auraInfo.refreshing) -- Dispel color: border = dispel type color (base), swipe = black + -- Same pattern as regular debuffs in showDebuff local frame = self.indicators.raidDebuffs[i] if frame.cooldown and frame.cooldown.SetSwipeColor then frame.cooldown:SetSwipeColor(0, 0, 0) end if auraInfo._hasSecrets and (auraInfo.dispelName == nil) then + -- Non-dispellable secret: red if frame.border then frame.border:SetColorTexture(1, 0, 0); frame.border:Show() end elseif auraInfo._hasSecrets and _dispelCurvesReady then local hlColor = _getCurveColor(unit, auraInstanceID, _dispelHighlightCurve) From 35e0aeef8a5e2d764b3a9aada7bab042cf2b6863 Mon Sep 17 00:00:00 2001 From: jdtoppin <6392799+jdtoppin@users.noreply.github.com> Date: Sun, 22 Mar 2026 22:54:12 -0600 Subject: [PATCH 10/61] feat: simplified duration font settings + centered countdown text - Use font-noOffset widget for Midnight indicators (no anchor/offset/color since Blizzard's countdown text doesn't support them) - Pre-Midnight retains full font2 widget with all positioning options - New font-noOffset:durationFont parser in widget builder and settings handler - Center countdown FontString on iconFrame for proper alignment - SetCountdownAbbrevThreshold(60) abbreviates above 60s Co-Authored-By: Claude Opus 4.6 (1M context) --- Indicators/Base.lua | 4 ++- Modules/Indicators/Indicators.lua | 45 +++++++++++++++++---------- Widgets/Widgets_IndicatorSettings.lua | 3 ++ 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/Indicators/Base.lua b/Indicators/Base.lua index 8d2dcd6a..e93584a5 100644 --- a/Indicators/Base.lua +++ b/Indicators/Base.lua @@ -94,9 +94,11 @@ local function ApplyCountdownFont(frame, font2) if not frame.cooldown.GetCountdownFontString then return end local cdText = frame.cooldown:GetCountdownFontString() if not cdText then return end - -- Re-parent once so text renders above icon (iconFrame is above cooldown) + -- Re-parent to iconFrame so text renders above icon, and center on the icon if frame.iconFrame and cdText:GetParent() ~= frame.iconFrame then cdText:SetParent(frame.iconFrame) + cdText:ClearAllPoints() + cdText:SetPoint("CENTER", frame.iconFrame, "CENTER", 0, 0) end if font2 then local fontFace = F.GetFont(font2[1]) or cdText:GetFont() diff --git a/Modules/Indicators/Indicators.lua b/Modules/Indicators/Indicators.lua index f5ff5124..faca9cc4 100644 --- a/Modules/Indicators/Indicators.lua +++ b/Modules/Indicators/Indicators.lua @@ -1590,6 +1590,9 @@ end local indicatorSettings local DEBUFFS_TOOLTIP1 = L["This will make these icons not click-through-able"].."|"..L["Tooltips need to be enabled in General tab"] local DEBUFFS_TOOLTIP2 = L["This will make these icons not click-through-able"] +-- Midnight: Blizzard's countdown text doesn't support anchor/offset, use simplified font widget. +-- Pre-Midnight: Cell's own duration text supports full positioning. +local midnightDurationFont = Cell.isMidnight and "font-noOffset:durationFont" or "font2:durationFont" if Cell.isRetail or Cell.isMists then indicatorSettings = { ["nameText"] = {"enabled", "color-class", "textWidth", "checkbutton:showGroupNumber", "vehicleNamePosition", "position", "frameLevel", "font-noOffset"}, @@ -1618,19 +1621,19 @@ if Cell.isRetail or Cell.isMists then ["aggroBar"] = {"enabled", "size", "position", "frameLevel"}, ["shieldBar"] = {"enabled", "checkbutton:onlyShowOvershields", "color-alpha", "height", "shieldBarPosition", "frameLevel"}, ["aoeHealing"] = {"|cffb7b7b7"..L["Display a gradient texture when the unit receives a heal from your certain healing spells."], "enabled", "builtInAoEHealings", "customAoEHealings", "color", "height"}, - ["externalCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInExternals", "customExternals", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, - ["defensiveCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInDefensives", "customDefensives", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, - ["allCooldowns"] = {"enabled", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, + ["externalCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInExternals", "customExternals", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["defensiveCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInDefensives", "customDefensives", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["allCooldowns"] = {"enabled", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, ["tankActiveMitigation"] = {"|cffb7b7b7"..I.GetTankActiveMitigationString(), "enabled", "color-class", "size", "position", "frameLevel"}, ["dispels"] = {"enabled", "dispelFilters", "highlightType", "dispelBlacklist", "iconStyle", "orientation", "size-square", "position", "frameLevel"}, - ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "durationVisibility", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, - ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "durationVisibility", "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, + ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "durationVisibility", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "durationVisibility", "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, ["privateAuras"] = {"|cffb7b7b7"..L["Due to restrictions of the private aura system, this indicator can only use Blizzard style."], "enabled", "privateAuraOptions", "size-square", "position", "frameLevel"}, ["targetedSpells"] = Cell.isMidnight and {"enabled", "targetedSpellsDisplayMode", "targetedSpellsGlow", "size-border", "num:3", "orientation", "position", "frameLevel", "font"} or {"enabled", "checkbutton:showAllSpells:"..L["Glow is only available to the spells in the list below"], "targetedSpellsDisplayMode", "targetedSpellsList", "targetedSpellsGlow", "size-border", "num:3", "orientation", "position", "frameLevel", "font"}, ["targetCounter"] = {"|cffff2727"..L["HIGH CPU USAGE"].."!|r |cffb7b7b7"..L["Check all visible enemy nameplates."], "enabled", "targetCounterFilters", "color", "position", "frameLevel", "font-noOffset"}, - ["crowdControls"] = {"enabled", "builtInCrowdControls", "customCrowdControls", "durationVisibility", "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, + ["crowdControls"] = {"enabled", "builtInCrowdControls", "customCrowdControls", "durationVisibility", "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, ["actions"] = {"|cffb7b7b7"..L["Play animation when the unit uses a specific spell/item. The list is global shared, not layout-specific."], "enabled", "actionsPreview", "actionsList"}, ["healthThresholds"] = {"enabled", "thresholds", "thickness"}, ["missingBuffs"] = {"|cffb7b7b7"..(L["%s in Utilities must be enabled to make this indicator work."]:format(Cell.GetAccentColorString()..L["Buff Tracker"].."|r")), "enabled", "size-square", "orientation", "position", "frameLevel"}, @@ -1665,12 +1668,12 @@ elseif Cell.isCata or Cell.isWrath then ["shieldBar"] = {"enabled", "checkbutton:onlyShowOvershields", "color-alpha", "height", "shieldBarPosition", "frameLevel"}, ["powerWordShield"] = {L["To show shield value, |cffff2727Glyph of Power Word: Shield|r is required"], "enabled", "checkbutton:shieldByMe", "shape", "size-square", "position", "frameLevel"}, ["aoeHealing"] = {"|cffb7b7b7"..L["Display a gradient texture when the unit receives a heal from your certain healing spells."], "enabled", "builtInAoEHealings", "customAoEHealings", "color", "height"}, - ["externalCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInExternals", "customExternals", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, - ["defensiveCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInDefensives", "customDefensives", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, - ["allCooldowns"] = {"enabled", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, + ["externalCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInExternals", "customExternals", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["defensiveCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInDefensives", "customDefensives", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["allCooldowns"] = {"enabled", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, ["dispels"] = {"enabled", "dispelFilters", "highlightType", "dispelBlacklist", "iconStyle", "orientation", "size-square", "position", "frameLevel"}, - ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "durationVisibility", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, - ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "durationVisibility", "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, + ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "durationVisibility", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "durationVisibility", "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, ["targetedSpells"] = {"enabled", "checkbutton:showAllSpells:"..L["Glow is only available to the spells in the list below"], "targetedSpellsDisplayMode", "targetedSpellsList", "targetedSpellsGlow", "size-border", "num:3", "orientation", "position", "frameLevel", "font"}, ["targetCounter"] = {"|cffff2727"..L["HIGH CPU USAGE"].."!|r |cffb7b7b7"..L["Check all visible enemy nameplates."], "enabled", "targetCounterFilters", "color", "position", "frameLevel", "font-noOffset"}, ["actions"] = {"|cffb7b7b7"..L["Play animation when the unit uses a specific spell/item. The list is global shared, not layout-specific."], "enabled", "actionsPreview", "actionsList"}, @@ -1701,12 +1704,12 @@ elseif Cell.isVanilla or Cell.isTBC then ["aggroBorder"] = {"enabled", "thickness", "frameLevel"}, ["aggroBar"] = {"enabled", "size", "position", "frameLevel"}, ["aoeHealing"] = {"|cffb7b7b7"..L["Display a gradient texture when the unit receives a heal from your certain healing spells."], "enabled", "builtInAoEHealings", "customAoEHealings", "color", "height"}, - ["externalCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInExternals", "customExternals", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, - ["defensiveCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInDefensives", "customDefensives", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, - ["allCooldowns"] = {"enabled", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, + ["externalCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInExternals", "customExternals", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["defensiveCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInDefensives", "customDefensives", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["allCooldowns"] = {"enabled", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, ["dispels"] = {"enabled", "dispelFilters", "highlightType", "dispelBlacklist", "iconStyle", "orientation", "size-square", "position", "frameLevel"}, - ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "durationVisibility", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, - ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "durationVisibility", "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", "font2:durationFont"}, + ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "durationVisibility", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "durationVisibility", "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, ["targetedSpells"] = {"enabled", "checkbutton:showAllSpells:"..L["Glow is only available to the spells in the list below"], "targetedSpellsDisplayMode", "targetedSpellsList", "targetedSpellsGlow", "size-border", "num:3", "orientation", "position", "frameLevel", "font"}, ["targetCounter"] = {"|cffff2727"..L["HIGH CPU USAGE"].."!|r |cffb7b7b7"..L["Check all visible enemy nameplates."], "enabled", "targetCounterFilters", "color", "position", "frameLevel", "font-noOffset"}, ["actions"] = {"|cffb7b7b7"..L["Play animation when the unit uses a specific spell/item. The list is global shared, not layout-specific."], "enabled", "actionsPreview", "actionsList"}, @@ -1840,6 +1843,16 @@ local function ShowIndicatorSettings(id) Cell.Fire("UpdateIndicators", notifiedLayout, indicatorName, "font", indicatorTable["font"]) end) + -- font-noOffset:durationFont (Midnight: simplified font widget for paired font config) + elseif string.find(currentSetting, "^font%-noOffset:") then + local _, setting = string.split(":", currentSetting) + -- Map setting name to font index (durationFont = index 2) + local index = setting == "durationFont" and 2 or 1 + w:SetDBValue(indicatorTable["font"][index], setting) + w:SetFunc(function() + Cell.Fire("UpdateIndicators", notifiedLayout, indicatorName, "font", indicatorTable["font"]) + end) + -- font1, font2 elseif string.find(currentSetting, "^font%d") then local index, setting = strmatch(currentSetting, "^font(%d):(.+)") diff --git a/Widgets/Widgets_IndicatorSettings.lua b/Widgets/Widgets_IndicatorSettings.lua index 649320f1..6d13c1b5 100644 --- a/Widgets/Widgets_IndicatorSettings.lua +++ b/Widgets/Widgets_IndicatorSettings.lua @@ -6861,6 +6861,9 @@ function Cell.CreateIndicatorSettings(parent, settingsTable) tinsert(widgetsTable, CreateSetting_Num(parent)) elseif string.find(setting, "^numPerLine:") then tinsert(widgetsTable, CreateSetting_NumPerLine(parent)) + elseif string.find(setting, "^font%-noOffset:") then + -- Midnight: simplified font widget for paired font configs (no anchor/offset) + tinsert(widgetsTable, CreateSetting_FontNoOffset(parent)) elseif string.find(setting, "^font") then tinsert(widgetsTable, CreateSetting_Font(parent, string.match(setting, "^(font%d?):?.*$"))) elseif string.find(setting, "^checkbutton4") then From e79c24d0083e8b6efcd6df2d74186846f60718e1 Mon Sep 17 00:00:00 2001 From: jdtoppin <6392799+jdtoppin@users.noreply.github.com> Date: Mon, 23 Mar 2026 00:51:27 -0600 Subject: [PATCH 11/61] feat: player cast coloring, new built-in spells, BuffTracker guard - Green border for player's own externals/defensives, yellow for others Uses |PLAYER server filter suffix for secret auras, sourceUnit check for non-secret - New built-in spells: Power Infusion (Priest), Blessing of Freedom (Paladin), Rewind + Verdant Embrace (Evoker), Strength of the Black Ox (Monk) - builtInExternals/Defensives always store by ID for consistent lookup - BuffTracker: InCombatLockdown guard before SendChatMessage - Removed all fingerprinting code (widget, file, stale comments) Co-Authored-By: Claude Opus 4.6 (1M context) --- Defaults/Indicator_DefaultSpells.lua | 13 +++++++---- RaidFrames/UnitButton.lua | 33 ++++++++++++++++++++++++---- Utilities/BuffTracker.lua | 2 ++ 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/Defaults/Indicator_DefaultSpells.lua b/Defaults/Indicator_DefaultSpells.lua index a86e3717..07890183 100644 --- a/Defaults/Indicator_DefaultSpells.lua +++ b/Defaults/Indicator_DefaultSpells.lua @@ -231,6 +231,8 @@ local externals = { -- true: track by name, false: track by id ["EVOKER"] = { [374227] = true, -- 微风 - Zephyr [357170] = true, -- 时间膨胀 - Time Dilation + [363534] = true, -- 回溯 - Rewind + [360995] = true, -- 翠绿拥抱 - Verdant Embrace [378441] = true, -- 时间停止 - Time Stop (pvp) [374348] = true, -- 新生光焰 - Renewing blaze }, @@ -256,6 +258,7 @@ local externals = { -- true: track by name, false: track by id [1022] = true, -- 保护祝福 - Blessing of Protection [6940] = true, -- 牺牲祝福 - Blessing of Sacrifice [204018] = true, -- 破咒祝福 - Blessing of Spellwarding + [1044] = true, -- 自由祝福 - Blessing of Freedom [31821] = true, -- 光环掌握 - Aura Mastery [210256] = true, -- 庇护祝福 - Blessing of Sanctuary [228050] = false, -- 圣盾术 (被遗忘的女王护卫) - Divine Shield @@ -266,6 +269,7 @@ local externals = { -- true: track by name, false: track by id ["PRIEST"] = { [33206] = true, -- 痛苦压制 - Pain Suppression [47788] = true, -- 守护之魂 - Guardian Spirit + [10060] = true, -- 能量灌注 - Power Infusion [62618] = true, -- 真言术:障 - Power Word: Barrier [213610] = true, -- 神圣守卫 - Holy Ward [197268] = true, -- 希望之光 - Ray of Hope @@ -302,9 +306,9 @@ local function UpdateExternals(id, trackByName) if name then builtInExternals[name] = true end - else - builtInExternals[id] = true end + -- Always store by ID for server-side filter matching on Midnight + builtInExternals[id] = true end function I.UpdateExternals(t) @@ -406,6 +410,7 @@ local defensives = { -- true: track by name, false: track by id [122278] = true, -- 躯不坏 - Dampen Harm [122783] = true, -- 散魔功 - Diffuse Magic [125174] = true, -- 业报之触 - Touch of Karma + [443113] = true, -- 黑牛之力 - Strength of the Black Ox }, ["PALADIN"] = { @@ -471,9 +476,9 @@ function I.UpdateDefensives(t) if name then builtInDefensives[name] = true end - else - builtInDefensives[id] = true end + -- Always store by ID for server-side filter matching on Midnight + builtInDefensives[id] = true end end end diff --git a/RaidFrames/UnitButton.lua b/RaidFrames/UnitButton.lua index c227407c..0d3cdb8a 100644 --- a/RaidFrames/UnitButton.lua +++ b/RaidFrames/UnitButton.lua @@ -1881,13 +1881,38 @@ local function HandleBuff(self, auraInfo) end end + -- Check if this is the player's own cast (for color differentiation) + local isPlayerCast = false + if isExternal or isDefensive then + if not auraInfo._hasSecrets then + -- Non-secret: check sourceUnit directly + isPlayerCast = source == "player" or source == "pet" + elseif _IsAuraFilteredOut then + -- Secret: use |PLAYER suffix on server filters + if isExternal then + isPlayerCast = not _IsAuraFilteredOut(unit, auraInstanceID, "HELPFUL|EXTERNAL_DEFENSIVE|PLAYER") + end + if not isPlayerCast and isDefensive then + isPlayerCast = not _IsAuraFilteredOut(unit, auraInstanceID, "HELPFUL|BIG_DEFENSIVE|PLAYER") + end + if not isPlayerCast then + isPlayerCast = not _IsAuraFilteredOut(unit, auraInstanceID, "HELPFUL|RAID|PLAYER") + end + end + end + + -- Border color: green for player's own casts, yellow for others + local borderR, borderG, borderB = 1, 0.85, 0 -- yellow (default) + if isPlayerCast then + borderR, borderG, borderB = 0, 0.8, 0 -- green + end + if enabledIndicators["defensiveCooldowns"] and isDefensive and self._buffs.defensiveFound < indicatorNums["defensiveCooldowns"] then self._buffs.defensiveFound = self._buffs.defensiveFound + 1 local frame = self.indicators.defensiveCooldowns[self._buffs.defensiveFound] if Cell.isMidnight then frame:SetCooldownFromAura(unit, auraInstanceID, icon, auraInfo.refreshing) - -- Yellow base, black swipe fills in - if frame.border then frame.border:SetColorTexture(1, 0.85, 0); frame.border:Show() end + if frame.border then frame.border:SetColorTexture(borderR, borderG, borderB); frame.border:Show() end if frame.cooldown and frame.cooldown.SetSwipeColor then frame.cooldown:SetSwipeColor(0, 0, 0) end else frame:SetCooldown(start, duration, nil, icon, count, auraInfo.refreshing) @@ -1900,7 +1925,7 @@ local function HandleBuff(self, auraInfo) local frame = self.indicators.externalCooldowns[self._buffs.externalFound] if Cell.isMidnight then frame:SetCooldownFromAura(unit, auraInstanceID, icon, auraInfo.refreshing) - if frame.border then frame.border:SetColorTexture(1, 0.85, 0); frame.border:Show() end + if frame.border then frame.border:SetColorTexture(borderR, borderG, borderB); frame.border:Show() end if frame.cooldown and frame.cooldown.SetSwipeColor then frame.cooldown:SetSwipeColor(0, 0, 0) end else frame:SetCooldown(start, duration, nil, icon, count, auraInfo.refreshing) @@ -1913,7 +1938,7 @@ local function HandleBuff(self, auraInfo) local frame = self.indicators.allCooldowns[self._buffs.allFound] if Cell.isMidnight then frame:SetCooldownFromAura(unit, auraInstanceID, icon, auraInfo.refreshing) - if frame.border then frame.border:SetColorTexture(1, 0.85, 0); frame.border:Show() end + if frame.border then frame.border:SetColorTexture(borderR, borderG, borderB); frame.border:Show() end if frame.cooldown and frame.cooldown.SetSwipeColor then frame.cooldown:SetSwipeColor(0, 0, 0) end else frame:SetCooldown(start, duration, nil, icon, count, auraInfo.refreshing) diff --git a/Utilities/BuffTracker.lua b/Utilities/BuffTracker.lua index d3bb29f8..87c2ff64 100644 --- a/Utilities/BuffTracker.lua +++ b/Utilities/BuffTracker.lua @@ -496,6 +496,8 @@ local function CreateBuffButton(parent, buff) -- chat b:HookScript("OnClick", function(self, button, down) if button == "RightButton" and (down == GetCVarBool("ActionButtonUseKeyDown")) then + -- SendChatMessage is protected during encounters on Midnight + if InCombatLockdown() then return end local msg = GetUnaffectedString(buff) if msg then UpdateSendChannel() From 9d1c7a65151858264a1d0607d72944b8ade1d6b4 Mon Sep 17 00:00:00 2001 From: jdtoppin <6392799+jdtoppin@users.noreply.github.com> Date: Mon, 23 Mar 2026 01:38:33 -0600 Subject: [PATCH 12/61] fix: toggle Blizzard countdown text on duration visibility change BorderIcon_ShowDuration now toggles SetHideCountdownNumbers when the user changes Duration Visibility (Always/Never). Without this, the countdown text only responded after a reload. Co-Authored-By: Claude Opus 4.6 (1M context) --- Indicators/Base.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Indicators/Base.lua b/Indicators/Base.lua index e93584a5..f26793bd 100644 --- a/Indicators/Base.lua +++ b/Indicators/Base.lua @@ -644,6 +644,10 @@ local function BorderIcon_ShowDuration(frame, show) else frame.duration:Hide() end + -- Toggle Blizzard's countdown text on Midnight + if Cell.isMidnight and frame.cooldown and frame.cooldown.SetHideCountdownNumbers then + frame.cooldown:SetHideCountdownNumbers(not show) + end end local function BorderIcon_UpdatePixelPerfect(frame) From 02d1a13533fe6c1c99df69002e4891ffe2a73213 Mon Sep 17 00:00:00 2001 From: jdtoppin <6392799+jdtoppin@users.noreply.github.com> Date: Mon, 23 Mar 2026 02:01:41 -0600 Subject: [PATCH 13/61] feat: simplified duration visibility for Midnight indicators Debuffs, raid debuffs, externals, defensives, all cooldowns, and crowd controls use Always/Never dropdown on Midnight instead of percentage/time thresholds (Blizzard's countdown text doesn't support thresholds). Custom indicators retain full threshold options. Restores CreateSetting_DurationVisibilitySimple widget with coercion of existing threshold values to Always. Known limitation: toggling Always/Never only takes effect on the next aura update, not immediately on active cooldowns. Co-Authored-By: Claude Opus 4.6 (1M context) --- Modules/Indicators/Indicators.lua | 37 ++++++++++--------- Widgets/Widgets_IndicatorSettings.lua | 52 +++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 17 deletions(-) diff --git a/Modules/Indicators/Indicators.lua b/Modules/Indicators/Indicators.lua index faca9cc4..148b65cc 100644 --- a/Modules/Indicators/Indicators.lua +++ b/Modules/Indicators/Indicators.lua @@ -1593,6 +1593,9 @@ local DEBUFFS_TOOLTIP2 = L["This will make these icons not click-through-able"] -- Midnight: Blizzard's countdown text doesn't support anchor/offset, use simplified font widget. -- Pre-Midnight: Cell's own duration text supports full positioning. local midnightDurationFont = Cell.isMidnight and "font-noOffset:durationFont" or "font2:durationFont" +-- Midnight: Blizzard's countdown only supports Always/Never, no thresholds. +-- Pre-Midnight: Cell's duration text supports percentage/time thresholds. +local midnightDurationVisibility = Cell.isMidnight and "durationVisibilitySimple" or "durationVisibility" if Cell.isRetail or Cell.isMists then indicatorSettings = { ["nameText"] = {"enabled", "color-class", "textWidth", "checkbutton:showGroupNumber", "vehicleNamePosition", "position", "frameLevel", "font-noOffset"}, @@ -1621,19 +1624,19 @@ if Cell.isRetail or Cell.isMists then ["aggroBar"] = {"enabled", "size", "position", "frameLevel"}, ["shieldBar"] = {"enabled", "checkbutton:onlyShowOvershields", "color-alpha", "height", "shieldBarPosition", "frameLevel"}, ["aoeHealing"] = {"|cffb7b7b7"..L["Display a gradient texture when the unit receives a heal from your certain healing spells."], "enabled", "builtInAoEHealings", "customAoEHealings", "color", "height"}, - ["externalCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInExternals", "customExternals", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, - ["defensiveCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInDefensives", "customDefensives", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, - ["allCooldowns"] = {"enabled", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["externalCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInExternals", "customExternals", midnightDurationVisibility, "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["defensiveCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInDefensives", "customDefensives", midnightDurationVisibility, "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["allCooldowns"] = {"enabled", midnightDurationVisibility, "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, ["tankActiveMitigation"] = {"|cffb7b7b7"..I.GetTankActiveMitigationString(), "enabled", "color-class", "size", "position", "frameLevel"}, ["dispels"] = {"enabled", "dispelFilters", "highlightType", "dispelBlacklist", "iconStyle", "orientation", "size-square", "position", "frameLevel"}, - ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "durationVisibility", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, - ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "durationVisibility", "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", midnightDurationVisibility, "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, midnightDurationVisibility, "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, ["privateAuras"] = {"|cffb7b7b7"..L["Due to restrictions of the private aura system, this indicator can only use Blizzard style."], "enabled", "privateAuraOptions", "size-square", "position", "frameLevel"}, ["targetedSpells"] = Cell.isMidnight and {"enabled", "targetedSpellsDisplayMode", "targetedSpellsGlow", "size-border", "num:3", "orientation", "position", "frameLevel", "font"} or {"enabled", "checkbutton:showAllSpells:"..L["Glow is only available to the spells in the list below"], "targetedSpellsDisplayMode", "targetedSpellsList", "targetedSpellsGlow", "size-border", "num:3", "orientation", "position", "frameLevel", "font"}, ["targetCounter"] = {"|cffff2727"..L["HIGH CPU USAGE"].."!|r |cffb7b7b7"..L["Check all visible enemy nameplates."], "enabled", "targetCounterFilters", "color", "position", "frameLevel", "font-noOffset"}, - ["crowdControls"] = {"enabled", "builtInCrowdControls", "customCrowdControls", "durationVisibility", "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["crowdControls"] = {"enabled", "builtInCrowdControls", "customCrowdControls", midnightDurationVisibility, "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, ["actions"] = {"|cffb7b7b7"..L["Play animation when the unit uses a specific spell/item. The list is global shared, not layout-specific."], "enabled", "actionsPreview", "actionsList"}, ["healthThresholds"] = {"enabled", "thresholds", "thickness"}, ["missingBuffs"] = {"|cffb7b7b7"..(L["%s in Utilities must be enabled to make this indicator work."]:format(Cell.GetAccentColorString()..L["Buff Tracker"].."|r")), "enabled", "size-square", "orientation", "position", "frameLevel"}, @@ -1668,12 +1671,12 @@ elseif Cell.isCata or Cell.isWrath then ["shieldBar"] = {"enabled", "checkbutton:onlyShowOvershields", "color-alpha", "height", "shieldBarPosition", "frameLevel"}, ["powerWordShield"] = {L["To show shield value, |cffff2727Glyph of Power Word: Shield|r is required"], "enabled", "checkbutton:shieldByMe", "shape", "size-square", "position", "frameLevel"}, ["aoeHealing"] = {"|cffb7b7b7"..L["Display a gradient texture when the unit receives a heal from your certain healing spells."], "enabled", "builtInAoEHealings", "customAoEHealings", "color", "height"}, - ["externalCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInExternals", "customExternals", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, - ["defensiveCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInDefensives", "customDefensives", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, - ["allCooldowns"] = {"enabled", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["externalCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInExternals", "customExternals", midnightDurationVisibility, "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["defensiveCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInDefensives", "customDefensives", midnightDurationVisibility, "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["allCooldowns"] = {"enabled", midnightDurationVisibility, "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, ["dispels"] = {"enabled", "dispelFilters", "highlightType", "dispelBlacklist", "iconStyle", "orientation", "size-square", "position", "frameLevel"}, - ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "durationVisibility", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, - ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "durationVisibility", "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", midnightDurationVisibility, "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, midnightDurationVisibility, "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, ["targetedSpells"] = {"enabled", "checkbutton:showAllSpells:"..L["Glow is only available to the spells in the list below"], "targetedSpellsDisplayMode", "targetedSpellsList", "targetedSpellsGlow", "size-border", "num:3", "orientation", "position", "frameLevel", "font"}, ["targetCounter"] = {"|cffff2727"..L["HIGH CPU USAGE"].."!|r |cffb7b7b7"..L["Check all visible enemy nameplates."], "enabled", "targetCounterFilters", "color", "position", "frameLevel", "font-noOffset"}, ["actions"] = {"|cffb7b7b7"..L["Play animation when the unit uses a specific spell/item. The list is global shared, not layout-specific."], "enabled", "actionsPreview", "actionsList"}, @@ -1704,12 +1707,12 @@ elseif Cell.isVanilla or Cell.isTBC then ["aggroBorder"] = {"enabled", "thickness", "frameLevel"}, ["aggroBar"] = {"enabled", "size", "position", "frameLevel"}, ["aoeHealing"] = {"|cffb7b7b7"..L["Display a gradient texture when the unit receives a heal from your certain healing spells."], "enabled", "builtInAoEHealings", "customAoEHealings", "color", "height"}, - ["externalCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInExternals", "customExternals", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, - ["defensiveCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInDefensives", "customDefensives", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, - ["allCooldowns"] = {"enabled", "durationVisibility", "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["externalCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInExternals", "customExternals", midnightDurationVisibility, "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["defensiveCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInDefensives", "customDefensives", midnightDurationVisibility, "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["allCooldowns"] = {"enabled", midnightDurationVisibility, "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, ["dispels"] = {"enabled", "dispelFilters", "highlightType", "dispelBlacklist", "iconStyle", "orientation", "size-square", "position", "frameLevel"}, - ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", "durationVisibility", "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, - ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, "durationVisibility", "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["debuffs"] = {"enabled", "checkbutton:dispellableByMe", "debuffBlacklist", "bigDebuffs", midnightDurationVisibility, "checkbutton2:showAnimation", "checkbutton3:showTooltip:"..DEBUFFS_TOOLTIP1, "checkbutton4:enableBlacklistShortcut:"..DEBUFFS_TOOLTIP2, "size-normal-big", "num:10", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["raidDebuffs"] = {"|cffb7b7b7"..L["You can config debuffs in %s"]:format(Cell.GetAccentColorString()..L["Raid Debuffs"].."|r"), "enabled", "checkbutton:onlyShowTopGlow", "checkbutton2:showTooltip:"..DEBUFFS_TOOLTIP1, midnightDurationVisibility, "size-border", "num:3", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, ["targetedSpells"] = {"enabled", "checkbutton:showAllSpells:"..L["Glow is only available to the spells in the list below"], "targetedSpellsDisplayMode", "targetedSpellsList", "targetedSpellsGlow", "size-border", "num:3", "orientation", "position", "frameLevel", "font"}, ["targetCounter"] = {"|cffff2727"..L["HIGH CPU USAGE"].."!|r |cffb7b7b7"..L["Check all visible enemy nameplates."], "enabled", "targetCounterFilters", "color", "position", "frameLevel", "font-noOffset"}, ["actions"] = {"|cffb7b7b7"..L["Play animation when the unit uses a specific spell/item. The list is global shared, not layout-specific."], "enabled", "actionsPreview", "actionsList"}, @@ -1806,7 +1809,7 @@ local function ShowIndicatorSettings(id) if currentSetting == "size-square" or currentSetting == "size-normal-big" then currentSetting = "size" end if currentSetting == "statusPosition" or currentSetting == "position-noHCenter" or currentSetting == "shieldBarPosition" then currentSetting = "position" end if currentSetting == "barOrientation" then currentSetting = "orientation" end - if currentSetting == "durationVisibility" then currentSetting = "showDuration" end + if currentSetting == "durationVisibility" or currentSetting == "durationVisibilitySimple" then currentSetting = "showDuration" end if currentSetting == "powerFormat" then currentSetting = "format" end -- enabled diff --git a/Widgets/Widgets_IndicatorSettings.lua b/Widgets/Widgets_IndicatorSettings.lua index 6d13c1b5..cc04cfdf 100644 --- a/Widgets/Widgets_IndicatorSettings.lua +++ b/Widgets/Widgets_IndicatorSettings.lua @@ -1520,6 +1520,57 @@ local function CreateSetting_DurationVisibility(parent) return widget end +-- Midnight: simplified duration visibility with only Always/Never options +-- (Blizzard's countdown text doesn't support percentage/time thresholds) +local function CreateSetting_DurationVisibilitySimple(parent) + local widget + + if not settingWidgets["durationVisibilitySimple"] then + widget = Cell.CreateFrame("CellIndicatorSettings_DurationVisibilitySimple", parent, 240, 50) + settingWidgets["durationVisibilitySimple"] = widget + + widget.durationVisibility = Cell.CreateDropdown(widget, 245) + widget.durationVisibility:SetPoint("TOPLEFT", 5, -20) + widget.durationVisibility:SetItems({ + { + ["text"] = L["Never"], + ["value"] = false, + ["onClick"] = function() + widget.func(false) + end, + }, + { + ["text"] = L["Always"], + ["value"] = true, + ["onClick"] = function() + widget.func(true) + end, + }, + }) + + widget.durationVisibilityText = widget:CreateFontString(nil, "OVERLAY", font_name) + widget.durationVisibilityText:SetText(L["showDuration"]) + widget.durationVisibilityText:SetPoint("BOTTOMLEFT", widget.durationVisibility, "TOPLEFT", 0, 1) + + function widget:SetFunc(func) + widget.func = func + end + + function widget:SetDBValue(durationVisibility) + -- coerce threshold values to "Always" since they can't work with secrets + if durationVisibility and durationVisibility ~= false then + durationVisibility = true + end + widget.durationVisibility:SetSelectedValue(durationVisibility) + end + else + widget = settingWidgets["durationVisibilitySimple"] + end + + widget:Show() + return widget +end + local function CreateSetting_Orientation(parent) local widget @@ -6797,6 +6848,7 @@ local builders = { ["healthFormat"] = CreateSetting_HealthFormat, ["powerFormat"] = CreateSetting_PowerFormat, ["durationVisibility"] = CreateSetting_DurationVisibility, + ["durationVisibilitySimple"] = CreateSetting_DurationVisibilitySimple, ["orientation"] = CreateSetting_Orientation, ["barOrientation"] = CreateSetting_BarOrientation, ["font-noOffset"] = CreateSetting_FontNoOffset, From 6354f2fc61cc7f667f0a177dddaa9c4d62db29d8 Mon Sep 17 00:00:00 2001 From: jdtoppin <6392799+jdtoppin@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:25:03 -0600 Subject: [PATCH 14/61] =?UTF-8?q?fix:=20review=20items=20=E2=80=94=20perfo?= =?UTF-8?q?rmance,=20dead=20code,=20UI=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I1: Move SetCountdownAbbrevThreshold to frame creation, cache ApplyCountdownFont via _countdownFontApplied flag I2: Reset font cache on settings change in Shared_SetFont I3: Remove dead Midnight toggle from Shared_ShowDuration S1: Preview alternates green (first icon = player cast) / yellow S2: Clearer comments on always-by-ID storage S3: Threshold coercion comment clarified Fix: BorderIcon_ShowDuration hides Cell's duration text on Midnight (only toggles Blizzard countdown, prevents dual text display) Fix: Hide custom spell field and showAnimation on Midnight for externals/defensives (custom spells can't be tracked with secrets) Co-Authored-By: Claude Opus 4.6 (1M context) --- Defaults/Indicator_DefaultSpells.lua | 6 ++-- Indicators/Base.lua | 41 +++++++++++++++------------ Modules/Indicators/Indicators.lua | 19 ++++++++++--- Widgets/Widgets_IndicatorSettings.lua | 4 ++- 4 files changed, 45 insertions(+), 25 deletions(-) diff --git a/Defaults/Indicator_DefaultSpells.lua b/Defaults/Indicator_DefaultSpells.lua index 07890183..68f38aba 100644 --- a/Defaults/Indicator_DefaultSpells.lua +++ b/Defaults/Indicator_DefaultSpells.lua @@ -307,7 +307,8 @@ local function UpdateExternals(id, trackByName) builtInExternals[name] = true end end - -- Always store by ID for server-side filter matching on Midnight + -- Also store by ID (in addition to name when trackByName is true) + -- so IsExternalCooldown/IsDefensiveCooldown can match by ID directly builtInExternals[id] = true end @@ -477,7 +478,8 @@ function I.UpdateDefensives(t) builtInDefensives[name] = true end end - -- Always store by ID for server-side filter matching on Midnight + -- Also store by ID (in addition to name when trackByName is true) + -- so IsExternalCooldown/IsDefensiveCooldown can match by ID directly builtInDefensives[id] = true end end diff --git a/Indicators/Base.lua b/Indicators/Base.lua index f26793bd..5ef739f4 100644 --- a/Indicators/Base.lua +++ b/Indicators/Base.lua @@ -119,9 +119,11 @@ local function Shared_SetFont(frame, font1, font2) I.SetFont(frame.duration, frame, unpack(font2)) -- Store duration font config for Midnight countdown text on CooldownFrame frame._durationFont = font2 - -- Live-update countdown FontString if it exists + -- Live-update countdown FontString if it exists; reset cache flag if Cell.isMidnight then + frame._countdownFontApplied = false ApplyCountdownFont(frame, font2) + frame._countdownFontApplied = true end end @@ -132,10 +134,6 @@ end local function Shared_ShowDuration(frame, show) frame.showDuration = show frame.duration:SetShown(show) - -- Toggle Blizzard's countdown text on Midnight BorderIcon - if Cell.isMidnight and frame.cooldown and frame.cooldown.SetHideCountdownNumbers then - frame.cooldown:SetHideCountdownNumbers(not show) - end end ------------------------------------------------- @@ -498,15 +496,14 @@ local function BorderIcon_SetCooldownFromAura(frame, unit, auraInstanceID, textu local durObj = _GetAuraDuration and _GetAuraDuration(unit, auraInstanceID) if durObj and frame.cooldown and frame.cooldown._SetCooldown and frame.cooldown.SetCooldownFromDurationObject then - -- Enable Blizzard's countdown text (respects showDuration setting) - if frame.showDuration then - frame.cooldown:SetHideCountdownNumbers(false) - frame.cooldown:SetCountdownAbbrevThreshold(60) - end + -- Countdown numbers visibility is managed by BorderIcon_ShowDuration frame.cooldown:SetReverse(true) frame.cooldown:SetCooldownFromDurationObject(durObj, true) - -- Apply font settings to countdown text - ApplyCountdownFont(frame, frame._durationFont) + -- Apply font settings once (cached via _countdownFontApplied flag) + if not frame._countdownFontApplied then + ApplyCountdownFont(frame, frame._durationFont) + frame._countdownFontApplied = true + end -- Keep border visible as base color (caller sets color); black swipe fills over it frame.cooldown:Show() else @@ -639,14 +636,18 @@ end local function BorderIcon_ShowDuration(frame, show) frame.showDuration = show - if show then - frame.duration:Show() - else - frame.duration:Hide() - end - -- Toggle Blizzard's countdown text on Midnight if Cell.isMidnight and frame.cooldown and frame.cooldown.SetHideCountdownNumbers then + -- Midnight: Cell's duration text is always hidden (produces invisible output + -- with secrets). Only toggle Blizzard's built-in countdown. + frame.duration:Hide() frame.cooldown:SetHideCountdownNumbers(not show) + else + -- Pre-Midnight: use Cell's own duration text + if show then + frame.duration:Show() + else + frame.duration:Hide() + end end end @@ -676,6 +677,10 @@ function I.CreateAura_BorderIcon(name, parent, borderSize) cooldown:SetSwipeTexture(Cell.vars.whiteTexture) cooldown:SetSwipeColor(1, 1, 1) cooldown:SetHideCountdownNumbers(true) + -- Midnight: set abbreviation threshold once at creation (shows "1m" above 60s) + if Cell.isMidnight and cooldown.SetCountdownAbbrevThreshold then + cooldown:SetCountdownAbbrevThreshold(60) + end -- disable omnicc cooldown.noCooldownCount = true -- prevent some addons from adding cooldown text diff --git a/Modules/Indicators/Indicators.lua b/Modules/Indicators/Indicators.lua index 148b65cc..7d6d9789 100644 --- a/Modules/Indicators/Indicators.lua +++ b/Modules/Indicators/Indicators.lua @@ -134,11 +134,15 @@ local function SetOnUpdate(indicator, type, icon, stack, extra) and indicator.cooldown._SetCooldown and not indicator.cooldown.SetMinMaxValues local function doPreview() if isMidnightBorderIcon and not type then - -- Buff cooldowns (no debuff type): yellow border base, black swipe fills in + -- Buff cooldowns (no debuff type): green = player cast, yellow = others indicator.icon:SetTexture(icon) indicator.stack:SetText(stack and stack > 1 and stack or "") if indicator.border then - indicator.border:SetColorTexture(1, 0.85, 0) + if indicator._isPreviewPlayerCast then + indicator.border:SetColorTexture(0, 0.8, 0) + else + indicator.border:SetColorTexture(1, 0.85, 0) + end indicator.border:Show() end if indicator.cooldown then @@ -563,16 +567,19 @@ local function InitIndicator(indicatorName) elseif indicatorName == "externalCooldowns" then local icons = {135936, 135964, 135966, 237510, 237542} for i = 1, 5 do + indicator[i]._isPreviewPlayerCast = (i == 1) -- first icon = "your cast" (green) SetOnUpdate(indicator[i], nil, icons[i], 0) end elseif indicatorName == "defensiveCooldowns" then local icons = {135919, 136120, 135841, 132362, 132199} for i = 1, 5 do + indicator[i]._isPreviewPlayerCast = (i == 1) SetOnUpdate(indicator[i], nil, icons[i], 0) end elseif indicatorName == "allCooldowns" then local icons = {135936, 136120, 135966, 132362, 237542} for i = 1, 5 do + indicator[i]._isPreviewPlayerCast = (i == 1) SetOnUpdate(indicator[i], nil, icons[i], 0) end elseif indicatorName == "missingBuffs" then @@ -1624,8 +1631,12 @@ if Cell.isRetail or Cell.isMists then ["aggroBar"] = {"enabled", "size", "position", "frameLevel"}, ["shieldBar"] = {"enabled", "checkbutton:onlyShowOvershields", "color-alpha", "height", "shieldBarPosition", "frameLevel"}, ["aoeHealing"] = {"|cffb7b7b7"..L["Display a gradient texture when the unit receives a heal from your certain healing spells."], "enabled", "builtInAoEHealings", "customAoEHealings", "color", "height"}, - ["externalCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInExternals", "customExternals", midnightDurationVisibility, "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, - ["defensiveCooldowns"] = {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInDefensives", "customDefensives", midnightDurationVisibility, "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["externalCooldowns"] = Cell.isMidnight + and {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInExternals", midnightDurationVisibility, "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont} + or {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInExternals", "customExternals", midnightDurationVisibility, "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, + ["defensiveCooldowns"] = Cell.isMidnight + and {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInDefensives", midnightDurationVisibility, "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont} + or {L["Even if disabled, the settings below affect \"Externals + Defensives\" indicator"], "enabled", "builtInDefensives", "customDefensives", midnightDurationVisibility, "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, ["allCooldowns"] = {"enabled", midnightDurationVisibility, "checkbutton:showAnimation", "glowOptions", "size", "num:5", "orientation", "position", "frameLevel", "font1:stackFont", midnightDurationFont}, ["tankActiveMitigation"] = {"|cffb7b7b7"..I.GetTankActiveMitigationString(), "enabled", "color-class", "size", "position", "frameLevel"}, ["dispels"] = {"enabled", "dispelFilters", "highlightType", "dispelBlacklist", "iconStyle", "orientation", "size-square", "position", "frameLevel"}, diff --git a/Widgets/Widgets_IndicatorSettings.lua b/Widgets/Widgets_IndicatorSettings.lua index cc04cfdf..334b492e 100644 --- a/Widgets/Widgets_IndicatorSettings.lua +++ b/Widgets/Widgets_IndicatorSettings.lua @@ -1557,7 +1557,9 @@ local function CreateSetting_DurationVisibilitySimple(parent) end function widget:SetDBValue(durationVisibility) - -- coerce threshold values to "Always" since they can't work with secrets + -- Coerce pre-Midnight threshold values (0.75, 10, etc.) to "Always" + -- since Blizzard's countdown text only supports on/off, not thresholds. + -- The saved value isn't modified — only the dropdown display is coerced. if durationVisibility and durationVisibility ~= false then durationVisibility = true end From b82b9882600b64ab3b0c3bc3206795d71b312ac5 Mon Sep 17 00:00:00 2001 From: jdtoppin <6392799+jdtoppin@users.noreply.github.com> Date: Tue, 24 Mar 2026 20:43:04 -0600 Subject: [PATCH 15/61] fix: tooltip RefreshData crash + utf8len crash on secret strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tooltip: CellSpellTooltip registered TOOLTIP_DATA_UPDATE at creation and never unregistered, causing RefreshData to crash with tainted color data in combat even when the tooltip wasn't visible. Fix: only register while tooltip is shown, plus InCombatLockdown guard. UpdateTextWidth: utf8len crashes on secret NPC name strings. Guard with F.IsValueNonSecret — pass secrets directly to SetText (C-level). Co-Authored-By: Claude Opus 4.6 (1M context) --- Widgets/Tooltip.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Widgets/Tooltip.lua b/Widgets/Tooltip.lua index 7eb3c0fa..ea5d737d 100644 --- a/Widgets/Tooltip.lua +++ b/Widgets/Tooltip.lua @@ -37,9 +37,17 @@ local function CreateTooltip(name, hasIcon) end if Cell.isRetail then - tooltip:RegisterEvent("TOOLTIP_DATA_UPDATE") + -- Only listen for TOOLTIP_DATA_UPDATE while the tooltip is visible. + -- Prevents stale tainted data from causing RefreshData crashes in combat + -- when the tooltip isn't even shown (Midnight secret value taint). + tooltip:HookScript("OnShow", function() + tooltip:RegisterEvent("TOOLTIP_DATA_UPDATE") + end) + tooltip:HookScript("OnHide", function() + tooltip:UnregisterEvent("TOOLTIP_DATA_UPDATE") + end) tooltip:SetScript("OnEvent", function() - -- Interface\FrameXML\GameTooltip.lua line924 + if Cell.isMidnight and InCombatLockdown() then return end tooltip:RefreshData() end) end From 93de6418e03b2b79644bef3555333d23ce0bdc84 Mon Sep 17 00:00:00 2001 From: Skye Date: Wed, 1 Apr 2026 00:51:59 -0300 Subject: [PATCH 16/61] Add Midnight fixes and prepare public fork --- .gitignore | 1 + Core.lua | 3 - Core_Cata.lua | 3 - Core_Mists.lua | 3 - Core_Vanilla.lua | 3 - Core_Wrath.lua | 3 - Defaults/Appearance_Defaults.lua | 4 +- Indicators/Base.lua | 419 ++++++++++++++++++++++++---- Indicators/Built-in.lua | 18 +- Locales/enUS.lua | 5 +- Locales/ptBR.lua | 4 +- Modules/Appearance/Appearance.lua | 180 +++++++++--- Modules/Indicators/Indicators.lua | 87 ++++-- Modules/RaidDebuffs/RaidDebuffs.lua | 7 - README.md | 3 + RaidFrames/MainFrame.lua | 15 - RaidFrames/UnitButton.lua | 10 +- Utils.lua | 17 +- 18 files changed, 608 insertions(+), 177 deletions(-) diff --git a/.gitignore b/.gitignore index 4c19d4e8..767166e6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .resources/ .utils/*.csv Libs/* +*.Zone.Identifier # excluded !.release/*.sh diff --git a/Core.lua b/Core.lua index 224cf9ac..1a13d157 100644 --- a/Core.lua +++ b/Core.lua @@ -45,9 +45,6 @@ Cell.MIN_QUICKASSIST_VERSION = 275 -- /run SetCVar("secretPvPMatchRestrictionsForced", 1) -- Reset: /run SetCVar("secretCombatRestrictionsForced", 0) ---@debug@ -local debugMode = true ---@end-debug@ function F.Debug(arg, ...) if debugMode then if type(arg) == "string" or type(arg) == "number" then diff --git a/Core_Cata.lua b/Core_Cata.lua index 617f611a..43b4fcda 100644 --- a/Core_Cata.lua +++ b/Core_Cata.lua @@ -35,9 +35,6 @@ Cell.MIN_LAYOUTS_VERSION = 246 Cell.MIN_INDICATORS_VERSION = 246 Cell.MIN_DEBUFFS_VERSION = 246 ---@debug@ -local debugMode = true ---@end-debug@ function F.Debug(arg, ...) if debugMode then if type(arg) == "string" or type(arg) == "number" then diff --git a/Core_Mists.lua b/Core_Mists.lua index 09e59c86..88c3c791 100644 --- a/Core_Mists.lua +++ b/Core_Mists.lua @@ -37,9 +37,6 @@ Cell.MIN_LAYOUTS_VERSION = 246 Cell.MIN_INDICATORS_VERSION = 246 Cell.MIN_DEBUFFS_VERSION = 246 ---@debug@ -local debugMode = true ---@end-debug@ function F.Debug(arg, ...) if debugMode then if type(arg) == "string" or type(arg) == "number" then diff --git a/Core_Vanilla.lua b/Core_Vanilla.lua index 5740e21b..083ab7cb 100644 --- a/Core_Vanilla.lua +++ b/Core_Vanilla.lua @@ -35,9 +35,6 @@ Cell.MIN_LAYOUTS_VERSION = 246 Cell.MIN_INDICATORS_VERSION = 246 Cell.MIN_DEBUFFS_VERSION = 246 ---@debug@ -local debugMode = true ---@end-debug@ function F.Debug(arg, ...) if debugMode then if type(arg) == "string" or type(arg) == "number" then diff --git a/Core_Wrath.lua b/Core_Wrath.lua index be4663f5..0cff2478 100644 --- a/Core_Wrath.lua +++ b/Core_Wrath.lua @@ -35,9 +35,6 @@ Cell.MIN_LAYOUTS_VERSION = 246 Cell.MIN_INDICATORS_VERSION = 246 Cell.MIN_DEBUFFS_VERSION = 246 ---@debug@ -local debugMode = true ---@end-debug@ function F.Debug(arg, ...) if debugMode then if type(arg) == "string" or type(arg) == "number" then diff --git a/Defaults/Appearance_Defaults.lua b/Defaults/Appearance_Defaults.lua index 5be6ffe5..d8473120 100644 --- a/Defaults/Appearance_Defaults.lua +++ b/Defaults/Appearance_Defaults.lua @@ -26,6 +26,7 @@ Cell.defaults.appearance = { ["durationColorEnabled"] = false, ["durationColors"] = {{0,1,0}, {1,1,0,0.5}, {1,0,0,3}}, }, + ["cooldownStyle"] = "VERTICAL", ["targetColor"] = {1, 0.31, 0.31, 1}, ["mouseoverColor"] = {1, 1, 1, 0.6}, ["highlightSize"] = 1, @@ -51,6 +52,7 @@ local buttonStyleIndices = { "colorThresholds", "colorThresholdsLoss", "auraIconOptions", + "cooldownStyle", "targetColor", "mouseoverColor", "highlightSize", @@ -71,4 +73,4 @@ function F.ResetButtonStyle() CellDB["appearance"][index] = Cell.defaults.appearance[index] end end -end \ No newline at end of file +end diff --git a/Indicators/Base.lua b/Indicators/Base.lua index 5ef739f4..420ca60b 100644 --- a/Indicators/Base.lua +++ b/Indicators/Base.lua @@ -89,17 +89,26 @@ end ------------------------------------------------- -- Apply font settings to Blizzard's CooldownFrame countdown text (Midnight) -- Only applies to CooldownFrame cooldowns (BorderIcon), not StatusBar (BarIcon) +local function GetCountdownFrame(frame) + return frame._countdownCooldown or frame.cooldown +end + local function ApplyCountdownFont(frame, font2) - if not frame.cooldown then return end - if not frame.cooldown.GetCountdownFontString then return end - local cdText = frame.cooldown:GetCountdownFontString() + local countdown = GetCountdownFrame(frame) + if not countdown then return end + if not countdown.GetCountdownFontString then return end + local cdText = countdown:GetCountdownFontString() if not cdText then return end - -- Re-parent to iconFrame so text renders above icon, and center on the icon - if frame.iconFrame and cdText:GetParent() ~= frame.iconFrame then - cdText:SetParent(frame.iconFrame) + + local textParent = frame._countdownTextParent or frame.iconFrame + if textParent then + if cdText:GetParent() ~= textParent then + cdText:SetParent(textParent) + end cdText:ClearAllPoints() - cdText:SetPoint("CENTER", frame.iconFrame, "CENTER", 0, 0) + cdText:SetPoint("CENTER", textParent, "CENTER", 0, 0) end + if font2 then local fontFace = F.GetFont(font2[1]) or cdText:GetFont() local fontSize = font2[2] or 11 @@ -136,6 +145,9 @@ local function Shared_ShowDuration(frame, show) frame.duration:SetShown(show) end +local STATUSBAR_INTERPOLATION_NONE = Enum and Enum.StatusBarInterpolation and Enum.StatusBarInterpolation.None +local STATUSBAR_TIMER_DIRECTION_ELAPSED = Enum and Enum.StatusBarTimerDirection and Enum.StatusBarTimerDirection.ElapsedTime + ------------------------------------------------- -- VerticalCooldown ------------------------------------------------- @@ -276,8 +288,10 @@ end -- SetCooldownStyle ------------------------------------------------- local function Shared_SetCooldownStyle(frame, style, noIcon) + style = style == "CLOCK" and "CLOCK" or "VERTICAL" if frame.style == style then return end + local oldCooldown = frame.cooldown if frame.cooldown then frame.cooldown:SetParent(nil) frame.cooldown:Hide() @@ -294,6 +308,13 @@ local function Shared_SetCooldownStyle(frame, style, noIcon) Shared_CreateCooldown_Vertical(frame) end end + + if oldCooldown and frame.stack and frame.stack:GetParent() == oldCooldown then + frame.stack:SetParent(frame.cooldown) + end + if oldCooldown and frame.duration and frame.duration:GetParent() == oldCooldown then + frame.duration:SetParent(frame.cooldown) + end end -------------------------------------------------- @@ -479,6 +500,192 @@ end local _GetAuraDuration = C_UnitAuras and C_UnitAuras.GetAuraDuration local _GetAuraAppDisplayCount = C_UnitAuras and C_UnitAuras.GetAuraApplicationDisplayCount +local function BorderIcon_GetCountdownCooldown(frame) + return frame._countdownCooldown or frame.cooldown +end + +local function BorderIcon_SetCountdownVisibility(frame, show) + local countdown = BorderIcon_GetCountdownCooldown(frame) + if countdown and countdown.SetHideCountdownNumbers then + countdown:SetHideCountdownNumbers(not show) + end +end + +local function BorderIcon_CreateClockCooldown(frame) + local cooldown = CreateFrame("Cooldown", nil, frame) + frame.cooldown = cooldown + cooldown:SetAllPoints(frame) + cooldown:SetSwipeTexture(Cell.vars.whiteTexture) + cooldown:SetSwipeColor(1, 1, 1) + cooldown:SetHideCountdownNumbers(true) + if Cell.isMidnight and cooldown.SetCountdownAbbrevThreshold then + cooldown:SetCountdownAbbrevThreshold(60) + end + cooldown.noCooldownCount = true + cooldown._SetCooldown = cooldown.SetCooldown + cooldown.SetCooldown = nil + + frame._countdownTextParent = frame.iconFrame +end + +local function BorderIcon_CreateVerticalCountdown(frame) + local countdown = CreateFrame("Cooldown", nil, frame.cooldown) + frame._countdownCooldown = countdown + countdown:SetFrameLevel(frame.cooldown:GetFrameLevel() + 1) + P.Point(countdown, "TOPLEFT") + P.Point(countdown, "BOTTOMRIGHT") + countdown:SetDrawSwipe(false) + countdown:SetDrawEdge(false) + countdown:SetDrawBling(false) + countdown:SetHideCountdownNumbers(true) + if Cell.isMidnight and countdown.SetCountdownAbbrevThreshold then + countdown:SetCountdownAbbrevThreshold(60) + end + countdown.noCooldownCount = true + countdown._SetCooldown = countdown.SetCooldown + countdown.SetCooldown = nil + + frame._countdownTextParent = countdown +end + +local function BorderIcon_Vertical_OnUpdate(self, elapsed) + self.elapsed = (self.elapsed or 0) + elapsed + if self.elapsed >= 0.1 then + self._currentValue = (self._currentValue or 0) + self.elapsed + if self._duration and self._currentValue > self._duration then + self._currentValue = self._duration + end + self:SetValue(self._currentValue) + self.elapsed = 0 + + if self._duration and self._currentValue >= self._duration then + self:SetScript("OnUpdate", nil) + if not self._cooldownDoneFired then + self._cooldownDoneFired = true + local onCooldownDone = self:GetScript("OnCooldownDone") + if onCooldownDone then + onCooldownDone(self) + end + end + end + end +end + +local function BorderIcon_Vertical_ShowCooldown(self, start, duration) + self._cooldownDoneFired = false + self:SetScript("OnUpdate", BorderIcon_Vertical_OnUpdate) + self.elapsed = 0.1 + self._duration = duration + self:SetMinMaxValues(0, duration) + self._currentValue = GetTime() - start + if self._currentValue < 0 then self._currentValue = 0 end + if self._currentValue > duration then self._currentValue = duration end + self:SetValue(self._currentValue) + self:Show() +end + +local function BorderIcon_Vertical_SetDurationObject(self, durObj) + self:SetScript("OnUpdate", nil) + self.elapsed = nil + self._duration = nil + self._currentValue = nil + self:SetMinMaxValues(0, 1) + self:SetTimerDuration(durObj, STATUSBAR_INTERPOLATION_NONE, STATUSBAR_TIMER_DIRECTION_ELAPSED) + if self.SetToTargetValue then + self:SetToTargetValue() + end + self:Show() +end + +local function BorderIcon_CreateVerticalCooldown(frame) + local cooldown = CreateFrame("StatusBar", nil, frame.iconFrame) + frame.cooldown = cooldown + cooldown:Hide() + cooldown:SetFrameLevel(frame.iconFrame:GetFrameLevel() + 1) + P.Point(cooldown, "TOPLEFT") + P.Point(cooldown, "BOTTOMRIGHT") + cooldown:SetOrientation("VERTICAL") + cooldown:SetReverseFill(true) + cooldown:SetStatusBarTexture(Cell.vars.whiteTexture) + cooldown._SetScript = cooldown.SetScript + cooldown._GetScript = cooldown.GetScript + cooldown.ShowCooldown = BorderIcon_Vertical_ShowCooldown + cooldown.SetDurationObject = BorderIcon_Vertical_SetDurationObject + cooldown.SetCooldown = BorderIcon_Vertical_ShowCooldown + cooldown.SetReverse = function(self, reverse) + self:SetReverseFill(reverse) + end + cooldown.SetSwipeColor = function(self, r, g, b, a) + self:GetStatusBarTexture():SetVertexColor(r, g, b, a or 0.77) + end + cooldown.SetScript = function(self, scriptType, handler) + if scriptType == "OnCooldownDone" then + self._onCooldownDone = handler + return + end + return self:_SetScript(scriptType, handler) + end + cooldown.GetScript = function(self, scriptType) + if scriptType == "OnCooldownDone" then + return self._onCooldownDone + end + return self:_GetScript(scriptType) + end + cooldown:GetStatusBarTexture():SetVertexColor(0, 0, 0, 0.77) + + local spark = cooldown:CreateTexture(nil, "OVERLAY") + cooldown.spark = spark + P.Height(spark, 1) + spark:SetColorTexture(0.7, 0.7, 0.7, 0.9) + spark:SetPoint("TOPLEFT", cooldown:GetStatusBarTexture(), "BOTTOMLEFT") + spark:SetPoint("TOPRIGHT", cooldown:GetStatusBarTexture(), "BOTTOMRIGHT") + + if Cell.isMidnight then + BorderIcon_CreateVerticalCountdown(frame) + else + frame._countdownTextParent = cooldown + end +end + +local function BorderIcon_SetCooldownStyle(frame, style) + style = style == "CLOCK" and "CLOCK" or "VERTICAL" + if frame.style == style then return end + + if frame.cooldown then + frame.cooldown:SetParent(nil) + frame.cooldown:Hide() + frame.cooldown = nil + end + if frame._countdownCooldown then + frame._countdownCooldown:SetParent(nil) + frame._countdownCooldown:Hide() + frame._countdownCooldown = nil + end + + frame.style = style + frame._countdownTextParent = nil + frame._countdownFontApplied = false + + if style == "CLOCK" then + BorderIcon_CreateClockCooldown(frame) + frame.stack:SetParent(frame.iconFrame) + frame.duration:SetParent(frame.iconFrame) + else + BorderIcon_CreateVerticalCooldown(frame) + frame.stack:SetParent(frame.cooldown) + frame.duration:SetParent(frame.cooldown) + end + + if Cell.isMidnight then + frame.duration:Hide() + BorderIcon_SetCountdownVisibility(frame, frame.showDuration) + if frame.showDuration then + ApplyCountdownFont(frame, frame._durationFont) + frame._countdownFontApplied = true + end + end +end + -- BorderIcon: SetCooldownFromAura — drives CooldownFrame with DurationObject local function BorderIcon_SetCooldownFromAura(frame, unit, auraInstanceID, texture, refreshing) -- Icon and stack @@ -494,27 +701,40 @@ local function BorderIcon_SetCooldownFromAura(frame, unit, auraInstanceID, textu -- Swipe color defaults to black; UnitButton.lua overrides border/swipe color -- for dispel types via bracket curves after this call. local durObj = _GetAuraDuration and _GetAuraDuration(unit, auraInstanceID) - if durObj and frame.cooldown and frame.cooldown._SetCooldown + if durObj and frame.style == "VERTICAL" and frame.cooldown and frame.cooldown.SetDurationObject then + frame.cooldown:SetDurationObject(durObj) + if frame._countdownCooldown and frame._countdownCooldown.SetCooldownFromDurationObject then + frame._countdownCooldown:SetReverse(true) + frame._countdownCooldown:SetCooldownFromDurationObject(durObj, true) + BorderIcon_SetCountdownVisibility(frame, frame.showDuration) + if frame.showDuration and not frame._countdownFontApplied then + ApplyCountdownFont(frame, frame._durationFont) + frame._countdownFontApplied = true + end + frame._countdownCooldown:Show() + end + elseif durObj and frame.cooldown and frame.cooldown._SetCooldown and frame.cooldown.SetCooldownFromDurationObject then - -- Countdown numbers visibility is managed by BorderIcon_ShowDuration frame.cooldown:SetReverse(true) frame.cooldown:SetCooldownFromDurationObject(durObj, true) - -- Apply font settings once (cached via _countdownFontApplied flag) - if not frame._countdownFontApplied then + BorderIcon_SetCountdownVisibility(frame, frame.showDuration) + if frame.showDuration and not frame._countdownFontApplied then ApplyCountdownFont(frame, frame._durationFont) frame._countdownFontApplied = true end - -- Keep border visible as base color (caller sets color); black swipe fills over it frame.cooldown:Show() else - -- No cooldown animation — show static border + if frame._countdownCooldown then + frame._countdownCooldown:Hide() + end frame.border:Show() frame.border:SetColorTexture(0, 0, 0) frame.cooldown:Hide() end - -- Duration text hidden on Midnight (SetFormattedText produces invisible output with secrets) - frame.duration:Hide() + if Cell.isMidnight then + frame.duration:Hide() + end frame:SetScript("OnUpdate", nil) frame:Show() @@ -584,6 +804,9 @@ local function BorderIcon_SetCooldown(frame, start, duration, debuffType, textur frame.border:Show() frame.border:SetColorTexture(r, g, b) frame.cooldown:Hide() + if frame._countdownCooldown then + frame._countdownCooldown:Hide() + end frame.duration:Hide() frame:SetScript("OnUpdate", nil) frame._start = nil @@ -593,29 +816,84 @@ local function BorderIcon_SetCooldown(frame, start, duration, debuffType, textur frame._threshold = nil frame._elapsedTime = nil else - frame.border:Hide() - frame.cooldown:Show() - frame.cooldown:SetSwipeColor(r, g, b) - frame.cooldown:_SetCooldown(start, duration) + frame:SetScript("OnUpdate", nil) + frame._start = nil + frame._duration = nil + frame._remain = nil + frame._elapsed = nil + frame._threshold = nil + frame._elapsedTime = nil - if not frame.showDuration then - frame.duration:Hide() + if frame.style == "VERTICAL" then + frame.border:Show() + frame.border:SetColorTexture(r, g, b) + frame.cooldown:ShowCooldown(start, duration) + if Cell.isMidnight and frame._countdownCooldown and frame._countdownCooldown._SetCooldown then + frame.duration:Hide() + frame._countdownCooldown:_SetCooldown(start, duration) + BorderIcon_SetCountdownVisibility(frame, frame.showDuration) + if frame.showDuration then + ApplyCountdownFont(frame, frame._durationFont) + frame._countdownFontApplied = true + end + frame._countdownCooldown:Show() + else + if not frame.showDuration then + frame.duration:Hide() + else + if frame.showDuration == true then + frame._threshold = duration + elseif frame.showDuration >= 1 then + frame._threshold = frame.showDuration + else -- < 1 + frame._threshold = frame.showDuration * duration + end + frame.duration:Show() + frame._start = start + frame._duration = duration + frame._elapsed = 0.1 -- update immediately + frame:SetScript("OnUpdate", useElapsedTime and Icon_OnUpdate_ElapsedTime or Icon_OnUpdate) + end + end else - if frame.showDuration == true then - frame._threshold = duration - elseif frame.showDuration >= 1 then - frame._threshold = frame.showDuration - else -- < 1 - frame._threshold = frame.showDuration * duration + frame.border:Hide() + frame.cooldown:Show() + frame.cooldown:SetSwipeColor(r, g, b) + frame.cooldown:_SetCooldown(start, duration) + + frame:SetScript("OnUpdate", nil) + frame._start = nil + frame._duration = nil + frame._remain = nil + frame._elapsed = nil + frame._threshold = nil + frame._elapsedTime = nil + + if Cell.isMidnight and frame.cooldown.SetHideCountdownNumbers then + frame.duration:Hide() + BorderIcon_SetCountdownVisibility(frame, frame.showDuration) + if frame.showDuration then + ApplyCountdownFont(frame, frame._durationFont) + frame._countdownFontApplied = true + end + else + if not frame.showDuration then + frame.duration:Hide() + else + if frame.showDuration == true then + frame._threshold = duration + elseif frame.showDuration >= 1 then + frame._threshold = frame.showDuration + else -- < 1 + frame._threshold = frame.showDuration * duration + end + frame.duration:Show() + frame._start = start + frame._duration = duration + frame._elapsed = 0.1 -- update immediately + frame:SetScript("OnUpdate", useElapsedTime and Icon_OnUpdate_ElapsedTime or Icon_OnUpdate) + end end - frame.duration:Show() - end - - if frame.showDuration then - frame._start = start - frame._duration = duration - frame._elapsed = 0.1 -- update immediately - frame:SetScript("OnUpdate", useElapsedTime and Icon_OnUpdate_ElapsedTime or Icon_OnUpdate) end end @@ -636,11 +914,15 @@ end local function BorderIcon_ShowDuration(frame, show) frame.showDuration = show - if Cell.isMidnight and frame.cooldown and frame.cooldown.SetHideCountdownNumbers then + if Cell.isMidnight then -- Midnight: Cell's duration text is always hidden (produces invisible output -- with secrets). Only toggle Blizzard's built-in countdown. frame.duration:Hide() - frame.cooldown:SetHideCountdownNumbers(not show) + BorderIcon_SetCountdownVisibility(frame, show) + if show then + ApplyCountdownFont(frame, frame._durationFont) + frame._countdownFontApplied = true + end else -- Pre-Midnight: use Cell's own duration text if show then @@ -657,6 +939,15 @@ local function BorderIcon_UpdatePixelPerfect(frame) P.Repoint(frame.iconFrame) P.Repoint(frame.stack) P.Repoint(frame.duration) + if frame.cooldown then + P.Repoint(frame.cooldown) + if frame.cooldown.spark then + P.Resize(frame.cooldown.spark) + end + end + if frame._countdownCooldown then + P.Repoint(frame._countdownCooldown) + end end function I.CreateAura_BorderIcon(name, parent, borderSize) @@ -671,27 +962,11 @@ function I.CreateAura_BorderIcon(name, parent, borderSize) border:SetAllPoints(frame) border:Hide() - local cooldown = CreateFrame("Cooldown", name.."Cooldown", frame) - frame.cooldown = cooldown - cooldown:SetAllPoints(frame) - cooldown:SetSwipeTexture(Cell.vars.whiteTexture) - cooldown:SetSwipeColor(1, 1, 1) - cooldown:SetHideCountdownNumbers(true) - -- Midnight: set abbreviation threshold once at creation (shows "1m" above 60s) - if Cell.isMidnight and cooldown.SetCountdownAbbrevThreshold then - cooldown:SetCountdownAbbrevThreshold(60) - end - -- disable omnicc - cooldown.noCooldownCount = true - -- prevent some addons from adding cooldown text - cooldown._SetCooldown = cooldown.SetCooldown - cooldown.SetCooldown = nil - local iconFrame = CreateFrame("Frame", name.."IconFrame", frame) frame.iconFrame = iconFrame P.Point(iconFrame, "TOPLEFT", frame, "TOPLEFT", borderSize, -borderSize) P.Point(iconFrame, "BOTTOMRIGHT", frame, "BOTTOMRIGHT", -borderSize, borderSize) - iconFrame:SetFrameLevel(cooldown:GetFrameLevel()+1) + iconFrame:SetFrameLevel(frame:GetFrameLevel() + 1) local icon = iconFrame:CreateTexture(name.."Icon", "ARTWORK") frame.icon = icon @@ -719,6 +994,7 @@ function I.CreateAura_BorderIcon(name, parent, borderSize) frame.SetCooldown = BorderIcon_SetCooldown frame.SetCooldownFromAura = BorderIcon_SetCooldownFromAura frame.ShowDuration = BorderIcon_ShowDuration + frame.SetCooldownStyle = BorderIcon_SetCooldownStyle -- BarIcon-compatible methods (no-ops for BorderIcon, needed when used as -- cooldown indicator child frames which call these on all children) frame.ShowAnimation = function() end @@ -726,6 +1002,8 @@ function I.CreateAura_BorderIcon(name, parent, borderSize) frame.SetupGlow = function() end frame.UpdatePixelPerfect = BorderIcon_UpdatePixelPerfect + BorderIcon_SetCooldownStyle(frame, Cell.isMidnight and CELL_COOLDOWN_STYLE or "CLOCK") + return frame end @@ -799,6 +1077,12 @@ local function BarIcon_ShowAnimation(frame, show) end end +local function BarIcon_SetCooldownStyle(frame, style) + Shared_SetCooldownStyle(frame, style) + ReCalcTexCoord(frame, frame:GetSize()) + frame:UpdatePixelPerfect() +end + local function BarIcon_UpdatePixelPerfect(frame) P.Resize(frame) P.Repoint(frame) @@ -847,6 +1131,7 @@ function I.CreateAura_BarIcon(name, parent) frame.ShowDuration = Shared_ShowDuration frame.ShowStack = Shared_ShowStack frame.ShowAnimation = BarIcon_ShowAnimation + frame.SetCooldownStyle = BarIcon_SetCooldownStyle frame.SetupGlow = Shared_SetupGlow frame.UpdatePixelPerfect = BarIcon_UpdatePixelPerfect @@ -1058,6 +1343,14 @@ local function Icons_ShowAnimation(icons, show) end end +local function Icons_SetCooldownStyle(icons, style) + for i = 1, icons.maxNum do + if icons[i].SetCooldownStyle then + icons[i]:SetCooldownStyle(style) + end + end +end + local function Icons_UpdatePixelPerfect(icons) P.Repoint(icons) P.Resize(icons) @@ -1088,6 +1381,7 @@ function I.CreateAura_Icons(name, parent, num) icons.ShowDuration = Icons_ShowDuration icons.ShowStack = Icons_ShowStack icons.ShowAnimation = Icons_ShowAnimation + icons.SetCooldownStyle = Icons_SetCooldownStyle icons.SetupGlow = I.Glow_SetupForChildren icons.UpdatePixelPerfect = Icons_UpdatePixelPerfect @@ -2335,6 +2629,11 @@ local function Block_UpdatePixelPerfect(frame) end end +local function Block_SetCooldownStyle(frame, style) + Shared_SetCooldownStyle(frame, style, true) + frame:UpdatePixelPerfect() +end + function I.CreateAura_Block(name, parent) local frame = CreateFrame("Frame", name, parent, "BackdropTemplate") frame:Hide() @@ -2352,6 +2651,7 @@ function I.CreateAura_Block(name, parent) frame.ShowStack = Shared_ShowStack frame.ShowDuration = Shared_ShowDuration frame.SetCooldown = Block_SetCooldown_Duration + frame.SetCooldownStyle = Block_SetCooldownStyle frame.SetupGlow = Shared_SetupGlow frame.UpdatePixelPerfect = Block_UpdatePixelPerfect @@ -2439,6 +2739,14 @@ local function Blocks_SetCooldown(frame, start, duration, debuffType, texture, c end end +local function Blocks_SetCooldownStyle(blocks, style) + for i = 1, blocks.maxNum do + if blocks[i].SetCooldownStyle then + blocks[i]:SetCooldownStyle(style) + end + end +end + function I.CreateAura_Blocks(name, parent, num) local blocks = CreateFrame("Frame", name, parent) blocks:Hide() @@ -2458,6 +2766,7 @@ function I.CreateAura_Blocks(name, parent, num) blocks.SetNumPerLine = Icons_SetNumPerLine blocks.ShowDuration = Icons_ShowDuration blocks.ShowStack = Icons_ShowStack + blocks.SetCooldownStyle = Blocks_SetCooldownStyle blocks.SetupGlow = I.Glow_SetupForChildren blocks.UpdatePixelPerfect = Icons_UpdatePixelPerfect @@ -2556,4 +2865,4 @@ function I.CreateAura_Border(name, parent) border.UpdatePixelPerfect = Border_UpdatePixelPerfect return border -end \ No newline at end of file +end diff --git a/Indicators/Built-in.lua b/Indicators/Built-in.lua index d095f1d6..2861733f 100644 --- a/Indicators/Built-in.lua +++ b/Indicators/Built-in.lua @@ -109,6 +109,14 @@ function I.Cooldowns_ShowAnimation(self, show) end end +function I.Cooldowns_SetCooldownStyle(self, style) + for i = 1, #self do + if self[i].SetCooldownStyle then + self[i]:SetCooldownStyle(style) + end + end +end + function I.Cooldowns_UpdatePixelPerfect(self) P.Repoint(self) for i = 1, #self do @@ -214,6 +222,7 @@ function I.CreateDefensiveCooldowns(parent) defensiveCooldowns.SetOrientation = I.Cooldowns_SetOrientation defensiveCooldowns.ShowDuration = I.Cooldowns_ShowDuration defensiveCooldowns.ShowAnimation = I.Cooldowns_ShowAnimation + defensiveCooldowns.SetCooldownStyle = I.Cooldowns_SetCooldownStyle defensiveCooldowns.SetupGlow = I.Glow_SetupForChildren defensiveCooldowns.UpdatePixelPerfect = I.Cooldowns_UpdatePixelPerfect @@ -241,6 +250,7 @@ function I.CreateExternalCooldowns(parent) externalCooldowns.SetOrientation = I.Cooldowns_SetOrientation externalCooldowns.ShowDuration = I.Cooldowns_ShowDuration externalCooldowns.ShowAnimation = I.Cooldowns_ShowAnimation + externalCooldowns.SetCooldownStyle = I.Cooldowns_SetCooldownStyle externalCooldowns.SetupGlow = I.Glow_SetupForChildren externalCooldowns.UpdatePixelPerfect = I.Cooldowns_UpdatePixelPerfect @@ -268,6 +278,7 @@ function I.CreateAllCooldowns(parent) allCooldowns.SetOrientation = I.Cooldowns_SetOrientation allCooldowns.ShowDuration = I.Cooldowns_ShowDuration allCooldowns.ShowAnimation = I.Cooldowns_ShowAnimation + allCooldowns.SetCooldownStyle = I.Cooldowns_SetCooldownStyle allCooldowns.SetupGlow = I.Glow_SetupForChildren allCooldowns.UpdatePixelPerfect = I.Cooldowns_UpdatePixelPerfect @@ -471,7 +482,8 @@ local function Debuffs_EnableBlacklistShortcut(debuffs, enabled) if enabled then debuffs[i]:SetScript("OnMouseUp", function(self, button, isInside) if button == "RightButton" and isInside and IsLeftAltKeyDown() and IsLeftControlKeyDown() - and self.spellId and not F.TContains(CellDB["debuffBlacklist"], self.spellId) then + and self.spellId and F.IsValueNonSecret(self.spellId) + and not F.TContains(CellDB["debuffBlacklist"], self.spellId) then -- print msg local name, icon = F.GetSpellInfo(self.spellId) if name and icon then @@ -515,6 +527,7 @@ function I.CreateDebuffs(parent) debuffs.ShowDuration = I.Cooldowns_ShowDuration debuffs.ShowAnimation = I.Cooldowns_ShowAnimation + debuffs.SetCooldownStyle = I.Cooldowns_SetCooldownStyle debuffs.UpdatePixelPerfect = I.Cooldowns_UpdatePixelPerfect debuffs.ShowTooltip = Debuffs_ShowTooltip @@ -2640,6 +2653,7 @@ function I.CreateMissingBuffs(parent) missingBuffs.SetSize = I.Cooldowns_SetSize missingBuffs.UpdateSize = I.Cooldowns_UpdateSize missingBuffs.SetOrientation = I.Cooldowns_SetOrientation + missingBuffs.SetCooldownStyle = I.Cooldowns_SetCooldownStyle missingBuffs.UpdatePixelPerfect = I.Cooldowns_UpdatePixelPerfect for i = 1, 3 do @@ -2697,4 +2711,4 @@ function I.ShowMissingBuff(unit, icon) if missingBuffsCounter[unit] > 3 then return end F.HandleUnitButton("unit", unit, ShowMissingBuff, missingBuffsCounter[unit], icon) -end \ No newline at end of file +end diff --git a/Locales/enUS.lua b/Locales/enUS.lua index 539b867f..fee7be99 100644 --- a/Locales/enUS.lua +++ b/Locales/enUS.lua @@ -32,6 +32,9 @@ select(2, ...).L = setmetatable({ ["showAnimation"] = "Show animation", ["showStack"] = "Show stack text", ["showTooltip"] = "Show aura tooltip", + ["Cooldown Style"] = "Cooldown Style", + ["Clock"] = "Clock", + ["Vertical"] = "Vertical", ["enableHighlight"] = "Highlight unit button", ["hideIfEmptyOrFull"] = "Hide if empty/full", ["onlyShowTopGlow"] = "Only show glow for the first debuff", @@ -1760,4 +1763,4 @@ select(2, ...).L = setmetatable({ return Key end end -}) \ No newline at end of file +}) diff --git a/Locales/ptBR.lua b/Locales/ptBR.lua index 2488f6cd..d6a8628c 100644 --- a/Locales/ptBR.lua +++ b/Locales/ptBR.lua @@ -118,6 +118,8 @@ L["change the order"] = "mudar a ordem" L["Changelogs"] = "Changelogs" L["Check all visible enemy nameplates."] = "Verifique todas as nameplates inimigas visíveis." L["Check If Exists"] = "Verifique se existe" +L["Clock"] = "Relógio" +L["Cooldown Style"] = "Estilo do Cooldown" L["Check if your group members need some raid buffs"] = "Verifique se os membros do seu grupo precisam de alguns buffs de raid" L["circledStackNums"] = "Números de stack circulados" L["Class Color"] = "Cor da Classe" @@ -719,4 +721,4 @@ L["You"] = "Você" L["You can config debuffs in %s"] = "Você pode configurar debuffs em %s" L["You can move it in Preview mode"] = "Você pode movê-lo no modo Preview" L["You can't do that while in combat."] = "Você não pode fazer isso em combate." -L["You don't have permission to do this"] = "Você não tem permissão pra fazer isso." \ No newline at end of file +L["You don't have permission to do this"] = "Você não tem permissão pra fazer isso." diff --git a/Modules/Appearance/Appearance.lua b/Modules/Appearance/Appearance.lua index 1020fedd..950d5779 100644 --- a/Modules/Appearance/Appearance.lua +++ b/Modules/Appearance/Appearance.lua @@ -593,9 +593,50 @@ local gradientCB, thresholdCP1, thresholdCP2, thresholdCP3, thresholdDropdown, c local gradientLossCB, thresholdLossCP1, thresholdLossCP2, thresholdLossCP3, thresholdLossDropdown1, thresholdLossDropdown2 local barAlpha, lossAlpha, bgAlpha, oorAlpha, predCB, absorbCB, invertColorCB, shieldCB, oversCB, reverseCB local predCustomCB, predColorPicker, absorbColorPicker, shieldColorPicker, oversColorPicker -local iconOptionsBtn, iconOptionsFrame, iconAnimationDropdown, durationRoundUpCB, durationDecimalText1, durationDecimalText2, durationDecimalDropdown, durationColorCB, durationNormalCP, durationPercentCP, durationSecondCP, durationPercentDD, durationSecondEB, durationSecondText +local iconOptionsBtn, iconOptionsFrame, cooldownStyleDropdown, iconAnimationDropdown, durationRoundUpCB, durationDecimalText1, durationDecimalText2, durationDecimalDropdown, durationColorCB, durationNormalCP, durationPercentCP, durationSecondCP, durationPercentDD, durationSecondEB, durationSecondText local LSM = LibStub("LibSharedMedia-3.0", true) + +local function GetConfiguredCooldownStyle() + local style = CellDB["appearance"]["cooldownStyle"] + if style ~= "CLOCK" and style ~= "VERTICAL" then + style = CELL_COOLDOWN_STYLE == "CLOCK" and "CLOCK" or "VERTICAL" + CellDB["appearance"]["cooldownStyle"] = style + end + return style +end + +local function ApplyCooldownStyleToButton(button, style) + if not button or not button.indicators then return end + + for _, indicator in pairs(button.indicators) do + if indicator and indicator.SetCooldownStyle then + indicator:SetCooldownStyle(style) + end + end +end + +local function UpdatePreviewCooldownStyle(style) + if not barIcon1 or not barIcon2 then return end + + if barIcon1.SetCooldownStyle then + barIcon1:SetCooldownStyle(style) + end + if barIcon2.SetCooldownStyle then + barIcon2:SetCooldownStyle(style) + end + + barIcon1:ShowAnimation(true) + barIcon2:ShowAnimation(true) + + if barIcon1:IsShown() then + barIcon1:SetCooldown(GetTime(), 13, "", 132155, 5) + end + if barIcon2:IsShown() then + barIcon2:SetCooldown(GetTime(), 13, nil, 136085, 0) + end +end + local function CheckTextures() local items = {} local textures, textureNames @@ -649,7 +690,7 @@ local function CreateIconOptionsFrame() appearanceTab.mask:Hide() end - iconOptionsFrame = Cell.CreateFrame("CellOptionsFrame_IconOptions", appearanceTab, 230, 235) + iconOptionsFrame = Cell.CreateFrame("CellOptionsFrame_IconOptions", appearanceTab, 230, 270) iconOptionsFrame:SetBackdropBorderColor(unpack(Cell.GetAccentColorTable())) iconOptionsFrame:SetPoint("TOP", iconOptionsBtn, "BOTTOM", 0, -5) iconOptionsFrame:SetPoint("RIGHT", -5, 0) @@ -665,9 +706,34 @@ local function CreateIconOptionsFrame() iconOptionsBtn:SetFrameLevel(appearanceTab:GetFrameLevel() + 1) end) + cooldownStyleDropdown = Cell.CreateDropdown(iconOptionsFrame, 180) + cooldownStyleDropdown:SetPoint("TOPLEFT", iconOptionsFrame, 10, -25) + cooldownStyleDropdown:SetItems({ + { + ["text"] = L["Vertical"], + ["value"] = "VERTICAL", + ["onClick"] = function() + CellDB["appearance"]["cooldownStyle"] = "VERTICAL" + Cell.Fire("UpdateAppearance", "cooldownStyle") + end, + }, + { + ["text"] = L["Clock"], + ["value"] = "CLOCK", + ["onClick"] = function() + CellDB["appearance"]["cooldownStyle"] = "CLOCK" + Cell.Fire("UpdateAppearance", "cooldownStyle") + end, + }, + }) + + local cooldownStyleText = iconOptionsFrame:CreateFontString(nil, "OVERLAY", "CELL_FONT_WIDGET") + cooldownStyleText:SetPoint("BOTTOMLEFT", cooldownStyleDropdown, "TOPLEFT", 0, 1) + cooldownStyleText:SetText(L["Cooldown Style"]) + -- icon animation iconAnimationDropdown = Cell.CreateDropdown(iconOptionsFrame, 180) - iconAnimationDropdown:SetPoint("TOPLEFT", iconOptionsFrame, 10, -25) + iconAnimationDropdown:SetPoint("TOPLEFT", cooldownStyleDropdown, "BOTTOMLEFT", 0, -25) iconAnimationDropdown:SetItems({ { ["text"] = L["+ Stack & Duration"], @@ -1630,31 +1696,7 @@ end -- functions ------------------------------------------------- local init -LoadButtonStyle = function() - if not init then CheckTextures() end - - UpdateColorPickers() - UpdateCheckButtons() - - barColorDropdown:SetSelectedValue(CellDB["appearance"]["barColor"][1]) - barColorPicker:SetColor(CellDB["appearance"]["barColor"][2]) - - fullColorCB:SetChecked(CellDB["appearance"]["fullColor"][1]) - fullColorPicker:SetColor(CellDB["appearance"]["fullColor"][2]) - fullColorPicker:SetEnabled(CellDB["appearance"]["fullColor"][1]) - - lossColorDropdown:SetSelectedValue(CellDB["appearance"]["lossColor"][1]) - lossColorPicker:SetColor(CellDB["appearance"]["lossColor"][2]) - - deathColorCB:SetChecked(CellDB["appearance"]["deathColor"][1]) - deathColorPicker:SetColor(CellDB["appearance"]["deathColor"][2]) - deathColorPicker:SetEnabled(CellDB["appearance"]["deathColor"][1]) - - powerColorDropdown:SetSelectedValue(CellDB["appearance"]["powerColor"][1]) - powerColorPicker:SetColor(CellDB["appearance"]["powerColor"][2]) - - barAnimationDropdown:SetSelected(L[CellDB["appearance"]["barAnimation"]]) - +local function LoadThresholdWidgets() local c = CellDB["appearance"]["colorThresholds"] gradientCB:SetChecked(c[6]) thresholdCP1:SetColor(c[1][1], c[1][2], c[1][3]) @@ -1670,7 +1712,9 @@ LoadButtonStyle = function() thresholdLossCP3:SetColor(d[3][1], d[3][2], d[3][3]) thresholdLossDropdown1:SetSelectedValue(d[4]) thresholdLossDropdown2:SetSelectedValue(d[5]) +end +local function LoadShieldAndAlphaWidgets() targetColorPicker:SetColor(CellDB["appearance"]["targetColor"]) mouseoverColorPicker:SetColor(CellDB["appearance"]["mouseoverColor"]) highlightSize:SetValue(CellDB["appearance"]["highlightSize"]) @@ -1680,7 +1724,6 @@ LoadButtonStyle = function() bgAlpha:SetValue(CellDB["appearance"]["bgAlpha"]*100) predCB:SetChecked(CellDB["appearance"]["healPrediction"][1]) - -- useLibCB:SetChecked(CellDB["appearance"]["useLibHealComm"]) absorbCB:SetChecked(CellDB["appearance"]["healAbsorb"][1]) invertColorCB:SetChecked(CellDB["appearance"]["healAbsorbInvertColor"]) shieldCB:SetChecked(CellDB["appearance"]["shield"][1]) @@ -1692,19 +1735,53 @@ LoadButtonStyle = function() absorbColorPicker:SetColor(unpack(CellDB["appearance"]["healAbsorb"][2])) shieldColorPicker:SetColor(unpack(CellDB["appearance"]["shield"][2])) oversColorPicker:SetColor(unpack(CellDB["appearance"]["overshield"][2])) +end - -- icon options - iconAnimationDropdown:SetSelectedValue(CellDB["appearance"]["auraIconOptions"]["animation"]) - durationRoundUpCB:SetChecked(CellDB["appearance"]["auraIconOptions"]["durationRoundUp"]) - Cell.SetEnabled(not CellDB["appearance"]["auraIconOptions"]["durationRoundUp"], durationDecimalText1, durationDecimalText2, durationDecimalDropdown) - durationDecimalDropdown:SetSelectedValue(CellDB["appearance"]["auraIconOptions"]["durationDecimal"]) - durationColorCB:SetChecked(CellDB["appearance"]["auraIconOptions"]["durationColorEnabled"]) - Cell.SetEnabled(CellDB["appearance"]["auraIconOptions"]["durationColorEnabled"], durationNormalCP, durationPercentCP, durationPercentDD, durationSecondCP, durationSecondEB, durationSecondText) - durationNormalCP:SetColor(CellDB["appearance"]["auraIconOptions"]["durationColors"][1]) - durationPercentCP:SetColor(CellDB["appearance"]["auraIconOptions"]["durationColors"][2][1], CellDB["appearance"]["auraIconOptions"]["durationColors"][2][2], CellDB["appearance"]["auraIconOptions"]["durationColors"][2][3]) - durationPercentDD:SetSelectedValue(CellDB["appearance"]["auraIconOptions"]["durationColors"][2][4]) - durationSecondCP:SetColor(CellDB["appearance"]["auraIconOptions"]["durationColors"][3][1], CellDB["appearance"]["auraIconOptions"]["durationColors"][3][2], CellDB["appearance"]["auraIconOptions"]["durationColors"][3][3]) - durationSecondEB:SetText(CellDB["appearance"]["auraIconOptions"]["durationColors"][3][4]) +local function LoadIconOptionWidgets() + local auraIconOptions = CellDB["appearance"]["auraIconOptions"] + + cooldownStyleDropdown:SetSelectedValue(GetConfiguredCooldownStyle()) + iconAnimationDropdown:SetSelectedValue(auraIconOptions["animation"]) + durationRoundUpCB:SetChecked(auraIconOptions["durationRoundUp"]) + Cell.SetEnabled(not auraIconOptions["durationRoundUp"], durationDecimalText1, durationDecimalText2, durationDecimalDropdown) + durationDecimalDropdown:SetSelectedValue(auraIconOptions["durationDecimal"]) + durationColorCB:SetChecked(auraIconOptions["durationColorEnabled"]) + Cell.SetEnabled(auraIconOptions["durationColorEnabled"], durationNormalCP, durationPercentCP, durationPercentDD, durationSecondCP, durationSecondEB, durationSecondText) + durationNormalCP:SetColor(auraIconOptions["durationColors"][1]) + durationPercentCP:SetColor(auraIconOptions["durationColors"][2][1], auraIconOptions["durationColors"][2][2], auraIconOptions["durationColors"][2][3]) + durationPercentDD:SetSelectedValue(auraIconOptions["durationColors"][2][4]) + durationSecondCP:SetColor(auraIconOptions["durationColors"][3][1], auraIconOptions["durationColors"][3][2], auraIconOptions["durationColors"][3][3]) + durationSecondEB:SetText(auraIconOptions["durationColors"][3][4]) +end + +LoadButtonStyle = function() + if not init then CheckTextures() end + + UpdateColorPickers() + UpdateCheckButtons() + + barColorDropdown:SetSelectedValue(CellDB["appearance"]["barColor"][1]) + barColorPicker:SetColor(CellDB["appearance"]["barColor"][2]) + + fullColorCB:SetChecked(CellDB["appearance"]["fullColor"][1]) + fullColorPicker:SetColor(CellDB["appearance"]["fullColor"][2]) + fullColorPicker:SetEnabled(CellDB["appearance"]["fullColor"][1]) + + lossColorDropdown:SetSelectedValue(CellDB["appearance"]["lossColor"][1]) + lossColorPicker:SetColor(CellDB["appearance"]["lossColor"][2]) + + deathColorCB:SetChecked(CellDB["appearance"]["deathColor"][1]) + deathColorPicker:SetColor(CellDB["appearance"]["deathColor"][2]) + deathColorPicker:SetEnabled(CellDB["appearance"]["deathColor"][1]) + + powerColorDropdown:SetSelectedValue(CellDB["appearance"]["powerColor"][1]) + powerColorPicker:SetColor(CellDB["appearance"]["powerColor"][2]) + + barAnimationDropdown:SetSelected(L[CellDB["appearance"]["barAnimation"]]) + + LoadThresholdWidgets() + LoadShieldAndAlphaWidgets() + LoadIconOptionWidgets() end LoadDebuffTypeColor = function() @@ -1831,6 +1908,8 @@ local function UpdateAppearance(which) -- icon options if not which or which == "icon" or which == "reset" then + CELL_COOLDOWN_STYLE = GetConfiguredCooldownStyle() + -- animation Cell.vars.iconAnimation = CellDB["appearance"]["auraIconOptions"]["animation"] @@ -1848,6 +1927,25 @@ local function UpdateAppearance(which) end end + if which == "cooldownStyle" or which == "reset" then + local cooldownStyle = GetConfiguredCooldownStyle() + CELL_COOLDOWN_STYLE = cooldownStyle + + if cooldownStyleDropdown then + cooldownStyleDropdown:SetSelectedValue(cooldownStyle) + end + + if init then + UpdatePreviewCooldownStyle(cooldownStyle) + end + + F.IterateAllUnitButtons(function(b) + ApplyCooldownStyleToButton(b, cooldownStyle) + end) + + Cell.Fire("UpdateIndicators") + end + -- scale if not which or which == "scale" then CellParent:SetScale(CellDB["appearance"]["scale"]) diff --git a/Modules/Indicators/Indicators.lua b/Modules/Indicators/Indicators.lua index 7d6d9789..115e5078 100644 --- a/Modules/Indicators/Indicators.lua +++ b/Modules/Indicators/Indicators.lua @@ -126,6 +126,34 @@ local function UpdatePreviewButton() end -- indicator preview onupdate +local function ApplyMidnightBorderIconPreview(indicator, debuffType) + if indicator.duration then + indicator.duration:Hide() + end + + local countdown = indicator._countdownCooldown or indicator.cooldown + if countdown and countdown.SetHideCountdownNumbers and indicator.showDuration then + countdown:SetHideCountdownNumbers(false) + end + + if indicator.cooldown and indicator.cooldown.SetSwipeColor then + indicator.cooldown:SetSwipeColor(0, 0, 0) + end + + if indicator.border then + local r, g, b + if debuffType ~= nil then + r, g, b = I.GetDebuffTypeColor(debuffType) + elseif indicator._isPreviewPlayerCast then + r, g, b = 0, 0.8, 0 + else + r, g, b = 1, 0.85, 0 + end + indicator.border:SetColorTexture(r, g, b) + indicator.border:Show() + end +end + local function SetOnUpdate(indicator, type, icon, stack, extra) indicator.preview = indicator.preview or CreateFrame("Frame", nil, indicator) -- Midnight BorderIcon preview: use reversed swipe so the colored border @@ -137,34 +165,17 @@ local function SetOnUpdate(indicator, type, icon, stack, extra) -- Buff cooldowns (no debuff type): green = player cast, yellow = others indicator.icon:SetTexture(icon) indicator.stack:SetText(stack and stack > 1 and stack or "") - if indicator.border then - if indicator._isPreviewPlayerCast then - indicator.border:SetColorTexture(0, 0.8, 0) - else - indicator.border:SetColorTexture(1, 0.85, 0) - end - indicator.border:Show() - end if indicator.cooldown then indicator.cooldown:SetReverse(true) - indicator.cooldown:SetSwipeColor(0, 0, 0) - if indicator.showDuration then - indicator.cooldown:SetHideCountdownNumbers(false) - end indicator.cooldown:_SetCooldown(GetTime(), 13) indicator.cooldown:Show() end + ApplyMidnightBorderIconPreview(indicator) indicator:Show() else indicator:SetCooldown(GetTime(), 13, type, icon, stack or 0, false, extra) if isMidnightBorderIcon then - -- Hide Cell's duration text; Blizzard's centered countdown replaces it - if indicator.duration then - indicator.duration:Hide() - end - if indicator.cooldown and indicator.showDuration then - indicator.cooldown:SetHideCountdownNumbers(false) - end + ApplyMidnightBorderIconPreview(indicator, type) end end end @@ -468,18 +479,12 @@ local function InitIndicator(indicatorName) indicator[i]:HookScript("OnShow", function() indicator[i]:SetCooldown(GetTime(), 13, types[i], "Interface\\Icons\\INV_Misc_QuestionMark", 7) if isMidnightBorderIcon then - if indicator[i].duration then indicator[i].duration:Hide() end - if indicator[i].cooldown and indicator[i].showDuration then - indicator[i].cooldown:SetHideCountdownNumbers(false) - end + ApplyMidnightBorderIconPreview(indicator[i], types[i]) end indicator[i].cooldown:SetScript("OnCooldownDone", function() indicator[i]:SetCooldown(GetTime(), 13, types[i], "Interface\\Icons\\INV_Misc_QuestionMark", 7) if isMidnightBorderIcon then - if indicator[i].duration then indicator[i].duration:Hide() end - if indicator[i].cooldown and indicator[i].showDuration then - indicator[i].cooldown:SetHideCountdownNumbers(false) - end + ApplyMidnightBorderIconPreview(indicator[i], types[i]) end end) end) @@ -551,11 +556,19 @@ local function InitIndicator(indicatorName) {"Magic", "Interface\\Icons\\spell_shadow_psychicscream"}, {"", "Interface\\Icons\\spell_nature_earthbind"}, } + local isMidnightBorderIcon = Cell.isMidnight and indicator[1] and indicator[1].cooldown + and indicator[1].cooldown._SetCooldown and not indicator[1].cooldown.SetMinMaxValues for i = 1, 3 do indicator[i]:HookScript("OnShow", function() indicator[i]:SetCooldown(GetTime(), 13, spells[i][1], spells[i][2], 7) + if isMidnightBorderIcon then + ApplyMidnightBorderIconPreview(indicator[i], spells[i][1]) + end indicator[i].cooldown:SetScript("OnCooldownDone", function() indicator[i]:SetCooldown(GetTime(), 13, spells[i][1], spells[i][2], 7) + if isMidnightBorderIcon then + ApplyMidnightBorderIconPreview(indicator[i], spells[i][1]) + end end) end) indicator[i]:HookScript("OnHide", function() @@ -2419,7 +2432,23 @@ local function UpdateLayout() end Cell.RegisterCallback("UpdateLayout", "IndicatorsTab_UpdateLayout", UpdateLayout) -local function UpdateAppearance() +local function UpdatePreviewCooldownStyle(style) + if not previewButton or not previewButton.indicators then return end + + for _, indicator in pairs(previewButton.indicators) do + if indicator and indicator.SetCooldownStyle then + indicator:SetCooldownStyle(style) + end + end +end + +local function UpdateAppearance(which) + if which == "cooldownStyle" or which == "reset" then + local style = CellDB["appearance"] and CellDB["appearance"]["cooldownStyle"] + style = style == "CLOCK" and "CLOCK" or "VERTICAL" + UpdatePreviewCooldownStyle(style) + end + if previewButton and currentLayout == Cell.vars.currentLayout then UpdatePreviewButton() end @@ -2437,4 +2466,4 @@ local function IndicatorsChanged(layout) listButtons[1]:Click() end end -Cell.RegisterCallback("IndicatorsChanged", "IndicatorsTab_IndicatorsChanged", IndicatorsChanged) \ No newline at end of file +Cell.RegisterCallback("IndicatorsChanged", "IndicatorsTab_IndicatorsChanged", IndicatorsChanged) diff --git a/Modules/RaidDebuffs/RaidDebuffs.lua b/Modules/RaidDebuffs/RaidDebuffs.lua index bc7ee61d..4b9b6932 100644 --- a/Modules/RaidDebuffs/RaidDebuffs.lua +++ b/Modules/RaidDebuffs/RaidDebuffs.lua @@ -41,10 +41,6 @@ local encounterJournalList = { -- }, -- }, } ---@debug@ -Cell_DevExpansionData = encounterJournalList -Cell_DevExpansionNames = {} ---@end-debug@ -- used to GetInstanceInfo/GetRealZoneText --> instanceId local instanceNameMapping = { @@ -116,9 +112,6 @@ local function LoadList() for tier = 1, num do local name = EJ_GetTierInfo(tier) encounterJournalList[name] = {} - --@debug@ - tinsert(Cell_DevExpansionNames, 1, name) - --@end-debug@ if tier ~= CURRENT_SEASON_INDEX then -- don't load raid for "Current Season" LoadInstanceList(tier, "raid", encounterJournalList[name]) diff --git a/README.md b/README.md index c38dccc5..647797b2 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ # Cell +This repository is a public fork maintained by `skyking-dev`. +Upstream sync target: . The addon identity remains `Cell`. + [![version](https://img.shields.io/github/v/release/enderneko/Cell)](https://github.com/enderneko/Cell/releases) [![GitHub commit activity](https://img.shields.io/github/commit-activity/m/enderneko/Cell)](https://github.com/enderneko/Cell/commits/master) [![last commit](https://img.shields.io/github/last-commit/enderneko/Cell)](https://github.com/enderneko/Cell/commits/master) diff --git a/RaidFrames/MainFrame.lua b/RaidFrames/MainFrame.lua index 556b6a8c..36e4ebeb 100644 --- a/RaidFrames/MainFrame.lua +++ b/RaidFrames/MainFrame.lua @@ -179,21 +179,6 @@ P.Point(loadingBar, "BOTTOMRIGHT", options, -1, 1) ------------------------------------------------- -- MemoryUsage ------------------------------------------------- ---@debug@ --- local memUsage = CreateFrame("Frame", nil, cellMainFrame) --- memUsage:SetSize(10, 10) --- memUsage:SetPoint("LEFT", raid, "RIGHT", 5, 0) --- memUsage.text = memUsage:CreateFontString(nil, "OVERLAY", "CELL_FONT_WIDGET") --- memUsage.text:SetPoint("LEFT") --- memUsage:SetScript("OnUpdate", function(self, elapsed) --- self.elapsed = (self.elapsed or 0) + elapsed --- if self.elapsed > 1 then --- UpdateAddOnMemoryUsage() --- memUsage.text:SetFormattedText("%.2fMB", GetAddOnMemoryUsage("Cell")/1024) --- self.elapsed = 0 --- end --- end) ---@end-debug@ ------------------------------------------------- -- fadeIn & fadeOut diff --git a/RaidFrames/UnitButton.lua b/RaidFrames/UnitButton.lua index 0d3cdb8a..a8e82a7c 100644 --- a/RaidFrames/UnitButton.lua +++ b/RaidFrames/UnitButton.lua @@ -4757,9 +4757,10 @@ function B.SetOrientation(button, orientation, rotateTexture) F.RotateTexture(overShieldGlow, 0) -- update overShieldGlowR + local reverseShieldAnchor = Cell.isMidnight and shieldBarR:GetStatusBarTexture() or shieldBarR P.ClearPoints(overShieldGlowR) - P.Point(overShieldGlowR, "TOP", shieldBarR, "TOPLEFT", 0, 0) - P.Point(overShieldGlowR, "BOTTOM", shieldBarR, "BOTTOMLEFT", 0, 0) + P.Point(overShieldGlowR, "TOP", reverseShieldAnchor, "TOPLEFT", 0, 0) + P.Point(overShieldGlowR, "BOTTOM", reverseShieldAnchor, "BOTTOMLEFT", 0, 0) P.Width(overShieldGlowR, 8) F.RotateTexture(overShieldGlowR, 0) @@ -4848,9 +4849,10 @@ function B.SetOrientation(button, orientation, rotateTexture) F.RotateTexture(overShieldGlow, 90) -- update overShieldGlowR + local reverseShieldAnchor = Cell.isMidnight and shieldBarR:GetStatusBarTexture() or shieldBarR P.ClearPoints(overShieldGlowR) - P.Point(overShieldGlowR, "LEFT", shieldBarR, "BOTTOMLEFT", 0, 0) - P.Point(overShieldGlowR, "RIGHT", shieldBarR, "BOTTOMRIGHT", 0, 0) + P.Point(overShieldGlowR, "LEFT", reverseShieldAnchor, "BOTTOMLEFT", 0, 0) + P.Point(overShieldGlowR, "RIGHT", reverseShieldAnchor, "BOTTOMRIGHT", 0, 0) P.Height(overShieldGlowR, 8) F.RotateTexture(overShieldGlowR, 90) diff --git a/Utils.lua b/Utils.lua index c7999edd..7a1290dd 100644 --- a/Utils.lua +++ b/Utils.lua @@ -571,8 +571,13 @@ function F.Copy(t) end function F.TContains(t, v) + if not t then return false end + if F.IsValueNonSecret and not F.IsValueNonSecret(v) then return false end + for _, value in pairs(t) do - if value == v then return true end + if (not F.IsValueNonSecret or F.IsValueNonSecret(value)) and value == v then + return true + end end return false end @@ -1504,7 +1509,7 @@ function F.IsFriend(unitFlags) end function F.IsPlayer(guid) - if guid then + if guid and F.IsValueNonSecret(guid) then return string.find(guid, "^Player") end end @@ -1513,19 +1518,19 @@ function F.IsPet(guid, unit) if unit then return strfind(unit, "pet%d*$") end - if guid then + if guid and F.IsValueNonSecret(guid) then return string.find(guid, "^Pet") end end function F.IsNPC(guid) - if guid then + if guid and F.IsValueNonSecret(guid) then return string.find(guid, "^Creature") end end function F.IsVehicle(guid) - if guid then + if guid and F.IsValueNonSecret(guid) then return string.find(guid, "^Vehicle") end end @@ -2597,4 +2602,4 @@ function F.IsValueNonSecret(val) if not Cell.isMidnight then return true end if not issecretvalue then return true end return not issecretvalue(val) -end \ No newline at end of file +end From 1766f1e359e06db51c1f445827e31a58d752bed5 Mon Sep 17 00:00:00 2001 From: Skye Date: Thu, 2 Apr 2026 01:00:14 -0300 Subject: [PATCH 17/61] Midnight dispel filtering and private aura options --- .gitignore | 10 ++ CHANGELOG.md | 8 ++ CLAUDE.md | 64 ------------ Defaults/Layout_Defaults.lua | 4 +- Indicators/Built-in.lua | 135 +++++++++++++++++++------- Modules/Indicators/Indicators.lua | 74 +++++++------- RaidFrames/UnitButton.lua | 55 ++++++++--- Widgets/Widgets_IndicatorSettings.lua | 21 +++- 8 files changed, 215 insertions(+), 156 deletions(-) delete mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index 767166e6..eaedd694 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,14 @@ Libs/* !Libs/LoadLibs.xml !Libs/LoadLibs_Classic.xml !Libs/LibTranslit-1.0 + + .DS_Store +.codex +Cell-r*-release-*.zip + +# local-only notes +MIDNIGHT_API_ANALISE.md + +# local tooling / references +skills/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e2a7519..c28ee365 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# r275.6 Midnight dispel filtering and private aura options + +## Indicators +- Dispels now use Midnight's per-aura server-side dispel filter so raid frames only flag debuffs you can actually remove on that specific unit. +- Secret dispellable auras now feed the same decision path as visible dispels, improving self-only and restricted dispel handling. +- Private Auras can now anchor and display more than one Blizzard private aura at a time. +- Added a new Private Aura option to control the maximum number of displayed private aura anchors while preserving Blizzard styling restrictions. + # r275.5 Added Midnight Raid Debuffs ## Raid Debuffs diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index c918071f..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,64 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -Cell is a World of Warcraft raid frame addon by enderneko. This fork adds fixes and enhancements for the WoW 12.0 (Midnight) expansion, particularly around secret value handling and new raid/dungeon debuff data. - -## Architecture - -- **Core.lua** — Addon initialization, event handling, main namespace (`Cell`, `F`, `I`, `P`) -- **Utils.lua** — Utility functions used throughout -- **RaidFrames/UnitButton.lua** — Main unit button logic including aura processing (`HandleBuff`, `HandleDebuff`, `UnitButton_UpdateAuras`) -- **Indicators/Base.lua** — Indicator rendering system (icons, bars, text overlays on unit frames) -- **Defaults/Indicator_DefaultSpells.lua** — Spell definitions for built-in indicators -- **RaidDebuffs/** — Per-expansion raid debuff definitions (e.g., `RaidDebuffs_Midnight.lua`) -- **Widgets/** — UI widget library -- **Comm/** — Addon communication -- **Modules/** — Feature modules (click casting, raid tools, etc.) - -## Key Patterns - -- Aura iteration: `ForEachAura` (full update via `GetAuraSlots` + `GetAuraDataBySlot`) and `ForEachAuraCache` (partial update from cached auras) -- `UnitButton_UpdateAuras` handles both full and partial updates (UNIT_AURA updateInfo) -- External cooldowns: matched via `I.IsExternalCooldown(name, spellId, source, unit)` -- Dispels: `dispelName` field on debuff auras, rendered via `self.indicators.dispels:SetDispels()` - -## WoW 12.0 Secret Values (Critical) - -WoW 12.0 introduced "secret values" that crash on boolean tests: -- `secretVal or 0`, `if secretVal then`, `secretVal and x` all crash -- Safe: `issecretvalue()`, C-level APIs (`SetText`, `SetValue`, `SetVertexColor`, `SetMinMaxValues`) -- `rawequal(x, nil)` is safe for nil checks on potentially-secret values -- Non-dispellable debuffs: `dispelName = nil`; dispellable: `dispelName = SECRET` -- Use `issecretvalue(aura.dispelName)` to detect dispellable vs non-dispellable -- `CooldownFrame:SetCooldownFromDurationObject(durObj, clearIfZero)` for secret-safe cooldown display -- `AbbreviateNumbers(value)` is C-level and accepts secrets -- `SetFormattedText` with secrets produces invisible output — don't use for duration text - -## Packaging - -```bash -# External libs cached at /Users/josiahtoppin/Documents/Projects/Cell-external-libs/ -# Required: LibStub, CallbackHandler-1.0, AceComm-3.0, LibSerialize, LibCustomGlow-1.0, LibSharedMedia-3.0, LibDeflate -mkdir -p /private/tmp/Cell-release-build -git archive HEAD | tar -x -C /private/tmp/Cell-release-build/Cell -cp -R /Users/josiahtoppin/Documents/Projects/Cell-external-libs/* /private/tmp/Cell-release-build/Cell/Libs/ -cd /private/tmp/Cell-release-build && zip -r .zip Cell/ -``` - -- Libs go into `Libs/` subdirectory (NOT addon root) -- When merging to master, use `git merge --ff-only` to avoid duplicate merge commits - -## Branches - -- `master` — main branch -- `jdtoppin-patch-1` — WoW 12.0 secret value fixes - -## Midnight Expansion Data - -- Released: March 2, 2026 -- Raids: The Voidspire (1307, 6 bosses), March on Quel'Danas (1308, 2 bosses) -- Dungeon debuffs: `RaidDebuffs/RaidDebuffs_Midnight.lua` + entry in `LoadRaidDebuffs.xml` -- Detailed encounter/spell data in memory file `midnight-expansion-data.md` diff --git a/Defaults/Layout_Defaults.lua b/Defaults/Layout_Defaults.lua index df0f0825..e3e8e484 100644 --- a/Defaults/Layout_Defaults.lua +++ b/Defaults/Layout_Defaults.lua @@ -471,7 +471,7 @@ Cell.defaults.layout = { ["position"] = {"TOP", "button", "TOP", 0, 3}, ["frameLevel"] = 25, ["size"] = {18, 18}, - ["privateAuraOptions"] = {true, false}, + ["privateAuraOptions"] = {true, false, 1}, }, -- 25 { ["name"] = "Targeted Spells", @@ -550,4 +550,4 @@ Cell.defaults.layoutAutoSwitch = { ["arena"] = "default", ["battleground15"] = "default", ["battleground40"] = "default", -} \ No newline at end of file +} diff --git a/Indicators/Built-in.lua b/Indicators/Built-in.lua index 2861733f..7138d8df 100644 --- a/Indicators/Built-in.lua +++ b/Indicators/Built-in.lua @@ -988,6 +988,46 @@ end ------------------------------------------------- -- private auras ------------------------------------------------- +local PRIVATE_AURAS_MAX = 5 + +local function PrivateAuras_GetMaxAuras(self, options) + local maxAuras = options and options[3] or self.maxAuras or 1 + maxAuras = tonumber(maxAuras) or 1 + maxAuras = math.floor(maxAuras + 0.5) + + if maxAuras < 1 then + maxAuras = 1 + elseif maxAuras > PRIVATE_AURAS_MAX then + maxAuras = PRIVATE_AURAS_MAX + end + + return maxAuras +end + +local function PrivateAuras_RemoveAllAnchors(self) + if not (C_UnitAuras and C_UnitAuras.RemovePrivateAuraAnchor) then return end + + for i = 1, #self do + local holder = self[i] + if holder.auraAnchorID then + C_UnitAuras.RemovePrivateAuraAnchor(holder.auraAnchorID) + holder.auraAnchorID = nil + end + end +end + +local function PrivateAuras_UpdateHolderVisibility(self, maxAuras) + for i = 1, #self do + if i <= maxAuras then + self[i]:Show() + else + self[i]:Hide() + end + end + + self:UpdateSize(maxAuras) +end + local function PrivateAuras_UpdatePrivateAuraAnchor(self, unit) -- 12.0.1+: AddPrivateAuraAnchor/RemovePrivateAuraAnchor cannot be called in combat. -- Defer until combat ends if needed. @@ -1007,46 +1047,47 @@ local function PrivateAuras_UpdatePrivateAuraAnchor(self, unit) return end - -- remove old - if self.auraAnchorID then - C_UnitAuras.RemovePrivateAuraAnchor(self.auraAnchorID) - self.unit = nil - self.auraAnchorID = nil - end + local maxAuras = PrivateAuras_GetMaxAuras(self) + PrivateAuras_RemoveAllAnchors(self) + self.unit = unit + + PrivateAuras_UpdateHolderVisibility(self, maxAuras) -- add new - if unit then + if unit and C_UnitAuras and C_UnitAuras.AddPrivateAuraAnchor then local _showCountdownFrame, _showCountdownNumbers = true, false if type(self.showCountdownFrame) == "boolean" then _showCountdownFrame = self.showCountdownFrame end if type(self.showCountdownNumbers) == "boolean" then _showCountdownNumbers = self.showCountdownNumbers end - self.unit = unit - self.auraAnchorID = C_UnitAuras.AddPrivateAuraAnchor({ - unitToken = unit, - auraIndex = 1, - parent = self, - showCountdownFrame = _showCountdownFrame, - showCountdownNumbers = _showCountdownNumbers, - iconInfo = { - iconWidth = self:GetWidth(), - iconHeight = self:GetHeight(), - borderScale = self:GetWidth() / 16, - iconAnchor = { - point = "CENTER", - relativeTo = self, - relativePoint = "CENTER", - offsetX = 0, - offsetY = 0, + for i = 1, maxAuras do + local holder = self[i] + holder.auraAnchorID = C_UnitAuras.AddPrivateAuraAnchor({ + unitToken = unit, + auraIndex = i, + parent = holder, + showCountdownFrame = _showCountdownFrame, + showCountdownNumbers = _showCountdownNumbers, + iconInfo = { + iconWidth = holder:GetWidth(), + iconHeight = holder:GetHeight(), + borderScale = holder:GetWidth() / 16, + iconAnchor = { + point = "CENTER", + relativeTo = holder, + relativePoint = "CENTER", + offsetX = 0, + offsetY = 0, + }, }, - }, - -- durationAnchor = { - -- point = "BOTTOMRIGHT", - -- relativeTo = self, - -- relativePoint = "BOTTOMRIGHT", - -- offsetX = 0, - -- offsetY = 0, - -- }, - }) + -- durationAnchor = { + -- point = "BOTTOMRIGHT", + -- relativeTo = holder, + -- relativePoint = "BOTTOMRIGHT", + -- offsetX = 0, + -- offsetY = 0, + -- }, + }) + end end end @@ -1057,15 +1098,41 @@ function I.CreatePrivateAuras(parent) privateAuras.UpdatePrivateAuraAnchor = PrivateAuras_UpdatePrivateAuraAnchor privateAuras._SetSize = privateAuras.SetSize + privateAuras.UpdateSize = I.Cooldowns_UpdateSize_WithSpacing + privateAuras.SetOrientation = I.Cooldowns_SetOrientation_WithSpacing + privateAuras.maxAuras = 1 + + for i = 1, PRIVATE_AURAS_MAX do + local holder = CreateFrame("Frame", nil, privateAuras) + tinsert(privateAuras, holder) + end + + privateAuras:SetOrientation("left-to-right") function privateAuras:SetSize(width, height) - privateAuras:_SetSize(width, height) + privateAuras.width = width + privateAuras.height = height + for i = 1, #privateAuras do + privateAuras[i]:SetSize(width, height) + end + PrivateAuras_UpdateHolderVisibility(privateAuras, PrivateAuras_GetMaxAuras(privateAuras)) privateAuras:UpdatePrivateAuraAnchor(privateAuras.unit) end function privateAuras:UpdateOptions(t) self.showCountdownFrame = t[1] self.showCountdownNumbers = t[2] + self.maxAuras = PrivateAuras_GetMaxAuras(self, t) + + for i = 1, #self do + local holder = self[i] + if holder.cooldown then + holder.cooldown:SetDrawSwipe(self.showCountdownFrame) + holder.cooldown:SetHideCountdownNumbers(not (self.showCountdownFrame and self.showCountdownNumbers)) + end + end + + PrivateAuras_UpdateHolderVisibility(self, self.maxAuras) privateAuras:UpdatePrivateAuraAnchor(privateAuras.unit) end end diff --git a/Modules/Indicators/Indicators.lua b/Modules/Indicators/Indicators.lua index 115e5078..1d4b2e34 100644 --- a/Modules/Indicators/Indicators.lua +++ b/Modules/Indicators/Indicators.lua @@ -497,39 +497,43 @@ local function InitIndicator(indicatorName) elseif indicatorName == "privateAuras" then indicator.isPrivateAuras = true - indicator.mask = indicator:CreateMaskTexture() - indicator.mask:SetTexture("interface/framegeneral/uiframeiconmask", "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE") - indicator.mask:SetAllPoints(indicator) - - indicator.icon = indicator:CreateTexture(nil, "ARTWORK") - indicator.icon:SetAllPoints(indicator) - indicator.icon:SetTexture(237555) - indicator.icon:AddMaskTexture(indicator.mask) - - indicator.border = indicator:CreateTexture(nil, "BORDER") - indicator.border:SetPoint("TOPLEFT", indicator.icon, -1, 0) - indicator.border:SetPoint("BOTTOMRIGHT", indicator.icon, 1, 0) - indicator.border:SetTexture([[Interface\Buttons\UI-Debuff-Overlays]]) - indicator.border:SetTexCoord(0.296875, 0.5703125, 0, 0.515625) - indicator.border:SetVertexColor(0.8, 0, 0) - - indicator.cooldown = CreateFrame("Cooldown", nil, indicator, "CooldownFrameTemplate") - indicator.cooldown:SetAllPoints(indicator) - indicator.cooldown:SetReverse(true) - indicator.cooldown:SetDrawEdge(false) - indicator.cooldown:SetDrawBling(false) - - local timer - indicator:HookScript("OnShow", function() - if timer then timer:Cancel() end - indicator.cooldown:SetCooldown(GetTime(), 15) - timer = C_Timer.NewTicker(15, function() - indicator.cooldown:SetCooldown(GetTime(), 15) + for i = 1, #indicator do + local holder = indicator[i] + + holder.mask = holder:CreateMaskTexture() + holder.mask:SetTexture("interface/framegeneral/uiframeiconmask", "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE") + holder.mask:SetAllPoints(holder) + + holder.icon = holder:CreateTexture(nil, "ARTWORK") + holder.icon:SetAllPoints(holder) + holder.icon:SetTexture(237555) + holder.icon:AddMaskTexture(holder.mask) + + holder.border = holder:CreateTexture(nil, "BORDER") + holder.border:SetPoint("TOPLEFT", holder.icon, -1, 0) + holder.border:SetPoint("BOTTOMRIGHT", holder.icon, 1, 0) + holder.border:SetTexture([[Interface\Buttons\UI-Debuff-Overlays]]) + holder.border:SetTexCoord(0.296875, 0.5703125, 0, 0.515625) + holder.border:SetVertexColor(0.8, 0, 0) + + holder.cooldown = CreateFrame("Cooldown", nil, holder, "CooldownFrameTemplate") + holder.cooldown:SetAllPoints(holder) + holder.cooldown:SetReverse(true) + holder.cooldown:SetDrawEdge(false) + holder.cooldown:SetDrawBling(false) + + local timer + holder:HookScript("OnShow", function() + if timer then timer:Cancel() end + holder.cooldown:SetCooldown(GetTime(), 15) + timer = C_Timer.NewTicker(15, function() + holder.cooldown:SetCooldown(GetTime(), 15) + end) end) - end) - indicator:HookScript("OnHide", function() - if timer then timer:Cancel() end - end) + holder:HookScript("OnHide", function() + if timer then timer:Cancel() end + end) + end elseif indicatorName == "targetedSpells" then indicator.isTargetedSpells = true @@ -835,8 +839,7 @@ local function UpdateIndicators(layout, indicatorName, setting, value, value2) end -- privateAuraOptions if t["privateAuraOptions"] then - indicator.cooldown:SetDrawSwipe(t["privateAuraOptions"][1]) - indicator.cooldown:SetHideCountdownNumbers(not (t["privateAuraOptions"][1] and t["privateAuraOptions"][2])) + indicator:UpdateOptions(t["privateAuraOptions"]) end -- update glow if t["glowOptions"] then @@ -1002,8 +1005,7 @@ local function UpdateIndicators(layout, indicatorName, setting, value, value2) indicator:Show() end elseif setting == "privateAuraOptions" then - indicator.cooldown:SetDrawSwipe(value[1]) - indicator.cooldown:SetHideCountdownNumbers(not (value[1] and value[2])) + indicator:UpdateOptions(value) elseif setting == "speed" then indicator:SetSpeed(value) elseif setting == "shape" then diff --git a/RaidFrames/UnitButton.lua b/RaidFrames/UnitButton.lua index a8e82a7c..0eee9d1a 100644 --- a/RaidFrames/UnitButton.lua +++ b/RaidFrames/UnitButton.lua @@ -1354,6 +1354,21 @@ local function ResetDebuffVars(self) self.states.BGOrb = nil -- TODO: move to _debuffs end +local function CanPlayerDispelAura(unit, auraInfo, debuffType) + if Cell.isMidnight and _IsAuraFilteredOut and unit and auraInfo and auraInfo.auraInstanceID then + local isFiltered = _IsAuraFilteredOut(unit, auraInfo.auraInstanceID, "HARMFUL|RAID_PLAYER_DISPELLABLE") + if F.IsValueNonSecret(isFiltered) then + return not isFiltered + end + end + + if auraInfo and auraInfo._hasSecrets then + return not (auraInfo.dispelName == nil) + end + + return I.CanDispel(debuffType) +end + local function HandleDebuff(self, auraInfo) local auraInstanceID = auraInfo.auraInstanceID @@ -1371,12 +1386,7 @@ local function HandleDebuff(self, auraInfo) local debuffType if auraInfo._hasSecrets then -- Secret aura: can't read dispelName as a Lua string for type matching. - -- Track auraInstanceID for curve-based dispel display in UpdateDebuffs. debuffType = "" - if isDispellable and unit then - self._dispelAuraID = auraInstanceID - self._dispelUnit = unit - end else debuffType = auraInfo.dispelName or "" end @@ -1421,16 +1431,21 @@ local function HandleDebuff(self, auraInfo) isDispelBlacklisted = Cell.vars.dispelBlacklist[spellId] or false end + local canPlayerDispelAura + local function GetCanPlayerDispelAura() + if canPlayerDispelAura == nil then + canPlayerDispelAura = CanPlayerDispelAura(unit, auraInfo, debuffType) and true or false + end + return canPlayerDispelAura + end + if enabledIndicators["debuffs"] and not isBlacklisted then -- all debuffs / only dispellableByMe - local canDispel = not indicatorBooleans["debuffs"] or I.CanDispel(debuffType) - -- 12.0+: when dispelName is secret, use server-side filter - if not canDispel and isDispellable and auraInfo._hasSecrets - and _IsAuraFilteredOut and unit then - canDispel = not _IsAuraFilteredOut(unit, - auraInstanceID, "HARMFUL|RAID_PLAYER_DISPELLABLE") + local canShowDebuff = not indicatorBooleans["debuffs"] + if not canShowDebuff then + canShowDebuff = GetCanPlayerDispelAura() end - if canDispel then + if canShowDebuff then if isBig then self._debuffs_big[auraInstanceID] = true else @@ -1473,9 +1488,14 @@ local function HandleDebuff(self, auraInfo) end end - if enabledIndicators["dispels"] and debuffType and debuffType ~= "" then + if enabledIndicators["dispels"] then -- all dispels / only dispellableByMe - if not indicatorBooleans["dispels"]["dispellableByMe"] or I.CanDispel(debuffType) then + local canShowDispel = not indicatorBooleans["dispels"]["dispellableByMe"] + if not canShowDispel then + canShowDispel = GetCanPlayerDispelAura() + end + + if canShowDispel and debuffType and debuffType ~= "" then if indicatorBooleans["dispels"][debuffType] then if isDispelBlacklisted then self._debuffs_dispel[debuffType] = false @@ -1483,6 +1503,11 @@ local function HandleDebuff(self, auraInfo) self._debuffs_dispel[debuffType] = true end end + elseif canShowDispel and auraInfo._hasSecrets and isDispellable and unit then + -- Secret dispels have no Lua-readable type, so keep one aura around for + -- the curve-based fallback in UpdateDebuffs. + self._dispelAuraID = auraInstanceID + self._dispelUnit = unit end end @@ -5003,9 +5028,7 @@ function B.UpdateAnimation(button) if Cell.isMidnight then -- Midnight: smooth animation handled via StatusBarInterpolation enum in SetValue(). -- Never use SetSmoothedValue mixin (does Lua Clamp arithmetic, crashes on secrets). - button.widgets.healthBar:ResetSmoothedValue() button.widgets.healthBar.SetBarValue = button.widgets.healthBar.SetValue - button.widgets.powerBar:ResetSmoothedValue() button.widgets.powerBar.SetBarValue = button.widgets.powerBar.SetValue elseif barAnimationType == "Smooth" then button.widgets.healthBar.SetBarValue = button.widgets.healthBar.SetSmoothedValue diff --git a/Widgets/Widgets_IndicatorSettings.lua b/Widgets/Widgets_IndicatorSettings.lua index 334b492e..6f1d921d 100644 --- a/Widgets/Widgets_IndicatorSettings.lua +++ b/Widgets/Widgets_IndicatorSettings.lua @@ -5994,30 +5994,43 @@ local function CreateSetting_PrivateAuraOptions(parent) local widget if not settingWidgets["privateAuraOptions"] then - widget = Cell.CreateFrame("CellIndicatorSettings_PrivateAuraOptions", parent, 240, 55) + widget = Cell.CreateFrame("CellIndicatorSettings_PrivateAuraOptions", parent, 240, 108) settingWidgets["privateAuraOptions"] = widget + widget.options = {true, false, 1} widget.cb1 = Cell.CreateCheckButton(widget, L["Show countdown swipe"]) widget.cb1:SetPoint("TOPLEFT", 5, -8) widget.cb2 = Cell.CreateCheckButton(widget, L["Show countdown number"]) widget.cb2:SetPoint("TOPLEFT", widget.cb1, "BOTTOMLEFT", 0, -7) + widget.maxAuras = Cell.CreateSlider(L["Max Displayed"], widget, 1, 5, 110, 1) + widget.maxAuras:SetPoint("TOPLEFT", widget.cb2, "BOTTOMLEFT", 0, -18) -- callback function widget:SetFunc(func) widget.cb1.onClick = function(checked) widget.cb2:SetEnabled(checked) - func({checked, widget.cb2:GetChecked()}) + widget.options[1] = checked + widget.options[2] = widget.cb2:GetChecked() + func(widget.options) end widget.cb2.onClick = function(checked) - func({widget.cb1:GetChecked(), checked}) + widget.options[1] = widget.cb1:GetChecked() + widget.options[2] = checked + func(widget.options) + end + widget.maxAuras.afterValueChangedFn = function(value) + widget.options[3] = value + func(widget.options) end end -- show db value function widget:SetDBValue(t) + widget.options = {t[1], t[2], t[3] or 1} widget.cb1:SetChecked(t[1]) widget.cb2:SetChecked(t[2]) widget.cb2:SetEnabled(t[1]) + widget.maxAuras:SetValue(t[3] or 1) end else widget = settingWidgets["privateAuraOptions"] @@ -6941,4 +6954,4 @@ function Cell.CreateIndicatorSettings(parent, settingsTable) end return widgetsTable -end \ No newline at end of file +end From aefcaec61762afa36695eb474e57c4aa38d327d3 Mon Sep 17 00:00:00 2001 From: Skye Date: Thu, 2 Apr 2026 13:58:54 -0300 Subject: [PATCH 18/61] Prepare r275.7-skyking-dev release --- .release/validate_package.sh | 111 ++++++++++++++++ Comm/Comm.lua | 210 +++++++++++++++++++----------- Comm/Nicknames.lua | 65 ++++----- Core.lua | 64 ++++++++- Core_Cata.lua | 64 ++++++++- Core_Mists.lua | 64 ++++++++- Core_Vanilla.lua | 62 +++++++++ Core_Wrath.lua | 64 ++++++++- README.md | 16 +++ README_zhCN.md | 16 +++ Utilities/BuffTracker.lua | 22 +--- Utilities/BuffTracker_Classic.lua | 19 +-- Utilities/DeathReport.lua | 6 +- Utilities/ReadyAndPull.lua | 12 +- Utilities/Request_Dispel.lua | 6 +- Utilities/Request_Show.lua | 8 +- Utilities/Request_Spell.lua | 6 +- Utils.lua | 99 ++++++++++++++ 18 files changed, 746 insertions(+), 168 deletions(-) create mode 100755 .release/validate_package.sh diff --git a/.release/validate_package.sh b/.release/validate_package.sh new file mode 100755 index 00000000..762569a2 --- /dev/null +++ b/.release/validate_package.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: .release/validate_package.sh + +Validates whether an extracted Cell package looks like a real installable addon +package or a source archive from GitHub. +EOF +} + +if [[ $# -ne 1 ]]; then + usage >&2 + exit 2 +fi + +input_path="$1" + +if [[ ! -d "$input_path" ]]; then + echo "ERROR: path does not exist or is not a directory: $input_path" >&2 + exit 2 +fi + +addon_root="$input_path" + +# GitHub source archives often unpack into a parent folder that contains the +# actual addon root as a nested "Cell" directory. +if [[ -d "$input_path/Cell" && -f "$input_path/Cell/Cell.toc" ]]; then + addon_root="$input_path/Cell" +fi + +if [[ ! -f "$addon_root/Cell.toc" ]]; then + echo "ERROR: could not find Cell.toc under: $addon_root" >&2 + exit 2 +fi + +required_lib_paths=( + "Libs/LibStub/LibStub.lua" + "Libs/CallbackHandler-1.0/CallbackHandler-1.0.xml" + "Libs/AceComm-3.0/AceComm-3.0.xml" + "Libs/LibSerialize/lib.xml" + "Libs/LibCustomGlow-1.0/LibCustomGlow-1.0.xml" + "Libs/LibSharedMedia-3.0/lib.xml" + "Libs/LibDeflate/lib.xml" +) + +source_markers=( + ".gitignore" + ".gitattributes" + ".pkgmeta" + ".github" + ".release" +) + +missing=() +markers_found=() + +for path in "${required_lib_paths[@]}"; do + if [[ ! -e "$addon_root/$path" ]]; then + missing+=("$path") + fi +done + +for path in "${source_markers[@]}"; do + if [[ -e "$addon_root/$path" ]]; then + markers_found+=("$path") + fi +done + +echo "Inspecting package root: $addon_root" +echo + +if [[ ${#missing[@]} -eq 0 ]]; then + echo "Required embedded libraries: OK" +else + echo "Required embedded libraries: MISSING" + for path in "${missing[@]}"; do + echo " - $path" + done +fi + +echo + +if [[ ${#markers_found[@]} -eq 0 ]]; then + echo "Source archive markers: none detected" +else + echo "Source archive markers detected:" + for path in "${markers_found[@]}"; do + echo " - $path" + done +fi + +echo + +if [[ ${#missing[@]} -gt 0 ]]; then + echo "RESULT: INVALID INSTALL PACKAGE" + echo "This looks like a source archive or an incomplete release." + echo "Install the packaged release asset instead of GitHub 'Source code'." + exit 1 +fi + +if [[ ${#markers_found[@]} -gt 0 ]]; then + echo "RESULT: SUSPICIOUS PACKAGE" + echo "Libraries are present, but repository-only files were included." + echo "Double-check which asset was uploaded or downloaded." + exit 1 +fi + +echo "RESULT: PACKAGE LOOKS INSTALLABLE" diff --git a/Comm/Comm.lua b/Comm/Comm.lua index 17a495c0..3bc151da 100644 --- a/Comm/Comm.lua +++ b/Comm/Comm.lua @@ -29,23 +29,126 @@ local function Deserialize(encoded) end ----------------------------------------- --- Comm restriction (Midnight 12.0.0+) --- Addon communications are blocked during active encounters, M+ keys, and PvP matches. +-- Comm queue and wrappers ----------------------------------------- -local function IsCommRestricted() - if not Cell.isMidnight then return false end - -- Check encounter - if IsEncounterInProgress and IsEncounterInProgress() then return true end - -- Check M+ - if C_MythicPlus and C_MythicPlus.IsRunActive and C_MythicPlus.IsRunActive() then return true end - -- Check PvP - if C_PvP and C_PvP.IsActiveBattlefield and C_PvP.IsActiveBattlefield() then return true end - return false +local commQueue = {} +local commQueueByKey = {} +local commFlushTicker + +local function StopCommFlushTicker() + if commFlushTicker then + commFlushTicker:Cancel() + commFlushTicker = nil + end +end + +local function StartCommFlushTicker() + if not Cell.isMidnight or commFlushTicker then return end + commFlushTicker = C_Timer.NewTicker(2, function() + if #commQueue == 0 then + StopCommFlushTicker() + return + end + if not F.IsCommRestricted() then + F.FlushCommQueue() + end + end) +end + +local function BuildCommQueueKey(prefix, message, distribution, target, priority) + return table.concat({ + tostring(prefix or ""), + tostring(distribution or ""), + tostring(target or ""), + tostring(priority or ""), + tostring(message or ""), + }, "\031") +end + +local function CanSendCommDistribution(distribution, target) + if distribution == "INSTANCE_CHAT" then + return IsInGroup(LE_PARTY_CATEGORY_INSTANCE) + elseif distribution == "RAID" then + return IsInRaid() + elseif distribution == "PARTY" then + return IsInGroup() + elseif distribution == "GUILD" or distribution == "OFFICER" then + return IsInGuild() + elseif distribution == "WHISPER" then + return target and target ~= "" + end + return true +end + +local function QueueCommMessage(prefix, message, distribution, target, priority, callbackFn, queueKey) + local key = queueKey or BuildCommQueueKey(prefix, message, distribution, target, priority) + local queued = commQueueByKey[key] + if queued then + queued.callbackFn = callbackFn + else + queued = { + key = key, + prefix = prefix, + message = message, + distribution = distribution, + target = target, + priority = priority, + callbackFn = callbackFn, + } + tinsert(commQueue, queued) + commQueueByKey[key] = queued + end + StartCommFlushTicker() + F.Debug("Cell: Comm queued - restricted context ("..tostring(prefix)..")") +end + +function F.FlushCommQueue() + if F.IsCommRestricted() then + StartCommFlushTicker() + return false + end + + if #commQueue == 0 then + StopCommFlushTicker() + return true + end + + local pending = commQueue + commQueue = {} + commQueueByKey = {} + StopCommFlushTicker() + + for _, queued in ipairs(pending) do + if CanSendCommDistribution(queued.distribution, queued.target) then + Comm:SendCommMessage(queued.prefix, queued.message, queued.distribution, queued.target, queued.priority, queued.callbackFn) + else + F.Debug("Cell: Comm dropped - invalid distribution after queue ("..tostring(queued.prefix)..")") + end + end + + return true end --- Export for use in other Comm files (e.g. Nicknames.lua) -function F.IsCommRestricted() - return IsCommRestricted() +function F.TrySendCommMessage(prefix, message, distribution, target, priority, callbackFn, options) + if not prefix or not message or not distribution then return false end + + options = options or {} + if Cell.isMidnight and F.IsCommRestricted() then + if options.queue == false then + F.Debug("Cell: Comm suppressed - restricted context ("..tostring(prefix)..")") + return false + end + QueueCommMessage(prefix, message, distribution, target, priority, callbackFn, options.queueKey) + return false, "queued" + end + + if not CanSendCommDistribution(distribution, target) then + F.Debug("Cell: Comm suppressed - invalid distribution ("..tostring(prefix)..")") + return false + end + + Comm:SendCommMessage(prefix, message, distribution, target, priority, callbackFn) + return true end ----------------------------------------- @@ -57,20 +160,6 @@ function F.Notify(type, ...) end end ------------------------------------------ --- shared ------------------------------------------ -local sendChannel -local function UpdateSendChannel() - if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) then - sendChannel = "INSTANCE_CHAT" - elseif IsInRaid() then - sendChannel = "RAID" - else - sendChannel = "PARTY" - end -end - ----------------------------------------- -- Check Version ----------------------------------------- @@ -83,25 +172,17 @@ eventFrame:RegisterEvent("GROUP_ROSTER_UPDATE") function eventFrame:GROUP_ROSTER_UPDATE() if IsInGroup() then eventFrame:UnregisterEvent("GROUP_ROSTER_UPDATE") - UpdateSendChannel() - -- Addon comms blocked during encounters/M+/PvP on Midnight 12.0.0+ - if IsCommRestricted() then - F.Debug("Cell: Comm suppressed - restricted context (CELL_VERSION group)") - return + local sendChannel = F.GetGroupCommChannel() + if sendChannel then + F.TrySendCommMessage("CELL_VERSION", Cell.version, sendChannel, nil, "NORMAL") end - Comm:SendCommMessage("CELL_VERSION", Cell.version, sendChannel, nil, "NORMAL") end end eventFrame:RegisterEvent("PLAYER_LOGIN") function eventFrame:PLAYER_LOGIN() if IsInGuild() then - -- Addon comms blocked during encounters/M+/PvP on Midnight 12.0.0+ - if IsCommRestricted() then - F.Debug("Cell: Comm suppressed - restricted context (CELL_VERSION guild)") - return - end - Comm:SendCommMessage("CELL_VERSION", Cell.version, "GUILD", nil, "NORMAL") + F.TrySendCommMessage("CELL_VERSION", Cell.version, "GUILD", nil, "NORMAL") end end @@ -136,26 +217,20 @@ function F.NotifyMarkLock(mark, name, class) name = F.GetClassColorStr(class)..name.."|r" F.Print(L["%s lock %s on %s."]:format(L["You"], F.GetMarkEscapeSequence(mark), name)) - UpdateSendChannel() - -- Addon comms blocked during encounters/M+/PvP on Midnight 12.0.0+ - if IsCommRestricted() then - F.Debug("Cell: Comm suppressed - restricted context (CELL_MARKS lock)") - return + local sendChannel = F.GetGroupCommChannel() + if sendChannel then + F.TrySendCommMessage("CELL_MARKS", Serialize({true, mark, name}), sendChannel, nil, "ALERT") end - Comm:SendCommMessage("CELL_MARKS", Serialize({true, mark, name}), sendChannel, nil, "ALERT") end function F.NotifyMarkUnlock(mark, name, class) name = F.GetClassColorStr(class)..name.."|r" F.Print(L["%s unlock %s from %s."]:format(L["You"], F.GetMarkEscapeSequence(mark), name)) - UpdateSendChannel() - -- Addon comms blocked during encounters/M+/PvP on Midnight 12.0.0+ - if IsCommRestricted() then - F.Debug("Cell: Comm suppressed - restricted context (CELL_MARKS unlock)") - return + local sendChannel = F.GetGroupCommChannel() + if sendChannel then + F.TrySendCommMessage("CELL_MARKS", Serialize({false, mark, name}), sendChannel, nil, "ALERT") end - Comm:SendCommMessage("CELL_MARKS", Serialize({false, mark, name}), sendChannel, nil, "ALERT") end ----------------------------------------- @@ -206,13 +281,10 @@ function F.CheckPriority() UpdatePriority() -- NOTE: needs time to calc myPriority C_Timer.After(1, function() - UpdateSendChannel() - -- Addon comms blocked during encounters/M+/PvP on Midnight 12.0.0+ - if IsCommRestricted() then - F.Debug("Cell: Comm suppressed - restricted context (CELL_CPRIO chk)") - return + local sendChannel = F.GetGroupCommChannel() + if sendChannel then + F.TrySendCommMessage("CELL_CPRIO", "chk", sendChannel, nil, "ALERT") end - Comm:SendCommMessage("CELL_CPRIO", "chk", sendChannel, nil, "ALERT") end) -- if t_check then t_check:Cancel() end -- t_check = C_Timer.NewTimer(2, function() @@ -228,13 +300,10 @@ Comm:RegisterComm("CELL_CPRIO", function(prefix, message, channel, sender) -- NOTE: wait for check requests if t_send then t_send:Cancel() end t_send = C_Timer.NewTimer(2, function() - UpdateSendChannel() - -- Addon comms blocked during encounters/M+/PvP on Midnight 12.0.0+ - if IsCommRestricted() then - F.Debug("Cell: Comm suppressed - restricted context (CELL_PRIO)") - return + local sendChannel = F.GetGroupCommChannel() + if sendChannel then + F.TrySendCommMessage("CELL_PRIO", tostring(myPriority), sendChannel, nil, "ALERT") end - Comm:SendCommMessage("CELL_PRIO", tostring(myPriority), sendChannel, nil, "ALERT") end) end) @@ -258,19 +327,14 @@ end) -- cross realm send ----------------------------------------- local function CrossRealmSendCommMessage(prefix, message, playerName, priority, callbackFn) - -- Addon comms blocked during encounters/M+/PvP on Midnight 12.0.0+ - if IsCommRestricted() then - F.Debug("Cell: Comm suppressed - restricted context (CrossRealm:", prefix, ")") - return - end -- NOTE: unit needs to be in your group, or it will always return true if UnitIsSameServer(playerName) then - Comm:SendCommMessage(prefix, message, "WHISPER", playerName, priority, callbackFn) + F.TrySendCommMessage(prefix, message, "WHISPER", playerName, priority, callbackFn) else if UnitInParty(playerName) then - Comm:SendCommMessage(prefix, playerName..":"..message, "PARTY", nil, priority, callbackFn) + F.TrySendCommMessage(prefix, playerName..":"..message, "PARTY", nil, priority, callbackFn) elseif UnitInRaid(playerName) then - Comm:SendCommMessage(prefix, playerName..":"..message, "RAID", nil, priority, callbackFn) + F.TrySendCommMessage(prefix, playerName..":"..message, "RAID", nil, priority, callbackFn) end end end @@ -462,4 +526,4 @@ hooksecurefunc("SetItemRef", function(link, text) local layoutName, playerName = text:match("|Hgarrmission:cell%-layout|h|cFFFF0066%[.+: (.+) %- ([^%s]+%-[^%s]+)%]|h|r") ShowReceivingFrame("Layout", playerName, layoutName) end -end) \ No newline at end of file +end) diff --git a/Comm/Nicknames.lua b/Comm/Nicknames.lua index b377dd67..fd651619 100644 --- a/Comm/Nicknames.lua +++ b/Comm/Nicknames.lua @@ -5,20 +5,6 @@ local F = Cell.funcs local LBW = LibStub:GetLibrary("LibBadWords") local Comm = LibStub:GetLibrary("AceComm-3.0") ------------------------------------------ --- shared ------------------------------------------ -local sendChannel -local function UpdateSendChannel() - if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) then - sendChannel = "INSTANCE_CHAT" - elseif IsInRaid() then - sendChannel = "RAID" - else - sendChannel = "PARTY" - end -end - ----------------------------------------- -- nickname ----------------------------------------- @@ -72,13 +58,10 @@ local function CheckNicknames() if CellDB["nicknames"]["sync"] then if nic_check then nic_check:Cancel() end nic_check = C_Timer.NewTimer(random(3), function() - UpdateSendChannel() - -- Addon comms blocked during encounters/M+/PvP on Midnight 12.0.0+ - if Cell.isMidnight and F.IsCommRestricted() then - F.Debug("Cell: Comm suppressed - restricted context (CELL_CNIC)") - return + local sendChannel = F.GetGroupCommChannel() + if sendChannel then + F.TrySendCommMessage("CELL_CNIC", "chk", sendChannel, nil, "ALERT") end - Comm:SendCommMessage("CELL_CNIC", "chk", sendChannel, nil, "ALERT") end) end end @@ -164,12 +147,12 @@ local function UpdateNicknames(which, value1, value2) if nic_check then nic_check:Cancel() end -- disabled, notify others - UpdateSendChannel() - -- Addon comms blocked during encounters/M+/PvP on Midnight 12.0.0+ - if Cell.isMidnight and F.IsCommRestricted() then - F.Debug("Cell: Comm suppressed - restricted context (CELL_NIC sync-off)") - else - Comm:SendCommMessage("CELL_NIC", "CELL_NONE", sendChannel) + local sendChannel = F.GetGroupCommChannel() + if sendChannel then + F.TrySendCommMessage("CELL_NIC", "CELL_NONE", sendChannel, nil, nil, nil, { + queue = false, + queueKey = "CELL_NIC:"..sendChannel, + }) end -- update all @@ -187,12 +170,11 @@ local function UpdateNicknames(which, value1, value2) -- notify others if IsInGroup() and CellDB["nicknames"]["sync"] then - UpdateSendChannel() - -- Addon comms blocked during encounters/M+/PvP on Midnight 12.0.0+ - if Cell.isMidnight and F.IsCommRestricted() then - F.Debug("Cell: Comm suppressed - restricted context (CELL_NIC mine)") - else - Comm:SendCommMessage("CELL_NIC", Cell.vars.playerNickname or "CELL_NONE", sendChannel) + local sendChannel = F.GetGroupCommChannel() + if sendChannel then + F.TrySendCommMessage("CELL_NIC", Cell.vars.playerNickname or "CELL_NONE", sendChannel, nil, nil, nil, { + queueKey = "CELL_NIC:"..sendChannel, + }) end end @@ -232,16 +214,17 @@ Comm:RegisterComm("CELL_CNIC", function(prefix, message, channel, sender) if nic_send then nic_send:Cancel() end nic_send = C_Timer.NewTimer(3, function() - UpdateSendChannel() - -- Addon comms blocked during encounters/M+/PvP on Midnight 12.0.0+ - if Cell.isMidnight and F.IsCommRestricted() then - F.Debug("Cell: Comm suppressed - restricted context (CELL_NIC nic_send)") - return - end + local sendChannel = F.GetGroupCommChannel() + if not sendChannel then return end if CellDB["nicknames"]["sync"] then - Comm:SendCommMessage("CELL_NIC", Cell.vars.playerNickname or "CELL_NONE", sendChannel) + F.TrySendCommMessage("CELL_NIC", Cell.vars.playerNickname or "CELL_NONE", sendChannel, nil, nil, nil, { + queueKey = "CELL_NIC:"..sendChannel, + }) else - Comm:SendCommMessage("CELL_NIC", "CELL_NONE", sendChannel) + F.TrySendCommMessage("CELL_NIC", "CELL_NONE", sendChannel, nil, nil, nil, { + queue = false, + queueKey = "CELL_NIC:"..sendChannel, + }) end end) end) @@ -294,4 +277,4 @@ f:SetScript("OnEvent", function() timer = C_Timer.NewTimer(3, UpdateAll) end) end -end) \ No newline at end of file +end) diff --git a/Core.lua b/Core.lua index 1a13d157..5dfa9e80 100644 --- a/Core.lua +++ b/Core.lua @@ -23,6 +23,68 @@ Cell.bFuncs = {} Cell.uFuncs = {} Cell.animations = {} +-- Provide safe accent-color fallbacks before Widgets.lua initializes its +-- richer helpers. This keeps later files from exploding if a mixed install +-- loads newer callers before the real widget helpers are available. +if not Cell.GetAccentColorRGB then + local fallbackAccentColor = {0.7, 0.7, 0.7} + local fallbackAccentColorString = "|cFFB2B2B2" + + local function ClampColorComponent(value) + value = tonumber(value) or 0 + if value < 0 then return 0 end + if value > 1 then return 1 end + return value + end + + local function UpdateFallbackAccentColorString() + fallbackAccentColorString = ("|cFF%02X%02X%02X"):format( + floor(ClampColorComponent(fallbackAccentColor[1]) * 255 + 0.5), + floor(ClampColorComponent(fallbackAccentColor[2]) * 255 + 0.5), + floor(ClampColorComponent(fallbackAccentColor[3]) * 255 + 0.5) + ) + end + + function Cell.OverrideAccentColor(cTable) + if type(cTable) ~= "table" then return end + + fallbackAccentColor[1] = ClampColorComponent(cTable[1] or fallbackAccentColor[1]) + fallbackAccentColor[2] = ClampColorComponent(cTable[2] or fallbackAccentColor[2]) + fallbackAccentColor[3] = ClampColorComponent(cTable[3] or fallbackAccentColor[3]) + UpdateFallbackAccentColorString() + end + + function Cell.GetAccentColorRGB() + return unpack(fallbackAccentColor) + end + + function Cell.GetAccentColorTable(alpha) + if alpha then + return {fallbackAccentColor[1], fallbackAccentColor[2], fallbackAccentColor[3], alpha} + end + + return fallbackAccentColor + end + + function Cell.GetAccentColorString() + return fallbackAccentColorString + end + + function Cell.ColorFontStringWithAccentColor(fs) + if fs and fs.SetTextColor then + fs:SetTextColor(unpack(fallbackAccentColor)) + end + end + + function Cell.WrapTextInAccentColor(text) + if WrapTextInColorCode then + return WrapTextInColorCode(text, fallbackAccentColorString) + end + + return fallbackAccentColorString .. text .. "|r" + end +end + ---@class CellFuncs local F = Cell.funcs local I = Cell.iFuncs @@ -1061,4 +1123,4 @@ end function Cell_OnAddonCompartmentClick() F.ShowOptionsFrame() -end \ No newline at end of file +end diff --git a/Core_Cata.lua b/Core_Cata.lua index 43b4fcda..5cbc1e23 100644 --- a/Core_Cata.lua +++ b/Core_Cata.lua @@ -23,6 +23,68 @@ Cell.bFuncs = {} Cell.uFuncs = {} Cell.animations = {} +-- Provide safe accent-color fallbacks before Widgets.lua initializes its +-- richer helpers. This keeps later files from exploding if a mixed install +-- loads newer callers before the real widget helpers are available. +if not Cell.GetAccentColorRGB then + local fallbackAccentColor = {0.7, 0.7, 0.7} + local fallbackAccentColorString = "|cFFB2B2B2" + + local function ClampColorComponent(value) + value = tonumber(value) or 0 + if value < 0 then return 0 end + if value > 1 then return 1 end + return value + end + + local function UpdateFallbackAccentColorString() + fallbackAccentColorString = ("|cFF%02X%02X%02X"):format( + floor(ClampColorComponent(fallbackAccentColor[1]) * 255 + 0.5), + floor(ClampColorComponent(fallbackAccentColor[2]) * 255 + 0.5), + floor(ClampColorComponent(fallbackAccentColor[3]) * 255 + 0.5) + ) + end + + function Cell.OverrideAccentColor(cTable) + if type(cTable) ~= "table" then return end + + fallbackAccentColor[1] = ClampColorComponent(cTable[1] or fallbackAccentColor[1]) + fallbackAccentColor[2] = ClampColorComponent(cTable[2] or fallbackAccentColor[2]) + fallbackAccentColor[3] = ClampColorComponent(cTable[3] or fallbackAccentColor[3]) + UpdateFallbackAccentColorString() + end + + function Cell.GetAccentColorRGB() + return unpack(fallbackAccentColor) + end + + function Cell.GetAccentColorTable(alpha) + if alpha then + return {fallbackAccentColor[1], fallbackAccentColor[2], fallbackAccentColor[3], alpha} + end + + return fallbackAccentColor + end + + function Cell.GetAccentColorString() + return fallbackAccentColorString + end + + function Cell.ColorFontStringWithAccentColor(fs) + if fs and fs.SetTextColor then + fs:SetTextColor(unpack(fallbackAccentColor)) + end + end + + function Cell.WrapTextInAccentColor(text) + if WrapTextInColorCode then + return WrapTextInColorCode(text, fallbackAccentColorString) + end + + return fallbackAccentColorString .. text .. "|r" + end +end + local F = Cell.funcs local I = Cell.iFuncs local P = Cell.pixelPerfectFuncs @@ -904,4 +966,4 @@ function SlashCmdList.CELL(msg, editbox) "|cFFFFB5C5/cell reset all|r: "..L["reset all Cell settings"].."." ) end -end \ No newline at end of file +end diff --git a/Core_Mists.lua b/Core_Mists.lua index 88c3c791..9475860d 100644 --- a/Core_Mists.lua +++ b/Core_Mists.lua @@ -23,6 +23,68 @@ Cell.bFuncs = {} Cell.uFuncs = {} Cell.animations = {} +-- Provide safe accent-color fallbacks before Widgets.lua initializes its +-- richer helpers. This keeps later files from exploding if a mixed install +-- loads newer callers before the real widget helpers are available. +if not Cell.GetAccentColorRGB then + local fallbackAccentColor = {0.7, 0.7, 0.7} + local fallbackAccentColorString = "|cFFB2B2B2" + + local function ClampColorComponent(value) + value = tonumber(value) or 0 + if value < 0 then return 0 end + if value > 1 then return 1 end + return value + end + + local function UpdateFallbackAccentColorString() + fallbackAccentColorString = ("|cFF%02X%02X%02X"):format( + floor(ClampColorComponent(fallbackAccentColor[1]) * 255 + 0.5), + floor(ClampColorComponent(fallbackAccentColor[2]) * 255 + 0.5), + floor(ClampColorComponent(fallbackAccentColor[3]) * 255 + 0.5) + ) + end + + function Cell.OverrideAccentColor(cTable) + if type(cTable) ~= "table" then return end + + fallbackAccentColor[1] = ClampColorComponent(cTable[1] or fallbackAccentColor[1]) + fallbackAccentColor[2] = ClampColorComponent(cTable[2] or fallbackAccentColor[2]) + fallbackAccentColor[3] = ClampColorComponent(cTable[3] or fallbackAccentColor[3]) + UpdateFallbackAccentColorString() + end + + function Cell.GetAccentColorRGB() + return unpack(fallbackAccentColor) + end + + function Cell.GetAccentColorTable(alpha) + if alpha then + return {fallbackAccentColor[1], fallbackAccentColor[2], fallbackAccentColor[3], alpha} + end + + return fallbackAccentColor + end + + function Cell.GetAccentColorString() + return fallbackAccentColorString + end + + function Cell.ColorFontStringWithAccentColor(fs) + if fs and fs.SetTextColor then + fs:SetTextColor(unpack(fallbackAccentColor)) + end + end + + function Cell.WrapTextInAccentColor(text) + if WrapTextInColorCode then + return WrapTextInColorCode(text, fallbackAccentColorString) + end + + return fallbackAccentColorString .. text .. "|r" + end +end + ---@class CellFuncs local F = Cell.funcs local I = Cell.iFuncs @@ -968,4 +1030,4 @@ end function Cell_OnAddonCompartmentClick() F.ShowOptionsFrame() -end \ No newline at end of file +end diff --git a/Core_Vanilla.lua b/Core_Vanilla.lua index 083ab7cb..213d803f 100644 --- a/Core_Vanilla.lua +++ b/Core_Vanilla.lua @@ -23,6 +23,68 @@ Cell.bFuncs = {} Cell.uFuncs = {} Cell.animations = {} +-- Provide safe accent-color fallbacks before Widgets.lua initializes its +-- richer helpers. This keeps later files from exploding if a mixed install +-- loads newer callers before the real widget helpers are available. +if not Cell.GetAccentColorRGB then + local fallbackAccentColor = {0.7, 0.7, 0.7} + local fallbackAccentColorString = "|cFFB2B2B2" + + local function ClampColorComponent(value) + value = tonumber(value) or 0 + if value < 0 then return 0 end + if value > 1 then return 1 end + return value + end + + local function UpdateFallbackAccentColorString() + fallbackAccentColorString = ("|cFF%02X%02X%02X"):format( + floor(ClampColorComponent(fallbackAccentColor[1]) * 255 + 0.5), + floor(ClampColorComponent(fallbackAccentColor[2]) * 255 + 0.5), + floor(ClampColorComponent(fallbackAccentColor[3]) * 255 + 0.5) + ) + end + + function Cell.OverrideAccentColor(cTable) + if type(cTable) ~= "table" then return end + + fallbackAccentColor[1] = ClampColorComponent(cTable[1] or fallbackAccentColor[1]) + fallbackAccentColor[2] = ClampColorComponent(cTable[2] or fallbackAccentColor[2]) + fallbackAccentColor[3] = ClampColorComponent(cTable[3] or fallbackAccentColor[3]) + UpdateFallbackAccentColorString() + end + + function Cell.GetAccentColorRGB() + return unpack(fallbackAccentColor) + end + + function Cell.GetAccentColorTable(alpha) + if alpha then + return {fallbackAccentColor[1], fallbackAccentColor[2], fallbackAccentColor[3], alpha} + end + + return fallbackAccentColor + end + + function Cell.GetAccentColorString() + return fallbackAccentColorString + end + + function Cell.ColorFontStringWithAccentColor(fs) + if fs and fs.SetTextColor then + fs:SetTextColor(unpack(fallbackAccentColor)) + end + end + + function Cell.WrapTextInAccentColor(text) + if WrapTextInColorCode then + return WrapTextInColorCode(text, fallbackAccentColorString) + end + + return fallbackAccentColorString .. text .. "|r" + end +end + local F = Cell.funcs local I = Cell.iFuncs local P = Cell.pixelPerfectFuncs diff --git a/Core_Wrath.lua b/Core_Wrath.lua index 0cff2478..8a29ab30 100644 --- a/Core_Wrath.lua +++ b/Core_Wrath.lua @@ -23,6 +23,68 @@ Cell.bFuncs = {} Cell.uFuncs = {} Cell.animations = {} +-- Provide safe accent-color fallbacks before Widgets.lua initializes its +-- richer helpers. This keeps later files from exploding if a mixed install +-- loads newer callers before the real widget helpers are available. +if not Cell.GetAccentColorRGB then + local fallbackAccentColor = {0.7, 0.7, 0.7} + local fallbackAccentColorString = "|cFFB2B2B2" + + local function ClampColorComponent(value) + value = tonumber(value) or 0 + if value < 0 then return 0 end + if value > 1 then return 1 end + return value + end + + local function UpdateFallbackAccentColorString() + fallbackAccentColorString = ("|cFF%02X%02X%02X"):format( + floor(ClampColorComponent(fallbackAccentColor[1]) * 255 + 0.5), + floor(ClampColorComponent(fallbackAccentColor[2]) * 255 + 0.5), + floor(ClampColorComponent(fallbackAccentColor[3]) * 255 + 0.5) + ) + end + + function Cell.OverrideAccentColor(cTable) + if type(cTable) ~= "table" then return end + + fallbackAccentColor[1] = ClampColorComponent(cTable[1] or fallbackAccentColor[1]) + fallbackAccentColor[2] = ClampColorComponent(cTable[2] or fallbackAccentColor[2]) + fallbackAccentColor[3] = ClampColorComponent(cTable[3] or fallbackAccentColor[3]) + UpdateFallbackAccentColorString() + end + + function Cell.GetAccentColorRGB() + return unpack(fallbackAccentColor) + end + + function Cell.GetAccentColorTable(alpha) + if alpha then + return {fallbackAccentColor[1], fallbackAccentColor[2], fallbackAccentColor[3], alpha} + end + + return fallbackAccentColor + end + + function Cell.GetAccentColorString() + return fallbackAccentColorString + end + + function Cell.ColorFontStringWithAccentColor(fs) + if fs and fs.SetTextColor then + fs:SetTextColor(unpack(fallbackAccentColor)) + end + end + + function Cell.WrapTextInAccentColor(text) + if WrapTextInColorCode then + return WrapTextInColorCode(text, fallbackAccentColorString) + end + + return fallbackAccentColorString .. text .. "|r" + end +end + local F = Cell.funcs local I = Cell.iFuncs local P = Cell.pixelPerfectFuncs @@ -899,4 +961,4 @@ function SlashCmdList.CELL(msg, editbox) "|cFFFFB5C5/cell reset all|r: "..L["reset all Cell settings"].."." ) end -end \ No newline at end of file +end diff --git a/README.md b/README.md index 647797b2..a4804341 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,22 @@ Hope you enjoy.   +## Installation + +Install Cell from a packaged release asset on GitHub Releases, CurseForge, or Wago. + +Do __not__ install the GitHub `Source code (zip)` or `Source code (tar.gz)` archives. Those source archives do not contain the embedded libraries declared in [`.pkgmeta`](./.pkgmeta), such as `LibCustomGlow-1.0`, `LibDeflate`, `LibSerialize`, `LibSharedMedia-3.0`, and others. + +If you extract an archive and see repo-only files like `.gitignore`, `.pkgmeta`, `.github`, or `.release`, you almost certainly downloaded a source archive instead of the installable addon package. + +Maintainers can validate an extracted package with: + +```bash +./.release/validate_package.sh +``` + +  + ## Features - __Layouts:__ auto switch layout by spec/role, supports party, raid, arena, and battleground. diff --git a/README_zhCN.md b/README_zhCN.md index 42850a11..79db5ca9 100644 --- a/README_zhCN.md +++ b/README_zhCN.md @@ -19,6 +19,22 @@ Cell 不轻量,也并非全能,其目标是提供相比以往更好的用户   +## 安装说明 + +请从 GitHub Releases 的正式发布附件、CurseForge 或 Wago 安装 Cell。 + +不要直接安装 GitHub 的 `Source code (zip)` 或 `Source code (tar.gz)`。这类源码压缩包不会包含 [`.pkgmeta`](./.pkgmeta) 里声明的内嵌库,比如 `LibCustomGlow-1.0`、`LibDeflate`、`LibSerialize`、`LibSharedMedia-3.0` 等。 + +如果你解压后看到 `.gitignore`、`.pkgmeta`、`.github`、`.release` 这类仓库文件,基本可以确定你下载的是源码包,而不是可直接安装的插件包。 + +维护者可以用下面的命令验证一个解压后的包是否正常: + +```bash +./.release/validate_package.sh +``` + +  + ## 特性与功能 - __布局:__ 按队伍类型/职责/专精自动切换布局,支持小队、团队、战场、竞技场。 diff --git a/Utilities/BuffTracker.lua b/Utilities/BuffTracker.lua index 87c2ff64..e9770cff 100644 --- a/Utilities/BuffTracker.lua +++ b/Utilities/BuffTracker.lua @@ -463,20 +463,8 @@ local function ShowMover(show) end Cell.RegisterCallback("ShowMover", "BuffTracker_ShowMover", ShowMover) -------------------------------------------------- -- buttons ------------------------------------------------- -local sendChannel -local function UpdateSendChannel() - if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) then - sendChannel = "INSTANCE_CHAT" - elseif IsInRaid() then - sendChannel = "RAID" - else - sendChannel = "PARTY" - end -end - local function CreateBuffButton(parent, buff) local b = CreateFrame("Button", nil, parent, "SecureActionButtonTemplate,BackdropTemplate") if parent then b:SetFrameLevel(parent:GetFrameLevel() + 1) end @@ -496,12 +484,12 @@ local function CreateBuffButton(parent, buff) -- chat b:HookScript("OnClick", function(self, button, down) if button == "RightButton" and (down == GetCVarBool("ActionButtonUseKeyDown")) then - -- SendChatMessage is protected during encounters on Midnight - if InCombatLockdown() then return end local msg = GetUnaffectedString(buff) if msg then - UpdateSendChannel() - SendChatMessage(msg, sendChannel) + local sendChannel = F.GetGroupCommChannel() + if sendChannel then + F.TrySendChatMessage(msg, sendChannel) + end end end end) @@ -963,4 +951,4 @@ local function UpdatePixelPerfect() b:UpdatePixelPerfect() end end -Cell.RegisterCallback("UpdatePixelPerfect", "BuffTracker_UpdatePixelPerfect", UpdatePixelPerfect) \ No newline at end of file +Cell.RegisterCallback("UpdatePixelPerfect", "BuffTracker_UpdatePixelPerfect", UpdatePixelPerfect) diff --git a/Utilities/BuffTracker_Classic.lua b/Utilities/BuffTracker_Classic.lua index 3e4fc63f..be47dd51 100644 --- a/Utilities/BuffTracker_Classic.lua +++ b/Utilities/BuffTracker_Classic.lua @@ -384,17 +384,6 @@ Cell.RegisterCallback("ShowMover", "BuffTracker_ShowMover", ShowMover) --------------------------------------------------------------------- -- buttons --------------------------------------------------------------------- -local sendChannel -local function UpdateSendChannel() - if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) then - sendChannel = "INSTANCE_CHAT" - elseif IsInRaid() then - sendChannel = "RAID" - else - sendChannel = "PARTY" - end -end - local function CreateBuffButton(parent, size, spell1, spell2, icon, index) local b = CreateFrame("Button", nil, parent, "SecureActionButtonTemplate,BackdropTemplate") if parent then b:SetFrameLevel(parent:GetFrameLevel() + 1) end @@ -412,8 +401,10 @@ local function CreateBuffButton(parent, size, spell1, spell2, icon, index) if button == "RightButton" and (down == GetCVarBool("ActionButtonUseKeyDown")) then local msg = F.GetUnaffectedString(index) if msg then - UpdateSendChannel() - SendChatMessage(msg, sendChannel) + local sendChannel = F.GetGroupCommChannel() + if sendChannel then + F.TrySendChatMessage(msg, sendChannel) + end end end end) @@ -881,4 +872,4 @@ local function UpdatePixelPerfect() b:UpdatePixelPerfect() end end -Cell.RegisterCallback("UpdatePixelPerfect", "BuffTracker_UpdatePixelPerfect", UpdatePixelPerfect) \ No newline at end of file +Cell.RegisterCallback("UpdatePixelPerfect", "BuffTracker_UpdatePixelPerfect", UpdatePixelPerfect) diff --git a/Utilities/DeathReport.lua b/Utilities/DeathReport.lua index f7c87754..27b82a58 100644 --- a/Utilities/DeathReport.lua +++ b/Utilities/DeathReport.lua @@ -21,9 +21,9 @@ local limit, count local function Send(msg) if Cell.hasHighestPriority then if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) then - SendChatMessage(strupper(ACTION_UNIT_DIED)..": "..msg, "INSTANCE_CHAT") + F.TrySendChatMessage(strupper(ACTION_UNIT_DIED)..": "..msg, "INSTANCE_CHAT") else - SendChatMessage(strupper(ACTION_UNIT_DIED)..": "..msg, IsInRaid() and "RAID" or "PARTY") + F.TrySendChatMessage(strupper(ACTION_UNIT_DIED)..": "..msg, IsInRaid() and "RAID" or "PARTY") end end end @@ -314,4 +314,4 @@ local function UpdateTools(which) end end end -Cell.RegisterCallback("UpdateTools", "DeathReport_UpdateTools", UpdateTools) \ No newline at end of file +Cell.RegisterCallback("UpdateTools", "DeathReport_UpdateTools", UpdateTools) diff --git a/Utilities/ReadyAndPull.lua b/Utilities/ReadyAndPull.lua index b99b2813..959fed4c 100644 --- a/Utilities/ReadyAndPull.lua +++ b/Utilities/ReadyAndPull.lua @@ -87,7 +87,7 @@ local function Start(sec, sendToChat) isPullTickerRunning = false pullBtn:SetText(L["Go!"]) if sendToChat then - SendChatMessage(L["Go!"], IsInRaid() and "RAID_WARNING" or "PARTY") + F.TrySendChatMessage(L["Go!"], IsInRaid() and "RAID_WARNING" or "PARTY") end elseif pullBtn.sec == -1 then pullBtn:SetText(L["Pull"]) @@ -95,9 +95,9 @@ local function Start(sec, sendToChat) pullBtn:SetText(pullBtn.sec) if sendToChat then if pullBtn.sec > 3 then - SendChatMessage(pullBtn.sec, IsInRaid() and "RAID" or "PARTY") + F.TrySendChatMessage(pullBtn.sec, IsInRaid() and "RAID" or "PARTY") else - SendChatMessage(pullBtn.sec, IsInRaid() and "RAID_WARNING" or "PARTY") + F.TrySendChatMessage(pullBtn.sec, IsInRaid() and "RAID_WARNING" or "PARTY") end end end @@ -339,11 +339,11 @@ local function UpdateTools(which) pullBtn:SetAttribute("type2", nil) pullBtn:SetScript("OnMouseUp", function(self, button) if button == "LeftButton" then - SendChatMessage(L["Pull in %d sec"]:format(CellDB["tools"]["readyAndPull"][3][2]), IsInRaid() and "RAID_WARNING" or "PARTY") + F.TrySendChatMessage(L["Pull in %d sec"]:format(CellDB["tools"]["readyAndPull"][3][2]), IsInRaid() and "RAID_WARNING" or "PARTY") Start(CellDB["tools"]["readyAndPull"][3][2], true) else if isPullTickerRunning then - SendChatMessage(L["Pull timer cancelled"], IsInRaid() and "RAID_WARNING" or "PARTY") + F.TrySendChatMessage(L["Pull timer cancelled"], IsInRaid() and "RAID_WARNING" or "PARTY") Stop() end end @@ -374,4 +374,4 @@ local function UpdatePixelPerfect() readyBtn:UpdatePixelPerfect() pullBtn:UpdatePixelPerfect() end -Cell.RegisterCallback("UpdatePixelPerfect", "RaidButtons_UpdatePixelPerfect", UpdatePixelPerfect) \ No newline at end of file +Cell.RegisterCallback("UpdatePixelPerfect", "RaidButtons_UpdatePixelPerfect", UpdatePixelPerfect) diff --git a/Utilities/Request_Dispel.lua b/Utilities/Request_Dispel.lua index 66694e68..2fb32678 100644 --- a/Utilities/Request_Dispel.lua +++ b/Utilities/Request_Dispel.lua @@ -117,12 +117,12 @@ local function CreateDRPane() drMacroEB = Cell.CreateEditBox(drPane, 412, 20) drMacroEB:SetPoint("TOPLEFT", drResponseDD, "BOTTOMLEFT", 0, -27) - drMacroEB:SetText("/run C_ChatInfo.SendAddonMessage(\"CELL_REQ_D\",\"D\",\"RAID\")") + drMacroEB:SetText("/run Cell.funcs.SendRequestAddonMessage(\"CELL_REQ_D\",\"D\")") drMacroEB:SetCursorPosition(0) drMacroEB:SetScript("OnTextChanged", function(self, userChanged) if userChanged then - drMacroEB:SetText("/run C_ChatInfo.SendAddonMessage(\"CELL_REQ_D\",\"D\",\"RAID\")") + drMacroEB:SetText("/run Cell.funcs.SendRequestAddonMessage(\"CELL_REQ_D\",\"D\")") drMacroEB:SetCursorPosition(0) drMacroEB:HighlightText() end @@ -476,4 +476,4 @@ local function ShowUtilitySettings(which) drPane:Hide() end end -Cell.RegisterCallback("ShowUtilitySettings", "DispelRequest_ShowUtilitySettings", ShowUtilitySettings) \ No newline at end of file +Cell.RegisterCallback("ShowUtilitySettings", "DispelRequest_ShowUtilitySettings", ShowUtilitySettings) diff --git a/Utilities/Request_Show.lua b/Utilities/Request_Show.lua index 9c274d9c..8b0eb73f 100644 --- a/Utilities/Request_Show.lua +++ b/Utilities/Request_Show.lua @@ -162,13 +162,13 @@ local function CheckSRConditions(spellId, unit, sender) return true else if srReplyCD then -- reply cooldown - SendChatMessage(GetSpellLink(spellId).." "..format(COOLDOWN_TIME, F.SecondsToTime(cdLeft)), "WHISPER", nil, sender) + F.TrySendChatMessage(GetSpellLink(spellId).." "..format(COOLDOWN_TIME, F.SecondsToTime(cdLeft)), "WHISPER", nil, sender) end return false end else -- NOTE: no require free cd if srReplyCD and not isReady then -- reply cd if cd - SendChatMessage(GetSpellLink(spellId).." "..format(COOLDOWN_TIME, F.SecondsToTime(cdLeft)), "WHISPER", nil, sender) + F.TrySendChatMessage(GetSpellLink(spellId).." "..format(COOLDOWN_TIME, F.SecondsToTime(cdLeft)), "WHISPER", nil, sender) end return true end @@ -254,7 +254,7 @@ function SR:COMBAT_LOG_EVENT_UNFILTERED(_, event, _, sourceGUID, sourceName, sou F.Debug("|cffdda15eSR_HIDE [|cffbc6c25CLEU:"..event.."|r]:|r", unit, buffId, Cell.vars.guids[sourceGUID]) -- cast msg (if castByMe) if sourceGUID == Cell.vars.playerGUID and srCastMsg then - SendChatMessage(srCastMsg, "WHISPER", nil, GetUnitName(unit, true)) + F.TrySendChatMessage(srCastMsg, "WHISPER", nil, GetUnitName(unit, true)) end -- clear srUnits[unit] = nil @@ -451,4 +451,4 @@ local function DR_UpdateRequests(which) end) end end -Cell.RegisterCallback("UpdateRequests", "DR_UpdateRequests", DR_UpdateRequests) \ No newline at end of file +Cell.RegisterCallback("UpdateRequests", "DR_UpdateRequests", DR_UpdateRequests) diff --git a/Utilities/Request_Spell.lua b/Utilities/Request_Spell.lua index 30ffd5e7..abf869e4 100644 --- a/Utilities/Request_Spell.lua +++ b/Utilities/Request_Spell.lua @@ -27,10 +27,10 @@ local function ShowSpellOptions(index) if responseType == "all" then srMacroText:SetText(L["Macro"]) - macroText = "/run C_ChatInfo.SendAddonMessage(\"CELL_REQ_S\",\""..spellId.."\",\"RAID\")" + macroText = "/run Cell.funcs.SendRequestAddonMessage(\"CELL_REQ_S\",\""..spellId.."\")" elseif responseType == "me" then srMacroText:SetText(L["Macro"]) - macroText = "/run C_ChatInfo.SendAddonMessage(\"CELL_REQ_S\",\""..spellId..":"..GetUnitName("player").."\",\"RAID\")" + macroText = "/run Cell.funcs.SendRequestAddonMessage(\"CELL_REQ_S\",\""..spellId..":"..GetUnitName("player").."\")" else -- whisper srMacroText:SetText(L["Contains"]) keywords = CellDB["spellRequest"]["spells"][index]["keywords"] @@ -775,4 +775,4 @@ local function ShowUtilitySettings(which) srPane:Hide() end end -Cell.RegisterCallback("ShowUtilitySettings", "SpellRequest_ShowUtilitySettings", ShowUtilitySettings) \ No newline at end of file +Cell.RegisterCallback("ShowUtilitySettings", "SpellRequest_ShowUtilitySettings", ShowUtilitySettings) diff --git a/Utils.lua b/Utils.lua index 7a1290dd..2030d9c7 100644 --- a/Utils.lua +++ b/Utils.lua @@ -2603,3 +2603,102 @@ function F.IsValueNonSecret(val) if not issecretvalue then return true end return not issecretvalue(val) end + +------------------------------------------------- +-- Midnight communication helpers +------------------------------------------------- +local restrictedChatTypes = { + PARTY = true, + PARTY_LEADER = true, + RAID = true, + RAID_LEADER = true, + RAID_WARNING = true, + INSTANCE_CHAT = true, + INSTANCE_CHAT_LEADER = true, + WHISPER = true, + GUILD = true, + OFFICER = true, + CHANNEL = true, +} + +local restrictedAddonChannels = { + PARTY = true, + RAID = true, + INSTANCE_CHAT = true, + WHISPER = true, + GUILD = true, + OFFICER = true, + CHANNEL = true, +} + +function F.IsCommRestricted() + if not Cell.isMidnight then return false end + if IsEncounterInProgress and IsEncounterInProgress() then return true end + if C_MythicPlus and C_MythicPlus.IsRunActive and C_MythicPlus.IsRunActive() then return true end + if C_PvP and C_PvP.IsActiveBattlefield and C_PvP.IsActiveBattlefield() then return true end + return false +end + +function F.IsSecretContextActive() + return F.IsAuraRestricted() or F.IsCooldownRestricted() or F.IsCommRestricted() +end + +function F.GetGroupCommChannel() + if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) then + return "INSTANCE_CHAT" + elseif IsInRaid() then + return "RAID" + elseif IsInGroup() then + return "PARTY" + end +end + +function F.CanSendChatMessage(chatType) + if not chatType then return false end + if not Cell.isMidnight then return true end + if restrictedChatTypes[chatType] and F.IsCommRestricted() then + return false + end + return true +end + +function F.TrySendChatMessage(msg, chatType, language, target) + if not msg or msg == "" or not chatType then return false end + msg = tostring(msg) + if not F.CanSendChatMessage(chatType) then + F.Debug("Cell: Chat suppressed - restricted context ("..tostring(chatType)..")") + return false + end + SendChatMessage(msg, chatType, language, target) + return true +end + +function F.CanSendAddonMessage(channel) + if not channel then return false end + if not Cell.isMidnight then return true end + if restrictedAddonChannels[channel] and F.IsCommRestricted() then + return false + end + return true +end + +function F.TrySendAddonMessage(prefix, message, channel, target) + if not prefix or not message or not channel then return false end + if not (C_ChatInfo and C_ChatInfo.SendAddonMessage) then return false end + message = tostring(message) + if not F.CanSendAddonMessage(channel) then + F.Debug("Cell: Addon message suppressed - restricted context ("..tostring(prefix)..")") + return false + end + C_ChatInfo.SendAddonMessage(prefix, message, channel, target) + return true +end + +function F.SendRequestAddonMessage(prefix, message, target) + local channel = F.GetGroupCommChannel() + if not channel then + F.Debug("Cell: Addon message suppressed - no group channel ("..tostring(prefix)..")") + return false + end + return F.TrySendAddonMessage(prefix, message, channel, target) +end From 705b1833580ad5b8edb93e8d62269bd061377b50 Mon Sep 17 00:00:00 2001 From: Skye Date: Wed, 8 Apr 2026 04:23:38 -0300 Subject: [PATCH 19/61] Prepare r275.8-skyking-dev release --- .gitignore | 5 +- CHANGELOG.md | 14 + Cell.toc | 4 +- Cell_Cata.toc | 4 +- Cell_Mists.toc | 4 +- Cell_TBC.toc | 4 +- Cell_Vanilla.toc | 4 +- Cell_Wrath.toc | 4 +- Comm/Comm.lua | 127 ++++++ Core.lua | 99 ++++- Defaults/Indicator_DefaultSpells.lua | 44 +- Locales/enUS.lua | 13 + Locales/ptBR.lua | 1 + Modules/About/About.lua | 97 ++++- Modules/About/Backup.lua | 350 ++++++++++++++- Modules/About/ImportExport.lua | 22 + Modules/ClickCastings/ImportExport.lua | 14 +- Modules/Indicators/Import.lua | 13 +- Modules/Indicators/Indicators.lua | 78 +++- Modules/Layouts/ImportExport.lua | 12 + Modules/OptionsFrame.lua | 66 ++- Modules/RaidDebuffs/ImportExport.lua | 13 +- Modules/RaidDebuffs/RaidDebuffs.lua | 419 +++++++++++++++++- Modules/Utilities/Utilities.lua | 25 +- RaidFrames/UnitButton.lua | 51 ++- Revise.lua | 249 ++++++++++- Utilities/LoadUtilities.xml | 3 +- Utilities/MidnightTools.lua | 567 +++++++++++++++++++++++++ Utilities/QuickAssist_ImportExport.lua | 14 +- Utils.lua | 164 +++++-- 30 files changed, 2344 insertions(+), 140 deletions(-) create mode 100644 Utilities/MidnightTools.lua diff --git a/.gitignore b/.gitignore index eaedd694..cd5e2008 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ .resources/ .utils/*.csv Libs/* -*.Zone.Identifier + # excluded !.release/*.sh @@ -24,8 +24,5 @@ Libs/* .codex Cell-r*-release-*.zip -# local-only notes -MIDNIGHT_API_ANALISE.md - # local tooling / references skills/ diff --git a/CHANGELOG.md b/CHANGELOG.md index c28ee365..52ecd2c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# r275.8-skyking-dev Compatibility reports, diagnostics, and curation tools + +## Compatibility & Stability +- Added a compatibility report that persists "No" on old-profile reset prompts and points to the affected layouts/indicators instead of asking again every login. +- Indicators now surface compatibility issues directly in the list, with red highlighting and tooltips for invalid spell IDs, duplicate built-ins, and missing built-ins. +- Fixed `secret boolean` crashes in range and group checks by guarding `UnitIsUnit`, `UnitInParty`, `UnitInRaid`, and related target-resolution helpers. + +## Tools & Backups +- Added an About notifications center plus automatic snapshots around imports and destructive flows, with reuse/retention controls for auto backups. +- Added Midnight diagnostics and utility tools for comm restrictions, queued sync traffic, and group version visibility. + +## Raid Debuffs +- Added raid debuff curation metadata, reporting, and review states to make Midnight debuff cleanup easier without losing the underlying spell list. + # r275.6 Midnight dispel filtering and private aura options ## Indicators diff --git a/Cell.toc b/Cell.toc index 1f0ac3cf..c74b8549 100644 --- a/Cell.toc +++ b/Cell.toc @@ -1,6 +1,6 @@ ## Interface: 120001 ## Title: Cell -## Version: r275-release +## Version: r275.8-skyking-dev ## Author: enderneko ## X-Flavor: Mainline ## SavedVariables: CellDB, CellDBBackup @@ -49,4 +49,4 @@ RaidFrames\Groups\PetFrame.lua RaidFrames\Groups\NPCFrame.lua RaidFrames\Groups\SpotlightFrame.lua -Supporters.lua \ No newline at end of file +Supporters.lua diff --git a/Cell_Cata.toc b/Cell_Cata.toc index 13c6ab21..ca3235d6 100644 --- a/Cell_Cata.toc +++ b/Cell_Cata.toc @@ -1,6 +1,6 @@ ## Interface: 40402 ## Title: Cell -## Version: r275-beta +## Version: r275.8-skyking-dev ## Author: enderneko ## X-Flavor: Cata ## SavedVariables: CellDB, CellDBBackup @@ -49,4 +49,4 @@ RaidFrames\Groups\PetFrame.lua RaidFrames\Groups\NPCFrame.lua RaidFrames\Groups\SpotlightFrame.lua -Supporters.lua \ No newline at end of file +Supporters.lua diff --git a/Cell_Mists.toc b/Cell_Mists.toc index 63c1e5b3..d3e93372 100644 --- a/Cell_Mists.toc +++ b/Cell_Mists.toc @@ -1,6 +1,6 @@ ## Interface: 50503 ## Title: Cell -## Version: r275-beta +## Version: r275.8-skyking-dev ## Author: enderneko ## X-Flavor: Mists ## SavedVariables: CellDB, CellDBBackup @@ -47,4 +47,4 @@ RaidFrames\Groups\PetFrame.lua RaidFrames\Groups\NPCFrame.lua RaidFrames\Groups\SpotlightFrame.lua -Supporters.lua \ No newline at end of file +Supporters.lua diff --git a/Cell_TBC.toc b/Cell_TBC.toc index 87a61566..786fddc9 100644 --- a/Cell_TBC.toc +++ b/Cell_TBC.toc @@ -1,6 +1,6 @@ ## Interface: 20505 ## Title: Cell -## Version: r275-beta +## Version: r275.8-skyking-dev ## Author: enderneko ## X-Flavor: Vanilla ## SavedVariables: CellDB, CellDBBackup @@ -49,4 +49,4 @@ RaidFrames\Groups\PetFrame.lua RaidFrames\Groups\NPCFrame.lua RaidFrames\Groups\SpotlightFrame.lua -Supporters.lua \ No newline at end of file +Supporters.lua diff --git a/Cell_Vanilla.toc b/Cell_Vanilla.toc index bfdc4e58..f6daabb8 100644 --- a/Cell_Vanilla.toc +++ b/Cell_Vanilla.toc @@ -1,6 +1,6 @@ ## Interface: 11508 ## Title: Cell -## Version: r275-beta +## Version: r275.8-skyking-dev ## Author: enderneko ## X-Flavor: Vanilla ## SavedVariables: CellDB, CellDBBackup @@ -49,4 +49,4 @@ RaidFrames\Groups\PetFrame.lua RaidFrames\Groups\NPCFrame.lua RaidFrames\Groups\SpotlightFrame.lua -Supporters.lua \ No newline at end of file +Supporters.lua diff --git a/Cell_Wrath.toc b/Cell_Wrath.toc index ff84af1b..2cb8720f 100644 --- a/Cell_Wrath.toc +++ b/Cell_Wrath.toc @@ -1,6 +1,6 @@ ## Interface: 38000 ## Title: Cell -## Version: r275-beta +## Version: r275.8-skyking-dev ## Author: enderneko ## X-Flavor: Wrath ## SavedVariables: CellDB, CellDBBackup @@ -49,4 +49,4 @@ RaidFrames\Groups\PetFrame.lua RaidFrames\Groups\NPCFrame.lua RaidFrames\Groups\SpotlightFrame.lua -Supporters.lua \ No newline at end of file +Supporters.lua diff --git a/Comm/Comm.lua b/Comm/Comm.lua index 3bc151da..9af689e2 100644 --- a/Comm/Comm.lua +++ b/Comm/Comm.lua @@ -6,6 +6,48 @@ local LibDeflate = LibStub:GetLibrary("LibDeflate") local deflateConfig = {level = 9} local Serializer = LibStub:GetLibrary("LibSerialize") local Comm = LibStub:GetLibrary("AceComm-3.0") +local midnightDiagnostics = Cell.vars.midnightDiagnostics or { + versions = {}, + lastVersionRequestAt = 0, + lastVersionBroadcastAt = 0, + commQueue = { + lastQueuedAt = 0, + lastFlushAt = 0, + }, +} +Cell.vars.midnightDiagnostics = midnightDiagnostics + +local function FireMidnightDiagnosticsChanged(reason) + Cell.Fire("MidnightDiagnosticsUpdated", reason) +end + +local function NormalizeSenderName(name) + if not name or name == "" then return name end + if not strfind(name, "-") then + name = name .. "-" .. GetNormalizedRealmName() + end + return name +end + +local function UpdateVersionDiagnostic(sender, version, channel) + if not sender or not version then return end + + sender = NormalizeSenderName(sender) + midnightDiagnostics.versions[sender] = { + version = version, + versionNum = tonumber(string.match(version, "%d+")) or 0, + channel = channel, + receivedAt = time(), + } + + FireMidnightDiagnosticsChanged("version") +end + +local function RecordSelfVersion(channel) + local selfName = Cell.vars.playerNameFull or F.UnitFullName("player") + if not selfName or not Cell.version then return end + UpdateVersionDiagnostic(selfName, Cell.version, channel or "SELF") +end local function Serialize(data) local serialized = Serializer:Serialize(data) -- serialize @@ -84,6 +126,10 @@ local function QueueCommMessage(prefix, message, distribution, target, priority, local key = queueKey or BuildCommQueueKey(prefix, message, distribution, target, priority) local queued = commQueueByKey[key] if queued then + queued.message = message + queued.target = target + queued.distribution = distribution + queued.priority = priority queued.callbackFn = callbackFn else queued = { @@ -98,8 +144,10 @@ local function QueueCommMessage(prefix, message, distribution, target, priority, tinsert(commQueue, queued) commQueueByKey[key] = queued end + midnightDiagnostics.commQueue.lastQueuedAt = time() StartCommFlushTicker() F.Debug("Cell: Comm queued - restricted context ("..tostring(prefix)..")") + FireMidnightDiagnosticsChanged("queue") end function F.FlushCommQueue() @@ -117,6 +165,7 @@ function F.FlushCommQueue() commQueue = {} commQueueByKey = {} StopCommFlushTicker() + midnightDiagnostics.commQueue.lastFlushAt = time() for _, queued in ipairs(pending) do if CanSendCommDistribution(queued.distribution, queued.target) then @@ -126,6 +175,7 @@ function F.FlushCommQueue() end end + FireMidnightDiagnosticsChanged("queue") return true end @@ -151,6 +201,68 @@ function F.TrySendCommMessage(prefix, message, distribution, target, priority, c return true end +function F.GetCommQueueSnapshot() + local snapshot = { + size = #commQueue, + lastQueuedAt = midnightDiagnostics.commQueue.lastQueuedAt, + lastFlushAt = midnightDiagnostics.commQueue.lastFlushAt, + entries = {}, + prefixCounts = {}, + } + + for _, queued in ipairs(commQueue) do + tinsert(snapshot.entries, { + prefix = queued.prefix, + distribution = queued.distribution, + target = queued.target, + priority = queued.priority, + }) + snapshot.prefixCounts[queued.prefix] = (snapshot.prefixCounts[queued.prefix] or 0) + 1 + end + + return snapshot +end + +function F.GetVersionDiagnosticsSnapshot() + local snapshot = { + entries = {}, + lastVersionRequestAt = midnightDiagnostics.lastVersionRequestAt, + lastVersionBroadcastAt = midnightDiagnostics.lastVersionBroadcastAt, + } + + for sender, info in pairs(midnightDiagnostics.versions) do + snapshot.entries[sender] = { + version = info.version, + versionNum = info.versionNum, + channel = info.channel, + receivedAt = info.receivedAt, + } + end + + return snapshot +end + +function F.RequestVersionDiagnostics() + RecordSelfVersion("SELF") + + local sendChannel = F.GetGroupCommChannel() + if not sendChannel then + FireMidnightDiagnosticsChanged("version") + return false + end + + midnightDiagnostics.lastVersionRequestAt = time() + midnightDiagnostics.lastVersionBroadcastAt = time() + F.TrySendCommMessage("CELL_VERSION_REQ", "req", sendChannel, nil, "ALERT", nil, { + queueKey = "CELL_VERSION_REQ:" .. sendChannel, + }) + F.TrySendCommMessage("CELL_VERSION", Cell.version, sendChannel, nil, "NORMAL", nil, { + queueKey = "CELL_VERSION:" .. sendChannel, + }) + FireMidnightDiagnosticsChanged("version") + return true +end + ----------------------------------------- -- for WA ----------------------------------------- @@ -174,6 +286,7 @@ function eventFrame:GROUP_ROSTER_UPDATE() eventFrame:UnregisterEvent("GROUP_ROSTER_UPDATE") local sendChannel = F.GetGroupCommChannel() if sendChannel then + midnightDiagnostics.lastVersionBroadcastAt = time() F.TrySendCommMessage("CELL_VERSION", Cell.version, sendChannel, nil, "NORMAL") end end @@ -181,13 +294,16 @@ end eventFrame:RegisterEvent("PLAYER_LOGIN") function eventFrame:PLAYER_LOGIN() + RecordSelfVersion("SELF") if IsInGuild() then + midnightDiagnostics.lastVersionBroadcastAt = time() F.TrySendCommMessage("CELL_VERSION", Cell.version, "GUILD", nil, "NORMAL") end end Comm:RegisterComm("CELL_VERSION", function(prefix, message, channel, sender) if sender == UnitName("player") then return end + UpdateVersionDiagnostic(sender, message, channel) local version = tonumber(string.match(message, "%d+")) local myVersion = tonumber(string.match(Cell.version, "%d+")) if (not CellDB["lastVersionCheck"] or time()-CellDB["lastVersionCheck"]>=25200) and version and myVersion and myVersion < version then @@ -196,6 +312,17 @@ Comm:RegisterComm("CELL_VERSION", function(prefix, message, channel, sender) end end) +Comm:RegisterComm("CELL_VERSION_REQ", function(prefix, message, channel, sender) + if sender == UnitName("player") then return end + if not channel then return end + + local target = channel == "WHISPER" and sender or nil + midnightDiagnostics.lastVersionBroadcastAt = time() + F.TrySendCommMessage("CELL_VERSION", Cell.version, channel, target, "NORMAL", nil, { + queueKey = "CELL_VERSION_REPLY:" .. tostring(channel) .. ":" .. tostring(target or ""), + }) +end) + ----------------------------------------- -- Notify Marks ----------------------------------------- diff --git a/Core.lua b/Core.lua index 5dfa9e80..dfe9846b 100644 --- a/Core.lua +++ b/Core.lua @@ -278,6 +278,34 @@ function eventFrame:ADDON_LOADED(arg1) if type(CellDB["snippets"]) ~= "table" then CellDB["snippets"] = {} end if not CellDB["snippets"][0] then CellDB["snippets"][0] = F.GetDefaultSnippet() end + if type(CellDB["midnightTools"]) ~= "table" then + CellDB["midnightTools"] = { + ["showQueueIndicator"] = true, + } + end + if CellDB["midnightTools"]["showQueueIndicator"] == nil then + CellDB["midnightTools"]["showQueueIndicator"] = true + end + if type(CellDB["systemTools"]) ~= "table" then + CellDB["systemTools"] = { + ["autoBackupsEnabled"] = true, + ["maxAutoBackups"] = 12, + ["maxNotifications"] = 40, + } + end + if CellDB["systemTools"]["autoBackupsEnabled"] == nil then + CellDB["systemTools"]["autoBackupsEnabled"] = true + end + if CellDB["systemTools"]["maxAutoBackups"] == nil then + CellDB["systemTools"]["maxAutoBackups"] = 12 + end + if CellDB["systemTools"]["maxNotifications"] == nil then + CellDB["systemTools"]["maxNotifications"] = 40 + end + if type(CellDB["addonNotifications"]) ~= "table" then + CellDB["addonNotifications"] = {} + end + Cell.vars.playerClass, Cell.vars.playerClassID = UnitClassBase("player") -- general -------------------------------------------------------------------------------- @@ -559,6 +587,7 @@ function eventFrame:ADDON_LOADED(arg1) -- raid debuffs --------------------------------------------------------------------------- if type(CellDB["raidDebuffs"]) ~= "table" then CellDB["raidDebuffs"] = {} end + if type(CellDB["raidDebuffsCuration"]) ~= "table" then CellDB["raidDebuffsCuration"] = {} end -- CellDB["raidDebuffs"] = { -- [instanceId] = { -- ["general"] = { @@ -1094,6 +1123,63 @@ function SlashCmdList.CELL(msg, editbox) F.Print(L["A 0-40 integer is required."]) end + elseif command == "midnight" then + if not Cell.isMidnight then + F.Print("Midnight tools are only available on Midnight builds.") + elseif rest == "print" or rest == "status" then + if F.PrintMidnightDiagnostics then + F.PrintMidnightDiagnostics() + end + elseif rest == "refresh" or rest == "versions" then + if F.RequestVersionDiagnostics and F.RequestVersionDiagnostics() then + F.Print("Midnight sync diagnostics refresh requested.") + else + F.Print("No group channel is available for Midnight sync diagnostics.") + end + elseif rest == "flush" then + if F.FlushCommQueue and F.FlushCommQueue() then + F.Print("Midnight comm queue flushed.") + else + F.Print("Midnight comm queue is still blocked by restrictions.") + end + elseif rest == "cvars" or rest == "test" then + if F.ShowMidnightTestCVars then + F.ShowMidnightTestCVars() + end + elseif F.ShowMidnightTools then + F.ShowMidnightTools() + end + + elseif command == "restrictions" then + if not Cell.isMidnight then + F.Print("Restriction diagnostics are only available on Midnight builds.") + elseif F.PrintMidnightDiagnostics then + F.PrintMidnightDiagnostics() + end + + elseif command == "syncdiag" then + if not Cell.isMidnight then + F.Print("Sync diagnostics are only available on Midnight builds.") + elseif rest == "refresh" then + if F.RequestVersionDiagnostics and F.RequestVersionDiagnostics() then + F.Print("Midnight sync diagnostics refresh requested.") + else + F.Print("No group channel is available for Midnight sync diagnostics.") + end + elseif rest == "flush" then + if F.FlushCommQueue and F.FlushCommQueue() then + F.Print("Midnight comm queue flushed.") + else + F.Print("Midnight comm queue is still blocked by restrictions.") + end + elseif rest == "print" then + if F.PrintMidnightDiagnostics then + F.PrintMidnightDiagnostics() + end + elseif F.ShowMidnightTools then + F.ShowMidnightTools() + end + -- elseif command == "buff" then -- rest = tonumber(rest:format("%d")) -- if rest and rest > 0 then @@ -1105,6 +1191,16 @@ function SlashCmdList.CELL(msg, editbox) -- end else + local midnightHelp = "" + if Cell.isMidnight then + midnightHelp = "\n".. + "|cFFFFB5C5/cell midnight|r: open Midnight Tools.\n".. + "|cFFFFB5C5/cell midnight cvars|r: show Midnight restriction test CVars.\n".. + "|cFFFFB5C5/cell syncdiag refresh|r: request version and sync diagnostics.\n".. + "|cFFFFB5C5/cell syncdiag flush|r: flush the queued Midnight comm messages.\n".. + "|cFFFFB5C5/cell restrictions|r: print the current Midnight restriction state." + end + F.Print(L["Available slash commands"]..":\n".. "|cFFFFB5C5/cell options|r, |cFFFFB5C5/cell opt|r: "..L["show Cell options frame"]..".\n".. "|cFFFFB5C5/cell healers|r: "..L["create a \"Healers\" indicator"]..".\n".. @@ -1116,7 +1212,8 @@ function SlashCmdList.CELL(msg, editbox) "|cFFFFB5C5/cell reset raiddebuffs|r: "..L["reset all Raid Debuffs"]..".\n".. "|cFFFFB5C5/cell reset snippets|r: "..L["reset all Code Snippets"]..".\n".. "|cFFFFB5C5/cell reset quickassist|r: "..L["reset Quick Assist for current spec"]..".\n".. - "|cFFFFB5C5/cell reset all|r: "..L["reset all Cell settings"].."." + "|cFFFFB5C5/cell reset all|r: "..L["reset all Cell settings"]..".".. + midnightHelp ) end end diff --git a/Defaults/Indicator_DefaultSpells.lua b/Defaults/Indicator_DefaultSpells.lua index 68f38aba..36e8fdd6 100644 --- a/Defaults/Indicator_DefaultSpells.lua +++ b/Defaults/Indicator_DefaultSpells.lua @@ -184,8 +184,13 @@ function I.UpdateAoEHealings(t) end function I.IsAoEHealing(name, id) - if not F.IsValueNonSecret(name) or not F.IsValueNonSecret(id) then return end - return builtInAoEHealings[name] or builtInAoEHealings[id] or customAoEHealings[id] + if F.IsValueNonSecret(name) and builtInAoEHealings[name] then + return true + end + + if F.IsValueNonSecret(id) then + return builtInAoEHealings[id] or customAoEHealings[id] + end end local summonDuration = { @@ -344,16 +349,24 @@ end local UnitIsUnit = UnitIsUnit local bos = F.GetSpellInfo(6940) -- 牺牲祝福 function I.IsExternalCooldown(name, id, source, target) - if not F.IsValueNonSecret(name) or not F.IsValueNonSecret(id) then return end - if name == bos then + local nameIsReadable = F.IsValueNonSecret(name) + local idIsReadable = F.IsValueNonSecret(id) + + if nameIsReadable and name == bos then if source and target then -- NOTE: hide bos on caster return not UnitIsUnit(source, target) else return true end - else - return builtInExternals[name] or builtInExternals[id] or customExternals[id] + end + + if nameIsReadable and builtInExternals[name] then + return true + end + + if idIsReadable then + return builtInExternals[id] or customExternals[id] end end @@ -418,6 +431,7 @@ local defensives = { -- true: track by name, false: track by id [498] = true, -- 圣佑术 - Divine Protection [642] = true, -- 圣盾术 - Divine Shield [31850] = true, -- 炽热防御者 - Ardent Defender + [86659] = true, -- 远古列王守卫 - Guardian of Ancient Kings (base buff) [212641] = true, -- 远古列王守卫 - Guardian of Ancient Kings [205191] = true, -- 以眼还眼 - Eye for an Eye [389539] = true, -- 戒卫 - Sentinel @@ -497,8 +511,13 @@ function I.UpdateDefensives(t) end function I.IsDefensiveCooldown(name, id) - if not F.IsValueNonSecret(name) or not F.IsValueNonSecret(id) then return end - return builtInDefensives[name] or builtInDefensives[id] or customDefensives[id] + if F.IsValueNonSecret(name) and builtInDefensives[name] then + return true + end + + if F.IsValueNonSecret(id) then + return builtInDefensives[id] or customDefensives[id] + end end ------------------------------------------------- @@ -1324,6 +1343,11 @@ function I.UpdateCrowdControls(t) end function I.IsCrowdControls(name, id) - if not F.IsValueNonSecret(name) or not F.IsValueNonSecret(id) then return end - return builtInCrowdControls[name] or builtInCrowdControls[id] or customCrowdControls[name] + if F.IsValueNonSecret(name) and (builtInCrowdControls[name] or customCrowdControls[name]) then + return true + end + + if F.IsValueNonSecret(id) then + return builtInCrowdControls[id] + end end diff --git a/Locales/enUS.lua b/Locales/enUS.lua index fee7be99..0e449f6c 100644 --- a/Locales/enUS.lua +++ b/Locales/enUS.lua @@ -71,6 +71,7 @@ select(2, ...).L = setmetatable({ ["Default"] = _G.DEFAULT, ["ABOUT"] = "Cell is a nice raid frame addon inspired by several great addons, such as CompactRaid, Grid2, Aptechka and VuhDo.\nWith a more human-friendly interface, Cell can provide a better user experience, better than ever.", + ["Compatibility"] = "Compatibility", ["RESET"] = "Cell requires a full reset after updating from a very old version", ["RESET_CHARACTER"] = "Cell requires a character profile reset after updating from a very old version", ["RESET_INCLUDES"] = "Only Click-Castings and Layout Auto Switch are included", @@ -92,6 +93,18 @@ select(2, ...).L = setmetatable({

If there are any issues after an update, check through all code snippets first.


+

r275.8-skyking-dev Compatibility reports, diagnostics, and curation tools

+

Compatibility & Stability

+

+ Added a compatibility report that persists "No" on old-profile reset prompts and points to affected layouts and indicators.

+

+ Indicators now flag compatibility problems directly in the list, with red highlighting and tooltips for invalid spell IDs and built-in mismatches.

+

* Fixed secret boolean crashes in range and group checks by guarding UnitIsUnit, UnitInParty, UnitInRaid, and related target-resolution helpers.

+

Tools & Backups

+

+ Added an About notifications center and automatic backups around imports and destructive flows.

+

+ Added Midnight diagnostics tools for comm restrictions, queued sync traffic, and group version visibility.

+

Raid Debuffs

+

+ Added raid debuff curation metadata, reporting, and review states for Midnight cleanup.

+
+

r275.5 Added Midnight Raid Debuffs

Raid Debuffs

+ Added initial Midnight expansion raid debuffs for all 12 instances (6 raids, 6 dungeons) and 41 bosses.

diff --git a/Locales/ptBR.lua b/Locales/ptBR.lua index d6a8628c..97d3a9d3 100644 --- a/Locales/ptBR.lua +++ b/Locales/ptBR.lua @@ -501,6 +501,7 @@ L["Report deaths to group"] = "Relatar mortes ao grupo" L["Request"] = "Pedir" L["Require font support"] = "Requer suporte de fonte" L["Require reload of the UI"] = "Exigir reload da UI" +L["Compatibility"] = "Compatibilidade" L["RESET"] = "Cell requer reinicialização completa após atualização de uma versão muito antiga" L["Reset"] = "Redefinir" L["Reset All"] = "Redefinir tudo" diff --git a/Modules/About/About.lua b/Modules/About/About.lua index 771312de..f5118659 100644 --- a/Modules/About/About.lua +++ b/Modules/About/About.lua @@ -10,8 +10,76 @@ aboutTab:Hide() local authorText, specialThanksText, supportersText1, supportersText2 local translatorsTextCN, translatorsTextKR, translatorsTextPT, translatorsTextDE, translatorsTextRU, translatorsTextFR, translatorsTextES, translatorsTextIT +local compatibilityBtn, compatibilityStatus, compatibilityFrame, compatibilityTextArea local UpdateFont +local function CreateCompatibilityReportFrame() + if compatibilityFrame then return end + + compatibilityFrame = CreateFrame("Frame", "CellOptionsFrame_CompatibilityReport", Cell.frames.optionsFrame, "BackdropTemplate") + compatibilityFrame:Hide() + compatibilityFrame:SetFrameLevel(Cell.frames.optionsFrame:GetFrameLevel() + 100) + compatibilityFrame:SetPoint("TOPLEFT", 1, -100) + P.Size(compatibilityFrame, 430, 250) + Cell.StylizeFrame(compatibilityFrame, nil, Cell.GetAccentColorTable()) + + local title = compatibilityFrame:CreateFontString(nil, "OVERLAY", "CELL_FONT_CLASS") + title:SetPoint("TOPLEFT", 5, -5) + title:SetText(L["Compatibility"] or "Compatibility") + + local closeBtn = Cell.CreateButton(compatibilityFrame, "×", "red", {18, 18}, false, false, "CELL_FONT_SPECIAL", "CELL_FONT_SPECIAL") + closeBtn:SetPoint("TOPRIGHT", -5, -1) + closeBtn:SetScript("OnClick", function() + compatibilityFrame:Hide() + end) + + compatibilityTextArea = Cell.CreateScrollEditBox(compatibilityFrame) + compatibilityTextArea:SetPoint("TOPLEFT", 5, -25) + compatibilityTextArea:SetPoint("BOTTOMRIGHT", -10, 5) + compatibilityTextArea.eb:SetAutoFocus(false) +end + +local function UpdateCompatibilityStatus() + if not compatibilityStatus then return end + + local report = F.GetCompatibilityReport and F.GetCompatibilityReport() + if not report or not report.hasWarnings then + compatibilityStatus:SetText("|cff77ff77No compatibility issues detected.|r") + if compatibilityBtn then + compatibilityBtn:SetEnabled(true) + end + return + end + + local summary = {} + if report.globalResetRecommended then + tinsert(summary, "global profile is too old") + end + if report.characterResetRecommended then + tinsert(summary, "character profile is too old") + end + if report.indicatorIssueCount > 0 then + tinsert(summary, ("%d indicator issue(s) found"):format(report.indicatorIssueCount)) + elseif report.layoutIssueCount > 0 then + tinsert(summary, ("%d layout issue(s) found"):format(report.layoutIssueCount)) + end + + compatibilityStatus:SetText("|cffff6b6bCompatibility warning:|r " .. table.concat(summary, ", ")) + if compatibilityBtn then + compatibilityBtn:SetEnabled(true) + end +end + +function F.ShowCompatibilityReport() + CreateCompatibilityReportFrame() + + local report = F.GetCompatibilityReport and F.GetCompatibilityReport() + compatibilityTextArea.eb:SetText(report and report.text or "No compatibility data available.") + compatibilityTextArea.eb:ClearFocus() + compatibilityTextArea.scrollFrame:ResetScroll() + compatibilityFrame:Show() +end + ------------------------------------------------- -- description ------------------------------------------------- @@ -32,12 +100,26 @@ local function CreateDescriptionPane() F.ShowCodeSnippets() end) + compatibilityBtn = Cell.CreateButton(descriptionPane, L["Compatibility"] or "Compatibility", "accent", {110, 17}) + compatibilityBtn:SetPoint("TOPRIGHT", snippetsBtn, "TOPLEFT", 1, 0) + compatibilityBtn:SetScript("OnClick", function() + F.ShowCompatibilityReport() + end) + local descText = descriptionPane:CreateFontString(nil, "OVERLAY", "CELL_FONT_WIDGET") descText:SetPoint("TOPLEFT", 5, -27) descText:SetPoint("RIGHT", -10, 0) + descText:SetPoint("BOTTOM", 0, 22) descText:SetJustifyH("LEFT") descText:SetSpacing(5) descText:SetText(L["ABOUT"]) + + compatibilityStatus = descriptionPane:CreateFontString(nil, "OVERLAY", "CELL_FONT_WIDGET") + compatibilityStatus:SetPoint("BOTTOMLEFT", 5, 6) + compatibilityStatus:SetPoint("BOTTOMRIGHT", -10, 6) + compatibilityStatus:SetJustifyH("LEFT") + compatibilityStatus:SetWordWrap(true) + UpdateCompatibilityStatus() end @@ -524,20 +606,25 @@ local function CreateImportExportPane() local iePane = Cell.CreateTitledPane(aboutTab, L["Import & Export All Settings"], 422, 50) iePane:SetPoint("TOPLEFT", 5, -595) - local importBtn = Cell.CreateButton(iePane, L["Import"], "accent-hover", {134, 20}) + local importBtn = Cell.CreateButton(iePane, L["Import"], "accent-hover", {100, 20}) importBtn:SetPoint("TOPLEFT", 5, -27) importBtn:SetScript("OnClick", F.ShowImportFrame) importBtn:SetTexture("Interface\\AddOns\\Cell\\Media\\Icons\\import", {16, 16}, {"LEFT", 2, 0}) - local exportBtn = Cell.CreateButton(iePane, L["Export"], "accent-hover", {134, 20}) + local exportBtn = Cell.CreateButton(iePane, L["Export"], "accent-hover", {100, 20}) exportBtn:SetPoint("TOPLEFT", importBtn, "TOPRIGHT", 5, 0) exportBtn:SetScript("OnClick", F.ShowExportFrame) exportBtn:SetTexture("Interface\\AddOns\\Cell\\Media\\Icons\\export", {16, 16}, {"LEFT", 2, 0}) - local backupBtn = Cell.CreateButton(iePane, L["Backups"], "accent-hover", {134, 20}) + local backupBtn = Cell.CreateButton(iePane, L["Backups"], "accent-hover", {100, 20}) backupBtn:SetPoint("TOPLEFT", exportBtn, "TOPRIGHT", 5, 0) backupBtn:SetScript("OnClick", F.ShowBackupFrame) backupBtn:SetTexture("Interface\\AddOns\\Cell\\Media\\Icons\\backup", {16, 16}, {"LEFT", 2, 0}) + + local notificationsBtn = Cell.CreateButton(iePane, "Notifications", "accent-hover", {100, 20}) + notificationsBtn:SetPoint("TOPLEFT", backupBtn, "TOPRIGHT", 5, 0) + notificationsBtn:SetScript("OnClick", F.ShowNotificationCenter) + notificationsBtn:SetTexture("Interface\\AddOns\\Cell\\Media\\Icons\\info2", {16, 16}, {"LEFT", 2, 0}) end ------------------------------------------------- @@ -559,11 +646,13 @@ local function ShowTab(tab) end aboutTab:Show() descriptionPane:SetTitle("Cell "..Cell.version) + UpdateCompatibilityStatus() else aboutTab:Hide() end end Cell.RegisterCallback("ShowOptionsTab", "AboutTab_ShowTab", ShowTab) +Cell.RegisterCallback("UpdateCompatibilityReport", "AboutTab_UpdateCompatibilityReport", UpdateCompatibilityStatus) UpdateFont = function(fs) if not fs then return end @@ -586,4 +675,4 @@ function Cell.UpdateAboutFont() UpdateFont(specialThanksText) UpdateFont(supportersText1) UpdateFont(supportersText2) -end \ No newline at end of file +end diff --git a/Modules/About/Backup.lua b/Modules/About/Backup.lua index 58f2ec0e..e5c2a362 100644 --- a/Modules/About/Backup.lua +++ b/Modules/About/Backup.lua @@ -4,9 +4,187 @@ local F = Cell.funcs local P = Cell.pixelPerfectFuncs local backupFrame +local notificationFrame local buttons = {} +local notificationButtons = {} local LoadBackups +local LoadNotifications local DATE_FORMAT = "%Y-%m-%d %H:%M:%S" +local DEFAULT_MAX_AUTO_BACKUPS = 12 +local DEFAULT_MAX_NOTIFICATIONS = 40 +local AUTO_BACKUP_RECENCY = 120 + +local function EnsureSupportTables() + if type(CellDB) ~= "table" then + return + end + + if type(CellDBBackup) ~= "table" then + CellDBBackup = {} + end + + if type(CellDB["systemTools"]) ~= "table" then + CellDB["systemTools"] = {} + end + if CellDB["systemTools"]["autoBackupsEnabled"] == nil then + CellDB["systemTools"]["autoBackupsEnabled"] = true + end + if CellDB["systemTools"]["maxAutoBackups"] == nil then + CellDB["systemTools"]["maxAutoBackups"] = DEFAULT_MAX_AUTO_BACKUPS + end + if CellDB["systemTools"]["maxNotifications"] == nil then + CellDB["systemTools"]["maxNotifications"] = DEFAULT_MAX_NOTIFICATIONS + end + + if type(CellDB["addonNotifications"]) ~= "table" then + CellDB["addonNotifications"] = {} + end + + return CellDB["systemTools"], CellDB["addonNotifications"] +end + +local function TrimAutoBackups() + local settings = EnsureSupportTables() + if not settings then return end + + local autoIndices = {} + for i, backup in ipairs(CellDBBackup) do + if backup["automatic"] then + tinsert(autoIndices, i) + end + end + + local overflow = #autoIndices - (settings["maxAutoBackups"] or DEFAULT_MAX_AUTO_BACKUPS) + if overflow <= 0 then return end + + for removed = 1, overflow do + tremove(CellDBBackup, autoIndices[removed] - (removed - 1)) + end +end + +local function GetBackupDisplayText(backup) + local prefix = backup["automatic"] and "|cFFB2B2B2[AUTO]|r " or "|cFF80FF00[MANUAL]|r " + if backup["tag"] and backup["tag"] ~= "" then + prefix = prefix .. "|cFF00CCFF[" .. backup["tag"] .. "]|r " + end + + return prefix .. backup["desc"] +end + +local function GetBackupCreatedText(backup) + if backup["createdAt"] then + return date("%m-%d %H:%M", backup["createdAt"]) + end + + return "" +end + +local function GetNotificationColor(kind) + if kind == "import" then + return 0.5, 1, 0 + elseif kind == "backup" then + return 0, 0.8, 1 + elseif kind == "warning" then + return 1, 0.3, 0.3 + end + + return 1, 1, 1 +end + +function F.AddAddonNotification(kind, title, message) + local settings, notifications = EnsureSupportTables() + if not settings or not notifications then return end + + tinsert(notifications, { + ["kind"] = kind or "info", + ["title"] = title or "Notification", + ["message"] = message or "", + ["createdAt"] = time(), + }) + + while #notifications > (settings["maxNotifications"] or DEFAULT_MAX_NOTIFICATIONS) do + tremove(notifications, 1) + end + + Cell.Fire("AddonNotificationsUpdated") +end + +function F.GetAddonNotifications() + local _, notifications = EnsureSupportTables() + return notifications or {} +end + +function F.ClearAddonNotifications() + local _, notifications = EnsureSupportTables() + if not notifications then return end + + wipe(notifications) + Cell.Fire("AddonNotificationsUpdated") +end + +function F.CreateBackupSnapshot(desc, options) + EnsureSupportTables() + + options = options or {} + desc = strtrim(desc or "") + if desc == "" then + desc = date(DATE_FORMAT) + end + + local backup = { + ["desc"] = desc, + ["version"] = Cell.version, + ["versionNum"] = Cell.versionNum, + ["DB"] = F.Copy(CellDB), + ["CharacterDB"] = CellCharacterDB and F.Copy(CellCharacterDB), + ["automatic"] = options["automatic"] and true or nil, + ["tag"] = options["tag"], + ["type"] = options["type"], + ["createdAt"] = time(), + ["signature"] = options["signature"], + } + + tinsert(CellDBBackup, backup) + TrimAutoBackups() + Cell.Fire("BackupsUpdated") + + return backup +end + +function F.CreateAutoBackup(desc, options) + local settings = EnsureSupportTables() + if not settings or not settings["autoBackupsEnabled"] then + return nil, "disabled" + end + + options = options or {} + local signature = options["signature"] or desc + + for i = #CellDBBackup, 1, -1 do + local backup = CellDBBackup[i] + if backup["automatic"] and backup["signature"] == signature and backup["createdAt"] and time() - backup["createdAt"] <= AUTO_BACKUP_RECENCY then + return backup, "reused" + end + end + + options["automatic"] = true + options["signature"] = signature + return F.CreateBackupSnapshot(desc, options), "created" +end + +function F.GetBackupNotificationText(backup, status) + if backup then + if status == "reused" then + return "Backup: " .. backup["desc"] .. " (reused)" + end + + return "Backup: " .. backup["desc"] + end + + if status == "disabled" then + return "Auto backup disabled" + end +end --------------------------------------------------------------------- -- create item @@ -65,7 +243,9 @@ local function CreateItem(index) local text = "|cFFFF7070"..L["Delete backup"].."?|r\n"..CellDBBackup[index]["desc"] local popup = Cell.CreateConfirmPopup(Cell.frames.aboutTab, 200, text, function() backupFrame:SetFrameLevel(Cell.frames.aboutTab:GetFrameLevel() + 50) + local desc = CellDBBackup[index]["desc"] tremove(CellDBBackup, index) + F.AddAddonNotification("backup", "Backup Deleted", desc) LoadBackups() end, function() backupFrame:SetFrameLevel(Cell.frames.aboutTab:GetFrameLevel() + 50) @@ -149,13 +329,11 @@ local function CreateBackupFrame() buttons[0]:SetScript("OnClick", function(self) local popup = Cell.CreatePopupEditBox(backupFrame, function(text) if strtrim(text) == "" then text = date(DATE_FORMAT) end - tinsert(CellDBBackup, { - ["desc"] = text, - ["version"] = Cell.version, - ["versionNum"] = Cell.versionNum, - ["DB"] = F.Copy(CellDB), - ["CharacterDB"] = CellCharacterDB and F.Copy(CellCharacterDB), + local backup = F.CreateBackupSnapshot(text, { + ["tag"] = "Manual", + ["type"] = "manual", }) + F.AddAddonNotification("backup", "Manual Backup Created", backup["desc"]) LoadBackups() end) popup:SetPoint("TOPLEFT", self) @@ -179,6 +357,85 @@ local function CreateBackupFrame() end) end +--------------------------------------------------------------------- +-- notifications +--------------------------------------------------------------------- +local function CreateNotificationItem(index) + local b = CreateFrame("Button", nil, notificationFrame.list.content, "BackdropTemplate") + Cell.StylizeFrame(b, {0.115, 0.115, 0.115, 0.9}, {0, 0, 0, 1}) + + b.title = b:CreateFontString(nil, "OVERLAY", "CELL_FONT_WIDGET") + b.title:SetPoint("TOPLEFT", 5, -5) + b.title:SetPoint("RIGHT", -80, 0) + b.title:SetJustifyH("LEFT") + b.title:SetWordWrap(false) + + b.time = b:CreateFontString(nil, "OVERLAY", "CELL_FONT_WIDGET_SMALL") + b.time:SetPoint("TOPRIGHT", -5, -5) + b.time:SetJustifyH("RIGHT") + + b.message = b:CreateFontString(nil, "OVERLAY", "CELL_FONT_WIDGET_SMALL") + b.message:SetPoint("TOPLEFT", b.title, "BOTTOMLEFT", 0, -3) + b.message:SetPoint("TOPRIGHT", -5, -18) + b.message:SetJustifyH("LEFT") + b.message:SetSpacing(2) + b.message:SetWordWrap(true) + + return b +end + +local function CreateNotificationFrame() + notificationFrame = CreateFrame("Frame", "CellOptionsFrame_Notifications", Cell.frames.aboutTab, "BackdropTemplate") + notificationFrame:Hide() + Cell.StylizeFrame(notificationFrame, nil, Cell.GetAccentColorTable()) + notificationFrame:EnableMouse(true) + notificationFrame:SetFrameLevel(Cell.frames.aboutTab:GetFrameLevel() + 50) + P.Size(notificationFrame, 430, 215) + notificationFrame:SetPoint("BOTTOMLEFT", P.Scale(1), 27) + + if not Cell.frames.aboutTab.mask then + Cell.CreateMask(Cell.frames.aboutTab, nil, {1, -1, -1, 1}) + Cell.frames.aboutTab.mask:Hide() + end + + local title = notificationFrame:CreateFontString(nil, "OVERLAY", "CELL_FONT_CLASS") + title:SetPoint("TOPLEFT", 5, -5) + title:SetText("Notifications") + + local clearBtn = Cell.CreateButton(notificationFrame, "Clear", "accent", {50, 18}) + clearBtn:SetPoint("TOPRIGHT", -28, -1) + clearBtn:SetScript("OnClick", function() + F.ClearAddonNotifications() + LoadNotifications() + end) + + local closeBtn = Cell.CreateButton(notificationFrame, "×", "red", {18, 18}, false, false, "CELL_FONT_SPECIAL", "CELL_FONT_SPECIAL") + closeBtn:SetPoint("TOPRIGHT", P.Scale(-5), P.Scale(-1)) + closeBtn:SetScript("OnClick", function() + notificationFrame:Hide() + end) + + local listFrame = Cell.CreateFrame(nil, notificationFrame) + listFrame:SetPoint("TOPLEFT", 5, -25) + listFrame:SetPoint("BOTTOMRIGHT", -5, 5) + listFrame:Show() + + Cell.CreateScrollFrame(listFrame) + notificationFrame.list = listFrame.scrollFrame + Cell.StylizeFrame(listFrame.scrollFrame, {0, 0, 0, 0}, Cell.GetAccentColorTable()) + listFrame.scrollFrame:SetScrollStep(44) + + notificationFrame:SetScript("OnHide", function() + notificationFrame:Hide() + Cell.frames.aboutTab.mask:Hide() + end) + + notificationFrame:SetScript("OnShow", function() + notificationFrame:SetFrameLevel(Cell.frames.aboutTab:GetFrameLevel() + 50) + Cell.frames.aboutTab.mask:Show() + end) +end + --------------------------------------------------------------------- -- load --------------------------------------------------------------------- @@ -186,7 +443,7 @@ function LoadBackups() backupFrame.list:ResetScroll() -- backups - for i, t in pairs(CellDBBackup) do + for i, t in ipairs(CellDBBackup) do if not buttons[i] then buttons[i] = CreateItem(i) @@ -202,10 +459,10 @@ function LoadBackups() buttons[i].version:SetText("|cffff2222"..L["Invalid"]) buttons[i].isInvalid = true else - buttons[i].version:SetText(t["version"]) + buttons[i].version:SetText(t["version"] .. (GetBackupCreatedText(t) ~= "" and " |cFF777777" .. GetBackupCreatedText(t) .. "|r" or "")) buttons[i].isInvalid = nil end - buttons[i].text:SetText(t["desc"]) + buttons[i].text:SetText(GetBackupDisplayText(t)) buttons[i]:Show() end @@ -229,6 +486,58 @@ function LoadBackups() backupFrame.list:SetContentHeight((n + 1) * P.Scale(20) + (n + 2) * P.Scale(5)) end +function LoadNotifications() + notificationFrame.list:ResetScroll() + + local notifications = F.GetAddonNotifications() + local shown = 0 + + for index = #notifications, 1, -1 do + local entry = notifications[index] + shown = shown + 1 + + if not notificationButtons[shown] then + notificationButtons[shown] = CreateNotificationItem(shown) + if shown == 1 then + notificationButtons[shown]:SetPoint("TOPLEFT", 5, -5) + else + notificationButtons[shown]:SetPoint("TOPLEFT", notificationButtons[shown-1], "BOTTOMLEFT", 0, -5) + end + notificationButtons[shown]:SetPoint("RIGHT", -5, 0) + P.Height(notificationButtons[shown], 40) + end + + local r, g, b = GetNotificationColor(entry["kind"]) + notificationButtons[shown].title:SetText(entry["title"] or "Notification") + notificationButtons[shown].title:SetTextColor(r, g, b) + notificationButtons[shown].time:SetText(entry["createdAt"] and date("%m-%d %H:%M", entry["createdAt"]) or "") + notificationButtons[shown].message:SetText(entry["message"] or "") + notificationButtons[shown]:Show() + end + + for i = shown + 1, #notificationButtons do + notificationButtons[i]:Hide() + end + + if shown == 0 then + if not notificationButtons[1] then + notificationButtons[1] = CreateNotificationItem(1) + notificationButtons[1]:SetPoint("TOPLEFT", 5, -5) + notificationButtons[1]:SetPoint("RIGHT", -5, 0) + P.Height(notificationButtons[1], 40) + end + + notificationButtons[1].title:SetText("No notifications yet") + notificationButtons[1].title:SetTextColor(0.7, 0.7, 0.7) + notificationButtons[1].time:SetText("") + notificationButtons[1].message:SetText("Imports, backups, and other important addon actions will show up here.") + notificationButtons[1]:Show() + shown = 1 + end + + notificationFrame.list:SetContentHeight(shown * P.Scale(40) + (shown + 1) * P.Scale(5)) +end + --------------------------------------------------------------------- -- show --------------------------------------------------------------------- @@ -239,4 +548,25 @@ function F.ShowBackupFrame() LoadBackups() backupFrame:Show() -end \ No newline at end of file +end + +function F.ShowNotificationCenter() + if not notificationFrame then + CreateNotificationFrame() + end + + LoadNotifications() + notificationFrame:Show() +end + +Cell.RegisterCallback("AddonNotificationsUpdated", "AboutNotifications_Reload", function() + if notificationFrame and notificationFrame:IsShown() then + LoadNotifications() + end +end) + +Cell.RegisterCallback("BackupsUpdated", "AboutBackups_Reload", function() + if backupFrame and backupFrame:IsShown() then + LoadBackups() + end +end) diff --git a/Modules/About/ImportExport.lua b/Modules/About/ImportExport.lua index a4cced90..8b7ceb79 100644 --- a/Modules/About/ImportExport.lua +++ b/Modules/About/ImportExport.lua @@ -13,11 +13,15 @@ local isImport, imported, exported = false, nil, "" local importExportFrame, importBtn, title, textArea, includeNicknamesCB, includeCharacterCB local confirmationFrame local ignoredIndices = {} +local pendingImportBackup, pendingImportBackupState --------------------------------------------------------------------- -- do import --------------------------------------------------------------------- local function DoImport(noReload) + imported["addonNotifications"] = nil + imported["systemTools"] = nil + -- raid debuffs for instanceID in pairs(imported["raidDebuffs"]) do if not Cell.snippetVars.loadedDebuffs[instanceID] then @@ -164,6 +168,15 @@ local function DoImport(noReload) CellDB[k] = v end + local backupText = F.GetBackupNotificationText(pendingImportBackup, pendingImportBackupState) + local message = "Selected Cell settings were imported." + if backupText then + message = message .. "\n" .. backupText + end + F.AddAddonNotification("import", "Profile Imported", message) + pendingImportBackup = nil + pendingImportBackupState = nil + if noReload then F.Print(L["Profile imported successfully."]) -- TODO: F.Print(L["Profile imported: %s."]) @@ -191,6 +204,8 @@ local function GetExportString(includeNicknames, includeCharacter) db["flavor"] = Cell.flavor db["fallbackGroupType"] = nil db["fallbackInMythic"] = nil + db["addonNotifications"] = nil + db["systemTools"] = nil local str = Serializer:Serialize(db) -- serialize str = LibDeflate:CompressDeflate(str, deflateConfig) -- compress @@ -225,6 +240,11 @@ local function CreateImportConfirmationFrame() button1:SetPoint("BOTTOMRIGHT", button2, "BOTTOMLEFT", P.Scale(1), 0) button1:SetBackdropBorderColor(Cell.GetAccentColorRGB()) button1:SetScript("OnClick", function() + pendingImportBackup, pendingImportBackupState = F.CreateAutoBackup("Auto backup before profile import", { + ["tag"] = "Profile", + ["type"] = "profile_import", + ["signature"] = "profile_import", + }) DoImport() confirmationFrame:Hide() importExportFrame:Hide() @@ -426,6 +446,8 @@ local function CreateImportExportFrame() success, data = Serializer:Deserialize(data) -- deserialize if success and data then + data["addonNotifications"] = nil + data["systemTools"] = nil title:SetText(L["Import"]..": r"..version) importBtn:SetEnabled(true) imported = data diff --git a/Modules/ClickCastings/ImportExport.lua b/Modules/ClickCastings/ImportExport.lua index d92f554d..2a41d05f 100644 --- a/Modules/ClickCastings/ImportExport.lua +++ b/Modules/ClickCastings/ImportExport.lua @@ -12,6 +12,12 @@ local isImport, imported, exported = false, {}, "" local importExportFrame, importBtn, title, textArea local function DoImport() + local backup, backupState = F.CreateAutoBackup("Auto backup before click-casting import", { + ["tag"] = "Click-Castings", + ["type"] = "clickcast_import", + ["signature"] = "clickcast_import:" .. tostring(Cell.vars.playerClass), + }) + if Cell.vars.clickCastings["useCommon"] then Cell.vars.clickCastings["common"] = imported else @@ -19,6 +25,12 @@ local function DoImport() end Cell.Fire("UpdateClickCastings") + local backupText = F.GetBackupNotificationText(backup, backupState) + local message = F.GetLocalizedClassName(Cell.vars.playerClass) + if backupText then + message = message .. "\n" .. backupText + end + F.AddAddonNotification("import", "Click-Castings Imported", message) importExportFrame:Hide() end @@ -167,4 +179,4 @@ function F.ShowClickCastingExportFrame(clickCastingTable) textArea:SetText(exported) textArea.eb:SetFocus(true) -end \ No newline at end of file +end diff --git a/Modules/Indicators/Import.lua b/Modules/Indicators/Import.lua index 3b7bef03..4a527a87 100644 --- a/Modules/Indicators/Import.lua +++ b/Modules/Indicators/Import.lua @@ -52,6 +52,11 @@ local function CreateIndicatorsImportFrame() ..L["|cff1Aff1AYes|r - Overwrite"].."\n|cffff1A1A"..L["No"].."|r - "..L["Cancel"] local popup = Cell.CreateConfirmPopup(Cell.frames.indicatorsTab, 250, text, function(self) + local backup, backupState = F.CreateAutoBackup("Auto backup before indicators import: " .. toLayoutName, { + ["tag"] = "Indicators", + ["type"] = "indicators_import", + ["signature"] = "indicators_import:" .. tostring(toLayout), + }) local toLayoutTable = CellDB["layouts"][toLayout] -- last custom index local lastIndex @@ -121,6 +126,12 @@ local function CreateIndicatorsImportFrame() Cell.Fire("UpdateIndicators", toLayout) Cell.Fire("IndicatorsChanged", toLayout) + local backupText = F.GetBackupNotificationText(backup, backupState) + local message = toLayoutName + if backupText then + message = message .. "\n" .. backupText + end + F.AddAddonNotification("import", "Indicators Imported", message) importFrame:Hide() end, function(self) importFrame:Hide() @@ -273,4 +284,4 @@ function F.ShowIndicatorsImportFrame(layout) toLayout = layout toLayoutName = toLayout == "default" and _G.DEFAULT or toLayout title:SetText(L["Import"].." > "..toLayoutName) -end \ No newline at end of file +end diff --git a/Modules/Indicators/Indicators.lua b/Modules/Indicators/Indicators.lua index 1d4b2e34..81421958 100644 --- a/Modules/Indicators/Indicators.lua +++ b/Modules/Indicators/Indicators.lua @@ -20,6 +20,7 @@ local selected, currentLayout, currentLayoutTable local LoadIndicatorList local listButtons = {} local ListHighlightFn +local currentLayoutIssueReport ------------------------------------------------- -- preview @@ -1481,6 +1482,45 @@ local auraTypeItems = { }, } +local listCompatibilityText + +local function UpdateIndicatorButtonVisual(id) + local button = listButtons[id] + local indicator = currentLayoutTable and currentLayoutTable["indicators"] and currentLayoutTable["indicators"][id] + if not button or not indicator then return end + + local issue = currentLayoutIssueReport and currentLayoutIssueReport.indicatorIssuesByIndex and currentLayoutIssueReport.indicatorIssuesByIndex[id] + button.compatibilityIssue = issue + + if issue then + if indicator["enabled"] then + button:SetTextColor(1, 0.45, 0.45, 1) + button.typeIcon:SetAlpha(0.85) + else + button:SetTextColor(0.65, 0.3, 0.3, 1) + button.typeIcon:SetAlpha(0.3) + end + else + if indicator["enabled"] then + button:SetTextColor(1, 1, 1, 1) + button.typeIcon:SetAlpha(0.55) + else + button:SetTextColor(0.466, 0.466, 0.466, 1) + button.typeIcon:SetAlpha(0.15) + end + end +end + +local function UpdateListCompatibilityText() + if not listCompatibilityText then return end + + if currentLayoutIssueReport and currentLayoutIssueReport.issueCount > 0 then + listCompatibilityText:SetText("|cffff6b6bCompatibility issues detected in this layout.|r\n|cffb7b7b7Hover red entries or open About > Compatibility.|r") + else + listCompatibilityText:SetText("") + end +end + local function CreateListPane() local listPane = Cell.CreateTitledPane(indicatorsTab, L["Indicators"], 136, 487) listPane:SetPoint("TOPLEFT", 5, -115) @@ -1588,6 +1628,12 @@ local function CreateListPane() F.ShowIndicatorsCopyFrame() end) Cell.RegisterForCloseDropdown(copyBtn) + + listCompatibilityText = listPane:CreateFontString(nil, "OVERLAY", "CELL_FONT_WIDGET_SMALL") + listCompatibilityText:SetPoint("BOTTOMLEFT", 2, 1) + listCompatibilityText:SetPoint("BOTTOMRIGHT", -2, 1) + listCompatibilityText:SetJustifyH("LEFT") + listCompatibilityText:SetSpacing(2) end ------------------------------------------------- @@ -1844,15 +1890,7 @@ local function ShowIndicatorSettings(id) w:SetFunc(function(value) indicatorTable[currentSetting] = value Cell.Fire("UpdateIndicators", notifiedLayout, indicatorName, currentSetting, value) - -- show enabled/disabled status - if value then - listButtons[id]:SetTextColor(1, 1, 1, 1) - else - listButtons[id]:SetTextColor(0.466, 0.466, 0.466, 1) - end - if listButtons[id].typeIcon then - listButtons[id].typeIcon:SetAlpha(value and 0.55 or 0.15) - end + UpdateIndicatorButtonVisual(id) end) -- checkbutton @@ -2186,6 +2224,7 @@ end LoadIndicatorList = function() F.Debug("|cffff7777LoadIndicatorList:|r", currentLayout) listFrame.scrollFrame:Reset() + currentLayoutIssueReport = F.GetCompatibilityLayoutIssues and F.GetCompatibilityLayoutIssues(currentLayout) local n = 0 for i, t in pairs(currentLayoutTable["indicators"]) do @@ -2196,7 +2235,15 @@ LoadIndicatorList = function() P.Size(listButtons[i].typeIcon, 16, 16) listButtons[i].ShowTooltip = function() - if listButtons[i]:GetFontString():IsTruncated() then + if listButtons[i].compatibilityIssue then + CellTooltip:SetOwner(listButtons[i], "ANCHOR_NONE") + CellTooltip:SetPoint("RIGHT", listButtons[i], "LEFT") + CellTooltip:AddLine(listButtons[i]:GetText()) + for _, line in ipairs(listButtons[i].compatibilityIssue.lines) do + CellTooltip:AddLine(line, 1, 0.45, 0.45, true) + end + CellTooltip:Show() + elseif listButtons[i]:GetFontString():IsTruncated() then CellTooltip:SetOwner(listButtons[i], "ANCHOR_NONE") CellTooltip:SetPoint("RIGHT", listButtons[i], "LEFT") CellTooltip:AddLine(listButtons[i]:GetText()) @@ -2263,14 +2310,7 @@ LoadIndicatorList = function() b.id = i n = i - -- show enabled/disabled status - if t["enabled"] then - b:SetTextColor(1, 1, 1, 1) - b.typeIcon:SetAlpha(0.55) - else - b:SetTextColor(0.466, 0.466, 0.466, 1) - b.typeIcon:SetAlpha(0.15) - end + UpdateIndicatorButtonVisual(i) b:SetParent(listFrame.scrollFrame.content) b:SetPoint("RIGHT") @@ -2281,6 +2321,8 @@ LoadIndicatorList = function() end b:Show() end + UpdateListCompatibilityText() + Cell.Fire("UpdateCompatibilityReport") listFrame.scrollFrame:SetContentHeight(P.Scale(20), n, -P.Scale(1)) ListHighlightFn = Cell.CreateButtonGroup(listButtons, ShowIndicatorSettings, function(id) diff --git a/Modules/Layouts/ImportExport.lua b/Modules/Layouts/ImportExport.lua index 3137171a..200c311d 100644 --- a/Modules/Layouts/ImportExport.lua +++ b/Modules/Layouts/ImportExport.lua @@ -13,6 +13,11 @@ local importExportFrame, importBtn, title, textArea local function DoImport(overwriteExisting) local name, layout = imported["name"], imported["data"] + local backup, backupState = F.CreateAutoBackup("Auto backup before layout import: " .. name, { + ["tag"] = "Layout", + ["type"] = "layout_import", + ["signature"] = "layout_import:" .. name, + }) -- indicators local builtInFound = {} @@ -71,6 +76,13 @@ local function DoImport(overwriteExisting) importExportFrame:Hide() end end + + local backupText = F.GetBackupNotificationText(backup, backupState) + local message = name + if backupText then + message = message .. "\n" .. backupText + end + F.AddAddonNotification("import", "Layout Imported", message) F.Print(L["Layout imported: %s."]:format(name)) end diff --git a/Modules/OptionsFrame.lua b/Modules/OptionsFrame.lua index 571838c8..3ac2a281 100644 --- a/Modules/OptionsFrame.lua +++ b/Modules/OptionsFrame.lua @@ -3,7 +3,7 @@ local L = Cell.L local F = Cell.funcs local P = Cell.pixelPerfectFuncs -local lastShownTab +local lastShownTab, init local optionsFrame = Cell.CreateFrame("CellOptionsFrame", Cell.frames.mainFrame, 432, 401) Cell.frames.optionsFrame = optionsFrame @@ -32,6 +32,58 @@ end ------------------------------------------------- local generalBtn, appearanceBtn, clickCastingsBtn, aboutBtn, layoutsBtn, indicatorsBtn, debuffsBtn, utilitiesBtn, closeBtn +local function CreateWarningBadge(button) + local badge = button:CreateFontString(nil, "OVERLAY", "CELL_FONT_WIDGET_TITLE") + badge:SetPoint("TOPRIGHT", -3, -1) + badge:SetText("|cffff5555!|r") + badge:Hide() + button.warningBadge = badge +end + +local function SetWarningState(button, enabled, tooltipText) + if not button or not button.warningBadge then return end + + if enabled then + button.warningBadge:Show() + button.ShowTooltip = function(self) + CellTooltip:SetOwner(self, "ANCHOR_NONE") + CellTooltip:SetPoint("BOTTOM", self, "TOP", 0, 3) + CellTooltip:AddLine("|cffff5555Compatibility warning|r") + if tooltipText and tooltipText ~= "" then + CellTooltip:AddLine(tooltipText, 1, 1, 1, true) + end + CellTooltip:Show() + end + button.HideTooltip = function() + CellTooltip:Hide() + end + else + button.warningBadge:Hide() + button.ShowTooltip = nil + button.HideTooltip = nil + end +end + +function F.UpdateCompatibilityTabWarnings() + if not init then return end + + local report = F.GetCompatibilityReport and F.GetCompatibilityReport() + if not report then return end + + local aboutTooltip + if report.hasWarnings then + aboutTooltip = "Open About > Compatibility to review old or unsupported settings." + end + + local indicatorsTooltip + if report.layoutIssueCount > 0 then + indicatorsTooltip = "Some indicator layouts still reference unsupported data, invalid spell IDs, or missing built-ins." + end + + SetWarningState(aboutBtn, report.hasWarnings, aboutTooltip) + SetWarningState(indicatorsBtn, report.layoutIssueCount > 0, indicatorsTooltip) +end + local function CreateTabButtons() generalBtn = Cell.CreateButton(optionsFrame, L["General"], "accent-hover", {105, 20}, false, false, "CELL_FONT_WIDGET_TITLE", "CELL_FONT_WIDGET_TITLE_DISABLE") appearanceBtn = Cell.CreateButton(optionsFrame, L["Appearance"], "accent-hover", {105, 20}, false, false, "CELL_FONT_WIDGET_TITLE", "CELL_FONT_WIDGET_TITLE_DISABLE") @@ -60,6 +112,9 @@ local function CreateTabButtons() closeBtn:SetPoint("BOTTOMLEFT", aboutBtn, "BOTTOMRIGHT", P.Scale(-1), 0) closeBtn:SetPoint("BOTTOMRIGHT", utilitiesBtn, "TOPRIGHT", 0, P.Scale(-1)) + CreateWarningBadge(indicatorsBtn) + CreateWarningBadge(aboutBtn) + RegisterDragForOptionsFrame(generalBtn) RegisterDragForOptionsFrame(appearanceBtn) RegisterDragForOptionsFrame(layoutsBtn) @@ -127,7 +182,6 @@ end ------------------------------------------------- -- show & hide ------------------------------------------------- -local init local function Init() if not init then init = true @@ -135,6 +189,7 @@ local function Init() P.Reborder(optionsFrame, true) CreateTabButtons() F.CreateUtilityList(utilitiesBtn) + F.UpdateCompatibilityTabWarnings() end end @@ -150,6 +205,7 @@ function F.ShowOptionsFrame() generalBtn:Click() end + F.UpdateCompatibilityTabWarnings() optionsFrame:Show() end @@ -192,6 +248,10 @@ function F.ShowUtilitiesTab() utilitiesBtn:Click() end +Cell.RegisterCallback("UpdateCompatibilityReport", "OptionsFrame_UpdateCompatibilityReport", function() + F.UpdateCompatibilityTabWarnings() +end) + ------------------------------------------------- -- InCombatLockdown ------------------------------------------------- @@ -246,4 +306,4 @@ end) local function UpdatePixelPerfect() P.Resize(optionsFrame) end -Cell.RegisterCallback("UpdatePixelPerfect", "OptionsFrame_UpdatePixelPerfect", UpdatePixelPerfect) \ No newline at end of file +Cell.RegisterCallback("UpdatePixelPerfect", "OptionsFrame_UpdatePixelPerfect", UpdatePixelPerfect) diff --git a/Modules/RaidDebuffs/ImportExport.lua b/Modules/RaidDebuffs/ImportExport.lua index aabfd1f1..e5e5d26d 100644 --- a/Modules/RaidDebuffs/ImportExport.lua +++ b/Modules/RaidDebuffs/ImportExport.lua @@ -79,8 +79,19 @@ local function CreateDebuffsImportExportFrame() else which = instanceName end + local backup, backupState = F.CreateAutoBackup("Auto backup before Raid Debuffs import: " .. which, { + ["tag"] = "Raid Debuffs", + ["type"] = "raiddebuffs_import", + ["signature"] = "raiddebuffs_import:" .. tostring(imported["instanceId"]) .. ":" .. tostring(imported["bossId"] or "all"), + }) F.UpdateRaidDebuffs(imported["instanceId"], imported["bossId"], imported["data"], which) F.ShowInstanceDebuffs(imported["instanceId"], imported["bossId"]) + local backupText = F.GetBackupNotificationText(backup, backupState) + local message = which + if backupText then + message = message .. "\n" .. backupText + end + F.AddAddonNotification("import", "Raid Debuffs Imported", message) importExportFrame:Hide() end, function(self) importExportFrame:Hide() @@ -262,4 +273,4 @@ function F.ShowRaidDebuffsExportFrame(instanceId, bossId) boss:SetText(L["Boss Name"]..": |cffffffff"..bossName) ShowData(instanceId, bossId) -end \ No newline at end of file +end diff --git a/Modules/RaidDebuffs/RaidDebuffs.lua b/Modules/RaidDebuffs/RaidDebuffs.lua index 4b9b6932..252f5d71 100644 --- a/Modules/RaidDebuffs/RaidDebuffs.lua +++ b/Modules/RaidDebuffs/RaidDebuffs.lua @@ -21,6 +21,7 @@ local instancesFrame, bossesFrame, debuffListFrame, detailsFrame local LoadExpansion, ShowInstances, ShowBosses, ShowDebuffs, ShowDetails, ShowInstanceImage, HideInstanceImage, ShowBossImage, HideBossImage, OpenEncounterJournal -- buttons local instanceButtons, bossButtons, debuffButtons = {}, {}, {} +local curationReportFrame, curationReportTitle, curationReportContext, curationReportTextArea ------------------------------------------------- -- prepare debuff list ------------------------------------------------- @@ -200,6 +201,185 @@ Cell.snippetVars.loadedDebuffs = loadedDebuffs local indices = {"order", "trackByID", "condition", "glowType", "glowOptions", "glowCondition", "glowTarget", "useElapsedTime"} +local curationStatusInfo = { + ["review"] = { + ["text"] = "Needs Review", + ["tag"] = "REV", + ["color"] = {1, 0.82, 0}, + }, + ["confirmed"] = { + ["text"] = "Confirmed", + ["tag"] = "OK", + ["color"] = {0.5, 1, 0}, + }, + ["trash"] = { + ["text"] = "Trash Mob", + ["tag"] = "TR", + ["color"] = {0, 0.8, 1}, + }, + ["non_debuff"] = { + ["text"] = "Non-Debuff", + ["tag"] = "ND", + ["color"] = {1, 0.3, 0.3}, + }, + ["ignore"] = { + ["text"] = "Ignore", + ["tag"] = "IG", + ["color"] = {0.7, 0.7, 0.7}, + }, +} + +local function EnsureRaidDebuffsCurationDB() + CellDB["raidDebuffsCuration"] = CellDB["raidDebuffsCuration"] or {} + return CellDB["raidDebuffsCuration"] +end + +local function NormalizeCurationBossId(instanceId, bossId) + if bossId == nil or bossId == instanceId or bossId == "general" then + return "general" + end + + return bossId +end + +local function GetRaidDebuffCurationEntry(instanceId, bossId, spellId, create) + local db = EnsureRaidDebuffsCurationDB() + local bossKey = NormalizeCurationBossId(instanceId, bossId) + + if create then + db[instanceId] = db[instanceId] or {} + db[instanceId][bossKey] = db[instanceId][bossKey] or {} + db[instanceId][bossKey][spellId] = db[instanceId][bossKey][spellId] or {} + return db[instanceId][bossKey][spellId] + end + + return db[instanceId] and db[instanceId][bossKey] and db[instanceId][bossKey][spellId] +end + +local function GetRaidDebuffCurationData(instanceId, bossId, spellId) + local entry = GetRaidDebuffCurationEntry(instanceId, bossId, spellId) + + return { + ["status"] = entry and entry["status"] or "review", + ["suggestedOrder"] = entry and entry["suggestedOrder"] or nil, + ["note"] = entry and entry["note"] or nil, + ["explicit"] = entry ~= nil, + } +end + +local function CleanupRaidDebuffCuration(instanceId, bossId, spellId) + local db = EnsureRaidDebuffsCurationDB() + local bossKey = NormalizeCurationBossId(instanceId, bossId) + local entry = db[instanceId] and db[instanceId][bossKey] and db[instanceId][bossKey][spellId] + if not entry then return end + + if (not entry["status"] or entry["status"] == "review") and not entry["suggestedOrder"] and not entry["note"] then + db[instanceId][bossKey][spellId] = nil + + if not next(db[instanceId][bossKey]) then + db[instanceId][bossKey] = nil + end + + if not next(db[instanceId]) then + db[instanceId] = nil + end + end +end + +local function SetRaidDebuffCurationField(instanceId, bossId, spellId, key, value) + local shouldCreate = value ~= nil and value ~= "" and not (key == "status" and value == "review") + local entry = GetRaidDebuffCurationEntry(instanceId, bossId, spellId, shouldCreate) + + if not entry then return GetRaidDebuffCurationData(instanceId, bossId, spellId) end + + if key == "note" then + value = strtrim(tostring(value or "")) + entry[key] = value ~= "" and value or nil + elseif key == "suggestedOrder" then + value = tonumber(value) + entry[key] = value and value > 0 and floor(value) or nil + elseif key == "status" then + entry[key] = value ~= "review" and value or nil + else + entry[key] = value + end + + CleanupRaidDebuffCuration(instanceId, bossId, spellId) + return GetRaidDebuffCurationData(instanceId, bossId, spellId) +end + +local function GetRaidDebuffCurationStatusInfo(status) + return curationStatusInfo[status] or curationStatusInfo["review"] +end + +local function GetSelectedBossKey() + return isGeneral and "general" or loadedBoss +end + +local function BuildRaidDebuffCurationReport(instanceId, bossId) + local bossKey = NormalizeCurationBossId(instanceId, bossId) + local bossTable = loadedDebuffs[instanceId] and loadedDebuffs[instanceId][bossKey] + local instanceName = instanceIdToName[instanceId] or tostring(instanceId) + local bossName = bossKey == "general" and bossIdToName[0] or (bossIdToName[bossKey] or tostring(bossKey)) + + if not bossTable then + return ("Raid Debuff Curation Report\n\nInstance: %s\nBoss: %s\n\nNo debuffs loaded for this context."):format(instanceName, bossName) + end + + local lines = { + "Raid Debuff Curation Report", + "", + "Instance: " .. instanceName, + "Boss: " .. bossName, + "", + } + local counts = { + ["review"] = 0, + ["confirmed"] = 0, + ["trash"] = 0, + ["non_debuff"] = 0, + ["ignore"] = 0, + } + + local function AddSpellLine(spellId, order) + local data = GetRaidDebuffCurationData(instanceId, bossKey, spellId) + local info = GetRaidDebuffCurationStatusInfo(data["status"]) + local spellName = F.GetSpellInfo(spellId) or tostring(spellId) + counts[data["status"]] = (counts[data["status"]] or 0) + 1 + + tinsert(lines, ("[%s] %s (%s)"):format(info["text"], spellName, spellId)) + tinsert(lines, ("Current order: %s"):format(order > 0 and order or "disabled")) + + if data["suggestedOrder"] then + tinsert(lines, ("Suggested order: %d"):format(data["suggestedOrder"])) + end + + if data["note"] then + tinsert(lines, "Note: " .. data["note"]) + end + + tinsert(lines, "") + end + + for _, spell in ipairs(bossTable["enabled"] or {}) do + AddSpellLine(spell["id"], spell["order"] or 0) + end + + for _, spell in ipairs(bossTable["disabled"] or {}) do + AddSpellLine(spell["id"], 0) + end + + table.insert(lines, 6, ("Review: %d Confirmed: %d Trash: %d Non-Debuff: %d Ignore: %d"):format( + counts["review"], + counts["confirmed"], + counts["trash"], + counts["non_debuff"], + counts["ignore"] + )) + + return table.concat(lines, "\n") +end + local function LoadDB(instanceId, bossId, bossTable) if not loadedDebuffs[instanceId][bossId] then loadedDebuffs[instanceId][bossId] = {["enabled"]={}, ["disabled"]={}} end -- load from db and set its order @@ -346,6 +526,66 @@ Cell.RegisterCallback("UpdateRaidDebuffs", "RaidDebuffsTab_UpdateRaidDebuffs", U ------------------------------------------------- local expansionDropdown, showCurrentBtn +local function CreateCurationReportFrame() + curationReportFrame = CreateFrame("Frame", "CellOptionsFrame_RaidDebuffsCurationReport", Cell.frames.raidDebuffsTab, "BackdropTemplate") + curationReportFrame:Hide() + Cell.StylizeFrame(curationReportFrame, nil, Cell.GetAccentColorTable()) + curationReportFrame:EnableMouse(true) + curationReportFrame:SetFrameLevel(Cell.frames.raidDebuffsTab:GetFrameLevel() + 50) + P.Size(curationReportFrame, 430, 250) + curationReportFrame:SetPoint("TOPLEFT", P.Scale(1), -100) + + if not Cell.frames.raidDebuffsTab.mask then + Cell.CreateMask(Cell.frames.raidDebuffsTab, nil, {1, -1, -1, 1}) + Cell.frames.raidDebuffsTab.mask:Hide() + end + + curationReportFrame:SetScript("OnHide", function() + if Cell.frames.raidDebuffsTab.mask then + Cell.frames.raidDebuffsTab.mask:Hide() + end + end) + + local closeBtn = Cell.CreateButton(curationReportFrame, "×", "red", {18, 18}, false, false, "CELL_FONT_SPECIAL", "CELL_FONT_SPECIAL") + closeBtn:SetPoint("TOPRIGHT", -5, -1) + closeBtn:SetScript("OnClick", function() + curationReportFrame:Hide() + end) + + curationReportTitle = curationReportFrame:CreateFontString(nil, "OVERLAY", "CELL_FONT_CLASS") + curationReportTitle:SetPoint("TOPLEFT", 5, -5) + curationReportTitle:SetText("Raid Debuff Curation") + + curationReportContext = curationReportFrame:CreateFontString(nil, "OVERLAY", "CELL_FONT_WIDGET") + curationReportContext:SetPoint("TOPLEFT", curationReportTitle, "BOTTOMLEFT", 0, -5) + curationReportContext:SetPoint("TOPRIGHT", closeBtn, "TOPLEFT", -5, 0) + curationReportContext:SetJustifyH("LEFT") + + curationReportTextArea = Cell.CreateScrollEditBox(curationReportFrame) + curationReportTextArea:SetPoint("TOPLEFT", 5, -45) + curationReportTextArea:SetPoint("BOTTOMRIGHT", -10, 5) + curationReportTextArea.eb:SetAutoFocus(false) +end + +local function ShowRaidDebuffsCurationReport(instanceId, bossId) + if not instanceId then return end + + if not curationReportFrame then + CreateCurationReportFrame() + end + + local bossKey = NormalizeCurationBossId(instanceId, bossId) + local instanceName = instanceIdToName[instanceId] or tostring(instanceId) + local bossName = bossKey == "general" and bossIdToName[0] or (bossIdToName[bossKey] or tostring(bossKey)) + + Cell.frames.raidDebuffsTab.mask:Show() + curationReportContext:SetText(("Instance: %s\nBoss: %s"):format(instanceName, bossName)) + curationReportTextArea.eb:SetText(BuildRaidDebuffCurationReport(instanceId, bossKey)) + curationReportTextArea.eb:ClearFocus() + curationReportTextArea.scrollFrame:ResetScroll() + curationReportFrame:Show() +end + local function OpenInstanceBoss(instanceName, bossName) if not instanceName or not instanceNameMapping[instanceName] then return end @@ -438,8 +678,8 @@ local function CreateWidgets() CellTooltip:SetOwner(helpBtn, "ANCHOR_NONE") CellTooltip:SetPoint("TOPLEFT", helpBtn, "TOPRIGHT", 6, 0) CellTooltip:AddLine(L["Want to help improve Raid Debuffs?"]) - CellTooltip:AddLine("|cffffffff"..L["Use %s addon"]:format("|cffff3030Instance Spell Collector|r")) - CellTooltip:AddLine("|cffffffff"..L["Then create a PR or submit a ticket on GitHub"]) + CellTooltip:AddLine("|cffffffffUse the curation fields to mark confirmed, trash, or non-debuff spells.") + CellTooltip:AddLine("|cffffffffOpen the curation report to review notes and suggested priorities for the current boss.") CellTooltip:Show() end) helpBtn:HookScript("OnLeave", function() @@ -1082,6 +1322,35 @@ local function UnregisterForDrag(b) b:SetScript("OnDragStop", nil) end +local function ApplyDebuffButtonCuration(button, sTable) + local curation = GetRaidDebuffCurationData(loadedInstance, GetSelectedBossKey(), sTable["id"]) + local info = GetRaidDebuffCurationStatusInfo(curation["status"]) + local r, g, b = unpack(info["color"]) + + button.curationData = curation + + if curation["explicit"] then + button.curationTag:SetText(info["tag"]) + button.curationTag:SetTextColor(r, g, b) + else + button.curationTag:SetText("") + end + + if sTable["order"] == 0 then + button:SetTextColor(0.4, 0.4, 0.4) + UnregisterForDrag(button) + button.enabled = nil + else + if curation["explicit"] and curation["status"] ~= "review" then + button:SetTextColor(r, g, b) + else + button:SetTextColor(1, 1, 1) + end + RegisterForDrag(button) + button.enabled = true + end +end + local last local function CreateDebuffButton(i, sTable) if not debuffButtons[i] then @@ -1095,7 +1364,9 @@ local function CreateDebuffButton(i, sTable) -- update text position debuffButtons[i]:GetFontString():ClearAllPoints() debuffButtons[i]:GetFontString():SetPoint("LEFT", debuffButtons[i].icon, "RIGHT", 2, 0) - debuffButtons[i]:GetFontString():SetPoint("RIGHT", -2, 0) + debuffButtons[i].curationTag = debuffButtons[i]:CreateFontString(nil, "OVERLAY", "CELL_FONT_WIDGET_SMALL") + debuffButtons[i].curationTag:SetPoint("RIGHT", -2, 0) + debuffButtons[i]:GetFontString():SetPoint("RIGHT", debuffButtons[i].curationTag, "LEFT", -2, 0) end debuffButtons[i]:Show() @@ -1113,15 +1384,7 @@ local function CreateDebuffButton(i, sTable) debuffButtons[i].spellId = sTable["id"] debuffButtons[i].spellTex = icon - if sTable["order"] == 0 then - debuffButtons[i]:SetTextColor(0.4, 0.4, 0.4) - UnregisterForDrag(debuffButtons[i]) - debuffButtons[i].enabled = nil - else - debuffButtons[i]:SetTextColor(1, 1, 1) - RegisterForDrag(debuffButtons[i]) - debuffButtons[i].enabled = true - end + ApplyDebuffButtonCuration(debuffButtons[i], sTable) debuffButtons[i].id = sTable["id"].."-"..i -- send spellId-buttonIndex to ShowDetails @@ -1194,6 +1457,19 @@ ShowDebuffs = function(bossId, buttonIndex) CellSpellTooltip:SetOwner(b, "ANCHOR_NONE") CellSpellTooltip:SetPoint("TOPRIGHT", b, "TOPLEFT", -1, 0) CellSpellTooltip:SetSpellByID(b.spellId, b.spellTex) + local curation = GetRaidDebuffCurationData(loadedInstance, GetSelectedBossKey(), b.spellId) + local info = GetRaidDebuffCurationStatusInfo(curation["status"]) + CellSpellTooltip:AddLine(" ") + CellSpellTooltip:AddLine("|cFFB2B2B2Midnight Curation|r") + CellSpellTooltip:AddLine(("Status: |cFFFFFFFF%s|r"):format(info["text"])) + if curation["suggestedOrder"] then + CellSpellTooltip:AddLine(("Suggested order: |cFFFFFFFF%d|r"):format(curation["suggestedOrder"])) + end + if curation["note"] then + CellSpellTooltip:AddLine("Note: |cFFFFFFFF" .. curation["note"] .. "|r") + elseif not curation["explicit"] then + CellSpellTooltip:AddLine("Note: |cFFB2B2B2No curation note yet|r") + end CellSpellTooltip:Show() end, function(b) debuffListFrame:GetScript("OnLeave")() @@ -1310,6 +1586,7 @@ end -- debuff details frame ------------------------------------------------- local spellIcon, spellNameText, spellIdText, enabledCB, trackByIdCB, useElapsedTimeCB +local curationStatusDropdown, curationSuggestedOrder, curationCurrentOrderText, curationNoteEditBox local conditionDropDown, conditionFrame, conditionOperator, conditionValue local glowTypeText, glowTypeDropdown, glowTargetDropdown, glowOptionsFrame, glowConditionType, glowConditionOperator, glowConditionValue, glowColor, glowLines, glowParticles, glowDuration, glowFrequency, glowLength, glowThickness, glowScale @@ -1317,6 +1594,12 @@ local LoadCondition, UpdateCondition local UpdateGlowType, LoadGlowOptions, LoadGlowCondition, ShowGlowPreview local conditionHeight, glowOptionsHeight, glowConditionHeight = 0, 0, 0 +local curationHeight = 80 + +local function UpdateDetailsHeight() + detailsFrame.scrollFrame:SetContentHeight(225 + curationHeight + glowOptionsHeight + glowConditionHeight + conditionHeight) + detailsFrame.scrollFrame:ResetScroll() +end local function CreateDetailsFrame() detailsFrame = Cell.CreateFrame("RaidDebuffsTab_DebuffDetails", debuffsTab) @@ -1481,12 +1764,104 @@ local function CreateDetailsFrame() end, L["Use Elapsed Time"], L["Display elapsed time since debuff applied"], L["Only affects duration text"]) useElapsedTimeCB:SetPoint("TOPLEFT", trackByIdCB, "BOTTOMLEFT", 0, -10) + -------------------------------------------------- + -- midnight curation + -------------------------------------------------- + local curationText = detailsContentFrame:CreateFontString(nil, "OVERLAY", "CELL_FONT_WIDGET") + curationText:SetText("Midnight Curation") + curationText:SetPoint("TOPLEFT", useElapsedTimeCB, "BOTTOMLEFT", 0, -10) + + curationStatusDropdown = Cell.CreateDropdown(detailsContentFrame, 117) + curationStatusDropdown:SetPoint("TOPLEFT", curationText, "BOTTOMLEFT", 0, -1) + curationStatusDropdown:SetItems({ + { + ["text"] = "Needs Review", + ["value"] = "review", + ["onClick"] = function() + SetRaidDebuffCurationField(loadedInstance, GetSelectedBossKey(), selectedSpellId, "status", "review") + ApplyDebuffButtonCuration(debuffButtons[selectedButtonIndex], selectedButtonIndex <= #currentBossTable["enabled"] and currentBossTable["enabled"][selectedButtonIndex] or currentBossTable["disabled"][selectedButtonIndex-#currentBossTable["enabled"]]) + end, + }, + { + ["text"] = "Confirmed", + ["value"] = "confirmed", + ["onClick"] = function() + SetRaidDebuffCurationField(loadedInstance, GetSelectedBossKey(), selectedSpellId, "status", "confirmed") + ApplyDebuffButtonCuration(debuffButtons[selectedButtonIndex], selectedButtonIndex <= #currentBossTable["enabled"] and currentBossTable["enabled"][selectedButtonIndex] or currentBossTable["disabled"][selectedButtonIndex-#currentBossTable["enabled"]]) + end, + }, + { + ["text"] = "Trash Mob", + ["value"] = "trash", + ["onClick"] = function() + SetRaidDebuffCurationField(loadedInstance, GetSelectedBossKey(), selectedSpellId, "status", "trash") + ApplyDebuffButtonCuration(debuffButtons[selectedButtonIndex], selectedButtonIndex <= #currentBossTable["enabled"] and currentBossTable["enabled"][selectedButtonIndex] or currentBossTable["disabled"][selectedButtonIndex-#currentBossTable["enabled"]]) + end, + }, + { + ["text"] = "Non-Debuff", + ["value"] = "non_debuff", + ["onClick"] = function() + SetRaidDebuffCurationField(loadedInstance, GetSelectedBossKey(), selectedSpellId, "status", "non_debuff") + ApplyDebuffButtonCuration(debuffButtons[selectedButtonIndex], selectedButtonIndex <= #currentBossTable["enabled"] and currentBossTable["enabled"][selectedButtonIndex] or currentBossTable["disabled"][selectedButtonIndex-#currentBossTable["enabled"]]) + end, + }, + { + ["text"] = "Ignore", + ["value"] = "ignore", + ["onClick"] = function() + SetRaidDebuffCurationField(loadedInstance, GetSelectedBossKey(), selectedSpellId, "status", "ignore") + ApplyDebuffButtonCuration(debuffButtons[selectedButtonIndex], selectedButtonIndex <= #currentBossTable["enabled"] and currentBossTable["enabled"][selectedButtonIndex] or currentBossTable["disabled"][selectedButtonIndex-#currentBossTable["enabled"]]) + end, + }, + }) + + local curationReportBtn = Cell.CreateButton(detailsContentFrame, "Report", "accent", {54, 20}, nil, nil, "CELL_FONT_WIDGET_SMALL", "CELL_FONT_WIDGET_SMALL") + curationReportBtn:SetPoint("LEFT", curationStatusDropdown, "RIGHT", 6, 0) + curationReportBtn:SetScript("OnClick", function() + ShowRaidDebuffsCurationReport(loadedInstance, GetSelectedBossKey()) + end) + + local curationSuggestedText = detailsContentFrame:CreateFontString(nil, "OVERLAY", "CELL_FONT_WIDGET") + curationSuggestedText:SetText("Suggested Order") + curationSuggestedText:SetPoint("TOPLEFT", curationStatusDropdown, "BOTTOMLEFT", 0, -8) + + curationSuggestedOrder = Cell.CreateEditBox(detailsContentFrame, 45, 20, nil, nil, true) + curationSuggestedOrder:SetPoint("TOPLEFT", curationSuggestedText, "BOTTOMLEFT", 0, -1) + curationSuggestedOrder:SetMaxLetters(3) + curationSuggestedOrder:SetJustifyH("RIGHT") + curationSuggestedOrder:SetScript("OnTextChanged", function(self, userChanged) + if userChanged then + SetRaidDebuffCurationField(loadedInstance, GetSelectedBossKey(), selectedSpellId, "suggestedOrder", self:GetText()) + end + end) + + curationCurrentOrderText = detailsContentFrame:CreateFontString(nil, "OVERLAY", "CELL_FONT_WIDGET_SMALL") + curationCurrentOrderText:SetPoint("LEFT", curationSuggestedOrder, "RIGHT", 8, 0) + curationCurrentOrderText:SetPoint("RIGHT", -2, 0) + curationCurrentOrderText:SetJustifyH("LEFT") + + local curationNoteText = detailsContentFrame:CreateFontString(nil, "OVERLAY", "CELL_FONT_WIDGET") + curationNoteText:SetText("Note") + curationNoteText:SetPoint("TOPLEFT", curationSuggestedOrder, "BOTTOMLEFT", 0, -8) + + curationNoteEditBox = Cell.CreateEditBox(detailsContentFrame, 177, 20) + curationNoteEditBox:SetPoint("TOPLEFT", curationNoteText, "BOTTOMLEFT", 0, -1) + curationNoteEditBox:SetPoint("RIGHT", -2, 0) + curationNoteEditBox:SetMaxLetters(120) + curationNoteEditBox:SetScript("OnTextChanged", function(self, userChanged) + if userChanged then + SetRaidDebuffCurationField(loadedInstance, GetSelectedBossKey(), selectedSpellId, "note", self:GetText()) + ApplyDebuffButtonCuration(debuffButtons[selectedButtonIndex], selectedButtonIndex <= #currentBossTable["enabled"] and currentBossTable["enabled"][selectedButtonIndex] or currentBossTable["disabled"][selectedButtonIndex-#currentBossTable["enabled"]]) + end + end) + -------------------------------------------------- -- condition -------------------------------------------------- local conditionText = detailsContentFrame:CreateFontString(nil, "OVERLAY", "CELL_FONT_WIDGET") conditionText:SetText(L["Condition"]) - conditionText:SetPoint("TOPLEFT", useElapsedTimeCB, "BOTTOMLEFT", 0, -10) + conditionText:SetPoint("TOPLEFT", curationNoteEditBox, "BOTTOMLEFT", 0, -10) -- conditionDropDown TODO: 同时持有另一个debuff conditionDropDown = Cell.CreateDropdown(detailsContentFrame, 117) @@ -1860,8 +2235,7 @@ LoadCondition = function(condition) end -- update scroll - detailsFrame.scrollFrame:SetContentHeight(225 + glowOptionsHeight + glowConditionHeight + conditionHeight) - detailsFrame.scrollFrame:ResetScroll() + UpdateDetailsHeight() end -- glow @@ -2052,8 +2426,7 @@ LoadGlowOptions = function(glowType, glowOptions) glowOptionsFrame:Hide() ShowGlowPreview("None") glowOptionsHeight = 0 - detailsFrame.scrollFrame:SetContentHeight(225 + conditionHeight) - detailsFrame.scrollFrame:ResetScroll() + UpdateDetailsHeight() return end @@ -2110,8 +2483,7 @@ LoadGlowOptions = function(glowType, glowOptions) glowOptionsFrame:Show() - detailsFrame.scrollFrame:SetContentHeight(225 + glowOptionsHeight + glowConditionHeight + conditionHeight) - detailsFrame.scrollFrame:ResetScroll() + UpdateDetailsHeight() end LoadGlowCondition = function(glowCondition) @@ -2132,8 +2504,7 @@ LoadGlowCondition = function(glowCondition) glowColor:SetPoint("TOPLEFT", glowConditionType, "BOTTOMLEFT", 0, -10) glowConditionHeight = 40 end - detailsFrame.scrollFrame:SetContentHeight(225 + glowOptionsHeight + glowConditionHeight + conditionHeight) - detailsFrame.scrollFrame:ResetScroll() + UpdateDetailsHeight() end -- spell description @@ -2190,6 +2561,12 @@ ShowDetails = function(spell) useElapsedTimeCB:SetChecked(spellTable["useElapsedTime"]) LoadCondition(spellTable["condition"]) + local curation = GetRaidDebuffCurationData(loadedInstance, GetSelectedBossKey(), spellId) + curationStatusDropdown:SetSelectedValue(curation["status"]) + curationSuggestedOrder:SetText(curation["suggestedOrder"] or "") + curationCurrentOrderText:SetText(isEnabled and ("Current order: " .. selectedButtonIndex) or "Current order: disabled") + curationNoteEditBox:SetText(curation["note"] or "") + local glowType = spellTable["glowType"] or "None" glowTypeDropdown:SetSelected(L[glowType]) glowTargetDropdown:SetSelectedValue(spellTable["glowTarget"]) diff --git a/Modules/Utilities/Utilities.lua b/Modules/Utilities/Utilities.lua index 4bf4caa0..36136560 100644 --- a/Modules/Utilities/Utilities.lua +++ b/Modules/Utilities/Utilities.lua @@ -36,6 +36,8 @@ function F.CreateUtilityList(anchor) dumbFS1:SetText(L["Quick Assist"]) local dumbFS2 = listFrame:CreateFontString(nil, "OVERLAY", "CELL_FONT_WIDGET") dumbFS2:SetText(L["Dispel Request"]) + local dumbFS3 = listFrame:CreateFontString(nil, "OVERLAY", "CELL_FONT_WIDGET") + dumbFS3:SetText("Midnight Tools") -- buttons buttons["raidTools"] = Cell.CreateButton(listFrame, L["Raid Tools"], "transparent-accent", {20, 20}, true) @@ -63,7 +65,19 @@ function F.CreateUtilityList(anchor) buttons["quickCast"].id = "quickCast" buttons["quickCast"]:SetPoint("TOPLEFT", buttons["quickAssist"], "BOTTOMLEFT") buttons["quickCast"]:SetPoint("TOPRIGHT", buttons["quickAssist"], "BOTTOMRIGHT") - P.Size(listFrame, ceil(max(dumbFS1:GetStringWidth(), dumbFS2:GetStringWidth())) + 13, 20*5) + + local totalButtons = 5 + local width = ceil(max(dumbFS1:GetStringWidth(), dumbFS2:GetStringWidth(), dumbFS3:GetStringWidth())) + 13 + + if Cell.isMidnight then + buttons["midnightTools"] = Cell.CreateButton(listFrame, "Midnight Tools", "transparent-accent", {20, 20}, true) + buttons["midnightTools"].id = "midnightTools" + buttons["midnightTools"]:SetPoint("TOPLEFT", buttons["quickCast"], "BOTTOMLEFT") + buttons["midnightTools"]:SetPoint("TOPRIGHT", buttons["quickCast"], "BOTTOMRIGHT") + totalButtons = 6 + end + + P.Size(listFrame, width, 20*totalButtons) else P.Size(listFrame, ceil(max(dumbFS1:GetStringWidth(), dumbFS2:GetStringWidth())) + 13, 20*3) end @@ -99,6 +113,7 @@ local utilityHeight = { ["dispelRequest"] = 420, ["quickAssist"] = 510, ["quickCast"] = 510, + ["midnightTools"] = 540, } local init @@ -122,4 +137,10 @@ end) function F.ShowQuickAssistTab() buttons["quickAssist"]:Click() -end \ No newline at end of file +end + +function F.ShowMidnightToolsTab() + if buttons["midnightTools"] then + buttons["midnightTools"]:Click() + end +end diff --git a/RaidFrames/UnitButton.lua b/RaidFrames/UnitButton.lua index 0eee9d1a..4f4c8385 100644 --- a/RaidFrames/UnitButton.lua +++ b/RaidFrames/UnitButton.lua @@ -86,6 +86,13 @@ local barAnimationType, highlightEnabled, predictionEnabled local shieldEnabled, overshieldEnabled, overshieldReverseFillEnabled local absorbEnabled, absorbInvertColor +local SECRET_HELPFUL_CAST_FALLBACK_WINDOW = 1.5 +local secretHelpfulCastFallbacks = { + [86150] = "defensive", -- Guardian of Ancient Kings (cast spell) + [86659] = "defensive", -- Guardian of Ancient Kings (base buff) + [212641] = "defensive", -- Guardian of Ancient Kings (glyph/model variant) +} + -- Midnight: Curve for CELL_FADE_OUT_HEALTH_PERCENT feature -- Maps health percent -> alpha so we can evaluate secret health% without comparisons local fadeOutHealthCurve @@ -1859,6 +1866,26 @@ local function ResetBuffVars(self) self.states.BGFlag = nil -- TODO: move to _buffs end +local function RememberSecretHelpfulCast(self, spellId) + if not F.IsValueNonSecret(spellId) then return end + + local fallbackKind = secretHelpfulCastFallbacks[spellId] + if not fallbackKind then return end + + self._recentSecretHelpfulCastKind = fallbackKind + self._recentSecretHelpfulCastAt = GetTime() + self._recentSecretHelpfulCastSpellId = spellId +end + +local function GetRecentSecretHelpfulCastKind(self) + local castAt = self._recentSecretHelpfulCastAt + if not castAt or GetTime() - castAt > SECRET_HELPFUL_CAST_FALLBACK_WINDOW then + return nil + end + + return self._recentSecretHelpfulCastKind +end + local function HandleBuff(self, auraInfo) local unit = self.states.displayedUnit local auraInstanceID = auraInfo.auraInstanceID @@ -1902,7 +1929,15 @@ local function HandleBuff(self, auraInfo) end -- Catch remaining raid-important secret buffs (e.g. Power Infusion) if not isDefensive and not isExternal then - isExternal = not _IsAuraFilteredOut(unit, auraInstanceID, "HELPFUL|RAID") + local isRaidImportant = not _IsAuraFilteredOut(unit, auraInstanceID, "HELPFUL|RAID") + if isRaidImportant then + local fallbackKind = GetRecentSecretHelpfulCastKind(self) + if fallbackKind == "defensive" then + isDefensive = true + else + isExternal = true + end + end end end @@ -2102,6 +2137,9 @@ local function ResetAuraTables(self) self._mirror_image = nil self._mass_barrier = nil self._mass_barrier_icon = nil + self._recentSecretHelpfulCastKind = nil + self._recentSecretHelpfulCastAt = nil + self._recentSecretHelpfulCastSpellId = nil end ------------------------------------------------- @@ -3946,6 +3984,9 @@ local function UnitButton_RegisterEvents(self) self:RegisterEvent("UNIT_DISPLAYPOWER") self:RegisterEvent("UNIT_AURA") + if Cell.isMidnight then + self:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED") + end self:RegisterEvent("UNIT_HEAL_PREDICTION") self:RegisterEvent("UNIT_ABSORB_AMOUNT_CHANGED") @@ -4017,7 +4058,7 @@ local function UnitButton_UnregisterEvents(self) self:UnregisterAllEvents() end -local function UnitButton_OnEvent(self, event, unit, arg) +local function UnitButton_OnEvent(self, event, unit, arg, arg2) if unit and (self.states.displayedUnit == unit or self.states.unit == unit) then if event == "UNIT_ENTERED_VEHICLE" or event == "UNIT_EXITED_VEHICLE" or event == "UNIT_CONNECTION" then self._updateRequired = 1 @@ -4080,6 +4121,9 @@ local function UnitButton_OnEvent(self, event, unit, arg) UnitButton_UpdatePowerTextColor(self) UnitButton_UpdatePowerText(self) + elseif event == "UNIT_SPELLCAST_SUCCEEDED" then + RememberSecretHelpfulCast(self, arg2) + elseif event == "UNIT_AURA" then UnitButton_UpdateAuras(self, arg) @@ -4205,6 +4249,9 @@ local function UnitButton_OnAttributeChanged(self, name, value) if not self.isSpotlight then Cell.vars.names[self.__unitName] = nil end self.__unitName = nil end + self._recentSecretHelpfulCastKind = nil + self._recentSecretHelpfulCastAt = nil + self._recentSecretHelpfulCastSpellId = nil wipe(self.states) -- Reset calculator predicted values to prevent stale data from previous unit if self.widgets and self.widgets.healthCalculator then diff --git a/Revise.lua b/Revise.lua index 292f0433..7d6f2a69 100644 --- a/Revise.lua +++ b/Revise.lua @@ -4,17 +4,252 @@ local F = Cell.funcs local I = Cell.iFuncs local U = Cell.uFuncs +local function GetRevisionNumber(db) + return db and db["revise"] and tonumber(string.match(db["revise"], "%d+")) or 0 +end + +local function GetCompatibilityDB() + if type(CellDB["compatibility"]) ~= "table" then + CellDB["compatibility"] = {} + end + return CellDB["compatibility"] +end + +local function GetIndicatorDisplayName(indicator) + if not indicator then return UNKNOWNOBJECT end + + if indicator["type"] == "built-in" and indicator["name"] and L[indicator["name"]] then + return L[indicator["name"]] + end + + return indicator["name"] or indicator["indicatorName"] or UNKNOWNOBJECT +end + +local function GetDefaultIndicatorDisplayName(indicatorName) + local index = Cell.defaults.indicatorIndices and Cell.defaults.indicatorIndices[indicatorName] + local indicator = index and Cell.defaults.layout and Cell.defaults.layout.indicators and Cell.defaults.layout.indicators[index] + if indicator and indicator["name"] and L[indicator["name"]] then + return L[indicator["name"]] + end + return indicator and indicator["name"] or indicatorName +end + +local function SortStrings(a, b) + return tostring(a) < tostring(b) +end + +local function SortNumbers(a, b) + return tonumber(a) < tonumber(b) +end + +local function GetInvalidAuraSpellIds(indicator) + local invalid = {} + if type(indicator) ~= "table" or type(indicator["auras"]) ~= "table" then + return invalid + end + + for _, aura in ipairs(indicator["auras"]) do + local spellId + if type(aura) == "number" then + spellId = aura + elseif type(aura) == "table" then + spellId = aura[1] + end + + if spellId and not F.GetSpellInfo(spellId) then + tinsert(invalid, tostring(spellId)) + end + end + + return invalid +end + +local function BuildCompatibilityReportData() + local report = { + dbRevision = GetRevisionNumber(CellDB), + charaDbRevision = GetRevisionNumber(CellCharacterDB), + globalResetRecommended = false, + characterResetRecommended = false, + layouts = {}, + layoutOrder = {}, + indicatorIssueCount = 0, + layoutIssueCount = 0, + issueCount = 0, + hasWarnings = false, + text = "", + } + + report.globalResetRecommended = CellDB["revise"] and report.dbRevision < Cell.MIN_VERSION + report.characterResetRecommended = CellCharacterDB and CellCharacterDB["revise"] and report.charaDbRevision < Cell.MIN_VERSION + + if type(CellDB["layouts"]) == "table" then + for layoutName in pairs(CellDB["layouts"]) do + tinsert(report.layoutOrder, layoutName) + end + table.sort(report.layoutOrder, SortStrings) + + for _, layoutName in ipairs(report.layoutOrder) do + local layout = CellDB["layouts"][layoutName] + local layoutReport = { + name = layoutName, + issueCount = 0, + lines = {}, + indicatorIssuesByIndex = {}, + } + + local builtInCounts = {} + local builtInIndices = {} + + if type(layout) == "table" and type(layout["indicators"]) == "table" then + for index, indicator in ipairs(layout["indicators"]) do + local indicatorIssue = { + name = GetIndicatorDisplayName(indicator), + lines = {}, + } + + if indicator["type"] == "built-in" then + local indicatorName = indicator["indicatorName"] or indicator["name"] or ("built-in-" .. index) + builtInCounts[indicatorName] = (builtInCounts[indicatorName] or 0) + 1 + builtInIndices[indicatorName] = builtInIndices[indicatorName] or {} + tinsert(builtInIndices[indicatorName], index) + + if not Cell.defaults.indicatorIndices[indicatorName] then + tinsert(indicatorIssue.lines, ("Unsupported built-in indicator: %s"):format(indicatorName)) + end + else + local invalidSpellIds = GetInvalidAuraSpellIds(indicator) + if #invalidSpellIds > 0 then + tinsert(indicatorIssue.lines, ("Invalid spell IDs: %s"):format(table.concat(invalidSpellIds, ", "))) + end + end + + if #indicatorIssue.lines > 0 then + layoutReport.indicatorIssuesByIndex[index] = indicatorIssue + layoutReport.issueCount = layoutReport.issueCount + #indicatorIssue.lines + end + end + end + + for indicatorName, indices in pairs(builtInIndices) do + if #indices > 1 then + for _, index in ipairs(indices) do + local indicatorIssue = layoutReport.indicatorIssuesByIndex[index] + if not indicatorIssue then + indicatorIssue = { + name = GetIndicatorDisplayName(layout["indicators"][index]), + lines = {}, + } + layoutReport.indicatorIssuesByIndex[index] = indicatorIssue + end + tinsert(indicatorIssue.lines, ("Duplicate built-in indicator: %s"):format(GetDefaultIndicatorDisplayName(indicatorName))) + layoutReport.issueCount = layoutReport.issueCount + 1 + end + end + end + + local missingBuiltIns = {} + for indicatorName in pairs(Cell.defaults.indicatorIndices or {}) do + if not builtInCounts[indicatorName] then + tinsert(missingBuiltIns, GetDefaultIndicatorDisplayName(indicatorName)) + end + end + + if #missingBuiltIns > 0 then + table.sort(missingBuiltIns, SortStrings) + tinsert(layoutReport.lines, ("Missing built-in indicators: %s"):format(table.concat(missingBuiltIns, ", "))) + layoutReport.issueCount = layoutReport.issueCount + 1 + end + + if layoutReport.issueCount > 0 then + report.layouts[layoutName] = layoutReport + report.issueCount = report.issueCount + layoutReport.issueCount + report.layoutIssueCount = report.layoutIssueCount + 1 + for _ in pairs(layoutReport.indicatorIssuesByIndex) do + report.indicatorIssueCount = report.indicatorIssueCount + 1 + end + end + end + end + + report.hasWarnings = report.globalResetRecommended or report.characterResetRecommended or report.issueCount > 0 + + local lines = {} + if report.globalResetRecommended then + tinsert(lines, ("Global profile revision r%d is older than the supported minimum r%d."):format(report.dbRevision, Cell.MIN_VERSION)) + end + if report.characterResetRecommended then + tinsert(lines, ("Character profile revision r%d is older than the supported minimum r%d. Review Click-Castings and Layout Auto Switch carefully."):format(report.charaDbRevision, Cell.MIN_VERSION)) + end + + for _, layoutName in ipairs(report.layoutOrder) do + local layoutReport = report.layouts[layoutName] + if layoutReport then + tinsert(lines, "") + tinsert(lines, ("[%s]"):format(layoutName == "default" and _G.DEFAULT or layoutName)) + + for _, line in ipairs(layoutReport.lines) do + tinsert(lines, "- " .. line) + end + + local indicatorIndices = {} + for index in pairs(layoutReport.indicatorIssuesByIndex) do + tinsert(indicatorIndices, index) + end + table.sort(indicatorIndices, SortNumbers) + + for _, index in ipairs(indicatorIndices) do + local indicatorIssue = layoutReport.indicatorIssuesByIndex[index] + tinsert(lines, ("- %s: %s"):format(indicatorIssue.name or ("Indicator " .. index), table.concat(indicatorIssue.lines, "; "))) + end + end + end + + if #lines == 0 then + tinsert(lines, "No compatibility issues detected.") + end + + report.text = table.concat(lines, "\n") + return report +end + +function F.GetCompatibilityReport() + return BuildCompatibilityReportData() +end + +function F.GetCompatibilityLayoutIssues(layoutName) + local report = BuildCompatibilityReportData() + return report.layouts[layoutName], report +end + +function F.HasCompatibilityWarnings() + return BuildCompatibilityReportData().hasWarnings +end + +local function DismissCompatibilityReset(kind) + GetCompatibilityDB()[kind] = true + + if F.AddAddonNotification then + F.AddAddonNotification("warning", "Compatibility warning saved", "Cell will stop asking for a reset for this profile. Open About > Compatibility to review what still needs attention.") + end + + Cell.Fire("UpdateCompatibilityReport") +end + function F.Revise() - local dbRevision = CellDB["revise"] and tonumber(string.match(CellDB["revise"], "%d+")) or 0 + local dbRevision = GetRevisionNumber(CellDB) F.Debug("DBRevision:", dbRevision) local charaDbRevision if CellCharacterDB then - charaDbRevision = CellCharacterDB["revise"] and tonumber(string.match(CellCharacterDB["revise"], "%d+")) or 0 + charaDbRevision = GetRevisionNumber(CellCharacterDB) F.Debug("CharaDBRevision:", charaDbRevision) end if CellDB["revise"] and dbRevision < Cell.MIN_VERSION then -- update from an unsupported version + if GetCompatibilityDB().globalResetDismissed then + return + end + local f = CreateFrame("Frame") f:RegisterEvent("PLAYER_ENTERING_WORLD") f:SetScript("OnEvent", function() @@ -23,6 +258,8 @@ function F.Revise() CellDB = nil CellCharacterDB = nil ReloadUI() + end, function() + DismissCompatibilityReset("globalResetDismissed") end) popup:SetPoint("TOPLEFT") end) @@ -30,6 +267,10 @@ function F.Revise() end if CellCharacterDB and CellCharacterDB["revise"] and charaDbRevision < Cell.MIN_VERSION then -- update from an unsupported version + if GetCompatibilityDB().characterResetDismissed then + return + end + local f = CreateFrame("Frame") f:RegisterEvent("PLAYER_ENTERING_WORLD") f:SetScript("OnEvent", function() @@ -37,6 +278,8 @@ function F.Revise() local popup = Cell.CreateConfirmPopup(CellAnchorFrame, 260, L["RESET_CHARACTER"].."\n|cFFB7B7B7"..L["RESET_INCLUDES"].."|r\n"..L["RESET_YES_NO"], function() CellCharacterDB = nil ReloadUI() + end, function() + DismissCompatibilityReset("characterResetDismissed") end) popup:SetPoint("TOPLEFT") end) @@ -3496,4 +3739,4 @@ function F.Revise() if CellCharacterDB then CellCharacterDB["revise"] = Cell.version end -end \ No newline at end of file +end diff --git a/Utilities/LoadUtilities.xml b/Utilities/LoadUtilities.xml index df2530e7..f30cfd73 100644 --- a/Utilities/LoadUtilities.xml +++ b/Utilities/LoadUtilities.xml @@ -18,4 +18,5 @@