mirror of
https://github.com/brues-code/pfUI.git
synced 2026-09-22 07:36:56 +00:00
6b458c9e27
* Migrate player castbar to ClassicAPI UNIT_SPELLCAST_* events
Drive the player cast path off ClassicAPI's synthesized, player-only
UNIT_SPELLCAST_* events instead of the vanilla SPELLCAST_* plus nampower
SPELL_{START,GO,DELAYED}_SELF mix. START/CHANNEL_START poll UnitCastingInfo/
UnitChannelInfo and stamp; STOP/CHANNEL_STOP/FAILED/INTERRUPTED clear;
SUCCEEDED drives tradeskill craft counting; DELAYED/CHANNEL_UPDATE re-poll.
Since UNIT_SPELLCAST_START fires for chained same-spell recasts, the nampower
SELF deps and their RunNextFrame co-hook-ordering workarounds are gone.
Remote target/focus bars keep SPELL_*_OTHER (the new events are player-only).
Fix the cast-start flash: StampBar now primes the fill on the stamp frame,
and ClearBar no longer snaps the bar to full (that snap lingered in the fade
tail and flashed when the next cast stamped).
Add a red flash on a cancelled cast: new appearance.castbar.failcolor,
flashed by ClearBar only when a cast was actually in progress.
Handle pushback Quartz-style: DELAYED/CHANNEL_UPDATE re-poll just the times
and accumulate the endMs shift into this.delay for the +/- indicator (cast
+X, channel -X), rather than a full restamp.
* Route target/focus castbars through remote UNIT_SPELLCAST_*
ClassicAPI now fires UNIT_SPELLCAST_* for remote tokens (target/focus/...)
via PollRemote, so drop the nampower SPELL_*_OTHER path and drive all three
bars off one event model. The gate is now token-based (arg1 == this.unitstr,
plus the arg1=="player" + UnitIsUnit case for target=self); the RunNextFrame
defer is gone since the remote poll fires after UnitCastingInfo is readable.
Cast detection is now nampower-free across every bar.
Use the event's rank payload (arg5): thread it through RefreshBar into
StampBar, which now only calls C_Spell.GetSpellSubtext as a fallback on the
retarget re-poll (PLAYER_TARGET/FOCUS_CHANGED) where no event is in hand.
* Drive nameplate castbars off UNIT_SPELLCAST_*
ClassicAPI now fires UNIT_SPELLCAST_* for nameplate tokens, so populate the
per-GUID castState cache from UNIT_SPELLCAST_{START,CHANNEL_START} (poll
PollCastInfo for the timing the payload omits) and clear it on
{STOP,CHANNEL_STOP} -- the remote poll fires STOP for natural end, interrupt,
and cancel alike. Replaces the nampower SPELL_{START,FAILED}_OTHER path; the
^nameplate arg1 gate ignores the target/focus/party fires of the same events,
and the NAME_PLATE_UNIT_ADDED seed still catches a plate spawning mid-cast.
Nameplate cast detection is now nampower-free.
* We no longer use these features from SuperWoW/Nampower
* Use secure unit attributes; remove mouseover scripts
Switch unitframes to attribute-driven clicks and remove mouseover tooltip handlers. Adds SetAttribute("unit") in UpdateVisibility and sets frame attributes (unit and type1='target') when creating frames. Removes OnEnter/OnLeave functions and their SetScript registrations and eliminates direct TargetUnit calls in ClickAction
* Use SetShown and simplify component default
Replace explicit Show/Hide conditionals in api/unitframes.lua with SetShown(...) for ressIcon, leaderIcon, lootIcon, pvpIcon, and restIcon to reduce branching and improve readability. Also remove the local shadowing of the parameter 'component' by using assignment (component = component or ""). No intended behavior changes.
* Drive unit-frame clicks via secure attributes
Replace the Lua OnClick dispatch (OnClick/ClickAction/RightClickAction) with
secure frame attributes: default type1=target / type2=menu, and EnableClickCast
now writes type/spell/macrotext/menu/target/focus attributes per button+modifier
instead of caching a clickactions table read at click time. Drops the now-dead
clickactions table and buttons list.
Also set a "unit" attribute on each frame (at creation and re-synced in
UpdateVisibility to the live token, so a party shown on the raid grid reports
partyN, not raidN) so the hovered unit resolves from the frame.
Match the target/focus/menu click keywords exactly rather than by prefix, so a
spell whose name starts with "focus"/"target"/"menu" (e.g. Focus Magic) casts
instead of being swallowed as the keyword action.
* Simplify marktracking colors and refresh per mark
Replace the hand-rolled ParseColor plus its default-color table with
GetStringColor -- the raidmarkercolor_* config keys already carry those same
defaults, so the fallback was dead code.
Restructure the refresh off observed mark tokens: UNIT_HEALTH/UNIT_MAXHEALTH
now arrive as arg1 == "markN", so refresh just that one row (UpdateRow) instead
of rescanning all eight on every nearby unit's health tick. A visibility flip
(into range / death / hp crossing 0) re-packs the rows, so UpdateRow hands off
to a full UpdateDisplay; RAID_TARGET_UPDATE / PLAYER_ENTERING_WORLD stay full
refreshes, with the 1s poll as the range-change backstop.
* not true anymore
* Poll marktracking fallback via C_Timer instead of OnUpdate
The 1s range-change safety net ran a per-frame OnUpdate that no-oped ~59 of
every 60 frames. Replace it with C_Timer.NewTicker(FALLBACK_INTERVAL,
UpdateDisplay) -- one wakeup per second off the shared timer driver -- and drop
the elapsed accumulator. The scanner frame is now purely event-driven.
* SuperWoW is now optional
* Update README feature list
Add 'Mouseover Unit Frames' and 'Click-casting' to the main features list. Update the SuperWoW entry to reflect that it tracks party/raid units on the minimap (replacing the prior SetMouseoverUnit note).
* Replace fixed-interval OnUpdate polls with C_Timer/RunNextFrame
Swap hand-rolled per-frame throttles for the modern timer primitives:
- turtle-wow: one-shot next-frame defer (self-hiding OnUpdate frame) -> RunNextFrame
- panel: clock, combat, and fps widgets -> NewTicker(1); guild roster -> NewTicker(60)
- minimap: coordinates text -> NewTicker(0.1)
- addonbuttons: one-shot init -> RunNextFrame; 5s button rescan -> NewTicker(5),
guarded by IsShown() to preserve the old "paused while hidden" behavior
Ticker callbacks reference frames via upvalue since 'this' is unbound outside
OnUpdate. Genuine per-frame work (bar fills, fades, drag) and polls already
coordinated through pfUI.throttle are left as-is.
* update pfQuest link
* Use SetShown/SetSize in panel module
Replace manual Show/Hide toggles with SetShown(not ... ) and replace SetWidth/SetHeight with SetSize in modules/panel.lua. Changes simplify toggle logic (timer, WorldMap, chat hide buttons) and unify sizing calls for timer, frames, textures, and microbutton. No behavior changes intended; purely refactor for conciseness and consistency.
* Bump ClassicAPI min version to 1.9.0
* Drive reagent counter from events instead of a polling OnUpdate
The pfReagentCounter frame ran an OnUpdate that (1) progressively rescanned
all 120 action slots one-per-100ms on any slot change and (2) recounted
reagent inventory on a 1s throttle. Replace both with direct event handling:
- ACTIONBAR_SLOT_CHANGED updates just the changed slot (arg1), full-scanning
only when arg1 is 0/nil, instead of restarting a ~12s rescan on every edit
- BAG_UPDATE_DELAYED recounts tracked reagents directly (it is already
Blizzard's coalesced bag event, so the extra 1s throttle was redundant)
- PLAYER_ENTERING_WORLD seeds the full reagent map once
UpdateSlot now seeds a new reagent's real count via GetItemCount so a freshly
placed reagent spell shows the correct number immediately. The updatecache ->
BarsUpdate render path and the IsReagentAction/GetReagentCount accessors are
unchanged.
530 lines
23 KiB
Lua
530 lines
23 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 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.
|
||
local function ClearBar(cb, failed)
|
||
local wasActive = cb.endTime ~= nil
|
||
cb.startTime, cb.endTime, cb.isChannel = nil, 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.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
|
||
cb.fadeout = 1
|
||
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
|
||
-- UNIT_SPELLCAST_START handler). Resets the per-craft spark to the left
|
||
-- edge of the bar.
|
||
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
|
||
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.
|
||
local function StampBar(cb, name, tex, startMs, endMs, spellID, isChannel, delayMs, isTradeskill, rank)
|
||
cb.startTime = startMs
|
||
cb.endTime = endMs
|
||
cb.isChannel = isChannel
|
||
cb.spellID = spellID
|
||
cb.activeName = name
|
||
cb.isTradeskill = isTradeskill
|
||
cb.delay = (delayMs or 0) / 1000
|
||
cb:SetAlpha(1)
|
||
cb.fadeout = nil
|
||
|
||
cb.bar:SetStatusBarColor(GetStringColor(C.appearance.castbar[isChannel and "channelcolor" or "castbarcolor"]))
|
||
|
||
-- Rank: prefer the value the UNIT_SPELLCAST_* event delivered (arg5, passed
|
||
-- through by RefreshBar). Only the retarget re-poll has no event in hand, so
|
||
-- it falls back to a lookup.
|
||
if not rank and spellID then
|
||
rank = C_Spell.GetSpellSubtext(spellID) or ""
|
||
end
|
||
rank = rank or ""
|
||
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:SetSize(size, 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
|
||
|
||
-- 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
|
||
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, rank)
|
||
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, isTradeskill, _, _, 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, isTradeskill, rank)
|
||
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:SetSize(16, 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(C.castbar[unitstr].namealign or "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(C.castbar[unitstr].timealign or "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)
|
||
|
||
-- 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
|
||
-- 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
|
||
if this:GetAlpha() ~= 0 then this:SetAlpha(0) end
|
||
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)
|
||
|
||
-- Per-craft spark for a tradeskill merge: position by (now - current
|
||
-- craft's start) / single-craft duration, clamped to [0, 1]. Snaps back
|
||
-- to the left edge each time StartTradeskillCraft fires for craft N+1.
|
||
if this.tradeskillTotal and this.tradeskillSingleMs and this.currentCraftStart then
|
||
local craftElapsed = now * 1000 - this.currentCraftStart
|
||
local p = craftElapsed / this.tradeskillSingleMs
|
||
if p < 0 then p = 0 elseif p > 1 then p = 1 end
|
||
local x = p * this.bar:GetWidth() - 1
|
||
this.bar.spark:ClearAllPoints()
|
||
this.bar.spark:SetPoint("TOP", this.bar, "TOPLEFT", x, 0)
|
||
this.bar.spark:SetPoint("BOTTOM", this.bar, "BOTTOMLEFT", x, 0)
|
||
end
|
||
|
||
if this.showtimer then
|
||
if (this.delay or 0) > 0 then
|
||
local prefix = "|cffffaaaa" .. (this.isChannel and "-" or "+") .. FormatCastbarTime(this.delay) .. " |r "
|
||
this.bar.right:SetText(prefix .. FormatCastbarTime(cur) .. " / " .. FormatCastbarTime(max))
|
||
else
|
||
this.bar.right:SetText(FormatCastbarTime(cur) .. " / " .. FormatCastbarTime(max))
|
||
end
|
||
end
|
||
end)
|
||
|
||
-- Cast lifecycle, entirely on ClassicAPI's UNIT_SPELLCAST_* events. They
|
||
-- fire per unit token: arg1=="player" for the player's own casts, and the
|
||
-- remote token(s) ("target", "focus", ...) for other units -- so one set of
|
||
-- events drives every bar with no Nampower dependency. The handler routes an
|
||
-- event to this bar when arg1 matches its unit, or -- since the player's own
|
||
-- casts only ever fire arg1=="player" -- when the bar's unit resolves to the
|
||
-- player (target=self). PLAYER_TARGET/FOCUS_CHANGED re-polls so a unit
|
||
-- already mid-cast when it becomes the target/focus still shows.
|
||
cb:RegisterEvent("UNIT_SPELLCAST_START")
|
||
cb:RegisterEvent("UNIT_SPELLCAST_STOP")
|
||
cb:RegisterEvent("UNIT_SPELLCAST_FAILED")
|
||
cb:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
|
||
cb:RegisterEvent("UNIT_SPELLCAST_DELAYED")
|
||
cb:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
|
||
cb:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START")
|
||
cb:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")
|
||
cb:RegisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")
|
||
if unitstr == "target" then
|
||
cb:RegisterEvent("PLAYER_TARGET_CHANGED")
|
||
elseif unitstr == "focus" then
|
||
cb:RegisterEvent("PLAYER_FOCUS_CHANGED")
|
||
end
|
||
|
||
cb:SetScript("OnEvent", function()
|
||
local unit = this.unitstr
|
||
|
||
if event == "PLAYER_TARGET_CHANGED" or event == "PLAYER_FOCUS_CHANGED" then
|
||
RefreshBar(this)
|
||
return
|
||
end
|
||
|
||
-- UNIT_SPELLCAST_* fire per unit token (arg1). Handle an event when it's
|
||
-- for this bar's unit, or -- since the player's own casts only ever fire
|
||
-- arg1=="player" -- when this bar's unit currently resolves to the player
|
||
-- (target=self / focus=self).
|
||
-- Args: arg1=unit, arg2=castGUID, arg3=spellID, arg4=name, arg5=rank.
|
||
if arg1 ~= unit and not (arg1 == "player" and UnitIsUnit(unit, "player")) then
|
||
return
|
||
end
|
||
|
||
if event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_CHANNEL_START" then
|
||
-- START also fires per craft in a same-spell chain, so an active merge
|
||
-- just resyncs the current craft's spark/label instead of restamping.
|
||
if this.tradeskillTotal then
|
||
StartTradeskillCraft(this)
|
||
else
|
||
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)
|
||
end
|
||
this.pendingTradeskillCount = nil
|
||
end
|
||
elseif event == "UNIT_SPELLCAST_SUCCEEDED" then
|
||
-- Per-craft completion during a tradeskill merge (arg3 = spellID):
|
||
-- 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
|
||
if this.tradeskillCompleted >= this.tradeskillTotal then
|
||
ClearBar(this)
|
||
else
|
||
UpdateTradeskillLabel(this)
|
||
end
|
||
end
|
||
elseif event == "UNIT_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 == "UNIT_SPELLCAST_STOP" then
|
||
-- STOP fires between crafts in a merge too (each craft is a new cast);
|
||
-- UNIT_SPELLCAST_SUCCEEDED owns the count, so only a non-merge cast
|
||
-- clears here.
|
||
if not this.tradeskillTotal then ClearBar(this) end
|
||
elseif event == "UNIT_SPELLCAST_FAILED" or event == "UNIT_SPELLCAST_INTERRUPTED" then
|
||
ClearBar(this, true)
|
||
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
|
||
-- 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 query = this.unitstr ~= "" and this.unitstr or this.unitname
|
||
local startMs, endMs
|
||
if this.isChannel then
|
||
local _, _, _, s, e = C_Spell.UnitChannelInfo(query)
|
||
startMs, endMs = s, e
|
||
else
|
||
local _, _, _, s, e = C_Spell.UnitCastingInfo(query)
|
||
startMs, endMs = s, e
|
||
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
|
||
end
|
||
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)
|
||
|
||
-- Tradeskill merge: hook DoTradeSkill so the player castbar knows the
|
||
-- requested count before the first UNIT_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 UNIT_SPELLCAST_START gives us plenty of time.
|
||
hooksecurefunc("DoTradeSkill", function(index, num)
|
||
if pfUI.castbar.player then
|
||
pfUI.castbar.player.pendingTradeskillCount = tonumber(num) or 1
|
||
end
|
||
end)
|
||
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) |