castbar: Quartz-style tradeskill merge with per-craft spark

Hook DoTradeSkill to capture the requested count, then on the first
SPELLCAST_START of an isTradeskill cast stretch endTime to span all
crafts so the player bar fills continuously across the chain. Mid-chain
SPELLCAST_START / SPELL_START_SELF events refresh the "(N)" remaining
label and reset a per-craft spark that crosses the bar once per craft.
SPELL_GO_SELF counts completions; SPELLCAST_STOP no-ops while merged.

Gated by a new C.castbar.player.mergetradeskill knob (default on).
This commit is contained in:
Brues
2026-06-26 18:43:31 -05:00
parent 65bbea6fa9
commit e0bcbbf2dc
3 changed files with 142 additions and 11 deletions
+1
View File
@@ -714,6 +714,7 @@ function pfUI:LoadConfig()
pfUI:UpdateConfig("castbar", "player", "txtleftoffy", "0") pfUI:UpdateConfig("castbar", "player", "txtleftoffy", "0")
pfUI:UpdateConfig("castbar", "player", "showlag", "0") pfUI:UpdateConfig("castbar", "player", "showlag", "0")
pfUI:UpdateConfig("castbar", "player", "showrank", "0") pfUI:UpdateConfig("castbar", "player", "showrank", "0")
pfUI:UpdateConfig("castbar", "player", "mergetradeskill", "1")
pfUI:UpdateConfig("castbar", "player", "txtrightoffx", "0") pfUI:UpdateConfig("castbar", "player", "txtrightoffx", "0")
pfUI:UpdateConfig("castbar", "player", "txtrightoffy", "0") pfUI:UpdateConfig("castbar", "player", "txtrightoffy", "0")
pfUI:UpdateConfig("castbar", "target", "hide_pfui", "0") pfUI:UpdateConfig("castbar", "target", "hide_pfui", "0")
+140 -11
View File
@@ -21,21 +21,66 @@ pfUI:RegisterModule("castbar", function ()
local function ClearBar(cb) local function ClearBar(cb)
cb.startTime, cb.endTime, cb.isChannel = nil, nil, nil cb.startTime, cb.endTime, cb.isChannel = nil, nil, nil
cb.activeName, cb.spellID = 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.lastMax = nil
cb.delay = 0 cb.delay = 0
cb.bar:SetMinMaxValues(1, 100) cb.bar:SetMinMaxValues(1, 100)
cb.bar:SetValue(100) cb.bar:SetValue(100)
if cb.bar.spark then cb.bar.spark:Hide() end
cb.fadeout = 1 cb.fadeout = 1
end end
-- "Spellname (N)" label for an active tradeskill merge — N is the number
-- of crafts remaining. Falls back to the bare spell name when ≤1 left.
local function UpdateTradeskillLabel(cb)
if not cb.tradeskillTotal or not cb.activeName or not cb.showname then return end
local remaining = cb.tradeskillTotal - (cb.tradeskillCompleted or 0)
if remaining > 1 then
cb.bar.left:SetText(string.format("%s (%d)", cb.activeName, remaining))
else
cb.bar.left:SetText(cb.activeName)
end
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)
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
UpdateTradeskillLabel(cb)
end
-- Mark the start of craft 2..N within an active merge (called from the
-- SPELLCAST_START / SPELL_START_SELF handlers). Resets the per-craft
-- spark to the left edge of the bar.
local function StartTradeskillCraft(cb)
cb.currentCraftStart = GetTime() * 1000
UpdateTradeskillLabel(cb)
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. OnUpdate
-- then animates the fill from this state without touching C_Spell. -- then animates the fill from this state without touching C_Spell.
local function StampBar(cb, name, tex, startMs, endMs, spellID, isChannel, delayMs) local function StampBar(cb, name, tex, startMs, endMs, spellID, isChannel, delayMs, isTradeskill)
cb.startTime = startMs cb.startTime = startMs
cb.endTime = endMs cb.endTime = endMs
cb.isChannel = isChannel cb.isChannel = isChannel
cb.spellID = spellID cb.spellID = spellID
cb.activeName = name cb.activeName = name
cb.isTradeskill = isTradeskill
cb.delay = (delayMs or 0) / 1000 cb.delay = (delayMs or 0) / 1000
cb:SetAlpha(1) cb:SetAlpha(1)
cb.fadeout = nil cb.fadeout = nil
@@ -83,7 +128,7 @@ pfUI:RegisterModule("castbar", function ()
ClearBar(cb) ClearBar(cb)
return return
end end
local name, _, tex, startMs, endMs, _, _, _, spellID, _, delayMs = C_Spell.UnitCastingInfo(query) local name, _, tex, startMs, endMs, isTradeskill, _, _, spellID, _, delayMs = C_Spell.UnitCastingInfo(query)
local isChan local isChan
if not name then if not name then
name, _, tex, startMs, endMs, _, _, spellID = C_Spell.UnitChannelInfo(query) name, _, tex, startMs, endMs, _, _, spellID = C_Spell.UnitChannelInfo(query)
@@ -100,7 +145,7 @@ pfUI:RegisterModule("castbar", function ()
end end
end end
if name and startMs and endMs then if name and startMs and endMs then
StampBar(cb, name, tex, startMs, endMs, spellID, isChan, delayMs) StampBar(cb, name, tex, startMs, endMs, spellID, isChan, delayMs, isTradeskill)
else else
ClearBar(cb) ClearBar(cb)
end end
@@ -166,6 +211,17 @@ pfUI:RegisterModule("castbar", function ()
cb.bar.lag:SetPoint("BOTTOMRIGHT", cb.bar, "BOTTOMRIGHT", 0, 0) cb.bar.lag:SetPoint("BOTTOMRIGHT", cb.bar, "BOTTOMRIGHT", 0, 0)
cb.bar.lag:SetTexture(1,.2,.2,.2) cb.bar.lag:SetTexture(1,.2,.2,.2)
-- 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
-- while a merge is active; hidden otherwise.
cb.bar.spark = cb.bar:CreateTexture(nil, "OVERLAY")
cb.bar.spark:SetTexture(1, 1, 1, 0.8)
cb.bar.spark:SetWidth(2)
cb.bar.spark:SetPoint("TOP", cb.bar, "TOPLEFT", 0, 0)
cb.bar.spark:SetPoint("BOTTOM", cb.bar, "BOTTOMLEFT", 0, 0)
cb.bar.spark:Hide()
-- OnUpdate animates the bar fill and fades it out on completion. All -- OnUpdate animates the bar fill and fades it out on completion. All
-- state transitions (cast start/stop/interrupt, channel start/stop, -- state transitions (cast start/stop/interrupt, channel start/stop,
-- pushback) come from the event handler below — we never poll C_Spell -- pushback) come from the event handler below — we never poll C_Spell
@@ -209,6 +265,19 @@ pfUI:RegisterModule("castbar", function ()
this.bar:SetValue(cur) 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.showtimer then
if (this.delay or 0) > 0 then if (this.delay or 0) > 0 then
local prefix = "|cffffaaaa" .. (this.isChannel and "-" or "+") .. FormatCastbarTime(this.delay) .. " |r " local prefix = "|cffffaaaa" .. (this.isChannel and "-" or "+") .. FormatCastbarTime(this.delay) .. " |r "
@@ -237,6 +306,9 @@ pfUI:RegisterModule("castbar", function ()
-- SPELLCAST_START never fires for them. nampower's SPELL_START_SELF -- SPELLCAST_START never fires for them. nampower's SPELL_START_SELF
-- (server-driven) is the only signal that shows them. -- (server-driven) is the only signal that shows them.
cb:RegisterEvent("SPELL_START_SELF") cb:RegisterEvent("SPELL_START_SELF")
if unitstr == "player" then
cb:RegisterEvent("SPELL_GO_SELF")
end
if unitstr ~= "player" and unitstr ~= "" then if unitstr ~= "player" and unitstr ~= "" then
cb:RegisterEvent("SPELL_START_OTHER") cb:RegisterEvent("SPELL_START_OTHER")
cb:RegisterEvent("SPELL_FAILED_OTHER") cb:RegisterEvent("SPELL_FAILED_OTHER")
@@ -262,7 +334,7 @@ pfUI:RegisterModule("castbar", function ()
-- propagates after). -- propagates after).
if arg3 == UnitGUID(unit) then if arg3 == UnitGUID(unit) then
local target = this local target = this
C_Timer.After(0, function() RefreshBar(target) end) RunNextFrame(function() RefreshBar(target) end)
end end
return return
end end
@@ -275,24 +347,70 @@ pfUI:RegisterModule("castbar", function ()
-- Vanilla SPELLCAST_* + SPELL_DELAYED_SELF fire only for the local -- Vanilla SPELLCAST_* + SPELL_DELAYED_SELF fire only for the local
-- player. Non-player bars handle them only when their unit currently -- player. Non-player bars handle them only when their unit currently
-- resolves to the player (target=self / focus=self). -- resolves to the player (target=self / focus=self).
if unit ~= "player" and UnitGUID(unit) ~= UnitGUID("player") then return end if not UnitIsUnit(unit, 'player') then return end
if event == "SPELLCAST_START" or event == "SPELLCAST_CHANNEL_START" then if event == "SPELLCAST_START" or event == "SPELLCAST_CHANNEL_START" then
RefreshBar(this) if this.tradeskillTotal then
-- Mid-chain craft N+1 of N. Keep the merged bar; resync the spark
-- to the new craft's start and refresh the "(N)" count label.
StartTradeskillCraft(this)
else
RefreshBar(this)
if this.isTradeskill and (this.pendingTradeskillCount or 0) > 1
and C.castbar.player.mergetradeskill == "1" then
EnterTradeskillMerge(this, this.startTime, this.endTime, this.pendingTradeskillCount)
end
this.pendingTradeskillCount = nil
end
elseif event == "SPELL_START_SELF" then elseif event == "SPELL_START_SELF" then
-- Catches chained same-spell recasts (no SPELLCAST_START fires). -- Catches chained same-spell recasts (no SPELLCAST_START fires) —
-- including tradeskill chaining where craft 2..N reuse one spell.
-- Defer one frame so ClassicAPI's SMSG_SPELL_START co-hook has -- Defer one frame so ClassicAPI's SMSG_SPELL_START co-hook has
-- stamped g_cast before we poll, regardless of co-hook order. -- stamped g_cast before we poll, regardless of co-hook order.
local target = this if this.tradeskillTotal then
C_Timer.After(0, function() RefreshBar(target) end) StartTradeskillCraft(this)
else
local target = this
RunNextFrame(function()
-- Re-check: SPELL_START_SELF (nampower co-hook) fires before vanilla
-- SPELLCAST_START on the same packet, so SPELLCAST_START may have
-- entered merge in this same frame. Don't restamp over it.
if target.tradeskillTotal then
StartTradeskillCraft(target)
return
end
RefreshBar(target)
if target.unitstr == "player" and target.isTradeskill
and (target.pendingTradeskillCount or 0) > 1
and C.castbar.player.mergetradeskill == "1" then
EnterTradeskillMerge(target, target.startTime, target.endTime, target.pendingTradeskillCount)
end
target.pendingTradeskillCount = nil
end)
end
elseif event == "SPELLCAST_CHANNEL_STOP" then elseif event == "SPELLCAST_CHANNEL_STOP" then
-- A channel's stop can arrive after a following cast already claimed -- A channel's stop can arrive after a following cast already claimed
-- the bar (channel->cast transition); only clear if a channel is -- the bar (channel->cast transition); only clear if a channel is
-- actually being shown, so it doesn't wipe an active cast bar. -- actually being shown, so it doesn't wipe an active cast bar.
if this.isChannel then ClearBar(this) end if this.isChannel then ClearBar(this) end
elseif event == "SPELLCAST_STOP" or event == "SPELLCAST_FAILED" elseif event == "SPELLCAST_STOP" then
or event == "SPELLCAST_INTERRUPTED" then -- During a tradeskill chain, SPELL_GO_SELF already counted this craft
-- and either cleared the bar (chain done) or kept it running. Only a
-- non-merge cast clears here.
if not this.tradeskillTotal then ClearBar(this) end
elseif event == "SPELLCAST_FAILED" or event == "SPELLCAST_INTERRUPTED" then
ClearBar(this) ClearBar(this)
elseif event == "SPELL_GO_SELF" then
-- arg2 = spellId. In a tradeskill merge, count each successful craft
-- and clear when the chain is done.
if this.tradeskillTotal and arg2 == this.tradeskillSpellID then
this.tradeskillCompleted = (this.tradeskillCompleted or 0) + 1
if this.tradeskillCompleted >= this.tradeskillTotal then
ClearBar(this)
else
UpdateTradeskillLabel(this)
end
end
elseif event == "SPELL_DELAYED_SELF" then elseif event == "SPELL_DELAYED_SELF" then
-- Cast pushback. nampower's event carries the delay (arg2); apply it -- Cast pushback. nampower's event carries the delay (arg2); apply it
-- locally rather than re-polling, so the bar doesn't depend on -- locally rather than re-polling, so the bar doesn't depend on
@@ -363,6 +481,17 @@ pfUI:RegisterModule("castbar", function ()
end end
UpdateMovable(pfUI.castbar.player) UpdateMovable(pfUI.castbar.player)
-- Tradeskill merge: hook DoTradeSkill so the player castbar knows the
-- requested count before the first SPELLCAST_START fires. Always-on hook
-- (the config knob is read at event time so toggling takes effect on the
-- next craft without a /reload). DoTradeSkill is synchronous; the server
-- roundtrip to SPELLCAST_START gives us plenty of time after this hook.
hooksecurefunc("DoTradeSkill", function(index, num)
if pfUI.castbar.player then
pfUI.castbar.player.pendingTradeskillCount = tonumber(num) or 1
end
end)
end end
-- [[ pfTargetCastbar ]] -- -- [[ pfTargetCastbar ]] --
+1
View File
@@ -2776,6 +2776,7 @@ pfUI:RegisterModule("gui", function ()
CreateConfig(nil, T["Left Text Y Offset"], C.castbar.player, "txtleftoffy") CreateConfig(nil, T["Left Text Y Offset"], C.castbar.player, "txtleftoffy")
CreateConfig(nil, T["Show Lag"], C.castbar.player, "showlag", "checkbox") CreateConfig(nil, T["Show Lag"], C.castbar.player, "showlag", "checkbox")
CreateConfig(nil, T["Show Rank"], C.castbar.player, "showrank", "checkbox") CreateConfig(nil, T["Show Rank"], C.castbar.player, "showrank", "checkbox")
CreateConfig(nil, T["Merge Tradeskill Casts"], C.castbar.player, "mergetradeskill", "checkbox")
CreateConfig(nil, T["Right Text X Offset"], C.castbar.player, "txtrightoffx") CreateConfig(nil, T["Right Text X Offset"], C.castbar.player, "txtrightoffx")
CreateConfig(nil, T["Right Text Y Offset"], C.castbar.player, "txtrightoffy") CreateConfig(nil, T["Right Text Y Offset"], C.castbar.player, "txtrightoffy")