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
+116 -151
View File
@@ -5,36 +5,68 @@ pfUI:RegisterModule("castbar", function ()
local rawborder, default_border = GetBorderSize("unitframes")
local cbtexture = pfUI.media[C.appearance.castbar.texture]
-- Helper function for castbar timer formatting
local function FormatCastbarTime(value)
if C.unitframes.castbardecimals == "1" then
-- 1 decimal, round half up (matches Blizzard spellbook display)
return string.format("%.1f", floor(value * 10 + 0.5) / 10)
else
-- 2 decimals (default)
return string.format("%.2f", value)
end
-- Castbar timer text: 1 decimal rounded half up (matches Blizzard spellbook
-- display), or 2 decimals (default)
local formatter = C_StringUtil.CreateNumericRuleFormatter()
if C.unitframes.castbardecimals == "1" then
formatter:AddBreakpoint({ threshold = 0, step = .1, format = "%.1f" })
else
formatter:AddBreakpoint({ threshold = 0, format = "%.2f" })
end
-- Clear cast state on the bar and start the fade-out. On a normal end the bar
-- is left at its current fill (a completed cast is already ~full; nothing
-- snaps to full, which used to flash for a frame when the next cast stamped
-- the bar). When `failed` is set for a cast that was actually in progress, the
-- bar flashes full red before fading — the cancelled-cast indicator.
-- "cur / max": a cast counts up, a channel counts down
local castText = {
{ property = Enum.DurationTextBindingProperty.ElapsedDuration, formatter = formatter },
{ property = Enum.DurationTextBindingProperty.TotalDuration, formatter = formatter },
}
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 wasActive = cb.endTime ~= nil
cb.startTime, cb.endTime, cb.isChannel = nil, nil, nil
if cb.expiry then cb.expiry:Cancel() end
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.isTradeskill = nil
cb.tradeskillTotal, cb.tradeskillCompleted, cb.tradeskillSpellID = nil, nil, nil
cb.tradeskillSingleMs, cb.currentCraftStart = nil, nil
cb.lastMax = nil
cb.tradeskillSingle = nil
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:IsShown() and not cb.fade:IsPlaying() then cb.fade:Play() end
end
@@ -51,44 +83,48 @@ pfUI:RegisterModule("castbar", function ()
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
-- fills continuously across the chain instead of resetting per craft.
-- 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).
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.tradeskillCompleted = 0
cb.tradeskillSpellID = cb.spellID
local single = endMs - startMs
cb.tradeskillSingleMs = single
cb.currentCraftStart = startMs
local mergedEnd = startMs + single * count
cb.endTime = mergedEnd
local duration = (mergedEnd - startMs) / 1000
cb.bar:SetMinMaxValues(0, duration)
cb.lastMax = duration
if cb.bar.spark then cb.bar.spark:Show() end
cb.tradeskillSingle = single
cb.craft:SetTimeFromStart(start, single)
cb.duration:SetTimeFromStart(start, single * count)
ArmExpiry(cb)
cb.bar.spark:Show()
cb:SetScript("OnUpdate", MoveSpark)
UpdateTradeskillLabel(cb)
end
-- 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
-- edge of the bar.
-- UNIT_SPELLCAST_START handler) and re-fit the bar's end to the crafts left.
local function StartTradeskillCraft(cb)
cb.currentCraftStart = GetTime() * 1000
local remaining = cb.tradeskillTotal - (cb.tradeskillCompleted or 0)
cb.endTime = cb.currentCraftStart + cb.tradeskillSingleMs * remaining
local duration = (cb.endTime - cb.startTime) / 1000
cb.bar:SetMinMaxValues(0, duration)
cb.lastMax = duration
local now = GetTime()
local remaining = cb.tradeskillTotal - cb.tradeskillCompleted
cb.craft:SetTimeFromStart(now, cb.tradeskillSingle)
cb.duration:SetTimeSpan(cb.duration:GetStartTime(), now + cb.tradeskillSingle * remaining)
ArmExpiry(cb)
UpdateTradeskillLabel(cb)
end
-- Stamp the bar with cast data and render text/icon/lag once. OnUpdate
-- then animates the fill from this state without touching C_Spell.
-- Stamp the bar with cast data and render text/icon/lag once. From here the
-- 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)
cb.startTime = startMs
cb.endTime = endMs
cb.duration:SetTimeSpan(startMs / 1000, endMs / 1000)
cb.active = true
cb.isChannel = isChannel
cb.spellID = spellID
cb.activeName = name
@@ -124,27 +160,18 @@ pfUI:RegisterModule("castbar", function ()
cb.icon:Hide()
end
local duration = (endMs - startMs) / 1000
if cb.showlag then
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()
else
cb.bar.lag:Hide()
end
cb.bar:SetMinMaxValues(0, duration)
cb.lastMax = duration
-- Prime the fill on this frame. StampBar otherwise leaves the previous
-- 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
cb.bar:SetTimerDuration(cb.duration, Enum.StatusBarInterpolation.Immediate,
isChannel and Enum.StatusBarTimerDirection.RemainingTime or Enum.StatusBarTimerDirection.ElapsedTime)
UpdateTimerText(cb)
ArmExpiry(cb)
end
-- 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.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
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: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:SetPoint("TOPRIGHT", cb.bar, "TOPRIGHT", 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
-- 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.
cb.bar.spark = cb.bar:CreateTexture(nil, "OVERLAY")
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:Hide()
-- Fade-out after a cast ends. The bar is hidden while idle, so its OnUpdate
-- only runs during a cast; unlock mode keeps it shown for dragging.
local function unlocked() return cb.drag and cb.drag:IsShown() end
-- Fade-out after a cast ends. The bar is hidden while idle; unlock mode
-- keeps it shown for dragging.
cb.fade = cb:CreateAnimationGroup()
local fade = cb.fade:CreateAnimation("Alpha")
fade:SetChange(-1)
fade:SetDuration(.4)
cb.fade:SetScript("OnFinished", function()
if not unlocked() 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
if not (cb.drag and cb.drag:IsShown()) then cb:Hide() end
end)
-- Cast lifecycle, entirely on ClassicAPI's UNIT_SPELLCAST_* events. They
@@ -383,7 +353,7 @@ pfUI:RegisterModule("castbar", function ()
RefreshBar(this, arg5)
if this.isTradeskill and (this.pendingTradeskillCount or 0) > 1
and C.castbar.player.mergetradeskill == "1" then
EnterTradeskillMerge(this, this.startTime, this.endTime, this.pendingTradeskillCount)
EnterTradeskillMerge(this, this.pendingTradeskillCount)
end
this.pendingTradeskillCount = nil
end
@@ -392,7 +362,7 @@ pfUI:RegisterModule("castbar", function ()
-- count and clear when the chain is done. A no-op for normal casts,
-- which are cleared by UNIT_SPELLCAST_STOP.
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
ClearBar(this)
else
@@ -412,35 +382,30 @@ pfUI:RegisterModule("castbar", function ()
elseif event == "UNIT_SPELLCAST_FAILED" or event == "UNIT_SPELLCAST_INTERRUPTED" then
ClearBar(this, true)
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)
end
elseif event == "UNIT_SPELLCAST_DELAYED" or event == "UNIT_SPELLCAST_CHANNEL_UPDATE" then
-- 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
-- would reset that total and re-render icon/text). ClassicAPI moves
-- endMs (SpellDelayed_h bumps g_cast.endMs; the MSG_CHANNEL_UPDATE
-- co-hook rewrites g_channel.endMs) while startMs stays put, so we diff
-- endMs. delay is a positive magnitude; OnUpdate signs it "+" for casts
-- and "-" for channels. Skipped during a tradeskill merge.
if this.endTime and not this.tradeskillTotal then
local startMs, endMs
-- would reset that total and re-render icon/text). Pushback moves the
-- end time while the start stays put, so we diff the end. delay is a
-- positive magnitude; UpdateTimerText signs it "+" for casts and "-"
-- for channels. Skipped during a tradeskill merge.
if this.active and not this.tradeskillTotal then
local duration
if this.isChannel then
local _, _, _, s, e = C_Spell.UnitChannelInfo(unit)
startMs, endMs = s, e
duration = UnitChannelDuration(unit)
else
local _, _, _, s, e = C_Spell.UnitCastingInfo(unit)
startMs, endMs = s, e
duration = UnitCastingDuration(unit)
end
if startMs and endMs then
local shift = this.isChannel and (this.endTime - endMs) or (endMs - this.endTime)
this.delay = (this.delay or 0) + shift / 1000
this.startTime = startMs
this.endTime = endMs
local newDuration = (endMs - startMs) / 1000
this.bar:SetMinMaxValues(0, newDuration)
this.lastMax = newDuration
if duration then
local shift = duration:GetEndTime() - this.duration:GetEndTime()
this.delay = this.delay + (this.isChannel and -shift or shift)
this.duration:Assign(duration)
UpdateTimerText(this)
ArmExpiry(this)
end
end
end
-65
View File
@@ -1646,71 +1646,6 @@ pfUI:RegisterModule("gui", function ()
end)
-- 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()
local header = CreateConfig(nil, T["Tooltip Update Rate"], nil, nil, "header")
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 raidGuidCache = {} -- guid -> name of the player's group
local offtanks = {}
local activeCasts = {} -- overlay -> true while its castbar sweeps
local zooming = {} -- overlay -> true while its target zoom animates
local auraSlots = {} -- reusable GetAuraSlots buffer for the per-plate aura scan
local targetGuid
@@ -100,6 +99,7 @@ pfUI:RegisterModule("nameplates", function ()
-- Config caching
-- ============================================================================
local cfg = {}
local castFormatter = C_StringUtil.CreateNumericRuleFormatter()
local debufffilter, debufflist
local function DebuffFilterPopulate()
@@ -158,7 +158,13 @@ pfUI:RegisterModule("nameplates", function ()
cfg.showguildname = N.showguildname == "1"
cfg.abbrevname = C.unitframes.abbrevname == "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.keeptarget = N.target == "1"
cfg.critters = N.critters == "1"
@@ -187,8 +193,6 @@ pfUI:RegisterModule("nameplates", function ()
cfg.rightclick = N.rightclick == "1"
cfg.hlr, cfg.hlg, cfg.hlb, cfg.hla = GetStringColor(N.highlightcolor)
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
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
-- (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
-- vs channel itself.
-- vs channel itself. A cast whose start was never seen has no duration.
local function PollCastInfo(unit)
local name, _, texture, startMs, endMs, _, _, noInterrupt, spellID = C_Spell.UnitCastingInfo(unit)
local isChannel
if not name then
name, _, texture, startMs, endMs, _, noInterrupt, spellID = C_Spell.UnitChannelInfo(unit)
local name, _, texture, _, _, _, _, noInterrupt, spellID = C_Spell.UnitCastingInfo(unit)
local duration, isChannel
if name then
duration = UnitCastingDuration(unit)
else
name, _, texture, _, _, _, noInterrupt, spellID = C_Spell.UnitChannelInfo(unit)
duration = name and UnitChannelDuration(unit)
isChannel = true
end
if not name or not startMs or not endMs then return nil end
if not duration then return nil end
return {
spellName = name,
spellID = spellID,
icon = texture,
startTime = startMs / 1000,
endTime = endMs / 1000,
duration = (endMs - startMs) / 1000,
duration = duration,
isChannel = isChannel,
noInterrupt = noInterrupt,
}
@@ -279,7 +284,7 @@ pfUI:RegisterModule("nameplates", function ()
local mobTargetGuid = UnitGUID(target)
local hasTarget = mobTargetGuid and mobTargetGuid ~= NULL_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")
if targetingPlayer then
@@ -819,8 +824,7 @@ pfUI:RegisterModule("nameplates", function ()
end
-- ============================================================================
-- Animation driver: castbar sweeps and the target zoom. Shown only while
-- something moves.
-- Animation driver: the target zoom. Shown only while a plate zooms.
-- ============================================================================
local driver = CreateFrame("Frame", nil, UIParent)
driver:Hide()
@@ -841,98 +845,8 @@ pfUI:RegisterModule("nameplates", function ()
driver:Show()
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()
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
local f = (now - plate.zoomStart) / ZOOM_TIME
if f >= 1 then
@@ -941,10 +855,57 @@ pfUI:RegisterModule("nameplates", function ()
end
SetZoom(plate, plate.zoomIn and f or 1 - f)
end
if not next(activeCasts) and not next(zooming) then this:Hide() end
if not next(zooming) then this:Hide() 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)
-- ============================================================================
@@ -1281,6 +1242,11 @@ pfUI:RegisterModule("nameplates", function ()
castbar.text:SetNonSpaceWrap(false)
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:SetPoint("CENTER", castbar, "CENTER")
castbar.spell:SetNonSpaceWrap(false)
@@ -1372,8 +1338,6 @@ pfUI:RegisterModule("nameplates", function ()
nameplate.castbar:SetStatusBarTexture(cbtexture or hptexture)
local cbr, cbg, cbb, cba = GetStringColor(C.appearance.castbar.castbarcolor)
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)
nameplate.castbar.text:SetFont(font, font_size, "OUTLINE")
@@ -1652,8 +1616,6 @@ pfUI:RegisterModule("nameplates", function ()
local plate = info and plateByGuid[guid]
if not plate then return end
info.noInterrupt = event == "UNIT_SPELLCAST_NOT_INTERRUPTIBLE"
-- UpdateCastbar only restyles on a new cast; force it
plate.castbar.lastEndTime = nil
UpdateCast(plate)
end
events.UNIT_SPELLCAST_NOT_INTERRUPTIBLE = events.UNIT_SPELLCAST_INTERRUPTIBLE