pfUI:RegisterModule("nameplates", function () -- disable original castbars pcall(SetCVar, "ShowVKeyCastbar", 0) -- Local function references for performance local GetTime = GetTime local UnitName = UnitName local UnitGUID = UnitGUID local UnitIsPlayer = UnitIsPlayer local UnitAffectingCombat = UnitAffectingCombat local UnitIsUnit = UnitIsUnit local UnitHealth = UnitHealth local UnitHealthMax = UnitHealthMax local UnitLevel = UnitLevel local UnitClassification = UnitClassification local UnitIsTapped = UnitIsTapped local UnitIsTappedByPlayer = UnitIsTappedByPlayer local UnitSelectionType = UnitSelectionType local UnitCreatureTypeID = UnitCreatureTypeID local UnitCreatedBySpell = UnitCreatedBySpell local pairs = pairs local next = next local tonumber = tonumber local strlower = strlower local strlen = strlen local floor = floor local ceil = ceil local abs = abs -- Everything a plate shows is driven by events. The engine's own nameplate -- regions are blanked at creation and never read again: a bound plate reads -- its unit through its "nameplateN" token, which ClassicAPI feeds every unit -- event (UNIT_HEALTH, UNIT_FLAGS, UNIT_TARGET, UNIT_AURA_DELTA, the casts, -- ...), each registered on the plate's own frame against that token. There -- is no per-tick loop over the plates and no periodic refresh: the only -- OnUpdate is `driver`, shown while a castbar sweeps or the target zoom -- animates and hidden the moment both sets run dry. 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 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" } -- UnitSelectionType: 0 hostile, 1 unfriendly, 2 neutral, 3 friendly (or a -- PvP-flagged player), 4 unflagged player. Type 3 is ambiguous, so the -- player check promotes *_NPC to *_PLAYER. local selectionTypes = { [0] = "ENEMY_NPC", [1] = "ENEMY_NPC", [2] = "NEUTRAL_NPC", [3] = "FRIENDLY_NPC", [4] = "FRIENDLY_PLAYER", } local NULL_GUID = "0x0000000000000000" local ZOOM_TIME = 0.2 local registry = {} -- engine plate -> true, every pool slot carrying a pfUI overlay local plateByGuid = {} -- guid -> overlay, the plates currently bound to a unit local plateByUnit = {} -- "nameplateN" -> overlay, the same set by token local castState = {} -- guid -> cast struct (see PollCastInfo) local threatMemory = {} -- guid -> true once the mob targeted the player local raidGuidCache = {} -- guid -> name of the player's group local offtanks = {} local activeCasts = {} -- overlay -> true while its castbar sweeps local zooming = {} -- overlay -> true while its target zoom animates local auraSlots = {} -- reusable GetAuraSlots buffer for the per-plate aura scan local targetGuid local playerInCombat local mouseBlocked -- a spell is waiting for a ground target, so the overlays let clicks through local myGuild local platecount = 0 -- Friendly zone nameplate disable state local savedHostileState = nil local savedFriendlyState = nil local inFriendlyZone = false -- ============================================================================ -- Config caching -- ============================================================================ local cfg = {} local debufffilter, debufflist local function DebuffFilterPopulate() debufffilter = C.nameplates.debuffs.filter debufflist = {} if debufffilter ~= "none" then for _, val in pairs({strsplit("#", C.nameplates.debuffs[debufffilter])}) do debufflist[strlower(val)] = true end end end local function DebuffFilter(effect) if debufffilter == "none" then return true end local listed = debufflist[strlower(effect)] if debufffilter == "blacklist" then return not listed end return listed end local function CacheConfig() local N = C.nameplates cfg.showcastbar = N.showcastbar == "1" cfg.targetcastbar = N.targetcastbar == "1" cfg.notargalpha = tonumber(N.notargalpha) or 0.5 if cfg.notargalpha > 1 then cfg.notargalpha = cfg.notargalpha / 100 end if cfg.notargalpha > 1 then cfg.notargalpha = 1 end cfg.namefightcolor = N.namefightcolor == "1" cfg.spellname = N.spellname == "1" cfg.showhp = N.showhp == "1" cfg.hptextformat = N.hptextformat cfg.showdebuffs = N.showdebuffs == "1" cfg.showdebuffs_hostile = N.showdebuffs_hostile == "1" cfg.showdebuffs_friendly = N.showdebuffs_friendly == "1" -- owndebuffs adds the PLAYER filter token so only auras whose caster GUID -- matches the local player come through cfg.debufffilter = N.owndebuffs == "1" and "HARMFUL|PLAYER" or "HARMFUL" cfg.showstacks = N.debuffs.showstacks == "1" cfg.debuffbottom = N.debuffs.position == "BOTTOM" cfg.targetzoom = N.targetzoom == "1" cfg.zoomval = (tonumber(N.targetzoomval) or 0.4) + 1 cfg.width = tonumber(N.width) or 120 cfg.heighthealth = tonumber(N.heighthealth) or 8 cfg.targetglow = N.targetglow == "1" cfg.targethighlight = N.targethighlight == "1" cfg.outcombatstate = N.outcombatstate == "1" cfg.barcombatstate = N.barcombatstate == "1" cfg.combatstate = cfg.outcombatstate or cfg.barcombatstate cfg.ccombatcasting = N.ccombatcasting == "1" cfg.ccombatthreat = N.ccombatthreat == "1" cfg.ccombatnothreat = N.ccombatnothreat == "1" cfg.ccombatstun = N.ccombatstun == "1" cfg.ccombatofftank = N.ccombatofftank == "1" cfg.font_size = N.use_unitfonts == "1" and C.global.font_unit_size or C.global.font_size cfg.totemicons = N.totemicons == "1" cfg.cpdisplay = N.cpdisplay == "1" cfg.showguildname = N.showguildname == "1" cfg.abbrevname = C.unitframes.abbrevname == "1" cfg.friendclassnamec = N.friendclassnamec == "1" cfg.castbardecimals = C.unitframes.castbardecimals == "1" cfg.fullhealth = N.fullhealth == "1" cfg.keeptarget = N.target == "1" cfg.critters = N.critters == "1" cfg.totems = N.totems == "1" cfg.hide = { ENEMY_NPC = N.enemynpc == "1", ENEMY_PLAYER = N.enemyplayer == "1", NEUTRAL_NPC = N.neutralnpc == "1", FRIENDLY_NPC = N.friendlynpc == "1", FRIENDLY_PLAYER = N.friendlyplayer == "1", } cfg.outline = { FRIENDLY_NPC = N.outfriendlynpc == "1", FRIENDLY_PLAYER = N.outfriendly == "1", NEUTRAL_NPC = N.outneutral == "1", ENEMY_NPC = N.outenemy == "1", ENEMY_PLAYER = N.outenemy == "1", } cfg.classcolor = { ENEMY_PLAYER = N.enemyclassc == "1", FRIENDLY_PLAYER = N.friendclassc == "1", } cfg.overlap = N.overlap == "1" cfg.useoverlay = cfg.overlap or N.vertical_offset ~= "0" -- the overlay takes the clicks cfg.clickable = N.clickthrough ~= "1" cfg.rightclick = N.rightclick == "1" cfg.hlr, cfg.hlg, cfg.hlb, cfg.hla = GetStringColor(N.highlightcolor) cfg.br, cfg.bg, cfg.bb, cfg.ba = GetStringColor(C.appearance.border.color) -- sweep rate of the castbars on plates other than the target's cfg.throttle_castbar = pfUI.throttle:Get("nameplates_castbar") local c = combatstate c.CASTING.r, c.CASTING.g, c.CASTING.b, c.CASTING.a = GetStringColor(N.combatcasting) c.THREAT.r, c.THREAT.g, c.THREAT.b, c.THREAT.a = GetStringColor(N.combatthreat) c.NOTHREAT.r, c.NOTHREAT.g, c.NOTHREAT.b, c.NOTHREAT.a = GetStringColor(N.combatnothreat) c.OFFTANK.r, c.OFFTANK.g, c.OFFTANK.b, c.OFFTANK.a = GetStringColor(N.combatofftank) c.STUN.r, c.STUN.g, c.STUN.b, c.STUN.a = GetStringColor(N.combatstun) offtanks = {} for _, v in pairs({strsplit("#", N.combatofftanks)}) do if v ~= "" then offtanks[strlower(v)] = true end end DebuffFilterPopulate() end CacheConfig() local function RebuildRaidGuidCache() table.wipe(raidGuidCache) 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 -- ============================================================================ -- Casts -- ============================================================================ -- One-shot C_Spell poll. Builds the cast struct for a UNIT_SPELLCAST_* event -- (the payload carries no timing) and seeds a plate that binds while its unit -- is already mid-cast (its START fired before the plate existed). Picks cast -- vs channel itself. local function PollCastInfo(unit) local name, _, texture, startMs, endMs, _, _, noInterrupt, spellID = C_Spell.UnitCastingInfo(unit) local isChannel if not name then name, _, texture, startMs, endMs, _, noInterrupt, 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, noInterrupt = noInterrupt, } end -- ============================================================================ -- Plate state -- ============================================================================ -- Updaters reference each other, so they are declared ahead and defined below. local UpdateBarColor, UpdateBorder, UpdateNameColor, UpdateLayout, UpdateDebuffs local UpdateLevel, UpdateCombatColor, UpdateCast, HideCast, SetZoom, Unbind -- Numeric CreatureType.dbc id for the plate's unit (8 = Critter, 11 = Totem). -- It's fixed per unit, so cache it and reset on bind. nil while the creature -- cache hasn't answered yet, in which case the next call asks again. local function CreatureType(plate) if not plate.creatureType then plate.creatureType = UnitCreatureTypeID(plate.unit) end return plate.creatureType end -- The combat-state colour for a bound plate, or nil when the unit shows no -- combat state. The mob's target comes through the plate's token chain -- ("nameplateNtarget"). Runs on the events that can change the answer: -- UNIT_TARGET, UNIT_FLAGS, the casts, the player's combat state and the -- group roster -- never on a timer. local function CombatStateColor(plate) if not playerInCombat or plate.friendly or not plate.incombat then return nil end local guid = plate.guid local target = plate.targetToken local mobTargetGuid = UnitGUID(target) local hasTarget = mobTargetGuid and mobTargetGuid ~= NULL_GUID local info = castState[guid] local isCasting = info and info.endTime > GetTime() local targetingPlayer = hasTarget and UnitIsUnit(target, "player") if targetingPlayer then threatMemory[guid] = true elseif hasTarget and not isCasting then threatMemory[guid] = nil end local targetName = hasTarget and (UnitName(target) or raidGuidCache[mobTargetGuid]) if cfg.ccombatcasting and isCasting then return combatstate.CASTING elseif cfg.ccombatthreat and (targetingPlayer or threatMemory[guid]) then return combatstate.THREAT elseif cfg.ccombatofftank and targetName and offtanks[strlower(targetName)] then return combatstate.OFFTANK elseif cfg.ccombatofftank and pfUI.uf and pfUI.uf.raid and targetName and pfUI.uf.raid.tankrole[targetName] then return combatstate.OFFTANK elseif cfg.ccombatnothreat and hasTarget then return combatstate.NOTHREAT elseif cfg.ccombatstun and not hasTarget then return combatstate.STUN end return nil end -- Returns true when the colour changed. local function ReadCombatColor(plate) local color = cfg.combatstate and CombatStateColor(plate) or nil if color == plate.combatColor then return nil end plate.combatColor = color return true end function UpdateCombatColor(plate) if ReadCombatColor(plate) then UpdateBarColor(plate) UpdateBorder(plate) end end -- Returns true when the type (or the class behind its colour) changed. local function ReadUnitType(plate) local unit = plate.unit local player = UnitIsPlayer(unit) and true or nil local unittype = selectionTypes[UnitSelectionType(unit)] or "ENEMY_NPC" if player then if unittype == "ENEMY_NPC" then unittype = "ENEMY_PLAYER" elseif unittype == "FRIENDLY_NPC" then unittype = "FRIENDLY_PLAYER" end end local class = player and UnitClassBase(unit) or nil if unittype == plate.unittype and class == plate.class then return nil end plate.player, plate.unittype, plate.class = player, unittype, class plate.friendly = unittype == "FRIENDLY_PLAYER" or unittype == "FRIENDLY_NPC" return true end local function UpdateUnitType(plate) if not ReadUnitType(plate) then return end ReadCombatColor(plate) UpdateBarColor(plate) UpdateBorder(plate) UpdateNameColor(plate) UpdateLayout(plate) UpdateDebuffs(plate) end -- UNIT_FLAGS: the combat bit, and the reaction can move with it local function UpdateFlags(plate) local incombat = UnitAffectingCombat(plate.unit) and true or nil local changed = incombat ~= plate.incombat plate.incombat = incombat UpdateUnitType(plate) if changed then UpdateCombatColor(plate) UpdateNameColor(plate) end end -- UNIT_DYNAMIC_FLAGS: tapped by someone else greys the bar local function UpdateTapped(plate) local unit = plate.unit local tapped = UnitIsTapped(unit) and not UnitIsTappedByPlayer(unit) and true or nil if tapped == plate.tapped then return end plate.tapped = tapped UpdateBarColor(plate) end function UpdateBarColor(plate) local unittype = plate.unittype local r, g, b, a = unpack(unitcolors[unittype]) if plate.class and cfg.classcolor[unittype] then r, g, b, a = PFUI_CLASS_COLORS[plate.class]:GetRGBA() end if plate.tapped then r, g, b, a = .5, .5, .5, .8 end if cfg.barcombatstate and plate.combatColor then local c = plate.combatColor r, g, b, a = c.r, c.g, c.b, c.a end local cache = plate.cache if r == cache.r and g == cache.g and b == cache.b then return end cache.r, cache.g, cache.b = r, g, b plate.health:SetStatusBarColor(r, g, b, a) end function UpdateBorder(plate) local r, g, b, a if cfg.outcombatstate then local c = plate.combatColor or combatstate.NONE r, g, b, a = c.r, c.g, c.b, c.a elseif plate.istarget and cfg.targethighlight then r, g, b, a = cfg.hlr, cfg.hlg, cfg.hlb, cfg.hla elseif cfg.outline[plate.unittype] then r, g, b, a = unpack(unitcolors[plate.unittype]) else r, g, b, a = cfg.br, cfg.bg, cfg.bb, cfg.ba end local cache = plate.cache if r == cache.br and g == cache.bg and b == cache.bb then return end cache.br, cache.bg, cache.bb = r, g, b plate.health.backdrop:SetBackdropBorderColor(r, g, b, a) end -- Class colour for friendly players when friendclassnamec is on, the fight -- colour while the unit and the player are both in combat, white otherwise. function UpdateNameColor(plate) local r, g, b, a = 1, 1, 1, 1 if cfg.friendclassnamec and plate.unittype == "FRIENDLY_PLAYER" and plate.class then r, g, b, a = PFUI_CLASS_COLORS[plate.class]:GetRGBA() elseif cfg.namefightcolor and playerInCombat and plate.incombat then r, g, b, a = 1, .4, .2, 1 end local cache = plate.cache if r == cache.nr and g == cache.ng and b == cache.nb then return end cache.nr, cache.ng, cache.nb = r, g, b plate.name:SetTextColor(r, g, b, a) end local function abbrevname(t) return string.sub(t,1,1)..". " end local function GetNameString(name) if cfg.abbrevname and name and strlen(name) > 20 then -- first try to only abbreviate the first word name = string.gsub(name, "^(%S+) ", abbrevname) -- abbreviate all if it still doesn't fit if strlen(name) > 20 then name = string.gsub(name, "(%S+) ", abbrevname) end end return name end -- Name, and the guild or subname line under it. Returns true when the -- layout has to follow: the guild line moves the name-only anchors, and a -- totem swapped in place keeps its unit while its name and drop spell change. local function ReadName(plate) local unit = plate.unit local relayout local name = UnitName(unit) if name ~= plate.rawname then plate.rawname = name plate.name:SetText(GetNameString(name)) -- a player too high to show a level is looked up by name UpdateLevel(plate) if plate.creatureType == 11 then relayout = true end end local guild = cfg.showguildname and (plate.player and GetGuildInfo(unit) or UnitSubName(unit)) or nil if guild then if guild ~= plate.guildtext then plate.guildtext = guild plate.guild:SetText(guild) end local mine = guild == myGuild if mine ~= plate.guildmine then plate.guildmine = mine if mine then plate.guild:SetTextColor(0, 0.9, 0, 1) else plate.guild:SetTextColor(0.8, 0.8, 0.8, 1) end end end local shown = guild and true or nil if shown ~= plate.guildshown then plate.guildshown = shown relayout = true end return relayout end -- UNIT_NAME_UPDATE local function UpdateName(plate) if ReadName(plate) then UpdateLayout(plate) end end -- UNIT_LEVEL / UNIT_CLASSIFICATION_CHANGED / PLAYER_LEVEL_UP function UpdateLevel(plate) local unit = plate.unit local level = UnitLevel(unit) local elite = UnitClassification(unit) if level <= 0 then if plate.player then local _, dblevel = GetUnitInfo(plate.rawname) if dblevel and dblevel > 0 then level = dblevel end else -- UnitLevel answers -1 exactly where the engine shows the skull: a -- world boss, or a hostile ten or more levels above the player elite = "boss" end end plate.levelnum = level > 0 and level or nil if level <= 0 then level = "??" end local cache = plate.cache if cache.level == level and cache.elite == elite then return end cache.level, cache.elite = level, elite plate.level:SetFormattedText("%s%s", level, elitestrings[elite] or "") -- Blizzard's own tiers: adding a flat offset to the channels desaturates -- every tier toward white and collapses the boundaries if plate.levelnum then local color = GetDifficultyColor(plate.levelnum) plate.level:SetTextColor(color.r, color.g, color.b, 1) elseif plate.player then plate.level:SetTextColor(1, 1, 1, 1) else plate.level:SetTextColor(1, .1, .1, 1) end end local function ApplyHealth(plate) local hp, hpmax = plate.hp, plate.hpmax plate.health:SetMinMaxValues(0, hpmax) plate.health:SetValue(hp) if not cfg.showhp then return end local text = plate.health.text if hpmax <= 0 then text:SetText("") return end -- A unit outside the group reports its health as a percentage (max 100); -- libhealth may know the real numbers for a mob by name and level. local rhp, rhpmax, known if hpmax ~= 100 then rhp, rhpmax, known = hp, hpmax, true elseif pfUI.libhealth and pfUI.libhealth.enabled then rhp, rhpmax, known = pfUI.libhealth:GetUnitHealthByName(plate.rawname, plate.levelnum, hp, hpmax) end local perc = ceil(hp / hpmax * 100) local setting = cfg.hptextformat if not known then if setting == "deficit" then text:SetFormattedText("-%d%%", 100 - perc) else text:SetFormattedText("%d%%", perc) end elseif setting == "curperc" then text:SetFormattedText("%s | %d%%", Abbreviate(rhp), perc) elseif setting == "cur" then text:SetText(Abbreviate(rhp)) elseif setting == "curmax" then text:SetFormattedText("%s - %s", Abbreviate(rhp), Abbreviate(rhpmax)) elseif setting == "curmaxs" then text:SetFormattedText("%s / %s", Abbreviate(rhp), Abbreviate(rhpmax)) elseif setting == "curmaxperc" then text:SetFormattedText("%s - %s | %d%%", Abbreviate(rhp), Abbreviate(rhpmax), perc) elseif setting == "curmaxpercs" then text:SetFormattedText("%s / %s | %d%%", Abbreviate(rhp), Abbreviate(rhpmax), perc) elseif setting == "deficit" then text:SetFormattedText("-%s", Abbreviate(rhpmax - rhp)) else -- "percent" as fallback text:SetFormattedText("%d%%", perc) end end -- Bar and text. Returns true when full health flipped, which can hide or -- show the bar. local function ReadHealth(plate) local unit = plate.unit local hp, hpmax = UnitHealth(unit), UnitHealthMax(unit) if hp == plate.hp and hpmax == plate.hpmax then return nil end plate.hp, plate.hpmax = hp, hpmax ApplyHealth(plate) local fullhp = hp == hpmax if fullhp == plate.fullhp then return nil end plate.fullhp = fullhp return true end -- UNIT_HEALTH / UNIT_MAXHEALTH local function UpdateHealth(plate) if ReadHealth(plate) then UpdateLayout(plate) end end -- true when the bar should be hidden, leaving name and level local function HidePlate(plate) -- keep some plates always visible according to config if cfg.fullhealth and not plate.fullhp then return nil end if cfg.keeptarget and plate.istarget then return nil end if cfg.hide[plate.unittype] then return true end if cfg.critters and CreatureType(plate) == 8 then return true end if cfg.totems and CreatureType(plate) == 11 then return true end return nil end local function UpdateGlow(plate) plate.glow:SetShown(plate.istarget and cfg.targetglow and plate.mode ~= "totem") end -- Which of the three looks the plate wears: the totem icon alone, name and -- level without a bar, or the full plate. Re-anchors only when the look -- changes. function UpdateLayout(plate) local mode, icon if cfg.totemicons and CreatureType(plate) == 11 then -- 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. local spellId = UnitCreatedBySpell(plate.unit) if spellId ~= plate.totemSpell then plate.totemSpell = spellId plate.totemIcon = spellId and C_Spell.GetSpellTexture(spellId) or nil end icon = plate.totemIcon end if icon then mode = "totem" if plate.totemShown ~= icon then plate.totemShown = icon plate.totem.icon:SetTexture(icon) end elseif HidePlate(plate) then mode = "name" else mode = "bar" end plate.mode = mode local key = plate.guildshown and mode .. "+guild" or mode if plate.layout ~= key then plate.layout = key local font_size = cfg.font_size if mode == "totem" then plate.level:Hide() plate.name:Hide() plate.health:Hide() plate.guild:Hide() plate.totem:Show() elseif mode == "name" 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.glow:SetPoint("CENTER", plate.name, "CENTER", 0, plate.guildshown and -(font_size / 2) - 2 or 0) plate.level:Show() plate.name:Show() plate.health:Hide() plate.guild:SetShown(plate.guildshown) 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.glow:SetPoint("CENTER", plate.health, "CENTER", 0, 0) plate.level:Show() plate.name:Show() plate.health:Show() plate.guild:SetShown(plate.guildshown) plate.totem:Hide() end end UpdateGlow(plate) end local function CreateDebuffIcon(plate, index) local icon = CreateFrame("Frame", plate.platename.."Debuff"..index, plate) icon:Hide() icon:SetFrameLevel(4) icon.icon = icon:CreateTexture(nil, "BACKGROUND") icon.icon:SetColorTexture(.3,1,.8,1) icon.icon:SetAllPoints(icon) icon.stacks = icon:CreateFontString(nil, "OVERLAY") icon.stacks:SetAllPoints(icon) icon.stacks:SetJustifyH("RIGHT") icon.stacks:SetJustifyV("BOTTOM") icon.stacks:SetTextColor(1,1,0) icon.cd = CreateCooldown(icon, "nameplate", plate.platename.."Debuff"..index.."Cooldown") icon.cd:SetFrameLevel(6) plate.debuffs[index] = icon 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) end -- UNIT_AURA_DELTA: one GetAuraSlots enumeration, then positional by-slot -- reads straight onto the icons. The scan allocates nothing. function UpdateDebuffs(plate) local index = 1 local friendly = plate.friendly if cfg.showdebuffs and (friendly and cfg.showdebuffs_friendly or (not friendly and cfg.showdebuffs_hostile)) then local unit = plate.unit local now = GetTime() local n = ScanAuraSlots(unit, cfg.debufffilter, auraSlots, 16) for i = 1, n do local effect, texture, stacks, _, duration, expirationTime = C_UnitAuras.UnitAuraBySlot(unit, auraSlots[i]) if not effect then break end if texture and DebuffFilter(effect) then local icon = plate.debuffs[index] if not icon then CreateDebuffIcon(plate, index) UpdateDebuffConfig(plate, index) icon = plate.debuffs[index] end icon:Show() icon.icon:SetTexture(texture) icon.icon:SetTexCoord(.078, .92, .079, .937) if stacks and stacks > 1 and cfg.showstacks then icon.stacks:SetText(stacks) icon.stacks:Show() else icon.stacks:Hide() end local timeleft = expirationTime and expirationTime > 0 and (expirationTime - now) if duration and timeleft 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 local start = now + timeleft - effDuration icon.cd:Show() if icon.cdEffect ~= effect or abs(icon.cdStart - start) > 0.5 then CooldownFrame_SetTimer(icon.cd, start, effDuration, 1) icon.cdEffect = effect icon.cdStart = start end else icon.cd:Hide() icon.cdEffect = nil end index = index + 1 if index > 16 then break end end end end -- hide remaining debuffs for i = index, table.getn(plate.debuffs) do plate.debuffs[i]:Hide() end end local function UpdateCombo(plate) local cp = plate.istarget and cfg.cpdisplay and GetComboPoints("target") or 0 if plate.cpShown == cp then return end plate.cpShown = cp for i = 1, 5 do plate.combopoints[i]:SetShown(i <= cp) end end local function UpdateAlpha(plate) local alpha = (plate.istarget or not targetGuid) and 1 or cfg.notargalpha if plate.alpha == alpha then return end plate.alpha = alpha plate:SetAlpha(alpha) end local function UpdateStrata(plate) local strata = plate.istarget and "LOW" or "BACKGROUND" if plate.strata == strata then return end plate.strata = strata plate:SetFrameStrata(strata) end -- ============================================================================ -- Animation driver: castbar sweeps and the target zoom. Shown only while -- something moves. -- ============================================================================ local driver = CreateFrame("Frame", nil, UIParent) driver:Hide() -- f = 0 is the configured bar, f = 1 the zoomed one function SetZoom(plate, f) plate.zoomf = f plate.health:SetSize(cfg.width * (1 + (cfg.zoomval - 1) * f), cfg.heighthealth * (1 + (cfg.zoomval * .9 - 1) * f)) end -- Continues from wherever the bar is, so a target swap mid-zoom doesn't jump. local function StartZoom(plate, zoomIn) local f = plate.zoomf or 0 if (zoomIn and f >= 1) or (not zoomIn and f <= 0) then return end plate.zoomIn = zoomIn plate.zoomStart = GetTime() - (zoomIn and f or 1 - f) * ZOOM_TIME zooming[plate] = true driver:Show() end -- 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 below. local function SetCastbarText(castbar, remaining) local rounded if cfg.castbardecimals 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 local function UpdateCastbar(plate, info, now) local castbar = plate.castbar local duration = info.duration if castbar.lastEndTime ~= info.endTime then castbar.lastEndTime = info.endTime castbar.lastTextTick = nil -- Relative 0..duration range to avoid float precision loss with large -- absolute timestamps. castbar:SetMinMaxValues(0, duration) SetCastbarShield(castbar, castbar.icon, info.noInterrupt, info.isChannel) if info.icon then castbar.icon.tex:SetTexture(info.icon) castbar.icon.tex:SetTexCoord(.1,.9,.1,.9) end castbar.spell:SetText(cfg.spellname and info.spellName or "") end local value = info.isChannel and (info.endTime - now) or (now - info.startTime) if value < 0 then value = 0 elseif value > duration then value = duration end castbar:SetValue(value) SetCastbarText(castbar, info.endTime - now) if not castbar.isShown then castbar.isShown = true castbar:Show() end end function HideCast(plate) activeCasts[plate] = nil local castbar = plate.castbar if castbar.isShown then castbar.isShown = nil castbar.lastEndTime = nil castbar:Hide() end end -- Start or stop the plate's castbar from the cast cache and the config. function UpdateCast(plate) local info = castState[plate.guid] if info and cfg.showcastbar and (plate.istarget or not cfg.targetcastbar) then if not activeCasts[plate] then activeCasts[plate] = true plate.casttick = 0 driver:Show() end UpdateCastbar(plate, info, GetTime()) else HideCast(plate) end end driver:SetScript("OnUpdate", function() local now = GetTime() -- The target's bar sweeps every frame; the others at the castbar rate. for plate in pairs(activeCasts) do if plate.istarget or plate.casttick + cfg.throttle_castbar <= now then plate.casttick = now local info = castState[plate.guid] if info and info.endTime > now then UpdateCastbar(plate, info, now) else -- ran out without a STOP: treat it as one castState[plate.guid] = nil HideCast(plate) UpdateCombatColor(plate) end end end for plate in pairs(zooming) do local f = (now - plate.zoomStart) / ZOOM_TIME if f >= 1 then f = 1 zooming[plate] = nil end SetZoom(plate, plate.zoomIn and f or 1 - f) end if not next(activeCasts) and not next(zooming) then this:Hide() end end) -- ============================================================================ -- Cast events (per plate) -- ============================================================================ local function OnCastStart(plate) -- the payload carries no timing, so poll it (PollCastInfo picks cast vs -- channel) castState[plate.guid] = PollCastInfo(plate.unit) UpdateCast(plate) UpdateCombatColor(plate) end local function OnCastStop(plate) -- ended: natural, interrupted or cancelled -- the poll fires STOP for all if not castState[plate.guid] then return end castState[plate.guid] = nil HideCast(plate) UpdateCombatColor(plate) end -- ============================================================================ -- Target -- ============================================================================ local function SetTarget(plate, istarget) plate.istarget = istarget UpdateStrata(plate) UpdateBorder(plate) UpdateLayout(plate) UpdateCombo(plate) UpdateCast(plate) if cfg.targetzoom then StartZoom(plate, istarget) end end -- ============================================================================ -- Binding a plate to a unit -- ============================================================================ -- Every unit event the plate listens to, on its own frame against its token. local PLATE_EVENTS = { "UNIT_HEALTH", "UNIT_MAXHEALTH", "UNIT_AURA_DELTA", "UNIT_FLAGS", "UNIT_DYNAMIC_FLAGS", "UNIT_FACTION", "UNIT_LEVEL", "UNIT_CLASSIFICATION_CHANGED", "UNIT_NAME_UPDATE", "UNIT_TARGET", "UNIT_SPELLCAST_START", "UNIT_SPELLCAST_CHANNEL_START", "UNIT_SPELLCAST_STOP", "UNIT_SPELLCAST_CHANNEL_STOP", } local NUM_PLATE_EVENTS = table.getn(PLATE_EVENTS) local plateEvents = { UNIT_HEALTH = UpdateHealth, UNIT_MAXHEALTH = UpdateHealth, UNIT_AURA_DELTA = UpdateDebuffs, UNIT_FLAGS = UpdateFlags, UNIT_DYNAMIC_FLAGS = UpdateTapped, UNIT_FACTION = UpdateUnitType, UNIT_LEVEL = UpdateLevel, UNIT_CLASSIFICATION_CHANGED = UpdateLevel, UNIT_NAME_UPDATE = UpdateName, UNIT_TARGET = UpdateCombatColor, UNIT_SPELLCAST_START = OnCastStart, UNIT_SPELLCAST_CHANNEL_START = OnCastStart, UNIT_SPELLCAST_STOP = OnCastStop, UNIT_SPELLCAST_CHANNEL_STOP = OnCastStop, } local function OnPlateEvent() local handler = plateEvents[event] if handler then handler(this) end end -- State the previous occupant left behind local PER_UNIT = { "layout", "mode", "creatureType", "totemSpell", "totemIcon", "totemShown", "hp", "hpmax", "fullhp", "rawname", "levelnum", "guildtext", "guildshown", "guildmine", "unittype", "class", "player", "friendly", "combatColor", "incombat", "tapped", } local NUM_PER_UNIT = table.getn(PER_UNIT) -- Everything, from the unit. Bind and config changes. Reads first, then one -- pass over the visuals. local function Refresh(plate) local unit = plate.unit plate.incombat = UnitAffectingCombat(unit) and true or nil plate.tapped = UnitIsTapped(unit) and not UnitIsTappedByPlayer(unit) and true or nil ReadUnitType(plate) ReadCombatColor(plate) ReadName(plate) ReadHealth(plate) UpdateBarColor(plate) UpdateBorder(plate) UpdateNameColor(plate) UpdateLayout(plate) UpdateDebuffs(plate) UpdateCombo(plate) UpdateStrata(plate) UpdateAlpha(plate) UpdateCast(plate) if cfg.targetzoom and plate.istarget then StartZoom(plate, true) end end function Unbind(unit) local plate = plateByUnit[unit] if not plate then return end local guid = plate.guid plateByUnit[unit] = nil if plateByGuid[guid] == plate then plateByGuid[guid] = nil end castState[guid] = nil threatMemory[guid] = 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:UnregisterAllEvents() HideCast(plate) zooming[plate] = nil if plate.zoomf and plate.zoomf > 0 then SetZoom(plate, 0) end plate.unit = nil plate.guid = nil plate.targetToken = nil plate.istarget = nil end local function Bind(unit) local parent = C_NamePlate.GetNamePlateForUnit(unit) local plate = parent and parent.nameplate if not plate then return end local guid = UnitGUID(unit) if not guid then return end -- ADDED can precede the REMOVED of this frame's or this unit's previous -- binding within one tick if plate.unit then Unbind(plate.unit) end local old = plateByGuid[guid] if old then Unbind(old.unit) end plate.unit = unit plate.guid = guid plate.targetToken = unit .. "target" plate.istarget = guid == targetGuid plateByGuid[guid] = plate plateByUnit[unit] = plate table.wipe(plate.cache) for i = 1, NUM_PER_UNIT do plate[PER_UNIT[i]] = nil end for i = 1, table.getn(plate.debuffs) do plate.debuffs[i].cdEffect = nil end -- 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. for i = 1, NUM_PLATE_EVENTS do plate:RegisterUnitEvent(PLATE_EVENTS[i], unit) end -- the unit may already be mid-cast (its START fired before this plate -- existed); ongoing casts arrive via the event castState[guid] = PollCastInfo(unit) Refresh(plate) end -- ============================================================================ -- Engine frame: size and click routing -- ============================================================================ -- The engine stores a default size into the plate at every bind and shows it -- afterwards, so OnShow is the moment to put ours back. Overlap mode shrinks -- the engine frame to a point so the engine's own spacing stops pushing the -- plates apart; otherwise it matches the overlay so it stays the click target. local function ApplyFrameLayout(parent) local plate = parent.nameplate if cfg.overlap then parent:SetSize(1, 1) else local scale = UIParent:GetScale() parent:SetSize(plate:GetWidth() * scale, plate:GetHeight() * scale) end parent:EnableMouse(cfg.clickable and not cfg.useoverlay) plate:EnableMouse(cfg.clickable and cfg.useoverlay and not mouseBlocked) end local function OnEngineShow() if this.nameplate then ApplyFrameLayout(this) end end -- CURRENT_SPELL_CAST_CHANGED: while a spell waits for a ground target the -- engine's own plate lets the click through; the overlay has to step aside. local function UpdateSpellTargeting() if not (cfg.useoverlay and cfg.clickable) then return end local blocked = SpellIsTargeting() and true or nil if blocked == mouseBlocked then return end mouseBlocked = blocked for _, plate in pairs(plateByGuid) do plate:EnableMouse(not blocked) end end local function DisableObject(object) if not object then return end if not object.GetObjectType then return end local otype = object:GetObjectType() if otype == "Texture" then object:SetTexture("") object:SetTexCoord(0, 0, 0, 0) elseif otype == "FontString" then object:SetWidth(0.001) elseif otype == "StatusBar" then object:SetStatusBarTexture("") end end local function OverlayClick() this.parent:Click() end -- create nameplate core local nameplates = CreateFrame("Frame", "pfNameplates", UIParent) nameplates.OnCreate = function(parent) platecount = platecount + 1 local platename = "pfNamePlate" .. platecount -- create pfUI nameplate overlay local nameplate = CreateFrame("Button", platename, parent) nameplate.platename = platename nameplate:EnableMouse(false) nameplate.parent = parent nameplate.cache = {} -- Each plate watches its own unit. With RegisterUnitEvent the token IS the -- subscription: the event arrives only at the plate it concerns, and -- `this` is already that plate. Bind points the registration at the new -- token; Unbind drops it. nameplate:SetScript("OnEvent", OnPlateEvent) -- Blank the engine's drawing. Its regions come in creation order: border, -- glow, name, level, skull, raid icon -- the raid icon is the one kept. DisableObject(parent:GetChildren()) for i, object in pairs({parent:GetRegions()}) do if i == 6 then nameplate.raidicon = object else DisableObject(object) end end -- 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(nil, "OVERLAY") combopoints[i].tex:SetAllPoints() if i < 3 then combopoints[i].tex:SetColorTexture(1, .3, .3, .75) elseif i < 4 then combopoints[i].tex:SetColorTexture(1, 1, .3, .75) else combopoints[i].tex:SetColorTexture(.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 cfg.debuffbottom then nameplate.debuffs[1]:SetPoint("TOPLEFT", this, "BOTTOMLEFT", 0, -4) end end) castbar:SetScript("OnHide", function() if cfg.debuffbottom 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 parent.nameplate = nameplate parent:HookScript("OnShow", OnEngineShow) nameplates.OnConfigChange(parent) end nameplates.OnConfigChange = function(parent) local nameplate = parent.nameplate local font = C.nameplates.use_unitfonts == "1" and pfUI.font_unit or pfUI.font_default local font_size = cfg.font_size local font_style = C.nameplates.name.fontstyle local glowr, glowg, glowb, glowa = GetStringColor(C.nameplates.glowcolor) local hptexture = pfUI.media[C.nameplates.healthtexture] local _, default_border = GetBorderSize("nameplates") local plate_width = C.nameplates.width + 50 local plate_height = C.nameplates.heighthealth + font_size + 5 local combo_size = 5 local healthoffset = tonumber(C.nameplates.health.offset) local orientation = C.nameplates.verticalhealth == "1" and "VERTICAL" or "HORIZONTAL" nameplate:SetSize(plate_width, plate_height) nameplate:SetPoint("TOP", parent, "TOP", 0, tonumber(C.nameplates.vertical_offset) or 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) nameplate.health:SetRotatesTexture(orientation == "VERTICAL") -- 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) zooming[nameplate] = nil SetZoom(nameplate, 0) CreateBackdrop(nameplate.health, default_border) nameplate.health.text:SetFont(font, font_size - 2, "OUTLINE") nameplate.health.text:SetJustifyH(C.nameplates.hptextpos) if not cfg.showhp then nameplate.health.text:SetText("") end 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) -- the anchors above are re-applied by the next layout pass, and the -- colour caches no longer describe what is on screen nameplate.layout = nil table.wipe(nameplate.cache) -- click routing: the overlay forwards to the engine plate when it is the -- one under the mouse; right mouse-look on whichever takes the clicks local clicker = cfg.useoverlay and nameplate or parent local other = cfg.useoverlay and parent or nameplate nameplate:SetScript("OnClick", cfg.useoverlay and OverlayClick or nil) clicker:SetScript("OnMouseDown", cfg.rightclick and nameplates.mouselook.OnMouseDown or nil) other:SetScript("OnMouseDown", nil) ApplyFrameLayout(parent) end -- ============================================================================ -- Nameplate visibility switches -- ============================================================================ 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 (friendly) if C.nameplates["showfriendly"] == "1" then _G.FRIENDNAMEPLATES_ON = true ShowFriendNameplates() else _G.FRIENDNAMEPLATES_ON = nil HideFriendNameplates() end end nameplates:SetGameVariables() -- Handle friendly zone nameplate disable feature local function UpdateZone() local disableHostile = C.nameplates["disable_hostile_in_friendly"] == "1" local disableFriendly = C.nameplates["disable_friendly_in_friendly"] == "1" if not (disableHostile or disableFriendly) then return end local nowFriendly = GetZonePVPInfo() == "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 nameplates.UpdateConfig = function() CacheConfig() -- 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 nowFriendly = GetZonePVPInfo() == "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() elseif savedHostileState == "1" then -- Restore hostile if option is off but we're in friendly zone _G.NAMEPLATES_ON = true ShowNameplates() end if disableFriendly then _G.FRIENDNAMEPLATES_ON = nil HideFriendNameplates() elseif savedFriendlyState == "1" then -- Restore friendly if option is off but we're in friendly zone _G.FRIENDNAMEPLATES_ON = true ShowFriendNameplates() end else if 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 end -- update nameplate visibility nameplates:SetGameVariables() end -- apply all config changes for parent in pairs(registry) do nameplates.OnConfigChange(parent) local plate = parent.nameplate if plate.unit then -- the config decides what the plate shows, so read the unit again for i = 1, NUM_PER_UNIT do plate[PER_UNIT[i]] = nil end Refresh(plate) end end end -- ============================================================================ -- Module events -- ============================================================================ local events = {} events.PLAYER_LOGOUT = function() -- Stop event handling during logout to prevent crash 132. The plates hold -- their own unit subscriptions, so silencing this frame alone would leave -- them dispatching through logout. Leaving the world destroys the units -- before PLAYER_LOGOUT fires, and a destroyed plate already dropped its -- events and lost its methods, so only the live ones need silencing. nameplates:UnregisterAllEvents() nameplates:SetScript("OnEvent", nil) driver:SetScript("OnUpdate", nil) for parent in pairs(registry) do local plate = parent.nameplate if plate and plate.UnregisterAllEvents then plate:UnregisterAllEvents() plate:SetScript("OnEvent", nil) end end end events.PLAYER_ENTERING_WORLD = function() CacheConfig() nameplates:SetGameVariables() RebuildRaidGuidCache() targetGuid = UnitGUID("target") playerInCombat = UnitAffectingCombat("player") and true or nil myGuild = GetGuildInfo("player") UpdateZone() end events.ZONE_CHANGED_NEW_AREA = UpdateZone events.PLAYER_GUILD_UPDATE = function(unit) if unit ~= "player" then return end myGuild = GetGuildInfo("player") for _, plate in pairs(plateByGuid) do UpdateName(plate) end end events.RAID_ROSTER_UPDATE = function() RebuildRaidGuidCache() -- the offtank list reads the group by name for _, plate in pairs(plateByGuid) do UpdateCombatColor(plate) end end events.PARTY_MEMBERS_CHANGED = events.RAID_ROSTER_UPDATE events.NAME_PLATE_CREATED = function(frame) -- arg1 = the nameplate Frame; build the pfUI overlay once per pool slot if frame and not registry[frame] then nameplates.OnCreate(frame) registry[frame] = true end end events.NAME_PLATE_UNIT_ADDED = Bind events.NAME_PLATE_UNIT_REMOVED = Unbind events.PLAYER_TARGET_CHANGED = function() local old = targetGuid targetGuid = UnitGUID("target") if old == targetGuid then return end local oldplate = old and plateByGuid[old] local newplate = targetGuid and plateByGuid[targetGuid] if oldplate then SetTarget(oldplate, nil) end if newplate then SetTarget(newplate, true) end -- having a target at all dims every other plate if (old == nil) ~= (targetGuid == nil) then for _, plate in pairs(plateByGuid) do UpdateAlpha(plate) end else if oldplate then UpdateAlpha(oldplate) end if newplate then UpdateAlpha(newplate) end end end events.PLAYER_COMBO_POINTS = function() local plate = targetGuid and plateByGuid[targetGuid] if plate then UpdateCombo(plate) end end events.UNIT_COMBO_POINTS = events.PLAYER_COMBO_POINTS events.PLAYER_REGEN_DISABLED = function() playerInCombat = true for _, plate in pairs(plateByGuid) do UpdateCombatColor(plate) UpdateNameColor(plate) end end events.PLAYER_REGEN_ENABLED = function() playerInCombat = nil table.wipe(threatMemory) for _, plate in pairs(plateByGuid) do UpdateCombatColor(plate) UpdateNameColor(plate) end end events.PLAYER_LEVEL_UP = function() -- difficulty colours, and the skull threshold, move with the player for _, plate in pairs(plateByGuid) do plate.cache.level = nil UpdateLevel(plate) end end events.CURRENT_SPELL_CAST_CHANGED = UpdateSpellTargeting -- The interruptible flips only fire for target and focus, not nameplate -- tokens. castState is keyed by GUID, so they still reach those units' plates. events.UNIT_SPELLCAST_INTERRUPTIBLE = function(unit) local guid = UnitGUID(unit) local info = guid and castState[guid] local plate = info and plateByGuid[guid] if not plate then return end info.noInterrupt = event == "UNIT_SPELLCAST_NOT_INTERRUPTIBLE" -- UpdateCastbar only restyles on a new cast; force it plate.castbar.lastEndTime = nil UpdateCast(plate) end events.UNIT_SPELLCAST_NOT_INTERRUPTIBLE = events.UNIT_SPELLCAST_INTERRUPTIBLE -- auto-attack state, for the right-click emulation below events.PLAYER_ENTER_COMBAT = function() nameplates.autoattack = true if PlayerFrame then PlayerFrame.inCombat = 1 end end events.PLAYER_LEAVE_COMBAT = function() nameplates.autoattack = nil if PlayerFrame then PlayerFrame.inCombat = nil end end for name in pairs(events) do if name == "UNIT_COMBO_POINTS" then nameplates:RegisterUnitEvent(name, "player") elseif name == "UNIT_SPELLCAST_INTERRUPTIBLE" or name == "UNIT_SPELLCAST_NOT_INTERRUPTIBLE" then nameplates:RegisterUnitEvent(name, "target", "focus") else nameplates:RegisterEvent(name) end end nameplates:SetScript("OnEvent", function() events[event](arg1) end) -- ============================================================================ -- Right-click mouselook -- ============================================================================ nameplates.mouselook = {} 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 end end -- releasing the button ends the mouselook. GLOBAL_MOUSE_UP fires for every -- release, while PLAYER_STOPPED_LOOKING needs the camera to have moved, and -- a click doesn't move it. pfUI.events:RegisterFrameEventAndCallback("GLOBAL_MOUSE_UP", function(_, button) local mouselook = nameplates.mouselook if button ~= "RightButton" or not mouselook.frame then return end -- past the threshold (0.5 second) it was a drag, so no click action follows if mouselook.time + tonumber(C.nameplates["clickthreshold"]) >= GetTime() then -- run a usual nameplate rightclick action mouselook.frame:Click("LeftButton") if UnitCanAttack("player", "target") and not nameplates.autoattack then AttackTarget() end end mouselook.frame = nil end) pfUI.nameplates = nameplates end)