533 lines
16 KiB
Lua
533 lines
16 KiB
Lua
BNP = BNP or {}
|
|
|
|
local casts = {}
|
|
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 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 or now >= testCast.endTime then
|
|
ResetTestCast(now)
|
|
end
|
|
return testCast
|
|
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 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 ApplyCastbarLayout(plate, bar)
|
|
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()
|
|
|
|
bar:ClearAllPoints()
|
|
bar:SetPoint("TOPLEFT", plate.healthbar, "BOTTOMLEFT", barStartOffset + xOffset, -gap + yOffset)
|
|
bar:SetPoint("TOPRIGHT", plate.healthbar, "BOTTOMRIGHT", 14 + xOffset, -gap + yOffset)
|
|
|
|
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 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()
|
|
bar:SetPoint("TOPLEFT", plate.healthbar, "BOTTOMLEFT", GetCastbarStartOffset() + xOffset, -gap + yOffset)
|
|
bar:SetPoint("TOPRIGHT", plate.healthbar, "BOTTOMRIGHT", 14 + xOffset, -gap + yOffset)
|
|
bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
|
|
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("Interface\\TargetingFrame\\UI-StatusBar")
|
|
bg:SetVertexColor(0.10, 0.10, 0.00, 0.85)
|
|
bg:SetAllPoints(bar)
|
|
|
|
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-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
|
|
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
|
|
|
|
if eventType == "START" or eventType == "CAST" or eventType == "CHANNEL" then
|
|
if timerMS <= 0 then
|
|
-- Instant casts do not need a visible castbar.
|
|
casts[guid] = nil
|
|
return
|
|
end
|
|
|
|
local name, texture = SpellData(spellID)
|
|
local now = GetTime()
|
|
local duration = timerMS / 1000
|
|
|
|
casts[guid] = {
|
|
name = name,
|
|
texture = texture,
|
|
startTime = now,
|
|
endTime = now + duration,
|
|
duration = duration,
|
|
channel = eventType == "CHANNEL",
|
|
}
|
|
elseif eventType == "FAIL" then
|
|
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
|
|
|
|
local now = GetTime()
|
|
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]
|
|
|
|
if cast and now < cast.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.
|
|
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
|
|
|
|
if castbarTestMode then
|
|
cast = GetTestCast(now)
|
|
else
|
|
guid = GetPlateGUID(plate)
|
|
cast = guid and casts[guid]
|
|
end
|
|
|
|
if not cast or now >= cast.endTime then
|
|
if (not castbarTestMode) and guid and cast and now >= cast.endTime then
|
|
casts[guid] = nil
|
|
end
|
|
MarkCastPlateInactive(plate)
|
|
else
|
|
local bar = CreateCastbar(plate)
|
|
if bar then
|
|
local bottomClearance = GetBottomAuraClearance(plate)
|
|
if bar.BNPBottomAuraClearance ~= bottomClearance then
|
|
ApplyCastbarLayout(plate, bar)
|
|
end
|
|
|
|
local current = now - cast.startTime
|
|
if current < 0 then current = 0 end
|
|
if current > cast.duration then current = cast.duration end
|
|
|
|
bar:SetMinMaxValues(0, cast.duration)
|
|
|
|
if cast.channel then
|
|
bar:SetValue(cast.duration - current)
|
|
else
|
|
bar:SetValue(current)
|
|
end
|
|
|
|
bar.text:SetText(cast.name)
|
|
bar:SetAlpha(1)
|
|
|
|
if bar.icon then
|
|
if IsCastbarIconEnabled() and cast.texture then
|
|
bar.icon.texture:SetTexture(cast.texture)
|
|
bar.icon:SetAlpha(1)
|
|
bar.icon:Show()
|
|
else
|
|
bar.icon:Hide()
|
|
end
|
|
end
|
|
|
|
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
|