Files
pfUI/modules/buff.lua
T
Brues 93ab07a662 Show cooldown text through ClassicAPI's native countdown
The cooldown module no longer builds its own text frame with a 10 Hz
OnUpdate per cooldown. The CooldownFrame_SetTimer hook now configures
each Cooldown frame's own countdown instead: SetHideCountdownNumbers
for the per-frame text toggles and omnicc's noCooldownCount,
SetMinimumCountdownDuration for the GCD threshold, and
GetCountdownFontString for the pfUI font and size. The engine ticks
the numbers, gated by the countdownForCooldowns cvar. The cvar gets a
checkbox in the Cooldown settings, and pfUI.SetupCVars turns it on.

The text comes from one shared C_StringUtil numeric rule formatter,
given to each frame with SetCountdownFormatter. Its breakpoints keep
pfUI's format: the number in its unit's cooldown color, a unit (m/h/d)
once the unit below passes 99, and tenths in the low color when
milliseconds are on. Values now round up. GetColoredTimeString formats
through the same formatter, so the buff, buffwatch and tooltip timers
match. A config:changed callback rebuilds the breakpoints, and every
frame picks them up on its next update.

The paper-doll slots swap their indicator Models for Cooldown frames,
as the spellbook and bags already do. They and the spellbook take the
BLIZZARD tag when they are created, replacing the per-update hooks in
the skin module. Models get no numbers. Hide Foreign Cooldown
Animations clears the draw flags instead of setting alpha to 0, so the
numbers stay visible. The buff module's sweep hides its numbers
because buff.timer carries the text.
2026-10-31 18:45:17 -05:00

412 lines
15 KiB
Lua

