From 3d7ac932e1fcd1836ff8a9bc19191db00a791773 Mon Sep 17 00:00:00 2001 From: OEVG <950+oevg@noreply.octowow.st> Date: Sun, 1 Nov 2026 00:42:19 +0000 Subject: [PATCH] Add max buffs and max debuffs settings - /tfb buffs 0-32 and /tfb debuffs 0-16, plus two options-panel sliders. - Saved settings are validated and reset when out of range. - A lowered debuff limit hides extra debuffs instead of spilling them into the buff row; the buff limit applies to spilled debuffs too. - Works on the native and ClassicAPI paths and with test mode. - Bump to 0.9.6; tests now 23 scenarios. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_019W3rwutyoLNHxXPycYWSMc --- CHANGELOG.md | 7 +++++++ README.md | 4 +++- Settings.lua | 43 +++++++++++++++++++++++++++++-------------- TargetFrameBuff.lua | 15 +++++++++++---- TargetFrameBuff.toc | 2 +- VALIDATION.md | 6 +++--- tests/test_addon.py | 33 +++++++++++++++++++++++++++++++++ 7 files changed, 87 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa22b86..346be44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.9.6 + +- Add settings for the most buffs (0-32) and debuffs (0-16) shown: `/tfb buffs N`, + `/tfb debuffs N` and two options-panel sliders. A lowered debuff limit hides the + extras instead of spilling them into the buff row. +- Tests: 23 scenarios. + ## 0.9.5 - Fix the `/tfb` help text: `|` is a chat escape character, so it printed garbled. diff --git a/README.md b/README.md index fdd0180..b2c941b 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ tooltips. Without ClassicAPI it behaves like the original: more icons, no timers | 1.17.10+ | Event-driven updates (`UNIT_AURA_DELTA`), no polling | | 1.17.11+ | **Recommended.** Fixes the reverse sweep briefly showing a bright icon just before an aura ends | -Harmful auras beyond the first 16 use free buff-row slots. ClassicAPI decides +Harmful auras beyond the first 16 use free buff-row slots (only while the debuff limit is 16; the buff limit still applies). ClassicAPI decides aura order and lists your own auras first. ## Settings @@ -38,6 +38,8 @@ aura order and lists your own auras first. | `/tfb sweeps [on\|off]` | Reverse cooldown sweeps | off | | `/tfb size 6-20` | Countdown font size | 9 | | `/tfb warning 0-30` | Seconds left at which text turns red | 5 | +| `/tfb buffs 0-32` | Most buffs shown | 32 | +| `/tfb debuffs 0-16` | Most debuffs shown. Below 16, extra debuffs are hidden rather than moved into the buff row. | 16 | | `/tfb test [buffs debuffs]` | Fake auras on your target for checking layout (default 32 and 16; 0-32 each). Run again or `/tfb test off` to stop. | off | | `/tfb reset` | Restore defaults | | diff --git a/Settings.lua b/Settings.lua index b6d8248..4e8166d 100644 --- a/Settings.lua +++ b/Settings.lua @@ -1,6 +1,7 @@ -- Settings remain usable through /tfb on unmodified 1.12 clients. TargetFrameBuffDefaults = { - timers = true, highlight = true, sweeps = false, timerSize = 9, warning = 5 + timers = true, highlight = true, sweeps = false, timerSize = 9, warning = 5, + maxBuffs = 32, maxDebuffs = 16 }; local function RefreshAuras() @@ -24,6 +25,12 @@ local function Validate() if (TargetFrameBuffDB.warning ~= TargetFrameBuffDB.warning or TargetFrameBuffDB.warning < 0 or TargetFrameBuffDB.warning > 30) then TargetFrameBuffDB.warning = 5; end + if (TargetFrameBuffDB.maxBuffs ~= TargetFrameBuffDB.maxBuffs or TargetFrameBuffDB.maxBuffs < 0 or TargetFrameBuffDB.maxBuffs > 32) then + TargetFrameBuffDB.maxBuffs = 32; + end + if (TargetFrameBuffDB.maxDebuffs ~= TargetFrameBuffDB.maxDebuffs or TargetFrameBuffDB.maxDebuffs < 0 or TargetFrameBuffDB.maxDebuffs > 16) then + TargetFrameBuffDB.maxDebuffs = 16; + end end local panel; @@ -52,18 +59,20 @@ local function CreateOptions() checks[i] = check; end local sliders = {}; - local sliderKeys = {"timerSize", "warning"}; - local sliderLabels = {"Timer font size", "Warning threshold (seconds)"}; - for i = 1, 2 do + local sliderKeys = {"timerSize", "warning", "maxBuffs", "maxDebuffs"}; + local sliderLabels = {"Timer font size", "Warning threshold (seconds)", "Max buffs shown", "Max debuffs shown"}; + local sliderMin = {6, 0, 0, 0}; + local sliderMax = {20, 30, 32, 16}; + for i = 1, 4 do local slider = CreateFrame("Slider", "TargetFrameBuffSlider"..i, panel, "OptionsSliderTemplate"); slider:SetPoint("TOPLEFT", panel, "TOPLEFT", 22, -190 - (i - 1) * 65); slider:SetWidth(240); - slider:SetMinMaxValues(i == 1 and 6 or 0, i == 1 and 20 or 30); + slider:SetMinMaxValues(sliderMin[i], sliderMax[i]); slider:SetValueStep(1); slider.key = sliderKeys[i]; slider.label = sliderLabels[i]; - getglobal(slider:GetName().."Low"):SetText(i == 1 and "6" or "0"); - getglobal(slider:GetName().."High"):SetText(i == 1 and "20" or "30"); + getglobal(slider:GetName().."Low"):SetText(sliderMin[i]); + getglobal(slider:GetName().."High"):SetText(sliderMax[i]); slider:SetScript("OnValueChanged", function() local value = math.floor(this:GetValue() + 0.5); TargetFrameBuffDB[this.key] = value; @@ -75,7 +84,7 @@ local function CreateOptions() local testButton = CreateFrame("Button", "TargetFrameBuffTestButton", panel, "UIPanelButtonTemplate"); testButton:SetWidth(180); testButton:SetHeight(22); - testButton:SetPoint("TOPLEFT", panel, "TOPLEFT", 22, -330); + testButton:SetPoint("TOPLEFT", panel, "TOPLEFT", 22, -450); testButton:SetText("Toggle test auras"); testButton:SetScript("OnClick", function() TargetFrameBuff_SetTest(not TargetFrameBuff_IsTest()); @@ -83,7 +92,7 @@ local function CreateOptions() local saved = {}; local function Sync() for i = 1, 3 do checks[i]:SetChecked(TargetFrameBuffDB[checks[i].key]); end - for i = 1, 2 do + for i = 1, 4 do sliders[i]:SetValue(TargetFrameBuffDB[sliders[i].key]); getglobal(sliders[i]:GetName().."Text"):SetText(sliders[i].label..": "..TargetFrameBuffDB[sliders[i].key]); end @@ -105,6 +114,12 @@ local function CreateOptions() InterfaceOptions_AddCategory(panel); end +-- command -> saved key, minimum, maximum +local NUMBER_OPTIONS = { + size = {"timerSize", 6, 20}, warning = {"warning", 0, 30}, + buffs = {"maxBuffs", 0, 32}, debuffs = {"maxDebuffs", 0, 16} +}; + SLASH_TARGETFRAMEBUFF1 = "/tfb"; SlashCmdList["TARGETFRAMEBUFF"] = function(message) Validate(); @@ -115,14 +130,14 @@ SlashCmdList["TARGETFRAMEBUFF"] = function(message) elseif (value == "") then TargetFrameBuffDB[command] = not TargetFrameBuffDB[command]; else Print("Use on or off."); return; end Print(command..": "..(TargetFrameBuffDB[command] and "on" or "off")); - elseif (command == "size" or command == "warning") then + elseif (NUMBER_OPTIONS[command]) then local number = tonumber(value); - local min, max = 0, 30; - if (command == "size") then min, max = 6, 20; end + local key, min, max = NUMBER_OPTIONS[command][1], NUMBER_OPTIONS[command][2], NUMBER_OPTIONS[command][3]; if (not number or number < min or number > max) then Print("Choose a number from "..min.." to "..max.."."); return; end - TargetFrameBuffDB[command == "size" and "timerSize" or "warning"] = math.floor(number + 0.5); + TargetFrameBuffDB[key] = math.floor(number + 0.5); + Print(command..": "..TargetFrameBuffDB[key]); elseif (command == "test") then local _, _, buffs, debuffs = string.find(value, "^(%d+)%s*(%d*)$"); local on; @@ -139,7 +154,7 @@ SlashCmdList["TARGETFRAMEBUFF"] = function(message) CreateOptions(); InterfaceOptionsFrame_OpenToCategory(panel); else - Print("/tfb timers, highlight or sweeps [on or off], /tfb size 6-20, /tfb warning 0-30, /tfb test [buffs debuffs], /tfb reset"); + Print("/tfb timers, highlight or sweeps [on or off], /tfb size 6-20, /tfb warning 0-30, /tfb buffs 0-32, /tfb debuffs 0-16, /tfb test [buffs debuffs], /tfb reset"); return; end RefreshAuras(); diff --git a/TargetFrameBuff.lua b/TargetFrameBuff.lua index f8138ec..de2ede5 100644 --- a/TargetFrameBuff.lua +++ b/TargetFrameBuff.lua @@ -244,6 +244,8 @@ end -- Stock 1.12 path: UnitBuff/UnitDebuff, no timing data. function TargetFrameBuff_UpdateNative() local button, buff, i; + local maxBuffs = TargetFrameBuff_Setting("maxBuffs"); + local maxDebuffs = TargetFrameBuff_Setting("maxDebuffs"); for i=1, TARGETFRAMEBUFF_MAX_TARGET_BUFFS do if (lTestMode) then buff = nil; @@ -252,7 +254,7 @@ function TargetFrameBuff_UpdateNative() buff = UnitBuff("target", i); end button = lBuffButtons[i]; - if (buff) then + if (buff and i <= maxBuffs) then getglobal("TargetFrameBuff"..i.."Icon"):SetTexture(buff); button:Show(); button.id = i; @@ -274,7 +276,7 @@ function TargetFrameBuff_UpdateNative() debuff, debuffApplications, debuffType = UnitDebuff("target", i); end button = lDebuffButtons[i]; - if (debuff) then + if (debuff and i <= maxDebuffs) then TargetFrameBuff_ColorBorder(i, debuffType); TargetFrameBuff_SetCount(i, debuffApplications); getglobal("TargetFrameDebuff"..i.."Icon"):SetTexture(debuff); @@ -390,6 +392,8 @@ function TargetFrameBuff_UpdateClassicAPI() local pass, filter, slots, n, index, token; local name, icon, count, dispelType, duration, expiration, source, removable; local batched = C_UnitAuras.GetAuraSlots and C_UnitAuras.UnitAuraBySlot; + local maxBuffs = TargetFrameBuff_Setting("maxBuffs"); + local maxDebuffs = TargetFrameBuff_Setting("maxDebuffs"); for pass = 1, 2 do if (pass == 1) then filter, slots = "HELPFUL", lHelpfulSlots; @@ -412,12 +416,15 @@ function TargetFrameBuff_UpdateClassicAPI() name, icon, count, dispelType, duration, expiration, source, removable = C_UnitAuras.UnitAura("target", index, filter); end if (not name and not icon) then break; end - if (pass == 2 and index <= TARGETFRAMEBUFF_MAX_TARGET_DEBUFFS) then + if (pass == 2 and index <= maxDebuffs and index <= TARGETFRAMEBUFF_MAX_TARGET_DEBUFFS) then numDebuffs = index; TargetFrameBuff_ColorBorder(index, dispelType); TargetFrameBuff_SetCount(index, count); TargetFrameBuff_ShowAura(lDebuffButtons[index], filter, index, icon, count, dispelType, duration, expiration, removable); - elseif (numBuffs < TARGETFRAMEBUFF_MAX_TARGET_BUFFS) then + elseif (pass == 2 and maxDebuffs < TARGETFRAMEBUFF_MAX_TARGET_DEBUFFS) then + -- A lowered debuff limit means no debuffs spill into the buff row. + break; + elseif (numBuffs < maxBuffs) then numBuffs = numBuffs + 1; TargetFrameBuff_ShowAura(lBuffButtons[numBuffs], filter, index, icon, count, dispelType, duration, expiration, removable); else diff --git a/TargetFrameBuff.toc b/TargetFrameBuff.toc index b0e02c5..ed04b81 100644 --- a/TargetFrameBuff.toc +++ b/TargetFrameBuff.toc @@ -1,5 +1,5 @@ ## Interface: 1700 -## Title: TargetFrameBuff v0.9.5 +## Title: TargetFrameBuff v0.9.6 ## Notes: Shows up to 32 Buffs & 16 Debuffs on a Target, with countdowns when ClassicAPI is installed ## Author: lua@lumpn.de (modified) ## SavedVariables: TargetFrameBuffDB diff --git a/VALIDATION.md b/VALIDATION.md index f5b1b2b..d3f57eb 100644 --- a/VALIDATION.md +++ b/VALIDATION.md @@ -1,8 +1,8 @@ -# TargetFrameBuff v0.9.5 validation +# TargetFrameBuff v0.9.6 validation -21 simulated Lua 5.1 regression scenarios passed, plus XML parsing. These cover +23 simulated Lua 5.1 regression scenarios passed, plus XML parsing. These cover old/native clients, aura reordering, overflow, settings, sweep lifecycle, delta -refreshes and full updates, the Debuff Timer overlap warning, test mode, target/pet/spellbook changes, reused delta payloads, +refreshes and full updates, the Debuff Timer overlap warning, test mode, buff/debuff limits, target/pet/spellbook changes, reused delta payloads, no polling in delta mode, and native-number/flash suppression. ClassicAPI 1.17.10+ is detected through GetAuraDataByAuraInstanceID, introduced with diff --git a/tests/test_addon.py b/tests/test_addon.py index da789fa..cc42cd9 100644 --- a/tests/test_addon.py +++ b/tests/test_addon.py @@ -253,6 +253,39 @@ assert(TargetFrameBuff_IsTest()) SlashCmdList.TARGETFRAMEBUFF("test off") assert(not TargetFrameBuff_IsTest() and not TargetFrameBuff1:IsShown() and not TargetFrameDebuff1:IsShown()) ''') +run("max buffs and debuffs limit display, no spill when lowered", 'InstallAPI(true)', ''' +for i=1,20 do auras.HELPFUL[i]=Aura("Buff"..i) end +for i=1,18 do auras.HARMFUL[i]=Aura("Debuff"..i) end +Tick() +assert(TargetFrameBuff20:IsShown() and TargetFrameDebuff16:IsShown()) +assert(TargetFrameBuff21Icon.texture=="Debuff17") +SlashCmdList.TARGETFRAMEBUFF("buffs 5"); SlashCmdList.TARGETFRAMEBUFF("debuffs 3") +assert(TargetFrameBuffDB.maxBuffs==5 and TargetFrameBuffDB.maxDebuffs==3) +assert(TargetFrameBuff5:IsShown() and not TargetFrameBuff6:IsShown()) +assert(TargetFrameBuff5Icon.texture=="Buff5") +assert(TargetFrameDebuff3:IsShown() and not TargetFrameDebuff4:IsShown()) +SlashCmdList.TARGETFRAMEBUFF("debuffs 0"); assert(not TargetFrameDebuff1:IsShown()) +SlashCmdList.TARGETFRAMEBUFF("buffs 0"); assert(not TargetFrameBuff1:IsShown()) +SlashCmdList.TARGETFRAMEBUFF("buffs 40"); assert(TargetFrameBuffDB.maxBuffs==0) +SlashCmdList.TARGETFRAMEBUFF("debuffs x"); assert(TargetFrameBuffDB.maxDebuffs==0) +SlashCmdList.TARGETFRAMEBUFF("reset") +assert(TargetFrameBuffDB.maxBuffs==32 and TargetFrameBuffDB.maxDebuffs==16) +assert(TargetFrameBuff21:IsShown() and TargetFrameBuff21Icon.texture=="Debuff17") +''') +run("limits on the native path, corrupt values and options sliders", ''' +nativeBuffs={"A","B","C","D"}; nativeDebuffs={{"X",1,"Magic"},{"Y",1,"Magic"},{"Z",1,"Magic"}} +TargetFrameBuffDB={maxBuffs=-5,maxDebuffs=99} +InterfaceOptions_AddCategory=function(p) registered=p end +InterfaceOptionsFrame_OpenToCategory=function(p) p.refresh() end +''', ''' +Tick() +assert(TargetFrameBuffDB.maxBuffs==32 and TargetFrameBuffDB.maxDebuffs==16) +assert(TargetFrameBuff4:IsShown() and TargetFrameDebuff3:IsShown()) +TargetFrameBuffSlider3:SetValue(2); TargetFrameBuffSlider4:SetValue(1) +assert(TargetFrameBuffDB.maxBuffs==2 and TargetFrameBuffDB.maxDebuffs==1) +assert(TargetFrameBuff2:IsShown() and not TargetFrameBuff3:IsShown()) +assert(TargetFrameDebuff1:IsShown() and not TargetFrameDebuff2:IsShown()) +''') ET.parse(addon/"TargetFrameBuff.xml") print("PASS XML parsing") print(f"{passed} Lua regression scenarios passed; XML parsed.")