mirror of
https://github.com/brues-code/pfUI.git
synced 2026-09-24 08:36:04 +00:00
6b458c9e27
* Migrate player castbar to ClassicAPI UNIT_SPELLCAST_* events
Drive the player cast path off ClassicAPI's synthesized, player-only
UNIT_SPELLCAST_* events instead of the vanilla SPELLCAST_* plus nampower
SPELL_{START,GO,DELAYED}_SELF mix. START/CHANNEL_START poll UnitCastingInfo/
UnitChannelInfo and stamp; STOP/CHANNEL_STOP/FAILED/INTERRUPTED clear;
SUCCEEDED drives tradeskill craft counting; DELAYED/CHANNEL_UPDATE re-poll.
Since UNIT_SPELLCAST_START fires for chained same-spell recasts, the nampower
SELF deps and their RunNextFrame co-hook-ordering workarounds are gone.
Remote target/focus bars keep SPELL_*_OTHER (the new events are player-only).
Fix the cast-start flash: StampBar now primes the fill on the stamp frame,
and ClearBar no longer snaps the bar to full (that snap lingered in the fade
tail and flashed when the next cast stamped).
Add a red flash on a cancelled cast: new appearance.castbar.failcolor,
flashed by ClearBar only when a cast was actually in progress.
Handle pushback Quartz-style: DELAYED/CHANNEL_UPDATE re-poll just the times
and accumulate the endMs shift into this.delay for the +/- indicator (cast
+X, channel -X), rather than a full restamp.
* Route target/focus castbars through remote UNIT_SPELLCAST_*
ClassicAPI now fires UNIT_SPELLCAST_* for remote tokens (target/focus/...)
via PollRemote, so drop the nampower SPELL_*_OTHER path and drive all three
bars off one event model. The gate is now token-based (arg1 == this.unitstr,
plus the arg1=="player" + UnitIsUnit case for target=self); the RunNextFrame
defer is gone since the remote poll fires after UnitCastingInfo is readable.
Cast detection is now nampower-free across every bar.
Use the event's rank payload (arg5): thread it through RefreshBar into
StampBar, which now only calls C_Spell.GetSpellSubtext as a fallback on the
retarget re-poll (PLAYER_TARGET/FOCUS_CHANGED) where no event is in hand.
* Drive nameplate castbars off UNIT_SPELLCAST_*
ClassicAPI now fires UNIT_SPELLCAST_* for nameplate tokens, so populate the
per-GUID castState cache from UNIT_SPELLCAST_{START,CHANNEL_START} (poll
PollCastInfo for the timing the payload omits) and clear it on
{STOP,CHANNEL_STOP} -- the remote poll fires STOP for natural end, interrupt,
and cancel alike. Replaces the nampower SPELL_{START,FAILED}_OTHER path; the
^nameplate arg1 gate ignores the target/focus/party fires of the same events,
and the NAME_PLATE_UNIT_ADDED seed still catches a plate spawning mid-cast.
Nameplate cast detection is now nampower-free.
* We no longer use these features from SuperWoW/Nampower
* Use secure unit attributes; remove mouseover scripts
Switch unitframes to attribute-driven clicks and remove mouseover tooltip handlers. Adds SetAttribute("unit") in UpdateVisibility and sets frame attributes (unit and type1='target') when creating frames. Removes OnEnter/OnLeave functions and their SetScript registrations and eliminates direct TargetUnit calls in ClickAction
* Use SetShown and simplify component default
Replace explicit Show/Hide conditionals in api/unitframes.lua with SetShown(...) for ressIcon, leaderIcon, lootIcon, pvpIcon, and restIcon to reduce branching and improve readability. Also remove the local shadowing of the parameter 'component' by using assignment (component = component or ""). No intended behavior changes.
* Drive unit-frame clicks via secure attributes
Replace the Lua OnClick dispatch (OnClick/ClickAction/RightClickAction) with
secure frame attributes: default type1=target / type2=menu, and EnableClickCast
now writes type/spell/macrotext/menu/target/focus attributes per button+modifier
instead of caching a clickactions table read at click time. Drops the now-dead
clickactions table and buttons list.
Also set a "unit" attribute on each frame (at creation and re-synced in
UpdateVisibility to the live token, so a party shown on the raid grid reports
partyN, not raidN) so the hovered unit resolves from the frame.
Match the target/focus/menu click keywords exactly rather than by prefix, so a
spell whose name starts with "focus"/"target"/"menu" (e.g. Focus Magic) casts
instead of being swallowed as the keyword action.
* Simplify marktracking colors and refresh per mark
Replace the hand-rolled ParseColor plus its default-color table with
GetStringColor -- the raidmarkercolor_* config keys already carry those same
defaults, so the fallback was dead code.
Restructure the refresh off observed mark tokens: UNIT_HEALTH/UNIT_MAXHEALTH
now arrive as arg1 == "markN", so refresh just that one row (UpdateRow) instead
of rescanning all eight on every nearby unit's health tick. A visibility flip
(into range / death / hp crossing 0) re-packs the rows, so UpdateRow hands off
to a full UpdateDisplay; RAID_TARGET_UPDATE / PLAYER_ENTERING_WORLD stay full
refreshes, with the 1s poll as the range-change backstop.
* not true anymore
* Poll marktracking fallback via C_Timer instead of OnUpdate
The 1s range-change safety net ran a per-frame OnUpdate that no-oped ~59 of
every 60 frames. Replace it with C_Timer.NewTicker(FALLBACK_INTERVAL,
UpdateDisplay) -- one wakeup per second off the shared timer driver -- and drop
the elapsed accumulator. The scanner frame is now purely event-driven.
* SuperWoW is now optional
* Update README feature list
Add 'Mouseover Unit Frames' and 'Click-casting' to the main features list. Update the SuperWoW entry to reflect that it tracks party/raid units on the minimap (replacing the prior SetMouseoverUnit note).
* Replace fixed-interval OnUpdate polls with C_Timer/RunNextFrame
Swap hand-rolled per-frame throttles for the modern timer primitives:
- turtle-wow: one-shot next-frame defer (self-hiding OnUpdate frame) -> RunNextFrame
- panel: clock, combat, and fps widgets -> NewTicker(1); guild roster -> NewTicker(60)
- minimap: coordinates text -> NewTicker(0.1)
- addonbuttons: one-shot init -> RunNextFrame; 5s button rescan -> NewTicker(5),
guarded by IsShown() to preserve the old "paused while hidden" behavior
Ticker callbacks reference frames via upvalue since 'this' is unbound outside
OnUpdate. Genuine per-frame work (bar fills, fades, drag) and polls already
coordinated through pfUI.throttle are left as-is.
* update pfQuest link
* Use SetShown/SetSize in panel module
Replace manual Show/Hide toggles with SetShown(not ... ) and replace SetWidth/SetHeight with SetSize in modules/panel.lua. Changes simplify toggle logic (timer, WorldMap, chat hide buttons) and unify sizing calls for timer, frames, textures, and microbutton. No behavior changes intended; purely refactor for conciseness and consistency.
* Bump ClassicAPI min version to 1.9.0
* Drive reagent counter from events instead of a polling OnUpdate
The pfReagentCounter frame ran an OnUpdate that (1) progressively rescanned
all 120 action slots one-per-100ms on any slot change and (2) recounted
reagent inventory on a 1s throttle. Replace both with direct event handling:
- ACTIONBAR_SLOT_CHANGED updates just the changed slot (arg1), full-scanning
only when arg1 is 0/nil, instead of restarting a ~12s rescan on every edit
- BAG_UPDATE_DELAYED recounts tracked reagents directly (it is already
Blizzard's coalesced bag event, so the extra 1s throttle was redundant)
- PLAYER_ENTERING_WORLD seeds the full reagent map once
UpdateSlot now seeds a new reagent's real count via GetItemCount so a freshly
placed reagent spell shows the correct number immediately. The updatecache ->
BarsUpdate render path and the IsReagentAction/GetReagentCount accessors are
unchanged.
1848 lines
72 KiB
Lua
1848 lines
72 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 UnitClass = UnitClass
|
|
local UnitLevel = UnitLevel
|
|
local UnitIsPlayer = UnitIsPlayer
|
|
local UnitIsDead = UnitIsDead
|
|
local UnitAffectingCombat = UnitAffectingCombat
|
|
local UnitIsUnit = UnitIsUnit
|
|
local UnitCanAssist = UnitCanAssist
|
|
local UnitHealth = UnitHealth
|
|
local UnitHealthMax = UnitHealthMax
|
|
local pairs = pairs
|
|
local tonumber = tonumber
|
|
local strlower = strlower
|
|
local strfind = strfind
|
|
local strlen = strlen
|
|
local floor = floor
|
|
local ceil = ceil
|
|
local abs = abs
|
|
local mathmod = math.mod
|
|
|
|
local unitcolors = {
|
|
["ENEMY_NPC"] = { .9, .2, .3, .8 },
|
|
["NEUTRAL_NPC"] = { 1, 1, .3, .8 },
|
|
["FRIENDLY_NPC"] = { .6, 1, 0, .8 },
|
|
["ENEMY_PLAYER"] = { .9, .2, .3, .8 },
|
|
["FRIENDLY_PLAYER"] = { .2, .6, 1, .8 }
|
|
}
|
|
|
|
local offtanks = {}
|
|
|
|
local combatstate = {
|
|
-- gets overwritten by user config
|
|
["OFFTANK"] = { r = .7, g = .4, b = .2, a = 1 },
|
|
["NOTHREAT"] = { r = .7, g = .7, b = .2, a = 1 },
|
|
["THREAT"] = { r = .7, g = .2, b = .2, a = 1 },
|
|
["CASTING"] = { r = .7, g = .2, b = .7, a = 1 },
|
|
["STUN"] = { r = .2, g = .7, b = .7, a = 1 },
|
|
["NONE"] = { r = .2, g = .2, b = .2, a = 1 },
|
|
}
|
|
|
|
local elitestrings = {
|
|
["elite"] = "+",
|
|
["rareelite"] = "R+",
|
|
["rare"] = "R",
|
|
["boss"] = "B"
|
|
}
|
|
|
|
-- Friendly zone nameplate disable state
|
|
local savedHostileState = nil
|
|
local savedFriendlyState = nil
|
|
local inFriendlyZone = false
|
|
local platecount = 0
|
|
local registry = {}
|
|
-- Subset of registry that currently has a unit assigned (between
|
|
-- NAME_PLATE_UNIT_ADDED and _REMOVED). The central loop iterates this instead
|
|
-- of the full pool so hidden pool slots aren't touched every tick.
|
|
local visiblePlates = {}
|
|
|
|
local raidGuidCache = {} -- guid -> name (rebuilt on RAID_ROSTER_UPDATE/PARTY_MEMBERS_CHANGED)
|
|
|
|
-- Per-GUID cast state, populated from ClassicAPI's UNIT_SPELLCAST_* events
|
|
-- (which now fire for nameplate tokens) and cleared on STOP / plate removal /
|
|
-- expiry. This replaces the old per-tick C_Spell poll on every visible plate:
|
|
-- cast detection is event driven, and GetCastInfo just reads this cache.
|
|
local castState = {}
|
|
-- guid -> nameplate, maintained on NAME_PLATE_UNIT_ADDED/_REMOVED so a cast
|
|
-- event can find its plate in O(1) and only cache casts we actually show.
|
|
local plateByGuid = {}
|
|
|
|
-- One-shot C_Spell poll. Builds the cast struct for a UNIT_SPELLCAST_* event
|
|
-- (the payload carries no timing) and seeds a plate that spawns while its unit
|
|
-- is already mid-cast (its START fired before the plate existed). Picks cast
|
|
-- vs channel itself. Never called per frame.
|
|
local function PollCastInfo(unit)
|
|
if not unit then return nil end
|
|
local name, _, texture, startMs, endMs, _, _, _, spellID = C_Spell.UnitCastingInfo(unit)
|
|
local isChannel
|
|
if not name then
|
|
name, _, texture, startMs, endMs, _, _, spellID = C_Spell.UnitChannelInfo(unit)
|
|
isChannel = true
|
|
end
|
|
if not name or not startMs or not endMs then return nil end
|
|
return {
|
|
spellName = name,
|
|
spellID = spellID,
|
|
icon = texture,
|
|
startTime = startMs / 1000,
|
|
endTime = endMs / 1000,
|
|
duration = (endMs - startMs) / 1000,
|
|
isChannel = isChannel,
|
|
}
|
|
end
|
|
|
|
-- Read a unit's current cast from the event-driven cache (keyed by GUID).
|
|
-- Returns the cached struct while the cast is still active, else nil (and
|
|
-- prunes the expired entry). Same struct shape and callers as before, minus
|
|
-- the per-tick poll.
|
|
local function GetCastInfo(unit)
|
|
if not unit then return nil end
|
|
local guid = UnitGUID(unit)
|
|
if not guid then return nil end
|
|
local info = castState[guid]
|
|
if info and info.endTime > GetTime() then return info end
|
|
if info then castState[guid] = nil end
|
|
return nil
|
|
end
|
|
|
|
local debuffCache = {} -- guid -> { [spellID] = { start, duration } }
|
|
-- Reusable per-plate debuff display buffer (avoid GC churn from per-call table creation)
|
|
local debuffDisplayBuf = {} -- [i] = { effect, texture, stacks, dtype, duration, timeleft }
|
|
for i = 1, 16 do debuffDisplayBuf[i] = {} end
|
|
local threatMemory = {} -- guid -> true if mob had player targeted
|
|
-- local debuffSeen = {} -- reusable table for debuff tracking (avoid GC churn)
|
|
|
|
-- PERF: visiblePlateCount maintained event-driven (NAME_PLATE_UNIT_ADDED/_REMOVED)
|
|
local visiblePlateCount = 0
|
|
|
|
-- ============================================================================
|
|
-- OPTIMIZATION: Config caching
|
|
-- ============================================================================
|
|
local cfg = {}
|
|
local function CacheConfig()
|
|
cfg.showcastbar = C.nameplates["showcastbar"] == "1"
|
|
cfg.targetcastbar = C.nameplates["targetcastbar"] == "1"
|
|
cfg.notargalpha = tonumber(C.nameplates.notargalpha) or 0.5
|
|
if cfg.notargalpha > 1 then cfg.notargalpha = cfg.notargalpha / 100 end
|
|
-- Clamp to 0.99 so non-target plates never reach 1.0 (used for target detection)
|
|
if cfg.notargalpha > 0.99 then cfg.notargalpha = 0.99 end
|
|
cfg.namefightcolor = C.nameplates.namefightcolor == "1"
|
|
cfg.spellname = C.nameplates.spellname == "1"
|
|
cfg.showhp = C.nameplates.showhp == "1"
|
|
cfg.showdebuffs = C.nameplates["showdebuffs"] == "1"
|
|
cfg.showdebuffs_hostile = C.nameplates["showdebuffs_hostile"] == "1"
|
|
cfg.showdebuffs_friendly = C.nameplates["showdebuffs_friendly"] == "1"
|
|
cfg.owndebuffs = C.nameplates["owndebuffs"] == "1"
|
|
cfg.targetzoom = C.nameplates.targetzoom == "1"
|
|
cfg.zoomval = (tonumber(C.nameplates.targetzoomval) or 0.4) + 1
|
|
cfg.width = tonumber(C.nameplates.width) or 120
|
|
cfg.heighthealth = tonumber(C.nameplates.heighthealth) or 8
|
|
cfg.targetglow = C.nameplates.targetglow == "1"
|
|
cfg.targethighlight = C.nameplates.targethighlight == "1"
|
|
cfg.outcombatstate = C.nameplates.outcombatstate == "1"
|
|
cfg.barcombatstate = C.nameplates.barcombatstate == "1"
|
|
cfg.ccombatcasting = C.nameplates.ccombatcasting == "1"
|
|
cfg.ccombatthreat = C.nameplates.ccombatthreat == "1"
|
|
cfg.ccombatnothreat = C.nameplates.ccombatnothreat == "1"
|
|
cfg.ccombatstun = C.nameplates.ccombatstun == "1"
|
|
cfg.ccombatofftank = C.nameplates.ccombatofftank == "1"
|
|
cfg.use_unitfonts = C.nameplates.use_unitfonts == "1"
|
|
cfg.font_size = cfg.use_unitfonts and C.global.font_unit_size or C.global.font_size
|
|
cfg.hptextformat = C.nameplates.hptextformat
|
|
-- NEW: Cache debuff config
|
|
cfg.debufftimers = C.nameplates.debufftimers == "1"
|
|
cfg.debuffanim = tonumber(C.nameplates.debuffanim) or 0
|
|
cfg.debufftext = tonumber(C.nameplates.debufftext) or 1
|
|
|
|
-- Rebuild offtanks lookup table
|
|
offtanks = {}
|
|
for k, v in pairs({strsplit("#", C.nameplates.combatofftanks)}) do
|
|
if v ~= "" then offtanks[string.lower(v)] = true end
|
|
end
|
|
end
|
|
|
|
local function RebuildOfftanks()
|
|
offtanks = {}
|
|
for k, v in pairs({strsplit("#", C.nameplates.combatofftanks)}) do
|
|
if v ~= "" then offtanks[string.lower(v)] = true end
|
|
end
|
|
end
|
|
RebuildOfftanks()
|
|
|
|
-- ============================================================================
|
|
-- OPTIMIZATION: Frame state cache
|
|
-- ============================================================================
|
|
local frameState = {
|
|
now = 0,
|
|
targetGuid = nil,
|
|
mouseoverGuid = nil,
|
|
}
|
|
|
|
-- cache default border color
|
|
local er, eg, eb, ea = GetStringColor(pfUI_config.appearance.border.color)
|
|
|
|
local NULL_GUID = "0x0000000000000000"
|
|
|
|
local function RebuildRaidGuidCache()
|
|
for k in pairs(raidGuidCache) do raidGuidCache[k] = nil end
|
|
for i = 1, GetNumRaidMembers() do
|
|
local g = UnitGUID("raid"..i)
|
|
if g then raidGuidCache[g] = UnitName("raid"..i) end
|
|
end
|
|
for i = 1, GetNumPartyMembers() do
|
|
local g = UnitGUID("party"..i)
|
|
if g then raidGuidCache[g] = UnitName("party"..i) end
|
|
end
|
|
local pg = UnitGUID("player")
|
|
if pg then raidGuidCache[pg] = UnitName("player") end
|
|
end
|
|
|
|
local combatColorCache = {} -- guid -> { color, expires }
|
|
|
|
local function GetCombatStateColor(guid, token)
|
|
-- PERF: Quick exit if player not in combat
|
|
if not UnitAffectingCombat("player") then return false end
|
|
if not token then return false end
|
|
if UnitCanAssist("player", token) then return false end
|
|
|
|
-- PERF: 0.2s throttle per guid - color changes are not time-critical
|
|
local now = frameState.now
|
|
local cached = combatColorCache[guid]
|
|
if cached and cached.expires > now then
|
|
return cached.color
|
|
end
|
|
|
|
if not UnitAffectingCombat(token) then return false end
|
|
|
|
-- The mob's current target via the nameplate token chain (ClassicAPI):
|
|
-- "nameplateNtarget" resolves to whatever this plate's unit is targeting,
|
|
-- so no GetUnitField("target") or SuperWoW "<guid>target" token needed.
|
|
local target = token .. "target"
|
|
local mobTargetGuid = UnitGUID(target)
|
|
local hasTarget = mobTargetGuid and mobTargetGuid ~= NULL_GUID
|
|
local color = false
|
|
|
|
local castInfo = GetCastInfo(token)
|
|
local isCasting = castInfo and castInfo.endTime and now < castInfo.endTime
|
|
local targetingPlayer = hasTarget and UnitIsUnit(target, "player")
|
|
|
|
if targetingPlayer then
|
|
threatMemory[guid] = true
|
|
elseif hasTarget and not isCasting then
|
|
threatMemory[guid] = nil
|
|
end
|
|
|
|
-- PERF: O(1) GUID lookup via raidGuidCache instead of O(40) loop
|
|
local targetName = hasTarget and (UnitName(target) or raidGuidCache[mobTargetGuid])
|
|
|
|
if cfg.ccombatcasting and isCasting then
|
|
color = combatstate.CASTING
|
|
elseif cfg.ccombatthreat and (targetingPlayer or threatMemory[guid]) then
|
|
color = combatstate.THREAT
|
|
elseif cfg.ccombatofftank and targetName and offtanks[strlower(targetName)] then
|
|
color = combatstate.OFFTANK
|
|
elseif cfg.ccombatofftank and pfUI.uf and pfUI.uf.raid and targetName and pfUI.uf.raid.tankrole[targetName] then
|
|
color = combatstate.OFFTANK
|
|
elseif cfg.ccombatnothreat and hasTarget then
|
|
color = combatstate.NOTHREAT
|
|
elseif cfg.ccombatstun and not hasTarget then
|
|
color = combatstate.STUN
|
|
end
|
|
|
|
combatColorCache[guid] = combatColorCache[guid] or {}
|
|
combatColorCache[guid].color = color
|
|
combatColorCache[guid].expires = now + 0.2
|
|
|
|
return color
|
|
end
|
|
|
|
local function DoNothing()
|
|
return
|
|
end
|
|
|
|
local function DisableObject(object)
|
|
if not object then return end
|
|
if not object.GetObjectType then return end
|
|
|
|
local otype = object:GetObjectType()
|
|
|
|
if otype == "Texture" then
|
|
object:SetTexture("")
|
|
object:SetTexCoord(0, 0, 0, 0)
|
|
elseif otype == "FontString" then
|
|
object:SetWidth(0.001)
|
|
elseif otype == "StatusBar" then
|
|
object:SetStatusBarTexture("")
|
|
end
|
|
end
|
|
|
|
local function TotemPlate(name)
|
|
if C.nameplates.totemicons == "1" then
|
|
for totem, icon in pairs(L["totems"]) do
|
|
if string.find(name, totem) then return icon end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function HidePlate(unittype, name, fullhp, target)
|
|
-- keep some plates always visible according to config
|
|
if C.nameplates.fullhealth == "1" and not fullhp then return nil end
|
|
if C.nameplates.target == "1" and target then return nil end
|
|
|
|
-- return true when something needs to be hidden
|
|
if C.nameplates.enemynpc == "1" and unittype == "ENEMY_NPC" then
|
|
return true
|
|
elseif C.nameplates.enemyplayer == "1" and unittype == "ENEMY_PLAYER" then
|
|
return true
|
|
elseif C.nameplates.neutralnpc == "1" and unittype == "NEUTRAL_NPC" then
|
|
return true
|
|
elseif C.nameplates.friendlynpc == "1" and unittype == "FRIENDLY_NPC" then
|
|
return true
|
|
elseif C.nameplates.friendlyplayer == "1" and unittype == "FRIENDLY_PLAYER" then
|
|
return true
|
|
elseif C.nameplates.critters == "1" and unittype == "NEUTRAL_NPC" then
|
|
for i, critter in pairs(L["critters"]) do
|
|
if string.lower(name) == string.lower(critter) then return true end
|
|
end
|
|
elseif C.nameplates.totems == "1" then
|
|
for totem in pairs(L["totems"]) do
|
|
if string.find(name, totem) then return true end
|
|
end
|
|
end
|
|
|
|
-- nothing to hide
|
|
return nil
|
|
end
|
|
|
|
local function abbrevname(t)
|
|
return string.sub(t,1,1)..". "
|
|
end
|
|
|
|
local function GetNameString(name)
|
|
local abbrev = pfUI_config.unitframes.abbrevname == "1" or nil
|
|
local size = 20
|
|
|
|
-- first try to only abbreviate the first word
|
|
if abbrev and name and strlen(name) > size then
|
|
name = string.gsub(name, "^(%S+) ", abbrevname)
|
|
end
|
|
|
|
-- abbreviate all if it still doesn't fit
|
|
if abbrev and name and strlen(name) > size then
|
|
name = string.gsub(name, "(%S+) ", abbrevname)
|
|
end
|
|
|
|
return name
|
|
end
|
|
|
|
|
|
local function GetUnitType(red, green, blue)
|
|
if red > .9 and green < .2 and blue < .2 then
|
|
return "ENEMY_NPC"
|
|
elseif red > .9 and green > .9 and blue < .2 then
|
|
return "NEUTRAL_NPC"
|
|
elseif red < .2 and green < .2 and blue > 0.9 then
|
|
return "FRIENDLY_PLAYER"
|
|
elseif red < .2 and green > .9 and blue < .2 then
|
|
return "FRIENDLY_NPC"
|
|
end
|
|
end
|
|
|
|
local filter, list, cache
|
|
local function DebuffFilterPopulate()
|
|
-- initialize variables
|
|
filter = C.nameplates["debuffs"]["filter"]
|
|
if filter == "none" then return end
|
|
list = C.nameplates["debuffs"][filter]
|
|
cache = {}
|
|
|
|
-- populate list
|
|
for _, val in pairs({strsplit("#", list)}) do
|
|
cache[strlower(val)] = true
|
|
end
|
|
end
|
|
|
|
local function DebuffFilter(effect)
|
|
if filter == "none" then return true end
|
|
if not cache then DebuffFilterPopulate() end
|
|
|
|
if filter == "blacklist" and cache[strlower(effect)] then
|
|
return nil
|
|
elseif filter == "blacklist" then
|
|
return true
|
|
elseif filter == "whitelist" and cache[strlower(effect)] then
|
|
return true
|
|
elseif filter == "whitelist" then
|
|
return nil
|
|
end
|
|
end
|
|
|
|
local function CreateDebuffIcon(plate, index)
|
|
plate.debuffs[index] = CreateFrame("Frame", plate.platename.."Debuff"..index, plate)
|
|
plate.debuffs[index]:Hide()
|
|
plate.debuffs[index]:SetFrameLevel(4)
|
|
|
|
plate.debuffs[index].icon = plate.debuffs[index]:CreateTexture(nil, "BACKGROUND")
|
|
plate.debuffs[index].icon:SetTexture(.3,1,.8,1)
|
|
plate.debuffs[index].icon:SetAllPoints(plate.debuffs[index])
|
|
|
|
plate.debuffs[index].stacks = plate.debuffs[index]:CreateFontString(nil, "OVERLAY")
|
|
plate.debuffs[index].stacks:SetAllPoints(plate.debuffs[index])
|
|
plate.debuffs[index].stacks:SetJustifyH("RIGHT")
|
|
plate.debuffs[index].stacks:SetJustifyV("BOTTOM")
|
|
plate.debuffs[index].stacks:SetTextColor(1,1,0)
|
|
|
|
-- PERF: Use lightweight fake cooldown frame when animation disabled
|
|
-- The Model-based CooldownFrameTemplate causes major lag with many nameplates
|
|
if cfg.debuffanim ~= 1 then
|
|
plate.debuffs[index].cd = CreateFrame("Frame", plate.platename.."Debuff"..index.."Cooldown", plate.debuffs[index])
|
|
plate.debuffs[index].cd:SetAllPoints(plate.debuffs[index])
|
|
plate.debuffs[index].cd:SetFrameLevel(6)
|
|
plate.debuffs[index].cd:SetScript("OnUpdate", CooldownFrame_OnUpdateModel)
|
|
plate.debuffs[index].cd.AdvanceTime = DoNothing
|
|
plate.debuffs[index].cd.SetSequence = DoNothing
|
|
plate.debuffs[index].cd.SetSequenceTime = DoNothing
|
|
else
|
|
-- Use CooldownFrameTemplate for animation
|
|
plate.debuffs[index].cd = CreateFrame(COOLDOWN_FRAME_TYPE, plate.platename.."Debuff"..index.."Cooldown", plate.debuffs[index], "CooldownFrameTemplate")
|
|
plate.debuffs[index].cd:SetAllPoints(plate.debuffs[index])
|
|
plate.debuffs[index].cd:SetFrameLevel(6)
|
|
end
|
|
|
|
-- Set initial config flags (will be cached per-cooldown later)
|
|
plate.debuffs[index].cd.pfCooldownStyleAnimation = cfg.debuffanim
|
|
plate.debuffs[index].cd.pfCooldownStyleText = cfg.debufftext
|
|
plate.debuffs[index].cd.pfCooldownType = "ALL"
|
|
end
|
|
|
|
local function UpdateDebuffConfig(nameplate, i)
|
|
if not nameplate.debuffs[i] then return end
|
|
|
|
-- update debuff positions
|
|
local width = tonumber(C.nameplates.width)
|
|
local debuffsize = tonumber(C.nameplates.debuffsize)
|
|
local debuffoffset = tonumber(C.nameplates.debuffoffset)
|
|
local limit = floor(width / debuffsize)
|
|
local font = C.nameplates.use_unitfonts == "1" and pfUI.font_unit or pfUI.font_default
|
|
local font_size = C.nameplates.use_unitfonts == "1" and C.global.font_unit_size or C.global.font_size
|
|
local font_style = C.nameplates.name.fontstyle
|
|
|
|
local aligna, alignb, offs, space
|
|
if C.nameplates.debuffs["position"] == "BOTTOM" then
|
|
aligna, alignb, offs, space = "TOPLEFT", "BOTTOMLEFT", -debuffoffset, -1
|
|
else
|
|
aligna, alignb, offs, space = "BOTTOMLEFT", "TOPLEFT", debuffoffset, 1
|
|
end
|
|
|
|
nameplate.debuffs[i].stacks:SetFont(font, font_size, font_style)
|
|
nameplate.debuffs[i]:ClearAllPoints()
|
|
if i == 1 then
|
|
nameplate.debuffs[i]:SetPoint(aligna, nameplate.health, alignb, 0, offs)
|
|
elseif i <= limit then
|
|
nameplate.debuffs[i]:SetPoint("LEFT", nameplate.debuffs[i-1], "RIGHT", 1, 0)
|
|
elseif i > limit and limit > 0 then
|
|
nameplate.debuffs[i]:SetPoint(aligna, nameplate.debuffs[i-limit], alignb, 0, space)
|
|
end
|
|
|
|
nameplate.debuffs[i]:SetSize(debuffsize, debuffsize)
|
|
|
|
-- Update cooldown display settings
|
|
if nameplate.debuffs[i].cd then
|
|
local cooldown_text = tonumber(C.nameplates.debufftext) or 1
|
|
local cooldown_anim = tonumber(C.nameplates.debuffanim) or 0
|
|
nameplate.debuffs[i].cd.pfCooldownStyleText = cooldown_text
|
|
nameplate.debuffs[i].cd.pfCooldownStyleAnimation = cooldown_anim
|
|
|
|
end
|
|
end
|
|
|
|
-- create nameplate core
|
|
local nameplates = CreateFrame("Frame", "pfNameplates", UIParent)
|
|
nameplates:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
nameplates:RegisterEvent("PLAYER_TARGET_CHANGED")
|
|
nameplates:RegisterEvent("PLAYER_LOGOUT")
|
|
nameplates:RegisterEvent("UNIT_COMBO_POINTS")
|
|
nameplates:RegisterEvent("PLAYER_COMBO_POINTS")
|
|
nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA")
|
|
nameplates:RegisterEvent("RAID_ROSTER_UPDATE")
|
|
nameplates:RegisterEvent("PARTY_MEMBERS_CHANGED")
|
|
nameplates:RegisterEvent("NAME_PLATE_CREATED")
|
|
nameplates:RegisterEvent("NAME_PLATE_UNIT_ADDED")
|
|
nameplates:RegisterEvent("NAME_PLATE_UNIT_REMOVED")
|
|
nameplates:RegisterEvent("UNIT_AURA")
|
|
nameplates:RegisterEvent("UNIT_FLAGS")
|
|
nameplates:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
|
|
nameplates:RegisterEvent("UNIT_SPELLCAST_START")
|
|
nameplates:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START")
|
|
nameplates:RegisterEvent("UNIT_SPELLCAST_STOP")
|
|
nameplates:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")
|
|
|
|
nameplates:SetScript("OnEvent", function()
|
|
-- Stop event handling during logout to prevent crash 132
|
|
if event == "PLAYER_LOGOUT" then
|
|
this:UnregisterAllEvents()
|
|
this:SetScript("OnEvent", nil)
|
|
this:SetScript("OnUpdate", nil)
|
|
if nameplates.mouselook then
|
|
nameplates.mouselook:SetScript("OnUpdate", nil)
|
|
end
|
|
return
|
|
|
|
elseif event == "PLAYER_ENTERING_WORLD" or event == "ZONE_CHANGED_NEW_AREA" then
|
|
if event == "PLAYER_ENTERING_WORLD" then
|
|
CacheConfig()
|
|
this:SetGameVariables()
|
|
RebuildRaidGuidCache()
|
|
frameState.targetGuid = UnitGUID("target")
|
|
end
|
|
|
|
-- Handle friendly zone nameplate disable feature
|
|
local disableHostile = C.nameplates["disable_hostile_in_friendly"] == "1"
|
|
local disableFriendly = C.nameplates["disable_friendly_in_friendly"] == "1"
|
|
|
|
if disableHostile or disableFriendly then
|
|
local pvpType = GetZonePVPInfo()
|
|
local nowFriendly = (pvpType == "friendly")
|
|
|
|
if nowFriendly and not inFriendlyZone then
|
|
-- Entering friendly zone - save current state and hide based on options
|
|
inFriendlyZone = true
|
|
savedHostileState = C.nameplates["showhostile"]
|
|
savedFriendlyState = C.nameplates["showfriendly"]
|
|
|
|
if disableHostile then
|
|
_G.NAMEPLATES_ON = nil
|
|
HideNameplates()
|
|
end
|
|
|
|
if disableFriendly then
|
|
_G.FRIENDNAMEPLATES_ON = nil
|
|
HideFriendNameplates()
|
|
end
|
|
elseif not nowFriendly and inFriendlyZone then
|
|
-- Leaving friendly zone - restore previous state
|
|
inFriendlyZone = false
|
|
|
|
if savedHostileState == "1" then
|
|
_G.NAMEPLATES_ON = true
|
|
ShowNameplates()
|
|
end
|
|
|
|
if savedFriendlyState == "1" then
|
|
_G.FRIENDNAMEPLATES_ON = true
|
|
ShowFriendNameplates()
|
|
end
|
|
|
|
savedHostileState = nil
|
|
savedFriendlyState = nil
|
|
end
|
|
end
|
|
|
|
elseif event == "RAID_ROSTER_UPDATE" or event == "PARTY_MEMBERS_CHANGED" then
|
|
RebuildRaidGuidCache()
|
|
|
|
elseif event == "NAME_PLATE_CREATED" then
|
|
-- arg1 = the nameplate Frame; build the pfUI overlay once per pool slot
|
|
if arg1 and not registry[arg1] then
|
|
nameplates.OnCreate(arg1)
|
|
registry[arg1] = arg1
|
|
end
|
|
|
|
elseif event == "NAME_PLATE_UNIT_ADDED" then
|
|
-- arg1 = "nameplateN" unit token. Cache the GUID for cache keys and the
|
|
-- token itself for token-based UnitX reads (stable per plate lifetime).
|
|
local plate = C_NamePlate.GetNamePlateForUnit(arg1)
|
|
if plate and plate.nameplate then
|
|
visiblePlates[plate] = plate
|
|
local guid = UnitGUID(arg1)
|
|
plate.nameplate.cachedGuid = guid
|
|
plate.nameplate.unit = arg1
|
|
if guid then
|
|
plateByGuid[guid] = plate.nameplate
|
|
-- Seed: the unit may already be mid-cast (its UNIT_SPELLCAST_START
|
|
-- fired before this plate existed). One poll here catches that;
|
|
-- ongoing casts arrive via the event.
|
|
castState[guid] = PollCastInfo(arg1)
|
|
end
|
|
nameplates.OnShow(plate)
|
|
end
|
|
visiblePlateCount = visiblePlateCount + 1
|
|
|
|
elseif event == "NAME_PLATE_UNIT_REMOVED" then
|
|
visiblePlateCount = visiblePlateCount > 0 and visiblePlateCount - 1 or 0
|
|
-- arg1 = "nameplateN" unit token; UnitGUID still resolves inside the
|
|
-- handler (the slot is freed after dispatch returns)
|
|
local plate = C_NamePlate.GetNamePlateForUnit(arg1)
|
|
if plate then visiblePlates[plate] = nil end
|
|
local guid = UnitGUID(arg1)
|
|
if guid then
|
|
if debuffCache[guid] then debuffCache[guid] = nil end
|
|
if threatMemory[guid] then threatMemory[guid] = nil end
|
|
if combatColorCache[guid] then combatColorCache[guid] = nil end
|
|
if castState[guid] then castState[guid] = nil end
|
|
if plateByGuid[guid] then plateByGuid[guid] = nil end
|
|
if plate and plate.nameplate and plate.nameplate.cachedGuid == guid then
|
|
plate.nameplate.cachedGuid = nil
|
|
plate.nameplate.unit = nil
|
|
end
|
|
end
|
|
|
|
elseif event == "UNIT_FLAGS" then
|
|
if arg1 and strfind(arg1, "^nameplate") then
|
|
local plate = C_NamePlate.GetNamePlateForUnit(arg1)
|
|
if plate and plate.nameplate then
|
|
plate.nameplate.eventcache = true
|
|
end
|
|
end
|
|
|
|
elseif event == "UPDATE_MOUSEOVER_UNIT" then
|
|
local new = UnitGUID("mouseover")
|
|
local old = frameState.mouseoverGuid
|
|
if new ~= old then
|
|
frameState.mouseoverGuid = new
|
|
local po = old and plateByGuid[old]
|
|
if po then po.eventcache = true end
|
|
local pn = new and plateByGuid[new]
|
|
if pn then pn.eventcache = true end
|
|
end
|
|
|
|
elseif event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_CHANNEL_START" then
|
|
-- ClassicAPI fires UNIT_SPELLCAST_* per unit token, including the caster's
|
|
-- "nameplateN". The payload has no timing, so poll it (PollCastInfo picks
|
|
-- cast vs channel) and cache -- only for a unit we have a plate for, so
|
|
-- the table stays bounded to on-screen casters.
|
|
if arg1 and strfind(arg1, "^nameplate") then
|
|
local guid = UnitGUID(arg1)
|
|
local plate = guid and plateByGuid[guid]
|
|
if plate then
|
|
castState[guid] = PollCastInfo(arg1)
|
|
if castState[guid] then
|
|
plate.castUpdate = true -- bypass the throttle so the bar shows now
|
|
end
|
|
end
|
|
end
|
|
|
|
elseif event == "UNIT_SPELLCAST_STOP" or event == "UNIT_SPELLCAST_CHANNEL_STOP" then
|
|
-- Cast/channel ended (natural, interrupted, or cancelled -- the poll fires
|
|
-- STOP for all three). Clear the cached cast and refresh its plate.
|
|
if arg1 and strfind(arg1, "^nameplate") then
|
|
local guid = UnitGUID(arg1)
|
|
if guid and castState[guid] then
|
|
castState[guid] = nil
|
|
local plate = plateByGuid[guid]
|
|
if plate then plate.castUpdate = true end
|
|
end
|
|
end
|
|
|
|
elseif event == "UNIT_AURA" then
|
|
-- ClassicAPI: fires with arg1 == "nameplateN" when a unit's aura set
|
|
-- changes (add/remove/modify). Flag the matching plate so OnUpdate does a
|
|
-- fresh C_UnitAuras read next tick instead of waiting on the 0.5s
|
|
-- throttle -- covers expirations, dispels, refreshes, and stack changes
|
|
-- in one event. Guard on the token prefix (UNIT_AURA also fires for
|
|
-- target/party/raid).
|
|
if arg1 and strfind(arg1, "^nameplate") then
|
|
local plate = C_NamePlate.GetNamePlateForUnit(arg1)
|
|
if plate and plate.nameplate then
|
|
plate.nameplate.auraUpdate = true
|
|
end
|
|
end
|
|
|
|
elseif event == "PLAYER_TARGET_CHANGED" then
|
|
frameState.targetGuid = UnitGUID('target')
|
|
-- Flag the target's plate for update
|
|
local plate = C_NamePlate.GetNamePlateForUnit("target")
|
|
if plate and plate.nameplate then
|
|
plate.nameplate.targetUpdate = true
|
|
end
|
|
-- Also propagate to all plates for alpha/strata updates
|
|
this.eventcache = true
|
|
|
|
elseif event == "PLAYER_COMBO_POINTS" or event == "UNIT_COMBO_POINTS" then
|
|
-- Only flag the target's plate for combo point update
|
|
local plate = C_NamePlate.GetNamePlateForUnit("target")
|
|
if plate and plate.nameplate then
|
|
plate.nameplate.comboUpdate = true
|
|
end
|
|
else
|
|
this.eventcache = true
|
|
end
|
|
end)
|
|
|
|
nameplates:SetScript("OnUpdate", function()
|
|
-- PERF: Throttle central OnUpdate to ~80 FPS (0.0125s)
|
|
-- Saves ~44% calls at 144 FPS while staying above 50 FPS target-plate rate
|
|
local now = GetTime()
|
|
if (this.frameTick or 0) + 0.01 > now then return end
|
|
this.frameTick = now
|
|
|
|
-- PERF: Cache GetTime() once per frame
|
|
frameState.now = now
|
|
|
|
if this.eventcache then
|
|
this.eventcache = nil
|
|
for plate in pairs(visiblePlates) do
|
|
plate.nameplate.eventcache = true
|
|
end
|
|
end
|
|
|
|
for plate in pairs(visiblePlates) do
|
|
if plate:IsVisible() then
|
|
nameplates.OnUpdate(plate, frameState)
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- combat tracker
|
|
nameplates.combat = CreateFrame("Frame")
|
|
nameplates.combat:RegisterEvent("PLAYER_ENTER_COMBAT")
|
|
nameplates.combat:RegisterEvent("PLAYER_LEAVE_COMBAT")
|
|
nameplates.combat:RegisterEvent("PLAYER_LOGOUT")
|
|
nameplates.combat:SetScript("OnEvent", function()
|
|
if event == "PLAYER_LOGOUT" then
|
|
this:UnregisterAllEvents()
|
|
this:SetScript("OnEvent", nil)
|
|
return
|
|
elseif event == "PLAYER_ENTER_COMBAT" then
|
|
this.inCombat = 1
|
|
if PlayerFrame then PlayerFrame.inCombat = 1 end
|
|
elseif event == "PLAYER_LEAVE_COMBAT" then
|
|
this.inCombat = nil
|
|
if PlayerFrame then PlayerFrame.inCombat = nil end
|
|
-- Clear threat memory when leaving combat
|
|
for k in pairs(threatMemory) do
|
|
threatMemory[k] = nil
|
|
end
|
|
end
|
|
end)
|
|
|
|
nameplates.OnCreate = function(frame)
|
|
local parent = frame or this
|
|
platecount = platecount + 1
|
|
local platename = "pfNamePlate" .. platecount
|
|
|
|
-- create pfUI nameplate overlay
|
|
local nameplate = CreateFrame("Button", platename, parent)
|
|
nameplate.platename = platename
|
|
nameplate:EnableMouse(0)
|
|
nameplate.parent = parent
|
|
nameplate.cache = {}
|
|
nameplate.original = {}
|
|
|
|
-- create shortcuts for all known elements and disable them
|
|
nameplate.original.healthbar, nameplate.original.castbar = parent:GetChildren()
|
|
DisableObject(nameplate.original.healthbar)
|
|
DisableObject(nameplate.original.castbar)
|
|
|
|
for i, object in pairs({parent:GetRegions()}) do
|
|
if NAMEPLATE_OBJECTORDER[i] and NAMEPLATE_OBJECTORDER[i] == "raidicon" then
|
|
nameplate[NAMEPLATE_OBJECTORDER[i]] = object
|
|
elseif NAMEPLATE_OBJECTORDER[i] then
|
|
nameplate.original[NAMEPLATE_OBJECTORDER[i]] = object
|
|
DisableObject(object)
|
|
else
|
|
DisableObject(object)
|
|
end
|
|
end
|
|
|
|
nameplate.original.healthbar:HookScript("OnValueChanged", nameplates.OnValueChanged)
|
|
|
|
-- adjust sizes and scaling of the nameplate
|
|
nameplate:SetScale(UIParent:GetScale())
|
|
|
|
nameplate.health = CreateFrame("StatusBar", nil, nameplate)
|
|
nameplate.health:SetFrameLevel(4) -- keep above glow
|
|
nameplate.health.text = nameplate.health:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
|
nameplate.health.text:SetAllPoints()
|
|
nameplate.health.text:SetTextColor(1,1,1,1)
|
|
|
|
nameplate.name = nameplate:CreateFontString(nil, "OVERLAY")
|
|
nameplate.name:SetPoint("TOP", nameplate, "TOP", 0, 0)
|
|
|
|
nameplate.glow = nameplate:CreateTexture(nil, "BACKGROUND")
|
|
nameplate.glow:SetPoint("CENTER", nameplate.health, "CENTER", 0, 0)
|
|
nameplate.glow:SetTexture(pfUI.media["img:dot"])
|
|
nameplate.glow:Hide()
|
|
|
|
nameplate.guild = nameplate:CreateFontString(nil, "OVERLAY")
|
|
nameplate.guild:SetPoint("BOTTOM", nameplate.health, "BOTTOM", 0, 0)
|
|
|
|
nameplate.level = nameplate:CreateFontString(nil, "OVERLAY")
|
|
nameplate.level:SetPoint("RIGHT", nameplate.health, "LEFT", -3, 0)
|
|
|
|
-- Create a dedicated high-level frame for the raid icon so it renders
|
|
-- ABOVE nameplate.health (FrameLevel 4) and stays visible even when
|
|
-- nameplates are toggled off (the Blizzard parent plate still exists).
|
|
nameplate.raidiconframe = CreateFrame("Frame", nil, nameplate)
|
|
nameplate.raidiconframe:SetFrameLevel(10)
|
|
nameplate.raidiconframe:SetAllPoints(nameplate)
|
|
nameplate.raidicon:SetParent(nameplate.raidiconframe)
|
|
nameplate.raidicon:SetDrawLayer("OVERLAY", 7)
|
|
if C.unitframes.blizzard_raidicons ~= "1" then
|
|
nameplate.raidicon:SetTexture(pfUI.media["img:raidicons"])
|
|
end
|
|
|
|
nameplate.totem = CreateFrame("Frame", nil, nameplate)
|
|
nameplate.totem:SetPoint("CENTER", nameplate, "CENTER", 0, 0)
|
|
nameplate.totem:SetSize(32, 32)
|
|
nameplate.totem.icon = nameplate.totem:CreateTexture(nil, "OVERLAY")
|
|
nameplate.totem.icon:SetTexCoord(.078, .92, .079, .937)
|
|
nameplate.totem.icon:SetAllPoints()
|
|
CreateBackdrop(nameplate.totem)
|
|
|
|
do -- debuffs
|
|
nameplate.debuffs = {}
|
|
CreateDebuffIcon(nameplate, 1)
|
|
end
|
|
|
|
do -- combopoints
|
|
local combopoints = { }
|
|
for i = 1, 5 do
|
|
combopoints[i] = CreateFrame("Frame", nil, nameplate)
|
|
combopoints[i]:Hide()
|
|
combopoints[i]:SetFrameLevel(8)
|
|
combopoints[i].tex = combopoints[i]:CreateTexture("OVERLAY")
|
|
combopoints[i].tex:SetAllPoints()
|
|
|
|
if i < 3 then
|
|
combopoints[i].tex:SetTexture(1, .3, .3, .75)
|
|
elseif i < 4 then
|
|
combopoints[i].tex:SetTexture(1, 1, .3, .75)
|
|
else
|
|
combopoints[i].tex:SetTexture(.3, 1, .3, .75)
|
|
end
|
|
end
|
|
nameplate.combopoints = combopoints
|
|
end
|
|
|
|
do -- castbar
|
|
local castbar = CreateFrame("StatusBar", nil, nameplate.health)
|
|
castbar:Hide()
|
|
|
|
castbar:SetScript("OnShow", function()
|
|
if C.nameplates.debuffs["position"] == "BOTTOM" then
|
|
nameplate.debuffs[1]:SetPoint("TOPLEFT", this, "BOTTOMLEFT", 0, -4)
|
|
end
|
|
end)
|
|
|
|
castbar:SetScript("OnHide", function()
|
|
if C.nameplates.debuffs["position"] == "BOTTOM" then
|
|
nameplate.debuffs[1]:SetPoint("TOPLEFT", this:GetParent(), "BOTTOMLEFT", 0, -4)
|
|
end
|
|
end)
|
|
|
|
castbar.text = castbar:CreateFontString("Status", "DIALOG", "GameFontNormal")
|
|
castbar.text:SetPoint("RIGHT", castbar, "LEFT", -4, 0)
|
|
castbar.text:SetNonSpaceWrap(false)
|
|
castbar.text:SetTextColor(1,1,1,.5)
|
|
|
|
castbar.spell = castbar:CreateFontString("Status", "DIALOG", "GameFontNormal")
|
|
castbar.spell:SetPoint("CENTER", castbar, "CENTER")
|
|
castbar.spell:SetNonSpaceWrap(false)
|
|
castbar.spell:SetTextColor(1,1,1,1)
|
|
|
|
castbar.icon = CreateFrame("Frame", nil, castbar)
|
|
castbar.icon.tex = castbar.icon:CreateTexture(nil, "BORDER")
|
|
castbar.icon.tex:SetAllPoints()
|
|
|
|
nameplate.castbar = castbar
|
|
end
|
|
|
|
-- Stagger tick to spread updates across frames (0.05s apart per plate)
|
|
nameplate.tick = GetTime() + mathmod(platecount, 10) * 0.05
|
|
|
|
parent.nameplate = nameplate
|
|
-- NOTE: OnUpdate is now handled centrally, not per-plate/
|
|
parent:SetScript("OnUpdate", nil) -- Disable Blizzard's OnUpdate
|
|
|
|
nameplates.OnConfigChange(parent)
|
|
end
|
|
|
|
nameplates.OnConfigChange = function(frame)
|
|
local parent = frame
|
|
local nameplate = frame.nameplate
|
|
|
|
local font = C.nameplates.use_unitfonts == "1" and pfUI.font_unit or pfUI.font_default
|
|
local font_size = C.nameplates.use_unitfonts == "1" and C.global.font_unit_size or C.global.font_size
|
|
local font_style = C.nameplates.name.fontstyle
|
|
local glowr, glowg, glowb, glowa = GetStringColor(C.nameplates.glowcolor)
|
|
local hlr, hlg, hlb, hla = GetStringColor(C.nameplates.highlightcolor)
|
|
local hptexture = pfUI.media[C.nameplates.healthtexture]
|
|
local rawborder, default_border = GetBorderSize("nameplates")
|
|
|
|
local plate_width = C.nameplates.width + 50
|
|
local plate_height = C.nameplates.heighthealth + font_size + 5
|
|
-- local plate_height_cast = C.nameplates.heighthealth + font_size + 5 + C.nameplates.heightcast + 5
|
|
local combo_size = 5
|
|
|
|
-- local width = tonumber(C.nameplates.width)
|
|
-- local debuffsize = tonumber(C.nameplates.debuffsize)
|
|
local healthoffset = tonumber(C.nameplates.health.offset)
|
|
local orientation = C.nameplates.verticalhealth == "1" and "VERTICAL" or "HORIZONTAL"
|
|
|
|
local c = combatstate -- load combat state colors
|
|
c.CASTING.r, c.CASTING.g, c.CASTING.b, c.CASTING.a = GetStringColor(C.nameplates.combatcasting)
|
|
c.THREAT.r, c.THREAT.g, c.THREAT.b, c.THREAT.a = GetStringColor(C.nameplates.combatthreat)
|
|
c.NOTHREAT.r, c.NOTHREAT.g, c.NOTHREAT.b, c.NOTHREAT.a = GetStringColor(C.nameplates.combatnothreat)
|
|
c.OFFTANK.r, c.OFFTANK.g, c.OFFTANK.b, c.OFFTANK.a = GetStringColor(C.nameplates.combatofftank)
|
|
c.STUN.r, c.STUN.g, c.STUN.b, c.STUN.a = GetStringColor(C.nameplates.combatstun)
|
|
|
|
RebuildOfftanks()
|
|
|
|
nameplate:SetSize(plate_width, plate_height)
|
|
nameplate:SetPoint("TOP", parent, "TOP", 0, 0)
|
|
|
|
nameplate.name:SetFont(font, font_size, font_style)
|
|
local nameTextPos = C.nameplates.nametextpos or "CENTER"
|
|
local nameAnchor = nameTextPos == "RIGHT" and { "BOTTOMRIGHT", "TOPRIGHT" }
|
|
or nameTextPos == "CENTER" and { "BOTTOM", "TOP" }
|
|
or { "BOTTOMLEFT", "TOPLEFT" }
|
|
nameplate.name:ClearAllPoints()
|
|
nameplate.name:SetPoint(nameAnchor[1], nameplate.health, nameAnchor[2], 0, -healthoffset)
|
|
nameplate.name:SetJustifyH(nameTextPos)
|
|
|
|
nameplate.health:SetOrientation(orientation)
|
|
-- Bar anchors to the plate directly so the name's JustifyH (configurable
|
|
-- below) can shift left/right without dragging the bar with it.
|
|
nameplate.health:ClearAllPoints()
|
|
nameplate.health:SetPoint("BOTTOM", nameplate, "BOTTOM", 0, 0)
|
|
nameplate.health:SetStatusBarTexture(hptexture)
|
|
nameplate.health:SetWidth(C.nameplates.width)
|
|
nameplate.health:SetHeight(C.nameplates.heighthealth)
|
|
nameplate.health.hlr, nameplate.health.hlg, nameplate.health.hlb, nameplate.health.hla = hlr, hlg, hlb, hla
|
|
|
|
CreateBackdrop(nameplate.health, default_border)
|
|
|
|
nameplate.health.text:SetFont(font, font_size - 2, "OUTLINE")
|
|
nameplate.health.text:SetJustifyH(C.nameplates.hptextpos)
|
|
|
|
nameplate.guild:SetFont(font, font_size, font_style)
|
|
|
|
nameplate.glow:SetSize(C.nameplates.width + 60, C.nameplates.heighthealth + 30)
|
|
nameplate.glow:SetVertexColor(glowr, glowg, glowb, glowa)
|
|
|
|
nameplate.raidicon:ClearAllPoints()
|
|
nameplate.raidicon:SetPoint("BOTTOM", nameplate.health, "TOP", C.nameplates.raidiconoffx, C.nameplates.raidiconoffy)
|
|
nameplate.level:SetFont(font, font_size, font_style)
|
|
nameplate.raidicon:SetSize(C.nameplates.raidiconsize, C.nameplates.raidiconsize)
|
|
|
|
for i=1,16 do
|
|
UpdateDebuffConfig(nameplate, i)
|
|
end
|
|
|
|
for i=1,5 do
|
|
nameplate.combopoints[i]:SetSize(combo_size, combo_size)
|
|
nameplate.combopoints[i]:SetPoint("TOPRIGHT", nameplate.health, "BOTTOMRIGHT", -(i-1)*(combo_size+default_border*3), -default_border*3)
|
|
CreateBackdrop(nameplate.combopoints[i], default_border)
|
|
end
|
|
|
|
nameplate.castbar:SetPoint("TOPLEFT", nameplate.health, "BOTTOMLEFT", 0, -default_border*3)
|
|
nameplate.castbar:SetPoint("TOPRIGHT", nameplate.health, "BOTTOMRIGHT", 0, -default_border*3)
|
|
nameplate.castbar:SetHeight(C.nameplates.heightcast)
|
|
-- Use the same castbar texture and color as the unit frame castbar (castbar.lua)
|
|
local cbtexture = pfUI.media[C.appearance.castbar.texture]
|
|
nameplate.castbar:SetStatusBarTexture(cbtexture or hptexture)
|
|
local cbr, cbg, cbb, cba = strsplit(",", C.appearance.castbar.castbarcolor)
|
|
nameplate.castbar:SetStatusBarColor(cbr, cbg, cbb, cba)
|
|
-- reset endTime cache so color/texture refresh takes effect on next cast
|
|
nameplate.castbar.lastEndTime = nil
|
|
CreateBackdrop(nameplate.castbar, default_border)
|
|
|
|
nameplate.castbar.text:SetFont(font, font_size, "OUTLINE")
|
|
nameplate.castbar.spell:SetFont(font, font_size, "OUTLINE")
|
|
nameplate.castbar.icon:SetPoint("BOTTOMLEFT", nameplate.castbar, "BOTTOMRIGHT", default_border*3, 0)
|
|
nameplate.castbar.icon:SetPoint("TOPLEFT", nameplate.health, "TOPRIGHT", default_border*3, 0)
|
|
nameplate.castbar.icon:SetWidth(C.nameplates.heightcast + default_border*3 + C.nameplates.heighthealth)
|
|
CreateBackdrop(nameplate.castbar.icon, default_border)
|
|
end
|
|
|
|
nameplates.OnValueChanged = function()
|
|
local plate = this:GetParent().nameplate
|
|
if plate and plate.health then
|
|
plate.health:SetMinMaxValues(plate.original.healthbar:GetMinMaxValues())
|
|
plate.health:SetValue(plate.original.healthbar:GetValue())
|
|
end
|
|
end
|
|
|
|
nameplates.OnDataChanged = function(self, plate)
|
|
local visible = plate:IsVisible()
|
|
local hp = plate.original.healthbar:GetValue()
|
|
local hpmin, hpmax = plate.original.healthbar:GetMinMaxValues()
|
|
local name = plate.original.name:GetText()
|
|
local level = plate.original.level:IsShown() and plate.original.level:GetObjectType() == "FontString" and tonumber(plate.original.level:GetText()) or "??"
|
|
|
|
-- reset per-unit cache when the plate is reassigned. Gate on GUID *and*
|
|
-- name — name alone misses pool reuse between same-named units (e.g. plate
|
|
-- held a player "Ironforge Guard" and is now reassigned to the NPC by the
|
|
-- same name), which would leak a stale "PLAYER" hint into GetUnitInfo.
|
|
-- Wipe the whole cache table: the PERF gates below ("only update X when
|
|
-- X changed") would otherwise skip bar/color/text updates when the new
|
|
-- unit happens to share a cached value with the previous occupant
|
|
-- (e.g., both at 60% HP percentage on plate pool reuse → bar stays at
|
|
-- the old fill until the new mob actually changes HP).
|
|
if plate.cache.name ~= name or plate.cache.guid ~= plate.cachedGuid then
|
|
table.wipe(plate.cache)
|
|
plate.cache.name = name
|
|
plate.cache.guid = plate.cachedGuid
|
|
plate.cdCache = nil -- new unit, reset spell-keyed timer cache
|
|
plate.name:SetText(GetNameString(name))
|
|
end
|
|
|
|
local target = plate.istarget
|
|
local mouseover = plate.cachedGuid and plate.cachedGuid == frameState.mouseoverGuid or nil
|
|
local unitstr = target and "target" or mouseover and "mouseover" or plate.cachedGuid or nil
|
|
|
|
-- resolve player vs npc from plate's own unit so libunitscan can't return
|
|
-- a player record for an NPC sharing the same name (e.g. Chromie). Stored
|
|
-- as true/false/nil so it doubles as the GetUnitInfo hint.
|
|
if plate.cache.player == nil and unitstr then
|
|
plate.cache.player = UnitIsPlayer(unitstr) and true or false
|
|
end
|
|
local class, ulevel, elite, player, guild = GetUnitInfo(name, true, plate.cache.player)
|
|
if plate.cache.player ~= nil then player = plate.cache.player or nil end
|
|
|
|
-- Use database level ONLY if current level is ?? (fixes ?? after reload, but doesn't override visible levels)
|
|
local levelFromDB = false
|
|
if level == "??" and ulevel and ulevel > 0 then
|
|
level = ulevel
|
|
levelFromDB = true
|
|
end
|
|
|
|
local red, green, blue = plate.original.healthbar:GetStatusBarColor()
|
|
local unittype = GetUnitType(red, green, blue) or "ENEMY_NPC"
|
|
local font_size = C.nameplates.use_unitfonts == "1" and C.global.font_unit_size or C.global.font_size
|
|
|
|
-- ignore players with npc names if plate level is lower than player level
|
|
if ulevel and ulevel > (level == "??" and -1 or level) then player = nil end
|
|
|
|
if player and unittype == "ENEMY_NPC" then unittype = "ENEMY_PLAYER" end
|
|
if player and unittype == "FRIENDLY_NPC" then unittype = "FRIENDLY_PLAYER" end
|
|
elite = plate.original.levelicon:IsShown() and not player and "boss" or elite
|
|
if not class then plate.wait_for_scan = true end
|
|
|
|
-- skip data updates on invisible frames
|
|
if not visible then return end
|
|
|
|
-- target event sometimes fires too quickly, where nameplate identifiers are not
|
|
-- yet updated. So while being inside this event, we cannot trust the unitstr.
|
|
if event == "PLAYER_TARGET_CHANGED" then unitstr = nil end
|
|
|
|
-- remove unitstr on unit name mismatch
|
|
if unitstr and UnitName(unitstr) ~= name then unitstr = nil end
|
|
|
|
-- always make sure to keep plate visible
|
|
plate:Show()
|
|
|
|
plate.glow:SetShown(target and cfg.targetglow)
|
|
|
|
-- target indicator
|
|
if cfg.outcombatstate then
|
|
local guid = plate.cachedGuid or ""
|
|
|
|
-- determine color based on combat state
|
|
local color = GetCombatStateColor(guid, plate.unit)
|
|
if not color then color = combatstate.NONE end
|
|
|
|
-- set border color
|
|
plate.health.backdrop:SetBackdropBorderColor(color.r, color.g, color.b, color.a)
|
|
elseif target and cfg.targethighlight then
|
|
plate.health.backdrop:SetBackdropBorderColor(plate.health.hlr, plate.health.hlg, plate.health.hlb, plate.health.hla)
|
|
elseif C.nameplates.outfriendlynpc == "1" and unittype == "FRIENDLY_NPC" then
|
|
plate.health.backdrop:SetBackdropBorderColor(unpack(unitcolors[unittype]))
|
|
elseif C.nameplates.outfriendly == "1" and unittype == "FRIENDLY_PLAYER" then
|
|
plate.health.backdrop:SetBackdropBorderColor(unpack(unitcolors[unittype]))
|
|
elseif C.nameplates.outneutral == "1" and strfind(unittype, "NEUTRAL") then
|
|
plate.health.backdrop:SetBackdropBorderColor(unpack(unitcolors[unittype]))
|
|
elseif C.nameplates.outenemy == "1" and strfind(unittype, "ENEMY") then
|
|
plate.health.backdrop:SetBackdropBorderColor(unpack(unitcolors[unittype]))
|
|
else
|
|
plate.health.backdrop:SetBackdropBorderColor(er,eg,eb,ea)
|
|
end
|
|
|
|
-- hide frames according to the configuration
|
|
local TotemIcon = TotemPlate(name)
|
|
|
|
if TotemIcon then
|
|
-- create totem icon
|
|
plate.totem.icon:SetTexture("Interface\\Icons\\" .. TotemIcon)
|
|
|
|
plate.glow:Hide()
|
|
plate.level:Hide()
|
|
plate.name:Hide()
|
|
plate.health:Hide()
|
|
plate.guild:Hide()
|
|
plate.totem:Show()
|
|
elseif HidePlate(unittype, name, (hpmax-hp == hpmin), target) then
|
|
plate.level:SetPoint("RIGHT", plate.name, "LEFT", -3, 0)
|
|
plate.name:SetParent(plate)
|
|
plate.guild:SetPoint("BOTTOM", plate.name, "BOTTOM", -2, -(font_size + 2))
|
|
|
|
plate.level:Show()
|
|
plate.name:Show()
|
|
plate.health:Hide()
|
|
if guild and C.nameplates.showguildname == "1" then
|
|
plate.glow:SetPoint("CENTER", plate.name, "CENTER", 0, -(font_size / 2) - 2)
|
|
else
|
|
plate.glow:SetPoint("CENTER", plate.name, "CENTER", 0, 0)
|
|
end
|
|
plate.totem:Hide()
|
|
else
|
|
plate.level:SetPoint("RIGHT", plate.health, "LEFT", -5, 0)
|
|
plate.name:SetParent(plate.health)
|
|
plate.guild:SetPoint("BOTTOM", plate.health, "BOTTOM", 0, -(font_size + 4))
|
|
|
|
plate.level:Show()
|
|
plate.name:Show()
|
|
plate.health:Show()
|
|
plate.glow:SetPoint("CENTER", plate.health, "CENTER", 0, 0)
|
|
plate.totem:Hide()
|
|
end
|
|
|
|
-- Gate level SetText behind a (level, elite) cache. string.format
|
|
-- here was firing every OnDataChanged tick per plate; with the cache
|
|
-- it only re-formats on real changes (level up / elite-state flip).
|
|
if plate.cache.level ~= level or plate.cache.elite ~= elite then
|
|
plate.cache.level = level
|
|
plate.cache.elite = elite
|
|
plate.level:SetText(string.format("%s%s", level, (elitestrings[elite] or "")))
|
|
end
|
|
|
|
-- Set level color from GetDifficultyColor when using DB level
|
|
if levelFromDB and type(level) == "number" then
|
|
local color = GetDifficultyColor(level)
|
|
plate.level:SetTextColor(color.r + 0.3, color.g + 0.3, color.b + 0.3, 1)
|
|
end
|
|
|
|
if guild and C.nameplates.showguildname == "1" then
|
|
plate.guild:SetText(guild)
|
|
if UnitIsInMyGuild(plate.unit) then
|
|
plate.guild:SetTextColor(0, 0.9, 0, 1)
|
|
else
|
|
plate.guild:SetTextColor(0.8, 0.8, 0.8, 1)
|
|
end
|
|
plate.guild:Show()
|
|
else
|
|
plate.guild:Hide()
|
|
end
|
|
|
|
-- PERF: Only update bar + HP text when values actually changed
|
|
if plate.cache.hp ~= hp or plate.cache.hpmax ~= hpmax then
|
|
plate.cache.hp = hp
|
|
plate.cache.hpmax = hpmax
|
|
plate.health:SetMinMaxValues(hpmin, hpmax)
|
|
plate.health:SetValue(hp)
|
|
|
|
if cfg.showhp then
|
|
local rhp, rhpmax, estimated
|
|
local unit = plate.unit
|
|
if unit then
|
|
local npHp = UnitHealth(unit)
|
|
local npMaxHp = UnitHealthMax(unit)
|
|
if npHp and npHp > 0 and npMaxHp and npMaxHp > 0 and npMaxHp ~= 100 then
|
|
rhp, rhpmax = npHp, npMaxHp
|
|
end
|
|
end
|
|
|
|
-- Fallback to existing methods
|
|
if not rhp then
|
|
if hpmax > 100 or (round(hpmax/100*hp) ~= hp) then
|
|
rhp, rhpmax = hp, hpmax
|
|
elseif pfUI.libhealth and pfUI.libhealth.enabled then
|
|
rhp, rhpmax, estimated = pfUI.libhealth:GetUnitHealthByName(name,level,tonumber(hp),tonumber(hpmax))
|
|
end
|
|
end
|
|
|
|
local setting = cfg.hptextformat
|
|
local hasdata = ( rhp and rhpmax ) or estimated or hpmax > 100 or (round(hpmax/100*hp) ~= hp)
|
|
|
|
if setting == "curperc" and hasdata and rhp then
|
|
plate.health.text:SetText(string.format("%s | %s%%", Abbreviate(rhp), ceil(hp/hpmax*100)))
|
|
elseif setting == "cur" and hasdata and rhp then
|
|
plate.health.text:SetText(string.format("%s", Abbreviate(rhp)))
|
|
elseif setting == "curmax" and hasdata and rhp then
|
|
plate.health.text:SetText(string.format("%s - %s", Abbreviate(rhp), Abbreviate(rhpmax)))
|
|
elseif setting == "curmaxs" and hasdata and rhp then
|
|
plate.health.text:SetText(string.format("%s / %s", Abbreviate(rhp), Abbreviate(rhpmax)))
|
|
elseif setting == "curmaxperc" and hasdata and rhp then
|
|
plate.health.text:SetText(string.format("%s - %s | %s%%", Abbreviate(rhp), Abbreviate(rhpmax), ceil(hp/hpmax*100)))
|
|
elseif setting == "curmaxpercs" and hasdata and rhp then
|
|
plate.health.text:SetText(string.format("%s / %s | %s%%", Abbreviate(rhp), Abbreviate(rhpmax), ceil(hp/hpmax*100)))
|
|
elseif setting == "deficit" and rhp then
|
|
plate.health.text:SetText(string.format("-%s" .. (hasdata and "" or "%%"), Abbreviate(rhpmax - rhp)))
|
|
else -- "percent" as fallback
|
|
plate.health.text:SetText(string.format("%s%%", ceil(hp/hpmax*100)))
|
|
end
|
|
else
|
|
plate.health.text:SetText()
|
|
end
|
|
end -- hp cache gate
|
|
|
|
local r, g, b, a = unpack(unitcolors[unittype])
|
|
|
|
if class then
|
|
if unittype == "ENEMY_PLAYER" and C.nameplates["enemyclassc"] == "1" then
|
|
r, g, b, a = PFUI_CLASS_COLORS[class]:GetRGBA()
|
|
elseif unittype == "FRIENDLY_PLAYER" and C.nameplates["friendclassc"] == "1" then
|
|
r, g, b, a = PFUI_CLASS_COLORS[class]:GetRGBA()
|
|
end
|
|
end
|
|
|
|
if unitstr and UnitIsTapped(unitstr) and not UnitIsTappedByPlayer(unitstr) then
|
|
r, g, b, a = .5, .5, .5, .8
|
|
end
|
|
|
|
if cfg.barcombatstate then
|
|
local guid = plate.cachedGuid or ""
|
|
local color = GetCombatStateColor(guid, plate.unit)
|
|
|
|
if color then
|
|
r, g, b, a = color.r, color.g, color.b, color.a
|
|
end
|
|
end
|
|
|
|
if r ~= plate.cache.r or g ~= plate.cache.g or b ~= plate.cache.b then
|
|
plate.health:SetStatusBarColor(r, g, b, a)
|
|
plate.cache.r, plate.cache.g, plate.cache.b = r, g, b
|
|
end
|
|
|
|
if r + g + b ~= plate.cache.namecolor and unittype == "FRIENDLY_PLAYER" and C.nameplates["friendclassnamec"] == "1" and class and PFUI_CLASS_COLORS[class] then
|
|
plate.name:SetTextColor(r, g, b, a)
|
|
plate.cache.namecolor = r + g + b
|
|
end
|
|
|
|
-- update combopoints
|
|
for i=1, 5 do plate.combopoints[i]:Hide() end
|
|
if target and C.nameplates.cpdisplay == "1" then
|
|
for i=1, GetComboPoints("target") do plate.combopoints[i]:Show() end
|
|
end
|
|
|
|
-- update debuffs
|
|
local index = 1
|
|
|
|
local isFriendly = unittype == "FRIENDLY_PLAYER" or unittype == "FRIENDLY_NPC"
|
|
local showDebuffsForType = cfg.showdebuffs and (isFriendly and cfg.showdebuffs_friendly or (not isFriendly and cfg.showdebuffs_hostile))
|
|
if showDebuffsForType then
|
|
-- Pull debuffs from C_UnitAuras (HARMFUL range). owndebuffs adds the
|
|
-- PLAYER filter token so only auras whose caster GUID matches the local
|
|
-- player come through. debuffDisplayBuf is a module-level reusable
|
|
-- buffer (no GC churn).
|
|
local debuffCount = 0
|
|
for i = 1, 16 do debuffDisplayBuf[i].effect = nil end
|
|
if unitstr then
|
|
local filter = cfg.owndebuffs and "HARMFUL|PLAYER" or "HARMFUL"
|
|
local auras = C_UnitAuras.GetUnitAuras(unitstr, filter)
|
|
local now = GetTime()
|
|
for _, aura in ipairs(auras) do
|
|
if debuffCount >= 16 then break end
|
|
debuffCount = debuffCount + 1
|
|
local timeleft = (aura.expirationTime and aura.expirationTime > 0) and (aura.expirationTime - now) or nil
|
|
local b = debuffDisplayBuf[debuffCount]
|
|
b.effect = aura.name
|
|
b.texture = aura.icon
|
|
b.stacks = aura.applications
|
|
b.dtype = aura.dispelName
|
|
b.duration = aura.duration
|
|
b.timeleft = timeleft
|
|
end
|
|
end
|
|
for i = 1, 16 do
|
|
local effect, texture, stacks, dtype, duration, timeleft
|
|
if debuffDisplayBuf[i].effect then
|
|
local b = debuffDisplayBuf[i]
|
|
effect, texture, stacks, dtype, duration, timeleft = b.effect, b.texture, b.stacks, b.dtype, b.duration, b.timeleft
|
|
end
|
|
|
|
if effect and texture and DebuffFilter(effect) then
|
|
if not plate.debuffs[index] then
|
|
CreateDebuffIcon(plate, index)
|
|
UpdateDebuffConfig(plate, index)
|
|
end
|
|
|
|
plate.debuffs[index]:Show()
|
|
plate.debuffs[index].icon:SetTexture(texture)
|
|
plate.debuffs[index].icon:SetTexCoord(.078, .92, .079, .937)
|
|
|
|
if stacks and stacks > 1 and C.nameplates.debuffs["showstacks"] == "1" then
|
|
plate.debuffs[index].stacks:SetText(stacks)
|
|
plate.debuffs[index].stacks:Show()
|
|
else
|
|
plate.debuffs[index].stacks:Hide()
|
|
end
|
|
|
|
if duration and timeleft and cfg.debufftimers then
|
|
-- C_UnitAuras returns the Spell.dbc base duration, which talents
|
|
-- can extend past — Shadow Affinity bumps SW:P from 18s to 24s,
|
|
-- so a fresh cast has timeleft > duration and `now + timeleft -
|
|
-- duration` lands in the future, which CooldownFrame_SetTimer
|
|
-- treats as "not yet started" (no swirl). Widen to whichever is
|
|
-- larger so start <= now.
|
|
local effDuration = duration > timeleft and duration or timeleft
|
|
plate.cdCache = plate.cdCache or {}
|
|
local newStart = GetTime() + timeleft - effDuration
|
|
local slotCache = plate.cdCache[index]
|
|
local cachedStart = slotCache and slotCache.effect == effect and slotCache.start
|
|
local cd = plate.debuffs[index].cd
|
|
cd:Show()
|
|
if not cachedStart or abs(cachedStart - newStart) > 0.5 then
|
|
if not cd.configCached or cd.cachedAnim ~= cfg.debuffanim or cd.cachedText ~= cfg.debufftext then
|
|
cd.pfCooldownStyleAnimation = cfg.debuffanim
|
|
cd.pfCooldownStyleText = cfg.debufftext
|
|
cd:SetAlpha(cfg.debuffanim == 1 and 1 or 0)
|
|
cd.cachedAnim = cfg.debuffanim
|
|
cd.cachedText = cfg.debufftext
|
|
cd.configCached = true
|
|
end
|
|
CooldownFrame_SetTimer(cd, newStart, effDuration, 1)
|
|
plate.cdCache[index] = plate.cdCache[index] or {}
|
|
plate.cdCache[index].effect = effect
|
|
plate.cdCache[index].start = newStart
|
|
end
|
|
end
|
|
|
|
index = index + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
-- hide remaining debuffs
|
|
for i = index, 16 do
|
|
if plate.debuffs[i] then
|
|
plate.debuffs[i]:Hide()
|
|
end
|
|
end
|
|
end
|
|
|
|
nameplates.OnShow = function(frame)
|
|
nameplates:OnDataChanged((frame or this).nameplate)
|
|
end
|
|
|
|
nameplates.OnUpdate = function(frame, state)
|
|
local nameplate = frame.nameplate
|
|
local now = state and state.now or GetTime()
|
|
|
|
-- cachedGuid is maintained by NAME_PLATE_UNIT_ADDED / _REMOVED events.
|
|
|
|
-- PERF: Intelligent throttling based on target/castbar status and plate count
|
|
-- Use GUID comparison as primary target detection: instant, immune to alpha transitions,
|
|
-- and immediately correct on de-target (unlike istarget which updates one tick later)
|
|
local targetGuid = state and state.targetGuid
|
|
local target = (targetGuid and nameplate.cachedGuid and targetGuid == nameplate.cachedGuid) or
|
|
(state and state.targetGuid and frame:GetAlpha() >= 0.99) or nil
|
|
-- Target plate castbar runs on its own dedicated frame (nameplates.castbarFrame).
|
|
-- For non-target plates with castbar active, use castbar throttle to ensure
|
|
-- smooth animation without overloading the central loop.
|
|
local isCastingNonTarget = not target and nameplate.castbar and nameplate.castbar:IsShown()
|
|
if not isCastingNonTarget and not target and cfg.showcastbar and nameplate.cachedGuid then
|
|
local castInfo = GetCastInfo(nameplate.unit)
|
|
if castInfo and castInfo.endTime > now then
|
|
isCastingNonTarget = true
|
|
end
|
|
end
|
|
|
|
-- hide castbar before throttle if no cast active
|
|
if not isCastingNonTarget and not target then
|
|
if nameplate.castbar.isShown then
|
|
nameplate.castbar.isShown = nil
|
|
nameplate.castbar.lastEndTime = nil
|
|
nameplate.castbar:Hide()
|
|
end
|
|
end
|
|
|
|
local throttle
|
|
if target then
|
|
throttle = pfUI.throttle:Get("nameplates_target")
|
|
elseif visiblePlateCount > 20 then
|
|
throttle = pfUI.throttle:Get("nameplates_mass")
|
|
else
|
|
throttle = pfUI.throttle:Get("nameplates")
|
|
end
|
|
|
|
-- Non-target plates with active castbar use the castbar throttle
|
|
if isCastingNonTarget then
|
|
local cbThrottle = pfUI.throttle:Get("nameplates_castbar")
|
|
if cbThrottle < throttle then throttle = cbThrottle end
|
|
end
|
|
|
|
-- Check for pending event updates (these bypass throttle for immediate response)
|
|
local hasEventUpdate = nameplate.eventcache or nameplate.auraUpdate or nameplate.castUpdate or nameplate.targetUpdate or nameplate.comboUpdate
|
|
|
|
-- Event updates bypass throttle
|
|
if not hasEventUpdate and (nameplate.lasttick or 0) + throttle > now then return end
|
|
nameplate.lasttick = now
|
|
|
|
-- =========================================================================
|
|
-- EVERYTHING BELOW RUNS AT THROTTLED RATE (50 FPS target, 10 FPS others)
|
|
-- =========================================================================
|
|
|
|
local update
|
|
local original = nameplate.original
|
|
local name = original.name:GetText()
|
|
local mouseover = nameplate.cachedGuid and nameplate.cachedGuid == frameState.mouseoverGuid or nil
|
|
|
|
-- trigger queued event update
|
|
if hasEventUpdate then
|
|
nameplates:OnDataChanged(nameplate)
|
|
nameplate.eventcache = nil
|
|
nameplate.auraUpdate = nil
|
|
nameplate.castUpdate = nil
|
|
nameplate.targetUpdate = nil
|
|
nameplate.comboUpdate = nil
|
|
end
|
|
|
|
-- =========================================================================
|
|
-- OVERLAP/CLICKTHROUGH HANDLING
|
|
-- =========================================================================
|
|
local useOverlap = C.nameplates["overlap"] == "1" or C.nameplates["vertical_offset"] ~= "0"
|
|
local clickable = C.nameplates["clickthrough"] ~= "1"
|
|
|
|
if not clickable then
|
|
frame:EnableMouse(false)
|
|
nameplate:EnableMouse(false)
|
|
else
|
|
local plate = useOverlap and nameplate or frame
|
|
plate:EnableMouse(clickable)
|
|
end
|
|
|
|
if C.nameplates["overlap"] == "1" then
|
|
if frame:GetWidth() > 1 then
|
|
frame:SetSize(1, 1)
|
|
end
|
|
else
|
|
if not nameplate.dwidth then
|
|
nameplate.dwidth = floor(nameplate:GetWidth() * UIParent:GetScale())
|
|
end
|
|
|
|
if floor(frame:GetWidth()) ~= nameplate.dwidth then
|
|
local nameW, nameH = nameplate:GetSize()
|
|
local uiScale = UIParent:GetScale()
|
|
frame:SetSize(nameW * uiScale, nameH * uiScale)
|
|
end
|
|
end
|
|
|
|
local mouseEnabled = nameplate:IsMouseEnabled()
|
|
if C.nameplates["clickthrough"] == "0" and C.nameplates["overlap"] == "1" and SpellIsTargeting() == mouseEnabled then
|
|
nameplate:EnableMouse(not mouseEnabled)
|
|
end
|
|
|
|
-- Cache strata changes
|
|
if nameplate.istarget ~= target then
|
|
nameplate.target_strata = nil
|
|
end
|
|
|
|
if target and nameplate.target_strata ~= 1 then
|
|
nameplate:SetFrameStrata("LOW")
|
|
nameplate.target_strata = 1
|
|
elseif not target and nameplate.target_strata ~= 0 then
|
|
nameplate:SetFrameStrata("BACKGROUND")
|
|
nameplate.target_strata = 0
|
|
end
|
|
|
|
nameplate.istarget = target
|
|
|
|
-- Set non-target plate alpha
|
|
local configAlpha = cfg.notargalpha or 0.5
|
|
local desiredAlpha = (target or not state.targetGuid) and 1 or configAlpha
|
|
|
|
if nameplate.cachedAlpha ~= desiredAlpha then
|
|
nameplate:SetAlpha(desiredAlpha)
|
|
nameplate.cachedAlpha = desiredAlpha
|
|
end
|
|
|
|
-- queue update on visual target update
|
|
if nameplate.cache.target ~= target then
|
|
nameplate.cache.target = target
|
|
update = true
|
|
end
|
|
|
|
-- queue update on visual mouseover update
|
|
if nameplate.cache.mouseover ~= mouseover then
|
|
nameplate.cache.mouseover = mouseover
|
|
update = true
|
|
end
|
|
|
|
-- trigger update when unit was found. Pass the cached player hint so the
|
|
-- retry probes the same table OnDataChanged will read on the next pass —
|
|
-- otherwise an NPC sharing a name with a known player flips wait_for_scan
|
|
-- off here, then OnDataChanged re-sets it, every frame until the mob scan
|
|
-- lands.
|
|
if nameplate.wait_for_scan and GetUnitInfo(name, true, nameplate.cache.player) then
|
|
nameplate.wait_for_scan = nil
|
|
update = true
|
|
end
|
|
|
|
-- trigger update when name color changed (includes combat state check)
|
|
local r, g, b = original.name:GetTextColor()
|
|
local inCombatWithPlayer = cfg.namefightcolor and UnitAffectingCombat(nameplate.unit) and UnitAffectingCombat("player")
|
|
|
|
if r + g + b ~= nameplate.cache.namecolor or (cfg.namefightcolor and nameplate.cache.inCombat ~= inCombatWithPlayer) then
|
|
nameplate.cache.namecolor = r + g + b
|
|
nameplate.cache.inCombat = inCombatWithPlayer
|
|
|
|
if cfg.namefightcolor then
|
|
if (r > .9 and g < .2 and b < .2) or inCombatWithPlayer then
|
|
nameplate.name:SetTextColor(1,0.4,0.2,1)
|
|
else
|
|
nameplate.name:SetTextColor(r,g,b,1)
|
|
end
|
|
else
|
|
nameplate.name:SetTextColor(1,1,1,1)
|
|
end
|
|
update = true
|
|
end
|
|
|
|
-- trigger update when level color changed
|
|
local r, g, b = original.level:GetTextColor()
|
|
r, g, b = r + .3, g + .3, b + .3
|
|
if r + g + b ~= nameplate.cache.levelcolor then
|
|
nameplate.cache.levelcolor = r + g + b
|
|
nameplate.level:SetTextColor(r,g,b,1)
|
|
update = true
|
|
end
|
|
|
|
-- use timer based updates
|
|
if not nameplate.tick or nameplate.tick < now then
|
|
update = true
|
|
end
|
|
|
|
-- run full updates if required
|
|
if update then
|
|
nameplates:OnDataChanged(nameplate)
|
|
nameplate.tick = now + .5
|
|
end
|
|
|
|
-- Zoom animation
|
|
if target and cfg.targetzoom then
|
|
if not nameplate.health.zoomed then
|
|
local zoomval = cfg.zoomval
|
|
local wc = cfg.width * zoomval
|
|
local hc = cfg.heighthealth * (zoomval * .9)
|
|
nameplate.health.targetWidth = wc
|
|
nameplate.health.targetHeight = hc
|
|
end
|
|
|
|
local w, h = nameplate.health:GetSize()
|
|
local wc, hc = nameplate.health.targetWidth, nameplate.health.targetHeight
|
|
|
|
if wc and hc then
|
|
if wc > w + 0.5 then
|
|
nameplate.health:SetWidth(w*1.05)
|
|
nameplate.health.zoomTransition = true
|
|
elseif hc > h + 0.5 then
|
|
nameplate.health:SetHeight(h*1.05)
|
|
nameplate.health.zoomTransition = true
|
|
else
|
|
if nameplate.health.zoomTransition then
|
|
nameplate.health:SetSize(wc, hc)
|
|
nameplate.health.zoomTransition = nil
|
|
end
|
|
nameplate.health.zoomed = true
|
|
end
|
|
end
|
|
elseif nameplate.health.zoomed or nameplate.health.zoomTransition then
|
|
local w, h = nameplate.health:GetSize()
|
|
local wc = cfg.width
|
|
local hc = cfg.heighthealth
|
|
|
|
if w > wc + 0.5 then
|
|
nameplate.health:SetWidth(w*.95)
|
|
elseif h > hc + 0.5 then
|
|
nameplate.health:SetHeight(h*0.95)
|
|
else
|
|
nameplate.health:SetSize(wc, hc)
|
|
nameplate.health.zoomTransition = nil
|
|
nameplate.health.zoomed = nil
|
|
nameplate.health.targetWidth = nil
|
|
nameplate.health.targetHeight = nil
|
|
end
|
|
end
|
|
|
|
-- Target plate castbar is handled by nameplates.castbarFrame (dedicated OnUpdate,
|
|
-- engine framerate, decoupled from central loop). Only update non-target castbars here.
|
|
local isTargetPlate = target or nameplate.istarget or (nameplate.health and nameplate.health.zoomed)
|
|
if cfg.showcastbar and not cfg.targetcastbar and not isTargetPlate then
|
|
local cbThrottle = pfUI.throttle:Get("nameplates_castbar")
|
|
if visiblePlateCount > 20 then
|
|
local massThrottle = pfUI.throttle:Get("nameplates_mass")
|
|
if massThrottle > cbThrottle then cbThrottle = massThrottle end
|
|
end
|
|
if (nameplate.castbar_tick or 0) + cbThrottle <= now then
|
|
nameplate.castbar_tick = now
|
|
nameplates.UpdateCastbar(nameplate, now)
|
|
end
|
|
elseif cfg.showcastbar and cfg.targetcastbar and not isTargetPlate then
|
|
if nameplate.castbar.isShown then
|
|
nameplate.castbar.isShown = nil
|
|
nameplate.castbar.lastEndTime = nil
|
|
nameplate.castbar:Hide()
|
|
end
|
|
elseif not cfg.showcastbar then
|
|
if nameplate.castbar.isShown then
|
|
nameplate.castbar.isShown = nil
|
|
nameplate.castbar.lastEndTime = nil
|
|
nameplate.castbar:Hide()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- DEDICATED TARGET CASTBAR FRAME
|
|
-- Runs on its own OnUpdate, decoupled from the central nameplate loop.
|
|
-- Mirrors the approach used in castbar.lua for the target unit frame castbar.
|
|
-- ============================================================================
|
|
|
|
-- Skip SetText when the rounded value hasn't changed since last frame.
|
|
-- `string.format("%.2f", ...)` allocates a fresh string every call; this
|
|
-- gate compares cheap integers and only formats when the displayed value
|
|
-- would actually differ. Reset via lastEndTime-changed branch above.
|
|
local function SetCastbarText(castbar, remaining)
|
|
local rounded
|
|
if C.unitframes.castbardecimals == "1" then
|
|
rounded = floor(remaining * 10)
|
|
if castbar.lastTextTick ~= rounded then
|
|
castbar.lastTextTick = rounded
|
|
castbar.text:SetText(rounded / 10)
|
|
end
|
|
else
|
|
rounded = floor(remaining * 100)
|
|
if castbar.lastTextTick ~= rounded then
|
|
castbar.lastTextTick = rounded
|
|
castbar.text:SetText(string.format("%.2f", remaining))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Shared castbar update logic (used by both dedicated frame and central loop)
|
|
nameplates.UpdateCastbar = function(nameplate, now)
|
|
if not nameplate or not nameplate.castbar then return end
|
|
local castInfo = GetCastInfo(nameplate.unit)
|
|
if not castInfo or castInfo.endTime < now then
|
|
nameplate.castbar.isShown = nil
|
|
nameplate.castbar.lastEndTime = nil
|
|
nameplate.castbar:Hide()
|
|
return
|
|
end
|
|
|
|
local isChannel = castInfo.isChannel
|
|
local duration = castInfo.duration
|
|
if nameplate.castbar.lastEndTime ~= castInfo.endTime then
|
|
nameplate.castbar.lastEndTime = castInfo.endTime
|
|
nameplate.castbar.lastTextTick = nil
|
|
-- Relative 0..duration range to avoid float precision loss with large
|
|
-- absolute timestamps.
|
|
nameplate.castbar:SetMinMaxValues(0, duration)
|
|
nameplate.castbar:SetStatusBarColor(GetStringColor(C.appearance.castbar[(isChannel and "channelcolor" or "castbarcolor")]))
|
|
if castInfo.icon then
|
|
nameplate.castbar.icon.tex:SetTexture(castInfo.icon)
|
|
nameplate.castbar.icon.tex:SetTexCoord(.1,.9,.1,.9)
|
|
end
|
|
if cfg.spellname then
|
|
nameplate.castbar.spell:SetText(castInfo.spellName)
|
|
else
|
|
nameplate.castbar.spell:SetText("")
|
|
end
|
|
end
|
|
|
|
local barValue = isChannel and (castInfo.endTime - now) or (now - castInfo.startTime)
|
|
if barValue < 0 then barValue = 0 end
|
|
if barValue > duration then barValue = duration end
|
|
nameplate.castbar:SetValue(barValue)
|
|
SetCastbarText(nameplate.castbar, castInfo.endTime - now)
|
|
if not nameplate.castbar.isShown then nameplate.castbar.isShown = true; nameplate.castbar:Show() end
|
|
end
|
|
|
|
-- Dedicated frame that updates ONLY the target plate castbar. Unthrottled:
|
|
-- now that casts are event-driven, this just reads the cache + SetValue, so
|
|
-- it animates the fill every frame for the smoothest sweep on the bar the
|
|
-- player watches most. (Non-target plates stay throttled via the central
|
|
-- loop's nameplates_castbar gate.)
|
|
nameplates.castbarFrame = CreateFrame("Frame", nil, UIParent)
|
|
nameplates.castbarFrame:SetScript("OnUpdate", function()
|
|
if not cfg.showcastbar or not frameState.targetGuid then return end
|
|
local nameplate = plateByGuid[frameState.targetGuid]
|
|
if not nameplate then return end
|
|
nameplates.UpdateCastbar(nameplate, GetTime())
|
|
end)
|
|
|
|
-- set nameplate game settings
|
|
nameplates.SetGameVariables = function()
|
|
-- update visibility (hostile)
|
|
if C.nameplates["showhostile"] == "1" then
|
|
_G.NAMEPLATES_ON = true
|
|
ShowNameplates()
|
|
else
|
|
_G.NAMEPLATES_ON = nil
|
|
HideNameplates()
|
|
end
|
|
|
|
-- update visibility (hostile)
|
|
if C.nameplates["showfriendly"] == "1" then
|
|
_G.FRIENDNAMEPLATES_ON = true
|
|
ShowFriendNameplates()
|
|
else
|
|
_G.FRIENDNAMEPLATES_ON = nil
|
|
HideFriendNameplates()
|
|
end
|
|
end
|
|
|
|
nameplates:SetGameVariables()
|
|
|
|
nameplates.UpdateConfig = function()
|
|
-- Refresh config cache for all cfg.* values
|
|
CacheConfig()
|
|
RebuildOfftanks()
|
|
|
|
-- update debuff filters
|
|
DebuffFilterPopulate()
|
|
|
|
-- Check friendly zone state when config changes
|
|
local disableHostile = C.nameplates["disable_hostile_in_friendly"] == "1"
|
|
local disableFriendly = C.nameplates["disable_friendly_in_friendly"] == "1"
|
|
local pvpType = GetZonePVPInfo()
|
|
local nowFriendly = (pvpType == "friendly")
|
|
|
|
if nowFriendly and (disableHostile or disableFriendly) then
|
|
if not inFriendlyZone then
|
|
-- Just entered friendly zone or feature just enabled
|
|
inFriendlyZone = true
|
|
savedHostileState = C.nameplates["showhostile"]
|
|
savedFriendlyState = C.nameplates["showfriendly"]
|
|
end
|
|
|
|
-- Apply current settings based on options
|
|
if disableHostile then
|
|
_G.NAMEPLATES_ON = nil
|
|
HideNameplates()
|
|
else
|
|
-- Restore hostile if option is off but we're in friendly zone
|
|
if savedHostileState == "1" then
|
|
_G.NAMEPLATES_ON = true
|
|
ShowNameplates()
|
|
end
|
|
end
|
|
|
|
if disableFriendly then
|
|
_G.FRIENDNAMEPLATES_ON = nil
|
|
HideFriendNameplates()
|
|
else
|
|
-- Restore friendly if option is off but we're in friendly zone
|
|
if savedFriendlyState == "1" then
|
|
_G.FRIENDNAMEPLATES_ON = true
|
|
ShowFriendNameplates()
|
|
end
|
|
end
|
|
|
|
return -- Don't call SetGameVariables
|
|
elseif inFriendlyZone and not (disableHostile or disableFriendly) then
|
|
-- Both features disabled while in friendly zone - restore state
|
|
inFriendlyZone = false
|
|
|
|
if savedHostileState == "1" then
|
|
C.nameplates["showhostile"] = savedHostileState
|
|
end
|
|
|
|
if savedFriendlyState == "1" then
|
|
C.nameplates["showfriendly"] = savedFriendlyState
|
|
end
|
|
|
|
savedHostileState = nil
|
|
savedFriendlyState = nil
|
|
-- Fall through to SetGameVariables to restore nameplates
|
|
end
|
|
|
|
-- update nameplate visibility
|
|
nameplates:SetGameVariables()
|
|
|
|
-- apply all config changes
|
|
for plate in pairs(registry) do
|
|
nameplates.OnConfigChange(plate)
|
|
if plate.nameplate and plate.nameplate.cachedGuid then
|
|
nameplates:OnDataChanged(plate.nameplate)
|
|
end
|
|
end
|
|
end
|
|
|
|
local hookOnConfigChange = nameplates.OnConfigChange
|
|
nameplates.OnConfigChange = function(self)
|
|
hookOnConfigChange(self)
|
|
|
|
local parent = self
|
|
local nameplate = self.nameplate
|
|
local plate = (C.nameplates["overlap"] == "1" or C.nameplates["vertical_offset"] ~= "0") and nameplate or parent
|
|
|
|
-- disable all clicks for now
|
|
parent:EnableMouse(false)
|
|
nameplate:EnableMouse(false)
|
|
|
|
-- adjust vertical offset
|
|
if C.nameplates["vertical_offset"] ~= "0" then
|
|
nameplate:SetPoint("TOP", parent, "TOP", 0, tonumber(C.nameplates["vertical_offset"]))
|
|
end
|
|
|
|
-- replace clickhandler
|
|
if C.nameplates["overlap"] == "1" or C.nameplates["vertical_offset"] ~= "0" then
|
|
plate:SetScript("OnClick", function() parent:Click() end)
|
|
end
|
|
|
|
-- enable mouselook on rightbutton down
|
|
if C.nameplates["rightclick"] == "1" then
|
|
plate:SetScript("OnMouseDown", nameplates.mouselook.OnMouseDown)
|
|
else
|
|
plate:SetScript("OnMouseDown", nil)
|
|
end
|
|
end
|
|
|
|
local hookOnDataChanged = nameplates.OnDataChanged
|
|
nameplates.OnDataChanged = function(self, nameplate)
|
|
hookOnDataChanged(self, nameplate)
|
|
|
|
-- make sure to keep mouse events disabled on parent nameplate
|
|
if (C.nameplates["overlap"] == "1" or C.nameplates["vertical_offset"] ~= "0") then
|
|
nameplate.parent:EnableMouse(false)
|
|
end
|
|
end
|
|
|
|
-- enable mouselook on rightbutton down
|
|
nameplates.mouselook = CreateFrame("Frame", nil, UIParent)
|
|
nameplates.mouselook.time = nil
|
|
nameplates.mouselook.frame = nil
|
|
nameplates.mouselook.OnMouseDown = function()
|
|
if arg1 and arg1 == "RightButton" then
|
|
MouselookStart()
|
|
|
|
-- start detection of the rightclick emulation
|
|
nameplates.mouselook.time = GetTime()
|
|
nameplates.mouselook.frame = this
|
|
nameplates.mouselook:Show()
|
|
end
|
|
end
|
|
|
|
nameplates.mouselook:SetScript("OnUpdate", function()
|
|
-- break here if nothing to do
|
|
if not this.time or not this.frame then
|
|
this:Hide()
|
|
return
|
|
end
|
|
|
|
-- if threshold is reached (0.5 second) no click action will follow
|
|
if not IsMouselooking() and this.time + tonumber(C.nameplates["clickthreshold"]) < GetTime() then
|
|
this:Hide()
|
|
return
|
|
end
|
|
|
|
-- run a usual nameplate rightclick action
|
|
if not IsMouselooking() then
|
|
this.frame:Click("LeftButton")
|
|
if UnitCanAttack("player", "target") and not nameplates.combat.inCombat then AttackTarget() end
|
|
this:Hide()
|
|
return
|
|
end
|
|
end)
|
|
|
|
pfUI.nameplates = nameplates
|
|
end) |