Files
pfUI/modules/nameplates.lua
T
Brues 6c64143b60 nameplates: filter libunitscan by player/npc to dodge name collisions
GetUnitInfo() probed the players table first, so an NPC sharing a name
with a known player (e.g. Chromie) inherited the player's class, level,
and guild. Adds an isPlayer hint to GetUnitInfo and resolves each
plate's actual unit type from its cached GUID via UnitIsPlayer before
the lookup. The plate's cache.player gates further work.
2026-06-22 10:11:56 -05:00

2007 lines
76 KiB
Lua

pfUI:RegisterModule("nameplates", function ()
-- disable original castbars
pcall(SetCVar, "ShowVKeyCastbar", 0)
-- Local function references for performance
local pfGetCastInfo = pfGetCastInfo -- provided by libcast for vanilla
local pfGetChannelInfo = pfGetChannelInfo -- provided by libcast for vanilla
local GetTime = GetTime
local UnitExists = UnitExists
local UnitName = UnitName
local UnitClass = UnitClass
local UnitLevel = UnitLevel
local UnitIsPlayer = UnitIsPlayer
local UnitIsDead = UnitIsDead
local UnitAffectingCombat = UnitAffectingCombat
local UnitIsUnit = UnitIsUnit
local UnitCanAssist = UnitCanAssist
local UnitHealth = UnitHealth
local UnitHealthMax = UnitHealthMax
local UnitMana = UnitMana
local UnitManaMax = UnitManaMax
local pairs = pairs
local tonumber = tonumber
local strlower = strlower
local strfind = strfind
local strlen = strlen
local floor = floor
local ceil = ceil
local abs = abs
local mathmod = math.mod
local unitcolors = {
["ENEMY_NPC"] = { .9, .2, .3, .8 },
["NEUTRAL_NPC"] = { 1, 1, .3, .8 },
["FRIENDLY_NPC"] = { .6, 1, 0, .8 },
["ENEMY_PLAYER"] = { .9, .2, .3, .8 },
["FRIENDLY_PLAYER"] = { .2, .6, 1, .8 }
}
local offtanks = {}
local combatstate = {
-- gets overwritten by user config
["OFFTANK"] = { r = .7, g = .4, b = .2, a = 1 },
["NOTHREAT"] = { r = .7, g = .7, b = .2, a = 1 },
["THREAT"] = { r = .7, g = .2, b = .2, a = 1 },
["CASTING"] = { r = .7, g = .2, b = .7, a = 1 },
["STUN"] = { r = .2, g = .7, b = .7, a = 1 },
["NONE"] = { r = .2, g = .2, b = .2, a = 1 },
}
local elitestrings = {
["elite"] = "+",
["rareelite"] = "R+",
["rare"] = "R",
["boss"] = "B"
}
-- Friendly zone nameplate disable state
local savedHostileState = nil
local savedFriendlyState = nil
local inFriendlyZone = false
local platecount = 0
local registry = {}
local raidGuidCache = {} -- guid -> name (rebuilt on RAID_ROSTER_UPDATE/PARTY_MEMBERS_CHANGED)
-- Helper function to safely access libdebuff cast data
local function GetCastInfo(guid)
return pfUI.libdebuff_casts and pfUI.libdebuff_casts[guid]
end
local guidTargetTokenCache = {} -- guid -> "<guid>target" interned string
local debuffCache = {} -- guid -> { [spellID] = { start, duration } }
-- Reusable per-plate debuff display buffer (avoid GC churn from per-call table creation)
local debuffDisplayBuf = {} -- [i] = { effect, texture, stacks, dtype, duration, timeleft }
for i = 1, 16 do debuffDisplayBuf[i] = {} end
local threatMemory = {} -- guid -> true if mob had player targeted
local debuffSeen = {} -- reusable table for debuff tracking (avoid GC churn)
-- PERF: Module-level IterDebuffs callback to avoid closure allocation per call
local _iterDebuffCount = 0
local function iterDebuffCallback(auraSlot, spellId, effect, texture, stacks, dtype, duration, timeleft)
if not texture or string.find(texture, "QuestionMark") then return end
_iterDebuffCount = _iterDebuffCount + 1
if _iterDebuffCount > 16 then return end
local b = debuffDisplayBuf[_iterDebuffCount]
b.effect, b.texture, b.stacks, b.dtype, b.duration, b.timeleft = effect, texture, stacks, dtype, duration, timeleft
end
-- PERF: Module-level IterDebuffs callback for PlateCacheDebuffs — same
-- rationale; PlateCacheDebuffs runs per visible plate per throttled tick.
local _pcdSelf, _pcdNow, _pcdId
local function iterPlateCacheDebuffsCallback(auraSlot, spellId, effect, texture, stacks, dtype, duration, timeleft)
if not effect or not texture then return end
_pcdId = _pcdId + 1
if _pcdId > 16 then return end
local stop = (timeleft and timeleft > 0) and (_pcdNow + timeleft) or nil
local start = stop and (stop - (duration or 0)) or _pcdNow
local cache = _pcdSelf.debuffcache[_pcdId] or {}
cache.effect = effect
cache.texture = texture
cache.stacks = stacks
cache.duration = duration or 0
cache.start = start
cache.stop = stop
cache.empty = nil
_pcdSelf.debuffcache[_pcdId] = cache
end
-- PERF: visiblePlateCount maintained event-driven (NAME_PLATE_UNIT_ADDED/_REMOVED)
local visiblePlateCount = 0
-- wipe polyfill
local wipe = wipe or function(t) for k in pairs(t) do t[k] = nil end end
-- Player GUID for filtering
local PlayerGUID = UnitGUID("player")
-- ============================================================================
-- OPTIMIZATION: Config caching
-- ============================================================================
local cfg = {}
local function CacheConfig()
cfg.showcastbar = C.nameplates["showcastbar"] == "1"
cfg.targetcastbar = C.nameplates["targetcastbar"] == "1"
cfg.notargalpha = tonumber(C.nameplates.notargalpha) or 0.5
if cfg.notargalpha > 1 then cfg.notargalpha = cfg.notargalpha / 100 end
-- Clamp to 0.99 so non-target plates never reach 1.0 (used for target detection)
if cfg.notargalpha > 0.99 then cfg.notargalpha = 0.99 end
cfg.namefightcolor = C.nameplates.namefightcolor == "1"
cfg.spellname = C.nameplates.spellname == "1"
cfg.showhp = C.nameplates.showhp == "1"
cfg.showdebuffs = C.nameplates["showdebuffs"] == "1"
cfg.showdebuffs_hostile = C.nameplates["showdebuffs_hostile"] == "1"
cfg.showdebuffs_friendly = C.nameplates["showdebuffs_friendly"] == "1"
cfg.targetzoom = C.nameplates.targetzoom == "1"
cfg.zoomval = (tonumber(C.nameplates.targetzoomval) or 0.4) + 1
cfg.width = tonumber(C.nameplates.width) or 120
cfg.heighthealth = tonumber(C.nameplates.heighthealth) or 8
cfg.targetglow = C.nameplates.targetglow == "1"
cfg.targethighlight = C.nameplates.targethighlight == "1"
cfg.outcombatstate = C.nameplates.outcombatstate == "1"
cfg.barcombatstate = C.nameplates.barcombatstate == "1"
cfg.ccombatcasting = C.nameplates.ccombatcasting == "1"
cfg.ccombatthreat = C.nameplates.ccombatthreat == "1"
cfg.ccombatnothreat = C.nameplates.ccombatnothreat == "1"
cfg.ccombatstun = C.nameplates.ccombatstun == "1"
cfg.ccombatofftank = C.nameplates.ccombatofftank == "1"
cfg.use_unitfonts = C.nameplates.use_unitfonts == "1"
cfg.font_size = cfg.use_unitfonts and C.global.font_unit_size or C.global.font_size
cfg.hptextformat = C.nameplates.hptextformat
-- NEW: Cache debuff config
cfg.debufftimers = C.nameplates.debufftimers == "1"
cfg.debuffanim = tonumber(C.nameplates.debuffanim) or 0
cfg.debufftext = tonumber(C.nameplates.debufftext) or 1
-- Rebuild offtanks lookup table
offtanks = {}
for k, v in pairs({strsplit("#", C.nameplates.combatofftanks)}) do
if v ~= "" then offtanks[string.lower(v)] = true end
end
end
local function RebuildOfftanks()
offtanks = {}
for k, v in pairs({strsplit("#", C.nameplates.combatofftanks)}) do
if v ~= "" then offtanks[string.lower(v)] = true end
end
end
RebuildOfftanks()
-- ============================================================================
-- OPTIMIZATION: Frame state cache
-- ============================================================================
local frameState = {
now = 0,
hasTarget = false,
targetGuid = nil,
hasMouseover = false,
}
-- cache default border color
local er, eg, eb, ea = GetStringColor(pfUI_config.appearance.border.color)
-- Vanilla Lua 5.0 bitwise check: math.mod(math.floor(value / flag), 2) ~= 0
local function HasFlag(flags, flag)
return math.mod(math.floor(flags / flag), 2) ~= 0
end
local UNIT_FLAG_IN_COMBAT = 524288 -- 0x00080000
local NULL_GUID = "0x0000000000000000"
local function RebuildRaidGuidCache()
for k in pairs(raidGuidCache) do raidGuidCache[k] = nil end
for i = 1, GetNumRaidMembers() do
local g = UnitGUID("raid"..i)
if g then raidGuidCache[g] = UnitName("raid"..i) end
end
for i = 1, GetNumPartyMembers() do
local g = UnitGUID("party"..i)
if g then raidGuidCache[g] = UnitName("party"..i) end
end
local pg = UnitGUID("player")
if pg then raidGuidCache[pg] = UnitName("player") end
end
local combatColorCache = {} -- guid -> { color, expires }
local function GetCombatStateColor(guid)
-- PERF: Quick exit if player not in combat
if not UnitAffectingCombat("player") then return false end
if UnitCanAssist("player", guid) then return false end
-- PERF: 0.2s throttle per guid - color changes are not time-critical
local now = frameState.now
local cached = combatColorCache[guid]
if cached and cached.expires > now then
return cached.color
end
local flags = GetUnitField and GetUnitField(guid, "flags")
if not flags then return false end
if not HasFlag(flags, UNIT_FLAG_IN_COMBAT) then return false end
local mobTargetGuid = GetUnitField and GetUnitField(guid, "target")
local hasTarget = mobTargetGuid and mobTargetGuid ~= NULL_GUID
-- PERF: cache the SuperWoW-style "<guid>target" unit token. The concat
-- intern-hits Lua's string pool every call; caching once per guid
-- saves the hash+lookup. Cleared in NAME_PLATE_UNIT_REMOVED.
local target = guidTargetTokenCache[guid]
if not target then
target = guid .. "target"
guidTargetTokenCache[guid] = target
end
local color = false
local castInfo = GetCastInfo(guid)
local isCasting = castInfo and castInfo.endTime and now < castInfo.endTime
local targetingPlayer = hasTarget and UnitIsUnit(target, "player")
if targetingPlayer then
threatMemory[guid] = true
elseif hasTarget and not isCasting then
threatMemory[guid] = nil
end
-- PERF: O(1) GUID lookup via raidGuidCache instead of O(40) loop
local targetName = hasTarget and (UnitName(target) or raidGuidCache[mobTargetGuid])
if cfg.ccombatcasting and isCasting then
color = combatstate.CASTING
elseif cfg.ccombatthreat and (targetingPlayer or threatMemory[guid]) then
color = combatstate.THREAT
elseif cfg.ccombatofftank and targetName and offtanks[strlower(targetName)] then
color = combatstate.OFFTANK
elseif cfg.ccombatofftank and pfUI.uf and pfUI.uf.raid and targetName and pfUI.uf.raid.tankrole[targetName] then
color = combatstate.OFFTANK
elseif cfg.ccombatnothreat and hasTarget then
color = combatstate.NOTHREAT
elseif cfg.ccombatstun and not hasTarget then
color = combatstate.STUN
end
combatColorCache[guid] = combatColorCache[guid] or {}
combatColorCache[guid].color = color
combatColorCache[guid].expires = now + 0.2
return color
end
local function DoNothing()
return
end
local function wipe(table)
if type(table) ~= "table" then
return
end
for k in pairs(table) do
table[k] = nil
end
end
local function DisableObject(object)
if not object then return end
if not object.GetObjectType then return end
local otype = object:GetObjectType()
if otype == "Texture" then
object:SetTexture("")
object:SetTexCoord(0, 0, 0, 0)
elseif otype == "FontString" then
object:SetWidth(0.001)
elseif otype == "StatusBar" then
object:SetStatusBarTexture("")
end
end
local function TotemPlate(name)
if C.nameplates.totemicons == "1" then
for totem, icon in pairs(L["totems"]) do
if string.find(name, totem) then return icon end
end
end
end
local function HidePlate(unittype, name, fullhp, target)
-- keep some plates always visible according to config
if C.nameplates.fullhealth == "1" and not fullhp then return nil end
if C.nameplates.target == "1" and target then return nil end
-- return true when something needs to be hidden
if C.nameplates.enemynpc == "1" and unittype == "ENEMY_NPC" then
return true
elseif C.nameplates.enemyplayer == "1" and unittype == "ENEMY_PLAYER" then
return true
elseif C.nameplates.neutralnpc == "1" and unittype == "NEUTRAL_NPC" then
return true
elseif C.nameplates.friendlynpc == "1" and unittype == "FRIENDLY_NPC" then
return true
elseif C.nameplates.friendlyplayer == "1" and unittype == "FRIENDLY_PLAYER" then
return true
elseif C.nameplates.critters == "1" and unittype == "NEUTRAL_NPC" then
for i, critter in pairs(L["critters"]) do
if string.lower(name) == string.lower(critter) then return true end
end
elseif C.nameplates.totems == "1" then
for totem in pairs(L["totems"]) do
if string.find(name, totem) then return true end
end
end
-- nothing to hide
return nil
end
local function abbrevname(t)
return string.sub(t,1,1)..". "
end
local function GetNameString(name)
local abbrev = pfUI_config.unitframes.abbrevname == "1" or nil
local size = 20
-- first try to only abbreviate the first word
if abbrev and name and strlen(name) > size then
name = string.gsub(name, "^(%S+) ", abbrevname)
end
-- abbreviate all if it still doesn't fit
if abbrev and name and strlen(name) > size then
name = string.gsub(name, "(%S+) ", abbrevname)
end
return name
end
local function GetUnitType(red, green, blue)
if red > .9 and green < .2 and blue < .2 then
return "ENEMY_NPC"
elseif red > .9 and green > .9 and blue < .2 then
return "NEUTRAL_NPC"
elseif red < .2 and green < .2 and blue > 0.9 then
return "FRIENDLY_PLAYER"
elseif red < .2 and green > .9 and blue < .2 then
return "FRIENDLY_NPC"
end
end
local filter, list, cache
local function DebuffFilterPopulate()
-- initialize variables
filter = C.nameplates["debuffs"]["filter"]
if filter == "none" then return end
list = C.nameplates["debuffs"][filter]
cache = {}
-- populate list
for _, val in pairs({strsplit("#", list)}) do
cache[strlower(val)] = true
end
end
local function DebuffFilter(effect)
if filter == "none" then return true end
if not cache then DebuffFilterPopulate() end
if filter == "blacklist" and cache[strlower(effect)] then
return nil
elseif filter == "blacklist" then
return true
elseif filter == "whitelist" and cache[strlower(effect)] then
return true
elseif filter == "whitelist" then
return nil
end
end
local function PlateCacheDebuffs(self, unitstr, verify)
if not self.debuffcache then self.debuffcache = {} end
if not libdebuff then return end
local now = GetTime()
-- Clear existing cache slots
for id = 1, 16 do
if self.debuffcache[id] then
self.debuffcache[id].empty = true
end
end
-- Use IterDebuffs if Nampower available, else fall back to slot loop
if unitstr and libdebuff.IterDebuffs and UnitGUID then
_pcdSelf, _pcdNow, _pcdId = self, now, 0
libdebuff:IterDebuffs(unitstr, iterPlateCacheDebuffsCallback)
else
for id = 1, 16 do
local effect, _, texture, stacks, _, duration, timeleft
effect, _, texture, stacks, _, duration, timeleft = libdebuff:UnitDebuff(unitstr, id)
if effect and timeleft and timeleft > 0 then
local start = now - ( (duration or 0) - ( timeleft or 0) )
local stop = now + timeleft
self.debuffcache[id] = self.debuffcache[id] or {}
self.debuffcache[id].effect = effect
self.debuffcache[id].texture = texture
self.debuffcache[id].stacks = stacks
self.debuffcache[id].duration = duration or 0
self.debuffcache[id].start = start
self.debuffcache[id].stop = stop
self.debuffcache[id].empty = nil
end
end
end
self.verify = verify
end
local function PlateUnitDebuff(self, id)
-- break on unknown data
if not self.debuffcache then return end
if not self.debuffcache[id] then return end
if not self.debuffcache[id].stop then return end
-- break on timeout debuffs
if self.debuffcache[id].empty then return end
if self.debuffcache[id].stop < GetTime() then return end
-- return cached debuff
local c = self.debuffcache[id]
return c.effect, c.rank, c.texture, c.stacks, c.dtype, c.duration, (c.stop - GetTime())
end
local function CreateDebuffIcon(plate, index)
plate.debuffs[index] = CreateFrame("Frame", plate.platename.."Debuff"..index, plate)
plate.debuffs[index]:Hide()
plate.debuffs[index]:SetFrameLevel(4)
plate.debuffs[index].icon = plate.debuffs[index]:CreateTexture(nil, "BACKGROUND")
plate.debuffs[index].icon:SetTexture(.3,1,.8,1)
plate.debuffs[index].icon:SetAllPoints(plate.debuffs[index])
plate.debuffs[index].stacks = plate.debuffs[index]:CreateFontString(nil, "OVERLAY")
plate.debuffs[index].stacks:SetAllPoints(plate.debuffs[index])
plate.debuffs[index].stacks:SetJustifyH("RIGHT")
plate.debuffs[index].stacks:SetJustifyV("BOTTOM")
plate.debuffs[index].stacks:SetTextColor(1,1,0)
-- PERF: Use lightweight fake cooldown frame when animation disabled
-- The Model-based CooldownFrameTemplate causes major lag with many nameplates
if cfg.debuffanim ~= 1 then
plate.debuffs[index].cd = CreateFrame("Frame", plate.platename.."Debuff"..index.."Cooldown", plate.debuffs[index])
plate.debuffs[index].cd:SetAllPoints(plate.debuffs[index])
plate.debuffs[index].cd:SetFrameLevel(6)
plate.debuffs[index].cd:SetScript("OnUpdate", CooldownFrame_OnUpdateModel)
plate.debuffs[index].cd.AdvanceTime = DoNothing
plate.debuffs[index].cd.SetSequence = DoNothing
plate.debuffs[index].cd.SetSequenceTime = DoNothing
else
-- Use CooldownFrameTemplate for animation
plate.debuffs[index].cd = CreateFrame(COOLDOWN_FRAME_TYPE, plate.platename.."Debuff"..index.."Cooldown", plate.debuffs[index], "CooldownFrameTemplate")
plate.debuffs[index].cd:SetAllPoints(plate.debuffs[index])
plate.debuffs[index].cd:SetFrameLevel(6)
end
-- Set initial config flags (will be cached per-cooldown later)
plate.debuffs[index].cd.pfCooldownStyleAnimation = cfg.debuffanim
plate.debuffs[index].cd.pfCooldownStyleText = cfg.debufftext
plate.debuffs[index].cd.pfCooldownType = "ALL"
end
local function UpdateDebuffConfig(nameplate, i)
if not nameplate.debuffs[i] then return end
-- update debuff positions
local width = tonumber(C.nameplates.width)
local debuffsize = tonumber(C.nameplates.debuffsize)
local debuffoffset = tonumber(C.nameplates.debuffoffset)
local limit = floor(width / debuffsize)
local font = C.nameplates.use_unitfonts == "1" and pfUI.font_unit or pfUI.font_default
local font_size = C.nameplates.use_unitfonts == "1" and C.global.font_unit_size or C.global.font_size
local font_style = C.nameplates.name.fontstyle
local aligna, alignb, offs, space
if C.nameplates.debuffs["position"] == "BOTTOM" then
aligna, alignb, offs, space = "TOPLEFT", "BOTTOMLEFT", -debuffoffset, -1
else
aligna, alignb, offs, space = "BOTTOMLEFT", "TOPLEFT", debuffoffset, 1
end
nameplate.debuffs[i].stacks:SetFont(font, font_size, font_style)
nameplate.debuffs[i]:ClearAllPoints()
if i == 1 then
nameplate.debuffs[i]:SetPoint(aligna, nameplate.health, alignb, 0, offs)
elseif i <= limit then
nameplate.debuffs[i]:SetPoint("LEFT", nameplate.debuffs[i-1], "RIGHT", 1, 0)
elseif i > limit and limit > 0 then
nameplate.debuffs[i]:SetPoint(aligna, nameplate.debuffs[i-limit], alignb, 0, space)
end
nameplate.debuffs[i]:SetWidth(tonumber(C.nameplates.debuffsize))
nameplate.debuffs[i]:SetHeight(tonumber(C.nameplates.debuffsize))
-- Update cooldown display settings
if nameplate.debuffs[i].cd then
local cooldown_text = tonumber(C.nameplates.debufftext) or 1
local cooldown_anim = tonumber(C.nameplates.debuffanim) or 0
nameplate.debuffs[i].cd.pfCooldownStyleText = cooldown_text
nameplate.debuffs[i].cd.pfCooldownStyleAnimation = cooldown_anim
end
end
-- create nameplate core
local nameplates = CreateFrame("Frame", "pfNameplates", UIParent)
nameplates:RegisterEvent("PLAYER_ENTERING_WORLD")
nameplates:RegisterEvent("PLAYER_TARGET_CHANGED")
nameplates:RegisterEvent("PLAYER_LOGOUT")
nameplates:RegisterEvent("UNIT_COMBO_POINTS")
nameplates:RegisterEvent("PLAYER_COMBO_POINTS")
nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA")
nameplates:RegisterEvent("RAID_ROSTER_UPDATE")
nameplates:RegisterEvent("PARTY_MEMBERS_CHANGED")
nameplates:RegisterEvent("NAME_PLATE_CREATED")
nameplates:RegisterEvent("NAME_PLATE_UNIT_ADDED")
nameplates:RegisterEvent("NAME_PLATE_UNIT_REMOVED")
if GetUnitField then
nameplates:RegisterEvent("UNIT_FLAGS_GUID")
end
-- Cast tracking handled by libdebuff (SPELL_START/GO/FAILED events)
-- No local event registration needed
-- Callback from libdebuff when auras change (GUID-based, event-driven)
nameplates.OnAuraUpdate = function(self, guid)
if not guid then return end
-- GUID is actual GUID (0xF13000...) from Nampower events
local plate = C_NamePlate.GetNamePlateForGUID(guid)
if plate and plate.nameplate then
-- Mark nameplate for aura update in next OnUpdate cycle
plate.nameplate.auraUpdate = true
end
end
-- Hook into libdebuff timer signal (fires when slotTimers written or cleared)
pfUI.libdebuff_on_unit_updated = pfUI.libdebuff_on_unit_updated or {}
table.insert(pfUI.libdebuff_on_unit_updated, function(guid)
local plate = C_NamePlate.GetNamePlateForGUID(guid)
if plate and plate.nameplate then
plate.nameplate.auraUpdate = true
end
end)
nameplates:SetScript("OnEvent", function()
-- Stop event handling during logout to prevent crash 132
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
this:SetScript("OnUpdate", nil)
if nameplates.mouselook then
nameplates.mouselook:SetScript("OnUpdate", nil)
end
return
elseif event == "PLAYER_ENTERING_WORLD" or event == "ZONE_CHANGED_NEW_AREA" then
if event == "PLAYER_ENTERING_WORLD" then
_, PlayerGUID = UnitExists("player")
CacheConfig()
this:SetGameVariables()
RebuildRaidGuidCache()
end
-- Handle friendly zone nameplate disable feature
local disableHostile = C.nameplates["disable_hostile_in_friendly"] == "1"
local disableFriendly = C.nameplates["disable_friendly_in_friendly"] == "1"
if disableHostile or disableFriendly then
local pvpType = GetZonePVPInfo()
local nowFriendly = (pvpType == "friendly")
if nowFriendly and not inFriendlyZone then
-- Entering friendly zone - save current state and hide based on options
inFriendlyZone = true
savedHostileState = C.nameplates["showhostile"]
savedFriendlyState = C.nameplates["showfriendly"]
if disableHostile then
_G.NAMEPLATES_ON = nil
HideNameplates()
end
if disableFriendly then
_G.FRIENDNAMEPLATES_ON = nil
HideFriendNameplates()
end
elseif not nowFriendly and inFriendlyZone then
-- Leaving friendly zone - restore previous state
inFriendlyZone = false
if savedHostileState == "1" then
_G.NAMEPLATES_ON = true
ShowNameplates()
end
if savedFriendlyState == "1" then
_G.FRIENDNAMEPLATES_ON = true
ShowFriendNameplates()
end
savedHostileState = nil
savedFriendlyState = nil
end
end
elseif event == "RAID_ROSTER_UPDATE" or event == "PARTY_MEMBERS_CHANGED" then
RebuildRaidGuidCache()
elseif event == "NAME_PLATE_CREATED" then
-- arg1 = the nameplate Frame; build the pfUI overlay once per pool slot
if arg1 and not registry[arg1] then
nameplates.OnCreate(arg1)
registry[arg1] = arg1
end
elseif event == "NAME_PLATE_UNIT_ADDED" then
-- arg1 = "nameplateN" unit token; resolve to GUID for cache keys
local plate = C_NamePlate.GetNamePlateForUnit(arg1)
if plate and plate.nameplate then
plate.nameplate.cachedGuid = UnitGUID(arg1)
nameplates.OnShow(plate)
end
visiblePlateCount = visiblePlateCount + 1
elseif event == "NAME_PLATE_UNIT_REMOVED" then
visiblePlateCount = visiblePlateCount > 0 and visiblePlateCount - 1 or 0
-- arg1 = "nameplateN" unit token; UnitGUID still resolves inside the
-- handler (the slot is freed after dispatch returns)
local guid = UnitGUID(arg1)
if guid then
if debuffCache[guid] then debuffCache[guid] = nil end
if threatMemory[guid] then threatMemory[guid] = nil end
if guidTargetTokenCache[guid] then guidTargetTokenCache[guid] = nil end
if combatColorCache[guid] then combatColorCache[guid] = nil end
local castInfo = GetCastInfo(guid)
if castInfo and castInfo.endTime and castInfo.endTime < GetTime() then
if pfUI.libdebuff_casts then pfUI.libdebuff_casts[guid] = nil end
end
local plate = C_NamePlate.GetNamePlateForUnit(arg1)
if plate and plate.nameplate and plate.nameplate.cachedGuid == guid then
plate.nameplate.cachedGuid = nil
end
end
elseif event == "UNIT_FLAGS_GUID" then
-- Nampower: fires instantly when any unit's flags change (e.g. stun, combat enter/leave)
-- arg1 = guid — directly flag that nameplate for immediate update, bypassing throttle
local plate = C_NamePlate.GetNamePlateForGUID(arg1)
if plate and plate.nameplate then
plate.nameplate.eventcache = true
end
elseif event == "PLAYER_TARGET_CHANGED" then
-- Flag target plate for update via GUID registry
local targetGuid = UnitGUID("target")
if targetGuid then
local plate = C_NamePlate.GetNamePlateForGUID(targetGuid)
if plate and plate.nameplate then
plate.nameplate.targetUpdate = true
end
end
-- Also propagate to all plates for alpha/strata updates
this.eventcache = true
elseif event == "PLAYER_COMBO_POINTS" or event == "UNIT_COMBO_POINTS" then
-- Only flag the target plate for combo point update
local targetGuid = UnitGUID("target")
if targetGuid then
local plate = C_NamePlate.GetNamePlateForGUID(targetGuid)
if plate and plate.nameplate then
plate.nameplate.comboUpdate = true
end
end
else
this.eventcache = true
end
end)
nameplates:SetScript("OnUpdate", function()
-- PERF: Throttle central OnUpdate to ~80 FPS (0.0125s)
-- Saves ~44% calls at 144 FPS while staying above 50 FPS target-plate rate
local now = GetTime()
if (this.frameTick or 0) + 0.01 > now then return end
this.frameTick = now
-- PERF: Cache GetTime() once per frame
frameState.now = now
frameState.hasTarget, frameState.targetGuid = UnitExists("target")
frameState.hasMouseover = UnitExists("mouseover")
-- propagate events to all nameplates
if this.eventcache then
this.eventcache = nil
for plate in pairs(registry) do
plate.eventcache = true
end
end
-- visiblePlateCount is maintained event-driven via NAME_PLATE_UNIT_ADDED/_REMOVED.
-- Central OnUpdate for all visible plates
for plate in pairs(registry) do
if plate:IsVisible() then
nameplates.OnUpdate(plate, frameState)
end
end
end)
-- combat tracker
nameplates.combat = CreateFrame("Frame")
nameplates.combat:RegisterEvent("PLAYER_ENTER_COMBAT")
nameplates.combat:RegisterEvent("PLAYER_LEAVE_COMBAT")
nameplates.combat:RegisterEvent("PLAYER_LOGOUT")
nameplates.combat:SetScript("OnEvent", function()
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
return
elseif event == "PLAYER_ENTER_COMBAT" then
this.inCombat = 1
if PlayerFrame then PlayerFrame.inCombat = 1 end
elseif event == "PLAYER_LEAVE_COMBAT" then
this.inCombat = nil
if PlayerFrame then PlayerFrame.inCombat = nil end
-- Clear threat memory when leaving combat
for k in pairs(threatMemory) do
threatMemory[k] = nil
end
end
end)
nameplates.OnCreate = function(frame)
local parent = frame or this
platecount = platecount + 1
platename = "pfNamePlate" .. platecount
-- create pfUI nameplate overlay
local nameplate = CreateFrame("Button", platename, parent)
nameplate.platename = platename
nameplate:EnableMouse(0)
nameplate.parent = parent
nameplate.cache = {}
nameplate.UnitDebuff = PlateUnitDebuff
nameplate.CacheDebuffs = PlateCacheDebuffs
nameplate.original = {}
-- create shortcuts for all known elements and disable them
nameplate.original.healthbar, nameplate.original.castbar = parent:GetChildren()
DisableObject(nameplate.original.healthbar)
DisableObject(nameplate.original.castbar)
for i, object in pairs({parent:GetRegions()}) do
if NAMEPLATE_OBJECTORDER[i] and NAMEPLATE_OBJECTORDER[i] == "raidicon" then
nameplate[NAMEPLATE_OBJECTORDER[i]] = object
elseif NAMEPLATE_OBJECTORDER[i] then
nameplate.original[NAMEPLATE_OBJECTORDER[i]] = object
DisableObject(object)
else
DisableObject(object)
end
end
HookScript(nameplate.original.healthbar, "OnValueChanged", nameplates.OnValueChanged)
-- adjust sizes and scaling of the nameplate
nameplate:SetScale(UIParent:GetScale())
nameplate.health = CreateFrame("StatusBar", nil, nameplate)
nameplate.health:SetFrameLevel(4) -- keep above glow
nameplate.health.text = nameplate.health:CreateFontString(nil, "OVERLAY", "GameFontNormal")
nameplate.health.text:SetAllPoints()
nameplate.health.text:SetTextColor(1,1,1,1)
nameplate.name = nameplate:CreateFontString(nil, "OVERLAY")
nameplate.name:SetPoint("TOP", nameplate, "TOP", 0, 0)
nameplate.glow = nameplate:CreateTexture(nil, "BACKGROUND")
nameplate.glow:SetPoint("CENTER", nameplate.health, "CENTER", 0, 0)
nameplate.glow:SetTexture(pfUI.media["img:dot"])
nameplate.glow:Hide()
nameplate.guild = nameplate:CreateFontString(nil, "OVERLAY")
nameplate.guild:SetPoint("BOTTOM", nameplate.health, "BOTTOM", 0, 0)
nameplate.level = nameplate:CreateFontString(nil, "OVERLAY")
nameplate.level:SetPoint("RIGHT", nameplate.health, "LEFT", -3, 0)
-- Create a dedicated high-level frame for the raid icon so it renders
-- ABOVE nameplate.health (FrameLevel 4) and stays visible even when
-- nameplates are toggled off (the Blizzard parent plate still exists).
nameplate.raidiconframe = CreateFrame("Frame", nil, nameplate)
nameplate.raidiconframe:SetFrameLevel(10)
nameplate.raidiconframe:SetAllPoints(nameplate)
nameplate.raidicon:SetParent(nameplate.raidiconframe)
nameplate.raidicon:SetDrawLayer("OVERLAY", 7)
if C.unitframes.blizzard_raidicons ~= "1" then
nameplate.raidicon:SetTexture(pfUI.media["img:raidicons"])
end
nameplate.totem = CreateFrame("Frame", nil, nameplate)
nameplate.totem:SetPoint("CENTER", nameplate, "CENTER", 0, 0)
nameplate.totem:SetHeight(32)
nameplate.totem:SetWidth(32)
nameplate.totem.icon = nameplate.totem:CreateTexture(nil, "OVERLAY")
nameplate.totem.icon:SetTexCoord(.078, .92, .079, .937)
nameplate.totem.icon:SetAllPoints()
CreateBackdrop(nameplate.totem)
do -- debuffs
nameplate.debuffs = {}
CreateDebuffIcon(nameplate, 1)
end
do -- combopoints
local combopoints = { }
for i = 1, 5 do
combopoints[i] = CreateFrame("Frame", nil, nameplate)
combopoints[i]:Hide()
combopoints[i]:SetFrameLevel(8)
combopoints[i].tex = combopoints[i]:CreateTexture("OVERLAY")
combopoints[i].tex:SetAllPoints()
if i < 3 then
combopoints[i].tex:SetTexture(1, .3, .3, .75)
elseif i < 4 then
combopoints[i].tex:SetTexture(1, 1, .3, .75)
else
combopoints[i].tex:SetTexture(.3, 1, .3, .75)
end
end
nameplate.combopoints = combopoints
end
do -- castbar
local castbar = CreateFrame("StatusBar", nil, nameplate.health)
castbar:Hide()
castbar:SetScript("OnShow", function()
if C.nameplates.debuffs["position"] == "BOTTOM" then
nameplate.debuffs[1]:SetPoint("TOPLEFT", this, "BOTTOMLEFT", 0, -4)
end
end)
castbar:SetScript("OnHide", function()
if C.nameplates.debuffs["position"] == "BOTTOM" then
nameplate.debuffs[1]:SetPoint("TOPLEFT", this:GetParent(), "BOTTOMLEFT", 0, -4)
end
end)
castbar.text = castbar:CreateFontString("Status", "DIALOG", "GameFontNormal")
castbar.text:SetPoint("RIGHT", castbar, "LEFT", -4, 0)
castbar.text:SetNonSpaceWrap(false)
castbar.text:SetTextColor(1,1,1,.5)
castbar.spell = castbar:CreateFontString("Status", "DIALOG", "GameFontNormal")
castbar.spell:SetPoint("CENTER", castbar, "CENTER")
castbar.spell:SetNonSpaceWrap(false)
castbar.spell:SetTextColor(1,1,1,1)
castbar.icon = CreateFrame("Frame", nil, castbar)
castbar.icon.tex = castbar.icon:CreateTexture(nil, "BORDER")
castbar.icon.tex:SetAllPoints()
nameplate.castbar = castbar
end
-- Stagger tick to spread updates across frames (0.05s apart per plate)
nameplate.tick = GetTime() + mathmod(platecount, 10) * 0.05
parent.nameplate = nameplate
-- NOTE: OnUpdate is now handled centrally, not per-plate/
parent:SetScript("OnUpdate", nil) -- Disable Blizzard's OnUpdate
nameplates.OnConfigChange(parent)
end
nameplates.OnConfigChange = function(frame)
local parent = frame
local nameplate = frame.nameplate
local font = C.nameplates.use_unitfonts == "1" and pfUI.font_unit or pfUI.font_default
local font_size = C.nameplates.use_unitfonts == "1" and C.global.font_unit_size or C.global.font_size
local font_style = C.nameplates.name.fontstyle
local glowr, glowg, glowb, glowa = GetStringColor(C.nameplates.glowcolor)
local hlr, hlg, hlb, hla = GetStringColor(C.nameplates.highlightcolor)
local hptexture = pfUI.media[C.nameplates.healthtexture]
local rawborder, default_border = GetBorderSize("nameplates")
local plate_width = C.nameplates.width + 50
local plate_height = C.nameplates.heighthealth + font_size + 5
local plate_height_cast = C.nameplates.heighthealth + font_size + 5 + C.nameplates.heightcast + 5
local combo_size = 5
local width = tonumber(C.nameplates.width)
local debuffsize = tonumber(C.nameplates.debuffsize)
local healthoffset = tonumber(C.nameplates.health.offset)
local orientation = C.nameplates.verticalhealth == "1" and "VERTICAL" or "HORIZONTAL"
local c = combatstate -- load combat state colors
c.CASTING.r, c.CASTING.g, c.CASTING.b, c.CASTING.a = GetStringColor(C.nameplates.combatcasting)
c.THREAT.r, c.THREAT.g, c.THREAT.b, c.THREAT.a = GetStringColor(C.nameplates.combatthreat)
c.NOTHREAT.r, c.NOTHREAT.g, c.NOTHREAT.b, c.NOTHREAT.a = GetStringColor(C.nameplates.combatnothreat)
c.OFFTANK.r, c.OFFTANK.g, c.OFFTANK.b, c.OFFTANK.a = GetStringColor(C.nameplates.combatofftank)
c.STUN.r, c.STUN.g, c.STUN.b, c.STUN.a = GetStringColor(C.nameplates.combatstun)
RebuildOfftanks()
nameplate:SetWidth(plate_width)
nameplate:SetHeight(plate_height)
nameplate:SetPoint("TOP", parent, "TOP", 0, 0)
nameplate.name:SetFont(font, font_size, font_style)
local nameTextPos = C.nameplates.nametextpos or "CENTER"
local nameAnchor = nameTextPos == "RIGHT" and { "BOTTOMRIGHT", "TOPRIGHT" }
or nameTextPos == "CENTER" and { "BOTTOM", "TOP" }
or { "BOTTOMLEFT", "TOPLEFT" }
nameplate.name:ClearAllPoints()
nameplate.name:SetPoint(nameAnchor[1], nameplate.health, nameAnchor[2], 0, -healthoffset)
nameplate.name:SetJustifyH(nameTextPos)
nameplate.health:SetOrientation(orientation)
-- Bar anchors to the plate directly so the name's JustifyH (configurable
-- below) can shift left/right without dragging the bar with it.
nameplate.health:ClearAllPoints()
nameplate.health:SetPoint("BOTTOM", nameplate, "BOTTOM", 0, 0)
nameplate.health:SetStatusBarTexture(hptexture)
nameplate.health:SetWidth(C.nameplates.width)
nameplate.health:SetHeight(C.nameplates.heighthealth)
nameplate.health.hlr, nameplate.health.hlg, nameplate.health.hlb, nameplate.health.hla = hlr, hlg, hlb, hla
CreateBackdrop(nameplate.health, default_border)
nameplate.health.text:SetFont(font, font_size - 2, "OUTLINE")
nameplate.health.text:SetJustifyH(C.nameplates.hptextpos)
nameplate.guild:SetFont(font, font_size, font_style)
nameplate.glow:SetWidth(C.nameplates.width + 60)
nameplate.glow:SetHeight(C.nameplates.heighthealth + 30)
nameplate.glow:SetVertexColor(glowr, glowg, glowb, glowa)
nameplate.raidicon:ClearAllPoints()
nameplate.raidicon:SetPoint("BOTTOM", nameplate.health, "TOP", C.nameplates.raidiconoffx, C.nameplates.raidiconoffy)
nameplate.level:SetFont(font, font_size, font_style)
nameplate.raidicon:SetWidth(C.nameplates.raidiconsize)
nameplate.raidicon:SetHeight(C.nameplates.raidiconsize)
for i=1,16 do
UpdateDebuffConfig(nameplate, i)
end
for i=1,5 do
nameplate.combopoints[i]:SetWidth(combo_size)
nameplate.combopoints[i]:SetHeight(combo_size)
nameplate.combopoints[i]:SetPoint("TOPRIGHT", nameplate.health, "BOTTOMRIGHT", -(i-1)*(combo_size+default_border*3), -default_border*3)
CreateBackdrop(nameplate.combopoints[i], default_border)
end
nameplate.castbar:SetPoint("TOPLEFT", nameplate.health, "BOTTOMLEFT", 0, -default_border*3)
nameplate.castbar:SetPoint("TOPRIGHT", nameplate.health, "BOTTOMRIGHT", 0, -default_border*3)
nameplate.castbar:SetHeight(C.nameplates.heightcast)
-- Use the same castbar texture and color as the unit frame castbar (castbar.lua)
local cbtexture = pfUI.media[C.appearance.castbar.texture]
nameplate.castbar:SetStatusBarTexture(cbtexture or hptexture)
local cbr, cbg, cbb, cba = strsplit(",", C.appearance.castbar.castbarcolor)
nameplate.castbar:SetStatusBarColor(cbr, cbg, cbb, cba)
-- reset endTime cache so color/texture refresh takes effect on next cast
nameplate.castbar.lastEndTime = nil
CreateBackdrop(nameplate.castbar, default_border)
nameplate.castbar.text:SetFont(font, font_size, "OUTLINE")
nameplate.castbar.spell:SetFont(font, font_size, "OUTLINE")
nameplate.castbar.icon:SetPoint("BOTTOMLEFT", nameplate.castbar, "BOTTOMRIGHT", default_border*3, 0)
nameplate.castbar.icon:SetPoint("TOPLEFT", nameplate.health, "TOPRIGHT", default_border*3, 0)
nameplate.castbar.icon:SetWidth(C.nameplates.heightcast + default_border*3 + C.nameplates.heighthealth)
CreateBackdrop(nameplate.castbar.icon, default_border)
end
nameplates.OnValueChanged = function()
local plate = this:GetParent().nameplate
if plate and plate.health then
plate.health:SetMinMaxValues(plate.original.healthbar:GetMinMaxValues())
plate.health:SetValue(plate.original.healthbar:GetValue())
end
end
nameplates.OnDataChanged = function(self, plate)
local visible = plate:IsVisible()
local hp = plate.original.healthbar:GetValue()
local hpmin, hpmax = plate.original.healthbar:GetMinMaxValues()
local name = plate.original.name:GetText()
local level = plate.original.level:IsShown() and plate.original.level:GetObjectType() == "FontString" and tonumber(plate.original.level:GetText()) or "??"
if not plate.cache.player and plate.cachedGuid then
plate.cache.player = UnitIsPlayer(plate.cachedGuid) and "PLAYER" or "NPC"
end
local isPlayer
if plate.cache.player then isPlayer = plate.cache.player == "PLAYER" end
local class, ulevel, elite, player, guild = GetUnitInfo(name, true, isPlayer)
-- Use database level ONLY if current level is ?? (fixes ?? after reload, but doesn't override visible levels)
local levelFromDB = false
if level == "??" and ulevel and ulevel > 0 then
level = ulevel
levelFromDB = true
end
local target = plate.istarget
local mouseover = UnitExists("mouseover") and plate.original.glow:IsShown() or nil
local unitstr = target and "target" or mouseover and "mouseover" or nil
local red, green, blue = plate.original.healthbar:GetStatusBarColor()
local unittype = GetUnitType(red, green, blue) or "ENEMY_NPC"
local font_size = C.nameplates.use_unitfonts == "1" and C.global.font_unit_size or C.global.font_size
-- use unit guid as unitstr if possible
if not unitstr then
unitstr = plate.cachedGuid
end
-- ignore players with npc names if plate level is lower than player level
if ulevel and ulevel > (level == "??" and -1 or level) then player = nil end
-- cache name and reset unittype on change
if plate.cache.name ~= name then
plate.cache.name = name
plate.cache.player = nil
plate.cdCache = nil -- new unit, reset spell-keyed timer cache
plate.name:SetText(GetNameString(name))
end
-- read and cache unittype
if plate.cache.player then
-- overwrite unittype from cache if existing
player = plate.cache.player == "PLAYER" and true or nil
elseif unitstr then
-- read unit type while unitstr is set
plate.cache.player = UnitIsPlayer(unitstr) and "PLAYER" or "NPC"
end
if player and unittype == "ENEMY_NPC" then unittype = "ENEMY_PLAYER" end
if player and unittype == "FRIENDLY_NPC" then unittype = "FRIENDLY_PLAYER" end
elite = plate.original.levelicon:IsShown() and not player and "boss" or elite
if not class then plate.wait_for_scan = true end
-- skip data updates on invisible frames
if not visible then return end
-- target event sometimes fires too quickly, where nameplate identifiers are not
-- yet updated. So while being inside this event, we cannot trust the unitstr.
if event == "PLAYER_TARGET_CHANGED" then unitstr = nil end
-- remove unitstr on unit name mismatch
if unitstr and UnitName(unitstr) ~= name then unitstr = nil end
-- use mobhealth values if addon is running
if (MobHealth3 or MobHealthFrame) and target and name == UnitName('target') and MobHealth_GetTargetCurHP() then
hp = MobHealth_GetTargetCurHP() > 0 and MobHealth_GetTargetCurHP() or hp
hpmax = MobHealth_GetTargetMaxHP() > 0 and MobHealth_GetTargetMaxHP() or hpmax
end
-- always make sure to keep plate visible
plate:Show()
if target and cfg.targetglow then
plate.glow:Show() else plate.glow:Hide()
end
-- target indicator
if cfg.outcombatstate then
local guid = plate.cachedGuid or ""
-- determine color based on combat state
local color = GetCombatStateColor(guid)
if not color then color = combatstate.NONE end
-- set border color
plate.health.backdrop:SetBackdropBorderColor(color.r, color.g, color.b, color.a)
elseif target and cfg.targethighlight then
plate.health.backdrop:SetBackdropBorderColor(plate.health.hlr, plate.health.hlg, plate.health.hlb, plate.health.hla)
elseif C.nameplates.outfriendlynpc == "1" and unittype == "FRIENDLY_NPC" then
plate.health.backdrop:SetBackdropBorderColor(unpack(unitcolors[unittype]))
elseif C.nameplates.outfriendly == "1" and unittype == "FRIENDLY_PLAYER" then
plate.health.backdrop:SetBackdropBorderColor(unpack(unitcolors[unittype]))
elseif C.nameplates.outneutral == "1" and strfind(unittype, "NEUTRAL") then
plate.health.backdrop:SetBackdropBorderColor(unpack(unitcolors[unittype]))
elseif C.nameplates.outenemy == "1" and strfind(unittype, "ENEMY") then
plate.health.backdrop:SetBackdropBorderColor(unpack(unitcolors[unittype]))
else
plate.health.backdrop:SetBackdropBorderColor(er,eg,eb,ea)
end
-- hide frames according to the configuration
local TotemIcon = TotemPlate(name)
if TotemIcon then
-- create totem icon
plate.totem.icon:SetTexture("Interface\\Icons\\" .. TotemIcon)
plate.glow:Hide()
plate.level:Hide()
plate.name:Hide()
plate.health:Hide()
plate.guild:Hide()
plate.totem:Show()
elseif HidePlate(unittype, name, (hpmax-hp == hpmin), target) then
plate.level:SetPoint("RIGHT", plate.name, "LEFT", -3, 0)
plate.name:SetParent(plate)
plate.guild:SetPoint("BOTTOM", plate.name, "BOTTOM", -2, -(font_size + 2))
plate.level:Show()
plate.name:Show()
plate.health:Hide()
if guild and C.nameplates.showguildname == "1" then
plate.glow:SetPoint("CENTER", plate.name, "CENTER", 0, -(font_size / 2) - 2)
else
plate.glow:SetPoint("CENTER", plate.name, "CENTER", 0, 0)
end
plate.totem:Hide()
else
plate.level:SetPoint("RIGHT", plate.health, "LEFT", -5, 0)
plate.name:SetParent(plate.health)
plate.guild:SetPoint("BOTTOM", plate.health, "BOTTOM", 0, -(font_size + 4))
plate.level:Show()
plate.name:Show()
plate.health:Show()
plate.glow:SetPoint("CENTER", plate.health, "CENTER", 0, 0)
plate.totem:Hide()
end
-- Gate level SetText behind a (level, elite) cache. string.format
-- here was firing every OnDataChanged tick per plate; with the cache
-- it only re-formats on real changes (level up / elite-state flip).
if plate.cache.level ~= level or plate.cache.elite ~= elite then
plate.cache.level = level
plate.cache.elite = elite
plate.level:SetText(string.format("%s%s", level, (elitestrings[elite] or "")))
end
-- Set level color from GetDifficultyColor when using DB level
if levelFromDB and type(level) == "number" then
local color = GetDifficultyColor(level)
plate.level:SetTextColor(color.r + 0.3, color.g + 0.3, color.b + 0.3, 1)
end
if guild and C.nameplates.showguildname == "1" then
plate.guild:SetText(guild)
if guild == GetGuildInfo("player") then
plate.guild:SetTextColor(0, 0.9, 0, 1)
else
plate.guild:SetTextColor(0.8, 0.8, 0.8, 1)
end
plate.guild:Show()
else
plate.guild:Hide()
end
-- PERF: Only update bar + HP text when values actually changed
if plate.cache.hp ~= hp or plate.cache.hpmax ~= hpmax then
plate.cache.hp = hp
plate.cache.hpmax = hpmax
plate.health:SetMinMaxValues(hpmin, hpmax)
plate.health:SetValue(hp)
if cfg.showhp then
local rhp, rhpmax, estimated
local guid = plate.cachedGuid
if guid and GetUnitField then
local npHp = GetUnitField(guid, "health")
local npMaxHp = GetUnitField(guid, "maxHealth")
if npHp and npHp > 0 and npMaxHp and npMaxHp > 0 and npMaxHp ~= 100 then
rhp, rhpmax = npHp, npMaxHp
end
end
-- Fallback to existing methods
if not rhp then
if hpmax > 100 or (round(hpmax/100*hp) ~= hp) then
rhp, rhpmax = hp, hpmax
elseif pfUI.libhealth and pfUI.libhealth.enabled then
rhp, rhpmax, estimated = pfUI.libhealth:GetUnitHealthByName(name,level,tonumber(hp),tonumber(hpmax))
end
end
local setting = cfg.hptextformat
local hasdata = ( rhp and rhpmax ) or estimated or hpmax > 100 or (round(hpmax/100*hp) ~= hp)
if setting == "curperc" and hasdata and rhp then
plate.health.text:SetText(string.format("%s | %s%%", Abbreviate(rhp), ceil(hp/hpmax*100)))
elseif setting == "cur" and hasdata and rhp then
plate.health.text:SetText(string.format("%s", Abbreviate(rhp)))
elseif setting == "curmax" and hasdata and rhp then
plate.health.text:SetText(string.format("%s - %s", Abbreviate(rhp), Abbreviate(rhpmax)))
elseif setting == "curmaxs" and hasdata and rhp then
plate.health.text:SetText(string.format("%s / %s", Abbreviate(rhp), Abbreviate(rhpmax)))
elseif setting == "curmaxperc" and hasdata and rhp then
plate.health.text:SetText(string.format("%s - %s | %s%%", Abbreviate(rhp), Abbreviate(rhpmax), ceil(hp/hpmax*100)))
elseif setting == "curmaxpercs" and hasdata and rhp then
plate.health.text:SetText(string.format("%s / %s | %s%%", Abbreviate(rhp), Abbreviate(rhpmax), ceil(hp/hpmax*100)))
elseif setting == "deficit" and rhp then
plate.health.text:SetText(string.format("-%s" .. (hasdata and "" or "%%"), Abbreviate(rhpmax - rhp)))
else -- "percent" as fallback
plate.health.text:SetText(string.format("%s%%", ceil(hp/hpmax*100)))
end
else
plate.health.text:SetText()
end
end -- hp cache gate
local r, g, b, a = unpack(unitcolors[unittype])
if class then
if unittype == "ENEMY_PLAYER" and C.nameplates["enemyclassc"] == "1" then
r, g, b, a = PFUI_CLASS_COLORS[class]:GetRGBA()
elseif unittype == "FRIENDLY_PLAYER" and C.nameplates["friendclassc"] == "1" then
r, g, b, a = PFUI_CLASS_COLORS[class]:GetRGBA()
end
end
if unitstr and UnitIsTapped(unitstr) and not UnitIsTappedByPlayer(unitstr) then
r, g, b, a = .5, .5, .5, .8
end
if cfg.barcombatstate then
local guid = plate.cachedGuid or ""
local color = GetCombatStateColor(guid)
if color then
r, g, b, a = color.r, color.g, color.b, color.a
end
end
if r ~= plate.cache.r or g ~= plate.cache.g or b ~= plate.cache.b then
plate.health:SetStatusBarColor(r, g, b, a)
plate.cache.r, plate.cache.g, plate.cache.b = r, g, b
end
if r + g + b ~= plate.cache.namecolor and unittype == "FRIENDLY_PLAYER" and C.nameplates["friendclassnamec"] == "1" and class and PFUI_CLASS_COLORS[class] then
plate.name:SetTextColor(r, g, b, a)
plate.cache.namecolor = r + g + b
end
-- update combopoints
for i=1, 5 do plate.combopoints[i]:Hide() end
if target and C.nameplates.cpdisplay == "1" then
for i=1, GetComboPoints("target") do plate.combopoints[i]:Show() end
end
-- update debuffs
local index = 1
local isFriendly = unittype == "FRIENDLY_PLAYER" or unittype == "FRIENDLY_NPC"
local showDebuffsForType = cfg.showdebuffs and (isFriendly and cfg.showdebuffs_friendly or (not isFriendly and cfg.showdebuffs_hostile))
if showDebuffsForType then
-- PERF: Cache verify string - only allocate new string when name/level actually changes
if name ~= plate.cachedVerifyName or level ~= plate.cachedVerifyLevel then
plate.cachedVerifyName = name
plate.cachedVerifyLevel = level
plate.cachedVerify = (name or "") .. ":" .. (level or "")
end
local verify = plate.cachedVerify
-- update cached debuffs
if C.nameplates["guessdebuffs"] == "1" and unitstr then
plate:CacheDebuffs(unitstr, verify)
end
-- update all debuff icons
-- Use IterDebuffs when Nampower available to avoid blind 16-slot loop
-- debuffDisplayBuf is a module-level reusable buffer (no GC churn)
local debuffCount = 0
for i = 1, 16 do debuffDisplayBuf[i].effect = nil end -- clear previous
if unitstr and libdebuff and libdebuff.IterDebuffs and UnitGUID then
_iterDebuffCount = 0
libdebuff:IterDebuffs(unitstr, iterDebuffCallback)
debuffCount = _iterDebuffCount
elseif unitstr and libdebuff then
for i = 1, 16 do
local effect, rank, texture, stacks, dtype, duration, timeleft
effect, rank, texture, stacks, dtype, duration, timeleft = libdebuff:UnitDebuff(unitstr, i)
if effect then
debuffCount = debuffCount + 1
local b = debuffDisplayBuf[debuffCount]
b.effect, b.texture, b.stacks, b.dtype, b.duration, b.timeleft = effect, texture, stacks, dtype, duration, timeleft
end
end
elseif plate.verify == verify then
for i = 1, 16 do
local effect, rank, texture, stacks, dtype, duration, timeleft = plate:UnitDebuff(i)
if effect then
debuffCount = debuffCount + 1
local b = debuffDisplayBuf[debuffCount]
b.effect, b.texture, b.stacks, b.dtype, b.duration, b.timeleft = effect, texture, stacks, dtype, duration, timeleft
end
end
end
for i = 1, 16 do
local effect, texture, stacks, dtype, duration, timeleft
if debuffDisplayBuf[i].effect then
local b = debuffDisplayBuf[i]
effect, texture, stacks, dtype, duration, timeleft = b.effect, b.texture, b.stacks, b.dtype, b.duration, b.timeleft
end
if effect and texture and DebuffFilter(effect) then
if not plate.debuffs[index] then
CreateDebuffIcon(plate, index)
UpdateDebuffConfig(plate, index)
end
plate.debuffs[index]:Show()
plate.debuffs[index].icon:SetTexture(texture)
plate.debuffs[index].icon:SetTexCoord(.078, .92, .079, .937)
if stacks and stacks > 1 and C.nameplates.debuffs["showstacks"] == "1" then
plate.debuffs[index].stacks:SetText(stacks)
plate.debuffs[index].stacks:Show()
else
plate.debuffs[index].stacks:Hide()
end
if duration and timeleft and cfg.debufftimers then
plate.cdCache = plate.cdCache or {}
local newStart = GetTime() + timeleft - duration
local slotCache = plate.cdCache[index]
local cachedStart = slotCache and slotCache.effect == effect and slotCache.start
local cd = plate.debuffs[index].cd
cd:Show()
if not cachedStart or abs(cachedStart - newStart) > 0.5 then
if not cd.configCached or cd.cachedAnim ~= cfg.debuffanim or cd.cachedText ~= cfg.debufftext then
cd.pfCooldownStyleAnimation = cfg.debuffanim
cd.pfCooldownStyleText = cfg.debufftext
cd:SetAlpha(cfg.debuffanim == 1 and 1 or 0)
cd.cachedAnim = cfg.debuffanim
cd.cachedText = cfg.debufftext
cd.configCached = true
end
CooldownFrame_SetTimer(cd, newStart, duration, 1)
plate.cdCache[index] = plate.cdCache[index] or {}
plate.cdCache[index].effect = effect
plate.cdCache[index].start = newStart
end
end
index = index + 1
end
end
end
-- hide remaining debuffs
for i = index, 16 do
if plate.debuffs[i] then
plate.debuffs[i]:Hide()
end
end
end
nameplates.OnShow = function(frame)
local frame = frame or this
local nameplate = frame.nameplate
-- cachedGuid is set by NAME_PLATE_UNIT_ADDED before this fires
local guid = nameplate.cachedGuid
if guid and pfUI.api.libunitscan and pfUI.api.libunitscan.ScanGuid then
-- notify libunitscan so it can cache unit data without mouseover
local name = nameplate.original.name:GetText()
local npcFlags = GetUnitField(guid, "npcFlags") or 0
pfUI.api.libunitscan.ScanGuid(guid, name, npcFlags == 0)
end
nameplates:OnDataChanged(nameplate)
end
nameplates.OnUpdate = function(frame, state)
local nameplate = frame.nameplate
local now = state and state.now or GetTime()
-- cachedGuid is maintained by NAME_PLATE_UNIT_ADDED / _REMOVED events.
-- PERF: Intelligent throttling based on target/castbar status and plate count
-- Use GUID comparison as primary target detection: instant, immune to alpha transitions,
-- and immediately correct on de-target (unlike istarget which updates one tick later)
local targetGuid = state and state.targetGuid
local target = (targetGuid and nameplate.cachedGuid and targetGuid == nameplate.cachedGuid) or
(state and state.hasTarget and frame:GetAlpha() >= 0.99) or nil
-- Target plate castbar runs on its own dedicated frame (nameplates.castbarFrame).
-- For non-target plates with castbar active, use castbar throttle to ensure
-- smooth animation without overloading the central loop.
local isCastingNonTarget = not target and nameplate.castbar and nameplate.castbar:IsShown()
if not isCastingNonTarget and not target and cfg.showcastbar and nameplate.cachedGuid then
local castInfo = GetCastInfo(nameplate.cachedGuid)
if castInfo and castInfo.spellID and castInfo.endTime and castInfo.endTime > now then
isCastingNonTarget = true
elseif pfGetCastInfo and nameplate.castUpdate then
isCastingNonTarget = true
end
end
-- hide castbar before throttle if no cast active
if not isCastingNonTarget and not target then
if nameplate.castbar.isShown then
nameplate.castbar.isShown = nil
nameplate.castbar.lastEndTime = nil
nameplate.castbar:Hide()
end
end
local throttle
if target then
throttle = pfUI.throttle:Get("nameplates_target")
elseif visiblePlateCount > 20 then
throttle = pfUI.throttle:Get("nameplates_mass")
else
throttle = pfUI.throttle:Get("nameplates")
end
-- Non-target plates with active castbar use the castbar throttle
if isCastingNonTarget then
local cbThrottle = pfUI.throttle:Get("nameplates_castbar")
if cbThrottle < throttle then throttle = cbThrottle end
end
-- Check for pending event updates (these bypass throttle for immediate response)
local hasEventUpdate = nameplate.eventcache or nameplate.auraUpdate or nameplate.castUpdate or nameplate.targetUpdate or nameplate.comboUpdate
-- Event updates bypass throttle
if not hasEventUpdate and (nameplate.lasttick or 0) + throttle > now then return end
nameplate.lasttick = now
-- =========================================================================
-- EVERYTHING BELOW RUNS AT THROTTLED RATE (50 FPS target, 10 FPS others)
-- =========================================================================
local update
local original = nameplate.original
local name = original.name:GetText()
local mouseover = state and state.hasMouseover and original.glow:IsShown() or nil
-- trigger queued event update
if hasEventUpdate then
nameplates:OnDataChanged(nameplate)
nameplate.eventcache = nil
nameplate.auraUpdate = nil
nameplate.castUpdate = nil
nameplate.targetUpdate = nil
nameplate.comboUpdate = nil
end
-- =========================================================================
-- OVERLAP/CLICKTHROUGH HANDLING
-- =========================================================================
local useOverlap = C.nameplates["overlap"] == "1" or C.nameplates["vertical_offset"] ~= "0"
local clickable = C.nameplates["clickthrough"] ~= "1"
if not clickable then
frame:EnableMouse(false)
nameplate:EnableMouse(false)
else
local plate = useOverlap and nameplate or frame
plate:EnableMouse(clickable)
end
if C.nameplates["overlap"] == "1" then
if frame:GetWidth() > 1 then
frame:SetWidth(1)
frame:SetHeight(1)
end
else
if not nameplate.dwidth then
nameplate.dwidth = floor(nameplate:GetWidth() * UIParent:GetScale())
end
if floor(frame:GetWidth()) ~= nameplate.dwidth then
frame:SetWidth(nameplate:GetWidth() * UIParent:GetScale())
frame:SetHeight(nameplate:GetHeight() * UIParent:GetScale())
end
end
local mouseEnabled = nameplate:IsMouseEnabled()
if C.nameplates["clickthrough"] == "0" and C.nameplates["overlap"] == "1" and SpellIsTargeting() == mouseEnabled then
nameplate:EnableMouse(not mouseEnabled)
end
-- Cache strata changes
if nameplate.istarget ~= target then
nameplate.target_strata = nil
end
if target and nameplate.target_strata ~= 1 then
nameplate:SetFrameStrata("LOW")
nameplate.target_strata = 1
elseif not target and nameplate.target_strata ~= 0 then
nameplate:SetFrameStrata("BACKGROUND")
nameplate.target_strata = 0
end
nameplate.istarget = target
-- Set non-target plate alpha
local configAlpha = cfg.notargalpha or 0.5
local desiredAlpha = (target or not state.hasTarget) and 1 or configAlpha
if nameplate.cachedAlpha ~= desiredAlpha then
nameplate:SetAlpha(desiredAlpha)
nameplate.cachedAlpha = desiredAlpha
end
-- queue update on visual target update
if nameplate.cache.target ~= target then
nameplate.cache.target = target
update = true
end
-- queue update on visual mouseover update
if nameplate.cache.mouseover ~= mouseover then
nameplate.cache.mouseover = mouseover
update = true
end
-- trigger update when unit was found
if nameplate.wait_for_scan and GetUnitInfo(name, true) then
nameplate.wait_for_scan = nil
update = true
end
-- trigger update when name color changed (includes combat state check)
local r, g, b = original.name:GetTextColor()
local inCombatWithPlayer = false
if cfg.namefightcolor then
local guid = nameplate.cachedGuid
if guid then
inCombatWithPlayer = UnitAffectingCombat(guid) and UnitAffectingCombat("player")
end
end
if r + g + b ~= nameplate.cache.namecolor or (cfg.namefightcolor and nameplate.cache.inCombat ~= inCombatWithPlayer) then
nameplate.cache.namecolor = r + g + b
nameplate.cache.inCombat = inCombatWithPlayer
if cfg.namefightcolor then
if (r > .9 and g < .2 and b < .2) or inCombatWithPlayer then
nameplate.name:SetTextColor(1,0.4,0.2,1)
else
nameplate.name:SetTextColor(r,g,b,1)
end
else
nameplate.name:SetTextColor(1,1,1,1)
end
update = true
end
-- trigger update when level color changed
local r, g, b = original.level:GetTextColor()
r, g, b = r + .3, g + .3, b + .3
if r + g + b ~= nameplate.cache.levelcolor then
nameplate.cache.levelcolor = r + g + b
nameplate.level:SetTextColor(r,g,b,1)
update = true
end
-- PERF: scan for debuff timeouts using indexed access instead of pairs()
if nameplate.debuffcache then
for id = 1, 16 do
local data = nameplate.debuffcache[id]
if data and ( not data.stop or data.stop < now ) and not data.empty then
data.empty = true
update = true
end
end
end
-- use timer based updates
if not nameplate.tick or nameplate.tick < now then
update = true
end
-- run full updates if required
if update then
nameplates:OnDataChanged(nameplate)
nameplate.tick = now + .5
end
-- Zoom animation
if target and cfg.targetzoom then
if not nameplate.health.zoomed then
local zoomval = cfg.zoomval
local wc = cfg.width * zoomval
local hc = cfg.heighthealth * (zoomval * .9)
nameplate.health.targetWidth = wc
nameplate.health.targetHeight = hc
end
local w, h = nameplate.health:GetWidth(), nameplate.health:GetHeight()
local wc, hc = nameplate.health.targetWidth, nameplate.health.targetHeight
if wc and hc then
if wc > w + 0.5 then
nameplate.health:SetWidth(w*1.05)
nameplate.health.zoomTransition = true
elseif hc > h + 0.5 then
nameplate.health:SetHeight(h*1.05)
nameplate.health.zoomTransition = true
else
if nameplate.health.zoomTransition then
nameplate.health:SetWidth(wc)
nameplate.health:SetHeight(hc)
nameplate.health.zoomTransition = nil
end
nameplate.health.zoomed = true
end
end
elseif nameplate.health.zoomed or nameplate.health.zoomTransition then
local w, h = nameplate.health:GetWidth(), nameplate.health:GetHeight()
local wc = cfg.width
local hc = cfg.heighthealth
if w > wc + 0.5 then
nameplate.health:SetWidth(w*.95)
elseif h > hc + 0.5 then
nameplate.health:SetHeight(h*0.95)
else
nameplate.health:SetWidth(wc)
nameplate.health:SetHeight(hc)
nameplate.health.zoomTransition = nil
nameplate.health.zoomed = nil
nameplate.health.targetWidth = nil
nameplate.health.targetHeight = nil
end
end
-- Target plate castbar is handled by nameplates.castbarFrame (dedicated OnUpdate,
-- engine framerate, decoupled from central loop). Only update non-target castbars here.
local isTargetPlate = target or nameplate.istarget or (nameplate.health and nameplate.health.zoomed)
if cfg.showcastbar and not cfg.targetcastbar and not isTargetPlate then
local cbThrottle = pfUI.throttle:Get("nameplates_castbar")
if visiblePlateCount > 20 then
local massThrottle = pfUI.throttle:Get("nameplates_mass")
if massThrottle > cbThrottle then cbThrottle = massThrottle end
end
if (nameplate.castbar_tick or 0) + cbThrottle <= now then
nameplate.castbar_tick = now
nameplates.UpdateCastbar(nameplate, now)
end
elseif cfg.showcastbar and cfg.targetcastbar and not isTargetPlate then
if nameplate.castbar.isShown then
nameplate.castbar.isShown = nil
nameplate.castbar.lastEndTime = nil
nameplate.castbar:Hide()
end
elseif not cfg.showcastbar then
if nameplate.castbar.isShown then
nameplate.castbar.isShown = nil
nameplate.castbar.lastEndTime = nil
nameplate.castbar:Hide()
end
end
end
-- ============================================================================
-- DEDICATED TARGET CASTBAR FRAME
-- Runs on its own OnUpdate, decoupled from the central nameplate loop.
-- Mirrors the approach used in castbar.lua for the target unit frame castbar.
-- ============================================================================
-- Skip SetText when the rounded value hasn't changed since last frame.
-- `string.format("%.2f", ...)` allocates a fresh string every call; this
-- gate compares cheap integers and only formats when the displayed value
-- would actually differ. Reset via lastEndTime-changed branch above.
local function SetCastbarText(castbar, remaining)
local rounded
if C.unitframes.castbardecimals == "1" then
rounded = floor(remaining * 10)
if castbar.lastTextTick ~= rounded then
castbar.lastTextTick = rounded
castbar.text:SetText(rounded / 10)
end
else
rounded = floor(remaining * 100)
if castbar.lastTextTick ~= rounded then
castbar.lastTextTick = rounded
castbar.text:SetText(string.format("%.2f", remaining))
end
end
end
-- Shared castbar update logic (used by both dedicated frame and central loop)
nameplates.UpdateCastbar = function(nameplate, now)
if not nameplate or not nameplate.castbar then return end
local unitstr = nameplate.cachedGuid
local castInfo = unitstr and GetCastInfo(unitstr)
if castInfo and castInfo.spellID then
if castInfo.startTime + castInfo.duration < now then
wipe(castInfo)
nameplate.castbar:Hide()
elseif castInfo.event == "CAST" or castInfo.event == "FAIL" then
wipe(castInfo)
nameplate.castbar:Hide()
else
-- Only update min/max, color and icon once per cast (when endTime changes)
local isChannel = castInfo.event == "CHANNEL"
local duration = castInfo.endTime - castInfo.startTime
if nameplate.castbar.lastEndTime ~= castInfo.endTime then
nameplate.castbar.lastEndTime = castInfo.endTime
nameplate.castbar.lastTextTick = nil
-- Use relative 0..duration range (same as castbar.lua) to avoid
-- floating-point precision loss with large absolute timestamps
nameplate.castbar:SetMinMaxValues(0, duration)
nameplate.castbar:SetStatusBarColor(strsplit(",", C.appearance.castbar[(isChannel and "channelcolor" or "castbarcolor")]))
if castInfo.icon then
nameplate.castbar.icon.tex:SetTexture(castInfo.icon)
nameplate.castbar.icon.tex:SetTexCoord(.1,.9,.1,.9)
end
if cfg.spellname then
nameplate.castbar.spell:SetText(castInfo.spellName)
else
nameplate.castbar.spell:SetText("")
end
end
local barValue
if isChannel then
barValue = castInfo.endTime - now
else
barValue = now - castInfo.startTime
end
barValue = barValue < 0 and 0 or barValue
barValue = barValue > duration and duration or barValue
PixelUtil.SetStatusBarValue(nameplate.castbar, barValue)
SetCastbarText(nameplate.castbar, castInfo.endTime - now)
if not nameplate.castbar.isShown then nameplate.castbar.isShown = true; nameplate.castbar:Show() end
end
else
-- libcast fallback (vanilla without Nampower)
if unitstr and pfGetCastInfo then
local cast, _, _, texture, startTime, endTime = pfGetCastInfo(unitstr)
local channel
if not cast then
channel, _, _, texture, startTime, endTime = pfGetChannelInfo(unitstr)
end
if cast or channel then
local effect = cast or channel
local duration = endTime - startTime
local max = duration / 1000
local cur = now - startTime / 1000
if channel then cur = max + startTime / 1000 - now end
if cur < 0 then cur = 0 end
if cur > max then cur = max end
if nameplate.castbar.lastEndTime ~= endTime then
nameplate.castbar.lastEndTime = endTime
nameplate.castbar.lastTextTick = nil
nameplate.castbar:SetMinMaxValues(0, max)
nameplate.castbar:SetStatusBarColor(strsplit(",", C.appearance.castbar[(channel and "channelcolor" or "castbarcolor")]))
if texture then
nameplate.castbar.icon.tex:SetTexture(texture)
nameplate.castbar.icon.tex:SetTexCoord(.1, .9, .1, .9)
end
if cfg.spellname then
nameplate.castbar.spell:SetText(effect)
else
nameplate.castbar.spell:SetText("")
end
end
PixelUtil.SetStatusBarValue(nameplate.castbar, cur)
SetCastbarText(nameplate.castbar, channel and cur or (max - cur))
if not nameplate.castbar.isShown then nameplate.castbar.isShown = true; nameplate.castbar:Show() end
else
nameplate.castbar.isShown = nil
nameplate.castbar.lastEndTime = nil
nameplate.castbar:Hide()
end
else
nameplate.castbar.isShown = nil
nameplate.castbar.lastEndTime = nil
nameplate.castbar:Hide()
end
end
end
-- Dedicated frame that updates ONLY the target plate castbar.
-- Uses nameplates_castbar throttle from libthrottle.
nameplates.castbarFrame = CreateFrame("Frame", nil, UIParent)
nameplates.castbarFrame:SetScript("OnUpdate", function()
if not cfg.showcastbar then return end
local now = GetTime()
local throttle = pfUI.throttle:Get("nameplates_castbar")
if (this.tick or 0) > now then return end
this.tick = now + throttle
local targetGuid = UnitExists("target") and UnitGUID("target")
if not targetGuid then return end
local frame = C_NamePlate.GetNamePlateForGUID(targetGuid)
if not frame or not frame.nameplate then return end
nameplates.UpdateCastbar(frame.nameplate, now)
end)
-- set nameplate game settings
nameplates.SetGameVariables = function()
-- update visibility (hostile)
if C.nameplates["showhostile"] == "1" then
_G.NAMEPLATES_ON = true
ShowNameplates()
else
_G.NAMEPLATES_ON = nil
HideNameplates()
end
-- update visibility (hostile)
if C.nameplates["showfriendly"] == "1" then
_G.FRIENDNAMEPLATES_ON = true
ShowFriendNameplates()
else
_G.FRIENDNAMEPLATES_ON = nil
HideFriendNameplates()
end
end
nameplates:SetGameVariables()
nameplates.UpdateConfig = function()
-- Refresh config cache for all cfg.* values
CacheConfig()
RebuildOfftanks()
-- update debuff filters
DebuffFilterPopulate()
-- Check friendly zone state when config changes
local disableHostile = C.nameplates["disable_hostile_in_friendly"] == "1"
local disableFriendly = C.nameplates["disable_friendly_in_friendly"] == "1"
local pvpType = GetZonePVPInfo()
local nowFriendly = (pvpType == "friendly")
if nowFriendly and (disableHostile or disableFriendly) then
if not inFriendlyZone then
-- Just entered friendly zone or feature just enabled
inFriendlyZone = true
savedHostileState = C.nameplates["showhostile"]
savedFriendlyState = C.nameplates["showfriendly"]
end
-- Apply current settings based on options
if disableHostile then
_G.NAMEPLATES_ON = nil
HideNameplates()
else
-- Restore hostile if option is off but we're in friendly zone
if savedHostileState == "1" then
_G.NAMEPLATES_ON = true
ShowNameplates()
end
end
if disableFriendly then
_G.FRIENDNAMEPLATES_ON = nil
HideFriendNameplates()
else
-- Restore friendly if option is off but we're in friendly zone
if savedFriendlyState == "1" then
_G.FRIENDNAMEPLATES_ON = true
ShowFriendNameplates()
end
end
return -- Don't call SetGameVariables
elseif inFriendlyZone and not (disableHostile or disableFriendly) then
-- Both features disabled while in friendly zone - restore state
inFriendlyZone = false
if savedHostileState == "1" then
C.nameplates["showhostile"] = savedHostileState
end
if savedFriendlyState == "1" then
C.nameplates["showfriendly"] = savedFriendlyState
end
savedHostileState = nil
savedFriendlyState = nil
-- Fall through to SetGameVariables to restore nameplates
end
-- update nameplate visibility
nameplates:SetGameVariables()
-- apply all config changes
for plate in pairs(registry) do
nameplates.OnConfigChange(plate)
if plate.nameplate and plate.nameplate.cachedGuid then
nameplates:OnDataChanged(plate.nameplate)
end
end
end
local hookOnConfigChange = nameplates.OnConfigChange
nameplates.OnConfigChange = function(self)
hookOnConfigChange(self)
local parent = self
local nameplate = self.nameplate
local plate = (C.nameplates["overlap"] == "1" or C.nameplates["vertical_offset"] ~= "0") and nameplate or parent
-- disable all clicks for now
parent:EnableMouse(false)
nameplate:EnableMouse(false)
-- adjust vertical offset
if C.nameplates["vertical_offset"] ~= "0" then
nameplate:SetPoint("TOP", parent, "TOP", 0, tonumber(C.nameplates["vertical_offset"]))
end
-- replace clickhandler
if C.nameplates["overlap"] == "1" or C.nameplates["vertical_offset"] ~= "0" then
plate:SetScript("OnClick", function() parent:Click() end)
end
-- enable mouselook on rightbutton down
if C.nameplates["rightclick"] == "1" then
plate:SetScript("OnMouseDown", nameplates.mouselook.OnMouseDown)
else
plate:SetScript("OnMouseDown", nil)
end
end
local hookOnDataChanged = nameplates.OnDataChanged
nameplates.OnDataChanged = function(self, nameplate)
hookOnDataChanged(self, nameplate)
-- make sure to keep mouse events disabled on parent nameplate
if (C.nameplates["overlap"] == "1" or C.nameplates["vertical_offset"] ~= "0") then
nameplate.parent:EnableMouse(false)
end
end
-- enable mouselook on rightbutton down
nameplates.mouselook = CreateFrame("Frame", nil, UIParent)
nameplates.mouselook.time = nil
nameplates.mouselook.frame = nil
nameplates.mouselook.OnMouseDown = function()
if arg1 and arg1 == "RightButton" then
MouselookStart()
-- start detection of the rightclick emulation
nameplates.mouselook.time = GetTime()
nameplates.mouselook.frame = this
nameplates.mouselook:Show()
end
end
nameplates.mouselook:SetScript("OnUpdate", function()
-- break here if nothing to do
if not this.time or not this.frame then
this:Hide()
return
end
-- if threshold is reached (0.5 second) no click action will follow
if not IsMouselooking() and this.time + tonumber(C.nameplates["clickthreshold"]) < GetTime() then
this:Hide()
return
end
-- run a usual nameplate rightclick action
if not IsMouselooking() then
this.frame:Click("LeftButton")
if UnitCanAttack("player", "target") and not nameplates.combat.inCombat then AttackTarget() end
this:Hide()
return
end
end)
pfUI.nameplates = nameplates
end)