Add files via upload

This commit is contained in:
Wurmschwanz
2026-09-11 20:14:54 +02:00
committed by GitHub
parent a92bb2e674
commit fe5f00bfca
53 changed files with 14977 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+3628
View File
File diff suppressed because it is too large Load Diff
+766
View File
@@ -0,0 +1,766 @@
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
-- BNP stretches the StatusBar between two anchors instead of assigning a
-- fixed SetWidth like ShaguTweaks does. On the Vanilla 1.12 client GetWidth()
-- can lag behind / disagree with that anchor-defined render width. Keep the
-- exact logical width calculated from the same healthbar anchors instead.
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 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)
-- Store the width implied by those two anchors. The +14 is the extension
-- beneath the Blizzard level badge; xOffset cancels because it moves both
-- edges equally. This is also the exact travel distance for the cast spark.
local healthWidth = plate.healthbar:GetWidth() or 0
bar.BNPRenderWidth = healthWidth + 14 - barStartOffset
if bar.BNPRenderWidth < 0 then bar.BNPRenderWidth = 0 end
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()
local initialStartOffset = GetCastbarStartOffset()
bar:SetPoint("TOPLEFT", plate.healthbar, "BOTTOMLEFT", initialStartOffset + xOffset, -gap + yOffset)
bar:SetPoint("TOPRIGHT", plate.healthbar, "BOTTOMRIGHT", 14 + xOffset, -gap + yOffset)
local initialHealthWidth = plate.healthbar:GetWidth() or 0
bar.BNPRenderWidth = initialHealthWidth + 14 - initialStartOffset
if bar.BNPRenderWidth < 0 then bar.BNPRenderWidth = 0 end
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-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
-- A CAST event with no timer is the completion event for a running timed
-- cast. Convert only that existing cast into feedback. This does not touch
-- the spark/progress implementation used while the cast is running.
if eventType == "CAST" and timerMS <= 0 then
local previous = casts[guid]
if previous 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
return
end
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
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,
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
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
local bottomClearance = GetBottomAuraClearance(plate)
if bar.BNPBottomAuraClearance ~= bottomClearance then
ApplyCastbarLayout(plate, bar)
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
+279
View File
@@ -0,0 +1,279 @@
BNP = BNP or {}
local fallbackColors = {
WARRIOR = { 0.78, 0.61, 0.43 },
MAGE = { 0.41, 0.80, 0.94 },
ROGUE = { 1.00, 0.96, 0.41 },
DRUID = { 1.00, 0.49, 0.04 },
HUNTER = { 0.67, 0.83, 0.45 },
SHAMAN = { 0.00, 0.44, 0.87 },
PRIEST = { 1.00, 1.00, 1.00 },
WARLOCK = { 0.58, 0.51, 0.79 },
PALADIN = { 0.96, 0.55, 0.73 },
}
local function GetColor(class)
if RAID_CLASS_COLORS and RAID_CLASS_COLORS[class] then
local c = RAID_CLASS_COLORS[class]
return c.r, c.g, c.b
end
local c = fallbackColors[class]
if c then return c[1], c[2], c[3] end
end
local function Enabled()
if BNP.AreClassColorsEnabled then return BNP:AreClassColorsEnabled() end
return not BNP_DB or BNP_DB.classColors ~= false
end
local function ClearClassCache(plate)
if not plate then return end
plate.BNPClassColorApplied = nil
plate.BNPClassColorToken = nil
plate.BNPClassGUID = nil
plate.BNPClassR = nil
plate.BNPClassG = nil
plate.BNPClassB = nil
end
local function ClearNativeColor(plate)
if not plate then return end
plate.BNPClassNativeGUID = nil
plate.BNPClassNativeR = nil
plate.BNPClassNativeG = nil
plate.BNPClassNativeB = nil
plate.BNPClassNativeA = nil
end
local function ResolvePlateUnit(plate)
if not plate or not plate.GetName then return nil, nil end
local token = plate:GetName(1)
if not token or not UnitExists(token) then
return token, nil
end
local exists, guid = UnitExists(token)
if exists then
return token, guid
end
return token, nil
end
local function CaptureNativeColor(plate, guid)
if not plate or not plate.healthbar or not guid then return end
if plate.BNPClassNativeGUID == guid and plate.BNPClassNativeR then return end
local r, g, b, a = plate.healthbar:GetStatusBarColor()
plate.BNPClassNativeGUID = guid
plate.BNPClassNativeR = r
plate.BNPClassNativeG = g
plate.BNPClassNativeB = b
plate.BNPClassNativeA = a or 1
end
local function GetFallbackNativeColor(token)
if not token or not UnitExists(token) then return nil end
if UnitCanAttack and UnitCanAttack("player", token) then
return 1, 0, 0, 1
end
if UnitIsFriend and UnitIsFriend("player", token) then
return 0, 1, 0, 1
end
return 1, 1, 0, 1
end
local function RestoreNativeColor(plate)
if not plate or not plate.healthbar then
ClearClassCache(plate)
return false
end
local token, guid = ResolvePlateUnit(plate)
if token and guid and UnitIsPlayer(token) then
if plate.BNPClassNativeGUID == guid and plate.BNPClassNativeR then
plate.healthbar:SetStatusBarColor(
plate.BNPClassNativeR,
plate.BNPClassNativeG,
plate.BNPClassNativeB,
plate.BNPClassNativeA or 1
)
else
local r, g, b, a = GetFallbackNativeColor(token)
if r then plate.healthbar:SetStatusBarColor(r, g, b, a or 1) end
end
end
ClearClassCache(plate)
return true
end
local function ApplyClassColor(plate)
if not Enabled() then
RestoreNativeColor(plate)
return false
end
if not plate or not plate.healthbar then return false end
local token, guid = ResolvePlateUnit(plate)
-- BNP only owns PLAYER class colors.
-- NPC/neutral/friendly-NPC colors stay completely under Blizzard control.
if not token or not guid or not UnitIsPlayer(token) then
ClearClassCache(plate)
return false
end
local _, class = UnitClass(token)
local r, g, b = GetColor(class)
if not r then
ClearClassCache(plate)
return false
end
-- Remember Blizzard's original player color before BNP overwrites it so the
-- Class Colors checkbox can restore the plate immediately without /reload.
CaptureNativeColor(plate, guid)
plate.BNPClassColorApplied = true
plate.BNPClassColorToken = token
plate.BNPClassGUID = guid
plate.BNPClassR = r
plate.BNPClassG = g
plate.BNPClassB = b
plate.healthbar:SetStatusBarColor(r, g, b)
return true
end
BNP.ApplyClassColor = ApplyClassColor
BNP.RestoreClassColor = RestoreNativeColor
function BNP:RefreshClassColors()
local plate
for plate in pairs(self.plates or {}) do
if plate and plate:IsShown() and plate.healthbar then
if Enabled() then
ClearClassCache(plate)
ApplyClassColor(plate)
else
RestoreNativeColor(plate)
end
end
end
end
local function EnforceClassColor(plate)
if not Enabled() then return end
if not plate or not plate.healthbar or not plate.BNPClassGUID then return end
local token, guid = ResolvePlateUnit(plate)
-- Never enforce a cached color after Blizzard recycled the frame.
if not token or not guid or guid ~= plate.BNPClassGUID or not UnitIsPlayer(token) then
ClearClassCache(plate)
ClearNativeColor(plate)
return
end
local r, g, b, a = plate.healthbar:GetStatusBarColor()
if math.abs(r - plate.BNPClassR) > 0.01
or math.abs(g - plate.BNPClassG) > 0.01
or math.abs(b - plate.BNPClassB) > 0.01 then
-- Blizzard can rewrite a player plate back to its native reaction color.
-- Keep that latest native value so disabling class colors restores cleanly.
plate.BNPClassNativeGUID = guid
plate.BNPClassNativeR = r
plate.BNPClassNativeG = g
plate.BNPClassNativeB = b
plate.BNPClassNativeA = a or 1
plate.healthbar:SetStatusBarColor(
plate.BNPClassR,
plate.BNPClassG,
plate.BNPClassB
)
end
end
if BNP.libnameplate then
table.insert(BNP.libnameplate.OnInit, function(plate)
local current = plate or this
if not current then return end
ClearClassCache(current)
ClearNativeColor(current)
ApplyClassColor(current)
local bar = current.healthbar
if bar and not current.BNPClassColorHooked then
local oldValueChanged = bar:GetScript("OnValueChanged")
bar:SetScript("OnValueChanged", function()
if oldValueChanged then oldValueChanged() end
EnforceClassColor(current)
end)
current.BNPClassColorHooked = true
end
end)
table.insert(BNP.libnameplate.OnShow, function(plate)
local current = plate or this
if not current then return end
ClearClassCache(current)
ClearNativeColor(current)
ApplyClassColor(current)
end)
end
-- Blizzard may rewrite player colors during combat/threat updates.
-- Re-apply only while the plate still resolves to the SAME player GUID.
-- If a recycled frame now represents an NPC/neutral unit, drop the cache
-- immediately and never touch its Blizzard color.
local elapsed = 0
local scanBucket = 0
local nextBucket = 0
local function GetClassScanBucket(plate)
if plate.BNPClassScanBucket == nil then
plate.BNPClassScanBucket = nextBucket
nextBucket = 1 - nextBucket
end
return plate.BNPClassScanBucket
end
local updater = CreateFrame("Frame")
updater:SetScript("OnUpdate", function()
elapsed = elapsed + arg1
if elapsed < 0.05 then return end
elapsed = 0
if not Enabled() then return end
-- Split the old 0.10s all-plate burst into two 0.05s buckets. Every plate is
-- still checked once per 0.10s, but large BG/raid updates no longer land in
-- one Lua spike.
scanBucket = 1 - scanBucket
local plate
for plate in pairs(BNP.plates or {}) do
if plate:IsShown() and plate.healthbar and GetClassScanBucket(plate) == scanBucket then
if plate.BNPClassGUID then
-- EnforceClassColor already validates token/GUID/player identity. The old
-- updater resolved the same unit once here and a second time inside it.
EnforceClassColor(plate)
else
local token, guid = ResolvePlateUnit(plate)
if token and guid and UnitIsPlayer(token) then
ApplyClassColor(plate)
end
end
end
end
end)
+504
View File
@@ -0,0 +1,504 @@
BNP = BNP or {}
local _, playerClass = UnitClass("player")
local COMBO_CLASS = (playerClass == "ROGUE" or playerClass == "DRUID")
local MAX_POINTS = 5
local POINT_GAP = 1
local POINT_SIZE = 16
local CUSTOM_POINT_TEXTURE = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\combo_point_orb.tga"
local function DarkComboPointBorderActive()
-- Keep this independent from both ShaguTweaks and Dark Nameplate Border.
-- Players can choose the dark Combo Point rim without changing any other UI.
return BNP.IsDarkComboPointBorderEnabled
and BNP:IsDarkComboPointBorderEnabled()
end
local function ApplyComboPointTexture(point, force)
if not point or not point.texture then return end
local dark = DarkComboPointBorderActive() and true or false
-- Gold and dark are stored side by side in one atlas. Switching texture
-- coordinates is reliable on old 1.12 clients and avoids their texture-file
-- cache retaining the gold asset when a second filename is selected.
if force or point.BNPComboPointTexture ~= CUSTOM_POINT_TEXTURE then
point.texture:SetTexture(CUSTOM_POINT_TEXTURE)
point.BNPComboPointTexture = CUSTOM_POINT_TEXTURE
end
if force or point.BNPComboPointDarkBorder ~= dark then
if dark then
point.texture:SetTexCoord(0.5, 1, 0, 1)
else
point.texture:SetTexCoord(0, 0.5, 0, 1)
end
if point.texture.SetVertexColor then
point.texture:SetVertexColor(1, 1, 1, 1)
end
point.BNPComboPointDarkBorder = dark
end
end
local function ApplyComboPointHolderStyle(holder, force)
if not holder or not holder.points then return end
local i
for i = 1, MAX_POINTS do
ApplyComboPointTexture(holder.points[i], force)
end
end
-- Combo points can remain on the previous unit after changing target on this
-- server. Track the owning GUID ourselves instead of tying the display to the
-- currently selected target.
local comboOwnerGUID = nil
local comboOwnerCount = 0
local comboOwnerSynthetic = false
-- Some 1.12 clients do not immediately expose the first point on a newly
-- selected target through GetComboPoints(). Remember the generator CAST, but
-- do NOT move ownership from CAST alone: melee generators can miss/dodge/parry.
-- Ownership is moved immediately when PLAYER_COMBO_POINTS confirms that a
-- combo-point change actually happened. This is event-driven and adds no scan.
local pendingGenerator = nil
local GENERATOR_CONFIRM_WINDOW = 0.75
local function Enabled()
return COMBO_CLASS and BNP.AreComboPointsEnabled and BNP:AreComboPointsEnabled()
end
local function GetUnitGUID(unit)
local exists, guid = UnitExists(unit)
if exists and guid then return guid end
return nil
end
local function GetTargetGUID()
return GetUnitGUID("target")
end
local function GetPlayerGUID()
return GetUnitGUID("player")
end
local function GetComboCount()
if not GetComboPoints then return 0 end
local count = tonumber(GetComboPoints()) or 0
if count < 0 then count = 0 end
if count > MAX_POINTS then count = MAX_POINTS end
return count
end
local ROGUE_GENERATOR_IDS = {
-- Sinister Strike
[1752]=1,[1757]=1,[1758]=1,[1759]=1,[1760]=1,[8621]=1,[11293]=1,[11294]=1,
-- Backstab
[53]=1,[2589]=1,[2590]=1,[2591]=1,[8721]=1,[11279]=1,[11280]=1,[11281]=1,
-- Gouge
[1776]=1,[1777]=1,[8629]=1,[11285]=1,[11286]=1,
-- Ambush
[8676]=1,[8724]=1,[8725]=1,[11267]=1,[11268]=1,[11269]=1,
-- Garrote
[703]=1,[8631]=1,[8632]=1,[8633]=1,[11289]=1,[11290]=1,
-- Cheap Shot / Hemorrhage / Ghostly Strike / Premeditation
[1833]=1,[16511]=1,[17347]=1,[17348]=1,[14278]=1,[14183]=2,
}
local DRUID_GENERATOR_IDS = {
-- Claw
[1082]=1,[3029]=1,[5201]=1,[9849]=1,[9850]=1,
-- Rake
[1822]=1,[1823]=1,[1824]=1,[9904]=1,
-- Shred
[5221]=1,[6800]=1,[8992]=1,[9829]=1,[9830]=1,
-- Ravage
[6785]=1,[6787]=1,[9866]=1,[9867]=1,
-- Pounce
[9005]=1,[9823]=1,[9827]=1,
-- Mangle (Cat), for clients/servers that expose TBC/custom IDs
[33876]=1,[33982]=1,[33983]=1,
}
local ROGUE_GENERATOR_NAMES = {
["sinister strike"]=1,["finsterer stoß"]=1,
["backstab"]=1,["meucheln"]=1,
["gouge"]=1,["solarplexus"]=1,
["ambush"]=1,["hinterhalt"]=1,
["garrote"]=1,["erdrosseln"]=1,
["cheap shot"]=1,["fieser trick"]=1,
["hemorrhage"]=1,["blutsturz"]=1,
["ghostly strike"]=1,["geisterhafter stoß"]=1,
["premeditation"]=2,
}
local DRUID_GENERATOR_NAMES = {
["claw"]=1,["klaue"]=1,
["rake"]=1,["krallenhieb"]=1,
["shred"]=1,["schreddern"]=1,
["ravage"]=1,["verheeren"]=1,
["pounce"]=1,["anspringen"]=1,
["mangle"]=1,["mangle (cat)"]=1,["zerfleischen"]=1,["zerfleischen (katze)"]=1,
}
local function ResolveSpellName(spellID)
if not spellID or not SpellInfo then return nil end
local ok, name = pcall(SpellInfo, spellID)
if ok and name then return string.lower(tostring(name)) end
return nil
end
local function GetGeneratorGain(spellID)
local id = tonumber(spellID)
if playerClass == "ROGUE" then
if id and ROGUE_GENERATOR_IDS[id] then return ROGUE_GENERATOR_IDS[id] end
local name = ResolveSpellName(id)
if name then return ROGUE_GENERATOR_NAMES[name] end
elseif playerClass == "DRUID" then
if id and DRUID_GENERATOR_IDS[id] then return DRUID_GENERATOR_IDS[id] end
local name = ResolveSpellName(id)
if name then return DRUID_GENERATOR_NAMES[name] end
end
return nil
end
local function BeginPendingGenerator(targetGUID, spellID, gain)
if not targetGUID or not gain then return end
pendingGenerator = {
guid = targetGUID,
spellID = tonumber(spellID),
gain = tonumber(gain) or 1,
started = GetTime and GetTime() or 0,
}
end
local function CancelPendingGenerator(targetGUID, spellID)
if not pendingGenerator then return end
if targetGUID and pendingGenerator.guid ~= targetGUID then return end
if spellID and pendingGenerator.spellID and tonumber(spellID) ~= pendingGenerator.spellID then return end
pendingGenerator = nil
end
local function ApplyPendingGenerator()
if not pendingGenerator then return false end
local now = GetTime and GetTime() or 0
local started = pendingGenerator.started or now
-- PLAYER_COMBO_POINTS should arrive essentially immediately after a real
-- point gain. Ignore stale CAST candidates so a missed attack can never be
-- applied later by an unrelated combo-point event.
if now - started > GENERATOR_CONFIRM_WINDOW then
pendingGenerator = nil
return false
end
local guid = pendingGenerator.guid
local gain = pendingGenerator.gain or 1
local targetGUID = GetTargetGUID()
local apiCount = 0
-- Only trust GetComboPoints for the pending unit while it is the current
-- target. If the broken 1.12 client still reports zero, PLAYER_COMBO_POINTS
-- itself is the confirmation and we use the inferred generator gain.
if targetGUID and targetGUID == guid then
apiCount = GetComboCount()
end
if apiCount > 0 then
comboOwnerGUID = guid
comboOwnerCount = apiCount
comboOwnerSynthetic = false
else
if comboOwnerGUID == guid then
comboOwnerCount = math.min(MAX_POINTS, (comboOwnerCount or 0) + gain)
else
-- First confirmed generator on a different target: old owner's points
-- are gone and ownership switches immediately to this GUID.
comboOwnerGUID = guid
comboOwnerCount = math.min(MAX_POINTS, gain)
end
comboOwnerSynthetic = true
end
pendingGenerator = nil
return true
end
-- Read authoritative API state when available. A zero on some *other* current
-- target must never delete the stored points from the previous owner.
local function SyncComboOwnerFromAPI(allowZeroOnOwner)
local targetGUID = GetTargetGUID()
if not targetGUID then return end
local count = GetComboCount()
if count > 0 then
comboOwnerGUID = targetGUID
comboOwnerCount = count
comboOwnerSynthetic = false
BNP.spentComboSnapshot = nil
pendingGenerator = nil
return
end
if allowZeroOnOwner and comboOwnerGUID == targetGUID and not pendingGenerator then
if comboOwnerCount > 0 then
BNP.spentComboSnapshot = {
guid = comboOwnerGUID,
count = comboOwnerCount,
time = GetTime and GetTime() or 0,
}
end
comboOwnerGUID = nil
comboOwnerCount = 0
comboOwnerSynthetic = false
end
end
-- Finishers need the points that belonged to their exact cast target. Prefer
-- our GUID owner because the client may already report zero after spending the
-- points; fall back to the live API only for the matching current target.
function BNP:GetTrackedComboPointsForGUID(guid)
if guid and comboOwnerGUID == guid and comboOwnerCount > 0 then
return comboOwnerCount
end
local snapshot = BNP.spentComboSnapshot
local now = GetTime and GetTime() or 0
if guid and snapshot and snapshot.guid == guid
and now - (snapshot.time or 0) <= 1.0
and (snapshot.count or 0) > 0 then
return snapshot.count
end
local targetGUID = GetTargetGUID()
if targetGUID and (not guid or targetGUID == guid) then
return GetComboCount()
end
return 0
end
local function GetSourcePoint()
if getglobal then return getglobal("TargetFrameComboPoint1") end
return nil
end
local function GetComboYOffset()
if BNP.GetComboPointsYOffset then return BNP:GetComboPointsYOffset() end
return 0
end
local function AnchorComboHolder(plate, holder)
if not plate or not holder or not plate.name then return end
holder:ClearAllPoints()
holder:SetPoint("BOTTOM", plate.name, "TOP", 0, 2 + GetComboYOffset())
end
local function ApplyTargetFrameScale(holder)
if not holder then return end
local source = GetSourcePoint()
local parent = holder:GetParent()
local sourceScale = 1.0
local parentScale = 1.0
if source and source.GetEffectiveScale then
sourceScale = tonumber(source:GetEffectiveScale()) or 1.0
elseif UIParent and UIParent.GetEffectiveScale then
sourceScale = tonumber(UIParent:GetEffectiveScale()) or 1.0
elseif UIParent and UIParent.GetScale then
sourceScale = tonumber(UIParent:GetScale()) or 1.0
end
if parent and parent.GetEffectiveScale then
parentScale = tonumber(parent:GetEffectiveScale()) or 1.0
end
if sourceScale <= 0 then sourceScale = 1.0 end
if parentScale <= 0 then parentScale = 1.0 end
holder:SetScale(sourceScale / parentScale)
end
local function CreateCustomPoint(parent)
local point = CreateFrame("Frame", nil, parent)
point:SetWidth(POINT_SIZE)
point:SetHeight(POINT_SIZE)
local tex = point:CreateTexture(nil, "ARTWORK")
tex:SetTexture(CUSTOM_POINT_TEXTURE)
tex:SetAllPoints(point)
tex:SetTexCoord(0, 0.5, 0, 1)
tex:SetBlendMode("BLEND")
point.texture = tex
point.BNPComboPointTexture = CUSTOM_POINT_TEXTURE
point.BNPComboPointDarkBorder = false
ApplyComboPointTexture(point)
point:Hide()
return point
end
local function EnsurePoints(plate)
if plate.BNPComboPoints then
AnchorComboHolder(plate, plate.BNPComboPoints)
ApplyTargetFrameScale(plate.BNPComboPoints)
ApplyComboPointHolderStyle(plate.BNPComboPoints)
return plate.BNPComboPoints
end
if not plate or not plate.name then return nil end
local parent = plate.BNPScaleWrapper or plate
local holder = CreateFrame("Frame", nil, parent)
holder:SetWidth(MAX_POINTS * POINT_SIZE + (MAX_POINTS - 1) * POINT_GAP)
holder:SetHeight(POINT_SIZE)
AnchorComboHolder(plate, holder)
holder:SetFrameLevel((plate:GetFrameLevel() or 1) + 12)
holder.points = {}
holder:Hide()
local i
for i = 1, MAX_POINTS do
local point = CreateCustomPoint(holder)
if i == 1 then
point:SetPoint("LEFT", holder, "LEFT", 0, 0)
else
point:SetPoint("LEFT", holder.points[i - 1], "RIGHT", POINT_GAP, 0)
end
holder.points[i] = point
end
ApplyComboPointHolderStyle(holder)
ApplyTargetFrameScale(holder)
plate.BNPComboPoints = holder
return holder
end
local function HidePoints(plate)
local holder = plate and plate.BNPComboPoints
if not holder then return end
local i
for i = 1, MAX_POINTS do holder.points[i]:Hide() end
holder:Hide()
end
local function UpdatePlate(plate)
if not plate or not plate:IsShown() then return end
if not Enabled() then HidePoints(plate) return end
local plateGUID = plate.GetName and plate:GetName(1) or nil
if not plateGUID or not comboOwnerGUID or plateGUID ~= comboOwnerGUID then
HidePoints(plate)
return
end
if comboOwnerCount <= 0 then
HidePoints(plate)
return
end
local holder = EnsurePoints(plate)
if not holder then return end
ApplyTargetFrameScale(holder)
local i
for i = 1, MAX_POINTS do
if i <= comboOwnerCount then holder.points[i]:Show() else holder.points[i]:Hide() end
end
holder:Show()
end
function BNP:RefreshComboPoints()
for plate in pairs(BNP.plates or {}) do
if plate:IsShown() then UpdatePlate(plate) else HidePoints(plate) end
end
if BNP.RefreshAllAuraLayouts then BNP:RefreshAllAuraLayouts() end
end
-- Settings can be changed while no unit owns Combo Points. Update every
-- already-created holder directly instead of depending on UpdatePlate(), which
-- correctly skips plates without an active Combo Point owner.
function BNP:RefreshComboPointBorderStyle()
local plate
for plate in pairs(BNP.plates or {}) do
if plate and plate.BNPComboPoints then
ApplyComboPointHolderStyle(plate.BNPComboPoints, true)
end
end
end
table.insert(BNP.libnameplate.OnInit, function(plate)
if COMBO_CLASS then EnsurePoints(plate) HidePoints(plate) end
end)
table.insert(BNP.libnameplate.OnShow, function(plate)
if COMBO_CLASS then
-- Positive API counts may be adopted when a plate appears, but a zero on a
-- newly selected target must not clear the previous owner's points.
SyncComboOwnerFromAPI(false)
HidePoints(plate)
UpdatePlate(plate)
end
end)
table.insert(BNP.libnameplate.OnUpdate, function(plate)
if COMBO_CLASS then UpdatePlate(plate) end
end)
if COMBO_CLASS then
local events = CreateFrame("Frame")
events:RegisterEvent("PLAYER_TARGET_CHANGED")
events:RegisterEvent("PLAYER_COMBO_POINTS")
events:RegisterEvent("PLAYER_ENTERING_WORLD")
events:RegisterEvent("UNIT_CASTEVENT")
events:SetScript("OnEvent", function()
if event == "UNIT_CASTEVENT" then
local casterGUID = arg1
local targetGUID = arg2
local eventType = arg3
local spellID = arg4
local playerGUID = GetPlayerGUID()
if not playerGUID or casterGUID ~= playerGUID then return end
local gain = GetGeneratorGain(spellID)
if not gain then return end
if eventType == "CAST" then
BeginPendingGenerator(targetGUID or GetTargetGUID(), spellID, gain)
elseif eventType == "FAIL" then
CancelPendingGenerator(targetGUID, spellID)
end
return
end
if event == "PLAYER_TARGET_CHANGED" then
-- This is intentionally positive-only. Changing target by itself must
-- never clear the combo points stored on the previous GUID.
SyncComboOwnerFromAPI(false)
BNP:RefreshComboPoints()
return
end
if event == "PLAYER_COMBO_POINTS" then
-- This is the success confirmation. Switch ownership immediately here,
-- never on UNIT_CASTEVENT CAST alone. If the Blizzard frame/API still
-- returns zero, the pending generator provides the synthetic first point.
if pendingGenerator then
if not ApplyPendingGenerator() then
SyncComboOwnerFromAPI(true)
end
else
SyncComboOwnerFromAPI(true)
end
BNP:RefreshComboPoints()
return
end
if event == "PLAYER_ENTERING_WORLD" then
pendingGenerator = nil
SyncComboOwnerFromAPI(false)
BNP:RefreshComboPoints()
end
end)
end
+592
View File
@@ -0,0 +1,592 @@
-- Elite dragon render probe ---------------------------------------------------
-- Kept directly in commands.lua on purpose so /bnp dragontest cannot depend
-- on a separate module loading successfully. This is render-only: it does not
-- alter level text, classification state, nameplate discovery, or saved data.
local function BNP_FindTargetPlateForDragonTest()
if C_NamePlate and type(C_NamePlate.GetNamePlateForUnit) == "function" then
local ok, plate = pcall(C_NamePlate.GetNamePlateForUnit, "target")
if ok and plate then return plate end
end
local exists, guid = UnitExists("target")
if not exists or not guid then return nil end
local plate
for plate in pairs(BNP.plates or {}) do
if plate and plate:IsShown() and plate.GetName then
local ok, token = pcall(plate.GetName, plate, 1)
if ok and token == guid then return plate end
end
end
return nil
end
local function BNP_ToggleDirectDragonRenderTest()
if not UnitExists("target") then
BNP:Print("Target a visible unit first.")
return
end
local plate = BNP_FindTargetPlateForDragonTest()
if not plate then
BNP:Print("Target nameplate not found. Keep the target nameplate visible and try again.")
return
end
local healthbar = plate.healthbar
if not healthbar and plate.GetChildren then
healthbar = plate:GetChildren()
plate.healthbar = healthbar
end
if not healthbar then
BNP:Print("Target healthbar not found.")
return
end
if not plate.BNPDirectDragonTestFrame then
-- Put the dragon directly on the healthbar BACKGROUND layer.
-- This keeps the exact size/position/scaling, but lets the bar, border
-- and level text draw in front of it at all times.
local t = healthbar:CreateTexture(nil, "BACKGROUND")
t:SetWidth(37)
t:SetHeight(28)
t:SetPoint("CENTER", healthbar, "RIGHT", 20, -1)
t:SetTexture("Interface\\TargetingFrame\\UI-TargetingFrame-Elite")
t:SetTexCoord(0.58, 1.00, 0.00, 0.78125)
t:Hide()
plate.BNPDirectDragonTestFrame = t
end
local f = plate.BNPDirectDragonTestFrame
if f:IsShown() then
f:Hide()
BNP:Print("Dragon test OFF.")
else
f:Show()
local classification = UnitClassification and UnitClassification("target") or "?"
BNP:Print("Dragon test ON. Classification: " .. tostring(classification) .. ".")
end
end
-- Automatic Elite / World Boss dragon ---------------------------------------
-- Keep this in commands.lua deliberately: /bnp dragontest and /bnp eliteinfo
-- prove that this file and this exact plate/render path work on the 1.12 client.
-- The automatic path reuses the SAME frame created by dragontest.
local BNP_EliteAutoNextScan = 0
local BNP_EliteAutoState = {
ticks = 0,
visible = 0,
elite = 0,
shown = 0,
lastClass = "nil",
lastError = "none",
}
local function BNP_GetOrCreateAutoDragon(plate)
if not plate then return nil end
-- Reuse the exact proven dragontest frame if it already exists.
if plate.BNPDirectDragonTestFrame then
return plate.BNPDirectDragonTestFrame
end
local healthbar = plate.healthbar
if not healthbar and plate.GetChildren then
healthbar = plate:GetChildren()
plate.healthbar = healthbar
end
if not healthbar then return nil end
-- Same render path as /bnp dragontest: a BACKGROUND texture directly
-- on the healthbar, so the dragon always stays behind bar/level elements.
local t = healthbar:CreateTexture(nil, "BACKGROUND")
t:SetWidth(37)
t:SetHeight(28)
t:SetPoint("CENTER", healthbar, "RIGHT", 20, -1)
t:SetTexture("Interface\\TargetingFrame\\UI-TargetingFrame-Elite")
t:SetTexCoord(0.58, 1.00, 0.00, 0.78125)
t:Hide()
plate.BNPDirectDragonTestFrame = t
return t
end
local function BNP_UpdateEliteDragonPlate(plate, unit)
if not plate or not plate:IsShown() then
if plate and plate.BNPDirectDragonTestFrame then
plate.BNPDirectDragonTestFrame:Hide()
end
return
end
local token = unit
if not token and plate.GetName then
token = plate:GetName(1)
end
local classification = nil
if token and UnitClassification then
classification = UnitClassification(token)
end
local dragon = BNP_GetOrCreateAutoDragon(plate)
if not dragon then return end
if classification == "elite" or classification == "worldboss" then
dragon:Show()
else
dragon:Hide()
end
end
local function BNP_RunEliteAutoScan()
local visible = 0
local elite = 0
local shown = 0
local lastClass = "nil"
local plate
for plate in pairs(BNP.plates or {}) do
if plate and plate:IsShown() then
visible = visible + 1
local classification = nil
local token = nil
if plate.GetName then
token = plate:GetName(1)
end
if token and UnitClassification then
classification = UnitClassification(token)
end
lastClass = tostring(classification or "nil")
local dragon = BNP_GetOrCreateAutoDragon(plate)
if dragon then
if classification == "elite" or classification == "worldboss" then
elite = elite + 1
dragon:Show()
shown = shown + 1
else
dragon:Hide()
end
end
elseif plate and plate.BNPDirectDragonTestFrame then
plate.BNPDirectDragonTestFrame:Hide()
end
end
BNP_EliteAutoState.visible = visible
BNP_EliteAutoState.elite = elite
BNP_EliteAutoState.shown = shown
BNP_EliteAutoState.lastClass = lastClass
end
-- Instant path ---------------------------------------------------------------
-- 1) BNP's own OnShow fires immediately when an already-known Blizzard plate
-- is shown/reused.
if BNP.libnameplate and BNP.libnameplate.OnShow then
table.insert(BNP.libnameplate.OnShow, function(plate)
BNP_UpdateEliteDragonPlate(plate)
end)
end
-- 2) ClassicAPI's nameplate event can resolve the unit token directly on the
-- frame it belongs to. This is the fastest path for newly appearing plates.
local BNP_EliteInstantEvents = CreateFrame("Frame", nil, UIParent)
local addedOK = pcall(BNP_EliteInstantEvents.RegisterEvent, BNP_EliteInstantEvents, "NAME_PLATE_UNIT_ADDED")
local removedOK = pcall(BNP_EliteInstantEvents.RegisterEvent, BNP_EliteInstantEvents, "NAME_PLATE_UNIT_REMOVED")
if addedOK or removedOK then
BNP_EliteInstantEvents:SetScript("OnEvent", function()
local unit = arg1
if not unit then return end
if event == "NAME_PLATE_UNIT_ADDED" then
if C_NamePlate and type(C_NamePlate.GetNamePlateForUnit) == "function" then
local ok, plate = pcall(C_NamePlate.GetNamePlateForUnit, unit)
if ok and plate then
BNP_UpdateEliteDragonPlate(plate, unit)
end
end
elseif event == "NAME_PLATE_UNIT_REMOVED" then
-- The fallback pass clears hidden/recycled frames. If ClassicAPI still
-- exposes the frame at removal time, hide it immediately as well.
if C_NamePlate and type(C_NamePlate.GetNamePlateForUnit) == "function" then
local ok, plate = pcall(C_NamePlate.GetNamePlateForUnit, unit)
if ok and plate and plate.BNPDirectDragonTestFrame then
plate.BNPDirectDragonTestFrame:Hide()
end
end
end
end)
end
-- Apply immediately to any plates that were already visible when this module
-- loaded. Afterwards the 0.50s scan is only a safety net.
pcall(BNP_RunEliteAutoScan)
local BNP_EliteAutoFrame = CreateFrame("Frame", nil, UIParent)
BNP_EliteAutoFrame:SetScript("OnUpdate", function()
local now = GetTime()
if now < BNP_EliteAutoNextScan then return end
BNP_EliteAutoNextScan = now + 0.50
BNP_EliteAutoState.ticks = BNP_EliteAutoState.ticks + 1
local ok, err = pcall(BNP_RunEliteAutoScan)
if not ok then
BNP_EliteAutoState.lastError = tostring(err or "unknown")
else
BNP_EliteAutoState.lastError = "none"
end
end)
BNP_EliteAutoFrame:Show()
local function BNP_PrintEliteInfo()
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffBNP ELITEINFO|r")
local targetExists, targetGUID = UnitExists("target")
local targetName = UnitName("target") or "?"
local targetClass = UnitClassification and UnitClassification("target") or "?"
local targetLevel = UnitLevel and UnitLevel("target") or "?"
DEFAULT_CHAT_FRAME:AddMessage(
"TARGET | "..tostring(targetName)..
" | level "..tostring(targetLevel)..
" | class "..tostring(targetClass)..
" | guid "..tostring(targetGUID or "nil")
)
if not targetExists then
DEFAULT_CHAT_FRAME:AddMessage("No target. Target a visible Elite and run /bnp eliteinfo again.")
return
end
local apiTargetPlate = nil
if C_NamePlate and type(C_NamePlate.GetNamePlateForUnit) == "function" then
local ok, frame = pcall(C_NamePlate.GetNamePlateForUnit, "target")
if ok then apiTargetPlate = frame end
end
DEFAULT_CHAT_FRAME:AddMessage("C_NamePlate(target) | "..tostring(apiTargetPlate and "FOUND" or "nil"))
local matchedPlate = nil
local visible = 0
local plate
for plate in pairs(BNP.plates or {}) do
if plate and plate:IsShown() then
visible = visible + 1
local rawToken = nil
if plate.GetName then
local ok, value = pcall(plate.GetName, plate, 1)
if ok then rawToken = value end
end
local directMatch = apiTargetPlate and plate == apiTargetPlate
local guidMatch = targetGUID and rawToken == targetGUID
if directMatch or guidMatch then
matchedPlate = plate
local rawExists, rawGUID = false, nil
if rawToken then rawExists, rawGUID = UnitExists(rawToken) end
local rawClass = "nil"
if rawToken and UnitClassification then
local ok, value = pcall(UnitClassification, rawToken)
if ok then rawClass = value end
end
DEFAULT_CHAT_FRAME:AddMessage(
"BNP MATCH | direct "..tostring(directMatch and 1 or 0)..
" | guid "..tostring(guidMatch and 1 or 0)..
" | token "..tostring(rawToken or "nil")
)
DEFAULT_CHAT_FRAME:AddMessage(
"BNP TOKEN | exists "..tostring(rawExists and 1 or 0)..
" | guid "..tostring(rawGUID or "nil")..
" | class "..tostring(rawClass or "nil")
)
break
end
end
end
DEFAULT_CHAT_FRAME:AddMessage("BNP visible plates | "..tostring(visible).." | target match "..tostring(matchedPlate and "YES" or "NO"))
local existing = 0
local guidMatchUnit = nil
local frameMatchUnit = nil
local i
for i = 1, 40 do
local unit = "nameplate"..tostring(i)
local exists, guid = UnitExists(unit)
if exists then
existing = existing + 1
local class = UnitClassification and UnitClassification(unit) or "?"
local frame = nil
if C_NamePlate and type(C_NamePlate.GetNamePlateForUnit) == "function" then
local ok, value = pcall(C_NamePlate.GetNamePlateForUnit, unit)
if ok then frame = value end
end
if targetGUID and guid == targetGUID then
guidMatchUnit = unit
DEFAULT_CHAT_FRAME:AddMessage(
"TOKEN GUID MATCH | "..unit..
" | class "..tostring(class)..
" | frame "..tostring(frame and "FOUND" or "nil")
)
end
if apiTargetPlate and frame == apiTargetPlate then
frameMatchUnit = unit
DEFAULT_CHAT_FRAME:AddMessage(
"TOKEN FRAME MATCH | "..unit..
" | guid "..tostring(guid or "nil")..
" | class "..tostring(class)
)
end
end
end
DEFAULT_CHAT_FRAME:AddMessage(
"nameplateN | existing "..tostring(existing)..
" | guidMatch "..tostring(guidMatchUnit or "NONE")..
" | frameMatch "..tostring(frameMatchUnit or "NONE")
)
end
SLASH_BLIZZNAMEPLATESPLUS1 = "/bnp"
SlashCmdList["BLIZZNAMEPLATESPLUS"] = function(msg)
msg = string.lower(msg or "")
if msg == "" or msg == "config" or msg == "options" then
if BNP.ToggleOptions then BNP:ToggleOptions() end
elseif msg == "spellprobe" or msg == "spellbook" then
if BNP.SpellbookProbe then BNP:SpellbookProbe() else BNP:Print("Spellbook probe is not loaded.") end
elseif msg == "spellprobe corruption" then
if BNP.SpellbookProbe then BNP:SpellbookProbe("corruption") end
elseif msg == "spellprobe agony" then
if BNP.SpellbookProbe then BNP:SpellbookProbe("agony") end
elseif msg == "spellprobe siphon" then
if BNP.SpellbookProbe then BNP:SpellbookProbe("siphon") end
elseif msg == "unknown" or msg == "learn" then
if BNP.PrintUnknownBeta then BNP:PrintUnknownBeta(false) else BNP:Print("Unknown collector is not loaded.") end
elseif msg == "unknown verbose" or msg == "learn verbose" then
if BNP.PrintUnknownBeta then BNP:PrintUnknownBeta(true) else BNP:Print("Unknown collector is not loaded.") end
elseif msg == "unknown reset" or msg == "learn reset" then
if BNP.ResetUnknownBeta then BNP:ResetUnknownBeta() end
BNP.unknownDirectEvents = {}
elseif msg == "auras" or msg == "targetauras" then
if BNP.PrintTargetAuraProbe then
BNP:PrintTargetAuraProbe()
else
BNP:Print("Target aura probe is not loaded.")
end
elseif msg == "raidcheck" then
local exists, guid = UnitExists("target")
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffBNP RAIDCHECK|r")
if not exists or not guid then
DEFAULT_CHAT_FRAME:AddMessage("No target / no target GUID.")
return
end
DEFAULT_CHAT_FRAME:AddMessage("Target: "..tostring(UnitName("target") or "?").." | GUID: "..tostring(guid))
local cache=BNP.guidAuras and BNP.guidAuras[guid]
local n=0
if cache then
local key,aura
for key,aura in pairs(cache) do
if type(aura)=="table" and aura.spellID then
n=n+1
local mode = aura.castPrimary and "CAST" or (aura.liveConfirmed and "LIVE" or "?")
DEFAULT_CHAT_FRAME:AddMessage(
"CACHE | "..tostring(key).." | ID "..tostring(aura.spellID).." | "..mode..
" | stacks "..tostring(aura.stacks or 0)..
" | left "..(aura.expires and string.format("%.1f", aura.expires-GetTime()) or "?")
)
end
end
end
if n==0 then DEFAULT_CHAT_FRAME:AddMessage("CACHE: EMPTY") end
local svState = BNP.svStateByGUID and BNP.svStateByGUID[guid]
if svState then
DEFAULT_CHAT_FRAME:AddMessage(
"SV STATE | present "..tostring(svState.present and 1 or 0)..
" | ID "..tostring(svState.spellID or "?")..
" | left "..(svState.expires and string.format("%.1f", svState.expires-GetTime()) or "?")..
" | refreshed "..(svState.lastRefresh and string.format("%.1fs ago", GetTime()-svState.lastRefresh) or "?")
)
else
DEFAULT_CHAT_FRAME:AddMessage("SV STATE: none")
end
-- Renderer-side SV diagnostics: tells us what the actual visible icon has
-- consumed from the GUID state, not just what the cache contains.
local rendered = false
local plate
for plate in pairs(BNP.plates or {}) do
if plate and plate:IsShown() and plate.GetName and plate:GetName(1) == guid then
local c = plate.BNPAuraContainer
if c and c.icons then
local ri
for ri = 1, table.getn(c.icons) do
local icon = c.icons[ri]
if icon and icon.BNPSVRenderedExpires then
DEFAULT_CHAT_FRAME:AddMessage(
"SV RENDER | icon "..tostring(ri)..
" | text "..tostring(icon.timer and icon.timer:GetText() or "?")..
" | left "..string.format("%.1f", (icon.BNPSVRenderedExpires or GetTime())-GetTime())..
" | rev "..tostring(icon.BNPSVRenderRevision or 0)
)
rendered = true
break
end
end
end
end
end
if not rendered then DEFAULT_CHAT_FRAME:AddMessage("SV RENDER: no visible SV icon") end
local pending = BNP.pendingAuras and BNP.pendingAuras[guid]
if pending then
DEFAULT_CHAT_FRAME:AddMessage("-- PENDING FOR TARGET --")
local pk,pv
for pk,pv in pairs(pending) do
if type(pv)=="table" then
DEFAULT_CHAT_FRAME:AddMessage(
"PENDING | "..tostring(pk)..
" | ID "..tostring(pv.spellID or "?")..
" | age "..string.format("%.1fs", GetTime()-(pv.created or GetTime()))
)
end
end
else
DEFAULT_CHAT_FRAME:AddMessage("PENDING: none")
end
DEFAULT_CHAT_FRAME:AddMessage("-- LAST TRACKED CAST EVENTS --")
local trace=BNP.raidTrace or {}
local start=math.max(1,table.getn(trace)-14)
local ti
for ti=start,table.getn(trace) do
local e=trace[ti]
DEFAULT_CHAT_FRAME:AddMessage(
tostring(e.stage)..
" | "..tostring(e.key or "-")..
" | ID "..tostring(e.spellID or "?")..
" | GUID "..tostring(e.guid or "nil")..
" | "..string.format("%.1fs ago",GetTime()-(e.time or GetTime()))
)
end
DEFAULT_CHAT_FRAME:AddMessage("-- LIVE TARGET AURAS (up to 64) --")
local i
for i=1,64 do
local tex,stacks,dtype,id=UnitDebuff("target",i)
if not tex then break end
local name=(id and SpellInfo and SpellInfo(id)) or "?"
DEFAULT_CHAT_FRAME:AddMessage("LIVE "..tostring(i).." | ID "..tostring(id or "?").." | "..tostring(name).." | stacks "..tostring(stacks or 0))
end
local svp = BNP.svProbe
if svp then
DEFAULT_CHAT_FRAME:AddMessage("-- SV PROBE --")
local last = svp.last
if last then
DEFAULT_CHAT_FRAME:AddMessage(
"SV LIVE | "..tostring(last.reason or "?").." | unit "..tostring(last.unit or "?")..
" | idx "..tostring(last.index or "?").." | ID "..tostring(last.spellID or "?")..
" | stacks "..tostring(last.stacks or 0)
)
DEFAULT_CHAT_FRAME:AddMessage(
"RET a3="..tostring(last.a3).." a5="..tostring(last.a5).." a6="..tostring(last.a6)..
" a7="..tostring(last.a7).." a8="..tostring(last.a8).." a9="..tostring(last.a9).." a10="..tostring(last.a10)
)
else
DEFAULT_CHAT_FRAME:AddMessage("SV LIVE: no captured snapshot")
end
local ev = svp.events or {}
local es = math.max(1, table.getn(ev)-9)
local ei
for ei=es,table.getn(ev) do
local e=ev[ei]
DEFAULT_CHAT_FRAME:AddMessage(
tostring(e.stage).." | ID "..tostring(e.spellID or "?").." | GUID "..tostring(e.guid or "nil")..
" | "..tostring(e.extra or "-").." | "..string.format("%.1fs ago",GetTime()-(e.time or GetTime()))
)
end
DEFAULT_CHAT_FRAME:AddMessage("-- SV RAW WINDOW --")
local raw = svp.raw or {}
if table.getn(raw) == 0 then
DEFAULT_CHAT_FRAME:AddMessage("RAW: no matching lines captured")
else
local rs = math.max(1, table.getn(raw)-13)
local ri
for ri=rs,table.getn(raw) do
local r=raw[ri]
DEFAULT_CHAT_FRAME:AddMessage(
"RAW "..string.format("%.1fs",GetTime()-(r.time or GetTime()))..
" | "..tostring(r.event or "?").." | "..tostring(r.raw or "")
)
end
end
end
elseif msg == "direct" then
if BNP.PrintUnknownDirectEvents then
BNP:PrintUnknownDirectEvents()
else
BNP:Print("Direct event collector is not loaded.")
end
elseif msg == "spelldb" or msg == "audit" then
if BNP.PrintSpellDBAudit then BNP:PrintSpellDBAudit() else BNP:Print("SpellDB audit is not loaded.") end
elseif msg == "spelldb reset" or msg == "audit reset" then
if BNP.ResetSpellDBAudit then BNP:ResetSpellDBAudit() end
elseif msg == "guid" or msg == "guiddebug" or msg == "diag" then
if BNP.PrintGUIDDiagnostic then BNP:PrintGUIDDiagnostic() else BNP:Print("GUID diagnostics are not loaded.") end
elseif msg == "dragontest" then
BNP_ToggleDirectDragonRenderTest()
elseif msg == "eliteinfo" then
BNP_PrintEliteInfo()
elseif msg == "eliteauto" then
BNP:Print("Elite auto | ticks " .. tostring(BNP_EliteAutoState.ticks) .. " | visible " .. tostring(BNP_EliteAutoState.visible) .. " | elite " .. tostring(BNP_EliteAutoState.elite) .. " | shown " .. tostring(BNP_EliteAutoState.shown) .. " | lastClass " .. tostring(BNP_EliteAutoState.lastClass) .. " | error " .. tostring(BNP_EliteAutoState.lastError))
elseif msg == "status" then
local visible = 0
local plate
for plate in pairs(BNP.plates) do
if plate:IsShown() then visible = visible + 1 end
end
BNP:Print("registered: " .. BNP.detected .. ", visible: " .. visible .. ", test spell: " .. tostring(BNP.corruptionName or "not found"))
elseif msg == "cache" then
local name = UnitName("target")
if not name then
BNP:Print("No target selected.")
return
end
local remaining = BNP.FindCachedCorruption and BNP.FindCachedCorruption(name)
BNP:Print("Cache for " .. name .. ": " .. tostring(remaining or "no Corruption found"))
elseif msg == "castbars on" then
if BNP.SetCastbarsEnabled then BNP:SetCastbarsEnabled(true) end
elseif msg == "castbars off" then
if BNP.SetCastbarsEnabled then BNP:SetCastbarsEnabled(false) end
elseif msg == "scale" then
BNP:Print("Current nameplate scale: " .. tostring(BNP:GetNameplateScale()))
elseif msg == "scale reset" then
if BNP.ResetNameplateScale then BNP:ResetNameplateScale() end
elseif string.find(msg, "^scale%s+") then
local _, _, value = string.find(msg, "^scale%s+([0-9%.]+)")
if BNP.SetNameplateScale then BNP:SetNameplateScale(value) end
elseif msg == "classcolors on" then
BNP_DB = BNP_DB or {}
BNP_DB.classColors = true
BNP:Print("Class colors enabled.")
elseif msg == "classcolors off" then
BNP_DB = BNP_DB or {}
BNP_DB.classColors = false
BNP:Print("Class colors disabled.")
elseif msg == "debug on" then
BNP:SetDebug(true)
elseif msg == "debug off" then
BNP:SetDebug(false)
elseif msg == "debug" then
BNP:SetDebug(not BNP.debugEnabled)
else
BNP:Print("Commands: /bnp unknown, /bnp direct, /bnp spellprobe, /bnp spelldb, /bnp guid, /bnp dragontest, /bnp eliteinfo, /bnp eliteauto, /bnp status, /bnp castbars on/off, /bnp scale <0.5-1.5>, /bnp scale reset, /bnp classcolors on/off, /bnp debug on, /bnp debug off")
end
end
+43
View File
@@ -0,0 +1,43 @@
if not BNP.libnameplate then return end
local function CreateMarker(plate)
if not plate then return end
BNP:RegisterPlate(plate)
if not plate.BNPMarker then
local marker = CreateFrame("Frame", nil, plate)
marker:SetWidth(14)
marker:SetHeight(14)
marker:SetPoint("BOTTOM", plate, "TOP", 0, 34)
marker:SetFrameLevel(30)
marker.texture = marker:CreateTexture(nil, "OVERLAY")
marker.texture:SetAllPoints(marker)
marker.texture:SetTexture("Interface\\Buttons\\WHITE8X8")
marker.texture:SetVertexColor(1, 0, 0, 1)
marker.text = marker:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
marker.text:SetPoint("CENTER", marker, "CENTER", 0, 0)
marker.text:SetText("+")
marker.text:SetTextColor(1, 1, 1)
plate.BNPMarker = marker
end
if BNP.debugEnabled then plate.BNPMarker:Show() else plate.BNPMarker:Hide() end
end
table.insert(BNP.libnameplate.OnInit, CreateMarker)
table.insert(BNP.libnameplate.OnShow, CreateMarker)
function BNP:SetDebug(enabled)
BNP.debugEnabled = enabled
BNP_DB = BNP_DB or {}
BNP_DB.debugEnabled = enabled
local plate
for plate in pairs(BNP.plates) do
if plate.BNPMarker then
if enabled then plate.BNPMarker:Show() else plate.BNPMarker:Hide() end
end
end
BNP:Print("Debug-Marker " .. (enabled and "aktiviert" or "deaktiviert") .. ".")
end
+815
View File
@@ -0,0 +1,815 @@
BNP = BNP or {}
local NON_TARGET_ALPHA = 0.55
local TARGET_RESOLVE_CACHE_TIME = 0.05
local ALPHA_IDENTITY_HOLD_TIME = 0.20
local FOREIGN_TAG_SCAN_INTERVAL = 0.10
local targetResolveCacheGUID = nil
local targetResolveCacheAt = -100
local targetResolveCacheFound = false
local classicTargetCacheGUID = nil
local classicTargetCacheAt = -100
local classicTargetCachePlate = nil
local classicTargetCacheAvailable = false
local targetGUIDCacheAt = -100
local targetGUIDCacheValue = nil
local TARGET_GUID_CACHE_TIME = 0.02
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
return token
end
local function GetVerifiedPlateGUID(plate)
if not plate or not plate.GetName then return nil end
local token = plate:GetName(1)
if not token then return nil end
-- Alpha must only be lowered after SuperWoW has positively resolved the
-- projected plate token. While rotating the camera Blizzard can recycle and
-- re-show a plate before its token is live again. During that short window
-- treating nil/stale identity as "non-target" caused random low-alpha pops.
local exists, guid = UnitExists(token)
if exists and guid then return guid end
return nil
end
local function GetTargetGUID()
-- Many per-frame plate paths ask for the same target GUID. Cache it for a
-- tiny window (roughly one rendered frame); PLAYER_TARGET_CHANGED explicitly
-- invalidates this cache, so target switches still update immediately.
local now = GetTime()
if now - targetGUIDCacheAt < TARGET_GUID_CACHE_TIME then
return targetGUIDCacheValue
end
local exists, guid = UnitExists("target")
if exists and guid then targetGUIDCacheValue = guid else targetGUIDCacheValue = nil end
targetGUIDCacheAt = now
return targetGUIDCacheValue
end
local function InvalidateTargetResolution()
targetResolveCacheGUID = nil
targetResolveCacheAt = -100
targetResolveCacheFound = false
classicTargetCacheGUID = nil
classicTargetCacheAt = -100
classicTargetCachePlate = nil
classicTargetCacheAvailable = false
targetGUIDCacheAt = -100
targetGUIDCacheValue = nil
end
local function GetClassicAPITargetPlate(targetGUID)
local now = GetTime()
if classicTargetCacheGUID == targetGUID and
now - classicTargetCacheAt < TARGET_RESOLVE_CACHE_TIME then
return classicTargetCachePlate, classicTargetCacheAvailable
end
local available = C_NamePlate and
type(C_NamePlate.GetNamePlateForUnit) == "function"
local targetPlate = nil
if available then
local ok, plate = pcall(C_NamePlate.GetNamePlateForUnit, "target")
if ok then targetPlate = plate end
end
classicTargetCacheGUID = targetGUID
classicTargetCacheAt = now
classicTargetCachePlate = targetPlate
classicTargetCacheAvailable = available and true or false
return targetPlate, classicTargetCacheAvailable
end
local function GetPlateToken(plate)
if not plate or not plate.GetName then return nil end
return plate:GetName(1)
end
local function GetRegionText(region)
if not region or not region.GetText then return nil end
return region:GetText()
end
local function PlateMatchesTargetVisual(plate)
if not UnitName then return false end
local targetName = UnitName("target")
local plateName = GetRegionText(plate and plate.name)
if not targetName or not plateName or targetName ~= plateName then
return false
end
-- Same-name mobs are common. A known, different level therefore rules the
-- plate out. If the level is hidden/"??", keeping another same-name plate at
-- full alpha is the safe failure mode; it is preferable to dimming the real
-- target because a projected token still carries its previous GUID.
if UnitLevel then
local targetLevel = UnitLevel("target")
local plateLevel = tonumber(GetRegionText(plate and plate.level))
if targetLevel and targetLevel > 0 and plateLevel and plateLevel > 0 and
targetLevel ~= plateLevel then
return false
end
end
return true
end
local function IsPlateCurrentTarget(plate, targetGUID)
if not plate or not targetGUID then return false end
local token = GetPlateToken(plate)
if token and UnitIsUnit then
local ok, sameUnit = pcall(UnitIsUnit, token, "target")
if ok and sameUnit then return true end
end
local plateGUID = GetVerifiedPlateGUID(plate)
if plateGUID and plateGUID == targetGUID then return true end
-- SuperWoW can briefly return the old, but still syntactically valid, GUID
-- for a recycled projected nameplate. The displayed Blizzard name/level has
-- already switched by then and protects the actual target from being dimmed.
return PlateMatchesTargetVisual(plate)
end
local function HasResolvedVisibleTarget(targetGUID)
local now = GetTime()
if targetResolveCacheGUID == targetGUID and
now - targetResolveCacheAt < TARGET_RESOLVE_CACHE_TIME then
return targetResolveCacheFound
end
local found = false
local plate
for plate in pairs(BNP.plates or {}) do
if plate and plate.IsShown and plate:IsShown() and
IsPlateCurrentTarget(plate, targetGUID) then
found = true
break
end
end
targetResolveCacheGUID = targetGUID
targetResolveCacheAt = now
targetResolveCacheFound = found
return found
end
local function IsForeignTagged(plate)
if not plate or not plate.GetName then return false end
local token = plate:GetName(1)
if not token then return false end
local exists = UnitExists(token)
if not exists then return false end
if UnitCanAttack and not UnitCanAttack("player", token) then
return false
end
if not UnitIsTapped or not UnitIsTapped(token) then
return false
end
if UnitIsTappedByPlayer and UnitIsTappedByPlayer(token) then
return false
end
return true
end
local function DesiredAlpha(plate, targetGUID)
-- No selected target: keep all nameplates fully visible.
targetGUID = targetGUID or GetTargetGUID()
if not targetGUID then return 1 end
local classicTargetPlate, classicTargetAvailable =
GetClassicAPITargetPlate(targetGUID)
if classicTargetAvailable then
-- Exact ClassicAPI frame resolution makes the old transition hold
-- unnecessary. More importantly, it lets us keep alpha stable even when a
-- crowded projected nameplate token briefly stops resolving. The previous
-- code treated an unresolved non-target token as alpha 1 and then faded it
-- again on the next tick, which made both the plate and its aura children
-- visibly pulse/flash in large stacked raids.
if not classicTargetPlate then return 1 end
-- The exact target frame is authoritative and must always stay opaque,
-- even while its SuperWoW token is transiently unavailable.
if plate == classicTargetPlate then return 1 end
-- GUID equality remains as a conservative fallback in case a client build
-- returns a wrapper/alias for the target nameplate rather than the exact
-- frame object tracked by BNP.
local plateGUID = GetVerifiedPlateGUID(plate)
if plateGUID and plateGUID == targetGUID then return 1 end
-- Once ClassicAPI has positively identified the real target frame, every
-- other visible plate is safely a non-target. Do not bounce it to alpha 1
-- merely because its projected token was nil for one raid-stack tick.
if BNP.GetNonTargetAlpha then
return BNP:GetNonTargetAlpha()
end
return 1
end
-- On target changes and recycled OnShow transitions, the projected token can
-- temporarily expose its previous valid GUID. Keep the plate neutral until
-- Blizzard/SuperWoW have had time to publish the new identity. This is only
-- the legacy path used when exact ClassicAPI nameplate resolution is absent.
if plate.BNPAlphaIdentityHoldUntil and
GetTime() < plate.BNPAlphaIdentityHoldUntil then
return 1
end
-- GUID equality is preferred. UnitIsUnit and the displayed Blizzard
-- name/level are conservative fallbacks for the stale-valid-GUID window.
if IsPlateCurrentTarget(plate, targetGUID) then
return 1
end
-- Do not dim anything until the selected target itself can be associated
-- with a currently visible plate. Uncertainty must fail to full opacity.
if not HasResolvedVisibleTarget(targetGUID) then
return 1
end
-- A recycled/reappearing plate can exist for a moment before SuperWoW has
-- attached a trustworthy GUID again. Never dim an unidentified plate; doing
-- so is what caused the random low-alpha flash when rotating the camera.
local plateGUID = GetVerifiedPlateGUID(plate)
if not plateGUID then return 1 end
-- Current target always stays fully visible.
if plateGUID == targetGUID then
return 1
end
-- Only positively identified non-target plates use the configured alpha.
if BNP.GetNonTargetAlpha then
return BNP:GetNonTargetAlpha()
end
return 1
end
local function SetLevelSafe(frame, level)
if frame and frame.SetFrameLevel and level then
if frame:GetFrameLevel() ~= level then
frame:SetFrameLevel(level)
end
end
end
local function ApplyTargetFrameLevel(plate)
if not plate or not plate:IsShown() then return end
-- IMPORTANT:
-- Never change the FrameStrata of projected Vanilla nameplates.
-- Their native client/world layer keeps them behind Blizzard UI frames.
-- Permanent target priority is therefore implemented only with FrameLevel.
if plate.BNPBaseFrameLevel == nil then
plate.BNPBaseFrameLevel = plate:GetFrameLevel() or 0
end
local wrapper = plate.BNPScaleWrapper
if wrapper and plate.BNPBaseWrapperLevel == nil then
plate.BNPBaseWrapperLevel = wrapper:GetFrameLevel() or plate.BNPBaseFrameLevel
end
local healthbar = plate.healthbar
if healthbar and plate.BNPBaseHealthbarLevel == nil then
plate.BNPBaseHealthbarLevel = healthbar:GetFrameLevel() or 1
end
-- ApplyTargetAlpha runs immediately before this function and publishes the
-- exact ClassicAPI/SuperWoW target identity on the plate.
local target = plate.BNPIsCurrentTarget and true or false
local plateLevel = plate.BNPBaseFrameLevel
local wrapperLevel = plate.BNPBaseWrapperLevel or plateLevel
local healthbarLevel = plate.BNPBaseHealthbarLevel or 1
if target then
-- Raise the target only relative to other nameplates while preserving the
-- native projected-nameplate strata beneath the Blizzard interface.
plateLevel = plateLevel + 50
wrapperLevel = wrapperLevel + 50
healthbarLevel = healthbarLevel + 50
end
SetLevelSafe(plate, plateLevel)
if wrapper then
SetLevelSafe(wrapper, wrapperLevel)
end
if healthbar then
-- The Blizzard border/name are regions of the raised wrapper, but the
-- actual StatusBar is its own frame and must be raised explicitly.
SetLevelSafe(healthbar, healthbarLevel)
end
-- Keep BNP's castbar above the healthbar (and therefore above the enlarged
-- target-glow texture, which belongs to the healthbar). The target plate is
-- raised dynamically by +50 levels, so a castbar that was created before a
-- target switch must follow that new level too. Otherwise the glow can draw
-- over the castbar when Target Glow Size is increased.
local castbar = plate.BNPCastbar
if castbar then
local castbarLevel = healthbarLevel + 1
SetLevelSafe(castbar, castbarLevel)
if castbar.icon then
SetLevelSafe(castbar.icon, castbarLevel + 1)
end
end
end
local function ApplyForeignTagVisual(plate, forceScan)
if not plate or not plate:IsShown() then return end
local bar = plate.healthbar
if not bar or not bar.SetStatusBarColor then return end
-- UnitExists/UnitCanAttack/UnitIsTapped are relatively expensive when called
-- for every nameplate on every rendered frame. Cache the ownership result for
-- 0.10s, but still enforce the already-known grey color every frame so client
-- color rewrites cannot reintroduce the old grey/red flicker.
local now = GetTime()
local foreign = plate.BNPForeignTagCached
if forceScan or not plate.BNPForeignTagScanAt or
now - plate.BNPForeignTagScanAt >= FOREIGN_TAG_SCAN_INTERVAL then
foreign = IsForeignTagged(plate) and true or false
plate.BNPForeignTagCached = foreign
plate.BNPForeignTagScanAt = now
end
if foreign then
if not plate.BNPForeignTagGrey then
local r, g, b, a = bar:GetStatusBarColor()
plate.BNPForeignTagOldR = r
plate.BNPForeignTagOldG = g
plate.BNPForeignTagOldB = b
plate.BNPForeignTagOldA = a
plate.BNPForeignTagGrey = true
end
-- Neutral grey: visually distinct from Target Focus transparency. Only
-- rewrite the StatusBar when something actually changed its color.
local r, g, b = bar:GetStatusBarColor()
if math.abs((r or 0) - 0.45) > 0.01 or
math.abs((g or 0) - 0.45) > 0.01 or
math.abs((b or 0) - 0.45) > 0.01 then
bar:SetStatusBarColor(0.45, 0.45, 0.45, plate.BNPForeignTagOldA or 1)
end
elseif plate.BNPForeignTagGrey then
-- Restore the color that BNP/Shagu/class-color logic had before the mob
-- became foreign-tagged. Clear state so future color changes can be learned.
bar:SetStatusBarColor(
plate.BNPForeignTagOldR or 1,
plate.BNPForeignTagOldG or 0,
plate.BNPForeignTagOldB or 0,
plate.BNPForeignTagOldA or 1
)
plate.BNPForeignTagGrey = nil
plate.BNPForeignTagOldR = nil
plate.BNPForeignTagOldG = nil
plate.BNPForeignTagOldB = nil
plate.BNPForeignTagOldA = nil
end
end
local function GetGlowVisualParent(plate)
if not plate then return nil end
local anchor = plate.healthbar or plate
local visualParent = plate.BNPScaleWrapper
if not visualParent and anchor and anchor.GetParent then
visualParent = anchor:GetParent()
end
return visualParent or plate
end
local function EnsureTargetGlow(plate)
if not plate then return nil end
if plate.BNPTargetGlow then return plate.BNPTargetGlow end
local anchor = plate.healthbar or plate
if not anchor then return nil end
-- Keep Blizzard's original healthbar/frame layering completely untouched.
-- The glow belongs to the healthbar itself on BACKGROUND, so it stays behind
-- the statusbar fill without raising/reparenting the Blizzard healthbar.
--
-- Direct port of ShaguPlates' target glow geometry:
-- nameplate.glow = nameplate:CreateTexture(nil, "BACKGROUND")
-- glow anchored to health CENTER
-- width = health width + 60
-- height = health height + 30
--
-- BNP's only intentional visual change is +3 px on X.
local glow = anchor:CreateTexture(nil, "BACKGROUND")
glow:SetTexture("Interface\\AddOns\\BlizzNameplatesPlus\\media\\shagu_target_glow.tga")
glow:SetPoint("CENTER", anchor, "CENTER", 3, 0)
local width = anchor.GetWidth and anchor:GetWidth() or 120
local height = anchor.GetHeight and anchor:GetHeight() or 8
local glowSize = BNP.GetTargetGlowSize and BNP:GetTargetGlowSize() or 0
glow:SetWidth(width + 60 + (glowSize * 2))
glow:SetHeight(height + 30 + (glowSize * 2))
local r, g, b = 1, 1, 1
if BNP.GetTargetGlowColor then
r, g, b = BNP:GetTargetGlowColor()
end
glow:SetVertexColor(r, g, b, 1)
glow:Hide()
plate.BNPTargetGlow = glow
return glow
end
local function ApplyTargetGlow(plate)
if not plate or not plate:IsShown() then return end
local enabled = BNP.IsTargetFocusEnabled and BNP:IsTargetFocusEnabled()
local isTarget = plate.BNPIsCurrentTarget and true or false
-- Do not create one glow texture for every nameplate. Only the actual target
-- needs it; previously even disabled/non-target plates allocated and updated
-- their own texture every frame.
if not enabled or not isTarget then
local existing = plate.BNPTargetGlow
if existing and existing.IsShown and existing:IsShown() then existing:Hide() end
return
end
local glow = EnsureTargetGlow(plate)
if not glow then return end
local anchor = plate.healthbar or plate
local width = anchor.GetWidth and anchor:GetWidth() or 120
local height = anchor.GetHeight and anchor:GetHeight() or 8
local glowSize = BNP.GetTargetGlowSize and BNP:GetTargetGlowSize() or 0
-- Geometry only changes when the healthbar dimensions or the user setting
-- changes. Avoid ClearAllPoints/SetPoint/SetWidth/SetHeight every frame.
if glow.BNPLastAnchor ~= anchor or
glow.BNPLastWidth ~= width or glow.BNPLastHeight ~= height or
glow.BNPLastSize ~= glowSize then
glow:ClearAllPoints()
glow:SetPoint("CENTER", anchor, "CENTER", 3, 0)
glow:SetWidth(width + 60 + (glowSize * 2))
glow:SetHeight(height + 30 + (glowSize * 2))
glow.BNPLastAnchor = anchor
glow.BNPLastWidth = width
glow.BNPLastHeight = height
glow.BNPLastSize = glowSize
end
local r, g, b = 1, 1, 1
if BNP.GetTargetGlowColor then r, g, b = BNP:GetTargetGlowColor() end
if glow.BNPLastR ~= r or glow.BNPLastG ~= g or glow.BNPLastB ~= b then
glow:SetVertexColor(r, g, b, 1)
glow.BNPLastR, glow.BNPLastG, glow.BNPLastB = r, g, b
end
local glowOpacity = BNP.GetTargetGlowOpacity and BNP:GetTargetGlowOpacity() or 1
if glow.BNPLastOpacity ~= glowOpacity then
glow:SetAlpha(glowOpacity)
glow.BNPLastOpacity = glowOpacity
end
if not glow:IsShown() then glow:Show() end
end
local TARGET_ARROW_TEXTURES = {
chevron = {
thin = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\target_arrow_chevron_thin.tga",
thick = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\target_arrow_chevron_thick.tga",
},
triangle = {
thin = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\target_arrow_triangle_thin.tga",
thick = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\target_arrow_triangle_thick.tga",
},
slim_triangle = {
thin = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\target_arrow_slim_triangle_thin.tga",
thick = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\target_arrow_slim_triangle_thick.tga",
},
double_chevron = {
thin = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\target_arrow_double_chevron_thin.tga",
thick = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\target_arrow_double_chevron_thick.tga",
},
diamond_tip = {
thin = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\target_arrow_diamond_tip_thin.tga",
thick = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\target_arrow_diamond_tip_thick.tga",
},
}
local function GetTargetArrowTexture(style, thick)
local textures = TARGET_ARROW_TEXTURES[style] or TARGET_ARROW_TEXTURES.chevron
return thick and textures.thick or textures.thin
end
local function GetTargetArrowOffsets(style)
local leftGap, rightGap, fallbackGap = -5, 2, 5
if style == "double_chevron" then
leftGap, rightGap, fallbackGap = -7, 4, 7
elseif style == "diamond_tip" then
leftGap, rightGap, fallbackGap = -6, 3, 6
elseif style == "triangle" or style == "slim_triangle" then
leftGap, rightGap, fallbackGap = -5, 3, 5
end
return leftGap, rightGap, fallbackGap
end
local function GetRightTargetArrowSide(plate)
if not plate then return nil end
if plate.BNPDirectDragonTestFrame and plate.BNPDirectDragonTestFrame.IsShown and
plate.BNPDirectDragonTestFrame:IsShown() then
return plate.BNPDirectDragonTestFrame
elseif plate.levelicon and plate.levelicon.IsShown and plate.levelicon:IsShown() then
return plate.levelicon
elseif plate.level and plate.level.IsShown and plate.level:IsShown() then
return plate.level
end
return nil
end
local function AnchorRightTargetArrow(plate, arrow, style)
if not plate or not arrow then return end
local anchor = plate.healthbar or plate
style = style or (BNP.GetTargetArrowStyle and BNP:GetTargetArrowStyle()) or "chevron"
local _, rightGap, fallbackGap = GetTargetArrowOffsets(style)
local side = GetRightTargetArrowSide(plate)
local anchorKey = side or anchor
-- The level/elite objects move with the nameplate automatically. Re-anchor
-- only when the actual side object or selected style changes.
if arrow.BNPLastAnchorKey == anchorKey and arrow.BNPLastAnchorStyle == style then
return
end
arrow:ClearAllPoints()
if side then
arrow:SetPoint("LEFT", side, "RIGHT", rightGap, -1)
else
arrow:SetPoint("LEFT", anchor, "RIGHT", fallbackGap, -1)
end
arrow.BNPLastAnchorKey = anchorKey
arrow.BNPLastAnchorStyle = style
end
local function EnsureTargetArrows(plate)
if not plate then return nil, nil end
if plate.BNPTargetArrowLeft and plate.BNPTargetArrowRight then
return plate.BNPTargetArrowLeft, plate.BNPTargetArrowRight
end
local anchor = plate.healthbar or plate
if not anchor or not anchor.CreateTexture then return nil, nil end
local left = anchor:CreateTexture(nil, "OVERLAY")
left:SetTexture(GetTargetArrowTexture("chevron", false))
left:SetWidth(14)
left:SetHeight(14)
left:SetTexCoord(0, 1, 0, 1)
left:SetPoint("RIGHT", anchor, "LEFT", -5, 0)
left.BNPLastAnchorStyle = "chevron"
left:Hide()
local right = anchor:CreateTexture(nil, "OVERLAY")
right:SetTexture(GetTargetArrowTexture("chevron", false))
right:SetWidth(14)
right:SetHeight(14)
right:SetTexCoord(1, 0, 0, 1)
AnchorRightTargetArrow(plate, right, "chevron")
right:Hide()
plate.BNPTargetArrowLeft = left
plate.BNPTargetArrowRight = right
return left, right
end
local function ApplyTargetArrows(plate)
if not plate or not plate:IsShown() then return end
local enabled = BNP.AreTargetArrowsEnabled and BNP:AreTargetArrowsEnabled()
local totemIconOnly = plate.BNPTotemLastKey and
BNP.AreTotemIndicatorsEnabled and BNP:AreTotemIndicatorsEnabled()
local isTarget = plate.BNPIsCurrentTarget and true or false
-- As with Target Glow, arrow textures only need to exist on a plate that has
-- actually been the target. This avoids two permanent textures per nameplate.
if not enabled or totemIconOnly or not isTarget then
local oldLeft, oldRight = plate.BNPTargetArrowLeft, plate.BNPTargetArrowRight
if oldLeft and oldLeft.IsShown and oldLeft:IsShown() then oldLeft:Hide() end
if oldRight and oldRight.IsShown and oldRight:IsShown() then oldRight:Hide() end
return
end
local left, right = EnsureTargetArrows(plate)
if not left or not right then return end
local anchor = plate.healthbar or plate
local arrowSize = BNP.GetTargetArrowSize and BNP:GetTargetArrowSize() or 14
local thick = BNP.AreTargetArrowsThick and BNP:AreTargetArrowsThick()
local style = BNP.GetTargetArrowStyle and BNP:GetTargetArrowStyle() or "chevron"
local texture = GetTargetArrowTexture(style, thick)
local leftGap = GetTargetArrowOffsets(style)
if left.BNPLastTexture ~= texture then
left:SetTexture(texture)
right:SetTexture(texture)
left.BNPLastTexture = texture
right.BNPLastTexture = texture
end
if left.BNPLastSize ~= arrowSize then
left:SetWidth(arrowSize)
left:SetHeight(arrowSize)
right:SetWidth(arrowSize)
right:SetHeight(arrowSize)
left.BNPLastSize = arrowSize
right.BNPLastSize = arrowSize
end
-- TexCoords are constant after creation; only anchors depend on style.
if left.BNPLastAnchorStyle ~= style then
left:ClearAllPoints()
left:SetPoint("RIGHT", anchor, "LEFT", leftGap, 0)
left.BNPLastAnchorStyle = style
end
AnchorRightTargetArrow(plate, right, style)
local r, g, b = 1, 1, 1
if BNP.GetTargetArrowColor then
r, g, b = BNP:GetTargetArrowColor()
elseif BNP.GetTargetGlowColor then
r, g, b = BNP:GetTargetGlowColor()
end
if left.BNPLastR ~= r or left.BNPLastG ~= g or left.BNPLastB ~= b then
left:SetVertexColor(r, g, b, 1)
right:SetVertexColor(r, g, b, 1)
left.BNPLastR, left.BNPLastG, left.BNPLastB = r, g, b
right.BNPLastR, right.BNPLastG, right.BNPLastB = r, g, b
end
if not left:IsShown() then left:Show() end
if not right:IsShown() then right:Show() end
end
local function ApplyTargetAlpha(plate)
if not plate or not plate:IsShown() then return end
local targetGUID = GetTargetGUID()
local totemIconOnly = plate.BNPTotemLastKey and
BNP.AreTotemIndicatorsEnabled and BNP:AreTotemIndicatorsEnabled()
local wanted = totemIconOnly and 1 or DesiredAlpha(plate, targetGUID)
if plate:GetAlpha() ~= wanted then
plate:SetAlpha(wanted)
end
-- Publish the same resolved target identity for detached aura containers.
-- They cannot inherit it through their shared WorldFrame parent.
local targetPlate = nil
local targetAvailable = false
local isCurrentTarget = false
if targetGUID then
targetPlate, targetAvailable = GetClassicAPITargetPlate(targetGUID)
if targetAvailable and targetPlate then
isCurrentTarget = plate == targetPlate
if not isCurrentTarget then
local plateGUID = GetVerifiedPlateGUID(plate)
isCurrentTarget = plateGUID and plateGUID == targetGUID
end
else
isCurrentTarget = IsPlateCurrentTarget(plate, targetGUID)
end
end
plate.BNPIsCurrentTarget = isCurrentTarget and true or false
-- ClassicAPI returns the underlying target nameplate frame itself. Force it
-- opaque after every alpha application. Even if SuperWoW momentarily reports
-- the recycled frame's previous valid GUID, both writes happen in the same
-- Lua update and the real target finishes the frame at alpha 1.
if targetGUID then
if targetAvailable and targetPlate and targetPlate.GetAlpha and targetPlate.SetAlpha then
targetPlate.BNPIsCurrentTarget = true
local ok, alpha = pcall(targetPlate.GetAlpha, targetPlate)
if not ok or alpha ~= 1 then
pcall(targetPlate.SetAlpha, targetPlate, 1)
end
end
end
end
local function InstallAlphaGuard(plate)
plate = plate or this
if not plate or plate.BNPFullAlphaGuard then return end
plate.BNPAlphaIdentityHoldUntil = GetTime() + ALPHA_IDENTITY_HOLD_TIME
local oldOnUpdate = plate:GetScript("OnUpdate")
plate:SetScript("OnUpdate", function()
if oldOnUpdate then oldOnUpdate() end
local current = this or plate
-- Apply the optional Y-offset after Blizzard has had a chance to update
-- the projected nameplate for this frame. This uses the OnUpdate BNP
-- already needs for alpha/glow/target-level handling.
if BNP.MaintainNameplateYOffset and BNP_DB and
BNP_DB.nameplateYOffset and BNP_DB.nameplateYOffset > 0 then
BNP:MaintainNameplateYOffset(current)
end
ApplyTargetAlpha(current)
ApplyTargetGlow(current)
ApplyTargetArrows(current)
ApplyForeignTagVisual(current)
ApplyTargetFrameLevel(current)
end)
plate.BNPFullAlphaGuard = true
ApplyTargetAlpha(plate)
end
if BNP.libnameplate then
table.insert(BNP.libnameplate.OnInit, function(plate)
InstallAlphaGuard(plate or this)
end)
table.insert(BNP.libnameplate.OnShow, function(plate)
local current = plate or this
if not current then return end
InvalidateTargetResolution()
current.BNPAlphaIdentityHoldUntil = GetTime() + ALPHA_IDENTITY_HOLD_TIME
if current.GetAlpha and current.SetAlpha and current:GetAlpha() ~= 1 then
current:SetAlpha(1)
end
InstallAlphaGuard(current)
ApplyTargetAlpha(current)
ApplyTargetGlow(current)
ApplyTargetArrows(current)
ApplyForeignTagVisual(current, true)
ApplyTargetFrameLevel(current)
end)
end
local targetEvents = CreateFrame("Frame")
targetEvents:RegisterEvent("PLAYER_TARGET_CHANGED")
targetEvents:SetScript("OnEvent", function()
InvalidateTargetResolution()
local targetGUID = GetTargetGUID()
local _, classicTargetAvailable = GetClassicAPITargetPlate(targetGUID)
local holdUntil = GetTime() + ALPHA_IDENTITY_HOLD_TIME
local plate
for plate in pairs(BNP.plates or {}) do
if classicTargetAvailable then
plate.BNPAlphaIdentityHoldUntil = nil
ApplyTargetAlpha(plate)
else
plate.BNPAlphaIdentityHoldUntil = holdUntil
if plate.GetAlpha and plate.SetAlpha and plate:GetAlpha() ~= 1 then
plate:SetAlpha(1)
end
end
ApplyTargetFrameLevel(plate)
ApplyTargetArrows(plate)
end
if BNP.RefreshAuraPriorityAlpha then BNP:RefreshAuraPriorityAlpha() end
end)
function BNP:RefreshTargetFocus()
InvalidateTargetResolution()
local plate
for plate in pairs(self.plates or {}) do
ApplyTargetAlpha(plate)
ApplyTargetGlow(plate)
ApplyTargetArrows(plate)
ApplyForeignTagVisual(plate)
end
end
function BNP:RefreshNonTargetAlpha()
InvalidateTargetResolution()
local plate
for plate in pairs(self.plates or {}) do
ApplyTargetAlpha(plate)
end
end
+73
View File
@@ -0,0 +1,73 @@
-- SuperWoW GUID probe.
-- This module does not alter aura tracking or rendering. It only inspects the
-- extended UnitExists return value and the unit token exposed by nameplates.
local function CleanText(text)
if not text then return nil end
text = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
text = string.gsub(text, "|r", "")
return text
end
local function SafePlateToken(plate)
if not plate or not plate.GetName then return nil end
-- SuperWoW extends frame:GetName(index). ShaguTweaks already uses
-- plate:GetName(1) for its SuperWoW nameplate castbar implementation.
local ok, token = pcall(function() return plate:GetName(1) end)
if ok then return token end
return nil
end
local function UnitExistsData(unit)
if not unit then return nil, nil end
local ok, exists, guid = pcall(UnitExists, unit)
if not ok then return nil, nil end
return exists, guid
end
function BNP:GetSuperWoWGUID(unit)
local exists, guid = UnitExistsData(unit)
if exists and guid then return guid end
return nil
end
function BNP:GetPlateSuperWoWData(plate)
local token = SafePlateToken(plate)
local exists, guid = UnitExistsData(token)
return token, exists, guid
end
function BNP:PrintGUIDDiagnostic()
BNP:Print("=== SuperWoW GUID Diagnose ===")
local targetName = UnitName("target")
local targetExists, targetGUID = UnitExistsData("target")
BNP:Print("Target: " .. tostring(targetName or "kein Ziel"))
BNP:Print("UnitExists(target): " .. tostring(targetExists) .. " | GUID: " .. tostring(targetGUID))
local visible = 0
local sameName = 0
local guidMatches = 0
local index = 0
for plate in pairs(BNP.plates) do
if plate and plate:IsShown() then
visible = visible + 1
local plateName = plate.name and plate.name.GetText and CleanText(plate.name:GetText()) or nil
if not targetName or plateName == targetName then
sameName = sameName + 1
index = index + 1
local token, exists, guid = BNP:GetPlateSuperWoWData(plate)
if targetGUID and guid and targetGUID == guid then guidMatches = guidMatches + 1 end
BNP:Print("Plate " .. index .. ": " .. tostring(plateName))
BNP:Print(" Token: " .. tostring(token) .. " | Exists: " .. tostring(exists))
BNP:Print(" GUID: " .. tostring(guid) .. " | Target-Match: " .. tostring(targetGUID and guid == targetGUID or false))
end
end
end
BNP:Print("Sichtbar: " .. visible .. " | gleicher Name: " .. sameName .. " | GUID-Matches: " .. guidMatches)
end
+265
View File
@@ -0,0 +1,265 @@
BNP = BNP or {}
local function Enabled()
return BNP.IsHealthTextEnabled and BNP:IsHealthTextEnabled()
end
local function ApplyHealthTextFont(text)
if not text then return end
local size = BNP.GetHealthTextFontSize and BNP:GetHealthTextFontSize() or 9
local outline = BNP.GetHealthTextOutline and BNP:GetHealthTextOutline() or "outline"
local flags = ""
if outline == "outline" then
flags = "OUTLINE"
elseif outline == "thick" then
flags = "THICKOUTLINE"
end
-- Avoid reapplying the font on every health update. Only touch the FontString
-- when the user actually changes size/outline or when the text is first made.
if text.BNPFontSize ~= size or text.BNPFontOutline ~= outline then
text:SetFont("Fonts\\FRIZQT__.TTF", size, flags)
text.BNPFontSize = size
text.BNPFontOutline = outline
end
end
local function CreateHealthText(plate)
if plate.BNPHealthPercent then return plate.BNPHealthPercent end
if not plate or not plate.healthbar then return nil end
-- Parent the text directly to the Blizzard health StatusBar. This keeps it
-- above the bar texture and automatically follows every frame-level change
-- made for Target Glow / permanent target priority. No separate holder can
-- become stale during target switches or nameplate recycling.
local anchor = plate.healthbar
local text = anchor:CreateFontString(nil, "OVERLAY")
ApplyHealthTextFont(text)
text:SetPoint("CENTER", anchor, "CENTER", 3, 0)
text:SetTextColor(1, 1, 1)
text:Hide()
plate.BNPHealthPercent = text
return text
end
local function GetPercentFromBar(plate)
if not plate or not plate.healthbar then return nil end
local minValue, maxValue = plate.healthbar:GetMinMaxValues()
local value = plate.healthbar:GetValue()
if value and minValue and maxValue and maxValue > minValue then
local pct = ((value - minValue) / (maxValue - minValue)) * 100
return math.floor(pct + 0.5)
end
return nil
end
local function GetPercentFromUnit(plate)
if not plate or not plate.GetName then return nil end
local token = plate:GetName(1)
if not token or not UnitExists(token) then return nil end
if not UnitHealth or not UnitHealthMax then return nil end
local health = UnitHealth(token)
local maxHealth = UnitHealthMax(token)
if health and maxHealth and maxHealth > 0 then
return math.floor((health / maxHealth) * 100 + 0.5)
end
return nil
end
local function GetHealthFromUnit(plate)
if not plate or not plate.GetName then return nil, nil end
local token = plate:GetName(1)
if not token or not UnitExists(token) then return nil, nil end
if not UnitHealth or not UnitHealthMax then return nil, nil end
local health = tonumber(UnitHealth(token))
local maxHealth = tonumber(UnitHealthMax(token))
if health and maxHealth and maxHealth > 0 then
return health, maxHealth
end
return nil, nil
end
local function GetHealthFromBar(plate)
if not plate or not plate.healthbar then return nil, nil end
local minValue, maxValue = plate.healthbar:GetMinMaxValues()
local value = plate.healthbar:GetValue()
value = tonumber(value)
minValue = tonumber(minValue)
maxValue = tonumber(maxValue)
if value and minValue and maxValue and maxValue > minValue then
return value - minValue, maxValue - minValue
end
return nil, nil
end
local function FormatHealth(value)
value = tonumber(value)
if not value then return nil end
if value < 0 then value = 0 end
-- Keep boss/player values readable inside the compact Blizzard healthbar.
if value >= 1000000 then
local shown = math.floor((value / 1000000) * 10 + 0.5) / 10
return tostring(shown) .. "m"
elseif value >= 10000 then
return tostring(math.floor(value / 1000 + 0.5)) .. "k"
elseif value >= 1000 then
local shown = math.floor((value / 1000) * 10 + 0.5) / 10
return tostring(shown) .. "k"
end
return tostring(math.floor(value + 0.5))
end
local function UpdatePlate(plate)
if not plate or not plate.healthbar then return end
local text = plate.BNPHealthPercent
if not Enabled() or not plate:IsShown() then
if text then text:Hide() end
return
end
text = text or CreateHealthText(plate)
if not text then return end
ApplyHealthTextFont(text)
local mode = BNP:GetHealthTextMode()
local health, maxHealth = GetHealthFromUnit(plate)
-- Fallback for clients/units where the SuperWoW unit token is temporarily
-- unavailable. This may be normalized health on stock Vanilla.
if health == nil or maxHealth == nil then
health, maxHealth = GetHealthFromBar(plate)
end
local pct = nil
if health and maxHealth and maxHealth > 0 then
pct = math.floor((health / maxHealth) * 100 + 0.5)
else
pct = GetPercentFromBar(plate)
if pct == nil then pct = GetPercentFromUnit(plate) end
end
if pct ~= nil then
if pct < 0 then pct = 0 end
if pct > 100 then pct = 100 end
end
local hpText = FormatHealth(health)
local display = nil
if mode == "percent" then
if pct ~= nil then display = tostring(pct) .. "%" end
elseif mode == "hp" then
display = hpText
elseif mode == "both" then
if hpText and pct ~= nil then
display = hpText .. " | " .. tostring(pct) .. "%"
elseif hpText then
display = hpText
elseif pct ~= nil then
display = tostring(pct) .. "%"
end
end
if not display or display == "" then
text:Hide()
return
end
if text.BNPLastHealthText ~= display then
text:SetText(display)
text.BNPLastHealthText = display
end
text:Show()
end
-- Intentionally simple fallback: only reads values and updates text.
-- No SetScript calls on healthbar, no SetAlpha, no color changes.
local updater = CreateFrame("Frame")
local elapsed = 0
local healthScanBucket = 0
local healthNextBucket = 0
local function GetHealthScanBucket(plate)
if plate.BNPHealthScanBucket == nil then
plate.BNPHealthScanBucket = healthNextBucket
healthNextBucket = 1 - healthNextBucket
end
return plate.BNPHealthScanBucket
end
updater:SetScript("OnUpdate", function()
elapsed = elapsed + arg1
if elapsed < 0.05 then return end
elapsed = 0
-- Health text is off by default. Do not walk every known nameplate when there
-- is literally nothing to update. Option changes refresh immediately.
if not Enabled() then return end
-- Keep the old 0.10s per-plate update rate, but distribute it over two frames.
healthScanBucket = 1 - healthScanBucket
local plate
for plate in pairs(BNP.plates or {}) do
if GetHealthScanBucket(plate) == healthScanBucket then
if plate:IsShown() then
UpdatePlate(plate)
elseif plate.BNPHealthPercent then
plate.BNPHealthPercent:Hide()
end
end
end
end)
function BNP:RefreshHealthPercent()
local plate
for plate in pairs(self.plates or {}) do
if plate and plate:IsShown() then
UpdatePlate(plate)
elseif plate and plate.BNPHealthPercent then
plate.BNPHealthPercent:Hide()
end
end
end
-- Refresh immediately when Blizzard reuses a nameplate instead of waiting for
-- the next 0.10 s fallback pass.
if BNP.libnameplate then
table.insert(BNP.libnameplate.OnShow, function(plate)
local current = plate or this
if current then UpdatePlate(current) end
end)
end
-- Target switches can change nameplate frame levels in the same frame. Because
-- the text now belongs to the healthbar this is only a value/visibility refresh,
-- not a layering workaround.
local healthEvents = CreateFrame("Frame")
healthEvents:RegisterEvent("PLAYER_TARGET_CHANGED")
healthEvents:RegisterEvent("PLAYER_ENTERING_WORLD")
healthEvents:SetScript("OnEvent", function()
if BNP.RefreshHealthPercent then
BNP:RefreshHealthPercent()
end
end)
File diff suppressed because it is too large Load Diff
+189
View File
@@ -0,0 +1,189 @@
BNP = BNP or {}
local function CaptureDefaultNameFontSize(plate)
if BNP.defaultNameFontSize then return end
if not plate or not plate.name or not plate.name.GetFont then return end
local _, size = plate.name:GetFont()
if size then BNP.defaultNameFontSize = size end
end
local function ApplyNameFont(plate)
if not plate or not plate.name or not plate.name.GetFont or not plate.name.SetFont then return end
CaptureDefaultNameFontSize(plate)
-- Nil means the user has never touched the new size control. Keep the
-- exact current Blizzard/skin-addon font size for backwards compatibility.
local configured = BNP_DB and tonumber(BNP_DB.nameFontSize)
if not configured then return end
local font, currentSize, flags = plate.name:GetFont()
if not font then return end
local size = BNP.GetNameFontSize and BNP:GetNameFontSize() or configured
if currentSize ~= size then
-- Keep the font face and flags exactly as they currently are. Only the
-- point size belongs to BNP, so DarkMode/font addons remain compatible.
plate.name:SetFont(font, size, flags)
end
end
function BNP:RefreshNameAppearance()
local plate
for plate in pairs(BNP.plates or {}) do
if plate then
ApplyNameFont(plate)
if BNP.ApplyNameFontYOffset then BNP:ApplyNameFontYOffset(plate) end
end
end
end
-- Independent name visibility controls for Blizzard nameplates.
-- BNP only hides the name FontString; health bars, level text, raid icons,
-- auras, immunities and click behavior remain untouched.
local function ResolvePlateType(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 = UnitExists(token)
if not exists then return nil end
if UnitIsPlayer and UnitIsPlayer(token) then
return "player"
end
return "npc"
end
local function ShouldHide(unitType)
if unitType == "player" then
if BNP.HidePlayerNamesEnabled then return BNP:HidePlayerNamesEnabled() end
return BNP_DB and BNP_DB.hidePlayerNames and true or false
elseif unitType == "npc" then
if BNP.HideNPCNamesEnabled then return BNP:HideNPCNamesEnabled() end
return BNP_DB and BNP_DB.hideNPCNames and true or false
end
return false
end
-- Authoritative query used by aura/immunity layout. Do not rely only on the
-- transient BNPNameHidden marker: on recycled/projected Vanilla nameplates the
-- name FontString and aura module callbacks can update in a different order.
function BNP:IsPlateNameHidden(plate)
if not plate then return false end
-- This helper is queried by the aura renderer at 20 Hz. In the default state
-- (both hide-name options off) return immediately without resolving a unit
-- token for every visible plate.
if not BNP_DB or (not BNP_DB.hidePlayerNames and not BNP_DB.hideNPCNames) then
return false
end
local unitType = ResolvePlateType(plate) or plate.BNPNameUnitType
if unitType then
return ShouldHide(unitType)
end
-- If identity is momentarily unresolved, keep the last BNP-owned state so a
-- recycled plate does not jump vertically for a single scan.
return plate.BNPNameHidden and true or false
end
local function RefreshPlateAuraLayout(plate)
if not plate then return end
if BNP.RefreshAuraLayoutForPlate then BNP:RefreshAuraLayoutForPlate(plate) end
if BNP.RefreshImmunityLayoutForPlate then BNP:RefreshImmunityLayoutForPlate(plate) end
end
local function RestoreIfBNPHid(plate)
if not plate or not plate.name then return end
if plate.BNPNameHidden then
plate.name:Show()
plate.BNPNameHidden = nil
end
end
local function UpdatePlateName(plate, resetIdentity)
if not plate or not plate.name then return end
ApplyNameFont(plate)
if BNP.ApplyNameFontYOffset then BNP:ApplyNameFontYOffset(plate) end
if resetIdentity then
-- Blizzard reuses nameplate frames. Never carry a hidden name from the
-- previous unit into a newly shown plate while its projected token is not
-- resolved yet.
plate.BNPNameUnitType = nil
RestoreIfBNPHid(plate)
end
local unitType = ResolvePlateType(plate)
if unitType then
plate.BNPNameUnitType = unitType
else
unitType = plate.BNPNameUnitType
end
if not unitType then return end
if ShouldHide(unitType) then
if not plate.BNPNameHidden then
plate.name:Hide()
plate.BNPNameHidden = true
elseif plate.name.IsShown and plate.name:IsShown() then
-- The client may re-show Blizzard regions while updating/recycling a
-- plate. Re-enforce only names BNP explicitly owns as hidden.
plate.name:Hide()
end
else
RestoreIfBNPHid(plate)
end
end
function BNP:RefreshNameVisibility()
local plate
for plate in pairs(BNP.plates or {}) do
if plate and plate.IsShown and plate:IsShown() then
UpdatePlateName(plate, false)
-- Re-anchor immediately when the option is toggled instead of waiting
-- for the aura module's next identity/update cycle.
RefreshPlateAuraLayout(plate)
elseif plate then
RestoreIfBNPHid(plate)
plate.BNPNameUnitType = nil
end
end
end
if BNP.libnameplate then
table.insert(BNP.libnameplate.OnInit, function(plate)
local current = plate or this
if not current then return end
UpdatePlateName(current, true)
end)
table.insert(BNP.libnameplate.OnShow, function(plate)
local current = plate or this
if not current then return end
UpdatePlateName(current, true)
end)
-- libnameplate updates are already throttled to 0.10s. This is deliberate:
-- if Blizzard re-shows the name FontString, the configured hidden state is
-- restored without adding a per-frame OnUpdate to every nameplate.
table.insert(BNP.libnameplate.OnUpdate, function(plate)
local current = plate or this
if not current or not current:IsShown() then return end
-- Both hide options are disabled by default. In that common case avoid
-- UnitExists/UnitIsPlayer work for every visible plate on every 0.10s pass.
-- If BNP previously hid this specific name, still run once to restore it.
local anyHideEnabled = BNP_DB and (BNP_DB.hidePlayerNames or BNP_DB.hideNPCNames)
if not anyHideEnabled and not current.BNPNameHidden then return end
UpdatePlateName(current, false)
end)
end
+468
View File
@@ -0,0 +1,468 @@
BNP = BNP or {}
local MIN_SCALE = 0.50
local MAX_SCALE = 1.50
local DEFAULT_SCALE = 1.00
local function Clamp(value)
if value < MIN_SCALE then return MIN_SCALE end
if value > MAX_SCALE then return MAX_SCALE end
return value
end
function BNP:GetNameplateScale()
BNP_DB = BNP_DB or {}
return Clamp(tonumber(BNP_DB.nameplateScale) or DEFAULT_SCALE)
end
local function EffectiveScale()
local uiScale = 1.0
if UIParent and UIParent.GetScale then
uiScale = UIParent:GetScale() or 1.0
end
return uiScale * BNP:GetNameplateScale()
end
local function CaptureAnchorState(object, plate, wrapper)
if not object or not object.GetPoint then return nil end
local state = {
object = object,
points = {},
}
local count = 1
if object.GetNumPoints then
count = object:GetNumPoints() or 1
if count < 1 then count = 1 end
end
local i
for i = 1, count do
local point, relativeTo, relativePoint, x, y = object:GetPoint(i)
if point then
if relativeTo == plate then
relativeTo = wrapper
end
state.points[table.getn(state.points) + 1] = {
point = point,
relativeTo = relativeTo or wrapper,
relativePoint = relativePoint or point,
x = x or 0,
y = y or 0,
}
end
end
return state
end
local function StateYOffset(state)
if state and state.BNPNameAnchor and BNP.GetNameFontYOffset then
return BNP:GetNameFontYOffset()
end
return 0
end
local function AnchorStateMatches(state)
if not state or not state.object or not state.object.GetPoint then return false end
local object = state.object
local expectedCount = table.getn(state.points)
local currentCount = expectedCount
if object.GetNumPoints then
currentCount = object:GetNumPoints() or 0
end
if currentCount ~= expectedCount then
return false
end
local i
for i = 1, expectedCount do
local expected = state.points[i]
local point, relativeTo, relativePoint, x, y = object:GetPoint(i)
if point ~= expected.point
or relativeTo ~= expected.relativeTo
or relativePoint ~= expected.relativePoint
or math.abs((x or 0) - expected.x) > 0.01
or math.abs((y or 0) - (expected.y + StateYOffset(state))) > 0.01 then
return false
end
end
return true
end
local function RestoreAnchorState(state, wrapper)
if not state or not state.object then return end
local object = state.object
if object.GetParent and object.SetParent and object:GetParent() ~= wrapper then
object:SetParent(wrapper)
end
-- Do nothing in the common case. Blizzard normally only rewrites a subset
-- of nameplate anchors while moving, most notably the level FontString.
if AnchorStateMatches(state) then return end
if not object.ClearAllPoints or not object.SetPoint then return end
object:ClearAllPoints()
local i
for i = 1, table.getn(state.points) do
local p = state.points[i]
object:SetPoint(
p.point,
p.relativeTo or wrapper,
p.relativePoint or p.point,
p.x,
p.y + StateYOffset(state)
)
end
end
local function SetHoverGlow(plate, shown)
if not plate or not plate.glow then return end
if shown then
if plate.glow.Show then
plate.glow:Show()
end
else
if plate.glow.Hide then
plate.glow:Hide()
end
end
end
-- SuperWoW exposes the attached unit GUID through nameplate:GetName(1).
-- Its SetMouseoverUnit(unit) API is specifically intended for custom unit
-- frames: set it on OnEnter and clear it on OnLeave. BNP's scale/Y-offset
-- wrapper owns the mouse hit area, so without this bridge the client never
-- receives a real "mouseover" unit even though clicks are forwarded to the
-- original Blizzard nameplate.
local function GetPlateMouseoverUnit(plate)
if not plate or not plate.GetName then return nil end
local ok, unit = pcall(function() return plate:GetName(1) end)
if not ok or not unit then return nil end
if UnitExists then
local existsOK, exists = pcall(UnitExists, unit)
if existsOK and not exists then return nil end
end
return unit
end
local function ShowPlateTooltip(wrapper, unit)
if not wrapper or not unit or not GameTooltip or not GameTooltip.SetUnit then return end
-- SetMouseoverUnit() has already made "mouseover" a valid unit. Prefer
-- that token so tooltip addons see the same unit surface as mouseover macros.
if GameTooltip_SetDefaultAnchor then
GameTooltip_SetDefaultAnchor(GameTooltip, wrapper)
elseif GameTooltip.SetOwner then
GameTooltip:SetOwner(wrapper, "ANCHOR_CURSOR")
end
local ok = pcall(function() GameTooltip:SetUnit("mouseover") end)
if not ok then
pcall(function() GameTooltip:SetUnit(unit) end)
end
if GameTooltip.Show then GameTooltip:Show() end
end
local function SetWrapperMouseover(plate, wrapper)
local unit = GetPlateMouseoverUnit(plate)
if not unit then return end
-- Keep ownership so a late OnLeave from an overlapping/recycled plate cannot
-- clear the mouseover unit that a newer wrapper has just established.
BNP.BNPMouseoverOwner = wrapper
wrapper.BNPMouseoverUnit = unit
if SetMouseoverUnit then
local ok = pcall(SetMouseoverUnit, unit)
if ok then
ShowPlateTooltip(wrapper, unit)
end
end
end
local function ClearWrapperMouseover(wrapper)
if BNP.BNPMouseoverOwner ~= wrapper then return end
BNP.BNPMouseoverOwner = nil
wrapper.BNPMouseoverUnit = nil
if SetMouseoverUnit then
pcall(SetMouseoverUnit)
end
if GameTooltip and GameTooltip.Hide then
GameTooltip:Hide()
end
end
local function SetYOffsetMouseMode(plate, wrapper, offset)
if not plate or not wrapper then return end
-- Always use the visual wrapper as BNP's interactive frame, including at
-- Y Offset 0. Previously offset 0 used Blizzard's native Button while
-- positive offsets used BNPScaleWrapper. That split mouse path caused the
-- original Blizzard mouseover glow to disappear at offset 0.
--
-- Keeping clicks, hit geometry and hover feedback on the same frame also
-- guarantees that the clickable area follows the visible plate at every
-- configured Y offset.
if plate.EnableMouse then
plate:EnableMouse(false)
end
if wrapper.EnableMouse then
wrapper:EnableMouse(true)
end
if wrapper.RegisterForClicks then
-- Preserve the classic Blizzard nameplate mouse behavior:
-- left click selects the unit, right click selects it and starts auto attack.
wrapper:RegisterForClicks("LeftButtonUp", "RightButtonUp")
end
if not wrapper.BNPClickForwardInstalled then
wrapper:SetScript("OnClick", function()
-- The wrapper exists only because BNP visually offsets/scales the original
-- Blizzard plate. Keep Blizzard's native nameplate click semantics by
-- forwarding the ACTUAL mouse button to the original Button.
--
-- parent:Click() without an argument always simulates LeftButton. That was
-- the reason right click turned into a plain target-select while BNP was
-- enabled. Do not call AttackTarget() manually here: it toggles auto attack
-- and is not equivalent to Blizzard's native nameplate right-click path.
local mouseButton = arg1 or "LeftButton"
local current = this or wrapper
local parent = current and current.plate or plate
if parent and parent:IsShown() and parent.Click then
parent:Click(mouseButton)
end
end)
wrapper.BNPClickForwardInstalled = true
end
if not wrapper.BNPMouseoverFeedbackInstalled then
wrapper:SetScript("OnEnter", function()
local current = this or wrapper
local parent = current and current.plate or plate
SetHoverGlow(parent, true)
SetWrapperMouseover(parent, current)
end)
wrapper:SetScript("OnLeave", function()
local current = this or wrapper
local parent = current and current.plate or plate
SetHoverGlow(parent, false)
ClearWrapperMouseover(current)
end)
-- A projected nameplate can disappear/recycle while the cursor is still
-- inside its old bounds. Clear SuperWoW's synthetic mouseover in that case
-- as well so "mouseover" can never remain stuck on a vanished unit.
wrapper:SetScript("OnHide", function()
local current = this or wrapper
ClearWrapperMouseover(current)
end)
wrapper.BNPMouseoverFeedbackInstalled = true
end
end
local function ApplyWrapperOffset(plate, wrapper)
if not plate or not wrapper then return end
local offset = 0
if BNP.GetNameplateYOffset then
offset = BNP:GetNameplateYOffset()
end
-- Move only the child visual container. The WorldFrame-projected Blizzard
-- nameplate itself keeps its original anchor and can continue to be
-- repositioned by the client every frame.
wrapper:ClearAllPoints()
wrapper:SetPoint("TOPLEFT", plate, "TOPLEFT", 0, offset)
wrapper:SetPoint("BOTTOMRIGHT", plate, "BOTTOMRIGHT", 0, offset)
SetYOffsetMouseMode(plate, wrapper, offset)
end
local function EnsureScaleWrapper(plate)
if not plate or plate.BNPScaleWrapper then return end
local originalWidth = plate:GetWidth()
local originalHeight = plate:GetHeight()
local wrapper = CreateFrame("Button", nil, plate)
wrapper:SetAllPoints(plate)
wrapper.plate = plate
wrapper.originalWidth = originalWidth
wrapper.originalHeight = originalHeight
wrapper.BNPAnchorStates = {}
wrapper:EnableMouse(false)
plate.BNPScaleWrapper = wrapper
-- Capture the ORIGINAL Blizzard anchors before changing parentage.
-- Those stored anchors become BNP's authoritative visual layout whenever
-- Y-offset is enabled.
if plate.healthbar then
local state = CaptureAnchorState(plate.healthbar, plate, wrapper)
if state then
table.insert(wrapper.BNPAnchorStates, state)
end
plate.healthbar:SetParent(wrapper)
RestoreAnchorState(state, wrapper)
plate.healthbar:SetFrameLevel(1)
end
local regions = { plate:GetRegions() }
local i, object
for i, object in pairs(regions) do
if object and object.SetParent then
local state = CaptureAnchorState(object, plate, wrapper)
if state then
if object == plate.name then
state.BNPNameAnchor = true
end
table.insert(wrapper.BNPAnchorStates, state)
end
object:SetParent(wrapper)
RestoreAnchorState(state, wrapper)
end
end
ApplyWrapperOffset(plate, wrapper)
end
function BNP:ApplyNameplateScale(plate)
if not plate then return end
EnsureScaleWrapper(plate)
local wrapper = plate.BNPScaleWrapper
if not wrapper then return end
local scale = EffectiveScale()
wrapper:SetScale(scale)
ApplyWrapperOffset(plate, wrapper)
-- ShaguTweaks also adjusts the original frame bounds so Blizzard's world
-- positioning remains correct while the child visuals honor UI scale.
plate:SetWidth(wrapper.originalWidth * scale)
plate:SetHeight(wrapper.originalHeight * scale)
end
function BNP:ApplyNameFontYOffset(plate)
if not plate then return end
EnsureScaleWrapper(plate)
local wrapper = plate.BNPScaleWrapper
if not wrapper then return end
local states = wrapper.BNPAnchorStates or {}
local i
for i = 1, table.getn(states) do
local state = states[i]
if state and state.BNPNameAnchor then
RestoreAnchorState(state, wrapper)
end
end
end
function BNP:ApplyNameFontYOffsetAll()
local plate
for plate in pairs(self.plates or {}) do
if plate then self:ApplyNameFontYOffset(plate) end
end
end
function BNP:ApplyNameplateScaleAll()
local plate
for plate in pairs(self.plates) do
self:ApplyNameplateScale(plate)
end
end
function BNP:ApplyNameplateYOffsetAll()
local plate
for plate in pairs(self.plates or {}) do
if plate then
self:ApplyNameplateScale(plate)
end
end
end
function BNP:MaintainNameplateYOffset(plate)
if not plate or not plate:IsShown() then return end
if not self.GetNameplateYOffset or self:GetNameplateYOffset() <= 0 then return end
EnsureScaleWrapper(plate)
local wrapper = plate.BNPScaleWrapper
if not wrapper then return end
-- Never rediscover regions through plate:GetRegions() here. Once BNP moves
-- Blizzard regions into the wrapper they are no longer guaranteed to be
-- returned by plate:GetRegions(). Keep and maintain the original region
-- objects captured during wrapper creation instead.
local states = wrapper.BNPAnchorStates or {}
local i
for i = 1, table.getn(states) do
RestoreAnchorState(states[i], wrapper)
end
ApplyWrapperOffset(plate, wrapper)
end
function BNP:SetNameplateScale(value)
value = tonumber(value)
if not value then
self:Print("Usage: /bnp scale 0.5-1.5")
return
end
value = Clamp(value)
BNP_DB = BNP_DB or {}
BNP_DB.nameplateScale = value
self:ApplyNameplateScaleAll()
self:Print("Nameplate scale multiplier set to " .. tostring(value) .. ".")
end
function BNP:ResetNameplateScale()
BNP_DB = BNP_DB or {}
BNP_DB.nameplateScale = DEFAULT_SCALE
self:ApplyNameplateScaleAll()
self:Print("Nameplate scale multiplier reset to 1.0 (Shagu/UI scale behavior).")
end
if BNP.libnameplate then
table.insert(BNP.libnameplate.OnInit, function(plate)
local current = plate or this
if not current then return end
BNP:ApplyNameplateScale(current)
end)
table.insert(BNP.libnameplate.OnShow, function(plate)
local current = plate or this
if not current then return end
BNP:ApplyNameplateScale(current)
end)
end
+75
View File
@@ -0,0 +1,75 @@
BNP = BNP or {}
-- ShaguTweaks Darkened UI compatibility -------------------------------------
-- ShaguTweaks darkens a nameplate once and marks it with plate.darkened=true.
-- Blizzard can later recycle that same plate and restore its original gold
-- textures while the flag remains true. Shagu then skips the recycled plate.
--
-- BNP does not implement its own dark mode here. It simply asks ShaguTweaks
-- to run its own DarkenFrame() again whenever Blizzard reuses a nameplate.
local pending = {}
local function ShaguDarkModeActive()
return ShaguTweaks
and ShaguTweaks.DarkMode
and ShaguTweaks.DarkenFrame
end
local function ReapplyShaguDarkMode(plate)
if not plate or not plate:IsShown() then return end
if not ShaguDarkModeActive() then return end
-- Clear Shagu's one-time marker because this Blizzard plate may have been
-- recycled for another unit and had its textures reset.
plate.darkened = nil
-- Use ShaguTweaks' own implementation/configuration as the single source of
-- truth. Its blacklist already protects StatusBars, spell icons, portraits,
-- etc. from being incorrectly darkened.
ShaguTweaks.DarkenFrame(plate)
plate.darkened = true
-- Dark Nameplate Border is an explicit BNP option and therefore owns this
-- one texture. Re-apply its selected state after Shagu's immediate and
-- delayed passes, otherwise an old dark tint can remain stuck after the
-- checkbox is switched off.
if BNP.RepairNativeNameplateVisuals then
BNP:RepairNativeNameplateVisuals(plate)
end
end
local function QueueReapply(plate)
if not plate then return end
-- Immediate pass handles normal OnShow.
ReapplyShaguDarkMode(plate)
-- Blizzard may restore some nameplate texture state immediately after
-- OnShow. Do one delayed pass and then stop; no permanent scanner is needed.
pending[plate] = GetTime() + 0.08
end
if BNP.libnameplate then
table.insert(BNP.libnameplate.OnInit, function(plate)
QueueReapply(plate or this)
end)
table.insert(BNP.libnameplate.OnShow, function(plate)
QueueReapply(plate or this)
end)
end
local updater = CreateFrame("Frame")
updater:SetScript("OnUpdate", function()
if not next(pending) then return end
local now = GetTime()
local plate, due
for plate, due in pairs(pending) do
if now >= due then
pending[plate] = nil
ReapplyShaguDarkMode(plate)
end
end
end)
+227
View File
@@ -0,0 +1,227 @@
BNP = BNP or {}
local wanted = {
corruption = { "Corruption", "Verderbnis" },
agony = { "Curse of Agony", "Fluch der Pein" },
siphon = { "Siphon Life", "Lebensentzug" },
shadow_word_pain = { "Shadow Word: Pain", "Schattenwort: Schmerz" },
}
local function isWanted(name, list)
if not name then return false end
local lower = string.lower(name)
local i
for i = 1, table.getn(list) do
if lower == string.lower(list[i]) then return true end
end
return false
end
local function normalizeNumber(value)
if not value then return nil end
value = string.gsub(value, ",", ".")
return tonumber(value)
end
-- WoW 1.12 uses Lua 5.0. Use string.find captures instead of string.match.
local function durationFromLine(text)
if not text then return nil end
local lower = string.lower(text)
local _, _, num
-- German: 14,95 Sek. / 14.95 Sekunden
_, _, num = string.find(lower, "([0-9]+[%,%.]?[0-9]*)%s*sek")
if num then return normalizeNumber(num) end
-- English: 14.95 sec / seconds
_, _, num = string.find(lower, "([0-9]+[%,%.]?[0-9]*)%s*sec")
if num then return normalizeNumber(num) end
-- Generic short form: "over 14.95 s" / "14,95 s"
_, _, num = string.find(lower, "over%s+([0-9]+[%,%.]?[0-9]*)%s+s")
if num then return normalizeNumber(num) end
return nil
end
local function getTooltip()
local tooltip = getglobal("BNPSpellProbeTooltip")
if tooltip then return tooltip end
tooltip = CreateFrame("GameTooltip", "BNPSpellProbeTooltip", UIParent, "GameTooltipTemplate")
tooltip:SetOwner(WorldFrame, "ANCHOR_NONE")
return tooltip
end
local function scanSpell(slot)
local tooltip = getTooltip()
tooltip:ClearLines()
tooltip:SetOwner(WorldFrame, "ANCHOR_NONE")
tooltip:SetSpell(slot, BOOKTYPE_SPELL)
local lines = {}
local bestDuration = nil
local i
for i = 1, tooltip:NumLines() do
local fs = getglobal("BNPSpellProbeTooltipTextLeft" .. i)
local text = fs and fs:GetText()
if text and text ~= "" then
table.insert(lines, text)
local parsed = durationFromLine(text)
if parsed and parsed > 0 then
bestDuration = parsed
end
end
end
return bestDuration, lines
end
local function findSpell(wantedNames)
local slot = 1
local found = nil
while true do
local name, rank = GetSpellName(slot, BOOKTYPE_SPELL)
if not name then break end
if isWanted(name, wantedNames) then
found = { slot = slot, name = name, rank = rank }
-- keep scanning so the highest learned rank wins
end
slot = slot + 1
end
return found
end
local function findSpellByID(def, spellID)
if not def or not spellID or not SpellInfo then return nil end
local wantedName, wantedRank = SpellInfo(spellID)
if not wantedName then return nil end
local slot = 1
local name, rank
while true do
name, rank = GetSpellName(slot, BOOKTYPE_SPELL)
if not name then break end
if isWanted(name, def.names or { wantedName }) then
-- SuperWoW SpellInfo exposes the localized rank when available. Matching
-- both name and rank lets us scan the exact spellbook rank that was cast
-- instead of always inheriting the highest learned rank's tooltip.
if wantedRank and wantedRank ~= "" and rank == wantedRank then
return { slot = slot, name = name, rank = rank }
end
end
slot = slot + 1
end
return nil
end
BNP.spellbookDurations = BNP.spellbookDurations or {}
BNP.spellbookDurationsByID = BNP.spellbookDurationsByID or {}
function BNP:RefreshSpellbookDurations()
-- Exact-rank tooltip values can change after talent/spellbook updates.
BNP.spellbookDurationsByID = {}
local id
for id, names in pairs(wanted) do
local spell = findSpell(names)
if spell then
local ok, duration = pcall(scanSpell, spell.slot)
if ok and duration and duration > 0 then
BNP.spellbookDurations[id] = duration
BNP.spellbookDurations[string.lower(spell.name)] = duration
end
end
end
end
function BNP:GetSpellbookDurationForSpellID(def, spellID)
if not def or not spellID then return nil end
BNP.spellbookDurationsByID = BNP.spellbookDurationsByID or {}
if BNP.spellbookDurationsByID[spellID] then
return BNP.spellbookDurationsByID[spellID]
end
local spell = findSpellByID(def, spellID)
if spell then
local ok, duration = pcall(scanSpell, spell.slot)
if ok and duration and duration > 0 then
BNP.spellbookDurationsByID[spellID] = duration
return duration
end
end
return nil
end
function BNP:GetSpellbookDurationForDef(def)
if not def then return nil end
BNP.spellbookDurations = BNP.spellbookDurations or {}
if def.key and BNP.spellbookDurations[def.key] then
return BNP.spellbookDurations[def.key]
end
if def.names then
local i
for i = 1, table.getn(def.names) do
local key = string.lower(def.names[i])
if BNP.spellbookDurations[key] then
return BNP.spellbookDurations[key]
end
end
-- Generic on-demand scan: works for every class, not only the original
-- Warlock/Priest probe list.
local spell = findSpell(def.names)
if spell then
local ok, duration = pcall(scanSpell, spell.slot)
if ok and duration and duration > 0 then
if def.key then BNP.spellbookDurations[def.key] = duration end
BNP.spellbookDurations[string.lower(spell.name)] = duration
return duration
end
end
end
return nil
end
function BNP:SpellbookProbe(key)
self:RefreshSpellbookDurations()
local keys = {}
if key and key ~= "" and wanted[key] then
table.insert(keys, key)
else
table.insert(keys, "corruption")
table.insert(keys, "agony")
table.insert(keys, "siphon")
table.insert(keys, "shadow_word_pain")
end
self:Print("=== Spellbook Tooltip Probe ===")
local k
for k = 1, table.getn(keys) do
local id = keys[k]
local spell = findSpell(wanted[id])
if not spell then
self:Print(id .. ": not found in spellbook")
else
local ok, duration, lines = pcall(scanSpell, spell.slot)
if not ok then
self:Print(spell.name .. ": tooltip scan ERROR: " .. tostring(duration))
else
self:Print(spell.name .. " " .. tostring(spell.rank or "") .. " | Slot " .. spell.slot .. " | Duration: " .. tostring(duration or "NOT DETECTED"))
local i
for i = 1, table.getn(lines) do
self:Print(" " .. i .. ": " .. lines[i])
end
end
end
end
end
+198
View File
@@ -0,0 +1,198 @@
BNP = BNP or {}
-- Tank Mode for SuperWoW GUID nameplates.
-- Default: GREEN = hostile NPC currently targets the player, RED = another unit.
-- Optional Invert Tank Colors reverses those two states.
-- Idle hostile NPCs keep their normal Blizzard color.
local GREEN_R, GREEN_G, GREEN_B = 0.00, 1.00, 0.00
local RED_R, RED_G, RED_B = 1.00, 0.00, 0.00
local playerGUID = nil
local function GetPlayerGUID()
local exists, guid = UnitExists("player")
if exists and guid then playerGUID = guid end
return playerGUID
end
local function GetPlateGUID(plate)
if not plate or not plate.GetName then return nil end
local guid = plate:GetName(1)
if guid and guid ~= "" then return guid end
return nil
end
local function GetTargetGUID(guid)
if not guid then return nil end
local exists, targetGUID = UnitExists(guid .. "target")
if exists then return targetGUID end
return nil
end
local function GetBar(plate)
return plate and (plate.healthbar or plate.healthBar) or nil
end
local function IsHostileNPC(guid)
if not guid or not UnitExists(guid) then return false end
if UnitIsPlayer(guid) then return false end
return UnitCanAttack("player", guid) and true or false
end
local function IsForeignTaggedGUID(guid)
if not guid or not UnitExists(guid) then return false end
if UnitCanAttack and not UnitCanAttack("player", guid) then return false end
if not UnitIsTapped or not UnitIsTapped(guid) then return false end
if UnitIsTappedByPlayer and UnitIsTappedByPlayer(guid) then return false end
return true
end
local function RestoreNormalColor(plate, bar)
-- Do not restore a cached RGB value. Outside an active Tank Mode state,
-- Blizzard owns the NPC healthbar color completely.
plate.BNPTankActive = nil
plate.BNPTankR = nil
plate.BNPTankG = nil
plate.BNPTankB = nil
end
local function ClearTankState(plate)
if not plate then return end
plate.BNPTankActive = nil
plate.BNPTankR = nil
plate.BNPTankG = nil
plate.BNPTankB = nil
end
local function ApplyTankColor(plate, bar, r, g, b)
bar:SetStatusBarColor(r, g, b, 1)
plate.BNPTankActive = true
plate.BNPTankR = r
plate.BNPTankG = g
plate.BNPTankB = b
end
local function EnforceTankColor(plate)
if not plate or not plate.BNPTankActive then return end
local bar = GetBar(plate)
if not bar or not plate.BNPTankR then return end
local r, g, b = bar:GetStatusBarColor()
if math.abs(r - plate.BNPTankR) > 0.01
or math.abs(g - plate.BNPTankG) > 0.01
or math.abs(b - plate.BNPTankB) > 0.01 then
bar:SetStatusBarColor(plate.BNPTankR, plate.BNPTankG, plate.BNPTankB, 1)
end
end
function BNP:UpdateTankModePlate(plate)
if not plate or not plate:IsShown() then return end
local bar = GetBar(plate)
if not bar then return end
if not self:IsTankModeEnabled() then
RestoreNormalColor(plate, bar)
return
end
local guid = GetPlateGUID(plate)
if not IsHostileNPC(guid) then
-- NPC reaction colors belong to Blizzard. Never restore a stale cached
-- Tank Mode color onto neutral/friendly/recycled nameplates.
ClearTankState(plate)
return
end
-- Foreign-tag grey has priority over Tank Mode. Previously Tank Mode wrote
-- RED every 0.10s while fullalpha.lua restored GREY every frame, producing
-- the visible grey/red flicker. Clear any cached tank color and let the
-- foreign-tag renderer own the bar until the mob is ours/untapped again.
if IsForeignTaggedGUID(guid) then
ClearTankState(plate)
return
end
local myGUID = GetPlayerGUID()
if not myGUID then return end
local targetGUID = GetTargetGUID(guid)
local inverted = self:AreTankModeColorsInverted()
if targetGUID and targetGUID == myGUID then
if inverted then
ApplyTankColor(plate, bar, RED_R, RED_G, RED_B)
else
ApplyTankColor(plate, bar, GREEN_R, GREEN_G, GREEN_B)
end
elseif targetGUID then
if inverted then
ApplyTankColor(plate, bar, GREEN_R, GREEN_G, GREEN_B)
else
ApplyTankColor(plate, bar, RED_R, RED_G, RED_B)
end
else
RestoreNormalColor(plate, bar)
end
end
function BNP:RefreshTankMode()
local plate
for plate in pairs(self.plates or {}) do
if plate and plate:IsShown() then
self:UpdateTankModePlate(plate)
end
end
end
function BNP:UpdateTankMode()
self:RefreshTankMode()
end
function BNP:InstallTankMode()
if self.BNPTankModeInstalled then return end
if not self.libnameplate or not self.libnameplate.OnUpdate then return end
GetPlayerGUID()
table.insert(self.libnameplate.OnInit, function(plate)
local current = plate or this
if not current then return end
ClearTankState(current)
local bar = GetBar(current)
if bar and not current.BNPTankColorHooked then
local old = bar:GetScript("OnValueChanged")
bar:SetScript("OnValueChanged", function()
if old then old() end
EnforceTankColor(current)
end)
current.BNPTankColorHooked = true
end
end)
table.insert(self.libnameplate.OnShow, function(plate)
local current = plate or this
if not current then return end
ClearTankState(current)
end)
table.insert(self.libnameplate.OnUpdate, function(plate, elapsed)
local current = plate or this
if not current then return end
-- Tank Mode is optional. Avoid all GUID/target/tap queries while it is off.
-- Only clean an old BNP-owned state if one is still present.
if not BNP:IsTankModeEnabled() then
if current.BNPTankActive then ClearTankState(current) end
return
end
BNP:UpdateTankModePlate(current)
EnforceTankColor(current)
end)
self.BNPTankModeInstalled = true
end
+417
View File
@@ -0,0 +1,417 @@
if not BNP.libnameplate then return end
-- Lightweight PvP totem indicators ------------------------------------------
-- Totem detection is intentionally nameplate/name based. It never reads or
-- modifies the aura cache, combat log timers or CC tracking.
local TOTEM_DEFS = {
{ key = "earthbind", spellID = 2484, names = { "Earthbind Totem", "Totem der Erdbindung" }, texture = "Interface\\Icons\\Spell_Nature_StrengthOfEarthTotem02" },
{ key = "stoneskin", spellID = 8071, names = { "Stoneskin Totem", "Totem der Steinhaut" }, texture = "Interface\\Icons\\Spell_Nature_StoneSkinTotem" },
{ key = "strengthofearth", spellID = 8075, names = { "Strength of Earth Totem", "Totem der Erdstärke" }, texture = "Interface\\Icons\\Spell_Nature_EarthBindTotem" },
{ key = "tremor", spellID = 8143, names = { "Tremor Totem", "Totem des Erdstosses", "Totem des Erdstoßes" }, texture = "Interface\\Icons\\Spell_Nature_TremorTotem" },
{ key = "grounding", spellID = 8177, names = { "Grounding Totem", "Totem der Erdung" }, texture = "Interface\\Icons\\Spell_Nature_GroundingTotem" },
{ key = "stoneclaw", spellID = 5730, names = { "Stoneclaw Totem", "Totem der Steinklaue" }, texture = "Interface\\Icons\\Spell_Nature_StoneClawTotem" },
{ key = "searing", spellID = 3599, names = { "Searing Totem", "Totem der Verbrennung" }, texture = "Interface\\Icons\\Spell_Fire_SearingTotem" },
{ key = "flametongue", spellID = 8227, names = { "Flametongue Totem", "Totem der Flammenzunge" }, texture = "Interface\\Icons\\Spell_Nature_GuardianWard" },
{ key = "magma", spellID = 8190, names = { "Magma Totem", "Totem des glühenden Magmas" }, texture = "Interface\\Icons\\Spell_Fire_SelfDestruct" },
{ key = "firenova", spellID = 1535, names = { "Fire Nova Totem", "Totem der Feuernova" }, texture = "Interface\\Icons\\Spell_Fire_SealOfFire" },
{ key = "frostres", spellID = 8181, names = { "Frost Resistance Totem", "Totem des Frostwiderstands" }, texture = "Interface\\Icons\\Spell_FrostResistanceTotem_01" },
{ key = "fireres", spellID = 8184, names = { "Fire Resistance Totem", "Totem des Feuerwiderstands" }, texture = "Interface\\Icons\\Spell_FireResistanceTotem_01" },
{ key = "healingstream", spellID = 5394, names = { "Healing Stream Totem", "Totem des heilenden Flusses" }, texture = "Interface\\Icons\\INV_Spear_04" },
{ key = "manaspring", spellID = 5675, names = { "Mana Spring Totem", "Totem der Manaquelle" }, texture = "Interface\\Icons\\Spell_Nature_ManaRegenTotem" },
{ key = "poisoncleansing", spellID = 8166, names = { "Poison Cleansing Totem", "Totem der Giftreinigung" }, texture = "Interface\\Icons\\Spell_Nature_PoisonCleansingTotem" },
{ key = "diseasecleansing", spellID = 8170, names = { "Disease Cleansing Totem", "Totem der Krankheitsreinigung" }, texture = "Interface\\Icons\\Spell_Nature_DiseaseCleansingTotem" },
{ key = "manatide", spellID = 16190, names = { "Mana Tide Totem", "Totem der Manaflut" }, texture = "Interface\\Icons\\Spell_Frost_SummonWaterElemental" },
{ key = "natureres", spellID = 10595, names = { "Nature Resistance Totem", "Totem des Naturwiderstands" }, texture = "Interface\\Icons\\Spell_Nature_NatureResistanceTotem" },
{ key = "windfury", spellID = 8512, names = { "Windfury Totem", "Totem des Windzorns" }, texture = "Interface\\Icons\\Spell_Nature_Windfury" },
{ key = "sentry", spellID = 6495, names = { "Sentry Totem", "Totem des Wachens" }, texture = "Interface\\Icons\\Spell_Nature_RemoveCurse" },
{ key = "graceofair", spellID = 8835, names = { "Grace of Air Totem", "Totem der Luftanmut" }, texture = "Interface\\Icons\\Spell_Nature_InvisibilityTotem" },
{ key = "tranquilair", spellID = 25908, names = { "Tranquil Air Totem", "Totem der beruhigenden Winde" }, texture = "Interface\\Icons\\Spell_Nature_Brilliance" },
{ key = "windwall", spellID = 15107, names = { "Windwall Totem", "Totem der Windmauer" }, texture = "Interface\\Icons\\Spell_Nature_EarthBind" },
}
local function CleanName(text)
if not text then return nil end
text = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
text = string.gsub(text, "|r", "")
text = string.gsub(text, "^%s+", "")
text = string.gsub(text, "%s+$", "")
if text == "" then return nil end
return text
end
local function NormalizeTotemName(text)
text = CleanName(text)
if not text then return nil end
text = string.lower(text)
-- Vanilla totem NPCs often carry their spell rank in the NPC name, e.g.
-- "Searing Totem VI" or "Windfury Totem II". Match the base totem name.
text = string.gsub(text, "%s+rank%s+%d+$", "")
text = string.gsub(text, "%s+%d+$", "")
text = string.gsub(text, "%s+[ivxlcdm]+$", "")
text = string.gsub(text, "%s+$", "")
return text
end
local function AddName(def, name)
if not name or name == "" then return end
local normalized = NormalizeTotemName(name)
local i
for i = 1, table.getn(def.names) do
if NormalizeTotemName(def.names[i]) == normalized then return end
end
table.insert(def.names, name)
end
local definitionsResolved = false
local function ResolveDefinitions()
if definitionsResolved then return end
definitionsResolved = true
if not SpellInfo then return end
local i
for i = 1, table.getn(TOTEM_DEFS) do
local def = TOTEM_DEFS[i]
local ok, name, _, texture = pcall(SpellInfo, def.spellID)
if ok then
AddName(def, name)
if texture and texture ~= "" then def.texture = texture end
end
end
end
local GENERIC_TOTEM_DEF = {
key = "generic",
names = {},
texture = "Interface\\Icons\\Spell_Nature_StoneClawTotem",
}
local function FindTotemDefinition(name)
local normalized = NormalizeTotemName(name)
if not normalized then return nil end
local i, n
for i = 1, table.getn(TOTEM_DEFS) do
local def = TOTEM_DEFS[i]
for n = 1, table.getn(def.names) do
if normalized == NormalizeTotemName(def.names[n]) then return def end
end
end
-- Future/custom server totems still get icon-only treatment even when BNP
-- does not yet know their exact spell icon. Only accept "totem" as a
-- complete word though: player names such as "Totemtiger" must never be
-- mistaken for a totem just because they contain those five letters.
if normalized == "totem"
or string.sub(normalized, 1, 6) == "totem "
or string.sub(normalized, -6) == " totem"
or string.find(normalized, " totem ", 1, true) then
return GENERIC_TOTEM_DEF
end
return nil
end
local function GetPlateName(plate)
if plate and plate.name and plate.name.GetText then
local name = CleanName(plate.name:GetText())
if name then return name end
end
if plate and plate.GetName and UnitName then
local token = plate:GetName(1)
if token then
local ok, name = pcall(UnitName, token)
if ok then return CleanName(name) end
end
end
return nil
end
local function PositionIndicator(plate, indicator)
if not plate or not indicator then return end
indicator:ClearAllPoints()
-- Icon-only mode: replace the visible nameplate at its original position.
if plate.healthbar then
indicator:SetPoint("CENTER", plate.healthbar, "CENTER", 0, 0)
else
indicator:SetPoint("CENTER", plate, "CENTER", 0, 0)
end
end
local function RememberAndHideVisual(object, state)
if not object or object == state.indicator then return end
if not object.SetAlpha or not object.GetAlpha then return end
if state.alpha[object] == nil then
state.alpha[object] = object:GetAlpha()
end
object:SetAlpha(0)
end
local TOTEM_VISUAL_RESCAN_INTERVAL = 0.50
local function CaptureTotemPlateVisuals(plate, state, now)
if not plate or not state then return end
-- Capturing every projected region/child allocates temporary Lua tables.
-- Doing that every rendered frame produced needless GC pressure in BGs with
-- several totems. Re-scan only occasionally so late-created BNP children
-- (castbar/combo/etc.) are still discovered without per-frame allocations.
local regions = { plate:GetRegions() }
local i
for i = 1, table.getn(regions) do
RememberAndHideVisual(regions[i], state)
end
local children = { plate:GetChildren() }
for i = 1, table.getn(children) do
RememberAndHideVisual(children[i], state)
end
state.nextCapture = (now or GetTime()) + TOTEM_VISUAL_RESCAN_INTERVAL
end
local function SuppressTotemPlateVisuals(plate, forceCapture)
if not plate then return end
local indicator = plate.BNPTotemIndicator
local state = plate.BNPTotemVisualState
local now = GetTime()
if not state then
state = { alpha = {}, indicator = indicator, nextCapture = 0 }
plate.BNPTotemVisualState = state
else
state.indicator = indicator
end
-- Important PvP totems should remain clearly visible even when BNP's
-- non-target alpha option would normally fade the underlying nameplate.
if plate.SetAlpha and plate.GetAlpha and plate:GetAlpha() ~= 1 then plate:SetAlpha(1) end
if forceCapture or not state.nextCapture or now >= state.nextCapture then
CaptureTotemPlateVisuals(plate, state, now)
end
-- Blizzard can restore alpha on its native regions every frame. Re-enforce
-- only the already-cached objects here; this loop allocates nothing.
local object
for object in pairs(state.alpha or {}) do
if object ~= indicator and object.GetAlpha and object.SetAlpha and object:GetAlpha() ~= 0 then
object:SetAlpha(0)
end
end
-- BNPTotemIndicator is itself a direct child of the plate.
if indicator and indicator.SetAlpha and indicator.GetAlpha and indicator:GetAlpha() ~= 1 then
indicator:SetAlpha(1)
end
end
local function RestoreTotemPlateVisuals(plate)
if not plate then return end
local state = plate.BNPTotemVisualState
if not state then return end
local object, alpha
for object, alpha in pairs(state.alpha or {}) do
if object and object.SetAlpha then
object:SetAlpha(alpha or 1)
end
end
plate.BNPTotemVisualState = nil
end
-- Blizzard can restore parts of a projected nameplate every frame. Once a
-- plate has been identified as a totem, suppress every normal visual again
-- after the plate's native/fullalpha OnUpdate has run. The replacement icon
-- remains a child of the plate and is explicitly excluded, so it keeps the
-- same proven projection/positioning path as the working detection version.
local function InstallTotemVisualGuard(plate)
if not plate or plate.BNPTotemVisualGuard then return end
local oldOnUpdate = plate:GetScript("OnUpdate")
plate:SetScript("OnUpdate", function()
if oldOnUpdate then oldOnUpdate() end
local current = this or plate
if not current then return end
if current.BNPTotemLastKey and
BNP.AreTotemIndicatorsEnabled and BNP:AreTotemIndicatorsEnabled() then
SuppressTotemPlateVisuals(current)
if current.BNPTotemIndicator then
if current.BNPTotemIndicator.GetAlpha and current.BNPTotemIndicator:GetAlpha() ~= 1 then
current.BNPTotemIndicator:SetAlpha(1)
end
if not current.BNPTotemIndicator:IsShown() then current.BNPTotemIndicator:Show() end
end
elseif current.BNPTotemVisualState then
RestoreTotemPlateVisuals(current)
end
end)
plate.BNPTotemVisualGuard = true
end
local function InstallTotemHideGuard(plate)
if not plate or plate.BNPTotemHideGuard then return end
local oldOnHide = plate:GetScript("OnHide")
plate:SetScript("OnHide", function()
if oldOnHide then oldOnHide() end
local current = this or plate
if not current then return end
if current.BNPTotemIndicator then current.BNPTotemIndicator:Hide() end
RestoreTotemPlateVisuals(current)
current.BNPTotemLastName = nil
current.BNPTotemLastKey = nil
end)
plate.BNPTotemHideGuard = true
end
local function ApplyIndicatorLayout(plate, force)
local indicator = plate and plate.BNPTotemIndicator
if not indicator then return end
local size = BNP.GetTotemIconSize and BNP:GetTotemIconSize() or 24
local level = plate.GetFrameLevel and ((plate:GetFrameLevel() or 0) + 2) or 2
if not force and
indicator.BNPLastSize == size and
indicator.BNPLastFrameLevel == level then
return
end
indicator.BNPLastSize = size
indicator.BNPLastFrameLevel = level
indicator:SetWidth(size)
indicator:SetHeight(size)
if indicator.SetFrameLevel then indicator:SetFrameLevel(level) end
PositionIndicator(plate, indicator)
end
local function CreateIndicator(plate)
if not plate or plate.BNPTotemIndicator then return end
local indicator = CreateFrame("Frame", nil, plate)
local size = BNP.GetTotemIconSize and BNP:GetTotemIconSize() or 24
indicator:SetWidth(size)
indicator:SetHeight(size)
indicator:SetFrameLevel((plate.GetFrameLevel and plate:GetFrameLevel() or 0) + 2)
local border = indicator:CreateTexture(nil, "BACKGROUND")
border:SetTexture(0, 0, 0, 1)
border:SetPoint("TOPLEFT", indicator, "TOPLEFT", -1, 1)
border:SetPoint("BOTTOMRIGHT", indicator, "BOTTOMRIGHT", 1, -1)
indicator.border = border
local texture = indicator:CreateTexture(nil, "ARTWORK")
texture:SetAllPoints(indicator)
texture:SetTexCoord(0.07, 0.93, 0.07, 0.93)
indicator.texture = texture
indicator:Hide()
plate.BNPTotemIndicator = indicator
ApplyIndicatorLayout(plate, true)
end
local function UpdateIndicator(plate, force)
if not plate then return end
CreateIndicator(plate)
local indicator = plate.BNPTotemIndicator
if not indicator then return end
ApplyIndicatorLayout(plate, force)
if not (BNP.AreTotemIndicatorsEnabled and BNP:AreTotemIndicatorsEnabled()) then
indicator:Hide()
RestoreTotemPlateVisuals(plate)
plate.BNPTotemLastName = nil
plate.BNPTotemLastKey = nil
return
end
ResolveDefinitions()
local name = GetPlateName(plate)
-- Nameplate identity is stable most of the time. If the displayed name did
-- not change, reuse the previous totem decision and avoid UnitIsPlayer/name
-- normalization work on every 0.10s libnameplate update.
if not force and name == plate.BNPTotemLastName then
if plate.BNPTotemLastKey then
if not indicator:IsShown() then indicator:Show() end
SuppressTotemPlateVisuals(plate)
else
if indicator:IsShown() then indicator:Hide() end
RestoreTotemPlateVisuals(plate)
end
return
end
-- Hard safety guard: a player nameplate can never be a shaman totem.
-- SuperWoW nameplate tokens make this check reliable in BGs and prevent
-- false positives even if a player deliberately uses a totem-like name.
local token = plate.GetName and plate:GetName(1) or nil
if token and UnitIsPlayer and UnitIsPlayer(token) then
indicator:Hide()
RestoreTotemPlateVisuals(plate)
plate.BNPTotemLastName = name
plate.BNPTotemLastKey = nil
return
end
plate.BNPTotemLastName = name
plate.BNPTotemLastKey = nil
local def = FindTotemDefinition(name)
if not def then
indicator:Hide()
RestoreTotemPlateVisuals(plate)
return
end
plate.BNPTotemLastKey = def.key
indicator.texture:SetTexture(def.texture)
indicator:Show()
SuppressTotemPlateVisuals(plate, force)
end
function BNP:RefreshTotemIndicators()
ResolveDefinitions()
local plate
for plate in pairs(self.plates or {}) do
if plate then UpdateIndicator(plate, true) end
end
end
-- Public table keeps the totem definitions centralized and easy to extend.
BNP.TotemIndicatorDefinitions = TOTEM_DEFS
table.insert(BNP.libnameplate.OnInit, function(plate)
CreateIndicator(plate)
InstallTotemVisualGuard(plate)
InstallTotemHideGuard(plate)
UpdateIndicator(plate, true)
end)
table.insert(BNP.libnameplate.OnShow, function(plate)
InstallTotemVisualGuard(plate)
InstallTotemHideGuard(plate)
plate.BNPTotemLastName = nil
plate.BNPTotemLastKey = nil
UpdateIndicator(plate, true)
end)
table.insert(BNP.libnameplate.OnUpdate, function(plate)
UpdateIndicator(plate, false)
end)
+498
View File
@@ -0,0 +1,498 @@
BNP = BNP or {}
BNP.unknownDirectEvents = BNP.unknownDirectEvents or {}
-- Beta spell collector -------------------------------------------------------
-- Purpose: help testers report missing Turtle/custom spell IDs without
-- editing Lua files. It records unknown own cast IDs and unknown debuff IDs
-- that newly appear shortly after one of the player's casts.
--
-- It is intentionally diagnostic only: it never changes tracking/rendering.
BNP.unknownBeta = BNP.unknownBeta or {
entries = {},
order = {},
recentCasts = {},
seenByGUID = {},
activeUntil = 0,
}
local _, playerClass = UnitClass("player")
local CLASS_AURAS = {
WARLOCK = BNP.WarlockAuras,
PRIEST = BNP.PriestAuras,
WARRIOR = BNP.WarriorAuras,
ROGUE = BNP.RogueAuras,
HUNTER = BNP.HunterAuras,
MAGE = BNP.MageAuras,
DRUID = BNP.DruidAuras,
SHAMAN = BNP.ShamanAuras,
PALADIN = BNP.PaladinAuras,
}
local AURA_DEFS = CLASS_AURAS[playerClass] or {}
local function GetPlayerGUID()
local exists, guid = UnitExists("player")
if exists and guid then return guid end
return nil
end
local function GetUnitGUID(unit)
local exists, guid = UnitExists(unit)
if exists and guid then return guid end
return nil
end
local function IsExplicitKnownID(spellID)
if not spellID then return false end
-- Dark Harvest is handled by dedicated Warlock logic.
if spellID == 52552 then return true end
local i, j, def
for i = 1, table.getn(AURA_DEFS) do
def = AURA_DEFS[i]
if def.spellIDs then
for j = 1, table.getn(def.spellIDs) do
if def.spellIDs[j] == spellID then return true end
end
end
if def.durations and def.durations[spellID] then
return true
end
end
return false
end
local function SpellName(spellID)
if not spellID or not SpellInfo then return "?" end
local name = SpellInfo(spellID)
return name or "?"
end
local function AddEntry(kind, spellID, spellName, guid, trigger, confidence)
if not spellID then return end
local key = tostring(kind) .. ":" .. tostring(spellID)
local data = BNP.unknownBeta
local old = data.entries[key]
if old then
old.count = (old.count or 1) + 1
old.lastSeen = GetTime()
if confidence == "TARGET" then old.confidence = confidence end
if trigger and not old.trigger then old.trigger = trigger end
return
end
local entry = {
kind = kind,
spellID = spellID,
name = spellName or SpellName(spellID),
class = playerClass or "?",
guid = guid,
trigger = trigger,
confidence = confidence or "?",
count = 1,
firstSeen = GetTime(),
lastSeen = GetTime(),
}
data.entries[key] = entry
table.insert(data.order, key)
end
local function RememberCast(spellID, targetGUID, eventType)
local data = BNP.unknownBeta
local now = GetTime()
data.activeUntil = now + 8.0
local cast = {
time = now,
spellID = spellID,
name = SpellName(spellID),
targetGUID = targetGUID,
eventType = eventType,
}
table.insert(data.recentCasts, cast)
while table.getn(data.recentCasts) > 20 do
table.remove(data.recentCasts, 1)
end
-- Unknown own CAST/CHANNEL IDs are useful too. Some trap/proc systems expose
-- the effect as a separate cast event before the aura is seen.
if spellID and not IsExplicitKnownID(spellID) then
AddEntry("CAST", spellID, cast.name, targetGUID, nil, "OWN")
end
end
local function FindRecentTrigger(guid, now)
local casts = BNP.unknownBeta.recentCasts
local i
for i = table.getn(casts), 1, -1 do
local cast = casts[i]
local age = now - cast.time
if age > 6 then
break
end
if cast.targetGUID and guid and cast.targetGUID == guid then
return cast, "TARGET"
end
-- Short global fallback catches effects such as traps where the placement
-- cast may not carry the eventual victim GUID.
if age <= 1.5 then
return cast, "RECENT"
end
end
return nil, nil
end
local function ScanUnit(unit, guid, now)
if not unit or not guid then return end
local seen = BNP.unknownBeta.seenByGUID[guid]
if not seen then
seen = {}
BNP.unknownBeta.seenByGUID[guid] = seen
end
local current = {}
local i
for i = 1, 32 do
local texture, stacks, dtype, auraSpellID = UnitDebuff(unit, i)
if not texture then break end
if auraSpellID then
current[auraSpellID] = true
if not seen[auraSpellID] and not IsExplicitKnownID(auraSpellID) then
local trigger, confidence = FindRecentTrigger(guid, now)
if trigger then
AddEntry(
"AURA",
auraSpellID,
SpellName(auraSpellID),
guid,
tostring(trigger.name) .. " [" .. tostring(trigger.spellID) .. "]",
confidence
)
end
end
end
end
-- Keep only currently present aura IDs so a later re-application can be
-- detected again without growing this table forever.
BNP.unknownBeta.seenByGUID[guid] = current
end
-- Indirect Effect Audit: traps/procs may never expose a UnitDebuff ID.
BNP.unknownBeta.combat = BNP.unknownBeta.combat or {}
local EFFECT_WINDOW = 8.0
local function IsUsefulEffectLine(raw)
if not raw or raw == "" then return false end
local lower = string.lower(raw)
-- Ignore obvious player-action/noise lines.
if string.find(lower, "you cast ", 1, true) then return false end
if string.find(lower, "auto shot", 1, true) then return false end
if string.find(lower, "login", 1, true) then return false end
-- Keep lines that actually describe a harmful aura/effect or its periodic damage.
if string.find(lower, "afflicted by", 1, true) then return true end
if string.find(lower, "is afflicted", 1, true) then return true end
if string.find(lower, "is affected by", 1, true) then return true end
if string.find(lower, "suffers", 1, true) then return true end
if string.find(lower, "periodic", 1, true) then return true end
return false
end
local function AddCombatEffect(raw)
if not IsUsefulEffectLine(raw) then return end
local now=GetTime()
local casts=BNP.unknownBeta.recentCasts
local trigger=nil
local i
for i=table.getn(casts),1,-1 do
local c=casts[i]
if now-c.time > EFFECT_WINDOW then break end
if c.spellID ~= 75 then trigger=c break end
end
if not trigger then return end
if string.find(string.lower(raw),"auto shot",1,true) then return end
local key=tostring(trigger.spellID)..":"..raw
local old=BNP.unknownBeta.combat[key]
if old then old.count=old.count+1 return end
BNP.unknownBeta.combat[key]={
raw=raw, triggerID=trigger.spellID, triggerName=trigger.name, count=1
}
end
local combatFrame=CreateFrame("Frame")
local combatEvents={
"CHAT_MSG_COMBAT_SELF_HITS",
"CHAT_MSG_SPELL_SELF_DAMAGE",
"CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE",
"CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE",
}
local cei
for cei=1,table.getn(combatEvents) do combatFrame:RegisterEvent(combatEvents[cei]) end
combatFrame:SetScript("OnEvent",function() AddCombatEffect(arg1) end)
local castFrame = CreateFrame("Frame")
castFrame:RegisterEvent("UNIT_CASTEVENT")
castFrame:SetScript("OnEvent", function()
local playerGUID = GetPlayerGUID()
if not playerGUID or arg1 ~= playerGUID then return end
local eventType = arg3
if eventType ~= "CAST" and eventType ~= "CHANNEL" then return end
RememberCast(arg4, arg2, eventType)
end)
local scanner = CreateFrame("Frame")
local elapsed = 0
scanner:SetScript("OnUpdate", function()
elapsed = elapsed + arg1
if elapsed < 0.35 then return end
elapsed = 0
local now = GetTime()
if not BNP.unknownBeta.activeUntil or now > BNP.unknownBeta.activeUntil then
return
end
local targetGUID = GetUnitGUID("target")
if targetGUID then
ScanUnit("target", targetGUID, now)
end
local plate
for plate in pairs(BNP.plates) do
if plate:IsShown() and plate.GetName then
local token = plate:GetName(1)
if token then
local guid = GetUnitGUID(token)
if guid then
ScanUnit(token, guid, now)
end
end
end
end
end)
function BNP:PrintUnknownBeta(verbose)
local data = self.unknownBeta
self:Print("=== BNP Beta: Unknown Auras (" .. tostring(playerClass or "?") .. ") ===")
local auraCount = 0
local i
for i = 1, table.getn(data.order) do
local entry = data.entries[data.order[i]]
if entry and entry.kind == "AURA" then
auraCount = auraCount + 1
local line =
tostring(auraCount) ..
" | AURA | ID " .. tostring(entry.spellID) ..
" | " .. tostring(entry.name) ..
" | x" .. tostring(entry.count or 1)
if entry.confidence then
line = line .. " | " .. tostring(entry.confidence)
end
self:Print(line)
if entry.trigger then
self:Print(" Trigger: " .. tostring(entry.trigger))
end
end
end
local combatCount = 0
local ck
for ck in pairs(data.combat or {}) do combatCount = combatCount + 1 end
if combatCount > 0 then
self:Print("--- Indirect Effects ---")
local key, e
for key, e in pairs(data.combat) do
self:Print("EFFECT | Trigger " .. tostring(e.triggerName) .. " [" .. tostring(e.triggerID) .. "] | x" .. tostring(e.count))
self:Print(" " .. tostring(e.raw))
end
end
if auraCount == 0 and combatCount == 0 then
self:Print("No unknown negative auras/effects found since login.")
end
if verbose then
self:Print("--- Verbose: unknown player cast IDs ---")
local castCount = 0
for i = 1, table.getn(data.order) do
local entry = data.entries[data.order[i]]
if entry and entry.kind == "CAST" then
castCount = castCount + 1
self:Print(
tostring(castCount) ..
" | CAST | ID " .. tostring(entry.spellID) ..
" | " .. tostring(entry.name) ..
" | x" .. tostring(entry.count or 1)
)
end
end
if castCount == 0 then
self:Print("No unknown cast IDs.")
end
else
self:Print("Open Missing Spell / Aura in the settings to copy a complete report.")
end
end
function BNP:ResetUnknownBeta()
self.unknownBeta.entries = {}
self.unknownBeta.order = {}
self.unknownBeta.recentCasts = {}
self.unknownBeta.seenByGUID = {}
self.unknownBeta.combat = {}
self.unknownBeta.activeUntil = 0
self:Print("Unknown spell/effect list cleared.")
end
-- Target Aura Probe ----------------------------------------------------------
-- Diagnostic helper for proc/talent auras that the normal unknown collector
-- may miss because they were already present in its seen-state.
function BNP:PrintTargetAuraProbe()
local exists, guid = UnitExists("target")
if not exists or not guid then
self:Print("Target Aura Probe: no target selected.")
return
end
self:Print("=== BNP Target Negative Auras ===")
self:Print("Target: " .. tostring(UnitName("target") or "?") .. " | GUID " .. tostring(guid))
local count = 0
local i
for i = 1, 32 do
local texture, stacks, dtype, auraSpellID = UnitDebuff("target", i)
if not texture then break end
count = count + 1
local name = auraSpellID and SpellName(auraSpellID) or "?"
self:Print(
tostring(count) ..
" | ID " .. tostring(auraSpellID or "?") ..
" | " .. tostring(name) ..
" | stacks " .. tostring(stacks or 0) ..
" | type " .. tostring(dtype or "?")
)
end
if count == 0 then
self:Print("No negative target auras found.")
end
end
-- Direct player spell collector --------------------------------------------
-- Captures successful player spell events that do not create a persistent
-- aura and therefore never appear in the normal unknown-aura collector.
local function GetLocalPlayerGUID()
local exists, guid = UnitExists("player")
if exists and guid then return guid end
return nil
end
local function RememberDirectEvent(eventType, spellID, targetGUID, extra)
if not spellID then return end
local name = nil
if SpellInfo then name = SpellInfo(spellID) end
name = name or ("Spell " .. tostring(spellID))
local key = tostring(eventType) .. ":" .. tostring(spellID)
local entry = BNP.unknownDirectEvents[key]
if not entry then
entry = {
eventType = eventType,
spellID = spellID,
name = name,
targetGUID = targetGUID,
count = 0,
extra = extra,
last = GetTime(),
}
BNP.unknownDirectEvents[key] = entry
end
entry.count = (entry.count or 0) + 1
entry.targetGUID = targetGUID or entry.targetGUID
entry.extra = extra or entry.extra
entry.last = GetTime()
end
local directFrame = CreateFrame("Frame")
directFrame:RegisterEvent("UNIT_CASTEVENT")
directFrame:SetScript("OnEvent", function()
local casterGUID = arg1
local targetGUID = arg2
local eventType = arg3
local spellID = arg4
local playerGUID = GetLocalPlayerGUID()
if not playerGUID or casterGUID ~= playerGUID then return end
if not spellID then return end
if eventType == "START" or eventType == "CAST" or eventType == "CHANNEL" then
RememberDirectEvent(eventType, spellID, targetGUID, arg5)
end
end)
function BNP:PrintUnknownDirectEvents()
local list = {}
local key, entry
for key, entry in pairs(self.unknownDirectEvents or {}) do
table.insert(list, entry)
end
table.sort(list, function(a, b)
return (a.last or 0) > (b.last or 0)
end)
if table.getn(list) == 0 then
self:Print("--- Direct Player Events: none captured ---")
return
end
self:Print("--- Direct Player Events ---")
local i, e
for i = 1, table.getn(list) do
e = list[i]
self:Print(
tostring(i) .. " | " ..
tostring(e.eventType) .. " | ID " ..
tostring(e.spellID) .. " | " ..
tostring(e.name) .. " | x" ..
tostring(e.count or 1)
)
end
end
+102
View File
@@ -0,0 +1,102 @@
BNP = BNP or {}
-- Safe one-shot native visual repair ---------------------------------------
-- UI texture patches can replace the files behind Blizzard's native
-- Nameplate-Border/Nameplate-Glow paths without changing the texture paths
-- returned by the client. Keep private copies of the original Vanilla assets
-- inside BNP so those patches cannot silently reskin or blank the plates.
--
-- Apply them only when a plate is initialized/shown. There is intentionally NO
-- heartbeat and no layout/parent/anchor enforcement here, so BNP never fights
-- WorldFrame movement while the player/camera is moving.
local BORDER_TEXTURE = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\classic_nameplate_border.tga"
local GLOW_TEXTURE = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\classic_nameplate_glow.tga"
local DARK_BORDER_R, DARK_BORDER_G, DARK_BORDER_B, DARK_BORDER_A = 0.3, 0.3, 0.3, 0.9
local function IsTotemIconOnly(plate)
return plate and plate.BNPTotemLastKey and
BNP.AreTotemIndicatorsEnabled and BNP:AreTotemIndicatorsEnabled()
end
local function RestoreBorder(plate)
local border = plate and plate.border
if not border then return end
if border.GetTexture and border.SetTexture and border:GetTexture() ~= BORDER_TEXTURE then
border:SetTexture(BORDER_TEXTURE)
end
if border.IsShown and border.Show and not border:IsShown() then
border:Show()
end
end
local function RestoreGlow(plate)
local glow = plate and plate.glow
if not glow then return end
-- Do not show the region here. Blizzard/BNP still controls its visibility
-- through the normal mouseover path; only its source art is protected.
if glow.GetTexture and glow.SetTexture and glow:GetTexture() ~= GLOW_TEXTURE then
glow:SetTexture(GLOW_TEXTURE)
end
end
local function SetBorderVertexColor(border, r, g, b, a)
if not border or not border.SetVertexColor then return end
if border.GetVertexColor then
local oldR, oldG, oldB, oldA = border:GetVertexColor()
if math.abs((oldR or 1) - r) < 0.001
and math.abs((oldG or 1) - g) < 0.001
and math.abs((oldB or 1) - b) < 0.001
and math.abs((oldA or 1) - a) < 0.001 then
return
end
end
border:SetVertexColor(r, g, b, a)
end
local function ApplyBorderStyle(plate)
local border = plate and plate.border
if not border then return end
if BNP.IsDarkNameplateBorderEnabled and BNP:IsDarkNameplateBorderEnabled() then
-- Match ShaguTweaks' default Darkened UI color, but touch only the native
-- nameplate border. Healthbars, glows, castbars and icons stay unchanged.
SetBorderVertexColor(border, DARK_BORDER_R, DARK_BORDER_G, DARK_BORDER_B, DARK_BORDER_A)
else
-- This option is authoritative for the nameplate border. In particular,
-- switching it off must restore the original gold even when ShaguTweaks'
-- global Darkened UI mode previously tinted this texture.
SetBorderVertexColor(border, 1, 1, 1, 1)
end
end
function BNP:RepairNativeNameplateVisuals(plate)
if not plate then return end
if IsTotemIconOnly(plate) then return end
RestoreBorder(plate)
RestoreGlow(plate)
ApplyBorderStyle(plate)
end
function BNP:RefreshNameplateBorderStyle()
local plate
for plate in pairs(BNP.plates or {}) do
self:RepairNativeNameplateVisuals(plate)
end
end
if BNP.libnameplate then
table.insert(BNP.libnameplate.OnInit, function(plate)
BNP:RepairNativeNameplateVisuals(plate or this)
end)
table.insert(BNP.libnameplate.OnShow, function(plate)
BNP:RepairNativeNameplateVisuals(plate or this)
end)
end