mirror of
https://github.com/brues-code/pfUI.git
synced 2026-11-03 09:26:57 +00:00
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:
+116
-151
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user