Files
benilla-octo/crates/benilla-app/assets/ui/UnitFrames.xml
T
SamandClaude Fable 5 f6f2af3da9 the combat log's wire completes, and PlayerModel is its own class
- the combat log's last eight wire bodies decode — PARTYKILLLOG, SPELLINSTAKILLLOG, PROCRESIST, SPELLORDAMAGE_IMMUNE, SPELLDISPELLOG, DISPEL_FAILED, ENCHANTMENTLOG, SPELLLOGEXECUTE — each layout read off vmangos's own writer and pinned by a byte-exact golden (DISPEL_FAILED carries no count, the list runs to the end of the body; SPELLLOGEXECUTE's POWER_DRAIN row is (amount, power, multiplier), verified at the client's bytes as well as the server's); with the eleven chat types that were waiting on them, 44 chat types become 55 — 29 sentence families, the death pair and the aura rows riding descriptor diffs because 1.12 has no "unit died" opcode, plus the six trailers ((glancing), (30 blocked), (12 absorbed) and kin) that had been missing from every family; also corrected: the durability-death line was going to the red error frame with hard-coded English, and SMSG_SPELLNONMELEEDAMAGELOG lacked its split-damage branch and its blocked wording
- PlayerModel is its own frame kind: SetUnit, RefreshUnit and SetRotation were published on Model, but they belong to the <PlayerModel> method table (0x84f1fc), and the client's lookup chains derived → base only, so a plain <Model> can never reach them — they got there from a strings scan read as ownership, and a strings hit answers whether a name EXISTS, not who owns it (SetUnit's one pooled string is referenced by the PlayerModel and GameTooltip tables and by no Model entry); the new PlayerModel → Model → Frame → Region chain unblocks CreateFrame("PlayerModel"), which is how pfUI builds every unit-frame portrait — and through pfUI, pfQuest and ShaguDPS
- a nil where a number goes is four shapes, not one law: a census of all 408 widget-registrar entries found 62 numeric positions that validate-or-raise, 43 that gate then stage an alternate, 64 that silently coerce to 0.0 and 5 that gate then store nothing — so SetMaxLetters(nil) completes and stores 0, "no limit" (the argument COUNT is checked exactly, the type is not), and the backdrop colour setters are asymmetric on purpose: a nil r/g/b channel is 0.0 but a nil alpha is OPAQUE, and reading every nil as 0.0 would have turned those borders transparent; both setters now also quantize ×255 into the packed 0xAARRGGBB byte quad the client actually stores, so 0.09 reads back as 23/255
- two more addon-killers: LIGHTYELLOW_FONT_COLOR_CODE was the one missing line of Fonts.xml's seven-line colour block (aux-addon concatenates it at file scope and died on its own first line), and the guard now enumerates the whole block l.4-20 instead of the names an earlier round had noticed absent; and the unit frames publish the reference's names — 1.12 has no *PowerBar anywhere, every bar is ManaBar and the colour table is ManaBarColor (8 of the top-20 addons read it by name), so ManaBarColor is the published table our own bars index into, with 22 more regions renamed to the reference's own

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 19:54:58 +07:00

