Files
BlizzNameplatesPlus/modules/personalnameplate.lua
T
OEVG 2f671797f1 Add OEVG nameplate appearance options
Match ShaguTweaks border themes, add modern health and cast textures, and make the level number adjustable. Keep target color on the border and use a dark masked level inset.
2026-11-08 23:05:57 -04:00

1004 lines
34 KiB
Lua

BNP = BNP or {}
-- Personal Nameplate -------------------------------------------------------
-- A lightweight player resource display that visually follows BNP's classic
-- nameplate styling. It is screen-space UI (not a projected world nameplate),
-- so it stays stable beneath the player and does not interfere with Blizzard's
-- nameplate discovery/click handling.
local FRAME_WIDTH = 128
local FRAME_HEIGHT = 45
local HEALTH_WIDTH = 104
local HEALTH_HEIGHT = 9
local POWER_WIDTH = 104
local POWER_HEIGHT = 5
local PERSONAL_DEBUFF_MAX_DURATION = 60
local PERSONAL_DEBUFF_MAX_ICONS = 6
local PERSONAL_DEBUFF_SPACING = 1
local PERSONAL_BUFF_MAX_DURATION = 60
local PERSONAL_BUFF_MAX_ICONS = 6
local PERSONAL_BUFF_SPACING = 1
-- Match BNP health text anchoring: healthbar CENTER +3px. The personal frame
-- is wider than the healthbar because of the level medallion, so centering
-- debuffs on the whole frame makes the row look shifted to the right.
local PERSONAL_HEALTH_TEXT_X = 3
local PERSONAL_HEALTH_TEXT_Y = 1
local PERSONAL_POWER_TEXT_Y = 1
local PERSONAL_CONTENT_CENTER_X = (5 + HEALTH_WIDTH / 2 + PERSONAL_HEALTH_TEXT_X) - (FRAME_WIDTH / 2)
local STATUS_TEXTURE = "Interface\\TargetingFrame\\UI-StatusBar"
local BORDER_TEXTURE = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\classic_nameplate_border"
-- The classic texture's health-bar portion occupies the first 108/128 pixels;
-- the final 20 pixels are the round level medallion. Cropping at 108 gives us
-- a native-looking closed border when the personal level is hidden.
local NO_LEVEL_BORDER_WIDTH = 108
local NO_LEVEL_BORDER_TEX_RIGHT = 108 / 128
local NO_LEVEL_BORDER_X = (FRAME_WIDTH - NO_LEVEL_BORDER_WIDTH) / 2
local NO_LEVEL_HEALTH_X = (FRAME_WIDTH - HEALTH_WIDTH) / 2
local NO_LEVEL_POWER_X = NO_LEVEL_HEALTH_X
local FONT = "Fonts\\FRIZQT__.TTF"
local classFallback = {
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 powerColors = {
[0] = { 0.00, 0.45, 1.00 }, -- Mana
[1] = { 1.00, 0.12, 0.12 }, -- Rage
[2] = { 1.00, 0.50, 0.25 }, -- Focus (custom clients/classes)
[3] = { 1.00, 0.85, 0.10 }, -- Energy
[4] = { 0.00, 0.70, 1.00 }, -- Happiness / fallback legacy resource
}
-- 1.12/custom clients are inconsistent here: UnitManaType may return the
-- old numeric id, a token such as "ENERGY", or a token as a second return.
-- Normalize all variants so the personal power bar never falls back to the
-- StatusBar's default white color for Rogues/Cat Form.
local powerTokenToType = {
MANA = 0,
RAGE = 1,
FOCUS = 2,
ENERGY = 3,
HAPPINESS = 4,
}
local function NormalizePowerType(value)
if type(value) == "number" then
return powerColors[value] and value or nil
end
if type(value) == "string" then
local numeric = tonumber(value)
if numeric and powerColors[numeric] then return numeric end
return powerTokenToType[string.upper(value)]
end
return nil
end
local function GetPlayerPowerType()
-- Prefer the newer API when a custom client exposes it.
if UnitPowerType then
local a, b = UnitPowerType("player")
local powerType = NormalizePowerType(a) or NormalizePowerType(b)
if powerType ~= nil then return powerType end
end
if UnitManaType then
local a, b = UnitManaType("player")
local powerType = NormalizePowerType(a) or NormalizePowerType(b)
if powerType ~= nil then return powerType end
end
-- Safe class fallbacks for the two classes whose resource is fixed in
-- Vanilla. Druids are intentionally not forced because their resource
-- changes with form and should be reported by UnitManaType/UnitPowerType.
local _, class = UnitClass("player")
if class == "ROGUE" then return 3 end
if class == "WARRIOR" then return 1 end
return 0
end
local personalFrame
local inCombat = false
local updateElapsed = 0
local debuffScanElapsed = 0
local buffScanElapsed = 0
local function FormatValue(value)
value = tonumber(value) or 0
if value < 0 then value = 0 end
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"
end
return tostring(math.floor(value + 0.5))
end
local function CreateSolidTexture(parent, layer)
local texture = parent:CreateTexture(nil, layer or "BACKGROUND")
texture:SetTexture("Interface\\ChatFrame\\ChatFrameBackground")
return texture
end
local function SetThinBorderColor(frame, r, g, b, a)
if not frame then return end
local edges = frame.BNPEdges
if not edges then return end
local i
for i = 1, table.getn(edges) do
edges[i]:SetVertexColor(r, g, b, a or 1)
end
end
local function CreateThinBorder(parent)
local edges = {}
local top = CreateSolidTexture(parent, "OVERLAY")
top:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, 0)
top:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, 0)
top:SetHeight(1)
table.insert(edges, top)
local bottom = CreateSolidTexture(parent, "OVERLAY")
bottom:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 0, 0)
bottom:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 0, 0)
bottom:SetHeight(1)
table.insert(edges, bottom)
local left = CreateSolidTexture(parent, "OVERLAY")
left:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, 0)
left:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 0, 0)
left:SetWidth(1)
table.insert(edges, left)
local right = CreateSolidTexture(parent, "OVERLAY")
right:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, 0)
right:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 0, 0)
right:SetWidth(1)
table.insert(edges, right)
parent.BNPEdges = edges
end
local function CreatePersonalFrame()
if personalFrame then return personalFrame end
local frame = CreateFrame("Frame", "BNPPersonalNameplate", UIParent)
frame:SetWidth(FRAME_WIDTH)
frame:SetHeight(FRAME_HEIGHT)
frame:SetFrameStrata("MEDIUM")
frame:SetFrameLevel(8)
frame:EnableMouse(false)
frame:Hide()
-- Main health bar, sized to sit inside BNP's classic nameplate border art.
local health = CreateFrame("StatusBar", nil, frame)
health:SetWidth(HEALTH_WIDTH)
health:SetHeight(HEALTH_HEIGHT)
health:SetPoint("TOPLEFT", frame, "TOPLEFT", 5, -19)
health:SetStatusBarTexture(STATUS_TEXTURE)
health:SetMinMaxValues(0, 1)
health:SetValue(1)
frame.healthbar = health
local healthBG = CreateSolidTexture(health, "BACKGROUND")
healthBG:SetAllPoints(health)
healthBG:SetVertexColor(0, 0, 0, 1)
frame.healthBG = healthBG
-- Put border art on a tiny higher-level overlay frame. Child StatusBars can
-- otherwise render above parent textures on old clients and eat the rim.
local overlay = CreateFrame("Frame", nil, frame)
overlay:SetAllPoints(frame)
overlay:SetFrameLevel(frame:GetFrameLevel() + 4)
overlay:EnableMouse(false)
frame.overlay = overlay
local border = overlay:CreateTexture(nil, "ARTWORK")
border:SetTexture(BORDER_TEXTURE)
border:SetWidth(128)
border:SetHeight(32)
border:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
frame.border = border
local healthText = health:CreateFontString(nil, "OVERLAY")
healthText:SetFont(FONT, 8, "OUTLINE")
healthText:SetPoint("CENTER", health, "CENTER", PERSONAL_HEALTH_TEXT_X, PERSONAL_HEALTH_TEXT_Y)
healthText:SetTextColor(1, 1, 1)
frame.healthText = healthText
-- BNP's classic border contains the small round level medallion at the right.
local levelText = overlay:CreateFontString(nil, "OVERLAY")
levelText:SetFont(FONT, 8, "OUTLINE")
levelText:SetPoint("CENTER", frame, "TOPLEFT", 115, -24)
levelText:SetTextColor(1, 0.82, 0)
frame.levelText = levelText
-- Slim power bar below the health nameplate. The 1px rim uses the same
-- gold/dark language as BNP's border while keeping the resource bar compact.
local powerHolder = CreateFrame("Frame", nil, frame)
powerHolder:SetWidth(108)
powerHolder:SetHeight(9)
powerHolder:SetPoint("TOPLEFT", frame, "TOPLEFT", 3, -34)
CreateThinBorder(powerHolder)
frame.powerHolder = powerHolder
local powerBG = CreateSolidTexture(powerHolder, "BACKGROUND")
powerBG:SetPoint("TOPLEFT", powerHolder, "TOPLEFT", 1, -1)
powerBG:SetPoint("BOTTOMRIGHT", powerHolder, "BOTTOMRIGHT", -1, 1)
powerBG:SetVertexColor(0, 0, 0, 0.78)
frame.powerBG = powerBG
local power = CreateFrame("StatusBar", nil, powerHolder)
power:SetWidth(POWER_WIDTH)
power:SetHeight(POWER_HEIGHT)
power:SetPoint("CENTER", powerHolder, "CENTER", 0, 0)
power:SetStatusBarTexture(STATUS_TEXTURE)
power:SetMinMaxValues(0, 1)
power:SetValue(1)
frame.powerbar = power
local powerText = power:CreateFontString(nil, "OVERLAY")
powerText:SetFont(FONT, 7, "OUTLINE")
powerText:SetPoint("CENTER", power, "CENTER", 0, PERSONAL_POWER_TEXT_Y)
powerText:SetTextColor(1, 1, 1)
frame.powerText = powerText
-- Short player debuffs sit centered above the personal plate. They use the
-- same compact icon + countdown language as BNP's normal aura row.
local debuffHolder = CreateFrame("Frame", nil, frame)
debuffHolder:SetWidth(FRAME_WIDTH)
debuffHolder:SetHeight(22)
debuffHolder:SetPoint("BOTTOM", frame, "TOP", PERSONAL_CONTENT_CENTER_X, 1)
debuffHolder:SetFrameLevel(frame:GetFrameLevel() + 6)
debuffHolder:EnableMouse(false)
debuffHolder.icons = {}
frame.debuffHolder = debuffHolder
local i
for i = 1, PERSONAL_DEBUFF_MAX_ICONS do
local icon = CreateFrame("Frame", nil, debuffHolder)
icon:SetWidth(18)
icon:SetHeight(18)
icon:SetFrameLevel(debuffHolder:GetFrameLevel())
icon:EnableMouse(false)
local iconBG = CreateSolidTexture(icon, "BACKGROUND")
iconBG:SetPoint("TOPLEFT", icon, "TOPLEFT", -1, 1)
iconBG:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 1, -1)
iconBG:SetVertexColor(0, 0, 0, 0.95)
local texture = icon:CreateTexture(nil, "ARTWORK")
texture:SetAllPoints(icon)
icon.texture = texture
local timer = icon:CreateFontString(nil, "OVERLAY")
timer:SetFont(FONT, 8, "OUTLINE")
timer:SetPoint("CENTER", icon, "CENTER", 0, 0)
timer:SetTextColor(1, 1, 1)
icon.timer = timer
local stack = icon:CreateFontString(nil, "OVERLAY")
stack:SetFont(FONT, 7, "OUTLINE")
stack:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", -1, 1)
stack:SetTextColor(1, 1, 1)
stack:SetText("")
icon.stack = stack
icon:Hide()
debuffHolder.icons[i] = icon
end
debuffHolder:Hide()
-- Short player buffs (HoTs, absorbs, short defensive effects) use a second
-- compact row. When debuffs are visible this row sits directly above them;
-- otherwise it drops down to the normal aura anchor above the plate.
local buffHolder = CreateFrame("Frame", nil, frame)
buffHolder:SetWidth(FRAME_WIDTH)
buffHolder:SetHeight(22)
buffHolder:SetPoint("BOTTOM", frame, "TOP", PERSONAL_CONTENT_CENTER_X, 1)
buffHolder:SetFrameLevel(frame:GetFrameLevel() + 6)
buffHolder:EnableMouse(false)
buffHolder.icons = {}
frame.buffHolder = buffHolder
for i = 1, PERSONAL_BUFF_MAX_ICONS do
local icon = CreateFrame("Frame", nil, buffHolder)
icon:SetWidth(18)
icon:SetHeight(18)
icon:SetFrameLevel(buffHolder:GetFrameLevel())
icon:EnableMouse(false)
local iconBG = CreateSolidTexture(icon, "BACKGROUND")
iconBG:SetPoint("TOPLEFT", icon, "TOPLEFT", -1, 1)
iconBG:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 1, -1)
iconBG:SetVertexColor(0, 0, 0, 0.95)
local texture = icon:CreateTexture(nil, "ARTWORK")
texture:SetAllPoints(icon)
icon.texture = texture
local timer = icon:CreateFontString(nil, "OVERLAY")
timer:SetFont(FONT, 8, "OUTLINE")
timer:SetPoint("CENTER", icon, "CENTER", 0, 0)
timer:SetTextColor(1, 1, 1)
icon.timer = timer
local stack = icon:CreateFontString(nil, "OVERLAY")
stack:SetFont(FONT, 7, "OUTLINE")
stack:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", -1, 1)
stack:SetTextColor(1, 1, 1)
stack:SetText("")
icon.stack = stack
icon:Hide()
buffHolder.icons[i] = icon
end
buffHolder:Hide()
personalFrame = frame
BNP.personalNameplateFrame = frame
return frame
end
local function GetPlayerHealthColor()
if BNP.IsPersonalNameplateClassColorEnabled and BNP:IsPersonalNameplateClassColorEnabled() then
local _, class = UnitClass("player")
if BNP_DB and BNP_DB.modernClassColors then
local c = BNP:GetModernClassColor(class)
if c then return c.r, c.g, c.b end
end
if RAID_CLASS_COLORS and class and RAID_CLASS_COLORS[class] then
local c = RAID_CLASS_COLORS[class]
return c.r, c.g, c.b
end
local c = class and classFallback[class]
if c then return c[1], c[2], c[3] end
end
return 0.10, 0.95, 0.10
end
local function ApplyLevelLayout(frame)
if not frame then return end
local hideLevel = BNP.IsPersonalNameplateLevelHidden and BNP:IsPersonalNameplateLevelHidden()
if hideLevel then
frame.border:ClearAllPoints()
frame.border:SetWidth(NO_LEVEL_BORDER_WIDTH)
frame.border:SetTexCoord(0, NO_LEVEL_BORDER_TEX_RIGHT, 0, 1)
frame.border:SetPoint("TOPLEFT", frame, "TOPLEFT", NO_LEVEL_BORDER_X, 0)
frame.healthbar:ClearAllPoints()
frame.healthbar:SetWidth(HEALTH_WIDTH)
frame.healthbar:SetPoint("TOPLEFT", frame, "TOPLEFT", NO_LEVEL_HEALTH_X, -19)
frame.powerHolder:ClearAllPoints()
frame.powerHolder:SetPoint("TOPLEFT", frame, "TOPLEFT", NO_LEVEL_POWER_X, -34)
frame.levelText:Hide()
else
frame.border:ClearAllPoints()
frame.border:SetWidth(128)
frame.border:SetTexCoord(0, 1, 0, 1)
frame.border:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
frame.healthbar:ClearAllPoints()
frame.healthbar:SetWidth(HEALTH_WIDTH)
frame.healthbar:SetPoint("TOPLEFT", frame, "TOPLEFT", 5, -19)
frame.powerHolder:ClearAllPoints()
frame.powerHolder:SetPoint("TOPLEFT", frame, "TOPLEFT", 3, -34)
end
end
local function UpdateStyle(frame)
if not frame then return end
ApplyLevelLayout(frame)
BNP:ApplyModernHealthTexture(frame)
local dark = BNP.IsDarkNameplateBorderEnabled and BNP:IsDarkNameplateBorderEnabled()
local hidden = BNP.IsNameplateBorderHidden and BNP:IsNameplateBorderHidden()
local blackBG = BNP.IsBlackHealthbarBackgroundEnabled and BNP:IsBlackHealthbarBackgroundEnabled()
if hidden then
frame.border:Hide()
else
frame.border:Show()
if dark then
frame.border:SetVertexColor(BNP:GetRecoloredBorderColor())
else
frame.border:SetVertexColor(1, 1, 1, 1)
end
end
local edge = dark and 0.32 or 0.78
local edgeG = dark and 0.32 or 0.60
local edgeB = dark and 0.32 or 0.08
if dark then edge, edgeG, edgeB = BNP:GetRecoloredBorderColor() end
SetThinBorderColor(frame.powerHolder, edge, edgeG, edgeB, hidden and 0 or 1)
if blackBG then
frame.healthBG:SetVertexColor(0, 0, 0, 1)
else
frame.healthBG:SetVertexColor(0, 0, 0, 1)
end
local hideLevel = BNP.IsPersonalNameplateLevelHidden and BNP:IsPersonalNameplateLevelHidden()
if hidden or hideLevel then frame.levelText:Hide() else frame.levelText:Show() end
local hr, hg, hb = GetPlayerHealthColor()
frame.healthbar:SetStatusBarColor(hr, hg, hb)
end
local function PositionPersonalAuraHolders(frame)
if not frame then return end
local hideLevel = BNP.IsPersonalNameplateLevelHidden and BNP:IsPersonalNameplateLevelHidden()
local contentCenterX = hideLevel and PERSONAL_HEALTH_TEXT_X or PERSONAL_CONTENT_CENTER_X
local debuffY = BNP.GetPersonalNameplateDebuffYOffset and BNP:GetPersonalNameplateDebuffYOffset() or 0
local buffY = BNP.GetPersonalNameplateBuffYOffset and BNP:GetPersonalNameplateBuffYOffset() or 0
local debuffX = BNP.GetPersonalNameplateDebuffXOffset and BNP:GetPersonalNameplateDebuffXOffset() or 0
local buffX = BNP.GetPersonalNameplateBuffXOffset and BNP:GetPersonalNameplateBuffXOffset() or 0
if frame.debuffHolder then
frame.debuffHolder:ClearAllPoints()
frame.debuffHolder:SetPoint("BOTTOM", frame, "TOP", contentCenterX + debuffX, 1 + debuffY)
end
if frame.buffHolder then
frame.buffHolder:ClearAllPoints()
if frame.debuffHolder and frame.debuffHolder:IsShown() then
frame.buffHolder:SetPoint("BOTTOM", frame, "TOP", contentCenterX + buffX, 25 + buffY)
else
frame.buffHolder:SetPoint("BOTTOM", frame, "TOP", contentCenterX + buffX, 1 + buffY)
end
end
end
local function UpdatePosition(frame)
if not frame then return end
local scale = BNP.GetPersonalNameplateScale and BNP:GetPersonalNameplateScale() or 1.0
local y = BNP.GetPersonalNameplateYOffset and BNP:GetPersonalNameplateYOffset() or -90
frame:SetScale(scale)
frame:ClearAllPoints()
frame:SetPoint("CENTER", UIParent, "CENTER", 0, y)
PositionPersonalAuraHolders(frame)
end
local function GetPersonalDebuffIconSize()
local size = BNP.GetIconSize and tonumber(BNP:GetIconSize()) or 18
if not size then size = 18 end
if size < 12 then size = 12 end
if size > 20 then size = 20 end
return size
end
local function FormatDebuffTimer(remaining)
remaining = tonumber(remaining)
if not remaining or remaining <= 0 then return "" end
if remaining >= 10 then return tostring(math.ceil(remaining)) end
return string.format("%.1f", remaining)
end
local function ReadPlayerDebuff(index)
if type(C_UnitAuras) == "table" and type(C_UnitAuras.GetDebuffDataByIndex) == "function" then
local ok, aura = pcall(C_UnitAuras.GetDebuffDataByIndex, "player", index)
if not ok or not aura then return nil end
return aura.icon or aura.texture, aura.applications or aura.count or 0, aura.duration, aura.expirationTime, aura.name, aura.spellId or aura.spellID
end
if type(C_UnitAuras) == "table" and type(C_UnitAuras.GetAuraDataByIndex) == "function" then
local ok, aura = pcall(C_UnitAuras.GetAuraDataByIndex, "player", index, "HARMFUL")
if not ok or not aura then return nil end
return aura.icon or aura.texture, aura.applications or aura.count or 0, aura.duration, aura.expirationTime, aura.name, aura.spellId or aura.spellID
end
if type(C_UnitAuras) == "table" and type(C_UnitAuras.UnitDebuff) == "function" then
local name, icon, count, dispelType, duration, expirationTime, source, isStealable, nameplateShowPersonal, spellID = C_UnitAuras.UnitDebuff("player", index)
if not name then return nil end
return icon, count or 0, duration, expirationTime, name, spellID
end
-- Legacy UnitDebuff on pure 1.12 usually has no reliable duration. Read it
-- only when duration/expiration are supplied by the custom client; otherwise
-- skip it rather than accidentally showing multi-minute effects.
if UnitDebuff then
local texture, count, dispelType, spellID, duration, expirationTime = UnitDebuff("player", index)
if not texture then return nil end
return texture, count or 0, duration, expirationTime, nil, spellID
end
return nil
end
local function ReadPlayerBuff(index)
if type(C_UnitAuras) == "table" and type(C_UnitAuras.GetBuffDataByIndex) == "function" then
local ok, aura = pcall(C_UnitAuras.GetBuffDataByIndex, "player", index)
if not ok or not aura then return nil end
return aura.icon or aura.texture, aura.applications or aura.count or 0, aura.duration, aura.expirationTime, aura.name, aura.spellId or aura.spellID
end
if type(C_UnitAuras) == "table" and type(C_UnitAuras.GetAuraDataByIndex) == "function" then
local ok, aura = pcall(C_UnitAuras.GetAuraDataByIndex, "player", index, "HELPFUL")
if not ok or not aura then return nil end
return aura.icon or aura.texture, aura.applications or aura.count or 0, aura.duration, aura.expirationTime, aura.name, aura.spellId or aura.spellID
end
if type(C_UnitAuras) == "table" and type(C_UnitAuras.UnitBuff) == "function" then
local name, icon, count, dispelType, duration, expirationTime, source, isStealable, nameplateShowPersonal, spellID = C_UnitAuras.UnitBuff("player", index)
if not name then return nil end
return icon, count or 0, duration, expirationTime, name, spellID
end
-- As with debuffs, legacy 1.12 UnitBuff has no dependable duration on its
-- own. Only use it when a custom client supplies duration + expiration.
if UnitBuff then
local texture, count, dispelType, spellID, duration, expirationTime = UnitBuff("player", index)
if not texture then return nil end
return texture, count or 0, duration, expirationTime, nil, spellID
end
return nil
end
local function HidePersonalDebuffs(frame)
if not frame or not frame.debuffHolder then return end
local i
for i = 1, table.getn(frame.debuffHolder.icons or {}) do
local icon = frame.debuffHolder.icons[i]
icon.timer:SetText("")
icon.stack:SetText("")
icon:Hide()
end
frame.debuffHolder:Hide()
PositionPersonalAuraHolders(frame)
end
local function UpdatePersonalDebuffs(frame, forceScan)
if not frame or not frame.debuffHolder then return end
if not (BNP.IsPersonalNameplateDebuffsEnabled and BNP:IsPersonalNameplateDebuffsEnabled()) then
frame.BNPPersonalDebuffEntries = nil
HidePersonalDebuffs(frame)
return
end
local now = GetTime()
local entries = frame.BNPPersonalDebuffEntries
if forceScan or not entries then
entries = {}
local index
for index = 1, 64 do
local texture, count, duration, expirationTime, name, spellID = ReadPlayerDebuff(index)
if not texture then break end
duration = tonumber(duration) or 0
expirationTime = tonumber(expirationTime) or 0
if duration > 0 and duration <= PERSONAL_DEBUFF_MAX_DURATION and expirationTime > now then
table.insert(entries, {
texture = texture,
count = tonumber(count) or 0,
duration = duration,
expirationTime = expirationTime,
name = name,
spellID = spellID,
})
if table.getn(entries) >= PERSONAL_DEBUFF_MAX_ICONS then break end
end
end
frame.BNPPersonalDebuffEntries = entries
end
local visible = 0
local i
for i = 1, table.getn(entries) do
if entries[i].expirationTime and entries[i].expirationTime > now then
visible = visible + 1
end
end
if visible < 1 then
HidePersonalDebuffs(frame)
return
end
local size = GetPersonalDebuffIconSize()
local totalWidth = visible * size + (visible - 1) * PERSONAL_DEBUFF_SPACING
local startX = -totalWidth / 2
local displayIndex = 0
for i = 1, table.getn(entries) do
local entry = entries[i]
if entry.expirationTime and entry.expirationTime > now then
displayIndex = displayIndex + 1
local icon = frame.debuffHolder.icons[displayIndex]
if icon then
if icon.BNPLastSize ~= size then
icon:SetWidth(size)
icon:SetHeight(size)
icon.timer:SetFont(FONT, math.max(7, math.floor(size * 0.44 + 0.5)), "OUTLINE")
icon.stack:SetFont(FONT, math.max(6, math.floor(size * 0.38 + 0.5)), "OUTLINE")
icon.BNPLastSize = size
end
icon:ClearAllPoints()
icon:SetPoint("LEFT", frame.debuffHolder, "CENTER", startX + (displayIndex - 1) * (size + PERSONAL_DEBUFF_SPACING), 0)
if icon.BNPLastTexture ~= entry.texture then
icon.texture:SetTexture(entry.texture)
icon.BNPLastTexture = entry.texture
end
icon.timer:SetText(FormatDebuffTimer(entry.expirationTime - now))
icon.stack:SetText(entry.count > 1 and tostring(entry.count) or "")
icon:Show()
end
end
end
for i = displayIndex + 1, table.getn(frame.debuffHolder.icons) do
local icon = frame.debuffHolder.icons[i]
icon.timer:SetText("")
icon.stack:SetText("")
icon:Hide()
end
frame.debuffHolder:Show()
PositionPersonalAuraHolders(frame)
end
local function HidePersonalBuffs(frame)
if not frame or not frame.buffHolder then return end
local i
for i = 1, table.getn(frame.buffHolder.icons or {}) do
local icon = frame.buffHolder.icons[i]
icon.timer:SetText("")
icon.stack:SetText("")
icon:Hide()
end
frame.buffHolder:Hide()
end
local function UpdatePersonalBuffs(frame, forceScan)
if not frame or not frame.buffHolder then return end
if not (BNP.IsPersonalNameplateBuffsEnabled and BNP:IsPersonalNameplateBuffsEnabled()) then
frame.BNPPersonalBuffEntries = nil
HidePersonalBuffs(frame)
return
end
local now = GetTime()
local entries = frame.BNPPersonalBuffEntries
if forceScan or not entries then
entries = {}
local index
for index = 1, 64 do
local texture, count, duration, expirationTime, name, spellID = ReadPlayerBuff(index)
if not texture then break end
duration = tonumber(duration) or 0
expirationTime = tonumber(expirationTime) or 0
-- Keep this row focused on combat-relevant temporary effects: HoTs,
-- absorbs/shields and short cooldown buffs. Long raid/world buffs are
-- intentionally ignored.
if duration > 0 and duration <= PERSONAL_BUFF_MAX_DURATION and expirationTime > now then
table.insert(entries, {
texture = texture,
count = tonumber(count) or 0,
duration = duration,
expirationTime = expirationTime,
name = name,
spellID = spellID,
})
if table.getn(entries) >= PERSONAL_BUFF_MAX_ICONS then break end
end
end
frame.BNPPersonalBuffEntries = entries
end
local visible = 0
local i
for i = 1, table.getn(entries) do
if entries[i].expirationTime and entries[i].expirationTime > now then
visible = visible + 1
end
end
if visible < 1 then
HidePersonalBuffs(frame)
return
end
local size = GetPersonalDebuffIconSize()
local totalWidth = visible * size + (visible - 1) * PERSONAL_BUFF_SPACING
local startX = -totalWidth / 2
local displayIndex = 0
for i = 1, table.getn(entries) do
local entry = entries[i]
if entry.expirationTime and entry.expirationTime > now then
displayIndex = displayIndex + 1
local icon = frame.buffHolder.icons[displayIndex]
if icon then
if icon.BNPLastSize ~= size then
icon:SetWidth(size)
icon:SetHeight(size)
icon.timer:SetFont(FONT, math.max(7, math.floor(size * 0.44 + 0.5)), "OUTLINE")
icon.stack:SetFont(FONT, math.max(6, math.floor(size * 0.38 + 0.5)), "OUTLINE")
icon.BNPLastSize = size
end
icon:ClearAllPoints()
icon:SetPoint("LEFT", frame.buffHolder, "CENTER", startX + (displayIndex - 1) * (size + PERSONAL_BUFF_SPACING), 0)
if icon.BNPLastTexture ~= entry.texture then
icon.texture:SetTexture(entry.texture)
icon.BNPLastTexture = entry.texture
end
icon.timer:SetText(FormatDebuffTimer(entry.expirationTime - now))
icon.stack:SetText(entry.count > 1 and tostring(entry.count) or "")
icon:Show()
end
end
end
for i = displayIndex + 1, table.getn(frame.buffHolder.icons) do
local icon = frame.buffHolder.icons[i]
icon.timer:SetText("")
icon.stack:SetText("")
icon:Hide()
end
frame.buffHolder:Show()
PositionPersonalAuraHolders(frame)
end
local function UpdateValues(frame)
if not frame then return end
-- Some custom 1.12 APIs return extra values from UnitHealth/UnitMana.
-- Passing such a multi-return call directly into tonumber() makes Lua treat
-- the second value as tonumber's optional base argument (and can throw
-- "bad argument #2 ... base out of range"). Capture only the first result.
local rawHealth = UnitHealth("player")
local rawMaxHealth = UnitHealthMax("player")
local health = tonumber(rawHealth) or 0
local maxHealth = tonumber(rawMaxHealth) or 0
if maxHealth < 1 then maxHealth = 1 end
if health < 0 then health = 0 end
if health > maxHealth then health = maxHealth end
frame.healthbar:SetMinMaxValues(0, maxHealth)
frame.healthbar:SetValue(health)
-- Personal Health Text mirrors BNP's regular nameplate display modes, but
-- remains independently configurable so the player HUD does not have to
-- match enemy nameplates.
local healthMode = BNP.GetPersonalNameplateHealthTextMode and BNP:GetPersonalNameplateHealthTextMode() or "both"
local healthDisplay = nil
if healthMode == "percent" then
local pct = math.floor((health / maxHealth) * 100 + 0.5)
if pct < 0 then pct = 0 end
if pct > 100 then pct = 100 end
healthDisplay = tostring(pct) .. "%"
elseif healthMode == "hp" then
healthDisplay = FormatValue(health)
elseif healthMode == "both" then
local pct = math.floor((health / maxHealth) * 100 + 0.5)
if pct < 0 then pct = 0 end
if pct > 100 then pct = 100 end
healthDisplay = FormatValue(health) .. " | " .. tostring(pct) .. "%"
end
if healthDisplay then
frame.healthText:SetText(healthDisplay)
frame.healthText:Show()
else
frame.healthText:SetText("")
frame.healthText:Hide()
end
local rawPower = UnitMana("player")
local rawMaxPower = UnitManaMax("player")
local power = tonumber(rawPower) or 0
local maxPower = tonumber(rawMaxPower) or 0
if maxPower < 1 then maxPower = 1 end
if power < 0 then power = 0 end
if power > maxPower then power = maxPower end
frame.powerbar:SetMinMaxValues(0, maxPower)
frame.powerbar:SetValue(power)
frame.powerText:SetText(FormatValue(power) .. " / " .. FormatValue(maxPower))
local powerType = GetPlayerPowerType()
local c = powerColors[powerType] or powerColors[0]
frame.powerbar:SetStatusBarColor(c[1], c[2], c[3], 1)
local rawLevel = UnitLevel and UnitLevel("player") or nil
local level = tonumber(rawLevel)
if level and level > 0 then
frame.levelText:SetText(tostring(math.floor(level)))
else
frame.levelText:SetText("")
end
end
local function OptionsPreviewActive()
return BNP.optionsFrame and BNP.optionsFrame.IsShown and BNP.optionsFrame:IsShown()
end
local function ShouldShow()
if not (BNP.IsPersonalNameplateEnabled and BNP:IsPersonalNameplateEnabled()) then
return false
end
if not (BNP.IsPersonalNameplateCombatOnly and BNP:IsPersonalNameplateCombatOnly()) then
return true
end
-- Keep it visible while the options window is open so Scale/Y Offset can be
-- adjusted outside combat without needing a training dummy.
return inCombat or OptionsPreviewActive()
end
local events = CreateFrame("Frame")
local updater = CreateFrame("Frame")
local runtimeEnabled = false
local RuntimeOnUpdate
local runtimeEvents = {
"PLAYER_ENTERING_WORLD",
"PLAYER_REGEN_DISABLED",
"PLAYER_REGEN_ENABLED",
"UNIT_HEALTH",
"UNIT_MANA",
"UNIT_AURA",
"PLAYER_LEVEL_UP",
}
local function SetRuntimeEnabled(enabled)
enabled = enabled and true or false
if enabled == runtimeEnabled then return end
runtimeEnabled = enabled
local i
if enabled then
for i = 1, table.getn(runtimeEvents) do
events:RegisterEvent(runtimeEvents[i])
end
updater:SetScript("OnUpdate", RuntimeOnUpdate)
else
for i = 1, table.getn(runtimeEvents) do
events:UnregisterEvent(runtimeEvents[i])
end
updater:SetScript("OnUpdate", nil)
updateElapsed = 0
debuffScanElapsed = 0
buffScanElapsed = 0
if personalFrame then personalFrame:Hide() end
end
end
local function Refresh()
local enabled = BNP.IsPersonalNameplateEnabled and BNP:IsPersonalNameplateEnabled()
if not enabled then
SetRuntimeEnabled(false)
if personalFrame then personalFrame:Hide() end
return
end
SetRuntimeEnabled(true)
local frame = CreatePersonalFrame()
UpdatePosition(frame)
UpdateStyle(frame)
UpdateValues(frame)
if ShouldShow() then
frame:Show()
UpdatePersonalDebuffs(frame, true)
UpdatePersonalBuffs(frame, true)
else
frame:Hide()
end
end
function BNP:RefreshPersonalNameplate()
Refresh()
end
function BNP:RefreshPersonalNameplateStyle()
if not (BNP.IsPersonalNameplateEnabled and BNP:IsPersonalNameplateEnabled()) then return end
if not personalFrame then return end
UpdateStyle(personalFrame)
UpdateValues(personalFrame)
if personalFrame:IsShown() then
UpdatePersonalDebuffs(personalFrame, true)
UpdatePersonalBuffs(personalFrame, true)
end
end
events:SetScript("OnEvent", function()
if event == "VARIABLES_LOADED" then
events:UnregisterEvent("VARIABLES_LOADED")
Refresh()
return
end
if not runtimeEnabled then return end
if event == "PLAYER_REGEN_DISABLED" then
inCombat = true
Refresh()
return
elseif event == "PLAYER_REGEN_ENABLED" then
inCombat = false
Refresh()
return
elseif event == "PLAYER_ENTERING_WORLD" then
if UnitAffectingCombat then
inCombat = UnitAffectingCombat("player") and true or false
else
inCombat = false
end
Refresh()
return
end
if (event == "UNIT_HEALTH" or event == "UNIT_MANA" or event == "UNIT_AURA") and arg1 and arg1 ~= "player" then
return
end
if personalFrame then
UpdateValues(personalFrame)
UpdateStyle(personalFrame)
if event == "UNIT_AURA" and personalFrame:IsShown() then
UpdatePersonalDebuffs(personalFrame, true)
UpdatePersonalBuffs(personalFrame, true)
end
end
end)
RuntimeOnUpdate = function()
updateElapsed = updateElapsed + arg1
if updateElapsed < 0.10 then return end
updateElapsed = 0
if not personalFrame then return end
-- Cheap 10 Hz fallback for custom 1.12 clients that do not emit every modern
-- player-resource event consistently. This script is attached only while the
-- Personal Nameplate feature itself is enabled.
local shouldShow = ShouldShow()
if shouldShow then
if not personalFrame:IsShown() then
personalFrame:Show()
debuffScanElapsed = 0.50
buffScanElapsed = 0.50
end
UpdateValues(personalFrame)
local forceDebuffScan = false
if BNP.IsPersonalNameplateDebuffsEnabled and BNP:IsPersonalNameplateDebuffsEnabled() then
debuffScanElapsed = debuffScanElapsed + 0.10
if debuffScanElapsed >= 0.50 then
debuffScanElapsed = 0
forceDebuffScan = true
end
end
UpdatePersonalDebuffs(personalFrame, forceDebuffScan)
local forceBuffScan = false
if BNP.IsPersonalNameplateBuffsEnabled and BNP:IsPersonalNameplateBuffsEnabled() then
buffScanElapsed = buffScanElapsed + 0.10
if buffScanElapsed >= 0.50 then
buffScanElapsed = 0
forceBuffScan = true
end
end
UpdatePersonalBuffs(personalFrame, forceBuffScan)
elseif personalFrame:IsShown() then
personalFrame:Hide()
end
end
-- Bootstrap once SavedVariables are available. If the feature is disabled,
-- this event removes itself and no Personal Nameplate runtime events or
-- OnUpdate polling stay active in the background.
events:RegisterEvent("VARIABLES_LOADED")