Add files via upload

This commit is contained in:
Wurmschwanz
2026-08-18 20:56:43 +02:00
committed by GitHub
parent d8d0325f2e
commit af071a5154
33 changed files with 6889 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
## Interface: 11200
## Title: Blizz Nameplates+
## Notes: Lightweight personal debuffs and class colors for Blizzard nameplates. Requires SuperWoW.
## Author: Wurmschwanz; nameplate discovery inspired by Shagu's Vanilla projects
## OptionalDeps: ShaguTweaks
## Version: 1.0.7
## SavedVariables: BNP_DB
core\core.lua
core\compat_shagutweaks.lua
core\ui.lua
core\config.lua
core\bridge.lua
core\libnameplate.lua
core\options.lua
spells\warlock.lua
spells\priest.lua
spells\warrior.lua
spells\rogue.lua
spells\hunter.lua
spells\mage.lua
spells\druid.lua
spells\shaman.lua
spells\paladin.lua
spells\registry_shadow.lua
modules\debug.lua
modules\spellprobe.lua
modules\scale.lua
modules\fullalpha.lua
modules\shagu_darkmode.lua
modules\unknown.lua
modules\auras.lua
modules\classcolors.lua
modules\castbar.lua
modules\guiddebug.lua
modules\tankmode.lua
modules\healthpercent.lua
modules\combopoints.lua
modules\commands.lua
+10
View File
@@ -0,0 +1,10 @@
BNP = BNP or {}
function BNP:RegisterPlate(plate)
plate = plate or this
if not plate then return end
if BNP.plates[plate] then return end
BNP.plates[plate] = true
BNP.detected = BNP.detected + 1
end
+94
View File
@@ -0,0 +1,94 @@
BNP = BNP or {}
-- Robust ShaguTweaks compatibility.
-- BNP owns Scale / Class Colors / Castbar while loaded.
-- ShaguTweaks settings are NOT changed.
BNP.shaguCompat = BNP.shaguCompat or {
patched = {},
registerHooked = false,
}
local wanted = {
["Nameplate Scale"] = true,
["Nameplate Class Colors"] = true,
["Nameplate Castbar"] = true,
}
local function IsWantedTitle(title)
if not title then return false end
if wanted[title] then return true end
if ShaguTweaks and ShaguTweaks.T then
if title == ShaguTweaks.T["Nameplate Scale"] then return true end
if title == ShaguTweaks.T["Nameplate Class Colors"] then return true end
if title == ShaguTweaks.T["Nameplate Castbar"] then return true end
end
return false
end
local function DisableModule(mod)
if not mod or type(mod) ~= "table" then return false end
if not IsWantedTitle(mod.title) then return false end
if mod.BNPCompatibilityPatched then return true end
mod.BNPOriginalEnable = mod.enable
mod.enable = function(self)
return
end
mod.BNPCompatibilityPatched = true
BNP.shaguCompat.patched[mod.title or "?"] = true
return true
end
local function PatchExistingModules()
if not ShaguTweaks or not ShaguTweaks.mods then return end
local title, mod
for title, mod in pairs(ShaguTweaks.mods) do
-- Some registered module tables use localized title as the map key.
if mod and not mod.title then mod.title = title end
DisableModule(mod)
end
end
local function HookRegister()
if not ShaguTweaks or not ShaguTweaks.register then return end
if BNP.shaguCompat.registerHooked then return end
local originalRegister = ShaguTweaks.register
ShaguTweaks.register = function(self, mod)
local registered = originalRegister(self, mod)
DisableModule(registered or mod)
return registered
end
BNP.shaguCompat.registerHooked = true
end
function BNP:ApplyShaguTweaksCompatibility()
if not ShaguTweaks then return false end
-- Covers BNP loading before Shagu's modules register.
HookRegister()
-- Covers BNP loading after Shagu's modules registered.
PatchExistingModules()
return true
end
-- Try immediately in case ShaguTweaks is already loaded.
BNP:ApplyShaguTweaksCompatibility()
-- Also retry after every addon load. This safely covers all Vanilla/Turtle
-- load-order variations without touching ShaguTweaks saved settings.
local loader = CreateFrame("Frame")
loader:RegisterEvent("ADDON_LOADED")
loader:SetScript("OnEvent", function()
BNP:ApplyShaguTweaksCompatibility()
end)
+156
View File
@@ -0,0 +1,156 @@
BNP = BNP or {}
BNP.defaults = BNP.defaults or {
nameplateScale = 1.0,
nameplateYOffset = 0,
nonTargetAlpha = 1.0,
iconSize = 18,
tankMode = false,
classColors = true,
castbars = true,
debuffs = true,
crowdControl = true,
separateCCRow = true,
showOtherCCs = true,
castbarHeight = 6,
minimapAngle = 225,
healthPercent = false,
healthText = "off",
targetFocus = true,
targetGlowColor = "white",
targetOnTop = true,
comboPoints = true,
}
function BNP:InitConfig()
BNP_DB = BNP_DB or {}
if BNP_DB.nameplateScale == nil then BNP_DB.nameplateScale = self.defaults.nameplateScale end
if BNP_DB.nameplateYOffset == nil then BNP_DB.nameplateYOffset = self.defaults.nameplateYOffset end
if BNP_DB.nonTargetAlpha == nil then BNP_DB.nonTargetAlpha = self.defaults.nonTargetAlpha end
if BNP_DB.iconSize == nil then BNP_DB.iconSize = self.defaults.iconSize end
if BNP_DB.tankMode == nil then BNP_DB.tankMode = self.defaults.tankMode end
if BNP_DB.classColors == nil then BNP_DB.classColors = self.defaults.classColors end
if BNP_DB.castbars == nil then BNP_DB.castbars = self.defaults.castbars end
if BNP_DB.debuffs == nil then BNP_DB.debuffs = self.defaults.debuffs end
if BNP_DB.crowdControl == nil then BNP_DB.crowdControl = (BNP_DB.debuffs ~= false) and true or false end
if BNP_DB.separateCCRow == nil then BNP_DB.separateCCRow = self.defaults.separateCCRow end
if BNP_DB.showOtherCCs == nil then BNP_DB.showOtherCCs = self.defaults.showOtherCCs end
if BNP_DB.castbarHeight == nil then BNP_DB.castbarHeight = self.defaults.castbarHeight end
if BNP_DB.minimapAngle == nil then BNP_DB.minimapAngle = self.defaults.minimapAngle end
if BNP_DB.healthPercent == nil then BNP_DB.healthPercent = self.defaults.healthPercent end
if BNP_DB.healthText == nil then
-- Backwards-compatible migration from the old Health % checkbox.
BNP_DB.healthText = BNP_DB.healthPercent and "percent" or self.defaults.healthText
end
if BNP_DB.targetFocus == nil then BNP_DB.targetFocus = self.defaults.targetFocus end
if BNP_DB.targetGlowColor == nil then BNP_DB.targetGlowColor = self.defaults.targetGlowColor end
if BNP_DB.targetOnTop == nil then BNP_DB.targetOnTop = self.defaults.targetOnTop end
if BNP_DB.comboPoints == nil then BNP_DB.comboPoints = self.defaults.comboPoints end
end
function BNP:GetNameplateScale()
return (BNP_DB and tonumber(BNP_DB.nameplateScale)) or self.defaults.nameplateScale
end
function BNP:GetNameplateYOffset()
local value = (BNP_DB and tonumber(BNP_DB.nameplateYOffset)) or self.defaults.nameplateYOffset or 0
if value < 0 then value = 0 end
if value > 50 then value = 50 end
return value
end
function BNP:GetNonTargetAlpha()
local value = (BNP_DB and tonumber(BNP_DB.nonTargetAlpha)) or self.defaults.nonTargetAlpha or 1.0
if value < 0.30 then value = 0.30 end
if value > 1.00 then value = 1.00 end
return value
end
function BNP:GetIconSize()
return (BNP_DB and tonumber(BNP_DB.iconSize)) or self.defaults.iconSize
end
function BNP:IsTankModeEnabled()
return BNP_DB and BNP_DB.tankMode and true or false
end
function BNP:AreClassColorsEnabled()
return not BNP_DB or BNP_DB.classColors ~= false
end
function BNP:AreCastbarsEnabled()
return not BNP_DB or BNP_DB.castbars ~= false
end
function BNP:GetCastbarHeight()
local value = (BNP_DB and tonumber(BNP_DB.castbarHeight)) or self.defaults.castbarHeight
if value < 4 then value = 4 end
if value > 14 then value = 14 end
return value
end
function BNP:GetHealthTextMode()
local mode = (BNP_DB and BNP_DB.healthText) or self.defaults.healthText or "off"
if mode ~= "off" and mode ~= "percent" and mode ~= "hp" and mode ~= "both" then
mode = "off"
end
return mode
end
function BNP:IsHealthPercentEnabled()
local mode = self:GetHealthTextMode()
return mode == "percent" or mode == "both"
end
function BNP:IsHealthTextEnabled()
return self:GetHealthTextMode() ~= "off"
end
function BNP:IsTargetFocusEnabled()
return not BNP_DB or BNP_DB.targetFocus ~= false
end
function BNP:IsTargetOnTopEnabled()
return not BNP_DB or BNP_DB.targetOnTop ~= false
end
function BNP:GetTargetGlowColor()
local key = (BNP_DB and BNP_DB.targetGlowColor) or self.defaults.targetGlowColor or "white"
local colors = {
white = { 1.00, 1.00, 1.00 },
gold = { 1.00, 0.82, 0.10 },
blue = { 0.25, 0.55, 1.00 },
green = { 0.25, 1.00, 0.35 },
red = { 1.00, 0.20, 0.20 },
purple = { 0.75, 0.35, 1.00 },
}
local c = colors[key] or colors.white
return c[1], c[2], c[3], key
end
function BNP:AreComboPointsEnabled()
return not BNP_DB or BNP_DB.comboPoints ~= false
end
function BNP:AreDebuffsEnabled()
return not BNP_DB or BNP_DB.debuffs ~= false
end
function BNP:AreCrowdControlEnabled()
return not BNP_DB or BNP_DB.crowdControl ~= false
end
function BNP:AreAnyAurasEnabled()
return self:AreDebuffsEnabled() or self:AreCrowdControlEnabled()
end
function BNP:IsSeparateCCRowEnabled()
return not BNP_DB or BNP_DB.separateCCRow ~= false
end
function BNP:ShowOtherPlayersCCs()
return not BNP_DB or BNP_DB.showOtherCCs ~= false
end
+37
View File
@@ -0,0 +1,37 @@
BNP = BNP or {}
BNP.version = "1.0.7"
BNP.plates = BNP.plates or {}
BNP.detected = 0
BNP.debugEnabled = false
function BNP:Print(msg)
if DEFAULT_CHAT_FRAME then
DEFAULT_CHAT_FRAME:AddMessage("|cff33ff99Blizz Nameplates+:|r " .. tostring(msg))
end
end
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("VARIABLES_LOADED")
eventFrame:SetScript("OnEvent", function()
BNP_DB = BNP_DB or {}
if BNP.InitConfig then BNP:InitConfig() end
if BNP.CreateMinimapButton then BNP:CreateMinimapButton() end
if BNP.InstallTankMode then BNP:InstallTankMode() end
if BNP_DB.debugEnabled == nil then BNP_DB.debugEnabled = false end
BNP.debugEnabled = BNP_DB.debugEnabled
if CombatLogAdd and SpellInfo then
BNP:Print(BNP.version .. " loaded. Standalone nameplates + SuperWoW tracking active.")
if ShaguTweaks then
BNP:ApplyShaguTweaksCompatibility()
local count = 0
local k
for k in pairs(BNP.shaguCompat.patched or {}) do count = count + 1 end
BNP:Print("ShaguTweaks compatibility active (" .. tostring(count) .. "/3 nameplate modules suppressed).")
end
else
BNP:Print("ERROR: SuperWoW API not detected. Blizz Nameplates+ requires SuperWoW.")
end
end)
+119
View File
@@ -0,0 +1,119 @@
BNP = BNP or {}
BNP.libnameplate = BNP.libnameplate or {
OnInit = {},
OnShow = {},
OnUpdate = {},
}
local lib = BNP.libnameplate
local known = {}
local initializedChildren = 0
local function IsNamePlate(frame)
if not frame or not frame.GetObjectType then return false end
if frame:GetObjectType() ~= "Button" then return false end
local firstRegion = frame:GetRegions()
if not firstRegion or not firstRegion.GetObjectType or not firstRegion.GetTexture then
return false
end
if firstRegion:GetObjectType() ~= "Texture" then return false end
return firstRegion:GetTexture() == "Interface\\Tooltips\\Nameplate-Border"
end
local function AttachPlateShortcuts(plate)
if plate.BNPShortcutsReady then return end
-- Vanilla Blizzard nameplates expose the health StatusBar as their first child.
local healthbar = plate:GetChildren()
plate.healthbar = healthbar
-- Vanilla region order:
-- 1 border, 2 glow, 3 name, 4 level, 5 level icon, 6 raid icon.
local regions = { plate:GetRegions() }
plate.border = regions[1]
plate.glow = regions[2]
plate.name = regions[3]
plate.level = regions[4]
plate.levelicon = regions[5]
plate.raidicon = regions[6]
plate.BNPShortcutsReady = true
end
local function Fire(list, plate)
plate = plate or this
if not plate then return end
local i
for i = 1, table.getn(list) do
if list[i] then
pcall(list[i], plate)
end
end
end
local function RegisterPlate(plate)
plate = plate or this
if not plate then return end
if known[plate] then return end
known[plate] = true
AttachPlateShortcuts(plate)
if BNP.RegisterPlate then
BNP:RegisterPlate(plate)
end
Fire(lib.OnInit, plate)
if plate:IsShown() then
Fire(lib.OnShow, plate)
end
-- Run OnShow callbacks when Blizzard reuses the same plate.
local oldOnShow = plate:GetScript("OnShow")
plate:SetScript("OnShow", function(self)
local current = self or this or plate
if not current then return end
if oldOnShow then oldOnShow(current) end
AttachPlateShortcuts(current)
Fire(lib.OnShow, current)
end)
end
local scanner = CreateFrame("Frame")
local scanElapsed = 0
scanner:SetScript("OnUpdate", function()
scanElapsed = scanElapsed + arg1
if scanElapsed < 0.10 then return end
scanElapsed = 0
local count = WorldFrame:GetNumChildren()
-- Full scan only when WorldFrame gained children. This mirrors the lightweight
-- discovery strategy used by Shagu's Vanilla nameplate code.
if count > initializedChildren then
local children = { WorldFrame:GetChildren() }
local i
for i = 1, table.getn(children) do
local frame = children[i]
if not known[frame] and IsNamePlate(frame) then
RegisterPlate(frame)
end
end
initializedChildren = count
end
-- Compatibility callback list for modules that want a throttled plate update.
if table.getn(lib.OnUpdate) > 0 then
local plate
for plate in pairs(known) do
if plate:IsShown() then
Fire(lib.OnUpdate, plate)
end
end
end
end)
+543
View File
@@ -0,0 +1,543 @@
BNP = BNP or {}
local _, optionsPlayerClass = UnitClass("player")
local comboOptionsClass = (optionsPlayerClass == "ROGUE" or optionsPlayerClass == "DRUID")
local function Round(value, step)
return math.floor((value / step) + 0.5) * step
end
local sliderCount = 0
local function CreateSlider(parent, label, minValue, maxValue, step, y)
sliderCount = sliderCount + 1
local slider = CreateFrame("Slider", "BNPOptionsSlider" .. sliderCount, parent, "OptionsSliderTemplate")
slider:SetPoint("TOPLEFT", parent, "TOPLEFT", 28, y)
slider:SetWidth(220)
slider:SetHeight(16)
slider:SetMinMaxValues(minValue, maxValue)
slider:SetValueStep(step)
getglobal(slider:GetName() .. "Low"):SetText(tostring(minValue))
getglobal(slider:GetName() .. "High"):SetText(tostring(maxValue))
getglobal(slider:GetName() .. "Text"):SetText(label)
return slider
end
local function CreateCheck(parent, label, y, onclick, x)
local check = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate")
check:SetPoint("TOPLEFT", parent, "TOPLEFT", x or 22, y)
check:SetWidth(24)
check:SetHeight(24)
local text = parent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
text:SetPoint("LEFT", check, "RIGHT", 4, 0)
text:SetText(label)
check.BNPLabel = text
check:SetScript("OnClick", onclick)
return check
end
function BNP:CreateOptions()
if self.optionsFrame then return end
local frame = CreateFrame("Frame", "BNPOptionsFrame", UIParent)
frame:SetWidth(310)
frame:SetHeight(comboOptionsClass and 897 or 837)
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 80)
frame:SetFrameStrata("DIALOG")
frame:SetBackdrop({
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
tile = true, tileSize = 32, edgeSize = 32,
insets = { left = 11, right = 12, top = 12, bottom = 11 }
})
frame:SetMovable(true)
frame:EnableMouse(true)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", function() this:StartMoving() end)
frame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
frame:Hide()
local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
title:SetPoint("TOP", frame, "TOP", 0, -18)
title:SetText("Blizz Nameplates+")
local version = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
version:SetPoint("TOP", title, "BOTTOM", 0, -4)
version:SetText("Version " .. tostring(BNP.version or "?"))
frame.versionText = version
local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -5, -5)
local function CreateSection(label, y)
local text = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
text:SetPoint("TOPLEFT", frame, "TOPLEFT", 22, y)
text:SetText(label)
local line = frame:CreateTexture(nil, "ARTWORK")
line:SetTexture(1, 1, 1, 0.16)
line:SetPoint("LEFT", text, "RIGHT", 8, 0)
line:SetPoint("RIGHT", frame, "RIGHT", -22, 0)
line:SetHeight(1)
return text
end
local function SetCheckEnabled(check, enabled)
if not check then return end
if enabled then
if check.Enable then check:Enable() end
check:SetAlpha(1.0)
if check.BNPLabel then check.BNPLabel:SetTextColor(1, 0.82, 0) end
else
if check.Disable then check:Disable() end
check:SetAlpha(0.45)
if check.BNPLabel then check.BNPLabel:SetTextColor(0.5, 0.5, 0.5) end
end
end
-- NAMEPLATES -------------------------------------------------------------
CreateSection("Nameplates", -68)
local scale = CreateSlider(frame, "Nameplate Scale", 0.70, 1.50, 0.05, -98)
scale:SetScript("OnValueChanged", function()
if not BNP_DB then return end
local value = Round(this:GetValue(), 0.05)
BNP_DB.nameplateScale = value
getglobal(this:GetName() .. "Text"):SetText("Nameplate Scale: " .. string.format("%.2f", value))
if BNP.ApplyNameplateScaleAll then BNP:ApplyNameplateScaleAll() end
end)
frame.scaleSlider = scale
local yOffset = CreateSlider(frame, "Nameplate Y Offset", 0, 50, 1, -140)
yOffset:SetScript("OnValueChanged", function()
if not BNP_DB then return end
local value = math.floor(this:GetValue() + 0.5)
BNP_DB.nameplateYOffset = value
getglobal(this:GetName() .. "Text"):SetText("Nameplate Y Offset: +" .. value)
if BNP.ApplyNameplateYOffsetAll then BNP:ApplyNameplateYOffsetAll() end
end)
frame.yOffsetSlider = yOffset
local nonTargetAlpha = CreateSlider(frame, "Non-Target Alpha", 30, 100, 5, -182)
nonTargetAlpha:SetScript("OnValueChanged", function()
if not BNP_DB then return end
local percent = math.floor((this:GetValue() / 5) + 0.5) * 5
if percent < 30 then percent = 30 end
if percent > 100 then percent = 100 end
BNP_DB.nonTargetAlpha = percent / 100
getglobal(this:GetName() .. "Text"):SetText("Non-Target Alpha: " .. percent .. "%")
if BNP.RefreshNonTargetAlpha then BNP:RefreshNonTargetAlpha() end
end)
frame.nonTargetAlphaSlider = nonTargetAlpha
local classColors = CreateCheck(frame, "Class Colors", -220, function()
BNP_DB.classColors = this:GetChecked() and true or false
if BNP_DB.classColors then
local plate
for plate in pairs(BNP.plates or {}) do
plate.BNPClassColorApplied = nil
plate.BNPClassR = nil
plate.BNPClassG = nil
plate.BNPClassB = nil
end
end
end)
frame.classColorsCheck = classColors
local tank = CreateCheck(frame, "Tank Mode", -246, function()
BNP_DB.tankMode = this:GetChecked() and true or false
if BNP.UpdateTankMode then BNP:UpdateTankMode() end
end)
frame.tankCheck = tank
local healthTextLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
healthTextLabel:SetPoint("TOPLEFT", frame, "TOPLEFT", 28, -280)
healthTextLabel:SetText("Health Text")
local healthTextDropdown = CreateFrame("Frame", "BNPHealthTextDropdown", frame, "UIDropDownMenuTemplate")
healthTextDropdown:SetPoint("TOPLEFT", frame, "TOPLEFT", 10, -292)
UIDropDownMenu_SetWidth(150, healthTextDropdown)
local healthModeLabels = {
off = "Off",
percent = "Percent",
hp = "HP",
both = "HP + Percent",
}
local function SetHealthTextMode(mode)
if not healthModeLabels[mode] then mode = "off" end
BNP_DB.healthText = mode
BNP_DB.healthPercent = (mode == "percent" or mode == "both")
UIDropDownMenu_SetSelectedValue(healthTextDropdown, mode)
UIDropDownMenu_SetText(healthModeLabels[mode], healthTextDropdown)
if BNP.RefreshHealthPercent then BNP:RefreshHealthPercent() end
end
UIDropDownMenu_Initialize(healthTextDropdown, function()
local modes = { "off", "percent", "hp", "both" }
local i
for i = 1, table.getn(modes) do
local mode = modes[i]
local info = {}
info.text = healthModeLabels[mode]
info.value = mode
info.func = function() SetHealthTextMode(this.value) end
info.checked = (BNP:GetHealthTextMode() == mode)
UIDropDownMenu_AddButton(info)
end
end)
frame.healthTextDropdown = healthTextDropdown
frame.SetHealthTextMode = SetHealthTextMode
local targetFocus = CreateCheck(frame, "Target Glow", -336, function()
BNP_DB.targetFocus = this:GetChecked() and true or false
if BNP.RefreshTargetFocus then BNP:RefreshTargetFocus() end
if frame.UpdateDependentControls then frame:UpdateDependentControls() end
end)
frame.targetFocusCheck = targetFocus
local glowColorLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
glowColorLabel:SetPoint("TOPLEFT", frame, "TOPLEFT", 42, -366)
glowColorLabel:SetText("Glow Color")
frame.glowColorLabel = glowColorLabel
local glowColorDropdown = CreateFrame("Frame", "BNPTargetGlowColorDropdown", frame, "UIDropDownMenuTemplate")
glowColorDropdown:SetPoint("TOPLEFT", frame, "TOPLEFT", 24, -378)
UIDropDownMenu_SetWidth(150, glowColorDropdown)
local glowColorLabels = {
white = "White",
gold = "Gold",
blue = "Blue",
green = "Green",
red = "Red",
purple = "Purple",
}
local function SetTargetGlowColor(color)
if not glowColorLabels[color] then color = "white" end
BNP_DB.targetGlowColor = color
UIDropDownMenu_SetSelectedValue(glowColorDropdown, color)
UIDropDownMenu_SetText(glowColorLabels[color], glowColorDropdown)
if BNP.RefreshTargetFocus then BNP:RefreshTargetFocus() end
end
UIDropDownMenu_Initialize(glowColorDropdown, function()
local colors = { "white", "gold", "blue", "green", "red", "purple" }
local i
for i = 1, table.getn(colors) do
local color = colors[i]
local info = {}
info.text = glowColorLabels[color]
info.value = color
info.func = function() SetTargetGlowColor(this.value) end
info.checked = ((BNP_DB and BNP_DB.targetGlowColor) or "white") == color
UIDropDownMenu_AddButton(info)
end
end)
frame.glowColorDropdown = glowColorDropdown
frame.SetTargetGlowColor = SetTargetGlowColor
local targetOnTop = CreateCheck(frame, "Target Plate on Top", -422, function()
BNP_DB.targetOnTop = this:GetChecked() and true or false
if BNP.RefreshTargetOnTop then BNP:RefreshTargetOnTop() end
end)
frame.targetOnTopCheck = targetOnTop
-- AURAS ------------------------------------------------------------------
CreateSection("Auras", -462)
local icon = CreateSlider(frame, "Aura Icon Size", 12, 32, 1, -492)
icon:SetScript("OnValueChanged", function()
if not BNP_DB then return end
local value = math.floor(this:GetValue() + 0.5)
BNP_DB.iconSize = value
getglobal(this:GetName() .. "Text"):SetText("Aura Icon Size: " .. value)
if BNP.RefreshAllAuraLayouts then BNP:RefreshAllAuraLayouts() end
end)
frame.iconSlider = icon
local debuffs = CreateCheck(frame, "Debuffs", -530, function()
BNP_DB.debuffs = this:GetChecked() and true or false
if BNP.RefreshDebuffVisibility then BNP:RefreshDebuffVisibility() end
end)
frame.debuffsCheck = debuffs
local crowdControl = CreateCheck(frame, "Crowd Control", -556, function()
BNP_DB.crowdControl = this:GetChecked() and true or false
if BNP.RefreshDebuffVisibility then BNP:RefreshDebuffVisibility() end
if frame.UpdateDependentControls then frame:UpdateDependentControls() end
end)
frame.crowdControlCheck = crowdControl
local separateCCRow = CreateCheck(frame, "Display CCs in Separate Row", -582, function()
BNP_DB.separateCCRow = this:GetChecked() and true or false
if BNP.RefreshAllAuraLayouts then BNP:RefreshAllAuraLayouts() end
if BNP.RefreshDebuffVisibility then BNP:RefreshDebuffVisibility() end
end, 42)
frame.separateCCRowCheck = separateCCRow
local showOtherCCs = CreateCheck(frame, "Show CCs from Other Players", -608, function()
BNP_DB.showOtherCCs = this:GetChecked() and true or false
if BNP.RefreshDebuffVisibility then BNP:RefreshDebuffVisibility() end
end, 42)
frame.showOtherCCsCheck = showOtherCCs
-- CASTBAR ----------------------------------------------------------------
CreateSection("Castbar", -648)
local castbars = CreateCheck(frame, "Castbars", -674, function()
local enabled = this:GetChecked() and true or false
if BNP.SetCastbarsEnabled then
BNP:SetCastbarsEnabled(enabled)
else
BNP_DB.castbars = enabled
end
end)
frame.castbarsCheck = castbars
local castbarHeight = CreateSlider(frame, "Castbar Height", 4, 14, 1, -710)
castbarHeight:SetScript("OnValueChanged", function()
if not BNP_DB then return end
local value = math.floor(this:GetValue() + 0.5)
BNP_DB.castbarHeight = value
getglobal(this:GetName() .. "Text"):SetText("Castbar Height: " .. value)
if BNP.RefreshCastbarHeights then BNP:RefreshCastbarHeights() end
end)
frame.castbarHeightSlider = castbarHeight
-- CLASS FEATURES ---------------------------------------------------------
local noteY = -764
if comboOptionsClass then
local classSection = CreateSection("Class Features", -750)
frame.classSection = classSection
local comboPoints = CreateCheck(frame, "Combo Points", -776, function()
BNP_DB.comboPoints = this:GetChecked() and true or false
if BNP.RefreshComboPoints then BNP:RefreshComboPoints() end
if BNP.RefreshAllAuraLayouts then BNP:RefreshAllAuraLayouts() end
end)
frame.comboPointsCheck = comboPoints
noteY = -816
end
local note = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
note:SetPoint("TOPLEFT", frame, "TOPLEFT", 28, noteY)
note:SetWidth(250)
note:SetJustifyH("LEFT")
note:SetText("Changes are applied immediately.\nTank Mode: green = targeting you, red = targeting someone else.")
function frame:UpdateDependentControls()
local ccEnabled = BNP:AreCrowdControlEnabled()
SetCheckEnabled(self.separateCCRowCheck, ccEnabled)
SetCheckEnabled(self.showOtherCCsCheck, ccEnabled)
local glowEnabled = BNP:IsTargetFocusEnabled()
if self.glowColorLabel then
self.glowColorLabel:SetTextColor(glowEnabled and 1 or 0.5, glowEnabled and 0.82 or 0.5, glowEnabled and 0 or 0.5)
end
if self.glowColorDropdown then
self.glowColorDropdown:SetAlpha(glowEnabled and 1.0 or 0.45)
if glowEnabled then
if UIDropDownMenu_EnableDropDown then UIDropDownMenu_EnableDropDown(self.glowColorDropdown) end
else
if UIDropDownMenu_DisableDropDown then UIDropDownMenu_DisableDropDown(self.glowColorDropdown) end
end
end
end
self.optionsFrame = frame
end
function BNP:SyncOptions()
self:CreateOptions()
local frame = self.optionsFrame
frame.scaleSlider:SetValue(self:GetNameplateScale())
if frame.yOffsetSlider then
local yOffset = self:GetNameplateYOffset()
frame.yOffsetSlider:SetValue(yOffset)
getglobal(frame.yOffsetSlider:GetName() .. "Text"):SetText("Nameplate Y Offset: +" .. yOffset)
end
if frame.nonTargetAlphaSlider then
local alphaPercent = math.floor((self:GetNonTargetAlpha() * 100) + 0.5)
frame.nonTargetAlphaSlider:SetValue(alphaPercent)
getglobal(frame.nonTargetAlphaSlider:GetName() .. "Text"):SetText("Non-Target Alpha: " .. alphaPercent .. "%")
end
frame.iconSlider:SetValue(self:GetIconSize())
frame.castbarHeightSlider:SetValue(self:GetCastbarHeight())
frame.classColorsCheck:SetChecked(self:AreClassColorsEnabled())
if frame.debuffsCheck then frame.debuffsCheck:SetChecked(self:AreDebuffsEnabled()) end
if frame.crowdControlCheck then frame.crowdControlCheck:SetChecked(self:AreCrowdControlEnabled()) end
if frame.separateCCRowCheck then frame.separateCCRowCheck:SetChecked(self:IsSeparateCCRowEnabled()) end
if frame.showOtherCCsCheck then frame.showOtherCCsCheck:SetChecked(self:ShowOtherPlayersCCs()) end
frame.castbarsCheck:SetChecked(self:AreCastbarsEnabled())
frame.tankCheck:SetChecked(self:IsTankModeEnabled())
if frame.comboPointsCheck and comboOptionsClass then
frame.comboPointsCheck:SetChecked(self:AreComboPointsEnabled())
end
if frame.healthTextDropdown then
local mode = self:GetHealthTextMode()
UIDropDownMenu_SetSelectedValue(frame.healthTextDropdown, mode)
local labels = { off = "Off", percent = "Percent", hp = "HP", both = "HP + Percent" }
UIDropDownMenu_SetText(labels[mode] or "Off", frame.healthTextDropdown)
end
frame.targetFocusCheck:SetChecked(self:IsTargetFocusEnabled())
if frame.glowColorDropdown then
local _, _, _, color = self:GetTargetGlowColor()
local labels = {
white = "White",
gold = "Gold",
blue = "Blue",
green = "Green",
red = "Red",
purple = "Purple",
}
UIDropDownMenu_SetSelectedValue(frame.glowColorDropdown, color or "white")
UIDropDownMenu_SetText(labels[color] or "White", frame.glowColorDropdown)
end
frame.targetOnTopCheck:SetChecked(self:IsTargetOnTopEnabled())
if frame.UpdateDependentControls then frame:UpdateDependentControls() end
frame.versionText:SetText("Version " .. tostring(self.version or "?"))
end
function BNP:ToggleOptions()
self:SyncOptions()
if self.optionsFrame:IsShown() then
self.optionsFrame:Hide()
else
self.optionsFrame:Show()
end
end
function BNP:CreateMinimapButton()
if self.minimapButton then return end
BNP_DB = BNP_DB or {}
if BNP_DB.minimapAngle == nil then BNP_DB.minimapAngle = 225 end
local button = CreateFrame("Button", "BNPMinimapButton", Minimap)
button:SetWidth(31)
button:SetHeight(31)
button:SetFrameStrata("MEDIUM")
button:SetFrameLevel((Minimap:GetFrameLevel() or 1) + 8)
button:RegisterForClicks("LeftButtonUp")
button:RegisterForDrag("LeftButton")
button:EnableMouse(true)
button:SetMovable(true)
-- Classic round minimap-button background.
local bg = button:CreateTexture(nil, "BACKGROUND")
bg:SetTexture("Interface\\Minimap\\UI-Minimap-Background")
bg:SetWidth(22)
bg:SetHeight(22)
bg:SetPoint("CENTER", button, "CENTER", 0, 0)
button.bg = bg
-- Addon identity: simple BN+ lettering instead of a spell/item icon.
-- Rendered directly by the client, so no external texture file is needed.
local iconBG = button:CreateTexture(nil, "ARTWORK")
iconBG:SetTexture(0.03, 0.03, 0.03, 1)
iconBG:SetWidth(20)
iconBG:SetHeight(20)
iconBG:SetPoint("CENTER", button, "CENTER", 0, 0)
button.iconBG = iconBG
local label = button:CreateFontString(nil, "OVERLAY")
label:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE")
label:SetPoint("CENTER", button, "CENTER", 0, 0)
label:SetText("BN+")
label:SetTextColor(0.20, 0.60, 1.00)
button.label = label
-- Standard Blizzard circular minimap border.
local border = button:CreateTexture(nil, "OVERLAY")
border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")
border:SetWidth(54)
border:SetHeight(54)
border:SetPoint("TOPLEFT", button, "TOPLEFT", 0, 0)
button.border = border
local highlight = button:CreateTexture(nil, "HIGHLIGHT")
highlight:SetTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")
highlight:SetBlendMode("ADD")
highlight:SetAllPoints(button)
button.highlight = highlight
local function UpdatePosition()
local angle = tonumber(BNP_DB.minimapAngle) or 225
local radius = 78
local rad = math.rad(angle)
button:ClearAllPoints()
button:SetPoint(
"CENTER",
Minimap,
"CENTER",
math.cos(rad) * radius,
math.sin(rad) * radius
)
end
local function UpdateAngleFromCursor()
local mx, my = Minimap:GetCenter()
local scale = 1
if UIParent.GetEffectiveScale then
scale = UIParent:GetEffectiveScale() or 1
elseif UIParent.GetScale then
scale = UIParent:GetScale() or 1
end
local cx, cy = GetCursorPosition()
cx = cx / scale
cy = cy / scale
local dx = cx - mx
local dy = cy - my
local angle
if math.atan2 then
angle = math.deg(math.atan2(dy, dx))
else
if dx == 0 then
angle = dy >= 0 and 90 or -90
else
angle = math.deg(math.atan(dy / dx))
if dx < 0 then angle = angle + 180 end
end
end
BNP_DB.minimapAngle = angle
UpdatePosition()
end
button:SetScript("OnDragStart", function()
this:SetScript("OnUpdate", UpdateAngleFromCursor)
end)
button:SetScript("OnDragStop", function()
this:SetScript("OnUpdate", nil)
UpdateAngleFromCursor()
end)
button:SetScript("OnClick", function()
BNP:ToggleOptions()
end)
button:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_LEFT")
GameTooltip:SetText("Blizz Nameplates+")
GameTooltip:AddLine("Left-click: Open settings", 1, 1, 1)
GameTooltip:AddLine("Drag: Move minimap button", 0.8, 0.8, 0.8)
GameTooltip:Show()
end)
button:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
UpdatePosition()
self.minimapButton = button
end
+15
View File
@@ -0,0 +1,15 @@
BNP = BNP or {}
-- Central visual constants. Keeping these values in one place lets us polish
-- the Blizzard-style presentation without touching aura tracking logic.
BNP.UI = BNP.UI or {
ICON_SIZE = 18,
ICON_OFFSET_Y = 15,
ICON_SPACING = 2,
TIMER_FONT = "Fonts\\FRIZQT__.TTF",
TIMER_SIZE = 8,
TIMER_OFFSET_X = 0,
TIMER_OFFSET_Y = 0,
TIMER_SHADOW_X = 1,
TIMER_SHADOW_Y = -1,
}
Binary file not shown.
+2280
View File
File diff suppressed because it is too large Load Diff
+264
View File
@@ -0,0 +1,264 @@
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())
-- 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.
bar:SetPoint("TOPLEFT", plate.healthbar, "BOTTOMLEFT", 15, -BAR_GAP)
bar:SetPoint("TOPRIGHT", plate.healthbar, "BOTTOMRIGHT", 14, -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("TOPLEFT", plate.healthbar, "BOTTOMLEFT", 0, -BAR_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() 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
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
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() then return end
local now = GetTime()
local plate
for plate in pairs(activeCastPlates) do
if not plate:IsShown() then
MarkCastPlateInactive(plate)
else
local guid = GetPlateGUID(plate)
local cast = guid and casts[guid]
if not cast or now >= cast.endTime then
if guid and cast and now >= cast.endTime then
casts[guid] = nil
end
MarkCastPlateInactive(plate)
else
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(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: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
+169
View File
@@ -0,0 +1,169 @@
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)
+299
View File
@@ -0,0 +1,299 @@
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
+146
View File
@@ -0,0 +1,146 @@
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
)
end
end
end
if n==0 then DEFAULT_CHAT_FRAME:AddMessage("CACHE: EMPTY") 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))
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 == "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 status, /bnp castbars on/off, /bnp scale <0.5-1.5>, /bnp scale reset, /bnp classcolors on/off, /bnp debug on, /bnp debug off")
end
end
+43
View File
@@ -0,0 +1,43 @@
if not BNP.libnameplate then return end
local function CreateMarker(plate)
if not plate then return end
BNP:RegisterPlate(plate)
if not plate.BNPMarker then
local marker = CreateFrame("Frame", nil, plate)
marker:SetWidth(14)
marker:SetHeight(14)
marker:SetPoint("BOTTOM", plate, "TOP", 0, 34)
marker:SetFrameLevel(30)
marker.texture = marker:CreateTexture(nil, "OVERLAY")
marker.texture:SetAllPoints(marker)
marker.texture:SetTexture("Interface\\Buttons\\WHITE8X8")
marker.texture:SetVertexColor(1, 0, 0, 1)
marker.text = marker:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
marker.text:SetPoint("CENTER", marker, "CENTER", 0, 0)
marker.text:SetText("+")
marker.text:SetTextColor(1, 1, 1)
plate.BNPMarker = marker
end
if BNP.debugEnabled then plate.BNPMarker:Show() else plate.BNPMarker:Hide() end
end
table.insert(BNP.libnameplate.OnInit, CreateMarker)
table.insert(BNP.libnameplate.OnShow, CreateMarker)
function BNP:SetDebug(enabled)
BNP.debugEnabled = enabled
BNP_DB = BNP_DB or {}
BNP_DB.debugEnabled = enabled
local plate
for plate in pairs(BNP.plates) do
if plate.BNPMarker then
if enabled then plate.BNPMarker:Show() else plate.BNPMarker:Hide() end
end
end
BNP:Print("Debug-Marker " .. (enabled and "aktiviert" or "deaktiviert") .. ".")
end
+282
View File
@@ -0,0 +1,282 @@
BNP = BNP or {}
local NON_TARGET_ALPHA = 0.55
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 GetTargetGUID()
local exists, guid = UnitExists("target")
if exists and guid then return guid end
return nil
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
-- Current target always stays fully visible.
local plateGUID = GetPlateGUID(plate)
if plateGUID and plateGUID == targetGUID then
return 1
end
-- Only 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.
-- Target Plate on Top 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 target = false
if BNP.IsTargetOnTopEnabled and BNP:IsTargetOnTopEnabled() then
local targetGUID = GetTargetGUID()
local plateGUID = GetPlateGUID(plate)
target = targetGUID and plateGUID and targetGUID == plateGUID
end
local plateLevel = plate.BNPBaseFrameLevel
local wrapperLevel = plate.BNPBaseWrapperLevel or plateLevel
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
end
SetLevelSafe(plate, plateLevel)
if wrapper then
SetLevelSafe(wrapper, wrapperLevel)
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 EnsureTargetGlow(plate)
if not plate then return nil end
if plate.BNPTargetGlow then return plate.BNPTargetGlow end
local anchor = plate.healthbar or plate
local visualParent = plate.BNPScaleWrapper or plate
-- Same glow geometry/position as the known-good reference build.
-- Only the draw parent changes: BACKGROUND of BNP's visual wrapper,
-- so the actual nameplate visuals render above it.
local glow = visualParent:CreateTexture(nil, "BACKGROUND")
glow:SetTexture("Interface\\AddOns\\BlizzNameplatesPlus\\media\\shagu_target_glow.tga")
glow:SetPoint("CENTER", anchor, "CENTER", 10, 0)
-- ShaguPlates uses health width + 60 and health height + 30.
-- Use the actual Blizzard/BNP bar size so this also follows BNP scaling.
local width = anchor.GetWidth and anchor:GetWidth() or 100
local height = anchor.GetHeight and anchor:GetHeight() or 10
glow:SetWidth(width + 59)
glow:SetHeight(height + 22)
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 100
local height = anchor.GetHeight and anchor:GetHeight() or 10
glow:ClearAllPoints()
glow:SetPoint("CENTER", anchor, "CENTER", 10, 0)
glow:SetWidth(width + 59)
glow:SetHeight(height + 22)
if BNP.GetTargetGlowColor then
local r, g, b = BNP:GetTargetGlowColor()
glow:SetVertexColor(r, g, b, 1)
end
glow:Show()
else
glow:Hide()
end
end
local function ApplyTargetAlpha(plate)
if not plate or not plate:IsShown() then return end
local wanted = DesiredAlpha(plate)
if plate:GetAlpha() ~= wanted then
plate:SetAlpha(wanted)
end
end
local function InstallAlphaGuard(plate)
plate = plate or this
if not plate or plate.BNPFullAlphaGuard then return end
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
InstallAlphaGuard(current)
ApplyTargetAlpha(current)
ApplyTargetGlow(current)
ApplyForeignTagVisual(current)
ApplyTargetFrameLevel(current)
end)
end
function BNP:RefreshTargetFocus()
local plate
for plate in pairs(self.plates or {}) do
ApplyTargetAlpha(plate)
ApplyTargetGlow(plate)
ApplyForeignTagVisual(plate)
end
end
function BNP:RefreshNonTargetAlpha()
local plate
for plate in pairs(self.plates or {}) do
ApplyTargetAlpha(plate)
end
end
function BNP:RefreshTargetOnTop()
local plate
for plate in pairs(self.plates or {}) do
ApplyTargetFrameLevel(plate)
end
end
+73
View File
@@ -0,0 +1,73 @@
-- SuperWoW GUID probe.
-- This module does not alter aura tracking or rendering. It only inspects the
-- extended UnitExists return value and the unit token exposed by nameplates.
local function CleanText(text)
if not text then return nil end
text = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
text = string.gsub(text, "|r", "")
return text
end
local function SafePlateToken(plate)
if not plate or not plate.GetName then return nil end
-- SuperWoW extends frame:GetName(index). ShaguTweaks already uses
-- plate:GetName(1) for its SuperWoW nameplate castbar implementation.
local ok, token = pcall(function() return plate:GetName(1) end)
if ok then return token end
return nil
end
local function UnitExistsData(unit)
if not unit then return nil, nil end
local ok, exists, guid = pcall(UnitExists, unit)
if not ok then return nil, nil end
return exists, guid
end
function BNP:GetSuperWoWGUID(unit)
local exists, guid = UnitExistsData(unit)
if exists and guid then return guid end
return nil
end
function BNP:GetPlateSuperWoWData(plate)
local token = SafePlateToken(plate)
local exists, guid = UnitExistsData(token)
return token, exists, guid
end
function BNP:PrintGUIDDiagnostic()
BNP:Print("=== SuperWoW GUID Diagnose ===")
local targetName = UnitName("target")
local targetExists, targetGUID = UnitExistsData("target")
BNP:Print("Target: " .. tostring(targetName or "kein Ziel"))
BNP:Print("UnitExists(target): " .. tostring(targetExists) .. " | GUID: " .. tostring(targetGUID))
local visible = 0
local sameName = 0
local guidMatches = 0
local index = 0
for plate in pairs(BNP.plates) do
if plate and plate:IsShown() then
visible = visible + 1
local plateName = plate.name and plate.name.GetText and CleanText(plate.name:GetText()) or nil
if not targetName or plateName == targetName then
sameName = sameName + 1
index = index + 1
local token, exists, guid = BNP:GetPlateSuperWoWData(plate)
if targetGUID and guid and targetGUID == guid then guidMatches = guidMatches + 1 end
BNP:Print("Plate " .. index .. ": " .. tostring(plateName))
BNP:Print(" Token: " .. tostring(token) .. " | Exists: " .. tostring(exists))
BNP:Print(" GUID: " .. tostring(guid) .. " | Target-Match: " .. tostring(targetGUID and guid == targetGUID or false))
end
end
end
BNP:Print("Sichtbar: " .. visible .. " | gleicher Name: " .. sameName .. " | GUID-Matches: " .. guidMatches)
end
+199
View File
@@ -0,0 +1,199 @@
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
-- Purely visual/read-only overlay. No hooks or writes to the healthbar.
local parent = plate.BNPScaleWrapper or plate
local text = parent:CreateFontString(nil, "OVERLAY")
text:SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE")
text:SetPoint("CENTER", plate.healthbar, "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
+274
View File
@@ -0,0 +1,274 @@
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 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)
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
wrapper.BNPAnchorStates = {}
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
+67
View File
@@ -0,0 +1,67 @@
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)
+177
View File
@@ -0,0 +1,177 @@
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
+181
View File
@@ -0,0 +1,181 @@
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
+498
View File
@@ -0,0 +1,498 @@
BNP = BNP or {}
BNP.unknownDirectEvents = BNP.unknownDirectEvents or {}
-- Beta spell collector -------------------------------------------------------
-- Purpose: help testers report missing Turtle/custom spell IDs without
-- editing Lua files. It records unknown own cast IDs and unknown debuff IDs
-- that newly appear shortly after one of the player's casts.
--
-- It is intentionally diagnostic only: it never changes tracking/rendering.
BNP.unknownBeta = BNP.unknownBeta or {
entries = {},
order = {},
recentCasts = {},
seenByGUID = {},
activeUntil = 0,
}
local _, playerClass = UnitClass("player")
local CLASS_AURAS = {
WARLOCK = BNP.WarlockAuras,
PRIEST = BNP.PriestAuras,
WARRIOR = BNP.WarriorAuras,
ROGUE = BNP.RogueAuras,
HUNTER = BNP.HunterAuras,
MAGE = BNP.MageAuras,
DRUID = BNP.DruidAuras,
SHAMAN = BNP.ShamanAuras,
PALADIN = BNP.PaladinAuras,
}
local AURA_DEFS = CLASS_AURAS[playerClass] or {}
local function GetPlayerGUID()
local exists, guid = UnitExists("player")
if exists and guid then return guid end
return nil
end
local function GetUnitGUID(unit)
local exists, guid = UnitExists(unit)
if exists and guid then return guid end
return nil
end
local function IsExplicitKnownID(spellID)
if not spellID then return false end
-- Dark Harvest is handled by dedicated Warlock logic.
if spellID == 52552 then return true end
local i, j, def
for i = 1, table.getn(AURA_DEFS) do
def = AURA_DEFS[i]
if def.spellIDs then
for j = 1, table.getn(def.spellIDs) do
if def.spellIDs[j] == spellID then return true end
end
end
if def.durations and def.durations[spellID] then
return true
end
end
return false
end
local function SpellName(spellID)
if not spellID or not SpellInfo then return "?" end
local name = SpellInfo(spellID)
return name or "?"
end
local function AddEntry(kind, spellID, spellName, guid, trigger, confidence)
if not spellID then return end
local key = tostring(kind) .. ":" .. tostring(spellID)
local data = BNP.unknownBeta
local old = data.entries[key]
if old then
old.count = (old.count or 1) + 1
old.lastSeen = GetTime()
if confidence == "TARGET" then old.confidence = confidence end
if trigger and not old.trigger then old.trigger = trigger end
return
end
local entry = {
kind = kind,
spellID = spellID,
name = spellName or SpellName(spellID),
class = playerClass or "?",
guid = guid,
trigger = trigger,
confidence = confidence or "?",
count = 1,
firstSeen = GetTime(),
lastSeen = GetTime(),
}
data.entries[key] = entry
table.insert(data.order, key)
end
local function RememberCast(spellID, targetGUID, eventType)
local data = BNP.unknownBeta
local now = GetTime()
data.activeUntil = now + 8.0
local cast = {
time = now,
spellID = spellID,
name = SpellName(spellID),
targetGUID = targetGUID,
eventType = eventType,
}
table.insert(data.recentCasts, cast)
while table.getn(data.recentCasts) > 20 do
table.remove(data.recentCasts, 1)
end
-- Unknown own CAST/CHANNEL IDs are useful too. Some trap/proc systems expose
-- the effect as a separate cast event before the aura is seen.
if spellID and not IsExplicitKnownID(spellID) then
AddEntry("CAST", spellID, cast.name, targetGUID, nil, "OWN")
end
end
local function FindRecentTrigger(guid, now)
local casts = BNP.unknownBeta.recentCasts
local i
for i = table.getn(casts), 1, -1 do
local cast = casts[i]
local age = now - cast.time
if age > 6 then
break
end
if cast.targetGUID and guid and cast.targetGUID == guid then
return cast, "TARGET"
end
-- Short global fallback catches effects such as traps where the placement
-- cast may not carry the eventual victim GUID.
if age <= 1.5 then
return cast, "RECENT"
end
end
return nil, nil
end
local function ScanUnit(unit, guid, now)
if not unit or not guid then return end
local seen = BNP.unknownBeta.seenByGUID[guid]
if not seen then
seen = {}
BNP.unknownBeta.seenByGUID[guid] = seen
end
local current = {}
local i
for i = 1, 32 do
local texture, stacks, dtype, auraSpellID = UnitDebuff(unit, i)
if not texture then break end
if auraSpellID then
current[auraSpellID] = true
if not seen[auraSpellID] and not IsExplicitKnownID(auraSpellID) then
local trigger, confidence = FindRecentTrigger(guid, now)
if trigger then
AddEntry(
"AURA",
auraSpellID,
SpellName(auraSpellID),
guid,
tostring(trigger.name) .. " [" .. tostring(trigger.spellID) .. "]",
confidence
)
end
end
end
end
-- Keep only currently present aura IDs so a later re-application can be
-- detected again without growing this table forever.
BNP.unknownBeta.seenByGUID[guid] = current
end
-- Indirect Effect Audit: traps/procs may never expose a UnitDebuff ID.
BNP.unknownBeta.combat = BNP.unknownBeta.combat or {}
local EFFECT_WINDOW = 8.0
local function IsUsefulEffectLine(raw)
if not raw or raw == "" then return false end
local lower = string.lower(raw)
-- Ignore obvious player-action/noise lines.
if string.find(lower, "you cast ", 1, true) then return false end
if string.find(lower, "auto shot", 1, true) then return false end
if string.find(lower, "login", 1, true) then return false end
-- Keep lines that actually describe a harmful aura/effect or its periodic damage.
if string.find(lower, "afflicted by", 1, true) then return true end
if string.find(lower, "is afflicted", 1, true) then return true end
if string.find(lower, "is affected by", 1, true) then return true end
if string.find(lower, "suffers", 1, true) then return true end
if string.find(lower, "periodic", 1, true) then return true end
return false
end
local function AddCombatEffect(raw)
if not IsUsefulEffectLine(raw) then return end
local now=GetTime()
local casts=BNP.unknownBeta.recentCasts
local trigger=nil
local i
for i=table.getn(casts),1,-1 do
local c=casts[i]
if now-c.time > EFFECT_WINDOW then break end
if c.spellID ~= 75 then trigger=c break end
end
if not trigger then return end
if string.find(string.lower(raw),"auto shot",1,true) then return end
local key=tostring(trigger.spellID)..":"..raw
local old=BNP.unknownBeta.combat[key]
if old then old.count=old.count+1 return end
BNP.unknownBeta.combat[key]={
raw=raw, triggerID=trigger.spellID, triggerName=trigger.name, count=1
}
end
local combatFrame=CreateFrame("Frame")
local combatEvents={
"CHAT_MSG_COMBAT_SELF_HITS",
"CHAT_MSG_SPELL_SELF_DAMAGE",
"CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE",
"CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE",
}
local cei
for cei=1,table.getn(combatEvents) do combatFrame:RegisterEvent(combatEvents[cei]) end
combatFrame:SetScript("OnEvent",function() AddCombatEffect(arg1) end)
local castFrame = CreateFrame("Frame")
castFrame:RegisterEvent("UNIT_CASTEVENT")
castFrame:SetScript("OnEvent", function()
local playerGUID = GetPlayerGUID()
if not playerGUID or arg1 ~= playerGUID then return end
local eventType = arg3
if eventType ~= "CAST" and eventType ~= "CHANNEL" then return end
RememberCast(arg4, arg2, eventType)
end)
local scanner = CreateFrame("Frame")
local elapsed = 0
scanner:SetScript("OnUpdate", function()
elapsed = elapsed + arg1
if elapsed < 0.35 then return end
elapsed = 0
local now = GetTime()
if not BNP.unknownBeta.activeUntil or now > BNP.unknownBeta.activeUntil then
return
end
local targetGUID = GetUnitGUID("target")
if targetGUID then
ScanUnit("target", targetGUID, now)
end
local plate
for plate in pairs(BNP.plates) do
if plate:IsShown() and plate.GetName then
local token = plate:GetName(1)
if token then
local guid = GetUnitGUID(token)
if guid then
ScanUnit(token, guid, now)
end
end
end
end
end)
function BNP:PrintUnknownBeta(verbose)
local data = self.unknownBeta
self:Print("=== BNP Beta: Unknown Auras (" .. tostring(playerClass or "?") .. ") ===")
local auraCount = 0
local i
for i = 1, table.getn(data.order) do
local entry = data.entries[data.order[i]]
if entry and entry.kind == "AURA" then
auraCount = auraCount + 1
local line =
tostring(auraCount) ..
" | AURA | ID " .. tostring(entry.spellID) ..
" | " .. tostring(entry.name) ..
" | x" .. tostring(entry.count or 1)
if entry.confidence then
line = line .. " | " .. tostring(entry.confidence)
end
self:Print(line)
if entry.trigger then
self:Print(" Trigger: " .. tostring(entry.trigger))
end
end
end
local combatCount = 0
local ck
for ck in pairs(data.combat or {}) do combatCount = combatCount + 1 end
if combatCount > 0 then
self:Print("--- Indirect Effects ---")
local key, e
for key, e in pairs(data.combat) do
self:Print("EFFECT | Trigger " .. tostring(e.triggerName) .. " [" .. tostring(e.triggerID) .. "] | x" .. tostring(e.count))
self:Print(" " .. tostring(e.raw))
end
end
if auraCount == 0 and combatCount == 0 then
self:Print("No unknown negative auras/effects found since login.")
end
if verbose then
self:Print("--- Verbose: unknown player cast IDs ---")
local castCount = 0
for i = 1, table.getn(data.order) do
local entry = data.entries[data.order[i]]
if entry and entry.kind == "CAST" then
castCount = castCount + 1
self:Print(
tostring(castCount) ..
" | CAST | ID " .. tostring(entry.spellID) ..
" | " .. tostring(entry.name) ..
" | x" .. tostring(entry.count or 1)
)
end
end
if castCount == 0 then
self:Print("No unknown cast IDs.")
end
else
self:Print("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
-- 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
+85
View File
@@ -0,0 +1,85 @@
BNP = BNP or {}
-- Expanded against ShaguPlates enUS debuff tables.
BNP.DruidAuras = {
{
key = "moonfire",
names = { "Moonfire" },
duration = 18,
},
{
key = "rake",
names = { "Rake" },
duration = 9,
},
{
key = "rip",
names = { "Rip" },
duration = 12,
},
{
key = "faerie_fire",
names = { "Faerie Fire" },
duration = 40,
},
{
key = "faerie_fire_feral",
names = { "Faerie Fire (Feral)" },
duration = 40,
},
{
key = "entangling_roots",
names = { "Entangling Roots" },
duration = 27,
},
{
key = "insect_swarm",
names = { "Insect Swarm" },
duration = 18,
},
{
key = "bash",
names = { "Bash" },
duration = 4,
},
{
key = "pounce",
names = { "Pounce" },
duration = 2,
},
{
key = "pounce_bleed",
names = { "Pounce Bleed" },
duration = 18,
},
{
key = "hibernate",
names = { "Hibernate" },
duration = 40,
},
{
key = "demoralizing_roar",
names = { "Demoralizing Roar" },
duration = 30,
},
{
key = "feral_charge_effect",
names = { "Feral Charge Effect" },
duration = 4,
},
{
key = "challenging_roar",
names = { "Challenging Roar" },
duration = 6,
},
{
key = "lacerate",
names = { "Lacerate" },
duration = 8,
},
{
key = "mangle",
names = { "Mangle" },
duration = 2,
},
}
+97
View File
@@ -0,0 +1,97 @@
BNP = BNP or {}
-- Expanded against ShaguPlates enUS debuff tables.
BNP.HunterAuras = {
{
key = "immolation_trap_effect",
names = { "Immolation Trap Effect" },
duration = 15,
spellIDs = { 14298 },
},
{
key = "explosive_trap_effect",
names = { "Explosive Trap Effect" },
duration = 20,
},
{
key = "freezing_trap_effect",
names = { "Freezing Trap Effect" },
duration = 20,
},
{
key = "serpent_sting",
names = { "Serpent Sting" },
duration = 15,
},
{
key = "viper_sting",
names = { "Viper Sting" },
duration = 8,
},
{
key = "scorpid_sting",
names = { "Scorpid Sting" },
duration = 20,
},
{
key = "improved_scorpid_sting",
names = { "Improved Scorpid Sting" },
duration = 20,
},
{
key = "wing_clip",
names = { "Wing Clip" },
duration = 10,
},
{
key = "improved_wing_clip",
names = { "Improved Wing Clip" },
duration = 5,
},
{
key = "concussive_shot",
names = { "Concussive Shot" },
duration = 4,
},
{
key = "improved_concussive_shot",
names = { "Improved Concussive Shot" },
duration = 3,
},
{
key = "scatter_shot",
names = { "Scatter Shot" },
duration = 4,
},
{
key = "wyvern_sting",
names = { "Wyvern Sting" },
duration = 12,
},
{
key = "entrapment",
names = { "Entrapment" },
duration = 5,
},
{
key = "counterattack",
names = { "Counterattack" },
duration = 5,
},
{
key = "intimidation",
names = { "Intimidation" },
duration = 3,
},
{
key = "scare_beast",
names = { "Scare Beast" },
duration = 20,
},
{
key = "hunters_mark",
names = { "Hunter's Mark" },
duration = 120,
shared = true,
},
}
+70
View File
@@ -0,0 +1,70 @@
BNP = BNP or {}
-- Expanded against ShaguPlates enUS debuff tables.
BNP.MageAuras = {
{
key = "frostbolt",
names = { "Frostbolt" },
duration = 9,
},
{
key = "fireball",
names = { "Fireball" },
duration = 8,
},
{
key = "pyroblast",
names = { "Pyroblast" },
duration = 12,
},
{
key = "polymorph",
names = { "Polymorph" },
duration = 50,
},
{
key = "cone_of_cold",
names = { "Cone of Cold" },
duration = 8,
},
{
key = "frost_nova",
names = { "Frost Nova" },
duration = 8,
},
{
key = "counterspell",
names = { "Counterspell" },
duration = 10,
},
{
key = "counterspell_silence",
names = { "Counterspell - Silenced" },
duration = 4,
},
{
key = "ignite",
names = { "Ignite" },
duration = 4,
},
{
key = "impact",
names = { "Impact" },
duration = 2,
},
{
key = "frostbite",
names = { "Frostbite" },
duration = 5,
},
{
key = "fire_vulnerability",
names = { "Fire Vulnerability" },
duration = 30,
},
{
key = "winters_chill",
names = { "Winter's Chill" },
duration = 15,
},
}
+54
View File
@@ -0,0 +1,54 @@
BNP = BNP or {}
-- Expanded against ShaguPlates enUS debuff tables.
BNP.PaladinAuras = {
{
key = "judgement_light",
names = { "Judgement of Light" },
duration = 10,
spellIDs = { 20344 },
},
{
key = "judgement_wisdom",
names = { "Judgement of Wisdom" },
duration = 10,
spellIDs = { 20355 },
},
{
key = "judgement_justice",
names = { "Judgement of Justice" },
duration = 10,
spellIDs = { 20184 },
},
{
key = "judgement_crusader",
names = { "Judgement of the Crusader" },
duration = 10,
spellIDs = { 20302 },
},
{
key = "hammer_of_justice",
names = { "Hammer of Justice" },
duration = 6,
},
{
key = "repentance",
names = { "Repentance" },
duration = 6,
},
{
key = "turn_undead",
names = { "Turn Undead" },
duration = 20,
},
{
key = "hand_of_reckoning",
names = { "Hand of Reckoning" },
duration = 3,
},
{
key = "crusader_strike",
names = { "Crusader Strike" },
duration = 30,
},
}
+75
View File
@@ -0,0 +1,75 @@
BNP = BNP or {}
-- Expanded against ShaguPlates enUS debuff tables.
BNP.PriestAuras = {
{
key = "shadow_word_pain",
names = { "Shadow Word: Pain", "Schattenwort: Schmerz" },
duration = 18,
},
{
key = "devouring_plague",
names = { "Devouring Plague" },
duration = 24,
},
{
key = "vampiric_embrace",
names = { "Vampiric Embrace" },
duration = 60,
},
{
key = "mind_flay",
names = { "Mind Flay" },
duration = 3,
},
{
key = "psychic_scream",
names = { "Psychic Scream" },
duration = 8,
},
{
key = "silence",
names = { "Silence" },
duration = 5,
},
{
key = "hex_of_weakness",
names = { "Hex of Weakness" },
duration = 120,
},
{
key = "touch_of_weakness",
names = { "Touch of Weakness" },
duration = 120,
},
{
key = "holy_fire",
names = { "Holy Fire" },
duration = 10,
},
{
key = "blackout",
names = { "Blackout" },
duration = 3,
},
{
key = "starshards",
names = { "Starshards" },
duration = 6,
},
{
key = "shadow_vulnerability",
names = { "Shadow Vulnerability" },
duration = 15,
},
{
key = "shackle_undead",
names = { "Shackle Undead" },
duration = 50,
},
{
key = "mind_control",
names = { "Mind Control" },
duration = 60,
},
}
+153
View File
@@ -0,0 +1,153 @@
BNP = BNP or {}
-- Shadow-only spell registry.
-- This module never controls tracking or rendering. It only audits whether a
-- successful local cast could be resolved by a future SpellDB implementation.
BNP.SpellDBShadow = BNP.SpellDBShadow or {
byID = {},
byName = {},
byTexture = {},
stats = { total = 0, id = 0, name = 0, texture = 0, unknown = 0, ignored = 0 },
recent = {},
}
local DB = BNP.SpellDBShadow
-- Successful SuperWoW cast events which are not hostile target auras and must
-- not count as missing SpellDB entries. More can be added as Turtle exposes
-- additional proc/engine events during testing.
local IGNORED_SPELL_IDS = {
[17941] = true, -- Shadow Trance / Nightfall proc
[836] = true, -- LOGINEFFECT engine event
}
local function Lower(value)
if not value then return nil end
return string.lower(value)
end
local function RegisterDef(def)
if not def then return end
if def.names then
for _, name in ipairs(def.names) do
DB.byName[Lower(name)] = def
end
end
if def.textureMatch then
DB.byTexture[Lower(def.textureMatch)] = def
end
if def.durations then
for spellID in pairs(def.durations) do
DB.byID[spellID] = def
end
end
if def.spellIDs then
for _, spellID in ipairs(def.spellIDs) do
DB.byID[spellID] = def
end
end
end
for _, def in ipairs(BNP.WarlockAuras or {}) do
RegisterDef(def)
end
local function FindByTexture(texture)
local lowerTexture = Lower(texture)
if not lowerTexture then return nil end
for fragment, def in pairs(DB.byTexture) do
if string.find(lowerTexture, fragment) then
return def
end
end
return nil
end
local function PushRecent(spellID, spellName, result, key)
local recent = DB.recent
table.insert(recent, 1, {
id = spellID,
name = spellName or "?",
result = result,
key = key or "?",
})
while table.getn(recent) > 10 do
table.remove(recent)
end
end
function BNP:AuditSpellDB(spellID)
if not spellID or not SpellInfo then return end
if IGNORED_SPELL_IDS[spellID] then
DB.stats.ignored = (DB.stats.ignored or 0) + 1
return
end
local spellName, _, texture = SpellInfo(spellID)
local def = DB.byID[spellID]
local result = "ID"
if not def and spellName then
def = DB.byName[Lower(spellName)]
result = "NAME"
end
if not def and texture then
def = FindByTexture(texture)
result = "TEXTURE"
end
DB.stats.total = DB.stats.total + 1
if def then
if result == "ID" then
DB.stats.id = DB.stats.id + 1
elseif result == "NAME" then
DB.stats.name = DB.stats.name + 1
else
DB.stats.texture = DB.stats.texture + 1
end
PushRecent(spellID, spellName, result, def.key)
else
DB.stats.unknown = DB.stats.unknown + 1
PushRecent(spellID, spellName, "UNKNOWN", "-")
end
end
function BNP:PrintSpellDBAudit()
local stats = DB.stats
self:Print("SpellDB Schattenmodus – Renderer unverändert.")
self:Print("Casts: " .. stats.total .. " | ID: " .. stats.id .. " | Name: " .. stats.name .. " | Textur: " .. stats.texture .. " | Unbekannt: " .. stats.unknown .. " | Ignoriert: " .. (stats.ignored or 0))
if table.getn(DB.recent) == 0 then
self:Print("Noch keine eigenen erfolgreichen Casts protokolliert.")
return
end
self:Print("Letzte Casts:")
for i = 1, table.getn(DB.recent) do
local entry = DB.recent[i]
self:Print(entry.name .. " [" .. tostring(entry.id) .. "] -> " .. entry.result .. " (" .. entry.key .. ")")
end
end
function BNP:ResetSpellDBAudit()
DB.stats.total = 0
DB.stats.id = 0
DB.stats.name = 0
DB.stats.texture = 0
DB.stats.unknown = 0
DB.stats.ignored = 0
for i = table.getn(DB.recent), 1, -1 do
table.remove(DB.recent, i)
end
self:Print("SpellDB-Protokoll zurückgesetzt.")
end
+75
View File
@@ -0,0 +1,75 @@
BNP = BNP or {}
-- Expanded against ShaguPlates enUS debuff tables.
BNP.RogueAuras = {
{
key = "garrote",
names = { "Garrote" },
duration = 18,
},
{
key = "rupture",
names = { "Rupture" },
duration = 6,
},
{
key = "gouge",
names = { "Gouge" },
duration = 4,
},
{
key = "kidney_shot",
names = { "Kidney Shot" },
duration = 6,
},
{
key = "cheap_shot",
names = { "Cheap Shot" },
duration = 4,
},
{
key = "sap",
names = { "Sap" },
duration = 45,
},
{
key = "blind",
names = { "Blind" },
duration = 10,
},
{
key = "expose_armor",
names = { "Expose Armor" },
duration = 30,
},
{
key = "kick_silenced",
names = { "Kick - Silenced" },
duration = 2,
},
{
key = "hemorrhage",
names = { "Hemorrhage" },
duration = 15,
},
{
key = "crippling_poison",
names = { "Crippling Poison" },
duration = 12,
},
{
key = "mind_numbing_poison",
names = { "Mind-numbing Poison" },
duration = 10,
},
{
key = "wound_poison",
names = { "Wound Poison" },
duration = 15,
},
{
key = "deadly_poison",
names = { "Deadly Poison", "Deadly Poison II", "Deadly Poison III", "Deadly Poison IV", "Deadly Poison V" },
duration = 12,
},
}
+40
View File
@@ -0,0 +1,40 @@
BNP = BNP or {}
-- Expanded against ShaguPlates enUS debuff tables.
BNP.ShamanAuras = {
{
key = "flame_shock",
names = { "Flame Shock" },
duration = 12,
},
{
key = "frost_shock",
names = { "Frost Shock" },
duration = 8,
},
{
key = "stormstrike",
names = { "Stormstrike" },
duration = 12,
},
{
key = "earth_shock",
names = { "Earth Shock" },
duration = 2,
},
{
key = "earthbind",
names = { "Earthbind" },
duration = 5,
},
{
key = "earthgrab",
names = { "Earthgrab" },
duration = 4,
},
{
key = "frostbrand_attack",
names = { "Frostbrand Attack" },
duration = 8,
},
}
+166
View File
@@ -0,0 +1,166 @@
BNP = BNP or {}
-- Warlock debuffs supported by the SuperWoW own-cast tracker.
-- Durations are Vanilla defaults. Rank-specific values are used where ranks
-- materially change the duration. Server-specific effects such as Dark
-- Harvest are intentionally handled in a later compatibility pass.
BNP.WarlockAuras = {
{
key = "corruption",
names = { "Corruption", "Verderbnis" },
texture = "Interface\\Icons\\Spell_Shadow_AbominationExplosion",
textureMatch = "abominationexplosion",
duration = 18,
durations = { [172] = 12, [6222] = 15, [6223] = 18, [7648] = 18, [11671] = 18, [11672] = 18 },
},
{
key = "curse_of_agony",
category = "CURSE",
names = { "Curse of Agony", "Fluch der Pein" },
texture = "Interface\\Icons\\Spell_Shadow_CurseOfSargeras",
textureMatch = "curseofsargeras",
duration = 24,
spellIDs = { 980, 1014, 6217, 11711, 11712, 11713 },
},
{
key = "siphon_life",
names = { "Siphon Life", "Lebensentzug" },
texture = "Interface\\Icons\\Spell_Shadow_Requiem",
textureMatch = "requiem",
duration = 30,
spellIDs = { 18265, 18879, 18880, 18881 },
},
{
key = "immolate",
names = { "Immolate", "Feuerbrand" },
texture = "Interface\\Icons\\Spell_Fire_Immolation",
textureMatch = "immolation",
duration = 15,
spellIDs = { 348, 707, 1094, 2941, 11665, 11667, 11668 },
},
{
key = "curse_of_doom",
category = "CURSE",
names = { "Curse of Doom", "Fluch der Verdammnis" },
texture = "Interface\\Icons\\Spell_Shadow_AuraOfDarkness",
textureMatch = "auraofdarkness",
duration = 60,
spellIDs = { 603, 30910 },
},
{
key = "curse_of_weakness",
category = "CURSE",
names = { "Curse of Weakness", "Fluch der Schwäche" },
texture = "Interface\\Icons\\Spell_Shadow_CurseOfMannoroth",
textureMatch = "curseofmannoroth",
duration = 120,
spellIDs = { 702, 1108, 6205, 7646, 11707, 11708 },
},
{
key = "curse_of_recklessness",
category = "CURSE",
names = { "Curse of Recklessness", "Fluch der Tollkühnheit" },
texture = "Interface\\Icons\\Spell_Shadow_UnholyStrength",
textureMatch = "unholystrength",
duration = 120,
spellIDs = { 704, 7658, 7659, 11717 },
},
{
key = "curse_of_tongues",
category = "CURSE",
names = { "Curse of Tongues", "Fluch der Sprachen" },
texture = "Interface\\Icons\\Spell_Shadow_CurseOfTounges",
textureMatch = "curseoftounges",
duration = 30,
spellIDs = { 1714, 11719 },
},
{
key = "curse_of_elements",
category = "CURSE",
names = { "Curse of the Elements", "Curse of Elements", "Fluch der Elemente" },
texture = "Interface\\Icons\\Spell_Shadow_ChillTouch",
textureMatch = "chilltouch",
duration = 300,
spellIDs = { 1490, 11721, 11722 },
},
{
key = "curse_of_shadow",
category = "CURSE",
names = { "Curse of Shadow", "Fluch der Schatten" },
texture = "Interface\\Icons\\Spell_Shadow_CurseOfAchimonde",
textureMatch = "curseofachimonde",
duration = 300,
spellIDs = { 17862, 17937 },
},
{
key = "curse_of_exhaustion",
category = "CURSE",
names = { "Curse of Exhaustion", "Fluch der Erschöpfung" },
texture = "Interface\\Icons\\Spell_Shadow_GrimWard",
textureMatch = "grimward",
duration = 12,
spellIDs = { 18223 },
},
{
key = "fear",
names = { "Fear", "Furcht" },
texture = "Interface\\Icons\\Spell_Shadow_Possession",
textureMatch = "possession",
duration = 20,
durations = { [5782] = 10, [6213] = 15, [6215] = 20 },
},
{
key = "howl_of_terror",
names = { "Howl of Terror", "Schreckensgeheul" },
texture = "Interface\\Icons\\Spell_Shadow_DeathScream",
textureMatch = "deathscream",
duration = 15,
durations = { [5484] = 10, [17928] = 15 },
},
{
key = "banish",
names = { "Banish", "Verbannen" },
texture = "Interface\\Icons\\Spell_Shadow_Cripple",
textureMatch = "cripple",
duration = 30,
durations = { [710] = 20, [18647] = 30 },
},
{
key = "death_coil",
names = { "Death Coil", "Todesmantel" },
texture = "Interface\\Icons\\Spell_Shadow_DeathCoil",
textureMatch = "deathcoil",
duration = 3,
spellIDs = { 6789, 17925, 17926 },
},
{
key = "drain_life",
names = { "Drain Life", "Blutsauger" },
texture = "Interface\\Icons\\Spell_Shadow_LifeDrain02_Purple",
textureMatch = "lifedrain02",
duration = 5,
spellIDs = { 689, 699, 709, 7651, 11699, 11700 },
},
{
key = "drain_soul",
names = { "Drain Soul", "Seelendieb" },
texture = "Interface\\Icons\\Spell_Shadow_Haunting",
textureMatch = "haunting",
duration = 15,
spellIDs = { 1120, 8288, 8289, 11675 },
},
{
key = "enslave_demon",
names = { "Enslave Demon", "Dämonensklave" },
texture = "Interface\\Icons\\Spell_Shadow_EnslaveDemon",
textureMatch = "enslavedemon",
duration = 300,
spellIDs = { 1098, 11725, 11726 },
},
{
key = "shadow_vulnerability",
names = { "Shadow Vulnerability" },
duration = 10,
spellIDs = { 17794 },
},
}
+109
View File
@@ -0,0 +1,109 @@
BNP = BNP or {}
-- Expanded against ShaguPlates enUS debuff tables.
BNP.WarriorAuras = {
{
key = "rend",
names = { "Rend" },
duration = 21,
},
{
key = "hamstring",
names = { "Hamstring" },
duration = 15,
},
{
key = "improved_hamstring",
names = { "Improved Hamstring" },
duration = 5,
},
{
key = "thunder_clap",
names = { "Thunder Clap" },
duration = 30,
aoe = true,
},
{
key = "demoralizing_shout",
names = { "Demoralizing Shout" },
duration = 30,
aoe = true,
},
{
key = "sunder_armor",
names = { "Sunder Armor" },
duration = 30,
},
{
key = "piercing_howl",
names = { "Piercing Howl" },
duration = 6,
aoe = true,
},
{
key = "mortal_strike",
names = { "Mortal Strike" },
duration = 10,
},
{
key = "intimidating_shout",
names = { "Intimidating Shout" },
duration = 8,
},
{
key = "mocking_blow",
names = { "Mocking Blow" },
duration = 6,
},
{
key = "disarm",
names = { "Disarm" },
duration = 10,
},
{
key = "taunt",
names = { "Taunt" },
duration = 3,
},
{
key = "challenging_shout",
names = { "Challenging Shout" },
duration = 6,
aoe = true,
},
{
key = "charge_stun",
names = { "Charge Stun" },
duration = 1,
},
{
key = "intercept_stun",
names = { "Intercept Stun" },
duration = 3,
},
{
key = "concussion_blow",
names = { "Concussion Blow" },
duration = 5,
},
{
key = "revenge_stun",
names = { "Revenge Stun" },
duration = 3,
},
{
key = "pummel",
names = { "Pummel" },
duration = 4,
},
{
key = "shield_bash",
names = { "Shield Bash" },
duration = 6,
},
{
key = "deep_wound",
names = { "Deep Wound", "Deep Wounds" },
duration = 12,
},
}