diff --git a/Config.lua b/Config.lua index d8c76bf2e..62611cbe8 100644 --- a/Config.lua +++ b/Config.lua @@ -891,6 +891,25 @@ DF.PartyDefaults = { fadeDeadStatusText = 1, fadeDeadUseCustomColor = false, + -- Health threshold fading (fade when health above threshold) + healthFadeEnabled = false, + healthFadeAlpha = 0.5, + healthFadeThreshold = 100, + hfCancelOnDispel = true, + hfElementSpecific = false, + hfHealthBarAlpha = 0.3, + hfBackgroundAlpha = 0.3, + hfNameTextAlpha = 0.5, + hfHealthTextAlpha = 0.5, + hfAurasAlpha = 0.5, + hfIconsAlpha = 0.5, + hfDispelOverlayAlpha = 0.3, + hfMyBuffIndicatorAlpha = 0.5, + hfPowerBarAlpha = 0.3, + hfMissingBuffAlpha = 0.5, + hfDefensiveIconAlpha = 0.5, + hfTargetedSpellAlpha = 0.5, + -- Debuff settings debuffAlpha = 1, debuffAnchor = "BOTTOMLEFT", @@ -1948,6 +1967,25 @@ DF.RaidDefaults = { fadeDeadStatusText = 1, fadeDeadUseCustomColor = false, + -- Health threshold fading (fade when health above threshold) + healthFadeEnabled = false, + healthFadeAlpha = 0.5, + healthFadeThreshold = 100, + hfCancelOnDispel = true, + hfElementSpecific = false, + hfHealthBarAlpha = 0.3, + hfBackgroundAlpha = 0.3, + hfNameTextAlpha = 0.5, + hfHealthTextAlpha = 0.5, + hfAurasAlpha = 0.5, + hfIconsAlpha = 0.5, + hfDispelOverlayAlpha = 0.3, + hfMyBuffIndicatorAlpha = 0.5, + hfPowerBarAlpha = 0.3, + hfMissingBuffAlpha = 0.5, + hfDefensiveIconAlpha = 0.5, + hfTargetedSpellAlpha = 0.5, + -- Debuff settings debuffAlpha = 1, debuffAnchor = "BOTTOMLEFT", diff --git a/DandersFrames.toc b/DandersFrames.toc index 450148503..4511be3b4 100644 --- a/DandersFrames.toc +++ b/DandersFrames.toc @@ -60,6 +60,7 @@ Features\PrivateAuras.lua Features\Highlights.lua Features\ElementAppearance.lua Features\Range.lua +Features\HealthFade.lua Features\Sort.lua Features\SecureSort.lua Features\Dispel.lua diff --git a/Debug/PerformanceTest.lua b/Debug/PerformanceTest.lua index f099b445d..b697ef775 100644 --- a/Debug/PerformanceTest.lua +++ b/Debug/PerformanceTest.lua @@ -15,6 +15,7 @@ DF.PerfTest = { enableDefensive = true, enableMissingBuff = true, enableRange = true, + enableHealthFade = true, enableHighlights = true, enableHealPrediction = true, enableAbsorbs = true, @@ -130,6 +131,7 @@ local function CreatePerfTestFrame() CreateCheckbox(frame, "Defensive Icon", "enableDefensive", col1X, yStart + (i * yStep)); i = i + 1 CreateCheckbox(frame, "Missing Buff Icon", "enableMissingBuff", col1X, yStart + (i * yStep)); i = i + 1 CreateCheckbox(frame, "Range Checking", "enableRange", col1X, yStart + (i * yStep)); i = i + 1 + CreateCheckbox(frame, "Health Threshold Fade", "enableHealthFade", col1X, yStart + (i * yStep)); i = i + 1 CreateCheckbox(frame, "Highlights (Target/Mouse)", "enableHighlights", col1X, yStart + (i * yStep)); i = i + 1 CreateCheckbox(frame, "Heal Prediction", "enableHealPrediction", col1X, yStart + (i * yStep)); i = i + 1 CreateCheckbox(frame, "Absorb Shields", "enableAbsorbs", col1X, yStart + (i * yStep)); i = i + 1 diff --git a/ExportCategories.lua b/ExportCategories.lua index 5e36873b4..e0be17514 100644 --- a/ExportCategories.lua +++ b/ExportCategories.lua @@ -837,6 +837,25 @@ DF.ExportCategories = { "hoverHighlightMode", "hoverHighlightThickness", + -- Health Threshold Fading + "healthFadeEnabled", + "healthFadeAlpha", + "healthFadeThreshold", + "hfCancelOnDispel", + "hfElementSpecific", + "hfHealthBarAlpha", + "hfBackgroundAlpha", + "hfNameTextAlpha", + "hfHealthTextAlpha", + "hfAurasAlpha", + "hfIconsAlpha", + "hfDispelOverlayAlpha", + "hfMyBuffIndicatorAlpha", + "hfPowerBarAlpha", + "hfMissingBuffAlpha", + "hfDefensiveIconAlpha", + "hfTargetedSpellAlpha", + -- Out of Range "oorEnabled", "rangeAlpha", diff --git a/Features/ElementAppearance.lua b/Features/ElementAppearance.lua index 44e98a990..7023905c7 100644 --- a/Features/ElementAppearance.lua +++ b/Features/ElementAppearance.lua @@ -12,8 +12,9 @@ local addonName, DF = ... -- Priority Order for determining appearance: -- 1. Aggro Override (health bar only) -- 2. Dead/Offline State --- 3. Out of Range (OOR) - element-specific or frame-level --- 4. Normal Settings +-- 3. Health Threshold Fading (above configurable health threshold) +-- 4. Out of Range (OOR) - element-specific or frame-level +-- 5. Normal Settings -- -- Integration Points: -- - Range timer (Range.lua) calls UpdateRangeAppearance every 0.2s @@ -118,6 +119,16 @@ local function IsOffline(frame) return not UnitIsConnected(unit) end +-- Check if unit is above health threshold (for health threshold fading) +local function IsHealthFaded(frame) + return frame.dfIsHealthFaded == true +end + +-- Check if health threshold fade is enabled +local function IsHealthFadeEnabled(db) + return db and db.healthFadeEnabled +end + -- Get class color for a unit local function GetClassColor(frame) local unit = frame.unit @@ -168,6 +179,8 @@ function DF:UpdateHealthBarAppearance(frame) if deadOrOffline and db.fadeDeadFrames then alpha = db.fadeDeadHealthBar or 1 + elseif IsHealthFadeEnabled(db) and IsHealthFaded(frame) and not deadOrOffline and db.hfElementSpecific then + alpha = DF:GetHealthFadeAlpha(frame, "healthBar") end -- ======================================== @@ -319,9 +332,10 @@ function DF:UpdateBackgroundAppearance(frame) -- ======================================== local finalAlpha = baseAlpha - -- Dead fade ALWAYS affects alpha when enabled if deadOrOffline and db.fadeDeadFrames then finalAlpha = db.fadeDeadBackground or 1 + elseif IsHealthFadeEnabled(db) and IsHealthFaded(frame) and not deadOrOffline and db.hfElementSpecific then + finalAlpha = DF:GetHealthFadeAlpha(frame, "background") end -- ======================================== @@ -402,6 +416,8 @@ function DF:UpdateNameTextAppearance(frame) if deadOrOffline and db.fadeDeadFrames then alpha = db.fadeDeadName or 1.0 + elseif IsHealthFadeEnabled(db) and IsHealthFaded(frame) and not deadOrOffline and db.hfElementSpecific then + alpha = DF:GetHealthFadeAlpha(frame, "nameText") end -- ======================================== @@ -458,6 +474,8 @@ function DF:UpdateHealthTextAppearance(frame) if deadOrOffline and db.fadeDeadFrames then alpha = db.fadeDeadHealthBar or 1 -- Health text follows health bar alpha + elseif IsHealthFadeEnabled(db) and IsHealthFaded(frame) and not deadOrOffline and db.hfElementSpecific then + alpha = DF:GetHealthFadeAlpha(frame, "healthText") end -- ======================================== @@ -493,10 +511,11 @@ function DF:UpdateStatusTextAppearance(frame) local c = db.statusTextColor or DEFAULT_COLOR_WHITE local r, g, b = c.r, c.g, c.b - -- Alpha based on dead fade settings local alpha = 1.0 if deadOrOffline and db.fadeDeadFrames then alpha = db.fadeDeadStatusText or 1.0 + elseif IsHealthFadeEnabled(db) and IsHealthFaded(frame) and not deadOrOffline and db.hfElementSpecific then + alpha = DF:GetHealthFadeAlpha(frame, "healthText") -- status text uses same key as health text for health fade end frame.statusText:SetTextColor(r, g, b, alpha) @@ -526,6 +545,8 @@ function DF:UpdatePowerBarAppearance(frame) if deadOrOffline and db.fadeDeadFrames then alpha = db.fadeDeadPowerBar or 0 + elseif IsHealthFadeEnabled(db) and IsHealthFaded(frame) and not deadOrOffline and db.hfElementSpecific then + alpha = DF:GetHealthFadeAlpha(frame, "powerBar") end if db.oorEnabled then @@ -556,6 +577,8 @@ function DF:UpdateBuffIconsAppearance(frame) local alpha = 1.0 if deadOrOffline and db.fadeDeadFrames then alpha = db.fadeDeadAuras or 1.0 + elseif IsHealthFadeEnabled(db) and IsHealthFaded(frame) and not deadOrOffline and db.hfElementSpecific then + alpha = DF:GetHealthFadeAlpha(frame, "auras") end if db.oorEnabled then @@ -595,6 +618,8 @@ function DF:UpdateDebuffIconsAppearance(frame) local alpha = 1.0 if deadOrOffline and db.fadeDeadFrames then alpha = db.fadeDeadAuras or 1.0 + elseif IsHealthFadeEnabled(db) and IsHealthFaded(frame) and not deadOrOffline and db.hfElementSpecific then + alpha = DF:GetHealthFadeAlpha(frame, "auras") end if db.oorEnabled then @@ -634,6 +659,8 @@ function DF:UpdateRoleIconAppearance(frame) local alpha = 1.0 if deadOrOffline and db.fadeDeadFrames then alpha = db.fadeDeadIcons or 1.0 + elseif IsHealthFadeEnabled(db) and IsHealthFaded(frame) and not deadOrOffline and db.hfElementSpecific then + alpha = DF:GetHealthFadeAlpha(frame, "icons") end if db.oorEnabled then @@ -659,6 +686,8 @@ function DF:UpdateLeaderIconAppearance(frame) local alpha = 1.0 if deadOrOffline and db.fadeDeadFrames then alpha = db.fadeDeadIcons or 1.0 + elseif IsHealthFadeEnabled(db) and IsHealthFaded(frame) and not deadOrOffline and db.hfElementSpecific then + alpha = DF:GetHealthFadeAlpha(frame, "icons") end if db.oorEnabled then @@ -684,6 +713,8 @@ function DF:UpdateRaidTargetIconAppearance(frame) local alpha = 1.0 if deadOrOffline and db.fadeDeadFrames then alpha = db.fadeDeadIcons or 1.0 + elseif IsHealthFadeEnabled(db) and IsHealthFaded(frame) and not deadOrOffline and db.hfElementSpecific then + alpha = DF:GetHealthFadeAlpha(frame, "icons") end if db.oorEnabled then @@ -709,6 +740,8 @@ function DF:UpdateReadyCheckIconAppearance(frame) local alpha = 1.0 if deadOrOffline and db.fadeDeadFrames then alpha = db.fadeDeadIcons or 1.0 + elseif IsHealthFadeEnabled(db) and IsHealthFaded(frame) and not deadOrOffline and db.hfElementSpecific then + alpha = DF:GetHealthFadeAlpha(frame, "icons") end if db.oorEnabled then @@ -734,6 +767,8 @@ function DF:UpdateCenterStatusIconAppearance(frame) local alpha = 1.0 if deadOrOffline and db.fadeDeadFrames then alpha = db.fadeDeadIcons or 1.0 + elseif IsHealthFadeEnabled(db) and IsHealthFaded(frame) and not deadOrOffline and db.hfElementSpecific then + alpha = DF:GetHealthFadeAlpha(frame, "icons") end if db.oorEnabled then @@ -757,27 +792,34 @@ function DF:UpdateDispelOverlayAppearance(frame) if DF.testMode or DF.raidTestMode then return end + local deadOrOffline = IsDeadOrOffline(frame) local inRange = GetInRange(frame) + local overlay = frame.dfDispelOverlay + local alpha = 1.0 + if deadOrOffline and db.fadeDeadFrames then + alpha = db.fadeDeadBackground or 1 + elseif IsHealthFadeEnabled(db) and IsHealthFaded(frame) and not deadOrOffline and db.hfElementSpecific then + alpha = DF:GetHealthFadeAlpha(frame, "dispelOverlay") + end if db.oorEnabled then local oorAlpha = db.oorDispelOverlayAlpha or 0.2 - - -- PERF: Apply to elements directly without creating a table - local overlay = frame.dfDispelOverlay - ApplyOORAlpha(overlay.gradient, inRange, 1.0, oorAlpha) - ApplyOORAlpha(overlay.borderTop, inRange, 1.0, oorAlpha) - ApplyOORAlpha(overlay.borderBottom, inRange, 1.0, oorAlpha) - ApplyOORAlpha(overlay.borderLeft, inRange, 1.0, oorAlpha) - ApplyOORAlpha(overlay.borderRight, inRange, 1.0, oorAlpha) - ApplyOORAlpha(overlay.icon, inRange, 1.0, oorAlpha) - - -- For EDGE style, delegate to the dedicated function that re-applies SetGradient + ApplyOORAlpha(overlay.gradient, inRange, alpha, oorAlpha) + ApplyOORAlpha(overlay.borderTop, inRange, alpha, oorAlpha) + ApplyOORAlpha(overlay.borderBottom, inRange, alpha, oorAlpha) + ApplyOORAlpha(overlay.borderLeft, inRange, alpha, oorAlpha) + ApplyOORAlpha(overlay.borderRight, inRange, alpha, oorAlpha) + ApplyOORAlpha(overlay.icon, inRange, alpha, oorAlpha) if DF.ApplyDispelOverlayAppearance then DF:ApplyDispelOverlayAppearance(frame) end else - -- Frame-level mode - dispel overlay follows frame alpha - if frame.dfDispelOverlay.gradient then frame.dfDispelOverlay.gradient:SetAlpha(1.0) end + if overlay.gradient then overlay.gradient:SetAlpha(alpha) end + if overlay.borderTop then overlay.borderTop:SetAlpha(alpha) end + if overlay.borderBottom then overlay.borderBottom:SetAlpha(alpha) end + if overlay.borderLeft then overlay.borderLeft:SetAlpha(alpha) end + if overlay.borderRight then overlay.borderRight:SetAlpha(alpha) end + if overlay.icon then overlay.icon:SetAlpha(alpha) end end end @@ -786,11 +828,16 @@ end -- ============================================================ function DF:UpdateMyBuffIndicatorAppearance(frame) - -- Delegate to the dedicated appearance function in MyBuffIndicators.lua - -- This ensures there's only ONE place that sets colors/alpha if DF.ApplyMyBuffIndicatorAppearance then DF:ApplyMyBuffIndicatorAppearance(frame) end + local db = GetDB(frame) + if not db then return end + if DF.testMode or DF.raidTestMode then return end + if not frame.dfMyBuffOverlay or not frame.dfMyBuffOverlay:IsShown() then return end + if not db.oorEnabled and IsHealthFadeEnabled(db) and IsHealthFaded(frame) and db.hfElementSpecific then + frame.dfMyBuffOverlay:SetAlpha(DF:GetHealthFadeAlpha(frame, "myBuffIndicator")) + end end -- ============================================================ @@ -811,14 +858,24 @@ function DF:UpdateMissingBuffAppearance(frame) local inRange = GetInRange(frame) + local alpha = 1.0 + if IsHealthFadeEnabled(db) and IsHealthFaded(frame) and db.hfElementSpecific then + alpha = DF:GetHealthFadeAlpha(frame, "missingBuff") + end + if db.oorEnabled then local oorAlpha = db.oorMissingBuffAlpha or 0.5 - - ApplyOORAlpha(frame.missingBuffIcon, inRange, 1.0, oorAlpha) - ApplyOORAlpha(frame.missingBuffBorderLeft, inRange, 1.0, oorAlpha) - ApplyOORAlpha(frame.missingBuffBorderRight, inRange, 1.0, oorAlpha) - ApplyOORAlpha(frame.missingBuffBorderTop, inRange, 1.0, oorAlpha) - ApplyOORAlpha(frame.missingBuffBorderBottom, inRange, 1.0, oorAlpha) + ApplyOORAlpha(frame.missingBuffIcon, inRange, alpha, oorAlpha) + ApplyOORAlpha(frame.missingBuffBorderLeft, inRange, alpha, oorAlpha) + ApplyOORAlpha(frame.missingBuffBorderRight, inRange, alpha, oorAlpha) + ApplyOORAlpha(frame.missingBuffBorderTop, inRange, alpha, oorAlpha) + ApplyOORAlpha(frame.missingBuffBorderBottom, inRange, alpha, oorAlpha) + else + frame.missingBuffIcon:SetAlpha(alpha) + if frame.missingBuffBorderLeft then frame.missingBuffBorderLeft:SetAlpha(alpha) end + if frame.missingBuffBorderRight then frame.missingBuffBorderRight:SetAlpha(alpha) end + if frame.missingBuffBorderTop then frame.missingBuffBorderTop:SetAlpha(alpha) end + if frame.missingBuffBorderBottom then frame.missingBuffBorderBottom:SetAlpha(alpha) end end end @@ -911,17 +968,28 @@ function DF:UpdateDefensiveIconAppearance(frame) local inRange = GetInRange(frame) local icon = frame.defensiveIcon + local alpha = 1.0 + if IsHealthFadeEnabled(db) and IsHealthFaded(frame) and db.hfElementSpecific then + alpha = DF:GetHealthFadeAlpha(frame, "defensiveIcon") + end + if db.oorEnabled then local oorAlpha = db.oorDefensiveIconAlpha or 0.5 - - -- PERF: Apply to elements directly without creating a table - ApplyOORAlpha(icon.texture, inRange, 1.0, oorAlpha) - ApplyOORAlpha(icon.borderLeft, inRange, 1.0, oorAlpha) - ApplyOORAlpha(icon.borderRight, inRange, 1.0, oorAlpha) - ApplyOORAlpha(icon.borderTop, inRange, 1.0, oorAlpha) - ApplyOORAlpha(icon.borderBottom, inRange, 1.0, oorAlpha) - ApplyOORAlpha(icon.cooldown, inRange, 1.0, oorAlpha) - ApplyOORAlpha(icon.count, inRange, 1.0, oorAlpha) + ApplyOORAlpha(icon.texture, inRange, alpha, oorAlpha) + ApplyOORAlpha(icon.borderLeft, inRange, alpha, oorAlpha) + ApplyOORAlpha(icon.borderRight, inRange, alpha, oorAlpha) + ApplyOORAlpha(icon.borderTop, inRange, alpha, oorAlpha) + ApplyOORAlpha(icon.borderBottom, inRange, alpha, oorAlpha) + ApplyOORAlpha(icon.cooldown, inRange, alpha, oorAlpha) + ApplyOORAlpha(icon.count, inRange, alpha, oorAlpha) + else + if icon.texture then icon.texture:SetAlpha(alpha) end + if icon.borderLeft then icon.borderLeft:SetAlpha(alpha) end + if icon.borderRight then icon.borderRight:SetAlpha(alpha) end + if icon.borderTop then icon.borderTop:SetAlpha(alpha) end + if icon.borderBottom then icon.borderBottom:SetAlpha(alpha) end + if icon.cooldown then icon.cooldown:SetAlpha(alpha) end + if icon.count then icon.count:SetAlpha(alpha) end end end @@ -943,11 +1011,16 @@ function DF:UpdateTargetedSpellAppearance(frame) local inRange = GetInRange(frame) + local alpha = 1.0 + if IsHealthFadeEnabled(db) and IsHealthFaded(frame) and db.hfElementSpecific then + alpha = DF:GetHealthFadeAlpha(frame, "targetedSpell") + end + if db.oorEnabled then local oorAlpha = db.oorTargetedSpellAlpha or 0.5 - ApplyOORAlpha(frame.targetedSpellContainer, inRange, 1.0, oorAlpha) + ApplyOORAlpha(frame.targetedSpellContainer, inRange, alpha, oorAlpha) else - frame.targetedSpellContainer:SetAlpha(1.0) + frame.targetedSpellContainer:SetAlpha(alpha) end end @@ -963,15 +1036,18 @@ function DF:UpdateFrameAppearance(frame) if DF.testMode or DF.raidTestMode then return end - -- Only apply frame-level alpha when NOT using element-specific mode if db.oorEnabled then - -- In oorEnabled mode, frame stays at full alpha ApplyOORAlpha(frame, true, 1.0, 1.0) else - -- Frame-level OOR mode local inRange = GetInRange(frame) - local outOfRangeAlpha = db.rangeFadeAlpha or 0.4 - ApplyOORAlpha(frame, inRange, 1.0, outOfRangeAlpha) + -- Frame-level: health-based fade (above threshold) or OOR + if IsHealthFadeEnabled(db) and IsHealthFaded(frame) and not db.hfElementSpecific then + local healthFadeAlpha = db.healthFadeAlpha or 0.5 + frame:SetAlpha(healthFadeAlpha) + else + local outOfRangeAlpha = db.rangeFadeAlpha or 0.4 + ApplyOORAlpha(frame, inRange, 1.0, outOfRangeAlpha) + end end end diff --git a/Features/HealthFade.lua b/Features/HealthFade.lua new file mode 100644 index 000000000..23645910a --- /dev/null +++ b/Features/HealthFade.lua @@ -0,0 +1,158 @@ +local addonName, DF = ... + +-- ============================================================ +-- HEALTH THRESHOLD FADE SYSTEM +-- Fades frames/elements when a unit's health is above a configurable +-- threshold (e.g. 100% or 80%). Uses the same pattern as Range.lua. +-- ============================================================ + +-- Upvalue all frequently used globals for performance +local UnitExists = UnitExists +local UnitIsDeadOrGhost = UnitIsDeadOrGhost +local UnitIsConnected = UnitIsConnected + +-- ============================================================ +-- CHECK IF UNIT IS ABOVE HEALTH THRESHOLD +-- Returns true if unit health percent is >= configured threshold. +-- We use frame.dfComputedAboveThreshold set by Core.lua's SetHealthBarValue +-- via a StatusBar OnValueChanged callback (receives resolved values). +-- ============================================================ + +local function IsAboveHealthThresholdFromFrame(frame) + if not frame or not frame.unit then + return false + end + + local unit = frame.unit + + if not UnitExists(unit) then + return false + end + + if UnitIsDeadOrGhost(unit) or not UnitIsConnected(unit) then + return false + end + + return frame.dfComputedAboveThreshold == true +end + +-- ============================================================ +-- UPDATE HEALTH FADE STATE FOR A FRAME +-- ============================================================ + +function DF:UpdateHealthFade(frame) + if not frame or not frame.unit then return end + + if frame.isPetFrame then + DF:UpdatePetHealthFade(frame) + return + end + + if DF.PerfTest and not DF.PerfTest.enableHealthFade then return end + if DF.testMode or DF.raidTestMode then return end + + local db = frame.isRaidFrame and DF:GetRaidDB() or DF:GetDB() + if not db or not db.healthFadeEnabled then + if frame.dfIsHealthFaded then + frame.dfIsHealthFaded = false + if DF.UpdateAllElementAppearances then + DF:UpdateAllElementAppearances(frame) + end + end + return + end + + local isAboveThreshold = IsAboveHealthThresholdFromFrame(frame) + + if isAboveThreshold and db.hfCancelOnDispel then + if frame.dfDispelOverlay and frame.dfDispelOverlay:IsShown() then + isAboveThreshold = false + end + end + + if frame.dfIsHealthFaded ~= isAboveThreshold then + frame.dfIsHealthFaded = isAboveThreshold + + if DF.UpdateAllElementAppearances then + DF:UpdateAllElementAppearances(frame) + end + end +end + +-- ============================================================ +-- UPDATE HEALTH FADE FOR PET FRAMES +-- ============================================================ + +function DF:UpdatePetHealthFade(frame) + if not frame or not frame.unit then return end + + if not UnitExists(frame.unit) then return end + + local db = frame.isRaidFrame and DF:GetRaidDB() or DF:GetDB() + if not db or not db.healthFadeEnabled then + frame.dfIsHealthFaded = false + return + end + + local isAboveThreshold = IsAboveHealthThresholdFromFrame(frame) + + if frame.dfIsHealthFaded ~= isAboveThreshold then + frame.dfIsHealthFaded = isAboveThreshold + + local healthFadeAlpha = db.healthFadeAlpha or 0.5 + + if frame.SetAlpha then + frame:SetAlpha(isAboveThreshold and healthFadeAlpha or 1.0) + end + if frame.healthBar then + frame.healthBar:SetAlpha(isAboveThreshold and healthFadeAlpha or 1.0) + end + end +end + +-- ============================================================ +-- HELPER: Check if a frame should be faded (above health threshold) +-- Used by ElementAppearance.lua +-- ============================================================ + +function DF:IsHealthFaded(frame) + if not frame then return false end + + local db = frame.isRaidFrame and DF:GetRaidDB() or DF:GetDB() + if not db or not db.healthFadeEnabled then + return false + end + + return frame.dfIsHealthFaded == true +end + +-- ============================================================ +-- HELPER: Get health fade alpha for an element +-- Used by ElementAppearance.lua +-- ============================================================ + +function DF:GetHealthFadeAlpha(frame, elementKey) + if not frame then return 1.0 end + + local db = frame.isRaidFrame and DF:GetRaidDB() or DF:GetDB() + if not db then return 1.0 end + + local alphaMap = { + healthBar = "hfHealthBarAlpha", + background = "hfBackgroundAlpha", + nameText = "hfNameTextAlpha", + healthText = "hfHealthTextAlpha", + auras = "hfAurasAlpha", + icons = "hfIconsAlpha", + dispelOverlay = "hfDispelOverlayAlpha", + powerBar = "hfPowerBarAlpha", + missingBuff = "hfMissingBuffAlpha", + defensiveIcon = "hfDefensiveIconAlpha", + targetedSpell = "hfTargetedSpellAlpha", + myBuffIndicator = "hfMyBuffIndicatorAlpha", + frame = "healthFadeAlpha", + } + + local dbKey = alphaMap[elementKey] or "healthFadeAlpha" + return db[dbKey] or 0.5 +end diff --git a/Features/Range.lua b/Features/Range.lua index dffc16a59..ac3095186 100644 --- a/Features/Range.lua +++ b/Features/Range.lua @@ -633,6 +633,9 @@ rangeAnimGroup:SetLooping("REPEAT") local function RangeCheckFrame(frame) if frame and frame:IsShown() then DF:UpdateRange(frame) + if DF.UpdateHealthFade then + DF:UpdateHealthFade(frame) + end end end @@ -671,6 +674,9 @@ rangeAnimGroup:SetScript("OnLoop", function() local frame = DF.raidPetFrames[i] if frame and not frame.dfPetHidden then DF:UpdatePetRange(frame) + if DF.UpdatePetHealthFade then + DF:UpdatePetHealthFade(frame) + end end end end @@ -697,6 +703,9 @@ rangeAnimGroup:SetScript("OnLoop", function() local petFrame = DF.petFrames.player if not petFrame.dfPetHidden then DF:UpdatePetRange(petFrame) + if DF.UpdatePetHealthFade then + DF:UpdatePetHealthFade(petFrame) + end end end @@ -705,6 +714,9 @@ rangeAnimGroup:SetScript("OnLoop", function() local frame = DF.partyPetFrames[i] if frame and not frame.dfPetHidden then DF:UpdatePetRange(frame) + if DF.UpdatePetHealthFade then + DF:UpdatePetHealthFade(frame) + end end end end diff --git a/Frames/Core.lua b/Frames/Core.lua index 7a332291d..c1fa6bb8b 100644 --- a/Frames/Core.lua +++ b/Frames/Core.lua @@ -74,6 +74,8 @@ end DF.GetSafeHealthPercent = GetSafeHealthPercent -- Helper to set health bar value safely +-- When health threshold fading is enabled, a hidden bar receives the same value so its +-- OnValueChanged callback gets the resolved (non-secret) percent and updates dfComputedAboveThreshold. local function SetHealthBarValue(bar, unit, frame) if not bar then return end @@ -97,6 +99,29 @@ local function SetHealthBarValue(bar, unit, frame) else bar:SetValue(pct) end + + -- Health threshold fading: use a hidden bar so OnValueChanged receives resolved value (safe with secret values) + if frame then + if not frame.dfHealthCheckBar then + frame.dfHealthCheckBar = CreateFrame("StatusBar", nil, bar) + frame.dfHealthCheckBar:SetSize(1, 1) + frame.dfHealthCheckBar:SetMinMaxValues(0, 100) + frame.dfHealthCheckBar:SetAlpha(0) + frame.dfHealthCheckBar:Hide() + frame.dfHealthCheckBar:SetScript("OnValueChanged", function(self, value) + if frame then + local thresholdDb = frame.isRaidFrame and (DF.GetRaidDB and DF:GetRaidDB()) or (DF.GetDB and DF:GetDB()) + local threshold = (thresholdDb and thresholdDb.healthFadeThreshold) or 100 + frame.dfComputedAboveThreshold = (value >= threshold - 0.5) + if DF.UpdateHealthFade then + DF:UpdateHealthFade(frame) + end + end + end) + end + frame.dfHealthCheckBar:SetMinMaxValues(0, 100) + frame.dfHealthCheckBar:SetValue(pct) + end end -- Export for use in other files diff --git a/Options/Options.lua b/Options/Options.lua index f09190376..4a707dee7 100644 --- a/Options/Options.lua +++ b/Options/Options.lua @@ -356,7 +356,7 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) local pageFading = CreateSubTab("display", "display_fading", "Fading") BuildPage(pageFading, function(self, db, Add, AddSpace, AddSyncPoint) -- Copy button at top right - Add(CreateCopyButton(self.child, {"rangeFade", "oor", "dead", "offline"}, "Fading"), 25, 2) + Add(CreateCopyButton(self.child, {"rangeFade", "oor", "dead", "offline", "healthFade", "hf"}, "Fading"), 25, 2) -- Sync point: ensures both columns start below the copy button AddSpace(10, "both") @@ -593,6 +593,86 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) Add(deadGroup, nil, 2) + -- ===== HEALTH THRESHOLD FADING (above health threshold) ===== + AddSpace(20, "both") + local hfGroup = GUI:CreateSettingsGroup(self.child, 560) + hfGroup:AddWidget(GUI:CreateHeader(self.child, "Health Threshold Fading"), 40) + hfGroup.tooltip = "Fade frames or elements when a unit's health is above the set threshold (e.g. 100% or 80%)." + + hfGroup:AddWidget(GUI:CreateCheckbox(self.child, "Enable Health Threshold Fade", db, "healthFadeEnabled", function() + self:RefreshStates() + DF:UpdateAllFrames() + DF:RefreshAllVisibleFrames() + end), 30) + + local function HideHFOptions(d) + return not d.healthFadeEnabled + end + local function HideHFElementOptions(d) + return not d.healthFadeEnabled or not d.hfElementSpecific + end + local function HideHFFrameAlpha(d) + return not d.healthFadeEnabled or d.hfElementSpecific + end + + local hfThreshold = hfGroup:AddWidget(GUI:CreateSlider(self.child, "Health Threshold (%)", 50, 100, 1, db, "healthFadeThreshold", nil, function() + DF:UpdateAllFrames() + DF:RefreshAllVisibleFrames() + end, false), 55) + hfThreshold.hideOn = HideHFOptions + hfThreshold.tooltip = "Units at or above this health percent are faded." + + local hfCancelDispel = hfGroup:AddWidget(GUI:CreateCheckbox(self.child, "Cancel Fade on Dispellable Debuff", db, "hfCancelOnDispel", function() + DF:UpdateAllFrames() + DF:RefreshAllVisibleFrames() + end), 30) + hfCancelDispel.hideOn = HideHFOptions + + -- Health fade sliders need UpdateAllFrameAppearances to force an immediate visual refresh. + -- Unlike OOR/dead fade which refresh on range/state changes, health fade alpha values + -- are only re-read during appearance updates, not triggered by FullFrameRefresh alone. + local function RefreshHealthFade() + DF:RefreshAllVisibleFrames() + if DF.UpdateAllFrameAppearances then DF:UpdateAllFrameAppearances() end + end + + local hfFrameAlpha = hfGroup:AddWidget(GUI:CreateSlider(self.child, "Frame Alpha (Above Threshold)", 0.1, 1.0, 0.05, db, "healthFadeAlpha", nil, RefreshHealthFade, true), 55) + hfFrameAlpha.hideOn = HideHFFrameAlpha + hfFrameAlpha.tooltip = "Frame opacity when health is above the threshold (element-specific disabled)." + + local hfElementSpecific = hfGroup:AddWidget(GUI:CreateCheckbox(self.child, "Enable Element-Specific Alpha", db, "hfElementSpecific", function() + self:RefreshStates() + RefreshHealthFade() + end), 30) + hfElementSpecific.hideOn = HideHFOptions + + local hfHealth = hfGroup:AddWidget(GUI:CreateSlider(self.child, "Health Bar Alpha", 0.0, 1.0, 0.05, db, "hfHealthBarAlpha", nil, RefreshHealthFade, true), 55) + hfHealth.hideOn = HideHFElementOptions + local hfBg = hfGroup:AddWidget(GUI:CreateSlider(self.child, "Background Alpha", 0.0, 1.0, 0.05, db, "hfBackgroundAlpha", nil, RefreshHealthFade, true), 55) + hfBg.hideOn = HideHFElementOptions + local hfName = hfGroup:AddWidget(GUI:CreateSlider(self.child, "Name Text Alpha", 0.0, 1.0, 0.05, db, "hfNameTextAlpha", nil, RefreshHealthFade, true), 55) + hfName.hideOn = HideHFElementOptions + local hfHealthText = hfGroup:AddWidget(GUI:CreateSlider(self.child, "Health Text Alpha", 0.0, 1.0, 0.05, db, "hfHealthTextAlpha", nil, RefreshHealthFade, true), 55) + hfHealthText.hideOn = HideHFElementOptions + local hfAuras = hfGroup:AddWidget(GUI:CreateSlider(self.child, "Auras Alpha", 0.0, 1.0, 0.05, db, "hfAurasAlpha", nil, RefreshHealthFade, true), 55) + hfAuras.hideOn = HideHFElementOptions + local hfIcons = hfGroup:AddWidget(GUI:CreateSlider(self.child, "Icons Alpha", 0.0, 1.0, 0.05, db, "hfIconsAlpha", nil, RefreshHealthFade, true), 55) + hfIcons.hideOn = HideHFElementOptions + local hfDispel = hfGroup:AddWidget(GUI:CreateSlider(self.child, "Dispel Overlay Alpha", 0.0, 1.0, 0.05, db, "hfDispelOverlayAlpha", nil, RefreshHealthFade, true), 55) + hfDispel.hideOn = HideHFElementOptions + local hfMyBuff = hfGroup:AddWidget(GUI:CreateSlider(self.child, "My Buff Indicator Alpha", 0.0, 1.0, 0.05, db, "hfMyBuffIndicatorAlpha", nil, RefreshHealthFade, true), 55) + hfMyBuff.hideOn = HideHFElementOptions + local hfPower = hfGroup:AddWidget(GUI:CreateSlider(self.child, "Power Bar Alpha", 0.0, 1.0, 0.05, db, "hfPowerBarAlpha", nil, RefreshHealthFade, true), 55) + hfPower.hideOn = HideHFElementOptions + local hfMissingBuff = hfGroup:AddWidget(GUI:CreateSlider(self.child, "Missing Buff Alpha", 0.0, 1.0, 0.05, db, "hfMissingBuffAlpha", nil, RefreshHealthFade, true), 55) + hfMissingBuff.hideOn = HideHFElementOptions + local hfDefensive = hfGroup:AddWidget(GUI:CreateSlider(self.child, "Defensive Icon Alpha", 0.0, 1.0, 0.05, db, "hfDefensiveIconAlpha", nil, RefreshHealthFade, true), 55) + hfDefensive.hideOn = HideHFElementOptions + local hfTargetedSpell = hfGroup:AddWidget(GUI:CreateSlider(self.child, "Targeted Spell Alpha", 0.0, 1.0, 0.05, db, "hfTargetedSpellAlpha", nil, RefreshHealthFade, true), 55) + hfTargetedSpell.hideOn = HideHFElementOptions + + Add(hfGroup, nil, "both") + -- See Also links AddSpace(20, "both") Add(GUI:CreateSeeAlso(self.child, { diff --git a/TestMode/TestMode.lua b/TestMode/TestMode.lua index 1d0840605..43cd96b14 100644 --- a/TestMode/TestMode.lua +++ b/TestMode/TestMode.lua @@ -662,8 +662,51 @@ function DF:UpdateTestFrame(frame, index, applyLayout) frame.dfTestDeadFadeAlphas = nil end + -- Health-based fading (above threshold): only when in range, alive, and option enabled + local healthPct = (testData.healthPercent or 1) * 100 + local threshold = db.fullHealthFadeThreshold or 100 + local isAboveHealthThreshold = not isOutOfRange and not applyDeadFade + and db.fullHealthFadeEnabled + and (healthPct >= threshold - 0.5) + if isAboveHealthThreshold and db.fhCancelOnDispel and frame.dfDispelOverlay and frame.dfDispelOverlay:IsShown() then + isAboveHealthThreshold = false + end + + if isAboveHealthThreshold then + if db.fhElementSpecific then + healthBarAlpha = db.fhHealthBarAlpha or 0.5 + backgroundAlpha = db.fhBackgroundAlpha or 0.5 + nameAlpha = db.fhNameTextAlpha or 0.5 + healthTextAlpha = db.fhHealthTextAlpha or 0.5 + aurasAlpha = db.fhAurasAlpha or 0.5 + iconsAlpha = db.fhIconsAlpha or 0.5 + powerBarAlpha = db.fhPowerBarAlpha or 0.5 + dispelAlpha = db.fhDispelOverlayAlpha or 0.5 + targetedSpellAlpha = db.fhTargetedSpellAlpha or 0.5 + else + local fhAlpha = db.fullHealthFadeAlpha or 0.5 + healthBarAlpha = fhAlpha + backgroundAlpha = fhAlpha + nameAlpha = fhAlpha + healthTextAlpha = fhAlpha + aurasAlpha = fhAlpha + iconsAlpha = fhAlpha + powerBarAlpha = fhAlpha + dispelAlpha = fhAlpha + targetedSpellAlpha = fhAlpha + end + frame.dfTestHealthFadeAlphas = { + icons = iconsAlpha, + power = powerBarAlpha, + dispel = dispelAlpha, + targetedSpell = targetedSpellAlpha, + } + else + frame.dfTestHealthFadeAlphas = nil + end + -- Update name color with appropriate alpha - -- OOR takes priority over dead fade + -- Priority: OOR > dead fade > health-based fade local finalNameAlpha = nameAlpha if not isOutOfRange and applyDeadFade then finalNameAlpha = db.fadeDeadName or 1.0 @@ -815,6 +858,19 @@ function DF:UpdateTestFrame(frame, index, applyLayout) end end + -- Frame-level alpha when not using element-specific OOR (same as live UpdateFrameAppearance) + if not db.oorEnabled then + if isOutOfRange then + frame:SetAlpha(db.rangeFadeAlpha or db.rangeAlpha or 0.55) + elseif isAboveHealthThreshold and not db.fhElementSpecific then + frame:SetAlpha(db.fullHealthFadeAlpha or 0.5) + else + frame:SetAlpha(1) + end + else + frame:SetAlpha(1) + end + -- Apply alpha to health text if frame.healthText then if db.healthTextUseClassColor then @@ -1184,14 +1240,14 @@ function DF:UpdateTestIcons(frame, testData) end end - -- Apply alpha to icons - check dead fade first, then OOR alpha + -- Apply alpha to icons - check dead fade first, then health-based fade, then OOR alpha local alpha = 1.0 if frame.dfTestDeadFadeAlphas and frame.dfTestDeadFadeAlphas.icons then - -- Dead fade takes priority alpha = frame.dfTestDeadFadeAlphas.icons elseif frame.dfDeadFadeApplied then - -- Dead fade already applied the correct alpha in ApplyDeadFade, don't override return + elseif frame.dfTestHealthFadeAlphas and frame.dfTestHealthFadeAlphas.icons then + alpha = frame.dfTestHealthFadeAlphas.icons elseif frame.dfTestOORAlphas and frame.dfTestOORAlphas.icons then alpha = frame.dfTestOORAlphas.icons end @@ -1635,12 +1691,14 @@ function DF:UpdateTestStatusIcons(frame, testData) end end - -- Apply alpha to status icons based on dead/OOR fade + -- Apply alpha to status icons based on dead / health-based / OOR fade local alpha = 1.0 if frame.dfTestDeadFadeAlphas and frame.dfTestDeadFadeAlphas.icons then alpha = frame.dfTestDeadFadeAlphas.icons elseif frame.dfDeadFadeApplied then return + elseif frame.dfTestHealthFadeAlphas and frame.dfTestHealthFadeAlphas.icons then + alpha = frame.dfTestHealthFadeAlphas.icons elseif frame.dfTestOORAlphas and frame.dfTestOORAlphas.icons then alpha = frame.dfTestOORAlphas.icons end @@ -3431,11 +3489,12 @@ function DF:UpdateTestPowerBar(frame, testData) end end - -- Apply OOR alpha or dead fade alpha + -- Apply dead / health-based / OOR alpha local alpha = 1.0 if frame.dfTestDeadFadeAlphas and frame.dfTestDeadFadeAlphas.power then - -- Dead fade takes priority alpha = frame.dfTestDeadFadeAlphas.power + elseif frame.dfTestHealthFadeAlphas and frame.dfTestHealthFadeAlphas.power then + alpha = frame.dfTestHealthFadeAlphas.power elseif frame.dfTestOORAlphas and frame.dfTestOORAlphas.power then alpha = frame.dfTestOORAlphas.power end @@ -5128,8 +5187,10 @@ function DF:UpdateTestTargetedSpell(frame, testData) local durationColor = db.targetedSpellDurationColor or {r = 1, g = 1, b = 1} local alpha = db.targetedSpellAlpha or 1.0 - -- Apply OOR alpha if frame is out of range - if frame.dfTestOORAlphas and frame.dfTestOORAlphas.targetedSpell then + -- Apply health-based or OOR alpha in test mode + if frame.dfTestHealthFadeAlphas and frame.dfTestHealthFadeAlphas.targetedSpell then + alpha = alpha * frame.dfTestHealthFadeAlphas.targetedSpell + elseif frame.dfTestOORAlphas and frame.dfTestOORAlphas.targetedSpell then alpha = alpha * frame.dfTestOORAlphas.targetedSpell end