2655 lines
159 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`/`$parentManaBar` 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, but its GLOBAL names are the reference's flat ones
(`PlayerName`, `TargetDeadText`, …), not `$parent`-derived — see OnLoad's `short` prefix.
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.
- The rest/attack state icons landed with decision 1082, the hit indicator with 0576, and
the leader / master-looter pair with report B317 — the wire those three waited on has
existed since 0434/0453, and the list below is what is still genuinely unbuilt.
- The target-of-target frame landed with decision 1576 — its own block at the bottom of
this file, on the `"targettarget"` unit token the app now feeds.
- OUT (no wire / future arcs): group indicator, raid target icon, playtime.
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 are live since 1576: with that frame shown the rows wrap at 5 instead of 6 and
the buffs open under the second debuff row, because the frame stands where a six-icon row
would reach.
-->
<!-- ── 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."
-- ── ManaBarColor (ref UnitFrame.lua l.1-6, verbatim) ────────────────────────────────
-- The reference's own table: keyed by the NUMERIC power type `UnitPowerType` returns, each
-- row carrying the `prefix` its bar's text is labelled with. **8 addons in the top-20 root
-- read `ManaBarColor` by name**, which is why it is the published one rather than a
-- benilla-private table — we used to keep the same five colours under
-- `BENILLA_POWER_COLORS`, keyed by token string and carrying no prefix, so every one of
-- those reads found nil.
--
-- One table, not two: the token map below is DERIVED from it rather than restated, so a
-- colour can never differ between what an addon reads and what our own bars paint.
ManaBarColor = {}
ManaBarColor[0] = { r = 0.00, g = 0.00, b = 1.00, prefix = MANA or "Mana" }
ManaBarColor[1] = { r = 1.00, g = 0.00, b = 0.00, prefix = RAGE_POINTS or "Rage" }
ManaBarColor[2] = { r = 1.00, g = 0.50, b = 0.25, prefix = FOCUS_POINTS or "Focus" }
ManaBarColor[3] = { r = 1.00, g = 1.00, b = 0.00, prefix = ENERGY_POINTS or "Energy" }
ManaBarColor[4] = { r = 0.00, g = 1.00, b = 1.00, prefix = HAPPINESS_POINTS or "Happiness" }
-- Our own bars look colours up by the TOKEN `UnitPowerType` also returns (its 2nd value),
-- so this maps token → the row above. Same objects, so `ManaBarColor` is the only source.
BENILLA_POWER_COLORS = {
MANA = ManaBarColor[0],
RAGE = ManaBarColor[1],
FOCUS = ManaBarColor[2],
ENERGY = ManaBarColor[3],
HAPPINESS = ManaBarColor[4],
}
-- ── 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.manabar = getglobal(name .. "ManaBar")
-- TWO prefixes, both the reference's own. The bars are named off the FRAME
-- (`PlayerFrameHealthBar`, ref-PlayerFrame.xml l.288) but the art, text and icons are
-- named off the UNIT (`PlayerName` l.58, `PlayerPortrait` l.35, `TargetDeadText`
-- ref-TargetFrame.xml l.173) — flat globals, which is what an addon reaches for.
-- `short` is the frame name minus its trailing "Frame".
local short = string.sub(name, 1, string.len(name) - 5)
-- Our ring art rides a $parentTextureFrame child (the reference nests two ANONYMOUS
-- frames there instead, ref-PlayerFrame.xml l.49-51), so the frame whose level the bars
-- are pushed under is still named off the frame.
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.manabar: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.manabar)
SetTextStatusBarText(frame.healthbar, getglobal(name .. "HealthBarText"))
SetTextStatusBarText(frame.manabar, getglobal(name .. "ManaBarText"))
if unit == "player" then
frame.healthbar.textLockable = 1
frame.manabar.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.manabar.owner = frame
frame.healthbar.ownerClick = click
frame.manabar.ownerClick = click
frame.nameText = getglobal(short .. "Name")
frame.levelText = getglobal(short .. "LevelText")
frame.deadText = getglobal(short .. "DeadText")
-- Target-only (the "??" skull on the level slot); getglobal returns nil for the
-- player, guarded at use.
frame.highLevel = getglobal(short .. "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(short .. "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(short .. "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(name .. "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(short .. "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(short .. "StatusTexture")
frame.attackBackground = getglobal(short .. "AttackBackground")
frame.restIcon = getglobal(short .. "RestIcon")
frame.attackIcon = getglobal(short .. "AttackIcon")
frame.statusGlow = getglobal(short .. "StatusGlow")
frame.restGlow = getglobal(short .. "RestGlow")
frame.attackGlow = getglobal(short .. "AttackGlow")
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")
-- The leader / master-looter icons and their four group wires (ref
-- PlayerFrame_OnLoad's remaining registrations; report B317). Handle-gated in the
-- shared Update exactly like statusTexture above — the target frame declares
-- neither texture, so it never runs the law, and the party member frames run
-- their own (PartyFrame.xml, a different predicate: an INDEX comparison).
frame.leaderIcon = getglobal(short .. "LeaderIcon")
frame.masterIcon = getglobal(short .. "MasterIcon")
frame:RegisterEvent("PARTY_MEMBERS_CHANGED")
frame:RegisterEvent("PARTY_LEADER_CHANGED")
frame:RegisterEvent("PARTY_LOOT_METHOD_CHANGED")
frame:RegisterEvent("RAID_ROSTER_UPDATE")
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
-- ── One unit's two bars, from its token ───────────────────────────────────────────────
-- Three frames in this file paint exactly this, and until 1576 each wrote it out: the
-- health bar's range and value, then the power bar's colour off BENILLA_POWER_COLORS and
-- its own range and value — or no power bar at all, this file's convention for a unit with
-- no resource. Returns whether the unit HAS one, which is what the pet frame swaps its
-- plate on (the reference has no such art for the others and leaves an empty rail drawn).
--
-- The bars are ARGUMENTS rather than fields: the player and target frames call theirs
-- `manabar` and the pet frame calls its `manabar`, following the reference's own two names
-- for one rail, and a helper that reached for a field would have to pick a side.
--
-- NO label on the power bar. 1.12 names the resource there (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.
function BenillaUnitFrame_PaintBars(healthbar, manabar, unit)
healthbar:SetMinMaxValues(0, UnitHealthMax(unit))
healthbar:SetValue(UnitHealth(unit))
local maxPower = UnitManaMax(unit)
if maxPower <= 0 then
manabar:Hide()
return false
end
manabar:Show()
local _, powerToken = UnitPowerType(unit)
local swatch = BENILLA_POWER_COLORS[powerToken] or BENILLA_POWER_COLORS.MANA
manabar:SetStatusBarColor(swatch.r, swatch.g, swatch.b)
manabar:SetMinMaxValues(0, maxPower)
manabar:SetValue(UnitMana(unit))
return true
end
function UnitFrame_Update()
local frame = this
local unit = frame.unit
if not UnitExists(unit) then
frame:Hide()
return
end
frame:Show()
BenillaUnitFrame_PaintBars(frame.healthbar, frame.manabar, unit)
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), ref TargetFrame_CheckDead (l.220-228) whole: Show/Hide a
-- region whose word the XML already carries. This site used to SetText("DEAD") a
-- LITERAL and blank it when alive, which put the KEY on screen in caps — `DEAD` is a
-- GlobalStrings key (l.898 = "Dead"), not the string, and typing a key as display text
-- is the "CREATE_MACROS" failure of 0983/0991 again. Nothing here names the word now:
-- it comes from the FontString's `text="DEAD"` attribute, which the loader resolves
-- through the global (loader/mod.rs resolve_text), locale and all.
--
-- `UnitIsConnected` is the reference's own second term and is what tells dead from
-- LINK-dead: a disconnected player reads 0 health here and is not dead. The ToT twin
-- (TargetofTarget_CheckDead) has carried it since it was written; this leg had not.
if frame.deadText then
if UnitHealth(unit) <= 0 and UnitIsConnected(unit) then
frame.deadText:Show()
else
frame.deadText:Hide()
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 leader / master-looter icons, player frame only (report B317).
if frame.leaderIcon then
BenillaPlayerFrame_UpdatePartyLeader(frame)
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). The target-of-target frame stands at the
-- target frame's bottom-RIGHT (decision 1576), which is where a six-icon row of
-- debuffs would reach: hence the wrap at 5 and, with a full buff row, the shrink
-- below. Both legs are the reference's, and both are live now that the frame exists.
local targetofTarget = TargetofTargetFrame:IsShown()
local debuffWrap = 6
if targetofTarget then
debuffWrap = 5
end
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)
if targetofTarget then
-- ref l.318-326 writes this as THREE branches (< 5, 5..9, >= 10) whose first
-- two are character-for-character identical; the buffs open under the second
-- debuff row until there is a third. Two branches say the same thing.
if numDebuffs >= 10 then
TargetFrameBuff1:SetPoint("TOPLEFT", "TargetFrameDebuff11", "BOTTOMLEFT", 0, -2)
else
TargetFrameBuff1:SetPoint("TOPLEFT", "TargetFrameDebuff6", "BOTTOMLEFT", 0, -2)
end
else
TargetFrameBuff1:SetPoint("TOPLEFT", "TargetFrameDebuff7", "BOTTOMLEFT", 0, -2)
end
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). Written verbatim while the wrap was
-- pinned at 6 so the ToT wrap-5 leg would drop in unchanged — it did (1576), and
-- both directions are covered: every button the wrap-5 pass moves is put back by the
-- wrap-6 pass, and Debuff5 is only ever re-seated onto the anchor it is authored with.
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
-- The leader / master-looter icons on the player's own frame (ref
-- PlayerFrame_UpdatePartyLeader, re-expressed onto frame-param style; report B317).
--
-- Two DIFFERENT predicates, and the asymmetry is the reference's own. The leader icon asks
-- IsPartyLeader() — a boolean about us — where the member frames ask whether the leader
-- INDEX equals their id, because the player has no index on that scale (0 is "the player
-- leads", not "party0"). The master icon then asks whether GetLootMethod's second return
-- is 0, which is that same scale reading "the player is master looter"; the group test
-- beside it is the reference's own guard, and it is not redundant with ours — a solo
-- player's master_looter is None, so `masterId == 0` is already false, but reproducing the
-- expression keeps us honest if that ever stops being true.
function BenillaPlayerFrame_UpdatePartyLeader(frame)
if IsPartyLeader() then
frame.leaderIcon:Show()
else
frame.leaderIcon:Hide()
end
local lootMethod, masterId = GetLootMethod()
if masterId == 0 and (GetNumPartyMembers() > 0 or GetNumRaidMembers() > 0) then
frame.masterIcon:Show()
else
frame.masterIcon: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="PlayerPortrait">
<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="$parentManaBar" 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="PlayerFrameTexture" 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).
The NAME is the reference's own, literally — and so is every other
art/text/icon region in the three unit frames. They used to derive
from `$parent`, which published `PlayerFrameTextureFrameNameText` and
left `PlayerName` (PlayerFrame.xml:58), `TargetName`
(TargetFrame.xml:152) and `PetName` (PetFrame.xml:69) — the names an
addon actually reaches for — nil. See BenillaUnitFrame_OnLoad's
`short` prefix for how the shared binder composes them now.
**This was tried once and REJECTED, and the rejection was right on
its evidence** — the block that stood here argued: (i) not free, since
`UnitFrame_OnLoad` composes these names inside `getglobal`, so a text
search finds nothing and the rename breaks every unit frame (it broke
40 tests); and (ii) buys nothing, since a "why" read-back over the
218-addon corpus matched ZERO error rows for `TargetName`, its one
syntactic caller being a `<Anchor relativeTo=>` in an addon that dies
earlier anyway. It closed "revisit if a row ever names it" (1222's
clause: a shadowed name with no observable caller is building against
a guess).
What changed (1719): the condition it named was met, and its cost
premise expired. A grep of the top-20 root gives `ManaBarColor` 8
readers, `PlayerFrameManaBar` 3, `PlayerFrameTexture`/
`TargetFrameTexture`/`PlayerStatusTexture`/`TargetFrameManaBar` 2
each, and `PlayerName`/`TargetName`/`PetName` 1 each. ShaguTweaks
dies on `TargetFrameManaBar` at `health-numbers.lua:22`. **Fifteen of
the twenty-two renames still have zero measured readers** and were
kept for consistency with the reference and because the cost was
already paid, NOT because demand was shown — say the word and they go
back. -->
<FontString name="PlayerName" 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="PlayerLevelText" 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="PlayerFrameHealthBarText" inherits="TextStatusBarText">
<Anchors>
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="50" y="3"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="PlayerFrameManaBarText" 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="PlayerPVPIcon" 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="PlayerStatusTexture" 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="PlayerAttackBackground" 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="PlayerHitIndicator" hidden="true" inherits="NumberFontNormalHuge">
<Anchors>
<Anchor point="CENTER" relativePoint="TOPLEFT"><Offset><AbsDimension x="73" y="-42"/></Offset></Anchor>
</Anchors>
</FontString>
<!-- Leader + master-looter icons (ref PlayerLeaderIcon / PlayerMasterIcon,
ref-PlayerFrame.xml l.147-170): 16×16 at the frame's TOPLEFT (44,-10)
and (80,-10) — above the portrait ring, left to right. The reference
seats them in this same OVERLAY layer and in the same frame-filling
child the state icons use, so the offsets are directly comparable to
$parentRestIcon's below. Shown by BenillaPlayerFrame_UpdatePartyLeader.
Report B317: the member frames have carried these since 0434 phase 2;
the player frame was built before the party wire and listed them OUT. -->
<Texture name="PlayerLeaderIcon" file="Interface\GroupFrame\UI-Group-LeaderIcon" hidden="true">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="44" y="-10"/></Offset></Anchor>
</Anchors>
</Texture>
<Texture name="PlayerMasterIcon" file="Interface\GroupFrame\UI-Group-MasterLooter" hidden="true">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="80" y="-10"/></Offset></Anchor>
</Anchors>
</Texture>
<!-- 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="PlayerRestIcon" 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="PlayerAttackIcon" file="Interface\CharacterFrame\UI-StateIcon" hidden="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="PlayerRestIcon"><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="PlayerStatusGlow" hidden="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="PlayerRestIcon"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<Texture name="PlayerRestGlow" 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="PlayerAttackGlow" 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="TargetPortrait">
<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="$parentManaBar" 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="TargetFrameTexture" 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="TargetPVPIcon" 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="TargetName" 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="TargetLevelText" 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="TargetHighLevelTexture" 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), spelled
the reference's own way: `text="DEAD"` is a GLOBAL-STRING LOOKUP, so
the word rendered is GlobalStrings.lua l.898's `DEAD = "Dead"` — the
key never reaches the screen. `hidden` like the ToT twin below, a
deliberate divergence: the reference leaves the region shown and
relies on CheckDead landing before the frame does. -->
<FontString name="TargetDeadText" inherits="GameFontNormalSmall" text="DEAD" hidden="true">
<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="TargetFrameHealthBarText" inherits="TextStatusBarText">
<Anchors>
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="-50" y="3"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="TargetFrameManaBarText" 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>
<!-- The target-of-target reconcile (decision 1576, ref TargetFrame_OnUpdate): one
boolean compare, and only while a target exists. See its note below. -->
<OnUpdate><![CDATA[TargetFrame_OnUpdate()]]></OnUpdate>
<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 TARGET-OF-TARGET FRAME (decision 1576). The 93×45 satellite at the target frame's
bottom-right: who the unit you have selected is fighting. Geometry, art and law are
MEASURED off the reference (ref-TargetFrame.xml l.515-690, ref-TargetFrame.lua l.494-585);
the code is our own, on this file's snapshot → Unit* → event data flow.
THE TOKEN IS THE SLICE. `"targettarget"` is 1.12's own token grammar and our resolver
already RECOGNISED it (the `target` prefix — benilla-ui unit/mod.rs); it simply never named
a unit, because nothing fed one. The app now does: the selected unit's own
`UNIT_FIELD_TARGET`, one hop through the guid→entity index (ui_unit.rs), with the aura list
(ui_aura.rs) and the portrait booth (portrait/mod.rs) following the same hop. Everything
below is then ordinary FrameXML over an ordinary unit token.
DRIVEN BY EVENTS, NOT BY A POLL. The reference calls `TargetofTarget_Update()` from this
frame's own OnUpdate, every frame, unconditionally — it has no other way to notice, since
its engine fires no UNIT_* event for this token. Ours fires them (the feed diffs the
snapshot and fires per field, exactly as it does for `"target"`), so the repaint rides the
events. What events cannot carry is a token going AWAY — the feed clears it silently, by
the same convention `"target"` uses — and that is precisely what the reference's own
reconcile line covers (`TargetFrame_OnUpdate`, ref l.256-260): one boolean compare per
frame, on a frame that only ticks while you have a target. That line we keep.
DEVIATIONS, noted:
- No hover tooltip and no right-click menu. The reference gives this frame neither — its
Scripts are OnLoad/OnEvent/OnUpdate/OnClick and nothing else — so neither is built here.
The four debuff buttons do carry the reference's own aura tooltip.
- The five display modes are a PREDICATE, not the reference's ladder. ref l.505-528 tests
them as an if/elseif chain in which the solo leg (state "3") calls neither Show nor Hide
when you are in a raid: the frame keeps whatever state it had, which is a leaked state,
not a rule. `TargetofTarget_ModeAllows` answers the question the five modes ask.
- A powerless unit hides its mana bar, this file's convention on every frame; the
reference leaves an empty 0/0 rail drawn.
- `TargetofTargetHealthCheck` guards its own divide. The reference divides by
`UnitManaMax`'s sibling `unitHPMax` with no zero test.
-->
<!-- The debuff button — ref TargetofTargetBuffButtonTemplate (TargetFrame.xml l.4-27): the
party template shrunk to 12×12 with a 13×13 border, which is why the reference re-sets
both in an OnLoad rather than authoring them. We author them. No stack-count region: the
reference's party template has none, and at 12 px a numeral is not readable anyway. -->
<Button name="BenillaTargetofTargetDebuffTemplate" virtual="true" hidden="true">
<Size><AbsDimension x="12" y="12"/></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="13" y="13"/></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>
</Layer>
</Layers>
<Scripts>
<OnEnter><![CDATA[
if GameTooltip then
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetUnitDebuff("targettarget", this:GetID())
end
]]></OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Button>
<Script><![CDATA[
-- The two saved settings (ref UIOptionsFrame.lua l.116-119), with the reference's own
-- defaults: the frame ships OFF, and its mode ships at "always". They are saved-variable
-- globals rather than CVars because that is what they are in 1.12 — the Interface panel's
-- `uvar` store (OptionsFrame.xml's three-store note), and RegisterForSave is what persists
-- them into benilla-config (saved.rs).
SHOW_TARGET_OF_TARGET = "0"
RegisterForSave("SHOW_TARGET_OF_TARGET")
SHOW_TARGET_OF_TARGET_STATE = "5"
RegisterForSave("SHOW_TARGET_OF_TARGET_STATE")
-- Does the group state allow the frame right now? The five modes, in the reference's own
-- numbering (its dropdown writes these strings, and OPTION_TOOLTIP_TARGETOFTARGET1..5
-- describes them in this order): 1 raid, 2 party, 3 solo, 4 party or raid, 5 always.
-- Written as a predicate rather than the reference's if/elseif ladder — see the DEVIATIONS
-- note above for the leaked state that ladder has in its solo leg. LOCAL: the reference has
-- no such function, and inventing a global for it would put a name in `_G` that no addon
-- written against 1.12 can expect (the `Benilla*` prefix is for the ones we must expose).
local function TargetofTarget_ModeAllows()
local mode = SHOW_TARGET_OF_TARGET_STATE
local raid = GetNumRaidMembers()
local party = GetNumPartyMembers()
if mode == "1" then return raid > 0 end
if mode == "2" then return party > 0 and raid == 0 end
if mode == "3" then return raid == 0 and party == 0 end
if mode == "4" then return raid > 0 or party > 0 end
if mode == "5" then return true end
return false
end
-- The visibility law + the repaint (ref TargetofTarget_Update, l.503-541). Four gates
-- besides the switch and the mode, all the reference's: there must BE a target, it must
-- have a target, it must not be YOU (your own target's target is your own target — the
-- frame would restate the frame it hangs off), and it must be alive.
function TargetofTarget_Update()
local frame = TargetofTargetFrame
local was = frame:IsShown()
local show = SHOW_TARGET_OF_TARGET == "1"
and UnitExists("target")
and UnitExists("targettarget")
and not UnitIsUnit("player", "target")
and UnitHealth("target") > 0
and TargetofTarget_ModeAllows()
if show then
frame:Show()
BenillaUnitFrame_PaintBars(frame.healthbar, frame.manabar, "targettarget")
TargetofTargetName:SetText(UnitName("targettarget") or "")
TargetofTarget_CheckDead()
-- The reference's own line (l.536), and it is not redundant with the health
-- check's tint: that one only ever touches a PLAYER's portrait, so a creature
-- inherits whatever alpha the last player left behind without this.
TargetofTargetPortrait:SetAlpha(1.0)
BenillaTargetofTarget_UpdateDebuffs()
else
frame:Hide()
end
-- The target's OWN aura rows wrap at 5 instead of 6 while this frame stands beside
-- them (BenillaTargetAuras_Update), so a change of shown state re-lays them out. The
-- reference re-runs them on the SHOW leg only (l.534) and never on the hide, which
-- leaves the rows wrapped short after the frame goes away.
if frame:IsShown() ~= was and UnitExists("target") then
BenillaTargetAuras_Update()
end
end
-- ref TargetofTarget_CheckDead (l.559-567): the DEAD word over a dimmed trough. The
-- connected test is the reference's and it is what tells "dead" from "linkdead" —
-- a disconnected player reads 0 health here and is not dead.
function TargetofTarget_CheckDead()
local down = UnitHealth("targettarget") <= 0 and UnitIsConnected("targettarget")
TargetofTargetBackground:SetAlpha(down and 0.9 or 1)
if down then
TargetofTargetDeadText:Show()
else
TargetofTargetDeadText:Hide()
end
end
-- The four readings a PLAYER's portrait can carry here (ref TargetofTargetHealthCheck's
-- own four SetVertexColor calls, l.573-583): a corpse greys, a released ghost goes blue,
-- a fifth of health or less runs red, and anything else is untinted.
local TOT_PORTRAIT_TINT = {
dead = { r = 0.35, g = 0.35, b = 0.35 },
ghost = { r = 0.2, g = 0.2, b = 0.75 },
hurt = { r = 1.0, g = 0.0, b = 0.0 },
alive = { r = 1.0, g = 1.0, b = 1.0 },
}
-- ref TargetofTargetHealthCheck (l.569-585), the health bar's OnValueChanged: the
-- portrait tints with the unit's state — grey dead, blue ghost, red under a fifth of its
-- health, white otherwise. PLAYERS ONLY, which is the reference's gate and not an
-- oversight: a creature's portrait carries no such reading anywhere in 1.12.
function TargetofTargetHealthCheck()
if not UnitIsPlayer("targettarget") then return end
local bar = this
local _, maxHP = bar:GetMinMaxValues()
local percent = 0
if maxHP > 0 then
percent = bar:GetValue() / maxHP
end
bar:GetParent().unitHPPercent = percent
-- The reading, then one paint. Priority is the reference's ladder: dead outranks
-- ghost outranks hurt, and anything else is untinted.
local tint = TOT_PORTRAIT_TINT.alive
if UnitIsDead("targettarget") then
tint = TOT_PORTRAIT_TINT.dead
elseif UnitIsGhost("targettarget") then
tint = TOT_PORTRAIT_TINT.ghost
elseif percent > 0 and percent <= 0.2 then
tint = TOT_PORTRAIT_TINT.hurt
end
TargetofTargetPortrait:SetVertexColor(tint.r, tint.g, tint.b, 1.0)
end
-- The four debuff buttons (ref calls RefreshBuffs(this, 0, "targettarget"), BuffFrame.lua
-- l.262-312, over MAX_PARTY_DEBUFFS = 4). Same shape as the pet frame's row, plus the
-- dispel tint the party/pet rows do not carry — RefreshBuffs colours its border by
-- debuffType exactly as the target frame's row does.
function BenillaTargetofTarget_UpdateDebuffs()
for i = 1, 4 do
local button = getglobal("TargetofTargetFrameDebuff" .. i)
local _, icon, _, debuffType = UnitDebuff("targettarget", i)
if icon then
getglobal(button:GetName() .. "Icon"):SetTexture(icon)
local color = DebuffTypeColor[debuffType] or DebuffTypeColor["none"]
getglobal(button:GetName() .. "Border"):SetVertexColor(color.r, color.g, color.b)
button:Show()
else
button:Hide()
end
end
end
function TargetofTarget_OnLoad()
local frame = this
frame.unit = "targettarget"
frame.healthbar = TargetofTargetHealthBar
frame.manabar = TargetofTargetManaBar
-- The recessed-bar look, the same mechanism every frame in this file uses (decision
-- 0884): 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. The reference
-- spends its level the other way round (RaiseFrameLevel on the TextureFrame); same
-- ordering, and ours matches its siblings here.
local level = TargetofTargetTextureFrame:GetFrameLevel()
frame.healthbar:SetFrameLevel(level - 1)
frame.manabar:SetFrameLevel(level - 1)
-- The live model portrait, bound once by token (see BenillaUnitFrame_OnLoad's note):
-- the app keeps this slot's bake current, and it only bakes at all while this frame is
-- on screen (portrait/mod.rs's `"targettarget"` arm).
SetPortraitTexture(TargetofTargetPortrait, "targettarget")
-- Every event the shown frame's paint reads. The `"target"` half is not incidental:
-- the visibility law gates on the TARGET being alive, so the target's own health has
-- to reach this frame too.
for _, wire in ipairs({
"PLAYER_ENTERING_WORLD", "PLAYER_TARGET_CHANGED", "PARTY_MEMBERS_CHANGED",
"RAID_ROSTER_UPDATE", "UNIT_HEALTH", "UNIT_MAXHEALTH", "UNIT_POWER_UPDATE",
"UNIT_MAXPOWER", "UNIT_DISPLAYPOWER", "UNIT_NAME_UPDATE", "UNIT_AURA",
}) do
frame:RegisterEvent(wire)
end
frame:EnableMouse(true)
TargetofTarget_Update()
end
-- Every UNIT_* event this frame registers names a unit in arg1, and exactly two of them
-- concern it: its own, and the target whose health gates its visibility. Everything else
-- (a party member's tick, the player's own) is somebody else's update.
function TargetofTarget_OnEvent(event)
if string.sub(event, 1, 5) == "UNIT_"
and arg1 ~= "targettarget" and arg1 ~= "target" then
return
end
TargetofTarget_Update()
end
-- ref TargetofTarget_OnClick (l.543-557).
function TargetofTarget_OnClick(button)
local armed = SpellIsTargeting()
if button ~= "LeftButton" then
-- Right-click cancels an armed spell and does nothing else: unlike the player,
-- target and pet frames, this one has no unit menu in the reference.
if armed and button == "RightButton" then SpellStopTargeting() end
return
end
-- Most specific first, the pet frame's order: an armed spell binds to the unit, then
-- a held item drops on it, and with an empty cursor the click SELECTS it — which is
-- the whole point of the frame, and the one leg the target frame's click does not have.
if armed then
SpellTargetUnit("targettarget")
return
end
if CursorHasItem() then
DropItemOnUnit("targettarget")
return
end
TargetUnit("targettarget")
end
-- ref TargetFrame_OnUpdate (l.256-260) — the one thing the event feed cannot carry. A
-- token that goes away is CLEARED, not announced (ui_unit.rs: "clearing a token isn't a
-- UNIT_* event"), so this reconciles the frame's shown state against the token's existence
-- once per frame. It runs only while the target frame itself is shown, which is only while
-- you have a target — the cost is one compare, and it is the reference's own line.
-- Both sides are normalised to booleans on purpose. They happen to agree today (a frame's
-- IsShown answers true/false, and so does UnitExists), but a REGION's IsShown answers the
-- 1.12 shape of 1/nil, and the day UnitExists is moved onto it a bare `~=` here would be
-- true forever — a silent per-frame full repaint, which is the one thing this line exists
-- to avoid.
function TargetFrame_OnUpdate()
local shown = TargetofTargetFrame:IsShown() and true or false
local exists = UnitExists("targettarget") and true or false
if shown ~= exists then
TargetofTarget_Update()
end
end
]]></Script>
<!-- 93×45 at the target frame's BOTTOMRIGHT (-35,-10) — ref TargetFrame.xml l.515-525. A
Frame, not the reference's Button: every unit frame in this file is one, and this engine
dispatches OnClick on a mouse-enabled frame (see TargetFrame/PetFrame above). -->
<Frame name="TargetofTargetFrame" parent="TargetFrame" hidden="true" frameStrata="LOW">
<Size><AbsDimension x="93" y="45"/></Size>
<Anchors>
<Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-35" y="-10"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<!-- The bars' black trough, 46×15 BOTTOMLEFT (42,13) (ref l.528-541). Its alpha is
what CheckDead dims. -->
<Texture name="TargetofTargetBackground">
<Size><AbsDimension x="46" y="15"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="42" y="13"/></Offset></Anchor>
</Anchors>
<Color r="0" g="0" b="0" a="0.5"/>
</Texture>
<!-- Portrait 35×35 TOPLEFT (6,-6) (ref l.543-556, BORDER→BACKGROUND for the same
ring-over-portrait layering the player and target frames use). -->
<Texture name="TargetofTargetPortrait">
<Size><AbsDimension x="35" y="35"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="6" y="-6"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<!-- Health bar 46×7 TOPRIGHT (-2,-15), UI-StatusBar, green (ref l.593-611). -->
<StatusBar name="TargetofTargetHealthBar">
<Size><AbsDimension x="46" y="7"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-2" y="-15"/></Offset></Anchor>
</Anchors>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="0.0" g="1.0" b="0.0"/>
<Scripts>
<OnValueChanged>TargetofTargetHealthCheck()</OnValueChanged>
</Scripts>
</StatusBar>
<!-- Power bar 46×7 TOPRIGHT (-2,-23); colour set at runtime (ref l.612-625). -->
<StatusBar name="TargetofTargetManaBar">
<Size><AbsDimension x="46" y="7"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-2" y="-23"/></Offset></Anchor>
</Anchors>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="0.0" g="0.0" b="1.0"/>
</StatusBar>
<!-- The plate art + text, over the bars (ref TargetofTargetTextureFrame l.558-591) —
it draws over the bar edges because OnLoad pushes the bars one frame level below
it, exactly as on the player and target frames. -->
<Frame name="TargetofTargetTextureFrame">
<Size><AbsDimension x="93" y="45"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<!-- The small plate, its own art file. The TexCoords are the reference's
(l.561-563) and they cut the 128×64 file to exactly this frame's
93×45: 0.015625..0.7265625 is x 2..93, 0..0.703125 is y 0..45. -->
<Texture name="TargetofTargetTexture" file="Interface\TargetingFrame\UI-TargetofTargetFrame">
<Size><AbsDimension x="93" y="45"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<TexCoords left="0.015625" right="0.7265625" top="0" bottom="0.703125"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<!-- Name 100×10 BOTTOMLEFT (42,2) (ref l.564-576). -->
<FontString name="TargetofTargetName" inherits="GameFontNormalSmall" justifyH="LEFT">
<Size><AbsDimension x="100" y="10"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="42" y="2"/></Offset></Anchor>
</Anchors>
</FontString>
<!-- DEAD, over the trough, CENTER (15,1) (ref l.576-587). -->
<FontString name="TargetofTargetDeadText" inherits="GameFontNormalSmall" text="DEAD" hidden="true">
<Anchors>
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="15" y="1"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
<!-- The four debuffs, ref geometry (l.626-663): the first hangs off the frame's
TOPRIGHT, then a 2×2 block. -->
<Button name="$parentDebuff1" inherits="BenillaTargetofTargetDebuffTemplate" id="1">
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="4" y="-10"/></Offset></Anchor>
</Anchors>
</Button>
<Button name="$parentDebuff2" inherits="BenillaTargetofTargetDebuffTemplate" id="2">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetofTargetFrameDebuff1" relativePoint="RIGHT"><Offset><AbsDimension x="1" y="0"/></Offset></Anchor>
</Anchors>
</Button>
<Button name="$parentDebuff3" inherits="BenillaTargetofTargetDebuffTemplate" id="3">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="TargetofTargetFrameDebuff1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor>
</Anchors>
</Button>
<Button name="$parentDebuff4" inherits="BenillaTargetofTargetDebuffTemplate" id="4">
<Anchors>
<Anchor point="LEFT" relativeTo="TargetofTargetFrameDebuff3" relativePoint="RIGHT"><Offset><AbsDimension x="1" y="0"/></Offset></Anchor>
</Anchors>
</Button>
</Frames>
<Scripts>
<OnLoad><![CDATA[TargetofTarget_OnLoad()]]></OnLoad>
<OnEvent><![CDATA[TargetofTarget_OnEvent(event)]]></OnEvent>
<OnClick><![CDATA[TargetofTarget_OnClick(arg1)]]></OnClick>
</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 = PetPortrait
frame.healthbar = PetFrameHealthBar
frame.manabar = PetFrameManaBar
frame.nameText = PetName
-- 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, PetFrameHealthBarText)
SetTextStatusBarText(frame.manabar, PetFrameManaBarText)
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
PetAttackModeTexture:SetVertexColor(1.0, 1.0, 1.0, 1.0)
PetAttackModeTexture:Show()
return
elseif event == "PET_ATTACK_STOP" then
PetAttackModeTexture: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 "")
-- The ref's art swap (PetFrame.lua's Update): a powerless pet wears the -NoMana plate,
-- which has no mana rail to leave empty. Which plate is exactly the shared paint's
-- return — the pet is the only frame here whose art carries that distinction.
if BenillaUnitFrame_PaintBars(frame.healthbar, frame.manabar, "pet") then
PetFrameTexture:SetTexture("Interface\\TargetingFrame\\UI-SmallTargetingFrame")
else
PetFrameTexture:SetTexture("Interface\\TargetingFrame\\UI-SmallTargetingFrame-NoMana")
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 PetAttackModeTexture: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
PetAttackModeTexture: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="PetPortrait">
<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="PetAttackModeTexture" 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="PetName" 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="PetFrameHealthBarText" inherits="TextStatusBarText">
<Anchors><Anchor point="CENTER" relativePoint="TOPLEFT"><Offset><AbsDimension x="82" y="-27"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="PetFrameManaBarText" 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>