mirror of
https://github.com/brues-code/pfUI.git
synced 2026-09-22 15:46:56 +00:00
f497c71523
Eight commits off classicapi_next. ClassicAPI's RegisterUnitEvent registers for an event but only delivers it when arg1 is one of the given units, so a handler for one unit stops waking for every other one in the world. The 26 registrations whose unit set is fixed at registration time now name it. The rule throughout is register the superset and keep the handler's own check -- the filter narrows what arrives, it does not decide what to act on. Guards that look unreachable stay put: the filter applies only when arg1 is a string, so an event that fires with a number or no argument is delivered as if plainly registered. Frames whose unit changes at runtime own their subscriptions instead of sorting events out per event. unitframes points them at the unitstr UpdateVisibility already computes -- replacing a string concat, and on a miss a second concat plus a UnitGUID call, for every frame on every unit event in the world -- and a frame that is not in use drops its unit events entirely. nameplates registers per plate against the plate's own token, which is also the only workable shape: slots have no cap, so any nameplate1..N list would have been a guess that fails in exactly the crowded scenes where plates matter. marktracking names mark1 through mark8. A registration keeps its kind, so none of these can be plain-registered first. Both teardown paths PLAYER_LOGOUT guards -- the crash 132 -- now cover the per-frame subscriptions: plates tear down rather than dispatching through logout, and a unit frame takes itself off the visibility scan so it cannot re-register what it just dropped. marktracking also drops its once-a-second full rebuild, which ran for the whole session whether or not a marker existed anywhere. The ticker is created and cancelled with group membership. It is deliberately not keyed on a mark being visible -- a marker on an out-of-range unit shows no row, and that is the case the poll exists to catch. nameplates gates the per-plate update against the floor across all four throttle categories before classifying it, instead of running a GetAlpha, a castbar IsShown, a cast lookup and up to two libthrottle:Get resolutions on plates throttled to 10fps that were going to return anyway. Nothing that would have updated can be turned away by a floor. The four throttles resolve in CacheConfig, where config changes already land. energytick sweeps the clock the server actually runs. There is one regen timer for every power, re-armed every 2s by Player::RegenerateAll and never touched by casting; the five-second rule changes what a tick pays, not when it lands. The sweep is a free-running phase lock on that clock, so Illumination refunds, potions and a Mana Spring totem on its own phase no longer snap the spark mid-cycle, and an 80ms band keeps a correct tick from hitching it at the wrap. The FSR window shades rather than predicting a share of spirit the client cannot compute -- the Casting Regen item ladder is equip auras absent from the buff list. The energy period is summed from SPELL_AURA_MOD_ENERGY_REGEN_TIME across the spellbook and buffs, so Blade Rush is found without GetTalentInfo(2, 16), an ordinal that does not fail when the tree changes but reads another talent's rank. macrotweak is gone -- ClassicAPI 1.15 covers it -- with its config entry, its GUI block, its translations in all eight locales, and actionbar's ButtonMacroScan, the #showtooltip scanner that fed it.
1963 lines
78 KiB
Lua
1963 lines
78 KiB
Lua
pfUI:RegisterModule("nameplates", function ()
|
|
-- disable original castbars
|
|
pcall(SetCVar, "ShowVKeyCastbar", 0)
|
|
|
|
-- Local function references for performance
|
|
local GetTime = GetTime
|
|
local UnitName = UnitName
|
|
local UnitIsPlayer = UnitIsPlayer
|
|
local UnitAffectingCombat = UnitAffectingCombat
|
|
local UnitIsUnit = UnitIsUnit
|
|
local UnitCanAssist = UnitCanAssist
|
|
local UnitHealth = UnitHealth
|
|
local UnitHealthMax = UnitHealthMax
|
|
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 myGuild = nil
|
|
local platecount = 0
|
|
local registry = {}
|
|
-- Subset of registry that currently has a unit assigned (between
|
|
-- NAME_PLATE_UNIT_ADDED and _REMOVED). The central loop iterates this instead
|
|
-- of the full pool so hidden pool slots aren't touched every tick.
|
|
local visiblePlates = {}
|
|
|
|
local raidGuidCache = {} -- guid -> name (rebuilt on RAID_ROSTER_UPDATE/PARTY_MEMBERS_CHANGED)
|
|
|
|
-- Per-GUID cast state, populated from ClassicAPI's UNIT_SPELLCAST_* events
|
|
-- (which now fire for nameplate tokens) and cleared on STOP / plate removal /
|
|
-- expiry. This replaces the old per-tick C_Spell poll on every visible plate:
|
|
-- cast detection is event driven, and GetCastInfo just reads this cache.
|
|
local castState = {}
|
|
-- guid -> nameplate, maintained on NAME_PLATE_UNIT_ADDED/_REMOVED so a cast
|
|
-- event can find its plate in O(1) and only cache casts we actually show.
|
|
local plateByGuid = {}
|
|
|
|
-- One-shot C_Spell poll. Builds the cast struct for a UNIT_SPELLCAST_* event
|
|
-- (the payload carries no timing) and seeds a plate that spawns while its unit
|
|
-- is already mid-cast (its START fired before the plate existed). Picks cast
|
|
-- vs channel itself. Never called per frame.
|
|
local function PollCastInfo(unit)
|
|
if not unit then return nil end
|
|
local name, _, texture, startMs, endMs, _, _, _, spellID = C_Spell.UnitCastingInfo(unit)
|
|
local isChannel
|
|
if not name then
|
|
name, _, texture, startMs, endMs, _, _, spellID = C_Spell.UnitChannelInfo(unit)
|
|
isChannel = true
|
|
end
|
|
if not name or not startMs or not endMs then return nil end
|
|
return {
|
|
spellName = name,
|
|
spellID = spellID,
|
|
icon = texture,
|
|
startTime = startMs / 1000,
|
|
endTime = endMs / 1000,
|
|
duration = (endMs - startMs) / 1000,
|
|
isChannel = isChannel,
|
|
}
|
|
end
|
|
|
|
-- Read a unit's current cast from the event-driven cache (keyed by GUID).
|
|
-- Returns the cached struct while the cast is still active, else nil (and
|
|
-- prunes the expired entry). Same struct shape and callers as before, minus
|
|
-- the per-tick poll.
|
|
local function GetCastInfo(unit)
|
|
if not unit then return nil end
|
|
local guid = UnitGUID(unit)
|
|
if not guid then return nil end
|
|
local info = castState[guid]
|
|
if info and info.endTime > GetTime() then return info end
|
|
if info then castState[guid] = nil end
|
|
return nil
|
|
end
|
|
|
|
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 auraSlots = {} -- reusable GetAuraSlots buffer for the per-plate aura scan
|
|
local threatMemory = {} -- guid -> true if mob had player targeted
|
|
-- local debuffSeen = {} -- reusable table for debuff tracking (avoid GC churn)
|
|
|
|
-- PERF: visiblePlateCount maintained event-driven (NAME_PLATE_UNIT_ADDED/_REMOVED)
|
|
local visiblePlateCount = 0
|
|
|
|
-- ============================================================================
|
|
-- 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.owndebuffs = C.nameplates["owndebuffs"] == "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
|
|
|
|
-- Throttle delays, resolved once instead of per plate per tick.
|
|
-- libthrottle:Get walks the saved-variable table, a defaults fallback and a
|
|
-- preset table, and can build a "<category>_custom" key -- the per-plate
|
|
-- OnUpdate was calling it one or two times for every visible plate, a
|
|
-- hundred times a second, just to decide it had nothing to do.
|
|
--
|
|
-- cfg.throttle_min is the floor across all four. No plate can ever be due
|
|
-- sooner than that, so the update can bail on it before working out which
|
|
-- category it actually belongs to.
|
|
cfg.throttle_target = pfUI.throttle:Get("nameplates_target")
|
|
cfg.throttle_mass = pfUI.throttle:Get("nameplates_mass")
|
|
cfg.throttle_normal = pfUI.throttle:Get("nameplates")
|
|
cfg.throttle_castbar = pfUI.throttle:Get("nameplates_castbar")
|
|
cfg.throttle_min = cfg.throttle_target
|
|
if cfg.throttle_mass < cfg.throttle_min then cfg.throttle_min = cfg.throttle_mass end
|
|
if cfg.throttle_normal < cfg.throttle_min then cfg.throttle_min = cfg.throttle_normal end
|
|
if cfg.throttle_castbar < cfg.throttle_min then cfg.throttle_min = cfg.throttle_castbar end
|
|
|
|
-- 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,
|
|
targetGuid = nil,
|
|
mouseoverGuid = nil,
|
|
}
|
|
|
|
-- cache default border color
|
|
local er, eg, eb, ea = GetStringColor(pfUI_config.appearance.border.color)
|
|
|
|
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, token)
|
|
-- PERF: Quick exit if player not in combat
|
|
if not UnitAffectingCombat("player") then return false end
|
|
if not token then return false end
|
|
if UnitCanAssist("player", token) 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
|
|
|
|
if not UnitAffectingCombat(token) then return false end
|
|
|
|
-- The mob's current target via the nameplate token chain (ClassicAPI):
|
|
-- "nameplateNtarget" resolves to whatever this plate's unit is targeting,
|
|
-- so no GetUnitField("target") or SuperWoW "<guid>target" token needed.
|
|
local target = token .. "target"
|
|
local mobTargetGuid = UnitGUID(target)
|
|
local hasTarget = mobTargetGuid and mobTargetGuid ~= NULL_GUID
|
|
local color = false
|
|
|
|
local castInfo = GetCastInfo(token)
|
|
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 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
|
|
|
|
-- Numeric CreatureType.dbc id for the plate's unit (8 = Critter, 11 = Totem).
|
|
-- It's fixed per unit, so cache it and reset on plate reuse. nil while the GUID
|
|
-- can't resolve yet (incl. the no-SuperWoW case, where cachedGuid is empty).
|
|
-- Locale-proof and covers custom units for free -- no name tables needed.
|
|
local function CreatureType(plate)
|
|
local guid = plate.cachedGuid
|
|
if not guid or guid == "" then return nil end
|
|
if not plate.creatureType then
|
|
plate.creatureType = UnitCreatureTypeID(guid) -- nil retries next call
|
|
end
|
|
return plate.creatureType
|
|
end
|
|
|
|
-- Totem icon: UnitCreatedBySpell returns the totem-drop spell, whose icon IS
|
|
-- the totem's icon. It's a broadcast descriptor field the client has for every
|
|
-- summoned unit in range, so it resolves immediately for passive and active
|
|
-- totems alike -- no self-aura read or attack-cast capture needed.
|
|
--
|
|
-- Re-read the spell every call rather than caching the icon outright: a shaman
|
|
-- can swap the totem in place (same unit, new drop spell) with no plate re-add
|
|
-- to invalidate a cache, so key the cached texture on the spell id and refresh
|
|
-- it only when the spell changes.
|
|
local function TotemPlate(plate)
|
|
if C.nameplates.totemicons ~= "1" then return nil end
|
|
if CreatureType(plate) ~= 11 then return nil end
|
|
local spellId = UnitCreatedBySpell(plate.cachedGuid)
|
|
if spellId ~= plate.totemSpell then
|
|
plate.totemSpell = spellId
|
|
plate.totemIcon = spellId and C_Spell.GetSpellTexture(spellId) or nil
|
|
end
|
|
return plate.totemIcon
|
|
end
|
|
|
|
local function HidePlate(unittype, fullhp, target, plate)
|
|
-- 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 CreatureType(plate) == 8 then
|
|
return true
|
|
elseif C.nameplates.totems == "1" and CreatureType(plate) == 11 then
|
|
return true
|
|
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 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("Model", 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]:SetSize(debuffsize, 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:RegisterUnitEvent("UNIT_COMBO_POINTS", "player")
|
|
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")
|
|
nameplates:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
|
|
-- UNIT_AURA / UNIT_FLAGS / UNIT_SPELLCAST_* are registered per plate, on the
|
|
-- plate's own frame, against its own token -- see OnCreate and
|
|
-- NAME_PLATE_UNIT_ADDED.
|
|
nameplates:RegisterEvent("PLAYER_GUILD_UPDATE")
|
|
|
|
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
|
|
-- The plates hold their own unit subscriptions now, so silencing this
|
|
-- frame alone would leave them dispatching through logout -- exactly what
|
|
-- this branch exists to prevent.
|
|
for plate in pairs(registry) do
|
|
if plate.nameplate then
|
|
plate.nameplate:UnregisterAllEvents()
|
|
plate.nameplate:SetScript("OnEvent", nil)
|
|
end
|
|
end
|
|
return
|
|
|
|
elseif event == "PLAYER_GUILD_UPDATE" and arg1 == 'player' then
|
|
myGuild = GetGuildInfo("player")
|
|
|
|
elseif event == "PLAYER_ENTERING_WORLD" or event == "ZONE_CHANGED_NEW_AREA" then
|
|
if event == "PLAYER_ENTERING_WORLD" then
|
|
CacheConfig()
|
|
this:SetGameVariables()
|
|
RebuildRaidGuidCache()
|
|
frameState.targetGuid = UnitGUID("target")
|
|
myGuild = GetGuildInfo("player")
|
|
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. Cache the GUID for cache keys and the
|
|
-- token itself for token-based UnitX reads (stable per plate lifetime).
|
|
local plate = C_NamePlate.GetNamePlateForUnit(arg1)
|
|
if plate and plate.nameplate then
|
|
visiblePlates[plate] = plate
|
|
local guid = UnitGUID(arg1)
|
|
plate.nameplate.cachedGuid = guid
|
|
plate.nameplate.unit = arg1
|
|
|
|
-- Point this plate's own subscriptions at the token it just took. On a
|
|
-- recycled frame these replace the previous unit rather than stacking:
|
|
-- RegisterUnitEvent on an already-filtered registration swaps the units.
|
|
plate.nameplate:RegisterUnitEvent("UNIT_AURA", arg1)
|
|
plate.nameplate:RegisterUnitEvent("UNIT_FLAGS", arg1)
|
|
plate.nameplate:RegisterUnitEvent("UNIT_SPELLCAST_START", arg1)
|
|
plate.nameplate:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_START", arg1)
|
|
plate.nameplate:RegisterUnitEvent("UNIT_SPELLCAST_STOP", arg1)
|
|
plate.nameplate:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_STOP", arg1)
|
|
plate.nameplate.creatureType = nil -- recompute for the new unit
|
|
plate.nameplate.totemIcon = nil
|
|
plate.nameplate.totemSpell = nil
|
|
if guid then
|
|
plateByGuid[guid] = plate.nameplate
|
|
-- Seed: the unit may already be mid-cast (its UNIT_SPELLCAST_START
|
|
-- fired before this plate existed). One poll here catches that;
|
|
-- ongoing casts arrive via the event.
|
|
castState[guid] = PollCastInfo(arg1)
|
|
end
|
|
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 plate = C_NamePlate.GetNamePlateForUnit(arg1)
|
|
if plate then visiblePlates[plate] = nil end
|
|
local guid = UnitGUID(arg1)
|
|
if guid then
|
|
if debuffCache[guid] then debuffCache[guid] = nil end
|
|
if threatMemory[guid] then threatMemory[guid] = nil end
|
|
if combatColorCache[guid] then combatColorCache[guid] = nil end
|
|
if castState[guid] then castState[guid] = nil end
|
|
if plateByGuid[guid] then plateByGuid[guid] = nil end
|
|
if plate and plate.nameplate and plate.nameplate.cachedGuid == guid then
|
|
plate.nameplate.cachedGuid = nil
|
|
plate.nameplate.unit = nil
|
|
-- Drop the subscriptions with the token: this slot is now free and the
|
|
-- next plate to take it would otherwise feed this frame its events.
|
|
plate.nameplate:UnregisterAllEvents()
|
|
end
|
|
end
|
|
|
|
elseif event == "UPDATE_MOUSEOVER_UNIT" then
|
|
local new = UnitGUID("mouseover")
|
|
local old = frameState.mouseoverGuid
|
|
if new ~= old then
|
|
frameState.mouseoverGuid = new
|
|
local po = old and plateByGuid[old]
|
|
if po then po.eventcache = true end
|
|
local pn = new and plateByGuid[new]
|
|
if pn then pn.eventcache = true end
|
|
end
|
|
|
|
elseif event == "PLAYER_TARGET_CHANGED" then
|
|
frameState.targetGuid = UnitGUID('target')
|
|
-- Flag the target's plate for update
|
|
local plate = C_NamePlate.GetNamePlateForUnit("target")
|
|
if plate and plate.nameplate then
|
|
plate.nameplate.targetUpdate = true
|
|
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's plate for combo point update
|
|
local plate = C_NamePlate.GetNamePlateForUnit("target")
|
|
if plate and plate.nameplate then
|
|
plate.nameplate.comboUpdate = true
|
|
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
|
|
|
|
if this.eventcache then
|
|
this.eventcache = nil
|
|
for plate in pairs(visiblePlates) do
|
|
plate.nameplate.eventcache = true
|
|
end
|
|
end
|
|
|
|
for plate in pairs(visiblePlates) 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
|
|
local 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.original = {}
|
|
|
|
-- Each plate watches its own unit. With RegisterUnitEvent the token IS the
|
|
-- subscription, so there is no central listener sifting every unit event in
|
|
-- the world for a "^nameplate" prefix and then resolving the plate back out
|
|
-- of arg1 -- the event arrives only at the plate it concerns, and `this` is
|
|
-- already that plate. NAME_PLATE_UNIT_ADDED points the registration at the
|
|
-- new token; _REMOVED drops it, which matters because freed slots are
|
|
-- reused and a stale token would feed this frame another unit's events.
|
|
nameplate:SetScript("OnEvent", function()
|
|
if event == "UNIT_AURA" then
|
|
-- a fresh C_UnitAuras read next tick rather than waiting out the 0.5s
|
|
-- throttle -- covers expiry, dispels, refreshes and stack changes
|
|
this.auraUpdate = true
|
|
elseif event == "UNIT_FLAGS" then
|
|
this.eventcache = true
|
|
elseif event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_CHANNEL_START" then
|
|
-- the payload carries no timing, so poll it (PollCastInfo picks cast
|
|
-- vs channel)
|
|
local guid = this.cachedGuid
|
|
if guid then
|
|
castState[guid] = PollCastInfo(this.unit)
|
|
if castState[guid] then
|
|
this.castUpdate = true -- bypass the throttle so the bar shows now
|
|
end
|
|
end
|
|
elseif event == "UNIT_SPELLCAST_STOP" or event == "UNIT_SPELLCAST_CHANNEL_STOP" then
|
|
-- ended: natural, interrupted or cancelled -- the poll fires STOP for all
|
|
local guid = this.cachedGuid
|
|
if guid and castState[guid] then
|
|
castState[guid] = nil
|
|
this.castUpdate = true
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- 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)
|
|
|
|
local NAMEPLATE_OBJECTORDER = { "border", "glow", "name", "level", "levelicon", "raidicon" }
|
|
|
|
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
|
|
|
|
nameplate.original.healthbar:HookScript("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:SetSize(32, 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:SetSize(plate_width, 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:SetSize(C.nameplates.width + 60, 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:SetSize(C.nameplates.raidiconsize, C.nameplates.raidiconsize)
|
|
|
|
for i=1,16 do
|
|
UpdateDebuffConfig(nameplate, i)
|
|
end
|
|
|
|
for i=1,5 do
|
|
nameplate.combopoints[i]:SetSize(combo_size, 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 = GetStringColor(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 "??"
|
|
|
|
-- reset per-unit cache when the plate is reassigned. Gate on GUID *and*
|
|
-- name — name alone misses pool reuse between same-named units (e.g. plate
|
|
-- held a player "Ironforge Guard" and is now reassigned to the NPC by the
|
|
-- same name), which would leak a stale "PLAYER" hint into GetUnitInfo.
|
|
-- Wipe the whole cache table: the PERF gates below ("only update X when
|
|
-- X changed") would otherwise skip bar/color/text updates when the new
|
|
-- unit happens to share a cached value with the previous occupant
|
|
-- (e.g., both at 60% HP percentage on plate pool reuse → bar stays at
|
|
-- the old fill until the new mob actually changes HP).
|
|
if plate.cache.name ~= name or plate.cache.guid ~= plate.cachedGuid then
|
|
table.wipe(plate.cache)
|
|
plate.cache.name = name
|
|
plate.cache.guid = plate.cachedGuid
|
|
plate.cdCache = nil -- new unit, reset spell-keyed timer cache
|
|
plate.name:SetText(GetNameString(name))
|
|
end
|
|
|
|
local target = plate.istarget
|
|
local mouseover = plate.cachedGuid and plate.cachedGuid == frameState.mouseoverGuid or nil
|
|
local unitstr = target and "target" or mouseover and "mouseover" or plate.cachedGuid or nil
|
|
|
|
-- 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 when it doesn't resolve to this plate's unit (stale istarget,
|
|
-- stale mouseover guid or a despawned unit). Must run before the cache fills
|
|
-- below -- an unrelated unitstr would poison cache.player/minion with the
|
|
-- *other* unit's answer, and the nil-gate would then never recompute it.
|
|
if unitstr and UnitName(unitstr) ~= name then unitstr = nil end
|
|
|
|
-- resolve player vs npc from plate's own unit so libunitscan can't return
|
|
-- a player record for an NPC sharing the same name (e.g. Chromie). Stored
|
|
-- as true/false/nil so it doubles as the GetUnitInfo hint.
|
|
if plate.cache.player == nil and unitstr then
|
|
plate.cache.player = UnitIsPlayer(unitstr) and true or false
|
|
end
|
|
if plate.cache.minion == nil and unitstr then
|
|
plate.cache.minion = UnitIsMinion(unitstr) and true or false
|
|
end
|
|
local class, ulevel, elite, player, guild
|
|
if not plate.cache.minion then
|
|
class, ulevel, elite, player, guild = GetUnitInfo(name, true, plate.cache.player)
|
|
end
|
|
if plate.cache.player ~= nil then player = plate.cache.player or nil end
|
|
|
|
-- 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 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
|
|
|
|
-- 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
|
|
|
|
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 and not plate.cache.minion then plate.wait_for_scan = true end
|
|
|
|
-- skip data updates on invisible frames
|
|
if not visible then return end
|
|
|
|
-- always make sure to keep plate visible
|
|
plate:Show()
|
|
|
|
plate.glow:SetShown(target and cfg.targetglow)
|
|
|
|
-- target indicator
|
|
if cfg.outcombatstate then
|
|
local guid = plate.cachedGuid or ""
|
|
|
|
-- determine color based on combat state
|
|
local color = GetCombatStateColor(guid, plate.unit)
|
|
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(plate)
|
|
|
|
if TotemIcon then
|
|
-- icon resolved from the totem-drop spell (already a full path)
|
|
plate.totem.icon:SetTexture(TotemIcon)
|
|
|
|
plate.glow:Hide()
|
|
plate.level:Hide()
|
|
plate.name:Hide()
|
|
plate.health:Hide()
|
|
plate.guild:Hide()
|
|
plate.totem:Show()
|
|
elseif HidePlate(unittype, (hpmax-hp == hpmin), target, plate) 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:SetFormattedText("%s%s", level, (elitestrings[elite] or ""))
|
|
end
|
|
|
|
-- Set level color from GetDifficultyColor when using DB level.
|
|
-- No brightening here: adding a flat offset to all three channels
|
|
-- desaturates every tier toward white and collapses the boundaries
|
|
-- (orange reads as yellow, red reads as orange). Use Blizzard's values.
|
|
-- Clearing cache.levelcolor forces the sync block in OnUpdate to
|
|
-- re-colour once the ?? resolves and it takes ownership again.
|
|
if levelFromDB and type(level) == "number" then
|
|
local color = GetDifficultyColor(level)
|
|
plate.level:SetTextColor(color.r, color.g, color.b, 1)
|
|
plate.cache.levelcolor = nil
|
|
end
|
|
|
|
-- remember who owns the level colour so the two writers cannot fight
|
|
plate.cache.levelfromdb = levelFromDB or nil
|
|
|
|
if guild and C.nameplates.showguildname == "1" then
|
|
plate.guild:SetText(guild)
|
|
if guild == myGuild 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 unit = plate.unit
|
|
if unit then
|
|
local npHp = UnitHealth(unit)
|
|
local npMaxHp = UnitHealthMax(unit)
|
|
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:SetFormattedText("%s | %s%%", Abbreviate(rhp), ceil(hp/hpmax*100))
|
|
elseif setting == "cur" and hasdata and rhp then
|
|
plate.health.text:SetFormattedText("%s", Abbreviate(rhp))
|
|
elseif setting == "curmax" and hasdata and rhp then
|
|
plate.health.text:SetFormattedText("%s - %s", Abbreviate(rhp), Abbreviate(rhpmax))
|
|
elseif setting == "curmaxs" and hasdata and rhp then
|
|
plate.health.text:SetFormattedText("%s / %s", Abbreviate(rhp), Abbreviate(rhpmax))
|
|
elseif setting == "curmaxperc" and hasdata and rhp then
|
|
plate.health.text:SetFormattedText("%s - %s | %s%%", Abbreviate(rhp), Abbreviate(rhpmax), ceil(hp/hpmax*100))
|
|
elseif setting == "curmaxpercs" and hasdata and rhp then
|
|
plate.health.text:SetFormattedText("%s / %s | %s%%", Abbreviate(rhp), Abbreviate(rhpmax), ceil(hp/hpmax*100))
|
|
elseif setting == "deficit" and rhp then
|
|
plate.health.text:SetFormattedText("-%s" .. (hasdata and "" or "%%"), Abbreviate(rhpmax - rhp))
|
|
else -- "percent" as fallback
|
|
plate.health.text:SetFormattedText("%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 and (
|
|
(unittype == "ENEMY_PLAYER" and C.nameplates["enemyclassc"] == "1") or
|
|
(unittype == "FRIENDLY_PLAYER" and C.nameplates["friendclassc"] == "1")
|
|
) then
|
|
r, g, b, a = PFUI_CLASS_COLORS[class]:GetRGBA()
|
|
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, plate.unit)
|
|
|
|
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
|
|
|
|
-- Friendly player names take this colour when friendclassnamec is on.
|
|
-- Ownership is recorded so the OnUpdate sync below stands down rather than
|
|
-- racing us: both writers used to share cache.namecolor despite storing
|
|
-- unrelated quantities (this colour vs Blizzard's name FontString), so
|
|
-- either could suppress the other -- and since nameplate.cache survives
|
|
-- pool reuse, a recycled plate could keep the previous unit's name colour.
|
|
local ownname = unittype == "FRIENDLY_PLAYER" and C.nameplates["friendclassnamec"] == "1"
|
|
and class and true or nil
|
|
|
|
if plate.cache.ownname ~= ownname then
|
|
plate.cache.ownname = ownname
|
|
-- ownership flipped: whichever writer is now in charge must re-assert
|
|
plate.cache.namecolor = nil
|
|
plate.cache.ownnamecolor = nil
|
|
end
|
|
|
|
-- read the class colour directly rather than reusing the bar's r,g,b: the
|
|
-- bar only carries a class colour when friendclassc happens to be on, and
|
|
-- it also picks up the tapped-grey and barcombatstate overrides, neither of
|
|
-- which belongs on the name.
|
|
if ownname then
|
|
local cr, cg, cb, ca = PFUI_CLASS_COLORS[class]:GetRGBA()
|
|
if cr + cg + cb ~= plate.cache.ownnamecolor then
|
|
plate.cache.ownnamecolor = cr + cg + cb
|
|
plate.name:SetTextColor(cr, cg, cb, ca)
|
|
end
|
|
end
|
|
|
|
if target and C.nameplates.cpdisplay == "1" then
|
|
local cp = GetComboPoints("target")
|
|
if plate.cpShown ~= cp then
|
|
for i=1, 5 do plate.combopoints[i]:SetShown(i <= cp) end
|
|
plate.cpShown = cp
|
|
end
|
|
elseif plate.cpShown then
|
|
for i=1, 5 do plate.combopoints[i]:Hide() end
|
|
plate.cpShown = nil
|
|
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
|
|
-- Pull debuffs from C_UnitAuras (HARMFUL range). owndebuffs adds the
|
|
-- PLAYER filter token so only auras whose caster GUID matches the local
|
|
-- player come through. debuffDisplayBuf is a module-level reusable
|
|
-- buffer (no GC churn).
|
|
local debuffCount = 0
|
|
for i = 1, 16 do debuffDisplayBuf[i].effect = nil end
|
|
if unitstr then
|
|
local filter = cfg.owndebuffs and "HARMFUL|PLAYER" or "HARMFUL"
|
|
local now = GetTime()
|
|
-- one GetAuraSlots enumeration, then positional by-slot reads straight
|
|
-- into the reusable buffer: the per-plate scan allocates nothing (no
|
|
-- per-aura table, no result array) and walks the aura array once
|
|
local n = ScanAuraSlots(unitstr, filter, auraSlots, 16)
|
|
for i = 1, n do
|
|
local aname, icon, count, dispelType, duration, expirationTime = C_UnitAuras.UnitAuraBySlot(unitstr, auraSlots[i])
|
|
if not aname then break end
|
|
debuffCount = debuffCount + 1
|
|
local b = debuffDisplayBuf[debuffCount]
|
|
b.effect = aname
|
|
b.texture = icon
|
|
b.stacks = count
|
|
b.dtype = dispelType
|
|
b.duration = duration
|
|
b.timeleft = (expirationTime and expirationTime > 0) and (expirationTime - now) or nil
|
|
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
|
|
-- C_UnitAuras returns the Spell.dbc base duration, which talents
|
|
-- can extend past — Shadow Affinity bumps SW:P from 18s to 24s,
|
|
-- so a fresh cast has timeleft > duration and `now + timeleft -
|
|
-- duration` lands in the future, which CooldownFrame_SetTimer
|
|
-- treats as "not yet started" (no swirl). Widen to whichever is
|
|
-- larger so start <= now.
|
|
local effDuration = duration > timeleft and duration or timeleft
|
|
plate.cdCache = plate.cdCache or {}
|
|
local newStart = GetTime() + timeleft - effDuration
|
|
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, effDuration, 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)
|
|
nameplates:OnDataChanged((frame or this).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.
|
|
|
|
-- Cheap gate first. The central loop calls this for every visible plate ~100
|
|
-- times a second, and classifying the plate below costs two C calls, a cast
|
|
-- lookup and a throttle resolution -- all of it wasted on a plate that is
|
|
-- throttled to 10fps. cfg.throttle_min is the floor across every category,
|
|
-- so nothing that would have updated can be turned away here; the real
|
|
-- category-specific throttle is still applied after the classification.
|
|
-- Event flags bypass both gates, as before.
|
|
local hasEventUpdate = nameplate.eventcache or nameplate.auraUpdate or nameplate.castUpdate or nameplate.targetUpdate or nameplate.comboUpdate
|
|
if not hasEventUpdate and (nameplate.lasttick or 0) + cfg.throttle_min > now then return 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)
|
|
local targetGuid = state and state.targetGuid
|
|
local target = (targetGuid and nameplate.cachedGuid and targetGuid == nameplate.cachedGuid) or
|
|
(state and state.targetGuid 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.unit)
|
|
if castInfo and castInfo.endTime > now 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
|
|
|
|
-- Resolved in CacheConfig, so these are table reads rather than a walk
|
|
-- through the saved variables and preset tables.
|
|
local throttle
|
|
if target then
|
|
throttle = cfg.throttle_target
|
|
elseif visiblePlateCount > 20 then
|
|
throttle = cfg.throttle_mass
|
|
else
|
|
throttle = cfg.throttle_normal
|
|
end
|
|
|
|
-- Non-target plates with active castbar use the castbar throttle
|
|
if isCastingNonTarget and cfg.throttle_castbar < throttle then
|
|
throttle = cfg.throttle_castbar
|
|
end
|
|
|
|
-- The category-specific gate. hasEventUpdate was read above, before the
|
|
-- classification, and still bypasses the 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 = nameplate.cachedGuid and nameplate.cachedGuid == frameState.mouseoverGuid 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:SetSize(1, 1)
|
|
end
|
|
else
|
|
if not nameplate.dwidth then
|
|
nameplate.dwidth = floor(nameplate:GetWidth() * UIParent:GetScale())
|
|
end
|
|
|
|
if floor(frame:GetWidth()) ~= nameplate.dwidth then
|
|
local nameW, nameH = nameplate:GetSize()
|
|
local uiScale = UIParent:GetScale()
|
|
frame:SetSize(nameW * uiScale, nameH * uiScale)
|
|
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.targetGuid) 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. Pass the cached player hint so the
|
|
-- retry probes the same table OnDataChanged will read on the next pass —
|
|
-- otherwise an NPC sharing a name with a known player flips wait_for_scan
|
|
-- off here, then OnDataChanged re-sets it, every frame until the mob scan
|
|
-- lands.
|
|
if nameplate.wait_for_scan and not nameplate.cache.minion and GetUnitInfo(name, true, nameplate.cache.player) then
|
|
nameplate.wait_for_scan = nil
|
|
update = true
|
|
end
|
|
|
|
-- trigger update when name color changed (includes combat state check).
|
|
-- Skipped once OnDataChanged owns the name colour (class-coloured friendly
|
|
-- players), so the two writers cannot overwrite each other.
|
|
if not nameplate.cache.ownname then
|
|
local r, g, b = original.name:GetTextColor()
|
|
local inCombatWithPlayer = cfg.namefightcolor and UnitAffectingCombat(nameplate.unit) and UnitAffectingCombat("player")
|
|
|
|
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
|
|
end
|
|
|
|
-- trigger update when level color changed. Skipped while the level came
|
|
-- from the database (?? plates), where OnDataChanged owns the colour --
|
|
-- otherwise both writers race and cache.levelcolor goes stale, which
|
|
-- leaves a recycled plate wearing the previous unit's colour.
|
|
if not nameplate.cache.levelfromdb then
|
|
local r, g, b = original.level:GetTextColor()
|
|
if r + g + b ~= nameplate.cache.levelcolor then
|
|
nameplate.cache.levelcolor = r + g + b
|
|
nameplate.level:SetTextColor(r,g,b,1)
|
|
update = true
|
|
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:GetSize()
|
|
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:SetSize(wc, 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:GetSize()
|
|
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:SetSize(wc, 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 = cfg.throttle_castbar
|
|
if visiblePlateCount > 20 and cfg.throttle_mass > cbThrottle then
|
|
cbThrottle = cfg.throttle_mass
|
|
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:SetFormattedText("%.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 castInfo = GetCastInfo(nameplate.unit)
|
|
if not castInfo or castInfo.endTime < now then
|
|
nameplate.castbar.isShown = nil
|
|
nameplate.castbar.lastEndTime = nil
|
|
nameplate.castbar:Hide()
|
|
return
|
|
end
|
|
|
|
local isChannel = castInfo.isChannel
|
|
local duration = castInfo.duration
|
|
if nameplate.castbar.lastEndTime ~= castInfo.endTime then
|
|
nameplate.castbar.lastEndTime = castInfo.endTime
|
|
nameplate.castbar.lastTextTick = nil
|
|
-- Relative 0..duration range to avoid float precision loss with large
|
|
-- absolute timestamps.
|
|
nameplate.castbar:SetMinMaxValues(0, duration)
|
|
nameplate.castbar:SetStatusBarColor(GetStringColor(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 = isChannel and (castInfo.endTime - now) or (now - castInfo.startTime)
|
|
if barValue < 0 then barValue = 0 end
|
|
if barValue > duration then barValue = duration end
|
|
nameplate.castbar:SetValue(barValue)
|
|
SetCastbarText(nameplate.castbar, castInfo.endTime - now)
|
|
if not nameplate.castbar.isShown then nameplate.castbar.isShown = true; nameplate.castbar:Show() end
|
|
end
|
|
|
|
-- Dedicated frame that updates ONLY the target plate castbar. Unthrottled:
|
|
-- now that casts are event-driven, this just reads the cache + SetValue, so
|
|
-- it animates the fill every frame for the smoothest sweep on the bar the
|
|
-- player watches most. (Non-target plates stay throttled via the central
|
|
-- loop's nameplates_castbar gate.)
|
|
nameplates.castbarFrame = CreateFrame("Frame", nil, UIParent)
|
|
nameplates.castbarFrame:SetScript("OnUpdate", function()
|
|
if not cfg.showcastbar or not frameState.targetGuid then return end
|
|
local nameplate = plateByGuid[frameState.targetGuid]
|
|
if not nameplate then return end
|
|
nameplates.UpdateCastbar(nameplate, GetTime())
|
|
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) |