Delete modules directory

This commit is contained in:
Wurmschwanz
2026-08-12 10:41:09 +02:00
committed by GitHub
parent 81a9e3f39f
commit ea774c6843
10 changed files with 0 additions and 1967 deletions
-606
View File
@@ -1,606 +0,0 @@
if not BNP.libnameplate then return end
if not CombatLogAdd or not SpellInfo then return end
local _, playerClass = UnitClass("player")
local UI = BNP.UI or {}
local ICON_SIZE = UI.ICON_SIZE or 18
local function GetIconSize()
return (BNP.GetIconSize and BNP:GetIconSize()) or ICON_SIZE
end
local ICON_OFFSET_Y = UI.ICON_OFFSET_Y or 15
local ICON_SPACING = UI.ICON_SPACING or 2
local UPDATE_INTERVAL = 0.05
-- SuperWoW GUID cache. Auras are learned while a unit is the current target
-- and remain attached to that unit's unique GUID after the target changes.
BNP.guidAuras = BNP.guidAuras or {}
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 MAX_VISIBLE_ICONS = 5
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 NameMatches(def, name)
if not name then return false end
local lower = string.lower(name)
for _, known in ipairs(def.names) do
if lower == string.lower(known) then return true end
end
return false
end
local function AuraMatches(def, effect, texture)
if NameMatches(def, effect) then return true end
if texture and def.textureMatch then
return string.find(string.lower(texture), def.textureMatch) and true or false
end
return false
end
local function ScanSpellbook()
if table.getn(AURA_DEFS) == 0 then return end
local i = 1
while true do
local name = GetSpellName(i, BOOKTYPE_SPELL)
if not name then break end
local texture = GetSpellTexture(i, BOOKTYPE_SPELL)
local lowerTexture = texture and string.lower(texture) or ""
for _, def in ipairs(AURA_DEFS) do
if NameMatches(def, name) or (def.textureMatch and string.find(lowerTexture, def.textureMatch)) then
def.localizedName = name
def.texture = texture or def.texture
end
end
i = i + 1
end
if BNP.RefreshSpellbookDurations then BNP:RefreshSpellbookDurations() end
end
local function CreateAuraIcon(parent, index)
local icon = CreateFrame("Frame", nil, parent)
local size = GetIconSize()
icon:SetWidth(size)
icon:SetHeight(size)
icon:SetFrameLevel(40)
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 CreateAuraContainer(plate)
if not plate then return end
BNP:RegisterPlate(plate)
if plate.BNPAuraContainer then return end
local size = GetIconSize()
local width = size * MAX_VISIBLE_ICONS + ICON_SPACING * (MAX_VISIBLE_ICONS - 1)
local container = CreateFrame("Frame", nil, plate)
container:SetWidth(width)
container:SetHeight(size)
container:SetFrameLevel(40)
if plate.healthbar then
container:SetPoint("BOTTOM", plate.healthbar, "TOP", 0, ICON_OFFSET_Y)
else
container:SetPoint("BOTTOM", plate, "BOTTOM", 0, 24)
end
container.icons = {}
for i = 1, MAX_VISIBLE_ICONS do
local icon = CreateAuraIcon(container, i)
if i == 1 then
icon:SetPoint("LEFT", container, "LEFT", 0, 0)
else
icon:SetPoint("LEFT", container.icons[i - 1], "RIGHT", ICON_SPACING, 0)
end
container.icons[i] = icon
end
container:Hide()
plate.BNPAuraContainer = container
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
return nil
end
local function GetTargetGUID()
local exists, guid = UnitExists("target")
if exists and guid then return guid end
return nil
end
-- Only successful casts by the local player are allowed to create aura
-- entries. This prevents target scans from importing debuffs cast by other
-- players. SuperWoW supplies caster GUID, target GUID and spell ID.
local function GetPlayerGUID()
local exists, guid = UnitExists("player")
if exists and guid then return guid end
return nil
end
local function FindAuraDef(spellID)
if not spellID or not SpellInfo then return nil, nil end
local spellName, _, texture = SpellInfo(spellID)
if not spellName and not texture then return nil, nil end
for _, def in ipairs(AURA_DEFS) do
if AuraMatches(def, spellName, texture) then
return def, texture, spellID
end
end
return nil, nil
end
-- Turtle WoW Dark Harvest (spell 52552).
-- It is reported by SuperWoW as CHANNEL; arg5 is channel length in ms.
-- A 30% shorter tick interval means aura time advances at 1/0.70 speed.
local DARK_HARVEST_SPELL_ID = 52552
local DARK_HARVEST_SPEED = 1 / 0.70
local DARK_HARVEST_AURAS = {
corruption=true, curse_of_agony=true, siphon_life=true,
curse_of_doom=true, drain_life=true, drain_soul=true,
}
BNP.darkHarvest = nil
local function StartDarkHarvest(guid, durationMS)
local now=GetTime()
local duration=tonumber(durationMS) or 8000
if duration > 100 then duration=duration/1000 end
BNP.darkHarvest={guid=guid, lastUpdate=now, ends=now+duration}
end
local function UpdateDarkHarvest(now)
local st=BNP.darkHarvest
if not st then return end
local untilTime=now
if untilTime > st.ends then untilTime=st.ends end
local delta=untilTime-st.lastUpdate
if delta > 0 then
local cache=BNP.guidAuras[st.guid]
if cache then
local bonus=delta*(DARK_HARVEST_SPEED-1)
local key,aura
for key,aura in pairs(cache) do
if DARK_HARVEST_AURAS[key] and type(aura)=="table" and aura.expires and aura.expires > st.lastUpdate then
aura.expires=aura.expires-bonus
end
end
end
st.lastUpdate=untilTime
end
if now >= st.ends then BNP.darkHarvest=nil end
end
-- Cast confirmation ---------------------------------------------------------
-- UNIT_CASTEVENT "CAST" means the player finished the cast, but it does not
-- guarantee that the hostile aura actually landed. Resist/Miss/Immune must
-- therefore never create a visible timer.
--
-- We first remember the cast as "pending". It becomes a real aura only after
-- UnitDebuff confirms the same spell ID on that exact SuperWoW GUID.
BNP.pendingAuras = BNP.pendingAuras or {}
local PENDING_TIMEOUT = 2.0
local function HasPendingAuras()
local guid, entries, key, pending
for guid, entries in pairs(BNP.pendingAuras) do
for key, pending in pairs(entries) do
if type(pending) == "table" then
return true
end
end
end
return false
end
local function GetUnitTokenForPlate(plate)
if not plate or not plate.GetName then return nil end
return plate:GetName(1)
end
local function ConfirmedSpellOnUnit(unit, spellID)
if not unit or not spellID then return false end
local i
for i = 1, 32 do
local texture, stacks, dtype, auraSpellID = UnitDebuff(unit, i)
if not texture then break end
if auraSpellID == spellID then
return true
end
end
return false
end
local function CommitPendingAura(guid, key, pending)
if not guid or not pending then return end
local def = pending.def
local cache = BNP.guidAuras[guid]
if not cache then
cache = {}
BNP.guidAuras[guid] = cache
end
-- Only replace mutually-exclusive curses after the new curse is confirmed
-- on the target. A resisted curse must never remove the existing one.
if def.category == "CURSE" and def.curseExclusive ~= false then
local _, oldDef
for _, oldDef in ipairs(AURA_DEFS) do
if oldDef.category == "CURSE" and oldDef.curseExclusive ~= false then
cache[oldDef.key] = nil
end
end
end
cache._nextOrder = (cache._nextOrder or 0) + 1
local aura = cache[key] or {}
aura.order = cache._nextOrder
aura.duration = pending.duration
aura.expires = GetTime() + pending.duration
aura.spellID = pending.spellID
aura.texture = pending.texture or def.texture
aura.stacks = 0
aura.confirmedAt = GetTime()
cache[key] = aura
end
local function ConfirmPendingForUnit(unit, guid, now)
if not unit or not guid then return end
local pendingForGUID = BNP.pendingAuras[guid]
if not pendingForGUID then return end
local key, pending
for key, pending in pairs(pendingForGUID) do
if type(pending) == "table" then
if ConfirmedSpellOnUnit(unit, pending.spellID) then
CommitPendingAura(guid, key, pending)
pendingForGUID[key] = nil
elseif now - pending.created > PENDING_TIMEOUT then
-- Nothing landed: resist, miss, immune, evade or otherwise failed.
pendingForGUID[key] = nil
end
end
end
end
local function CleanupPending(now)
local guid, entries, key, pending
for guid, entries in pairs(BNP.pendingAuras) do
local any = false
for key, pending in pairs(entries) do
if type(pending) == "table" then
if now - pending.created > PENDING_TIMEOUT then
entries[key] = nil
else
any = true
end
end
end
if not any then BNP.pendingAuras[guid] = nil end
end
end
local castEvents = CreateFrame("Frame")
castEvents:RegisterEvent("UNIT_CASTEVENT")
castEvents:SetScript("OnEvent", function()
local casterGUID = arg1
local targetGUID = arg2
local eventType = arg3
local spellID = arg4
local playerGUID = GetPlayerGUID()
if not playerGUID or casterGUID ~= playerGUID or not targetGUID then return end
-- Dark Harvest is a CHANNEL event, not CAST. Handle only this exact
-- Turtle spell here so normal aura creation remains CAST-only.
if eventType == "CHANNEL" and spellID == DARK_HARVEST_SPELL_ID then
StartDarkHarvest(targetGUID, arg5)
return
end
if eventType ~= "CAST" then return end
-- Shadow audit only: this never changes the existing lookup or renderer.
if BNP.AuditSpellDB then BNP:AuditSpellDB(spellID) end
local def, texture = FindAuraDef(spellID)
if not def then return end
local duration = nil
if BNP.GetSpellbookDurationForDef then
duration = BNP:GetSpellbookDurationForDef(def)
end
duration = duration or (def.durations and def.durations[spellID]) or def.duration
local pendingForGUID = BNP.pendingAuras[targetGUID]
if not pendingForGUID then
pendingForGUID = {}
BNP.pendingAuras[targetGUID] = pendingForGUID
end
pendingForGUID[def.key] = {
def = def,
spellID = spellID,
texture = texture or def.texture,
duration = duration,
created = GetTime(),
}
end)
-- Live aura removal ---------------------------------------------------------
-- Timers alone are not enough for CC/roots/traps: effects may be dispelled,
-- broken by damage, removed by trinkets, or otherwise end early.
--
-- We NEVER create auras from this scan. It may only remove already-confirmed
-- own auras from the GUID cache when their exact spell ID is no longer present.
local REMOVAL_SCAN_INTERVAL = 0.10
local REMOVAL_GRACE = 0.35
local function CacheHasTrackedAuras(cache)
if not cache then return false end
local key, aura
for key, aura in pairs(cache) do
if type(aura) == "table" and aura.spellID and aura.expires then
return true
end
end
return false
end
local function SyncAuraRemoval(unit, guid, now)
if not unit or not guid then return end
local cache = BNP.guidAuras[guid]
if not CacheHasTrackedAuras(cache) then return end
local present = {}
local i
for i = 1, 32 do
local texture, stacks, dtype, auraSpellID = UnitDebuff(unit, i)
if not texture then break end
if auraSpellID then
present[auraSpellID] = true
end
end
local key, aura
for key, aura in pairs(cache) do
if type(aura) == "table" and aura.spellID and aura.expires then
-- Give the client a tiny grace period immediately after confirmation so
-- transient aura-list updates cannot remove a freshly-landed effect.
local age = now - (aura.confirmedAt or 0)
if age >= REMOVAL_GRACE and not present[aura.spellID] then
cache[key] = nil
end
end
end
end
local function FormatTimer(remaining)
if not remaining or remaining <= 0 then return "" end
-- Keep long durations compact on the small nameplate icons.
-- Examples: 300s -> 5m, 61s -> 2m, 59s -> 59.
if remaining >= 3600 then
return math.ceil(remaining / 3600) .. "h"
elseif remaining >= 60 then
return math.ceil(remaining / 60) .. "m"
end
return tostring(math.ceil(remaining))
end
local function UpdatePlate(plate)
if not plate or not plate:IsShown() then return end
if not plate.BNPAuraContainer then CreateAuraContainer(plate) end
local container = plate.BNPAuraContainer
if not container then return end
local guid = GetPlateGUID(plate)
local cache = guid and BNP.guidAuras[guid] or nil
local now = GetTime()
-- Reuse one small table per plate. Active auras are packed without gaps and
-- sorted by the order in which they were first applied to this GUID.
local active = container.activeAuras
if not active then
active = {}
container.activeAuras = active
end
local activePool = container.activeAuraPool
if not activePool then
activePool = {}
for i = 1, MAX_VISIBLE_ICONS do activePool[i] = {} end
container.activeAuraPool = activePool
end
local previousCount = table.getn(active)
for i = 1, previousCount do active[i] = nil end
local activeCount = 0
for _, def in ipairs(AURA_DEFS) do
local aura = cache and cache[def.key]
local remaining = aura and aura.expires and (aura.expires - now) or nil
if remaining and remaining > 0 then
if activeCount < MAX_VISIBLE_ICONS then
activeCount = activeCount + 1
local entry = activePool[activeCount]
active[activeCount] = entry
entry.def = def
entry.aura = aura
entry.remaining = remaining
end
elseif cache and aura then
cache[def.key] = nil
end
end
table.sort(active, function(a, b)
return (a.aura.order or 0) < (b.aura.order or 0)
end)
local visibleCount = table.getn(active)
local iconSize = GetIconSize()
local maxWidth = iconSize * MAX_VISIBLE_ICONS + ICON_SPACING * (MAX_VISIBLE_ICONS - 1)
local rowWidth = 0
if visibleCount > 0 then
rowWidth = iconSize * visibleCount + ICON_SPACING * (visibleCount - 1)
end
local startX = (maxWidth - rowWidth) / 2
for i = 1, table.getn(container.icons) do
local icon = container.icons[i]
local entry = active[i]
if entry then
icon:ClearAllPoints()
icon:SetPoint("LEFT", container, "LEFT", startX + (i - 1) * (iconSize + ICON_SPACING), 0)
icon.texture:SetTexture(entry.aura.texture or entry.def.texture)
local timerText = FormatTimer(entry.remaining)
if icon.lastTimerText ~= timerText then
icon.timer:SetText(timerText)
icon.lastTimerText = timerText
end
icon:Show()
else
icon.lastTimerText = nil
icon.timer:SetText("")
icon:Hide()
end
end
if visibleCount > 0 then
container:Show()
else
container:Hide()
end
end
function BNP:RefreshAuraLayout(plate)
if not plate or not plate.BNPAuraContainer then return end
local container = plate.BNPAuraContainer
local size = GetIconSize()
local width = size * MAX_VISIBLE_ICONS + ICON_SPACING * (MAX_VISIBLE_ICONS - 1)
container:SetWidth(width)
container:SetHeight(size)
local i
for i = 1, table.getn(container.icons) do
container.icons[i]:SetWidth(size)
container.icons[i]:SetHeight(size)
end
end
function BNP:RefreshAllAuraLayouts()
local plate
for plate in pairs(BNP.plates or {}) do
self:RefreshAuraLayout(plate)
end
end
ScanSpellbook()
table.insert(BNP.libnameplate.OnInit, CreateAuraContainer)
table.insert(BNP.libnameplate.OnShow, CreateAuraContainer)
-- Keep the proven central update loop and the existing icon implementation.
-- No CooldownFrame templates and no frames or tables are created here.
local renderer = CreateFrame("Frame")
local elapsedTotal = 0
local removalElapsed = 0
renderer:SetScript("OnUpdate", function()
elapsedTotal = elapsedTotal + arg1
removalElapsed = removalElapsed + arg1
if elapsedTotal < UPDATE_INTERVAL then return end
elapsedTotal = 0
local now = GetTime()
UpdateDarkHarvest(now)
local hasPending = HasPendingAuras()
local doRemovalScan = removalElapsed >= REMOVAL_SCAN_INTERVAL
if doRemovalScan then removalElapsed = 0 end
local targetGUID = nil
if hasPending or doRemovalScan then
targetGUID = GetTargetGUID()
end
if hasPending and targetGUID then
-- Current target is the most reliable confirmation source.
ConfirmPendingForUnit("target", targetGUID, now)
end
if doRemovalScan and targetGUID then
SyncAuraRemoval("target", targetGUID, now)
end
-- The normal renderer always runs. Extra GUID/token/UnitDebuff work is only
-- performed while pending casts exist or during the throttled removal scan.
for plate in pairs(BNP.plates) do
if plate:IsShown() and (hasPending or doRemovalScan) then
local guid = GetPlateGUID(plate)
local token = GetUnitTokenForPlate(plate)
if guid and token then
if hasPending then
ConfirmPendingForUnit(token, guid, now)
end
if doRemovalScan then
SyncAuraRemoval(token, guid, now)
end
end
end
UpdatePlate(plate)
end
if hasPending then
CleanupPending(now)
end
end)
-212
View File
@@ -1,212 +0,0 @@
BNP = BNP or {}
local casts = {}
local BAR_HEIGHT = 6
local function GetCastbarHeight()
if BNP.GetCastbarHeight then return BNP:GetCastbarHeight() end
return BAR_HEIGHT
end
local BAR_GAP = 2
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())
bar:SetPoint("TOPLEFT", plate.healthbar, "BOTTOMLEFT", 0, -BAR_GAP)
bar:SetPoint("TOPRIGHT", plate.healthbar, "BOTTOMRIGHT", 0, -BAR_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("RIGHT", bar, "LEFT", -2, 0)
icon:SetFrameLevel(bar:GetFrameLevel())
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 updater = CreateFrame("Frame")
updater:SetScript("OnUpdate", function()
-- Castbar animation is intentionally updated every rendered frame.
-- Event/GUID detection remains event-driven; only the cheap visual progress
-- interpolation runs here, so the bar no longer advances in visible 20ms steps.
local enabled = Enabled()
local now = GetTime()
local plate
for plate in pairs(BNP.plates) do
if enabled and plate:IsShown() then
local guid = GetPlateGUID(plate)
local cast = guid and casts[guid]
if cast and now < cast.endTime then
local bar = CreateCastbar(plate)
if bar then
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(plate:GetAlpha())
if bar.icon then
if cast.texture then
bar.icon.texture:SetTexture(cast.texture)
bar.icon:SetAlpha(plate:GetAlpha())
bar.icon:Show()
else
bar.icon:Hide()
end
end
bar:Show()
end
else
if guid and cast and now >= cast.endTime then
casts[guid] = nil
end
Hide(plate)
end
else
Hide(plate)
end
end
end)
function BNP:RefreshCastbarHeights()
local plate
local height = GetCastbarHeight()
for plate in pairs(BNP.plates or {}) do
if plate and plate.BNPCastbar then
plate.BNPCastbar:SetHeight(height)
end
end
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
-147
View File
@@ -1,147 +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
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)
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.05 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
if not token or not guid or guid ~= plate.BNPClassGUID or not UnitIsPlayer(token) then
ClearClassCache(plate)
else
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
elseif token and guid and UnitIsPlayer(token) then
ApplyClassColor(plate)
end
end
end
end)
-70
View File
@@ -1,70 +0,0 @@
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
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 == "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 spellprobe, /bnp spelldb, /bnp guid, /bnp status, /bnp castbars on/off, /bnp scale <0.5-1.5>, /bnp scale reset, /bnp classcolors on/off, /bnp debug on, /bnp debug off")
end
end
-43
View File
@@ -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
-73
View File
@@ -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
-114
View File
@@ -1,114 +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 EnsureScaleWrapper(plate)
if not plate or plate.BNPScaleWrapper then return end
local originalWidth = plate:GetWidth()
local originalHeight = plate:GetHeight()
local wrapper = CreateFrame("Frame", nil, plate)
wrapper:SetAllPoints(plate)
wrapper.plate = plate
wrapper.originalWidth = originalWidth
wrapper.originalHeight = originalHeight
plate.BNPScaleWrapper = wrapper
-- Match ShaguTweaks: move Blizzard nameplate visuals into a child frame.
-- Never SetScale() on the original WorldFrame nameplate itself, otherwise
-- its projected screen position shifts toward the unit's feet.
if plate.healthbar then
plate.healthbar:SetParent(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
object:SetParent(wrapper)
end
end
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)
-- 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: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
-177
View File
@@ -1,177 +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
BNP.spellbookDurations = BNP.spellbookDurations or {}
function BNP:RefreshSpellbookDurations()
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: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
-153
View File
@@ -1,153 +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 RememberNormalColor(plate, bar)
if plate.BNPTankActive then return end
local r, g, b, a = bar:GetStatusBarColor()
if r then plate.BNPNormalColor = { r, g, b, a or 1 } end
end
local function RestoreNormalColor(plate, bar)
local c = plate.BNPNormalColor
if c then bar:SetStatusBarColor(c[1], c[2], c[3], c[4] or 1) 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
RestoreNormalColor(plate, bar)
return
end
local myGUID = GetPlayerGUID()
if not myGUID then return end
RememberNormalColor(plate, bar)
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
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.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
-372
View File
@@ -1,372 +0,0 @@
BNP = BNP 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("A screenshot of this output is enough. Raw data: /bnp unknown verbose")
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