mirror of
https://github.com/brues-code/pfUI.git
synced 2026-11-02 17:06:57 +00:00
The overlay inherits the engine plate's world stratum, below every UI stratum. Moving it to BACKGROUND/LOW put it level-for-level against the chat panels, so plates drew over the chat. The target now rises above the other plates by raising its engine frame and re-parenting the overlay, which carries the subtree's levels along. Debuff icons take their level from the health bar so ones grown on a raised plate stay above it.
1718 lines
62 KiB
Lua
1718 lines
62 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 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()
|
|
-- relative: a raised target plate can grow icons too
|
|
icon:SetFrameLevel(plate.health:GetFrameLevel())
|
|
|
|
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(icon:GetFrameLevel() + 2)
|
|
|
|
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
|
|
|
|
-- The engine's plates draw in the world frame's stratum, under every UI
|
|
-- stratum, and the overlay inherits it; a SetFrameStrata would lift it into
|
|
-- the UI's and over the chat. The target rises above the other plates by
|
|
-- level instead: raise the engine frame, then re-parent the overlay, which
|
|
-- moves its whole subtree to the new parent level.
|
|
local TARGET_RAISE = 20
|
|
|
|
local function UpdateStacking(plate)
|
|
local raised = plate.istarget and true or nil
|
|
if plate.raised == raised then return end
|
|
plate.raised = raised
|
|
local parent = plate.parent
|
|
parent:SetFrameLevel(plate.baselevel + (raised and TARGET_RAISE or 0))
|
|
-- SetParent does nothing for the same parent, so it goes through nil
|
|
plate:SetParent(nil)
|
|
plate:SetParent(parent)
|
|
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
|
|
UpdateStacking(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)
|
|
UpdateStacking(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.baselevel = parent:GetFrameLevel()
|
|
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)
|