pfUI:RegisterModule("buff", function ()
-- Hide Blizz
BuffFrame:Hide()
BuffFrame:UnregisterAllEvents()
TemporaryEnchantFrame:Hide()
TemporaryEnchantFrame:UnregisterAllEvents()
local br, bg, bb, ba = GetStringColor(pfUI_config.appearance.border.color)
-- Player aura slot ids, enumerated once per refresh pass (ScanPlayerAuraSlots)
-- and read per button by its aura index: one GetAuraSlots walk per range
-- instead of a by-index walk per button.
local helpfulSlots, harmfulSlots = {}, {}
local function ScanPlayerAuraSlots()
ScanAuraSlots("player", "HELPFUL", helpfulSlots, 32)
ScanAuraSlots("player", "HARMFUL", harmfulSlots, 16)
end
local function RefreshBuffButton(buff)
if buff.btype == "HELPFUL" then
if C.buffs.separateweapons == "1" then
buff.id = buff.gid - (buff.weapon ~= nil and buff.gid or 0)
else
buff.id = buff.gid - pfUI.buff.wepbuffs.count
end
else
buff.id = buff.gid
end
if not buff.backdrop then
CreateBackdrop(buff)
CreateBackdropShadow(buff)
end
local slots = buff.btype == "HELPFUL" and helpfulSlots or harmfulSlots
local name, icon, count, dispelType, duration, expirationTime, _, _, _, spellId = C_UnitAuras.UnitAuraBySlot("player", slots[buff.id])
--detect weapon buffs
if buff.btype == "HELPFUL" and ((C.buffs.separateweapons == "0" and buff.gid <= pfUI.buff.wepbuffs.count) or (pfUI.buff.wepbuffs.count > 0 and buff.weapon ~= nil)) then
local mh, mhtime, mhcharge, oh, ohtime, ohcharge = GetWeaponEnchantInfo()
if pfUI.buff.wepbuffs.count == 2 then
if buff.gid == 1 then
buff.mode = "MAINHAND"
else
buff.mode = "OFFHAND"
end
else
if C.buffs.separateweapons == "0" then
buff.mode = mh and "MAINHAND" or oh and "OFFHAND"
else
if buff.gid == 1 then
buff.mode = mh and "MAINHAND" or oh and "OFFHAND"
else
buff:Hide()
return
end
end
end
-- Set Weapon Texture and Border
if buff.mode == "MAINHAND" then
buff.texture:SetTexture(GetInventoryItemTexture("player", 16))
buff.backdrop:SetBackdropBorderColor(GetItemQualityColor(GetInventoryItemQuality("player", 16) or 1))
elseif buff.mode == "OFFHAND" then
buff.texture:SetTexture(GetInventoryItemTexture("player", 17))
buff.backdrop:SetBackdropBorderColor(GetItemQualityColor(GetInventoryItemQuality("player", 17) or 1))
end
elseif name and (( buff.btype == "HARMFUL" and C.buffs.debuffs == "1" ) or ( buff.btype == "HELPFUL" and C.buffs.buffs == "1" )) then
-- Set Buff Texture and Border
buff.mode = buff.btype
buff.expirationTime = expirationTime
buff.stackCount = count
buff.spellId = spellId
buff.texture:SetTexture(icon)
if buff.btype == "HARMFUL" then
local dispelColor = C_UnitAuras.GetAuraDispelTypeColor(dispelType)
buff.backdrop:SetBackdropBorderColor(dispelColor:GetRGBA())
else
buff.backdrop:SetBackdropBorderColor(br,bg,bb,ba)
end
else
buff:Hide()
return
end
-- drive the sweep from the aura duration (weapon enchants and permanent
-- auras report no usable total, so those buttons stay text-only)
if C.buffs.cooldown_anim == "1" and buff.mode == buff.btype and duration and duration > 0 and expirationTime and expirationTime > 0 then
local now = GetTime()
local timeleft = expirationTime - now
if timeleft > 0 then
local effDuration = duration > timeleft and duration or timeleft
local newStart = now + timeleft - effDuration
if buff.cdStart == nil or abs(buff.cdStart - newStart) > .5 or buff.cdDuration ~= effDuration then
buff.cd:SetCooldown(newStart, effDuration)
buff.cdStart = newStart
buff.cdDuration = effDuration
end
else
buff.cd:Clear()
buff.cdStart = nil
end
else
buff.cd:Clear()
buff.cdStart = nil
end
buff:Show()
end
local function CreateBuffButton(i, btype, weapon)
local buttonName, buttonParent
if btype == "HELPFUL" then
if weapon == 1 then
buttonName = "pfWepBuffFrame" .. i
buttonParent = pfUI.buff.wepbuffs
else
buttonName = "pfBuffFrameBuff" .. i
buttonParent = pfUI.buff.buffs
end
else
buttonName = "pfDebuffFrameBuff" .. i
buttonParent = pfUI.buff.debuffs
end
local buff = CreateFrame("Button", buttonName, buttonParent)
buff.texture = buff:CreateTexture("BuffIcon" .. i, "BACKGROUND")
buff.texture:SetTexCoord(.07,.93,.07,.93)
buff.texture:SetAllPoints(buff)
-- overlay lifts the texts above the sweep: a child frame renders above
-- all of its parent's own regions, so the timer would dim under the
-- dark wedge otherwise
buff.overlay = CreateFrame("Frame", nil, buff)
buff.overlay:SetAllPoints(buff)
buff.timer = buff.overlay:CreateFontString(nil, "OVERLAY", "GameFontNormal")
buff.timer:SetTextColor(1,1,1,1)
buff.timer:SetJustifyH("CENTER")
buff.timer:SetJustifyV("CENTER")
buff.stacks = buff.overlay:CreateFontString(nil, "OVERLAY", "GameFontNormal")
buff.stacks:SetTextColor(1,1,1,1)
buff.stacks:SetJustifyH("RIGHT")
buff.stacks:SetJustifyV("BOTTOM")
buff.stacks:SetAllPoints(buff)
-- radial sweep (the template covers the button and starts hidden).
-- RefreshBuffButton drives it through the frame's own methods, never
-- CooldownFrame_SetTimer, so the cooldown module's hook leaves it alone;
-- text stays with buff.timer, so the frame's own numbers are hidden
buff.cd = CreateFrame("Cooldown", buttonName .. "Cooldown", buff, "CooldownFrameTemplate")
buff.cd:SetHideCountdownNumbers(true)
buff.cd:SetFrameLevel(buff:GetFrameLevel() + 1)
buff.overlay:SetFrameLevel(buff:GetFrameLevel() + 2)
buff:RegisterForClicks("RightButtonUp")
buff.weapon = weapon
buff.btype = btype
buff.gid = i
-- PERF: OnUpdate moved to consolidated parent frame handler (see pfUI.buff:SetScript("OnUpdate"))
-- Individual buff frames no longer have their own OnUpdate
buff:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
if this.mode == this.btype then
GameTooltip:SetUnitAura("player", this.id, this.btype)
if IsShiftKeyDown() then
local aura = C_UnitAuras.GetAuraDataByIndex("player", this.id, this.btype)
local playerlist = aura and GetUnbuffedRoster(aura.name) or ""
if strlen(playerlist) > 0 then
GameTooltip:AddLine(" ")
GameTooltip:AddLine(T["Unbuffed"] .. ":", .3, 1, .8)
GameTooltip:AddLine(playerlist,1,1,1,1)
GameTooltip:Show()
end
end
elseif this.mode == "MAINHAND" then
GameTooltip:SetInventoryItem("player", 16)
elseif this.mode == "OFFHAND" then
GameTooltip:SetInventoryItem("player", 17)
end
end)
buff:SetScript("OnLeave", GameTooltip_Hide)
buff:SetScript("OnClick", function()
if this.spellId then
C_Spell.CancelSpellByID(this.spellId)
end
end)
RefreshBuffButton(buff)
return buff
end
pfUI.buff = CreateFrame("Frame", "pfGlobalBuffFrame", UIParent)
pfUI.buff:RegisterEvent("PLAYER_AURAS_CHANGED")
pfUI.buff:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
pfUI.buff:RegisterUnitEvent("UNIT_MODEL_CHANGED", "player")
pfUI.buff:RegisterEvent("BUFF_UPDATE_DURATION_SELF")
pfUI.buff:RegisterEvent("DEBUFF_UPDATE_DURATION_SELF")
pfUI.buff:SetScript("OnEvent", function()
if C.buffs.weapons == "1" then
local mh, mhtime, mhcharge, oh, ohtime, ohcharge = GetWeaponEnchantInfo()
pfUI.buff.wepbuffs.count = (mh and 1 or 0) + (oh and 1 or 0)
else
pfUI.buff.wepbuffs.count = 0
end
ScanPlayerAuraSlots()
for i=1,32 do
RefreshBuffButton(pfUI.buff.buffs.buttons[i])
end
for i=1,16 do
RefreshBuffButton(pfUI.buff.debuffs.buttons[i])
end
if C.buffs.separateweapons == "1" then
for i=1,2 do
RefreshBuffButton(pfUI.buff.wepbuffs.buttons[i])
end
end
end)
-- PERF: Consolidated OnUpdate handler for all buff timers
-- This replaces 50 individual OnUpdate handlers with a single one
pfUI.buff:SetScript("OnUpdate", function()
local now = GetTime()
if not this.nextUpdate then this.nextUpdate = now + 0.1 end
if this.nextUpdate > now then return end
this.nextUpdate = now + 0.1
-- Cache weapon enchant info once per update cycle
local mh, mhtime, mhcharge, oh, ohtime, ohcharge = GetWeaponEnchantInfo()
-- Update all visible buff buttons
local buttons = pfUI.buff.buffs.buttons
for i = 1, 32 do
local buff = buttons[i]
if buff:IsShown() then
local timeleft, stacks = 0, 0
if buff.mode == buff.btype then
timeleft = buff.expirationTime > 0 and (buff.expirationTime - now) or 0
stacks = buff.stackCount or 0
elseif buff.mode == "MAINHAND" then
timeleft = mhtime and mhtime / 1000 or 0
stacks = mhcharge or 0
elseif buff.mode == "OFFHAND" then
timeleft = ohtime and ohtime / 1000 or 0
stacks = ohcharge or 0
end
buff.timer:SetText(timeleft > 0 and GetColoredTimeString(timeleft) or "")
buff.stacks:SetText(stacks > 1 and stacks or "")
end
end
-- Update all visible debuff buttons
buttons = pfUI.buff.debuffs.buttons
for i = 1, 16 do
local buff = buttons[i]
if buff:IsShown() then
local timeleft = buff.expirationTime > 0 and (buff.expirationTime - now) or 0
local stacks = buff.stackCount or 0
buff.timer:SetText(timeleft > 0 and GetColoredTimeString(timeleft) or "")
buff.stacks:SetText(stacks > 1 and stacks or "")
end
end
-- Update weapon buff buttons if separate
if C.buffs.separateweapons == "1" then
buttons = pfUI.buff.wepbuffs.buttons
for i = 1, 2 do
local buff = buttons[i]
if buff:IsShown() then
local timeleft, stacks = 0, 0
if buff.mode == "MAINHAND" then
timeleft = mhtime and mhtime / 1000 or 0
stacks = mhcharge or 0
elseif buff.mode == "OFFHAND" then
timeleft = ohtime and ohtime / 1000 or 0
stacks = ohcharge or 0
end
buff.timer:SetText(timeleft > 0 and GetColoredTimeString(timeleft) or "")
buff.stacks:SetText(stacks > 1 and stacks or "")
end
end
end
end)
-- CreateBuffButton refreshes each new button from the slot buffers
ScanPlayerAuraSlots()
-- Weapon Buffs
pfUI.buff.wepbuffs = CreateFrame("Frame", "pfWepBuffFrame", UIParent)
pfUI.buff.wepbuffs.count = 0
pfUI.buff.wepbuffs.buttons = {}
for i=1,2 do
pfUI.buff.wepbuffs.buttons[i] = CreateBuffButton(i, "HELPFUL", 1)
end
-- Buff Frame
pfUI.buff.buffs = CreateFrame("Frame", "pfBuffFrame", UIParent)
pfUI.buff.buffs.buttons = {}
for i=1,32 do
pfUI.buff.buffs.buttons[i] = CreateBuffButton(i, "HELPFUL")
end
-- Debuffs
pfUI.buff.debuffs = CreateFrame("Frame", "pfDebuffFrame", UIParent)
pfUI.buff.debuffs.buttons = {}
for i=1,16 do
pfUI.buff.debuffs.buttons[i] = CreateBuffButton(i, "HARMFUL")
end
-- config loading
function pfUI.buff:UpdateConfigBuffButton(buff)
local fontsize = C.buffs.fontsize == "-1" and C.global.font_size or C.buffs.fontsize
local buffSize, buffSpacing = tonumber(C.buffs.size), tonumber(C.buffs.spacing)
local rowcount, relFrame, offsetX, offsetY
if buff.btype == "HELPFUL" then
if buff.weapon == 1 and C.buffs.separateweapons == "1" then
rowcount = floor((buff.gid-1) / tonumber(C.buffs.wepbuffrowsize))
relFrame = pfUI.buff.wepbuffs
offsetX = -(buff.gid-1-rowcount*tonumber(C.buffs.wepbuffrowsize))*(buffSize+2*buffSpacing)
offsetY = -(rowcount) * ((C.buffs.textinside == "1" and 0 or (fontsize*1.5))+buffSize+2*buffSpacing)
else
rowcount = floor((buff.gid-1) / tonumber(C.buffs.buffrowsize))
relFrame = pfUI.buff.buffs
offsetX = -(buff.gid-1-rowcount*tonumber(C.buffs.buffrowsize))*(buffSize+2*buffSpacing)
offsetY = -(rowcount) * ((C.buffs.textinside == "1" and 0 or (fontsize*1.5))+buffSize+2*buffSpacing)
end
else
rowcount = floor((buff.gid-1) / tonumber(C.buffs.debuffrowsize))
relFrame = pfUI.buff.debuffs
offsetX = -(buff.gid-1-rowcount*tonumber(C.buffs.debuffrowsize))*(buffSize+2*buffSpacing)
offsetY = -(rowcount) * ((C.buffs.textinside == "1" and 0 or (fontsize*1.5))+buffSize+2*buffSpacing)
end
buff:SetSize(buffSize, buffSize)
buff.cd:SetReverse(C.buffs.cooldown_reverse == "1")
buff:ClearAllPoints()
buff:SetPoint("TOPRIGHT", relFrame, "TOPRIGHT",offsetX, offsetY)
buff.timer:SetFont(pfUI.font_default, fontsize, "OUTLINE")
buff.stacks:SetFont(pfUI.font_default, fontsize+1, "OUTLINE")
buff.timer:SetHeight(fontsize * 1.3)
buff.timer:ClearAllPoints()
if C.buffs.textinside == "1" then
buff.timer:SetAllPoints(buff)
else
buff.timer:SetPoint("TOP", buff, "BOTTOM", 0, -3)
end
end
function pfUI.buff:UpdateConfig()
local fontsize = C.buffs.fontsize == "-1" and C.global.font_size or C.buffs.fontsize
local spacing = tonumber(C.buffs.spacing)
local cell = tonumber(C.buffs.size) + 2 * spacing
local rowextra = C.buffs.textinside == "1" and 0 or (fontsize * 1.5)
local function SizeBuffFrame(frame, rowsize, count)
rowsize = tonumber(rowsize)
frame:SetSize(rowsize * cell, ceil(count / rowsize) * (rowextra + cell))
end
SizeBuffFrame(pfUI.buff.buffs, C.buffs.buffrowsize, 32)
pfUI.buff.buffs:SetPoint("TOPRIGHT", pfUI.minimap or UIParent, "TOPLEFT", -4 * spacing, 0)
UpdateMovable(pfUI.buff.buffs)
SizeBuffFrame(pfUI.buff.debuffs, C.buffs.debuffrowsize, 16)
pfUI.buff.debuffs:SetPoint("TOPRIGHT", pfUI.buff.buffs, "BOTTOMRIGHT", 0, 0)
UpdateMovable(pfUI.buff.debuffs)
if C.buffs.separateweapons == "1" then
pfUI.buff.wepbuffs:ClearAllPoints()
SizeBuffFrame(pfUI.buff.wepbuffs, C.buffs.wepbuffrowsize, 2)
pfUI.buff.wepbuffs:SetPoint("TOPRIGHT", pfUI.buff.debuffs, "BOTTOMRIGHT", 0, 0)
pfUI.buff.wepbuffs:Show()
UpdateMovable(pfUI.buff.wepbuffs)
else
pfUI.buff.wepbuffs:Hide()
RemoveMovable(pfUI.buff.wepbuffs)
end
for i=1,32 do
pfUI.buff:UpdateConfigBuffButton(pfUI.buff.buffs.buttons[i])
end
for i=1,16 do
pfUI.buff:UpdateConfigBuffButton(pfUI.buff.debuffs.buttons[i])
end
for i=1,2 do
pfUI.buff:UpdateConfigBuffButton(pfUI.buff.wepbuffs.buttons[i])
end
pfUI.buff:GetScript("OnEvent")()
end
pfUI.buff:UpdateConfig()
end)