pfUI:RegisterModule("castbar", "vanilla", 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 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 script with throttle for performance optimization cb:SetScript("OnUpdate", function() -- Throttle for performance if (this.tick or 0) > GetTime() then return end this.tick = GetTime() + 0.020 -- ~50 FPS for smooth castbar if this.drag and this.drag:IsShown() then this:SetAlpha(1) return end if not UnitExists(this.unitstr) then this:SetAlpha(0) end if this.fadeout and this:GetAlpha() > 0 then if this:GetAlpha() == 0 then this.fadeout = nil end this:SetAlpha(this:GetAlpha()-0.05) end local channel = nil local query = this.unitstr ~= "" and this.unitstr or this.unitname if not query then return end -- Check if we have a GUID-based focus (Turtle WoW native GUID) local focusGuid = nil if this.unitstr and string.find(this.unitstr, "^0x") then focusGuid = this.unitstr elseif this.unitstr and this.unitstr == "player" and GetUnitGUID then focusGuid = GetUnitGUID("player") elseif this.unitstr and this.unitstr ~= "player" then local guid = GetUnitGUID(this.unitstr) if guid then focusGuid = guid end end this.focusGuid = focusGuid -- Try libdebuff_casts first for GUID-based units (works with Turtle GUID + Nampower events) local cast, nameSubtext, text, texture, startTime, endTime local castBlocked = false if focusGuid and pfUI.libdebuff_casts and pfUI.libdebuff_casts[focusGuid] then local castData = pfUI.libdebuff_casts[focusGuid] if castData.event == "CAST" or castData.event == "FAIL" then castBlocked = true pfUI.libdebuff_casts[focusGuid] = nil elseif (castData.event == "START" or castData.event == "CHANNEL") and castData.endTime and castData.endTime > GetTime() then cast = castData.spellName texture = castData.icon startTime = castData.startTime * 1000 endTime = castData.endTime * 1000 -- Try to get rank from spell DB via spellID (nameSubtext is not stored in libdebuff_casts) if castData.spellID and GetSpellRecField then nameSubtext = GetSpellRecField(castData.spellID, "rank") or "" else nameSubtext = "" end if castData.event == "CHANNEL" then channel = cast end end end local useLibcastForPlayer = this.unitstr == "player" -- For player: use player name to query libcast.db directly if not cast and useLibcastForPlayer then query = UnitName("player") end -- Fallback: pfGetCastInfo only when no focusGuid (Nampower not available for this unit). -- If we have a focusGuid, libdebuff is authoritative - don't fall back to libcast -- even if no cast is active (prevents false positives e.g. spellbook clicks on CD spells). if not cast and not castBlocked and not focusGuid and pfGetCastInfo then cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill = pfGetCastInfo(query) end if not cast and not castBlocked and not focusGuid and pfGetChannelInfo then channel, nameSubtext, text, texture, startTime, endTime, isTradeSkill = pfGetChannelInfo(this.unitstr or this.unitname) cast = channel end if cast then local duration = endTime - startTime local max = duration / 1000 local cur = GetTime() - startTime / 1000 this:SetAlpha(1) local spellname = this.showname and cast and cast .. " " or "" local rank = this.showrank and nameSubtext and nameSubtext ~= "" and string.format("|cffaaffcc[%s]|r", nameSubtext) or "" if this.endTime ~= endTime then this.bar:SetStatusBarColor(strsplit(",", C.appearance.castbar[(channel and "channelcolor" or "castbarcolor")])) this.bar.left:SetText(spellname .. rank) this.fadeout = nil this.endTime = endTime -- set texture if texture and this.showicon then local size = this:GetHeight() this.icon:Show() this.icon:SetHeight(size) this.icon:SetWidth(size) -- Override with item icon from libdebuff_casts or persistent item icon cache local useTexture = texture local useItemName = nil if pfUI.libdebuff_casts or pfUI.libdebuff_item_icons then local castGuid = nil if this.unitstr and UnitExists then local guid = GetUnitGUID(this.unitstr) castGuid = guid end if castGuid then -- First check active cast data if pfUI.libdebuff_casts and pfUI.libdebuff_casts[castGuid] and pfUI.libdebuff_casts[castGuid].itemID then useTexture = pfUI.libdebuff_casts[castGuid].icon or texture -- Fallback to persistent item icon cache elseif pfUI.libdebuff_item_icons and pfUI.libdebuff_item_icons[castGuid] then useTexture = pfUI.libdebuff_item_icons[castGuid].icon or texture useItemName = pfUI.libdebuff_item_icons[castGuid].name end end end this.icon.texture:SetTexture(useTexture) this.bar:SetPoint("TOPLEFT", this.icon, "TOPRIGHT", this.spacing, 0) -- Override spell name with item name for item-triggered casts if useItemName and this.showname then this.bar.left:SetText(useItemName .. " " .. rank) end else this.bar:SetPoint("TOPLEFT", this, 0, 0) this.icon:Hide() end if this.showlag then local _, _, lag = GetNetStats() local width = this:GetWidth() / (duration/1000) * (lag/1000) this.bar.lag:SetWidth(math.min(this:GetWidth(), width)) else this.bar.lag:Hide() end end local newMax = duration / 1000 if this.lastMax ~= newMax then this.bar:SetMinMaxValues(0, newMax) this.lastMax = newMax end if channel then cur = max + startTime/1000 - GetTime() end cur = cur > max and max or cur cur = cur < 0 and 0 or cur this.bar:SetValue(cur) if this.showtimer then if this.delay and this.delay > 0 then local delay = "|cffffaaaa" .. (channel and "-" or "+") .. FormatCastbarTime(this.delay) .. " |r " this.bar.right:SetText(delay .. FormatCastbarTime(cur) .. " / " .. FormatCastbarTime(max)) else this.bar.right:SetText(FormatCastbarTime(cur) .. " / " .. FormatCastbarTime(max)) end end this.fadeout = nil else this.bar:SetMinMaxValues(1,100) this.bar:SetValue(100) this.lastMax = nil this.fadeout = 1 this.delay = 0 this.itemIconApplied = nil end end) -- register for spell delay -- Prefer Nampower's SPELL_DELAYED_SELF (gives casterGuid + delayMs directly). -- Fall back to vanilla SPELLCAST_DELAYED if Nampower is not available. local playerarg = nil local function ApplyPushback(delayMs) if not delayMs or delayMs <= 0 or not this.endTime then return end this.delay = (this.delay or 0) + delayMs / 1000 this.endTime = this.endTime + delayMs local focusGuid = this.focusGuid if focusGuid and pfUI.libdebuff_casts and pfUI.libdebuff_casts[focusGuid] then pfUI.libdebuff_casts[focusGuid].endTime = this.endTime / 1000 end end cb:RegisterEvent("SPELL_DELAYED_SELF") cb:RegisterEvent(CASTBAR_EVENT_CAST_DELAY) cb:RegisterEvent(CASTBAR_EVENT_CHANNEL_DELAY) cb:RegisterEvent(CASTBAR_EVENT_CAST_START) cb:RegisterEvent(CASTBAR_EVENT_CHANNEL_START) cb:SetScript("OnEvent", function() if this.unitstr and not UnitIsUnit(this.unitstr, "player") then return end if event == "SPELL_DELAYED_SELF" then -- arg1=casterGuid, arg2=delayMs (Nampower, most accurate) ApplyPushback(arg2) elseif event == CASTBAR_EVENT_CAST_DELAY then -- SPELLCAST_DELAYED fallback intentionally removed - addon requires Nampower. -- Cast pushback is handled by SPELL_DELAYED_SELF above. return elseif event == CASTBAR_EVENT_CHANNEL_DELAY then -- SPELLCAST_CHANNEL_UPDATE fires when a channel is pushed back by damage. -- arg1 = new remaining time in ms. Channel ends sooner = newEndTime < this.endTime. if not this.endTime or not arg1 then return end local newEndTime = GetTime() * 1000 + arg1 local diff = this.endTime - newEndTime -- positive = time lost to pushback if diff > 50 then this.delay = (this.delay or 0) + diff / 1000 this.endTime = newEndTime local focusGuid = this.focusGuid if focusGuid and pfUI.libdebuff_casts and pfUI.libdebuff_casts[focusGuid] then pfUI.libdebuff_casts[focusGuid].endTime = newEndTime / 1000 end end elseif event == CASTBAR_EVENT_CAST_START or event == CASTBAR_EVENT_CHANNEL_START then playerarg = pfUI.client <= 11200 or arg1 == "player" and true or nil if playerarg then this.delay = 0 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() -- reset unitstr for vanilla focus frame emulation if pfUI.client <= 11200 then pfUI.castbar.focus.unitstr = nil end 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 -- keep unit values in sync with focus unitframe HookScript(pfUI.castbar.focus, "OnUpdate", function() -- clear on empty focus frames if pfUI.uf.focus.unitname == nil and pfUI.uf.focus.label == nil then pfUI.castbar.focus.unitstr = nil pfUI.castbar.focus.unitname = nil return end -- skip on initial values if pfUI.uf.focus.unitname == "focus" then return end if pfUI.uf.focus.unitname == "" then return end -- try to obtain a unitstr pfUI.castbar.focus.unitstr = string.format("%s%s", (pfUI.uf.focus.label or ""), (pfUI.uf.focus.id or "")) pfUI.castbar.focus.unitstr = pfUI.castbar.focus.unitstr == "" and nil or pfUI.castbar.focus.unitstr if pfUI.castbar.focus.unitstr then -- read non-lowercase unitname when possible pfUI.castbar.focus.unitname = UnitName(pfUI.castbar.focus.unitstr) or pfUI.castbar.focus.unitname elseif strlower(pfUI.castbar.focus.unitname) ~= strlower(pfUI.uf.focus.unitname) then -- sync unitname with focus frame's lowercase value pfUI.castbar.focus.unitname = pfUI.uf.focus.unitname end end) UpdateMovable(pfUI.castbar.focus) end end)