Files
pfUI/api/unitframes.lua
T
brues-code 6b458c9e27 Modernize cast bars, unit-frame clicks, and mark tracking with ClassicAPI (#32)
* 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.
2026-08-04 14:27:15 -05:00

2623 lines
96 KiB
Lua

-- load pfUI environment
setfenv(1, pfUI:GetEnvironment())
pfUI.uf = CreateFrame("Frame", nil, UIParent)
pfUI.uf.frames = {}
-- ============================================================================
-- GUID-based Roster Tracking for Smart Updates
-- Only updates frames where the unit actually changed, not ALL 40 frames
-- ============================================================================
pfUI.uf.guidTracker = {
-- Maps frame to its last known GUID: frame -> guid
frameToGuid = {},
}
-- Clear all GUID tracking (forces full update next time)
function pfUI.uf.ClearGuidTracking()
pfUI.uf.guidTracker.frameToGuid = {}
end
-- slash command to toggle unitframe test mode
pfUI.api.RegisterSlashCommand("PFTEST", { "/pftest", "/pfuftest" }, function()
pfUI.uf.showall = not pfUI.uf.showall
if pfUI.uf.raid and pfUI.uf.raid.LayoutPets then pfUI.uf.raid:LayoutPets() end
end, true)
-- HoT buff indicators that need name verification because their icons are
-- reused by other spells. Maps icon (lowercased) → expected aura name +
-- libpredict key for the prediction integration.
local HOT_INDICATORS = {
[strlower(C_Spell.GetSpellTexture(774))] = { name = strlower(C_Spell.GetSpellName(774)), predict = "Reju" },
[strlower(C_Spell.GetSpellTexture(139))] = { name = strlower(C_Spell.GetSpellName(139)), predict = "Renew" },
[strlower(C_Spell.GetSpellTexture(8936))] = { name = strlower(C_Spell.GetSpellName(8936)), predict = "Regr" },
}
local glow = {
edgeFile = pfUI.media["img:glow"], edgeSize = 8,
insets = {left = 0, right = 0, top = 0, bottom = 0},
}
local glow2 = {
edgeFile = pfUI.media["img:glow2"], edgeSize = 8,
insets = {left = 0, right = 0, top = 0, bottom = 0},
}
local function DoNothing()
return
end
local function BuffOnEnter()
local parent = this:GetParent()
if not parent.label then return end
local unit = parent.label .. parent.id
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
GameTooltip:SetUnitAura(unit, this.id, "HELPFUL")
if IsShiftKeyDown() then
local aura = C_UnitAuras.GetAuraDataByIndex(unit, this.id, "HELPFUL")
local playerlist = aura and GetUnbuffedRoster(aura.name) or ""
if strlen(playerlist) > 0 then
GameTooltip:AddLine(" ")
GameTooltip:AddLine(T["Unbuffed"] .. ":", .3, 1, .8)
GameTooltip:AddLine(playerlist,1,1,1,1)
GameTooltip:Show()
end
end
end
local function BuffOnLeave()
GameTooltip:Hide()
end
local function BuffOnClick()
if this:GetParent().label == "player" then
local aura = C_UnitAuras.GetAuraDataByIndex("player", this.id, "HELPFUL")
if aura and aura.spellId then C_Spell.CancelSpellByID(aura.spellId) end
end
end
local function DebuffOnEnter()
if not this:GetParent().label then return end
local unitstr = this:GetParent().label .. this:GetParent().id
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
if this:GetParent().label ~= "player" then
local parent = this:GetParent()
-- selfdebuff filters the displayed list to player-cast harmful auras, but
-- SetUnitAura's index has to be into the engine's full HARMFUL list. Look
-- up the displayed aura via the PLAYER filter, then scan engine slots for
-- one whose name + sourceGUID match.
if parent.config and parent.config.selfdebuff == "1" then
local ownAura = C_UnitAuras.GetAuraDataByIndex(unitstr, this.id, "HARMFUL|PLAYER")
if ownAura then
for gameSlot = 1, 16 do
local check = C_UnitAuras.GetDebuffDataByIndex(unitstr, gameSlot)
if check and check.name == ownAura.name and check.sourceGUID == ownAura.sourceGUID then
GameTooltip:SetUnitAura(unitstr, gameSlot, "HARMFUL")
return
end
end
end
end
end
GameTooltip:SetUnitAura(unitstr, this.id, "HARMFUL")
end
local function DebuffOnLeave()
GameTooltip:Hide()
end
local function DebuffOnClick()
if this:GetParent().label == "player" then
local aura = C_UnitAuras.GetAuraDataByIndex("player", this.id, "HARMFUL")
if aura and aura.spellId then C_Spell.CancelSpellByID(aura.spellId) end
end
end
local visibilityscan = CreateFrame("Frame", "pfUnitFrameVisibility", UIParent)
visibilityscan.frames = {}
visibilityscan:SetScript("OnUpdate", function()
if ( this.limit or 1) > GetTime() then return else this.limit = GetTime() + .2 end
for frame in pairs(this.frames) do frame:UpdateVisibility() end
end)
-- ============================================================================
-- GetUnitStats - health + power for a unit token.
-- Returns: hp, maxHp, power, maxPower, powerType
-- ============================================================================
function pfUI.api.GetUnitStats(unitstr)
local powerType = UnitPowerType(unitstr) or 0
return UnitHealth(unitstr) or 0,
UnitHealthMax(unitstr) or 1,
UnitPower(unitstr, powerType) or 0,
UnitPowerMax(unitstr, powerType) or 1,
powerType
end
local aggrodata = { }
function pfUI.api.UnitHasAggro(unit)
-- Only cache positive results to allow instant detection when aggro changes
if aggrodata[unit] and aggrodata[unit].state > 0 and GetTime() < aggrodata[unit].check + 1 then
return aggrodata[unit].state
end
aggrodata[unit] = aggrodata[unit] or { }
aggrodata[unit].check = GetTime()
aggrodata[unit].state = 0
if UnitExists(unit) and UnitIsFriend(unit, "player") then
for u in pairs(pfValidUnits) do
local t = u .. "target"
local tt = t .. "target"
if UnitExists(t) and UnitIsUnit(t, unit) and UnitCanAttack(u, unit) then
aggrodata[unit].state = aggrodata[unit].state + 1
end
if UnitExists(tt) and UnitIsUnit(tt, unit) and UnitCanAttack(t, unit) then
aggrodata[unit].state = aggrodata[unit].state + 1
end
end
end
return aggrodata[unit].state
end
pfUI.uf.glow = CreateFrame("Frame", nil, UIParent)
pfUI.uf.glow:SetScript("OnUpdate", function()
local fpsmod = GetFramerate() / 30
if not this.val or this.val >= .8 then
this.mod = -0.01 / fpsmod
elseif this.val <= .4 then
this.mod = 0.01 / fpsmod
end
this.val = this.val + this.mod
end)
pfUI.uf.glow.mod = 0
pfUI.uf.glow.val = 0.6
function pfUI.uf.glow.UpdateGlowAnimation()
local val = pfUI.uf.glow.val or 0.6
if val < 0.4 then val = 0.4 end
if val > 0.8 then val = 0.8 end
this:SetAlpha(val)
end
function pfUI.uf:UpdateVisibility()
local self = self or this
-- cache result of strsub to avoid repeating calls
if not self.cache_raid then
if strsub(self:GetName(),0,6) == "pfRaid" then
self.cache_raid = tonumber(strsub(self:GetName(),7,8)) or 0
else
self.cache_raid = 0
end
end
-- show groupframes as raid
if self.cache_raid > 0 then
local id = self.cache_raid
-- always show self in raidframes
if not IsInGroup() and C.unitframes.selfinraid == "1" and id == 1 then
self.id = ""
self.label = "player"
-- use raidframes for groups
elseif not IsInRaid() and IsInGroup() and C.unitframes.raidforgroup == "1" then
if id == 1 then
self.id = ""
self.label = "player"
elseif id <= 5 then
self.id = id - 1
self.label = "party"
end
-- reset to regular raid unitstrings
elseif self.label == "party" or self.label == "player" then
self.id = id
self.label = "raid"
end
end
-- display every unit as player while pfUI.uf.showall is set
if pfUI.uf.showall then
self._label = self._label or self.label
self._id = self._id or self.id
self.label, self.id = "player", ""
elseif not pfUI.uf.showall and self._label and self._id then
self.label, self.id = self._label, self._id
self._label, self._id = nil, nil
end
local unitstr = string.format("%s%s", self.label or "", self.id or "")
self:SetAttribute("unit", unitstr ~= "" and unitstr or nil)
local visibility = string.format("[target=%s,exists] show; hide", unitstr)
-- Group frames are redundant when the group is already shown as a raid grid:
-- either an actual raid, or a party promoted to the raid grid via
-- raidforgroup. hide_in_raid gates whether we hide them in that case.
local hide_group = C["unitframes"]["group"]["hide_in_raid"] == "1"
and (IsInRaid() or (C.unitframes.raidforgroup == "1" and IsInGroup()))
if pfUI.unlock and pfUI.unlock:IsShown() then
-- display during unlock mode
visibility = "show"
self.visible = true
elseif self.config.visible == "0" then
-- frame shall not be visible
visibility = "hide"
self.visible = nil
elseif hide_group and self.cache_raid == 0 and self.label and strsub(self.label,0,5) == "party" then
-- hide group while shown as a raid grid and option is set (raid frames
-- carry label "party" under raidforgroup, so exclude them by cache_raid)
visibility = "hide"
self.visible = nil
elseif ( self.fname == "Group0" or self.fname == "PartyPet0" or self.fname == "Party0Target" )
and (not IsInGroup() or hide_group) then
-- hide self in group if solo or shown as a raid grid
visibility = "hide"
self.visible = nil
end
-- vanilla visibility
if self.unitname then
self:Show()
elseif visibility == "hide" then
self:Hide()
elseif visibility == "show" then
self:Show()
else
if UnitName(unitstr) then
-- hide existing but too far away pet and pets of old group members
if self.label == "partypet" then
if not UnitIsVisible(unitstr) or not UnitExists("party" .. self.id) then
self:Hide()
return
end
elseif self.label == "raidpet" then
if not UnitIsVisible(unitstr) or not UnitExists("raid" .. self.id) then
self:Hide()
return
end
elseif self.label == "pettarget" then
if not UnitIsVisible(unitstr) or not UnitExists("pet") then
self:Hide()
return
end
end
self:Show()
else
self.lastUnit = nil
self:Hide()
end
end
end
function pfUI.uf:UpdateFrameSize()
local rawborder, default_border = GetBorderSize("unitframes")
local spacing = self.config.pspace * GetPerfectPixel()
local width = self.config.width
local height = self.config.height
local pheight = self.config.pheight
local ptwidth = self.config.portraitwidth
local ptheight = self.config.portraitheight
local real_height = height + spacing + pheight + 2*default_border
if spacing ~= abs(spacing) and abs(spacing) > tonumber(pheight) then
real_height = height
spacing = 0
end
local portrait = 0
if self.config.portrait == "left" or self.config.portrait == "right" then
if ptwidth == "-1" and ptheight == "-1" then
-- align portrait size to frame
self.portrait:SetSize(real_height, real_height)
portrait = real_height + spacing + 2*default_border
else
-- use custom portrait size
self.portrait:SetSize(ptwidth, ptheight)
portrait = ptwidth + spacing + 2*default_border
end
end
self:SetSize(width + portrait, real_height)
end
function pfUI.uf:UpdateConfig()
local f = self
local C = pfUI_config
local rawborder, default_border = GetBorderSize("unitframes")
local spacing = f.config.pspace * GetPerfectPixel()
local cooldown_text = tonumber(f.config.cooldown_text)
local cooldown_anim = tonumber(f.config.cooldown_anim)
local relative_point = "BOTTOM"
if f.config.panchor == "TOPLEFT" then
relative_point = "BOTTOMLEFT"
elseif f.config.panchor == "TOPRIGHT" then
relative_point = "BOTTOMRIGHT"
end
f.dispellable = nil
f.indicators = nil
f.indicator_custom = nil
f.alpha_visible = tonumber(f.config.alpha_visible)
f.alpha_outrange = tonumber(f.config.alpha_outrange)
f.alpha_offline = tonumber(f.config.alpha_offline)
f:SetFrameStrata("MEDIUM")
f.glow:SetFrameStrata("BACKGROUND")
f.glow:SetFrameLevel(0)
f.glow:SetBackdrop(glow2)
f.glow:SetPoint("TOPLEFT", f, "TOPLEFT", -6 - default_border,6 + default_border)
f.glow:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 6 + default_border,-6 - default_border)
f.glow:SetScript("OnUpdate", pfUI.uf.glow.UpdateGlowAnimation)
f.glow:Hide()
f.combat:SetSize(tonumber(f.config.squaresize), tonumber(f.config.squaresize))
f.combat:ClearAllPoints()
f.combat:SetPoint(f.config.squarepos, 0, 0)
f.combat:Hide()
f.hp:ClearAllPoints()
f.hp:SetPoint("TOP", 0, 0)
f.hp:SetSize(f.config.width, f.config.height)
if tonumber(f.config.height) < 0 then f.hp:Hide() end
pfUI.api.CreateBackdrop(f.hp, default_border)
f.hp.bar:SetStatusBarTexture(pfUI.media[f.config.bartexture])
f.hp.bar:SetAllPoints(f.hp)
if f.config.verticalbar == "1" then
f.hp.bar:SetOrientation("VERTICAL")
else
f.hp.bar:SetOrientation("HORIZONTAL")
end
local custombg = f.config.defcolor == "0" and f.config.custombg or C.unitframes.custombg
local custombgcolor = f.config.defcolor == "0" and f.config.custombgcolor or C.unitframes.custombgcolor
if custombg == "1" then
local cr, cg, cb, ca = GetStringColor(custombgcolor)
cr, cg, cb, ca = tonumber(cr), tonumber(cg), tonumber(cb), tonumber(ca)
f.hp.bar:SetStatusBarBackgroundTexture(cr,cg,cb,ca)
end
f.power:ClearAllPoints()
f.power:SetPoint(f.config.panchor, f.hp, relative_point, f.config.poffx, -2 * default_border - spacing + f.config.poffy * GetPerfectPixel())
f.power:SetSize((f.config.pwidth ~= "-1" and f.config.pwidth or f.config.width), f.config.pheight)
if tonumber(f.config.pheight) < 0 then f.power:Hide() end
pfUI.api.CreateBackdrop(f.power, default_border)
f.power.bar:SetFrameLevel(f.power:GetFrameLevel() + 1)
f.power.bar:SetStatusBarTexture(pfUI.media[f.config.pbartexture])
f.power.bar:SetAllPoints(f.power)
local custompbg = f.config.defcolor == "0" and f.config.custompbg or C.unitframes.custompbg
local custompbgcolor = f.config.defcolor == "0" and f.config.custompbgcolor or C.unitframes.custompbgcolor
if custompbg == "1" then
local cr, cg, cb, ca = GetStringColor(custompbgcolor)
cr, cg, cb, ca = tonumber(cr), tonumber(cg), tonumber(cb), tonumber(ca)
f.power.bar:SetStatusBarBackgroundTexture(cr,cg,cb,ca)
end
local fontname, fontsize, fontstyle
if f.config.customfont == "1" then
fontname = pfUI.media[f.config.customfont_name]
fontsize = tonumber(f.config.customfont_size)
fontstyle = f.config.customfont_style
else
fontname = pfUI.font_unit
fontsize = tonumber(C.global.font_unit_size)
fontstyle = C.global.font_unit_style
end
-- Druid secondary mana bar: texture/color/size/position below the power bar,
-- using its own C.unitframes.druidmana* config. Values are read in
-- UpdateDruidMana; here we only lay it out.
if f.druidmana then
local DC = C.unitframes
local dmTexture = DC.druidmanatexture or "Interface\\AddOns\\pfUI\\img\\bar"
f.druidmana:SetStatusBarTexture(pfUI.media[dmTexture] or dmTexture)
f.druidmana:SetFrameLevel(f:GetFrameLevel() + 5)
local manacolor = f.config.defcolor == "0" and f.config.manacolor or C.unitframes.manacolor
f.druidmana:SetStatusBarColor(GetStringColor(manacolor))
local dmHeight = tonumber(DC.druidmanaheight) or 10
local dmWidth = DC.druidmanawidth or "-1"
local dmOffX = tonumber(DC.druidmanaoffx) or 0
local dmOffY = tonumber(DC.druidmanaoffy) or 0
local dmSpace = tonumber(DC.druidmanaspace) or -3
local dmSpacing = -2 * default_border - dmSpace
f.druidmana:SetHeight(dmHeight)
f.druidmana:ClearAllPoints()
local w = dmWidth ~= "-1" and tonumber(dmWidth) or nil
if w then
f.druidmana:SetWidth(w)
f.druidmana:SetPoint("TOP", f.power, "BOTTOM", dmOffX, dmSpacing + dmOffY)
else
f.druidmana:SetPoint("TOPLEFT", f.power, "BOTTOMLEFT", dmOffX, dmSpacing + dmOffY)
f.druidmana:SetPoint("TOPRIGHT", f.power, "BOTTOMRIGHT", dmOffX, dmSpacing + dmOffY)
end
if not f.druidmana._hasbd then
CreateBackdrop(f.druidmana, default_border)
CreateBackdropShadow(f.druidmana)
f.druidmana._hasbd = true
end
local tr, tg, tb = ManaBarColor[Enum.PowerType.Mana].r, ManaBarColor[Enum.PowerType.Mana].g, ManaBarColor[Enum.PowerType.Mana].b
if C.unitframes.pastel == "1" then
tr, tg, tb = (tr + .75) * .5, (tg + .75) * .5, (tb + .75) * .5
end
f.druidmana.text:SetFont(fontname, fontsize, fontstyle)
f.druidmana.text:SetTextColor(tr, tg, tb, 1)
end
f.portrait.tex:SetAllPoints(f.portrait)
f.portrait.tex:SetTexCoord(.1, .9, .1, .9)
f.portrait.model:SetAllPoints(f.portrait)
if f.config.portrait == "bar" then
f.portrait:SetParent(f.hp.bar)
f.portrait:SetAllPoints(f.hp.bar)
f.portrait:SetAlpha(C.unitframes.portraitalpha)
if f.portrait.backdrop then f.portrait.backdrop:Hide() end
-- place portrait below fonts
f.portrait.model:SetFrameLevel(3)
f.portrait:Show()
elseif f.config.portrait == "left" then
f.portrait:SetParent(f)
f.portrait:ClearAllPoints()
f.portrait:SetPoint("LEFT", f, "LEFT", 0, 0)
f.hp:ClearAllPoints()
f.hp:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
f.portrait:SetAlpha(f:GetAlpha())
-- make sure incHeal is above
f.portrait:SetFrameStrata("BACKGROUND")
f.portrait.model:SetFrameStrata("BACKGROUND")
f.portrait.model:SetFrameLevel(1)
pfUI.api.CreateBackdrop(f.portrait, default_border)
f.portrait.backdrop:Show()
f.portrait:Show()
elseif f.config.portrait == "right" then
f.portrait:SetParent(f)
f.portrait:ClearAllPoints()
f.portrait:SetPoint("RIGHT", f, "RIGHT", 0, 0)
f.hp:ClearAllPoints()
f.hp:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
f.portrait:SetAlpha(f:GetAlpha())
-- make sure incHeal is above
f.portrait:SetFrameStrata("BACKGROUND")
f.portrait.model:SetFrameStrata("BACKGROUND")
f.portrait.model:SetFrameLevel(1)
pfUI.api.CreateBackdrop(f.portrait, default_border)
f.portrait.backdrop:Show()
f.portrait:Show()
else
f.portrait:Hide()
end
if f.group then
f.group:SetShown(f.config.raidgrouplabel == "1")
local xoff = tonumber(f.config.grouplabelxoff) or 0
local yoff = tonumber(f.config.grouplabelyoff) or 8
f.group:SetPoint("TOPLEFT", f, "BOTTOMLEFT", xoff, yoff)
f.group:SetPoint("TOPRIGHT", f, "BOTTOMRIGHT", xoff, yoff)
end
if f.config.hitindicator == "1" then
f.feedbackText:SetFont(pfUI.media[f.config.hitindicatorfont], f.config.hitindicatorsize, "OUTLINE")
f.feedbackFontHeight = f.config.hitindicatorsize
f.feedbackStartTime = GetTime()
if f.config.portrait == "bar" or f.config.portrait == "off" then
f.feedbackText:SetParent(f.texts)
f.feedbackText:ClearAllPoints()
f.feedbackText:SetPoint("CENTER", f.hp.bar, "CENTER")
else
f.feedbackText:SetParent(f.portrait)
f.feedbackText:ClearAllPoints()
f.feedbackText:SetPoint("CENTER", f.portrait, "CENTER")
end
f:RegisterEvent("UNIT_COMBAT")
else
f.feedbackText:Hide()
f:UnregisterEvent("UNIT_COMBAT")
end
f.hpLeftText:SetFontObject(GameFontWhite)
f.hpLeftText:SetFont(fontname, fontsize, fontstyle)
f.hpLeftText:SetJustifyH("LEFT")
f.hpLeftText:ClearAllPoints()
f.hpLeftText:SetPoint("TOPLEFT",f.hp.bar, "TOPLEFT", 2*(default_border + f.config.txthpleftoffx), 1 + tonumber(f.config.txthpleftoffy))
f.hpLeftText:SetPoint("BOTTOMRIGHT",f.hp.bar, "BOTTOMRIGHT", -2*(default_border + f.config.txthpleftoffx), f.config.txthpleftoffy)
f.hpRightText:SetFontObject(GameFontWhite)
f.hpRightText:SetFont(fontname, fontsize, fontstyle)
f.hpRightText:SetJustifyH("RIGHT")
f.hpRightText:ClearAllPoints()
f.hpRightText:SetPoint("TOPLEFT",f.hp.bar, "TOPLEFT", 2*(default_border + f.config.txthprightoffx), 1 + tonumber(f.config.txthprightoffy))
f.hpRightText:SetPoint("BOTTOMRIGHT",f.hp.bar, "BOTTOMRIGHT", -2*(default_border + f.config.txthprightoffx), f.config.txthprightoffy)
f.hpCenterText:SetFontObject(GameFontWhite)
f.hpCenterText:SetFont(fontname, fontsize, fontstyle)
f.hpCenterText:SetJustifyH("CENTER")
f.hpCenterText:ClearAllPoints()
f.hpCenterText:SetPoint("TOPLEFT",f.hp.bar, "TOPLEFT", f.config.txthpcenteroffx, 1 + tonumber(f.config.txthpcenteroffy))
f.hpCenterText:SetPoint("BOTTOMRIGHT",f.hp.bar, "BOTTOMRIGHT", f.config.txthpcenteroffx, f.config.txthpcenteroffy)
f.powerLeftText:SetFontObject(GameFontWhite)
f.powerLeftText:SetFont(fontname, fontsize, fontstyle)
f.powerLeftText:SetJustifyH("LEFT")
f.powerLeftText:ClearAllPoints()
f.powerLeftText:SetPoint("TOPLEFT",f.power.bar, "TOPLEFT", 2*(default_border + f.config.txtpowerleftoffx), 1 + tonumber(f.config.txtpowerleftoffy))
f.powerLeftText:SetPoint("BOTTOMRIGHT",f.power.bar, "BOTTOMRIGHT", -2*(default_border + f.config.txtpowerleftoffx), f.config.txtpowerleftoffy)
f.powerRightText:SetFontObject(GameFontWhite)
f.powerRightText:SetFont(fontname, fontsize, fontstyle)
f.powerRightText:SetJustifyH("RIGHT")
f.powerRightText:ClearAllPoints()
f.powerRightText:SetPoint("TOPLEFT",f.power.bar, "TOPLEFT", 2*(default_border + f.config.txtpowerrightoffx), 1 + tonumber(f.config.txtpowerrightoffy))
f.powerRightText:SetPoint("BOTTOMRIGHT",f.power.bar, "BOTTOMRIGHT", -2*(default_border + f.config.txtpowerrightoffx), f.config.txtpowerrightoffy)
f.powerCenterText:SetFontObject(GameFontWhite)
f.powerCenterText:SetFont(fontname, fontsize, fontstyle)
f.powerCenterText:SetJustifyH("CENTER")
f.powerCenterText:ClearAllPoints()
f.powerCenterText:SetPoint("TOPLEFT",f.power.bar, "TOPLEFT", f.config.txtpowercenteroffx, 1 + tonumber(f.config.txtpowercenteroffy))
f.powerCenterText:SetPoint("BOTTOMRIGHT",f.power.bar, "BOTTOMRIGHT", f.config.txtpowercenteroffx, f.config.txtpowercenteroffy)
f.incHeal:SetSize(f.config.width, f.config.height)
f.incHeal.texture:SetTexture(pfUI.media["img:bar"])
local cr, cg, cb, ca = GetStringColor(f.config.healcolor)
cr, cg, cb, ca = tonumber(cr), tonumber(cg), tonumber(cb), tonumber(ca)
f.incHeal.texture:SetVertexColor(cr, cg, cb, ca)
f.incHeal:Hide()
if f.config.verticalbar == "0" then
f.incHeal:ClearAllPoints()
f.incHeal:SetPoint("TOPLEFT", f.hp.bar, "TOPLEFT", 0, 0)
else
f.incHeal:ClearAllPoints()
f.incHeal:SetPoint("BOTTOM", f.hp.bar, "BOTTOM", 0, 0)
end
f.ressIcon:SetFrameLevel(16)
f.ressIcon:SetSize(32, 32)
f.ressIcon:SetPoint("CENTER", f, "CENTER", 0, 4)
f.ressIcon.texture:SetTexture(pfUI.media["img:ress"])
f.ressIcon.texture:SetAllPoints(f.ressIcon)
f.ressIcon:Hide()
f.leaderIcon:SetSize(10, 10)
f.leaderIcon:SetPoint("CENTER", f, "TOPLEFT", 0, 0)
f.leaderIcon.texture:SetTexture("Interface\\GROUPFRAME\\UI-Group-LeaderIcon")
f.leaderIcon.texture:SetAllPoints(f.leaderIcon)
f.leaderIcon:Hide()
f.lootIcon:SetSize(10, 10)
f.lootIcon:SetPoint("CENTER", f, "LEFT", 0, 0)
f.lootIcon.texture:SetTexture("Interface\\GROUPFRAME\\UI-Group-MasterLooter")
f.lootIcon.texture:SetAllPoints(f.lootIcon)
f.lootIcon:Hide()
f.pvpIcon:SetSize(f.config.pvpiconsize, f.config.pvpiconsize)
f.pvpIcon:SetPoint(f.config.pvpiconalign, f, f.config.pvpiconalign, f.config.pvpiconoffx, f.config.pvpiconoffy)
f.pvpIcon.texture:SetTexture(pfUI.media["img:pvp"])
f.pvpIcon.texture:SetAllPoints(f.pvpIcon)
f.pvpIcon.texture:SetVertexColor(1,1,1,.5)
f.pvpIcon:Hide()
f.raidIcon:SetSize(f.config.raidiconsize, f.config.raidiconsize)
f.raidIcon:SetPoint("CENTER", f, f.config.raidiconalign, f.config.raidiconoffx, f.config.raidiconoffy)
local raidIconTex = C.unitframes.blizzard_raidicons == "1" and "Interface\\TargetingFrame\\UI-RaidTargetingIcons" or pfUI.media["img:raidicons"]
f.raidIcon.texture:SetTexture(raidIconTex)
f.raidIcon.texture:SetAllPoints(f.raidIcon)
f.raidIcon:Hide()
f.restIcon:SetSize(16, 16)
f.restIcon:SetPoint("TOP", f, "TOPLEFT", 0, -1)
f.restIcon.texture:SetTexture("Interface\\CharacterFrame\\UI-StateIcon", true)
f.restIcon.texture:SetTexCoord(0, .5, 0, .421875)
f.restIcon.texture:SetAllPoints(f.restIcon)
f.restIcon:Hide()
f.happinessIcon:SetSize(tonumber(C.unitframes.pet.happinesssize), tonumber(C.unitframes.pet.happinesssize))
f.happinessIcon:SetPoint("CENTER", f, "TOPLEFT", default_border, -default_border)
f.happinessIcon.texture:SetTexture(pfUI.media["img:neutral"])
f.happinessIcon.texture:SetAllPoints(f.happinessIcon)
f.happinessIcon.texture:SetVertexColor(1, 1, 0, 1)
f.happinessIcon:Hide()
if f.config.buffs == "off" then
for i=1, 32 do
if f.buffs and f.buffs[i] then
f.buffs[i]:Hide()
f.buffs[i] = nil
end
end
f.buffs = nil
else
f.buffs = f.buffs or {}
for i=1, 32 do
if i > tonumber(f.config.bufflimit) then break end
local perrow = f.config.buffperrow
local row = floor((i-1) / perrow)
f.buffs[i] = f.buffs[i] or CreateFrame("Button", "pfUI" .. f.fname .. "Buff" .. i, f)
f.buffs[i].texture = f.buffs[i].texture or f.buffs[i]:CreateTexture()
f.buffs[i].texture:SetTexCoord(.08, .92, .08, .92)
f.buffs[i].texture:SetAllPoints()
f.buffs[i].stacks = f.buffs[i].stacks or f.buffs[i]:CreateFontString(nil, "OVERLAY", f.buffs[i])
f.buffs[i].stacks:SetFont(pfUI.font_unit, C.global.font_unit_size, "OUTLINE")
f.buffs[i].stacks:SetPoint("BOTTOMRIGHT", f.buffs[i], 2, -2)
f.buffs[i].stacks:SetJustifyH("LEFT")
f.buffs[i].stacks:SetShadowColor(0, 0, 0)
f.buffs[i].stacks:SetShadowOffset(0.8, -0.8)
f.buffs[i].stacks:SetTextColor(1,1,.5)
f.buffs[i]:SetFrameLevel(12)
f.buffs[i]:RegisterForClicks("RightButtonUp")
f.buffs[i]:ClearAllPoints()
local invert_h, invert_v, af
if f.config.buffs == "TOPLEFT" then
invert_h = 1
invert_v = 1
af = "BOTTOMLEFT"
elseif f.config.buffs == "BOTTOMLEFT" then
invert_h = -1
invert_v = 1
af = "TOPLEFT"
elseif f.config.buffs == "TOPRIGHT" then
invert_h = 1
invert_v = -1
af = "BOTTOMRIGHT"
elseif f.config.buffs == "BOTTOMRIGHT" then
invert_h = -1
invert_v = -1
af = "TOPRIGHT"
end
local anchor = f.config.portraitheight ~= "-1" and f.hp or f
if anchor == f.hp and (f.config.buffs == "BOTTOMLEFT" or f.config.buffs == "BOTTOMRIGHT") then
anchor = f.power
end
local multiply = C.appearance.border.force_blizz == "1" and 1 or 2
f.buffs[i]:SetPoint(af, anchor, f.config.buffs,
invert_v * (i-1-row*perrow)*(multiply*default_border + f.config.buffsize + 1),
invert_h * (row*(multiply*default_border + f.config.buffsize + 1) + (multiply*default_border + 1)))
f.buffs[i]:SetSize(f.config.buffsize, f.config.buffsize)
-- Create CD frame if it doesn't exist
if not f.buffs[i].cd then
if cooldown_anim == 1 then
-- Animation enabled: Use Model frame with CooldownFrameTemplate
f.buffs[i].cd = CreateFrame(COOLDOWN_FRAME_TYPE, f.buffs[i]:GetName() .. "Cooldown", f.buffs[i], "CooldownFrameTemplate")
else
-- Animation disabled: Use regular Frame with dummy functions
f.buffs[i].cd = CreateFrame("Frame", f.buffs[i]:GetName() .. "Cooldown", f.buffs[i])
f.buffs[i].cd.AdvanceTime = DoNothing
f.buffs[i].cd.SetSequence = DoNothing
f.buffs[i].cd.SetSequenceTime = DoNothing
end
end
-- Always update CD properties (in case size changed)
local cdScale = f.config.buffsize / 32
f.buffs[i].cd:ClearAllPoints()
f.buffs[i].cd:SetScale(cdScale)
f.buffs[i].cd:SetAllPoints(f.buffs[i])
f.buffs[i].cd:SetFrameLevel(14)
f.buffs[i].cd.pfCooldownType = "ALL"
f.buffs[i].cd.pfCooldownStyleText = cooldown_text
f.buffs[i].cd.pfCooldownStyleAnimation = cooldown_anim
f.buffs[i].cd:SetAlpha(cooldown_anim == 1 and 1 or 0)
-- immediately show/hide existing cooldown text
if f.buffs[i].cd.pfCooldownText then
f.buffs[i].cd.pfCooldownText:SetShown(cooldown_text == 1)
end
f.buffs[i].id = i
f.buffs[i]:Hide()
CreateBackdrop(f.buffs[i], default_border)
f.buffs[i]:SetScript("OnEnter", BuffOnEnter)
f.buffs[i]:SetScript("OnLeave", BuffOnLeave)
f.buffs[i]:SetScript("OnClick", BuffOnClick)
end
end
if f.config.debuffs == "off" then
for i=1, 32 do
if f.debuffs and f.debuffs[i] then
f.debuffs[i]:Hide()
f.debuffs[i] = nil
end
end
f.debuffs = nil
else
f.debuffs = f.debuffs or {}
for i=1, 32 do
if i > tonumber(f.config.debufflimit) then break end
f.debuffs[i] = f.debuffs[i] or CreateFrame("Button", "pfUI" .. f.fname .. "Debuff" .. i, f)
f.debuffs[i].texture = f.debuffs[i].texture or f.debuffs[i]:CreateTexture()
f.debuffs[i].texture:SetTexCoord(.08, .92, .08, .92)
f.debuffs[i].texture:SetAllPoints()
f.debuffs[i].stacks = f.debuffs[i].stacks or f.debuffs[i]:CreateFontString(nil, "OVERLAY", f.debuffs[i])
f.debuffs[i].stacks:SetFont(pfUI.font_unit, C.global.font_unit_size, "OUTLINE")
f.debuffs[i].stacks:SetPoint("BOTTOMRIGHT", f.debuffs[i], 2, -2)
f.debuffs[i].stacks:SetJustifyH("LEFT")
f.debuffs[i].stacks:SetShadowColor(0, 0, 0)
f.debuffs[i].stacks:SetShadowOffset(0.8, -0.8)
f.debuffs[i].stacks:SetTextColor(1,1,.5)
f.debuffs[i]:SetFrameLevel(12)
f.debuffs[i]:RegisterForClicks("RightButtonUp")
f.debuffs[i]:ClearAllPoints()
f.debuffs[i]:SetSize(f.config.debuffsize, f.config.debuffsize)
f.debuffs[i]:SetNormalTexture(nil)
-- Create CD frame if it doesn't exist
if not f.debuffs[i].cd then
if cooldown_anim == 1 then
-- Animation enabled: Use Model frame with CooldownFrameTemplate
f.debuffs[i].cd = CreateFrame(COOLDOWN_FRAME_TYPE, f.debuffs[i]:GetName() .. "Cooldown", f.debuffs[i], "CooldownFrameTemplate")
else
-- Animation disabled: Use regular Frame with dummy functions
f.debuffs[i].cd = CreateFrame("Frame", f.debuffs[i]:GetName() .. "Cooldown", f.debuffs[i])
f.debuffs[i].cd.AdvanceTime = DoNothing
f.debuffs[i].cd.SetSequence = DoNothing
f.debuffs[i].cd.SetSequenceTime = DoNothing
end
end
-- Always update CD properties (in case size changed)
local cdScale = f.config.debuffsize / 32
f.debuffs[i].cd:ClearAllPoints()
f.debuffs[i].cd:SetScale(cdScale)
f.debuffs[i].cd:SetAllPoints(f.debuffs[i])
f.debuffs[i].cd:SetFrameLevel(14)
f.debuffs[i].cd.pfCooldownType = "ALL"
f.debuffs[i].cd.pfCooldownStyleText = cooldown_text
f.debuffs[i].cd.pfCooldownStyleAnimation = cooldown_anim
f.debuffs[i].cd:SetAlpha(cooldown_anim == 1 and 1 or 0)
-- immediately show/hide existing cooldown text
if f.debuffs[i].cd.pfCooldownText then
f.debuffs[i].cd.pfCooldownText:SetShown(cooldown_text == 1)
end
f.debuffs[i].id = i
f.debuffs[i]:Hide()
CreateBackdrop(f.debuffs[i], default_border)
f.debuffs[i]:SetScript("OnEnter", DebuffOnEnter)
f.debuffs[i]:SetScript("OnLeave", DebuffOnLeave)
f.debuffs[i]:SetScript("OnClick", DebuffOnClick)
end
end
if f.config.visible == "1" then
pfUI.uf:RefreshUnit(f, "all")
f:EnableScripts()
f:EnableEvents()
f:UpdateFrameSize()
else
f:UnregisterAllEvents()
f:Hide()
end
end
function pfUI.uf.OnShow()
pfUI.uf:RefreshUnit(this, "portrait")
pfUI.uf:RefreshUnit(this, "base")
end
-- GUID-keyed aura start-time tracker, fed by Nampower's BUFF_ADDED_OTHER /
-- BUFF_REMOVED_OTHER. Non-player units have aura.expirationTime == 0 (server
-- doesn't broadcast time), so we reconstruct the swirl from "when we saw it
-- applied" + aura.duration (Spell.dbc base). Only auras we actually witnessed
-- being cast get tracked — pre-existing auras (already on a unit when you
-- target them) intentionally show no timer rather than a fabricated one.
pfUI.uf.aura_starts = {} -- {[guid] = {[spellId] = startTime}}
local auraTracker = CreateFrame("Frame", "pfUF_AuraTracker", UIParent)
auraTracker:RegisterEvent("BUFF_ADDED_OTHER")
auraTracker:RegisterEvent("BUFF_REMOVED_OTHER")
auraTracker:SetScript("OnEvent", function()
-- Nampower arg layout: arg1=guid, arg3=spellId. State code 0=added, 1=removed,
-- 2=modified; we treat any added/modified as "reset start = now" and let the
-- REMOVED event clear the entry.
if not arg1 or not arg3 or arg3 == 0 then return end
if event == "BUFF_REMOVED_OTHER" then
if pfUI.uf.aura_starts[arg1] then
pfUI.uf.aura_starts[arg1][arg3] = nil
end
else
pfUI.uf.aura_starts[arg1] = pfUI.uf.aura_starts[arg1] or {}
pfUI.uf.aura_starts[arg1][arg3] = GetTime()
end
end)
function pfUI.uf.OnEvent()
-- Handle shutdown to prevent crash 132
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
this:SetScript("OnUpdate", nil)
return
end
-- update indicators
if event == "PARTY_LEADER_CHANGED" or
event == "PARTY_LOOT_METHOD_CHANGED" or
event == "PARTY_MEMBERS_CHANGED" or
event == "RAID_TARGET_UPDATE" or
event == "RAID_ROSTER_UPDATE" or
event == "PLAYER_UPDATE_RESTING"
then
this.update_indicators = true
end
-- abort on broken unitframes (e.g focus)
if not this.label then return end
-- update regular frames
if event == "PLAYER_ENTERING_WORLD" then
this.update_full = true
-- Clear GUID tracking on zone change for full rebuild
if pfUI.uf.ClearGuidTracking then pfUI.uf.ClearGuidTracking() end
elseif this.label == "target" and event == "PLAYER_TARGET_CHANGED" and not pfScanActive == true then
this.update_full = true
-- immediately update raid icon to avoid delay on target swap
if this.raidIcon and this.config and this.config.raidicon == "1" then
local raidIcon = UnitName("target") and GetRaidTargetIndex("target")
if raidIcon then
SetRaidTargetIconTexture(this.raidIcon.texture, raidIcon)
this.raidIcon:Show()
else
this.raidIcon:Hide()
end
end
elseif ( this.label == "raid" or this.label == "party" or this.label == "player" ) and event == "PARTY_MEMBERS_CHANGED" then
-- Smart update: check if THIS frame's unit actually changed
if pfUI.uf.guidTracker and this.id then
local unit = this.label == "player" and "player" or (this.label .. this.id)
local newGuid = UnitGUID(unit)
local oldGuid = pfUI.uf.guidTracker.frameToGuid[this]
if newGuid ~= oldGuid then
pfUI.uf.guidTracker.frameToGuid[this] = newGuid
this.update_full = true
end
else
this.update_full = true
end
elseif ( this.label == "raid" or this.label == "party" ) and event == "PARTY_MEMBER_ENABLE" then
this.update_full = true
elseif ( this.label == "raid" or this.label == "party" ) and event == "PARTY_MEMBER_DISABLE" then
this.update_full = true
elseif ( this.label == "raid" or this.label == "party" ) and event == "RAID_ROSTER_UPDATE" then
-- Note: Smart GUID-based updates are handled in raid.lua OnUpdate
-- after frame IDs are reassigned. We don't set update_full here anymore
-- for raid frames to avoid the freeze.
if this.label == "party" then
-- Party frames still need the old logic (no smart tracking yet)
this.update_full = true
end
-- Raid frames: update_full is set by raid.lua GUID tracker
elseif this.label == "pet" and event == "UNIT_PET" then
this.update_full = true
elseif this.label == "player" and (event == "PLAYER_AURAS_CHANGED" or event == "PLAYER_EQUIPMENT_CHANGED") then
this.update_aura = true
elseif this.label == "pet" and event == "UNIT_HAPPINESS" then
this.update_full = true
-- UNIT_XXX Events
elseif arg1 and (arg1 == this.label .. this.id or (UnitGUID and arg1 == UnitGUID(this.label .. this.id))) then
if event == "UNIT_PORTRAIT_UPDATE" or event == "UNIT_MODEL_CHANGED" then
this.update_portrait = true
elseif event == "UNIT_AURA" then
this.update_aura = true
elseif event == "UNIT_FACTION" then
this.update_pvp = true
elseif event == "UNIT_COMBAT" then
CombatFeedback_OnCombatEvent(arg2, arg3, arg4, arg5)
else
this.update_base = true
end
end
end
-- Local reference for performance
local _GetTime = GetTime
-- Global cached time for libpredict and other functions
pfUI.uf.now = 0
-- ============================================================================
-- OnUpdate - eventless per-frame work (range check, aggro glow) and draining
-- the event-set update flags. Frames refresh on events only; no polling.
-- ============================================================================
function pfUI.uf.OnUpdate()
local now = _GetTime()
pfUI.uf.now = now
-- update combat feedback (no throttle - needs immediate feedback)
if this.feedbackText then CombatFeedback_OnUpdate(arg1) end
-- Throttle raid/party frames for performance
if this.label == "raid" or this.label == "party" then
if (this.throttleTick or 0) > now then
return
end
this.throttleTick = now + 0.1 -- Default: 10 FPS
end
-- ============================================================================
-- EVENTLESS ACTIONS (Range Check, Online/Offline, Aggro) - MUST RUN ALWAYS
-- These run on their own timer, independent of event-based updates
-- ============================================================================
if this.label then
-- Combat/Aggro Indicators (throttled to 0.2s)
if not this.lastCombatCheck then this.lastCombatCheck = now + 0.2 end
if this.lastCombatCheck < now then
this.lastCombatCheck = now + 0.2
if this.config and this.config.squareaggro == "1" and pfUI.api.UnitHasAggro(this.label .. this.id) > 0 then
this.combat.tex:SetTexture(1,.2,0)
this.combat:Show()
elseif this.config and this.config.squarecombat == "1" and UnitAffectingCombat(this.label .. this.id) then
this.combat.tex:SetTexture(1,1,.2)
this.combat:Show()
elseif this.combat then
this.combat:Hide()
end
end
-- Range Check / Online-Offline State (throttled)
-- Validate tick value - it should be an interval (e.g. 0.5), not a timestamp
local tickInterval = this.tick
if tickInterval and tickInterval > 10 then
-- tick is a timestamp, not an interval - ignore it
tickInterval = nil
end
-- Reset lastTick if it's invalid (much larger than now, e.g. from corrupted state)
if this.lastTick and this.lastTick > now + 10 then
this.lastTick = nil
end
if not this.lastTick then this.lastTick = now + (tickInterval or .5) end
if this.lastTick < now then
local unitstr = this.label .. this.id
this.lastTick = now + (tickInterval or .5)
-- target target has a huge delay, make sure to not tick during range checks
if this.label == "targettarget" or this.label == "targettargettarget" then
local name = UnitName(this.label)
if name ~= this.namebuf1 then
this.namebuf1 = name
elseif name ~= this.namebuf2 then
this.namebuf2 = name
else
pfUI.uf:RefreshUnitState(this)
pfUI.uf:RefreshIndicators(this)
end
else
pfUI.uf:RefreshUnitState(this)
pfUI.uf:RefreshIndicators(this)
end
if this.config and this.config.glowaggro == "1" and pfUI.api.UnitHasAggro(unitstr) > 0 then
this.glow:SetBackdropBorderColor(1,.2,0)
this.glow:Show()
elseif this.config and this.config.glowcombat == "1" and UnitAffectingCombat(unitstr) then
this.glow:SetBackdropBorderColor(1,1,.2)
this.glow:Show()
elseif this.glow then
this.glow:Hide()
end
-- update everything on eventless frames (targettarget, etc)
if this.tick then
pfUI.uf:RefreshUnit(this, "all")
end
end
-- Heal Prediction (throttled to 0.1s for responsiveness)
if libpredict and this.incHeal then
if not this.lastHealTick then this.lastHealTick = now end
if this.lastHealTick < now then
this.lastHealTick = now + 0.1
local unit = this.label .. this.id
local heal = libpredict:UnitGetIncomingHeals(unit)
local health, maxHealth = UnitHealth(unit), UnitHealthMax(unit)
if heal - health - maxHealth ~= this.predictstate then
local overhealperc = tonumber(this.config.overhealperc)
this.predictstate = heal - health - maxHealth
if heal > 0 and (health < maxHealth or overhealperc > 0 ) then
local width = this.config.width
local height = this.config.height
if this.config.verticalbar == "0" then
local healthWidth = width * (health / maxHealth)
local incWidth = width * heal / maxHealth
if healthWidth + incWidth > width * (1+(overhealperc/100)) then
incWidth = width * (1+overhealperc/100) - healthWidth
end
if this.config.invert_healthbar == "1" then
this.incHeal:SetWidth(incWidth)
else
this.incHeal:SetWidth(incWidth + healthWidth)
end
else
local healthHeight = height * (health / maxHealth)
local incHeight = height * heal / maxHealth
if healthHeight + incHeight > height * (1+(overhealperc/100)) then
incHeight = height * (1+overhealperc/100) - healthHeight
end
if this.config.invert_healthbar == "1" then
this.incHeal:SetHeight(incHeight)
else
this.incHeal:SetHeight(incHeight + healthHeight)
end
end
this.incHeal:Show()
else
this.incHeal:Hide()
end
end
-- update ressurections
local ress = libpredict:UnitHasIncomingResurrection(unit)
this.ressIcon:SetShown(ress and UnitIsDeadOrGhost(unit))
end
end
end
-- ============================================================================
-- EVENT-BASED UPDATES (Health, Mana, Auras, etc.)
-- ============================================================================
-- process indicator update events
if this.update_indicators then
pfUI.uf:RefreshIndicators(this)
this.update_indicators = nil
end
-- process all queued unit events
if this.update_full then
-- process full updates
pfUI.uf:RefreshUnit(this, "all")
-- clear update caches
this.update_full = nil
this.update_base = nil
this.update_aura = nil
this.update_portrait = nil
this.update_pvp = nil
else
-- process individual events
if this.update_aura then
pfUI.uf:RefreshUnit(this, "aura")
this.update_aura = nil
this.update_base = true
end
if this.update_portrait then
pfUI.uf:RefreshUnit(this, "portrait")
this.update_portrait = nil
this.update_base = true
end
if this.update_pvp then
pfUI.uf:RefreshUnit(this, "pvp")
this.update_pvp = nil
end
if this.update_base then
pfUI.uf:RefreshUnit(this, "base")
this.update_base = nil
end
end
if not this.label or this.label == "" then return end
-- update portrait on first visible frame
if this.portrait and this.portrait.model and this.portrait.model.update then
this.portrait.model.lastUnit = UnitName(this.portrait.model.update)
this.portrait.model:SetUnit(this.portrait.model.update)
this.portrait.model:SetCamera(0)
this.portrait.model.update = nil
end
end
function pfUI.uf:EnableEvents()
local f = self
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:RegisterEvent("PLAYER_LOGOUT")
f:RegisterEvent("UNIT_DISPLAYPOWER")
f:RegisterEvent("UNIT_HEALTH")
f:RegisterEvent("UNIT_MAXHEALTH")
f:RegisterEvent("UNIT_MANA")
f:RegisterEvent("UNIT_MAXMANA")
f:RegisterEvent("UNIT_RAGE")
f:RegisterEvent("UNIT_MAXRAGE")
f:RegisterEvent("UNIT_ENERGY")
f:RegisterEvent("UNIT_MAXENERGY")
f:RegisterEvent("UNIT_FOCUS")
f:RegisterEvent("UNIT_PORTRAIT_UPDATE")
f:RegisterEvent("UNIT_MODEL_CHANGED")
f:RegisterEvent("UNIT_FACTION")
f:RegisterEvent("UNIT_AURA") -- frame=buff, frame=debuff
f:RegisterEvent("PLAYER_AURAS_CHANGED") -- label=player && frame=buff
f:RegisterEvent("PLAYER_EQUIPMENT_CHANGED") -- label=player && frame=buff (ClassicAPI: weapon-enchant buffs)
f:RegisterEvent("PARTY_MEMBERS_CHANGED") -- label=party, frame=leaderIcon
f:RegisterEvent("PARTY_LEADER_CHANGED") -- frame=leaderIcon
f:RegisterEvent("RAID_ROSTER_UPDATE") -- label=raidIcon
f:RegisterEvent("PLAYER_UPDATE_RESTING") -- label=restIcon
f:RegisterEvent("PLAYER_TARGET_CHANGED") -- label=target
f:RegisterEvent("PARTY_LOOT_METHOD_CHANGED") -- frame=lootIcon
f:RegisterEvent("RAID_TARGET_UPDATE") -- frame=raidIcon
f:RegisterEvent("UNIT_PET")
f:RegisterEvent("UNIT_HAPPINESS")
f:RegisterForClicks('LeftButtonUp', 'RightButtonUp',
'MiddleButtonUp', 'Button4Up', 'Button5Up')
end
function pfUI.uf:EnableScripts()
local f = self
f:SetAttribute("type1", "target")
f:SetAttribute("type2", "menu")
f:SetScript("OnShow", pfUI.uf.OnShow)
f:SetScript("OnEvent", pfUI.uf.OnEvent)
f:SetScript("OnUpdate", pfUI.uf.OnUpdate)
f:EnableClickCast()
-- add frame to visibility refresh handler
visibilityscan.frames[f] = true
end
function pfUI.uf:CreateUnitFrame(unit, id, config, tick)
local fname = (( unit == "Party" ) and "Group" or (unit or "")) .. (id or "")
local unit = strlower(unit or "")
local id = strlower(id or "")
-- fake party0 units as self
if unit == "party" and id == "0" then
unit, id = "player", ""
end
if unit == "partypet" and id == "0" then
unit, id = "pet", ""
end
if unit == "pettarget" and id == "0" then
unit, id = "pettarget", ""
end
if unit == "party0target" then
unit, id = "target", ""
end
local f = CreateFrame("Button", "pf" .. fname, UIParent)
-- add unitframe functions
f.UpdateFrameSize = pfUI.uf.UpdateFrameSize
f.UpdateVisibility = pfUI.uf.UpdateVisibility
f.UpdateConfig = pfUI.uf.UpdateConfig
f.EnableScripts = pfUI.uf.EnableScripts
f.EnableEvents = pfUI.uf.EnableEvents
f.EnableClickCast = pfUI.uf.EnableClickCast
f.GetColor = pfUI.uf.GetColor
-- cache values to the frame
f.label = strlower(unit)
f.fname = fname
f.id = id
f.config = config or pfUI_config.unitframes.fallback
f.tick = tick
-- disable events for unknown unitstrings
if not pfValidUnits[strlower(unit) .. id] then
f.unitname = unit
f.label, f.id = "", ""
f.RegisterEvent = function() return end
end
CreateBackdropShadow(f)
f.hp = CreateFrame("Frame",nil, f)
f.hp.bar = CreateStatusBar(nil, f.hp)
f.power = CreateFrame("Frame",nil, f)
f.power.bar = CreateStatusBar(nil, f.power)
-- Druid secondary mana bar: shows base mana (read via ClassicAPI
-- UnitPower(unit, Enum.PowerType.Mana), which works regardless of the active
-- power) while shapeshifted into a form that uses energy/rage. Player +
-- target only;
-- styled/positioned in UpdateConfig, driven in UpdateDruidMana.
if C.unitframes.druidmanabar == "1" and (f.label == "player" or f.label == "target") then
f.druidmana = CreateFrame("StatusBar", "pfDruidMana_" .. f.label .. f.id, f)
f.druidmana.text = f.druidmana:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
f.druidmana.text:SetPoint("CENTER", f.druidmana, "CENTER", 0, 0)
f.druidmana.text:SetJustifyH("CENTER")
f.druidmana:Hide()
end
f.glow = CreateFrame("Frame", nil, f)
f.combat = CreateFrame("Frame", nil, f.hp.bar)
f.combat.tex = f.combat:CreateTexture(nil, "OVERLAY")
f.combat.tex:SetAllPoints()
f.texts = CreateFrame("Frame", nil, f)
f.texts:SetFrameLevel(16)
f.texts:SetAllPoints()
f.hpLeftText = f.texts:CreateFontString("Status", "OVERLAY", "GameFontNormalSmall")
f.hpRightText = f.texts:CreateFontString("Status", "OVERLAY", "GameFontNormalSmall")
f.hpCenterText = f.texts:CreateFontString("Status", "OVERLAY", "GameFontNormalSmall")
f.powerLeftText = f.texts:CreateFontString("Status", "OVERLAY", "GameFontNormalSmall")
f.powerRightText = f.texts:CreateFontString("Status", "OVERLAY", "GameFontNormalSmall")
f.powerCenterText = f.texts:CreateFontString("Status", "OVERLAY", "GameFontNormalSmall")
f.incHeal = CreateFrame("Frame", nil, f.hp)
-- Texture lives on hp.bar for correct draw order:
-- hp.bar.bg (BACKGROUND) < incHeal (BORDER) < hp.bar.bar (NORMAL)
f.incHeal.texture = f.hp.bar:CreateTexture(nil, "BORDER")
f.incHeal.texture:SetAllPoints(f.incHeal)
-- Override Show/Hide since texture lives on hp.bar, not incHeal
f.incHeal._Show = f.incHeal.Show
f.incHeal._Hide = f.incHeal.Hide
f.incHeal.Show = function(self) self:_Show() self.texture:Show() end
f.incHeal.Hide = function(self) self:_Hide() self.texture:Hide() end
f.incHeal.texture:Hide()
f.ressIcon = CreateFrame("Frame", nil, f.hp.bar)
f.ressIcon.texture = f.ressIcon:CreateTexture(nil,"BACKGROUND")
f.leaderIcon = CreateFrame("Frame", nil, f.hp.bar)
f.leaderIcon.texture = f.leaderIcon:CreateTexture(nil,"BACKGROUND")
f.lootIcon = CreateFrame("Frame",nil, f.hp.bar)
f.lootIcon.texture = f.lootIcon:CreateTexture(nil,"BACKGROUND")
f.pvpIcon = CreateFrame("Frame", nil, f.hp.bar)
f.pvpIcon.texture = f.pvpIcon:CreateTexture(nil,"BACKGROUND")
f.raidIcon = CreateFrame("Frame", nil, f.hp.bar)
f.raidIcon.texture = f.raidIcon:CreateTexture(nil,"ARTWORK")
f.restIcon = CreateFrame("Frame", nil, f.hp.bar)
f.restIcon.texture = f.restIcon:CreateTexture(nil, "BACKGROUND")
f.happinessIcon = CreateFrame("Frame", nil, f.hp.bar)
f.happinessIcon.texture = f.happinessIcon:CreateTexture(nil, "BACKGROUND")
f.portrait = CreateFrame("Frame", "pfPortrait" .. f.label .. f.id, f)
f.portrait.tex = f.portrait:CreateTexture("pfPortraitTexture" .. f.label .. f.id, "OVERLAY")
f.portrait.model = CreateFrame("PlayerModel", "pfPortraitModel" .. f.label .. f.id, f.portrait)
f.portrait.model.next = CreateFrame("PlayerModel", nil, nil)
f.feedbackText = f:CreateFontString("pfHitIndicator" .. f.label .. f.id, "OVERLAY", "NumberFontNormalHuge")
if f.label == "raid" and mod(f.id, 5) == 1 then
local group = math.ceil(f.id/5)
f.group = f.group or f.hp.bar:CreateFontString("Status", "OVERLAY", "GameFontNormalSmall")
f.group:SetFont(pfUI.font_unit, 8, "OUTLINE")
f.group:SetTextColor(1,1,1,.8)
f.group:SetHeight(16)
f.group:SetText("Group " .. group)
f.group:Hide()
end
f:Hide()
f:UpdateConfig()
f:UpdateFrameSize()
f:EnableScripts()
f:EnableEvents()
if f.config.visible == "1" then
pfUI.uf:RefreshUnit(f, "all")
f:EnableScripts()
f:EnableEvents()
f:UpdateFrameSize()
else
f:UnregisterAllEvents()
f:Hide()
end
if f.label ~= "" then
f:SetAttribute("unit", f.label .. f.id)
end
-- register frame for clique
_G.ClickCastFrames = ClickCastFrames or {}
ClickCastFrames[f] = true
table.insert(pfUI.uf.frames, f)
return f
end
function pfUI.uf:RefreshUnitState(unit)
local alpha = unit.alpha_visible
local unlock = pfUI.unlock and pfUI.unlock:IsShown() or nil
if not UnitIsConnected(unit.label .. unit.id) and not unlock then
-- offline
alpha = unit.alpha_offline
unit.hp.bar:SetMinMaxValues(0, 100, true)
unit.power.bar:SetMinMaxValues(0, 100, true)
unit.hp.bar:SetValue(0)
unit.power.bar:SetValue(0)
elseif unit.config.faderange == "1" and not pfUI.api.UnitInRange(unit.label .. unit.id, 4) and not unlock then
alpha = unit.alpha_outrange
end
-- skip if alpha is already correct
if floor(unit:GetAlpha()*10+.5) == floor(alpha*10+.5) then return end
-- set unitframe alpha
unit:SetAlpha(alpha)
-- refresh portrait alpha
if unit.config.portrait == "bar" then
unit.portrait:SetAlpha(pfUI_config.unitframes.portraitalpha)
end
-- refresh debuff indicator alpha
local disptype = unit.config.debuff_indicator
local indicator = unit.hp.bar.debuffindicators
if indicator then
indicator:SetAlpha(0)
if ( disptype == "4" or disptype == "3" ) then
indicator:SetAlpha(1)
elseif disptype == "2" then
indicator:SetAlpha(.4)
elseif disptype == "1" then
indicator:SetAlpha(.2)
end
end
end
function pfUI.uf:RefreshIndicators(unit)
if not unit.label or not unit.id then return end
local unitstr = unit.label .. unit.id
if unit.leaderIcon then -- Leader Icon
unit.leaderIcon:SetShown(unit.config.leadericon == "1" and UnitIsPartyLeader(unitstr) and IsInGroup())
end
if unit.lootIcon then -- Loot Icon
if unit.config.looticon == "0" then
unit.lootIcon:Hide()
else
-- no third return value here.. but leaving this as a hint
local method, group, raid = GetLootMethod()
local name = group and UnitName(group == 0 and "player" or "party"..group) or raid and UnitName("raid"..raid) or nil
unit.lootIcon:SetShown(name and name == UnitName(unitstr))
end
end
if unit.pvpIcon then -- PvP Icon
unit.pvpIcon:SetShown(unit.config.showPVP == "1" and UnitIsPVP(unitstr))
end
if unit.restIcon and unit:GetName() == "pfPlayer" then -- Rest Icon
unit.restIcon:SetShown(C.unitframes.player.showRest == "1" and UnitIsUnit(unitstr, "player") and IsResting())
end
if unit.happinessIcon and unit:GetName() == "pfPet" then -- Happiness Icon
local pclass = UnitClassBase("player")
if unit.config.happinessicon == "0" or pclass ~= "HUNTER" then
unit.happinessIcon:Hide()
else
if UnitIsVisible("pet") then
local happiness = GetPetHappiness()
if happiness == 1 then
unit.happinessIcon.texture:SetTexture(pfUI.media["img:sad"..unit.config.happinessicon])
unit.happinessIcon.texture:SetVertexColor(1, 0, 0, 1)
elseif happiness == 2 then
unit.happinessIcon.texture:SetTexture(pfUI.media["img:neutral"..unit.config.happinessicon])
unit.happinessIcon.texture:SetVertexColor(1, 1, 0, 1)
else
unit.happinessIcon.texture:SetTexture(pfUI.media["img:happy"..unit.config.happinessicon])
unit.happinessIcon.texture:SetVertexColor(0, 1, 0, 1)
end
unit.happinessIcon:Show()
else
unit.happinessIcon:Hide()
end
end
end
if unit.raidIcon then -- Raid Icon
local raidIcon = UnitName(unitstr) and GetRaidTargetIndex(unitstr)
if unit.config.raidicon == "1" and raidIcon then
SetRaidTargetIconTexture(unit.raidIcon.texture, raidIcon)
unit.raidIcon:Show()
else
unit.raidIcon:Hide()
end
end
end
-- Druid secondary mana bar update. Reads base mana with ClassicAPI
-- UnitPower/UnitPowerMax(unit, Enum.PowerType.Mana), which return the mana
-- slot regardless of the unit's active power -- so it works while shapeshifted,
-- with no nampower/GetUnitField dependency. Shown only while off mana
-- (Cat=energy, Bear=rage); non-player frames only for druid units.
function pfUI.uf:UpdateDruidMana(unit)
local bar = unit.druidmana
local unitstr = unit.label .. unit.id
if not UnitExists(unitstr) then bar:Hide() return end
if unit.label ~= "player" then
local cls = UnitClassBase(unitstr)
if cls ~= "DRUID" then bar:Hide() return end
end
if UnitPowerType(unitstr) == Enum.PowerType.Mana then bar:Hide() return end
local mana, maxmana = UnitPower(unitstr, Enum.PowerType.Mana), UnitPowerMax(unitstr, Enum.PowerType.Mana)
if not maxmana or maxmana == 0 then bar:Hide() return end
bar:SetMinMaxValues(0, maxmana)
bar:SetValue(mana)
if C.unitframes.druidmanatext == "1" then
bar.text:SetText(pfUI.api.Abbreviate(mana) .. "/" .. pfUI.api.Abbreviate(maxmana))
else
bar.text:SetText("")
end
bar:Show()
end
function pfUI.uf:RefreshUnit(unit, component)
-- break early on misconfigured UF's
if not unit.label then return end
if not unit.hp then return end
if not unit.power then return end
if not unit.id then unit.id = "" end
component = component or ""
-- don't update scanner activity
if unit.label == "target" or unit.label == "targettarget" or unit.label == "targettargettarget" then
if pfScanActive == true then return end
end
-- hide unused and invalid frames
unit:UpdateVisibility()
-- return on invisible unit frames
if not unit:IsShown() and not unit.visible then return end
-- create required fields
local unitstr = unit.label..unit.id
local rawborder, default_border = GetBorderSize("unitframes")
-- save current values
unit.namecache = UnitName(unitstr)
-- buffs
if unit.buffs and ( component == "all" or component == "aura" ) then
for i=1, unit.config.bufflimit do
if not unit.buffs[i] then break end
local aura = C_UnitAuras.GetBuffDataByIndex(unitstr, i)
if aura then
unit.buffs[i].texture:SetTexture(aura.icon)
unit.buffs[i]:Show()
if aura.applications > 1 then
unit.buffs[i].stacks:SetText(aura.applications)
else
unit.buffs[i].stacks:SetText("")
end
if aura.expirationTime > 0 then
-- pfUI's cooldown text falls into a 2^32 wraparound branch when start > GetTime(),
-- which happens for talent-extended buffs where the real duration exceeds aura.duration
-- (the Spell.dbc base). Anchor start to now in that case to keep the remaining math sane.
local now = GetTime()
local start = aura.expirationTime - aura.duration
local duration = aura.duration
if start > now or duration <= 0 then
start, duration = now, aura.expirationTime - now
end
if duration > 0 then
CooldownFrame_SetTimer(unit.buffs[i].cd, start, duration, 1)
else
CooldownFrame_SetTimer(unit.buffs[i].cd, 0, 0, 0)
end
elseif aura.duration > 0 then
local guid = UnitGUID(unitstr)
local guidStarts = guid and pfUI.uf.aura_starts[guid]
local start = guidStarts and guidStarts[aura.spellId]
if start then
CooldownFrame_SetTimer(unit.buffs[i].cd, start, aura.duration, 1)
else
CooldownFrame_SetTimer(unit.buffs[i].cd, 0, 0, 0)
end
else
CooldownFrame_SetTimer(unit.buffs[i].cd, 0, 0, 0)
end
else
unit.buffs[i]:Hide()
end
end
end
-- debuffs
if unit.debuffs and ( component == "all" or component == "aura" ) then
local texture, stacks, dtype
local perrow = unit.config.debuffperrow
local bperrow = unit.config.buffperrow
local selfdebuff = unit.config.selfdebuff
local invert_h, invert_v, af
if unit.config.debuffs == "TOPLEFT" then
invert_h = 1
invert_v = 1
af = "BOTTOMLEFT"
elseif unit.config.debuffs == "BOTTOMLEFT" then
invert_h = -1
invert_v = 1
af = "TOPLEFT"
elseif unit.config.debuffs == "TOPRIGHT" then
invert_h = 1
invert_v = -1
af = "BOTTOMRIGHT"
elseif unit.config.debuffs == "BOTTOMRIGHT" then
invert_h = -1
invert_v = -1
af = "TOPRIGHT"
end
local buffrow, reposition = 0, ( component == "all" and true or nil )
if unit.config.buffs == unit.config.debuffs then
if unit.buffs[0*bperrow+1] and unit.buffs[0*bperrow+1]:IsShown() then buffrow = buffrow + 1 end
if unit.buffs[1*bperrow+1] and unit.buffs[1*bperrow+1]:IsShown() then buffrow = buffrow + 1 end
if unit.buffs[2*bperrow+1] and unit.buffs[2*bperrow+1]:IsShown() then buffrow = buffrow + 1 end
if unit.buffs[3*bperrow+1] and unit.buffs[3*bperrow+1]:IsShown() then buffrow = buffrow + 1 end
end
if buffrow ~= unit.lastbuffrow then
unit.lastbuffrow = buffrow
reposition = true
end
for i=1, unit.config.debufflimit do
if not unit.debuffs[i] then break end
local row = floor((i-1) / unit.config.debuffperrow)
if reposition then
local anchor = unit.config.portraitheight ~= "-1" and unit.hp or unit
if anchor == unit.hp and (unit.config.debuffs == "BOTTOMLEFT" or unit.config.debuffs == "BOTTOMRIGHT") then
anchor = unit.power
end
local multiply = C.appearance.border.force_blizz == "1" and 1 or 2
unit.debuffs[i]:SetPoint(af, anchor, unit.config.debuffs,
invert_v * (i-1-row*perrow)*(multiply*default_border + unit.config.debuffsize + 1),
invert_h * ((row+buffrow)*(multiply*default_border + unit.config.debuffsize + 1) + (multiply*default_border + 1)))
end
-- selfdebuff narrows to player-cast harmful auras via the PLAYER filter.
-- Player-frame debuffs aren't gated on it (it'd hide most party-applied
-- effects on you).
local filter = (unit.label ~= "player" and selfdebuff == "1") and "HARMFUL|PLAYER" or "HARMFUL"
local aura = C_UnitAuras.GetAuraDataByIndex(unitstr, i, filter)
if aura then
texture, stacks, dtype = aura.icon, aura.applications, aura.dispelName
else
texture, stacks, dtype = nil, 0, nil
end
unit.debuffs[i].texture:SetTexture(texture)
local dispelColor = C_UnitAuras.GetAuraDispelTypeColor(dtype or "")
unit.debuffs[i].backdrop:SetBackdropBorderColor(dispelColor:GetRGBA())
if texture then
unit.debuffs[i]:Show()
if aura and aura.expirationTime > 0 then
-- Cap start to now so talent-extended debuffs (expirationTime past
-- the dbc base duration) don't push start into the future and trip
-- CooldownFrame_SetTimer's 2^32-wraparound branch.
local now = GetTime()
local start = aura.expirationTime - aura.duration
local duration = aura.duration
if start > now or duration <= 0 then
start, duration = now, aura.expirationTime - now
end
if duration > 0 then
CooldownFrame_SetTimer(unit.debuffs[i].cd, start, duration, 1)
else
CooldownFrame_SetTimer(unit.debuffs[i].cd, 0, 0, 0)
end
else
CooldownFrame_SetTimer(unit.debuffs[i].cd, 0, 0, 0)
end
if stacks > 1 then
unit.debuffs[i].stacks:SetText(stacks)
else
unit.debuffs[i].stacks:SetText("")
end
else
unit.debuffs[i]:Hide()
end
end
end
-- indicators
if component == "all" or component == "aura" then
if not unit.dispellable and unit.config.debuff_indicator ~= "0" then
unit.dispellable = pfUI.uf:SetupDebuffFilter((unit.config.debuff_ind_class == "0" and true or nil))
elseif not unit.dispellable then
unit.dispellable = {}
end
if table.getn(unit.dispellable) > 0 then
unit.hp.bar.debuffindicators = unit.hp.bar.debuffindicators or CreateFrame("Frame", nil, unit.hp.bar)
-- 0 = OFF, 1 = Legacy, 2 = Glow, 3 = Square, 4 = Icons
local disptype = unit.config.debuff_indicator
local indicator = unit.hp.bar.debuffindicators
local indipos = unit.config.debuff_ind_pos
local count = 0
local size
if disptype == "4" or disptype == "3" then
size = unit.hp.bar:GetHeight() * tonumber(unit.config.debuff_ind_size)
if size ~= indicator.size or disptype ~= indicator.disp or indipos ~= indicator.ipos then
indicator:ClearAllPoints()
indicator:SetPoint(indipos, 0, 0)
indicator:SetSize(size, size)
indicator.size = size
indicator.disp = disptype
indicator.ipos = indipos
end
elseif disptype == "2" or disptype == "1" then
size = "FULL"
if size ~= indicator.size or disptype ~= indicator.disp or indipos ~= indicator.ipos then
indicator:ClearAllPoints()
indicator:SetAllPoints(unit.hp.bar)
indicator.size = size
indicator.disp = disptype
indicator.ipos = indipos
end
end
for _, debuff in pairs(unit.dispellable) do
indicator[debuff] = indicator[debuff] or CreateFrame("Frame", nil, indicator)
indicator[debuff]:SetParent(indicator)
indicator[debuff].tex = indicator[debuff].tex or indicator[debuff]:CreateTexture(nil)
indicator[debuff].tex:SetAllPoints(indicator[debuff])
if indicator.size ~= indicator[debuff].size or disptype ~= indicator[debuff].disp then
local dispelColor = C_UnitAuras.GetAuraDispelTypeColor(debuff)
if disptype == "4" then
indicator[debuff].tex:SetTexture(pfUI.media["img:"..debuff])
indicator[debuff].tex:SetVertexColor(dispelColor:GetRGBA())
indicator[debuff].tex:Show()
indicator[debuff]:ClearAllPoints()
indicator[debuff]:SetSize(size, size)
indicator[debuff]:SetBackdrop(nil)
elseif disptype == "3" then
indicator[debuff].tex:SetTexture(dispelColor:GetRGBA())
indicator[debuff].tex:SetVertexColor(1,1,1,1)
indicator[debuff].tex:Show()
indicator[debuff]:ClearAllPoints()
indicator[debuff]:SetSize(size, size)
indicator[debuff]:SetBackdrop(nil)
elseif disptype == "2" then
indicator[debuff].tex:Hide()
indicator[debuff]:SetAllPoints(unit.hp.bar)
indicator[debuff]:SetBackdrop(glow)
indicator[debuff]:SetBackdropBorderColor(dispelColor:GetRGBA())
elseif disptype == "1" then
indicator[debuff].tex:SetTexture(dispelColor:GetRGBA())
indicator[debuff].tex:SetVertexColor(1,1,1,1)
indicator[debuff].tex:Show()
indicator[debuff]:SetAllPoints(unit.hp.bar)
indicator[debuff]:SetBackdrop(nil)
end
indicator[debuff].size = indicator.size
indicator[debuff].disp = indicator.disp
end
indicator[debuff].visible = nil
for i=1,16 do
local a = C_UnitAuras.GetDebuffDataByIndex(unitstr, i)
local dtype = a and a.dispelName
if dtype == debuff then
indicator[debuff].visible = true
end
end
if indicator[debuff].visible then
indicator[debuff]:Show()
indicator:Show()
indicator:SetAlpha(0)
if disptype == "4" or disptype == "3" then
indicator:SetAlpha(1)
elseif disptype == "2" then
indicator:SetAlpha(.4)
elseif disptype == "1" then
indicator:SetAlpha(.2)
end
if disptype == "4" or disptype == "3" then
indicator[debuff]:SetPoint("LEFT", indicator, "LEFT", count*(size+1), 0)
count = count + 1
end
else
indicator[debuff]:Hide()
end
end
if disptype == "4" or disptype == "3" then
indicator:SetWidth(count*(size+1))
end
elseif unit.hp.bar.debuffindicators then
unit.hp.bar.debuffindicators:Hide()
end
if not unit.indicators and unit.config.buff_indicator == "1" then
unit.indicators = pfUI.uf:SetupBuffIndicators(unit.config)
elseif not unit.indicators then
unit.indicators = {}
end
if not unit.indicator_custom and unit.config.buff_indicator == "1" then
unit.indicator_custom = {}
for k, v in pairs({strsplit("#", unit.config.custom_indicator)}) do
unit.indicator_custom[k] = string.lower(v)
end
elseif not unit.indicator_custom then
unit.indicator_custom = {}
end
local pos = 1
if table.getn(unit.indicators) > 0 then
for _, aura in ipairs(C_UnitAuras.GetUnitAuras(unitstr, "HELPFUL")) do
local texLower = string.lower(aura.icon)
local timeleft = aura.expirationTime > 0 and (aura.expirationTime - GetTime()) or nil
for _, filter in pairs(unit.indicators) do
if filter == texLower then
local hot = HOT_INDICATORS[texLower]
if hot and string.lower(aura.name) ~= hot.name then
break -- texture matches but name disambiguates (e.g. shared icon)
end
if hot then
local start, duration, prediction = libpredict:GetHotDuration(unitstr, hot.predict)
pfUI.uf:AddIcon(unit, pos, aura.icon, timeleft or prediction, aura.applications, tonumber(start), tonumber(duration))
else
pfUI.uf:AddIcon(unit, pos, aura.icon, timeleft, aura.applications)
end
pos = pos + 1
break
end
end
end
end
if table.getn(unit.indicator_custom) > 0 then
for _, aura in ipairs(C_UnitAuras.GetUnitAuras(unitstr, "HELPFUL")) do
local timeleft = aura.expirationTime > 0 and (aura.expirationTime - GetTime()) or nil
local lowerName = string.lower(aura.name)
for _, filter in pairs(unit.indicator_custom) do
if filter == lowerName then
pfUI.uf:AddIcon(unit, pos, aura.icon, timeleft, aura.applications)
pos = pos + 1
break
end
end
end
local debuffFilter = unit.config.selfdebuff == "1" and "HARMFUL|PLAYER" or "HARMFUL"
for i=1,16 do -- scan for custom debuffs
local aura = C_UnitAuras.GetAuraDataByIndex(unitstr, i, debuffFilter)
if aura then
local timeleft = aura.expirationTime > 0 and (aura.expirationTime - GetTime()) or nil
for _, filter in pairs(unit.indicator_custom) do
if filter == string.lower(aura.name) then
pfUI.uf:AddIcon(unit, pos, aura.icon, timeleft, aura.applications)
pos = pos + 1
break
end
end
end
end
end
-- hide unused icon slots
for pos=pos, 6 do pfUI.uf:HideIcon(unit, pos) end
end
-- portrait
if unit.portrait and ( component == "all" or component == "portrait" ) then
if C.unitframes.always2dportrait == "1" then
unit.portrait.tex:Show()
unit.portrait.model:Hide()
SetPortraitTexture(unit.portrait.tex, unitstr)
else
if not UnitIsVisible(unitstr) or not UnitIsConnected(unitstr) then
if unit.config.portrait == "bar" then
unit.portrait.tex:Hide()
unit.portrait.model:Hide()
elseif C.unitframes.portraittexture == "1" then
unit.portrait.tex:Show()
unit.portrait.model:Hide()
SetPortraitTexture(unit.portrait.tex, unitstr)
else
unit.portrait.tex:Hide()
unit.portrait.model:Show()
unit.portrait.model:SetModelScale(4.25)
unit.portrait.model:SetPosition(0, 0, -1)
unit.portrait.model:SetModel("Interface\\Buttons\\talktomequestionmark.mdx")
end
else
if unit.config.portrait == "bar" then
unit.portrait:SetAlpha(C.unitframes.portraitalpha)
else
unit.portrait:SetAlpha(1)
end
unit.portrait.tex:Hide()
unit.portrait.model:Show()
if component == "portrait" then
-- regular portrait update after event
unit.portrait.model.update = unitstr
else
-- detect portrait change without events
unit.portrait.model.next:SetUnit(unitstr)
if unit.portrait.model.lastUnit ~= UnitName(unitstr) or unit.portrait.model:GetModel() ~= unit.portrait.model.next:GetModel() then
unit.portrait.model.update = unitstr
end
end
end
end
end
-- base frame
if component == "all" or component == "base" then
-- Unit HP/MP with Nampower Integration
local hp, hpmax, power, powermax, powerType = pfUI.api.GetUnitStats(unitstr)
-- Store original values for color calculations (before invert_healthbar modifies hp)
local hp_orig, hpmax_orig = hp, hpmax
if unit.config.invert_healthbar == "1" then
hp = hpmax - hp
end
unit.hp.bar:SetMinMaxValues(0, hpmax, true)
unit.hp.bar:SetValue(hp)
unit.power.bar:SetMinMaxValues(0, powermax, true)
unit.power.bar:SetValue(power)
-- Hide power bar text for NPCs without real power (power == 0)
local isNPC = not UnitIsPlayer(unitstr) and not UnitPlayerControlled(unitstr)
local npcNoPower = isNPC and (not power or power == 0)
-- set healthbar color
local custom_active = nil
local customfullhp = unit.config.defcolor == "0" and unit.config.customfullhp or C.unitframes.customfullhp
local customcolor = unit.config.defcolor == "0" and unit.config.customcolor or C.unitframes.customcolor
local customfade = unit.config.defcolor == "0" and unit.config.customfade or C.unitframes.customfade
local custom = unit.config.defcolor == "0" and unit.config.custom or C.unitframes.custom
local r, g, b, a = .2, .2, .2, 1
-- O(1) optimization: use cached hp/hpmax instead of UnitHealth()/UnitHealthMax() API calls
if customfullhp == "1" and hp_orig == hpmax_orig then
r, g, b, a = GetStringColor(customcolor)
custom_active = true
elseif custom == "0" then
if UnitIsPlayer(unitstr) then
_, r, g, b = GetUnitColor(unitstr)
elseif unit.label == "pet" then
local happiness = GetPetHappiness()
if happiness == 1 then
r, g, b = 1, 0, 0
elseif happiness == 2 then
r, g, b = 1, 1, 0
else
r, g, b = 0, 1, 0
end
else
local color = UnitReactionColor[UnitReaction(unitstr, "player")]
if color then r, g, b = color.r, color.g, color.b end
end
elseif custom == "1" then
r, g, b, a = GetStringColor(customcolor)
custom_active = true
elseif custom == "2" then
-- O(1) optimization: use cached hp/hpmax instead of UnitHealth()/UnitHealthMax() API calls
if hpmax_orig > 0 then
r, g, b = GetColorGradient(hp_orig / hpmax_orig)
else
r, g, b = 0, 0, 0
end
end
if C.unitframes.pastel == "1" and not custom_active then
r, g, b = (r + .5) * .5, (g + .5) * .5, (b + .5) * .5
end
if customfade == "1" then
-- fade custom color into default color
-- O(1) optimization: use cached hp/hpmax instead of UnitHealth()/UnitHealthMax() API calls
local perc = hpmax_orig > 0 and (hp_orig / hpmax_orig) or 0
local cr, cg, cb, ca = GetStringColor(customcolor)
r = (cr*perc) + (r*(1-perc))
g = (cg*perc) + (g*(1-perc))
b = (cb*perc) + (b*(1-perc))
end
unit.hp.bar:SetStatusBarColor(r, g, b, a)
-- set powerbar color
local mana = unit.config.defcolor == "0" and unit.config.manacolor or C.unitframes.manacolor
local rage = unit.config.defcolor == "0" and unit.config.ragecolor or C.unitframes.ragecolor
local energy = unit.config.defcolor == "0" and unit.config.energycolor or C.unitframes.energycolor
local focus = unit.config.defcolor == "0" and unit.config.focuscolor or C.unitframes.focuscolor
local r, g, b, a = .5, .5, .5, 1
local utype = UnitPowerType(unitstr)
if utype == Enum.PowerType.Mana then
r, g, b, a = GetStringColor(mana)
elseif utype == Enum.PowerType.Rage then
r, g, b, a = GetStringColor(rage)
elseif utype == Enum.PowerType.Focus then
r, g, b, a = GetStringColor(focus)
elseif utype == Enum.PowerType.Energy then
r, g, b, a = GetStringColor(energy)
end
unit.power.bar:SetStatusBarColor(r, g, b, a)
if UnitName(unitstr) then
unit.hpLeftText:SetText(pfUI.uf:GetStatusValue(unit, "hpleft"))
unit.hpCenterText:SetText(pfUI.uf:GetStatusValue(unit, "hpcenter"))
unit.hpRightText:SetText(pfUI.uf:GetStatusValue(unit, "hpright"))
-- Hide power text for NPCs without a real power system
local cfgLeft = unit.config.txtpowerleft
local cfgCenter = unit.config.txtpowercenter
local cfgRight = unit.config.txtpowerright
if npcNoPower and cfgLeft and strfind(cfgLeft, "power") then
unit.powerLeftText:SetText("")
else
unit.powerLeftText:SetText(pfUI.uf:GetStatusValue(unit, "powerleft"))
end
if npcNoPower and cfgCenter and strfind(cfgCenter, "power") then
unit.powerCenterText:SetText("")
else
unit.powerCenterText:SetText(pfUI.uf:GetStatusValue(unit, "powercenter"))
end
if npcNoPower and cfgRight and strfind(cfgRight, "power") then
unit.powerRightText:SetText("")
else
unit.powerRightText:SetText(pfUI.uf:GetStatusValue(unit, "powerright"))
end
if UnitIsTapped(unitstr) and not UnitIsTappedByPlayer(unitstr) then
unit.hp.bar:SetStatusBarColor(.5,.5,.5,.5)
end
end
if unit.druidmana then pfUI.uf:UpdateDruidMana(unit) end
pfUI.uf:RefreshUnitState(unit)
end
end
local modifiers = {
[""] = "",
["alt"] = "_alt",
["ctrl"] = "_ctrl",
["shift"] = "_shift",
}
function pfUI.uf:EnableClickCast()
if self.config.clickcast ~= "1" then return end
for bid = 1, 5 do -- LeftButton, RightButton, MiddleButton, Button4, Button5
for modifier, mconf in pairs(modifiers) do
local bconf = bid == 1 and "" or bid
local action = pfUI_config.unitframes["clickcast"..bconf..mconf]
if action and action ~= "" then
local prefix = modifier ~= "" and (modifier .. "-") or ""
local low = string.lower(action)
if low == "menu" then
self:SetAttribute(prefix .. "type" .. bid, "menu")
elseif low == "target" then
self:SetAttribute(prefix .. "type" .. bid, "target")
elseif low == "focus" then
self:SetAttribute(prefix .. "type" .. bid, "focus")
elseif string.find(action, "^/") then
self:SetAttribute(prefix .. "type" .. bid, "macro")
self:SetAttribute(prefix .. "macrotext" .. bid, action)
else
self:SetAttribute(prefix .. "type" .. bid, "spell")
self:SetAttribute(prefix .. "spell" .. bid, action)
end
end
end
end
end
function pfUI.uf:AddIcon(frame, pos, icon, timeleft, stacks, start, duration)
local showtime = frame.config.indicator_time == "1" and true or nil
local showstacks = frame.config.indicator_stacks == "1" and true or nil
local position = frame.config.indicator_pos or "TOPLEFT"
local iconsize = tonumber(frame.config.indicator_size)
local spacing = tonumber(frame.config.indicator_spacing)
if not frame.hp then return end
local frame = frame.hp.bar
if pos > 6 or pos > ceil(frame:GetWidth() / iconsize) then return end
frame.icon = frame.icon or CreateFrame("Frame", nil, frame)
if not frame.icon[pos] then
frame.icon[pos] = CreateFrame("Frame", nil, frame.icon)
frame.icon[pos]:SetParent(frame)
frame.icon[pos].tex = frame.icon[pos]:CreateTexture("OVERLAY")
frame.icon[pos].tex:SetAllPoints(frame.icon[pos])
frame.icon[pos].tex:SetTexCoord(.08, .92, .08, .92)
frame.icon[pos].stacks = frame.icon[pos]:CreateFontString(nil, "OVERLAY")
frame.icon[pos].stacks:SetPoint("BOTTOMRIGHT", 0, 0)
frame.icon[pos].stacks:SetJustifyH("RIGHT")
frame.icon[pos].stacks:SetJustifyV("BOTTOM")
-- Check if parent frame has cooldown animation enabled
local parent_cooldown_anim = frame.config and tonumber(frame.config.cooldown_anim) or 1
if parent_cooldown_anim == 1 then
frame.icon[pos].cd = CreateFrame(COOLDOWN_FRAME_TYPE, nil, frame.icon[pos])
else
frame.icon[pos].cd = CreateFrame("Frame", nil, frame.icon[pos])
frame.icon[pos].cd.AdvanceTime = DoNothing
frame.icon[pos].cd.SetSequence = DoNothing
frame.icon[pos].cd.SetSequenceTime = DoNothing
end
frame.icon[pos].cd.pfCooldownStyleAnimation = 0
frame.icon[pos].cd.pfCooldownType = "ALL"
frame.icon[pos].cd:SetFrameLevel(48)
end
-- update icon configuration
if frame.icon[pos].iconsize ~= iconsize or frame.icon[pos].spacing ~= spacing then
frame.icon[pos]:SetSize(iconsize, iconsize)
frame.icon[pos]:SetPoint("TOPLEFT", frame.icon, "TOPLEFT", (pos-1)*(iconsize + spacing), 0)
frame.icon[pos].stacks:SetFont(pfUI.font_unit, math.max(iconsize/3, 10), "OUTLINE")
frame.icon[pos].iconsize = iconsize
frame.icon[pos].spacing = spacing
end
-- update icon
if frame.icon[pos].icon ~= icon then
frame.icon[pos].tex:SetTexture(icon)
frame.icon[pos].icon = icon
end
-- show remaining time if config is set
if showtime and start and duration and timeleft < 100 and iconsize > 9 then
CooldownFrame_SetTimer(frame.icon[pos].cd, start, duration, 1)
elseif showtime and timeleft and timeleft < 100 and iconsize > 9 then
CooldownFrame_SetTimer(frame.icon[pos].cd, GetTime(), timeleft, 1)
else
CooldownFrame_SetTimer(frame.icon[pos].cd, GetTime(), 0, 1)
end
-- show stacks if config is set
if showstacks and stacks and stacks > 1 and iconsize > 9 then
frame.icon[pos].stacks:SetText(stacks)
else
frame.icon[pos].stacks:SetText("")
end
-- update parent icon size
if frame.icon.iconsize ~= iconsize then
frame.icon:SetHeight(iconsize)
frame.icon.iconsize = iconsize
end
-- update parent position
if frame.icon.position ~= position then
frame.icon:ClearAllPoints()
frame.icon:SetPoint(position, frame, position, 0, 0)
frame.icon.position = position
end
frame.icon[pos]:Show()
frame.icon:SetWidth((pos-1)*(iconsize+spacing)+iconsize)
end
function pfUI.uf:HideIcon(frame, pos)
if not frame or not frame.hp or not frame.hp.bar then return end
local frame = frame.hp.bar
if frame.icon and frame.icon[pos] then
frame.icon[pos]:Hide()
end
end
function pfUI.uf:SetupDebuffFilter(allclasses)
local myclass = UnitClassBase("player")
local debuffs = {}
if myclass == "PALADIN" or myclass == "PRIEST" or myclass == "WARLOCK" or allclasses then
table.insert(debuffs, "Magic")
end
if myclass == "DRUID" or myclass == "PALADIN" or myclass == "SHAMAN" or allclasses then
table.insert(debuffs, "Poison")
end
if myclass == "PRIEST" or myclass == "PALADIN" or myclass == "SHAMAN" or allclasses then
table.insert(debuffs, "Disease")
end
if myclass == "DRUID" or myclass == "MAGE" or allclasses then
table.insert(debuffs, "Curse")
end
return debuffs
end
function pfUI.uf:SetupBuffIndicators(config)
local myclass = UnitClassBase("player")
local indicators = {}
if config.show_buffs == "1" then -- buffs
if myclass == "DRUID" then
-- Mark of the Wild
table.insert(indicators, "interface\\icons\\spell_nature_regeneration")
-- Gift of the Wild
table.insert(indicators, "interface\\icons\\spell_nature_giftofthewild")
-- Thorns
table.insert(indicators, "interface\\icons\\spell_nature_thorns")
end
if myclass == "PRIEST" then
-- Prayer Of Fortitude"
table.insert(indicators, "interface\\icons\\spell_holy_wordfortitude")
table.insert(indicators, "interface\\icons\\spell_holy_prayeroffortitude")
-- Prayer of Spirit
table.insert(indicators, "interface\\icons\\spell_holy_divinespirit")
table.insert(indicators, "interface\\icons\\spell_holy_prayerofspirit")
-- Shadow Protection
table.insert(indicators, "interface\\icons\\spell_shadow_antishadow")
table.insert(indicators, "interface\\icons\\spell_holy_prayerofshadowprotection")
-- Fear Ward
table.insert(indicators, "interface\\icons\\spell_holy_excorcism")
end
if myclass == "PALADIN" then
-- Blessing of Salvation
table.insert(indicators, "interface\\icons\\spell_holy_greaterblessingofsalvation")
table.insert(indicators, "interface\\icons\\spell_holy_sealofsalvation")
-- Blessing of Wisdom
table.insert(indicators, "interface\\icons\\spell_holy_sealofwisdom")
table.insert(indicators, "interface\\icons\\spell_holy_greaterblessingofwisdom")
-- Blessing of Sanctuary
table.insert(indicators, "interface\\icons\\spell_nature_lightningshield")
table.insert(indicators, "interface\\icons\\spell_holy_greaterblessingofsanctuary")
-- Blessing of Kings
table.insert(indicators, "interface\\icons\\spell_magic_magearmor")
table.insert(indicators, "interface\\icons\\spell_magic_greaterblessingofkings")
-- Blessing of Might
table.insert(indicators, "interface\\icons\\spell_holy_fistofjustice")
table.insert(indicators, "interface\\icons\\spell_holy_greaterblessingofkings")
-- Blessing of Light
table.insert(indicators, "interface\\icons\\spell_holy_prayerofhealing02")
table.insert(indicators, "interface\\icons\\spell_holy_greaterblessingoflight")
-- Blessing of Sacrifice
table.insert(indicators, "interface\\icons\\spell_holy_sealofsacrifice")
-- Blessing of Freedom
table.insert(indicators, "interface\\icons\\spell_holy_sealofvalor")
-- Blessing of Protection
table.insert(indicators, "interface\\icons\\spell_holy_sealofprotection")
end
if myclass == "WARLOCK" then
-- Fire Shield
table.insert(indicators, "interface\\icons\\spell_fire_firearmor")
-- Blood Pact
table.insert(indicators, "interface\\icons\\spell_shadow_bloodboil")
-- Soulstone
table.insert(indicators, "interface\\icons\\spell_shadow_soulgem")
-- Unending Breath
table.insert(indicators, "interface\\icons\\spell_shadow_demonbreath")
-- Detect Greater Invisibility or Detect Invisibility
table.insert(indicators, "interface\\icons\\spell_shadow_detectinvisibility")
-- Detect Lesser Invisibility
table.insert(indicators, "interface\\icons\\spell_shadow_detectlesserinvisibility")
-- Paranoia
table.insert(indicators, "interface\\icons\\Spell_Shadow_AuraOfDarkness")
end
if myclass == "WARRIOR" then
-- Battle Shout
table.insert(indicators, "interface\\icons\\ability_warrior_battleshout")
-- Commanding Shout (TBC)
table.insert(indicators, "interface\\icons\\ability_warrior_rallyingcry")
end
if myclass == "MAGE" then
-- Arcane Intellect
table.insert(indicators, "interface\\icons\\spell_holy_magicalsentry")
table.insert(indicators, "interface\\icons\\spell_holy_arcaneintellect")
-- Dampen Magic
table.insert(indicators, "interface\\icons\\spell_nature_abolishmagic")
-- Amplify Magic
table.insert(indicators, "interface\\icons\\spell_holy_flashheal")
end
if myclass == "HUNTER" then
-- Aspect of the Wild
table.insert(indicators, "interface\\icons\\spell_nature_protectionformnature")
-- Aspect of the Pack
table.insert(indicators, "interface\\icons\\ability_mount_whitetiger")
-- Misdirection (TBC)
table.insert(indicators, "interface\\icons\\ability_hunter_misdirection")
end
if myclass == "SHAMAN" then
-- Earth Shield (TBC)
table.insert(indicators, "interface\\icons\\spell_nature_skinofearth")
end
end
if config.show_procs == "1" then -- procs
if myclass == "SHAMAN" or config.all_procs == "1" then
-- Ancestral Fortitude
table.insert(indicators, "interface\\icons\\spell_nature_undyingstrength")
-- Healing Way
table.insert(indicators, "interface\\icons\\spell_nature_healingway")
-- Totemic Power (known issue: one conflicts with Blessed Sunfruit buff)
table.insert(indicators, "interface\\icons\\spell_holy_spiritualguidence")
table.insert(indicators, "interface\\icons\\spell_holy_devotion")
table.insert(indicators, "interface\\icons\\spell_holy_holynova")
table.insert(indicators, "interface\\icons\\spell_magic_magearmor")
end
if myclass == "PRIEST" or config.all_procs == "1" then
-- Inspiration
table.insert(indicators, "interface\\icons\\inv_shield_06")
end
end
if config.show_hots == "1" then -- hots
if myclass == "PRIEST" or config.all_hots == "1" then
-- Renew
table.insert(indicators, "interface\\icons\\spell_holy_renew")
-- Power Word: Shield
table.insert(indicators, "interface\\icons\\spell_holy_powerwordshield")
-- Prayer of Mending (TBC)
table.insert(indicators, "interface\\icons\\spell_holy_prayerofmendingtga")
end
if myclass == "DRUID" or config.all_hots == "1" then
-- Regrowth
table.insert(indicators, "interface\\icons\\spell_nature_resistnature")
-- Rejuvenation
table.insert(indicators, "interface\\icons\\spell_nature_rejuvenation")
-- Lifebloom
table.insert(indicators, "interface\\icons\\inv_misc_herb_felblossom")
end
end
if config.show_totems == "1" and myclass == "SHAMAN" then -- totems
-- Strength of Earth Totem
table.insert(indicators, "interface\\icons\\spell_nature_earthbindtotem")
-- Stoneskin Totem
table.insert(indicators, "interface\\icons\\spell_nature_stoneskintotem")
-- Mana Spring Totem
table.insert(indicators, "interface\\icons\\spell_nature_manaregentotem")
-- Mana Tide Totem
table.insert(indicators, "interface\\icons\\spell_frost_summonwaterelemental")
-- Healing Spring Totem
table.insert(indicators, "interface\\icons\\inv_spear_04")
-- Tranquil Air Totem
table.insert(indicators, "interface\\icons\\spell_nature_brilliance")
-- Grace of Air Totem
table.insert(indicators, "interface\\icons\\spell_nature_invisibilitytotem")
-- Grounding Totem
table.insert(indicators, "interface\\icons\\spell_nature_groundingtotem")
-- Nature Resistance Totem
table.insert(indicators, "interface\\icons\\spell_nature_natureresistancetotem")
-- Fire Resistance Totem
table.insert(indicators, "interface\\icons\\spell_fireresistancetotem_01")
-- Frost Resistance Totem
table.insert(indicators, "interface\\icons\\spell_frostresistancetotem_01")
end
return indicators
end
local function abbrevname(t)
return string.sub(t,1,1)..". "
end
function pfUI.uf:GetNameString(unitstr)
local name = UnitName(unitstr)
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
function pfUI.uf:GetLevelString(unitstr)
local level = UnitLevel(unitstr)
if level == -1 then level = "??" end
local elite = UnitClassification(unitstr)
if elite == "worldboss" then
level = level .. "B"
elseif elite == "rareelite" then
level = level .. "R+"
elseif elite == "elite" then
level = level .. "+"
elseif elite == "rare" then
level = level .. "R"
end
return level
end
function pfUI.uf:GetStatusValue(unit, pos)
if not pos or not unit then return end
local config = unit.config["txt"..pos]
local unitstr = unit.label .. unit.id
local frame = unit[pos .. "Text"]
-- as a fallback, draw the name
if pos == "center" and not config then
config = "unit"
end
-- Get stats with Nampower Integration
local hp, hpmax, mp, mpmax, powerType = pfUI.api.GetUnitStats(unitstr)
local rhp, rhpmax = hp, hpmax
-- Use libhealth for mob health estimation (overrides Nampower/Standard)
if pfUI.libhealth and pfUI.libhealth.enabled then
rhp, rhpmax = pfUI.libhealth:GetUnitHealth(unitstr)
end
if config == "unit" then
local name = unit:GetColor("unit") .. pfUI.uf:GetNameString(unitstr)
local level = unit:GetColor("level") .. pfUI.uf:GetLevelString(unitstr)
return level .. " " .. name
elseif config == "unitrev" then
local name = unit:GetColor("unit") .. pfUI.uf:GetNameString(unitstr)
local level = unit:GetColor("level") .. pfUI.uf:GetLevelString(unitstr)
return name .. " " .. level
elseif config == "name" then
return unit:GetColor("unit") .. pfUI.uf:GetNameString(unitstr)
elseif config == "nameshort" then
return unit:GetColor("unit") .. strsub(UnitName(unitstr), 0, 3)
elseif config == "level" then
return unit:GetColor("level") .. pfUI.uf:GetLevelString(unitstr)
elseif config == "class" then
if UnitIsPlayer(unitstr) then
return unit:GetColor("class") .. (UnitClass(unitstr) or UNKNOWN)
else
return ""
end
elseif config == "ownername" then
local owner
if unit.label == "raidpet" then owner = "raid" .. unit.id
elseif unit.label == "partypet" then owner = "party" .. unit.id
elseif unit.label == "pet" then owner = "player" end
if not (owner and UnitExists(owner)) then return "" end
local color = ""
if unit.config["classcolor"] == "1" and UnitIsPlayer(owner) then
local _, r, g, b = GetUnitColor(owner)
if C.unitframes.pastel == "1" then r, g, b = (r+.75)*.5, (g+.75)*.5, (b+.75)*.5 end
color = rgbhex(r, g, b)
end
return color .. pfUI.uf:GetNameString(owner)
-- health
elseif config == "health" then
return unit:GetColor("health") .. pfUI.api.Abbreviate(rhp)
elseif config == "healthmax" then
return unit:GetColor("health") .. pfUI.api.Abbreviate(rhpmax)
elseif config == "healthperc" then
return unit:GetColor("health") .. ceil(hp / hpmax * 100)
elseif config == "healthmiss" then
local health = ceil(rhp - rhpmax)
if UnitIsDead(unitstr) then
return unit:GetColor("health") .. DEAD
elseif health == 0 then
return unit:GetColor("health") .. "0"
else
return unit:GetColor("health") .. pfUI.api.Abbreviate(health)
end
elseif config == "healthdyn" then
if hp ~= hpmax then
return unit:GetColor("health") .. pfUI.api.Abbreviate(rhp) .. " - " .. ceil(hp / hpmax * 100) .. "%"
else
return unit:GetColor("health") .. pfUI.api.Abbreviate(rhp)
end
elseif config == "namehealth" then
local health = ceil(rhp - rhpmax)
if UnitIsDead(unitstr) then
return unit:GetColor("health") .. DEAD
elseif health == 0 then
return unit:GetColor("unit") .. pfUI.uf:GetNameString(unitstr)
else
return unit:GetColor("health") .. pfUI.api.Abbreviate(health)
end
elseif config == "namehealthbreak" then
local health = ceil(rhp - rhpmax)
if UnitIsDead(unitstr) then
return unit:GetColor("unit") .. pfUI.uf:GetNameString(unitstr) .. "\n" .. unit:GetColor("health") .. DEAD
elseif health == 0 then
return unit:GetColor("unit") .. pfUI.uf:GetNameString(unitstr)
else
return unit:GetColor("unit") .. pfUI.uf:GetNameString(unitstr) .. "\n" .. unit:GetColor("health") .. pfUI.api.Abbreviate(-health)
end
elseif config == "shortnamehealth" then
local health = ceil(rhp - rhpmax)
if UnitIsDead(unitstr) then
return unit:GetColor("health") .. DEAD
elseif health == 0 then
return unit:GetColor("unit") .. strsub(UnitName(unitstr), 0, 3)
else
return unit:GetColor("health") .. pfUI.api.Abbreviate(health)
end
elseif config == "healthminmax" then
return unit:GetColor("health") .. pfUI.api.Abbreviate(rhp) .. "/" .. pfUI.api.Abbreviate(rhpmax)
-- mana/power/focus
elseif config == "power" then
return unit:GetColor("power") .. pfUI.api.Abbreviate(mp)
elseif config == "powermax" then
return unit:GetColor("power") .. pfUI.api.Abbreviate(mpmax)
elseif config == "powerperc" then
local perc = UnitPowerMax(unitstr) > 0 and ceil(mp / mpmax * 100) or 0
return unit:GetColor("power") .. perc
elseif config == "powermiss" then
local power = ceil(mp - mpmax)
if power == 0 then
return unit:GetColor("power") .. "0"
else
return unit:GetColor("power") .. pfUI.api.Abbreviate(power)
end
elseif config == "powerdyn" then
-- show percentage when only mana is less than 100%
if mp ~= mpmax and UnitPowerType(unitstr) == Enum.PowerType.Mana then
return unit:GetColor("power") .. pfUI.api.Abbreviate(mp) .. " - " .. ceil(mp / mpmax * 100) .. "%"
else
return unit:GetColor("power") .. pfUI.api.Abbreviate(mp)
end
elseif config == "powerminmax" then
return unit:GetColor("power") .. pfUI.api.Abbreviate(mp) .. "/" .. pfUI.api.Abbreviate(mpmax)
else
return ""
end
end
function pfUI.uf.GetColor(self, preset)
local config = self.config
local unitstr = self.label .. self.id
local r, g, b = 1, 1, 1
if preset == "unit" and config["classcolor"] == "1" then
if UnitIsPlayer(unitstr) then
_, r, g, b = GetUnitColor(unitstr)
elseif self.label == "pet" then
local happiness = GetPetHappiness()
if happiness == 1 then
r, g, b = 1, 0, 0
elseif happiness == 2 then
r, g, b = 1, 1, 0
else
r, g, b = 0, 1, 0
end
else
local color = UnitReactionColor[UnitReaction(unitstr, "player")]
if color then r, g, b = color.r, color.g, color.b end
end
elseif preset == "class" and config["classcolor"] == "1" then
_, r, g, b = GetUnitColor(unitstr)
elseif preset == "reaction" and config["classcolor"] == "1" then
local color = UnitReactionColor[UnitReaction(unitstr, "player")]
r, g, b = color.r, color.g, color.b
elseif preset == "health" and config["healthcolor"] == "1" then
local hp, hpmax = UnitHealth(unitstr), UnitHealthMax(unitstr)
if hpmax and hpmax > 0 then
r, g, b = GetColorGradient(hp / hpmax)
else
r, g, b = 0, 0, 0
end
elseif preset == "power" and config["powercolor"] == "1" then
local color = ManaBarColor[UnitPowerType(unitstr)]
r, g, b = color.r, color.g, color.b
elseif preset == "level" and config["levelcolor"] == "1" then
local color = GetDifficultyColor(UnitLevel(unitstr))
r, g, b = color.r, color.g, color.b
end
if C.unitframes.pastel == "1" then
r, g, b = (r + .75) * .5, (g + .75) * .5, (b + .75) * .5
end
return rgbhex(r,g,b)
end