Apply Patch

This commit is contained in:
2026-10-30 16:43:11 +00:00
parent 3a23425849
commit bb7b1fee21
7 changed files with 1256 additions and 0 deletions
+140
View File
@@ -0,0 +1,140 @@
-- Settings remain usable through /tfb on unmodified 1.12 clients.
TargetFrameBuffDefaults = {
timers = true, highlight = true, sweeps = false, timerSize = 9, warning = 5
};
local function RefreshAuras()
if (TargetFrameBuff_Update and TargetFrameBuff1) then
TargetFrameBuff_Update();
end
end
local function Print(message)
DEFAULT_CHAT_FRAME:AddMessage("|cffffd100TargetFrameBuff:|r "..message);
end
local function Validate()
if (type(TargetFrameBuffDB) ~= "table") then TargetFrameBuffDB = {}; end
for key, value in pairs(TargetFrameBuffDefaults) do
if (type(TargetFrameBuffDB[key]) ~= type(value)) then TargetFrameBuffDB[key] = value; end
end
if (TargetFrameBuffDB.timerSize ~= TargetFrameBuffDB.timerSize or TargetFrameBuffDB.timerSize < 6 or TargetFrameBuffDB.timerSize > 20) then
TargetFrameBuffDB.timerSize = 9;
end
if (TargetFrameBuffDB.warning ~= TargetFrameBuffDB.warning or TargetFrameBuffDB.warning < 0 or TargetFrameBuffDB.warning > 30) then
TargetFrameBuffDB.warning = 5;
end
end
local panel;
local function CreateOptions()
if (panel or not InterfaceOptions_AddCategory) then return; end
panel = CreateFrame("Frame", "TargetFrameBuffOptions", UIParent);
panel.name = "TargetFrameBuff";
local title = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge");
title:SetPoint("TOPLEFT", panel, "TOPLEFT", 16, -16);
title:SetText("TargetFrameBuff");
local note = panel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall");
note:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -12);
note:SetText("ClassicAPI features. Sweeps work best with ClassicAPI 1.17.1 or newer.");
local checks = {};
local labels = {"Show countdown text", "Highlight enemy buffs you can remove", "Show cooldown sweeps"};
local keys = {"timers", "highlight", "sweeps"};
for i = 1, 3 do
local check = CreateFrame("CheckButton", "TargetFrameBuffOption"..i, panel, "UICheckButtonTemplate");
check:SetPoint("TOPLEFT", panel, "TOPLEFT", 16, -65 - (i - 1) * 32);
check.key = keys[i];
getglobal(check:GetName().."Text"):SetText(labels[i]);
check:SetScript("OnClick", function()
TargetFrameBuffDB[this.key] = this:GetChecked() and true or false;
RefreshAuras();
end);
checks[i] = check;
end
local sliders = {};
local sliderKeys = {"timerSize", "warning"};
local sliderLabels = {"Timer font size", "Warning threshold (seconds)"};
for i = 1, 2 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: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");
slider:SetScript("OnValueChanged", function()
local value = math.floor(this:GetValue() + 0.5);
TargetFrameBuffDB[this.key] = value;
getglobal(this:GetName().."Text"):SetText(this.label..": "..value);
RefreshAuras();
end);
sliders[i] = slider;
end
local saved = {};
local function Sync()
for i = 1, 3 do checks[i]:SetChecked(TargetFrameBuffDB[checks[i].key]); end
for i = 1, 2 do
sliders[i]:SetValue(TargetFrameBuffDB[sliders[i].key]);
getglobal(sliders[i]:GetName().."Text"):SetText(sliders[i].label..": "..TargetFrameBuffDB[sliders[i].key]);
end
end
panel.refresh = function()
for key in pairs(TargetFrameBuffDefaults) do saved[key] = TargetFrameBuffDB[key]; end
Sync();
end;
panel.cancel = function()
for key, value in pairs(saved) do TargetFrameBuffDB[key] = value; end
Sync();
RefreshAuras();
end;
panel.default = function()
for key, value in pairs(TargetFrameBuffDefaults) do TargetFrameBuffDB[key] = value; end
Sync();
RefreshAuras();
end;
InterfaceOptions_AddCategory(panel);
end
SLASH_TARGETFRAMEBUFF1 = "/tfb";
SlashCmdList["TARGETFRAMEBUFF"] = function(message)
Validate();
local _, _, command, value = string.find(string.lower(message or ""), "^%s*(%S*)%s*(.-)%s*$");
if (command == "timers" or command == "highlight" or command == "sweeps") then
if (value == "on") then TargetFrameBuffDB[command] = true;
elseif (value == "off") then TargetFrameBuffDB[command] = false;
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
local number = tonumber(value);
local min, max = 0, 30;
if (command == "size") then min, max = 6, 20; end
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);
elseif (command == "reset") then
TargetFrameBuffDB = nil;
Validate();
elseif (command == "" and InterfaceOptionsFrame_OpenToCategory) then
CreateOptions();
InterfaceOptionsFrame_OpenToCategory(panel);
else
Print("/tfb timers|highlight|sweeps [on|off], /tfb size 6-20, /tfb warning 0-30, /tfb reset");
return;
end
RefreshAuras();
end;
local loader = CreateFrame("Frame");
loader:RegisterEvent("ADDON_LOADED");
loader:SetScript("OnEvent", function()
if (arg1 == "TargetFrameBuff") then
Validate();
CreateOptions();
RefreshAuras();
this:UnregisterEvent("ADDON_LOADED");
end
end);
+494
View File
@@ -0,0 +1,494 @@
-- Title: TargetFrameBuff v0.9.2
-- Notes: Shows up to 32 Buffs & 16 Debuffs on a Target (Buffs above, Debuffs below),
-- with remaining-time countdowns when ClassicAPI is installed
-- Author: lua@lumpn.de (modified)
local lOriginal_TargetDebuffButton_Update = nil;
local TARGETFRAMEBUFF_MAX_TARGET_DEBUFFS = 16;
local TARGETFRAMEBUFF_MAX_TARGET_BUFFS = 32;
-- How often the countdowns refresh, in seconds.
local TARGETFRAMEBUFF_TIMER_INTERVAL = 0.2;
local lBuffButtons = {};
local lDebuffButtons = {};
local lTimerElapsed = 0;
local lAuraElapsed = 0;
local lHasAuraDelta = false;
local lActiveTimers = {};
local lActiveTimerCount = 0;
local TargetFrameBuff_UpdateTimer;
-- Catch refreshes that change timing without a UNIT_AURA event.
local TARGETFRAMEBUFF_AURA_INTERVAL = 0.5;
local lHelpfulSlots = {};
local lHarmfulSlots = {};
local lRemovableColor = {r = 1, g = 0.82, b = 0};
local function TargetFrameBuff_Setting(key)
if (TargetFrameBuffDB and TargetFrameBuffDB[key] ~= nil) then
return TargetFrameBuffDB[key];
end
return TargetFrameBuffDefaults[key];
end
-- ClassicAPI (a 1.12 client DLL) backports C_UnitAuras, which reports when
-- each aura on the target expires. Without it, the addon works as before,
-- just without countdowns.
local function TargetFrameBuff_HasClassicAPI()
return C_UnitAuras and C_UnitAuras.UnitAura and GameTooltip.SetUnitAura and true;
end
-- Tooltip for an aura read through ClassicAPI. auraIndex and auraFilter are in
-- ClassicAPI's HELPFUL/HARMFUL index space, so the tooltip always matches the
-- icon, even for a debuff the server parked in a buff slot.
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);
GameTooltip:Show();
end
-- Add a countdown label to a button. It sits on its own child frame, one level
-- above the button, so it draws over the icon and the debuff border.
local function TargetFrameBuff_AddTimer(button)
local holder = CreateFrame("Frame", nil, button);
holder:SetAllPoints(button);
holder:SetFrameLevel(button:GetFrameLevel() + 2);
local timer = holder:CreateFontString(nil, "OVERLAY");
timer:SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE");
timer:SetPoint("CENTER", button, "CENTER", 0, 0);
timer:Hide();
if (not button.auraCount) then
button.auraCount = holder:CreateFontString(nil, "OVERLAY");
button.auraCount:SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE");
button.auraCount:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 1, -1);
button.auraCount:Hide();
local stockCount = getglobal(button:GetName().."Count");
if (stockCount) then stockCount:Hide(); end
end
button.timer = timer;
button.timerText = nil;
button.expiration = 0;
end
-- hook original update-function
function TargetFrameBuff_OnLoad()
local i, button, overlay;
for i=1, TARGETFRAMEBUFF_MAX_TARGET_BUFFS do
lBuffButtons[i] = getglobal("TargetFrameBuff"..i);
button = lBuffButtons[i];
overlay = CreateFrame("Frame", nil, button);
overlay:SetAllPoints(button);
overlay:SetFrameLevel(button:GetFrameLevel() + 2);
button.auraCount = overlay:CreateFontString(nil, "OVERLAY");
button.auraCount:SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE");
button.auraCount:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 1, -1);
button.auraCount:Hide();
button.auraBorder = overlay:CreateTexture(nil, "OVERLAY");
button.auraBorder:SetTexture("Interface\\Buttons\\UI-Debuff-Overlays");
button.auraBorder:SetTexCoord(0.296875, 0.5703125, 0, 0.515625);
button.auraBorder:SetPoint("CENTER", button, "CENTER", 0, 0);
button.auraBorder:SetWidth(23);
button.auraBorder:SetHeight(23);
button.auraBorder:Hide();
end
for i=1, TARGETFRAMEBUFF_MAX_TARGET_DEBUFFS do
lDebuffButtons[i] = getglobal("TargetFrameDebuff"..i);
end
if (TargetFrameBuff_HasClassicAPI()) then
for i=1, TARGETFRAMEBUFF_MAX_TARGET_BUFFS do
button = lBuffButtons[i];
TargetFrameBuff_AddTimer(button);
button:SetScript("OnEnter", TargetFrameBuff_AuraOnEnter);
button:SetScript("OnUpdate", nil);
end
for i=1, TARGETFRAMEBUFF_MAX_TARGET_DEBUFFS do
button = lDebuffButtons[i];
TargetFrameBuff_AddTimer(button);
button:SetScript("OnEnter", TargetFrameBuff_AuraOnEnter);
button:SetScript("OnUpdate", nil);
end
end
-- The instance lookup was introduced alongside UNIT_AURA_DELTA. Gate the
-- custom registration so older clients never register an unknown event.
lHasAuraDelta = TargetFrameBuff_HasClassicAPI() and C_UnitAuras.GetAuraDataByAuraInstanceID and true or false;
if (lHasAuraDelta) then TargetFrameBuffFrame:RegisterEvent("UNIT_AURA_DELTA"); end
TargetFrameBuffFrame:RegisterEvent("UNIT_AURA");
TargetFrameBuffFrame:RegisterEvent("SPELLS_CHANGED");
TargetFrameBuffFrame:RegisterEvent("UNIT_PET");
TargetFrameBuffFrame:RegisterEvent("PLAYER_TARGET_CHANGED");
TargetFrameBuffFrame:SetScript("OnEvent", function()
if (event == "PLAYER_TARGET_CHANGED" or event == "SPELLS_CHANGED"
or (event == "UNIT_PET" and arg1 == "player")
or (event == "UNIT_AURA_DELTA" and arg1 == "target")
or (not lHasAuraDelta and event == "UNIT_AURA" and arg1 == "target")) then
-- Rebuild the small display as one snapshot. Do not retain the delta's
-- reused payload tables; removals can also reorder all filtered indices.
TargetFrameBuff_Update();
end
end);
lOriginal_TargetDebuffButton_Update = TargetDebuffButton_Update;
TargetDebuffButton_Update = TargetFrameBuff_Update;
lOriginal_TargetDebuffButton_Update();
TargetFrameBuff_Restore();
TargetFrameBuff_Update();
end
-- use extended update-function (original code from FrameXML/TargetFrame.lua)
function TargetFrameBuff_Update()
if (TargetFrameBuff_HasClassicAPI() and lBuffButtons[1] and lBuffButtons[1].timer) then
TargetFrameBuff_UpdateClassicAPI();
else
TargetFrameBuff_UpdateNative();
end
TargetFrameBuff_Layout();
-- Draw countdowns right away instead of waiting for the next tick.
lTimerElapsed = TARGETFRAMEBUFF_TIMER_INTERVAL;
end
-- Color a debuff border by debuff type (Magic, Curse, Disease, Poison).
-- Without this, a border keeps the color of whatever debuff last used that
-- slot, so debuffs show the wrong category.
local function TargetFrameBuff_ColorBorder(i, debuffType)
local debuffBorder = getglobal("TargetFrameDebuff"..i.."Border");
if (debuffBorder and DebuffTypeColor) then
local color = DebuffTypeColor[debuffType or "none"] or DebuffTypeColor["none"];
if (color) then
debuffBorder:SetVertexColor(color.r, color.g, color.b);
end
end
end
local function TargetFrameBuff_SetCount(i, count)
local stockCount = getglobal("TargetFrameDebuff"..i.."Count");
local debuffCount = lDebuffButtons[i].auraCount or stockCount;
if (debuffCount ~= stockCount) then stockCount:Hide(); end
if (count and count > 1) then
debuffCount:SetText(count);
debuffCount:Show();
else
debuffCount:Hide();
end
end
-- Stock 1.12 path: UnitBuff/UnitDebuff, no timing data.
function TargetFrameBuff_UpdateNative()
local button, buff, i;
for i=1, TARGETFRAMEBUFF_MAX_TARGET_BUFFS do
buff = UnitBuff("target", i);
button = lBuffButtons[i];
if (buff) then
getglobal("TargetFrameBuff"..i.."Icon"):SetTexture(buff);
button:Show();
button.id = i;
else
button:Hide();
end
end
local debuff, debuffApplications, debuffType;
for i=1, TARGETFRAMEBUFF_MAX_TARGET_DEBUFFS do
debuff, debuffApplications, debuffType = UnitDebuff("target", i);
button = lDebuffButtons[i];
if (debuff) then
TargetFrameBuff_ColorBorder(i, debuffType);
TargetFrameBuff_SetCount(i, debuffApplications);
getglobal("TargetFrameDebuff"..i.."Icon"):SetTexture(debuff);
button:Show();
button.id = i;
else
button:Hide();
end
end
end
-- Slot ids are used only during this scan, never cached on buttons.
local function TargetFrameBuff_Sweep(button, duration, expiration)
local enabled = TargetFrameBuff_Setting("sweeps");
if (enabled and not button.cooldown and not button.noCooldown) then
local ok, cd = pcall(CreateFrame, "Cooldown", nil, button, "CooldownFrameTemplate");
if (ok and cd and cd.SetCooldown) then
button.cooldown = cd;
cd:SetAllPoints(button);
cd:SetFrameLevel(button:GetFrameLevel() + 1);
cd:SetReverse(true);
cd:SetDrawEdge(true);
if (cd.SetHideCountdownNumbers) then cd:SetHideCountdownNumbers(true); end
if (cd.SetDrawBling) then cd:SetDrawBling(false); end
cd:Hide();
else
button.noCooldown = true;
end
end
local cd = button.cooldown;
if (not cd) then return; end
if (not enabled or not duration or duration <= 0 or not expiration or expiration <= GetTime()) then
if (cd.Clear) then cd:Clear(); else cd:Hide(); end
button.sweepExpiration = nil;
elseif (button.sweepExpiration ~= expiration or button.sweepDuration ~= duration) then
cd:SetCooldown(expiration - duration, duration);
button.sweepExpiration = expiration;
button.sweepDuration = duration;
end
end
local function TargetFrameBuff_HideAura(button)
button.expiration = 0;
button.auraIndex = nil;
button.auraFilter = nil;
button.timerText = nil;
button.timer:Hide();
if (button.cooldown) then
if (button.cooldown.Clear) then button.cooldown:Clear(); else button.cooldown:Hide(); end
end
button.sweepExpiration = nil;
if (button.auraCount) then button.auraCount:Hide(); end
if (button.auraBorder) then button.auraBorder:Hide(); end
if (GameTooltip:IsOwned(button)) then GameTooltip:Hide(); end
button:Hide();
end
local function TargetFrameBuff_ShowAura(button, filter, index, icon, count, dispelType, duration, expiration, removable)
if (button.auraIcon ~= icon) then
getglobal(button:GetName().."Icon"):SetTexture(icon);
button.auraIcon = icon;
end
button.auraFilter = filter;
button.auraIndex = index;
button.id = index;
button.expiration = expiration or 0;
if (button.auraCount) then
if (count and count > 1) then
button.auraCount:SetText(count);
button.auraCount:Show();
else
button.auraCount:Hide();
end
end
if (button.auraBorder) then
local color;
if (filter == "HARMFUL") then
color = DebuffTypeColor[dispelType or "none"] or DebuffTypeColor["none"];
elseif (removable and TargetFrameBuff_Setting("highlight")) then
color = lRemovableColor;
end
if (color) then
button.auraBorder:SetVertexColor(color.r, color.g, color.b);
button.auraBorder:Show();
else
button.auraBorder:Hide();
end
end
TargetFrameBuff_Sweep(button, duration, expiration);
button:Show();
if (TargetFrameBuff_Setting("timers") and button.expiration > GetTime()) then
lActiveTimerCount = lActiveTimerCount + 1;
lActiveTimers[lActiveTimerCount] = button;
TargetFrameBuff_UpdateTimer(button, GetTime());
else
button.timerText = nil;
button.timer:Hide();
end
if (GameTooltip:IsOwned(button)) then
GameTooltip:SetUnitAura("target", index, filter);
end
end
-- Refresh icons, counts and expiration together so index reordering cannot
-- attach a new aura's timer to an old icon. Older ClassicAPI uses the same
-- presentation path, with its positional index getter.
function TargetFrameBuff_UpdateClassicAPI()
local oldTimerCount = lActiveTimerCount;
lActiveTimerCount = 0;
lAuraElapsed = 0;
local numBuffs, numDebuffs = 0, 0;
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;
for pass = 1, 2 do
if (pass == 1) then
filter, slots = "HELPFUL", lHelpfulSlots;
else
filter, slots = "HARMFUL", lHarmfulSlots;
end
if (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
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);
end
if (not name and not icon) then break; end
if (pass == 2 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
numBuffs = numBuffs + 1;
TargetFrameBuff_ShowAura(lBuffButtons[numBuffs], filter, index, icon, count, dispelType, duration, expiration, removable);
else
break;
end
end
end
for index = numBuffs + 1, TARGETFRAMEBUFF_MAX_TARGET_BUFFS do
TargetFrameBuff_HideAura(lBuffButtons[index]);
end
for index = numDebuffs + 1, TARGETFRAMEBUFF_MAX_TARGET_DEBUFFS do
TargetFrameBuff_HideAura(lDebuffButtons[index]);
end
for index = lActiveTimerCount + 1, oldTimerCount do
lActiveTimers[index] = nil;
end
end
function TargetFrameBuff_Layout()
local button;
-- Position buffs above target frame
TargetFrameBuff1:ClearAllPoints();
TargetFrameBuff1:SetPoint("BOTTOMLEFT", "TargetFrame", "TOPLEFT", 5, -10);
-- Position debuffs below target frame using default positioning (no gap)
-- Layout: 4-4-8 (three rows: 4 debuffs, 4 debuffs, 8 debuffs)
-- Debuff 1 uses default WoW positioning to avoid gap with target frame
-- TargetFrameDebuff1:ClearAllPoints();
-- TargetFrameDebuff1:SetPoint("TOPLEFT", "TargetFrame", "BOTTOMLEFT", 5, 0);
-- Position remaining buttons in 4-4-8 layout
for i=2, TARGETFRAMEBUFF_MAX_TARGET_DEBUFFS do
button = lDebuffButtons[i];
if button then
button:ClearAllPoints();
if i == 5 then
-- Row 2: Start below button 1
button:SetPoint("TOPLEFT", "TargetFrameDebuff1", "BOTTOMLEFT", 0, -2);
elseif i == 9 then
-- Row 3: Start below button 5
button:SetPoint("TOPLEFT", "TargetFrameDebuff5", "BOTTOMLEFT", 0, -2);
else
-- Position horizontally relative to previous button
button:SetPoint("LEFT", "TargetFrameDebuff"..(i-1), "RIGHT", 3, 0);
end
end
end
end
-- "1h", "12m", "45", ... rounded up, so a countdown never shows 0 while
-- the aura is still up.
local function TargetFrameBuff_FormatTime(remaining)
if (remaining >= 3600) then
return math.ceil(remaining / 3600).."h";
elseif (remaining >= 60) then
return math.ceil(remaining / 60).."m";
end
return tostring(math.ceil(remaining));
end
TargetFrameBuff_UpdateTimer = function(button, now)
if (not button:IsShown() or not button.auraIndex) then
return;
end
local size = TargetFrameBuff_Setting("timerSize");
if (button.timerSize ~= size) then
button.timer:SetFont("Fonts\\FRIZQT__.TTF", size, "OUTLINE");
button.timerSize = size;
end
local remaining = button.expiration - now;
if (not TargetFrameBuff_Setting("timers") or button.expiration <= 0 or remaining <= 0) then
if (button.timerText) then
button.timerText = nil;
button.timer:Hide();
end
return;
end
local text = TargetFrameBuff_FormatTime(remaining);
if (text ~= button.timerText) then
button.timerText = text;
button.timer:SetText(text);
button.timer:Show();
end
if (remaining <= TargetFrameBuff_Setting("warning")) then
button.timer:SetTextColor(1, 0.1, 0.1);
else
button.timer:SetTextColor(1, 1, 1);
end
end
-- Driven by TargetFrameBuffFrame, which is parented to TargetFrame, so this
-- only runs while you have a target.
function TargetFrameBuff_OnUpdate(elapsed)
if (not lBuffButtons[1] or not lBuffButtons[1].timer or not TargetFrameBuff_HasClassicAPI()) then
return;
end
elapsed = elapsed or 0;
lAuraElapsed = lAuraElapsed + elapsed;
if (not lHasAuraDelta and lAuraElapsed >= TARGETFRAMEBUFF_AURA_INTERVAL) then
TargetFrameBuff_UpdateClassicAPI();
end
if (lActiveTimerCount == 0) then
lTimerElapsed = 0;
return;
end
lTimerElapsed = lTimerElapsed + elapsed;
if (lTimerElapsed < TARGETFRAMEBUFF_TIMER_INTERVAL) then return; end
lTimerElapsed = 0;
local now = GetTime();
local i = lActiveTimerCount;
while (i >= 1) do
local button = lActiveTimers[i];
TargetFrameBuff_UpdateTimer(button, now);
if (not button:IsShown() or button.expiration <= now or not TargetFrameBuff_Setting("timers")) then
lActiveTimers[i] = lActiveTimers[lActiveTimerCount];
lActiveTimers[lActiveTimerCount] = nil;
lActiveTimerCount = lActiveTimerCount - 1;
end
i = i - 1;
end
end
function TargetFrameBuff_Restore()
-- Resize debuffs to full size
local button, debuffFrame;
for i=1, TARGETFRAMEBUFF_MAX_TARGET_DEBUFFS do
button = getglobal("TargetFrameDebuff"..i);
if button then
debuffFrame = getglobal("TargetFrameDebuff"..i.."Border");
button:SetWidth(21);
button:SetHeight(21);
if debuffFrame then
debuffFrame:SetWidth(23);
debuffFrame:SetHeight(23);
end
end
end
end
+8
View File
@@ -0,0 +1,8 @@
## Interface: 1700
## Title: TargetFrameBuff v0.9.2
## Notes: Shows up to 32 Buffs & 16 Debuffs on a Target, with countdowns when ClassicAPI is installed
## Author: lua@lumpn.de (modified)
## SavedVariables: TargetFrameBuffDB
## OptionalDeps: !!!ClassicAPI
## Dependencies:
TargetFrameBuff.xml
+266
View File
@@ -0,0 +1,266 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="Settings.lua"/>
<Script file="TargetFrameBuff.lua"/>
<!-- 11 additional buff buttons (original code from FrameXML/TargetFrame.xml) -->
<Button name="TargetFrameBuff6" inherits="TargetBuffButtonTemplate" id="6" parent="TargetFrame">
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="TargetFrameBuff1" relativePoint="TOPLEFT">
<Offset>
<AbsDimension x="0" y="2"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff7" inherits="TargetBuffButtonTemplate" id="7" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff6" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff8" inherits="TargetBuffButtonTemplate" id="8" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff7" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff9" inherits="TargetBuffButtonTemplate" id="9" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff8" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff10" inherits="TargetBuffButtonTemplate" id="10" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff9" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff11" inherits="TargetBuffButtonTemplate" id="11" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff10" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff12" inherits="TargetBuffButtonTemplate" id="12" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff11" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff13" inherits="TargetBuffButtonTemplate" id="13" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff12" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff14" inherits="TargetBuffButtonTemplate" id="14" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff13" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff15" inherits="TargetBuffButtonTemplate" id="15" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff14" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff16" inherits="TargetBuffButtonTemplate" id="16" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff15" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<!-- 16 more buff buttons for a total of 32: row 3 (17-27) and row 4 (28-32) -->
<Button name="TargetFrameBuff17" inherits="TargetBuffButtonTemplate" id="17" parent="TargetFrame">
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="TargetFrameBuff6" relativePoint="TOPLEFT">
<Offset>
<AbsDimension x="0" y="2"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff18" inherits="TargetBuffButtonTemplate" id="18" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff17" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff19" inherits="TargetBuffButtonTemplate" id="19" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff18" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff20" inherits="TargetBuffButtonTemplate" id="20" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff19" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff21" inherits="TargetBuffButtonTemplate" id="21" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff20" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff22" inherits="TargetBuffButtonTemplate" id="22" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff21" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff23" inherits="TargetBuffButtonTemplate" id="23" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff22" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff24" inherits="TargetBuffButtonTemplate" id="24" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff23" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff25" inherits="TargetBuffButtonTemplate" id="25" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff24" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff26" inherits="TargetBuffButtonTemplate" id="26" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff25" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff27" inherits="TargetBuffButtonTemplate" id="27" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff26" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff28" inherits="TargetBuffButtonTemplate" id="28" parent="TargetFrame">
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="TargetFrameBuff17" relativePoint="TOPLEFT">
<Offset>
<AbsDimension x="0" y="2"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff29" inherits="TargetBuffButtonTemplate" id="29" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff28" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff30" inherits="TargetBuffButtonTemplate" id="30" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff29" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff31" inherits="TargetBuffButtonTemplate" id="31" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff30" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="TargetFrameBuff32" inherits="TargetBuffButtonTemplate" id="32" parent="TargetFrame">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetFrameBuff31" relativePoint="RIGHT">
<Offset>
<AbsDimension x="3" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<!-- Defined after the buttons so they all exist when OnLoad runs -->
<Frame name="TargetFrameBuffFrame" parent="TargetFrame">
<Scripts>
<OnLoad>
TargetFrameBuff_OnLoad();
</OnLoad>
<OnUpdate>
TargetFrameBuff_OnUpdate(arg1);
</OnUpdate>
</Scripts>
</Frame>
</Ui>
+19
View File
@@ -0,0 +1,19 @@
# TargetFrameBuff v0.9.2 validation
17 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, 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
UNIT_AURA_DELTA. Each relevant delta triggers a full display snapshot rather than
retaining event tables or maintaining a second aura cache. This preserves sorted
indices for icon/tooltip alignment. Older ClassicAPI polls every 0.5 seconds.
Countdown text remains on its existing 0.2-second active-only updater.
The native countdown is explicitly hidden per sweep; no global CVar is changed.
New Clear and SetDrawBling methods are optional, preserving older versions.
Not tested inside a running WoW client. Verify recasts, target switches, auras
expiring, and sweeps with countdownForCooldowns enabled. Keep ShaguTweaks Debuff
Timer disabled to prevent overlapping addon countdowns. No FPS gain is claimed.
+110
View File
@@ -0,0 +1,110 @@
unpack = unpack or table.unpack
getglobal = function(n) return _G[n] end
now = 100
GetTime = function() return now end
SlashCmdList = {}
messages = {}
DEFAULT_CHAT_FRAME = {AddMessage=function(self,s) table.insert(messages,s) end}
DebuffTypeColor = {none={r=.8,g=0,b=0}, Magic={r=0,g=.5,b=1}, Poison={r=0,g=1,b=0}}
frames = {}
local M = {}
function M:GetName() return self.name end
function M:SetAllPoints(x) self.allPoints=x end
function M:GetFrameLevel() return self.level or 1 end
function M:SetFrameLevel(n) self.level=n end
function M:SetFont(path,size,flags) self.font=path; self.fontSize=size end
function M:SetPoint(...) self.point={...} end
function M:ClearAllPoints() self.point=nil end
function M:Show() self.shown=true end
function M:Hide() self.shown=false end
function M:IsShown() return self.shown end
function M:SetWidth(v) self.width=v end
function M:SetHeight(v) self.height=v end
function M:SetText(v) self.text=v end
function M:SetTextColor(...) self.color={...} end
function M:SetVertexColor(...) self.color={...} end
function M:SetTexture(v) self.texture=v end
function M:SetTexCoord(...) self.coords={...} end
function M:SetScript(k,v) self.scripts[k]=v end
function M:RegisterEvent(k) self.events[k]=true end
function M:UnregisterEvent(k) self.events[k]=nil end
function M:SetChecked(v) self.checked=v end
function M:GetChecked() return self.checked end
function M:SetMinMaxValues(a,b) self.min=a; self.max=b end
function M:SetValueStep(v) self.step=v end
function M:SetValue(v)
self.value=v
if self.scripts.OnValueChanged then local old=this; this=self; self.scripts.OnValueChanged(); this=old end
end
function M:GetValue() return self.value end
function M:SetCooldown(start,duration) self.start=start; self.duration=duration; self.calls=(self.calls or 0)+1; self:Show() end
function M:SetReverse(v) self.reverse=v end
function M:SetDrawEdge(v) self.edge=v end
function M:IsOwned(button) return self.owner==button end
function M:SetOwner(button) self.owner=button end
function M:SetUnitAura(unit,index,filter) self.tooltip={unit,index,filter} end
local function New(name)
local f=setmetatable({name=name,shown=true,scripts={},events={}}, {__index=M})
if name then _G[name]=f end
return f
end
function M:CreateFontString(name) return New(name) end
function M:CreateTexture(name) return New(name) end
function CreateFrame(kind,name,parent,template)
if kind=="Cooldown" and noCooldown then error("Unknown frame type") end
local f=New(name); f.parent=parent; f.kind=kind
table.insert(frames,f)
if template=="UICheckButtonTemplate" then New(name.."Text") end
if template=="OptionsSliderTemplate" then New(name.."Text"); New(name.."Low"); New(name.."High") end
return f
end
UIParent=New("UIParent")
GameTooltip=New("GameTooltip")
TargetFrame=New("TargetFrame")
TargetFrameBuffFrame=New("TargetFrameBuffFrame")
for i=1,32 do New("TargetFrameBuff"..i); New("TargetFrameBuff"..i.."Icon") end
for i=1,16 do New("TargetFrameDebuff"..i); New("TargetFrameDebuff"..i.."Icon"); New("TargetFrameDebuff"..i.."Count"); New("TargetFrameDebuff"..i.."Border") end
TargetDebuffButton_Update=function() end
auras={HELPFUL={},HARMFUL={}}
nativeBuffs={}
nativeDebuffs={}
UnitBuff=function(u,i) return nativeBuffs[i] end
UnitDebuff=function(u,i) local a=nativeDebuffs[i]; if a then return unpack(a) end end
calls={index=0,slots=0,bySlot=0}
local epoch=0
local slotMap={}
function Aura(icon,count,duration,expiration,removable,dispel)
return {icon,icon,count or 1,dispel or "Magic",duration or 30,expiration or 130,"player",removable or false}
end
function InstallAPI(batched)
C_UnitAuras={}
C_UnitAuras.UnitAura=function(u,i,filter)
calls.index=calls.index+1
local a=auras[filter][i]
if a then return unpack(a,1,8) end
end
if batched then
C_UnitAuras.GetAuraSlots=function(u,filter,max,token,buf)
calls.slots=calls.slots+1; epoch=epoch+1; slotMap={}
local n=table.getn(auras[filter])
for i=1,n do buf[i]=epoch*1000+i; slotMap[buf[i]]=auras[filter][i] end
for i=n+1,table.getn(buf) do buf[i]=nil end
buf.n=n
return nil,n
end
C_UnitAuras.UnitAuraBySlot=function(u,id)
calls.bySlot=calls.bySlot+1
assert(slotMap[id],"stale slot used")
return unpack(slotMap[id],1,8)
end
end
end
function LoadSettings()
for _,f in ipairs(frames) do
if f.events.ADDON_LOADED then
this=f; arg1="TargetFrameBuff"; f.scripts.OnEvent()
end
end
end
function Tick() TargetFrameBuff_Update(); TargetFrameBuff_OnUpdate(.2) end
+219
View File
@@ -0,0 +1,219 @@
import sys
from pathlib import Path
import xml.etree.ElementTree as ET
sys.path.insert(0, str(Path("work/pythonlibs").resolve()))
from lupa.lua51 import LuaRuntime
addon = Path(__file__).resolve().parents[1]
mock = Path(__file__).with_name("mock_wow.lua").read_text(encoding="utf-8-sig")
passed = 0
def run(name, setup, body):
global passed
lua = LuaRuntime()
lua.execute(mock)
lua.execute(setup)
lua.execute((addon/"Settings.lua").read_text(encoding="utf-8-sig"))
lua.execute((addon/"TargetFrameBuff.lua").read_text(encoding="utf-8-sig"))
lua.execute("TargetFrameBuff_OnLoad(); LoadSettings()")
lua.execute(body)
passed += 1
print("PASS", name)
run("batched scans, reordered icons/timers and fresh tooltip indices",
'InstallAPI(true); auras.HELPFUL={Aura("A",1,30,130),Aura("B",1,40,140)}',
'''
Tick()
assert(calls.index==0)
assert(TargetFrameBuff1Icon.texture=="A" and TargetFrameBuff1.timer.text=="30")
auras.HELPFUL={Aura("B",1,40,150),Aura("A",1,30,125)}
Tick()
assert(TargetFrameBuff1Icon.texture=="B" and TargetFrameBuff1.timer.text=="50")
assert(TargetFrameBuff2Icon.texture=="A" and TargetFrameBuff2.timer.text=="25")
this=TargetFrameBuff2; this.scripts.OnEnter()
assert(GameTooltip.tooltip[2]==2 and GameTooltip.tooltip[3]=="HELPFUL")
auras.HELPFUL={Aura("C",1,50,160)}
Tick()
assert(TargetFrameBuff1Icon.texture=="C" and not TargetFrameBuff2:IsShown())
assert(not GameTooltip:IsShown())
''')
run("overflow debuff stacks, borders and cleanup on reuse", 'InstallAPI(true)', '''
auras.HELPFUL={Aura("Buff",3)}
for i=1,18 do auras.HARMFUL[i]=Aura("Debuff"..i,4,20,120,false,"Poison") end
Tick()
assert(TargetFrameBuff1.auraCount.text==3)
assert(TargetFrameBuff2Icon.texture=="Debuff17")
assert(TargetFrameBuff2.auraIndex==17 and TargetFrameBuff2.auraFilter=="HARMFUL")
assert(TargetFrameBuff2.auraCount.text==4 and TargetFrameBuff2.auraBorder.color[2]==1)
this=TargetFrameBuff2; this.scripts.OnEnter()
assert(GameTooltip.tooltip[2]==17 and GameTooltip.tooltip[3]=="HARMFUL")
auras.HARMFUL={}; auras.HELPFUL={Aura("Buff"),Aura("Plain")}
Tick()
assert(not TargetFrameBuff2.auraCount:IsShown() and not TargetFrameBuff2.auraBorder:IsShown())
assert(not TargetFrameDebuff1:IsShown())
''')
run("removable buff highlight toggle", 'InstallAPI(true); auras.HELPFUL={Aura("Purge",1,30,130,true)}', '''
Tick(); assert(TargetFrameBuff1.auraBorder:IsShown())
SlashCmdList.TARGETFRAMEBUFF("highlight off"); Tick()
assert(not TargetFrameBuff1.auraBorder:IsShown())
SlashCmdList.TARGETFRAMEBUFF("highlight on"); Tick()
assert(TargetFrameBuff1.auraBorder:IsShown())
''')
run("sweep start, refresh, unchanged timer, disable, expiration and missing duration",
'InstallAPI(true); auras.HELPFUL={Aura("A",1,30,130)}', '''
assert(not TargetFrameBuff1.cooldown)
SlashCmdList.TARGETFRAMEBUFF("sweeps on"); Tick()
local cd=TargetFrameBuff1.cooldown
assert(cd.start==100 and cd.duration==30 and cd.reverse==true and cd.edge==true)
local n=cd.calls; Tick(); assert(cd.calls==n)
auras.HELPFUL[1][6]=140; Tick(); assert(cd.start==110 and cd.calls==n+1)
SlashCmdList.TARGETFRAMEBUFF("sweeps off"); Tick(); assert(not cd:IsShown())
SlashCmdList.TARGETFRAMEBUFF("sweeps on"); Tick(); assert(cd:IsShown())
now=141; Tick(); assert(not cd:IsShown() and not TargetFrameBuff1.timer:IsShown())
auras.HELPFUL[1][5]=0; auras.HELPFUL[1][6]=0; Tick(); assert(not cd:IsShown())
''')
run("unknown timers, font size and warning settings", 'InstallAPI(true)', '''
auras.HELPFUL={Aura("A",1,0,0)}; Tick(); assert(not TargetFrameBuff1.timer:IsShown())
auras.HELPFUL={Aura("A",1,10,106)}; Tick()
assert(TargetFrameBuff1.timer.color[2]==1)
SlashCmdList.TARGETFRAMEBUFF("warning 10"); Tick()
assert(TargetFrameBuff1.timer.color[2]==.1)
SlashCmdList.TARGETFRAMEBUFF("size 14"); Tick()
assert(TargetFrameBuff1.timer.fontSize==14)
assert(TargetFrameBuff1.timer.font=="Fonts\\\\FRIZQT__.TTF")
SlashCmdList.TARGETFRAMEBUFF("timers off"); Tick(); assert(not TargetFrameBuff1.timer:IsShown())
SlashCmdList.TARGETFRAMEBUFF("timers on"); Tick(); assert(TargetFrameBuff1.timer:IsShown())
SlashCmdList.TARGETFRAMEBUFF("size 999"); assert(TargetFrameBuffDB.timerSize==14)
''')
run("older ClassicAPI and unsupported Cooldown fallback", '''
InstallAPI(false); noCooldown=true; auras.HELPFUL={Aura("Old",2,20,120)}
''', '''
SlashCmdList.TARGETFRAMEBUFF("sweeps on"); Tick()
assert(TargetFrameBuff1.noCooldown and TargetFrameBuff1.timer.text=="20")
assert(calls.index>0 and calls.slots==0)
auras.HELPFUL={Aura("Refreshed",1,30,140)}; Tick()
assert(TargetFrameBuff1Icon.texture=="Refreshed")
''')
run("native client fallback and settings persistence", '''
nativeBuffs={"Native"}; nativeDebuffs={{"Poison",3,"Poison"}}
TargetFrameBuffDB={timers=false,highlight=false,sweeps=false,timerSize=12,warning=7}
''', '''
Tick()
assert(TargetFrameBuff1Icon.texture=="Native")
assert(TargetFrameDebuff1Count.text==3)
assert(not TargetFrameBuff1.timer)
assert(TargetFrameBuffDB.timers==false and TargetFrameBuffDB.timerSize==12)
SlashCmdList.TARGETFRAMEBUFF("sweeps on"); Tick()
assert(not TargetFrameBuff1.cooldown)
''')
run("full 48-icon scan call budget and target clear", 'InstallAPI(true)', '''
for i=1,32 do auras.HELPFUL[i]=Aura("B"..i) end
for i=1,16 do auras.HARMFUL[i]=Aura("D"..i) end
calls={index=0,slots=0,bySlot=0}; Tick()
assert(calls.index==0 and calls.slots==2 and calls.bySlot==48)
assert(TargetFrameBuff32:IsShown() and TargetFrameDebuff16:IsShown())
auras={HELPFUL={},HARMFUL={}}; Tick()
assert(not TargetFrameBuff1:IsShown() and not TargetFrameDebuff16:IsShown())
''')
run("options panel controls, cancel and defaults", '''
InstallAPI(true)
InterfaceOptions_AddCategory=function(p) registered=p end
InterfaceOptionsFrame_OpenToCategory=function(p) p.refresh() end
''', '''
assert(registered.name=="TargetFrameBuff")
SlashCmdList.TARGETFRAMEBUFF("")
this=TargetFrameBuffOption3; this:SetChecked(true); this.scripts.OnClick()
assert(TargetFrameBuffDB.sweeps==true)
TargetFrameBuffSlider1:SetValue(13); assert(TargetFrameBuffDB.timerSize==13)
registered.cancel()
assert(TargetFrameBuffDB.sweeps==false and TargetFrameBuffDB.timerSize==9)
TargetFrameBuffDB.timerSize=15; registered.default(); assert(TargetFrameBuffDB.timerSize==9)
''')
run("corrupt saved settings recover", 'TargetFrameBuffDB={timerSize=999,warning=-1,timers="bad"}', '''
assert(TargetFrameBuffDB.timerSize==9 and TargetFrameBuffDB.warning==5 and TargetFrameBuffDB.timers==true)
''')
run("aura disappears just before hover and lower count overlay", 'InstallAPI(true); auras.HELPFUL={Aura("A")}; auras.HARMFUL={Aura("D",4)}', '''
Tick()
assert(TargetFrameDebuff1.auraCount.text==4)
assert(not TargetFrameDebuff1Count:IsShown())
auras.HELPFUL={}
this=TargetFrameBuff1; this.scripts.OnEnter()
assert(not GameTooltip:IsShown() and not TargetFrameBuff1.auraIndex)
''')
run("cached countdown avoids aura reads; silent recast fallback", 'InstallAPI(true); auras.HELPFUL={Aura("A",1,30,130)}', '''
calls={index=0,slots=0,bySlot=0}
now=101; TargetFrameBuff_OnUpdate(.2)
assert(calls.slots==0 and TargetFrameBuff1.timer.text=="29")
auras.HELPFUL[1][6]=150
TargetFrameBuff_OnUpdate(.2); assert(calls.slots==0)
TargetFrameBuff_OnUpdate(.1)
assert(calls.slots==2 and TargetFrameBuff1.timer.text=="49")
''')
run("unit event filtering and target change refresh", 'InstallAPI(true); auras.HELPFUL={Aura("A")}', '''
calls={index=0,slots=0,bySlot=0}
event="UNIT_AURA"; arg1="party1"; TargetFrameBuffFrame.scripts.OnEvent()
assert(calls.slots==0)
auras.HELPFUL={Aura("B")}; arg1="target"; TargetFrameBuffFrame.scripts.OnEvent()
assert(calls.slots==2 and TargetFrameBuff1Icon.texture=="B")
auras.HELPFUL={}; event="PLAYER_TARGET_CHANGED"; TargetFrameBuffFrame.scripts.OnEvent()
assert(not TargetFrameBuff1:IsShown())
''')
run("idle countdown stops but fallback discovers new timing", 'InstallAPI(true); auras.HELPFUL={Aura("A",1,0,0)}', '''
local writes=0
TargetFrameBuff1.timer.SetText=function() writes=writes+1 end
calls={index=0,slots=0,bySlot=0}
TargetFrameBuff_OnUpdate(.2); TargetFrameBuff_OnUpdate(.2)
assert(writes==0 and calls.slots==0)
auras.HELPFUL[1][5]=30; auras.HELPFUL[1][6]=130
TargetFrameBuff_OnUpdate(.1)
assert(writes==1 and TargetFrameBuff1.timer:IsShown())
SlashCmdList.TARGETFRAMEBUFF("timers off")
writes=0; TargetFrameBuff_OnUpdate(.2)
assert(writes==0 and not TargetFrameBuff1.timer:IsShown())
''')
run("delta mode removes polling and refreshes timing/reordering", 'InstallAPI(true); C_UnitAuras.GetAuraDataByAuraInstanceID=function() end; auras.HELPFUL={Aura("A")}', '''
assert(TargetFrameBuffFrame.events.UNIT_AURA_DELTA)
calls={index=0,slots=0,bySlot=0}
for i=1,20 do TargetFrameBuff_OnUpdate(.2) end
assert(calls.slots==0)
auras.HELPFUL={Aura("B",3,60,160)}
event="UNIT_AURA_DELTA"; arg1="party1"; arg2={updatedAuraInstanceIDs={1}}
TargetFrameBuffFrame.scripts.OnEvent(); assert(calls.slots==0)
arg1="target"; TargetFrameBuffFrame.scripts.OnEvent()
assert(calls.slots==2 and TargetFrameBuff1Icon.texture=="B" and TargetFrameBuff1.timer.text=="1m")
arg2.updatedAuraInstanceIDs[1]=999
now=101; TargetFrameBuff_OnUpdate(.2)
assert(TargetFrameBuff1.timer.text=="59" and calls.slots==2)
auras.HELPFUL={}; arg2={isFullUpdate=true}; TargetFrameBuffFrame.scripts.OnEvent()
assert(not TargetFrameBuff1:IsShown())
''')
run("delta mode refreshes target switches and spell/pet capability changes", 'InstallAPI(true); C_UnitAuras.GetAuraDataByAuraInstanceID=function() end', '''
auras.HELPFUL={Aura("A")}; event="PLAYER_TARGET_CHANGED"; TargetFrameBuffFrame.scripts.OnEvent()
assert(TargetFrameBuff1Icon.texture=="A")
calls={index=0,slots=0,bySlot=0}; event="UNIT_AURA"; arg1="target"; TargetFrameBuffFrame.scripts.OnEvent()
assert(calls.slots==0)
event="SPELLS_CHANGED"; TargetFrameBuffFrame.scripts.OnEvent(); assert(calls.slots==2)
event="UNIT_PET"; arg1="player"; TargetFrameBuffFrame.scripts.OnEvent(); assert(calls.slots==4)
''')
run("modern sweeps suppress native numbers and bling, clear on removal", '''
InstallAPI(true); auras.HELPFUL={Aura("A")}
local baseCreate=CreateFrame
CreateFrame=function(kind,name,parent,template)
local f=baseCreate(kind,name,parent,template)
if kind=="Cooldown" then
f.SetHideCountdownNumbers=function(self,v) self.hideNumbers=v end
f.SetDrawBling=function(self,v) self.bling=v end
f.Clear=function(self) self.cleared=true; self:Hide() end
end
return f
end
''', '''
SlashCmdList.TARGETFRAMEBUFF("sweeps on")
local cd=TargetFrameBuff1.cooldown
assert(cd.hideNumbers==true and cd.bling==false)
SlashCmdList.TARGETFRAMEBUFF("sweeps off"); assert(cd.cleared and not cd:IsShown())
SlashCmdList.TARGETFRAMEBUFF("sweeps on"); cd.cleared=false
auras.HELPFUL={}; Tick(); assert(cd.cleared and not cd:IsShown())
''')
ET.parse(addon/"TargetFrameBuff.xml")
print("PASS XML parsing")
print(f"{passed} Lua regression scenarios passed; XML parsed.")