Files
benilla-octo/crates/benilla-app/assets/ui/UnitFrames.xml
T
SamandClaude Fable 5 a683d5524f the interface code is ours: the transcription debt is paid
- forty files that carried the reference's literal Lua are rewritten as our own expression of the behaviour — worst identical run 61 lines down to 7 — and the README now says exactly that
- the overlap gate measures the code itself across all four reference corpora, and ratchets: the number may only go down
- a lost session ends at the login screen instead of silently re-authenticating and stealing the account back
- a building's skybox is a real model: Caverns of Time's white ceiling becomes its painted nebula and additive stars

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 19:21:03 +07:00

2115 lines
126 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<Ui>
<!--
benilla's player + target unit frames (decision 0068 slice-1 game shell), RE-SKINNED to the
real 1.12 UI-TargetingFrame art + geometry (dressing arc, decision 0084). The geometry below
is QUOTED from the real FrameXML (scratchpad ref-PlayerFrame.xml, ref-TargetFrame.xml; line
citations inline), turned onto our own snapshot → Unit* → event data flow. The player and
target are horizontal MIRRORS of one shared art (Interface\TargetingFrame\UI-TargetingFrame):
the player samples it flipped (portrait window on the LEFT), the target un-flipped (window on
the RIGHT). The mirror is a `<TexCoords left> right`; it reaches the vertex buffer un-normalized
via the engine's UvRect (see crates/benilla/src/ui_pass.rs).
LAYERING (matches the ref's structure). The draw order must be trough < bars < ring art < text:
the frame's metal well-edges overlap the bar edges so the bars read RECESSED, not pasted on top.
The ref gets this with a child `TargetFrameTextureFrame` (ring art) and `SetFrameLevel(bar,
textureFrame-1)` to push the bars beneath it, and we do the same, in BenillaUnitFrame_OnLoad:
the parent holds trough + portrait (+ target's name plate) in BACKGROUND; the
`$parentHealthBar`/`$parentPowerBar` StatusBars sit at the PARENT's frame level; and
`$parentTextureFrame` (232×100, filling the parent) holds the ring art (BACKGROUND) + name/level/dead text
(OVERLAY) one level ABOVE them, so it draws over the bar edges and the portrait.
That SetFrameLevel is load-bearing, and this file used to do without it. Until decision 0884
the bars were held under the ring art by DECLARING `$parentTextureFrame` after them, leaning on
the old draw key's insertion-order tie-break between siblings at one `(strata, level)`. 0884
pinned the real law at the binary: the draw LAYER is bucket-wide and outranks the frame, so
siblings interleave by layer and the bars' ARTWORK fills painted straight over the TextureFrame's
BACKGROUND ring art (the frames read as pasted-on slabs — director report, 2026-08-02). Frame
level is the one term above the layer, which is exactly why the reference spends a level here.
Do not "simplify" this back into declaration order. The ring's art is a THIN
band with transparent corners (not an opaque surround), so the square is hidden by the portrait
itself — set via SetPortraitToTexture, masked to its inscribed circle (the client's portraits are
always round: a model render or a circular-cropped icon), so no corner reaches the ring's gap. The
text lives under the TextureFrame, so its globals carry the `…TextureFrame…` name prefix (OnLoad).
The shared black `…Background` trough sits behind the unfilled bar portion.
DATA FLOW UNCHANGED (pin: re-anchor + re-skin, don't rewrite the eventing): BenillaUnitFrame_*
handlers, the power-color table, hide-on-nonexistent-unit. Names resolve asynchronously through
the name-query cache (UNIT_NAME_UPDATE repaints). A powerless unit (maxPower 0) hides its power
bar, as the real frames do. The target frame's "DEAD" text shows when the target's health hits
zero (ref TargetFrame_CheckDead, ref-TargetFrame.lua l.220-228).
DEVIATIONS (noted, not bugs):
- Portrait: the live model bake (SetPortraitTexture(region, unit) → crates/benilla/src/portrait.rs),
the modern high-res 2D take on the ref's low-res model snapshot (director's call; wow-re
portrait RE §5-verified). Circle-cut at draw (ui_quad.wgsl, the ref's stencil shape); the
TemporaryPortrait stand-in shows while a model streams in; the bake poses a fresh
instance at Stand through the model's authored portrait camera, helms/shoulders/held
items riding their bones (portrait/).
- Bar value FontStrings (ref-PlayerFrame.xml l.79-96) are omitted — the real client hides them
unless the statusText CVar is set; we match the default look.
- TargetFrameNameBackground (the level plate) is faction-tinted (TargetFrame_CheckFaction,
ref-TargetFrame.lua l.144-191). A PLAYER-controlled target takes the player branch (red when
mutually hostile, yellow attackable-not-hostile, green when we can assist them but they are
PvP-flagged, else blue — a player never reads the *reaction* green); an NPC
takes the reaction swatch, resolved from the same faction-template / reputation decode the
selection ring uses (ui_unit.rs → UnitState.reaction → UnitReaction). The player branch's
two middle legs — yellow "attackable but not hostile" (l.158) and green "assist +
PvP-flagged" (l.163) — landed with the PvP wire (decision 0646); only the tapped-by-other
grey (l.176) is still deferred, on the tap wire. The ref gates the player
branch on UnitPlayerControlled (which also covers pets); we resolve its player half via
UnitIsPlayer, so a player-controlled PET takes the NPC reaction branch until that flag lands.
- The classification border swap (elite/rare art, ref-TargetFrame.lua l.205-218) LANDED with
decision 0782 — UnitClassification is bound and TargetFrame_CheckClassification
below runs the ref's branch table. Two things worth knowing at this altitude. The rank
`UnitClassification` reads is the client's GATED rank (`0x605620`): a pet or charmed unit
reports rank 0, so an enslaved elite wears the plain border, and because the tooltip's
ELITE/BOSS word and UnitLevel's world-boss skull read the same getter, the gate lives at
the app's single write (ui_unit.rs enrich_unit), not per reader. And UNIT_CLASSIFICATION_
CHANGED is the repaint wire, edge-fired on that rank — without it the border would only be
right on re-target, since Update is otherwise the sole caller.
- The level slot runs the full CheckLevel law (ref-TargetFrame.lua l.119-142, re-expressed in
Update below): corpse or "can't tell" (UnitLevel ≤ 0) → the HighLevelTexture skull;
otherwise the number, GetDifficultyColor-tinted when attackable, gold when not.
- The PVP icon landed with decision 0646 (the three-branch law in BenillaUnitFrame_UpdatePvP).
Its `PlayerPVPIconHitArea` did NOT, deliberately: the ref's hit area draws nothing at the
default settings — GameTooltip_AddNewbieTip with SHOW_NEWBIE_TIPS off and noNormalText=1
(which is exactly how it is called) shows no tooltip at all — while an enableMouse frame
over the portrait would eat the player frame's own hover and right-click. Zero pixels for a
real regression is not a trade; it comes back with the newbie-tip cvar if that ever ships.
RE-VERIFIED in 0652, both legs: ref-GameTooltip.lua l.107-123 takes the `not noNormalText`
branch when tips are off (so `noNormalText=1` → nothing), and "newbie" does not appear
anywhere in WoW.exe's strings, so SHOW_NEWBIE_TIPS has no cvar producer to be on.
- OUT (no wire / future arcs): leader/master/rest/attack icons, group indicator,
hit indicators, raid target icon, playtime, target-of-target.
TARGET AURA ROWS (0255's deferred slice): 5 buffs / 16 debuffs under the frame, geometry +
law QUOTED from ref-TargetFrame.xml (button templates l.26-87, instances l.293-481) and
TargetDebuffButton_Update (ref-TargetFrame.lua l.263-387): friend ⇒ buff row first, else
debuffs first and buffs below row 2; icons 21px shrinking to 17 when the debuff count
reaches the wrap; dispel-tinted UI-Debuff-Overlays border; stack count above 1; NO timers
(the 1.12 wire carries no duration for another unit — byte-verified, decision 0257 B6). The
list UnitBuff/UnitDebuff enumerate for the target is the app's straight descriptor read,
ascending raw slot within the half — the verified other-unit law (0257) — so icons sit in
server slot order, not insertion order, exactly like the reference. The target-of-target
branches take the not-shown leg (wrap 6, buffs under Debuff7) until a ToT frame exists.
-->
<!-- ── TargetBuffButtonTemplate (ref TargetFrame.xml l.9-27, VERBATIM) ──────────────────────
A reference template addons INHERIT and we did not declare. `CT_UnitFrames` builds its own
`CT_UnitFrames_BuffButtonTemplate` on top of it (CT_TargetFrame.xml:4), and an unresolved
`inherits=` is SILENT — no load error, no session error, nothing in any column. The addon
simply gets a button with no size, no icon region and no tooltip scripts, and nothing
anywhere says so. That invisibility is why this row only ever appeared in the report's
missing-TEMPLATES ranking.
Transcribed verbatim because every piece of it already resolves here: `GameTooltip:SetOwner`
and `GameTooltip:SetUnitBuff` are both ours, and the icon is a plain `setAllPoints` texture.
The `$parentIcon` name matters — that is what a consumer getglobals to set the texture. -->
<Button name="TargetBuffButtonTemplate" virtual="true">
<Size><AbsDimension x="21" y="21"/></Size>
<Layers>
<Layer level="ARTWORK">
<Texture name="$parentIcon" setAllPoints="true"/>
</Layer>
</Layers>
<Scripts>
<OnEnter>
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT", 15, -25);
GameTooltip:SetUnitBuff("target", this.id);
</OnEnter>
<OnLeave>
GameTooltip:Hide();
</OnLeave>
</Scripts>
</Button>
<Script><![CDATA[
-- The right-click-menu explanations the detailed-tooltip fork shows in place of the unit
-- lines (BenillaUnitFrame_OnEnter below) — ref GlobalStrings.lua l.2993/3081/2754/2755
-- (enUS), fallback form: the real localized GlobalStrings.lua runs before this file at
-- runtime (ui_script/mod.rs), so these only fill in for the tests. CombatText.xml's shape.
PARTY_OPTIONS_LABEL = PARTY_OPTIONS_LABEL or "Party Options"
PLAYER_OPTIONS_LABEL = PLAYER_OPTIONS_LABEL or "Player Options"
NEWBIE_TOOLTIP_PARTYOPTIONS = NEWBIE_TOOLTIP_PARTYOPTIONS or "If you belong to a party, you can right-click your portrait to bring up the Party Options menu. This menu will allow you to quit the party or see what looting rules the party is using. A party leader can add or remove party members, as well as change the party's looting rules."
NEWBIE_TOOLTIP_PLAYEROPTIONS = NEWBIE_TOOLTIP_PLAYEROPTIONS or "Right-click to bring up special commands for interacting with another player. You can inspect their equipment, issue a party invite, initiate a trade, or challenge a player to a duel. A group leader can promote or remove that player from the group."
-- The client's PowerBarColor values, keyed by the UnitPowerType token.
BENILLA_POWER_COLORS = {
MANA = { r = 0.0, g = 0.0, b = 1.0 },
RAGE = { r = 1.0, g = 0.0, b = 0.0 },
FOCUS = { r = 1.0, g = 0.5, b = 0.25 },
ENERGY = { r = 1.0, g = 1.0, b = 0.0 },
HAPPINESS = { r = 0.0, g = 1.0, b = 1.0 },
}
-- ── The raid-target mark atlas (ref TargetFrame.lua l.460-463 + l.475-484) ──────────────
-- `Interface\TargetingFrame\UI-RaidTargetingIcons` is one 256x256 sheet of 4x4 64px cells,
-- in mark order: star, circle, diamond, triangle, moon, square, cross, skull. The helper
-- turns a 1..8 mark index into that cell's texcoords on a CALLER-SUPPLIED texture — it owns
-- no frame and reads no unit, which is why an addon can hand it a texture of its own.
--
-- Three corpus addons do, in two codebases (`Optional` and `oRA2` ship the same two files):
-- CustomNameplates options.lua:363 (its options preview)
-- Optional / oRA2 MainTank.lua:1216, PlayerTarget.lua:1472
-- We answer `GetRaidTargetIndex(unit)` already (0434 §6); this is the other half — what an
-- addon DRAWS with that number — and without it those lines raise while redrawing a mark.
--
-- KEPT WHOLE, INCLUDING THE REFERENCE'S OWN SLIP: the row is derived with
-- `/ RAID_TARGET_TEXTURE_ROWS` while the column uses `mod(.., RAID_TARGET_TEXTURE_COLUMNS)`.
-- Rows and columns are both 4, so the two agree for every index this sheet holds and the
-- output is right; "fixing" it to divide by COLUMNS computes the same coords today and would
-- quietly diverge from the client the day a caller changes a constant. These are globals in
-- the reference for exactly that reason — addons read them, and two of ours are addons that
-- draw their own marks.
RAID_TARGET_ICON_DIMENSION = 64
RAID_TARGET_TEXTURE_DIMENSION = 256
RAID_TARGET_TEXTURE_COLUMNS = 4
RAID_TARGET_TEXTURE_ROWS = 4
function SetRaidTargetIconTexture(texture, raidTargetIconIndex)
raidTargetIconIndex = raidTargetIconIndex - 1
local left, right, top, bottom
local coordIncrement = RAID_TARGET_ICON_DIMENSION / RAID_TARGET_TEXTURE_DIMENSION
left = mod(raidTargetIconIndex, RAID_TARGET_TEXTURE_COLUMNS) * coordIncrement
right = left + coordIncrement
top = floor(raidTargetIconIndex / RAID_TARGET_TEXTURE_ROWS) * coordIncrement
bottom = top + coordIncrement
texture:SetTexCoord(left, right, top, bottom)
end
-- The client's UnitReactionColor table (ref-TargetFrame.lua l.6-14), indexed by
-- UnitReaction 1..8 (hated..exalted): 1-2 red, 3 orange, 4 neutral yellow, 5-7 green. The
-- name-plate reaction tint (TargetFrame_CheckFaction) reads it — this is what makes a neutral
-- beast's plate yellow, a hostile's red, a friendly's green.
BENILLA_REACTION_COLORS = {
{ r = 1.0, g = 0.0, b = 0.0 },
{ r = 1.0, g = 0.0, b = 0.0 },
{ r = 1.0, g = 0.5, b = 0.0 },
{ r = 1.0, g = 1.0, b = 0.0 },
{ r = 0.0, g = 1.0, b = 0.0 },
{ r = 0.0, g = 1.0, b = 0.0 },
{ r = 0.0, g = 1.0, b = 0.0 },
}
-- Wire an instance to its unit token (called from each frame's OnLoad), cache its child
-- widgets, register the event set, and do the first paint. deadText is target-only
-- (getglobal returns nil for the player, which has no DEAD region) — guarded at use.
function BenillaUnitFrame_OnLoad(frame, unit)
frame.unit = unit
local name = frame:GetName()
frame.healthBar = getglobal(name .. "HealthBar")
frame.powerBar = getglobal(name .. "PowerBar")
-- The name/level/dead text live inside $parentTextureFrame (so they draw over the ring art
-- that draws over the bars), so their global names carry the TextureFrame prefix.
local tf = name .. "TextureFrame"
-- The recessed-bar look, the reference's own mechanism (ref-TargetFrame.lua l.32-34):
-- push the bars one frame level BELOW the ring art's TextureFrame. Frame level is the
-- only key term above the draw layer (decision 0884), so this is the only thing that can
-- hold a BACKGROUND ring texture over an ARTWORK bar fill. See the LAYERING note up top.
local textureFrameLevel = getglobal(tf):GetFrameLevel()
frame.healthBar:SetFrameLevel(textureFrameLevel - 1)
frame.powerBar:SetFrameLevel(textureFrameLevel - 1)
-- The on-bar numerals (TextStatusBar.xml, decision 1082; the switch that shows them
-- is 1140's Status Bar Text). Wiring is unconditional — a frame with no text regions
-- gets a no-op, since SetTextStatusBarText drops a nil text and UpdateTextString skips
-- a bar with no TextString — so the target frame is one FontString pair away from
-- having them the day its hover lands. `textLockable` is the REFERENCE's own split
-- and the only conditional here: 1.12 sets it on the player's two bars, the pet's two
-- and the XP bar, and on nothing else, which is exactly why Status Bar Text pins your
-- own numbers and your pet's but never the target's (decision 1143).
if TextStatusBar_Initialize then -- engine tests load file subsets
TextStatusBar_Initialize(frame.healthBar)
TextStatusBar_Initialize(frame.powerBar)
SetTextStatusBarText(frame.healthBar, getglobal(tf .. "HealthBarText"))
SetTextStatusBarText(frame.powerBar, getglobal(tf .. "PowerBarText"))
if unit == "player" then
frame.healthBar.textLockable = 1
frame.powerBar.textLockable = 1
end
end
-- The hover seam (1146): who this bar belongs to, and what its click means.
local click = TargetFrame_OnClick
if unit == "player" then
click = PlayerFrame_OnClick
end
frame.healthBar.owner = frame
frame.powerBar.owner = frame
frame.healthBar.ownerClick = click
frame.powerBar.ownerClick = click
frame.nameText = getglobal(tf .. "NameText")
frame.levelText = getglobal(tf .. "LevelText")
frame.deadText = getglobal(tf .. "DeadText")
-- Target-only (the "??" skull on the level slot); getglobal returns nil for the
-- player, guarded at use.
frame.highLevel = getglobal(tf .. "HighLevelTexture")
-- Target-only (the level/name plate); getglobal returns nil for the player, guarded at use.
frame.nameBackground = getglobal(name .. "NameBackground")
-- The live model portrait (the real API's global, ref-UnitFrame.lua): bind the portrait
-- region to this frame's unit token; the app renders that unit's model off-screen and feeds
-- the bake back into the region (crates/benilla/src/portrait.rs). The binding is a token, so
-- one call suffices — the app keeps the image current per token (it re-bakes on target /
-- model change), which is why the ref's UNIT_PORTRAIT_UPDATE re-call isn't needed here (the
-- event joins the surface when addon parity wants it). Until a bake lands (no unit / model
-- still loading) the circle is simply empty.
frame.portrait = getglobal(name .. "Portrait")
SetPortraitTexture(frame.portrait, unit)
-- The PvP flag icon (decision 0646), under the TextureFrame like the ring art it
-- draws over — see BenillaUnitFrame_UpdatePvP.
frame.pvpIcon = getglobal(tf .. "PVPIcon")
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("UNIT_HEALTH")
frame:RegisterEvent("UNIT_MAXHEALTH")
frame:RegisterEvent("UNIT_LEVEL")
frame:RegisterEvent("UNIT_POWER_UPDATE")
frame:RegisterEvent("UNIT_MAXPOWER")
frame:RegisterEvent("UNIT_DISPLAYPOWER")
frame:RegisterEvent("UNIT_NAME_UPDATE")
-- The PvP icon's repaint wire (decision 0646 §2) — in the reference exactly the three
-- frames that draw the icon register it (PlayerFrame, TargetFrame, PartyMemberFrame)
-- and nothing else does.
frame:RegisterEvent("UNIT_FACTION")
frame:RegisterEvent("PLAYER_TARGET_CHANGED")
-- The aura rows and the classification border are target-only (ref TargetFrame_OnLoad
-- registers UNIT_AURA and UNIT_CLASSIFICATION_CHANGED; PlayerFrame registers neither,
-- and there is no CheckClassification for the player — your own frame never wears a
-- dragon). The ring-art region is the one the border swap re-textures, so only the
-- target caches the handle: its presence is what gates the swap in Update, the same
-- nil-handle idiom highLevel/nameBackground use above.
if unit == "target" then
frame:RegisterEvent("UNIT_AURA")
frame:RegisterEvent("UNIT_CLASSIFICATION_CHANGED")
frame.frameTexture = getglobal(tf .. "Texture")
end
-- The portrait hit indicator (decision 0576): only the player frame registers
-- UNIT_COMBAT in 1.12 (ref PlayerFrame_OnLoad — the target frame has no feedback
-- text). Ref: CombatFeedback_Initialize(PlayerHitIndicator, 30).
if unit == "player" then
frame.feedbackText = getglobal(tf .. "HitIndicator")
frame.feedbackFontHeight = 30
frame:RegisterEvent("UNIT_COMBAT")
-- The status flash (ref PlayerFrame_OnLoad, decision 1082): the resting zzz /
-- auto-attack swords under the portrait, the gold/red ring, and the pulse
-- counters. Handles cached like pvpIcon above; their presence is what gates
-- PlayerFrame_UpdateStatus in the shared Update.
frame.statusTexture = getglobal(tf .. "StatusTexture")
frame.attackBackground = getglobal(tf .. "AttackBackground")
frame.restIcon = getglobal(tf .. "RestIcon")
frame.attackIcon = getglobal(tf .. "AttackIcon")
frame.statusGlow = getglobal(name .. "StatusGlow")
frame.restGlow = getglobal(name .. "StatusGlowRestGlow")
frame.attackGlow = getglobal(name .. "StatusGlowAttackGlow")
frame.statusCounter = 0
frame.statusSign = -1
frame.attackBackground:SetVertexColor(0.8, 0.1, 0.1)
frame.attackBackground:SetAlpha(0.4)
-- Its four wires (ref PlayerFrame_OnLoad's registrations): auto-attack toggling
-- (ENTER/LEAVE_COMBAT), the aggro list (REGEN_DISABLED/ENABLED), and the rest
-- flag (UPDATE_RESTING); PLAYER_ENTERING_WORLD is registered above for all units.
frame:RegisterEvent("PLAYER_ENTER_COMBAT")
frame:RegisterEvent("PLAYER_LEAVE_COMBAT")
frame:RegisterEvent("PLAYER_REGEN_DISABLED")
frame:RegisterEvent("PLAYER_REGEN_ENABLED")
frame:RegisterEvent("PLAYER_UPDATE_RESTING")
end
-- A plain Frame isn't mouse-enabled by construction (only Button/EditBox are); enable it so
-- the hover tooltip fires (the real client's unit frames take the mouse to target/tooltip).
frame:EnableMouse(true)
UnitFrame_Update()
end
-- Hover: the engine's SetUnit — the byte-verified unit line law (decision 0276: name/
-- subtitle/level composition/PvP/Skinnable + the health bar), then the ref's reaction
-- recolor of line 1 (ref UnitFrame_OnEnter l.68-76: the FrameXML half of the split).
-- The plate goes to the DEFAULT corner, not beside the frame — ref UnitFrame_OnEnter
-- l.56 is GameTooltip_SetDefaultAnchor(GameTooltip, this): unit-frame tooltips live at
-- the screen's bottom-right like world hovers. (The TOOLTIP_UPDATE_TIME re-poll is not
-- ours: the engine's health watcher is the live half of it, and name/level don't drift
-- mid-hover yet.)
--
-- The DETAILED-TOOLTIP FORK (ref UnitFrame_OnEnter l.58-67, director-approved 0663) is the
-- surprising half: with tips on — 1.12's default, decision 0661 — the frame explains its
-- RIGHT-CLICK MENU instead of the unit, and returns BEFORE SetUnit, so the health/level
-- lines never render at all. Your own portrait always takes it; another player's takes it
-- whenever they're your target. The ref excepts PartyMemberFrame1-4 by name; ours are a
-- separate file with their own OnEnter (PartyFrame.xml), so there is nothing to except here.
--
-- One divergence, named: the ref gates the second branch on UnitPlayerControlled("target"),
-- which is true for a player OR a player's pet/charm. This engine has no such binding and no
-- flag to build one from — UnitIsPlayer is the nearest honest read, so a charmed mob or
-- another player's pet falls through to the unit tooltip where the reference would explain
-- the menu. The player case, which is the one anybody hovers, is exact.
-- ref UnitFrame_OnEnter l.55-57, ahead of everything else: the ONLY lit/grey cursor split
-- over a UI element in 1.12, and it is authored here in Lua rather than in the engine
-- (wow-re item-target-cursor-and-dropitemonunit.md 4.3). Bag and paper-doll slots have no
-- such branch, which is why they show the ordinary pointer.
-- A BAR's own hover (decision 1146; ref TextStatusBar's OnEnter/OnLeave on a mouse-enabled
-- bar). Two jobs, and the second is the one a naive transcription drops: reveal the numbers
-- while the pointer is on the bar, then hand the hover ON to the frame underneath, because
-- a mouse-enabled child swallows the parent's OnEnter — and with it the unit tooltip AND
-- the cast-cursor split that only exists over unit frames. Same for the click: the ref
-- routes its bars' OnMouseUp straight back to the frame's own handler (PlayerFrame.xml
-- l.304-306), which is what keeps a bar click targeting.
--
-- `owner`/`ownerClick` are stamped at load rather than read from GetParent(), because the
-- pet's bars are siblings of its TextureFrame, not children of the frame they belong to.
function BenillaUnitFrameBar_OnEnter(bar)
if TextStatusBar_UpdateTextString then
TextStatusBar_UpdateTextString(bar)
ShowTextStatusBarText(bar)
end
if bar.owner and BenillaUnitFrame_OnEnter and bar.owner.unit then
BenillaUnitFrame_OnEnter(bar.owner)
end
end
function BenillaUnitFrameBar_OnLeave(bar)
if HideTextStatusBarText then
HideTextStatusBarText(bar)
end
if bar.owner and BenillaUnitFrame_OnLeave and bar.owner.unit then
BenillaUnitFrame_OnLeave(bar.owner)
end
end
function BenillaUnitFrameBar_OnMouseUp(bar, button)
if bar.ownerClick then
bar.ownerClick(button)
end
end
-- The reference's names for the two unit-frame hover hooks, `this`-shaped, delegating to
-- ours. `TipBuddy.lua:2770-2773` is the exact idiom this exists for:
-- originalUnitFrame_OnEnter = UnitFrame_OnEnter
-- function UnitFrame_OnEnter() originalUnitFrame_OnEnter() ... end
-- Against a missing global that captured nil and raised on the first hover; against a
-- RENAMED one it would have called ours with no frame. See ActionBar.xml for the full note.
function UnitFrame_OnEnter()
BenillaUnitFrame_OnEnter(this)
end
function UnitFrame_OnLeave()
BenillaUnitFrame_OnLeave(this)
end
function BenillaUnitFrame_OnEnter(frame)
local unit = frame.unit
if SpellIsTargeting() then
if SpellCanTargetUnit(unit) then
SetCursor("CAST_CURSOR")
else
SetCursor("CAST_ERROR_CURSOR")
end
end
if not UnitExists(unit) then return end
GameTooltip_SetDefaultAnchor(GameTooltip, frame)
if SHOW_NEWBIE_TIPS == "1" then
if frame:GetName() == "PlayerFrame" then
BenillaTooltip_AddNewbieTipFor(frame, PARTY_OPTIONS_LABEL, 1.0, 1.0, 1.0,
NEWBIE_TOOLTIP_PARTYOPTIONS)
return
elseif UnitIsPlayer("target") and not UnitIsUnit("target", "player")
and not UnitIsUnit("target", "pet") then
BenillaTooltip_AddNewbieTipFor(frame, PLAYER_OPTIONS_LABEL, 1.0, 1.0, 1.0,
NEWBIE_TOOLTIP_PLAYEROPTIONS)
return
end
end
if GameTooltip:SetUnit(unit) then
getglobal("GameTooltipTextLeft1"):SetTextColor(GameTooltip_UnitColor(unit))
end
end
-- ref UnitFrame_OnLeave l.84-88: the fade is the tips-OFF branch. With detailed tips on the
-- plate HIDES outright — the ref's own pairing, and the right one: an explanation that
-- lingers on a ramp reads as a stuck tooltip, where a unit readout fading is the client's
-- signature. (Items Hide, units fade — that split still holds for the world mouseover.)
function BenillaUnitFrame_OnLeave(frame)
-- ref UnitFrame_OnLeave l.86: leaving a unit frame while still targeting goes to the
-- GREY cast cursor, not to the base one — you have left the only thing the armed spell
-- could have bound.
if SpellIsTargeting() then SetCursor("CAST_ERROR_CURSOR") end
if SHOW_NEWBIE_TIPS == "1" then
GameTooltip:Hide()
else
GameTooltip:FadeOut()
end
end
function UnitFrame_OnEvent(event)
local frame = this
-- UNIT_* events name their unit in arg1; ignore other units' updates.
if string.sub(event, 1, 5) == "UNIT_" and arg1 ~= frame.unit then
return
end
-- The hit-indicator feed (ref PlayerFrame_OnEvent's UNIT_COMBAT arm): no repaint —
-- the event carries feedback, not state.
if event == "UNIT_COMBAT" then
CombatFeedback_OnCombatEvent(frame, arg2, arg3, arg4, arg5)
return
end
-- The status flash's state arms (ref PlayerFrame_OnEvent l.100-115, decision 1082):
-- inCombat = auto-attack active, onHateList = on something's aggro list; a world
-- entry resets both (the ref's own PLAYER_ENTERING_WORLD arm). The repaint itself
-- rides the shared Update below — this file's posture (the PvP sound note).
if event == "PLAYER_ENTERING_WORLD" then
frame.inCombat = nil
frame.onHateList = nil
elseif event == "PLAYER_ENTER_COMBAT" then
frame.inCombat = 1
elseif event == "PLAYER_LEAVE_COMBAT" then
frame.inCombat = nil
elseif event == "PLAYER_REGEN_DISABLED" then
frame.onHateList = 1
elseif event == "PLAYER_REGEN_ENABLED" then
frame.onHateList = nil
end
UnitFrame_Update()
-- The player's flag-change sound (decision 0646 §2/§4). The reference plays
-- igPVPUpdate inside PlayerFrame_UpdatePvPStatus's two show branches, which it only
-- ever reaches from UNIT_FACTION; our repaint runs from the shared Update on every
-- event, so the sound rides the (edge-fired) event instead of the repaint — one
-- sound per real flag change, which is what the reference audibly does.
if event == "UNIT_FACTION" and frame.unit == "player" and frame.pvpShown then
PlaySound("igPVPUpdate")
end
end
-- The PvP flag icon (decision 0646 §4). The reference writes this law twice with
-- identical branches — PlayerFrame_UpdatePvPStatus (ref-PlayerFrame.lua l.55-79) and the
-- tail of TargetFrame_CheckFaction (ref-TargetFrame.lua l.193-202) — so it lives once
-- here. Branch ORDER is the reference's and it is load-bearing: free-for-all wins
-- outright (an FFA-flagged player shows the FFA skull even while ordinarily flagged), and
-- the faction leg needs BOTH a resolvable side and the flag. `frame.pvpShown` is what the
-- caller sounds off.
function BenillaUnitFrame_UpdatePvP(frame)
local icon = frame.pvpIcon
if not icon then return end
local unit = frame.unit
local texture
if UnitIsPVPFreeForAll(unit) then
texture = "Interface\\TargetingFrame\\UI-PVP-FFA"
else
local factionGroup = UnitFactionGroup(unit)
if factionGroup and UnitIsPVP(unit) then
texture = "Interface\\TargetingFrame\\UI-PVP-" .. factionGroup
end
end
frame.pvpShown = texture ~= nil
if texture then
icon:SetTexture(texture)
icon:Show()
else
icon:Hide()
end
end
-- ── Combat feedback: the portrait hit indicator (decision 0576) ──────────────────────
-- Our implementation of what ref CombatFeedback.lua specifies, on frame-param style (the
-- ref reads `this`). Timings, colors and size factors are its MEASURED values. The words
-- come from the GLOBALS, exactly as the ref builds this table (CombatFeedback.lua l.6-17:
-- CombatFeedbackText["MISS"] = TEXT(MISS)), so a localized install shows its own — the app
-- runs the real GlobalStrings.lua off the patch chain before this file (ui_script/mod.rs).
-- The enUS literals are the fallback for the test harnesses, which load no patch chain.
COMBATFEEDBACK_FADEINTIME = 0.2
COMBATFEEDBACK_HOLDTIME = 0.7
COMBATFEEDBACK_FADEOUTTIME = 0.3
CombatFeedbackText = {
INTERRUPT = INTERRUPT or "Interrupt",
MISS = MISS or "Miss",
RESIST = RESIST or "Resist",
DODGE = DODGE or "Dodge",
PARRY = PARRY or "Parry",
BLOCK = BLOCK or "Block",
EVADE = EVADE or "Evade",
IMMUNE = IMMUNE or "Immune",
DEFLECT = DEFLECT or "Deflect",
ABSORB = ABSORB or "Absorb",
REFLECT = REFLECT or "Reflect",
}
-- The indicator says one of two things: a NUMBER, when something moved a bar, or a WORD,
-- when nothing did. Everything below is that split. `scale` is a multiple of the frame's
-- own indicator height — 1 is "as authored" — and the multiples themselves are the
-- reference's measurements: half again for a big hit, three quarters for one that was
-- stopped, half for one that could never land.
-- The two events that move a bar the friendly way, and the colour each prints in. (A
-- WOUND prints a number too, but its colour depends on the school, so it is settled in
-- place below.)
local COMBAT_FEEDBACK_GAIN = {
HEAL = { r = 0.0, g = 1.0, b = 0.0 },
ENERGIZE = { r = 0.41, g = 0.8, b = 0.94 },
}
-- The words that do not print at the indicator's authored height. Everything else does.
local COMBAT_FEEDBACK_WORD_SCALE = {
IMMUNE = 0.5,
BLOCK = 0.75,
}
-- A wound that did no damage was stopped by one of three things, and in each case the
-- flag naming it is also the key of the word for it. Anything else was a plain miss.
local COMBAT_FEEDBACK_SOAKED = { ABSORB = 1, BLOCK = 1, RESIST = 1 }
-- What a zero-damage wound reads as: the word, and the size it prints at. Being stopped
-- reads small; missing outright reads full size, because a miss is the ordinary case.
local function CombatFeedback_Stopped(flags)
if COMBAT_FEEDBACK_SOAKED[flags] then
return CombatFeedbackText[flags], 0.75
end
return CombatFeedbackText["MISS"], 1.0
end
-- The size a landed blow reads at. A crushing blow is as loud as a critical; a glancing
-- one is as quiet as a soak.
local function CombatFeedback_HitScale(flags)
if flags == "CRITICAL" or flags == "CRUSHING" then
return 1.5
elseif flags == "GLANCING" then
return 0.75
end
return 1.0
end
function CombatFeedback_OnCombatEvent(frame, event, flags, amount, type)
local feedbackText = frame.feedbackText
if not feedbackText then return end
-- The default reading, which most events take unchanged: this event's own word, at the
-- authored height, in white. An event with no word at all prints nothing, which is the
-- honest thing to do with a feedback kind we do not have a string for.
local text = CombatFeedbackText[event]
local scale = 1.0
local r, g, b = 1.0, 1.0, 1.0
if event == "WOUND" then
if amount == 0 then
text, scale = CombatFeedback_Stopped(flags)
else
text = amount
scale = CombatFeedback_HitScale(flags)
-- A non-zero school means a spell landed it: spell damage prints yellow where
-- a physical hit prints white.
if type > 0 then
r, g, b = 1.0, 1.0, 0.0
end
end
else
local gain = COMBAT_FEEDBACK_GAIN[event]
if gain then
text = amount
r, g, b = gain.r, gain.g, gain.b
-- A critical heal or power gain reads big. Note this is the ONLY flag that
-- resizes one — a wound's crushing/glancing legs do not apply here.
if flags == "CRITICAL" then
scale = 1.5
end
else
scale = COMBAT_FEEDBACK_WORD_SCALE[event] or 1.0
end
end
frame.feedbackStartTime = GetTime()
feedbackText:SetTextHeight(frame.feedbackFontHeight * scale)
feedbackText:SetText(text)
feedbackText:SetTextColor(r, g, b)
feedbackText:SetAlpha(0.0)
feedbackText:Show()
end
-- The fade envelope (ref CombatFeedback_OnUpdate): 0.2 s ramp in, 0.7 s hold, 0.3 s
-- ramp out, then Hide. GetTime-based like the ref — no per-tick accumulation.
function CombatFeedback_OnUpdate(frame)
local feedbackText = frame.feedbackText
if not (feedbackText and feedbackText:IsVisible()) then return end
local elapsedTime = GetTime() - frame.feedbackStartTime
local fadeInTime = COMBATFEEDBACK_FADEINTIME
if elapsedTime < fadeInTime then
feedbackText:SetAlpha(elapsedTime / fadeInTime)
return
end
local holdTime = COMBATFEEDBACK_HOLDTIME
if elapsedTime < fadeInTime + holdTime then
feedbackText:SetAlpha(1.0)
return
end
local fadeOutTime = COMBATFEEDBACK_FADEOUTTIME
if elapsedTime < fadeInTime + holdTime + fadeOutTime then
feedbackText:SetAlpha(1.0 - (elapsedTime - holdTime - fadeInTime) / fadeOutTime)
return
end
feedbackText:Hide()
end
-- Pull the unit's current state and repaint. A non-existent unit hides the whole frame.
-- The classification border art (TargetFrame_CheckClassification, ref-TargetFrame.lua
-- l.205-218) — re-expressed branch for branch, and the shape is the surprise: 1.12 has FIVE
-- classifications but only THREE border textures, because elite, rareelite and worldboss all
-- take the same Elite art. There is no rare-elite border in this build and no rare-elite
-- tooltip word either (the level line's rank table is {"", ELITE, ELITE, BOSS, ""}), so
-- "rareelite" is a distinction the API makes and the pixels never do. Verified rather than
-- assumed, because later expansions DO ship UI-TargetingFrame-Rare-Elite and it would be the
-- natural thing to write: that path is absent from 1.12's patch chain entirely, and the ref
-- Lua sends rareelite to the Elite art.
--
-- SetTexture is the whole operation — no TexCoords, size or anchor changes. That is only
-- sound because all three BLPs are 256x128 (checked, not assumed), so the region's
-- XML-declared TexCoords (0.09375..1.0 x 0..0.78125 = the 232x100 crop) frame all three
-- identically, and our SetTexture touches only the path (it leaves tex_coords alone).
function TargetFrame_CheckClassification()
local frame = this
local classification = UnitClassification(frame.unit)
if classification == "worldboss" or classification == "rareelite" or classification == "elite" then
frame.frameTexture:SetTexture("Interface\\TargetingFrame\\UI-TargetingFrame-Elite")
elseif classification == "rare" then
frame.frameTexture:SetTexture("Interface\\TargetingFrame\\UI-TargetingFrame-Rare")
else
frame.frameTexture:SetTexture("Interface\\TargetingFrame\\UI-TargetingFrame")
end
end
function UnitFrame_Update()
local frame = this
local unit = frame.unit
if not UnitExists(unit) then
frame:Hide()
return
end
frame:Show()
frame.healthBar:SetMinMaxValues(0, UnitHealthMax(unit))
frame.healthBar:SetValue(UnitHealth(unit))
local maxPower = UnitManaMax(unit)
if maxPower > 0 then
frame.powerBar:Show()
local _, token = UnitPowerType(unit)
local c = BENILLA_POWER_COLORS[token] or BENILLA_POWER_COLORS.MANA
frame.powerBar:SetStatusBarColor(c.r, c.g, c.b)
-- NO label. 1.12 names the resource here (ref UnitFrame_UpdateManaType l.129 →
-- "Rage 0 / 100" off ManaBarColor's own prefix field, and CharacterFrame_OnLoad
-- labels health and XP the same way). Shipped in 1146, cut on the director's call
-- the same day: bare numbers read better on these bars (decision 1147). The
-- mechanism stays — SetTextStatusBarTextPrefix is live and one call brings any
-- label back — only the calls are gone.
frame.powerBar:SetMinMaxValues(0, maxPower)
frame.powerBar:SetValue(UnitMana(unit))
else
frame.powerBar:Hide()
end
frame.nameText:SetText(UnitName(unit) or "Unknown")
-- The level slot (TargetFrame_CheckLevel, ref-TargetFrame.lua l.119-142): a corpse
-- target or one whose level "can't be told" (UnitLevel ≤ 0 — the −1 return for a
-- world boss / a hostile 10+ levels up) shows the skull instead of the number; a
-- told level is difficulty-colored when the target is attackable (GetDifficultyColor,
-- the same con law the V-plate paints its level with) and plain gold when not.
-- The player frame has no skull region and shows its own level, always gold.
local level = UnitLevel(unit)
if frame.highLevel then
if UnitIsCorpse(unit) or level <= 0 then
frame.levelText:Hide()
frame.highLevel:Show()
else
frame.levelText:SetText(level)
if UnitCanAttack("player", unit) then
local color = GetDifficultyColor(level)
frame.levelText:SetVertexColor(color.r, color.g, color.b)
else
frame.levelText:SetVertexColor(1.0, 0.82, 0.0)
end
frame.levelText:Show()
frame.highLevel:Hide()
end
else
frame.levelText:SetText(level)
end
-- Tint the name/level plate (target only; TargetFrame_CheckFaction, ref-TargetFrame.lua
-- l.144-191). A PLAYER-CONTROLLED target takes the player branch (l.145-174); only an NPC
-- takes the reaction swatch (l.179-190): the resolved UnitReactionColor (neutral →
-- yellow), or blue when the reaction can't be told (nil) — a player never reads the
-- reaction green, which is why the two paths differ at all.
-- The player branch runs the ref's full ladder now that the
-- PvP wire carries it (decision 0646 §4): hostile red, else attackable-but-not-hostile
-- yellow (l.158), else "we can assist them but they're PvP-flagged" green (l.163), else
-- blue. Only the tapped-by-other grey (l.176) is still deferred — it needs the tap wire.
-- The ref's first leg tests mutual attackability (UnitCanAttack both ways); our
-- UnitCanAttack carries only the player→unit direction, so UnitIsEnemy (reaction ≤ 2)
-- stands in for it — the same substitution this branch has always made. (The ref gates on
-- UnitPlayerControlled, which also covers pets; UnitIsPlayer is the player half we can
-- resolve — see the header deviation.)
if frame.nameBackground then
if UnitIsPlayer(unit) then
local c
if UnitIsEnemy(unit, "player") then
c = BENILLA_REACTION_COLORS[2]
elseif UnitCanAttack("player", unit) then
c = BENILLA_REACTION_COLORS[4]
elseif UnitIsPVP(unit) then
c = BENILLA_REACTION_COLORS[6]
end
if c then
frame.nameBackground:SetVertexColor(c.r, c.g, c.b)
else
frame.nameBackground:SetVertexColor(0.0, 0.0, 1.0)
end
else
local reaction = UnitReaction(unit, "player")
local c = reaction and BENILLA_REACTION_COLORS[reaction]
if c then
frame.nameBackground:SetVertexColor(c.r, c.g, c.b)
else
frame.nameBackground:SetVertexColor(0.0, 0.0, 1.0)
end
end
end
-- The classification border (target only) — the ref's TargetFrame_Update calls
-- CheckClassification right here, between CheckFaction and CheckDead.
if frame.frameTexture then
TargetFrame_CheckClassification()
end
-- DEAD text (target only): shown at zero health (ref TargetFrame_CheckDead). Blanked
-- via SetText when alive — a site that predates region Show/Hide (region.rs has the
-- real pair now, the level slot above uses it); equivalent either way.
if frame.deadText then
if UnitHealth(unit) <= 0 then
frame.deadText:SetText("DEAD")
else
frame.deadText:SetText("")
end
end
-- The PvP flag icon, on both frames (decision 0646 §4).
BenillaUnitFrame_UpdatePvP(frame)
-- The status flash, player frame only (handle-gated like pvpIcon; decision 1082).
if frame.statusTexture then
PlayerFrame_UpdateStatus()
end
-- The aura rows (target only) — the ref's TargetFrame_Update →
-- TargetDebuffButton_Update: repainted on PLAYER_TARGET_CHANGED and UNIT_AURA alike.
if unit == "target" then
BenillaTargetAuras_Update()
end
end
-- ref TargetFrame.lua l.2-3.
MAX_TARGET_DEBUFFS = 16
MAX_TARGET_BUFFS = 5
-- TargetDebuffButton_Update (ref-TargetFrame.lua l.263-387), re-expressed onto the Era API
-- (decision 0255: UnitBuff returns name,icon,count,debuffType,… — the 1.12 original returned
-- the texture first). Only called with a live target (UnitFrame_Update guards).
-- DebuffTypeColor is defined by BuffFrame.xml, which loads AFTER this file — safe, because
-- this function only runs on events, never at load (no target exists at load).
function BenillaTargetAuras_Update()
local button
local numBuffs = 0
for i = 1, MAX_TARGET_BUFFS do
local name, icon = UnitBuff("target", i)
button = getglobal("TargetFrameBuff" .. i)
if name then
getglobal(button:GetName() .. "Icon"):SetTexture(icon or BENILLA_FALLBACK_ICON)
button:Show()
numBuffs = numBuffs + 1
else
button:Hide()
end
end
local numDebuffs = 0
for i = 1, MAX_TARGET_DEBUFFS do
local name, icon, count, debuffType = UnitDebuff("target", i)
button = getglobal("TargetFrameDebuff" .. i)
if name then
getglobal(button:GetName() .. "Icon"):SetTexture(icon or BENILLA_FALLBACK_ICON)
local color = DebuffTypeColor[debuffType] or DebuffTypeColor["none"]
local countText = getglobal(button:GetName() .. "Count")
if count and count > 1 then
countText:SetText(count)
else
countText:SetText("")
end
getglobal(button:GetName() .. "Border"):SetVertexColor(color.r, color.g, color.b)
button:Show()
numDebuffs = numDebuffs + 1
else
button:Hide()
end
end
-- The dynamic anchors (ref l.312-331). No target-of-target frame exists yet, so its
-- branches take the not-shown leg: wrap 6, buffs open under Debuff7.
local targetofTarget = false
local debuffWrap = 6
if UnitIsFriend("player", "target") then
TargetFrameBuff1:SetPoint("TOPLEFT", "TargetFrame", "BOTTOMLEFT", 5, 32)
TargetFrameDebuff1:SetPoint("TOPLEFT", "TargetFrameBuff1", "BOTTOMLEFT", 0, -2)
else
TargetFrameDebuff1:SetPoint("TOPLEFT", "TargetFrame", "BOTTOMLEFT", 5, 32)
TargetFrameBuff1:SetPoint("TOPLEFT", "TargetFrameDebuff7", "BOTTOMLEFT", 0, -2)
end
-- Shrink when the debuffs would overlap the frame (ref l.340-347): 21→17 icons,
-- 23→19 borders.
local debuffSize, debuffFrameSize
if (targetofTarget and numBuffs == 5) or numDebuffs >= debuffWrap then
debuffSize = 17
debuffFrameSize = 19
else
debuffSize = 21
debuffFrameSize = 23
end
for i = 1, 5 do
button = getglobal("TargetFrameBuff" .. i)
button:SetWidth(debuffSize)
button:SetHeight(debuffSize)
end
-- The ref resizes only the FIRST ROW's debuffs (l.358-368) — rows 2+ stay 17px always.
for i = 1, 6 do
button = getglobal("TargetFrameDebuff" .. i)
local border = getglobal(button:GetName() .. "Border")
border:SetWidth(debuffFrameSize)
border:SetHeight(debuffFrameSize)
button:SetWidth(debuffSize)
button:SetHeight(debuffSize)
end
-- Re-seat the wrap points (ref l.370-385) — constant with wrap pinned at 6, kept
-- verbatim so the ToT wrap-5 leg drops in unchanged.
getglobal("TargetFrameDebuff" .. debuffWrap):ClearAllPoints()
getglobal("TargetFrameDebuff" .. debuffWrap):SetPoint("LEFT", getglobal("TargetFrameDebuff" .. (debuffWrap - 1)), "RIGHT", 3, 0)
getglobal("TargetFrameDebuff" .. (debuffWrap + 1)):ClearAllPoints()
getglobal("TargetFrameDebuff" .. (debuffWrap + 1)):SetPoint("TOPLEFT", "TargetFrameDebuff1", "BOTTOMLEFT", 0, -2)
getglobal("TargetFrameDebuff" .. (debuffWrap + 2)):ClearAllPoints()
getglobal("TargetFrameDebuff" .. (debuffWrap + 2)):SetPoint("LEFT", getglobal("TargetFrameDebuff" .. (debuffWrap + 1)), "RIGHT", 3, 0)
if debuffWrap == 5 then
TargetFrameDebuff11:ClearAllPoints()
TargetFrameDebuff11:SetPoint("TOPLEFT", "TargetFrameDebuff6", "BOTTOMLEFT", 0, -2)
else
TargetFrameDebuff11:ClearAllPoints()
TargetFrameDebuff11:SetPoint("LEFT", "TargetFrameDebuff10", "RIGHT", 3, 0)
end
end
-- The aura hover (ref TargetDebuffButtonTemplate/TargetBuffButtonTemplate OnEnter,
-- l.60-62/l.79-81): the aura tooltip, seated BOTTOMRIGHT of the button.
function BenillaTargetAuraButton_OnEnter(button, harmful)
if not GameTooltip then return end -- engine tests load file subsets
GameTooltip:SetOwner(button, "ANCHOR_BOTTOMRIGHT", 15, -25)
if harmful then
GameTooltip:SetUnitDebuff("target", button:GetID())
else
GameTooltip:SetUnitBuff("target", button:GetID())
end
end
-- The status flash's paint (ref PlayerFrame_UpdateStatus, re-expressed onto frame-param
-- style; decision 1082). Priority is the ref's: RESTING wins over combat — sitting in an
-- inn mid-auto-attack shows the zzz — then the auto-attack state (gold ring/zzz vs red
-- ring/swords/red disc), then bare swords for on-someone's-aggro-list, then nothing.
function PlayerFrame_UpdateStatus()
local frame = this
if IsResting() then
frame.statusTexture:SetVertexColor(1.0, 0.88, 0.25, 1.0)
frame.statusTexture:Show()
frame.restIcon:Show()
frame.attackIcon:Hide()
frame.restGlow:Show()
frame.attackGlow:Hide()
frame.statusGlow:Show()
frame.attackBackground:Hide()
elseif frame.inCombat then
frame.statusTexture:SetVertexColor(1.0, 0.0, 0.0, 1.0)
frame.statusTexture:Show()
frame.attackIcon:Show()
frame.restIcon:Hide()
frame.attackGlow:Show()
frame.restGlow:Hide()
frame.statusGlow:Show()
frame.attackBackground:Show()
elseif frame.onHateList then
frame.attackIcon:Show()
frame.restIcon:Hide()
frame.statusGlow:Hide()
frame.attackBackground:Hide()
else
frame.statusTexture:Hide()
frame.restIcon:Hide()
frame.attackIcon:Hide()
frame.statusGlow:Hide()
frame.attackBackground:Hide()
end
end
-- ref PlayerFrame_OnUpdate: the status pulse — a 0.5 s triangle wave between alpha
-- 55/255 and 255/255 on the ADD-blend status ring and the icon glow, run only while the
-- ring is visible (resting or auto-attacking); the hit-indicator fade rides the same
-- tick, exactly as before.
function PlayerFrame_OnUpdate(elapsed)
local frame = this
if frame.statusTexture:IsVisible() then
local counter = frame.statusCounter + elapsed
local sign = frame.statusSign
if counter > 0.5 then
sign = -sign
frame.statusSign = sign
end
counter = mod(counter, 0.5)
frame.statusCounter = counter
local alpha
if sign == 1 then
alpha = (55 + counter * 400) / 255
else
alpha = (255 - counter * 400) / 255
end
frame.statusTexture:SetAlpha(alpha)
frame.statusGlow:SetAlpha(alpha)
end
CombatFeedback_OnUpdate(frame)
end
-- Left-click the player frame to target yourself (ref PlayerFrame_OnClick, the LeftButton
-- branch: an item on the cursor auto-equips, otherwise TargetUnit("player")); right-click
-- opens the SELF menu (ref's ToggleDropDownMenu at (106, 27), decision 0434 phase 5). The
-- ref's frame is a Button; ours is a mouse-enabled Frame, which fires OnClick on release
-- with arg1 = the button. The self-target itself is the byte-verified way the client
-- selects its own unit; the world-model click is a separate path, deliberately left out
-- (director's call).
function PlayerFrame_OnClick(button)
if button == "RightButton" then
ToggleDropDownMenu(1, nil, PlayerFrameDropDown, "PlayerFrame", 106, 27)
return
end
if button ~= "LeftButton" then return end
if CursorHasItem() then
AutoEquipCursorItem()
else
TargetUnit("player")
end
end
-- The right-click dropdowns (decision 0434 phase 5; ref TargetFrame.lua l.431-455,
-- PlayerFrame.lua l.235-241): MENU displayMode over the shared kit (UIDropDownMenu.xml),
-- the menus themselves from UnitPopup.xml. The menu pick is the ref's law: self → SELF,
-- party member → PARTY, other player → PLAYER, NPC → the bare raid-target icon list.
function TargetFrameDropDown_OnLoad()
UIDropDownMenu_Initialize(this, TargetFrameDropDown_Initialize, "MENU")
end
-- Which menu the CURRENT target gets, and the heading that menu needs (only the bare mark
-- list carries one — the unit menus title themselves from the unit). Most specific first:
-- yourself → SELF
-- your own pet → PET. Selecting your pet and right-clicking the TARGET
-- frame is still the pet's menu; the ref's own second
-- branch (TargetFrame.lua l.440-441), which sat as a
-- comment here until the "pet" token (0990) and then the
-- menu itself (1066).
-- anything that is not a player → an NPC, which gets only the raid-mark list
-- a player, in your party or not → PARTY / PLAYER
local function TargetFrameMenu()
if UnitIsUnit("target", "player") then
return "SELF"
end
if UnitIsUnit("target", "pet") then
return "PET"
end
if not UnitIsPlayer("target") then
return "RAID_TARGET_ICON", RAID_TARGET_ICON
end
if UnitInParty("target") then
return "PARTY"
end
return "PLAYER"
end
function TargetFrameDropDown_Initialize()
local menu, name = TargetFrameMenu()
if menu then
UnitPopup_ShowMenu(TargetFrameDropDown, menu, "target", name)
end
end
function PlayerFrameDropDown_OnLoad()
UIDropDownMenu_Initialize(this, PlayerFrameDropDown_Initialize, "MENU")
end
function PlayerFrameDropDown_Initialize()
UnitPopup_ShowMenu(PlayerFrameDropDown, "SELF", "player")
end
-- ref TargetFrame_OnClick: right-click opens the unit menu at (120, 10). The ref's
-- LeftButton branch (SpellTargetUnit / DropItemOnUnit) rides the spell-cursor and
-- item-drop slices — a bare left-click is a no-op on the ref too.
function TargetFrame_OnClick(button)
if button == "RightButton" then
ToggleDropDownMenu(1, nil, TargetFrameDropDown, "TargetFrame", 120, 10)
end
end
]]></Script>
<!--
PLAYER FRAME (ref-PlayerFrame.xml). Button 232×100 at UIParent TOPLEFT (-19,-4) (l.4-13); the
negative x trims the art's transparent left padding, seating the visible frame flush at the
screen's top-left, exactly as the real client does (our UIParent is the screen).
-->
<!-- frameStrata BACKGROUND is the ref's own (`PlayerFrame.xml` l.4) — the unit frames live
BELOW the MEDIUM panel windows on purpose, and the player frame shares the top-left corner
with the left-slot panel. See the note on PartyFrame.xml's template (decision 0597) for why
the default MEDIUM makes a unit frame's bar children draw through a window's background. -->
<Frame name="PlayerFrame" frameStrata="BACKGROUND">
<Size><AbsDimension x="232" y="100"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="-19" y="-4"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<!-- The bar trough: 119×41 black a=0.5 behind both bars (ref l.20-32). -->
<Texture name="$parentBackground">
<Size><AbsDimension x="119" y="41"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="106" y="-22"/></Offset></Anchor>
</Anchors>
<Color r="0" g="0" b="0" a="0.5"/>
</Texture>
<!-- Portrait 64×64 TOPLEFT (42,-12) (ref l.35-46, which puts it in ARTWORK; ours sits
in BACKGROUND, a leftover of the pre-0884 layering. The ring art now clears it by
frame LEVEL, and nothing else in this layer overlaps the portrait's square, so the
two are indistinguishable here — left alone rather than churned inside a bug fix).
Texture set in OnLoad via SetPortraitToTexture (circle-masked). -->
<Texture name="$parentPortrait">
<Size><AbsDimension x="64" y="64"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="42" y="-12"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
<!-- The ring art + name/level text live in $parentTextureFrame below, one frame level
ABOVE the bars, so they draw OVER the bar edges — the recessed-bar look. OnLoad puts
them there (SetFrameLevel); see the LAYERING note at the top of this file. -->
</Layers>
<Frames>
<!-- Health bar 119×12 TOPLEFT (106,-41), UI-StatusBar, green (ref l.288-315). -->
<StatusBar name="$parentHealthBar" enableMouse="true">
<Size><AbsDimension x="119" y="12"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="106" y="-41"/></Offset></Anchor>
</Anchors>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="0.0" g="1.0" b="0.0"/>
<Scripts>
<OnEvent>if TextStatusBar_OnEvent then TextStatusBar_OnEvent(arg1, arg2) end</OnEvent>
<OnValueChanged>if TextStatusBar_OnValueChanged then TextStatusBar_OnValueChanged() end</OnValueChanged>
<OnEnter>BenillaUnitFrameBar_OnEnter(self)</OnEnter>
<OnLeave>BenillaUnitFrameBar_OnLeave(self)</OnLeave>
<OnMouseUp>BenillaUnitFrameBar_OnMouseUp(self, arg1)</OnMouseUp>
</Scripts>
</StatusBar>
<!-- Power bar 119×12 TOPLEFT (106,-52), UI-StatusBar; color set at runtime (ref l.316-341). -->
<StatusBar name="$parentPowerBar" enableMouse="true">
<Size><AbsDimension x="119" y="12"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="106" y="-52"/></Offset></Anchor>
</Anchors>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="0.0" g="0.0" b="1.0"/>
<Scripts>
<OnEvent>if TextStatusBar_OnEvent then TextStatusBar_OnEvent(arg1, arg2) end</OnEvent>
<OnValueChanged>if TextStatusBar_OnValueChanged then TextStatusBar_OnValueChanged() end</OnValueChanged>
<OnEnter>BenillaUnitFrameBar_OnEnter(self)</OnEnter>
<OnLeave>BenillaUnitFrameBar_OnLeave(self)</OnLeave>
<OnMouseUp>BenillaUnitFrameBar_OnMouseUp(self, arg1)</OnMouseUp>
</Scripts>
</StatusBar>
<!-- The ring art + text, over the bars (ref TargetFrameTextureFrame). 232×100 fills the
parent; it draws over the bar edges because OnLoad pushes the bars one frame level
BELOW it (the ref's SetFrameLevel(bar, textureFrame-1)) — NOT because of where it
is declared. See the LAYERING note at the top of this file. -->
<Frame name="$parentTextureFrame">
<Size><AbsDimension x="232" y="100"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<!-- Ring art, MIRRORED: TexCoords left=1.0 right=0.09375 top=0 bottom=0.78125 (ref l.55-57). -->
<Texture name="$parentTexture" file="Interface\TargetingFrame\UI-TargetingFrame">
<Size><AbsDimension x="232" y="100"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<TexCoords left="1.0" right="0.09375" top="0" bottom="0.78125"/>
</Texture>
<!-- Name and level ride BACKGROUND with the ring art, like the ref's
(ref-PlayerFrame.xml l.54-97) — NOT the OVERLAY the state icons sit
in. The layer choice is load-bearing for the level number: the zzz /
crossed-swords badge (OVERLAY, below) sits exactly on the level
badge, and the client's bucket-wide texture-before-fontstring order
(decision 0884) means a fontstring can never hide UNDER a texture
of its own layer — the ref hides the number while resting by putting
it in a LOWER layer than the badge. Shipped in OVERLAY by 1082, the
number rode ON the badge (director report, 2026-08-07); decision 1093. -->
<!-- Name 100×10 CENTER (50,19) (ref l.58-69).
A DIVERGENCE, measured and deliberately kept. The reference names the
three unit-frame name FontStrings LITERALLY — `PlayerName`
(PlayerFrame.xml:58), `TargetName` (TargetFrame.xml:152), `PetName`
(PetFrame.xml:69) — where ours derive from `$parent`. That is 1222's
shadowed-global class on a region, and it was tried: the rename is
NOT free and it buys nothing.
· Not free — `UnitFrame_OnLoad` binds the region by COMPOSING the
name (`getglobal(tf .. "NameText")`, l.174), so a text search for
the derived global finds zero hits and the rename still breaks
every unit frame. It broke 40 tests. A name built inside
`getglobal` is invisible to a grep — the same trap that once left
CleanMinimap in the tail for three rounds.
· Buys nothing — the survey's "why" read-back matches ZERO
error rows for `TargetName` across
the 218-addon corpus. The one syntactic caller,
`CT_UnitFrames/CT_TargetFrame.xml:181`'s
`<Anchor relativeTo="TargetName">`, is not what stops that addon:
it dies at its own `CT_PlayerFrame.lua:206`.
So: 1222's rejected clause applies (a shadowed name with no observable
caller is building against a guess), and the cost lands on the frames
the director looks at every session. Revisit if a row ever names it. -->
<FontString name="$parentNameText" inherits="GameFontNormalSmall" text="Unknown">
<Size><AbsDimension x="100" y="10"/></Size>
<Anchors>
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="50" y="19"/></Offset></Anchor>
</Anchors>
</FontString>
<!-- Level number CENTER (-63,-16) (ref l.70-78). -->
<FontString name="$parentLevelText" inherits="GameFontNormalSmall">
<Anchors>
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="-63" y="-16"/></Offset></Anchor>
</Anchors>
</FontString>
<!-- The on-bar numerals, "value / max" (decision 1143). The reference's
own seats are CENTER (50,4) and (50,-8) (ref-PlayerFrame.xml l.79-96);
ours sit **1 px lower** — the director's call on the shipped result,
2026-08-08, and the deviation is recorded rather than the literals
(decision 1145). Why a digit string reads high at the ref's own offset:
a FontString centres on its LINE BOX, ascent + descent, and digits
occupy only the ascent — so the visible mass sits above the box centre
and a numerals-only string looks lifted inside a 12 px bar. The nudge
is optical, and it belongs on the seat rather than on the centring,
which every other string in the UI shares.
The LAYER is the reference's: BACKGROUND beside the name and level, not
the OVERLAY the state badges sit in, for the reason spelled out above.
They draw over the bar FILL because the whole TextureFrame outranks the
bars by a frame level. Empty until Status Bar Text is on or (later) the
bar is hovered — TextStatusBar.xml owns that decision, not this file. -->
<FontString name="$parentHealthBarText" inherits="TextStatusBarText">
<Anchors>
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="50" y="3"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="$parentPowerBarText" inherits="TextStatusBarText">
<Anchors>
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="50" y="-9"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
<Layer level="ARTWORK">
<!-- The PvP flag icon, 64×64 TOPLEFT (18,-20) (ref PlayerPVPIcon,
ref-PlayerFrame.xml l.124-135) — ARTWORK over the BACKGROUND ring art,
like the ref's. Texture chosen at runtime (decision 0646 §4); the ref's
PlayerPVPIconHitArea is deliberately OUT (see the header). -->
<Texture name="$parentPVPIcon" hidden="true">
<Size><AbsDimension x="64" y="64"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="18" y="-20"/></Offset></Anchor>
</Anchors>
</Texture>
<!-- The status ring (ref PlayerStatusTexture, ref-PlayerFrame.xml l.99-111):
the ADD-blend glow ring around the whole frame, tinted gold while
resting / red while auto-attacking, pulsed by the OnUpdate flash
(decision 1082). 190×66 TOPLEFT (35,-8), the ring art's own recess. -->
<Texture name="$parentStatusTexture" file="Interface\CharacterFrame\UI-Player-Status" alphaMode="ADD" hidden="true">
<Size><AbsDimension x="190" y="66"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="35" y="-8"/></Offset></Anchor>
</Anchors>
<TexCoords left="0" right="0.74609375" top="0" bottom="0.53125"/>
</Texture>
<!-- ref PlayerAttackBackground (l.112-123): the dark red disc behind the
attack state icon; OnLoad tints it (0.8,0.1,0.1) at alpha 0.4. -->
<Texture name="$parentAttackBackground" file="Interface\TargetingFrame\UI-TargetingFrame-AttackBackground" hidden="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="37" y="-50"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
<Layer level="OVERLAY">
<!-- The portrait hit indicator (ref PlayerHitIndicator, decision 0576):
CENTER at (73,-42) from the frame's TOPLEFT — the portrait circle's
center. Hidden until a UNIT_COMBAT event lands; CombatFeedback drives
text/size/color and the fade. -->
<FontString name="$parentHitIndicator" hidden="true" inherits="NumberFontNormalHuge">
<Anchors>
<Anchor point="CENTER" relativePoint="TOPLEFT"><Offset><AbsDimension x="73" y="-42"/></Offset></Anchor>
</Anchors>
</FontString>
<!-- The state icon pair under the portrait (ref PlayerRestIcon /
PlayerAttackIcon, ref-PlayerFrame.xml l.171-201): the two halves of
UI-StateIcon — zzz left, crossed swords right (decision 1082). -->
<Texture name="$parentRestIcon" file="Interface\CharacterFrame\UI-StateIcon" hidden="true">
<Size><AbsDimension x="31" y="33"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="37" y="-49"/></Offset></Anchor>
</Anchors>
<TexCoords left="0" right="0.5" top="0" bottom="0.421875"/>
</Texture>
<Texture name="$parentAttackIcon" file="Interface\CharacterFrame\UI-StateIcon" hidden="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRestIcon"><Offset><AbsDimension x="1" y="1"/></Offset></Anchor>
</Anchors>
<TexCoords left="0.5" right="1.0" top="0" bottom="0.5"/>
</Texture>
</Layer>
</Layers>
</Frame>
<!-- The state icon's own ADD-blend glow (ref PlayerStatusGlow, ref-PlayerFrame.xml
l.216-248): a sibling FRAME over the icon so the whole thing can pulse as one
SetAlpha group with the status ring (the OnUpdate flash, decision 1082). -->
<Frame name="$parentStatusGlow" hidden="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentTextureFrameRestIcon"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<Texture name="$parentRestGlow" file="Interface\CharacterFrame\UI-StateIcon" alphaMode="ADD">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"/>
</Anchors>
<TexCoords left="0" right="0.5" top="0.5" bottom="1.0"/>
</Texture>
<Texture name="$parentAttackGlow" file="Interface\CharacterFrame\UI-StateIcon" alphaMode="ADD">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"/>
</Anchors>
<TexCoords left="0.5" right="1.0" top="0.5" bottom="1.0"/>
</Texture>
</Layer>
</Layers>
</Frame>
</Frames>
<Scripts>
<OnLoad><![CDATA[BenillaUnitFrame_OnLoad(this, "player")]]></OnLoad>
<OnEvent><![CDATA[UnitFrame_OnEvent(event)]]></OnEvent>
<OnUpdate><![CDATA[PlayerFrame_OnUpdate(arg1)]]></OnUpdate>
<OnEnter><![CDATA[BenillaUnitFrame_OnEnter(this)]]></OnEnter>
<OnLeave><![CDATA[BenillaUnitFrame_OnLeave(this)]]></OnLeave>
<OnClick><![CDATA[PlayerFrame_OnClick(arg1)]]></OnClick>
</Scripts>
</Frame>
<!-- The SELF-menu dropdown host (ref $parentDropDown, PlayerFrame.xml): hidden state-holder
only — the list anchors to the player frame by name, never to this. -->
<Frame name="PlayerFrameDropDown" parent="PlayerFrame" inherits="UIDropDownMenuTemplate" hidden="true">
<Size><AbsDimension x="10" y="10"/></Size>
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="-20" y="-10"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>PlayerFrameDropDown_OnLoad()</OnLoad>
</Scripts>
</Frame>
<!-- ref TargetDebuffButtonTemplate (l.26-68): 17×17 icon, 19×19 UI-Debuff-Overlays border
(dispel-tinted at update), bottom-right stack count. Sizes are re-set every update by the
shrink law (first row only, verbatim ref quirk). -->
<Button name="BenillaTargetDebuffTemplate" virtual="true">
<Size><AbsDimension x="17" y="17"/></Size>
<Layers>
<Layer level="ARTWORK">
<Texture name="$parentIcon" setAllPoints="true"/>
</Layer>
<Layer level="OVERLAY">
<Texture name="$parentBorder" file="Interface\Buttons\UI-Debuff-Overlays">
<Size><AbsDimension x="19" y="19"/></Size>
<Anchors><Anchor point="CENTER"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0.296875" right="0.5703125" top="0" bottom="0.515625"/>
</Texture>
<FontString name="$parentCount" inherits="NumberFontNormalSmall">
<Anchors><Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-1" y="0"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnEnter>BenillaTargetAuraButton_OnEnter(this, true)</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Button>
<!-- ref TargetBuffButtonTemplate (l.69-87): 21×21 icon, no border, no count. -->
<Button name="BenillaTargetBuffTemplate" virtual="true">
<Size><AbsDimension x="21" y="21"/></Size>
<Layers>
<Layer level="ARTWORK">
<Texture name="$parentIcon" setAllPoints="true"/>
</Layer>
</Layers>
<Scripts>
<OnEnter>BenillaTargetAuraButton_OnEnter(this, false)</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Button>
<!--
TARGET FRAME (ref-TargetFrame.xml). Button 232×100 at UIParent TOPLEFT (250,-4) (l.88-98). The
un-mirrored twin of the player frame: portrait window on the RIGHT, bars anchored TOPRIGHT.
-->
<!-- hidden="true" is a deliberate deviation from the ref (whose TargetFrame starts shown and
gets its no-target Hide behind the loading screen, before audio is live): our OnLoad
Update runs with audio up, so starting shown would fire OnHide's lost-target sound at
every login. Steady-state behavior is identical — Update Shows/Hides on target changes. -->
<!-- frameStrata LOW is the ref's own (`TargetFrame.xml` l.88) — one above the player frame's
BACKGROUND, still below the MEDIUM panels. Same reasoning as PlayerFrame above. -->
<Frame name="TargetFrame" hidden="true" frameStrata="LOW">
<Size><AbsDimension x="232" y="100"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="250" y="-4"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<!-- Bar trough 119×41 black a=0.5 TOPRIGHT (-106,-22) (ref l.104-116). -->
<Texture name="$parentBackground">
<Size><AbsDimension x="119" y="41"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-106" y="-22"/></Offset></Anchor>
</Anchors>
<Color r="0" g="0" b="0" a="0.5"/>
</Texture>
<!-- The level/name plate 119×19 TOPRIGHT (-106,-22) (ref l.117-128) — target only,
untinted (see the header deviation on faction tinting). -->
<Texture name="$parentNameBackground" file="Interface\TargetingFrame\UI-TargetingFrame-LevelBackground">
<Size><AbsDimension x="119" y="19"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-106" y="-22"/></Offset></Anchor>
</Anchors>
</Texture>
<!-- Portrait 64×64 TOPRIGHT (-42,-12) (ref l.131-142, BORDER→BACKGROUND for the same
ring-over-portrait layering as the player). -->
<Texture name="$parentPortrait">
<Size><AbsDimension x="64" y="64"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-42" y="-12"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
<!-- The ring art + name/level/dead text live in $parentTextureFrame below (declared AFTER
the bars) so they draw OVER the bar edges — the recessed-bar look. -->
</Layers>
<Frames>
<!-- Health bar 119×12 TOPRIGHT (-106,-41), UI-StatusBar, green (ref l.252-273). -->
<StatusBar name="$parentHealthBar" enableMouse="true">
<Size><AbsDimension x="119" y="12"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-106" y="-41"/></Offset></Anchor>
</Anchors>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="0.0" g="1.0" b="0.0"/>
<Scripts>
<OnEvent>if TextStatusBar_OnEvent then TextStatusBar_OnEvent(arg1, arg2) end</OnEvent>
<OnValueChanged>if TextStatusBar_OnValueChanged then TextStatusBar_OnValueChanged() end</OnValueChanged>
<OnEnter>BenillaUnitFrameBar_OnEnter(self)</OnEnter>
<OnLeave>BenillaUnitFrameBar_OnLeave(self)</OnLeave>
<OnMouseUp>BenillaUnitFrameBar_OnMouseUp(self, arg1)</OnMouseUp>
</Scripts>
</StatusBar>
<!-- Power bar 119×12 TOPRIGHT (-106,-52), UI-StatusBar; color set at runtime (ref l.274-292). -->
<StatusBar name="$parentPowerBar" enableMouse="true">
<Size><AbsDimension x="119" y="12"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-106" y="-52"/></Offset></Anchor>
</Anchors>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="0.0" g="0.0" b="1.0"/>
<Scripts>
<OnEvent>if TextStatusBar_OnEvent then TextStatusBar_OnEvent(arg1, arg2) end</OnEvent>
<OnValueChanged>if TextStatusBar_OnValueChanged then TextStatusBar_OnValueChanged() end</OnValueChanged>
<OnEnter>BenillaUnitFrameBar_OnEnter(self)</OnEnter>
<OnLeave>BenillaUnitFrameBar_OnLeave(self)</OnLeave>
<OnMouseUp>BenillaUnitFrameBar_OnMouseUp(self, arg1)</OnMouseUp>
</Scripts>
</StatusBar>
<!-- The ring art + text, over the bars (ref TargetFrameTextureFrame); it draws over the
bar edges because OnLoad pushes the bars one frame level BELOW it (the ref's
SetFrameLevel(bar, textureFrame-1)) — NOT because of where it is declared. See the
LAYERING note at the top of this file. -->
<Frame name="$parentTextureFrame">
<Size><AbsDimension x="232" y="100"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<!-- Ring art, UN-mirrored: TexCoords left=0.09375 right=1.0 (ref l.146-151). -->
<Texture name="$parentTexture" file="Interface\TargetingFrame\UI-TargetingFrame">
<Size><AbsDimension x="232" y="100"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<TexCoords left="0.09375" right="1.0" top="0" bottom="0.78125"/>
</Texture>
</Layer>
<Layer level="ARTWORK">
<!-- The PvP flag icon, 64×64 TOPRIGHT (3,-20) (ref TargetPVPIcon,
ref-TargetFrame.xml l.208-219) — the player's icon mirrored to the
frame's other edge, same law (decision 0646 §4). -->
<Texture name="$parentPVPIcon" hidden="true">
<Size><AbsDimension x="64" y="64"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="3" y="-20"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
<Layer level="OVERLAY">
<!-- Name 100×10 CENTER (-50,19) (ref l.152-163). -->
<FontString name="$parentNameText" inherits="GameFontNormalSmall" text="Unknown">
<Size><AbsDimension x="100" y="10"/></Size>
<Anchors>
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="-50" y="19"/></Offset></Anchor>
</Anchors>
</FontString>
<!-- Level number CENTER (63,-16) (ref l.164-172). -->
<FontString name="$parentLevelText" inherits="GameFontNormalSmall">
<Anchors>
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="63" y="-16"/></Offset></Anchor>
</Anchors>
</FontString>
<!-- The "too high to tell" skull, 16×16 on the level slot (ref
TargetHighLevelTexture, l.184-195): shown for a corpse target or
UnitLevel ≤ 0, replacing the number (TargetFrame_CheckLevel). -->
<Texture name="$parentHighLevelTexture" file="Interface\TargetingFrame\UI-TargetingFrame-Skull">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="63" y="-16"/></Offset></Anchor>
</Anchors>
</Texture>
<!-- DEAD, CENTER (-50,3), shown at zero health (ref l.173-181). Blank while alive. -->
<FontString name="$parentDeadText" inherits="GameFontNormalSmall">
<Anchors>
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="-50" y="3"/></Offset></Anchor>
</Anchors>
</FontString>
<!-- The target's numerals (decision 1146), mirrored from the player's seats
the way the whole frame is: x = −50 against the player's +50, the same
1 px optical drop (1145). 1143 wired these bars and left the regions
out because nothing could ever paint them — 1.12 never makes the target
bars textLockable, so Status Bar Text does not pin them. The HOVER is
what they were waiting for, and it is the reference's behaviour whole:
on the target, the numbers are something you go and look at. -->
<FontString name="$parentHealthBarText" inherits="TextStatusBarText">
<Anchors>
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="-50" y="3"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="$parentPowerBarText" inherits="TextStatusBarText">
<Anchors>
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="-50" y="-9"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
<!-- The aura rows (ref l.293-481, names re-based on $parent). Static anchors are the
ref's; the update re-seats Debuff1/6/7/8/11 and Buff1 every pass (the friend swap +
the wrap law). Row 1 debuffs 1-6 (+3 gaps), row 2 debuffs 7-16, buffs below. -->
<Button name="$parentDebuff1" inherits="BenillaTargetDebuffTemplate" id="1">
<Anchors><Anchor point="TOPLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="5" y="32"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff2" inherits="BenillaTargetDebuffTemplate" id="2">
<Anchors><Anchor point="LEFT" relativeTo="$parentDebuff1" relativePoint="RIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff3" inherits="BenillaTargetDebuffTemplate" id="3">
<Anchors><Anchor point="LEFT" relativeTo="$parentDebuff2" relativePoint="RIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff4" inherits="BenillaTargetDebuffTemplate" id="4">
<Anchors><Anchor point="LEFT" relativeTo="$parentDebuff3" relativePoint="RIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff5" inherits="BenillaTargetDebuffTemplate" id="5">
<Anchors><Anchor point="LEFT" relativeTo="$parentDebuff4" relativePoint="RIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff6" inherits="BenillaTargetDebuffTemplate" id="6">
<Anchors><Anchor point="LEFT" relativeTo="$parentDebuff5" relativePoint="RIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff7" inherits="BenillaTargetDebuffTemplate" id="7">
<Anchors><Anchor point="TOPLEFT" relativeTo="$parentDebuff1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff8" inherits="BenillaTargetDebuffTemplate" id="8">
<Anchors><Anchor point="LEFT" relativeTo="$parentDebuff7" relativePoint="RIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff9" inherits="BenillaTargetDebuffTemplate" id="9">
<Anchors><Anchor point="LEFT" relativeTo="$parentDebuff8" relativePoint="RIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff10" inherits="BenillaTargetDebuffTemplate" id="10">
<Anchors><Anchor point="LEFT" relativeTo="$parentDebuff9" relativePoint="RIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff11" inherits="BenillaTargetDebuffTemplate" id="11">
<Anchors><Anchor point="LEFT" relativeTo="$parentDebuff10" relativePoint="RIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff12" inherits="BenillaTargetDebuffTemplate" id="12">
<Anchors><Anchor point="LEFT" relativeTo="$parentDebuff11" relativePoint="RIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff13" inherits="BenillaTargetDebuffTemplate" id="13">
<Anchors><Anchor point="LEFT" relativeTo="$parentDebuff12" relativePoint="RIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff14" inherits="BenillaTargetDebuffTemplate" id="14">
<Anchors><Anchor point="LEFT" relativeTo="$parentDebuff13" relativePoint="RIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff15" inherits="BenillaTargetDebuffTemplate" id="15">
<Anchors><Anchor point="LEFT" relativeTo="$parentDebuff14" relativePoint="RIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff16" inherits="BenillaTargetDebuffTemplate" id="16">
<Anchors><Anchor point="LEFT" relativeTo="$parentDebuff15" relativePoint="RIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentBuff1" inherits="BenillaTargetBuffTemplate" id="1">
<Anchors><Anchor point="TOPLEFT" relativeTo="$parentDebuff1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentBuff2" inherits="BenillaTargetBuffTemplate" id="2">
<Anchors><Anchor point="LEFT" relativeTo="$parentBuff1" relativePoint="RIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentBuff3" inherits="BenillaTargetBuffTemplate" id="3">
<Anchors><Anchor point="LEFT" relativeTo="$parentBuff2" relativePoint="RIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentBuff4" inherits="BenillaTargetBuffTemplate" id="4">
<Anchors><Anchor point="LEFT" relativeTo="$parentBuff3" relativePoint="RIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentBuff5" inherits="BenillaTargetBuffTemplate" id="5">
<Anchors><Anchor point="LEFT" relativeTo="$parentBuff4" relativePoint="RIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</Button>
</Frames>
<Scripts>
<OnLoad><![CDATA[BenillaUnitFrame_OnLoad(this, "target")]]></OnLoad>
<OnEvent><![CDATA[UnitFrame_OnEvent(event)]]></OnEvent>
<OnEnter><![CDATA[BenillaUnitFrame_OnEnter(this)]]></OnEnter>
<OnLeave><![CDATA[BenillaUnitFrame_OnLeave(this)]]></OnLeave>
<!-- The select/deselect pair rides the frame's visibility, exactly the ref
(TargetFrame_OnShow/OnHide, TargetFrame.lua l.104-117). In 5875 data all three
select kits resolve to the same iSelectTarget.wav — the reaction branch is
data-plumbing fidelity, not an audible difference. -->
<OnShow><![CDATA[
if UnitIsEnemy("target", "player") then
PlaySound("igCreatureAggroSelect")
elseif UnitIsFriend("player", "target") then
PlaySound("igCharacterNPCSelect")
else
PlaySound("igCreatureNeutralSelect")
end
]]></OnShow>
<!-- ref TargetFrame_OnHide: the lost-target sound + close any open unit menu (a
cleared target must not leave its menu acting on a stale unit). -->
<OnHide><![CDATA[
PlaySound("INTERFACESOUND_LOSTTARGETUNIT")
CloseDropDownMenus()
]]></OnHide>
<OnClick><![CDATA[TargetFrame_OnClick(arg1)]]></OnClick>
</Scripts>
</Frame>
<!-- The target-menu dropdown host (ref $parentDropDown, TargetFrame.xml): hidden
state-holder only, like the player's above. -->
<Frame name="TargetFrameDropDown" parent="TargetFrame" inherits="UIDropDownMenuTemplate" hidden="true">
<Size><AbsDimension x="10" y="10"/></Size>
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="-20" y="-10"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>TargetFrameDropDown_OnLoad()</OnLoad>
</Scripts>
</Frame>
<!--
THE PET FRAME (decision 0990). Geometry and art are MEASURED off ref PetFrame.xml +
PetFrame.lua and the behaviour is theirs; the code is our own. 128×53 parented to
the player frame at TOPLEFT (80,-60) — a static anchor, deliberately: the reference does not
list PetFrame in UIPARENT_MANAGED_FRAME_POSITIONS at all, because it rides its parent.
The art is the SMALL targeting frame, and it has two states the ref swaps at runtime
(PetFrame.lua's Update): `UI-SmallTargetingFrame` when the pet has a power bar, `-NoMana`
when it has none. That is not decoration — a warlock's imp has mana and a hunter's boar has
focus, but a skeleton has neither, and the plain art draws a mana rail with nothing in it.
HAPPINESS is no longer deferred — decision 1005 built it, off the carve 0990 was waiting
for. `GetPetHappiness 0x4be900` returns a bucket the CLIENT has already computed from
PetPersonality.dbc's own three thresholds, so nothing here is derived from vmangos and
0988's warning was answered rather than dodged. Its three siblings (`GetPetLoyalty`,
`GetPetTrainingPoints`, `GetPetExperience`) landed with it.
The PET RIGHT-CLICK MENU is no longer deferred either — decision 1066 built the last of
the five things it was waiting on (the pet paper doll, the fifth, landed as 1057). The
frame's non-left-click arm opens PetFrameDropDown below; the rows, their fork and their
dialogs are UnitPopup.xml's.
STILL DEFERRED, with its reason:
- PartyMemberBuffTooltip on hover (the ref's OnEnter panel): it belongs to the party
frames' own deferred buff panel, not here.
-->
<Script><![CDATA[
function PetFrame_OnLoad()
local frame = this
frame.portrait = PetFrameTextureFramePortrait
frame.healthBar = PetFrameHealthBar
frame.manaBar = PetFrameManaBar
frame.nameText = PetFrameTextureFrameName
-- The recessed-bar look, the same mechanism the player/target/party frames use
-- (decision 0884): push the bars one frame level BELOW the art's TextureFrame, because
-- the draw layer is bucket-wide and the frame level is the one key term above it.
local level = PetFrameTextureFrame:GetFrameLevel()
frame.healthBar:SetFrameLevel(level - 1)
frame.manaBar:SetFrameLevel(level - 1)
-- The pet's numerals, textLockable like the ref's (PetFrame.xml l.139/164; 1143).
if TextStatusBar_Initialize then -- engine tests load file subsets
TextStatusBar_Initialize(frame.healthBar)
TextStatusBar_Initialize(frame.manaBar)
SetTextStatusBarText(frame.healthBar, PetFrameTextureFrameHealthBarText)
SetTextStatusBarText(frame.manaBar, PetFrameTextureFrameManaBarText)
frame.healthBar.textLockable = 1
frame.manaBar.textLockable = 1
end
-- The pet's bars hover for their numbers, but hand nothing on: the pet frame has no
-- OnEnter of its own, and its click is its own handler (1146).
frame.healthBar.ownerClick = PetFrame_OnClick
frame.manaBar.ownerClick = PetFrame_OnClick
frame.attackModeCounter = 0
frame.attackModeSign = -1
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("UNIT_PET")
frame:RegisterEvent("UNIT_AURA")
frame:RegisterEvent("UNIT_HEALTH")
frame:RegisterEvent("UNIT_MAXHEALTH")
frame:RegisterEvent("UNIT_POWER_UPDATE")
frame:RegisterEvent("UNIT_MAXPOWER")
frame:RegisterEvent("UNIT_DISPLAYPOWER")
frame:RegisterEvent("UNIT_NAME_UPDATE")
frame:RegisterEvent("PET_ATTACK_START")
frame:RegisterEvent("PET_ATTACK_STOP")
frame:RegisterEvent("UNIT_HAPPINESS")
frame:EnableMouse(true)
PetFrame_Update()
end
-- ref PetFrame_OnEvent, with both of its argument gates kept: UNIT_PET names the OWNER
-- ("player" — wow-re §9 carves the arg), every other UNIT_* names the pet itself. Getting
-- those backwards is how a pet frame ends up repainting off the player's health.
function PetFrame_OnEvent(event)
local frame = this
if event == "PET_ATTACK_START" then
PetFrameAttackMode:SetVertexColor(1.0, 1.0, 1.0, 1.0)
PetFrameAttackMode:Show()
return
elseif event == "PET_ATTACK_STOP" then
PetFrameAttackMode:Hide()
return
elseif event == "UNIT_HAPPINESS" then
-- The ref's own last arm: repaint the icon alone, not the whole frame.
PetFrame_SetHappiness()
return
elseif event == "UNIT_PET" then
if arg1 ~= "player" then return end
elseif string.sub(event, 1, 5) == "UNIT_" and arg1 ~= "pet" then
return
end
PetFrame_Update()
end
function PetFrame_Update()
local frame = this
if not UnitExists("pet") then
frame:Hide()
return
end
frame:Show()
SetPortraitTexture(frame.portrait, "pet")
frame.nameText:SetText(UnitName("pet") or "")
frame.healthBar:SetMinMaxValues(0, UnitHealthMax("pet"))
frame.healthBar:SetValue(UnitHealth("pet"))
-- The ref's art swap (PetFrame.lua's Update): a powerless pet wears the -NoMana plate,
-- which has no mana rail to leave empty. The bar hides with it, as on every other frame.
local maxPower = UnitManaMax("pet")
if maxPower > 0 then
PetFrameTexture:SetTexture("Interface\\TargetingFrame\\UI-SmallTargetingFrame")
frame.manaBar:Show()
local _, token = UnitPowerType("pet")
local c = BENILLA_POWER_COLORS[token] or BENILLA_POWER_COLORS.MANA
frame.manaBar:SetStatusBarColor(c.r, c.g, c.b)
frame.manaBar:SetMinMaxValues(0, maxPower)
frame.manaBar:SetValue(UnitMana("pet"))
else
PetFrameTexture:SetTexture("Interface\\TargetingFrame\\UI-SmallTargetingFrame-NoMana")
frame.manaBar:Hide()
end
BenillaPetFrame_UpdateDebuffs()
PetFrame_SetHappiness()
end
-- ── The happiness face (decision 1005; ref PetFrame_SetHappiness, PetFrame.lua l.132-156)
--
-- `Interface\PetPaperDollFrame\UI-PetHappiness` is one row of three faces, and each cut is
-- a MEASURED value off that file. The buckets run RIGHT to left across the strip — bucket
-- 1 (unhappy) takes the rightmost cut and bucket 3 (happy) sits at x=0 — which is why the
-- left edges below descend as the index climbs. A bucket outside 1..3 selects no cell at
-- all and the icon keeps whatever face it was already wearing, deliberately: see the note
-- on bucket 0 below.
local PET_HAPPINESS_CELL = {
{ 0.375, 0.5625, 0, 0.359375 },
{ 0.1875, 0.375, 0, 0.359375 },
{ 0, 0.1875, 0, 0.359375 },
}
-- `GetPetHappiness` hands back a PRE-BUCKETED 1/2/3 — the client does the thresholding
-- against PetPersonality.dbc, not this function — plus the two numbers that bucket
-- selects. Three things here are load-bearing and each is the reference's own:
-- * the hide test is on the bucket being ABSENT, not on it being zero, so a bucket of
-- **0** (truthy in Lua) leaves the icon SHOWING with whatever texcoords it had.
-- Testing `happiness == 0` or coercing 0 to nil would hide a frame the reference shows.
-- * only a HUNTER's pet has a face at all — `HasPetUI`'s second return is the whole gate,
-- and its first return is nothing to do with this frame.
-- * the loyalty line is chosen by the SIGN of loyaltyRate, which is why that return is
-- unscaled and allowed to be negative. Exactly zero means "steady", and says nothing.
function PetFrame_SetHappiness()
local happiness, damagePercentage, loyaltyRate = GetPetHappiness()
local _, isHunterPet = HasPetUI()
if not (happiness and isHunterPet) then
PetFrameHappiness:Hide()
return
end
PetFrameHappiness:Show()
local cell = PET_HAPPINESS_CELL[happiness]
if cell then
PetFrameHappinessTexture:SetTexCoord(cell[1], cell[2], cell[3], cell[4])
end
PetFrameHappiness.tooltip = getglobal("PET_HAPPINESS" .. happiness)
PetFrameHappiness.tooltipDamage = format(PET_DAMAGE_PERCENTAGE, damagePercentage)
if loyaltyRate < 0 then
PetFrameHappiness.tooltipLoyalty = getglobal("LOSING_LOYALTY")
elseif loyaltyRate > 0 then
PetFrameHappiness.tooltipLoyalty = getglobal("GAINING_LOYALTY")
else
PetFrameHappiness.tooltipLoyalty = nil
end
end
-- The happiness face's hover. Both extra lines are drawn the same way — plain white,
-- nothing in the right-hand column — and either can be absent, so the stanza is written
-- once. The whole tooltip waits on the headline: until SetHappiness has run there is
-- nothing to say, and an empty plate is worse than none.
local function AddHappinessTooltipLine(text)
if text then
GameTooltip:AddLine(text, "", 1, 1, 1)
end
end
function BenillaPetHappiness_OnEnter(frame)
if not frame.tooltip then
return
end
GameTooltip:SetOwner(frame, "ANCHOR_RIGHT")
GameTooltip:SetText(frame.tooltip)
AddHappinessTooltipLine(frame.tooltipDamage)
AddHappinessTooltipLine(frame.tooltipLoyalty)
GameTooltip:Show()
end
-- The four debuff buttons (ref PetFrame.xml's $parentDebuff1..4 over RefreshBuffs). The pet
-- carries no aura timers — no unit but yourself does on the 1.12 wire (decision 0257 B6) —
-- so these are icon + border + stack count, the party rows' shape exactly.
function BenillaPetFrame_UpdateDebuffs()
for i = 1, 4 do
local button = getglobal("PetFrameDebuff" .. i)
local _, icon, count = UnitDebuff("pet", i)
if icon then
getglobal(button:GetName() .. "Icon"):SetTexture(icon)
local text = getglobal(button:GetName() .. "Count")
if count and count > 1 then
text:SetText(count)
text:Show()
else
text:Hide()
end
button:Show()
else
button:Hide()
end
end
end
-- ref PetFrame_OnUpdate: the attack overlay's alpha ramp — a 0.5 s-per-leg triangle between
-- 55/255 and 1, sign-flipped at each end (the ref's own counter shape, transcribed).
function PetFrame_OnUpdate(elapsed)
local frame = this
if not PetFrameAttackMode:IsVisible() then return end
local counter = frame.attackModeCounter + elapsed
local sign = frame.attackModeSign
if counter > 0.5 then
sign = -sign
frame.attackModeSign = sign
end
counter = mod(counter, 0.5)
frame.attackModeCounter = counter
local alpha
if sign == 1 then
alpha = (55 + (counter * 400)) / 255
else
alpha = (255 - (counter * 400)) / 255
end
PetFrameAttackMode:SetVertexColor(1.0, 1.0, 1.0, alpha)
end
-- Clicking the pet frame (decision 1066 built the right-hand half). The LEFT button hands
-- the pet whatever the cursor is carrying; every other button opens its menu — except for
-- the one case that is easy to drop, a RIGHT click with a spell already armed, which puts
-- the spell away instead. That exception is the right button's alone: a middle click while
-- casting still opens the menu.
function PetFrame_OnClick(button)
if button ~= "LeftButton" then
if button == "RightButton" and SpellIsTargeting() then
SpellStopTargeting()
else
-- No anchor arguments, unlike the player and target frames: this menu opens at
-- the CURSOR (ref PetFrame.lua l.128).
ToggleDropDownMenu(1, nil, PetFrameDropDown)
end
return
end
-- Most specific first: an armed spell binds to the pet, then a held item drops on it
-- (this is how you feed one), and with an empty cursor the click simply selects it.
if SpellIsTargeting() then
SpellTargetUnit("pet")
elseif CursorHasItem() then
DropItemOnUnit("pet")
else
TargetUnit("pet")
end
end
-- The PET menu's owner (ref PetFrame.lua l.158-166). The UnitExists gate is the ref's own,
-- and it is not redundant with the frame being hidden: this dropdown is also what the
-- TARGET frame's menu falls through to when you have your own pet selected.
function PetFrameDropDown_OnLoad()
UIDropDownMenu_Initialize(this, PetFrameDropDown_Initialize, "MENU")
end
function PetFrameDropDown_Initialize()
if UnitExists("pet") then
UnitPopup_ShowMenu(PetFrameDropDown, "PET", "pet")
end
end
function BenillaPetFrame_OnEnter(frame)
-- The pet frame is a unit frame in the reference, and takes the same cursor split:
-- hovering it with a spell armed lights or greys exactly as the target frame does.
if SpellIsTargeting() then
if SpellCanTargetUnit("pet") then
SetCursor("CAST_CURSOR")
else
SetCursor("CAST_ERROR_CURSOR")
end
end
if not UnitExists("pet") then return end
GameTooltip_SetDefaultAnchor(GameTooltip, frame)
if GameTooltip:SetUnit("pet") then
getglobal("GameTooltipTextLeft1"):SetTextColor(GameTooltip_UnitColor("pet"))
end
end
]]></Script>
<!-- The debuff button — ref PartyBuffButtonTemplate (PartyFrameTemplates.xml l.3-35), which is
what the ref's PetFrame inherits for its four: a 15×15 icon under a 17×17
UI-Debuff-Overlays border. Untinted: the ref's party/pet buttons never recolor it (only the
target frame's dispel-tint law does), so this stays the flat swatch the TexCoords select. -->
<Button name="BenillaPetDebuffTemplate" virtual="true" hidden="true">
<Size><AbsDimension x="15" y="15"/></Size>
<Layers>
<Layer level="ARTWORK">
<Texture name="$parentIcon" setAllPoints="true"/>
</Layer>
<Layer level="OVERLAY">
<Texture name="$parentBorder" file="Interface\Buttons\UI-Debuff-Overlays">
<Size><AbsDimension x="17" y="17"/></Size>
<Anchors><Anchor point="CENTER"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0.296875" right="0.5703125" top="0" bottom="0.515625"/>
</Texture>
<FontString name="$parentCount" inherits="NumberFontNormalSmall">
<Anchors><Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-1" y="0"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnEnter><![CDATA[
if GameTooltip then
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetUnitDebuff("pet", this:GetID())
end
]]></OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Button>
<!-- frameStrata LOW matches the party frames' and the target's — above the player frame's
BACKGROUND (which it overlaps), still below the MEDIUM panels. -->
<Frame name="PetFrame" parent="PlayerFrame" hidden="true" frameStrata="LOW">
<Size><AbsDimension x="128" y="53"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="80" y="-60"/></Offset></Anchor>
</Anchors>
<Frames>
<!-- The PET-menu dropdown host (ref $parentDropDown, PetFrame.xml l.35-51): a hidden
state-holder, like the player and target frames'. It carries the ref's own id=1
and its 10×10 size; nothing anchors to it, because this menu opens at the cursor.
It lives INSIDE the pet frame, which means it is only ever initialized once the
pet frame exists — the same nesting the reference uses. -->
<Frame name="PetFrameDropDown" inherits="UIDropDownMenuTemplate" id="1" hidden="true">
<Size><AbsDimension x="10" y="10"/></Size>
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="-20" y="-20"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>PetFrameDropDown_OnLoad()</OnLoad>
</Scripts>
</Frame>
<!-- Bars first, pushed one frame level under the TextureFrame at load (see OnLoad). -->
<StatusBar name="$parentHealthBar" enableMouse="true">
<Size><AbsDimension x="70" y="8"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="47" y="-22"/></Offset></Anchor></Anchors>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="0" g="1.0" b="0"/>
<Scripts>
<OnEvent>if TextStatusBar_OnEvent then TextStatusBar_OnEvent(arg1, arg2) end</OnEvent>
<OnValueChanged>if TextStatusBar_OnValueChanged then TextStatusBar_OnValueChanged() end</OnValueChanged>
<OnEnter>BenillaUnitFrameBar_OnEnter(self)</OnEnter>
<OnLeave>BenillaUnitFrameBar_OnLeave(self)</OnLeave>
<OnMouseUp>BenillaUnitFrameBar_OnMouseUp(self, arg1)</OnMouseUp>
</Scripts>
</StatusBar>
<StatusBar name="$parentManaBar" enableMouse="true">
<Size><AbsDimension x="70" y="8"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="47" y="-29"/></Offset></Anchor></Anchors>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="0" g="0" b="1.0"/>
<Scripts>
<OnEvent>if TextStatusBar_OnEvent then TextStatusBar_OnEvent(arg1, arg2) end</OnEvent>
<OnValueChanged>if TextStatusBar_OnValueChanged then TextStatusBar_OnValueChanged() end</OnValueChanged>
<OnEnter>BenillaUnitFrameBar_OnEnter(self)</OnEnter>
<OnLeave>BenillaUnitFrameBar_OnLeave(self)</OnLeave>
<OnMouseUp>BenillaUnitFrameBar_OnMouseUp(self, arg1)</OnMouseUp>
</Scripts>
</StatusBar>
<Frame name="$parentTextureFrame">
<Size><AbsDimension x="128" y="53"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Layers>
<Layer level="BACKGROUND">
<!-- Portrait 37×37 TOPLEFT (7,-6), under the art's round window (ref
PetPortrait, PetFrame.xml l.16-27). -->
<Texture name="$parentPortrait">
<Size><AbsDimension x="37" y="37"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="7" y="-6"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
<Layer level="BORDER">
<!-- 128×64 art at (0,-2) — it hangs 13 px below the frame rect, as
authored. The file is swapped at update (see the header). -->
<Texture name="PetFrameTexture" file="Interface\TargetingFrame\UI-SmallTargetingFrame">
<Size><AbsDimension x="128" y="64"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
<Layer level="ARTWORK">
<!-- The attacking overlay (ref PetAttackModeTexture, l.104-118): the right
29.7% of UI-Player-AttackStatus, ADD-blended, pulsed by OnUpdate. -->
<Texture name="PetFrameAttackMode" file="Interface\TargetingFrame\UI-Player-AttackStatus" alphaMode="ADD" hidden="true">
<Size><AbsDimension x="76" y="64"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="6" y="-9"/></Offset></Anchor></Anchors>
<TexCoords left="0.703125" right="1.0" top="0" bottom="1.0"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString name="$parentName" inherits="GameFontNormalSmall" justifyH="LEFT">
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="50" y="33"/></Offset></Anchor></Anchors>
</FontString>
<!-- The pet's numerals: the reference's LITERAL seats, CENTER-from-TOPLEFT
(82,-27) and (82,-38) (PetFrame.xml l.87-104). 1143 anchored these to
the bars' own centres instead, on a comparison that was simply wrong —
it read the ref's TEXT offsets against OUR BAR offsets and called the
pitch different. The bars are byte-identical to the reference's, 70x8
at (47,-22) and (47,-29), so the ref's text literals apply unchanged.
The overlap that produced (director report, 2026-08-08, decision 1147)
is what the literals exist to avoid: a 7 px bar pitch has no room for
two centred ~12 px strings, so the reference drops the SECOND one clear
of its bar (the mana bar ends at -37; its numerals sit at -38, just
under it) rather than centring it. Centring both is what collided.
No 1145 nudge here: that pixel was for a string centred in a bar, and
the reference has already placed these deliberately off-centre. -->
<FontString name="$parentHealthBarText" inherits="TextStatusBarText">
<Anchors><Anchor point="CENTER" relativePoint="TOPLEFT"><Offset><AbsDimension x="82" y="-27"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="$parentManaBarText" inherits="TextStatusBarText">
<Anchors><Anchor point="CENTER" relativePoint="TOPLEFT"><Offset><AbsDimension x="82" y="-38"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
<!-- HAPPINESS (ref PetFrameHappiness, PetFrame.xml l.233-253; decision 1005). 24×23
anchored LEFT to the frame's RIGHT at (-7,-4), so it sits just inside the plate's
right edge. The file is a three-icon strip and the AUTHORED TexCoords are
happiness 3's — the ref ships the happy face and re-cuts it at update. -->
<Frame name="$parentHappiness" enableMouse="true">
<Size><AbsDimension x="24" y="23"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="RIGHT"><Offset><AbsDimension x="-7" y="-4"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentTexture" file="Interface\PetPaperDollFrame\UI-PetHappiness" setAllPoints="true">
<Size><AbsDimension x="24" y="23"/></Size>
<TexCoords left="0" right="0.1875" top="0" bottom="0.359375"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnEnter>
BenillaPetHappiness_OnEnter(this)
</OnEnter>
<OnLeave>
GameTooltip:Hide()
</OnLeave>
</Scripts>
</Frame>
<!-- The four debuffs, ref geometry: the first at (48,-42), then +2 px chained right. -->
<Button name="$parentDebuff1" inherits="BenillaPetDebuffTemplate" id="1">
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="48" y="-42"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff2" inherits="BenillaPetDebuffTemplate" id="2">
<Anchors><Anchor point="LEFT" relativeTo="PetFrameDebuff1" relativePoint="RIGHT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff3" inherits="BenillaPetDebuffTemplate" id="3">
<Anchors><Anchor point="LEFT" relativeTo="PetFrameDebuff2" relativePoint="RIGHT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentDebuff4" inherits="BenillaPetDebuffTemplate" id="4">
<Anchors><Anchor point="LEFT" relativeTo="PetFrameDebuff3" relativePoint="RIGHT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor></Anchors>
</Button>
</Frames>
<Scripts>
<OnLoad>PetFrame_OnLoad()</OnLoad>
<OnEvent>PetFrame_OnEvent(event)</OnEvent>
<OnUpdate>PetFrame_OnUpdate(arg1)</OnUpdate>
<OnEnter>BenillaPetFrame_OnEnter(this)</OnEnter>
<OnLeave>if SpellIsTargeting() then SetCursor("CAST_ERROR_CURSOR") end GameTooltip:FadeOut()</OnLeave>
<OnClick>PetFrame_OnClick(arg1)</OnClick>
</Scripts>
</Frame>
</Ui>