Cast bars: fill and count down from duration objects, drop the OnUpdate sweeps

Player/target/focus bars and nameplate castbars hand their fill to
StatusBar:SetTimerDuration and their timer text to a duration text binding.
Pushback re-reads UnitCastingDuration/UnitChannelDuration; a C_Timer clears a
bar whose STOP never arrives. The nameplate castbar throttle is obsolete and
goes with its settings page.
This commit is contained in:
Brues
2026-11-02 16:12:15 -06:00
parent 001e09c1b7
commit 9f5ebf367f
8 changed files with 192 additions and 334 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ do
-- on. There pfUI does load, and will throw wherever it reaches for something -- on. There pfUI does load, and will throw wherever it reaches for something
-- the installed DLL doesn't have yet -- the popup names the cause so those -- the installed DLL doesn't have yet -- the popup names the cause so those
-- errors aren't a mystery. -- errors aren't a mystery.
local PFUI_CLASSIC_API_MIN = 11813 -- (X*10000 + Y*100 + Z) local PFUI_CLASSIC_API_MIN = 11900 -- (X*10000 + Y*100 + Z)
local PFUI_CLASSIC_API_LATEST = PFUI_CLASSIC_API_MIN local PFUI_CLASSIC_API_LATEST = PFUI_CLASSIC_API_MIN
local PFUI_CLASSIC_API_WEBSITE = "https://github.com/brues-code/ClassicAPI" local PFUI_CLASSIC_API_WEBSITE = "https://github.com/brues-code/ClassicAPI"
local PFUI_CLASSIC_API_LATEST_URL = PFUI_CLASSIC_API_WEBSITE .. "/releases/latest" local PFUI_CLASSIC_API_LATEST_URL = PFUI_CLASSIC_API_WEBSITE .. "/releases/latest"
+1 -1
View File
@@ -23,7 +23,7 @@ Provides:
- C_NamePlate - Event-driven nameplates (no more per-frame WorldFrame scanning) with real per-plate unit tokens and GUIDs - C_NamePlate - Event-driven nameplates (no more per-frame WorldFrame scanning) with real per-plate unit tokens and GUIDs
- C_UnitAuras - Replaces over 3k lines of hand-rolled aura tracking - C_UnitAuras - Replaces over 3k lines of hand-rolled aura tracking
- C_Spell - Replaces thousands of hardcoded spell names for each locale and powers castbar - C_Spell - Replaces thousands of hardcoded spell names for each locale and powers castbar
- Real cast bars - Actual cast/channel timing, spellID and interrupt state for any unit (C_Spell.UnitCastingInfo) - Real cast bars - Actual cast/channel timing, spellID and interrupt state for any unit (C_Spell.UnitCastingInfo), filled and counted down engine-side (UnitCastingDuration, StatusBar:SetTimerDuration, duration text bindings) with no Lua OnUpdate
- Focus - A genuine `focus` unit plus `/focus` and `/clearfocus` (FocusUnit / ClearFocus) - Focus - A genuine `focus` unit plus `/focus` and `/clearfocus` (FocusUnit / ClearFocus)
- C_EquipmentSet - Full Equipment Manager with GUID-tracked gear sets (tells apart duplicate/enchanted items) - C_EquipmentSet - Full Equipment Manager with GUID-tracked gear sets (tells apart duplicate/enchanted items)
- Instant Bag Sorting - C_Container.MoveItem / SwapItems instead of cursor pickup/drop - Instant Bag Sorting - C_Container.MoveItem / SwapItems instead of cursor pickup/drop
-1
View File
@@ -907,7 +907,6 @@ pfUI_translation["deDE"] = {
["Chat Tab"] = "Chat Tab", ["Chat Tab"] = "Chat Tab",
["Libpredict"] = "Libpredict", ["Libpredict"] = "Libpredict",
["Panel Alignment"] = "Panel Ausrichtung", ["Panel Alignment"] = "Panel Ausrichtung",
["Nameplate Update Rate"] = "Namensplaketten Aktualisierungsrate",
["Target/Casting Plates"] = "Ziel/Cast Plaketten", ["Target/Casting Plates"] = "Ziel/Cast Plaketten",
["Tooltip Update Rate"] = "Tooltip Aktualisierungsrate", ["Tooltip Update Rate"] = "Tooltip Aktualisierungsrate",
["Cursor Follow"] = "Cursor Folgen", ["Cursor Follow"] = "Cursor Folgen",
-1
View File
@@ -939,7 +939,6 @@ pfUI_translation["enUS"] = {
["Chat Tab"] = nil, ["Chat Tab"] = nil,
["Libpredict"] = nil, ["Libpredict"] = nil,
["Panel Alignment"] = nil, ["Panel Alignment"] = nil,
["Nameplate Update Rate"] = nil,
["Target/Casting Plates"] = nil, ["Target/Casting Plates"] = nil,
["Tooltip Update Rate"] = nil, ["Tooltip Update Rate"] = nil,
["Cursor Follow"] = nil, ["Cursor Follow"] = nil,
-2
View File
@@ -31,7 +31,6 @@ libthrottle.presetNames = {
-- Default throttle categories -- Default throttle categories
libthrottle.defaults = { libthrottle.defaults = {
nameplates_castbar = "custom",
tooltip_cursor = "fastest", tooltip_cursor = "fastest",
chat_tab = "custom", chat_tab = "custom",
swingtimer = "custom", swingtimer = "custom",
@@ -155,7 +154,6 @@ libthrottle:SetScript("OnEvent", function()
end end
-- Set defaults for custom fields if missing -- Set defaults for custom fields if missing
if not _G.pfUI_throttle.nameplates_castbar_custom then _G.pfUI_throttle.nameplates_castbar_custom = "100" end
if not _G.pfUI_throttle.tooltip_cursor_custom then _G.pfUI_throttle.tooltip_cursor_custom = "10" end if not _G.pfUI_throttle.tooltip_cursor_custom then _G.pfUI_throttle.tooltip_cursor_custom = "10" end
if not _G.pfUI_throttle.chat_tab_custom then _G.pfUI_throttle.chat_tab_custom = "10" end if not _G.pfUI_throttle.chat_tab_custom then _G.pfUI_throttle.chat_tab_custom = "10" end
if not _G.pfUI_throttle.swingtimer_custom then _G.pfUI_throttle.swingtimer_custom = "50" end if not _G.pfUI_throttle.swingtimer_custom then _G.pfUI_throttle.swingtimer_custom = "50" end
+116 -151
View File
@@ -5,36 +5,68 @@ pfUI:RegisterModule("castbar", function ()
local rawborder, default_border = GetBorderSize("unitframes") local rawborder, default_border = GetBorderSize("unitframes")
local cbtexture = pfUI.media[C.appearance.castbar.texture] local cbtexture = pfUI.media[C.appearance.castbar.texture]
-- Helper function for castbar timer formatting -- Castbar timer text: 1 decimal rounded half up (matches Blizzard spellbook
local function FormatCastbarTime(value) -- display), or 2 decimals (default)
if C.unitframes.castbardecimals == "1" then local formatter = C_StringUtil.CreateNumericRuleFormatter()
-- 1 decimal, round half up (matches Blizzard spellbook display) if C.unitframes.castbardecimals == "1" then
return string.format("%.1f", floor(value * 10 + 0.5) / 10) formatter:AddBreakpoint({ threshold = 0, step = .1, format = "%.1f" })
else else
-- 2 decimals (default) formatter:AddBreakpoint({ threshold = 0, format = "%.2f" })
return string.format("%.2f", value)
end
end end
-- Clear cast state on the bar and start the fade-out. On a normal end the bar -- "cur / max": a cast counts up, a channel counts down
-- is left at its current fill (a completed cast is already ~full; nothing local castText = {
-- snaps to full, which used to flash for a frame when the next cast stamped { property = Enum.DurationTextBindingProperty.ElapsedDuration, formatter = formatter },
-- the bar). When `failed` is set for a cast that was actually in progress, the { property = Enum.DurationTextBindingProperty.TotalDuration, formatter = formatter },
-- bar flashes full red before fading — the cancelled-cast indicator. }
local channelText = {
{ property = Enum.DurationTextBindingProperty.RemainingDuration, formatter = formatter },
{ property = Enum.DurationTextBindingProperty.TotalDuration, formatter = formatter },
}
-- The bar's fill and timer text both follow cb.duration on their own; only
-- the pushback total in front of the text needs a refresh.
local function UpdateTimerText(cb)
if not cb.showtimer then return end
local prefix = ""
if cb.delay > 0 then
prefix = "|cffffaaaa" .. (cb.isChannel and "-" or "+") .. formatter:FormatNumber(cb.delay) .. " |r "
end
cb.timer:SetTextFormat(prefix .. "{} / {}", cb.isChannel and channelText or castText)
cb.timer:Enable()
end
-- STOP doesn't arrive for every cast (a synthetic cast has none, a remote
-- unit's can be missed out of range), so the bar also clears at its end time.
local function ArmExpiry(cb)
if cb.expiry then cb.expiry:Cancel() end
cb.expiry = C_Timer.NewTimer(cb.duration:GetRemainingDuration(), cb.onExpire)
end
-- Clear cast state on the bar and start the fade-out. The fill freezes where
-- the cast stopped (SetMinMaxValues drops the timer). When `failed` is set for
-- a cast that was actually in progress, the bar flashes full red before
-- fading — the cancelled-cast indicator.
local function ClearBar(cb, failed) local function ClearBar(cb, failed)
local wasActive = cb.endTime ~= nil if cb.expiry then cb.expiry:Cancel() end
cb.startTime, cb.endTime, cb.isChannel = nil, nil, nil cb.expiry = nil
cb.timer:Disable()
cb:SetScript("OnUpdate", nil)
if cb.active then
cb.bar:SetMinMaxValues(0, 1)
if failed then
cb.bar:SetStatusBarColor(GetStringColor(C.appearance.castbar.failcolor))
cb.bar:SetValue(1)
else
cb.bar:SetValue(cb.isChannel and cb.duration:GetRemainingPercent() or cb.duration:GetElapsedPercent())
end
end
cb.active, cb.isChannel = nil, nil
cb.activeName, cb.spellID = nil, nil cb.activeName, cb.spellID = nil, nil
cb.isTradeskill = nil cb.isTradeskill = nil
cb.tradeskillTotal, cb.tradeskillCompleted, cb.tradeskillSpellID = nil, nil, nil cb.tradeskillTotal, cb.tradeskillCompleted, cb.tradeskillSpellID = nil, nil, nil
cb.tradeskillSingleMs, cb.currentCraftStart = nil, nil cb.tradeskillSingle = nil
cb.lastMax = nil
cb.delay = 0 cb.delay = 0
if failed and wasActive then
cb.bar:SetStatusBarColor(GetStringColor(C.appearance.castbar.failcolor))
cb.bar:SetMinMaxValues(0, 1)
cb.bar:SetValue(1)
end
if cb.bar.spark then cb.bar.spark:Hide() end if cb.bar.spark then cb.bar.spark:Hide() end
if cb:IsShown() and not cb.fade:IsPlaying() then cb.fade:Play() end if cb:IsShown() and not cb.fade:IsPlaying() then cb.fade:Play() end
end end
@@ -51,44 +83,48 @@ pfUI:RegisterModule("castbar", function ()
end end
end end
-- Per-craft spark position: the current craft's share of the bar's width.
-- Snaps back to the left edge each time StartTradeskillCraft restarts it.
local function MoveSpark()
local x = this.craft:GetElapsedPercent() * this.bar:GetWidth() - 1
this.bar.spark:SetPoint("TOP", this.bar, "TOPLEFT", x, 0)
this.bar.spark:SetPoint("BOTTOM", this.bar, "BOTTOMLEFT", x, 0)
end
-- Stretch the bar to span all queued crafts (Quartz-style merge): the bar -- Stretch the bar to span all queued crafts (Quartz-style merge): the bar
-- fills continuously across the chain instead of resetting per craft. -- fills continuously across the chain instead of resetting per craft.
-- Also arms the per-craft spark — a thin marker that crosses the bar once -- Also arms the per-craft spark — a thin marker that crosses the bar once
-- per craft (so it moves N× faster than the main fill on an N-stack). -- per craft (so it moves N× faster than the main fill on an N-stack).
local function EnterTradeskillMerge(cb, startMs, endMs, count) local function EnterTradeskillMerge(cb, count)
local start, single = cb.duration:GetStartTime(), cb.duration:GetTotalDuration()
cb.tradeskillTotal = count cb.tradeskillTotal = count
cb.tradeskillCompleted = 0 cb.tradeskillCompleted = 0
cb.tradeskillSpellID = cb.spellID cb.tradeskillSpellID = cb.spellID
local single = endMs - startMs cb.tradeskillSingle = single
cb.tradeskillSingleMs = single cb.craft:SetTimeFromStart(start, single)
cb.currentCraftStart = startMs cb.duration:SetTimeFromStart(start, single * count)
local mergedEnd = startMs + single * count ArmExpiry(cb)
cb.endTime = mergedEnd cb.bar.spark:Show()
local duration = (mergedEnd - startMs) / 1000 cb:SetScript("OnUpdate", MoveSpark)
cb.bar:SetMinMaxValues(0, duration)
cb.lastMax = duration
if cb.bar.spark then cb.bar.spark:Show() end
UpdateTradeskillLabel(cb) UpdateTradeskillLabel(cb)
end end
-- Mark the start of craft 2..N within an active merge (called from the -- Mark the start of craft 2..N within an active merge (called from the
-- UNIT_SPELLCAST_START handler). Resets the per-craft spark to the left -- UNIT_SPELLCAST_START handler) and re-fit the bar's end to the crafts left.
-- edge of the bar.
local function StartTradeskillCraft(cb) local function StartTradeskillCraft(cb)
cb.currentCraftStart = GetTime() * 1000 local now = GetTime()
local remaining = cb.tradeskillTotal - (cb.tradeskillCompleted or 0) local remaining = cb.tradeskillTotal - cb.tradeskillCompleted
cb.endTime = cb.currentCraftStart + cb.tradeskillSingleMs * remaining cb.craft:SetTimeFromStart(now, cb.tradeskillSingle)
local duration = (cb.endTime - cb.startTime) / 1000 cb.duration:SetTimeSpan(cb.duration:GetStartTime(), now + cb.tradeskillSingle * remaining)
cb.bar:SetMinMaxValues(0, duration) ArmExpiry(cb)
cb.lastMax = duration
UpdateTradeskillLabel(cb) UpdateTradeskillLabel(cb)
end end
-- Stamp the bar with cast data and render text/icon/lag once. OnUpdate -- Stamp the bar with cast data and render text/icon/lag once. From here the
-- then animates the fill from this state without touching C_Spell. -- bar's timer and text binding follow cb.duration without any Lua.
local function StampBar(cb, name, tex, startMs, endMs, spellID, isChannel, delayMs, isTradeskill, rank, noInterrupt) local function StampBar(cb, name, tex, startMs, endMs, spellID, isChannel, delayMs, isTradeskill, rank, noInterrupt)
cb.startTime = startMs cb.duration:SetTimeSpan(startMs / 1000, endMs / 1000)
cb.endTime = endMs cb.active = true
cb.isChannel = isChannel cb.isChannel = isChannel
cb.spellID = spellID cb.spellID = spellID
cb.activeName = name cb.activeName = name
@@ -124,27 +160,18 @@ pfUI:RegisterModule("castbar", function ()
cb.icon:Hide() cb.icon:Hide()
end end
local duration = (endMs - startMs) / 1000
if cb.showlag then if cb.showlag then
local _, _, lag = GetNetStats() local _, _, lag = GetNetStats()
cb.bar.lag:SetWidth(math.min(cb:GetWidth(), cb:GetWidth() / duration * (lag/1000))) cb.bar.lag:SetWidth(math.min(cb:GetWidth(), cb:GetWidth() / cb.duration:GetTotalDuration() * (lag/1000)))
cb.bar.lag:Show() cb.bar.lag:Show()
else else
cb.bar.lag:Hide() cb.bar.lag:Hide()
end end
cb.bar:SetMinMaxValues(0, duration) cb.bar:SetTimerDuration(cb.duration, Enum.StatusBarInterpolation.Immediate,
cb.lastMax = duration isChannel and Enum.StatusBarTimerDirection.RemainingTime or Enum.StatusBarTimerDirection.ElapsedTime)
UpdateTimerText(cb)
-- Prime the fill on this frame. StampBar otherwise leaves the previous ArmExpiry(cb)
-- value in place (ClearBar, run on the prior cast's STOP, leaves it full),
-- so the bar would flash full for the frame between here and the next
-- OnUpdate tick. Reset the throttle too so the timer text updates promptly.
local nowSec = GetTime()
local cur = isChannel and (endMs / 1000 - nowSec) or (nowSec - startMs / 1000)
if cur < 0 then cur = 0 elseif cur > duration then cur = duration end
cb.bar:SetValue(cur)
cb.tick = 0
end end
-- One-shot poll: read C_Spell for the bar's unit, stamp or clear. Called -- One-shot poll: read C_Spell for the bar's unit, stamp or clear. Called
@@ -186,6 +213,10 @@ pfUI:RegisterModule("castbar", function ()
cb:SetFrameLevel(8) cb:SetFrameLevel(8)
cb.unitstr = unitstr cb.unitstr = unitstr
cb.delay = 0
cb.duration = C_DurationUtil.CreateDuration()
cb.craft = C_DurationUtil.CreateDuration() -- the current craft of a tradeskill merge
cb.onExpire = function() ClearBar(cb) end
-- icon -- icon
cb.icon = CreateFrame("Frame", nil, cb) cb.icon = CreateFrame("Frame", nil, cb)
@@ -231,6 +262,10 @@ pfUI:RegisterModule("castbar", function ()
cb.bar.right:SetFont(font, font_size, "OUTLINE") cb.bar.right:SetFont(font, font_size, "OUTLINE")
cb.bar.right:SetJustifyH(C.castbar[unitstr].timealign or "RIGHT") cb.bar.right:SetJustifyH(C.castbar[unitstr].timealign or "RIGHT")
cb.timer = C_DurationUtil.CreateDurationTextBinding()
cb.timer:SetFontString(cb.bar.right)
cb.timer:SetDuration(cb.duration)
cb.bar.lag = cb.bar:CreateTexture(nil, "OVERLAY") cb.bar.lag = cb.bar:CreateTexture(nil, "OVERLAY")
cb.bar.lag:SetPoint("TOPRIGHT", cb.bar, "TOPRIGHT", 0, 0) cb.bar.lag:SetPoint("TOPRIGHT", cb.bar, "TOPRIGHT", 0, 0)
cb.bar.lag:SetPoint("BOTTOMRIGHT", cb.bar, "BOTTOMRIGHT", 0, 0) cb.bar.lag:SetPoint("BOTTOMRIGHT", cb.bar, "BOTTOMRIGHT", 0, 0)
@@ -238,7 +273,7 @@ pfUI:RegisterModule("castbar", function ()
-- Per-craft progress spark for tradeskill merge — a thin vertical line -- Per-craft progress spark for tradeskill merge — a thin vertical line
-- that crosses the bar once per craft in the chain (so on a 5-stack it -- that crosses the bar once per craft in the chain (so on a 5-stack it
-- moves 5x faster than the main fill). Position is updated by OnUpdate -- moves 5x faster than the main fill). Position is updated by MoveSpark
-- while a merge is active; hidden otherwise. -- while a merge is active; hidden otherwise.
cb.bar.spark = cb.bar:CreateTexture(nil, "OVERLAY") cb.bar.spark = cb.bar:CreateTexture(nil, "OVERLAY")
cb.bar.spark:SetColorTexture(1, 1, 1, 0.8) cb.bar.spark:SetColorTexture(1, 1, 1, 0.8)
@@ -247,79 +282,14 @@ pfUI:RegisterModule("castbar", function ()
cb.bar.spark:SetPoint("BOTTOM", cb.bar, "BOTTOMLEFT", 0, 0) cb.bar.spark:SetPoint("BOTTOM", cb.bar, "BOTTOMLEFT", 0, 0)
cb.bar.spark:Hide() cb.bar.spark:Hide()
-- Fade-out after a cast ends. The bar is hidden while idle, so its OnUpdate -- Fade-out after a cast ends. The bar is hidden while idle; unlock mode
-- only runs during a cast; unlock mode keeps it shown for dragging. -- keeps it shown for dragging.
local function unlocked() return cb.drag and cb.drag:IsShown() end
cb.fade = cb:CreateAnimationGroup() cb.fade = cb:CreateAnimationGroup()
local fade = cb.fade:CreateAnimation("Alpha") local fade = cb.fade:CreateAnimation("Alpha")
fade:SetChange(-1) fade:SetChange(-1)
fade:SetDuration(.4) fade:SetDuration(.4)
cb.fade:SetScript("OnFinished", function() cb.fade:SetScript("OnFinished", function()
if not unlocked() then cb:Hide() end if not (cb.drag and cb.drag:IsShown()) then cb:Hide() end
end)
-- OnUpdate animates the bar fill. All state transitions (cast start/stop/interrupt, channel start/stop,
-- pushback) come from the event handler below — we never poll C_Spell
-- here.
cb:SetScript("OnUpdate", function()
if (this.tick or 0) > GetTime() then return end
this.tick = GetTime() + 0.020 -- ~50 FPS
if unlocked() then
this.fade:Stop()
return
end
if this.fade:IsPlaying() then return end
if not this.endTime then
this:Hide()
return
end
-- Non-player bars: if the unit disappears (target died / detarget),
-- drop the bar immediately.
if this.unitstr ~= "player" and not UnitExists(this.unitstr) then
ClearBar(this)
return
end
local now = GetTime()
local endSec = this.endTime / 1000
if now >= endSec then
ClearBar(this)
return
end
local startSec = this.startTime / 1000
local max = endSec - startSec
local cur = this.isChannel and (endSec - now) or (now - startSec)
if cur > max then cur = max end
if cur < 0 then cur = 0 end
this.bar:SetValue(cur)
-- Per-craft spark for a tradeskill merge: position by (now - current
-- craft's start) / single-craft duration, clamped to [0, 1]. Snaps back
-- to the left edge each time StartTradeskillCraft fires for craft N+1.
if this.tradeskillTotal and this.tradeskillSingleMs and this.currentCraftStart then
local craftElapsed = now * 1000 - this.currentCraftStart
local p = craftElapsed / this.tradeskillSingleMs
if p < 0 then p = 0 elseif p > 1 then p = 1 end
local x = p * this.bar:GetWidth() - 1
this.bar.spark:ClearAllPoints()
this.bar.spark:SetPoint("TOP", this.bar, "TOPLEFT", x, 0)
this.bar.spark:SetPoint("BOTTOM", this.bar, "BOTTOMLEFT", x, 0)
end
if this.showtimer then
if (this.delay or 0) > 0 then
local prefix = "|cffffaaaa" .. (this.isChannel and "-" or "+") .. FormatCastbarTime(this.delay) .. " |r "
this.bar.right:SetText(prefix .. FormatCastbarTime(cur) .. " / " .. FormatCastbarTime(max))
else
this.bar.right:SetText(FormatCastbarTime(cur) .. " / " .. FormatCastbarTime(max))
end
end
end) end)
-- Cast lifecycle, entirely on ClassicAPI's UNIT_SPELLCAST_* events. They -- Cast lifecycle, entirely on ClassicAPI's UNIT_SPELLCAST_* events. They
@@ -383,7 +353,7 @@ pfUI:RegisterModule("castbar", function ()
RefreshBar(this, arg5) RefreshBar(this, arg5)
if this.isTradeskill and (this.pendingTradeskillCount or 0) > 1 if this.isTradeskill and (this.pendingTradeskillCount or 0) > 1
and C.castbar.player.mergetradeskill == "1" then and C.castbar.player.mergetradeskill == "1" then
EnterTradeskillMerge(this, this.startTime, this.endTime, this.pendingTradeskillCount) EnterTradeskillMerge(this, this.pendingTradeskillCount)
end end
this.pendingTradeskillCount = nil this.pendingTradeskillCount = nil
end end
@@ -392,7 +362,7 @@ pfUI:RegisterModule("castbar", function ()
-- count and clear when the chain is done. A no-op for normal casts, -- count and clear when the chain is done. A no-op for normal casts,
-- which are cleared by UNIT_SPELLCAST_STOP. -- which are cleared by UNIT_SPELLCAST_STOP.
if this.tradeskillTotal and arg3 == this.tradeskillSpellID then if this.tradeskillTotal and arg3 == this.tradeskillSpellID then
this.tradeskillCompleted = (this.tradeskillCompleted or 0) + 1 this.tradeskillCompleted = this.tradeskillCompleted + 1
if this.tradeskillCompleted >= this.tradeskillTotal then if this.tradeskillCompleted >= this.tradeskillTotal then
ClearBar(this) ClearBar(this)
else else
@@ -412,35 +382,30 @@ pfUI:RegisterModule("castbar", function ()
elseif event == "UNIT_SPELLCAST_FAILED" or event == "UNIT_SPELLCAST_INTERRUPTED" then elseif event == "UNIT_SPELLCAST_FAILED" or event == "UNIT_SPELLCAST_INTERRUPTED" then
ClearBar(this, true) ClearBar(this, true)
elseif event == "UNIT_SPELLCAST_INTERRUPTIBLE" or event == "UNIT_SPELLCAST_NOT_INTERRUPTIBLE" then elseif event == "UNIT_SPELLCAST_INTERRUPTIBLE" or event == "UNIT_SPELLCAST_NOT_INTERRUPTIBLE" then
if this.endTime then if this.active then
SetCastbarShield(this.bar, this.icon, event == "UNIT_SPELLCAST_NOT_INTERRUPTIBLE", this.isChannel) SetCastbarShield(this.bar, this.icon, event == "UNIT_SPELLCAST_NOT_INTERRUPTIBLE", this.isChannel)
end end
elseif event == "UNIT_SPELLCAST_DELAYED" or event == "UNIT_SPELLCAST_CHANNEL_UPDATE" then elseif event == "UNIT_SPELLCAST_DELAYED" or event == "UNIT_SPELLCAST_CHANNEL_UPDATE" then
-- Pushback: a cast delayed later or a channel shortened. Following -- Pushback: a cast delayed later or a channel shortened. Following
-- Quartz, re-poll just the times and accumulate the shift into a running -- Quartz, re-read just the times and accumulate the shift into a running
-- this.delay for the +/- indicator, rather than a full restamp (which -- this.delay for the +/- indicator, rather than a full restamp (which
-- would reset that total and re-render icon/text). ClassicAPI moves -- would reset that total and re-render icon/text). Pushback moves the
-- endMs (SpellDelayed_h bumps g_cast.endMs; the MSG_CHANNEL_UPDATE -- end time while the start stays put, so we diff the end. delay is a
-- co-hook rewrites g_channel.endMs) while startMs stays put, so we diff -- positive magnitude; UpdateTimerText signs it "+" for casts and "-"
-- endMs. delay is a positive magnitude; OnUpdate signs it "+" for casts -- for channels. Skipped during a tradeskill merge.
-- and "-" for channels. Skipped during a tradeskill merge. if this.active and not this.tradeskillTotal then
if this.endTime and not this.tradeskillTotal then local duration
local startMs, endMs
if this.isChannel then if this.isChannel then
local _, _, _, s, e = C_Spell.UnitChannelInfo(unit) duration = UnitChannelDuration(unit)
startMs, endMs = s, e
else else
local _, _, _, s, e = C_Spell.UnitCastingInfo(unit) duration = UnitCastingDuration(unit)
startMs, endMs = s, e
end end
if startMs and endMs then if duration then
local shift = this.isChannel and (this.endTime - endMs) or (endMs - this.endTime) local shift = duration:GetEndTime() - this.duration:GetEndTime()
this.delay = (this.delay or 0) + shift / 1000 this.delay = this.delay + (this.isChannel and -shift or shift)
this.startTime = startMs this.duration:Assign(duration)
this.endTime = endMs UpdateTimerText(this)
local newDuration = (endMs - startMs) / 1000 ArmExpiry(this)
this.bar:SetMinMaxValues(0, newDuration)
this.lastMax = newDuration
end end
end end
end end
-65
View File
@@ -1646,71 +1646,6 @@ pfUI:RegisterModule("gui", function ()
end) end)
-- Throttling Menu -- Throttling Menu
CreateGUIEntry(T["Throttling"], T["Nameplates"], function()
local header = CreateConfig(nil, T["Nameplate Update Rate"], nil, nil, "header")
header:GetParent().objectCount = header:GetParent().objectCount - 1
header:SetHeight(20)
local castbarCustom
CreateConfig(function()
if castbarCustom and castbarCustom.input then
local isCustom = pfUI.throttle:IsCustom("nameplates_castbar")
if not isCustom then
castbarCustom.input:EnableMouse(false)
castbarCustom.input:EnableKeyboard(false)
castbarCustom.input:ClearFocus()
castbarCustom.input:SetTextColor(.5,.5,.5,1)
castbarCustom.input:SetText(tostring(pfUI.throttle:GetFps("nameplates_castbar")))
else
castbarCustom.input:EnableMouse(true)
castbarCustom.input:EnableKeyboard(true)
castbarCustom.input:SetTextColor(.2,1,.8,1)
if _G.pfUI_throttle.nameplates_castbar_custom then
castbarCustom.input:SetText(_G.pfUI_throttle.nameplates_castbar_custom)
end
end
end
end, T["Castbar Plates"], _G.pfUI_throttle, "nameplates_castbar", "dropdown", {
"very_slow:" .. T["Very Slow"] .. " (2 FPS)",
"slow:" .. T["Slow"] .. " (5 FPS)",
"normal:" .. T["Normal"] .. " (10 FPS)",
"fast:" .. T["Fast"] .. " (20 FPS)",
"very_fast:" .. T["Very Fast"] .. " (30 FPS)",
"fastest:" .. T["Fastest"] .. " (50 FPS)",
"custom:" .. T["Custom"],
})
castbarCustom = CreateConfig(nil, T["Custom FPS"], _G.pfUI_throttle, "nameplates_castbar_custom")
local isCustom = pfUI.throttle:IsCustom("nameplates_castbar")
if not isCustom then
castbarCustom.input:EnableMouse(false)
castbarCustom.input:EnableKeyboard(false)
castbarCustom.input:SetTextColor(.5,.5,.5,1)
castbarCustom.input:SetText(tostring(pfUI.throttle:GetFps("nameplates_castbar")))
else
castbarCustom.input:EnableMouse(true)
castbarCustom.input:EnableKeyboard(true)
castbarCustom.input:SetTextColor(.2,1,.8,1)
if _G.pfUI_throttle.nameplates_castbar_custom then
castbarCustom.input:SetText(_G.pfUI_throttle.nameplates_castbar_custom)
end
end
-- Spacer before reset button
local spacer = CreateConfig(nil, " ", nil, nil, "header")
spacer:GetParent().objectCount = spacer:GetParent().objectCount - 1
spacer:SetHeight(10)
-- Reset to defaults button
CreateConfig(nil, T["Reset to Defaults"], nil, nil, "button", function()
pfUI.throttle:ResetToDefault("nameplates_castbar")
_G.pfUI_throttle.nameplates_castbar_custom = "100"
Reload()
end, true)
end)
CreateGUIEntry(T["Throttling"], T["Tooltips"], function() CreateGUIEntry(T["Throttling"], T["Tooltips"], function()
local header = CreateConfig(nil, T["Tooltip Update Rate"], nil, nil, "header") local header = CreateConfig(nil, T["Tooltip Update Rate"], nil, nil, "header")
header:GetParent().objectCount = header:GetParent().objectCount - 1 header:GetParent().objectCount = header:GetParent().objectCount - 1
+74 -112
View File
@@ -82,7 +82,6 @@ pfUI:RegisterModule("nameplates", function ()
local threatMemory = {} -- guid -> true once the mob targeted the player local threatMemory = {} -- guid -> true once the mob targeted the player
local raidGuidCache = {} -- guid -> name of the player's group local raidGuidCache = {} -- guid -> name of the player's group
local offtanks = {} local offtanks = {}
local activeCasts = {} -- overlay -> true while its castbar sweeps
local zooming = {} -- overlay -> true while its target zoom animates local zooming = {} -- overlay -> true while its target zoom animates
local auraSlots = {} -- reusable GetAuraSlots buffer for the per-plate aura scan local auraSlots = {} -- reusable GetAuraSlots buffer for the per-plate aura scan
local targetGuid local targetGuid
@@ -100,6 +99,7 @@ pfUI:RegisterModule("nameplates", function ()
-- Config caching -- Config caching
-- ============================================================================ -- ============================================================================
local cfg = {} local cfg = {}
local castFormatter = C_StringUtil.CreateNumericRuleFormatter()
local debufffilter, debufflist local debufffilter, debufflist
local function DebuffFilterPopulate() local function DebuffFilterPopulate()
@@ -158,7 +158,13 @@ pfUI:RegisterModule("nameplates", function ()
cfg.showguildname = N.showguildname == "1" cfg.showguildname = N.showguildname == "1"
cfg.abbrevname = C.unitframes.abbrevname == "1" cfg.abbrevname = C.unitframes.abbrevname == "1"
cfg.friendclassnamec = N.friendclassnamec == "1" cfg.friendclassnamec = N.friendclassnamec == "1"
cfg.castbardecimals = C.unitframes.castbardecimals == "1" -- the plate castbars' text bindings hold this formatter, so they pick the
-- change up on their next tick
if C.unitframes.castbardecimals == "1" then
castFormatter:SetBreakpoints({ { threshold = 0, step = .1, rounding = Enum.NumericRuleFormatRounding.Down, format = "%.1f" } })
else
castFormatter:SetBreakpoints({ { threshold = 0, format = "%.2f" } })
end
cfg.fullhealth = N.fullhealth == "1" cfg.fullhealth = N.fullhealth == "1"
cfg.keeptarget = N.target == "1" cfg.keeptarget = N.target == "1"
cfg.critters = N.critters == "1" cfg.critters = N.critters == "1"
@@ -187,8 +193,6 @@ pfUI:RegisterModule("nameplates", function ()
cfg.rightclick = N.rightclick == "1" cfg.rightclick = N.rightclick == "1"
cfg.hlr, cfg.hlg, cfg.hlb, cfg.hla = GetStringColor(N.highlightcolor) cfg.hlr, cfg.hlg, cfg.hlb, cfg.hla = GetStringColor(N.highlightcolor)
cfg.br, cfg.bg, cfg.bb, cfg.ba = GetStringColor(C.appearance.border.color) cfg.br, cfg.bg, cfg.bb, cfg.ba = GetStringColor(C.appearance.border.color)
-- sweep rate of the castbars on plates other than the target's
cfg.throttle_castbar = pfUI.throttle:Get("nameplates_castbar")
local c = combatstate local c = combatstate
c.CASTING.r, c.CASTING.g, c.CASTING.b, c.CASTING.a = GetStringColor(N.combatcasting) c.CASTING.r, c.CASTING.g, c.CASTING.b, c.CASTING.a = GetStringColor(N.combatcasting)
@@ -227,22 +231,23 @@ pfUI:RegisterModule("nameplates", function ()
-- One-shot C_Spell poll. Builds the cast struct for a UNIT_SPELLCAST_* event -- One-shot C_Spell poll. Builds the cast struct for a UNIT_SPELLCAST_* event
-- (the payload carries no timing) and seeds a plate that binds while its unit -- (the payload carries no timing) and seeds a plate that binds while its unit
-- is already mid-cast (its START fired before the plate existed). Picks cast -- is already mid-cast (its START fired before the plate existed). Picks cast
-- vs channel itself. -- vs channel itself. A cast whose start was never seen has no duration.
local function PollCastInfo(unit) local function PollCastInfo(unit)
local name, _, texture, startMs, endMs, _, _, noInterrupt, spellID = C_Spell.UnitCastingInfo(unit) local name, _, texture, _, _, _, _, noInterrupt, spellID = C_Spell.UnitCastingInfo(unit)
local isChannel local duration, isChannel
if not name then if name then
name, _, texture, startMs, endMs, _, noInterrupt, spellID = C_Spell.UnitChannelInfo(unit) duration = UnitCastingDuration(unit)
else
name, _, texture, _, _, _, noInterrupt, spellID = C_Spell.UnitChannelInfo(unit)
duration = name and UnitChannelDuration(unit)
isChannel = true isChannel = true
end end
if not name or not startMs or not endMs then return nil end if not duration then return nil end
return { return {
spellName = name, spellName = name,
spellID = spellID, spellID = spellID,
icon = texture, icon = texture,
startTime = startMs / 1000, duration = duration,
endTime = endMs / 1000,
duration = (endMs - startMs) / 1000,
isChannel = isChannel, isChannel = isChannel,
noInterrupt = noInterrupt, noInterrupt = noInterrupt,
} }
@@ -279,7 +284,7 @@ pfUI:RegisterModule("nameplates", function ()
local mobTargetGuid = UnitGUID(target) local mobTargetGuid = UnitGUID(target)
local hasTarget = mobTargetGuid and mobTargetGuid ~= NULL_GUID local hasTarget = mobTargetGuid and mobTargetGuid ~= NULL_GUID
local info = castState[guid] local info = castState[guid]
local isCasting = info and info.endTime > GetTime() local isCasting = info and not info.duration:HasExpired()
local targetingPlayer = hasTarget and UnitIsUnit(target, "player") local targetingPlayer = hasTarget and UnitIsUnit(target, "player")
if targetingPlayer then if targetingPlayer then
@@ -819,8 +824,7 @@ pfUI:RegisterModule("nameplates", function ()
end end
-- ============================================================================ -- ============================================================================
-- Animation driver: castbar sweeps and the target zoom. Shown only while -- Animation driver: the target zoom. Shown only while a plate zooms.
-- something moves.
-- ============================================================================ -- ============================================================================
local driver = CreateFrame("Frame", nil, UIParent) local driver = CreateFrame("Frame", nil, UIParent)
driver:Hide() driver:Hide()
@@ -841,98 +845,8 @@ pfUI:RegisterModule("nameplates", function ()
driver:Show() driver:Show()
end end
-- Skip SetText when the rounded value hasn't changed since last frame.
-- `string.format("%.2f", ...)` allocates a fresh string every call; this
-- gate compares cheap integers and only formats when the displayed value
-- would actually differ. Reset via lastEndTime-changed branch below.
local function SetCastbarText(castbar, remaining)
local rounded
if cfg.castbardecimals then
rounded = floor(remaining * 10)
if castbar.lastTextTick ~= rounded then
castbar.lastTextTick = rounded
castbar.text:SetText(rounded / 10)
end
else
rounded = floor(remaining * 100)
if castbar.lastTextTick ~= rounded then
castbar.lastTextTick = rounded
castbar.text:SetFormattedText("%.2f", remaining)
end
end
end
local function UpdateCastbar(plate, info, now)
local castbar = plate.castbar
local duration = info.duration
if castbar.lastEndTime ~= info.endTime then
castbar.lastEndTime = info.endTime
castbar.lastTextTick = nil
-- Relative 0..duration range to avoid float precision loss with large
-- absolute timestamps.
castbar:SetMinMaxValues(0, duration)
SetCastbarShield(castbar, castbar.icon, info.noInterrupt, info.isChannel)
if info.icon then
castbar.icon.tex:SetTexture(info.icon)
castbar.icon.tex:SetTexCoord(.1,.9,.1,.9)
end
castbar.spell:SetText(cfg.spellname and info.spellName or "")
end
local value = info.isChannel and (info.endTime - now) or (now - info.startTime)
if value < 0 then value = 0 elseif value > duration then value = duration end
castbar:SetValue(value)
SetCastbarText(castbar, info.endTime - now)
if not castbar.isShown then
castbar.isShown = true
castbar:Show()
end
end
function HideCast(plate)
activeCasts[plate] = nil
local castbar = plate.castbar
if castbar.isShown then
castbar.isShown = nil
castbar.lastEndTime = nil
castbar:Hide()
end
end
-- Start or stop the plate's castbar from the cast cache and the config.
function UpdateCast(plate)
local info = castState[plate.guid]
if info and cfg.showcastbar and (plate.istarget or not cfg.targetcastbar) then
if not activeCasts[plate] then
activeCasts[plate] = true
plate.casttick = 0
driver:Show()
end
UpdateCastbar(plate, info, GetTime())
else
HideCast(plate)
end
end
driver:SetScript("OnUpdate", function() driver:SetScript("OnUpdate", function()
local now = GetTime() local now = GetTime()
-- The target's bar sweeps every frame; the others at the castbar rate.
for plate in pairs(activeCasts) do
if plate.istarget or plate.casttick + cfg.throttle_castbar <= now then
plate.casttick = now
local info = castState[plate.guid]
if info and info.endTime > now then
UpdateCastbar(plate, info, now)
else
-- ran out without a STOP: treat it as one
castState[plate.guid] = nil
HideCast(plate)
UpdateCombatColor(plate)
end
end
end
for plate in pairs(zooming) do for plate in pairs(zooming) do
local f = (now - plate.zoomStart) / ZOOM_TIME local f = (now - plate.zoomStart) / ZOOM_TIME
if f >= 1 then if f >= 1 then
@@ -941,10 +855,57 @@ pfUI:RegisterModule("nameplates", function ()
end end
SetZoom(plate, plate.zoomIn and f or 1 - f) SetZoom(plate, plate.zoomIn and f or 1 - f)
end end
if not next(zooming) then this:Hide() end
if not next(activeCasts) and not next(zooming) then this:Hide() end
end) end)
-- ============================================================================
-- Castbars: the bar's timer and its text binding follow the cast's duration
-- object on their own. Lua only starts, restyles and stops them.
-- ============================================================================
local function ShowCast(plate, info)
local castbar = plate.castbar
castbar:SetTimerDuration(info.duration, Enum.StatusBarInterpolation.Immediate,
info.isChannel and Enum.StatusBarTimerDirection.RemainingTime or Enum.StatusBarTimerDirection.ElapsedTime)
castbar.timer:SetDuration(info.duration)
castbar.timer:Enable()
SetCastbarShield(castbar, castbar.icon, info.noInterrupt, info.isChannel)
if info.icon then
castbar.icon.tex:SetTexture(info.icon)
castbar.icon.tex:SetTexCoord(.1,.9,.1,.9)
end
castbar.spell:SetText(cfg.spellname and info.spellName or "")
-- armed exactly while the bar shows
if castbar.expiry then castbar.expiry:Cancel() end
castbar.expiry = C_Timer.NewTimer(info.duration:GetRemainingDuration(), castbar.onExpire)
castbar:Show()
end
function HideCast(plate)
local castbar = plate.castbar
if not castbar.expiry then return end
castbar.expiry:Cancel()
castbar.expiry = nil
castbar.timer:Disable()
castbar:Hide()
end
-- Start or stop the plate's castbar from the cast cache and the config.
function UpdateCast(plate)
local info = castState[plate.guid]
if info and not info.duration:HasExpired() and cfg.showcastbar and (plate.istarget or not cfg.targetcastbar) then
ShowCast(plate, info)
else
HideCast(plate)
end
end
-- The bar's cast ran out without a STOP: treat it as one.
local function ExpireCast(plate)
castState[plate.guid] = nil
HideCast(plate)
UpdateCombatColor(plate)
end
-- ============================================================================ -- ============================================================================
-- Cast events (per plate) -- Cast events (per plate)
-- ============================================================================ -- ============================================================================
@@ -1281,6 +1242,11 @@ pfUI:RegisterModule("nameplates", function ()
castbar.text:SetNonSpaceWrap(false) castbar.text:SetNonSpaceWrap(false)
castbar.text:SetTextColor(1,1,1,.5) castbar.text:SetTextColor(1,1,1,.5)
castbar.timer = C_DurationUtil.CreateDurationTextBinding()
castbar.timer:SetFontString(castbar.text)
castbar.timer:SetFormatter(castFormatter)
castbar.onExpire = function() ExpireCast(nameplate) end
castbar.spell = castbar:CreateFontString("Status", "DIALOG", "GameFontNormal") castbar.spell = castbar:CreateFontString("Status", "DIALOG", "GameFontNormal")
castbar.spell:SetPoint("CENTER", castbar, "CENTER") castbar.spell:SetPoint("CENTER", castbar, "CENTER")
castbar.spell:SetNonSpaceWrap(false) castbar.spell:SetNonSpaceWrap(false)
@@ -1372,8 +1338,6 @@ pfUI:RegisterModule("nameplates", function ()
nameplate.castbar:SetStatusBarTexture(cbtexture or hptexture) nameplate.castbar:SetStatusBarTexture(cbtexture or hptexture)
local cbr, cbg, cbb, cba = GetStringColor(C.appearance.castbar.castbarcolor) local cbr, cbg, cbb, cba = GetStringColor(C.appearance.castbar.castbarcolor)
nameplate.castbar:SetStatusBarColor(cbr, cbg, cbb, cba) nameplate.castbar:SetStatusBarColor(cbr, cbg, cbb, cba)
-- reset endTime cache so color/texture refresh takes effect on next cast
nameplate.castbar.lastEndTime = nil
CreateBackdrop(nameplate.castbar, default_border) CreateBackdrop(nameplate.castbar, default_border)
nameplate.castbar.text:SetFont(font, font_size, "OUTLINE") nameplate.castbar.text:SetFont(font, font_size, "OUTLINE")
@@ -1652,8 +1616,6 @@ pfUI:RegisterModule("nameplates", function ()
local plate = info and plateByGuid[guid] local plate = info and plateByGuid[guid]
if not plate then return end if not plate then return end
info.noInterrupt = event == "UNIT_SPELLCAST_NOT_INTERRUPTIBLE" info.noInterrupt = event == "UNIT_SPELLCAST_NOT_INTERRUPTIBLE"
-- UpdateCastbar only restyles on a new cast; force it
plate.castbar.lastEndTime = nil
UpdateCast(plate) UpdateCast(plate)
end end
events.UNIT_SPELLCAST_NOT_INTERRUPTIBLE = events.UNIT_SPELLCAST_INTERRUPTIBLE events.UNIT_SPELLCAST_NOT_INTERRUPTIBLE = events.UNIT_SPELLCAST_INTERRUPTIBLE