mirror of
https://github.com/brues-code/pfUI.git
synced 2026-09-27 18:06:02 +00:00
822c873132
Drive the cast bar from cast lifecycle events instead of polling C_Spell every frame. OnUpdate now only animates a stamped start/end and fades out; all state transitions come from events: - player: vanilla SPELLCAST_START / _STOP / _FAILED / _INTERRUPTED / _CHANNEL_START / _CHANNEL_STOP, plus nampower SPELL_START_SELF (the only signal for a chained same-spell recast, which never runs the client cast path) and SPELL_DELAYED_SELF for pushback (applied from its delayMs arg). - non-player (target/focus): nampower SPELL_START_OTHER / SPELL_FAILED_OTHER + PLAYER_TARGET_CHANGED / PLAYER_FOCUS_CHANGED. Data comes from ClassicAPI's C_Spell.UnitCastingInfo / UnitChannelInfo (player exact; other units from the SMSG_SPELL_START cache). SPELL_START_* re-polls are deferred one frame so ClassicAPI's packet co-hook has stamped before the read. SPELLCAST_CHANNEL_STOP only clears when a channel is shown, so a lagged channel-stop doesn't wipe a following cast's bar. Remove the now-unused CASTBAR_EVENT_* constants from compat/vanilla.lua.
420 lines
17 KiB
Lua
420 lines
17 KiB
Lua
pfUI:RegisterModule("castbar", function ()
|
|
|
|
local font = C.castbar.use_unitfonts == "1" and pfUI.font_unit or pfUI.font_default
|
|
local font_size = C.castbar.use_unitfonts == "1" and C.global.font_unit_size or C.global.font_size
|
|
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
|
|
end
|
|
|
|
-- Clear cast state on the bar. Shows the bar full for one frame, then
|
|
-- OnUpdate fades it out.
|
|
local function ClearBar(cb)
|
|
cb.startTime, cb.endTime, cb.isChannel = nil, nil, nil
|
|
cb.activeName, cb.spellID = nil, nil
|
|
cb.lastMax = nil
|
|
cb.delay = 0
|
|
cb.bar:SetMinMaxValues(1, 100)
|
|
cb.bar:SetValue(100)
|
|
cb.fadeout = 1
|
|
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.
|
|
local function StampBar(cb, name, tex, startMs, endMs, spellID, isChannel, delayMs)
|
|
cb.startTime = startMs
|
|
cb.endTime = endMs
|
|
cb.isChannel = isChannel
|
|
cb.spellID = spellID
|
|
cb.activeName = name
|
|
cb.delay = (delayMs or 0) / 1000
|
|
cb:SetAlpha(1)
|
|
cb.fadeout = nil
|
|
|
|
cb.bar:SetStatusBarColor(strsplit(",", C.appearance.castbar[isChannel and "channelcolor" or "castbarcolor"]))
|
|
|
|
local rank = ""
|
|
if spellID and GetSpellRecField then
|
|
rank = GetSpellRecField(spellID, "rank") or ""
|
|
end
|
|
local spellname = (cb.showname and name) and (name .. " ") or ""
|
|
local rankstr = (cb.showrank and rank ~= "") and string.format("|cffaaffcc[%s]|r", rank) or ""
|
|
cb.bar.left:SetText(spellname .. rankstr)
|
|
|
|
if tex and cb.showicon then
|
|
local size = cb:GetHeight()
|
|
cb.icon:Show()
|
|
cb.icon:SetHeight(size)
|
|
cb.icon:SetWidth(size)
|
|
cb.icon.texture:SetTexture(tex)
|
|
cb.bar:SetPoint("TOPLEFT", cb.icon, "TOPRIGHT", cb.spacing, 0)
|
|
else
|
|
cb.bar:SetPoint("TOPLEFT", cb, 0, 0)
|
|
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:Show()
|
|
else
|
|
cb.bar.lag:Hide()
|
|
end
|
|
|
|
cb.bar:SetMinMaxValues(0, duration)
|
|
cb.lastMax = duration
|
|
end
|
|
|
|
-- One-shot poll: read C_Spell for the bar's unit, stamp or clear. Called
|
|
-- from event handlers (cast start, target/focus change), never per-frame.
|
|
local function RefreshBar(cb)
|
|
local query = cb.unitstr ~= "" and cb.unitstr or cb.unitname
|
|
if not query or (cb.unitstr ~= "" and not UnitExists(cb.unitstr)) then
|
|
ClearBar(cb)
|
|
return
|
|
end
|
|
local name, _, tex, startMs, endMs, _, _, _, spellID, _, delayMs = C_Spell.UnitCastingInfo(query)
|
|
local isChan
|
|
if not name then
|
|
name, _, tex, startMs, endMs, _, _, spellID = C_Spell.UnitChannelInfo(query)
|
|
isChan = true
|
|
end
|
|
-- Synthetic fallback for abilities the engine treats as instant-cast but
|
|
-- that have a meaningful wait window (e.g. Turtle WoW Steady Shot on the
|
|
-- ranged-swing queue). Per-unit table populated by module-side hooks.
|
|
if not name and pfUI.synthetic_casts and pfUI.synthetic_casts[query] then
|
|
local s = pfUI.synthetic_casts[query]
|
|
if s.endMs > GetTime() * 1000 then
|
|
name, tex, startMs, endMs, spellID = s.name, s.icon, s.startMs, s.endMs, s.spellID
|
|
isChan = nil
|
|
end
|
|
end
|
|
if name and startMs and endMs then
|
|
StampBar(cb, name, tex, startMs, endMs, spellID, isChan, delayMs)
|
|
else
|
|
ClearBar(cb)
|
|
end
|
|
end
|
|
|
|
local function CreateCastbar(name, parent, unitstr, unitname)
|
|
local cb = CreateFrame("Frame", name, parent or UIParent)
|
|
|
|
cb:SetHeight(C.global.font_size * 1.5)
|
|
cb:SetFrameStrata("MEDIUM")
|
|
cb:SetFrameLevel(8)
|
|
|
|
cb.unitstr = unitstr
|
|
cb.unitname = unitname
|
|
|
|
-- icon
|
|
cb.icon = CreateFrame("Frame", nil, cb)
|
|
cb.icon:SetPoint("TOPLEFT", 0, 0)
|
|
cb.icon:SetHeight(16)
|
|
cb.icon:SetWidth(16)
|
|
|
|
cb.icon.texture = cb.icon:CreateTexture(nil, "OVERLAY")
|
|
cb.icon.texture:SetAllPoints()
|
|
cb.icon.texture:SetTexCoord(.08, .92, .08, .92)
|
|
CreateBackdrop(cb.icon, default_border)
|
|
|
|
-- statusbar
|
|
cb.bar = CreateFrame("StatusBar", nil, cb)
|
|
cb.bar:SetStatusBarTexture(cbtexture)
|
|
cb.bar:ClearAllPoints()
|
|
cb.bar:SetAllPoints(cb)
|
|
cb.bar:SetMinMaxValues(0, 100)
|
|
cb.bar:SetValue(20)
|
|
local r,g,b,a = strsplit(",", C.appearance.castbar.castbarcolor)
|
|
cb.bar:SetStatusBarColor(r,g,b,a)
|
|
CreateBackdrop(cb.bar, default_border)
|
|
CreateBackdropShadow(cb.bar)
|
|
|
|
-- text left
|
|
cb.bar.left = cb.bar:CreateFontString("Status", "DIALOG", "GameFontNormal")
|
|
cb.bar.left:ClearAllPoints()
|
|
cb.bar.left:SetPoint("TOPLEFT", cb.bar, "TOPLEFT", 3 + C.castbar[unitstr].txtleftoffx, C.castbar[unitstr].txtleftoffy)
|
|
cb.bar.left:SetPoint("BOTTOMRIGHT", cb.bar, "BOTTOMRIGHT", -3 + C.castbar[unitstr].txtleftoffx, C.castbar[unitstr].txtleftoffy)
|
|
cb.bar.left:SetNonSpaceWrap(false)
|
|
cb.bar.left:SetFontObject(GameFontWhite)
|
|
cb.bar.left:SetTextColor(1,1,1,1)
|
|
cb.bar.left:SetFont(font, font_size, "OUTLINE")
|
|
cb.bar.left:SetJustifyH("left")
|
|
|
|
-- text right
|
|
cb.bar.right = cb.bar:CreateFontString("Status", "DIALOG", "GameFontNormal")
|
|
cb.bar.right:ClearAllPoints()
|
|
cb.bar.right:SetPoint("TOPLEFT", cb.bar, "TOPLEFT", 3 + C.castbar[unitstr].txtrightoffx, C.castbar[unitstr].txtrightoffy)
|
|
cb.bar.right:SetPoint("BOTTOMRIGHT", cb.bar, "BOTTOMRIGHT", -3 + C.castbar[unitstr].txtrightoffx, C.castbar[unitstr].txtrightoffy)
|
|
cb.bar.right:SetNonSpaceWrap(false)
|
|
cb.bar.right:SetFontObject(GameFontWhite)
|
|
cb.bar.right:SetTextColor(1,1,1,1)
|
|
cb.bar.right:SetFont(font, font_size, "OUTLINE")
|
|
cb.bar.right:SetJustifyH("right")
|
|
|
|
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)
|
|
cb.bar.lag:SetTexture(1,.2,.2,.2)
|
|
|
|
-- OnUpdate animates the bar fill and fades it out on completion. 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 this.drag and this.drag:IsShown() then
|
|
this:SetAlpha(1)
|
|
return
|
|
end
|
|
|
|
if this.fadeout and this:GetAlpha() > 0 then
|
|
this:SetAlpha(this:GetAlpha() - 0.05)
|
|
if this:GetAlpha() <= 0 then this.fadeout = nil end
|
|
return
|
|
end
|
|
|
|
if not this.endTime then return end
|
|
|
|
-- Non-player bars: if the unit disappears (target died / detarget),
|
|
-- drop the bar immediately.
|
|
if this.unitstr ~= "" and 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)
|
|
|
|
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)
|
|
|
|
-- Cast lifecycle events. Player bars react to vanilla SPELLCAST_*; non-
|
|
-- player bars also react to Nampower SPELL_*_OTHER (gated by the
|
|
-- NP_EnableSpell{Start,Go}Events CVars, enabled by libdebuff) plus the
|
|
-- retarget event. Player events also feed non-player bars for the
|
|
-- target=self case.
|
|
cb:RegisterEvent("SPELLCAST_START")
|
|
cb:RegisterEvent("SPELLCAST_STOP")
|
|
cb:RegisterEvent("SPELLCAST_FAILED")
|
|
cb:RegisterEvent("SPELLCAST_INTERRUPTED")
|
|
cb:RegisterEvent("SPELLCAST_CHANNEL_START")
|
|
cb:RegisterEvent("SPELLCAST_CHANNEL_STOP")
|
|
cb:RegisterEvent("SPELLCAST_CHANNEL_UPDATE")
|
|
cb:RegisterEvent("SPELL_DELAYED_SELF")
|
|
-- Chained same-spell recasts never run the client cast path (the 1.12
|
|
-- engine short-circuits at spellID == current-cast), so vanilla
|
|
-- SPELLCAST_START never fires for them. nampower's SPELL_START_SELF
|
|
-- (server-driven) is the only signal that shows them.
|
|
cb:RegisterEvent("SPELL_START_SELF")
|
|
if unitstr ~= "player" and unitstr ~= "" then
|
|
cb:RegisterEvent("SPELL_START_OTHER")
|
|
cb:RegisterEvent("SPELL_FAILED_OTHER")
|
|
if unitstr == "target" then
|
|
cb:RegisterEvent("PLAYER_TARGET_CHANGED")
|
|
elseif unitstr == "focus" then
|
|
cb:RegisterEvent("PLAYER_FOCUS_CHANGED")
|
|
end
|
|
end
|
|
|
|
cb:SetScript("OnEvent", function()
|
|
local unit = this.unitstr
|
|
|
|
if event == "PLAYER_TARGET_CHANGED" or event == "PLAYER_FOCUS_CHANGED" then
|
|
RefreshBar(this)
|
|
return
|
|
end
|
|
|
|
if event == "SPELL_START_OTHER" then
|
|
-- arg3=casterGuid. Defer one frame so ClassicAPI's UnitChannelInfo
|
|
-- can see the engine's +0x228 broadcast for remote-unit channels
|
|
-- (the cohook+packet handler runs in the same frame; the broadcast
|
|
-- propagates after).
|
|
if arg3 == UnitGUID(unit) then
|
|
local target = this
|
|
C_Timer.After(0, function() RefreshBar(target) end)
|
|
end
|
|
return
|
|
end
|
|
|
|
if event == "SPELL_FAILED_OTHER" then
|
|
if arg1 == UnitGUID(unit) then ClearBar(this) end
|
|
return
|
|
end
|
|
|
|
-- Vanilla SPELLCAST_* + SPELL_DELAYED_SELF fire only for the local
|
|
-- player. Non-player bars handle them only when their unit currently
|
|
-- resolves to the player (target=self / focus=self).
|
|
if unit ~= "player" and UnitGUID(unit) ~= UnitGUID("player") then return end
|
|
|
|
if event == "SPELLCAST_START" or event == "SPELLCAST_CHANNEL_START" then
|
|
RefreshBar(this)
|
|
elseif event == "SPELL_START_SELF" then
|
|
-- Catches chained same-spell recasts (no SPELLCAST_START fires).
|
|
-- Defer one frame so ClassicAPI's SMSG_SPELL_START co-hook has
|
|
-- stamped g_cast before we poll, regardless of co-hook order.
|
|
local target = this
|
|
C_Timer.After(0, function() RefreshBar(target) end)
|
|
elseif event == "SPELLCAST_CHANNEL_STOP" then
|
|
-- A channel's stop can arrive after a following cast already claimed
|
|
-- the bar (channel->cast transition); only clear if a channel is
|
|
-- actually being shown, so it doesn't wipe an active cast bar.
|
|
if this.isChannel then ClearBar(this) end
|
|
elseif event == "SPELLCAST_STOP" or event == "SPELLCAST_FAILED"
|
|
or event == "SPELLCAST_INTERRUPTED" then
|
|
ClearBar(this)
|
|
elseif event == "SPELL_DELAYED_SELF" then
|
|
-- Cast pushback. nampower's event carries the delay (arg2); apply it
|
|
-- locally rather than re-polling, so the bar doesn't depend on
|
|
-- ClassicAPI's SMSG_SPELL_DELAYED co-hook having bumped g_cast before
|
|
-- this fires (co-hook order vs nampower is not guaranteed).
|
|
if not this.endTime or not arg2 then return end
|
|
local delayMs = tonumber(arg2) or 0
|
|
if delayMs > 0 then
|
|
this.delay = (this.delay or 0) + delayMs / 1000
|
|
this.endTime = this.endTime + delayMs
|
|
local newDuration = (this.endTime - this.startTime) / 1000
|
|
this.bar:SetMinMaxValues(0, newDuration)
|
|
this.lastMax = newDuration
|
|
end
|
|
elseif event == "SPELLCAST_CHANNEL_UPDATE" then
|
|
-- Channel pushback. ClassicAPI doesn't track channel delay in
|
|
-- g_channel, so we adjust endTime + delay locally and resize the
|
|
-- bar so OnUpdate animates against the new total.
|
|
if not this.endTime or not arg1 then return end
|
|
local newEndMs = GetTime() * 1000 + arg1
|
|
local diff = this.endTime - newEndMs
|
|
if diff > 50 then
|
|
this.delay = (this.delay or 0) + diff / 1000
|
|
this.endTime = newEndMs
|
|
local newDuration = (this.endTime - this.startTime) / 1000
|
|
this.bar:SetMinMaxValues(0, newDuration)
|
|
this.lastMax = newDuration
|
|
end
|
|
end
|
|
end)
|
|
|
|
cb:SetAlpha(0)
|
|
return cb
|
|
end
|
|
|
|
pfUI.castbar = CreateFrame("Frame", "pfCastBar", UIParent)
|
|
|
|
-- hide blizzard
|
|
if C.castbar.player.hide_blizz == "1" then
|
|
CastingBarFrame:SetScript("OnShow", function() CastingBarFrame:Hide() end)
|
|
CastingBarFrame:UnregisterAllEvents()
|
|
CastingBarFrame:Hide()
|
|
end
|
|
|
|
-- [[ pfPlayerCastbar ]] --
|
|
if C.castbar.player.hide_pfui == "0" then
|
|
pfUI.castbar.player = CreateCastbar("pfPlayerCastbar", UIParent, "player")
|
|
pfUI.castbar.player.showicon = C.castbar.player.showicon == "1" and true or nil
|
|
pfUI.castbar.player.showname = C.castbar.player.showname == "1" and true or nil
|
|
pfUI.castbar.player.showtimer = C.castbar.player.showtimer == "1" and true or nil
|
|
pfUI.castbar.player.showlag = C.castbar.player.showlag == "1" and true or nil
|
|
pfUI.castbar.player.showrank = C.castbar.player.showrank == "1" and true or nil
|
|
pfUI.castbar.player.spacing = default_border * 2 + tonumber(C.unitframes.player.pspace) * GetPerfectPixel()
|
|
|
|
if pfUI.uf.player then
|
|
local anchor = pfUI.uf.player.portrait:GetHeight() > pfUI.uf.player:GetHeight() and pfUI.uf.player.power or pfUI.uf.player
|
|
local width = C.castbar.player.width ~= "-1" and C.castbar.player.width or anchor:GetWidth()
|
|
pfUI.castbar.player:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -pfUI.castbar.player.spacing)
|
|
pfUI.castbar.player:SetWidth(width)
|
|
else
|
|
local width = C.castbar.player.width ~= "-1" and C.castbar.player.width or 200
|
|
pfUI.castbar.player:SetPoint("CENTER", 0, -200)
|
|
pfUI.castbar.player:SetWidth(width)
|
|
end
|
|
|
|
if C.castbar.player.height ~= "-1" then
|
|
pfUI.castbar.player:SetHeight(C.castbar.player.height)
|
|
end
|
|
|
|
UpdateMovable(pfUI.castbar.player)
|
|
end
|
|
|
|
-- [[ pfTargetCastbar ]] --
|
|
if C.castbar.target.hide_pfui == "0" then
|
|
pfUI.castbar.target = CreateCastbar("pfTargetCastbar", UIParent, "target")
|
|
pfUI.castbar.target.showicon = C.castbar.target.showicon == "1" and true or nil
|
|
pfUI.castbar.target.showname = C.castbar.target.showname == "1" and true or nil
|
|
pfUI.castbar.target.showtimer = C.castbar.target.showtimer == "1" and true or nil
|
|
pfUI.castbar.target.showlag = C.castbar.target.showlag == "1" and true or nil
|
|
pfUI.castbar.target.showrank = C.castbar.target.showrank == "1" and true or nil
|
|
pfUI.castbar.target.spacing = default_border * 2 + tonumber(C.unitframes.target.pspace) * GetPerfectPixel()
|
|
|
|
if pfUI.uf.target then
|
|
local anchor = pfUI.uf.target.portrait:GetHeight() > pfUI.uf.target:GetHeight() and pfUI.uf.target.power or pfUI.uf.target
|
|
local width = C.castbar.target.width ~= "-1" and C.castbar.target.width or anchor:GetWidth()
|
|
pfUI.castbar.target:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -pfUI.castbar.target.spacing)
|
|
pfUI.castbar.target:SetWidth(width)
|
|
else
|
|
local width = C.castbar.target.width ~= "-1" and C.castbar.target.width or 200
|
|
pfUI.castbar.target:SetPoint("CENTER", 0, -225)
|
|
pfUI.castbar.target:SetWidth(width)
|
|
end
|
|
|
|
if C.castbar.target.height ~= "-1" then
|
|
pfUI.castbar.target:SetHeight(C.castbar.target.height)
|
|
end
|
|
|
|
UpdateMovable(pfUI.castbar.target)
|
|
end
|
|
|
|
-- [[ pfFocusCastbar ]] --
|
|
if C.castbar.focus.hide_pfui == "0" and pfUI.uf.focus then
|
|
pfUI.castbar.focus = CreateCastbar("pfFocusCastbar", UIParent, "focus")
|
|
pfUI.castbar.focus.showicon = C.castbar.focus.showicon == "1" and true or nil
|
|
pfUI.castbar.focus.showname = C.castbar.focus.showname == "1" and true or nil
|
|
pfUI.castbar.focus.showtimer = C.castbar.focus.showtimer == "1" and true or nil
|
|
pfUI.castbar.focus.showlag = C.castbar.focus.showlag == "1" and true or nil
|
|
pfUI.castbar.focus.showrank = C.castbar.focus.showrank == "1" and true or nil
|
|
pfUI.castbar.focus.spacing = default_border * 2 + tonumber(C.unitframes.focus.pspace) * GetPerfectPixel()
|
|
|
|
local anchor = pfUI.uf.focus.portrait:GetHeight() > pfUI.uf.focus:GetHeight() and pfUI.uf.focus.power or pfUI.uf.focus
|
|
local width = C.castbar.focus.width ~= "-1" and C.castbar.focus.width or anchor:GetWidth()
|
|
pfUI.castbar.focus:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -pfUI.castbar.focus.spacing)
|
|
pfUI.castbar.focus:SetWidth(width)
|
|
|
|
if C.castbar.focus.height ~= "-1" then
|
|
pfUI.castbar.focus:SetHeight(C.castbar.focus.height)
|
|
end
|
|
|
|
-- bind castbar to the "focus" unit token; the GUID resolves at read time
|
|
pfUI.castbar.focus.unitstr = "focus"
|
|
|
|
UpdateMovable(pfUI.castbar.focus)
|
|
end
|
|
end) |