diff --git a/modules/gui.lua b/modules/gui.lua index 11ab9529..8a930355 100644 --- a/modules/gui.lua +++ b/modules/gui.lua @@ -2935,7 +2935,7 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function () CreateConfig(U["nameplates"], T["Always Show On Target Units"], C.nameplates, "target", "checkbox") CreateConfig(U["nameplates"], T["Vertical Healthbar"], C.nameplates, "verticalhealth", "checkbox") - CreateConfig(nil, T["SuperWoW Settings"], nil, nil, "header") + CreateConfig(nil, T["Nampower Settings"], nil, nil, "header") CreateConfig(U["nameplates"], T["Overwrite Border Color With Combat State"], C.nameplates, "outcombatstate", "checkbox") CreateConfig(U["nameplates"], T["Overwrite Health Color With Combat State"], C.nameplates, "barcombatstate", "checkbox") CreateConfig(U["nameplates"], T["Overwrite If Unit Is Attacking You"], C.nameplates, "ccombatthreat", "checkbox") diff --git a/modules/nameplates.lua b/modules/nameplates.lua index e9c0ed8c..a6ef6682 100644 --- a/modules/nameplates.lua +++ b/modules/nameplates.lua @@ -58,10 +58,10 @@ pfUI:RegisterModule("nameplates", "vanilla", function () } -- catch all nameplates - local childs = {} -- PERF: Reuse table instead of creating new one each scan + local childs = {} local regions, plate local initialized = 0 - + -- Friendly zone nameplate disable state local savedHostileState = nil local savedFriendlyState = nil @@ -74,15 +74,30 @@ pfUI:RegisterModule("nameplates", "vanilla", function () -- OPTIMIZATION: GUID-based registries for O(1) lookups -- ============================================================================ local guidRegistry = {} -- guid -> plate (for direct event routing) - + 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 debuffCache = {} -- guid -> { [spellID] = { start, duration } } local threatMemory = {} -- guid -> true if mob had player targeted - local debuffSeen = {} -- reusable table for debuff tracking (avoid GC churn) + + -- PERF (Nampower): Reusable per-plate debuff display buffer — avoids GC churn + -- from per-call table creation. Module-level, cleared before each use. + local debuffDisplayBuf = {} + for i = 1, 16 do debuffDisplayBuf[i] = {} end + + -- PERF (Nampower): Module-level IterDebuffs callback — avoids 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: Track visible plate count for adaptive throttling local visiblePlateCount = 0 @@ -113,6 +128,7 @@ pfUI:RegisterModule("nameplates", "vanilla", function () 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.zoominstant = C.nameplates.targetzoominstant == "1" cfg.width = tonumber(C.nameplates.width) or 120 cfg.heighthealth = tonumber(C.nameplates.heighthealth) or 8 cfg.targetglow = C.nameplates.targetglow == "1" @@ -127,12 +143,20 @@ pfUI:RegisterModule("nameplates", "vanilla", function () 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 + 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 -- ============================================================================ @@ -146,40 +170,93 @@ pfUI:RegisterModule("nameplates", "vanilla", function () -- cache default border color local er, eg, eb, ea = GetStringColor(pfUI_config.appearance.border.color) + -- ============================================================================ + -- PERF (Nampower): Bitwise flag helper (Lua 5.0 compatible) + -- ============================================================================ + 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" + + -- ============================================================================ + -- PERF (Nampower): raidGuidCache for O(1) offtank target-name lookup + -- ============================================================================ + local function RebuildRaidGuidCache() + for k in pairs(raidGuidCache) do raidGuidCache[k] = nil end + for i = 1, GetNumRaidMembers() do + local g = GetUnitGUID("raid"..i) + if g then raidGuidCache[g] = UnitName("raid"..i) end + end + for i = 1, GetNumPartyMembers() do + local g = GetUnitGUID("party"..i) + if g then raidGuidCache[g] = UnitName("party"..i) end + end + local pg = GetUnitGUID("player") + if pg then raidGuidCache[pg] = UnitName("player") end + end + + -- ============================================================================ + -- PERF (Nampower): combatColorCache throttles GetCombatStateColor to 0.2s/guid. + -- Uses GetUnitField(flags) instead of UnitAffectingCombat(guid) and + -- raidGuidCache instead of UnitName(target) for O(1) offtank detection. + -- ============================================================================ + local combatColorCache = {} -- guid -> { color, expires } + local function GetCombatStateColor(guid) - -- PERF: Quick exit if not in combat if not UnitAffectingCombat("player") then return false end - if not UnitAffectingCombat(guid) then return false end if UnitCanAssist("player", guid) then return false end + -- PERF: 0.2s throttle per guid + local now = frameState.now + local cached = combatColorCache[guid] + if cached and cached.expires > now then + return cached.color + end + + -- PERF (Nampower): GetUnitField flags instead of UnitAffectingCombat(guid) + 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(guid, "target") + local hasTarget = mobTargetGuid and mobTargetGuid ~= NULL_GUID + local target = guid.."target" local color = false local castInfo = GetCastInfo(guid) - local isCasting = castInfo and castInfo.endTime and frameState.now < castInfo.endTime - local targetingPlayer = UnitIsUnit(target, "player") + local isCasting = castInfo and castInfo.endTime and now < castInfo.endTime + local targetingPlayer = hasTarget and UnitIsUnit(target, "player") - -- Remember if mob targets player, clear only when targeting someone else while NOT casting if targetingPlayer then threatMemory[guid] = true - elseif UnitExists(target) and not isCasting then + elseif hasTarget and not isCasting then threatMemory[guid] = nil end + -- PERF (Nampower): O(1) GUID lookup via raidGuidCache + 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 UnitName(target) and offtanks[strlower(UnitName(target))] then + elseif cfg.ccombatofftank and targetName and offtanks[strlower(targetName)] then color = combatstate.OFFTANK - elseif cfg.ccombatofftank and pfUI.uf and pfUI.uf.raid and pfUI.uf.raid.tankrole[UnitName(target)] then + 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 UnitExists(target) then + elseif cfg.ccombatnothreat and hasTarget then color = combatstate.NOTHREAT - elseif cfg.ccombatstun and not UnitExists(target) and not UnitIsPlayer(guid) then + 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 @@ -188,22 +265,16 @@ pfUI:RegisterModule("nameplates", "vanilla", function () end local function wipe(table) - if type(table) ~= "table" then - return - end - for k in pairs(table) do - table[k] = nil - end + if type(table) ~= "table" then return end + for k in pairs(table) do table[k] = nil end end local function IsNamePlate(frame) if frame:GetObjectType() ~= NAMEPLATE_FRAMETYPE then return nil end regions = plate:GetRegions() - if not regions then return nil end if not regions.GetObjectType then return nil end if not regions.GetTexture then return nil end - if regions:GetObjectType() ~= "Texture" then return nil end return regions:GetTexture() == "Interface\\Tooltips\\Nameplate-Border" or nil end @@ -211,9 +282,7 @@ pfUI:RegisterModule("nameplates", "vanilla", function () 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) @@ -233,21 +302,13 @@ pfUI:RegisterModule("nameplates", "vanilla", function () 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 + 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 @@ -257,8 +318,6 @@ pfUI:RegisterModule("nameplates", "vanilla", function () if string.find(name, totem) then return true end end end - - -- nothing to hide return nil end @@ -269,42 +328,29 @@ pfUI:RegisterModule("nameplates", "vanilla", function () 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" + 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 @@ -313,41 +359,42 @@ pfUI:RegisterModule("nameplates", "vanilla", function () 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 + 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 + if self.debuffcache[id] then self.debuffcache[id].empty = true end end - - -- Get GUID - unitstr may already be a GUID from plate.parent:GetName(1) - local guid = unitstr - if unitstr and not string.find(unitstr, "^0x") and GetUnitGUID then - guid = GetUnitGUID(unitstr) or unitstr - end - - if guid then + -- PERF (Nampower): Use IterDebuffs if available, else fall back to slot loop + if unitstr and libdebuff.IterDebuffs and GetUnitGUID then + local id = 0 + libdebuff:IterDebuffs(unitstr, function(auraSlot, spellId, effect, texture, stacks, dtype, duration, timeleft) + if not effect or not texture then return end + id = id + 1 + if id > 16 then return end + local stop = (timeleft and timeleft > 0) and (now + timeleft) or nil + local start = stop and (stop - (duration or 0)) or now + 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) + else for id = 1, 16 do local effect, _, texture, stacks, dtype, duration, timeleft - effect, _, texture, stacks, dtype, duration, timeleft = libdebuff:UnitDebuff(guid, id) + effect, _, texture, stacks, dtype, duration, timeleft = libdebuff:UnitDebuff(unitstr, id) if effect and texture then local stop = (timeleft and timeleft > 0) and (now + timeleft) or nil local start = stop and (stop - (duration or 0)) or now @@ -362,29 +409,23 @@ pfUI:RegisterModule("nameplates", "vanilla", function () 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 - - -- break on timeout debuffs + if not self.debuffcache[id].stop then return end if self.debuffcache[id].empty then return end - if self.debuffcache[id].stop and self.debuffcache[id].stop < GetTime() then return end - - -- return cached debuff + if self.debuffcache[id].stop < GetTime() then return end local c = self.debuffcache[id] - local timeleft = c.stop and (c.stop - GetTime()) or -1 - return c.effect, c.rank, c.texture, c.stacks, c.dtype, c.duration, timeleft + 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(1) + plate.debuffs[index]:SetFrameLevel(4) plate.debuffs[index].icon = plate.debuffs[index]:CreateTexture(nil, "BACKGROUND") plate.debuffs[index].icon:SetTexture(.3,1,.8,1) @@ -397,21 +438,20 @@ pfUI:RegisterModule("nameplates", "vanilla", function () 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 pfUI.client <= 11200 and 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 or TBC+ 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" @@ -419,8 +459,6 @@ pfUI:RegisterModule("nameplates", "vanilla", function () 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) @@ -428,14 +466,12 @@ pfUI:RegisterModule("nameplates", "vanilla", function () 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 @@ -445,52 +481,41 @@ pfUI:RegisterModule("nameplates", "vanilla", function () 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 - - -- Update scale for TBC+ + nameplate.debuffs[i].cd.pfCooldownStyleText = tonumber(C.nameplates.debufftext) or 1 + nameplate.debuffs[i].cd.pfCooldownStyleAnimation = tonumber(C.nameplates.debuffanim) or 0 if pfUI.client > 11200 then - local debuffsize = tonumber(C.nameplates.debuffsize) - local cdScale = debuffsize / 32 - nameplate.debuffs[i].cd:SetScale(cdScale) + nameplate.debuffs[i].cd:SetScale(debuffsize / 32) end 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") - - -- Cast tracking handled by libdebuff (SPELL_START/GO/FAILED events) - -- No local event registration needed + 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") + -- PERF (Nampower): UNIT_FLAGS_GUID fires instantly on flag changes (stun, combat) + if GetUnitField then + nameplates:RegisterEvent("UNIT_FLAGS_GUID") + end -- Callback from libdebuff when auras change (GUID-based, event-driven) nameplates.OnAuraUpdate = function(self, guid) if not guid then return end - - -- Fast path: GUID already registered local plate = guidRegistry[guid] if plate and plate.nameplate then plate.nameplate.auraUpdate = true return end - - -- Fallback: plate may have become visible but OnUpdate hasn't run yet - -- to register its GUID. Use GetName(1) directly since cachedGuid may - -- not be set yet if OnUpdate hasn't fired for this plate. + -- Fallback: plate visible but GUID not yet registered for _, frame in pairs(registry) do if frame.nameplate then local frameGuid = frame:GetName(1) @@ -504,8 +529,16 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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 = guidRegistry[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) @@ -514,59 +547,48 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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 + 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 - + 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 == "UNIT_FLAGS_GUID" then + -- PERF (Nampower): Instant flag change notification — bypass throttle + local plate = guidRegistry[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 = GetUnitGUID("target") if targetGuid then local plate = guidRegistry[targetGuid] @@ -574,11 +596,9 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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 = GetUnitGUID("target") if targetGuid then local plate = guidRegistry[targetGuid] @@ -592,25 +612,19 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") end) nameplates:SetScript("OnUpdate", function() - -- PERF: Throttle central OnUpdate to ~100 FPS (0.01s) 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 + for plate in pairs(registry) do plate.eventcache = true end end - -- PERF: Update visible plate count periodically for adaptive throttling if frameState.now - lastVisibleCheck > 0.5 then lastVisibleCheck = frameState.now local count = 0 @@ -620,21 +634,14 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") visiblePlateCount = count end - -- Throttle ONLY the nameplate scanner local scanThrottle = nameplates.combat and nameplates.combat.inCombat and 0.1 or 0.05 - local shouldScan = (this.tick or 0) <= frameState.now - if shouldScan then + if (this.tick or 0) <= frameState.now then this.tick = frameState.now + scanThrottle - - -- detect new nameplates parentcount = WorldFrame:GetNumChildren() if initialized < parentcount then - -- PERF: Reuse childs table instead of creating new one - local newchilds = { WorldFrame:GetChildren() } - for i = 1, parentcount do - childs[i] = newchilds[i] - end - + for i = table.getn(childs), 1, -1 do childs[i] = nil end + local tmp = { WorldFrame:GetChildren() } + for i = 1, parentcount do childs[i] = tmp[i] end for i = initialized + 1, parentcount do plate = childs[i] if IsNamePlate(plate) and not registry[plate] then @@ -642,42 +649,25 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") registry[plate] = plate end end - initialized = parentcount end end - -- Central OnUpdate for all visible plates for plate in pairs(registry) do if plate:IsVisible() then nameplates.OnUpdate(plate, frameState) else - -- PERF: Clean up ALL caches for hidden plates to prevent memory leak local guid = plate.nameplate and plate.nameplate.cachedGuid if guid then - -- Remove from guidRegistry - if guidRegistry[guid] == plate then - guidRegistry[guid] = nil - end - - -- Clean cast cache ONLY if cast has expired - -- (Don't delete active casts just because plate was hidden briefly) + if guidRegistry[guid] == plate then guidRegistry[guid] = nil end local castInfo = GetCastInfo(guid) if castInfo and castInfo.endTime and castInfo.endTime < frameState.now then - if pfUI.libdebuff_casts then - pfUI.libdebuff_casts[guid] = nil - end - end - - -- Clean debuffCache - if debuffCache[guid] then - debuffCache[guid] = nil - end - - -- Clean threatMemory - if threatMemory[guid] then - threatMemory[guid] = nil + if pfUI.libdebuff_casts then pfUI.libdebuff_casts[guid] = nil end end + if debuffCache[guid] then debuffCache[guid] = nil end + if threatMemory[guid] then threatMemory[guid] = nil end + -- PERF (Nampower): Also clean combatColorCache for hidden plates + if combatColorCache[guid] then combatColorCache[guid] = nil end end end end @@ -699,10 +689,8 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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 + for k in pairs(threatMemory) do threatMemory[k] = nil end + for k in pairs(combatColorCache) do combatColorCache[k] = nil end end end) @@ -711,7 +699,6 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") platecount = platecount + 1 platename = "pfNamePlate" .. platecount - -- create pfUI nameplate overlay local nameplate = CreateFrame("Button", platename, parent) nameplate.platename = platename nameplate:EnableMouse(0) @@ -721,7 +708,6 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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) @@ -739,11 +725,10 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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:SetFrameLevel(4) nameplate.health.text = nameplate.health:CreateFontString(nil, "OVERLAY", "GameFontNormal") nameplate.health.text:SetAllPoints() nameplate.health.text:SetTextColor(1,1,1,1) @@ -790,13 +775,9 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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) + 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 @@ -835,13 +816,11 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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 HookScript(parent, "OnShow", nameplates.OnShow) - -- NOTE: OnUpdate is now handled centrally, not per-plate/ - parent:SetScript("OnUpdate", nil) -- Disable Blizzard's OnUpdate + parent:SetScript("OnUpdate", nil) nameplates.OnConfigChange(parent) nameplates.OnShow(parent) @@ -858,62 +837,43 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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 + local c = combatstate 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) - offtanks = {} - for k, v in pairs({strsplit("#", C.nameplates.combatofftanks)}) do - offtanks[string.lower(v)] = true - end - nameplate:SetWidth(plate_width) nameplate:SetHeight(plate_height) nameplate:SetPoint("TOP", parent, "TOP", 0, 0) - nameplate.name:SetFont(font, font_size, font_style) - nameplate.health:SetOrientation(orientation) nameplate.health:SetPoint("TOP", nameplate.name, "BOTTOM", 0, healthoffset) 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,16 do UpdateDebuffConfig(nameplate, i) end for i=1,5 do nameplate.combopoints[i]:SetWidth(combo_size) @@ -925,15 +885,12 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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) @@ -944,8 +901,17 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") nameplates:OnDataChanged(nameplate) end - nameplates.OnValueChanged = function(arg1) - nameplates:OnDataChanged(this:GetParent().nameplate) + -- ============================================================================ + -- PERF (ShaguPlates): OnValueChanged only syncs bar values — never triggers + -- the expensive OnDataChanged. HP text updates happen inside OnDataChanged + -- when the hp/hpmax cache detects an actual change. + -- ============================================================================ + 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) @@ -955,14 +921,13 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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 "??" local class, ulevel, elite, player, guild = GetUnitData(name, true) - - -- 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 @@ -970,26 +935,19 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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.parent:GetName(1) - end + if not unitstr then unitstr = plate.parent:GetName(1) 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 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 @@ -998,38 +956,23 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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 (skip for GUIDs - they're always valid) if unitstr and not string.find(unitstr, "^0x") 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 + if target and cfg.targetglow then plate.glow:Show() else plate.glow:Hide() end - -- target indicator if cfg.outcombatstate then local guid = plate.parent:GetName(1) 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) @@ -1045,27 +988,16 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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.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() + 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 @@ -1076,18 +1008,22 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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.level:Show(); plate.name:Show(); plate.health:Show() plate.glow:SetPoint("CENTER", plate.health, "CENTER", 0, 0) plate.totem:Hide() end + -- PERF: Cache level string + local levelStr = (plate.cachedLevelVal == level and plate.cachedElite == elite) and plate.cachedLevelStr + if not levelStr then + levelStr = string.format("%s%s", level, (elitestrings[elite] or "")) + plate.cachedLevelStr = levelStr + plate.cachedLevelVal = level + plate.cachedElite = elite + end plate.name:SetText(GetNameString(name)) - plate.level:SetText(string.format("%s%s", level, (elitestrings[elite] or ""))) - - -- Set level color from GetDifficultyColor when using DB level + plate.level:SetText(levelStr) + 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) @@ -1105,53 +1041,52 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") plate.guild:Hide() end - plate.health:SetMinMaxValues(hpmin, hpmax) - plate.health:SetValue(hp) + -- PERF (ShaguPlates): 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 - - -- Try Nampower first for real HP values via GUID - local guid = plate.parent:GetName(1) - 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 then - rhp, rhpmax = npHp, npMaxHp + if cfg.showhp then + local rhp, rhpmax, estimated + local guid = plate.parent:GetName(1) + 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 then + rhp, rhpmax = npHp, npMaxHp + end + end + 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 + -- PERF (ShaguPlates): concat instead of string.format + local setting = cfg.hptextformat + local hasdata = (rhp and rhpmax) or estimated or hpmax > 100 or (round(hpmax/100*hp) ~= hp) + local pct = ceil(hp/hpmax*100) + if setting == "curperc" and hasdata and rhp then + plate.health.text:SetText(Abbreviate(rhp).." | "..pct.."%") + elseif setting == "cur" and hasdata and rhp then + plate.health.text:SetText(Abbreviate(rhp)) + elseif setting == "curmax" and hasdata and rhp then + plate.health.text:SetText(Abbreviate(rhp).." - "..Abbreviate(rhpmax)) + elseif setting == "curmaxs" and hasdata and rhp then + plate.health.text:SetText(Abbreviate(rhp).." / "..Abbreviate(rhpmax)) + elseif setting == "curmaxperc" and hasdata and rhp then + plate.health.text:SetText(Abbreviate(rhp).." - "..Abbreviate(rhpmax).." | "..pct.."%") + elseif setting == "curmaxpercs" and hasdata and rhp then + plate.health.text:SetText(Abbreviate(rhp).." / "..Abbreviate(rhpmax).." | "..pct.."%") + elseif setting == "deficit" and rhp then + plate.health.text:SetText("-"..Abbreviate(rhpmax - rhp)..(hasdata and "" or "%")) + else + plate.health.text:SetText(pct.."%") 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 local r, g, b, a = unpack(unitcolors[unittype]) @@ -1169,10 +1104,7 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") if cfg.barcombatstate then local guid = plate.parent:GetName(1) or "" local color = GetCombatStateColor(guid) - - if color then - r, g, b, a = color.r, color.g, color.b, color.a - end + 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 @@ -1185,65 +1117,102 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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 + -- ========================================================================= + -- 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 - local verify = string.format("%s:%s", (name or ""), (level or "")) - -- update cached debuffs + if showDebuffsForType then + 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 + if C.nameplates["guessdebuffs"] == "1" and unitstr then plate:CacheDebuffs(unitstr, verify) end - -- update all debuff icons - use direct UnitDebuff when unitstr available - for i = 1, 16 do - local effect, rank, texture, stacks, dtype, duration, timeleft + -- PERF (Nampower): reusable debuffDisplayBuf + IterDebuffs — no GC churn + local debuffCount = 0 + for i = 1, 16 do debuffDisplayBuf[i].effect = nil end - if unitstr and libdebuff then + if unitstr and libdebuff and libdebuff.IterDebuffs and GetUnitGUID 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) - elseif plate.verify == verify then - effect, rank, texture, stacks, dtype, duration, timeleft = plate:UnitDebuff(i) + if not effect then break end + if texture 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 not effect then break end + if texture 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 - if effect and texture and DebuffFilter(effect) then + for i = 1, debuffCount do + local b = debuffDisplayBuf[i] + if b.effect and b.texture and DebuffFilter(b.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) + local debuff = plate.debuffs[index] - 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() + -- PERF (ShaguPlates): Only call API when value actually changed + if debuff.lastTexture ~= b.texture then + debuff.lastTexture = b.texture + debuff.icon:SetTexture(b.texture) + debuff.icon:SetTexCoord(.078, .92, .079, .937) end - if duration and timeleft and timeleft >= 0 and cfg.debufftimers then - -- PERF: Only update cooldown if start time changed significantly - local cd = plate.debuffs[index].cd - local newStart = GetTime() + timeleft - duration + if not debuff.isShown then + debuff.isShown = true + debuff:Show() + end - -- Reset cachedStart if the debuff in this slot changed (slots shift when one expires) - if cd.cachedEffect ~= effect then - cd.cachedEffect = effect - cd.cachedStart = nil + if debuff.lastStacks ~= b.stacks then + debuff.lastStacks = b.stacks + if b.stacks and b.stacks > 1 and C.nameplates.debuffs["showstacks"] == "1" then + debuff.stacks:SetText(b.stacks) + debuff.stacks:Show() + else + debuff.stacks:Hide() end + end - if not cd.cachedStart or abs(cd.cachedStart - newStart) > 0.5 then - -- Update config flags only on first run or config change + if b.duration and b.timeleft and cfg.debufftimers then + plate.cdCache = plate.cdCache or {} + local newStart = GetTime() + b.timeleft - b.duration + -- slot-based cache: invalidates automatically when a different debuff shifts into this slot + local slotCache = plate.cdCache[index] + local cachedStart = slotCache and slotCache.effect == b.effect and slotCache.start + local cd = debuff.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 @@ -1252,10 +1221,10 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") cd.cachedText = cfg.debufftext cd.configCached = true end - - cd:Show() - CooldownFrame_SetTimer(cd, newStart, duration, 1) - cd.cachedStart = newStart + CooldownFrame_SetTimer(cd, newStart, b.duration, 1) + plate.cdCache[index] = plate.cdCache[index] or {} + plate.cdCache[index].effect = b.effect + plate.cdCache[index].start = newStart end end @@ -1264,13 +1233,18 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") end end - -- hide remaining debuffs + -- PERF (ShaguPlates): Full state reset on hide for i = index, 16 do - if plate.debuffs[i] then - plate.debuffs[i]:Hide() - if plate.debuffs[i].cd then - plate.debuffs[i].cd.cachedStart = nil + local debuff = plate.debuffs[i] + if debuff and debuff.isShown then + debuff.isShown = nil + debuff.lastTexture = nil + debuff.lastStacks = nil + if debuff.cd then + debuff.cd.cachedStart = nil + debuff.cd.cachedEffect = nil end + debuff:Hide() end end end @@ -1279,11 +1253,23 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") local frame = frame or this local nameplate = frame.nameplate - -- Register GUID when plate becomes visible local guid = frame:GetName(1) if guid then + -- PERF (ShaguPlates): Clean up old GUID mapping when plate is reused + if nameplate.cachedGuid and nameplate.cachedGuid ~= guid then + if guidRegistry[nameplate.cachedGuid] == frame then + guidRegistry[nameplate.cachedGuid] = nil + end + end nameplate.cachedGuid = guid guidRegistry[guid] = frame + + -- PERF (Nampower): Notify libunitscan to cache unit data without requiring mouseover + if pfUI.api and pfUI.api.libunitscan and pfUI.api.libunitscan.ScanGuid then + local unitName = nameplate.original.name:GetText() + local npcFlags = GetUnitField and GetUnitField(guid, "npcFlags") or 0 + pfUI.api.libunitscan.ScanGuid(guid, unitName, npcFlags == 0) + end end nameplates:OnDataChanged(nameplate) @@ -1292,8 +1278,7 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") nameplates.OnUpdate = function(frame, state) local nameplate = frame.nameplate local now = state and state.now or GetTime() - - -- Update GUID registry (lightweight, needed for event routing) + local guid = frame:GetName(1) if guid and guid ~= nameplate.cachedGuid then if nameplate.cachedGuid and guidRegistry[nameplate.cachedGuid] == frame then @@ -1302,26 +1287,101 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") nameplate.cachedGuid = guid guidRegistry[guid] = frame end - - -- 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) + + -- PERF: GUID-based target detection (stable, immune to alpha transitions) 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 + + -- PERF (ShaguPlates): Only set alpha when value actually changed + local desiredAlpha = (target or not state.hasTarget) and 1 or cfg.notargalpha + if nameplate.cachedAlpha ~= desiredAlpha then + nameplate:SetAlpha(desiredAlpha) + nameplate.cachedAlpha = desiredAlpha end + -- ========================================================================= + -- CASTBAR (inline, ShaguPlates approach — no dedicated frame overhead) + -- ========================================================================= + local castbar = nameplate.castbar + local showCast = cfg.showcastbar and (not cfg.targetcastbar or target) + local castInfo = showCast and nameplate.cachedGuid and GetCastInfo(nameplate.cachedGuid) + + if castInfo and castInfo.spellID and castInfo.endTime and now < castInfo.endTime + and castInfo.event ~= "CAST" and castInfo.event ~= "FAIL" then + local isChannel = castInfo.event == "CHANNEL" + local duration = castInfo.endTime - castInfo.startTime + + if castbar.lastEndTime ~= castInfo.endTime then + castbar.lastEndTime = castInfo.endTime + castbar:SetMinMaxValues(0, duration) + castbar:SetStatusBarColor(strsplit(",", C.appearance.castbar[(isChannel and "channelcolor" or "castbarcolor")])) + if castInfo.icon then + castbar.icon.tex:SetTexture(castInfo.icon) + castbar.icon.tex:SetTexCoord(.1,.9,.1,.9) + end + castbar.spell:SetText(cfg.spellname and castInfo.spellName or "") + end + + local barValue = isChannel and (castInfo.endTime - now) or (now - castInfo.startTime) + barValue = barValue < 0 and 0 or (barValue > duration and duration or barValue) + castbar:SetValue(barValue) + + -- PERF (ShaguPlates): Only update timer text when bucket changes + local timeLeft = floor((castInfo.endTime - now) * 10) + if castbar.lastTime ~= timeLeft then + castbar.lastTime = timeLeft + if C.unitframes.castbardecimals == "1" then + castbar.text:SetText(floor((castInfo.endTime - now) * 10) / 10) + else + castbar.text:SetText(string.format("%.2f", castInfo.endTime - now)) + end + end + + if not castbar.isShown then castbar.isShown = true; castbar:Show() end + + elseif showCast and nameplate.cachedGuid and pfGetCastInfo then + -- libcast fallback (vanilla without Nampower) + local cast, _, _, texture, startTime, endTime = pfGetCastInfo(nameplate.cachedGuid) + local channel + if not cast then channel, _, _, texture, startTime, endTime = pfGetChannelInfo(nameplate.cachedGuid) 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 + cur = cur < 0 and 0 or (cur > max and max or cur) + if castbar.lastEndTime ~= endTime then + castbar.lastEndTime = endTime + castbar:SetMinMaxValues(0, max) + castbar:SetStatusBarColor(strsplit(",", C.appearance.castbar[(channel and "channelcolor" or "castbarcolor")])) + if texture then castbar.icon.tex:SetTexture(texture); castbar.icon.tex:SetTexCoord(.1,.9,.1,.9) end + castbar.spell:SetText(cfg.spellname and effect or "") + end + castbar:SetValue(cur) + local remaining = channel and cur or (max - cur) + local timeLeft = floor(remaining * 10) + if castbar.lastTime ~= timeLeft then + castbar.lastTime = timeLeft + if C.unitframes.castbardecimals == "1" then + castbar.text:SetText(floor(remaining * 10) / 10) + else + castbar.text:SetText(string.format("%.2f", remaining)) + end + end + if not castbar.isShown then castbar.isShown = true; castbar:Show() end + else + if castbar.isShown then castbar.isShown = nil; castbar.lastEndTime = nil; castbar.lastTime = nil; castbar:Hide() end + end + else + if castbar.isShown then castbar.isShown = nil; castbar.lastEndTime = nil; castbar.lastTime = nil; castbar:Hide() end + end + + -- ========================================================================= + -- THROTTLE + -- ========================================================================= + local isCastingNonTarget = not target and castbar.isShown local throttle if target then throttle = pfUI.throttle:Get("nameplates_target") @@ -1330,30 +1390,24 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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) + -- EVERYTHING BELOW RUNS AT THROTTLED RATE -- ========================================================================= - + 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 @@ -1363,47 +1417,34 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") nameplate.comboUpdate = nil end - -- ========================================================================= - -- VANILLA OVERLAP/CLICKTHROUGH HANDLING - -- ========================================================================= + -- VANILLA OVERLAP/CLICKTHROUGH if pfUI.client <= 11200 then 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) + 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 + 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 end - -- Cache strata changes - if nameplate.istarget ~= target then - nameplate.target_strata = nil - end + if nameplate.istarget ~= target then nameplate.target_strata = nil end if target and nameplate.target_strata ~= 1 then nameplate:SetFrameStrata("LOW") @@ -1415,47 +1456,20 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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.cache.target ~= target then nameplate.cache.target = target; update = true end + if nameplate.cache.mouseover ~= mouseover then nameplate.cache.mouseover = mouseover; update = true end + if nameplate.wait_for_scan and GetUnitData(name, true) then nameplate.wait_for_scan = nil; update = true end - 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 GetUnitData(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 + local g2 = nameplate.cachedGuid + if g2 then inCombatWithPlayer = UnitAffectingCombat(g2) 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) @@ -1468,7 +1482,6 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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 @@ -1477,352 +1490,133 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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 + 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 + 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 + -- ========================================================================= + -- ZOOM (ShaguPlates: instant zoom option) + -- ========================================================================= 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 + local wc = cfg.width * cfg.zoomval + local hc = cfg.heighthealth * (cfg.zoomval * .9) + if cfg.zoominstant then + if not nameplate.health.zoomed then + nameplate.health:SetWidth(wc); nameplate.health:SetHeight(hc); nameplate.health.zoomed = true + end + else + if not nameplate.health.zoomed then + nameplate.health.targetWidth = wc; nameplate.health.targetHeight = hc + end + local w, h = nameplate.health:GetWidth(), nameplate.health:GetHeight() + local twc, thc = nameplate.health.targetWidth, nameplate.health.targetHeight + if twc and thc then + if twc > w + 0.5 then nameplate.health:SetWidth(w*1.05); nameplate.health.zoomTransition = true + elseif thc > h + 0.5 then nameplate.health:SetHeight(h*1.05); nameplate.health.zoomTransition = true + else + if nameplate.health.zoomTransition then + nameplate.health:SetWidth(twc); nameplate.health:SetHeight(thc); nameplate.health.zoomTransition = nil + end + nameplate.health.zoomed = true 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) + if cfg.zoominstant then + nameplate.health:SetWidth(cfg.width); nameplate.health:SetHeight(cfg.heighthealth) + nameplate.health.zoomTransition = nil; nameplate.health.zoomed = nil + nameplate.health.targetWidth = nil; nameplate.health.targetHeight = nil 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 - -- Non-target castbar: apply separate throttle. - -- In mass pulls (20+ plates) cap at 10 FPS to save performance. - 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 - -- targetcastbar=true means only show on target → hide all non-target castbars - nameplate.castbar:Hide() - elseif not cfg.showcastbar then - nameplate.castbar:Hide() - 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. - -- ============================================================================ - - -- 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) - -- identical to the endTime cache pattern in castbar.lua - local isChannel = castInfo.event == "CHANNEL" - local duration = castInfo.endTime - castInfo.startTime - if nameplate.castbar.lastEndTime ~= castInfo.endTime then - nameplate.castbar.lastEndTime = castInfo.endTime - -- Use relative 0..duration range (same as castbar.lua) to avoid - -- floating-point precision loss with large absolute timestamps - nameplate.castbar:SetMinMaxValues(0, duration) - -- match cast/channel color from appearance config, same as castbar.lua - 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 - -- relative value: elapsed (cast) or remaining (channel) in seconds - local barValue - if isChannel then - barValue = castInfo.endTime - now -- countdown + local w, h = nameplate.health:GetWidth(), nameplate.health:GetHeight() + if w > cfg.width + 0.5 then nameplate.health:SetWidth(w*.95) + elseif h > cfg.heighthealth + 0.5 then nameplate.health:SetHeight(h*0.95) else - barValue = now - castInfo.startTime -- elapsed + nameplate.health:SetWidth(cfg.width); nameplate.health:SetHeight(cfg.heighthealth) + nameplate.health.zoomTransition = nil; nameplate.health.zoomed = nil + nameplate.health.targetWidth = nil; nameplate.health.targetHeight = nil end - barValue = barValue < 0 and 0 or barValue - barValue = barValue > duration and duration or barValue - nameplate.castbar:SetValue(barValue) - local remaining = castInfo.endTime - now - if C.unitframes.castbardecimals == "1" then - nameplate.castbar.text:SetText(floor(remaining * 10) / 10) - else - nameplate.castbar.text:SetText(string.format("%.2f", remaining)) - end - nameplate.castbar:Show() - 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 - -- Only update min/max, color and icon once per cast - if nameplate.castbar.lastEndTime ~= endTime then - nameplate.castbar.lastEndTime = endTime - 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 - nameplate.castbar:SetValue(cur) - local remaining = channel and cur or (max - cur) - if C.unitframes.castbardecimals == "1" then - nameplate.castbar.text:SetText(floor(remaining * 10) / 10) - else - nameplate.castbar.text:SetText(string.format("%.2f", remaining)) - end - nameplate.castbar:Show() - else - nameplate.castbar.lastEndTime = nil - nameplate.castbar:Hide() - end - else - nameplate.castbar.lastEndTime = nil - nameplate.castbar:Hide() end end end - -- Dedicated frame that updates ONLY the target plate castbar on engine framerate. - -- Throttled at 0.020s (~50 FPS) independent of the central nameplate loop. - nameplates.castbarFrame = CreateFrame("Frame", nil, UIParent) - nameplates.castbarFrame:SetScript("OnUpdate", function() - if not cfg.showcastbar then return end - local now = GetTime() - if (this.tick or 0) > now then return end - this.tick = now + 0.020 -- ~50 FPS, matches castbar.lua target castbar - - -- find the current target plate via guidRegistry - local targetGuid = UnitExists("target") and GetUnitGUID("target") - if not targetGuid then return end - - local frame = guidRegistry[targetGuid] - if not frame or not frame.nameplate then return end - - local nameplate = frame.nameplate - -- only run if showcastbar is on and targetcastbar mode is respected - local isTargetPlate = true - if not cfg.targetcastbar then - -- targetcastbar=false means show on ALL plates, target handled here anyway - end - nameplates.UpdateCastbar(nameplate, now) - end) - - -- set nameplate game settings nameplates.SetGameVariables = function() - -- update visibility (hostile) if C.nameplates["showhostile"] == "1" then - _G.NAMEPLATES_ON = true - ShowNameplates() + _G.NAMEPLATES_ON = true; ShowNameplates() else - _G.NAMEPLATES_ON = nil - HideNameplates() + _G.NAMEPLATES_ON = nil; HideNameplates() end - - -- update visibility (hostile) if C.nameplates["showfriendly"] == "1" then - _G.FRIENDNAMEPLATES_ON = true - ShowFriendNameplates() + _G.FRIENDNAMEPLATES_ON = true; ShowFriendNameplates() else - _G.FRIENDNAMEPLATES_ON = nil - HideFriendNameplates() + _G.FRIENDNAMEPLATES_ON = nil; HideFriendNameplates() end end nameplates:SetGameVariables() nameplates.UpdateConfig = function() - -- Refresh config cache for all cfg.* values CacheConfig() - - -- update debuff filters + RebuildOfftanks() 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 + if disableHostile then _G.NAMEPLATES_ON = nil; HideNameplates() + elseif savedHostileState == "1" then _G.NAMEPLATES_ON = true; ShowNameplates() end + if disableFriendly then _G.FRIENDNAMEPLATES_ON = nil; HideFriendNameplates() + elseif savedFriendlyState == "1" then _G.FRIENDNAMEPLATES_ON = true; ShowFriendNameplates() end + return 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 + if savedHostileState == "1" then C.nameplates["showhostile"] = savedHostileState end + if savedFriendlyState == "1" then C.nameplates["showfriendly"] = savedFriendlyState end + savedHostileState = nil; savedFriendlyState = nil end - -- update nameplate visibility nameplates:SetGameVariables() - -- apply all config changes for plate in pairs(registry) do + plate.nameplate.cachedAlpha = nil nameplates.OnConfigChange(plate) end end if pfUI.client <= 11200 then - -- handle vanilla only settings 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 + parent:EnableMouse(false); nameplate:EnableMouse(false) 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 @@ -1833,22 +1627,17 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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() @@ -1856,24 +1645,14 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") 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 this.time or not this.frame then this:Hide(); return end if not IsMouselooking() and this.time + tonumber(C.nameplates["clickthreshold"]) < GetTime() then - this:Hide() - return + 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 + this:Hide(); return end end) end