From cbf459104bb0b22d53068986dfb7c8e227f0b1bb Mon Sep 17 00:00:00 2001 From: OEVG <950+oevg@noreply.octowow.st> Date: Sun, 1 Nov 2026 00:22:03 +0000 Subject: [PATCH] Add /tfb test mode for previewing target buffs and debuffs - /tfb test [buffs debuffs] and an options-panel button fill the target frame with fake auras (default 32 buffs, 16 debuffs) to check layout, stacks, borders, timers and sweeps. Targets the player if nothing is targeted. - Fake auras restart when they expire so timers keep running. - Works on the native path too (icons, stacks, borders only). - Bump to 0.9.4; tests now 21 scenarios. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_019W3rwutyoLNHxXPycYWSMc --- CHANGELOG.md | 6 +++ README.md | 7 ++++ Settings.lua | 19 ++++++++- TargetFrameBuff.lua | 97 ++++++++++++++++++++++++++++++++++++++++++--- TargetFrameBuff.toc | 2 +- VALIDATION.md | 6 +-- tests/test_addon.py | 27 +++++++++++++ 7 files changed, 153 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a2589a..a21927e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.9.4 + +- Add `/tfb test [buffs debuffs]` and an options-panel button: fake auras on the + current target for checking layout, timers, stacks, borders and sweeps. +- Tests: 21 scenarios. + ## 0.9.3 - Warn once in chat when ShaguTweaks Debuff Timer is drawing on target debuffs. diff --git a/README.md b/README.md index 8df1857..fdd0180 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,17 @@ 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 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 | | Settings are saved and kept even while ClassicAPI is absent. +**Test mode** fills the target frame with fake buffs and debuffs so you can check +layout, borders, stack counts and, with ClassicAPI, timers, sweeps and the +red warning. It targets you if you have no target, restarts expiring fake auras +so timers keep running, and is not saved; `/reload` turns it off. Debuffs past 16 +spill into the buff row, as real ones do. The options panel has a matching button. + ## Compatibility with ShaguTweaks Checked against the upstream [ShaguTweaks](https://github.com/shagu/ShaguTweaks) source. diff --git a/Settings.lua b/Settings.lua index 55fbe20..a15226f 100644 --- a/Settings.lua +++ b/Settings.lua @@ -72,6 +72,14 @@ local function CreateOptions() end); sliders[i] = slider; end + local testButton = CreateFrame("Button", "TargetFrameBuffTestButton", panel, "UIPanelButtonTemplate"); + testButton:SetWidth(180); + testButton:SetHeight(22); + testButton:SetPoint("TOPLEFT", panel, "TOPLEFT", 22, -330); + testButton:SetText("Toggle test auras"); + testButton:SetScript("OnClick", function() + TargetFrameBuff_SetTest(not TargetFrameBuff_IsTest()); + end); local saved = {}; local function Sync() for i = 1, 3 do checks[i]:SetChecked(TargetFrameBuffDB[checks[i].key]); end @@ -115,6 +123,15 @@ SlashCmdList["TARGETFRAMEBUFF"] = function(message) Print("Choose a number from "..min.." to "..max.."."); return; end TargetFrameBuffDB[command == "size" and "timerSize" or "warning"] = math.floor(number + 0.5); + elseif (command == "test") then + local _, _, buffs, debuffs = string.find(value, "^(%d+)%s*(%d*)$"); + local on; + if (value == "off") then on = false; + elseif (value == "" ) then on = not TargetFrameBuff_IsTest(); + elseif (value == "on" or buffs) then on = true; + else Print("Use /tfb test [on|off] or /tfb test ."); return; end + local _, nb, nd = TargetFrameBuff_SetTest(on, tonumber(buffs), tonumber(debuffs)); + Print("test auras: "..(on and ("on ("..nb.." buffs, "..nd.." debuffs). Needs a target; /tfb test turns it off.") or "off")); elseif (command == "reset") then TargetFrameBuffDB = nil; Validate(); @@ -122,7 +139,7 @@ SlashCmdList["TARGETFRAMEBUFF"] = function(message) CreateOptions(); InterfaceOptionsFrame_OpenToCategory(panel); else - Print("/tfb timers|highlight|sweeps [on|off], /tfb size 6-20, /tfb warning 0-30, /tfb reset"); + Print("/tfb timers|highlight|sweeps [on|off], /tfb size 6-20, /tfb warning 0-30, /tfb test [buffs debuffs], /tfb reset"); return; end RefreshAuras(); diff --git a/TargetFrameBuff.lua b/TargetFrameBuff.lua index df55661..f8138ec 100644 --- a/TargetFrameBuff.lua +++ b/TargetFrameBuff.lua @@ -26,6 +26,42 @@ local lHelpfulSlots = {}; local lHarmfulSlots = {}; local lRemovableColor = {r = 1, g = 0.82, b = 0}; +-- Test mode (/tfb test): fake auras on whatever you are targeting, for checking +-- layout, timers, stacks and borders. Lasts until turned off or /reload. +local lTestMode = false; +local lTestBuffs = 32; +local lTestDebuffs = 16; +local lTestStart = 0; +local lTestElapsed = 0; +local TEST_ICONS = { + "Interface\\Icons\\Spell_Holy_WordFortitude", "Interface\\Icons\\Spell_Nature_Regeneration", + "Interface\\Icons\\Spell_Shadow_ShadowWordPain", "Interface\\Icons\\Spell_Fire_FlameBolt", + "Interface\\Icons\\Spell_Frost_FrostArmor02", "Interface\\Icons\\Spell_Nature_Slow", + "Interface\\Icons\\Ability_Rogue_Rupture", "Interface\\Icons\\Spell_Holy_PowerWordShield" +}; +local TEST_TYPES = {"Magic", "Curse", "Disease", "Poison"}; +-- Mixed lengths so seconds, minutes, hours and the red warning all appear. +local TEST_DURATIONS = {10, 18, 45, 90, 300, 900, 1800, 3600, 7200}; + +-- Lua 5.0 has no % operator. +local function TargetFrameBuff_Mod(a, b) + return a - b * math.floor(a / b); +end + +-- Same values as UnitAura: name, icon, count, dispelType, duration, expiration, +-- source, removable. Each aura restarts when its time runs out. +local function TargetFrameBuff_TestAura(pass, index) + local duration = TEST_DURATIONS[TargetFrameBuff_Mod(index * 2, table.getn(TEST_DURATIONS)) + 1]; + local expiration = lTestStart + duration * (math.floor((GetTime() - lTestStart) / duration) + 1); + local count = 1; + if (TargetFrameBuff_Mod(index, 4) == 0) then count = TargetFrameBuff_Mod(index, 7) + 2; end + local dispelType = nil; + if (pass == 2) then dispelType = TEST_TYPES[TargetFrameBuff_Mod(index, 5) + 1]; end + local icon = TEST_ICONS[TargetFrameBuff_Mod(index, table.getn(TEST_ICONS)) + 1]; + return "Test aura "..index, icon, count, dispelType, duration, expiration, "player", + (pass == 1 and TargetFrameBuff_Mod(index, 5) == 0); +end + local function TargetFrameBuff_Setting(key) if (TargetFrameBuffDB and TargetFrameBuffDB[key] ~= nil) then return TargetFrameBuffDB[key]; @@ -49,7 +85,11 @@ local function TargetFrameBuff_AuraOnEnter() TargetFrameBuff_UpdateClassicAPI(); if (not this.auraIndex) then GameTooltip:Hide(); return; end GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT", 15, -25); - GameTooltip:SetUnitAura("target", this.auraIndex, this.auraFilter); + if (lTestMode) then + GameTooltip:SetText("Test "..(this.auraFilter == "HARMFUL" and "debuff " or "buff ")..this.auraIndex); + else + GameTooltip:SetUnitAura("target", this.auraIndex, this.auraFilter); + end GameTooltip:Show(); end @@ -205,7 +245,12 @@ end function TargetFrameBuff_UpdateNative() local button, buff, i; for i=1, TARGETFRAMEBUFF_MAX_TARGET_BUFFS do - buff = UnitBuff("target", i); + if (lTestMode) then + buff = nil; + if (i <= lTestBuffs) then buff = TEST_ICONS[TargetFrameBuff_Mod(i, table.getn(TEST_ICONS)) + 1]; end + else + buff = UnitBuff("target", i); + end button = lBuffButtons[i]; if (buff) then getglobal("TargetFrameBuff"..i.."Icon"):SetTexture(buff); @@ -218,7 +263,16 @@ function TargetFrameBuff_UpdateNative() local debuff, debuffApplications, debuffType; for i=1, TARGETFRAMEBUFF_MAX_TARGET_DEBUFFS do - debuff, debuffApplications, debuffType = UnitDebuff("target", i); + if (lTestMode) then + debuff, debuffApplications, debuffType = nil, nil, nil; + if (i <= lTestDebuffs) then + debuff = TEST_ICONS[TargetFrameBuff_Mod(i, table.getn(TEST_ICONS)) + 1]; + debuffApplications = TargetFrameBuff_Mod(i, 4) == 0 and 3 or 1; + debuffType = TEST_TYPES[TargetFrameBuff_Mod(i, 5) + 1]; + end + else + debuff, debuffApplications, debuffType = UnitDebuff("target", i); + end button = lDebuffButtons[i]; if (debuff) then TargetFrameBuff_ColorBorder(i, debuffType); @@ -320,7 +374,7 @@ local function TargetFrameBuff_ShowAura(button, filter, index, icon, count, disp button.timerText = nil; button.timer:Hide(); end - if (GameTooltip:IsOwned(button)) then + if (GameTooltip:IsOwned(button) and not lTestMode) then GameTooltip:SetUnitAura("target", index, filter); end end @@ -342,13 +396,17 @@ function TargetFrameBuff_UpdateClassicAPI() else filter, slots = "HARMFUL", lHarmfulSlots; end - if (batched) then + if (lTestMode) then + n = (pass == 1) and lTestBuffs or lTestDebuffs; + elseif (batched) then token, n = C_UnitAuras.GetAuraSlots("target", filter, nil, nil, slots); else n = TARGETFRAMEBUFF_MAX_TARGET_BUFFS + TARGETFRAMEBUFF_MAX_TARGET_DEBUFFS; end for index = 1, n do - if (batched) then + if (lTestMode) then + name, icon, count, dispelType, duration, expiration, source, removable = TargetFrameBuff_TestAura(pass, index); + elseif (batched) then name, icon, count, dispelType, duration, expiration, source, removable = C_UnitAuras.UnitAuraBySlot("target", slots[index]); else name, icon, count, dispelType, duration, expiration, source, removable = C_UnitAuras.UnitAura("target", index, filter); @@ -455,6 +513,13 @@ end -- Driven by TargetFrameBuffFrame, which is parented to TargetFrame, so this -- only runs while you have a target. function TargetFrameBuff_OnUpdate(elapsed) + if (lTestMode) then + lTestElapsed = lTestElapsed + (elapsed or 0); + if (lTestElapsed >= 1) then + lTestElapsed = 0; + TargetFrameBuff_Update(); + end + end if (not lBuffButtons[1] or not lBuffButtons[1].timer or not TargetFrameBuff_HasClassicAPI()) then return; end @@ -500,3 +565,23 @@ function TargetFrameBuff_Restore() end end end + + +-- Turn test mode on or off. buffs (0-32) and debuffs (0-32) are optional; debuffs +-- past 16 spill into the buff row, as real ones do with ClassicAPI. +function TargetFrameBuff_SetTest(enabled, buffs, debuffs) + lTestMode = enabled and true or false; + if (buffs) then lTestBuffs = math.max(0, math.min(TARGETFRAMEBUFF_MAX_TARGET_BUFFS, math.floor(buffs))); end + if (debuffs) then lTestDebuffs = math.max(0, math.min(TARGETFRAMEBUFF_MAX_TARGET_BUFFS, math.floor(debuffs))); end + lTestStart = GetTime(); + lTestElapsed = 0; + if (lTestMode and UnitExists and TargetUnit and not UnitExists("target")) then + TargetUnit("player"); + end + TargetFrameBuff_Update(); + return lTestMode, lTestBuffs, lTestDebuffs; +end + +function TargetFrameBuff_IsTest() + return lTestMode; +end diff --git a/TargetFrameBuff.toc b/TargetFrameBuff.toc index 7c39eee..93baaf6 100644 --- a/TargetFrameBuff.toc +++ b/TargetFrameBuff.toc @@ -1,5 +1,5 @@ ## Interface: 1700 -## Title: TargetFrameBuff v0.9.3 +## Title: TargetFrameBuff v0.9.4 ## 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 e3ad39f..fb17b62 100644 --- a/VALIDATION.md +++ b/VALIDATION.md @@ -1,8 +1,8 @@ -# TargetFrameBuff v0.9.3 validation +# TargetFrameBuff v0.9.4 validation -19 simulated Lua 5.1 regression scenarios passed, plus XML parsing. These cover +21 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, target/pet/spellbook changes, reused delta payloads, +refreshes and full updates, the Debuff Timer overlap warning, test mode, 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 b2b7909..da789fa 100644 --- a/tests/test_addon.py +++ b/tests/test_addon.py @@ -226,6 +226,33 @@ TargetFrameDebuff1.cd={readable={}} Tick(); Tick() for _,m in ipairs(messages) do assert(not string.find(m,"Debuff Timer",1,true)) end ''') +run("test mode fills all 32 buffs and 16 debuffs, then restores real auras", 'InstallAPI(true); auras.HELPFUL={Aura("Real")}', ''' +Tick(); assert(TargetFrameBuff1Icon.texture=="Real" and not TargetFrameBuff2:IsShown()) +SlashCmdList.TARGETFRAMEBUFF("test") +assert(TargetFrameBuff_IsTest()) +for i=1,32 do assert(_G["TargetFrameBuff"..i]:IsShown() and _G["TargetFrameBuff"..i].timer.text) end +for i=1,16 do assert(_G["TargetFrameDebuff"..i]:IsShown() and _G["TargetFrameDebuff"..i].timer.text) end +assert(TargetFrameBuff5.auraBorder:IsShown() and not TargetFrameBuff6.auraBorder:IsShown()) +assert(TargetFrameBuff4.auraCount.text==6) +this=TargetFrameBuff3; this.scripts.OnEnter() +assert(GameTooltip.tooltip==nil) +now=now+500; Tick(); TargetFrameBuff_OnUpdate(1) +for i=1,32 do assert(_G["TargetFrameBuff"..i]:IsShown()) end +SlashCmdList.TARGETFRAMEBUFF("test") +assert(not TargetFrameBuff_IsTest()) +assert(TargetFrameBuff1Icon.texture=="Real" and not TargetFrameBuff2:IsShown()) +''') +run("test mode custom counts, clamping, native path and bad input", 'nativeBuffs={}; nativeDebuffs={}', ''' +SlashCmdList.TARGETFRAMEBUFF("test 3 5") +assert(TargetFrameBuff3:IsShown() and not TargetFrameBuff4:IsShown()) +assert(TargetFrameDebuff5:IsShown() and not TargetFrameDebuff6:IsShown()) +SlashCmdList.TARGETFRAMEBUFF("test 40 99") +assert(TargetFrameBuff32:IsShown() and TargetFrameDebuff16:IsShown()) +SlashCmdList.TARGETFRAMEBUFF("test banana") +assert(TargetFrameBuff_IsTest()) +SlashCmdList.TARGETFRAMEBUFF("test off") +assert(not TargetFrameBuff_IsTest() and not TargetFrameBuff1:IsShown() and not TargetFrameDebuff1:IsShown()) +''') ET.parse(addon/"TargetFrameBuff.xml") print("PASS XML parsing") print(f"{passed} Lua regression scenarios passed; XML parsed.")