BNP = BNP or {} local casts = {} local castFeedback = {} local BAR_HEIGHT = 6 local BAR_GAP = 4 local MODERN_ICON_BORDER_SCALE = 23 / 14 local CLASSIC_FRAME_INSET = 3 local CLASSIC_GOLD_R, CLASSIC_GOLD_G, CLASSIC_GOLD_B = 1.0, 0.8, 0.0 local CAST_SPARK_SIZE = 20 local CAST_SUCCESS_FEEDBACK_DURATION = 0.60 local CAST_FAIL_FEEDBACK_DURATION = 0.30 local TEST_CAST_FEEDBACK_DURATION = 0.60 local CLASSIC_BACKDROP = { edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 8, edgeSize = 12, insets = { left = 3, right = 3, top = 3, bottom = 3 }, } -- Runtime-only visual preview. This is intentionally not saved in BNP_DB so -- a /reload always returns to normal cast detection. local castbarTestMode = false local testCastStart = 0 local TEST_CAST_DURATION = 4 local TEST_CAST_TEXTURE = "Interface\\Icons\\Spell_Fire_Fireball02" local testCast = { name = "Test Cast", texture = TEST_CAST_TEXTURE, startTime = 0, endTime = 0, duration = TEST_CAST_DURATION, channel = false, } local function ResetTestCast(now) now = now or GetTime() testCastStart = now testCast.startTime = now testCast.endTime = now + TEST_CAST_DURATION testCast.duration = TEST_CAST_DURATION end local function GetTestCast(now) if testCastStart == 0 then ResetTestCast(now) end if now < testCast.endTime then return testCast, nil end local feedbackEnd = testCast.endTime + TEST_CAST_FEEDBACK_DURATION if now < feedbackEnd then return nil, { state = "success", name = testCast.name, texture = testCast.texture, duration = testCast.duration, value = testCast.duration, percent = 1, endTime = feedbackEnd, flashDuration = TEST_CAST_FEEDBACK_DURATION, } end ResetTestCast(now) return testCast, nil end local function GetCastbarHeight() if BNP.GetCastbarHeight then return BNP:GetCastbarHeight() end return BAR_HEIGHT end local function GetCastbarFontSize() if BNP.GetCastbarFontSize then return BNP:GetCastbarFontSize() end return 10 end local function GetCastbarStyle() if BNP.GetCastbarStyle then return BNP:GetCastbarStyle() end return "modern" end local function IsClassicCastbarStyle() return GetCastbarStyle() == "classic" end local function IsCastbarIconEnabled() if BNP.IsCastbarIconEnabled then return BNP:IsCastbarIconEnabled() end return not BNP_DB or BNP_DB.castbarIcon ~= false end local function GetCastbarIconSize() if IsClassicCastbarStyle() then -- The Classic castbar backdrop extends three pixels above and below the -- configured bar height. Give the icon the same complete outer height. return GetCastbarHeight() + (CLASSIC_FRAME_INSET * 2) end -- Modern keeps the spell icon exactly square with the configured castbar -- height. Both frames share the same top anchor, so their visible rows stay -- aligned at every slider value from 4 to 20. return GetCastbarHeight() end local function GetCastbarStartOffset() if not IsCastbarIconEnabled() then return 0 end if IsClassicCastbarStyle() then return GetCastbarIconSize() end return GetCastbarIconSize() + 1 end local function GetCastbarXOffset() if BNP.GetCastbarXOffset then return BNP:GetCastbarXOffset() end return 0 end local function GetCastbarYOffset() if BNP.GetCastbarYOffset then return BNP:GetCastbarYOffset() end return 0 end local function SetCastbarSpark(bar, state, alpha) if not bar or not bar.spark then return end -- On the Vanilla 1.12 client GetWidth() can lag a layout update. -- Use the validated fixed logical width also used by the castbar itself, -- never a transient/projected healthbar measurement. local minValue, maxValue = bar:GetMinMaxValues() local currentValue = bar:GetValue() or minValue or 0 minValue = minValue or 0 maxValue = maxValue or 1 local percent = 0 if maxValue > minValue then percent = (currentValue - minValue) / (maxValue - minValue) end if percent < 0 then percent = 0 end if percent > 1 then percent = 1 end local width = bar.BNPRenderWidth or bar:GetWidth() or 0 bar.spark:ClearAllPoints() bar.spark:SetPoint("CENTER", bar, "LEFT", width * percent, 0) if state == "success" then bar.spark:SetVertexColor(0.20, 1.00, 0.20) elseif state == "fail" then bar.spark:SetVertexColor(1.00, 0.20, 0.20) else -- Untinted during the cast, matching ShaguTweaks' original spark. bar.spark:SetVertexColor(1.00, 1.00, 1.00) end bar.spark:SetAlpha(alpha or 1) bar.spark:Show() end local function ApplyCastbarFont(text) if not text or not text.GetFont or not text.SetFont then return end local font, currentSize, flags = text:GetFont() if not font then return end local size = GetCastbarFontSize() if text.BNPModernFontFlags == nil then text.BNPModernFontFlags = flags or "" end local wantedFlags = IsClassicCastbarStyle() and "THINOUTLINE" or text.BNPModernFontFlags if currentSize ~= size or (flags or "") ~= wantedFlags then -- Keep Blizzard's font face. Classic adds ShaguTweaks' thin outline; -- Modern restores the original font flags. text:SetFont(font, size, wantedFlags) end end local BOTTOM_AURA_EDGE_GAP = 4 local function FrameShown(frame) return frame and frame.IsShown and frame:IsShown() and true or false end local function IsBottomDebuffPosition(position) return position == "bottom_mid" or position == "bottom_left" or position == "bottom_right" end local function GetBottomAuraClearance(plate) if not plate then return 0 end local debuffPosition = BNP.GetDebuffPosition and BNP:GetDebuffPosition() or "top" local debuffsEnabled = not BNP.AreDebuffsEnabled or BNP:AreDebuffsEnabled() local auraSize = BNP.GetIconSize and BNP:GetIconSize() or 18 if debuffsEnabled and IsBottomDebuffPosition(debuffPosition) and FrameShown(plate.BNPAuraContainer) then return BOTTOM_AURA_EDGE_GAP + auraSize end return 0 end local function RegionShown(region) if not region then return false end if region.IsShown then return region:IsShown() end return true end local function GetCastbarEndExtension(plate) -- Preserve the classic +14px room while either native right-side level -- region is actually visible. When Hide Level removes the numeric text and -- there is no boss/level icon, the castbar ends flush with the healthbar. if plate and (RegionShown(plate.level) or RegionShown(plate.levelicon)) then return 14 end return 0 end -- Occasionally the projected Blizzard healthbar reports transitional geometry -- when its nameplate moves/recycles. Do not let one bad width stretch the castbar -- across the screen or displace its progress spark. Dimensions here are local -- frame units; the normal nameplate/target scale still comes from the wrapper. local CAST_MIN_HEALTH_WIDTH = 25 local CAST_MAX_HEALTH_WIDTH = 400 local CAST_WIDTH_JUMP_FACTOR = 1.50 local CAST_FALLBACK_HEALTH_WIDTH = 110 local function GetSafeHealthbarWidth(plate, bar) if not plate or not plate.healthbar or not bar then return CAST_FALLBACK_HEALTH_WIDTH end local measured = plate.healthbar:GetWidth() local previous = bar.BNPStableHealthWidth local wrapper = plate.BNPScaleWrapper local originalWidth = wrapper and wrapper.originalWidth -- Reject an oversized FIRST measurement too: otherwise the bad value -- itself would be cached as the reference for all subsequent casts. local firstWidthPlausible = previous or type(originalWidth) ~= "number" or originalWidth < 80 or originalWidth > CAST_MAX_HEALTH_WIDTH or (type(measured) == "number" and measured <= originalWidth * 2) if type(measured) == "number" and measured >= CAST_MIN_HEALTH_WIDTH and measured <= CAST_MAX_HEALTH_WIDTH and firstWidthPlausible and (not previous or (measured >= (previous / CAST_WIDTH_JUMP_FACTOR) and measured <= (previous * CAST_WIDTH_JUMP_FACTOR))) then bar.BNPStableHealthWidth = measured return measured end if previous then return previous end -- Even the first cast on a newly projected nameplate can land on a bad -- geometry frame. The original plate width is a better fallback than the -- transient healthbar width; if missing, use a Classic-sized default. local fallback = originalWidth if type(fallback) ~= "number" or fallback < CAST_MIN_HEALTH_WIDTH or fallback > CAST_MAX_HEALTH_WIDTH then fallback = CAST_FALLBACK_HEALTH_WIDTH end return fallback end local function ApplyCastbarLayout(plate, bar, healthWidth) if not plate or not plate.healthbar or not bar then return end local gap = BAR_GAP + GetBottomAuraClearance(plate) local xOffset = GetCastbarXOffset() local yOffset = GetCastbarYOffset() local barStartOffset = GetCastbarStartOffset() local endExtension = GetCastbarEndExtension(plate) healthWidth = healthWidth or GetSafeHealthbarWidth(plate, bar) -- A recycled plate may have received the wrapper AFTER the castbar was -- created. Both castbar and icon must inherit the exact same parent scale. local parent = plate.BNPScaleWrapper or plate if bar:GetParent() ~= parent then bar:SetParent(parent) end if bar.icon and bar.icon:GetParent() ~= parent then bar.icon:SetParent(parent) end -- Anchor only the left edge and set a validated logical width explicitly. -- The old dual-edge TOPLEFT/TOPRIGHT stretch followed bad healthbar geometry -- during projection/movement, sometimes leaving a huge castbar for one unit. local renderWidth = healthWidth + endExtension - barStartOffset if renderWidth < 1 then renderWidth = 1 end bar:ClearAllPoints() bar:SetPoint("TOPLEFT", plate.healthbar, "BOTTOMLEFT", barStartOffset + xOffset, -gap + yOffset) bar:SetWidth(renderWidth) bar.BNPRenderWidth = renderWidth bar.BNPLayoutHealthWidth = healthWidth bar.BNPLayoutEndExtension = endExtension if bar.icon then bar.icon:ClearAllPoints() local iconYOffset = IsClassicCastbarStyle() and CLASSIC_FRAME_INSET or 0 bar.icon:SetPoint("TOPLEFT", plate.healthbar, "BOTTOMLEFT", xOffset, -gap + yOffset + iconYOffset) end bar.BNPBottomAuraClearance = GetBottomAuraClearance(plate) end local function Enabled() if BNP.AreCastbarsEnabled then return BNP:AreCastbarsEnabled() end return not BNP_DB or BNP_DB.castbars ~= false end local function GetPlateGUID(plate) if not plate or not plate.GetName then return nil end local token = plate:GetName(1) if not token then return nil end local exists, guid = UnitExists(token) if exists and guid then return guid end -- On SuperWoW the plate token itself can already be the GUID. return token end local function SpellData(spellID) if SpellInfo and spellID then local name, _, texture = SpellInfo(spellID) return name or "Casting", texture end return "Casting", nil end -- Only the presentation changes between styles. Cast state and animation stay -- on BNP's GUID-based path instead of ShaguTweaks' name-based fallback. local function ApplyCastbarStyle(bar) if not bar then return end local texture = BNP:GetCastbarFillTexture() if bar.BNPCastbarTexture ~= texture then local r, g, b, a = bar:GetStatusBarColor() bar:SetStatusBarTexture(texture) if r then bar:SetStatusBarColor(r, g, b, a or 1) end bar.background:SetTexture(texture) bar.BNPCastbarTexture = texture end local classic = IsClassicCastbarStyle() if bar.classicBackdrop then if classic then bar.classicBackdrop:Show() else bar.classicBackdrop:Hide() end end if bar.icon then local iconSize = GetCastbarIconSize() bar.icon:SetWidth(iconSize) bar.icon:SetHeight(iconSize) if bar.icon.texture then bar.icon.texture:ClearAllPoints() if classic then bar.icon.texture:SetPoint("CENTER", bar.icon, "CENTER", 0, 0) bar.icon.texture:SetWidth(GetCastbarHeight()) bar.icon.texture:SetHeight(GetCastbarHeight()) else bar.icon.texture:SetAllPoints(bar.icon) end end if bar.icon.border then if classic then bar.icon.border:Hide() else local borderSize = math.floor((iconSize * MODERN_ICON_BORDER_SCALE) + 0.5) bar.icon.border:SetWidth(borderSize) bar.icon.border:SetHeight(borderSize) bar.icon.border:Show() end end if bar.icon.classicBackdrop then if classic then bar.icon.classicBackdrop:Show() else bar.icon.classicBackdrop:Hide() end end if not IsCastbarIconEnabled() then bar.icon:Hide() end end ApplyCastbarFont(bar.text) end local function CreateCastbar(plate) if plate.BNPCastbar then return plate.BNPCastbar end if not plate.healthbar then return nil end local parent = plate.BNPScaleWrapper or plate local bar = CreateFrame("StatusBar", nil, parent) bar:SetHeight(GetCastbarHeight()) -- Center the castbar under the complete Blizzard nameplate. -- Extend beneath the level badge instead of stopping at the healthbar edge. -- Compact row: with the optional icon enabled, icon + castbar together span -- the complete plate width. Without it, the bar starts at the left edge and -- uses the freed space itself. local gap = BAR_GAP + GetBottomAuraClearance(plate) local xOffset = GetCastbarXOffset() local yOffset = GetCastbarYOffset() -- Initial placement uses the same guarded one-edge geometry as all later -- layout updates. Never stretch the first frame between two live anchors. local initialStartOffset = GetCastbarStartOffset() local initialHealthWidth = GetSafeHealthbarWidth(plate, bar) local initialEndExtension = GetCastbarEndExtension(plate) local initialRenderWidth = initialHealthWidth + initialEndExtension - initialStartOffset if initialRenderWidth < 1 then initialRenderWidth = 1 end bar:SetPoint("TOPLEFT", plate.healthbar, "BOTTOMLEFT", initialStartOffset + xOffset, -gap + yOffset) bar:SetWidth(initialRenderWidth) bar.BNPRenderWidth = initialRenderWidth bar.BNPLayoutHealthWidth = initialHealthWidth bar.BNPLayoutEndExtension = initialEndExtension local texture = BNP:GetCastbarFillTexture() bar:SetStatusBarTexture(texture) bar.BNPCastbarTexture = texture bar:SetStatusBarColor(1.0, 0.8, 0.0) bar:SetMinMaxValues(0, 1) bar:SetValue(0) bar:SetFrameLevel((plate.healthbar:GetFrameLevel() or 1) + 1) bar:Hide() local bg = bar:CreateTexture(nil, "BACKGROUND") bg:SetTexture(texture) bg:SetVertexColor(0.10, 0.10, 0.00, 0.85) bg:SetAllPoints(bar) bar.background = bg local text = bar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") ApplyCastbarFont(text) text:SetPoint("CENTER", bar, "CENTER", 0, 0) text:SetTextColor(1, 1, 1) bar.text = text -- ShaguTweaks-style spark attached to the actual BNP cast progress. -- This is presentation-only: it shares the existing frame-perfect updater -- and does not add another scan or OnUpdate loop. local spark = bar:CreateTexture(nil, "OVERLAY") spark:SetTexture("Interface\\CastingBar\\UI-CastingBar-Spark") spark:SetWidth(CAST_SPARK_SIZE) spark:SetHeight(CAST_SPARK_SIZE) spark:SetBlendMode("ADD") spark:Hide() bar.spark = spark -- ShaguTweaks-inspired Classic frame. It remains hidden in the default -- Modern style and adds no second progress or animation layer. local classicBackdrop = CreateFrame("Frame", nil, bar) classicBackdrop:SetFrameLevel(bar:GetFrameLevel()) classicBackdrop:SetPoint("TOPLEFT", bar, "TOPLEFT", -3, 3) classicBackdrop:SetPoint("BOTTOMRIGHT", bar, "BOTTOMRIGHT", 3, -3) classicBackdrop:SetBackdrop(CLASSIC_BACKDROP) classicBackdrop:SetBackdropBorderColor(CLASSIC_GOLD_R, CLASSIC_GOLD_G, CLASSIC_GOLD_B) classicBackdrop:Hide() bar.classicBackdrop = classicBackdrop -- Spell icon to the left of the castbar. local icon = CreateFrame("Frame", nil, parent) icon:SetWidth(14) icon:SetHeight(14) local iconYOffset = IsClassicCastbarStyle() and CLASSIC_FRAME_INSET or 0 icon:SetPoint("TOPLEFT", plate.healthbar, "BOTTOMLEFT", xOffset, -gap + yOffset + iconYOffset) icon:SetFrameLevel(bar:GetFrameLevel() + 1) local texture = icon:CreateTexture(nil, "ARTWORK") texture:SetAllPoints(icon) icon.texture = texture local border = icon:CreateTexture(nil, "OVERLAY") border:SetTexture("Interface\\Buttons\\UI-Quickslot2") border:SetPoint("CENTER", icon, "CENTER", 0, 0) border:SetWidth(23) border:SetHeight(23) icon.border = border local iconClassicBackdrop = CreateFrame("Frame", nil, icon) iconClassicBackdrop:SetFrameLevel(icon:GetFrameLevel() + 1) iconClassicBackdrop:SetAllPoints(icon) iconClassicBackdrop:SetBackdrop(CLASSIC_BACKDROP) iconClassicBackdrop:SetBackdropBorderColor(CLASSIC_GOLD_R, CLASSIC_GOLD_G, CLASSIC_GOLD_B) iconClassicBackdrop:Hide() icon.classicBackdrop = iconClassicBackdrop icon:Hide() bar.icon = icon plate.BNPCastbar = bar ApplyCastbarStyle(bar) return bar end local function Hide(plate) if plate and plate.BNPCastbar then if plate.BNPCastbar.spark then plate.BNPCastbar.spark:Hide() plate.BNPCastbar.spark:SetAlpha(1) plate.BNPCastbar.spark:SetVertexColor(1.00, 1.00, 1.00) end plate.BNPCastbar:Hide() if plate.BNPCastbar.icon then plate.BNPCastbar.icon:Hide() end end end -- Exact SuperWoW event path used by ShaguTweaks' superwow module: -- START / CAST / CHANNEL create cast state; FAIL removes it. local eventFrame = CreateFrame("Frame") eventFrame:RegisterEvent("UNIT_CASTEVENT") eventFrame:SetScript("OnEvent", function() local guid = arg1 local eventType = arg3 local spellID = arg4 local timerMS = tonumber(arg5) or 0 if not guid then return end -- SuperWoW CAST is the successful completion event for a cast already -- started with START/CHANNEL. Do not use arg5/timerMS to identify completion: -- SuperWoW may still provide a non-zero timer on CAST. Match the running spell -- instead, mirroring SuperAPI_Castlib's live-event handling. if eventType == "CAST" then local previous = casts[guid] local sameSpell = previous and (previous.spellID == nil or spellID == nil or tostring(previous.spellID) == tostring(spellID)) if previous and sameSpell and previous.duration and previous.duration > 0 then local now = GetTime() castFeedback[guid] = { state = "success", name = previous.name, texture = previous.texture, duration = previous.duration, value = previous.channel and 0 or previous.duration, percent = previous.channel and 0 or 1, endTime = now + CAST_SUCCESS_FEEDBACK_DURATION, flashDuration = CAST_SUCCESS_FEEDBACK_DURATION, } casts[guid] = nil end -- A CAST without a matching running cast is an instant/proc/non-bar event. -- It must not create or restart a castbar. return end if eventType == "START" or eventType == "CHANNEL" then if timerMS <= 0 then -- Instant casts do not need a visible castbar. casts[guid] = nil castFeedback[guid] = nil return end local name, texture = SpellData(spellID) local now = GetTime() local duration = timerMS / 1000 -- A fresh cast immediately replaces any short success/fail flash that may -- still be visible from the previous cast on this GUID. castFeedback[guid] = nil casts[guid] = { name = name, texture = texture, spellID = spellID, startTime = now, endTime = now + duration, duration = duration, channel = eventType == "CHANNEL", } elseif eventType == "FAIL" then local previous = casts[guid] if previous and previous.duration and previous.duration > 0 then local now = GetTime() local current = now - previous.startTime if current < 0 then current = 0 end if current > previous.duration then current = previous.duration end local value = previous.channel and (previous.duration - current) or current if value < 0 then value = 0 end castFeedback[guid] = { state = "fail", name = previous.name, texture = previous.texture, duration = previous.duration, value = value, percent = value / previous.duration, endTime = now + CAST_FAIL_FEEDBACK_DURATION, flashDuration = CAST_FAIL_FEEDBACK_DURATION, } end casts[guid] = nil end end) local activeCastPlates = {} local function MarkCastPlateActive(plate) if not plate then return end activeCastPlates[plate] = true end local function MarkCastPlateInactive(plate) if not plate then return end activeCastPlates[plate] = nil Hide(plate) end -- Lightweight discovery pass. This does NOT animate bars; it only checks -- visible plates for active cast state at 20 Hz. local discovery = CreateFrame("Frame") local discoveryElapsed = 0 discovery:SetScript("OnUpdate", function() discoveryElapsed = discoveryElapsed + arg1 if discoveryElapsed < 0.05 then return end discoveryElapsed = 0 if not Enabled() and not castbarTestMode then local plate for plate in pairs(activeCastPlates) do MarkCastPlateInactive(plate) end return end -- No active timed cast and no short success/fail feedback means there is -- nothing to discover. UNIT_CASTEVENT wakes this path automatically by -- creating an entry in casts/castFeedback, so idle gameplay no longer walks -- every visible nameplate 20 times per second. if not castbarTestMode and not next(casts) and not next(castFeedback) then local plate for plate in pairs(activeCastPlates) do MarkCastPlateInactive(plate) end return end local now = GetTime() -- Drop expired feedback even if its nameplate disappeared before the short -- flash could be rendered. This keeps the GUID table bounded over time. local feedbackGUID, staleFeedback for feedbackGUID, staleFeedback in pairs(castFeedback) do if not staleFeedback.endTime or now >= staleFeedback.endTime then castFeedback[feedbackGUID] = nil end end local plate for plate in pairs(BNP.plates) do if plate:IsShown() then if castbarTestMode then -- Preview every visible nameplate. No GUID/cast lookup is needed. MarkCastPlateActive(plate) else local guid = GetPlateGUID(plate) local cast = guid and casts[guid] local feedback = guid and castFeedback[guid] -- If the 20 Hz discovery pass happens to run on the exact frame a cast -- completes, preserve the active plate and hand it a success flash. -- Otherwise discovery could hide the bar before the frame updater gets -- a chance to render the green endpoint. if guid and cast and now >= cast.endTime then if activeCastPlates[plate] then castFeedback[guid] = { state = "success", name = cast.name, texture = cast.texture, duration = cast.duration, value = cast.channel and 0 or cast.duration, percent = cast.channel and 0 or 1, endTime = now + CAST_SUCCESS_FEEDBACK_DURATION, flashDuration = CAST_SUCCESS_FEEDBACK_DURATION, } feedback = castFeedback[guid] end casts[guid] = nil cast = nil end if (cast and now < cast.endTime) or (feedback and now < feedback.endTime) then MarkCastPlateActive(plate) elseif activeCastPlates[plate] then MarkCastPlateInactive(plate) end end elseif activeCastPlates[plate] then MarkCastPlateInactive(plate) end end end) -- Frame-perfect animation remains unchanged visually, but only active -- castbars are updated every rendered frame. The Shagu-style spark rides this -- same update, so it adds no extra per-frame scan. local updater = CreateFrame("Frame") updater:SetScript("OnUpdate", function() if not Enabled() and not castbarTestMode then return end local now = GetTime() local plate for plate in pairs(activeCastPlates) do if not plate:IsShown() then MarkCastPlateInactive(plate) else local guid local cast local feedback if castbarTestMode then cast, feedback = GetTestCast(now) else guid = GetPlateGUID(plate) cast = guid and casts[guid] feedback = guid and castFeedback[guid] end -- A naturally completed timed cast is considered successful unless -- SuperWoW already sent FAIL, which removes the cast and creates the red -- feedback state in the event handler above. if (not castbarTestMode) and guid and cast and now >= cast.endTime then castFeedback[guid] = { state = "success", name = cast.name, texture = cast.texture, duration = cast.duration, value = cast.channel and 0 or cast.duration, percent = cast.channel and 0 or 1, endTime = now + CAST_SUCCESS_FEEDBACK_DURATION, flashDuration = CAST_SUCCESS_FEEDBACK_DURATION, } casts[guid] = nil cast = nil feedback = castFeedback[guid] end if feedback and now >= feedback.endTime then if guid then castFeedback[guid] = nil end feedback = nil end if not cast and not feedback then MarkCastPlateInactive(plate) else local bar = CreateCastbar(plate) if bar then -- Active bars already run through this updater. Check only their -- lightweight geometry here; no additional nameplate scan is added. -- Recover on the next frame when projected width/parent changes, -- while rejecting any transient oversized healthbar measurement. local bottomClearance = GetBottomAuraClearance(plate) local healthWidth = GetSafeHealthbarWidth(plate, bar) local endExtension = GetCastbarEndExtension(plate) local expectedParent = plate.BNPScaleWrapper or plate if bar.BNPBottomAuraClearance ~= bottomClearance or bar.BNPLayoutHealthWidth ~= healthWidth or bar.BNPLayoutEndExtension ~= endExtension or bar:GetParent() ~= expectedParent or (bar.icon and bar.icon:GetParent() ~= expectedParent) then ApplyCastbarLayout(plate, bar, healthWidth) end -- Do not allow an external layout reset or reused nameplate frame -- to leave the castbar/icon at a different local scale or height. -- Normal target/nameplate scaling is inherited from the wrapper. if bar:GetScale() ~= 1 then bar:SetScale(1) end if bar.icon and bar.icon:GetScale() ~= 1 then bar.icon:SetScale(1) end local desiredHeight = GetCastbarHeight() if math.abs((bar:GetHeight() or 0) - desiredHeight) > 0.1 then bar:SetHeight(desiredHeight) end local desiredWidth = bar.BNPRenderWidth or 1 local currentWidth = bar:GetWidth() or 0 if currentWidth > desiredWidth * 1.5 or (currentWidth > 1 and currentWidth < desiredWidth / 1.5) then bar:SetWidth(desiredWidth) end local name local texture local value local duration local percent local sparkState local sparkAlpha = 1 if cast then local current = now - cast.startTime if current < 0 then current = 0 end if current > cast.duration then current = cast.duration end duration = cast.duration if cast.channel then value = cast.duration - current else value = current end if duration > 0 then percent = value / duration else percent = 0 end name = cast.name texture = cast.texture else duration = feedback.duration value = feedback.value percent = feedback.percent name = feedback.name texture = feedback.texture sparkState = feedback.state -- Keep the success/fail flash crisp at first, then softly fade the -- spark during the final part of its short lifetime. local remaining = feedback.endTime - now local flashDuration = feedback.flashDuration or CAST_FAIL_FEEDBACK_DURATION sparkAlpha = remaining / flashDuration if sparkAlpha < 0 then sparkAlpha = 0 end if sparkAlpha > 1 then sparkAlpha = 1 end end bar:SetMinMaxValues(0, duration or 1) bar:SetValue(value or 0) bar.text:SetText(name or "Casting") bar:SetAlpha(1) -- Make cast feedback unmistakable without adding any extra updater. -- The normal bar stays Blizzard-gold; a finished cast briefly turns -- the filled bar + spark green, while a failed/interrupted cast turns -- them red. The next active cast restores the normal color immediately. if sparkState == "success" then bar:SetStatusBarColor(0.10, 1.00, 0.10) elseif sparkState == "fail" then bar:SetStatusBarColor(1.00, 0.15, 0.15) else bar:SetStatusBarColor(1.00, 0.80, 0.00) end if bar.icon then if IsCastbarIconEnabled() and texture then bar.icon.texture:SetTexture(texture) bar.icon:SetAlpha(1) bar.icon:Show() else bar.icon:Hide() end end SetCastbarSpark(bar, sparkState, sparkAlpha) bar:Show() end end end end end) function BNP:IsCastbarTestMode() return castbarTestMode and true or false end function BNP:SetCastbarTestMode(enabled) castbarTestMode = enabled and true or false if castbarTestMode then ResetTestCast(GetTime()) else -- Remove every preview immediately. Real casts are rediscovered on the -- next lightweight discovery pass without touching their stored state. local plate for plate in pairs(activeCastPlates) do MarkCastPlateInactive(plate) end end if self.optionsFrame and self.optionsFrame.castbarTestCheck then self.optionsFrame.castbarTestCheck:SetChecked(castbarTestMode) end end function BNP:RefreshCastbarLayout() local plate local height = GetCastbarHeight() for plate in pairs(BNP.plates or {}) do if plate and plate.BNPCastbar then plate.BNPCastbar:SetHeight(height) ApplyCastbarStyle(plate.BNPCastbar) ApplyCastbarLayout(plate, plate.BNPCastbar) end end end function BNP:RefreshCastbarStyle() self:RefreshCastbarLayout() end function BNP:RefreshCastbarHeights() self:RefreshCastbarLayout() end function BNP:SetCastbarsEnabled(enabled) BNP_DB = BNP_DB or {} BNP_DB.castbars = enabled and true or false if not enabled then local plate for plate in pairs(BNP.plates) do Hide(plate) end end self:Print("Nameplate castbars " .. (enabled and "enabled." or "disabled.")) end