Delete modules directory
This commit is contained in:
File diff suppressed because it is too large
Load Diff
-3474
File diff suppressed because it is too large
Load Diff
@@ -1,386 +0,0 @@
|
||||
BNP = BNP or {}
|
||||
|
||||
local casts = {}
|
||||
local BAR_HEIGHT = 6
|
||||
|
||||
-- Runtime-only visual preview. This is intentionally not saved in BNP_DB so
|
||||
-- a /reload always returns to normal cast detection.
|
||||
local castbarTestMode = false
|
||||
local testCastStart = 0
|
||||
local TEST_CAST_DURATION = 4
|
||||
local TEST_CAST_TEXTURE = "Interface\\Icons\\Spell_Fire_Fireball02"
|
||||
|
||||
local testCast = {
|
||||
name = "Test Cast",
|
||||
texture = TEST_CAST_TEXTURE,
|
||||
startTime = 0,
|
||||
endTime = 0,
|
||||
duration = TEST_CAST_DURATION,
|
||||
channel = false,
|
||||
}
|
||||
|
||||
local function ResetTestCast(now)
|
||||
now = now or GetTime()
|
||||
testCastStart = now
|
||||
testCast.startTime = now
|
||||
testCast.endTime = now + TEST_CAST_DURATION
|
||||
testCast.duration = TEST_CAST_DURATION
|
||||
end
|
||||
|
||||
local function GetTestCast(now)
|
||||
if testCastStart == 0 or now >= testCast.endTime then
|
||||
ResetTestCast(now)
|
||||
end
|
||||
return testCast
|
||||
end
|
||||
|
||||
local function GetCastbarHeight()
|
||||
if BNP.GetCastbarHeight then return BNP:GetCastbarHeight() end
|
||||
return BAR_HEIGHT
|
||||
end
|
||||
local BAR_GAP = 4
|
||||
|
||||
local function GetCastbarSpacing()
|
||||
if BNP.GetCastbarSpacing then return BNP:GetCastbarSpacing() end
|
||||
return BAR_GAP
|
||||
end
|
||||
|
||||
local BOTTOM_AURA_EDGE_GAP = 4
|
||||
local BOTTOM_AURA_ROW_SPACING = 2
|
||||
|
||||
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 = GetCastbarSpacing() + GetBottomAuraClearance(plate)
|
||||
|
||||
bar:ClearAllPoints()
|
||||
bar:SetPoint("TOPLEFT", plate.healthbar, "BOTTOMLEFT", 15, -gap)
|
||||
bar:SetPoint("TOPRIGHT", plate.healthbar, "BOTTOMRIGHT", 14, -gap)
|
||||
|
||||
if bar.icon then
|
||||
bar.icon:ClearAllPoints()
|
||||
bar.icon:SetPoint("TOPLEFT", plate.healthbar, "BOTTOMLEFT", 0, -gap)
|
||||
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
|
||||
|
||||
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: icon + castbar together span the complete plate width.
|
||||
-- The icon begins at the healthbar's left edge; the bar continues directly
|
||||
-- beside it and ends beneath the level badge.
|
||||
local gap = GetCastbarSpacing() + GetBottomAuraClearance(plate)
|
||||
bar:SetPoint("TOPLEFT", plate.healthbar, "BOTTOMLEFT", 15, -gap)
|
||||
bar:SetPoint("TOPRIGHT", plate.healthbar, "BOTTOMRIGHT", 14, -gap)
|
||||
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")
|
||||
text:SetPoint("CENTER", bar, "CENTER", 0, 0)
|
||||
text:SetTextColor(1, 1, 1)
|
||||
bar.text = text
|
||||
|
||||
-- Spell icon to the left of the castbar.
|
||||
local icon = CreateFrame("Frame", nil, parent)
|
||||
icon:SetWidth(14)
|
||||
icon:SetHeight(14)
|
||||
icon:SetPoint("TOPLEFT", plate.healthbar, "BOTTOMLEFT", 0, -gap)
|
||||
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
|
||||
|
||||
icon:Hide()
|
||||
bar.icon = icon
|
||||
|
||||
plate.BNPCastbar = bar
|
||||
return bar
|
||||
end
|
||||
|
||||
local function Hide(plate)
|
||||
if plate and plate.BNPCastbar then
|
||||
plate.BNPCastbar:Hide()
|
||||
if plate.BNPCastbar.icon then
|
||||
plate.BNPCastbar.icon:Hide()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Exact SuperWoW event path used by ShaguTweaks' superwow module:
|
||||
-- START / CAST / CHANNEL create cast state; FAIL removes it.
|
||||
local eventFrame = CreateFrame("Frame")
|
||||
eventFrame:RegisterEvent("UNIT_CASTEVENT")
|
||||
eventFrame:SetScript("OnEvent", function()
|
||||
local guid = arg1
|
||||
local eventType = arg3
|
||||
local spellID = arg4
|
||||
local timerMS = tonumber(arg5) or 0
|
||||
|
||||
if not guid then return end
|
||||
|
||||
if eventType == "START" or eventType == "CAST" or eventType == "CHANNEL" then
|
||||
if timerMS <= 0 then
|
||||
-- Instant casts do not need a visible castbar.
|
||||
casts[guid] = nil
|
||||
return
|
||||
end
|
||||
|
||||
local name, texture = SpellData(spellID)
|
||||
local now = GetTime()
|
||||
local duration = timerMS / 1000
|
||||
|
||||
casts[guid] = {
|
||||
name = name,
|
||||
texture = texture,
|
||||
startTime = now,
|
||||
endTime = now + duration,
|
||||
duration = duration,
|
||||
channel = eventType == "CHANNEL",
|
||||
}
|
||||
elseif eventType == "FAIL" then
|
||||
casts[guid] = nil
|
||||
end
|
||||
end)
|
||||
|
||||
local activeCastPlates = {}
|
||||
|
||||
local function MarkCastPlateActive(plate)
|
||||
if not plate then return end
|
||||
activeCastPlates[plate] = true
|
||||
end
|
||||
|
||||
local function MarkCastPlateInactive(plate)
|
||||
if not plate then return end
|
||||
activeCastPlates[plate] = nil
|
||||
Hide(plate)
|
||||
end
|
||||
|
||||
-- Lightweight discovery pass. This does NOT animate bars; it only checks
|
||||
-- visible plates for active cast state at 20 Hz.
|
||||
local discovery = CreateFrame("Frame")
|
||||
local discoveryElapsed = 0
|
||||
discovery:SetScript("OnUpdate", function()
|
||||
discoveryElapsed = discoveryElapsed + arg1
|
||||
if discoveryElapsed < 0.05 then return end
|
||||
discoveryElapsed = 0
|
||||
|
||||
if not Enabled() and not castbarTestMode then
|
||||
local plate
|
||||
for plate in pairs(activeCastPlates) do
|
||||
MarkCastPlateInactive(plate)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local now = GetTime()
|
||||
local plate
|
||||
for plate in pairs(BNP.plates) do
|
||||
if plate:IsShown() then
|
||||
if castbarTestMode then
|
||||
-- Preview every visible nameplate. No GUID/cast lookup is needed.
|
||||
MarkCastPlateActive(plate)
|
||||
else
|
||||
local guid = GetPlateGUID(plate)
|
||||
local cast = guid and casts[guid]
|
||||
|
||||
if cast and now < cast.endTime then
|
||||
MarkCastPlateActive(plate)
|
||||
elseif activeCastPlates[plate] then
|
||||
MarkCastPlateInactive(plate)
|
||||
end
|
||||
end
|
||||
elseif activeCastPlates[plate] then
|
||||
MarkCastPlateInactive(plate)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Frame-perfect animation remains unchanged visually, but only active
|
||||
-- castbars are updated every rendered frame.
|
||||
local updater = CreateFrame("Frame")
|
||||
updater:SetScript("OnUpdate", function()
|
||||
if not Enabled() and not castbarTestMode then return end
|
||||
|
||||
local now = GetTime()
|
||||
local plate
|
||||
for plate in pairs(activeCastPlates) do
|
||||
if not plate:IsShown() then
|
||||
MarkCastPlateInactive(plate)
|
||||
else
|
||||
local guid
|
||||
local cast
|
||||
|
||||
if castbarTestMode then
|
||||
cast = GetTestCast(now)
|
||||
else
|
||||
guid = GetPlateGUID(plate)
|
||||
cast = guid and casts[guid]
|
||||
end
|
||||
|
||||
if not cast or now >= cast.endTime then
|
||||
if (not castbarTestMode) and guid and cast and now >= cast.endTime then
|
||||
casts[guid] = nil
|
||||
end
|
||||
MarkCastPlateInactive(plate)
|
||||
else
|
||||
local bar = CreateCastbar(plate)
|
||||
if bar then
|
||||
local bottomClearance = GetBottomAuraClearance(plate)
|
||||
if bar.BNPBottomAuraClearance ~= bottomClearance then
|
||||
ApplyCastbarLayout(plate, bar)
|
||||
end
|
||||
|
||||
local current = now - cast.startTime
|
||||
if current < 0 then current = 0 end
|
||||
if current > cast.duration then current = cast.duration end
|
||||
|
||||
bar:SetMinMaxValues(0, cast.duration)
|
||||
|
||||
if cast.channel then
|
||||
bar:SetValue(cast.duration - current)
|
||||
else
|
||||
bar:SetValue(current)
|
||||
end
|
||||
|
||||
bar.text:SetText(cast.name)
|
||||
bar:SetAlpha(1)
|
||||
|
||||
if bar.icon then
|
||||
if cast.texture then
|
||||
bar.icon.texture:SetTexture(cast.texture)
|
||||
bar.icon:SetAlpha(1)
|
||||
bar.icon:Show()
|
||||
else
|
||||
bar.icon:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
bar:Show()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
function BNP:IsCastbarTestMode()
|
||||
return castbarTestMode and true or false
|
||||
end
|
||||
|
||||
function BNP:SetCastbarTestMode(enabled)
|
||||
castbarTestMode = enabled and true or false
|
||||
|
||||
if castbarTestMode then
|
||||
ResetTestCast(GetTime())
|
||||
else
|
||||
-- Remove every preview immediately. Real casts are rediscovered on the
|
||||
-- next lightweight discovery pass without touching their stored state.
|
||||
local plate
|
||||
for plate in pairs(activeCastPlates) do
|
||||
MarkCastPlateInactive(plate)
|
||||
end
|
||||
end
|
||||
|
||||
if self.optionsFrame and self.optionsFrame.castbarTestCheck then
|
||||
self.optionsFrame.castbarTestCheck:SetChecked(castbarTestMode)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function BNP:RefreshCastbarLayout()
|
||||
local plate
|
||||
local height = GetCastbarHeight()
|
||||
|
||||
for plate in pairs(BNP.plates or {}) do
|
||||
if plate and plate.BNPCastbar then
|
||||
plate.BNPCastbar:SetHeight(height)
|
||||
ApplyCastbarLayout(plate, plate.BNPCastbar)
|
||||
end
|
||||
end
|
||||
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
|
||||
@@ -1,169 +0,0 @@
|
||||
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 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 ApplyClassColor(plate)
|
||||
if not Enabled() then
|
||||
ClearClassCache(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
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
return
|
||||
end
|
||||
|
||||
local r, g, b = 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
|
||||
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)
|
||||
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)
|
||||
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 updater = CreateFrame("Frame")
|
||||
updater:SetScript("OnUpdate", function()
|
||||
elapsed = elapsed + arg1
|
||||
if elapsed < 0.10 then return end
|
||||
elapsed = 0
|
||||
|
||||
if not Enabled() then return end
|
||||
|
||||
local plate
|
||||
for plate in pairs(BNP.plates) do
|
||||
if plate:IsShown() and plate.healthbar then
|
||||
local token, guid = ResolvePlateUnit(plate)
|
||||
|
||||
if plate.BNPClassGUID then
|
||||
EnforceClassColor(plate)
|
||||
elseif token and guid and UnitIsPlayer(token) then
|
||||
ApplyClassColor(plate)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
@@ -1,299 +0,0 @@
|
||||
BNP = BNP or {}
|
||||
|
||||
local _, playerClass = UnitClass("player")
|
||||
local COMBO_CLASS = (playerClass == "ROGUE" or playerClass == "DRUID")
|
||||
|
||||
local MAX_POINTS = 5
|
||||
local POINT_GAP = 1
|
||||
|
||||
local function Enabled()
|
||||
return COMBO_CLASS and BNP.AreComboPointsEnabled and BNP:AreComboPointsEnabled()
|
||||
end
|
||||
|
||||
local function GetTargetGUID()
|
||||
local exists, guid = UnitExists("target")
|
||||
if exists and guid then return guid end
|
||||
return nil
|
||||
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 function GetSourcePoint()
|
||||
if getglobal then
|
||||
return getglobal("TargetFrameComboPoint1")
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function GetSourceSize()
|
||||
local source = GetSourcePoint()
|
||||
local w, h = 12, 16
|
||||
|
||||
if source then
|
||||
if source.GetWidth then w = tonumber(source:GetWidth()) or w end
|
||||
if source.GetHeight then h = tonumber(source:GetHeight()) or h end
|
||||
end
|
||||
|
||||
if w <= 0 then w = 12 end
|
||||
if h <= 0 then h = 16 end
|
||||
return w, h
|
||||
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
|
||||
|
||||
-- Match the actual on-screen scale of Blizzard's TargetFrame combo points.
|
||||
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 CopySourceTextureAppearance(dst, src)
|
||||
if not dst or not src then return end
|
||||
|
||||
if src.GetTexture and dst.SetTexture then
|
||||
dst:SetTexture(src:GetTexture())
|
||||
end
|
||||
|
||||
if src.GetTexCoord and dst.SetTexCoord then
|
||||
local l, r, t, b = src:GetTexCoord()
|
||||
if l then dst:SetTexCoord(l, r, t, b) end
|
||||
end
|
||||
|
||||
if src.GetVertexColor and dst.SetVertexColor then
|
||||
local r, g, b, a = src:GetVertexColor()
|
||||
if r then dst:SetVertexColor(r, g, b, a or 1) end
|
||||
end
|
||||
|
||||
if src.GetBlendMode and dst.SetBlendMode then
|
||||
local mode = src:GetBlendMode()
|
||||
if mode then dst:SetBlendMode(mode) end
|
||||
end
|
||||
end
|
||||
|
||||
local function CreateExactPoint(parent)
|
||||
local source = GetSourcePoint()
|
||||
local pointW, pointH = GetSourceSize()
|
||||
|
||||
local point = CreateFrame("Frame", nil, parent)
|
||||
point:SetWidth(pointW)
|
||||
point:SetHeight(pointH)
|
||||
|
||||
local copied = false
|
||||
|
||||
-- Clone the actual texture regions from Blizzard's TargetFrame combo point.
|
||||
-- This guarantees the same client artwork/skin instead of guessing atlas cuts.
|
||||
if source and source.GetRegions then
|
||||
local regions = { source:GetRegions() }
|
||||
local i
|
||||
|
||||
for i = 1, table.getn(regions) do
|
||||
local src = regions[i]
|
||||
if src and src.GetObjectType and src:GetObjectType() == "Texture" then
|
||||
local tex = point:CreateTexture(nil, "ARTWORK")
|
||||
CopySourceTextureAppearance(tex, src)
|
||||
|
||||
local w = src.GetWidth and tonumber(src:GetWidth()) or pointW
|
||||
local h = src.GetHeight and tonumber(src:GetHeight()) or pointH
|
||||
if not w or w <= 0 then w = pointW end
|
||||
if not h or h <= 0 then h = pointH end
|
||||
|
||||
tex:SetWidth(w)
|
||||
tex:SetHeight(h)
|
||||
|
||||
-- Preserve the standard TargetFrame relative offsets where possible.
|
||||
local anchored = false
|
||||
if src.GetPoint then
|
||||
local pnt, rel, relp, x, y = src:GetPoint(1)
|
||||
if pnt then
|
||||
-- Source textures are normally anchored to their own combo frame.
|
||||
-- Re-anchor them to our clone with the same point/offset.
|
||||
tex:SetPoint(pnt, point, relp or pnt, x or 0, y or 0)
|
||||
anchored = true
|
||||
end
|
||||
end
|
||||
|
||||
if not anchored then
|
||||
tex:SetPoint("CENTER", point, "CENTER", 0, 0)
|
||||
end
|
||||
|
||||
copied = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Fallback only if the client does not expose TargetFrameComboPoint1 regions.
|
||||
if not copied then
|
||||
local base = point:CreateTexture(nil, "BACKGROUND")
|
||||
base:SetTexture("Interface\\ComboFrame\\ComboPoint")
|
||||
base:SetWidth(12)
|
||||
base:SetHeight(16)
|
||||
base:SetPoint("TOPLEFT", point, "TOPLEFT", 0, 0)
|
||||
base:SetTexCoord(0, 0.375, 0, 1)
|
||||
|
||||
local highlight = point:CreateTexture(nil, "ARTWORK")
|
||||
highlight:SetTexture("Interface\\ComboFrame\\ComboPoint")
|
||||
highlight:SetWidth(8)
|
||||
highlight:SetHeight(16)
|
||||
highlight:SetPoint("TOPLEFT", point, "TOPLEFT", 2, 0)
|
||||
highlight:SetTexCoord(0.375, 0.5625, 0, 1)
|
||||
end
|
||||
|
||||
point:Hide()
|
||||
return point
|
||||
end
|
||||
|
||||
local function EnsurePoints(plate)
|
||||
if plate.BNPComboPoints then
|
||||
ApplyTargetFrameScale(plate.BNPComboPoints)
|
||||
return plate.BNPComboPoints
|
||||
end
|
||||
|
||||
if not plate or not plate.name then return nil end
|
||||
|
||||
local pointW, pointH = GetSourceSize()
|
||||
local parent = plate.BNPScaleWrapper or plate
|
||||
|
||||
local holder = CreateFrame("Frame", nil, parent)
|
||||
holder:SetWidth(MAX_POINTS * pointW + (MAX_POINTS - 1) * POINT_GAP)
|
||||
holder:SetHeight(pointH)
|
||||
|
||||
-- Keep the position the user already approved.
|
||||
holder:SetPoint("BOTTOM", plate.name, "TOP", 0, 2)
|
||||
holder:SetFrameLevel((plate:GetFrameLevel() or 1) + 12)
|
||||
holder.points = {}
|
||||
holder:Hide()
|
||||
|
||||
local i
|
||||
for i = 1, MAX_POINTS do
|
||||
local point = CreateExactPoint(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
|
||||
|
||||
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 targetGUID = GetTargetGUID()
|
||||
local plateGUID = plate.GetName and plate:GetName(1) or nil
|
||||
|
||||
if not targetGUID or not plateGUID or targetGUID ~= plateGUID then
|
||||
HidePoints(plate)
|
||||
return
|
||||
end
|
||||
|
||||
local count = GetComboCount()
|
||||
if count <= 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 <= count then
|
||||
holder.points[i]:Show()
|
||||
else
|
||||
holder.points[i]:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
holder:Show()
|
||||
end
|
||||
|
||||
function BNP:RefreshComboPoints()
|
||||
local plate
|
||||
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
|
||||
|
||||
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
|
||||
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:SetScript("OnEvent", function()
|
||||
BNP:RefreshComboPoints()
|
||||
end)
|
||||
end
|
||||
@@ -1,592 +0,0 @@
|
||||
-- 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
|
||||
@@ -1,43 +0,0 @@
|
||||
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
|
||||
@@ -1,569 +0,0 @@
|
||||
BNP = BNP or {}
|
||||
|
||||
local NON_TARGET_ALPHA = 0.55
|
||||
local TARGET_RESOLVE_CACHE_TIME = 0.05
|
||||
local ALPHA_IDENTITY_HOLD_TIME = 0.20
|
||||
|
||||
local targetResolveCacheGUID = nil
|
||||
local targetResolveCacheAt = -100
|
||||
local targetResolveCacheFound = false
|
||||
local classicTargetCacheGUID = nil
|
||||
local classicTargetCacheAt = -100
|
||||
local classicTargetCachePlate = nil
|
||||
local classicTargetCacheAvailable = false
|
||||
|
||||
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()
|
||||
local exists, guid = UnitExists("target")
|
||||
if exists and guid then return guid end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function InvalidateTargetResolution()
|
||||
targetResolveCacheGUID = nil
|
||||
targetResolveCacheAt = -100
|
||||
targetResolveCacheFound = false
|
||||
classicTargetCacheGUID = nil
|
||||
classicTargetCacheAt = -100
|
||||
classicTargetCachePlate = nil
|
||||
classicTargetCacheAvailable = false
|
||||
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)
|
||||
-- No selected target: keep all nameplates fully visible.
|
||||
local targetGUID = 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
|
||||
end
|
||||
|
||||
local function ApplyForeignTagVisual(plate)
|
||||
if not plate or not plate:IsShown() then return end
|
||||
|
||||
local bar = plate.healthbar
|
||||
if not bar or not bar.SetStatusBarColor then return end
|
||||
|
||||
local foreign = IsForeignTagged(plate)
|
||||
|
||||
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.
|
||||
bar:SetStatusBarColor(0.45, 0.45, 0.45, plate.BNPForeignTagOldA or 1)
|
||||
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
|
||||
glow:SetWidth(width + 60)
|
||||
glow:SetHeight(height + 30)
|
||||
|
||||
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 glow = EnsureTargetGlow(plate)
|
||||
if not glow then return end
|
||||
|
||||
local enabled = BNP.IsTargetFocusEnabled and BNP:IsTargetFocusEnabled()
|
||||
if not enabled then
|
||||
glow:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
local targetGUID = GetTargetGUID()
|
||||
local plateGUID = GetPlateGUID(plate)
|
||||
|
||||
if targetGUID and plateGUID and targetGUID == plateGUID then
|
||||
local anchor = plate.healthbar or plate
|
||||
local width = anchor.GetWidth and anchor:GetWidth() or 120
|
||||
local height = anchor.GetHeight and anchor:GetHeight() or 8
|
||||
|
||||
|
||||
glow:ClearAllPoints()
|
||||
glow:SetPoint("CENTER", anchor, "CENTER", 3, 0)
|
||||
glow:SetWidth(width + 60)
|
||||
glow:SetHeight(height + 30)
|
||||
|
||||
if BNP.GetTargetGlowColor then
|
||||
local r, g, b = BNP:GetTargetGlowColor()
|
||||
glow:SetVertexColor(r, g, b, 1)
|
||||
end
|
||||
|
||||
glow:SetAlpha(1)
|
||||
glow:Show()
|
||||
else
|
||||
glow:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
local function ApplyTargetAlpha(plate)
|
||||
if not plate or not plate:IsShown() then return end
|
||||
local totemIconOnly = plate.BNPTotemLastKey and
|
||||
BNP.AreTotemIndicatorsEnabled and BNP:AreTotemIndicatorsEnabled()
|
||||
local wanted = totemIconOnly and 1 or DesiredAlpha(plate)
|
||||
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 targetGUID = GetTargetGUID()
|
||||
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 then
|
||||
BNP:MaintainNameplateYOffset(current)
|
||||
end
|
||||
|
||||
ApplyTargetAlpha(current)
|
||||
ApplyTargetGlow(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)
|
||||
ApplyForeignTagVisual(current)
|
||||
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)
|
||||
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)
|
||||
ApplyForeignTagVisual(plate)
|
||||
end
|
||||
end
|
||||
|
||||
function BNP:RefreshNonTargetAlpha()
|
||||
InvalidateTargetResolution()
|
||||
local plate
|
||||
for plate in pairs(self.plates or {}) do
|
||||
ApplyTargetAlpha(plate)
|
||||
end
|
||||
end
|
||||
@@ -1,73 +0,0 @@
|
||||
-- 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
|
||||
@@ -1,223 +0,0 @@
|
||||
BNP = BNP or {}
|
||||
|
||||
local function Enabled()
|
||||
return BNP.IsHealthTextEnabled and BNP:IsHealthTextEnabled()
|
||||
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")
|
||||
text:SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE")
|
||||
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
|
||||
|
||||
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
|
||||
updater:SetScript("OnUpdate", function()
|
||||
elapsed = elapsed + arg1
|
||||
if elapsed < 0.10 then return end
|
||||
elapsed = 0
|
||||
|
||||
local plate
|
||||
for plate in pairs(BNP.plates or {}) do
|
||||
if plate:IsShown() then
|
||||
UpdatePlate(plate)
|
||||
elseif plate.BNPHealthPercent then
|
||||
plate.BNPHealthPercent:Hide()
|
||||
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)
|
||||
@@ -1,969 +0,0 @@
|
||||
if not BNP or not BNP.libnameplate then return end
|
||||
|
||||
-- PvP immunity / anti-fear indicators (strict whitelist) -------------------
|
||||
-- Keep this deliberately close to BNP's proven aura/nameplate path:
|
||||
-- SuperWoW's projected nameplate token (plate:GetName(1)) is used directly
|
||||
-- with UnitBuff, just like auras.lua uses it directly with UnitDebuff.
|
||||
-- No GUID -> token reverse conversion and no C_UnitAuras dependency here.
|
||||
|
||||
BNP.guidImmunities = BNP.guidImmunities or {}
|
||||
|
||||
local UI = BNP.UI or {}
|
||||
local MAX_ICONS = 4
|
||||
local ICON_SPACING = UI.ICON_SPACING or 2
|
||||
local SIDE_SPACING = 4
|
||||
local BOTTOM_SPACING = 4
|
||||
local BASE_TOP_OFFSET = UI.ICON_OFFSET_Y or 15
|
||||
local COMBO_LAYOUT_OFFSET = 16
|
||||
local MISSING_GRACE = 0.20
|
||||
local NAME_HIDDEN_COMPACT_SHIFT = 10
|
||||
|
||||
local _, playerClass = UnitClass("player")
|
||||
|
||||
local IMM_ERR_LAST = {}
|
||||
local function ReportImmunityError(stage, err)
|
||||
local now = GetTime and GetTime() or 0
|
||||
local last = IMM_ERR_LAST[stage] or -100
|
||||
if now - last < 1.5 then return end
|
||||
IMM_ERR_LAST[stage] = now
|
||||
local msg = "|cffff4444BNP IMM ERROR [" .. tostring(stage) .. "]|r " .. tostring(err)
|
||||
if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(msg)
|
||||
end
|
||||
end
|
||||
|
||||
local IMMUNITY_DEFS = {
|
||||
{
|
||||
key = "divine_shield",
|
||||
names = { "Divine Shield", "Gottesschild" },
|
||||
spellIDs = { 642, 1020 },
|
||||
durations = { [642] = 10, [1020] = 12 },
|
||||
defaultDuration = 12,
|
||||
textureMatches = { "spell_holy_divineintervention" },
|
||||
selfOnly = true,
|
||||
},
|
||||
{
|
||||
key = "divine_protection",
|
||||
names = { "Divine Protection", "Göttlicher Schutz" },
|
||||
spellIDs = { 498, 5573 },
|
||||
durations = { [498] = 6, [5573] = 8 },
|
||||
defaultDuration = 8,
|
||||
selfOnly = true,
|
||||
},
|
||||
{
|
||||
key = "blessing_of_protection",
|
||||
names = { "Blessing of Protection", "Segen des Schutzes" },
|
||||
spellIDs = { 1022, 5599, 10278 },
|
||||
durations = { [1022] = 6, [5599] = 8, [10278] = 10 },
|
||||
defaultDuration = 10,
|
||||
textureMatches = { "spell_holy_sealofprotection" },
|
||||
},
|
||||
{
|
||||
key = "ice_block",
|
||||
names = { "Ice Block", "Eisblock" },
|
||||
spellIDs = { 11958 },
|
||||
durations = { [11958] = 10 },
|
||||
defaultDuration = 10,
|
||||
textureMatches = { "spell_frost_frost" },
|
||||
selfOnly = true,
|
||||
},
|
||||
{
|
||||
key = "berserker_rage",
|
||||
names = { "Berserker Rage", "Berserkerwut" },
|
||||
spellIDs = { 18499 },
|
||||
durations = { [18499] = 10 },
|
||||
defaultDuration = 10,
|
||||
selfOnly = true,
|
||||
},
|
||||
{
|
||||
key = "death_wish",
|
||||
names = { "Death Wish", "Todeswunsch" },
|
||||
spellIDs = { 12328 },
|
||||
durations = { [12328] = 30 },
|
||||
defaultDuration = 30,
|
||||
selfOnly = true,
|
||||
},
|
||||
{
|
||||
key = "recklessness",
|
||||
names = { "Recklessness", "Tollkühnheit" },
|
||||
spellIDs = { 1719 },
|
||||
durations = { [1719] = 15 },
|
||||
defaultDuration = 15,
|
||||
selfOnly = true,
|
||||
},
|
||||
{
|
||||
key = "fear_ward",
|
||||
names = { "Fear Ward", "Furchtzauberschutz" },
|
||||
spellIDs = { 6346 },
|
||||
durations = { [6346] = 600 },
|
||||
defaultDuration = 600,
|
||||
},
|
||||
{
|
||||
key = "will_of_the_forsaken",
|
||||
names = { "Will of the Forsaken", "Wille der Verlassenen" },
|
||||
spellIDs = { 7744 },
|
||||
durations = { [7744] = 5 },
|
||||
defaultDuration = 5,
|
||||
selfOnly = true,
|
||||
},
|
||||
}
|
||||
|
||||
local DEF_BY_ID = {}
|
||||
local DEF_BY_NAME = {}
|
||||
|
||||
local function Lower(value)
|
||||
if type(value) ~= "string" or value == "" then return nil end
|
||||
return string.lower(value)
|
||||
end
|
||||
|
||||
local function DefDuration(def, spellID)
|
||||
if def and def.durations and spellID and def.durations[spellID] then
|
||||
return def.durations[spellID]
|
||||
end
|
||||
return def and def.defaultDuration or nil
|
||||
end
|
||||
|
||||
local function BuildLookup()
|
||||
local i, j, def, id, name
|
||||
for i = 1, table.getn(IMMUNITY_DEFS) do
|
||||
def = IMMUNITY_DEFS[i]
|
||||
if def.names then
|
||||
for j = 1, table.getn(def.names) do
|
||||
name = Lower(def.names[j])
|
||||
if name then DEF_BY_NAME[name] = def end
|
||||
end
|
||||
end
|
||||
if def.spellIDs then
|
||||
for j = 1, table.getn(def.spellIDs) do
|
||||
id = def.spellIDs[j]
|
||||
DEF_BY_ID[id] = def
|
||||
if SpellInfo then
|
||||
local spellName = SpellInfo(id)
|
||||
spellName = Lower(spellName)
|
||||
if spellName then DEF_BY_NAME[spellName] = def end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
BuildLookup()
|
||||
|
||||
local function TextureMatches(def, texture)
|
||||
local t = Lower(texture)
|
||||
if not def or not t or not def.textureMatches then return false end
|
||||
local i
|
||||
for i = 1, table.getn(def.textureMatches) do
|
||||
local pattern = Lower(def.textureMatches[i])
|
||||
if pattern and string.find(t, pattern, 1, true) then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function FindDef(spellID, name, texture)
|
||||
-- STRICT WHITELIST: an aura is only an immunity when its spell ID or exact
|
||||
-- spell name is configured above. Do not guess from a shared icon/texture.
|
||||
-- Many unrelated server buffs reuse Blizzard icons (e.g. armor/world buffs),
|
||||
-- which previously allowed false positives such as Forest Armor.
|
||||
local sid = tonumber(spellID)
|
||||
if sid then
|
||||
return DEF_BY_ID[sid]
|
||||
end
|
||||
|
||||
local n = Lower(name)
|
||||
if n then
|
||||
return DEF_BY_NAME[n]
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function GetIconSize()
|
||||
if BNP.GetImmunityIconSize then return BNP:GetImmunityIconSize() end
|
||||
if BNP.GetIconSize then return BNP:GetIconSize() end
|
||||
return 18
|
||||
end
|
||||
|
||||
local function GetPosition()
|
||||
if BNP.GetImmunityPosition then return BNP:GetImmunityPosition() end
|
||||
return "top"
|
||||
end
|
||||
|
||||
local function GetVisualParent(plate)
|
||||
if plate and plate.GetParent then
|
||||
local p = plate:GetParent()
|
||||
if p then return p end
|
||||
end
|
||||
return WorldFrame or UIParent
|
||||
end
|
||||
|
||||
local function GetBaseTopOffset()
|
||||
local y = BASE_TOP_OFFSET
|
||||
if (playerClass == "ROGUE" or playerClass == "DRUID")
|
||||
and BNP.AreComboPointsEnabled and BNP:AreComboPointsEnabled() then
|
||||
y = y + COMBO_LAYOUT_OFFSET
|
||||
end
|
||||
return y
|
||||
end
|
||||
|
||||
local function IsTopDebuffPosition(position)
|
||||
return position == "top" or position == "top_left" or position == "top_right"
|
||||
end
|
||||
|
||||
local function GetTopOffset()
|
||||
local auraSize = BNP.GetIconSize and BNP:GetIconSize() or 18
|
||||
local ccSize = BNP.GetCCIconSize and BNP:GetCCIconSize() or auraSize
|
||||
local debuffPosition = BNP.GetDebuffPosition and BNP:GetDebuffPosition() or "top"
|
||||
local ccPosition = BNP.GetCCPosition and BNP:GetCCPosition() or debuffPosition
|
||||
local debuffs = not BNP.AreDebuffsEnabled or BNP:AreDebuffsEnabled()
|
||||
local ccs = not BNP.AreCrowdControlEnabled or BNP:AreCrowdControlEnabled()
|
||||
local separateCC = ccs and debuffs and ccPosition == "top" and IsTopDebuffPosition(debuffPosition)
|
||||
and BNP.IsSeparateCCRowEnabled and BNP:IsSeparateCCRowEnabled()
|
||||
local y = GetBaseTopOffset()
|
||||
|
||||
if separateCC then
|
||||
y = y + auraSize + ICON_SPACING + ccSize + ICON_SPACING
|
||||
else
|
||||
local rowHeight = 0
|
||||
if debuffs and IsTopDebuffPosition(debuffPosition) then rowHeight = math.max(rowHeight, auraSize) end
|
||||
if ccs and ccPosition == "top" then rowHeight = math.max(rowHeight, ccSize) end
|
||||
if rowHeight > 0 then y = y + rowHeight + ICON_SPACING end
|
||||
end
|
||||
return y
|
||||
end
|
||||
|
||||
local function FrameShown(frame)
|
||||
return frame and frame.IsShown and frame:IsShown() and true or false
|
||||
end
|
||||
|
||||
local function GetBottomOffset(plate)
|
||||
local auraSize = BNP.GetIconSize and BNP:GetIconSize() or 18
|
||||
local ccSize = BNP.GetCCIconSize and BNP:GetCCIconSize() or auraSize
|
||||
local debuffPosition = BNP.GetDebuffPosition and BNP:GetDebuffPosition() or "top"
|
||||
local ccPosition = BNP.GetCCPosition and BNP:GetCCPosition() or debuffPosition
|
||||
local debuffs = not BNP.AreDebuffsEnabled or BNP:AreDebuffsEnabled()
|
||||
local ccs = not BNP.AreCrowdControlEnabled or BNP:AreCrowdControlEnabled()
|
||||
local separateCC = ccs and debuffs and ccPosition == "bottom" and debuffPosition == "bottom"
|
||||
and BNP.IsSeparateCCRowEnabled and BNP:IsSeparateCCRowEnabled()
|
||||
|
||||
local harmfulDepth = 0
|
||||
if separateCC then
|
||||
-- The CC row is physically the second row, so preserve the first-row
|
||||
-- offset whenever that second row is visible.
|
||||
if FrameShown(plate and plate.BNPCCContainer) then
|
||||
harmfulDepth = auraSize + ICON_SPACING + ccSize
|
||||
elseif FrameShown(plate and plate.BNPAuraContainer) then
|
||||
harmfulDepth = auraSize
|
||||
end
|
||||
else
|
||||
if debuffs and debuffPosition == "bottom" and FrameShown(plate and plate.BNPAuraContainer) then
|
||||
harmfulDepth = math.max(harmfulDepth, auraSize)
|
||||
end
|
||||
if ccs and ccPosition == "bottom" then
|
||||
if ccPosition == debuffPosition then
|
||||
if FrameShown(plate and plate.BNPAuraContainer) then harmfulDepth = math.max(harmfulDepth, ccSize) end
|
||||
elseif FrameShown(plate and plate.BNPCCContainer) then
|
||||
harmfulDepth = math.max(harmfulDepth, ccSize)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local y = BOTTOM_SPACING
|
||||
if harmfulDepth > 0 then y = y + harmfulDepth + ICON_SPACING end
|
||||
return y
|
||||
end
|
||||
|
||||
local function GetSideOffset(position)
|
||||
local auraSize = BNP.GetIconSize and BNP:GetIconSize() or 18
|
||||
local ccSize = BNP.GetCCIconSize and BNP:GetCCIconSize() or auraSize
|
||||
local debuffPosition = BNP.GetDebuffPosition and BNP:GetDebuffPosition() or "top"
|
||||
local ccPosition = BNP.GetCCPosition and BNP:GetCCPosition() or debuffPosition
|
||||
local debuffs = not BNP.AreDebuffsEnabled or BNP:AreDebuffsEnabled()
|
||||
local ccs = not BNP.AreCrowdControlEnabled or BNP:AreCrowdControlEnabled()
|
||||
local rowHeight = 0
|
||||
if debuffs and debuffPosition == position then rowHeight = math.max(rowHeight, auraSize) end
|
||||
if ccs and ccPosition == position then rowHeight = math.max(rowHeight, ccSize) end
|
||||
if rowHeight > 0 then return (rowHeight / 2) + ICON_SPACING + (GetIconSize() / 2) end
|
||||
return 0
|
||||
end
|
||||
|
||||
local function GetRegionWidth(region)
|
||||
if not region then return 0 end
|
||||
if region.IsShown and not region:IsShown() then return 0 end
|
||||
if region.GetStringWidth then
|
||||
local w = tonumber(region:GetStringWidth()) or 0
|
||||
if w > 0 then return w end
|
||||
end
|
||||
if region.GetWidth then return math.max(0, tonumber(region:GetWidth()) or 0) end
|
||||
return 0
|
||||
end
|
||||
|
||||
local function GetRightSpacing(plate)
|
||||
local extra = GetRegionWidth(plate and plate.level) + GetRegionWidth(plate and plate.levelicon)
|
||||
if extra > 0 then extra = extra + 6 end
|
||||
return SIDE_SPACING + extra
|
||||
end
|
||||
|
||||
local function AnchorContainer(plate, container)
|
||||
if not plate or not container then return end
|
||||
container:ClearAllPoints()
|
||||
local position = GetPosition()
|
||||
local anchor = plate.healthbar or plate
|
||||
|
||||
if position == "left" then
|
||||
container:SetPoint("RIGHT", anchor, "LEFT", -SIDE_SPACING, GetSideOffset("left"))
|
||||
elseif position == "right" then
|
||||
container:SetPoint("LEFT", anchor, "RIGHT", GetRightSpacing(plate), GetSideOffset("right"))
|
||||
else
|
||||
local nameHidden = (BNP.IsPlateNameHidden and BNP:IsPlateNameHidden(plate)) or plate.BNPNameHidden
|
||||
local compactShift = nameHidden and -NAME_HIDDEN_COMPACT_SHIFT or 0
|
||||
if plate.healthbar then
|
||||
container:SetPoint("BOTTOM", plate.healthbar, "TOP", 0, GetTopOffset() + compactShift)
|
||||
else
|
||||
container:SetPoint("BOTTOM", plate, "BOTTOM", 0, 24 + GetTopOffset() + compactShift)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function BNP:RefreshImmunityLayoutForPlate(plate)
|
||||
if not plate or not plate.BNPImmunityContainer then return end
|
||||
AnchorContainer(plate, plate.BNPImmunityContainer)
|
||||
end
|
||||
|
||||
local function ApplyFrameLevel(plate, container)
|
||||
if not plate or not container then return end
|
||||
if plate.GetFrameStrata and container.GetFrameStrata and container.SetFrameStrata then
|
||||
local strata = plate:GetFrameStrata()
|
||||
if strata and container:GetFrameStrata() ~= strata then
|
||||
container:SetFrameStrata(strata)
|
||||
end
|
||||
end
|
||||
if plate.GetFrameLevel and container.GetFrameLevel and container.SetFrameLevel then
|
||||
local level = math.max(0, (tonumber(plate:GetFrameLevel()) or 0) + 1)
|
||||
if container:GetFrameLevel() ~= level then container:SetFrameLevel(level) end
|
||||
local i
|
||||
for i = 1, table.getn(container.icons or {}) do
|
||||
local icon = container.icons[i]
|
||||
if icon and icon.GetFrameLevel and icon.SetFrameLevel and icon:GetFrameLevel() ~= level then
|
||||
icon:SetFrameLevel(level)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function CreateIcon(parent, index)
|
||||
local size = GetIconSize()
|
||||
local icon = CreateFrame("Frame", nil, parent)
|
||||
icon:SetWidth(size)
|
||||
icon:SetHeight(size)
|
||||
|
||||
local texture = icon:CreateTexture(nil, "ARTWORK")
|
||||
texture:SetAllPoints(icon)
|
||||
icon.texture = texture
|
||||
|
||||
local timer = icon:CreateFontString(nil, "OVERLAY")
|
||||
timer:SetFont(UI.TIMER_FONT or "Fonts\\FRIZQT__.TTF", UI.TIMER_SIZE or 8, "OUTLINE")
|
||||
timer:SetPoint("CENTER", icon, "CENTER", UI.TIMER_OFFSET_X or 0, UI.TIMER_OFFSET_Y or 0)
|
||||
timer:SetTextColor(1, 1, 1)
|
||||
icon.timer = timer
|
||||
|
||||
icon.index = index
|
||||
icon:Hide()
|
||||
return icon
|
||||
end
|
||||
|
||||
local function LayoutContainer(plate, container)
|
||||
if not plate or not container then return end
|
||||
local size = GetIconSize()
|
||||
local position = GetPosition()
|
||||
local i
|
||||
|
||||
if container.BNPLastSize ~= size or container.BNPLastIconPosition ~= position then
|
||||
container.BNPLastSize = size
|
||||
container.BNPLastIconPosition = position
|
||||
container:SetWidth(size * MAX_ICONS + ICON_SPACING * (MAX_ICONS - 1))
|
||||
container:SetHeight(size)
|
||||
for i = 1, table.getn(container.icons or {}) do
|
||||
local icon = container.icons[i]
|
||||
icon:SetWidth(size)
|
||||
icon:SetHeight(size)
|
||||
icon:ClearAllPoints()
|
||||
if position == "left" then
|
||||
icon:SetPoint("RIGHT", container, "RIGHT", -((i - 1) * (size + ICON_SPACING)), 0)
|
||||
else
|
||||
icon:SetPoint("LEFT", container, "LEFT", (i - 1) * (size + ICON_SPACING), 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Cheap layout key; avoids touching anchors every scan.
|
||||
local layoutKey = tostring(position) .. ":" ..
|
||||
tostring(BNP.GetDebuffPosition and BNP:GetDebuffPosition() or "top") .. ":" ..
|
||||
tostring(BNP.GetCCPosition and BNP:GetCCPosition() or "top") .. ":" ..
|
||||
tostring(BNP.IsSeparateCCRowEnabled and BNP:IsSeparateCCRowEnabled()) .. ":" ..
|
||||
tostring(BNP.GetIconSize and BNP:GetIconSize() or 18) .. ":" ..
|
||||
tostring(BNP.GetCCIconSize and BNP:GetCCIconSize() or 18) .. ":" .. tostring(size) .. ":" ..
|
||||
tostring(plate.BNPNameHidden and true or false) .. ":" ..
|
||||
tostring(FrameShown(plate.BNPAuraContainer)) .. ":" ..
|
||||
tostring(FrameShown(plate.BNPCCContainer))
|
||||
|
||||
if container.BNPLastLayoutKey ~= layoutKey then
|
||||
container.BNPLastLayoutKey = layoutKey
|
||||
AnchorContainer(plate, container)
|
||||
end
|
||||
ApplyFrameLevel(plate, container)
|
||||
end
|
||||
|
||||
local function HideContainer(plate)
|
||||
local container = plate and plate.BNPImmunityContainer
|
||||
if not container then return end
|
||||
local i
|
||||
for i = 1, table.getn(container.icons or {}) do container.icons[i]:Hide() end
|
||||
container:Hide()
|
||||
end
|
||||
|
||||
local function InstallHideGuard(plate)
|
||||
if not plate or plate.BNPImmunityHideGuard then return end
|
||||
local oldOnHide = plate:GetScript("OnHide")
|
||||
plate:SetScript("OnHide", function()
|
||||
if oldOnHide then oldOnHide() end
|
||||
HideContainer(this or plate)
|
||||
end)
|
||||
plate.BNPImmunityHideGuard = true
|
||||
end
|
||||
|
||||
local function CreateContainer(plate)
|
||||
if not plate or plate.BNPImmunityContainer then return end
|
||||
if BNP.RegisterPlate then BNP:RegisterPlate(plate) end
|
||||
|
||||
local container = CreateFrame("Frame", nil, GetVisualParent(plate))
|
||||
container.icons = {}
|
||||
local i
|
||||
for i = 1, MAX_ICONS do container.icons[i] = CreateIcon(container, i) end
|
||||
plate.BNPImmunityContainer = container
|
||||
LayoutContainer(plate, container)
|
||||
container:Hide()
|
||||
InstallHideGuard(plate)
|
||||
end
|
||||
|
||||
local function GetCache(guid)
|
||||
if not guid then return nil end
|
||||
local cache = BNP.guidImmunities[guid]
|
||||
if not cache then
|
||||
cache = { _nextOrder = 0 }
|
||||
BNP.guidImmunities[guid] = cache
|
||||
end
|
||||
return cache
|
||||
end
|
||||
|
||||
local function CommitSeen(guid, def, spellID, texture, duration, expires, now, source)
|
||||
if not guid or not def then return end
|
||||
local cache = GetCache(guid)
|
||||
local entry = cache[def.key]
|
||||
if not entry then
|
||||
cache._nextOrder = (cache._nextOrder or 0) + 1
|
||||
entry = { order = cache._nextOrder }
|
||||
cache[def.key] = entry
|
||||
end
|
||||
|
||||
entry.spellID = tonumber(spellID) or entry.spellID
|
||||
entry.texture = texture or entry.texture
|
||||
entry.duration = tonumber(duration) or entry.duration or DefDuration(def, tonumber(spellID))
|
||||
entry.lastSeen = now
|
||||
entry.missingSince = nil
|
||||
if source == "capi" then
|
||||
entry.capiConfirmed = true
|
||||
entry.capiMissingSince = nil
|
||||
elseif source == "unitbuff" then
|
||||
entry.unitBuffConfirmed = true
|
||||
entry.unitBuffMissingSince = nil
|
||||
end
|
||||
|
||||
expires = tonumber(expires)
|
||||
if expires and expires > now then
|
||||
entry.expires = expires
|
||||
elseif not entry.expires or entry.expires <= now then
|
||||
local d = entry.duration or DefDuration(def, entry.spellID)
|
||||
if d then entry.expires = now + d end
|
||||
end
|
||||
end
|
||||
|
||||
-- These full immunities also cleanse harmful effects in Classic. Keep the
|
||||
-- harmful-debuff module hard-suppressed while the immunity entry is active.
|
||||
local CLEANSE_ALL = {
|
||||
divine_shield = true,
|
||||
ice_block = true,
|
||||
}
|
||||
|
||||
local function ApplyCleanseSuppression(guid, def, entry, now)
|
||||
if not guid or not def or not CLEANSE_ALL[def.key] then return end
|
||||
if not entry then return end
|
||||
local remaining = entry.expires and (entry.expires - now) or nil
|
||||
if remaining and remaining <= 0 then return end
|
||||
if BNP.SuppressHarmfulAurasForImmunity then
|
||||
-- Renewable short hold: immediate cleanse now, but if Ice Block/Bubble is
|
||||
-- clicked off early the suppression naturally expires within a fraction
|
||||
-- of a second instead of hiding new debuffs until the guessed full timer.
|
||||
BNP:SuppressHarmfulAurasForImmunity(guid, 0.45)
|
||||
end
|
||||
end
|
||||
|
||||
local function ParseDurationExpiry(now, a5, a6, a7, a8, a9, a10)
|
||||
local vals = { a5, a6, a7, a8, a9, a10 }
|
||||
local i
|
||||
for i = 1, table.getn(vals) - 1 do
|
||||
local d = tonumber(vals[i])
|
||||
local e = tonumber(vals[i + 1])
|
||||
if d and e and d > 0 and d <= 3600 and e > now then return d, e end
|
||||
end
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local function ReconcileMissingSource(guid, source, seen, now)
|
||||
local cache = guid and BNP.guidImmunities[guid] or nil
|
||||
if not cache then return end
|
||||
|
||||
local d, def, entry, confirmedKey, missingKey
|
||||
if source == "capi" then
|
||||
confirmedKey = "capiConfirmed"
|
||||
missingKey = "capiMissingSince"
|
||||
else
|
||||
confirmedKey = "unitBuffConfirmed"
|
||||
missingKey = "unitBuffMissingSince"
|
||||
end
|
||||
|
||||
for d = 1, table.getn(IMMUNITY_DEFS) do
|
||||
def = IMMUNITY_DEFS[d]
|
||||
entry = cache[def.key]
|
||||
if entry and entry[confirmedKey] then
|
||||
if seen and seen[def.key] then
|
||||
entry[missingKey] = nil
|
||||
else
|
||||
if not entry[missingKey] then entry[missingKey] = now end
|
||||
if now - entry[missingKey] >= MISSING_GRACE then
|
||||
-- Once a live aura source has positively confirmed this exact
|
||||
-- immunity, that same source becomes authoritative for early
|
||||
-- removals (Ice Block clicked off, Bubble canceled/removed, dispel).
|
||||
cache[def.key] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ScanClassicAPIBuffs(unit, guid, now)
|
||||
if not unit or not guid then return false end
|
||||
if type(C_UnitAuras) ~= "table" then return false end
|
||||
|
||||
local seen = {}
|
||||
|
||||
-- Preferred path: ask ClassicAPI directly for the whitelisted spell IDs.
|
||||
-- Its aura cache is evicted by the client's real OnAuraRemoved hook, so an
|
||||
-- Ice Block that is clicked off disappears here immediately instead of
|
||||
-- living until the guessed cast timer ends.
|
||||
if type(C_UnitAuras.GetUnitAuraBySpellID) == "function" then
|
||||
local anyCallWorked = false
|
||||
local d, j, def, sid, ok, aura
|
||||
for d = 1, table.getn(IMMUNITY_DEFS) do
|
||||
def = IMMUNITY_DEFS[d]
|
||||
if def.spellIDs then
|
||||
for j = 1, table.getn(def.spellIDs) do
|
||||
sid = def.spellIDs[j]
|
||||
ok, aura = pcall(C_UnitAuras.GetUnitAuraBySpellID, unit, sid, "HELPFUL")
|
||||
if ok then
|
||||
anyCallWorked = true
|
||||
if type(aura) == "table" then
|
||||
local auraID = tonumber(aura.spellId or aura.spellID) or sid
|
||||
local matched = FindDef(auraID, aura.name, aura.icon or aura.texture)
|
||||
if matched then
|
||||
seen[matched.key] = true
|
||||
CommitSeen(guid, matched, auraID, aura.icon or aura.texture,
|
||||
tonumber(aura.duration), tonumber(aura.expirationTime), now, "capi")
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
-- If this build rejects the exact getter, fall back to index scan
|
||||
-- below and never use a failed call as negative evidence.
|
||||
anyCallWorked = false
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if not anyCallWorked and d == 1 then break end
|
||||
end
|
||||
|
||||
if anyCallWorked then
|
||||
ReconcileMissingSource(guid, "capi", seen, now)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local getter = nil
|
||||
local useFilter = false
|
||||
if type(C_UnitAuras.GetBuffDataByIndex) == "function" then
|
||||
getter = C_UnitAuras.GetBuffDataByIndex
|
||||
elseif type(C_UnitAuras.GetAuraDataByIndex) == "function" then
|
||||
getter = C_UnitAuras.GetAuraDataByIndex
|
||||
useFilter = true
|
||||
end
|
||||
if not getter then return false end
|
||||
|
||||
local completed = true
|
||||
local i
|
||||
for i = 1, 48 do
|
||||
local ok, aura
|
||||
if useFilter then
|
||||
ok, aura = pcall(getter, unit, i, "HELPFUL")
|
||||
else
|
||||
ok, aura = pcall(getter, unit, i)
|
||||
end
|
||||
if not ok then completed = false break end
|
||||
if not aura then break end
|
||||
|
||||
if type(aura) == "table" then
|
||||
local spellID = tonumber(aura.spellId or aura.spellID)
|
||||
local def = FindDef(spellID, aura.name, aura.icon or aura.texture)
|
||||
if def then
|
||||
seen[def.key] = true
|
||||
CommitSeen(guid, def, spellID, aura.icon or aura.texture,
|
||||
tonumber(aura.duration), tonumber(aura.expirationTime), now, "capi")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if completed then ReconcileMissingSource(guid, "capi", seen, now) end
|
||||
return completed
|
||||
end
|
||||
|
||||
local function ScanUnitBuffs(unit, guid, now)
|
||||
if not unit or not guid or not UnitBuff then return false end
|
||||
|
||||
local seen = {}
|
||||
local completed = true
|
||||
local i
|
||||
for i = 1, 64 do
|
||||
local ok, texture, applications, buffID, a4, a5, a6, a7, a8, a9, a10 = pcall(UnitBuff, unit, i)
|
||||
if not ok then completed = false break end
|
||||
if not texture then break end
|
||||
|
||||
local spellID = tonumber(buffID)
|
||||
if not spellID then
|
||||
if type(a4) == "number" then spellID = a4
|
||||
elseif type(a5) == "number" then spellID = a5 end
|
||||
end
|
||||
if spellID and spellID < -1 then spellID = spellID + 65536 end
|
||||
|
||||
local auraName = nil
|
||||
if spellID and SpellInfo then auraName = SpellInfo(spellID) end
|
||||
local def = FindDef(spellID, auraName, texture)
|
||||
if def then
|
||||
seen[def.key] = true
|
||||
local duration, expires = ParseDurationExpiry(now, a4, a5, a6, a7, a8, a9, a10)
|
||||
CommitSeen(guid, def, spellID, texture, duration, expires, now, "unitbuff")
|
||||
end
|
||||
end
|
||||
|
||||
-- UnitBuff is only allowed to remove an immunity after UnitBuff itself has
|
||||
-- positively seen that exact whitelist entry at least once. This preserves
|
||||
-- the cast-event fallback on clients where hostile helpful auras are hidden,
|
||||
-- while still making clicked-off buffs disappear immediately when visible.
|
||||
if completed then ReconcileMissingSource(guid, "unitbuff", seen, now) end
|
||||
return completed
|
||||
end
|
||||
|
||||
local function ResolveClassicAuraUnit(guid, targetGUID)
|
||||
if not guid then return nil end
|
||||
if targetGUID and guid == targetGUID then return "target" end
|
||||
|
||||
if type(UnitTokenFromGUID) == "function" then
|
||||
local ok, token = pcall(UnitTokenFromGUID, guid)
|
||||
if ok and token and token ~= "" then return token end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function FormatTimer(remaining)
|
||||
if not remaining or remaining <= 0 then return "" end
|
||||
if remaining >= 3600 then return math.ceil(remaining / 3600) .. "h" end
|
||||
if remaining >= 60 then return math.ceil(remaining / 60) .. "m" end
|
||||
return tostring(math.ceil(remaining))
|
||||
end
|
||||
|
||||
local function Render(plate, guid, now)
|
||||
local container = plate and plate.BNPImmunityContainer
|
||||
if not container then return end
|
||||
LayoutContainer(plate, container)
|
||||
|
||||
local cache = guid and BNP.guidImmunities[guid] or nil
|
||||
local active = {}
|
||||
local d, def, entry, remaining
|
||||
for d = 1, table.getn(IMMUNITY_DEFS) do
|
||||
def = IMMUNITY_DEFS[d]
|
||||
entry = cache and cache[def.key] or nil
|
||||
remaining = entry and entry.expires and (entry.expires - now) or nil
|
||||
if entry and (not entry.expires or (remaining and remaining > 0)) then
|
||||
ApplyCleanseSuppression(guid, def, entry, now)
|
||||
if table.getn(active) < MAX_ICONS then
|
||||
table.insert(active, { def = def, entry = entry, remaining = remaining })
|
||||
end
|
||||
elseif cache and entry and entry.expires and remaining and remaining <= 0 then
|
||||
cache[def.key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
if table.getn(active) > 1 then
|
||||
table.sort(active, function(a, b) return (a.entry.order or 0) < (b.entry.order or 0) end)
|
||||
end
|
||||
|
||||
local count = table.getn(active)
|
||||
local size = GetIconSize()
|
||||
local visibleWidth = size
|
||||
if count > 1 then visibleWidth = size * count + ICON_SPACING * (count - 1) end
|
||||
container:SetWidth(visibleWidth)
|
||||
|
||||
local i
|
||||
for i = 1, MAX_ICONS do
|
||||
local icon = container.icons[i]
|
||||
local item = active[i]
|
||||
if item then
|
||||
local texture = item.entry.texture
|
||||
if not texture and item.entry.spellID and SpellInfo then
|
||||
local _, _, spellTexture = SpellInfo(item.entry.spellID)
|
||||
texture = spellTexture
|
||||
end
|
||||
if texture then icon.texture:SetTexture(texture) end
|
||||
icon.timer:SetText(FormatTimer(item.remaining))
|
||||
icon:Show()
|
||||
else
|
||||
icon.timer:SetText("")
|
||||
icon:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
if count > 0 then container:Show() else container:Hide() end
|
||||
end
|
||||
|
||||
local function GetTargetGUID()
|
||||
if not UnitExists then return nil end
|
||||
local exists, guid = UnitExists("target")
|
||||
if exists and guid then return guid end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function GetPlateGUID(plate)
|
||||
if not plate or not plate.GetName then return nil end
|
||||
local token = plate:GetName(1)
|
||||
if token and token ~= "" then return token end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function UpdatePlate(plate)
|
||||
if not plate or not plate:IsShown() then
|
||||
HideContainer(plate)
|
||||
return
|
||||
end
|
||||
if BNP.ArePvPImmunitiesEnabled and not BNP:ArePvPImmunitiesEnabled() then
|
||||
HideContainer(plate)
|
||||
return
|
||||
end
|
||||
if not plate.BNPImmunityContainer then CreateContainer(plate) end
|
||||
|
||||
local now = GetTime()
|
||||
local guid = BNP.GetStablePlateAuraGUID and BNP.GetStablePlateAuraGUID(plate, now) or GetPlateGUID(plate)
|
||||
if not guid then HideContainer(plate) return end
|
||||
|
||||
local targetGUID = GetTargetGUID()
|
||||
local unit = nil
|
||||
if targetGUID and targetGUID == guid then
|
||||
unit = "target"
|
||||
else
|
||||
unit = GetPlateGUID(plate)
|
||||
end
|
||||
|
||||
-- Scan the two aura surfaces independently. ClassicAPI nameplateN tokens are
|
||||
-- preferred when available; SuperWoW's raw GUID token remains the legacy
|
||||
-- UnitBuff fallback. Do not let one resolver's player check block the other.
|
||||
local classicUnit = ResolveClassicAuraUnit(guid, targetGUID)
|
||||
local classicCanScan = classicUnit ~= nil
|
||||
if classicCanScan and UnitIsPlayer then
|
||||
classicCanScan = UnitIsPlayer(classicUnit) and true or false
|
||||
end
|
||||
if classicCanScan then ScanClassicAPIBuffs(classicUnit, guid, now) end
|
||||
|
||||
local legacyCanScan = unit ~= nil
|
||||
if legacyCanScan and UnitIsPlayer then
|
||||
legacyCanScan = UnitIsPlayer(unit) and true or false
|
||||
end
|
||||
if legacyCanScan then ScanUnitBuffs(unit, guid, now) end
|
||||
|
||||
Render(plate, guid, now)
|
||||
end
|
||||
|
||||
-- UNIT_CASTEVENT is a fast timer seed. Once ClassicAPI/UnitBuff has
|
||||
-- positively confirmed the aura, that live source also removes it early when
|
||||
-- the buff is canceled, dispelled, or otherwise ends before its timer.
|
||||
local castFrame = CreateFrame("Frame")
|
||||
castFrame:RegisterEvent("UNIT_CASTEVENT")
|
||||
local function HandleImmunityCastEvent()
|
||||
local casterGUID = arg1
|
||||
local targetGUID = arg2
|
||||
local eventType = arg3
|
||||
local spellID = tonumber(arg4)
|
||||
if spellID and spellID < -1 then spellID = spellID + 65536 end
|
||||
|
||||
-- Keep the last raw event for /bnpimm diagnostics. This is intentionally
|
||||
-- cheap and lets us verify instant enemy casts without another addon.
|
||||
BNP._immLastCast = {
|
||||
casterGUID = casterGUID,
|
||||
targetGUID = targetGUID,
|
||||
eventType = eventType,
|
||||
spellID = spellID,
|
||||
time = GetTime(),
|
||||
}
|
||||
|
||||
if not casterGUID or not spellID then return end
|
||||
if eventType ~= "CAST" and eventType ~= "CHANNEL" then return end
|
||||
|
||||
local texture = nil
|
||||
local spellName = nil
|
||||
if SpellInfo then
|
||||
spellName, _, texture = SpellInfo(spellID)
|
||||
end
|
||||
local def = DEF_BY_ID[spellID] or FindDef(spellID, spellName, texture)
|
||||
if not def then return end
|
||||
|
||||
local guid = def.selfOnly and casterGUID or (targetGUID or casterGUID)
|
||||
local now = GetTime()
|
||||
local duration = DefDuration(def, spellID)
|
||||
if duration then
|
||||
CommitSeen(guid, def, spellID, texture, duration, now + duration, now)
|
||||
local cache = BNP.guidImmunities and BNP.guidImmunities[guid]
|
||||
ApplyCleanseSuppression(guid, def, cache and cache[def.key], now)
|
||||
end
|
||||
end
|
||||
|
||||
castFrame:SetScript("OnEvent", function()
|
||||
local ok, err = pcall(HandleImmunityCastEvent)
|
||||
if not ok then ReportImmunityError("cast", err) end
|
||||
end)
|
||||
|
||||
function BNP:RefreshImmunityVisibility()
|
||||
local plate
|
||||
for plate in pairs(BNP.plates or {}) do
|
||||
if plate then
|
||||
if self:ArePvPImmunitiesEnabled() then
|
||||
if not plate.BNPImmunityContainer then CreateContainer(plate) end
|
||||
else
|
||||
HideContainer(plate)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function BNP:RefreshAllImmunityLayouts()
|
||||
local plate
|
||||
for plate in pairs(BNP.plates or {}) do
|
||||
if plate and plate.BNPImmunityContainer then
|
||||
plate.BNPImmunityContainer.BNPLastLayoutKey = nil
|
||||
plate.BNPImmunityContainer.BNPLastSize = nil
|
||||
plate.BNPImmunityContainer.BNPLastIconPosition = nil
|
||||
LayoutContainer(plate, plate.BNPImmunityContainer)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Small temporary diagnostic for this test build. Target a bubbled player and
|
||||
-- type /bnpimm. It prints the raw UnitBuff slots BNP sees, including spell ID
|
||||
-- when SuperWoW exposes it. This makes server/client differences obvious without
|
||||
-- relying on another addon's truncated error window.
|
||||
SLASH_BNPIMMUNITYDEBUG1 = "/bnpimm"
|
||||
SlashCmdList["BNPIMMUNITYDEBUG"] = function()
|
||||
if not DEFAULT_CHAT_FRAME then return end
|
||||
local exists, guid = UnitExists("target")
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff33ff99BNP IMM DEBUG|r targetGUID=" .. tostring(guid or "nil") .. " player=" .. tostring(UnitIsPlayer and UnitIsPlayer("target")))
|
||||
|
||||
local last = BNP._immLastCast
|
||||
if last then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(" last CASTEVENT type=" .. tostring(last.eventType) .. " id=" .. tostring(last.spellID) .. " caster=" .. tostring(last.casterGUID) .. " target=" .. tostring(last.targetGUID))
|
||||
else
|
||||
DEFAULT_CHAT_FRAME:AddMessage(" last CASTEVENT: none seen")
|
||||
end
|
||||
|
||||
if not exists then return end
|
||||
|
||||
local capiFound = 0
|
||||
if type(C_UnitAuras) == "table" then
|
||||
local getter = nil
|
||||
local useFilter = false
|
||||
if type(C_UnitAuras.GetBuffDataByIndex) == "function" then
|
||||
getter = C_UnitAuras.GetBuffDataByIndex
|
||||
elseif type(C_UnitAuras.GetAuraDataByIndex) == "function" then
|
||||
getter = C_UnitAuras.GetAuraDataByIndex
|
||||
useFilter = true
|
||||
end
|
||||
DEFAULT_CHAT_FRAME:AddMessage(" ClassicAPI aura getter=" .. tostring(getter ~= nil))
|
||||
if getter then
|
||||
local i
|
||||
for i = 1, 16 do
|
||||
local ok, aura
|
||||
if useFilter then ok, aura = pcall(getter, "target", i, "HELPFUL")
|
||||
else ok, aura = pcall(getter, "target", i) end
|
||||
if not ok then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(" CAPI error slot " .. tostring(i) .. ": " .. tostring(aura))
|
||||
break
|
||||
end
|
||||
if not aura then break end
|
||||
if type(aura) == "table" then
|
||||
local sid = tonumber(aura.spellId or aura.spellID)
|
||||
local def = FindDef(sid, aura.name, aura.icon or aura.texture)
|
||||
if def then capiFound = capiFound + 1 end
|
||||
if def then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(" CAPI MATCH " .. tostring(i) .. " id=" .. tostring(sid) .. " name=" .. tostring(aura.name) .. " tex=" .. tostring(aura.icon or aura.texture))
|
||||
end
|
||||
else
|
||||
DEFAULT_CHAT_FRAME:AddMessage(" CAPI slot " .. tostring(i) .. " returned " .. type(aura) .. ": " .. tostring(aura))
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
DEFAULT_CHAT_FRAME:AddMessage(" ClassicAPI C_UnitAuras: unavailable")
|
||||
end
|
||||
|
||||
local ubFound = 0
|
||||
local i
|
||||
for i = 1, 32 do
|
||||
local ok, texture, stacks, buffID, a4, a5, a6 = pcall(UnitBuff, "target", i)
|
||||
if not ok then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(" UnitBuff error slot " .. tostring(i) .. ": " .. tostring(texture))
|
||||
break
|
||||
end
|
||||
if not texture then break end
|
||||
local spellID = tonumber(buffID)
|
||||
if not spellID and type(a4) == "number" then spellID = a4 end
|
||||
if spellID and spellID < -1 then spellID = spellID + 65536 end
|
||||
local def = FindDef(spellID, spellID and SpellInfo and SpellInfo(spellID) or nil, texture)
|
||||
if def then ubFound = ubFound + 1 end
|
||||
if def then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(" UB MATCH " .. tostring(i) .. " id=" .. tostring(spellID) .. " tex=" .. tostring(texture))
|
||||
end
|
||||
end
|
||||
if capiFound == 0 and ubFound == 0 then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(" no configured immunity matched live target buffs")
|
||||
end
|
||||
end
|
||||
|
||||
-- Sanity-check the whole module callback so one optional PvP feature can never
|
||||
-- stop any other BNP nameplate module.
|
||||
local function SafeCreateContainer(plate)
|
||||
local ok, err = pcall(CreateContainer, plate)
|
||||
if not ok then ReportImmunityError("create", err) end
|
||||
end
|
||||
|
||||
local function SafeUpdatePlate(plate)
|
||||
local ok, err = pcall(UpdatePlate, plate)
|
||||
if not ok then ReportImmunityError("update", err) end
|
||||
end
|
||||
|
||||
table.insert(BNP.libnameplate.OnInit, SafeCreateContainer)
|
||||
table.insert(BNP.libnameplate.OnShow, SafeCreateContainer)
|
||||
table.insert(BNP.libnameplate.OnUpdate, SafeUpdatePlate)
|
||||
@@ -1,134 +0,0 @@
|
||||
BNP = BNP or {}
|
||||
|
||||
-- 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
UpdatePlateName(current, false)
|
||||
end)
|
||||
end
|
||||
@@ -1,353 +0,0 @@
|
||||
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 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) > 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
|
||||
)
|
||||
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
|
||||
|
||||
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)
|
||||
end)
|
||||
|
||||
wrapper:SetScript("OnLeave", function()
|
||||
local current = this or wrapper
|
||||
local parent = current and current.plate or plate
|
||||
SetHoverGlow(parent, false)
|
||||
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
|
||||
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: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
|
||||
@@ -1,67 +0,0 @@
|
||||
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
|
||||
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)
|
||||
@@ -1,227 +0,0 @@
|
||||
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
|
||||
@@ -1,181 +0,0 @@
|
||||
BNP = BNP or {}
|
||||
|
||||
-- Tank Mode for SuperWoW GUID nameplates.
|
||||
-- GREEN = hostile NPC currently targets the player.
|
||||
-- RED = hostile NPC currently targets another unit.
|
||||
-- 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)
|
||||
|
||||
if targetGUID and targetGUID == myGUID then
|
||||
ApplyTankColor(plate, bar, GREEN_R, GREEN_G, GREEN_B)
|
||||
elseif targetGUID then
|
||||
ApplyTankColor(plate, bar, RED_R, RED_G, RED_B)
|
||||
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
|
||||
BNP:UpdateTankModePlate(current)
|
||||
EnforceTankColor(current)
|
||||
end)
|
||||
|
||||
self.BNPTankModeInstalled = true
|
||||
end
|
||||
@@ -1,368 +0,0 @@
|
||||
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. Standard Classic totems above
|
||||
-- keep their proper icon; unknown names containing "totem" use a neutral
|
||||
-- fallback icon instead of leaving the full nameplate visible.
|
||||
if 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 function SuppressTotemPlateVisuals(plate)
|
||||
if not plate then return end
|
||||
|
||||
local indicator = plate.BNPTotemIndicator
|
||||
local state = plate.BNPTotemVisualState
|
||||
if not state then
|
||||
state = { alpha = {}, indicator = indicator }
|
||||
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 then plate:SetAlpha(1) end
|
||||
|
||||
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
|
||||
|
||||
-- BNPTotemIndicator is itself a direct child of the plate.
|
||||
if indicator and indicator.SetAlpha 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
|
||||
current.BNPTotemIndicator:SetAlpha(1)
|
||||
current.BNPTotemIndicator:Show()
|
||||
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)
|
||||
if not force and name == plate.BNPTotemLastName then
|
||||
if plate.BNPTotemLastKey then
|
||||
indicator:Show()
|
||||
SuppressTotemPlateVisuals(plate)
|
||||
else
|
||||
indicator:Hide()
|
||||
RestoreTotemPlateVisuals(plate)
|
||||
end
|
||||
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)
|
||||
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)
|
||||
@@ -1,498 +0,0 @@
|
||||
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
|
||||
Reference in New Issue
Block a user