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