diff --git a/API_Check.lua b/API_Check.lua index 9c7e7f7c..8af2c0b7 100644 --- a/API_Check.lua +++ b/API_Check.lua @@ -13,7 +13,7 @@ do -- on. There pfUI does load, and will throw wherever it reaches for something -- the installed DLL doesn't have yet -- the popup names the cause so those -- errors aren't a mystery. - local PFUI_CLASSIC_API_MIN = 11813 -- (X*10000 + Y*100 + Z) + local PFUI_CLASSIC_API_MIN = 11900 -- (X*10000 + Y*100 + Z) local PFUI_CLASSIC_API_LATEST = PFUI_CLASSIC_API_MIN local PFUI_CLASSIC_API_WEBSITE = "https://github.com/brues-code/ClassicAPI" local PFUI_CLASSIC_API_LATEST_URL = PFUI_CLASSIC_API_WEBSITE .. "/releases/latest" diff --git a/README.md b/README.md index 00c8f3f4..c0ac8a69 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Provides: - C_NamePlate - Event-driven nameplates (no more per-frame WorldFrame scanning) with real per-plate unit tokens and GUIDs - C_UnitAuras - Replaces over 3k lines of hand-rolled aura tracking - C_Spell - Replaces thousands of hardcoded spell names for each locale and powers castbar -- Real cast bars - Actual cast/channel timing, spellID and interrupt state for any unit (C_Spell.UnitCastingInfo) +- Real cast bars - Actual cast/channel timing, spellID and interrupt state for any unit (C_Spell.UnitCastingInfo), filled and counted down engine-side (UnitCastingDuration, StatusBar:SetTimerDuration, duration text bindings) with no Lua OnUpdate - Focus - A genuine `focus` unit plus `/focus` and `/clearfocus` (FocusUnit / ClearFocus) - C_EquipmentSet - Full Equipment Manager with GUID-tracked gear sets (tells apart duplicate/enchanted items) - Instant Bag Sorting - C_Container.MoveItem / SwapItems instead of cursor pickup/drop diff --git a/env/translations_deDE.lua b/env/translations_deDE.lua index cb8a2565..27684efc 100644 --- a/env/translations_deDE.lua +++ b/env/translations_deDE.lua @@ -907,7 +907,6 @@ pfUI_translation["deDE"] = { ["Chat Tab"] = "Chat Tab", ["Libpredict"] = "Libpredict", ["Panel Alignment"] = "Panel Ausrichtung", - ["Nameplate Update Rate"] = "Namensplaketten Aktualisierungsrate", ["Target/Casting Plates"] = "Ziel/Cast Plaketten", ["Tooltip Update Rate"] = "Tooltip Aktualisierungsrate", ["Cursor Follow"] = "Cursor Folgen", diff --git a/env/translations_enUS.lua b/env/translations_enUS.lua index ec007ef6..c8ac9ed2 100644 --- a/env/translations_enUS.lua +++ b/env/translations_enUS.lua @@ -939,7 +939,6 @@ pfUI_translation["enUS"] = { ["Chat Tab"] = nil, ["Libpredict"] = nil, ["Panel Alignment"] = nil, - ["Nameplate Update Rate"] = nil, ["Target/Casting Plates"] = nil, ["Tooltip Update Rate"] = nil, ["Cursor Follow"] = nil, diff --git a/libs/libthrottle.lua b/libs/libthrottle.lua index a985e9e1..dbc53676 100644 --- a/libs/libthrottle.lua +++ b/libs/libthrottle.lua @@ -31,7 +31,6 @@ libthrottle.presetNames = { -- Default throttle categories libthrottle.defaults = { - nameplates_castbar = "custom", tooltip_cursor = "fastest", chat_tab = "custom", swingtimer = "custom", @@ -155,7 +154,6 @@ libthrottle:SetScript("OnEvent", function() end -- Set defaults for custom fields if missing - if not _G.pfUI_throttle.nameplates_castbar_custom then _G.pfUI_throttle.nameplates_castbar_custom = "100" end if not _G.pfUI_throttle.tooltip_cursor_custom then _G.pfUI_throttle.tooltip_cursor_custom = "10" end if not _G.pfUI_throttle.chat_tab_custom then _G.pfUI_throttle.chat_tab_custom = "10" end if not _G.pfUI_throttle.swingtimer_custom then _G.pfUI_throttle.swingtimer_custom = "50" end diff --git a/modules/castbar.lua b/modules/castbar.lua index 437e230a..cceb5ba1 100644 --- a/modules/castbar.lua +++ b/modules/castbar.lua @@ -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 diff --git a/modules/gui.lua b/modules/gui.lua index 85a8bb94..e2cda286 100644 --- a/modules/gui.lua +++ b/modules/gui.lua @@ -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 diff --git a/modules/nameplates.lua b/modules/nameplates.lua index 4f0798a2..f92ae839 100644 --- a/modules/nameplates.lua +++ b/modules/nameplates.lua @@ -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